Commit 1562b4b7 by DigHuang Committed by GitHub

refactor(sandbox): enforce sandbox validation and sync parallel variables (#7220)

* refactor(sandbox): enforce strict sandbox env validation at startup; fix loop end i18n

* fix(workflow): sync parallel task variables back to parent state on success

* chat schema

* schema

* test

* fix: test

---------

Co-authored-by: archer <545436317@qq.com>
parent be34feb2
...@@ -324,7 +324,7 @@ services: ...@@ -324,7 +324,7 @@ services:
<<: [*x-no-proxy-config] <<: [*x-no-proxy-config]
PORT: 3000 PORT: 3000
VM_RUNTIME: docker VM_RUNTIME: docker
VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_VOLUME_MANAGER_TOKEN VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN
VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀 VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀
VM_LOG_LEVEL: info VM_LOG_LEVEL: info
healthcheck: healthcheck:
......
...@@ -324,7 +324,7 @@ services: ...@@ -324,7 +324,7 @@ services:
<<: [*x-no-proxy-config] <<: [*x-no-proxy-config]
PORT: 3000 PORT: 3000
VM_RUNTIME: docker VM_RUNTIME: docker
VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_VOLUME_MANAGER_TOKEN VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN
VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀 VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀
VM_LOG_LEVEL: info VM_LOG_LEVEL: info
healthcheck: healthcheck:
......
...@@ -324,7 +324,7 @@ services: ...@@ -324,7 +324,7 @@ services:
<<: [*x-no-proxy-config] <<: [*x-no-proxy-config]
PORT: 3000 PORT: 3000
VM_RUNTIME: docker VM_RUNTIME: docker
VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_VOLUME_MANAGER_TOKEN VM_AUTH_TOKEN: *x-volume-manager-auth-token # 对应 AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN
VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀 VM_VOLUME_NAME_PREFIX: fastgpt-session # volume 名称前缀
VM_LOG_LEVEL: info VM_LOG_LEVEL: info
healthcheck: healthcheck:
......
...@@ -5,30 +5,28 @@ description: 'FastGPT V4.15.0-beta7 Release Notes' ...@@ -5,30 +5,28 @@ description: 'FastGPT V4.15.0-beta7 Release Notes'
## 📦 Upgrade Guide ## 📦 Upgrade Guide
This is the final beta release before the 4.15.0 stable release. If you deployed any 4.15.0-beta version, upgrade to this version first, complete all upgrade steps introduced during the beta period, and then update all images to the stable release. See [4.15.0](./41500.mdx) for the stable release images. ### 1. Open-source config.json Configuration Removed
### 1. Remove Open-Source `config.json` Configuration The `config.json` configuration file has been removed. All configuration is now provided through environment variables:
The `config.json` configuration file has been removed. All settings now use environment variables:
```dotenv ```dotenv
# MCP Server proxy endpoint, used by the MCP usage page to build the SSE URL (do not include a trailing /) # MCP Server proxy endpoint, used on the MCP usage page to build the SSE URL (do not include a trailing /)
SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003 SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003
# ==================== Enhanced PDF Parsing (Optional) ==================== # ==================== Enhanced PDF Parsing (Optional) ====================
# Custom PDF parsing service URL # Custom PDF parsing service endpoint
# CUSTOM_PDF_PARSE_URL= # CUSTOM_PDF_PARSE_URL=
# Custom PDF parsing service key # Custom PDF parsing service key
# CUSTOM_PDF_PARSE_KEY= # CUSTOM_PDF_PARSE_KEY=
# Doc2x PDF parsing service key # Doc2x PDF parsing service key
# DOC2X_KEY= # DOC2X_KEY=
# IntSig TextIn service App ID # TextIn service App ID
# TEXTIN_APP_ID= # TEXTIN_APP_ID=
# IntSig TextIn service Secret Code # TextIn service Secret Code
# TEXTIN_SECRET_CODE= # TEXTIN_SECRET_CODE=
# Vector search hnsw ef_search parameter. Applies only to PG / OB / OpenGauss # hnsw ef_search parameter for vector retrieval. Only applies to PG / OB / OpenGauss.
HNSW_EF_SEARCH=100 HNSW_EF_SEARCH=100
# Maximum scanned tuple count for vector search. Applies only to PG # Maximum scanned rows for vector retrieval. Only applies to PG.
HNSW_MAX_SCAN_TUPLES=100000 HNSW_MAX_SCAN_TUPLES=100000
# ==================== Knowledge Base Processing Concurrency Control ==================== # ==================== Knowledge Base Processing Concurrency Control ====================
...@@ -42,20 +40,28 @@ QA_MAX_PROCESS=10 ...@@ -42,20 +40,28 @@ QA_MAX_PROCESS=10
VLM_MAX_PROCESS=10 VLM_MAX_PROCESS=10
``` ```
### 2. Add the SSE MCP Endpoint for the Commercial Edition ### 2. Commercial Edition: Add the SSE MCP Endpoint
This configuration has been removed from the admin panel. Add the following environment variable to the `fastgpt` service: This setting has been removed from admin and must now be added as an environment variable to the `fastgpt` service:
```dotenv ```dotenv
SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003 SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003
``` ```
### 3. Update Images ### 3. OpenSandbox Variable Updates
The OpenSandbox Volume Manager configuration is now required, and the environment variables have been renamed to:
- Update the fastgpt-app (FastGPT main service) image tag to v4.15.0-beta7. ```dotenv
- Update the fastgpt-pro (FastGPT Commercial Edition) image tag to v4.15.0-beta7. AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL=http://localhost:3005
AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN=vmtoken
```
### 4. Run the Workflow V1 to V2 Migration (Optional) ### 4. Update Images
See the [4.15.0 stable image tags](./41500.mdx) and update all images to the stable release.
### 5. Run the Workflow V1 to V2 Migration (Optional)
Only users who have deployed a FastGPT version earlier than `<4.8` need to run this step. Only users who have deployed a FastGPT version earlier than `<4.8` need to run this step.
...@@ -96,7 +102,7 @@ Migration behavior: ...@@ -96,7 +102,7 @@ Migration behavior:
5. Missing `node.name` falls back to `flowType`, and missing `input.label` falls back to `input.key`. 5. Missing `node.name` falls back to `flowType`, and missing `input.label` falls back to `input.key`.
6. Before writing, the script validates `nodes`, `edges`, and `chatConfig` with `PublishAppBodySchema`. Documents that fail validation are not written and are included in the endpoint response. 6. Before writing, the script validates `nodes`, `edges`, and `chatConfig` with `PublishAppBodySchema`. Documents that fail validation are not written and are included in the endpoint response.
### 5. Run the Workflow V2 Enum and Structure Cleanup ### 6. Run the Workflow V2 Enum and Structure Cleanup
Some historical workflow nodes may have stored TypeScript enum expression strings directly in MongoDB, for example: Some historical workflow nodes may have stored TypeScript enum expression strings directly in MongoDB, for example:
...@@ -154,13 +160,47 @@ Cleanup behavior: ...@@ -154,13 +160,47 @@ Cleanup behavior:
The response includes separate statistics for `apps`, `appVersions`, and `total`, including scanned documents, fixable documents, Zod error count, successful writes, failed writes, enum expression statistics, change samples, and error samples. The response includes separate statistics for `apps`, `appVersions`, and `total`, including scanned documents, fixable documents, Zod error count, successful writes, failed writes, enum expression statistics, change samples, and error samples.
## ⚙️ Optimizations ### 7. Clean Up Duplicate Chat Headers
Some historical data may contain duplicate `chats` records with the same `appId + chatId`, which can prevent the new unique index from being created. After upgrading, run the duplicate chat header cleanup script to keep the record with the latest `updateTime`. If multiple records have the same `updateTime`, the record with the largest `_id` is kept.
Migration script path: `projects/app/src/pages/api/admin/dataClean/cleanupDuplicateChats.ts`. This endpoint is only for this upgrade migration and is not a public OpenAPI endpoint.
The endpoint uses dry-run mode by default. It scans duplicate groups and returns samples without deleting data:
```bash
curl -X POST 'https://your-domain/api/admin/dataClean/cleanupDuplicateChats' \
-H 'Content-Type: application/json' \
-H 'rootkey: YOUR_ROOT_KEY' \
-d '{"dryRun":true,"sampleLimit":20}'
```
After confirming the returned statistics, set `dryRun` to `false` to delete duplicates:
```bash
curl -X POST 'https://your-domain/api/admin/dataClean/cleanupDuplicateChats' \
-H 'Content-Type: application/json' \
-H 'rootkey: YOUR_ROOT_KEY' \
-d '{"dryRun":false,"sampleLimit":20}'
```
Request parameters:
| Parameter | Type | Default | Description |
| ------------- | ------- | ------- | ------------------------------------------------------------ |
| `dryRun` | boolean | `true` | Whether to scan and report statistics without deleting. |
| `sampleLimit` | number | `20` | Number of duplicate group samples to return. Range: `0~100`. |
Cleanup behavior:
1. Virtual machine file URLs now use the new API. 1. Scans duplicate chat headers in the `chats` collection by `appId + chatId`.
2. Keeps the record with the latest `updateTime`; if timestamps are equal, `_id` descending order is used as a stable fallback.
3. In non-dry-run mode, deletes only duplicate `chats` headers. Messages in `chatitems` and `chat_item_responses` are not deleted.
4. The response includes duplicate group count, estimated delete count, actual delete count, and duplicate group samples.
## 🐛 Fixes ## 🐛 Fixes
1. Fixed historical V1 workflow data that could fail validation under the new save payload structure. 1. Fixed historical V1 workflow data that could fail validation under the new save payload structure.
2. Fixed dirty `FlowNodeInputTypeEnum.*`, `FlowNodeOutputTypeEnum.*`, and `WorkflowIOValueTypeEnum.*` expression strings in workflow node configuration that could break input rendering and IO type checks. 2. Fixed dirty `FlowNodeInputTypeEnum.*`, `FlowNodeOutputTypeEnum.*`, and `WorkflowIOValueTypeEnum.*` expression strings in workflow node configuration that could break input rendering and IO type checks.
3. Fixed AgentV2 MCP not being able to retrieve schemas. 3. Fixed AgentV2 MCP not being able to retrieve schemas.
4. Fixed variable updates not being written back at the end of batch execution nodes. 4. Fixed workflow text boxes where pressing Ctrl+C while selecting text could be intercepted by node copy handling, preventing text from being copied.
...@@ -50,12 +50,21 @@ VLM_MAX_PROCESS=10 ...@@ -50,12 +50,21 @@ VLM_MAX_PROCESS=10
SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003 SSE_MCP_SERVER_PROXY_ENDPOINT=http://localhost:3003
``` ```
### 3. 更新镜像 ### 3. OpenSandbox 变量更新
OpenSandbox Volume Manager 配置变为必填,并且环境变量改名为:
```dotenv
AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL=http://localhost:3005
AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN=vmtoken
```
### 4. 更新镜像
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.15.0-beta7 - 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.15.0-beta7
- 更新 fastgpt-pro(fastgpt 商业版) 镜像 tag: v4.15.0-beta7 - 更新 fastgpt-pro(fastgpt 商业版) 镜像 tag: v4.15.0-beta7
### 4. 执行工作流 V1 升级 V2 迁移(可选) ### 5. 执行工作流 V1 升级 V2 迁移(可选)
该步骤仅需部署过 `<4.8` 版本 FastGPT 的用户执行。 该步骤仅需部署过 `<4.8` 版本 FastGPT 的用户执行。
...@@ -96,7 +105,7 @@ curl -X POST 'https://你的域名/api/admin/dataClean/v1WorkflowToV2' \ ...@@ -96,7 +105,7 @@ curl -X POST 'https://你的域名/api/admin/dataClean/v1WorkflowToV2' \
5. 缺失 `node.name` 时用 `flowType` 兜底,缺失 `input.label` 时用 `input.key` 兜底。 5. 缺失 `node.name` 时用 `flowType` 兜底,缺失 `input.label` 时用 `input.key` 兜底。
6. 写库前使用 `PublishAppBodySchema` 校验 `nodes`、`edges`、`chatConfig`,校验失败的文档不会写入,并会记录到接口返回结果。 6. 写库前使用 `PublishAppBodySchema` 校验 `nodes`、`edges`、`chatConfig`,校验失败的文档不会写入,并会记录到接口返回结果。
### 5. 执行工作流 V2 枚举与结构脏数据清洗 ### 6. 执行工作流 V2 枚举与结构脏数据清洗
部分历史工作流节点可能把 TypeScript 枚举表达式字符串直接写入 MongoDB,例如: 部分历史工作流节点可能把 TypeScript 枚举表达式字符串直接写入 MongoDB,例如:
...@@ -154,6 +163,44 @@ curl -X POST 'https://你的域名/api/admin/dataClean/initWorkflowData' \ ...@@ -154,6 +163,44 @@ curl -X POST 'https://你的域名/api/admin/dataClean/initWorkflowData' \
返回结果会分别展示 `apps`、`appVersions` 和 `total` 的统计,包括扫描文档数、可修复文档数、Zod 错误数量、写入成功数量、写入失败数量、枚举表达式统计、变更样本和错误样本。 返回结果会分别展示 `apps`、`appVersions` 和 `total` 的统计,包括扫描文档数、可修复文档数、Zod 错误数量、写入成功数量、写入失败数量、枚举表达式统计、变更样本和错误样本。
### 7. 清理重复 Chat 会话头
部分历史数据可能存在相同 `appId + chatId` 的重复 `chats` 会话头,导致新版本创建唯一索引失败。升级后可执行重复会话头清理脚本,保留 `updateTime` 最新的一条记录;如果 `updateTime` 相同,则保留 `_id` 最大的一条。
迁移脚本位置:`projects/app/src/pages/api/admin/dataClean/cleanupDuplicateChats.ts`。该接口仅用于本次升级迁移,不作为 OpenAPI 对外接口。
接口默认 dry-run,只扫描重复组并返回样本,不删除数据:
```bash
curl -X POST 'https://你的域名/api/admin/dataClean/cleanupDuplicateChats' \
-H 'Content-Type: application/json' \
-H 'rootkey: 你的ROOT_KEY' \
-d '{"dryRun":true,"sampleLimit":20}'
```
确认返回统计无误后,将 `dryRun` 改为 `false` 执行删除:
```bash
curl -X POST 'https://你的域名/api/admin/dataClean/cleanupDuplicateChats' \
-H 'Content-Type: application/json' \
-H 'rootkey: 你的ROOT_KEY' \
-d '{"dryRun":false,"sampleLimit":20}'
```
接口参数:
| 参数 | 类型 | 默认值 | 说明 |
| ------------- | ------- | ------ | ---------------------------------------- |
| `dryRun` | boolean | `true` | 是否只扫描统计不删除。 |
| `sampleLimit` | number | `20` | 返回重复组样本数量,取值范围为 `0~100`。 |
清理逻辑:
1. 按 `appId + chatId` 扫描 `chats` 集合中的重复会话头。
2. 每组保留 `updateTime` 最新的一条;若时间相同,用 `_id` 倒序作为稳定兜底。
3. 非 dry-run 时只删除重复的 `chats` 会话头,不删除 `chatitems` 和 `chat_item_responses` 中的消息内容。
4. 返回结果包含重复组数量、预计删除数量、实际删除数量和重复组样本。
## ⚙️ 优化 ## ⚙️ 优化
1. 虚拟机文件地址使用新 API。 1. 虚拟机文件地址使用新 API。
...@@ -164,3 +211,4 @@ curl -X POST 'https://你的域名/api/admin/dataClean/initWorkflowData' \ ...@@ -164,3 +211,4 @@ curl -X POST 'https://你的域名/api/admin/dataClean/initWorkflowData' \
2. 修复工作流节点配置中 `FlowNodeInputTypeEnum.*`、`FlowNodeOutputTypeEnum.*` 和 `WorkflowIOValueTypeEnum.*` 枚举表达式字符串脏数据导致输入渲染和 IO 类型判断异常的问题。 2. 修复工作流节点配置中 `FlowNodeInputTypeEnum.*`、`FlowNodeOutputTypeEnum.*` 和 `WorkflowIOValueTypeEnum.*` 枚举表达式字符串脏数据导致输入渲染和 IO 类型判断异常的问题。
3. AgentV2 mcp 拿不到 schema。 3. AgentV2 mcp 拿不到 schema。
4. 批量执行节点最后未回写变量更新。 4. 批量执行节点最后未回写变量更新。
5. 工作流文本框,ctrl+c 复制文本内容时,会被节点复制抢占,导致无法复制文本。
...@@ -167,8 +167,8 @@ ...@@ -167,8 +167,8 @@
"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-27T22:05:51+08:00", "content/plugin/system-tool-development.en.mdx": "2026-06-27T22:05:51+08:00",
"content/plugin/system-tool-development.mdx": "2026-06-27T22:05:51+08:00", "content/plugin/system-tool-development.mdx": "2026-06-27T22:05:51+08:00",
"content/self-host/config/env.en.mdx": "2026-06-30T12:13:17+08:00", "content/self-host/config/env.en.mdx": "2026-06-30T16:19:46+08:00",
"content/self-host/config/env.mdx": "2026-06-30T12:13:17+08:00", "content/self-host/config/env.mdx": "2026-06-30T16:19:46+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",
...@@ -181,10 +181,10 @@ ...@@ -181,10 +181,10 @@
"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.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/remote-debug-suite.mdx": "2026-06-27T22:05:51+08:00",
"content/self-host/config/sandbox/common.en.mdx": "2026-06-30T13:59:36+08:00", "content/self-host/config/sandbox/common.en.mdx": "2026-06-30T14:56:33+08:00",
"content/self-host/config/sandbox/common.mdx": "2026-06-30T13:59:36+08:00", "content/self-host/config/sandbox/common.mdx": "2026-06-30T14:56:33+08:00",
"content/self-host/config/sandbox/sealosdevbox.en.mdx": "2026-06-30T13:59:36+08:00", "content/self-host/config/sandbox/sealosdevbox.en.mdx": "2026-06-30T14:56:33+08:00",
"content/self-host/config/sandbox/sealosdevbox.mdx": "2026-06-30T13:59:36+08:00", "content/self-host/config/sandbox/sealosdevbox.mdx": "2026-06-30T14:56:33+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",
...@@ -310,8 +310,8 @@ ...@@ -310,8 +310,8 @@
"content/self-host/upgrading/4-15/41505.mdx": "2026-06-29T10:45:58+08:00", "content/self-host/upgrading/4-15/41505.mdx": "2026-06-29T10:45:58+08:00",
"content/self-host/upgrading/4-15/41506.en.mdx": "2026-06-29T00:47:43+08:00", "content/self-host/upgrading/4-15/41506.en.mdx": "2026-06-29T00:47:43+08:00",
"content/self-host/upgrading/4-15/41506.mdx": "2026-06-29T00:47:43+08:00", "content/self-host/upgrading/4-15/41506.mdx": "2026-06-29T00:47:43+08:00",
"content/self-host/upgrading/4-15/41507.en.mdx": "2026-06-30T14:32:39+08:00", "content/self-host/upgrading/4-15/41507.en.mdx": "2026-06-30T16:19:46+08:00",
"content/self-host/upgrading/4-15/41507.mdx": "2026-06-30T14:32:39+08:00", "content/self-host/upgrading/4-15/41507.mdx": "2026-06-30T16:19:46+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",
...@@ -452,6 +452,6 @@ ...@@ -452,6 +452,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-30T13:59:36+08:00", "content/toc.en.mdx": "2026-06-30T14:56:33+08:00",
"content/toc.mdx": "2026-06-30T13:59:36+08:00" "content/toc.mdx": "2026-06-30T14:56:33+08:00"
} }
\ No newline at end of file
import { agentSandboxProviderList } from './constants';
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
export const agentSandboxProviderRequiredEnvKeys = {
sealosdevbox: [
'AGENT_SANDBOX_SEALOS_BASEURL',
'AGENT_SANDBOX_SEALOS_TOKEN',
'AGENT_SANDBOX_SEALOS_IMAGE'
],
opensandbox: ['AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'AGENT_SANDBOX_OPENSANDBOX_API_KEY'],
e2b: ['AGENT_SANDBOX_E2B_API_KEY']
} satisfies Record<SandboxProviderType, readonly string[]>;
export type AgentSandboxEnvSource = Record<string, string | undefined>;
const isAgentSandboxProvider = (provider: string | undefined): provider is SandboxProviderType =>
agentSandboxProviderList.includes(provider as SandboxProviderType);
/**
* 判断系统是否显式配置了 Agent 虚拟机能力。
* 必须基于原始 env 判断,避免被服务端 env schema 的默认 provider 误判为已启用。
*/
export const hasAgentSandboxConfig = (env: AgentSandboxEnvSource): boolean => {
const provider = env.AGENT_SANDBOX_PROVIDER;
if (!isAgentSandboxProvider(provider)) {
return false;
}
return agentSandboxProviderRequiredEnvKeys[provider].every((key) => !!env[key]);
};
import { describe, expect, it } from 'vitest';
import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants';
import {
agentSandboxProviderRequiredEnvKeys,
hasAgentSandboxConfig
} from '@fastgpt/global/core/ai/sandbox/env';
describe('agent sandbox env config', () => {
it('keeps provider list aligned with required env keys', () => {
expect(Object.keys(agentSandboxProviderRequiredEnvKeys).sort()).toEqual(
[...agentSandboxProviderList].sort()
);
});
it('requires all provider env keys before enabling agent sandbox', () => {
expect(
hasAgentSandboxConfig({
AGENT_SANDBOX_PROVIDER: 'sealosdevbox',
AGENT_SANDBOX_SEALOS_BASEURL: 'https://devbox.example.com',
AGENT_SANDBOX_SEALOS_TOKEN: 'token'
})
).toBe(false);
expect(
hasAgentSandboxConfig({
AGENT_SANDBOX_PROVIDER: 'sealosdevbox',
AGENT_SANDBOX_SEALOS_BASEURL: 'https://devbox.example.com',
AGENT_SANDBOX_SEALOS_TOKEN: 'token',
AGENT_SANDBOX_SEALOS_IMAGE: 'runtime/fastgpt:stable'
})
).toBe(true);
});
it('ignores missing or unsupported providers', () => {
expect(hasAgentSandboxConfig({})).toBe(false);
expect(hasAgentSandboxConfig({ AGENT_SANDBOX_PROVIDER: 'unknown' })).toBe(false);
});
});
...@@ -18,8 +18,8 @@ export type VolumeManagerConfig = { ...@@ -18,8 +18,8 @@ export type VolumeManagerConfig = {
*/ */
export function getVolumeManagerEnvConfig(): VolumeManagerConfig { export function getVolumeManagerEnvConfig(): VolumeManagerConfig {
return { return {
enable: serviceEnv.AGENT_SANDBOX_ENABLE_VOLUME, enable: true,
url: serviceEnv.AGENT_SANDBOX_VOLUME_MANAGER_URL!, url: serviceEnv.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL!,
token: serviceEnv.AGENT_SANDBOX_VOLUME_MANAGER_TOKEN token: serviceEnv.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN
}; };
} }
...@@ -91,9 +91,7 @@ export const getSessionVolumeConfig = async ( ...@@ -91,9 +91,7 @@ export const getSessionVolumeConfig = async (
const vmConfig = getVolumeManagerEnvConfig(); const vmConfig = getVolumeManagerEnvConfig();
if (!vmConfig.enable) return undefined; if (!vmConfig.enable) return undefined;
if (!vmConfig.url) { if (!vmConfig.url) {
throw new Error( throw new Error('AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL is required');
'AGENT_SANDBOX_VOLUME_MANAGER_URL is required when AGENT_SANDBOX_ENABLE_VOLUME=true'
);
} }
const claimName = await ensureSessionVolume(sandboxId); const claimName = await ensureSessionVolume(sandboxId);
const volumeResult = buildVolumeConfig(claimName); const volumeResult = buildVolumeConfig(claimName);
......
...@@ -13,7 +13,8 @@ const ChatItemResponseSchema = new Schema({ ...@@ -13,7 +13,8 @@ const ChatItemResponseSchema = new Schema({
}, },
sourceType: { sourceType: {
type: String, type: String,
enum: Object.values(ChatSourceTypeEnum) enum: Object.values(ChatSourceTypeEnum),
required: true
}, },
// 历史物理字段名,业务语义为 sourceId;App 场景才是真实 appId。 // 历史物理字段名,业务语义为 sourceId;App 场景才是真实 appId。
appId: { appId: {
...@@ -39,6 +40,7 @@ const ChatItemResponseSchema = new Schema({ ...@@ -39,6 +40,7 @@ const ChatItemResponseSchema = new Schema({
} }
}); });
/* TODO: 未全面检查操作,所以这里暂时不加 sourceType 的索引。 */
// 按 chat item 拉取完整 nodeResponse rows;复合索引包含 _id,避免详情读取时额外排序。 // 按 chat item 拉取完整 nodeResponse rows;复合索引包含 _id,避免详情读取时额外排序。
ChatItemResponseSchema.index({ appId: 1, chatId: 1, chatItemDataId: 1, _id: 1 }); ChatItemResponseSchema.index({ appId: 1, chatId: 1, chatItemDataId: 1, _id: 1 });
ChatItemResponseSchema.index({ ChatItemResponseSchema.index({
...@@ -48,7 +50,6 @@ ChatItemResponseSchema.index({ ...@@ -48,7 +50,6 @@ ChatItemResponseSchema.index({
chatItemDataId: 1, chatItemDataId: 1,
_id: 1 _id: 1
}); });
// Clear expired response // Clear expired response
ChatItemResponseSchema.index({ teamId: 1, time: -1 }); ChatItemResponseSchema.index({ teamId: 1, time: -1 });
......
...@@ -32,7 +32,8 @@ const ChatItemSchema = new Schema({ ...@@ -32,7 +32,8 @@ const ChatItemSchema = new Schema({
}, },
sourceType: { sourceType: {
type: String, type: String,
enum: Object.values(ChatSourceTypeEnum) enum: Object.values(ChatSourceTypeEnum),
required: true
}, },
dataId: { dataId: {
type: String, type: String,
...@@ -96,6 +97,7 @@ const ChatItemSchema = new Schema({ ...@@ -96,6 +97,7 @@ const ChatItemSchema = new Schema({
} }
}); });
/* TODO: 未全面检查操作,所以这里暂时不加 sourceType 的索引。 */
/* /*
delete by app; delete by app;
delete by chat id; delete by chat id;
......
import { connectionMongo, getMongoModel } from '../../common/mongo'; import { connectionMongo, getMongoModel } from '../../common/mongo';
import { getLogger, LogCategories } from '../../common/logger';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import { type ChatSchemaType } from '@fastgpt/global/core/chat/type'; import { type ChatSchemaType } from '@fastgpt/global/core/chat/type';
import { import {
...@@ -31,7 +30,8 @@ const ChatSchema = new Schema({ ...@@ -31,7 +30,8 @@ const ChatSchema = new Schema({
}, },
sourceType: { sourceType: {
type: String, type: String,
enum: Object.values(ChatSourceTypeEnum) enum: Object.values(ChatSourceTypeEnum),
required: true
}, },
// 历史物理字段名,业务语义为 sourceId;App 场景才是真实 appId。 // 历史物理字段名,业务语义为 sourceId;App 场景才是真实 appId。
appId: { appId: {
...@@ -133,135 +133,120 @@ const ChatSchema = new Schema({ ...@@ -133,135 +133,120 @@ const ChatSchema = new Schema({
userId: Schema.Types.ObjectId userId: Schema.Types.ObjectId
}); });
try { ChatSchema.index({ chatId: 1 });
ChatSchema.index({ chatId: 1 }); // Delete by appid; init chat; update chat; auth chat;
// Delete by appid; init chat; update chat; auth chat; ChatSchema.index({ sourceType: 1, appId: 1, chatId: 1 }, { unique: true });
ChatSchema.index({ appId: 1, chatId: 1 }, { unique: true });
ChatSchema.index( // timer, clear history
{ sourceType: 1, appId: 1, chatId: 1 }, ChatSchema.index({ updateTime: -1, teamId: 1 });
{ unique: true, name: 'sourceType_1_appId_1_chatId_1' } ChatSchema.index({ teamId: 1, updateTime: -1 });
);
// get user history(Cookie)
ChatSchema.index({ tmbId: 1, appId: 1, deleteTime: 1, top: -1, updateTime: -1 });
// Clear history(share),Init 4121 /* ===== 条件索引 ===== */
ChatSchema.index( // Clear history(share),Init 4121
{ appId: 1, outLinkUid: 1, tmbId: 1 }, ChatSchema.index(
{ { appId: 1, outLinkUid: 1, tmbId: 1 },
partialFilterExpression: { {
outLinkUid: { $exists: true } partialFilterExpression: {
} outLinkUid: { $exists: true }
} }
); }
);
// get user history // get share chat history
ChatSchema.index({ tmbId: 1, appId: 1, deleteTime: 1, top: -1, updateTime: -1 }); ChatSchema.index(
ChatSchema.index({ { shareId: 1, outLinkUid: 1, updateTime: -1 },
sourceType: 1, {
tmbId: 1, partialFilterExpression: {
appId: 1, shareId: { $exists: true }
deleteTime: 1,
top: -1,
updateTime: -1
});
// get share chat history
ChatSchema.index(
{ shareId: 1, outLinkUid: 1, updateTime: -1 },
{
partialFilterExpression: {
shareId: { $exists: true }
}
} }
); }
);
/* get chat logs */ /* get chat logs */
// 1. Common get // 1. Common get
ChatSchema.index({ appId: 1, updateTime: -1 }); ChatSchema.index({ appId: 1, updateTime: -1 });
// Get history(tmbId) // Get history(tmbId)
ChatSchema.index({ appId: 1, tmbId: 1, updateTime: -1 }); ChatSchema.index({ appId: 1, tmbId: 1, updateTime: -1 });
// clearHistory(API) // clearHistory(API)
ChatSchema.index({ appId: 1, source: 1, tmbId: 1, updateTime: -1 }); ChatSchema.index({ appId: 1, source: 1, tmbId: 1, updateTime: -1 });
// Periodic cleanup for chats stuck in generating state. // Periodic cleanup for chats stuck in generating state.
ChatSchema.index( ChatSchema.index(
{ chatGenerateStatus: 1, updateTime: 1 }, { chatGenerateStatus: 1, updateTime: 1 },
{ {
partialFilterExpression: { partialFilterExpression: {
chatGenerateStatus: ChatGenerateStatusEnum.generating chatGenerateStatus: ChatGenerateStatusEnum.generating
}
} }
); }
);
/* 反馈过滤的索引 */ /* 反馈过滤的索引 */
// 2. Has good feedback filter // 2. Has good feedback filter
ChatSchema.index( ChatSchema.index(
{ {
appId: 1, appId: 1,
hasGoodFeedback: 1, hasGoodFeedback: 1,
updateTime: -1 updateTime: -1
}, },
{ {
partialFilterExpression: { partialFilterExpression: {
hasGoodFeedback: true hasGoodFeedback: true
}
} }
); }
// Has bad feedback filter );
ChatSchema.index( // Has bad feedback filter
{ ChatSchema.index(
appId: 1, {
hasBadFeedback: 1, appId: 1,
updateTime: -1 hasBadFeedback: 1,
}, updateTime: -1
{ },
partialFilterExpression: { {
hasBadFeedback: true partialFilterExpression: {
} hasBadFeedback: true
} }
); }
// 3. Has unread good feedback filter );
ChatSchema.index( // 3. Has unread good feedback filter
{ ChatSchema.index(
appId: 1, {
hasUnreadGoodFeedback: 1, appId: 1,
updateTime: -1 hasUnreadGoodFeedback: 1,
}, updateTime: -1
{ },
partialFilterExpression: { {
hasUnreadGoodFeedback: true partialFilterExpression: {
} hasUnreadGoodFeedback: true
} }
); }
// Has unread bad feedback filter );
ChatSchema.index( // Has unread bad feedback filter
{ ChatSchema.index(
appId: 1, {
hasUnreadBadFeedback: 1, appId: 1,
updateTime: -1 hasUnreadBadFeedback: 1,
}, updateTime: -1
{ },
partialFilterExpression: { {
hasUnreadBadFeedback: true partialFilterExpression: {
} hasUnreadBadFeedback: true
} }
); }
// Has error filter );
ChatSchema.index( // Has error filter
{ ChatSchema.index(
appId: 1, {
errorCount: 1, appId: 1,
updateTime: -1 errorCount: 1,
}, updateTime: -1
{ },
partialFilterExpression: { {
errorCount: { $gt: 0 } partialFilterExpression: {
} errorCount: { $gt: 0 }
} }
); }
);
// timer, clear history
ChatSchema.index({ updateTime: -1, teamId: 1 });
ChatSchema.index({ teamId: 1, updateTime: -1 });
} catch (error) {
const logger = getLogger(LogCategories.INFRA.MONGO);
logger.error('Failed to build chat indexes', { error });
}
export const MongoChat = getMongoModel<ChatSchemaType>(chatCollectionName, ChatSchema); export const MongoChat = getMongoModel<ChatSchemaType>(chatCollectionName, ChatSchema);
...@@ -93,9 +93,10 @@ export const dispatchParallelRun = async (props: Props): Promise<Response> => { ...@@ -93,9 +93,10 @@ export const dispatchParallelRun = async (props: Props): Promise<Response> => {
: `${taskResponseIdPrefix}_task_${index}`; : `${taskResponseIdPrefix}_task_${index}`;
try { try {
const taskVariableState = props.variableState.clone();
const response = await runWorkflow({ const response = await runWorkflow({
...props, ...props,
variableState: props.variableState.clone(), variableState: taskVariableState,
nodeResponseParentId: taskResponseId, nodeResponseParentId: taskResponseId,
runtimeNodes: taskRuntimeNodes, runtimeNodes: taskRuntimeNodes,
runtimeEdges: taskRuntimeEdges runtimeEdges: taskRuntimeEdges
...@@ -111,6 +112,12 @@ export const dispatchParallelRun = async (props: Props): Promise<Response> => { ...@@ -111,6 +112,12 @@ export const dispatchParallelRun = async (props: Props): Promise<Response> => {
accumulatedPoints += attemptPoints; accumulatedPoints += attemptPoints;
const result = parseTaskResponse({ index, response }); const result = parseTaskResponse({ index, response });
if (result.success) {
const taskVariables = taskVariableState.toRuntimeRecord();
for (const [key, value] of Object.entries(taskVariables)) {
await props.variableState.set(key, value);
}
}
const attemptResult = { const attemptResult = {
...result, ...result,
taskResponseId, taskResponseId,
......
import z from 'zod';
// 系统最大字符串处理长度
export const SYSTEM_STRING_LENGTH_UNIT = 1_000_000;
// Log 枚举
export const LogLevelSchema = z.enum(['trace', 'debug', 'info', 'warning', 'error', 'fatal']);
// S3
export const StorageVendorSchema = z.enum(['minio', 'aws-s3', 'cos', 'oss']);
export const StorageCosProtocolSchema = z.enum(['https:', 'http:']);
...@@ -4,38 +4,24 @@ import { isPhaseProductionBuild } from '@fastgpt/global/common/system/constants' ...@@ -4,38 +4,24 @@ import { isPhaseProductionBuild } from '@fastgpt/global/common/system/constants'
import { DEFAULT_MAX_FOLDER_DEPTH } from '@fastgpt/global/common/parentFolder/depth'; import { DEFAULT_MAX_FOLDER_DEPTH } from '@fastgpt/global/common/parentFolder/depth';
import { BoolSchema, IntSchema, NumSchema, UrlSchema } from '@fastgpt/global/common/zod'; import { BoolSchema, IntSchema, NumSchema, UrlSchema } from '@fastgpt/global/common/zod';
import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants'; import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants';
import { hasAgentSandboxConfig as hasAgentSandboxConfigFromEnv } from '@fastgpt/global/core/ai/sandbox/env'; import {
AgentSandboxProxyUrlSchema,
getAgentSandboxMissingRequiredEnvKeys,
getRuntimeEnv,
isAgentSandboxProvider
} from './env.util';
import {
LogLevelSchema,
StorageVendorSchema,
StorageCosProtocolSchema,
SYSTEM_STRING_LENGTH_UNIT
} from './env.const';
const defaultableIntSchema = (defaultValue: number) => const defaultableIntSchema = (defaultValue: number) =>
z.preprocess( z.preprocess(
(value) => (value === '' || value === undefined ? defaultValue : value), (value) => (value === '' || value === undefined ? defaultValue : value),
z.coerce.number<number>().int().nonnegative() z.coerce.number<number>().int().nonnegative()
); );
// 系统最大字符串处理长度
const SYSTEM_STRING_LENGTH_UNIT = 1_000_000;
// 枚举
const LogLevelSchema = z.enum(['trace', 'debug', 'info', 'warning', 'error', 'fatal']);
const StorageVendorSchema = z.enum(['minio', 'aws-s3', 'cos', 'oss']);
const StorageCosProtocolSchema = z.enum(['https:', 'http:']);
const AgentSandboxProxyUrlSchema = z.string().refine((url) => /^wss?:\/\//.test(url), {
message: 'AGENT_SANDBOX_PROXY_URL must start with ws:// or wss://'
});
const TEST_INVOKE_TOKEN_SECRET = 'fastgpt_test_invoke_token_secret_32';
/**
* 测试套件会在多个 workspace(包含 pro/admin 子模块)里直接导入 serviceEnv。
* 生产启动仍要求显式配置 INVOKE_TOKEN_SECRET;仅 Vitest/测试环境允许注入稳定测试密钥,
* 避免每个测试项目都重复维护同一个必填运行时密钥。
*/
const getRuntimeEnv = (): NodeJS.ProcessEnv => ({
...process.env,
INVOKE_TOKEN_SECRET:
process.env.INVOKE_TOKEN_SECRET ??
(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'
? TEST_INVOKE_TOKEN_SECRET
: undefined)
});
export const serviceEnv = createEnv({ export const serviceEnv = createEnv({
skipValidation: isPhaseProductionBuild, skipValidation: isPhaseProductionBuild,
...@@ -45,16 +31,19 @@ export const serviceEnv = createEnv({ ...@@ -45,16 +31,19 @@ export const serviceEnv = createEnv({
SYNC_INDEX: BoolSchema.default(true), SYNC_INDEX: BoolSchema.default(true),
// ==================== 密钥 ==================== // ==================== 密钥 ====================
ROOT_KEY: z
.string()
.min(6, 'ROOT_KEY must be at least 6 characters')
.default('fastgpt_root_key'),
TOKEN_KEY: z TOKEN_KEY: z
.string() .string()
.min(6, 'TOKEN_KEY must be at least 6 characters') .min(6, 'TOKEN_KEY must be at least 6 characters')
.default('fastgpt_token_key'), .default('fastgpt_token_key'),
FILE_TOKEN_KEY: z.string().min(6, 'FILE_TOKEN_KEY must be at least 6 characters'), FILE_TOKEN_KEY: z.string().min(6, 'FILE_TOKEN_KEY must be at least 6 characters'),
AES256_SECRET_KEY: z.string().min(6, 'AES256_SECRET_KEY must be at least 6 characters'), AES256_SECRET_KEY: z.string().min(6, 'AES256_SECRET_KEY must be at least 6 characters'),
ROOT_KEY: z
.string() // Invoke 反向调用相关。该密钥用于签发/校验插件反向调用 JWT,必须显式配置,避免未配置时落到公开默认值。
.min(6, 'ROOT_KEY must be at least 6 characters') INVOKE_TOKEN_SECRET: z.string().min(32, 'INVOKE_TOKEN_SECRET must be at least 32 characters'),
.default('fastgpt_root_key'),
// ==================== 服务地址与集成 ==================== // ==================== 服务地址与集成 ====================
// 插件 // 插件
...@@ -96,9 +85,8 @@ export const serviceEnv = createEnv({ ...@@ -96,9 +85,8 @@ export const serviceEnv = createEnv({
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: z.string().default('fastgpt-agent-sandbox'), AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: z.string().default('fastgpt-agent-sandbox'),
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: z.string().default('latest'), AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: z.string().default('latest'),
AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: BoolSchema.default(true), AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: BoolSchema.default(true),
AGENT_SANDBOX_ENABLE_VOLUME: BoolSchema.default(false), AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL: UrlSchema.optional(),
AGENT_SANDBOX_VOLUME_MANAGER_URL: UrlSchema.default('http://localhost:3005'), AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN: z.string().optional(),
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: z.string().optional(),
AGENT_SANDBOX_DISK_MB: NumSchema.min(1).default(1024).meta({ AGENT_SANDBOX_DISK_MB: NumSchema.min(1).default(1024).meta({
description: description:
'Agent sandbox 磁盘大小基准(MB)。冷归档包上限等于该值,Skill 包和 IDE 单文件上限按该值的一半四舍五入计算。' 'Agent sandbox 磁盘大小基准(MB)。冷归档包上限等于该值,Skill 包和 IDE 单文件上限按该值的一半四舍五入计算。'
...@@ -357,10 +345,7 @@ export const serviceEnv = createEnv({ ...@@ -357,10 +345,7 @@ export const serviceEnv = createEnv({
FEISHU_BASE_URL: UrlSchema.default('https://open.feishu.cn'), FEISHU_BASE_URL: UrlSchema.default('https://open.feishu.cn'),
DINGTALK_BASE_URL: UrlSchema.default('https://api.dingtalk.com'), DINGTALK_BASE_URL: UrlSchema.default('https://api.dingtalk.com'),
DINGTALK_OAPI_BASE_URL: UrlSchema.default('https://oapi.dingtalk.com'), DINGTALK_OAPI_BASE_URL: UrlSchema.default('https://oapi.dingtalk.com'),
YUQUE_DATASET_BASE_URL: UrlSchema.default('https://www.yuque.com'), YUQUE_DATASET_BASE_URL: UrlSchema.default('https://www.yuque.com')
// Invoke 反向调用相关。该密钥用于签发/校验插件反向调用 JWT,必须显式配置,避免未配置时落到公开默认值。
INVOKE_TOKEN_SECRET: z.string().min(32, 'INVOKE_TOKEN_SECRET must be at least 32 characters')
}, },
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: getRuntimeEnv(), runtimeEnv: getRuntimeEnv(),
...@@ -370,16 +355,22 @@ export const serviceEnv = createEnv({ ...@@ -370,16 +355,22 @@ export const serviceEnv = createEnv({
} }
}); });
/* ===== Check ===== */
if (serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY > serviceEnv.WORKFLOW_MAX_LOOP_TIMES) { if (serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY > serviceEnv.WORKFLOW_MAX_LOOP_TIMES) {
throw new Error( throw new Error(
`Invalid environment configuration: WORKFLOW_PARALLEL_MAX_CONCURRENCY (${serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY}) must not exceed WORKFLOW_MAX_LOOP_TIMES (${serviceEnv.WORKFLOW_MAX_LOOP_TIMES})` `Invalid environment configuration: WORKFLOW_PARALLEL_MAX_CONCURRENCY (${serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY}) must not exceed WORKFLOW_MAX_LOOP_TIMES (${serviceEnv.WORKFLOW_MAX_LOOP_TIMES})`
); );
} }
if (!isPhaseProductionBuild && hasAgentSandboxConfigFromEnv(process.env)) { if (!isPhaseProductionBuild) {
if (!serviceEnv.AGENT_SANDBOX_PROXY_URL) { // 共享 serviceEnv 会被 pro/admin 等项目导入,这里只校验 provider 运行态必填环境变量。
// 主站浏览器直连 agent-sandbox-proxy 的配置由 projects/app 启动流程单独校验。
const missingAgentSandboxEnvKeys = getAgentSandboxMissingRequiredEnvKeys(process.env);
if (missingAgentSandboxEnvKeys.length > 0) {
throw new Error( throw new Error(
'AGENT_SANDBOX_PROXY_URL is required when Agent Sandbox is enabled. Please configure a browser-accessible ws:// or wss:// agent-sandbox-proxy URL.' `Invalid Agent Sandbox environment variables: ${missingAgentSandboxEnvKeys.join(
', '
)} are required when AGENT_SANDBOX_PROVIDER is ${serviceEnv.AGENT_SANDBOX_PROVIDER}.`
); );
} }
} }
...@@ -388,7 +379,8 @@ export const SYSTEM_MAX_STRING_LENGTH = ...@@ -388,7 +379,8 @@ export const SYSTEM_MAX_STRING_LENGTH =
serviceEnv.SYSTEM_MAX_STRING_LENGTH_M * SYSTEM_STRING_LENGTH_UNIT; serviceEnv.SYSTEM_MAX_STRING_LENGTH_M * SYSTEM_STRING_LENGTH_UNIT;
/** /**
* 判断系统是否显式配置了 Agent 虚拟机能力 * 判断系统是否显式配置了 Agent 虚拟机 provider
* 必须直读 process.env,避免空环境被 schema 默认值误判为已启用 * 启动阶段会先校验 provider 配套 env,避免前端拿到半配置的沙盒能力
*/ */
export const hasAgentSandboxConfig = (): boolean => hasAgentSandboxConfigFromEnv(process.env); export const hasAgentSandboxConfig = (): boolean =>
isAgentSandboxProvider(process.env.AGENT_SANDBOX_PROVIDER);
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants';
import z from 'zod';
const TEST_INVOKE_TOKEN_SECRET = 'fastgpt_test_invoke_token_secret_32';
/**
* 测试套件会在多个 workspace(包含 pro/admin 子模块)里直接导入 serviceEnv。
* 生产启动仍要求显式配置 INVOKE_TOKEN_SECRET;仅 Vitest/测试环境允许注入稳定测试密钥,
* 避免每个测试项目都重复维护同一个必填运行时密钥。
*/
export const getRuntimeEnv = (): NodeJS.ProcessEnv => ({
...process.env,
INVOKE_TOKEN_SECRET:
process.env.INVOKE_TOKEN_SECRET ??
(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'
? TEST_INVOKE_TOKEN_SECRET
: undefined)
});
/* ===== sandbox ===== */
export const AgentSandboxProxyUrlSchema = z.string().refine((url) => /^wss?:\/\//.test(url), {
message: 'AGENT_SANDBOX_PROXY_URL must start with ws:// or wss://'
});
const agentSandboxProviderRequiredEnvKeys = {
sealosdevbox: [
'AGENT_SANDBOX_SEALOS_BASEURL',
'AGENT_SANDBOX_SEALOS_TOKEN',
'AGENT_SANDBOX_SEALOS_IMAGE'
],
opensandbox: [
'AGENT_SANDBOX_OPENSANDBOX_BASEURL',
'AGENT_SANDBOX_OPENSANDBOX_API_KEY',
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL',
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN'
],
e2b: ['AGENT_SANDBOX_E2B_API_KEY']
} satisfies Record<SandboxProviderType, readonly string[]>;
export const isAgentSandboxProvider = (
provider: string | undefined
): provider is SandboxProviderType =>
agentSandboxProviderList.includes(provider as SandboxProviderType);
/**
* 获取已配置 provider 缺失的运行态必填环境变量。
* 未配置合法 provider 时不启用 Agent Sandbox,因此不要求任何 provider 配套变量。
*/
export const getAgentSandboxMissingRequiredEnvKeys = (env: NodeJS.ProcessEnv): string[] => {
const provider = env.AGENT_SANDBOX_PROVIDER;
if (!isAgentSandboxProvider(provider)) {
return [];
}
return agentSandboxProviderRequiredEnvKeys[provider].filter((key) => !env[key]);
};
/* ===== Sandbox proxy ===== */
const agentSandboxProxyRequiredEnvKeys = [
'AGENT_SANDBOX_PROXY_SECRET',
'AGENT_SANDBOX_PROXY_URL'
] as const;
/**
* 校验 FastGPT app 浏览器直连 agent-sandbox-proxy 所需环境变量。
* 该能力只属于主站 app 的 sandbox editor/proxy 链路,不能放在共享 serviceEnv 中校验,
* 否则 pro/admin 等只复用服务端能力的项目会被不必要的 proxy 配置阻塞。
*/
export const validateAgentSandboxProxyEnv = (): void => {
const provider = process.env.AGENT_SANDBOX_PROVIDER;
if (!agentSandboxProviderList.includes(provider as (typeof agentSandboxProviderList)[number])) {
return;
}
const missingAgentSandboxProxyEnvKeys = agentSandboxProxyRequiredEnvKeys.filter(
(key) => !process.env[key]
);
if (missingAgentSandboxProxyEnvKeys.length === 0) {
return;
}
throw new Error(
`Invalid Agent Sandbox proxy environment variables: ${missingAgentSandboxProxyEnvKeys.join(
', '
)} are required when AGENT_SANDBOX_PROVIDER is ${provider}.`
);
};
...@@ -18,16 +18,41 @@ import { ...@@ -18,16 +18,41 @@ import {
upsertRunningSandboxInstance upsertRunningSandboxInstance
} from '@fastgpt/service/core/ai/sandbox/infrastructure/instance/repository'; } from '@fastgpt/service/core/ai/sandbox/infrastructure/instance/repository';
import { connectionMongo } from '@fastgpt/service/common/mongo'; import { connectionMongo } from '@fastgpt/service/common/mongo';
import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants'; import {
import { hasAgentSandboxConfig } from '@fastgpt/global/core/ai/sandbox/env'; agentSandboxProviderList,
SandboxStatusEnum,
SandboxTypeEnum
} from '@fastgpt/global/core/ai/sandbox/constants';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { delay } from '@fastgpt/global/common/system/utils'; import { delay } from '@fastgpt/global/common/system/utils';
import { getRunningSandboxId } from '@fastgpt/service/core/ai/sandbox/utils/id'; import { getRunningSandboxId } from '@fastgpt/service/core/ai/sandbox/utils/id';
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
const { Types } = connectionMongo; const { Types } = connectionMongo;
const hasSandboxEnv = const agentSandboxProviderRequiredEnvKeys = {
process.env.SANDBOX_INTEGRATION === 'true' && hasAgentSandboxConfig(process.env); sealosdevbox: [
'AGENT_SANDBOX_SEALOS_BASEURL',
'AGENT_SANDBOX_SEALOS_TOKEN',
'AGENT_SANDBOX_SEALOS_IMAGE'
],
opensandbox: ['AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'AGENT_SANDBOX_OPENSANDBOX_API_KEY'],
e2b: ['AGENT_SANDBOX_E2B_API_KEY']
} satisfies Record<SandboxProviderType, readonly string[]>;
const isAgentSandboxProvider = (provider: string | undefined): provider is SandboxProviderType =>
agentSandboxProviderList.includes(provider as SandboxProviderType);
const hasFullAgentSandboxEnv = (): boolean => {
const provider = process.env.AGENT_SANDBOX_PROVIDER;
if (!isAgentSandboxProvider(provider)) {
return false;
}
return agentSandboxProviderRequiredEnvKeys[provider].every((key) => !!process.env[key]);
};
const hasSandboxEnv = process.env.SANDBOX_INTEGRATION === 'true' && hasFullAgentSandboxEnv();
const runFullIntegration = process.env.SANDBOX_INTEGRATION_FULL === 'true'; const runFullIntegration = process.env.SANDBOX_INTEGRATION_FULL === 'true';
vi.mock('@fastgpt/service/env', () => ({ vi.mock('@fastgpt/service/env', () => ({
...@@ -51,9 +76,10 @@ vi.mock('@fastgpt/service/env', () => ({ ...@@ -51,9 +76,10 @@ vi.mock('@fastgpt/service/env', () => ({
AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: envBool( AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: envBool(
process.env.AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY process.env.AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY
), ),
AGENT_SANDBOX_ENABLE_VOLUME: envBool(process.env.AGENT_SANDBOX_ENABLE_VOLUME), AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL:
AGENT_SANDBOX_VOLUME_MANAGER_URL: process.env.AGENT_SANDBOX_VOLUME_MANAGER_URL, process.env.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL,
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: process.env.AGENT_SANDBOX_VOLUME_MANAGER_TOKEN, AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN:
process.env.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN,
AGENT_SANDBOX_DISK_MB: agentSandboxDiskMB, AGENT_SANDBOX_DISK_MB: agentSandboxDiskMB,
AGENT_SANDBOX_E2B_API_KEY: process.env.AGENT_SANDBOX_E2B_API_KEY AGENT_SANDBOX_E2B_API_KEY: process.env.AGENT_SANDBOX_E2B_API_KEY
......
...@@ -36,6 +36,10 @@ vi.mock('@fastgpt/service/core/ai/sandbox/application/runtime/mirrors', () => ({ ...@@ -36,6 +36,10 @@ vi.mock('@fastgpt/service/core/ai/sandbox/application/runtime/mirrors', () => ({
prepareSandboxRuntimeMirrors: mirrorMock.prepareSandboxRuntimeMirrors prepareSandboxRuntimeMirrors: mirrorMock.prepareSandboxRuntimeMirrors
})); }));
vi.mock('@fastgpt/service/core/ai/sandbox/infrastructure/provider/runtimeProfile', () => ({
getSandboxRuntimeProfile: () => ({ workDirectory: '/workspace' })
}));
vi.mock('@fastgpt/service/common/s3/sources/chat', () => ({ vi.mock('@fastgpt/service/common/s3/sources/chat', () => ({
getS3ChatSource: () => ({ getS3ChatSource: () => ({
uploadChatFile: s3Mock.uploadChatFile, uploadChatFile: s3Mock.uploadChatFile,
......
...@@ -12,6 +12,10 @@ const originalEnv = { ...@@ -12,6 +12,10 @@ const originalEnv = {
AGENT_SANDBOX_OPENSANDBOX_RUNTIME: process.env.AGENT_SANDBOX_OPENSANDBOX_RUNTIME, AGENT_SANDBOX_OPENSANDBOX_RUNTIME: process.env.AGENT_SANDBOX_OPENSANDBOX_RUNTIME,
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO, AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO,
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG, AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG,
AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL:
process.env.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL,
AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN:
process.env.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN,
AGENT_SANDBOX_DISK_MB: process.env.AGENT_SANDBOX_DISK_MB, AGENT_SANDBOX_DISK_MB: process.env.AGENT_SANDBOX_DISK_MB,
AGENT_SANDBOX_PROXY_SECRET: process.env.AGENT_SANDBOX_PROXY_SECRET, AGENT_SANDBOX_PROXY_SECRET: process.env.AGENT_SANDBOX_PROXY_SECRET,
AGENT_SANDBOX_PROXY_URL: process.env.AGENT_SANDBOX_PROXY_URL, AGENT_SANDBOX_PROXY_URL: process.env.AGENT_SANDBOX_PROXY_URL,
...@@ -66,6 +70,14 @@ describe('sandbox provider config', () => { ...@@ -66,6 +70,14 @@ describe('sandbox provider config', () => {
'AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG', 'AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG',
originalEnv.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG originalEnv.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG
); );
vi.stubEnv(
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL',
originalEnv.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL
);
vi.stubEnv(
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN',
originalEnv.AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN
);
vi.stubEnv('AGENT_SANDBOX_DISK_MB', originalEnv.AGENT_SANDBOX_DISK_MB); vi.stubEnv('AGENT_SANDBOX_DISK_MB', originalEnv.AGENT_SANDBOX_DISK_MB);
vi.stubEnv( vi.stubEnv(
'AGENT_SANDBOX_WS_MAX_MESSAGE_BYTES', 'AGENT_SANDBOX_WS_MAX_MESSAGE_BYTES',
...@@ -196,22 +208,6 @@ describe('sandbox provider config', () => { ...@@ -196,22 +208,6 @@ describe('sandbox provider config', () => {
}); });
}); });
it('requires sealos runtime image when runtime adapter config is requested', async () => {
vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', 'https://devbox.example.com');
vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', 'sealos-token');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', undefined);
const { getSandboxAdapterConfig } = await loadSandboxConfigModule();
expect(() =>
getSandboxAdapterConfig({
provider: 'sealosdevbox',
runtime: true,
sessionId: 'session-1'
})
).toThrow('AGENT_SANDBOX_SEALOS_IMAGE is required for sealosdevbox provider');
});
it('normalizes missing provider env values before validation', async () => { it('normalizes missing provider env values before validation', async () => {
vi.resetModules(); vi.resetModules();
vi.doMock('@fastgpt/service/env', () => ({ vi.doMock('@fastgpt/service/env', () => ({
...@@ -243,30 +239,6 @@ describe('sandbox provider config', () => { ...@@ -243,30 +239,6 @@ describe('sandbox provider config', () => {
} }
}); });
it('allows empty proxy secret before agent sandbox credentials are configured', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://opensandbox.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', '');
vi.stubEnv('AGENT_SANDBOX_PROXY_SECRET', '');
vi.resetModules();
const { serviceEnv } = await import('@fastgpt/service/env');
expect(serviceEnv.AGENT_SANDBOX_PROXY_SECRET).toBeUndefined();
});
it('rejects short proxy secret when agent sandbox is configured', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://opensandbox.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', 'opensandbox-api-key');
vi.stubEnv('AGENT_SANDBOX_PROXY_SECRET', 'short');
vi.resetModules();
await expect(import('@fastgpt/service/env')).rejects.toThrow(
'Invalid environment variables. Please check: AGENT_SANDBOX_PROXY_SECRET'
);
});
it('parses opensandbox config and runtime create config from env', async () => { it('parses opensandbox config and runtime create config from env', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://opensandbox.local'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://opensandbox.local');
...@@ -274,6 +246,8 @@ describe('sandbox provider config', () => { ...@@ -274,6 +246,8 @@ describe('sandbox provider config', () => {
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_RUNTIME', 'docker'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_RUNTIME', 'docker');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO', 'fastgpt-agent-sandbox'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO', 'fastgpt-agent-sandbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG', 'test'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG', 'test');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL', 'http://volume-manager.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN', 'volume-token');
const { getSandboxAdapterConfig } = await loadSandboxConfigModule(); const { getSandboxAdapterConfig } = await loadSandboxConfigModule();
......
...@@ -32,12 +32,12 @@ describe('sandbox runtime profile', () => { ...@@ -32,12 +32,12 @@ describe('sandbox runtime profile', () => {
}); });
it('uses fixed /workspace as opensandbox work directory', async () => { it('uses fixed /workspace as opensandbox work directory', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', '');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO', 'runtime-image'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO', 'runtime-image');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG', 'stable'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG', 'stable');
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule(); const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
const runtimeProfile = getSandboxRuntimeProfile(); const runtimeProfile = getSandboxRuntimeProfile('opensandbox');
expect(runtimeProfile).toMatchObject({ expect(runtimeProfile).toMatchObject({
provider: 'opensandbox', provider: 'opensandbox',
...@@ -52,11 +52,11 @@ describe('sandbox runtime profile', () => { ...@@ -52,11 +52,11 @@ describe('sandbox runtime profile', () => {
}); });
it('uses devbox defaults for sealosdevbox provider', async () => { it('uses devbox defaults for sealosdevbox provider', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', '');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable'); vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule(); const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
const runtimeProfile = getSandboxRuntimeProfile(); const runtimeProfile = getSandboxRuntimeProfile('sealosdevbox');
expect(runtimeProfile).toMatchObject({ expect(runtimeProfile).toMatchObject({
provider: 'sealosdevbox', provider: 'sealosdevbox',
...@@ -71,32 +71,21 @@ describe('sandbox runtime profile', () => { ...@@ -71,32 +71,21 @@ describe('sandbox runtime profile', () => {
}); });
it('uses sealos work directory from env', async () => { it('uses sealos work directory from env', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', '');
vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace'); vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable'); vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule(); const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
expect(getSandboxRuntimeProfile()).toMatchObject({ expect(getSandboxRuntimeProfile('sealosdevbox')).toMatchObject({
provider: 'sealosdevbox', provider: 'sealosdevbox',
workDirectory: '/custom/devbox/workspace', workDirectory: '/custom/devbox/workspace',
entrypoint: '' entrypoint: ''
}); });
}); });
it('requires sealos runtime image when building create config', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', undefined);
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
const runtimeProfile = getSandboxRuntimeProfile();
expect(() => runtimeProfile.buildConfig()).toThrow(
'AGENT_SANDBOX_SEALOS_IMAGE is required for sealosdevbox provider'
);
});
it('builds provider-specific create config through runtime profile', async () => { it('builds provider-specific create config through runtime profile', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', '');
vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace'); vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable'); vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
......
...@@ -14,9 +14,8 @@ describe('sandbox volume config', () => { ...@@ -14,9 +14,8 @@ describe('sandbox volume config', () => {
it('reads volume-manager configuration from service env', async () => { it('reads volume-manager configuration from service env', async () => {
vi.doMock('@fastgpt/service/env', () => ({ vi.doMock('@fastgpt/service/env', () => ({
serviceEnv: { serviceEnv: {
AGENT_SANDBOX_ENABLE_VOLUME: true, AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL: 'http://volume-manager.local',
AGENT_SANDBOX_VOLUME_MANAGER_URL: 'http://volume-manager.local', AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN: 'volume-token'
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: 'volume-token'
} }
})); }));
......
...@@ -145,7 +145,7 @@ describe('sandbox volume service', () => { ...@@ -145,7 +145,7 @@ describe('sandbox volume service', () => {
volumeConfigMock.config.url = ''; volumeConfigMock.config.url = '';
await expect(getSessionVolumeConfig('session-1')).rejects.toThrow( await expect(getSessionVolumeConfig('session-1')).rejects.toThrow(
'AGENT_SANDBOX_VOLUME_MANAGER_URL is required' 'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL is required'
); );
}); });
......
...@@ -8,6 +8,10 @@ import { ...@@ -8,6 +8,10 @@ import {
import type { SandboxClient } from '@fastgpt/service/core/ai/sandbox/interface/runtime'; import type { SandboxClient } from '@fastgpt/service/core/ai/sandbox/interface/runtime';
import type { DirectoryEntry, FileInfo, FileReadResult } from '@fastgpt-sdk/sandbox-adapter'; import type { DirectoryEntry, FileInfo, FileReadResult } from '@fastgpt-sdk/sandbox-adapter';
vi.mock('@fastgpt/service/core/ai/sandbox/infrastructure/provider/runtimeProfile', () => ({
getSandboxRuntimeProfile: () => ({ workDirectory: '/workspace' })
}));
// ─── helpers ─────────────────────────────────────────────────────────────── // ─── helpers ───────────────────────────────────────────────────────────────
function makeProvider( function makeProvider(
......
...@@ -48,6 +48,7 @@ describe('getChatItems', () => { ...@@ -48,6 +48,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -189,6 +190,7 @@ describe('getChatItems', () => { ...@@ -189,6 +190,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -271,6 +273,7 @@ describe('getChatItems', () => { ...@@ -271,6 +273,7 @@ describe('getChatItems', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: otherChatId, chatId: otherChatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -734,6 +737,7 @@ describe('getChatItems', () => { ...@@ -734,6 +737,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'ai-data-id', dataId: 'ai-data-id',
...@@ -744,6 +748,7 @@ describe('getChatItems', () => { ...@@ -744,6 +748,7 @@ describe('getChatItems', () => {
await MongoChatItemResponse.create([ await MongoChatItemResponse.create([
{ {
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: aiItem.dataId, chatItemDataId: aiItem.dataId,
...@@ -758,6 +763,7 @@ describe('getChatItems', () => { ...@@ -758,6 +763,7 @@ describe('getChatItems', () => {
}, },
{ {
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: aiItem.dataId, chatItemDataId: aiItem.dataId,
...@@ -799,6 +805,7 @@ describe('getChatItems', () => { ...@@ -799,6 +805,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'fallback-ai-data-id', dataId: 'fallback-ai-data-id',
...@@ -816,6 +823,7 @@ describe('getChatItems', () => { ...@@ -816,6 +823,7 @@ describe('getChatItems', () => {
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: 'fallback-ai-data-id', chatItemDataId: 'fallback-ai-data-id',
...@@ -844,6 +852,7 @@ describe('getChatItems', () => { ...@@ -844,6 +852,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'empty-inline-ai-data-id', dataId: 'empty-inline-ai-data-id',
...@@ -854,6 +863,7 @@ describe('getChatItems', () => { ...@@ -854,6 +863,7 @@ describe('getChatItems', () => {
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: 'empty-inline-ai-data-id', chatItemDataId: 'empty-inline-ai-data-id',
...@@ -886,6 +896,7 @@ describe('getChatItems', () => { ...@@ -886,6 +896,7 @@ describe('getChatItems', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: aiDataId, dataId: aiDataId,
...@@ -911,6 +922,7 @@ describe('getChatItems', () => { ...@@ -911,6 +922,7 @@ describe('getChatItems', () => {
await MongoChatItemResponse.create([ await MongoChatItemResponse.create([
{ {
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: aiDataId, chatItemDataId: aiDataId,
...@@ -942,6 +954,7 @@ describe('getChatItems', () => { ...@@ -942,6 +954,7 @@ describe('getChatItems', () => {
}, },
{ {
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: aiDataId, chatItemDataId: aiDataId,
...@@ -966,6 +979,7 @@ describe('getChatItems', () => { ...@@ -966,6 +979,7 @@ describe('getChatItems', () => {
}, },
{ {
teamId: testUser.teamId, teamId: testUser.teamId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
chatItemDataId: aiDataId, chatItemDataId: aiDataId,
...@@ -1048,6 +1062,7 @@ describe('updateChatFeedbackCount', () => { ...@@ -1048,6 +1062,7 @@ describe('updateChatFeedbackCount', () => {
chatId, chatId,
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
source: ChatSourceEnum.online source: ChatSourceEnum.online
}); });
...@@ -1066,6 +1081,7 @@ describe('updateChatFeedbackCount', () => { ...@@ -1066,6 +1081,7 @@ describe('updateChatFeedbackCount', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
......
...@@ -40,7 +40,7 @@ const createChatTree = async ({ ...@@ -40,7 +40,7 @@ const createChatTree = async ({
}) => { }) => {
const teamId = '65f000000000000000000001'; const teamId = '65f000000000000000000001';
const tmbId = '65f000000000000000000002'; const tmbId = '65f000000000000000000002';
const chatSource = legacy ? {} : { sourceType }; const chatSource = { sourceType };
await MongoChat.create({ await MongoChat.create({
...chatSource, ...chatSource,
......
...@@ -7,7 +7,8 @@ const base = { ...@@ -7,7 +7,8 @@ const base = {
teamId: '654a4107c32f3bf5f998452f', teamId: '654a4107c32f3bf5f998452f',
tmbId: '654a4107c32f3bf5f9984530', tmbId: '654a4107c32f3bf5f9984530',
appId: '67e0d5535c02d1d5cdede71f', appId: '67e0d5535c02d1d5cdede71f',
chatId: 'interactive-chat-id' chatId: 'interactive-chat-id',
sourceType: ChatSourceTypeEnum.app
}; };
const chatSource = { const chatSource = {
......
...@@ -443,6 +443,7 @@ describe('pushChatRecords', () => { ...@@ -443,6 +443,7 @@ describe('pushChatRecords', () => {
}).lean(); }).lean();
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId: testTeamId, teamId: testTeamId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: props.chatId, chatId: props.chatId,
chatItemDataId: aiItem?.dataId, chatItemDataId: aiItem?.dataId,
...@@ -555,6 +556,7 @@ describe('pushChatRecords', () => { ...@@ -555,6 +556,7 @@ describe('pushChatRecords', () => {
chatId: props.chatId, chatId: props.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: props.source, source: props.source,
chatGenerateStatus: ChatGenerateStatusEnum.generating, chatGenerateStatus: ChatGenerateStatusEnum.generating,
hasBeenRead: false hasBeenRead: false
...@@ -563,6 +565,7 @@ describe('pushChatRecords', () => { ...@@ -563,6 +565,7 @@ describe('pushChatRecords', () => {
{ {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: props.chatId, chatId: props.chatId,
dataId: responseChatItemId, dataId: responseChatItemId,
...@@ -572,6 +575,7 @@ describe('pushChatRecords', () => { ...@@ -572,6 +575,7 @@ describe('pushChatRecords', () => {
{ {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: props.chatId, chatId: props.chatId,
dataId: responseChatItemId, dataId: responseChatItemId,
...@@ -611,6 +615,7 @@ describe('pushChatRecords', () => { ...@@ -611,6 +615,7 @@ describe('pushChatRecords', () => {
chatId: props.chatId, chatId: props.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: props.source, source: props.source,
chatGenerateStatus: ChatGenerateStatusEnum.generating, chatGenerateStatus: ChatGenerateStatusEnum.generating,
hasBeenRead: false hasBeenRead: false
...@@ -618,6 +623,7 @@ describe('pushChatRecords', () => { ...@@ -618,6 +623,7 @@ describe('pushChatRecords', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: props.chatId, chatId: props.chatId,
dataId: responseChatItemId, dataId: responseChatItemId,
...@@ -702,6 +708,7 @@ describe('pushChatRecords', () => { ...@@ -702,6 +708,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.Human, obj: ChatRoleEnum.Human,
value: [ value: [
...@@ -739,6 +746,7 @@ describe('pushChatRecords', () => { ...@@ -739,6 +746,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [ value: [
...@@ -776,6 +784,7 @@ describe('pushChatRecords', () => { ...@@ -776,6 +784,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -853,6 +862,7 @@ describe('pushChatRecords', () => { ...@@ -853,6 +862,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -942,6 +952,7 @@ describe('pushChatRecords', () => { ...@@ -942,6 +952,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -1068,6 +1079,7 @@ describe('pushChatRecords', () => { ...@@ -1068,6 +1079,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'plan-ask-data-id', dataId: 'plan-ask-data-id',
...@@ -1125,6 +1137,7 @@ describe('pushChatRecords', () => { ...@@ -1125,6 +1137,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'plan-ask-data-id', dataId: 'plan-ask-data-id',
...@@ -1171,6 +1184,7 @@ describe('pushChatRecords', () => { ...@@ -1171,6 +1184,7 @@ describe('pushChatRecords', () => {
chatId: props.chatId, chatId: props.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
source: props.source, source: props.source,
title: 'Test Chat' title: 'Test Chat'
...@@ -1180,6 +1194,7 @@ describe('pushChatRecords', () => { ...@@ -1180,6 +1194,7 @@ describe('pushChatRecords', () => {
chatId: props.chatId, chatId: props.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.Human, obj: ChatRoleEnum.Human,
dataId: 'prepared-round-data-id', dataId: 'prepared-round-data-id',
...@@ -1193,6 +1208,7 @@ describe('pushChatRecords', () => { ...@@ -1193,6 +1208,7 @@ describe('pushChatRecords', () => {
chatId: props.chatId, chatId: props.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'prepared-round-data-id', dataId: 'prepared-round-data-id',
...@@ -1267,6 +1283,7 @@ describe('pushChatRecords', () => { ...@@ -1267,6 +1283,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -1313,6 +1330,7 @@ describe('pushChatRecords', () => { ...@@ -1313,6 +1330,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -1381,6 +1399,7 @@ describe('pushChatRecords', () => { ...@@ -1381,6 +1399,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -1438,6 +1457,7 @@ describe('pushChatRecords', () => { ...@@ -1438,6 +1457,7 @@ describe('pushChatRecords', () => {
chatId: 'test-chat-id', chatId: 'test-chat-id',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
dataId: 'data-id-1', dataId: 'data-id-1',
...@@ -1460,6 +1480,7 @@ describe('pushChatRecords', () => { ...@@ -1460,6 +1480,7 @@ describe('pushChatRecords', () => {
// Create an existing response // Create an existing response
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId: testTeamId, teamId: testTeamId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: 'test-chat-id', chatId: 'test-chat-id',
chatItemDataId: 'data-id-1', chatItemDataId: 'data-id-1',
...@@ -1474,6 +1495,7 @@ describe('pushChatRecords', () => { ...@@ -1474,6 +1495,7 @@ describe('pushChatRecords', () => {
}); });
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId: testTeamId, teamId: testTeamId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: 'test-chat-id', chatId: 'test-chat-id',
chatItemDataId: 'data-id-1', chatItemDataId: 'data-id-1',
......
...@@ -15,51 +15,42 @@ const hasIndex = ( ...@@ -15,51 +15,42 @@ const hasIndex = (
); );
}); });
const findIndex = (
indexes: ReturnType<typeof MongoChat.schema.indexes>,
keys: Record<string, 1 | -1>
) => indexes.find(([indexKeys]) => JSON.stringify(indexKeys) === JSON.stringify(keys));
describe('chat schema indexes', () => { describe('chat schema indexes', () => {
it('keeps sourceType optional without silently defaulting new writes to app', () => { it('requires sourceType for new chat writes without silently defaulting to app', () => {
for (const schema of [MongoChat.schema, MongoChatItem.schema, MongoChatItemResponse.schema]) { for (const schema of [MongoChat.schema, MongoChatItem.schema, MongoChatItemResponse.schema]) {
const sourceTypePath = schema.path('sourceType'); const sourceTypePath = schema.path('sourceType');
expect(sourceTypePath?.options.required).toBeUndefined(); expect(sourceTypePath?.options.required).toBe(true);
expect(sourceTypePath?.options.default).toBeUndefined(); expect(sourceTypePath?.options.default).toBeUndefined();
} }
}); });
it('declares source-aware unique chat identity index while keeping legacy app index', () => { it('declares source-aware unique chat identity index', () => {
const indexes = MongoChat.schema.indexes(); const indexes = MongoChat.schema.indexes();
const sourceAwareIndex = findIndex(indexes, { sourceType: 1, appId: 1, chatId: 1 });
expect(hasIndex(indexes, { appId: 1, chatId: 1 }, { unique: true })).toBe(true); expect(sourceAwareIndex?.[1]?.unique).toBe(true);
expect(
hasIndex(
indexes,
{
sourceType: 1,
appId: 1,
chatId: 1
},
{
unique: true,
name: 'sourceType_1_appId_1_chatId_1'
}
)
).toBe(true);
}); });
it('declares source-aware chat item read and pagination indexes', () => { it('keeps legacy chat item read and pagination indexes', () => {
const indexes = MongoChatItem.schema.indexes(); const indexes = MongoChatItem.schema.indexes();
expect(hasIndex(indexes, { sourceType: 1, appId: 1, chatId: 1, dataId: 1 })).toBe(true); expect(hasIndex(indexes, { appId: 1, chatId: 1, dataId: 1 })).toBe(true);
expect(hasIndex(indexes, { sourceType: 1, appId: 1, chatId: 1, deleteTime: 1 })).toBe(true); expect(hasIndex(indexes, { appId: 1, chatId: 1, deleteTime: 1 })).toBe(true);
expect(hasIndex(indexes, { sourceType: 1, appId: 1, chatId: 1, _id: -1 })).toBe(true); expect(hasIndex(indexes, { appId: 1, chatId: 1, _id: -1 })).toBe(true);
expect(hasIndex(indexes, { sourceType: 1, appId: 1, chatId: 1, obj: 1, _id: -1 })).toBe(true); expect(hasIndex(indexes, { appId: 1, chatId: 1, obj: 1, _id: -1 })).toBe(true);
}); });
it('declares source-aware node response lookup index', () => { it('keeps legacy node response lookup index', () => {
const indexes = MongoChatItemResponse.schema.indexes(); const indexes = MongoChatItemResponse.schema.indexes();
expect( expect(
hasIndex(indexes, { hasIndex(indexes, {
sourceType: 1,
appId: 1, appId: 1,
chatId: 1, chatId: 1,
chatItemDataId: 1, chatItemDataId: 1,
......
...@@ -39,6 +39,7 @@ const createChat = (override: Record<string, unknown> = {}) => ...@@ -39,6 +39,7 @@ const createChat = (override: Record<string, unknown> = {}) =>
chatId: base.chatId, chatId: base.chatId,
teamId: base.teamId, teamId: base.teamId,
tmbId: base.tmbId, tmbId: base.tmbId,
sourceType: base.sourceType,
appId: base.appId, appId: base.appId,
source: ChatSourceEnum.online, source: ChatSourceEnum.online,
...override ...override
......
...@@ -87,6 +87,7 @@ describe('chat dataId validation', () => { ...@@ -87,6 +87,7 @@ describe('chat dataId validation', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'same-round-id', dataId: 'same-round-id',
...@@ -112,6 +113,7 @@ describe('chat dataId validation', () => { ...@@ -112,6 +113,7 @@ describe('chat dataId validation', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'existing-ai', dataId: 'existing-ai',
...@@ -166,6 +168,7 @@ describe('chat dataId validation', () => { ...@@ -166,6 +168,7 @@ describe('chat dataId validation', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'existing-human', dataId: 'existing-human',
...@@ -196,6 +199,7 @@ describe('chat dataId validation', () => { ...@@ -196,6 +199,7 @@ describe('chat dataId validation', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'existing-ai', dataId: 'existing-ai',
......
...@@ -185,6 +185,7 @@ describe('prepare chat round', () => { ...@@ -185,6 +185,7 @@ describe('prepare chat round', () => {
chatId: params.chatId, chatId: params.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: params.source, source: params.source,
sourceName: 'original-source-name', sourceName: 'original-source-name',
updateTime: originalUpdateTime, updateTime: originalUpdateTime,
...@@ -208,6 +209,7 @@ describe('prepare chat round', () => { ...@@ -208,6 +209,7 @@ describe('prepare chat round', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: params.chatId, chatId: params.chatId,
dataId: 'duplicated-ai-data-id', dataId: 'duplicated-ai-data-id',
...@@ -245,11 +247,13 @@ describe('prepare chat round', () => { ...@@ -245,11 +247,13 @@ describe('prepare chat round', () => {
chatId: params.chatId, chatId: params.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: params.source source: params.source
}); });
await MongoChatItem.create({ await MongoChatItem.create({
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
chatId: params.chatId, chatId: params.chatId,
dataId: 'previous-ai-data-id', dataId: 'previous-ai-data-id',
...@@ -386,6 +390,7 @@ describe('prepare chat round', () => { ...@@ -386,6 +390,7 @@ describe('prepare chat round', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: params.source, source: params.source,
title: 'Manual Topic', title: 'Manual Topic',
customTitle: 'Manual Topic' customTitle: 'Manual Topic'
......
...@@ -9,11 +9,13 @@ import { ReadFileTooData } from '@fastgpt/service/core/workflow/dispatch/ai/tool ...@@ -9,11 +9,13 @@ import { ReadFileTooData } from '@fastgpt/service/core/workflow/dispatch/ai/tool
const { const {
getSandboxToolInfoMock, getSandboxToolInfoMock,
getSandboxRuntimeProfileMock,
prepareSandboxToolRuntimeMock, prepareSandboxToolRuntimeMock,
runAgentSandboxEntrypointMock, runAgentSandboxEntrypointMock,
withAgentSandboxInitLeaseMock withAgentSandboxInitLeaseMock
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
getSandboxToolInfoMock: vi.fn(), getSandboxToolInfoMock: vi.fn(),
getSandboxRuntimeProfileMock: vi.fn(),
prepareSandboxToolRuntimeMock: vi.fn(), prepareSandboxToolRuntimeMock: vi.fn(),
runAgentSandboxEntrypointMock: vi.fn(), runAgentSandboxEntrypointMock: vi.fn(),
withAgentSandboxInitLeaseMock: vi.fn(async ({ fn }: { fn: () => Promise<unknown> }) => fn()) withAgentSandboxInitLeaseMock: vi.fn(async ({ fn }: { fn: () => Promise<unknown> }) => fn())
...@@ -35,6 +37,7 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/runtime', async (importOrigi ...@@ -35,6 +37,7 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/runtime', async (importOrigi
await importOriginal<typeof import('@fastgpt/service/core/ai/sandbox/interface/runtime')>(); await importOriginal<typeof import('@fastgpt/service/core/ai/sandbox/interface/runtime')>();
return { return {
...original, ...original,
getSandboxRuntimeProfile: getSandboxRuntimeProfileMock,
runAgentSandboxEntrypoint: runAgentSandboxEntrypointMock, runAgentSandboxEntrypoint: runAgentSandboxEntrypointMock,
withAgentSandboxInitLease: withAgentSandboxInitLeaseMock withAgentSandboxInitLease: withAgentSandboxInitLeaseMock
}; };
...@@ -55,6 +58,7 @@ describe('useToolCatalog', () => { ...@@ -55,6 +58,7 @@ describe('useToolCatalog', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
getSandboxToolInfoMock.mockReturnValue(undefined); getSandboxToolInfoMock.mockReturnValue(undefined);
getSandboxRuntimeProfileMock.mockReturnValue({ workDirectory: '/workspace' });
prepareSandboxToolRuntimeMock.mockResolvedValue({ prepareSandboxToolRuntimeMock.mockResolvedValue({
provider: { provider: {
execute: vi.fn() execute: vi.fn()
......
...@@ -74,11 +74,18 @@ const makeDispatchFlowResponse = ( ...@@ -74,11 +74,18 @@ const makeDispatchFlowResponse = (
} as DispatchFlowResponse; } as DispatchFlowResponse;
}; };
const makeVariableState = () => const makeVariableState = (runtimeVariables: Record<string, unknown> = {}) =>
({ ({
clone: () => makeVariableState(), clone: vi.fn(() => makeVariableState(runtimeVariables)),
toRuntimeRecord: () => ({}), get: vi.fn((key: string) => runtimeVariables[key]),
toStoreRecord: () => ({}) set: vi.fn(async (key: string, value: unknown) => {
runtimeVariables[key] = value;
return value;
}),
getStoreValue: vi.fn((key: string) => runtimeVariables[key]),
getFileStoreValueByRuntimeUrl: vi.fn(),
toRuntimeRecord: () => ({ ...runtimeVariables }),
toStoreRecord: () => ({ ...runtimeVariables })
}) as any; }) as any;
const makeProps = (override: Record<string, any> = {}) => { const makeProps = (override: Record<string, any> = {}) => {
...@@ -232,4 +239,76 @@ describe('dispatchParallelRun', () => { ...@@ -232,4 +239,76 @@ describe('dispatchParallelRun', () => {
expect(runWorkflowMock.mock.calls[0][0].nodeResponseParentId).toBe('parallelRun1_task_0'); expect(runWorkflowMock.mock.calls[0][0].nodeResponseParentId).toBe('parallelRun1_task_0');
}); });
it('成功任务结束后把 clone 中的全局变量提交回父状态', async () => {
const taskVariableState = makeVariableState({ count: 2 });
const parentVariableState = {
...makeVariableState(),
clone: vi.fn(() => taskVariableState),
set: vi.fn()
};
runWorkflowMock.mockResolvedValue(
makeDispatchFlowResponse({
nodeResponses: [
makeResponseItem('nestedEnd', {
moduleType: FlowNodeTypeEnum.nestedEnd,
loopOutputValue: 'done'
})
]
})
);
await dispatchParallelRun(
makeProps({
variableState: parentVariableState
})
);
expect(parentVariableState.set).toHaveBeenCalledWith('count', 2);
});
it('失败 attempt 不提交全局变量更新,重试成功后只提交成功 attempt 的更新', async () => {
const failedClone = makeVariableState({ count: 1 });
const successClone = makeVariableState({ count: 2 });
const parentVariableState = {
...makeVariableState(),
clone: vi.fn().mockReturnValueOnce(failedClone).mockReturnValueOnce(successClone),
set: vi.fn()
};
runWorkflowMock
.mockResolvedValueOnce(
makeDispatchFlowResponse({
nodeResponses: [
makeResponseItem('failed-node', {
error: 'failed'
})
]
})
)
.mockResolvedValueOnce(
makeDispatchFlowResponse({
nodeResponses: [
makeResponseItem('nestedEnd', {
moduleType: FlowNodeTypeEnum.nestedEnd,
loopOutputValue: 'done'
})
]
})
);
await dispatchParallelRun(
makeProps({
params: {
loopInputArray: ['a'],
[NodeInputKeyEnum.childrenNodeIdList]: [],
[NodeInputKeyEnum.parallelRunMaxConcurrency]: 1,
[NodeInputKeyEnum.parallelRunMaxRetryTimes]: 1
},
variableState: parentVariableState
})
);
expect(parentVariableState.set).toHaveBeenCalledTimes(1);
expect(parentVariableState.set).toHaveBeenCalledWith('count', 2);
});
}); });
...@@ -11,9 +11,12 @@ const originalEnv = { ...@@ -11,9 +11,12 @@ const originalEnv = {
VITEST: process.env.VITEST, VITEST: process.env.VITEST,
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER, AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER,
AGENT_SANDBOX_SEALOS_BASEURL: process.env.AGENT_SANDBOX_SEALOS_BASEURL,
AGENT_SANDBOX_SEALOS_TOKEN: process.env.AGENT_SANDBOX_SEALOS_TOKEN,
AGENT_SANDBOX_SEALOS_IMAGE: process.env.AGENT_SANDBOX_SEALOS_IMAGE,
AGENT_SANDBOX_E2B_API_KEY: process.env.AGENT_SANDBOX_E2B_API_KEY,
AGENT_SANDBOX_OPENSANDBOX_BASEURL: process.env.AGENT_SANDBOX_OPENSANDBOX_BASEURL, AGENT_SANDBOX_OPENSANDBOX_BASEURL: process.env.AGENT_SANDBOX_OPENSANDBOX_BASEURL,
AGENT_SANDBOX_OPENSANDBOX_API_KEY: process.env.AGENT_SANDBOX_OPENSANDBOX_API_KEY, AGENT_SANDBOX_OPENSANDBOX_API_KEY: process.env.AGENT_SANDBOX_OPENSANDBOX_API_KEY
AGENT_SANDBOX_PROXY_URL: process.env.AGENT_SANDBOX_PROXY_URL
}; };
const importServiceEnv = async () => { const importServiceEnv = async () => {
...@@ -32,15 +35,12 @@ describe('serviceEnv', () => { ...@@ -32,15 +35,12 @@ describe('serviceEnv', () => {
vi.stubEnv('VITEST', originalEnv.VITEST); vi.stubEnv('VITEST', originalEnv.VITEST);
vi.stubEnv('NODE_ENV', originalEnv.NODE_ENV); vi.stubEnv('NODE_ENV', originalEnv.NODE_ENV);
vi.stubEnv('AGENT_SANDBOX_PROVIDER', originalEnv.AGENT_SANDBOX_PROVIDER); vi.stubEnv('AGENT_SANDBOX_PROVIDER', originalEnv.AGENT_SANDBOX_PROVIDER);
vi.stubEnv( vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', originalEnv.AGENT_SANDBOX_SEALOS_BASEURL);
'AGENT_SANDBOX_OPENSANDBOX_BASEURL', vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', originalEnv.AGENT_SANDBOX_SEALOS_TOKEN);
originalEnv.AGENT_SANDBOX_OPENSANDBOX_BASEURL vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', originalEnv.AGENT_SANDBOX_SEALOS_IMAGE);
); vi.stubEnv('AGENT_SANDBOX_E2B_API_KEY', originalEnv.AGENT_SANDBOX_E2B_API_KEY);
vi.stubEnv( vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', originalEnv.AGENT_SANDBOX_OPENSANDBOX_BASEURL);
'AGENT_SANDBOX_OPENSANDBOX_API_KEY', vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', originalEnv.AGENT_SANDBOX_OPENSANDBOX_API_KEY);
originalEnv.AGENT_SANDBOX_OPENSANDBOX_API_KEY
);
vi.stubEnv('AGENT_SANDBOX_PROXY_URL', originalEnv.AGENT_SANDBOX_PROXY_URL);
}); });
it('validates SYSTEM_MAX_STRING_LENGTH_M during service env init', async () => { it('validates SYSTEM_MAX_STRING_LENGTH_M during service env init', async () => {
...@@ -127,32 +127,23 @@ describe('serviceEnv', () => { ...@@ -127,32 +127,23 @@ describe('serviceEnv', () => {
expect(customEnv.serviceEnv.AGENT_SANDBOX_DISK_MB).toBe(333); expect(customEnv.serviceEnv.AGENT_SANDBOX_DISK_MB).toBe(333);
}); });
it('未启用 Agent Sandbox 时允许 AGENT_SANDBOX_PROXY_URL 为空', async () => { it('配置 sealosdevbox 后缺少运行镜像会阻止启动', async () => {
vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey');
vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret');
vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret);
vi.stubEnv('VITEST', 'true');
vi.stubEnv('AGENT_SANDBOX_PROVIDER', '');
vi.stubEnv('AGENT_SANDBOX_PROXY_URL', '');
await expect(importServiceEnv()).resolves.toBeDefined();
});
it('启用 opensandbox 时要求配置 AGENT_SANDBOX_PROXY_URL', async () => {
vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey'); vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey');
vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret'); vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret');
vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret); vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret);
vi.stubEnv('VITEST', 'true'); vi.stubEnv('VITEST', 'true');
vi.stubEnv('NODE_ENV', 'development'); vi.stubEnv('NODE_ENV', 'development');
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://mock-opensandbox.local'); vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', 'http://mock-sealos.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', 'mock-opensandbox-api-key'); vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', 'mock-sealos-token');
vi.stubEnv('AGENT_SANDBOX_PROXY_URL', ''); vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', '');
await expect(importServiceEnv()).rejects.toThrow('AGENT_SANDBOX_PROXY_URL is required'); await expect(importServiceEnv()).rejects.toThrow(
'AGENT_SANDBOX_SEALOS_IMAGE are required when AGENT_SANDBOX_PROVIDER is sealosdevbox'
);
}); });
it('启用 opensandbox 时要求 AGENT_SANDBOX_PROXY_URL 是 WebSocket 地址', async () => { it('启用 Agent Sandbox 时不要求共享服务配置 app proxy 环境变量', async () => {
vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey'); vi.stubEnv('FILE_TOKEN_KEY', 'filetokenkey');
vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret'); vi.stubEnv('AES256_SECRET_KEY', 'fastgptsecret');
vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret); vi.stubEnv('INVOKE_TOKEN_SECRET', validInvokeTokenSecret);
...@@ -161,8 +152,11 @@ describe('serviceEnv', () => { ...@@ -161,8 +152,11 @@ describe('serviceEnv', () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox'); vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'opensandbox');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://mock-opensandbox.local'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'http://mock-opensandbox.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', 'mock-opensandbox-api-key'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_API_KEY', 'mock-opensandbox-api-key');
vi.stubEnv('AGENT_SANDBOX_PROXY_URL', 'http://localhost:1006'); vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL', 'http://mock-volume-manager.local');
vi.stubEnv('AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN', 'mock-volume-manager-token');
vi.stubEnv('AGENT_SANDBOX_PROXY_SECRET', '');
vi.stubEnv('AGENT_SANDBOX_PROXY_URL', '');
await expect(importServiceEnv()).rejects.toThrow('AGENT_SANDBOX_PROXY_URL'); await expect(importServiceEnv()).resolves.toBeDefined();
}); });
}); });
import { describe, expect, it } from 'vitest';
import { getAgentSandboxMissingRequiredEnvKeys } from '@fastgpt/service/env.util';
describe('env util', () => {
it('requires opensandbox volume manager env when opensandbox provider is enabled', () => {
expect(
getAgentSandboxMissingRequiredEnvKeys({
AGENT_SANDBOX_PROVIDER: 'opensandbox',
AGENT_SANDBOX_OPENSANDBOX_BASEURL: 'http://opensandbox.local',
AGENT_SANDBOX_OPENSANDBOX_API_KEY: 'opensandbox-key'
} as NodeJS.ProcessEnv)
).toEqual([
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL',
'AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN'
]);
});
it('does not require opensandbox volume manager env for other providers', () => {
expect(
getAgentSandboxMissingRequiredEnvKeys({
AGENT_SANDBOX_PROVIDER: 'e2b',
AGENT_SANDBOX_E2B_API_KEY: 'e2b-key'
} as NodeJS.ProcessEnv)
).toEqual([]);
});
});
...@@ -21,9 +21,7 @@ export default defineConfig({ ...@@ -21,9 +21,7 @@ export default defineConfig({
process.env.FILE_TOKEN_KEY ?? process.env.FILE_TOKEN_KEY ??
'bfd697e7e798f75deaf2d31210bc93a2e41ad4eed9e7831071d77821b7b97cff', 'bfd697e7e798f75deaf2d31210bc93a2e41ad4eed9e7831071d77821b7b97cff',
AES256_SECRET_KEY: process.env.AES256_SECRET_KEY ?? 'fastgpt_test_aes256_secret_key', AES256_SECRET_KEY: process.env.AES256_SECRET_KEY ?? 'fastgpt_test_aes256_secret_key',
INVOKE_TOKEN_SECRET: INVOKE_TOKEN_SECRET: process.env.INVOKE_TOKEN_SECRET ?? 'fastgpt_test_invoke_token_secret_32'
process.env.INVOKE_TOKEN_SECRET ?? 'fastgpt_test_invoke_token_secret_32',
AGENT_SANDBOX_PROVIDER: 'opensandbox'
}, },
coverage: { coverage: {
enabled: true, enabled: true,
......
...@@ -169,6 +169,7 @@ ...@@ -169,6 +169,7 @@
"parallel_full_results": "Full Results", "parallel_full_results": "Full Results",
"parallel_full_results_desc": "Array of the same length as the input, each item is {success, message, data}: on success success=true, message is empty, data is the output; on failure success=false, message contains the error, data is null.", "parallel_full_results_desc": "Array of the same length as the input, each item is {success, message, data}: on success success=true, message is empty, data is the output; on failure success=false, message contains the error, data is null.",
"parallel_run": "Parallel Run", "parallel_run": "Parallel Run",
"parallel_run_end_intro": "Select a variable to use as the batch execution result output.",
"parallel_run_execution_logic": "Execution Logic", "parallel_run_execution_logic": "Execution Logic",
"parallel_run_max_concurrency": "Max Concurrency", "parallel_run_max_concurrency": "Max Concurrency",
"parallel_run_max_concurrency_tip": "Maximum number of tasks running in parallel (range: 1 to the upper limit, default 5).", "parallel_run_max_concurrency_tip": "Maximum number of tasks running in parallel (range: 1 to the upper limit, default 5).",
......
...@@ -169,6 +169,7 @@ ...@@ -169,6 +169,7 @@
"parallel_full_results": "完整结果", "parallel_full_results": "完整结果",
"parallel_full_results_desc": "与输入数组等长的结果数组,每项形如 {success, message, data}:成功时 success=true、message 为空、data 为输出值;失败时 success=false、message 为错误信息、data 为 null。", "parallel_full_results_desc": "与输入数组等长的结果数组,每项形如 {success, message, data}:成功时 success=true、message 为空、data 为输出值;失败时 success=false、message 为错误信息、data 为 null。",
"parallel_run": "并行执行", "parallel_run": "并行执行",
"parallel_run_end_intro": "选择变量,作为批量执行的结果输出。",
"parallel_run_execution_logic": "执行逻辑", "parallel_run_execution_logic": "执行逻辑",
"parallel_run_max_concurrency": "最大并发数", "parallel_run_max_concurrency": "最大并发数",
"parallel_run_max_concurrency_tip": "同时并行执行的最大任务数,范围 1~上限值(默认 5)。", "parallel_run_max_concurrency_tip": "同时并行执行的最大任务数,范围 1~上限值(默认 5)。",
......
...@@ -169,6 +169,7 @@ ...@@ -169,6 +169,7 @@
"parallel_full_results": "完整結果", "parallel_full_results": "完整結果",
"parallel_full_results_desc": "與輸入陣列等長的結果陣列,每項形如 {success, message, data}:成功時 success=true、message 為空、data 為輸出值;失敗時 success=false、message 為錯誤訊息、data 為 null。", "parallel_full_results_desc": "與輸入陣列等長的結果陣列,每項形如 {success, message, data}:成功時 success=true、message 為空、data 為輸出值;失敗時 success=false、message 為錯誤訊息、data 為 null。",
"parallel_run": "並行執行", "parallel_run": "並行執行",
"parallel_run_end_intro": "選擇變數,作為批量執行的結果輸出。",
"parallel_run_execution_logic": "執行邏輯", "parallel_run_execution_logic": "執行邏輯",
"parallel_run_max_concurrency": "最大並發數", "parallel_run_max_concurrency": "最大並發數",
"parallel_run_max_concurrency_tip": "同時並行執行的最大任務數,範圍 1~上限值(預設 5)。", "parallel_run_max_concurrency_tip": "同時並行執行的最大任務數,範圍 1~上限值(預設 5)。",
......
Subproject commit 7a41268519c8dab4f1f7fa55edf93869ee507bae Subproject commit 120262a5c3bdf2b0a7b7d16a851ad200dfe6edcb
...@@ -55,10 +55,8 @@ AGENT_SANDBOX_OPENSANDBOX_RUNTIME=docker ...@@ -55,10 +55,8 @@ AGENT_SANDBOX_OPENSANDBOX_RUNTIME=docker
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO=registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO=registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG=v0.1 AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG=v0.1
AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY=true AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY=true
# Volume 持久化配置(opensandbox provider 下可选) AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_URL=http://localhost:3005
AGENT_SANDBOX_ENABLE_VOLUME=true AGENT_SANDBOX_OPENSANDBOX_VOLUME_MANAGER_TOKEN=vmtoken
AGENT_SANDBOX_VOLUME_MANAGER_URL=http://localhost:3005
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN=vmtoken
# E2B 配置(PROVIDER=e2b 时生效) # E2B 配置(PROVIDER=e2b 时生效)
AGENT_SANDBOX_E2B_API_KEY= AGENT_SANDBOX_E2B_API_KEY=
......
...@@ -33,7 +33,8 @@ export async function registerNodeInstrumentation() { ...@@ -33,7 +33,8 @@ export async function registerNodeInstrumentation() {
{ configureLogger, getLogger, LogCategories }, { configureLogger, getLogger, LogCategories },
{ configureMetrics }, { configureMetrics },
{ configureTracing }, { configureTracing },
{ InitialErrorEnum } { InitialErrorEnum },
{ validateAgentSandboxProxyEnv }
] = await Promise.all([ ] = await Promise.all([
import('@fastgpt/service/common/mongo/init'), import('@fastgpt/service/common/mongo/init'),
import('@fastgpt/service/common/mongo/index'), import('@fastgpt/service/common/mongo/index'),
...@@ -55,7 +56,8 @@ export async function registerNodeInstrumentation() { ...@@ -55,7 +56,8 @@ export async function registerNodeInstrumentation() {
import('@fastgpt/service/common/logger'), import('@fastgpt/service/common/logger'),
import('@fastgpt/service/common/metrics'), import('@fastgpt/service/common/metrics'),
import('@fastgpt/service/common/tracing'), import('@fastgpt/service/common/tracing'),
import('@fastgpt/service/common/system/constants') import('@fastgpt/service/common/system/constants'),
import('@fastgpt/service/env.util')
]); ]);
await Promise.all([ await Promise.all([
...@@ -76,6 +78,11 @@ export async function registerNodeInstrumentation() { ...@@ -76,6 +78,11 @@ export async function registerNodeInstrumentation() {
action: () => initGlobalVariables(), action: () => initGlobalVariables(),
logger logger
}); });
await runInitializationStep({
step: 'validate-agent-sandbox-proxy-env',
action: () => validateAgentSandboxProxyEnv(),
logger
});
await Promise.all([ await Promise.all([
runInitializationStep({ runInitializationStep({
......
const formInputSelector = 'input, textarea, select';
const contentEditableSelector = '[contenteditable]';
const getElementFromNode = (target?: EventTarget | Node | null): Element | null => {
if (!target) return null;
const node = target as Node & {
nodeType?: number;
parentElement?: Element | null;
};
if (node.nodeType === 1) {
return node as unknown as Element;
}
return node.parentElement ?? null;
};
const getClassName = (element: Element) => {
const className = (element as { className?: unknown }).className;
if (typeof className === 'string') return className.toLowerCase();
if (
className &&
typeof className === 'object' &&
typeof (className as { baseVal?: unknown }).baseVal === 'string'
) {
return (className as { baseVal: string }).baseVal.toLowerCase();
}
return '';
};
const isEditableElement = (target?: EventTarget | Node | null) => {
const element = getElementFromNode(target);
if (!element) return false;
if (element.closest(formInputSelector)) return true;
const contentEditableElement = element.closest(contentEditableSelector);
if (contentEditableElement) {
const editableValue = contentEditableElement.getAttribute('contenteditable');
if (editableValue !== 'false') return true;
}
const className = getClassName(element);
return className.includes('prompteditor') || className.includes('contenteditable');
};
/**
* 判断工作流画布快捷键是否应让文本编辑区优先处理。
* ahooks 的全局 keydown 回调在 Lexical/contenteditable 选区场景下可能拿到 body 作为
* target,因此需要同时检查事件目标、当前焦点和 Selection 锚点。
*/
export const isWorkflowShortcutInputtingTarget = (target?: EventTarget | Node | null) => {
if (isEditableElement(target)) return true;
if (typeof document !== 'undefined' && isEditableElement(document.activeElement)) return true;
const selection =
typeof window !== 'undefined' && typeof window.getSelection === 'function'
? window.getSelection()
: null;
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return false;
return isEditableElement(selection.anchorNode) || isEditableElement(selection.focusNode);
};
...@@ -12,6 +12,7 @@ import { WorkflowBufferDataContext } from '../../context/workflowInitContext'; ...@@ -12,6 +12,7 @@ import { WorkflowBufferDataContext } from '../../context/workflowInitContext';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { WorkflowUIContext } from '../../context/workflowUIContext'; import { WorkflowUIContext } from '../../context/workflowUIContext';
import { isWorkflowShortcutInputtingTarget } from './keyboard';
export const useKeyboard = () => { export const useKeyboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -31,17 +32,8 @@ export const useKeyboard = () => { ...@@ -31,17 +32,8 @@ export const useKeyboard = () => {
const isDowningCtrl = useKeyPress(['Meta', 'Control']); const isDowningCtrl = useKeyPress(['Meta', 'Control']);
const hasInputtingElement = useCallback(() => { const hasInputtingElement = useCallback((event?: KeyboardEvent) => {
const activeElement = document.activeElement; return isWorkflowShortcutInputtingTarget(event?.target);
if (activeElement) {
const tagName = activeElement.tagName.toLowerCase();
const className = activeElement.className.toLowerCase();
if (tagName === 'input' || tagName === 'textarea') return true;
if (className.includes('prompteditor')) return true;
}
return false;
}, []); }, []);
const onCopy = useCallback(async () => { const onCopy = useCallback(async () => {
...@@ -123,7 +115,7 @@ export const useKeyboard = () => { ...@@ -123,7 +115,7 @@ export const useKeyboard = () => {
//@ts-ignore //@ts-ignore
.concat(newNodes) .concat(newNodes)
); );
} catch (error) {} } catch {}
}, [ }, [
computedNewNodeName, computedNewNodeName,
hasInputtingElement, hasInputtingElement,
...@@ -136,10 +128,12 @@ export const useKeyboard = () => { ...@@ -136,10 +128,12 @@ export const useKeyboard = () => {
useKeyPressEffect(['ctrl.c', 'meta.c'], (e) => { useKeyPressEffect(['ctrl.c', 'meta.c'], (e) => {
if (!mouseInCanvas) return; if (!mouseInCanvas) return;
if (hasInputtingElement(e)) return;
onCopy(); onCopy();
}); });
useKeyPressEffect(['ctrl.v', 'meta.v'], (e) => { useKeyPressEffect(['ctrl.v', 'meta.v'], (e) => {
if (!mouseInCanvas) return; if (!mouseInCanvas) return;
if (hasInputtingElement(e)) return;
onPaste(); onPaste();
}); });
useKeyPressEffect(['ctrl.s', 'meta.s'], (e) => { useKeyPressEffect(['ctrl.s', 'meta.s'], (e) => {
......
...@@ -41,9 +41,9 @@ const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => { ...@@ -41,9 +41,9 @@ const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const parallelRunIntro = useMemoEnhance(() => { const parallelRunIntro = useMemoEnhance(() => {
const parentNode = getNodeById(parentNodeId); const parentNode = getNodeById(parentNodeId);
return parentNode?.flowNodeType === FlowNodeTypeEnum.parallelRun return parentNode?.flowNodeType === FlowNodeTypeEnum.parallelRun
? 'workflow:parallel_run_end_intro' ? t('workflow:parallel_run_end_intro')
: undefined; : undefined;
}, [getNodeById, parentNodeId]); }, [getNodeById, parentNodeId, t]);
// Get loopEnd input value type // Get loopEnd input value type
const valueType = useMemo(() => { const valueType = useMemo(() => {
......
...@@ -56,10 +56,6 @@ export const getSandboxProxyWsUrl = ({ ...@@ -56,10 +56,6 @@ export const getSandboxProxyWsUrl = ({
const { agentSandboxProxyUrl = '' } = useSystemStore.getState().feConfigs; const { agentSandboxProxyUrl = '' } = useSystemStore.getState().feConfigs;
const proxyBaseUrl = agentSandboxProxyUrl.replace(/\/+$/, ''); const proxyBaseUrl = agentSandboxProxyUrl.replace(/\/+$/, '');
if (!proxyBaseUrl) {
throw new Error('AGENT_SANDBOX_PROXY_URL is required but not configured');
}
return `${proxyBaseUrl}/${channel}?ticket=${encodeURIComponent(ticket)}`; return `${proxyBaseUrl}/${channel}?ticket=${encodeURIComponent(ticket)}`;
}; };
......
...@@ -15,6 +15,7 @@ import z from 'zod'; ...@@ -15,6 +15,7 @@ import z from 'zod';
* ============================================================================ */ * ============================================================================ */
const DEFAULT_SAMPLE_LIMIT = 20; const DEFAULT_SAMPLE_LIMIT = 20;
const CLEANUP_DUPLICATE_CHATS_INDEX_NAME = 'idx_cleanup_duplicate_chats_appId_chatId';
const CleanupDuplicateChatsBodySchema = z const CleanupDuplicateChatsBodySchema = z
.object({ .object({
...@@ -69,9 +70,7 @@ const CleanupDuplicateChatsResponseSchema = z.object({ ...@@ -69,9 +70,7 @@ const CleanupDuplicateChatsResponseSchema = z.object({
sampleLimit: z.number().int().nonnegative().meta({ description: '返回样本数量限制' }), sampleLimit: z.number().int().nonnegative().meta({ description: '返回样本数量限制' }),
samples: z.array(DuplicateChatGroupSampleSchema).meta({ description: '重复组样本' }) samples: z.array(DuplicateChatGroupSampleSchema).meta({ description: '重复组样本' })
}); });
export type CleanupDuplicateChatsResponseType = z.infer< export type CleanupDuplicateChatsResponseType = z.infer<typeof CleanupDuplicateChatsResponseSchema>;
typeof CleanupDuplicateChatsResponseSchema
>;
type DuplicateKeyGroup = { type DuplicateKeyGroup = {
_id: { _id: {
...@@ -93,6 +92,30 @@ const stringifyId = (value: unknown) => { ...@@ -93,6 +92,30 @@ const stringifyId = (value: unknown) => {
return String(value); return String(value);
}; };
/**
* 为重复会话头清理创建临时查询索引。
*
* 历史库里唯一索引可能因为重复数据没有建成功,迁移前先补一个非唯一索引,
* 避免全表扫描;如果已经存在同 key 索引(无论唯一/非唯一),直接复用。
*/
const ensureDuplicateChatCleanupIndex = async () => {
const indexes = await MongoChat.collection.indexes();
const hasAppIdChatIdIndex = indexes.some((index) => {
const keys = Object.keys(index.key);
return keys.length === 2 && index.key.appId === 1 && index.key.chatId === 1;
});
if (hasAppIdChatIdIndex) return;
await MongoChat.collection.createIndex(
{ appId: 1, chatId: 1 },
{
name: CLEANUP_DUPLICATE_CHATS_INDEX_NAME,
background: true
}
);
};
const findDuplicateChatGroups = () => const findDuplicateChatGroups = () =>
MongoChat.aggregate<DuplicateKeyGroup>( MongoChat.aggregate<DuplicateKeyGroup>(
[ [
...@@ -129,6 +152,8 @@ const findDuplicateChatDocs = (group: DuplicateKeyGroup) => ...@@ -129,6 +152,8 @@ const findDuplicateChatDocs = (group: DuplicateKeyGroup) =>
export async function runCleanupDuplicateChatsMigration( export async function runCleanupDuplicateChatsMigration(
params: CleanupDuplicateChatsBodyType params: CleanupDuplicateChatsBodyType
): Promise<CleanupDuplicateChatsResponseType> { ): Promise<CleanupDuplicateChatsResponseType> {
await ensureDuplicateChatCleanupIndex();
const duplicateGroups = await findDuplicateChatGroups(); const duplicateGroups = await findDuplicateChatGroups();
let duplicateDocumentCount = 0; let duplicateDocumentCount = 0;
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
ChatItemResponseCollectionName ChatItemResponseCollectionName
} from '@fastgpt/service/core/chat/constants'; } from '@fastgpt/service/core/chat/constants';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { type ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum, type ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants'; import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants';
import { sanitizeCsvField } from '@fastgpt/service/common/file/csv'; import { sanitizeCsvField } from '@fastgpt/service/common/file/csv';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
...@@ -31,6 +31,10 @@ import { ExportChatLogsBodySchema } from '@fastgpt/global/openapi/core/app/log/a ...@@ -31,6 +31,10 @@ import { ExportChatLogsBodySchema } from '@fastgpt/global/openapi/core/app/log/a
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
const logger = getLogger(LogCategories.MODULE.APP.LOGS); const logger = getLogger(LogCategories.MODULE.APP.LOGS);
const appChatSourceMatch = {
$or: [{ sourceType: ChatSourceTypeEnum.app }, { sourceType: { $exists: false } }]
};
const formatJsonString = (data: any) => { const formatJsonString = (data: any) => {
if (data == null) return ''; if (data == null) return '';
if (typeof data === 'object') { if (typeof data === 'object') {
...@@ -99,6 +103,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) { ...@@ -99,6 +103,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) {
const where = { const where = {
appId: new Types.ObjectId(appId), appId: new Types.ObjectId(appId),
$and: [appChatSourceMatch],
// Feedback type filtering (BEFORE pagination for performance) // Feedback type filtering (BEFORE pagination for performance)
...(feedbackType === 'has_feedback' && ...(feedbackType === 'has_feedback' &&
!unreadOnly && { !unreadOnly && {
...@@ -174,7 +179,16 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) { ...@@ -174,7 +179,16 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) {
{ {
$match: { $match: {
$expr: { $expr: {
$and: [{ $eq: ['$appId', '$$appId'] }, { $eq: ['$chatId', '$$chatId'] }] $and: [
{ $eq: ['$appId', '$$appId'] },
{ $eq: ['$chatId', '$$chatId'] },
{
$or: [
{ $eq: ['$sourceType', ChatSourceTypeEnum.app] },
{ $eq: [{ $type: '$sourceType' }, 'missing'] }
]
}
]
} }
} }
}, },
...@@ -248,7 +262,16 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) { ...@@ -248,7 +262,16 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) {
{ {
$match: { $match: {
$expr: { $expr: {
$and: [{ $eq: ['$appId', '$$appId'] }, { $eq: ['$chatId', '$$chatId'] }] $and: [
{ $eq: ['$appId', '$$appId'] },
{ $eq: ['$chatId', '$$chatId'] },
{
$or: [
{ $eq: ['$sourceType', ChatSourceTypeEnum.app] },
{ $eq: [{ $type: '$sourceType' }, 'missing'] }
]
}
]
} }
} }
}, },
......
...@@ -16,6 +16,11 @@ import { ...@@ -16,6 +16,11 @@ import {
} from '@fastgpt/global/openapi/core/app/log/api'; } from '@fastgpt/global/openapi/core/app/log/api';
import { DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; import { DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
const appChatSourceMatch = {
$or: [{ sourceType: ChatSourceTypeEnum.app }, { sourceType: { $exists: false } }]
};
async function handler(req: ApiRequestProps): Promise<GetLogUsersResponse> { async function handler(req: ApiRequestProps): Promise<GetLogUsersResponse> {
const { const {
...@@ -42,6 +47,7 @@ async function handler(req: ApiRequestProps): Promise<GetLogUsersResponse> { ...@@ -42,6 +47,7 @@ async function handler(req: ApiRequestProps): Promise<GetLogUsersResponse> {
{ {
$match: { $match: {
appId: new Types.ObjectId(appId), appId: new Types.ObjectId(appId),
...appChatSourceMatch,
updateTime: { updateTime: {
$gte: new Date(dateStart), $gte: new Date(dateStart),
$lte: new Date(dateEnd) $lte: new Date(dateEnd)
......
...@@ -22,6 +22,11 @@ import { ...@@ -22,6 +22,11 @@ import {
type getAppChatLogsResponseType type getAppChatLogsResponseType
} from '@fastgpt/global/openapi/core/app/log/api'; } from '@fastgpt/global/openapi/core/app/log/api';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
const appChatSourceMatch = {
$or: [{ sourceType: ChatSourceTypeEnum.app }, { sourceType: { $exists: false } }]
};
async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType> { async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType> {
const { const {
...@@ -56,6 +61,7 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType ...@@ -56,6 +61,7 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType
const where = { const where = {
appId: new Types.ObjectId(appId), appId: new Types.ObjectId(appId),
$and: [appChatSourceMatch],
// Feedback type filtering (BEFORE pagination for performance) // Feedback type filtering (BEFORE pagination for performance)
...(feedbackType === 'has_feedback' && ...(feedbackType === 'has_feedback' &&
!unreadOnly && { !unreadOnly && {
...@@ -132,7 +138,16 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType ...@@ -132,7 +138,16 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType
{ {
$match: { $match: {
$expr: { $expr: {
$and: [{ $eq: ['$appId', '$$appId'] }, { $eq: ['$chatId', '$$chatId'] }] $and: [
{ $eq: ['$appId', '$$appId'] },
{ $eq: ['$chatId', '$$chatId'] },
{
$or: [
{ $eq: ['$sourceType', ChatSourceTypeEnum.app] },
{ $eq: [{ $type: '$sourceType' }, 'missing'] }
]
}
]
} }
} }
}, },
...@@ -192,7 +207,16 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType ...@@ -192,7 +207,16 @@ async function handler(req: ApiRequestProps): Promise<getAppChatLogsResponseType
{ {
$match: { $match: {
$expr: { $expr: {
$and: [{ $eq: ['$appId', '$$appId'] }, { $eq: ['$chatId', '$$chatId'] }] $and: [
{ $eq: ['$appId', '$$appId'] },
{ $eq: ['$chatId', '$$chatId'] },
{
$or: [
{ $eq: ['$sourceType', ChatSourceTypeEnum.app] },
{ $eq: [{ $type: '$sourceType' }, 'missing'] }
]
}
]
} }
} }
}, },
......
...@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({ ...@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
getAgentSandboxMaxFileBytes: vi.fn(), getAgentSandboxMaxFileBytes: vi.fn(),
getReadStream: vi.fn(), getReadStream: vi.fn(),
getSandboxClient: vi.fn(), getSandboxClient: vi.fn(),
getSandboxRuntimeProfile: vi.fn(),
resolveFormData: vi.fn(), resolveFormData: vi.fn(),
writeFiles: vi.fn() writeFiles: vi.fn()
})); }));
...@@ -38,6 +39,10 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/runtime', () => ({ ...@@ -38,6 +39,10 @@ vi.mock('@fastgpt/service/core/ai/sandbox/interface/runtime', () => ({
getSandboxClient: mocks.getSandboxClient getSandboxClient: mocks.getSandboxClient
})); }));
vi.mock('@fastgpt/service/core/ai/sandbox/infrastructure/provider/runtimeProfile', () => ({
getSandboxRuntimeProfile: mocks.getSandboxRuntimeProfile
}));
import handler from '@/pages/api/core/ai/sandbox/upload'; import handler from '@/pages/api/core/ai/sandbox/upload';
const createReq = () => const createReq = () =>
...@@ -51,6 +56,7 @@ describe('sandbox upload API', () => { ...@@ -51,6 +56,7 @@ describe('sandbox upload API', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.getAgentSandboxMaxFileBytes.mockReturnValue(10 * 1024 * 1024); mocks.getAgentSandboxMaxFileBytes.mockReturnValue(10 * 1024 * 1024);
mocks.getSandboxRuntimeProfile.mockReturnValue({ workDirectory: '/workspace' });
mocks.getReadStream.mockReturnValue(Readable.from([new Uint8Array([1, 2, 3])])); mocks.getReadStream.mockReturnValue(Readable.from([new Uint8Array([1, 2, 3])]));
mocks.resolveFormData.mockResolvedValue({ mocks.resolveFormData.mockResolvedValue({
data: { data: {
......
...@@ -21,7 +21,7 @@ import { MongoResourcePermission } from '@fastgpt/service/support/permission/sch ...@@ -21,7 +21,7 @@ import { MongoResourcePermission } from '@fastgpt/service/support/permission/sch
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema'; import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { MongoSystemTool } from '@fastgpt/service/core/plugin/tool/systemToolSchema'; import { MongoSystemTool } from '@fastgpt/service/core/plugin/tool/systemToolSchema';
// Mock dependencies for queue functionality // Mock dependencies for queue functionality
...@@ -45,10 +45,9 @@ vi.mock('@fastgpt/service/common/file/image/controller', () => ({ ...@@ -45,10 +45,9 @@ vi.mock('@fastgpt/service/common/file/image/controller', () => ({
})); }));
// Import mocked modules for type access // Import mocked modules for type access
import { getQueue, getWorker, QueueNames } from '@fastgpt/service/common/bullmq'; import { getQueue, QueueNames } from '@fastgpt/service/common/bullmq';
const mockGetQueue = vi.mocked(getQueue); const mockGetQueue = vi.mocked(getQueue);
const mockGetWorker = vi.mocked(getWorker);
describe('App Delete Queue', () => { describe('App Delete Queue', () => {
beforeEach(() => { beforeEach(() => {
...@@ -464,6 +463,7 @@ describe('App Delete Data Cleanup Verification', () => { ...@@ -464,6 +463,7 @@ describe('App Delete Data Cleanup Verification', () => {
appId: appId, appId: appId,
teamId: teamId, teamId: teamId,
tmbId: rootUser.tmbId, tmbId: rootUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
chatId: `test-chat-${timestamp}`, chatId: `test-chat-${timestamp}`,
title: 'Test Chat', title: 'Test Chat',
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
...@@ -477,6 +477,7 @@ describe('App Delete Data Cleanup Verification', () => { ...@@ -477,6 +477,7 @@ describe('App Delete Data Cleanup Verification', () => {
appId: appId, appId: appId,
teamId: teamId, teamId: teamId,
tmbId: rootUser.tmbId, tmbId: rootUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
chatId: `test-chat-${timestamp}`, chatId: `test-chat-${timestamp}`,
time: timestamp, time: timestamp,
obj: 'Human', obj: 'Human',
...@@ -490,6 +491,7 @@ describe('App Delete Data Cleanup Verification', () => { ...@@ -490,6 +491,7 @@ describe('App Delete Data Cleanup Verification', () => {
appId: appId, appId: appId,
teamId: teamId, teamId: teamId,
tmbId: rootUser.tmbId, tmbId: rootUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
chatItemId: `test-chat-item-${timestamp}`, chatItemId: `test-chat-item-${timestamp}`,
time: timestamp, time: timestamp,
text: 'This is a test response', text: 'This is a test response',
......
...@@ -12,6 +12,7 @@ import type { ...@@ -12,6 +12,7 @@ import type {
GetLogUsersBody, GetLogUsersBody,
GetLogUsersResponse GetLogUsersResponse
} from '@fastgpt/global/openapi/core/app/log/api'; } from '@fastgpt/global/openapi/core/app/log/api';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
type EmptyQuery = Record<string, never>; type EmptyQuery = Record<string, never>;
...@@ -99,6 +100,7 @@ describe('getUsers API', () => { ...@@ -99,6 +100,7 @@ describe('getUsers API', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat 1' title: 'Chat 1'
...@@ -108,6 +110,7 @@ describe('getUsers API', () => { ...@@ -108,6 +110,7 @@ describe('getUsers API', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat 2' title: 'Chat 2'
...@@ -118,6 +121,7 @@ describe('getUsers API', () => { ...@@ -118,6 +121,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'external-user-1', outLinkUid: 'external-user-1',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Chat 3' title: 'Chat 3'
...@@ -175,6 +179,7 @@ describe('getUsers API', () => { ...@@ -175,6 +179,7 @@ describe('getUsers API', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: teamMember2._id, tmbId: teamMember2._id,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat Search 1' title: 'Chat Search 1'
...@@ -185,6 +190,7 @@ describe('getUsers API', () => { ...@@ -185,6 +190,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'alice-user', outLinkUid: 'alice-user',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Chat Search 2' title: 'Chat Search 2'
...@@ -238,6 +244,7 @@ describe('getUsers API', () => { ...@@ -238,6 +244,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'user-a', outLinkUid: 'user-a',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Sort 1' title: 'Sort 1'
...@@ -248,6 +255,7 @@ describe('getUsers API', () => { ...@@ -248,6 +255,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'user-a', outLinkUid: 'user-a',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Sort 2' title: 'Sort 2'
...@@ -258,6 +266,7 @@ describe('getUsers API', () => { ...@@ -258,6 +266,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'user-a', outLinkUid: 'user-a',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Sort 3' title: 'Sort 3'
...@@ -269,6 +278,7 @@ describe('getUsers API', () => { ...@@ -269,6 +278,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'user-b', outLinkUid: 'user-b',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Sort 4' title: 'Sort 4'
...@@ -308,6 +318,7 @@ describe('getUsers API', () => { ...@@ -308,6 +318,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'online-user', outLinkUid: 'online-user',
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Online Chat' title: 'Online Chat'
...@@ -318,6 +329,7 @@ describe('getUsers API', () => { ...@@ -318,6 +329,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'share-user', outLinkUid: 'share-user',
sourceType: ChatSourceTypeEnum.app,
source: 'share', source: 'share',
updateTime: now, updateTime: now,
title: 'Share Chat' title: 'Share Chat'
...@@ -328,6 +340,7 @@ describe('getUsers API', () => { ...@@ -328,6 +340,7 @@ describe('getUsers API', () => {
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
outLinkUid: 'api-user', outLinkUid: 'api-user',
sourceType: ChatSourceTypeEnum.app,
source: 'api', source: 'api',
updateTime: now, updateTime: now,
title: 'API Chat' title: 'API Chat'
......
...@@ -13,7 +13,7 @@ import type { ...@@ -13,7 +13,7 @@ import type {
getAppChatLogsBody, getAppChatLogsBody,
getAppChatLogsResponseType getAppChatLogsResponseType
} from '@fastgpt/global/openapi/core/app/log/api'; } from '@fastgpt/global/openapi/core/app/log/api';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
type EmptyQuery = Record<string, never>; type EmptyQuery = Record<string, never>;
...@@ -83,6 +83,7 @@ describe('logs list API - errorFilter', () => { ...@@ -83,6 +83,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat without error', title: 'Chat without error',
...@@ -94,6 +95,7 @@ describe('logs list API - errorFilter', () => { ...@@ -94,6 +95,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat with error', title: 'Chat with error',
...@@ -106,6 +108,7 @@ describe('logs list API - errorFilter', () => { ...@@ -106,6 +108,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-all-1', chatId: 'chat-all-1',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Normal response' } }], value: [{ text: { content: 'Normal response' } }],
...@@ -115,6 +118,7 @@ describe('logs list API - errorFilter', () => { ...@@ -115,6 +118,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-all-2', chatId: 'chat-all-2',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Error response' } }], value: [{ text: { content: 'Error response' } }],
...@@ -163,6 +167,7 @@ describe('logs list API - errorFilter', () => { ...@@ -163,6 +167,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat without error', title: 'Chat without error',
...@@ -173,6 +178,7 @@ describe('logs list API - errorFilter', () => { ...@@ -173,6 +178,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Chat with error', title: 'Chat with error',
...@@ -183,6 +189,7 @@ describe('logs list API - errorFilter', () => { ...@@ -183,6 +189,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'Another chat without error', title: 'Another chat without error',
...@@ -196,6 +203,7 @@ describe('logs list API - errorFilter', () => { ...@@ -196,6 +203,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-error-filter-1', chatId: 'chat-error-filter-1',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Normal response' } }], value: [{ text: { content: 'Normal response' } }],
...@@ -205,6 +213,7 @@ describe('logs list API - errorFilter', () => { ...@@ -205,6 +213,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-error-filter-2', chatId: 'chat-error-filter-2',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Error response' } }], value: [{ text: { content: 'Error response' } }],
...@@ -222,6 +231,7 @@ describe('logs list API - errorFilter', () => { ...@@ -222,6 +231,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-error-filter-3', chatId: 'chat-error-filter-3',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Another normal response' } }], value: [{ text: { content: 'Another normal response' } }],
...@@ -268,6 +278,7 @@ describe('logs list API - errorFilter', () => { ...@@ -268,6 +278,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: new Date(now.getTime() - i * 1000), updateTime: new Date(now.getTime() - i * 1000),
title: `Chat with error ${i}`, title: `Chat with error ${i}`,
...@@ -281,6 +292,7 @@ describe('logs list API - errorFilter', () => { ...@@ -281,6 +292,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: new Date(now.getTime() - (i + 5) * 1000), updateTime: new Date(now.getTime() - (i + 5) * 1000),
title: `Chat without error ${i}`, title: `Chat without error ${i}`,
...@@ -295,6 +307,7 @@ describe('logs list API - errorFilter', () => { ...@@ -295,6 +307,7 @@ describe('logs list API - errorFilter', () => {
chatId: chat.chatId, chatId: chat.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Error response' } }], value: [{ text: { content: 'Error response' } }],
...@@ -313,6 +326,7 @@ describe('logs list API - errorFilter', () => { ...@@ -313,6 +326,7 @@ describe('logs list API - errorFilter', () => {
chatId: chat.chatId, chatId: chat.chatId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Normal response' } }], value: [{ text: { content: 'Normal response' } }],
...@@ -376,6 +390,7 @@ describe('logs list API - errorFilter', () => { ...@@ -376,6 +390,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'User 1 with error', title: 'User 1 with error',
...@@ -387,6 +402,7 @@ describe('logs list API - errorFilter', () => { ...@@ -387,6 +402,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'User 1 without error', title: 'User 1 without error',
...@@ -398,6 +414,7 @@ describe('logs list API - errorFilter', () => { ...@@ -398,6 +414,7 @@ describe('logs list API - errorFilter', () => {
appId: testAppId, appId: testAppId,
teamId: testTeamId, teamId: testTeamId,
tmbId: teamMember2._id, tmbId: teamMember2._id,
sourceType: ChatSourceTypeEnum.app,
source: 'online', source: 'online',
updateTime: now, updateTime: now,
title: 'User 2 with error', title: 'User 2 with error',
...@@ -411,6 +428,7 @@ describe('logs list API - errorFilter', () => { ...@@ -411,6 +428,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-user-error-1', chatId: 'chat-user-error-1',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Error' } }], value: [{ text: { content: 'Error' } }],
...@@ -428,6 +446,7 @@ describe('logs list API - errorFilter', () => { ...@@ -428,6 +446,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-user-error-2', chatId: 'chat-user-error-2',
teamId: testTeamId, teamId: testTeamId,
tmbId: testTmbId, tmbId: testTmbId,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Normal' } }], value: [{ text: { content: 'Normal' } }],
...@@ -437,6 +456,7 @@ describe('logs list API - errorFilter', () => { ...@@ -437,6 +456,7 @@ describe('logs list API - errorFilter', () => {
chatId: 'chat-user-error-3', chatId: 'chat-user-error-3',
teamId: testTeamId, teamId: testTeamId,
tmbId: teamMember2._id, tmbId: teamMember2._id,
sourceType: ChatSourceTypeEnum.app,
appId: testAppId, appId: testAppId,
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,
value: [{ text: { content: 'Error' } }], value: [{ text: { content: 'Error' } }],
......
...@@ -4,7 +4,11 @@ import { ...@@ -4,7 +4,11 @@ import {
type AdminUpdateFeedbackResponseType type AdminUpdateFeedbackResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import {
ChatRoleEnum,
ChatSourceEnum,
ChatSourceTypeEnum
} from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -13,6 +17,8 @@ import { getUser } from '@test/datas/users'; ...@@ -13,6 +17,8 @@ import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request'; import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest'; import { describe, expect, it, beforeEach } from 'vitest';
type EmptyQuery = Record<string, never>;
describe('adminUpdate api test', () => { describe('adminUpdate api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>; let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string; let appId: string;
...@@ -38,6 +44,7 @@ describe('adminUpdate api test', () => { ...@@ -38,6 +44,7 @@ describe('adminUpdate api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -48,6 +55,7 @@ describe('adminUpdate api test', () => { ...@@ -48,6 +55,7 @@ describe('adminUpdate api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
...@@ -69,21 +77,22 @@ describe('adminUpdate api test', () => { ...@@ -69,21 +77,22 @@ describe('adminUpdate api test', () => {
const q = 'What is AI?'; const q = 'What is AI?';
const a = 'AI stands for Artificial Intelligence'; const a = 'AI stands for Artificial Intelligence';
const res = await Call<AdminUpdateFeedbackBodyType, {}, AdminUpdateFeedbackResponseType>( const res = await Call<
handler, AdminUpdateFeedbackBodyType,
{ EmptyQuery,
auth: testUser, AdminUpdateFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
datasetId, chatId,
feedbackDataId, dataId,
q, datasetId,
a feedbackDataId,
} q,
a
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -105,21 +114,22 @@ describe('adminUpdate api test', () => { ...@@ -105,21 +114,22 @@ describe('adminUpdate api test', () => {
it('should fail when user does not have permission', async () => { it('should fail when user does not have permission', async () => {
const unauthorizedUser = await getUser('unauthorized-user-admin'); const unauthorizedUser = await getUser('unauthorized-user-admin');
const res = await Call<AdminUpdateFeedbackBodyType, {}, AdminUpdateFeedbackResponseType>( const res = await Call<
handler, AdminUpdateFeedbackBodyType,
{ EmptyQuery,
auth: unauthorizedUser, AdminUpdateFeedbackResponseType
body: { >(handler, {
appId, auth: unauthorizedUser,
chatId, body: {
dataId, appId,
datasetId: getNanoid(), chatId,
feedbackDataId: getNanoid(), dataId,
q: 'test', datasetId: getNanoid(),
a: 'test' feedbackDataId: getNanoid(),
} q: 'test',
a: 'test'
} }
); });
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
......
...@@ -4,7 +4,11 @@ import { ...@@ -4,7 +4,11 @@ import {
type CloseCustomFeedbackResponseType type CloseCustomFeedbackResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import {
ChatRoleEnum,
ChatSourceEnum,
ChatSourceTypeEnum
} from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -13,6 +17,8 @@ import { getUser } from '@test/datas/users'; ...@@ -13,6 +17,8 @@ import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request'; import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest'; import { describe, expect, it, beforeEach } from 'vitest';
type EmptyQuery = Record<string, never>;
describe.sequential('closeCustom api test', () => { describe.sequential('closeCustom api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>; let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string; let appId: string;
...@@ -38,6 +44,7 @@ describe.sequential('closeCustom api test', () => { ...@@ -38,6 +44,7 @@ describe.sequential('closeCustom api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -48,6 +55,7 @@ describe.sequential('closeCustom api test', () => { ...@@ -48,6 +55,7 @@ describe.sequential('closeCustom api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
...@@ -65,18 +73,19 @@ describe.sequential('closeCustom api test', () => { ...@@ -65,18 +73,19 @@ describe.sequential('closeCustom api test', () => {
}); });
it('should close custom feedback successfully', async () => { it('should close custom feedback successfully', async () => {
const res = await Call<CloseCustomFeedbackBodyType, {}, CloseCustomFeedbackResponseType>( const res = await Call<
handler, CloseCustomFeedbackBodyType,
{ EmptyQuery,
auth: testUser, CloseCustomFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
index: 1 chatId,
} dataId,
index: 1
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
expect(res.error).toBeUndefined(); expect(res.error).toBeUndefined();
...@@ -96,36 +105,38 @@ describe.sequential('closeCustom api test', () => { ...@@ -96,36 +105,38 @@ describe.sequential('closeCustom api test', () => {
it('should fail when user does not have permission', async () => { it('should fail when user does not have permission', async () => {
const unauthorizedUser = await getUser(`unauthorized-user-close-${Math.random()}`); const unauthorizedUser = await getUser(`unauthorized-user-close-${Math.random()}`);
const res = await Call<CloseCustomFeedbackBodyType, {}, CloseCustomFeedbackResponseType>( const res = await Call<
handler, CloseCustomFeedbackBodyType,
{ EmptyQuery,
auth: unauthorizedUser, CloseCustomFeedbackResponseType
body: { >(handler, {
appId, auth: unauthorizedUser,
chatId, body: {
dataId, appId,
index: 0 chatId,
} dataId,
index: 0
} }
); });
expect(res.code).toBe(500); expect(res.code).toBe(500);
expect(res.error).toBeDefined(); expect(res.error).toBeDefined();
}); });
it('should handle closing first feedback', async () => { it('should handle closing first feedback', async () => {
const res = await Call<CloseCustomFeedbackBodyType, {}, CloseCustomFeedbackResponseType>( const res = await Call<
handler, CloseCustomFeedbackBodyType,
{ EmptyQuery,
auth: testUser, CloseCustomFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
index: 0 chatId,
} dataId,
index: 0
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
...@@ -140,18 +151,19 @@ describe.sequential('closeCustom api test', () => { ...@@ -140,18 +151,19 @@ describe.sequential('closeCustom api test', () => {
}); });
it('should handle closing last feedback', async () => { it('should handle closing last feedback', async () => {
const res = await Call<CloseCustomFeedbackBodyType, {}, CloseCustomFeedbackResponseType>( const res = await Call<
handler, CloseCustomFeedbackBodyType,
{ EmptyQuery,
auth: testUser, CloseCustomFeedbackResponseType
body: { >(handler, {
appId, auth: testUser,
chatId, body: {
dataId, appId,
index: 2 chatId,
} dataId,
index: 2
} }
); });
expect(res.code).toBe(200); expect(res.code).toBe(200);
......
...@@ -40,6 +40,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -40,6 +40,7 @@ describe('getFeedbackRecordIds api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -51,6 +52,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -51,6 +52,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-1', dataId: 'data-1',
...@@ -65,6 +67,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -65,6 +67,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-2', dataId: 'data-2',
...@@ -79,6 +82,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -79,6 +82,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-3', dataId: 'data-3',
...@@ -93,6 +97,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -93,6 +97,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-4', dataId: 'data-4',
...@@ -107,6 +112,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -107,6 +112,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-5', dataId: 'data-5',
...@@ -119,6 +125,7 @@ describe('getFeedbackRecordIds api test', () => { ...@@ -119,6 +125,7 @@ describe('getFeedbackRecordIds api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: 'data-6', dataId: 'data-6',
......
...@@ -4,7 +4,11 @@ import { ...@@ -4,7 +4,11 @@ import {
type UpdateFeedbackReadStatusResponseType type UpdateFeedbackReadStatusResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import {
ChatRoleEnum,
ChatSourceEnum,
ChatSourceTypeEnum
} from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -39,6 +43,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -39,6 +43,7 @@ describe('updateFeedbackReadStatus api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -49,6 +54,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -49,6 +54,7 @@ describe('updateFeedbackReadStatus api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
...@@ -156,6 +162,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -156,6 +162,7 @@ describe('updateFeedbackReadStatus api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: sharedDataId, dataId: sharedDataId,
...@@ -175,6 +182,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -175,6 +182,7 @@ describe('updateFeedbackReadStatus api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: sharedDataId, dataId: sharedDataId,
...@@ -232,6 +240,7 @@ describe('updateFeedbackReadStatus api test', () => { ...@@ -232,6 +240,7 @@ describe('updateFeedbackReadStatus api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: humanDataId, dataId: humanDataId,
......
...@@ -4,7 +4,11 @@ import { ...@@ -4,7 +4,11 @@ import {
type UpdateUserFeedbackResponseType type UpdateUserFeedbackResponseType
} from '@fastgpt/global/openapi/core/chat/feedback/api'; } from '@fastgpt/global/openapi/core/chat/feedback/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import {
ChatRoleEnum,
ChatSourceEnum,
ChatSourceTypeEnum
} from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema'; import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
...@@ -39,6 +43,7 @@ describe('updateUserFeedback api test', () => { ...@@ -39,6 +43,7 @@ describe('updateUserFeedback api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -63,6 +68,7 @@ describe('updateUserFeedback api test', () => { ...@@ -63,6 +68,7 @@ describe('updateUserFeedback api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
...@@ -291,6 +297,7 @@ describe('updateUserFeedback api test', () => { ...@@ -291,6 +297,7 @@ describe('updateUserFeedback api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
......
...@@ -63,6 +63,7 @@ describe('batchDelete api test', () => { ...@@ -63,6 +63,7 @@ describe('batchDelete api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
...@@ -78,6 +79,7 @@ describe('batchDelete api test', () => { ...@@ -78,6 +79,7 @@ describe('batchDelete api test', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -100,6 +102,7 @@ describe('batchDelete api test', () => { ...@@ -100,6 +102,7 @@ describe('batchDelete api test', () => {
MongoChatItemResponse.create({ MongoChatItemResponse.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -411,6 +414,7 @@ describe('batchDelete api test', () => { ...@@ -411,6 +414,7 @@ describe('batchDelete api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId: otherAppId, appId: otherAppId,
chatId: otherChatId, chatId: otherChatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
...@@ -452,6 +456,7 @@ describe('batchDelete api test', () => { ...@@ -452,6 +456,7 @@ describe('batchDelete api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
......
import handler from '@/pages/api/core/chat/history/clearHistories'; import handler from '@/pages/api/core/chat/history/clearHistories';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
...@@ -45,6 +45,7 @@ describe('clearHistories api test', () => { ...@@ -45,6 +45,7 @@ describe('clearHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.online source: ChatSourceEnum.online
...@@ -87,6 +88,7 @@ describe('clearHistories api test', () => { ...@@ -87,6 +88,7 @@ describe('clearHistories api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: otherUser.teamId, teamId: otherUser.teamId,
tmbId: otherUser.tmbId, tmbId: otherUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: otherChatId, chatId: otherChatId,
source: ChatSourceEnum.online source: ChatSourceEnum.online
...@@ -133,6 +135,7 @@ describe('clearHistories api test', () => { ...@@ -133,6 +135,7 @@ describe('clearHistories api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: apiChatId, chatId: apiChatId,
source: ChatSourceEnum.api source: ChatSourceEnum.api
......
...@@ -48,6 +48,7 @@ describe('delHistory api test', () => { ...@@ -48,6 +48,7 @@ describe('delHistory api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -169,11 +170,20 @@ describe('delHistory api test', () => { ...@@ -169,11 +170,20 @@ describe('delHistory api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId: legacyChatId, chatId: legacyChatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
}) })
]); ]);
await MongoChat.updateOne(
{
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId,
chatId: legacyChatId
},
{ $unset: { sourceType: '' } }
);
const res = await Call<any, { skillId: string; chatId: string }, any>(handler, { const res = await Call<any, { skillId: string; chatId: string }, any>(handler, {
auth: testUser, auth: testUser,
......
...@@ -51,6 +51,7 @@ describe('getHistories api test', () => { ...@@ -51,6 +51,7 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: chatIds[0], chatId: chatIds[0],
source: ChatSourceEnum.online, source: ChatSourceEnum.online,
...@@ -60,6 +61,7 @@ describe('getHistories api test', () => { ...@@ -60,6 +61,7 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: chatIds[1], chatId: chatIds[1],
source: ChatSourceEnum.online, source: ChatSourceEnum.online,
...@@ -69,6 +71,7 @@ describe('getHistories api test', () => { ...@@ -69,6 +71,7 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: chatIds[2], chatId: chatIds[2],
source: ChatSourceEnum.online, source: ChatSourceEnum.online,
...@@ -138,6 +141,7 @@ describe('getHistories api test', () => { ...@@ -138,6 +141,7 @@ describe('getHistories api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: apiChatId, chatId: apiChatId,
source: ChatSourceEnum.api, source: ChatSourceEnum.api,
...@@ -243,6 +247,7 @@ describe('getHistories api test', () => { ...@@ -243,6 +247,7 @@ describe('getHistories api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: otherUser.teamId, teamId: otherUser.teamId,
tmbId: otherUser.tmbId, tmbId: otherUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId: String(otherApp._id), appId: String(otherApp._id),
chatId: otherChatId, chatId: otherChatId,
source: ChatSourceEnum.online, source: ChatSourceEnum.online,
...@@ -292,6 +297,7 @@ describe('getHistories api test', () => { ...@@ -292,6 +297,7 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: shareChatId, chatId: shareChatId,
source: ChatSourceEnum.share, source: ChatSourceEnum.share,
...@@ -302,6 +308,7 @@ describe('getHistories api test', () => { ...@@ -302,6 +308,7 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: otherShareChatId, chatId: otherShareChatId,
source: ChatSourceEnum.share, source: ChatSourceEnum.share,
...@@ -482,12 +489,17 @@ describe('getHistories api test', () => { ...@@ -482,12 +489,17 @@ describe('getHistories api test', () => {
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId: legacyChatId, chatId: legacyChatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
title: 'Legacy Skill Debug Session' title: 'Legacy Skill Debug Session'
}) })
]); ]);
await MongoChat.updateOne(
{ appId: skillId, chatId: legacyChatId },
{ $unset: { sourceType: '' } }
);
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, { const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
auth: testUser, auth: testUser,
......
import handler from '@/pages/api/core/chat/history/updateHistory'; import handler from '@/pages/api/core/chat/history/updateHistory';
import type { UpdateHistoryBodyType } from '@fastgpt/global/openapi/core/chat/history/api'; import type { UpdateHistoryBodyType } from '@fastgpt/global/openapi/core/chat/history/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
...@@ -45,6 +45,7 @@ describe('updateHistory api test', () => { ...@@ -45,6 +45,7 @@ describe('updateHistory api test', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
......
...@@ -31,6 +31,7 @@ describe('delete chat record api', () => { ...@@ -31,6 +31,7 @@ describe('delete chat record api', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId, dataId,
...@@ -60,6 +61,7 @@ describe('delete chat record api', () => { ...@@ -60,6 +61,7 @@ describe('delete chat record api', () => {
await MongoChat.create({ await MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.test source: ChatSourceEnum.test
...@@ -180,7 +182,7 @@ describe('delete chat record api', () => { ...@@ -180,7 +182,7 @@ describe('delete chat record api', () => {
const skillChatId = getNanoid(); const skillChatId = getNanoid();
const contentId = getNanoid(); const contentId = getNanoid();
await Promise.all([ const [, , legacyItem] = await Promise.all([
MongoChat.create({ MongoChat.create({
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
...@@ -204,6 +206,7 @@ describe('delete chat record api', () => { ...@@ -204,6 +206,7 @@ describe('delete chat record api', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId: skillChatId, chatId: skillChatId,
dataId: contentId, dataId: contentId,
...@@ -211,6 +214,17 @@ describe('delete chat record api', () => { ...@@ -211,6 +214,17 @@ describe('delete chat record api', () => {
value: [{ type: 'text', text: { content: 'legacy answer' } }] value: [{ type: 'text', text: { content: 'legacy answer' } }]
}) })
]); ]);
await MongoChatItem.updateOne({ _id: legacyItem._id }, { $unset: { sourceType: '' } });
await MongoChatItem.updateOne(
{
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId,
chatId: skillChatId,
dataId: contentId,
'value.0.text.content': 'legacy answer'
},
{ $unset: { sourceType: '' } }
);
const res = await Call<DeleteChatRecordBodyType, Record<string, never>>(handler, { const res = await Call<DeleteChatRecordBodyType, Record<string, never>>(handler, {
auth: testUser, auth: testUser,
...@@ -223,7 +237,7 @@ describe('delete chat record api', () => { ...@@ -223,7 +237,7 @@ describe('delete chat record api', () => {
expect(res.code).toBe(200); expect(res.code).toBe(200);
const [skillItem, legacyItem] = await Promise.all([ const [skillItem, legacyResultItem] = await Promise.all([
MongoChatItem.findOne({ MongoChatItem.findOne({
sourceType: ChatSourceTypeEnum.skillEdit, sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
...@@ -238,7 +252,7 @@ describe('delete chat record api', () => { ...@@ -238,7 +252,7 @@ describe('delete chat record api', () => {
}).lean() }).lean()
]); ]);
expect(skillItem?.deleteTime).toBeInstanceOf(Date); expect(skillItem?.deleteTime).toBeInstanceOf(Date);
expect(legacyItem?.deleteTime).toBeNull(); expect(legacyResultItem?.deleteTime).toBeNull();
}); });
it('should reject read-only skill collaborator when deleting skill edit chat item', async () => { it('should reject read-only skill collaborator when deleting skill edit chat item', async () => {
......
...@@ -92,6 +92,7 @@ describe('getRecords_v2 skill edit target', () => { ...@@ -92,6 +92,7 @@ describe('getRecords_v2 skill edit target', () => {
teamId: testUser.teamId, teamId: testUser.teamId,
tmbId: testUser.tmbId, tmbId: testUser.tmbId,
userId: testUser.userId, userId: testUser.userId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId, chatId,
dataId: getNanoid(), dataId: getNanoid(),
...@@ -99,6 +100,15 @@ describe('getRecords_v2 skill edit target', () => { ...@@ -99,6 +100,15 @@ describe('getRecords_v2 skill edit target', () => {
value: [{ type: 'text', text: { content: 'legacy item' } }] value: [{ type: 'text', text: { content: 'legacy item' } }]
}) })
]); ]);
await MongoChatItem.updateOne(
{
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId,
chatId,
'value.0.text.content': 'legacy item'
},
{ $unset: { sourceType: '' } }
);
const res = await Call<GetRecordsV2BodyType, any, GetRecordsV2ResponseType>(handler, { const res = await Call<GetRecordsV2BodyType, any, GetRecordsV2ResponseType>(handler, {
auth: testUser, auth: testUser,
......
...@@ -76,6 +76,7 @@ describe('system openapi chat auth', () => { ...@@ -76,6 +76,7 @@ describe('system openapi chat auth', () => {
await MongoChat.create({ await MongoChat.create({
teamId: user.teamId, teamId: user.teamId,
tmbId: user.tmbId, tmbId: user.tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
source: ChatSourceEnum.api, source: ChatSourceEnum.api,
...@@ -89,6 +90,7 @@ describe('system openapi chat auth', () => { ...@@ -89,6 +90,7 @@ describe('system openapi chat auth', () => {
teamId: user.teamId, teamId: user.teamId,
tmbId: user.tmbId, tmbId: user.tmbId,
userId: user.userId, userId: user.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: firstDataId, dataId: firstDataId,
...@@ -105,6 +107,7 @@ describe('system openapi chat auth', () => { ...@@ -105,6 +107,7 @@ describe('system openapi chat auth', () => {
teamId: user.teamId, teamId: user.teamId,
tmbId: user.tmbId, tmbId: user.tmbId,
userId: user.userId, userId: user.userId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId, chatId,
dataId: secondDataId, dataId: secondDataId,
......
import { afterEach, describe, expect, it, vi } from 'vitest';
import { isWorkflowShortcutInputtingTarget } from '@/pageComponents/app/detail/WorkflowComponents/Flow/hooks/keyboard';
const createElementMock = ({
closestMap = {},
className = ''
}: {
closestMap?: Record<string, unknown>;
className?: string;
}) => {
const element = {
nodeType: 1,
className,
closest: vi.fn((selector: string) => closestMap[selector] ?? null),
getAttribute: vi.fn((attr: string) => {
if (attr !== 'contenteditable') return null;
return 'true';
})
};
return element;
};
describe('isWorkflowShortcutInputtingTarget', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('should treat native form input targets as inputting', () => {
const input = createElementMock({
closestMap: {
'input, textarea, select': true
}
});
expect(isWorkflowShortcutInputtingTarget(input as unknown as EventTarget)).toBe(true);
});
it('should treat Lexical contenteditable selection as inputting', () => {
const editor = createElementMock({
closestMap: {
'[contenteditable]': {
getAttribute: () => 'true'
}
}
});
vi.stubGlobal('window', {
getSelection: () => ({
rangeCount: 1,
isCollapsed: false,
anchorNode: {
parentElement: editor
},
focusNode: {
parentElement: editor
}
})
});
expect(isWorkflowShortcutInputtingTarget(undefined)).toBe(true);
});
it('should ignore collapsed selections outside editable targets', () => {
vi.stubGlobal('window', {
getSelection: () => ({
rangeCount: 1,
isCollapsed: true,
anchorNode: null,
focusNode: null
})
});
expect(isWorkflowShortcutInputtingTarget(undefined)).toBe(false);
});
it('should not treat canvas targets as inputting', () => {
const canvas = createElementMock({});
expect(isWorkflowShortcutInputtingTarget(canvas as unknown as EventTarget)).toBe(false);
});
});
...@@ -77,6 +77,7 @@ const createLegacySkillDebugChat = async ({ ...@@ -77,6 +77,7 @@ const createLegacySkillDebugChat = async ({
await MongoChat.create({ await MongoChat.create({
teamId, teamId,
tmbId, tmbId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId, chatId,
source: ChatSourceEnum.test, source: ChatSourceEnum.test,
...@@ -85,6 +86,7 @@ const createLegacySkillDebugChat = async ({ ...@@ -85,6 +86,7 @@ const createLegacySkillDebugChat = async ({
await MongoChatItem.create({ await MongoChatItem.create({
teamId, teamId,
tmbId, tmbId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId, chatId,
dataId: `${chatId}-item`, dataId: `${chatId}-item`,
...@@ -93,6 +95,7 @@ const createLegacySkillDebugChat = async ({ ...@@ -93,6 +95,7 @@ const createLegacySkillDebugChat = async ({
}); });
await MongoChatItemResponse.create({ await MongoChatItemResponse.create({
teamId, teamId,
sourceType: ChatSourceTypeEnum.skillEdit,
appId: skillId, appId: skillId,
chatId, chatId,
chatItemDataId: `${chatId}-item`, chatItemDataId: `${chatId}-item`,
......
...@@ -135,6 +135,7 @@ describe('cleanupDuplicateChats data clean API', () => { ...@@ -135,6 +135,7 @@ describe('cleanupDuplicateChats data clean API', () => {
await MongoChatItem.create({ await MongoChatItem.create({
teamId, teamId,
tmbId, tmbId,
sourceType: ChatSourceTypeEnum.app,
appId, appId,
chatId: 'duplicate-chat', chatId: 'duplicate-chat',
dataId: 'item-1', dataId: 'item-1',
......
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceEnum, ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
...@@ -311,6 +311,7 @@ describe('authChatCompletionHeaderRequest', () => { ...@@ -311,6 +311,7 @@ describe('authChatCompletionHeaderRequest', () => {
chatId, chatId,
teamId: owner.teamId, teamId: owner.teamId,
tmbId: member.tmbId, tmbId: member.tmbId,
sourceType: ChatSourceTypeEnum.app,
source: ChatSourceEnum.api source: ChatSourceEnum.api
}); });
...@@ -346,6 +347,7 @@ describe('authChatCompletionHeaderRequest', () => { ...@@ -346,6 +347,7 @@ describe('authChatCompletionHeaderRequest', () => {
chatId, chatId,
teamId: owner.teamId, teamId: owner.teamId,
tmbId: memberA.tmbId, tmbId: memberA.tmbId,
sourceType: ChatSourceTypeEnum.app,
source: ChatSourceEnum.api source: ChatSourceEnum.api
}); });
......
...@@ -35,9 +35,7 @@ export default defineConfig({ ...@@ -35,9 +35,7 @@ export default defineConfig({
process.env.FILE_TOKEN_KEY ?? process.env.FILE_TOKEN_KEY ??
'bfd697e7e798f75deaf2d31210bc93a2e41ad4eed9e7831071d77821b7b97cff', 'bfd697e7e798f75deaf2d31210bc93a2e41ad4eed9e7831071d77821b7b97cff',
AES256_SECRET_KEY: process.env.AES256_SECRET_KEY ?? 'fastgpt_test_aes256_secret_key', AES256_SECRET_KEY: process.env.AES256_SECRET_KEY ?? 'fastgpt_test_aes256_secret_key',
INVOKE_TOKEN_SECRET: INVOKE_TOKEN_SECRET: process.env.INVOKE_TOKEN_SECRET ?? 'fastgpt_test_invoke_token_secret_32'
process.env.INVOKE_TOKEN_SECRET ?? 'fastgpt_test_invoke_token_secret_32',
AGENT_SANDBOX_PROVIDER: 'opensandbox'
}, },
coverage: { coverage: {
enabled: true, enabled: true,
......
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