From cc3a91d009df9a9b273b93c21b46c85bce692217 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 26 Mar 2026 18:25:57 +0800 Subject: [PATCH] Opensandbox (#6657) * Opensandbox (#6651) * volumn manager * feat: opensandbox volumn * perf: action (#6654) * perf: action * doc * doc * deploy tml * update template --- .claude/CLAUDE.md | 10 +- .github/workflows/build-agent-sandbox.yml | 255 ++++++++++++++ ...ild-sandbox.yml => build-code-sandbox.yml} | 55 ++- .github/workflows/build-fastgpt.yml | 13 +- .github/workflows/build-marketplace.yml | 10 +- .github/workflows/build-mcp-server.yml | 13 +- .github/workflows/preview-fastgpt-build.yml | 10 +- .github/workflows/test-sandbox.yaml | 4 +- deploy/args.json | 4 +- deploy/dev/docker-compose.cn.yml | 2 +- deploy/dev/docker-compose.yml | 2 +- deploy/docker/cn/docker-compose.milvus.yml | 5 +- deploy/docker/cn/docker-compose.oceanbase.yml | 5 +- deploy/docker/cn/docker-compose.pg.yml | 5 +- deploy/docker/cn/docker-compose.seekdb.yml | 5 +- deploy/docker/cn/docker-compose.zilliz.yml | 5 +- .../docker/global/docker-compose.milvus.yml | 5 +- .../global/docker-compose.oceanbase.yml | 5 +- deploy/docker/global/docker-compose.pg.yml | 5 +- .../docker/global/docker-compose.seekdb.yml | 5 +- .../docker/global/docker-compose.ziliiz.yml | 5 +- deploy/templates/docker-compose.prod.yml | 1 + .../docs/self-host/upgrading/4-14/41410.mdx | 8 +- document/data/doc-last-modified.json | 4 +- .../docker/cn/docker-compose.milvus.yml | 5 +- .../docker/cn/docker-compose.oceanbase.yml | 5 +- .../deploy/docker/cn/docker-compose.pg.yml | 5 +- .../docker/cn/docker-compose.seekdb.yml | 5 +- .../docker/cn/docker-compose.zilliz.yml | 5 +- .../docker/global/docker-compose.milvus.yml | 5 +- .../global/docker-compose.oceanbase.yml | 5 +- .../docker/global/docker-compose.pg.yml | 5 +- .../docker/global/docker-compose.seekdb.yml | 5 +- .../docker/global/docker-compose.ziliiz.yml | 5 +- packages/service/core/ai/sandbox/config.ts | 130 +++++++ .../service/core/ai/sandbox/controller.ts | 183 ++++++---- packages/service/core/ai/sandbox/schema.ts | 6 + packages/service/core/ai/sandbox/type.ts | 27 +- .../dispatch/ai/agent/sub/sandbox/index.ts | 4 +- .../workflow/dispatch/ai/tool/toolCall.ts | 5 +- packages/service/env.ts | 13 +- packages/service/package.json | 2 +- pnpm-lock.yaml | 328 ++++++++++++++---- .../base => agent-sandbox}/Dockerfile | 6 +- projects/agent-sandbox/entrypoint.sh | 27 ++ .../base => agent-sandbox}/settings.json | 0 projects/app/.env.template | 14 + .../src/pages/api/core/ai/sandbox/download.ts | 4 +- .../app/src/pages/api/core/ai/sandbox/file.ts | 5 +- .../{sandbox => code-sandbox}/.dockerignore | 0 .../{sandbox => code-sandbox}/.env.template | 0 projects/{sandbox => code-sandbox}/.gitignore | 0 projects/{sandbox => code-sandbox}/Dockerfile | 10 +- projects/{sandbox => code-sandbox}/README.md | 6 +- projects/{sandbox => code-sandbox}/build.sh | 0 .../{sandbox => code-sandbox}/package.json | 2 +- .../requirements.txt | 0 .../{sandbox => code-sandbox}/src/config.ts | 0 projects/{sandbox => code-sandbox}/src/env.ts | 0 .../{sandbox => code-sandbox}/src/index.ts | 0 .../src/pool/base-process-pool.ts | 0 .../src/pool/process-pool.ts | 0 .../src/pool/python-process-pool.ts | 0 .../src/pool/worker.py | 0 .../src/pool/worker.ts | 0 .../{sandbox => code-sandbox}/src/types.ts | 0 .../src/utils/index.ts | 0 .../src/utils/logger.ts | 0 .../src/utils/semaphore.ts | 0 .../test/benchmark/bench-sandbox-python.sh | 0 .../test/benchmark/bench-sandbox.sh | 0 .../test/compat/legacy-js.test.ts | 0 .../test/compat/legacy-python.test.ts | 0 .../test/integration/api.test.ts | 0 .../test/integration/functional.test.ts | 0 .../test/unit/boundary.test.ts | 0 .../test/unit/process-pool.test.ts | 0 .../test/unit/resource-limits.test.ts | 0 .../test/unit/security.test.ts | 0 .../test/unit/semaphore.test.ts | 0 .../{sandbox => code-sandbox}/tsconfig.json | 0 .../vitest.config.ts | 0 projects/sandbox-sync-agent/Dockerfile | 32 -- .../Dockerfile.docker-runtime | 35 -- .../sandbox-sync-agent/base/entrypoint.sh | 19 - projects/sandbox-sync-agent/build.sh | 227 ------------ .../sandbox-sync-agent/docker-entrypoint.sh | 19 - projects/sandbox-sync-agent/entrypoint.sh | 16 - projects/sandbox-sync-agent/http_server.py | 65 ---- .../pool-skill-sandbox.yaml | 112 ------ projects/sandbox-sync-agent/supervisord.conf | 24 -- projects/sandbox-sync-agent/sync.sh | 83 ----- projects/volume-manager/.dockerignore | 1 + projects/volume-manager/.env.template | 25 ++ projects/volume-manager/Dockerfile | 15 + projects/volume-manager/README.md | 144 ++++++++ projects/volume-manager/bun.lock | 209 +++++++++++ projects/volume-manager/package.json | 20 ++ .../src/drivers/DockerVolumeDriver.ts | 70 ++++ .../src/drivers/IVolumeDriver.ts | 9 + .../src/drivers/K8sVolumeDriver.ts | 114 ++++++ projects/volume-manager/src/env.ts | 23 ++ projects/volume-manager/src/index.ts | 34 ++ projects/volume-manager/src/routes/volumes.ts | 39 +++ .../src/services/VolumeService.ts | 19 + projects/volume-manager/src/utils/logger.ts | 11 + projects/volume-manager/src/utils/naming.ts | 12 + .../test/unit/DockerVolumeDriver.test.ts | 58 ++++ .../test/unit/K8sVolumeDriver.test.ts | 82 +++++ .../test/unit/VolumeService.test.ts | 44 +++ .../volume-manager/test/unit/naming.test.ts | 38 ++ projects/volume-manager/tsconfig.json | 11 + projects/volume-manager/vitest.config.ts | 7 + .../ai/sandbox/sandbox.integration.test.ts | 39 ++- 114 files changed, 1966 insertions(+), 953 deletions(-) create mode 100644 .github/workflows/build-agent-sandbox.yml rename .github/workflows/{build-sandbox.yml => build-code-sandbox.yml} (68%) create mode 100644 packages/service/core/ai/sandbox/config.ts rename projects/{sandbox-sync-agent/base => agent-sandbox}/Dockerfile (89%) create mode 100644 projects/agent-sandbox/entrypoint.sh rename projects/{sandbox-sync-agent/base => agent-sandbox}/settings.json (100%) rename projects/{sandbox => code-sandbox}/.dockerignore (100%) rename projects/{sandbox => code-sandbox}/.env.template (100%) rename projects/{sandbox => code-sandbox}/.gitignore (100%) rename projects/{sandbox => code-sandbox}/Dockerfile (85%) rename projects/{sandbox => code-sandbox}/README.md (98%) rename projects/{sandbox => code-sandbox}/build.sh (100%) rename projects/{sandbox => code-sandbox}/package.json (97%) rename projects/{sandbox => code-sandbox}/requirements.txt (100%) rename projects/{sandbox => code-sandbox}/src/config.ts (100%) rename projects/{sandbox => code-sandbox}/src/env.ts (100%) rename projects/{sandbox => code-sandbox}/src/index.ts (100%) rename projects/{sandbox => code-sandbox}/src/pool/base-process-pool.ts (100%) rename projects/{sandbox => code-sandbox}/src/pool/process-pool.ts (100%) rename projects/{sandbox => code-sandbox}/src/pool/python-process-pool.ts (100%) rename projects/{sandbox => code-sandbox}/src/pool/worker.py (100%) rename projects/{sandbox => code-sandbox}/src/pool/worker.ts (100%) rename projects/{sandbox => code-sandbox}/src/types.ts (100%) rename projects/{sandbox => code-sandbox}/src/utils/index.ts (100%) rename projects/{sandbox => code-sandbox}/src/utils/logger.ts (100%) rename projects/{sandbox => code-sandbox}/src/utils/semaphore.ts (100%) rename projects/{sandbox => code-sandbox}/test/benchmark/bench-sandbox-python.sh (100%) rename projects/{sandbox => code-sandbox}/test/benchmark/bench-sandbox.sh (100%) rename projects/{sandbox => code-sandbox}/test/compat/legacy-js.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/compat/legacy-python.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/integration/api.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/integration/functional.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/unit/boundary.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/unit/process-pool.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/unit/resource-limits.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/unit/security.test.ts (100%) rename projects/{sandbox => code-sandbox}/test/unit/semaphore.test.ts (100%) rename projects/{sandbox => code-sandbox}/tsconfig.json (100%) rename projects/{sandbox => code-sandbox}/vitest.config.ts (100%) delete mode 100644 projects/sandbox-sync-agent/Dockerfile delete mode 100644 projects/sandbox-sync-agent/Dockerfile.docker-runtime delete mode 100644 projects/sandbox-sync-agent/base/entrypoint.sh delete mode 100755 projects/sandbox-sync-agent/build.sh delete mode 100644 projects/sandbox-sync-agent/docker-entrypoint.sh delete mode 100644 projects/sandbox-sync-agent/entrypoint.sh delete mode 100644 projects/sandbox-sync-agent/http_server.py delete mode 100644 projects/sandbox-sync-agent/pool-skill-sandbox.yaml delete mode 100644 projects/sandbox-sync-agent/supervisord.conf delete mode 100644 projects/sandbox-sync-agent/sync.sh create mode 100644 projects/volume-manager/.dockerignore create mode 100644 projects/volume-manager/.env.template create mode 100644 projects/volume-manager/Dockerfile create mode 100644 projects/volume-manager/README.md create mode 100644 projects/volume-manager/bun.lock create mode 100644 projects/volume-manager/package.json create mode 100644 projects/volume-manager/src/drivers/DockerVolumeDriver.ts create mode 100644 projects/volume-manager/src/drivers/IVolumeDriver.ts create mode 100644 projects/volume-manager/src/drivers/K8sVolumeDriver.ts create mode 100644 projects/volume-manager/src/env.ts create mode 100644 projects/volume-manager/src/index.ts create mode 100644 projects/volume-manager/src/routes/volumes.ts create mode 100644 projects/volume-manager/src/services/VolumeService.ts create mode 100644 projects/volume-manager/src/utils/logger.ts create mode 100644 projects/volume-manager/src/utils/naming.ts create mode 100644 projects/volume-manager/test/unit/DockerVolumeDriver.test.ts create mode 100644 projects/volume-manager/test/unit/K8sVolumeDriver.test.ts create mode 100644 projects/volume-manager/test/unit/VolumeService.test.ts create mode 100644 projects/volume-manager/test/unit/naming.test.ts create mode 100644 projects/volume-manager/tsconfig.json create mode 100644 projects/volume-manager/vitest.config.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 22903ba5a1..f3f34fad8d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -30,7 +30,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ### Projects (应用程序) - `projects/app/` - 主 NextJS Web 应用(前端 + API 路由) -- `projects/sandbox/` - NestJS 代码执行沙箱服务 +- `projects/code-sandbox/` - Bun + Hono 代码执行沙箱服务 - `projects/mcp_server/` - Model Context Protocol 服务器实现 ### 关键目录 @@ -55,10 +55,10 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 - `cd projects/app && pnpm build` - 构建 NextJS 应用 - `cd projects/app && pnpm start` - 启动生产服务器 -**沙箱 (projects/sandbox/)**: -- `cd projects/sandbox && pnpm dev` - 以监视模式启动 NestJS 开发服务器 -- `cd projects/sandbox && pnpm build` - 构建 NestJS 应用 -- `cd projects/sandbox && pnpm test` - 运行 Jest 测试 +**代码沙箱 (projects/code-sandbox/)**: +- `cd projects/code-sandbox && pnpm dev` - 以监视模式启动(Bun) +- `cd projects/code-sandbox && pnpm build` - 构建沙箱服务 +- `cd projects/code-sandbox && pnpm test` - 运行 Vitest 测试 **MCP 服务器 (projects/mcp_server/)**: - `cd projects/mcp_server && bun dev` - 使用 Bun 以监视模式启动 diff --git a/.github/workflows/build-agent-sandbox.yml b/.github/workflows/build-agent-sandbox.yml new file mode 100644 index 0000000000..e9c774d43f --- /dev/null +++ b/.github/workflows/build-agent-sandbox.yml @@ -0,0 +1,255 @@ +name: Build agent-sandbox images +on: + workflow_dispatch: + inputs: + version: + description: 'Image version tag (e.g. v1.0.0)' + required: true + type: string + +jobs: + # ── agent-sandbox ────────────────────────────────────────────────────────── + build-fastgpt-agent-sandbox-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + strategy: + matrix: + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-agent-sandbox-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-agent-sandbox-buildx- + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build for ${{ matrix.arch }} + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: projects/agent-sandbox/Dockerfile + platforms: linux/${{ matrix.arch }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=fastgpt-agent-sandbox image + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-sandbox",push-by-digest=true,push=true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-fastgpt-agent-sandbox-${{ github.sha }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + release-fastgpt-agent-sandbox-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + needs: build-fastgpt-agent-sandbox-images + runs-on: ubuntu-24.04 + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} + password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_NAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-fastgpt-agent-sandbox-${{ github.sha }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set image name and tag + run: | + VERSION="${{ github.event.inputs.version }}" + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-sandbox:${VERSION}" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-sandbox:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-agent-sandbox:${VERSION}" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-agent-sandbox:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-agent-sandbox:${VERSION}" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-agent-sandbox:latest" >> $GITHUB_ENV + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}\n${Docker_Hub_Tag}\n${Docker_Hub_Latest}")" + for TAG in $TAGS; do + docker buildx imagetools create -t $TAG \ + $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-agent-sandbox@sha256:%s ' *) + sleep 5 + done + + # ── volume-manager ───────────────────────────────────────────────────────── + build-fastgpt-volume-manager-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + strategy: + matrix: + include: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on || 'ubuntu-24.04' }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-volume-manager-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-volume-manager-buildx- + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build for ${{ matrix.arch }} + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: projects/volume-manager/Dockerfile + platforms: linux/${{ matrix.arch }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=fastgpt-agent-volume-manager image + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-volume-manager",push-by-digest=true,push=true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-fastgpt-agent-volume-manager-${{ github.sha }}-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + release-fastgpt-volume-manager-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + needs: build-fastgpt-volume-manager-images + runs-on: ubuntu-24.04 + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} + password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_NAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-fastgpt-agent-volume-manager-${{ github.sha }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set image name and tag + run: | + VERSION="${{ github.event.inputs.version }}" + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-volume-manager:${VERSION}" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-agent-volume-manager:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-agent-volume-manager:${VERSION}" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-agent-volume-manager:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-agent-volume-manager:${VERSION}" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-agent-volume-manager:latest" >> $GITHUB_ENV + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}\n${Docker_Hub_Tag}\n${Docker_Hub_Latest}")" + for TAG in $TAGS; do + docker buildx imagetools create -t $TAG \ + $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-agent-volume-manager@sha256:%s ' *) + sleep 5 + done diff --git a/.github/workflows/build-sandbox.yml b/.github/workflows/build-code-sandbox.yml similarity index 68% rename from .github/workflows/build-sandbox.yml rename to .github/workflows/build-code-sandbox.yml index f2f112076e..71a2105aac 100644 --- a/.github/workflows/build-sandbox.yml +++ b/.github/workflows/build-code-sandbox.yml @@ -1,13 +1,13 @@ -name: Build fastgpt-sandbox images +name: Build fastgpt-code-sandbox images on: workflow_dispatch: push: paths: - - 'projects/sandbox/**' + - 'projects/code-sandbox/**' tags: - 'v*' jobs: - build-fastgpt-sandbox-images: + build-fastgpt-code-sandbox-images: permissions: packages: write contents: read @@ -45,23 +45,18 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_NAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Build for ${{ matrix.arch }} id: build uses: docker/build-push-action@v6 with: context: . - file: projects/sandbox/Dockerfile + file: projects/code-sandbox/Dockerfile platforms: linux/${{ matrix.arch }} labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.description=fastgpt-sandbox image - outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox",push-by-digest=true,push=true + org.opencontainers.image.description=fastgpt-code-sandbox image + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox",push-by-digest=true,push=true cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache @@ -74,18 +69,18 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v4 with: - name: digests-fastgpt-sandbox-${{ github.sha }}-${{ matrix.arch }} + name: digests-fastgpt-code-sandbox-${{ github.sha }}-${{ matrix.arch }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 - release-fastgpt-sandbox-images: + release-fastgpt-code-sandbox-images: permissions: packages: write contents: read attestations: write id-token: write - needs: build-fastgpt-sandbox-images + needs: build-fastgpt-code-sandbox-images runs-on: ubuntu-24.04 steps: - name: Login to GitHub Container Registry @@ -100,17 +95,11 @@ jobs: registry: registry.cn-hangzhou.aliyuncs.com username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_NAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Download digests uses: actions/download-artifact@v4 with: path: ${{ runner.temp }}/digests - pattern: digests-fastgpt-sandbox-${{ github.sha }}-* + pattern: digests-fastgpt-code-sandbox-${{ github.sha }}-* merge-multiple: true - name: Set up Docker Buildx @@ -119,27 +108,23 @@ jobs: - name: Set image name and tag run: | if [[ "${{ github.ref_name }}" == "main" ]]; then - echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV else - echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV - echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV - echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV - echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV - echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-code-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-code-sandbox:latest" >> $GITHUB_ENV fi - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | - TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}\n${Docker_Hub_Tag}\n${Docker_Hub_Latest}")" + TAGS="$(echo -e "${Git_Tag}\n${Git_Latest}\n${Ali_Tag}\n${Ali_Latest}")" for TAG in $TAGS; do docker buildx imagetools create -t $TAG \ - $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox@sha256:%s ' *) + $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-code-sandbox@sha256:%s ' *) sleep 5 done diff --git a/.github/workflows/build-fastgpt.yml b/.github/workflows/build-fastgpt.yml index 1f81f438a2..7553ea6d5a 100644 --- a/.github/workflows/build-fastgpt.yml +++ b/.github/workflows/build-fastgpt.yml @@ -57,17 +57,6 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Ali Hub - uses: docker/login-action@v3 - with: - registry: registry.cn-hangzhou.aliyuncs.com - username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} - password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_NAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Build for ${{ matrix.archs.arch }} id: build @@ -81,7 +70,7 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.description=${{ matrix.sub_routes.repo }} image - outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }},${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }},${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}",push-by-digest=true,push=true + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}",push-by-digest=true,push=true cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/build-marketplace.yml b/.github/workflows/build-marketplace.yml index 78dd9bff9e..11599112c9 100644 --- a/.github/workflows/build-marketplace.yml +++ b/.github/workflows/build-marketplace.yml @@ -40,12 +40,6 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Ali Hub - uses: docker/login-action@v3 - with: - registry: registry.cn-hangzhou.aliyuncs.com - username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} - password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} - name: Build for ${{ matrix.arch }} id: build @@ -57,7 +51,7 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.description=fastgpt-marketplace image - outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-marketplace",push-by-digest=true,push=true + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace",push-by-digest=true,push=true cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache @@ -138,7 +132,7 @@ jobs: # Create manifest for Ali Cloud echo "Creating manifest for Ali Cloud: ${Ali_Tag}" docker buildx imagetools create -t ${Ali_Tag} \ - $(printf '${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-marketplace@sha256:%s ' *) + $(printf 'ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace@sha256:%s ' *) echo "✅ Ali Cloud manifest created" echo "" diff --git a/.github/workflows/build-mcp-server.yml b/.github/workflows/build-mcp-server.yml index 6fd9d8736d..591968012c 100644 --- a/.github/workflows/build-mcp-server.yml +++ b/.github/workflows/build-mcp-server.yml @@ -45,17 +45,6 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Ali Hub - uses: docker/login-action@v3 - with: - registry: registry.cn-hangzhou.aliyuncs.com - username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }} - password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_NAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Build for ${{ matrix.arch }} id: build @@ -67,7 +56,7 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.description=fastgpt-mcp_server image - outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server",push-by-digest=true,push=true + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server",push-by-digest=true,push=true cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/preview-fastgpt-build.yml b/.github/workflows/preview-fastgpt-build.yml index 5a96c79eba..9bf92f6400 100644 --- a/.github/workflows/preview-fastgpt-build.yml +++ b/.github/workflows/preview-fastgpt-build.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - image: [fastgpt, sandbox, mcp_server] + image: [fastgpt, code-sandbox, mcp_server] fail-fast: false steps: @@ -35,10 +35,10 @@ jobs: echo "DOCKERFILE=projects/app/Dockerfile" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-pr image" >> $GITHUB_OUTPUT echo "IMAGE_NAME=fastgpt" >> $GITHUB_OUTPUT - elif [[ "${{ matrix.image }}" == "sandbox" ]]; then - echo "DOCKERFILE=projects/sandbox/Dockerfile" >> $GITHUB_OUTPUT - echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT - echo "IMAGE_NAME=fastgpt-sandbox" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.image }}" == "code-sandbox" ]]; then + echo "DOCKERFILE=projects/code-sandbox/Dockerfile" >> $GITHUB_OUTPUT + echo "DESCRIPTION=fastgpt-code-sandbox-pr image" >> $GITHUB_OUTPUT + echo "IMAGE_NAME=fastgpt-code-sandbox" >> $GITHUB_OUTPUT elif [[ "${{ matrix.image }}" == "mcp_server" ]]; then echo "DOCKERFILE=projects/mcp_server/Dockerfile" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-mcp_server-pr image" >> $GITHUB_OUTPUT diff --git a/.github/workflows/test-sandbox.yaml b/.github/workflows/test-sandbox.yaml index aa34a7a114..f62e7943de 100644 --- a/.github/workflows/test-sandbox.yaml +++ b/.github/workflows/test-sandbox.yaml @@ -2,7 +2,7 @@ name: 'Sandbox-Test' on: pull_request: paths: - - 'projects/sandbox/**' + - 'projects/code-sandbox/**' workflow_dispatch: permissions: @@ -31,4 +31,4 @@ jobs: run: pnpm install - name: Run Unit Tests - run: pnpm --filter=sandbox test + run: pnpm --filter=code-sandbox test diff --git a/deploy/args.json b/deploy/args.json index 984f793af7..30a369dbf1 100644 --- a/deploy/args.json +++ b/deploy/args.json @@ -1,7 +1,7 @@ { "tags": { - "fastgpt": "v4.14.9.3", - "fastgpt-sandbox": "v4.14.9.3", + "fastgpt": "v4.14.9.5", + "fastgpt-sandbox": "v4.14.9.5", "fastgpt-mcp_server": "v4.14.9", "fastgpt-plugin": "v0.5.5", "aiproxy": "v0.3.5", diff --git a/deploy/dev/docker-compose.cn.yml b/deploy/dev/docker-compose.cn.yml index fb0b9349c1..648c0b5230 100644 --- a/deploy/dev/docker-compose.cn.yml +++ b/deploy/dev/docker-compose.cn.yml @@ -136,7 +136,7 @@ services: retries: 3 sandbox: container_name: sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 ports: - 3002:3000 networks: diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index 9433afb692..7592f75aef 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -136,7 +136,7 @@ services: retries: 3 sandbox: container_name: sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 ports: - 3002:3000 networks: diff --git a/deploy/docker/cn/docker-compose.milvus.yml b/deploy/docker/cn/docker-compose.milvus.yml index cd56aa0a24..11601b9c64 100644 --- a/deploy/docker/cn/docker-compose.milvus.yml +++ b/deploy/docker/cn/docker-compose.milvus.yml @@ -191,7 +191,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -265,11 +265,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/cn/docker-compose.oceanbase.yml b/deploy/docker/cn/docker-compose.oceanbase.yml index 202227e765..e761d9cf53 100644 --- a/deploy/docker/cn/docker-compose.oceanbase.yml +++ b/deploy/docker/cn/docker-compose.oceanbase.yml @@ -168,7 +168,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -242,11 +242,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/cn/docker-compose.pg.yml b/deploy/docker/cn/docker-compose.pg.yml index f1972465ac..af370d0c64 100644 --- a/deploy/docker/cn/docker-compose.pg.yml +++ b/deploy/docker/cn/docker-compose.pg.yml @@ -149,7 +149,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -223,11 +223,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/cn/docker-compose.seekdb.yml b/deploy/docker/cn/docker-compose.seekdb.yml index d665cddc38..8a4a9aa12d 100644 --- a/deploy/docker/cn/docker-compose.seekdb.yml +++ b/deploy/docker/cn/docker-compose.seekdb.yml @@ -155,7 +155,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -229,11 +229,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/cn/docker-compose.zilliz.yml b/deploy/docker/cn/docker-compose.zilliz.yml index 58a1665939..55c4330f07 100644 --- a/deploy/docker/cn/docker-compose.zilliz.yml +++ b/deploy/docker/cn/docker-compose.zilliz.yml @@ -133,7 +133,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -207,11 +207,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/global/docker-compose.milvus.yml b/deploy/docker/global/docker-compose.milvus.yml index 459a711f76..4c7b2da454 100644 --- a/deploy/docker/global/docker-compose.milvus.yml +++ b/deploy/docker/global/docker-compose.milvus.yml @@ -191,7 +191,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -265,11 +265,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/global/docker-compose.oceanbase.yml b/deploy/docker/global/docker-compose.oceanbase.yml index 356167186e..9ef2bc2356 100644 --- a/deploy/docker/global/docker-compose.oceanbase.yml +++ b/deploy/docker/global/docker-compose.oceanbase.yml @@ -168,7 +168,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -242,11 +242,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/global/docker-compose.pg.yml b/deploy/docker/global/docker-compose.pg.yml index f283cbee86..630a8c93ba 100644 --- a/deploy/docker/global/docker-compose.pg.yml +++ b/deploy/docker/global/docker-compose.pg.yml @@ -149,7 +149,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -223,11 +223,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/global/docker-compose.seekdb.yml b/deploy/docker/global/docker-compose.seekdb.yml index 18088f91b1..02b593350c 100644 --- a/deploy/docker/global/docker-compose.seekdb.yml +++ b/deploy/docker/global/docker-compose.seekdb.yml @@ -155,7 +155,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -229,11 +229,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/docker/global/docker-compose.ziliiz.yml b/deploy/docker/global/docker-compose.ziliiz.yml index 35007ab085..100fb24c85 100644 --- a/deploy/docker/global/docker-compose.ziliiz.yml +++ b/deploy/docker/global/docker-compose.ziliiz.yml @@ -133,7 +133,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -207,11 +207,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/deploy/templates/docker-compose.prod.yml b/deploy/templates/docker-compose.prod.yml index afe13fd50e..0c9461c500 100644 --- a/deploy/templates/docker-compose.prod.yml +++ b/deploy/templates/docker-compose.prod.yml @@ -211,6 +211,7 @@ ${{vec.db}} - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/content/docs/self-host/upgrading/4-14/41410.mdx b/document/content/docs/self-host/upgrading/4-14/41410.mdx index b42ebabbf7..6bc9d6bb95 100644 --- a/document/content/docs/self-host/upgrading/4-14/41410.mdx +++ b/document/content/docs/self-host/upgrading/4-14/41410.mdx @@ -3,11 +3,15 @@ title: 'V4.14.10(进行中)' description: 'FastGPT V4.14.10 更新说明' --- +## 注意 + +1. 代码沙盒镜像名变更: `{{hub}}/fastgpt-sandbox` -> `{{hub}}/fastgpt-code-sandbox` ## 🚀 新增内容 -1. 飞书发布渠道,支持流输出。 -2. 目录最大上限,可通过环境变量配置。 +1. 增加 OpenSandbox docker 部署方案及适配,并支持通过挂载 volumn 进行数据持久化。 +2. 飞书发布渠道,支持流输出。 +3. 目录最大上限,可通过环境变量配置。 ## ⚙️ 优化 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index e3e3fbfd51..170cdc97cb 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -220,7 +220,7 @@ "document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00", - "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-25T14:45:38+08:00", + "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-26T16:35:07+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", @@ -240,7 +240,7 @@ "document/content/docs/self-host/upgrading/4-14/41481.en.mdx": "2026-03-09T12:02:02+08:00", "document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00", "document/content/docs/self-host/upgrading/4-14/4149.en.mdx": "2026-03-23T12:17:04+08:00", - "document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-25T14:45:38+08:00", + "document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-25T20:20:19+08:00", "document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00", diff --git a/document/public/deploy/docker/cn/docker-compose.milvus.yml b/document/public/deploy/docker/cn/docker-compose.milvus.yml index cd56aa0a24..11601b9c64 100644 --- a/document/public/deploy/docker/cn/docker-compose.milvus.yml +++ b/document/public/deploy/docker/cn/docker-compose.milvus.yml @@ -191,7 +191,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -265,11 +265,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/cn/docker-compose.oceanbase.yml b/document/public/deploy/docker/cn/docker-compose.oceanbase.yml index 202227e765..e761d9cf53 100644 --- a/document/public/deploy/docker/cn/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/cn/docker-compose.oceanbase.yml @@ -168,7 +168,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -242,11 +242,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/cn/docker-compose.pg.yml b/document/public/deploy/docker/cn/docker-compose.pg.yml index f1972465ac..af370d0c64 100644 --- a/document/public/deploy/docker/cn/docker-compose.pg.yml +++ b/document/public/deploy/docker/cn/docker-compose.pg.yml @@ -149,7 +149,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -223,11 +223,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/cn/docker-compose.seekdb.yml b/document/public/deploy/docker/cn/docker-compose.seekdb.yml index d665cddc38..8a4a9aa12d 100644 --- a/document/public/deploy/docker/cn/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/cn/docker-compose.seekdb.yml @@ -155,7 +155,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -229,11 +229,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/cn/docker-compose.zilliz.yml b/document/public/deploy/docker/cn/docker-compose.zilliz.yml index 58a1665939..55c4330f07 100644 --- a/document/public/deploy/docker/cn/docker-compose.zilliz.yml +++ b/document/public/deploy/docker/cn/docker-compose.zilliz.yml @@ -133,7 +133,7 @@ services: fastgpt: container_name: fastgpt - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.3 # git + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -207,11 +207,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.3 + image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/global/docker-compose.milvus.yml b/document/public/deploy/docker/global/docker-compose.milvus.yml index 459a711f76..4c7b2da454 100644 --- a/document/public/deploy/docker/global/docker-compose.milvus.yml +++ b/document/public/deploy/docker/global/docker-compose.milvus.yml @@ -191,7 +191,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -265,11 +265,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/global/docker-compose.oceanbase.yml b/document/public/deploy/docker/global/docker-compose.oceanbase.yml index 356167186e..9ef2bc2356 100644 --- a/document/public/deploy/docker/global/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/global/docker-compose.oceanbase.yml @@ -168,7 +168,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -242,11 +242,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/global/docker-compose.pg.yml b/document/public/deploy/docker/global/docker-compose.pg.yml index f283cbee86..630a8c93ba 100644 --- a/document/public/deploy/docker/global/docker-compose.pg.yml +++ b/document/public/deploy/docker/global/docker-compose.pg.yml @@ -149,7 +149,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -223,11 +223,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/global/docker-compose.seekdb.yml b/document/public/deploy/docker/global/docker-compose.seekdb.yml index 18088f91b1..02b593350c 100644 --- a/document/public/deploy/docker/global/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/global/docker-compose.seekdb.yml @@ -155,7 +155,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -229,11 +229,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/document/public/deploy/docker/global/docker-compose.ziliiz.yml b/document/public/deploy/docker/global/docker-compose.ziliiz.yml index 35007ab085..100fb24c85 100644 --- a/document/public/deploy/docker/global/docker-compose.ziliiz.yml +++ b/document/public/deploy/docker/global/docker-compose.ziliiz.yml @@ -133,7 +133,7 @@ services: fastgpt: container_name: fastgpt - image: ghcr.io/labring/fastgpt:v4.14.9.3 # git + image: ghcr.io/labring/fastgpt:v4.14.9.5 # git ports: - 3000:3000 networks: @@ -207,11 +207,12 @@ services: - ./config.json:/app/data/config.json code-sandbox: container_name: code-sandbox - image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.3 + image: ghcr.io/labring/fastgpt-sandbox:v4.14.9.5 networks: - fastgpt restart: always environment: + <<: [*x-log-config] LOG_OTEL_SERVICE_NAME: fastgpt-code-sandbox SANDBOX_TOKEN: *x-code-sandbox-token # ===== Resource Limits ===== diff --git a/packages/service/core/ai/sandbox/config.ts b/packages/service/core/ai/sandbox/config.ts new file mode 100644 index 0000000000..071d8a9697 --- /dev/null +++ b/packages/service/core/ai/sandbox/config.ts @@ -0,0 +1,130 @@ +import { env } from '../../../env'; +import type { + OpenSandboxConfigType, + OpenSandboxConnectionConfig +} from '@fastgpt-sdk/sandbox-adapter'; +import type { SandboxStorageType } from './type'; + +// ---- sealosdevbox ---- +export type SealosConnectionConfig = { + baseUrl: string; + token: string; + sandboxId: string; +}; + +export const getSealosConnectionConfig = (sandboxId: string): SealosConnectionConfig => { + if (!env.AGENT_SANDBOX_SEALOS_BASEURL || !env.AGENT_SANDBOX_SEALOS_TOKEN) { + throw new Error('AGENT_SANDBOX_SEALOS_BASEURL / AGENT_SANDBOX_SEALOS_TOKEN required'); + } + return { + baseUrl: env.AGENT_SANDBOX_SEALOS_BASEURL, + token: env.AGENT_SANDBOX_SEALOS_TOKEN, + sandboxId + }; +}; + +// ---- opensandbox ---- +export const getOpenSandboxConnectionConfig = ({ + sessionId +}: { + sessionId: string; +}): OpenSandboxConnectionConfig => { + if (!env.AGENT_SANDBOX_OPENSANDBOX_BASEURL) { + throw new Error('AGENT_SANDBOX_OPENSANDBOX_BASEURL is required'); + } + return { + sessionId, + useServerProxy: env.AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY, + baseUrl: env.AGENT_SANDBOX_OPENSANDBOX_BASEURL, + apiKey: env.AGENT_SANDBOX_OPENSANDBOX_API_KEY, + runtime: env.AGENT_SANDBOX_OPENSANDBOX_RUNTIME + }; +}; + +export const buildOpenSandboxCreateConfig = ( + opts: { + volumes?: OpenSandboxConfigType['volumes']; + resourceLimits?: OpenSandboxConfigType['resourceLimits']; + } = {} +): OpenSandboxConfigType => { + if (!env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO) { + throw new Error('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO is required for opensandbox provider'); + } + return { + image: { + repository: env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO, + tag: env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG + }, + ...(opts.resourceLimits ? { resourceLimits: opts.resourceLimits } : {}), + ...(opts.volumes ? { volumes: opts.volumes } : {}) + }; +}; + +// ---- volume-manager ---- +export type VolumeManagerConfig = { + url: string; + token?: string; + mountPath: string; +}; +export type VolumeManagerResult = { + volumes: OpenSandboxConfigType['volumes']; + storage: SandboxStorageType; +}; +const vmConfig = { + enable: env.AGENT_SANDBOX_ENABLE_VOLUME, + url: env.AGENT_SANDBOX_VOLUME_MANAGER_URL!, + token: env.AGENT_SANDBOX_VOLUME_MANAGER_TOKEN, + mountPath: env.AGENT_SANDBOX_VOLUME_MANAGER_MOUNT_PATH +}; +export const buildVolumeConfig = (claimName: string, mountPath: string): VolumeManagerResult => { + return { + volumes: [{ name: 'workspace', pvc: { claimName }, mountPath }], + storage: { + volumes: [{ name: 'workspace', claimName, mountPath }], + mountPath + } + }; +}; +export const ensureSessionVolume = async (sessionId: string): Promise => { + const headers: Record = { 'Content-Type': 'application/json' }; + if (vmConfig.token) headers['Authorization'] = `Bearer ${vmConfig.token}`; + + const res = await fetch(`${vmConfig.url}/v1/volumes/ensure`, { + method: 'POST', + headers, + body: JSON.stringify({ sessionId }) + }); + if (!res.ok) { + throw new Error(`volume-manager error: ${res.status} ${await res.text()}`); + } + const { claimName } = (await res.json()) as { claimName: string }; + return claimName; +}; +export const deleteSessionVolume = async (sessionId: string): Promise => { + if (!vmConfig.enable) return; + const headers: Record = {}; + if (vmConfig.token) headers['Authorization'] = `Bearer ${vmConfig.token}`; + + const res = await fetch(`${vmConfig.url}/v1/volumes/${encodeURIComponent(sessionId)}`, { + method: 'DELETE', + headers + }); + if (!res.ok && res.status !== 404) { + throw new Error(`volume-manager error: ${res.status} ${await res.text()}`); + } +}; + +export const getVolumeManagerConfig = async ( + sandboxId: string +): Promise => { + if (!vmConfig.enable) return undefined; + if (!vmConfig.url) { + throw new Error( + 'AGENT_SANDBOX_VOLUME_MANAGER_URL is required when AGENT_SANDBOX_ENABLE_VOLUME=true' + ); + } + const claimName = await ensureSessionVolume(sandboxId); + const volumeResult = buildVolumeConfig(claimName, vmConfig.mountPath); + + return volumeResult; +}; diff --git a/packages/service/core/ai/sandbox/controller.ts b/packages/service/core/ai/sandbox/controller.ts index f91b694299..18868ed445 100644 --- a/packages/service/core/ai/sandbox/controller.ts +++ b/packages/service/core/ai/sandbox/controller.ts @@ -11,6 +11,14 @@ import { type ISandbox, type ResourceLimits } from '@fastgpt-sdk/sandbox-adapter'; +import { + getOpenSandboxConnectionConfig, + getSealosConnectionConfig, + buildOpenSandboxCreateConfig, + getVolumeManagerConfig, + deleteSessionVolume, + type VolumeManagerResult +} from './config'; import { getLogger, LogCategories } from '../../../common/logger'; import { setCron } from '../../../common/system/cron'; import { subMinutes } from 'date-fns'; @@ -32,66 +40,52 @@ export class SandboxClient { readonly provider: ISandbox; constructor( - props: - | { - sandboxId: string; - } - | UnionIdType, - opts: { + private readonly props: { + sandboxId: string; + appId?: string; + userId?: string; + chatId?: string; + }, + private readonly opts: { resourceLimits?: ResourceLimits; - } = {} - ) { - if ('sandboxId' in props) { - this.sandboxId = props.sandboxId; - } else { - this.appId = props.appId; - this.userId = props.userId; - this.chatId = props.chatId; - this.sandboxId = generateSandboxId(this.appId, this.userId, this.chatId); + vmConfig?: VolumeManagerResult | undefined; } + ) { + this.sandboxId = props.sandboxId; + this.appId = props.appId; + this.userId = props.userId; + this.chatId = props.chatId; const providerName = env.AGENT_SANDBOX_PROVIDER; - const params = (() => { - if (providerName === 'sealosdevbox') { - if (!env.AGENT_SANDBOX_SEALOS_BASEURL || !env.AGENT_SANDBOX_SEALOS_TOKEN) { - throw new Error('AGENT_SANDBOX_SEALOS_BASEURL / AGENT_SANDBOX_SEALOS_TOKEN required'); - } - return { - provider: 'sealosdevbox' as const, - config: { - baseUrl: env.AGENT_SANDBOX_SEALOS_BASEURL, - token: env.AGENT_SANDBOX_SEALOS_TOKEN, - sandboxId: this.sandboxId - }, - createConfig: undefined - }; - } else if (providerName === 'opensandbox') { - return { - provider: 'opensandbox' as const, - config: { - baseUrl: env.AGENT_SANDBOX_OPENSANDBOX_BASEURL, - token: env.AGENT_SANDBOX_OPENSANDBOX_TOKEN, - sandboxId: this.sandboxId - } - }; - } else if (providerName === 'e2b') { - return { - provider: 'e2b' as const, - config: { - apiKey: env.AGENT_SANDBOX_E2B_API_KEY, - sandboxId: this.sandboxId - } - }; - } else if (!providerName) { - throw new Error( - 'AGENT_SANDBOX_PROVIDER is not configured. Please set it in your environment variables.' - ); - } else { - throw new Error(`Unsupported sandbox provider: ${env.AGENT_SANDBOX_PROVIDER}`); + if (providerName === 'sealosdevbox') { + const config = getSealosConnectionConfig(this.sandboxId); + this.provider = createSandbox('sealosdevbox', config, undefined); + } else if (providerName === 'opensandbox') { + // volumes 在 ensureAvailable 中异步获取后重建 provider,此处用基础 createConfig + this.provider = createSandbox( + 'opensandbox', + getOpenSandboxConnectionConfig({ sessionId: this.sandboxId }), + buildOpenSandboxCreateConfig({ + resourceLimits: opts?.resourceLimits, + volumes: opts?.vmConfig?.volumes + }) + ); + } else if (providerName === 'e2b') { + if (!env.AGENT_SANDBOX_E2B_API_KEY) { + throw new Error('AGENT_SANDBOX_E2B_API_KEY required'); } - })(); - this.provider = createSandbox(params.provider, params.config, params.createConfig); + this.provider = createSandbox('e2b', { + apiKey: env.AGENT_SANDBOX_E2B_API_KEY, + sandboxId: this.sandboxId + }); + } else if (!providerName) { + throw new Error( + 'AGENT_SANDBOX_PROVIDER is not configured. Please set it in your environment variables.' + ); + } else { + throw new Error(`Unsupported sandbox provider: ${env.AGENT_SANDBOX_PROVIDER}`); + } } async ensureAvailable() { @@ -106,6 +100,18 @@ export class SandboxClient { ...(this.appId ? { appId: this.appId } : {}), ...(this.userId ? { userId: this.userId } : {}), ...(this.chatId ? { chatId: this.chatId } : {}), + storage: this.opts?.vmConfig?.storage, + ...(this.opts?.resourceLimits && { + limit: { + cpuCount: this.opts?.resourceLimits?.cpuCount, + memoryMiB: this.opts?.resourceLimits?.memoryMiB, + diskGiB: this.opts?.resourceLimits?.diskGiB + } + }), + metadata: { + sessionKey: this.sandboxId, + volumeEnabled: !!this.opts?.vmConfig + }, createdAt: new Date() } }, @@ -142,6 +148,9 @@ export class SandboxClient { async delete() { await this.provider.delete(); + await deleteSessionVolume(this.sandboxId).catch((err) => { + logger.error('Failed to delete sandbox volume', { sandboxId: this.sandboxId, error: err }); + }); await MongoSandboxInstance.deleteOne({ sandboxId: this.sandboxId }); } @@ -154,6 +163,30 @@ export class SandboxClient { } } +export const getSandboxClient = async ( + props: + | { + sandboxId: string; + } + | UnionIdType, + opts: { + resourceLimits?: ResourceLimits; + } = {} +) => { + const sandboxId = (() => { + if ('sandboxId' in props) { + return props.sandboxId; + } else { + return generateSandboxId(props.appId, props.userId, props.chatId); + } + })(); + const vmConfig = await getVolumeManagerConfig(sandboxId); + + const sandbox = new SandboxClient({ ...props, sandboxId }, { ...opts, vmConfig }); + await sandbox.ensureAvailable(); + return sandbox; +}; + // ==== Delete Sandboxes ==== export const deleteSandboxesByChatIds = async ({ appId, @@ -166,15 +199,13 @@ export const deleteSandboxesByChatIds = async ({ if (!instances.length) return; await Promise.allSettled( - instances.map((doc) => - new SandboxClient({ - sandboxId: doc.sandboxId - }) - .delete() - .catch((err) => { - logger.error('Failed to delete sandbox', { sandboxId: doc.sandboxId, error: err }); - }) - ) + instances.map(async (doc) => { + const client = await getSandboxClient({ sandboxId: doc.sandboxId }); + await client.delete().catch((err) => { + logger.error('Failed to delete sandbox', { sandboxId: doc.sandboxId, error: err }); + return Promise.reject(err); + }); + }) ); }; export const deleteSandboxesByAppId = async (appId: string) => { @@ -182,11 +213,12 @@ export const deleteSandboxesByAppId = async (appId: string) => { if (!instances.length) return; await Promise.allSettled( - instances.map((doc) => - new SandboxClient({ - sandboxId: doc.sandboxId - }).delete() - ) + instances.map(async (doc) => { + const client = await getSandboxClient({ sandboxId: doc.sandboxId }); + await client.delete().catch((err) => { + logger.error('Failed to delete sandbox', { sandboxId: doc.sandboxId, error: err }); + }); + }) ); }; @@ -201,14 +233,11 @@ export const cronJob = async () => { logger.info('Found running sandboxes inactive > 5 min', { count: instances.length }); - await batchRun(instances, (doc) => - new SandboxClient({ - sandboxId: doc.sandboxId - }) - .stop() - .catch((error) => { - logger.error('Failed to stop sandbox', { sandboxId: doc.sandboxId, error }); - }) - ); + await batchRun(instances, async (doc) => { + const client = await getSandboxClient({ sandboxId: doc.sandboxId }); + await client.stop().catch((err) => { + logger.error('Failed to stop sandbox', { sandboxId: doc.sandboxId, error: err }); + }); + }); }); }; diff --git a/packages/service/core/ai/sandbox/schema.ts b/packages/service/core/ai/sandbox/schema.ts index 38dd2a867b..32dfbb3f1a 100644 --- a/packages/service/core/ai/sandbox/schema.ts +++ b/packages/service/core/ai/sandbox/schema.ts @@ -44,6 +44,12 @@ const SandboxInstanceSchema = new Schema({ }, limit: { type: SandboxLimitSchema.shape + }, + storage: { + type: Schema.Types.Mixed + }, + metadata: { + type: Schema.Types.Mixed } }); diff --git a/packages/service/core/ai/sandbox/type.ts b/packages/service/core/ai/sandbox/type.ts index 9cd22d6cd3..cd39156560 100644 --- a/packages/service/core/ai/sandbox/type.ts +++ b/packages/service/core/ai/sandbox/type.ts @@ -2,7 +2,7 @@ import z from 'zod'; import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants'; // ---- 沙盒实例 DB 类型 ---- -export const SandboxProviderSchema = z.enum(['sealosdevbox']); +export const SandboxProviderSchema = z.enum(['sealosdevbox', 'opensandbox', 'e2b']); export type SandboxProviderType = z.infer; export const SandboxLimitSchema = z.object({ @@ -10,6 +10,26 @@ export const SandboxLimitSchema = z.object({ memoryMiB: z.number(), diskGiB: z.number() }); + +export const SandboxVolumeSchema = z.object({ + name: z.string(), + claimName: z.string().optional(), + mountPath: z.string(), + subPath: z.string().optional() +}); + +export const SandboxStorageSchema = z.object({ + volumes: z.array(SandboxVolumeSchema).optional(), + mountPath: z.string().optional() +}); +export type SandboxStorageType = z.infer; + +export const SandboxMetadataSchema = z.object({ + sessionKey: z.string().optional(), + volumeEnabled: z.boolean().optional() +}); +export type SandboxMetadataType = z.infer; + export const SandboxInstanceZodSchema = z.object({ _id: z.string(), sandboxId: z.string(), @@ -20,7 +40,8 @@ export const SandboxInstanceZodSchema = z.object({ lastActiveAt: z.date(), createdAt: z.date(), limit: SandboxLimitSchema.nullish(), - provider: SandboxProviderSchema + provider: SandboxProviderSchema, + storage: SandboxStorageSchema.nullish(), + metadata: SandboxMetadataSchema.nullish() }); - export type SandboxInstanceSchemaType = z.infer; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts index a84e079bde..8708ec875d 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts @@ -1,4 +1,4 @@ -import { SandboxClient } from '../../../../../../ai/sandbox/controller'; +import { getSandboxClient } from '../../../../../../ai/sandbox/controller'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; @@ -39,7 +39,7 @@ export const dispatchSandboxShell = async ({ const moduleName = parseI18nString(SANDBOX_NAME, lang); try { - const sandboxInstance = new SandboxClient({ + const sandboxInstance = await getSandboxClient({ appId, userId, chatId diff --git a/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts b/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts index 9bf0ec1d57..52e485b517 100644 --- a/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts +++ b/packages/service/core/workflow/dispatch/ai/tool/toolCall.ts @@ -25,7 +25,7 @@ import { SANDBOX_NAME, SANDBOX_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/constants'; -import { SandboxClient } from '../../../../ai/sandbox/controller'; +import { getSandboxClient } from '../../../../ai/sandbox/controller'; import { getSandboxToolWorkflowResponse } from './constants'; import { getErrText } from '@fastgpt/global/common/error/utils'; @@ -251,12 +251,11 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise=20'} '@emnapi/core@1.3.1': @@ -2719,8 +2735,8 @@ packages: '@fastgpt-sdk/plugin@0.3.8': resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==} - '@fastgpt-sdk/sandbox-adapter@0.0.31': - resolution: {integrity: sha512-Ex1nQNJo0BCFhy1FqsjOyTys1b+tDvzYexDHXUo+34rVF0AFYXhp7KRcoKlCyF2LFvFtHxPK2ggbUUP3Mr5lRQ==} + '@fastgpt-sdk/sandbox-adapter@0.0.33': + resolution: {integrity: sha512-XsfMn5rdxFMTyNuQjp97vJcQBeJIphZ+qtpVxcy4pABSk7B90Xf8nYCZ09OLTfcbql+flQ4P3C9HoDIi64ZMkA==} engines: {node: '>=18'} '@fastgpt-sdk/storage@0.6.15': @@ -4693,8 +4709,8 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - '@types/bun@1.3.10': - resolution: {integrity: sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==} + '@types/bun@1.3.11': + resolution: {integrity: sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -5083,12 +5099,26 @@ packages: '@vitest/expect@1.6.1': resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.1.1': resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==} '@vitest/expect@4.0.16': resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.1.1': resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==} peerDependencies: @@ -5111,6 +5141,9 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.1.1': resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} @@ -5120,6 +5153,9 @@ packages: '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.1.1': resolution: {integrity: sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==} @@ -5129,6 +5165,9 @@ packages: '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.1.1': resolution: {integrity: sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==} @@ -5138,6 +5177,9 @@ packages: '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.1.1': resolution: {integrity: sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==} @@ -5147,6 +5189,9 @@ packages: '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.1.1': resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} @@ -5663,8 +5708,8 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -5720,8 +5765,8 @@ packages: bullmq@5.52.2: resolution: {integrity: sha512-fK/dKIv8ymyys4K+zeNEPA+yuYWzRPmBWUmwIMz8DvYekadl8VG19yUx94Na0n0cLAi+spdn3a/+ufkYK7CBUg==} - bun-types@1.3.10: - resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==} + bun-types@1.3.11: + resolution: {integrity: sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==} bundle-n-require@1.1.2: resolution: {integrity: sha512-bEk2jakVK1ytnZ9R2AAiZEeK/GxPUM8jvcRxHZXifZDMcjkI4EG/GlsJ2YGSVYT9y/p/gA9/0yDY8rCGsSU6Tg==} @@ -6610,8 +6655,8 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - e2b@2.14.1: - resolution: {integrity: sha512-g0NPZNzwIaePTahu9ixBtqrw9IZQ8ThK8dt+DU394+jmxQJ+69c2t8A0j973/j+bHo3QdNFxIRIH6zDcC3ueaw==} + e2b@2.18.0: + resolution: {integrity: sha512-umWvNhKx3dWUfzcLPLO5Ep1qB5cTLuJlAlnxfKVUnOwx3yaO+H1PaAE2fihT4etEu3BNs3pHvdwa1VM+uMxe0w==} engines: {node: '>=20'} eastasianwidth@0.2.0: @@ -10594,8 +10639,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.5.11: - resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} terser@5.39.0: @@ -10671,6 +10716,10 @@ packages: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -11174,6 +11223,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.1.1: resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11275,6 +11329,31 @@ packages: jsdom: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.1.1: resolution: {integrity: sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -13260,9 +13339,9 @@ snapshots: '@dmsnell/diff-match-patch@1.1.0': {} - '@e2b/code-interpreter@2.3.3': + '@e2b/code-interpreter@2.4.0': dependencies: - e2b: 2.14.1 + e2b: 2.18.0 '@emnapi/core@1.3.1': dependencies: @@ -13666,9 +13745,9 @@ snapshots: '@fortaine/fetch-event-source': 3.0.6 zod: 4.1.12 - '@fastgpt-sdk/sandbox-adapter@0.0.31': + '@fastgpt-sdk/sandbox-adapter@0.0.33': dependencies: - '@e2b/code-interpreter': 2.3.3 + '@e2b/code-interpreter': 2.4.0 '@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: @@ -15944,9 +16023,9 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.17.24 - '@types/bun@1.3.10': + '@types/bun@1.3.11': dependencies: - bun-types: 1.3.10 + bun-types: 1.3.11 '@types/chai@5.2.3': dependencies: @@ -16454,6 +16533,13 @@ snapshots: '@vitest/utils': 1.6.1 chai: 4.5.0 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + tinyrainbow: 1.2.0 + '@vitest/expect@3.1.1': dependencies: '@vitest/spy': 3.1.1 @@ -16470,6 +16556,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/mocker@2.1.9(vite@5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0) + '@vitest/mocker@3.1.1(vite@6.2.2(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.1.1 @@ -16502,6 +16596,10 @@ snapshots: optionalDependencies: vite: 6.2.2(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.1.1': dependencies: tinyrainbow: 2.0.0 @@ -16516,6 +16614,11 @@ snapshots: p-limit: 5.0.0 pathe: 1.1.2 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.1.1': dependencies: '@vitest/utils': 3.1.1 @@ -16532,6 +16635,12 @@ snapshots: pathe: 1.1.2 pretty-format: 29.7.0 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@3.1.1': dependencies: '@vitest/pretty-format': 3.1.1 @@ -16548,6 +16657,10 @@ snapshots: dependencies: tinyspy: 2.2.1 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.1.1': dependencies: tinyspy: 3.0.2 @@ -16561,6 +16674,12 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + '@vitest/utils@3.1.1': dependencies: '@vitest/pretty-format': 3.1.1 @@ -17229,7 +17348,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -17294,7 +17413,7 @@ snapshots: transitivePeerDependencies: - supports-color - bun-types@1.3.10: + bun-types@1.3.11: dependencies: '@types/node': 20.17.24 @@ -18209,7 +18328,7 @@ snapshots: duplexer@0.1.2: {} - e2b@2.14.1: + e2b@2.18.0: dependencies: '@bufbuild/protobuf': 2.11.0 '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.11.0) @@ -18220,7 +18339,7 @@ snapshots: glob: 11.1.0 openapi-fetch: 0.14.1 platform: 1.3.6 - tar: 7.5.11 + tar: 7.5.13 eastasianwidth@0.2.0: {} @@ -21008,7 +21127,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.2: dependencies: @@ -23269,7 +23388,7 @@ snapshots: fast-fifo: 1.3.2 streamx: 2.22.0 - tar@7.5.11: + tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -23342,6 +23461,8 @@ snapshots: tinypool@1.0.2: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyrainbow@3.0.3: {} @@ -23893,6 +24014,24 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.1.1(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -24011,6 +24150,41 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.0 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0) + vite-node: 2.1.9(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.0.13 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@vitest/expect': 3.1.1 diff --git a/projects/sandbox-sync-agent/base/Dockerfile b/projects/agent-sandbox/Dockerfile similarity index 89% rename from projects/sandbox-sync-agent/base/Dockerfile rename to projects/agent-sandbox/Dockerfile index efce855685..6ef10636c1 100644 --- a/projects/sandbox-sync-agent/base/Dockerfile +++ b/projects/agent-sandbox/Dockerfile @@ -1,5 +1,5 @@ -# Skill Sandbox 基础镜像 -# 提供 code-server 开发环境,供 K8s Sidecar 和 Docker 双进程两种运行时使用 +# Agent Sandbox 镜像 +# 提供 code-server 开发环境 # # 构建:docker build -t fastgpt-agent-sandbox:latest . # 产物:fastgpt-agent-sandbox:latest @@ -29,7 +29,7 @@ RUN curl -fsSL https://code-server.dev/install.sh | sh # Create a non-root user for security RUN useradd --create-home --shell /bin/bash sandbox -USER sandbox +USER root WORKDIR /home/sandbox # Copy VS Code settings diff --git a/projects/agent-sandbox/entrypoint.sh b/projects/agent-sandbox/entrypoint.sh new file mode 100644 index 0000000000..b39c9c15ec --- /dev/null +++ b/projects/agent-sandbox/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Set work directory from environment variable, default to /home/sandbox +WORKDIR="${FASTGPT_WORKDIR:-/home/sandbox}" +mkdir -p "${WORKDIR}" + +# Capture the flag before unsetting, then clear all FastGPT runtime vars +_ENABLE_CODE_SERVER="${FASTGPT_ENABLE_CODE_SERVER}" +unset FASTGPT_SESSION_ID FASTGPT_WORKDIR FASTGPT_ENABLE_CODE_SERVER + +# Start code-server or sleep forever +if [ "${_ENABLE_CODE_SERVER}" = "true" ]; then + # --bind-addr 0.0.0.0:8080 allows access from outside the container + # --auth none removes password protection + exec code-server \ + --bind-addr 0.0.0.0:8080 \ + --auth none \ + --disable-telemetry \ + --disable-update-check \ + --disable-workspace-trust \ + --disable-getting-started-override \ + --app-name "Skills" \ + --user-data-dir /home/sandbox/.local/share/code-server \ + "${WORKDIR}" +else + exec sleep infinity +fi diff --git a/projects/sandbox-sync-agent/base/settings.json b/projects/agent-sandbox/settings.json similarity index 100% rename from projects/sandbox-sync-agent/base/settings.json rename to projects/agent-sandbox/settings.json diff --git a/projects/app/.env.template b/projects/app/.env.template index 4d0546a628..121f68908d 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -37,8 +37,22 @@ AIPROXY_API_TOKEN=aiproxy # Agent sandbox AGENT_SANDBOX_PROVIDER= +# Sealos devbox AGENT_SANDBOX_SEALOS_BASEURL= AGENT_SANDBOX_SEALOS_TOKEN= +# OpenSandbox 配置(PROVIDER=opensandbox 时生效) +AGENT_SANDBOX_OPENSANDBOX_BASEURL= +AGENT_SANDBOX_OPENSANDBOX_API_KEY= +AGENT_SANDBOX_OPENSANDBOX_RUNTIME=docker +AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO=registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox +AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG=latest +# Volume 持久化配置(opensandbox provider 下可选) +AGENT_SANDBOX_ENABLE_VOLUME=false +AGENT_SANDBOX_VOLUME_MANAGER_URL= +AGENT_SANDBOX_VOLUME_MANAGER_TOKEN= +# E2B 配置(PROVIDER=e2b 时生效) +AGENT_SANDBOX_E2B_API_KEY= + # 辅助生成模型(暂时只能指定一个,需保证系统中已激活该模型) HELPER_BOT_MODEL=qwen-max diff --git a/projects/app/src/pages/api/core/ai/sandbox/download.ts b/projects/app/src/pages/api/core/ai/sandbox/download.ts index 834e8310da..e2656cb4f4 100644 --- a/projects/app/src/pages/api/core/ai/sandbox/download.ts +++ b/projects/app/src/pages/api/core/ai/sandbox/download.ts @@ -2,7 +2,7 @@ import type { NextApiResponse } from 'next'; import { NextAPI } from '@/service/middleware/entry'; import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { authChatCrud } from '@/service/support/permission/auth/chat'; -import { SandboxClient } from '@fastgpt/service/core/ai/sandbox/controller'; +import { getSandboxClient, type SandboxClient } from '@fastgpt/service/core/ai/sandbox/controller'; import archiver from 'archiver'; import { z } from 'zod'; import { OutLinkChatAuthSchema } from '@fastgpt/global/support/permission/chat'; @@ -29,7 +29,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse): Promise ``` diff --git a/projects/sandbox/build.sh b/projects/code-sandbox/build.sh similarity index 100% rename from projects/sandbox/build.sh rename to projects/code-sandbox/build.sh diff --git a/projects/sandbox/package.json b/projects/code-sandbox/package.json similarity index 97% rename from projects/sandbox/package.json rename to projects/code-sandbox/package.json index 845c6694bb..6f5a3b9e83 100644 --- a/projects/sandbox/package.json +++ b/projects/code-sandbox/package.json @@ -1,5 +1,5 @@ { - "name": "sandbox", + "name": "code-sandbox", "version": "5.0.0", "description": "FastGPT Code Sandbox - Bun + Hono + 统一子进程模型", "author": "", diff --git a/projects/sandbox/requirements.txt b/projects/code-sandbox/requirements.txt similarity index 100% rename from projects/sandbox/requirements.txt rename to projects/code-sandbox/requirements.txt diff --git a/projects/sandbox/src/config.ts b/projects/code-sandbox/src/config.ts similarity index 100% rename from projects/sandbox/src/config.ts rename to projects/code-sandbox/src/config.ts diff --git a/projects/sandbox/src/env.ts b/projects/code-sandbox/src/env.ts similarity index 100% rename from projects/sandbox/src/env.ts rename to projects/code-sandbox/src/env.ts diff --git a/projects/sandbox/src/index.ts b/projects/code-sandbox/src/index.ts similarity index 100% rename from projects/sandbox/src/index.ts rename to projects/code-sandbox/src/index.ts diff --git a/projects/sandbox/src/pool/base-process-pool.ts b/projects/code-sandbox/src/pool/base-process-pool.ts similarity index 100% rename from projects/sandbox/src/pool/base-process-pool.ts rename to projects/code-sandbox/src/pool/base-process-pool.ts diff --git a/projects/sandbox/src/pool/process-pool.ts b/projects/code-sandbox/src/pool/process-pool.ts similarity index 100% rename from projects/sandbox/src/pool/process-pool.ts rename to projects/code-sandbox/src/pool/process-pool.ts diff --git a/projects/sandbox/src/pool/python-process-pool.ts b/projects/code-sandbox/src/pool/python-process-pool.ts similarity index 100% rename from projects/sandbox/src/pool/python-process-pool.ts rename to projects/code-sandbox/src/pool/python-process-pool.ts diff --git a/projects/sandbox/src/pool/worker.py b/projects/code-sandbox/src/pool/worker.py similarity index 100% rename from projects/sandbox/src/pool/worker.py rename to projects/code-sandbox/src/pool/worker.py diff --git a/projects/sandbox/src/pool/worker.ts b/projects/code-sandbox/src/pool/worker.ts similarity index 100% rename from projects/sandbox/src/pool/worker.ts rename to projects/code-sandbox/src/pool/worker.ts diff --git a/projects/sandbox/src/types.ts b/projects/code-sandbox/src/types.ts similarity index 100% rename from projects/sandbox/src/types.ts rename to projects/code-sandbox/src/types.ts diff --git a/projects/sandbox/src/utils/index.ts b/projects/code-sandbox/src/utils/index.ts similarity index 100% rename from projects/sandbox/src/utils/index.ts rename to projects/code-sandbox/src/utils/index.ts diff --git a/projects/sandbox/src/utils/logger.ts b/projects/code-sandbox/src/utils/logger.ts similarity index 100% rename from projects/sandbox/src/utils/logger.ts rename to projects/code-sandbox/src/utils/logger.ts diff --git a/projects/sandbox/src/utils/semaphore.ts b/projects/code-sandbox/src/utils/semaphore.ts similarity index 100% rename from projects/sandbox/src/utils/semaphore.ts rename to projects/code-sandbox/src/utils/semaphore.ts diff --git a/projects/sandbox/test/benchmark/bench-sandbox-python.sh b/projects/code-sandbox/test/benchmark/bench-sandbox-python.sh similarity index 100% rename from projects/sandbox/test/benchmark/bench-sandbox-python.sh rename to projects/code-sandbox/test/benchmark/bench-sandbox-python.sh diff --git a/projects/sandbox/test/benchmark/bench-sandbox.sh b/projects/code-sandbox/test/benchmark/bench-sandbox.sh similarity index 100% rename from projects/sandbox/test/benchmark/bench-sandbox.sh rename to projects/code-sandbox/test/benchmark/bench-sandbox.sh diff --git a/projects/sandbox/test/compat/legacy-js.test.ts b/projects/code-sandbox/test/compat/legacy-js.test.ts similarity index 100% rename from projects/sandbox/test/compat/legacy-js.test.ts rename to projects/code-sandbox/test/compat/legacy-js.test.ts diff --git a/projects/sandbox/test/compat/legacy-python.test.ts b/projects/code-sandbox/test/compat/legacy-python.test.ts similarity index 100% rename from projects/sandbox/test/compat/legacy-python.test.ts rename to projects/code-sandbox/test/compat/legacy-python.test.ts diff --git a/projects/sandbox/test/integration/api.test.ts b/projects/code-sandbox/test/integration/api.test.ts similarity index 100% rename from projects/sandbox/test/integration/api.test.ts rename to projects/code-sandbox/test/integration/api.test.ts diff --git a/projects/sandbox/test/integration/functional.test.ts b/projects/code-sandbox/test/integration/functional.test.ts similarity index 100% rename from projects/sandbox/test/integration/functional.test.ts rename to projects/code-sandbox/test/integration/functional.test.ts diff --git a/projects/sandbox/test/unit/boundary.test.ts b/projects/code-sandbox/test/unit/boundary.test.ts similarity index 100% rename from projects/sandbox/test/unit/boundary.test.ts rename to projects/code-sandbox/test/unit/boundary.test.ts diff --git a/projects/sandbox/test/unit/process-pool.test.ts b/projects/code-sandbox/test/unit/process-pool.test.ts similarity index 100% rename from projects/sandbox/test/unit/process-pool.test.ts rename to projects/code-sandbox/test/unit/process-pool.test.ts diff --git a/projects/sandbox/test/unit/resource-limits.test.ts b/projects/code-sandbox/test/unit/resource-limits.test.ts similarity index 100% rename from projects/sandbox/test/unit/resource-limits.test.ts rename to projects/code-sandbox/test/unit/resource-limits.test.ts diff --git a/projects/sandbox/test/unit/security.test.ts b/projects/code-sandbox/test/unit/security.test.ts similarity index 100% rename from projects/sandbox/test/unit/security.test.ts rename to projects/code-sandbox/test/unit/security.test.ts diff --git a/projects/sandbox/test/unit/semaphore.test.ts b/projects/code-sandbox/test/unit/semaphore.test.ts similarity index 100% rename from projects/sandbox/test/unit/semaphore.test.ts rename to projects/code-sandbox/test/unit/semaphore.test.ts diff --git a/projects/sandbox/tsconfig.json b/projects/code-sandbox/tsconfig.json similarity index 100% rename from projects/sandbox/tsconfig.json rename to projects/code-sandbox/tsconfig.json diff --git a/projects/sandbox/vitest.config.ts b/projects/code-sandbox/vitest.config.ts similarity index 100% rename from projects/sandbox/vitest.config.ts rename to projects/code-sandbox/vitest.config.ts diff --git a/projects/sandbox-sync-agent/Dockerfile b/projects/sandbox-sync-agent/Dockerfile deleted file mode 100644 index f601c5b19a..0000000000 --- a/projects/sandbox-sync-agent/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# 基于 base/ 目录构建的 fastgpt-agent-sandbox:latest,在其基础上注入 Sync Agent -# -# 构建顺序: -# 1. cd base && docker build -t fastgpt-agent-sandbox:latest . -# 2. docker build -f Dockerfile -t fastgpt-agent-sandbox:k8s . - -FROM fastgpt-agent-sandbox:latest - -USER root - -# 安装 Sync Agent 依赖 -RUN apt-get update && apt-get install -y \ - inotify-tools \ - && rm -rf /var/lib/apt/lists/* - -# 安装 MinIO Client (mc) -RUN curl -O https://dl.min.io/client/mc/release/linux-amd64/mc && \ - chmod +x mc && \ - mv mc /usr/local/bin/ - -COPY sync.sh /sync.sh -COPY entrypoint.sh /entrypoint.sh -COPY http_server.py /http_server.py - -RUN chmod +x /sync.sh /entrypoint.sh - -# 8081: Sync Agent HTTP API(健康检查 / 手动触发同步) -EXPOSE 8081 - -USER sandbox - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/projects/sandbox-sync-agent/Dockerfile.docker-runtime b/projects/sandbox-sync-agent/Dockerfile.docker-runtime deleted file mode 100644 index 61c35f4540..0000000000 --- a/projects/sandbox-sync-agent/Dockerfile.docker-runtime +++ /dev/null @@ -1,35 +0,0 @@ -# Docker 双进程模式镜像 -# 基于 base/ 目录构建的 fastgpt-agent-sandbox:latest,在其基础上注入 Sync Agent -# -# 构建顺序: -# 1. cd base && docker build -t fastgpt-agent-sandbox:latest . -# 2. docker build -f Dockerfile.docker-runtime -t fastgpt-agent-sandbox:docker . -FROM fastgpt-agent-sandbox:latest - -USER root - -# 安装 Sync Agent 依赖 -RUN apt-get update && apt-get install -y \ - inotify-tools \ - supervisor \ - && rm -rf /var/lib/apt/lists/* - -# 安装 MinIO Client -RUN curl -O https://dl.min.io/client/mc/release/linux-amd64/mc && \ - chmod +x mc && \ - mv mc /usr/local/bin/ - -# 复制 Sync Agent 脚本 -COPY sync.sh /opt/sync-agent/sync.sh -COPY http_server.py /opt/sync-agent/http_server.py -COPY docker-entrypoint.sh /opt/sync-agent/docker-entrypoint.sh -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -RUN chmod +x /opt/sync-agent/sync.sh \ - /opt/sync-agent/docker-entrypoint.sh && \ - mkdir -p /var/log/supervisor && \ - chown -R sandbox:sandbox /var/log/supervisor - -USER sandbox - -ENTRYPOINT ["/opt/sync-agent/docker-entrypoint.sh"] diff --git a/projects/sandbox-sync-agent/base/entrypoint.sh b/projects/sandbox-sync-agent/base/entrypoint.sh deleted file mode 100644 index 52eed6a5b7..0000000000 --- a/projects/sandbox-sync-agent/base/entrypoint.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Set work directory from environment variable, default to /home/sandbox -WORKDIR="${FASTGPT_WORKDIR:-/home/sandbox}" -mkdir -p "${WORKDIR}" - -# Start code-server -# --bind-addr 0.0.0.0:8080 allows access from outside the container -# --auth none removes password protection -exec code-server \ - --bind-addr 0.0.0.0:8080 \ - --auth none \ - --disable-telemetry \ - --disable-update-check \ - --disable-workspace-trust \ - --disable-getting-started-override \ - --app-name "Skills" \ - --user-data-dir /home/sandbox/.local/share/code-server \ - "${WORKDIR}" diff --git a/projects/sandbox-sync-agent/build.sh b/projects/sandbox-sync-agent/build.sh deleted file mode 100755 index 5447132157..0000000000 --- a/projects/sandbox-sync-agent/build.sh +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Build script for sandbox-sync-agent images. -# Usage: ./build.sh [OPTIONS] -# -# Images: -# base/Dockerfile -> fastgpt-agent-sandbox:latest (base image) -# Dockerfile -> fastgpt-agent-sandbox:k8s (K8s sidecar) -# Dockerfile.docker-runtime -> fastgpt-agent-sandbox:docker (Docker dual-process) - -# --------------------------------------------------------------------------- -# Defaults -# --------------------------------------------------------------------------- -REGISTRY="" -TAG="latest" -TARGET="all" -NO_CACHE="" -PLATFORM="" - -# --------------------------------------------------------------------------- -# Parse arguments -# --------------------------------------------------------------------------- -while [[ $# -gt 0 ]]; do - case "$1" in - --registry) - REGISTRY="${2:?'--registry requires a value'}" - shift 2 - ;; - --tag) - TAG="${2:?'--tag requires a value'}" - shift 2 - ;; - --target) - TARGET="${2:?'--target requires a value (base|k8s|docker|all)'}" - shift 2 - ;; - --no-cache) - NO_CACHE="--no-cache" - shift - ;; - --platform) - PLATFORM="${2:?'--platform requires a value, e.g. linux/amd64'}" - shift 2 - ;; - -h|--help) - echo "Usage: $0 [--registry ] [--tag ] [--target base|k8s|docker|all] [--no-cache] [--platform ]" - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac -done - -# Validate --target -case "$TARGET" in - base|k8s|docker|all) ;; - *) - echo "Error: --target must be one of: base, k8s, docker, all" >&2 - exit 1 - ;; -esac - -# --------------------------------------------------------------------------- -# Always run from the directory that contains this script -# --------------------------------------------------------------------------- -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# --------------------------------------------------------------------------- -# Helper: build a docker image name -# $1 = variant suffix (latest | k8s | docker) -# Returns the full image reference based on registry / tag settings. -# --------------------------------------------------------------------------- -image_name() { - local suffix="$1" - local name="fastgpt-agent-sandbox:${suffix}" - - # Override the tag portion when the user supplied --tag and suffix == "latest" - # (base image is always tagged :latest locally; remote tag uses user-supplied tag) - if [[ -n "$REGISTRY" ]]; then - if [[ "$suffix" == "latest" ]]; then - echo "${REGISTRY}/fastgpt-agent-sandbox:${TAG}" - else - echo "${REGISTRY}/fastgpt-agent-sandbox-${suffix}:${TAG}" - fi - else - if [[ "$suffix" == "latest" && "$TAG" != "latest" ]]; then - echo "fastgpt-agent-sandbox:${TAG}" - else - echo "$name" - fi - fi -} - -# --------------------------------------------------------------------------- -# Helper: build extra docker flags -# --------------------------------------------------------------------------- -extra_flags() { - local flags="$NO_CACHE" - if [[ -n "$PLATFORM" ]]; then - flags="$flags --platform $PLATFORM" - fi - echo "$flags" -} - -# --------------------------------------------------------------------------- -# Print a section header -# --------------------------------------------------------------------------- -section() { - echo "" - echo "========================================" - echo " $*" - echo "========================================" -} - -# --------------------------------------------------------------------------- -# Build base image -# --------------------------------------------------------------------------- -build_base() { - section "Building BASE image" - - # The base/ subdirectory is the build context - local local_tag="fastgpt-agent-sandbox:latest" - # shellcheck disable=SC2046 - docker build \ - -t "$local_tag" \ - $(extra_flags) \ - base/ - - echo "Built: $local_tag" - - # If a registry or non-default tag is requested, add the remote tag as well - if [[ -n "$REGISTRY" ]] || [[ "$TAG" != "latest" ]]; then - local remote_tag - remote_tag="$(image_name latest)" - if [[ "$remote_tag" != "$local_tag" ]]; then - docker tag "$local_tag" "$remote_tag" - echo "Tagged: $remote_tag" - fi - fi -} - -# --------------------------------------------------------------------------- -# Build K8s sidecar image -# --------------------------------------------------------------------------- -build_k8s() { - section "Building K8S image" - - local tag - if [[ -n "$REGISTRY" ]]; then - tag="${REGISTRY}/fastgpt-agent-sandbox-k8s:${TAG}" - else - tag="fastgpt-agent-sandbox:k8s" - fi - - # shellcheck disable=SC2046 - docker build \ - -f Dockerfile \ - -t "$tag" \ - $(extra_flags) \ - . - - echo "Built: $tag" -} - -# --------------------------------------------------------------------------- -# Build Docker dual-process image -# --------------------------------------------------------------------------- -build_docker() { - section "Building DOCKER-RUNTIME image" - - local tag - if [[ -n "$REGISTRY" ]]; then - tag="${REGISTRY}/fastgpt-agent-sandbox-docker:${TAG}" - else - tag="fastgpt-agent-sandbox:docker" - fi - - # shellcheck disable=SC2046 - docker build \ - -f Dockerfile.docker-runtime \ - -t "$tag" \ - $(extra_flags) \ - . - - echo "Built: $tag" -} - -# --------------------------------------------------------------------------- -# Print summary of built images -# --------------------------------------------------------------------------- -print_summary() { - section "Build Summary" - echo "" - docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" \ - | grep -E "REPOSITORY|fastgpt-agent-sandbox" || true -} - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- -echo "Target : $TARGET" -echo "Tag : $TAG" -echo "Registry: ${REGISTRY:-'(none)'}" -echo "Platform: ${PLATFORM:-'(default)'}" -echo "No-cache: ${NO_CACHE:-'(no)'}" - -# base must be built before k8s / docker when building all -if [[ "$TARGET" == "all" || "$TARGET" == "base" ]]; then - build_base -fi - -if [[ "$TARGET" == "all" || "$TARGET" == "k8s" ]]; then - build_k8s -fi - -if [[ "$TARGET" == "all" || "$TARGET" == "docker" ]]; then - build_docker -fi - -print_summary - -echo "" -echo "Done." diff --git a/projects/sandbox-sync-agent/docker-entrypoint.sh b/projects/sandbox-sync-agent/docker-entrypoint.sh deleted file mode 100644 index fb7879f1cd..0000000000 --- a/projects/sandbox-sync-agent/docker-entrypoint.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -# 配置 MinIO Client -mc alias set minio ${FASTGPT_MINIO_ENDPOINT} ${FASTGPT_MINIO_ACCESS_KEY} ${FASTGPT_MINIO_SECRET_KEY} --api S3v4 - -# 确保 bucket 存在 -mc mb minio/${FASTGPT_MINIO_BUCKET} --ignore-existing || true - -# Prepare work directory with correct permissions -export FASTGPT_WORKDIR="${FASTGPT_WORKDIR:-/home/sandbox}" -mkdir -p "${FASTGPT_WORKDIR}" - -# 是否启动 code-server(默认 true) -# 仅需文件同步时设置 FASTGPT_ENABLE_CODE_SERVER=false -export FASTGPT_ENABLE_CODE_SERVER=${FASTGPT_ENABLE_CODE_SERVER:-true} - -# 使用 supervisord 启动进程 -exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/projects/sandbox-sync-agent/entrypoint.sh b/projects/sandbox-sync-agent/entrypoint.sh deleted file mode 100644 index 8118ad2ef5..0000000000 --- a/projects/sandbox-sync-agent/entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -e - -# 配置 MinIO Client -mc alias set minio ${FASTGPT_MINIO_ENDPOINT} ${FASTGPT_MINIO_ACCESS_KEY} ${FASTGPT_MINIO_SECRET_KEY} --api S3v4 - -# 确保 bucket 存在 -mc mb minio/${FASTGPT_MINIO_BUCKET} --ignore-existing || true - -# Pass FASTGPT_WORKDIR as FASTGPT_SYNC_PATH if FASTGPT_SYNC_PATH is not explicitly set -if [ -z "${FASTGPT_SYNC_PATH}" ] && [ -n "${FASTGPT_WORKDIR}" ]; then - export FASTGPT_SYNC_PATH="${FASTGPT_WORKDIR}" -fi - -# 启动 sync 服务 -exec /sync.sh diff --git a/projects/sandbox-sync-agent/http_server.py b/projects/sandbox-sync-agent/http_server.py deleted file mode 100644 index a45026574d..0000000000 --- a/projects/sandbox-sync-agent/http_server.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -""" -Sync Agent HTTP 服务 -提供健康检查和手动触发同步接口,读取 sync.sh 写入的状态文件。 -""" -import http.server -import json -import os -import pathlib -from datetime import datetime, timezone - -STATE_DIR = pathlib.Path(os.environ.get('STATE_DIR', '/tmp/sync-state')) -HTTP_PORT = int(os.environ.get('HTTP_PORT', '8081')) - - -class SyncAgentHandler(http.server.BaseHTTPRequestHandler): - def log_message(self, format, *args): - pass # 抑制每次请求的访问日志 - - def _read_state(self): - try: - last_sync = (STATE_DIR / 'last_sync').read_text().strip() - except Exception: - last_sync = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') - try: - pending = int((STATE_DIR / 'pending_count').read_text().strip()) - except Exception: - pending = 0 - return last_sync, pending - - def _send_json(self, code, body): - data = json.dumps(body).encode() - self.send_response(code) - self.send_header('Content-Type', 'application/json') - self.send_header('Content-Length', str(len(data))) - self.end_headers() - self.wfile.write(data) - - def do_GET(self): - if self.path == '/health': - last_sync, pending = self._read_state() - self._send_json(200, { - 'status': 'healthy', - 'lastSync': last_sync, - 'pendingCount': pending - }) - else: - self.send_response(404) - self.end_headers() - - def do_POST(self): - if self.path == '/sync': - STATE_DIR.mkdir(parents=True, exist_ok=True) - (STATE_DIR / 'trigger').touch() - self._send_json(200, {'success': True}) - else: - self.send_response(404) - self.end_headers() - - -if __name__ == '__main__': - STATE_DIR.mkdir(parents=True, exist_ok=True) - server = http.server.HTTPServer(('', HTTP_PORT), SyncAgentHandler) - print(f'[Sync] HTTP server listening on :{HTTP_PORT}', flush=True) - server.serve_forever() diff --git a/projects/sandbox-sync-agent/pool-skill-sandbox.yaml b/projects/sandbox-sync-agent/pool-skill-sandbox.yaml deleted file mode 100644 index 2a2eb07f8b..0000000000 --- a/projects/sandbox-sync-agent/pool-skill-sandbox.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: sandbox.opensandbox.io/v1alpha1 -kind: Pool -metadata: - name: skill-sandbox-with-sync - namespace: opensandbox - labels: - app: skill-sandbox - component: sync-enabled -spec: - # 预热池大小 - minReady: 2 - maxSize: 20 - - template: - metadata: - labels: - app: skill-sandbox - spec: - volumes: - - name: workspace - emptyDir: - sizeLimit: 1Gi - - containers: - # 主容器:Skill Sandbox(code-server 开发环境) - - name: sandbox - image: fastgpt-agent-sandbox:latest - imagePullPolicy: IfNotPresent - env: - # FASTGPT_WORKDIR: the workspace directory opened by code-server. - # This is a subdirectory of the volume mountPath (/home/sandbox), - # keeping code under /home/sandbox/workspace separate from home files. - - name: FASTGPT_WORKDIR - value: "/home/sandbox/workspace" - volumeMounts: - - name: workspace - mountPath: /home/sandbox - ports: - - containerPort: 8080 - name: code-server - resources: - requests: - cpu: 500m - memory: 512Mi - limits: - cpu: 2 - memory: 2Gi - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - - # Sidecar:Sync Agent(MinIO 文件同步) - - name: sync-agent - image: fastgpt-agent-sandbox:k8s - imagePullPolicy: IfNotPresent - env: - - name: FASTGPT_MINIO_ENDPOINT - valueFrom: - secretKeyRef: - name: minio-credentials - key: endpoint - - name: FASTGPT_MINIO_ACCESS_KEY - valueFrom: - secretKeyRef: - name: minio-credentials - key: accessKey - - name: FASTGPT_MINIO_SECRET_KEY - valueFrom: - secretKeyRef: - name: minio-credentials - key: secretKey - - name: FASTGPT_MINIO_BUCKET - value: "fastgpt-private" - - name: FASTGPT_SESSION_ID - valueFrom: - fieldRef: - fieldPath: metadata.labels['session-id'] - - name: FASTGPT_SYNC_PATH - value: "/home/sandbox/workspace" - - name: SYNC_INTERVAL - value: "60" - - name: HTTP_PORT - value: "8081" - volumeMounts: - - name: workspace - mountPath: /home/sandbox - ports: - - containerPort: 8081 - name: sync-api - resources: - requests: - cpu: 100m - memory: 64Mi - limits: - cpu: 500m - memory: 256Mi - livenessProbe: - httpGet: - path: /health - port: 8081 - initialDelaySeconds: 10 - periodSeconds: 30 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /health - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 diff --git a/projects/sandbox-sync-agent/supervisord.conf b/projects/sandbox-sync-agent/supervisord.conf deleted file mode 100644 index c0d0e6a689..0000000000 --- a/projects/sandbox-sync-agent/supervisord.conf +++ /dev/null @@ -1,24 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/var/log/supervisor/supervisord.log -pidfile=/tmp/supervisord.pid - -# Sandbox 主进程(code-server) -# 通过环境变量 FASTGPT_ENABLE_CODE_SERVER=true|false 控制是否启动,默认 true -[program:code-server] -command=/home/sandbox/entrypoint.sh -user=sandbox -autostart=%(ENV_FASTGPT_ENABLE_CODE_SERVER)s -autorestart=true -stdout_logfile=/var/log/supervisor/code-server.log -stderr_logfile=/var/log/supervisor/code-server-error.log - -# Sync Agent 后台进程(始终启动) -[program:sync-agent] -command=/opt/sync-agent/sync.sh -user=sandbox -autostart=true -autorestart=true -environment=FASTGPT_SYNC_PATH="%(ENV_FASTGPT_WORKDIR)s",HTTP_SERVER_PATH="/opt/sync-agent/http_server.py" -stdout_logfile=/var/log/supervisor/sync-agent.log -stderr_logfile=/var/log/supervisor/sync-agent-error.log diff --git a/projects/sandbox-sync-agent/sync.sh b/projects/sandbox-sync-agent/sync.sh deleted file mode 100644 index dd988c3051..0000000000 --- a/projects/sandbox-sync-agent/sync.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/sh - -SYNC_PATH=${FASTGPT_SYNC_PATH:-/home/sandbox} -BUCKET_PATH="minio/${FASTGPT_MINIO_BUCKET}/agent-sessions/${FASTGPT_SESSION_ID}" -SYNC_INTERVAL=${SYNC_INTERVAL:-60} -HTTP_PORT=${HTTP_PORT:-8081} -STATE_DIR="${STATE_DIR:-/tmp/sync-state}" -# K8s 模式默认 /http_server.py,Docker 模式通过 supervisord.conf 注入 /opt/sync-agent/http_server.py -HTTP_SERVER_PATH="${HTTP_SERVER_PATH:-/http_server.py}" - -mkdir -p "${STATE_DIR}" - -LAST_SYNC_FILE="${STATE_DIR}/last_sync" -PENDING_FILE="${STATE_DIR}/pending_count" -TRIGGER_FILE="${STATE_DIR}/trigger" - -# 初始化状态 -date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_SYNC_FILE}" -echo "0" > "${PENDING_FILE}" - -# 1. 启动时下载历史文件 -echo "[Sync] Downloading files from ${BUCKET_PATH}..." -mc mirror "${BUCKET_PATH}" "${SYNC_PATH}" --overwrite || true -date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_SYNC_FILE}" - -# 2. 启动 HTTP 健康检查服务(后台) -echo "[Sync] Starting HTTP server on port ${HTTP_PORT}..." -python3 "${HTTP_SERVER_PATH}" & - -# 3. 启动后台全量同步(定时 + 手动触发) -( - while true; do - sleep "${SYNC_INTERVAL}" - - # 检查手动触发 - if [ -f "${TRIGGER_FILE}" ]; then - rm -f "${TRIGGER_FILE}" - echo "[Sync] Manual sync triggered via POST /sync" - fi - - echo "[Sync] Periodic sync to MinIO..." - mc mirror "${SYNC_PATH}" "${BUCKET_PATH}" --overwrite - date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_SYNC_FILE}" - echo "0" > "${PENDING_FILE}" - done -) & - -# 4. 使用 inotify 监听实时变更(前台,保持进程存活) -echo "[Sync] Watching ${SYNC_PATH} for changes..." -inotifywait -m -r -e create,modify,move,delete --format '%w%f' "${SYNC_PATH}" | while read -r file; do - # 过滤临时文件(锚定行尾) - if echo "$file" | grep -qE '\.(tmp|swp|~)$'; then - continue - fi - - echo "[Sync] Change detected: $file" - - # 更新待同步计数 - PENDING=$(cat "${PENDING_FILE}" 2>/dev/null || echo "0") - echo $((PENDING + 1)) > "${PENDING_FILE}" - - # 计算相对路径 - rel_path="${file#${SYNC_PATH}/}" - - if [ -f "$file" ]; then - # 文件创建/修改:上传到 MinIO - mc cp "$file" "${BUCKET_PATH}/${rel_path}" - date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_SYNC_FILE}" - # 成功后将 pending 减 1 - PENDING=$(cat "${PENDING_FILE}" 2>/dev/null || echo "1") - PENDING=$((PENDING - 1)) - [ "${PENDING}" -lt 0 ] && PENDING=0 - echo "${PENDING}" > "${PENDING_FILE}" - elif [ ! -e "$file" ]; then - # 文件删除 - mc rm "${BUCKET_PATH}/${rel_path}" || true - date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_SYNC_FILE}" - PENDING=$(cat "${PENDING_FILE}" 2>/dev/null || echo "1") - PENDING=$((PENDING - 1)) - [ "${PENDING}" -lt 0 ] && PENDING=0 - echo "${PENDING}" > "${PENDING_FILE}" - fi -done diff --git a/projects/volume-manager/.dockerignore b/projects/volume-manager/.dockerignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/projects/volume-manager/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/projects/volume-manager/.env.template b/projects/volume-manager/.env.template new file mode 100644 index 0000000000..a0ae955a26 --- /dev/null +++ b/projects/volume-manager/.env.template @@ -0,0 +1,25 @@ +# 服务监听端口 +VM_PORT=3000 +# 复制为 .env 后修改 + +# 鉴权 Token(必填),FastGPT 侧对应 AGENT_SANDBOX_VOLUME_MANAGER_TOKEN +VM_AUTH_TOKEN=changeme + +# 运行时类型:docker(Docker named volume)或 kubernetes(k8s PVC) +VM_RUNTIME=docker + +# Docker socket 路径(仅 docker 模式) +VM_DOCKER_SOCKET=/var/run/docker.sock + +# k8s 命名空间(仅 kubernetes 模式) +VM_K8S_NAMESPACE=opensandbox +# k8s StorageClass 名称(仅 kubernetes 模式) +VM_K8S_PVC_STORAGE_CLASS=standard +# k8s PVC 容量(仅 kubernetes 模式) +VM_K8S_PVC_STORAGE_SIZE=1Gi + +# volume 名称前缀,最终 volume 名为 {prefix}-{sessionId hash} +VM_VOLUME_NAME_PREFIX=fastgpt-session + +# 日志级别:debug | info | none +VM_LOG_LEVEL=info diff --git a/projects/volume-manager/Dockerfile b/projects/volume-manager/Dockerfile new file mode 100644 index 0000000000..a66abe919a --- /dev/null +++ b/projects/volume-manager/Dockerfile @@ -0,0 +1,15 @@ +# Build: +# docker build -t fastgpt-volume-manager:latest . + +FROM oven/bun:1.3-alpine + +WORKDIR /app + +COPY package.json ./ +RUN bun install --frozen-lockfile + +COPY . . + +EXPOSE 3001 + +CMD ["bun", "src/index.ts"] diff --git a/projects/volume-manager/README.md b/projects/volume-manager/README.md new file mode 100644 index 0000000000..d8f00574c1 --- /dev/null +++ b/projects/volume-manager/README.md @@ -0,0 +1,144 @@ +# volume-manager + +FastGPT Agent 沙箱存储卷管理服务。负责为每个 Agent 会话创建和销毁持久化存储卷,支持 Kubernetes PVC 和 Docker Volume 两种运行时。 + +## 技术栈 + +- **Runtime**: [Bun](https://bun.sh) +- **HTTP 框架**: [Hono](https://hono.dev) +- **参数校验**: [Zod](https://zod.dev) +- **测试**: [Vitest](https://vitest.dev) + +## 快速开始 + +```bash +# 开发模式(热重载) +bun dev + +# 构建 +bun run build + +# 生产启动 +bun start + +# 运行测试 +bun test +``` + +## API + +所有 `/v1/*` 路由需要在请求头中携带 `Authorization: Bearer `。 + +### 健康检查 + +``` +GET /health +``` + +响应:`{ "status": "ok" }` + +### 确保存储卷存在 + +``` +POST /v1/volumes/ensure +Content-Type: application/json + +{ "sessionId": "<24位十六进制字符串>" } +``` + +- 卷已存在:返回 `200`,`{ "claimName": "...", "created": false }` +- 卷新建:返回 `201`,`{ "claimName": "...", "created": true }` + +### 删除存储卷 + +``` +DELETE /v1/volumes/:sessionId +``` + +响应:`204 No Content`(幂等,卷不存在时同样返回 204) + +## 环境变量 + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `VM_AUTH_TOKEN` | ✅ | - | API 鉴权 Token | +| `VM_RUNTIME` | | `kubernetes` | 运行时:`kubernetes` 或 `docker` | +| `VM_PORT` | | `3001` | 监听端口 | +| `VM_LOG_LEVEL` | | `info` | 日志级别:`debug` / `info` / `none` | +| `VM_VOLUME_NAME_PREFIX` | | `fastgpt-session` | 卷名前缀 | +| `VM_DOCKER_SOCKET` | | `/var/run/docker.sock` | Docker socket 路径(docker 模式) | +| `VM_K8S_NAMESPACE` | | `opensandbox` | PVC 所在命名空间(k8s 模式) | +| `VM_K8S_PVC_STORAGE_CLASS` | | `standard` | PVC StorageClass(k8s 模式) | +| `VM_K8S_PVC_STORAGE_SIZE` | | `1Gi` | PVC 容量(k8s 模式) | + +## Kubernetes 部署要求 + +### StorageClass + +volume-manager 默认使用 StorageClass `fastgpt-local`(可通过 `VM_K8S_PVC_STORAGE_CLASS` 覆盖)。参考配置: + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: fastgpt-local +provisioner: rancher.io/local-path +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +``` + +关键特性说明: + +- `reclaimPolicy: Delete`:PVC 删除时自动清理底层数据 +- `volumeBindingMode: WaitForFirstConsumer`:延迟绑定,等待 Pod 调度后再绑定节点 + +也可使用集群现有的其他 StorageClass,需支持 `ReadWriteOnce` accessMode。 + +### RBAC 权限 + +volume-manager 需要在 `VM_K8S_NAMESPACE` 命名空间内操作 PVC,最小权限如下: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "create", "delete"] +``` + +volume-manager 使用集群内 ServiceAccount 认证,无需挂载外部 kubeconfig。 + +### 部署检查清单 + +- [ ] 命名空间 `opensandbox`(或自定义值)已存在 +- [ ] StorageClass `fastgpt-local`(或自定义值)已创建并可用 +- [ ] ServiceAccount + Role + RoleBinding 已创建 +- [ ] Secret 中包含有效的 `VM_AUTH_TOKEN` + +## 项目结构 + +``` +src/ +├── index.ts # 入口,HTTP 服务器初始化 +├── env.ts # 环境变量校验 +├── routes/ +│ └── volumes.ts # /v1/volumes 路由 +├── services/ +│ └── VolumeService.ts # 业务逻辑层 +├── drivers/ +│ ├── IVolumeDriver.ts # 驱动接口 +│ ├── DockerVolumeDriver.ts +│ └── K8sVolumeDriver.ts +└── utils/ + ├── naming.ts # 卷名生成(sessionId → volume name) + └── logger.ts # 日志工具 +``` + +## 日志 + +通过 `VM_LOG_LEVEL` 控制: + +- `none` — 关闭所有业务日志 +- `info` — 输出关键操作(请求进入、操作结果) +- `debug` — 输出详细信息(驱动层请求 URL、HTTP 响应状态) diff --git a/projects/volume-manager/bun.lock b/projects/volume-manager/bun.lock new file mode 100644 index 0000000000..8d148a4261 --- /dev/null +++ b/projects/volume-manager/bun.lock @@ -0,0 +1,209 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@fastgpt/volume-manager", + "dependencies": { + "hono": "^4.6.0", + "zod": "^4", + }, + "devDependencies": { + "@types/bun": "latest", + "vitest": "^2.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], + + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], + + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], + + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], + + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], + + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], + + "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + } +} diff --git a/projects/volume-manager/package.json b/projects/volume-manager/package.json new file mode 100644 index 0000000000..cc6901668d --- /dev/null +++ b/projects/volume-manager/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fastgpt/volume-manager", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "bun build src/index.ts --outdir dist --target bun", + "start": "bun dist/index.js", + "test": "vitest --run" + }, + "dependencies": { + "hono": "^4.6.0", + "zod": "^4" + }, + "devDependencies": { + "@types/bun": "latest", + "vitest": "^2.0.0" + } +} diff --git a/projects/volume-manager/src/drivers/DockerVolumeDriver.ts b/projects/volume-manager/src/drivers/DockerVolumeDriver.ts new file mode 100644 index 0000000000..78ec30f3ba --- /dev/null +++ b/projects/volume-manager/src/drivers/DockerVolumeDriver.ts @@ -0,0 +1,70 @@ +import type { IVolumeDriver, EnsureResult } from './IVolumeDriver'; +import { toVolumeName } from '../utils/naming'; +import { env } from '../env'; +import { logDebug } from '../utils/logger'; + +export class DockerVolumeDriver implements IVolumeDriver { + private readonly socketPath: string; + private readonly prefix: string; + + constructor(socketPath = env.VM_DOCKER_SOCKET, prefix = env.VM_VOLUME_NAME_PREFIX) { + this.socketPath = socketPath; + this.prefix = prefix; + } + + private dockerFetch(path: string, init?: RequestInit): Promise { + // Bun supports unix socket via the `unix` fetch option + return fetch(`http://localhost/v1.41${path}`, { + ...init, + // @ts-ignore - Bun-specific option + unix: this.socketPath + }); + } + + async ensure(sessionId: string): Promise { + const name = toVolumeName(this.prefix, sessionId); + + // Check if volume already exists + logDebug(`Docker inspect volume name=${name}`); + const inspectRes = await this.dockerFetch(`/volumes/${name}`); + logDebug(`Docker inspect volume status=${inspectRes.status}`); + + if (inspectRes.ok) { + return { claimName: name, created: false }; + } + + if (inspectRes.status !== 404) { + const text = await inspectRes.text().catch(() => ''); + throw new Error(`Docker volume inspect failed (${inspectRes.status}): ${text}`); + } + + // Create volume + logDebug(`Docker create volume name=${name}`); + const createRes = await this.dockerFetch('/volumes/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Name: name }) + }); + logDebug(`Docker create volume status=${createRes.status}`); + + if (!createRes.ok) { + const text = await createRes.text().catch(() => ''); + throw new Error(`Docker volume create failed (${createRes.status}): ${text}`); + } + + return { claimName: name, created: true }; + } + + async remove(sessionId: string): Promise { + const name = toVolumeName(this.prefix, sessionId); + logDebug(`Docker remove volume name=${name}`); + const res = await this.dockerFetch(`/volumes/${name}`, { method: 'DELETE' }); + logDebug(`Docker remove volume status=${res.status}`); + + // 404 is idempotent success + if (!res.ok && res.status !== 404) { + const text = await res.text().catch(() => ''); + throw new Error(`Docker volume delete failed (${res.status}): ${text}`); + } + } +} diff --git a/projects/volume-manager/src/drivers/IVolumeDriver.ts b/projects/volume-manager/src/drivers/IVolumeDriver.ts new file mode 100644 index 0000000000..4ec339b070 --- /dev/null +++ b/projects/volume-manager/src/drivers/IVolumeDriver.ts @@ -0,0 +1,9 @@ +export type EnsureResult = { + claimName: string; + created: boolean; +}; + +export interface IVolumeDriver { + ensure(sessionId: string): Promise; + remove(sessionId: string): Promise; +} diff --git a/projects/volume-manager/src/drivers/K8sVolumeDriver.ts b/projects/volume-manager/src/drivers/K8sVolumeDriver.ts new file mode 100644 index 0000000000..7b74dc220b --- /dev/null +++ b/projects/volume-manager/src/drivers/K8sVolumeDriver.ts @@ -0,0 +1,114 @@ +import { readFileSync } from 'fs'; +import type { IVolumeDriver, EnsureResult } from './IVolumeDriver'; +import { toVolumeName } from '../utils/naming'; +import { env } from '../env'; +import { logDebug } from '../utils/logger'; + +const K8S_API = 'https://kubernetes.default.svc'; +const TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'; +const CA_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'; + +function readToken(): string { + return readFileSync(TOKEN_PATH, 'utf-8').trim(); +} + +function fetchOpts(extra: RequestInit = {}): RequestInit { + return { ...extra, tls: { ca: readFileSync(CA_PATH, 'utf-8') } } as RequestInit; +} + +function pvcBody(name: string, sessionId: string): object { + return { + apiVersion: 'v1', + kind: 'PersistentVolumeClaim', + metadata: { + name, + namespace: env.VM_K8S_NAMESPACE, + labels: { 'fastgpt/session-id': sessionId } + }, + spec: { + accessModes: ['ReadWriteOnce'], + resources: { requests: { storage: env.VM_K8S_PVC_STORAGE_SIZE } }, + storageClassName: env.VM_K8S_PVC_STORAGE_CLASS + } + }; +} + +export class K8sVolumeDriver implements IVolumeDriver { + private readonly namespace: string; + private readonly prefix: string; + + constructor(namespace = env.VM_K8S_NAMESPACE, prefix = env.VM_VOLUME_NAME_PREFIX) { + this.namespace = namespace; + this.prefix = prefix; + } + + private headers(): Record { + return { + Authorization: `Bearer ${readToken()}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + }; + } + + private pvcUrl(name?: string): string { + const base = `${K8S_API}/api/v1/namespaces/${this.namespace}/persistentvolumeclaims`; + return name ? `${base}/${name}` : base; + } + + async ensure(sessionId: string): Promise { + const name = toVolumeName(this.prefix, sessionId); + const getUrl = this.pvcUrl(name); + + logDebug(`K8s GET PVC url=${getUrl}`); + const getRes = await fetch(getUrl, fetchOpts({ headers: this.headers() })); + logDebug(`K8s GET PVC status=${getRes.status}`); + + if (getRes.ok) { + return { claimName: name, created: false }; + } + + if (getRes.status !== 404) { + const text = await getRes.text().catch(() => ''); + throw new Error(`K8s PVC GET failed (${getRes.status}): ${text}`); + } + + const postUrl = this.pvcUrl(); + logDebug(`K8s POST PVC url=${postUrl} name=${name}`); + const createRes = await fetch( + postUrl, + fetchOpts({ + method: 'POST', + headers: this.headers(), + body: JSON.stringify(pvcBody(name, sessionId)) + }) + ); + logDebug(`K8s POST PVC status=${createRes.status}`); + + if (!createRes.ok) { + const text = await createRes.text().catch(() => ''); + throw new Error(`K8s PVC create failed (${createRes.status}): ${text}`); + } + + return { claimName: name, created: true }; + } + + async remove(sessionId: string): Promise { + const name = toVolumeName(this.prefix, sessionId); + const delUrl = this.pvcUrl(name); + + logDebug(`K8s DELETE PVC url=${delUrl}`); + const res = await fetch( + delUrl, + fetchOpts({ + method: 'DELETE', + headers: this.headers() + }) + ); + logDebug(`K8s DELETE PVC status=${res.status}`); + + if (!res.ok && res.status !== 404) { + const text = await res.text().catch(() => ''); + throw new Error(`K8s PVC delete failed (${res.status}): ${text}`); + } + } +} diff --git a/projects/volume-manager/src/env.ts b/projects/volume-manager/src/env.ts new file mode 100644 index 0000000000..57e072c209 --- /dev/null +++ b/projects/volume-manager/src/env.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +const schema = z.object({ + VM_AUTH_TOKEN: z.string().min(1), + VM_RUNTIME: z.enum(['docker', 'kubernetes']).default('kubernetes'), + VM_DOCKER_SOCKET: z.string().default('/var/run/docker.sock'), + VM_K8S_NAMESPACE: z.string().default('opensandbox'), + VM_K8S_PVC_STORAGE_CLASS: z.string().default('standard'), + VM_K8S_PVC_STORAGE_SIZE: z.string().default('1Gi'), + VM_VOLUME_NAME_PREFIX: z.string().default('fastgpt-session'), + VM_PORT: z.coerce.number().default(3001), + VM_LOG_LEVEL: z.enum(['debug', 'info', 'none']).default('info') +}); + +const result = schema.safeParse(process.env); + +if (!result.success) { + const missing = result.error.issues.map((i) => i.path.join('.')).join(', '); + console.error(`[volume-manager] Invalid environment variables: ${missing}`); + process.exit(1); +} + +export const env = result.data; diff --git a/projects/volume-manager/src/index.ts b/projects/volume-manager/src/index.ts new file mode 100644 index 0000000000..d2d2444b13 --- /dev/null +++ b/projects/volume-manager/src/index.ts @@ -0,0 +1,34 @@ +import { Hono } from 'hono'; +import { env } from './env'; +import { DockerVolumeDriver } from './drivers/DockerVolumeDriver'; +import { K8sVolumeDriver } from './drivers/K8sVolumeDriver'; +import { VolumeService } from './services/VolumeService'; +import { volumeRoutes } from './routes/volumes'; + +// Select driver based on runtime +const driver = env.VM_RUNTIME === 'docker' ? new DockerVolumeDriver() : new K8sVolumeDriver(); +const service = new VolumeService(driver); + +const app = new Hono(); + +// Health check (no auth) +app.get('/health', (c) => c.json({ status: 'ok' })); + +// Auth middleware for /v1/* routes +app.use('/v1/*', async (c, next) => { + const authHeader = c.req.header('Authorization'); + if (authHeader !== `Bearer ${env.VM_AUTH_TOKEN}`) { + return c.json({ error: 'Unauthorized' }, 401); + } + await next(); +}); + +// Volume routes +app.route('/v1/volumes', volumeRoutes(service)); + +const server = Bun.serve({ + port: env.VM_PORT, + fetch: app.fetch +}); + +console.log(`[volume-manager] Listening on port ${server.port} (runtime: ${env.VM_RUNTIME})`); diff --git a/projects/volume-manager/src/routes/volumes.ts b/projects/volume-manager/src/routes/volumes.ts new file mode 100644 index 0000000000..a8cd657245 --- /dev/null +++ b/projects/volume-manager/src/routes/volumes.ts @@ -0,0 +1,39 @@ +import { Hono } from 'hono'; +import { z } from 'zod'; +import type { VolumeService } from '../services/VolumeService'; +import { logInfo } from '../utils/logger'; + +const ensureBodySchema = z.object({ + sessionId: z.string() +}); + +export function volumeRoutes(service: VolumeService): Hono { + const app = new Hono(); + + // POST /v1/volumes/ensure + app.post('/ensure', async (c) => { + const body = await c.req.json().catch(() => null); + const parsed = ensureBodySchema.safeParse(body); + if (!parsed.success) { + return c.json({ error: 'Invalid request body', details: parsed.error.issues }, 400); + } + + const { sessionId } = parsed.data; + logInfo(`POST /v1/volumes/ensure sessionId=${sessionId}`); + const result = await service.ensure(sessionId); + const status = result.created ? 201 : 200; + logInfo(`ensure done claimName=${result.claimName} created=${result.created} status=${status}`); + return c.json(result, status); + }); + + // DELETE /v1/volumes/:sessionId + app.delete('/:sessionId', async (c) => { + const sessionId = c.req.param('sessionId'); + logInfo(`DELETE /v1/volumes/${sessionId}`); + await service.remove(sessionId); + logInfo(`remove done sessionId=${sessionId}`); + return c.body(null, 204); + }); + + return app; +} diff --git a/projects/volume-manager/src/services/VolumeService.ts b/projects/volume-manager/src/services/VolumeService.ts new file mode 100644 index 0000000000..cfbf52a923 --- /dev/null +++ b/projects/volume-manager/src/services/VolumeService.ts @@ -0,0 +1,19 @@ +import type { IVolumeDriver, EnsureResult } from '../drivers/IVolumeDriver'; +import { logDebug } from '../utils/logger'; + +export class VolumeService { + constructor(private readonly driver: IVolumeDriver) {} + + async ensure(sessionId: string): Promise { + logDebug(`VolumeService.ensure sessionId=${sessionId}`); + const result = await this.driver.ensure(sessionId); + logDebug(`VolumeService.ensure done claimName=${result.claimName} created=${result.created}`); + return result; + } + + async remove(sessionId: string): Promise { + logDebug(`VolumeService.remove sessionId=${sessionId}`); + await this.driver.remove(sessionId); + logDebug(`VolumeService.remove done sessionId=${sessionId}`); + } +} diff --git a/projects/volume-manager/src/utils/logger.ts b/projects/volume-manager/src/utils/logger.ts new file mode 100644 index 0000000000..d6b954c344 --- /dev/null +++ b/projects/volume-manager/src/utils/logger.ts @@ -0,0 +1,11 @@ +import { env } from '../env'; + +export function logInfo(msg: string, ...args: unknown[]): void { + if (env.VM_LOG_LEVEL === 'none') return; + console.log(`[volume-manager] ${msg}`, ...args); +} + +export function logDebug(msg: string, ...args: unknown[]): void { + if (env.VM_LOG_LEVEL !== 'debug') return; + console.log(`[volume-manager:debug] ${msg}`, ...args); +} diff --git a/projects/volume-manager/src/utils/naming.ts b/projects/volume-manager/src/utils/naming.ts new file mode 100644 index 0000000000..e173eed084 --- /dev/null +++ b/projects/volume-manager/src/utils/naming.ts @@ -0,0 +1,12 @@ +// sessionId: lowercase alphanumeric and hyphens, no leading/trailing hyphen, 1-253 chars +const SESSION_ID_RE = /^[a-z0-9]([a-z0-9-]{0,251}[a-z0-9])?$/; + +export function toVolumeName(prefix: string, sessionId: string): string { + const normalized = sessionId.toLowerCase(); + if (!SESSION_ID_RE.test(normalized)) { + throw new Error( + `Invalid sessionId: must be lowercase alphanumeric/hyphens, got "${sessionId}"` + ); + } + return `${prefix}-${normalized}`; +} diff --git a/projects/volume-manager/test/unit/DockerVolumeDriver.test.ts b/projects/volume-manager/test/unit/DockerVolumeDriver.test.ts new file mode 100644 index 0000000000..ac133489e4 --- /dev/null +++ b/projects/volume-manager/test/unit/DockerVolumeDriver.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +const VALID_ID = 'a1b2c3d4e5f6a1b2c3d4e5f6'; +const VOLUME_NAME = `fastgpt-session-${VALID_ID}`; + +// Mock env before importing driver +vi.mock('../../src/env', () => ({ + env: { + VM_DOCKER_SOCKET: '/var/run/docker.sock', + VM_VOLUME_NAME_PREFIX: 'fastgpt-session' + } +})); + +describe('DockerVolumeDriver', () => { + let fetchMock: ReturnType; + + beforeEach(async () => { + fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('ensure returns created=false when volume already exists', async () => { + fetchMock.mockResolvedValueOnce({ ok: true, status: 200 }); + const { DockerVolumeDriver } = await import('../../src/drivers/DockerVolumeDriver'); + const driver = new DockerVolumeDriver(); + const result = await driver.ensure(VALID_ID); + expect(result).toEqual({ claimName: VOLUME_NAME, created: false }); + }); + + it('ensure creates volume on 404', async () => { + fetchMock + .mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' }) + .mockResolvedValueOnce({ ok: true, status: 201 }); + const { DockerVolumeDriver } = await import('../../src/drivers/DockerVolumeDriver'); + const driver = new DockerVolumeDriver(); + const result = await driver.ensure(VALID_ID); + expect(result).toEqual({ claimName: VOLUME_NAME, created: true }); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('ensure throws on unexpected inspect error', async () => { + fetchMock.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'server error' }); + const { DockerVolumeDriver } = await import('../../src/drivers/DockerVolumeDriver'); + const driver = new DockerVolumeDriver(); + await expect(driver.ensure(VALID_ID)).rejects.toThrow('500'); + }); + + it('remove treats 404 as success', async () => { + fetchMock.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' }); + const { DockerVolumeDriver } = await import('../../src/drivers/DockerVolumeDriver'); + const driver = new DockerVolumeDriver(); + await expect(driver.remove(VALID_ID)).resolves.toBeUndefined(); + }); +}); diff --git a/projects/volume-manager/test/unit/K8sVolumeDriver.test.ts b/projects/volume-manager/test/unit/K8sVolumeDriver.test.ts new file mode 100644 index 0000000000..b675d32d4c --- /dev/null +++ b/projects/volume-manager/test/unit/K8sVolumeDriver.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +const VALID_ID = 'a1b2c3d4e5f6a1b2c3d4e5f6'; +const VOLUME_NAME = `fastgpt-session-${VALID_ID}`; + +vi.mock('../../src/env', () => ({ + env: { + VM_K8S_NAMESPACE: 'opensandbox', + VM_VOLUME_NAME_PREFIX: 'fastgpt-session', + VM_K8S_PVC_STORAGE_CLASS: 'standard', + VM_K8S_PVC_STORAGE_SIZE: '1Gi' + } +})); + +// Mock token and CA file reads +vi.mock('fs', () => ({ + readFileSync: vi.fn((path: string) => { + if (path.endsWith('ca.crt')) return 'mock-ca-cert'; + return 'mock-token'; + }) +})); + +describe('K8sVolumeDriver', () => { + let fetchMock: ReturnType; + + beforeEach(() => { + fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('ensure returns created=false when PVC already exists', async () => { + fetchMock.mockResolvedValueOnce({ ok: true, status: 200 }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + const result = await driver.ensure(VALID_ID); + expect(result).toEqual({ claimName: VOLUME_NAME, created: false }); + }); + + it('ensure creates PVC on 404', async () => { + fetchMock + .mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' }) + .mockResolvedValueOnce({ ok: true, status: 201 }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + const result = await driver.ensure(VALID_ID); + expect(result).toEqual({ claimName: VOLUME_NAME, created: true }); + }); + + it('ensure throws on unexpected GET error', async () => { + fetchMock.mockResolvedValueOnce({ ok: false, status: 403, text: async () => 'forbidden' }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + await expect(driver.ensure(VALID_ID)).rejects.toThrow('403'); + }); + + it('fetch calls include tls.ca from ca.crt', async () => { + fetchMock.mockResolvedValueOnce({ ok: true, status: 200 }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + await driver.ensure(VALID_ID); + const [, opts] = fetchMock.mock.calls[0]; + expect((opts as any).tls?.ca).toBe('mock-ca-cert'); + }); + + it('remove treats 404 as success', async () => { + fetchMock.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + await expect(driver.remove(VALID_ID)).resolves.toBeUndefined(); + }); + + it('remove throws on unexpected DELETE error', async () => { + fetchMock.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'error' }); + const { K8sVolumeDriver } = await import('../../src/drivers/K8sVolumeDriver'); + const driver = new K8sVolumeDriver(); + await expect(driver.remove(VALID_ID)).rejects.toThrow('500'); + }); +}); diff --git a/projects/volume-manager/test/unit/VolumeService.test.ts b/projects/volume-manager/test/unit/VolumeService.test.ts new file mode 100644 index 0000000000..63439f1b22 --- /dev/null +++ b/projects/volume-manager/test/unit/VolumeService.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock env before importing service (logger transitively imports env) +vi.mock('../../src/env', () => ({ + env: { VM_LOG_LEVEL: 'none' } +})); + +import { VolumeService } from '../../src/services/VolumeService'; +import type { IVolumeDriver } from '../../src/drivers/IVolumeDriver'; + +function makeDriver(): IVolumeDriver { + return { + ensure: vi.fn(), + remove: vi.fn() + }; +} + +const VALID_ID = 'a1b2c3d4e5f6a1b2c3d4e5f6'; + +describe('VolumeService', () => { + let driver: IVolumeDriver; + let service: VolumeService; + + beforeEach(() => { + driver = makeDriver(); + service = new VolumeService(driver); + }); + + it('delegates ensure to driver', async () => { + vi.mocked(driver.ensure).mockResolvedValue({ + claimName: 'fastgpt-session-' + VALID_ID, + created: true + }); + const result = await service.ensure(VALID_ID); + expect(driver.ensure).toHaveBeenCalledWith(VALID_ID); + expect(result.created).toBe(true); + }); + + it('delegates remove to driver', async () => { + vi.mocked(driver.remove).mockResolvedValue(undefined); + await service.remove(VALID_ID); + expect(driver.remove).toHaveBeenCalledWith(VALID_ID); + }); +}); diff --git a/projects/volume-manager/test/unit/naming.test.ts b/projects/volume-manager/test/unit/naming.test.ts new file mode 100644 index 0000000000..10e345d08b --- /dev/null +++ b/projects/volume-manager/test/unit/naming.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { toVolumeName } from '../../src/utils/naming'; + +describe('toVolumeName', () => { + it('returns prefix-sessionId for valid 24-char hex', () => { + const id = 'a'.repeat(24); + expect(toVolumeName('fastgpt-session', id)).toBe(`fastgpt-session-${id}`); + }); + + it('normalizes uppercase to lowercase', () => { + expect(toVolumeName('pfx', 'ABC123')).toBe('pfx-abc123'); + }); + + it('accepts debug-mode sessionId', () => { + const id = 'debug-69bb6a1aee77d10e6fb58e2d-7BdojPlukIQw'; + expect(toVolumeName('fastgpt-session', id)).toBe(`fastgpt-session-${id.toLowerCase()}`); + }); + + it('accepts single character sessionId', () => { + expect(toVolumeName('pfx', 'a')).toBe('pfx-a'); + }); + + it('throws for sessionId with leading hyphen', () => { + expect(() => toVolumeName('pfx', '-abc123')).toThrow('Invalid sessionId'); + }); + + it('throws for sessionId with trailing hyphen', () => { + expect(() => toVolumeName('pfx', 'abc123-')).toThrow('Invalid sessionId'); + }); + + it('throws for empty sessionId', () => { + expect(() => toVolumeName('pfx', '')).toThrow('Invalid sessionId'); + }); + + it('throws for sessionId with invalid characters', () => { + expect(() => toVolumeName('pfx', 'abc_123')).toThrow('Invalid sessionId'); + }); +}); diff --git a/projects/volume-manager/tsconfig.json b/projects/volume-manager/tsconfig.json new file mode 100644 index 0000000000..15b3fcefec --- /dev/null +++ b/projects/volume-manager/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "types": ["bun-types"] + }, + "include": ["src", "test"] +} diff --git a/projects/volume-manager/vitest.config.ts b/projects/volume-manager/vitest.config.ts new file mode 100644 index 0000000000..635d6444cf --- /dev/null +++ b/projects/volume-manager/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node' + } +}); diff --git a/test/cases/service/core/ai/sandbox/sandbox.integration.test.ts b/test/cases/service/core/ai/sandbox/sandbox.integration.test.ts index ba65424a83..58bcc649ee 100644 --- a/test/cases/service/core/ai/sandbox/sandbox.integration.test.ts +++ b/test/cases/service/core/ai/sandbox/sandbox.integration.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect, afterAll, beforeAll, vi } from 'vitest'; import { MongoSandboxInstance } from '@fastgpt/service/core/ai/sandbox/schema'; import { - SandboxClient, + getSandboxClient, + type SandboxClient, deleteSandboxesByChatIds, deleteSandboxesByAppId } from '@fastgpt/service/core/ai/sandbox/controller'; @@ -11,16 +12,26 @@ import { delay } from '@fastgpt/global/common/system/utils'; const { Types } = connectionMongo; -const hasSandboxEnv = !!( - process.env.AGENT_SANDBOX_PROVIDER && - process.env.AGENT_SANDBOX_SEALOS_BASEURL && - process.env.AGENT_SANDBOX_SEALOS_TOKEN -); +const hasSandboxEnv = !!process.env.AGENT_SANDBOX_PROVIDER; vi.mock('@fastgpt/service/env', () => ({ env: { AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER, AGENT_SANDBOX_SEALOS_BASEURL: process.env.AGENT_SANDBOX_SEALOS_BASEURL, - AGENT_SANDBOX_SEALOS_TOKEN: process.env.AGENT_SANDBOX_SEALOS_TOKEN + AGENT_SANDBOX_SEALOS_TOKEN: process.env.AGENT_SANDBOX_SEALOS_TOKEN, + + AGENT_SANDBOX_OPENSANDBOX_BASEURL: process.env.AGENT_SANDBOX_OPENSANDBOX_BASEURL, + AGENT_SANDBOX_OPENSANDBOX_API_KEY: process.env.AGENT_SANDBOX_OPENSANDBOX_API_KEY, + AGENT_SANDBOX_OPENSANDBOX_RUNTIME: process.env.AGENT_SANDBOX_OPENSANDBOX_RUNTIME, + AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO, + AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG, + AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: + process.env.AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY, + AGENT_SANDBOX_ENABLE_VOLUME: process.env.AGENT_SANDBOX_ENABLE_VOLUME, + AGENT_SANDBOX_VOLUME_MANAGER_URL: process.env.AGENT_SANDBOX_VOLUME_MANAGER_URL, + AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: process.env.AGENT_SANDBOX_VOLUME_MANAGER_TOKEN, + AGENT_SANDBOX_VOLUME_MANAGER_MOUNT_PATH: '/home/sandbox', + + AGENT_SANDBOX_E2B_API_KEY: process.env.AGENT_SANDBOX_E2B_API_KEY } })); @@ -35,7 +46,7 @@ describe.skipIf(!hasSandboxEnv).sequential('Sandbox Integration', () => { // 测试开始前,确认 workspace 存在 beforeAll(async () => { - sandbox = new SandboxClient(testParams); + sandbox = await getSandboxClient(testParams); const result = await sandbox.exec(`mkdir -p ${testDir} && cd ${testDir}`); expect(result.exitCode).toBe(0); await delay(2000); @@ -152,8 +163,8 @@ describe.skipIf(!hasSandboxEnv).sequential('Sandbox Integration', () => { const chatId1 = `${testParams.chatId}-1`; const chatId2 = `${testParams.chatId}-2`; - const sandbox1 = new SandboxClient({ ...testParams, chatId: chatId1 }); - const sandbox2 = new SandboxClient({ ...testParams, chatId: chatId2 }); + const sandbox1 = await getSandboxClient({ ...testParams, chatId: chatId1 }); + const sandbox2 = await getSandboxClient({ ...testParams, chatId: chatId2 }); await sandbox1.exec('echo test1'); await sandbox2.exec('echo test2'); @@ -173,8 +184,8 @@ describe.skipIf(!hasSandboxEnv).sequential('Sandbox Integration', () => { const chatId1 = `${testParams.chatId}-app-1`; const chatId2 = `${testParams.chatId}-app-2`; - const sandbox1 = new SandboxClient({ ...testParams, chatId: chatId1 }); - const sandbox2 = new SandboxClient({ ...testParams, chatId: chatId2 }); + const sandbox1 = await getSandboxClient({ ...testParams, chatId: chatId1 }); + const sandbox2 = await getSandboxClient({ ...testParams, chatId: chatId2 }); await sandbox1.exec('echo test1'); await sandbox2.exec('echo test2'); @@ -219,8 +230,8 @@ describe.skipIf(!hasSandboxEnv).sequential('Sandbox Integration', () => { }); it('should handle concurrent sandbox creation with same chatId', async () => { - const sandbox1 = new SandboxClient(testParams); - const sandbox2 = new SandboxClient(testParams); + const sandbox1 = await getSandboxClient(testParams); + const sandbox2 = await getSandboxClient(testParams); const results = await Promise.all([sandbox1.exec('echo test1'), sandbox2.exec('echo test2')]);