Commit cc3a91d0 by Archer Committed by GitHub

Opensandbox (#6657)

* Opensandbox (#6651)

* volumn manager

* feat: opensandbox volumn

* perf: action (#6654)

* perf: action

* doc

* doc

* deploy tml

* update template
parent d0f96723
......@@ -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 以监视模式启动
......
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
......@@ -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
......
......@@ -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 ""
......
......@@ -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
......
......@@ -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
......
......@@ -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
{
"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",
......
......@@ -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:
......
......@@ -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:
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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. 目录最大上限,可通过环境变量配置。
## ⚙️ 优化
......
......@@ -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",
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
......@@ -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 =====
......
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<string> => {
const headers: Record<string, string> = { '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<void> => {
if (!vmConfig.enable) return;
const headers: Record<string, string> = {};
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<VolumeManagerResult | undefined> => {
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;
};
......@@ -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 });
});
});
});
};
......@@ -44,6 +44,12 @@ const SandboxInstanceSchema = new Schema({
},
limit: {
type: SandboxLimitSchema.shape
},
storage: {
type: Schema.Types.Mixed
},
metadata: {
type: Schema.Types.Mixed
}
});
......
......@@ -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<typeof SandboxProviderSchema>;
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<typeof SandboxStorageSchema>;
export const SandboxMetadataSchema = z.object({
sessionKey: z.string().optional(),
volumeEnabled: z.boolean().optional()
});
export type SandboxMetadataType = z.infer<typeof SandboxMetadataSchema>;
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<typeof SandboxInstanceZodSchema>;
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
......
......@@ -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<Respo
try {
const params = SandboxShellToolSchema.parse(parseJsonArgs(call.function.arguments));
const instance = new SandboxClient({
const instance = await getSandboxClient({
appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid),
chatId: workflowProps.chatId
});
const result = await instance.exec(params.command, params.timeout);
const stringToolResponse = JSON.stringify({
......
......@@ -10,11 +10,22 @@ const LogLevelSchema = z.enum(['trace', 'debug', 'info', 'warning', 'error', 'fa
export const env = createEnv({
server: {
// ===== Agent sandbox =====
AGENT_SANDBOX_PROVIDER: z.enum(['sealosdevbox', 'opensandbox', 'e2b']).optional(),
AGENT_SANDBOX_SEALOS_BASEURL: z.string().url().optional(),
AGENT_SANDBOX_SEALOS_TOKEN: z.string().optional(),
AGENT_SANDBOX_OPENSANDBOX_BASEURL: z.string().url().optional(),
AGENT_SANDBOX_OPENSANDBOX_TOKEN: z.string().optional(),
AGENT_SANDBOX_OPENSANDBOX_API_KEY: z.string().optional(),
AGENT_SANDBOX_OPENSANDBOX_RUNTIME: z.enum(['docker', 'kubernetes']).default('docker'),
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: z.string().optional(),
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: z.string().default('latest'),
AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: BoolSchema.default(true),
AGENT_SANDBOX_ENABLE_VOLUME: BoolSchema.default(false),
AGENT_SANDBOX_VOLUME_MANAGER_URL: z.string().url().optional(),
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: z.string().optional(),
AGENT_SANDBOX_VOLUME_MANAGER_MOUNT_PATH: z.string().default('/workspace'),
AGENT_SANDBOX_E2B_API_KEY: z.string().optional(),
LOG_ENABLE_CONSOLE: BoolSchema.default(true),
......
......@@ -8,7 +8,7 @@
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.31",
"@fastgpt-sdk/sandbox-adapter": "^0.0.33",
"@fastgpt-sdk/otel": "catalog:",
"@fastgpt-sdk/storage": "catalog:",
"@fastgpt/global": "workspace:*",
......
# 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
......
......@@ -4,16 +4,24 @@
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}"
# 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
......@@ -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
......
......@@ -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<void
});
// 创建沙盒实例
const sandbox = new SandboxClient({
const sandbox = await getSandboxClient({
appId,
userId: uid,
chatId
......
......@@ -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 } from '@fastgpt/service/core/ai/sandbox/controller';
import {
SandboxFileOperationBodySchema,
type SandboxFileOperationResponse
......@@ -27,7 +27,7 @@ async function handler(
});
// 创建沙盒实例
const sandbox = new SandboxClient({
const sandbox = await getSandboxClient({
appId,
userId: uid,
chatId
......@@ -47,6 +47,7 @@ async function handler(
type: entry.isDirectory ? ('directory' as const) : ('file' as const),
size: entry.isFile ? entry.size : undefined
}));
return { action: 'list', files };
}
......
......@@ -11,7 +11,7 @@ RUN apk add --no-cache nodejs npm && npm install -g pnpm@9
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/global ./packages/global
COPY packages/service ./packages/service
COPY projects/sandbox/ ./projects/sandbox/
COPY projects/code-sandbox/ ./projects/code-sandbox/
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache curl ca-certificates && update-ca-certificates
......@@ -24,7 +24,7 @@ RUN if [ -z "$proxy" ]; then \
fi
# 编译主入口文件
RUN cd /app/projects/sandbox && pnpm build
RUN cd /app/projects/code-sandbox && pnpm build
# ===== Runner Stage =====
FROM oven/bun:1-alpine AS runner
......@@ -33,14 +33,14 @@ WORKDIR /app
ARG proxy
# 复制编译产物(包含 worker 文件,不需要 node_modules)
COPY --from=builder /app/projects/sandbox/dist /app/sandbox
COPY --from=builder /app/projects/code-sandbox/dist /app/code-sandbox
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# 安装 Python、依赖包及工具
RUN apk add --no-cache python3 py3-pip libffi util-linux && \
apk add --no-cache --virtual .build-deps gcc g++ musl-dev python3-dev libffi-dev
COPY projects/sandbox/requirements.txt /tmp/requirements.txt
COPY projects/code-sandbox/requirements.txt /tmp/requirements.txt
RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt && \
rm /tmp/requirements.txt && \
apk del .build-deps
......@@ -56,4 +56,4 @@ ENV SANDBOX_PORT=3000
EXPOSE 3000
CMD ["bun", "/app/sandbox/index.js"]
CMD ["bun", "/app/code-sandbox/index.js"]
......@@ -54,13 +54,13 @@ bun run test
```bash
# 构建
docker build -f projects/sandbox/Dockerfile -t fastgpt-sandbox .
docker build -f projects/code-sandbox/Dockerfile -t fastgpt-code-sandbox .
# 运行
docker run -p 3000:3000 \
-e SANDBOX_TOKEN=your-secret-token \
-e SANDBOX_POOL_SIZE=20 \
fastgpt-sandbox
fastgpt-code-sandbox
```
## API
......@@ -193,7 +193,7 @@ test/
1. **安装包**
```bash
cd projects/sandbox
cd projects/code-sandbox
bun add <package-name>
```
......
{
"name": "sandbox",
"name": "code-sandbox",
"version": "5.0.0",
"description": "FastGPT Code Sandbox - Bun + Hono + 统一子进程模型",
"author": "",
......
# 基于 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"]
# 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"]
#!/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 <prefix>] [--tag <version>] [--target base|k8s|docker|all] [--no-cache] [--platform <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."
#!/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
#!/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
#!/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()
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
[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
#!/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
# 服务监听端口
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
# 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"]
# 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 <VM_AUTH_TOKEN>`
### 健康检查
```
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 响应状态)
{
"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"
}
}
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<Response> {
// 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<EnsureResult> {
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<void> {
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}`);
}
}
}
export type EnsureResult = {
claimName: string;
created: boolean;
};
export interface IVolumeDriver {
ensure(sessionId: string): Promise<EnsureResult>;
remove(sessionId: string): Promise<void>;
}
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<string, string> {
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<EnsureResult> {
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<void> {
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}`);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment