Commit 85244870 by Archer Committed by GitHub

feat: zod schema (#6740)

* feat: zod schema

move file api

feat: chat and dataset zod

* fix: review

* feat: dataset openapi

* fix: test

* update cr
parent fc6953fc
...@@ -446,7 +446,7 @@ interface DebugDataType { ...@@ -446,7 +446,7 @@ interface DebugDataType {
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
entryNodeIds: string[]; entryNodeIds: string[];
variables: Record<string, any>; variables: Record<string, any>;
history?: ChatItemType[]; history?: ChatItemMiniType[];
query?: UserChatItemValueItemType[]; query?: UserChatItemValueItemType[];
workflowInteractiveResponse?: WorkflowInteractiveResponseType; workflowInteractiveResponse?: WorkflowInteractiveResponseType;
} }
......
...@@ -128,10 +128,10 @@ export type WechatPollJobData = { shareId: string }; ...@@ -128,10 +128,10 @@ export type WechatPollJobData = { shareId: string };
```typescript ```typescript
// packages/service/support/outLink/schema.ts // packages/service/support/outLink/schema.ts
OutLinkSchema.index({ shareId: -1 }); OutLinkSchemaType.index({ shareId: -1 });
OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 }); OutLinkSchemaType.index({ teamId: 1, tmbId: 1, appId: 1 });
// 条件索引: 仅索引 wechat online 渠道,用于服务重启恢复 // 条件索引: 仅索引 wechat online 渠道,用于服务重启恢复
OutLinkSchema.index( OutLinkSchemaType.index(
{ type: 1, 'app.status': 1 }, { type: 1, 'app.status': 1 },
{ partialFilterExpression: { type: 'wechat', 'app.status': 'online' } } { partialFilterExpression: { type: 'wechat', 'app.status': 'online' } }
); );
......
...@@ -401,7 +401,7 @@ export async function downloadAndStoreMedia(params: { ...@@ -401,7 +401,7 @@ export async function downloadAndStoreMedia(params: {
```typescript ```typescript
async function processUserGroup( async function processUserGroup(
outLink: OutLinkSchema<WechatAppType>, outLink: OutLinkSchemaType<WechatAppType>,
group: ParsedMessageGroup group: ParsedMessageGroup
): Promise<void> { ): Promise<void> {
const app = outLink.app; const app = outLink.app;
......
...@@ -16,4 +16,4 @@ description: FastGPT API 开发规范。重点强调使用 zod schema 定义入 ...@@ -16,4 +16,4 @@ description: FastGPT API 开发规范。重点强调使用 zod schema 定义入
## 说明文档 ## 说明文档
[API 设计规范](../design/common/api/index.md) [API 设计规范](../../design/api/index.md)
\ No newline at end of file \ No newline at end of file
# 后端错误处理检查标准
## 1. 异步操作未覆盖错误处理 🔴
所有 `async/await` 操作都可能抛出异常,未捕获的错误会导致 Promise 静默失败或未处理的拒绝。
```typescript
// ❌ 无 try-catch,错误向上抛出且无上下文
async function deleteUser(userId: string) {
await db.users.deleteOne({ id: userId });
}
// ✅ 捕获错误,记录上下文,重新抛出
async function deleteUser(userId: string): Promise<void> {
try {
const result = await db.users.deleteOne({ id: userId });
if (result.deletedCount === 0) {
throw new Error(`User not found: ${userId}`);
}
} catch (error) {
addLog.error(error, `Failed to delete user: ${userId}`);
throw error;
}
}
```
**例外情况**:API 路由的顶层 handler 通常由框架统一捕获,内部 service 可以直接 throw,不需要每层都 try-catch。重点检查的是**没有上层兜底的独立异步调用**
---
## 2. 错误信息丢失 🟡
catch 中创建新 Error 但不保留原始错误,导致问题难以排查。
```typescript
// ❌ 原始错误信息丢失
catch (error) {
throw new Error('Save failed'); // 为什么失败?不知道
}
// ❌ 空 catch,静默失败
catch (error) {
// 什么都不做
}
// ✅ 保留错误链
catch (error) {
addLog.error(error, 'Save user failed');
throw new Error(`Save user failed: ${String(error)}`, { cause: error });
}
// ✅ 确需忽略时,必须写明原因
catch (error) {
// 清理临时文件失败不影响主流程,记录日志后忽略
addLog.warn(error, 'Temp file cleanup failed');
}
```
---
## 3. Fire-and-Forget 未挂 catch 🔴
不 await 的 Promise 如果抛出错误,会成为未处理的 rejection,在 Node.js 中可能导致进程崩溃。
```typescript
// ❌ 错误无处捕获
sendNotification(userId);
updateLastLogin(userId);
// ✅ 如不需要等待,至少挂 .catch
sendNotification(userId).catch(err =>
addLog.warn(err, 'Notification send failed, non-critical')
);
// ✅ 或用 void 明确表示有意忽略(需团队约定)
void sendNotification(userId).catch(err =>
addLog.warn(err, 'Notification failed')
);
```
---
## 4. 业务错误与系统错误混淆 🟡
业务错误(如"用户不存在")应返回明确的业务状态码,而不是 500。
```typescript
// ❌ 业务错误被当成系统错误
async function getUser(userId: string) {
const user = await db.users.findById(userId);
if (!user) throw new Error('User not found'); // 被框架捕获后返回 500
}
// ✅ 使用业务错误类(FastGPT 中使用 ERROR_ENUM)
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
async function getUser(userId: string) {
const user = await db.users.findById(userId);
if (!user) {
throw new Error(ERROR_ENUM.unAuthUser); // 框架识别为 4xx 业务错误
}
return user;
}
```
# 后端性能检查标准
## 1. CPU 密集型同步操作阻塞事件循环 🔴
Node.js 是单线程的,长时间的同步计算会阻塞所有并发请求。即使是几百毫秒的阻塞,在高并发下也是严重问题。
**高危模式**
```typescript
// ❌ 同步解析/序列化超大 JSON(几十MB会卡住几百ms)
const data = JSON.parse(fs.readFileSync('/path/to/huge.json', 'utf8'));
// ❌ 大数组的同步复杂计算
const result = largeArray.reduce((acc, item) => {
return acc + heavyComputation(item); // 10万条数据×复杂计算
}, 0);
// ❌ 同步读取大文件
const content = fs.readFileSync('/path/to/large/file');
```
**修复方案**
```typescript
// ✅ 拆分到多个 tick,释放事件循环
async function processLargeArray(items: Item[]) {
const CHUNK_SIZE = 1000;
const results: Result[] = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
results.push(...chunk.map(item => process(item)));
await new Promise(resolve => setImmediate(resolve)); // 释放事件循环
}
return results;
}
// ✅ CPU 密集型任务使用 worker_threads
import { Worker } from 'worker_threads';
// ✅ 文件使用流式处理
import { createReadStream } from 'fs';
const stream = createReadStream('/path/to/large/file');
```
**判断标准**:单次操作耗时预计超过 50ms 的同步计算都应考虑异步化或分片处理。
---
## 2. N+1 查询问题 🟡
在循环中对每条记录单独查询数据库,导致 N 条记录触发 N+1 次数据库请求。
```typescript
// ❌ 循环内查询:100条用户 → 101次数据库请求
const users = await db.users.find({}).toArray();
for (const user of users) {
user.posts = await db.posts.find({ userId: user._id }).toArray();
}
// ✅ 一次查询 + 内存关联:2次数据库请求
const users = await db.users.find({}).toArray();
const userIds = users.map(u => u._id);
const allPosts = await db.posts
.find({ userId: { $in: userIds } })
.toArray();
const postsByUser = new Map<string, Post[]>();
allPosts.forEach(post => {
const key = String(post.userId);
postsByUser.set(key, [...(postsByUser.get(key) ?? []), post]);
});
users.forEach(user => {
user.posts = postsByUser.get(String(user._id)) ?? [];
});
```
**识别信号**:循环体内出现 `await db.xxx.find/findOne/findById` 即为高度可疑。
---
## 3. 全量加载未分页 🟡
不加 `limit` 地拉取集合数据,当数据量增长到几万甚至几十万条时,单次请求会消耗大量内存和时间。
```typescript
// ❌ 无限制全量拉取
const allLogs = await db.logs.find({ userId }).toArray();
const allItems = await db.collection.find({}).toArray();
// ✅ 强制分页
const PAGE_SIZE = 50;
const logs = await db.logs
.find({ userId })
.sort({ createdAt: -1 })
.skip((page - 1) * PAGE_SIZE)
.limit(PAGE_SIZE)
.toArray();
// ✅ 如确实需要全量(如后台任务),使用游标流式处理
const cursor = db.collection.find({}).batchSize(100);
for await (const doc of cursor) {
await processDoc(doc);
}
```
---
## 4. 查询未投影(返回多余字段)🟢
MongoDB 默认返回文档所有字段。当文档包含大字段(如 `content``vectorData``fileContent`)时,不投影会显著增加网络传输和内存开销。
```typescript
// ❌ 返回整个文档(包含大字段)
const datasets = await db.datasets.find({ teamId }).toArray();
// ✅ 只取需要的字段
const datasets = await db.datasets
.find({ teamId })
.project({ name: 1, type: 1, createdAt: 1 }) // 排除 vectorData 等大字段
.toArray();
```
---
## 5. 缺少索引的高频查询 🟡
在没有索引的字段上进行 `find`/`findOne` 会触发全表扫描,随数据量线性增长。
**检查方法**:查看查询条件中的字段,对照 Model 定义确认是否有对应索引。
```typescript
// 检查 Model 定义是否有索引
const DatasetSchema = new Schema({
teamId: { type: String, required: true, index: true }, // ✅ 有索引
name: { type: String }, // ❌ 无索引但被频繁查询?
});
// 高频查询条件
db.datasets.find({ teamId, name }); // name 无索引 → 需要添加
```
# 后端安全检查标准
## 1. NoSQL 注入 🔴
**核心风险**:MongoDB 操作符(`$gt``$where``$regex``$ne` 等)可通过 HTTP 请求体注入。接口将入参直接透传到查询条件时,攻击者可绕过权限校验或泄露数据。
**典型攻击**
```
POST /api/login
{ "username": { "$gt": "" }, "password": { "$gt": "" } }
→ db.users.findOne({ username: { $gt: "" }, password: { $gt: "" } })
→ 匹配所有用户,绕过密码校验
```
**高危模式**
```typescript
// ❌ 入参直接作为查询条件
const { username, password } = req.body;
await db.users.findOne({ username, password });
// ❌ 对象字段透传进查询
async function getUser({ filter }: { filter: object }) {
return db.users.findOne(filter);
}
// ❌ updateOne 条件字段未校验
await db.collection.updateOne(
{ _id: req.body.id }, // id 可能是 { $gt: "" }
{ $set: req.body.update } // update 可能注入 $where
);
```
**修复方案**
```typescript
// ✅ 方案1:zod schema 严格校验(推荐)
const LoginSchema = z.object({
username: z.string().min(1).max(50),
password: z.string().min(1).max(100)
});
const { username, password } = LoginSchema.parse(req.body);
// ✅ 方案2:动态查询使用字段白名单
const ALLOWED_FIELDS = ['status', 'type', 'teamId'] as const;
function buildSafeFilter(raw: Record<string, unknown>) {
return ALLOWED_FIELDS.reduce((acc, key) => {
if (raw[key] !== undefined && typeof raw[key] === 'string') {
acc[key] = raw[key] as string;
}
return acc;
}, {} as Record<string, string>);
}
// ✅ _id 字段强制 ObjectId 转换
await db.collection.findOne({ _id: new Types.ObjectId(id) });
```
**检查清单**
- [ ] 接口入参经过 zod schema 校验
- [ ] 查询条件字段均为原始类型(`string`/`number`/`boolean`
- [ ] `req.body` 对象字段未直接传入 MongoDB 操作符位置
- [ ] `_id` 字段使用 `new Types.ObjectId(id)` 转换
---
## 2. 命令注入 / 路径遍历 🔴
```typescript
// ❌ 危险:用户输入拼入 shell 命令
exec(`convert ${req.body.filename} output.png`);
// filename = "; rm -rf /" → 执行恶意命令
// ❌ 危险:路径拼接未过滤 ../
const filePath = path.join('/uploads', req.body.path);
// path = "../../etc/passwd"
// ✅ 使用 execFile 并传数组参数
execFile('convert', [sanitizedFilename, 'output.png']);
// ✅ 校验路径在允许目录内
const resolved = path.resolve('/uploads', req.body.path);
if (!resolved.startsWith(path.resolve('/uploads'))) {
throw new Error('Invalid path');
}
```
---
## 3. 死循环风险 🔴
**高危模式**
```typescript
// ❌ 递归无终止条件
async function processNode(nodeId: string) {
const node = await getNode(nodeId);
await processNode(node.parentId); // parentId 可能形成环形引用
}
// ❌ while 无退出条件
while (queue.length > 0) {
const item = queue.shift();
queue.push(...item.children); // children 可能重新推入导致无限循环
}
```
**修复方案**
```typescript
// ✅ 递归:深度限制 + 访问集合
async function processNode(nodeId: string, visited = new Set<string>(), depth = 0) {
if (depth > 100 || visited.has(nodeId)) return;
visited.add(nodeId);
const node = await getNode(nodeId);
await processNode(node.parentId, visited, depth + 1);
}
// ✅ 循环:最大迭代次数
const MAX_ITER = 10000;
let iter = 0;
while (queue.length > 0) {
if (++iter > MAX_ITER) throw new Error('Max iterations exceeded');
// ...
}
```
---
## 4. 数据膨胀 🟡
无约束的数据积累导致集合无限增长,查询变慢、内存溢出或磁盘耗尽。
```typescript
// ❌ 数组字段无上限 push
await db.collection.updateOne(
{ _id: id },
{ $push: { logs: newLog } } // 日志数组可无限增长
);
// ❌ 批量写入无上限
await db.collection.insertMany(items); // items 可能有几十万条
// ✅ $push + $slice 保留最近 N 条
await db.collection.updateOne(
{ _id: id },
{ $push: { logs: { $each: [newLog], $slice: -100 } } }
);
// ✅ 批量写入加上限校验
const MAX_BATCH = 1000;
if (items.length > MAX_BATCH) {
throw new Error(`Batch size exceeds limit: ${items.length}`);
}
```
---
## 5. 敏感信息保护 🔴
```typescript
// ❌ 硬编码密钥
const API_KEY = 'sk-1234567890abcdef';
// ❌ 日志包含密码/token
addLog.info('User login', { userId, email, password });
// ✅ 使用环境变量
const API_KEY = process.env.OPENAI_API_KEY;
if (!API_KEY) throw new Error('OPENAI_API_KEY is required');
// ✅ 日志过滤敏感字段
const { password, token, ...safeUser } = user;
addLog.info('User login', safeUser);
// ✅ API 响应过滤敏感字段
const { password: _, ...safeResponse } = userData;
res.json(safeResponse);
```
# 前端 React 性能检查标准
## 1. 不必要的组件重渲染 🟡
父组件状态变化导致子组件不必要地重新渲染,在昂贵组件(复杂列表、图表、编辑器)上会造成明显卡顿。
**识别信号**:子组件接收的 props 在父组件状态变化时并未改变,但子组件仍然重渲染。
```typescript
// ❌ 父组件 count 变化 → ExpensiveChild 不必要地重渲染
const Parent = ({ items }: { items: Item[] }) => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{items.map(item => <ExpensiveChild data={item} key={item.id} />)}
</>
);
};
// ✅ 用 React.memo 跳过 props 未变化的渲染
const ExpensiveChild = React.memo(function ExpensiveChild({ data }: { data: Item }) {
return <div>{/* 昂贵渲染 */}</div>;
});
```
**注意**`React.memo` 对 props 做浅比较,如果传入的是每次新建的对象/函数引用,memo 无效——需要配合下面的优化。
---
## 2. 渲染函数中创建对象或函数 🟡
每次渲染都创建新的对象/数组/函数引用,导致子组件的 `React.memo` 失效,或 `useEffect` 依赖项频繁触发。
```typescript
// ❌ 每次渲染都创建新的函数和对象引用
const MyComponent = ({ items }: { items: Item[] }) => {
return (
<>
{items.map(item => (
<Child
key={item.id}
onClick={() => handleClick(item.id)} // 每次渲染新函数
options={{ enable: true, mode: 'edit' }} // 每次渲染新对象
/>
))}
</>
);
};
// ✅ 用 useCallback/useMemo 稳定引用
const MyComponent = ({ items }: { items: Item[] }) => {
const handleClick = useCallback((id: string) => {
// 处理逻辑
}, []); // 依赖项为空,引用永远稳定
const options = useMemo(() => ({ enable: true, mode: 'edit' }), []);
return (
<>
{items.map(item => (
<Child
key={item.id}
onClick={() => handleClick(item.id)}
options={options}
/>
))}
</>
);
};
```
**判断标准**:只有当子组件是 `React.memo` 包裹的,或该引用是某个 `useEffect`/`useCallback` 的依赖项时,才需要稳定引用。普通非 memo 组件的 props 无需此优化。
---
## 3. 昂贵计算未缓存 🟡
在渲染函数中进行复杂的数组操作(sort、filter、reduce 的链式调用),每次渲染都重新计算,即使输入数据未变化。
```typescript
// ❌ 每次渲染都重新排序和过滤
const ExpensiveList = ({ items }: { items: Item[] }) => {
const sortedItems = [...items].sort((a, b) => a.value - b.value);
const filteredItems = sortedItems.filter(item => item.active);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
};
// ✅ useMemo 缓存计算结果,只在 items 变化时重新计算
const ExpensiveList = ({ items }: { items: Item[] }) => {
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.value - b.value),
[items]
);
const filteredItems = useMemo(
() => sortedItems.filter(item => item.active),
[sortedItems]
);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
};
```
**判断标准**:操作数组长度超过 100 条,或包含复杂排序/计算逻辑时,值得用 `useMemo` 缓存。简单的几条数据无需优化。
---
## 4. 大列表缺少虚拟化 🟢
一次性渲染几百上千个 DOM 节点,会导致首次渲染慢、滚动卡顿、内存占用高。
```typescript
// ❌ 渲染1000条数据 → 1000个真实DOM节点
const List = ({ items }: { items: Item[] }) => (
<div>
{items.map(item => <Row key={item.id} data={item} />)}
</div>
);
// ✅ 虚拟化(仅渲染可见区域的节点)
import { VariableSizeList } from 'react-window';
const VirtualList = ({ items }: { items: Item[] }) => (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={() => 50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<Row data={items[index]} />
</div>
)}
</VariableSizeList>
);
```
**判断标准**:列表超过 200 条且需要同时显示在页面上时,建议虚拟化。
# 前端安全检查标准
## 1. XSS 攻击 🔴
跨站脚本攻击(XSS)通过注入恶意脚本到页面中执行,可窃取用户 Cookie、Session 或劫持页面行为。
### 1.1 dangerouslySetInnerHTML 未净化
```typescript
// ❌ 直接渲染用户输入的 HTML,攻击者可注入 <script>alert('xss')</script>
const UserProfile = ({ user }: { user: User }) => (
<p dangerouslySetInnerHTML={{ __html: user.bio }} />
);
// ✅ 方案1:避免 dangerouslySetInnerHTML,用 React 文本节点渲染(自动转义)
const UserProfile = ({ user }: { user: User }) => (
<p>{user.bio}</p> // React 自动转义,安全
);
// ✅ 方案2:确实需要渲染富文本时,使用 DOMPurify 净化
import DOMPurify from 'dompurify';
const UserProfile = ({ user }: { user: User }) => {
const cleanBio = DOMPurify.sanitize(user.bio, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br']
});
return <p dangerouslySetInnerHTML={{ __html: cleanBio }} />;
};
```
### 1.2 URL 注入
```typescript
// ❌ 危险:href 使用用户输入,可能注入 javascript:alert(1)
const Link = ({ href, text }: { href: string; text: string }) => (
<a href={href}>{text}</a>
);
// ✅ 校验 URL 协议
function sanitizeHref(href: string): string {
try {
const url = new URL(href);
if (!['http:', 'https:'].includes(url.protocol)) {
return '#'; // 拒绝 javascript: 等协议
}
return href;
} catch {
return '#';
}
}
const Link = ({ href, text }: { href: string; text: string }) => (
<a href={sanitizeHref(href)}>{text}</a>
);
```
### 1.3 eval / 动态代码执行
```typescript
// ❌ 危险:执行用户输入的代码
eval(userInput);
new Function('return ' + userInput)();
// ✅ 永远不要对用户输入执行 eval/Function
// 如需动态计算,使用安全的表达式解析库(如 mathjs)
```
---
## 2. 敏感信息暴露在前端 🔴
### 2.1 API Key 硬编码在前端代码
```typescript
// ❌ 危险:密钥打包进前端 bundle,用户可通过 DevTools 获取
const API_KEY = 'sk-1234567890abcdef';
const response = await fetch('/api/endpoint', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
// ✅ 密钥只存在于服务端,前端调用自己的 API 路由
const response = await fetch('/api/my-proxy-route', {
headers: { 'Authorization': `Bearer ${userToken}` } // 用户 token,非服务密钥
});
```
### 2.2 敏感数据存储在 localStorage
```typescript
// ❌ localStorage 可被 XSS 攻击读取
localStorage.setItem('userToken', token);
localStorage.setItem('apiKey', apiKey);
// ✅ 敏感 token 存储在 httpOnly Cookie(无法被 JS 读取)
// 由服务端 Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// 前端无需手动操作,自动随请求发送
```
### 2.3 用户敏感信息打印到控制台
```typescript
// ❌ 生产环境日志暴露敏感信息
console.log('User data:', { userId, email, token, password });
// ✅ 生产环境避免打印敏感字段
if (process.env.NODE_ENV !== 'production') {
console.log('Debug:', { userId });
}
```
---
## 3. CSRF 防护 🟡
跨站请求伪造(CSRF)诱导用户在已登录状态下执行恶意操作。
**检查点**
- 状态变更接口(POST/PUT/DELETE)是否验证了 CSRF Token 或依赖 `SameSite` Cookie?
- 是否接受来自任意 Origin 的跨域请求?
```typescript
// ✅ FastGPT 中通过 Authorization header 携带 token 天然防 CSRF
// (CSRF 攻击无法读取其他域的 Cookie/Header)
fetch('/api/endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
```
# 前端 TypeScript 质量检查标准
## 1. any 类型滥用 🔴
`any` 类型会关闭 TypeScript 的类型检查,使得类型错误只能在运行时暴露,而非编译时发现。
**识别信号**
- 变量、参数、返回值声明为 `any`
- 使用 `as any` 进行类型断言
- API 响应数据直接使用,无类型约束
```typescript
// ❌ any 让类型系统形同虚设
async function fetchData(id: any): any {
const result: any = await db.collection('data').findOne({ id });
return result;
}
// ✅ 明确的类型定义
type UserData = {
id: string;
name: string;
email: string;
createdAt: Date;
};
async function fetchData(id: string): Promise<UserData | null> {
const result = await db.collection<UserData>('data').findOne({ id });
return result;
}
```
**例外情况**
- 第三方库确实没有类型定义时,可以用 `unknown` 代替 `any`,然后通过类型守卫收窄
- 临时调试代码(但合并前必须移除)
---
## 2. 不安全的类型断言 🟡
类型断言(`as Type`)绕过了编译器检查,如果断言错误,运行时会产生难以调试的问题。
```typescript
// ❌ 双重断言(最危险,完全跳过类型检查)
const user = data as any as User;
// ❌ 无根据的断言(data 可能不是 User)
const user = data as User;
user.profile.avatar; // 如果 data 不符合 User 结构,运行时报错
// ✅ 使用类型守卫(编译时 + 运行时都安全)
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof (value as User).id === 'string'
);
}
if (isUser(data)) {
console.log(data.id); // 安全
}
// ✅ 使用 zod 验证外部数据(API 响应、localStorage 读取等)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
});
const user = UserSchema.parse(apiResponse); // 解析失败自动抛出
```
---
## 3. 类型定义不完整 🟡
使用 `object``{}` 或过于宽泛的联合类型,导致 TypeScript 无法提供有效的自动补全和错误提示。
```typescript
// ❌ object 类型无任何约束
function updateUser(id: string, data: object) {
return db.users.updateOne({ id }, { $set: data });
}
// ❌ 函数参数数量超过2个,未用对象收拢
function createItem(name: string, type: string, teamId: string, createdBy: string) {
// ...
}
// ✅ 明确类型定义
type UpdateUserData = {
name?: string;
email?: string;
avatar?: string;
};
function updateUser(id: string, data: UpdateUserData) {
return db.users.updateOne({ id }, { $set: data });
}
// ✅ 多参数用对象收拢(FastGPT 规范:超过2个参数必须用对象)
type CreateItemParams = {
name: string;
type: string;
teamId: string;
createdBy: string;
};
function createItem({ name, type, teamId, createdBy }: CreateItemParams) {
// ...
}
```
---
## 4. 非空断言过度使用 🟡
非空断言(`!`)告诉 TypeScript"这个值不可能是 null/undefined",如果判断错误,会在运行时抛出 `Cannot read properties of null`
```typescript
// ❌ 危险:如果 user 为 null,运行时崩溃
const email = user!.email;
const name = data!.profile!.name;
// ✅ 方案1:提前校验
if (!user) throw new Error('User not found');
const email = user.email; // 此后 TypeScript 知道 user 非空
// ✅ 方案2:可选链(不确定是否存在时)
const name = data?.profile?.name ?? 'Unknown';
// ✅ 方案3:只在确实不可能为空的地方使用(需加注释说明原因)
// teamId 在此处由 authMiddleware 保证非空
const teamId = req.headers.teamId!;
```
---
## 快速检查表
| 检查项 | 级别 |
|--------|------|
| 无 `any` 类型声明 | 🔴 |
| 无 `as any as Type` 双重断言 | 🔴 |
| 函数参数有明确类型约束 | 🟡 |
| 超过2个参数使用对象收拢 | 🟡 |
| `!` 非空断言有校验或注释支撑 | 🟡 |
| 外部数据(API响应)经过 zod 或类型守卫处理 | 🟡 |
# API 路由开发规范
FastGPT 使用 Next.js API Routes,需要遵循特定的开发模式。
### 2.1 路由定义
**文件位置**: `projects/app/src/pages/api/`
**审查要点**:
- ✅ 路由文件使用命名导出,不支持默认导出
- ✅ 使用 `NextAPIRequest``NextAPIResponse` 类型
- ✅ 支持的 HTTP 方法明确 (`GET`, `POST`, `PUT`, `DELETE`)
- ✅ 返回统一的响应格式
**示例**:
```typescript
import type { NextAPIRequest, NextAPIResponse } from '@fastgpt/service/type/next';
import { APIError } from '@fastgpt/service/core/error/controller';
export default async function handler(req: NextAPIRequest, res: NextAPIResponse) {
try {
if (req.method !== 'POST') {
throw new Error('Method not allowed');
}
// 处理逻辑...
const result = await processData(req.body);
res.json(result);
} catch (error) {
APIError(error)(req, res);
}
}
```
### 2.2 类型合约
**文件位置**: `packages/global/openapi/`
**审查要点**:
- ✅ API 合约定义在 OpenAPI 规范文件中
- ✅ 请求参数有完整的类型定义
- ✅ 响应格式有完整的类型定义
- ✅ 错误响应有说明
### 2.3 业务逻辑
**文件位置**:
- 通用逻辑: `packages/service/`
- 项目特定逻辑: `projects/app/src/service/`
**审查要点**:
- ✅ 业务逻辑与 API 路由分离
- ✅ 服务函数有明确的类型定义
- ✅ 错误处理统一
### 2.4 权限验证
**审查要点**:
- ✅ 所有 API 路由都有权限验证 (除了公开端点)
- ✅ 使用 `parseHeaderCert` 解析认证头
- ✅ 验证用户对资源的所有权
- ✅ 敏感操作需要额外验证
**示例**:
```typescript
import { parseHeaderCert } from '@fastgpt/global/support/permission/controller';
export default async function handler(req: NextAPIRequest, res: NextAPIResponse) {
try {
// 解析认证头
const { userId, teamId } = await parseHeaderCert(req);
// 验证权限
const resource = await Resource.findById(resourceId);
if (!resource || resource.userId !== userId) {
throw new Error('Permission denied');
}
// 继续处理...
} catch (error) {
APIError(error)(req, res);
}
}
```
### 2.5 错误处理
**审查要点**:
- ✅ 使用 try-catch 包裹所有异步操作
- ✅ 使用 `APIError` 统一错误响应
- ✅ 错误信息不暴露敏感数据
- ✅ HTTP 状态码正确
---
...@@ -64,5 +64,3 @@ const users = await User.find({}) ...@@ -64,5 +64,3 @@ const users = await User.find({})
- ✅ 处理重复键错误 (code 11000) - ✅ 处理重复键错误 (code 11000)
- ✅ 处理连接错误 - ✅ 处理连接错误
- ✅ 错误日志包含上下文信息 - ✅ 错误日志包含上下文信息
---
...@@ -65,17 +65,21 @@ export const YourComponent = React.memo(function YourComponent({ ...@@ -65,17 +65,21 @@ export const YourComponent = React.memo(function YourComponent({
## 3.4 国际化 ## 3.4 国际化
**审查要点**: **审查要点**:
- ✅ 所有用户可见文本使用 `i18nT` - ✅ 所有用户可见文本使用 `t`, 服务端使用`i18nT`
- ✅ 翻译 key 使用命名空间 - ✅ 翻译 key 使用命名空间
- ✅ 动态文本使用插值 - ✅ 动态文本使用插值
**示例**: **示例**:
```typescript ```typescript
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
const message = i18nT('user:welcome', { name: userName }); const message = i18nT('user:welcome', { name: userName });
``` ```
```typescript
import { useTranslation } from 'next-i18next';
const { t } = useTranslation();
const message = t('user:welcome', { name: userName });
```
## 3.5 性能优化 ## 3.5 性能优化
**审查要点**: **审查要点**:
...@@ -84,5 +88,3 @@ const message = i18nT('user:welcome', { name: userName }); ...@@ -84,5 +88,3 @@ const message = i18nT('user:welcome', { name: userName });
- ✅ 避免在渲染中创建新对象/函数 - ✅ 避免在渲染中创建新对象/函数
- ✅ 使用 `useMemo` 缓存计算结果 - ✅ 使用 `useMemo` 缓存计算结果
- ✅ 使用 `useCallback` 缓存函数 - ✅ 使用 `useCallback` 缓存函数
\ No newline at end of file
---
...@@ -48,5 +48,3 @@ import { UserType } from '@fastgpt/global/core/user/type'; ...@@ -48,5 +48,3 @@ import { UserType } from '@fastgpt/global/core/user/type';
- ✅ 类型文件使用 `.d.ts` 扩展名 - ✅ 类型文件使用 `.d.ts` 扩展名
- ✅ 复杂类型放在独立的类型文件 - ✅ 复杂类型放在独立的类型文件
- ✅ 使用 `export type` 导出类型 - ✅ 使用 `export type` 导出类型
---
# Service 解耦规范
## 核心原则
**后端 service 之间不允许互相引用,只允许单向依赖,跨 service 的协调由上层 controller 完成。**
如果 serviceA 确实需要 serviceB 的数据,**由 controller 提前调用 serviceB,将结果以参数形式传入 serviceA**,而不是让 serviceA 自行 import serviceB。
---
## 依赖方向
```
Controller / API Handler
↓ 调用 serviceB → 得到结果
↓ 将结果作为参数传入 serviceA
Service A Service B Service C
↓ 调用 ↓ 调用 ↓ 调用
Repository Repository Repository
(DB Model) (DB Model) (DB Model)
```
**合法方向**
- `controller``service`(上层调用下层,允许)
- `service` → 本模块的 DB Model(允许)
- `service``packages/service/common/`(公共工具,允许)
- `service` 接收其他 service 的**数据结果**(以参数形式传入,允许)
**违规方向**
- `serviceA` import `serviceB`(同级 service 互相引用,禁止)
- `serviceA``controllerB`(service 引用上层,禁止)
---
## 违规模式识别
### 1. 同级 service 直接 import
```typescript
// ❌ 违规:datasetService 直接引用 workflowService
// packages/service/core/dataset/service.ts
import { dispatchWorkflow } from '../workflow/service';
export async function deleteDataset(datasetId: string) {
await MongoDataset.deleteOne({ _id: datasetId });
await dispatchWorkflow({ datasetId }); // 违规:跨 service 调用
}
```
**识别方式**:在 `packages/service/core/xxx/` 目录下的文件中,import 了同级其他模块的 service 文件。
---
### 2. 循环依赖
```typescript
// ❌ serviceA 引用 serviceB,serviceB 也引用 serviceA → 循环依赖
// dataset/service.ts
import { updateAppDataset } from '../app/service';
// app/service.ts
import { getDatasetInfo } from '../dataset/service';
```
循环依赖会导致模块加载时出现 `undefined` 错误,且极难排查。
---
### 3. 在 service 内触发副作用业务
```typescript
// ❌ service 内部触发了属于其他业务域的逻辑
export async function updateDatasetCollection(collectionId: string, data: UpdateData) {
await MongoDatasetCollection.updateOne({ _id: collectionId }, { $set: data });
// 违规:更新数据集集合后,直接触发通知或工作流——这是其他业务域的职责
await sendTeamNotification(teamId, 'collection_updated');
await triggerRebuildIndex(collectionId);
}
```
---
## 正确模式:由 Controller 协调
### 模式1:Controller 顺序调用多个 service
```typescript
// ✅ controller 层负责协调多个 service
// projects/app/src/pages/api/core/dataset/collection/update.ts
import { updateDatasetCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { sendTeamNotification } from '@fastgpt/service/support/user/team/controller';
import { triggerRebuildIndex } from '@fastgpt/service/core/dataset/training/controller';
export default async function handler(req, res) {
const { collectionId, ...data } = req.body;
// 1. 更新集合(dataset service 只做自己的事)
await updateDatasetCollection(collectionId, data);
// 2. 发送通知(由 controller 层调用通知 service)
await sendTeamNotification(teamId, 'collection_updated');
// 3. 触发重建索引(由 controller 层调用训练 service)
await triggerRebuildIndex(collectionId);
res.json({ success: true });
}
```
### 模式2:serviceA 需要 serviceB 的数据 → Controller 提前获取并以参数传入
当 serviceA 的某个函数需要用到 serviceB 的查询结果时,**不要让 serviceA 内部去调用 serviceB**,而是由 controller 先查询,再将结果作为参数传给 serviceA。
```typescript
// ❌ 违规:serviceA 内部自己去查 serviceB 的数据
// packages/service/core/dataset/service.ts
import { getTeamInfo } from '../support/user/team/service'; // 跨 service import
export async function checkDatasetQuota(datasetId: string) {
const dataset = await MongoDataset.findById(datasetId);
const team = await getTeamInfo(dataset.teamId); // 违规:直接调用其他 service
return dataset.usedSize < team.maxDatasetSize;
}
// ✅ 正确:controller 提前获取 team 信息,以参数形式传入
// packages/service/core/dataset/service.ts
export async function checkDatasetQuota(
datasetId: string,
teamMaxSize: number // ← 由 controller 传入,service 不关心数据从哪来
) {
const dataset = await MongoDataset.findById(datasetId);
return dataset.usedSize < teamMaxSize;
}
// projects/app/src/pages/api/core/dataset/xxx.ts(controller 层)
import { getTeamInfo } from '@fastgpt/service/support/user/team/controller';
import { checkDatasetQuota } from '@fastgpt/service/core/dataset/service';
export default async function handler(req, res) {
const { datasetId, teamId } = req.body;
// controller 负责获取跨域数据
const team = await getTeamInfo(teamId);
// 将所需数据作为参数传入 service
const hasQuota = await checkDatasetQuota(datasetId, team.maxDatasetSize);
// ...
}
```
**这个模式的好处**
- `checkDatasetQuota` 可以独立测试,只需传入 `datasetId``teamMaxSize`,无需 mock 整个 team service
- service 的职责更纯粹:只处理本模块的数据,不感知其他模块的存在
```
---
## 公共逻辑的处理
如果多个 service 都需要某个逻辑,不应让它们互相引用,而应将公共逻辑**下沉到 common 层**。
```
packages/service/
├── common/ ← 公共工具层,可被所有 service 引用
│ ├── error/
│ ├── file/
│ └── string/
├── core/
│ ├── dataset/
│ │ └── service.ts ← 只引用 common/,不引用 core/workflow/
│ └── workflow/
│ └── service.ts ← 只引用 common/,不引用 core/dataset/
```
```typescript
// ✅ 公共逻辑下沉到 common 层
// packages/service/common/permission/utils.ts
export function checkTeamPermission(teamId: string, userId: string) { ... }
// dataset/service.ts 和 workflow/service.ts 都可以引用 common
import { checkTeamPermission } from '../../common/permission/utils';
```
---
## 审查检查清单
审查新增或修改的 service 文件时,重点检查:
- [ ] 文件顶部的 `import` 列表中,是否有引用同级或跨域的 service 文件?
- [ ] 是否出现了 `import xxx from '../otherModule/service'` 或 `from '../otherModule/controller'`?
- [ ] 若有跨模块调用,是否可以通过**上移到 controller 层**来解决?
- [ ] 公共逻辑是否应该下沉到 `common/` 而不是通过互相引用实现?
**快速 grep 方法**:
```bash
# 找出 service 文件中可能的跨 service 引用
grep -rn "from '\.\./[^/]*/service" packages/service/core/
grep -rn "from '\.\./[^/]*/controller" packages/service/core/
```
---
## 为什么这样设计
**可测试性**:service 不依赖其他 service,可以独立 mock 测试,无需构造复杂的依赖图。
**可维护性**:修改一个 service 不会意外影响另一个 service,降低改动的波及范围。
**可读性**:阅读 controller 代码时,业务流程一目了然——"先做 A,再做 B,再做 C",而不是藏在 service 内部的隐式调用链。
**避免循环依赖**:互相引用极易形成循环依赖,导致运行时 `undefined` 错误,且 TypeScript 编译不会报错,只会在运行时暴露。
...@@ -243,7 +243,7 @@ type ResponseType = { ...@@ -243,7 +243,7 @@ type ResponseType = {
temperature?: number; // Temperature temperature?: number; // Temperature
maxToken?: number; // Model max tokens maxToken?: number; // Model max tokens
quoteList?: SearchDataResponseItemType[]; // Citation list quoteList?: SearchDataResponseItemType[]; // Citation list
historyPreview?: ChatItemType[]; // Context preview (history may be truncated) historyPreview?: ChatItemMiniType[]; // Context preview (history may be truncated)
similarity?: number; // Minimum similarity threshold similarity?: number; // Minimum similarity threshold
limit?: number; // Max citation tokens limit?: number; // Max citation tokens
......
...@@ -243,7 +243,7 @@ type ResponseType = { ...@@ -243,7 +243,7 @@ type ResponseType = {
temperature?: number; // 温度 temperature?: number; // 温度
maxToken?: number; // 模型的最大token maxToken?: number; // 模型的最大token
quoteList?: SearchDataResponseItemType[]; // 引用列表 quoteList?: SearchDataResponseItemType[]; // 引用列表
historyPreview?: ChatItemType[]; // 上下文预览(历史记录会被裁剪) historyPreview?: ChatItemMiniType[]; // 上下文预览(历史记录会被裁剪)
similarity?: number; // 最低相关度 similarity?: number; // 最低相关度
limit?: number; // 引用上限token limit?: number; // 引用上限token
......
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
"document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.en.mdx": "2026-03-31T23:15:29+08:00", "document/content/docs/self-host/upgrading/4-14/41410.en.mdx": "2026-03-31T23:15:29+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-04-08T16:15:25+08:00", "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-04-08T16:15:25+08:00",
"document/content/docs/self-host/upgrading/4-14/41411.mdx": "2026-04-09T15:12:39+08:00", "document/content/docs/self-host/upgrading/4-14/41411.mdx": "2026-04-10T13:58:10+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
......
...@@ -10,6 +10,7 @@ import type { ...@@ -10,6 +10,7 @@ import type {
} from 'openai/resources'; } from 'openai/resources';
import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type'; import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type';
import type { Stream } from 'openai/streaming'; import type { Stream } from 'openai/streaming';
import z from 'zod';
// Extension of ChatCompletionMessageParam, Add file url type // Extension of ChatCompletionMessageParam, Add file url type
export type ChatCompletionContentPartFile = { export type ChatCompletionContentPartFile = {
...@@ -74,16 +75,14 @@ export type ChatCompletion = SdkChatCompletion & { ...@@ -74,16 +75,14 @@ export type ChatCompletion = SdkChatCompletion & {
error?: any; error?: any;
}; };
export type CompletionFinishReason = export const CompletionFinishReasonSchema = z
| 'error' .union([
| 'close' z.enum(['error', 'close', 'stop', 'length', 'tool_calls', 'content_filter', 'function_call']),
| 'stop' z.literal(null),
| 'length' z.undefined()
| 'tool_calls' ])
| 'content_filter' .meta({ description: '模型完成原因' });
| 'function_call' export type CompletionFinishReason = z.infer<typeof CompletionFinishReasonSchema>;
| null
| undefined;
export type { Stream }; export type { Stream };
......
import type { import type {
AIChatItemValueItemType, AIChatItemValueItemType,
ChatItemType, ChatItemMiniType,
ChatItemValueItemType, ChatItemValueItemType,
RuntimeUserPromptType, RuntimeUserPromptType,
SystemChatItemValueItemType, SystemChatItemValueItemType,
...@@ -46,7 +46,7 @@ export const chats2GPTMessages = ({ ...@@ -46,7 +46,7 @@ export const chats2GPTMessages = ({
reserveId, reserveId,
reserveTool = false reserveTool = false
}: { }: {
messages: ChatItemType[]; messages: ChatItemMiniType[];
reserveId: boolean; reserveId: boolean;
reserveTool?: boolean; reserveTool?: boolean;
}): ChatCompletionMessageParam[] => { }): ChatCompletionMessageParam[] => {
...@@ -218,7 +218,7 @@ export const GPTMessages2Chats = ({ ...@@ -218,7 +218,7 @@ export const GPTMessages2Chats = ({
reserveTool?: boolean; reserveTool?: boolean;
reserveReason?: boolean; reserveReason?: boolean;
getToolInfo?: (name: string) => { name: string; avatar: string }; getToolInfo?: (name: string) => { name: string; avatar: string };
}): ChatItemType[] => { }): ChatItemMiniType[] => {
const chatMessages = messages const chatMessages = messages
.map((item) => { .map((item) => {
const obj = GPT2Chat[item.role]; const obj = GPT2Chat[item.role];
...@@ -402,7 +402,7 @@ export const GPTMessages2Chats = ({ ...@@ -402,7 +402,7 @@ export const GPTMessages2Chats = ({
.filter((item) => item.value.length > 0); .filter((item) => item.value.length > 0);
// Merge data with the same dataId(Sequential obj merging) // Merge data with the same dataId(Sequential obj merging)
const result = chatMessages.reduce((result: ChatItemType[], currentItem) => { const result = chatMessages.reduce((result: ChatItemMiniType[], currentItem) => {
const lastItem = result[result.length - 1]; const lastItem = result[result.length - 1];
if (lastItem && lastItem.dataId === currentItem.dataId && lastItem.obj === currentItem.obj) { if (lastItem && lastItem.dataId === currentItem.dataId && lastItem.obj === currentItem.obj) {
...@@ -455,7 +455,7 @@ export const runtimePrompt2ChatsValue = (prompt: { ...@@ -455,7 +455,7 @@ export const runtimePrompt2ChatsValue = (prompt: {
return value; return value;
}; };
export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemType[] => { export const getSystemPrompt_ChatItemType = (prompt?: string): ChatItemMiniType[] => {
if (!prompt) return []; if (!prompt) return [];
return [ return [
{ {
......
...@@ -88,3 +88,10 @@ export enum ChatStatusEnum { ...@@ -88,3 +88,10 @@ export enum ChatStatusEnum {
running = 'running', running = 'running',
finish = 'finish' finish = 'finish'
} }
export enum GetChatTypeEnum {
normal = 'normal',
outLink = 'outLink',
team = 'team',
home = 'home'
}
import type { SearchDataResponseItemType } from '../dataset/type'; import { SearchDataResponseItemSchema } from '../dataset/type';
import type { ChatSourceEnum, ChatStatusEnum } from './constants'; import type { ChatSourceEnum } from './constants';
import { ChatFileTypeEnum, ChatRoleEnum } from './constants'; import { ChatFileTypeEnum, ChatRoleEnum } from './constants';
import type { FlowNodeTypeEnum } from '../workflow/node/constant'; import { FlowNodeTypeEnum } from '../workflow/node/constant';
import type { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import type { AppSchemaType, VariableItemType } from '../app/type'; import { AppSchemaTypeSchema, type AppSchemaType, type VariableItemType } from '../app/type';
import type { DispatchNodeResponseType } from '../workflow/runtime/type'; import { DispatchNodeResponseSchema } from '../workflow/runtime/type';
import { WorkflowInteractiveResponseTypeSchema } from '../workflow/template/system/interactive/type'; import { WorkflowInteractiveResponseTypeSchema } from '../workflow/template/system/interactive/type';
import type { FlowNodeInputItemType } from '../workflow/type/io'; import type { FlowNodeInputItemType } from '../workflow/type/io';
import z from 'zod'; import z from 'zod';
import { AgentPlanSchema } from '../ai/agent/type'; import { AgentPlanSchema } from '../ai/agent/type';
export const ChatHistoryItemResSchema = DispatchNodeResponseSchema.extend({
nodeId: z.string(),
id: z.string(),
moduleType: z.enum(FlowNodeTypeEnum),
moduleName: z.string()
});
export type ChatHistoryItemResType = z.infer<typeof ChatHistoryItemResSchema>;
/* One tool run response */ /* One tool run response */
export type ToolRunResponseItemType = any; export type ToolRunResponseItemType = any;
/* tool module response */ /* tool module response */
...@@ -200,43 +208,51 @@ export const AIChatItemValueSchema = z.object({ ...@@ -200,43 +208,51 @@ export const AIChatItemValueSchema = z.object({
export type AIChatItemValueItemType = z.infer<typeof AIChatItemValueSchema>; export type AIChatItemValueItemType = z.infer<typeof AIChatItemValueSchema>;
// TODO 待迁移成 zod export const AIChatItemSchema = z.object({
export type AIChatItemType = { obj: z.literal(ChatRoleEnum.AI),
obj: ChatRoleEnum.AI; value: z.array(AIChatItemValueSchema),
value: AIChatItemValueItemType[]; memories: z.record(z.string(), z.any()).optional(),
memories?: Record<string, any>; userGoodFeedback: z.string().optional(),
userGoodFeedback?: string; userBadFeedback: z.string().optional(),
userBadFeedback?: string; customFeedbacks: z.array(z.string()).optional(),
customFeedbacks?: string[]; adminFeedback: AdminFbkSchema.optional(),
adminFeedback?: AdminFbkType; isFeedbackRead: z.boolean().optional(),
isFeedbackRead?: boolean; durationSeconds: z.number().optional(),
errorMsg: z.string().optional(),
durationSeconds?: number; citeCollectionIds: z.array(z.string()).optional(),
errorMsg?: string; [DispatchNodeResponseKeyEnum.nodeResponse]: z.array(ChatHistoryItemResSchema).optional().meta({
citeCollectionIds?: string[]; description: '节点响应'
})
/** });
* 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema export type AIChatItemType = z.infer<typeof AIChatItemSchema>;
*/
[DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[]; export const ChatItemValueItemSchema = z.union([
}; UserChatItemValueItemSchema,
SystemChatItemValueItemSchema,
export type ChatItemValueItemType = AIChatItemValueSchema
| UserChatItemValueItemType ]);
| SystemChatItemValueItemType export type ChatItemValueItemType = z.infer<typeof ChatItemValueItemSchema>;
| AIChatItemValueItemType;
export type ChatItemObjItemType = UserChatItemType | SystemChatItemType | AIChatItemType; export const ChatItemObjItemSchema = z.union([
UserChatItemSchema,
export type ChatItemSchemaType = ChatItemObjItemType & { SystemChatItemSchema,
dataId: string; AIChatItemSchema
chatId: string; ]);
userId: string; export type ChatItemObjItemType = z.infer<typeof ChatItemObjItemSchema>;
teamId: string;
tmbId: string; export const ChatItemDBSchema = ChatItemObjItemSchema.and(
appId: string; z.object({
time: Date; dataId: z.string(),
deleteTime?: Date | null; chatId: z.string(),
}; userId: z.string(),
teamId: z.string(),
tmbId: z.string(),
appId: z.string(),
time: z.date(),
deleteTime: z.date().nullish()
})
);
export type ChatItemDBSchemaType = z.infer<typeof ChatItemDBSchema>;
// Client error show // Client error show
const ErrorTextItemSchema = z.object({ const ErrorTextItemSchema = z.object({
...@@ -245,67 +261,66 @@ const ErrorTextItemSchema = z.object({ ...@@ -245,67 +261,66 @@ const ErrorTextItemSchema = z.object({
}); });
export type ErrorTextItemType = z.infer<typeof ErrorTextItemSchema>; export type ErrorTextItemType = z.infer<typeof ErrorTextItemSchema>;
export type ResponseTagItemType = {
useAgentSandbox?: boolean;
totalQuoteList?: SearchDataResponseItemType[];
toolCiteLinks?: ToolCiteLinksType[];
errorText?: ErrorTextItemType;
/** @deprecated */
llmModuleAccount?: number;
/** @deprecated */
historyPreviewLength?: number;
};
export type ChatItemType = ChatItemObjItemType & {
dataId?: string;
} & ResponseTagItemType;
/* --------- chat item response ---------- */ /* --------- chat item response ---------- */
export type ChatItemResponseSchemaType = { export const ChatItemResponseSchema = z.object({
teamId: string; teamId: z.string(),
appId: string; appId: z.string(),
chatId: string; chatId: z.string(),
chatItemDataId: string; chatItemDataId: z.string(),
data: ChatHistoryItemResType; data: ChatHistoryItemResSchema
}; });
export type ChatItemResponseSchemaType = z.infer<typeof ChatItemResponseSchema>;
/* --------- team chat --------- */ /* --------- team chat --------- */
export type ChatAppListSchema = { export const ChatAppListSchema = z.object({
apps: AppSchemaType[]; apps: z.array(AppSchemaTypeSchema),
teamInfo: any; teamInfo: z.any(),
uid?: string; uid: z.string().optional()
}; });
export type ChatAppListSchemaType = z.infer<typeof ChatAppListSchema>;
/* ---------- history ------------- */ /* ---------- history ------------- */
export type HistoryItemType = { export const HistoryItemSchema = z.object({
chatId: string; chatId: z.string(),
updateTime: Date; updateTime: z.date(),
customTitle?: string; customTitle: z.string().optional(),
title: string; title: z.string()
}; });
export type ChatHistoryItemType = HistoryItemType & { export type HistoryItemType = z.infer<typeof HistoryItemSchema>;
appId: string;
top?: boolean;
};
/* ------- response data ------------ */ export const ChatHistoryItemSchema = HistoryItemSchema.extend({
export type ChatHistoryItemResType = DispatchNodeResponseType & { appId: z.string(),
nodeId: string; top: z.boolean().optional()
id: string; });
moduleType: FlowNodeTypeEnum; export type ChatHistoryItemType = z.infer<typeof ChatHistoryItemSchema>;
moduleName: string;
};
/* ------- response data ------------ */
export const ToolCiteLinksSchema = z.object({ export const ToolCiteLinksSchema = z.object({
name: z.string(), name: z.string(),
url: z.string() url: z.string()
}); });
export type ToolCiteLinksType = z.infer<typeof ToolCiteLinksSchema>; export type ToolCiteLinksType = z.infer<typeof ToolCiteLinksSchema>;
export const ResponseTagItemSchema = z.object({
useAgentSandbox: z.boolean().optional(),
totalQuoteList: z.array(SearchDataResponseItemSchema).optional(),
toolCiteLinks: z.array(ToolCiteLinksSchema).optional(),
errorText: ErrorTextItemSchema.optional(),
llmModuleAccount: z.number().optional().meta({ deprecated: true }),
historyPreviewLength: z.number().optional().meta({ deprecated: true })
});
export type ResponseTagItemType = z.infer<typeof ResponseTagItemSchema>;
/* dispatch run time */ /* dispatch run time */
export const RuntimeUserPromptSchema = z.object({ export const RuntimeUserPromptSchema = z.object({
files: z.array(UserChatItemFileItemSchema), files: z.array(UserChatItemFileItemSchema),
text: z.string() text: z.string()
}); });
export type RuntimeUserPromptType = z.infer<typeof RuntimeUserPromptSchema>; export type RuntimeUserPromptType = z.infer<typeof RuntimeUserPromptSchema>;
export const ChatItemMiniSchema = ChatItemObjItemSchema.and(
z.object({
dataId: z.string().optional()
})
).and(ResponseTagItemSchema);
export type ChatItemMiniType = z.infer<typeof ChatItemMiniSchema>;
...@@ -4,7 +4,7 @@ import { ChatRoleEnum, ChatSourceEnum } from './constants'; ...@@ -4,7 +4,7 @@ import { ChatRoleEnum, ChatSourceEnum } from './constants';
import { import {
type AIChatItemValueItemType, type AIChatItemValueItemType,
type ChatHistoryItemResType, type ChatHistoryItemResType,
type ChatItemType, type ChatItemMiniType,
type UserChatItemValueItemType type UserChatItemValueItemType
} from './type'; } from './type';
import { sliceStrStartEnd } from '../../common/string/tools'; import { sliceStrStartEnd } from '../../common/string/tools';
...@@ -96,7 +96,7 @@ ${stepText}`; ...@@ -96,7 +96,7 @@ ${stepText}`;
}; };
// Concat 2 -> 1, and sort by role // Concat 2 -> 1, and sort by role
export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => { export const concatHistories = (histories1: ChatItemMiniType[], histories2: ChatItemMiniType[]) => {
const newHistories = [...histories1, ...histories2]; const newHistories = [...histories1, ...histories2];
return newHistories.sort((a, b) => { return newHistories.sort((a, b) => {
if (a.obj === ChatRoleEnum.System) { if (a.obj === ChatRoleEnum.System) {
...@@ -106,7 +106,10 @@ export const concatHistories = (histories1: ChatItemType[], histories2: ChatItem ...@@ -106,7 +106,10 @@ export const concatHistories = (histories1: ChatItemType[], histories2: ChatItem
}); });
}; };
export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue = '新对话') => { export const getChatTitleFromChatMessage = (
message?: ChatItemMiniType,
defaultValue = '新对话'
) => {
// @ts-ignore // @ts-ignore
const textMsg = message?.value.find((item) => 'text' in item && item.text); const textMsg = message?.value.find((item) => 'text' in item && item.text);
...@@ -119,11 +122,11 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue ...@@ -119,11 +122,11 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue
// Keep the first n and last n characters // Keep the first n and last n characters
export const getHistoryPreview = ( export const getHistoryPreview = (
completeMessages: ChatItemType[], completeMessages: ChatItemMiniType[],
size = 100, size = 100,
useVision = false useVision = false
): { ): {
obj: `${ChatRoleEnum}`; obj: ChatRoleEnum;
value: string; value: string;
}[] => { }[] => {
return completeMessages.map((item, i) => { return completeMessages.map((item, i) => {
......
import type { ChunkSettingsType, DatasetDataIndexItemType, DatasetSchemaType } from './type'; import type { ChunkSettingsType, DatasetDataIndexItemType } from './type';
import type { DatasetCollectionTypeEnum, DatasetCollectionDataProcessModeEnum } from './constants'; import type { DatasetCollectionTypeEnum, DatasetCollectionDataProcessModeEnum } from './constants';
import type { ParentIdType } from '../../common/parentFolder/type'; import type { ParentIdType } from '../../common/parentFolder/type';
import type { APIFileItemType } from './apiDataset/type'; import type { APIFileItemType } from './apiDataset/type';
/* ================= dataset ===================== */
export type DatasetUpdateBody = {
id: string;
apiDatasetServer?: DatasetSchemaType['apiDatasetServer'];
parentId?: ParentIdType;
name?: string;
avatar?: string;
intro?: string;
agentModel?: string;
vlmModel?: string;
websiteConfig?: DatasetSchemaType['websiteConfig'];
externalReadUrl?: DatasetSchemaType['externalReadUrl'];
defaultPermission?: DatasetSchemaType['defaultPermission'];
chunkSettings?: DatasetSchemaType['chunkSettings'];
// sync schedule
autoSync?: boolean;
};
/* ================= collection ===================== */ /* ================= collection ===================== */
// Input + store params // Input + store params
type DatasetCollectionStoreDataType = ChunkSettingsType & { type DatasetCollectionStoreDataType = ChunkSettingsType & {
parentId?: string; parentId?: ParentIdType;
metadata?: Record<string, any>; metadata?: Record<string, any>;
customPdfParse?: boolean; customPdfParse?: boolean;
......
import { RequireOnlyOne } from '../../../common/type/utils'; import { ParentIdSchema } from '../../../common/parentFolder/type';
import type { ParentIdType } from '../../../common/parentFolder/type'; import z from 'zod';
export type APIFileItemType = { export const APIFileItemSchema = z.object({
id: string; id: z.string(),
rawId: string; rawId: z.string(),
parentId: ParentIdType; parentId: ParentIdSchema,
name: string; name: z.string(),
type: 'file' | 'folder'; type: z.enum(['file', 'folder']),
updateTime: Date; updateTime: z.date(),
createTime: Date; createTime: z.date(),
hasChild?: boolean; hasChild: z.boolean().optional()
}; });
export type APIFileItemType = z.infer<typeof APIFileItemSchema>;
// Api dataset config // Api dataset config
export type APIFileServer = { export const APIFileServerSchema = z
baseUrl: string; .object({
authorization?: string; baseUrl: z.string(),
basePath?: string; authorization: z.string().optional(),
}; basePath: z.string().optional()
export type FeishuServer = { })
appId: string; .meta({ description: 'API 服务器配置' });
appSecret?: string; export type APIFileServerType = z.infer<typeof APIFileServerSchema>;
folderToken: string; export const FeishuServerSchema = z
}; .object({
export type YuqueServer = { appId: z.string(),
userId: string; appSecret: z.string().optional(),
token?: string; folderToken: z.string()
basePath?: string; })
}; .meta({ description: '飞书服务器配置' });
export type FeishuServerType = z.infer<typeof FeishuServerSchema>;
export const YuqueServerSchema = z
.object({
userId: z.string(),
token: z.string().optional(),
basePath: z.string().optional()
})
.meta({ description: '语雀服务器配置' });
export type YuqueServerType = z.infer<typeof YuqueServerSchema>;
export type ApiDatasetServerType = { export const ApiDatasetServerSchema = z
apiServer?: APIFileServer; .object({
feishuServer?: FeishuServer; apiServer: APIFileServerSchema.optional(),
yuqueServer?: YuqueServer; feishuServer: FeishuServerSchema.optional(),
}; yuqueServer: YuqueServerSchema.optional()
})
.meta({ description: '第三方知识库配置' });
export type ApiDatasetServerType = z.infer<typeof ApiDatasetServerSchema>;
// Api dataset api // Api dataset api
export const ApiFileReadContentResponseSchema = z.object({
title: z.string().optional(),
rawText: z.string()
});
export type ApiFileReadContentResponseType = z.infer<typeof ApiFileReadContentResponseSchema>;
export type ApiFileReadContentResponse = { export const APIFileReadResponseSchema = z.object({
title?: string; url: z.string()
rawText: string; });
}; export type APIFileReadResponseType = z.infer<typeof APIFileReadResponseSchema>;
export type APIFileReadResponse = {
url: string;
};
export type ApiDatasetDetailResponse = APIFileItemType; export type ApiDatasetDetailResponse = APIFileItemType;
import json5 from 'json5'; import json5 from 'json5';
import { checkStrOversize, replaceVariable, valToStr } from '../../../common/string/tools'; import { checkStrOversize, replaceVariable, valToStr } from '../../../common/string/tools';
import { ChatRoleEnum } from '../../../core/chat/constants'; import { ChatRoleEnum } from '../../../core/chat/constants';
import type { ChatItemType } from '../../../core/chat/type'; import type { ChatItemMiniType } from '../../../core/chat/type';
import type { NodeOutputItemType } from './type'; import type { NodeOutputItemType } from './type';
import { ChatCompletionRequestMessageRoleEnum } from '../../ai/constants'; import { ChatCompletionRequestMessageRoleEnum } from '../../ai/constants';
import { import {
...@@ -161,7 +161,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum) ...@@ -161,7 +161,7 @@ export const valueTypeFormat = (value: any, valueType?: WorkflowIOValueTypeEnum)
2. Check that the workflow starts at the interaction node 2. Check that the workflow starts at the interaction node
*/ */
export const getLastInteractiveValue = ( export const getLastInteractiveValue = (
histories: ChatItemType[] histories: ChatItemMiniType[]
): WorkflowInteractiveResponseType | undefined => { ): WorkflowInteractiveResponseType | undefined => {
const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI); const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI);
......
export type ClassifyQuestionAgentItemType = { import z from 'zod';
value: string;
key: string; export const ClassifyQuestionAgentItemSchema = z
}; .object({
value: z.string().meta({ description: '分类值' }),
key: z.string().meta({ description: '分类键' })
})
.meta({ description: '分类问题Agent项' });
export type ClassifyQuestionAgentItemType = z.infer<typeof ClassifyQuestionAgentItemSchema>;
import type { RequireOnlyOne } from '../common/type/utils'; import type { RequireOnlyOne } from '../common/type/utils';
import { z } from 'zod'; import { z } from 'zod';
/* 按 offset 分页 */
export const PaginationSchema = z.object({ export const PaginationSchema = z.object({
pageSize: z.union([z.number(), z.string()]).optional().describe('每页条数'), pageSize: z.union([z.number(), z.string()]).optional().describe('每页条数'),
offset: z.union([z.number(), z.string()]).optional().describe('偏移量(与页码二选一)'), offset: z.union([z.number(), z.string()]).optional().describe('偏移量(与页码二选一)'),
...@@ -24,3 +25,61 @@ export type PaginationResponseType<T = any> = { ...@@ -24,3 +25,61 @@ export type PaginationResponseType<T = any> = {
total: number; total: number;
list: T[]; list: T[];
}; };
export type PaginationResponse<T = any> = PaginationResponseType<T>;
/* 按 cursor 分页 */
export const LinkedPaginationSchema = <TShape extends z.ZodRawShape>(extraShape?: TShape) =>
z.object({
pageSize: z
.int()
.positive()
.optional()
.default(10)
.meta({ example: 15, description: '每页条数' }),
anchor: z.any().optional().meta({ description: '当前锚点(如 chunkIndex)' }),
initialId: z.string().optional().meta({
example: '68ad85a7463006c963799a05',
description: '初始定位数据 ID'
}),
nextId: z.string().optional().meta({
example: '68ad85a7463006c963799a06',
description: '向后翻页的游标 ID'
}),
prevId: z.string().optional().meta({
example: '68ad85a7463006c963799a04',
description: '向前翻页的游标 ID'
}),
...(extraShape ?? ({} as TShape))
});
export type LinkedPaginationProps<T = {}, A = any> = T & {
pageSize: number;
anchor?: A;
initialId?: string;
nextId?: string;
prevId?: string;
};
export const LinkedListResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
z.object({
list: z
.array(
z.intersection(
itemSchema,
z.object({
id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '数据 ID' }),
anchor: z.any().optional().meta({ description: '锚点值' })
})
)
)
.meta({ description: '数据列表' }),
hasMorePrev: z.boolean().meta({ example: false, description: '是否还有更多前置数据' }),
hasMoreNext: z.boolean().meta({ example: true, description: '是否还有更多后置数据' })
});
export type LinkedListResponse<T = {}, A = any> = {
list: Array<T & { id: string; anchor?: A }>;
hasMorePrev: boolean;
hasMoreNext: boolean;
};
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ObjectIdSchema } from '../../../../common/type/mongo';
import z from 'zod'; import z from 'zod';
import { AppChatConfigTypeSchema } from '../../../../core/app/type';
import { AppTypeEnum } from '../../../../core/app/constants';
import { FlowNodeInputItemTypeSchema } from '../../../../core/workflow/type/io';
/* Init */ /* Init */
// Online chat // Online chat
...@@ -19,13 +22,25 @@ export const InitChatQuerySchema = z ...@@ -19,13 +22,25 @@ export const InitChatQuerySchema = z
}); });
export type InitChatQueryType = z.infer<typeof InitChatQuerySchema>; export type InitChatQueryType = z.infer<typeof InitChatQuerySchema>;
export const InitChatResponseSchema = z.object({ export const InitChatResponseSchema = z.object({
chatId: z.string().min(1).describe('对话ID'), chatId: z.string().optional().describe('对话ID'),
appId: ObjectIdSchema.describe('应用ID'), appId: ObjectIdSchema.describe('应用ID'),
userAvatar: z.string().optional().describe('用户头像'), userAvatar: z.string().optional().describe('用户头像'),
title: z.string().min(1).describe('对话标题'), title: z.string().describe('对话标题'),
variables: z.record(z.string(), z.any()).optional().describe('全局变量值'), variables: z.record(z.string(), z.any()).optional().describe('全局变量值'),
app: z.object({}).describe('应用配置') app: z
.object({
chatConfig: AppChatConfigTypeSchema.optional().describe('聊天配置'),
chatModels: z.array(z.string()).optional().describe('聊天模型'),
name: z.string().min(1).describe('应用名称'),
avatar: z.string().describe('应用头像'),
intro: z.string().describe('应用简介'),
canUse: z.boolean().optional().describe('是否可用'),
type: z.enum(AppTypeEnum).describe('应用类型'),
pluginInputs: z.array(FlowNodeInputItemTypeSchema).describe('插件输入')
})
.describe('应用配置')
}); });
export type InitChatResponseType = z.infer<typeof InitChatResponseSchema>;
/* ============ v2/chat/stop ============ */ /* ============ v2/chat/stop ============ */
export const StopV2ChatSchema = z export const StopV2ChatSchema = z
...@@ -56,42 +71,3 @@ export const StopV2ChatResponseSchema = z ...@@ -56,42 +71,3 @@ export const StopV2ChatResponseSchema = z
} }
}); });
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>; 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>;
...@@ -3,80 +3,50 @@ import { TagsMap } from '../../../tag'; ...@@ -3,80 +3,50 @@ import { TagsMap } from '../../../tag';
import { import {
StopV2ChatSchema, StopV2ChatSchema,
StopV2ChatResponseSchema, StopV2ChatResponseSchema,
PresignChatFilePostUrlSchema, InitChatQuerySchema,
PresignChatFileGetUrlSchema InitChatResponseSchema
} from './api'; } from './api';
import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type';
import { z } from 'zod';
export const ChatControllerPath: OpenAPIPath = { export const ChatControllerPath: OpenAPIPath = {
'/v2/chat/stop': { '/core/chat/init': {
post: { get: {
summary: '停止 Agent 运行', summary: '初始化聊天',
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。 description: '初始化聊天',
LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`,
tags: [TagsMap.chatController],
requestBody: {
content: {
'application/json': {
schema: StopV2ChatSchema
}
}
},
responses: {
200: {
description: '成功停止工作流',
content: {
'application/json': {
schema: StopV2ChatResponseSchema
}
}
}
}
}
},
'/core/chat/file/presignChatFilePostUrl': {
post: {
summary: '获取文件上传 URL',
description: '获取文件上传 URL',
tags: [TagsMap.chatController], tags: [TagsMap.chatController],
requestBody: { requestParams: {
content: { query: InitChatQuerySchema
'application/json': {
schema: PresignChatFilePostUrlSchema
}
}
}, },
responses: { responses: {
200: { 200: {
description: '成功上传对话文件预签名 URL', description: '成功返回聊天初始化信息',
content: { content: {
'application/json': { 'application/json': {
schema: CreatePostPresignedUrlResultSchema schema: InitChatResponseSchema
} }
} }
} }
} }
} }
}, },
'/core/chat/file/presignChatFileGetUrl': { '/v2/chat/stop': {
post: { post: {
summary: '获取文件预览地址', summary: '停止 Agent 运行',
description: '获取文件预览地址', description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。
LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`,
tags: [TagsMap.chatController], tags: [TagsMap.chatController],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: PresignChatFileGetUrlSchema schema: StopV2ChatSchema
} }
} }
}, },
responses: { responses: {
200: { 200: {
description: '成功获取对话文件预签名 URL', description: '成功停止工作流',
content: { content: {
'application/json': { 'application/json': {
schema: z.string() schema: StopV2ChatResponseSchema
} }
} }
} }
......
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import z from 'zod';
/* ============ 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>;
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
import { import { PresignChatFilePostUrlSchema, PresignChatFileGetUrlSchema } from './api';
GetQuoteBodySchema, import { CreatePostPresignedUrlResultSchema } from '../../../../../service/common/s3/type';
GetQuoteResponseSchema, import { z } from 'zod';
GetCollectionQuoteBodySchema,
GetCollectionQuoteResSchema
} from './api';
export const ChatQuotePath: OpenAPIPath = { export const ChatFilePath: OpenAPIPath = {
'/core/chat/quote/getQuote': { '/core/chat/file/presignChatFilePostUrl': {
post: { post: {
summary: '获取对话引用数据', summary: '获取文件上传 URL',
description: '获取指定对话消息的数据集引用列表,需要对话访问权限', description: '获取文件上传 URL',
tags: [TagsMap.chatPage], tags: [TagsMap.chatFile],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: GetQuoteBodySchema schema: PresignChatFilePostUrlSchema
} }
} }
}, },
responses: { responses: {
200: { 200: {
description: '成功返回引用数据列表', description: '成功上传对话文件预签名 URL',
content: { content: {
'application/json': { 'application/json': {
schema: GetQuoteResponseSchema schema: CreatePostPresignedUrlResultSchema
} }
} }
} }
} }
} }
}, },
'/core/chat/quote/getCollectionQuote': { '/core/chat/file/presignChatFileGetUrl': {
post: { post: {
summary: '获取集合分页引用数据', summary: '获取文件预览地址',
description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限', description: '获取文件预览地址',
tags: [TagsMap.chatPage], tags: [TagsMap.chatFile],
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
schema: GetCollectionQuoteBodySchema schema: PresignChatFileGetUrlSchema
} }
} }
}, },
responses: { responses: {
200: { 200: {
description: '成功返回分页引用数据', description: '成功获取对话文件预签名 URL',
content: { content: {
'application/json': { 'application/json': {
schema: GetCollectionQuoteResSchema schema: z.string()
} }
} }
} }
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
} from '../../../../core/chat/helperBot/type'; } from '../../../../core/chat/helperBot/type';
import { topAgentParamsSchema } from '../../../../core/chat/helperBot/topAgent/type'; import { topAgentParamsSchema } from '../../../../core/chat/helperBot/topAgent/type';
import { z } from 'zod'; import { z } from 'zod';
import type { PaginationResponse } from '../../../../../web/common/fetch/type'; import type { PaginationResponse } from '../../../api';
import { ChatFileTypeEnum } from '../../../../core/chat/constants'; import { ChatFileTypeEnum } from '../../../../core/chat/constants';
// 分页获取记录 // 分页获取记录
......
...@@ -7,18 +7,22 @@ import { GetRecentlyUsedAppsResponseSchema } from './api'; ...@@ -7,18 +7,22 @@ import { GetRecentlyUsedAppsResponseSchema } from './api';
import { TagsMap } from '../../tag'; import { TagsMap } from '../../tag';
import { ChatControllerPath } from './controler'; import { ChatControllerPath } from './controler';
import { HelperBotPath } from './helperBot'; import { HelperBotPath } from './helperBot';
import { ChatQuotePath } from './quote/index';
import { ChatInputGuidePath } from './inputGuide/index'; import { ChatInputGuidePath } from './inputGuide/index';
import { OutLinkChatPath } from './outLink/index';
import { ChatRecordPath } from './record/index';
import { ChatFilePath } from './file';
export const ChatPath: OpenAPIPath = { export const ChatPath: OpenAPIPath = {
...ChatFeedbackPath,
...ChatFilePath,
...ChatSettingPath, ...ChatSettingPath,
...ChatFavouriteAppPath, ...ChatFavouriteAppPath,
...ChatFeedbackPath,
...ChatHistoryPath, ...ChatHistoryPath,
...ChatControllerPath, ...ChatControllerPath,
...HelperBotPath, ...HelperBotPath,
...ChatQuotePath,
...ChatInputGuidePath, ...ChatInputGuidePath,
...OutLinkChatPath,
...ChatRecordPath,
'/core/chat/recentlyUsed': { '/core/chat/recentlyUsed': {
get: { get: {
......
import { z } from 'zod'; import { z } from 'zod';
import { PaginationSchema } from '../../../api'; import { PaginationSchema } from '../../../api';
import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ObjectIdSchema } from '../../../../common/type/mongo';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
/* ============================================================================ /* ============================================================================
* API: 获取对话输入引导列表 * API: 获取对话输入引导列表
...@@ -30,3 +31,127 @@ export const ChatInputGuideListResponseSchema = z.object({ ...@@ -30,3 +31,127 @@ export const ChatInputGuideListResponseSchema = z.object({
total: z.number().meta({ example: 10, description: '总数' }) total: z.number().meta({ example: 10, description: '总数' })
}); });
export type ChatInputGuideListResponseType = z.infer<typeof ChatInputGuideListResponseSchema>; export type ChatInputGuideListResponseType = z.infer<typeof ChatInputGuideListResponseSchema>;
/* ============================================================================
* API: 统计对话输入引导总数
* Route: GET /api/core/chat/inputGuide/countTotal
* Method: GET
* Description: 获取指定应用的对话输入引导总数
* Tags: ['Chat', 'InputGuide', 'Read']
* ============================================================================ */
export const CountChatInputGuideTotalQuerySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' })
});
export type CountChatInputGuideTotalQueryType = z.infer<typeof CountChatInputGuideTotalQuerySchema>;
export const CountChatInputGuideTotalResponseSchema = z.object({
total: z.number().int().nonnegative().meta({ example: 10, description: '总数' })
});
export type CountChatInputGuideTotalResponseType = z.infer<
typeof CountChatInputGuideTotalResponseSchema
>;
/* ============================================================================
* API: 创建对话输入引导
* Route: POST /api/core/chat/inputGuide/create
* Method: POST
* Description: 批量创建对话输入引导文本
* Tags: ['Chat', 'InputGuide', 'Write']
* ============================================================================ */
export const CreateChatInputGuideBodySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
textList: z
.array(z.string())
.min(1)
.meta({ example: ['如何开始使用?', '有哪些功能?'], description: '引导文本列表' })
});
export type CreateChatInputGuideBodyType = z.infer<typeof CreateChatInputGuideBodySchema>;
export const CreateChatInputGuideResponseSchema = z.object({
insertLength: z
.number()
.int()
.nonnegative()
.meta({ example: 2, description: '实际插入成功的数量' })
});
export type CreateChatInputGuideResponseType = z.infer<typeof CreateChatInputGuideResponseSchema>;
/* ============================================================================
* API: 删除对话输入引导
* Route: DELETE /api/core/chat/inputGuide/delete
* Method: DELETE
* Description: 批量删除指定的对话输入引导
* Tags: ['Chat', 'InputGuide', 'Delete']
* ============================================================================ */
export const DeleteChatInputGuideBodySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
dataIdList: z
.array(z.string())
.min(1)
.meta({
example: ['68ad85a7463006c963799a05', '68ad85a7463006c963799a06'],
description: '要删除的引导 ID 列表'
})
});
export type DeleteChatInputGuideBodyType = z.infer<typeof DeleteChatInputGuideBodySchema>;
export const DeleteChatInputGuideResponseSchema = z.object({});
export type DeleteChatInputGuideResponseType = z.infer<typeof DeleteChatInputGuideResponseSchema>;
/* ============================================================================
* API: 删除应用所有对话输入引导
* Route: DELETE /api/core/chat/inputGuide/deleteAll
* Method: DELETE
* Description: 删除指定应用的所有对话输入引导
* Tags: ['Chat', 'InputGuide', 'Delete']
* ============================================================================ */
export const DeleteAllChatInputGuideBodySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' })
});
export type DeleteAllChatInputGuideBodyType = z.infer<typeof DeleteAllChatInputGuideBodySchema>;
export const DeleteAllChatInputGuideResponseSchema = z.object({});
export type DeleteAllChatInputGuideResponseType = z.infer<
typeof DeleteAllChatInputGuideResponseSchema
>;
/* ============================================================================
* API: 查询对话输入引导(公开接口)
* Route: POST /api/core/chat/inputGuide/query
* Method: POST
* Description: 根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权
* Tags: ['Chat', 'InputGuide', 'Read']
* ============================================================================ */
export const QueryChatInputGuideBodySchema = OutLinkChatAuthSchema.extend({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
searchKey: z.string().meta({ example: '如何使用', description: '搜索关键词' })
});
export type QueryChatInputGuideBodyType = z.infer<typeof QueryChatInputGuideBodySchema>;
export const QueryChatInputGuideResponseSchema = z.array(
z.string().meta({ example: '如何开始使用?', description: '引导文本' })
);
export type QueryChatInputGuideResponseType = z.infer<typeof QueryChatInputGuideResponseSchema>;
/* ============================================================================
* API: 更新对话输入引导
* Route: PUT /api/core/chat/inputGuide/update
* Method: PUT
* Description: 更新指定的对话输入引导文本
* Tags: ['Chat', 'InputGuide', 'Write']
* ============================================================================ */
export const UpdateChatInputGuideBodySchema = z.object({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
dataId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '要更新的引导 ID' }),
text: z.string().min(1).meta({ example: '如何开始使用?', description: '新的引导文本' })
});
export type UpdateChatInputGuideBodyType = z.infer<typeof UpdateChatInputGuideBodySchema>;
export const UpdateChatInputGuideResponseSchema = z.object({});
export type UpdateChatInputGuideResponseType = z.infer<typeof UpdateChatInputGuideResponseSchema>;
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
import { ChatInputGuideListBodySchema, ChatInputGuideListResponseSchema } from './api'; import {
ChatInputGuideListBodySchema,
ChatInputGuideListResponseSchema,
CountChatInputGuideTotalResponseSchema,
CreateChatInputGuideBodySchema,
CreateChatInputGuideResponseSchema,
DeleteChatInputGuideBodySchema,
DeleteChatInputGuideResponseSchema,
DeleteAllChatInputGuideBodySchema,
DeleteAllChatInputGuideResponseSchema,
QueryChatInputGuideBodySchema,
QueryChatInputGuideResponseSchema,
UpdateChatInputGuideBodySchema,
UpdateChatInputGuideResponseSchema
} from './api';
export const ChatInputGuidePath: OpenAPIPath = { export const ChatInputGuidePath: OpenAPIPath = {
'/core/chat/inputGuide/list': { '/core/chat/inputGuide/list': {
...@@ -26,5 +40,150 @@ export const ChatInputGuidePath: OpenAPIPath = { ...@@ -26,5 +40,150 @@ export const ChatInputGuidePath: OpenAPIPath = {
} }
} }
} }
},
'/core/chat/inputGuide/countTotal': {
get: {
summary: '统计对话输入引导总数',
description: '获取指定应用的对话输入引导总数',
tags: [TagsMap.chatInputGuide],
parameters: [
{
in: 'query',
name: 'appId',
schema: { type: 'string', example: '68ad85a7463006c963799a05', description: '应用 ID' },
required: true
}
],
responses: {
200: {
description: '成功返回总数',
content: {
'application/json': {
schema: CountChatInputGuideTotalResponseSchema
}
}
}
}
}
},
'/core/chat/inputGuide/create': {
post: {
summary: '创建对话输入引导',
description: '批量创建对话输入引导文本',
tags: [TagsMap.chatInputGuide],
requestBody: {
content: {
'application/json': {
schema: CreateChatInputGuideBodySchema
}
}
},
responses: {
200: {
description: '成功返回插入数量',
content: {
'application/json': {
schema: CreateChatInputGuideResponseSchema
}
}
}
}
}
},
'/core/chat/inputGuide/delete': {
delete: {
summary: '删除对话输入引导',
description: '批量删除指定的对话输入引导',
tags: [TagsMap.chatInputGuide],
requestBody: {
content: {
'application/json': {
schema: DeleteChatInputGuideBodySchema
}
}
},
responses: {
200: {
description: '删除成功',
content: {
'application/json': {
schema: DeleteChatInputGuideResponseSchema
}
}
}
}
}
},
'/core/chat/inputGuide/deleteAll': {
delete: {
summary: '删除应用所有对话输入引导',
description: '删除指定应用的所有对话输入引导',
tags: [TagsMap.chatInputGuide],
requestBody: {
content: {
'application/json': {
schema: DeleteAllChatInputGuideBodySchema
}
}
},
responses: {
200: {
description: '删除成功',
content: {
'application/json': {
schema: DeleteAllChatInputGuideResponseSchema
}
}
}
}
}
},
'/core/chat/inputGuide/query': {
post: {
summary: '查询对话输入引导(公开接口)',
description: '根据搜索词查询对话输入引导,支持分享链接和团队 Token 鉴权',
tags: [TagsMap.chatInputGuide],
requestBody: {
content: {
'application/json': {
schema: QueryChatInputGuideBodySchema
}
}
},
responses: {
200: {
description: '成功返回引导文本列表',
content: {
'application/json': {
schema: QueryChatInputGuideResponseSchema
}
}
}
}
}
},
'/core/chat/inputGuide/update': {
put: {
summary: '更新对话输入引导',
description: '更新指定的对话输入引导文本',
tags: [TagsMap.chatInputGuide],
requestBody: {
content: {
'application/json': {
schema: UpdateChatInputGuideBodySchema
}
}
},
responses: {
200: {
description: '更新成功',
content: {
'application/json': {
schema: UpdateChatInputGuideResponseSchema
}
}
}
}
}
} }
}; };
import z from 'zod';
// ============= Init OutLink Chat =============
export const InitOutLinkChatQuerySchema = z.object({
chatId: z.string().optional().describe('会话ID'),
shareId: z.string().describe('分享链接ID'),
outLinkUid: z.string().describe('外链用户ID')
});
export type InitOutLinkChatQueryType = z.infer<typeof InitOutLinkChatQuerySchema>;
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import { InitOutLinkChatQuerySchema } from './api';
export const OutLinkChatPath: OpenAPIPath = {
'/core/chat/outLink/init': {
get: {
summary: '初始化外链会话',
description: '通过分享链接初始化会话,获取应用配置和历史会话信息',
tags: [TagsMap.chatPage],
requestParams: {
query: InitOutLinkChatQuerySchema
},
responses: {
200: {
description: '成功返回会话初始化信息'
}
}
}
}
};
import { z } from 'zod'; import z from 'zod';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import { ObjectIdSchema } from '../../../../common/type/mongo';
import { DatasetCiteItemSchema } from '../../../../core/dataset/type'; import { DatasetCiteItemSchema } from '../../../../core/dataset/type';
import { LinkedListResponseSchema, LinkedPaginationSchema, PaginationSchema } from '../../../api';
import { ChatItemMiniSchema } from '../../../../core/chat/type';
import { AppTTSConfigTypeSchema } from '../../../../core/app/type';
import { GetChatTypeEnum } from '../../../../core/chat/constants';
/* ============================================================================
* API: 获取对话响应详细数据
* Route: GET /api/core/chat/record/getResData
* Method: GET
* Description: 根据 dataId 获取对话中某条 AI 回复的详细响应数据
* ============================================================================ */
export const GetResDataQuerySchema = OutLinkChatAuthSchema.extend({
appId: z.string().describe('应用ID'),
chatId: z.string().optional().describe('会话ID'),
dataId: z.string().describe('对话数据ID')
});
export type GetResDataQueryType = z.infer<typeof GetResDataQuerySchema>;
/* ============================================================================
* API: 删除对话记录
* Route: DELETE /api/core/chat/record/delete
* Method: DELETE
* Description: 软删除指定的对话消息记录(设置 deleteTime)
* ============================================================================ */
export const DeleteChatRecordBodySchema = OutLinkChatAuthSchema.extend({
appId: ObjectIdSchema.meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
chatId: z.string().meta({ example: 'chat123', description: '会话 ID' }),
contentId: z.string().optional().meta({
example: 'content123',
description: '要删除的消息 ID'
}),
delFile: z.coerce.boolean().optional().meta({
example: false,
description: '是否同时删除关联文件'
})
});
export type DeleteChatRecordBodyType = z.infer<typeof DeleteChatRecordBodySchema>;
export const DeleteChatRecordResponseSchema = z.object({});
export type DeleteChatRecordResponseType = z.infer<typeof DeleteChatRecordResponseSchema>;
/* ============================================================================ /* ============================================================================
* API: 获取对话引用数据 * API: 获取对话引用数据
* Route: POST /api/core/chat/quote/getQuote * Route: POST /api/core/chat/quote/getQuote
* Method: POST * Method: POST
* Description: 获取指定对话消息的数据集引用列表 * Description: 获取指定对话消息的数据集引用列表
* Tags: ['Chat', 'Quote', 'Read']
* ============================================================================ */ * ============================================================================ */
export const GetQuoteBodySchema = OutLinkChatAuthSchema.extend({ export const GetQuoteBodySchema = OutLinkChatAuthSchema.extend({
...@@ -46,7 +87,6 @@ export type GetQuoteResponseType = z.infer<typeof GetQuoteResponseSchema>; ...@@ -46,7 +87,6 @@ export type GetQuoteResponseType = z.infer<typeof GetQuoteResponseSchema>;
* Route: POST /api/core/chat/quote/getCollectionQuote * Route: POST /api/core/chat/quote/getCollectionQuote
* Method: POST * Method: POST
* Description: 以链式分页方式获取指定集合的引用数据,支持前后翻页 * Description: 以链式分页方式获取指定集合的引用数据,支持前后翻页
* Tags: ['Chat', 'Quote', 'Read']
* ============================================================================ */ * ============================================================================ */
export const GetCollectionQuoteBodySchema = OutLinkChatAuthSchema.extend({ export const GetCollectionQuoteBodySchema = OutLinkChatAuthSchema.extend({
...@@ -73,3 +113,64 @@ export const GetCollectionQuoteResSchema = z.object({ ...@@ -73,3 +113,64 @@ export const GetCollectionQuoteResSchema = z.object({
hasMoreNext: z.boolean().describe('是否还有更多后置数据') hasMoreNext: z.boolean().describe('是否还有更多后置数据')
}); });
export type GetCollectionQuoteResType = z.infer<typeof GetCollectionQuoteResSchema>; export type GetCollectionQuoteResType = z.infer<typeof GetCollectionQuoteResSchema>;
/* ============================================================================
* API: 分页获取对话记录
* Route: POST /api/core/chat/record/getPaginationRecords
* Method: POST
* Description: 分页获取指定应用和会话的对话记录,支持多种鉴权模式
* ============================================================================ */
const GetRecordPropsSchema = z.object({
appId: ObjectIdSchema.meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
chatId: z.string().optional().meta({ example: 'chat123', description: '会话 ID' }),
loadCustomFeedbacks: z.boolean().optional().meta({
example: false,
description: '是否加载自定义反馈'
}),
type: z
.enum(GetChatTypeEnum)
.optional()
.meta({ example: 'normal', description: '获取类型,影响数据过滤规则' }),
includeDeleted: z.boolean().optional().meta({
example: false,
description: '是否包含已删除的记录'
})
});
export const GetPaginationRecordsBodySchema = PaginationSchema.extend(
OutLinkChatAuthSchema.shape
).extend(GetRecordPropsSchema.shape);
export type GetPaginationRecordsBodyType = z.infer<typeof GetPaginationRecordsBodySchema>;
export const GetPaginationRecordsResponseSchema = z.object({
list: z.array(z.any()).meta({ description: '对话记录列表' }),
total: z.number().int().nonnegative().meta({ example: 10, description: '总数' })
});
export type GetPaginationRecordsResponseType = z.infer<typeof GetPaginationRecordsResponseSchema>;
/* ============================================================================
* API: 获取对话记录(v2)
* Route: POST /api/core/chat/record/getRecordsV2
* Method: POST
* Description: 获取对话记录(v2)
* ============================================================================ */
export const GetRecordsV2BodySchema = LinkedPaginationSchema(GetRecordPropsSchema.shape);
export type GetRecordsV2BodyType = z.infer<typeof GetRecordsV2BodySchema>;
export const GetRecordsV2ResponseSchema = LinkedListResponseSchema(ChatItemMiniSchema).extend({
total: z.int()
});
export type GetRecordsV2ResponseType = z.infer<typeof GetRecordsV2ResponseSchema>;
/* ============================================================================
* API: 获取语音合成
* Route: POST /api/core/chat/record/getSpeech
* Method: POST
* Description: 将文本转换为语音,返回二进制音频数据流
* ============================================================================ */
export const GetChatSpeechBodySchema = OutLinkChatAuthSchema.extend({
appId: z.string().meta({ example: '68ad85a7463006c963799a05', description: '应用 ID' }),
ttsConfig: AppTTSConfigTypeSchema.meta({ description: 'TTS 配置' }),
input: z.string().meta({ example: '你好,世界', description: '要转换的文本内容' })
});
export type GetChatSpeechBodyType = z.infer<typeof GetChatSpeechBodySchema>;
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import {
GetResDataQuerySchema,
DeleteChatRecordBodySchema,
DeleteChatRecordResponseSchema,
GetQuoteBodySchema,
GetQuoteResponseSchema,
GetCollectionQuoteBodySchema,
GetCollectionQuoteResSchema,
GetPaginationRecordsBodySchema,
GetPaginationRecordsResponseSchema,
GetRecordsV2BodySchema,
GetRecordsV2ResponseSchema,
GetChatSpeechBodySchema
} from './api';
export const ChatRecordPath: OpenAPIPath = {
'/core/chat/record/getPaginationRecords': {
post: {
summary: '分页获取对话记录',
description: '分页获取指定应用和会话的对话记录,支持多种鉴权模式',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: GetPaginationRecordsBodySchema
}
}
},
responses: {
200: {
description: '成功返回对话记录',
content: {
'application/json': {
schema: GetPaginationRecordsResponseSchema
}
}
}
}
}
},
'/core/chat/record/getRecords_v2': {
post: {
summary: '根据锚点获取对话记录',
description: '根据锚点获取指定应用和会话的对话记录,支持多种鉴权模式',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: GetRecordsV2BodySchema
}
}
},
responses: {
200: {
description: '成功返回对话记录',
content: {
'application/json': {
schema: GetRecordsV2ResponseSchema
}
}
}
}
}
},
'/core/chat/record/getResData': {
get: {
summary: '获取对话响应详细数据',
description: '根据 dataId 获取对话中某条 AI 回复的详细响应数据',
tags: [TagsMap.chatRecord],
requestParams: {
query: GetResDataQuerySchema
},
responses: {
200: {
description: '成功返回响应数据'
}
}
}
},
'/core/chat/record/getQuote': {
post: {
summary: '获取对话引用数据',
description: '获取指定对话消息的数据集引用列表,需要对话访问权限',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: GetQuoteBodySchema
}
}
},
responses: {
200: {
description: '成功返回引用数据列表',
content: {
'application/json': {
schema: GetQuoteResponseSchema
}
}
}
}
}
},
'/core/chat/record/getCollectionQuote': {
post: {
summary: '获取集合分页引用数据',
description: '以链式分页方式获取指定集合的引用数据,支持前后翻页,需要对话访问权限',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: GetCollectionQuoteBodySchema
}
}
},
responses: {
200: {
description: '成功返回分页引用数据',
content: {
'application/json': {
schema: GetCollectionQuoteResSchema
}
}
}
}
}
},
'/core/chat/record/delete': {
delete: {
summary: '删除对话记录',
description: '软删除指定的对话消息记录(设置 deleteTime)',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: DeleteChatRecordBodySchema
}
}
},
responses: {
200: {
description: '删除成功',
content: {
'application/json': {
schema: DeleteChatRecordResponseSchema
}
}
}
}
}
},
'/core/chat/record/getSpeech': {
post: {
summary: '获取语音合成',
description: '将文本转换为语音,返回二进制音频数据流',
tags: [TagsMap.chatRecord],
requestBody: {
content: {
'application/json': {
schema: GetChatSpeechBodySchema
}
}
},
responses: {
200: {
description: '成功返回二进制音频数据流'
}
}
}
}
};
import { ParentIdSchema } from '../../../../common/parentFolder/type';
import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ObjectIdSchema } from '../../../../common/type/mongo';
import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat';
import z from 'zod'; import z from 'zod';
// ============= Scroll Collections =============
export const ScrollCollectionsBodySchema = z.object({
datasetId: z.string(),
parentId: z.string().nullable().optional().default(null),
searchText: z.string().optional().default(''),
selectFolder: z.boolean().optional().default(false),
filterTags: z.array(z.string()).optional().default([]),
simple: z.boolean().optional().default(false)
});
export type ScrollCollectionsBodyType = z.infer<typeof ScrollCollectionsBodySchema>;
// ============= Update Collection =============
export const UpdateDatasetCollectionBodySchema = z.object({
id: ObjectIdSchema.optional().describe('集合ID,与 datasetId+externalFileId 二选一'),
parentId: ParentIdSchema.describe('父级目录ID'),
name: z.string().optional().describe('集合名称'),
tags: z.array(z.string()).optional().describe('标签列表(标签名称,非ID)'),
forbid: z.boolean().optional().describe('是否禁用'),
createTime: z.coerce.date().optional().describe('创建时间'),
datasetId: z.string().optional().describe('数据集ID,配合 externalFileId 使用'),
externalFileId: z.string().optional().describe('外部文件ID,配合 datasetId 使用')
});
export type UpdateDatasetCollectionBodyType = z.infer<typeof UpdateDatasetCollectionBodySchema>;
// ============= Export Collection =============
// Schema 1: Basic collection export with authentication // Schema 1: Basic collection export with authentication
const BasicExportSchema = z const BasicExportSchema = z
.object({ .object({
......
import type { OpenAPIPath } from '../../../type'; import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag'; import { TagsMap } from '../../../tag';
import { ExportCollectionBodySchema } from './api'; import {
ExportCollectionBodySchema,
ScrollCollectionsBodySchema,
UpdateDatasetCollectionBodySchema
} from './api';
export const DatasetCollectionPath: OpenAPIPath = { export const DatasetCollectionPath: OpenAPIPath = {
'/core/dataset/collection/scrollList': {
post: {
summary: '获取数据集集合列表(滚动分页)',
description: '获取数据集集合列表(滚动分页)',
tags: [TagsMap.datasetCollection],
requestBody: {
content: {
'application/json': {
schema: ScrollCollectionsBodySchema
}
}
},
responses: {
200: {
description: '成功返回集合列表'
}
}
}
},
'/core/dataset/collection/update': {
post: {
summary: '更新数据集集合信息',
description: '更新数据集集合信息,支持通过集合ID或数据集ID+外部文件ID定位集合',
tags: [TagsMap.datasetCollection],
requestBody: {
content: {
'application/json': {
schema: UpdateDatasetCollectionBodySchema
}
}
},
responses: {
200: {
description: '成功更新集合信息'
}
}
}
},
'/core/dataset/collection/export': { '/core/dataset/collection/export': {
post: { post: {
summary: '下载集合的所有数据块', summary: '下载集合的所有数据块',
......
import type { OpenAPIPath } from '../../type'; import type { OpenAPIPath } from '../../type';
import { TagsMap } from '../../tag';
import { DatasetDataPath } from './data'; import { DatasetDataPath } from './data';
import { DatasetCollectionPath } from './collection'; import { DatasetCollectionPath } from './collection';
import {
CreateDatasetBodySchema,
CreateDatasetWithFilesBodySchema,
DeleteDatasetQuerySchema,
GetDatasetDetailQuerySchema,
GetDatasetListBodySchema,
GetDatasetPathsQuerySchema,
UpdateDatasetBodySchema,
ResumeDatasetInheritPermissionBodySchema,
CreateDatasetFolderBodySchema,
SearchDatasetTestBodySchema,
ExportDatasetQuerySchema
} from './api';
export const DatasetPath: OpenAPIPath = { export const DatasetPath: OpenAPIPath = {
...DatasetDataPath, '/core/dataset/create': {
...DatasetCollectionPath post: {
summary: '创建知识库',
description: '创建新的知识库,支持多种类型(普通知识库、文件夹、网站知识库等)',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: CreateDatasetBodySchema
}
}
},
responses: {
200: {
description: '成功返回新创建的知识库 ID'
}
}
}
},
'/core/dataset/createWithFiles': {
post: {
summary: '创建知识库并上传文件',
description: '一步完成知识库创建和文件上传,自动创建集合并开始数据处理',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: CreateDatasetWithFilesBodySchema
}
}
},
responses: {
200: {
description: '成功返回知识库信息和向量模型配置'
}
}
}
},
'/core/dataset/folder/create': {
post: {
summary: '创建知识库文件夹',
description: '创建知识库文件夹,用于组织和管理知识库',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: CreateDatasetFolderBodySchema
}
}
},
responses: {
200: {
description: '成功创建文件夹'
}
}
}
},
'/core/dataset/list': {
post: {
summary: '获取知识库列表',
description: '获取当前用户有权限访问的知识库列表,支持按类型和关键词筛选',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: GetDatasetListBodySchema
}
}
},
responses: {
200: {
description: '成功返回知识库列表'
}
}
}
},
'/core/dataset/paths': {
get: {
summary: '获取知识库路径',
description: '获取知识库的父级路径链,用于面包屑导航',
tags: [TagsMap.datasetCommon],
requestParams: {
query: GetDatasetPathsQuerySchema
},
responses: {
200: {
description: '成功返回路径列表'
}
}
}
},
'/core/dataset/detail': {
get: {
summary: '获取知识库详情',
description: '获取知识库详细信息,包括模型配置、权限和同步状态',
tags: [TagsMap.datasetCommon],
requestParams: {
query: GetDatasetDetailQuerySchema
},
responses: {
200: {
description: '成功返回知识库详情'
}
}
}
},
'/core/dataset/delete': {
delete: {
summary: '删除知识库',
description: '删除知识库及其所有子知识库,需要所有者权限',
tags: [TagsMap.datasetCommon],
requestParams: {
query: DeleteDatasetQuerySchema
},
responses: {
200: {
description: '成功删除知识库'
}
}
}
},
'/core/dataset/update': {
put: {
summary: '更新知识库',
description: '更新知识库信息、配置或移动知识库到其他目录',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: UpdateDatasetBodySchema
}
}
},
responses: {
200: {
description: '成功更新知识库'
}
}
}
},
'/core/dataset/resumeInheritPermission': {
put: {
summary: '恢复知识库继承权限',
description: '恢复知识库的继承权限,使其权限与父级保持一致',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: ResumeDatasetInheritPermissionBodySchema
}
}
},
responses: {
200: {
description: '成功恢复继承权限'
}
}
}
},
'/core/dataset/searchTest': {
post: {
summary: '搜索测试',
description: '对知识库执行搜索测试,支持多种搜索模式、重排序和问题扩展',
tags: [TagsMap.datasetCommon],
requestBody: {
content: {
'application/json': {
schema: SearchDatasetTestBodySchema
}
}
},
responses: {
200: {
description: '成功返回搜索结果列表及耗时信息'
}
}
}
},
'/core/dataset/exportAll': {
get: {
summary: '导出知识库全部数据',
description: '以流式 CSV 格式导出知识库及其所有子知识库的数据',
tags: [TagsMap.datasetCommon],
requestParams: {
query: ExportDatasetQuerySchema
},
responses: {
200: {
description: '流式返回 CSV 文件'
}
}
}
},
...DatasetCollectionPath,
...DatasetDataPath
}; };
...@@ -39,16 +39,22 @@ export const openAPIDocument = createDocument({ ...@@ -39,16 +39,22 @@ export const openAPIDocument = createDocument({
tags: [TagsMap.aiSkill, TagsMap.sandbox] tags: [TagsMap.aiSkill, TagsMap.sandbox]
}, },
{ {
name: '对话', name: '对话模块配置',
tags: [TagsMap.chatSetting, TagsMap.chatPage] tags: [TagsMap.chatSetting, TagsMap.chatPage, TagsMap.chatInputGuide]
}, },
{ {
name: '对话管理', name: '对话模块使用',
tags: [TagsMap.chatHistory, TagsMap.chatController, TagsMap.chatFeedback] tags: [
TagsMap.chatHistory,
TagsMap.chatFeedback,
TagsMap.chatFile,
TagsMap.chatRecord,
TagsMap.chatController
]
}, },
{ {
name: '知识库', name: '知识库',
tags: [TagsMap.datasetCollection] tags: [TagsMap.datasetCommon, TagsMap.datasetCollection]
}, },
{ {
name: '插件系统', name: '插件系统',
......
...@@ -3,10 +3,12 @@ import type { OpenAPIPath } from '../type'; ...@@ -3,10 +3,12 @@ import type { OpenAPIPath } from '../type';
import { WalletPath } from './wallet'; import { WalletPath } from './wallet';
import { ApiKeyPath } from './openapi'; import { ApiKeyPath } from './openapi';
import { CustomDomainPath } from './customDomain'; import { CustomDomainPath } from './customDomain';
import { OutLinkPath } from './outLink';
export const SupportPath: OpenAPIPath = { export const SupportPath: OpenAPIPath = {
...UserPath, ...UserPath,
...WalletPath, ...WalletPath,
...ApiKeyPath, ...ApiKeyPath,
...CustomDomainPath ...CustomDomainPath,
...OutLinkPath
}; };
import z from 'zod';
import { PublishChannelEnum } from '../../../support/outLink/constant';
import { ObjectIdSchema } from '../../../common/type/mongo';
// ============= OutLink List =============
export const OutLinkListQuerySchema = z.object({
appId: ObjectIdSchema.describe('应用ID'),
type: z.enum(PublishChannelEnum).describe('发布渠道类型')
});
export type OutLinkListQueryType = z.infer<typeof OutLinkListQuerySchema>;
import type { OpenAPIPath } from '../../type';
import { TagsMap } from '../../tag';
import { OutLinkListQuerySchema } from './api';
export const OutLinkPath: OpenAPIPath = {
'/support/outLink/list': {
get: {
summary: '获取应用的发布渠道列表',
description: '查询指定应用的所有 OutLink 发布渠道配置',
tags: [TagsMap.publishChannel],
requestParams: {
query: OutLinkListQuerySchema
},
responses: {
200: {
description: '成功返回发布渠道列表'
}
}
}
}
};
...@@ -17,17 +17,23 @@ export const TagsMap = { ...@@ -17,17 +17,23 @@ export const TagsMap = {
appPer: '应用权限', appPer: '应用权限',
mcpTools: 'MCP 工具管理', mcpTools: 'MCP 工具管理',
// Chat - home /* ===== Chat ===== */
chatPage: '对话页面通用', chatPage: '对话页面通用',
chatHistory: '历史记录管理', chatHistory: '历史记录管理',
chatController: '对话操作',
chatFeedback: '对话反馈', chatFeedback: '对话反馈',
chatFile: '文件操作',
chatRecord: '对话记录管理',
chatController: '对话操作',
chatSetting: '门户页配置', chatSetting: '门户页配置',
// 辅助功能
chatInputGuide: '对话输入引导', chatInputGuide: '对话输入引导',
// Dataset // Dataset
datasetCollection: '集合', datasetCommon: '知识库管理',
datasetData: '数据', datasetCollection: '集合管理',
datasetCollectionController: '集合操作',
datasetData: '数据管理',
datasetTraining: '训练管理',
// Plugin // Plugin
pluginToolTag: '工具标签', pluginToolTag: '工具标签',
......
import { z } from 'zod'; import { z } from 'zod';
import type { HistoryItemType } from '../../core/chat/type'; import type { HistoryItemType } from '../../core/chat/type';
import type { OutLinkSchema, PlaygroundVisibilityConfigType } from './type'; import type { OutLinkSchemaType, PlaygroundVisibilityConfigType } from './type';
import { PlaygroundVisibilityConfigSchema } from './type'; import { PlaygroundVisibilityConfigSchema } from './type';
export type AuthOutLinkInitProps = { export type AuthOutLinkInitProps = {
...@@ -8,7 +8,7 @@ export type AuthOutLinkInitProps = { ...@@ -8,7 +8,7 @@ export type AuthOutLinkInitProps = {
tokenUrl?: string; tokenUrl?: string;
}; };
export type AuthOutLinkChatProps = { ip?: string | null; outLinkUid: string; question: string }; export type AuthOutLinkChatProps = { ip?: string | null; outLinkUid: string; question: string };
export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSchema }; export type AuthOutLinkLimitProps = AuthOutLinkChatProps & { outLink: OutLinkSchemaType };
export type AuthOutLinkResponse = { export type AuthOutLinkResponse = {
uid: string; uid: string;
}; };
......
...@@ -58,7 +58,7 @@ export type OutlinkAppType = ...@@ -58,7 +58,7 @@ export type OutlinkAppType =
| WechatAppType | WechatAppType
| undefined; | undefined;
export type OutLinkSchema<T extends OutlinkAppType = undefined> = { export type OutLinkSchemaType<T extends OutlinkAppType = undefined> = {
_id: string; _id: string;
shareId: string; shareId: string;
teamId: string; teamId: string;
...@@ -108,16 +108,16 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = { ...@@ -108,16 +108,16 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
export type OutLinkEditType<T extends OutlinkAppType = undefined> = { export type OutLinkEditType<T extends OutlinkAppType = undefined> = {
_id?: string; _id?: string;
name: string; name: string;
showCite?: OutLinkSchema<T>['showCite']; showCite?: OutLinkSchemaType<T>['showCite'];
showRunningStatus?: OutLinkSchema<T>['showRunningStatus']; showRunningStatus?: OutLinkSchemaType<T>['showRunningStatus'];
showSkillReferences?: OutLinkSchema<T>['showSkillReferences']; showSkillReferences?: OutLinkSchemaType<T>['showSkillReferences'];
showFullText?: OutLinkSchema<T>['showFullText']; showFullText?: OutLinkSchemaType<T>['showFullText'];
canDownloadSource?: OutLinkSchema<T>['canDownloadSource']; canDownloadSource?: OutLinkSchemaType<T>['canDownloadSource'];
// response when request // response when request
immediateResponse?: string; immediateResponse?: string;
// response when error or other situation // response when error or other situation
defaultResponse?: string; defaultResponse?: string;
limit?: OutLinkSchema<T>['limit']; limit?: OutLinkSchemaType<T>['limit'];
// config for specific platform // config for specific platform
app?: T; app?: T;
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
OwnerPermissionVal, OwnerPermissionVal,
OwnerRoleVal OwnerRoleVal
} from './constant'; } from './constant';
import z from 'zod';
export type PerConstructPros = { export type PerConstructPros = {
role?: RoleValueType; role?: RoleValueType;
...@@ -135,3 +136,8 @@ export class Permission { ...@@ -135,3 +136,8 @@ export class Permission {
this.updatePermissionCallback?.(); this.updatePermissionCallback?.();
} }
} }
// 仅用于 TypeScript 类型推导,运行时不做实例验证
export const PermissionSchema = z
.custom<Permission>(() => true)
.meta({ description: '权限对象(Permission 类实例)' });
import type { import type {
ApiDatasetDetailResponse,
FeishuServer,
YuqueServer
} from '@fastgpt/global/core/dataset/apiDataset/type';
import type {
DeepRagSearchProps, DeepRagSearchProps,
SearchDatasetDataResponse SearchDatasetDataResponse
} from '../../core/dataset/search/controller'; } from '../../core/dataset/search/controller';
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
type ChatCompletionTool type ChatCompletionTool
} from '@fastgpt/global/core/ai/type'; } from '@fastgpt/global/core/ai/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { WorkerNameEnum, getWorkerController } from '../../../worker/utils'; import { WorkerNameEnum, getWorkerController } from '../../../worker/utils';
import type { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import type { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import { getLogger, LogCategories } from '../../logger'; import { getLogger, LogCategories } from '../../logger';
...@@ -45,7 +45,7 @@ export const countGptMessagesTokens = async ( ...@@ -45,7 +45,7 @@ export const countGptMessagesTokens = async (
} }
}; };
export const countMessagesTokens = (messages: ChatItemType[]) => { export const countMessagesTokens = (messages: ChatItemMiniType[]) => {
const adaptMessages = chats2GPTMessages({ messages, reserveId: true }); const adaptMessages = chats2GPTMessages({ messages, reserveId: true });
return countGptMessagesTokens(adaptMessages); return countGptMessagesTokens(adaptMessages);
......
...@@ -9,6 +9,8 @@ const SERVICE_LOCAL_HOST = ...@@ -9,6 +9,8 @@ const SERVICE_LOCAL_HOST =
: `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`; : `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`;
export const isInternalAddress = async (url: string): Promise<boolean> => { export const isInternalAddress = async (url: string): Promise<boolean> => {
if (isDevEnv) return false;
const isInternalIPv6 = (ip: string): boolean => { const isInternalIPv6 = (ip: string): boolean => {
// 移除 IPv6 地址中的方括号(如果有) // 移除 IPv6 地址中的方括号(如果有)
const cleanIp = ip.replace(/^\[|\]$/g, ''); const cleanIp = ip.replace(/^\[|\]$/g, '');
......
import { replaceVariable } from '@fastgpt/global/common/string/tools'; import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getLLMModel } from '../model'; import { getLLMModel } from '../model';
import { filterGPTMessageByMaxContext } from '../llm/utils'; import { filterGPTMessageByMaxContext } from '../llm/utils';
...@@ -130,7 +130,7 @@ export const queryExtension = async ({ ...@@ -130,7 +130,7 @@ export const queryExtension = async ({
}: { }: {
chatBg?: string; chatBg?: string;
query: string; query: string;
histories: ChatItemType[]; histories: ChatItemMiniType[];
llmModel: string; llmModel: string;
embeddingModel: string; embeddingModel: string;
generateCount?: number; generateCount?: number;
......
import { connectionMongo, getMongoModel } from '../../common/mongo'; import { connectionMongo, getMongoModel } from '../../common/mongo';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import { type ChatItemSchemaType } from '@fastgpt/global/core/chat/type'; import { type ChatItemDBSchemaType } from '@fastgpt/global/core/chat/type';
import { ChatRoleMap } from '@fastgpt/global/core/chat/constants'; import { ChatRoleMap } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { import {
...@@ -103,7 +103,7 @@ ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 }); ...@@ -103,7 +103,7 @@ ChatItemSchema.index({ appId: 1, chatId: 1, _id: -1 });
// Query by role (AI/Human), get latest chat item, permission check // Query by role (AI/Human), get latest chat item, permission check
ChatItemSchema.index({ appId: 1, chatId: 1, obj: 1, _id: -1 }); ChatItemSchema.index({ appId: 1, chatId: 1, obj: 1, _id: -1 });
export const MongoChatItem = getMongoModel<ChatItemSchemaType>( export const MongoChatItem = getMongoModel<ChatItemDBSchemaType>(
ChatItemCollectionName, ChatItemCollectionName,
ChatItemSchema ChatItemSchema
); );
import type { ChatHistoryItemResType, ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatHistoryItemResType, ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema'; import { MongoChatItem } from './chatItemSchema';
import { MongoChat } from './chatSchema'; import { MongoChat } from './chatSchema';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
...@@ -35,7 +35,7 @@ export async function getChatItems({ ...@@ -35,7 +35,7 @@ export async function getChatItems({
prevId?: string; prevId?: string;
nextId?: string; nextId?: string;
}): Promise<{ }): Promise<{
histories: ChatItemType[]; histories: ChatItemMiniType[];
total: number; total: number;
hasMorePrev: boolean; hasMorePrev: boolean;
hasMoreNext: boolean; hasMoreNext: boolean;
......
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { getS3ChatSource } from '../../common/s3/sources/chat'; import { getS3ChatSource } from '../../common/s3/sources/chat';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
...@@ -8,10 +8,10 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; ...@@ -8,10 +8,10 @@ import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { clone, cloneDeep } from 'lodash'; import { clone, cloneDeep } from 'lodash';
export const addPreviewUrlToChatItems = async ( export const addPreviewUrlToChatItems = async (
histories: ChatItemType[], histories: ChatItemMiniType[],
type: 'chatFlow' | 'workflowTool' type: 'chatFlow' | 'workflowTool'
) => { ) => {
async function addToChatflow(item: ChatItemType) { async function addToChatflow(item: ChatItemMiniType) {
for await (const value of item.value) { for await (const value of item.value) {
if ('file' in value && value.file && value.file.key) { if ('file' in value && value.file && value.file.key) {
const { url } = await s3ChatSource.createGetChatFileURL({ const { url } = await s3ChatSource.createGetChatFileURL({
...@@ -23,7 +23,7 @@ export const addPreviewUrlToChatItems = async ( ...@@ -23,7 +23,7 @@ export const addPreviewUrlToChatItems = async (
} }
} }
async function addToWorkflowTool(item: ChatItemType) { async function addToWorkflowTool(item: ChatItemMiniType) {
if (item.obj !== ChatRoleEnum.Human || !Array.isArray(item.value)) return; if (item.obj !== ChatRoleEnum.Human || !Array.isArray(item.value)) return;
for (let j = 0; j < item.value.length; j++) { for (let j = 0; j < item.value.length; j++) {
......
import type { import type {
ApiFileReadContentResponse, ApiFileReadContentResponseType,
APIFileReadResponse, APIFileReadResponseType,
ApiDatasetDetailResponse, ApiDatasetDetailResponse,
APIFileServer APIFileServerType
} from '@fastgpt/global/core/dataset/apiDataset/type'; } from '@fastgpt/global/core/dataset/apiDataset/type';
import { type Method } from 'axios'; import { type Method } from 'axios';
import { createProxyAxios } from '../../../../common/api/axios'; import { createProxyAxios } from '../../../../common/api/axios';
...@@ -29,7 +29,7 @@ type APIFileListResponse = { ...@@ -29,7 +29,7 @@ type APIFileListResponse = {
hasChild?: boolean; hasChild?: boolean;
}; };
export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }) => { export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServerType }) => {
const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET);
const instance = createProxyAxios({ const instance = createProxyAxios({
baseURL: apiServer.baseUrl, baseURL: apiServer.baseUrl,
...@@ -177,7 +177,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } ...@@ -177,7 +177,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
apiFileId: string; apiFileId: string;
customPdfParse?: boolean; customPdfParse?: boolean;
datasetId: string; datasetId: string;
}): Promise<ApiFileReadContentResponse> => { }): Promise<ApiFileReadContentResponseType> => {
const data = await request< const data = await request<
{ {
title?: string; title?: string;
...@@ -239,7 +239,11 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer } ...@@ -239,7 +239,11 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
}; };
const getFilePreviewUrl = async ({ apiFileId }: { apiFileId: string }) => { const getFilePreviewUrl = async ({ apiFileId }: { apiFileId: string }) => {
const { url } = await request<APIFileReadResponse>(`/v1/file/read`, { id: apiFileId }, 'GET'); const { url } = await request<APIFileReadResponseType>(
`/v1/file/read`,
{ id: apiFileId },
'GET'
);
if (!url || typeof url !== 'string') { if (!url || typeof url !== 'string') {
return Promise.reject('Invalid response url'); return Promise.reject('Invalid response url');
......
import type { import type {
APIFileItemType, APIFileItemType,
ApiFileReadContentResponse, ApiFileReadContentResponseType,
ApiDatasetDetailResponse, ApiDatasetDetailResponse,
FeishuServer FeishuServerType
} from '@fastgpt/global/core/dataset/apiDataset/type'; } from '@fastgpt/global/core/dataset/apiDataset/type';
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { type Method } from 'axios'; import { type Method } from 'axios';
...@@ -33,7 +33,7 @@ type FeishuFileListResponse = { ...@@ -33,7 +33,7 @@ type FeishuFileListResponse = {
const feishuBaseUrl = process.env.FEISHU_BASE_URL || 'https://open.feishu.cn'; const feishuBaseUrl = process.env.FEISHU_BASE_URL || 'https://open.feishu.cn';
const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET);
export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: FeishuServer }) => { export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: FeishuServerType }) => {
const instance = createProxyAxios({ const instance = createProxyAxios({
baseURL: feishuBaseUrl, baseURL: feishuBaseUrl,
timeout: 60000 timeout: 60000
...@@ -150,7 +150,7 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu ...@@ -150,7 +150,7 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu
apiFileId apiFileId
}: { }: {
apiFileId: string; apiFileId: string;
}): Promise<ApiFileReadContentResponse> => { }): Promise<ApiFileReadContentResponseType> => {
const [{ content }, { document }] = await Promise.all([ const [{ content }, { document }] = await Promise.all([
request<{ content: string }>( request<{ content: string }>(
`/open-apis/docx/v1/documents/${apiFileId}/raw_content`, `/open-apis/docx/v1/documents/${apiFileId}/raw_content`,
......
import type { import type {
APIFileItemType, APIFileItemType,
ApiFileReadContentResponse, ApiFileReadContentResponseType,
YuqueServer, YuqueServerType,
ApiDatasetDetailResponse ApiDatasetDetailResponse
} from '@fastgpt/global/core/dataset/apiDataset/type'; } from '@fastgpt/global/core/dataset/apiDataset/type';
import { type Method } from 'axios'; import { type Method } from 'axios';
...@@ -42,7 +42,7 @@ type YuqueTocListResponse = { ...@@ -42,7 +42,7 @@ type YuqueTocListResponse = {
const yuqueBaseUrl = process.env.YUQUE_DATASET_BASE_URL || 'https://www.yuque.com'; const yuqueBaseUrl = process.env.YUQUE_DATASET_BASE_URL || 'https://www.yuque.com';
export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServer }) => { export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServerType }) => {
const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET); const logger = getLogger(LogCategories.MODULE.DATASET.API_DATASET);
const instance = createProxyAxios({ const instance = createProxyAxios({
baseURL: yuqueBaseUrl, baseURL: yuqueBaseUrl,
...@@ -202,7 +202,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ ...@@ -202,7 +202,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
apiFileId apiFileId
}: { }: {
apiFileId: string; apiFileId: string;
}): Promise<ApiFileReadContentResponse> => { }): Promise<ApiFileReadContentResponseType> => {
if (typeof apiFileId !== 'string') return Promise.reject('Invalid file id'); if (typeof apiFileId !== 'string') return Promise.reject('Invalid file id');
const [parentId, fileId] = apiFileId.split(/-(.*?)-(.*)/); const [parentId, fileId] = apiFileId.split(/-(.*?)-(.*)/);
......
...@@ -28,7 +28,7 @@ import { MongoDatasetCollectionTags } from '../tag/schema'; ...@@ -28,7 +28,7 @@ import { MongoDatasetCollectionTags } from '../tag/schema';
import { computeFilterIntersection } from './utils'; import { computeFilterIntersection } from './utils';
import { readFromSecondary } from '../../../common/mongo/utils'; import { readFromSecondary } from '../../../common/mongo/utils';
import { MongoDatasetDataText } from '../data/dataTextSchema'; import { MongoDatasetDataText } from '../data/dataTextSchema';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { datasetSearchQueryExtension } from './utils'; import { datasetSearchQueryExtension } from './utils';
import type { RerankModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { RerankModelItemType } from '@fastgpt/global/core/ai/model.schema';
...@@ -41,7 +41,7 @@ import { getLogger, LogCategories } from '../../../common/logger'; ...@@ -41,7 +41,7 @@ import { getLogger, LogCategories } from '../../../common/logger';
const logger = getLogger(LogCategories.MODULE.DATASET.DATA); const logger = getLogger(LogCategories.MODULE.DATASET.DATA);
export type SearchDatasetDataProps = { export type SearchDatasetDataProps = {
histories: ChatItemType[]; histories: ChatItemMiniType[];
teamId: string; teamId: string;
uid?: string; uid?: string;
tmbId?: string; tmbId?: string;
......
import { queryExtension } from '../../ai/functions/queryExtension'; import { queryExtension } from '../../ai/functions/queryExtension';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { hashStr } from '@fastgpt/global/common/string/tools'; import { hashStr } from '@fastgpt/global/common/string/tools';
import { getLogger, LogCategories } from '../../../common/logger'; import { getLogger, LogCategories } from '../../../common/logger';
...@@ -28,7 +28,7 @@ export const datasetSearchQueryExtension = async ({ ...@@ -28,7 +28,7 @@ export const datasetSearchQueryExtension = async ({
llmModel?: string; llmModel?: string;
embeddingModel?: string; embeddingModel?: string;
extensionBg?: string; extensionBg?: string;
histories?: ChatItemType[]; histories?: ChatItemMiniType[];
}) => { }) => {
const filterSamQuery = (queries: string[]) => { const filterSamQuery = (queries: string[]) => {
const set = new Set<string>(); const set = new Set<string>();
......
/* Abandoned */ /* Abandoned */
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { type SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type'; import { type SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
import { runWorkflow } from '../index'; import { runWorkflow } from '../index';
...@@ -21,12 +21,12 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; ...@@ -21,12 +21,12 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number; [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number;
app: SelectAppItemType; app: SelectAppItemType;
}>; }>;
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.answerText]: string;
[NodeOutputKeyEnum.history]: ChatItemType[]; [NodeOutputKeyEnum.history]: ChatItemMiniType[];
}>; }>;
export const dispatchAppRequest = async (props: Props): Promise<Response> => { export const dispatchAppRequest = async (props: Props): Promise<Response> => {
......
...@@ -11,7 +11,7 @@ import { getNodeErrResponse, getHistories } from '../../utils'; ...@@ -11,7 +11,7 @@ import { getNodeErrResponse, getHistories } from '../../utils';
import type { import type {
AIChatItemValueItemType, AIChatItemValueItemType,
ChatHistoryItemResType, ChatHistoryItemResType,
ChatItemType ChatItemMiniType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { import {
...@@ -47,7 +47,7 @@ import { getLogger, LogCategories } from '../../../../../common/logger'; ...@@ -47,7 +47,7 @@ import { getLogger, LogCategories } from '../../../../../common/logger';
import { env } from '../../../../../env'; import { env } from '../../../../../env';
export type DispatchAgentModuleProps = ModuleDispatchProps<{ export type DispatchAgentModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[]; [NodeInputKeyEnum.history]?: ChatItemMiniType[];
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.aiChatVision]?: boolean; [NodeInputKeyEnum.aiChatVision]?: boolean;
......
...@@ -28,7 +28,7 @@ type DatasetSearchParams = { ...@@ -28,7 +28,7 @@ type DatasetSearchParams = {
datasets: SelectedDatasetType[]; datasets: SelectedDatasetType[];
similarity: number; similarity: number;
maxTokens: number; maxTokens: number;
searchMode: `${DatasetSearchModeEnum}`; searchMode: DatasetSearchModeEnum;
embeddingWeight?: number; embeddingWeight?: number;
usingReRank: boolean; usingReRank: boolean;
rerankModel?: string; rerankModel?: string;
......
...@@ -3,7 +3,7 @@ import { SubAppIds } from '@fastgpt/global/core/workflow/node/agent/constants'; ...@@ -3,7 +3,7 @@ import { SubAppIds } from '@fastgpt/global/core/workflow/node/agent/constants';
import { parseUrlToFileType } from '../../../../../utils/context'; import { parseUrlToFileType } from '../../../../../utils/context';
import { getLogger, LogCategories } from '../../../../../../../common/logger'; import { getLogger, LogCategories } from '../../../../../../../common/logger';
import { getHistoryFileLinks } from '../../../../tools/readFiles'; import { getHistoryFileLinks } from '../../../../tools/readFiles';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import z from 'zod'; import z from 'zod';
...@@ -41,7 +41,7 @@ export const formatFileInput = ({ ...@@ -41,7 +41,7 @@ export const formatFileInput = ({
fileUrls?: string[]; fileUrls?: string[];
requestOrigin?: string; requestOrigin?: string;
maxFiles: number; maxFiles: number;
histories: ChatItemType[]; histories: ChatItemMiniType[];
useSkill: boolean; useSkill: boolean;
}): { }): {
filesMap: Record<string, string>; filesMap: Record<string, string>;
......
import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils';
import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
...@@ -44,7 +44,7 @@ import { formatModelChars2Points } from '../../../../support/wallet/usage/utils' ...@@ -44,7 +44,7 @@ import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'
export type ChatProps = ModuleDispatchProps< export type ChatProps = ModuleDispatchProps<
AIChatNodeProps & { AIChatNodeProps & {
[NodeInputKeyEnum.userChatInput]?: string; [NodeInputKeyEnum.userChatInput]?: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number; [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number;
[NodeInputKeyEnum.aiChatDatasetQuote]?: SearchDataResponseItemType[]; [NodeInputKeyEnum.aiChatDatasetQuote]?: SearchDataResponseItemType[];
} }
>; >;
...@@ -52,7 +52,7 @@ export type ChatResponse = DispatchNodeResultType< ...@@ -52,7 +52,7 @@ export type ChatResponse = DispatchNodeResultType<
{ {
[NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.answerText]: string;
[NodeOutputKeyEnum.reasoningText]?: string; [NodeOutputKeyEnum.reasoningText]?: string;
[NodeOutputKeyEnum.history]: ChatItemType[]; [NodeOutputKeyEnum.history]: ChatItemMiniType[];
}, },
{ {
[NodeOutputKeyEnum.errorText]: string; [NodeOutputKeyEnum.errorText]: string;
...@@ -339,7 +339,7 @@ async function getMultiInput({ ...@@ -339,7 +339,7 @@ async function getMultiInput({
usageId, usageId,
runningUserInfo runningUserInfo
}: { }: {
histories: ChatItemType[]; histories: ChatItemMiniType[];
inputFiles: UserChatItemFileItemType[]; inputFiles: UserChatItemFileItemType[];
fileLinks?: string[]; fileLinks?: string[];
stringQuoteText?: string; // file quote stringQuoteText?: string; // file quote
...@@ -421,7 +421,7 @@ async function getChatMessages({ ...@@ -421,7 +421,7 @@ async function getChatMessages({
version?: string; version?: string;
useDatasetQuote: boolean; useDatasetQuote: boolean;
histories: ChatItemType[]; histories: ChatItemMiniType[];
systemPrompt: string; systemPrompt: string;
userChatInput: string; userChatInput: string;
...@@ -465,7 +465,7 @@ async function getChatMessages({ ...@@ -465,7 +465,7 @@ async function getChatMessages({
.filter(Boolean) .filter(Boolean)
.join('\n\n===---===---===\n\n'); .join('\n\n===---===---===\n\n');
const messages: ChatItemType[] = [ const messages: ChatItemMiniType[] = [
...getSystemPrompt_ChatItemType(concatenateSystemPrompt), ...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
...histories, ...histories,
{ {
......
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type'; import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
...@@ -21,7 +21,7 @@ const logger = getLogger(LogCategories.MODULE.WORKFLOW.AI); ...@@ -21,7 +21,7 @@ const logger = getLogger(LogCategories.MODULE.WORKFLOW.AI);
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.aiModel]: string; [NodeInputKeyEnum.aiModel]: string;
[NodeInputKeyEnum.aiSystemPrompt]?: string; [NodeInputKeyEnum.aiSystemPrompt]?: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number; [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number;
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.agents]: ClassifyQuestionAgentItemType[]; [NodeInputKeyEnum.agents]: ClassifyQuestionAgentItemType[];
}>; }>;
...@@ -110,7 +110,7 @@ const completions = async ({ ...@@ -110,7 +110,7 @@ const completions = async ({
lastMemory, lastMemory,
params: { agents, systemPrompt = '', userChatInput } params: { agents, systemPrompt = '', userChatInput }
}: ActionProps) => { }: ActionProps) => {
const messages: ChatItemType[] = [ const messages: ChatItemMiniType[] = [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: [ value: [
......
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils'; import { filterGPTMessageByMaxContext } from '../../../ai/llm/utils';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type'; import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
...@@ -34,7 +34,7 @@ import { createLLMResponse } from '../../../ai/llm/request'; ...@@ -34,7 +34,7 @@ import { createLLMResponse } from '../../../ai/llm/request';
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema'; import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[]; [NodeInputKeyEnum.history]?: ChatItemMiniType[];
[NodeInputKeyEnum.contextExtractInput]: string; [NodeInputKeyEnum.contextExtractInput]: string;
[NodeInputKeyEnum.extractKeys]: ContextExtractAgentItemType[]; [NodeInputKeyEnum.extractKeys]: ContextExtractAgentItemType[];
[NodeInputKeyEnum.description]: string; [NodeInputKeyEnum.description]: string;
...@@ -188,7 +188,7 @@ const toolChoice = async (props: ActionProps) => { ...@@ -188,7 +188,7 @@ const toolChoice = async (props: ActionProps) => {
lastMemory lastMemory
} = props; } = props;
const messages: ChatItemType[] = [ const messages: ChatItemMiniType[] = [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: [ value: [
...@@ -293,7 +293,7 @@ const completions = async (props: ActionProps) => { ...@@ -293,7 +293,7 @@ const completions = async (props: ActionProps) => {
params: { content, description } params: { content, description }
} = props; } = props;
const messages: ChatItemType[] = [ const messages: ChatItemMiniType[] = [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: [ value: [
......
...@@ -11,7 +11,7 @@ import { runToolCall } from './toolCall'; ...@@ -11,7 +11,7 @@ import { runToolCall } from './toolCall';
import { type DispatchToolModuleProps, type ToolNodeItemType } from './type'; import { type DispatchToolModuleProps, type ToolNodeItemType } from './type';
import type { import type {
UserChatItemFileItemType, UserChatItemFileItemType,
ChatItemType, ChatItemMiniType,
UserChatItemValueItemType UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
...@@ -152,8 +152,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< ...@@ -152,8 +152,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
.filter(Boolean) .filter(Boolean)
.join('\n\n===---===---===\n\n'); .join('\n\n===---===---===\n\n');
const messages: ChatItemType[] = (() => { const messages: ChatItemMiniType[] = (() => {
const value: ChatItemType[] = [ const value: ChatItemMiniType[] = [
...getSystemPrompt_ChatItemType(concatenateSystemPrompt), ...getSystemPrompt_ChatItemType(concatenateSystemPrompt),
// Add file input prompt to histories // Add file input prompt to histories
...chatHistories.map((item) => { ...chatHistories.map((item) => {
...@@ -319,7 +319,7 @@ const getMultiInput = async ({ ...@@ -319,7 +319,7 @@ const getMultiInput = async ({
uId uId
}: { }: {
runningUserInfo: ChatDispatchProps['runningUserInfo']; runningUserInfo: ChatDispatchProps['runningUserInfo'];
histories: ChatItemType[]; histories: ChatItemMiniType[];
fileLinks?: string[]; fileLinks?: string[];
requestOrigin?: string; requestOrigin?: string;
maxFiles: number; maxFiles: number;
......
...@@ -6,13 +6,13 @@ import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; ...@@ -6,13 +6,13 @@ import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import type { DispatchFlowResponse } from '../../type'; import type { DispatchFlowResponse } from '../../type';
import type { AIChatItemValueItemType, ChatItemType } from '@fastgpt/global/core/chat/type'; import type { AIChatItemValueItemType, ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema';
import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema';
export type DispatchToolModuleProps = ModuleDispatchProps<{ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[]; [NodeInputKeyEnum.history]?: ChatItemMiniType[];
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.fileUrlList]?: string[]; [NodeInputKeyEnum.fileUrlList]?: string[];
......
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import { runWorkflow } from '../index'; import { runWorkflow } from '../index';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
...@@ -25,14 +25,14 @@ import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils'; ...@@ -25,14 +25,14 @@ import { getRunningUserInfoByTmbId } from '../../../../support/user/team/utils';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number; [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number;
[NodeInputKeyEnum.fileUrlList]?: string[]; [NodeInputKeyEnum.fileUrlList]?: string[];
[NodeInputKeyEnum.forbidStream]?: boolean; [NodeInputKeyEnum.forbidStream]?: boolean;
[NodeInputKeyEnum.fileUrlList]?: string[]; [NodeInputKeyEnum.fileUrlList]?: string[];
}>; }>;
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
[NodeOutputKeyEnum.answerText]: string; [NodeOutputKeyEnum.answerText]: string;
[NodeOutputKeyEnum.history]: ChatItemType[]; [NodeOutputKeyEnum.history]: ChatItemMiniType[];
}>; }>;
export const dispatchRunAppNode = async (props: Props): Promise<Response> => { export const dispatchRunAppNode = async (props: Props): Promise<Response> => {
......
...@@ -23,7 +23,7 @@ type DatasetSearchProps = ModuleDispatchProps<{ ...@@ -23,7 +23,7 @@ type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSimilarity]: number; [NodeInputKeyEnum.datasetSimilarity]: number;
[NodeInputKeyEnum.datasetMaxTokens]: number; [NodeInputKeyEnum.datasetMaxTokens]: number;
[NodeInputKeyEnum.userChatInput]?: string; [NodeInputKeyEnum.userChatInput]?: string;
[NodeInputKeyEnum.datasetSearchMode]: `${DatasetSearchModeEnum}`; [NodeInputKeyEnum.datasetSearchMode]: DatasetSearchModeEnum;
[NodeInputKeyEnum.datasetSearchEmbeddingWeight]?: number; [NodeInputKeyEnum.datasetSearchEmbeddingWeight]?: number;
[NodeInputKeyEnum.datasetSearchUsingReRank]: boolean; [NodeInputKeyEnum.datasetSearchUsingReRank]: boolean;
......
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type';
import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
...@@ -13,7 +13,7 @@ import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runti ...@@ -13,7 +13,7 @@ import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runti
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.aiModel]: string; [NodeInputKeyEnum.aiModel]: string;
[NodeInputKeyEnum.aiSystemPrompt]?: string; [NodeInputKeyEnum.aiSystemPrompt]?: string;
[NodeInputKeyEnum.history]?: ChatItemType[] | number; [NodeInputKeyEnum.history]?: ChatItemMiniType[] | number;
[NodeInputKeyEnum.userChatInput]: string; [NodeInputKeyEnum.userChatInput]: string;
}>; }>;
type Response = DispatchNodeResultType<{ type Response = DispatchNodeResultType<{
......
...@@ -10,7 +10,7 @@ import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; ...@@ -10,7 +10,7 @@ import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { parseUrlToFileType } from '../../utils/context'; import { parseUrlToFileType } from '../../utils/context';
import { readFileContentByBuffer } from '../../../../common/file/read/utils'; import { readFileContentByBuffer } from '../../../../common/file/read/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { addDays } from 'date-fns'; import { addDays } from 'date-fns';
import { getNodeErrResponse } from '../utils'; import { getNodeErrResponse } from '../utils';
import { isInternalAddress, PRIVATE_URL_TEXT } from '../../../../common/system/utils'; import { isInternalAddress, PRIVATE_URL_TEXT } from '../../../../common/system/utils';
...@@ -105,7 +105,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => { ...@@ -105,7 +105,7 @@ export const dispatchReadFiles = async (props: Props): Promise<Response> => {
} }
}; };
export const getHistoryFileLinks = (histories: ChatItemType[]) => { export const getHistoryFileLinks = (histories: ChatItemMiniType[]) => {
return histories return histories
.filter((item) => { .filter((item) => {
if (item.obj === ChatRoleEnum.Human) { if (item.obj === ChatRoleEnum.Human) {
......
import path from 'path'; import path from 'path';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ChatItemType } from '@fastgpt/global/core/chat/type'; import type { ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import type { VariableItemType } from '@fastgpt/global/core/app/type'; import type { VariableItemType } from '@fastgpt/global/core/app/type';
import { encryptSecret } from '../../../common/secret/aes256gcm'; import { encryptSecret } from '../../../common/secret/aes256gcm';
...@@ -228,7 +228,10 @@ export const filterToolNodeIdByEdges = ({ ...@@ -228,7 +228,10 @@ export const filterToolNodeIdByEdges = ({
.map((edge) => edge.target); .map((edge) => edge.target);
}; };
export const getHistories = (history?: ChatItemType[] | number, histories: ChatItemType[] = []) => { export const getHistories = (
history?: ChatItemMiniType[] | number,
histories: ChatItemMiniType[] = []
) => {
if (!history) return []; if (!history) return [];
// Select reference history // Select reference history
if (Array.isArray(history)) return history; if (Array.isArray(history)) return history;
......
...@@ -57,6 +57,9 @@ export const env = createEnv({ ...@@ -57,6 +57,9 @@ export const env = createEnv({
APP_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000), APP_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000),
DATASET_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000), DATASET_FOLDER_MAX_AMOUNT: z.coerce.number().int().positive().default(1000),
// ===== Security =====
CHECK_INTERNAL_IP: BoolSchema.default(false).meta({ description: '是否启用内网 IP 检查' }),
// Beta features // Beta features
// Whether the Skill feature is enabled (frontend entries + backend runtime) // Whether the Skill feature is enabled (frontend entries + backend runtime)
SHOW_SKILL: BoolSchema.default(false) SHOW_SKILL: BoolSchema.default(false)
......
...@@ -6,7 +6,7 @@ import type { ...@@ -6,7 +6,7 @@ import type {
} from '@fastgpt/global/support/outLink/api'; } from '@fastgpt/global/support/outLink/api';
import { axios } from '../../../common/api/axios'; import { axios } from '../../../common/api/axios';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import type { OutLinkSchema } from '@fastgpt/global/support/outLink/type'; import type { OutLinkSchemaType } from '@fastgpt/global/support/outLink/type';
import { addMinutes } from 'date-fns'; import { addMinutes } from 'date-fns';
import { S3_KEY_PATH_INVALID_CHARS } from '../../../common/s3/constants'; import { S3_KEY_PATH_INVALID_CHARS } from '../../../common/s3/constants';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
...@@ -49,7 +49,7 @@ export const authOutLinkInit = async ({ ...@@ -49,7 +49,7 @@ export const authOutLinkInit = async ({
return { uid }; return { uid };
}; };
const authIpLimit = async ({ ip, outLink }: { ip: string; outLink: OutLinkSchema }) => { const authIpLimit = async ({ ip, outLink }: { ip: string; outLink: OutLinkSchemaType }) => {
if (!outLink.limit || !outLink.limit.QPM) { if (!outLink.limit || !outLink.limit.QPM) {
return; return;
} }
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
storeEdges2RuntimeEdges, storeEdges2RuntimeEdges,
storeNodes2RuntimeNodes storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import type { OutlinkAppType, OutLinkSchema } from '@fastgpt/global/support/outLink/type'; import type { OutlinkAppType, OutLinkSchemaType } from '@fastgpt/global/support/outLink/type';
import { getAppLatestVersion } from '../../../core/app/version/controller'; import { getAppLatestVersion } from '../../../core/app/version/controller';
import { MongoApp } from '../../../core/app/schema'; import { MongoApp } from '../../../core/app/schema';
import { getChatItems } from '../../../core/chat/controller'; import { getChatItems } from '../../../core/chat/controller';
...@@ -69,7 +69,7 @@ export const resetChat = ({ appId, chatId }: { appId: string; chatId: string }) ...@@ -69,7 +69,7 @@ export const resetChat = ({ appId, chatId }: { appId: string; chatId: string })
}; };
export type outLinkInvokeChatProps<T extends OutlinkAppType> = { export type outLinkInvokeChatProps<T extends OutlinkAppType> = {
outLinkConfig: OutLinkSchema<T>; outLinkConfig: OutLinkSchemaType<T>;
chatId: string; // specific chat chatId: string; // specific chat
query: UserChatItemValueItemType[]; query: UserChatItemValueItemType[];
res?: NextApiResponse; res?: NextApiResponse;
......
import { connectionMongo, getMongoModel } from '../../common/mongo'; import { connectionMongo, getMongoModel } from '../../common/mongo';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
import { type OutLinkSchema as SchemaType } from '@fastgpt/global/support/outLink/type'; import { type OutLinkSchemaType } from '@fastgpt/global/support/outLink/type';
import { import {
TeamCollectionName, TeamCollectionName,
TeamMemberCollectionName TeamMemberCollectionName
...@@ -111,16 +111,12 @@ OutLinkSchema.virtual('associatedApp', { ...@@ -111,16 +111,12 @@ OutLinkSchema.virtual('associatedApp', {
const logger = getLogger(LogCategories.INFRA.MONGO); const logger = getLogger(LogCategories.INFRA.MONGO);
try { OutLinkSchema.index({ shareId: -1 });
OutLinkSchema.index({ shareId: -1 }); OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 });
OutLinkSchema.index({ teamId: 1, tmbId: 1, appId: 1 }); // Wechat polling recovery: find online channels on startup
// Wechat polling recovery: find online channels on startup OutLinkSchema.index(
OutLinkSchema.index(
{ type: 1, 'app.status': 1 }, { type: 1, 'app.status': 1 },
{ partialFilterExpression: { type: 'wechat', 'app.status': 'online' } } { partialFilterExpression: { type: 'wechat', 'app.status': 'online' } }
); );
} catch (error) {
logger.error('Failed to build outlink indexes', { error });
}
export const MongoOutLink = getMongoModel<SchemaType>('outlinks', OutLinkSchema); export const MongoOutLink = getMongoModel<OutLinkSchemaType>('outlinks', OutLinkSchema);
...@@ -2,7 +2,7 @@ import { getWorker, getQueue, QueueNames, type Job } from '../../../common/bullm ...@@ -2,7 +2,7 @@ import { getWorker, getQueue, QueueNames, type Job } from '../../../common/bullm
import { getLogger, LogCategories } from '../../../common/logger'; import { getLogger, LogCategories } from '../../../common/logger';
import { ILinkClient } from './ilinkClient'; import { ILinkClient } from './ilinkClient';
import type { WechatPollJobData } from './type'; import type { WechatPollJobData } from './type';
import type { OutLinkSchema, WechatAppType } from '@fastgpt/global/support/outLink/type'; import type { OutLinkSchemaType, WechatAppType } from '@fastgpt/global/support/outLink/type';
import { MongoOutLink } from '../../../support/outLink/schema'; import { MongoOutLink } from '../../../support/outLink/schema';
import { outlinkInvokeChat } from '../../../support/outLink/runtime/utils'; import { outlinkInvokeChat } from '../../../support/outLink/runtime/utils';
import { setRedisCache, getRedisCache } from '../../../common/redis/cache'; import { setRedisCache, getRedisCache } from '../../../common/redis/cache';
...@@ -23,7 +23,7 @@ async function processWechatPollJob(job: Job<WechatPollJobData>): Promise<void> ...@@ -23,7 +23,7 @@ async function processWechatPollJob(job: Job<WechatPollJobData>): Promise<void>
// 1. 获取渠道配置 // 1. 获取渠道配置
const outLink = (await MongoOutLink.findOne({ const outLink = (await MongoOutLink.findOne({
shareId shareId
}).lean()) as unknown as OutLinkSchema<WechatAppType>; }).lean()) as unknown as OutLinkSchemaType<WechatAppType>;
if (!outLink || !outLink.app) { if (!outLink || !outLink.app) {
logger.warn('OutLink not found, stop polling', { shareId }); logger.warn('OutLink not found, stop polling', { shareId });
return; return;
...@@ -111,7 +111,7 @@ async function processWechatPollJob(job: Job<WechatPollJobData>): Promise<void> ...@@ -111,7 +111,7 @@ async function processWechatPollJob(job: Job<WechatPollJobData>): Promise<void>
/* ============ 处理单个用户分组 ============ */ /* ============ 处理单个用户分组 ============ */
async function processUserGroup( async function processUserGroup(
outLink: OutLinkSchema<WechatAppType>, outLink: OutLinkSchemaType<WechatAppType>,
group: ParsedMessageGroup group: ParsedMessageGroup
): Promise<void> { ): Promise<void> {
const app = outLink.app; const app = outLink.app;
......
import { type AppDetailType } from '@fastgpt/global/core/app/type'; import { type AppDetailType } from '@fastgpt/global/core/app/type';
import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; import { type OutlinkAppType, type OutLinkSchemaType } from '@fastgpt/global/support/outLink/type';
import { MongoOutLink } from '../../outLink/schema'; import { MongoOutLink } from '../../outLink/schema';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
...@@ -17,7 +17,7 @@ export async function authOutLinkCrud({ ...@@ -17,7 +17,7 @@ export async function authOutLinkCrud({
}): Promise< }): Promise<
AuthResponseType & { AuthResponseType & {
app: AppDetailType; app: AppDetailType;
outLink: OutLinkSchema; outLink: OutLinkSchemaType;
} }
> { > {
const result = await parseHeaderCert(props); const result = await parseHeaderCert(props);
...@@ -62,7 +62,7 @@ export async function authOutLinkValid<T extends OutlinkAppType = any>({ ...@@ -62,7 +62,7 @@ export async function authOutLinkValid<T extends OutlinkAppType = any>({
if (!shareId) { if (!shareId) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid); return Promise.reject(OutLinkErrEnum.linkUnInvalid);
} }
const outLinkConfig = await MongoOutLink.findOne({ shareId }).lean<OutLinkSchema<T>>(); const outLinkConfig = await MongoOutLink.findOne({ shareId }).lean<OutLinkSchemaType<T>>();
if (!outLinkConfig) { if (!outLinkConfig) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid); return Promise.reject(OutLinkErrEnum.linkUnInvalid);
......
import type { PaginationProps, PaginationResponseType } from '@fastgpt/global/openapi/api';
export type { PaginationProps, PaginationResponseType as PaginationResponse };
export type LinkedPaginationProps<T = {}, A = any> = T & {
pageSize: number;
anchor?: A;
initialId?: string;
nextId?: string;
prevId?: string;
};
export type LinkedListResponse<T = {}, A = any> = {
list: Array<T & { id: string; anchor?: A }>;
hasMorePrev: boolean;
hasMoreNext: boolean;
};
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { type LinkedListResponse, type LinkedPaginationProps } from '../common/fetch/type'; import { type LinkedListResponse, type LinkedPaginationProps } from '@fastgpt/global/openapi/api';
import { Box, type BoxProps } from '@chakra-ui/react'; import { Box, type BoxProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useScroll, useMemoizedFn, useDebounceEffect, useLatest } from 'ahooks'; import { useScroll, useMemoizedFn, useDebounceEffect, useLatest } from 'ahooks';
......
...@@ -23,7 +23,7 @@ import { ...@@ -23,7 +23,7 @@ import {
useThrottleEffect useThrottleEffect
} from 'ahooks'; } from 'ahooks';
import { type PaginationProps, type PaginationResponse } from '../common/fetch/type'; import { type PaginationProps, type PaginationResponse } from '@fastgpt/global/openapi/api';
import MyMenu from '../components/common/MyMenu'; import MyMenu from '../components/common/MyMenu';
import { useSystem } from './useSystem'; import { useSystem } from './useSystem';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
......
...@@ -18,8 +18,7 @@ import { ...@@ -18,8 +18,7 @@ import {
defaultWhisperConfig defaultWhisperConfig
} from '@fastgpt/global/core/app/constants'; } from '@fastgpt/global/core/app/constants';
import { createContext, useContextSelector } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { getChatResData } from '@/web/core/chat/record/api';
import { getChatResData } from '@/web/core/chat/api';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { useCreation } from 'ahooks'; import { useCreation } from 'ahooks';
......
...@@ -8,7 +8,7 @@ import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext'; ...@@ -8,7 +8,7 @@ import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { getQuoteDataList } from '@/web/core/chat/api'; import { getQuoteDataList } from '@/web/core/chat/record/api';
const QuoteList = React.memo(function QuoteList({ const QuoteList = React.memo(function QuoteList({
chatItemDataId = '', chatItemDataId = '',
......
import { type ExportChatType } from '@/types/chat'; import { type ExportChatType } from '@/types/chat';
import { type ChatItemType } from '@fastgpt/global/core/chat/type'; import { type ChatItemMiniType } from '@fastgpt/global/core/chat/type';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { htmlTemplate } from '@/web/core/chat/constants'; import { htmlTemplate } from '@/web/core/chat/constants';
import { fileDownload } from '@/web/common/file/utils'; import { fileDownload } from '@/web/common/file/utils';
...@@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next'; ...@@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next';
export const useChatBox = () => { export const useChatBox = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const onExportChat = useCallback( const onExportChat = useCallback(
({ type, history }: { type: ExportChatType; history: ChatItemType[] }) => { ({ type, history }: { type: ExportChatType; history: ChatItemMiniType[] }) => {
const getHistoryHtml = () => { const getHistoryHtml = () => {
const historyDom = document.getElementById('history'); const historyDom = document.getElementById('history');
if (!historyDom) return; if (!historyDom) return;
......
...@@ -14,7 +14,7 @@ import type { ...@@ -14,7 +14,7 @@ import type {
import type { ChatSiteItemType } from './type'; import type { ChatSiteItemType } from './type';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Button, Checkbox, Flex } from '@chakra-ui/react'; import { Box, Checkbox, Flex } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus'; import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
...@@ -26,7 +26,7 @@ import { ...@@ -26,7 +26,7 @@ import {
updateChatUserFeedback, updateChatUserFeedback,
updateFeedbackReadStatus updateFeedbackReadStatus
} from '@/web/core/chat/feedback/api'; } from '@/web/core/chat/feedback/api';
import { delChatRecordById } from '@/web/core/chat/api'; import { delChatRecordById } from '@/web/core/chat/record/api';
import type { AdminMarkType } from './components/SelectMarkCollection'; import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api'; import { postQuestionGuide } from '@/web/core/ai/api';
......
import { DatasetCollectionTypeEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetCollectionTypeEnum, DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import type { PaginationProps } from '@fastgpt/web/common/fetch/type'; import type { PaginationProps } from '@fastgpt/global/openapi/api';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
/* ===== dataset ===== */ /* ===== dataset ===== */
......
import type { AppChatConfigType, AppTTSConfigType } from '@fastgpt/global/core/app/type';
import type { AdminFbkType } from '@fastgpt/global/core/chat/type';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import type { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { GetChatTypeEnum } from '@/global/core/chat/constants';
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
export type GetChatSpeechProps = OutLinkChatAuthProps & {
appId: string;
ttsConfig: AppTTSConfigType;
input: string;
shareId?: string;
};
/* ---------- chat ----------- */ /* ---------- chat ----------- */
export type GetChatRecordsProps = OutLinkChatAuthProps & {
appId: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
type?: `${GetChatTypeEnum}`;
includeDeleted?: boolean;
};
export type InitOutLinkChatProps = {
chatId?: string;
shareId: string;
outLinkUid: string;
};
export type InitTeamChatProps = { export type InitTeamChatProps = {
teamId: string; teamId: string;
appId: string; appId: string;
chatId?: string; chatId?: string;
teamToken: string; teamToken: string;
}; };
export type InitChatResponse = {
chatId?: string;
appId: string;
userAvatar?: string;
title?: string;
variables?: Record<string, any>;
app: {
chatConfig?: AppChatConfigType;
chatModels?: string[];
name: string;
avatar: string;
intro: string;
canUse?: boolean;
type: `${AppTypeEnum}`;
pluginInputs: FlowNodeInputItemType[];
};
};
/* -------- chat item ---------- */
export type DeleteChatItemProps = OutLinkChatAuthProps & {
appId: string;
chatId: string;
contentId?: string;
delFile?: boolean;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment