sync update from LAN repo (source dhk-1 @ 3b1ec5b4)

This commit is contained in:
gozeros committed 2026-11-05 10:56:34 +08:00
1 parent da6521b6cb
commit 264e2200d6
995 files changed
+148105 -13399

No files matched your search

@@ -1,5 +1,36 @@
# 登录鉴权扩展接口文档(auth_login)
## 账号、注册与单点总开关(2026-10-05)
管理员设置 `GET/PUT /api/system-settings/authentication`,请求和响应是完整对象:
```json
{"password_login_enabled":false,"registration_enabled":false,"sso_enabled":true,"single_session_enabled":false}
```
密码登录页面、自助注册与同账号单处登录默认关闭,外部系统免登默认开启,已有管理员保存的设置不变。至少保留一种登录方式;自助注册要求密码登录开启。无个人密码的存量用户使用原共享口令,未配置则使用 `123ewq`,因此关闭外部系统免登无需先给管理员设个人密码。未登录页面通过 `GET /api/v1/auth/options` 读取同四字段和 `needs_setup`(当前没有管理员),响应禁止缓存。这里只公开开关,不公开共享密码、个人密码或上游密钥。
| 接口 | 请求 | 成功响应 |
| --- | --- | --- |
| `POST /api/v1/auth/login/local` | form-urlencoded `username`、`password`;用户名或邮箱 | `UsernameLoginResponse` + HttpOnly Cookie |
| `POST /api/v1/auth/register` | JSON `{username,password}`;兼容 `{email,password}`,两种账号字段只传一项 | `201` + `UsernameLoginResponse`,自动登录普通用户 |
| `POST /api/v1/auth/initialize` | JSON `{email,password}`;只允许系统没有管理员时 | `201` + `UsernameLoginResponse`,创建首任管理员 |
| `GET /api/v1/auth/me` | 当前会话 | `id,email,system_role,needs_setup,has_password,requires_current_password,uses_default_password` |
| `POST /api/v1/auth/change-password` | JSON `{current_password,new_password}` | `message,access_token,token_type,expires_in` + 更新 Cookie |
| `PUT /api/v1/auth/users/{id}/login-password` | 管理员 JSON `{password}`;也可用 `/reset-password` 别名 | 用户列表项;重置自己时附 `access_token,token_type,expires_in` |
`UsernameLoginResponse` 为 `access_token,token_type,expires_in,user_id,email,system_role,needs_setup,created,username`。前端收到改密/本人重置成功响应后必须同步替换本地 Bearer,否则会被旧版本 token 拒绝。新增 `auth_usernames` 表用数据库唯一键保护并发新建账号的用户名;新旧域名注册同名也只能成功一次。ORM 自动创建该表,历史用户在新建前按邮箱 local-part 检查。用户名单点在初次查询后发生并发注册/改密时,会再次校验最终账号凭据后才签发会话。
新密码为 8–255 字符,拒绝常见弱密码和首尾空格;所有用户都必须提供 `current_password`,无个人凭据时用原共享口令(未配置则 `123ewq`)首次设置,不能留空。用户名接受字母、数字、`.`、`_`、`-`,首尾须为字母或数字,也接受有效邮箱。用户名不区分大小写;已存在的相同邮箱 local-part 不得通过换域名重复注册或覆盖。重复返回 `409`,校验失败 `422`,关闭方式或越权返回 `403`,错误密码 `401`(改密码的错误当前密码为 `400`),登录限速 `429`。新注册沿用现有黑名单语义默认放行;已有封禁账号仍被拦截。
新密码与「登录口令」统一在 `password_hash`、`login_password` 保存同一版本化散列;两种登录入口接受同一密码。历史明文个人口令优先于旧 `password_hash`,可直接用于密码登录、URL 口令登录或验证改密,修改后即改存散列。管理员列表仅对历史明文保留旧兼容,任何散列均不返回,新增 `has_login_password` 表示是否已设置。管理员设置/重置不回显密码;已有密码不得清空。复制新登录链接时使用本次输入,之后需重新输入或重置。修改/重置原子递增 `token_version`,此前全部会话与旧密码失效;并发过期修改返回 `409`,普通审批不能覆写凭据或复活旧会话。
`sso_enabled=false` 后,用户名直登、上游 token、OAuth 及 `/api/parallel-agents/auth/token` 都拒绝登录。并行面板兼容入口现复用标准用户名登录的密码、限速和封禁校验,可附 `?password=`。关闭自助注册不影响已启用的单点对外部身份自动建普通用户。用户名含 `admin` 不再授予或提升管理员权限;历史已经是管理员的账号不变。首任管理员初始化使用独立单例 claim 与用户插入的同一数据库事务,跨工作进程并发也最多成功一次。部署无需人工增加列,ORM 启动自动创建 `auth_initialization` 表;管理员恢复仍使用 `python -m app.gateway.auth.reset_admin`,同样同步两套凭据。
启用 `password_login_enabled` 后,用户名/并行面板登录也必须验证个人密码或原共享口令(未配置 `auth_login.username_login.password` 时为 `123ewq`);个人凭据存在时共享口令不能替代。JWT 的 `auth_method=password/passphrase/sso` 仅由实际完成校验的登录路径显式写入。Gateway 与 LangGraph 同时拒绝缺少标记的历史/免密会话,要求重新登录;已验证的上游 SSO 仍受独立 `sso_enabled` 控制。开发环境 `DEER_FLOW_AUTH_DISABLED` 也不能绕过已开启的密码登录或单处登录限制。关闭密码登录时保留旧用户名单点及共享口令门行为;`create_access_token` 不会自动给未经校验的调用添加可信标记。
「单点登录(同账号仅一处在线)」由独立 `single_session_enabled` 控制,默认关闭;`sso_enabled` 仅表示既有外部单点/免登入口,两个功能分开。开启单处登录后,每次成功的账号密码、用户名口令、上游 token、并行面板、注册或初始化登录,都在数据库内原子递增该账号 `token_version` 后签新 JWT,旧会话立即返回 401,前端轮询 `/me` 后退出旧页面。失败登录不会踢人,关闭时允许多处会话并存;修改/重置密码仍始终撤销旧会话。并发登录通过同一行的原子递增保证最终仅最高版本会话有效;事务还比较已验证的密码/口令与权限快照,避免并发重置密码后旧凭据借新登录版本恢复。
在原有登录之上新增两种自定义登录模式,均由 `config.yaml` 的 `auth_login` 段控制。
## 配置
@@ -33,7 +64,7 @@ auth_login:
## 功能一:用户名直登 + 口令门
在原 `POST /api/v1/auth/login/username` 上叠加一个共享口令校验。`require_password=false`(默认)时行为完全不变(免密直登 + 自动注册)。
此前提为系统设置的 `sso_enabled=true`。账号已设置个人密码/口令时,始终校验该凭据;未设置个人凭据时,开启账号密码登录或 `require_password` 任一开关都会要求共享口令(未配置则为 `123ewq`)。
### 请求
@@ -44,8 +75,8 @@ Content-Type: application/json
{ "username": "alice" }
```
- `?password=`:仅当 `require_password=true` 时需要。值需等于 `username_login.password`;该项留空时默认口令为 `123ewq`。
- 口令门是**单一共享口令**,只决定"放不放行",不影响登录成的是哪个账号。
- `?password=`:账号已有个人密码/口令时必须匹配个人凭据;否则仅当 `require_password=true` 时需要匹配共享口令。
- 共享口令仅适用于没有个人凭据的账号,不影响目标账号选择。
- 校验失败按 IP 计入登录限速(5 次/5 分钟锁定),与本地密码登录共用同一限速器。
### 响应
@@ -4,29 +4,25 @@
## 前置配置
在「业务链条管理」页面新增的 **任务分析**(业务码 `b5-1`)行中选择要使用的单智能体。没有配置智能体时,深链接不会进入问答页。
在「业务链条管理」页面新增的 **任务分析**(业务码 `b5-1`)行中选择要使用的默认单智能体。深链接也可以用 `?agentId=` 直接指定本次问答使用的智能体(优先于配置行);两者都没有时,深链接不会进入问答页。
服务端为接入写入端配置环境变量:
```text
DEERFLOW_TASK_DEEPLINK_INGEST_KEY=<随机且足够长的共享密钥>
```
该密钥只用于下面的 `PUT`;浏览器读取详情仍需正常登录态。
服务端无需为接入配置任何密钥。
## 写入或更新任务详情
```http
PUT /api/task-deeplink-details/b5-1/{id}
Content-Type: application/json
X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
```
该接口为**开放写入**:不需要登录态,也不需要任何鉴权头,接入方服务端直接调用即可。写入者仅作审计记录。
```json
{
"taskName": "重点活动舆情研判",
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
"overview": "活动前后七天的综合研判任务",
"type": "yuqing",
"extra": {
"sourceSystem": "B5",
"priority": "high",
@@ -38,8 +34,9 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
}
```
- `id` 与深链接中的 `taskId` 必须完全一致;存储键为 `(type, id)`,此处 `type` 固定为 `b5-1`。
- `taskName`、`taskContent` 必填;`overview`、`extra` 可选。
- `id` 与深链接中的 `taskId` 必须完全一致;存储键为 `(type, id, 业务类型)`,此处路径 `type` 固定为 `b5-1`。
- `taskName`、`taskContent` 必填;`overview`、`type`、`extra` 可选。
- `type` 是业务类型(≤64 字符),与深链接 `?type=` 对应:**同一个 id 可以存多个不同类型的详情快照**,各自独立版本号;不传 `type` 时写入无类型分区(与深链不传 `?type=` 的读取口径一致)。
- `extra` 必须是 JSON 对象,最大 64 KiB;用于接入方的扩展字段。
- 这是幂等接口,接入方不需要预先查询「是否已存」。每次直接 `PUT` 完整快照即可:
- 首次写入:创建记录;
@@ -53,6 +50,7 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
"data": {
"type": "b5-1",
"id": "task-20261020-001",
"taskType": "yuqing",
"taskName": "重点活动舆情研判",
"taskContent": "围绕活动期间的舆情风险、重点对象和处置建议开展分析。",
"overview": "活动前后七天的综合研判任务",
@@ -68,7 +66,7 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
}
```
`created` 表示本次是否首次创建;`changed` 表示本次是否实际写入。对同样内容的重试,响应为 `created: false`、`changed: false`。
`created` 表示本次是否首次创建;`changed` 表示本次是否实际写入。对同样内容的重试,响应为 `created: false`、`changed: false`。响应中的 `taskType` 回显写入时的业务类型(注意与固定为 `"b5-1"` 的 `type` 字段区分)。
## 用户跳转
@@ -78,5 +76,19 @@ X-Integration-Key: <DEERFLOW_TASK_DEEPLINK_INGEST_KEY>
/#/login?taskId=task-20261020-001&goPath=b5-1
```
登录后前端读取本系统详情接口,将任务名称和任务详情展示在左上角,标题固定为「任务分析」;`taskName`、`taskContent`、`overview` 与完整 `extra` 会作为每轮问答的隐藏任务上下文提供给配置的智能体。聊天记录按 `(taskId, b5-1)` 独立保存,不会混入其它任务深链接或通用聊天。
可选参数:
| 参数 | 说明 |
| --- | --- |
| `agentId` | 直接指定本次问答使用的智能体 id,优先于「业务链条管理」b5-1 行的配置。 |
| `type` | 业务类型(≤64 字符,超长截断)。**详情快照与聊天记录都按 `(taskId, type)` 独立分区**:同一任务传入不同 `type` 时读取各自的详情、维护各自的对话历史,互不可见。 |
| `theme` | 页面主题色。**不传时默认深蓝色**;传入则按传入值解析明暗(`dark`/`dark-blue`/`深色`/`深蓝` → 深色,其余如 `lightblue` → 浅色)。主题只在任务分析页面与本次深链会话内生效,不影响用户在系统其它页面的外观偏好。 |
示例:
```text
/#/login?taskId=task-20261020-001&goPath=b5-1&agentId=analyst-01&type=yuqing
```
登录后前端按 `(taskId, type)` 读取本系统详情接口(对应 `GET /api/task-deeplink-details/b5-1/{id}?type=…`,需登录态),将任务名称和任务详情展示在左上角,标题固定为「任务分析」;`taskName`、`taskContent`、`overview` 与完整 `extra` 会作为每轮问答的隐藏任务上下文提供给智能体。聊天记录同样按 `(taskId, type)` 独立保存,不会混入其它任务深链、其它类型或通用聊天;未传 `type` 时归入该任务的无类型分区(与历史链接的行为一致)。
@@ -0,0 +1,36 @@
# PostgreSQL 聊天记录月度归档
## 数据范围与月份
页面月份来自业务数据库 `threads_meta.created_at`,按北京时间的**会话创建月份**归档。一次归档会搬迁该月会话的全部 LangGraph checkpoints、checkpoint blobs 和 pending writes;跨月继续的会话仍完整放在创建月份的 SQLite 文件中。系统线程和当前状态为运行中的会话会跳过。
聊天列表元数据仍留在原业务数据库(MySQL 配置不变)。LangGraph Store 中的用户记忆、智能体数据和其他应用表不属于聊天 checkpoint,不会被清理。
## 固定目录与现有 SQLite 配置
默认目录:`$DEER_FLOW_HOME/data/chat-archives/`;未设置 `DEER_FLOW_HOME` 时为项目目录下 `.deer-flow/data/chat-archives/`。可用 `DEER_FLOW_CHAT_ARCHIVE_DIR` 指定其他绝对路径。目录内容:
- `chat-YYYY-MM.db`:每个归档月份一个 LangGraph SQLite checkpoint 数据库。
- `catalog.db`:月份任务、会话到月份的映射、复制/删除/回滚状态。
- `.locks/`:Worker 与主服务共享的会话级跨进程读写锁。
归档 SQLite 是叠加的历史层,不会修改 `checkpointer.connection_string`、`sqlite_connection_string` 或旧的 `dual_read_sqlite_paths`。PG 仍是写入目标;查询某个 thread 时先读 PG,缺少 checkpoint 才按 `catalog.db` 找到相应月份 SQLite。历史会话页面和 LangGraph `history/state` 接口使用同一套回退逻辑。首次管理员启动归档任务后才创建索引;此前聊天写入不创建锁文件、不额外写归档数据。仅打开页面只查 `threads_meta` 月份汇总,不扫描 PG 检查点表,也不创建归档目录或索引。
如果启用 Worker 池,归档目录必须位于主服务和 Worker 共同挂载的**可写持久卷**。默认目录落在 `$DEER_FLOW_HOME/data` 下,标准 Docker 部署的 data volume 会共享该路径;自定义路径需要放入相同的共享卷。PostgreSQL Worker 检查会在卷未共享时拒绝启动,避免只在主容器可见的归档导致历史查询丢失。
## 操作流程
归档、清理、回滚通过 SQLite 原子任务认领,保证全局同一时间只运行一个月份任务,避免多个管理请求同时压满 PostgreSQL 和磁盘 I/O。
1. 管理员打开「设置 → 聊天记录归档」。页面按月聚合轻量会话元数据,不会打开页面就扫描 PostgreSQL 的 80GB checkpoint 表,也不会创建归档状态文件;同时显示归档卷可用空间。
2. 选择已经结束的月份执行「归档到 SQLite」。任务后台逐会话复制并核对 checkpoint id;进度写在固定目录,关页或服务重启后可重新发起继续处理。
3. 确认状态为「归档已校验」后,才可点击「删除已归档 PG 数据」。删除任务会在每个会话上取得共享卷锁,等待正在进行的 checkpoint 写入结束,再次复制并验证 SQLite,最后删除 PostgreSQL 中该 thread 的 checkpoints、blobs 和 writes。
4. 页面「回滚到 PostgreSQL」会把该月 SQLite checkpoint 重新写回 PostgreSQL 并校验;SQLite 副本会保留,历史读取可继续回退。
归档、清理、回滚均按会话运行,并限制并发到单个月任务。目标卷会保留至少 2 GiB 空间;低于阈值时任务暂停,复制校验失败时会保留该会话的 PostgreSQL 数据。清理后 PostgreSQL 可以复用空闲页,但普通 `DELETE` 不保证操作系统看到的数据库文件立即变小;系统不会自动执行可能带来明显 I/O 和锁表风险的 `VACUUM FULL`。
## 部署与验证
更新后台 `app/`、`packages/`、`tests/` 和前端 dist 后重启主服务。数据库不需要新增 PostgreSQL migration;首次任务会创建本地 SQLite 月库和归档目录。部署前确保该卷有足够空间容纳选中月份的副本,并将 `DEER_FLOW_HOME/data` 纳入备份。
回归测试覆盖归档复制、checkpoint 校验、PG 删除后的透明 SQLite 回退、回滚,以及 Worker 共享归档卷校验。线上执行前先在单个月份做归档和历史读取验收;归档按钮本身不会删 PG,删除操作须管理员单独点击。
@@ -82,6 +82,14 @@ python scripts/setup_force_situation_chain.py
python scripts/setup_biz_detail_assistant.py
```
`task-context` 与 `task-detail-update` 读取 `TASK_DETAIL_AGENT_USERNAME` 和
`TASK_DETAIL_AGENT_PASSWORD`,通过本 Gateway 的 `/api/v1/auth/login/local` 领取短期 JWT 后再调用
任务详情接口。因此部署时必须在**技能沙箱运行环境**中注入这两个变量和
`TASK_DETAIL_GATEWAY_URL`,并启用 `password_login_enabled`;不得把密码写入技能文件、SOUL、
命令行或检索输入。`setup_biz_detail_assistant.py` 同样改用账号密码登录,并可通过
`DEERFLOW_SETUP_BASE`、`DEERFLOW_SETUP_USERNAME`、`DEERFLOW_SETUP_PASSWORD` 覆盖本地默认值;
脚本只在正式密码登录被策略关闭(403)时回退旧用户名登录,其他失败均停止,不会绕过凭据校验。
业务链安装脚本默认只更新配置,不立即触发真实接口。需要立即执行时显式追加
`--trigger`。链路阶段为:接口数据采集 → 当前判断威胁接口采集 → 情报摘要与校验。
@@ -49,7 +49,6 @@ models:
use: deerflow.models.openai_codex_provider:CodexChatModel
model: gpt-5.4
supports_thinking: true
supports_reasoning_effort: true
- name: claude-sonnet-4.6
display_name: Claude Sonnet 4.6 (Claude Code OAuth)
@@ -59,6 +58,15 @@ models:
supports_thinking: true
```
Whether a model exposes the per-chat reasoning-effort selector is managed by
an administrator in **Model Management** and persisted in the
`model_capabilities` database table. For upgrade compatibility, a legacy
`supports_reasoning_effort: true` value is imported once at startup only when
that model has no database row. Existing database values (including `false`)
are never overwritten; after that first import the database is authoritative.
Remove the legacy YAML field when convenient. Model Management also removes it
the next time the model list is saved, and new configurations should not add it.
**Auth behavior for CLI-backed providers**:
- `CodexChatModel` loads Codex CLI auth from `~/.codex/auth.json`
- The Codex Responses endpoint currently rejects `max_tokens` and `max_output_tokens`, so `CodexChatModel` does not expose a request-level token cap
@@ -78,6 +78,8 @@ WeKnora 前端不做任何修改。DeerFlow 原“知识库资源管理”入口
- 原模式:保留现有 LLMWiki iframe 页面;
- WeKnora 模式:渲染 DeerFlow 原生 React 页面,提供个人库、公共库、直接发布和检索。点击卡片进入独立知识库路由,内容区复刻 WeKnora 的详情页结构,同时保留 DeerFlow 的导航、登录和权限壳。Wiki 库按 **Wiki → 文档 → 图谱** 排列并默认打开 Wiki;标准库不显示 Wiki 标签,按 **文档 → 图谱** 排列。Wiki 视图包含页面目录、搜索、页面正文和引用摘要;文档以卡片网格展示,点击后从右侧抽屉查看元数据、原文件与真实解析分块;图谱视图渲染 WeKnora Wiki 页面节点和边。新建知识库可选择“Wiki 知识库”或“标准知识库”,详情页支持文件、网页 URL、手工 Markdown 三种导入方式。问答选择器沿用 WeKnora 的紧凑搜索/分组/多选交互,颜色、间距和组件沿用 DeerFlow 设计系统;智能体创建和编辑页使用同一数据源的内嵌绑定面板。
Wiki 知识库可选填“参考目录”,支持逐行录入或上传 `.txt/.md`。参考目录只帮助生成模型理解用户期望的信息组织方式,不是目录白名单,也不是必须逐项套用的固定模板。WeKnora 可以根据原始资料新增、合并、拆分、改名、调整或省略目录;Gateway 不会为参考目录预建文件夹,也不会在生成后移动页面、删除目录或清理参考目录之外的有效结构。`directory_paths` 与兼容字段 `parent_directories` 均遵循这一语义。未填写时沿用原有自动 Wiki 生成方式;编辑时清空只表示不再向后续生成提供该参考,不撤销已有页面或目录。
问答检索命中以 DeerFlow 来源卡片展示。“查看原文”调用 DeerFlow 的 `/api/llmwiki/sources/context`,由服务端在重新校验 DeerFlow 用户权限后向 WeKnora 读取命中切片及相邻上下文,不向浏览器暴露 WeKnora 原始资源 ID。
## 主要接口
@@ -0,0 +1,164 @@
# 线上多人互相拖慢:源码排查与隔离复现
排查日期:2026-10-22。范围:当前业务源码、部署脚本和无网络的隔离探针。
未连接线上主机、模型或数据库,以下数值不是线上时延或容量结论。原始排查完成后,已按
本次授权落实下述源码优化;尚未部署到线上,未修改线上配置或数据。
## 已落实的优化
1. **同步 IO 隔离**:产物递归扫描、技能 ZIP 预览、内容探测和文本写入使用独立有界
executor(4 工作者、最多 8 个已提交作业),与登录密码校验的默认线程池隔离。
JSONL/本地事件 spool 使用另一组 2 工作者、4 个已提交作业;取消调用方不会提前归还
尚在执行的磁盘作业名额。权限检查仍在扫描/读写之前执行。
2. **完成 run 内存回收**:实际 task 完成后默认保留 300 秒,每 10 秒最多核对 200 项。
只有终态、错误和最终 metadata/kwargs 已在 RunStore 核对成功才移除实时记录;有未完成
task、存储失败或快照不一致则保留。不会删 SQL run 行、checkpoint、聊天记录或 SSE
缓冲。共享用户仍通过已授权的线程查询持久化 run;所有跨线程 run id 继续校验。
旧 run 即使比新 run 更晚收尾,也不能因新 run 已回收而重新覆盖线程状态。
3. **事件转存磁盘**:原默认 `memory` 后端工厂改用本地 SQLite spool,在
`runtime_home()/run-events/events.sqlite3` 保存完整内容,以事务分配递增 seq 并批量提交,
查询按页读取,不在内存永久堆积完成事件。没有自动 TTL 删除或消息截断;独立
`MemoryRunEventStore` 类保留供测试使用。显式 `db` 仍用业务 SQL/MySQL,`jsonl` 保留旧
目录/文件格式;没有迁移或删除这些历史存储。spool 及 WAL 需放在可写持久化本地卷。
4. **公平调度**:后台 subagent 默认全局 3、每用户 2;后台会商默认全局 4、每用户 2。
按用户轮转,排队不占执行工作者;队列有全局和每用户上限,满时明确报错。
subagent 超时包含排队时长。取消/超时后的实际协程必须结束才释放名额,避免旧任务
尚在写 checkpoint 时启动过多新任务。身份 context 继续传入执行线程和事件循环。
5. **会商延迟 checkpoint**:前台/后台内部建席位、总控线程只写业务元数据;init 不再
通过 HTTP 给空线程追加席位消息。清单随真实首轮输入一起保存;旧客户端未传
seat_meta 时仍从 thread 映射构造真实席位 ID。后续广播和历史 checkpoint 保留。
配置见 README 的 `runtime_contention` 示例,均为可选字段,重启生效。原
`ROUNDTABLE_BACKGROUND_SLOTS_PER_USER` 环境变量继续覆盖每用户会商额度(不超过全局额度)。
这些额度按 Gateway 进程计算;当前实时 run/bridge 仍在进程内,不能据此直接增 worker。
测试覆盖:真实 HTTP/SQL 权限与共享访问、run 回收后的历史和终态重连、Last-Event-ID 回放、
旧 run 收尾隔离、慢扫描期间另一用户创建线程、并发事件序号/分页/重启完整性、批次回滚与
磁盘失败后的 journal 重试、A 用户积压时 B 用户准入、排队超时/取消、真实子任务取消确认、
后台会商取消确认、会商 init 无 checkpoint 依赖和连续两轮消息完整性。使用本地 SQLite、
内存 saver 与受控协程,无线上 PG/MySQL/模型实测;不据此承诺线上时延或并发容量。
最终验证(2026-10-22):后端 747 passed / 1 skipped;前端 `pnpm test:thread-history`
35 passed;35 个新增/改动 Python 文件 Ruff 全部通过,改动的 harness 模块未引入
`app.*` 依赖,`git diff --check` 通过。后端测试入口:
```text
packages/harness/tests/
tests/test_position_roundtable_sessions.py
tests/test_roundtable_role_boundary.py
tests/test_roundtable_model_fallback.py
tests/test_roundtable_seat_modes.py
tests/test_roundtable_gather_phase.py
tests/test_model_concurrency.py
tests/test_account_authentication.py
tests/test_artifact_preview.py
tests/test_artifact_library.py
```
## 结论
2026-10-23 已在最新 master 上整合:保留任务深链独立卡片存储、task/agent/action
投影索引、长历史 slim_display 和前端检索进度分轮隔离。整合后扩大回归到后端
780 passed / 1 skipped(额外覆盖任务深链接口、详情分区、一键清空与索引迁移),
前端历史/恢复 37 passed,改动 Python 文件 Ruff 与 `git diff --check` 均通过。
前端全量 typecheck 仍报告既有的 81 项错误,本次修改的 hooks/background-history/
api-client 文件无类型错误;该检查未宣称全量通过。
不能只归因于 PG 数据量。优化前还存在事件循环上的同步文件扫描、未回收的运行记录和
内存事件、跨用户共享的子智能体队列、业务连接池与线程池竞争等路径。
业务表使用 MySQL 只隔离了数据库来源,没有隔离 Gateway 的 CPU、内存、线程池与业务连接池。
单 worker 本身不意味着不能并发;这里的风险是慢同步工作在唯一事件循环上执行。
现有 RunManager/MemoryStreamBridge 的实时控制状态在进程内,直接增加 worker 又会导致
重连、取消和 join 请求找不到原进程中的运行,不能当作当前架构的安全扩容方案。
## 优化前已确认的代码路径
| 优先级 | 位置与现象 | 对其他人的影响 | 适用边界 |
| --- | --- | --- | --- |
| P0 | `routers/artifacts.py::list_artifacts` 在 async 路由内直接调用同步 `collect_outputs_listing`,递归扫描、resolve/stat、排序 | 扫描期间同一事件循环无法及时处理其他请求和 SSE | 同步调用已复现;线上时长取决于文件量、磁盘和挂载 |
| P0 | `runtime/runs/manager.py` 定义了 cleanup,但当前应用调用链没有调用;`_runs` 保留完成记录、kwargs 和 task 引用 | 内存随历史运行累积;创建/限流/列表仍遍历全部记录,在事件循环上消耗 CPU | 当前代码路径确定;线上累计数/RSS 未取得 |
| P0 | 默认 `run_events.backend=memory` 的 `MemoryRunEventStore` 仅 append,没有自动 TTL/总量预算 | 工具输出、消息和 trace 长期积累,增加内存压力及扫描/GC 成本 | 线上若已使用 db/jsonl,此项不按默认内存后端判断 |
| P0 | `subagents/executor.py` 全进程 `_scheduler_pool(max_workers=3)`,调度线程等待执行 future 完成才释放 | 一名用户占用 3 个后台子任务时,另一人的子任务排队;排队尚未进入执行 timeout | 普通后台 subagent 路径;不能推广为所有模型调用最多 3 路 |
| P1 | 登录与 run、权限、菜单、任务等仓储注入同一个业务 SQLAlchemy session factory | 慢查询/锁等待占满 MySQL 池后,登录和菜单也要等连接;移走 PG 不能避免 | 连接池共享确定,线上是否已耗尽要观测 checkout/慢查询 |
| P1 | 密码校验、文档转换和多个文件/扫描任务都用默认 `asyncio.to_thread` executor | 长转换/扫描占满工作线程时,另一个人的密码验证或工具 IO 排队 | YAML 中 100 是同一总池上限,不是每人 100;AnyIO 另有自己的池 |
| P1 | 当前安装的 PG saver 实例在 `_cursor` 取连接前持有实例锁;SQLite saver 同样串行 | 某条慢 checkpoint 查询/写入使其他会话状态读写排队 | 上一轮无 PG 网络测试已验证;线上必须核对实际 saver/wheel 版本 |
| P1 | 会商内部 `_create_thread_direct` 和后台 `_create_thread` 仍直接写 `empty_checkpoint` | 批量建席位仍争用 saver;上一轮普通 POST 的延迟初始化没有覆盖这两个入口 | 见 `routers/multi_agent.py` 与 `roundtable_inprocess_gateway.py` |
| P1 | worker 同步递归 serialize,`services.format_sse` 在事件循环中同步 JSON 编码 | 大聊天状态/工具输出形成 CPU 和带宽尖峰,同进程其他请求延迟 | 不等于每个 token 都重发全量历史,取决于实际 stream mode 与事件大小 |
| P1 | 模型总量限流默认关闭、每用户额度默认 0;后台会商只有每用户 2 槽的局部限制 | 多人/多席位同时压向同一模型服务,没有为前台保留容量,上游排队和重试会放大等待 | 线上管理员设置可能覆盖默认;每用户会商限制不是全站后台上限 |
RunManager 的锁内没有等待数据库 IO,不能把它描述成“持全局锁等待 MySQL”;其已确认问题是
完成记录没有接入回收,以及锁内同步全表扫描随累计运行数增长。
MemoryStreamBridge 每个 run 的事件条数有 256 默认上限,结束 60 秒后也会 cleanup,
不能与无限累计的 MemoryRunEventStore 混淆。但它没有按字节限制,包含大 values 的活动运行
仍可能占用大量内存;连接复用/replay 也会再次编码事件。
## 其他需要对照线上负载的放大因素
- MySQL metadata SQL 条件下推后仍可能没有适用索引。共享 taskId/agent_id/actionId 的 JSON
条件、标题 contains 和排序应检查真实 EXPLAIN;不能把“只发一条 SELECT”视为“查询恒快”。
- `services._maybe_sync_legacy_agent_records` 的 TTL 为 3600 秒。过期请求需要等待同一个
同步任务,期间按智能体目录读文件并逐个调用业务库;冷启动/整点附近的突刺应核对
`[start-run-timing] legacy-agent sync`。正常 TTL 内已有快路径,不能说每次发送都会全扫。
- `load_system_settings()` 没有读缓存,直接读 JSON 并校验;模型准入与多个中间件会调用它。
文件放在慢共享盘时会增加同步 IO;若文件小且本机缓存命中,通常不是首要瓶颈。
- 产物预览仅在启用 API 加载、没有命中 write_file 流式路径且运行中时每秒刷新一次,loader
获取完整响应文本。它不是每秒全目录列表扫描。大报告多窗口预览可能放大磁盘/网络/前端
内存压力;通用文件下载已用 FileResponse,不应误报为所有下载都在路由内整文件读取。
- 模型服务自身的并发槽、GPU 显存、长上下文、重试、知识检索/外部技能接口需要独立观测。
网关请求已经发出而首 token 很晚,不能自动归因于数据库。
- 当前仓库没有足以代表该线上入口的 Nginx/Ingress 配置,不能判断线上 buffering、连接数、
超时、HTTP 版本或负载均衡是否正确。开发启动器含 reload,正式启动器默认单 worker;
应核对线上实际启动命令,不能从源码脚本推断线上一定在热重载模式。
- 模板同时存在 SQLite 和 MySQL+PG 配置,其中一个 postgres 模板 `use_postgres=false`。
必须以线上配置路径、环境覆盖和启动日志确认实际后端,不能只看 type=postgres。
## 隔离探针证据
探针仅调用实际代码中的本地对象、临时目录和 SQLite 测试池,没有网络/模型调用。
机器、操作系统、文件缓存等都会影响绝对数值,仅用来验证串扰机制。
| 场景 | 观察 |
| --- | --- |
| 真实产物列表函数,临时目录 1,200 文件 | 路由耗时 1,640ms,同事件循环的无关 2ms timer 迟到约 1,638ms |
| 同一函数/目录,作为对照放入 to_thread(未修改生产代码) | 扫描约 1,344ms,timer 迟到约 14ms;工作没有消失,但释放了事件循环 |
| 实际 format_sse,构造约 6.27MiB values | 编码约 47ms,timer 迟到约 45ms |
| RunManager 含 0 / 10,000 / 50,000 个已完成记录 | 全部仍被保留;启用两种准入计数时,新 run 准入中位约 0.016 / 3.195 / 15.376ms |
| MemoryRunEventStore 写入 1,000 个唯一 16KiB 假 trace | 1,000 个全部保留;tracemalloc 约增加 16.13MiB,没有自动过期 |
| 真实 subagent execute_async + 受控 future | 4 个独立任务只启动 3 个,第 4 个保持 pending;释放 1 个后才启动第 4 个 |
| 共享 SQLAlchemy 池测试,2 个连接均被占用 | 无关第三个连接等待超时;使用 SQLite QueuePool 验证池行为,不是线上 MySQL 测量 |
本次机器可复跑入口及原始结果(未提交到 Git,不包含凭证或用户消息):
- 根目录 `.runtime/concurrency_audit_20261022.py`
- 根目录 `.runtime/concurrency_subagents_20261022.py`
- 根目录 `.runtime/concurrency_audit_20261022.json`
## 线上最小观测集合
用同一北京时间窗口关联以下指标,再确定哪个是此次线上事件的主因:
1. Gateway 进程数、代码版本、实际数据库/checkpointer 类型,CPU/RSS/可用内存/容器重启数。
2. 事件循环 lag 与登录、菜单、普通小查询、run 创建的 P50/P95/P99。
3. MySQL 活跃连接、连接池借用等待/超时、慢查询和锁等待;PG 另记录 checkpoint 的锁等待、
借连接时间、SQL 时间、响应体大小。两层池分开,不只看数据库 CPU。
4. 运行中的前台/后台/子智能体数量、每用户占用、排队时间、executor 的排队/执行时间。
5. 模型请求发送时间、首 token 时间、完整生成时间、429/超时/重试和同一模型服务总请求数。
6. SSE 帧大小/编码耗时、产物扫描文件数/耗时、索引/入库/会商后台作业与故障窗口的重合度。
若登录和菜单也慢,先查事件循环、RSS/CPU、共享 MySQL 池和密码线程池;若这些正常而只有
出字慢,优先区分模型上游、checkpoint 与子任务排队。服务运行越久越慢且重启缓解时,
优先核对完成 run/内存事件数量,但重启缓解本身不是泄漏的充分证据。
## 建议处理顺序
1. **先切断已确认的串扰**:把产物同步扫描和 ZIP 读取等重 IO 移离事件循环,并给扫描并发
单独预算;补完成 run 回收和事件存储/保留策略。回收前保证持久化查询、取消和断线恢复兼容,
不删除仍在运行的任务或未持久化的数据。
2. **统一准入和公平调度**:后台任务全站额度 + 每用户额度 + 可观测队列;子智能体名额可配、
公平取用,排队超时从入队计算;为前台保留资源。会商内部创建统一采用验证过的延迟初始化。
3. **隔离资源**:认证轻任务与文档转换/扫描分 executor,业务轻查询与后台重查询设独立预算,
保持 MySQL 总连接数有界;按实际 EXPLAIN 补索引,验证 saver 的并发实现。
4. **最后做容量扩展**:实时流传输和运行控制共享化后,再验证多 worker/多实例;结合真实
模型容量做分级负载测试。单纯扩大线程池、连接池或 worker 数,不能消除上述等待链。
@@ -0,0 +1,401 @@
# 普通知识库智能推理检索模式设计
> 状态:已实现,与当前代码一致
>
> 适用范围:通用问答、普通智能体问答中的 WeKnora 普通知识库
>
> 更新日期:2026-10-21
>
> WeKnora 行为核查基线:v0.7.2,commit 3d5d8bfcdfeeea266b292b71cea616847af28d0f
## 1. 目标
系统在原有经典检索之外增加“知识库智能检索”模式。经典模式完整保留并始终作为新对话的默认模式,模式选择器中经典检索位于智能检索上方。新模式的全局开关默认关闭,只有管理员能在设置页开启;开关开启只表示向用户开放智能检索选项,用户可在需要更全面梳理时主动切换。已有线程继续按自身 metadata 恢复模式,不被全局开关批量改写。
开关和本功能新增配置只保存在数据库中,不写 `system_settings.json`、`config.yaml` 或其他配置文件。智能模式在一次用户提问内执行模型与工具的多轮循环。
用户只需直接提问,不需要在问题中说明先检索什么、再检索什么。每个新问题先执行原始文档向量/混合检索取得直接相关的分片、标题和实体;只有问题需要目录结构、概念关系、时间线或扩展线索,且当前 WeKnora 支持原生 Wiki 正则接口时,才用这些线索执行 Wiki 正则检索。Wiki 有命中时,模型只选择与回答有关的页面深读;模型未选择时,中间件读取排序第一的页面作为最低证据基线。长 Wiki 页面会使用 `next_cursor` 连续读取到 `complete=true`。之后只有在 Wiki 暴露出首轮 RAG 没有覆盖且回答确实需要的线索时,才继续改写查询、做原始切片正则核验或读取文档上下文。证据足够就停止检索并回答;Wiki 与原始文档冲突时,以原始文档为准。
检索结果是回答的背景资料,不替代模型原有的理解、归纳、写作和推理能力。模型可以使用稳定通用知识解释术语与背景,但必须把库内事实、基于材料的推断和一般知识区分清楚,不能用模型记忆编造知识库专属事实。
最终回答不要求带 [1]、[2] 等编号。
## 2. 与原 WeKnora 逻辑的对应关系
本实现对齐 WeKnora 智能推理的搜索、深读和证据缺口循环。按当前验收要求,在 WeKnora 混合检索策略之上增加确定性的证据基线:先搜索原始分片,再结合原始命中的标题和实体搜索 Wiki,并至少深读一个相关 Wiki 页面。中间件会推迟越序的 Wiki 调用并补齐缺少的阶段,但不会为了走流程而读取全部 Wiki 命中页。基线完成后,后续轮次由模型根据问题类型和真实证据缺口决定。后续补强路径如下:
| 问题类型 | 固定首轮后的补强面 | 深读动作 |
|---|---|---|
| 概念、主题、关系、整体脉络 | Wiki 正则搜索 | 读取命中 Wiki 页正文 |
| 数字、日期、编号、条款、原句 | 原始知识搜索或切片正则搜索 | 读取命中切片的邻近上下文或文档分块 |
| 同时需要框架与事实 | 先原始 RAG,再用 Wiki 扩展 | 用新的原始查询或切片正则核验 Wiki 暴露的关键事实 |
原始知识搜索沿用 WeKnora 的 knowledge-search 接口。 该接口由 WeKnora 执行向量或混合召回、融合和排序。 精确词、代码、编号和固定短语由切片正则检索补充。
经典检索同时使用两层语义召回:WeKnora 原生 knowledge-search 检索上传原文的分片,DeerFlow 本地 Wiki 向量索引检索生成后的 Wiki 页面章节,并与本地 Wiki 关键词结果融合。两套向量覆盖的语料和职责不同,前者提供原始事实细节,后者提供整理后的主题结构与概念关联,不属于对同一份内容的重复编码。当前经典检索仍实际消费本地 Wiki 向量,因此保留 DeerFlow Wiki 向量索引;关闭它会降低经典模式的 Wiki 语义召回。
智能检索的 Wiki 搜索只调用 WeKnora 原生 `/api/v1/knowledgebase/{id}/wiki/search`,采用大小写不敏感的安全 POSIX 风格正则表达式。多个已选知识库在受控并发下各发起一次原生请求,不再枚举 Wiki 目录或逐页读取正文来模拟正则检索。若当前 WeKnora 没有该接口或返回 404/405,本轮静默跳过 Wiki 正则通道,并从后续模型调用中移除 Wiki 搜索工具,防止更换 pattern 后重复探测;系统继续使用原文分片和模型原有能力回答。 Wiki 搜索结果只作为页面入口。 模型需要调用页面读取工具获得正文,不能仅凭标题或摘要完成事实回答。
原始切片命中后,模型需要调用文档分块工具补齐相邻内容或后续分页。 Wiki 与原始文档冲突时,系统提示模型优先采用能定位到原始文档切片的证据,并说明差异。 首轮无结果时,系统提示模型改写查询一次。
仍有明确证据缺口时,系统提示模型选择能弥补该缺口的检索面。没有新增收益时应停止检索。中间件会在同一用户回合对原始查询、正则、Wiki pattern、`page_ref+cursor` 和文档分页做确定性去重;Wiki 的新 `next_cursor` 属于有效续读,不视为重复。上游核查入口:
- Agent preset:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/config/agent_type_presets.yaml
- Agent system prompt:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/config/prompt_templates/agent_system_prompt.yaml
- Knowledge search tool:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/knowledge_search.go
- Chunk grep tool:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/grep_chunks.go
- Document reader:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/list_knowledge_chunks.go
- Wiki tools:https://github.com/Tencent/WeKnora/blob/3d5d8bfcdfeeea266b292b71cea616847af28d0f/internal/agent/tools/wiki_tools.go
## 3. 模式与开关
会话请求字段为 knowledge_retrieval_mode。 允许值只有:
- classic
- smart_reasoning
请求省略该字段或传空值时,后端按 classic 处理。 前端每次发送都显式携带当前模式。 线程 metadata 只用于恢复前端控件,不是后端本轮执行的隐式输入。
这保证旧客户端和缺少合法模式 metadata 的旧会话继续使用经典模式。新对话始终默认 classic;全局开关开启后只增加 smart_reasoning 可选项,关闭或读取失败时仅提供 classic。管理员切换全局开关不会批量改变历史会话。
管理员关闭开关也不会删除历史对话。 历史线程若保存为 smart_reasoning,关闭全局开关后必须切回 classic 才能继续发送。 模式不写入全局浏览器偏好。
新会话页面间的临时交接使用现有页面状态和 sessionStorage。
## 4. 数据库存储
全局开关表为 knowledge_retrieval_settings。 该表使用 scope 作为主键。 当前全局记录的 scope 为 global。
主要字段如下:
| 字段 | 含义 |
|---|---|
| scope | 设置作用域,当前为 global |
| smart_reasoning_enabled | 是否向用户开放智能模式选项;不改变新对话默认的经典模式 |
| revision | 设置修订号 |
| updated_by | 最近修改人 |
| updated_at | 最近修改时间 |
数据库没有记录时,读取结果为 false。 迁移只创建表,不插入开启记录。 因此升级后功能保持默认关闭。
迁移文件为 20261014_01_knowledge_retrieval_settings.py。 SQL 部署使用 SQLAlchemy Repository。 无数据库的单元测试使用同一 Store 接口的内存实现。
生产功能不读取或写入本地配置文件。
## 5. 管理接口
读取接口:
GET /api/system-settings/ordinary-knowledge-retrieval
响应字段为 smart_reasoning_enabled。
更新接口:
PUT /api/system-settings/ordinary-knowledge-retrieval
请求字段为 smart_reasoning_enabled。
GET 供对话客户端和设置页读取当前状态。
PUT 仅管理员可调用。
前端读取失败时按关闭处理,并允许用户重试读取。 后端每次启动智能模式请求时重新读取数据库开关。 前端隐藏控件不能替代后端校验。
## 6. 适用知识范围
智能模式只支持 WeKnora 普通知识库。 至少需要选择一个普通知识库。 选择助手知识库时拒绝智能模式。
普通库与助手库混选时也拒绝智能模式。 智能体会话只能使用该智能体已绑定的普通知识库。 普通用户只能使用自己有读取权限的本地 mapping。
管理员也必须通过现有 mapping 存储读取有效映射。 请求和工具参数始终使用本地 mapping ID。 模型不能直接传入远端 WeKnora knowledge base ID 扩大范围。
越权、不存在和未绑定的知识库统一按不可访问处理。 智能模式启动前完成一次范围授权。 五个检索工具每次调用时再次读取当前用户、智能体绑定和 mapping 权限。
因此管理员撤权或绑定发生变化后,后续工具调用会立即失败。 深读工具还会校验 chunk 或 document 实际所属的远端知识库。 只知道其他知识库的 chunk ID、document ID 或 page_ref 不能越权读取。
## 7. 请求执行流程
一次请求按以下顺序执行:
1. Gateway 解析本轮显式 knowledge_retrieval_mode。
2. classic 直接进入原有经典检索链路。
3. smart_reasoning 读取数据库全局开关。
4. Gateway 校验普通库数量、助手库混选、智能体绑定和用户权限。
5. Gateway 校验 WeKnora runtime 与 mapping Store 可用。
6. Gateway 持久化本轮模式和知识库选择到线程 metadata。
7. metadata 写入失败时,将尚未启动的 run 标记为失败,不启动模型。
8. Gateway 把服务端确认后的模式和范围写入 RunnableConfig。
9. 智能模式关闭经典的预检索提示和编号引用提示。
10. Lead Agent 挂载五个智能检索工具和专用中间件。
11. 专用中间件保证本轮先执行原始文档 RAG,再执行 Wiki 搜索;Wiki 有命中时至少完整读取一个相关页面。模型再根据 Wiki 暴露的真实证据缺口自主执行后续搜索和深读。
12. 模型在证据充分或继续检索无收益时生成最终回答。
智能模式的 Gateway 上下文构建只做授权和范围准备。 它不在第一次模型调用前塞入固定数量的一次性检索片段。 检索决策由本轮模型和工具循环完成。
服务端会覆盖调用方试图设置的 disable_tools。 服务端也会从 excluded_tools 中移除五个智能检索工具。 智能模式会排除经典 llmwiki_search。
经典模式会排除五个智能检索工具。 两种模式不会同时暴露两套知识检索入口。
## 8. 五个真实工具
### 8.1 ordinary_knowledge_search
用途:对原始文档切片做向量或混合检索。 输入为 queries,允许 1 至 5 个非空查询。 单个查询最长 500 个字符。
多个查询并行调用 WeKnora /api/v1/knowledge-search。 结果按 chunk 合并。 同一 chunk 命中多个改写时保留 matched_queries。
同一 chunk 的分数取各次命中的最高值。 结果按分数倒序,最多返回 30 条。 某个查询失败时,其余查询结果仍可返回。
所有查询都失败时返回结构化失败。
### 8.2 ordinary_chunk_regex_search
用途:在原始文档标题和切片中查找精确词、编号、代码或固定短语。 输入为一个大小写不敏感的安全 POSIX 风格正则表达式。 表达式最长 256 个字符。
不接受 Python 专属扩展、反向引用和明显的嵌套量词风险模式。 每次正则执行有 50 毫秒硬超时。 首先调用 POST /api/v1/chunks/grep。
原生响应必须声明 contract 为 weknora-v0.7.2-grep-v1。 合同不匹配按上游错误处理,不静默接受未知格式。 只有原生接口返回 HTTP 404 或 405 时,才进入兼容回退。
兼容回退只使用现有 list_documents 和 list_chunks 接口,并按知识库、文档和切片页轮转扫描。
一个大知识库不能独占全局扫描预算。 兼容回退最多扫描 500 个切片,最多返回 30 条结果。 兼容响应明确返回 compatibility_fallback 为 true。
兼容响应的 contract 为 weknora-existing-api-compat-v1。 部分知识库失败时返回已成功范围的结果和结构化 warnings。 全部知识库失败时返回结构化失败。
### 8.3 ordinary_document_chunks
用途:读取命中切片的邻近上下文,或分页读取整篇文档的分块。 identifier_type 支持 chunk_id 和 knowledge_id。 chunk_id 模式先读取切片并核验所属知识库。
校验通过后返回该切片前后各两个相邻切片。 knowledge_id 模式先核验文档所属知识库。 校验通过后按 page 和 page_size 读取分块。
page 从 1 开始。 page_size 限制为 1 至 100。 返回 total,模型可继续翻页直到覆盖所需正文。
### 8.4 ordinary_wiki_search
用途:在 Wiki 页面中做正则搜索。 匹配字段包括:
- title
- slug
- summary
- aliases
- content
每个 mapping 单独选择读取通道。 本地镜像只有同时满足以下条件才被采用:
- 有 last_completed_at
- state 为 idle
- 没有 last_error
- failed_page_count 为 0
- local_page_count 等于 remote_page_count
- 最近同步结果不是失败或部分完成
某个 mapping 的镜像不完整时,只将该 mapping 回退到 WeKnora provider。 其他镜像完整的 mapping 继续走本地通道。 全局最多扫描 500 个页面候选。
扫描预算按 mapping 公平分配。 正文匹配范围对单页限制为前 50000 个字符。 最多返回 30 个页面入口。
响应带 channel、coverage、partial、warnings 和 scan_truncated。 全部 mapping 都不可用时返回结构化失败。 检索成功但没有匹配时返回 ok 为 true、result_count 为 0。
### 8.5 ordinary_wiki_read_page
用途:读取 ordinary_wiki_search 命中的完整 Wiki 正文。 输入 page_ref 必须来自 Wiki 搜索结果。 page_ref 是范围引用,不是权限凭据。
工具调用时仍会重新校验 mapping 权限。 读取通道与 Wiki 搜索相同:完整镜像优先,否则走 provider。 单次最多返回 30000 个字符。
长页面返回以下续读字段:
- complete
- next_cursor
- content_offset
- content_end
- content_length
- content_truncated
模型把 next_cursor 传回同一工具即可读取下一段。 cursor 与 page_ref 绑定。 把一个页面的 cursor 用到另一个页面会返回 INVALID_CONTINUATION_CURSOR。
按顺序拼接各段 content_markdown 可以重建完整正文。 页面结果同时返回 title、slug、summary、aliases、links 和 source_refs。
### 8.6 检索进度与 `result_items` 合同
五个工具通过 LangGraph custom stream 发出 `knowledge_retrieval_progress` v1 事件。事件只展示实际查询、公开聚合统计和有界结果预览,不包含模型内部思考、系统提示、原始 provider 异常或未授权范围的数据。`started` 事件用于显示正在执行的步骤;成功的 `completed` 事件除 `result_count`、`partial`、`channel`、扫描数量等聚合字段外,还可包含:
```json
{
"result_items": [],
"result_items_truncated": false
}
```
`result_items` 最多 20 项。每项公共字段如下:
| 字段 | 含义 |
|---|---|
| `kind` | `wiki_page` 或 `document_chunk` |
| `knowledge_base_id` | 本地、已选择且已授权的 mapping ID |
| `knowledge_base` | 知识库显示名称 |
| `title` | Wiki 页面或原始文档标题 |
Wiki 项还可包含 `page_ref`、`slug` 和最长 1200 字符的 `summary`。Wiki 读取项可附带最长 8000 字符的 `content_markdown`、`content_offset`、`content_end`、`content_length` 和 `content_truncated`。原始文档项可包含 `chunk_id`、`knowledge_id`、最长 1600 字符的 `content`、`content_truncated`,以及可选的 `chunk_index`、`score`、`is_requested`。超过 20 项或投影时丢弃了不能安全展示的结果时,`result_items_truncated` 为 true。
进度投影只从本次工具已经授权的结果生成。它不会复制远端 WeKnora/provider knowledge base ID,也不会把 provider 返回的任意未知字段透传给浏览器。缺少本地 mapping ID、mapping 不在本轮选择范围、或 `page_ref` 解码后与本地 mapping/slug 不一致的项会被丢弃。右侧详情接口还会根据当前用户重新授权本地 mapping ID,`result_items` 中的标识不能替代权限校验。
## 9. 多轮推理约束
智能模式在系统提示中加入所选知识空间约束。模型把本轮选择的普通知识库作为具体事实的首要来源,不能用 Web、MCP、外部技能或子智能体扩大检索范围。模型原有的理解、归纳、写作和推理能力仍然可用,也可以用稳定通用知识解释背景;但必须明确区分库内事实、基于材料的推断和一般知识,不能靠模型记忆补造知识库专属事实。用户明确要求只依据知识库时,按更严格范围回答。
模型可以在五个工具之间多次切换。每个新问题必须先调用 `ordinary_knowledge_search`,一次可提交 1 至 5 个查询改写;再结合原始命中调用 `ordinary_wiki_search`。有 Wiki 命中时,优先保留模型选择的相关页面;模型未选择页面时,中间件读取排序第一的页面,不再强制读取最多 5 个命中的全部页面。即使模型试图越序或直接回答,中间件也会补齐这个最低证据基线。完成后,多轮次数由证据是否覆盖问题决定,不固定为 N 次,也不要求每轮把所有工具都调用一遍。
精确命中后可调用 ordinary_document_chunks 深读。Wiki 命中后只深读确实与问题有关的页面。已选择的长 Wiki 页面由中间件携带 `next_cursor` 连续读取到 `complete=true`;失败、畸形结果或无法继续的空游标作为终止尝试处理,避免无限循环。
同一回合中,相同 raw query、正则 pattern、Wiki pattern、`page_ref+cursor` 和文档分页会被过滤;`chunk_id` 上下文读取按切片本身去重,不能通过无效的分页参数重复执行。一个批次同时包含旧查询和新查询时,只删除旧查询并继续执行新的扩展查询。模型调用前还会收到不展示给用户的已执行动作清单;如果模型仍只返回重复调用且没有正文,中间件会移除五个智能检索工具并以 `tool_choice=none` 做一次隐藏的最终合成重试,要求基于本轮已有证据完整回答。该重试不会再次进入检索循环;重试仍无正文时才返回证据不足的安全说明。
Wiki 与原始文档冲突时,以本轮原始文档切片为权威证据。知识库专属事实证据不足时必须明确说明缺少什么;一般背景可以由模型能力补充,但不能伪装成知识库结论。
## 10. 历史证据清理
完整聊天记录和工具轨迹仍保存在 checkpoint 中。 前端历史展示和审计数据不会被删除。 在新问题发给模型前,SmartReasoningRetrievalMiddleware 只重写本次模型请求。
它找到最近一个真实用户消息作为当前轮边界。 边界之前的五类智能检索 tool call 和对应 ToolMessage 不再发给模型。 旧工具正文会被固定提示替代,说明历史检索不构成本轮证据。
当前用户轮中的工具调用和结果保持不变。 非智能检索工具调用保持不变。 这样每个新问题都重新检索,同时保留完整持久化历史。
## 11. 40 次模型调用上限
智能模式对每个真实用户回合设置最多 40 次 Lead Agent 模型调用。 计数从最近一个真实用户消息之后开始。 前 39 次允许模型继续检索或回答。
第 40 次被保留为最终合成调用。 进入第 40 次时,五个检索工具从请求中移除。 请求强制 tool_choice 为 none。
系统追加隐藏指令,要求以本轮已获取证据为事实基础立即回答,并允许用通用知识解释背景但不得补造知识库专属事实。 如果模型仍返回工具调用,Middleware 会剥离工具调用并结束图执行。 如果第 40 次没有可用回答,返回固定的证据不足提示。
达到上限后不会发生第 41 次模型调用。 该限制只挂载在 smart_reasoning 模式。
## 12. 回答与引用
智能模式将 rag_mode_enabled 强制设为 false。 它不注入经典的 RAG_CITATION_PROMPT。 它不挂载 RagCitationMiddleware。
因此最终正文不强制出现数字编号引用。 模型可以自然提及文档名或 Wiki 页面名。 模型应先覆盖用户问题,再说明资料冲突或证据缺口。
工具仍返回结构化知识库名、文档名、page_ref、chunk_id 和读取范围。 完整工具轨迹仍进入现有运行记录。
## 13. 失败语义
Gateway 在模型启动前使用 HTTP 错误阻止无效请求。 主要错误如下:
| 场景 | HTTP | code |
|---|---:|---|
| mode 不是允许值 | 422 | INVALID_KNOWLEDGE_RETRIEVAL_MODE |
| 管理员未开启智能模式 | 409 | SMART_REASONING_DISABLED |
| 设置 Store 不可用 | 503 | KNOWLEDGE_RETRIEVAL_SETTINGS_UNAVAILABLE |
| 混选或只选助手知识库 | 422 | SMART_REASONING_ORDINARY_ONLY |
| 没有选择普通知识库 | 422 | SMART_REASONING_SCOPE_REQUIRED |
| WeKnora runtime 不可用 | 503 | SMART_REASONING_PROVIDER_UNAVAILABLE |
| mapping Store 不可用 | 503 | SMART_REASONING_STORE_UNAVAILABLE |
| mapping 不存在、越权或未绑定 | 404 | KNOWLEDGE_SCOPE_NOT_FOUND |
相关 metadata 写入失败时,尚未启动的 run 被标记为失败。 模型不会在模式或范围没有持久化的情况下启动。 工具失败使用统一 JSON 结构。
成功响应包含 ok 为 true、operation、result_count 和 results。 全部检索通道失败时返回 ok 为 false 和安全错误信息。 部分范围失败时返回 ok 为 true、partial 为 true 和 warnings。
成功执行但零命中不是系统错误。 零命中返回 ok 为 true、result_count 为 0,交给模型改写或切换检索面。 无效正则返回 INVALID_REGEX。
正则执行超时返回 REGEX_TIMEOUT。 跨范围 chunk、document 或页面读取返回通用的不可访问错误。 未归一化异常会转换为统一失败,不向模型返回 Python 堆栈。
每个工具输出有 48000 字符上限。 超过上限时按完整结果项裁剪,并标记 output_truncated。
## 14. 经典模式兼容
classic 继续调用 build_llmwiki_rag_context。 它保留原有一次性预检索、上下文注入和 llmwiki_search 工具。其原始分片通道使用 WeKnora 自身的向量/混合检索,Wiki 通道使用 DeerFlow 本地 Wiki 向量与关键词融合;两类语料互补,当前均保留。经典模式原有编号引用行为不因本功能改变。
旧客户端不传 mode 时自动进入 classic。全局开关只控制 smart_reasoning 是否作为可选模式开放;无论开关状态,新对话都默认 classic,关闭开关不会影响 classic。
本功能也不复用前端已有的 multi_round_rag execution profile。 multi_round_rag 的外部调用语义与本功能的知识库范围、权限和会话执行不同。 knowledge_retrieval_mode 与模型选择、推理强度和 execution_profile 相互独立。
## 15. 前端实现
管理员入口位于“设置 → 知识检索”。 设置页展示当前数据库开关和说明。 通用问答与智能体问答输入框都支持“知识库智能检索”切换。
新会话始终初始化为 classic,模式选择器按“经典检索、智能检索”的顺序展示;管理员开启全局开关后才显示并允许用户主动选择 smart_reasoning。已有线程始终从 metadata 恢复各自保存的模式。前端每次发送前执行以下校验:
- 全局开关已读取并开启
- 知识库选择已加载完成
- 至少选择一个普通知识库
- 没有选择助手知识库
校验失败时显示明确提示并停止发送。 后端仍重复执行全部权威校验。 输入框可显示历史线程的智能模式状态,便于用户切回 classic。
前端不会在开关关闭时把 smart_reasoning 静默降级成 classic。
智能检索执行期间,“智能检索过程”保持展开,连续显示首轮原始文档搜索、Wiki 搜索、相关页面读取和按需扩展深读。检索结束后自动收起,只保留轮数、原始文档命中数和 Wiki 命中数摘要;用户可以随时手动展开或再次收起。历史消息重放时也默认收起。
展开后,每个检索步骤在“查询结果”中显示可查看的结果项。带有本地 mapping ID 和稳定页面/文档标识的结果可以点击,并在当前聊天页面右侧打开现有 WeKnora 来源抽屉:Wiki 项定位到 Wiki 页面,原始项定位到对应文档或切片。打开抽屉不会改变聊天主路由。旧历史轨迹若没有本地 mapping ID、Wiki slug、chunk ID 或 document ID 等稳定标识,只显示不可点击的标题摘要,前端不会按标题猜测来源。
## 16. 主要实现文件
后端设置与迁移:
- packages/harness/deerflow/persistence/knowledge_retrieval_settings/
- packages/harness/deerflow/persistence/migrations/versions/20261014_01_knowledge_retrieval_settings.py
- app/gateway/routers/system_settings.py
- app/gateway/deps.py
Gateway 范围与运行配置:
- app/gateway/llmwiki_rag.py
- app/gateway/services.py
工具与 WeKnora 适配:
- packages/harness/deerflow/tools/builtins/llmwiki_reasoning_tools.py
- packages/harness/deerflow/integrations/weknora/client.py
- packages/harness/deerflow/tools/tools.py
模型提示与运行保护:
- packages/harness/deerflow/agents/lead_agent/agent.py
- packages/harness/deerflow/agents/middlewares/smart_reasoning_retrieval_middleware.py
前端:
- frontend-web/src/components/workspace/settings/knowledge-retrieval-settings-page.tsx
- frontend-web/src/components/workspace/settings/settings-dialog.tsx
- frontend-web/src/components/workspace/input-box.tsx
- frontend-web/src/core/threads/knowledge-retrieval-mode.ts
- frontend-web/src/core/threads/hooks.ts
- frontend-web/src/core/threads/smart-knowledge-retrieval.ts
- frontend-web/src/components/workspace/messages/smart-knowledge-retrieval-trace.tsx
- frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx
- frontend-web/src/pages/ChatPage.tsx
- frontend-web/src/pages/AgentChatPage.tsx
## 17. 测试
设置和权限测试:
- tests/test_knowledge_retrieval_settings.py
- tests/test_llmwiki_smart_reasoning.py
检索工具测试覆盖:
- 多查询合并和 matched_queries
- 原始搜索全部失败与部分失败
- 安全正则和执行超时
- provider grep 原生合同校验
- 404 和 405 时的兼容回退
- 多知识库公平扫描
- 兼容回退标记和结构化告警
- 文档和切片跨范围拒绝
- Wiki 按 mapping 选择本地或远端通道
- Wiki 全部失败和部分失败
- Wiki 长页面续读和 cursor 绑定
- `result_items` 只包含本轮已授权的本地 mapping ID
- provider knowledge base ID 和未知 provider 字段不会进入进度流
- Wiki `page_ref` 与 mapping/slug 不一致时拒绝投影
- 结果项数量和预览长度有界,并正确标记截断
Middleware 测试:
- tests/test_smart_reasoning_retrieval_middleware.py
- 历史智能检索 call/result 配对清理
- 当前轮证据保留
- 非智能工具保留
- OpenAI 原始 tool_calls 兼容
- 第 40 次强制合成
- 禁止第 41 次模型调用
- 首轮原始 RAG 在 Wiki 搜索前执行,原始命中标题可参与 Wiki pattern 生成
- Wiki 多页命中只读取模型选择的相关页面,不强制遍历全部页面
- 同一轮对查询、正则、页面游标与文档分页去重,混合批次保留新的扩展动作
- 长 Wiki 页面按 `next_cursor` 续读完成后才进入后续扩展或回答
- 失败或无后续游标时安全终止,不形成注入循环
前端模式测试:
- frontend-web/src/core/threads/knowledge-retrieval-mode.test.ts
- 模式归一化
- 每次请求显式携带 mode
- 管理员开关、选库状态和助手库混选校验
- 检索中保持展开、完成和历史轨迹默认收起,并支持手动开合
- 新轨迹结果可点击打开右侧来源抽屉
- 缺少稳定标识的旧历史结果保持不可点击
2026-10-18 调整后的中间件专项为 34 passed;相关四组后端测试合计 145 passed。相关 Python 文件通过 Ruff。
本次前端模式、目录解析、进度匹配、抽屉定位与检索轨迹专项测试为 31 passed,pnpm build 通过。
## 18. 验收标准
以下条件同时满足时,本功能可验收:
1. 新数据库无设置记录时,智能模式默认关闭。
2. 非管理员不能修改全局开关。
3. 关闭开关时,伪造 smart_reasoning 请求被后端拒绝。
4. classic 在开关关闭和开启时都可正常使用。
5. 新会话始终默认 classic 且经典检索位于模式选择器上方;全局开关开启后用户可主动切换 smart_reasoning,历史线程始终按 metadata 恢复显示。
6. smart_reasoning 请求必须显式携带模式。
7. 智能模式没有普通库、混选助手库或越权时不能启动模型。
8. 每个工具调用重新校验当前权限。
9. 用户只直接提问时,每轮新问题都依次完成原始 RAG、Wiki 搜索和至少一个相关 Wiki 页面的完整读取,并可按证据缺口继续扩展。
10. 原始正则在 provider 不支持 grep 时自动使用有界兼容回退。
11. Wiki 搜索按 mapping 独立选择完整本地镜像或 provider。
12. 长 Wiki 页面能用 next_cursor 连续读完。
13. 全部通道失败与零命中具有不同语义。
14. 新用户轮不会把旧轮智能检索正文重新当作证据。
15. 单个用户回合最多执行 40 次 Lead Agent 模型调用。
16. 智能模式最终回答不强制编号引用。
17. 智能检索过程在执行时展开,结束和历史重放时默认收起,并允许手动展开或收起。
18. 带本地授权 mapping ID 和稳定来源标识的新检索结果可点击,并在聊天页右侧抽屉查看。
19. 缺少稳定来源标识的旧历史结果不可点击,前端不按标题猜测来源。
20. 进度流不泄露 provider knowledge base ID、未授权结果、提示词或模型内部思考。
21. 设置、模式和 Wiki 生成相关新增配置均由数据库持久化,不写运行时配置文件。
22. 同一用户回合不会重复执行相同 query、正则、Wiki `page_ref+cursor` 或文档分页;没有证据缺口时不会为了展示流程继续检索。
23. 知识库证据作为事实基础,但不会关闭模型原有的归纳、解释和写作能力;通用背景与库内事实保持可辨认。
## 19. 已知边界
切片正则兼容扫描是有界扫描,不承诺遍历无限大的全部语料。 响应中的 scan_truncated 会明确表示达到扫描上限。 模型可据此改写查询,优先使用向量或混合检索缩小范围,再做精确深读。
Wiki 搜索对候选页面和单页匹配正文也有明确上限。 Wiki 页面正文读取支持 continuation,因此已命中的长页面可以完整读完。 page_ref 和 continuation cursor 是内部不透明引用。
它们用于绑定页面与续读位置,不替代每次工具调用的权限校验。 本实现不改变原有 WeKnora 数据管理、经典检索和现有会话记录。
旧会话仍可展示当时保存的检索步骤和标题,但早期工具结果若没有本地 mapping ID 或稳定来源标识,就无法安全构造详情目标,因此保持不可点击。新产生的进度事件最多投影 20 个结果项;完整工具结果和回答证据仍按既有运行记录保存。
@@ -0,0 +1,56 @@
# 后端性能日志排查
此功能不需要安装 `py-spy`、`perf` 等工具。后台会为业务 `/api/` 请求打印一行简洁耗时(健康检查和 worker 池控制接口除外);超过 500 ms、返回 5xx 或抛出异常的请求会附带分阶段细节。每 5 秒最多报告一次超过 200 ms 的事件循环卡顿。日志不记录请求体、聊天内容、查询参数、SQL 原文、账号信息或密钥。
## 查看慢接口阶段耗时
更新并重启后台后,在宿主机执行:
```bash
docker logs --since 10m cmzs-deerflow-deerflow-backend-1 2>&1 | grep 'PERF'
```
慢请求会有一行 `PERF http_request` 日志,主要字段:
- `total_ms`:完整请求耗时。
- `headers_ms`:服务开始返回响应头前的耗时;大通常说明服务端处理或等待较慢。
- `endpoint_ms`:接口函数本身的耗时。
- `outside_handler_ms`:接口函数以外的耗时,包括鉴权、中间件、依赖解析和响应序列化。
- `stages`:接口内部分阶段的耗时,单位为毫秒。
- `event_loop_lag_ms`:最近测得的主事件循环延迟。
- `node`、`worker_access_ms`、`worker_upstream_headers_ms`、`worker_fallback`:请求是否走子 work、代理耗时,以及是否因容量不足回退主服务。
- `api_inflight_start` / `route_inflight_start`:请求刚进入时正在处理的 API 总数、同一路由请求数;`*_end` 是该请求结束记录时仍在处理的数量。
- `db_pool`:慢请求、异常请求和并发达到 8 个时,记录业务数据库连接池已借出数、池大小和溢出连接数。用于判断首屏并发/连续刷新是否挤满 MySQL 连接池。
- `exception_type`:只记录异常类型,不记录可能含敏感数据的异常消息。
- `status=499`:浏览器刷新、切页或取消请求导致服务端提前结束,不表示接口内部失败。
接口 INFO 日志依赖后台日志级别允许 INFO。若 `config.yaml` 中设置为 WARNING,请临时调到 INFO;慢请求和 5xx 仍会以 WARNING/ERROR 级别记录。
重点接口的 `stages` 含义:
- `auth_options`:`load_system_settings` 是系统设置读取;`count_admin_users` 是业务库查询。
- `agents_list`:`agents_*_query_db` 和 `*_db` 是数据库调用;`agent_response_models`、`agents_filter_and_sort` 是进程内转换/排序;`agents_*_assembly` 汇总列表组装耗时。
- `llmwiki_knowledge_bases`:`weknora_knowledge_base_list_wait` 是等待 WeKnora 列表结果;`local_knowledge_base_query_db` 是本地数据库查询;`conversation_deposit_mapping_sync` 是会话知识库映射同步。
- `llmwiki_agent_knowledge_bases`:`actor_and_agent_access` 是用户与智能体权限查询;`load_agent_binding_config` 是读取智能体配置文件;`local_knowledge_base_mapping_query` 是按智能体绑定 ID 读取本地映射;`weknora_knowledge_base_list_wait` 是等待远端列表。
知识库列表接口(`/api/llmwiki/knowledge-bases` 和智能体绑定列表 `/api/llmwiki/agents/{id}/knowledge-bases`)会等待 WeKnora 列表请求,进程内缓存有效期为 15 秒。若 `PERF http_request` 的 `stages.weknora_knowledge_base_list_wait` 接近 16 秒,同时出现相近耗时的 `PERF weknora_knowledge_base_list upstream_ms=...`,就能确认主要耗时在 WeKnora 请求。`PERF weknora_http_request` 会把远端目录请求拆成 `auth_ms`(获取/刷新 WeKnora 登录凭据)与 `network_ms`(HTTP 请求等待);这样能看出慢在登录还是远端 API 网络/处理。若远端耗时很快但接口仍慢,则看其他阶段。智能体绑定接口另记录 `actor_and_agent_access`(权限与智能体查询)、`load_agent_binding_config`(配置文件读取)和 `local_knowledge_base_mapping_query`(本地数据库映射查询)。
ORM 数据库执行时间超过 500 ms 会打印 `PERF db_query`,包含请求 ID、数据库类型、SQL 操作类型、耗时和 SQL 指纹,不包含 SQL 语句和参数。请求 ID 与 `http_request` 行相同,可据此判断慢 SQL 属于哪个接口。SQL 错误只记录错误类型。可用 `DEERFLOW_SLOW_SQL_LOG_MS` 单独调整数据库慢查询阈值。
首屏或反复刷新若引起请求洪峰,可看同一时间 `api_inflight_start`、`route_inflight_start` 是否升高,以及 `db_pool.checked_out` 是否逼近 `size + max_overflow`。如果接口等待明显但没有慢 SQL,重点看 `event_loop_lag_ms` 和 `outside_handler_ms`;这表示等待可能发生在连接获取、文件/磁盘、鉴权中间件或其它同步代码中。
如果 `total_ms` 很高、接口 `stages` 都很低,或同时出现 `PERF event_loop_stall`,问题更可能在接口函数之外的中间件/同步代码,或事件循环被其他任务占用。可以在卡顿发生时抓当下正在执行的 Python 函数。
## 抓当前 Python 函数堆栈
后台启动时会注册标准库 `faulthandler` 的 `SIGUSR1` 处理器。先在宿主机查容器里的 Python 主机 PID,再发信号:
```bash
docker top cmzs-deerflow-deerflow-backend-1 -eo pid,ppid,comm,args
sudo kill -USR1 <上一步查到的Python主机PID>
docker logs --since 2m cmzs-deerflow-deerflow-backend-1 2>&1 | less
```
在 `Current thread` 段查看最忙线程当时的 Python 文件和函数;其它线程也会一起输出。请在 CPU 正高时连续抓 2 到 3 次,每次间隔几秒:同一个函数反复出现通常是持续热点,不同调用栈则可能是瞬时任务。
若运行环境已有程序占用 `SIGUSR1`,可在后台容器环境设置 `DEERFLOW_ENABLE_STACK_DUMP=0` 关闭此处理器。额外详细输出可临时设置 `DEERFLOW_PERF_DIAGNOSTICS=1`,只会逐次记录 `options`、`agents`、知识库列表和智能体绑定知识库接口;HTTP 慢请求阈值可通过 `DEERFLOW_SLOW_REQUEST_LOG_MS` 调整,默认 500 ms。修改环境变量后需重启后台。
@@ -15,6 +15,8 @@ This directory contains detailed documentation for the DeerFlow backend.
| Document | Description |
|----------|-------------|
| [ORDINARY_KNOWLEDGE_SMART_REASONING_RETRIEVAL_ZH.md](ORDINARY_KNOWLEDGE_SMART_REASONING_RETRIEVAL_ZH.md) | 普通知识库智能推理检索:管理员数据库开关、原文与 Wiki 首轮双检索、WeKnora 式多轮深读、权限与验收设计 |
| [WIKI_HIERARCHY_AND_FOCUS_GENERATION_ZH.md](WIKI_HIERARCHY_AND_FOCUS_GENERATION_ZH.md) | Wiki 创建时按目录层级生成、按重点要求生成及异步协调设计 |
| [KNOWLEDGE_SPACE_RETRIEVAL_AND_WIKI_EXPANSION_ZH.md](KNOWLEDGE_SPACE_RETRIEVAL_AND_WIKI_EXPANSION_ZH.md) | 知识空间严格优先检索、混合召回、Wiki 图扩展与可编辑全库摘要实现方案 |
| [STREAMING.md](STREAMING.md) | Token-level streaming design: Gateway vs DeerFlowClient paths, `stream_mode` semantics, per-id dedup |
| [FILE_UPLOAD.md](FILE_UPLOAD.md) | File upload functionality |
@@ -0,0 +1,198 @@
# 创建线程延迟优化与业务库 / Checkpoint 分离
日期:2026-10-22。
## 本次实现
适配官方 [#2865](https://github.com/bytedance/deer-flow/pull/2865) 与
[#4230](https://github.com/bytedance/deer-flow/pull/4230),不切换部署数据库。
### 会话检索与总数
`ThreadMetaRepository.search/count` 共享 SQL 条件构建。字符串 / 空值 metadata 过滤和系统线程
排除在 SQLite / PostgreSQL 中先于分页执行;常见 MySQL 字符串任务、智能体和行动过滤
同样下推。无剩余 Python 条件时,search 只发一条分页 SELECT,count 只发一条 COUNT。
这会减少向应用搬运的行数;没有为 JSON 字段自动创建索引,是否还需要表达式索引应由线上
执行计划确定,不能由“已下推 SQL”推断“数据库不再扫描候选行”。
保持原有规则:
- 普通会话仍由 `resolve_user_id` 隔离,get / check_access / 更新 / 删除授权不变。
- 非空 `metadata.taskId` 过滤仍开启任务共享;taskId、agent_id、actionId 全部条件同时生效。
- 默认列表继续隐藏 truthy `system`、非空白 `task_analysis_seat` 和既有系统 thread_type。
- search/count 使用相同条件;先过滤再分页,updated_at 相同按 thread_id 倒序稳定排序。
- 保留本地 `md.get(key) == None` 同时命中缺失键和 JSON null 的语义;数值过滤
保留 Python 的 `True == 1 == 1.0`,字符串数字不与数值混用。
- 嵌套 dict/list、特殊键名、超出 int64 的过滤值继续精确做剩余 Python 过滤,绝不忽略。
独立 openGauss 方言暂留原扫描路径。PG / MySQL 数值(含布尔)过滤也留在兼容路径,避免
DECIMAL / NUMERIC 与 Python 浮点数语义不同或 DOUBLE 舍入大整数改变任务匹配结果;
同一次查询的字符串条件仍可下推。SQLite 数值比较可直接下推。
- MySQL 字符串 id 按二进制比较,避免数据库默认不区分大小写 / 尾随空格的排序规则扩大命中。
使用 MySQL 5.7 可用的 JSON 函数;Python 空白判定通过完整字符替换实现,不依赖 5.7 的
字节正则去判定中文。
### PG ORM 连接池
`database.command_timeout` 默认 30 秒,可设有限正数或 null(关闭命令 deadline)。
只有 asyncpg 驱动接收该参数,MySQL / SQLite / psycopg 不接收。
PG 的 `pool_recycle` 未配置时默认 300 秒;MySQL / openGauss 保留 1800 秒,显式值不变。
初始建连与“数据库不存在、建库后重建 engine”使用相同参数。
```yaml
database:
backend: postgres
postgres_url: $DATABASE_URL
pool_size: 5
max_overflow: 10
pool_timeout: 30
pool_recycle: 300
command_timeout: 30
```
`pool_timeout` 是等待 ORM 池连接的期限,`command_timeout` 是 asyncpg 命令期限。
它们不限制 Python saver 锁排队,也不作用于使用 psycopg_pool 的 LangGraph checkpoint。
业务库已经是 MySQL 时,这个 PG ORM 补丁不会改善独立 PG checkpoint 的等待。
## 先确定部署真实使用的配置
仓库中存在不同配置文件,不能把源码模板当成生产进程最终配置:
- `offline-backend-20260512/config.yaml`:database.backend=mysql,checkpointer.type=postgres,
但 use_postgres=false;未被环境变量覆盖时,实际 checkpoint 回退 SQLite。
- `offline-backend-20260512/backend/config.yaml`:database/checkpointer 均为 SQLite。
- legacy `checkpointer:` 优先于 `database:`,`DEER_FLOW_USE_PG_CHECKPOINTER` 又可覆盖
legacy use_postgres。还应核对启动器选择的配置路径与已发布的后端版本。
现有 `GATEWAY_OPERATION_TIMING operation=thread_create` 日志会输出 metadata_store、
checkpointer 类型和各阶段耗时。默认延迟初始化时 checkpointer=deferred,只有
metadata_lookup / metadata_create;显式立即初始化时才有 checkpoint_create。
先看实际类型是 AsyncPostgresSaver、AsyncSqliteSaver 还是迁移代理,再判断瓶颈属于哪个库。
业务 MySQL 与 checkpoint PG 即使用同一应用,也有独立连接池。
## 创建线程能否只写 MySQL
**仅在创建阶段跳过 PG checkpoint,技术上可行;完整问答只使用 MySQL,当前实现不支持。**
已经实现延迟初始化:`POST /api/threads` 默认只创建业务元数据,不获取/查询/写入
checkpointer。业务库为 MySQL 时,创建阶段只使用 MySQL。metadata 保留 taskId、agent_id、
actionId 等既有分区信息,owner/user_id 仍由服务端控制;幂等重试保持既有记录。
已授权、尚无 checkpoint 的会话读取返回空 state/history;首次 run 由图运行时正常创建
checkpoint,继续使用原持久化、恢复和审计路径。需要旧行为的集成调用方可显式传入
`initialize_checkpoint: true`,等待立即写入空 checkpoint。
`GET /api/threads/{id}` 有业务记录时默认只读业务库,values 为空,running 状态映射 busy。
需要完整图状态请调用 `/state`,或显式 `?include_state=true`。仅有 checkpoint 的旧会话
保留读取兜底;所有路径仍先通过原 owner/project 权限装饰器,任务共享语义不变。
单纯去掉 await 并后台 `aput(empty_checkpoint)` 会与首次 run 竞态,晚到的空 checkpoint
可能成为新 head,不能这样处理。延迟初始化也只是把一部分 checkpoint 成本移到首次 run,
并不保证更快出现首字,必须分别测创建延迟和首字延迟。
完整移除 PG 还需要实现或验证 MySQL checkpointer(当前只有 memory/sqlite/postgres),并迁移
checkpoint/blob/write、兼容父链、恢复、回滚和子图。直接改 backend=mysql 不是此功能。
改用 SQLite 也保留 saver 串行 / 单写者竞争,不能据此承诺多人使用更快。
## 最终 History 能否后台刷新
**已实现不阻塞输入的后台状态同步,使用 SDK 的外部 history 接口。**
原 hook `fetchStateHistory: { limit: 1 }`,`onFinish(state, runMeta)` 使用最终 state。
锁定版本 `@langchain/langgraph-sdk@1.9.28` 的 `stream.lgp.js` 在 onSuccess 中先 await
`history.mutate`,再调用 onFinish,随后才结束 loading。`fetchStateHistory:false` 时,
有参数的 onFinish 仍会要求刷新,并把 getHistory 换成 getState,后者也读取 checkpointer。
因此简单关 history 或给服务端加 BackgroundTasks,不能消除这段等待。
实现细节:
1. 后端主图明确 checkpoint_ns='',完整消费并关闭 alist 后再加工响应,避免序列化继续
占用 saver 实例锁;最新 head 返回完整 channel values,其余条目不重复消息。
2. 前端 `BackgroundHistory` 提供 SDK 外部 history。post-stream mutate 立即提交流式快照,
不等待网络;快照不携带旧 checkpoint id。服务器明确 end 后解除运行中,允许继续输入,
再在后台读取 limit=1 的最终历史;网络 EOF/错误仍使用原终态确认和重连流程。
3. thread id + run id + epoch + request id 共同保护写回。新一轮发送、切线程和卸载会使旧
请求失效,即使 fetch 忽略 abort 也不能回写;新线程采用服务器 id 不会误清空首轮流。
同一轮历史若缺少最新消息或内容尚为早期前缀,也不会覆盖已显示回答。
4. 每次后台读限制 15 秒,完成同步失败最多重试一次(间隔 500ms;401/403/404 不重试)。
失败保留流式正文并提示可继续提问;已有消息不会被清空。正常 end 的业务完成回调立即
使用本轮完整流快照,避免下一轮改变页面上下文而漏掉日志/归档;后台同步不重复回调。
断流恢复可能遗漏最终 SSE,其回调仍等待有效同步,失败时使用保留快照;下一轮正常
发送使用 checkpoint:null 跟随服务端最新 head,避免以旧快照分叉。
`isStateSyncing` 与运行中 loading 分开。EOF 若发生在完整 values 帧之间,保留 SDK
已积累的更新 token 消息,直到终态确认/恢复完成。
后台 history 仍消耗数据库和 saver 锁容量,能改善交互等待,但不能代替解决后端串行瓶颈。
## 登录与业务接口是否查询 PG
登录链路为 `routers/auth.py` → `get_local_provider()` → `LocalAuthProvider` →
`SQLiteUserRepository(sf)`。仓储类名为历史命名,实际 sf 来自
`init_engine_from_config(config.database)`,与 checkpoint 的独立连接池无关。
密码、用户名/口令、token 兑换、本地用户读取、session/token_version 和岗位授权同步
均使用业务仓储;token 兑换另外会请求上游身份接口,岗位同步也可能增加 MySQL 等待,
这些等待不能归因为 PG。CSRF、口令验证、白名单、单会话策略均未更改。
| 路径 | database.backend=mysql 时的数据来源 |
| --- | --- |
| 登录配置中的账号统计、登录/注册、身份查询、用户/岗位权限 | MySQL(策略配置仍可能来自配置文件) |
| 会话创建、搜索/count、已有元数据的线程信息 | MySQL;显式 eager/include_state 除外 |
| 智能体、任务、轻应用、菜单、业务映射等 SQL 仓储 | 共享业务 session factory → MySQL |
| state/history/messages、聊天执行/恢复/删除、从聊天提取资料 | checkpoint(配置为 PG 时访问 PG) |
| AI 写作等图执行的暂停/继续、管理员查看聊天问答明细 | 同属图状态或聊天内容,仍需要 checkpoint |
分库配置示例(不包含实际连接信息):
```yaml
database:
backend: mysql
mysql_url: $MYSQL_DATABASE_URL
checkpointer:
type: postgres
use_postgres: true
connection_string: $CHECKPOINT_POSTGRES_URL
```
本次未修改现有部署连接串、迁移或删除旧表。已有数据库不会因改配置自动迁移,
不能在未迁移业务数据时直接把线上 database.backend 从 postgres 改成 mysql。
尚未完全拆开的边界:LangGraph BaseStore 目前跟随 legacy checkpointer 配置,
旧线程元数据迁移和部分运行时 store 使用它;Gateway 启动也仍等待 checkpoint/store
初始化与历史迁移。因此“已运行服务的业务请求不查询 checkpoint”不等于“PG 故障不影响
Gateway 冷启动”。若需要故障域完全独立,应后续拆运行时初始化/部署并迁移旧 Store,
不能以切换到易失内存或跳过旧数据的方式冒充完成。配置文件与文件型资料也未迁入 MySQL。
## 验证范围
后端相关回归:**133 passed**;前端 `pnpm test:thread-history`:**35 passed**。
从 backend 目录执行(Windows 将 PYTHONPATH 分隔符换为分号):
```bash
PYTHONPATH=packages/harness:. .venv/bin/python -m pytest \
packages/harness/tests/test_thread_metadata_sql_filters.py \
packages/harness/tests/test_database_pool_deadlines.py \
packages/harness/tests/test_thread_operation_timing.py \
packages/harness/tests/test_thread_lazy_checkpoint.py \
packages/harness/tests/test_run_completion_timeout.py \
packages/harness/tests/test_run_stream_response_headers.py \
tests/test_account_authentication.py \
tests/test_gauss_database.py tests/test_ensure_orm_columns_sync.py -q
```
- 新增 SQLite 实际查询回归:标量 / 缺失值 / 嵌套兼容、系统排除、共享任务、agent/action
分区、普通用户隔离与修改权限、分页、标题/状态过滤,以及 1200 条噪声下只发单条查询。
- 新增 PG/MySQL SQL 编译与参数绑定、MySQL 二进制 id 比较、Gauss/服务端数值兼容回归。
- 新增 ORM 真实初始化路径的参数捕获:初次 / 建库重试、null 和自定义命令超时、MySQL /
SQLite 参数隔离、配置校验。
- HTTP + SQL + SQLite saver 验证持锁时 20 个创建请求完成、首轮真实 LangGraph 初始化与
第二轮续聊、元数据空线程读写、私有/任务共享权限、history 释放锁和取消关闭迭代器。
- 登录配置/密码登录/用户名口令登录/token 兑换/身份/用户列表使用实际 SQL 用户仓储,并安装
禁止访问 checkpoint 的桩,验证这些请求不会走聊天后端。
- 前端含实际安装 SDK 的 SSE 解码和 StreamManager 集成,验证 history 挂起不阻塞结束、
旧响应/新轮次/切线程/卸载隔离、异常/EOF恢复、超时/重试、旧副本保护和流式状态保留。
- 修改文件 Ruff 与 `git diff --check` 通过。前端全量 `pnpm typecheck` 仍有 81 条既有
类型错误;与 HEAD 基线比对并修正新增测试的类型后,本次改动文件无新增报错。
- 本机 Docker Linux daemon 未运行,未以真实 PG/MySQL 服务做集成或负载验证;不把 SQLite
查询与驱动配置测试当作线上延迟改善比例。发布前仍应在对应版本数据库做隔离集成验证。
- 额外尝试收集本机原有 `tests/test_gauss_mysql_compat.py` 时,因仓库缺少其导入的
`deerflow.persistence.gauss_mysql_compat` 模块失败;该本机测试文件未列入上述 133 项,
未为其添加与本次补丁无关的兼容模块。
@@ -0,0 +1,253 @@
# Wiki 知识库参考目录与重点要求生成设计
> 状态:已实现,与当前代码一致
> 更新日期:2026-10-19
## 1. 目标与语义边界
创建或编辑 Wiki 知识库时提供两个互相独立的选填项:
- **参考目录**:管理员或知识库所有者给出一至三级目录,帮助生成模型理解期望的信息组织方式。它不是目录白名单,也不是必须逐项套用的固定模板。
- **重点要求**:用户描述允许抽取、组织和呈现的重点范围。系统把它作为严格的内容范围约束,并在生成完成后清理重点范围之外的自动生成页面。
模型可以依据原始资料新增、合并、拆分、改名、调整或省略参考目录。内容与参考目录冲突时,以资料实际内容和清晰表达为准。系统不得为了匹配参考目录预建空目录、强制移动页面、删除目录或删除参考目录之外的有效结构。
两项均可留空。全部留空时沿用 WeKnora 原有自动生成方式;原有 `content_instructions` 和 `extraction_instructions` 高级指令继续有效。本功能的权威配置全部保存在数据库,不写入 `config.yaml`、JSON 或其他运行时配置文件。
## 2. 用户交互
### 2.1 参考目录
前端支持两种输入格式,最多三级,不能混用。
Markdown 标题:
```markdown
# 政策法规
## 国内政策
### 2026 年
## 国际规则
# 操作指南
## 申报流程
```
每级两个空格的缩进文本:
```text
政策法规
国内政策
2026 年
国际规则
操作指南
申报流程
```
无缩进的普通逐行文本按一级参考目录处理。用户也可以下载示例文件,修改后导入 `.md` 或 `.txt` 文件。表单实时显示树形预览、节点数、规范化长度和逐行错误,并明确提示模型可以按实际资料调整结构。
### 2.2 重点要求
重点要求是最多 2000 个字符的自由文本,例如:
```text
重点提取适用对象、申请条件、时间期限、审批部门和例外情形;
涉及不同年份时优先说明最新规则,并保留历史差异。
```
重点要求限制自动生成页面的内容范围,同时不允许模型降低事实依据要求或凭空补全资料。只填写重点、不填写参考目录时,系统保留 WeKnora 自动生成的目录结构,只过滤与重点无关的自动生成页面。
### 2.3 编辑语义
- 两项都支持创建后修改。
- 清空参考目录只表示不再向后续生成提供该参考,不删除或移动已经存在的远端目录和页面。
- 清空重点要求后,后续生成不再追加重点范围提示,也不再运行重点页面过滤。
- 保存时只更新请求中明确出现的 Wiki 配置字段,未出现字段保持原值。
### 2.4 生成后的智能检索与查看
参考目录和重点要求只影响 Wiki 的生成与整理,不要求用户在问答时重复这些指令。管理员开启普通知识库智能检索、且用户在会话中选择该模式后,用户可以直接提问;运行时先检索原始文档分片,再根据原始命中检索 Wiki 并读取相关页面,之后只针对证据缺口扩展检索。Wiki 与原始文档冲突时以原文为准。最终回答不强制输出 `[1]`、`[2]` 等编号或编号参考文献列表。
智能检索过程在执行时展开,完成后默认收起。用户展开后可以点击带稳定来源标识的 Wiki 页面或原始文档结果,在聊天页右侧来源抽屉查看详情。详情读取仍按当前用户和本地 mapping 重新鉴权。
## 3. API 合同
创建接口:
```http
POST /api/llmwiki/knowledge-bases
```
编辑接口:
```http
PATCH /api/llmwiki/knowledge-bases/{mapping_id}
```
两者的 `wiki_config` 使用同一结构:
```json
{
"content_instructions": "原有内容生成要求,可选",
"extraction_instructions": "原有信息抽取要求,可选",
"directory_paths": [
["政策法规"],
["政策法规", "国内政策"],
["政策法规", "国内政策", "2026 年"]
],
"focus_instructions": "重点说明适用对象和截止时间"
}
```
`parent_directories` 作为旧版一级目录兼容字段继续接受。无论使用 `directory_paths` 还是 `parent_directories`,目录都只是生成提示中的参考结构,不能触发远端目录预建或生成后目录整理。若同一请求同时提供两个字段,两者的根目录必须完全一致;新客户端统一使用 `directory_paths`。
知识库详情响应中的 `wiki_config` 返回:
- 原始内容指令、原始抽取指令;
- `directory_paths`、兼容的 `parent_directories`;
- `focus_instructions`;
- 配置 schema、revision 和应用状态;
- 参考目录提示写入 WeKnora 指令的覆盖状态及节点计数。
## 4. 参考目录规范化与校验
前后端执行一致的基础校验,后端是最终安全边界:
| 规则 | 限制 |
|---|---:|
| 最大深度 | 3 级 |
| 最大节点数 | 100 |
| 单个目录名 | 100 个字符 |
| 规范化目录总长度 | 6000 个字符 |
| 重点要求 | 2000 个字符 |
目录名称先执行 Unicode NFKC 规范化和首尾空白清理,并拒绝空名称、`.`、`..`、路径分隔字符、控制字符、同一父目录下的等价重复名称、跨级跳转以及非法缩进。后端将缺失的父路径自动补齐,并按父节点先于子节点的稳定顺序保存完整路径集合。
这些规则只保证参考数据能够稳定存储并安全写入提示词,不把参考目录变成业务上的硬约束。
## 5. 有效提示编译
内容生成指令依次合并:
1. 用户原始 `content_instructions`;
2. 重点要求及“只生成直接相关且有资料支撑页面”的严格内容范围;
3. 参考目录树及“可以按资料新增、合并、拆分、改名、调整或省略目录”的软性组织指引。
信息抽取指令依次合并:
1. 用户原始 `extraction_instructions`;
2. 重点要求及“只抽取直接相关、可追溯事实”的严格内容范围。
原始指令和重点要求不会被静默截断。基础内容超过 4000 字符时返回 422。参考目录提示按父子顺序装入剩余预算;无法放入全部节点时,详情响应返回 `partial` 覆盖和实际节点数。未写入的节点仍保存在数据库中,但不执行任何本地目录补建或强制归档。
## 6. 生成与生成后处理
### 6.1 创建和编辑时不操作目录
创建或编辑知识库时,Gateway 只把编译后的提示传给 WeKnora,并把原始配置保存到本地数据库。无论输入来自 `directory_paths` 还是兼容字段 `parent_directories`,都不得:
- 预建参考目录骨架;
- 因参考目录变更而移动 Wiki 页面或文件夹;
- 删除参考目录之外的文件夹、目录页或普通页面;
- 周期性扫描并强制恢复参考目录。
WeKnora 生成出的目录结构就是当前资料对应的有效结构,可在后续生成中自然调整。
### 6.2 重点范围过滤
只有非空的重点要求需要生成后协调。WeKnora 的知识库级 `is_processing` 可能早于 Wiki 生成结束变为 `false`,因此协调器还会检查每篇文档的 `pending_subtasks_count` 和 finalizing/processing 状态;确认全部结束后再执行重点判定。
重点判定遵循以下规则:
- 页面主体直接提供重点所需的事实、指标、事件、关系、责任或实施细节时保留;
- 只作为背景出现、与重点无关的人物、机构、地点或主题自动生成页面可以删除;
- 人工维护页不因重点过滤被删除;
- 判定按有界批次执行,漏项会补判,不能把“没有返回”解释成删除;
- 肯定决定可按配置签名和正文指纹写入页面元数据,供相同内容重跑复用;
- 所有判定完成前不做删除;判定缺项、格式非法、标记写入失败或模型不可用时,本轮不做破坏性变更;
- 重点过滤始终保留 WeKnora 已生成的目录结构,不移动页面,不删除文件夹或目录页。
参考目录本身不触发 mutation-settle 或周期协调;只有重点过滤和本地 Wiki 索引同步按各自条件调度。
## 7. 数据库存储
迁移 `20261014_02_llmwiki_generation_config.py` 在 `llmwiki_knowledge_bases` 增加:
| 字段 | 用途 |
|---|---|
| `wiki_directory_paths` | 完整一至三级参考目录路径数组 |
| `wiki_focus_instructions` | 原始重点要求 |
| `wiki_content_instructions_raw` | 原始内容生成要求 |
| `wiki_extraction_instructions_raw` | 原始信息抽取要求 |
| `wiki_generation_managed` | 是否由当前 Wiki 配置流程管理 |
| `wiki_config_schema_version` | 当前 schema 版本;参考目录语义为版本 3 |
| `wiki_config_revision` | 每次有效配置变更递增 |
| `wiki_config_apply_status` | `active` 或 `not_applicable` |
| `wiki_config_apply_error` | 保留的应用错误字段 |
旧字段 `wiki_parent_directories` 继续保存根目录投影,兼容既有数据和旧客户端,但不保留旧的强制目录行为。SQL、内存存储与映射响应使用相同字段语义。上述生成选项、原始指令、应用状态和 revision 均以数据库记录为权威来源;前端本地状态只用于未提交表单。
后台协调器会幂等升级版本 2 的受管 Wiki:从数据库保存的原始指令、重点要求和参考目录重新编译有效提示,远端更新成功后再写入版本 3 并增加 revision;失败时保留版本 2、记录错误并稍后重试。未知版本或缺少原始配置的旧记录不会被自动覆盖。
页面级重点保留判定存放在 WeKnora 的数据库页元数据中,不使用本地文件。它只缓存某一重点版本和正文指纹下的肯定决定,不替代管理员原始配置。
## 8. 一致性和失败处理
### 8.1 创建
创建顺序为:校验与编译、创建 WeKnora 知识库、写入本地映射。参考目录不增加远端目录初始化步骤。WeKnora 创建成功但数据库写入失败时,系统尝试删除新建的远端知识库,避免留下无本地映射的孤儿库。
### 8.2 编辑
编辑前读取当前远端配置和数据库原始配置:
- 参考目录或指令校验失败:不修改远端和数据库;
- WeKnora 更新失败:数据库保持原值;
- WeKnora 更新成功但数据库提交失败:尝试恢复 WeKnora 之前的名称、描述和两段有效指令,然后把错误返回调用方。
目录参考的保存失败不会留下预建目录,因为该流程不调用远端目录创建接口。
### 8.3 权限
沿用知识库原有授权边界:只有知识库所有者或管理员可创建和修改配置;公开浏览接口只返回调用者有权查看的内容。
## 9. 主要实现位置
后端:
- `app/gateway/wiki_generation_config.py`:重点规范化、参考目录提示与有效指令编译;
- `app/gateway/wiki_directory_constraints.py`:输入规范化和重点范围过滤,不负责强制目录整理;
- `app/gateway/routers/llmwiki.py`:创建、编辑、响应投影和补偿事务;
- `app/gateway/llmwiki_index_scheduler.py`:重点过滤与本地 Wiki 索引的异步协调;
- `packages/harness/deerflow/persistence/llmwiki/`:数据库和内存存储;
- `packages/harness/deerflow/persistence/migrations/versions/20261014_02_llmwiki_generation_config.py`:数据库迁移。
前端:
- `src/core/llmwiki/directory-tree.ts`:参考目录文本解析、校验和格式化;
- `src/strategy-components/api/llmwiki.ts`:API 类型和请求;
- `src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeWorkspace.tsx`:创建、编辑、示例下载、文件导入和树预览。
## 10. 测试与验收
自动化至少覆盖:
- Markdown、缩进文本、路径规范化、重复和越界校验;
- 重点指令、参考目录和原始高级指令的合并、4000 字符预算和目录提示覆盖计数;
- 创建与编辑会持久化参考目录,但不调用远端文件夹创建、页面移动或目录删除接口;
- 新字段和兼容字段都不会启动目录协调;
- 重点页面过滤、失败前不破坏及幂等重跑;
- 编辑时远端失败不写本地,数据库失败恢复远端指令;
- SQL/内存存储字段往返、迁移单头和迁移兼容性;
- 前端参考目录解析、示例文件和生产构建。
验收时至少使用下列组合:
1. 两项均为空,行为与原有 Wiki 创建一致;
2. 只给参考目录,模型可以采用、补充、合并或调整目录,系统不创建固定骨架,也不移动或删除生成结构;
3. 只给重点要求,只保留重点范围内且有来源依据的自动生成页面,同时目录保持不变;
4. 两项同时填写,参考目录提供组织方向,重点范围独立生效;
5. 使用兼容字段 `parent_directories` 创建或编辑,仍然只有参考语义;
6. 编辑后 revision 增加,刷新页面仍从数据库恢复原始配置;
7. 清空参考目录后不再向后续生成提供参考,已有目录和内容不被删除或移动;
8. 重启服务或更换浏览器后,参考目录、重点要求、原始指令和 revision 仍从数据库恢复;
9. 生成后的 Wiki 页面可进入智能检索,并可在右侧抽屉查看来源详情;
10. 文档 Wiki 子任务尚未结束时不提前运行重点过滤,全部结束后才清理重点范围外的自动生成页面。
@@ -370,7 +370,8 @@ client.get_memory()
| POST | `/api/threads/{id}/runs/{rid}/cancel` | 取消运行 |
| GET | `/api/threads/{id}/runs/{rid}/join` | 加入 SSE 流 |
| GET | `/api/threads/{id}/runs/{rid}/messages` | 分页获取消息 |
| GET | `/api/threads/{id}/messages` | 线程所有消息(含反馈) |
| GET | `/api/threads/{id}/messages` | 从最新 checkpoint 读取按时间正序的完整消息字段 |
| GET | `/api/threads/{id}/run-messages` | 跨运行读取 event-store 展示消息(含反馈) |
### 8.3 无状态运行
@@ -2,6 +2,25 @@
DeerFlow includes automatic conversation summarization to handle long conversations that approach model token limits. When enabled, the system automatically condenses older messages while preserving recent context.
## Administrator recent-three-turn window
Administrators can enable a simpler deployment-wide sliding window in
「设置 → 上下文管理」. It is stored as
`system_settings.json -> conversation_context.recent_turn_window_enabled`, is
disabled by default, and is exposed through
`GET/PUT /api/system-settings/conversation-context` (write access is admin-only).
When enabled, every lead-agent model call receives only the latest three real
user turns, counting the current request, plus the assistant messages and tool
results that belong to those turns. Hidden business context immediately attached
to the first retained turn and system instructions are preserved. Trimming is
performed on `ModelRequest` only, so checkpoint history and the transcript shown
to the user remain complete.
This mode takes priority over the per-chat 「上下文压缩」 option and does not run
the summarization model. Turning it off restores the existing summarization
behavior without losing earlier messages.
## Hard input limit (always-on safety net)
Summarization is a quality feature and may be disabled by the user. A model can