Merge remote-tracking branch 'upstream/main' into panchen/dev_hide
|
|
@ -2,9 +2,16 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(tree:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm --version:*)",
|
||||
"Bash(test:*)"
|
||||
"Bash(pip show:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(test:*)",
|
||||
"Bash(lsof:*)",
|
||||
"WebSearch",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
741
.github/workflows/docker-build.yaml
vendored
|
|
@ -1,11 +1,10 @@
|
|||
name: Create and publish Docker images with specific build args
|
||||
name: Build and Push Docker Image (Production)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
tags:
|
||||
- v*
|
||||
|
||||
|
|
@ -13,429 +12,14 @@ env:
|
|||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build-main-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (latest)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda126-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda126 tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda126-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda126)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
USE_CUDA_VER=cu126
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda126-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-ollama-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-ollama-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (ollama)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_OLLAMA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-slim-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name: Build Slim Image (amd64)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
|
|
@ -443,28 +27,20 @@ jobs:
|
|||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (slim tag)
|
||||
- name: Extract metadata for Docker images
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
|
|
@ -489,312 +65,37 @@ jobs:
|
|||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-slim-${{ matrix.platform }}-
|
||||
prefix=cache-slim-linux-amd64-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (slim)
|
||||
- name: Build and push Docker image (slim)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_SLIM=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-slim-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge-main-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-main-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-main-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
- name: Output image tags
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda126-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda126-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda126-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-ollama-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-ollama-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-ollama-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-slim-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-slim-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-slim-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default slim tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-slim,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
echo "## 🐳 Docker 镜像构建成功" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**镜像标签:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**拉取命令:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ env.FULL_IMAGE_NAME }}:slim" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
|
|
|||
800
.github/workflows_bak/docker-build.yaml
vendored
Normal file
|
|
@ -0,0 +1,800 @@
|
|||
name: Create and publish Docker images with specific build args
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build-main-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (latest)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-main-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-cuda126-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (cuda126 tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-cuda126-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (cuda126)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_CUDA=true
|
||||
USE_CUDA_VER=cu126
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-cuda126-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-ollama-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-ollama-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (ollama)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_OLLAMA=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
build-slim-image:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (slim tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-slim,onlatest=true
|
||||
|
||||
- name: Extract metadata for Docker cache
|
||||
id: cache-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
||||
flavor: |
|
||||
prefix=cache-slim-${{ matrix.platform }}-
|
||||
latest=false
|
||||
|
||||
- name: Build Docker image (slim)
|
||||
uses: docker/build-push-action@v5
|
||||
id: build
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
||||
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
||||
build-args: |
|
||||
BUILD_HASH=${{ github.sha }}
|
||||
USE_SLIM=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-slim-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge-main-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-main-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-main-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-cuda126-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-cuda126-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-cuda126-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default latest tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-cuda126,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-ollama-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-ollama-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-ollama-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default ollama tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-ollama,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
merge-slim-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-slim-image]
|
||||
steps:
|
||||
# GitHub Packages requires the entire repository name to be in lowercase
|
||||
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
||||
- name: Set repository and image name to lowercase
|
||||
run: |
|
||||
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
IMAGE_NAME: '${{ github.repository }}'
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: digests-slim-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker images (default slim tag)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.FULL_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=git-
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
|
||||
flavor: |
|
||||
latest=${{ github.ref == 'refs/heads/main' }}
|
||||
suffix=-slim,onlatest=true
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
1
.gitignore
vendored
|
|
@ -40,6 +40,7 @@ share/python-wheels/
|
|||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
data
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
Open WebUI 是一个功能丰富的自托管 AI 平台,支持完全离线运行。核心技术栈:
|
||||
CyberLover 是一个功能丰富的自托管 AI 平台,支持完全离线运行。核心技术栈:
|
||||
- **前端**: SvelteKit 4 + TypeScript + Vite 5 + Tailwind CSS 4
|
||||
- **后端**: Python 3.11 + FastAPI + SQLAlchemy
|
||||
- **部署**: Docker 多阶段构建,生产环境前后端同容器运行
|
||||
|
|
|
|||
232
Dockerfile
|
|
@ -1,92 +1,45 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Initialize device type args
|
||||
# use build args in the docker build command with --build-arg="BUILDARG=true"
|
||||
ARG USE_CUDA=false
|
||||
ARG USE_OLLAMA=false
|
||||
ARG USE_SLIM=false
|
||||
ARG USE_PERMISSION_HARDENING=false
|
||||
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
||||
ARG USE_CUDA_VER=cu128
|
||||
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
||||
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
||||
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
||||
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
||||
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
ARG USE_RERANKING_MODEL=""
|
||||
|
||||
# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
|
||||
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
|
||||
|
||||
ARG BUILD_HASH=dev-build
|
||||
# Override at your own risk - non-root configurations are untested
|
||||
ARG UID=0
|
||||
ARG GID=0
|
||||
|
||||
######## WebUI frontend ########
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine3.20 AS build
|
||||
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
|
||||
ARG BUILD_HASH
|
||||
|
||||
# ========== 配置 Alpine 镜像源 ==========
|
||||
RUN echo "https://mirrors.aliyun.com/alpine/v3.20/main" > /etc/apk/repositories && \
|
||||
echo "https://mirrors.aliyun.com/alpine/v3.20/community" >> /etc/apk/repositories && \
|
||||
apk update
|
||||
|
||||
# ========== 增加 Node.js 堆内存限制 ==========
|
||||
# Set Node.js options (heap limit Allocation failed - JavaScript heap out of memory)
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
# ========== 配置 npm 镜像源 ==========
|
||||
RUN npm config set registry https://registry.npmmirror.com && \
|
||||
npm config set fetch-timeout 120000 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 20000 && \
|
||||
npm config set fetch-retry-maxtimeout 120000 && \
|
||||
npm config set maxsockets 5
|
||||
|
||||
# ========== 配置二进制包镜像 ==========
|
||||
ENV ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
|
||||
SASS_BINARY_SITE=https://npmmirror.com/mirrors/node-sass/ \
|
||||
PHANTOMJS_CDNURL=https://nppmirror.com/mirrors/phantomjs/ \
|
||||
CHROMEDRIVER_CDNURL=https://npmmirror.com/mirrors/chromedriver/ \
|
||||
OPERADRIVER_CDNURL=https://npmmirror.com/mirrors/operadriver/ \
|
||||
PYTHON_MIRROR=https://npmmirror.com/mirrors/python/
|
||||
|
||||
# ========== 配置代理(可选)==========
|
||||
ARG HTTP_PROXY
|
||||
ARG HTTPS_PROXY
|
||||
ENV HTTP_PROXY=${HTTP_PROXY}
|
||||
ENV HTTPS_PROXY=${HTTPS_PROXY}
|
||||
ENV NO_PROXY=localhost,127.0.0.1,mirrors.aliyun.com,registry.nppmirror.com,nppmirror.com
|
||||
|
||||
# ========== 安装必要工具 ==========
|
||||
RUN apk add --no-cache git python3 make g++ && \
|
||||
if [ -n "$HTTP_PROXY" ]; then \
|
||||
git config --global http.proxy ${HTTP_PROXY} && \
|
||||
git config --global https.proxy ${HTTPS_PROXY} && \
|
||||
git config --global http.sslVerify false; \
|
||||
fi
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ========== 安装依赖(不使用 --ignore-scripts)==========
|
||||
# to store git revision in build
|
||||
RUN apk add --no-cache git
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --force
|
||||
|
||||
RUN echo "==================================" && \
|
||||
echo "Starting npm install" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "==================================" && \
|
||||
npm cache clean --force && \
|
||||
npm install --legacy-peer-deps --no-audit --no-fund || \
|
||||
(echo "First attempt failed, retrying..." && \
|
||||
rm -rf node_modules package-lock.json && \
|
||||
npm install --legacy-peer-deps --no-audit --no-fund) && \
|
||||
echo "==================================" && \
|
||||
echo "npm install completed" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "=================================="
|
||||
|
||||
# ========== 构建前端 ==========
|
||||
COPY . .
|
||||
ENV APP_BUILD_HASH=${BUILD_HASH}
|
||||
|
||||
RUN echo "==================================" && \
|
||||
echo "Starting frontend build" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "==================================" && \
|
||||
npm run build && \
|
||||
echo "==================================" && \
|
||||
echo "Build completed" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "=================================="
|
||||
RUN npm run build
|
||||
|
||||
######## WebUI backend ########
|
||||
FROM python:3.11-slim-bookworm AS base
|
||||
|
|
@ -105,6 +58,7 @@ ARG GID
|
|||
## Basis ##
|
||||
ENV ENV=prod \
|
||||
PORT=8080 \
|
||||
# pass build args to the build
|
||||
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
||||
USE_CUDA_DOCKER=${USE_CUDA} \
|
||||
USE_SLIM_DOCKER=${USE_SLIM} \
|
||||
|
|
@ -124,23 +78,31 @@ ENV OPENAI_API_KEY="" \
|
|||
ANONYMIZED_TELEMETRY=false
|
||||
|
||||
#### Other models #########################################################
|
||||
## whisper TTS model settings ##
|
||||
ENV WHISPER_MODEL="base" \
|
||||
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" \
|
||||
RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
||||
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" \
|
||||
TIKTOKEN_ENCODING_NAME="cl100k_base" \
|
||||
TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken" \
|
||||
HF_HOME="/app/backend/data/cache/embedding/models"
|
||||
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
||||
|
||||
# ========== 配置 Hugging Face 镜像 ==========
|
||||
ENV HF_ENDPOINT=https://hf-mirror.com
|
||||
## RAG Embedding model settings ##
|
||||
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
||||
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
## Tiktoken model settings ##
|
||||
ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
|
||||
TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
|
||||
|
||||
## Hugging Face download cache ##
|
||||
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
## Torch Extensions ##
|
||||
# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
|
||||
|
||||
#### Other models ##########################################################
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
ENV HOME=/root
|
||||
|
||||
# ========== 创建用户和组 ==========
|
||||
# Create user and group if not root
|
||||
RUN if [ $UID -ne 0 ]; then \
|
||||
if [ $GID -ne 0 ]; then \
|
||||
addgroup --gid $GID app; \
|
||||
|
|
@ -148,15 +110,13 @@ RUN if [ $UID -ne 0 ]; then \
|
|||
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
||||
fi
|
||||
|
||||
RUN mkdir -p $HOME/.cache/chroma && \
|
||||
echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id && \
|
||||
chown -R $UID:$GID /app $HOME
|
||||
RUN mkdir -p $HOME/.cache/chroma
|
||||
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
||||
|
||||
# ========== 配置 Debian 镜像源 ==========
|
||||
RUN sed -i 's@deb.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources && \
|
||||
sed -i 's@security.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources
|
||||
# Make sure the user has access to the app and root directory
|
||||
RUN chown -R $UID:$GID /app $HOME
|
||||
|
||||
# ========== 安装系统依赖 ==========
|
||||
# Install common system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
git build-essential pandoc gcc netcat-openbsd curl jq \
|
||||
|
|
@ -164,100 +124,56 @@ RUN apt-get update && \
|
|||
ffmpeg libsm6 libxext6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ========== 配置 pip 镜像源 ==========
|
||||
RUN pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||
pip3 config set install.trusted-host mirrors.aliyun.com && \
|
||||
pip3 config set global.timeout 600
|
||||
|
||||
# ========== 配置 uv 使用镜像源 ==========
|
||||
ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \
|
||||
UV_EXTRA_INDEX_URL="" \
|
||||
UV_NO_CACHE=0
|
||||
|
||||
# ========== 安装 Python 依赖 ==========
|
||||
# install python dependencies
|
||||
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
|
||||
|
||||
RUN echo "==================================" && \
|
||||
echo "Installing Python dependencies" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "==================================" && \
|
||||
pip3 install uv && \
|
||||
RUN pip3 install --no-cache-dir uv && \
|
||||
if [ "$USE_CUDA" = "true" ]; then \
|
||||
echo "Installing PyTorch with CUDA support..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://mirrors.aliyun.com/pypi/simple/ \
|
||||
--trusted-host mirrors.aliyun.com || \
|
||||
(echo "Aliyun failed, trying Tsinghua mirror..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||
--trusted-host pypi.tuna.tsinghua.edu.cn) || \
|
||||
(echo "Mirrors failed, trying official PyTorch repo..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER) && \
|
||||
echo "Installing other requirements with uv..." && \
|
||||
uv pip install --system -r requirements.txt \
|
||||
--index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||
if [ "$USE_SLIM" != "true" ]; then \
|
||||
echo "Downloading models..." && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
fi; \
|
||||
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
else \
|
||||
echo "Installing PyTorch CPU version..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://mirrors.aliyun.com/pypi/simple/ \
|
||||
--trusted-host mirrors.aliyun.com || \
|
||||
(echo "Aliyun failed, trying Tsinghua mirror..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||
--trusted-host pypi.tuna.tsinghua.edu.cn) || \
|
||||
(echo "Tsinghua failed, trying USTC mirror..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://mirrors.ustc.edu.cn/pypi/web/simple/ \
|
||||
--trusted-host mirrors.ustc.edu.cn) || \
|
||||
(echo "All mirrors failed, trying official PyTorch CPU repo..." && \
|
||||
pip3 install torch torchvision torchaudio \
|
||||
--index-url https://download.pytorch.org/whl/cpu) && \
|
||||
echo "Installing other requirements with uv..." && \
|
||||
uv pip install --system -r requirements.txt \
|
||||
--index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||
if [ "$USE_SLIM" != "true" ]; then \
|
||||
echo "Downloading models..." && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
fi; \
|
||||
fi && \
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
if [ "$USE_SLIM" != "true" ]; then \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
fi; \
|
||||
fi; \
|
||||
mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \
|
||||
echo "==================================" && \
|
||||
echo "Python dependencies installed" && \
|
||||
echo "Time: $(date)" && \
|
||||
echo "=================================="
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# ========== 安装 Ollama ==========
|
||||
# Install Ollama if requested
|
||||
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
date +%s > /tmp/ollama_build_hash && \
|
||||
echo "Installing Ollama..." && \
|
||||
export HF_ENDPOINT=https://hf-mirror.com && \
|
||||
curl -fsSL https://ollama.com/install.sh | sh || \
|
||||
(echo "Ollama installation failed, trying with proxy..." && \
|
||||
export http_proxy=http://host.docker.internal:7897 && \
|
||||
export https_proxy=http://host.docker.internal:7897 && \
|
||||
curl -fsSL https://ollama.com/install.sh | sh); \
|
||||
echo "Cache broken at timestamp: `cat /tmp/ollama_build_hash`" && \
|
||||
curl -fsSL https://ollama.com/install.sh | sh && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# ========== 复制构建文件 ==========
|
||||
# copy embedding weight from build
|
||||
# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
||||
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
||||
|
||||
# copy built frontend files
|
||||
COPY --chown=$UID:$GID --from=build /app/build /app/build
|
||||
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
||||
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
|
||||
|
||||
# copy backend files
|
||||
COPY --chown=$UID:$GID ./backend .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
|
||||
|
||||
# ========== 权限加固 ==========
|
||||
# Minimal, atomic permission hardening for OpenShift (arbitrary UID):
|
||||
# - Group 0 owns /app and /root
|
||||
# - Directories are group-writable and have SGID so new files inherit GID 0
|
||||
RUN if [ "$USE_PERMISSION_HARDENING" = "true" ]; then \
|
||||
set -eux; \
|
||||
chgrp -R 0 /app /root || true; \
|
||||
|
|
@ -272,4 +188,4 @@ ARG BUILD_HASH
|
|||
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
||||
ENV DOCKER=true
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
CMD [ "bash", "start.sh"]
|
||||
|
|
|
|||
168
Jenkinsfile
vendored
|
|
@ -1,168 +0,0 @@
|
|||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
REPO_URL = 'git@github.com:ai-friend-coming/open-webui-next.git'
|
||||
IMAGE_NAME = 'open-webui-custom'
|
||||
IMAGE_TAG = "${BUILD_NUMBER}"
|
||||
OUTPUT_DIR = '/var/docker-images'
|
||||
DOCKER_FILE_PATH = 'Dockerfile'
|
||||
}
|
||||
|
||||
options {
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
timeout(time: 1, unit: 'HOURS')
|
||||
timestamps()
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('准备工作') {
|
||||
steps {
|
||||
script {
|
||||
echo "========================================="
|
||||
echo "开始构建 Build #${BUILD_NUMBER}"
|
||||
echo "仓库: ${REPO_URL}"
|
||||
echo "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo "========================================="
|
||||
|
||||
// 检查Docker是否可用
|
||||
sh 'docker --version'
|
||||
sh 'docker info'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('检出代码') {
|
||||
steps {
|
||||
script {
|
||||
echo "从 ${REPO_URL} 检出代码..."
|
||||
}
|
||||
|
||||
// 使用更简单的checkout方式
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: '*/main']],
|
||||
userRemoteConfigs: [[
|
||||
url: "${REPO_URL}",
|
||||
credentialsId: 'github-ssh' // 改成你实际创建的凭据ID
|
||||
]]
|
||||
])
|
||||
|
||||
script {
|
||||
echo "代码检出完成"
|
||||
sh 'ls -la'
|
||||
sh 'git log --oneline -1 || echo "无法获取git日志"'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('验证 Dockerfile') {
|
||||
steps {
|
||||
script {
|
||||
echo "检查 Dockerfile..."
|
||||
sh """
|
||||
if [ ! -f "${DOCKER_FILE_PATH}" ]; then
|
||||
echo "错误: 找不到 Dockerfile: ${DOCKER_FILE_PATH}"
|
||||
echo "当前目录内容:"
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
echo "Dockerfile 存在"
|
||||
echo "--- Dockerfile 内容 (前20行) ---"
|
||||
head -20 "${DOCKER_FILE_PATH}"
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('创建输出目录') {
|
||||
steps {
|
||||
script {
|
||||
echo "创建输出目录: ${OUTPUT_DIR}"
|
||||
sh """
|
||||
sudo mkdir -p ${OUTPUT_DIR}
|
||||
sudo chmod 777 ${OUTPUT_DIR}
|
||||
ls -ld ${OUTPUT_DIR}
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('构建 Docker 镜像') {
|
||||
steps {
|
||||
script {
|
||||
echo "开始构建镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
sh """
|
||||
docker build \
|
||||
-t ${IMAGE_NAME}:${IMAGE_TAG} \
|
||||
-t ${IMAGE_NAME}:latest \
|
||||
-f ${DOCKER_FILE_PATH} \
|
||||
.
|
||||
|
||||
echo "镜像构建完成"
|
||||
docker images | grep ${IMAGE_NAME} || echo "未找到镜像"
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('导出镜像') {
|
||||
steps {
|
||||
script {
|
||||
echo "导出镜像到 ${OUTPUT_DIR}"
|
||||
sh """
|
||||
echo "导出 ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||
docker save ${IMAGE_NAME}:${IMAGE_TAG} | gzip > ${OUTPUT_DIR}/${IMAGE_NAME}-${IMAGE_TAG}.tar.gz
|
||||
|
||||
echo "导出 ${IMAGE_NAME}:latest..."
|
||||
docker save ${IMAGE_NAME}:latest | gzip > ${OUTPUT_DIR}/${IMAGE_NAME}-latest.tar.gz
|
||||
|
||||
echo "导出完成"
|
||||
ls -lh ${OUTPUT_DIR}/${IMAGE_NAME}*.tar.gz
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('清理旧镜像') {
|
||||
steps {
|
||||
script {
|
||||
echo "清理本地镜像..."
|
||||
sh """
|
||||
docker rmi ${IMAGE_NAME}:${IMAGE_TAG} 2>/dev/null || echo "镜像已删除或不存在"
|
||||
|
||||
# 保留latest标签,方便下次使用
|
||||
# docker rmi ${IMAGE_NAME}:latest 2>/dev/null || true
|
||||
|
||||
# 清理悬空镜像
|
||||
docker image prune -f --filter "dangling=true" || true
|
||||
|
||||
echo "清理完成"
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
script {
|
||||
echo "========================================="
|
||||
echo "✅ 构建成功!"
|
||||
echo "镜像文件位置: ${OUTPUT_DIR}"
|
||||
sh "ls -lh ${OUTPUT_DIR}/${IMAGE_NAME}*.tar.gz || true"
|
||||
echo "========================================="
|
||||
}
|
||||
}
|
||||
failure {
|
||||
echo "❌ 构建失败,请检查上方日志"
|
||||
}
|
||||
always {
|
||||
script {
|
||||
echo "流水线执行完成"
|
||||
// 可选:清理工作空间(注释掉以便调试)
|
||||
// cleanWs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
291
LOCAL_SETUP.md
|
|
@ -1,291 +0,0 @@
|
|||
# Open WebUI 本地开发环境搭建指南
|
||||
|
||||
本指南提供 Open WebUI 项目的本地开发环境配置和运行步骤。
|
||||
|
||||
## 系统要求
|
||||
|
||||
- **Node.js**: v20.19.5 或更高版本
|
||||
- **Python**: 3.12.x (推荐使用 pyenv 管理)
|
||||
- **npm**: 10.8.2 或更高版本
|
||||
- **操作系统**: macOS / Linux / Windows
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 前端
|
||||
- **框架**: SvelteKit 4 + TypeScript
|
||||
- **构建工具**: Vite 5
|
||||
- **样式**: Tailwind CSS 4
|
||||
|
||||
### 后端
|
||||
- **语言**: Python 3.12
|
||||
- **框架**: FastAPI
|
||||
- **数据库**: SQLite (开发环境) / PostgreSQL (生产环境)
|
||||
- **ORM**: SQLAlchemy + Peewee
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 1. 安装 pyenv (如果系统 Python 版本不是 3.12)
|
||||
|
||||
```bash
|
||||
# macOS (使用 Homebrew)
|
||||
brew install pyenv
|
||||
|
||||
# 配置 shell 环境变量 (添加到 ~/.zshrc 或 ~/.bash_profile)
|
||||
export PATH="$HOME/.pyenv/bin:$PATH"
|
||||
eval "$(pyenv init -)"
|
||||
```
|
||||
|
||||
### 2. 安装 Python 3.12
|
||||
|
||||
```bash
|
||||
# 使用 pyenv 安装 Python 3.12
|
||||
pyenv install 3.12
|
||||
|
||||
# 验证安装
|
||||
pyenv versions
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
### 前端依赖
|
||||
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
**注意**: 需要使用 `--legacy-peer-deps` 标志来解决 @tiptap 包的版本冲突问题。
|
||||
|
||||
### 后端依赖
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 创建 Python 3.12 虚拟环境
|
||||
/Users/你的用户名/.pyenv/versions/3.12.12/bin/python3 -m venv venv
|
||||
|
||||
# 或者,如果系统 Python 已是 3.12
|
||||
python3 -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate # macOS/Linux
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# 升级 pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 运行开发服务器
|
||||
|
||||
### 启动后端服务 (端口 8080)
|
||||
|
||||
```bash
|
||||
# 在 backend 目录下,激活虚拟环境后
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python -m uvicorn open_webui.main:app --reload --port 8080 --host 0.0.0.0
|
||||
```
|
||||
|
||||
后端服务将运行在: **http://localhost:8080**
|
||||
|
||||
后端启动时会自动:
|
||||
- 运行数据库迁移 (Alembic)
|
||||
- 初始化 SQLite 数据库
|
||||
- 配置向量数据库 (ChromaDB)
|
||||
|
||||
### 启动前端服务 (端口 5050)
|
||||
|
||||
```bash
|
||||
# 在项目根目录
|
||||
npm run dev:5050
|
||||
```
|
||||
|
||||
前端服务将运行在: **http://localhost:5050**
|
||||
|
||||
首次启动时会自动:
|
||||
- 下载 Pyodide 包 (浏览器内 Python 运行时)
|
||||
- 预加载常用 Python 包 (numpy, pandas, matplotlib 等)
|
||||
|
||||
## 访问应用
|
||||
|
||||
打开浏览器访问: **http://localhost:5050**
|
||||
|
||||
前端会通过 Vite 代理将 API 请求转发到后端 (8080 端口)。
|
||||
|
||||
## 开发工作流
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
open-webui-next/
|
||||
├── src/ # 前端源码
|
||||
│ ├── routes/ # SvelteKit 路由
|
||||
│ ├── lib/
|
||||
│ │ ├── apis/ # API 客户端
|
||||
│ │ ├── components/ # Svelte 组件
|
||||
│ │ ├── stores/ # 全局状态管理
|
||||
│ │ └── i18n/ # 国际化
|
||||
├── backend/ # 后端源码
|
||||
│ ├── open_webui/
|
||||
│ │ ├── main.py # FastAPI 入口
|
||||
│ │ ├── routers/ # API 路由
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ └── migrations/ # 数据库迁移
|
||||
│ ├── requirements.txt
|
||||
│ └── venv/ # Python 虚拟环境
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### 常用开发命令
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
npm run dev # 启动开发服务器 (默认端口 5173)
|
||||
npm run dev:5050 # 启动开发服务器 (端口 5050)
|
||||
npm run build # 构建生产版本
|
||||
npm run lint # 代码检查
|
||||
npm run format # 代码格式化
|
||||
npm run test:frontend # 运行单元测试
|
||||
npm run i18n:parse # 解析并更新翻译文件
|
||||
```
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
# 在 backend 目录下,激活虚拟环境后
|
||||
|
||||
# 启动开发服务器 (自动重载)
|
||||
python -m uvicorn open_webui.main:app --reload --port 8080
|
||||
|
||||
# 代码格式化
|
||||
black .
|
||||
|
||||
# 数据库迁移
|
||||
cd backend
|
||||
alembic revision --autogenerate -m "描述" # 生成迁移脚本
|
||||
alembic upgrade head # 执行迁移
|
||||
```
|
||||
|
||||
### 热重载
|
||||
|
||||
- **前端**: Vite 自动检测文件变化并热重载
|
||||
- **后端**: uvicorn 的 `--reload` 参数自动检测 Python 代码变化并重启
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. npm install 失败
|
||||
|
||||
**问题**: 依赖版本冲突
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### 2. 后端 Python 版本不兼容
|
||||
|
||||
**问题**: `unstructured` 包不支持 Python 3.13+
|
||||
|
||||
**解决方案**: 使用 Python 3.12:
|
||||
```bash
|
||||
pyenv install 3.12
|
||||
cd backend
|
||||
rm -rf venv
|
||||
/Users/你的用户名/.pyenv/versions/3.12.12/bin/python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 端口被占用
|
||||
|
||||
**问题**: 8080 或 5050 端口已被使用
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
lsof -i :8080
|
||||
lsof -i :5050
|
||||
|
||||
# 终止进程
|
||||
kill -9 <PID>
|
||||
|
||||
# 或者使用不同端口
|
||||
# 前端:
|
||||
npm run dev -- --port 3000
|
||||
|
||||
# 后端:
|
||||
python -m uvicorn open_webui.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### 4. 前端 Pyodide 下载慢
|
||||
|
||||
**问题**: 首次启动下载 Pyodide 包较慢
|
||||
|
||||
**解决方案**: 耐心等待,包会缓存在 `node_modules` 中,后续启动会很快。
|
||||
|
||||
### 5. 数据库迁移错误
|
||||
|
||||
**问题**: Alembic 迁移失败
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 删除数据库重新初始化 (仅开发环境)
|
||||
rm backend/data/webui.db
|
||||
python -m uvicorn open_webui.main:app --reload --port 8080
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
后端可通过环境变量配置,创建 `backend/.env` 文件:
|
||||
|
||||
```bash
|
||||
# 数据库
|
||||
DATABASE_URL=sqlite:///data/webui.db
|
||||
|
||||
# 向量数据库
|
||||
VECTOR_DB=chroma
|
||||
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
||||
|
||||
# CORS (开发环境)
|
||||
CORS_ALLOW_ORIGIN=*
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
## 生产部署
|
||||
|
||||
生产环境使用 Docker 部署,详见项目根目录的 `Dockerfile` 和 `docker-compose.yaml`。
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t open-webui .
|
||||
|
||||
# 运行容器
|
||||
docker run -d -p 8080:8080 -v open-webui:/app/backend/data open-webui
|
||||
```
|
||||
|
||||
## 更多资源
|
||||
|
||||
- **项目文档**: [CLAUDE.md](./CLAUDE.md)
|
||||
- **API 文档**: http://localhost:8080/docs (启动后端后访问)
|
||||
- **官方仓库**: https://github.com/open-webui/open-webui
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-11-08
|
||||
|
|
@ -1,88 +1,36 @@
|
|||
# 本地开发故障排除
|
||||
# Open WebUI Troubleshooting Guide
|
||||
|
||||
## 问题: "Open WebUI 需要后端服务" 错误
|
||||
## Understanding the Open WebUI Architecture
|
||||
|
||||
### 🔧 快速解决方案
|
||||
The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
|
||||
|
||||
**请在浏览器中进行硬刷新:**
|
||||
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
|
||||
|
||||
- **macOS**: `Cmd + Shift + R`
|
||||
- **Windows/Linux**: `Ctrl + Shift + R`
|
||||
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
|
||||
|
||||
然后检查是否解决问题。
|
||||
## Open WebUI: Server Connection Error
|
||||
|
||||
### 📋 详细排查步骤
|
||||
If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
|
||||
|
||||
#### 1. 打开浏览器开发者工具
|
||||
|
||||
- **macOS**: `Cmd + Option + I`
|
||||
- **Windows/Linux**: `F12`
|
||||
|
||||
#### 2. 检查 Console (控制台)
|
||||
|
||||
查找是否有错误消息,特别是:
|
||||
- 红色的错误信息
|
||||
- 网络请求失败
|
||||
- CORS 相关错误
|
||||
|
||||
#### 3. 检查 Network (网络) 标签
|
||||
|
||||
1. 切换到 Network 标签
|
||||
2. 刷新页面
|
||||
3. 查找对 `http://localhost:8080/api/config` 的请求
|
||||
4. 如果找到,点击查看:
|
||||
- **Status** 应该是 `200`
|
||||
- **Response** 应该包含 JSON 配置
|
||||
|
||||
#### 4. 清除本地存储
|
||||
|
||||
1. 在开发者工具中,转到 **Application** 标签
|
||||
2. 左侧找到 **Local Storage**
|
||||
3. 展开并点击 `http://localhost:5050`
|
||||
4. 点击右键 → **Clear**
|
||||
5. 刷新页面
|
||||
|
||||
### ✅ 验证服务状态
|
||||
|
||||
在终端运行:
|
||||
**Example Docker Command**:
|
||||
|
||||
```bash
|
||||
# 测试后端 API
|
||||
curl http://localhost:8080/api/config
|
||||
|
||||
# 检查端口占用
|
||||
lsof -i :8080 -i :5050 | grep LISTEN
|
||||
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
如果 curl 命令返回 JSON 配置,说明后端正常运行。
|
||||
### Error on Slow Responses for Ollama
|
||||
|
||||
### 🔄 重启服务 (如果需要)
|
||||
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
|
||||
|
||||
如果上述方法无效,停止当前服务 (`Ctrl + C`) 并重新启动:
|
||||
### General Connection Errors
|
||||
|
||||
**后端:**
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python -m uvicorn open_webui.main:app --reload --port 8080 --host 0.0.0.0
|
||||
```
|
||||
**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
|
||||
|
||||
**前端:**
|
||||
```bash
|
||||
npm run dev:5050
|
||||
```
|
||||
**Troubleshooting Steps**:
|
||||
|
||||
### 🌐 尝试不同端口
|
||||
1. **Verify Ollama URL Format**:
|
||||
- When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
|
||||
- In the Open WebUI, navigate to "Settings" > "General".
|
||||
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
|
||||
|
||||
如果端口冲突,可以使用不同端口:
|
||||
|
||||
**前端:**
|
||||
```bash
|
||||
npm run dev -- --port 3000
|
||||
```
|
||||
|
||||
然后访问 `http://localhost:3000`
|
||||
|
||||
---
|
||||
|
||||
**还有问题?** 查看 `/Users/sylar/my_ws/open-webui-next/LOCAL_SETUP.md` 获取完整设置指南。
|
||||
By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def version_callback(value: bool):
|
|||
if value:
|
||||
from open_webui.env import VERSION
|
||||
|
||||
typer.echo(f"Open WebUI version: {VERSION}")
|
||||
typer.echo(f"CyberLover version: {VERSION}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -975,7 +975,7 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
|||
if ENV == "prod":
|
||||
if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
|
||||
if USE_OLLAMA_DOCKER.lower() == "true":
|
||||
# if you use all-in-one docker container (Open WebUI + Ollama)
|
||||
# if you use all-in-one docker container (CyberLover + Ollama)
|
||||
# with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
|
||||
OLLAMA_BASE_URL = "http://localhost:11434"
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ for source in log_sources:
|
|||
|
||||
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
||||
|
||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
||||
if WEBUI_NAME != "Open WebUI":
|
||||
WEBUI_NAME += " (Open WebUI)"
|
||||
WEBUI_NAME = os.environ.get("WEBUI_NAME", "CyberLover")
|
||||
if WEBUI_NAME != "CyberLover":
|
||||
WEBUI_NAME += " (CyberLover)"
|
||||
|
||||
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
||||
|
||||
|
|
|
|||
BIN
backend/open_webui/favicon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -609,14 +609,14 @@ async def lifespan(app: FastAPI):
|
|||
|
||||
|
||||
app = FastAPI(
|
||||
title="Open WebUI",
|
||||
title="CyberLover",
|
||||
docs_url="/docs" if ENV == "dev" else None,
|
||||
openapi_url="/openapi.json" if ENV == "dev" else None,
|
||||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# For Open WebUI OIDC/OAuth2
|
||||
# For CyberLover OIDC/OAuth2
|
||||
oauth_manager = OAuthManager(app)
|
||||
app.state.oauth_manager = oauth_manager
|
||||
|
||||
|
|
@ -1916,7 +1916,7 @@ async def get_app_changelog():
|
|||
@app.get("/api/usage")
|
||||
async def get_current_usage(user=Depends(get_verified_user)):
|
||||
"""
|
||||
Get current usage statistics for Open WebUI.
|
||||
Get current usage statistics for CyberLover.
|
||||
This is an experimental endpoint and subject to change.
|
||||
"""
|
||||
try:
|
||||
|
|
@ -2087,6 +2087,11 @@ async def healthcheck_with_db():
|
|||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
|
||||
@app.get("/favicon.png")
|
||||
async def get_favicon():
|
||||
return FileResponse(os.path.join(STATIC_DIR, "favicon.png"))
|
||||
|
||||
|
||||
@app.get("/cache/{path:path}")
|
||||
async def serve_cache_file(
|
||||
path: str,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ class UsersTable:
|
|||
"email": email,
|
||||
"role": role,
|
||||
"profile_image_url": profile_image_url,
|
||||
# AI-Friend, 默认启用记忆功能
|
||||
"settings": {"ui": {"memory": True}},
|
||||
"last_active_at": int(time.time()),
|
||||
"created_at": int(time.time()),
|
||||
"updated_at": int(time.time()),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class ExternalWebLoader(BaseLoader):
|
|||
response = requests.post(
|
||||
self.external_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) External Web Loader",
|
||||
"User-Agent": "CyberLover (https://github.com/open-webui/open-webui) External Web Loader",
|
||||
"Authorization": f"Bearer {self.external_api_key}",
|
||||
},
|
||||
json={
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ class MilvusClient(VectorDBBase):
|
|||
"""
|
||||
Maps the traditional collection name to multi-tenant collection and resource ID.
|
||||
|
||||
WARNING: This mapping relies on current Open WebUI naming conventions for
|
||||
collection names. If Open WebUI changes how it generates collection names
|
||||
WARNING: This mapping relies on current CyberLover naming conventions for
|
||||
collection names. If CyberLover changes how it generates collection names
|
||||
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
|
||||
formats), this mapping will break and route data to incorrect collections.
|
||||
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ class QdrantClient(VectorDBBase):
|
|||
Returns:
|
||||
tuple: (collection_name, tenant_id)
|
||||
|
||||
WARNING: This mapping relies on current Open WebUI naming conventions for
|
||||
collection names. If Open WebUI changes how it generates collection names
|
||||
WARNING: This mapping relies on current CyberLover naming conventions for
|
||||
collection names. If CyberLover changes how it generates collection names
|
||||
(e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash
|
||||
formats), this mapping will break and route data to incorrect collections.
|
||||
POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
|||
|
||||
class S3VectorClient(VectorDBBase):
|
||||
"""
|
||||
AWS S3 Vector integration for Open WebUI Knowledge.
|
||||
AWS S3 Vector integration for CyberLover Knowledge.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -80,7 +80,7 @@ class S3VectorClient(VectorDBBase):
|
|||
if not isinstance(metadata, dict) or len(metadata) <= 10:
|
||||
return metadata
|
||||
|
||||
# Keep only the first 10 keys, prioritizing important ones based on actual Open WebUI metadata
|
||||
# Keep only the first 10 keys, prioritizing important ones based on actual CyberLover metadata
|
||||
important_keys = [
|
||||
"text", # The actual document content
|
||||
"file_id", # File ID
|
||||
|
|
@ -570,7 +570,7 @@ class S3VectorClient(VectorDBBase):
|
|||
)
|
||||
|
||||
# Return in GetResult format
|
||||
# The Open WebUI GetResult expects lists of lists, so we wrap each list
|
||||
# The CyberLover GetResult expects lists of lists, so we wrap each list
|
||||
if all_ids:
|
||||
return GetResult(
|
||||
ids=[all_ids], documents=[all_documents], metadatas=[all_metadatas]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ def search_external(
|
|||
response = requests.post(
|
||||
external_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"User-Agent": "CyberLover (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Authorization": f"Bearer {external_api_key}",
|
||||
},
|
||||
json={
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ def search_firecrawl(
|
|||
response = requests.post(
|
||||
firecrawl_search_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"User-Agent": "CyberLover (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Authorization": f"Bearer {firecrawl_api_key}",
|
||||
},
|
||||
json={
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ def search_searxng(
|
|||
response = requests.get(
|
||||
query_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"User-Agent": "CyberLover (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Accept": "text/html",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def search_yacy(
|
|||
query_url,
|
||||
auth=yacy_auth,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"User-Agent": "CyberLover (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Accept": "text/html",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
detail = None
|
||||
|
||||
status_code = 500
|
||||
detail = f"Open WebUI: Server Connection Error"
|
||||
detail = f"CyberLover: Server Connection Error"
|
||||
|
||||
if r is not None:
|
||||
status_code = r.status
|
||||
|
|
@ -450,7 +450,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
raise HTTPException(
|
||||
status_code=getattr(r, "status", 500) if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
elif request.app.state.config.TTS_ENGINE == "azure":
|
||||
|
|
@ -509,7 +509,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
raise HTTPException(
|
||||
status_code=getattr(r, "status", 500) if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
elif request.app.state.config.TTS_ENGINE == "transformers":
|
||||
|
|
@ -637,7 +637,7 @@ def transcription_handler(request, file_path, metadata):
|
|||
except Exception:
|
||||
detail = f"External: {e}"
|
||||
|
||||
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
|
||||
raise Exception(detail if detail else "CyberLover: Server Connection Error")
|
||||
|
||||
elif request.app.state.config.STT_ENGINE == "deepgram":
|
||||
try:
|
||||
|
|
@ -708,7 +708,7 @@ def transcription_handler(request, file_path, metadata):
|
|||
detail = f"External: {res['error'].get('message', '')}"
|
||||
except Exception:
|
||||
detail = f"External: {e}"
|
||||
raise Exception(detail if detail else "Open WebUI: Server Connection Error")
|
||||
raise Exception(detail if detail else "CyberLover: Server Connection Error")
|
||||
|
||||
elif request.app.state.config.STT_ENGINE == "azure":
|
||||
# Check file exists and size
|
||||
|
|
@ -825,7 +825,7 @@ def transcription_handler(request, file_path, metadata):
|
|||
|
||||
raise HTTPException(
|
||||
status_code=getattr(r, "status_code", 500) if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ async def send_post_request(
|
|||
log.error(f"Failed to parse error response: {e}")
|
||||
raise HTTPException(
|
||||
status_code=r.status,
|
||||
detail=f"Open WebUI: Server Connection Error",
|
||||
detail=f"CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
r.raise_for_status() # Raises an error for bad responses (4xx, 5xx)
|
||||
|
|
@ -199,7 +199,7 @@ async def send_post_request(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail=detail if e else "Open WebUI: Server Connection Error",
|
||||
detail=detail if e else "CyberLover: Server Connection Error",
|
||||
)
|
||||
finally:
|
||||
if not stream:
|
||||
|
|
@ -276,7 +276,7 @@ async def verify_connection(
|
|||
except aiohttp.ClientError as e:
|
||||
log.exception(f"Client error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Open WebUI: Server Connection Error"
|
||||
status_code=500, detail="CyberLover: Server Connection Error"
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f"Unexpected error: {e}")
|
||||
|
|
@ -503,7 +503,7 @@ async def get_ollama_tags(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
|
|
@ -641,7 +641,7 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
else:
|
||||
return {"version": False}
|
||||
|
|
@ -875,7 +875,7 @@ async def copy_model(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -945,7 +945,7 @@ async def delete_model(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1009,7 +1009,7 @@ async def show_model_info(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1101,7 +1101,7 @@ async def embed(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1188,7 +1188,7 @@ async def embeddings(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1611,7 +1611,7 @@ async def get_openai_models(
|
|||
]
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
error_detail = "CyberLover: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ async def get_headers_and_cookies(
|
|||
**(
|
||||
{
|
||||
"HTTP-Referer": "https://openwebui.com/",
|
||||
"X-Title": "Open WebUI",
|
||||
"X-Title": "CyberLover",
|
||||
}
|
||||
if "openrouter.ai" in url
|
||||
else {}
|
||||
|
|
@ -349,7 +349,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else "Open WebUI: Server Connection Error",
|
||||
detail=detail if detail else "CyberLover: Server Connection Error",
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
|
|
@ -623,7 +623,7 @@ async def get_models(
|
|||
# ClientError covers all aiohttp requests issues
|
||||
log.exception(f"Client error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Open WebUI: Server Connection Error"
|
||||
status_code=500, detail="CyberLover: Server Connection Error"
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f"Unexpected error: {e}")
|
||||
|
|
@ -720,12 +720,12 @@ async def verify_connection(
|
|||
# ClientError covers all aiohttp requests issues
|
||||
log.exception(f"Client error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Open WebUI: Server Connection Error"
|
||||
status_code=500, detail="CyberLover: Server Connection Error"
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f"Unexpected error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Open WebUI: Server Connection Error"
|
||||
status_code=500, detail="CyberLover: Server Connection Error"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -973,7 +973,7 @@ async def generate_chat_completion(
|
|||
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail="Open WebUI: Server Connection Error",
|
||||
detail="CyberLover: Server Connection Error",
|
||||
)
|
||||
finally:
|
||||
if not streaming:
|
||||
|
|
@ -1055,7 +1055,7 @@ async def embeddings(request: Request, form_data: dict, user):
|
|||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail="Open WebUI: Server Connection Error",
|
||||
detail="CyberLover: Server Connection Error",
|
||||
)
|
||||
finally:
|
||||
if not streaming:
|
||||
|
|
@ -1148,7 +1148,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail="Open WebUI: Server Connection Error",
|
||||
detail="CyberLover: Server Connection Error",
|
||||
)
|
||||
finally:
|
||||
if not streaming:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Experimental SCIM 2.0 Implementation for Open WebUI
|
||||
Experimental SCIM 2.0 Implementation for CyberLover
|
||||
Provides System for Cross-domain Identity Management endpoints for users and groups
|
||||
|
||||
NOTE: This is an experimental implementation and may not fully comply with SCIM 2.0 standards, and is subject to change.
|
||||
|
|
|
|||
BIN
backend/open_webui/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
0
backend/open_webui/static/custom.css
Normal file
BIN
backend/open_webui/static/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
backend/open_webui/static/favicon-dark.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
backend/open_webui/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
backend/open_webui/static/favicon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
40
backend/open_webui/static/favicon.svg
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name="图层_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 73.34 69.32">
|
||||
<!-- Generator: Adobe Illustrator 29.5.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 137) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2.4px;
|
||||
}
|
||||
|
||||
.st0, .st1 {
|
||||
fill: none;
|
||||
stroke: #040000;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #040000;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M36.67,58.4l-20-20.5c-5.5-5.5-5.5-14.5,0-20s14.5-4.5,20,1.5c5.5-6,14.5-7,20-1.5s5.5,14.5,0,20l-20,20.5Z"/>
|
||||
<line class="st1" x1="36.67" y1="28.91" x2="36.67" y2="45.74"/>
|
||||
<circle class="st2" cx="36.67" cy="28.63" r="2"/>
|
||||
<circle class="st2" cx="36.67" cy="46.25" r="2"/>
|
||||
<g>
|
||||
<polyline class="st1" points="24.45 24.17 29.26 28.42 29.26 42.52"/>
|
||||
<circle class="st2" cx="23.8" cy="23.68" r="2"/>
|
||||
<circle class="st2" cx="29.26" cy="42.68" r="2"/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline class="st1" points="44.08 42.52 44.08 28.42 48.89 24.17"/>
|
||||
<circle class="st2" cx="49.53" cy="23.68" r="2"/>
|
||||
<circle class="st2" cx="44.08" cy="42.68" r="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
0
backend/open_webui/static/loader.js
Normal file
BIN
backend/open_webui/static/logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
21
backend/open_webui/static/site.webmanifest
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "CyberLover",
|
||||
"short_name": "Lover",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
backend/open_webui/static/splash-dark.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
backend/open_webui/static/splash.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 14 KiB |
1
backend/open_webui/static/user-import.csv
Normal file
|
|
@ -0,0 +1 @@
|
|||
Name,Email,Password,Role
|
||||
|
BIN
backend/open_webui/static/user.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
backend/open_webui/static/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
backend/open_webui/static/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -241,7 +241,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
|||
).rstrip("/")
|
||||
|
||||
oauth_client_metadata = OAuthClientMetadata(
|
||||
client_name="Open WebUI",
|
||||
client_name="CyberLover",
|
||||
redirect_uris=[f"{redirect_base_url}/oauth/clients/{client_id}/callback"],
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
response_types=["code"],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""OpenTelemetry metrics bootstrap for Open WebUI.
|
||||
"""OpenTelemetry metrics bootstrap for CyberLover.
|
||||
|
||||
This module initialises a MeterProvider that sends metrics to an OTLP
|
||||
collector. The collector is responsible for exposing a Prometheus
|
||||
|
|
|
|||
1088
docs/DOCKER-DEPLOYMENT.md
Normal file
676
docs/LOCAL-PUSH-GUIDE.md
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
# 本地镜像构建与推送指南
|
||||
|
||||
本指南介绍如何在本地构建 Docker 镜像并手动推送到 GitHub Container Registry (GHCR)。
|
||||
|
||||
## 当前仓库信息
|
||||
|
||||
- **仓库**: `ai-friend-coming/open-webui-next`
|
||||
- **镜像仓库**: `ghcr.io/ai-friend-coming/open-webui-next`
|
||||
- **当前分支**: `main`
|
||||
- **当前 commit**: `88396a16e`
|
||||
|
||||
---
|
||||
|
||||
## 前置准备
|
||||
|
||||
### 1. 登录 GitHub Container Registry
|
||||
|
||||
#### 创建 Personal Access Token (PAT)
|
||||
|
||||
1. 访问 https://github.com/settings/tokens
|
||||
2. 点击 "Generate new token" → "Generate new token (classic)"
|
||||
3. 设置权限:
|
||||
- `write:packages` - 推送容器镜像
|
||||
- `read:packages` - 拉取容器镜像
|
||||
- `delete:packages` - (可选) 删除镜像
|
||||
4. 生成并保存 Token
|
||||
|
||||
#### 登录 GHCR
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用 PAT 登录 (推荐)
|
||||
export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin
|
||||
|
||||
# 方式 2: 交互式登录
|
||||
docker login ghcr.io -u ai-friend-coming
|
||||
# Password: [输入 PAT]
|
||||
```
|
||||
|
||||
成功登录后会显示:
|
||||
```
|
||||
Login Succeeded
|
||||
```
|
||||
|
||||
### 2. 验证 Docker 环境
|
||||
|
||||
```bash
|
||||
# 检查 Docker 版本
|
||||
docker --version
|
||||
# 推荐: Docker version 24.0.0 或更高
|
||||
|
||||
# 检查 Buildx 插件
|
||||
docker buildx version
|
||||
# 推荐: v0.11.0 或更高
|
||||
|
||||
# 创建 Buildx builder (如果不存在)
|
||||
docker buildx create --name multiarch-builder --use
|
||||
docker buildx inspect --bootstrap
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 构建与推送流程
|
||||
|
||||
### 方式一: 模拟 GitHub Actions 流程 (推荐)
|
||||
|
||||
完全模拟 `.github/workflows/docker-build.yaml` 的构建流程:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# build-and-push.sh
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# ============ 配置变量 ============
|
||||
REGISTRY="ghcr.io"
|
||||
IMAGE_NAME="ai-friend-coming/open-webui-next"
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}"
|
||||
|
||||
# 获取 Git 信息
|
||||
BUILD_HASH=$(git rev-parse HEAD)
|
||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
||||
BRANCH=$(git branch --show-current)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
echo "========================================="
|
||||
echo "构建信息:"
|
||||
echo " 仓库: ${IMAGE_NAME}"
|
||||
echo " 分支: ${BRANCH}"
|
||||
echo " Commit: ${SHORT_HASH}"
|
||||
echo " 时间: ${TIMESTAMP}"
|
||||
echo "========================================="
|
||||
|
||||
# ============ 镜像标签生成 ============
|
||||
TAGS=(
|
||||
"${FULL_IMAGE_NAME}:slim" # 主标签
|
||||
"${FULL_IMAGE_NAME}:${BRANCH}-slim" # 分支标签
|
||||
"${FULL_IMAGE_NAME}:git-${SHORT_HASH}-slim" # Git commit 标签
|
||||
"${FULL_IMAGE_NAME}:${TIMESTAMP}-slim" # 时间戳标签
|
||||
)
|
||||
|
||||
# 如果在 main 分支,添加 latest-slim 标签
|
||||
if [ "$BRANCH" = "main" ]; then
|
||||
TAGS+=("${FULL_IMAGE_NAME}:latest-slim")
|
||||
fi
|
||||
|
||||
# 构建标签参数
|
||||
TAG_ARGS=""
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS="${TAG_ARGS} -t ${tag}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "将构建以下标签:"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo " - ${tag}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ============ 构建镜像 ============
|
||||
echo "开始构建镜像..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${BUILD_HASH}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
${TAG_ARGS} \
|
||||
--load \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "✅ 镜像构建成功!"
|
||||
echo ""
|
||||
|
||||
# ============ 推送镜像 ============
|
||||
read -p "是否推送镜像到 GHCR? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "开始推送镜像..."
|
||||
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo "推送: ${tag}"
|
||||
docker push "${tag}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ 所有镜像推送成功!"
|
||||
echo ""
|
||||
echo "拉取命令:"
|
||||
echo " docker pull ${FULL_IMAGE_NAME}:slim"
|
||||
echo ""
|
||||
echo "查看镜像:"
|
||||
echo " https://github.com/${IMAGE_NAME}/pkgs/container/open-webui-next"
|
||||
else
|
||||
echo "跳过推送"
|
||||
fi
|
||||
|
||||
# ============ 清理 ============
|
||||
echo ""
|
||||
read -p "是否清理本地构建缓存? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "清理构建缓存..."
|
||||
docker builder prune -f
|
||||
echo "✅ 缓存清理完成"
|
||||
fi
|
||||
```
|
||||
|
||||
**使用方法**:
|
||||
|
||||
```bash
|
||||
# 添加执行权限
|
||||
chmod +x build-and-push.sh
|
||||
|
||||
# 执行构建
|
||||
./build-and-push.sh
|
||||
```
|
||||
|
||||
### 方式二: 手动分步执行
|
||||
|
||||
#### 1. 构建镜像
|
||||
|
||||
```bash
|
||||
# 获取当前 commit SHA
|
||||
BUILD_HASH=$(git rev-parse HEAD)
|
||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
||||
|
||||
# 构建镜像
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${BUILD_HASH}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:main-slim \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:git-${SHORT_HASH}-slim \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
**构建参数说明**:
|
||||
- `--platform linux/amd64`: 构建 x86_64 架构镜像
|
||||
- `--build-arg BUILD_HASH`: 传入 Git commit SHA
|
||||
- `--build-arg USE_SLIM=true`: 构建精简版 (不预装模型)
|
||||
- `-t`: 指定镜像标签 (可以多个)
|
||||
- `--load`: 加载到本地 Docker (用于单平台构建)
|
||||
|
||||
#### 2. 验证镜像
|
||||
|
||||
```bash
|
||||
# 查看镜像大小
|
||||
docker images | grep open-webui-next
|
||||
|
||||
# 查看镜像详细信息
|
||||
docker inspect ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
|
||||
# 测试运行
|
||||
docker run --rm -p 8080:8080 ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
```
|
||||
|
||||
#### 3. 推送镜像
|
||||
|
||||
```bash
|
||||
# 推送所有标签
|
||||
docker push ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
docker push ghcr.io/ai-friend-coming/open-webui-next:main-slim
|
||||
docker push ghcr.io/ai-friend-coming/open-webui-next:git-${SHORT_HASH}-slim
|
||||
```
|
||||
|
||||
或批量推送:
|
||||
|
||||
```bash
|
||||
# 批量推送
|
||||
docker images | grep "ghcr.io/ai-friend-coming/open-webui-next" | awk '{print $1":"$2}' | xargs -I {} docker push {}
|
||||
```
|
||||
|
||||
#### 4. 验证推送
|
||||
|
||||
```bash
|
||||
# 从 GHCR 拉取验证
|
||||
docker pull ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
|
||||
# 访问 GitHub Packages 页面
|
||||
# https://github.com/ai-friend-coming/open-webui-next/pkgs/container/open-webui-next
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 构建不同镜像变体
|
||||
|
||||
### Slim 版本 (默认, 推荐)
|
||||
|
||||
```bash
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 镜像较小 (~7.8GB)
|
||||
- 首次运行时自动下载 AI 模型
|
||||
- 适合生产环境
|
||||
|
||||
### 标准版本 (预装模型)
|
||||
|
||||
```bash
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=false \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:latest \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 镜像较大 (~10GB)
|
||||
- 预装 AI 模型,启动更快
|
||||
- 适合离线环境
|
||||
|
||||
### CUDA 版本 (GPU 加速)
|
||||
|
||||
```bash
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_CUDA=true \
|
||||
--build-arg USE_CUDA_VER=cu128 \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:cuda \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 支持 NVIDIA GPU 加速
|
||||
- 需要宿主机安装 NVIDIA Docker runtime
|
||||
- 镜像更大 (~15GB)
|
||||
|
||||
---
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 1. 多架构构建 (amd64 + arm64)
|
||||
|
||||
```bash
|
||||
# 创建 multiarch builder
|
||||
docker buildx create --name multiarch --use
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
# 构建并推送多架构镜像
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
--push \
|
||||
.
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 多架构构建会自动推送 (不支持 `--load`)
|
||||
- ARM64 构建可能需要 1-2 小时
|
||||
|
||||
### 2. 使用缓存加速构建
|
||||
|
||||
```bash
|
||||
# 第一次构建: 导出缓存
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
--cache-to type=registry,ref=ghcr.io/ai-friend-coming/open-webui-next:cache-slim-amd64 \
|
||||
--load \
|
||||
.
|
||||
|
||||
# 后续构建: 使用缓存
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
--cache-from type=registry,ref=ghcr.io/ai-friend-coming/open-webui-next:cache-slim-amd64 \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
**效果**: 构建时间从 5 分钟降低到 1-2 分钟
|
||||
|
||||
### 3. 本地缓存 (更快)
|
||||
|
||||
```bash
|
||||
# 使用本地缓存
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH=$(git rev-parse HEAD) \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
--cache-to type=local,dest=/tmp/docker-cache \
|
||||
--cache-from type=local,src=/tmp/docker-cache \
|
||||
--load \
|
||||
.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 构建内存不足
|
||||
|
||||
**错误**:
|
||||
```
|
||||
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
- ✅ 确认 Dockerfile 第 30 行已取消注释: `ENV NODE_OPTIONS="--max-old-space-size=4096"`
|
||||
- ✅ 增加 Docker 内存限制: Docker Desktop → Settings → Resources → Memory (建议 8GB+)
|
||||
|
||||
### 2. 推送权限被拒绝
|
||||
|
||||
**错误**:
|
||||
```
|
||||
denied: permission_denied: write_package
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 检查登录状态
|
||||
docker info | grep Username
|
||||
|
||||
# 重新登录
|
||||
docker logout ghcr.io
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin
|
||||
|
||||
# 确认 PAT 有 write:packages 权限
|
||||
```
|
||||
|
||||
### 3. 镜像推送超时
|
||||
|
||||
**错误**:
|
||||
```
|
||||
error: timeout exceeded
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 增加 Docker 推送超时
|
||||
export DOCKER_CLIENT_TIMEOUT=300
|
||||
export COMPOSE_HTTP_TIMEOUT=300
|
||||
|
||||
# 或分别推送每个标签
|
||||
docker push ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
```
|
||||
|
||||
### 4. Buildx 不可用
|
||||
|
||||
**错误**:
|
||||
```
|
||||
ERROR: buildx: command not found
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 更新 Docker Desktop 到最新版本
|
||||
# 或手动安装 Buildx 插件
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
curl -Lo ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.11.2/buildx-v0.11.2.linux-amd64
|
||||
chmod +x ~/.docker/cli-plugins/docker-buildx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 清理与维护
|
||||
|
||||
### 清理本地镜像
|
||||
|
||||
```bash
|
||||
# 删除 dangling 镜像
|
||||
docker image prune -f
|
||||
|
||||
# 删除所有未使用的镜像
|
||||
docker image prune -a -f
|
||||
|
||||
# 删除特定镜像
|
||||
docker rmi ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
```
|
||||
|
||||
### 清理构建缓存
|
||||
|
||||
```bash
|
||||
# 清理 Buildx 缓存
|
||||
docker buildx prune -f
|
||||
|
||||
# 清理所有 Docker 缓存 (谨慎使用)
|
||||
docker system prune -a --volumes -f
|
||||
```
|
||||
|
||||
### 查看镜像层信息
|
||||
|
||||
```bash
|
||||
# 使用 dive 工具分析镜像
|
||||
docker run --rm -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
wagoodman/dive:latest \
|
||||
ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动化脚本示例
|
||||
|
||||
### 完整的 CI/CD 本地模拟脚本
|
||||
|
||||
保存为 `scripts/local-build.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/local-build.sh
|
||||
# 完整的本地构建、测试、推送流程
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# 配置
|
||||
REGISTRY="ghcr.io"
|
||||
IMAGE_NAME="ai-friend-coming/open-webui-next"
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}"
|
||||
VARIANT="slim"
|
||||
|
||||
# Git 信息
|
||||
BUILD_HASH=$(git rev-parse HEAD)
|
||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
||||
BRANCH=$(git branch --show-current)
|
||||
|
||||
# 检查工作目录
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
echo_error "请在项目根目录运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查未提交的更改
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo_warn "存在未提交的更改:"
|
||||
git status --short
|
||||
read -p "继续构建? (y/n): " -n 1 -r
|
||||
echo
|
||||
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
|
||||
fi
|
||||
|
||||
echo_info "========================================="
|
||||
echo_info "构建配置:"
|
||||
echo_info " 仓库: ${IMAGE_NAME}"
|
||||
echo_info " 分支: ${BRANCH}"
|
||||
echo_info " Commit: ${SHORT_HASH}"
|
||||
echo_info " 变体: ${VARIANT}"
|
||||
echo_info "========================================="
|
||||
|
||||
# 1. 构建镜像
|
||||
echo_info "步骤 1/5: 构建镜像..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${BUILD_HASH}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ${FULL_IMAGE_NAME}:${VARIANT} \
|
||||
-t ${FULL_IMAGE_NAME}:git-${SHORT_HASH}-${VARIANT} \
|
||||
--load \
|
||||
.
|
||||
|
||||
# 2. 验证镜像大小
|
||||
echo_info "步骤 2/5: 验证镜像..."
|
||||
IMAGE_SIZE=$(docker images ${FULL_IMAGE_NAME}:${VARIANT} --format "{{.Size}}")
|
||||
echo_info " 镜像大小: ${IMAGE_SIZE}"
|
||||
|
||||
# 3. 测试镜像
|
||||
echo_info "步骤 3/5: 测试镜像..."
|
||||
CONTAINER_ID=$(docker run -d -p 8081:8080 ${FULL_IMAGE_NAME}:${VARIANT})
|
||||
echo_info " 测试容器 ID: ${CONTAINER_ID}"
|
||||
|
||||
# 等待健康检查
|
||||
echo_info " 等待服务启动 (最多 60 秒)..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8081/health > /dev/null 2>&1; then
|
||||
echo_info " ✅ 健康检查通过"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
[ $i -eq 60 ] && echo_error "健康检查超时" && docker logs ${CONTAINER_ID} && exit 1
|
||||
done
|
||||
|
||||
# 清理测试容器
|
||||
docker stop ${CONTAINER_ID} > /dev/null
|
||||
docker rm ${CONTAINER_ID} > /dev/null
|
||||
echo_info " 测试容器已清理"
|
||||
|
||||
# 4. 推送镜像
|
||||
echo_info "步骤 4/5: 推送镜像到 GHCR..."
|
||||
read -p "确认推送? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# 检查登录状态
|
||||
if ! docker info | grep -q "Username: ai-friend-coming"; then
|
||||
echo_error "未登录 GHCR,请先登录:"
|
||||
echo " echo \$CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker push ${FULL_IMAGE_NAME}:${VARIANT}
|
||||
docker push ${FULL_IMAGE_NAME}:git-${SHORT_HASH}-${VARIANT}
|
||||
echo_info " ✅ 推送成功"
|
||||
else
|
||||
echo_warn "跳过推送"
|
||||
fi
|
||||
|
||||
# 5. 清理
|
||||
echo_info "步骤 5/5: 清理..."
|
||||
docker builder prune -f > /dev/null
|
||||
echo_info " 构建缓存已清理"
|
||||
|
||||
echo_info "========================================="
|
||||
echo_info "✅ 构建流程完成!"
|
||||
echo_info ""
|
||||
echo_info "拉取命令:"
|
||||
echo_info " docker pull ${FULL_IMAGE_NAME}:${VARIANT}"
|
||||
echo_info ""
|
||||
echo_info "查看镜像:"
|
||||
echo_info " https://github.com/${IMAGE_NAME}/pkgs/container/open-webui-next"
|
||||
echo_info "========================================="
|
||||
```
|
||||
|
||||
**使用方法**:
|
||||
|
||||
```bash
|
||||
chmod +x scripts/local-build.sh
|
||||
./scripts/local-build.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 构建前检查清单
|
||||
|
||||
- [ ] 代码已提交到 Git
|
||||
- [ ] Docker 有足够内存 (8GB+)
|
||||
- [ ] 已登录 GHCR
|
||||
- [ ] 磁盘空间充足 (20GB+)
|
||||
|
||||
### 2. 标签命名规范
|
||||
|
||||
```bash
|
||||
# 生产环境
|
||||
ghcr.io/ai-friend-coming/open-webui-next:v1.2.3-slim
|
||||
|
||||
# 测试环境
|
||||
ghcr.io/ai-friend-coming/open-webui-next:dev-slim
|
||||
|
||||
# 特性分支
|
||||
ghcr.io/ai-friend-coming/open-webui-next:feature-auth-slim
|
||||
```
|
||||
|
||||
### 3. 安全建议
|
||||
|
||||
- 不要在脚本中硬编码 PAT
|
||||
- 使用环境变量: `export CR_PAT=xxx`
|
||||
- 定期轮换 PAT (90 天)
|
||||
- 使用 `.gitignore` 排除敏感文件
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 使用 `--cache-from` 复用缓存
|
||||
- 本地缓存构建结果到 `/tmp`
|
||||
- 使用 SSD 存储 Docker 数据
|
||||
- 增大 Docker 内存限制
|
||||
|
||||
---
|
||||
|
||||
## 常用命令速查
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
docker buildx build -t IMAGE:TAG --load .
|
||||
|
||||
# 推送
|
||||
docker push IMAGE:TAG
|
||||
|
||||
# 拉取
|
||||
docker pull IMAGE:TAG
|
||||
|
||||
# 登录
|
||||
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
|
||||
|
||||
# 清理
|
||||
docker system prune -a -f
|
||||
|
||||
# 查看镜像
|
||||
docker images | grep open-webui-next
|
||||
|
||||
# 测试镜像
|
||||
docker run --rm -p 8080:8080 IMAGE:TAG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024-11-14
|
||||
|
|
@ -10,11 +10,11 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|||
class CustomBuildHook(BuildHookInterface):
|
||||
def initialize(self, version, build_data):
|
||||
super().initialize(version, build_data)
|
||||
stderr.write(">>> Building Open Webui frontend\n")
|
||||
stderr.write(">>> Building CyberLover frontend\n")
|
||||
npm = shutil.which("npm")
|
||||
if npm is None:
|
||||
raise RuntimeError(
|
||||
"NodeJS `npm` is required for building Open Webui but it was not found"
|
||||
"NodeJS `npm` is required for building CyberLover but it was not found"
|
||||
)
|
||||
stderr.write("### npm install\n")
|
||||
subprocess.run([npm, "install", "--force"], check=True) # noqa: S603
|
||||
|
|
|
|||
68
package-lock.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^3.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.22",
|
||||
"kokoro-js": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
|
|
@ -109,6 +110,7 @@
|
|||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"cypress": "^13.15.0",
|
||||
|
|
@ -3969,6 +3971,16 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jszip": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
|
||||
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jszip": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
|
|
@ -5711,8 +5723,7 @@
|
|||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||
},
|
||||
"node_modules/cose-base": {
|
||||
"version": "1.0.3",
|
||||
|
|
@ -8196,6 +8207,12 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||
|
|
@ -8468,8 +8485,7 @@
|
|||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -8601,6 +8617,18 @@
|
|||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.22",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
|
||||
|
|
@ -8758,6 +8786,15 @@
|
|||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.29.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
|
||||
|
|
@ -9923,6 +9960,12 @@
|
|||
"quansync": "^0.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/paneforge": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz",
|
||||
|
|
@ -10446,8 +10489,7 @@
|
|||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/promise-map-series": {
|
||||
"version": "0.3.0",
|
||||
|
|
@ -10889,7 +10931,6 @@
|
|||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
|
|
@ -11203,8 +11244,7 @@
|
|||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
|
@ -11713,6 +11753,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
|
|
@ -11997,7 +12043,6 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
|
|
@ -12807,8 +12852,7 @@
|
|||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"cypress": "^13.15.0",
|
||||
|
|
@ -107,6 +108,7 @@
|
|||
"idb": "^7.1.1",
|
||||
"js-sha256": "^0.10.1",
|
||||
"jspdf": "^3.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.22",
|
||||
"kokoro-js": "^1.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
|
|
|
|||
322
scripts/README.md
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
# 构建脚本使用指南
|
||||
|
||||
本目录包含用于本地构建和推送 Docker 镜像的自动化脚本。
|
||||
|
||||
## 脚本列表
|
||||
|
||||
### 1. `build-and-push.sh` - 完整构建和推送流程 (推荐)
|
||||
|
||||
**功能**: 完整的构建、测试、推送流程,包含健康检查
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
# 1. 登录 GHCR (如需推送)
|
||||
export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin
|
||||
|
||||
# 2. 运行脚本
|
||||
./scripts/build-and-push.sh
|
||||
```
|
||||
|
||||
**流程**:
|
||||
1. 检查 Git 状态和未提交更改
|
||||
2. 构建 Docker 镜像 (slim 版本)
|
||||
3. 验证镜像大小和 ID
|
||||
4. 运行健康检查测试 (可选)
|
||||
5. 推送镜像到 GHCR (需确认)
|
||||
6. 清理构建缓存 (可选)
|
||||
|
||||
**适用场景**: 正式发布前的完整测试和推送
|
||||
|
||||
---
|
||||
|
||||
### 2. `quick-build.sh` - 快速本地构建
|
||||
|
||||
**功能**: 快速构建镜像用于本地测试,不推送
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
./scripts/quick-build.sh
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 无交互式确认
|
||||
- 仅构建,不推送
|
||||
- 构建速度快 (如有缓存)
|
||||
|
||||
**适用场景**: 本地开发和快速测试
|
||||
|
||||
---
|
||||
|
||||
### 3. `simulate-workflow.sh` - 模拟 GitHub Actions
|
||||
|
||||
**功能**: 完整模拟 `.github/workflows/docker-build.yaml` 的执行流程
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
# 设置 CR_PAT (可选)
|
||||
export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN
|
||||
|
||||
# 运行模拟
|
||||
./scripts/simulate-workflow.sh
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 模拟 GitHub Actions 环境变量
|
||||
- 创建独立的 Buildx builder
|
||||
- 使用 registry 缓存
|
||||
- 输出格式与 GitHub Actions 一致
|
||||
|
||||
**适用场景**:
|
||||
- 测试 workflow 配置
|
||||
- 在推送代码前验证构建流程
|
||||
- 排查 CI/CD 问题
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 场景 1: 本地快速测试
|
||||
|
||||
```bash
|
||||
# 1. 快速构建
|
||||
./scripts/quick-build.sh
|
||||
|
||||
# 2. 运行测试
|
||||
docker run -d -p 8080:8080 ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
|
||||
# 3. 验证
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### 场景 2: 发布新版本
|
||||
|
||||
```bash
|
||||
# 1. 确保代码已提交
|
||||
git add .
|
||||
git commit -m "feat: add new feature"
|
||||
git push
|
||||
|
||||
# 2. 登录 GHCR
|
||||
export CR_PAT=ghp_xxxxxxxxxxxx
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin
|
||||
|
||||
# 3. 构建和推送
|
||||
./scripts/build-and-push.sh
|
||||
# 按提示操作: 运行健康检查 → 确认推送 → 清理缓存
|
||||
|
||||
# 4. 验证推送成功
|
||||
docker pull ghcr.io/ai-friend-coming/open-webui-next:slim
|
||||
```
|
||||
|
||||
### 场景 3: 测试 GitHub Actions workflow
|
||||
|
||||
```bash
|
||||
# 1. 模拟 workflow 执行
|
||||
export CR_PAT=ghp_xxxxxxxxxxxx
|
||||
./scripts/simulate-workflow.sh
|
||||
|
||||
# 2. 查看构建结果
|
||||
docker images | grep open-webui-next
|
||||
|
||||
# 3. 如果成功,推送代码触发真实 workflow
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
### 必需软件
|
||||
|
||||
- **Docker**: 24.0.0+
|
||||
- **Docker Buildx**: v0.11.0+
|
||||
- **Git**: 任意版本
|
||||
- **Bash**: 4.0+
|
||||
|
||||
### 系统要求
|
||||
|
||||
- **内存**: 建议 8GB+
|
||||
- **磁盘空间**: 至少 20GB 可用空间
|
||||
- **网络**: 需要访问 ghcr.io
|
||||
|
||||
### Docker 配置
|
||||
|
||||
确保 Docker 有足够的资源:
|
||||
|
||||
```bash
|
||||
# Docker Desktop 配置 (推荐):
|
||||
# - Memory: 8GB
|
||||
# - Swap: 2GB
|
||||
# - Disk image size: 64GB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 构建失败 - 内存不足
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
- 确认 `Dockerfile` 第 30 行已启用: `ENV NODE_OPTIONS="--max-old-space-size=4096"`
|
||||
- 增加 Docker Desktop 内存限制到 8GB+
|
||||
|
||||
### 2. 推送失败 - 未登录
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
unauthorized: authentication required
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 设置 PAT
|
||||
export CR_PAT=ghp_your_token_here
|
||||
|
||||
# 登录 GHCR
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin
|
||||
```
|
||||
|
||||
### 3. 脚本无执行权限
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Permission denied
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
chmod +x scripts/*.sh
|
||||
```
|
||||
|
||||
### 4. Buildx 不可用
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
ERROR: buildx: command not found
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 安装 Buildx 插件
|
||||
docker buildx install
|
||||
|
||||
# 或更新 Docker Desktop 到最新版本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 标签规则
|
||||
|
||||
所有脚本都会生成以下标签:
|
||||
|
||||
| 标签格式 | 示例 | 说明 |
|
||||
|---------|------|------|
|
||||
| `slim` | `ghcr.io/ai-friend-coming/open-webui-next:slim` | 主标签 (main 分支) |
|
||||
| `latest-slim` | `ghcr.io/ai-friend-coming/open-webui-next:latest-slim` | 最新版本 (仅 main 分支) |
|
||||
| `{branch}-slim` | `ghcr.io/ai-friend-coming/open-webui-next:main-slim` | 分支标签 |
|
||||
| `git-{sha}-slim` | `ghcr.io/ai-friend-coming/open-webui-next:git-88396a1-slim` | Git commit 标签 |
|
||||
|
||||
---
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
### PAT (Personal Access Token) 管理
|
||||
|
||||
1. **权限设置**: 仅授予 `read:packages` 和 `write:packages`
|
||||
2. **存储位置**: 使用环境变量,不要硬编码到脚本
|
||||
3. **轮换周期**: 建议每 90 天轮换一次
|
||||
4. **作用域**: 为不同用途创建不同的 PAT
|
||||
|
||||
### 环境变量设置
|
||||
|
||||
```bash
|
||||
# 临时设置 (当前会话)
|
||||
export CR_PAT=ghp_xxxxxxxxxxxx
|
||||
|
||||
# 永久设置 (添加到 ~/.bashrc 或 ~/.zshrc)
|
||||
echo 'export CR_PAT=ghp_xxxxxxxxxxxx' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
**注意**: 不要提交 PAT 到 Git 仓库
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 使用构建缓存
|
||||
|
||||
```bash
|
||||
# 第一次构建会较慢 (5-10 分钟)
|
||||
./scripts/build-and-push.sh
|
||||
|
||||
# 后续构建会使用缓存 (1-2 分钟)
|
||||
# 前提: 没有清理缓存
|
||||
```
|
||||
|
||||
### 并行构建 (高级)
|
||||
|
||||
如果需要同时构建多个变体:
|
||||
|
||||
```bash
|
||||
# 构建 slim 和 cuda 版本
|
||||
docker buildx build --build-arg USE_SLIM=true -t IMAGE:slim . &
|
||||
docker buildx build --build-arg USE_CUDA=true -t IMAGE:cuda . &
|
||||
wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 清理命令
|
||||
|
||||
### 清理所有本地镜像
|
||||
|
||||
```bash
|
||||
# 删除所有 open-webui-next 镜像
|
||||
docker images | grep open-webui-next | awk '{print $3}' | xargs docker rmi -f
|
||||
|
||||
# 清理悬空镜像
|
||||
docker image prune -f
|
||||
|
||||
# 清理所有未使用的镜像
|
||||
docker image prune -a -f
|
||||
```
|
||||
|
||||
### 清理构建缓存
|
||||
|
||||
```bash
|
||||
# 清理 Buildx 缓存
|
||||
docker buildx prune -f
|
||||
|
||||
# 清理所有 Docker 数据 (谨慎使用)
|
||||
docker system prune -a --volumes -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [本地推送完整指南](../docs/LOCAL-PUSH-GUIDE.md)
|
||||
- [Docker 部署指南](../docs/DOCKER-DEPLOYMENT.md)
|
||||
- [GitHub Actions Workflow](../.github/workflows/docker-build.yaml)
|
||||
|
||||
---
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果遇到问题:
|
||||
|
||||
1. 查看脚本输出的错误信息
|
||||
2. 参考本文档的"常见问题"部分
|
||||
3. 查看详细文档: `docs/LOCAL-PUSH-GUIDE.md`
|
||||
4. 提交 Issue: https://github.com/ai-friend-coming/open-webui-next/issues
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024-11-14
|
||||
215
scripts/build-and-push.sh
Executable file
|
|
@ -0,0 +1,215 @@
|
|||
#!/bin/bash
|
||||
# build-and-push.sh - 本地构建并推送镜像到 GHCR
|
||||
# 使用方法: ./scripts/build-and-push.sh
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# ============ 颜色输出 ============
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
echo_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
||||
|
||||
# ============ 配置变量 ============
|
||||
REGISTRY="ghcr.io"
|
||||
IMAGE_NAME="ai-friend-coming/open-webui-next"
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}"
|
||||
|
||||
# 获取 Git 信息
|
||||
BUILD_HASH=$(git rev-parse HEAD)
|
||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
||||
BRANCH=$(git branch --show-current)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
# 检查工作目录
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
echo_error "请在项目根目录运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo_info "========================================="
|
||||
echo_info "构建信息:"
|
||||
echo_info " 仓库: ${IMAGE_NAME}"
|
||||
echo_info " 分支: ${BRANCH}"
|
||||
echo_info " Commit: ${SHORT_HASH} (${BUILD_HASH:0:40}...)"
|
||||
echo_info " 时间: ${TIMESTAMP}"
|
||||
echo_info "========================================="
|
||||
echo ""
|
||||
|
||||
# 检查未提交的更改
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo_warn "存在未提交的更改:"
|
||||
git status --short
|
||||
echo ""
|
||||
read -p "继续构建? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo_info "已取消"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============ 镜像标签生成 ============
|
||||
TAGS=(
|
||||
"${FULL_IMAGE_NAME}:slim" # 主标签
|
||||
"${FULL_IMAGE_NAME}:${BRANCH}-slim" # 分支标签
|
||||
"${FULL_IMAGE_NAME}:git-${SHORT_HASH}-slim" # Git commit 标签
|
||||
)
|
||||
|
||||
# 如果在 main 分支,添加 latest-slim 标签
|
||||
if [ "$BRANCH" = "main" ]; then
|
||||
TAGS+=("${FULL_IMAGE_NAME}:latest-slim")
|
||||
fi
|
||||
|
||||
# 构建标签参数
|
||||
TAG_ARGS=""
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS="${TAG_ARGS} -t ${tag}"
|
||||
done
|
||||
|
||||
echo_info "将构建以下标签:"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo " - ${tag}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ============ 构建镜像 ============
|
||||
echo_step "步骤 1/4: 构建 Docker 镜像"
|
||||
echo_info "开始构建镜像 (这可能需要 5-10 分钟)..."
|
||||
echo ""
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${BUILD_HASH}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
${TAG_ARGS} \
|
||||
--load \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo_info "✅ 镜像构建成功!"
|
||||
echo ""
|
||||
|
||||
# ============ 验证镜像 ============
|
||||
echo_step "步骤 2/4: 验证镜像"
|
||||
|
||||
IMAGE_SIZE=$(docker images ${FULL_IMAGE_NAME}:slim --format "{{.Size}}")
|
||||
IMAGE_ID=$(docker images ${FULL_IMAGE_NAME}:slim --format "{{.ID}}")
|
||||
|
||||
echo_info "镜像信息:"
|
||||
echo " - ID: ${IMAGE_ID}"
|
||||
echo " - 大小: ${IMAGE_SIZE}"
|
||||
echo ""
|
||||
|
||||
# ============ 测试镜像 ============
|
||||
echo_step "步骤 3/4: 测试镜像"
|
||||
read -p "是否运行健康检查测试? (y/n): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo_info "启动测试容器 (端口 8081)..."
|
||||
|
||||
CONTAINER_ID=$(docker run -d -p 8081:8080 ${FULL_IMAGE_NAME}:slim)
|
||||
echo_info "容器 ID: ${CONTAINER_ID}"
|
||||
echo ""
|
||||
|
||||
echo_info "等待服务启动 (最多 120 秒)..."
|
||||
HEALTH_CHECK_PASSED=false
|
||||
|
||||
for i in {1..120}; do
|
||||
if curl -sf http://localhost:8081/health > /dev/null 2>&1; then
|
||||
echo_info "✅ 健康检查通过 (耗时 ${i} 秒)"
|
||||
HEALTH_CHECK_PASSED=true
|
||||
break
|
||||
fi
|
||||
|
||||
# 每 10 秒显示一次进度
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo_info " 等待中... (${i}/120 秒)"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$HEALTH_CHECK_PASSED" = false ]; then
|
||||
echo_error "健康检查超时!"
|
||||
echo_error "容器日志:"
|
||||
docker logs ${CONTAINER_ID} | tail -50
|
||||
docker stop ${CONTAINER_ID} > /dev/null
|
||||
docker rm ${CONTAINER_ID} > /dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 清理测试容器
|
||||
echo_info "清理测试容器..."
|
||||
docker stop ${CONTAINER_ID} > /dev/null
|
||||
docker rm ${CONTAINER_ID} > /dev/null
|
||||
echo ""
|
||||
else
|
||||
echo_warn "跳过健康检查"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ============ 推送镜像 ============
|
||||
echo_step "步骤 4/4: 推送镜像到 GHCR"
|
||||
echo_info "将推送 ${#TAGS[@]} 个标签到 ${REGISTRY}"
|
||||
echo ""
|
||||
|
||||
read -p "确认推送? (y/n): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# 检查登录状态
|
||||
if ! docker info 2>/dev/null | grep -q "Username"; then
|
||||
echo_error "未登录 GHCR!"
|
||||
echo_error "请先执行:"
|
||||
echo " export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN"
|
||||
echo " echo \$CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo_info "开始推送镜像..."
|
||||
echo ""
|
||||
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo_info "推送: ${tag}"
|
||||
docker push "${tag}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo_info "✅ 所有镜像推送成功!"
|
||||
echo ""
|
||||
echo_info "拉取命令:"
|
||||
echo " docker pull ${FULL_IMAGE_NAME}:slim"
|
||||
echo ""
|
||||
echo_info "查看镜像:"
|
||||
echo " https://github.com/${IMAGE_NAME}/pkgs/container/open-webui-next"
|
||||
echo ""
|
||||
else
|
||||
echo_warn "跳过推送"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ============ 清理 ============
|
||||
echo_info "清理选项:"
|
||||
read -p "是否清理本地构建缓存? (y/n): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo_info "清理构建缓存..."
|
||||
docker builder prune -f
|
||||
echo_info "✅ 缓存清理完成"
|
||||
else
|
||||
echo_warn "保留构建缓存 (下次构建会更快)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo_info "========================================="
|
||||
echo_info "✅ 构建流程完成!"
|
||||
echo_info "========================================="
|
||||
29
scripts/quick-build.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
# quick-build.sh - 快速本地构建 (不推送)
|
||||
# 使用方法: ./scripts/quick-build.sh
|
||||
|
||||
set -e
|
||||
|
||||
# 获取 Git 信息
|
||||
BUILD_HASH=$(git rev-parse HEAD)
|
||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "🏗️ 开始快速构建..."
|
||||
echo "📋 Commit: ${SHORT_HASH}"
|
||||
echo ""
|
||||
|
||||
# 构建镜像
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${BUILD_HASH}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:slim \
|
||||
-t ghcr.io/ai-friend-coming/open-webui-next:dev \
|
||||
--load \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "✅ 构建完成!"
|
||||
echo ""
|
||||
echo "🚀 运行命令:"
|
||||
echo " docker run -d -p 8080:8080 ghcr.io/ai-friend-coming/open-webui-next:slim"
|
||||
181
scripts/simulate-workflow.sh
Executable file
|
|
@ -0,0 +1,181 @@
|
|||
#!/bin/bash
|
||||
# simulate-workflow.sh - 模拟 GitHub Actions workflow 完整流程
|
||||
# 使用方法: ./scripts/simulate-workflow.sh
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo_job() { echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${BLUE}[JOB]${NC} $1"; }
|
||||
echo_step() { echo -e "${GREEN}[STEP]${NC} $1"; }
|
||||
echo_info() { echo -e " $1"; }
|
||||
|
||||
# ============ 模拟 GitHub Actions 环境变量 ============
|
||||
export GITHUB_REPOSITORY="ai-friend-coming/open-webui-next"
|
||||
export GITHUB_SHA=$(git rev-parse HEAD)
|
||||
export GITHUB_REF=$(git symbolic-ref HEAD)
|
||||
export GITHUB_REF_NAME=$(git branch --show-current)
|
||||
export IMAGE_NAME="${GITHUB_REPOSITORY,,}" # 转小写
|
||||
export FULL_IMAGE_NAME="ghcr.io/${IMAGE_NAME}"
|
||||
|
||||
echo_job "Build Slim Image (amd64)"
|
||||
echo ""
|
||||
|
||||
# ============ Step 1: Set repository and image name ============
|
||||
echo_step "Set repository and image name to lowercase"
|
||||
echo_info "IMAGE_NAME=${IMAGE_NAME}"
|
||||
echo_info "FULL_IMAGE_NAME=${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
|
||||
# ============ Step 2: Checkout repository ============
|
||||
echo_step "Checkout repository"
|
||||
echo_info "Repository: ${GITHUB_REPOSITORY}"
|
||||
echo_info "Branch: ${GITHUB_REF_NAME}"
|
||||
echo_info "Commit: ${GITHUB_SHA:0:7}"
|
||||
echo ""
|
||||
|
||||
# ============ Step 3: Set up Docker Buildx ============
|
||||
echo_step "Set up Docker Buildx"
|
||||
if ! docker buildx inspect github-actions > /dev/null 2>&1; then
|
||||
docker buildx create --name github-actions --use
|
||||
echo_info "Created new builder: github-actions"
|
||||
else
|
||||
docker buildx use github-actions
|
||||
echo_info "Using existing builder: github-actions"
|
||||
fi
|
||||
docker buildx inspect --bootstrap
|
||||
echo ""
|
||||
|
||||
# ============ Step 4: Log in to GitHub Container Registry ============
|
||||
echo_step "Log in to GitHub Container Registry"
|
||||
echo_info "Registry: ghcr.io"
|
||||
echo_info "Username: $(whoami)"
|
||||
|
||||
if [ -z "$CR_PAT" ]; then
|
||||
echo -e "${YELLOW}[WARN]${NC} CR_PAT 环境变量未设置,跳过登录"
|
||||
echo_info "如需推送,请设置: export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN"
|
||||
SKIP_PUSH=true
|
||||
else
|
||||
echo $CR_PAT | docker login ghcr.io -u ai-friend-coming --password-stdin > /dev/null 2>&1
|
||||
echo_info "✅ Login Succeeded"
|
||||
SKIP_PUSH=false
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============ Step 5: Extract metadata for Docker images ============
|
||||
echo_step "Extract metadata for Docker images"
|
||||
|
||||
# 生成标签
|
||||
if [ "$GITHUB_REF_NAME" = "main" ]; then
|
||||
TAGS=(
|
||||
"${FULL_IMAGE_NAME}:slim"
|
||||
"${FULL_IMAGE_NAME}:latest-slim"
|
||||
"${FULL_IMAGE_NAME}:${GITHUB_REF_NAME}-slim"
|
||||
"${FULL_IMAGE_NAME}:git-${GITHUB_SHA:0:7}-slim"
|
||||
)
|
||||
else
|
||||
TAGS=(
|
||||
"${FULL_IMAGE_NAME}:${GITHUB_REF_NAME}-slim"
|
||||
"${FULL_IMAGE_NAME}:git-${GITHUB_SHA:0:7}-slim"
|
||||
)
|
||||
fi
|
||||
|
||||
echo_info "Tags:"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo_info " - ${tag}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 构建标签参数
|
||||
TAG_ARGS=""
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS="${TAG_ARGS} --tag ${tag}"
|
||||
done
|
||||
|
||||
# ============ Step 6: Extract metadata for Docker cache ============
|
||||
echo_step "Extract metadata for Docker cache"
|
||||
CACHE_TAG="${FULL_IMAGE_NAME}:cache-slim-linux-amd64-${GITHUB_REF_NAME}"
|
||||
echo_info "Cache: ${CACHE_TAG}"
|
||||
echo ""
|
||||
|
||||
# ============ Step 7: Build and push Docker image ============
|
||||
echo_step "Build and push Docker image (slim)"
|
||||
echo_info "Platform: linux/amd64"
|
||||
echo_info "Build args: BUILD_HASH=${GITHUB_SHA}, USE_SLIM=true"
|
||||
echo ""
|
||||
|
||||
if [ "$SKIP_PUSH" = true ]; then
|
||||
# 仅构建,不推送
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${GITHUB_SHA}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
${TAG_ARGS} \
|
||||
--cache-from type=registry,ref=${CACHE_TAG} \
|
||||
--load \
|
||||
.
|
||||
echo ""
|
||||
echo -e "${YELLOW}[WARN]${NC} 跳过推送 (CR_PAT 未设置)"
|
||||
else
|
||||
# 构建并推送
|
||||
read -p "确认推送到 GHCR? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${GITHUB_SHA}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
${TAG_ARGS} \
|
||||
--cache-from type=registry,ref=${CACHE_TAG} \
|
||||
--cache-to type=registry,ref=${CACHE_TAG},mode=max \
|
||||
--push \
|
||||
.
|
||||
echo ""
|
||||
echo_info "✅ Image pushed successfully"
|
||||
else
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg BUILD_HASH="${GITHUB_SHA}" \
|
||||
--build-arg USE_SLIM=true \
|
||||
${TAG_ARGS} \
|
||||
--cache-from type=registry,ref=${CACHE_TAG} \
|
||||
--load \
|
||||
.
|
||||
echo ""
|
||||
echo -e "${YELLOW}[WARN]${NC} 用户取消推送"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============ Step 8: Inspect image ============
|
||||
echo_step "Inspect image"
|
||||
if [ "$SKIP_PUSH" = false ] && [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
docker buildx imagetools inspect ${FULL_IMAGE_NAME}:slim
|
||||
else
|
||||
docker images | grep open-webui-next | head -5
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ============ Step 9: Output image tags ============
|
||||
echo_step "Output image tags"
|
||||
echo ""
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}🐳 Docker 镜像构建成功${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo "📦 镜像标签:"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo " ${tag}"
|
||||
done
|
||||
echo ""
|
||||
echo "🚀 拉取命令:"
|
||||
echo " docker pull ${FULL_IMAGE_NAME}:slim"
|
||||
echo ""
|
||||
echo "🌐 查看镜像:"
|
||||
echo " https://github.com/${GITHUB_REPOSITORY}/pkgs/container/open-webui-next"
|
||||
echo ""
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<title>Open WebUI</title>
|
||||
<title>CyberLover</title>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -746,7 +746,7 @@
|
|||
{$i18n.t('Warning')}:
|
||||
</span>
|
||||
{$i18n.t(
|
||||
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
|
||||
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.'
|
||||
)}
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
};
|
||||
|
||||
const shareHandler = async () => {
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
|
||||
// remove snapshot from feedbacks
|
||||
const feedbacksToShare = feedbacks.map((f) => {
|
||||
|
|
@ -407,7 +407,7 @@
|
|||
}}
|
||||
>
|
||||
<div class=" self-center mr-2 font-medium line-clamp-1">
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
{$i18n.t('Share to CyberLover Community')}
|
||||
</div>
|
||||
|
||||
<div class=" self-center">
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
return null;
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
|
|
@ -594,7 +594,7 @@
|
|||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
{$i18n.t('Made by CyberLover Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(`Open WebUI uses faster-whisper internally.`)}
|
||||
{$i18n.t(`CyberLover uses faster-whisper internally.`)}
|
||||
|
||||
<a
|
||||
class=" hover:underline dark:text-gray-200 text-gray-800"
|
||||
|
|
@ -561,7 +561,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(`Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.`)}
|
||||
{$i18n.t(`CyberLover uses SpeechT5 and CMU Arctic speaker embeddings.`)}
|
||||
|
||||
To learn more about SpeechT5,
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@
|
|||
{$i18n.t('Help')}
|
||||
</div>
|
||||
<div class=" text-xs text-gray-500">
|
||||
{$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
|
||||
{$i18n.t('Discover how to use CyberLover and seek support from the community.')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@
|
|||
>
|
||||
> It looks like you have over 50 users, that usually falls under organizational usage.
|
||||
>
|
||||
> Open WebUI is completely free to use as-is, with no restrictions or hidden limits, and we'd love to keep it that way. 🌱
|
||||
> CyberLover is completely free to use as-is, with no restrictions or hidden limits, and we'd love to keep it that way. 🌱
|
||||
>
|
||||
> By supporting the project through sponsorship or an enterprise license, you’re not only helping us stay independent, you’re also helping us ship new features faster, improve stability, and grow the project for the long haul. With an *enterprise license*, you also get additional perks like dedicated support, customization options, and more, all at a fraction of what it would cost to build and maintain internally.
|
||||
>
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>#{channel?.name ?? 'Channel'} • Open WebUI</title>
|
||||
<title>#{channel?.name ?? 'Channel'} • CyberLover</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@
|
|||
let imageGenerationEnabled = false;
|
||||
let webSearchEnabled = false;
|
||||
let codeInterpreterEnabled = false;
|
||||
let memoryEnabled = true;
|
||||
|
||||
let showCommands = false;
|
||||
|
||||
|
|
@ -166,6 +167,7 @@
|
|||
selectedFilterIds = [];
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
memoryEnabled = true;
|
||||
|
||||
const storageChatInput = sessionStorage.getItem(
|
||||
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||
|
|
@ -190,6 +192,7 @@
|
|||
webSearchEnabled = input.webSearchEnabled;
|
||||
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
||||
memoryEnabled = input.memoryEnabled;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
|
@ -250,6 +253,7 @@
|
|||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
memoryEnabled = true;
|
||||
|
||||
setDefaults();
|
||||
};
|
||||
|
|
@ -296,6 +300,10 @@
|
|||
if (model.info?.meta?.capabilities?.['code_interpreter']) {
|
||||
codeInterpreterEnabled = model.info.meta.defaultFeatureIds.includes('code_interpreter');
|
||||
}
|
||||
|
||||
if (model.info?.meta?.capabilities?.['memory']) {
|
||||
memoryEnabled = model.info.meta.defaultFeatureIds.includes('memory');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -559,6 +567,7 @@
|
|||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
memoryEnabled = true;
|
||||
|
||||
try {
|
||||
const input = JSON.parse(storageChatInput);
|
||||
|
|
@ -571,6 +580,7 @@
|
|||
webSearchEnabled = input.webSearchEnabled;
|
||||
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
||||
memoryEnabled = input.memoryEnabled;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
|
@ -1749,6 +1759,11 @@
|
|||
features = { ...features, memory: true };
|
||||
}
|
||||
|
||||
// 如果用户手动切换了记忆开关,覆盖全局设置
|
||||
if (memoryEnabled !== undefined && memoryEnabled !== ($settings?.memory ?? false)) {
|
||||
features = { ...features, memory: memoryEnabled };
|
||||
}
|
||||
|
||||
return features;
|
||||
};
|
||||
|
||||
|
|
@ -2454,6 +2469,7 @@
|
|||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:memoryEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
toolServers={$toolServers}
|
||||
|
|
@ -2506,6 +2522,7 @@
|
|||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:memoryEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
toolServers={$toolServers}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import Image from '../common/Image.svelte';
|
||||
import Switch from '../common/Switch.svelte';
|
||||
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
|
|
@ -108,6 +109,7 @@
|
|||
export let imageGenerationEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let memoryEnabled = false;
|
||||
|
||||
let showInputVariablesModal = false;
|
||||
let inputVariablesModalCallback = (variableValues) => {};
|
||||
|
|
@ -138,7 +140,8 @@
|
|||
selectedFilterIds,
|
||||
imageGenerationEnabled,
|
||||
webSearchEnabled,
|
||||
codeInterpreterEnabled
|
||||
codeInterpreterEnabled,
|
||||
memoryEnabled
|
||||
});
|
||||
|
||||
const inputVariableHandler = async (text: string): Promise<string> => {
|
||||
|
|
@ -475,6 +478,9 @@
|
|||
$config?.features?.enable_code_interpreter &&
|
||||
($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter);
|
||||
|
||||
let showMemoryButton = false;
|
||||
$: showMemoryButton = $settings?.memory !== undefined;
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTo({
|
||||
|
|
@ -1330,6 +1336,7 @@
|
|||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
memoryEnabled = false;
|
||||
}
|
||||
}}
|
||||
on:paste={async (e) => {
|
||||
|
|
@ -1461,6 +1468,9 @@
|
|||
class="flex self-center w-[1px] h-4 mx-1 bg-gray-200/50 dark:bg-gray-800/50"
|
||||
/>
|
||||
|
||||
|
||||
<!-- AI-Friend 屏蔽扩展菜单-->
|
||||
{#if false}
|
||||
{#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || (toggleFilters && toggleFilters.length > 0)}
|
||||
<IntegrationsMenu
|
||||
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
|
||||
|
|
@ -1473,6 +1483,7 @@
|
|||
bind:webSearchEnabled
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:memoryEnabled
|
||||
closeOnOutsideClick={integrationsMenuCloseOnOutsideClick}
|
||||
onShowValves={(e) => {
|
||||
const { type, id } = e;
|
||||
|
|
@ -1496,6 +1507,24 @@
|
|||
</div>
|
||||
</IntegrationsMenu>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if showMemoryButton}
|
||||
<div
|
||||
class="flex items-center gap-2 bg-transparent hover:bg-gray-50 dark:hover:bg-gray-800 rounded-full px-2.5 py-1.5 transition"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 text-gray-700 dark:text-gray-300">
|
||||
<Sparkles className="size-4" strokeWidth="1.5" />
|
||||
<span class="text-sm">{$i18n.t('Memory')}</span>
|
||||
</div>
|
||||
<Switch
|
||||
bind:state={memoryEnabled}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedModelIds.length === 1 && $models.find((m) => m.id === selectedModelIds[0])?.has_user_valves}
|
||||
<div class="ml-1 flex gap-1.5">
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
export let imageGenerationEnabled = false;
|
||||
export let showCodeInterpreterButton = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let memoryEnabled = false;
|
||||
|
||||
export let onShowValves: Function;
|
||||
export let onClose: Function;
|
||||
|
|
|
|||
|
|
@ -1488,7 +1488,9 @@
|
|||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
<!-- AI friend 屏蔽提问提示 -->
|
||||
{#if false}
|
||||
{#if (isLastMessage || ($settings?.keepFollowUpPrompts ?? false)) && message.done && !readOnly && (message?.followUps ?? []).length > 0}
|
||||
<div class="mt-2.5" in:fade={{ duration: 100 }}>
|
||||
<FollowUps
|
||||
|
|
@ -1505,6 +1507,7 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
export let imageGenerationEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
export let memoryEnabled = false;
|
||||
|
||||
export let onSelect = (e) => {};
|
||||
export let onChange = (e) => {};
|
||||
|
|
@ -213,6 +214,7 @@
|
|||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:memoryEnabled
|
||||
bind:atSelectedModel
|
||||
bind:showCommands
|
||||
{toolServers}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
|
||||
{#if $config?.license_metadata}
|
||||
<div class="mb-2 text-xs">
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
{#if !$WEBUI_NAME.includes('CyberLover')}
|
||||
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
|
||||
{/if}
|
||||
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
class="text-xs text-gray-400 dark:text-gray-500">Copyright (c) {new Date().getFullYear()} <a
|
||||
href="https://openwebui.com"
|
||||
target="_blank"
|
||||
class="underline">Open WebUI (Timothy Jaeryang Baek)</a
|
||||
class="underline">CyberLover (Timothy Jaeryang Baek)</a
|
||||
>
|
||||
All rights reserved.
|
||||
|
||||
|
|
@ -175,9 +175,9 @@ modification, are permitted provided that the following conditions are met:
|
|||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
|
||||
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "CyberLover" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
|
||||
|
||||
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
|
||||
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "CyberLover" branding shall constitute a material breach of license.
|
||||
|
||||
6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
|
||||
|
||||
|
|
|
|||
|
|
@ -241,8 +241,10 @@
|
|||
<UpdatePassword />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.features?.enable_api_key ?? true) || $user?.role === 'admin'}
|
||||
|
||||
<!-- ai-friend 屏蔽api显示 -->
|
||||
{#if $user?.role === 'admin'}
|
||||
<!-- {#if ($config?.features?.enable_api_key ?? true) || $user?.role === 'admin'} -->
|
||||
<div class="flex justify-between items-center text-sm mt-2">
|
||||
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@
|
|||
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
'CORS must be properly configured by the provider to allow requests from CyberLover.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
getPinnedChatList
|
||||
} from '$lib/apis/chats';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
|
@ -41,22 +42,25 @@
|
|||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let chats = JSON.parse(event.target.result);
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
}
|
||||
}
|
||||
importChats(chats);
|
||||
};
|
||||
|
||||
if (importFiles.length > 0) {
|
||||
reader.readAsText(importFiles[0]);
|
||||
extractChatsFromFile(importFiles[0])
|
||||
.then((chats) => {
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
toast.error($i18n.t('Failed to convert OpenAI chats'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
importChats(chats);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import error:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +138,7 @@
|
|||
bind:this={chatImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
accept=".json,.zip"
|
||||
hidden
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@
|
|||
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
|
||||
target="_blank"
|
||||
>
|
||||
Help us translate Open WebUI!
|
||||
Help us translate CyberLover!
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
'CORS must be properly configured by the provider to allow requests from CyberLover.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@
|
|||
keywords: [
|
||||
'about app',
|
||||
'about me',
|
||||
'about open webui',
|
||||
'about CyberLover',
|
||||
'about page',
|
||||
'about us',
|
||||
'aboutapp',
|
||||
|
|
@ -716,6 +716,8 @@
|
|||
</button>
|
||||
{/if}
|
||||
{:else if tabId === 'personalization'}
|
||||
<!-- ai-friend 屏蔽个性化选项 -->
|
||||
{#if $user?.role === 'admin'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-personalization"
|
||||
|
|
@ -739,6 +741,7 @@
|
|||
</div>
|
||||
<div class=" self-center">{$i18n.t('Personalization')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
{:else if tabId === 'audio'}
|
||||
<button
|
||||
role="tab"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
const _chat = chat.chat;
|
||||
console.log('share', _chat);
|
||||
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
const url = 'https://openwebui.com';
|
||||
// const url = 'http://localhost:5173';
|
||||
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to Open WebUI Community')}
|
||||
{$i18n.t('Share to CyberLover Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
|
||||
<div class="px-5 pb-5 w-full flex flex-col justify-center">
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-300 mb-2">
|
||||
{$i18n.t('Open WebUI can use tools provided by any OpenAPI server.')} <br /><a
|
||||
{$i18n.t('CyberLover can use tools provided by any OpenAPI server.')} <br /><a
|
||||
class="underline"
|
||||
href="https://github.com/open-webui/openapi-servers"
|
||||
target="_blank">{$i18n.t('Learn more about OpenAPI tool servers.')}</a
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -9,6 +10,7 @@
|
|||
import Collapsible from './Collapsible.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import Plus from '../icons/Plus.svelte';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
|
||||
export let open = true;
|
||||
|
||||
|
|
@ -48,26 +50,29 @@
|
|||
// If dropped items aren't files, reject them
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file && file.type === 'application/json') {
|
||||
console.log('Dropped file is a JSON file!');
|
||||
const isJSON = file && file.type === 'application/json';
|
||||
const isZIP =
|
||||
file &&
|
||||
(file.type === 'application/zip' ||
|
||||
file.type === 'application/x-zip-compressed' ||
|
||||
file.name.endsWith('.zip'));
|
||||
|
||||
// Read the JSON file with FileReader
|
||||
const reader = new FileReader();
|
||||
reader.onload = async function (event) {
|
||||
try {
|
||||
const fileContent = JSON.parse(event.target.result);
|
||||
console.log('Parsed JSON Content: ', fileContent);
|
||||
if (file && (isJSON || isZIP)) {
|
||||
console.log('Dropped file is a JSON or ZIP file!');
|
||||
|
||||
extractChatsFromFile(file)
|
||||
.then((fileContent) => {
|
||||
console.log('Parsed Content: ', fileContent);
|
||||
open = true;
|
||||
dispatch('import', fileContent);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start reading the file
|
||||
reader.readAsText(file);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error processing file:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
} else {
|
||||
console.error('Only JSON file types are supported.');
|
||||
console.error('Only JSON and ZIP file types are supported.');
|
||||
toast.error($i18n.t('Only JSON and ZIP file types are supported.'));
|
||||
}
|
||||
} else {
|
||||
open = true;
|
||||
|
|
|
|||
|
|
@ -53,11 +53,14 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- ai-friend 屏蔽管理员账号显示 -->
|
||||
{#if false}
|
||||
{#if adminDetails}
|
||||
<div class="mt-4 text-sm font-medium text-center">
|
||||
<div>{$i18n.t('Admin')}: {adminDetails.name} ({adminDetails.email})</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -56,12 +56,15 @@
|
|||
import ChannelItem from './Sidebar/ChannelItem.svelte';
|
||||
import PencilSquare from '../icons/PencilSquare.svelte';
|
||||
import Search from '../icons/Search.svelte';
|
||||
import Sparkles from '../icons/Sparkles.svelte';
|
||||
import SearchModal from './SearchModal.svelte';
|
||||
import FolderModal from './Sidebar/Folders/FolderModal.svelte';
|
||||
import Sidebar from '../icons/Sidebar.svelte';
|
||||
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
|
||||
import Note from '../icons/Note.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { extractChatsFromFile } from '$lib/utils/chatImport';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
|
|
@ -86,6 +89,10 @@
|
|||
|
||||
let newFolderId = null;
|
||||
|
||||
// Import Chats state
|
||||
let importFiles;
|
||||
let chatImportInputElement: HTMLInputElement;
|
||||
|
||||
const initFolders = async () => {
|
||||
const folderList = await getFolders(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
|
|
@ -349,6 +356,58 @@
|
|||
selectedChatId = null;
|
||||
};
|
||||
|
||||
// Import Chats handler
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
||||
if (importFiles.length > 0) {
|
||||
extractChatsFromFile(importFiles[0])
|
||||
.then((chats) => {
|
||||
console.log(chats);
|
||||
if (getImportOrigin(chats) == 'openai') {
|
||||
try {
|
||||
chats = convertOpenAIChats(chats);
|
||||
} catch (error) {
|
||||
console.log('Unable to import chats:', error);
|
||||
toast.error($i18n.t('Failed to convert OpenAI chats'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
importChatsHandler(chats);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import error:', error);
|
||||
toast.error($i18n.t(error.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const importChatsHandler = async (_chats) => {
|
||||
for (const chat of _chats) {
|
||||
console.log(chat);
|
||||
|
||||
if (chat.chat) {
|
||||
await importChat(
|
||||
localStorage.token,
|
||||
chat.chat,
|
||||
chat.meta ?? {},
|
||||
false,
|
||||
null,
|
||||
chat?.created_at ?? null,
|
||||
chat?.updated_at ?? null
|
||||
);
|
||||
} else {
|
||||
// Legacy format
|
||||
await importChat(localStorage.token, chat, {}, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
pinnedChats.set(await getPinnedChatList(localStorage.token));
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
|
||||
let unsubscribers = [];
|
||||
onMount(async () => {
|
||||
showPinnedChat = localStorage?.showPinnedChat ? localStorage.showPinnedChat === 'true' : true;
|
||||
|
|
@ -823,6 +882,25 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
||||
<a
|
||||
id="sidebar-memory-button"
|
||||
class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||
href="/memories"
|
||||
on:click={itemClickHandler}
|
||||
draggable="false"
|
||||
aria-label={$i18n.t('Memory')}
|
||||
>
|
||||
<div class="self-center">
|
||||
<Sparkles strokeWidth="2" className="size-4.5" />
|
||||
</div>
|
||||
|
||||
<div class="flex self-center translate-y-[0.5px]">
|
||||
<div class=" self-center text-sm font-primary">{$i18n.t('Memory')}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
|
||||
<div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
|
||||
<a
|
||||
|
|
@ -1199,10 +1277,52 @@
|
|||
</Folder>
|
||||
</div>
|
||||
|
||||
<!-- Separator -->
|
||||
<hr class="border-gray-100 dark:border-gray-850 mx-1.5 my-1.5" />
|
||||
|
||||
<div class="px-1.5 pt-1.5 pb-2 sticky bottom-0 z-10 -mt-3 sidebar">
|
||||
<div
|
||||
class=" sidebar-bg-gradient-to-t bg-linear-to-t from-gray-50 dark:from-gray-950 to-transparent from-50% pointer-events-none absolute inset-0 -z-10 -mt-6"
|
||||
></div>
|
||||
|
||||
<!-- Import Chats Button -->
|
||||
<input
|
||||
id="sidebar-chat-import-input"
|
||||
bind:this={chatImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json,.zip"
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
|
||||
on:click={() => {
|
||||
chatImportInputElement.click();
|
||||
}}
|
||||
draggable="false"
|
||||
aria-label={$i18n.t('Import Chats')}
|
||||
>
|
||||
<div class="self-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex self-center translate-y-[0.5px]">
|
||||
<div class="self-center text-sm font-primary">{$i18n.t('Import Chats')}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="flex flex-col font-primary">
|
||||
{#if $user !== undefined && $user !== null}
|
||||
<UserMenu
|
||||
|
|
|
|||
243
src/lib/components/layout/Sidebar/MemoryPanel.svelte
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { memories, showMemoryPanel } from '$lib/stores';
|
||||
import {
|
||||
deleteMemoriesByUserId,
|
||||
deleteMemoryById,
|
||||
getMemories
|
||||
} from '$lib/apis/memories';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import AddMemoryModal from '$lib/components/chat/Settings/Personalization/AddMemoryModal.svelte';
|
||||
import EditMemoryModal from '$lib/components/chat/Settings/Personalization/EditMemoryModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
let loading = false;
|
||||
let showAddMemoryModal = false;
|
||||
let showEditMemoryModal = false;
|
||||
let selectedMemory = null;
|
||||
let showClearConfirmDialog = false;
|
||||
|
||||
const loadMemories = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const data = await getMemories(localStorage.token);
|
||||
memories.set(data || []);
|
||||
} catch (error) {
|
||||
toast.error($i18n.t('Failed to load memories'));
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onClearConfirmed = async () => {
|
||||
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res && $memories.length > 0) {
|
||||
toast.success($i18n.t('Memory cleared successfully'));
|
||||
memories.set([]);
|
||||
}
|
||||
showClearConfirmDialog = false;
|
||||
};
|
||||
|
||||
const handleDeleteMemory = async (memoryId: string) => {
|
||||
const res = await deleteMemoryById(localStorage.token, memoryId).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Memory deleted successfully'));
|
||||
await loadMemories();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if ($showMemoryPanel && $memories.length === 0) {
|
||||
loadMemories();
|
||||
}
|
||||
});
|
||||
|
||||
$: if ($showMemoryPanel && $memories.length === 0 && !loading) {
|
||||
loadMemories();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $showMemoryPanel}
|
||||
<div class="px-2.5 mb-2 flex flex-col space-y-1">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-2 py-2">
|
||||
<div class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('Memory')}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tooltip content={$i18n.t('Add Memory')}>
|
||||
<button
|
||||
class="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 rounded-lg transition"
|
||||
on:click={() => {
|
||||
showAddMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('Clear memory')}>
|
||||
<button
|
||||
class="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 rounded-lg transition text-red-500"
|
||||
on:click={() => {
|
||||
if ($memories.length > 0) {
|
||||
showClearConfirmDialog = true;
|
||||
} else {
|
||||
toast.error($i18n.t('No memories to clear'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory List -->
|
||||
<div class="flex flex-col space-y-1 max-h-96 overflow-y-auto">
|
||||
{#if loading}
|
||||
<div class="text-center py-8 text-sm text-gray-500">
|
||||
{$i18n.t('Loading...')}
|
||||
</div>
|
||||
{:else if $memories.length === 0}
|
||||
<div class="text-center py-8 text-sm text-gray-500">
|
||||
<div class="mb-2">{$i18n.t('No memories yet')}</div>
|
||||
<button
|
||||
class="text-xs text-gray-600 dark:text-gray-400 hover:underline"
|
||||
on:click={() => {
|
||||
showAddMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Add your first memory')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $memories as memory}
|
||||
<div
|
||||
class="group flex items-start gap-2 p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm text-gray-800 dark:text-gray-200 line-clamp-2 break-words">
|
||||
{memory.content}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
{dayjs(memory.updated_at * 1000).format('ll')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition">
|
||||
<Tooltip content={$i18n.t('Edit')}>
|
||||
<button
|
||||
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-800 rounded-lg"
|
||||
on:click={() => {
|
||||
selectedMemory = memory;
|
||||
showEditMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-800 rounded-lg text-red-500"
|
||||
on:click={() => handleDeleteMemory(memory.id)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="border-b border-gray-100 dark:border-gray-850 my-2"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ConfirmDialog
|
||||
title={$i18n.t('Clear Memory')}
|
||||
message={$i18n.t('Are you sure you want to clear all memories? This action cannot be undone.')}
|
||||
show={showClearConfirmDialog}
|
||||
on:confirm={onClearConfirmed}
|
||||
on:cancel={() => {
|
||||
showClearConfirmDialog = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<AddMemoryModal
|
||||
bind:show={showAddMemoryModal}
|
||||
on:save={async () => {
|
||||
await loadMemories();
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditMemoryModal
|
||||
bind:show={showEditMemoryModal}
|
||||
memory={selectedMemory}
|
||||
on:save={async () => {
|
||||
await loadMemories();
|
||||
}}
|
||||
/>
|
||||
|
|
@ -295,6 +295,8 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- ai-friend 暂时不支持option -->
|
||||
{#if false}
|
||||
{#if focused && (filteredOptions.length > 0 || filteredItems.length > 0)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
|
|
@ -385,4 +387,5 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
};
|
||||
|
||||
const shareModelHandler = async (model) => {
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
|
|
@ -612,7 +612,7 @@
|
|||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
{$i18n.t('Made by CyberLover Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
};
|
||||
|
||||
const shareHandler = async (prompt) => {
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
|
|
@ -408,7 +408,7 @@
|
|||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
{$i18n.t('Made by CyberLover Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
return null;
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Redirecting you to Open WebUI Community'));
|
||||
toast.success($i18n.t('Redirecting you to CyberLover Community'));
|
||||
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
|
|
@ -504,7 +504,7 @@
|
|||
{#if $config?.features.enable_community_sharing}
|
||||
<div class=" my-16">
|
||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||
{$i18n.t('Made by Open WebUI Community')}
|
||||
{$i18n.t('Made by CyberLover Community')}
|
||||
</div>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
<div class="my-2">
|
||||
{$i18n.t(
|
||||
'Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.'
|
||||
'Your entire contribution will go directly to the plugin developer; CyberLover does not take any percentage. However, the chosen funding platform might have its own fees.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { browser, dev } from '$app/environment';
|
||||
// import { version } from '../../package.json';
|
||||
|
||||
export const APP_NAME = 'Open WebUI';
|
||||
export const APP_NAME = 'CyberLover';
|
||||
|
||||
export const WEBUI_HOSTNAME = browser ? (dev ? `${location.hostname}:8080` : ``) : '';
|
||||
export const WEBUI_BASE_URL = browser ? (dev ? `http://${WEBUI_HOSTNAME}` : ``) : ``;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"Add text content": "",
|
||||
"Add User": "اضافة مستخدم",
|
||||
"Add User Group": "",
|
||||
"Add your first memory": "",
|
||||
"Additional Config": "",
|
||||
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
|
||||
"Additional Parameters": "",
|
||||
|
|
@ -337,7 +338,7 @@
|
|||
"Copy Link": "أنسخ الرابط",
|
||||
"Copy to clipboard": "",
|
||||
"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح",
|
||||
"CORS must be properly configured by the provider to allow requests from Open WebUI.": "",
|
||||
"CORS must be properly configured by the provider to allow requests from CyberLover.": "",
|
||||
"Create": "",
|
||||
"Create a knowledge base": "",
|
||||
"Create a model": "إنشاء نموذج",
|
||||
|
|
@ -365,6 +366,10 @@
|
|||
"Custom description enabled": "",
|
||||
"Custom Parameter Name": "",
|
||||
"Custom Parameter Value": "",
|
||||
"CyberLover can use tools provided by any OpenAPI server.": "",
|
||||
"CyberLover uses faster-whisper internally.": "",
|
||||
"CyberLover uses SpeechT5 and CMU Arctic speaker embeddings.": "",
|
||||
"CyberLover version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"Danger Zone": "",
|
||||
"Dark": "مظلم",
|
||||
"Data Controls": "",
|
||||
|
|
@ -432,7 +437,7 @@
|
|||
"Discover a model": "اكتشف نموذجا",
|
||||
"Discover a prompt": "اكتشاف موجه",
|
||||
"Discover a tool": "",
|
||||
"Discover how to use Open WebUI and seek support from the community.": "",
|
||||
"Discover how to use CyberLover and seek support from the community.": "",
|
||||
"Discover wonders": "",
|
||||
"Discover, download, and explore custom functions": "",
|
||||
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
||||
|
|
@ -694,6 +699,7 @@
|
|||
"Fade Effect for Streaming Text": "",
|
||||
"Failed to add file.": "",
|
||||
"Failed to connect to {{URL}} OpenAPI tool server": "",
|
||||
"Failed to convert OpenAI chats": "",
|
||||
"Failed to copy link": "",
|
||||
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
||||
"Failed to delete note": "",
|
||||
|
|
@ -704,6 +710,7 @@
|
|||
"Failed to import models": "",
|
||||
"Failed to load chat preview": "",
|
||||
"Failed to load file content.": "",
|
||||
"Failed to load memories": "",
|
||||
"Failed to move chat": "",
|
||||
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
||||
"Failed to render diagram": "",
|
||||
|
|
@ -958,7 +965,7 @@
|
|||
"Lost": "",
|
||||
"Low": "",
|
||||
"LTR": "من جهة اليسار إلى اليمين",
|
||||
"Made by Open WebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||
"Made by CyberLover Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||
"Make password visible in the user interface": "",
|
||||
"Make sure to enclose them with": "تأكد من إرفاقها",
|
||||
"Make sure to export a workflow.json file as API format from ComfyUI.": "",
|
||||
|
|
@ -981,7 +988,7 @@
|
|||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"May": "مايو",
|
||||
"MCP": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.": "",
|
||||
"Medium": "",
|
||||
"Memories accessible by LLMs will be shown here.": "سيتم عرض الذكريات التي يمكن الوصول إليها بواسطة LLMs هنا.",
|
||||
"Memory": "الذاكرة",
|
||||
|
|
@ -1075,6 +1082,7 @@
|
|||
"No inference engine with management support found": "",
|
||||
"No knowledge found": "",
|
||||
"No memories to clear": "",
|
||||
"No memories yet": "",
|
||||
"No model IDs": "",
|
||||
"No models found": "",
|
||||
"No models selected": "",
|
||||
|
|
@ -1124,6 +1132,7 @@
|
|||
"Only alphanumeric characters and hyphens are allowed": "",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.",
|
||||
"Only collections can be edited, create a new knowledge base to edit/add documents.": "",
|
||||
"Only JSON and ZIP file types are supported.": "",
|
||||
"Only markdown files are allowed": "",
|
||||
"Only select users and groups with permission can access": "",
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
|
||||
|
|
@ -1139,10 +1148,6 @@
|
|||
"Open new chat": "فتح محادثة جديده",
|
||||
"Open Sidebar": "",
|
||||
"Open User Profile Menu": "",
|
||||
"Open WebUI can use tools provided by any OpenAPI server.": "",
|
||||
"Open WebUI uses faster-whisper internally.": "",
|
||||
"Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.": "",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenAI API": "OpenAI API",
|
||||
"OpenAI API Config": "OpenAI API إعدادات",
|
||||
|
|
@ -1262,7 +1267,7 @@
|
|||
"Reasoning Tags": "",
|
||||
"Record": "",
|
||||
"Record voice": "سجل صوت",
|
||||
"Redirecting you to Open WebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Redirecting you to CyberLover Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Reference Chats": "",
|
||||
|
|
@ -1438,7 +1443,7 @@
|
|||
"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
|
||||
"Share": "كشاركة",
|
||||
"Share Chat": "مشاركة الدردشة",
|
||||
"Share to Open WebUI Community": "OpenWebUI شارك في مجتمع",
|
||||
"Share to CyberLover Community": "OpenWebUI شارك في مجتمع",
|
||||
"Share your background and interests": "",
|
||||
"Shared with you": "",
|
||||
"Sharing Permissions": "",
|
||||
|
|
@ -1758,7 +1763,7 @@
|
|||
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
||||
"Your Account": "",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; CyberLover does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"YouTube": "Youtube",
|
||||
"Youtube Language": "",
|
||||
"Youtube Proxy URL": ""
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"Add text content": "إضافة محتوى نصي",
|
||||
"Add User": "إضافة مستخدم",
|
||||
"Add User Group": "إضافة مجموعة مستخدمين",
|
||||
"Add your first memory": "",
|
||||
"Additional Config": "",
|
||||
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
|
||||
"Additional Parameters": "",
|
||||
|
|
@ -337,7 +338,7 @@
|
|||
"Copy Link": "نسخ الرابط",
|
||||
"Copy to clipboard": "نسخ إلى الحافظة",
|
||||
"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح!",
|
||||
"CORS must be properly configured by the provider to allow requests from Open WebUI.": "يجب أن يتم تكوين CORS بشكل صحيح من قبل المزود للسماح بالطلبات من Open WebUI.",
|
||||
"CORS must be properly configured by the provider to allow requests from CyberLover.": "يجب أن يتم تكوين CORS بشكل صحيح من قبل المزود للسماح بالطلبات من CyberLover.",
|
||||
"Create": "إنشاء",
|
||||
"Create a knowledge base": "إنشاء قاعدة معرفة",
|
||||
"Create a model": "إنشاء نموذج",
|
||||
|
|
@ -365,6 +366,10 @@
|
|||
"Custom description enabled": "",
|
||||
"Custom Parameter Name": "",
|
||||
"Custom Parameter Value": "",
|
||||
"CyberLover can use tools provided by any OpenAPI server.": "",
|
||||
"CyberLover uses faster-whisper internally.": "تستخدم واجهة WebUI أداة faster-whisper داخليًا.",
|
||||
"CyberLover uses SpeechT5 and CMU Arctic speaker embeddings.": "تستخدم WebUI نموذج SpeechT5 وتضمينات صوتية من CMU Arctic.",
|
||||
"CyberLover version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "إصدار WebUI الحالي (v{{OPEN_WEBUI_VERSION}}) أقل من الإصدار المطلوب (v{{REQUIRED_VERSION}})",
|
||||
"Danger Zone": "منطقة الخطر",
|
||||
"Dark": "داكن",
|
||||
"Data Controls": "",
|
||||
|
|
@ -432,7 +437,7 @@
|
|||
"Discover a model": "اكتشف نموذجا",
|
||||
"Discover a prompt": "اكتشاف موجه",
|
||||
"Discover a tool": "اكتشف أداة",
|
||||
"Discover how to use Open WebUI and seek support from the community.": "اكتشف كيفية استخدام Open WebUI واطلب الدعم من المجتمع.",
|
||||
"Discover how to use CyberLover and seek support from the community.": "اكتشف كيفية استخدام CyberLover واطلب الدعم من المجتمع.",
|
||||
"Discover wonders": "اكتشف العجائب",
|
||||
"Discover, download, and explore custom functions": "اكتشف، حمّل، واستعرض الوظائف المخصصة",
|
||||
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
||||
|
|
@ -694,6 +699,7 @@
|
|||
"Fade Effect for Streaming Text": "",
|
||||
"Failed to add file.": "فشل في إضافة الملف.",
|
||||
"Failed to connect to {{URL}} OpenAPI tool server": "",
|
||||
"Failed to convert OpenAI chats": "",
|
||||
"Failed to copy link": "",
|
||||
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
||||
"Failed to delete note": "",
|
||||
|
|
@ -704,6 +710,7 @@
|
|||
"Failed to import models": "",
|
||||
"Failed to load chat preview": "",
|
||||
"Failed to load file content.": "",
|
||||
"Failed to load memories": "",
|
||||
"Failed to move chat": "",
|
||||
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
||||
"Failed to render diagram": "",
|
||||
|
|
@ -958,7 +965,7 @@
|
|||
"Lost": "ضائع",
|
||||
"Low": "",
|
||||
"LTR": "من جهة اليسار إلى اليمين",
|
||||
"Made by Open WebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||
"Made by CyberLover Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||
"Make password visible in the user interface": "",
|
||||
"Make sure to enclose them with": "تأكد من إرفاقها",
|
||||
"Make sure to export a workflow.json file as API format from ComfyUI.": "تأكد من تصدير ملف workflow.json بصيغة API من ComfyUI.",
|
||||
|
|
@ -981,7 +988,7 @@
|
|||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"May": "مايو",
|
||||
"MCP": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.": "",
|
||||
"Medium": "",
|
||||
"Memories accessible by LLMs will be shown here.": "سيتم عرض الذكريات التي يمكن الوصول إليها بواسطة LLMs هنا.",
|
||||
"Memory": "الذاكرة",
|
||||
|
|
@ -1075,6 +1082,7 @@
|
|||
"No inference engine with management support found": "لم يتم العثور على محرك استدلال يدعم الإدارة",
|
||||
"No knowledge found": "لم يتم العثور على معرفة",
|
||||
"No memories to clear": "لا توجد ذاكرة لمسحها",
|
||||
"No memories yet": "",
|
||||
"No model IDs": "لا توجد معرّفات نماذج",
|
||||
"No models found": "لم يتم العثور على نماذج",
|
||||
"No models selected": "لم يتم اختيار نماذج",
|
||||
|
|
@ -1124,6 +1132,7 @@
|
|||
"Only alphanumeric characters and hyphens are allowed": "يُسمح فقط بالحروف والأرقام والواصلات",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.",
|
||||
"Only collections can be edited, create a new knowledge base to edit/add documents.": "يمكن تعديل المجموعات فقط، أنشئ قاعدة معرفة جديدة لتعديل أو إضافة مستندات.",
|
||||
"Only JSON and ZIP file types are supported.": "",
|
||||
"Only markdown files are allowed": "",
|
||||
"Only select users and groups with permission can access": "يمكن الوصول فقط من قبل المستخدمين والمجموعات المصرح لهم",
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
|
||||
|
|
@ -1139,10 +1148,6 @@
|
|||
"Open new chat": "فتح محادثة جديده",
|
||||
"Open Sidebar": "",
|
||||
"Open User Profile Menu": "",
|
||||
"Open WebUI can use tools provided by any OpenAPI server.": "",
|
||||
"Open WebUI uses faster-whisper internally.": "تستخدم واجهة WebUI أداة faster-whisper داخليًا.",
|
||||
"Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.": "تستخدم WebUI نموذج SpeechT5 وتضمينات صوتية من CMU Arctic.",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "إصدار WebUI الحالي (v{{OPEN_WEBUI_VERSION}}) أقل من الإصدار المطلوب (v{{REQUIRED_VERSION}})",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenAI API": "OpenAI API",
|
||||
"OpenAI API Config": "OpenAI API إعدادات",
|
||||
|
|
@ -1262,7 +1267,7 @@
|
|||
"Reasoning Tags": "",
|
||||
"Record": "",
|
||||
"Record voice": "سجل صوت",
|
||||
"Redirecting you to Open WebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Redirecting you to CyberLover Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "يقلل من احتمال توليد إجابات غير منطقية. القيم الأعلى (مثل 100) تعطي إجابات أكثر تنوعًا، بينما القيم الأدنى (مثل 10) تكون أكثر تحفظًا.",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "أشر إلى نفسك باسم \"المستخدم\" (مثل: \"المستخدم يتعلم الإسبانية\")",
|
||||
"Reference Chats": "",
|
||||
|
|
@ -1438,7 +1443,7 @@
|
|||
"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
|
||||
"Share": "كشاركة",
|
||||
"Share Chat": "مشاركة الدردشة",
|
||||
"Share to Open WebUI Community": "OpenWebUI شارك في مجتمع",
|
||||
"Share to CyberLover Community": "OpenWebUI شارك في مجتمع",
|
||||
"Share your background and interests": "",
|
||||
"Shared with you": "",
|
||||
"Sharing Permissions": "",
|
||||
|
|
@ -1758,7 +1763,7 @@
|
|||
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
||||
"Your Account": "",
|
||||
"Your account status is currently pending activation.": "حالة حسابك حالياً بانتظار التفعيل.",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "سيتم توجيه كامل مساهمتك مباشرة إلى مطور المكون الإضافي؛ لا تأخذ Open WebUI أي نسبة. ومع ذلك، قد تفرض منصة التمويل المختارة رسومًا خاصة بها.",
|
||||
"Your entire contribution will go directly to the plugin developer; CyberLover does not take any percentage. However, the chosen funding platform might have its own fees.": "سيتم توجيه كامل مساهمتك مباشرة إلى مطور المكون الإضافي؛ لا تأخذ CyberLover أي نسبة. ومع ذلك، قد تفرض منصة التمويل المختارة رسومًا خاصة بها.",
|
||||
"YouTube": "Youtube",
|
||||
"Youtube Language": "لغة YouTube",
|
||||
"Youtube Proxy URL": "رابط بروكسي YouTube"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"Add text content": "Добавяне на текстово съдържание",
|
||||
"Add User": "Добавяне на потребител",
|
||||
"Add User Group": "Добавяне на потребителска група",
|
||||
"Add your first memory": "",
|
||||
"Additional Config": "",
|
||||
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
|
||||
"Additional Parameters": "",
|
||||
|
|
@ -337,7 +338,7 @@
|
|||
"Copy Link": "Копиране на връзка",
|
||||
"Copy to clipboard": "Копиране в клипборда",
|
||||
"Copying to clipboard was successful!": "Копирането в клипборда беше успешно!",
|
||||
"CORS must be properly configured by the provider to allow requests from Open WebUI.": "CORS трябва да бъде правилно конфигуриран от доставчика, за да позволи заявки от Open WebUI.",
|
||||
"CORS must be properly configured by the provider to allow requests from CyberLover.": "CORS трябва да бъде правилно конфигуриран от доставчика, за да позволи заявки от CyberLover.",
|
||||
"Create": "Създай",
|
||||
"Create a knowledge base": "Създаване на база знания",
|
||||
"Create a model": "Създаване на модел",
|
||||
|
|
@ -365,6 +366,10 @@
|
|||
"Custom description enabled": "",
|
||||
"Custom Parameter Name": "",
|
||||
"Custom Parameter Value": "",
|
||||
"CyberLover can use tools provided by any OpenAPI server.": "",
|
||||
"CyberLover uses faster-whisper internally.": "CyberLover използва вътрешно по-бързо-whisper.",
|
||||
"CyberLover uses SpeechT5 and CMU Arctic speaker embeddings.": "CyberLover използва SpeechT5 и CMU Arctic говорителни вграждания.",
|
||||
"CyberLover version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Версията на CyberLover (v{{OPEN_WEBUI_VERSION}}) е по-ниска от необходимата версия (v{{REQUIRED_VERSION}})",
|
||||
"Danger Zone": "",
|
||||
"Dark": "Тъмен",
|
||||
"Data Controls": "",
|
||||
|
|
@ -432,7 +437,7 @@
|
|||
"Discover a model": "Открийте модел",
|
||||
"Discover a prompt": "Откриване на промпт",
|
||||
"Discover a tool": "Открийте инструмент",
|
||||
"Discover how to use Open WebUI and seek support from the community.": "Открийте как да използвате Open WebUI и потърсете подкрепа от общността.",
|
||||
"Discover how to use CyberLover and seek support from the community.": "Открийте как да използвате CyberLover и потърсете подкрепа от общността.",
|
||||
"Discover wonders": "Открийте чудеса",
|
||||
"Discover, download, and explore custom functions": "Открийте, изтеглете и разгледайте персонализирани функции",
|
||||
"Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове",
|
||||
|
|
@ -694,6 +699,7 @@
|
|||
"Fade Effect for Streaming Text": "",
|
||||
"Failed to add file.": "Неуспешно добавяне на файл.",
|
||||
"Failed to connect to {{URL}} OpenAPI tool server": "",
|
||||
"Failed to convert OpenAI chats": "",
|
||||
"Failed to copy link": "",
|
||||
"Failed to create API Key.": "Неуспешно създаване на API ключ.",
|
||||
"Failed to delete note": "",
|
||||
|
|
@ -704,6 +710,7 @@
|
|||
"Failed to import models": "",
|
||||
"Failed to load chat preview": "",
|
||||
"Failed to load file content.": "",
|
||||
"Failed to load memories": "",
|
||||
"Failed to move chat": "",
|
||||
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
|
||||
"Failed to render diagram": "",
|
||||
|
|
@ -958,7 +965,7 @@
|
|||
"Lost": "Изгубено",
|
||||
"Low": "",
|
||||
"LTR": "LTR",
|
||||
"Made by Open WebUI Community": "Направено от OpenWebUI общността",
|
||||
"Made by CyberLover Community": "Направено от OpenWebUI общността",
|
||||
"Make password visible in the user interface": "",
|
||||
"Make sure to enclose them with": "Уверете се, че сте заключени с",
|
||||
"Make sure to export a workflow.json file as API format from ComfyUI.": "Уверете се, че експортирате файл workflow.json като API формат от ComfyUI.",
|
||||
|
|
@ -981,7 +988,7 @@
|
|||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модела могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
|
||||
"May": "Май",
|
||||
"MCP": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.": "",
|
||||
"Medium": "",
|
||||
"Memories accessible by LLMs will be shown here.": "Мемории достъпни от LLMs ще бъдат показани тук.",
|
||||
"Memory": "Памет",
|
||||
|
|
@ -1075,6 +1082,7 @@
|
|||
"No inference engine with management support found": "Не е намерен механизъм за извод с поддръжка на управлението",
|
||||
"No knowledge found": "Не са намерени знания",
|
||||
"No memories to clear": "",
|
||||
"No memories yet": "",
|
||||
"No model IDs": "Няма ИД-та на моделите",
|
||||
"No models found": "Не са намерени модели",
|
||||
"No models selected": "Няма избрани модели",
|
||||
|
|
@ -1124,6 +1132,7 @@
|
|||
"Only alphanumeric characters and hyphens are allowed": "Разрешени са само буквено-цифрови знаци и тирета",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерични знаци и тире са разрешени в командния низ.",
|
||||
"Only collections can be edited, create a new knowledge base to edit/add documents.": "Само колекциите могат да бъдат редактирани, създайте нова база от знания, за да редактирате/добавяте документи.",
|
||||
"Only JSON and ZIP file types are supported.": "",
|
||||
"Only markdown files are allowed": "",
|
||||
"Only select users and groups with permission can access": "Само избрани потребители и групи с разрешение могат да имат достъп",
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
|
||||
|
|
@ -1139,10 +1148,6 @@
|
|||
"Open new chat": "Отвори нов чат",
|
||||
"Open Sidebar": "",
|
||||
"Open User Profile Menu": "",
|
||||
"Open WebUI can use tools provided by any OpenAPI server.": "",
|
||||
"Open WebUI uses faster-whisper internally.": "Open WebUI използва вътрешно по-бързо-whisper.",
|
||||
"Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.": "Open WebUI използва SpeechT5 и CMU Arctic говорителни вграждания.",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Версията на Open WebUI (v{{OPEN_WEBUI_VERSION}}) е по-ниска от необходимата версия (v{{REQUIRED_VERSION}})",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenAI API": "API на OpenAI",
|
||||
"OpenAI API Config": "OpenAI API конфигурация",
|
||||
|
|
@ -1262,7 +1267,7 @@
|
|||
"Reasoning Tags": "",
|
||||
"Record": "Запиши",
|
||||
"Record voice": "Записване на глас",
|
||||
"Redirecting you to Open WebUI Community": "Пренасочване към OpenWebUI общността",
|
||||
"Redirecting you to CyberLover Community": "Пренасочване към OpenWebUI общността",
|
||||
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Отнасяйте се към себе си като \"Потребител\" (напр. \"Потребителят учи испански\")",
|
||||
"Reference Chats": "",
|
||||
|
|
@ -1434,7 +1439,7 @@
|
|||
"Settings saved successfully!": "Настройките са запазени успешно!",
|
||||
"Share": "Подели",
|
||||
"Share Chat": "Подели Чат",
|
||||
"Share to Open WebUI Community": "Споделете с OpenWebUI Общността",
|
||||
"Share to CyberLover Community": "Споделете с OpenWebUI Общността",
|
||||
"Share your background and interests": "",
|
||||
"Shared with you": "",
|
||||
"Sharing Permissions": "Права за споделяне",
|
||||
|
|
@ -1754,7 +1759,7 @@
|
|||
"You're now logged in.": "Сега вие влязохте в системата.",
|
||||
"Your Account": "",
|
||||
"Your account status is currently pending activation.": "Статусът на вашия акаунт в момента очаква активиране.",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Цялата ви вноска ще отиде директно при разработчика на плъгина; Open WebUI не взима никакъв процент. Въпреки това, избраната платформа за финансиране може да има свои собствени такси.",
|
||||
"Your entire contribution will go directly to the plugin developer; CyberLover does not take any percentage. However, the chosen funding platform might have its own fees.": "Цялата ви вноска ще отиде директно при разработчика на плъгина; CyberLover не взима никакъв процент. Въпреки това, избраната платформа за финансиране може да има свои собствени такси.",
|
||||
"YouTube": "Youtube",
|
||||
"Youtube Language": "Youtube език",
|
||||
"Youtube Proxy URL": "Youtube Прокси URL"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"Add text content": "",
|
||||
"Add User": "ইউজার যোগ করুন",
|
||||
"Add User Group": "",
|
||||
"Add your first memory": "",
|
||||
"Additional Config": "",
|
||||
"Additional configuration options for marker. This should be a JSON string with key-value pairs. For example, '{\"key\": \"value\"}'. Supported keys include: disable_links, keep_pageheader_in_output, keep_pagefooter_in_output, filter_blank_pages, drop_repeated_text, layout_coverage_threshold, merge_threshold, height_tolerance, gap_threshold, image_threshold, min_line_length, level_count, default_level": "",
|
||||
"Additional Parameters": "",
|
||||
|
|
@ -337,7 +338,7 @@
|
|||
"Copy Link": "লিংক কপি করুন",
|
||||
"Copy to clipboard": "",
|
||||
"Copying to clipboard was successful!": "ক্লিপবোর্ডে কপি করা সফল হয়েছে",
|
||||
"CORS must be properly configured by the provider to allow requests from Open WebUI.": "",
|
||||
"CORS must be properly configured by the provider to allow requests from CyberLover.": "",
|
||||
"Create": "",
|
||||
"Create a knowledge base": "",
|
||||
"Create a model": "একটি মডেল তৈরি করুন",
|
||||
|
|
@ -365,6 +366,10 @@
|
|||
"Custom description enabled": "",
|
||||
"Custom Parameter Name": "",
|
||||
"Custom Parameter Value": "",
|
||||
"CyberLover can use tools provided by any OpenAPI server.": "",
|
||||
"CyberLover uses faster-whisper internally.": "",
|
||||
"CyberLover uses SpeechT5 and CMU Arctic speaker embeddings.": "",
|
||||
"CyberLover version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"Danger Zone": "",
|
||||
"Dark": "ডার্ক",
|
||||
"Data Controls": "",
|
||||
|
|
@ -432,7 +437,7 @@
|
|||
"Discover a model": "একটি মডেল আবিষ্কার করুন",
|
||||
"Discover a prompt": "একটি প্রম্পট খুঁজে বের করুন",
|
||||
"Discover a tool": "",
|
||||
"Discover how to use Open WebUI and seek support from the community.": "",
|
||||
"Discover how to use CyberLover and seek support from the community.": "",
|
||||
"Discover wonders": "",
|
||||
"Discover, download, and explore custom functions": "",
|
||||
"Discover, download, and explore custom prompts": "কাস্টম প্রম্পটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন",
|
||||
|
|
@ -694,6 +699,7 @@
|
|||
"Fade Effect for Streaming Text": "",
|
||||
"Failed to add file.": "",
|
||||
"Failed to connect to {{URL}} OpenAPI tool server": "",
|
||||
"Failed to convert OpenAI chats": "",
|
||||
"Failed to copy link": "",
|
||||
"Failed to create API Key.": "API Key তৈরি করা যায়নি।",
|
||||
"Failed to delete note": "",
|
||||
|
|
@ -704,6 +710,7 @@
|
|||
"Failed to import models": "",
|
||||
"Failed to load chat preview": "",
|
||||
"Failed to load file content.": "",
|
||||
"Failed to load memories": "",
|
||||
"Failed to move chat": "",
|
||||
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
|
||||
"Failed to render diagram": "",
|
||||
|
|
@ -958,7 +965,7 @@
|
|||
"Lost": "",
|
||||
"Low": "",
|
||||
"LTR": "LTR",
|
||||
"Made by Open WebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
|
||||
"Made by CyberLover Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
|
||||
"Make password visible in the user interface": "",
|
||||
"Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না",
|
||||
"Make sure to export a workflow.json file as API format from ComfyUI.": "",
|
||||
|
|
@ -981,7 +988,7 @@
|
|||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "একসঙ্গে সর্বোচ্চ তিনটি মডেল ডাউনলোড করা যায়। দয়া করে পরে আবার চেষ্টা করুন।",
|
||||
"May": "মে",
|
||||
"MCP": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "",
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.": "",
|
||||
"Medium": "",
|
||||
"Memories accessible by LLMs will be shown here.": "LLMs দ্বারা অ্যাক্সেসযোগ্য মেমোরিগুলি এখানে দেখানো হবে।",
|
||||
"Memory": "মেমোরি",
|
||||
|
|
@ -1075,6 +1082,7 @@
|
|||
"No inference engine with management support found": "",
|
||||
"No knowledge found": "",
|
||||
"No memories to clear": "",
|
||||
"No memories yet": "",
|
||||
"No model IDs": "",
|
||||
"No models found": "",
|
||||
"No models selected": "",
|
||||
|
|
@ -1124,6 +1132,7 @@
|
|||
"Only alphanumeric characters and hyphens are allowed": "",
|
||||
"Only alphanumeric characters and hyphens are allowed in the command string.": "কমান্ড স্ট্রিং-এ শুধুমাত্র ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন ব্যবহার করা যাবে।",
|
||||
"Only collections can be edited, create a new knowledge base to edit/add documents.": "",
|
||||
"Only JSON and ZIP file types are supported.": "",
|
||||
"Only markdown files are allowed": "",
|
||||
"Only select users and groups with permission can access": "",
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
|
||||
|
|
@ -1139,10 +1148,6 @@
|
|||
"Open new chat": "নতুন চ্যাট খুলুন",
|
||||
"Open Sidebar": "",
|
||||
"Open User Profile Menu": "",
|
||||
"Open WebUI can use tools provided by any OpenAPI server.": "",
|
||||
"Open WebUI uses faster-whisper internally.": "",
|
||||
"Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.": "",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenAI API": "OpenAI এপিআই",
|
||||
"OpenAI API Config": "OpenAI এপিআই কনফিগ",
|
||||
|
|
@ -1262,7 +1267,7 @@
|
|||
"Reasoning Tags": "",
|
||||
"Record": "",
|
||||
"Record voice": "ভয়েস রেকর্ড করুন",
|
||||
"Redirecting you to Open WebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
|
||||
"Redirecting you to CyberLover Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
|
||||
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Reference Chats": "",
|
||||
|
|
@ -1434,7 +1439,7 @@
|
|||
"Settings saved successfully!": "সেটিংগুলো সফলভাবে সংরক্ষিত হয়েছে",
|
||||
"Share": "শেয়ার করুন",
|
||||
"Share Chat": "চ্যাট শেয়ার করুন",
|
||||
"Share to Open WebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
|
||||
"Share to CyberLover Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
|
||||
"Share your background and interests": "",
|
||||
"Shared with you": "",
|
||||
"Sharing Permissions": "",
|
||||
|
|
@ -1754,7 +1759,7 @@
|
|||
"You're now logged in.": "আপনি এখন লগইন করা অবস্থায় আছেন",
|
||||
"Your Account": "",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"Your entire contribution will go directly to the plugin developer; CyberLover does not take any percentage. However, the chosen funding platform might have its own fees.": "",
|
||||
"YouTube": "YouTube",
|
||||
"Youtube Language": "",
|
||||
"Youtube Proxy URL": ""
|
||||
|
|
|
|||