Commit a361a73d by Archer Committed by GitHub

fix: title generation and fix chat api parase (#7192)

* fix: title generation

* refactor: unify chat target responses

* fix: chat api parse

* fix: treat agent abort as normal finish

* fix: vm button animate

* fix: share auth

* fix: sandbox target

* perf: chat ux

* fix: issue

* test: align share history auth input

* fix: sadnbox

* fix(sandbox): avoid package zip pollution; keep archive on restore failure

* fix(sandbox): simplify edit-debug creation and robustly clean up stale provider records

* fix: skill edit

* fix: narrow sandbox restore archive state

* fix: action

* fix: sandbox init

* fix(sandbox): allow stale provider archived instance to init for migration

* refactor(sandbox): touch active instances during initialization & enforce team verification on export/deploy

* fix: return upgrading status for runtime upgrade conflicts

* refactor(sandbox): layer runtime archive services

* doc

* perf: permission modal

* perf: pdf fallback. sandbox ux

* doc

* doc

* test: update sandbox skill edit expectations

---------

Co-authored-by: Xianquan <whoeverimf5@gmail.com>
Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com>
parent 03e45196
...@@ -89,6 +89,23 @@ const service2 = (props: {id:string; service1: typeof service1 }) => { ...@@ -89,6 +89,23 @@ const service2 = (props: {id:string; service1: typeof service1 }) => {
- 多个 schema 文件(如 `evalSchema.ts` + `evalItemSchema.ts`**合并**到单个 `schema.ts` - 多个 schema 文件(如 `evalSchema.ts` + `evalItemSchema.ts`**合并**到单个 `schema.ts`
## 代码风格 ## 代码风格
### 统一导出入口
目录需要对外聚合导出时,只在该目录的 `index.ts` 中统一 re-export。不要为了兼容旧路径创建“转发文件”,也不要让已迁移的旧文件继续 `export ... from ...`
- 对外引用优先从目录入口导入,例如 `@fastgpt/service/core/ai/llm/request`
-`index.ts` 文件应承载实际实现、类型定义或模块私有逻辑,不作为兼容层 re-export
- 旧路径迁移时,直接修改所有引用到新的统一入口;确认无引用后删除旧文件
```typescript
// ❌ 不好的实践:兼容旧路径的转发文件
export { createWorkflowStreamResponseContext } from '@fastgpt/service/core/workflow/utils/streamResponseContext';
// ✅ 好的实践:只在 index.ts 聚合导出
export { createLLMResponse } from './createLLMResponse';
```
### 使用 `type` 进行类型声明,不使用 `interface` ### 使用 `type` 进行类型声明,不使用 `interface`
```typescript ```typescript
......
...@@ -21,16 +21,16 @@ jobs: ...@@ -21,16 +21,16 @@ jobs:
operations-per-run: 10000 operations-per-run: 10000
# 6个月≈180天,标记Issue为过时 # 6个月≈180天,标记Issue为过时
days-before-issue-stale: 90 days-before-issue-stale: 180
# 标记后7天无活动,自动关闭Issue(缓冲期,可设为0立即关闭) # 标记后7天无活动,自动关闭Issue(缓冲期,可设为0立即关闭)
days-before-issue-close: 7 days-before-issue-close: 7
# Issue相关提示与标签 # Issue相关提示与标签
stale-issue-message: > stale-issue-message: >
This issue has not been updated for more than 90 days and is marked as stale. This issue has not been updated for more than 180 days and is marked as stale.
If there is no further activity within 7 days, it will be automatically closed. Please reply to this issue if you need to continue following up on it. If there is no further activity within 7 days, it will be automatically closed. Please reply to this issue if you need to continue following up on it.
close-issue-message: > close-issue-message: >
This issue has been automatically closed due to prolonged inactivity (more than 90 days plus a 7-day grace period). This issue has been automatically closed due to prolonged inactivity (more than 180 days plus a 7-day grace period).
If this issue still needs to be resolved, you can reopen it and supplement the relevant information. If this issue still needs to be resolved, you can reopen it and supplement the relevant information.
stale-issue-label: 'stale' stale-issue-label: 'stale'
close-issue-label: 'auto-closed' close-issue-label: 'auto-closed'
......
...@@ -66,7 +66,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ...@@ -66,7 +66,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据
## 开发注意事项 ## 开发注意事项
- **包管理器**: 使用 pnpm 及 workspace 配置 - **包管理器**: 使用 pnpm 及 workspace 配置
- **Node 版本**: 需要 Node.js >=20.x, pnpm >=9.x - **Node 版本**: 需要 Node.js >=20.x, pnpm =10.x
- **数据库**: 支持 MongoDB、带 pgvector 的 PostgreSQL 或 Milvus 向量存储 - **数据库**: 支持 MongoDB、带 pgvector 的 PostgreSQL 或 Milvus 向量存储
- **AI 集成**: 通过统一接口支持多个 AI 提供商 - **AI 集成**: 通过统一接口支持多个 AI 提供商
- **国际化**: 完整支持中文、英文和日文 - **国际化**: 完整支持中文、英文和日文
...@@ -87,7 +87,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ...@@ -87,7 +87,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据
## 代码规范 ## 代码规范
[FastGPT 代码规范](./.agents/code/syntax.md) - 所有代码编写、修改、重构和测试调整都必须遵守 [FastGPT 代码规范](./.agents/code/syntax.md)。开始改动前先查看相关规范;如果规范与当前实现习惯冲突,优先按规范执行,并只在有明确业务或兼容性理由时说明例外。
### API 入参校验 ### API 入参校验
...@@ -170,3 +170,4 @@ function agent_loop(用户需求){ ...@@ -170,3 +170,4 @@ function agent_loop(用户需求){
3. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。 3. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。
4. 文件输出,使用正确的编码格式,例如UTF-8。 4. 文件输出,使用正确的编码格式,例如UTF-8。
5. 除非用户指明,否则不要编写总结报告。 5. 除非用户指明,否则不要编写总结报告。
6. 每次回复前,先回复一个:"🫡"。
...@@ -71,9 +71,7 @@ These variables are mainly validated by `packages/service/env.ts` and apply to ` ...@@ -71,9 +71,7 @@ These variables are mainly validated by `packages/service/env.ts` and apply to `
| `AGENT_SANDBOX_PROXY_SECRET` | Empty | Shared HMAC secret for the app and agent-sandbox-proxy. Required when Agent Sandbox is enabled; must be at least 32 bytes. | | `AGENT_SANDBOX_PROXY_SECRET` | Empty | Shared HMAC secret for the app and agent-sandbox-proxy. Required when Agent Sandbox is enabled; must be at least 32 bytes. |
| `AGENT_SANDBOX_PROXY_URL` | Empty | Browser-accessible WebSocket URL for agent-sandbox-proxy. Must start with `ws://` or `wss://`. | | `AGENT_SANDBOX_PROXY_URL` | Empty | Browser-accessible WebSocket URL for agent-sandbox-proxy. Must start with `ws://` or `wss://`. |
| `AGENT_SANDBOX_FREE_TIP` | `false` | Whether the frontend shows the Agent Sandbox free-use hint. | | `AGENT_SANDBOX_FREE_TIP` | `false` | Whether the frontend shows the Agent Sandbox free-use hint. |
| `AGENT_SANDBOX_ARCHIVE_MAX_SIZE` | `50` | Maximum Agent sandbox cold archive package size, in MB. Used for archive upload, download, and packaging checks. | | `AGENT_SANDBOX_DISK_MB` | `1024` | Agent Sandbox disk-size baseline, in MB. Cold archive packages use the full value; Skill packages and IDE single-file operations use half the value, rounded to the nearest MB. |
| `AGENT_SANDBOX_SKILL_MAX_SIZE` | `10` | Maximum Skill sandbox package size, in MB. Used for Skill package upload, download, and publishing checks. |
| `AGENT_SANDBOX_MAX_FILE_SIZE` | `10` | Maximum single-file size for Agent sandbox IDE reads, writes, and uploads, in MB. |
| `AGENT_SANDBOX_MAX_EDIT_DEBUG` | `100` | Limit for Agent edit/debug sandboxes. | | `AGENT_SANDBOX_MAX_EDIT_DEBUG` | `100` | Limit for Agent edit/debug sandboxes. |
| `AGENT_SANDBOX_NPM_REGISTRY` | Empty | npm registry used by npm, yarn, pnpm, and bun inside Agent sandboxes. | | `AGENT_SANDBOX_NPM_REGISTRY` | Empty | npm registry used by npm, yarn, pnpm, and bun inside Agent sandboxes. |
| `AGENT_SANDBOX_PYPI_INDEX_URL` | Empty | PyPI index URL used by pip, `python -m pip`, and uv inside Agent sandboxes. | | `AGENT_SANDBOX_PYPI_INDEX_URL` | Empty | PyPI index URL used by pip, `python -m pip`, and uv inside Agent sandboxes. |
......
...@@ -71,9 +71,7 @@ description: projects/app、projects/code-sandbox 与 pro/admin 环境变量说 ...@@ -71,9 +71,7 @@ description: projects/app、projects/code-sandbox 与 pro/admin 环境变量说
| `AGENT_SANDBOX_PROXY_SECRET` | 空 | agent-sandbox-proxy 与主站共用的 HMAC 密钥;启用 Agent Sandbox 时必填,至少 32 字节。 | | `AGENT_SANDBOX_PROXY_SECRET` | 空 | agent-sandbox-proxy 与主站共用的 HMAC 密钥;启用 Agent Sandbox 时必填,至少 32 字节。 |
| `AGENT_SANDBOX_PROXY_URL` | 空 | 浏览器访问 agent-sandbox-proxy 的 WebSocket 地址,必须以 `ws://` 或 `wss://` 开头。 | | `AGENT_SANDBOX_PROXY_URL` | 空 | 浏览器访问 agent-sandbox-proxy 的 WebSocket 地址,必须以 `ws://` 或 `wss://` 开头。 |
| `AGENT_SANDBOX_FREE_TIP` | `false` | 前端是否展示 Agent Sandbox 免费提示。 | | `AGENT_SANDBOX_FREE_TIP` | `false` | 前端是否展示 Agent Sandbox 免费提示。 |
| `AGENT_SANDBOX_ARCHIVE_MAX_SIZE` | `50` | Agent 沙箱冷归档包大小上限,单位 MB;用于归档包的上传、下载和打包校验。 | | `AGENT_SANDBOX_DISK_MB` | `1024` | Agent 沙箱磁盘大小基准,单位 MB;冷归档包上限等于该值,Skill 包和 IDE 单文件上限为该值的一半并四舍五入。 |
| `AGENT_SANDBOX_SKILL_MAX_SIZE` | `10` | Skill sandbox 包大小上限,单位 MB;用于 Skill 包上传、下载和打包发布校验。 |
| `AGENT_SANDBOX_MAX_FILE_SIZE` | `10` | Agent 沙箱 IDE 单文件读写和上传大小上限,单位 MB。 |
| `AGENT_SANDBOX_MAX_EDIT_DEBUG` | `100` | Agent 编辑/调试沙箱数量限制。 | | `AGENT_SANDBOX_MAX_EDIT_DEBUG` | `100` | Agent 编辑/调试沙箱数量限制。 |
| `AGENT_SANDBOX_NPM_REGISTRY` | 空 | Agent 沙箱内 npm、yarn、pnpm、bun 使用的 npm registry。 | | `AGENT_SANDBOX_NPM_REGISTRY` | 空 | Agent 沙箱内 npm、yarn、pnpm、bun 使用的 npm registry。 |
| `AGENT_SANDBOX_PYPI_INDEX_URL` | 空 | Agent 沙箱内 pip、`python -m pip`、uv 使用的 PyPI index URL。 | | `AGENT_SANDBOX_PYPI_INDEX_URL` | 空 | Agent 沙箱内 pip、`python -m pip`、uv 使用的 PyPI index URL。 |
......
...@@ -47,10 +47,8 @@ MAX_FOLDER_DEPTH=4 ...@@ -47,10 +47,8 @@ MAX_FOLDER_DEPTH=4
WORKFLOW_MAX_LOOP_TIMES=100 WORKFLOW_MAX_LOOP_TIMES=100
# Parallel node concurrency limit. The final value is clamped to [5, 100] # Parallel node concurrency limit. The final value is clamped to [5, 100]
WORKFLOW_PARALLEL_MAX_CONCURRENCY=10 WORKFLOW_PARALLEL_MAX_CONCURRENCY=10
# Agent Sandbox cold archive package size limit, in MB # Agent Sandbox disk-size baseline, in MB
AGENT_SANDBOX_ARCHIVE_MAX_SIZE=50 AGENT_SANDBOX_DISK_MB=1024
# Skill sandbox package size limit, in MB
AGENT_SANDBOX_SKILL_MAX_SIZE=10
# Registry used by npm/yarn/bun inside Agent Sandbox # Registry used by npm/yarn/bun inside Agent Sandbox
AGENT_SANDBOX_NPM_REGISTRY= AGENT_SANDBOX_NPM_REGISTRY=
# Python package index used by pip/uv inside Agent Sandbox # Python package index used by pip/uv inside Agent Sandbox
......
...@@ -34,10 +34,8 @@ MAX_FOLDER_DEPTH=4 ...@@ -34,10 +34,8 @@ MAX_FOLDER_DEPTH=4
WORKFLOW_MAX_LOOP_TIMES=100 WORKFLOW_MAX_LOOP_TIMES=100
# 并行节点并发上限,最终会 clamp 到 [5, 100] # 并行节点并发上限,最终会 clamp 到 [5, 100]
WORKFLOW_PARALLEL_MAX_CONCURRENCY=10 WORKFLOW_PARALLEL_MAX_CONCURRENCY=10
# Agent 沙箱冷归档包大小上限,单位 MB # Agent 沙箱磁盘大小基准,单位 MB
AGENT_SANDBOX_ARCHIVE_MAX_SIZE=50 AGENT_SANDBOX_DISK_MB=1024
# Skill sandbox 包大小上限,单位 MB
AGENT_SANDBOX_SKILL_MAX_SIZE=10
# Agent Sandbox 内 npm/yarn/bun 使用的 registry # Agent Sandbox 内 npm/yarn/bun 使用的 registry
AGENT_SANDBOX_NPM_REGISTRY= AGENT_SANDBOX_NPM_REGISTRY=
# Agent Sandbox 内 pip/uv 使用的 Python package index # Agent Sandbox 内 pip/uv 使用的 Python package index
......
...@@ -31,14 +31,14 @@ AGENT_SANDBOX_PROXY_URL=ws://{{host}}:1006 ...@@ -31,14 +31,14 @@ AGENT_SANDBOX_PROXY_URL=ws://{{host}}:1006
If Agent Sandbox is enabled, also update the following images: If Agent Sandbox is enabled, also update the following images:
- Add the fastgpt-agent-sandbox-proxy image with tag v0.2.0-beta3. - Add the fastgpt-agent-sandbox-proxy image with tag v0.2.0-beta2.
- Update the fastgpt-agent-sandbox image tag to v0.2.0-beta3. - Update the fastgpt-agent-sandbox image tag to v0.2.0-beta2.
Also add the `fastgpt-agent-sandbox-proxy` service to `docker-compose.yml`. The example below uses the China Mainland image registry. For global deployments, change the image to `ghcr.io/labring/fastgpt-agent-sandbox-proxy:v0.2.0-beta3`: Also add the `fastgpt-agent-sandbox-proxy` service to `docker-compose.yml`. The example below uses the China Mainland image registry. For global deployments, change the image to `ghcr.io/labring/fastgpt-agent-sandbox-proxy:v0.2.0-beta2`:
```yml ```yml
fastgpt-agent-sandbox-proxy: fastgpt-agent-sandbox-proxy:
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox-proxy:v0.2.0-beta3 image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox-proxy:v0.2.0-beta2
container_name: fastgpt-agent-sandbox-proxy container_name: fastgpt-agent-sandbox-proxy
restart: always restart: always
ports: ports:
......
...@@ -32,9 +32,9 @@ AGENT_SANDBOX_PROXY_URL=ws://{{host}}:1006 ...@@ -32,9 +32,9 @@ AGENT_SANDBOX_PROXY_URL=ws://{{host}}:1006
如果启用 Agent Sandbox,需同步更新下面镜像: 如果启用 Agent Sandbox,需同步更新下面镜像:
- 新增 fastgpt-agent-sandbox-proxy 镜像 tag: v0.2.0-beta2 - 新增 fastgpt-agent-sandbox-proxy 镜像 tag: v0.2.0-beta2
- 更新 fastgpt-agent-sandbox 镜像 tag: v0.2.0-beta3 - 更新 fastgpt-agent-sandbox 镜像 tag: v0.2.0-beta2
同时在 `docker-compose.yml` 中新增 `` 服务。下面示例使用国内镜像源,海外部署可将镜像改为 `ghcr.io/labring/fastgpt-agent-sandbox-proxy:v0.2.0-beta2`: 同时在 `docker-compose.yml` 中新增 `fastgpt-agent-sandbox-proxy` 服务。下面示例使用国内镜像源,海外部署可将镜像改为 `ghcr.io/labring/fastgpt-agent-sandbox-proxy:v0.2.0-beta2`:
```yml ```yml
fastgpt-agent-sandbox-proxy: fastgpt-agent-sandbox-proxy:
......
...@@ -11,15 +11,7 @@ The chat title generation model is no longer configured through the `CHAT_TITLE_ ...@@ -11,15 +11,7 @@ The chat title generation model is no longer configured through the `CHAT_TITLE_
If you previously configured `CHAT_TITLE_MODEL`, remove it from the `fastgpt` and `fastgpt-pro` environment variables, then select the corresponding model in the UI. If you previously configured `CHAT_TITLE_MODEL`, remove it from the `fastgpt` and `fastgpt-pro` environment variables, then select the corresponding model in the UI.
### 2. Add team isolation to LLM request traces ### 2. Clean up legacy Skill Debug chat data
LLM request traces (`llm_request_records`) now include a `teamId` field. `GET /api/core/ai/record/getRecord` queries records by `{ requestId, teamId }` for the current team, preventing a `requestId` from being used to read another team's request body, retrieved Knowledge Base chunks, or model response.
The unique index on `llm_request_records` has also changed from the single `requestId` field to the compound unique index `{ teamId: 1, requestId: 1 }`. If your self-hosted deployment has `SYNC_INDEX` disabled, run an index sync after upgrading so the old `requestId_1` unique index is removed.
Risk: trace records written before this upgrade do not have `teamId`, so they can no longer be queried by `requestId` after the upgrade. The UI will treat them as expired. These records already have a TTL and are intended only for temporary debugging. Export the relevant logs or keep the original request details before upgrading if you need to investigate historical calls.
### 3. Clean up legacy Skill Debug chat data
This version migrates Skill Edit chats to the standard Chat storage model. Historical Skill Debug data wrote `skillId` into the physical `appId` field in the three Chat collections and did not include `sourceType`. Historical sandbox instance records also need `sourceType/sourceId` backfilled. After the upgrade, new Skill Edit chats will not read those legacy records, but we recommend running the root-only initialization API once to migrate sandbox instance ownership fields and clean up legacy Skill Debug chats. This endpoint is only for this upgrade migration and is not exposed as an OpenAPI endpoint. This version migrates Skill Edit chats to the standard Chat storage model. Historical Skill Debug data wrote `skillId` into the physical `appId` field in the three Chat collections and did not include `sourceType`. Historical sandbox instance records also need `sourceType/sourceId` backfilled. After the upgrade, new Skill Edit chats will not read those legacy records, but we recommend running the root-only initialization API once to migrate sandbox instance ownership fields and clean up legacy Skill Debug chats. This endpoint is only for this upgrade migration and is not exposed as an OpenAPI endpoint.
...@@ -60,22 +52,52 @@ Migration logic: ...@@ -60,22 +52,52 @@ Migration logic:
This endpoint does not backfill `sourceType` for existing App Chat records. This endpoint does not backfill `sourceType` for existing App Chat records.
### 4. Configure package registry mirrors for Agent Sandbox ### 3. Update environment variables (optional)
Agent Sandbox now supports package registry mirror configuration. When configured, FastGPT writes mirror configuration files for npm, yarn, bun, pip, and uv under the sandbox HOME directory during sandbox initialization. This improves dependency installation stability in private networks or cross-region network environments. Agent Sandbox now supports package registry mirror configuration. When configured, FastGPT writes mirror configuration files for npm, yarn, bun, pip, and uv under the sandbox HOME directory during sandbox initialization. This improves dependency installation stability in private networks or cross-region network environments.
```dotenv ```dotenv
# Registry used by npm/yarn/bun inside Agent Sandbox # npm registry used by npm/yarn/pnpm/bun inside Agent Sandbox
AGENT_SANDBOX_NPM_REGISTRY= AGENT_SANDBOX_NPM_REGISTRY=
# Python package index used by pip/uv inside Agent Sandbox # PyPI index URL used by pip/python -m pip/uv inside Agent Sandbox
AGENT_SANDBOX_PYPI_INDEX_URL= AGENT_SANDBOX_PYPI_INDEX_URL=
``` ```
The configuration is cached by content hash in the sandbox runtime state, so the same sandbox only rewrites these files when the configuration changes. The configuration is cached by content hash in the sandbox runtime state, so the same sandbox only rewrites these files when the configuration changes.
### 4. Update images
- Update the fastgpt-app (FastGPT main service) image tag to v4.15.0-beta6.
- Update the fastgpt-pro (FastGPT commercial edition) image tag to v4.15.0-beta6.
- Update the fastgpt-plugin image tag to v1.0.0-beta6.
- Update the aiproxy image tag to v0.6.2.
If Agent Sandbox is enabled, also update the following images:
- Update the fastgpt-agent-sandbox-proxy image tag to v0.2.0-beta3.
- Update the fastgpt-agent-sandbox image tag to v0.2.0.-beta3.
## Risks
### 1. Team isolation added to LLM request traces
LLM request traces (`llm_request_records`) now include a `teamId` field. `GET /api/core/ai/record/getRecord` queries records by `{ requestId, teamId }` for the current team, preventing a `requestId` from being used to read another team's request body, retrieved Knowledge Base chunks, or model response.
The unique index on `llm_request_records` has also changed from the single `requestId` field to the compound unique index `{ teamId: 1, requestId: 1 }`. If your self-hosted deployment has `SYNC_INDEX` disabled, run an index sync after upgrading so the old `requestId_1` unique index is removed.
Risk: trace records written before this upgrade do not have `teamId`, so they can no longer be queried by `requestId` after the upgrade. The UI will treat them as expired. These records already have a TTL and are intended only for temporary debugging. Export the relevant logs or keep the original request details before upgrading if you need to investigate historical calls.
## 🚀 New Features
1. The commercial edition now supports local direct-connect debugging for FastGPT plugins.
## ⚙️ Improvements ## ⚙️ Improvements
1. Chat title generation now uses the system default model configuration, making it easier to switch at runtime and manage consistently. 1. Chat title generation now uses the system default model configuration, making it easier to switch at runtime and manage consistently.
2. LLM request traces are now queried with team isolation, and the unique index is now `{ teamId, requestId }` to prevent request IDs from exposing sensitive traces across teams. 2. LLM request traces are now queried with team isolation, and the unique index is now `{ teamId, requestId }` to prevent request IDs from exposing sensitive traces across teams.
3. Skill Edit chats now use the standard Chat storage and cleanup flow, and legacy Skill Debug chats can be cleaned through the initialization API. 3. Skill Edit chats now use the standard Chat storage and cleanup flow, and legacy Skill Debug chats can be cleaned through the initialization API.
4. Agent Sandbox now supports npm and PyPI mirror configuration. During initialization, it writes common package manager configuration files to reduce dependency installation failures inside the sandbox. 4. Agent Sandbox now supports npm and PyPI mirror configuration. During initialization, it writes common package manager configuration files to reduce dependency installation failures inside the sandbox.
## Code Improvements
1. The chat takeover API has been abstracted from app-specific handling into a platform-level capability.
...@@ -11,15 +11,7 @@ description: 'FastGPT V4.15.0-beta6 更新说明' ...@@ -11,15 +11,7 @@ description: 'FastGPT V4.15.0-beta6 更新说明'
如此前配置过 `CHAT_TITLE_MODEL`,升级后可从 `fastgpt` 和 `fastgpt-pro` 的环境变量中移除,并在页面中重新选择对应模型。 如此前配置过 `CHAT_TITLE_MODEL`,升级后可从 `fastgpt` 和 `fastgpt-pro` 的环境变量中移除,并在页面中重新选择对应模型。
### 2. LLM 请求追踪记录增加团队隔离 ### 2. 清理旧 Skill Debug 对话数据
LLM 请求追踪记录(`llm_request_records`)新增 `teamId` 字段,`GET /api/core/ai/record/getRecord` 会按当前登录团队查询 `{ requestId, teamId }`,避免仅凭 `requestId` 读取其他团队的请求体、知识库召回片段和模型响应。
同时,`llm_request_records` 的唯一索引从单字段 `requestId` 调整为复合唯一索引 `{ teamId: 1, requestId: 1 }`。如自托管环境关闭了 `SYNC_INDEX`,升级后需要执行一次索引同步,确保旧的 `requestId_1` 唯一索引被移除。
风险点:升级前已写入的旧追踪记录没有 `teamId`,升级后将无法再通过 `requestId` 查询,页面会按追踪记录已过期处理。该记录本身有 TTL,仅用于临时排查模型调用详情;如需排查历史问题,请在升级前导出相关日志或保留原始请求信息。
### 3. 清理旧 Skill Debug 对话数据
本版本将 Skill Edit 对话迁移到标准 Chat 存储模型。历史 Skill Debug 数据曾把 `skillId` 写入 Chat 三表的物理 `appId` 字段,且没有 `sourceType`;历史 sandbox 实例也需要补齐 `sourceType/sourceId`。升级后旧 Skill Debug 对话不会被新 Skill Edit 对话读取,但建议执行一次 root-only 初始化接口完成 sandbox 实例字段迁移并清理旧 Skill Debug 对话。该接口仅用于本次升级迁移,不作为 OpenAPI 对外接口。 本版本将 Skill Edit 对话迁移到标准 Chat 存储模型。历史 Skill Debug 数据曾把 `skillId` 写入 Chat 三表的物理 `appId` 字段,且没有 `sourceType`;历史 sandbox 实例也需要补齐 `sourceType/sourceId`。升级后旧 Skill Debug 对话不会被新 Skill Edit 对话读取,但建议执行一次 root-only 初始化接口完成 sandbox 实例字段迁移并清理旧 Skill Debug 对话。该接口仅用于本次升级迁移,不作为 OpenAPI 对外接口。
...@@ -60,22 +52,58 @@ curl -X POST 'https://你的域名/api/admin/4150/init4150-beta6' \ ...@@ -60,22 +52,58 @@ curl -X POST 'https://你的域名/api/admin/4150/init4150-beta6' \
该接口不会回填几亿条历史 App Chat 的 `sourceType`。 该接口不会回填几亿条历史 App Chat 的 `sourceType`。
### 4. Agent Sandbox 包管理镜像源配置 ### 3. 更新环境变量(可选)
Agent Sandbox 新增包管理镜像源配置。配置后,Agent Sandbox 初始化时会在 sandbox HOME 下写入 npm、yarn、bun、pip 和 uv 的镜像配置文件,提升在私有网络或跨境网络环境中安装依赖的稳定性。 Agent Sandbox 新增包管理镜像源配置。配置后,Agent Sandbox 初始化时会在 sandbox HOME 下写入 npm、yarn、bun、pip 和 uv 的镜像配置文件,提升在私有网络或跨境网络环境中安装依赖的稳定性。
```dotenv ```dotenv
# Agent Sandbox 内 npm/yarn/bun 使用的 registry # Agent Sandbox 内 npm/yarn/pnpm/bun 使用的 npm registry
AGENT_SANDBOX_NPM_REGISTRY= AGENT_SANDBOX_NPM_REGISTRY=
# Agent Sandbox 内 pip/uv 使用的 Python package index # Agent Sandbox 内 pip/python -m pip/uv 使用的 PyPI index URL
AGENT_SANDBOX_PYPI_INDEX_URL= AGENT_SANDBOX_PYPI_INDEX_URL=
``` ```
该配置会按内容 hash 缓存在 sandbox runtime state 中,同一个 sandbox 仅在配置变化时重新写入。 该配置会按内容 hash 缓存在 sandbox runtime state 中,同一个 sandbox 仅在配置变化时重新写入。
### 4. 更新镜像
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.15.0-beta6
- 更新 fastgpt-pro(fastgpt 商业版) 镜像 tag: v4.15.0-beta6
- 更新 fastgpt-plugin 镜像 tag: v1.0.0-beta6
- 更新 aiproxy 镜像 tag: v0.6.2
如果启用 Agent Sandbox,需同步更新下面镜像:
- 更新 fastgpt-agent-sandbox-proxy 镜像 tag: v0.2.0-beta3
- 更新 fastgpt-agent-sandbox 镜像 tag: v0.2.0-beta3
## 风险点
### 1. LLM 请求追踪记录增加团队隔离
LLM 请求追踪记录(`llm_request_records`)新增 `teamId` 字段,`GET /api/core/ai/record/getRecord` 会按当前登录团队查询 `{ requestId, teamId }`,避免仅凭 `requestId` 读取其他团队的请求体、知识库召回片段和模型响应。
同时,`llm_request_records` 的唯一索引从单字段 `requestId` 调整为复合唯一索引 `{ teamId: 1, requestId: 1 }`。如自托管环境关闭了 `SYNC_INDEX`,升级后需要执行一次索引同步,确保旧的 `requestId_1` 唯一索引被移除。
风险点:升级前已写入的旧追踪记录没有 `teamId`,升级后将无法再通过 `requestId` 查询,页面会按追踪记录已过期处理。该记录本身有 TTL,仅用于临时排查模型调用详情;如需排查历史问题,请在升级前导出相关日志或保留原始请求信息。
## 🚀 新增内容
1. 商业版支持本地直连 FastGPT 调试插件。
2. 沙盒支持自定义 npm 和 pip 源。
## ⚙️ 优化 ## ⚙️ 优化
1. 对话标题生成模型改为使用系统默认模型配置管理,便于运行时切换和统一维护。 1. 对话标题生成模型改为使用系统默认模型配置管理,便于运行时切换和统一维护。
2. LLM 请求追踪记录按团队隔离查询,唯一索引调整为 `{ teamId, requestId }`,避免 `requestId` 被其他团队复用读取敏感 trace。 2. LLM 请求追踪记录按团队隔离查询,唯一索引调整为 `{ teamId, requestId }`,避免 `requestId` 被其他团队复用读取敏感 trace。
3. Skill Edit 对话统一使用标准 Chat 存储和清理链路,历史 Skill Debug 对话可通过初始化接口清理。 3. Skill Edit 对话统一使用标准 Chat 存储和清理链路,历史 Skill Debug 对话可通过初始化接口清理。
4. Agent Sandbox 支持配置 npm 和 PyPI 镜像源,初始化时自动写入常见包管理器配置,减少 sandbox 内依赖安装失败。 4. Agent Sandbox 支持配置 npm 和 PyPI 镜像源,初始化时自动写入常见包管理器配置,减少 sandbox 内依赖安装失败。
5. PDF 解析兼容 `linux/arm64 + Alpine/musl` 架构,回退到 `pdfjs` 解析方案。
## 🐛 修复
1. chat/completions 接口,返回 nodeResponse 时候,过滤掉了 q/a/index,该版本恢复返回。
## 代码优化
1. chat 接管接口抽象,不再绑定 app, 改成平台级别通用。
...@@ -17,12 +17,12 @@ ...@@ -17,12 +17,12 @@
"content/guide/build/evaluation.mdx": "2026-05-07T15:06:40+08:00", "content/guide/build/evaluation.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/faq.en.mdx": "2026-06-04T16:10:15+08:00", "content/guide/build/faq.en.mdx": "2026-06-04T16:10:15+08:00",
"content/guide/build/faq.mdx": "2026-06-04T16:10:15+08:00", "content/guide/build/faq.mdx": "2026-06-04T16:10:15+08:00",
"content/guide/build/general/ai_settings.en.mdx": "2026-05-08T18:08:04+08:00", "content/guide/build/general/ai_settings.en.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/build/general/ai_settings.mdx": "2026-05-08T18:08:04+08:00", "content/guide/build/general/ai_settings.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/build/general/chat_input_guide.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/build/general/chat_input_guide.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/general/chat_input_guide.mdx": "2026-06-01T17:19:55+08:00", "content/guide/build/general/chat_input_guide.mdx": "2026-06-01T17:19:55+08:00",
"content/guide/build/general/fileInput.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/build/general/fileInput.en.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/build/general/fileInput.mdx": "2026-05-07T15:06:40+08:00", "content/guide/build/general/fileInput.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/build/general/voiceInput.en.mdx": "2026-06-24T18:16:41+08:00", "content/guide/build/general/voiceInput.en.mdx": "2026-06-24T18:16:41+08:00",
"content/guide/build/general/voiceInput.mdx": "2026-06-24T18:16:41+08:00", "content/guide/build/general/voiceInput.mdx": "2026-06-24T18:16:41+08:00",
"content/guide/build/general/welcomeText.en.mdx": "2026-06-24T18:16:41+08:00", "content/guide/build/general/welcomeText.en.mdx": "2026-06-24T18:16:41+08:00",
...@@ -105,8 +105,8 @@ ...@@ -105,8 +105,8 @@
"content/guide/chat/quoteList.mdx": "2026-05-07T15:06:40+08:00", "content/guide/chat/quoteList.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/dataset/collection_tags.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/dataset/collection_tags.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/dataset/collection_tags.mdx": "2026-05-07T15:06:40+08:00", "content/guide/dataset/collection_tags.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/dataset/dataset_engine.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/dataset/dataset_engine.en.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/dataset/dataset_engine.mdx": "2026-05-07T15:06:40+08:00", "content/guide/dataset/dataset_engine.mdx": "2026-06-27T18:25:45+08:00",
"content/guide/dataset/faq.en.mdx": "2026-06-04T16:10:15+08:00", "content/guide/dataset/faq.en.mdx": "2026-06-04T16:10:15+08:00",
"content/guide/dataset/faq.mdx": "2026-06-04T16:10:15+08:00", "content/guide/dataset/faq.mdx": "2026-06-04T16:10:15+08:00",
"content/guide/dataset/rag.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/dataset/rag.en.mdx": "2026-05-07T15:06:40+08:00",
...@@ -137,8 +137,8 @@ ...@@ -137,8 +137,8 @@
"content/guide/version/cloud/privacy.mdx": "2026-05-07T15:06:40+08:00", "content/guide/version/cloud/privacy.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/version/cloud/terms.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/version/cloud/terms.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/version/cloud/terms.mdx": "2026-05-28T13:54:38+08:00", "content/guide/version/cloud/terms.mdx": "2026-05-28T13:54:38+08:00",
"content/guide/version/commercial.en.mdx": "2026-06-27T18:52:43+08:00", "content/guide/version/commercial.en.mdx": "2026-06-27T22:05:51+08:00",
"content/guide/version/commercial.mdx": "2026-06-27T18:52:43+08:00", "content/guide/version/commercial.mdx": "2026-06-27T22:05:51+08:00",
"content/guide/version/opensource/intro.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/version/opensource/intro.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/version/opensource/intro.mdx": "2026-05-07T15:06:40+08:00", "content/guide/version/opensource/intro.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/version/opensource/license.en.mdx": "2026-05-07T15:06:40+08:00", "content/guide/version/opensource/license.en.mdx": "2026-05-07T15:06:40+08:00",
...@@ -165,10 +165,10 @@ ...@@ -165,10 +165,10 @@
"content/plugin/intro.mdx": "2026-06-09T16:03:58+08:00", "content/plugin/intro.mdx": "2026-06-09T16:03:58+08:00",
"content/plugin/model-presets.en.mdx": "2026-06-04T16:10:15+08:00", "content/plugin/model-presets.en.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/model-presets.mdx": "2026-06-04T16:10:15+08:00", "content/plugin/model-presets.mdx": "2026-06-04T16:10:15+08:00",
"content/plugin/system-tool-development.en.mdx": "2026-06-27T17:05:53+08:00", "content/plugin/system-tool-development.en.mdx": "2026-06-27T22:05:51+08:00",
"content/plugin/system-tool-development.mdx": "2026-06-27T17:05:53+08:00", "content/plugin/system-tool-development.mdx": "2026-06-27T22:05:51+08:00",
"content/self-host/config/env.en.mdx": "2026-06-26T17:38:34+08:00", "content/self-host/config/env.en.mdx": "2026-06-29T00:06:32+08:00",
"content/self-host/config/env.mdx": "2026-06-26T17:38:34+08:00", "content/self-host/config/env.mdx": "2026-06-29T00:06:32+08:00",
"content/self-host/config/json.en.mdx": "2026-06-22T11:01:59+08:00", "content/self-host/config/json.en.mdx": "2026-06-22T11:01:59+08:00",
"content/self-host/config/json.mdx": "2026-06-22T11:01:59+08:00", "content/self-host/config/json.mdx": "2026-06-22T11:01:59+08:00",
"content/self-host/config/model/intro.en.mdx": "2026-06-04T16:10:15+08:00", "content/self-host/config/model/intro.en.mdx": "2026-06-04T16:10:15+08:00",
...@@ -179,6 +179,8 @@ ...@@ -179,6 +179,8 @@
"content/self-host/config/model/siliconCloud.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/model/siliconCloud.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/object-storage.en.mdx": "2026-05-21T11:24:48+08:00", "content/self-host/config/object-storage.en.mdx": "2026-05-21T11:24:48+08:00",
"content/self-host/config/object-storage.mdx": "2026-05-21T11:24:48+08:00", "content/self-host/config/object-storage.mdx": "2026-05-21T11:24:48+08:00",
"content/self-host/config/remote-debug-suite.en.mdx": "2026-06-27T22:05:51+08:00",
"content/self-host/config/remote-debug-suite.mdx": "2026-06-27T22:05:51+08:00",
"content/self-host/config/signoz.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/signoz.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/signoz.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/config/signoz.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/bge-rerank.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/custom-models/bge-rerank.en.mdx": "2026-04-26T21:08:47+08:00",
...@@ -291,8 +293,8 @@ ...@@ -291,8 +293,8 @@
"content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-15/41500.en.mdx": "2026-06-27T11:15:35+08:00", "content/self-host/upgrading/4-15/41500.en.mdx": "2026-06-29T00:06:32+08:00",
"content/self-host/upgrading/4-15/41500.mdx": "2026-06-27T11:15:35+08:00", "content/self-host/upgrading/4-15/41500.mdx": "2026-06-29T00:06:32+08:00",
"content/self-host/upgrading/4-15/41501.mdx": "2026-06-23T21:09:39+08:00", "content/self-host/upgrading/4-15/41501.mdx": "2026-06-23T21:09:39+08:00",
"content/self-host/upgrading/4-15/41502.en.mdx": "2026-05-25T11:21:30+08:00", "content/self-host/upgrading/4-15/41502.en.mdx": "2026-05-25T11:21:30+08:00",
"content/self-host/upgrading/4-15/41502.mdx": "2026-06-23T13:54:06+08:00", "content/self-host/upgrading/4-15/41502.mdx": "2026-06-23T13:54:06+08:00",
...@@ -300,10 +302,10 @@ ...@@ -300,10 +302,10 @@
"content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00", "content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00",
"content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-10T19:02:59+08:00", "content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-10T19:02:59+08:00",
"content/self-host/upgrading/4-15/41504.mdx": "2026-06-15T23:34:43+08:00", "content/self-host/upgrading/4-15/41504.mdx": "2026-06-15T23:34:43+08:00",
"content/self-host/upgrading/4-15/41505.en.mdx": "2026-06-23T13:54:06+08:00", "content/self-host/upgrading/4-15/41505.en.mdx": "2026-06-28T21:06:49+08:00",
"content/self-host/upgrading/4-15/41505.mdx": "2026-06-24T13:53:05+08:00", "content/self-host/upgrading/4-15/41505.mdx": "2026-06-28T21:06:49+08:00",
"content/self-host/upgrading/4-15/41506.en.mdx": "2026-06-27T11:15:35+08:00", "content/self-host/upgrading/4-15/41506.en.mdx": "2026-06-28T21:06:49+08:00",
"content/self-host/upgrading/4-15/41506.mdx": "2026-06-27T11:15:35+08:00", "content/self-host/upgrading/4-15/41506.mdx": "2026-06-29T00:06:32+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
...@@ -444,6 +446,6 @@ ...@@ -444,6 +446,6 @@
"content/self-host/upgrading/outdated/499.mdx": "2026-05-07T15:06:40+08:00", "content/self-host/upgrading/outdated/499.mdx": "2026-05-07T15:06:40+08:00",
"content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00",
"content/toc.en.mdx": "2026-06-27T18:52:43+08:00", "content/toc.en.mdx": "2026-06-27T22:05:51+08:00",
"content/toc.mdx": "2026-06-27T18:52:43+08:00" "content/toc.mdx": "2026-06-27T22:05:51+08:00"
} }
\ No newline at end of file
...@@ -7,7 +7,8 @@ const startCode = 510000; ...@@ -7,7 +7,8 @@ const startCode = 510000;
export enum SandboxErrEnum { export enum SandboxErrEnum {
agentSandboxPermissionDenied = 'agentSandboxPermissionDenied', agentSandboxPermissionDenied = 'agentSandboxPermissionDenied',
agentSandboxInitializing = 'agentSandboxInitializing', agentSandboxInitializing = 'agentSandboxInitializing',
runtimeUpgradeFailed = 'runtimeUpgradeFailed' runtimeUpgradeFailed = 'runtimeUpgradeFailed',
runtimeUpgradeInProgress = 'runtimeUpgradeInProgress'
} }
const sandboxErr = [ const sandboxErr = [
...@@ -23,6 +24,11 @@ const sandboxErr = [ ...@@ -23,6 +24,11 @@ const sandboxErr = [
{ {
statusText: SandboxErrEnum.runtimeUpgradeFailed, statusText: SandboxErrEnum.runtimeUpgradeFailed,
message: i18nT('common:code_error.sandbox_error.runtime_upgrade_failed') message: i18nT('common:code_error.sandbox_error.runtime_upgrade_failed')
},
{
statusText: SandboxErrEnum.runtimeUpgradeInProgress,
message: i18nT('skill:sandbox_runtime_upgrade_in_progress'),
httpStatus: 409
} }
]; ];
......
...@@ -56,7 +56,6 @@ export type FastGPTFeConfigsType = { ...@@ -56,7 +56,6 @@ export type FastGPTFeConfigsType = {
show_pay?: boolean; show_pay?: boolean;
show_openai_account?: boolean; show_openai_account?: boolean;
show_promotion?: boolean; show_promotion?: boolean;
show_team_chat?: boolean;
show_compliance_copywriting?: boolean; show_compliance_copywriting?: boolean;
show_aiproxy?: boolean; show_aiproxy?: boolean;
show_coupon?: boolean; show_coupon?: boolean;
......
...@@ -16,6 +16,7 @@ export const SandboxStatusEnum = { ...@@ -16,6 +16,7 @@ export const SandboxStatusEnum = {
export type SandboxStatusType = (typeof SandboxStatusEnum)[keyof typeof SandboxStatusEnum]; export type SandboxStatusType = (typeof SandboxStatusEnum)[keyof typeof SandboxStatusEnum];
// ---- 沙盒实例类型 ---- // ---- 沙盒实例类型 ----
/** @deprecated sandbox 实例归属统一使用 sourceType/sourceId;该枚举仅用于历史数据迁移。 */
export enum SandboxTypeEnum { export enum SandboxTypeEnum {
editDebug = 'edit-debug', editDebug = 'edit-debug',
sessionRuntime = 'session-runtime' sessionRuntime = 'session-runtime'
......
...@@ -136,7 +136,10 @@ export const SandboxStorageSchema = z.object({ ...@@ -136,7 +136,10 @@ export const SandboxStorageSchema = z.object({
}); });
export const SandboxInstanceDetailSchema = z.object({ export const SandboxInstanceDetailSchema = z.object({
type: SandboxTypeSchema, type: SandboxTypeSchema.optional().meta({
deprecated: true,
description: '旧版 sandbox 场景字段;业务归属统一使用 sourceType/sourceId。'
}),
teamId: z.string(), teamId: z.string(),
tmbId: z.string(), tmbId: z.string(),
skillId: z.string().optional(), skillId: z.string().optional(),
......
...@@ -16,6 +16,12 @@ export const SelectedAgentSkillItemTypeSchema = z.object({ ...@@ -16,6 +16,12 @@ export const SelectedAgentSkillItemTypeSchema = z.object({
isDeleted: z.boolean().default(false) isDeleted: z.boolean().default(false)
}); });
export type SelectedAgentSkillItemType = z.infer<typeof SelectedAgentSkillItemTypeSchema>; export type SelectedAgentSkillItemType = z.infer<typeof SelectedAgentSkillItemTypeSchema>;
export const StoredSelectedAgentSkillItemTypeSchema = SelectedAgentSkillItemTypeSchema.pick({
skillId: true
});
export type StoredSelectedAgentSkillItemType = z.infer<
typeof StoredSelectedAgentSkillItemTypeSchema
>;
/* ===== Tool ===== */ /* ===== Tool ===== */
export const SelectedToolItemTypeSchema = FlowNodeTemplateTypeSchema.extend({ export const SelectedToolItemTypeSchema = FlowNodeTemplateTypeSchema.extend({
......
...@@ -218,9 +218,6 @@ export const AppSchemaTypeSchema = z.object({ ...@@ -218,9 +218,6 @@ export const AppSchemaTypeSchema = z.object({
}), }),
inited: BoolSchema.optional().meta({ inited: BoolSchema.optional().meta({
deprecated: true deprecated: true
}),
teamTags: z.array(z.string()).optional().meta({
deprecated: true
}) })
}); });
export type AppSchemaType = z.infer<typeof AppSchemaTypeSchema>; export type AppSchemaType = z.infer<typeof AppSchemaTypeSchema>;
......
...@@ -30,7 +30,6 @@ export enum ChatSourceEnum { ...@@ -30,7 +30,6 @@ export enum ChatSourceEnum {
share = 'share', share = 'share',
api = 'api', api = 'api',
cronJob = 'cronJob', cronJob = 'cronJob',
team = 'team',
feishu = 'feishu', feishu = 'feishu',
official_account = 'official_account', official_account = 'official_account',
wecom = 'wecom', wecom = 'wecom',
...@@ -70,10 +69,6 @@ export const ChatSourceMap = { ...@@ -70,10 +69,6 @@ export const ChatSourceMap = {
name: i18nT('chat:source_cronJob'), name: i18nT('chat:source_cronJob'),
color: '#FF81AE' color: '#FF81AE'
}, },
[ChatSourceEnum.team]: {
name: i18nT('common:core.chat.logs.team'),
color: '#42CFC6'
},
[ChatSourceEnum.feishu]: { [ChatSourceEnum.feishu]: {
name: i18nT('common:core.chat.logs.feishu'), name: i18nT('common:core.chat.logs.feishu'),
color: '#39CC83' color: '#39CC83'
...@@ -105,7 +100,6 @@ export enum ChatStatusEnum { ...@@ -105,7 +100,6 @@ export enum ChatStatusEnum {
export enum GetChatTypeEnum { export enum GetChatTypeEnum {
normal = 'normal', normal = 'normal',
outLink = 'outLink', outLink = 'outLink',
team = 'team',
home = 'home' home = 'home'
} }
......
...@@ -57,10 +57,6 @@ export type SandboxStatusPhase = ...@@ -57,10 +57,6 @@ export type SandboxStatusPhase =
| 'downloadingPackage' // downloading skill package from MinIO | 'downloadingPackage' // downloading skill package from MinIO
| 'uploadingPackage' // uploading package into sandbox container | 'uploadingPackage' // uploading package into sandbox container
| 'extractingPackage' // extracting package in sandbox | 'extractingPackage' // extracting package in sandbox
// Runtime image upgrade phases
| 'runtimeUpgradeRequired' // existing edit-debug sandbox uses an outdated runtime image
| 'runtimeUpgradeArchiving' // archiving workspace before recreating with current image
| 'runtimeUpgradeArchived' // outdated runtime is archived or removed; caller should refresh/restart
// Lazy-init phases // Lazy-init phases
| 'lazyInit' // LLM first calls sandbox tool, triggers container creation | 'lazyInit' // LLM first calls sandbox tool, triggers container creation
// Terminal phases // Terminal phases
...@@ -324,12 +320,23 @@ export const HistoryItemSchema = z.object({ ...@@ -324,12 +320,23 @@ export const HistoryItemSchema = z.object({
}); });
export type HistoryItemType = z.infer<typeof HistoryItemSchema>; export type HistoryItemType = z.infer<typeof HistoryItemSchema>;
export const ChatHistoryItemSchema = HistoryItemSchema.extend({ const ChatHistoryItemExtraShape = {
appId: z.string(),
top: z.boolean().optional(), top: z.boolean().optional(),
chatGenerateStatus: z.enum(ChatGenerateStatusEnum).optional(), chatGenerateStatus: z.enum(ChatGenerateStatusEnum).optional(),
hasBeenRead: z.boolean().optional() hasBeenRead: z.boolean().optional()
}); };
export const ChatHistoryItemSchema = z.union([
HistoryItemSchema.extend({
appId: z.string(),
skillId: z.undefined().optional(),
...ChatHistoryItemExtraShape
}),
HistoryItemSchema.extend({
appId: z.undefined().optional(),
skillId: z.string(),
...ChatHistoryItemExtraShape
})
]);
export type ChatHistoryItemType = z.infer<typeof ChatHistoryItemSchema>; export type ChatHistoryItemType = z.infer<typeof ChatHistoryItemSchema>;
/* ------- response data ------------ */ /* ------- response data ------------ */
......
import { z } from 'zod'; import { z } from 'zod';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { AppQGConfigTypeSchema } from '../../../../core/app/type';
import { ChatMessageSchema } from '../api'; import { ChatMessageSchema } from '../api';
import { createOutLinkChatTargetInputSchema, transformChatAuthTargetInput } from '../../chat/api';
/* ============================================================================ /* ============================================================================
* API: 创建问题引导 * API: 创建问题引导
...@@ -10,7 +12,10 @@ import { ChatMessageSchema } from '../api'; ...@@ -10,7 +12,10 @@ import { ChatMessageSchema } from '../api';
* Tags: ['AI', 'Agent', 'Read'] * Tags: ['AI', 'Agent', 'Read']
* ============================================================================ */ * ============================================================================ */
export const CreateQuestionGuideBodySchema = OutLinkChatAuthSchema.extend({ export const CreateQuestionGuideBodySchema = z.object({
outLinkAuthData: OutLinkChatAuthSchema.optional().meta({
description: '外链鉴权数据。share 模式传 shareId/outLinkUid。'
}),
messages: z.array(ChatMessageSchema).meta({ messages: z.array(ChatMessageSchema).meta({
description: '对话历史消息列表' description: '对话历史消息列表'
}) })
...@@ -24,3 +29,43 @@ export const CreateQuestionGuideResponseSchema = z.array(z.string()).meta({ ...@@ -24,3 +29,43 @@ export const CreateQuestionGuideResponseSchema = z.array(z.string()).meta({
}); });
export type CreateQuestionGuideResponseType = z.infer<typeof CreateQuestionGuideResponseSchema>; export type CreateQuestionGuideResponseType = z.infer<typeof CreateQuestionGuideResponseSchema>;
/* ============================================================================
* API: 创建会话问题引导
* Route: POST /api/core/ai/agent/v2/createQuestionGuide
* Method: POST
* Description: 基于指定会话历史生成推荐问题,支持普通 App、外链和 Skill Edit 调试
* Tags: ['AI', 'Agent', 'Chat', 'Read']
* ============================================================================ */
export const CreateQuestionGuideV2BodyRawSchema = createOutLinkChatTargetInputSchema({
chatId: z.string().min(1).meta({
example: 'chat-1',
description: '会话 ID'
}),
questionGuide: AppQGConfigTypeSchema.optional().meta({
description: '问题引导配置;App 会话未传时使用应用最新版本中的问题引导配置'
}),
outLinkAuthData: OutLinkChatAuthSchema.optional().meta({
description: '外链鉴权数据,兼容旧前端把 shareId/outLinkUid 放在嵌套对象中的传参方式'
})
}).meta({
example: {
chatId: 'chat-1',
questionGuide: {
open: true,
model: 'gpt-4o-mini'
},
outLinkAuthData: {
shareId: 'share-1',
outLinkUid: 'outlink-user-1'
}
}
});
export const CreateQuestionGuideV2BodySchema = CreateQuestionGuideV2BodyRawSchema.transform(
transformChatAuthTargetInput
);
export type CreateQuestionGuideV2BodyType = z.infer<typeof CreateQuestionGuideV2BodyRawSchema>;
export type CreateQuestionGuideV2BodyRuntimeType = z.infer<typeof CreateQuestionGuideV2BodySchema>;
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { DevApiTagsMap } from '../../../tag'; import { DevApiTagsMap } from '../../../tag';
import { CreateQuestionGuideBodySchema, CreateQuestionGuideResponseSchema } from './api'; import {
CreateQuestionGuideBodySchema,
CreateQuestionGuideResponseSchema,
CreateQuestionGuideV2BodyRawSchema
} from './api';
export const AgentPath: OpenAPIPath = { export const AgentPath: OpenAPIPath = {
'/core/ai/agent/createQuestionGuide': { '/core/ai/agent/createQuestionGuide': {
...@@ -26,5 +30,30 @@ export const AgentPath: OpenAPIPath = { ...@@ -26,5 +30,30 @@ export const AgentPath: OpenAPIPath = {
} }
} }
} }
},
'/core/ai/agent/v2/createQuestionGuide': {
post: {
summary: '创建会话问题引导',
description: '基于指定会话历史生成推荐问题,支持普通 App、外链和 Skill Edit 调试',
tags: [DevApiTagsMap.aiCommon],
requestBody: {
content: {
'application/json': {
schema: CreateQuestionGuideV2BodyRawSchema
}
}
},
responses: {
200: {
description: '成功返回推荐的引导问题列表',
content: {
'application/json': {
schema: CreateQuestionGuideResponseSchema
}
}
}
}
}
} }
}; };
...@@ -3,8 +3,9 @@ import z from 'zod'; ...@@ -3,8 +3,9 @@ import z from 'zod';
import { import {
ChatGenerateStatusSchema, ChatGenerateStatusSchema,
createOutLinkChatTargetInputSchema, createOutLinkChatTargetInputSchema,
transformChatTargetInput transformChatAuthTargetInput
} from '../chat/api'; } from '../chat/api';
import { OutLinkChatAuthSchema } from '../../../support/permission/chat';
// Query Params // Query Params
export const GetLLMRequestRecordParamsSchema = z.object({ export const GetLLMRequestRecordParamsSchema = z.object({
...@@ -63,17 +64,18 @@ export const ChatMessageSchema = z.object({ ...@@ -63,17 +64,18 @@ export const ChatMessageSchema = z.object({
}); });
/* ============================================================================ /* ============================================================================
* 断线续传:GET /api/core/chat/resume(与 v2/chat/completions 配套;支持站内、分享、团队域名鉴权) * 断线续传:GET /api/core/chat/resume(与 v2/chat/completions 配套;支持站内和分享鉴权)
* ============================================================================ */ * ============================================================================ */
export const ResumeStreamParamsRawSchema = createOutLinkChatTargetInputSchema({ export const ResumeStreamParamsRawSchema = createOutLinkChatTargetInputSchema({
teamId: ObjectIdSchema.optional(), outLinkAuthData: OutLinkChatAuthSchema.optional().meta({
shareId: z.string().optional(), description: '外链鉴权数据。GET query 中需 JSON 序列化。'
outLinkUid: z.string().optional(), }),
chatId: z.string().meta({ example: 'bEdzC6PNupZrr1RoVutMF2DL', description: '聊天 ID' }) chatId: z.string().meta({ example: 'bEdzC6PNupZrr1RoVutMF2DL', description: '聊天 ID' })
}); });
export const ResumeStreamParamsSchema = export const ResumeStreamParamsSchema = ResumeStreamParamsRawSchema.transform(
ResumeStreamParamsRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type ResumeStreamParams = z.infer<typeof ResumeStreamParamsRawSchema>; export type ResumeStreamParams = z.infer<typeof ResumeStreamParamsRawSchema>;
export type ResumeStreamRuntimeParams = z.infer<typeof ResumeStreamParamsSchema>; export type ResumeStreamRuntimeParams = z.infer<typeof ResumeStreamParamsSchema>;
......
...@@ -40,7 +40,7 @@ export const AIPath: OpenAPIPath = { ...@@ -40,7 +40,7 @@ export const AIPath: OpenAPIPath = {
get: { get: {
summary: '恢复流式响应', summary: '恢复流式响应',
description: description:
'与 /v2/chat/completions 配套;GET query 传 appId 或 skillId(二选一)以及 chatId,团队空间场景可额外传 teamId/teamToken。已完成对话可返回 JSON;若对话仍在生成中,则必须请求 SSE,否则返回 406。', '与 /v2/chat/completions 配套;GET query 传 appId、skillId 或 outLinkAuthData(三选一)以及 chatId。已完成对话可返回 JSON;若对话仍在生成中,则必须请求 SSE,否则返回 406。',
tags: [DevApiTagsMap.aiCommon], tags: [DevApiTagsMap.aiCommon],
requestParams: { requestParams: {
query: ResumeStreamParamsRawSchema query: ResumeStreamParamsRawSchema
......
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import z from 'zod'; import z from 'zod';
import { createChatTargetInputSchema, transformChatTargetInput } from '../../chat/api'; import { createOutLinkChatTargetInputSchema, transformChatAuthTargetInput } from '../../chat/api';
const SandboxBaseShape = { const SandboxBaseShape = {
chatId: z.string().meta({ chatId: z.string().meta({
...@@ -11,15 +11,15 @@ const SandboxBaseShape = { ...@@ -11,15 +11,15 @@ const SandboxBaseShape = {
}; };
const withSandboxTarget = <T extends z.ZodRawShape>(shape: T) => const withSandboxTarget = <T extends z.ZodRawShape>(shape: T) =>
createChatTargetInputSchema({ createOutLinkChatTargetInputSchema({
...SandboxBaseShape, ...SandboxBaseShape,
...shape ...shape
}).transform(transformChatTargetInput); }).transform(transformChatAuthTargetInput);
/** /**
* 下载文件或目录 - 请求体(响应为文件流或 ZIP) * 下载文件或目录 - 请求体(响应为文件流或 ZIP)
*/ */
export const SandboxDownloadBodyRawSchema = createChatTargetInputSchema({ export const SandboxDownloadBodyRawSchema = createOutLinkChatTargetInputSchema({
...SandboxBaseShape, ...SandboxBaseShape,
path: z.string().optional().default('.').describe('要下载的路径(文件或目录)') path: z.string().optional().default('.').describe('要下载的路径(文件或目录)')
}); });
...@@ -36,7 +36,7 @@ export const SandboxDownloadResponseSchema = z ...@@ -36,7 +36,7 @@ export const SandboxDownloadResponseSchema = z
/** /**
* 检查沙盒是否存在 * 检查沙盒是否存在
*/ */
export const SandboxCheckExistBodyRawSchema = createChatTargetInputSchema(SandboxBaseShape); export const SandboxCheckExistBodyRawSchema = createOutLinkChatTargetInputSchema(SandboxBaseShape);
export const SandboxCheckExistBodySchema = withSandboxTarget({}); export const SandboxCheckExistBodySchema = withSandboxTarget({});
export const SandboxCheckExistResponseSchema = z.object({ export const SandboxCheckExistResponseSchema = z.object({
exists: z.boolean().describe('沙盒是否存在') exists: z.boolean().describe('沙盒是否存在')
...@@ -51,7 +51,7 @@ export type SandboxCheckExistResponse = z.infer<typeof SandboxCheckExistResponse ...@@ -51,7 +51,7 @@ export type SandboxCheckExistResponse = z.infer<typeof SandboxCheckExistResponse
export const SandboxChannelSchema = z.enum(['fs', 'terminal']).describe('沙盒 WebSocket 通道'); export const SandboxChannelSchema = z.enum(['fs', 'terminal']).describe('沙盒 WebSocket 通道');
export const SandboxTicketPermissionSchema = z.enum(['read', 'write']).describe('沙盒 Ticket 权限'); export const SandboxTicketPermissionSchema = z.enum(['read', 'write']).describe('沙盒 Ticket 权限');
export const SandboxGetTicketBodyRawSchema = createChatTargetInputSchema({ export const SandboxGetTicketBodyRawSchema = createOutLinkChatTargetInputSchema({
...SandboxBaseShape, ...SandboxBaseShape,
channel: SandboxChannelSchema, channel: SandboxChannelSchema,
permission: SandboxTicketPermissionSchema.optional() permission: SandboxTicketPermissionSchema.optional()
...@@ -76,7 +76,7 @@ export type SandboxGetTicketResponse = z.infer<typeof SandboxGetTicketResponseSc ...@@ -76,7 +76,7 @@ export type SandboxGetTicketResponse = z.infer<typeof SandboxGetTicketResponseSc
/** /**
* 获取 HTML 预览链接 - 请求/响应 * 获取 HTML 预览链接 - 请求/响应
*/ */
export const SandboxGetHtmlPreviewLinkBodyRawSchema = createChatTargetInputSchema({ export const SandboxGetHtmlPreviewLinkBodyRawSchema = createOutLinkChatTargetInputSchema({
...SandboxBaseShape, ...SandboxBaseShape,
filePath: z.string().describe('文件路径') filePath: z.string().describe('文件路径')
}); });
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
AgentSkillSourceSchema, AgentSkillSourceSchema,
AgentSkillTypeSchema, AgentSkillTypeSchema,
ExtractedSkillPackageSchema, ExtractedSkillPackageSchema,
SandboxImageConfigSchema,
SandboxProviderStatusSchema, SandboxProviderStatusSchema,
SkillPackageSchema, SkillPackageSchema,
ZipEntryInfoSchema ZipEntryInfoSchema
...@@ -150,21 +149,39 @@ export type ImportSkillBody = z.infer<typeof ImportSkillBodySchema>; ...@@ -150,21 +149,39 @@ export type ImportSkillBody = z.infer<typeof ImportSkillBodySchema>;
export const ImportSkillResponseSchema = IdSchema; export const ImportSkillResponseSchema = IdSchema;
export type ImportSkillResponse = z.infer<typeof ImportSkillResponseSchema>; export type ImportSkillResponse = z.infer<typeof ImportSkillResponseSchema>;
export const CreateEditDebugSandboxBodySchema = z.object({ export const SkillRuntimeStatusSchema = z.enum(['readyToInit', 'upgradeRequired', 'upgrading']);
skillId: IdSchema, export const SkillRuntimeArchiveStateSchema = z.enum([
image: SandboxImageConfigSchema.optional(), 'archiving',
archiveForUpgrade: z.boolean().optional() 'deleting',
'archived',
'restoring',
'failed'
]);
export const SkillRuntimeBodySchema = z.object({
skillId: IdSchema.describe('技能 ID')
}); });
export type CreateEditDebugSandboxBody = z.infer<typeof CreateEditDebugSandboxBodySchema>; export type SkillRuntimeBody = z.infer<typeof SkillRuntimeBodySchema>;
export const CreateEditDebugSandboxResponseSchema = z.object({ export const SkillRuntimeStatusResponseSchema = z.object({
sandboxId: z.string().describe('FastGPT sandbox instance key'), sandboxId: z.string().describe('FastGPT sandbox instance key'),
status: SandboxProviderStatusSchema.pick({ status: SkillRuntimeStatusSchema.describe('Skill Edit runtime 当前状态'),
state: true, archiveState: SkillRuntimeArchiveStateSchema.optional().describe('底层 sandbox 归档状态'),
message: true canUpgrade: z.boolean().describe('当前是否允许触发 runtime 升级'),
shouldPoll: z.boolean().describe('客户端是否应继续轮询 getStatus'),
shouldInit: z.boolean().describe('客户端是否应执行 runtime init'),
lastError: z.string().optional().describe('上次 runtime 升级归档失败原因')
});
export type SkillRuntimeStatusResponse = z.infer<typeof SkillRuntimeStatusResponseSchema>;
export const SkillRuntimeInitEventSchema = z
.object({
sandboxId: z.string().describe('FastGPT sandbox instance key'),
phase: z.string().describe('Sandbox 初始化阶段'),
message: z.string().optional().describe('阶段消息或错误信息')
}) })
}); .describe('Skill Edit runtime init SSE sandboxStatus event');
export type CreateEditDebugSandboxResponse = z.infer<typeof CreateEditDebugSandboxResponseSchema>; export type SkillRuntimeInitEvent = z.infer<typeof SkillRuntimeInitEventSchema>;
export const GetSandboxInfoQuerySchema = z.object({ export const GetSandboxInfoQuerySchema = z.object({
sandboxId: SandboxInstanceKeySchema sandboxId: SandboxInstanceKeySchema
......
...@@ -2,8 +2,6 @@ import type { OpenAPIPath } from '../../../type'; ...@@ -2,8 +2,6 @@ import type { OpenAPIPath } from '../../../type';
import { DevApiTagsMap } from '../../../tag'; import { DevApiTagsMap } from '../../../tag';
import { import {
ListAppsBySkillIdResponseSchema, ListAppsBySkillIdResponseSchema,
CreateEditDebugSandboxBodySchema,
CreateEditDebugSandboxResponseSchema,
CreateSkillBodySchema, CreateSkillBodySchema,
CreateSkillFolderBodySchema, CreateSkillFolderBodySchema,
CreateSkillFolderResponseSchema, CreateSkillFolderResponseSchema,
...@@ -24,6 +22,9 @@ import { ...@@ -24,6 +22,9 @@ import {
SaveDeploySkillBodySchema, SaveDeploySkillBodySchema,
SaveDeploySkillResponseSchema, SaveDeploySkillResponseSchema,
SkillDebugChatBodySchema, SkillDebugChatBodySchema,
SkillRuntimeBodySchema,
SkillRuntimeInitEventSchema,
SkillRuntimeStatusResponseSchema,
SwitchSkillVersionBodySchema, SwitchSkillVersionBodySchema,
UpdateSkillBodySchema, UpdateSkillBodySchema,
UpdateSkillVersionBodySchema UpdateSkillVersionBodySchema
...@@ -243,15 +244,63 @@ export const SkillPath: OpenAPIPath = { ...@@ -243,15 +244,63 @@ export const SkillPath: OpenAPIPath = {
} }
} }
}, },
'/core/ai/skill/edit': { '/core/ai/skill/runtime/getStatus': {
post: { post: {
summary: '创建编辑调试沙盒', summary: '获取技能编辑沙盒 runtime 状态',
description: '为技能创建 edit-debug 沙盒,返回 SSE sandboxStatus 事件流', description: '检查 Skill Edit runtime 是否可直接初始化、需要升级或正在升级',
tags: [DevApiTagsMap.aiSkill], tags: [DevApiTagsMap.aiSkill],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: CreateEditDebugSandboxBodySchema schema: SkillRuntimeBodySchema
}
}
},
responses: {
200: {
description: '成功返回 runtime 状态',
content: {
'application/json': {
schema: SkillRuntimeStatusResponseSchema
}
}
}
}
}
},
'/core/ai/skill/runtime/upgrade': {
post: {
summary: '触发技能编辑沙盒 runtime 升级',
description: '触发旧 runtime 工作区归档,客户端随后通过 getStatus 轮询结果',
tags: [DevApiTagsMap.aiSkill],
requestBody: {
content: {
'application/json': {
schema: SkillRuntimeBodySchema
}
}
},
responses: {
200: {
description: '成功返回触发后的 runtime 状态',
content: {
'application/json': {
schema: SkillRuntimeStatusResponseSchema
}
}
}
}
}
},
'/core/ai/skill/runtime/init': {
post: {
summary: '初始化技能编辑沙盒 runtime',
description: '启动、恢复或复用 Skill Edit sandbox,返回 SSE sandboxStatus 事件流',
tags: [DevApiTagsMap.aiSkill],
requestBody: {
content: {
'application/json': {
schema: SkillRuntimeBodySchema
} }
} }
}, },
...@@ -260,7 +309,7 @@ export const SkillPath: OpenAPIPath = { ...@@ -260,7 +309,7 @@ export const SkillPath: OpenAPIPath = {
description: '返回 text/event-stream 事件流', description: '返回 text/event-stream 事件流',
content: { content: {
'text/event-stream': { 'text/event-stream': {
schema: CreateEditDebugSandboxResponseSchema schema: SkillRuntimeInitEventSchema
} }
} }
} }
......
...@@ -330,10 +330,6 @@ export const GetAppDetailResponseSchema = AppSchemaTypeSchema.extend({ ...@@ -330,10 +330,6 @@ export const GetAppDetailResponseSchema = AppSchemaTypeSchema.extend({
description: '旧版初始化状态', description: '旧版初始化状态',
deprecated: true deprecated: true
}), }),
teamTags: AppSchemaTypeSchema.shape.teamTags.meta({
description: '旧版团队标签',
deprecated: true
}),
permission: AppPermissionSchema permission: AppPermissionSchema
}).meta({ }).meta({
description: '应用详情' description: '应用详情'
...@@ -368,11 +364,7 @@ export const UpdateAppBodySchema = z ...@@ -368,11 +364,7 @@ export const UpdateAppBodySchema = z
intro: z.string().optional().meta({ description: '应用介绍' }), intro: z.string().optional().meta({ description: '应用介绍' }),
nodes: z.array(OpenAPIStoreNodeItemTypeSchema).optional().meta({ description: '应用节点配置' }), nodes: z.array(OpenAPIStoreNodeItemTypeSchema).optional().meta({ description: '应用节点配置' }),
edges: AppSchemaTypeSchema.shape.edges.optional().meta({ description: '应用连线' }), edges: AppSchemaTypeSchema.shape.edges.optional().meta({ description: '应用连线' }),
chatConfig: OpenAPIAppChatConfigSchema.optional().meta({ description: '聊天配置' }), chatConfig: OpenAPIAppChatConfigSchema.optional().meta({ description: '聊天配置' })
teamTags: AppSchemaTypeSchema.shape.teamTags.optional().meta({
description: '旧版团队标签',
deprecated: true
})
}) })
.meta({ .meta({
example: { example: {
......
...@@ -58,33 +58,45 @@ export const ChatCompletionAuthProxySchema = z ...@@ -58,33 +58,45 @@ export const ChatCompletionAuthProxySchema = z
}); });
export type ChatCompletionAuthProxy = z.infer<typeof ChatCompletionAuthProxySchema>; export type ChatCompletionAuthProxy = z.infer<typeof ChatCompletionAuthProxySchema>;
export const CompletionsPropsSchema = OutLinkChatAuthSchema.extend(WebCompletionsSchema.shape) export const CompletionsPropsSchema = WebCompletionsSchema.extend({
.extend(ChatCompletionCreateParamsSchema.shape) ...ChatCompletionCreateParamsSchema.shape,
.extend({ outLinkAuthData: nullishToUndefined(OutLinkChatAuthSchema.optional()).meta({
authProxy: nullishToUndefined(ChatCompletionAuthProxySchema.optional()).meta({ description: '外链鉴权数据。share 模式传 shareId/outLinkUid。'
description: 'API Key 代理调用身份' }),
}), authProxy: nullishToUndefined(ChatCompletionAuthProxySchema.optional()).meta({
variables: nullishToUndefined(z.record(z.string(), z.any()).default({})).meta({ description: 'API Key 代理调用身份'
description: '全局变量或插件输入' }),
}), variables: nullishToUndefined(z.record(z.string(), z.any()).default({})).meta({
responseChatItemId: nullishToUndefined( description: '全局变量或插件输入'
z }),
.string() responseChatItemId: nullishToUndefined(
.default(() => getNanoid()) z
.meta({ .string()
description: '自定义响应的 assistant 的消息 ID,如果不传入,则自动生成一个' .default(() => getNanoid())
}) .meta({
), description: '自定义响应的 assistant 的消息 ID,如果不传入,则自动生成一个'
detail: nullishToUndefined(z.boolean().default(false)).meta({ })
description: '是否返回详细信息,包括 reasoning_content, tool_calls, usage 等' ),
}), detail: nullishToUndefined(z.boolean().default(false)).meta({
retainDatasetCite: nullishToUndefined(z.boolean().default(false)).meta({ description: '是否返回详细信息,包括 reasoning_content, tool_calls, usage 等'
description: '是否保留数据集引用' }),
}), retainDatasetCite: nullishToUndefined(z.boolean().default(false)).meta({
showSkillReferences: nullishToUndefined(z.boolean().default(false)).meta({ description: '是否保留数据集引用'
description: '是否显示技能引用' }),
}) showSkillReferences: nullishToUndefined(z.boolean().default(false)).meta({
}); description: '是否显示技能引用'
})
}).superRefine(({ outLinkAuthData }, ctx) => {
const hasShareId = !!outLinkAuthData?.shareId;
const hasOutLinkUid = !!outLinkAuthData?.outLinkUid;
if (hasShareId !== hasOutLinkUid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'outLinkAuthData.shareId and outLinkAuthData.outLinkUid must be provided together'
});
}
});
export type CompletionsProps = z.infer<typeof CompletionsPropsSchema>; export type CompletionsProps = z.infer<typeof CompletionsPropsSchema>;
/* =============== Response =============== */ /* =============== Response =============== */
......
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import z from 'zod'; import z from 'zod';
import { AppTypeEnum } from '../../../../core/app/constants'; import { AppTypeEnum } from '../../../../core/app/constants';
import { ChatGenerateStatusEnum, ChatSourceTypeEnum } from '../../../../core/chat/constants'; import { ChatGenerateStatusEnum } from '../../../../core/chat/constants';
import { OpenAPIFlowNodeInputItemTypeSchema } from '../../workflow/node'; import { OpenAPIFlowNodeInputItemTypeSchema } from '../../workflow/node';
import { OpenAPIAppChatConfigSchema } from '../../app/common/api'; import { OpenAPIAppChatConfigSchema } from '../../app/common/api';
import { import {
ChatGenerateStatusSchema, ChatGenerateStatusSchema,
createChatTargetInputSchema, createChatTargetInputSchema,
createChatTargetResponseSchema,
createOutLinkChatTargetInputSchema,
transformChatAuthTargetInput,
transformChatTargetInput transformChatTargetInput
} from '../api'; } from '../api';
...@@ -27,20 +29,8 @@ export const InitChatQuerySchema = InitChatQueryRawSchema.transform(transformCha ...@@ -27,20 +29,8 @@ export const InitChatQuerySchema = InitChatQueryRawSchema.transform(transformCha
export type InitChatQueryType = z.infer<typeof InitChatQueryRawSchema>; export type InitChatQueryType = z.infer<typeof InitChatQueryRawSchema>;
export type InitChatQueryRuntimeType = z.infer<typeof InitChatQuerySchema>; export type InitChatQueryRuntimeType = z.infer<typeof InitChatQuerySchema>;
/** 团队空间 init:`/api/core/chat/team/init` */ export const InitChatResponseSchema = createChatTargetResponseSchema({
export const InitTeamChatQuerySchema = z.object({
teamId: z.string().min(1),
appId: z.string().min(1),
chatId: z.string().optional(),
teamToken: z.string().min(1)
});
export type InitTeamChatQueryType = z.infer<typeof InitTeamChatQuerySchema>;
export const InitChatResponseSchema = z.object({
chatId: z.string().optional().describe('会话ID'), chatId: z.string().optional().describe('会话ID'),
sourceType: z.enum(ChatSourceTypeEnum).describe('会话所属资源类型'),
sourceId: ObjectIdSchema.describe('会话所属资源 ID'),
appId: ObjectIdSchema.optional().describe('真实应用 ID,仅 sourceType=app 时返回'),
userAvatar: z.string().optional().describe('用户头像'), userAvatar: z.string().optional().describe('用户头像'),
title: z.string().describe('对话标题'), title: z.string().describe('对话标题'),
variables: z.record(z.string(), z.any()).optional().describe('全局变量值'), variables: z.record(z.string(), z.any()).optional().describe('全局变量值'),
...@@ -63,12 +53,11 @@ export const InitChatResponseSchema = z.object({ ...@@ -63,12 +53,11 @@ export const InitChatResponseSchema = z.object({
export type InitChatResponseType = z.infer<typeof InitChatResponseSchema>; export type InitChatResponseType = z.infer<typeof InitChatResponseSchema>;
/* ============ v2/chat/stop ============ */ /* ============ v2/chat/stop ============ */
export const StopV2ChatRawSchema = createChatTargetInputSchema({ export const StopV2ChatRawSchema = createOutLinkChatTargetInputSchema({
chatId: z.string().min(1).describe('会话ID'), chatId: z.string().min(1).describe('会话ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
}).meta({ }).meta({
example: { example: {
appId: '1234567890',
chatId: '1234567890', chatId: '1234567890',
outLinkAuthData: { outLinkAuthData: {
shareId: '1234567890', shareId: '1234567890',
...@@ -76,7 +65,7 @@ export const StopV2ChatRawSchema = createChatTargetInputSchema({ ...@@ -76,7 +65,7 @@ export const StopV2ChatRawSchema = createChatTargetInputSchema({
} }
} }
}); });
export const StopV2ChatSchema = StopV2ChatRawSchema.transform(transformChatTargetInput); export const StopV2ChatSchema = StopV2ChatRawSchema.transform(transformChatAuthTargetInput);
export type StopV2ChatParams = z.infer<typeof StopV2ChatRawSchema>; export type StopV2ChatParams = z.infer<typeof StopV2ChatRawSchema>;
export type StopV2ChatRuntimeParams = z.infer<typeof StopV2ChatSchema>; export type StopV2ChatRuntimeParams = z.infer<typeof StopV2ChatSchema>;
......
import z from 'zod'; import z from 'zod';
import { createChatTargetInputSchema, transformChatTargetInput } from '../api'; import { createOutLinkChatTargetInputSchema, transformChatAuthTargetInput } from '../api';
const FeedbackTargetSchema = { const FeedbackTargetSchema = {
chatId: z.string().min(1).meta({ chatId: z.string().min(1).meta({
...@@ -13,15 +13,16 @@ const FeedbackTargetSchema = { ...@@ -13,15 +13,16 @@ const FeedbackTargetSchema = {
}; };
/* =============== updateFeedbackReadStatus =============== */ /* =============== updateFeedbackReadStatus =============== */
export const UpdateFeedbackReadStatusBodyRawSchema = createChatTargetInputSchema({ export const UpdateFeedbackReadStatusBodyRawSchema = createOutLinkChatTargetInputSchema({
...FeedbackTargetSchema, ...FeedbackTargetSchema,
isRead: z.boolean().meta({ isRead: z.boolean().meta({
example: true, example: true,
description: '是否已读' description: '是否已读'
}) })
}); });
export const UpdateFeedbackReadStatusBodySchema = export const UpdateFeedbackReadStatusBodySchema = UpdateFeedbackReadStatusBodyRawSchema.transform(
UpdateFeedbackReadStatusBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type UpdateFeedbackReadStatusBodyType = z.infer< export type UpdateFeedbackReadStatusBodyType = z.infer<
typeof UpdateFeedbackReadStatusBodyRawSchema typeof UpdateFeedbackReadStatusBodyRawSchema
>; >;
...@@ -40,7 +41,7 @@ export type UpdateFeedbackReadStatusResponseType = z.infer< ...@@ -40,7 +41,7 @@ export type UpdateFeedbackReadStatusResponseType = z.infer<
>; >;
/* =============== adminUpdate =============== */ /* =============== adminUpdate =============== */
export const AdminUpdateFeedbackBodyRawSchema = createChatTargetInputSchema({ export const AdminUpdateFeedbackBodyRawSchema = createOutLinkChatTargetInputSchema({
...FeedbackTargetSchema, ...FeedbackTargetSchema,
datasetId: z.string().min(1).meta({ datasetId: z.string().min(1).meta({
example: 'dataset123', example: 'dataset123',
...@@ -59,8 +60,9 @@ export const AdminUpdateFeedbackBodyRawSchema = createChatTargetInputSchema({ ...@@ -59,8 +60,9 @@ export const AdminUpdateFeedbackBodyRawSchema = createChatTargetInputSchema({
description: '答案内容(可选)' description: '答案内容(可选)'
}) })
}); });
export const AdminUpdateFeedbackBodySchema = export const AdminUpdateFeedbackBodySchema = AdminUpdateFeedbackBodyRawSchema.transform(
AdminUpdateFeedbackBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type AdminUpdateFeedbackBodyType = z.infer<typeof AdminUpdateFeedbackBodyRawSchema>; export type AdminUpdateFeedbackBodyType = z.infer<typeof AdminUpdateFeedbackBodyRawSchema>;
export type AdminUpdateFeedbackBodyRuntimeType = z.infer<typeof AdminUpdateFeedbackBodySchema>; export type AdminUpdateFeedbackBodyRuntimeType = z.infer<typeof AdminUpdateFeedbackBodySchema>;
...@@ -68,15 +70,16 @@ export const AdminUpdateFeedbackResponseSchema = z.undefined().meta({ descriptio ...@@ -68,15 +70,16 @@ export const AdminUpdateFeedbackResponseSchema = z.undefined().meta({ descriptio
export type AdminUpdateFeedbackResponseType = z.infer<typeof AdminUpdateFeedbackResponseSchema>; export type AdminUpdateFeedbackResponseType = z.infer<typeof AdminUpdateFeedbackResponseSchema>;
/* =============== closeCustom =============== */ /* =============== closeCustom =============== */
export const CloseCustomFeedbackBodyRawSchema = createChatTargetInputSchema({ export const CloseCustomFeedbackBodyRawSchema = createOutLinkChatTargetInputSchema({
...FeedbackTargetSchema, ...FeedbackTargetSchema,
index: z.number().int().nonnegative().meta({ index: z.number().int().nonnegative().meta({
example: 0, example: 0,
description: '自定义反馈的索引位置' description: '自定义反馈的索引位置'
}) })
}); });
export const CloseCustomFeedbackBodySchema = export const CloseCustomFeedbackBodySchema = CloseCustomFeedbackBodyRawSchema.transform(
CloseCustomFeedbackBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type CloseCustomFeedbackBodyType = z.infer<typeof CloseCustomFeedbackBodyRawSchema>; export type CloseCustomFeedbackBodyType = z.infer<typeof CloseCustomFeedbackBodyRawSchema>;
export type CloseCustomFeedbackBodyRuntimeType = z.infer<typeof CloseCustomFeedbackBodySchema>; export type CloseCustomFeedbackBodyRuntimeType = z.infer<typeof CloseCustomFeedbackBodySchema>;
...@@ -84,7 +87,7 @@ export const CloseCustomFeedbackResponseSchema = z.undefined().meta({ descriptio ...@@ -84,7 +87,7 @@ export const CloseCustomFeedbackResponseSchema = z.undefined().meta({ descriptio
export type CloseCustomFeedbackResponseType = z.infer<typeof CloseCustomFeedbackResponseSchema>; export type CloseCustomFeedbackResponseType = z.infer<typeof CloseCustomFeedbackResponseSchema>;
/* =============== updateUserFeedback =============== */ /* =============== updateUserFeedback =============== */
export const UpdateUserFeedbackBodyRawSchema = createChatTargetInputSchema({ export const UpdateUserFeedbackBodyRawSchema = createOutLinkChatTargetInputSchema({
...FeedbackTargetSchema, ...FeedbackTargetSchema,
userGoodFeedback: z.string().nullish().meta({ userGoodFeedback: z.string().nullish().meta({
example: '回答很好', example: '回答很好',
...@@ -95,8 +98,9 @@ export const UpdateUserFeedbackBodyRawSchema = createChatTargetInputSchema({ ...@@ -95,8 +98,9 @@ export const UpdateUserFeedbackBodyRawSchema = createChatTargetInputSchema({
description: '用户差评反馈内容' description: '用户差评反馈内容'
}) })
}); });
export const UpdateUserFeedbackBodySchema = export const UpdateUserFeedbackBodySchema = UpdateUserFeedbackBodyRawSchema.transform(
UpdateUserFeedbackBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type UpdateUserFeedbackBodyType = z.infer<typeof UpdateUserFeedbackBodyRawSchema>; export type UpdateUserFeedbackBodyType = z.infer<typeof UpdateUserFeedbackBodyRawSchema>;
export type UpdateUserFeedbackBodyRuntimeType = z.infer<typeof UpdateUserFeedbackBodySchema>; export type UpdateUserFeedbackBodyRuntimeType = z.infer<typeof UpdateUserFeedbackBodySchema>;
...@@ -104,7 +108,7 @@ export const UpdateUserFeedbackResponseSchema = z.undefined().meta({ description ...@@ -104,7 +108,7 @@ export const UpdateUserFeedbackResponseSchema = z.undefined().meta({ description
export type UpdateUserFeedbackResponseType = z.infer<typeof UpdateUserFeedbackResponseSchema>; export type UpdateUserFeedbackResponseType = z.infer<typeof UpdateUserFeedbackResponseSchema>;
/* =============== getFeedbackRecordIds =============== */ /* =============== getFeedbackRecordIds =============== */
export const GetFeedbackRecordIdsBodyRawSchema = createChatTargetInputSchema({ export const GetFeedbackRecordIdsBodyRawSchema = createOutLinkChatTargetInputSchema({
chatId: z.string().meta({ chatId: z.string().meta({
example: 'chat123', example: 'chat123',
description: '对话 ID' description: '对话 ID'
...@@ -118,8 +122,9 @@ export const GetFeedbackRecordIdsBodyRawSchema = createChatTargetInputSchema({ ...@@ -118,8 +122,9 @@ export const GetFeedbackRecordIdsBodyRawSchema = createChatTargetInputSchema({
description: '是否只返回未读的反馈' description: '是否只返回未读的反馈'
}) })
}); });
export const GetFeedbackRecordIdsBodySchema = export const GetFeedbackRecordIdsBodySchema = GetFeedbackRecordIdsBodyRawSchema.transform(
GetFeedbackRecordIdsBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type GetFeedbackRecordIdsBodyType = z.infer<typeof GetFeedbackRecordIdsBodyRawSchema>; export type GetFeedbackRecordIdsBodyType = z.infer<typeof GetFeedbackRecordIdsBodyRawSchema>;
export type GetFeedbackRecordIdsBodyRuntimeType = z.infer<typeof GetFeedbackRecordIdsBodySchema>; export type GetFeedbackRecordIdsBodyRuntimeType = z.infer<typeof GetFeedbackRecordIdsBodySchema>;
......
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { AppFileSelectConfigTypeSchema } from '../../../../core/app/type/config.schema'; import { AppFileSelectConfigTypeSchema } from '../../../../core/app/type/config.schema';
import z from 'zod'; import z from 'zod';
import { createChatTargetInputSchema, transformChatTargetInput } from '../api'; import { createOutLinkChatTargetInputSchema, transformChatAuthTargetInput } from '../api';
/* ============ chat file ============ */ /* ============ chat file ============ */
const withChatFileTarget = <T extends z.ZodRawShape>(shape: T) => const withChatFileTarget = <T extends z.ZodRawShape>(shape: T) =>
createChatTargetInputSchema(shape).transform(transformChatTargetInput); createOutLinkChatTargetInputSchema(shape).transform(transformChatAuthTargetInput);
export const PresignChatFileGetUrlRawSchema = createChatTargetInputSchema({ export const PresignChatFileGetUrlRawSchema = createOutLinkChatTargetInputSchema({
key: z.string().min(1).describe('文件key'), key: z.string().min(1).describe('文件key'),
chatId: z.string().min(1).describe('对话ID'), chatId: z.string().min(1).describe('对话ID'),
mode: z.enum(['proxy', 'presigned']).optional().describe('下载方式'), mode: z.enum(['proxy', 'presigned']).optional().describe('下载方式'),
...@@ -15,7 +15,6 @@ export const PresignChatFileGetUrlRawSchema = createChatTargetInputSchema({ ...@@ -15,7 +15,6 @@ export const PresignChatFileGetUrlRawSchema = createChatTargetInputSchema({
}).meta({ }).meta({
example: { example: {
key: '1234567890', key: '1234567890',
appId: '1234567890',
chatId: '1234567890', chatId: '1234567890',
outLinkAuthData: { outLinkAuthData: {
shareId: '1234567890', shareId: '1234567890',
...@@ -32,7 +31,7 @@ export const PresignChatFileGetUrlSchema = withChatFileTarget({ ...@@ -32,7 +31,7 @@ export const PresignChatFileGetUrlSchema = withChatFileTarget({
export type PresignChatFileGetUrlParams = z.input<typeof PresignChatFileGetUrlSchema>; export type PresignChatFileGetUrlParams = z.input<typeof PresignChatFileGetUrlSchema>;
export type PresignChatFileGetUrlRuntimeParams = z.output<typeof PresignChatFileGetUrlSchema>; export type PresignChatFileGetUrlRuntimeParams = z.output<typeof PresignChatFileGetUrlSchema>;
export const PresignChatFilePostUrlRawSchema = createChatTargetInputSchema({ export const PresignChatFilePostUrlRawSchema = createOutLinkChatTargetInputSchema({
filename: z.string().min(1).describe('文件名'), filename: z.string().min(1).describe('文件名'),
chatId: z.string().min(1).describe('对话ID'), chatId: z.string().min(1).describe('对话ID'),
fileSelectConfig: AppFileSelectConfigTypeSchema.describe('本次上传控件的文件选择配置'), fileSelectConfig: AppFileSelectConfigTypeSchema.describe('本次上传控件的文件选择配置'),
...@@ -40,7 +39,6 @@ export const PresignChatFilePostUrlRawSchema = createChatTargetInputSchema({ ...@@ -40,7 +39,6 @@ export const PresignChatFilePostUrlRawSchema = createChatTargetInputSchema({
}).meta({ }).meta({
example: { example: {
filename: '1234567890', filename: '1234567890',
appId: '1234567890',
chatId: '1234567890', chatId: '1234567890',
fileSelectConfig: { fileSelectConfig: {
canSelectFile: true, canSelectFile: true,
......
...@@ -3,12 +3,14 @@ import { ChatSourceEnum } from '../../../../core/chat/constants'; ...@@ -3,12 +3,14 @@ import { ChatSourceEnum } from '../../../../core/chat/constants';
import { PaginationSchema, PaginationResponseSchema } from '../../../api'; import { PaginationSchema, PaginationResponseSchema } from '../../../api';
import { import {
ChatGenerateStatusSchema, ChatGenerateStatusSchema,
createChatTargetResponseSchema,
createChatTargetInputSchema, createChatTargetInputSchema,
createOptionalOutLinkChatTargetInputSchema, createOptionalOutLinkChatTargetInputSchema,
createOutLinkChatTargetInputSchema, createOutLinkChatTargetInputSchema,
refineOptionalChatTargetInput, refineOptionalChatTargetInput,
transformChatAuthTargetInput,
transformChatTargetInput, transformChatTargetInput,
transformOptionalChatTargetInput transformOptionalChatAuthTargetInput
} from '../api'; } from '../api';
// Get chat sessions schema // Get chat sessions schema
...@@ -23,33 +25,30 @@ export const GetHistoriesBodyRawSchema = PaginationSchema.extend( ...@@ -23,33 +25,30 @@ export const GetHistoriesBodyRawSchema = PaginationSchema.extend(
createOptionalOutLinkChatTargetInputSchema(GetHistoriesPropsSchema).shape createOptionalOutLinkChatTargetInputSchema(GetHistoriesPropsSchema).shape
).superRefine(refineOptionalChatTargetInput); ).superRefine(refineOptionalChatTargetInput);
export const GetHistoriesBodySchema = GetHistoriesBodyRawSchema.transform( export const GetHistoriesBodySchema = GetHistoriesBodyRawSchema.transform(
transformOptionalChatTargetInput transformOptionalChatAuthTargetInput
); );
export type GetHistoriesBodyType = z.infer<typeof GetHistoriesBodyRawSchema>; export type GetHistoriesBodyType = z.infer<typeof GetHistoriesBodyRawSchema>;
export type GetHistoriesBodyRuntimeType = z.infer<typeof GetHistoriesBodySchema>; export type GetHistoriesBodyRuntimeType = z.infer<typeof GetHistoriesBodySchema>;
export const GetHistoriesResponseSchema = PaginationResponseSchema( const GetHistoriesResponseItemSchema = createChatTargetResponseSchema({
z.object({ chatId: z.string(),
chatId: z.string(), updateTime: z.coerce.date(),
updateTime: z.coerce.date(), customTitle: z.string().optional(),
appId: z.string(), title: z.string(),
customTitle: z.string().optional(), top: z.boolean().optional(),
title: z.string(), chatGenerateStatus: ChatGenerateStatusSchema.optional(),
top: z.boolean().optional(), hasBeenRead: z.boolean().optional()
chatGenerateStatus: ChatGenerateStatusSchema.optional(), });
hasBeenRead: z.boolean().optional() export const GetHistoriesResponseSchema = PaginationResponseSchema(GetHistoriesResponseItemSchema);
})
);
export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema>; export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema>;
const GetHistoryStatusPropsSchema = { const GetHistoryStatusPropsSchema = {
chatIds: z.array(z.string().min(1)).min(1).max(200).describe('需要刷新状态的会话 ID 列表') chatIds: z.array(z.string().min(1)).min(1).max(200).describe('需要刷新状态的会话 ID 列表')
}; };
export const GetHistoryStatusBodyRawSchema = createOptionalOutLinkChatTargetInputSchema( export const GetHistoryStatusBodyRawSchema =
GetHistoryStatusPropsSchema createOutLinkChatTargetInputSchema(GetHistoryStatusPropsSchema);
);
export const GetHistoryStatusBodySchema = GetHistoryStatusBodyRawSchema.transform( export const GetHistoryStatusBodySchema = GetHistoryStatusBodyRawSchema.transform(
transformOptionalChatTargetInput transformChatAuthTargetInput
); );
export type GetHistoryStatusBodyType = z.infer<typeof GetHistoryStatusBodyRawSchema>; export type GetHistoryStatusBodyType = z.infer<typeof GetHistoryStatusBodyRawSchema>;
export type GetHistoryStatusBodyRuntimeType = z.infer<typeof GetHistoryStatusBodySchema>; export type GetHistoryStatusBodyRuntimeType = z.infer<typeof GetHistoryStatusBodySchema>;
...@@ -71,7 +70,9 @@ const MarkChatReadPropsSchema = { ...@@ -71,7 +70,9 @@ const MarkChatReadPropsSchema = {
}; };
export const MarkChatReadBodyRawSchema = export const MarkChatReadBodyRawSchema =
createOutLinkChatTargetInputSchema(MarkChatReadPropsSchema); createOutLinkChatTargetInputSchema(MarkChatReadPropsSchema);
export const MarkChatReadBodySchema = MarkChatReadBodyRawSchema.transform(transformChatTargetInput); export const MarkChatReadBodySchema = MarkChatReadBodyRawSchema.transform(
transformChatAuthTargetInput
);
export type MarkChatReadBodyType = z.infer<typeof MarkChatReadBodyRawSchema>; export type MarkChatReadBodyType = z.infer<typeof MarkChatReadBodyRawSchema>;
export type MarkChatReadBodyRuntimeType = z.infer<typeof MarkChatReadBodySchema>; export type MarkChatReadBodyRuntimeType = z.infer<typeof MarkChatReadBodySchema>;
...@@ -83,9 +84,9 @@ const UpdateHistoryPropsSchema = { ...@@ -83,9 +84,9 @@ const UpdateHistoryPropsSchema = {
top: z.boolean().optional().describe('是否置顶') top: z.boolean().optional().describe('是否置顶')
}; };
export const UpdateHistoryBodyRawSchema = export const UpdateHistoryBodyRawSchema =
createOptionalOutLinkChatTargetInputSchema(UpdateHistoryPropsSchema); createOutLinkChatTargetInputSchema(UpdateHistoryPropsSchema);
export const UpdateHistoryBodySchema = UpdateHistoryBodyRawSchema.transform( export const UpdateHistoryBodySchema = UpdateHistoryBodyRawSchema.transform(
transformOptionalChatTargetInput transformChatAuthTargetInput
); );
export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodyRawSchema>; export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodyRawSchema>;
export type UpdateHistoryBodyRuntimeType = z.infer<typeof UpdateHistoryBodySchema>; export type UpdateHistoryBodyRuntimeType = z.infer<typeof UpdateHistoryBodySchema>;
...@@ -95,15 +96,15 @@ export const DelChatHistoryRawSchema = createOptionalOutLinkChatTargetInputSchem ...@@ -95,15 +96,15 @@ export const DelChatHistoryRawSchema = createOptionalOutLinkChatTargetInputSchem
chatId: z.string().min(1).describe('会话ID') chatId: z.string().min(1).describe('会话ID')
}); });
export const DelChatHistorySchema = DelChatHistoryRawSchema.transform( export const DelChatHistorySchema = DelChatHistoryRawSchema.transform(
transformOptionalChatTargetInput transformOptionalChatAuthTargetInput
); );
export type DelChatHistoryType = z.infer<typeof DelChatHistoryRawSchema>; export type DelChatHistoryType = z.infer<typeof DelChatHistoryRawSchema>;
export type DelChatHistoryRuntimeType = z.infer<typeof DelChatHistorySchema>; export type DelChatHistoryRuntimeType = z.infer<typeof DelChatHistorySchema>;
// Clear all chat sessions schema // Clear all chat sessions schema
export const ClearChatHistoriesRawSchema = createOptionalOutLinkChatTargetInputSchema({}); export const ClearChatHistoriesRawSchema = createOutLinkChatTargetInputSchema({});
export const ClearChatHistoriesSchema = ClearChatHistoriesRawSchema.transform( export const ClearChatHistoriesSchema = ClearChatHistoriesRawSchema.transform(
transformOptionalChatTargetInput transformChatAuthTargetInput
); );
export type ClearChatHistoriesType = z.infer<typeof ClearChatHistoriesRawSchema>; export type ClearChatHistoriesType = z.infer<typeof ClearChatHistoriesRawSchema>;
export type ClearChatHistoriesRuntimeType = z.infer<typeof ClearChatHistoriesSchema>; export type ClearChatHistoriesRuntimeType = z.infer<typeof ClearChatHistoriesSchema>;
......
import { z } from 'zod'; import { z } from 'zod';
import { PaginationSchema } from '../../../api'; import { PaginationSchema } from '../../../api';
import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ObjectIdSchema } from '../../../../common/type/mongo';
import { ChatSourceTypeEnum } from '../../../../core/chat/constants';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
/* ============================================================================ /* ============================================================================
...@@ -125,15 +126,25 @@ export type DeleteAllChatInputGuideResponseType = z.infer< ...@@ -125,15 +126,25 @@ export type DeleteAllChatInputGuideResponseType = z.infer<
* API: 查询对话输入引导(公开接口) * API: 查询对话输入引导(公开接口)
* Route: POST /api/core/chat/inputGuide/query * Route: POST /api/core/chat/inputGuide/query
* Method: POST * Method: POST
* Description: 根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权 * Description: 根据搜索词查询对话输入引导,支持应用和分享链接鉴权
* Tags: ['Chat', 'InputGuide', 'Read'] * Tags: ['Chat', 'InputGuide', 'Read']
* ============================================================================ */ * ============================================================================ */
export const QueryChatInputGuideBodySchema = OutLinkChatAuthSchema.extend({ export const QueryChatInputGuideBodyRawSchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }), sourceType: z.enum(ChatSourceTypeEnum).meta({
example: ChatSourceTypeEnum.app,
description: '会话归属资源类型'
}),
sourceId: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05',
description: '会话归属资源 ID'
}),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据'),
searchKey: z.string().meta({ example: '如何使用', description: '搜索关键词' }) searchKey: z.string().meta({ example: '如何使用', description: '搜索关键词' })
}); });
export type QueryChatInputGuideBodyType = z.infer<typeof QueryChatInputGuideBodySchema>; export const QueryChatInputGuideBodySchema = QueryChatInputGuideBodyRawSchema;
export type QueryChatInputGuideBodyType = z.infer<typeof QueryChatInputGuideBodyRawSchema>;
export type QueryChatInputGuideRuntimeBodyType = z.infer<typeof QueryChatInputGuideBodySchema>;
export const QueryChatInputGuideResponseSchema = z.array( export const QueryChatInputGuideResponseSchema = z.array(
z.string().meta({ example: '如何开始使用?', description: '引导文本' }) z.string().meta({ example: '如何开始使用?', description: '引导文本' })
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
DeleteChatInputGuideResponseSchema, DeleteChatInputGuideResponseSchema,
DeleteAllChatInputGuideBodySchema, DeleteAllChatInputGuideBodySchema,
DeleteAllChatInputGuideResponseSchema, DeleteAllChatInputGuideResponseSchema,
QueryChatInputGuideBodySchema, QueryChatInputGuideBodyRawSchema,
QueryChatInputGuideResponseSchema, QueryChatInputGuideResponseSchema,
UpdateChatInputGuideBodySchema, UpdateChatInputGuideBodySchema,
UpdateChatInputGuideResponseSchema UpdateChatInputGuideResponseSchema
...@@ -141,12 +141,12 @@ export const ChatInputGuidePath: OpenAPIPath = { ...@@ -141,12 +141,12 @@ export const ChatInputGuidePath: OpenAPIPath = {
'/core/chat/inputGuide/query': { '/core/chat/inputGuide/query': {
post: { post: {
summary: '查询对话输入引导(公开接口)', summary: '查询对话输入引导(公开接口)',
description: '根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权', description: '根据搜索词查询对话输入引导,支持应用和分享链接鉴权',
tags: [DevApiTagsMap.chatInputGuide], tags: [DevApiTagsMap.chatInputGuide],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: QueryChatInputGuideBodySchema schema: QueryChatInputGuideBodyRawSchema
} }
} }
}, },
......
import z from 'zod'; import z from 'zod';
import { InitChatResponseSchema } from '../controler/api';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
// ============= Init OutLink Chat ============= // ============= Init OutLink Chat =============
export const InitOutLinkChatQuerySchema = z.object({ export const InitOutLinkChatQuerySchema = z.object({
chatId: z.string().optional().describe('会话ID'), chatId: z.string().optional().describe('会话ID'),
shareId: z.string().describe('分享链接ID'), outLinkAuthData: OutLinkChatAuthSchema.describe('外链鉴权数据。GET query 中需 JSON 序列化。')
outLinkUid: z.string().describe('外链用户ID')
}); });
export type InitOutLinkChatQueryType = z.infer<typeof InitOutLinkChatQuerySchema>; export type InitOutLinkChatQueryType = z.infer<typeof InitOutLinkChatQuerySchema>;
export const InitOutLinkChatResponseSchema = InitChatResponseSchema;
export type InitOutLinkChatResponseType = z.infer<typeof InitOutLinkChatResponseSchema>;
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { DevApiTagsMap } from '../../../tag'; import { DevApiTagsMap } from '../../../tag';
import { InitOutLinkChatQuerySchema } from './api'; import { InitOutLinkChatQuerySchema, InitOutLinkChatResponseSchema } from './api';
export const OutLinkChatPath: OpenAPIPath = { export const OutLinkChatPath: OpenAPIPath = {
'/core/chat/outLink/init': { '/core/chat/outLink/init': {
...@@ -13,7 +13,12 @@ export const OutLinkChatPath: OpenAPIPath = { ...@@ -13,7 +13,12 @@ export const OutLinkChatPath: OpenAPIPath = {
}, },
responses: { responses: {
200: { 200: {
description: '成功返回会话初始化信息' description: '成功返回会话初始化信息',
content: {
'application/json': {
schema: InitOutLinkChatResponseSchema
}
}
} }
} }
} }
......
...@@ -5,14 +5,19 @@ import { DatasetCiteItemSchema } from '../../../../core/dataset/type'; ...@@ -5,14 +5,19 @@ import { DatasetCiteItemSchema } from '../../../../core/dataset/type';
import { LinkedListResponseSchema, LinkedPaginationSchema, PaginationSchema } from '../../../api'; import { LinkedListResponseSchema, LinkedPaginationSchema, PaginationSchema } from '../../../api';
import { ChatItemMiniSchema } from '../../../../core/chat/type'; import { ChatItemMiniSchema } from '../../../../core/chat/type';
import { AppTTSConfigTypeSchema } from '../../../../core/app/type'; import { AppTTSConfigTypeSchema } from '../../../../core/app/type';
import { GetChatTypeEnum } from '../../../../core/chat/constants'; import { ChatSourceTypeEnum, GetChatTypeEnum } from '../../../../core/chat/constants';
import { import {
createChatTargetInputSchema,
createOutLinkChatTargetInputSchema, createOutLinkChatTargetInputSchema,
refineRequiredChatTargetInput, refineRequiredChatTargetInput,
transformChatTargetInput transformChatAuthTargetInput
} from '../api'; } from '../api';
const GetRecordTypeSchema = z.enum([
GetChatTypeEnum.normal,
GetChatTypeEnum.outLink,
GetChatTypeEnum.home
]);
const QueryStringArraySchema = z const QueryStringArraySchema = z
.union([z.string(), z.array(z.string())]) .union([z.string(), z.array(z.string())])
.optional() .optional()
...@@ -34,7 +39,9 @@ export const GetResDataQueryRawSchema = createOutLinkChatTargetInputSchema({ ...@@ -34,7 +39,9 @@ export const GetResDataQueryRawSchema = createOutLinkChatTargetInputSchema({
chatId: z.string().optional().describe('会话ID'), chatId: z.string().optional().describe('会话ID'),
dataId: z.string().describe('对话数据ID') dataId: z.string().describe('对话数据ID')
}); });
export const GetResDataQuerySchema = GetResDataQueryRawSchema.transform(transformChatTargetInput); export const GetResDataQuerySchema = GetResDataQueryRawSchema.transform(
transformChatAuthTargetInput
);
export type GetResDataQueryType = z.infer<typeof GetResDataQueryRawSchema>; export type GetResDataQueryType = z.infer<typeof GetResDataQueryRawSchema>;
export type GetResDataQueryRuntimeType = z.infer<typeof GetResDataQuerySchema>; export type GetResDataQueryRuntimeType = z.infer<typeof GetResDataQuerySchema>;
...@@ -52,8 +59,9 @@ const DeleteChatRecordPropsSchema = { ...@@ -52,8 +59,9 @@ const DeleteChatRecordPropsSchema = {
export const DeleteChatRecordBodyRawSchema = createOutLinkChatTargetInputSchema( export const DeleteChatRecordBodyRawSchema = createOutLinkChatTargetInputSchema(
DeleteChatRecordPropsSchema DeleteChatRecordPropsSchema
); );
export const DeleteChatRecordBodySchema = export const DeleteChatRecordBodySchema = DeleteChatRecordBodyRawSchema.transform(
DeleteChatRecordBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type DeleteChatRecordBodyType = z.infer<typeof DeleteChatRecordBodyRawSchema>; export type DeleteChatRecordBodyType = z.infer<typeof DeleteChatRecordBodyRawSchema>;
export type DeleteChatRecordBodyRuntimeType = z.infer<typeof DeleteChatRecordBodySchema>; export type DeleteChatRecordBodyRuntimeType = z.infer<typeof DeleteChatRecordBodySchema>;
...@@ -80,7 +88,7 @@ const QuoteBodyPropsSchema = { ...@@ -80,7 +88,7 @@ const QuoteBodyPropsSchema = {
}; };
export const GetQuoteBodyRawSchema = createOutLinkChatTargetInputSchema(QuoteBodyPropsSchema); export const GetQuoteBodyRawSchema = createOutLinkChatTargetInputSchema(QuoteBodyPropsSchema);
export const GetQuoteBodySchema = GetQuoteBodyRawSchema.transform(transformChatTargetInput); export const GetQuoteBodySchema = GetQuoteBodyRawSchema.transform(transformChatAuthTargetInput);
export type GetQuoteBodyType = z.infer<typeof GetQuoteBodyRawSchema>; export type GetQuoteBodyType = z.infer<typeof GetQuoteBodyRawSchema>;
export type GetQuoteBodyRuntimeType = z.infer<typeof GetQuoteBodySchema>; export type GetQuoteBodyRuntimeType = z.infer<typeof GetQuoteBodySchema>;
...@@ -121,8 +129,9 @@ export type GetQuoteResponseType = z.infer<typeof GetQuoteResponseSchema>; ...@@ -121,8 +129,9 @@ export type GetQuoteResponseType = z.infer<typeof GetQuoteResponseSchema>;
export const GetCollectionQuoteBodyRawSchema = createOutLinkChatTargetInputSchema( export const GetCollectionQuoteBodyRawSchema = createOutLinkChatTargetInputSchema(
CollectionQuoteBodyPropsSchema CollectionQuoteBodyPropsSchema
); );
export const GetCollectionQuoteBodySchema = export const GetCollectionQuoteBodySchema = GetCollectionQuoteBodyRawSchema.transform(
GetCollectionQuoteBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type GetCollectionQuoteBodyType = z.infer<typeof GetCollectionQuoteBodyRawSchema>; export type GetCollectionQuoteBodyType = z.infer<typeof GetCollectionQuoteBodyRawSchema>;
export type GetCollectionQuoteBodyRuntimeType = z.infer<typeof GetCollectionQuoteBodySchema>; export type GetCollectionQuoteBodyRuntimeType = z.infer<typeof GetCollectionQuoteBodySchema>;
...@@ -151,10 +160,10 @@ const GetRecordPropsSchema = { ...@@ -151,10 +160,10 @@ const GetRecordPropsSchema = {
example: false, example: false,
description: '是否加载自定义反馈' description: '是否加载自定义反馈'
}), }),
type: z type: GetRecordTypeSchema.optional().meta({
.enum(GetChatTypeEnum) example: GetChatTypeEnum.normal,
.optional() description: '获取类型,影响数据过滤规则'
.meta({ example: 'normal', description: '获取类型,影响数据过滤规则' }), }),
includeDeleted: z.boolean().optional().meta({ includeDeleted: z.boolean().optional().meta({
example: false, example: false,
description: '是否包含已删除的记录' description: '是否包含已删除的记录'
...@@ -163,8 +172,9 @@ const GetRecordPropsSchema = { ...@@ -163,8 +172,9 @@ const GetRecordPropsSchema = {
export const GetPaginationRecordsBodyRawSchema = PaginationSchema.extend( export const GetPaginationRecordsBodyRawSchema = PaginationSchema.extend(
createOutLinkChatTargetInputSchema(GetRecordPropsSchema).shape createOutLinkChatTargetInputSchema(GetRecordPropsSchema).shape
).superRefine(refineRequiredChatTargetInput); ).superRefine(refineRequiredChatTargetInput);
export const GetPaginationRecordsBodySchema = export const GetPaginationRecordsBodySchema = GetPaginationRecordsBodyRawSchema.transform(
GetPaginationRecordsBodyRawSchema.transform(transformChatTargetInput); transformChatAuthTargetInput
);
export type GetPaginationRecordsBodyType = z.infer<typeof GetPaginationRecordsBodyRawSchema>; export type GetPaginationRecordsBodyType = z.infer<typeof GetPaginationRecordsBodyRawSchema>;
export type GetPaginationRecordsBodyRuntimeType = z.infer<typeof GetPaginationRecordsBodySchema>; export type GetPaginationRecordsBodyRuntimeType = z.infer<typeof GetPaginationRecordsBodySchema>;
...@@ -181,10 +191,12 @@ export type GetPaginationRecordsResponseType = z.infer<typeof GetPaginationRecor ...@@ -181,10 +191,12 @@ export type GetPaginationRecordsResponseType = z.infer<typeof GetPaginationRecor
* Description: 获取对话(v2) * Description: 获取对话(v2)
* ============================================================================ */ * ============================================================================ */
export const GetRecordsV2BodyRawSchema = LinkedPaginationSchema( export const GetRecordsV2BodyRawSchema = LinkedPaginationSchema(
createChatTargetInputSchema(GetRecordPropsSchema).shape createOutLinkChatTargetInputSchema(GetRecordPropsSchema).shape
).superRefine(refineRequiredChatTargetInput); ).superRefine(refineRequiredChatTargetInput);
export const GetRecordsV2BodySchema = GetRecordsV2BodyRawSchema.transform(transformChatTargetInput); export const GetRecordsV2BodySchema = GetRecordsV2BodyRawSchema.transform(
export type GetRecordsV2BodyType = z.infer<typeof GetRecordsV2BodyRawSchema>; transformChatAuthTargetInput
);
export type GetRecordsV2BodyType = z.input<typeof GetRecordsV2BodyRawSchema>;
export type GetRecordsV2BodyRuntimeType = z.infer<typeof GetRecordsV2BodySchema>; export type GetRecordsV2BodyRuntimeType = z.infer<typeof GetRecordsV2BodySchema>;
export const GetRecordsV2ResponseSchema = LinkedListResponseSchema(ChatItemMiniSchema).extend({ export const GetRecordsV2ResponseSchema = LinkedListResponseSchema(ChatItemMiniSchema).extend({
total: z.int() total: z.int()
...@@ -198,11 +210,10 @@ export type GetRecordsV2ResponseType = z.infer<typeof GetRecordsV2ResponseSchema ...@@ -198,11 +210,10 @@ export type GetRecordsV2ResponseType = z.infer<typeof GetRecordsV2ResponseSchema
* Description: 将文本转换为语音,返回二进制音频数据流 * Description: 将文本转换为语音,返回二进制音频数据流
* ============================================================================ */ * ============================================================================ */
export const GetChatSpeechBodySchema = OutLinkChatAuthSchema.extend({ export const GetChatSpeechBodySchema = createOutLinkChatTargetInputSchema({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
ttsConfig: AppTTSConfigTypeSchema.meta({ description: 'TTS 配置' }), ttsConfig: AppTTSConfigTypeSchema.meta({ description: 'TTS 配置' }),
input: z.string().meta({ example: '你好,世界', description: '要转换的文本内容' }) input: z.string().meta({ example: '你好,世界', description: '要转换的文本内容' })
}); }).transform(transformChatAuthTargetInput);
export type GetChatSpeechBodyType = z.infer<typeof GetChatSpeechBodySchema>; export type GetChatSpeechBodyType = z.infer<typeof GetChatSpeechBodySchema>;
/* ============================================================================ /* ============================================================================
...@@ -212,12 +223,14 @@ export type GetChatSpeechBodyType = z.infer<typeof GetChatSpeechBodySchema>; ...@@ -212,12 +223,14 @@ export type GetChatSpeechBodyType = z.infer<typeof GetChatSpeechBodySchema>;
* Description: 将 multipart 表单里的音频转换为文本 * Description: 将 multipart 表单里的音频转换为文本
* ============================================================================ */ * ============================================================================ */
export const AudioTranscriptionsDataRawSchema = createOutLinkChatTargetInputSchema({ export const AudioTranscriptionsDataRawSchema = z.object({
sourceType: z.enum(ChatSourceTypeEnum).describe('会话归属资源类型'),
sourceId: ObjectIdSchema.describe('会话归属资源 ID'),
chatId: z.string().min(1).max(256).describe('会话 ID'), chatId: z.string().min(1).max(256).describe('会话 ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据'),
duration: z.coerce.number().optional().describe('录音时长,单位秒') duration: z.coerce.number().optional().describe('录音时长,单位秒')
}); });
export const AudioTranscriptionsDataSchema = export const AudioTranscriptionsDataSchema = AudioTranscriptionsDataRawSchema;
AudioTranscriptionsDataRawSchema.transform(transformChatTargetInput);
export type AudioTranscriptionsDataType = z.infer<typeof AudioTranscriptionsDataRawSchema>; export type AudioTranscriptionsDataType = z.infer<typeof AudioTranscriptionsDataRawSchema>;
export type AudioTranscriptionsDataRuntimeType = z.infer<typeof AudioTranscriptionsDataSchema>; export type AudioTranscriptionsDataRuntimeType = z.infer<typeof AudioTranscriptionsDataSchema>;
......
...@@ -86,7 +86,7 @@ export const ChatRecordPath: OpenAPIPath = { ...@@ -86,7 +86,7 @@ export const ChatRecordPath: OpenAPIPath = {
post: { post: {
summary: '获取对话引用数据', summary: '获取对话引用数据',
description: '获取指定对话消息的数据集引用列表,需要对话访问权限', description: '获取指定对话消息的数据集引用列表,需要对话访问权限',
tags: [DevApiTagsMap.chatRecord], tags: [DevApiTagsMap.chatRecord, SystemOpenApiTagMap.chat],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
...@@ -110,7 +110,7 @@ export const ChatRecordPath: OpenAPIPath = { ...@@ -110,7 +110,7 @@ export const ChatRecordPath: OpenAPIPath = {
post: { post: {
summary: '获取集合分页引用数据', summary: '获取集合分页引用数据',
description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限', description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限',
tags: [DevApiTagsMap.chatRecord], tags: [DevApiTagsMap.chatRecord, SystemOpenApiTagMap.chat],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
ParentTreePathItemSchema ParentTreePathItemSchema
} from '../../../../common/parentFolder/type'; } from '../../../../common/parentFolder/type';
import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ObjectIdSchema } from '../../../../common/type/mongo';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { import {
DatasetCollectionSyncResultEnum, DatasetCollectionSyncResultEnum,
DatasetCollectionDataProcessModeEnum, DatasetCollectionDataProcessModeEnum,
...@@ -18,6 +17,11 @@ import { ...@@ -18,6 +17,11 @@ import {
import { PermissionSchema } from '../../../../support/permission/controller'; import { PermissionSchema } from '../../../../support/permission/controller';
import { PaginationResponseSchema, PaginationSchema } from '../../../api'; import { PaginationResponseSchema, PaginationSchema } from '../../../api';
import z from 'zod'; import z from 'zod';
import {
createOptionalOutLinkChatTargetInputSchema,
transformChatAuthTargetInput,
transformOptionalChatAuthTargetInput
} from '../../chat/api';
// ============= Scroll Collections ============= // ============= Scroll Collections =============
/** /**
...@@ -52,6 +56,7 @@ const BasicExportSchema = z ...@@ -52,6 +56,7 @@ const BasicExportSchema = z
.object({ .object({
collectionId: ObjectIdSchema.describe('集合ID') collectionId: ObjectIdSchema.describe('集合ID')
}) })
.strict()
.meta({ .meta({
description: '通过身份鉴权导出集合', description: '通过身份鉴权导出集合',
example: { example: {
...@@ -59,28 +64,46 @@ const BasicExportSchema = z ...@@ -59,28 +64,46 @@ const BasicExportSchema = z
} }
}); });
// Schema 2: Export from chat context with outlink authentication // Schema 2: Export from chat context with app/skill/outLink authentication
const ChatExportSchema = OutLinkChatAuthSchema.extend({ const ChatExportRawSchema = createOptionalOutLinkChatTargetInputSchema({
collectionId: ObjectIdSchema.describe('集合ID'), collectionId: ObjectIdSchema.describe('集合ID'),
appId: ObjectIdSchema.describe('应用ID'), chatId: z.string().min(1).max(256).describe('会话ID'),
chatId: ObjectIdSchema.describe('会话ID'), chatItemDataId: z.string().min(1).max(256).describe('对话ID'),
chatItemDataId: z.string().describe('对话ID'),
chatTime: z.coerce.date().optional().describe('对话时间') chatTime: z.coerce.date().optional().describe('对话时间')
}).meta({ })
description: '对话中导出集合,可通过 chatId 等身份信息', .superRefine((data, ctx) => {
example: { const hasAppTarget = !!data.appId;
collectionId: '1234567890', const hasSkillTarget = !!data.skillId;
appId: '1234567890', const hasShareAuth = !!(data.outLinkAuthData?.shareId && data.outLinkAuthData?.outLinkUid);
chatId: '1234567890',
chatItemDataId: '1234567890', if (!hasAppTarget && !hasSkillTarget && !hasShareAuth) {
chatTime: '2025-12-30T00:00:00.000Z', ctx.addIssue({
shareId: '1234567890', code: z.ZodIssueCode.custom,
outLinkUid: '1234567890' message: '对话导出必须提供 appId、skillId 或 share auth'
} });
}); }
})
.meta({
description: '对话中导出集合,可通过 chatId 等身份信息',
example: {
collectionId: '1234567890',
chatId: '1234567890',
chatItemDataId: '1234567890',
chatTime: '2025-12-30T00:00:00.000Z',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export const ExportCollectionBodySchema = z.union([BasicExportSchema, ChatExportSchema]); export const ExportCollectionBodyRawSchema = z.union([ChatExportRawSchema, BasicExportSchema]);
export type ExportCollectionBodyType = z.infer<typeof ExportCollectionBodySchema>; export const ExportCollectionBodySchema = z.union([
ChatExportRawSchema.transform(transformChatAuthTargetInput),
BasicExportSchema
]);
export type ExportCollectionBodyType = z.infer<typeof ExportCollectionBodyRawSchema>;
export type ExportCollectionRuntimeBodyType = z.infer<typeof ExportCollectionBodySchema>;
// ============= Delete Collection ============= // ============= Delete Collection =============
export const DeleteCollectionQuerySchema = z.object({ export const DeleteCollectionQuerySchema = z.object({
...@@ -151,13 +174,16 @@ export const GetCollectionPathsResponseSchema = z.array(ParentTreePathItemSchema ...@@ -151,13 +174,16 @@ export const GetCollectionPathsResponseSchema = z.array(ParentTreePathItemSchema
export type GetCollectionPathsResponseType = z.infer<typeof GetCollectionPathsResponseSchema>; export type GetCollectionPathsResponseType = z.infer<typeof GetCollectionPathsResponseSchema>;
// ============= Read Collection Source ============= // ============= Read Collection Source =============
export const ReadCollectionSourceBodySchema = OutLinkChatAuthSchema.extend({ export const ReadCollectionSourceBodyRawSchema = createOptionalOutLinkChatTargetInputSchema({
collectionId: ObjectIdSchema.meta({ description: '集合 ID' }), collectionId: ObjectIdSchema.meta({ description: '集合 ID' }),
appId: ObjectIdSchema.optional().meta({ description: '应用 ID(对话中使用)' }),
chatId: z.string().min(1).optional().meta({ description: '对话 ID(对话中使用)' }), chatId: z.string().min(1).optional().meta({ description: '对话 ID(对话中使用)' }),
chatItemDataId: z.string().min(1).optional().meta({ description: '对话消息 ID(对话中使用)' }) chatItemDataId: z.string().min(1).optional().meta({ description: '对话消息 ID(对话中使用)' })
}); });
export type ReadCollectionSourceBodyType = z.infer<typeof ReadCollectionSourceBodySchema>; export const ReadCollectionSourceBodySchema = ReadCollectionSourceBodyRawSchema.transform(
transformOptionalChatAuthTargetInput
);
export type ReadCollectionSourceBodyType = z.infer<typeof ReadCollectionSourceBodyRawSchema>;
export type ReadCollectionSourceRuntimeBodyType = z.infer<typeof ReadCollectionSourceBodySchema>;
export const ReadCollectionSourceResponseSchema = z.object({ export const ReadCollectionSourceResponseSchema = z.object({
type: z.literal('url').meta({ description: '资源类型' }), type: z.literal('url').meta({ description: '资源类型' }),
......
...@@ -4,13 +4,13 @@ import { SystemOpenApiTagMap } from '../../../tag'; ...@@ -4,13 +4,13 @@ import { SystemOpenApiTagMap } from '../../../tag';
import { import {
DeleteCollectionBodySchema, DeleteCollectionBodySchema,
DeleteCollectionQuerySchema, DeleteCollectionQuerySchema,
ExportCollectionBodySchema, ExportCollectionBodyRawSchema,
GetCollectionDetailQuerySchema, GetCollectionDetailQuerySchema,
GetCollectionPathsQuerySchema, GetCollectionPathsQuerySchema,
GetCollectionTrainingDetailQuerySchema, GetCollectionTrainingDetailQuerySchema,
GetCollectionTrainingDetailResponseSchema, GetCollectionTrainingDetailResponseSchema,
ListCollectionV2BodySchema, ListCollectionV2BodySchema,
ReadCollectionSourceBodySchema, ReadCollectionSourceBodyRawSchema,
ScrollCollectionsBodySchema, ScrollCollectionsBodySchema,
SyncCollectionBodySchema, SyncCollectionBodySchema,
UpdateDatasetCollectionBodySchema UpdateDatasetCollectionBodySchema
...@@ -137,7 +137,7 @@ export const DatasetCollectionPath: OpenAPIPath = { ...@@ -137,7 +137,7 @@ export const DatasetCollectionPath: OpenAPIPath = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: ReadCollectionSourceBodySchema schema: ReadCollectionSourceBodyRawSchema
} }
} }
}, },
...@@ -175,7 +175,7 @@ export const DatasetCollectionPath: OpenAPIPath = { ...@@ -175,7 +175,7 @@ export const DatasetCollectionPath: OpenAPIPath = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: ExportCollectionBodySchema schema: ExportCollectionBodyRawSchema
} }
} }
}, },
......
...@@ -9,6 +9,10 @@ import { ...@@ -9,6 +9,10 @@ import {
import { DatasetCollectionDataProcessModeEnum } from '../../../../core/dataset/constants'; import { DatasetCollectionDataProcessModeEnum } from '../../../../core/dataset/constants';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { PaginationSchema, PaginationResponseSchema } from '../../../api'; import { PaginationSchema, PaginationResponseSchema } from '../../../api';
import {
createOptionalOutLinkChatTargetInputSchema,
transformOptionalChatAuthTargetInput
} from '../../chat/api';
const PushDataChunkSchema = z.object({ const PushDataChunkSchema = z.object({
q: z.string().optional().meta({ q: z.string().optional().meta({
...@@ -145,16 +149,11 @@ export type DeleteDatasetDataIndexResponse = z.infer<typeof DeleteDatasetDataInd ...@@ -145,16 +149,11 @@ export type DeleteDatasetDataIndexResponse = z.infer<typeof DeleteDatasetDataInd
* API: 获取引用数据 * API: 获取引用数据
* Route: POST /api/core/dataset/data/getQuoteData * Route: POST /api/core/dataset/data/getQuoteData
* ============================================================================ */ * ============================================================================ */
export const GetQuoteDataBodySchema = OutLinkChatAuthSchema.extend({ export const GetQuoteDataBodyRawSchema = createOptionalOutLinkChatTargetInputSchema({
id: ObjectIdSchema.meta({ id: ObjectIdSchema.meta({
example: '68ad85a7463006c963799a05', example: '68ad85a7463006c963799a05',
description: '数据 ID' description: '数据 ID'
}), }),
// 对话模式下的额外字段(三者必须同时提供,否则走 API 模式)
appId: ObjectIdSchema.optional().meta({
example: '68ad85a7463006c963799a10',
description: '应用 ID(对话模式必填)'
}),
chatId: z.string().optional().meta({ chatId: z.string().optional().meta({
example: '68ad85a7463006c963799a11', example: '68ad85a7463006c963799a11',
description: '对话 ID(对话模式必填)' description: '对话 ID(对话模式必填)'
...@@ -165,10 +164,23 @@ export const GetQuoteDataBodySchema = OutLinkChatAuthSchema.extend({ ...@@ -165,10 +164,23 @@ export const GetQuoteDataBodySchema = OutLinkChatAuthSchema.extend({
}) })
}).refine( }).refine(
(d) => (d) =>
(!!d.chatId && !!d.appId && !!d.chatItemDataId) || (!d.chatId && !d.appId && !d.chatItemDataId), (!!d.chatId &&
{ message: '对话模式下 appId / chatId / chatItemDataId 必须同时提供' } (!!d.appId ||
!!d.skillId ||
!!(d.outLinkAuthData?.shareId && d.outLinkAuthData?.outLinkUid)) &&
!!d.chatItemDataId) ||
(!d.chatId &&
!d.appId &&
!d.skillId &&
!(d.outLinkAuthData?.shareId || d.outLinkAuthData?.outLinkUid) &&
!d.chatItemDataId),
{ message: '对话模式下 chat target / chatId / chatItemDataId 必须同时提供' }
);
export const GetQuoteDataBodySchema = GetQuoteDataBodyRawSchema.transform(
transformOptionalChatAuthTargetInput
); );
export type GetQuoteDataBody = z.infer<typeof GetQuoteDataBodySchema>; export type GetQuoteDataBody = z.infer<typeof GetQuoteDataBodyRawSchema>;
export type GetQuoteDataRuntimeBody = z.infer<typeof GetQuoteDataBodySchema>;
export const GetQuoteDataResponseSchema = z.object({ export const GetQuoteDataResponseSchema = z.object({
q: z.string().meta({ q: z.string().meta({
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
GetDatasetDataDetailQuerySchema, GetDatasetDataDetailQuerySchema,
UpdateDatasetDataBodySchema, UpdateDatasetDataBodySchema,
DeleteDatasetDataQuerySchema, DeleteDatasetDataQuerySchema,
GetQuoteDataBodySchema, GetQuoteDataBodyRawSchema,
InsertDataBodySchema, InsertDataBodySchema,
InsertImagesBodySchema, InsertImagesBodySchema,
PushDataBodySchema, PushDataBodySchema,
...@@ -196,7 +196,7 @@ export const DatasetDataPath: OpenAPIPath = { ...@@ -196,7 +196,7 @@ export const DatasetDataPath: OpenAPIPath = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: GetQuoteDataBodySchema schema: GetQuoteDataBodyRawSchema
} }
} }
}, },
......
...@@ -22,9 +22,6 @@ export const UpdateTeamBodySchema = z.object({ ...@@ -22,9 +22,6 @@ export const UpdateTeamBodySchema = z.object({
avatar: z.string().optional().meta({ avatar: z.string().optional().meta({
description: '团队头像 URL' description: '团队头像 URL'
}), }),
teamDomain: z.string().optional().meta({
description: '团队域名'
}),
openaiAccount: OpenaiAccountSchema.optional().meta({ openaiAccount: OpenaiAccountSchema.optional().meta({
description: 'OpenAI 账号配置' description: 'OpenAI 账号配置'
}), }),
......
...@@ -6,11 +6,37 @@ export const ShareChatAuthSchema = z.object({ ...@@ -6,11 +6,37 @@ export const ShareChatAuthSchema = z.object({
}); });
export type ShareChatAuthProps = z.infer<typeof ShareChatAuthSchema>; export type ShareChatAuthProps = z.infer<typeof ShareChatAuthSchema>;
export const TeamChatAuthSchema = z.object({ /**
teamId: z.string().optional().describe('团队ID'), * 解析 API 边界传入的外链鉴权数据。
teamToken: z.string().optional().describe('团队Token') *
}); * GET query 会把对象序列化成字符串;这里统一兼容 JSON string,让业务层只处理对象形态。
export type TeamChatAuthProps = z.infer<typeof TeamChatAuthSchema>; * 非 JSON 字符串保留原值交给 zod 报错,避免静默吞掉非法请求。
*/
export const parseOutLinkChatAuthInput = (value: unknown) => {
if (typeof value !== 'string') return value;
try {
return JSON.parse(value);
} catch {
return value;
}
};
export const OutLinkChatAuthSchema = z.union([
ShareChatAuthSchema,
z.string().transform((value, ctx) => {
const parsedValue = parseOutLinkChatAuthInput(value);
const parsedAuth = ShareChatAuthSchema.safeParse(parsedValue);
if (!parsedAuth.success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid outLinkAuthData'
});
return z.NEVER;
}
export const OutLinkChatAuthSchema = ShareChatAuthSchema.extend(TeamChatAuthSchema.shape); return parsedAuth.data;
})
]);
export type OutLinkChatAuthProps = z.infer<typeof OutLinkChatAuthSchema>; export type OutLinkChatAuthProps = z.infer<typeof OutLinkChatAuthSchema>;
...@@ -6,8 +6,7 @@ export enum AuthUserTypeEnum { ...@@ -6,8 +6,7 @@ export enum AuthUserTypeEnum {
token = 'token', token = 'token',
root = 'root', root = 'root',
apikey = 'apikey', apikey = 'apikey',
outLink = 'outLink', outLink = 'outLink'
teamDomain = 'teamDomain'
} }
export enum PermissionTypeEnum { export enum PermissionTypeEnum {
......
export const TeamCollectionName = 'teams'; export const TeamCollectionName = 'teams';
export const TeamMemberCollectionName = 'team_members'; export const TeamMemberCollectionName = 'team_members';
export const TeamTagsCollectionName = 'team_tags';
export enum TeamMemberRoleEnum { export enum TeamMemberRoleEnum {
owner = 'owner' owner = 'owner'
......
...@@ -18,7 +18,6 @@ export type CreateTeamProps = { ...@@ -18,7 +18,6 @@ export type CreateTeamProps = {
export type UpdateTeamProps = Omit<ThirdPartyAccountType, 'externalWorkflowVariable'> & { export type UpdateTeamProps = Omit<ThirdPartyAccountType, 'externalWorkflowVariable'> & {
name?: string; name?: string;
avatar?: string; avatar?: string;
teamDomain?: string;
externalWorkflowVariable?: { key: string; value: string }; externalWorkflowVariable?: { key: string; value: string };
}; };
......
export type AuthTeamTagTokenProps = {
teamId: string;
teamToken: string;
};
export type AuthTokenFromTeamDomainResponse = {
success: boolean;
msg?: string;
message?: string;
data: {
uid: string;
tags: string[];
};
};
...@@ -23,7 +23,6 @@ export type TeamSchema = { ...@@ -23,7 +23,6 @@ export type TeamSchema = {
avatar: string; avatar: string;
createTime: Date; createTime: Date;
balance: number; balance: number;
teamDomain: string;
limit: { limit: {
lastExportDatasetTime: Date; lastExportDatasetTime: Date;
lastWebsiteSyncTime: Date; lastWebsiteSyncTime: Date;
...@@ -33,18 +32,6 @@ export type TeamSchema = { ...@@ -33,18 +32,6 @@ export type TeamSchema = {
deleteTime?: Date; deleteTime?: Date;
} & ThirdPartyAccountType; } & ThirdPartyAccountType;
export type tagsType = {
label: string;
key: string;
};
export type TeamTagSchema = TeamTagItemType & {
_id: string;
teamId: string;
createTime: Date;
updateTime?: Date;
};
export type TeamMemberSchema = { export type TeamMemberSchema = {
_id: string; _id: string;
teamId: string; teamId: string;
...@@ -71,7 +58,6 @@ export const TeamTmbItemSchema = ThidPartyAccountSchema.extend({ ...@@ -71,7 +58,6 @@ export const TeamTmbItemSchema = ThidPartyAccountSchema.extend({
avatar: z.string(), avatar: z.string(),
balance: z.number().optional(), balance: z.number().optional(),
tmbId: z.string(), tmbId: z.string(),
teamDomain: z.string(),
role: z.enum(TeamMemberRoleEnum), role: z.enum(TeamMemberRoleEnum),
status: z.enum(TeamMemberStatusEnum), status: z.enum(TeamMemberStatusEnum),
notificationAccount: z.string().optional(), notificationAccount: z.string().optional(),
...@@ -113,11 +99,6 @@ export type TeamMemberItemType< ...@@ -113,11 +99,6 @@ export type TeamMemberItemType<
} }
: unknown); : unknown);
export type TeamTagItemType = {
label: string;
key: string;
};
export type TeamInvoiceHeaderType = { export type TeamInvoiceHeaderType = {
teamName: string; teamName: string;
unifiedCreditCode: string; unifiedCreditCode: string;
......
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { openAPIDocument } from '@fastgpt/global/openapi/provider/devapi'; import { openAPIDocument } from '@fastgpt/global/openapi/provider/devapi';
import { apiDocOpenAPIDocument } from '@fastgpt/global/openapi/provider/systemopenapi';
import { GetPreviewNodeQuerySchema } from '@fastgpt/global/openapi/core/app/tool/api'; import { GetPreviewNodeQuerySchema } from '@fastgpt/global/openapi/core/app/tool/api';
describe('GetPreviewNodeQuerySchema', () => { describe('GetPreviewNodeQuerySchema', () => {
...@@ -64,4 +65,12 @@ describe('GetPreviewNodeQuerySchema', () => { ...@@ -64,4 +65,12 @@ describe('GetPreviewNodeQuerySchema', () => {
expect(tags).toContain('会话管理'); expect(tags).toContain('会话管理');
expect(tags.some((tag) => tag.startsWith('systemOpenAPI:'))).toBe(false); expect(tags.some((tag) => tag.startsWith('systemOpenAPI:'))).toBe(false);
}); });
it('includes chat quote APIs in System OpenAPI document', () => {
expect(apiDocOpenAPIDocument.paths['/core/chat/record/getQuote']?.post).toBeDefined();
expect(apiDocOpenAPIDocument.paths['/core/chat/record/getCollectionQuote']?.post).toBeDefined();
expect(apiDocOpenAPIDocument.paths['/core/chat/record/getQuote']?.post?.tags).toEqual([
'对话管理'
]);
});
}); });
...@@ -63,7 +63,6 @@ describe('ChatSourceEnum', () => { ...@@ -63,7 +63,6 @@ describe('ChatSourceEnum', () => {
expect(ChatSourceEnum.share).toBe('share'); expect(ChatSourceEnum.share).toBe('share');
expect(ChatSourceEnum.api).toBe('api'); expect(ChatSourceEnum.api).toBe('api');
expect(ChatSourceEnum.cronJob).toBe('cronJob'); expect(ChatSourceEnum.cronJob).toBe('cronJob');
expect(ChatSourceEnum.team).toBe('team');
expect(ChatSourceEnum.feishu).toBe('feishu'); expect(ChatSourceEnum.feishu).toBe('feishu');
expect(ChatSourceEnum.official_account).toBe('official_account'); expect(ChatSourceEnum.official_account).toBe('official_account');
expect(ChatSourceEnum.wecom).toBe('wecom'); expect(ChatSourceEnum.wecom).toBe('wecom');
......
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { import {
ShareChatAuthSchema, ShareChatAuthSchema,
TeamChatAuthSchema,
OutLinkChatAuthSchema OutLinkChatAuthSchema
} from '@fastgpt/global/support/permission/chat'; } from '@fastgpt/global/support/permission/chat';
...@@ -63,70 +62,11 @@ describe('permission/chat', () => { ...@@ -63,70 +62,11 @@ describe('permission/chat', () => {
}); });
}); });
describe('TeamChatAuthSchema', () => {
it('should validate valid TeamChatAuth object', () => {
const validData = {
teamId: 'test-team-id',
teamToken: 'test-token'
};
const result = TeamChatAuthSchema.safeParse(validData);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.teamId).toBe('test-team-id');
expect(result.data.teamToken).toBe('test-token');
}
});
it('should validate object with only teamId', () => {
const validData = {
teamId: 'test-team-id'
};
const result = TeamChatAuthSchema.safeParse(validData);
expect(result.success).toBe(true);
});
it('should validate object with only teamToken', () => {
const validData = {
teamToken: 'test-token'
};
const result = TeamChatAuthSchema.safeParse(validData);
expect(result.success).toBe(true);
});
it('should validate empty object', () => {
const result = TeamChatAuthSchema.safeParse({});
expect(result.success).toBe(true);
});
it('should reject invalid teamId type', () => {
const invalidData = {
teamId: 123
};
const result = TeamChatAuthSchema.safeParse(invalidData);
expect(result.success).toBe(false);
});
it('should reject invalid teamToken type', () => {
const invalidData = {
teamToken: false
};
const result = TeamChatAuthSchema.safeParse(invalidData);
expect(result.success).toBe(false);
});
});
describe('OutLinkChatAuthSchema', () => { describe('OutLinkChatAuthSchema', () => {
it('should validate valid OutLinkChatAuth object with all fields', () => { it('should validate valid OutLinkChatAuth object with share fields', () => {
const validData = { const validData = {
shareId: 'test-share-id', shareId: 'test-share-id',
outLinkUid: 'test-uid', outLinkUid: 'test-uid'
teamId: 'test-team-id',
teamToken: 'test-token'
}; };
const result = OutLinkChatAuthSchema.safeParse(validData); const result = OutLinkChatAuthSchema.safeParse(validData);
...@@ -134,8 +74,6 @@ describe('permission/chat', () => { ...@@ -134,8 +74,6 @@ describe('permission/chat', () => {
if (result.success) { if (result.success) {
expect(result.data.shareId).toBe('test-share-id'); expect(result.data.shareId).toBe('test-share-id');
expect(result.data.outLinkUid).toBe('test-uid'); expect(result.data.outLinkUid).toBe('test-uid');
expect(result.data.teamId).toBe('test-team-id');
expect(result.data.teamToken).toBe('test-token');
} }
}); });
...@@ -149,39 +87,40 @@ describe('permission/chat', () => { ...@@ -149,39 +87,40 @@ describe('permission/chat', () => {
expect(result.success).toBe(true); expect(result.success).toBe(true);
}); });
it('should validate object with team fields only', () => { it('should validate empty object', () => {
const validData = { const result = OutLinkChatAuthSchema.safeParse({});
teamId: 'test-team-id',
teamToken: 'test-token'
};
const result = OutLinkChatAuthSchema.safeParse(validData);
expect(result.success).toBe(true); expect(result.success).toBe(true);
}); });
it('should validate empty object', () => { it('should parse JSON string and return an auth object', () => {
const result = OutLinkChatAuthSchema.safeParse({}); const result = OutLinkChatAuthSchema.safeParse(
JSON.stringify({
shareId: 'test-share-id',
outLinkUid: 'test-uid'
})
);
expect(result.success).toBe(true); expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toEqual({
shareId: 'test-share-id',
outLinkUid: 'test-uid'
});
}
}); });
it('should reject invalid field types', () => { it('should reject invalid field types', () => {
const invalidData = { const invalidData = {
shareId: 123, shareId: 123
teamId: true
}; };
const result = OutLinkChatAuthSchema.safeParse(invalidData); const result = OutLinkChatAuthSchema.safeParse(invalidData);
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
it('should validate mixed valid and optional fields', () => { it('should reject invalid JSON string', () => {
const validData = { const result = OutLinkChatAuthSchema.safeParse('{invalid-json');
shareId: 'test-share-id', expect(result.success).toBe(false);
teamToken: 'test-token'
};
const result = OutLinkChatAuthSchema.safeParse(validData);
expect(result.success).toBe(true);
}); });
}); });
}); });
...@@ -90,8 +90,8 @@ describe('wallet/usage/tools', () => { ...@@ -90,8 +90,8 @@ describe('wallet/usage/tools', () => {
expect(result).toBe(UsageSourceEnum.fastgpt); expect(result).toBe(UsageSourceEnum.fastgpt);
}); });
it('should handle teamDomain authType', () => { it('should handle token authType', () => {
const result = getUsageSourceByAuthType({ authType: AuthUserTypeEnum.teamDomain }); const result = getUsageSourceByAuthType({ authType: AuthUserTypeEnum.token });
expect(result).toBe(UsageSourceEnum.fastgpt); expect(result).toBe(UsageSourceEnum.fastgpt);
}); });
}); });
......
...@@ -15,6 +15,7 @@ export enum TimerIdEnum { ...@@ -15,6 +15,7 @@ export enum TimerIdEnum {
chatHistoryCleanup = 'chatHistoryCleanup', chatHistoryCleanup = 'chatHistoryCleanup',
datasetSyncSchedulerReconcile = 'datasetSyncSchedulerReconcile', datasetSyncSchedulerReconcile = 'datasetSyncSchedulerReconcile',
archiveInactiveSandboxes = 'archiveInactiveSandboxes', archiveInactiveSandboxes = 'archiveInactiveSandboxes',
clearStaleArchivingSandboxes = 'clearStaleArchivingSandboxes',
/** 纠正长时间卡在 generating 的会话状态 */ /** 纠正长时间卡在 generating 的会话状态 */
cleanStaleGeneratingChat = 'cleanStaleGeneratingChat' cleanStaleGeneratingChat = 'cleanStaleGeneratingChat'
} }
......
import { type FastGPTConfigFileType } from '@fastgpt/global/common/system/types'; import { type FastGPTConfigFileType } from '@fastgpt/global/common/system/types';
import { isIPv6 } from 'net'; import { isIPv6 } from 'net';
import { getLogger, LogCategories } from '../logger'; import { getLogger, LogCategories } from '../logger';
import {
getAgentSandboxArchiveMaxBytes,
getAgentSandboxMaxFileBytes,
getAgentSandboxSkillMaxBytes
} from '../../core/ai/sandbox/interface/config';
import { hasAgentSandboxConfig, serviceEnv } from '../../env'; import { hasAgentSandboxConfig, serviceEnv } from '../../env';
const logger = getLogger(LogCategories.ERROR); const logger = getLogger(LogCategories.ERROR);
...@@ -26,9 +31,9 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => { ...@@ -26,9 +31,9 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => {
config.feConfigs.limit = { config.feConfigs.limit = {
...config.feConfigs.limit, ...config.feConfigs.limit,
agentSandboxMaxEditDebug: serviceEnv.AGENT_SANDBOX_MAX_EDIT_DEBUG, agentSandboxMaxEditDebug: serviceEnv.AGENT_SANDBOX_MAX_EDIT_DEBUG,
agentSandboxArchiveMaxBytes: serviceEnv.AGENT_SANDBOX_ARCHIVE_MAX_SIZE * 1024 * 1024, agentSandboxArchiveMaxBytes: getAgentSandboxArchiveMaxBytes(),
skillSandboxMaxBytes: serviceEnv.AGENT_SANDBOX_SKILL_MAX_SIZE * 1024 * 1024, skillSandboxMaxBytes: getAgentSandboxSkillMaxBytes(),
agentSandboxMaxFileBytes: serviceEnv.AGENT_SANDBOX_MAX_FILE_SIZE * 1024 * 1024, agentSandboxMaxFileBytes: getAgentSandboxMaxFileBytes(),
maxFolderDepth: serviceEnv.MAX_FOLDER_DEPTH maxFolderDepth: serviceEnv.MAX_FOLDER_DEPTH
}; };
......
...@@ -44,6 +44,7 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>( ...@@ -44,6 +44,7 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>(
throwError = true, throwError = true,
body, body,
custonHeaders, custonHeaders,
timeout,
userKey, userKey,
maxContinuations = 1, maxContinuations = 1,
saveLLMResponseRecord = true, saveLLMResponseRecord = true,
...@@ -98,6 +99,7 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>( ...@@ -98,6 +99,7 @@ export const createLLMResponse = async <T extends ChatCompletionCreateParams>(
}, },
modelData, modelData,
userKey, userKey,
timeout,
options: { options: {
headers: { headers: {
Accept: 'application/json, text/plain, */*', Accept: 'application/json, text/plain, */*',
......
...@@ -64,6 +64,8 @@ export type CreateLLMResponseProps< ...@@ -64,6 +64,8 @@ export type CreateLLMResponseProps<
// 上层中断时返回 true。底层会 abort stream 并用 finish_reason=close 表达正常关闭。 // 上层中断时返回 true。底层会 abort stream 并用 finish_reason=close 表达正常关闭。
isAborted?: () => boolean | undefined | null; isAborted?: () => boolean | undefined | null;
custonHeaders?: Record<string, string>; custonHeaders?: Record<string, string>;
// 单次底层模型请求超时时间。辅助类 LLM 请求可传较短值,避免阻塞主链路。
timeout?: number;
// finish_reason=length 时最多连续请求的次数,避免模型一直返回 length 造成死循环。 // finish_reason=length 时最多连续请求的次数,避免模型一直返回 length 造成死循环。
maxContinuations?: number; maxContinuations?: number;
// 是否保存 LLM 请求响应详情。内部辅助调用可关闭,避免污染用户可见的请求记录。 // 是否保存 LLM 请求响应详情。内部辅助调用可关闭,避免污染用户可见的请求记录。
......
/**
* 沙盒业务层:汇总后台任务和管理脚本所需沙盒能力。
*
* 负责收口 cron、批量归档和当前 provider 查询;不承载迁移脚本自己的 Mongo 修复逻辑。
*/
import { getConfiguredSandboxProvider as resolveConfiguredSandboxProvider } from '../infrastructure/provider/config';
export { cronJob as runSandboxArchiveCron } from './cron';
export {
archiveInactiveSandboxes,
archiveSandboxResources,
type SandboxArchiveResult
} from './archive';
export type { SandboxProviderType } from '../type';
/**
* 返回当前启用的 sandbox provider。
*
* 管理脚本通过业务层读取 provider,避免接口层直接暴露 infrastructure 配置模块。
*/
export function getConfiguredSandboxProvider() {
return resolveConfiguredSandboxProvider();
}
/**
* 沙盒业务层:注册 sandbox 闲置暂停和归档定时任务。
*
* 只负责 cron 调度和锁控制,具体 stop/archive 流程交给对应业务服务。
*/
import { SANDBOX_SUSPEND_MINUTES } from '@fastgpt/global/core/ai/sandbox/constants'; import { SANDBOX_SUSPEND_MINUTES } from '@fastgpt/global/core/ai/sandbox/constants';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../common/logger';
import { setCron } from '../../../../common/system/cron'; import { setCron } from '../../../../common/system/cron';
import { subMinutes } from 'date-fns'; import { subMinutes } from 'date-fns';
import { findInactiveRunningSandboxResources } from '../instance/repository'; import { findInactiveRunningSandboxResources } from '../infrastructure/instance/repository';
import { stopSandboxResources } from './resource'; import { stopSandboxResources } from './resource';
import { checkTimerLock } from '../../../../common/system/timerLock/utils'; import { checkTimerLock } from '../../../../common/system/timerLock/utils';
import { TimerIdEnum } from '../../../../common/system/timerLock/constants'; import { TimerIdEnum } from '../../../../common/system/timerLock/constants';
import { archiveInactiveSandboxes } from './archive'; import { archiveInactiveSandboxes, clearStaleArchivingSandboxes } from './archive';
const logger = getLogger(LogCategories.MODULE.AI.SANDBOX); const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
...@@ -17,7 +22,7 @@ const logger = getLogger(LogCategories.MODULE.AI.SANDBOX); ...@@ -17,7 +22,7 @@ const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
* 避免 cron 直接理解 provider 和 Mongo 更新细节。 * 避免 cron 直接理解 provider 和 Mongo 更新细节。
*/ */
export const cronJob = async () => { export const cronJob = async () => {
setCron('*/5 * * * *', async () => { setCron('*/10 * * * *', async () => {
const instances = await findInactiveRunningSandboxResources( const instances = await findInactiveRunningSandboxResources(
subMinutes(new Date(), SANDBOX_SUSPEND_MINUTES) subMinutes(new Date(), SANDBOX_SUSPEND_MINUTES)
); );
...@@ -41,4 +46,16 @@ export const cronJob = async () => { ...@@ -41,4 +46,16 @@ export const cronJob = async () => {
logger.error('Sandbox archive cron failed', { error }); logger.error('Sandbox archive cron failed', { error });
}); });
}); });
setCron('*/10 * * * *', async () => {
const locked = await checkTimerLock({
timerId: TimerIdEnum.clearStaleArchivingSandboxes,
lockMinuted: 9
});
if (!locked) return;
await clearStaleArchivingSandboxes().catch((error) => {
logger.error('Sandbox stale archiving cleanup cron failed', { error });
});
});
}; };
import { type SandboxClient } from '@fastgpt/service/core/ai/sandbox/service/runtime'; /**
import type archiver from 'archiver'; * 沙盒业务层:处理已有 sandbox 的文件写入、读取、路径解析和目录打包。
*
* 只操作调用方传入的 sandbox/client,不创建、恢复或清理 sandbox 实例。
*/
import type { FileWriteEntry, ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import mime from 'mime'; import mime from 'mime';
import { getSandboxRuntimeProfile } from '@fastgpt/service/core/ai/sandbox/runtime/profile'; import { pickOutboundAxios } from '../../../../common/api/axios';
import type { SandboxClient } from './runtime/client';
import { getSandboxRuntimeProfile } from '../infrastructure/provider/runtimeProfile';
export type SandboxUrlFile = {
path: string;
url: string;
};
export type SandboxFileContent = { export type SandboxFileContent = {
content: Buffer; content: Buffer;
...@@ -9,25 +20,56 @@ export type SandboxFileContent = { ...@@ -9,25 +20,56 @@ export type SandboxFileContent = {
fileName: string; fileName: string;
}; };
type ResolveSandboxWorkspacePathOptions = {
allowAbsolutePath?: boolean;
};
type SandboxDirectoryArchive = {
append: (source: Buffer, data: { name: string }) => void;
};
const MAX_ARCHIVE_DEPTH = 20;
const trimSandboxPathRight = (value: string) => (value === '/' ? '' : value.replace(/\/+$/, '')); const trimSandboxPathRight = (value: string) => (value === '/' ? '' : value.replace(/\/+$/, ''));
const getSandboxWorkDirectory = () => getSandboxRuntimeProfile().workDirectory; const getSandboxWorkDirectory = () => getSandboxRuntimeProfile().workDirectory;
const isWithinSandboxWorkspace = (path: string, workDirectory: string) => { const isWithinSandboxWorkspace = (path: string, workDirectory: string) => {
const workspace = trimSandboxPathRight(workDirectory); const workspace = trimSandboxPathRight(workDirectory);
return path === workspace || path.startsWith(`${workspace}/`); return path === workspace || path.startsWith(`${workspace}/`);
}; };
type ResolveSandboxWorkspacePathOptions = { /**
allowAbsolutePath?: boolean; * 将远程 URL 文件写入已存在的 sandbox 实例。
}; *
* 这里不负责 sandbox 生命周期,只统一处理下载和 writeFiles。
*/
export async function writeUrlFilesToSandbox(sandbox: ISandbox, files: SandboxUrlFile[]) {
const writeFileTasks: Promise<FileWriteEntry>[] = [];
for (const { path, url } of files) {
if (!path) continue;
writeFileTasks.push(
pickOutboundAxios(url)
.get<ArrayBuffer>(url, {
responseType: 'arraybuffer'
})
.then((response) => ({
path,
data: response.data
}))
);
}
if (writeFileTasks.length === 0) return;
await sandbox.writeFiles(await Promise.all(writeFileTasks));
}
/** /**
* 将编辑器传入的相对路径锚定到 sandbox workspace。 * 将编辑器传入的相对路径锚定到 sandbox workspace。
* *
* SandboxEditor 以 `.` 表示工作区根目录;Sealos provider 自身的 `.` 会落到 * SandboxEditor 以 `.` 表示工作区根目录;Sealos provider 自身的 `.` 会落到
* `/home/devbox`,因此 API 边界必须显式把相对路径解析到当前运行态 workDirectory。公开 API * `/home/devbox`,因此 API 边界必须显式把相对路径解析到当前运行态 workDirectory。
* 的用户输入默认拒绝绝对路径,避免具备 sandbox 权限的用户读取 workspace 外文件;provider 返回的
* 内部路径可通过 allowAbsolutePath 显式放行。
*/ */
export function resolveSandboxWorkspacePath( export function resolveSandboxWorkspacePath(
path: string | undefined, path: string | undefined,
...@@ -59,7 +101,9 @@ export function resolveSandboxWorkspacePath( ...@@ -59,7 +101,9 @@ export function resolveSandboxWorkspacePath(
/** /**
* 判断沙盒路径是否为目录。 * 判断沙盒路径是否为目录。
* 当 provider 查询不到信息时,对根路径和以斜杠结尾的路径做兼容性兜底,避免旧沙盒实现误报。 *
* 当 provider 查询不到信息时,对根路径和以斜杠结尾的路径做兼容性兜底,
* 避免旧沙盒实现误报。
*/ */
export async function isSandboxPathDirectory( export async function isSandboxPathDirectory(
sandbox: SandboxClient, sandbox: SandboxClient,
...@@ -76,6 +120,7 @@ export async function isSandboxPathDirectory( ...@@ -76,6 +120,7 @@ export async function isSandboxPathDirectory(
/** /**
* 读取沙盒文件内容并返回下载/预览所需的 Buffer、contentType 和文件名。 * 读取沙盒文件内容并返回下载/预览所需的 Buffer、contentType 和文件名。
*
* preview=true 时按扩展名推断 MIME,用于编辑器内联预览;非 preview 始终以二进制下载方式返回。 * preview=true 时按扩展名推断 MIME,用于编辑器内联预览;非 preview 始终以二进制下载方式返回。
*/ */
export async function getSandboxFileContent( export async function getSandboxFileContent(
...@@ -92,8 +137,8 @@ export async function getSandboxFileContent( ...@@ -92,8 +137,8 @@ export async function getSandboxFileContent(
} }
const fileName = providerPath.split('/').pop() || 'file'; const fileName = providerPath.split('/').pop() || 'file';
// 注意:preview 模式下 contentType 由文件路径决定,可能返回 text/html / image/svg+xml 等危险类型。 // preview 模式下 contentType 由文件路径决定。若后续允许同源直接导航到该内容,
// 若未来有任何代码让浏览器直接导航到 download 端点(iframe / window.open 等),需确保这类内容不被同源渲染,否则会造成存储型 XSS // 需要重新评估 HTML/SVG 等类型的渲染风险
const contentType = preview const contentType = preview
? (mime.getType(providerPath) ?? 'application/octet-stream') ? (mime.getType(providerPath) ?? 'application/octet-stream')
: 'application/octet-stream'; : 'application/octet-stream';
...@@ -105,15 +150,15 @@ export async function getSandboxFileContent( ...@@ -105,15 +150,15 @@ export async function getSandboxFileContent(
}; };
} }
const MAX_ARCHIVE_DEPTH = 20;
/** /**
* 递归把目录加入 ZIP 归档。 * 递归把目录加入 ZIP 归档。
* 读取失败的文件会被跳过,目录深度超过 MAX_ARCHIVE_DEPTH 时停止递归,避免异常目录结构导致打包失控。 *
* 读取失败的文件会被跳过,目录深度超过 MAX_ARCHIVE_DEPTH 时停止递归,
* 避免异常目录结构导致打包失控。
*/ */
export async function addDirectoryToArchive( export async function addDirectoryToArchive(
sandbox: SandboxClient, sandbox: SandboxClient,
archive: archiver.Archiver, archive: SandboxDirectoryArchive,
dirPath: string, dirPath: string,
archivePath: string, archivePath: string,
depth: number = 0 depth: number = 0
......
/**
* 沙盒业务层:编排 sandbox 资源停止、删除和业务归属清理。
*
* 负责调用 provider 删除/停止、同步 Mongo 状态并清理 volume/archive 对象。
*/
import { batchRun } from '@fastgpt/global/common/system/utils'; import { batchRun } from '@fastgpt/global/common/system/utils';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../common/logger';
import { import {
deleteSandboxResourceRecord, deleteSandboxResourceRecord,
findSandboxInstanceBySandboxIdAndTeam,
findSandboxResourceBySandboxIdAndTeam,
findSandboxResourcesBySource, findSandboxResourcesBySource,
findSandboxResourcesBySourceChatIds, findSandboxResourcesBySourceChatIds,
findSkillRelatedSandboxResources, findSkillRelatedSandboxResources,
markSandboxResourceStopped, markSandboxResourceStopped,
type SandboxResourceRef type SandboxResourceRef
} from '../instance/repository'; } from '../infrastructure/instance/repository';
import { buildSandboxResourceAdapter } from '../provider/adapter'; import { getSandboxProviderConfig } from '../infrastructure/provider/config';
import { deleteSessionVolume } from '../volume/service'; import { buildSandboxResourceAdapter } from '../infrastructure/provider/adapter';
import { deleteSessionVolume } from '../infrastructure/volume/service';
import { getS3SandboxSource } from '../../../../common/s3/sources/sandbox'; import { getS3SandboxSource } from '../../../../common/s3/sources/sandbox';
import type { SandboxInstanceSchemaType } from '../type';
const logger = getLogger(LogCategories.MODULE.AI.SANDBOX); const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
export type GetSandboxInfoParams = {
sandboxId: string;
teamId: string;
};
export type DeleteSandboxParams = {
sandboxId: string;
teamId: string;
};
const deleteSandboxResources = async (instances: SandboxResourceRef[]) => { const deleteSandboxResources = async (instances: SandboxResourceRef[]) => {
if (!instances.length) return; if (!instances.length) return;
...@@ -52,7 +71,7 @@ export async function stopSandboxResource(resource: SandboxResourceRef): Promise ...@@ -52,7 +71,7 @@ export async function stopSandboxResource(resource: SandboxResourceRef): Promise
*/ */
export async function deleteSandboxResource( export async function deleteSandboxResource(
resource: SandboxResourceRef, resource: SandboxResourceRef,
opts: { keepVolume?: boolean } = {} opts: { keepVolume?: boolean; keepArchive?: boolean } = {}
): Promise<void> { ): Promise<void> {
const sandbox = buildSandboxResourceAdapter(resource); const sandbox = buildSandboxResourceAdapter(resource);
...@@ -66,16 +85,64 @@ export async function deleteSandboxResource( ...@@ -66,16 +85,64 @@ export async function deleteSandboxResource(
}); });
} }
await deleteSandboxResourceRecord(resource); await deleteSandboxResourceRecord(resource);
await getS3SandboxSource() if (!opts.keepArchive) {
.deleteWorkspaceArchive({ await getS3SandboxSource()
sandboxId: resource.sandboxId .deleteWorkspaceArchive({
}) sandboxId: resource.sandboxId
.catch((err) => { })
logger.error('Failed to delete sandbox archive', { .catch((err: unknown) => {
sandboxId: resource.sandboxId, logger.error('Failed to delete sandbox archive', {
error: err sandboxId: resource.sandboxId,
error: err
});
}); });
}); }
}
/**
* 查询当前 provider 下指定团队可访问的 sandbox 信息。
*
* 只读取本地实例表,不触发 provider 创建、恢复或归档流程。
*/
export async function getSandboxInfo(
params: GetSandboxInfoParams
): Promise<SandboxInstanceSchemaType> {
const { sandboxId, teamId } = params;
const providerConfig = getSandboxProviderConfig();
const sandbox = await findSandboxInstanceBySandboxIdAndTeam({
provider: providerConfig.provider,
sandboxId,
teamId
});
if (!sandbox) {
throw new Error('Sandbox not found or access denied');
}
return sandbox.toObject<SandboxInstanceSchemaType>();
}
/**
* 删除当前 provider 下指定团队可访问的 sandbox。
*
* 删除动作会同时清理远端资源、本地实例记录和归档对象。
*/
export async function deleteSandbox(params: DeleteSandboxParams): Promise<void> {
const { sandboxId, teamId } = params;
const providerConfig = getSandboxProviderConfig();
const instanceDoc = await findSandboxResourceBySandboxIdAndTeam({
provider: providerConfig.provider,
sandboxId,
teamId
});
if (!instanceDoc) {
throw new Error('Sandbox not found or access denied');
}
logger.info('[Sandbox] Deleting sandbox', { sandboxId });
await deleteSandboxResource(instanceDoc);
} }
/** /**
......
/**
* 沙盒业务层:提供运行态 SandboxClient。
*
* 负责 ensureAvailable、执行命令和文件读写等运行态用例,不承载工具调用或 Skill 部署编排。
*/
import type { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../common/logger';
import { import {
type ExecuteResult, type ExecuteResult,
type ISandbox, type ISandbox,
type ResourceLimits, type ResourceLimits,
type SandboxCreateSpec type SandboxCreateSpec
} from '@fastgpt-sdk/sandbox-adapter'; } from '@fastgpt-sdk/sandbox-adapter';
import { getSessionVolumeConfig, type VolumeManagerResult } from '../volume/service'; import {
import { buildRuntimeSandboxAdapter } from '../provider/adapter'; getSessionVolumeConfig,
import { getConfiguredSandboxProvider } from '../provider/config'; type VolumeManagerResult
import { ensureConnectedSandboxRunning } from '../provider/lifecycle'; } from '../../infrastructure/volume/service';
import { deleteSandboxResource, stopSandboxResource } from './resource'; import { buildRuntimeSandboxAdapter } from '../../infrastructure/provider/adapter';
import { upsertRunningSandboxInstance } from '../instance/repository'; import { getConfiguredSandboxProvider } from '../../infrastructure/provider/config';
import type { SandboxProviderType } from '../type'; import { ensureConnectedSandboxRunning } from '../../infrastructure/provider/lifecycle';
import { deleteSandboxResource, stopSandboxResource } from '../resource';
import {
existsSandboxInstanceBySandboxId,
upsertRunningSandboxInstance
} from '../../infrastructure/instance/repository';
import type { SandboxProviderType } from '../../type';
import { import {
assertSandboxNotArchivedOrBusy, assertSandboxNotArchivedOrBusy,
SandboxArchiveStateError, SandboxArchiveStateError,
restoreArchivedSandboxBeforeUse restoreArchivedSandboxBeforeUse
} from './archive'; } from '../archive';
const logger = getLogger(LogCategories.MODULE.AI.SANDBOX); const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
...@@ -44,6 +55,7 @@ type SandboxClientOptions = { ...@@ -44,6 +55,7 @@ type SandboxClientOptions = {
vmConfig?: VolumeManagerResult | undefined; vmConfig?: VolumeManagerResult | undefined;
createConfig?: SandboxCreateSpec; createConfig?: SandboxCreateSpec;
restoreArchived?: boolean; restoreArchived?: boolean;
failedArchivePolicy?: 'throw' | 'clearAndContinue';
}; };
/** /**
...@@ -151,11 +163,16 @@ export class SandboxClient { ...@@ -151,11 +163,16 @@ export class SandboxClient {
/** /**
* 删除当前运行态 client 对应的资源记录和远端资源。 * 删除当前运行态 client 对应的资源记录和远端资源。
*/ */
async delete() { async delete({ keepArchive = false }: { keepArchive?: boolean } = {}) {
await deleteSandboxResource({ await deleteSandboxResource(
provider: this.providerName, {
sandboxId: this.sandboxId provider: this.providerName,
}); sandboxId: this.sandboxId
},
{
keepArchive
}
);
} }
/** /**
...@@ -187,6 +204,26 @@ const resolveSandboxClientProps = (props: SandboxClientQuery): SandboxClientProp ...@@ -187,6 +204,26 @@ const resolveSandboxClientProps = (props: SandboxClientQuery): SandboxClientProp
}; };
/** /**
* 检查指定运行态 sandbox 是否已有本地实例记录。
*
* 只读本地实例表,不连接 provider,也不触发归档恢复或远端创建。
*/
export async function checkSandboxRuntimeInstanceExists(
props: Pick<SandboxClientQuery, 'sandboxId'>,
opts: { providerName?: SandboxProviderType } = {}
) {
if (!props.sandboxId) {
throw new Error('sandboxId is required');
}
const providerName = opts.providerName ?? getConfiguredSandboxProvider();
return existsSandboxInstanceBySandboxId({
provider: providerName,
sandboxId: props.sandboxId
});
}
/**
* 获取当前业务会话的运行态 sandbox client。 * 获取当前业务会话的运行态 sandbox client。
* *
* 调用方必须按 sourceType/sourceId 计算 sandboxId;这里不再接收 appId 等旧业务字段, * 调用方必须按 sourceType/sourceId 计算 sandboxId;这里不再接收 appId 等旧业务字段,
...@@ -225,7 +262,8 @@ export const getSandboxClient = async ( ...@@ -225,7 +262,8 @@ export const getSandboxClient = async (
: undefined, : undefined,
vmConfig: vmConfig ?? null, vmConfig: vmConfig ?? null,
storage: vmConfig?.storage, storage: vmConfig?.storage,
createConfig: opts.createConfig createConfig: opts.createConfig,
failedArchivePolicy: opts.failedArchivePolicy ?? 'throw'
}); });
} }
vmConfig ??= providerName === 'opensandbox' ? await getSessionVolumeConfig(sandboxId) : undefined; vmConfig ??= providerName === 'opensandbox' ? await getSessionVolumeConfig(sandboxId) : undefined;
......
/**
* 沙盒业务层:执行 sandbox 运行态入口脚本并提供初始化并发锁。
*
* 负责脚本 hash 状态、输出裁剪和 Redis 临界区保护,不处理具体 Skill 版本部署。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../common/logger';
import { serviceEnv } from '../../../../env'; import { serviceEnv } from '../../../../../env';
import { isRedisLeaseError, withRedisLease } from '../../../../common/redis/lock'; import { isRedisLeaseError, withRedisLease } from '../../../../../common/redis/lock';
import { createAgentSandboxInitializingError } from '../error'; import { createAgentSandboxInitializingError } from '../../error';
import type { SandboxPrepareContext, SandboxPrepareStep } from './prepare'; import type { SandboxPrepareContext, SandboxPrepareStep } from './prepare';
import { buildRuntimeHash } from './utils'; import { buildRuntimeHash } from '../../utils';
import { import {
getRuntimeStateValue, getRuntimeStateValue,
readSandboxRuntimeState, readSandboxRuntimeState,
......
/**
* 沙盒业务层:处理运行态输入文件注入和当前目录提示。
*
* 只封装运行态文件写入辅助,不负责 workspace 打包或编辑器文件下载。
*/
import type { FileWriteEntry, ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { FileWriteEntry, ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { SANDBOX_USER_FILES_PATH } from '@fastgpt/global/core/ai/sandbox/constants'; import { SANDBOX_USER_FILES_PATH } from '@fastgpt/global/core/ai/sandbox/constants';
import { pickOutboundAxios } from '../../../../common/api/axios'; import { pickOutboundAxios } from '../../../../../common/api/axios';
import { getSafeSandboxInputFilename } from './utils'; import { getSafeSandboxInputFilename } from '../../utils';
export type SandboxInputFile = { export type SandboxInputFile = {
name: string; name: string;
......
/**
* 沙盒业务层:解析运行中 sandbox 的 HOME 目录。
*
* 只读取当前实例的环境状态,不把 HOME 固化到 provider profile。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../common/logger';
const logger = getLogger(LogCategories.MODULE.AI.AGENT); const logger = getLogger(LogCategories.MODULE.AI.AGENT);
......
import { getSandboxClient, type SandboxClient } from '../service/runtime'; /**
import { createAgentSandboxPermissionDeniedError } from '../error'; * 沙盒业务层:准备 Agent 运行态 sandbox。
import { checkTeamSandboxPermission } from '../../../../support/permission/teamLimit'; *
import { getSandboxRuntimeProfile } from './profile'; * 负责权限校验、sandboxId 计算和运行态 client 创建,不注入具体 Skill 包。
*/
import { getSandboxClient, type SandboxClient } from './client';
import { createAgentSandboxPermissionDeniedError } from '../../error';
import { checkTeamSandboxPermission } from '../../../../../support/permission/teamLimit';
import { getSandboxRuntimeProfile as resolveSandboxRuntimeProfile } from '../../infrastructure/provider/runtimeProfile';
import type {
SandboxRuntimeProfile,
SandboxRuntimeScenario
} from '../../infrastructure/provider/runtimeProfile';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getRunningSandboxId } from './id'; import { getRunningSandboxId } from '../../utils/id';
import type { SandboxProviderType } from '../../type';
export type { SandboxRuntimeProfile, SandboxRuntimeScenario };
export type AgentSandboxRuntimeContext = { export type AgentSandboxRuntimeContext = {
sandboxClient: SandboxClient; sandboxClient: SandboxClient;
...@@ -11,6 +23,16 @@ export type AgentSandboxRuntimeContext = { ...@@ -11,6 +23,16 @@ export type AgentSandboxRuntimeContext = {
}; };
/** /**
* 读取指定 provider 的运行态 profile。
*
* 对外业务只通过 application/interface 读取工作目录和启动配置,provider 选择与默认值
* 仍由 infrastructure/runtimeProfile 维护。
*/
export function getSandboxRuntimeProfile(provider?: SandboxProviderType): SandboxRuntimeProfile {
return resolveSandboxRuntimeProfile(provider);
}
/**
* 准备 Agent 运行需要的 sandbox runtime。 * 准备 Agent 运行需要的 sandbox runtime。
* *
* 该函数只负责 sandbox 维度:权限校验、实例获取和 runtime profile 解析。 * 该函数只负责 sandbox 维度:权限校验、实例获取和 runtime profile 解析。
...@@ -41,13 +63,18 @@ export async function prepareAgentSandboxRuntime({ ...@@ -41,13 +63,18 @@ export async function prepareAgentSandboxRuntime({
userId, userId,
chatId chatId
}); });
const sandboxClient = await getSandboxClient({ const sandboxClient = await getSandboxClient(
sandboxId, {
sourceType, sandboxId,
sourceId, sourceType,
userId: sourceType === ChatSourceTypeEnum.app ? userId : '', sourceId,
chatId userId: sourceType === ChatSourceTypeEnum.app ? userId : '',
}); chatId
},
{
failedArchivePolicy: 'clearAndContinue'
}
);
const runtimeProfile = getSandboxRuntimeProfile(); const runtimeProfile = getSandboxRuntimeProfile();
return { return {
......
/**
* 沙盒业务层:为运行态 sandbox 准备包管理器镜像源。
*
* 负责根据环境配置写入 npm/pip/uv 等镜像文件,不处理 Skill 包部署。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../common/logger';
import { serviceEnv } from '../../../../env'; import { serviceEnv } from '../../../../../env';
import { buildRuntimeHash, joinSandboxPath } from './utils'; import { buildRuntimeHash, joinSandboxPath } from '../../utils';
import { import {
getRuntimeStateValue, getRuntimeStateValue,
readSandboxRuntimeState, readSandboxRuntimeState,
......
/**
* 沙盒业务层:定义运行态 sandbox 初始化步骤编排器。
*
* 只组合工作目录、输入文件、镜像源等 prepare step,不直接操作 Mongo 或 provider 配置。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { import {
......
/**
* 沙盒业务层:同步内置 Skill 文件到运行态 sandbox。
*
* 只管理 sandbox HOME 下的内置 Skill 目录,不进入用户 workspace 或发布包。
*/
import type { FileWriteEntry, ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { FileWriteEntry, ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import type { import type {
BuiltinSkillSource, BuiltinSkillSource,
BuiltinSkillSourceFile BuiltinSkillSourceFile
} from '@fastgpt/global/core/ai/skill/runtime/builtin'; } from '@fastgpt/global/core/ai/skill/runtime/builtin';
import { getSandboxBuiltinSkillsRootPath } from '../../sandbox/runtime/profile/utils'; import { getSandboxBuiltinSkillsRootPath } from '../../../infrastructure/provider/runtimeProfile/utils';
import { buildRuntimeHash, joinSandboxPath } from '../../sandbox/runtime/utils'; import { buildRuntimeHash, joinSandboxPath } from '../../../utils';
import { import {
getRuntimeStateValue, getRuntimeStateValue,
readSandboxRuntimeState, readSandboxRuntimeState,
setRuntimeStateValue, setRuntimeStateValue,
writeSandboxRuntimeState writeSandboxRuntimeState
} from '../../sandbox/runtime/state'; } from '../state';
const BUILTIN_SKILL_STATE_HASH_PREFIX = 'builtinSkill:'; const BUILTIN_SKILL_STATE_HASH_PREFIX = 'builtinSkill:';
......
/**
* 沙盒业务层:在运行态 sandbox 内注入和扫描已发布 Skill。
*
* 负责读取 Skill 版本包并写入现有 sandbox,不创建或清理 sandbox 实例。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { MongoAgentSkills } from '../model/schema'; import { MongoAgentSkills } from '../../../../skill/model/schema';
import { MongoAgentSkillsVersion } from '../version/schema'; import { MongoAgentSkillsVersion } from '../../../../skill/version/schema';
import { downloadSkillPackage } from '../package'; import { downloadSkillPackage } from '../../../../skill/package';
import { parseSkillMarkdown, getSkillsRootPath } from '../utils'; import { parseSkillMarkdown } from '../../../../skill/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../common/logger';
import type { DeployedSkillInfo, DeployedSkillVersion } from './types'; import type { DeployedSkillInfo, DeployedSkillVersion } from './types';
import { serviceEnv } from '../../../../env'; import { getAgentSandboxSkillMaxBytes } from '../../../interface/config';
import { joinSandboxPath } from '../../sandbox/runtime/utils'; import { joinSandboxPath } from '../../../utils';
import { authSkillByTmbId } from '../../../../support/permission/skill/auth'; import { authSkillByTmbId } from '../../../../../../support/permission/skill/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/skill'; import { SkillErrEnum } from '@fastgpt/global/common/error/code/skill';
...@@ -18,6 +23,9 @@ const logger = getLogger(LogCategories.MODULE.AI.AGENT); ...@@ -18,6 +23,9 @@ const logger = getLogger(LogCategories.MODULE.AI.AGENT);
const parseCommandOutputNulls = (stdout: string) => stdout.split('\0').filter(Boolean); const parseCommandOutputNulls = (stdout: string) => stdout.split('\0').filter(Boolean);
const SKILL_INFO_SCAN_PRUNE_DIRS = ['node_modules', '.venv', 'venv']; const SKILL_INFO_SCAN_PRUNE_DIRS = ['node_modules', '.venv', 'venv'];
const getRuntimeSkillsRootPath = (workDirectory: string): string =>
joinSandboxPath(workDirectory, 'projects');
const buildSkillInfoFindCommand = (dir: string) => { const buildSkillInfoFindCommand = (dir: string) => {
const pruneClause = SKILL_INFO_SCAN_PRUNE_DIRS.map((name) => `-name ${shellQuote(name)}`).join( const pruneClause = SKILL_INFO_SCAN_PRUNE_DIRS.map((name) => `-name ${shellQuote(name)}`).join(
' -o ' ' -o '
...@@ -136,7 +144,7 @@ export const injectAgentSkillFilesToSandbox = async ({ ...@@ -136,7 +144,7 @@ export const injectAgentSkillFilesToSandbox = async ({
tmbId: string; tmbId: string;
workDirectory: string; workDirectory: string;
}): Promise<DeployedSkillVersion[]> => { }): Promise<DeployedSkillVersion[]> => {
const skillsRootPath = getSkillsRootPath(workDirectory); const skillsRootPath = getRuntimeSkillsRootPath(workDirectory);
const prepareSkillsRootResult = await sandbox.execute(`mkdir -p ${shellQuote(skillsRootPath)}`); const prepareSkillsRootResult = await sandbox.execute(`mkdir -p ${shellQuote(skillsRootPath)}`);
if (prepareSkillsRootResult.exitCode !== 0) { if (prepareSkillsRootResult.exitCode !== 0) {
throw new Error(`Failed to prepare skill directory: ${prepareSkillsRootResult.stderr}`); throw new Error(`Failed to prepare skill directory: ${prepareSkillsRootResult.stderr}`);
...@@ -260,7 +268,7 @@ export const injectAgentSkillFilesToSandbox = async ({ ...@@ -260,7 +268,7 @@ export const injectAgentSkillFilesToSandbox = async ({
({ targetDir }) => !deployableTargetDirs.has(targetDir) ({ targetDir }) => !deployableTargetDirs.has(targetDir)
); );
const maxPackageBytes = serviceEnv.AGENT_SANDBOX_SKILL_MAX_SIZE * 1024 * 1024; const maxPackageBytes = getAgentSandboxSkillMaxBytes();
const results = await Promise.all( const results = await Promise.all(
missingSkills.map(async ({ skill, version, versionId, targetDir }) => { missingSkills.map(async ({ skill, version, versionId, targetDir }) => {
try { try {
......
/**
* 沙盒业务层:执行已部署 Skill 版本目录内的 entrypoint。
*
* 只维护 sandbox runtime state 中的执行标记,不创建版本或修改 Skill 数据。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../common/logger';
import type { DeployedSkillVersion } from './types'; import type { DeployedSkillVersion } from './types';
import { import { buildLimitedOutputShellCommand, executeEntrypointCommand } from '../entrypoint';
buildLimitedOutputShellCommand, import { joinSandboxPath } from '../../../utils';
executeEntrypointCommand
} from '../../sandbox/runtime/entrypoint';
import { joinSandboxPath } from '../../sandbox/runtime/utils';
import { import {
getRuntimeStateValue, getRuntimeStateValue,
readSandboxRuntimeState, readSandboxRuntimeState,
setRuntimeStateValue, setRuntimeStateValue,
writeSandboxRuntimeState writeSandboxRuntimeState
} from '../../sandbox/runtime/state'; } from '../state';
const logger = getLogger(LogCategories.MODULE.AI.AGENT); const logger = getLogger(LogCategories.MODULE.AI.AGENT);
......
/**
* 沙盒业务层:聚合运行态 Skill 部署、扫描和 entrypoint 能力。
*
* 仅供 sandbox interface/runtime 对外导出,不作为外部业务直接引用入口。
*/
export type { DeployedSkillInfo, DeployedSkillVersion } from './types'; export type { DeployedSkillInfo, DeployedSkillVersion } from './types';
export { getAgentSkillInfos, injectAgentSkillFilesToSandbox } from './core'; export { getAgentSkillInfos, injectAgentSkillFilesToSandbox } from './core';
export { getBuiltinSkillsRootPath, syncBuiltinSkillsToSandbox } from './builtin'; export { getBuiltinSkillsRootPath, syncBuiltinSkillsToSandbox } from './builtin';
......
/**
* 沙盒业务层:定义 Skill 版本包下载和部署的 prepare step。
*
* 只服务 sandbox runtime 初始化链路,不负责 Skill 版本创建或权限校验。
*/
import type { SandboxStatusPhase } from '@fastgpt/global/core/chat/type'; import type { SandboxStatusPhase } from '@fastgpt/global/core/chat/type';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import type { SandboxPrepareContext, SandboxPrepareStep } from '../../sandbox/runtime/prepare'; import type { SandboxPrepareContext, SandboxPrepareStep } from '../prepare';
import { joinSandboxPath } from '../../sandbox/runtime/utils'; import { joinSandboxPath } from '../../../utils';
import { serviceEnv } from '../../../../env'; import { getAgentSandboxSkillMaxBytes } from '../../../interface/config';
import { DEFAULT_GITIGNORE_CONTENT, downloadSkillPackage } from '../package'; import { DEFAULT_GITIGNORE_CONTENT, downloadSkillPackage } from '../../../../skill/package';
export type SkillPackagePrepareContext = SandboxPrepareContext & { export type SkillPackagePrepareContext = SandboxPrepareContext & {
packageBuffer?: Buffer; packageBuffer?: Buffer;
...@@ -66,7 +71,7 @@ export const deployDownloadedSkillPackage = ...@@ -66,7 +71,7 @@ export const deployDownloadedSkillPackage =
} }
const zipPath = joinSandboxPath(skillsRootPath, 'package.zip'); const zipPath = joinSandboxPath(skillsRootPath, 'package.zip');
const maxPackageBytes = serviceEnv.AGENT_SANDBOX_SKILL_MAX_SIZE * 1024 * 1024; const maxPackageBytes = getAgentSandboxSkillMaxBytes();
const writeResults = await context.sandbox.writeFiles([ const writeResults = await context.sandbox.writeFiles([
{ {
......
// Info about a single SKILL.md entry available to the agent. /**
* 沙盒业务层:声明运行态 Skill 扫描和部署结果类型。
*
* 只定义 sandbox 内 Skill 元数据,不访问数据库或 provider。
*/
export type DeployedSkillInfo = { export type DeployedSkillInfo = {
id: string; id: string;
name: string; name: string;
......
/**
* 沙盒业务层:读写 sandbox HOME 下的 FastGPT runtime 状态文件。
*
* 只记录轻量执行标记,不承担数据库状态或 provider 生命周期管理。
*/
import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import { shellQuote } from '@fastgpt/global/common/string/utils'; import { shellQuote } from '@fastgpt/global/common/string/utils';
import { getLogger, LogCategories } from '../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../common/logger';
import { resolveSandboxHome } from './home'; import { resolveSandboxHome } from './home';
import { joinSandboxPath } from './utils'; import { joinSandboxPath } from '../../utils';
const logger = getLogger(LogCategories.MODULE.AI.AGENT); const logger = getLogger(LogCategories.MODULE.AI.AGENT);
......
import { mongoSessionRun } from '../../../../common/mongo/sessionRun'; /**
import { Types } from '../../../../common/mongo'; * 沙盒业务层:从 Skill Edit sandbox 保存并发布新的技能版本。
import { getLogger, LogCategories } from '../../../../common/logger'; *
import { updateCurrentVersion } from '../manage'; * API 层负责鉴权和请求校验;本文件负责定位运行态 edit-debug sandbox、打包工作区、
import { removeSkillPackageTTL, uploadSkillPackage } from '../package'; * 上传版本包、创建版本记录,并同步运行实例的版本元数据。
import { packageSkillInSandbox } from './sandbox'; */
import { getEditDebugSandboxId } from './config'; import { mongoSessionRun } from '../../../../../common/mongo/sessionRun';
import { createVersion } from '../version'; import { Types } from '../../../../../common/mongo';
import { getSandboxRuntimeProfile } from '../../sandbox/runtime/profile'; import { getLogger, LogCategories } from '../../../../../common/logger';
import { getSandboxProviderConfig } from '../../sandbox/provider/config'; import { updateCurrentVersion } from '../../../skill/manage';
import { removeSkillPackageTTL, uploadSkillPackage } from '../../../skill/package';
import { packageSkillInSandbox } from './runtime';
import { getEditDebugSandboxId } from '../../../skill/edit/config';
import { createVersion } from '../../../skill/version';
import { getSandboxRuntimeProfile } from '../../infrastructure/provider/runtimeProfile';
import { getSandboxProviderConfig } from '../../infrastructure/provider/config';
import { import {
findSandboxInstanceBySandboxId, findSandboxInstanceBySandboxIdAndSource,
updateSandboxInstanceRecordBySandboxId updateSandboxInstanceRecordBySandboxId
} from '../../sandbox/instance/repository'; } from '../../infrastructure/instance/repository';
import { MongoAgentSkills } from '../model/schema'; import { MongoAgentSkills } from '../../../skill/model/schema';
import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants'; import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { SkillErrEnum } from '@fastgpt/global/common/error/code/skill'; import { SkillErrEnum } from '@fastgpt/global/common/error/code/skill';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
...@@ -42,14 +48,19 @@ export async function saveDeploySkillFromSandbox({ ...@@ -42,14 +48,19 @@ export async function saveDeploySkillFromSandbox({
versionName versionName
}: SaveDeploySkillFromSandboxParams): Promise<SaveDeploySkillResponse> { }: SaveDeploySkillFromSandboxParams): Promise<SaveDeploySkillResponse> {
const providerConfig = getSandboxProviderConfig(); const providerConfig = getSandboxProviderConfig();
const sandboxInfo = await findSandboxInstanceBySandboxId({ const sandboxInfo = await findSandboxInstanceBySandboxIdAndSource({
provider: providerConfig.provider, provider: providerConfig.provider,
sandboxId: getEditDebugSandboxId(skillId), sandboxId: getEditDebugSandboxId(skillId),
status: SandboxStatusEnum.running, sourceType: ChatSourceTypeEnum.skillEdit,
type: SandboxTypeEnum.editDebug sourceId: skillId,
status: SandboxStatusEnum.running
}); });
if (!sandboxInfo || sandboxInfo.status !== SandboxStatusEnum.running) { if (
!sandboxInfo ||
sandboxInfo.status !== SandboxStatusEnum.running ||
sandboxInfo.metadata?.teamId !== teamId
) {
return Promise.reject(new UserError('Edit sandbox not found or not running')); return Promise.reject(new UserError('Edit sandbox not found or not running'));
} }
...@@ -123,7 +134,7 @@ export async function saveDeploySkillFromSandbox({ ...@@ -123,7 +134,7 @@ export async function saveDeploySkillFromSandbox({
}); });
// 发布新版本成功后,更新运行中沙盒实例的 versionId,保证后续版本切换时能够正确执行版本比对和容器重建 // 发布新版本成功后,更新运行中沙盒实例的 versionId,保证后续版本切换时能够正确执行版本比对和容器重建
await updateSandboxInstanceRecordBySandboxId({ const updatedSandboxInfo = await updateSandboxInstanceRecordBySandboxId({
provider: providerConfig.provider, provider: providerConfig.provider,
sandboxId: sandboxInfo.sandboxId, sandboxId: sandboxInfo.sandboxId,
sourceType: ChatSourceTypeEnum.skillEdit, sourceType: ChatSourceTypeEnum.skillEdit,
...@@ -131,14 +142,25 @@ export async function saveDeploySkillFromSandbox({ ...@@ -131,14 +142,25 @@ export async function saveDeploySkillFromSandbox({
metadata: { metadata: {
...(sandboxInfo.metadata || {}), ...(sandboxInfo.metadata || {}),
versionId versionId
} },
touchActive: true
}).catch((err) => { }).catch((err) => {
logger.error('[Sandbox] Failed to update sandbox versionId after deploy', { logger.error('[Sandbox] Failed to update sandbox versionId after deploy', {
sandboxId: sandboxInfo.sandboxId, sandboxId: sandboxInfo.sandboxId,
versionId, versionId,
error: err error: err
}); });
return null;
}); });
if (!updatedSandboxInfo) {
logger.warn(
'[Sandbox] Skip updating sandbox versionId after deploy because sandbox state changed',
{
sandboxId: sandboxInfo.sandboxId,
versionId
}
);
}
return deployResult; return deployResult;
} }
/**
* 沙盒业务层:定义 sandbox 文件编辑工具。
*
* 只描述工具参数和执行逻辑,运行态准备由 toolCall 编排层负责。
*/
import z from 'zod'; import z from 'zod';
import { defineTool } from './type'; import { defineTool } from './type';
import { SANDBOX_EDIT_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_EDIT_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
......
/**
* 沙盒业务层:定义 sandbox 文件临时下载链接工具。
*
* 只负责把 sandbox 文件流转存到 chat S3 临时对象,不处理运行态生命周期。
*/
import z from 'zod'; import z from 'zod';
import path from 'path'; import path from 'path';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { addHours } from 'date-fns'; import { addHours } from 'date-fns';
import { defineTool } from './type'; import { defineTool } from './type';
import { getS3ChatSource } from '../../../../common/s3/sources/chat'; import { getS3ChatSource } from '../../../../../common/s3/sources/chat';
import { jwtSignS3ObjectKey } from '../../../../common/s3/utils'; import { jwtSignS3ObjectKey } from '../../../../../common/s3/utils';
import { SANDBOX_GET_FILE_URL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_GET_FILE_URL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
const SandboxGetFileUrlToolSchema = z.object({ const SandboxGetFileUrlToolSchema = z.object({
......
/**
* 沙盒业务层:编排 sandbox tool 调用。
*
* 负责工具参数校验、运行态 sandbox 准备和工具执行,不直接暴露给外部业务调用。
*/
import { sandboxToolMap } from '@fastgpt/global/core/ai/sandbox/tools'; import { sandboxToolMap } from '@fastgpt/global/core/ai/sandbox/tools';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { localeType } from '@fastgpt/global/common/i18n/type';
...@@ -8,13 +13,13 @@ import { toolMap as readFileToolMap } from './readFile.tool'; ...@@ -8,13 +13,13 @@ import { toolMap as readFileToolMap } from './readFile.tool';
import { toolMap as searchToolMap } from './search.tool'; import { toolMap as searchToolMap } from './search.tool';
import { toolMap as shellToolMap } from './shell.tool'; import { toolMap as shellToolMap } from './shell.tool';
import { toolMap as writeFileToolMap } from './writeFile.tool'; import { toolMap as writeFileToolMap } from './writeFile.tool';
import { getSandboxClient, type SandboxClient } from '../service/runtime'; import { getSandboxClient, type SandboxClient } from '../runtime/client';
import { parseJsonArgs } from '../../utils'; import { parseJsonArgs } from '../../../utils';
import { writeUrlFilesToSandbox } from '../service/file'; import { writeUrlFilesToSandbox } from '../file';
import { getSandboxRuntimeProfile } from '../runtime/profile'; import { getSandboxRuntimeProfile } from '../../infrastructure/provider/runtimeProfile';
import { preparePackageMirrors, prepareSandbox } from '../runtime/prepare'; import { preparePackageMirrors, prepareSandbox } from '../runtime/prepare';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getRunningSandboxId } from '../runtime/id'; import { getRunningSandboxId } from '../../utils/id';
const ToolMap = { const ToolMap = {
...editFileToolMap, ...editFileToolMap,
...@@ -87,13 +92,18 @@ export const runSandboxTools = async ({ ...@@ -87,13 +92,18 @@ export const runSandboxTools = async ({
}); });
const instance = const instance =
sandboxClient ?? sandboxClient ??
(await getSandboxClient({ (await getSandboxClient(
sandboxId, {
sourceType, sandboxId,
sourceId, sourceType,
userId: sourceType === ChatSourceTypeEnum.app ? userId : '', sourceId,
chatId userId: sourceType === ChatSourceTypeEnum.app ? userId : '',
})); chatId
},
{
failedArchivePolicy: 'clearAndContinue'
}
));
const result = await tool.execute({ const result = await tool.execute({
sourceType, sourceType,
sourceId, sourceId,
...@@ -136,13 +146,18 @@ export const prepareSandboxToolRuntime = async ({ ...@@ -136,13 +146,18 @@ export const prepareSandboxToolRuntime = async ({
userId, userId,
chatId chatId
}); });
const instance = await getSandboxClient({ const instance = await getSandboxClient(
sandboxId, {
sourceType, sandboxId,
sourceId, sourceType,
userId: sourceType === ChatSourceTypeEnum.app ? userId : '', sourceId,
chatId userId: sourceType === ChatSourceTypeEnum.app ? userId : '',
}); chatId
},
{
failedArchivePolicy: 'clearAndContinue'
}
);
const runtimeProfile = getSandboxRuntimeProfile(); const runtimeProfile = getSandboxRuntimeProfile();
await prepareSandbox( await prepareSandbox(
{ {
......
/**
* 沙盒业务层:定义 sandbox 文件读取工具。
*
* 只描述工具参数和执行逻辑,运行态准备由 toolCall 编排层负责。
*/
import z from 'zod'; import z from 'zod';
import { defineTool } from './type'; import { defineTool } from './type';
import { SANDBOX_READ_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_READ_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
......
/**
* 沙盒业务层:定义 sandbox 工作区搜索工具。
*
* 只描述工具参数和执行逻辑,运行态准备由 toolCall 编排层负责。
*/
import z from 'zod'; import z from 'zod';
import { defineTool } from './type'; import { defineTool } from './type';
import { SANDBOX_SEARCH_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_SEARCH_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
......
/**
* 沙盒业务层:定义 sandbox shell 执行工具。
*
* 只描述工具参数和执行逻辑,运行态准备由 toolCall 编排层负责。
*/
import z from 'zod'; import z from 'zod';
import { defineTool } from './type'; import { defineTool } from './type';
import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
......
/**
* 沙盒业务层:定义 sandbox tool 的内部注册结构。
*
* 只描述工具 schema 与执行函数类型,不负责运行态准备或对外接口导出。
*/
import type { z } from 'zod'; import type { z } from 'zod';
import type { SandboxClient } from '../service/runtime'; import type { SandboxClient } from '../runtime/client';
import type { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
type ToolExecuteContext<P> = { type ToolExecuteContext<P> = {
......
/**
* 沙盒业务层:定义 sandbox 文件写入工具。
*
* 只描述工具参数和执行逻辑,运行态准备由 toolCall 编排层负责。
*/
import z from 'zod'; import z from 'zod';
import { defineTool } from './type'; import { defineTool } from './type';
import { SANDBOX_WRITE_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_WRITE_FILE_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
......
/**
* 沙盒模块错误工厂。
*
* 统一把 sandbox 业务错误转换成 UserError,不承载 provider 或数据库逻辑。
*/
import { SandboxErrEnum } from '@fastgpt/global/common/error/code/sandbox'; import { SandboxErrEnum } from '@fastgpt/global/common/error/code/sandbox';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
......
import { connectionMongo, getMongoModel } from '../../../../common/mongo'; /**
* 沙盒原子层:定义 SandboxInstance Mongo schema。
*
* 只描述本地实例记录结构,不编排 provider、归档或运行态流程。
*/
import { connectionMongo, getMongoModel } from '../../../../../common/mongo';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import type { SandboxInstanceSchemaType } from '../type'; import type { SandboxInstanceSchemaType } from '../../type';
import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants'; import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { SandboxLimitSchema, SandboxProviderSchema } from '../type'; import { SandboxLimitSchema, SandboxProviderSchema } from '../../type';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
/** /**
...@@ -37,6 +42,7 @@ const SandboxInstanceSchema = new Schema({ ...@@ -37,6 +42,7 @@ const SandboxInstanceSchema = new Schema({
}, },
userId: String, userId: String,
chatId: String, chatId: String,
// @deprecated sandbox 归属统一使用 sourceType/sourceId;保留字段仅为历史数据迁移与兼容读取。
type: { type: {
type: String, type: String,
enum: Object.values(SandboxTypeEnum) enum: Object.values(SandboxTypeEnum)
...@@ -69,38 +75,12 @@ const SandboxInstanceSchema = new Schema({ ...@@ -69,38 +75,12 @@ const SandboxInstanceSchema = new Schema({
} }
}); });
// @deprecated 旧 appId 维度索引仅用于迁移窗口和历史数据观察,新业务查询必须使用 sourceType/sourceId。
SandboxInstanceSchema.index(
{ provider: 1, appId: 1, userId: 1, chatId: 1 },
{
unique: true,
partialFilterExpression: {
// Keep the index compatible with Mongo-compatible backends that do not
// support `$ne: null` inside partial indexes.
appId: { $exists: true },
userId: { $exists: true },
chatId: { $exists: true }
}
}
);
SandboxInstanceSchema.index({ status: 1, lastActiveAt: 1, 'metadata.archive.state': 1 });
SandboxInstanceSchema.index({ provider: 1, sandboxId: 1 }, { unique: true }); SandboxInstanceSchema.index({ provider: 1, sandboxId: 1 }, { unique: true });
// @deprecated 旧 appId 维度索引仅用于迁移窗口和历史数据观察,新业务查询必须使用 sourceType/sourceId。
SandboxInstanceSchema.index(
{ appId: 1, chatId: 1 },
{
unique: true,
partialFilterExpression: {
appId: { $exists: true },
chatId: { $exists: true },
type: { $exists: true }
}
}
);
// @deprecated 旧 Skill Edit 归属索引仅用于迁移窗口,新业务不得写入或查询 metadata.skillId。
SandboxInstanceSchema.index({ 'metadata.skillId': 1 });
SandboxInstanceSchema.index({ type: 1, chatId: 1 });
SandboxInstanceSchema.index({ sourceType: 1, sourceId: 1, chatId: 1 }); SandboxInstanceSchema.index({ sourceType: 1, sourceId: 1, chatId: 1 });
SandboxInstanceSchema.index({ sourceType: 1, status: 1, provider: 1, 'metadata.archive.state': 1 });
SandboxInstanceSchema.index({ status: 1, lastActiveAt: 1, 'metadata.archive.state': 1 });
SandboxInstanceSchema.index({ 'metadata.archive.state': 1, 'metadata.archive.startedAt': 1 });
SandboxInstanceSchema.index({ 'metadata.archive.state': 1, 'metadata.archive.deleteStartedAt': 1 });
/** /**
* sandbox 实例 Mongo model。 * sandbox 实例 Mongo model。
......
/**
* 沙盒原子层:构造不同 provider 的 SDK adapter。
*
* 只做 provider 配置到 SDK 入参的转换,不编排运行态、归档或数据库状态。
*/
import { import {
createSandbox, createSandbox,
type ISandbox, type ISandbox,
...@@ -5,7 +10,7 @@ import { ...@@ -5,7 +10,7 @@ import {
type SandboxCreateSpec type SandboxCreateSpec
} from '@fastgpt-sdk/sandbox-adapter'; } from '@fastgpt-sdk/sandbox-adapter';
import { getSandboxAdapterConfig, type SandboxProviderConfig } from './config'; import { getSandboxAdapterConfig, type SandboxProviderConfig } from './config';
import type { SandboxProviderType } from '../type'; import type { SandboxProviderType } from '../../type';
import type { VolumeManagerResult } from '../volume/service'; import type { VolumeManagerResult } from '../volume/service';
function assertNever(value: never): never { function assertNever(value: never): never {
......
import { serviceEnv } from '../../../../env'; /**
* 沙盒原子层:解析 provider 连接配置和创建配置。
*
* 只负责把环境变量转换成 adapter 可用的配置,不执行远端生命周期动作。
*/
import { serviceEnv } from '../../../../../env';
import { getAgentSandboxMaxFileBytes } from '../../interface/config';
import type { SandboxCreateSpec, SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter'; import type { SandboxCreateSpec, SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
import type { VolumeManagerResult } from '../volume/service'; import type { VolumeManagerResult } from '../volume/service';
import { getSandboxRuntimeProfile, buildBaseSandboxRuntimeEnv } from '../runtime/profile'; import { getSandboxRuntimeProfile, buildBaseSandboxRuntimeEnv } from './runtimeProfile';
type SandboxRuntime = 'kubernetes' | 'docker'; type SandboxRuntime = 'kubernetes' | 'docker';
...@@ -115,7 +121,7 @@ export function getSandboxAdapterConfig({ ...@@ -115,7 +121,7 @@ export function getSandboxAdapterConfig({
sessionId, sessionId,
workDirectory: profile.workDirectory, workDirectory: profile.workDirectory,
ideAgentBindAddr: serviceEnv.IDE_AGENT_BIND_ADDR, ideAgentBindAddr: serviceEnv.IDE_AGENT_BIND_ADDR,
ideAgentMaxFileBytes: serviceEnv.AGENT_SANDBOX_MAX_FILE_SIZE * 1024 * 1024 ideAgentMaxFileBytes: getAgentSandboxMaxFileBytes()
}) })
: undefined; : undefined;
......
import { getLogger, LogCategories } from '../../../../common/logger'; /**
* 沙盒原子层:封装 provider 连接、断开和运行态探测。
*
* 这里只操作远端 sandbox adapter,不维护 FastGPT 本地实例状态。
*/
import { getLogger, LogCategories } from '../../../../../common/logger';
import { import {
type ISandbox, type ISandbox,
type OpenSandboxAdapter, type OpenSandboxAdapter,
......
/**
* 沙盒原子层:定义 E2B 的运行态 profile。
*
* 只负责 E2B createConfig 合并语义,不连接远端实例。
*/
import type { SandboxRuntimeProfile } from './types'; import type { SandboxRuntimeProfile } from './types';
import { getSandboxSkillsRootPath, mergeStringRecord, mergeUnknownRecord } from './utils'; import { getSandboxSkillsRootPath, mergeStringRecord, mergeUnknownRecord } from './utils';
......
/**
* 沙盒原子层:选择当前 provider 的运行态 profile。
*
* 只维护 provider 到 profile 的路由,不执行 sandbox 生命周期或业务状态判断。
*/
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter'; import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
import { serviceEnv } from '../../../../../env'; import { serviceEnv } from '../../../../../../env';
import { buildE2BRuntimeProfile } from './e2b'; import { buildE2BRuntimeProfile } from './e2b';
import { buildOpenSandboxRuntimeProfile } from './opensandbox'; import { buildOpenSandboxRuntimeProfile } from './opensandbox';
import { buildSealosRuntimeProfile } from './sealosdevbox'; import { buildSealosRuntimeProfile } from './sealosdevbox';
......
import { serviceEnv } from '../../../../../env'; /**
* 沙盒原子层:定义 OpenSandbox 的运行态 profile。
*
* 只负责 OpenSandbox createConfig 映射,不连接远端实例。
*/
import { serviceEnv } from '../../../../../../env';
import type { SandboxRuntimeProfile } from './types'; import type { SandboxRuntimeProfile } from './types';
import { import {
getSandboxSkillsRootPath, getSandboxSkillsRootPath,
......
import { serviceEnv } from '../../../../../env'; /**
* 沙盒原子层:定义 Sealos Devbox 的运行态 profile。
*
* 只负责 Devbox createConfig 映射,不连接远端实例。
*/
import { serviceEnv } from '../../../../../../env';
import type { SandboxRuntimeProfile } from './types'; import type { SandboxRuntimeProfile } from './types';
import { getSandboxSkillsRootPath, mergeStringRecord, mergeUnknownRecord } from './utils'; import { getSandboxSkillsRootPath, mergeStringRecord, mergeUnknownRecord } from './utils';
import { parseImageSpec } from '@fastgpt-sdk/sandbox-adapter'; import { parseImageSpec } from '@fastgpt-sdk/sandbox-adapter';
......
/**
* 沙盒原子层:声明 provider runtime profile 的输入输出契约。
*
* 只定义类型,不执行环境读取、远端调用或业务状态判断。
*/
import type { SandboxImageConfigType } from '@fastgpt/global/core/ai/sandbox/type'; import type { SandboxImageConfigType } from '@fastgpt/global/core/ai/sandbox/type';
import type { SandboxCreateSpec, SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter'; import type { SandboxCreateSpec, SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
import type { VolumeManagerResult } from '../../volume/service'; import type { VolumeManagerResult } from '../../volume/service';
......
This diff is collapsed. Click to expand it.
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