mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-26 04:32:50 +00:00
Compare commits
No commits in common. "main" and "v4.14.4-cve" have entirely different histories.
main
...
v4.14.4-cv
|
|
@ -1,672 +0,0 @@
|
|||
---
|
||||
name: workflow-stop-design
|
||||
description: 工作流暂停逻辑设计方案
|
||||
---
|
||||
|
||||
## 1. Redis 状态管理方案
|
||||
|
||||
### 1.1 状态键设计
|
||||
|
||||
**Redis Key 结构:**
|
||||
```typescript
|
||||
// Key 格式: agent_runtime_stopping:{appId}:{chatId}
|
||||
const WORKFLOW_STATUS_PREFIX = 'agent_runtime_stopping';
|
||||
|
||||
type WorkflowStatusKey = `${typeof WORKFLOW_STATUS_PREFIX}:${string}:${string}`;
|
||||
|
||||
// 示例: agent_runtime_stopping:app_123456:chat_789012
|
||||
```
|
||||
|
||||
**状态值设计:**
|
||||
- **存在键 (值为 1)**: 工作流应该停止
|
||||
- **不存在键**: 工作流正常运行
|
||||
- **设计简化**: 不使用状态枚举,仅通过键的存在与否判断
|
||||
|
||||
**参数类型定义:**
|
||||
```typescript
|
||||
type WorkflowStatusParams = {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
};
|
||||
```
|
||||
|
||||
### 1.2 状态生命周期管理
|
||||
|
||||
**状态转换流程:**
|
||||
```
|
||||
正常运行(无键) → 停止中(键存在) → 完成(删除键)
|
||||
```
|
||||
|
||||
**TTL 设置:**
|
||||
- **停止标志 TTL**: 60 秒
|
||||
- 原因: 避免因意外情况导致的键泄漏
|
||||
- 正常情况下会在工作流完成时主动删除
|
||||
- **工作流完成后**: 直接删除 Redis 键
|
||||
- 原因: 不需要保留终态,减少 Redis 内存占用
|
||||
|
||||
### 1.3 核心函数说明
|
||||
|
||||
**1. setAgentRuntimeStop**
|
||||
- **功能**: 设置停止标志
|
||||
- **参数**: `{ appId, chatId }`
|
||||
- **实现**: 使用 `SETEX` 命令,设置键值为 1,TTL 60 秒
|
||||
|
||||
**2. shouldWorkflowStop**
|
||||
- **功能**: 检查工作流是否应该停止
|
||||
- **参数**: `{ appId, chatId }`
|
||||
- **返回**: `Promise<boolean>` - true=应该停止, false=继续运行
|
||||
- **实现**: GET 命令获取键值,存在则返回 true
|
||||
|
||||
**3. delAgentRuntimeStopSign**
|
||||
- **功能**: 删除停止标志
|
||||
- **参数**: `{ appId, chatId }`
|
||||
- **实现**: DEL 命令删除键
|
||||
|
||||
**4. waitForWorkflowComplete**
|
||||
- **功能**: 等待工作流完成(停止标志被删除)
|
||||
- **参数**: `{ appId, chatId, timeout?, pollInterval? }`
|
||||
- **实现**: 轮询检查停止标志是否被删除,超时返回
|
||||
|
||||
### 1.4 边界情况处理
|
||||
|
||||
**1. Redis 操作失败**
|
||||
- **错误处理**: 所有 Redis 操作都包含 `.catch()` 错误处理
|
||||
- **降级策略**:
|
||||
- `shouldWorkflowStop`: 出错时返回 `false` (认为不需要停止,继续运行)
|
||||
- `delAgentRuntimeStopSign`: 出错时记录错误日志,但不影响主流程
|
||||
- **设计原因**: Redis 异常不应阻塞工作流运行,降级到继续执行策略
|
||||
|
||||
**2. TTL 自动清理**
|
||||
- **TTL 设置**: 60 秒
|
||||
- **清理时机**: Redis 自动清理过期键
|
||||
- **设计原因**:
|
||||
- 避免因异常情况导致的 Redis 键泄漏
|
||||
- 自动清理减少手动维护成本
|
||||
- 60 秒足够大多数工作流完成停止操作
|
||||
|
||||
**3. stop 接口等待超时**
|
||||
- **超时时间**: 5 秒
|
||||
- **超时策略**: `waitForWorkflowComplete` 在 5 秒内轮询检查停止标志是否被删除
|
||||
- **超时处理**: 5 秒后直接返回,不影响工作流继续执行
|
||||
- **设计原因**:
|
||||
- 避免前端长时间等待
|
||||
- 5 秒足够大多数节点完成当前操作
|
||||
- 用户体验优先,超时后前端可选择重试或放弃
|
||||
|
||||
**4. 并发停止请求**
|
||||
- **处理方式**: 多次调用 `setAgentRuntimeStop` 是安全的,Redis SETEX 是幂等操作
|
||||
- **设计原因**: 避免用户多次点击停止按钮导致的问题
|
||||
|
||||
---
|
||||
|
||||
## 2. Redis 工具函数实现
|
||||
|
||||
**位置**: `packages/service/core/workflow/dispatch/workflowStatus.ts`
|
||||
|
||||
```typescript
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import { getGlobalRedisConnection } from '../../../common/redis/index';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
const WORKFLOW_STATUS_PREFIX = 'agent_runtime_stopping';
|
||||
const TTL = 60; // 60秒
|
||||
|
||||
export const StopStatus = 'STOPPING';
|
||||
|
||||
export type WorkflowStatusParams = {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
// 获取工作流状态键
|
||||
export const getRuntimeStatusKey = (params: WorkflowStatusParams): string => {
|
||||
return `${WORKFLOW_STATUS_PREFIX}:${params.appId}:${params.chatId}`;
|
||||
};
|
||||
|
||||
// 设置停止标志
|
||||
export const setAgentRuntimeStop = async (params: WorkflowStatusParams): Promise<void> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
await redis.setex(key, TTL, 1);
|
||||
};
|
||||
|
||||
// 删除停止标志
|
||||
export const delAgentRuntimeStopSign = async (params: WorkflowStatusParams): Promise<void> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
await redis.del(key).catch((err) => {
|
||||
addLog.error(`[Agent Runtime Stop] Delete stop sign error`, err);
|
||||
});
|
||||
};
|
||||
|
||||
// 检查工作流是否应该停止
|
||||
export const shouldWorkflowStop = (params: WorkflowStatusParams): Promise<boolean> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
return redis
|
||||
.get(key)
|
||||
.then((res) => !!res)
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 等待工作流完成(停止标志被删除)
|
||||
* @param params 工作流参数
|
||||
* @param timeout 超时时间(毫秒),默认5秒
|
||||
* @param pollInterval 轮询间隔(毫秒),默认50毫秒
|
||||
*/
|
||||
export const waitForWorkflowComplete = async ({
|
||||
appId,
|
||||
chatId,
|
||||
timeout = 5000,
|
||||
pollInterval = 50
|
||||
}: {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
timeout?: number;
|
||||
pollInterval?: number;
|
||||
}) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const sign = await shouldWorkflowStop({ appId, chatId });
|
||||
|
||||
// 如果停止标志已被删除,说明工作流已完成
|
||||
if (!sign) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待下一次轮询
|
||||
await delay(pollInterval);
|
||||
}
|
||||
|
||||
// 超时后直接返回
|
||||
return;
|
||||
};
|
||||
```
|
||||
|
||||
**测试用例位置**: `test/cases/service/core/app/workflow/workflowStatus.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, test, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
setAgentRuntimeStop,
|
||||
delAgentRuntimeStopSign,
|
||||
shouldWorkflowStop,
|
||||
waitForWorkflowComplete
|
||||
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
|
||||
|
||||
describe('Workflow Status Redis Functions', () => {
|
||||
const testAppId = 'test_app_123';
|
||||
const testChatId = 'test_chat_456';
|
||||
|
||||
beforeEach(async () => {
|
||||
// 清理测试数据
|
||||
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
|
||||
});
|
||||
|
||||
test('should set stopping sign', async () => {
|
||||
await setAgentRuntimeStop({ appId: testAppId, chatId: testChatId });
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for non-existent status', async () => {
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false after deleting stop sign', async () => {
|
||||
await setAgentRuntimeStop({ appId: testAppId, chatId: testChatId });
|
||||
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(false);
|
||||
});
|
||||
|
||||
test('should wait for workflow completion', async () => {
|
||||
// 设置初始停止标志
|
||||
await setAgentRuntimeStop({ appId: testAppId, chatId: testChatId });
|
||||
|
||||
// 模拟异步完成(删除停止标志)
|
||||
setTimeout(async () => {
|
||||
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
|
||||
}, 500);
|
||||
|
||||
// 等待完成
|
||||
await waitForWorkflowComplete({
|
||||
appId: testAppId,
|
||||
chatId: testChatId,
|
||||
timeout: 2000
|
||||
});
|
||||
|
||||
// 验证停止标志已被删除
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(false);
|
||||
});
|
||||
|
||||
test('should timeout when waiting too long', async () => {
|
||||
await setAgentRuntimeStop({ appId: testAppId, chatId: testChatId });
|
||||
|
||||
// 等待超时(不删除标志)
|
||||
await waitForWorkflowComplete({
|
||||
appId: testAppId,
|
||||
chatId: testChatId,
|
||||
timeout: 100
|
||||
});
|
||||
|
||||
// 验证停止标志仍然存在
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle concurrent stop sign operations', async () => {
|
||||
// 并发设置停止标志
|
||||
await Promise.all([
|
||||
setAgentRuntimeStop({ appId: testAppId, chatId: testChatId }),
|
||||
setAgentRuntimeStop({ appId: testAppId, chatId: testChatId })
|
||||
]);
|
||||
|
||||
// 停止标志应该存在
|
||||
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
|
||||
expect(shouldStop).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 3. 工作流停止检测机制改造
|
||||
|
||||
### 3.1 修改位置
|
||||
|
||||
**文件**: `packages/service/core/workflow/dispatch/index.ts`
|
||||
|
||||
### 3.2 工作流启动时的停止检测机制
|
||||
|
||||
**改造点 1: 停止检测逻辑 (行 196-216)**
|
||||
|
||||
使用内存变量 + 定时轮询 Redis 的方式:
|
||||
|
||||
```typescript
|
||||
import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus';
|
||||
|
||||
// 初始化停止检测
|
||||
let stopping = false;
|
||||
const checkIsStopping = (): boolean => {
|
||||
if (apiVersion === 'v2') {
|
||||
return stopping;
|
||||
}
|
||||
if (apiVersion === 'v1') {
|
||||
if (!res) return false;
|
||||
return res.closed || !!res.errored;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// v2 版本: 启动定时器定期检查 Redis
|
||||
const checkStoppingTimer =
|
||||
apiVersion === 'v2'
|
||||
? setInterval(async () => {
|
||||
stopping = await shouldWorkflowStop({
|
||||
appId: runningAppInfo.id,
|
||||
chatId
|
||||
});
|
||||
}, 100)
|
||||
: undefined;
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- v2 版本使用内存变量 `stopping` + 100ms 定时器轮询 Redis
|
||||
- v1 版本仍使用原有的 `res.closed/res.errored` 检测
|
||||
- 轮询频率 100ms,平衡性能和响应速度
|
||||
|
||||
**改造点 2: 工作流完成后清理 (行 232-249)**
|
||||
|
||||
```typescript
|
||||
return runWorkflow({
|
||||
...data,
|
||||
checkIsStopping, // 传递检测函数
|
||||
query,
|
||||
histories,
|
||||
// ... 其他参数
|
||||
}).finally(async () => {
|
||||
// 清理定时器
|
||||
if (streamCheckTimer) {
|
||||
clearInterval(streamCheckTimer);
|
||||
}
|
||||
if (checkStoppingTimer) {
|
||||
clearInterval(checkStoppingTimer);
|
||||
}
|
||||
|
||||
// Close mcpClient connections
|
||||
Object.values(mcpClientMemory).forEach((client) => {
|
||||
client.closeConnection();
|
||||
});
|
||||
|
||||
// 工作流完成后删除 Redis 记录
|
||||
await delAgentRuntimeStopSign({
|
||||
appId: runningAppInfo.id,
|
||||
chatId
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 节点执行前的停止检测
|
||||
|
||||
**位置**: `packages/service/core/workflow/dispatch/index.ts:861-868`
|
||||
|
||||
在 `checkNodeCanRun` 方法中,每个节点执行前检查:
|
||||
|
||||
```typescript
|
||||
private async checkNodeCanRun(
|
||||
node: RuntimeNodeItemType,
|
||||
skippedNodeIdList = new Set<string>()
|
||||
) {
|
||||
// ... 其他检查逻辑 ...
|
||||
|
||||
// Check queue status
|
||||
if (data.maxRunTimes <= 0) {
|
||||
addLog.error('Max run times is 0', {
|
||||
appId: data.runningAppInfo.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止检测
|
||||
if (checkIsStopping()) {
|
||||
addLog.warn('Workflow stopped', {
|
||||
appId: data.runningAppInfo.id,
|
||||
nodeId: node.nodeId,
|
||||
nodeName: node.name
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ... 执行节点逻辑 ...
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 直接调用 `checkIsStopping()` 同步方法
|
||||
- 内部会检查内存变量 `stopping`
|
||||
- 定时器每 100ms 更新一次该变量
|
||||
- 检测到停止时记录日志并直接返回,不执行节点
|
||||
|
||||
## 4. v2/chat/stop 接口设计
|
||||
|
||||
### 4.1 接口规范
|
||||
|
||||
**接口路径**: `/api/v2/chat/stop`
|
||||
|
||||
**Schema 位置**: `packages/global/openapi/core/chat/api.ts`
|
||||
|
||||
**接口文档位置**: `packages/global/openapi/core/chat/index.ts`
|
||||
|
||||
**请求方法**: POST
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
// packages/global/openapi/core/chat/api.ts
|
||||
export const StopV2ChatSchema = z
|
||||
.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID'),
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
|
||||
});
|
||||
|
||||
export type StopV2ChatParams = z.infer<typeof StopV2ChatSchema>;
|
||||
```
|
||||
|
||||
**响应格式**:
|
||||
```typescript
|
||||
export const StopV2ChatResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean().describe('是否成功停止')
|
||||
});
|
||||
|
||||
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>;
|
||||
```
|
||||
|
||||
### 4.2 接口实现
|
||||
|
||||
**文件位置**: `projects/app/src/pages/api/v2/chat/stop.ts`
|
||||
|
||||
```typescript
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import {
|
||||
setAgentRuntimeStop,
|
||||
waitForWorkflowComplete
|
||||
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
|
||||
import { StopV2ChatSchema, type StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/controler/api';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> {
|
||||
const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body);
|
||||
|
||||
// 鉴权 (复用聊天 CRUD 鉴权)
|
||||
await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
});
|
||||
|
||||
// 设置停止标志
|
||||
await setAgentRuntimeStop({
|
||||
appId,
|
||||
chatId
|
||||
});
|
||||
|
||||
// 等待工作流完成 (最多等待 5 秒)
|
||||
await waitForWorkflowComplete({ appId, chatId, timeout: 5000 });
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
```
|
||||
|
||||
**接口文档** (`packages/global/openapi/core/chat/index.ts`):
|
||||
|
||||
```typescript
|
||||
export const ChatPath: OpenAPIPath = {
|
||||
// ... 其他路径
|
||||
|
||||
'/v2/chat/stop': {
|
||||
post: {
|
||||
summary: '停止 Agent 运行',
|
||||
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。
|
||||
LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`,
|
||||
tags: [TagsMap.chatPage],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: StopV2ChatSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功停止工作流',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: StopV2ChatResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 接口使用 `authChatCrud` 进行鉴权,支持 Token 和 API Key
|
||||
- 支持分享链接和团队空间的鉴权数据
|
||||
- 设置停止标志后等待最多 5 秒
|
||||
- 无论是否超时,都返回 `success: true`
|
||||
|
||||
## 5. 前端改造
|
||||
|
||||
由于当前代码已经能够正常工作,且 v2 版本的后端已经实现了基于 Redis 的停止机制,前端可以保持现有的简单实现:
|
||||
|
||||
**保持现有实现的原因**:
|
||||
1. 后端已经通过定时器轮询 Redis 实现了停止检测
|
||||
2. 前端调用 `abort()` 后,后端会在下个检测周期(100ms内)发现停止标志
|
||||
3. 简化前端逻辑,避免增加复杂性
|
||||
4. 用户体验上,立即中断连接响应更快
|
||||
|
||||
**可选的增强方案**:
|
||||
|
||||
如果需要在前端显示更详细的停止状态,可以添加 API 客户端函数:
|
||||
|
||||
**文件位置**: `projects/app/src/web/core/chat/api.ts`
|
||||
|
||||
```typescript
|
||||
import { POST } from '@/web/common/api/request';
|
||||
import type { StopV2ChatParams, StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/controler/api';
|
||||
|
||||
/**
|
||||
* 停止 v2 版本工作流运行
|
||||
*/
|
||||
export const stopV2Chat = (data: StopV2ChatParams) =>
|
||||
POST<StopV2ChatResponse>('/api/v2/chat/stop', data);
|
||||
```
|
||||
|
||||
**增强的 abortRequest 函数**:
|
||||
|
||||
```typescript
|
||||
/* Abort chat completions, questionGuide */
|
||||
const abortRequest = useMemoizedFn(async (reason: string = 'stop') => {
|
||||
// 先调用 abort 中断连接
|
||||
chatController.current?.abort(new Error(reason));
|
||||
questionGuideController.current?.abort(new Error(reason));
|
||||
pluginController.current?.abort(new Error(reason));
|
||||
|
||||
// v2 版本: 可选地通知后端优雅停止
|
||||
if (chatBoxData?.app?.version === 'v2' && appId && chatId) {
|
||||
try {
|
||||
await stopV2Chat({
|
||||
appId,
|
||||
chatId,
|
||||
outLinkAuthData
|
||||
});
|
||||
} catch (error) {
|
||||
// 静默失败,不影响用户体验
|
||||
console.warn('Failed to notify backend to stop workflow', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- **推荐**: 保持当前简单实现,后端已经足够健壮
|
||||
- **可选**: 如果需要更精确的停止状态追踪,可以实现上述增强方案
|
||||
|
||||
## 6. 完整调用流程
|
||||
|
||||
### 6.1 正常停止流程
|
||||
|
||||
```
|
||||
用户点击停止按钮
|
||||
↓
|
||||
前端: abortRequest()
|
||||
↓
|
||||
前端: chatController.abort() [立即中断 HTTP 连接]
|
||||
↓
|
||||
[可选] 前端: POST /api/v2/chat/stop
|
||||
↓
|
||||
后端: setAgentRuntimeStop(appId, chatId) [设置停止标志]
|
||||
↓
|
||||
后端: 定时器检测到 Redis 停止标志,更新内存变量 stopping = true
|
||||
↓
|
||||
后端: 下个节点执行前 checkIsStopping() 返回 true
|
||||
↓
|
||||
后端: 停止处理新节点,记录日志
|
||||
↓
|
||||
后端: 工作流 finally 块删除 Redis 停止标志
|
||||
↓
|
||||
[可选] 后端: waitForWorkflowComplete() 检测到停止标志被删除
|
||||
↓
|
||||
[可选] 前端: 显示停止成功提示
|
||||
```
|
||||
|
||||
### 6.2 超时流程
|
||||
|
||||
```
|
||||
[可选] 前端: POST /api/v2/chat/stop
|
||||
↓
|
||||
后端: setAgentRuntimeStop(appId, chatId)
|
||||
↓
|
||||
后端: waitForWorkflowComplete(timeout=5s)
|
||||
↓
|
||||
后端: 5秒后停止标志仍存在
|
||||
↓
|
||||
后端: 返回成功响应 (不区分超时)
|
||||
↓
|
||||
[可选] 前端: 显示成功提示
|
||||
↓
|
||||
后端: 工作流继续运行,最终完成后删除停止标志
|
||||
```
|
||||
|
||||
### 6.3 工作流自然完成流程
|
||||
|
||||
```
|
||||
工作流运行中
|
||||
↓
|
||||
所有节点执行完成
|
||||
↓
|
||||
dispatchWorkFlow.finally()
|
||||
↓
|
||||
删除 Redis 停止标志
|
||||
↓
|
||||
清理定时器
|
||||
↓
|
||||
60秒 TTL 确保即使删除失败也会自动清理
|
||||
```
|
||||
|
||||
### 6.4 时序说明
|
||||
|
||||
**关键时间点**:
|
||||
- **100ms**: 后端定时器检查 Redis 停止标志的频率
|
||||
- **5s**: stop 接口等待工作流完成的超时时间
|
||||
- **60s**: Redis 键的 TTL,自动清理时间
|
||||
|
||||
**响应时间**:
|
||||
- 用户点击停止 → HTTP 连接中断: **立即** (前端 abort)
|
||||
- 停止标志写入 Redis: **< 50ms** (Redis SETEX 操作)
|
||||
- 后端检测到停止: **< 100ms** (定时器轮询周期)
|
||||
- 当前节点停止执行: **取决于节点类型**
|
||||
- LLM 流式输出: **立即**中断流
|
||||
- HTTP 请求节点: **等待请求完成**
|
||||
- 其他节点: **等待当前操作完成**
|
||||
|
||||
## 7. 测试策略
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
**Redis 工具函数测试**:
|
||||
- `setAgentRuntimeStop` / `shouldWorkflowStop` 基本功能
|
||||
- `delAgentRuntimeStopSign` 删除功能
|
||||
- `waitForWorkflowComplete` 等待机制和超时
|
||||
- 并发操作安全性
|
||||
|
||||
**文件位置**: `test/cases/service/core/app/workflow/workflowStatus.test.ts`
|
||||
|
||||
**测试用例**:
|
||||
```typescript
|
||||
describe('Workflow Status Redis Functions', () => {
|
||||
test('should set stopping sign')
|
||||
test('should return false for non-existent status')
|
||||
test('should detect stopping status')
|
||||
test('should return false after deleting stop sign')
|
||||
test('should wait for workflow completion')
|
||||
test('should timeout when waiting too long')
|
||||
test('should delete workflow stop sign')
|
||||
test('should handle concurrent stop sign operations')
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -73,6 +73,7 @@ jobs:
|
|||
--label "org.opencontainers.image.description=${{ steps.config.outputs.DESCRIPTION }}" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${{ steps.config.outputs.DOCKER_REPO_TAGGED }} \
|
||||
.
|
||||
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ event取值:
|
|||
<Tab value="请求示例">
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/history/getHistories' \
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/getHistories' \
|
||||
--header 'Authorization: Bearer [apikey]' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
|
|
@ -679,7 +679,7 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/history/getH
|
|||
<Tab value="请求示例">
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/history/updateHistory' \
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/updateHistory' \
|
||||
--header 'Authorization: Bearer [apikey]' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
|
|
@ -721,7 +721,7 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/history/upda
|
|||
<Tab value="请求示例">
|
||||
|
||||
```bash
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/history/updateHistory' \
|
||||
curl --location --request POST 'http://localhost:3000/api/core/chat/updateHistory' \
|
||||
--header 'Authorization: Bearer [apikey]' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
|
|
@ -763,7 +763,7 @@ curl --location --request POST 'http://localhost:3000/api/core/chat/history/upda
|
|||
<Tab value="请求示例">
|
||||
|
||||
```bash
|
||||
curl --location --request DELETE 'http://localhost:3000/api/core/chat/history/delHistory?chatId=[chatId]&appId=[appId]' \
|
||||
curl --location --request DELETE 'http://localhost:3000/api/core/chat/delHistory?chatId=[chatId]&appId=[appId]' \
|
||||
--header 'Authorization: Bearer [apikey]'
|
||||
```
|
||||
|
||||
|
|
@ -800,7 +800,7 @@ curl --location --request DELETE 'http://localhost:3000/api/core/chat/history/de
|
|||
<Tab value="请求示例">
|
||||
|
||||
```bash
|
||||
curl --location --request DELETE 'http://localhost:3000/api/core/chat/history/clearHistories?appId=[appId]' \
|
||||
curl --location --request DELETE 'http://localhost:3000/api/core/chat/clearHistories?appId=[appId]' \
|
||||
--header 'Authorization: Bearer [apikey]'
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,14 @@ description: 'FastGPT V4.14.5 更新说明'
|
|||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。
|
||||
2. 更新Agent/工具时,会更新其上层所有目录的更新时间,以便其会排在列表前面。
|
||||
3. 门户页支持配置单个应用运行可见度配。
|
||||
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
1. 优化获取 redis 所有 key 的逻辑,避免大量获取时导致阻塞。
|
||||
2. MongoDB, Redis 和 MQ 的重连逻辑优化。
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
1. MCP 工具创建时,使用自定义鉴权头会报错。
|
||||
2. 获取对话日志列表时,如果用户头像为空,会抛错。
|
||||
3. chatAgent 未开启问题优化时,前端 UI 显示开启。
|
||||
4. 加载默认模型时,maxTokens 字段未赋值,导致模型最大响应值配置为空。
|
||||
5. S3 文件清理队列因网络稳定问题出现阻塞,导致删除任务不再执行。
|
||||
|
||||
|
||||
## 插件
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-09-29T11:52:39+08:00",
|
||||
"document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/openapi/app.mdx": "2025-09-26T13:18:51+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-12-18T13:49:45+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-11-14T13:21:17+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-12-09T23:33:32+08:00",
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
"document/content/docs/protocol/terms.en.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-12-17T17:44:38+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-12-09T23:33:32+08:00",
|
||||
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
|
|
@ -120,7 +120,6 @@
|
|||
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
|
||||
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
|
||||
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
|
||||
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-21T23:28:19+08:00",
|
||||
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { OutLinkChatAuthProps } from '../../support/permission/chat';
|
||||
import type { OutLinkChatAuthProps } from '../../support/permission/chat.d';
|
||||
|
||||
export type preUploadImgProps = OutLinkChatAuthProps & {
|
||||
// expiredTime?: Date;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,8 @@ export enum TrackEnum {
|
|||
clickOperationalAd = 'clickOperationalAd',
|
||||
closeOperationalAd = 'closeOperationalAd',
|
||||
teamChatQPM = 'teamChatQPM',
|
||||
|
||||
// Admin cron job tracks
|
||||
subscriptionDeleted = 'subscriptionDeleted',
|
||||
freeAccountCleanup = 'freeAccountCleanup',
|
||||
auditLogCleanup = 'auditLogCleanup',
|
||||
chatHistoryCleanup = 'chatHistoryCleanup',
|
||||
|
||||
// web tracks
|
||||
clientError = 'clientError'
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ export enum SystemConfigsTypeEnum {
|
|||
fastgptPro = 'fastgptPro',
|
||||
systemMsgModal = 'systemMsgModal',
|
||||
license = 'license',
|
||||
operationalAd = 'operationalAd',
|
||||
activityAd = 'activityAd'
|
||||
operationalAd = 'operationalAd'
|
||||
}
|
||||
|
||||
export const SystemConfigsTypeMap = {
|
||||
|
|
@ -22,8 +21,5 @@ export const SystemConfigsTypeMap = {
|
|||
},
|
||||
[SystemConfigsTypeEnum.operationalAd]: {
|
||||
label: 'operationalAd'
|
||||
},
|
||||
[SystemConfigsTypeEnum.activityAd]: {
|
||||
label: 'activityAd'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const AppLogKeysEnumMap = {
|
|||
};
|
||||
|
||||
export const DefaultAppLogKeys = [
|
||||
{ key: AppLogKeysEnum.SOURCE, enable: false },
|
||||
{ key: AppLogKeysEnum.SOURCE, enable: true },
|
||||
{ key: AppLogKeysEnum.USER, enable: true },
|
||||
{ key: AppLogKeysEnum.TITLE, enable: true },
|
||||
{ key: AppLogKeysEnum.SESSION_ID, enable: false },
|
||||
|
|
|
|||
|
|
@ -59,9 +59,6 @@ export type AppSchema = {
|
|||
inited?: boolean;
|
||||
/** @deprecated */
|
||||
teamTags: string[];
|
||||
|
||||
// 软删除字段
|
||||
deleteTime?: Date | null;
|
||||
};
|
||||
|
||||
export type AppListItemType = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import type { OutLinkChatAuthType } from '../../support/permission/chat/type';
|
||||
import { OutLinkChatAuthSchema } from '../../support/permission/chat/type';
|
||||
import { ObjectIdSchema } from '../../common/type/mongo';
|
||||
import z from 'zod';
|
||||
|
||||
export const PresignChatFileGetUrlSchema = z
|
||||
.object({
|
||||
key: z.string().min(1),
|
||||
appId: ObjectIdSchema,
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional()
|
||||
})
|
||||
.meta({
|
||||
description: '获取对话文件预览链接',
|
||||
example: {
|
||||
key: '1234567890',
|
||||
appId: '1234567890',
|
||||
outLinkAuthData: {
|
||||
shareId: '1234567890',
|
||||
outLinkUid: '1234567890'
|
||||
}
|
||||
}
|
||||
});
|
||||
export type PresignChatFileGetUrlParams = z.infer<typeof PresignChatFileGetUrlSchema> & {
|
||||
outLinkAuthData?: OutLinkChatAuthType;
|
||||
};
|
||||
|
||||
export const PresignChatFilePostUrlSchema = z
|
||||
.object({
|
||||
filename: z.string().min(1),
|
||||
appId: ObjectIdSchema,
|
||||
chatId: ObjectIdSchema,
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional()
|
||||
})
|
||||
.meta({
|
||||
description: '获取上传对话文件预签名 URL',
|
||||
example: {
|
||||
filename: '1234567890',
|
||||
appId: '1234567890',
|
||||
chatId: '1234567890',
|
||||
outLinkAuthData: {
|
||||
shareId: '1234567890',
|
||||
outLinkUid: '1234567890'
|
||||
}
|
||||
}
|
||||
});
|
||||
export type PresignChatFilePostUrlParams = z.infer<typeof PresignChatFilePostUrlSchema> & {
|
||||
outLinkAuthData?: OutLinkChatAuthType;
|
||||
};
|
||||
|
||||
export const UpdateChatFeedbackSchema = z
|
||||
.object({
|
||||
appId: z.string().min(1),
|
||||
chatId: z.string().min(1),
|
||||
dataId: z.string().min(1),
|
||||
userBadFeedback: z.string().optional(),
|
||||
userGoodFeedback: z.string().optional()
|
||||
})
|
||||
.meta({
|
||||
description: '更新对话反馈',
|
||||
example: {
|
||||
appId: '1234567890',
|
||||
chatId: '1234567890',
|
||||
dataId: '1234567890',
|
||||
userBadFeedback: '1234567890',
|
||||
userGoodFeedback: '1234567890'
|
||||
}
|
||||
});
|
||||
export type UpdateChatFeedbackProps = z.infer<typeof UpdateChatFeedbackSchema>;
|
||||
|
|
@ -51,8 +51,6 @@ export type ChatSchemaType = {
|
|||
hasBadFeedback?: boolean;
|
||||
hasUnreadGoodFeedback?: boolean;
|
||||
hasUnreadBadFeedback?: boolean;
|
||||
|
||||
deleteTime?: Date | null;
|
||||
};
|
||||
|
||||
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
|
||||
|
|
@ -199,7 +197,7 @@ export type HistoryItemType = {
|
|||
};
|
||||
export type ChatHistoryItemType = HistoryItemType & {
|
||||
appId: string;
|
||||
top?: boolean;
|
||||
top: boolean;
|
||||
};
|
||||
|
||||
/* ------- response data ------------ */
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ export type ExternalProviderType = {
|
|||
/* workflow props */
|
||||
export type ChatDispatchProps = {
|
||||
res?: NextApiResponse;
|
||||
checkIsStopping: () => boolean;
|
||||
lang?: localeType;
|
||||
requestOrigin?: string;
|
||||
mode: 'test' | 'chat' | 'debug';
|
||||
|
|
@ -64,7 +63,7 @@ export type ChatDispatchProps = {
|
|||
};
|
||||
uid: string; // Who run this workflow
|
||||
|
||||
chatId: string;
|
||||
chatId?: string;
|
||||
responseChatItemId?: string;
|
||||
histories: ChatItemType[];
|
||||
variables: Record<string, any>; // global variable
|
||||
|
|
@ -77,7 +76,7 @@ export type ChatDispatchProps = {
|
|||
maxRunTimes: number;
|
||||
isToolCall?: boolean;
|
||||
workflowStreamResponse?: WorkflowResponseType;
|
||||
apiVersion?: 'v1' | 'v2';
|
||||
version?: 'v1' | 'v2';
|
||||
|
||||
workflowDispatchDeep: number;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { createDocument } from 'zod-openapi';
|
||||
import { DashboardPath } from './admin/core/dashboard';
|
||||
import { TagsMap } from './tag';
|
||||
import { AdminSupportPath } from './admin/support';
|
||||
|
||||
export const adminOpenAPIDocument = createDocument({
|
||||
openapi: '3.1.0',
|
||||
|
|
@ -11,18 +10,13 @@ export const adminOpenAPIDocument = createDocument({
|
|||
description: 'FastGPT Admin API 文档'
|
||||
},
|
||||
paths: {
|
||||
...DashboardPath,
|
||||
...AdminSupportPath
|
||||
...DashboardPath
|
||||
},
|
||||
servers: [{ url: '/api' }],
|
||||
'x-tagGroups': [
|
||||
{
|
||||
name: '仪表盘',
|
||||
tags: [TagsMap.adminDashboard]
|
||||
},
|
||||
{
|
||||
name: '系统配置',
|
||||
tags: [TagsMap.adminInform]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import {
|
|||
} from './api';
|
||||
import { TagsMap } from '../../../tag';
|
||||
|
||||
export * from './api';
|
||||
|
||||
export const DashboardPath: OpenAPIPath = {
|
||||
'/admin/core/dashboard/getUserStats': {
|
||||
get: {
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { AdminUserPath } from './user';
|
||||
import type { OpenAPIPath } from '../../type';
|
||||
|
||||
export const AdminSupportPath: OpenAPIPath = {
|
||||
...AdminUserPath
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { AdminInformPath } from './inform';
|
||||
import type { OpenAPIPath } from '../../../type';
|
||||
|
||||
export const AdminUserPath: OpenAPIPath = {
|
||||
...AdminInformPath
|
||||
};
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import { InformLevelEnum } from '../../../../../support/user/inform/constants';
|
||||
|
||||
// Send system inform
|
||||
export const SendSystemInformBodySchema = z.object({
|
||||
title: z.string().meta({ description: '通知标题' }),
|
||||
content: z.string().meta({ description: '通知内容' }),
|
||||
level: z.enum(InformLevelEnum).meta({ description: '通知等级' })
|
||||
});
|
||||
export type SendSystemInformBodyType = z.infer<typeof SendSystemInformBodySchema>;
|
||||
|
||||
// Update system modal
|
||||
export const UpdateSystemModalBodySchema = z.object({
|
||||
content: z.string().meta({ description: '系统弹窗内容' })
|
||||
});
|
||||
export type UpdateSystemModalBodyType = z.infer<typeof UpdateSystemModalBodySchema>;
|
||||
|
||||
// Update operational ad
|
||||
export const UpdateOperationalAdBodySchema = z.object({
|
||||
operationalAdImage: z.string().meta({ description: '活动图片URL' }),
|
||||
operationalAdLink: z.string().meta({ description: '活动链接' })
|
||||
});
|
||||
export type UpdateOperationalAdBodyType = z.infer<typeof UpdateOperationalAdBodySchema>;
|
||||
|
||||
// Update activity ad
|
||||
export const UpdateActivityAdBodySchema = z.object({
|
||||
activityAdImage: z.string().meta({ description: '底部广告图片URL' }),
|
||||
activityAdLink: z.string().meta({ description: '底部广告链接' })
|
||||
});
|
||||
export type UpdateActivityAdBodyType = z.infer<typeof UpdateActivityAdBodySchema>;
|
||||
|
||||
// Response schemas
|
||||
export const SystemMsgModalResponseSchema = z
|
||||
.object({
|
||||
id: z.string().meta({ description: '弹窗ID' }),
|
||||
content: z.string().meta({ description: '弹窗内容' })
|
||||
})
|
||||
.optional();
|
||||
export type SystemMsgModalValueType = z.infer<typeof SystemMsgModalResponseSchema>;
|
||||
|
||||
export const OperationalAdResponseSchema = z
|
||||
.object({
|
||||
id: z.string().meta({ description: '广告ID' }),
|
||||
operationalAdImage: z.string().meta({ description: '广告图片URL' }),
|
||||
operationalAdLink: z.string().meta({ description: '广告链接' })
|
||||
})
|
||||
.optional();
|
||||
export type OperationalAdResponseType = z.infer<typeof OperationalAdResponseSchema>;
|
||||
|
||||
export const ActivityAdResponseSchema = z
|
||||
.object({
|
||||
id: z.string().meta({ description: '广告ID' }),
|
||||
activityAdImage: z.string().meta({ description: '广告图片URL' }),
|
||||
activityAdLink: z.string().meta({ description: '广告链接' })
|
||||
})
|
||||
.optional();
|
||||
export type ActivityAdResponseType = z.infer<typeof ActivityAdResponseSchema>;
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
import type { OpenAPIPath } from '../../../../type';
|
||||
import {
|
||||
SendSystemInformBodySchema,
|
||||
UpdateSystemModalBodySchema,
|
||||
UpdateOperationalAdBodySchema,
|
||||
UpdateActivityAdBodySchema,
|
||||
SystemMsgModalResponseSchema,
|
||||
OperationalAdResponseSchema,
|
||||
ActivityAdResponseSchema
|
||||
} from './api';
|
||||
import { TagsMap } from '../../../../tag';
|
||||
|
||||
export const AdminInformPath: OpenAPIPath = {
|
||||
'/admin/support/user/inform/sendSystemInform': {
|
||||
post: {
|
||||
summary: '发送系统通知给所有用户',
|
||||
description: '向所有用户发送系统通知消息',
|
||||
tags: [TagsMap.adminInform],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SendSystemInformBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功发送系统通知',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/support/user/inform/getSystemMsgModal': {
|
||||
get: {
|
||||
summary: '获取系统弹窗内容',
|
||||
description: '获取系统消息弹窗的内容',
|
||||
tags: [TagsMap.adminInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取系统弹窗内容',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SystemMsgModalResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/admin/support/user/inform/updateSystemModal': {
|
||||
post: {
|
||||
summary: '更新系统弹窗内容',
|
||||
description: '更新系统消息弹窗的内容',
|
||||
tags: [TagsMap.adminInform],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateSystemModalBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新系统弹窗',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/support/user/inform/getOperationalAd': {
|
||||
get: {
|
||||
summary: '获取运营广告',
|
||||
description: '获取运营广告的图片和链接',
|
||||
tags: [TagsMap.adminInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取运营广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: OperationalAdResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/admin/support/user/inform/updateOperationalAd': {
|
||||
post: {
|
||||
summary: '更新运营广告',
|
||||
description: '更新运营广告的图片和链接',
|
||||
tags: [TagsMap.adminInform],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateOperationalAdBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新运营广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/support/user/inform/getActivityAd': {
|
||||
get: {
|
||||
summary: '获取活动广告',
|
||||
description: '获取活动广告的图片和链接',
|
||||
tags: [TagsMap.adminInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取活动广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ActivityAdResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/admin/support/user/inform/updateActivityAd': {
|
||||
post: {
|
||||
summary: '更新活动广告',
|
||||
description: '更新活动广告的图片和链接',
|
||||
tags: [TagsMap.adminInform],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateActivityAdBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新活动广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,17 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const PaginationSchema = z.object({
|
||||
pageSize: z.union([z.number(), z.string()]).optional().describe('每页条数'),
|
||||
offset: z.union([z.number(), z.string()]).optional().describe('偏移量(与页码二选一)'),
|
||||
pageNum: z.union([z.number(), z.string()]).optional().describe('页码(与偏移量二选一)')
|
||||
pageSize: z.union([z.number(), z.string()]),
|
||||
offset: z.union([z.number(), z.string()]).optional(),
|
||||
pageNum: z.union([z.number(), z.string()]).optional()
|
||||
});
|
||||
export type PaginationType = z.infer<typeof PaginationSchema>;
|
||||
|
||||
export const PaginationResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
||||
z.object({
|
||||
total: z.number().optional().default(0),
|
||||
list: z.array(itemSchema).optional().default([])
|
||||
});
|
||||
export type PaginationResponseType<T extends z.ZodTypeAny> = z.infer<
|
||||
ReturnType<typeof PaginationResponseSchema<T>>
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import type { OpenAPIPath } from '../../type';
|
||||
import { AppLogPath } from './log';
|
||||
import { PublishChannelPath } from './publishChannel';
|
||||
|
||||
export const AppPath: OpenAPIPath = {
|
||||
...AppLogPath,
|
||||
...PublishChannelPath
|
||||
...AppLogPath
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,15 +31,12 @@ export type updateLogKeysBody = z.infer<typeof UpdateLogKeysBodySchema>;
|
|||
export const ChatLogItemSchema = z.object({
|
||||
_id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '对话日志 ID' }),
|
||||
chatId: z.string().meta({ example: 'chat123', description: '对话 ID' }),
|
||||
title: z.string().nullish().meta({ example: '用户对话', description: '对话标题' }),
|
||||
title: z.string().optional().meta({ example: '用户对话', description: '对话标题' }),
|
||||
customTitle: z.string().nullish().meta({ example: '自定义标题', description: '自定义对话标题' }),
|
||||
source: z.enum(ChatSourceEnum).meta({ example: ChatSourceEnum.api, description: '对话来源' }),
|
||||
sourceName: z.string().nullish().meta({ example: 'API调用', description: '来源名称' }),
|
||||
sourceName: z.string().optional().meta({ example: 'API调用', description: '来源名称' }),
|
||||
updateTime: z.date().meta({ example: '2024-01-01T00:30:00.000Z', description: '更新时间' }),
|
||||
createTime: z
|
||||
.date()
|
||||
.nullish()
|
||||
.meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }),
|
||||
createTime: z.date().meta({ example: '2024-01-01T00:00:00.000Z', description: '创建时间' }),
|
||||
messageCount: z.int().nullish().meta({ example: 10, description: '消息数量' }),
|
||||
userGoodFeedbackCount: z.int().nullish().meta({ example: 3, description: '好评反馈数量' }),
|
||||
userBadFeedbackCount: z.int().nullish().meta({ example: 1, description: '差评反馈数量' }),
|
||||
|
|
@ -53,7 +50,7 @@ export const ChatLogItemSchema = z.object({
|
|||
totalPoints: z.number().nullish().meta({ example: 150.5, description: '总积分消耗' }),
|
||||
outLinkUid: z.string().nullish().meta({ example: 'outLink123', description: '外链用户 ID' }),
|
||||
tmbId: z.string().nullish().meta({ example: 'tmb123', description: '团队成员 ID' }),
|
||||
sourceMember: SourceMemberSchema.nullish().meta({ description: '来源成员信息' }),
|
||||
sourceMember: SourceMemberSchema.optional().meta({ description: '来源成员信息' }),
|
||||
versionName: z.string().nullish().meta({ example: 'v1.0.0', description: '版本名称' }),
|
||||
region: z.string().nullish().meta({ example: '中国', description: '区域' })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import { PlaygroundPath } from './playground';
|
||||
|
||||
export const PublishChannelPath = {
|
||||
...PlaygroundPath
|
||||
};
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import { ObjectIdSchema } from '../../../../../common/type/mongo';
|
||||
|
||||
// Playground Visibility Config Fields
|
||||
const PlaygroundVisibilityConfigFieldsSchema = z.object({
|
||||
showRunningStatus: z.boolean().meta({
|
||||
example: true,
|
||||
description: '是否显示运行状态'
|
||||
}),
|
||||
showCite: z.boolean().meta({
|
||||
example: true,
|
||||
description: '是否显示引用'
|
||||
}),
|
||||
showFullText: z.boolean().meta({
|
||||
example: true,
|
||||
description: '是否显示全文'
|
||||
}),
|
||||
canDownloadSource: z.boolean().meta({
|
||||
example: true,
|
||||
description: '是否可下载来源'
|
||||
})
|
||||
});
|
||||
|
||||
// Get Playground Visibility Config Parameters
|
||||
export const GetPlaygroundVisibilityConfigParamsSchema = z.object({
|
||||
appId: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '应用 ID'
|
||||
})
|
||||
});
|
||||
export type GetPlaygroundVisibilityConfigParamsType = z.infer<
|
||||
typeof GetPlaygroundVisibilityConfigParamsSchema
|
||||
>;
|
||||
|
||||
// Playground Visibility Config Response
|
||||
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigFieldsSchema;
|
||||
export type PlaygroundVisibilityConfigResponseType = z.infer<
|
||||
typeof PlaygroundVisibilityConfigResponseSchema
|
||||
>;
|
||||
|
||||
// Update Playground Visibility Config Parameters
|
||||
export const UpdatePlaygroundVisibilityConfigParamsSchema = z
|
||||
.object({
|
||||
appId: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '应用 ID'
|
||||
})
|
||||
})
|
||||
.extend(PlaygroundVisibilityConfigFieldsSchema.shape);
|
||||
export type UpdatePlaygroundVisibilityConfigParamsType = z.infer<
|
||||
typeof UpdatePlaygroundVisibilityConfigParamsSchema
|
||||
>;
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import type { OpenAPIPath } from '../../../../type';
|
||||
import {
|
||||
GetPlaygroundVisibilityConfigParamsSchema,
|
||||
PlaygroundVisibilityConfigResponseSchema,
|
||||
UpdatePlaygroundVisibilityConfigParamsSchema
|
||||
} from './api';
|
||||
import { TagsMap } from '../../../../tag';
|
||||
|
||||
export const PlaygroundPath: OpenAPIPath = {
|
||||
'/api/support/outLink/playground/config': {
|
||||
get: {
|
||||
summary: '获取门户配置',
|
||||
description:
|
||||
'获取指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置',
|
||||
tags: [TagsMap.publishChannel],
|
||||
requestParams: {
|
||||
query: GetPlaygroundVisibilityConfigParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功返回门户配置',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PlaygroundVisibilityConfigResponseSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
400: {
|
||||
description: '请求参数错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
code: z.literal(500),
|
||||
statusText: z.literal('Invalid Params'),
|
||||
message: z.string(),
|
||||
data: z.null()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
401: {
|
||||
description: '用户未授权',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
code: z.literal(401),
|
||||
statusText: z.literal('unAuthorization'),
|
||||
message: z.string(),
|
||||
data: z.null()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/support/outLink/playground/update': {
|
||||
post: {
|
||||
summary: '更新门户配置',
|
||||
description:
|
||||
'更新指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置。如果配置不存在则创建新配置',
|
||||
tags: [TagsMap.publishChannel],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdatePlaygroundVisibilityConfigParamsSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新门户配置',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.null()
|
||||
}
|
||||
}
|
||||
},
|
||||
400: {
|
||||
description: '请求参数错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
code: z.literal(500),
|
||||
statusText: z.literal('Invalid Params'),
|
||||
message: z.string(),
|
||||
data: z.null()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
401: {
|
||||
description: '用户未授权',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
code: z.literal(401),
|
||||
statusText: z.literal('unAuthorization'),
|
||||
message: z.string(),
|
||||
data: z.null()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { ObjectIdSchema } from '../../../common/type/mongo';
|
||||
import z from 'zod';
|
||||
|
||||
/* Recently Used Apps */
|
||||
export const GetRecentlyUsedAppsResponseSchema = z.array(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
name: z.string().min(1).describe('应用名称'),
|
||||
avatar: z.string().min(1).describe('应用头像')
|
||||
})
|
||||
);
|
||||
export type GetRecentlyUsedAppsResponseType = z.infer<typeof GetRecentlyUsedAppsResponseSchema>;
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat/type';
|
||||
import { ObjectIdSchema } from '../../../../common/type/mongo';
|
||||
import z from 'zod';
|
||||
|
||||
/* ============ v2/chat/stop ============ */
|
||||
export const StopV2ChatSchema = z
|
||||
.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID'),
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
appId: '1234567890',
|
||||
chatId: '1234567890',
|
||||
outLinkAuthData: {
|
||||
shareId: '1234567890',
|
||||
outLinkUid: '1234567890'
|
||||
}
|
||||
}
|
||||
});
|
||||
export type StopV2ChatParams = z.infer<typeof StopV2ChatSchema>;
|
||||
|
||||
export const StopV2ChatResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean().describe('是否成功停止')
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
success: true
|
||||
}
|
||||
});
|
||||
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>;
|
||||
|
||||
/* ============ chat file ============ */
|
||||
export const PresignChatFileGetUrlSchema = z
|
||||
.object({
|
||||
key: z.string().min(1).describe('文件key'),
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
key: '1234567890',
|
||||
appId: '1234567890',
|
||||
outLinkAuthData: {
|
||||
shareId: '1234567890',
|
||||
outLinkUid: '1234567890'
|
||||
}
|
||||
}
|
||||
});
|
||||
export type PresignChatFileGetUrlParams = z.infer<typeof PresignChatFileGetUrlSchema>;
|
||||
|
||||
export const PresignChatFilePostUrlSchema = z
|
||||
.object({
|
||||
filename: z.string().min(1).describe('文件名'),
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID'),
|
||||
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
filename: '1234567890',
|
||||
appId: '1234567890',
|
||||
chatId: '1234567890',
|
||||
outLinkAuthData: {
|
||||
shareId: '1234567890',
|
||||
outLinkUid: '1234567890'
|
||||
}
|
||||
}
|
||||
});
|
||||
export type PresignChatFilePostUrlParams = z.infer<typeof PresignChatFilePostUrlSchema>;
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import type { OpenAPIPath } from '../../../type';
|
||||
import { TagsMap } from '../../../tag';
|
||||
import {
|
||||
StopV2ChatSchema,
|
||||
StopV2ChatResponseSchema,
|
||||
PresignChatFilePostUrlSchema,
|
||||
PresignChatFileGetUrlSchema
|
||||
} from './api';
|
||||
import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ChatControllerPath: OpenAPIPath = {
|
||||
'/v2/chat/stop': {
|
||||
post: {
|
||||
summary: '停止 Agent 运行',
|
||||
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。
|
||||
LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`,
|
||||
tags: [TagsMap.chatController],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: StopV2ChatSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功停止工作流',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: StopV2ChatResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/presignChatFilePostUrl': {
|
||||
post: {
|
||||
summary: '获取文件上传 URL',
|
||||
description: '获取文件上传 URL',
|
||||
tags: [TagsMap.chatController],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PresignChatFilePostUrlSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功上传对话文件预签名 URL',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CreatePostPresignedUrlResultSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/presignChatFileGetUrl': {
|
||||
post: {
|
||||
summary: '获取文件预览地址',
|
||||
description: '获取文件预览地址',
|
||||
tags: [TagsMap.chatController],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PresignChatFileGetUrlSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取对话文件预签名 URL',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -105,11 +105,11 @@ export const UpdateUserFeedbackBodySchema = z.object({
|
|||
example: 'data123',
|
||||
description: '消息数据 ID'
|
||||
}),
|
||||
userGoodFeedback: z.string().nullish().meta({
|
||||
userGoodFeedback: z.string().optional().nullable().meta({
|
||||
example: '回答很好',
|
||||
description: '用户好评反馈内容'
|
||||
}),
|
||||
userBadFeedback: z.string().nullish().meta({
|
||||
userBadFeedback: z.string().optional().nullable().meta({
|
||||
example: '回答不准确',
|
||||
description: '用户差评反馈内容'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,9 +14,81 @@ import {
|
|||
} from './api';
|
||||
|
||||
export const ChatFeedbackPath: OpenAPIPath = {
|
||||
'/core/chat/feedback/updateFeedbackReadStatus': {
|
||||
post: {
|
||||
summary: '更新反馈阅读状态',
|
||||
description: '标记指定消息的反馈为已读或未读状态',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateFeedbackReadStatusBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新反馈阅读状态',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateFeedbackReadStatusResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/adminUpdate': {
|
||||
post: {
|
||||
summary: '管理员标注反馈',
|
||||
description: '管理员为指定消息添加或更新标注反馈,包含数据集关联信息',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AdminUpdateFeedbackBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新管理员反馈标注',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AdminUpdateFeedbackResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/closeCustom': {
|
||||
post: {
|
||||
summary: '关闭自定义反馈',
|
||||
description: '删除或关闭指定索引位置的自定义反馈条目',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CloseCustomFeedbackBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功关闭自定义反馈',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CloseCustomFeedbackResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/updateUserFeedback': {
|
||||
post: {
|
||||
summary: '添加/更新用户反馈',
|
||||
summary: '更新用户反馈',
|
||||
description: '用户对消息添加或更新好评/差评反馈',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
|
|
@ -61,77 +133,5 @@ export const ChatFeedbackPath: OpenAPIPath = {
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/updateFeedbackReadStatus': {
|
||||
post: {
|
||||
summary: '应用管理员-更新反馈阅读状态',
|
||||
description: '标记指定消息的反馈为已读或未读状态',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateFeedbackReadStatusBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新反馈阅读状态',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateFeedbackReadStatusResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/adminUpdate': {
|
||||
post: {
|
||||
summary: '应用管理员-标注反馈',
|
||||
description: '管理员为指定消息添加或更新标注反馈,包含数据集关联信息',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AdminUpdateFeedbackBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功更新管理员反馈标注',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AdminUpdateFeedbackResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/feedback/closeCustom': {
|
||||
post: {
|
||||
summary: '应用管理员-关闭自定义反馈',
|
||||
description: '删除或关闭指定索引位置的自定义反馈条目',
|
||||
tags: [TagsMap.chatFeedback],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CloseCustomFeedbackBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功关闭自定义反馈',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CloseCustomFeedbackResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import z from 'zod';
|
||||
import { ObjectIdSchema } from '../../../../common/type/mongo';
|
||||
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
|
||||
import { ChatSourceEnum } from '../../../../core/chat/constants';
|
||||
import { PaginationSchema, PaginationResponseSchema } from '../../../api';
|
||||
|
||||
// Get chat histories schema
|
||||
export const GetHistoriesBodySchema = PaginationSchema.extend(OutLinkChatAuthSchema.shape).extend({
|
||||
appId: z.string().optional().describe('应用ID'),
|
||||
source: z.enum(ChatSourceEnum).optional().describe('对话来源'),
|
||||
startCreateTime: z.string().optional().describe('创建时间开始'),
|
||||
endCreateTime: z.string().optional().describe('创建时间结束'),
|
||||
startUpdateTime: z.string().optional().describe('更新时间开始'),
|
||||
endUpdateTime: z.string().optional().describe('更新时间结束')
|
||||
});
|
||||
export type GetHistoriesBodyType = z.infer<typeof GetHistoriesBodySchema>;
|
||||
export const GetHistoriesResponseSchema = PaginationResponseSchema(
|
||||
z.object({
|
||||
chatId: z.string(),
|
||||
updateTime: z.date(),
|
||||
appId: z.string(),
|
||||
customTitle: z.string().optional(),
|
||||
title: z.string(),
|
||||
top: z.boolean().optional()
|
||||
})
|
||||
);
|
||||
export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema>;
|
||||
|
||||
// Update chat history schema
|
||||
export const UpdateHistoryBodySchema = OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID'),
|
||||
title: z.string().optional().describe('标题'),
|
||||
customTitle: z.string().optional().describe('自定义标题'),
|
||||
top: z.boolean().optional().describe('是否置顶')
|
||||
})
|
||||
);
|
||||
export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodySchema>;
|
||||
|
||||
// Delete single chat history schema
|
||||
export const DelChatHistorySchema = OutLinkChatAuthSchema.extend({
|
||||
appId: ObjectIdSchema.describe('应用ID'),
|
||||
chatId: z.string().min(1).describe('对话ID')
|
||||
});
|
||||
export type DelChatHistoryType = z.infer<typeof DelChatHistorySchema>;
|
||||
|
||||
// Clear all chat histories schema
|
||||
export const ClearChatHistoriesSchema = OutLinkChatAuthSchema.extend({
|
||||
appId: ObjectIdSchema.describe('应用ID')
|
||||
});
|
||||
export type ClearChatHistoriesType = z.infer<typeof ClearChatHistoriesSchema>;
|
||||
|
||||
// Batch delete chat histories schema (for log manager)
|
||||
export const ChatBatchDeleteBodySchema = z.object({
|
||||
appId: ObjectIdSchema,
|
||||
chatIds: z
|
||||
.array(z.string().min(1))
|
||||
.min(1)
|
||||
.meta({
|
||||
description: '对话ID列表',
|
||||
example: ['chat_123456', 'chat_789012']
|
||||
})
|
||||
});
|
||||
export type ChatBatchDeleteBodyType = z.infer<typeof ChatBatchDeleteBodySchema>;
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import type { OpenAPIPath } from '../../../type';
|
||||
import { TagsMap } from '../../../tag';
|
||||
import {
|
||||
GetHistoriesBodySchema,
|
||||
GetHistoriesResponseSchema,
|
||||
UpdateHistoryBodySchema,
|
||||
ChatBatchDeleteBodySchema,
|
||||
DelChatHistorySchema,
|
||||
ClearChatHistoriesSchema
|
||||
} from './api';
|
||||
|
||||
export const ChatHistoryPath: OpenAPIPath = {
|
||||
'/core/chat/history/getHistories': {
|
||||
post: {
|
||||
summary: '获取对话历史列表',
|
||||
description: '分页获取指定应用的对话历史记录',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetHistoriesBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取对话历史列表',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetHistoriesResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/updateHistory': {
|
||||
put: {
|
||||
summary: '修改对话历史',
|
||||
description: '修改对话历史的标题、自定义标题或置顶状态',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UpdateHistoryBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功修改对话历史'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/delHistory': {
|
||||
delete: {
|
||||
summary: '删除单个对话历史',
|
||||
description: '软删除指定的单个对话记录',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DelChatHistorySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功删除对话'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/clearHistories': {
|
||||
delete: {
|
||||
summary: '清空应用对话历史',
|
||||
description: '清空指定应用的所有对话记录(软删除)',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestParams: {
|
||||
query: ClearChatHistoriesSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功清空对话历史'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/history/batchDelete': {
|
||||
post: {
|
||||
summary: '批量删除对话历史',
|
||||
description: '批量删除指定应用的多个对话记录(真实删除),需应用日志权限。',
|
||||
tags: [TagsMap.chatHistory],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ChatBatchDeleteBodySchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功删除对话'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -2,29 +2,58 @@ import type { OpenAPIPath } from '../../type';
|
|||
import { ChatSettingPath } from './setting';
|
||||
import { ChatFavouriteAppPath } from './favourite/index';
|
||||
import { ChatFeedbackPath } from './feedback/index';
|
||||
import { ChatHistoryPath } from './history/index';
|
||||
import { GetRecentlyUsedAppsResponseSchema } from './api';
|
||||
import { z } from 'zod';
|
||||
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
|
||||
import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api';
|
||||
import { TagsMap } from '../../tag';
|
||||
import { ChatControllerPath } from './controler';
|
||||
|
||||
export const ChatPath: OpenAPIPath = {
|
||||
...ChatSettingPath,
|
||||
...ChatFavouriteAppPath,
|
||||
...ChatFeedbackPath,
|
||||
...ChatHistoryPath,
|
||||
...ChatControllerPath,
|
||||
|
||||
'/core/chat/recentlyUsed': {
|
||||
get: {
|
||||
summary: '获取最近使用的应用',
|
||||
description: '获取最近使用的应用',
|
||||
'/core/chat/presignChatFileGetUrl': {
|
||||
post: {
|
||||
summary: '获取对话文件预签名 URL',
|
||||
description: '获取对话文件的预签名 URL',
|
||||
tags: [TagsMap.chatPage],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PresignChatFileGetUrlSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功返回最近使用的应用',
|
||||
description: '成功获取对话文件预签名 URL',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetRecentlyUsedAppsResponseSchema
|
||||
schema: z.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/core/chat/presignChatFilePostUrl': {
|
||||
post: {
|
||||
summary: '上传对话文件预签名 URL',
|
||||
description: '上传对话文件的预签名 URL',
|
||||
tags: [TagsMap.chatPage],
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: PresignChatFilePostUrlSchema
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功上传对话文件预签名 URL',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: CreatePostPresignedUrlResultSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { createDocument } from 'zod-openapi';
|
||||
import { ChatPath } from './core/chat';
|
||||
import { ApiKeyPath } from './support/openapi';
|
||||
import { TagsMap } from './tag';
|
||||
import { PluginPath } from './core/plugin';
|
||||
import { WalletPath } from './support/wallet';
|
||||
import { CustomDomainPath } from './support/customDomain';
|
||||
import { AppPath } from './core/app';
|
||||
import { SupportPath } from './support';
|
||||
|
||||
export const openAPIDocument = createDocument({
|
||||
openapi: '3.1.0',
|
||||
|
|
@ -15,26 +17,28 @@ export const openAPIDocument = createDocument({
|
|||
paths: {
|
||||
...AppPath,
|
||||
...ChatPath,
|
||||
...ApiKeyPath,
|
||||
...PluginPath,
|
||||
...SupportPath
|
||||
...WalletPath,
|
||||
...CustomDomainPath
|
||||
},
|
||||
servers: [{ url: '/api' }],
|
||||
'x-tagGroups': [
|
||||
{
|
||||
name: 'Agent 应用',
|
||||
tags: [TagsMap.appLog, TagsMap.publishChannel]
|
||||
tags: [TagsMap.appLog]
|
||||
},
|
||||
{
|
||||
name: '对话管理',
|
||||
tags: [TagsMap.chatHistory, TagsMap.chatPage, TagsMap.chatFeedback, TagsMap.chatSetting]
|
||||
tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatFeedback]
|
||||
},
|
||||
{
|
||||
name: '插件系统',
|
||||
tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam]
|
||||
},
|
||||
{
|
||||
name: '用户体系',
|
||||
tags: [TagsMap.userInform, TagsMap.walletBill, TagsMap.walletDiscountCoupon]
|
||||
name: '支付系统',
|
||||
tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon]
|
||||
},
|
||||
{
|
||||
name: '通用-辅助功能',
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { UserPath } from './user';
|
||||
import type { OpenAPIPath } from '../type';
|
||||
import { WalletPath } from './wallet';
|
||||
import { ApiKeyPath } from './openapi';
|
||||
import { CustomDomainPath } from './customDomain';
|
||||
|
||||
export const SupportPath: OpenAPIPath = {
|
||||
...UserPath,
|
||||
...WalletPath,
|
||||
...ApiKeyPath,
|
||||
...CustomDomainPath
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { UserInformPath } from './inform';
|
||||
import type { OpenAPIPath } from '../../type';
|
||||
|
||||
export const UserPath: OpenAPIPath = {
|
||||
...UserInformPath
|
||||
};
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import type { OpenAPIPath } from '../../../type';
|
||||
import {
|
||||
SystemMsgModalResponseSchema,
|
||||
OperationalAdResponseSchema,
|
||||
ActivityAdResponseSchema
|
||||
} from '../../../admin/support/user/inform/api';
|
||||
import { TagsMap } from '../../../tag';
|
||||
|
||||
export const UserInformPath: OpenAPIPath = {
|
||||
'/proApi/support/user/inform/getSystemMsgModal': {
|
||||
get: {
|
||||
summary: '获取系统弹窗内容',
|
||||
description: '获取系统消息弹窗的内容',
|
||||
tags: [TagsMap.userInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取系统弹窗内容',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: SystemMsgModalResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/proApi/support/user/inform/getOperationalAd': {
|
||||
get: {
|
||||
summary: '获取运营广告',
|
||||
description: '获取运营广告的图片和链接',
|
||||
tags: [TagsMap.userInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取运营广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: OperationalAdResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/proApi/support/user/inform/getActivityAd': {
|
||||
get: {
|
||||
summary: '获取活动广告',
|
||||
description: '获取活动广告的图片和链接',
|
||||
tags: [TagsMap.userInform],
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取活动广告',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ActivityAdResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -25,32 +25,6 @@ export const BillListResponseSchema = z.object({
|
|||
});
|
||||
export type GetBillListResponseType = z.infer<typeof BillListResponseSchema>;
|
||||
|
||||
// Bill detail
|
||||
export const BillDetailQuerySchema = z.object({
|
||||
billId: ObjectIdSchema.meta({ description: '订单 ID' })
|
||||
});
|
||||
export type BillDetailQueryType = z.infer<typeof BillDetailQuerySchema>;
|
||||
export const BillDetailResponseSchema = BillSchema.safeExtend({
|
||||
discountCouponName: z.string().optional(),
|
||||
couponDetail: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
type: z.enum(CouponTypeEnum),
|
||||
subscriptions: z.array(
|
||||
z.object({
|
||||
type: z.enum(SubTypeEnum),
|
||||
durationDay: z.number(),
|
||||
totalPoints: z.number().optional(),
|
||||
level: z.enum(StandardSubLevelEnum).optional(),
|
||||
extraDatasetSize: z.number().optional(),
|
||||
customConfig: z.record(z.string(), z.any()).optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
export type BillDetailResponseType = z.infer<typeof BillDetailResponseSchema>;
|
||||
|
||||
// Create
|
||||
export const CreateStandPlanBillSchema = z
|
||||
.object({
|
||||
|
|
@ -114,14 +88,30 @@ export const CheckPayResultResponseSchema = z.object({
|
|||
});
|
||||
export type CheckPayResultResponseType = z.infer<typeof CheckPayResultResponseSchema>;
|
||||
|
||||
// Bill detail
|
||||
export const BillDetailResponseSchema = BillSchema.safeExtend({
|
||||
discountCouponName: z.string().optional(),
|
||||
couponDetail: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
type: z.enum(CouponTypeEnum),
|
||||
subscriptions: z.array(
|
||||
z.object({
|
||||
type: z.enum(SubTypeEnum),
|
||||
durationDay: z.number(),
|
||||
totalPoints: z.number().optional(),
|
||||
level: z.enum(StandardSubLevelEnum).optional(),
|
||||
extraDatasetSize: z.number().optional(),
|
||||
customConfig: z.record(z.string(), z.any()).optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
export type BillDetailResponseType = z.infer<typeof BillDetailResponseSchema>;
|
||||
|
||||
// Cancel bill
|
||||
export const CancelBillPropsSchema = z.object({
|
||||
billId: ObjectIdSchema.meta({ description: '订单 ID' })
|
||||
});
|
||||
export type CancelBillPropsType = z.infer<typeof CancelBillPropsSchema>;
|
||||
|
||||
// Check pay result
|
||||
export const CheckPayResultQuerySchema = z.object({
|
||||
payId: ObjectIdSchema.meta({ description: '订单 ID' })
|
||||
});
|
||||
export type CheckPayResultQueryType = z.infer<typeof CheckPayResultQuerySchema>;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import {
|
|||
CheckPayResultResponseSchema,
|
||||
BillDetailResponseSchema,
|
||||
BillListQuerySchema,
|
||||
CancelBillPropsSchema,
|
||||
CheckPayResultQuerySchema,
|
||||
BillDetailQuerySchema
|
||||
CancelBillPropsSchema
|
||||
} from './api';
|
||||
import { TagsMap } from '../../../tag';
|
||||
import { ObjectIdSchema } from '../../../../common/type/mongo';
|
||||
|
|
@ -70,7 +68,11 @@ export const BillPath: OpenAPIPath = {
|
|||
description: '检查订单的支付状态,用于轮询支付结果',
|
||||
tags: [TagsMap.walletBill],
|
||||
requestParams: {
|
||||
query: CheckPayResultQuerySchema
|
||||
query: z.object({
|
||||
payId: ObjectIdSchema.meta({
|
||||
description: '订单 ID'
|
||||
})
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
|
|
@ -90,7 +92,11 @@ export const BillPath: OpenAPIPath = {
|
|||
description: '根据订单 ID 获取订单详细信息,包括优惠券名称等',
|
||||
tags: [TagsMap.walletBill],
|
||||
requestParams: {
|
||||
query: BillDetailQuerySchema
|
||||
query: z.object({
|
||||
billId: ObjectIdSchema.meta({
|
||||
description: '订单 ID'
|
||||
})
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ export const TagsMap = {
|
|||
|
||||
// Chat - home
|
||||
chatPage: '对话页',
|
||||
chatController: '对话框操作',
|
||||
chatHistory: '对话历史管理',
|
||||
chatSetting: '门户页配置',
|
||||
chatFeedback: '对话反馈',
|
||||
|
||||
|
|
@ -14,16 +12,11 @@ export const TagsMap = {
|
|||
pluginToolTag: '工具标签',
|
||||
pluginTeam: '团队插件管理',
|
||||
|
||||
// Publish Channel
|
||||
publishChannel: '发布渠道',
|
||||
|
||||
/* Support */
|
||||
// Wallet
|
||||
walletBill: '订单',
|
||||
walletDiscountCoupon: '优惠券',
|
||||
customDomain: '自定义域名',
|
||||
// User
|
||||
userInform: '用户通知',
|
||||
|
||||
/* Common */
|
||||
// APIKey
|
||||
|
|
@ -35,7 +28,5 @@ export const TagsMap = {
|
|||
pluginAdmin: '管理员插件管理',
|
||||
pluginToolAdmin: '管理员系统工具管理',
|
||||
// Data
|
||||
adminDashboard: '管理员仪表盘',
|
||||
// Inform
|
||||
adminInform: '管理员通知管理'
|
||||
adminDashboard: '管理员仪表盘'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { z } from 'zod';
|
||||
import type { HistoryItemType } from '../../core/chat/type.d';
|
||||
import type { OutLinkSchema, PlaygroundVisibilityConfigType } from './type.d';
|
||||
import { PlaygroundVisibilityConfigSchema } from './type.d';
|
||||
import type { OutLinkSchema } from './type.d';
|
||||
|
||||
export type AuthOutLinkInitProps = {
|
||||
outLinkUid: string;
|
||||
|
|
@ -12,20 +10,3 @@ export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSch
|
|||
export type AuthOutLinkResponse = {
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export const UpdatePlaygroundVisibilityConfigBodySchema = PlaygroundVisibilityConfigSchema.extend({
|
||||
appId: z.string().min(1, 'App ID is required')
|
||||
});
|
||||
export type UpdatePlaygroundVisibilityConfigBody = z.infer<
|
||||
typeof UpdatePlaygroundVisibilityConfigBodySchema
|
||||
>;
|
||||
|
||||
export const PlaygroundVisibilityConfigQuerySchema = z.object({
|
||||
appId: z.string().min(1, 'App ID is required')
|
||||
});
|
||||
export type PlaygroundVisibilityConfigQuery = z.infer<typeof PlaygroundVisibilityConfigQuerySchema>;
|
||||
|
||||
export const PlaygroundVisibilityConfigResponseSchema = PlaygroundVisibilityConfigSchema;
|
||||
export type PlaygroundVisibilityConfigResponse = z.infer<
|
||||
typeof PlaygroundVisibilityConfigResponseSchema
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,5 @@ export enum PublishChannelEnum {
|
|||
feishu = 'feishu',
|
||||
dingtalk = 'dingtalk',
|
||||
wecom = 'wecom',
|
||||
officialAccount = 'official_account',
|
||||
playground = 'playground'
|
||||
officialAccount = 'official_account'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { z } from 'zod';
|
||||
import { AppSchema } from '../../core/app/type';
|
||||
import type { PublishChannelEnum } from './constant';
|
||||
import { RequireOnlyOne } from '../../common/type/utils';
|
||||
|
|
@ -64,14 +63,14 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
|
|||
lastTime: Date;
|
||||
type: PublishChannelEnum;
|
||||
|
||||
// whether to show the quote
|
||||
showCite: boolean;
|
||||
// whether to show the running status
|
||||
showRunningStatus: boolean;
|
||||
// whether to show the full text reader
|
||||
showFullText: boolean;
|
||||
// whether can download source
|
||||
canDownloadSource: boolean;
|
||||
// whether the response content is detailed
|
||||
responseDetail: boolean;
|
||||
// whether to hide the node status
|
||||
showNodeStatus?: boolean;
|
||||
// wheter to show the full text reader
|
||||
// showFullText?: boolean;
|
||||
// whether to show the complete quote
|
||||
showRawSource?: boolean;
|
||||
|
||||
// response when request
|
||||
immediateResponse?: string;
|
||||
|
|
@ -94,10 +93,10 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
|
|||
export type OutLinkEditType<T = undefined> = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
showCite?: OutLinkSchema<T>['showCite'];
|
||||
showRunningStatus?: OutLinkSchema<T>['showRunningStatus'];
|
||||
showFullText?: OutLinkSchema<T>['showFullText'];
|
||||
canDownloadSource?: OutLinkSchema<T>['canDownloadSource'];
|
||||
responseDetail?: OutLinkSchema<T>['responseDetail'];
|
||||
showNodeStatus?: OutLinkSchema<T>['showNodeStatus'];
|
||||
// showFullText?: OutLinkSchema<T>['showFullText'];
|
||||
showRawSource?: OutLinkSchema<T>['showRawSource'];
|
||||
// response when request
|
||||
immediateResponse?: string;
|
||||
// response when error or other situation
|
||||
|
|
@ -107,12 +106,3 @@ export type OutLinkEditType<T = undefined> = {
|
|||
// config for specific platform
|
||||
app?: T;
|
||||
};
|
||||
|
||||
export const PlaygroundVisibilityConfigSchema = z.object({
|
||||
showRunningStatus: z.boolean(),
|
||||
showCite: z.boolean(),
|
||||
showFullText: z.boolean(),
|
||||
canDownloadSource: z.boolean()
|
||||
});
|
||||
|
||||
export type PlaygroundVisibilityConfigType = z.infer<typeof PlaygroundVisibilityConfigSchema>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
type ShareChatAuthProps = {
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
};
|
||||
type TeamChatAuthProps = {
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
export type OutLinkChatAuthProps = ShareChatAuthProps & TeamChatAuthProps;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import z from 'zod';
|
||||
|
||||
export const ShareChatAuthSchema = z.object({
|
||||
shareId: z.string().optional().describe('分享链接ID'),
|
||||
outLinkUid: z.string().optional().describe('外链用户ID')
|
||||
});
|
||||
export type ShareChatAuthProps = z.infer<typeof ShareChatAuthSchema>;
|
||||
|
||||
export const TeamChatAuthSchema = z.object({
|
||||
teamId: z.string().optional().describe('团队ID'),
|
||||
teamToken: z.string().optional().describe('团队Token')
|
||||
});
|
||||
export type TeamChatAuthProps = z.infer<typeof TeamChatAuthSchema>;
|
||||
|
||||
export const OutLinkChatAuthSchema = ShareChatAuthSchema.extend(TeamChatAuthSchema.shape);
|
||||
export type OutLinkChatAuthProps = z.infer<typeof OutLinkChatAuthSchema>;
|
||||
|
|
@ -2,7 +2,8 @@ import type { auditLogMap, adminAuditLogMap } from '../../../../web/support/user
|
|||
|
||||
export enum AdminAuditEventEnum {
|
||||
ADMIN_LOGIN = 'ADMIN_LOGIN',
|
||||
|
||||
ADMIN_UPDATE_SYSTEM_MODAL = 'ADMIN_UPDATE_SYSTEM_MODAL',
|
||||
ADMIN_SEND_SYSTEM_INFORM = 'ADMIN_SEND_SYSTEM_INFORM',
|
||||
ADMIN_ADD_USER = 'ADMIN_ADD_USER',
|
||||
ADMIN_UPDATE_USER = 'ADMIN_UPDATE_USER',
|
||||
ADMIN_UPDATE_TEAM = 'ADMIN_UPDATE_TEAM',
|
||||
|
|
@ -20,13 +21,7 @@ export enum AdminAuditEventEnum {
|
|||
ADMIN_DELETE_PLUGIN = 'ADMIN_DELETE_PLUGIN',
|
||||
ADMIN_CREATE_PLUGIN_GROUP = 'ADMIN_CREATE_PLUGIN_GROUP',
|
||||
ADMIN_UPDATE_PLUGIN_GROUP = 'ADMIN_UPDATE_PLUGIN_GROUP',
|
||||
ADMIN_DELETE_PLUGIN_GROUP = 'ADMIN_DELETE_PLUGIN_GROUP',
|
||||
|
||||
// Inform
|
||||
ADMIN_UPDATE_SYSTEM_MODAL = 'ADMIN_UPDATE_SYSTEM_MODAL',
|
||||
ADMIN_SEND_SYSTEM_INFORM = 'ADMIN_SEND_SYSTEM_INFORM',
|
||||
ADMIN_UPDATE_ACTIVITY_AD = 'ADMIN_UPDATE_ACTIVITY_AD',
|
||||
ADMIN_UPDATE_OPERATIONAL_AD = 'ADMIN_UPDATE_OPERATIONAL_AD'
|
||||
ADMIN_DELETE_PLUGIN_GROUP = 'ADMIN_DELETE_PLUGIN_GROUP'
|
||||
}
|
||||
|
||||
export enum AuditEventEnum {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { SourceMemberType } from '../user/type';
|
||||
import type { AuditEventEnum } from './constants';
|
||||
|
||||
export type TeamAuditSchemaType = {
|
||||
export type OperationLogSchema = {
|
||||
_id: string;
|
||||
tmbId: string;
|
||||
teamId: string;
|
||||
|
|
@ -10,7 +10,7 @@ export type TeamAuditSchemaType = {
|
|||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TeamAuditListItemType = {
|
||||
export type OperationListItemType = {
|
||||
_id: string;
|
||||
sourceMember: SourceMemberType;
|
||||
event: `${AuditEventEnum}`;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ export type UserType = {
|
|||
|
||||
export const SourceMemberSchema = z.object({
|
||||
name: z.string().meta({ example: '张三', description: '成员名称' }),
|
||||
avatar: z.string().nullish().meta({ description: '成员头像' }),
|
||||
avatar: z
|
||||
.string()
|
||||
.meta({ example: 'https://cloud.fastgpt.cn/avatar.png', description: '成员头像' }),
|
||||
status: z
|
||||
.enum(TeamMemberStatusEnum)
|
||||
.meta({ example: TeamMemberStatusEnum.active, description: '成员状态' })
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { StandardSubLevelEnum, SubModeEnum } from './constants';
|
||||
import { TeamSubSchema } from './type.d';
|
||||
|
||||
export type StandardSubPlanParams = {
|
||||
level: `${StandardSubLevelEnum}`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import type { StandardSubLevelEnum, SubModeEnum, SubTypeEnum } from './constants';
|
||||
|
||||
// Content of plan
|
||||
export type TeamStandardSubPlanItemType = {
|
||||
name?: string;
|
||||
desc?: string; // Plan description
|
||||
price: number; // read price / month
|
||||
|
||||
pointPrice: number; // read price/ one thousand
|
||||
|
||||
totalPoints: number; // n
|
||||
maxTeamMember: number;
|
||||
maxAppAmount: number; // max app or plugin amount
|
||||
maxDatasetAmount: number;
|
||||
maxDatasetSize: number;
|
||||
|
||||
requestsPerMinute?: number;
|
||||
appRegistrationCount?: number;
|
||||
chatHistoryStoreDuration: number; // n day
|
||||
websiteSyncPerDataset?: number;
|
||||
auditLogStoreDuration?: number;
|
||||
ticketResponseTime?: number;
|
||||
customDomain?: number;
|
||||
|
||||
// Custom plan specific fields
|
||||
priceDescription?: string;
|
||||
customFormUrl?: string;
|
||||
customDescriptions?: string[];
|
||||
};
|
||||
|
||||
export type StandSubPlanLevelMapType = Record<
|
||||
`${StandardSubLevelEnum}`,
|
||||
TeamStandardSubPlanItemType
|
||||
>;
|
||||
|
||||
export type PointsPackageItem = {
|
||||
points: number;
|
||||
month: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type SubPlanType = {
|
||||
[SubTypeEnum.standard]?: StandSubPlanLevelMapType;
|
||||
planDescriptionUrl?: string;
|
||||
appRegistrationUrl?: string;
|
||||
communitySupportTip?: string;
|
||||
[SubTypeEnum.extraDatasetSize]: {
|
||||
price: number;
|
||||
};
|
||||
[SubTypeEnum.extraPoints]: {
|
||||
packages: PointsPackageItem[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TeamSubSchema = {
|
||||
_id: string;
|
||||
teamId: string;
|
||||
type: `${SubTypeEnum}`;
|
||||
startTime: Date;
|
||||
expiredTime: Date;
|
||||
|
||||
currentMode: `${SubModeEnum}`;
|
||||
nextMode: `${SubModeEnum}`;
|
||||
currentSubLevel: StandardSubLevelEnum;
|
||||
nextSubLevel: StandardSubLevelEnum;
|
||||
maxTeamMember?: number;
|
||||
maxApp?: number;
|
||||
maxDataset?: number;
|
||||
|
||||
// custom level configurations
|
||||
requestsPerMinute?: number;
|
||||
chatHistoryStoreDuration?: number;
|
||||
maxDatasetSize?: number;
|
||||
websiteSyncPerDataset?: number;
|
||||
appRegistrationCount?: number;
|
||||
auditLogStoreDuration?: number;
|
||||
ticketResponseTime?: number;
|
||||
customDomain?: number;
|
||||
|
||||
totalPoints: number;
|
||||
surplusPoints: number;
|
||||
|
||||
currentExtraDatasetSize: number;
|
||||
};
|
||||
|
||||
export type TeamPlanStatusType = {
|
||||
[SubTypeEnum.standard]?: TeamSubSchema;
|
||||
standardConstants?: TeamStandardSubPlanItemType;
|
||||
|
||||
totalPoints: number;
|
||||
usedPoints: number;
|
||||
|
||||
// standard + extra
|
||||
datasetMaxSize: number;
|
||||
};
|
||||
|
||||
export type ClientTeamPlanStatusType = TeamPlanStatusType & {
|
||||
usedMember: number;
|
||||
usedAppAmount: number;
|
||||
usedDatasetSize: number;
|
||||
usedDatasetIndexSize: number;
|
||||
usedRegistrationCount: number;
|
||||
};
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import z from 'zod';
|
||||
import { StandardSubLevelEnum, SubModeEnum, SubTypeEnum } from './constants';
|
||||
import { ObjectIdSchema } from '../../../common/type/mongo';
|
||||
|
||||
// Content of plan
|
||||
export const TeamStandardSubPlanItemSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
desc: z.string().optional(),
|
||||
price: z.number(),
|
||||
|
||||
totalPoints: z.int(), // 总积分
|
||||
maxTeamMember: z.int(),
|
||||
maxAppAmount: z.int(),
|
||||
maxDatasetAmount: z.int(),
|
||||
maxDatasetSize: z.int(),
|
||||
|
||||
requestsPerMinute: z.int().optional(), // QPM
|
||||
appRegistrationCount: z.int().optional(), // 应用备案数量
|
||||
chatHistoryStoreDuration: z.int(), // 历史记录保留天数
|
||||
websiteSyncPerDataset: z.int().optional(), // 站点同步最大页面
|
||||
auditLogStoreDuration: z.int().optional(), // 审计日志保留天数
|
||||
ticketResponseTime: z.int().optional(), // 工单支持时间
|
||||
customDomain: z.int().optional(), // 自定义域名数量
|
||||
|
||||
// 定制套餐
|
||||
priceDescription: z.string().optional(), // 价格描述
|
||||
customFormUrl: z.string().optional(), // 自定义表单 URL
|
||||
customDescriptions: z.array(z.string()).optional(), // 自定义描述
|
||||
|
||||
// Active
|
||||
annualBonusPoints: z.int().optional(), // 年度赠送积分
|
||||
|
||||
// @deprecated
|
||||
pointPrice: z.number().optional()
|
||||
});
|
||||
export type TeamStandardSubPlanItemType = z.infer<typeof TeamStandardSubPlanItemSchema>;
|
||||
|
||||
export const StandSubPlanLevelMapSchema = z.record(
|
||||
z.enum(StandardSubLevelEnum),
|
||||
TeamStandardSubPlanItemSchema
|
||||
);
|
||||
export type StandSubPlanLevelMapType = z.infer<typeof StandSubPlanLevelMapSchema>;
|
||||
|
||||
export const PointsPackageItemSchema = z.object({
|
||||
points: z.int(),
|
||||
month: z.int(),
|
||||
price: z.number(),
|
||||
activityBonusPoints: z.int().optional() // 活动赠送积分
|
||||
});
|
||||
export type PointsPackageItem = z.infer<typeof PointsPackageItemSchema>;
|
||||
|
||||
export const SubPlanSchema = z.object({
|
||||
[SubTypeEnum.standard]: StandSubPlanLevelMapSchema.optional(),
|
||||
[SubTypeEnum.extraDatasetSize]: z.object({ price: z.number() }).optional(),
|
||||
[SubTypeEnum.extraPoints]: z.object({ packages: PointsPackageItemSchema.array() }).optional(),
|
||||
planDescriptionUrl: z.string().optional(),
|
||||
appRegistrationUrl: z.string().optional(),
|
||||
communitySupportTip: z.string().optional(),
|
||||
activityExpirationTime: z.date().optional()
|
||||
});
|
||||
export type SubPlanType = z.infer<typeof SubPlanSchema>;
|
||||
|
||||
export const TeamSubSchema = z.object({
|
||||
_id: ObjectIdSchema,
|
||||
teamId: ObjectIdSchema,
|
||||
type: z.enum(SubTypeEnum),
|
||||
startTime: z.date(),
|
||||
expiredTime: z.date(),
|
||||
|
||||
currentMode: z.enum(SubModeEnum),
|
||||
nextMode: z.enum(SubModeEnum),
|
||||
currentSubLevel: z.enum(StandardSubLevelEnum),
|
||||
nextSubLevel: z.enum(StandardSubLevelEnum),
|
||||
|
||||
maxTeamMember: z.int().optional(),
|
||||
maxApp: z.int().optional(),
|
||||
maxDataset: z.int().optional(),
|
||||
totalPoints: z.int(),
|
||||
annualBonusPoints: z.int().optional(),
|
||||
surplusPoints: z.int(),
|
||||
currentExtraDatasetSize: z.int(),
|
||||
|
||||
// 定制版特有属性
|
||||
requestsPerMinute: z.int().optional(),
|
||||
chatHistoryStoreDuration: z.int().optional(),
|
||||
maxDatasetSize: z.int().optional(),
|
||||
websiteSyncPerDataset: z.int().optional(),
|
||||
appRegistrationCount: z.int().optional(),
|
||||
auditLogStoreDuration: z.int().optional(),
|
||||
ticketResponseTime: z.int().optional(),
|
||||
customDomain: z.int().optional()
|
||||
});
|
||||
export type TeamSubSchemaType = z.infer<typeof TeamSubSchema>;
|
||||
|
||||
export const TeamPlanStatusSchema = z.object({
|
||||
[SubTypeEnum.standard]: TeamSubSchema.optional(),
|
||||
standardConstants: TeamStandardSubPlanItemSchema.optional(),
|
||||
totalPoints: z.int(),
|
||||
usedPoints: z.int(),
|
||||
datasetMaxSize: z.int()
|
||||
});
|
||||
export type TeamPlanStatusType = z.infer<typeof TeamPlanStatusSchema>;
|
||||
|
||||
export const ClientTeamPlanStatusSchema = TeamPlanStatusSchema.extend({
|
||||
usedMember: z.int(),
|
||||
usedAppAmount: z.int(),
|
||||
usedDatasetSize: z.int(),
|
||||
usedDatasetIndexSize: z.int(),
|
||||
usedRegistrationCount: z.int()
|
||||
});
|
||||
export type ClientTeamPlanStatusType = z.infer<typeof ClientTeamPlanStatusSchema>;
|
||||
|
|
@ -8,7 +8,6 @@ import {
|
|||
} from 'bullmq';
|
||||
import { addLog } from '../system/log';
|
||||
import { newQueueRedisConnection, newWorkerRedisConnection } from '../redis';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
const defaultWorkerOpts: Omit<ConnectionOptions, 'connection'> = {
|
||||
removeOnComplete: {
|
||||
|
|
@ -26,7 +25,6 @@ export enum QueueNames {
|
|||
|
||||
// Delete Queue
|
||||
datasetDelete = 'datasetDelete',
|
||||
appDelete = 'appDelete',
|
||||
// @deprecated
|
||||
websiteSync = 'websiteSync'
|
||||
}
|
||||
|
|
@ -60,7 +58,7 @@ export function getQueue<DataType, ReturnType = void>(
|
|||
|
||||
// default error handler, to avoid unhandled exceptions
|
||||
newQueue.on('error', (error) => {
|
||||
addLog.error(`MQ Queue] error`, error);
|
||||
addLog.error(`MQ Queue [${name}]: ${error.message}`, error);
|
||||
});
|
||||
queues.set(name, newQueue);
|
||||
return newQueue;
|
||||
|
|
@ -76,59 +74,18 @@ export function getWorker<DataType, ReturnType = void>(
|
|||
return worker as Worker<DataType, ReturnType>;
|
||||
}
|
||||
|
||||
const createWorker = () => {
|
||||
const newWorker = new Worker<DataType, ReturnType>(name.toString(), processor, {
|
||||
connection: newWorkerRedisConnection(),
|
||||
...defaultWorkerOpts,
|
||||
// BullMQ Worker important settings
|
||||
lockDuration: 600000, // 10 minutes for large file operations
|
||||
stalledInterval: 30000, // Check for stalled jobs every 30s
|
||||
maxStalledCount: 3, // Move job to failed after 1 stall (default behavior)
|
||||
...opts
|
||||
});
|
||||
|
||||
// Worker is ready to process jobs (fired on initial connection and after reconnection)
|
||||
newWorker.on('ready', () => {
|
||||
addLog.info(`[MQ Worker] ready`, { name });
|
||||
});
|
||||
// default error handler, to avoid unhandled exceptions
|
||||
newWorker.on('error', async (error) => {
|
||||
addLog.error(`[MQ Worker] error`, {
|
||||
message: error.message,
|
||||
data: { name }
|
||||
});
|
||||
});
|
||||
// Critical: Worker has been closed - remove from pool and restart
|
||||
newWorker.on('closed', async () => {
|
||||
addLog.warn(`[MQ Worker] closed, attempting restart...`);
|
||||
|
||||
// Clean up: remove all listeners to prevent memory leaks
|
||||
newWorker.removeAllListeners();
|
||||
|
||||
// Retry create new worker with infinite retries
|
||||
while (true) {
|
||||
try {
|
||||
// Call getWorker to create a new worker (now workers.get(name) returns undefined)
|
||||
const worker = createWorker();
|
||||
workers.set(name, worker);
|
||||
addLog.info(`[MQ Worker] restarted successfully`);
|
||||
break;
|
||||
} catch (error) {
|
||||
addLog.error(`[MQ Worker] failed to restart, retrying...`, error);
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
newWorker.on('paused', async () => {
|
||||
addLog.warn(`[MQ Worker] paused`);
|
||||
await delay(1000);
|
||||
newWorker.resume();
|
||||
});
|
||||
|
||||
return newWorker;
|
||||
};
|
||||
|
||||
const newWorker = createWorker();
|
||||
const newWorker = new Worker<DataType, ReturnType>(name.toString(), processor, {
|
||||
connection: newWorkerRedisConnection(),
|
||||
...defaultWorkerOpts,
|
||||
...opts
|
||||
});
|
||||
// default error handler, to avoid unhandled exceptions
|
||||
newWorker.on('error', (error) => {
|
||||
addLog.error(`MQ Worker [${name}]: ${error.message}`, error);
|
||||
});
|
||||
newWorker.on('failed', (jobId, error) => {
|
||||
addLog.error(`MQ Worker [${name}]: ${error.message}`, error);
|
||||
});
|
||||
workers.set(name, newWorker);
|
||||
return newWorker;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import './init';
|
||||
import { getAllKeysByPrefix, getGlobalRedisConnection } from '../../common/redis';
|
||||
import { getGlobalRedisConnection } from '../../common/redis';
|
||||
import type { SystemCacheKeyEnum } from './type';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { initCache } from './init';
|
||||
|
|
@ -18,14 +18,11 @@ export const refreshVersionKey = async (key: `${SystemCacheKeyEnum}`, id?: strin
|
|||
|
||||
const val = randomUUID();
|
||||
const versionKey = id ? `${cachePrefix}${key}:${id}` : `${cachePrefix}${key}`;
|
||||
|
||||
if (id === '*') {
|
||||
const pattern = `${cachePrefix}${key}`;
|
||||
const keys = await getAllKeysByPrefix(pattern);
|
||||
const pattern = `${cachePrefix}${key}:*`;
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
const pipeline = redis.pipeline();
|
||||
pipeline.del(keys);
|
||||
await pipeline.exec();
|
||||
await redis.del(keys);
|
||||
}
|
||||
} else {
|
||||
await redis.set(versionKey, val);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import { type preUploadImgProps } from '@fastgpt/global/common/file/api';
|
|||
import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import { MongoImage } from './schema';
|
||||
import { type ClientSession, Types } from '../../../common/mongo';
|
||||
import { guessBase64ImageType } from './utils';
|
||||
import { guessBase64ImageType } from '../utils';
|
||||
import { readFromSecondary } from '../../mongo/utils';
|
||||
import { addHours } from 'date-fns';
|
||||
import { imageFileType } from '@fastgpt/global/common/file/constants';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
import { S3Sources } from '../../s3/type';
|
||||
import { getS3AvatarSource } from '../../s3/sources/avatar';
|
||||
import { isS3ObjectKey } from '../../s3/utils';
|
||||
import path from 'path';
|
||||
|
|
|
|||
|
|
@ -1,98 +1,8 @@
|
|||
import axios from 'axios';
|
||||
import { addLog } from '../../system/log';
|
||||
import { serverRequestBaseUrl } from '../../api/serverRequest';
|
||||
import { getFileContentTypeFromHeader, guessBase64ImageType } from '../utils';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
import { getContentTypeFromHeader } from '../utils';
|
||||
|
||||
// 图片格式魔数映射表
|
||||
const IMAGE_SIGNATURES: { type: string; magic: number[]; check?: (buffer: Buffer) => boolean }[] = [
|
||||
{ type: 'image/jpeg', magic: [0xff, 0xd8, 0xff] },
|
||||
{ type: 'image/png', magic: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
|
||||
{ type: 'image/gif', magic: [0x47, 0x49, 0x46, 0x38] },
|
||||
{
|
||||
type: 'image/webp',
|
||||
magic: [0x52, 0x49, 0x46, 0x46],
|
||||
check: (buffer) => buffer.length >= 12 && buffer.slice(8, 12).toString('ascii') === 'WEBP'
|
||||
},
|
||||
{ type: 'image/bmp', magic: [0x42, 0x4d] },
|
||||
{ type: 'image/tiff', magic: [0x49, 0x49, 0x2a, 0x00] },
|
||||
{ type: 'image/tiff', magic: [0x4d, 0x4d, 0x00, 0x2a] },
|
||||
{ type: 'image/svg+xml', magic: [0x3c, 0x73, 0x76, 0x67] },
|
||||
{ type: 'image/x-icon', magic: [0x00, 0x00, 0x01, 0x00] }
|
||||
];
|
||||
|
||||
// 有效的图片 MIME 类型
|
||||
const VALID_IMAGE_TYPES = new Set([
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/bmp',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'image/x-icon',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/ico',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/avif'
|
||||
]);
|
||||
|
||||
// Base64 首字符到图片类型的映射
|
||||
const BASE64_PREFIX_MAP: Record<string, string> = {
|
||||
'/': 'image/jpeg',
|
||||
i: 'image/png',
|
||||
R: 'image/gif',
|
||||
U: 'image/webp',
|
||||
Q: 'image/bmp',
|
||||
P: 'image/svg+xml',
|
||||
T: 'image/tiff',
|
||||
J: 'image/jp2',
|
||||
S: 'image/x-tga',
|
||||
I: 'image/ief',
|
||||
V: 'image/vnd.microsoft.icon',
|
||||
W: 'image/vnd.wap.wbmp',
|
||||
X: 'image/x-xbitmap',
|
||||
Z: 'image/x-xpixmap',
|
||||
Y: 'image/x-xwindowdump'
|
||||
};
|
||||
|
||||
const DEFAULT_IMAGE_TYPE = 'image/jpeg';
|
||||
|
||||
export const isValidImageContentType = (contentType: string): boolean => {
|
||||
if (!contentType) return false;
|
||||
return VALID_IMAGE_TYPES.has(contentType);
|
||||
};
|
||||
|
||||
export const detectImageTypeFromBuffer = (buffer: Buffer): string | undefined => {
|
||||
if (!buffer || buffer.length === 0) return;
|
||||
|
||||
for (const { type, magic, check } of IMAGE_SIGNATURES) {
|
||||
if (buffer.length < magic.length) continue;
|
||||
|
||||
const matches = magic.every((byte, index) => buffer[index] === byte);
|
||||
if (matches && (!check || check(buffer))) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const guessBase64ImageType = (str: string): string => {
|
||||
if (!str || typeof str !== 'string') return DEFAULT_IMAGE_TYPE;
|
||||
|
||||
// 尝试从 base64 解码并检测文件头
|
||||
try {
|
||||
const buffer = Buffer.from(str, 'base64');
|
||||
const detectedType = detectImageTypeFromBuffer(buffer);
|
||||
if (detectedType) return detectedType;
|
||||
} catch {}
|
||||
|
||||
// 回退到首字符映射
|
||||
return BASE64_PREFIX_MAP[str.charAt(0)] || DEFAULT_IMAGE_TYPE;
|
||||
};
|
||||
|
||||
export const getImageBase64 = async (url: string) => {
|
||||
addLog.debug(`Load image to base64: ${url}`);
|
||||
|
|
@ -106,26 +16,10 @@ export const getImageBase64 = async (url: string) => {
|
|||
})
|
||||
);
|
||||
|
||||
const buffer = Buffer.from(response.data);
|
||||
const base64 = buffer.toString('base64');
|
||||
const headerContentType = getContentTypeFromHeader(response.headers['content-type']);
|
||||
|
||||
// 检测图片类型的优先级策略
|
||||
const imageType = (() => {
|
||||
// 1. 如果 Header 是有效的图片类型,直接使用
|
||||
if (headerContentType && isValidImageContentType(headerContentType)) {
|
||||
return headerContentType;
|
||||
}
|
||||
|
||||
// 2. 使用文件头检测(适用于通用二进制类型或无效类型)
|
||||
const detectedType = detectImageTypeFromBuffer(buffer);
|
||||
if (detectedType) {
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
// 3. 回退到 base64 推断
|
||||
return guessBase64ImageType(base64);
|
||||
})();
|
||||
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||
const imageType =
|
||||
getFileContentTypeFromHeader(response.headers['content-type']) ||
|
||||
guessBase64ImageType(base64);
|
||||
|
||||
return {
|
||||
completeBase64: `data:${imageType};base64,${base64}`,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,37 @@ export const removeFilesByPaths = (paths: string[]) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getContentTypeFromHeader = (header: string): string | undefined => {
|
||||
return header?.toLowerCase()?.split(';')?.[0]?.trim();
|
||||
export const guessBase64ImageType = (str: string) => {
|
||||
const imageTypeMap: Record<string, string> = {
|
||||
'/': 'image/jpeg',
|
||||
i: 'image/png',
|
||||
R: 'image/gif',
|
||||
U: 'image/webp',
|
||||
Q: 'image/bmp',
|
||||
P: 'image/svg+xml',
|
||||
T: 'image/tiff',
|
||||
J: 'image/jp2',
|
||||
S: 'image/x-tga',
|
||||
I: 'image/ief',
|
||||
V: 'image/vnd.microsoft.icon',
|
||||
W: 'image/vnd.wap.wbmp',
|
||||
X: 'image/x-xbitmap',
|
||||
Z: 'image/x-xpixmap',
|
||||
Y: 'image/x-xwindowdump'
|
||||
};
|
||||
|
||||
const defaultType = 'image/jpeg';
|
||||
if (typeof str !== 'string' || str.length === 0) {
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
const firstChar = str.charAt(0);
|
||||
return imageTypeMap[firstChar] || defaultType;
|
||||
};
|
||||
|
||||
export const getFileContentTypeFromHeader = (header: string): string | undefined => {
|
||||
const contentType = header.split(';')[0];
|
||||
return contentType;
|
||||
};
|
||||
|
||||
export const clearDirFiles = (dirPath: string) => {
|
||||
|
|
|
|||
|
|
@ -54,8 +54,10 @@ export const NextEntry = ({
|
|||
if (error instanceof ZodError) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
message: 'Data validation error',
|
||||
error,
|
||||
error: {
|
||||
message: 'Validation error',
|
||||
details: error.message
|
||||
},
|
||||
url: req.url
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,6 @@ export const pushTrack = {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Admin cron job tracks
|
||||
subscriptionDeleted: (data: {
|
||||
teamId: string;
|
||||
subscriptionType: string;
|
||||
|
|
@ -187,23 +185,5 @@ export const pushTrack = {
|
|||
expiredTime: data.expiredTime
|
||||
}
|
||||
});
|
||||
},
|
||||
auditLogCleanup: (data: { teamId: string; retentionDays: number }) => {
|
||||
return createTrack({
|
||||
event: TrackEnum.auditLogCleanup,
|
||||
data: {
|
||||
teamId: data.teamId,
|
||||
retentionDays: data.retentionDays
|
||||
}
|
||||
});
|
||||
},
|
||||
chatHistoryCleanup: (data: { teamId: string; retentionDays: number }) => {
|
||||
return createTrack({
|
||||
event: TrackEnum.chatHistoryCleanup,
|
||||
data: {
|
||||
teamId: data.teamId,
|
||||
retentionDays: data.retentionDays
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,13 +31,26 @@ export async function connectMongo(props: {
|
|||
db.set('strictQuery', 'throw');
|
||||
|
||||
db.connection.on('error', async (error) => {
|
||||
console.error('mongo error', error);
|
||||
});
|
||||
db.connection.on('connected', async () => {
|
||||
console.log('mongo connected');
|
||||
console.log('mongo error', error);
|
||||
try {
|
||||
if (db.connection.readyState !== 0) {
|
||||
RemoveListeners();
|
||||
await db.disconnect();
|
||||
await delay(1000);
|
||||
await connectMongo(props);
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
db.connection.on('disconnected', async () => {
|
||||
console.error('mongo disconnected');
|
||||
console.log('mongo disconnected');
|
||||
try {
|
||||
if (db.connection.readyState !== 0) {
|
||||
RemoveListeners();
|
||||
await db.disconnect();
|
||||
await delay(1000);
|
||||
await connectMongo(props);
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
|
||||
await db.connect(url, {
|
||||
|
|
@ -51,9 +64,9 @@ export async function connectMongo(props: {
|
|||
maxIdleTimeMS: 300000, // 空闲连接超时: 5分钟,防止空闲连接长时间占用资源
|
||||
retryWrites: true, // 重试写入: 重试写入失败的操作
|
||||
retryReads: true, // 重试读取: 重试读取失败的操作
|
||||
serverSelectionTimeoutMS: 10000, // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞
|
||||
heartbeatFrequencyMS: 5000 // 5s 进行一次健康检查
|
||||
serverSelectionTimeoutMS: 10000 // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞
|
||||
});
|
||||
console.log('mongo connected');
|
||||
|
||||
connectedCb?.();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,51 +3,27 @@ import Redis from 'ioredis';
|
|||
|
||||
const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
|
||||
|
||||
// Base Redis options for connection reliability
|
||||
const REDIS_BASE_OPTION = {
|
||||
// Retry strategy: exponential backoff with unlimited retries for stability
|
||||
retryStrategy: (times: number) => {
|
||||
// Never give up retrying to ensure worker keeps running
|
||||
const delay = Math.min(times * 50, 2000); // Max 2s between retries
|
||||
if (times > 10) {
|
||||
addLog.error(`[Redis connection failed] attempt ${times}, will keep retrying...`);
|
||||
} else {
|
||||
addLog.warn(`Redis reconnecting... attempt ${times}, delay ${delay}ms`);
|
||||
}
|
||||
return delay; // Always return a delay to keep retrying
|
||||
},
|
||||
// Reconnect on specific errors (Redis master-slave switch, network issues)
|
||||
reconnectOnError: (err: any) => {
|
||||
const reconnectErrors = ['READONLY', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'];
|
||||
const message = typeof err?.message === 'string' ? err.message : String(err ?? '');
|
||||
|
||||
const shouldReconnect = reconnectErrors.some((errType) => message.includes(errType));
|
||||
if (shouldReconnect) {
|
||||
addLog.warn(`Redis reconnecting due to error: ${message}`);
|
||||
}
|
||||
return shouldReconnect;
|
||||
},
|
||||
// Connection timeout
|
||||
connectTimeout: 10000, // 10 seconds
|
||||
// Enable offline queue to buffer commands when disconnected
|
||||
enableOfflineQueue: true
|
||||
};
|
||||
|
||||
export const newQueueRedisConnection = () => {
|
||||
const redis = new Redis(REDIS_URL, {
|
||||
...REDIS_BASE_OPTION,
|
||||
// Limit retries for queue operations
|
||||
maxRetriesPerRequest: 3
|
||||
const redis = new Redis(REDIS_URL);
|
||||
redis.on('connect', () => {
|
||||
console.log('Redis connected');
|
||||
});
|
||||
redis.on('error', (error) => {
|
||||
console.error('Redis connection error', error);
|
||||
});
|
||||
return redis;
|
||||
};
|
||||
|
||||
export const newWorkerRedisConnection = () => {
|
||||
const redis = new Redis(REDIS_URL, {
|
||||
...REDIS_BASE_OPTION,
|
||||
// BullMQ requires maxRetriesPerRequest: null for blocking operations
|
||||
maxRetriesPerRequest: null
|
||||
});
|
||||
redis.on('connect', () => {
|
||||
console.log('Redis connected');
|
||||
});
|
||||
redis.on('error', (error) => {
|
||||
console.error('Redis connection error', error);
|
||||
});
|
||||
return redis;
|
||||
};
|
||||
|
||||
|
|
@ -55,44 +31,22 @@ export const FASTGPT_REDIS_PREFIX = 'fastgpt:';
|
|||
export const getGlobalRedisConnection = () => {
|
||||
if (global.redisClient) return global.redisClient;
|
||||
|
||||
global.redisClient = new Redis(REDIS_URL, {
|
||||
...REDIS_BASE_OPTION,
|
||||
keyPrefix: FASTGPT_REDIS_PREFIX,
|
||||
maxRetriesPerRequest: 3
|
||||
});
|
||||
global.redisClient = new Redis(REDIS_URL, { keyPrefix: FASTGPT_REDIS_PREFIX });
|
||||
|
||||
global.redisClient.on('connect', () => {
|
||||
addLog.info('[Global Redis] connected');
|
||||
addLog.info('Redis connected');
|
||||
});
|
||||
global.redisClient.on('error', (error) => {
|
||||
addLog.error('[Global Redis] connection error', error);
|
||||
});
|
||||
global.redisClient.on('close', () => {
|
||||
addLog.warn('[Global Redis] connection closed');
|
||||
addLog.error('Redis connection error', error);
|
||||
});
|
||||
|
||||
return global.redisClient;
|
||||
};
|
||||
|
||||
export const getAllKeysByPrefix = async (key: string) => {
|
||||
if (!key) return [];
|
||||
|
||||
const redis = getGlobalRedisConnection();
|
||||
const prefix = FASTGPT_REDIS_PREFIX;
|
||||
const pattern = `${prefix}${key}:*`;
|
||||
|
||||
let cursor = '0';
|
||||
const batchSize = 1000; // SCAN 每次取多少
|
||||
const results: string[] = [];
|
||||
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize);
|
||||
cursor = nextCursor;
|
||||
|
||||
for (const k of keys) {
|
||||
results.push(k.replace(FASTGPT_REDIS_PREFIX, ''));
|
||||
}
|
||||
} while (cursor !== '0');
|
||||
|
||||
return results;
|
||||
const keys = (await redis.keys(`${FASTGPT_REDIS_PREFIX}${key}:*`)).map((key) =>
|
||||
key.replace(FASTGPT_REDIS_PREFIX, '')
|
||||
);
|
||||
return keys;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { addLog } from '../system/log';
|
|||
import { replaceSensitiveText } from '@fastgpt/global/common/string/tools';
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
import { clearCookie } from '../../support/permission/auth/common';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
export interface ResponseType<T = any> {
|
||||
code: number;
|
||||
|
|
@ -17,9 +16,8 @@ export interface ProcessedError {
|
|||
code: number;
|
||||
statusText: string;
|
||||
message: string;
|
||||
shouldClearCookie: boolean;
|
||||
data?: any;
|
||||
zodError?: any;
|
||||
shouldClearCookie: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,7 +31,6 @@ export function processError(params: {
|
|||
defaultCode?: number;
|
||||
}): ProcessedError {
|
||||
const { error, url, defaultCode = 500 } = params;
|
||||
let zodError;
|
||||
|
||||
const errResponseKey = typeof error === 'string' ? error : error?.message;
|
||||
|
||||
|
|
@ -68,16 +65,6 @@ export function processError(params: {
|
|||
// 3. 根据错误类型记录不同级别的日志
|
||||
if (error instanceof UserError) {
|
||||
addLog.info(`Request error: ${url}, ${msg}`);
|
||||
} else if (error instanceof ZodError) {
|
||||
zodError = (() => {
|
||||
try {
|
||||
return JSON.parse(error.message);
|
||||
} catch (error) {}
|
||||
})();
|
||||
addLog.error(`[Zod] Error in ${url}`, {
|
||||
data: zodError
|
||||
});
|
||||
msg = error.message;
|
||||
} else {
|
||||
addLog.error(`System unexpected error: ${url}, ${msg}`, error);
|
||||
}
|
||||
|
|
@ -87,8 +74,7 @@ export function processError(params: {
|
|||
code: defaultCode,
|
||||
statusText: 'error',
|
||||
message: replaceSensitiveText(msg),
|
||||
shouldClearCookie: false,
|
||||
zodError
|
||||
shouldClearCookie: false
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -117,8 +103,7 @@ export const jsonRes = <T = any>(
|
|||
code: processedError.code,
|
||||
statusText: processedError.statusText,
|
||||
message: message || processedError.message,
|
||||
data: processedError.data !== undefined ? processedError.data : null,
|
||||
zodError: processedError.zodError
|
||||
data: processedError.data !== undefined ? processedError.data : null
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
Client,
|
||||
type RemoveOptions,
|
||||
type CopyConditions,
|
||||
S3Error,
|
||||
InvalidObjectNameError,
|
||||
InvalidXMLError
|
||||
} from 'minio';
|
||||
import { Client, type RemoveOptions, type CopyConditions, S3Error } from 'minio';
|
||||
import {
|
||||
type CreatePostPresignedUrlOptions,
|
||||
type CreatePostPresignedUrlParams,
|
||||
|
|
@ -24,26 +17,6 @@ import { type Readable } from 'node:stream';
|
|||
import { type UploadFileByBufferParams, UploadFileByBufferSchema } from '../type';
|
||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
// Check if the error is a "file not found" type error, which should be treated as success
|
||||
export const isFileNotFoundError = (error: any): boolean => {
|
||||
if (error instanceof S3Error) {
|
||||
// Handle various "not found" error codes
|
||||
return (
|
||||
error.code === 'NoSuchKey' ||
|
||||
error.code === 'InvalidObjectName' ||
|
||||
error.message === 'Not Found' ||
|
||||
error.message ===
|
||||
'The request signature we calculated does not match the signature you provided. Check your key and signing method.' ||
|
||||
error.message.includes('Resource name contains bad components') ||
|
||||
error.message.includes('Object name contains unsupported characters.')
|
||||
);
|
||||
}
|
||||
if (error instanceof InvalidObjectNameError || error instanceof InvalidXMLError) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export class S3BaseBucket {
|
||||
private _client: Client;
|
||||
private _externalClient: Client | undefined;
|
||||
|
|
@ -121,7 +94,7 @@ export class S3BaseBucket {
|
|||
temporary: false
|
||||
}
|
||||
});
|
||||
await this.removeObject(from);
|
||||
await this.delete(from);
|
||||
}
|
||||
|
||||
async copy({
|
||||
|
|
@ -147,17 +120,24 @@ export class S3BaseBucket {
|
|||
return this.client.copyObject(bucket, to, `${bucket}/${from}`, options?.copyConditions);
|
||||
}
|
||||
|
||||
async removeObject(objectKey: string, options?: RemoveOptions): Promise<void> {
|
||||
return this.client.removeObject(this.bucketName, objectKey, options).catch((err) => {
|
||||
if (isFileNotFoundError(err)) {
|
||||
return Promise.resolve();
|
||||
async delete(objectKey: string, options?: RemoveOptions): Promise<void> {
|
||||
try {
|
||||
if (!objectKey) return Promise.resolve();
|
||||
|
||||
// 把连带的 parsed 数据一起删除
|
||||
const fileParsedPrefix = `${path.dirname(objectKey)}/${path.basename(objectKey, path.extname(objectKey))}-parsed`;
|
||||
await this.addDeleteJob({ prefix: fileParsedPrefix });
|
||||
|
||||
return await this.client.removeObject(this.bucketName, objectKey, options);
|
||||
} catch (error) {
|
||||
if (error instanceof S3Error) {
|
||||
if (error.code === 'InvalidObjectName') {
|
||||
addLog.warn(`${this.bucketName} delete object not found: ${objectKey}`, error);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
addLog.error(`[S3 delete error]`, {
|
||||
message: err.message,
|
||||
data: { code: err.code, key: objectKey }
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 列出文件
|
||||
|
|
|
|||
|
|
@ -27,7 +27,26 @@ export async function clearExpiredMinioFiles() {
|
|||
const bucket = global.s3BucketMap[bucketName];
|
||||
|
||||
if (bucket) {
|
||||
await bucket.addDeleteJob({ key: file.minioKey });
|
||||
await bucket.delete(file.minioKey);
|
||||
|
||||
if (!file.minioKey.includes('-parsed/')) {
|
||||
try {
|
||||
const dir = path.dirname(file.minioKey);
|
||||
const basename = path.basename(file.minioKey);
|
||||
const ext = path.extname(basename);
|
||||
|
||||
if (ext) {
|
||||
const nameWithoutExt = path.basename(basename, ext);
|
||||
const parsedPrefix = `${dir}/${nameWithoutExt}-parsed`;
|
||||
|
||||
await bucket.addDeleteJob({ prefix: parsedPrefix });
|
||||
addLog.info(`Scheduled deletion of parsed images: ${parsedPrefix}`);
|
||||
}
|
||||
} catch (error) {
|
||||
addLog.debug(`Failed to schedule parsed images deletion for ${file.minioKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
await MongoS3TTL.deleteOne({ _id: file._id });
|
||||
|
||||
success++;
|
||||
|
|
@ -38,6 +57,12 @@ export async function clearExpiredMinioFiles() {
|
|||
addLog.warn(`Bucket not found: ${file.bucketName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof S3Error &&
|
||||
error.message.includes('Object name contains unsupported characters.')
|
||||
) {
|
||||
await MongoS3TTL.deleteOne({ _id: file._id });
|
||||
}
|
||||
fail++;
|
||||
addLog.error(`Failed to delete minio file: ${file.minioKey}`, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { getQueue, getWorker, QueueNames } from '../bullmq';
|
||||
import { addLog } from '../system/log';
|
||||
import path from 'path';
|
||||
import { batchRun } from '@fastgpt/global/common/system/utils';
|
||||
import { isFileNotFoundError, type S3BaseBucket } from './buckets/base';
|
||||
import pLimit from 'p-limit';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
export type S3MQJobData = {
|
||||
key?: string;
|
||||
|
|
@ -11,131 +9,81 @@ export type S3MQJobData = {
|
|||
bucketName: string;
|
||||
};
|
||||
|
||||
const jobOption = {
|
||||
attempts: 10,
|
||||
removeOnFail: {
|
||||
count: 10000, // 保留10000个失败任务
|
||||
age: 14 * 24 * 60 * 60 // 14 days
|
||||
},
|
||||
removeOnComplete: true,
|
||||
backoff: {
|
||||
delay: 2000,
|
||||
type: 'exponential'
|
||||
}
|
||||
};
|
||||
export const addS3DelJob = async (data: S3MQJobData): Promise<void> => {
|
||||
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete);
|
||||
const jobId = (() => {
|
||||
if (data.key) {
|
||||
return data.key;
|
||||
|
||||
await queue.add(
|
||||
'delete-s3-files',
|
||||
{ ...data },
|
||||
{
|
||||
attempts: 3,
|
||||
removeOnFail: false,
|
||||
removeOnComplete: true,
|
||||
backoff: {
|
||||
delay: 2000,
|
||||
type: 'exponential'
|
||||
}
|
||||
}
|
||||
if (data.keys) {
|
||||
return undefined;
|
||||
}
|
||||
if (data.prefix) {
|
||||
return data.prefix;
|
||||
}
|
||||
throw new Error('Invalid s3 delete job data');
|
||||
})();
|
||||
await queue.add('delete-s3-files', data, { jobId, ...jobOption });
|
||||
};
|
||||
|
||||
export const prefixDel = async (bucket: S3BaseBucket, prefix: string) => {
|
||||
addLog.debug(`[S3 delete] delete prefix: ${prefix}`);
|
||||
let tasks: Promise<any>[] = [];
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
const stream = bucket.listObjectsV2(prefix, true);
|
||||
|
||||
let settled = false;
|
||||
const finish = (error?: any) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
stream?.removeAllListeners?.();
|
||||
stream?.destroy?.();
|
||||
|
||||
if (error) {
|
||||
addLog.error(`[S3 delete] delete prefix failed`, error);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// stream 可能会中断,没有触发 end 和 error,导致 promise 不返回,需要增加定时器兜底。
|
||||
timer = setTimeout(() => {
|
||||
addLog.error(`[S3 delete] delete prefix timeout: ${prefix}`);
|
||||
finish('Timeout');
|
||||
}, 60000);
|
||||
|
||||
stream.on('data', (file) => {
|
||||
if (!file.name) return;
|
||||
tasks.push(bucket.removeObject(file.name));
|
||||
});
|
||||
stream.on('end', async () => {
|
||||
if (tasks.length === 0) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
const results = await Promise.allSettled(tasks);
|
||||
const failed = results.some((r) => r.status === 'rejected');
|
||||
if (failed) {
|
||||
return finish('Some deletes failed');
|
||||
}
|
||||
finish();
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
if (isFileNotFoundError(err)) {
|
||||
return finish();
|
||||
}
|
||||
addLog.error(`[S3 delete] delete prefix: ${prefix} error`, err);
|
||||
return finish(err);
|
||||
});
|
||||
stream.on('pause', () => {
|
||||
addLog.warn(`[S3 delete] delete prefix: ${prefix} paused`);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
);
|
||||
};
|
||||
export const startS3DelWorker = async () => {
|
||||
return getWorker<S3MQJobData>(
|
||||
QueueNames.s3FileDelete,
|
||||
async (job) => {
|
||||
let { prefix, bucketName, key, keys } = job.data;
|
||||
const bucket = global.s3BucketMap[bucketName];
|
||||
const { prefix, bucketName, key, keys } = job.data;
|
||||
const limit = pLimit(10);
|
||||
const bucket = s3BucketMap[bucketName];
|
||||
if (!bucket) {
|
||||
addLog.error(`Bucket not found: ${bucketName}`);
|
||||
return;
|
||||
return Promise.reject(`Bucket not found: ${bucketName}`);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
keys = [key];
|
||||
await bucket.delete(key);
|
||||
}
|
||||
if (keys) {
|
||||
addLog.debug(`[S3 delete] delete keys: ${keys.length}`);
|
||||
await batchRun(keys, async (key) => {
|
||||
await bucket.removeObject(key);
|
||||
// Delete parsed
|
||||
if (!key.includes('-parsed/')) {
|
||||
const fileParsedPrefix = `${path.dirname(key)}/${path.basename(key, path.extname(key))}-parsed`;
|
||||
await prefixDel(bucket, fileParsedPrefix);
|
||||
}
|
||||
});
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const key of keys) {
|
||||
const p = limit(() => retryFn(() => bucket.delete(key)));
|
||||
tasks.push(p);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
if (prefix) {
|
||||
await prefixDel(bucket, prefix);
|
||||
const tasks: Promise<void>[] = [];
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const stream = bucket.listObjectsV2(prefix, true);
|
||||
stream.on('data', async (file) => {
|
||||
if (!file.name) return;
|
||||
|
||||
const p = limit(() =>
|
||||
// 因为封装的 delete 方法里,包含前缀删除,这里不能再使用,避免循环。
|
||||
retryFn(() => bucket.client.removeObject(bucket.bucketName, file.name))
|
||||
);
|
||||
tasks.push(p);
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
const results = await Promise.allSettled(tasks);
|
||||
const failed = results.filter((r) => r.status === 'rejected');
|
||||
if (failed.length > 0) {
|
||||
reject('Some deletes failed');
|
||||
}
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
console.error('listObjects stream error', err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 6
|
||||
concurrency: 1
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class S3AvatarSource extends S3PublicBucket {
|
|||
async deleteAvatar(avatar: string, session?: ClientSession): Promise<void> {
|
||||
const key = avatar.slice(this.prefix.length);
|
||||
await MongoS3TTL.deleteOne({ minioKey: key, bucketName: this.bucketName }, session);
|
||||
await this.removeObject(key);
|
||||
await this.delete(key);
|
||||
}
|
||||
|
||||
async refreshAvatar(newAvatar?: string, oldAvatar?: string, session?: ClientSession) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { LogLevelEnum } from './log/constant';
|
|||
import { connectionMongo } from '../mongo/index';
|
||||
import { getMongoLog } from './log/schema';
|
||||
import { getLogger } from '../otel/log';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
export enum EventTypeEnum {
|
||||
outLinkBot = '[Outlink bot]',
|
||||
|
|
@ -152,7 +151,7 @@ export const addLog = {
|
|||
error(msg: string, error?: any) {
|
||||
this.log(LogLevelEnum.error, msg, {
|
||||
...(error?.data && { data: error?.data }),
|
||||
message: getErrText(error),
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
...(error?.config && {
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -176,8 +176,7 @@ class PgClass {
|
|||
const time = Date.now() - start;
|
||||
|
||||
if (time > 300) {
|
||||
const safeSql = sql.replace(/'\[[^\]]*?\]'/g, "'[x]'");
|
||||
addLog.warn(`pg query time: ${time}ms, sql: ${safeSql}`);
|
||||
addLog.warn(`pg query time: ${time}ms, sql: ${sql}`);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -120,40 +120,38 @@ export const loadSystemModels = async (init = false, language = 'en') => {
|
|||
]);
|
||||
|
||||
// Load system model from local
|
||||
systemModels.forEach((model) => {
|
||||
const mergeObject = (obj1: any, obj2: any) => {
|
||||
if (!obj1 && !obj2) return undefined;
|
||||
const formatObj1 = typeof obj1 === 'object' ? obj1 : {};
|
||||
const formatObj2 = typeof obj2 === 'object' ? obj2 : {};
|
||||
return { ...formatObj1, ...formatObj2 };
|
||||
};
|
||||
await Promise.all(
|
||||
systemModels.map(async (model) => {
|
||||
const mergeObject = (obj1: any, obj2: any) => {
|
||||
if (!obj1 && !obj2) return undefined;
|
||||
const formatObj1 = typeof obj1 === 'object' ? obj1 : {};
|
||||
const formatObj2 = typeof obj2 === 'object' ? obj2 : {};
|
||||
return { ...formatObj1, ...formatObj2 };
|
||||
};
|
||||
|
||||
const dbModel = dbModels.find((item) => item.model === model.model);
|
||||
const provider = getModelProvider(dbModel?.metadata?.provider || model.provider, language);
|
||||
const dbModel = dbModels.find((item) => item.model === model.model);
|
||||
const provider = getModelProvider(dbModel?.metadata?.provider || model.provider, language);
|
||||
|
||||
const modelData: any = {
|
||||
...model,
|
||||
...dbModel?.metadata,
|
||||
provider: provider.id,
|
||||
avatar: provider.avatar,
|
||||
type: dbModel?.metadata?.type || model.type,
|
||||
isCustom: false,
|
||||
const modelData: any = {
|
||||
...model,
|
||||
...dbModel?.metadata,
|
||||
provider: provider.id,
|
||||
avatar: provider.avatar,
|
||||
type: dbModel?.metadata?.type || model.type,
|
||||
isCustom: false,
|
||||
|
||||
...(model.type === ModelTypeEnum.llm && {
|
||||
maxResponse: model.maxTokens || 4000
|
||||
}),
|
||||
|
||||
...(model.type === ModelTypeEnum.llm && dbModel?.metadata?.type === ModelTypeEnum.llm
|
||||
? {
|
||||
maxResponse: dbModel?.metadata?.maxResponse ?? model.maxTokens ?? 4000,
|
||||
defaultConfig: mergeObject(model.defaultConfig, dbModel?.metadata?.defaultConfig),
|
||||
fieldMap: mergeObject(model.fieldMap, dbModel?.metadata?.fieldMap),
|
||||
maxTokens: undefined
|
||||
}
|
||||
: {})
|
||||
};
|
||||
pushModel(modelData);
|
||||
});
|
||||
...(model.type === ModelTypeEnum.llm && dbModel?.metadata?.type === ModelTypeEnum.llm
|
||||
? {
|
||||
maxResponse: dbModel?.metadata?.maxResponse ?? model.maxTokens ?? 1000,
|
||||
defaultConfig: mergeObject(model.defaultConfig, dbModel?.metadata?.defaultConfig),
|
||||
fieldMap: mergeObject(model.fieldMap, dbModel?.metadata?.fieldMap),
|
||||
maxTokens: undefined
|
||||
}
|
||||
: {})
|
||||
};
|
||||
pushModel(modelData);
|
||||
})
|
||||
);
|
||||
|
||||
// Custom model(Not in system config)
|
||||
dbModels.forEach((dbModel) => {
|
||||
|
|
@ -238,7 +236,8 @@ export const loadSystemModels = async (init = false, language = 'en') => {
|
|||
);
|
||||
} catch (error) {
|
||||
console.error('Load models error', error);
|
||||
|
||||
// @ts-ignore
|
||||
global.systemModelList = undefined;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { getAIApi } from '../config';
|
|||
import { countPromptTokens } from '../../../common/string/tiktoken/index';
|
||||
import { EmbeddingTypeEnm } from '@fastgpt/global/core/ai/constants';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
type GetVectorProps = {
|
||||
model: EmbeddingModelItemType;
|
||||
|
|
@ -40,70 +38,55 @@ export async function getVectorsByText({ model, input, type, headers }: GetVecto
|
|||
|
||||
for (const chunk of chunks) {
|
||||
// input text to vector
|
||||
const result = await retryFn(() =>
|
||||
ai.embeddings
|
||||
.create(
|
||||
{
|
||||
...model.defaultConfig,
|
||||
...(type === EmbeddingTypeEnm.db && model.dbConfig),
|
||||
...(type === EmbeddingTypeEnm.query && model.queryConfig),
|
||||
model: model.model,
|
||||
input: chunk
|
||||
},
|
||||
model.requestUrl
|
||||
? {
|
||||
path: model.requestUrl,
|
||||
headers: {
|
||||
...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}),
|
||||
...headers
|
||||
}
|
||||
const result = await ai.embeddings
|
||||
.create(
|
||||
{
|
||||
...model.defaultConfig,
|
||||
...(type === EmbeddingTypeEnm.db && model.dbConfig),
|
||||
...(type === EmbeddingTypeEnm.query && model.queryConfig),
|
||||
model: model.model,
|
||||
input: chunk
|
||||
},
|
||||
model.requestUrl
|
||||
? {
|
||||
path: model.requestUrl,
|
||||
headers: {
|
||||
...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}),
|
||||
...headers
|
||||
}
|
||||
: { headers }
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data) {
|
||||
addLog.error('[Embedding Error] not responding', {
|
||||
message: '',
|
||||
data: {
|
||||
response: res,
|
||||
model: model.model,
|
||||
inputLength: chunk.length
|
||||
}
|
||||
});
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
if (!res?.data?.[0]?.embedding) {
|
||||
// @ts-ignore
|
||||
const msg = res.data?.err?.message || '';
|
||||
addLog.error('[Embedding Error]', {
|
||||
message: msg,
|
||||
data: {
|
||||
response: res,
|
||||
model: model.model,
|
||||
inputLength: chunk.length
|
||||
}
|
||||
});
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
}
|
||||
: { headers }
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data) {
|
||||
addLog.error('[Embedding] API is not responding', res);
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
if (!res?.data?.[0]?.embedding) {
|
||||
// @ts-ignore
|
||||
const msg = res.data?.err?.message || 'Embedding API Error';
|
||||
addLog.error('[Embedding] API Error', {
|
||||
message: msg,
|
||||
data: res
|
||||
});
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
|
||||
const [tokens, vectors] = await Promise.all([
|
||||
(async () => {
|
||||
if (res.usage) return res.usage.total_tokens;
|
||||
const [tokens, vectors] = await Promise.all([
|
||||
(async () => {
|
||||
if (res.usage) return res.usage.total_tokens;
|
||||
|
||||
const tokens = await Promise.all(chunk.map((item) => countPromptTokens(item)));
|
||||
return tokens.reduce((sum, item) => sum + item, 0);
|
||||
})(),
|
||||
Promise.all(
|
||||
res.data.map((item) => formatVectors(item.embedding, model.normalization))
|
||||
)
|
||||
]);
|
||||
const tokens = await Promise.all(chunk.map((item) => countPromptTokens(item)));
|
||||
return tokens.reduce((sum, item) => sum + item, 0);
|
||||
})(),
|
||||
Promise.all(res.data.map((item) => formatVectors(item.embedding, model.normalization)))
|
||||
]);
|
||||
|
||||
return {
|
||||
tokens,
|
||||
vectors
|
||||
};
|
||||
})
|
||||
);
|
||||
return {
|
||||
tokens,
|
||||
vectors
|
||||
};
|
||||
});
|
||||
|
||||
totalTokens += result.tokens;
|
||||
allVectors.push(...result.vectors);
|
||||
|
|
@ -114,13 +97,7 @@ export async function getVectorsByText({ model, input, type, headers }: GetVecto
|
|||
vectors: allVectors
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error(`[Embedding Error]`, {
|
||||
message: getErrText(error),
|
||||
data: {
|
||||
model: model.model,
|
||||
inputLengths: formatInput.map((item) => item.length)
|
||||
}
|
||||
});
|
||||
addLog.error(`[Embedding] request error`, error);
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,17 +628,12 @@ const createChatCompletion = async ({
|
|||
('iterator' in response || 'controller' in response);
|
||||
|
||||
const getEmptyResponseTip = () => {
|
||||
addLog.warn(`LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`;
|
||||
} else {
|
||||
addLog.error(`LLM response empty`, {
|
||||
message: '',
|
||||
data: body
|
||||
});
|
||||
}
|
||||
return i18nT('chat:LLM_model_response_empty');
|
||||
};
|
||||
|
|
@ -657,18 +652,13 @@ const createChatCompletion = async ({
|
|||
getEmptyResponseTip
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error(`LLM response error`, error);
|
||||
addLog.warn(`LLM response error`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User ai api error`, {
|
||||
message: getErrText(error),
|
||||
baseUrl: userKey?.baseUrl,
|
||||
data: body
|
||||
});
|
||||
return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`);
|
||||
} else {
|
||||
addLog.error(`LLM response error`, {
|
||||
message: getErrText(error),
|
||||
data: body
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function reRankRecall({
|
|||
headers?: Record<string, string>;
|
||||
}): Promise<ReRankCallResult> {
|
||||
if (!model) {
|
||||
return Promise.reject('No rerank model');
|
||||
return Promise.reject('[Rerank] No rerank model');
|
||||
}
|
||||
if (documents.length === 0) {
|
||||
return Promise.resolve({
|
||||
|
|
@ -67,7 +67,7 @@ export function reRankRecall({
|
|||
addLog.info('ReRank finish:', { time: Date.now() - start });
|
||||
|
||||
if (!data?.results || data?.results?.length === 0) {
|
||||
addLog.error('[Rerank Error]', { message: 'Empty result', data });
|
||||
addLog.error('[Rerank] Empty result', { data });
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -81,7 +81,7 @@ export function reRankRecall({
|
|||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
addLog.error('[Rerank Error]', err);
|
||||
addLog.error('[Rerank] request error', err);
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import {
|
|||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
|
||||
import { MongoApp } from './schema';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { encryptSecretValue, storeSecretValue } from '../../common/secret/utils';
|
||||
import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
|
||||
import { type ClientSession } from '../../common/mongo';
|
||||
import { MongoEvaluation } from './evaluation/evalSchema';
|
||||
import { removeEvaluationJob } from './evaluation/mq';
|
||||
import { MongoChatItem } from '../chat/chatItemSchema';
|
||||
|
|
@ -21,15 +23,10 @@ import { MongoChatSetting } from '../chat/setting/schema';
|
|||
import { MongoResourcePermission } from '../../support/permission/schema';
|
||||
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { removeImageByPath } from '../../common/file/image/controller';
|
||||
import { mongoSessionRun } from '../../common/mongo/sessionRun';
|
||||
import { MongoAppLogKeys } from './logs/logkeysSchema';
|
||||
import { MongoChatItemResponse } from '../chat/chatItemResponseSchema';
|
||||
import { getS3ChatSource } from '../../common/s3/sources/chat';
|
||||
import { MongoAppChatLog } from './logs/chatLogsSchema';
|
||||
import { MongoAppRegistration } from '../../support/appRegistration/schema';
|
||||
import { MongoMcpKey } from '../../support/mcp/schema';
|
||||
import { MongoAppRecord } from './record/schema';
|
||||
import { mongoSessionRun } from '../../common/mongo/sessionRun';
|
||||
import { addLog } from '../../common/system/log';
|
||||
|
||||
export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => {
|
||||
if (!nodes) return;
|
||||
|
|
@ -139,96 +136,113 @@ export const getAppBasicInfoByIds = async ({ teamId, ids }: { teamId: string; id
|
|||
}));
|
||||
};
|
||||
|
||||
export const deleteAppDataProcessor = async ({
|
||||
app,
|
||||
teamId
|
||||
}: {
|
||||
app: AppSchema;
|
||||
teamId: string;
|
||||
}) => {
|
||||
const appId = String(app._id);
|
||||
|
||||
// 1. 删除应用头像
|
||||
await removeImageByPath(app.avatar);
|
||||
// 2. 删除聊天记录和S3文件
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId });
|
||||
await MongoAppChatLog.deleteMany({ teamId, appId });
|
||||
await MongoChatItemResponse.deleteMany({ appId });
|
||||
await MongoChatItem.deleteMany({ appId });
|
||||
await MongoChat.deleteMany({ appId });
|
||||
|
||||
// 3. 删除应用相关数据(使用事务)
|
||||
{
|
||||
// 删除分享链接
|
||||
await MongoOutLink.deleteMany({ appId });
|
||||
// 删除 OpenAPI 配置
|
||||
await MongoOpenApi.deleteMany({ appId });
|
||||
// 删除应用版本
|
||||
await MongoAppVersion.deleteMany({ appId });
|
||||
// 删除聊天输入引导
|
||||
await MongoChatInputGuide.deleteMany({ appId });
|
||||
// 删除精选应用记录
|
||||
await MongoChatFavouriteApp.deleteMany({ teamId, appId });
|
||||
// 从快捷应用中移除对应应用
|
||||
await MongoChatSetting.updateMany({ teamId }, { $pull: { quickAppIds: { $in: [appId] } } });
|
||||
// 删除权限记录
|
||||
await MongoResourcePermission.deleteMany({
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
teamId,
|
||||
resourceId: appId
|
||||
});
|
||||
// 删除日志密钥
|
||||
await MongoAppLogKeys.deleteMany({ appId });
|
||||
|
||||
// 删除应用注册记录
|
||||
await MongoAppRegistration.deleteMany({ appId });
|
||||
// 删除应用从MCP key apps数组中移除
|
||||
await MongoMcpKey.updateMany({ teamId, 'apps.appId': appId }, { $pull: { apps: { appId } } });
|
||||
|
||||
// 删除应用本身
|
||||
await MongoApp.deleteOne({ _id: appId });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAppsImmediate = async ({
|
||||
export const onDelOneApp = async ({
|
||||
teamId,
|
||||
appIds
|
||||
appId,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appIds: string[];
|
||||
appId: string;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
const apps = await findAppAndAllChildren({
|
||||
teamId,
|
||||
appId,
|
||||
fields: '_id avatar'
|
||||
});
|
||||
|
||||
const deletedAppIds = apps
|
||||
.filter((app) => !AppFolderTypeList.includes(app.type))
|
||||
.map((app) => String(app._id));
|
||||
|
||||
// Remove eval job
|
||||
const evalJobs = await MongoEvaluation.find(
|
||||
{
|
||||
teamId,
|
||||
appId: { $in: appIds }
|
||||
appId: { $in: apps.map((app) => app._id) }
|
||||
},
|
||||
'_id'
|
||||
).lean();
|
||||
await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id)));
|
||||
|
||||
// Remove app record
|
||||
await MongoAppRecord.deleteMany({ teamId, appId: { $in: appIds } });
|
||||
};
|
||||
const del = async (app: AppSchema, session: ClientSession) => {
|
||||
const appId = String(app._id);
|
||||
|
||||
export const updateParentFoldersUpdateTime = ({ parentId }: { parentId?: string | null }) => {
|
||||
mongoSessionRun(async (session) => {
|
||||
const existsId = new Set<string>();
|
||||
while (true) {
|
||||
if (!parentId || existsId.has(parentId)) return;
|
||||
// 删除分享链接
|
||||
await MongoOutLink.deleteMany({
|
||||
appId
|
||||
}).session(session);
|
||||
// Openapi
|
||||
await MongoOpenApi.deleteMany({
|
||||
appId
|
||||
}).session(session);
|
||||
|
||||
existsId.add(parentId);
|
||||
// delete version
|
||||
await MongoAppVersion.deleteMany({
|
||||
appId
|
||||
}).session(session);
|
||||
|
||||
const parentApp = await MongoApp.findById(parentId, 'parentId updateTime');
|
||||
if (!parentApp) return;
|
||||
await MongoChatInputGuide.deleteMany({
|
||||
appId
|
||||
}).session(session);
|
||||
|
||||
parentApp.updateTime = new Date();
|
||||
await parentApp.save({ session });
|
||||
// 删除精选应用记录
|
||||
await MongoChatFavouriteApp.deleteMany({
|
||||
teamId,
|
||||
appId
|
||||
}).session(session);
|
||||
|
||||
// 递归更新上层
|
||||
parentId = parentApp.parentId;
|
||||
// 从快捷应用中移除对应应用
|
||||
await MongoChatSetting.updateMany(
|
||||
{ teamId },
|
||||
{ $pull: { quickAppIds: { id: String(appId) } } }
|
||||
).session(session);
|
||||
|
||||
// Del permission
|
||||
await MongoResourcePermission.deleteMany({
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
teamId,
|
||||
resourceId: appId
|
||||
}).session(session);
|
||||
|
||||
await MongoAppLogKeys.deleteMany({
|
||||
appId
|
||||
}).session(session);
|
||||
|
||||
// delete app
|
||||
await MongoApp.deleteOne(
|
||||
{
|
||||
_id: appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
// Delete avatar
|
||||
await removeImageByPath(app.avatar, session);
|
||||
};
|
||||
|
||||
// Delete chats
|
||||
for await (const app of apps) {
|
||||
const appId = String(app._id);
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId });
|
||||
await MongoChatItemResponse.deleteMany({
|
||||
appId
|
||||
});
|
||||
await MongoChatItem.deleteMany({
|
||||
appId
|
||||
});
|
||||
await MongoChat.deleteMany({
|
||||
appId
|
||||
});
|
||||
await getS3ChatSource().deleteChatFilesByPrefix({ appId });
|
||||
}
|
||||
|
||||
for await (const app of apps) {
|
||||
if (session) {
|
||||
await del(app, session);
|
||||
}
|
||||
}).catch((err) => {
|
||||
addLog.error('updateParentFoldersUpdateTime error', err);
|
||||
});
|
||||
|
||||
await mongoSessionRun((session) => del(app, session));
|
||||
}
|
||||
|
||||
return deletedAppIds;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import { getQueue, getWorker, QueueNames } from '../../../common/bullmq';
|
||||
import { appDeleteProcessor } from './processor';
|
||||
|
||||
export type AppDeleteJobData = {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
};
|
||||
|
||||
// 创建工作进程
|
||||
export const initAppDeleteWorker = () => {
|
||||
return getWorker<AppDeleteJobData>(QueueNames.appDelete, appDeleteProcessor, {
|
||||
concurrency: 1, // 确保同时只有1个删除任务
|
||||
removeOnFail: {
|
||||
age: 30 * 24 * 60 * 60 // 保留30天失败记录
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加删除任务
|
||||
export const addAppDeleteJob = (data: AppDeleteJobData) => {
|
||||
// 创建删除队列
|
||||
const appDeleteQueue = getQueue<AppDeleteJobData>(QueueNames.appDelete, {
|
||||
defaultJobOptions: {
|
||||
attempts: 10,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 5000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: { age: 30 * 24 * 60 * 60 } // 保留30天失败记录
|
||||
}
|
||||
});
|
||||
|
||||
const jobId = `${String(data.teamId)}:${String(data.appId)}`;
|
||||
|
||||
// Use jobId to automatically prevent duplicate deletion tasks (BullMQ feature)
|
||||
return appDeleteQueue.add('delete_app', data, {
|
||||
jobId,
|
||||
delay: 1000 // Delay 1 second to ensure API response completes
|
||||
});
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import type { Processor } from 'bullmq';
|
||||
import type { AppDeleteJobData } from './index';
|
||||
import { findAppAndAllChildren, deleteAppDataProcessor } from '../controller';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import { batchRun } from '@fastgpt/global/common/system/utils';
|
||||
import type { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { MongoApp } from '../schema';
|
||||
|
||||
const deleteApps = async ({ teamId, apps }: { teamId: string; apps: AppSchema[] }) => {
|
||||
const results = await batchRun(
|
||||
apps,
|
||||
async (app) => {
|
||||
await deleteAppDataProcessor({ app, teamId });
|
||||
},
|
||||
3
|
||||
);
|
||||
|
||||
return results.flat();
|
||||
};
|
||||
|
||||
export const appDeleteProcessor: Processor<AppDeleteJobData> = async (job) => {
|
||||
const { teamId, appId } = job.data;
|
||||
const startTime = Date.now();
|
||||
|
||||
addLog.info(`[App Delete] Start deleting app: ${appId} for team: ${teamId}`);
|
||||
|
||||
try {
|
||||
// 1. 查找应用及其所有子应用
|
||||
const apps = await findAppAndAllChildren({
|
||||
teamId,
|
||||
appId
|
||||
});
|
||||
|
||||
if (!apps || apps.length === 0) {
|
||||
addLog.warn(`[App Delete] App not found: ${appId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 安全检查:确保所有要删除的应用都已标记为 deleteTime
|
||||
const markedForDelete = await MongoApp.find(
|
||||
{
|
||||
_id: { $in: apps.map((app) => app._id) },
|
||||
teamId,
|
||||
deleteTime: { $ne: null }
|
||||
},
|
||||
{ _id: 1 }
|
||||
).lean();
|
||||
|
||||
if (markedForDelete.length !== apps.length) {
|
||||
addLog.warn(
|
||||
`[App Delete] Safety check: ${markedForDelete.length}/${apps.length} apps marked for deletion`,
|
||||
{
|
||||
markedAppIds: markedForDelete.map((app) => app._id),
|
||||
totalAppIds: apps.map((app) => app._id)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const childrenLen = apps.length - 1;
|
||||
const appIds = apps.map((app) => app._id);
|
||||
|
||||
// 3. 执行真正的删除操作(只删除已经标记为 deleteTime 的数据)
|
||||
await deleteApps({
|
||||
teamId,
|
||||
apps
|
||||
});
|
||||
|
||||
addLog.info(`[App Delete] Successfully deleted app: ${appId} and ${childrenLen} children`, {
|
||||
duration: Date.now() - startTime,
|
||||
totalApps: appIds.length,
|
||||
appIds
|
||||
});
|
||||
} catch (error: any) {
|
||||
addLog.error(`[App Delete] Failed to delete app: ${appId}`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import {
|
||||
TeamCollectionName,
|
||||
TeamMemberCollectionName
|
||||
} from '@fastgpt/global/support/user/team/constant';
|
||||
import { getMongoModel, Schema } from '../../../common/mongo';
|
||||
import { AppCollectionName } from '../schema';
|
||||
import type { AppRecordType } from './type';
|
||||
|
||||
export const AppRecordCollectionName = 'app_records';
|
||||
|
||||
const AppRecordSchema = new Schema(
|
||||
{
|
||||
tmbId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamMemberCollectionName,
|
||||
required: true
|
||||
},
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamCollectionName,
|
||||
required: true
|
||||
},
|
||||
appId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: AppCollectionName,
|
||||
required: true
|
||||
},
|
||||
lastUsedTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: false
|
||||
}
|
||||
);
|
||||
|
||||
AppRecordSchema.index({ tmbId: 1, lastUsedTime: -1 }); // 查询用户最近使用的应用
|
||||
AppRecordSchema.index({ tmbId: 1, appId: 1 }, { unique: true }); // 防止重复记录
|
||||
AppRecordSchema.index({ teamId: 1, appId: 1 }); // 用于清理权限失效的记录
|
||||
|
||||
export const MongoAppRecord = getMongoModel<AppRecordType>(
|
||||
AppRecordCollectionName,
|
||||
AppRecordSchema
|
||||
);
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const AppRecordSchemaZod = z.object({
|
||||
_id: z.string(),
|
||||
tmbId: z.string(),
|
||||
teamId: z.string(),
|
||||
appId: z.string(),
|
||||
lastUsedTime: z.date()
|
||||
});
|
||||
|
||||
// TypeScript types inferred from Zod schemas
|
||||
export type AppRecordType = z.infer<typeof AppRecordSchemaZod>;
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { MongoAppRecord } from './schema';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
|
||||
export const recordAppUsage = async ({
|
||||
appId,
|
||||
tmbId,
|
||||
teamId
|
||||
}: {
|
||||
appId: string;
|
||||
tmbId: string;
|
||||
teamId: string;
|
||||
}) => {
|
||||
await MongoAppRecord.updateOne(
|
||||
{ tmbId, appId },
|
||||
{
|
||||
$set: {
|
||||
teamId,
|
||||
lastUsedTime: new Date()
|
||||
}
|
||||
},
|
||||
{
|
||||
upsert: true
|
||||
}
|
||||
).catch((error) => {
|
||||
addLog.error('recordAppUsage error', error);
|
||||
});
|
||||
};
|
||||
|
|
@ -119,12 +119,6 @@ const AppSchema = new Schema(
|
|||
inited: Boolean,
|
||||
teamTags: {
|
||||
type: [String]
|
||||
},
|
||||
|
||||
// 软删除标记字段
|
||||
deleteTime: {
|
||||
type: Date,
|
||||
default: null // null表示未删除,有值表示删除时间
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -144,6 +138,5 @@ AppSchema.index(
|
|||
);
|
||||
// Admin count
|
||||
AppSchema.index({ type: 1 });
|
||||
AppSchema.index({ deleteTime: 1 });
|
||||
|
||||
export const MongoApp = getMongoModel<AppType>(AppCollectionName, AppSchema);
|
||||
|
|
|
|||
|
|
@ -95,13 +95,7 @@ const ChatSchema = new Schema({
|
|||
hasGoodFeedback: Boolean,
|
||||
hasBadFeedback: Boolean,
|
||||
hasUnreadGoodFeedback: Boolean,
|
||||
hasUnreadBadFeedback: Boolean,
|
||||
|
||||
deleteTime: {
|
||||
type: Date,
|
||||
default: null,
|
||||
select: false
|
||||
}
|
||||
hasUnreadBadFeedback: Boolean
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -109,16 +103,13 @@ try {
|
|||
|
||||
ChatSchema.index({ chatId: 1 });
|
||||
// get user history
|
||||
ChatSchema.index({ tmbId: 1, appId: 1, deleteTime: 1, top: -1, updateTime: -1 });
|
||||
// get share chat history
|
||||
ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 });
|
||||
|
||||
ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 });
|
||||
// delete by appid; clear history; init chat; update chat; auth chat; get chat;
|
||||
ChatSchema.index({ appId: 1, chatId: 1 });
|
||||
|
||||
/* get chat logs */
|
||||
// 1. No feedback filter
|
||||
ChatSchema.index({ teamId: 1, appId: 1, source: 1, tmbId: 1, deleteTime: 1, updateTime: -1 });
|
||||
ChatSchema.index({ teamId: 1, appId: 1, source: 1, tmbId: 1, updateTime: -1 });
|
||||
|
||||
/* 反馈过滤的索引 */
|
||||
// 2. Has good feedback filter
|
||||
|
|
@ -129,13 +120,11 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasGoodFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasGoodFeedback: true,
|
||||
deleteTime: null
|
||||
hasGoodFeedback: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -147,13 +136,11 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasBadFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasBadFeedback: true,
|
||||
deleteTime: null
|
||||
hasBadFeedback: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -165,13 +152,11 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasUnreadGoodFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasUnreadGoodFeedback: true,
|
||||
deleteTime: null
|
||||
hasUnreadGoodFeedback: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -183,19 +168,19 @@ try {
|
|||
source: 1,
|
||||
tmbId: 1,
|
||||
hasUnreadBadFeedback: 1,
|
||||
deleteTime: 1,
|
||||
updateTime: -1
|
||||
},
|
||||
{
|
||||
partialFilterExpression: {
|
||||
hasUnreadBadFeedback: true,
|
||||
deleteTime: null
|
||||
hasUnreadBadFeedback: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// get share chat history
|
||||
ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 });
|
||||
|
||||
// timer, clear history
|
||||
ChatSchema.index({ updateTime: -1, teamId: 1 });
|
||||
ChatSchema.index({ teamId: 1, updateTime: -1 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export type Props = {
|
|||
nodes: StoreNodeItemType[];
|
||||
appChatConfig?: AppChatConfigType;
|
||||
variables?: Record<string, any>;
|
||||
isUpdateUseTime: boolean;
|
||||
newTitle: string;
|
||||
source: `${ChatSourceEnum}`;
|
||||
sourceName?: string;
|
||||
|
|
@ -218,6 +219,7 @@ export async function saveChat(props: Props) {
|
|||
nodes,
|
||||
appChatConfig,
|
||||
variables,
|
||||
isUpdateUseTime,
|
||||
newTitle,
|
||||
source,
|
||||
sourceName,
|
||||
|
|
@ -312,9 +314,6 @@ export async function saveChat(props: Props) {
|
|||
outLinkUid,
|
||||
metadata: metadataUpdate,
|
||||
updateTime: new Date()
|
||||
},
|
||||
$setOnInsert: {
|
||||
createTime: new Date()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -391,6 +390,18 @@ export async function saveChat(props: Props) {
|
|||
} catch (error) {
|
||||
addLog.error('Push chat log error', error);
|
||||
}
|
||||
|
||||
if (isUpdateUseTime) {
|
||||
await MongoApp.updateOne(
|
||||
{ _id: appId },
|
||||
{
|
||||
updateTime: new Date()
|
||||
},
|
||||
{
|
||||
...writePrimary
|
||||
}
|
||||
).catch();
|
||||
}
|
||||
} catch (error) {
|
||||
addLog.error(`update chat history error`, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export async function delDatasetRelevantData({
|
|||
datasets,
|
||||
session
|
||||
}: {
|
||||
datasets: { _id: string; teamId: string }[];
|
||||
datasets: DatasetSchemaType[];
|
||||
session: ClientSession;
|
||||
}) {
|
||||
if (!datasets.length) return;
|
||||
|
|
@ -115,6 +115,24 @@ export async function delDatasetRelevantData({
|
|||
// Delete vector data
|
||||
await deleteDatasetDataVector({ teamId, datasetIds });
|
||||
|
||||
for (const datasetId of datasetIds) {
|
||||
// Delete dataset_data_texts in batches by datasetId
|
||||
await MongoDatasetDataText.deleteMany({
|
||||
teamId,
|
||||
datasetId
|
||||
}).maxTimeMS(300000); // Reduce timeout for single batch
|
||||
// Delete dataset_datas in batches by datasetId
|
||||
await MongoDatasetData.deleteMany({
|
||||
teamId,
|
||||
datasetId
|
||||
}).maxTimeMS(300000);
|
||||
}
|
||||
|
||||
// Delete source: 兼容旧版的图片
|
||||
await delCollectionRelatedSource({ collections });
|
||||
// Delete vector data
|
||||
await deleteDatasetDataVector({ teamId, datasetIds });
|
||||
|
||||
// delete collections
|
||||
await MongoDatasetCollection.deleteMany({
|
||||
teamId,
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ export const addDatasetDeleteJob = (data: DatasetDeleteJobData) => {
|
|||
}
|
||||
});
|
||||
|
||||
const jobId = `${String(data.teamId)}:${String(data.datasetId)}`;
|
||||
const jobId = `${data.teamId}:${data.datasetId}`;
|
||||
|
||||
// 使用去重机制,避免重复删除
|
||||
return datasetDeleteQueue.add('delete_dataset', data, {
|
||||
jobId,
|
||||
return datasetDeleteQueue.add(jobId, data, {
|
||||
deduplication: { id: jobId },
|
||||
delay: 1000 // 延迟1秒执行,确保API响应完成
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Processor } from 'bullmq';
|
||||
import { addDatasetDeleteJob, type DatasetDeleteJobData } from './index';
|
||||
import type { DatasetDeleteJobData } from './index';
|
||||
import { delDatasetRelevantData, findDatasetAndAllChildren } from '../controller';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import type { DatasetSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { MongoDatasetCollectionTags } from '../tag/schema';
|
||||
import { removeDatasetSyncJobScheduler } from '../datasetSync';
|
||||
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
|
||||
|
|
@ -11,54 +12,36 @@ import { MongoDatasetTraining } from '../training/schema';
|
|||
|
||||
export const deleteDatasetsImmediate = async ({
|
||||
teamId,
|
||||
datasetIds
|
||||
datasets
|
||||
}: {
|
||||
teamId: string;
|
||||
datasetIds: string[];
|
||||
datasets: DatasetSchemaType[];
|
||||
}) => {
|
||||
const datasetIds = datasets.map((d) => d._id);
|
||||
|
||||
// delete training data
|
||||
await MongoDatasetTraining.deleteMany({
|
||||
MongoDatasetTraining.deleteMany({
|
||||
teamId,
|
||||
datasetId: { $in: datasetIds }
|
||||
});
|
||||
|
||||
// Remove cron job
|
||||
await Promise.all(
|
||||
datasetIds.map((id) => {
|
||||
return removeDatasetSyncJobScheduler(id);
|
||||
})
|
||||
);
|
||||
};
|
||||
// Clear a team datasets
|
||||
export const deleteTeamAllDatasets = async (teamId: string) => {
|
||||
const datasets = await MongoDataset.find(
|
||||
{
|
||||
teamId
|
||||
},
|
||||
{ _id: 1, parentId: 1 }
|
||||
);
|
||||
await deleteDatasetsImmediate({
|
||||
teamId,
|
||||
datasetIds: datasets.map((d) => d._id)
|
||||
});
|
||||
await Promise.all(
|
||||
datasets.map((dataset) => {
|
||||
if (dataset.parentId) return;
|
||||
return addDatasetDeleteJob({
|
||||
teamId,
|
||||
datasetId: dataset._id
|
||||
});
|
||||
// 只处理已标记删除的数据集
|
||||
if (datasetIds.includes(dataset._id)) {
|
||||
return removeDatasetSyncJobScheduler(dataset._id);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 批量删除函数
|
||||
const deleteDatasets = async ({
|
||||
export const deleteDatasets = async ({
|
||||
teamId,
|
||||
datasets
|
||||
}: {
|
||||
teamId: string;
|
||||
datasets: { _id: string; avatar: string; teamId: string }[];
|
||||
datasets: DatasetSchemaType[];
|
||||
}) => {
|
||||
const datasetIds = datasets.map((d) => d._id);
|
||||
|
||||
|
|
@ -98,8 +81,7 @@ export const datasetDeleteProcessor: Processor<DatasetDeleteJobData> = async (jo
|
|||
// 1. 查找知识库及其所有子知识库
|
||||
const datasets = await findDatasetAndAllChildren({
|
||||
teamId,
|
||||
datasetId,
|
||||
fields: '_id teamId avatar'
|
||||
datasetId
|
||||
});
|
||||
|
||||
if (!datasets || datasets.length === 0) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export type ChatResponse = DispatchNodeResultType<
|
|||
export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResponse> => {
|
||||
let {
|
||||
res,
|
||||
checkIsStopping,
|
||||
requestOrigin,
|
||||
stream = false,
|
||||
retainDatasetCite = true,
|
||||
|
|
@ -202,7 +201,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
|||
requestOrigin
|
||||
},
|
||||
userKey: externalProvider.openaiAccount,
|
||||
isAborted: checkIsStopping,
|
||||
isAborted: () => res?.closed,
|
||||
onReasoning({ text }) {
|
||||
if (!aiChatReasoning) return;
|
||||
workflowStreamResponse?.({
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
|
|||
const { messages, toolNodes, toolModel, childrenInteractiveParams, ...workflowProps } = props;
|
||||
const {
|
||||
res,
|
||||
checkIsStopping,
|
||||
requestOrigin,
|
||||
runtimeNodes,
|
||||
runtimeEdges,
|
||||
|
|
@ -130,7 +129,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
|
|||
retainDatasetCite,
|
||||
useVision: aiChatVision
|
||||
},
|
||||
isAborted: checkIsStopping,
|
||||
isAborted: () => res?.closed,
|
||||
userKey: externalProvider.openaiAccount,
|
||||
onReasoning({ text }) {
|
||||
if (!aiChatReasoning) return;
|
||||
|
|
|
|||
|
|
@ -59,11 +59,10 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
|
|||
import { i18nT } from '../../../../web/i18n/utils';
|
||||
import { clone } from 'lodash';
|
||||
import { validateFileUrlDomain } from '../../../common/security/fileUrlValidator';
|
||||
import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus';
|
||||
|
||||
type Props = Omit<
|
||||
ChatDispatchProps,
|
||||
'checkIsStopping' | 'workflowDispatchDeep' | 'timezone' | 'externalProvider' | 'cloneVariables'
|
||||
'workflowDispatchDeep' | 'timezone' | 'externalProvider' | 'cloneVariables'
|
||||
> & {
|
||||
runtimeNodes: RuntimeNodeItemType[];
|
||||
runtimeEdges: RuntimeEdgeItemType[];
|
||||
|
|
@ -88,17 +87,7 @@ export async function dispatchWorkFlow({
|
|||
concatUsage,
|
||||
...data
|
||||
}: Props & WorkflowUsageProps): Promise<DispatchFlowResponse> {
|
||||
const {
|
||||
res,
|
||||
stream,
|
||||
runningUserInfo,
|
||||
runningAppInfo,
|
||||
lastInteractive,
|
||||
histories,
|
||||
query,
|
||||
chatId,
|
||||
apiVersion
|
||||
} = data;
|
||||
const { res, stream, runningUserInfo, runningAppInfo, lastInteractive, histories, query } = data;
|
||||
|
||||
// Check url valid
|
||||
const invalidInput = query.some((item) => {
|
||||
|
|
@ -112,8 +101,6 @@ export async function dispatchWorkFlow({
|
|||
addLog.info('[Workflow run] Invalid file url');
|
||||
return Promise.reject(new UserError('Invalid file url'));
|
||||
}
|
||||
|
||||
/* Init function */
|
||||
// Check point
|
||||
await checkTeamAIPoints(runningUserInfo.teamId);
|
||||
|
||||
|
|
@ -133,22 +120,7 @@ export async function dispatchWorkFlow({
|
|||
});
|
||||
}
|
||||
return usageId;
|
||||
})(),
|
||||
// Add preview url to chat items
|
||||
await addPreviewUrlToChatItems(histories, 'chatFlow'),
|
||||
// Add preview url to query
|
||||
...query.map(async (item) => {
|
||||
if (item.type !== ChatItemValueTypeEnum.file || !item.file?.key) return;
|
||||
item.file.url = await getS3ChatSource().createGetChatFileURL({
|
||||
key: item.file.key,
|
||||
external: true
|
||||
});
|
||||
}),
|
||||
// Remove stopping sign
|
||||
delAgentRuntimeStopSign({
|
||||
appId: runningAppInfo.id,
|
||||
chatId
|
||||
})
|
||||
})()
|
||||
]);
|
||||
|
||||
let streamCheckTimer: NodeJS.Timeout | null = null;
|
||||
|
|
@ -180,6 +152,16 @@ export async function dispatchWorkFlow({
|
|||
}
|
||||
}
|
||||
|
||||
// Add preview url to chat items
|
||||
await addPreviewUrlToChatItems(histories, 'chatFlow');
|
||||
for (const item of query) {
|
||||
if (item.type !== ChatItemValueTypeEnum.file || !item.file?.key) continue;
|
||||
item.file.url = await getS3ChatSource().createGetChatFileURL({
|
||||
key: item.file.key,
|
||||
external: true
|
||||
});
|
||||
}
|
||||
|
||||
// Get default variables
|
||||
const cloneVariables = clone(data.variables);
|
||||
const defaultVariables = {
|
||||
|
|
@ -191,34 +173,12 @@ export async function dispatchWorkFlow({
|
|||
timezone
|
||||
}))
|
||||
};
|
||||
// MCP
|
||||
|
||||
let mcpClientMemory = {} as Record<string, MCPClient>;
|
||||
// Stop sign(没有 apiVersion,说明不会有暂停)
|
||||
let stopping = false;
|
||||
const checkIsStopping = (): boolean => {
|
||||
if (apiVersion === 'v2') {
|
||||
return stopping;
|
||||
}
|
||||
if (apiVersion === 'v1') {
|
||||
if (!res) return false;
|
||||
return res.closed || !!res.errored;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const checkStoppingTimer =
|
||||
apiVersion === 'v2'
|
||||
? setInterval(async () => {
|
||||
stopping = await shouldWorkflowStop({
|
||||
appId: runningAppInfo.id,
|
||||
chatId
|
||||
});
|
||||
}, 100)
|
||||
: undefined;
|
||||
|
||||
// Init some props
|
||||
return runWorkflow({
|
||||
...data,
|
||||
checkIsStopping,
|
||||
query,
|
||||
histories,
|
||||
timezone,
|
||||
|
|
@ -229,24 +189,15 @@ export async function dispatchWorkFlow({
|
|||
concatUsage,
|
||||
mcpClientMemory,
|
||||
cloneVariables
|
||||
}).finally(async () => {
|
||||
}).finally(() => {
|
||||
if (streamCheckTimer) {
|
||||
clearInterval(streamCheckTimer);
|
||||
}
|
||||
if (checkStoppingTimer) {
|
||||
clearInterval(checkStoppingTimer);
|
||||
}
|
||||
|
||||
// Close mcpClient connections
|
||||
Object.values(mcpClientMemory).forEach((client) => {
|
||||
client.closeConnection();
|
||||
});
|
||||
|
||||
// 工作流完成后删除 Redis 记录
|
||||
await delAgentRuntimeStopSign({
|
||||
appId: runningAppInfo.id,
|
||||
chatId
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -259,14 +210,14 @@ type RunWorkflowProps = ChatDispatchProps & {
|
|||
};
|
||||
export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowResponse> => {
|
||||
let {
|
||||
apiVersion,
|
||||
checkIsStopping,
|
||||
res,
|
||||
runtimeNodes = [],
|
||||
runtimeEdges = [],
|
||||
histories = [],
|
||||
variables = {},
|
||||
externalProvider,
|
||||
retainDatasetCite = true,
|
||||
version = 'v1',
|
||||
responseDetail = true,
|
||||
responseAllData = true,
|
||||
usageId,
|
||||
|
|
@ -377,6 +328,10 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
|||
});
|
||||
}
|
||||
|
||||
get connectionIsActive(): boolean {
|
||||
return !res?.closed && !res?.errored;
|
||||
}
|
||||
|
||||
// Add active node to queue (if already in the queue, it will not be added again)
|
||||
addActiveNode(nodeId: string) {
|
||||
if (this.activeRunQueue.has(nodeId)) {
|
||||
|
|
@ -630,7 +585,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
|||
})();
|
||||
|
||||
// Response node response
|
||||
if (apiVersion === 'v2' && !data.isToolCall && isRootRuntime && formatResponseData) {
|
||||
if (version === 'v2' && !data.isToolCall && isRootRuntime && formatResponseData) {
|
||||
data.workflowStreamResponse?.({
|
||||
event: SseResponseEventEnum.flowNodeResponse,
|
||||
data: responseAllData
|
||||
|
|
@ -858,8 +813,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (checkIsStopping()) {
|
||||
addLog.warn('Workflow stopped', {
|
||||
if (!this.connectionIsActive) {
|
||||
addLog.warn('Request is closed/errored', {
|
||||
appId: data.runningAppInfo.id,
|
||||
nodeId: node.nodeId,
|
||||
nodeName: node.name
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import { addLog } from '../../../common/system/log';
|
||||
import { getGlobalRedisConnection } from '../../../common/redis/index';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
const WORKFLOW_STATUS_PREFIX = 'agent_runtime_stopping';
|
||||
const TTL = 60; // 1分钟
|
||||
|
||||
export const StopStatus = 'STOPPING';
|
||||
|
||||
export type WorkflowStatusParams = {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
// 获取工作流状态键
|
||||
export const getRuntimeStatusKey = (params: WorkflowStatusParams): string => {
|
||||
return `${WORKFLOW_STATUS_PREFIX}:${params.appId}:${params.chatId}`;
|
||||
};
|
||||
|
||||
// 暂停任务
|
||||
export const setAgentRuntimeStop = async (params: WorkflowStatusParams): Promise<void> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
await redis.set(key, 1, 'EX', TTL);
|
||||
};
|
||||
|
||||
// 删除任务状态
|
||||
export const delAgentRuntimeStopSign = async (params: WorkflowStatusParams): Promise<void> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
await redis.del(key).catch((err) => {
|
||||
addLog.error(`[Agent Runtime Stop] Delete stop sign error`, err);
|
||||
});
|
||||
};
|
||||
|
||||
// 检查工作流是否应该停止
|
||||
export const shouldWorkflowStop = (params: WorkflowStatusParams): Promise<boolean> => {
|
||||
const redis = getGlobalRedisConnection();
|
||||
const key = getRuntimeStatusKey(params);
|
||||
return redis
|
||||
.get(key)
|
||||
.then((res) => !!res)
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 等待工作流完成(记录被删除)
|
||||
* @param params 工作流参数
|
||||
* @param timeout 超时时间(毫秒),默认5秒
|
||||
* @param pollInterval 轮询间隔(毫秒),默认50毫秒
|
||||
* @returns true=正常完成, false=超时
|
||||
*/
|
||||
export const waitForWorkflowComplete = async ({
|
||||
appId,
|
||||
chatId,
|
||||
timeout = 5000,
|
||||
pollInterval = 50
|
||||
}: {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
timeout?: number;
|
||||
pollInterval?: number;
|
||||
}) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const sign = await shouldWorkflowStop({ appId, chatId });
|
||||
|
||||
// 如果没有暂停中的标志,则认为已经完成任务了。
|
||||
if (!sign) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待下一次轮询
|
||||
await delay(pollInterval);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
|
@ -29,8 +29,6 @@ const PromotionRecordSchema = new Schema({
|
|||
}
|
||||
});
|
||||
|
||||
PromotionRecordSchema.index({ userId: 1 });
|
||||
|
||||
export const MongoPromotionRecord = getMongoModel<PromotionRecordType>(
|
||||
'promotionRecord',
|
||||
PromotionRecordSchema
|
||||
|
|
|
|||
|
|
@ -43,21 +43,19 @@ const OutLinkSchema = new Schema({
|
|||
type: Date
|
||||
},
|
||||
|
||||
showRunningStatus: {
|
||||
responseDetail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showCite: {
|
||||
showNodeStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: true
|
||||
},
|
||||
showFullText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
canDownloadSource: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
// showFullText: {
|
||||
// type: Boolean
|
||||
// },
|
||||
showRawSource: {
|
||||
type: Boolean
|
||||
},
|
||||
limit: {
|
||||
maxUsagePoints: {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { Schema, getMongoLogModel } from '../../../common/mongo';
|
||||
import { type TeamAuditSchemaType } from '@fastgpt/global/support/user/audit/type';
|
||||
import { type OperationLogSchema } from '@fastgpt/global/support/user/audit/type';
|
||||
import { AdminAuditEventEnum, AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
|
||||
import {
|
||||
TeamCollectionName,
|
||||
TeamMemberCollectionName
|
||||
} from '@fastgpt/global/support/user/team/constant';
|
||||
|
||||
export const TeamAuditCollectionName = 'operationLogs';
|
||||
export const OperationLogCollectionName = 'operationLogs';
|
||||
|
||||
const TeamAuditSchema = new Schema({
|
||||
const OperationLogSchema = new Schema({
|
||||
tmbId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamMemberCollectionName,
|
||||
|
|
@ -34,10 +34,9 @@ const TeamAuditSchema = new Schema({
|
|||
}
|
||||
});
|
||||
|
||||
TeamAuditSchema.index({ teamId: 1, tmbId: 1, event: 1 });
|
||||
TeamAuditSchema.index({ timestamp: 1, teamId: 1 });
|
||||
OperationLogSchema.index({ teamId: 1, tmbId: 1, event: 1 });
|
||||
|
||||
export const MongoTeamAudit = getMongoLogModel<TeamAuditSchemaType>(
|
||||
TeamAuditCollectionName,
|
||||
TeamAuditSchema
|
||||
export const MongoOperationLog = getMongoLogModel<OperationLogSchema>(
|
||||
OperationLogCollectionName,
|
||||
OperationLogSchema
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { i18nT } from '../../../../web/i18n/utils';
|
||||
import { MongoTeamAudit } from './schema';
|
||||
import { MongoOperationLog } from './schema';
|
||||
import type {
|
||||
AdminAuditEventEnum,
|
||||
AuditEventEnum,
|
||||
|
|
@ -86,7 +86,7 @@ export function addAuditLog<T extends AuditEventEnum | AdminAuditEventEnum>({
|
|||
params?: any;
|
||||
}) {
|
||||
retryFn(() =>
|
||||
MongoTeamAudit.create({
|
||||
MongoOperationLog.create({
|
||||
tmbId: tmbId,
|
||||
teamId: teamId,
|
||||
event,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export type SystemMsgModalValueType = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
SubModeEnum,
|
||||
SubTypeEnum
|
||||
} from '@fastgpt/global/support/wallet/sub/constants';
|
||||
import type { TeamSubSchemaType } from '@fastgpt/global/support/wallet/sub/type';
|
||||
import type { TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
|
||||
|
||||
export const subCollectionName = 'team_subscriptions';
|
||||
|
||||
|
|
@ -67,12 +67,18 @@ const SubSchema = new Schema({
|
|||
customDomain: Number,
|
||||
|
||||
// stand sub and extra points sub. Plan total points
|
||||
totalPoints: Number,
|
||||
// plan surplus points
|
||||
surplusPoints: Number,
|
||||
totalPoints: {
|
||||
type: Number
|
||||
},
|
||||
surplusPoints: {
|
||||
// plan surplus points
|
||||
type: Number
|
||||
},
|
||||
|
||||
// extra dataset size
|
||||
currentExtraDatasetSize: Number
|
||||
currentExtraDatasetSize: {
|
||||
type: Number
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -100,4 +106,4 @@ try {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
export const MongoTeamSub = getMongoModel<TeamSubSchemaType>(subCollectionName, SubSchema);
|
||||
export const MongoTeamSub = getMongoModel<TeamSubSchema>(subCollectionName, SubSchema);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {
|
|||
import { MongoTeamSub } from './schema';
|
||||
import {
|
||||
type TeamPlanStatusType,
|
||||
type TeamSubSchemaType
|
||||
} from '@fastgpt/global/support/wallet/sub/type';
|
||||
type TeamSubSchema
|
||||
} from '@fastgpt/global/support/wallet/sub/type.d';
|
||||
import dayjs from 'dayjs';
|
||||
import { type ClientSession } from '../../../common/mongo';
|
||||
import { addMonths } from 'date-fns';
|
||||
|
|
@ -29,7 +29,7 @@ export const getStandardPlanConfig = (level: `${StandardSubLevelEnum}`) => {
|
|||
return global.subPlans?.standard?.[level];
|
||||
};
|
||||
|
||||
export const sortStandPlans = (plans: TeamSubSchemaType[]) => {
|
||||
export const sortStandPlans = (plans: TeamSubSchema[]) => {
|
||||
return plans.sort(
|
||||
(a, b) =>
|
||||
standardSubLevelMap[b.currentSubLevel].weight - standardSubLevelMap[a.currentSubLevel].weight
|
||||
|
|
|
|||
|
|
@ -8,17 +8,7 @@ import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';
|
|||
|
||||
export const useUploadAvatar = (
|
||||
api: (params: { filename: string }) => Promise<CreatePostPresignedUrlResult>,
|
||||
{
|
||||
onSuccess,
|
||||
maxW = 300,
|
||||
maxH = 300,
|
||||
maxSize = 1024 * 500 // 500KB
|
||||
}: {
|
||||
onSuccess?: (avatar: string) => void;
|
||||
maxW?: number;
|
||||
maxH?: number;
|
||||
maxSize?: number;
|
||||
} = {}
|
||||
{ onSuccess }: { onSuccess?: (avatar: string) => void } = {}
|
||||
) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -42,9 +32,8 @@ export const useUploadAvatar = (
|
|||
const compressed = base64ToFile(
|
||||
await compressBase64Img({
|
||||
base64Img: await fileToBase64(file),
|
||||
maxW,
|
||||
maxH,
|
||||
maxSize
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
}),
|
||||
file.name
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,7 @@ import MyIcon from '../Icon';
|
|||
import { iconPaths } from '../Icon/constants';
|
||||
import MyImage from '../Image/MyImage';
|
||||
|
||||
const Avatar = ({
|
||||
w = '30px',
|
||||
src,
|
||||
...props
|
||||
}: Omit<ImageProps, 'src'> & { src?: string | null }) => {
|
||||
const Avatar = ({ w = '30px', src, ...props }: ImageProps) => {
|
||||
// @ts-ignore
|
||||
const isIcon = !!iconPaths[src as any];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<svg viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6261 3.29289C13.0166 2.90237 13.6498 2.90237 14.0403 3.29289C14.4308 3.68342 14.4308 4.31658 14.0403 4.70711L6.70694 12.0404C6.31642 12.431 5.68325 12.431 5.29273 12.0404L1.9594 8.70711C1.56887 8.31658 1.56887 7.68342 1.9594 7.29289C2.34992 6.90237 2.98309 6.90237 3.37361 7.29289L5.99984 9.91912L12.6261 3.29289Z"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M12.6262 3.29289C13.0167 2.90237 13.6499 2.90237 14.0404 3.29289C14.4309 3.68342 14.4309 4.31658 14.0404 4.70711L6.70707 12.0404C6.31654 12.431 5.68338 12.431 5.29285 12.0404L1.95952 8.70711C1.56899 8.31658 1.56899 7.68342 1.95952 7.29289C2.35004 6.90237 2.98321 6.90237 3.37373 7.29289L5.99996 9.91912L12.6262 3.29289Z"
|
||||
fill="#3370FF" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 475 B |
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '../Icon';
|
||||
import { useRequest2 } from '../../../hooks/useRequest';
|
||||
|
|
@ -11,9 +11,9 @@ import {
|
|||
HStack,
|
||||
Box,
|
||||
Button,
|
||||
PopoverArrow
|
||||
PopoverArrow,
|
||||
Portal
|
||||
} from '@chakra-ui/react';
|
||||
import { useMemoEnhance } from '../../../hooks/useMemoEnhance';
|
||||
|
||||
const PopoverConfirm = ({
|
||||
content,
|
||||
|
|
@ -32,13 +32,13 @@ const PopoverConfirm = ({
|
|||
Trigger: React.ReactNode;
|
||||
placement?: PlacementWithLogical;
|
||||
offset?: [number, number];
|
||||
onConfirm: () => Promise<any> | any;
|
||||
onConfirm: () => any;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const map = useMemoEnhance(() => {
|
||||
const map = useMemo(() => {
|
||||
const map = {
|
||||
info: {
|
||||
variant: 'primary',
|
||||
|
|
@ -56,7 +56,7 @@ const PopoverConfirm = ({
|
|||
const firstFieldRef = React.useRef(null);
|
||||
const { onOpen, onClose, isOpen } = useDisclosure();
|
||||
|
||||
const { runAsync: onclickConfirm, loading } = useRequest2(async () => onConfirm(), {
|
||||
const { runAsync: onclickConfirm, loading } = useRequest2(onConfirm, {
|
||||
onSuccess: onClose
|
||||
});
|
||||
|
||||
|
|
@ -90,14 +90,7 @@ const PopoverConfirm = ({
|
|||
</HStack>
|
||||
<HStack mt={2} justifyContent={'flex-end'}>
|
||||
{showCancel && (
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Button variant={'whiteBase'} size="sm" onClick={onClose}>
|
||||
{cancelText || t('common:Cancel')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ export function useLinkedScroll<
|
|||
pageSize = 10,
|
||||
params = {},
|
||||
currentData,
|
||||
defaultScroll = 'top',
|
||||
showErrorToast = true
|
||||
defaultScroll = 'top'
|
||||
}: {
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
currentData?: { id: string; anchor?: any };
|
||||
defaultScroll?: 'top' | 'bottom';
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -107,8 +105,7 @@ export function useLinkedScroll<
|
|||
onFinally() {
|
||||
isInit.current = true;
|
||||
},
|
||||
manual: false,
|
||||
errorToast: showErrorToast ? undefined : ''
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
|
|
@ -156,8 +153,7 @@ export function useLinkedScroll<
|
|||
return response;
|
||||
},
|
||||
{
|
||||
refreshDeps: [hasMorePrev, isLoading, params, pageSize],
|
||||
errorToast: showErrorToast ? undefined : ''
|
||||
refreshDeps: [hasMorePrev, isLoading, params, pageSize]
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -192,8 +188,7 @@ export function useLinkedScroll<
|
|||
return response;
|
||||
},
|
||||
{
|
||||
refreshDeps: [hasMoreNext, isLoading, params, pageSize],
|
||||
errorToast: showErrorToast ? undefined : ''
|
||||
refreshDeps: [hasMoreNext, isLoading, params, pageSize]
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export function useScrollPagination<
|
|||
} catch (error: any) {
|
||||
if (showErrorToast) {
|
||||
toast({
|
||||
title: t(getErrText(error, t('common:core.chat.error.data_error'))),
|
||||
title: getErrText(error, t('common:core.chat.error.data_error')),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue