Compare commits

...
1184 Commits
Author SHA1 Message Date
gozeros b8ad8fda94 sync: complete remaining files (source ji) 2026-10-21 15:15:16 +08:00
gozeros e602d88695 sync update from LAN repo (source ji @ 85078a31) 2026-10-21 15:09:16 +08:00
Administrator 5d6627af50 feat: 完善策略优选项目工作流 2026-09-09 20:49:59 +08:00
Administrator a26fd32eba feat: add unified strategy project workflow 2026-09-09 15:40:29 +08:00
Administrator 67d12929fc Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	offline-backend-20260512/backend/app/gateway/app.py
2026-09-09 10:00:24 +08:00
Administrator 5fd2e8302c feat: add strategy selection workbench 2026-09-09 01:08:11 +08:00
董宏凯 4f7f07504a Merge branch 'master-nj' into 'master'
fix(buttons): 行动分析宿主路由支持切换任务id/行动id

See merge request knowledge-base-bj/deerflow-code!39
2026-09-07 09:53:09 +00:00
董宏凯 1244fb1f2f 代码提交 2026-09-07 17:30:53 +08:00
吉鑫升 3ca5528640 feat(knowledge): support standalone file ingestion and configurable wiki entry
Use neutral knowledge-service labels and sanitize user-facing task errors.
2026-09-07 17:07:27 +08:00
董宏凯 250db4de04 fix(buttons): 行动分析宿主路由支持切换任务id/行动id
- TaskButtonsPage: 宿主路由追加任务id时,行动分析(xdfx) tab 下增加 idKind 下拉框可选任务id/行动id,与外部链接类型一致
- runTaskButton: host 分支按 idKind 取 ctx.actionId 或 ctx.taskId(行动id缺失回退任务id),与 url 分支同规则
- AGENTS.md: 同步更新 idKind 适用范围说明(url/host 均适用)
2026-09-07 15:56:50 +08:00
董宏凯 f9b6d7e322 Merge branch 'master-nj' into 'master'
feat: 新增报告协作页,并强化 Deep Research 运行诊断与报告兜底

See merge request knowledge-base-bj/deerflow-code!38
2026-09-07 06:58:42 +00:00
董宏凯 6edb6fa7aa 本地模式线上区分-v1 2026-09-07 14:57:17 +08:00
董宏凯 cf9fe58905 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
#	frontend-web/src/pages/WorkspaceRoutes.tsx
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/app/gateway/deps.py
#	offline-backend-20260512/backend/pyproject.toml
2026-09-07 14:46:36 +08:00
董宏凯 56bb4f141d 代码提交 2026-09-06 22:17:53 +08:00
董宏凯 75f288b241 代码提交 2026-09-06 20:49:35 +08:00
董宏凯 bf42a48fcc 代码提交 2026-09-06 20:25:14 +08:00
董宏凯andCursor 5e953c9fcb feat(report-collaboration): complete RC-BE-015–018 backend, eval suite, and MySQL sync
Ship report rewrite/version APIs, security budget and audit persistence, AgentScope worker wiring, and the 20-task quality eval harness so the frontend can integrate against a stable contract once enabled in config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:32:15 +08:00
董宏凯 814a44f0b9 代码修改 2026-09-05 11:28:25 +08:00
董宏凯 b80433f091 chore: 同步仓库执行说明与约束文档
功能说明:\n- 更新仓库根目录 AGENTS.md 为当前约定和功能说明口径\n- 主要是开发约束与架构说明的同步,供后续提交与协作流程对齐
2026-09-04 23:18:59 +08:00
董宏凯 5c75e1af16 feat(deep-research): 强化任务链路可靠性并接入 agentfx 报告助手种子
功能说明:\n- 重构 Deep Research 调度与执行链,补齐 dispatcher、executor、live hub 与依赖注入入口,提升任务下发、执行、回调的稳定性\n- 增强运行时持久化与恢复能力,接入业务映射、任务来源与任务事件日志模型,覆盖持久化与执行可靠性关键路径测试\n- 新增 agentfx 报告助手 seed 与任务报告导入脚本配置,支持离线环境快速初始化与任务流模板回填\n- 完善 deep-research 与 thread_runs 路由参数编排,兼容不同业务映射与运行入口的前后端联动\n- 补充前端 agentfx deep-link 解析,保证 deep-research 场景下的登录跳转与任务上下文一致
2026-09-04 23:18:52 +08:00
董宏凯 438772f0b7 feat(frontend): 上线报告协作与多轮 RAG 消息聚合体验
功能说明:\n- 新增报告协作页(Report Collaboration)核心状态层、消息适配与会话面板,支持按节点/阶段选择和协作上下文展示\n- 补齐 report-collaboration API 与类型定义,并接入页面路由/构建路径,形成可运行的协作链路\n- 重构多轮 RAG 与 write/artefact 消息分组逻辑,优化 AgentChat 与 DeepResearch 场景下的消息展示顺序与可读性\n- 新增/更新 DeepResearch 前端页面(Workbench、报告配置、沙箱联动)以承接报告生成与复用能力\n- 增加报表协作相关单测,覆盖事件归并、消息适配与关键 reducer 行为
2026-09-04 23:18:27 +08:00
吉鑫升 dea4697ea0 feat: improve knowledge sync and entity alignment 2026-09-04 18:43:12 +08:00
g zy 7dbac9cc56 Merge branch 'gzy_browser' into 'master'
fix

See merge request knowledge-base-bj/deerflow-code!37
2026-09-04 03:21:48 +00:00
Rocky Kwok a723a9ca02 fix 2026-09-04 11:16:38 +08:00
董宏凯 e58da3fbdf 代码提交 2026-09-03 20:06:54 +08:00
吉鑫升 60e2c72514 feat: preserve Wiki vectors across knowledge transfers and add local BGE-M3
Unify Wiki reading, preserve metadata and graph links, add asynchronous file-backed transfers, and reuse compatible vectors. Includes regression coverage and live acceptance notes.
2026-09-03 19:21:10 +08:00
董宏凯 1eae02f782 代码提交 2026-09-03 17:45:06 +08:00
董宏凯andCursor db333a4c33 feat: 重构多轮RAG展示并完善报告协作、业务链审核与 agentfx 种子
抽出专用多轮RAG消息列表并升级会话模型;强化报告协作画布与检查器;业务链普通用户改为提交审核;扩展 agentfx 报告技能种子,并将含 admin 的用户名自动提权为管理员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 17:34:16 +08:00
董宏凯andCursor f109a95535 feat: 新增报告协作页,并强化 Deep Research 运行诊断与报告兜底
支持浏览器侧素材快照撰写、拒绝报告读时修复、Mermaid PNG 导出与 Word 导出加固。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 11:16:50 +08:00
吉鑫升 49d9ced04f Merge branch 'ji' into master 2026-09-03 10:25:57 +08:00
董宏凯 c2153ec238 Merge branch 'master-nj' into 'master'
feat(workflow): 增加会话级多智能体任务规划运行能力

See merge request knowledge-base-bj/deerflow-code!36
2026-09-02 09:55:59 +00:00
吉鑫升 6b724bd00d feat: improve assistant knowledge import and database compatibility 2026-09-02 17:55:17 +08:00
董宏凯 65d6b61d5d 开发计划 2026-09-02 14:57:34 +08:00
董宏凯 1c891df8fb feat(backend): 新增页面报告设计器 Seed 与模板工具
功能说明:\n- 新增页面报告设计器的路由级 seed 接口与初始化模板资源加载能力\n- 新增内置页面报告模板工具,实现报告生成流程中的模板路径与参数接入\n- 补充页面报告模板与相关资产目录,供运行时/离线环境直接读取\n- 将页面报告配置与工具链纳入后端启动路径,支持离线部署下可复用的默认模板
2026-09-02 12:52:40 +08:00
董宏凯 6d326a21cd feat(backend): 完善 Workflow 与 DeepResearch 的运行链路与持久化能力
功能说明:\n- 重构并增强工作流网关执行链路,支持更完整的 proposal 与 run 调度参数透传\n- 增加 DeepResearch/workflow 运行事件持久化能力并补齐建表 SQL 与查询/保存接口适配\n- 优化 lead agent 与 deep research runner 的中间态处理,增强线程收集与执行链并发控制\n- 调整工作流执行器与任务路由,提升兼容性并完善错误与回放行为\n- 补充回归测试:deep_research 持久化、workflow 并发、定义加载、引擎执行、run 流程等覆盖
2026-09-02 12:52:37 +08:00
董宏凯 f5bf7897b9 feat(frontend): 支持 Roundtable 写文件流式预览与消息流聚合展示
功能说明:\n- 为写文件工具链增加流式预览状态模型(in-flight、chunk 归并、已完成)\n- 在消息面板中支持 write_file 流式 artifact 的动态插入/更新与完成态呈现\n- 优化 artifact 详情页的文件名与来源显示,兼容流式生成中文件路径\n- 在 Roundtable 工作流 step2 流程中注入与监听 write_file 事件,支持多阶段消息统一展示\n- 补充单测覆盖:streaming-write-preview、write-file-in-flight、streaming-artifacts 等关键路径
2026-09-02 12:52:33 +08:00
Administrator fbd8dec48f feat: add skill and assistant knowledge bases 2026-09-02 00:49:13 +08:00
吉鑫升 dd0ee282df feat: add artifact library and improve public wiki 2026-09-01 18:18:28 +08:00
董宏凯 553f72604a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-09-01 15:36:47 +08:00
董宏凯 9455160101 fix(prompt-input): 提供可选上下文入口并避免历史路由切换期异常 2026-09-01 14:51:21 +08:00
董宏凯 519e4c0c01 feat(chat): 修复 Strategy/Workspace 统一聊天布局与历史路由归属 2026-09-01 14:51:16 +08:00
董宏凯 dbf62e5889 feat(deep-research): 优化深度研究侧边栏与结构配置弹窗的暗色主题适配
为深度研究沙箱预览区和工作流配置弹窗补充暗色下背景与边框配色,避免在深色模式出现对比度不足和样式不一致。
2026-09-01 13:47:39 +08:00
董宏凯 aeb9a55497 feat(agent-chat): 修复智能体会话归属与历史路由恢复:侧边栏按当前agent过滤会话并携带agent上下文跳转,补充旧线程路径回溯到智能体工作区,防止误入通用问答,并完善输入框记忆按钮显隐与侧边栏展开入口,提升体验一致性。 2026-09-01 13:47:31 +08:00
董宏凯 051e66f7d8 feat(chat): 优化聊天页布局与 iframe 工具栏行为 2026-09-01 12:49:03 +08:00
董宏凯 1706a24182 feat(workflow): 增加会话级多智能体任务规划运行能力
提交范围聚焦于工作流编排新增:新增 workflow_planning 路由与前后端运行入口(workflow_planning.py、workflow_proposal_planner.py、workflow_deep_research_adapter.py),支持基于会话的提案-规划-执行流程,满足任务驱动式多智能体协作。

完成网关层运行时适配:扩展 workflow_resource_checks、workflow_executor、workflow_runs、workflow_agent_runner 与依赖注入,新增/更新配置(config.yaml、workflow_config),并补充 lead_agent 路径与节点实现,确保 Planner 与普通工作流运行态衔接一致。

补齐持久化与建模:新增 workflow_planning 持久化模块与 MySQL migration(20260831_01_workflow_planning_sessions),注册 SQL 与模型、内存 store,并扩展错误/表达式/validator/节点语义以承载新会话态输入输出。

增强文档与测试:新增会话式多智能体编排中文说明、更新开发文档与 AGENTS 指南、补充 workflow_engine/workflow_runs 回归测试,提高新路径的行为可验证性;同时留存 seed 资产(workflow-planner)便于一体化部署。
2026-09-01 11:47:30 +08:00
董宏凯 38857c425b 代码提交 2026-09-01 11:09:23 +08:00
董宏凯 99f1ca6029 Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code 2026-08-31 23:21:32 +08:00
董宏凯 a020458eeb Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code 2026-08-31 23:21:00 +08:00
董宏凯 8e49fd2e95 代码分析 2026-08-31 23:20:48 +08:00
fengxiangdebaozi 57b6a89f20 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
# Conflicts:
#	frontend-web/src/components/workspace/messages/message-group.tsx
#	offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py
2026-08-31 20:12:05 +08:00
吉鑫升 90e94994f5 Merge remote-tracking branch 'origin/master' into master 2026-08-31 18:06:10 +08:00
吉鑫升 8e0d755b57 fix: preserve knowledge base references 2026-08-31 18:02:05 +08:00
吉鑫升 40542db400 feat: add configurable chat display modes 2026-08-31 17:49:48 +08:00
董宏凯 ac92f29db1 chore(persistence): 增加 Workflow Studio 表重建开关与迁移兼容修正
提交内容:为数据库生命周期增加按白名单清单重建 Workflow Studio 表的受限流程,限定仅 MySQL 且检查当前连接数据库名称一致,避免误删非目标 schema。

启动流程中新增 DEER_FLOW_REBUILD_WORKFLOW_SCHEMA 环境变量入口与持久层重建 API,并在 lifespan 里按需执行;导出到 persistence 包,便于后续调用与测试复用。

迁移与模型修复:将 workflow 相关大文本字段切到 MySQL LONGTEXT,不再给 TEXT/LONGTEXT 设置 server_default;新增 draft_canvas_schema_json 的迁移回填与非空约束变更,规避 MySQL 5.7 对默认值限制。

测试覆盖:补充 workflow 定义 ddl 长文本断言与 schema 重建保护性测试(仅允许清单表、数据库名不匹配直接拒绝),并把新测试文件加入版本追踪。
2026-08-31 16:45:06 +08:00
董宏凯 f0ddade8a7 feat(roundtable): 增强多智能体 Step2 流式推理与产物展示
提交内容:将 roundtable 的流式消息拆分为文本与 think 推理,支持思考内容以 <think> 形式内联渲染,解决仅返回 reasoning token 时的内容空洞问题。新增 stream-reasoning 工具与单元测试,保证 reasoning 首包到正文的闭合行为稳定。

交互与预览修正:write_file/str_replace 工具的沙箱打开策略改为更接近实时席位行为,不再因为文件后缀限制阻塞临时脚本写入可视化;通过新增 streaming-artifacts 工具统一处理可预览路径、最终交付产物过滤与目录快照比对,支持 bash/脚本直接落盘场景在流式阶段自动发现新增文档。

体验细节:步骤面板文件卡片从工具路径与最终 artifact 统一收敛,避免 read_file 点击误触发;自动滚动逻辑新增布局高度抖动识别,防止模型推理渲染导致的误判向上滚动。
2026-08-31 16:45:01 +08:00
董宏凯 fbf9031e25 feat(会商第三步): 恢复普通会商完整成果并隔离8BF报告模式
- 为普通多智能体会商恢复报告完成后的结构化流程图、最终方案展示、经典结果绘制和大屏多页,并保持三个生成流独立并行。

 - 8BF 入口显式采用 report-only 模式:只生成和编辑 Markdown 总结报告,不再启动或露出流程图、结果绘制及大屏功能。

 - 结构化流程图增加抽取、校验、暂停、失败重试和歧义选择交互;报告完成后状态文案准确切换,自动滚动定位到抽取与校验进度。

 - 为总结报告、经典 HTML 绘制和大屏虚拟 write_file 补齐完成结果及历史恢复处理,避免并行分支或旧草稿让完成产物误显示为生成中。

 - 最终方案展示中的已落盘文件直接按完成态预览,并恢复 MD、HTML 等文件类型徽标,不再被提交按钮覆盖。
2026-08-30 21:24:47 +08:00
董宏凯 2254841822 fix(会商第二步): 恢复总控派活实时展示并修正沙箱预览状态
- 流式解析改为跟踪当前 tool call id,连续多次 agent_orchestration 派活时前端即时展示最新席位和任务,不再等模型完整回复后一次性出现。

 - 总控派活步骤按 tool call 独立归档并合并增量参数,避免同名工具调用互相覆盖或只显示半截任务。

 - 沙箱自动打开改为按单个产物预览键管理:用户关闭当前文件后不反复弹出,但后续新文件仍可自动预览。

 - 为圆桌虚拟 write_file 补齐工具完成结果,并在席位收尾和历史草稿恢复时兜底收口,防止完成文件长期显示“正在生成”。
2026-08-30 21:24:30 +08:00
董宏凯 74fc849241 fix(会商滚动): 让第二、三步新消息与结构化进度即时定位到底部
- 为圆桌滚动钩子增加强制落底能力,阶段切换和布局变化不再被平滑滚动的旧高度截断。\n- 第二步用户发送消息后立即下滑到底部,不再等待模型首个输出。\n- 经典结果绘制与大屏对话面板接入内容高度观察,新增气泡、Markdown 和工具卡继续增高时保持跟随。\n- 保留用户手动上翻时的正常阅读行为,仅在新一轮发起或关键阶段切换时强制定位。
2026-08-30 21:24:08 +08:00
董宏凯 dd560ceb46 fix: 会商沙箱预览头仅在可提交的 Markdown 产物显示提交按钮 2026-08-30 20:02:21 +08:00
董宏凯 a4f95a4fa9 fix: 会商第一步隐藏继续追问与跳过此项按钮 2026-08-30 20:02:21 +08:00
董宏凯 b92a26cdd4 fix: 会商第二步只对可预览成品出文件卡/开沙箱,去掉干预并发送按钮 2026-08-30 20:02:21 +08:00
董宏凯 7066798ab0 chore: 配置新增 qwen3.8-flash 模型 2026-08-30 20:02:15 +08:00
董宏凯 222595d6c4 feat: 工作流画布支持草稿试运行(target=draft 执行当前草稿图) 2026-08-30 20:02:15 +08:00
董宏凯 3384f2e51c fix: 会商席位与总控角色隔离,编排能力只归总控 2026-08-30 20:02:09 +08:00
董宏凯 ff80cc110d fix: 写作管线步骤条改用不涉及检索的文案
- 「拆解检索问题」→「梳理写作思路」(已梳理出 N 个论述角度)
- 「检索参考资料」→「整理对话资料」(已整理 1/2 个论述角度的
  资料 · 累计 N 条)
- 管线实际只汇总对话已收集的资料再成稿,原文案会让用户误以为
  管线在重新检索;测试补标签断言防止回退
2026-08-30 16:21:02 +08:00
董宏凯 2ab3ab8444 fix: 写作管线整批接收对话检索结果,不再自己检索
- 相关性过滤新增位置通道:收割器给每条材料标记来源消息位置,课题
  请求之后的检索结果整批入池(与语言/词面无关)——线上中文课题 +
  英文检索结果被纯词面匹配误杀 31/35 条,报告只带着 4 条残料在写
- 课题切换保护保留:新请求之前的旧课题检索仍按词面强过滤,不凑
  门槛、不入报告;对话里找不到课题消息时退化为原词面过滤
- 删除死代码 HarvestPlusSearchMaterialProvider(retrieve=True 的
  自检索补充分支,早已被显式忽略):写作管线只负责写文件,检索
  全部由对话完成成为结构保证
- 测试:锚点规则(澄清回答不充当新课题)、中文课题+英文资料整批
  保留、课题切换旧检索过滤等场景,25 个用例入库(此前该测试文件
  一直被 tests/* 忽略规则挡在版本库外)
2026-08-30 16:20:55 +08:00
董宏凯 f7e6ed65bd fix: retry httpx timeout family as transient LLM errors
Live incident: the compatible-mode provider stalled mid-stream after
200 OK (~17s without data) and raised httpx.ReadTimeout, which was not
in the retriable exception-name set, so the call failed after 1
attempt and killed the whole run. Add ReadTimeout/ConnectTimeout/
WriteTimeout/PoolTimeout/TimeoutError/ConnectError to the transient
set in LLMErrorHandlingMiddleware (retry with backoff, then a
transient fallback message instead of generic), and add ReadTimeout/
ConnectTimeout to ai_writing's classify_exception for model
switching.
2026-08-30 15:53:51 +08:00
董宏凯 4964967295 fix: writing mode first-report allows only the pipeline to write the report
- Strip write_file/str_replace/bash from the model binding in the
  first-report run: after_model rewrites only fire after a full model
  response, so any bound write tool let weak models draft the whole
  essay into tool args (write_file content or bash cat>/python
  heredocs) for tens of invisible seconds before interception
- Hallucinated hand-writes are still handled: after_model rewrites
  bash cat >/python open('w')/tee and leftover write calls to
  deep_research_report; wrap_tool_call rejects whatever reaches the
  tool layer; post-success writes are dropped, nudged to a 2-3
  sentence summary, and patched with a fallback closing message so
  the turn never ends silently
- The rewritten pipeline call carries topic = original request
  (clarification answers skipped) plus focus = clarification answers,
  so the pipeline writes the clarified subject instead of the raw
  prompt text; prompts updated to match (tools unavailable, call the
  tool directly, do not draft first)
- Frontend write-step interception and streaming preview scoping now
  applies only in writing mode; normal mode shows/previews writes
  exactly as before
2026-08-30 15:53:43 +08:00
董宏凯andCursor 49c01cb72b fix: writing mode uses chat search then pipeline write only
Keep deep_research_report as a write-only deliverable from conversation materials, block post-success rewrite loops with a summary nudge, fill thin/empty pools from model knowledge, and open the sandbox only on real report.md content.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 12:41:04 +08:00
董宏凯andCursor 9afb9750e5 Merge branch 'master' into master-nj
Keep Win7 flowchart SVG labels and bring over Mermaid syntax repair plus HTML chart color downleveling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:28:52 +08:00
董宏凯andCursor f18994fd38 fix: show generated HTML charts on Win7 Chrome
Rewrite oklch/color-mix and space-separated rgb before iframe preview so pie and bar fills are not dropped on Chrome 109.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:25:09 +08:00
董宏凯andCursor 7c15ca6710 fix: show sandbox flowcharts on Win7 Chrome and repair common Mermaid syntax
Disable HTML labels so nodes use native SVG text, and rewrite bare edge labels plus invalid class statements before render.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:05:29 +08:00
董宏凯 31cfa93408 feat: 智能体列表页引导改为手动触发,完成/跳过不再跳转创建页
- AgentGuide 新增 autoStart 属性(默认 true),列表页挂载传 false:进智能体列表页不再自动弹引导,仅由头部「使用引导」按钮手动触发
- 去掉 list 段与创建页的联动:finish 不再 router.push,最后一步按钮恢复默认「完成」,跳过/走完都就地结束并写记忆
- AGENTS.md 同步更新智能体引导条目
2026-08-30 08:53:34 +08:00
董宏凯andCursor b164e70541 fix: unlock send immediately after SSE close
Normal Q&A no longer waits on GET /runs before the next message. Probe run status only on 409 or dropped streams.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 08:48:53 +08:00
董宏凯 17cf849361 代码提交 2026-08-30 01:25:20 +08:00
董宏凯andCursor 4ac7bc21cd feat: 优化对话历史与会商引导体验
列表引导改为手动触发且不跳转创建页;历史 Tab 展示会商步骤/链条与深度研究模板;Step2 增加暂停与内容高度跟随滚动,并修复旧草稿恢复兜底。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 20:25:21 +08:00
董宏凯andCursor 832e180c52 fix: 改进流式 write_file 沙箱预览与 Mermaid 旧浏览器兼容
同轮次非末尾 write_file 也能自动打开沙箱并跳过 JSON 大文件预览;Mermaid 改用 SVG 标签;agentfx 强制先写 md 再写 json。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 16:21:39 +08:00
董宏凯andCursor 875001a8c0 feat: 对话历史页收纳会商/写作/深度研究 Tab,侧栏移除重复入口
ChatsPage 新增四类历史 Tab 与统一搜索筛选 UI;简洁模式侧栏不再展示会商与写作列表;深度研究页支持 ?session= 深链打开指定记录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 15:43:02 +08:00
董宏凯andCursor 36653c53a6 feat: 补充对话历史页深度研究 Tab 组件
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 13:01:58 +08:00
董宏凯andCursor 37517c2778 fix: 嵌套深链改为按路由去 chrome,顶层不再清除共用 embed.session
避免全量父页清掉老版内层深链仍依赖的 embed.session;深链问答/会商/大屏绘制由 isHostNestedSurfacePath 控制 chrome,并抽取对话历史 Tab 共享行组件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 13:01:46 +08:00
吉鑫升 3715af3138 chore: move offline release assets to standalone repository 2026-08-27 23:35:57 +08:00
吉鑫升 973500a0f0 fix: preserve LF in offline deployment assets 2026-08-27 22:35:57 +08:00
吉鑫升 d9a94245fc feat: standardize DeerFlow offline deployment 2026-08-27 22:06:43 +08:00
董宏凯 1764d0d5f2 Merge branch 'master-nj' into 'master'
fix: 8BF 无 taskId 开全新会话,公共链条发布联动席位智能体

See merge request knowledge-base-bj/deerflow-code!35
2026-08-27 13:48:17 +00:00
董宏凯andCursor 84370e2072 fix: 8BF 无 taskId 开全新会话,公共链条发布联动席位智能体
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 21:47:31 +08:00
董宏凯 0289a76fce Merge branch 'master-nj' into 'master'
feat: 新增 agentfx 任务深链与研判报告入库流程

See merge request knowledge-base-bj/deerflow-code!34
2026-08-27 10:39:04 +00:00
董宏凯andCursor 32e509b2b3 fix: 岗位会商 handoff 将上游产物镜像到当前席位沙箱
席位线程隔离导致下游 read_file 找不到上游路径;镜像到 workspace/upstream 并改写 handoff 引用,附带安全路径校验与 SQL 回退测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 18:35:17 +08:00
董宏凯 440b490b0d Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-27 16:33:33 +08:00
董宏凯andCursor 52b9cbf707 feat: 新增 agentfx 任务深链与研判报告入库流程
支持 goPath=agentfx 的 iframe 嵌入问答、内置研判助手种子与报告导入协助卡,并完善深度研究流式报告能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 16:32:22 +08:00
吉鑫升 1b43205c1b feat: improve skill uploads and agent runtime stability 2026-08-27 11:28:03 +08:00
吉鑫升 11326eeb1c feat: improve skill uploads and agent runtime stability 2026-08-27 11:14:23 +08:00
吉鑫升 485a8fa9d9 feat: standardize Word export and Office conversion 2026-08-27 00:09:53 +08:00
董宏凯 4ce2f3a502 Merge branch 'master-nj' into 'master'
feat: 智能体问答页右侧面板受控折叠,收起整列归零并在 header 提供展开入口

See merge request knowledge-base-bj/deerflow-code!33
2026-08-26 10:25:11 +00:00
董宏凯andCursor 6e9e89a6cb fix: 流式自动开沙箱分阶段收起侧栏,避免同帧布局卡顿
新增 preOpen 两阶段标记,先错开收起左右侧栏再展开沙箱;智能体详情面板据此提前收起,减少与沙箱展开同帧叠加造成的卡顿。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 18:24:06 +08:00
董宏凯andCursor 90d29ed232 feat: 智能体问答页右侧面板受控折叠,收起整列归零并在 header 提供展开入口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 17:37:23 +08:00
董宏凯 d9de41dd67 代码修改 2026-08-26 17:15:04 +08:00
董宏凯 f1245da3e4 Merge branch 'master-nj' into 'master'
feat: 舆情分析智能体改为经 Gateway 转发外部 AG-UI 网关

See merge request knowledge-base-bj/deerflow-code!32
2026-08-26 09:00:55 +00:00
董宏凯 38c3de39f0 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj
# Conflicts:
#	AGENTS.md
#	frontend-web/src/pages/AgentChatPage.tsx
2026-08-26 16:37:41 +08:00
董宏凯andCursor 33639e5b89 fix: 修复嵌套 iframe 污染 embed 会话与协助卡后流式显示异常
顶层窗口通过 topOwned 标记自愈老版嵌套页写脏的 embed.session;协助卡提交后避免冻结 checkpoint 覆盖实时 SSE,并仅在服务端回显新 human 消息时清除乐观气泡。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 16:28:10 +08:00
吉鑫升 676b00afb6 Merge branch 'ji' into master 2026-08-26 16:07:37 +08:00
吉鑫升 0980eea975 feat: unify formal Word export 2026-08-26 16:02:23 +08:00
董宏凯 eba3008482 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-26 15:48:23 +08:00
吉鑫升 9b904ac3dd Merge branch 'ji' into master
# Conflicts:
#	offline-backend-20260512/backend/CLAUDE.md
#	offline-backend-20260512/backend/README.md
#	offline-backend-20260512/backend/app/gateway/routers/llmwiki.py
#	offline-backend-20260512/backend/config.yaml
#	offline-backend-20260512/backend/packages/harness/deerflow/config/llmwiki_config.py
#	offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
2026-08-26 15:46:58 +08:00
董宏凯 704438b3d2 bug修改 2026-08-26 15:46:49 +08:00
吉鑫升 d35d075669 feat: add compact toolbox and public wiki vector search 2026-08-26 14:40:57 +08:00
董宏凯 910cee9e67 用户引导沙箱优化 2026-08-26 14:14:26 +08:00
董宏凯 21f3787b8b 代码修改 2026-08-26 11:30:39 +08:00
董宏凯 33b719d460 用户引导 2026-08-26 11:09:43 +08:00
董宏凯 c6fcf51e0c 用户引导 2026-08-26 09:41:53 +08:00
董宏凯andCursor 62a3faa7c8 feat: 舆情分析智能体改为经 Gateway 转发外部 AG-UI 网关
前端不再直连上游 SSE,改为 POST /api/sentiment-agent/stream 由后台代理转发,支持关闭 TLS 校验并回传完整错误信息。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 18:26:48 +08:00
董宏凯 d87dfec2de Merge branch 'master-nj' into 'master'
feat: 业务链条发布审核流程 + 编辑器去除岗位职责输入框

See merge request knowledge-base-bj/deerflow-code!31
2026-08-25 09:35:16 +00:00
董宏凯 5c0864bf0a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-25 17:33:40 +08:00
董宏凯andCursor 8ce3a62826 fix: 岗位会商历史按用户隔离,并为 write-file 预览启用 AI 写作视图
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 17:32:56 +08:00
董宏凯 8c3d6a02fc feat: 业务链条发布审核流程 + 编辑器去除岗位职责输入框
- 发布改为可选「确认审核 / 直接发布」:链条编辑页与圆桌「业务链条」弹窗行为一致;审核中可撤回
- 管理员在业务链条列表新增「待审核」筛选,审核发布后进入公共库
- 后端 roundtable_chains 新增 pending_review 字段(启动期自动补列)与 scope=pending 筛选;新增审核流程测试
- 编辑器席位去掉「岗位职责」输入框(保存时存量值原样保留)
2026-08-25 17:30:29 +08:00
吉鑫升 e053b36b6e fix: harden wiki delivery and leaderboard performance 2026-08-25 16:58:13 +08:00
吉鑫升 9f29ecae9e fix: harden wiki delivery and leaderboard performance 2026-08-25 16:57:42 +08:00
吉鑫升 4b86106a24 fix: restore embedded wiki images and optimize vector sync 2026-08-25 11:26:27 +08:00
董宏凯 043b0900d5 Merge branch 'master-nj' into 'master'
Master nj

See merge request knowledge-base-bj/deerflow-code!30
2026-08-25 03:26:03 +00:00
吉鑫升 0c5e2e8cb0 fix: restore embedded wiki images and optimize vector sync 2026-08-25 11:24:01 +08:00
董宏凯 40d06b781a 接口调整 2026-08-25 11:23:53 +08:00
董宏凯 c5328e6a35 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-24 20:47:55 +08:00
董宏凯 babc5e95c6 代码提交 2026-08-24 20:47:31 +08:00
吉鑫升 1736227723 feat: complete public wiki and embedded previews 2026-08-24 20:41:24 +08:00
吉鑫升 30943159bf feat: complete public wiki and embedded previews 2026-08-24 20:40:44 +08:00
董宏凯 007ed5a7de Merge branch 'master-nj' into 'master'
Master nj

See merge request knowledge-base-bj/deerflow-code!29
2026-08-24 09:08:51 +00:00
董宏凯 ebc727e4fd 代码提交 2026-08-24 17:08:22 +08:00
吉鑫升 bc49df4561 feat: add public wiki embeds and ES query routing 2026-08-24 15:21:28 +08:00
董宏凯 cdb8aa5f81 弹窗样式修改 2026-08-24 14:45:25 +08:00
吉鑫升 e818c24769 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	.gitignore
#	frontend-web/src/components/workspace/artifacts/context.tsx
#	frontend-web/src/components/workspace/messages/message-group.tsx
#	offline-backend-20260512/backend/CLAUDE.md
#	offline-backend-20260512/backend/README.md
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/app/gateway/deps.py
#	offline-backend-20260512/backend/app/gateway/routers/llmwiki.py
2026-08-24 14:21:19 +08:00
吉鑫升 49babb6d25 feat: improve wiki document and artifact views 2026-08-24 14:01:10 +08:00
Administrator 3785345353 feat: add local LLMWiki wiki vector index 2026-08-24 00:49:31 +08:00
董宏凯 daaa0eab26 Merge branch 'master-nj' into 'master'
feat: 深度研究支持独立新报告与原文改写,并完善沙箱、模型管理与全文改写

See merge request knowledge-base-bj/deerflow-code!28
2026-08-23 14:30:21 +00:00
董宏凯andCursor 541bc3280f fix: 岗位会商本轮只允许 present_files 一次,避免重复产物卡
弱模型写完报告后仍会再调用一次 present_files,前端会画出两张相同卡片。上限同时覆盖呈现,已呈现则只催收尾。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-23 21:26:57 +08:00
董宏凯andCursor b81d200eca fix: 协助卡停轮与提交防 409,隐藏配置向导会话,并限制岗位会商产物重复写入
澄清中间件改为 ToolMessage + after_model 停轮,前端提交前取消残留 run;系统/向导 thread_type 从对话列表排除;岗位总结席位加 write_file 上限。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-23 20:29:59 +08:00
董宏凯andCursor 51afebb394 feat: 报告结构支持检索方向/来源与智能新增,并修复协助卡停轮与宿主跳转
为深度研究报告结构增加检索方向与技能来源、对话式智能新增页与详情排版;澄清中间件在 after_model 可靠结束本轮,避免弱模型出卡后继续生成;任务按钮补充 host 跳转与相关深链能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 17:46:06 +08:00
董宏凯andCursor a5a9c952e8 feat: 深度研究支持独立新报告与原文改写,并完善沙箱、模型管理与全文改写
新报告不再覆盖当前文件卡片,写入时打开沙箱;步骤条仅在改写原文时位于卡片上方。同时补齐改写本地回退、模型管理与会商第三步相关改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 21:46:04 +08:00
吉鑫升 4c9f75f31e feat: add fixed questions and improve authentication 2026-08-21 17:58:39 +08:00
董宏凯 7c34e18ad6 Merge branch 'master-nj' into 'master'
fix: 单轮 RAG 步骤条思考内容支持可点击参考序号

See merge request knowledge-base-bj/deerflow-code!27
2026-08-20 09:04:49 +00:00
董宏凯andCursor 11f928bc95 feat: 新增独立企业深度研究工作台,并接入工作流报告结构与 8BF 深色锁定
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 16:43:03 +08:00
董宏凯andCursor e881c242fe feat: 岗位职责迁至业务链席位,并支持 WeKnora 嵌入页知识库切换
将岗位职责从智能体配置迁移到业务链席位 JSON,会商面板只读展示;WeKnora 嵌入 iframe 支持在授权可见范围内切换知识库并重新签发会话。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 22:26:40 +08:00
董宏凯andCursor e860357563 fix: 单轮 RAG 步骤条思考内容支持可点击参考序号
把思考区的 `[n]` 按正文同一套引用规则渲染,并去掉回答气泡里重复的 Thought for a few seconds。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 21:21:22 +08:00
董宏凯 c69fde22bd Merge branch 'master-nj' into 'master'
feat: 支持会商 Markdown 产物提交到任务过程,并补充 Angular 对接文档

See merge request knowledge-base-bj/deerflow-code!26
2026-08-19 12:38:26 +00:00
董宏凯andCursor 0ce060693b feat: 会商链条启停与深度研究报告结构,并完善问答展示与 8BF 返回。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 20:37:34 +08:00
董宏凯 be4851555d 代码提交 2026-08-19 17:11:36 +08:00
董宏凯andCursor 3afa76894f feat: 完善会商产物提交匹配与流式写文件预览
席位流程图按已提交 Markdown 对齐;流式未命名产物不再误请求 artifacts API,并兼容临时文件名路径。同时补强会话流中断识别与 8BF/岗位会商展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 17:08:52 +08:00
董宏凯andCursor acf538ac01 feat: 简介模式菜单按类型 A/B 分套,兼容未配置的旧部署
两套网页可通过 runtime-config.js 的 VITE_COMPACT_MENU_TYPE 加载各自左侧菜单;缺省或旧页面不传参数时仍读取原来的类型 A 数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 16:59:34 +08:00
董宏凯andCursor 5e1765d889 feat: 完善 8BF 会商第一步产物与智能体列表,第二步对话区对齐第一步样式
抽出共用分析成果卡片,第一步展示第二、三步 Markdown 产物;智能体列表对齐岗位会商选择样式。第二步对话面板改为深蓝顶栏,第一步保留可配置返回按钮。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 12:12:35 +08:00
董宏凯andCursor 30799f17be fix: 节流会商流式写文件预览,避免 Maximum update depth
write_file 的 content 在每个 SSE chunk 中持续增长,原先会反复触发 React 状态更新并打断整页;现改为按身份入参去重、节流发布 stub,并收口第 3 步为 Markdown 报告工作台。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 23:23:46 +08:00
董宏凯andCursor dcfaa199bf feat: 支持会商 Markdown 产物提交到任务过程,并补充 Angular 对接文档
8BF/会商工作区可将 Markdown 产物一键提交到任务过程报告,后端记录提交状态以便跨用户与刷新后保持已提交;同时为 Angular 智能体对话对接文档补上前后端参考代码索引。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 20:08:51 +08:00
董宏凯 33b0af333c Merge branch 'master-nj' into 'master'
增强 Deep Research 可靠性,并接入 8BF 任务卡片与会商上下文。

See merge request knowledge-base-bj/deerflow-code!25
2026-08-18 09:55:17 +00:00
董宏凯 172250e6b5 代码提交 2026-08-18 17:54:07 +08:00
董宏凯andCursor 15a46b903a 增强文档改写配图与深度研究流式稳定性,并完善 8BF/会商工作区。
全文改写支持可选配图与思考过程展示,深度研究接入 DashScope 原生出图并剥离内联 think 标签;8BF 会商补齐第一步工作区与任务卡回退,同时限制热重载扫描范围。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 17:46:11 +08:00
吉鑫升 8af4fed89d feat: update knowledge workspace and menu configuration 2026-08-18 15:23:14 +08:00
Administrator 64a5305bbf feat: add markdown word editor for knowledge workspace 2026-08-18 00:44:29 +08:00
董宏凯 819e537aa3 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-17 19:30:37 +08:00
吉鑫升 68c4896088 fix: stabilize auth sync and stream recovery for ji 2026-08-17 19:27:39 +08:00
董宏凯andCursor 771957429a 增强 Deep Research 可靠性,并接入 8BF 任务卡片与会商上下文。
修复 MySQL 读写分离下的持久化与 SSE 重连问题,报告完成前先落库;8BF 嵌入页支持 taskId 任务卡与独立意图 metadata;前端统一 createClientId 并更新 runtime 配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 18:16:05 +08:00
吉鑫升 5bf08c83a6 Merge remote-tracking branch 'origin/master' into ji 2026-08-17 15:53:36 +08:00
吉鑫升 3ab2c9b282 feat: improve WeKnora source preview and wiki management 2026-08-17 15:53:19 +08:00
吉鑫升 94cf37a2ae fix: normalize system messages before model calls 2026-08-17 10:12:43 +08:00
董宏凯 47faa9bcdf 代码提交 2026-08-16 21:48:38 +08:00
董宏凯 5901246030 Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-16 20:43:43 +08:00
董宏凯 44e0b7596b Merge branch 'master-baogao' into 'master'
Master baogao

See merge request knowledge-base-bj/deerflow-code!24
2026-08-16 11:21:33 +00:00
董宏凯 178572bc7f 代码提交 2026-08-16 19:17:45 +08:00
董宏凯 70da7c5649 代码提交 2026-08-15 12:51:58 +08:00
fengxiangdebaozi 5c586652af daimhebing1 2026-08-14 21:25:29 +08:00
fengxiangdebaozi 067f602732 代码提交 2026-08-14 19:31:15 +08:00
董宏凯 96bfdacee5 Merge branch 'master-nj' into 'master'
Master nj

See merge request knowledge-base-bj/deerflow-code!23
2026-08-14 05:01:17 +00:00
董宏凯 500dab29ab Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-14 12:57:04 +08:00
董宏凯 4d688ddbd8 代码提交 2026-08-14 12:56:55 +08:00
Administrator 8193cc53d5 fix: stabilize embedded WeKnora detail page 2026-08-14 11:32:33 +08:00
Administrator a28ad2140e fix: stabilize embedded WeKnora detail page 2026-08-14 11:29:58 +08:00
董宏凯 7b37415604 Merge branch 'master-nj' into 'master'
feat: 岗位会商命令幂等与圆桌后台作业租约调度

See merge request knowledge-base-bj/deerflow-code!22
2026-08-14 03:17:53 +00:00
董宏凯 c4d244881f Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-14 11:15:37 +08:00
董宏凯andCursor 75963c9ad9 feat: 岗位会商命令幂等与圆桌后台作业租约调度
岗位会商写操作引入 command_id 幂等与版本锁,前端支持同键重试;圆桌后台作业改为 DB 租约 dispatcher、去重与草稿 outbox 补偿,并持久化 intent turns。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:08:12 +08:00
Administrator 11e54550ed fix: harden WeKnora proxy and configurable search 2026-08-13 23:44:47 +08:00
fengxiangdebaozi 4f536d9b39 feat(frontend): add workspace single and multi round rag 2026-08-13 21:05:31 +08:00
Administrator b527bc94bb fix: stabilize WeKnora embed session cookie 2026-08-13 20:42:24 +08:00
Administrator 54fd0e7316 fix: route WeKnora embed through backend prefix 2026-08-13 20:22:50 +08:00
Administrator 6dbaf40f99 fix: preserve WeKnora embed proxy prefix 2026-08-13 19:17:01 +08:00
Administrator 3aeb6ac5ce fix: authenticate WeKnora API client 2026-08-13 17:41:30 +08:00
董宏凯 5a4881053d Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-13 16:44:47 +08:00
董宏凯 cebc44ff82 代码提交 2026-08-13 16:43:31 +08:00
Administrator 56044d9cb7 fix: improve llmwiki search integration 2026-08-13 16:03:08 +08:00
吉鑫升 4809ee7db3 fix: update offline backend llmwiki support 2026-08-13 11:17:11 +08:00
Administrator a4a1e73b8e chore: configure VS Code launch tasks 2026-08-13 10:36:06 +08:00
Administrator 5123b7bdd7 chore: configure WeKnora admin password 2026-08-13 10:29:08 +08:00
Administrator e189349eeb fix: sync llmwiki embed theme 2026-08-13 10:25:49 +08:00
董宏凯 f2e81d8875 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-12 16:00:35 +08:00
董宏凯 e5e0f3a7c3 代码提交 2026-08-12 16:00:26 +08:00
吉鑫升 c4fd9dd7da Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	.gitignore
#	frontend-web/src/components/workspace/input-box.tsx
#	frontend-web/src/core/threads/types.ts
#	frontend-web/src/pages/ChatPage.tsx
2026-08-12 15:55:51 +08:00
吉鑫升 b4d076a2d4 fix: harden WeKnora knowledge permissions and details 2026-08-12 15:45:28 +08:00
董宏凯 bc82072710 Merge branch 'master-nj' into 'master'
feat: 管理员可删除内置技能并管理技能内文件

See merge request knowledge-base-bj/deerflow-code!21
2026-08-12 07:08:19 +00:00
董宏凯andCursor bf023d746a feat: 管理员可删除内置技能并管理技能内文件
新增内置技能删除与技能文件删除 API,允许管理员编辑/删除内置技能文件;技能设置页与文件预览弹窗同步支持删除操作。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 15:05:29 +08:00
董宏凯 fb5024ea82 Merge branch 'master-nj' into 'master'
feat: 模型配置支持删除,岗位行动规划对接任务系统字典与批量新增 Skill

See merge request knowledge-base-bj/deerflow-code!20
2026-08-12 06:48:24 +00:00
董宏凯andCursor c9c2dc7ea8 feat: 模型配置支持删除,岗位行动规划对接任务系统字典与批量新增 Skill
模型管理新增 DELETE 接口与前端删除能力(支持含斜杠的模型名);行动规划改为 9 字段 JSON,prepare 传入 taskId/字典并绑定 position-roundtable-action-create Skill。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 14:47:14 +08:00
董宏凯 2a869f4ff6 Merge branch 'master-nj' into 'master'
feat: 添加管理员模型配置页面与 config.yaml 模型管理 API

See merge request knowledge-base-bj/deerflow-code!19
2026-08-12 06:44:18 +00:00
fengxiangdebaozi af509eb670 Merge branch 'master-nj' of http://192.168.0.200/knowledge-base-bj/deerflow-code into master-nj 2026-08-12 14:32:57 +08:00
fengxiangdebaozi 783e698b6e 代码提交 2026-08-12 14:32:47 +08:00
董宏凯andCursor ba912acf72 feat: 添加管理员模型配置页面与 config.yaml 模型管理 API
新增 /api/admin/models 路由(读写 config.yaml models 段、连接测试、写前备份),以及工作区「模型配置」管理页与侧栏入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 21:45:56 +08:00
吉鑫升 e0c09f6704 feat: integrate WeKnora llmwiki knowledge flow 2026-08-11 18:13:30 +08:00
董宏凯 17a12bb7b6 Merge branch 'master-nj' into 'master'
feat: 岗位会商独立意图智能体与四项意图 schema

See merge request knowledge-base-bj/deerflow-code!18
2026-08-11 05:00:55 +00:00
董宏凯andCursor f8757d1a43 feat: 岗位会商独立意图智能体与四项意图 schema
岗位会商 Step 1 改用 position-roundtable-intent 智能体,输出 coreGoals/riskWarnings/keyPoints/strategicSignificance 四项数组;支持单轮多张协助卡、按轮次计澄清上限,并兼容旧会话 objective/constraints 映射。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 13:00:16 +08:00
董宏凯 69b95f0d6d Merge branch 'master-nj' into 'master'
feat(frontend): 岗位会商页面布局与意图理解交互优化

See merge request knowledge-base-bj/deerflow-code!17
2026-08-11 04:59:37 +00:00
董宏凯 ca32a22c0d 代码提交 2026-08-10 15:11:47 +08:00
董宏凯 3eb307c5c5 代码提交 2026-08-10 14:26:49 +08:00
董宏凯 49af497085 代码修改 2026-08-05 21:15:36 +08:00
董宏凯andCursor 8b732b43ee fix(frontend): 同路径二次 write_file 时重挂岗位会商沙箱
按 tool-call 身份区分预览,避免同路径重写复用旧 previewKey 导致沙箱不刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 00:54:15 +08:00
董宏凯andCursor 69e455274a fix(frontend): 岗位会商沙箱在 write_file 流式阶段实时打开并刷新
content 先于 path 到达时用临时文件名开沙箱,流式快照改走外部 store 避免整页重渲染卡顿。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 00:51:38 +08:00
董宏凯andCursor e939dc4548 feat(frontend): 岗位会商页面布局与意图理解交互优化
完善 PositionRoundtable 样式与输入组件,补充 SVG 资源,并同步 Step1 意图相关逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 21:52:54 +08:00
董宏凯 1ffead7ee2 岗位会商样式修改 2026-08-02 15:33:26 +08:00
董宏凯 a0a75a9964 Merge branch 'dhk-1' into 'master'
fix: avoid MySQL thread_meta refresh races and harden roundtable bootstrap

See merge request knowledge-base-bj/deerflow-code!16
2026-07-31 13:11:29 +00:00
董宏凯 37964d1f95 代码修改 2026-07-31 21:09:54 +08:00
董宏凯andCursor a9d66536c3 fix: avoid MySQL thread_meta refresh races and harden roundtable bootstrap
Skip post-commit ORM refresh on threads_meta creates so concurrent MySQL
read/write-split deployments no longer fail with Could not refresh instance;
clean up partial roundtable threads on checkpoint failure, and ship related
position-roundtable/frontend build fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 21:00:56 +08:00
董宏凯 a968c3b5cd Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-code!15
2026-07-29 14:27:46 +00:00
董宏凯 e52261fc3d fix: preserve full chat embed mode navigation 2026-07-29 22:25:42 +08:00
董宏凯 4450f342d8 页面嵌套修改 2026-07-29 21:53:32 +08:00
董宏凯 213dd841b5 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1 2026-07-28 19:18:52 +08:00
董宏凯 816c46d447 代码提交 2026-07-28 19:17:40 +08:00
g zy db857ddd4d Merge branch 'gzy_dev_new' into 'master'
fix: fix iframe agent selector width

See merge request knowledge-base-bj/deerflow-code!14
2026-07-28 03:30:12 +00:00
Rocky Kwok 9be0c6555f fix: fix iframe agent selector width 2026-07-28 11:27:47 +08:00
g zy e475c37f1d Merge branch 'gzy_dev_new' into 'master'
Gzy dev new

See merge request knowledge-base-bj/deerflow-code!13
2026-07-28 03:14:47 +00:00
Rocky Kwok d86c9d48a6 fix: remove iframe general chat entry 2026-07-28 11:12:00 +08:00
Rocky Kwok 008c9ede6e fix: preserve iframe plugin chat mode 2026-07-28 11:03:28 +08:00
董宏凯 151ecceb85 feat: 更新主题配置与系统设置 2026-07-27 21:31:09 +08:00
董宏凯 1555d5344b feat: 优化8BF业务链流程图与深色配置弹窗 2026-07-27 21:30:57 +08:00
董宏凯 bd8ec41914 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-code!12
2026-07-27 11:25:40 +00:00
董宏凯 1e21e559fb merge: 合并远端 dhk-1 最新改动 2026-07-27 19:22:59 +08:00
董宏凯 e496221dc3 Merge remote-tracking branch 'origin/master' into HEAD 2026-07-27 19:19:01 +08:00
董宏凯 5bf3955b26 代码提交 2026-07-27 19:16:12 +08:00
董宏凯 1ab3329f6d feat: 新增8BF多智能体会商页面 2026-07-27 19:00:14 +08:00
董宏凯 e70bb4aa6f feat: 新增8BF多智能体会商页面 2026-07-27 18:59:42 +08:00
g zy b22d0200c3 Merge branch 'gzy_dev_new' into 'master'
fix(frontend): preserve iframe theme across login

See merge request knowledge-base-bj/deerflow-code!11
2026-07-27 02:16:32 +00:00
Rocky Kwok fb73ae0471 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into gzy_dev_new 2026-07-27 10:06:33 +08:00
Rocky Kwok 75aad4eef6 fix(frontend): preserve iframe theme across login
The iframe login used a fixed queryless redirect and called setTheme, which both dropped the iframe theme and persisted it into the normal site's shared theme storage.
2026-07-27 09:54:22 +08:00
董宏凯 cec2996861 Merge branch 'dhk-1' into 'master'
feat(沙箱): md 产物支持 AI 写作编辑与划词改写

See merge request knowledge-base-bj/deerflow-code!9
2026-07-26 13:20:20 +00:00
fengxiangdebaozi 239b798c5d Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1
# Conflicts:
#	frontend-web/src/components/workspace/workspace-nav-menu.tsx
#	offline-backend-20260512/backend/app/gateway/routers/multi_agent.py
2026-07-26 20:55:50 +08:00
董宏凯 f1e53ef5c0 功能:完善自定义主题与独立登录会话 2026-07-26 18:19:50 +08:00
g zy a368037e81 Merge branch 'gzy_dev_new' into 'master'
fix:iframe切换颜色

See merge request knowledge-base-bj/deerflow-code!8
2026-07-26 09:10:27 +00:00
Rocky Kwok 59f2f56124 fix:iframe切换颜色 2026-07-26 17:01:09 +08:00
董宏凯 3779bb69ef fix: load model keys from environment 2026-07-26 13:08:09 +08:00
董宏凯 25153dbd84 代码修改 2026-07-24 14:12:42 +08:00
董宏凯 91ba47bf47 代码提交 2026-07-24 11:24:11 +08:00
董宏凯 28bf1ac7b2 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1
# Conflicts:
#	frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx
#	frontend-web/src/position-roundtable/components/PositionNodeChat.tsx
2026-07-24 10:41:38 +08:00
董宏凯 45df500a9c 代码提交 2026-07-24 10:33:28 +08:00
fengxiangdebaozi f32de510fe 代码泰囧 2026-07-23 16:27:35 +08:00
fengxiangdebaozi 21722b9f5a 代码提交 2026-07-23 16:03:34 +08:00
董宏凯 f49def09ea 代码修改 2026-07-23 16:00:39 +08:00
董宏凯 4948a80c56 代码提交 2026-07-23 15:53:43 +08:00
董宏凯 7d249a81ce Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1
# Conflicts:
#	frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx
#	frontend-web/src/position-roundtable/components/PositionNodeChat.tsx
2026-07-23 15:11:43 +08:00
董宏凯 1d07025f74 代码修改 2026-07-23 15:08:00 +08:00
fengxiangdebaozi 91c5d43c48 代码提交 2026-07-23 15:07:23 +08:00
董宏凯 b60d9ed09e 代码修改 2026-07-23 14:29:12 +08:00
董宏凯 56740ecf56 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1 2026-07-23 12:38:52 +08:00
董宏凯 9f788e37de 样式修改 2026-07-23 12:38:48 +08:00
fengxiangdebaozi 0634337858 feat: 支持登录隐藏壳层和路由跳转参数 2026-07-23 11:16:11 +08:00
董宏凯 08ce3dda73 merge: integrate dhk-dev into dhk-1 2026-07-23 10:38:48 +08:00
董宏凯 c8f69781e4 样式修改 2026-07-23 10:10:47 +08:00
董宏凯 13bfa719ee feat: refine position roundtable workflow 2026-07-22 22:22:38 +08:00
董宏凯 359bd28f2c fix: polish position roundtable artifact preview 2026-07-22 18:41:46 +08:00
fengxiangdebaozi 7bd032ec77 feat: 支持岗位会商任务和意图编辑 2026-07-22 17:44:34 +08:00
董宏凯 790267ac43 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1
# Conflicts:
#	frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx
2026-07-22 17:02:07 +08:00
董宏凯 d9844e0180 打开沙箱错误修改 2026-07-22 16:53:56 +08:00
fengxiangdebaozi 6ade3b69b9 feat: refine position roundtable workflow 2026-07-22 16:53:10 +08:00
董宏凯 ae184f434d Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1 2026-07-22 15:43:13 +08:00
董宏凯 055523bf33 代码修改 2026-07-22 15:43:06 +08:00
fengxiangdebaozi 866bc0f8d8 样式修改 2026-07-22 15:42:29 +08:00
fengxiangdebaozi 1bc203b5cd 岗位会商 2026-07-22 15:40:00 +08:00
fengxiangdebaozi 1cd99a5a90 feat(position-roundtable): add delivery rejection workflow 2026-07-21 20:16:52 +08:00
fengxiangdebaozi 2ca8a23102 feat(position-roundtable): add delivery workflow scaffolding 2026-07-21 19:48:39 +08:00
fengxiangdebaozi 97ce19973d docs(position-roundtable): plan delivery workflow 2026-07-21 19:31:08 +08:00
fengxiangdebaozi 783774d3f3 fix(position-roundtable): align header controls 2026-07-21 19:30:55 +08:00
fengxiangdebaozi 85e119db08 feat(position-roundtable): add standalone position collaboration 2026-07-21 17:50:24 +08:00
吉鑫升 5436719bfb Merge branch 'master-dev' into 'master' 2026-07-21 14:47:46 +08:00
吉鑫升 e571caaf2c fix: keep offline start script minimal 2026-07-21 14:46:58 +08:00
吉鑫升 5d2997b6a0 fix: preserve offline deployment LF files 2026-07-21 14:39:57 +08:00
吉鑫升 f8cb7be18c chore: add offline backend wheelhouse deployment 2026-07-21 14:35:06 +08:00
董宏凯 b93b82fcaf Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-1 2026-07-21 11:00:19 +08:00
董宏凯 d0b4832ed7 代码提交 2026-07-21 10:58:56 +08:00
fengxiangdebaozi 4ffd5a3c94 feat(frontend): add position roundtable workspace 2026-07-20 20:41:37 +08:00
董宏凯 2d6992d656 feat(frontend): adapt TDesign themes 2026-07-20 20:10:22 +08:00
董宏凯 bb7291a97c Merge branch 'dhk-1' into 'master'
feat: 支持深色主题顶栏 Logo 位置配置

See merge request knowledge-base-bj/deerflow-code!5
2026-07-17 13:08:45 +00:00
fengxiangdebaozi a699545c4c feat: 支持深色主题顶栏 Logo 位置配置 2026-07-17 21:07:48 +08:00
董宏凯 2c331aa744 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-code!4
2026-07-16 14:45:42 +00:00
fengxiangdebaozi 82dd9fb08f feat: 支持主题系统Logo与欢迎页开关 2026-07-16 22:31:22 +08:00
fengxiangdebaozi b97aa77adc fix: 会商界面适配字号设置 2026-07-16 16:26:43 +08:00
fengxiangdebaozi d7d101814f fix: 修复深色导航与会商悬浮样式 2026-07-16 11:21:52 +08:00
董宏凯 871d98e6f2 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-code!3
2026-07-16 01:57:37 +00:00
fengxiangdebaozi 3cd093a8a2 地址修改 2026-07-16 09:56:54 +08:00
fengxiangdebaozi 4a59da7ff0 fix: 优化侧栏并接入品牌脱敏 2026-07-16 09:48:40 +08:00
董宏凯 d7ad0624b7 代码提交 2026-07-15 16:13:03 +08:00
董宏凯 27e6b0b23e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-code into dhk-dev
# Conflicts:
#	frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
2026-07-11 19:01:10 +08:00
董宏凯 55b5c2cc9f 代码部署说明 2026-07-11 18:58:16 +08:00
董宏凯 d5d5199f6d Merge branch 'dhk-1' into 'master'
代码合并

See merge request knowledge-base-bj/deerflow-code!2
2026-07-11 10:16:38 +00:00
fengxiangdebaozi 0bd0ac41cd 代码合并 2026-07-11 18:16:08 +08:00
董宏凯 b1c396717b Merge branch 'dhk-1' into 'master'
feat(message-token-usage): 速度条下方常驻颜色含义图例

See merge request knowledge-base-bj/deerflow-code!1
2026-07-11 10:12:52 +00:00
fengxiangdebaozi 388a9133fc fix: adapt frontend for legacy Chrome and runtime config 2026-07-11 16:55:08 +08:00
董宏凯 221e947488 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-dev
# Conflicts:
#	frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
#	frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx
#	offline-backend-20260512/backend/CLAUDE.md
#	offline-backend-20260512/backend/app/gateway/services.py
#	offline-backend-20260512/backend/tests/test_ai_writing_knowledge_base.py
2026-07-03 22:28:21 +08:00
董宏凯 a1c63a1fe5 代码提交 2026-07-03 22:11:50 +08:00
吉鑫升 db2f21191c fix(frontend): handle clipboard fallback and markdown word export 2026-07-03 16:54:16 +08:00
董宏凯 195ef3ee40 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!169
2026-07-03 08:37:06 +00:00
fengxiangdebaozi f3511e5cd0 样式修改 2026-07-03 16:35:52 +08:00
fengxiangdebaozi 96947d7636 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-07-03 14:56:49 +08:00
fengxiangdebaozi 034a379ecd 技能提交 2026-07-03 14:56:35 +08:00
董宏凯 da856f2b58 fix: improve ai writing chat streaming and intent handling 2026-07-02 17:43:23 +08:00
董宏凯 845c87e3ee feat: add AI writing chat workspace 2026-07-02 13:56:46 +08:00
董宏凯 7d9c0894f8 fix: limit roundtable chain rail height 2026-07-02 11:17:52 +08:00
吉鑫升 ae30a20739 Merge remote-tracking branch 'origin/master' into ji 2026-07-02 11:15:06 +08:00
吉鑫升 7e7d3c9846 fix: truncate rwfx 3q context 2026-07-02 11:05:12 +08:00
吉鑫升 daff70c34f fix: include rwfx 3q status in context 2026-07-02 10:43:16 +08:00
董宏凯 94188db9ea Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!168
2026-07-02 02:15:43 +00:00
董宏凯 b12497898a fix: polish roundtable chain rail toggle 2026-07-02 10:14:40 +08:00
董宏凯 3e51208024 feat: add roundtable chain rail scroll 2026-07-02 10:06:24 +08:00
吉鑫升 36a3851233 feat: inject 3q context into rwfx chat 2026-07-02 10:03:34 +08:00
董宏凯 54190d7a83 提交代码 2026-07-01 19:24:23 +08:00
fengxiangdebaozi 2215ec3c49 feat: add user-assisted xdfx skill flow 2026-07-01 19:21:18 +08:00
董宏凯 d21dbeb214 feat: refine ai writing search flow 2026-07-01 18:00:14 +08:00
董宏凯 ba0c32dcee Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!167
2026-07-01 09:51:15 +00:00
fengxiangdebaozi f5efe9b041 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-07-01 17:50:33 +08:00
fengxiangdebaozi a4a44e1154 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-07-01 17:50:06 +08:00
fengxiangdebaozi cef08638af 技能修改 2026-07-01 17:49:51 +08:00
董宏凯 025e43f4c5 feat: improve ai writing imitation flow 2026-07-01 17:16:12 +08:00
董宏凯 fa04ab8b5a fix: filter position-granted agent favorites 2026-07-01 14:51:14 +08:00
董宏凯 f3e5a3bb35 fix: prevent notebook writing redirect to agent chat 2026-07-01 14:48:40 +08:00
董宏凯 0547fb1715 提交 2026-07-01 11:22:26 +08:00
董宏凯 bb6b9bd84b feat: add ai writing imitation mode 2026-07-01 11:21:41 +08:00
g zy f485d4fff3 Merge branch 'gzy' into 'master-dev'
fix: 注释插件管理

See merge request knowledge-base-bj/deerflow-server!166
2026-07-01 02:25:19 +00:00
董宏凯 4bfaeb728b fix: disable ssl verification for intranet ai writing search 2026-07-01 10:21:52 +08:00
Rocky Kwok 2b0ba2a68b fix: 注释插件管理 2026-07-01 10:08:46 +08:00
董宏凯 c8cdd1d0b2 Merge branch 'dhk-1' into 'master'
统一外部 access_token 存储与轻应用 URL 拼接,修复嵌入会话鉴权隔离。

See merge request knowledge-base-bj/deerflow-server!165
2026-06-30 11:21:42 +00:00
董宏凯 8fbfd35296 Add scheduled HTML templates no19-no27 2026-06-30 19:18:48 +08:00
董宏凯 dfb745a5e4 代码提交 2026-06-30 16:04:51 +08:00
吉鑫升 c23350f17b fix: isolate user concurrency from internal agent runs 2026-06-30 14:56:29 +08:00
吉鑫升 124eb04a6b Merge remote-tracking branch 'origin/master-dev' into ji-dev 2026-06-30 11:34:45 +08:00
吉鑫升 b4206d6421 Add Redis-backed concurrency gate 2026-06-30 11:25:44 +08:00
董宏凯andCursor 537e2e4d06 fix(登录): token 登录补全 userInfo + 全方式打印路由参数
- 把「拉外部用户信息(yUserId→consumer)」与登录鉴权方式解耦:只要 URL 带
  yUserId 就填充 userInfo(含 token 登录,consumer 失败非致命),修复 token
  登录后 userInfo 残缺、iframe/轻应用拿不到 yUserId/userName/gateDepartName/
  typeName/accessToken 的问题。
- token 登录用 auth.username 写入正确用户名(不覆盖 consumer 已落的 userName)。
- 保留 yUserId 优先级(onlineTicket/toolToken 仅在无 yUserId 时启用)。
- 新增登录路由参数 + 落地结果日志(令牌脱敏),方便排查各登录方式取值问题。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:36:33 +08:00
董宏凯andCursor 9268c73776 feat(沙箱): md 产物支持 AI 写作编辑与划词改写
- 新增 MarkdownAiEditor,复用 AI 写作的 TipTap 编辑器与 AI 改写流程

- 沙箱 md 增加 AI 写作视图切换,默认仍为预览;单文件隐藏文件名避免工具栏错位

- 单智能体聊天页开启 allowVirtualMarkdownEdit,生成完成后可编辑

- 会商总结页续看时自动收起研讨原料侧栏

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:08:55 +08:00
fengxiangdebaozi 6378cd33bd 统一外部 access_token 存储与轻应用 URL 拼接,修复嵌入会话鉴权隔离。
将 access_token 纳入 scoped storage 迁移,yUserId 登录优先并独立持久化 token;轻应用/按钮管理默认改用 access_token 参数名,支持 hash 路由占位符替换与自动追加。
2026-06-29 23:00:31 +08:00
fengxiangdebaozi 8c6d08d6bf Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-29 18:08:37 +08:00
fengxiangdebaozi 29082b349d 技能修改 2026-06-29 18:08:09 +08:00
Administrator 7d8656bfde fix xdfx task start context recovery 2026-06-29 17:45:46 +08:00
fengxiangdebaozi 45ce14e15a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-29 17:39:52 +08:00
g zy 8947223255 Merge branch 'gzy' into 'master-dev'
fix: iframe底部选择智能体

See merge request knowledge-base-bj/deerflow-server!164
2026-06-29 09:36:11 +00:00
Rocky Kwok 5a420189b1 fix: iframe底部选择智能体 2026-06-29 17:35:36 +08:00
g zy 17650afe96 Merge branch 'revert-f22a6a99' into 'master'
Revert "Merge branch 'revert-d8af6c8f' into 'master'"

See merge request knowledge-base-bj/deerflow-server!163
2026-06-29 09:19:24 +00:00
g zy 80fce34c1a Revert "Merge branch 'revert-d8af6c8f' into 'master'"
This reverts merge request !158
2026-06-29 09:19:17 +00:00
g zy ae31c50424 Merge branch 'gzy' into 'master-dev'
Gzy

See merge request knowledge-base-bj/deerflow-server!162
2026-06-29 09:14:43 +00:00
Rocky Kwok 794aab1007 fix: recode 2026-06-29 17:14:01 +08:00
Rocky Kwok be06cac223 fix: reset code 2026-06-29 17:12:18 +08:00
Administrator b0db8cc3d8 feat: 优化定时任务 Markdown 阅读页 2026-06-29 17:08:38 +08:00
g zy 0dab5a8121 Merge branch 'gzy' into 'master-dev'
fix: chore

See merge request knowledge-base-bj/deerflow-server!161
2026-06-29 09:05:30 +00:00
Rocky Kwok c012146c18 fix: chore 2026-06-29 17:04:50 +08:00
fengxiangdebaozi 6851c96a45 技能提交 2026-06-29 16:59:23 +08:00
g zy f75f4f9db1 Merge branch 'gzy' into 'master-dev'
feat: iframe增加智能体显示

See merge request knowledge-base-bj/deerflow-server!159
2026-06-29 08:47:03 +00:00
g zy f22a6a9957 Merge branch 'revert-d8af6c8f' into 'master'
Revert "Merge branch 'gzy' into 'master'"

See merge request knowledge-base-bj/deerflow-server!158
2026-06-29 08:45:40 +00:00
fengxiangdebaozi 210cb5117f fix: polish compact workspace controls 2026-06-29 16:44:13 +08:00
Administrator 0c663f5f7c Merge remote-tracking branch 'origin/master' into ji-dev 2026-06-29 16:43:45 +08:00
g zy 75e8a90ac7 Revert "Merge branch 'gzy' into 'master'"
This reverts merge request !157
2026-06-29 08:43:19 +00:00
g zy d8af6c8f48 Merge branch 'gzy' into 'master'
feat: iframe增加智能体显示

See merge request knowledge-base-bj/deerflow-server!157
2026-06-29 08:43:11 +00:00
Rocky Kwok 1ab4216ccb feat: iframe增加智能体显示 2026-06-29 16:42:38 +08:00
Administrator d39b58255d 并发版 2026-06-29 16:09:51 +08:00
Administrator cd2ca406a5 Merge branch 'master-dev' into ji-dev 2026-06-29 15:12:24 +08:00
Administrator 637be00946 feat: improve agent skill selection 2026-06-29 15:09:27 +08:00
Administrator 0dd93421c4 feat: enhance public task markdown preview 2026-06-29 14:42:47 +08:00
Administrator e27321cb23 feat: enhance public task markdown preview 2026-06-29 14:40:26 +08:00
g zy 6157b42f5a Merge branch 'gzy' into 'master'
Gzy

See merge request knowledge-base-bj/deerflow-server!156
2026-06-29 06:33:12 +00:00
Rocky Kwok be4b180d92 fix: 智能体对应 2026-06-29 14:31:55 +08:00
Rocky Kwok e9e4c181b9 feat: 插件管理及域名和智能体对应 2026-06-29 14:31:40 +08:00
fengxiangdebaozi eb61b332a2 Add XDFX skill data artifacts 2026-06-29 10:25:36 +08:00
fengxiangdebaozi 4fce6f9104 Add intranet retrieval diagnostics for AI writing 2026-06-29 10:21:10 +08:00
fengxiangdebaozi bcd656fb49 代码提交 2026-06-28 18:50:59 +08:00
Administrator 2d3033434b Fix async thread title generation flow 2026-06-28 18:09:48 +08:00
Administrator d522498cc9 ji 2026-06-28 00:23:01 +08:00
fengxiangdebaozi 65449ffd71 merge dhk-1 yuser login cache 2026-06-27 19:58:57 +08:00
fengxiangdebaozi 006c5bc4c1 fix: cache yuser login info by user id 2026-06-27 19:57:06 +08:00
Administrator a046d19ce1 fix: improve model concurrency limiting 2026-06-27 18:57:01 +08:00
Administrator aca089e8ef feat: add per-model concurrency limits 2026-06-27 18:57:01 +08:00
董宏凯 24eb629773 Merge branch 'dhk-1' into 'master'
feat(ai写作): 历史记录可继续未完成的会话(从checkpoint续跑)

See merge request knowledge-base-bj/deerflow-server!155
2026-06-27 09:31:57 +00:00
fengxiangdebaozi 1847eb9d6a fix(auth): normalize external accessToken and preserve on empty yUser login 2026-06-27 17:29:52 +08:00
董宏凯 44d7304f8e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-27 16:00:47 +08:00
董宏凯andCursor 5de74e90dc feat(ai写作): 左栏智能体 Tab 内嵌问答并优化模块侧栏
AI 写作页默认展示智能体 Tab,在页内复用 ai-writing 模块配置与 AgentChatPage;确认类型后侧栏扁平展示类型名并支持整块收起动画。同步历史回看、流式续跑、改写菜单与 researcher 检索相关改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 15:53:51 +08:00
Administrator 6fdca41a1d Merge branch 'ji' into master 2026-06-27 15:13:45 +08:00
Administrator c978a25b7f Fix 3qfx task context fallback 2026-06-27 15:12:28 +08:00
董宏凯andClaude Opus 4.8 c11307aa67 feat(ai写作): 历史记录可继续未完成的会话(从checkpoint续跑)
历史列表里 in_progress/draft_ready/review_ready 记录加绿色▶️「继续未完成」按钮
(点标题亦可);done/error/background 不可继续。多 worker 安全——与4个确认卡同机制,
全基于共享 checkpoint(gateway GET /threads/{id}/state + /history 读,任意 worker)。

链路:continueWriting(sessionId) → closeHistory + 设活跃 sessionId → useStream
fetchStateHistory 拉该 thread checkpoint 进 stream.values → host rehydrate effect
(continueTargetRef 守卫,每 thread 一次)用 buildRehydrateSnapshot 重建已完成阶段;
status=awaiting_* → 显示确认卡等用户操作,否则 submit({command:{}}) 从 checkpoint
续跑当前阶段。重新加回 buildRehydrateSnapshot + rehydrate action。

入口在侧栏 WritingHistoryItem。刷新=重置不受影响(这是显式点击,非自动重连)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:12:47 +08:00
吉鑫升 199269712f Merge branch 'ji' into master
账号级登录口令(白名单管理页设明文口令,凭 /login/<用户名>?password= 直登)。
2026-06-27 11:19:58 +08:00
吉鑫升andClaude Opus 4.8 9572b66ecb feat(白名单): 账号级登录口令——管理员设明文口令,用户凭 /login/<用户名>?password= 直登
白名单管理页可给单个账号设明文登录口令(users.login_password,可空):
设置后该账号用户名直登(/login/username)须带匹配 ?password=,且优先于全局共享口令门;
未设口令的账号行为不变。前端登录 URL 已支持透传 ?password=,无需改登录页。

- 后端:UserRow/User 新增 login_password + 仓储映射;/login/username 账号级口令闸门
  (_lookup_user_by_username 仅查不建);新管理员接口 PUT /users/{id}/login-password;
  UserListItem 暴露明文(仅 admin 列表);migration 20260625_04 + 幂等补列 SQL。
- 前端:白名单页新增「登录口令」列(设置/修改/清除 + 复制登录链接);
  core/auth api+hooks 增 setUserLoginPassword/useSetUserLoginPassword(含 Y-Log)。
- 文档:根 + 后端 CLAUDE.md 同步。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:03:39 +08:00
董宏凯 45315f18d2 Merge branch 'dhk-1' into 'master'
fix(ai写作): 恢复内网直连检索(知识库检索)空地址回退默认端点

See merge request knowledge-base-bj/deerflow-server!154
2026-06-26 16:02:52 +00:00
董宏凯andClaude Opus 4.8 e889cc25cf fix(ai写作): 历史记录 in_progress 不再误显示「进行中」转圈
刷新=重置后,开始过但没跑完的会话 DB status 永远停在 in_progress、且无任何活进程,
历史列表却转圈显示「进行中」,误导用户以为还在运行。改为中性、不转圈的「未完成」
(text-slate-400)。真正在跑的 background(后台挂起,有 executor 驱动)保留「挂起中」转圈。
改 WRITING_STATUS_PILL(侧栏列表) + STATUS_LABELS(下拉)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:01:32 +08:00
董宏凯andClaude Opus 4.8 8385d1913a fix(ai写作): 继续从checkpoint续跑(修resume_is_map) + 刷新=重置不重连
后端 worker(runtime/runs/worker.py): 空 command(无 resume/update/goto)不再构造
Command()(会因无挂起 interrupt 抛 'resume_is_map' 未绑定错误),改为 astream(None)
让 Pregel 续跑 checkpoint 里挂起的当前阶段节点。有 resume/update/goto 时(4个干预卡)
仍走 Command,行为不变。前端"继续"发空 command:{} 即从当前阶段续跑、不回 intent。

按用户要求把"断网/刷新自动重连续跑"整套移除(刷新=重置即可):
- 删 sessionStorage 持久化 + reconnectTargetRef + 恢复/持久化 effect + host rehydrate
- 删 buildRehydrateSnapshot/rehydrate action(保留 clear_streaming)
- 提交去掉 onDisconnect:'continue'/streamResumable;reconnectOnMount 改 false
- 卸载不再自动后台挂起;刷新断 SSE 后端默认 cancel 取消 run,页面回 idle

手动暂停/继续保留:暂停=stream.stop(),继续=clear_streaming+空command 从checkpoint续。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:44:37 +08:00
董宏凯andClaude Opus 4.8 9f462beec5 fix(ai写作): 继续(暂停后)从checkpoint续跑而非从头重跑意图
根因:resumeWriting 的 stream.submit(undefined) 没带 command。后端 worker
(runtime/runs/worker.py:283) 只有收到 command 才翻成 Command() 让 Pregel 从最近
checkpoint 恢复;不带 command 时按 graph_input 起新一轮=从图入口 intent_parser
重跑,于是"规划大纲/检索素材时暂停后继续又从识别意图开始"。

修复:继续时带空 command:{},走 Command 恢复路径(与4个干预卡续跑同机制),
Pregel 从 checkpoint 续跑当前未完成阶段(已完成阶段不丢),不再重跑 intent。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:31:48 +08:00
董宏凯andClaude Opus 4.8 4f0c75e76e feat(ai写作): 暂停/继续 + 断网刷新后自动重连续跑
需求:写作任一活跃阶段(素材搜集/大纲/写作/审核中)断网也能继续。

断网续跑(纯前端,后端无需改):
- 所有 stream.submit 加 onDisconnect:'continue'(断开后端不取消 run,继续跑到
  下一个确认点等待);不开 streamResumable(避免与 rehydrate 重复累积时间线)
- 活跃 sessionId 存 sessionStorage,刷新后渲染期同步恢复 → threadId 首帧即交给
  useStream(reconnectOnMount) 重连该 run
- 重连后用 graph values 快照 buildRehydrateSnapshot 重建 reducer(rehydrate
  action),素材/大纲/草稿/审核/时间线立刻回显;currentPause 由 interrupt effect 设
- 去掉卸载时"自动后台挂起跑完"(会自动替用户确认),改为自然续跑等待;显式后台挂起按钮保留

手动暂停/继续:
- ENABLE_PAUSE_RESUME_UI 开启;暂停=stream.stop()(已完成阶段已落 checkpoint),
  继续=clear_streaming 后从 checkpoint 重新 submit 续跑(重跑当前阶段,已完成不丢)
- 按钮"停止"→"暂停"(Pause 图标)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:20:48 +08:00
董宏凯andClaude Opus 4.8 2097587b42 feat(ai写作): 左侧研究栏加历史记录列表(参考会商样式)+侧栏可收起
- ResearchSidebar 底部新增「历史记录」列表,跟随侧栏自然滚动;样式对齐
  多智能体会商 HistoryDropdown(圆角卡片+divide-y+状态药丸+打开/改名/删除)
- 点历史记录在右侧区域显示历史回看,左侧栏保留(showSidebar 含历史态)
- 侧栏顶部加「收起」按钮,收起为细窄展开条(页面承载 sidebarCollapsed)
- 「挂起中」记录点击经 window 事件 ai-writing:show-progress 打开进度流程图弹窗

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:54:10 +08:00
董宏凯 53af352ce8 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-26 22:14:31 +08:00
董宏凯andClaude Opus 4.8 0089af94d3 fix(ai写作): 恢复内网直连检索(知识库检索)空地址回退默认端点
knowledge_base 检索类型遇到空 intranet_search_url 会短路报「未配置」、返回 0
素材,导致「内网直连检索不能用」。旧版 get_search_provider(search_provider=
intranet) 在空地址时由 IntranetSearchProvider 回退内置默认内网端点,所以不配
地址也能直连。本次去掉该短路,空地址原样下传由 provider 回退默认端点,恢复旧逻辑。

- researcher.py: knowledge_base 分支无条件构造 IntranetSearchProvider
- test_ai_writing_knowledge_base.py: 空地址用例改为断言回退默认端点仍出素材
- backend CLAUDE.md: 检索类型段说明更新

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:13:19 +08:00
吉鑫升 26ab6f663c Merge branch 'ji' into master
完善并发监控图表(整数刻度Y轴 + 图例 + 说明)
2026-06-26 21:23:40 +08:00
吉鑫升andClaude Opus 4.8 bce0abba6e fix(监控): 完善并发监控图表——整数刻度Y轴 + 图例 + 说明
- 修复并发量图左轴出现「0 0 0 1 1」重复刻度:值很小时改用整数刻度(nice
  ticks,0/1/2…无重复),并修正按 axisMax 归一
- 每张图加图例(带当前读数)+ 一句中文说明,讲清每条线含义:
  并发量=峰值(红)/平均(橙);出字速度=平均(蓝)/最慢(紫)
- 修复两张图共用同一个 SVG 渐变 id 导致的颜色串用(改 useId 唯一化)
- 空数据态更友好(虚线占位 + 引导开启采样)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:23:08 +08:00
董宏凯 c732fd75e4 Merge branch 'dhk-1' into 'master'
feat(定时任务): 新增 9 个 HTML 模板,下拉补齐为 no.1~no.18(只展示)

See merge request knowledge-base-bj/deerflow-server!153
2026-06-26 12:51:31 +00:00
董宏凯 89232d7521 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-26 20:50:47 +08:00
吉鑫升 9ef8b1990e Merge branch 'ji' into master
实时并发监控 + 真正停止对话 + 409 中文化(管理员排行榜)
2026-06-26 20:40:56 +08:00
吉鑫升andClaude Opus 4.8 26f8010a57 feat(监控): 管理员排行榜实时并发监控 + 真正停止对话 + 409 中文化
- 排行榜页顶部新增「实时并发监控」面板(admin-only):当前并发大模型调用数 +
  调用列表(用户/智能体/类型/已运行时长),每行「停止」+「全部停止」经
  RunManager.cancel 真正中断 LLM 流式调用,减轻大模型压力
- 并发量折线图(峰值/均值,按分钟/小时):新增轻量后台采样器写入新表
  concurrency_samples(create_all 自动建表),可按分/时聚合
- 大模型出字速度折线图(tokens/秒,按分钟/小时):复用 llm_call_metrics 聚合,
  便于定位是模型/供应商慢
- 后台采样可开关、默认关闭(opt-in)、运行时切换免重启、对线上零影响
  (单条极小 INSERT、保留期清理、查询有上限、全程 best-effort)
- 409 中文化:上一轮未结束又发新问题时返回中文提示,前端 onError 友好映射

后端:runtime/runs/manager.py(list_active)、persistence/concurrency(新模块)、
config/system_settings(ConcurrencyMonitorSettings)、gateway/routers/admin_active_runs、
app.py(采样后台任务)、services.py(409)。前端:core/admin/{active-runs,hooks}、
components/workspace/admin/concurrency-monitor、AdminUserLeaderboardPage、threads/hooks。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:29:26 +08:00
董宏凯andClaude Opus 4.8 abf97cdc78 feat(大屏绘制): 战情大屏隐藏顶部标题区,只保留导航 Tab
DashboardApp 顶部 Header(标题+副标题+生成时间)在 usingPages 模式下整块隐藏,
dashboard-agent 战情大屏直接从导航 Tab 起;会商 Step3 大屏行为不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:36:29 +08:00
fengxiangdebaozi 4d10590511 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-26 11:04:19 +08:00
fengxiangdebaoziandClaude Opus 4.8 3d60651491 feat(ai-writing): 检索类型加回「知识库检索」(直连内网 ES 接口)
弱模型常识别不了配置技能(deepseek-chat 等),恢复旧的"不用技能"检索路径
作为独立检索类型 knowledge_base:绕过技能编排,直接调
ai_writing.intranet_search_url 内网知识库 ES 接口(IntranetSearchProvider)
对检索词并发检索;未配地址 → 无素材 + skill_error 告警。

- researcher.py: material_source 归一保留 knowledge_base + 新增直连内网分支
- state.py: material_source 三值注释
- 前端 AIWritingForm/WritingFormContext/types: 下拉新增「知识库检索」
- tests/test_ai_writing_knowledge_base.py: 直连/未配/不编排技能 3 用例
- backend CLAUDE.md: 检索类型段更新为三值

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:04:57 +08:00
董宏凯andClaude Opus 4.8 a400989f64 feat(定时任务): 新增 9 个 HTML 模板,下拉补齐为 no.1~no.18(只展示)
- 从 zt-0625 加入 no.8/no.9/no.10/no.11/no.14/no.15/no.16/no.17/no.18
- 新 no.8/no.9 的 id 用 no8b/no9b(no8/no9 已被显示名 no.12/no.13 的模板占用)
- 重排 TEMPLATES 使下拉按显示名升序 no.1→no.18;均 convertible=False 只展示
- 测试覆盖全部 id 存在、显示名覆盖 1~18、各模板文件可读为完整 HTML

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:55:34 +08:00
董宏凯 2a1f50f726 Merge branch 'ji' into 'master'
fix(ai写作): 弱模型也能真正执行技能脚本(执行优先的强提示)

See merge request knowledge-base-bj/deerflow-server!152
2026-06-25 15:00:57 +00:00
fengxiangdebaoziandClaude Opus 4.8 158d157af4 feat(ai写作): 内网检索接口执行诊断写入 material_package.intranet_debug(排查用)
- material_package 新增 intranet_debug:是否配置/端点/知识库/超时/每个检索词命中数与
  报错/原始响应片段/总命中数/是否被采用为兜底/status(not_configured|pending|ran|
  cancelled|crashed)。随 values 事件回前端,前端忽略该字段,仅供 devtools 排查。
- 新增 config ai_writing.intranet_search_debug(默认 false):开启后即使技能成功也跑完
  内网并记录完整结果,用于验证内网接口是否真能通。
- 前端不改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:55:27 +08:00
fengxiangdebaoziandClaude Opus 4.8 1ed004fbb1 fix(ai写作): 弱模型也能真正执行技能脚本(执行优先的强提示)
弱模型在 lead agent 能执行 py 脚本、在 ai写作子智能体却不能:根因是子智能体的
_MATERIAL_OUTPUT_CONTRACT 过度强调"只输出 JSON",弱模型据此跳过工具执行直接吐空 JSON。
对齐 lead agent 的 skill_system 强框架,把契约改成"执行优先":

- 第一步强制先用 bash 实际运行技能脚本(给出 cd 技能目录 && python3 scripts/..py 的命令形态),
  严禁不调用工具就直接输出 JSON/凭记忆编造
- JSON 输出降级为第二步"检索完成后再整理"
- task 末尾追加"现在立刻开始执行检索"的强指令,进一步推动弱模型先动手

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:50:10 +08:00
吉鑫升 38f98cdf5c Merge branch 'ji' into master
# Conflicts:
#	offline-backend-20260512/backend/scripts/sql/column_additions_mysql.sql
2026-06-25 21:49:12 +08:00
吉鑫升andClaude Opus 4.8 a417124c21 feat(auth): 登录白名单细化为黑名单模式(approved 默认放行)+ 同步构建产物
- approved 放行标记默认 True:新账号首次登录即放行,登录白名单开关打开时只拦截
  被管理员显式「取消放行」(approved=False) 的用户;enforce_user_approved 放在
  各登录端点拿到 user 后、签发 JWT 前调用,admin 永远放行。
- 同步调整 LoginWhitelistPage / local_provider / models / system_settings /
  迁移 20260625_02 / user model / column_additions_mysql.sql 与 CLAUDE.md。
- 更新 backend/backend.zip 构建产物。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:41:15 +08:00
吉鑫升andClaude Opus 4.8 f3f11461de fix(open-3qfx): 免登录建会话补 default 用户上下文,修复 thread_meta 不落库
开放接口 /api/open/3qfx/ask 免登录、无 user context,start_run 写 thread_meta
前先 thread_store.get() 默认 user_id=AUTO,resolve_user_id(AUTO) 在无 context 时
抛 RuntimeError 被 start_run 的 try/except 当「非致命」吞掉,thread_meta 行从未
创建,前端按 metadata.taskId 怎么也搜不到这条会话(run 却已建)。

- 调 start_run 前显式 set_current_user 为 default 用户桶(后台 run_agent task
  在 create_task 时快照此 context,故后台运行也归属 default,符合任务深链对话
  全局共享、按创建者解析文件的语义),thread_meta 正常落库。
- 把 cop_task_detail_* 配置补到实际加载的 offline-backend-20260512/config.yaml
  (此前只加在不生效的 backend/config.yaml)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:40:57 +08:00
董宏凯 2e79cee24a Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!151
2026-06-25 13:30:25 +00:00
fengxiangdebaozi 1fd997303e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-25 21:29:14 +08:00
吉鑫升 cc017a22c7 Merge branch 'ji' into master 2026-06-25 20:38:33 +08:00
AdministratorandClaude Opus 4.8 5f01e8dd03 feat(message-token-usage): 速度条图例按当前速度只显示对应颜色
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:29:26 +08:00
fengxiangdebaozi ce69905950 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-25 19:54:58 +08:00
fengxiangdebaozi 71e7b9dd63 代码提交 2026-06-25 19:54:37 +08:00
吉鑫升andClaude Opus 4.8 58357547c9 Merge branch 'ji' into master
按钮管理「登录参数」两套实现冲突解决——两分支各自独立给跳转按钮加了登录态追加能力,
二者正交、保留两套,互不影响:
- master: login_params (JSON 列 login_params_json,从 userInfo 解析,类似轻应用登录参数)
- ji: append_auth + auth_token_param + auth_name_param (token 登录时追加 token1+用户名)

迁移 revision 碰撞修复(两分支都用了 20260625_01):重编号为线性链
  20260620_02 → 20260625_01 (login_params) → 20260625_02 (auth_params) → 20260625_03 (users_approved)
并同步 column_additions_mysql.sql 注释里的 migration 编号。

前后端 task_buttons(model/sql/memory/router/前端 type·api·admin 页)均保留两套字段;
两份 CLAUDE.md 文档合并为同时记录两种机制。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:36:43 +08:00
AdministratorandClaude Opus 4.8 fe7c6cb8f7 feat(open-3qfx): 开放接口按 taskId 直接发起 3qfx 第一次问答
新增 POST /api/open/3qfx/ask(无需登录,/api/open/ 白名单 + CSRF 豁免):
只传一个 taskId,后端解析 3Q 业务配置的智能体 + 按 id 拉取 cop-task-detail
拼出与前端一致的开场白,新建带 metadata.{taskId,agent_id} 的真实会话并后台
启动持久化运行,立即返回 thread_id(随后在 3qfx 任务工作区对话列表观看)。

- start_run() 增加 keyword-only enforce_access(默认 True);开放接口传 False
  绕过按当前(无)登录用户的智能体可见性校验
- task_deeplink 配置新增 cop_task_detail_url / _authorization / _test + mock
- tests/test_open_3qfx.py(10 项,tests/ 被 .gitignore 故未入库)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:19:17 +08:00
fengxiangdebaoziandClaude Opus 4.8 dc5b83e8cd Merge branch 'dhk-3': 会商第一步分析记录按业务链筛选
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:09:37 +08:00
fengxiangdebaoziandClaude Opus 4.8 27fffbec61 feat(会商第一步): 分析记录按业务链筛选(漏斗下拉, 选项从草稿chainTitle去重)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:08:36 +08:00
AdministratorandClaude Opus 4.8 d35d121261 feat(auth): 登录白名单也拦「接口问答」(已登录的 run 接口按用户放行)
开关开启时,未放行用户(非 admin)即便持旧 token,直接调发起问答的 run 接口
(/api/threads/{id}/runs[/stream|/wait]、/api/runs/{stream|wait})也返回 403,
不止拦登录。在 AuthMiddleware 解析出 user 后仅对问答 run 请求复用
enforce_user_approved;GET 列表/join/cancel/feedback 及普通浏览不受影响;
内部/渠道调用与无鉴权 /api/open/chat 不在此范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:37:47 +08:00
fengxiangdebaozi 368673c3b4 Merge branch 'dhk-1': ai写作技能检索稳定接收数据 + 内网兜底优化 2026-06-25 17:06:01 +08:00
fengxiangdebaoziandClaude Opus 4.8 854de9b43c fix(ai写作): 技能检索稳定接收数据(复用参考文献提取+截断抢救+上限早停)
- 复用主智能体「参考文献」模式的提取逻辑(deerflow.runtime.references)解析技能JSON,字段并集兼容
- bash输出中间截断时逐条抢救完整记录对象,不再因大JSON解析失败而全丢
- 所有退出路径(超时/递归上限/异常)都从tool输出收割,不依赖模型最终汇总
- 达到max_materials上限即停止驱动模型,修复步骤条爆100+条/素材超额
- 步骤条实时显示每次检索获得多少条数据
- 内网兜底:与技能并行预跑、响应解析兼容两套接口字段、0结果换简单检索词重试、超时放宽到10分钟

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:05:09 +08:00
Administrator 0aa55eb333 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-25 16:09:51 +08:00
AdministratorandClaude Opus 4.8 1d1ab384ef feat(auth): 登录白名单(注册后管理员审批放行 + 全局开关)
用户仍可自助注册,普通账号默认 approved=False(待审批),admin 建号即 True;
存量用户迁移回填 True 避免锁死。系统设置 login_whitelist.enabled 总开关:关=
所有人可登录,开=仅已放行用户(或 admin)可登录。三登录端点建号后、发 token 前
经 enforce_user_approved 拦截,未放行返回 403 PENDING_APPROVAL。管理员在「白名单
管理」页(设置下拉入口)切总开关 + 逐用户放行。取消放行只拦下次登录。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:09:38 +08:00
吉鑫升andClaude Opus 4.8 864224e07c feat(message-token-usage): 速度条下方常驻颜色含义图例
生成结束后在速度条下方展示一行图例(SpeedLegend),把进度条配色归并为用户视角的
延迟正常/微长/长 + 无响应四类说明,帮助用户理解速度条颜色代表的模型生成延迟状态。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:07:54 +08:00
吉鑫升andClaude Opus 4.8 04b38a4a25 fix(task-buttons): append_auth 显式 server_default 修复旧库 /api/task-buttons 报 500
旧库已存在 task_buttons 表时,启动期自动补列 _ensure_orm_columns_sync 会跳过
「NOT NULL 且无 server_default」的列,导致 2026-06-25 新增的 append_auth 永远补不上,
GET /api/task-buttons 查询报 Unknown column → 500。给该列加 server_default="0" 后,
重启即自动 ALTER TABLE 补列,与迁移 20260625_01 保持一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:57:26 +08:00
吉鑫升andClaude Opus 4.8 f8b40353ef feat(task-buttons): 跳转按钮可追加登录态 token+username + token 登录离线测试映射
rwfx/3qfx/xdfx 任务工作区的可配置跳转按钮(按钮管理)新增「追加登录态」开关:
url 类型按钮可在外链后拼 ?token=<token1>&username=<登录名>(参数名可配,默认
token/username),供跳转目标外部系统识别当前登录用户。

- 仅 token 登录(login/token)生效: token1 = ?authToken=,username = 后端用
  token 调 getTokenInfo 解析出的原始登录名(非邮箱前缀/非解 JWT)。二者登录时
  经 persistLoginCredentials 存入 login.token1/login.username(scoped-storage
  自动隔离嵌入/独立会话);非 token 登录读不到 token1 则跳过追加。
- 后端 UsernameLoginResponse 回传 username(原始名,规避邮箱归一化有损);
  token_login 加 mock_tokens 离线测试映射(123ewq->admin),命中即跳过外网
  getTokenInfo,便于本地全链路联调。
- task_buttons 加列 append_auth/auth_token_param/auth_name_param
  (migration 20260625_01 + 幂等 SQL)。

Tests: test_task_buttons.py, test_auth_login_modes.py(mock token/username 回传)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:52:07 +08:00
fengxiangdebaoziandClaude Opus 4.8 893f1e6933 fix(会商第三步): 流程图入库限 rwfx-A + 修复跳过报告生成流程图出现双调度中卡片
- 入库功能仅 rwfx-A(businessCode 6BF)深链会商模式提供,其它业务深链生成流程图不再出现入库按钮/询问
- 流程图抽取串行化:auto 自动出图与用户「跳过报告直接生成流程图」手动出图竞态时,先中止上一轮、复用/移除被取代气泡,保证任意时刻只有一张流程图卡片

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:29:45 +08:00
fengxiangdebaoziandClaude Opus 4.8 04e6d0aa00 fix(ai写作): 前端实时接收技能步骤(此前被白名单丢弃,只在快照里一次性显示)
根因在前端:后端 _emit_skill_step 以 writer({"type":"skill_search_step", **step})
发出,**step 里的 type(invoke_skill/list_skills/skill_result/select_skills/
match_skills/skill_error/fallback/stop_early)会覆盖外层 "skill_search_step",
所以 custom 事件实际 type 是这些内层值。customToProgressEvent 的白名单没有它们,
直接 return null 丢弃——后端逐条实时发(SSE id 时间戳已证实分散在 0~76s),前端却
全扔了,技能步骤只能等节点结束的素材包快照一次性出现(前端一直 loading)。

修复:customToProgressEvent 按内层 step 类型识别,实时归一成 skill_search_step
事件(与快照里的 step 同格式),让技能调用过程一步步实时进时间线。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 01:20:21 +08:00
fengxiangdebaoziandClaude Opus 4.8 1d6659f9e7 fix(ai写作): 技能步骤从节点协程 emit,custom 事件真正实时刷给前端
前端/SDK 都是实时的(onCustomEvent 逐条即触发,draft_chunk 同链路就实时)。
问题在后端:技能步骤原来在节点内的嵌套 agent.astream 里直接 emit,custom 事件
被攒到整段跑完才一次性吐到 SSE(后端在跑、前端一直 loading)。

改为:嵌套 astream 跑后台任务,只把工具调用塞进 asyncio.Queue;真正的 writer
由 researcher 节点**自身协程**从队列取出来调(与 draft_chunk 同一上下文),
custom 事件即被 LangGraph 实时刷给前端。撤掉上一版无效的 sleep(0)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 01:04:49 +08:00
fengxiangdebaoziandClaude Opus 4.8 a4a67c1a00 fix(ai写作): 技能流式每步主动让出事件循环,custom 事件实时刷给前端
前端 skill_search_step 已是 pushImmediate(实时渲染),但事件在节点内的嵌套
agent.astream 里发出后没及时让出事件循环,被攒到整段跑完才一次性吐到 SSE,
前端看着像卡死。每个 chunk 处理完 await asyncio.sleep(0) 主动让出,促使刚 emit
的 custom 事件实时刷给前端(对齐 draft_chunk 的实时行为)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:48:57 +08:00
fengxiangdebaoziandClaude Opus 4.8 8f773d3f1b fix(ai写作): 技能调用实时流式显示 + 去掉大纲拼检索词/复用检索词
- 技能调用过程改为实时流式:run_skill_via_real_agent 不再 to_thread 阻塞跑
  executor.execute(),而是复用其 agent/state 构建后在图事件循环里 astream,
  每个工具调用(读 SKILL.md / bash / 检索)实时推一条步骤到时间线——内网慢时
  用户能看到技能在执行,不会以为卡死。整段 asyncio.wait_for 卡 5 分钟超时。
- 回退检索词相关改动(实测无用、且结合大纲后检索词变长不利按词匹配):
  - intent_parser 不再顺便生成 keywords,删 precomputed_keywords 状态字段;
  - researcher 不再复用,每次自己现生成检索词(回到原行为);
  - 检索词提示词去掉「结合大纲/{outline_section}」,删 keyword_outline_section。
  保留:关思考、技能真实运行时、技能目录修复、内网兜底。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:33:57 +08:00
fengxiangdebaoziandClaude Opus 4.8 c0305c9269 fix(ai写作): 修复内网技能检不出东西(技能目录路径 + 模型 + 兜底 + 开关)
对比主智能体与 AI 写作两份调用日志定位到根因:真实 agent 路径只注入了
SKILL.md 内容、没告诉模型技能在容器内的目录,模型用相对路径跑脚本
(python3 scripts/search)→ No such file。主智能体则 cd 进技能绝对目录再跑。

- _skill_agent:注入 skill.get_container_path() 的绝对目录,要求先 cd 再执行
  脚本/用绝对路径(对齐主智能体 skill_view 拿 location 后的行为)。
- researcher:跑技能的 agent 改用用户选定的写作主模型(model_name_fallback=None),
  不再用常配成外网的 keyword_model,避免技能 agent 连不上自己的模型。
- researcher:所有技能都 0 条时,最终兜底直连内网 ES 接口(IntranetSearchProvider),
  受 ai_writing.intranet_search_url 控制。
- config.yaml:开 researcher_skill_agent=true(内网技能必须走真实运行时),
  新增 intranet_search_url / intranet_knowledge_base 兜底配置项。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:52:32 +08:00
吉鑫升andClaude Opus 4.8 6989dd45f1 fix(chat): 问答回答时间戳不再漂移到当前电脑时间
AI 回答无自身时间戳,原先在首次渲染时用 new Date() 兜底,导致
未在原会话完成、或从历史重新加载的回答显示加载时刻而非真实时间。
改为让 AI 回答继承同轮提问的可靠 client_ts(turnTime),并停止给
AI 消息打 now 戳;显示层优先用提问时间,可自愈历史错误时间。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:57:21 +08:00
fengxiangdebaoziandClaude Opus 4.8 d8b43e7579 fix(ai写作): 技能检索超时硬编码 5 分钟(内网慢检索不被过早切断)
内网检索很慢,原 120s 在三个真正生效的层把检索切断。统一硬编码 300s,
不依赖环境变量(部署方不一定会配):
- researcher._SKILL_SEARCH_TIMEOUT_SECONDS:120(env) → 300(写死),删掉 _env_int;
  作用于直连/定向技能检索的 wait_for,也传给真实智能体路径。
- IntranetSearchProvider 默认 httpx 超时:120 → 300(直连内网 ES 的真正瓶颈)。
- run_skill_via_real_agent 整段运行上限:max(timeout_s, 300),至少 5 分钟。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:54:42 +08:00
fengxiangdebaozi 4f68235c94 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-24 22:45:42 +08:00
吉鑫升 c1143564ef Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-24 22:42:21 +08:00
吉鑫升andClaude Opus 4.8 ea2ea9783e feat(task-deeplink): xdfx 开场信息追加行动标题与行动内容
xdfx(行动分析)深链开场信息原先只附行动id(action_id),现额外
拼上行动标题(actionName)与行动内容(narration)。优先读登录时
缓存的行动详情(readActionDetail),无缓存再 fetchActionDetail,避免
重复请求。buildOpeningText 第三参数由 actionId 改为 ActionOpeningInfo。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:34:25 +08:00
吉鑫升andClaude Opus 4.8 e3e93712b6 feat(task-deeplink): xdfx 开场信息追加行动标题与行动内容
xdfx(行动分析)深链开场信息原先只附行动id(action_id),现额外
拼上行动标题(actionName)与行动内容(narration)。优先读登录时
缓存的行动详情(readActionDetail),无缓存再 fetchActionDetail,避免
重复请求。buildOpeningText 第三参数由 actionId 改为 ActionOpeningInfo。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:33:09 +08:00
fengxiangdebaoziandClaude Opus 4.8 afb78a6943 feat(ai写作): 素材收集专家按真实智能体运行时跑技能 + 步骤条对齐
- 新增 run_skill_via_real_agent:复用 SubagentExecutor(与 lead agent / 子智能体
  同一套运行时),加载技能 SKILL.md + 全套工具 + 懒加载沙箱,让技能的真实检索逻辑
  (读文件 / 调内网接口 / MCP)真正执行,再按 JSON 契约收割结构化素材;researcher 的
  _execute_skill 在 researcher_skill_agent 开启时切到它,失败/空结果回退直连检索。
  解决内网技能(在别的智能体里好使)在 AI 写作里检不出东西的问题。
- 步骤条轻量对齐别的智能体:技能调用拆成「读取 SKILL.md → 执行检索 → 检索到 N 条」
  三段(前两段新增,结果段沿用 skill_result);前端 SkillStepsCard 零改动。
- 含 roundtable-planning(业务链选择 / Step2 编排)的进行中改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:19:30 +08:00
董宏凯andClaude Opus 4.8 1a127ef602 chore(定时任务): 替换 no.3 模板内容为 no.3-1.html(仍只展示)
- templates/no3/no3.html 覆盖为新版(73KB,const DATA = {…} 内联)
- id/name/注册不变,仍为 convertible=False 只展示

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:17:24 +08:00
AdministratorandClaude Opus 4.8 acc29c8121 fix(middlewares): 标题模型推理不再泄漏到 UI 流 + open/ephemeral 会话避免无谓 DB 访问
- title: 标题模型用隔离 config(不继承父级 streaming callbacks),force_disable_thinking,<think> 块清洗,避免推理 token 进入实时流/运行日志
- uploads: 无文件的普通对话轮次直接跳过,不再解析线程归属(避免命中 DB)
- thread_data: ephemeral 会话用当前登录用户解析数据目录
- thread_paths: open-/__skill-review__ 等合成线程视为 local-only,直接用当前用户
- frontend: 新增 build:test 构建脚本

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:20:12 +08:00
董宏凯andClaude Opus 4.8 ca31490241 chore(定时任务): no.9 模板显示名称改为 no.13(id 仍为 no9)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:11:33 +08:00
董宏凯andClaude Opus 4.8 6c42f86e30 chore(定时任务): no.8 模板显示名称改为 no.12(id 仍为 no8)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:09:56 +08:00
董宏凯 5b818836d7 Merge branch 'dhk-1' into 'master'
fix(登录): redirect 透传登录 URL 其余 query 到目标页

See merge request knowledge-base-bj/deerflow-server!150
2026-06-24 11:49:00 +00:00
fengxiangdebaozi 3ea8e73389 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-24 19:48:15 +08:00
fengxiangdebaoziandClaude Opus 4.8 c117e296cb feat(ai写作): 草稿局部修改只改命中章节,支持改章节标题,修改时右侧流式重写
用户在草稿确认填修改意见(user_revise)时,按章节定位只重写命中章节、
其余字节级保留;新增范围识别区分「改标题」与「改正文」,改标题同步回写
outline 使其持久生效(修复改章节标题不生效);整体性/结构性意见自动回退
整篇重写。前端新增 isRevising:修改时右侧先恢复初始占位(清空旧稿),
重写流到达后实时展示被改章节的流式重写。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:45:00 +08:00
fengxiangdebaoziandClaude Opus 4.8 42f55251f2 feat(ai写作): 历史回看右侧成稿可编辑并保存回历史记录
- 后端新增 PUT /api/ai-writing/sessions/{id}/draft(属主校验,draft_title 可选)
- 前端历史模式放开只读,工具栏新增「保存」按钮持久化修改
- 实时写作完成态保持原样(仅导出,不落库)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:32:11 +08:00
fengxiangdebaoziandClaude Opus 4.8 304e1b637a feat(ai写作): 意图解析顺便生成检索词 + 修复快捷模式
- 意图解析(intent_parser)与意图理解共用同一次 LLM 调用顺便生成检索词
  (结合意图 + 大纲覆盖各章节主题),存入 state.precomputed_keywords;
  researcher 首轮直接复用,省掉素材收集阶段单独的「生成检索词」LLM 往返。
  解析失败/为空时回退到 researcher 自己生成(优雅降级);re_search 仍重新生成。
  keyword_outline_section 提到 _utils 供两处共用。
- 修复快捷模式:之前被「大纲先行确认」改动拖累,先误成「问一堆问题」,再被我
  过度修正成「不生成大纲」。现修正为——快捷模式 = 用默认选项、不逐项澄清提问
  (make_lead_agent 在快捷模式下直接不挂 ask_clarification 工具,物理上问不了),
  但**仍生成并确认大纲**再提交;普通模式不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:32:10 +08:00
fengxiangdebaoziandClaude Opus 4.8 49173d9c28 feat(ai写作): 大纲整理可见化 + 检索词结合大纲 + 素材/大纲阶段关思考
- 草稿确认卡:「给出修改意见」「让编辑继续审核」移到「确认定稿」上方。
- 大纲整理阶段文案:用户自带大纲时显示「整理你的大纲」而非「规划大纲」
  (OutlineStreamingView / 骨架屏 / 状态栏,按 request.userOutline 切换)。
- 素材收集专家:用户自带大纲时把大纲原文带入检索词生成,让关键词结合写作
  意图覆盖各章节主题(_keyword_outline_section,agent + legacy 两路)。
- 素材收集(检索词/排序)与大纲整理/生成阶段强制关闭思考
  (create_chat_model force_disable_thinking:GLM thinking.disabled +
  Qwen/vLLM enable_thinking=false),关不掉的模型仍由 ThinkTagParser 把
  <think> 分流到思考框正常展示。disable_thinking 串入
  _utils.get_model/llm_json 与 _agent_runner.stream/run_writing_agent。
- 含草稿「局部修改」WIP(writer_draft 只重写命中章节)及外观控件改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:32:10 +08:00
董宏凯andClaude Opus 4.8 8678ceb422 feat(定时任务): 新增第九个 HTML 模板 no.9(只展示)
- no.9.html(160KB)复制进 templates/no9/;数据用 `const DATA = {…}`(= 前有空格,同 no.6),
  当前引擎正则要求 const DATA=,不匹配,故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.9
- 测试覆盖 no.9 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:17:56 +08:00
董宏凯andClaude Opus 4.8 31cd19aabb feat(定时任务): 新增第八个 HTML 模板 no.8(只展示)
- no.8.html(77KB)复制进 templates/no8/;数据存于 <script id="DATA"
  type="application/json"> 元素(同 no.4/no.7 元素注入),当前引擎只支持 const VAR= 行注入,
  故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.8
- 测试覆盖 no.8 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:16:34 +08:00
董宏凯andClaude Opus 4.8 60029ae3dc feat(定时任务): 新增第七个 HTML 模板 no.7(只展示)
- no.7.html(605KB)复制进 templates/no7/;数据存于 <script id="PAGE_DATA"
  type="application/json"> 元素(同 no.4 类元素注入),当前引擎只支持 const VAR= 行注入,
  故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.7
- 测试覆盖 no.7 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:13:38 +08:00
董宏凯andClaude Opus 4.8 4f62703477 feat(定时任务): 新增第六个 HTML 模板 no.6(只展示)
- no.6.html(266KB)复制进 templates/no6/;数据用 `const DATA = {…}`(= 前有空格),
  当前引擎正则要求 const DATA=,不匹配,故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.6
- 测试覆盖 no.6 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:09:47 +08:00
董宏凯andClaude Opus 4.8 c0af23aefe feat(定时任务): 新增第五个 HTML 模板 no.5(只展示)
- no.5.html(174KB)复制进 templates/no5/;数据用 `let DATA = {…}`(let+空格,非
  const DATA= 行),当前引擎行注入正则不匹配,故先注册为 convertible=False 只展示
- 前端下拉/参考列表自动出现 no.5
- 测试覆盖 no.5 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:51:12 +08:00
董宏凯andClaude Opus 4.8 6a6195f883 feat(定时任务): 新增第四个 HTML 模板 no.4(只展示)
- no.4.html(3.9MB)复制进 templates/no4/;数据存于 <script id="app-data"
  type="application/json"> 元素(非 const DATA= 行),当前引擎只支持 const VAR= 行注入,
  故先注册为 convertible=False 只展示,转换后续接(需扩展元素注入)
- 前端下拉/参考列表从 API 拉取,no.4 自动出现
- 测试覆盖 no.4 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:45:40 +08:00
吉鑫升 1a87077642 Merge branch 'ji' into master
并入: loop 跨事件循环 DB 访问修复(asyncmy attached to a different loop)、y-log 前缀、全角参考文献角标识别

# Conflicts:
#	CLAUDE.md
2026-06-24 17:13:45 +08:00
fengxiangdebaoziandClaude Opus 4.8 e9c8d1cea2 feat(ai写作): 自带大纲/会商及研究模板等前后端改动
提交工作区中 AI 写作(表单/时间线/历史/上下文/流式 hook/类型)、
研究模板配置页、lead_agent、researcher/graph/state 及 gateway/config
的进行中改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:15:39 +08:00
fengxiangdebaoziandClaude Opus 4.8 d2c76100f2 perf(ai写作): 用户自带大纲整理提速并流式化
用户自带大纲时「整理大纲」原有两个阻塞式非流式 LLM 调用,内网模型
<think> 关不掉导致用户长时间干等:

- 素材关联(每次都调)改为确定性词重叠 _assign_materials_heuristic,
  零 LLM、瞬时;有编号/标题/缩进的大纲整理几乎零等待。
- 散文化大纲的 LLM 归一改为流式 _stream_normalize_user_outline:
  ThinkTagParser 把思考推思考框、正文实时打字机展示并累积解析;
  并在 OUTLINE_NORMALIZE_USER 末尾追加 /no_think。
- researcher 路径只做确定性解析,解析不出返回 None,交给 writer_outline
  流式归一(outline_chunk 在该阶段天然映射大纲思考框)。
- 移除废弃的 OUTLINE_MATERIAL_ASSIGN_* 提示词与 llm_json 死引用。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:15:39 +08:00
吉鑫升andClaude Opus 4.8 6aff165d09 fix(db): 路由跨 loop 的 DB 访问回引擎主 loop,消除 asyncmy "attached to a different loop"
会商/定时任务/沙箱工具等在 worker 线程的临时 loop 上用 asyncio.run 跑全局引擎连接池的
DB 操作,连接被绑定到随即关闭的临时 loop;池子回收/释放该连接时 asyncmy
_terminate_graceful_close 在错误的 loop 上触发 future,持续刷
"got Future attached to a different loop"。

- engine.py: 新增 run_db_blocking(fire_and_forget_db_write 的带返回值版本)。离开主 loop
  时用 run_coroutine_threadsafe 把协程送回主 loop 执行、阻塞调用线程拿结果;仅在无主 loop
  或正同步处于主 loop 内(送回会死锁)时退回临时 loop。
- thread_paths / prompt / skill_tools: 三处同步→异步阻塞读桥(任务owner解析/常驻技能/
  技能可见性)改走 run_db_blocking,删除各自重复的 _run_coro_blocking。
- llm/tool metrics 中间件: fire-and-forget 写改走 fire_and_forget_db_write。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:57:04 +08:00
吉鑫升andClaude Opus 4.8 5ecf28e60c feat(y-log/knowledge): 日志前缀改 CM助手-全量 + 入库 nodeName 按标题去时间戳保持一致
1. Y-Log 上报 optDetail 前缀 B1-CM助手 → CM助手-全量(全量替换 44 个前端文件 + 文档)
2. 知识库入库 nodeName 改为只取 md5(标题).md(去掉时间戳),同一篇 md 每次入库一致,避免重复记录

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:46:51 +08:00
g zy fb103b6ad9 Merge branch 'gzy' into 'master'
Gzy

See merge request knowledge-base-bj/deerflow-server!149
2026-06-24 07:42:20 +00:00
Rocky Kwok 71a865dc0c Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-24 15:41:25 +08:00
Rocky Kwok 8ed54448c2 feat: 打开 browser_act 2026-06-24 15:40:56 +08:00
董宏凯andClaude Opus 4.8 5a2441cc9f feat(定时任务): 新增第三个 HTML 模板 no.3(只展示)
- no.3.html(3.8MB)复制进 templates/no3/,无 const DATA= 注入点 → 注册为
  convertible=False 只展示模板(同 no.2,后续接转换再开)
- 前端下拉/参考列表从 API 拉取,no.3 自动出现,无需前端改动
- 测试覆盖 no.3 存在/只展示/文件可读

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:38:49 +08:00
董宏凯andClaude Opus 4.8 089d3e171f fix(定时任务): 抽取开关只控制模板HTML数据,不再影响Markdown等正常操作
- 之前关闭抽取会整段跳过 gather/agent,连带把 Markdown 生成也跳过了
- 改为:抽取开关只决定 HTML 用「抽取数据」还是「模板默认数据」;require_markdown 独立——
  需要时照常跑 gather 产出 .md 并校验/带出,与开关无关
- gather 仅在 do_extract(开关开+有描述+可转换) 或 require_markdown 时跑一次,两者复用同一次
- 只展示模板恒不抽取,但仍尊重 require_markdown;_finalize_template_html 支持 extra_files 带出 .md
- 测试:关闭抽取+必须Markdown → 仍产出.md/不转换/HTML用默认数据

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:23:45 +08:00
xu hd ed71df1b60 Merge branch 'xuhd' into 'master'
fix(notebook): 来源搜索导入后立刻显示占位卡并刷新来源列表

See merge request knowledge-base-bj/deerflow-server!148
2026-06-24 05:15:19 +00:00
xuhdandClaude Sonnet 4.6 85cd2c888e fix(notebook): 来源搜索导入后立刻显示占位卡并刷新来源列表
- handleImport 成功后调用 onUploadComplete 通知父组件(原先缺失)并关闭弹窗
- 新增 pendingImportTitles state,同步导入后立刻渲染 PendingSourceCard
- sources 刷新后按标题匹配移除占位卡并自动选中新来源

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:14:36 +08:00
董宏凯andClaude Opus 4.8 6673121f3f feat(定时任务): 选内置模板时加「执行数据抽取」开关,关闭则直接用默认数据
- html_config.template_extract(默认关,仅参考选 tpl:<id> 时显示开关):
  关闭→跳过检索/转换,直接用模板默认数据填充;开启→才执行检索+转换抽取
- 后端 _run_html_template_reference 在只展示分支后先判开关
- 前端 ScheduledTasksPage:参考选内置模板时显示 Switch,存进 html_config.template_extract,编辑回显
- 测试覆盖 开/关 两条路径

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:09:16 +08:00
董宏凯andClaude Opus 4.8 4898ea0a85 build(harness): 显式把 no.1/no.2 HTML 模板纳入打包,保证新机器内置可见
- harness pyproject [tool.hatch.build] artifacts 显式包含 templates/**/*.html,
  任何构建方式(wheel/sdist)都带上模板(已验证 wheel 内含 no1/no2)
- 模板文件本就 git 跟踪、源码安装直读、内置 agent 启动自动 seed —— 新机器 clone
  装好即可直接看到
- 加自检测试 test_all_registered_template_files_exist

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:05:43 +08:00
董宏凯andClaude Opus 4.8 7d9a0637ca fix(定时任务): 模板子页面空内容回退原数据,避免子页面显示不全
- 引擎 merge_with_reference:按顶层键(=子页面)逐一合并,转换结果里没抽到有效内容的
  子页面/字段回退模板原有数据(_has_content/_merge_fill 递归,列表/标量整体保留,
  字典逐键回退空分支);整体为空等价于返回模板默认数据
- _run_html_template_reference:描述与任务prompt都为空→直接用原数据不检索;转换成功后
  对结果做 merge_with_reference 补空子页面
- _run_template_fill_attempt 同样套用 merge,空子页面一致回退
- 测试覆盖 _has_content/merge 各场景 + 端到端空子页面回退/空描述用原数据

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:20:12 +08:00
董宏凯andClaude Opus 4.8 ba885ca201 fix(定时任务): require_html 选内置模板时用真实模板出页,保留全部子页面
- 模板是 JS 驱动多页 SPA,子页面靠脚本从 DATA 渲染;之前用 style_reference 抽 CSS 摘要
  让模型自由渲染,结果只剩主页、子页面全丢
- 改为:_run_html_page_attempt 见 tpl:<id> 参考即提前分支到 _run_html_template_reference,
  用真实模板文件出页——可转换模板=检索数据→转换→fill_template;只展示模板=原样输出;
  转换失败兜底默认数据,保证页面与子页面始终可用
- 抽出 _convert_to_template_data / _finalize_template_html 复用,删除已废弃的 style_reference
- 测试覆盖:真模板保留 const DATA/script/id=tabs;只展示原样;转换失败回退

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:47:16 +08:00
董宏凯andClaude Opus 4.8 ec60b77a71 feat(定时任务): 必须生成HTML的「参考收藏页面」加入 no.1/no.2 模板作为风格参考
- 引擎 template_fill.style_reference:抽取模板 <style> CSS + 去脚本结构骨架(~13-28KB),
  剥掉巨大数据常量,供 require_html 渲染模型模仿风格;reference_template_id 解析 tpl:<id>
- 调度器 _run_html_page_attempt:参考来源为 tpl:<id> 时用模板风格参考,否则走收藏 store
- 前端 ScheduledTasksPage:require_html 模式下「参考收藏页面」列表加入「模板:no.1/no.2」,
  值存 tpl:<id>;htmlTemplates 在 require_html 时也拉取
- 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:09:48 +08:00
董宏凯andClaude Opus 4.8 ae3788a87d feat(定时任务): 模板数据转换器对话页加「页面模板+渲染预览」调试入口
- 后端 POST /api/scheduled-tasks/template-fill/render:不调模型,抽取回复里的 JSON
  → 校验 → 填充,返回完整 HTML + 校验错误/缺失字段(非法也照样填充便于看效果),
  只展示模板原样返回
- 前端仅 template-json-builder 智能体的 AgentChatPage:顶部加页面模板下拉 +「渲染预览」
  按钮,取最近一条 AI 回复填进所选模板,弹窗用 sandbox=allow-scripts iframe 预览 +
  校验状态;严格按 agent id 门控,不影响其他智能体对话页
- 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:00:18 +08:00
吉鑫升andClaude Opus 4.8 3e68260afa fix(citations): 识别全角参考文献角标(全角数字/合并/区间)
正文里的引用角标识别此前全角括号只支持单个半角数字 【1】,
导致 【1】(全角数字)、【1,2】/【1,2】/【1、2】(合并)、
【1-3】/【2~5】(区间) 等中文常见写法无法识别成参考文献。

补齐全角括号的「全角数字 / 合并 / 区间」三种形态,转成半角后交给
原有半角逻辑统一处理;仍保留 isValidIndex 闸门,避免把正文里的
【3】(越界)、【附录】(非数字) 误判为引用。新增 inline-citations.test.ts。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 22:14:52 +08:00
fengxiangdebaoziandClaude Opus 4.8 f551c7af9b fix(登录): redirect 透传登录 URL 其余 query 到目标页
?redirect=/page/strategy/dashboard-agent&taskId=3334 时把 taskId 等非登录消费参数
透传到目标页 query(目标自带同名参数优先),鉴权类参数不透传以防泄漏到地址栏。
修复跳转后目标页「缺少 taskId」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 22:10:40 +08:00
董宏凯 b1ef4ccb1a Merge branch 'dhk-1' into 'master'
feat(定时任务): 模板转换支持纯文本/含JSON文本/纯JSON三种源输入

See merge request knowledge-base-bj/deerflow-server!147
2026-06-23 13:53:18 +00:00
fengxiangdebaozi e3631a9fd6 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 21:52:27 +08:00
fengxiangdebaoziandClaude Opus 4.8 8241f8dd6b feat(登录): 新增 ?redirect= 参数,登录后跳转指定站内页面
支持 ?redirect=page/strategy/dashboard-agent(可不带前导斜杠、可自带 query),
优先级高于 taskId/goPath 深链与默认首页;仅接受站内相对路径,拒绝绝对/协议相对
URL 与反斜杠以防开放式跳转。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:51:59 +08:00
董宏凯andClaude Opus 4.8 3d2cc9e5cd fix(深链会商): Step2 文件产物虚拟预览仅在有内容时用,否则回退 HTTP 按创建者解析(修复跨账号看不到)
- useStep2Orchestration.openArtifactPreview: stub 无内容(str_replace/跨账号草稿恢复)时不再短路,
  回退到 artifacts HTTP 接口,并优先用子席位 thread(roundtable 线程→后端按对话创建者解析+放行跨账号)
- findSeatArtifactSource 一并返回 stub 捕获的 content 供判断
- 附:模板数据转换器「源数据抽取」手工测试清单文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:47:46 +08:00
fengxiangdebaozi e263d86fc3 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 21:22:02 +08:00
fengxiangdebaozi 151384b26d 代码修改 2026-06-23 21:21:49 +08:00
fengxiangdebaozi 030ede6887 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 21:07:05 +08:00
fengxiangdebaozi e6ca2bf64f Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 21:06:19 +08:00
fengxiangdebaozi 47cff9e2f9 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 21:06:09 +08:00
fengxiangdebaozi 6742346eb4 代码提交 2026-06-23 21:05:58 +08:00
吉鑫升 b5e682cfd2 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-23 21:01:05 +08:00
吉鑫升 2193c4484b Merge remote-tracking branch 'origin/master' into ji 2026-06-23 20:58:41 +08:00
Administrator 4fc3d7dae2 Merge remote-tracking branch 'origin/master' into ji 2026-06-23 20:57:01 +08:00
Administrator 6f0f9fb1fc Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	CLAUDE.md
2026-06-23 20:52:39 +08:00
AdministratorandClaude Opus 4.8 df9c59e21c feat(y-log): 问答按 LLM 调用各记一条 + 全量补齐各 API 操作埋点
- 问答日志从「每轮 onFinish 一条」改为 useThreadStream onLangChainEvent
  监听 on_chat_model_end,每次大模型调用结束各上报一条(含子代理)
- 广覆盖各 API client 成功路径:查看(view)/查询(search)/新增(create)/
  编辑(edit)/删除(delete)/启停(enabled·disabled)/导入导出,补齐此前
  整模块零埋点与漏掉的单条详情 GET;跳过高频 typeahead/轮询/bootstrap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:42:54 +08:00
AdministratorandClaude Opus 4.8 6131eda219 feat(y-log): 问答按 LLM 调用各记一条 + 全量补齐各 API 操作埋点
- 问答日志从「每轮 onFinish 一条」改为 useThreadStream onLangChainEvent
  监听 on_chat_model_end,每次大模型调用结束各上报一条(含子代理)
- 广覆盖各 API client 成功路径:查看(view)/查询(search)/新增(create)/
  编辑(edit)/删除(delete)/启停(enabled·disabled)/导入导出,补齐此前
  整模块零埋点与漏掉的单条详情 GET;跳过高频 typeahead/轮询/bootstrap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:41:07 +08:00
吉鑫升andClaude Opus 4.8 14082b3e7b fix(深链): 删除接口空响应体也判成功
dropSixOrSevenData 成功时返回空响应体,原 success: obj.success === true
永远为 false,导致删除已成功却总弹「清除失败/无法覆盖」。改为
success: obj.success !== false:请求未抛错即算成功,仅显式 {success:false} 才失败。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:28:18 +08:00
董宏凯andClaude Opus 4.8 00181ebde8 feat(定时任务): 模板转换支持纯文本/含JSON文本/纯JSON三种源输入
- template_fill.extract_embedded_json:字符串感知平衡扫描,从含 JSON 的文本里抠出
  内嵌 {…}/[…](纯文本返回 None)
- 调度器转换 prompt 预识别内嵌 JSON,单独标注「识别到的结构化数据」并保留原文;
  纯文本则让模型按语义理解;conversion prompt 重写覆盖三种形态
- 内置 agent SOUL 说明三种源数据形态
- 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:28:05 +08:00
董宏凯 320f2ef814 Merge branch 'dhk-1' into 'master'
feat(深链会商): rwfx-A/3qfx-A 首条消息附带 taskId,xdfx-A 附带 taskId+actionId

See merge request knowledge-base-bj/deerflow-server!146
2026-06-23 12:09:45 +00:00
fengxiangdebaoziandClaude Opus 4.8 8b022bffc5 feat(深链会商): rwfx-A/3qfx-A 首条消息附带 taskId,xdfx-A 附带 taskId+actionId
会商 Step1 自动发送的「任务信息」现追加任务/行动 id,与单智能体路径口径一致,
id 标签统一用驼峰(taskId / actionId)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:08:33 +08:00
董宏凯 5fddb08e04 Merge branch 'dhk-1' into 'master'
fix(roundtable): xdfx-A 深链会商聊天记录按行动id隔离存储

See merge request knowledge-base-bj/deerflow-server!145
2026-06-23 11:48:10 +00:00
fengxiangdebaozi 0fedb6ffd8 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-23 19:47:54 +08:00
董宏凯andClaude Opus 4.8 498e3c6a84 fix(深链): 「上一步」进大屏绘制页强制显示全局导航栏+侧边栏
- PageLayout 对 /strategy/dashboard-agent 路由强制 pageSidebarAllowed(不受界面模式/
  会话级 sidebar 标志影响;仍尊重 embed=1 嵌入态隐藏),避免从去 chrome 的深链工作区进来后整页无顶栏/侧栏
- 「上一步」按钮 openMode 改回 self(同窗导航;forceChrome 后同窗/新窗都能带回 chrome)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:47:02 +08:00
fengxiangdebaozi 85aa242fd5 Merge branch 'master' into dhk-1 2026-06-23 19:45:41 +08:00
fengxiangdebaoziandClaude Opus 4.8 6f94a40303 fix(ai-writing): 写作分栏拖拽条不再压住左侧滚动条
隐形拖拽热区只向右(草稿区留白)延伸 + 抓手微右移,避免鼠标抓左侧时间线滚动条时
误触左右 resize 导致滑不动;不留任何视觉间隙。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:37:30 +08:00
董宏凯andClaude Opus 4.8 34893007a7 feat(深链会商): rwfx-A 复用单智能体 3q 预检闸门,未完成引导前往三情会商(3qfx-A)
- 抽出共用组件 RwfxSqReportGate(单/多智能体共用),targetGoPath 参数化「前往」目标
- 单智能体 TaskFxWorkspace 改为 import 共用组件(默认 3qfx),清理因抽离失效的 import
- 多智能体会商 RoundtablePlanningPage 深链 rwfx-A 挂同一闸门,目标 3qfx-A(三情会商)
- 改一处两处都生效,避免双份逻辑漂移

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:26:00 +08:00
Administrator 053d3e300e Merge branch 'ji' into master (操作日志 Y-Log 上报)
# Conflicts:
#	CLAUDE.md
2026-06-23 19:22:53 +08:00
吉鑫升andClaude Opus 4.8 a1fbfaf864 feat(xdfx): 行动历史查询与删除(与 rwfx 共用 dropSixOrSevenData 删除接口)
- 删除接口统一为 dropSixOrSevenData:rwfx 传 ?taskId=、xdfx 传 ?actionId=,另一参数直接不出现在 URL
- xdfx 工作区新增「清除记录」按钮(按行动id 删除),删除前预检「行动历史是否存在」
- action_history_url 配置位(地址待提供,留空则不查询/不阻断删除)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:14:16 +08:00
fengxiangdebaoziandClaude Opus 4.8 b8c78ba06e feat(ai-writing): 大纲先行确认 + 新文章类型识别 + 参考文献 + 编辑器修复
- 写作配置 Q&A 改为「先流式生成大纲并多轮确认 → 再确认意图」,放宽轮数上限
- 意图识别出列表外的新文章类型时带上类型名+简介(仅本次写作临时使用)
- 成稿支持参考文献:正文 [n] 角标 + 文末按出现顺序的「参考文献」列表(表单开关,默认开)
- 流式接取未闭合 <think> 思考内容,首 token 前加「正在整理…」占位
- 修复右侧编辑器输入空格只出一次、光标跳字(避免自激回灌 setContent 重置光标)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:07:03 +08:00
董宏凯andClaude Opus 4.8 cd33aaeded feat(定时任务): 内置「模板数据转换器」按 HTML 模板填充 JSON 生成大屏
- 模板填充引擎 template_fill.py:模板注册表 + schema 骨架 + 结构校验 + 行级安全填充;
  模板自带 DATA 即 schema 来源。no.1 可转换填充(含 MANIFEST 更新时间刷新),
  no.2 暂为「只展示」(convertible=False) 原样输出已有数据
- 内置 agent template-json-builder(seed + SOUL),随 lifespan 注入,自动出现在
  定时任务「执行智能体」下拉
- 调度器新增模板填充执行分支 _run_template_fill_attempt:源 JSON→大模型转换→
  结构校验→失败回喂重试;空源/只展示模板直接用模板默认数据,绝不失败
- API:GET /api/scheduled-tasks/html-templates
- 前端:选中该 agent 后条件渲染「HTML模板」下拉 + .json 上传,只展示型模板隐藏 JSON 输入
- 文档同步(前后端 CLAUDE.md)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:00:00 +08:00
吉鑫升andClaude Opus 4.8 5aa66fc07d chore(backend): 放宽递归与循环检测阈值
- build_run_config recursion_limit 100 -> 250
- LoopDetectionMiddleware 默认阈值放宽(warn/hard/window/tool-freq)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:48:23 +08:00
吉鑫升andClaude Opus 4.8 23d170cde1 fix(rwfx): 3q 无数据时不默认发送问题,仅「不去3q分析」才发送
问题:rwfx 进入时的 3q 预检弹框与「目的树为空自动发送第一次问题」相互独立,
导致 3q 无数据时弹框出现的同时问题已被自动发出。

修复:新增 rwfx 专用 3q 门控(rwfx-sq-gate.tsx,RwfxSqGateProvider + useRwfxSqGate),
把自动发送交给门控——
- 3q 完成/无法判定 → 立即发送;
- 3q 未完成 → 弹「前往 3qfx」框并挂起发送,仅当用户「暂不(不去3q分析)」或关闭弹框才发送;
  点「前往 3qfx」则跳转并丢弃发送。
TaskPurposePanel 自动开始改调 gateStart(onStart);手动「开始问答」/覆盖确认不受影响。
替换原独立的 RwfxSqReportGate(它弹同一个框但没门控自动发送)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:48:23 +08:00
AdministratorandClaude Opus 4.8 d2be3b206d feat(y-log): 操作日志上报(前端直连 + 后端公开只读下发地址)
- 后端新增 y_log 公开只读配置:YLogConfig + AppConfig.y_log +
  GET /api/public/config/y-log,地址写在 config.yaml(mtime 热重载)
- 前端核心 src/core/log/:writeYLog(双门控/异步即发即忘/永不抛错/
  去ip-host/对象序列化裁剪/optResult恒0)+ 配置加载 + token1 隔离存取
- 登录(token)持久化 token1 并报登录、登出先报后清;启动预热配置
- 各 API client 成功路径埋点:敏感词/菜单/按钮/技能地址/轻应用、
  会商草稿+任务草稿+后台任务、智能体/定时任务/知识库、Word导出,
  以及问答完成钩子(ChatPage/AgentChatPage onFinish)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:46:08 +08:00
董宏凯andClaude Opus 4.8 1a2ef0a537 feat(大屏绘制): 战情大屏样式优化与脱敏全覆盖
- 去掉顶栏与「生成于」时间标签;无数据时仅显示「暂无数据」、隐藏导航与标题
- 战情各页 + 导航 Tab 文本全部脱敏(响应式取后台敏感词,避免 replaceDesensitizeMap 时序竞态)
- 全页最小字号 16px,调整行高/对齐;表格加横向滚动防溢出
- 关键事件改时间线样式(灰色虚线 + 浅红空心圆圈节点),敌情三块改单行全宽
- 应对方案渲染 Markdown;处置建议由 7 列大表改为单条卡片(字段标签改 #222222)
- 通用过滤引用角标([[1]] / 资料[[1]] / (资料[[1]]) 等,解析末尾整树清洗)
- safelist + 本页兜底 CSS 修复 border 语义色回退成灰色(时间线圆圈/FooterNote 强调条)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:36:36 +08:00
xu hd ce3a5b6bf9 Merge branch 'xuhd' into 'master'
feat(frontend): 来源弹窗加loading + 笔记页暗色适配 + 主题透传

See merge request knowledge-base-bj/deerflow-server!144
2026-06-23 03:28:12 +00:00
xuhdandClaude Sonnet 4.6 91a6183757 feat(notebook): 来源上传优化 + 工作室问答停止回答功能
- 上传来源后显示处理中占位卡,轮询成功后自动选中该来源
- 失败时不选中,占位卡去重(已在真实列表中则不显示)
- 输入框三态:空→无按钮,有内容→发送箭头,streaming→停止按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 11:27:40 +08:00
g zy 076cdc746e Merge branch 'gzy' into 'master'
Gzy

See merge request knowledge-base-bj/deerflow-server!143
2026-06-23 02:57:41 +00:00
Rocky Kwok 1c2e524b28 fix: iframe页面子智能体不跳出,增加按钮 2026-06-23 10:56:51 +08:00
xuhdandClaude Sonnet 4.6 42ce20b4df feat(frontend): 来源弹窗加loading + 笔记页暗色适配 + 主题透传
- 来源上传弹窗 A系统基因库 tab 搜索时显示 loading spinner
- NetworkActor 组件初始加载时显示 loading spinner(初始状态设为 true)
- StudioNotebooksPage 笔记卡片支持暗色主题样式
- StudioNotebookDetailPage / NotebookAIWritingPage 跳转时透传 themekey 参数

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:07:13 +08:00
Rocky Kwok 1cbc0e2dd6 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-23 09:21:01 +08:00
董宏凯andClaude Opus 4.8 fc632f26e9 feat(大屏绘制): 战情七页版式 + 对接 selectSqReport 真实数据
- 新增 dashboard-agent 独占战情多页(总览/敌情/我情/战场环境/情况判断/决心建议/处置建议),
  经 DashboardApp 的可选 pages 插槽注入,会商 Step3 大屏多页样式零影响
- 总览由六类数据聚合;六个详情页对接 selectSqReport(防御式解析,容错不抛错,
  接口为空显示「暂无数据」)
- 展示层 desensitizeDeep 脱敏 + 请求加载框;去掉所有「更多」按钮

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:47:43 +08:00
fengxiangdebaozi adabfa2fe3 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-22 21:03:19 +08:00
吉鑫升 ec56fa671b Merge branch 'ji'
# Conflicts:
#	CLAUDE.md
2026-06-22 21:02:35 +08:00
fengxiangdebaoziandClaude Opus 4.8 f144a59c97 fix(roundtable): xdfx-A 深链会商聊天记录按行动id隔离存储
xdfx 深链 URL ?taskId= 原始值即 actionId(登录时解析成真实任务id)。
一个任务可派生多个行动,各行动会商需互相独立,故聊天记录/后台作业
按 actionId 存取,而非解析出的真实 taskId。

新增 recordTaskIdOf(prefill)=actionId??taskId,应用到草稿 CRUD/回显
(useDraftPersistence/useRoundtableDrafts/loadLatestByTask) 与后台作业
(startJob/getJobByDraft/cancelJob/startJobKeepalive)。目的树/入库/任务
详情/底部快捷跳转栏仍用真实 taskId。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:01:34 +08:00
吉鑫升andClaude Opus 4.8 dd942cdd60 feat(xdfx): 行动分析聊天记录按行动id划分 + 首问追加 action_id
xdfx 单智能体问答不再按 task_id 划分聊天记录,改按「行动id(action_id)」划分:
- TaskContext 增加 partitionByAction 标记 + taskThreadMetadata 统一产出线程标签
- xdfx 新建线程额外打 metadata.actionId,对话列表按 actionId(+agent_id) 过滤
- 仍保留 metadata.taskId 以维持后端「任务深链对话全局共享」语义
- 首次提问开场信息追加「行动id(action_id)为…」
- useTaskThreads 增加 actionId 过滤参数;useChatsBase 从上下文派生
rwfx / 3qfx 行为不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:00:22 +08:00
吉鑫升andClaude Opus 4.8 fbec222eb4 Merge branch 'ji' into master
合并 ji 的功能:开放问答接口(/api/open/chat[/stream] 回传生成文件 + 临时不留存)、
技能地址管理、智能体详情「最近对话」等。
冲突 frontend StrategyRoutes.tsx 已解决:同时保留 admin/roundtable-diagnostics(master)
与 admin/skill-addresses(ji) 两个管理员路由。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:39:35 +08:00
吉鑫升andClaude Opus 4.8 7c55a577ad feat(open-chat): 开放问答接口回传生成文件 + 临时不留存
- /api/open/chat[/stream] 新增 include_files(默认开),把智能体在 outputs 目录
  生成的文件(name/virtual_path/encoding(text|base64)/content)随结果回传,
  解决"报告写进文件、answer 只回一段总结"导致拿不到完整内容的问题;
  既无 answer 也无文件时返回 502。
- 无状态运行标记 ephemeral:lead agent 跳过记忆与知识库中间件(不召回/不留存/
  不自动沉淀),取完文件后清理临时线程目录,做到"通过本接口的聊天记录不存储"。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:36:30 +08:00
董宏凯 57ffe1a706 Merge branch 'dhk-1' into 'master'
feat(layout): 简洁模式右上角加换肤 + 界面模式下拉切换

See merge request knowledge-base-bj/deerflow-server!142
2026-06-22 11:17:40 +00:00
吉鑫升andClaude Opus 4.8 06b169d7a1 feat(skill-addresses): 新增技能地址修改记录,支持单次记录回退
后端:
- 每次「应用」写一份 {id}.manifest.json(记录本次 from→to 替换明细/文件数/处数),
  放在备份目录的同级,避免被回退误恢复或计入文件数
- GET /backups 返回每条记录的 replaced_total + replacements;POST /rollback/{id} 回退单条

前端:
- 把「撤销上次」升级为「修改记录」面板:每条显示时间/改了哪些地址/文件数,
  带「回退此次」(二次确认);应用后自动展开
- BackupEntry 类型扩展 replacedTotal + replacements

同步更新前后端 CLAUDE.md。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:05:05 +08:00
吉鑫升andClaude Opus 4.8 7487395e44 feat(skill-addresses): 地址检索升级为多关键词匹配
- 空格分隔多个关键词,全部命中(地址或技能名任一包含)才显示,便于在大量地址中收窄
- 搜索框加清除按钮 + 更清晰的占位提示
- 过滤时列表头显示「命中 / 总数」

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:55:44 +08:00
吉鑫升andClaude Opus 4.8 26557b30a0 feat(skill-addresses): 将「技能地址管理」并入管理员「技能管理」页的标签页
- SkillAddressesPage 主体抽成可复用的 SkillAddressesPanel(默认导出保留旧路由作直链回退)
- AdminSkillsPage 顶部加 Tabs:技能列表 + 技能地址管理(嵌入 panel)
- 移除 page-sidebar 左下角设置下拉里两处独立的「技能地址管理」入口
- 同步更新前后端 CLAUDE.md 文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:35:42 +08:00
吉鑫升andClaude Opus 4.8 f8228847ab fix(skill-addresses): 补提交被 gitignore 漏掉的 address_scan 引擎模块
skills/ 忽略规则误伤了 packages/harness/deerflow/skills/address_scan.py,
导致部署环境缺模块、启动报 ModuleNotFoundError: deerflow.skills.address_scan。
强制纳入跟踪。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:29:10 +08:00
吉鑫升andClaude Opus 4.8 6b480fdbe9 feat: 新增技能地址管理与开放问答接口,完善任务工作区
- 新增「技能地址管理」页面与 /api/skill-addresses 路由(扫描/预览/批量替换技能内地址)
- 完善 /api/open/chat 开放问答接口与模型工厂、网关配置
- 调整任务工作区、侧边栏与路由
- 将本地 0622/ 临时目录与 __pycache__/ 加入忽略,不提交

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:17:54 +08:00
fengxiangdebaoziandClaude Opus 4.8 0a456e9146 feat(ai-writing): 后台挂起改为即时返回,乐观标记历史「挂起中」
点击「转后台」后立刻 reset 回初始页,挂起请求 fire-and-forget;
历史下拉靠 markBackground/clearBackground 乐观标记马上显示「挂起中」,
不等后端确认,与「多智能体会商」交互一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:07:53 +08:00
fengxiangdebaoziandClaude Opus 4.8 b59cbd51d9 feat(ai-writing): 各阶段模型调用自动容错,失败时换下一个模型继续
参考多智能体会商的容错思路:意图解析/大纲/正文/审核四个 LLM
环节失败时,按 config.yaml models 顺序换下一个模型重跑该环节,
并通过 model_switch 事件在前端时间线提示「『X』模型<原因>,
已自动切换到『Y』继续」。正文逐章节、审核逐维度独立切换,
已生成的章节/维度保留;意图解析成功的模型还会写回 state
供下游阶段沿用,避免每阶段重复经历同一次失败。

- nodes/_model_fallback.py: 候选模型链 + 异常分类 + model_switch
  事件构造(沿用 ROUNDTABLE_MODEL_FALLBACK[_MAX] env 开关)
- intent_parser/writer_outline/writer_draft/editor: 失败循环换模型
- 前端 types/reducer/timeline: 新增 model_switch 事件类型,按
  stage 复位对应流式缓冲并以 amber 系统提示渲染

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:07:53 +08:00
fengxiangdebaoziandClaude Opus 4.8 23743ee193 chore: 精简 AI 写作示例文案
ai-writing 表单/检索/欢迎页的占位与示例改为通用文案。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:07:53 +08:00
董宏凯andClaude Opus 4.8 7748661963 feat(roundtable): 历史卡片显示所用业务链(自由模式) + 诊断只收报错日志
- 历史「分析记录」卡片/侧边栏列表:把「第X步」改为本次会商所用业务链名,
  无链条显示「自由模式」。后端 list 轻量 meta 从 step2 正则抽取 chain_title
  (不整体解析大块 JSON),个人草稿 + task 草稿两个 store 都覆盖。
- 会商诊断改为只收集报错日志:record_diagnostic 写库前丢弃 level==info
  运行日志(begin/consensus/gather/job_started),只留 warning/error。
  单点收口覆盖前台/后台/recorder 全部写路径;诊断页标题改「会商报错日志」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:36:31 +08:00
fengxiangdebaoziandClaude Opus 4.8 e28277b21b feat(layout): 全量模式右上角也加界面模式切换
复用 appearance-controls 的 LayoutModeSwitcher,Header 操作区在换肤旁
补上界面模式下拉,与简洁模式右上角保持一致。LayoutModeSwitcher 新增
fallback 属性指明 mode 未设置时的兜底界面(Header 传 full,ChatPage 传 compact)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:43:55 +08:00
吉鑫升andClaude Opus 4.8 32476c7ad4 feat: 智能体详情新增「最近对话」列表,完善推荐问题与任务深链配置
- 新增 AgentRecentChats 组件,在智能体详情面板底部按 agent_id 展示最近对话
- 调整问答/智能体侧边栏与最近对话列表展示逻辑
- 补充 dashboard-input、task-deeplink 相关配置与线程工具方法
- 后端推荐问题接口标签处理及 position_sync 微调,新增相关测试

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:47:43 +08:00
Rocky Kwok ab305b3514 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-22 14:44:24 +08:00
吉鑫升andClaude Opus 4.8 16ba5deb0e feat(agents): 智能体归属/发布操作前增加「账号已切换」提醒
- 新增 useLiveAccount 钩子:跟踪「实时」登录账号(随 storage/focus 刷新),
  并与组件挂载时捕获的账号对比;token 存 localStorage 会被同浏览器其他标签页
  共享,另一标签页登录会静默替换本页 API 所用账号
- agent-card / agent-detail-panel / publish-square-picker-dialog / NewAgentPage
  接入该钩子,切换账号后对归属/发布类操作做提醒,避免误归属

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:30:15 +08:00
吉鑫升andClaude Opus 4.8 2b6424b3ff feat(open-chat): 新增 /api/open/chat 开放问答接口
- 无需登录(/api/open/ 加入 auth 白名单 + CSRF 豁免),供外部系统直接调用本项目智能体做问答
- 可指定模型(model_name)与智能体(agent_name 中文名称,扫描 agents/*/config.yaml 解析为运行 id);GET /api/open/agents 列出可选名称
- 只返回最终结果(answer = 最后一段 markdown),不含中间过程/工具调用
- 可靠关闭思考:thinking_enabled=false 时下发 thinking_force_disabled,经 create_chat_model(force_disable_thinking=True) 强制注入 enable_thinking=false(及 GLM thinking.type=disabled),覆盖只声明 supports_thinking 的内网 vLLM/Qwen 模型(与生成标题关思考一致)
- 一次性问答设 is_scheduled_run 跳过自动起标题

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:21:56 +08:00
fengxiangdebaoziandClaude Opus 4.8 79f1a004a0 feat(layout): 简洁模式右上角加换肤 + 界面模式下拉切换
抽出共享 appearance-controls(ThemeSwitcher 按 tone 适配 banner/content
配色,新增图标+下拉的 LayoutModeSwitcher);Header 复用换肤组件,简洁模式
ChatPage 顶栏右上角补上换肤与界面模式切换,与全量模式保持一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:02:51 +08:00
Rocky Kwok b89933e60a chore: ignore 2026-06-22 10:55:00 +08:00
fengxiangdebaozi 548cfd1719 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-22 10:47:15 +08:00
fengxiangdebaozi 7d32b41e90 加载框样式 2026-06-22 10:46:57 +08:00
董宏凯 6e3896fed3 Merge branch 'dhk-1' 2026-06-21 20:43:32 +08:00
董宏凯andClaude Opus 4.8 545ec178b8 fix(roundtable): 第三步重新分析前复位入库状态,避免重分析后仍问是否入库
重跑总结/重新或修改流程图前整体复位 ingest 状态为 idle(隐藏旧产物的入库
询问卡/协助卡/手动入库按钮);流程图类重分析出新图后 runFlowExtract 在 idle
时重新询问,重新生成报告则在报告重写完成后(若已有流程图)重新询问是否入库。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:43:06 +08:00
董宏凯andClaude Opus 4.8 6dd1f9c017 fix(roundtable): 模型容错提示顺序前置 + 回看历史时残留「加载中」气泡强制收尾
- 第二步模型自动容错:系统提示气泡改为插入到失败气泡正前方,使「已切换其他
  模型」先于新模型重答过程显示(旧实现追加到列表末尾,反而排在答案之后)。
- 加载历史草稿(hydrateFromDraft)时把任何残留 streaming 的气泡强制收尾为静止
  态(去加载光标、角标改「已结束」、空正文补「(已结束)」),避免用户点聊天
  记录回看时误以为研讨仍在进行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:42:54 +08:00
董宏凯andClaude Opus 4.8 6b963fd918 fix(roundtable): 修复管理员连续编辑第二条他人业务链保存 404「已删除」
编辑器 chainId 闸门由「整组件生命周期只加载一次」改为「按已应用的 chainId 追踪、变化即重载」。
此编辑器实例在多次导航间可能不卸载(从业务链弹窗进来时尤甚),旧闸门导致第二次打开另一条链条
时根本不加载,currentChainId 停在上一条 → 保存打到错误 id → 后端 404,必须重新登录才恢复。
同一 chainId 不重复加载、也不覆盖编辑器内的手动切换;getChain 取不到时给出明确提示。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:21:49 +08:00
董宏凯andClaude Opus 4.8 f13a1bb7fe feat(roundtable): 业务链条配置增加管理员权限,管理员可配所有链条
- 后端 store/路由:scope=all(仅管理员)列全量;get/update/delete 加 is_admin 旁路,管理员可操作任意 owner 链条且不转移归属;非管理员请求 all 退化为 mine
- 前端列表页/会商弹窗:管理员多「全部链条」筛选 tab,canManage=isAdmin||isMine 放开发布/设置/删除
- 编辑器:管理员可见发布按钮;按 chainId 找不到本地列表时直接 getChain 拉取(修复管理员从「全部链条」进编辑器空白),并修掉自身无链条时加载卡死的边界

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:17:19 +08:00
董宏凯 02115bccfe Merge branch 'dhk-1' into 'master'
feat(roundtable): 会商席位「技能识别强化」改为按需开关(智能体级 + 链条每席位级)

See merge request knowledge-base-bj/deerflow-server!141
2026-06-21 08:59:10 +00:00
董宏凯andClaude Opus 4.8 48744441ba fix(roundtable): 修复会商消息列表滚动条被分栏手柄抢占、且过细难抓
- Step2/Step3 分栏拖拽手柄 ::after 命中区(16px,居中跨分隔线)向左探出 8px
  压住左侧消息列表纵向滚动条,抓滑块误触发左右拖动;改为命中区整体移到
  分隔线右侧(沙箱一侧),左侧滚动条让出
- 会商页滚动条 6px→12px(透明边框+background-clip 收成带内边距圆条),更易抓
- 覆盖 Step1/Step2/Step3(summary/report)所有消息列表,含有沙箱与流程图分栏场景

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 00:42:40 +08:00
董宏凯andClaude Opus 4.8 5ae6261e82 fix(roundtable): 结构化抽取按实际产出抽全(不固定条数/不编造),提升准确度
不动 rwfx 链路结构(任务→目的→行为体→预期行为→驱动因素→叙事;仅预期行为↔驱动因素
1:1,其余皆 1:N)。只修准确度:

- 叙事不再凭空多出来一堆:驱动因素→叙事 仍是 1:N,但叙事条数严格按报告/叙事席位
  实际写出的来(写几条抽几条),删掉「每个驱动因素抽出它名下的若干条叙事」这种诱导
  逐驱动因素编造的措辞——根治「叙事席位只产 3 条、流程图却冒出十几条」
- 去掉所有写死的条数假设(目的「通常3个」、预期行为「3-5条」、叙事「2-4条」、
  候选「10→3」)——各席位交付条数本就不固定,一律以实际产出为准,不漏不多不硬凑

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 00:34:00 +08:00
董宏凯andClaude Opus 4.8 a76e28e018 fix(roundtable): 结构化抽取严格以报告为准,根治从席位候选池抽出一堆目的
报告只锁定 3 个目的(从 10 个候选里取舍),但流程图抽出一堆目的——根因是之前
「候选项也要抽全」+ 把各席位原始交付当补充来源,诱导模型去席位候选池里把被淘汰的
候选也抽进来。

按用户原则修正优先级(优先方案总结报告及其文件内容,只有报告不足/缺失才看席位产物):
- extraction_spec _SOURCE_NOTE:改为「严格以报告为准;席位仅兜底(报告整层缺失/只说N条
  没逐条列才用),绝不用席位扩充报告已取舍的层、淘汰候选一律不抽」
- _COMPLETENESS_NOTE:由「候选项也要抽全(含落选)」改为「按报告逐条抽全、不漏也不多」——
  报告列几条抽几条(不漏),但报告已锁定 N 条的层只抽这 N 条、不去候选池凑数(不多)
- rwfx completeness_note 目的/叙事:抽报告最终采用的(3 个目的/几条叙事),不抽候选池
- flow_extract._material:席位交付明确标为「兜底参考,仅报告不足时用」,禁止扩充已取舍层
- _extract_messages / _validate_messages:同步改为报告为准、补漏但不新增被淘汰候选

经核对:本次方案总结报告本身准确(3 目的=席位最终 Top3、3 行为体、12 预期行为↔12 驱动
因素已做 1:1 配对、3 叙事),问题只在抽取从席位候选池过度抽取。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 00:08:23 +08:00
董宏凯andClaude Opus 4.8 f63a1faf3b fix(roundtable): rwfx 结构化抽取纯树·单父兜底,根治叙事多对一乱线
rwfx 全链是一棵树:除预期行为↔驱动因素 1:1 外其余皆 1:N,任何节点只有一个父节点、
绝无多对一/多对多/汇聚。但弱模型常把「叙事」连到一片驱动因素 → 汇聚乱线。

- extraction_spec:ChainSpec 加 single_parent 标志(rwfx=True,xdfx 有汇聚=False);
  helper is_single_parent_chain / chain_level_rank;强化 rwfx ②「叙事只有一个父驱动因素」
  + 新增④「全链是树,每个非根节点恰一个 from」
- flow_extract:_collapse_to_single_parent 确定性兜底——纯树业务把每个节点的多父入边
  收敛到「紧邻上一层」的单一父节点(只删多余入边、不新增、不动节点),在 final 前执行;
  非纯树业务(xdfx)跳过
- dag-types/types:DAG 派活忠实本阶段目标——不擅自加数量上限/精简、不替席位加体例风格、
  逐条全部输出、不诱导脱离输入编造(前后台同步)
- Tests: tests/test_flow_extract_single_parent.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:37:49 +08:00
董宏凯andClaude Opus 4.8 3d75040297 fix(roundtable): 会商消息列表只竖向滚,修复滑块被横向拖动抢占难点击
消息列表用 `overflow-y-auto` 单写时,按 CSS 规范另一轴(overflow-x)会被计算成
auto——任何子元素溢出一两像素就让整列横向可滚,鼠标移到右侧竖滚条滑块时极易误
触发横向拖动、抓不住滑块。给消息流容器补 `overflow-x-hidden` 只保留竖向滚动;
宽内容(代码块/表格)在各自气泡内已有 max-w-full + overflow-x-auto 自带横滚,裁掉
列表横向不影响查看。

覆盖 Step1/Step2(会商)/Step3 总结·报告·大屏 五处聊天消息流容器。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:58:44 +08:00
董宏凯andClaude Opus 4.8 8447790c47 fix(roundtable): 预期行为↔驱动因素严格 1:1,根治结构化抽取多对一/一节点多出边
报告把某行为体的多个驱动因素合并写进一个表格单元格(其余预期行为行留空),
导致结构化抽取把「多条预期行为汇聚到一个驱动因素」(多对一)、或一个节点冒出
多条连线。根因:1:1 两层节点数不相等时,模型与确定性补边(_derive_layered_edges)
都只能退化成多对一/比例平铺。

修复(让两层严格等量、一一配对):
- extraction_spec rwfx relations_note①: 强化为「严格 1:1」——为每条预期行为归纳
  那一个核心驱动因素、两层等量、每个驱动因素 from 只连一条预期行为、严禁多对一/
  一节点多出边;源数据把多个驱动因素合并在一处时必须按预期行为拆开/重新归纳成等量
- extraction_spec completeness_note: 驱动因素层节点数=预期行为层节点数(1:1 特例)
- build_summary_directive: 让报告本身把 1:1 相邻层写成逐条配对表(一行一对、数量相等),
  不要把多个下层条目用「+」合并塞进一个单元格或只写第一行——从源头消除多对一
- flow_extract 校验新增「一对一层级必须严格 1:1、绝不多对一」必查必补项

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:43:48 +08:00
董宏凯andClaude Opus 4.8 b5f11ad790 fix(roundtable): 结构化抽取逐条抽全(候选项不漏)+报告不足回看席位交付
修复深链接(rwfx等)结构化流程图抽取「同层多条只抽一条」的准确性问题:
报告里 3 个候选目的只抽出 1 个、每个行为体名下多条预期行为被合并成 1 条、
3 组候选叙事只画 2 个且连线错。

- extraction_spec: 新增业务无关的 _COMPLETENESS_NOTE「逐条抽全、候选项也要抽」
  注入通用规范+写实规范; rwfx completeness_note 显式点名候选目的/每行为体多条
  预期行为/候选叙事都要全抽; _SOURCE_NOTE 改为「报告为主+席位交付补充」
- flow_extract._material: 报告与各席位原始交付一起喂给模型(报告把某层条目压缩
  时回到交付逐条抽全,冲突以报告为准),不再 report present 就只用报告
- flow_extract 抽取/校验 prompt: 补「逐条抽全+候选项全抽+不许合并+不要只照搬
  方案总纲精简主干」,校验新增「候选/多条目是否抽全」必查必补项

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:16:27 +08:00
董宏凯andClaude Opus 4.8 d383b0e9dd feat(roundtable): 第二/三步模型自动容错(出问题自动换下一个模型)
模型出问题时按 config.yaml models[] 顺序自动切到下一个模型继续,并告知用户
「『X』模型异常 → 已切换『Y』继续」。前台实时 + 后台挂起两条路径都覆盖。

- 中间件兜底/熔断 AIMessage 打 LLM_ERROR_MARKER 标记,is_llm_error_message 可识别
- roundtable_model_fallback: fallback_model_chain 候选链 + reason_text + 总开关
  env ROUNDTABLE_MODEL_FALLBACK
- 触发: 异常(start_run失败/idle卡死) / 兜底错误文案 / 交付为空(仅seat/leader)
- 后台 InProcessRoundtableGateway._run_turn_with_fallback 包住每轮 run
- 前台 _special_run/_leader_run 收尾检测失败→model_switch 帧+换模型重流
- 结构化抽取 flow_extract round1 astream 同款换模型循环
- Step2 单席位异常隔离: 单席位失败不拖垮整批/整链/整个作业
- 时间线插「系统提示」气泡 + toast + 诊断 model_switch 事件
- Tests: test_roundtable_model_fallback.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:37:45 +08:00
董宏凯andClaude Opus 4.8 ae2706dad3 feat(roundtable): xdfx 行动名喂入 summary/抽取 + 第三步意图理解增强
- xdfx:把本次「行动名称」喂进 summary 材料(buildMaterials 加 actionName) 与结构化抽取
  材料(flow_extract._material 加【本次行动】抬头),让报告/流程图都贴着该行动展开
- 第三步意图识别(classify_flow_intent)增强:新增 can_ingest 上下文 + ingest 意图;重写
  _INTENT_SYS 走语义理解——「介绍/解释/为什么/总结一下」等对内容的提问→qa(文字答);
  「帮我入库」(可入库时)→ingest→直接入库;能判断就给具体意图不轻易 unclear;
  不可入库时误判 ingest 自动降级 qa。前端 classifyFlowIntent 传 canIngest,ask 新增 ingest 分支

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:39:24 +08:00
董宏凯 fba417918f feat(roundtable): xdfx 左上角只显行动名/cop-task-detail可mock/抽取根节点用行动名
- xdfx 深链左上角「当前行动」只显示行动名称,去掉行动叙事
- cop-task-detail 加本地假数据开关 VITE_MOCK_COP_TASK_DETAIL(仿 VITE_MOCK_SQ_REPORT):
  true 走 mockCopTaskDetail(dev=true,sit/network=false);真实接口未就绪时调试用
- 结构化抽取(flow-json):FlowExtractRequest 加 root_label,xdfx 强制根节点 n1 的 label
  用本次「行动名称」(前端 useStep3Summary 传 readActionDetail().actionName);extract+validate 两段都注入

注:行动详情 mock(actionName/narration) 在 config.yaml(按惯例不提交)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:18:02 +08:00
董宏凯andClaude Opus 4.8 b513a0d56c feat(roundtable): 深链接第一步任务/行动展示 + xdfx 行动详情接入会商
- Step1 深链接(taskId)下:左栏顶部「业务链选择」隐藏,改显当前任务(rwfx/3qfx:任务名称+描述)
  或当前行动(xdfx:行动名称+叙事);顶栏「业务链条」入口隐藏;「新建任务」改「重新分析」
  (复位 autoStarted + resetToStep1,对同一任务重做意图分析)
- xdfx 行动详情接入会商:fetchActionDetail 取到即 persistActionDetail 缓存;新增
  readActionDetail/persistActionDetail + ACTION_DETAIL_STORAGE_KEY;resolveTaskDeeplinkTarget
  会商分支为 xdfx 把 actionId/actionName 带入 roundtablePrefill
- xdfx 首条交给任务理解的消息 = 任务名称 + 任务描述 + 行动名称
- 「原任务已有结果」(目的树预览 headerLeftSlot) 仅 rwfx-A(6BF) 显示

注:行动详情 mock(actionName/narration) 在 config.yaml(按惯例不提交,单独管理)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:05:27 +08:00
董宏凯andClaude Opus 4.8 b1b1ff98f5 feat(roundtable): 深链接业务链抽取规范 + 总结报告文件干净/对话强制核对
深链接会商(taskId+goPath,businessCode)下按所选业务链完整层级强制校正
结构化抽取(flow-json) 与 方案总结报告:

- 新增 harness 层 extraction_spec(app+harness 共用,单份领域知识两角色用法):
  build_structure_directive / build_summary_directive 按 businessCode 取
  · rwfx(6BF):线性 任务→目的→行为体→预期行为→驱动因素→叙事;预期行为↔驱动因素 1:1、余 1:N
  · xdfx(7BF):分叉汇聚 DAG,根=行动;行动→受众目标/关键传播节点(1:N)→脆弱点(1:N)→策略(多对一)
  · 其余业务通用规范(强约束完整性+命名+取数、不套固定模板);business_code 空→零副作用
- 结构化抽取:flow_extract._hierarchy_hint + _validate_messages 用 structure_directive
  权威覆盖默认层级(根治"无论什么业务都按目的→行为体抽/停在行为体");前端 streamFlowExtract
  传 business_code(源 ingestBusinessCode)
- 方案总结报告:前台 multi_agent._special_run(summary) + 后台 build_summary_prompt 注入
  business 完整性核对(StartJobRequest/StartParams 透传)
- 报告产物策略:.md 报告文件保持完整、干净、无需修改(不写 待补充/不足/⚠️);
  「核对说明」(意图对齐+业务链完整性+不足/可补充)改为只在对话回复正文、且每次必出
- 种子 roundtable-structure SOUL 层级顺序改 spec-driven、去 rwfx 写死示例

Tests: test_roundtable_extraction_spec / test_roundtable_summary_prompt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:13:03 +08:00
董宏凯andClaude Opus 4.8 3ab674e438 feat(roundtable): Step2 提速四件套 + 方案总结报告意图对齐
Step2 提速:
- 每席位推理深度覆盖(seat_mode): 业务链条逐席位可配 flash/thinking/pro/ultra,
  覆盖链条级 seatMode;前台三处 dispatch + 后台 job 透传, run_seat 用 _SEAT_MODE_PARAMS 解析
- recommend 后端批内并行: engine._run_recommend 由串行 for 改 asyncio.gather(对齐前端)
- 研讨前并行取数两段式(gather_first, 业务链条 opt-in): 研讨前全席位并行只取数,
  产出作为共享资料注入研讨轮(_seat_message gathered + 前台 runGatherPhase);
  新增 gather_first 列(migration 20260620_02 + idempotent SQL)
- 削减重复广播: suppressPreBroadcast 扩到 recommend(round>=2)/chain(i>0)/chain收口,
  保留首轮背景打底,省 N 次读写大 thread

方案总结报告效果:
- summary 加"意图对齐+缺口核对+防幻觉护栏": SOUL + 后端 build_summary_prompt +
  前端 buildSummaryPrompt 三处对齐; 推断标注(综合推断)、缺真实信息标注待补充交给用户、绝不凭空编

Tests: test_roundtable_seat_modes / test_roundtable_gather_phase / test_roundtable_summary_prompt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:28:02 +08:00
董宏凯andClaude Opus 4.8 9a1b674950 feat(roundtable): 会商席位「技能识别强化」改为按需开关(智能体级 + 链条每席位级)
弱模型补偿指令从全域生效改为默认关闭、两个 opt-in 开关取 OR:
- 智能体级: AgentConfig.seat_skill_directive(config.yaml),在智能体编辑弹窗
  (agent-card / agent-detail-panel)开关;agents 路由 create/update/读写全程穿。
- 业务链条每席位级: 存进链条 seats JSON(ChainSeatSchema.seat_skill_directive,
  无需 DB 列/迁移),在链条编辑器每个席位卡片右侧 ✨ 图标→下拉框(开关+说明)配置;
  按 agentId 查表穿到前台 useStep2Orchestration 各席位调用 + 后台作业
  seat_skill_directives:{agentId:bool} → InProcessRoundtableGateway.run_seat。
- ROUNDTABLE_SEAT_SKILL_DIRECTIVE 降级为总熔断开关(默认放行,不再 force-on)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:37:29 +08:00
董宏凯 9278fd772f Merge branch 'dhk-1' into 'master'
fix(roundtable): 会商 Step2/Step3 沙箱产物按对话创建者解析,修复历史/跨用户查看不了

See merge request knowledge-base-bj/deerflow-server!140
2026-06-19 15:47:18 +00:00
董宏凯 a8c06ca7dc 代码提交 2026-06-19 23:46:53 +08:00
董宏凯andClaude Opus 4.8 762d3620c4 feat(roundtable): 会商前端调用报错收集 + 诊断页用户名/范围切换
- 前端调用报错收集(用户实际遇到的报错,服务端诊断看不到的也收):新增
  POST /api/roundtable-diagnostics/report(非 admin,scope="client")+ 前端上报器
  roundtable-planning/api/diagnostics-report.ts。streamMultiAgent(第二步席位 +
  第三步报告/结构化/绘制/大屏)覆盖 HTTP 错误(502)、SSE error 帧、worker error 帧
  (模型没调用成功)、fetch 网络不可达、流式中途中断,reported 去重;initMultiAgent /
  intent / recommend / 后台作业接口同步上报。
- 诊断页:每条报错显示上报用户名(👤,user_id→邮箱@前缀,后端 best-effort 解析);
  第一个筛选改为「后台挂起 / 前台调用」切换(前台调用=foreground,client,后端 scope
  支持逗号多值),阶段下拉随范围联动(后台→二三步;前台→一二三步)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:45:34 +08:00
董宏凯andClaude Opus 4.8 4a403c95c5 feat(roundtable): 会商诊断日志 + 后台挂起卡死兜底(idle 超时)
- 新增会商全流程诊断日志:新表 roundtable_diagnostics + 仓储 + admin API
  (/api/roundtable-diagnostics) + 管理员诊断页(会商页右上角设置按钮入口),
  覆盖第一/二/三步及后台执行器/进程内网关各处报错与关键事件,支持按
  范围/阶段/级别/事件/关键词筛选 + 分页。诊断写入全程 best-effort,绝不打断会商。
- 修复后台挂起「执行完第一个席位后永久卡死」:进程内网关每轮 agent run 改为
  idle(无活动)超时(默认 20min)——只要还在持续产出 token/工具事件就一直等,
  仅当连续无任何输出才判卡死并取消+报错+落 seat_turn_timeout 诊断;慢但活着的
  生成绝不打断。可经 ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS 调整。
- 一并提交进行中的会商 seat-skill 注入 / 业务链条编辑器相关改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:55:56 +08:00
fengxiangdebaoziandClaude Opus 4.8 8ca16c74bc fix(roundtable): 会商第三步各席位产物弹窗经 portal 挂 body,修复顶部被顶栏盖住
页面 <main> 的 `relative z-0` 新建层叠上下文,把内部 fixed 弹窗困在 z-0 层级,
被会商页头(z-40)/全局顶栏(z-45)盖住标题栏。ContentModal 与 ArtifactPreviewModal
改用 createPortal 挂到 document.body,脱离该上下文后正常盖在顶栏之上。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:12:45 +08:00
fengxiangdebaoziandClaude Opus 4.8 60626c384a fix(roundtable): 会商 Step2/Step3 沙箱产物按对话创建者解析,修复历史/跨用户查看不了
圆桌会商系统线程(metadata.thread_type==roundtable)的 Step2 席位/总控 +
Step3 报告/总结/大屏/结构化 沙箱产物(md/流程图/报告)落在创建者目录下,
之前按当前登录解析路径 + owner_check 校验 → 历史草稿重开或 taskId 会商被
其他账号打开时 403/404 查看不了。沿用任务深链对话方案(23e6685)扩展到圆桌:

- thread_paths._is_creator_shared_thread:taskId 或 thread_type==roundtable
  都解析到对话创建者(threads_meta.user_id),覆盖所有线程路径关口
- ThreadMetaRepository.check_access:圆桌系统线程同样跳过 owner 过滤
  (artifacts 路由 owner_check 据此放行其他账号/历史查看)

线程 id 仅存在于草稿中,无关账号无法发现,与 taskId 共享模型一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:51:05 +08:00
董宏凯 551c748aa8 Merge branch 'dhk-1' into 'master'
feat(roundtable): 第三步「数据入库」由结构化输出智能体自行调用技能

See merge request knowledge-base-bj/deerflow-server!139
2026-06-19 09:59:51 +00:00
fengxiangdebaoziandClaude Opus 4.8 03c155f8a7 feat(roundtable): 第三步「数据入库」由结构化输出智能体自行调用技能
- 入库改为让 roundtable-structure 走完整 agent 运行时(ingest run policy)
  自行挑选/读取/调用入库技能并传入 taskId,不再前端代调演示
- 新增 /api/flow-extract/ingest-eligibility(智能体判断当前业务场景是否有
  合适入库技能,决定入库按钮是否显示)+ /api/multi-agent/structure/init
- 入库前判断是否已生成流程图:无则发起「是否先生成流程图」用户协助
- 「知识库入库」更名为「数据入库」

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:58:36 +08:00
董宏凯 3b165d9ea5 Merge branch 'dhk-1' into 'master'
feat(agent-sidebar): 独立问答智能体列表栏增加「最近的对话」区(参考首页)

See merge request knowledge-base-bj/deerflow-server!138
2026-06-19 09:53:27 +00:00
董宏凯andClaude Opus 4.8 a85d2480b4 feat(auth): 独立登录会话开关(设置-外观)—— 同浏览器多账号互不覆盖
复用 iframe 嵌入的按标签页隔离机制:新增 ?isolate=1 标志,authStorage()
从「仅嵌入」放宽为 isEmbedActive() || isIsolatedSession(),独立态走
sessionStorage 故本标签页登录态不被其它标签页覆盖。设置→外观→「登录会话」
加 Switch,开启时先 migrateAuthToSession() 把 localStorage 受保护键快照搬进
sessionStorage 再翻标志,无感不登出;AuthSyncWatcher 跨标签监听对隔离态实时
忽略,运行时开关即刻生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:51:08 +08:00
fengxiangdebaoziandClaude Opus 4.8 a9f01bec79 feat(task-buttons): 外链按钮新增「打开方式」(新窗口/当前窗口)
按钮管理(/page/strategy/admin/task-buttons)的 url 外链按钮可配置打开方式:
- blank=新窗口(默认,window.open(_blank),行为同改前)
- self=当前窗口(window.location.assign,嵌入态即当前 iframe)

新增 TaskButton.openMode 字段贯通前后端:前端 task-buttons.ts 类型+种子+
runTaskButton 打开逻辑、api 客户端 snake⇄camel 映射、TaskButtonsPage 下拉;
后端 router schema + ORM open_mode 列(可空默认 blank,启动自动 ALTER 补列,
无需手动迁移)+ sql/memory store 归一化。仅 url 类型生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:09:01 +08:00
吉鑫升andClaude Opus 4.8 e906a911b2 feat(agent-sidebar): 「智能体」区支持收起/展开
标题栏改为可折叠头(chevron 切换),收起后仅留标题栏并隐藏来源下拉/搜索/列表,
下方「最近的对话」占满剩余空间;收起状态持久化到 localStorage。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:09:01 +08:00
吉鑫升andClaude Opus 4.8 8cc2650e98 feat(agent-sidebar): 独立问答智能体列表栏增加「最近的对话」区(参考首页)
左栏改为上下双区:上半智能体列表(来源下拉+名称搜索),下半新增「最近的对话」——
跨智能体近期会话、计数徽标、历史搜索、点击打开/当前高亮/置顶标记,含重命名/置顶/
收藏/分享/导出/删除菜单。会话链接走 usePathOfThread,自动带路由命名空间与 agent_id。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:09:01 +08:00
Administrator 593daa91ec Merge branch 'ji' into master 2026-06-19 15:43:23 +08:00
董宏凯 a6dd0fdd85 Merge branch 'dhk-1' into 'master'
fix(dashboard): 抽取 report-json 前剥离 <think> 思考过程做容错

See merge request knowledge-base-bj/deerflow-server!137
2026-06-19 07:42:29 +00:00
董宏凯andClaude Opus 4.8 30cf0fed91 fix(dashboard): 抽取 report-json 前剥离 <think> 思考过程做容错
推理模型(DeepSeek-R1/QwQ 等)输出的 <think>...</think> 思考里常含
```json/裸 {…},会被误当候选导致抽到错误/残缺数据。抽取前先剥离 think:
- 前端 report-template.ts::extractReportData 开头 stripReasoning(复用 reasoning.ts,
  兼容完整/多段/流式未闭合/只吐闭合四形态)
- 后端 report_json.py 新增 _strip_reasoning 在 extract_report_json 开头调用
- report-template.test.ts +3 例(均通过)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:36:34 +08:00
吉鑫升andClaude Opus 4.8 1f2a1aaa76 feat(agent-sidebar): 「智能体」区支持收起/展开
标题栏改为可折叠头(chevron 切换),收起后仅留标题栏并隐藏来源下拉/搜索/列表,
下方「最近的对话」占满剩余空间;收起状态持久化到 localStorage。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:04:30 +08:00
吉鑫升andClaude Opus 4.8 c1c23d9136 feat(agent-sidebar): 独立问答智能体列表栏增加「最近的对话」区(参考首页)
左栏改为上下双区:上半智能体列表(来源下拉+名称搜索),下半新增「最近的对话」——
跨智能体近期会话、计数徽标、历史搜索、点击打开/当前高亮/置顶标记,含重命名/置顶/
收藏/分享/导出/删除菜单。会话链接走 usePathOfThread,自动带路由命名空间与 agent_id。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:42:40 +08:00
Administrator 4b2a93a542 Merge branch 'ji' into master
合并 md/产物按对话创建者解析路径修复(切换账号找不到文件)与 master 现有前端改动。
2026-06-19 14:39:54 +08:00
AdministratorandClaude Opus 4.8 23e6685977 fix(task-deeplink): 共享任务对话的 md/产物按对话创建者解析路径,修复切换账号后找不到文件
xdfx/rwfx/3qfx 任务深链对话按 metadata.taskId 全局共享、不区分用户,但生成的
md/artifacts 与上传文件物理存在 users/{user_id}/threads/{thread_id}/ 下,user_id
之前取当前登录账号。导致 A 账号生成的文件,B 账号打开同一共享对话时后端去
users/B/ 找 → 404「找不到」。

新增 deerflow/runtime/thread_paths.py(resolve_path_user_id /
aresolve_path_user_id):带 taskId 的共享对话统一解析到对话创建者
(threads_meta.user_id),个人对话仍走当前登录(行为不变)。在所有线程文件路径关口替换:
写入源 ThreadDataMiddleware、present_files、uploads(manager+middleware+router)、
产物读写(path_utils → artifacts/skills router)、嵌入式 client.get_artifact。
正向结果进程内缓存;无 DB/内存态/无行均安全回退到当前登录。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:35:22 +08:00
董宏凯 bc9936bb2b Merge branch 'dhk-1' into 'master'
feat(dashboard-agent): 深链大屏接入研判情报报告接口 selectSqReport + 样例假数据

See merge request knowledge-base-bj/deerflow-server!136
2026-06-19 06:30:58 +00:00
fengxiangdebaozi ef615bbfb6 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-19 14:15:13 +08:00
董宏凯 d8f32690a3 feat(dashboard-agent): 真实模式先请求数据再抽取 + 重新生成/编辑闸门
- dashboard-input: 缓存仅 mock 开发用,真实模式不读缓存、不兜底假数据,
  只用接口真实返回;新增 forceRefresh 跳过缓存强拉最新数据
- DashboardAgentPage: 请求期间「正在请求任务数据…」加载态 + 失败提示/重试;
  重新生成(redraw)/编辑(ask)前先 forceRefresh 拉最新数据,
  guardedGenerate 闸门——拿到数据才生成,拿不到 toast 取消(防并发)
2026-06-19 14:04:42 +08:00
fengxiangdebaoziandClaude Opus 4.8 31f9838095 feat(embed): iframe 嵌入模式(去 chrome + 主题隔离 / hideJump / 登录用户隔离)
外部系统用 iframe 套用深链单智能体问答/多智能体会商时,三个按标签页(sessionStorage)
开关,由深链 URL 参数开启、跨登录跳转/刷新/页内导航存活,绝不跨标签污染正常系统页:

- embed=1(可带 theme=):去全局导航栏 + 侧边栏;按外部主题色强制明/暗,走
  next-themes forcedTheme(不写 localStorage),嵌入侧不调 setTheme/persistSidebarEnabled
- hideJump=1:隐藏深链底部「快捷跳转」栏 TaskJumpBar(底部「下一步」按钮 + 右侧悬浮菜单)
- 登录隔离:嵌入态鉴权/身份键(deerflow.auth/userInfo/login.*/tools_token 等)改走
  sessionStorage(core/auth/scoped-storage.ts 的 authStorage),iframe 登录用户与正常
  登录用户完全隔离、互不覆盖;token 读取统一过 getStoredAuth,改中心函数即全体生效;
  跨标签账号变更监听 AuthSyncWatcher 嵌入态禁用

嵌入会话在 main.tsx 渲染前同步启用(早于任何鉴权写入)。正常标签页 isXxx() 恒 false,
行为零变化。新增 core/embed/embed-session.ts、core/auth/scoped-storage.ts。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:02:36 +08:00
吉鑫升andClaude Opus 4.8 13ef1942f2 feat(agent-sidebar): 独立问答智能体列表栏顶部增加智能体名称模糊搜索
下拉来源切换下面加搜索框:防抖 300ms 走服务端 search(按原始名 + 内置缩写
脱敏映射匹配,与智能体管理页一致),在当前来源(我的/某广场)内搜全量;
带清除按钮、无结果显示「未找到匹配的智能体」、搜索中复用 isFetching 半透明指示。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:16:39 +08:00
董宏凯andClaude Opus 4.8 2b58fe6c8b feat(dashboard-agent): 深链大屏接入研判情报报告接口 selectSqReport + 样例假数据
- dashboard-input: 请求 {consumer}/cop/selectSqReport?taskId=,新增 extractSqReport
  反序列化各条 contentJson、按 categoryType 归类成干净结构喂给智能体
- 真实接口未就绪:新增 sq-report-mock(.json/-data.ts) 样例假数据 + adapter,
  request.ts 按 VITE_MOCK_SQ_REPORT 开关挂载(机制同 cop-task-detail)
- env:dev=true 走假数据,prod/network/sit=false 真实接口

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:19:31 +08:00
fengxiangdebaozi 81ca13280b Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-19 10:03:10 +08:00
fengxiangdebaozi bb7283dcd9 接口 2026-06-19 10:03:00 +08:00
吉鑫升andClaude Opus 4.8 0bd4524334 feat(task-deeplink): 首次进入且任务无历史时自动发送,无需手动点开始回答/覆盖
新增「首次进入且该任务无聊天对话」信号 taskAutoStart:TaskFxIndexResolver 在
「无历史 → /new」时注入 state,用户主动点「新建对话」则不带。

- 3qfx/xdfx:taskAutoStart 时自动发出开场词(一次性 ref,不受 mount 时 threadId
  抖动影响、不会重复发),不显示「开始回答」按钮;用户主动新建对话才显示按钮手动发。
- rwfx:闸门 autoStartWhenEmpty = autoStartWhenPurposeEmpty && taskAutoStart,即仅
  首次无对话历史时目的树为空才自动开始;目的树有数据始终弹「覆盖并开始问答」确认(不误删 B 系统数据)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:51:50 +08:00
fengxiangdebaozi ce78cbf936 代码提交 2026-06-18 21:09:37 +08:00
吉鑫升andClaude Opus 4.8 6aaa4c841e feat(task-deeplink): 3qfx/xdfx 新建对话改手动「开始回答」+ xdfx 行动详情卡片 + 独立问答智能体列表栏
- AgentChatPage: 纯绑定深链(3qfx/xdfx)不再自动发开场信息,改由右上角「开始回答」按钮手动触发,从根上消除新建对话竞态导致的重复发送;移除 autoSentArmedRef/streamThreadId 相关逻辑
- task-deeplink.ts: 抽出 fetchActionDetail 返回整包 ActionDetail,fetchTaskIdFromAction 复用之
- TaskFxWorkspace: xdfx 左栏「当前任务」下新增「行动详情」卡片(行动名称/叙事)
- StrategyRoutes/WorkspaceRoutes: 独立智能体问答整页新增最左「智能体列表」栏(showAgentList);xdfx 右侧宿主改用 TaskBindChat(精简版)
- 新增 agent-chat-sidebar.tsx

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:55:41 +08:00
吉鑫升 179a3f2b2f Merge branch 'ji' into master 2026-06-18 19:24:03 +08:00
吉鑫升andClaude Opus 4.8 678035afed fix(frontend): 修复 3qfx/xdfx「新建对话」开场白重复发送两次
根因:mount 进入 /new 时 useThreadChat 会抖动 threadId(useState 先给一个 uuid,
紧接着 effect 又 setThreadId(新 uuid))。旧去重用 taskAutoSentRef,而 reset effect
依赖 threadId,被抖动重置回 false;此时 onCreated 未回、streamThreadId 仍为
undefined(守卫放行),自动发送 effect 二次触发 → 发两次、创建两个线程,回答开始时
切到最终线程,表现为「开始回答后突然变成一个」。

改为「武装」标志去重,只在会话真正创建完成(isNewThread→false)后才重新武装,
对 threadId 抖动免疫,保证每个新建会话恰好自动发一次。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:19:23 +08:00
董宏凯 758a8fda09 Merge branch 'dhk-1' into 'master'
feat(roundtable): taskId 会商记录独立存储(不分权) + 第三步生成过程对话/步骤条入库

See merge request knowledge-base-bj/deerflow-server!135
2026-06-18 11:18:24 +00:00
fengxiangdebaoziandClaude Opus 4.8 3a315798e9 feat(roundtable): 深链会商页引入底部快捷跳转栏(去编辑/弹框查看)
抽出 TaskJumpBar 为独立组件,单智能体问答与多智能体会商共用;会商深链
taskId 模式按业务渲染按钮管理配置,并隐藏「编辑」与「弹框查看」按钮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:17:45 +08:00
董宏凯andClaude Opus 4.8 bd5623ebc5 fix(dashboard): 大屏绘制独立页顶栏深色适配 + 修复卡 loading/重绘无数据
- 顶栏(dashboard-agent-topbar)深色下改深蓝(《深蓝色 UI 规范》卡片#122347/
  页面#0C1836/边框#1A2F5A/文字#C5CFE0),不再白底;仅作用于顶栏。
- init effect 始终并行拉输入 JSON 写入 inputRef,恢复历史后「重新绘制」/续问
  才有源数据,否则回退空 prompt 生成不出数据。
- 修复卡在「正在加载任务大屏…」:init effect 改为只依赖 [taskId]、start/
  hydrateTranscript 经 ref 读取、finally 无条件 setLoading(false),避免模型异步
  加载触发 cleanup 把首跑标记 cancelled 后 loading 永不置 false。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:01:30 +08:00
fengxiangdebaozi 6a8f118ad0 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
# Conflicts:
#	CLAUDE.md
2026-06-18 17:53:23 +08:00
董宏凯andClaude Opus 4.8 3758122cf7 style(workspace): 全量模式通用问答左栏深色深蓝玻璃适配
深色(.dark)主题下,全量模式聊天页左栏(GeneralQaSidebar)由扁平 bg-sidebar
改为「深蓝主题底图 + 半透明蓝罩」玻璃面板:与右侧 workspace-chat-bg 主区同一张
fixed 底图、左右连续,叠蓝罩略深一档作面板区分。浅色/浅蓝主题不命中,仍用
sidebar token,内部元素走语义 token 自适配。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:45:17 +08:00
董宏凯andClaude Opus 4.8 03b6fb724c feat(roundtable): 深链策划流程小图 + 流程图弹窗横向/缩略图/深蓝适配
- Step1 消息列表头部:深链 taskId 模式取到目的树后,左侧标题换成「策划流程」
  彩色层级小图(任务→目的→…→叙事,每级配色对齐流程图节点 + 类型小图标),
  点击打开任务目的流程图弹窗;仅第一步、有数据时出现。
- TaskPurposeDialog 流程图:横向(LR)布局 + 右下角缩略图(MiniMap)。
- FlowGraphPanel 深蓝主题适配(随 ThemeContext):容器/头部/画布点阵/连线/图例/
  小地图/节点详情弹窗 + 节点配色(FLOW_NODE_TYPE_META_DARK),浅色红主题不受影响。
- 圆桌侧栏深色兜底:neutral 家族 + 米色 hex(#FAF6F5/#ECE7E4/#E8D0CC) 映射到深蓝
  token(关系图背板、第N步 tag 等不再泛白)。

附带提交工作区其余暂存改动(AI 写作等),不含 config.yaml。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:39:19 +08:00
吉鑫升 3aa629f0df Merge branch 'ji' into master 2026-06-18 17:17:11 +08:00
吉鑫升andClaude Opus 4.8 e5ca717783 fix(frontend): 修复 3qfx/xdfx 任务深链「新建对话」开场白发进上一个会话
根因竞态:从已有会话点「新建对话」时 isNewThread 立刻翻 true,但 useThreadStream
的 onStreamThreadId 慢一帧才归零;自动发送 effect 同帧 submit,而 SDK 的
SubmitOptions.threadId 仅在 hook 线程为空时生效,于是开场白发进了上一个对话
(表现为「一直在同一个聊天框里发消息」,首次正常、之后每次都串)。

- useThreadStream 追加返回当前绑定的流线程 id onStreamThreadId(向后兼容)
- AgentChatPage 自动发送 effect 增加守卫 streamThreadId == null:等底层 stream
  切到全新线程后再发,开场白即落到新建对话

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:16:19 +08:00
fengxiangdebaoziandClaude Opus 4.8 85cca36ddb feat(roundtable): 入库门控——未挂载入库技能则不显示入库按钮、也不执行入库
- 门控改为「是否挂载了入库相关技能」:按 name/name_zh/description 含 `ingest` 或「入库」
  识别任意启用的入库相关技能,不写死示例技能名(真实入库技能改名也能自动识别;
  知识库检索类技能不误命中)。
- 操作层加保险:runIngest 在 ingestAvailableRef 为假时直接 no-op,未挂载技能/非深链
  即使被触发也不执行任何入库动作。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:05:53 +08:00
吉鑫升andClaude Opus 4.8 29cc004b69 feat(frontend): 任务工作区/目的树脱敏 — 业务名→业务码 + 数据值过 desensitize
- 种子脱敏表新增 任务分析→rwfx / 三情分析→3qfx / 行动分析→xdfx(整词在前,优先于「任务」「行动」匹配)
- TaskFxWorkspace:左上业务名 + 底部快捷跳转按钮标签过 desensitize
- TaskPurposePanel:目的树表头/单元格正文/理由过 desensitize
- 硬编码 UI 文案与 admin 按钮管理页保持原文(与菜单管理一致)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:52:59 +08:00
fengxiangdebaoziandClaude Opus 4.8 19028ba627 feat(roundtable): 第三步流程图后入库(示例技能 · 仅深链 taskId 模式)
- 流程图生成完成后,发起用户协助询问是否入库:现在 / 稍后 / 不入库。
- 仅**深链(taskId)模式**(会商 rwfx-A / 3qfx-A / xdfx-A 等)且入库技能启用时才出现入库
  询问 / 协助卡 / 手动入库按钮;普通会商不受影响。
- 「稍后入库」后输入框自由文字做意图识别(后端 /api/flow-extract/ingest-intent:
  start_ingest / add_data / other / unclear),模糊则发起入库协助;另提供手动入库按钮。
- 入库执行复用总结智能体的同一线程,在消息列表出一条「知识库入库」气泡并显示步骤条
  (调度 → 调用入库技能 →(补充内容)→ 提交 → 完成),与第二步同款。
- 入库数据以**用户最后修改后的流程图**为准;并把深链 **taskId 一并传给入库技能**(技能必需)。
- 入库状态(asking/later/done 等)随第三步草稿存取,重载不重复询问。
- 入库技能先用示例(演示用):skills/public/knowledge-base-ingest/SKILL.md(force-add,
  skills/ 目录默认 gitignore),后续替换为真实入库技能时沿用同名即无缝切换。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:49:36 +08:00
吉鑫升 77aa895a70 Merge branch 'ji' into master
# Conflicts:
#	offline-backend-20260512/backend/CLAUDE.md
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/app/gateway/deps.py
2026-06-18 16:36:30 +08:00
AdministratorandClaude Opus 4.8 f762a76ecf feat: 任务工作区 UI 细化(底部按钮主题色/左上业务名/右侧智能体信息默认收起/xdfx目的弹框按行动id解析)
- 底部「快捷跳转」按钮改用主题色 var(--color-main)(随红/蓝主题自适配),hover 用 --brand-950
- 三个工作区左上角标明当前分析页面(任务分析/三情分析/行动分析)
- 进入任务工作区(taskGate/taskBind)时右侧「智能体信息」面板默认收起(AgentDetailPanel defaultCollapsed)
- xdfx「弹框查看」目的树:先用行动id换回任务id 再查(TaskPurposeDialog resolveFromActionId);
  仅深链原始行动id(≠任务id)时触发,切换业务进入不误解析

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:19:02 +08:00
AdministratorandClaude Opus 4.8 095469b8a9 feat: 任务深链对话(metadata.taskId)全局共享、不区分用户
任务工作区(rwfx/3qfx/xdfx)的对话改为按 taskId(+agent_id) 全局共享、跨用户可见可续聊,
不再按创建者隔离;普通个人对话仍按 owner 隔离不变。

- ThreadMetaStore.search/count:metadata 含 taskId 时跳过 owner 过滤(SQL+Memory)
- check_access / _check_ownership / update_metadata / delete / _get_owned_record:
  行 metadata 含 taskId 时视为共享,任何登录用户可读/写/续聊/改标题状态/删除
- 覆盖 require_permission(owner_check) + runs 续聊 owner 校验 + 列表
- 测试 tests/test_thread_meta_task_sharing.py(SQL 跨用户可见/隔离/check_access)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:54:51 +08:00
AdministratorandClaude Opus 4.8 16afa9bcbb feat: 任务工作区可配置跳转按钮(按钮管理) + 底部快捷跳转栏 + 目的树收起/空自动问答
- 新增「按钮管理」admin 页(/admin/task-buttons):按业务(rwfx/3qfx/xdfx)配置跳转按钮,
  支持改名称/类型(外链·切换业务·目的树弹框)/目标/追加参数名(id·task_id)/任务id·行动id/启停/排序
- 跳转按钮从左侧栏移到工作区底部「快捷跳转」栏:红底白字、靠右、变大;rwfx 历史 4 按钮纳入配置
- 后端新增全局共享 store /api/task-buttons (table task_buttons,admin 整体替换)
- xdfx 行动id/任务id:登录暂存原始行动id,按钮可选追加任一;id 参数名可自定义
- rwfx 清除记录移到「新建对话」下、去掉任务操作框;查询任务目的为空时自动发第一次问答
- 目的树面板默认收起;「弹框查看」抽成可复用 TaskPurposeDialog,可作为按钮配置到任意页面
- 文档(根/后端 CLAUDE.md) + 后端测试 tests/test_task_buttons.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:41:05 +08:00
fengxiangdebaozi b04b830039 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
# Conflicts:
#	frontend-web/src/roundtable-planning/hooks/useStep3Dashboard.ts
2026-06-18 15:18:41 +08:00
fengxiangdebaoziandClaude Opus 4.8 a8da6c2632 feat(roundtable): taskId 会商记录独立存储(不分权) + 第三步生成过程对话/步骤条入库
taskId 深链会商存储独立化:
- 新增独立表 roundtable_task_drafts + 路由 /api/roundtable-task-drafts,只按 task_id
  归属、完全不按 user 分权(created_by 仅审计),与个人 roundtable_drafts 物理分离;
  个人草稿表恢复严格按用户隔离(去掉原 task 放权)。
- 后台研讨作业按 taskId 共享:roundtable_jobs 加 task_id,get/get_by_draft/stream/
  resume/cancel 支持 ?task_id= 跨用户读取与控制,作业报告写回 task-draft 存储。
- 前端:新增 api/task-drafts.ts 客户端,useDraftPersistence / useRoundtableDrafts /
  roundtable-jobs / DispatchChainJobModal / RoundtablePlanningPage 按 taskId 路由读写。
- 迁移 20260618_01 + column_additions_mysql.sql(jobs.task_id) + 数据搬迁脚本
  scripts/migrate_roundtable_task_drafts.py(把历史 task_id 行迁出个人表)。

第三步聊天记录像第二步一样存全:
- 总结报告 / 经典看板 / 大屏 三条流的生成过程对话(中间文字 + 步骤条 + stub)落库,
  加载草稿整段回显,而非只剩最终 md / 流程图 / HTML。DraftStep3Snapshot 扩展
  dialogues/seatStubMessages 槽位;存稿走页面 render 期 ref,恢复走各 hook hydrateSaved
  (旧草稿无字段则回退单条「历史记录」气泡)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:11:50 +08:00
董宏凯 62549ab976 Merge branch 'dhk-1' into 'master'
feat: 圆桌第3步大屏多页改用专职智能体 + 健壮 report-json 抽取

See merge request knowledge-base-bj/deerflow-server!134
2026-06-18 06:27:18 +00:00
董宏凯andClaude Opus 4.8 32772b7448 feat(dashboard): 大屏绘制智能体独立页面(按 taskId 存取,不分权)
- 新增带导航+侧边栏的独立页面 DashboardAgentPage:按 taskId 取/存会话,
  有有效 report-json 直显大屏(DashboardApp),否则进生成过程(不自动跳)+输入框可改
- 后端 dashboard_sessions 存储(model/sql/__init__)+路由/by-task(GET/PUT 不分权)
  + deps/app/models 接线;新表靠 create_all 建,无迁移
- 扩展 useStep3Dashboard:buildInitialPrompt(输入任务JSON) + hydrateTranscript(恢复聊天记录)
- 抽出共享 DashboardChatPanel;report-template 增 buildDashboardPromptFromJson
- 输入 seam core/auth/dashboard-input:JSON 接口未就绪先用占位/演示数据兜底
- 路由 StrategyRoutes dashboard-agent + 侧边栏「问答管理→大屏绘制」
- 圆桌业务链卡片「关系图」改横向布局
- 顺带提交:AIWritingContext 登录切换处理、thread-access-error helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:26:27 +08:00
吉鑫升andClaude Opus 4.8 ecb79ce49c feat: 3qfx/xdfx 任务深链 1:N 工作区 + xdfx 行动id解析 + 3q详情链接
- 3qfx/xdfx 单智能体改走任务工作区(TaskFxWorkspace):按 taskId+agent_id 隔离的
  1:N 对话列表;3qfx 纯绑定(taskBind 自动发开场信息),xdfx 带目的树闸门(taskGate)
- 左上角「当前任务」卡片:留存查询到的任务名称+简介(readCopTaskDetail)
- rwfx 工作区新增「切换到三情分析」按钮,带同一 taskId 进入 3qfx
- xdfx 深链传入的是行动id:resolveDeeplinkTaskId 经 action 接口解析出真正任务id
  (可配置 action_detail_url + action_test/mock.action_detail,离线可测)
- 目的树面板「弹框查看」旁新增可配置「3q详情」链接(detail_3q_url + ?id=任务id)
- AgentChatPage 拆分 taskGate/taskBind;use-task-threads 增加 agentId 过滤隔离

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:37:05 +08:00
fengxiangdebaoziandClaude Opus 4.8 500a562bb6 fix(roundtable): 校验环节真正"逐个查 from 缺则补"——删掉自相矛盾的"你不用管连线"
校验本就把第一次抽取的完整 JSON(extracted)传给模型,但开头一句"连线由系统按
层级自动生成、你不用管连线"与第①要点"查每个非根节点 from 缺则补"自相矛盾,
模型读到前者就跳过补 from。删掉该句,改成明确指令:逐个节点检查上面这份 JSON
是不是缺字段,只要某节点缺 from 就补上正确的 from,绝不让非根节点悬空。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:32:59 +08:00
fengxiangdebaoziandClaude Opus 4.8 b5744a1378 feat(roundtable): 结构化抽取连线质量加固——尊重模型 from + 弱模型层级兜底
- parse_flow_graph 选边改为两全策略:模型把连线连全了(仅根节点无入边)→直接用
  模型的 from/edges;模型漏连(≥2 节点无入边,最常见就是漏 from 导致平铺)→按 group
  层级关系确定性推导补全,永不平铺,连线质量与模型能力解耦。
- SOUL.md 增加正确/错误节点示例(带 from vs 漏 from)+ 硬性要求:除根节点外每个
  节点都必须填 from;画法④拆成 group/对应顺序/from 三件事并强调"漏 from 最常见"。
- 校验 prompt 把"连接关系"提为第①要点:逐个检查非根节点是否有 from,缺了必须补对。
- _STRUCTURE 兜底 prompt 同步要求填 from。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:28:07 +08:00
fengxiangdebaoziandClaude Opus 4.8 b8b71523b5 fix: 流程图连线推导加固——个别节点漏 group 时按发射顺序就近归层(carry-forward)
弱模型可能漏标个别节点的 group,原逻辑一漏标就整体放弃推导导致节点平铺。改为漏 group 的
节点沿用前一节点的层级,只有开头无 group / 全程无 group 才回退模型连边。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:18:05 +08:00
fengxiangdebaoziandClaude Opus 4.8 cc06d3f4d2 feat: 流程图连线后端确定性推导(弱模型也连对) + 校验不删边/不简化理由 + 调度中即时反馈
- 连线质量:新增 _derive_layered_edges——连线不再信任模型,后端按节点 group 分层 +
  层级对应关系确定性推导(任务→各目的扇出、相邻等量层按下标一一对应、不规则层对回退模型边)。
  模型只需 group 标对 + 各层内按相同对应顺序排列,连线就一定对。SOUL/_STRUCTURE 据此调整。
- 校验不删边:validate 要求节点数≥原图 + gen() 兜底「校验后节点/边变少则回退抽取结果」。
- 不简化理由:_SHAPE + SOUL detail 改为完整理由原样保留、不简化/精简。
- 调度中即时反馈:ask() 意图分类前立刻建「调度中」气泡,定意图后各分支复用(relabelDialogue
  + runFlowExtract.bubbleId),不再等 intent 接口返回才出内容。
- 测试 tests/test_flow_extract_parse.py 加层级连线推导 3 例(共 10 过)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:56:04 +08:00
fengxiangdebaoziandClaude Opus 4.8 0890b3c375 feat: 第三步流程图后端抽取智能体 + 意图识别 + 问答/画布编辑
- 新增专职「结构化抽取智能体」roundtable-structure(种子资产 SOUL+config,注册进 seeder
  启动 seed+sync 进 DB,可在智能体管理页编辑、未来挂入库技能)。
- /api/flow-extract/stream 改用该 agent 的 SOUL(load_agent_soul,缺失回退内置默认) +
  模型(前端所选 > agent 配置 > 默认);抽取/校验/编辑统一。SOUL 先输出简要分析过程再出
  flow-json,模型慢时也有实时反馈(含 <think> 思考流)。
- 新增 /api/flow-extract/intent 意图识别(参考 ai_writing 一次性 JSON 范式):识别
  重做报告/改流程图/重画流程图/提问/模糊;前端按意图分流,模糊则发起澄清让用户点选,
  不再用关键词硬路由(修「说重做报告却出流程图」)。
- Phase 2 问答修改:edit 模式(当前图+指令→改图→校验);Phase 3 画布手动改/删节点。
- 修:用户消息发送即上屏(不等模型/分类返回);regenerate_report 不再清空对话历史;
  全屏 portal 到 body 工具栏恢复;解析兼容独立 edges 数组。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:19:02 +08:00
董宏凯andClaude Opus 4.8 ed887f762e feat: 圆桌第3步大屏多页改用专职智能体 + 健壮 report-json 抽取
修复「大屏多页抽取 json 又失败」:旧实现复用画 HTML 的 roundtable-report
智能体 + rfind 围栏 + 单次 json.loads 的简单粗暴抽取,弱模型跑偏/格式偏差即失败。

- 新内置专职单例 roundtable-dashboard「大屏多页数据智能体」:SOUL 专讲逐席位
  分析 → report-json 契约,禁工具/禁文件/禁 HTML;新 run policy dashboard 禁用一切工具
- harness 新增 report_json.py:多策略抽取(report-json/json/裸 JSON/截断)+ schema
  归一化(移植前端 extractReportData/coerceReportData),返回可直接渲染的归一化 JSON
- 后台 run_dashboard_data 改用专职 agent + dashboard policy + 健壮抽取
- 新增 /api/multi-agent/dashboard/init;前端 initDashboardThread + useStep3Dashboard 改用之

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:47:21 +08:00
fengxiangdebaozi bb05f7b1de Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-17 21:31:30 +08:00
fengxiangdebaozi d3880f51b3 第三步结构化输出优化 2026-06-17 21:31:22 +08:00
董宏凯 a2fcd85746 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-17 20:41:56 +08:00
吉鑫升andClaude Opus 4.8 47bf0c2a1b Merge branch 'ji' into master
合入 ji 的两处前端修复(任务深链开场信息补 task_id + 加宽任务目的弹框)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:40:47 +08:00
吉鑫升andClaude Opus 4.8 85dc5466c5 fix(frontend): 任务深链开场信息补充 task_id + 加宽任务目的弹框
- rwfx 任务深链开场信息末尾追加「任务id(task_id)为xxx」,让后端能识别任务
  (buildOpeningText 增加 taskId 参数,覆盖 pendingAgentMessage 与任务上下文两条路径)。
- 任务目的「弹框查看」用 sm:!max-w-[90vw] 覆盖 DialogContent 基类的 sm:max-w-lg
  (此前无前缀的 max-w-4xl 被基类响应式类压回 512px,故加宽不生效)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:39:22 +08:00
董宏凯 c97d88d33f Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-17 20:38:59 +08:00
董宏凯 60cc6665ce tij 2026-06-17 20:38:51 +08:00
董宏凯 d82973c8c8 Merge branch 'dhk-1' into 'master'
feat: 业务链条配置管理导航与顶栏用户名统一

See merge request knowledge-base-bj/deerflow-server!133
2026-06-17 12:25:17 +00:00
fengxiangdebaoziandClaude Opus 4.8 fca39eeaee feat: taskId 深链会商按任务隔离草稿 + 第三步自动结构化流程图
- 深链 ?taskId=&goPath=*-A 透传 taskId 进会商,按 taskId 隔离存取草稿(一个 taskId 多条、可新建);
  后端新增 GET /api/roundtable-drafts/by-task/{id} 列表 + RoundtableJobRepository.status_map_by_drafts 富化。
- 第三步生成 md 报告后自动结构化输出流程图(仅 taskId 模式):有报告基于报告、无报告基于各席位交付,
  整段内联数据来源逐条抽取;层级优先用业务链总控提示词写明的层级,否则用 stages 的 goal。
- 流程图节点加 group(层级名)标签 + detail(条目名称+理由);强 prompt 压制 coordinatorPrompt 报告锚定,
  禁阶段编号节点;FLOW_STRUCTURE_RULE 精确画法(根=任务、每层每条成节点、按对应关系串分支、排除汇总段落)。
- flow-graph 解析容错:JSON.parse 失败退回正则逐字段抽取,兼容裸换行/尾逗号,避免节点被整体丢弃。
- 修:重开第二步后第三步基于新一轮分析重新生成(复位 step3 闸门 + 中断旧三流)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:45:35 +08:00
董宏凯andCursor e6a41dbc97 Merge branch 'master' into dhk-1
Integrate task deeplink/FX workspace updates from master while keeping dhk-1 menu-overrides and sensitive-words documentation in CLAUDE.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 19:39:14 +08:00
董宏凯andClaude Opus 4.8 6746fe4a78 feat: 导航/侧边栏菜单脱敏 + 圆桌会商各步输入框自适应高度
- 菜单标签展示层脱敏:useSidebarMenuItems 直接用 /api/sensitive-words
  结果构造脱敏器,覆盖导航栏/三套侧边栏皮肤/三级子导航;菜单管理页用原始树不受影响
- 新增可复用 AutoGrowTextarea(自适应高度多行框),替换圆桌 Step1/2/3 输入框:
  多行时自动扩展、到上限框内滚动;Step2 两个单行 input 改多行;统一 Enter 发送/Shift+Enter 换行
- 各步输入框最大展开高度翻倍(Step1 320 / Step2 280 / Step3 224)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:13:48 +08:00
董宏凯andClaude Opus 4.8 25e4e3dbc9 feat: 敏感词管理页面(脱敏词后台化,限 lqq 账号)
- 后端 /api/sensitive-words:全局共享词表,开放读、admin 或 lqq 整体替换写
- 前端 desensitize 种子化:SEED_DESENSITIZE_MAP + 运行时 replaceDesensitizeMap
- 启动加载后台配置回灌脱敏映射;管理页支持增删改启停 + 一键导入种子词
- 入口置于左下角设置下拉(全量/简洁两种侧边栏均有),仅用户名 lqq 可见

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:05:07 +08:00
吉鑫升 af8db1d24c Merge branch 'ji' into master 2026-06-17 17:39:23 +08:00
Administrator bb2e896fba Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-17 17:37:48 +08:00
AdministratorandClaude Opus 4.8 0dca637645 style: 任务FX工作区操作区从右下角浮层移入左侧任务栏底部
- TaskFloatingActions 重命名为 TaskActionsPanel,挂到对话列表底部
- 不再 absolute 覆盖右侧聊天输入框;编辑/下一步/清除记录改为操作卡片
- 左侧栏样式增强(标题/会话数/选中态高亮)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:37:35 +08:00
吉鑫升andClaude Opus 4.8 54fff09e74 feat(frontend): 不带 sidebar 的聊天页顶栏右侧也展示名称+岗位
紧凑布局(无全局 Header)下复用 UserIdentityBadge,数据源与全量模式 Header 一致,仅在 !compactChrome 时渲染,避免与全局顶栏重复。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:36:39 +08:00
董宏凯andClaude Opus 4.8 39d7dab3e2 chore: 提交 AI 写作研究侧栏改动与前端开发环境变量
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:32:40 +08:00
董宏凯andClaude Opus 4.8 fc7576fea3 feat: 圆桌会商 Step1 左栏样式优化(业务链关系图/分析记录卡片)
- 业务链:红色主题卡片头+单选圆点+席位胶囊+描述;选中可展开关系图,
  席位按 stages 分组用彩色圆形小图标排列,悬停浮层显示名称/描述
- 分析记录:圆角改 5px,新增已完成/未完成标签与「第 N 步」进度,
  标题/状态/时间/步骤四角布局,加选中高亮与 hover 阴影
- 修复滚动容器 overflow 裁剪导致卡片左边框被切

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:31:24 +08:00
吉鑫升 6c47da82c4 Merge branch 'ji' into master 2026-06-17 17:21:15 +08:00
吉鑫升andClaude Opus 4.8 392394b4c9 fix(db): 修复 alembic 多 head 导致后端启动失败
迁移图存在两处问题导致 `alembic upgrade head`(网关启动时执行)报
"Multiple head revisions are present":
1. 20260612_01 分叉出 20260615_01 与 20260616_03 两个 head 未合并;
2. 20260611_01、20260616_02 各被两个文件重复占用,污染 DAG 产生幽灵 head。

处理:
- roundtable_draft_task_id 20260611_01 -> 20260611_02(接在 square_id 之后),
  20260612_01 父节点改指 20260611_02;
- skill_favorites 20260616_02 -> 20260616_04(接在 knowledge_folders 之后),
  20260616_03 父节点改指 20260616_04;
- 新增 no-op 合并迁移 20260617_01 合并 (20260615_01, 20260616_03)。

保留 20260611_01 / 20260616_02 原 id 不变,仅重命名重复副本,已 stamp 的库不受影响;
所有迁移均有 _has_table/_has_column 守卫,全链路在已建表的库上重跑为安全 no-op。
`alembic heads` 现仅余单一 head 20260617_01,无重复告警。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:19:29 +08:00
吉鑫升andClaude Opus 4.8 a2bb8c80a2 feat: 任务目的树表格化展示 + 切换会话停止老对话服务端 run
- task-purpose: normalizeNode 容错(未知 *Dto 兜底/文本字段改名兜底);新增 flattenPurposeTree 拍平目的树
- TaskPurposePanel: 目的树改表格展示(列数自适应),支持弹框放大查看
- threads/hooks: 新增 stopThreadRun,真正取消服务端 run(非仅断开本地 SSE)
- AgentChatPage: taskGate 下切换/卸载会话时停掉老对话服务端 run
- TaskFxWorkspace: 操作按钮(编辑/下一步/清除)改右下角常驻红色浮层

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:42:05 +08:00
AdministratorandClaude Opus 4.8 ba0ec20532 chore: .env.development 也开启 cop-task-detail mock(VITE_MOCK_COP_TASK_DETAIL=true)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:10:57 +08:00
AdministratorandClaude Opus 4.8 138a5adb1c chore: 前端 cop-task-detail 走 mock + 后端 task_deeplink 同步到 backend/config.yaml
- frontend-web/.env.production: VITE_MOCK_COP_TASK_DETAIL=true(consumer 外网不可达,登录深链任务详情走本地假数据)
- backend/config.yaml: 补上 task_deeplink 段(test/delete_test=true 走 mock),与外层 config.yaml 对齐;服务器实际加载的是 backend/config.yaml,否则 /api/public/config/task-deeplink 返回空导致 rwfx 目的树闸门失效

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:08:54 +08:00
吉鑫升andClaude Opus 4.8 a2d767e2db feat: rwfx 任务深链支持目的树闸门 + 任务↔对话(1:N)绑定
后端
- config.yaml 新增 task_deeplink 段:完整 URL(无 base)+ test/delete_test 两个独立测试开关
  + mock 假数据(目的树 / delete 结果,可随时编辑)
- 新增 TaskDeeplinkConfig 模型挂到 AppConfig;新增公开只读接口
  GET /api/public/config/task-deeplink 暴露给前端(/api/public 已在鉴权白名单)

前端
- 登录深链 goPath=rwfx 进入「任务工作区」:左侧按 metadata.taskId 过滤的对话列表
  (新建对话 / 编辑 / 下一步 / 清除任务记录)+ 右侧问答
- 新建对话(含首次进入)顶部展示目的树闸门:有数据弹覆盖确认→调 delete,success 才开始问答;
  恢复历史对话不跑闸门
- 新建 thread 打 metadata.taskId 标签实现「一个任务挂多个对话」(1:N)
- 地址/开关/假数据全部走后端配置,拿不到时用内置默认值兜底;test/delete_test=true 走假数据

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:59:02 +08:00
g zy dd631d57d4 Merge branch 'gzy' into 'master'
feat: 按钮样式变更

See merge request knowledge-base-bj/deerflow-server!131
2026-06-17 06:29:32 +00:00
ZyGuo a3c8c50212 feat: 按钮样式变更 2026-06-17 13:49:06 +08:00
董宏凯andClaude Opus 4.8 ef1a12ab74 feat: zzJC/zzXD 左栏智能体列表对齐全量版彩色卡片样式
- 抽出共享配色助手 agent-icon-color.ts(按 id 散列、分浅色/深蓝主题),
  general-qa-sidebar 与 agent-module-sidebar 共用
- AgentModuleSidebar:背景改白底、去毛玻璃;智能体项改为彩色渐变图标卡片
  (图标+名称+两行简介+选中态主色描边/右侧主色条);分组标题与聊天记录配色统一
- 保留按类型分组/折叠与会话过滤逻辑

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:11:27 +08:00
董宏凯andClaude Opus 4.8 1c8b5b23a1 feat: 菜单管理(侧边栏覆盖层)
管理员可对一/二/三级菜单(含注入的 iframe 轻应用节点)改名、停用/启用、
排序、跨层级移动。静态菜单仍是图标/路由来源,后端只存每节点覆盖项。

后端: /api/menu-overrides(全局共享;开放读、admin 原子替换;≤3 级校验),
新增 persistence/menu_overrides 持久化层(create_all 自动建表)。
前端: 合并逻辑 menu-overrides.ts、MenuManagementPage(@dnd-kit 拖拽+移动到
父级选择器+上下箭头+改名/停用)、左下角设置入口、Header/侧边栏自动生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:34:12 +08:00
fengxiangdebaozi 8d0759123a chore: 同步 backend CLAUDE.md 文档换行格式 2026-06-16 22:43:18 +08:00
fengxiangdebaozi 318f6c0c0c feat: 业务链条配置管理导航与顶栏用户名统一
业务链条页增加配置管理入口与返回导航;配置管理页通过 aliasPaths 保持侧栏选中;顶栏用户名与设置个人信息页共用 getAccountDisplayName。
2026-06-16 22:42:42 +08:00
fengxiangdebaozi d8102e8db8 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 22:18:22 +08:00
AdministratorandClaude Opus 4.8 5b610fd807 feat: 上下文压缩支持状态持久化缓存,前端过滤隐藏消息
- summarization_middleware 摘要缓存写入 LangGraph thread state,跨进程/重启可复用
- 摘要消息改为 SystemMessage,提示模型静默使用、不泄露
- thread_state 新增 context_summary_cache 字段
- app_config 加载时记录 summarization 配置日志
- config.yaml 压缩触发/保留阈值调整为生产值(token 预算)
- 前端 input-box / message-list 过滤 hide_from_ui 消息

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:16:06 +08:00
董宏凯 7afdf5b363 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 22:01:18 +08:00
董宏凯 6c5a2cffe3 配置页面修改 2026-06-16 22:01:06 +08:00
fengxiangdebaozi 0bad0eb306 代码隐藏 2026-06-16 22:00:30 +08:00
吉鑫升 5f39879e4d Merge remote-tracking branch 'origin/master' into ji 2026-06-16 21:08:37 +08:00
吉鑫升andClaude Opus 4.8 342fa8265a feat: 知识批量沉淀改为后台任务,避免 nginx 504
- /ingest/threads/batch 新增 background 参数(默认 true),立即返回 {queued:true}
- 抽取循环抽出 _run_batch_ingest,后台路径显式传入 created_by
- 前端知识库页与输入框、线程 hooks/types 配套调整
- config.yaml 上下文压缩测试值

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:07:17 +08:00
董宏凯 1fa2338ac3 Merge branch 'dhk-1' into 'master'
feat(task): 任务详情(cop-task-detail)登录深链增加本地假数据开关

See merge request knowledge-base-bj/deerflow-server!130
2026-06-16 13:07:13 +00:00
董宏凯 56e17f826c Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 21:06:22 +08:00
董宏凯 bc5c2fa3d4 样式修改 2026-06-16 21:06:14 +08:00
fengxiangdebaozi 44cb118f95 bug修复 2026-06-16 20:59:44 +08:00
fengxiangdebaoziandClaude Opus 4.8 1ece2c5a4f fix: 修复全量模式综合管理用户排行榜泄漏到其他侧边栏
综合管理菜单存在两个 id 相同的「用户排行榜」子项,侧边栏以
key={item.id} 渲染导致 React keyed reconciliation 失效,离开综合
管理切换到其他一级菜单时重复 key 的节点无法正确卸载,残留到新的
侧边栏。删除冗余的 adminOnly 重复项(保留的项已通过 userPath 同时
覆盖 admin/非 admin)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:59:14 +08:00
董宏凯andClaude Opus 4.8 2fce5e61c9 feat(ui): 全量版通用问答侧栏深蓝主题适配 + 任务深链接口路径修正
- general-qa-sidebar:侧栏底色 bg-white → bg-sidebar(随主题,深蓝主题=深蓝);
  「新对话」按钮底色 color-mix 混 transparent 而非 white;智能体图标底色改用
  《深蓝 UI 规范》§1.1.7/§1.1.8 的蓝-青系(替换原高彩度玫瑰/琥珀/翠绿渐变)
- task-deeplink:cop-task-detail 接口路径修正 taskAnalysSearch → taskAnalyseSearch
- 同步 mock-data 路径 + CLAUDE.md 文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:57:21 +08:00
董宏凯andClaude Opus 4.8 d5d0494928 feat(task): 任务详情(cop-task-detail)登录深链增加本地假数据开关
- 新增 VITE_MOCK_COP_TASK_DETAIL 开关(默认 false 走真实接口;true 时该接口走本地假数据,
  供 consumer 外网不可达时调试),与 VITE_USE_REAL_DATA 独立
- utils/request.ts 按开关把 cop-task-detail 请求切到本地 mock adapter
- task-mock-data.ts 增加 cop-task-detail 假数据 + matcher/adapter
- 各 .env(development/network/production/sit) + .env.example 补充该开关说明

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:57:21 +08:00
Administrator b380575f16 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/core/page-layout/sidebar-menu.ts
#	frontend-web/src/core/system-settings/api.ts
#	frontend-web/src/core/system-settings/hooks.ts
#	frontend-web/src/strategy-components/Header.tsx
#	offline-backend-20260512/backend/CLAUDE.md
#	offline-backend-20260512/backend/app/gateway/routers/system_settings.py
#	offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py
#	offline-backend-20260512/backend/scripts/sql/column_additions_mysql.sql
2026-06-16 20:01:42 +08:00
AdministratorandClaude Opus 4.8 c21a1127f8 feat: 提炼模板弹窗加宽,后端配置上下文压缩测试值
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:50:33 +08:00
董宏凯 e621ce88e6 Merge branch 'dhk-1' into 'master'
feat(ai-writing): 进入写作后自动收起研究模板侧栏

See merge request knowledge-base-bj/deerflow-server!129
2026-06-16 11:50:26 +00:00
董宏凯 761491ec41 代码修改 2026-06-16 19:50:07 +08:00
董宏凯 a35ed77ff0 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 19:41:40 +08:00
董宏凯andClaude Opus 4.8 245ac969a7 feat(task-deeplink): 会商/智能体问答嵌入任务管理布局
任务深链(goPath=3qfx/3qfx-A 等)跳转的多智能体会商 / 单智能体问答
改为嵌进「任务管理」对应业务上下文,侧边栏保持 COH中任务→对应业务 高亮。

- StrategyRoutes: task-management 配下新增 6 条嵌入路由(每业务 roundtable + agent)
- AgentChatPage: 新增 chatBasePath 可选属性,覆盖发消息后 URL 改写 / 新会话路径
- task-deeplink: 跳转目标改为业务别 task-management 嵌入路由(BUSINESS_TASK_BASE)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:41:28 +08:00
fengxiangdebaoziandClaude Opus 4.8 ab21911696 feat(ai-writing): 进入写作后自动收起研究模板侧栏
把侧栏+内容区抽成 Provider 内组件 AIWritingBody,读 useAIWriting 状态:
status==='idle' && 非历史模式才显示研究模板侧栏;开始写作/查看历史后收起,写作区铺满。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:41:05 +08:00
董宏凯 380407ae2f Merge branch 'dhk-1' into 'master'
代码提交

See merge request knowledge-base-bj/deerflow-server!128
2026-06-16 11:31:03 +00:00
fengxiangdebaozi c0e89044c7 代码提交 2026-06-16 19:30:36 +08:00
吉鑫升andClaude Opus 4.8 4f0494a97a feat: 聊天增加回到最新悬浮钮,智能体面板生成即收起
- 复用 ConversationScrollButton 接入 MessageList,滚离底部时显示一键回到最新问答的悬浮按钮;新增 scrollButtonClassName 以适配各页输入框高度
- 智能体详情面板改用 open+selectedArtifact 判定收起,流式写文件开始即收起让位预览,不再等生成结束

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:24:48 +08:00
董宏凯 baf3fadce6 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!127
2026-06-16 11:02:34 +00:00
fengxiangdebaozi ecf068a61c Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 19:01:59 +08:00
fengxiangdebaozi ecaaec939d 路由修改 2026-06-16 19:01:52 +08:00
董宏凯 e3c1adb71a Merge branch 'dhk-1' into 'master'
feat(qa-center): 问答中心左侧栏改造(0616 需求 2-5)

See merge request knowledge-base-bj/deerflow-server!126
2026-06-16 10:56:47 +00:00
董宏凯 ce165ece69 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 18:56:13 +08:00
董宏凯 63bddfdea7 代码修改 2026-06-16 18:56:04 +08:00
fengxiangdebaozi 208a02a912 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 17:59:35 +08:00
fengxiangdebaozi f750b7f04d 地址修改 2026-06-16 17:59:27 +08:00
fengxiangdebaoziandClaude Opus 4.8 ba5aeefe98 feat(ai-writing): 研究模板独立实体 + 配置页 + 内置范例 + 点击直接进入写作(0616 #5)
后端:
- 新持久化包 research_templates(表 ai_writing_research_templates,create_all 建表,注册 models/__init__),deps 接 research_template_store
- 路由 /api/ai-writing/research-templates CRUD(在 ai_writing.py)
- 内置范例 网摘/周报/要讯(DEFAULT_RESEARCH_TEMPLATES + seed_defaults,表空时由 list 接口幂等播种)
- tests/test_research_templates.py(9 passed)

前端:
- api/ai-writing-research-templates.ts + hooks/useResearchTemplates.ts
- 配置页 ResearchTemplatesConfigPage(/page/canvas/research-templates)CRUD
- ResearchSidebar 研究模板改用研究模板实体;点击回填整张表单并直接 startWriting 进入写作;顶部齿轮进配置页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:59:05 +08:00
吉鑫升andClaude Opus 4.8 d82b5ed884 feat: 摘要中间件增加压缩日志,提炼模板编辑框定高
- summarization_middleware: 真正压缩与复用缓存均记录 info 日志,便于无 stream writer 场景(定时任务/渠道/测试)排查
- KnowledgeManage: 提炼模板 system prompt 编辑框固定为 50vh 高度,内部滚动

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:40:14 +08:00
xu hd 12f50123dc Merge branch 'xuhd' into 'master'
Xuhd

See merge request knowledge-base-bj/deerflow-server!125
2026-06-16 09:27:02 +00:00
xuhd 4511c0dee7 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-06-16 17:26:19 +08:00
xuhd f6809349d2 来源上传组件样式优化 2026-06-16 17:26:13 +08:00
吉鑫升andClaude Opus 4.8 97da7dc87f feat: 推荐问题配置支持按标签搜索并在列表展示标签
- 后端列表接口为每个推荐问题附带 tags,并支持 tag_ids OR 过滤
- 前端配置页新增 TagSearchBar(标题/内容搜索 + 标签筛选),卡片展示标签
- 打标签后失效 recommended-questions 查询,列表即时刷新
- 过滤时禁用拖拽排序,避免破坏 sort_order

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:52:30 +08:00
董宏凯andClaude Opus 4.8 45049975c2 feat(roundtable): 多智能体会商 + 业务链条页脱敏
- 区域级脱敏 Hook useDesensitizeSubtree:对左右侧面板、顶部标题、
  业务链条选择器/列表弹窗/列表页/配置页/映射页整片脱敏(文本节点,
  跳过 input/textarea 等可编辑控件)
- 消息列表正文保持原文,仅智能体名称脱敏(senderName / 席位名 / 流程图节点)
- Step3 总结/报告/大屏、推荐弹窗等结果面板脱敏
- 沙箱产物预览不脱敏:ArtifactFileDetail 加 disableDesensitize,会商沙箱传入
- desensitize.ts 新增 desensitizeDeep 递归脱敏结构化数据

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:43:29 +08:00
fengxiangdebaoziandClaude Opus 4.8 0e4caa5a66 feat(ai-writing): 课题研究笔记本内嵌打开 + 新建/所有空间入口(0616 #5)
- 左栏点笔记本/所有空间 → 右侧内嵌打开(不跳路由),写作布局保持挂载(display:none,不丢 setup 线程),顶部「返回写作」
- ResearchSidebar 加「新建空间」(弹窗 createSearchSpace) 与「所有空间」入口
- StudioNotebookDetailPage 加可选 notebookId prop;StudioNotebooksPage 加可选 onOpenNotebook(内嵌时卡片走回调而非 Link),列表内点空间内嵌打开详情
- 内嵌笔记本详情用 ChatRuntime 包裹(修复 useArtifacts must be used within an ArtifactsProvider)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:38:22 +08:00
fengxiangdebaoziandClaude Opus 4.8 f39cffcb9a feat(qa-center): 问答中心左侧栏改造(0616 需求 2-5)
- 多智能体会商:Step1 落地左栏「业务链(public 前3) + 分析记录」;点链=勾选(autoChainResult),第一步澄清后点「进入研讨」才按链执行
- zzJC/zzXD:独立模块路由 /page/strategy/qa/{zzjc,zzxd},常驻左栏(按 agent 标签分组 + 仅本模块会话的聊天记录),点智能体模块内切换不跳页,默认落地配置类型下首个智能体
- zzJC/zzXD admin 设置:左栏齿轮→单选类型(TDesign)配置面板+智能体预览;后端 system_settings.agent_modules 持久化(GET 开放/PUT admin)+ test_agent_modules
- 课题研究:左栏「研究模板(文章类型) + 笔记本」;模板点击预填表单,笔记本点击设为素材来源(selectedNotebookId 上提 WritingFormContext)
- 通用问答:全量模式历史侧栏默认展开

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:38:22 +08:00
吉鑫升andClaude Opus 4.8 5182ea5d11 feat: 技能收藏与常驻(always-on)支持
- 新增 skill_favorites / skill_always_on 迁移与持久化模型字段
- skills 路由、存储、前端设置页支持收藏与常驻
- 调整摘要中间件与 lead agent 提示词
- 同步岗位 position_sync 及相关配置清理

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:37:43 +08:00
吉鑫升andClaude Opus 4.8 5ebbb82d66 feat: 知识库文件夹/模板、技能压缩设置与摘要窗口调优
- 知识库新增文件夹与模板支持(模型/仓储/服务/迁移),前端知识管理组件
- 岗位管理模型与接口完善
- 技能新增压缩设置页与相关存储/工具能力
- 系统设置扩展配置项
- 摘要中间件 keep 窗口改用 token 预算(16000),减少重复压缩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:23:54 +08:00
吉鑫升andClaude Opus 4.8 e6662cf673 refactor: 移除提示词模板页面及相关接口,精简岗位/标签/用户提示词模型
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:32:21 +08:00
董宏凯 fe0dc8d1a5 Merge branch 'dhk-1' into 'master'
feat(roundtable): 第二步席位执行模式(快速/思考/专业问答/多智能体)+ 子智能体任务卡片 + 后台完成跨页通知 + 链条默认模式持久化

See merge request knowledge-base-bj/deerflow-server!124
2026-06-16 06:19:04 +00:00
董宏凯 8794c51f88 提交 2026-06-16 14:18:15 +08:00
董宏凯andClaude Opus 4.8 8d1669adad feat(roundtable): 后台作业按用户并发限流 + 草稿持久化优化
后台挂起作业每个 agent 轮次(leader/seat/report/summary/dashboard)开跑前
先取该用户的后台并发额度(默认 2,ROUNDTABLE_BACKGROUND_SLOTS_PER_USER 可调),
跑完释放;同一用户多个后台作业共享额度,避免把单连接 SQLite checkpointer 和
离线模型端点打满拖垮前台实时会商。前台实时路径不经过此额度,不受限。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:17:30 +08:00
董宏凯andClaude Opus 4.8 da3d38013e feat(ai-writing): 素材收集技能调用失败时容错回退到其他技能
默认/选中技能调用失败(超时、报错、全部检索词失败)时不再直接报错,
改用其他技能继续检索:节点 emit skill_error 步骤(带真实原因),配置技能
全部失败且无新素材时跑 fallback 兜底(内置知识库检索优先,再到目录其它已
启用技能)。前端 SkillStepsCard 渲染 skill_error(玫红)/ fallback(琥珀),
时间线展示"技能报错→改用其他技能搜集"。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:15:49 +08:00
g zy b2ef4ce89e Merge branch 'gzy' into 'master'
feat: iframe增加智能体选择

See merge request knowledge-base-bj/deerflow-server!123
2026-06-16 05:07:03 +00:00
ZyGuo f525c49267 feat: iframe增加智能体选择 2026-06-16 13:06:36 +08:00
吉鑫升andClaude Opus 4.8 838e986670 feat: 导航菜单新增岗位管理与提示词模板入口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:14:47 +08:00
吉鑫升 fa70b31873 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/components/workspace/input-box.tsx
#	offline-backend-20260512/backend/CLAUDE.md
2026-06-16 11:07:31 +08:00
吉鑫升andClaude Opus 4.8 3855c30edd feat: 新增岗位管理、提示词模板与资源标签,完善用户提示词与推荐问题接口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:49:36 +08:00
董宏凯 c7dccf8c0b Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 10:09:35 +08:00
董宏凯 2526304394 sd 2026-06-16 10:09:30 +08:00
fengxiangdebaozi e1da39253a Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-16 10:06:14 +08:00
fengxiangdebaozi 6b49741e69 代码修改 2026-06-16 10:05:54 +08:00
吉鑫升andClaude Fable 5 2f414c6e08 fix: HTML 预览子页面改用 JS 切换,避免沙箱 blob 导航被拦截
上一版通过设置 location.hash 触发 CSS :target 切页,但沙箱 blob
iframe 禁止导航到自身 blob URL,浏览器报 Not allowed to load local
resource,且 :target 仅在真实片段导航时更新,注定失效。改为完全
不动 hash,用 JS 模拟 html-multipage 技能的 .page/.pages 显示隐藏
(内联 display 覆盖类级 CSS),普通目录锚点仍平滑滚动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 09:19:15 +08:00
董宏凯andClaude Opus 4.8 9ed5420381 feat(light-app): 应用管理页加类型分类筛选 + 修复深色顶栏标题高分屏内边距
- 应用管理工具栏新增「全部/新页跳转/iframe嵌套」分类切换,选中 iframe
  嵌套时出现挂载菜单下拉,可按 mountLocation 进一步筛选(深色/浅蓝主题适配)
- 深色顶栏 .header-center-title 内边距由固定 155px 改为 8.07vw,随视口
  等比缩放以匹配 background cover,修复 4K 等高分屏两侧菜单贴近中间的问题

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:33:18 +08:00
董宏凯andClaude Sonnet 4.6 69c8c14c91 fix(nav): 修复问答梳理等带子路径页面的导航高亮丢失问题
KnowledgeBasePage 加载后自动跳转至 /knowledge/:noteId,原精确路径匹配
导致顶栏和侧边栏选中态丢失。将 hasActiveChild / isItemActive /
menuItemContainsPath 等 8 处判断统一改为前缀匹配
(pathname === path || pathname.startsWith(path + "/")),
覆盖浅色侧边栏、深色侧边栏、顶部导航栏三套组件。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:50:26 +08:00
董宏凯andClaude Sonnet 4.6 88b1a330a6 fix(roundtable): 修复弹窗被顶部导航栏遮挡的 z-index 问题
将全局 Header z-[2101] 降为 z-[45]、圆桌页内部 header z-[2000] 降为 z-[40],
使两者低于 Shadcn Dialog 遮罩层 z-50,弹窗打开时可正常覆盖导航栏。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:32:07 +08:00
董宏凯 9e481cdcaa Merge branch 'dhk-1' into 'master'
feat(theme): 浅蓝色主题适配 —— COH任务三子页 + 应用中心子页

See merge request knowledge-base-bj/deerflow-server!122
2026-06-15 10:21:08 +00:00
fengxiangdebaozi db50493577 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-15 18:20:39 +08:00
fengxiangdebaoziandClaude Opus 4.8 3907034a1d feat(roundtable): 第二步席位执行模式(快速/思考/专业问答/多智能体)+ 子智能体任务卡片 + 后台完成跨页通知 + 链条默认模式持久化
- 席位执行模式:选择参会智能体弹窗加模式选择器,映射 thinking/reasoning/subagent
  透传 recommend/chain/dag 三条编排;后端 _special_run 覆盖 seat policy(仅席位,
  总控不变);ultra 解锁席位 task 子代理(custom 流回传 task_running 实时进度)。
- 子智能体任务卡片:复用主聊天 SubtaskCard + SubtaskContext,从席位流解析 task
  调用/结果,在 Step2 气泡渲染多张任务卡片(覆盖业务链条路径)。
- 后台挂起也按所选模式跑(StartParams→inprocess gateway→run_seat 覆盖 policy)。
- 后台作业完成全工作区跨页通知:RoundtableJobNotifier 挂在 PageLayout 常驻层,
  作业终态弹「达成共识」同款浮窗。
- 业务链条可配「默认执行模式」并持久化(roundtable_chains.seat_mode;选链时弹窗
  默认用它,仍可临时改)。
- 修复后台 Step3 HTML 看板概率截断:deepseek max_tokens=4096 单轮写不完长 HTML,
  产物自愈重试上限 2→5,让 append 续写写完整(同惠及 summary/dashboard)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:20:20 +08:00
董宏凯andClaude Sonnet 4.6 b1b2540dc0 feat(theme): 浅蓝色主题适配 —— COH任务三子页 + 应用中心子页
- ThemeContext: 新增 data-theme="lightblue" 属性管理,支持红/蓝双浅色主题
- strategy-data.css: 新增 [data-theme="lightblue"] 变量覆盖块(与浅红默认共存)
- SentimentAnalyzes / TaskAnalyzes / ActionAnalyzes / KanbanColumn / TaskCard / ActionColumn:
  isDark ? dark : isLightBlue ? blue : red 三路适配
- LightAppListPage: 应用列表页品牌色(标题点、徽章、搜索 focus、计数、进入按钮)浅蓝适配
- LightAppNoteQAPage: 问答发送按钮浅蓝适配
- light-app-management.css: 新增 [data-theme="lightblue"] CSS 覆盖块(主按钮、保存、开关、输入 focus 等)
- light-app-pages.css: 浅蓝模式页面底色轻染 #F9FCFF

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:16:06 +08:00
董宏凯 5dc1ed5685 Merge branch 'dhk-1' into 'master'
fix(sandbox): 修复 Markdown 编辑保存后内容被重置回原值

See merge request knowledge-base-bj/deerflow-server!121
2026-06-15 09:11:09 +00:00
董宏凯 1ad8da5ef2 代码提交 2026-06-15 17:04:53 +08:00
董宏凯andClaude Opus 4.8 22e9f18d08 chore(chat): 移除 landing 紧凑态底部 V1.2 免责声明
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:55:10 +08:00
董宏凯andClaude Opus 4.8 b6c9265e31 feat(light-app): 轻应用中心列表/管理页深蓝主题适配
- 新增 light-app-pages.css / light-app-management.css,引入《深蓝色 UI 规范》
  tokens,.dark 下覆盖页面底图、页头文案、主按钮、表格、下拉弹层等
- 列表/管理页根节点加 className 钩子与 dark: 背景类,浅色样式不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:55:06 +08:00
董宏凯andClaude Opus 4.8 ee6ab38c37 feat(nav): 浅色模式导航重构(顶栏一级 + 侧边栏二级图标卡 + 三级右侧列)
浅色(红/蓝)主题改为与深色一致的导航逻辑,颜色经 --brand-* 变量自动切换:
- 一级导航正常横排进顶栏(白字落在 banner 上,选中加粗 + 底部粗白线)
- 二级导航为侧边栏图标卡片(图标主题色、文字 #222222、选中铺满列宽 + 右侧粗边)
- 三级导航改到侧边栏二级右侧的「三级列」:激活二级项有三级时整列展开、
  侧边栏 96→236px 整体变宽并带过渡动画,主内容左内边距同步平滑右移
- 顶栏三级横条改为仅深色用;浅色不支持侧边栏收起
- 任务管理二级项改用区分图标(ClipboardList/Activity/Gauge/Radar)
- 字体统一用全局 --font-sans,修复雅黑兜底下加粗不生效
- 抽出 getActiveThirdLevelItems / useActiveThirdLevel 供侧边栏与 PageRoutes 共用

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:53:21 +08:00
董宏凯andClaude Opus 4.8 fbd237531e fix(roundtable-share): 修复分享拿不到总控结论/各席位交付 + 分享总结报告与大屏多页
排查发现后台作业把结果写进 step2.runs[activeRun],而顶层 step2 残留前台 seed
的陈旧气泡(无交付、无结论)。_result_sections 旧逻辑「顶层为空才回退 run」因顶层
非空而永不回退,导致分享页 consensus 与 seat_deliveries 为空。改为优先读活动 run
(与前端 hydrate 一致),已有分享链接无需重建即生效。

同时把第三步另外两个产物纳入分享:
- 方案总结报告(step3.summaryReport.md,Markdown 内联渲染)
- 大屏多页(step3.dashboardJson,用应用内 React 大屏 DashboardApp 渲染,新增
  /roundtable-share/:code/dashboard 路由,与应用渲染/新窗口打开同源)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:57:33 +08:00
董宏凯andClaude Opus 4.8 49101ef0e8 feat(roundtable): 第二/三步沙箱 md 文件支持用户编辑
复用主聊天页同款 ArtifactFileDetail 编辑能力:新增 allowVirtualMarkdownEdit
开关,圆桌沙箱里 write-file 虚拟 md 在流式结束后切回真实 artifacts API
(可加载 + PATCH 保存),由 Step2SandboxLayout 与 HighFidelityReport 启用。
主聊天页等其它调用方默认关闭、行为不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:57:18 +08:00
董宏凯 8751ee7361 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-15 11:39:24 +08:00
董宏凯andClaude Haiku 4.5 01012ff300 feat(settings): 轻应用登录参数与界面模式管理员默认
- 登录时从路由参数 yUserId/access_token 获取并存入 userInfo
- 轻应用管理页路由参数新增 userName/gateDepartName/typeName 选项
- 添加系统默认界面模式设置(管理员可指定全量/简洁)
- useLayoutMode hook 支持优先级:用户自选 > 管理员默认 > 会话级

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-15 11:39:02 +08:00
fengxiangdebaoziandClaude Opus 4.8 2a2bb4d28e fix(sandbox): 修复 Markdown 编辑保存后内容被重置回原值
useArtifactContent 查询键含 publicShareCode(5 元素),而 onSuccess
的 setQueryData 仅用 4 元素,精确哈希不匹配导致写入幽灵缓存、真实缓存
仍为旧值;保存后 useEffect 据旧 content 重置 draftContent。改用
setQueriesData 前缀匹配命中真实缓存。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:37:24 +08:00
fengxiangdebaozi ce04f14f58 Merge branch 'origin/dhk-1' into dhk-1
Resolve CLAUDE.md conflict by keeping remote version.
2026-06-15 11:13:42 +08:00
fengxiangdebaozi 16b36e2b9f 代码修改 2026-06-15 11:02:33 +08:00
董宏凯 c597941128 Merge branch 'dhk-1' into 'master'
fix(roundtable): 查看编排流程图按钮直接进入基本信息步骤

See merge request knowledge-base-bj/deerflow-server!120
2026-06-14 14:29:41 +00:00
董宏凯andCursor ab22b50faf fix(roundtable): 查看编排流程图按钮直接进入基本信息步骤
移除弹窗预览,改为跳转到第二步以便同时编辑基本信息并查看流程图。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 19:37:22 +08:00
董宏凯 7785091bae Merge branch 'dhk-1' into 'master'
feat(圆桌): 业务链条配置页流程图点阵放大 + 侧边栏菜单项按设计稿还原

See merge request knowledge-base-bj/deerflow-server!119
2026-06-14 11:31:50 +00:00
董宏凯 56449ed129 feat(ui): theme & layout improvements
- Dark-blue theme background assets and styling integration
- Appearance settings with layoutMode (full/compact) preference
- Settings dialog and auth context updates
- AI writing form and panel refinements
- Open Canvas context updates
- Roundtable planning styling and dashboard enhancements
- Strategy components and header updates
- Light app management and routing improvements
- Global style enhancements for dark theme
- Various page layout and workspace routing optimizations
2026-06-14 19:30:54 +08:00
董宏凯 da2864f422 fix(chat): align compactChrome logic with layoutMode preference for dark-blue bg
ChatPage's compactChrome was still using readSidebarEnabled() alone, but
PageRoutes now honors the explicit layoutMode setting (full/compact). When a
user sets layout to 'full' without portal login, PageRoutes shows chrome but
ChatPage didn't apply workspace-chat-bg, hiding the dark-blue theme background.

Now ChatPage mirrors PageRoutes' logic: prioritize layoutMode, fallback to
readSidebarEnabled() if unset.
2026-06-14 19:30:04 +08:00
董宏凯andCursor c702119e66 feat(ui): 浅蓝主题、外观设置与圆桌看板主题适配,优化工作区布局
新增 lightblue 主题与外观设置联动,圆桌 Step3 看板/report 统一配色;工作区侧边栏、输入框与轻应用 iframe 主题参数同步更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 12:43:16 +08:00
董宏凯 30d4f9667b 样式修改 2026-06-13 20:43:18 +08:00
董宏凯andClaude Opus 4.8 cd3a71baab feat(agents): 内置智能体筛选(仅管理员)+ 内置 agent 默认用途标签
- 后端 scope=builtin(user_id IS NULL)加入 sql/memory 的 scope 过滤;
  /api/agents 扩展 scope 正则并加 admin 闸门(非管理员请求 403)
- 新增 _builtin_agent_tag_seed:启动时幂等给功能型内置 agent 按用途打标签
  (roundtable-* → 多智能体规划,ai-writing-* → AI写作),只追加不覆盖;
  无用途的内置 agent 不打标签
- 前端 agent-gallery 新增「内置智能体」Tab(仅 system_role==admin 可见),
  scope 类型在 api.ts/hooks.ts 同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:42:26 +08:00
董宏凯andClaude Opus 4.8 391409b6c0 feat(圆桌): 查看运行中作业改为接管回前端续跑 + 后台挂起续跑方案文档
- onView 运行中/待澄清作业改走 takeOverRunningJob:cancelJob 停后端 →
  handleLoadDraft 恢复作业真实线程/协调名/对话 → 解除只读闸门 →
  resumeLiveTakeover 在作业线程上由前端继续实时流式编排
- hydrateFromDraft 同步写 threadIdsRef/coordinatorNameRef,使接管同帧可读真实线程
- 新增 docs/multi-agent-后台挂起续跑实现方案.md(仅方案,待实现)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:49:19 +08:00
董宏凯andClaude Opus 4.8 25dbeb7a9a feat(圆桌): 业务链条配置页流程图点阵放大 + 侧边栏菜单项按设计稿还原
- ChainFlowDiagram 点阵背景点尺寸/不透明度上调,更接近设计稿颗粒感
- 全局侧边栏菜单项:锐利方角(2px)、选中整高左竖条、图标 hover 微放大、间距微调;保留原菜单内容与红/青配色

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:41:50 +08:00
董宏凯 12360c6f3f Merge branch 'dhk-1' into 'master'
轻应用主题参数:随当前主题色映射传值

See merge request knowledge-base-bj/deerflow-server!118
2026-06-13 10:01:30 +00:00
董宏凯andClaude Opus 4.8 61754acb87 feat(圆桌): 后台挂起按钮移至顶部暂停按钮右侧 + 实时研讨复位历史闸门
- Step2 顶部「暂停/恢复会商」右侧新增「后台挂起」按钮(查看历史时隐藏),移除底部干预栏里的旧入口
- handleConfirmAgents 从第一步开启实时研讨时复位 loadedFromDraftRef,修复看过历史后再开新讨论按钮不出现的问题
- 大屏 dashboard 多组件样式与业务链条编辑页细节调整

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:37:14 +08:00
董宏凯andClaude Opus 4.8 634cb1413b feat(圆桌): 业务链条配置页按设计稿还原
统计卡 + 筛选工具栏(我的/公共/搜索/排序/网格列表) + 富链条卡(流程脉络图),
数据来自 /api/roundtable-chains 按用户隔离;品牌主色 #CF1322,深蓝模式按规范取色。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:32:40 +08:00
董宏凯andClaude Opus 4.8 2fa40c5e96 feat(圆桌): 大屏多页席位多样式+新窗口React页+布局优化+切历史记录刷新
- 新窗口打开改为渲染应用内 React 大屏(独立全屏路由 /roundtable-dashboard,
  经 localStorage 传数据,无侧边栏),替代原自包含 HTML blob
- 总览页布局:上半两列(总控共识+竞争力排行),核心风险分布下移到数据研判中心后
- 席位交付/执行路线网格列数自适应(grid.ts):不足4个卡片撑满整行
- 席位专页主体多样式变体(SeatShowcase):框架矩阵/洞察卡阵/双栏对比/清单核对/
  发布声明,按席位序号轮转分配,总结席位固定发布声明,全部数据驱动
- 修复切换历史记录时大屏/报告不刷新:渲染期复位入口闸门+hydrate依赖草稿id,
  hydrateSaved清残留data;数据切换时大屏自动回总览页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:32:32 +08:00
董宏凯 432c12fbac @
feat(圆桌): Step3 大屏多页模板重构 + 独立 dashboard 组件化

- 新增 roundtable-planning/dashboard 模块化大屏(DashboardApp + 各分析卡片
  组件 + data-context + 类型/样式)
- Step3DashboardMode 接入新 dashboard,report-template 大屏 HTML 模板重写
- useStep3Dashboard 配套调整

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:32:49 +08:00
董宏凯 146c8e76f0 @
feat(圆桌): 业务链条显示配置人 + 配置页拆两步并新增精美流程图

- 列表/弹窗卡片显示「配置人」(后端把 owner user_id 解析为 email,
  ChainResponse 增 owner_id/owner_name,store 输出 owner_id)
- 配置编辑器拆为两步:第一步基本信息(描述改多行文本),第二步链条编排
- 第一步有编排内容时,左侧基本信息 + 右侧 ChainFlowDiagram 流程图:
  直角连线、点节点开详情抽屉、连线显示阶段目标(依赖)、滚轮缩放、
  拖动平移、全屏 + 全屏右下角缩略图

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:32:37 +08:00
董宏凯 e9f44c8f0f @
feat(圆桌): 大屏多页浅色模板重写为蓝图工程图风(参考设计图)

- 浅色 palette 改玫瑰红(#E11D48)+slate 中性+白底网格,席位色序对齐设计图
- 卡片四角红色角标 + slate 网格 overlay(蓝图风);KPI 加彩色图标
- 共识竖条摘要 + 要点列表;席位卡进度条 + 风险药丸;席位专页精致化
- 新增折线图 svgLine(趋势),donut 升级镂空中心数字+图例;契约加 line 类型
- 图表保持纯 SVG(零依赖离线自包含,沙箱/新窗口/下载/分享通用,与参考源码一致)
- 深色保持深蓝 token 套同结构

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
2026-06-13 10:51:10 +08:00
董宏凯 f3202f95b6 Merge remote-tracking branch 'origin/dhk-1' into dhk-1 2026-06-13 10:37:16 +08:00
董宏凯 d5f319d9cb @
feat(圆桌): Step3 三产物并行 + 大屏多页模式 + 后台挂起查看恢复流程

后台挂起 Step3:
- engine 总结报告/结果绘制/大屏数据 asyncio.gather 三流并行,故障隔离
- request.state 回归修复(进程内虚拟 request 补 state),恢复后台报告
- ROUNDTABLE_JOB_GATEWAY 默认 inprocess;产物自愈重试(截断/缺失纠偏)
- 大屏后端只产 report-json(dashboardJson),前端按当前主题渲染

结果绘制大屏多页模式(模式二):
- report-template 模板渲染(report-json→大屏HTML),零截断、主题跟随
- 与模式一同款交互(流式+沙箱+可修改);沙箱新窗口打开按钮

最终方案展示:左侧席位栏+总控头像、交付/文件合并、收展动画、内联沙箱

后台挂起查看=恢复实时流程:
- handleLoadDraft 返回 Promise,await hydrate 后订阅 streamJob SSE 跟随
- saveDraftNow 强制存(发起前+查看前各一次)防丢

第二步优化:总控判断串/并行(并行单列可写)、席位上下文+交付模板、
peerDeliveries、失败重试、共识标记、流程图小窗(缩放/拖动/并行批)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
2026-06-13 10:36:46 +08:00
fengxiangdebaoziandClaude Opus 4.8 74324f4585 轻应用主题参数:随当前主题色映射传值
- 主题参数改为按系统主题(red/dark-blue)映射取目标应用对应的值
- 应用管理弹窗用结构化映射编辑器(逐主题色配置目标值,默认同名)
- iframe 嵌套显式传入当前主题色,切主题时 src 重建
- 读取/保存时规范化映射串;解析支持 light/dark 等别名,取不到回退主题色名而非恒为 red

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:30:31 +08:00
吉鑫升andClaude Fable 5 21e53310f8 fix: HTML 预览锚点点击同步 hash,修复多页面技能子页面切换失效
预览 iframe 注入的锚点拦截脚本只 preventDefault + scrollIntoView,
URL hash 从未改变,html-multipage 技能基于 CSS :target 的子页面
切换永远不命中,子页面 section 保持隐藏,点击无效。改为拦截后
同步 location.hash 触发 :target 切页,仅当目标元素已渲染时再平滑滚动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:32 +08:00
董宏凯 a7596001c5 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 21:46:05 +08:00
董宏凯 93243298e1 代码修改 2026-06-12 21:45:55 +08:00
fengxiangdebaozi ca098e821f Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 21:39:48 +08:00
fengxiangdebaozi 00d6bd8595 主题色修改 2026-06-12 21:38:38 +08:00
董宏凯 f0c85c4795 后台挂起功能修改 2026-06-12 21:38:20 +08:00
吉鑫升andClaude Opus 4.8 74113b3e6e feat: 智能体与技能列表显示创建时间字段
- 新增共享 formatDateTime 工具(YYYY-MM-DD HH:mm)
- agent-card 卡片/列表视图在发布者下方显示创建时间
- skill 设置页 metaNodes 显示创建时间

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:20:33 +08:00
董宏凯 cca954d01c Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 20:29:03 +08:00
吉鑫升 d825cc7cfe Merge branch 'ji' 2026-06-12 20:28:27 +08:00
董宏凯 ffca4261d7 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 20:27:39 +08:00
董宏凯 865a0bfaf6 代码修改 2026-06-12 20:27:34 +08:00
吉鑫升andClaude Opus 4.8 595da9f339 fix: 私密广场首次加载默认 page=1,使搜索/标签过滤生效
私密广场标签页传 page: pages.private,初始为 undefined,被 JSON.stringify 丢弃后后端走非分页分支忽略 search/tag_ids。补上 ?? 1 兜底,与主标签页一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:27:29 +08:00
吉鑫升andClaude Opus 4.8 44b4a4beba feat: 注册知识入库接口 + 优化大规模知识图谱力导向布局
- 后端 Gateway 挂载 /api/knowledge-ingest 路由(发布 md 制品)
- 知识图谱力模拟按节点规模缩放斥力/截断距离/冷却速率,避免数百节点时布局长时间不收敛
- 斥力与碰撞改为基于均匀网格的近邻对计算,并增加每帧最大位移限制防止抖动与飞出

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:14:22 +08:00
董宏凯 c66c359432 Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!117
2026-06-12 11:33:18 +00:00
fengxiangdebaozi 1214ea0d91 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 19:30:56 +08:00
fengxiangdebaozi cfa05e01c4 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 19:30:46 +08:00
Administrator 6a71272b79 Merge remote-tracking branch 'origin/master' into ji 2026-06-12 18:48:20 +08:00
Administrator b9b55b4e5b fix: 调整提问导航悬浮索引到右侧居中 2026-06-12 18:48:18 +08:00
董宏凯andCursor 3f2aef2925 Merge remote-tracking branch 'origin/dhk-1' into dhk-1,保留本地应用中心菜单配置。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 18:48:11 +08:00
fengxiangdebaozi 7a8a29639f 代码修改 2026-06-12 18:45:04 +08:00
董宏凯andCursor becfd9b211 Merge branch 'origin/dhk-1' into dhk-1,解决 sidebar-menu 冲突并保留应用中心菜单。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 18:38:53 +08:00
fengxiangdebaozi c6b17d5946 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 18:34:14 +08:00
fengxiangdebaozi e24e058ab2 代码修改 2026-06-12 18:32:58 +08:00
Administrator e74ef4b7dc Merge branch 'ji' into master 2026-06-12 18:28:13 +08:00
董宏凯 c2f6d4fd6f Merge branch 'dhk-1' into 'master'
AI 写作四阶段内置智能体 Phase 1-4 落地

See merge request knowledge-base-bj/deerflow-server!116
2026-06-12 10:20:27 +00:00
Administrator 2862eee33b chore: 提交本地修改 2026-06-12 18:09:07 +08:00
fengxiangdebaozi 3ad920457a Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 17:54:10 +08:00
fengxiangdebaozi 768c6860f5 feat: Header 顶部二三级菜单与 AI 写作表单收起优化 2026-06-12 17:53:52 +08:00
董宏凯andCursor 369efc784b 应用管理页样式对齐参考设计,修复参数明细弹层被裁切问题。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 17:50:56 +08:00
xu hd 522327d807 Merge branch 'xuhd' into 'master'
feat(studio): 上传组件用我的笔记 token + 关闭上传弹窗刷新来源列表

See merge request knowledge-base-bj/deerflow-server!115
2026-06-12 09:38:20 +00:00
xuhd 915233c5b6 Merge remote-tracking branch 'origin/master' into xuhd 2026-06-12 17:24:13 +08:00
吉鑫升andClaude Opus 4.8 229830ee89 feat: 智能体 md 产物一键发布到外部知识入库接口
- 智能体新增「入库模板名」(template_name) 配置,持久化到 config.yaml
  并经 AgentCreate/Update/Response 回读(NewAgentPage / AgentDetailPanel)
- 新增 POST /api/knowledge-ingest/publish:把 md 标题+全文发到 knowes,
  标题字段先「名称」失败再用同一 nodeName 换「标题」重试一次
- md 产物预览右上角新增「发布入库」按钮(仅智能体会话且配置了模板名时显示)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:38:04 +08:00
董宏凯andCursor dd329e7a4f 圆桌第二步支持按席位筛选会商记录,左侧角色卡与消息头像联动。
新增 seat-filter 归一化匹配逻辑,点击角色卡或气泡头像可只看该席位发言,并支持一键显示全部。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 16:36:58 +08:00
g zy da56a53a9c Merge branch 'gzy' into 'master'
Gzy

See merge request knowledge-base-bj/deerflow-server!114
2026-06-12 08:33:39 +00:00
ZyGuo 3f014a6fe1 feat: chats_iframe去掉侧边栏和主题 2026-06-12 16:32:28 +08:00
吉鑫升andClaude Fable 5 d9d5780ea7 feat: 提问导航悬浮索引 + 知识库召回会话级开关 + 智能体技能白名单收紧
- 新增 QuestionNavigator:聊天/智能体页右侧悬浮提问索引,点击滚动定位到对应消息(message-list 为 human 消息生成锚点 id)
- 知识库召回开关从单会话头部按钮迁移到侧边栏,改为会话级内存开关(刷新重置,不再写 localStorage),开启时所有对话默认携带 knowledge_rag_enabled
- 智能体可配置技能收紧为「内置 + 自己的 + 已发布到广场的」(新增 core/skills/utils.ts 的 isAgentSkillCandidate,NewAgentPage 与 agent-detail-panel 共用)
- 智能体详情面板在 artifact 预览展开/新文件生成时自动收起让位
- 启用 html-multipage 技能并更新后端 CLAUDE.md 文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:10:00 +08:00
董宏凯 9002ae157a Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 15:48:53 +08:00
董宏凯 f279613f5d 代码提交 2026-06-12 15:48:48 +08:00
董宏凯andCursor b58e731c19 圆桌会商各步接入流式回答速度指示器,复用主聊天 SpeedPill 与全局开关。
新增 DialogueSpeedMeter 与 useDialogueSpeedMetrics,在 Step1/2/3 气泡底部展示 tok/s 与耗时。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 15:43:58 +08:00
fengxiangdebaozi 9d73f27146 feat: light app center and iframe refactor 2026-06-12 15:40:09 +08:00
吉鑫升andClaude Fable 5 81f2a48c6d feat: 公开嵌入页主题支持外部指定 + 排行榜入口移至左上角
- 公开页(/share、/public、/roundtable-share)新增 ?theme= 与
  deerflow:set-theme postMessage 两种外部主题指定方式,经
  usePublicEmbedTheme 接成 next-themes forcedTheme,不写 localStorage
- 排行榜入口从欢迎语旁移到 workspace-header 左上角应用名旁
- 新增 docs/embed-public-pages-theme.md 嵌入主题使用文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:12:17 +08:00
吉鑫升andClaude Fable 5 57a87d6322 feat: MySQL 索引长度兼容 + 启动自动补列 + 未发布徽标与查询加载态
后端:
- 索引/唯一键改用 mysql_length 前缀索引,适配 MySQL 767 字节限制(error 1071):knowledge_notes、knowledge_entities、knowledge_graph_blacklist、agent_skills、feedback
- engine.py 新增 _ensure_orm_columns_sync:启动时对比 ORM 元数据,自动 ALTER TABLE 补齐缺失的可空/带默认值列及其索引
- 新增 scripts/audit_index_key_lengths.py 审计全部索引键长度
- 更新 backend/CLAUDE.md 迁移说明

前端:
- 智能体卡片/技能列表:所有者与管理员可见未发布徽标
- 智能体广场:服务端查询进行中时灰显旧结果并悬浮加载提示
- 排行榜入口从 Header 移到欢迎语旁
- threads/hooks.ts:抽取 updateCachedThreadLists 统一处理数组与分页两种缓存形态

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:51:50 +08:00
董宏凯 728a4b1170 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 14:10:08 +08:00
董宏凯 9de85591bc 提交 2026-06-12 14:09:12 +08:00
董宏凯andClaude Fable 5 086c280e5d 圆桌第三步流程图模式 + 后台 DAG 任务编排
- 方案总结报告:用户要求结构化输出/流程图时跳过 md 直出 flow-json 流程图,
  报告用文字展示;报告与流程图均可多轮修改、互相切换(detectFlowIntent 识别意图)
- 流程图画布放右侧沙箱位(sidePanel 插槽)不覆盖消息列表,可与 md 预览切换;
  节点随容器宽度自适应缩放并 fitView 居中
- 后台挂起支持真 DAG 编排(分层并行/阶段目标/总控提示词),新增
  orchestration_plan 持久化列 + 迁移;大弹窗流程图按 DAG 分层渲染

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:52:17 +08:00
AdministratorandClaude Opus 4.8 da677f87d5 feat: 公开页背景色支持 iframe 外部配置 + CM助手旁排行榜入口
- 新增 usePublicBackground:公开页(排行榜/分享/会商分享/计划任务)支持
  嵌套方通过 ?bg=/?text= 查询参数或 postMessage 覆盖背景/文字色
- Header 左上角 CM助手 旁新增「排行榜」按钮,新标签打开公开排行榜页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:21:25 +08:00
吉鑫升andClaude Fable 5 28098b3155 feat: 知识库实体筛选、技能白名单收紧、嵌入主题与提问组件优化
- 知识库笔记列表支持按实体精确筛选(路由/服务/仓储层透传,关系图索引查询)
- 自定义智能体技能白名单在 skill_view 与 skill_manage 的 list/view 别名上同样生效;
  config 未声明 skills 时视为无可见技能
- 嵌入圆桌主题解析默认深蓝,强制主题期间不再写 localStorage 覆盖主站选择
- 智能体提问建议拆出 agent-question-chips 组件,编辑器与详情面板联动优化
- 新增 qa-client 问答测试脚本

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:09:32 +08:00
xuhdandClaude Opus 4.8 c268348e29 feat(studio): 上传组件用我的笔记 token + 关闭上传弹窗刷新来源列表
- document-upload-popup: 新增 getNotebookToken(),上传/导入到笔记后端
  (source/import、documents/fileupload)改用「我的笔记」token;
  其余请求由调用方在 init.headers 自带 Authorization,不再注入本系统 token
- query-client-provider: 导出 queryClient 单例,便于非 hook 处读取缓存的 token
- network-actor: consumerUrl 改为读 VITE_AIPI_CONSUMER_URL;修复 source/import
  headers 语法并去掉重复 token
- StudioNotebookDetailPage: DocumentUploadDialogProvider 增加 onClose,
  关闭上传弹窗时刷新来源列表与 source_count

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:14:49 +08:00
fengxiangdebaozi b33426416b Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-12 09:19:47 +08:00
fengxiangdebaozi cf3dd2d225 代码修改 2026-06-12 09:19:38 +08:00
吉鑫升 11bc442f10 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-12 09:19:23 +08:00
ZyGuo 41ce95cbf1 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-12 09:07:23 +08:00
董宏凯 75258db0a2 提交 2026-06-12 00:38:17 +08:00
董宏凯andClaude Fable 5 df8bf895ef 圆桌第三步与业务链条增强:主题跟随/插话暂停/提示词优化/结构化流程图
- 结果绘制跟随日/夜间主题(深色按深蓝 UI 规范令牌绘制),提示词注入真实生成时间
- 绘制支持暂停与随时插话(线程存活时免带原料续线程省 token)、失败自动重试一次、草稿主题不匹配提示条
- 总结报告优先执行用户最终总结要求(结构化输出/重点席位可整体替换默认章节)
- 业务链条总控提示词与各阶段目标加「AI 优化」按钮(新接口 POST /api/roundtable-chains/optimize-prompt)
- 总结报告 flow-json 结构化流程图:流式增量绘制、最新连线蚂蚁线动画、拖拽/缩放/复位/全屏、节点详情弹窗、沙箱切换按钮/无沙箱自动打开
- roundtable-report / roundtable-summary 两个内置智能体 SOUL 更新(双主题、多轮修改允许读底稿、KPI 估算标注、流程数据契约)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:37:45 +08:00
Administrator 1b86373a54 feat: 知识库标签/实体关联跳转 + 会话重连容错
前端:
- Wiki 标签与实体改为可点击,弹出 RelatedNotesDialog 查看关联知识
- 知识库首次进入自动打开最近笔记,避免空白阅读区
- 流式重连识别 "Run not found",清理 sessionStorage 残留 key 并静默回退历史

后端:
- 线程鉴权区分 not found / not accessible,并支持 DB 切换产生的孤儿线程认领
- run 查询/join 在内存未命中时回退持久化存储,重启后返回干净的终止 SSE 而非 404
2026-06-11 23:19:09 +08:00
董宏凯andCursor a4a9a4d26e AI 写作四阶段内置智能体 Phase 1-4 落地
新增 ai-writing-researcher/outliner/writer/editor 种子 agent、_agent_runner 与 use_builtin_agents 灰度开关;四节点可按 flag 切换 SOUL 模式;时间线补大纲规划师配色并更新改造文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 22:58:38 +08:00
董宏凯 1a41ac97d2 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-11 22:09:40 +08:00
董宏凯 55aab67383 需求梳理 2026-06-11 22:09:35 +08:00
fengxiangdebaozi 76e2e67af8 Merge branch 'dhk-1' into master 2026-06-11 21:52:39 +08:00
fengxiangdebaozi b682ff5d08 Step3 总结报告支持随时插话、暂停与附件上传
生成中可打断并续问,输入框支持上传参考文件并随消息注入总结线程。
2026-06-11 21:51:59 +08:00
fengxiangdebaozi 2d74f8883e Step3 总结报告与顶栏主题背景图
新增 roundtable-summary 智能体及前后端 Step3 Markdown 总结流程;顶栏在浅/深主题下分别使用红/深蓝横幅背景。
2026-06-11 21:51:59 +08:00
吉鑫升andClaude Fable 5 e7b51a30a3 fix: 智能体可见性对齐——列表可见即可打开问答,消除 404
get_visible 此前比列表过滤更严格(仅 内置/本人/已发布),导致私密广场
智能体(转私密强制 published=false)和「我的」里已收藏但被取消发布的
智能体能在列表看到、点开问答却 404(前端连带报 X.map is not a function)。

- AgentRepository._visible_filter / MemoryAgentStore._is_fetchable:
  单条读取放宽为所有列表口径的并集(+is_private 豁免、+本人收藏豁免)
- 详情接口与 _enforce_agent_access 运行门禁:管理员回退 get_any,
  对齐 ?admin=true 全量列表
- 普通未发布草稿对陌生人仍不可见,隐私不放开
- 本地新增 tests/test_agent_visibility.py 覆盖 SQL/Memory 双实现
  (tests/ 目录在本仓库被 gitignore,不随提交)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:25:36 +08:00
吉鑫升 0b98d18483 Merge remote-tracking branch 'origin/master' into ji 2026-06-11 20:28:15 +08:00
吉鑫升andClaude Fable 5 3d6f35391e feat: 对话列表显示总数,SOUL 编辑区高度自适应收敛
- 新增 POST /threads/count:与 /threads/search 同一套过滤(标题搜索、
  系统会话排除),仅返回 {total};ThreadMetaStore 增加 count 方法
  (SQL 走 COUNT(*),JSON 过滤分批扫描计数)
- 对话页面搜索框下常驻显示「共 N 条对话」,分页条显示 当前页/总页数
  (useThreadsCount,仅随筛选条件变化,翻页不重复请求)
- 智能体右侧 SOUL 预览改为 40vh 上限自适应展开(不再全部展开);
  详情面板/卡片编辑/创建页的 SOUL 输入框统一为适中高度 + 可拖拽

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:26:44 +08:00
吉鑫升 10047de745 Merge remote-tracking branch 'origin/master' 2026-06-11 20:00:26 +08:00
吉鑫升 82600a3404 Merge branch 'ji' 2026-06-11 19:59:58 +08:00
董宏凯 b73eb77b95 Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!113
2026-06-11 11:59:48 +00:00
董宏凯 1fca24765a 代码修改 2026-06-11 19:59:23 +08:00
吉鑫升andClaude Opus 4.8 f107ebc46d feat: 完善知识图谱交互与 Agent/线程持久化
- 知识图谱画布、侧边栏、详情面板交互优化,新增图谱相关 API 与 hooks
- Agent 卡片与详情面板调整,对话页面重构
- 线程元数据持久化增强(base/memory/sql),knowledge 仓储与导出补充
- 网关路由 agents/knowledge/threads 同步更新

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:58:34 +08:00
吉鑫升 f2ffae8a22 Merge remote-tracking branch 'origin/master' into master 2026-06-11 18:59:15 +08:00
吉鑫升andClaude Opus 4.8 ea78fe9726 fix(checkpointer): use_postgres 回退为 false 恢复本地 SQLite 聊天历史
- 发版 40206dc9 将 checkpointer 切到空的远程 Postgres 导致历史不可见,
  且迁移/dual-read 未生效;翻回 SQLite 恢复历史
- config.yaml(backend): 注释掉暂不使用的模型条目

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:58:28 +08:00
吉鑫升andClaude Opus 4.8 7d61bc09bc feat: 新增表格文件预览,优化知识图谱与 Agent 详情交互
- 新增 spreadsheet-preview 组件及 xlsx 解析工具,支持产物中的表格文件预览
- 优化知识图谱画布、详情面板及样式
- 调整 Agent 详情面板、聊天与消息列表交互

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:43:09 +08:00
董宏凯 b1231fd981 Merge branch 'dhk-1' into 'master'
wujie应用联调

See merge request knowledge-base-bj/deerflow-server!112
2026-06-11 09:41:04 +00:00
董宏凯andCursor bcc3d617a0 Merge branch 'ji' into dhk-1
Resolve column_additions_mysql.sql conflict: keep square_id columns from master and roundtable_drafts.task_id from dhk-1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:40:08 +08:00
fengxiangdebaozi 6890176c76 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-11 17:38:43 +08:00
fengxiangdebaozi d12c50ea4e 样式修改 2026-06-11 17:38:36 +08:00
董宏凯 31525e79a8 套用wujie修改 2026-06-11 17:36:20 +08:00
董宏凯 c48317672c 样式修改 2026-06-11 17:21:24 +08:00
董宏凯 f3ca996116 wujie应用联调 2026-06-11 17:11:28 +08:00
吉鑫升 5daa4b3e20 Merge remote-tracking branch 'origin/master' into ji 2026-06-11 16:39:30 +08:00
吉鑫升 80e84de868 Merge remote-tracking branch 'origin/hlh' into ji 2026-06-11 16:35:18 +08:00
吉鑫升 7efd811e75 feat: update knowledge and agent UI 2026-06-11 16:18:38 +08:00
xu hd 5e68f81a1f Merge branch 'xuhd' into 'master'
chore(env): 添加我的笔记相关环境变量配置

See merge request knowledge-base-bj/deerflow-server!111
2026-06-11 08:05:17 +00:00
吉鑫升andClaude Fable 5 bebe4feccf chore(leaderboard): 用户排行榜当天快照刷新间隔默认调整为半天
ADMIN_LEADERBOARD_TODAY_TTL_SECONDS 默认值 300s -> 43200s,
当天数据约每半天重算一次;调度器 tick、凌晨定稿与手动“刷新当天”不变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:57:27 +08:00
吉鑫升andClaude Opus 4.8 e684de30bd feat(knowledge): 知识库 wiki/知识图谱重构,新增发布广场选择弹窗
- 知识库页拆分出 wiki 组件(侧边栏/目录树/Markdown 阅读器等)
- 知识图谱页拆分出 knowledge-graph 组件并增强交互
- 新增发布广场选择弹窗 publish-square-picker-dialog
- config.yaml 新增 MiniMax-M2.5 / Ling-flash-2.0 / DeepSeek-V4-Pro 模型

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:52:25 +08:00
xuhd bd0d922737 Merge branch 'xuhd' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-06-11 14:20:29 +08:00
xuhd 1668c1da87 Merge remote-tracking branch 'origin/master' into xuhd
# Conflicts:
#	frontend-web/.env.production
#	frontend-web/.env.sit
#	frontend-web/package.json
#	offline-backend-20260512/backend/config.yaml
2026-06-11 14:19:01 +08:00
xuhdandClaude Opus 4.8 95dec0e72e fix(studio): 修复流式返回时空间引用角标渲染错乱
Streamdown 的 parseIncompleteMarkdown 会把流式途中未闭合的 [[cite:N
当作未完成链接改写成 [[cite:N](streamdown:incomplete-link),导致
rehypeCitations 匹配失败、引用闪成坏链接。送入渲染前剥除未完成的引用
标记,待 ]] 到齐再由 rehype 渲染为上标。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:11:21 +08:00
liuq_1233 5a33f66ad6 修改上传来源的文件类型提示 2026-06-11 11:58:52 +08:00
xuhdandClaude Opus 4.8 33276dae76 docs(knowledge): 经济交往时间线 markdown 排版规范化
在加粗事件标题后补空行,统一列表前的空白。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:45:47 +08:00
xuhdandClaude Opus 4.8 b0e48b6bda refactor(studio): 文案“我的笔记/笔记本”统一改为“我的空间”
涉及空间详情页、AI 写作表单/上下文/类型、researcher 注释及 env 注释。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:45:35 +08:00
吉鑫升andClaude Opus 4.8 f0aec4cd7f feat(square): 新增发布广场功能,智能体/技能按广场分类展示
- 后端新增 PublishSquare/PublishSquaresSettings 配置与读写接口
- agents/skills 增加 square_id 字段及对应迁移脚本
- 前端新增发布广场设置页、技能转移选择器,画廊按广场分 Tab 展示
- lead_agent prompt 及若干持久化层适配调整

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:37:42 +08:00
xuhdandClaude Opus 4.8 5ed636bce4 chore(frontend): env 注释中“笔记本”改为“空间”
统一前端各环境 env 文件注释文案,与界面命名保持一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:31:49 +08:00
xuhdandClaude Opus 4.8 fa6191b448 fix(studio): 移除来源卡片悬停 tooltip,失败来源显示报错并可删除
- 去掉来源卡片 hover 时弹出的自定义 tooltip div
- status 为 failed 的来源卡片显示红色报错图标与提示,禁用选中,保留删除按钮

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:30:52 +08:00
董宏凯 93ca273312 Merge branch 'tjl' into 'master'
tjl

See merge request knowledge-base-bj/deerflow-server!110
2026-06-11 03:21:02 +00:00
xuhd 82d14351fc 修改名称 笔记本改为 空间 2026-06-11 11:19:26 +08:00
吉鑫升andClaude Opus 4.8 3845d5090b feat(knowledge): 支持文件导入沉淀知识,知识图谱可视化随主题切换
- 后端新增 /api/knowledge/ingest/file:上传 md/txt/pdf/word/ppt/excel,
  转 Markdown 后走 wiki-capture LLM 蒸馏为陈述式知识笔记,失败回退逐字草稿
- service.capture_file + 文档专用蒸馏提示词,按主题拆分 1~4 页
- 知识图谱导出 HTML 支持 ?theme=light|dark,布局稳定后冻结,
  按度数缩放节点,悬停/点击聚焦邻居
- 前端知识库页新增「导入文件」对话框与 ingestKnowledgeFile API
- 智能体问答页与简介侧边栏 UI 优化;批量人物蒸馏脚本提取正文逻辑修正

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:25:27 +08:00
吉鑫升andClaude Opus 4.8 17bb3715d1 feat(agents): 智能体问答页落地态展示常用问题入口并默认展开简介栏
- 落地页 Hero 以"Hi,我是{名称}"问候,将管理员配置的常用问题渲染为可点击的引导气泡,点击即发起对话
- 右侧智能体简介栏改为默认展开,作为主要"专家信息"卡片,仍可手动收起

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:52:27 +08:00
ZyGuo f28426063f Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-11 09:46:10 +08:00
吉鑫升 6efe1b80c5 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-11 09:34:45 +08:00
吉鑫升andClaude Opus 4.8 01696f75e9 新增批量人物蒸馏脚本,精简排行榜相关接口
- batch-distill: 独立单文件脚本,按时间窗调用线上服务批量蒸馏人物档案
- 精简 admin_users / public_leaderboard 路由及前端排行榜页面

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:34:34 +08:00
AdministratorandClaude Opus 4.8 e97017b297 feat(agents): 智能体常用问题——侧边栏展示并一键提问
新增 common_questions(常用问题)字段:后端贯通 agents_config、
config.yaml 读写与 create/update API,前端 Agent 类型补充字段并在
AgentChatPage 新增 AgentInfoSidebar 组件展示常用问题,点击即填充并提交。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:34:39 +08:00
AdministratorandClaude Opus 4.8 22222308a1 feat(summarization): 非破坏性上下文压缩——保留原始对话仅在请求时瞬态压缩
重写 DeerFlowSummarizationMiddleware:将 stock before_model 的
RemoveMessage(REMOVE_ALL_MESSAGES) 改为 no-op,改在 wrap_model_call 中
瞬态压缩为 [summary, *recent],完整原始对话仍保留在 state 中,前端继续
渲染用户真实问答。summary 消息标记 hide_from_ui 且永不持久化,按 thread
缓存以避免每次模型调用都触发 LLM。前端 isHiddenFromUIMessage 额外隐藏旧
版本持久化的 name="summary" 消息。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:34:28 +08:00
Administrator 7fb3870247 @
feat(summarization): 非破坏性上下文压缩——保留原始对话仅在请求时瞬态压缩

重写 DeerFlowSummarizationMiddleware:将 stock before_model 的
RemoveMessage(REMOVE_ALL_MESSAGES) 改为 no-op,改在 wrap_model_call 中
瞬态压缩为 [summary, *recent],完整原始对话仍保留在 state 中,前端继续
渲染用户真实问答。summary 消息标记 hide_from_ui 且永不持久化,按 thread
缓存以避免每次模型调用都触发 LLM。前端 isHiddenFromUIMessage 额外隐藏旧
版本持久化的 name="summary" 消息。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-11 08:34:09 +08:00
董宏凯 c57696cf80 Merge branch 'dhk-1' into 'master'
接口地址修改

See merge request knowledge-base-bj/deerflow-server!109
2026-06-10 13:57:49 +00:00
董宏凯 0784f30e23 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 21:57:12 +08:00
董宏凯 9af93ecde6 接口地址修改 2026-06-10 21:57:04 +08:00
fengxiangdebaozi d9e3a87911 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 19:39:06 +08:00
fengxiangdebaozi f01789a17e 多智能体会商上传文件 2026-06-10 19:38:58 +08:00
董宏凯 688514eab6 Merge branch 'dhk-1' into 'master'
接口地址修改

See merge request knowledge-base-bj/deerflow-server!108
2026-06-10 11:25:24 +00:00
董宏凯 3d069179d6 接口地址修改 2026-06-10 19:24:58 +08:00
董宏凯 f90a6e7082 Merge branch 'dhk-1' into 'master'
code

See merge request knowledge-base-bj/deerflow-server!107
2026-06-10 11:14:40 +00:00
董宏凯 406c02ad07 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 19:08:50 +08:00
董宏凯 3bc824b462 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 19:08:39 +08:00
董宏凯 c705e0b5ae 样式修改 2026-06-10 19:08:32 +08:00
吉鑫升 3a04ba5726 Merge remote-tracking branch 'origin/master' into ji 2026-06-10 19:01:16 +08:00
董宏凯 60a6db8b47 Merge branch 'dhk-1' into 'master'
feat(roundtable): 第一步识别出的意图支持手动编辑并保存

See merge request knowledge-base-bj/deerflow-server!106
2026-06-10 10:59:29 +00:00
fengxiangdebaozi cf5550ab0a 接口地址修改 2026-06-10 18:58:55 +08:00
董宏凯 19aeba5fab Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 17:44:51 +08:00
董宏凯 d9afd8d887 代码提交 2026-06-10 17:44:44 +08:00
hkdong 7422f083b4 tjl 2026-06-10 17:19:27 +08:00
xuhd 06508cfb7c Merge remote-tracking branch 'origin/master' into xuhd 2026-06-10 17:15:51 +08:00
xuhdandClaude Opus 4.8 980d8ba44f chore(deps): declare mermaid dependency
补上 b0fdeab9 引入 mermaid 图表渲染(roundtable 产物预览)时漏提交的
依赖声明,否则全新 pnpm install 后构建会因找不到 mermaid 失败。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:15:28 +08:00
吉鑫升andClaude Opus 4.8 d658b05ee9 feat(skills): 安装成功后引导填写/AI生成中文名 + 生成超时提至60s
- 上传安装技能成功后,弹窗进入「设置中文名」步骤:用户可手动填写,或用所选大模型自动生成默认值,亦可跳过(回退英文名)
- 技能中文名生成超时由 30s 提高到 60s(思考模式构建时已关闭)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:14:42 +08:00
xuhdandClaude Opus 4.8 123fe36ee4 refactor(studio): rename 笔记本 to 空间 in workspace nav and notebook pages
统一术语:侧边栏「新建笔记本/所有笔记本」、笔记列表与笔记本管理页的
新建按钮与弹窗标题,全部由"笔记本"改为"空间"。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:13:23 +08:00
xuhdandClaude Opus 4.8 4bf4b96fa9 feat(notebook): redesign ai-writing header to space-info-only glass pill
移除品牌"索财笔记"与右侧"这是我的笔记"标签,header 只保留空间信息;
空间徽章重做成玻璃胶囊(渐变图标徽标 + 已连接状态点),高度 44→52px,
顶部加品牌高光线与 hover 微光,延续索财深蓝品牌色阶。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:06:21 +08:00
fengxiangdebaozi b8680275e5 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-10 17:01:25 +08:00
fengxiangdebaozi e03ca7b4bd 代码修改 2026-06-10 17:01:17 +08:00
吉鑫升andClaude Opus 4.8 3178b4c45e fix(auth): CSRF/鉴权中间件兼容反向代理路径前缀 + 无会话请求豁免
清空浏览器缓存后(尤其 token 换登录)报「CSRF token missing」。根因:
反向代理把请求转发到带前缀的路径(如 /xxx/api/v1/auth/login/token),
FastAPI 用 root_path 剥前缀做路由,但 Starlette 中间件跑在路由之前,
看到的仍是带前缀的 path,导致 CSRF 豁免名单与公开路径白名单(精确/前缀
匹配)全部失配。清缓存前靠 localStorage 残留 token 当 Bearer 绕过 CSRF,
清掉后裸奔才暴露。

- 新增 proxy_path.app_relative_path:匹配前按 root_path / X-Forwarded-Prefix
  剥掉代理前缀(无前缀时 no-op)。
- csrf_middleware:should_check_csrf / is_auth_endpoint 改用应用相对路径;
  补上漏收的 /api/v1/auth/login/token 豁免;新增「无 access_token 会话
  cookie 则跳过 CSRF」兜底(无会话请求不存在可被跨站伪造利用的会话)。
- auth_middleware:公开路径判断改用应用相对路径,避免 /xxx/api/public/*
  分享页被误判 401。
- langgraph_auth:无 access_token cookie 时跳过 CSRF,与网关链路一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:51:01 +08:00
吉鑫升andClaude Opus 4.8 413f92e270 fix(migrations): 合并 alembic 多头(skill_name_zh 与 chain-stages 分支)
20260609_01(skills.name_zh)直接挂在 20260608_01 之下,与
20260608_02..06(chain-stages/parallel-agent 分支)形成两个 head,导致
`alembic upgrade head` 报 Multiple head revisions。新增空 merge 版本
20260609_02 同时指向 20260608_06 与 20260609_01,使 head 归一。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:39:37 +08:00
董宏凯 bd426ed1d3 da 2026-06-10 12:51:13 +08:00
董宏凯 ca9960c8bc 代码修改 2026-06-10 12:51:05 +08:00
xuhd 6b9e1f3c2a 2222 2026-06-10 12:37:13 +08:00
xuhd b8d3a549df buggggg 2026-06-10 11:27:48 +08:00
吉鑫升andClaude Opus 4.8 546dec67a1 fix(agents): 私有 tab 未解锁时隐藏搜索栏
私有广场只有在功能可用且校验过密码后才解锁;未解锁时展示门禁/解锁提示并
隐藏关键词/标签搜索栏(此时没有可筛选的列表)。TagSearchBar 按 tab key 重挂载,
切 tab 时内部关键词输入与展开状态随父级筛选一起重置。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:24:52 +08:00
吉鑫升andClaude Opus 4.8 d61aa853f1 feat(admin): 用户排行榜每日快照——手动刷新当天 + 凌晨自动定稿前一天
- 新增 POST /api/admin/users/leaderboard/refresh-today:同步强制重建当天快照
  并返回合并区间;前端 useRefreshTodayLeaderboard + AdminUserLeaderboardPage
  「刷新当天」按钮
- Gateway 调度循环在每日 ADMIN_LEADERBOARD_FINALIZE_HOUR(默认凌晨1点)对前一天
  强制重算一次,避免临近午夜落库的 runs/tool-calls 漏统计;判定逻辑下沉为纯函数
  deerflow.persistence.admin_stats.finalize.should_finalize_previous_day
- 新增 scripts/sql/column_additions_mysql.sql:既有 MySQL 库手动补列的幂等脚本
  (Alembic 自动迁移已禁用,create_all 不改既有表)
- CLAUDE.md 补充排行榜快照、Schema 迁移约定,并补齐技能/知识库选模型的文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:24:21 +08:00
吉鑫升andClaude Opus 4.8 96e0d695c0 feat(skills,knowledge): 中文名补全与知识库沉淀支持可选大模型并明确大模型报错
技能中文名:
- 单个「AI 生成」与「一键补充中文名」均可在请求体 model_name 选择大模型
- 一键补充改为仅管理员开放
- _generate_skill_name_zh 在模型自身失败(构建/超时/API 报错)时抛
  SkillNameZhModelError,与模型返回无效名称区分;单个接口返回
  502「大模型报错,无法补全技能中文名」,批量在 model_error 中带回代表性报错

知识库沉淀:
- 对话保存到知识库 / 批量导入均新增可选 model_name(透传至 capture_thread
  → extract_thread_llm_multi)
- 批量返回 llm_fallback 计数;模型异常时仍按既有逻辑回退规则抽取并落草稿,
  前端明确提示「已改用规则抽取」(保存按钮保持后台异步)

前端新增 components/workspace/model-name-select.tsx 复用下拉(默认=系统配置模型)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:23:01 +08:00
吉鑫升andClaude Opus 4.8 8fbbfc8093 feat(frontend): 广场搜索体验优化与切换tab重置筛选
- 智能体广场:切换 我的/广场/私密广场 时清空重置搜索/标签/排序/发布者筛选
- 通用搜索栏(TagSearchBar):新增「搜索」按钮与回车立即触发(跳过防抖)、
  搜索中转圈提示,新增 hideTags / searchPlaceholder 选项
- 定时任务广场:新增客户端搜索(名称/摘要/创建者,兼容脱敏文本)
- agents 分页 hook 暴露 isFetching 以驱动搜索中状态

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:37:39 +08:00
ZyGuo 853a349265 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-10 09:19:37 +08:00
ZyGuo 621d4c82af chore: apik 2026-06-10 09:18:38 +08:00
Administrator 87e0ae683e Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/config.yaml
#	offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py
#	offline-backend-20260512/backend/packages/harness/deerflow/runtime/checkpointer/async_provider.py
2026-06-10 09:09:26 +08:00
AdministratorandClaude Opus 4.8 44c0f6189b wip: 合并 master 前暂存本地改动
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:36:47 +08:00
董宏凯 64afa9a1ed 代码提交 2026-06-09 22:52:12 +08:00
AdministratorandClaude Opus 4.8 1ce15fbb15 style(knowledge): 实体改回纯展示(不可点击),chip 精致化
按需求去掉点击筛选,实体为静态彩色 chip:色点 + 名称 + 胶囊型类型标签。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:34:59 +08:00
董宏凯 ace30af202 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-09 20:08:06 +08:00
董宏凯 559340143e 代码提交 2026-06-09 19:51:56 +08:00
董宏凯 ae29bb4ca8 Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!105
2026-06-09 11:48:57 +00:00
AdministratorandClaude Opus 4.8 f6af9379b7 feat(knowledge): 笔记页实体可点击筛选 + chip 美化
- 实体改为按钮:点击即在左侧列表筛出提及该实体的知识(清空来源/状态筛选)
- chip 加阴影/hover 上浮/类型 ring 标签/实体区 Sparkles 图标,更精致

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:48:20 +08:00
董宏凯 57e286b8ad Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-09 19:45:51 +08:00
fengxiangdebaozi 7acedfd385 代码修改 2026-06-09 19:41:48 +08:00
董宏凯 946799d19d Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-09 19:41:08 +08:00
董宏凯 7282a32859 提交代码 2026-06-09 19:41:01 +08:00
董宏凯 5080f1dc39 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!104
2026-06-09 09:19:14 +00:00
fengxiangdebaozi cba165933c 提交代码 2026-06-09 17:18:40 +08:00
xuhdandClaude Opus 4.7 e54f42c0eb fix(skill_manage): refuse agent writes to human-authored skills
Even after the previous owner check, the agent could still patch/edit
a UI-uploaded skill (source=upload) it happened to own. Uploaded skills
represent human-reviewed content; letting the agent rewrite them risks
silent drift from what the user approved.

Add _assert_skill_is_agent_authored which reads usage.json and refuses
unless source==agent. Wired into all five write actions
(create/edit/patch/delete/write_file/remove_file). Skills with no usage
entry default to source=upload, so legacy and adopted-orphan skills are
also refused — only skills the agent created via mark_agent_created in
the create flow stay editable.

Note: this check is independent of skill_store availability (it reads
the usage file, not the DB), so it runs even when skill_store is None.
Tests that exercise non-create paths against pre-existing skills will
need to call mark_agent_created first or mock get_entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 16:00:44 +08:00
xuhdandClaude Opus 4.7 c493639119 feat(skill-review): strict-mode prompt + raise min_tool_calls 3 to 5
The review agent was creating too many low-value skills. Three changes
tighten the funnel:

- review_worker prompt: rewritten in "保守模式". Default decision is
  do-nothing. To create, all five AND conditions must hold (>=5 tool
  calls with step dependencies, an explicit fail->fix->success episode,
  cross-scenario reusability, no >=70% overlap with skill_list, can
  produce >=300 chars of substance). Adds 7 explicit NO categories
  (single-tool loops, one-off queries, LLM common knowledge, etc.)
  and a 4-question self-check before any create. Hard cap of 1 new
  skill per review, with patch preferred over create.
- min_tool_calls: 3 -> 5. Matches the prompt's ">=5 steps" hard
  condition so trivial turns never reach the LLM reviewer.
- Updated config.yaml, the default-value assertion in
  test_skill_review_worker, and the middleware docstring's "(default
  3)" annotation to keep all three references in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 16:00:29 +08:00
董宏凯andClaude Opus 4.8 cc5ada1f3d feat(roundtable): 第一步识别出的意图支持手动编辑并保存
「当前意图摘要」卡新增编辑态:核心目标(必填)、约束、关键假设(可增删列表),
保存后由 useStep1Intent.updateIntentReady 覆盖权威 intentReady,下游(Step 2
种子消息 buildInitialIntent、侧栏摘要卡、step2Locked)随之生效,并落草稿持久化。

持久化用「计数 + effect」触发 handleManualSave,避免 enqueueSave 闭包在 setState
flush 前读到旧意图的竞态(与 step2/step3 计数触发器同构)。聊天气泡内的绿色意图卡
保持为历史记录,右侧「当前意图摘要」为可编辑的当前值。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:58:24 +08:00
xuhdandClaude Opus 4.7 539a8bcec8 fix(skill_manage): block cross-user writes on patch/edit/write_file/remove_file
Only delete had a DB owner check via _assert_agent_may_delete. The other
four write actions only called ensure_custom_skill_is_editable, which
just verifies the directory exists under skills/custom/ — it does not
check owner_user_id. Any agent that knew a skill's name could overwrite
or strip support files from another user's UI-uploaded skill.

Generalise the helper to _assert_agent_may_modify (same three rules:
no DB row / built-in owner=None / owner mismatch) and call it from
edit, patch, delete, write_file, and remove_file. Hoist user_id +
skill_store to the top of the lock block so the four newly-guarded
branches stay one-liners and create/delete reuse the same handles.
Behaviour preserved when skill_store is None (no DB) — matches the
existing delete fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 15:49:26 +08:00
xuhdandClaude Opus 4.7 4416348c51 fix(studio): hide sidebar group until token resolves; remove VITE_STUDIO_ENABLED
- workspace-nav-studio: return null while isLoading so the "工作室" group
  no longer briefly mounts and then unmounts when getTokenByEmail returns
  "用户不存在" (404). Skeleton branch became dead code, removed.
- studio/hooks: switch all three token queries from retry: 1 to false —
  account-not-found is a permanent error; retry just doubled isError latency.
- env: drop VITE_STUDIO_ENABLED everywhere (env.ts + 7 env files +
  workspace-nav-chat-list). Sidebar availability is now decided at runtime
  by token success instead of a build-time flag.
- env: add VITE_AIPI_PRESENT_URL + VITE_AIPI_KNOWES_URL for the document
  upload popup's A-system jump + B-system knowledge iframe; reuse the
  existing VITE_AIPI_KNOWLEDGE_URL for the Y-system iframe.
- document-upload-popup: replace 8 hardcoded URLs (6 present, 1 knowes,
  1 knowlibrary) with the corresponding env vars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 15:48:24 +08:00
xuhd bd0eceda61 Merge remote-tracking branch 'origin/master' into xuhd
# Conflicts:
#	offline-backend-20260512/backend/config.yaml
2026-06-09 15:43:40 +08:00
fengxiangdebaozi dddd3c1670 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-09 14:06:18 +08:00
fengxiangdebaozi 10795d55e6 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-09 14:06:01 +08:00
fengxiangdebaozi 230fab013a 代码合并 2026-06-09 14:05:51 +08:00
董宏凯 2082d20b88 Merge branch 'dhk-1' into 'master'
feat(roundtable): 业务链条流程图改弹窗 + 每阶段「本阶段目标」注入总控

See merge request knowledge-base-bj/deerflow-server!103
2026-06-09 05:43:00 +00:00
董宏凯andClaude Opus 4.8 c230f7a6c6 feat(parallel-agents): 子智能体禁派活/脚本(防模仿总控) + 面板能力完善
后端 parallel_agents.py:每个并行席位 excluded_tools 增加 agent_orchestration
(派活) 与 bash(脚本逃逸),并在任务消息里加「角色边界」护栏——根治弱模型拿到
带「总控/业务链条」语气的任务后模仿总控输出「总控调度指令…」、绕道 glob/bash
翻找派活脚本的问题。新增 /api/parallel-agents/auth/token 取 token 端点
(auth/csrf 豁免)。

parallel-agents-vue 面板:协作澄清卡片、步骤图标/步骤展示、API 与类型完善等。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:41:39 +08:00
董宏凯andClaude Opus 4.8 3405795371 fix(roundtable): 业务链条「本阶段目标」串行链可保存/回显 + 新增「总控提示词」
本阶段目标 bug:每步只有一个智能体(无并行)时,stageGroupsToStagePayload/
stageGroupsToStageGoals 一律折叠为 null(旧设计=线性链),导致目标保存即丢、
无从回显。改为「有并行 或 任一 stage 填了目标」就落显式分层,串行带目标链按
分层 DAG 跑(总控逐阶段按目标派活);BusinessChainPicker 同步:有并行/有目标/
有总控提示都走 dag。

新增「总控提示词」(per-chain,仅 dag 模式生效):写给总控的整体派活指引。
后端 roundtable_chains 加 coordinator_prompt LONGTEXT 列 + migration 20260608_06;
前端编辑器加输入框,注入 buildDagStageLeaderTask 每阶段派活提示。填了总控提示
也强制显式分层(forceExplicit),否则纯线性链里被静默忽略。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:41:20 +08:00
AdministratorandClaude Opus 4.8 b70ed38d51 feat(knowledge): 图谱跟随删除 + 笔记页展示实体 + 标签美化
图谱不随删除变化的根因:实体/关系删除时从不清理,build_graph 又把所有实体
都当节点,导致已删笔记的实体(六福珠宝/COMEX/CM助手…)永久残留。

- build_graph 只保留被「活笔记」提及的实体,孤儿实体节点不再出现(render 层兜底,
  历史脏数据无需迁移即生效)
- delete_note 删除时 clear_note_relations + prune_orphan_entities,DB 同步收敛
- get_note 返回 entities(repo.list_note_entities),笔记详情页展示实体
- 前端:实体按类型彩色 chip 展示(人物/机构/概念/产品/技术/地区…),标签改为 pill 样式
- 测试:get_note 返回实体 + 删除后图谱与实体表同步收敛

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:39:25 +08:00
AdministratorandClaude Opus 4.8 70bb2783b9 chore(knowledge): 每次沉淀必打入口/结果日志,便于确认是否触发
success 路径原先完全静默,导致"看不到日志"无法区分"没触发"还是"日志不可见"。
现在每次 capture_thread 至少打 [knowledge] capturing… + 结果/跳过/失败 各一行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:11:54 +08:00
AdministratorandClaude Opus 4.8 d9fc7c9fdf chore(knowledge): 沉淀决策日志统一加 [knowledge] 前缀,跳过提到 WARNING 级
便于 docker logs 一条 grep 抓全部捕获决策(跳过/回退/失败):
  docker logs deerflow-vite-gateway 2>&1 | grep "\[knowledge\]"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:55:44 +08:00
董宏凯 1938c80061 代码提交 2026-06-08 22:52:18 +08:00
AdministratorandClaude Opus 4.8 76320ca5a3 fix(knowledge): 模型判定 should_ingest=false 时直接跳过,不再落草稿
日志显示寒暄/无效会话并非 LLM 调用失败,而是模型正确返回 should_ingest=false。
上一版把这种情况也当失败回退、堆成草稿,反而把无价值对话存进了库。

- llm_extractor: 模型显式 should_ingest=false 且无 notes → 抛 LLMRejectedIngest
- capture_thread: 捕获后抛 EmptyThreadError → 整条跳过、不入库(寒暄/总结此页这类不再进库)
- 仅当真正失败(API 报错 / 非 JSON)才回退规则抽取并落草稿
- 兼容包装 extract_thread_llm 吞掉该异常返回 None
- 更新测试(should_ingest=false 跳过)+ CLAUDE.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:49:00 +08:00
AdministratorandClaude Opus 4.8 4aef81284a feat(knowledge): 出处标记渲染 + 相关笔记按标签精确匹配 + LLM 回退落草稿
A) 前端把 ^[inferred]/^[ambiguous] 渲染成「推测」「存疑」小角标(原先原样显示文本)
B) 相关笔记只按"区分性标签"精确匹配(去掉标题/摘要关键词检索——中文分词导致
   助手/知识图谱/页面 等泛词误关联,如「AI助手打招呼」被错误拉进 Related);
   无区分性标签则不关联、不渲染 ## Related 段
C) LLM 提炼启用但失败时不再静默回退成"原文堆砌":
   - 回退笔记落为 draft(草稿)而非 approved,便于复核
   - capture_thread 记 WARNING 并返回 llm_fallback;router 透出该标志
   - llm_extractor 在 JSON 解析失败/无 notes 时补充可诊断的 WARNING 日志

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:22:28 +08:00
AdministratorandClaude Opus 4.8 e7ff07debd feat(knowledge): 项目名改 zncm + 去除元链接噪音 + 创建人显示人名
- project_name 默认 deerflow → zncm(config.yaml + 配置默认 + 模块内字面量)
- related_links() 返回空:笔记 Related 不再注入 index/_meta/taxonomy/hub 结构链接,
  只保留真实相关笔记的可点 [[标题]];无相关笔记且无来源时不再渲染 ## Related 段
- 移除随之多余的项目 hub 页脚手架(index.md/_meta/taxonomy.md 仍由 obsidian 脚手架保留,可解析)
- GET /notes/{id} 用 auth provider 把 created_by/updated_by(UUID) 解析为邮箱
  (created_by_name/updated_by_name),前端 footer 显示人名、解析不到才显示短 id
- 仅对新沉淀笔记生效,现有笔记不动

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:50:56 +08:00
AdministratorandClaude Opus 4.8 df3fac1032 feat(knowledge): 可跳转 wikilink + 真实相关 note + 一对话多页
- [[wikilink]] 解析器:note id / vault_path / 标题 / vault 元页 / missing
- 前端 MarkdownRenderer 把非数字 [[...]] 渲染成可点链接,元页弹窗查看,缺失置灰
- vault 脚手架补建项目 hub 页,三个结构性链接(index/taxonomy/hub)始终可跳
- capture 时按标题+摘要+区分性标签检索同主题 note,注入真实 [[标题]] 到 Related + 记 related 图谱边
- LLM 提取支持 notes[] 数组,一次对话拆 1~4 页并按标题互链(multi_note_enabled 开关)
- 新增接口 POST /resolve、GET /notes/{id}/wikilinks、GET /vault-page
- 新增配置 related_notes_enabled / related_notes_limit / multi_note_enabled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:20:31 +08:00
吉鑫升andClaude Opus 4.8 92a46003f8 feat: 知识库一键后台保存 + 对话置顶仅首页 + 排行榜页面增强
- 知识库「保存」改为一键触发,无需填表;后端经 BackgroundTasks 异步沉淀到 wiki,点击立即返回 {queued:true}
- 对话列表置顶分区仅在第一页展示,翻页后不再重复顶置
- 公开/管理员排行榜页面重构与维度增强;排行榜页改为整页统一滚动

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:59:55 +08:00
Administrator 24db448cfd Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-08 19:12:34 +08:00
hlh 9e14c2e289 禁用了知识库技能,添加了知识库memory 2026-06-08 18:35:05 +08:00
AdministratorandClaude Opus 4.8 e750df3c0d docs: 知识库阶段二至四(RAG/治理/图谱)更新 CLAUDE.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:33:08 +08:00
AdministratorandClaude Opus 4.8 81e4fd92ce feat(knowledge): 阶段三 知识治理 + 阶段四 图谱增强
阶段三(治理):
- sensitive.py:写入前脱敏 token/密钥/手机号/身份证/邮箱(sensitive_redaction_enabled)
- 版本快照:更新前写 knowledge_note_versions;GET /notes/{id}/versions
- dedup.py:标题/摘要 Jaccard + 内容哈希查重;GET /duplicates、POST /merge(来源并入主条目、重复项归档)
- auto_ingest.py:独立后台队列/线程沉淀,不阻塞问答;中间件 after_agent 在 auto_ingest_enabled 时入队;价值判断(最小字符)+ skip_existing 去重;自动沉淀落 draft 待审

阶段四(图谱):
- LLM 抽取 entities/relations;service 存 knowledge_entities/knowledge_relations 并连边
- exporter.build_graph 支持实体节点 + note→entity/entity→entity 边;graph.html/json 增强
- GET /api/knowledge/graph 返回产品图谱数据

前端:知识库页「查重合并」对话框 + 相关 api/hooks
配置:sensitive_redaction_enabled / dedup_similarity_threshold / entity_extraction_enabled
测试:tests/test_knowledge.py 20 passed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:31:33 +08:00
AdministratorandClaude Opus 4.8 683cc6d76e feat(knowledge): 阶段二 向量 RAG 与召回注入
- embeddings.py:OpenAI 兼容 embedding 客户端,未配置时关键词兜底
- chunking.py + knowledge_embeddings 表:Markdown 分块与向量索引(Python 余弦)
- search.py:keyword/vector/hybrid 三种模式,向量不可用自动降级
- 笔记创建/更新自动重建向量索引;新增 /reindex 维护接口
- KnowledgeRagMiddleware:按对话级开关 knowledge_rag_enabled 注入 <knowledge-context>,接入 lead agent 中间件链(默认随 config.knowledge.rag_enabled)
- service.build_rag_context(wiki-query 风格分层召回)
- 笔记版本快照(knowledge_note_versions)+ GET /notes/{id}/versions
- 前端:聊天页「知识库」对话级 RAG 开关 + 类型字段
- 新增 entities/relations 表(阶段四预置)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:18:04 +08:00
吉鑫升 fb692a0555 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-08 18:02:14 +08:00
吉鑫升andClaude Opus 4.8 40206dc94d fix: Postgres store 自愈连接池 + 启用 PG checkpointer + 分析查询超时按服务端适配
- async_provider: AsyncPostgresStore 改用连接池(check_connection 校验),
  避免单条长连接在服务重启/空闲超时后永久失效;池大小可经
  DEER_FLOW_PG_POOL_MIN/MAX 环境变量调整
- admin_users: 排行榜分析查询的每语句超时按服务端区分
  (MySQL>=5.7.4 max_execution_time / MariaDB max_statement_time /
  MySQL<=5.6 跳过 / PostgreSQL statement_timeout)
- config.yaml: checkpointer.use_postgres 置为 true

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:01:55 +08:00
吉鑫升andClaude Opus 4.8 50d70a5050 feat: 对话列表支持分页、收藏、置顶
收藏(is_favorite)/置顶(is_pinned)作为布尔标记存入对话已有 metadata,
复用现有 PATCH /api/threads/{id} 与 search 往返,无需后端改动与迁移。

- hooks: 新增 useUpdateThreadMetadata + useToggleThreadPin/Favorite
- utils: 新增 isThreadPinned/isThreadFavorite/sortThreadsPinnedFirst
- 侧边栏: 置顶优先排序 + 下拉菜单收藏/置顶项 + 置顶图标
- 对话页: 置顶区 + 全部/收藏过滤 + 页码翻页(每页20) + 行内收藏/置顶按钮
- i18n: zh-CN/en-US/types 新增相关文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:01:03 +08:00
AdministratorandClaude Opus 4.8 8463289f75 feat(knowledge): LLM 提炼陈述式知识(wiki-capture 规范),规则抽取兜底
之前沉淀直接堆砌对话原文。现默认用大模型按 wiki-capture 规范提炼为 Context/Finding/Reasoning/Implications/Related 结构化陈述式知识,过滤寒暄与中间推理;LLM 不可用或解析失败时回退规则抽取。新增 llm_extractor.py + 配置 llm_extract_enabled/extract_model_name/extract_max_input_chars。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:00:46 +08:00
董宏凯andClaude Opus 4.8 b101ee7fc9 feat(roundtable): 业务链条左栏卡片拖动手柄 + 加入按钮 + 智能体详情抽屉
- 左栏候选卡片加拖动手柄(与右栏席位一致),仅点手柄才拖动
- 加入改为显式「加入」按钮,点卡片本体改为打开详情抽屉
- 右栏席位操作区加「查看详情」按钮,打开同一抽屉
- 新增 AgentDetailSheet:按需拉取(useAgent)展示名称/模型/描述/
  标签/技能+工具组/人设,对齐智能体详情页内容

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:00:42 +08:00
AdministratorandClaude Opus 4.8 d06c8d49d7 feat: 工作台左侧导航新增「知识库」入口
之前只加在了 strategy shell 的 sidebar-menu,工作台 shell 用的是 WorkspaceNavChatList,这里补上入口指向 /page/workspace/knowledge。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:55:11 +08:00
AdministratorandClaude Opus 4.8 e193c2df21 feat: 接入知识库聊天入口与前端路由
在聊天页工具栏接入「保存到知识库」按钮,并补回 /page/workspace/knowledge 列表/详情/图谱路由(此前被回退丢失)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:43:55 +08:00
AdministratorandClaude Opus 4.8 cc99ebba81 feat: 产品内知识库(obsidian-wiki 兼容 vault)
后端:新增自包含 deerflow.knowledge 模块(models/repository/schemas/
extractor/sources/markdown_vault/manifest/obsidian_adapter/exporter/
search/service),通过 deps.py 装配为 app.state.knowledge_service,
挂载 /api/knowledge 路由;config.yaml 增加 knowledge 配置段,
AppConfig 注册 KnowledgeConfig,ORM 注册 4 张 knowledge_* 表。

前端:新增知识库页面(KnowledgeBasePage/KnowledgeGraphPage)、
core/knowledge api/hooks/types、聊天侧 knowledge-save-trigger,
侧边栏增加「知识库」入口。

知识库为全局共享,不做 user_id 隔离,写入记录 created_by/updated_by
作为审计。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:38:28 +08:00
吉鑫升andClaude Opus 4.8 80d2db6315 feat: 公开用户排行榜页面 + 修复 checkpointer 连接与前端依赖
- 新增公开排行榜页面与后端 public_leaderboard 路由
- 完善 skills/agents 路由
- checkpointer Postgres 改用连接池(自愈), 解决连接关闭后定时任务连环失败
- 修复前端 mermaid 依赖未声明导致 Vite 解析失败 (新增 pnpm-lock)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:09:28 +08:00
董宏凯 c05e7df48e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-08 16:31:00 +08:00
董宏凯andClaude Opus 4.8 834df64650 feat(roundtable): 业务链条流程图改弹窗 + 每阶段「本阶段目标」注入总控
- 业务链条配置页流程图从底部内嵌改为按钮 + Dialog 弹窗查看
- 每个 Stage 新增可选「本阶段目标」文本框,告诉总控本阶段要产出
  什么、如何依赖上一阶段产出(仅分层 DAG 生效)
- StageGroups 重构为 StageGroup[]({seats, goal?}),goal 随 stage
  在增删改/重排中自动跟随;新增 stageGoals 持久化(与 stages 同下标对齐)
- 运行时注入:buildDagStageLeaderTask(总控派活,主)+ pickDagSeatTask
  (席位任务,兜底)
- 后端 roundtable_chains 加 stage_goals 列 + 校验 + Alembic 20260608_04
- 老链条/草稿无该字段时不注入,行为不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:10:46 +08:00
吉鑫升 8113b822c5 docs: add product knowledge base plan 2026-06-08 16:02:43 +08:00
AdministratorandClaude Opus 4.8 a53e565e5a feat: 管理菜单增加「用户排行榜」入口
侧边栏综合管理与右上角工作区下拉菜单新增「用户排行榜」(admin only),
指向 /page/strategy/admin/user-leaderboard,并默认展开综合管理分组。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:47:15 +08:00
AdministratorandClaude Opus 4.8 d4a71ce384 feat: 浏览器扩展支持交互式页面操作 (browser_act)
在原有只读抓取 (browser_fetch_page) 基础上,新增 observe→act 循环,
让 Agent 能在用户的 Chrome 中实际操作网页:点击/输入/滚动/等待/页内跳转。

后端:
- 新工具 browser_act,按 session 复用标签页,返回带编号的可交互元素清单+正文
- browser_context 任务队列扩展 kind/session_id/action 与 observation 结果
- /api/browser-context 路由下发 act 任务、接收 observation
- BrowserContextConfig 增加 actions_enabled 等开关,tools.py 两级门控
- 接线 app.py 路由挂载、app_config.py 配置字段、config.yaml 工具注册

前端:
- ChatPage 处理「发送当前页」回跳的 browser_context query 参数并自动提交

扩展 (browser-extension/deer-web-clipper):
- shared.js 自包含注入函数 deerObservePage / deerPerformAction
- background.js 按 kind 分发任务,session→tab 映射存 chrome.storage.session

测试:tests/test_browser_act.py(队列往返 + 工具校验 + 模拟扩展成功路径,8 passed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:47:00 +08:00
g zy 004853f030 Merge branch 'gzy_iframedf' into 'master'
feat: 登录token回传至宿主

See merge request knowledge-base-bj/deerflow-server!102
2026-06-08 04:35:35 +00:00
ZyGuo 37be59b0dd feat: 登录token回传至宿主 2026-06-08 12:33:24 +08:00
董宏凯 273503ddb9 Merge branch 'dhk-1' into 'master'
样式修改

See merge request knowledge-base-bj/deerflow-server!101
2026-06-07 15:54:16 +00:00
董宏凯 ce431ee155 样式修改 2026-06-07 23:53:52 +08:00
董宏凯 12844e341c Merge branch 'dhk-1' into 'master'
feat(roundtable): 业务链条「发布为公共」+ 我的/公共筛选

See merge request knowledge-base-bj/deerflow-server!100
2026-06-07 14:56:48 +00:00
董宏凯andCursor eaf1e075f2 fix(roundtable): 本地 SQLite 路径与离线并行席位禁用 web_search
将 backend 配置中的 SQLite/checkpointer 改为相对路径以便本地开发,并在离线部署下禁止并行席位使用 web_search,统一走技能检索。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 22:55:50 +08:00
董宏凯andClaude Opus 4.8 7affaaf7c0 feat(roundtable): 业务链条「发布为公共」+ 我的/公共筛选
后端:roundtable_chains 加 is_public 列;list_chains 支持 scope=mine|public(public=所有已发布,任何账号可查看/选入研讨);get_chain 放开公共可见;响应加 is_public/is_mine;update 支持 is_public(发布/取消,owner 限定,编辑/删除仍仅 owner)。迁移 20260608_03 接在合并 head 20260608_02 之后,保证单一 alembic head(多 head 会让启动自动迁移失败、静默降级 create_all → 列缺失 500)。

前端:listChains(scope) + setChainPublic;BusinessChain 加 isPublic/isMine;列表弹窗与研讨选择器加「我的/公共」筛选 tab;列表卡片发布/取消发布 + 公共徽标(别人的只读);配置页头部加发布按钮(已保存且本人)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:30:23 +08:00
董宏凯 90f3564d0b Merge branch 'dhk-1' into 'master'
fix(roundtable): 后台任务临时跳过 Step3 报告生成

See merge request knowledge-base-bj/deerflow-server!99
2026-06-07 13:28:21 +00:00
董宏凯 05d14bb364 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-07 21:25:54 +08:00
董宏凯andClaude Opus 4.8 7b8c11852e feat(roundtable): 圆桌 Step2 分层 DAG 编排(并行/沙箱双模式 + 流程图 + 按需取全文)
数据模型/编辑器:链条加 stages 分层(前后端 + alembic 20260607_03);BusinessChainEditorPage 竖列 Stage 编辑 + 底部只读流程图预览;lib/dag-types + chain-stages 纯逻辑(含单测)。

调度(runDagOrchestration,方案 A 总控驱动):每个 stage 由总控经 agent_orchestration 派活给计划钉死的席位,批内 Promise.all 并行、批间 barrier 串行;末尾交回 runOrchestration 收口。抑制 leader 预广播 + 干净席位任务,根治『席位模仿派活』。

执行子模式:快速(双列·产物走正文·禁写文件 seat_parallel)/ 沙箱(单列·写 .md·沙箱跟随预览 seat);统一禁 bash(防 .py 脚本),提示词禁 .py。

显示:头像条 + 两列/单列 + ref 全量累积 + 节流 flush(仅选中渲染);DAG 流程图弹窗(按 stage 分列,节点随执行点亮,只到共识);沙箱跟随当前席位(防错位 + 修第一个席位自动开沙箱)。

按需取全文:新增 harness 只读工具 read_peer_delivery(从 run context 取网关注入的前序席位完整交付),席位默认看摘要、需要时取全文。

修:宽表溢出、并行自动下滑、data-lov-id Fragment 警告等。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:20:58 +08:00
董宏凯andClaude Opus 4.8 5164fd362c feat: 重新设计浏览器标签页图标(智能助手中心)
玫红渐变磁贴 + 白色对话气泡 + 气泡内 AI 闪光星 + 角落小闪光,围绕「助手 / 多种智能工具」主题;并在 index.html 补上 favicon 链接。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:20:58 +08:00
董宏凯andClaude Opus 4.8 2e689e4482 docs(roundtable): 圆桌 DAG 编排 + 并行执行显示分步需求文档
锁定三项设计决策:分层 Stage(非自由连线)、并行跳过 leader 前端直派、主区固定 2 列(窄屏降 1 列)。含数据模型/编辑器交互/并行调度/禁写文件/高性能流式渲染/分步实现计划(Phase 1-8)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:20:58 +08:00
董宏凯andCursor 1ee2e6ca0a perf(roundtable): 轻量 thread 接口降低广播与席位状态核对开销
新增 messages/append 与 last-ai-message 接口,圆桌广播和总控派活轮改走服务端轻量读写,避免 loopback 传输整份 state。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:20:58 +08:00
董宏凯andCursor 924c8f2fac fix(roundtable): 优化 Step2 总控派活稳定性与并发编排
限制总控工具白名单、关闭圆桌记忆注入、综合轮 synthesis_mode 与编排 epoch 防双发,并优化广播异步化及 Postgres checkpointer 连接池。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:20:58 +08:00
Administrator 0001f86826 feat: 完善公开任务预览和用户问答历史 2026-06-07 21:18:16 +08:00
fengxiangdebaozi 9c9c912657 refactor(strategy): 任务分析页移除演示数据并接入真实接口
舆情分析与规划任务分析看板统一改为后端任务列表拉取,去掉 mock/演示切换,并修正 running 列状态与日期筛选交互。
2026-06-06 20:23:58 +08:00
董宏凯 024c8ee5a1 Merge branch 'ji' into 'master'
feat: 重新设计浏览器标签页图标(智能助手中心)

See merge request knowledge-base-bj/deerflow-server!97
2026-06-06 01:21:19 +00:00
董宏凯 2e7985639e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-06 09:19:15 +08:00
AdministratorandClaude Opus 4.8 e7c745a512 feat: 排行榜日快照分析 + 数据库连接池/索引调优
- 排行榜改为按天预计算快照(admin_leaderboard_daily_stats),请求只读取合并,重统计移至后台调度;过去日不可变、当天 TTL 刷新,避免在线聚合长时间占用数据库
- 为 runs/tool_call_metrics/llm_call_metrics/threads_meta 增加面向时间段+用户聚合的复合索引(model __table_args__ + migration 20260608_01)
- 连接池可调:DatabaseConfig 新增 max_overflow / pool_timeout,接入 postgres/mysql 引擎;config.yaml 上调 pool_size=20/max_overflow=20
- 新增手动执行的索引脚本 scripts/sql/leaderboard_indexes_mysql.sql(幂等,适用于已存在数据、create_all 不补索引且 alembic 关闭的部署)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:52:24 +08:00
董宏凯 1b75887615 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-05 23:47:07 +08:00
董宏凯andClaude Opus 4.8 a1bcc6eed4 feat: 重新设计浏览器标签页图标(智能助手中心)
玫红渐变磁贴 + 白色对话气泡 + 气泡内 AI 闪光星 + 角落小闪光,围绕「助手 / 多种智能工具」主题;并在 index.html 补上 favicon 链接。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:45:28 +08:00
fengxiangdebaozi 0bf6285287 fix(roundtable): 后台任务临时跳过 Step3 报告生成
后台挂起路径在真模型下仍无法稳定写出报告 HTML,先只跑完 Step2 研讨并正确设置 furthest_step;测试可通过 enable_report 覆盖验证报告链路。
2026-06-05 23:41:28 +08:00
Administrator 4df71f32a8 fix: use sqlite backend config 2026-06-05 22:44:21 +08:00
fengxiangdebaozi 44b7f9fa99 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-05 22:40:44 +08:00
fengxiangdebaozi 03329387b3 样式修改 2026-06-05 22:22:38 +08:00
吉鑫升 e5ba898fc7 hide user leaderboard entry 2026-06-05 22:11:31 +08:00
吉鑫升 a41b5a6d4c fix: improve agent handoff and leaderboard scheduling 2026-06-05 21:46:04 +08:00
董宏凯 cbbde887d4 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-05 21:17:31 +08:00
董宏凯 89724d6e4d Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!96
2026-06-05 13:17:03 +00:00
董宏凯 6d5325d444 代码修改 2026-06-05 21:16:35 +08:00
吉鑫升 db1b32612b fix: run leaderboard stats in background 2026-06-05 21:15:20 +08:00
吉鑫升 f4ccef8c70 fix: optimize leaderboard and agent handoff 2026-06-05 21:06:23 +08:00
董宏凯 99aa064e6c Merge branch 'ji' into 'master'
Ji

See merge request knowledge-base-bj/deerflow-server!95
2026-06-05 12:36:02 +00:00
吉鑫升 24a5ca0c06 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-05 20:07:03 +08:00
吉鑫升 6eda572f31 tijiaodaim 2026-06-05 20:06:55 +08:00
董宏凯 7b6dd9ba6f Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-05 19:56:51 +08:00
董宏凯 1341767692 Merge branch 'dhk-1' into 'master'
feat: 多智能体会商草稿分享(import 复制 / view 公开结果页)

See merge request knowledge-base-bj/deerflow-server!94
2026-06-05 11:48:08 +00:00
董宏凯andClaude Opus 4.8 f845805706 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
解决 StrategyRoutes.tsx import 冲突:保留双方各自唯一的页面导入(AgentsPage/ChatPage/ChatsPage + AdminUserLeaderboardPage/SettingsPage),去掉与下方重复的 13 个 import。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:46:19 +08:00
董宏凯andClaude Opus 4.8 b2132dd668 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
解决 artifact 预览组件冲突:mermaid 渲染(dhk-1) 与 markdown 锚点跳转(master) 取并集;package-lock.json 按 master 删除(项目用 pnpm)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:43:02 +08:00
fengxiangdebaozi fc5eaa2941 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-05 19:28:19 +08:00
fengxiangdebaozi 045bb11abf 样式修改 2026-06-05 19:28:09 +08:00
董宏凯 9a31155581 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-05 19:00:28 +08:00
董宏凯andCursor 9e510ce088 feat: 优化会商公开分享页,展示总控结论与各席位交付
公开分享页拆分为落地页与独立报告页,后端补充 consensus_text 与 seat_deliveries 字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 19:00:13 +08:00
fengxiangdebaozi a642ba91fa Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-05 18:58:37 +08:00
fengxiangdebaozi b0fdeab995 代码修改 2026-06-05 18:58:28 +08:00
吉鑫升 1bf1fc6512 增加依赖 2026-06-05 17:36:53 +08:00
吉鑫升 392373b4b7 chore: add qwen model config 2026-06-05 17:32:53 +08:00
吉鑫升 f3def88342 feat: add sqlite checkpoint fallback switch 2026-06-05 17:28:44 +08:00
吉鑫升 690d09a3d1 feat: add workspace view mode settings 2026-06-05 17:21:49 +08:00
吉鑫升 9471e72171 feat: add checkpoint postgres migration admin 2026-06-05 17:16:31 +08:00
g zy 9ebde3b315 Merge branch 'gzy_iframedf' into 'master'
feat: iframe传 文件进入

See merge request knowledge-base-bj/deerflow-server!93
2026-06-05 07:10:59 +00:00
吉鑫升andClaude Opus 4.8 eaa061dd06 feat: 新增技能置顶功能
- 后端:新增 PUT /skills/custom/{name}/featured 接口,在 usage 中实现 set_featured 与 featured 字段(与 pinned 正交,仅影响排序)
- 前端:新增 setSkillFeatured API、置顶/取消置顶操作及中英文文案
- ScheduledTasksPage:优化助手选择下拉框的宽度与文本换行
- config.yaml:整理 log_system 配置块

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:02:50 +08:00
ZyGuo 5f6ab41798 feat: iframe传 文件进入 2026-06-05 14:15:42 +08:00
xuhdandClaude Sonnet 4.6 9adb0fee28 fix(notebook): preserve selected notebook/docs when switching material source with studioToken
- Initialize materialSource via WritingFormProvider initialMaterialSource prop
- Don't clear selectedNotebookId/selectedDocumentIds on source switch when studioToken is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:58:34 +08:00
xuhdandClaude Sonnet 4.6 c577833e7c fix(notebook): default material source to notebook when studioToken or initialNotebookId is set
Replace useEffect-based approach with synchronous effectiveMaterialSource
derived value to avoid first-render flash and timing issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:53:10 +08:00
xuhdandClaude Sonnet 4.6 d57bcd9aa2 feat(notebook): lock material source to notebook when studioToken is provided
- Auto-switch materialSource to 'notebook' on mount when initialNotebookId is set
- Replace material source dropdown with static label when studioToken is present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:45:24 +08:00
xuhdandClaude Sonnet 4.6 0e7149eec3 feat(notebook): load notebook space via token and pre-select docs/sources
- NotebookAIWritingPage reads `token` from URL, fetches space data from
  notebook-token API (search_space_id, doc_ids, user_token) and space
  name from searchspaces API; keep-alive ping every 50s resets Redis TTL
- Pass user_token as studioToken through AIWritingPage → AIWritingLayout
  → WritingFormPanel → AIWritingPanel → AIWritingForm to skip
  getTokenByEmail when studio token is already available
- useStudioNotebooks accepts enabled flag; useNotebookSources accepts
  overrideToken to bypass the internal token fetch
- AIWritingForm hides notebook selector when studioToken is provided
  (notebook is pre-locked) and pre-selects initialDocumentIds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:38:08 +08:00
xuhd e53c1a0f7b feat(notebook): pre-select notebook source when notebook_id is in URL 2026-06-05 13:35:35 +08:00
xuhd e689493723 feat(notebook): add /page/notebook/ai-writing route for 索财笔记 view 2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 e6d3226c47 feat(studio): replace notes source upload with multi-tab popup
- Add document-upload-popup.tsx with tabs for B系统/A系统/Y系统/XWT/upload
- Add network-actor.tsx for XWT search tab
- Add empty.tsx for empty state
- Refactor SourcesDialog: left panel becomes single "上传来源" button that opens new popup
- Wire DocumentUploadDialogProvider around StudioNotebookDetailPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 2c2650ff07 feat(skill-evolution): expose model_name as per-user configurable field
前端恢复模型选择器,后端允许 model_name 写入 per-user skill_evolution.json。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 603d667608 fix(skill-evolution): strip non-user-configurable fields from per-user overrides
load_user_overrides 只保留 interval_hours/stale_after_days/archive_after_days,
过滤掉老文件中的 enabled、model_name 等字段,防止旧数据覆盖全局开关。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 e9125b0900 test(curator): add scheduler startup non-blocking and execution tests
验证 curator 调度器不阻塞启动、启动延迟后真实执行、无锁时跳过。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 d0c696e523 refactor(skill-evolution): simplify config UI to 3 user-editable fields
前端配置页只保留运行间隔、陈旧阈值、归档阈值三个字段,
其余配置(enabled、model_name 等)走 config.yaml 全局默认。
后端 PUT /api/curator/config 仅保存这三个字段到 per-user skill_evolution.json。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhd 6f9afcdba2 修复技能审核agent提示词 2026-06-05 13:32:19 +08:00
xuhd 705de81c78 fix(skill-review): use uuid per review run to avoid checkpoint context bleed 2026-06-05 13:32:19 +08:00
xuhd 335bc3cf48 fix(skill-review): propagate user_id through review chain for correct DB ownership 2026-06-05 13:32:19 +08:00
xuhd 857798ff2f fix(skill-review): pass RunnableConfig with thread_id to satisfy ThreadDataMiddleware 2026-06-05 13:32:19 +08:00
xuhd 3955bd72b5 refactor(skill-review): use create_agent with skill_evolution disabled to allow autonomous skill creation 2026-06-05 13:32:19 +08:00
xuhd b5d80025e9 fix(skill-review): lower CREATE threshold and log agent decision 2026-06-05 13:32:19 +08:00
xuhd 5b5e4f8ff0 fix(thread-data): guard runtime.context None on line 110 2026-06-05 13:32:19 +08:00
xuhd ad5bd22c5e refactor(skill-review): use make_lead_agent instead of create_react_agent
Reuses the full middleware chain (ToolErrorHandlingMiddleware,
LLMErrorHandlingMiddleware, etc.) so tool errors like 'already exists'
and invalid SKILL.md are converted to ToolMessages by the middleware,
not silently swallowed or crashing the review task.
2026-06-05 13:32:19 +08:00
xuhd b2ed6f1acf fix(skill-review): add strict SKILL.md format spec to review prompt 2026-06-05 13:32:19 +08:00
xuhd 2a8364e902 fix(skill-review): use runtime.context to get thread_id 2026-06-05 13:32:19 +08:00
xuhd 23a18ec318 fix(skill-review): after_agent must be sync, not async 2026-06-05 13:32:19 +08:00
xuhd d713f0209d feat(curator): restrict per-user config to archiving thresholds only 2026-06-05 13:32:19 +08:00
xuhd 0a21d37146 chore(config): enable skill_evolution, review_worker, and curator (24h) in config.yaml 2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 c645142f82 feat(skill-evolution): register SkillReviewMiddleware in lead agent chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 0e7a1de4db feat(skill-evolution): add SkillReviewMiddleware to trigger post-turn review
Implements SkillReviewMiddleware that hooks into after_agent to call
maybe_start_review() as a background task after each agent turn, without
blocking the HTTP response. Exceptions are caught and logged only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 ef3ef433c1 feat(skill-evolution): add SkillReviewWorker for passive post-turn skill creation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhd 0da91be9b1 test(skill-evolution): fix unused import and add max_messages assertion 2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 69454ada69 feat(skill-evolution): add ReviewWorkerConfig to SkillEvolutionConfig
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
xuhdandClaude Sonnet 4.6 1126fedea8 chore(env): 添加我的笔记相关环境变量配置
- VITE_AIPI_FRONTEND_URL: 我的笔记前端地址
- VITE_STUDIO_ENABLED: 控制侧边栏我的笔记入口显示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 13:32:19 +08:00
董宏凯 69c28e3563 代码修改 2026-06-05 13:04:44 +08:00
吉鑫升andClaude Opus 4.8 b117bb1dc2 Revert skill list layout and tune leaderboard report
- Revert skill settings to vertical list layout with pinned sorting
- Default leaderboard range 30d -> 7d, hide stats while report generating
- Reframe leaderboard report prompt as user-behavior analysis (cache v5)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:34:39 +08:00
董宏凯 9c9330114f Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-05 11:20:22 +08:00
董宏凯andCursor 0fd314632a feat: 多智能体会商草稿分享(import 复制 / view 公开结果页)
支持会商草稿生成分享码、导入副本与公开只读结果页,并在侧边栏展示最近会商记录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 11:19:09 +08:00
吉鑫升 0d91c74289 style: enlarge leaderboard report dialog 2026-06-05 11:17:09 +08:00
吉鑫升 d59f9e218f feat: enhance leaderboard AI report 2026-06-05 11:00:08 +08:00
fengxiangdebaozi 9bbd04d6cf 后台挂起功能优化 2026-06-05 10:59:56 +08:00
吉鑫升 22be6325a8 Resolve merge conflicts 2026-06-05 10:28:45 +08:00
吉鑫升 e96cc97462 Merge origin/master into ji 2026-06-05 10:04:11 +08:00
Administrator 92f5cb8fe3 enhance admin leaderboard and previews 2026-06-05 01:51:48 +08:00
Administrator a5897caec4 enhance leaderboard reporting and grouping 2026-06-05 00:02:05 +08:00
Administrator 88c83b4dd7 refine admin leaderboard experience 2026-06-04 23:41:19 +08:00
Administrator fe0ede178e polish admin leaderboard analytics 2026-06-04 21:09:36 +08:00
fengxiangdebaozi cca3d8eb41 字体大小修改 2026-06-04 20:49:32 +08:00
Administrator 5590f7d85a style: polish admin user leaderboard 2026-06-04 20:22:37 +08:00
fengxiangdebaozi 90bced3ed2 第二次生成结果后台挂起 2026-06-04 20:18:34 +08:00
fengxiangdebaozi 19399482e1 后台挂起 2026-06-04 20:00:58 +08:00
吉鑫升 7657c45aae Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-04 19:37:19 +08:00
吉鑫升 e6e0832c8a chore: commit local changes 2026-06-04 19:35:56 +08:00
Administrator 8f80b7c1ef feat: add admin user leaderboard analytics 2026-06-04 19:35:11 +08:00
董宏凯 c766e3fa3b Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!92
2026-06-04 11:10:18 +00:00
fengxiangdebaozi 788118ce3d 下载图片 2026-06-04 19:08:52 +08:00
fengxiangdebaozi 77f8ded1e6 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-04 18:50:50 +08:00
fengxiangdebaozi 9d863f63e5 代码提交 2026-06-04 18:50:31 +08:00
Administrator 1e4ef070cf fix: 调整提示词和 agent 管理入口 2026-06-04 18:45:16 +08:00
吉鑫升 c1336ad5b3 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/components/workspace/settings/memory-settings-page.tsx
#	frontend-web/src/core/auth/sync.tsx
2026-06-04 17:49:26 +08:00
吉鑫升 143e0478ef Merge remote-tracking branch 'origin/ji' into ji 2026-06-04 17:46:50 +08:00
吉鑫升 cbd07c9d1b chore: update ji branch work 2026-06-04 17:46:11 +08:00
AdministratorandClaude Opus 4.8 e3b0958c23 fix: drive logout from real 401s, not local storage changes
apiFetch now redirects to login only on a genuine backend 401 (auth endpoints excluded, only when previously logged in). AuthSyncWatcher no longer treats localStorage.clear()/unrelated writes as a logout, fixing spurious sign-outs (e.g. when uploading skills).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:41:46 +08:00
g zy b843ab6542 Merge branch 'gzy_iframedf' into 'master'
feat: 中断回复

See merge request knowledge-base-bj/deerflow-server!91
2026-06-04 09:03:00 +00:00
ZyGuo dccce9355d feat: 中断回复 2026-06-04 17:00:56 +08:00
吉鑫升 6c30e649e3 Merge remote-tracking branch 'origin/ji' into ji 2026-06-04 16:59:05 +08:00
吉鑫升 bcebc8d5fb feat: 支持智能体置顶配置 2026-06-04 16:58:42 +08:00
AdministratorandClaude Opus 4.8 225398972d fix: add missing axios dependency to frontend-web
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:53:07 +08:00
AdministratorandClaude Opus 4.8 87f764bfa9 feat: improve markdown rendering, artifact file preview, and public scheduled task page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:52:30 +08:00
fengxiangdebaozi 47217e9158 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-04 15:41:03 +08:00
fengxiangdebaozi 10334ddfe1 feat: AI writing history, Word export, roundtable dispatch chain 2026-06-04 15:40:15 +08:00
董宏凯 002e71f971 Merge branch 'dhk-1' into 'master'
样式修改

See merge request knowledge-base-bj/deerflow-server!89
2026-06-04 06:55:59 +00:00
吉鑫升andClaude Opus 4.8 1f2b1e3b8a feat: 删除提问时级联删除其回答,并移除回答上的删除按钮
- 删除按钮只保留在用户提问气泡上;AI 回答不再单独提供删除
- 后端 DELETE /messages/{id} 改为按整轮删除:从提问删到下一条提问之前,
  返回实际删除的 deleted_ids,保持 checkpoint 合法(无孤立工具结果/回答)
- 前端隐藏后端返回的整批 id,删除后即时消失
- 更新删除相关中英文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:34:52 +08:00
吉鑫升andClaude Opus 4.8 5d12616211 chore: 将登录失败次数上限从 5 提高到 100
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:34:42 +08:00
吉鑫升 9704eff9f7 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-04 14:28:22 +08:00
吉鑫升andClaude Opus 4.8 362a8268e8 feat: 支持删除单条聊天消息(用户提问与模型回答)
- 后端新增 DELETE /api/threads/{thread_id}/messages/{message_id},从最新 checkpoint 的 messages 通道过滤掉指定 id 并写回新 checkpoint
- 前端 useDeleteMessage 钩子 + DeleteMessageButton(带确认框)组件
- 在用户问题气泡与助手回答工具栏上各加删除按钮,仅对已落库消息显示
- MessageList 本地隐藏已删消息并在切换会话时重置;只读/分享场景禁用删除
- 新增 deleteMessage 相关中英文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:28:11 +08:00
fengxiangdebaozi c0e3c5d692 样式修改 2026-06-04 14:19:53 +08:00
AdministratorandClaude Opus 4.8 94c02ded2a feat: add user preferences, agent name desensitization and related improvements
- Add user preferences persistence (model, migration, router, frontend core)
- Add agent name desensitization config
- Add agent_id to user custom prompts with migration
- Enhance agents persistence (base, memory, model, sql) and scheduler service
- Improve frontend agent cards/gallery, scheduled tasks, custom prompt menu
- Add pagination UI component; remove stale package-lock.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:40:18 +08:00
董宏凯 998bd0ce06 Merge branch 'zhujingyi' into 'master'
'feat:技能样式改成卡片式'

See merge request knowledge-base-bj/deerflow-server!87
2026-06-04 03:13:33 +00:00
fengxiangdebaozi 6b6465b5e3 Merge remote branch into zhujingyi 2026-06-04 11:12:45 +08:00
董宏凯 57da50b8d6 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!88
2026-06-04 03:10:18 +00:00
fengxiangdebaozi f87f6031de Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-04 11:09:29 +08:00
fengxiangdebaozi 5b686824c5 样式修改 2026-06-04 11:08:42 +08:00
g zy 95487d362f Merge branch 'gzy_iframedf' into 'master'
feat: 从iframe对接模型配置

See merge request knowledge-base-bj/deerflow-server!86
2026-06-04 03:05:26 +00:00
ZyGuo f496546cc0 feat: 从iframe对接模型配置 2026-06-04 11:03:50 +08:00
zjy 9423d7e892 'feat:技能样式改成卡片式' 2026-06-04 10:44:55 +08:00
fengxiangdebaozi 062f984262 ai写作位置调整 2026-06-04 10:40:16 +08:00
吉鑫升andClaude Opus 4.8 925675ab54 chore: update ji local changes
- frontend: scheduled tasks pages, system-settings api, workspace nav menu
- backend: system_settings router, app_config, log_system_config, config.yaml

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:18:32 +08:00
董宏凯 c05513ae1e Merge branch 'dhk-1' into 'master'
fix(roundtable): 文件卡截断、Step3 完整 HTML 与跨标签页登录体验

See merge request knowledge-base-bj/deerflow-server!85
2026-06-03 13:46:33 +00:00
fengxiangdebaozi f257ccb682 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 21:45:21 +08:00
fengxiangdebaozi d5b0bd4d31 fix(roundtable): 文件卡截断、Step3 完整 HTML 与跨标签页登录体验
第二步消息列表长文件名不再溢出卡片;第三步绘制结束后从沙箱回灌完整 HTML(修复 append 续写 stub 被截短),进入第三步时关闭第二步沙箱;HTML 预览与提示词强调 utf-8/中文。跨标签页切换账号改为 toast 提示而非强制刷新;方案总结智能体 SOUL 去掉「AI 自动生成」标签要求。
2026-06-03 21:44:46 +08:00
董宏凯 5026bd1887 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 21:07:17 +08:00
董宏凯 b0b0cf7b9e 地址修改 2026-06-03 21:07:12 +08:00
fengxiangdebaozi bf96cf4cb4 接口地址修改 2026-06-03 19:12:30 +08:00
吉鑫升 aeef1dca59 Merge remote-tracking branch 'origin/ji' into ji 2026-06-03 19:01:56 +08:00
吉鑫升 a1d460e220 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/components/workspace/messages/message-list-item.tsx
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/html_page.py
#	offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
2026-06-03 19:01:45 +08:00
吉鑫升 e2a962de32 chore: update ji local changes 2026-06-03 18:57:49 +08:00
董宏凯 dbf1f46fa1 Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!84
2026-06-03 10:57:34 +00:00
fengxiangdebaozi ce07a27b8d Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 18:54:45 +08:00
fengxiangdebaozi 8f6e08eb2f Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 18:54:26 +08:00
董宏凯 a4b14e4911 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!78
2026-06-03 09:38:24 +00:00
fengxiangdebaozi 56114562cb fix: ²¹Æë Step3 ɳÏä²Ù×÷²¢Îȶ¨д×÷³ÎÇåÁ÷³Ì
Ϊ roundtable µÚÈý²½ɳÏ䲹»ØÐ´°¿ڴò¿ªÓëÏÂÔزÙ×÷£¬²¢ÐÞÕýд×÷ÅäÖóÎÇåµÄÌύÁ÷³ÌÓëĬÈÏËزÄÀ´Դ£¬±ÜÃâÓû§Ôڶ࿨Ƭ³¡¾°ÏÂÌáǰÌύ»ò½øÈ벻·ûºÏԤÆڵļìË÷·¾¶¡£
2026-06-03 17:36:52 +08:00
董宏凯 ba7834f222 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 17:27:18 +08:00
董宏凯andClaude Opus 4.8 d49406d2f1 Merge branch 'master' into dhk-1
解决 8 处冲突:
- app.py:同时挂载 public_embed + html_page_favorites 两个 router
- .env:保留 dhk-1 的 CORS 配置(含 5174、ALLOW_ALL=0)
- message-list.tsx:并入双方新增(onSetupWriting* 解构 + contentClassName/fitMessageRowsToContent 类型)
- WorkspaceRoutes.tsx:采用 master 的路由结构(含 chats_iframe/IframeChatPage),补回 dhk-1 的 BusinessChainEditorPage 导入与 roundtable/chains 路由
- LoginPage.tsx:合并两套登录(master 的 token 换登录 + dhk-1 的 online-ticket),并集依赖数组
- ScheduledTasksPage.tsx:采用 master 的 HTML 定时任务表单(isHtmlTask 分支)
- AIWritingForm.tsx:保留 dhk-1 的 useWritingForm 重构,补回 master 的笔记来源 useState;WritingFormContext.materialSource 扩展支持 'notebook'
- useAIWritingStream.ts:保留 dhk-1 重构(仅 reducer/helpers,useAIWriting 已移至 AIWritingContext),不并入 master 在此文件的旧实现

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:26:52 +08:00
fengxiangdebaozi a81d8adc90 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 16:34:25 +08:00
fengxiangdebaozi 3eae4a0f45 代码修改 2026-06-03 16:34:17 +08:00
董宏凯 016f921cf0 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 16:28:34 +08:00
董宏凯 9c8230a867 跨域修改 2026-06-03 16:28:29 +08:00
董宏凯andCursor aea7724cdb 修复 embed 跨域并持久化圆桌 Step3 结果
- Gateway 默认 CORS 白名单增加 5174,embed 文档补充跨域配置说明
- 圆桌草稿新增 step3 快照字段与迁移,结果绘制 HTML 可随草稿恢复

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:27:42 +08:00
fengxiangdebaozi 79d87ad661 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-03 16:22:51 +08:00
fengxiangdebaozi ad4b27b7ca feat: 无界 ticket 登录、AI 写作配置确认与定时任务弹窗修复
无界 onlineTicket 对接 MaxKey 与 consumer 接口并用 sub 登录;新增 setup_writing 写作配置确认流;定时任务创建弹窗限制多行高度并支持滚动。
2026-06-03 16:20:45 +08:00
董宏凯andCursor 40ba81ee6a 新增 iframe 结果接口与圆桌 Step3 结果绘制
- 公开 API:GET /api/public/embed/sessions/{sessionId}/result 返回 messages、generated_text、generated_files
- 圆桌 Step3:内置 roundtable-report 智能体、report/init 与结果绘制 UI

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 13:57:37 +08:00
xu hd d0e71a8104 Merge branch 'xuhd' into 'master'
技能详情对话框新增文件浏览面板:支持预览、编辑、下载、复制路径

See merge request knowledge-base-bj/deerflow-server!83
2026-06-03 04:36:31 +00:00
xuhd f2d7d43e38 Merge remote-tracking branch 'origin/master' into xuhd 2026-06-03 11:55:44 +08:00
g zy d7cb19ee96 Merge branch 'gzy_iframedf' into 'master'
Gzy iframedf

See merge request knowledge-base-bj/deerflow-server!82
2026-06-03 03:51:09 +00:00
ZyGuo 9d425a19e8 fix: 去掉iframe页顶部栏 2026-06-03 11:49:18 +08:00
xuhdandClaude Sonnet 4.6 48f37df3d5 chore(env): 更新本地开发环境配置
- 开启 GATEWAY_CORS_ALLOW_ALL=1(本地开发跨域放行)
- 更新前端 .env.development

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:45:54 +08:00
fengxiangdebaozi ab876fc7d8 feat(ai-writing): 支持用户粘贴已有大纲并按结构生成
表单可提交 userOutline,后端解析/归一用户大纲并关联素材;补充大纲测试用例与右侧编辑器滑词/段落菜单文档。
2026-06-03 11:43:44 +08:00
xuhdandClaude Sonnet 4.6 712326387f merge: 合并 origin/xuhd,解决 message-list-item 和 WorkspaceRoutes 冲突
- message-list-item: 保留 EmbeddedPageContext + 消息内联编辑两套功能
- WorkspaceRoutes: 保留 IframeChatPage + NotebookDetailPageWithKey

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 11:41:49 +08:00
AdministratorandClaude Opus 4.8 133054e2dd feat(skills/settings): 技能复制 + 设置持久化等会话前改动
- 技能复制:新增 POST 复制接口(_rewrite_skill_frontmatter_name 重写 frontmatter name + 校验 + 去重),前端 skills api/hooks 与技能设置页配套
- 设置持久化:lastAgentId、线程 RAG 模式锁(settings/index、local)
- i18n(en/zh/types)、agent-card/input-box/message-list 等页面文案与交互微调
- scheduler/element.py 小修

(本批为会话前已存在的工作区改动,按要求一并提交;构建产物 backend.zip 未提交)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:38:47 +08:00
AdministratorandClaude Opus 4.8 adcc5a358b feat(scheduled-tasks): 新增 HTML 页面定时任务(生成/预览/收藏/技能)
后端:
- 新增 task_kind=html_page 任务类型,与普通定时任务并存,配置存 execution_context(无需迁移)
- 执行流水线:智能体检索数据 -> 选定模型渲染 HTML -> 清洗/校验 -> 失败自动修复 -> 完整性闸门(缺 doctype/html/head/body/title 或残留脚本即失败) -> 归档 index.html -> 落库
- 新增 scheduled_html_pages / html_page_favorites 表与 store;收藏去重(不可重复)、可删除、收藏时提取可复用风格
- HTML 预览接口(鉴权 + 公开)、收藏接口
- 渲染注入已启用的 HTML 技能(metadata.kind=html_page);全程使用用户所选模型,去除硬编码 GLM-5
- 定时任务跳过 TitleMiddleware,避免标题生成回退默认模型

前端:
- 创建/编辑弹窗增加任务类型与 HTML 配置(页面要求/限制/参考收藏页/模型下拉)
- 运行详情页与公开页 iframe 沙箱预览 HTML;收藏按钮(已收藏禁用)
- 参考收藏页可选可删

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:35:38 +08:00
AdministratorandClaude Opus 4.8 5c2379577b fix(auth): token 登录将令牌放入 Authorization: Bearer 头
token 换登录调上游 getTokenInfo 时参数传错:原来发静态头
Authorization: Bearer admin + 令牌放 access_token cookie。上游实际
从 Authorization: Bearer <token> 头读取令牌。

- 鉴权头改为 "Bearer "+前端传入 token,删除 access_token cookie
- service_authorization 配置项弃用(保留兼容,不再参与请求)
- 新增回归测试 test_token_login_upstream_call.py
- 同步更新 AUTH_LOGIN.md / CLAUDE.md / config.yaml 注释 / login_config 文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:35:38 +08:00
AdministratorandClaude Opus 4.8 c5352f2ee7 本地启动环境修复与配置
- config.yaml: checkpointer 连接串改为相对路径(原为他机绝对路径,本地无法创建)
- frontend-web: 补充缺失的 dayjs 直接依赖(lockfile 已有,package.json 缺失)
- .gitignore: 忽略 backend/uv.lock 并取消其跟踪

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:35:38 +08:00
ZyGuo 5574364438 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf 2026-06-03 11:33:50 +08:00
ZyGuo cb0b30690e fix: 去掉用户气泡中<>的显示 2026-06-03 11:33:23 +08:00
g zy 8442e6ec8d Merge branch 'gzy_iframedf' into 'master'
fix: 传图修复

See merge request knowledge-base-bj/deerflow-server!80
2026-06-03 03:12:40 +00:00
ZyGuo 8ba3a67401 fix: 传图修复 2026-06-03 11:12:10 +08:00
g zy f2dc03b51e Merge branch 'gzy_iframedf' into 'master'
Gzy iframedf

See merge request knowledge-base-bj/deerflow-server!79
2026-06-03 03:08:10 +00:00
ZyGuo 5d39aa55bd feat: 支持传图 2026-06-03 10:51:42 +08:00
xuhdandClaude Sonnet 4.6 22574d3ada fix(setup_agent): 限制技能数量最多 5 个
- middleware 拦截阶段静默截断 skills[:5],超出不报错不中断流程
- 工具执行阶段同样截断,双重兜底

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:48:13 +08:00
董宏凯 0727b4564b 圆桌规划第3步修改 2026-06-03 10:01:10 +08:00
董宏凯 6521218308 样式修改 2026-06-03 09:54:11 +08:00
ZyGuo 34a9f15cb4 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf 2026-06-03 09:41:14 +08:00
ZyGuo a5b65f4087 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf 2026-06-03 09:41:09 +08:00
AdministratorandClaude Opus 4.8 f590f672be feat(skills/settings): 技能复制 + 设置持久化等会话前改动
- 技能复制:新增 POST 复制接口(_rewrite_skill_frontmatter_name 重写 frontmatter name + 校验 + 去重),前端 skills api/hooks 与技能设置页配套
- 设置持久化:lastAgentId、线程 RAG 模式锁(settings/index、local)
- i18n(en/zh/types)、agent-card/input-box/message-list 等页面文案与交互微调
- scheduler/element.py 小修

(本批为会话前已存在的工作区改动,按要求一并提交;构建产物 backend.zip 未提交)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:48:19 +08:00
AdministratorandClaude Opus 4.8 d9afaca51f feat(scheduled-tasks): 新增 HTML 页面定时任务(生成/预览/收藏/技能)
后端:
- 新增 task_kind=html_page 任务类型,与普通定时任务并存,配置存 execution_context(无需迁移)
- 执行流水线:智能体检索数据 -> 选定模型渲染 HTML -> 清洗/校验 -> 失败自动修复 -> 完整性闸门(缺 doctype/html/head/body/title 或残留脚本即失败) -> 归档 index.html -> 落库
- 新增 scheduled_html_pages / html_page_favorites 表与 store;收藏去重(不可重复)、可删除、收藏时提取可复用风格
- HTML 预览接口(鉴权 + 公开)、收藏接口
- 渲染注入已启用的 HTML 技能(metadata.kind=html_page);全程使用用户所选模型,去除硬编码 GLM-5
- 定时任务跳过 TitleMiddleware,避免标题生成回退默认模型

前端:
- 创建/编辑弹窗增加任务类型与 HTML 配置(页面要求/限制/参考收藏页/模型下拉)
- 运行详情页与公开页 iframe 沙箱预览 HTML;收藏按钮(已收藏禁用)
- 参考收藏页可选可删

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:46:20 +08:00
董宏凯 3b5525a0a5 ai写作修改 2026-06-02 22:55:22 +08:00
董宏凯 fdfb888be9 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-02 21:26:21 +08:00
fengxiangdebaozi 02eee4bd3e 页面修改 2026-06-02 21:25:44 +08:00
董宏凯 b257963551 ai写作 2026-06-02 21:22:31 +08:00
fengxiangdebaozi fb43c036f6 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-02 21:15:53 +08:00
fengxiangdebaozi 9ae6184dc1 业务链条 2026-06-02 21:15:46 +08:00
董宏凯 7efedc7b44 ai写作修改 2026-06-02 20:36:22 +08:00
AdministratorandClaude Opus 4.8 e1e9be8daa fix(auth): token 登录将令牌放入 Authorization: Bearer 头
token 换登录调上游 getTokenInfo 时参数传错:原来发静态头
Authorization: Bearer admin + 令牌放 access_token cookie。上游实际
从 Authorization: Bearer <token> 头读取令牌。

- 鉴权头改为 "Bearer "+前端传入 token,删除 access_token cookie
- service_authorization 配置项弃用(保留兼容,不再参与请求)
- 新增回归测试 test_token_login_upstream_call.py
- 同步更新 AUTH_LOGIN.md / CLAUDE.md / config.yaml 注释 / login_config 文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:33:38 +08:00
Administrator 247ff86f2f Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-06-02 19:06:42 +08:00
吉鑫升 89cb5ab2f1 Merge remote-tracking branch 'origin/master' into ji 2026-06-02 17:40:56 +08:00
吉鑫升andClaude Opus 4.8 c484481964 登录鉴权:新增用户名口令门 + token 换登录两种模式
- config 新增 auth_login 段(login_config.py):
  - username_login.require_password 开启后,用户名直登必须带 ?password=,口令留空默认 123ewq
  - token_login 用上游 token 调 getTokenInfo 换用户名再登录,用户名不在库自动注册;含 verify_ssl/ca_cert_path 处理 HTTPS 自签证书
- auth.py:口令门、/login/token 端点、共享 _get_or_create_user_by_username 与 _fetch_username_from_token;登录类报错文案改中文
- auth_middleware:/login/token 加入免鉴权白名单
- 前端 api.ts/LoginPage:透传 ?password=、?authToken=;修复错误体为对象时显示 [object Object] 的问题
- 文档:backend/CLAUDE.md + docs/AUTH_LOGIN.md 接口说明

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:39:56 +08:00
xuhdandClaude Sonnet 4.6 613cece224 fix(studio): 笔记切换页面不刷新 + 侧边栏错误提示友好化
- WorkspaceRoutes 给 StudioNotebookDetailPage 加 key={notebookId},切换笔记时强制重新挂载
- createSearchSpace 接口失败时解析响应体 detail 字段,直接展示服务端错误信息

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 17:24:08 +08:00
吉鑫升andClaude Opus 4.8 5aa1fdca04 识图与文件上传/预览:可配置视觉模型 + 修复上传与回显
后端:
- 新增可配置识图模型(config: vision.model_name/prompt/max_tokens)。配置后 view_image 委托该模型识别并返回文字结果,文本主模型也能“看图”;未配置时回退到主模型自带视觉能力。新增 deerflow/models/vision.py 与 VisionConfig。
- 修复 Windows 上传全部报 unsafe:open_upload_file_no_symlink 不再强制要求 POSIX 专有的 O_NOFOLLOW,缺失时降级(仍用 lstat/fstat 防符号链接),Windows 追加 O_BINARY。
- 修复 normalize_input 丢弃 human 消息 additional_kwargs:保留 files/client_ts 等,使上传文件能回传到 UI 并被 UploadsMiddleware 识别。
- 约束 agent 行为:上传图片时引导只用 view_image,禁止用 read_file/grep/bash/python 解析图片字节;view_image 工具说明同步加固。
- 启用文档转换(uploads.auto_convert_documents),PDF/Word/Excel/PPT 自动转 Markdown 供 read_file/grep 读取。
- 订正若干模型错误的 supports_vision 标记。

前端:
- 修复上传文件不回显:parseUploadedFiles 不再因 "(empty)" 提前返回。
- 修复 artifact 401 破图:改用带 Authorization 头的 apiFetch 取 blob 显示(新增 useAuthedObjectUrl)。
- 新增上传文件预览弹窗:点击卡片可查看图片/文本/Markdown、Office(读转换后的 .md)、PDF;文件卡片改为可点击。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:43:53 +08:00
xuhdandClaude Sonnet 4.6 73700cca54 fix(ai_writing): 笔记来源素材使用真实检索分数并打印请求日志
- 笔记 chunk 的 relevance_score 改用接口返回的 score 字段(向量相似度/RRF),接口无分数时降级为位置估算
- _search_notebook 增加 INFO 级日志,输出每次请求的 payload 和响应 status/body,便于调试

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:31:57 +08:00
ZyGuo 3c92e87156 feat: 标题去除特殊tag 2026-06-02 16:24:01 +08:00
ZyGuo 0122d3e361 feat: 生成中状态 2026-06-02 16:18:00 +08:00
董宏凯 30410b0a93 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!77
2026-06-02 07:19:19 +00:00
fengxiangdebaozi 5c5632967c 样式修改 2026-06-02 15:16:39 +08:00
fengxiangdebaozi d872f4e177 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-02 14:11:24 +08:00
fengxiangdebaozi 677faab273 代码提交 2026-06-02 11:50:22 +08:00
董宏凯 d40ee80680 Merge branch 'zhujingyi' into 'master'
Zhujingyi

See merge request knowledge-base-bj/deerflow-server!76
2026-06-02 03:49:25 +00:00
董宏凯 79dd6b9be0 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!75
2026-06-02 03:48:56 +00:00
ZyGuo 49cfc687f9 fix: 对话区域显示问题修复 2026-06-02 11:26:28 +08:00
fengxiangdebaozi f0ccd6fb09 代码提交 2026-06-02 11:04:25 +08:00
zjy 372d4f7933 'feat:暗色模式样式修改' 2026-06-02 11:01:07 +08:00
fengxiangdebaozi 6621519f11 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-02 10:47:29 +08:00
fengxiangdebaozi 1777a0171b d 2026-06-02 10:47:01 +08:00
fengxiangdebaozi 93e702f9ca fix(frontend): show raw chat messages and improve roundtable draft saves 2026-06-02 10:45:20 +08:00
fengxiangdebaozi 0d8cdf3afb Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-01 22:17:44 +08:00
fengxiangdebaozi fa41d620fd 字号调整 2026-06-01 22:17:39 +08:00
AdministratorandClaude Opus 4.8 93543919d4 本地启动环境修复与配置
- config.yaml: checkpointer 连接串改为相对路径(原为他机绝对路径,本地无法创建)
- frontend-web: 补充缺失的 dayjs 直接依赖(lockfile 已有,package.json 缺失)
- .gitignore: 忽略 backend/uv.lock 并取消其跟踪

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 20:29:29 +08:00
董宏凯 001b04ac5a Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-01 20:00:46 +08:00
董宏凯 417ec01fb2 代码修改 2026-06-01 20:00:05 +08:00
fengxiangdebaozi 0c5938a5a3 代码代码修改 2026-06-01 19:54:45 +08:00
ZyGuo 2e6eff43ad feat: 去掉demo按钮 2026-06-01 18:13:23 +08:00
董宏凯 e9c56f8635 部署 2026-06-01 17:33:40 +08:00
ZyGuo 5f88efb2c9 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy_iframedf 2026-06-01 17:29:30 +08:00
ZyGuo e6af567206 iframe 页面优化 2026-06-01 17:27:57 +08:00
吉鑫升 43cbcd86be Merge remote-tracking branch 'origin/master' into ji 2026-06-01 17:11:08 +08:00
董宏凯 44931f8205 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!74
2026-06-01 09:01:23 +00:00
吉鑫升 bc7622b0bd Merge remote-tracking branch 'origin/master' into ji 2026-06-01 16:59:56 +08:00
吉鑫升andClaude Opus 4.8 c860ee85f7 技能展示适配:支持多结果集(result sets)渲染
- 后端 extensions_config 新增 SkillDisplayResultSetConfig,SkillDisplayConfig 增加 resultSets 字段
- threads 引用响应增加 resultSetKey / resultSetLabel,runtime/references 适配多结果集
- 前端 skill-display 适配器/运行时/类型支持多结果集渲染,聊天框、消息组、技能展示面板相应更新

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:42:39 +08:00
ZyGuo 16adb90653 feat: 登陆后跳转位置增加iframe判断 2026-06-01 16:13:47 +08:00
fengxiangdebaozi a5d877207e Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-06-01 15:11:14 +08:00
fengxiangdebaozi 685abb6aa9 深蓝色ui 2026-06-01 15:11:05 +08:00
ZyGuo 0922c8299f feat: iframe page 2026-06-01 14:16:13 +08:00
董宏凯 8adb4d5cef 一键部署 2026-06-01 13:53:57 +08:00
ZyGuo c767566cb6 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-06-01 10:29:39 +08:00
ZyGuo a2cc1832f2 chore: ignore 2026-06-01 10:27:31 +08:00
zjy ee8e2b3225 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into zhujingyi 2026-05-29 09:56:12 +08:00
ZyGuo fa65e67069 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-05-27 12:07:07 +08:00
ZyGuo cfd0faf5e7 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-05-27 10:01:35 +08:00
ZyGuo ffee030548 chore: ignore 2026-05-27 10:01:15 +08:00
ZyGuo 56ff310f29 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-05-26 14:40:01 +08:00
ZyGuo 38b59cc198 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into gzy 2026-05-20 13:42:37 +08:00
ZyGuo 043a1aab21 update git ignore 2026-05-19 16:12:12 +08:00
3294 changed files with 605229 additions and 169911 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "md-viewer-frontend",
"runtimeExecutable": "npx",
"runtimeArgs": ["ng", "serve", "--port", "4200"],
"port": 4200,
"cwd": "F:\\angular\\md-viewer\\frontend"
}
]
}
+2 -1
View File
@@ -4,7 +4,8 @@
"PowerShell(uv --version)",
"PowerShell(netstat -ano)",
"PowerShell(Get-NetTCPConnection -LocalPort 8001 -ErrorAction SilentlyContinue)",
"PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })"
"PowerShell(Get-Process -Id 20560 -ErrorAction SilentlyContinue | Select-Object Id,ProcessName,Path,StartTime; try { \\(Invoke-WebRequest -Uri \"http://127.0.0.1:8001/health\" -UseBasicParsing -TimeoutSec 5\\).Content } catch { \"health check failed: $_\" })",
"Bash(curl -s -X POST -H \"Content-Type: application/json\" -d '{\"username\":\"demo\"}' http://localhost:8001/api/v1/auth/login/username)"
]
}
}
+20
View File
@@ -0,0 +1,20 @@
import os
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
"119.254.86.251",
port=22322,
username="root",
password=os.environ["TARGET_SSH_PASSWORD"],
timeout=15,
auth_timeout=15,
banner_timeout=15,
)
_, stdout, stderr = client.exec_command(os.environ["TARGET_SSH_COMMAND"], timeout=30)
print(stdout.read().decode(errors="replace"), end="")
print(stderr.read().decode(errors="replace"), end="")
client.close()
+35
View File
@@ -0,0 +1,35 @@
# Never send source-control history, local dependencies, runtime state, or
# credentials to the Docker daemon. The offline images are built from the
# repository root by deploy/1panel-offline/Dockerfile.*.
.git
.runtime
.history
tmp
dist
# Credentials and environment-specific configuration are supplied at runtime
# through the offline delivery package, never copied into an image layer.
**/.env
**/.env.*
offline-backend-20260512/backend/.deer-flow
offline-backend-20260512/backend/logs
offline-backend-20260512/backend/docker-data
offline-backend-20260512/backend/dist
offline-backend-20260512/backend/wheelhouse
offline-backend-20260512/backend/offline-wheels
offline-backend-20260512/backend/.venv
offline-backend-20260512/backend/.pytest_cache
offline-backend-20260512/backend/.ruff_cache
# Reproducibly install frontend dependencies inside the image instead.
frontend-web/node_modules
frontend-web/dist
frontend-web/.vite
frontend-web/.offline-pack-build
frontend-web/.offline-extract-test
frontend-web/node_modules-offline-*.tar.gz
# Editor and operating-system metadata.
**/__pycache__
**/*.py[cod]
**/.DS_Store
+144 -6
View File
@@ -1,7 +1,11 @@
# Workspace/editor
.vscode/
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
!.vscode/extensions.json
!.vscode/settings.json
frontend-web/.vscode/
node_modules/
# Claude Code local-only settings (per-user permissions, not shared)
.claude/settings.local.json
@@ -53,6 +57,7 @@ offline-backend-20260505/bundle/stop_backend.sh
# offline-backend-20260512 runtime artifacts (large files, local config, data)
offline-backend-20260512/.env.example
offline-backend-20260512/.env
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260512/SHA256SUMS
offline-backend-20260512/config.yaml.example
@@ -64,7 +69,10 @@ offline-backend-20260512/wheelhouse/
offline-backend-20260512/backend/.deer-flow/data/*.db
offline-backend-20260512/backend/.deer-flow/data/*.db-*
offline-backend-20260512/backend/.deer-flow/*
offline-backend-20260512/start_backend.sh
offline-backend-20260512/backend/.env
offline-backend-20260512/backend/.env.local
offline-backend-20260512/backend/.extensions_config.json.lock
offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh
@@ -80,9 +88,139 @@ offline-backend-20260512/backend/skill_evolution.json
skills/
hermes-agent-main/
.gitignore
offline-backend-20260512/backend/tests/*
offline-backend-20260512/backend/tests/*
!offline-backend-20260512/backend/tests/test_large_file_safety.py
# Keep answer-level feedback persistence, scoring, resolver, and migration regressions versioned.
!offline-backend-20260512/backend/tests/test_answer_feedback.py
# Keep wx common-agent category configuration regressions versioned.
!offline-backend-20260512/backend/tests/test_wx_common_agents.py
# Keep the openGauss URL/dialect/schema compatibility regression coverage versioned.
!offline-backend-20260512/backend/tests/test_gauss_database.py
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
# Keep formal Word OOXML and embedded-font regression coverage versioned.
!offline-backend-20260512/backend/tests/test_word_export.py
# Keep generated-file library aggregation and filtering regression coverage versioned.
!offline-backend-20260512/backend/tests/test_artifact_library.py
# Keep Web App site artifact (multi-page site) regression coverage versioned.
!offline-backend-20260512/backend/tests/test_web_app_sites.py
# Keep the model configuration regression coverage versioned even though
# this deployment repository otherwise ignores newly added backend tests.
!offline-backend-20260512/backend/tests/test_model_management.py
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
# Keep strategy-selection built-in agent seed and safety-contract regressions versioned.
!offline-backend-20260512/backend/tests/test_strategy_selection_seed.py
# Keep fixed-question exact-match and streaming regression coverage versioned.
!offline-backend-20260512/backend/tests/test_fixed_questions.py
# Keep configurable es_query system-prompt routing regression coverage versioned.
!offline-backend-20260512/backend/tests/test_es_query_routing_prompt.py
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
# Keep assistant-source snapshot publication atomic across re-import jobs.
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
# Deep-research reliability regression coverage.
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
!offline-backend-20260512/backend/tests/test_menu_overrides.py
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
!offline-backend-20260512/backend/tests/test_upload_policy.py
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
!offline-backend-20260512/backend/tests/test_artifact_preview.py
# Independent enterprise-research persistence, HTTP and race regressions.
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
# Roundtable coordinator/seat capability isolation regressions.
!offline-backend-20260512/backend/tests/test_roundtable_role_boundary.py
# Workflow Studio: schemas / definitions / engine / run-lifecycle regressions
# plus the concurrency remediation suite (atomic resume, draft CAS, publish
# counter, event seq) — required in CI per docs/WORKFLOW_STUDIO_BACKEND_REMEDIATION_ZH.md P1-06.
!offline-backend-20260512/backend/tests/test_workflow_schemas.py
!offline-backend-20260512/backend/tests/test_workflow_definitions.py
!offline-backend-20260512/backend/tests/test_workflow_engine.py
!offline-backend-20260512/backend/tests/test_workflow_runs.py
!offline-backend-20260512/backend/tests/test_workflow_concurrency.py
!offline-backend-20260512/backend/tests/test_workflow_schema_rebuild.py
# Writing-mode first-report pipeline enforcement regressions.
!offline-backend-20260512/backend/tests/test_writing_mode_report_middleware.py
!offline-backend-20260512/backend/tests/test_writing_rewrite_prompt.py
# LLM call timeout retry classification regression coverage.
!offline-backend-20260512/backend/tests/test_llm_error_handling_middleware.py
# Adaptive context recovery, large-payload and real-adapter streaming regressions.
!offline-backend-20260512/backend/tests/test_context_recovery.py
!offline-backend-20260512/backend/tests/test_context_recovery_transport.py
!offline-backend-20260512/backend/tests/test_summarization_multiturn.py
# Writing-mode pipeline conversation-material regressions.
!offline-backend-20260512/backend/tests/test_deep_research_report_tool.py
# Report collaboration (RC-BE-000–018) regression coverage.
!offline-backend-20260512/backend/tests/test_report_collaboration_*.py
!offline-backend-20260512/backend/tests/test_ensure_orm_columns_sync.py
deploy
.history
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
!frontend-web/src/components/workspace/skills/
# Cloned third-party repos (local only)
git-clone/
# Repo-root offline dependency pack (local only)
/dist/
# Frontend offline pack working directories
frontend-web/.offline-pack-build/
frontend-web/.offline-extract-test/
frontend-web-node_modules-offline-*.tar.gz
# Local dependency caches and backend scratch archives
/.pnpm-store/
/offline-backend-20260512/backend/backend*.zip
# Local offline bundles and scratch tooling
/*.tar.gz
/offline-backend-20260512/*.zip
/tmp/
.codegraph
codegraph.mdc
docs/aidev-docs
docs/backups
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
!frontend-web/src/components/workspace/skills/
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
!offline-backend-20260512/backend/skills/
offline-backend-20260512/backend/skills/*
!offline-backend-20260512/backend/skills/public/
offline-backend-20260512/backend/skills/public/*
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
# uv lockfile (not tracked per project preference)
offline-backend-20260512/backend/uv.lock
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
# Local-only scratch directory (not committed)
0622/
# Python bytecode cache
__pycache__/
# misc
.DS_Store
+6
View File
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-python.python",
"ms-python.debugpy"
]
}
+66
View File
@@ -0,0 +1,66 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "DeerFlow: Backend FastAPI (8001)",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app.gateway.app:app",
"--host",
"0.0.0.0",
"--port",
"8001"
],
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
"python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
"envFile": "${workspaceFolder}/offline-backend-20260512/.env",
"env": {
"PYTHONPATH": "${workspaceFolder}/offline-backend-20260512/backend;${workspaceFolder}/offline-backend-20260512/backend/.deer-flow/python-vendor",
"GATEWAY_CORS_ORIGINS": "http://localhost:5174,http://127.0.0.1:5174,http://localhost:8080,http://127.0.0.1:8080",
"UV_LINK_MODE": "copy",
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
},
"console": "integratedTerminal",
"redirectOutput": true,
"justMyCode": false,
"subProcess": true
},
{
"name": "DeerFlow: Frontend Vite (5174)",
"type": "node-terminal",
"request": "launch",
"command": "npm.cmd run dev -- --host 0.0.0.0 --port 5174",
"cwd": "${workspaceFolder}/frontend-web",
"env": {
"VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001",
"VITE_LANGGRAPH_BASE_URL": "http://127.0.0.1:8001/api"
},
"serverReadyAction": {
"pattern": "Local:\\s+(https?://[^\\s]+)",
"uriFormat": "%s",
"action": "openExternally"
}
},
{
"name": "DeerFlow: Open Frontend in Chrome",
"type": "node-terminal",
"request": "launch",
"preLaunchTask": "DeerFlow: VS Code Start",
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Start-Process 'http://127.0.0.1:5174/'\"",
"cwd": "${workspaceFolder}"
}
],
"compounds": [
{
"name": "DeerFlow: Full Stack (Backend + Frontend)",
"preLaunchTask": "DeerFlow: Stop Full Stack",
"configurations": [
"DeerFlow: Backend FastAPI (8001)",
"DeerFlow: Frontend Vite (5174)"
],
"stopAll": true
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
"python.terminal.activateEnvironment": true,
"typescript.tsdk": "frontend-web/node_modules/typescript/lib",
"npm.packageManager": "npm"
}
+175
View File
@@ -0,0 +1,175 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "DeerFlow: VS Code Start",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/vscode-start.ps1",
"-OpenBrowser"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "dedicated",
"clear": true
},
"problemMatcher": []
},
{
"label": "DeerFlow: Start Full Stack and Wait",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/start-all-and-wait-frontend.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Start Full Stack",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/start-all.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Stop Full Stack",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/stop-all.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Status",
"type": "process",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}/scripts/status.ps1"
],
"problemMatcher": []
},
{
"label": "DeerFlow: Check Local Dev Environment",
"type": "shell",
"command": "$backendPython = '${workspaceFolder}\\offline-backend-20260512\\backend\\.venv\\Scripts\\python.exe'; if (-not (Test-Path -LiteralPath $backendPython)) { throw 'Backend virtual environment is missing. Run uv sync in offline-backend-20260512/backend first.' }; & $backendPython -c \"import uvicorn; print('Backend Python OK')\"; npm --prefix '${workspaceFolder}\\frontend-web' exec vite -- --version",
"options": {
"shell": {
"executable": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command"
]
}
},
"problemMatcher": []
},
{
"label": "DeerFlow: Install Frontend Dependencies",
"type": "process",
"command": "npm",
"args": [
"ci",
"--no-audit",
"--no-fund"
],
"options": {
"cwd": "${workspaceFolder}/frontend-web"
},
"problemMatcher": []
},
{
"label": "DeerFlow: Frontend Typecheck",
"type": "process",
"command": "npm",
"args": [
"run",
"typecheck"
],
"options": {
"cwd": "${workspaceFolder}/frontend-web"
},
"problemMatcher": "$tsc"
},
{
"label": "DeerFlow: Frontend Build",
"type": "process",
"command": "npm",
"args": [
"run",
"build"
],
"options": {
"cwd": "${workspaceFolder}/frontend-web"
},
"problemMatcher": []
},
{
"label": "DeerFlow: Backend Tests",
"type": "process",
"command": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
"args": [
"-m",
"pytest",
"tests/",
"-v"
],
"options": {
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
"env": {
"PYTHONPATH": ".",
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
}
},
"problemMatcher": []
},
{
"label": "DeerFlow: WeKnora Backend Tests",
"type": "process",
"command": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
"args": [
"-m",
"pytest",
"tests/test_llmwiki_weknora.py",
"-v"
],
"options": {
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
"env": {
"PYTHONPATH": ".",
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
}
},
"problemMatcher": []
}
]
}
@@ -0,0 +1,61 @@
# 深度研究第三轮:检索稳定性修复 + 参考文献确定性 + 页面对齐 AI 写作改造
## A. 两个 bug 的排查结论与修复
### A1. 第二次检索「网络波动」——确认是真问题(DDG 限流 + 错误语义混淆 + 零重试)
**排查结论**(用户判断"确实是网络波动"基本正确,但代码层面放大了它):
- 「网络波动」文案不存在于任何代码中——是收集员 LLM 看到 `{"error": "No results found"}` 信封后自己编的解释
- 真实根因:DDG html 端点对同 IP 快速连续请求限流(403/429 挑战页)→ `DDGSException` → `ddg_search/tools.py::_search_text` 把异常吞成 `[]` → 上层把「被限流」和「真没结果」混成同一个错误信封;工具层完全没有重试
**修复**(`packages/harness/deerflow/community/ddg_search/tools.py`):
1. `_search_text` 对异常加 2 次重试 + 指数退避(约 1.5s / 3s + 抖动)
2. 错误信封区分场景:真无结果 → `no results`;多次尝试仍失败 → `web search failed after N attempts (rate limited or network), 换一组检索词稍后重试`——让模型不再瞎猜「网络波动」
3. 收集员 SOUL.md 行为准则加一条(同步改种子资产 + 已安装副本 `.deer-flow/agents/deep-research-collector/`):搜索返回 error 信封时先换检索词重试一轮,而不是直接告知用户网络异常
### A2. 参考文献缺失——代码确有隐患(当前是模型自发行为,时有时无)
**排查结论**:后端没有任何「参考来源」追加逻辑;四个 runner 的报告 prompt 只要求内联 `[N]` 引用。出现过的参考列表是模型看到上下文头 `[N] 标题(URL)` 后**自发**补写的——弱模型下不可靠,与"信息够不够"无必然关系。
**修复**(prompt 指示 + 确定性兜底双保险):
1. 四个 runner(basic / detailed / deep / multi_agent)的报告 prompt 各加一条:报告最后附「## 参考来源」,按 `[N] 标题. URL` 列出正文实际引用过的来源
2. `basic.py` 新增共享函数 `ensure_reference_section(report, context_list, materials)`:报告缺少参考小节时,解析正文引用的 `[N]` 编号,按压缩上下文顺序生成编号列表追加(无编号时列全部来源,封顶 20 条;已有参考小节则不重复追加)。在各 runner 最终 `write_text("report.md", ...)` 前调用(约 7 个调用点,跳过零材料 fallback)
3. 新增测试 `tests/test_deep_research_references.py`:helper 单测(正常追加/编号映射/不重复追加/无 URL 来源)+ runner 集成断言
## B. 页面改造(核心需求):左列复用 AI 写作对话形态,右列改真沙箱
### B1. 左列 = AI 写作式对话收集面板(仿 WritingSetupChat)
- **空状态欢迎屏**(无消息时隐藏头部,展示欢迎卡片,WritingWelcome 式)+ 有消息后 `MessageList`(沿用现有绑 collector 线程的接线)+ 首 token 前「正在整理…」占位条
- **输入框换成白卡片 PromptInput 套件**(与 AI 写作完全同款组件):卡片内一行「当前对话模型」Select(沿用 ModelNameSelect)+ 高级配置入口;下方 PromptInputTextarea + PromptInputFooter + PromptInputSubmit(`status` ready/streaming,流式中变停止按钮)。经典模式入口保留在高级配置对话框
- **配置卡片进对话流**(对标 AI 写作「识别出意图后出现表单」):
- `message-list.tsx` 最小扩展(约 15 行):新增分组 `assistant:research-approval`(识别 `additional_kwargs.setup_research_approval`)+ 可选 prop `researchApprovalSlot?: ReactNode`;不传 slot 或无该载荷时零行为变化——普通对话/AI 写作完全不受影响
- workbench:收集回合结束(awaiting_report,即"意图识别完成")时向 `displayThread` 尾部注入一条虚拟 ai 消息(WritingSetupChat 的 displayThread 同款手法),slot 传现有 `ReportConfigCard` → 卡片作为助手下一轮出现在对话流里
- 写作进度卡、完成卡、摘要气泡、追问列表保留在列表下方卡片区(报告正文移到右侧沙箱)
### B2. 右列 = 工作区真沙箱(Step2SandboxLayout 的 stub 线程模式)
- 新建 `deep-research/ResearchSandboxPanel.tsx`(约 100 行,含关闭/重开)+ `useReportSandbox.ts` hook(仿 `useStep3Report` 的 stub 机制):
- threadStub:`messages = [{type:'ai', tool_calls:[{name:'write_file', args:{path:'/mnt/user-data/outputs/report.md', content: <流式报告>}}]}]`,`isLoading = job 运行中 && phase !== 'summarizing'`
- `report_delta` 首帧 → `openArtifact(write-file: 虚拟URL)` 自动展开右栏;`run.report` 每帧 upsert stub(带防抹空守卫);刷新/切换会话用 `session.reportMarkdown` 重建
- `ArtifactFileDetail` 自带能力全部免费获得:**流式期 code 视图自动滚底 → 报告写完 isLoading 翻 false 自动切 preview + 脉冲提示**(正是「生成完直接切预览」)、Streamdown 渲染的 Markdown(比现在的好看)、另存 Word/复制/下载、默认走展示层脱敏
- 布局:ResizablePanelGroup chat 60 / sandbox 40(对齐 ChatBox/圆桌),沙箱可关闭、头部按钮重开
- **删除自定义 `ResearchSandbox` 组件**;legacy 会话与经典模式共用同一右栏(`run.report` 数据源相同,行为一致)
- 已知小限制(可接受):报告配图模式下沙箱内图片可能无法经 artifacts API 解析(配图默认关闭;真需要时后续把产物落 thread artifacts)
### B3. 验证
- 后端:`PYTHONPATH=. uv run --no-sync pytest tests/ -k deep_research -q --ignore=tests/test_notification_templates.py` 全绿 + ruff;改完配置/SOUL 后 touch 触发 dev 服务重载
- 前端:`pnpm typecheck` 零新增错误(基线 51)
- 手动走查:连续两次检索不再误报、弱模型报告末尾必带参考来源、页面全流程(欢迎屏 → 对话收集 → 流内配置卡 → 沙箱流式 → 自动预览 → 摘要/追问)
## 涉及文件
**后端**:`community/ddg_search/tools.py`、`deep_research/runners/{basic,detailed,deep,multi_agent}.py`、收集员 SOUL.md(种子 + 已安装副本)、`tests/test_deep_research_references.py`(新)
**前端**:`components/workspace/messages/message-list.tsx`(可选 slot 分组)、`pages/deep-research/DeepResearchWorkbench.tsx`(大改)、`pages/deep-research/ResearchSandboxPanel.tsx`(新)、`pages/deep-research/useReportSandbox.ts`(新)
## 明确不动
- AI 写作 / 普通对话现有组件行为(message-list.tsx 仅加可选分组,缺省零变化)
- 报告写作管线的写作逻辑本身(prompt 加一条要求 + 末尾确定性追加参考小节,均为附加式)
- 深度研究后端会话/任务/事件模型(本轮不动)
@@ -0,0 +1,113 @@
# Web App 产物模式:多页面站点 / 流程图 / 地图(可降级 + 独立页面风格)
## 总体架构
让智能体在沙箱里产出**完整多页面站点**(site.json 清单 + src/pages + public 数据),平台统一构建(预置 node_modules,永不 npm install)与受控静态托管,前端新注册一个**风格独立**的「站点预览」路由页。
**两条铁律(本次新增的核心原则)**:
1. **零侵入**:新页面不复用现有工作台风格/布局组件,走独立路由 + 独立样式表;对已有代码的修改仅限「一个带开关的入口按钮」。
2. **可降级**:沙箱/模板/node/构建链任何一环部署失败,Web App 功能整体退回"不可用"状态并优雅提示,智能体回退到现有单页 HTML 模式,**现有功能零影响**。
```
智能体(技能 web-app)
0. 检查可用性:/mnt/web-app-template 缺失 → 回退现有单页 HTML 模式(行为与今天完全一致)
1. scaffold → outputs/my-site/(排除 node_modules/dist)
2. 写 src/pages/*.tsx、public/data.geojson、编辑 site.json
3. build_site.py build → 软链/junction 预置 node_modules → vite build → dist/(失败自修复 ≤3 次)
4. present_files site.json
平台(Gateway,全部能力探测 + 开关保护)
GET /api/web-app/capability # 可用性探测(配置/模板/node_modules/node 四项检查)
GET /api/threads/{tid}/sites # 站点列表 + 构建状态
POST /api/threads/{tid}/sites/{name}/build # 触发构建
GET /api/threads/{tid}/sites/{name}/build-status
POST /api/threads/{tid}/sites/{name}/preview-url → 签发短时效签名 URL
GET /api/public/site-preview/{token}/{path} # 静态托管 dist/
前端(独立路由 + 独立风格)
#/sites/:threadId/:siteName # SiteStudioPage:全新视觉风格,无现有 chrome
产物面板 site.json →「打开站点预览」按钮(仅 capability 可用时渲染)
```
已确认决策:混合构建(技能自修复 + 平台按钮兜底);预览工作台先行;GeoJSON 优先 + 可配置底图。
---
## 一、页面风格:独立设计系统(不用现有风格)
新页面 `SiteStudioPage` 做成**深色"预览工作室"**风格,视觉上与现有工作台完全区隔:
- **独立路由**:直接注册在 `App.tsx` 顶层(`/share/:shareCode`、`/login` 同款先例),**不进** `PageLayout`/`WorkspaceLayout`/`Header`/`PageSidebar`——零现有 chrome 继承,零布局耦合。
- **独立样式**:新建 `src/pages/site-studio/site-studio.css`(自建 design tokens:深空灰底 `#0f1117` 系、靛蓝/青渐变强调色、玻璃拟态面板、细腻边框与阴影、系统字体栈),所有类名 `site-studio-` 前缀,**只被该页面 import**,不碰 tailwind 配置、全局 CSS、主题 provider。
- **布局**:
- 顶部悬浮工具栏:站点标题 + 构建状态胶囊(成功/构建中/失败三态,带呼吸灯动画)+ 设备切换分段控件(桌面/平板/手机)+ 刷新/重新构建/新窗口打开;
- 左侧窄导航栏:site.json 的页面列表(图标 + 标题,点击设 iframe hash 切页),可折叠成纯图标模式;
- 中央:**浏览器壳**造型(圆角 + 顶部仿 URL 地址条 + 红黄绿窗控点缀)内嵌 iframe,设备切换时宽度平滑过渡动画;
- 底部可折叠**构建控制台**(等宽字体日志、错误红色高亮、构建时长);
- 加载/失败/未启用态都有设计过的插画式空状态(纯 CSS/SVG,不引组件库新依赖)。
- 登录守卫:轻量检查 stored auth,缺失跳 `#/login`(复用既有跳转工具,不动鉴权体系)。
## 二、降级与故障隔离矩阵(部署失败不影响已有功能)
| 故障点 | 行为 | 对现有功能影响 |
|---|---|---|
| config `web_app.enabled: false`(默认关) | capability 返回不可用;入口按钮不渲染;技能第 0 步探测失败即回退单页 HTML | 无 |
| 模板目录/node_modules 未预置(发版遗漏) | 同上:capability 四项检查(配置/模板/依赖/node 二进制)逐项报告原因 | 无 |
| sandbox.mounts 加了模板挂载但目录缺失 | 已核实 `local_sandbox_provider.py:81-90` 对不存在宿主路径**静默跳过仅告警**;Aio provider 同样按"存在才挂"实现 | 无(沿用既有容错语义) |
| vite build 失败/超时 | 子进程隔离(`asyncio.to_thread` + 超时强杀 + 每站点并发锁),错误收进 JSON;预览页显示日志;智能体读报错自修复 | 无(不进 gateway 请求主链路) |
| 签名 URL 过期/伪造 | 403 JSON,页面自动重签 | 无 |
| 前端新页面任何异常 | 独立路由独立样式,最坏就是该页白屏,error boundary 兜底 | 无 |
| 产物面板入口按钮 | 唯一改动点:capability 请求失败/不可用 → 按钮不渲染,**原 UI 一字不变** | 无 |
| 新 router 注册 | 纯增量 include_router,异常全部在路由内转结构化响应 | 无 |
技能回退语义(关键):SKILL.md 第 0 步强制探测 `ls /mnt/web-app-template/scripts/build_site.py`——探测失败时明确指示"改用现有方式产出单个 xxx.html",保证部署不完整时智能体产物质量与今天一致。
## P1 站点模板(`offline-backend-20260512/backend/templates/web-app-template/`,源码入库,node_modules gitignore 由发版仓库预置)
- Vite + React + react-router-dom(**HashRouter**:静态托管路径下无需服务端 fallback)+ **纯 CSS design tokens**(不用 Tailwind v4,规避 oklch 在 chrome100 老内核失效)+ build target es2018。
- 依赖:react 系、@xyflow/react、maplibre-gl、echarts、mermaid、pmtiles、vite/@vitejs/plugin-react(lockfile 锁死版本)。
- 内置组件:`AppShell`(读 site.json 导航)、`FlowCanvas`(xyflow 流程图封装,自动分层布局)、`GeoMap`(**默认无底图模式**:背景 + GeoJSON 点线面离线开箱即用;site.json 可配 tilesUrl/PMTiles)、`ChartPanel`(echarts)、`MermaidBlock`(静态图)。
- `router.tsx` 用 `import.meta.glob('../pages/*.tsx')` 自动注册路由——智能体丢页面文件即生效,零手动接线。
- `site.json`:`{ title, description, nav[], map:{tilesUrl?, tilesType?, pmtiles?}, theme? }`。
- Makefile 加 `make webapp-template`(开发机一次性 pnpm install);发版仓库构建脚本负责镜像内预置 node_modules(文档写明)。
- config.yaml `sandbox.mounts` 增模板只读挂载(`templates/web-app-template → /mnt/web-app-template`,缺失自动跳过已验证);AioSandboxProvider `_get_thread_mounts` 小扩展:同样"存在才挂"地补读 config 自定义 mounts。
## P2 构建脚本 + 技能包(`skills/public/web-app/`)
- `SKILL.md`:触发词(多页面网站/站点/web app/带流程图或地图的网页);工作流 = ①可用性探测(失败即回退单页 HTML)②scaffold ③配置 site.json ④写页面 ⑤数据放 public/ ⑥build(失败读 JSON 报错修复重试 ≤3)⑦present_files site.json ⑧引导点「站点预览」;硬约束:不手写 dist、一律 `/mnt/user-data/...` 虚拟路径。
- `scripts/build_site.py`(纯标准库):`check`(能力探测 JSON)/ `scaffold`(copytree 忽略 node_modules、dist)/ `build`(定位 node → 软链 node_modules 进站点:Linux symlink、Windows `mklink /J` junction 免管理员 → `node vite.js build` → 结构化 JSON 输出,180s 超时)/ `status`。
- 模板定位顺序:env `DEERFLOW_WEBAPP_TEMPLATE` → `/mnt/web-app-template`(沙箱内)→ 后端相对路径(宿主侧)。
## P3 后端:纯引擎 + 路由(全部能力开关保护)
- `packages/harness/deerflow/webapp/`(纯引擎,不 import app.*,对齐 `address_scan.py` 先例):站点发现(outputs/* 下 site.json + dist 状态)、构建执行(子进程 + 锁 + 超时)、HMAC token(`tid|site|exp`,密钥首用生成持久化 `{base_dir}/.web-app-secret` 0600)。
- `app/gateway/routers/sites.py`(app.py 注册):
- `GET /api/web-app/capability`:enabled + 模板 + node_modules + node 四项检查,逐项给原因(前端与技能共用);
- 列表/build/build-status/preview-url:`@require_permission("threads", "read", owner_check=True)`,能力不可用时返回结构化 `{"available": false, "reason"}` 而非 5xx;
- `GET /api/public/site-preview/{token}/{path}`:验签 + 过期 → `aresolve_thread_virtual_path` 且**限定站点 dist/ 根**(复用遍历防护)→ 正确 MIME(.js/.mjs→text/javascript、.geojson→application/json、.wasm)→ **HTML inline** + `CSP: default-src 'self' data: blob:; worker-src 'self' blob:`(maplibre worker 需要)+ `X-Content-Type-Options: nosniff` + `Cache-Control: no-cache` + ETag(mtime)。
- config.yaml + `deerflow/config/configuration.py` 加 `web_app:`(enabled **默认 false**、build_timeout、token_ttl、template_dir、node 覆盖)——与 `workflows` 同款总开关模式。
- 测试 `tests/test_web_app_sites.py`:capability 降级路径、token 签发/过期/伪造、路径穿越拒绝、MIME、站点发现(构建子进程 mock)。
## P4 前端
- `src/core/sites/api.ts`:capability/sites/build/status/mintPreviewUrl + TanStack Query hooks(照 `light-apps.ts` 模式)。
- `src/pages/site-studio/SiteStudioPage.tsx` + `site-studio.css` + `App.tsx` 注册顶层路由 `/sites/:threadId/:siteName`(独立风格见第一节;iframe `sandbox="allow-scripts allow-forms allow-popups"` 无 allow-same-origin,延续既有安全契约;token 过期自动重签;设备切换;重新构建 → 轮询 build-status → cache-bust 刷新)。
- `artifact-file-detail.tsx` **唯一既有文件改动**:产物路径以 site.json 结尾且 capability 可用时渲染「打开站点预览」按钮(navigate 到新路由);capability 请求失败/超时一律不渲染,原 UI 不变。
- token URL 构造等纯函数补单测;`pnpm typecheck` 通过。
## P5 测试与文档
- 后端 `make test` + ruff;前端 typecheck + 单测;`build_site.py` 本机真实跑通 scaffold→build→静态服务器自测 dist。
- 文档:AGENTS.md 增「Web App 产物」小节(含降级矩阵);发版文档补 node_modules 预置说明。
## 安全边界
短时效签名 URL(默认 2h)只覆盖该站点 dist/ 静态读;dist/ 根限定 + 复用 `aresolve_thread_virtual_path` 遍历防护;HTML inline 仅在 site-preview 路径开放(不影响 artifacts 接口既有强制 attachment);CSP + nosniff + iframe sandbox 三重纵深。
## 二期(本期不做)
源码编辑 + 保存即重建;跨对话站点 gallery;PMTiles 底图资产打包;`visualizations/` 内联图表(进行中,互补不冲突)的底图配置统一(其 map-renderer.tsx 硬编码 Google 瓦片属离线隐患,另行处理)。
## 风险
- Windows 开发机 junction 链接免管理员,node 前端机必有;生产镜像已含 nodejs+npm。
- Aio provider 自定义 mounts 扩展是小改动,按"存在才挂"实现即可,不影响现有挂载。
- dist/ 出现在产物目录扫描(文件噪音)——一期保持现状,二期优化。
+119
View File
@@ -0,0 +1,119 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
This is a customized offline deployment of **DeerFlow** — a LangGraph-based AI super agent platform. The repo contains two sub-projects:
- `offline-backend-20260512/backend/` — Python backend (FastAPI Gateway + LangGraph agent runtime)
- `frontend-web/` — Vite + React 19 frontend (TypeScript, pnpm)
Managed together via shell scripts in `scripts/`.
## Offline Release Repository
Linux AMD64 离线发版脚本、wheelhouse、`deploy.env` 模板和部署文档统一维护在
`knowledge-base-bj/deerflow-offline-deployment`,不要把离线镜像或发版资产提交到
本业务源码仓库。正式发版从该仓库执行 `scripts/build-latest-master.sh`,脚本会读取
本仓库最新 `master` 的干净源码快照。
## Commands
### Full Application (from repo root)
```bash
./scripts/start-all.sh # Start backend + frontend as background daemons
./scripts/stop-all.sh # Stop both
./scripts/restart-all.sh # Restart both
./scripts/status.sh # Check running status
```
Logs and PID files are written to `.runtime/logs/` and `.runtime/pids/`.
### Backend Only (from `offline-backend-20260512/backend/`)
```bash
make install # Install Python dependencies (uv)
make dev # Run Gateway API with hot-reload on port 8001
make gateway # Run Gateway API without reload
make test # Run all tests: PYTHONPATH=. uv run pytest tests/ -v
make lint # Lint with ruff
make format # Format with ruff
```
Run a single test file:
```bash
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
```
### Frontend Only (from `frontend-web/`)
```bash
pnpm install # Install dependencies
pnpm dev # Start Vite dev server on port 5174
pnpm build # Production build
pnpm typecheck # TypeScript type checking (tsc --noEmit)
```
## Service Ports
| Service | Port | Notes |
|---------|------|-------|
| Backend (Gateway API) | 8001 | FastAPI + LangGraph runtime |
| Frontend (Vite) | 5174 | React SPA |
Frontend connects to backend via env vars `VITE_BACKEND_BASE_URL` and `VITE_LANGGRAPH_BASE_URL` (injected by `scripts/start-frontend.sh`).
## Architecture
### Backend
The backend (`offline-backend-20260512/backend/`) is split into two layers:
- **`packages/harness/deerflow/`** — publishable agent framework (`deerflow.*` imports): LangGraph lead agent, middleware chain, sandbox, tools, MCP, memory, models, skills. **Never imports from `app.*`.**
- **`app/`** — application layer (`app.*` imports): FastAPI Gateway routers, IM channel integrations (Feishu, Slack, Telegram, DingTalk).
The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph entry point. It wraps a middleware chain (~18 middlewares assembled in strict order) around LLM calls, handling: thread isolation, sandbox lifecycle, uploads, memory, plan mode, vision, subagent delegation, and loop detection.
**Middleware order matters** — middlewares are appended in a fixed sequence; `ClarificationMiddleware` must always be last. See `offline-backend-20260512/backend/AGENTS.md` for the full ordered list.
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `AGENTS.md`/`CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
### Frontend
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **Routing**: React Router v7; top-level routes in `src/pages/PageRoutes.tsx`, workspace sub-routes in `src/pages/WorkspaceRoutes.tsx`
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat`, sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` reuses the engine (`AIWritingContext` + `useStream` + `AIWritingDraftPanel`); **idle keeps the classic setup layout** (`WritingSetupChat` + collapsible form) and switches to the chat workbench once writing starts; left panel `components/ai-writing-chat/WritingChatPanel.tsx` = **unified chronological feed** (`ChatFlowTimeline.tsx`, chat bubbles anchor-interleaved into the progress timeline so triggered events render below the user's message) + button-only intervention cards (`ChatInterventionCards.tsx`, inline textareas removed) + bottom unified smart composer. Intent resolution: frontend fast path + `POST /api/ai-writing/sessions/{id}/intent` → `deerflow/agents/ai_writing/intent_router.py` (per-pause action whitelist, high-cost confidence gates, payload cleaning); supports QA bypass, clarify follow-ups, control intents, and material-stage auto re-search + pending-intent resume. Tests `tests/test_ai_writing_intent.py`; plan doc `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
- **菜单管理 (Menu Management, admin defaults + personal layouts)**: `MenuManagementPage` (`pages/MenuManagementPage.tsx`) is available from「设置和更多」and the global Header account dropdown. Administrators edit the shared default menu; ordinary users edit only their own full/compact menu snapshot. For users with a saved snapshot, known node ids keep the personal `label`/`disabled`/`parentId`/`sortOrder` (or compact placement/order), while administrator defaults for node ids absent from that snapshot flow through—so newly-added code menus and iframe light apps follow the current default without overwriting older personal choices. Temporarily absent dynamic nodes are preserved when an unrelated personal edit is saved;「恢复默认」deletes only that user's snapshot. The static tree (`core/page-layout/sidebar-menu.ts`) remains the source of icons/routes and administrator defaults remain sparse per-node deltas. Merge/snapshot helpers live in `core/page-layout/menu-overrides.ts` and `compact-menu-overrides.ts`; full-menu writes carry the complete effective parent hierarchy for cycle/strict three-level validation while persisting only the sparse admin delta or full personal snapshot. Backend `/api/menu-overrides` uses `menu_overrides` (default) + `user_menu_override_configs` (per-user snapshot); `/api/compact-menu-overrides` uses `compact_menu_overrides` (type `A` / `B` defaults) + `user_compact_menu_override_configs` (per-user/type snapshots). Each website selects its compact type via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`; `CompactToolbox` renders `toolbox` items.
- **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答经 Gateway `POST /api/sentiment-agent/stream` 转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块 `src/core/sentiment-agent/`。
- **简洁模式用户引导 (Compact-mode onboarding tour)**: 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React `Guide` 组件驱动的分步引导。组件 `components/workspace/compact-mode-guide.tsx`(挂在 `ChatPage`):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的 `data-compact-guide` 标记定位(`workspace-nav-chat-list` / `recent-chat-list` / `workspace-nav-menu` / `appearance-controls` / `custom-prompt-menu.tsx` 的 `CustomPromptBar`(提示词栏,仅新对话落地态渲染)与其 `ManagePromptsDialog`(`prompt-dialog` 锚点)/ ChatPage 输入框容器),**启动时按目标是否存在过滤步骤**(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。**提示词管理弹窗步骤是动态目标**:弹窗平时不渲染,引导切到该步前先广播 `PROMPT_DIALOG_GUIDE_EVENT`(`custom-prompt-menu.tsx` 导出)让 `CustomPromptBar` 打开 `ManagePromptsDialog` 并**接管**(`owner: true` 期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以 `owner: false` 收回弹窗,4 秒打不开则跳过该步防卡死。**TDesign Guide × Radix 模态互操作**:Radix 模态弹窗打开时会把 `body` 置 `pointer-events:none`(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),`styles/tdesign-theme.css` 末尾对 `.t-guide__overlay/__highlight/__popup/__wrapper` 显式 `pointer-events:auto` 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage `deerflow.compact-guide-finished` 记忆(浏览器级);**中途刷新/离开不记忆,下次进入还会再演示**。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播 `compact-guide:start` 窗口事件直接启动,否则写 sessionStorage 请求并导航 `/chats/new` 由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。`eligible` 排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。
- **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
- **技能上下线 (Skill Availability, owner/admin)**: the global `Skill.enabled` flag is the online/offline switch. Taking a skill offline persists `skills.<name>.enabled=false` in `extensions_config.json` but does **not** delete its directory or remove it from existing agent/workflow configurations; the row remains visible and bringing it online restores those references. Administrators may toggle every skill from 「技能管理」; ordinary users may toggle only their own non-built-in `custom` skills from 「技能 → 技能列表 → 我的」. Offline skills must be absent from every model-facing skill catalogue (`lead` prompt, `search_skills`, `skill_list`, `skill_view`, subagents, AI writing/deep research/roundtable and Workflow run-start validation), from agent skill pickers, and from direct sandbox access: `read_file`/`ls`/`glob`/`grep`/`bash` reject offline, unknown, root-wide and category-wide `/mnt/skills` paths. The backend mutation is `PUT /api/skills/{name}`, authorizes either an administrator or the persisted owner of the target custom skill, patches the raw document so MCP placeholders and unrelated extension fields survive, uses atomic replacement when supported plus a Docker single-file-bind fallback, and refreshes the skill prompt cache. Launchers keep the runtime extensions file in a writable persisted directory. Existing agent/strategy-agent references stay visible as 「已下线」 and may be removed but not newly selected.
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id, or action id when `idKind:"action"` — same selector as url buttons, xdfx only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url **or host** button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
- **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ 每次检索 JSON 原样落盘 `/mnt/user-data/workspace/hits/qN.json` → **一条命令** `task-report-build`(group `all`)技能函数转换成 14 类 `task-reports.json`(模型禁止手写该 JSON;分页技能 `task-report-{enemy,our,env,judge}` 仅用于事后补跑单页;五者都是共享引擎 `task-report-lib/convert_lib.py` 的瘦 CLI,`resolve_hit_files` 对虚拟绝对路径读空的情况自动回退到相对写法并在 `warnings` 里列出尝试过的路径,免得智能体自己写脚本排查)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` → 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "Line truncated
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
- **Web App 产物模式(多页面站点 / 流程图 / 地图,默认关闭、全链路可降级)**: 让智能体产出**完整站点**而非单个 xxx.html。沙箱技能 `skills/public/web-app` 用模板(`backend/templates/web-app-template`,经 `config.yaml sandbox.mounts` 只读挂载到 `/mnt/web-app-template`,目录缺失自动跳过)脚手架到 `outputs/<site>/`(site.json 清单 + `src/pages/*.tsx` 文件名即路由 + `public/` 数据),再跑 `scripts/build_site.py build` 用**预置 node_modules**(symlink/junction 链接,离线永不 npm install)执行 vite build 出 `dist/`;内置组件 `FlowCanvas`(@xyflow/react)/`GeoMap`(MapLibre,默认无底图 GeoJSON 模式,site.json 可配 tilesUrl/PMTiles)/`ChartPanel`(ECharts)/`MermaidBlock`。平台侧 `app/gateway/routers/web_app_sites.py` + 纯引擎 `deerflow/webapp/`(发现/子进程构建/HMAC 签名):`GET /api/web-app/capability`(enabled/模板/依赖/node 四项探测)、`GET|POST /api/threads/{tid}/sites[...]{build,build-status,preview-url}`,静态托管走 `GET /api/public/site-preview/{token}/{path}`(`/api/public/` 免鉴权命名空间,**唯一允许 HTML inline 的出口**——签名令牌 + dist/ 根限定 + 按请求真实 origin 显式声明的 CSP(iframe sandbox 不透明 origin 下 'self' 失效)+ nosniff;artifacts 接口既有的强制 attachment 不变)。前端:**独立风格**预览工作台 `pages/site-studio/SiteStudioPage.tsx`(顶层路由 `#/sites/:threadId/:siteName`,`App.tsx` 注册,不进 PageLayout/WorkspaceLayout;自带 site-studio.css 深色设计体系,零全局样式污染;iframe `sandbox="allow-scripts allow-forms allow-popups allow-modals"` 无 allow-same-origin)+ 产物面板 `artifact-file-detail.tsx` 里 site.json 的「站点预览」按钮(capability 可用才渲染,唯一既有文件改动)。**降级铁律**:默认**完全关闭**(`sandbox.mounts` 的模板挂载条目与文件末尾的 `web_app:` 段在 config.yaml 里**均为注释状态**);开启需**同时放开两处**(挂载让沙箱技能可探测 `/mnt/web-app-template`,`web_app.enabled: true` 开平台 API——只开一处会得到"生成了看不了"或"看不了也不生成"的中间态)。`build_site.py check` 在宿主侧会读 config.yaml 的 `web_app.enabled`(env `DEERFLOW_WEBAPP_ENABLED`/`DEERFLOW_WEBAPP_CONFIG` 可覆盖;沙箱内读不到配置时"挂载即视为开")。任何一环(配置/模板/node_modules/node)缺失 → capability 不可用 + 技能第 0 步探测失败即**回退单页 HTML 模式** + 入口按钮不渲染 + 构建失败只收进任务状态,现有功能零影响。配置 `web_app:`(enabled/template_dir/build_timeout_seconds/preview_token_ttl_seconds/node_path);测试 `tests/test_web_app_sites.py`;开发机 `make webapp-template` 预置依赖,离线发版由 deerflow-offline-deployment 仓库在 CI 预置 node_modules 进镜像。
### Configuration
Backend config lives in `offline-backend-20260512/backend/config.yaml`. Values starting with `$` resolve as environment variables (e.g., `$OPENAI_API_KEY`). MCP servers and skills are configured in `extensions_config.json` in the same directory.
## Detailed Backend Reference
The backend has its own comprehensive documentation at:
- `offline-backend-20260512/backend/AGENTS.md` — architecture, middleware chain, all API routes, config schema, sandbox/memory/subagent systems
- `offline-backend-20260512/backend/README.md` — setup guide, technology stack
+19 -1
View File
@@ -73,16 +73,34 @@ The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
### Frontend
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **Routing**: React Router v7; top-level routes in `src/pages/PageRoutes.tsx`, workspace sub-routes in `src/pages/WorkspaceRoutes.tsx`
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks)
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
- **普通问答地图可视化**: shared `MarkdownContent` recognizes only dedicated `map-json` fences and otherwise preserves the legacy Markdown/Streamdown path byte-for-byte. `components/workspace/visualizations/` validates the bounded map contract with Zod, lazy-loads MapLibre, and provides loading/error/text fallbacks so malformed or unreachable maps never blank the message. The lead-agent prompt emits `map-json` only when the user explicitly asks for a map; flowcharts and other graphics continue through the existing Markdown/Mermaid path. The raster tile template comes from the single post-build setting `runtime-config.js` → `VITE_MAP_TILE_URL` (Google by default); replace it with the intranet `{z}/{x}/{y}` endpoint for disconnected deployments.
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是初始化脚本读取的瓦片模板。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`,地图使用 Leaflet `data-df-map`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** left-nav is a separate overlay: `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`.
- **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`.
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, openMode, loginParams, appendAuth, authTokenParam, authNameParam, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route (e.g. `/task/item-aiceh/step-analysis-audience`), `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). Independent-tab clicks toast and do nothing. **`url` buttons support two independent, coexisting login-append mechanisms**: (1) **`loginParams` (追加登录参数, like 应用管理/Light App Center)** — a list of `{key, source, customValue}` resolved at click time: `runTaskButton` reads `userInfo` (via `readUserInfo` from `strategy-components/lib/light-app`, embed-isolated through `authStorage`) and appends `?{key}={value}` for each (`source` = a userInfo field such as `accessToken`/`yUserId`, or `other` → literal `customValue`); empty list = nothing appended. Reuses `LOGIN_SOURCE_OPTIONS` from light-app; stored as JSON column `task_buttons.login_params_json` (`PortableJSON`, migration `20260625_01`). (2) **`appendAuth` (追加登录态)** — appends `?{authTokenParam}={token1}&{authNameParam}={username}` (default param names `token`/`username`) so the跳转目标外部系统 can identify the current user — e.g. `…/action/sentiment?token=<JWT>&username=lizhaoxing`. **Gated on token login**: `token1` = the `?authToken=` used by token-exchange login; `username` = the raw login name the backend resolved from `getTokenInfo`. Both are stashed at login by `persistLoginCredentials` into `login.token1`/`login.username` (`core/auth/login-credentials.ts`, scoped via [[scoped-storage]] so嵌入/独立会话隔离 + 随独立会话迁移). Only token login writes them → a non-token-login session has no `token1`, so `runTaskButton` silently skips the auth append (the jump still works, just without those two params). The frontend reads `username` straight from the new `UsernameLoginResponse.username` field (the stored email local-part is lossy — lowercased/normalized — so not used), **not** by decoding the JWT. Stored as columns `task_buttons.{append_auth, auth_token_param, auth_name_param}` (migration `20260625_02`). The admin page renders both editors for `url` buttons (loginParams 虚线框 + 追加登录态 switch). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. **Bottom 快捷跳转 bar groups by prev/next**: `TaskJumpBar` renders only「上一步/下一步」-type buttons in the bottom bar — a `prev` group (id 含 `prev` 或文案「上一步」, 描边次要样式 + ←图标) rendered **左侧**, then a `next` group (id 含 `next` 或文案「下一步」, 实心主色) on the **右侧** (container `justify-end`); everything else goes to the right-edge floating menu. The default 上一步 button (`rwfx-prev`, `url`, target = `DASHBOARD_AGENT_ROUTE` `/page/strategy/dashboard-agent`, `appendTaskId` + `idParam=taskId` → `?taskId=任务id`, `openMode=self`) 默认指向**应用内**「大屏绘制」Line truncated
- **实时并发监控 (Concurrency / System-Pressure Monitor, admin-only)**: a live panel mounted at the **top of the admin 排行榜 page** (`components/workspace/admin/concurrency-monitor.tsx` `ConcurrencyMonitorPanel`, rendered in `pages/AdminUserLeaderboardPage.tsx`) showing **当前并发的大模型调用数 + 列表(用户/智能体/类型/已运行时长)**, each row with a 「停止」button (and a 「全部停止」) that **truly aborts** that run's in-flight model stream via `RunManager.cancel` (`POST /api/admin/active-runs/{run_id}/cancel`) — to relieve pressure when 使用人数过多. Two line charts (minute/hour toggle): **并发量折线图** (peak/avg, `GET /api/admin/active-runs/concurrency`, fed by a lightweight background sampler into `concurrency_samples`) and **大模型出字速度折线图** (tokens/sec, `GET /api/admin/active-runs/token-speed`, aggregated from `llm_call_metrics`) so a dip pinpoints where the bottleneck is. A **「后台采样」Switch** (`system_settings.json → concurrency_monitor.enabled`, **default off / opt-in**, `GET|PUT /api/admin/active-runs/settings`) turns the background sampling on/off **at runtime, no restart** (the live concurrency count + 「停止」 work even while sampling is off — only the 并发量 history chart needs it on); everything is designed to **never impact production** (one tiny INSERT/15s, retention-purged, best-effort). Client/hooks: `core/admin/active-runs.ts` + `core/admin/hooks.ts` (`useActiveRuns`/`useConcurrencySeries`/`useTokenSpeedSeries`/`useCancel*`/`useMonitorSettings`). See the backend CLAUDE.md「Admin Concurrency Monitor」.
- **会话冲突 409 中文化**: when a user submits a new question while the previous turn is still streaming, the backend's `RunManager.create_or_reject` rejects with HTTP 409 (`services.py` now returns a **Chinese** detail「当前对话还在生成回答…」). `core/threads/hooks.ts` `onError` detects the conflict (`isActiveRunConflictError`: 409 / "already has an active run" / the Chinese text). A premature SSE close (SDK treats it as success and restores the send button while the backend run is still occupying the thread) is recovered by `confirmOrRejoin`: hold the stop button, `joinStream` the live run, and on 409 auto-reconnect instead of a fake AI bubble. `sse_consumer` no longer ends a resumable (`on_disconnect=continue`) stream on a false-positive `is_disconnected()`.
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(2.5) **`?hideChrome=1`** — **仅**去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),**不**强制主题、**不**隔离鉴权(区别于 `embed`:只想要一个干净全屏页面,如「大屏绘制」`/page/strategy/dashboard-agent?taskId=…&hideChrome=1`)。新增 `EmbedSession.hideChrome` 字段 + `readHideChromeParam()` + `isChromeHidden()`(取 `embed || hideChrome` 之并),与 `hideJump` 完全对称;`PageRoutes.tsx` 的 `PageLayout` 把 `chromeHidden`(`isChromeHidden() || readHideChromeParam(location.search)==="on"`)并入 `pageSidebarAllowed`,且其**优先级高于** dashboard-agent 路由原本的强制全局 chrome(`forceChromeForDashboardAgent`)。`hideChrome=0` 可显式关闭。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
- **操作日志(Y-Log)上报** — 用户的关键操作(登录登出/查询/查看/增删改/启停/导入导出/问答等)成功后,**前端直连**外部接口 `POST {write_url}`(默认 `https://ch1.b.uat.4.cn/api/y-log/write`)记一条审计日志。核心模块 `src/core/log/`:`y-log.ts`(`writeYLog(params)` + `OptType` 枚举 0/1/3..12;**异步即发即忘、永不 await、永不抛错、失败静默**,自动把完整 url 转相对路径去 ip/host、对象入参/返回自动 `JSON.stringify` + 50k 裁剪、`optResult` 恒 0 只报成功),`y-log-config.ts`(从公开只读 `GET /api/public/config/y-log` 拉 `{enabled, writeUrl}`,模块级缓存 + 去重 + 默认兜底;`App.tsx` `AppProviders` 启动预热 `fetchYLogConfig()`),`y-log-token.ts`(token1 = token 登录的 `?authToken=`,经 [[scoped-storage]] 的 `authStorage*` 存取,嵌入态自动隔离)。**双门控**:仅当①存在 token1(即用 token 登录,`LoginPage` 成功后 `persistYLogToken(authToken)`,登出 `account-settings-page.tsx` 先报后清 token1)且②配置 `enabled && writeUrl` 才上报;否则一律不发。`optDetail` 统一「CM助手-全量-…」(**禁内部框架名**)。埋点**广覆盖**各 API client 的成功路径——原则是「一个用户操作(查看/查询/新增/编辑/删除/启停/导入导出)= 一条日志」,每个 API client 的**单条详情 GET(查看→view 4)/ 列表查询 GET(search 3)/ 新增 POST(create 9)/ 改 PUT·PATCH(edit 5)/ 删 DELETE(delete 6)/ 启停(enabled 11 / disabled 10)/ 导入导出(import 7 / export 8)**都各落一条,仅跳过高频 typeahead/状态轮询/纯 bootstrap config 读。覆盖文件:`strategy-components/api/{sensitive-words,menu-overrides,task-buttons,skill-addresses,light-apps,business-mapping}.ts`、`roundtable-planning/api/{drafts,task-drafts,roundtable-jobs,recommend,chains,multi-agent,dashboard-sessions}.ts`、`core/{agents,scheduled-tasks,knowledge,positions,tags,studio,system-settings,memory,mcp,models,custom-prompts,user-preferences,notifications,recommended-questions,user-qa,curator,admin,thread-shares,roundtable-draft-shares,tool-metrics,llm-metrics,note,artifacts}/api.ts`、`core/api/markdown-docx-export.ts`。**问答(Q&A)改为「一次大模型调用 = 一条日志」**:在共享的 `core/threads/hooks.ts` `useThreadStream` 的 `onLangChainEvent` 里监听 `on_chat_model_end`,每次模型调用结束就上报一条(optType 12,`reqParam` 取本次 input 的最后一条 user 文本 + 模型名,`returnParam` 取本次输出文本)——一个问答轮次里 lead agent / 子代理的**每次** LLM 调用都各记一条,覆盖所有走 `useThreadStream` 的页面(`ChatPage`/`AgentChatPage`/创建智能体/iframe/会商等);`ChatPage`/`AgentChatPage` 的 `onFinish` 仍额外保留一条轮次汇总。后端仅新增公开只读配置:`deerflow/config/y_log_config.py`(`YLogConfig{enabled,write_url}`)挂到 `AppConfig.y_log`,`app/gateway/routers/public_config.py` 加 `GET /api/public/config/y-log`,地址写在 `config.yaml` 的 `y_log` 段(mtime 热重载,前端无需重建);**不做后端代理**。Tests:`tests/test_y_log_config.py`。
### Configuration
Binary file not shown.
Submodule _skills_src/ai-skills added at cde1c8385e.
Submodule _skills_src/anthropics-skills added at 5754626092.
Submodule _skills_src/awesome-claude-skills added at 92568c1eda.
Submodule _skills_src/ce-kit added at d4973c4827.
+1
Submodule _skills_src/csm added at 3fa16a94e0.
Submodule _skills_src/superpowers added at 6fd4507659.
Submodule _skills_src/tapestry-skills added at 80e1dc56df.
+33
View File
@@ -0,0 +1,33 @@
# 批量人物蒸馏 → llmwiki
一个**完全独立的单文件脚本 + 内置定时器**:读 Excel 名单,串行调用线上 DeerFlow 服务问答接口,
复用「女娲 / huashu-nuwa」技能逐个蒸馏人物,**由脚本抓取回复并保存**成 llmwiki 风格 markdown。
- **不联网**:只用已配置的知识库查询技能(`knowledge-search-v2`)检索资料。
- **定时执行**:每天 **23:00 ~ 次日 08:00** 自动跑,到点暂停、次日继续。
- **md 由 py 保存**:agent 只产出内容,脚本抓最终回复写成 `.md`(不让项目自己写文件)。
## 你只需要做两件事
1. **改一行地址**:打开 `distill_people.py`,把顶部 `BASE_URL = "..."` 改成你的线上服务地址。
2. **放名单**:同目录放 `people.xlsx`(或 `people.csv`),**第一列 = 人名**,其余列=可选提示。
其余(用户名 `distiller`、口令 `123ewq`、技能名、时间窗)都已内置,不用填。
## 运行
```bash
python distill_people.py # 常驻,按 23:00~08:00 窗口自动执行(推荐挂后台)
python distill_people.py --now # 忽略时间窗,立刻把名单跑完(测试用)
python distill_people.py --redo # 忽略已完成,全部重做
```
读 `.xlsx` 需要 `openpyxl`;用 `.csv` 则零额外依赖。
## 输出(都在 output/ 一个文件夹)
- `<人名>.md` —— 每个人的 llmwiki 档案
- `manifest.json` —— 执行清单(状态/耗时/run_id)
- `index.md` —— 可读清单表(谁完成/失败,点开看 md)
**断点续跑**:已完成的人自动跳过;中途往名单里加人,下个执行窗会带上。
+563
View File
@@ -0,0 +1,563 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量人物蒸馏 → llmwiki markdown(完全独立的单文件客户端 + 内置定时器)
特点
----
- 串行(一个一个来)调用正在运行的 DeerFlow 服务问答接口
- 复用线上「女娲 / huashu-nuwa」技能做提炼;**不联网**,只用已配置的
知识库查询技能(knowledge-search-v2)检索资料
- agent 只负责产出内容,**markdown 由本脚本抓取最终回复后自己保存**
- 内置时间窗:每天 23:00 ~ 次日 08:00 才执行,到点自动暂停、次日继续
- 断点续跑:已完成的人自动跳过;中途加名单也会被带上
你只需要改一处:下面的 BASE_URL(线上服务地址)。其余都不用填。
名单
----
脚本同目录放 people.xlsx 或 people.csv,**第一列 = 人名**,其余列=可选提示。
运行
----
python distill_people.py # 常驻,按 23:00~08:00 窗口自动执行
python distill_people.py --now # 忽略时间窗,立刻把名单跑完(测试用)
python distill_people.py --redo # 忽略已完成,全部重做
"""
from __future__ import annotations
import argparse
import csv
import json
import re
import sys
import time
from datetime import datetime, time as dtime
from pathlib import Path
try:
import requests
except ImportError:
sys.exit("缺少依赖 requests:pip install requests")
# ════════════════════════════════════════════════════════════════════════
# 只改这一行:你的线上服务地址(不带结尾斜杠)
# ════════════════════════════════════════════════════════════════════════
BASE_URL = "http://47.94.209.59:2026/"
# ════════════════════════════════════════════════════════════════════════
# ---- 以下都已内置,正常无需改动 ----
USERNAME = "distiller" # 免密自动注册
PASSWORD = "123ewq" # 共享口令(你们开了口令门)
NUWA_SKILL = "huashu-nuwa" # 女娲技能
KB_SKILL = "knowledge-search-v2" # 知识库查询技能(不联网,用它检索)
DEPTH = "full" # full 完整 / fast 精简
HERE = Path(__file__).resolve().parent
OUTPUT_DIR = HERE / "output"
PEOPLE_FILE_DEFAULT = HERE / "people.example.csv"
# 执行时间窗:23:00 ~ 次日 08:00(跨午夜)
WIN_START = dtime(23, 0)
WIN_END = dtime(8, 0)
PER_PERSON_TIMEOUT = 1800 # 单人最长等待(秒)
POLL_INTERVAL = 5 # 轮询间隔(秒)
HTTP_TIMEOUT = 60
TERMINAL_STATUS = {"success", "error", "timeout", "interrupted"}
# ================================ llmwiki 模板 ================================
LLMWIKI_TEMPLATE = """\
---
name: <中文名/常用名>
aliases: [<别名/英文名...>]
type: person
era: <在世 | 历史人物>
domains: [<领域1>, <领域2>]
distilled_at: <YYYY-MM-DD>
sources_count: <检索到的来源条数>
primary_source_ratio: <一手来源占比, 0~1 的小数>
confidence: <high | medium | low>
---
# <人物名>
> **一句话定位**:<此人看世界的独特方式,一句话>
## 身份卡
- **活跃年代**:
- **核心身份**:
- **自述(50字, 以其语气)**:
## 核心观点(真信念 · 反复出现的主张)
1. ...
## 心智模型(3–7 个;每个含:一句话 / 证据场景≥2 / 应用 / 局限 / 来源)
### 1. <模型名>
- **一句话**:
- **证据场景**:
- **应用方式**:
- **局限**:
- **来源**:
## 决策启发式(5–10 条 · 如果 X 则 Y,带案例)
- 若 <X> → <Y>
## 表达 DNA
| 维度 | 特征 |
|------|------|
| 句式 | |
| 高频词 | |
| 幽默 | |
| 确定性 | |
| 引用习惯 | |
## 价值观与反模式
- **价值观排序**:
- **明确反对**:
## 内在张力(≥2 对矛盾)
- <A> ↔ <B>
## 经历时间线
| 年份 | 事件 | 思想意义 |
|------|------|---------|
| | | |
## 代表语录
- “...” —(出处/年份,标一手/二手)
## 智识谱系
- **受影响于**:
- **影响了**:
## 外部评价与争议
-
## 诚实边界
- <至少 3 条具体局限,含信息截止日期>
## 调研来源
**一手(N)**:
**二手(N)**:
"""
def build_prompt(name: str, hints: str, depth: str) -> str:
depth_note = (
"本次走【完整模式】:尽量覆盖著作/对话/表达/他者评价/决策/时间线 6 个维度。"
if depth == "full"
else "本次走【精简模式】:重点覆盖核心观点、心智模型、时间线三块即可。"
)
hint_block = f"\n补充提示(来自名单其它列,可作为检索关键词):{hints}\n" if hints.strip() else ""
return f"""\
你的任务:把人物【{name}】蒸馏成一份 llmwiki 风格的人物档案 markdown。
请复用本服务中已启用的「女娲 / {NUWA_SKILL}」技能的提炼方法论
(心智模型三重验证、表达DNA分析、诚实边界)。
⚠️ 本环境**无法联网,严禁使用 WebSearch 等任何联网工具**。
所有资料**只能通过已配置的知识库查询技能「{KB_SKILL}」检索获取**。
请充分调用该知识库技能,围绕该人物多角度检索(生平、观点、著作、语录、决策、评价等),
基于检索到的内容做提炼。知识库里查不到的部分,如实在「诚实边界」标注,不要编造。
{depth_note}
{hint_block}
输出要求:
1. **不要写任何文件,不要调用 write_file / present_files**。
2. 直接在你的回复正文里输出**完整中文 markdown 全文**,严格按下面模板结构
(保留各级标题,逐项填实,不要保留尖括号占位符),**不要任何额外解说、前言或结语**:
------------------ 模板开始 ------------------
{LLMWIKI_TEMPLATE}
------------------ 模板结束 ------------------
现在开始,独立完成,不要向我提问。"""
# ================================ HTTP 客户端 ================================
class DeerFlowClient:
def __init__(self, base_url: str):
self.base = base_url.rstrip("/")
self.s = requests.Session()
self.token: str | None = None
def _headers(self) -> dict:
h = {"Content-Type": "application/json"}
if self.token:
h["Authorization"] = f"Bearer {self.token}" # 只用 Bearer,不带 cookie → 跳过 CSRF
return h
def login(self) -> bool:
url = f"{self.base}/api/v1/auth/login/username"
params = {"password": PASSWORD} if PASSWORD else None
try:
r = self.s.post(url, params=params, json={"username": USERNAME}, timeout=HTTP_TIMEOUT)
except requests.RequestException as e:
print(f" [登录] 连接失败:{e}")
return False
if r.status_code == 200:
self.token = r.json().get("access_token")
self.s.cookies.clear()
print(f" [登录] 成功,用户={USERNAME}")
return True
print(f" [登录] 返回 {r.status_code}:{r.text[:200]}")
return False
def ensure_skill_enabled(self, name: str) -> None:
try:
r = self.s.get(f"{self.base}/api/skills", headers=self._headers(), timeout=HTTP_TIMEOUT)
if r.status_code == 200:
skills = r.json().get("skills", [])
hit = next((x for x in skills if x.get("name") == name), None)
if hit is None:
print(f" [技能] 警告:服务里没找到 '{name}'")
return
if hit.get("enabled"):
print(f" [技能] '{name}' 已启用")
return
pr = self.s.put(f"{self.base}/api/skills/{name}", headers=self._headers(),
json={"enabled": True}, timeout=HTTP_TIMEOUT)
print(f" [技能] 启用 '{name}' → {pr.status_code}")
except requests.RequestException as e:
print(f" [技能] 检查 '{name}' 失败(忽略):{e}")
def create_thread(self) -> str:
r = self.s.post(f"{self.base}/api/threads", headers=self._headers(), json={}, timeout=HTTP_TIMEOUT)
r.raise_for_status()
return r.json()["thread_id"]
def start_run(self, thread_id: str, prompt: str) -> str:
body = {
"input": {"messages": [{"role": "user", "content": prompt}]},
"excluded_tools": ["ask_clarification"], # 禁澄清,无人值守不卡住
}
r = self.s.post(f"{self.base}/api/threads/{thread_id}/runs",
headers=self._headers(), json=body, timeout=HTTP_TIMEOUT)
r.raise_for_status()
return r.json()["run_id"]
def poll_run(self, thread_id: str, run_id: str) -> str:
deadline = time.monotonic() + PER_PERSON_TIMEOUT
last = ""
while time.monotonic() < deadline:
try:
r = self.s.get(f"{self.base}/api/threads/{thread_id}/runs/{run_id}",
headers=self._headers(), timeout=HTTP_TIMEOUT)
if r.status_code == 200:
st = r.json().get("status", "")
if st != last:
print(f" ...状态:{st}")
last = st
if st in TERMINAL_STATUS:
return st
except requests.RequestException as e:
print(f" 轮询出错(重试):{e}")
time.sleep(POLL_INTERVAL)
return "timeout"
def fetch_result_text(self, thread_id: str, run_id: str) -> str | None:
"""抓取该 thread 的最终 AI 回复文本(由 py 保存为 md)。
注意:本部署的 `/runs/{rid}/messages`、`/messages`、`/events` 端点都返回空
(事件库按登录用户过滤,后台 worker 写入时未带 user_id → 读不到)。
可靠来源是 LangGraph 检查点状态 `/api/threads/{tid}/state` → values.messages,
其中每条消息形如 {"type": "human"|"ai"|"tool", "content": <str | 内容块列表>, ...}。
取最后一条有正文的 ai 消息即可。
"""
try:
r = self.s.get(f"{self.base}/api/threads/{thread_id}/state",
headers=self._headers(), timeout=HTTP_TIMEOUT)
if r.status_code != 200:
print(f" 取回复返回 {r.status_code}:{r.text[:200]}")
return None
messages = (r.json().get("values") or {}).get("messages") or []
# 从后往前找最后一条「有正文」的 AI 回复(跳过只含工具调用的中间消息)
for msg in reversed(messages):
if not isinstance(msg, dict) or msg.get("type") != "ai":
continue
txt = _extract_text(msg)
if txt and txt.strip():
return txt
except requests.RequestException as e:
print(f" 取回复出错:{e}")
return None
def _extract_text(payload) -> str:
"""从一条消息的 content 里提取纯文本。
payload 可能是:
- LangChain 消息的 model_dump 字典(正文在 payload["content"])
- 直接的字符串
- 内容块列表([{"type":"text","text":"..."}, ...])
"""
if payload is None:
return ""
# 若是整条消息的 model_dump,正文在其 content 字段
if isinstance(payload, dict):
c = payload.get("content", payload.get("text", ""))
else:
c = payload
if isinstance(c, str):
return c
if isinstance(c, list):
parts = []
for b in c:
if isinstance(b, dict):
# 只取文本块,跳过 tool_use / thinking 等非文本块
if b.get("type") in (None, "text", "text_block"):
parts.append(b.get("text") or b.get("content") or "")
elif b.get("text"):
parts.append(b["text"])
elif isinstance(b, str):
parts.append(b)
return "\n".join(p for p in parts if p)
return str(c) if c else ""
def _clean_markdown(text: str) -> str:
"""去掉 agent 可能套上的 ```markdown ... ``` 围栏,并规整 YAML frontmatter。"""
t = text.strip()
m = re.match(r"^```(?:markdown|md)?\s*\n(.*)\n```$", t, re.S)
if m:
t = m.group(1).strip()
return _normalize_frontmatter(t)
def _normalize_frontmatter(text: str) -> str:
"""修复模型常见的 frontmatter 跑偏,让 YAML 能正常解析。
模型经常把字段名写成 markdown 加粗(``**name:**``)、加空行、留行尾双空格,
这些都会让 frontmatter 解析失败。这里把开头 ``---`` 到下一个 ``---`` 之间的内容
规整为干净的 ``key: value`` 形式。正文(第二个 ``---`` 之后)保持原样。
"""
if not text.startswith("---"):
return text
# 拆出 frontmatter 块:开头 --- 与下一行单独的 --- 之间
m = re.match(r"^---[ \t]*\n(.*?)\n---[ \t]*\n?(.*)$", text, re.S)
if not m:
return text
body_fm, rest = m.group(1), m.group(2)
out_lines = []
for line in body_fm.splitlines():
s = line.rstrip() # 去行尾双空格
s = s.replace("**", "") # 去 markdown 加粗标记
if not s.strip():
continue # 丢掉空行
out_lines.append(s)
return "---\n" + "\n".join(out_lines) + "\n---\n\n" + rest.lstrip("\n")
# ================================ 名单读取 ================================
def slugify(name: str) -> str:
s = re.sub(r'[\\/:*?"<>|]+', "", name).strip()
s = re.sub(r"\s+", "_", s)
return s or "unnamed"
def read_people(path: Path) -> list[dict]:
if not path.exists():
return []
rows: list[list[str]] = []
if path.suffix.lower() in (".xlsx", ".xlsm"):
try:
import openpyxl
except ImportError:
sys.exit("读取 .xlsx 需要 openpyxl:pip install openpyxl(或把名单存成 .csv)")
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
for row in wb.active.iter_rows(values_only=True):
rows.append(["" if c is None else str(c).strip() for c in row])
elif path.suffix.lower() == ".csv":
with open(path, encoding="utf-8-sig", newline="") as f:
rows = [[c.strip() for c in r] for r in csv.reader(f)]
else:
sys.exit(f"不支持的名单格式:{path.suffix}(用 .xlsx 或 .csv)")
if rows and re.search(r"(姓名|名字|人物|name)", rows[0][0] if rows[0] else "", re.I):
rows = rows[1:]
people, seen = [], set()
for r in rows:
if not r or not r[0].strip():
continue
name = r[0].strip()
if name in seen:
continue
seen.add(name)
hints = " | ".join(x for x in r[1:] if x.strip())
people.append({"name": name, "hints": hints})
return people
# ================================ 时间窗 ================================
def in_window(now: datetime | None = None) -> bool:
now = now or datetime.now()
t = now.time()
return t >= WIN_START or t < WIN_END # 跨午夜
def seconds_until_window(now: datetime | None = None) -> int:
now = now or datetime.now()
if in_window(now):
return 0
target = now.replace(hour=WIN_START.hour, minute=WIN_START.minute, second=0, microsecond=0)
return max(1, int((target - now).total_seconds()))
def sleep_interruptible(total: int) -> None:
end = time.monotonic() + total
while time.monotonic() < end:
time.sleep(min(30, max(1, int(end - time.monotonic()))))
# ================================ 蒸馏单人 ================================
def distill_one(client: DeerFlowClient, person: dict, out_dir: Path,
manifest: dict, save_manifest, depth: str) -> None:
name, hints = person["name"], person["hints"]
slug = slugify(name)
md_path = out_dir / f"{slug}.md"
t0 = time.time()
rec = {"name": name, "slug": slug, "status": "running",
"started_at": _now(), "file": f"{slug}.md"}
manifest[name] = rec
save_manifest()
try:
tid = client.create_thread()
run_id = client.start_run(tid, build_prompt(name, hints, depth))
status = client.poll_run(tid, run_id)
rec.update(thread_id=tid, run_id=run_id, run_status=status)
text = client.fetch_result_text(tid, run_id) if status == "success" else None
if status == "success" and text:
md = _clean_markdown(text)
md_path.write_text(md, encoding="utf-8")
rec["status"] = "succeeded"
rec["chars"] = len(md)
print(f" ✅ 完成 → {md_path.name}({len(md)} 字,耗时 {int(time.time()-t0)}s)")
else:
rec["status"] = "failed"
rec["error"] = f"run={status}, text={'有' if text else '无'}"
if text:
(out_dir / f"{slug}.partial.md").write_text(text, encoding="utf-8")
print(f" ❌ 失败(run={status})")
except Exception as e:
rec["status"] = "failed"
rec["error"] = str(e)
print(f" ❌ 异常:{e}")
rec["finished_at"] = _now()
rec["seconds"] = int(time.time() - t0)
manifest[name] = rec
save_manifest()
# ================================ 主流程 ================================
def main():
ap = argparse.ArgumentParser(description="批量人物蒸馏 → llmwiki(定时窗 23:00~08:00)")
ap.add_argument("excel", nargs="?", default=str(PEOPLE_FILE_DEFAULT),
help="名单文件(.xlsx 或 .csv),默认同目录 people.xlsx")
ap.add_argument("--now", action="store_true", help="忽略时间窗,立刻跑完整批(测试用)")
ap.add_argument("--redo", action="store_true", help="忽略已完成,全部重做")
args = ap.parse_args()
excel = Path(args.excel)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
manifest_path = OUTPUT_DIR / "manifest.json"
manifest: dict = {}
if manifest_path.exists():
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
manifest = {}
# 校验名单存在
if not read_people(excel):
sys.exit(f"名单为空或不存在:{excel}\n请放 people.xlsx(第一列写人名)。")
# --redo:一次性清掉名单里这些人的旧记录,让他们重做一遍;
# 之后本次会话内跑成功的会正常计为「已完成」,循环才会往后推进
# (否则每轮 pending() 都把已完成的人当未完成,永远卡在第 1 个人)
if args.redo:
for p in read_people(excel):
manifest.pop(p["name"], None)
client = DeerFlowClient(BASE_URL)
if not client.login():
sys.exit("登录失败。请检查 BASE_URL 是否指向正在运行的服务、口令是否正确。")
client.ensure_skill_enabled(NUWA_SKILL)
client.ensure_skill_enabled(KB_SKILL)
def save_manifest():
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
_write_index(OUTPUT_DIR, read_people(excel), manifest)
def pending() -> list[dict]:
people = read_people(excel)
out = []
for p in people:
r = manifest.get(p["name"], {})
done = r.get("status") == "succeeded" and (OUTPUT_DIR / f"{slugify(p['name'])}.md").exists()
if not done:
out.append(p)
return out
print(f"启动 | 服务={BASE_URL} | 输出={OUTPUT_DIR}")
print(f"模式={'立即跑完' if args.now else '定时窗 23:00~08:00'} | 名单={excel}")
try:
while True:
todo = pending()
if not todo:
print(f"[{_now()}] 全部已完成,待命中(每小时复查名单)...")
if args.now:
break
sleep_interruptible(3600)
continue
if not args.now and not in_window():
wait = seconds_until_window()
nxt = datetime.now().replace(hour=WIN_START.hour, minute=0, second=0, microsecond=0)
print(f"[{_now()}] 不在执行窗口,剩 {len(todo)} 人待蒸;"
f"休眠到 {nxt.strftime('%H:%M')}(约 {wait//60} 分钟)...")
sleep_interruptible(wait)
continue
# 在窗口内(或 --now):处理一个人,然后回到循环重新判断窗口/名单
p = todo[0]
idx_total = len(read_people(excel))
done_n = sum(1 for v in manifest.values() if v.get("status") == "succeeded")
print(f"[{_now()}] ({done_n}/{idx_total} 已完成) 蒸馏:{p['name']}")
distill_one(client, p, OUTPUT_DIR, manifest, save_manifest, DEPTH)
except KeyboardInterrupt:
print("\n已手动停止。重跑脚本会从未完成处继续。")
ok = sum(1 for r in manifest.values() if r.get("status") == "succeeded")
fail = sum(1 for r in manifest.values() if r.get("status") == "failed")
print(f"\n结束:成功 {ok} / 失败 {fail}")
print(f"结果目录:{OUTPUT_DIR} | 清单:{manifest_path} | 索引:{OUTPUT_DIR/'index.md'}")
def _now() -> str:
return time.strftime("%Y-%m-%d %H:%M:%S")
def _write_index(out_dir: Path, people: list[dict], manifest: dict) -> None:
lines = ["# 蒸馏执行清单", "", f"更新时间:{_now()}", "",
"| # | 人物 | 状态 | 字数 | 耗时 | 文件 |",
"|---|------|------|------|------|------|"]
badge = {"succeeded": "✅完成", "failed": "❌失败", "running": "⏳进行中"}
for i, p in enumerate(people, 1):
r = manifest.get(p["name"], {})
st = badge.get(r.get("status", ""), "⌛排队")
f = r.get("file", "")
link = f"[{f}]({f})" if r.get("status") == "succeeded" and f else "-"
lines.append(f"| {i} | {p['name']} | {st} | {r.get('chars','-')} | {r.get('seconds','-')} | {link} |")
(out_dir / "index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
姓名,领域(可选提示),资料链接或备注(可选)
查理·芒格,投资/多元思维,
理查德·费曼,物理/教学,
纳瓦尔·拉维坎特,创业/杠杆,https://nav.al/
1 姓名 领域(可选提示) 资料链接或备注(可选)
2 查理·芒格 投资/多元思维
3 理查德·费曼 物理/教学
4 纳瓦尔·拉维坎特 创业/杠杆 https://nav.al/
@@ -0,0 +1,136 @@
# zncm Web Clipper
这是给 zncm 使用的 Chrome 插件,安装后默认连接 `config.js` 中配置好的服务地址。
默认地址配置在 `browser-extension/deer-web-clipper/config.js`:
```js
globalThis.ZNCM_WEB_CLIPPER_CONFIG = Object.freeze({
defaultWebBaseUrl: "http://47.94.209.59:2026",
defaultApiBaseUrl: "http://47.94.209.59:2026",
});
```
如果部署地址变化,只需要修改 `config.js` 里的这两个字段,然后重新加载插件。
正常情况下不需要手动配置 URL,也不需要手动复制 token。
## 能做什么
1. 把当前浏览器标签页发送到 zncm,作为 Markdown 附件进入聊天。
2. 接收 zncm 聊天中由 `browser_fetch_page` 工具创建的网页抓取任务。
3. 插件在 Chrome 中打开目标网页,读取可见文本和表格,再把结果回传给 zncm。
4. 在普通网页右下角显示“zncm 页面问答”浮窗,直接把当前页面内容交给 zncm 问答。
5. 接收 zncm 的 `browser_act` 交互式操作任务:在 Chrome 中打开页面并按 zncm 的指令**操作页面**——点击、输入、滚动、等待、页内跳转,每一步把页面上的可交互元素清单和正文回传给 zncm,由 zncm 决定下一步(observe→act 循环)。
## 页面操作(browser_act)
除了只读抓取,zncm 还能驱动插件**实际操作网页**,完成登录、填表、翻页找数据等多步任务:
1. zncm 调用 `browser_act` 工具,先 `navigate` 打开页面。
2. 插件打开标签页(同一会话会复用这个标签页),扫描页面上的可交互元素,给每个元素编号后连同正文回传给 zncm。
3. zncm 根据元素编号决定下一步(点第 3 个按钮、在第 1 个输入框填字……),再次调用 `browser_act`。
4. 插件执行动作、重新扫描页面、回传新状态,如此循环,直到任务完成。
5. 任务结束时 zncm 用 `action="close"` 关闭该会话标签页。
支持的动作:`navigate`(打开/跳转 URL)、`click`(点击元素)、`type`(输入文字,可选回车提交)、`scroll`(滚动到元素或下翻一屏)、`wait`(等待某 CSS 选择器出现)、`extract`(重新读取正文)、`observe`(仅重新扫描)、`close`(关闭会话标签页)。
> 提示:页面操作能力较强(可点击任意按钮、提交表单),可在后端 `config.yaml` 用 `browser_context.actions_enabled: false` 关闭,只保留只读抓取。
## 后端启用
在 `offline-backend-20260512/backend/config.yaml` 中保持:
```yaml
browser_context:
enabled: true # 只读抓取 + 页面问答
actions_enabled: true # 交互式页面操作 browser_act(不需要可设为 false)
```
修改配置后需要重启后端。
## 安装插件
1. 打开 Chrome 的 `chrome://extensions`。
2. 开启右上角的“开发者模式”。
3. 点击“加载已解压的扩展程序”。
4. 选择目录:`browser-extension/deer-web-clipper`。
5. 安装后插件名会显示为 `zncm Web Clipper`。
## 安装即用
插件默认已经开启:
- 自动识别已打开 zncm 页面的服务地址。
- 自动同步已登录 zncm 的 token。
- 自动轮询 zncm 聊天触发的浏览器抓取任务。
- 页面问答框默认开启。
如果你打开的是 `config.js` 里配置的默认服务器,通常无需进入设置页。
如果部署地址变了,保持一个已登录的 zncm 页面打开,然后在插件设置里点击:
- `重新识别服务地址`
- `同步 token`
## 自动同步 Token
插件会自动从两个位置获取登录 token:
1. zncm API 域名下的 `access_token` Cookie,包括 HttpOnly Cookie。
2. 已登录 zncm 前端页面里的浏览器本地登录态。
切换登录账号后,插件后台每次轮询任务前都会重新同步 token;点击插件里的“同步 token”也可以立即刷新。
如果当前浏览器已经登出 zncm,且打开的 zncm 页面里没有 token,插件会清空旧 token,避免继续用上一个账号请求。
## 页面问答框
安装插件后,普通 `http` / `https` 页面右下角会出现 `AI` 按钮。点击后可以在当前网页上直接提问,例如:
```text
总结这页的核心结论
提取页面里的价格和时间
这张表里哪个指标最高?
```
插件会读取当前页面可见文本和表格,发送到 zncm,并把 zncm 的回答显示回浮窗里。多轮追问会复用同一个 zncm 会话;如果页面 URL 变化,会自动重新开始当前页会话。
是否显示问答框可以自己控制:
- 插件弹窗里勾选或取消“启用页面问答框”。
- 插件设置页里勾选或取消“启用页面问答框”。
关闭问答框只会隐藏页面浮窗,不会影响“发送当前页”和 zncm 聊天中的 `browser_fetch_page` 抓取任务。
## 手动发送当前网页
点击插件图标,选择“发送当前页”。
插件会读取当前页面的可见文本和表格,发送到 zncm,并打开一个新的 zncm 聊天页面。zncm 会把网页内容作为上传文件注入到首条问题里。
## 在 zncm 聊天中触发浏览器抓取
在 zncm 中输入带有明确 URL 的问题,例如:
```text
使用 browser_fetch_page 打开 https://example.com/report,提取页面里的关键表格数据并总结。
```
执行流程:
1. zncm 调用 `browser_fetch_page` 工具创建浏览器任务。
2. 插件后台轮询到任务。
3. 插件在 Chrome 中打开目标网页。
4. 插件提取页面文本和表格。
5. 插件把结果回传给 zncm。
6. zncm 基于回传内容回答你的问题。
这个方式适合读取需要浏览器登录态、内网权限或当前 Chrome 环境才能访问的页面。
## 注意事项
- 目标 URL 必须是明确的 `http` 或 `https` 地址。
- 插件不能读取 `chrome://` 等浏览器内部页面。
- 如果目标网站阻止扩展脚本注入,抓取可能失败。
- 后台任务默认会打开新标签页;是否抓取完成后关闭标签页,可在插件设置里配置。
@@ -0,0 +1,957 @@
importScripts("config.js", "shared.js");
const ALARM_NAME = "deer-browser-task-poll";
const LEGACY_LOCAL_API_BASE_URL = "http://localhost:8001";
const LEGACY_LOCAL_WEB_BASE_URL = "http://localhost:5174";
let pollTimer = null;
let isPolling = false;
let startupPromise = null;
const POLLING_SCHEDULE_SETTING_KEYS = new Set(["taskPollingEnabled", "taskPollIntervalSeconds"]);
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSettings() {
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
const discovered = await chrome.storage.local.get({
discoveredApiBaseUrl: "",
discoveredWebBaseUrl: "",
});
if (settings.autoDiscoverServiceUrl) {
settings.apiBaseUrl = discovered.discoveredApiBaseUrl || settings.apiBaseUrl;
settings.webBaseUrl = discovered.discoveredWebBaseUrl || settings.webBaseUrl;
}
const updates = {};
if (!settings.apiBaseUrl || settings.apiBaseUrl === LEGACY_LOCAL_API_BASE_URL) {
updates.apiBaseUrl = DEER_CLIPPER_DEFAULTS.apiBaseUrl;
}
if (!settings.webBaseUrl || settings.webBaseUrl === LEGACY_LOCAL_WEB_BASE_URL) {
updates.webBaseUrl = DEER_CLIPPER_DEFAULTS.webBaseUrl;
}
if (!settings.workerId) {
updates.workerId = deerMakeWorkerId();
}
if (Object.keys(updates).length > 0) {
await chrome.storage.sync.set(updates);
Object.assign(settings, updates);
}
return settings;
}
function pollIntervalMs(settings) {
const seconds = Number(settings.taskPollIntervalSeconds || DEER_CLIPPER_DEFAULTS.taskPollIntervalSeconds);
return Math.max(3, seconds) * 1000;
}
function originMatchPattern(value) {
try {
const url = new URL(deerNormalizeBaseUrl(value));
return `${url.protocol}//${url.host}/*`;
} catch {
return "";
}
}
function normalizeServiceBase(value) {
return deerNormalizeBaseUrl(value || "");
}
function serviceBaseFromTabUrl(value) {
try {
const url = new URL(value);
const path = url.pathname.replace(/\/+$/, "");
if (path === "/deerflow" || path.startsWith("/deerflow/")) {
return `${url.origin}/deerflow`;
}
return url.origin;
} catch {
return "";
}
}
function apiCandidatesForWebBase(webBase, currentApiBase) {
const candidates = [];
const add = (value) => {
const normalized = normalizeServiceBase(value);
if (normalized && !candidates.includes(normalized)) {
candidates.push(normalized);
}
};
add(webBase);
try {
const url = new URL(normalizeServiceBase(webBase));
if (["5173", "5174", "3000"].includes(url.port)) {
const apiUrl = new URL(url.href);
apiUrl.port = "8001";
add(apiUrl.href);
}
} catch {
// Ignore invalid candidates.
}
add(currentApiBase);
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
add(LEGACY_LOCAL_API_BASE_URL);
return candidates;
}
async function browserContextStatus(apiBaseUrl) {
try {
const response = await fetch(`${apiBaseUrl}/api/browser-context/status`, {
credentials: "include",
});
// The gateway may enforce authentication globally before this otherwise
// public discovery endpoint is reached. A 401/403 still proves that this
// candidate is the zncm API paired with the open frontend tab.
if (response.status === 401 || response.status === 403) {
return { enabled: true, authenticationRequired: true };
}
if (!response.ok) {
return null;
}
const data = await response.json().catch(() => null);
if (data && typeof data.enabled === "boolean") {
return data;
}
} catch {
return null;
}
return null;
}
function tabLooksLikeZncm(tab) {
const value = `${tab.url || ""} ${tab.title || ""}`.toLowerCase();
return (
value.includes("deerflow") ||
value.includes("deer") ||
value.includes("zncm") ||
value.includes("/page/workspace") ||
value.includes("/chats/") ||
value.includes("#/login")
);
}
async function findOpenZncmTabs(settings) {
const tabs = [];
const seen = new Set();
const addTabs = (items) => {
for (const tab of items || []) {
if (tab?.id && !seen.has(tab.id)) {
seen.add(tab.id);
tabs.push(tab);
}
}
};
for (const pattern of [
originMatchPattern(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl),
originMatchPattern(DEER_CLIPPER_DEFAULTS.webBaseUrl),
]) {
if (!pattern) {
continue;
}
try {
addTabs(await chrome.tabs.query({ url: pattern }));
} catch {
// Continue with broader discovery below.
}
}
try {
const allTabs = await chrome.tabs.query({ url: ["http://*/*", "https://*/*"] });
addTabs(allTabs.filter(tabLooksLikeZncm));
} catch {
// Ignore tab discovery failures.
}
return tabs;
}
async function discoverServiceBases(settings, force = false) {
if (!settings.autoDiscoverServiceUrl && !force) {
return settings;
}
let webBaseUrl = normalizeServiceBase(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
let discoveredWebBaseUrl = webBaseUrl;
let source = "configured";
const tabs = await findOpenZncmTabs(settings);
if (tabs.length > 0) {
const tabBase = serviceBaseFromTabUrl(tabs[0].url);
if (tabBase) {
discoveredWebBaseUrl = tabBase;
source = `tab:${tabs[0].url || tabBase}`;
}
}
let apiBaseUrl = normalizeServiceBase(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
let verified = false;
for (const candidate of apiCandidatesForWebBase(discoveredWebBaseUrl, apiBaseUrl)) {
const status = await browserContextStatus(candidate);
if (status) {
apiBaseUrl = candidate;
webBaseUrl = discoveredWebBaseUrl;
verified = true;
source = status.enabled ? `${source}:browser_context_enabled` : `${source}:browser_context_disabled`;
break;
}
}
if (!verified && force) {
source = `${source}:api_not_verified`;
}
const next = {
...settings,
apiBaseUrl,
webBaseUrl,
lastServiceSyncAt: new Date().toISOString(),
lastServiceSyncSource: source,
};
if (
force ||
settings.apiBaseUrl !== next.apiBaseUrl ||
settings.webBaseUrl !== next.webBaseUrl ||
settings.lastServiceSyncSource !== next.lastServiceSyncSource
) {
// Discovered endpoints are runtime state, not user-authored preferences.
// Keep them in storage.local so Chrome sync write quotas cannot break task
// polling. The options page may still persist explicit user choices.
await chrome.storage.local.set({
discoveredApiBaseUrl: next.apiBaseUrl,
discoveredWebBaseUrl: next.webBaseUrl,
lastServiceSyncAt: next.lastServiceSyncAt,
lastServiceSyncSource: next.lastServiceSyncSource,
});
}
return next;
}
async function getCookieToken(settings) {
if (!chrome.cookies?.get) {
return null;
}
const apiBaseUrl = deerNormalizeBaseUrl(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
try {
const cookie = await chrome.cookies.get({ url: apiBaseUrl, name: "access_token" });
if (cookie?.value) {
return { token: cookie.value, source: `cookie:${new URL(apiBaseUrl).host}:access_token` };
}
} catch (error) {
console.warn("Failed to read zncm access_token cookie", error);
}
return null;
}
async function getTokenFromOpenZncmTab(settings) {
const tabs = await findOpenZncmTabs(settings);
let sawZncmTab = false;
for (const tab of tabs) {
if (!tab.id) {
continue;
}
sawZncmTab = true;
try {
const [result] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: deerExtractAuthTokenFromPage,
});
const tokenInfo = result?.result;
if (tokenInfo?.token) {
const tabBase = serviceBaseFromTabUrl(tab.url);
if (tabBase && settings.autoDiscoverServiceUrl && settings.webBaseUrl !== tabBase) {
await chrome.storage.sync.set({
webBaseUrl: tabBase,
lastServiceSyncAt: new Date().toISOString(),
lastServiceSyncSource: `token-tab:${tab.url || tabBase}`,
});
}
return {
token: tokenInfo.token,
source: tokenInfo.source || `tab:${tab.url || "open-deer-tab"}`,
};
}
} catch (error) {
console.warn("Failed to read zncm token from tab", tab.id, error);
}
}
if (sawZncmTab) {
return { token: "", source: "open-deer-tab", clearStale: true };
}
return null;
}
async function syncAuthToken(settings = null, forceStatus = false) {
let current = settings || (await getSettings());
current = await discoverServiceBases(current);
if (!current.tokenSyncEnabled && !forceStatus) {
return current;
}
const tokenInfo = (await getTokenFromOpenZncmTab(current)) || (await getCookieToken(current));
if (!tokenInfo?.token) {
if (tokenInfo?.clearStale && current.accessToken) {
const next = {
...current,
accessToken: "",
lastTokenSyncSource: "cleared:open-deer-tab-no-token",
lastTokenSyncAt: new Date().toISOString(),
};
await chrome.storage.sync.set({
accessToken: "",
lastTokenSyncSource: next.lastTokenSyncSource,
lastTokenSyncAt: next.lastTokenSyncAt,
});
return next;
}
return current;
}
const next = {
...current,
accessToken: tokenInfo.token,
lastTokenSyncSource: tokenInfo.source,
lastTokenSyncAt: new Date().toISOString(),
};
if (
forceStatus ||
current.accessToken !== next.accessToken ||
current.lastTokenSyncSource !== next.lastTokenSyncSource
) {
await chrome.storage.sync.set({
accessToken: next.accessToken,
lastTokenSyncSource: next.lastTokenSyncSource,
lastTokenSyncAt: next.lastTokenSyncAt,
});
}
return next;
}
async function refreshAlarm(settings) {
await chrome.alarms.clear(ALARM_NAME);
if (!settings.taskPollingEnabled) {
return;
}
const minutes = Math.max(0.5, pollIntervalMs(settings) / 60000);
await chrome.alarms.create(ALARM_NAME, { delayInMinutes: minutes, periodInMinutes: minutes });
}
async function scheduleNextPoll(delayMs = null) {
if (pollTimer) {
clearTimeout(pollTimer);
pollTimer = null;
}
const settings = await getSettings();
if (!settings.taskPollingEnabled) {
return;
}
const ms = delayMs ?? pollIntervalMs(settings);
pollTimer = setTimeout(() => {
void pollOnce().catch((error) => console.warn("zncm task poll failed", error));
}, ms);
}
async function waitForTabLoad(tabId, timeoutSeconds) {
const timeoutMs = Math.max(5, Number(timeoutSeconds || 30)) * 1000;
try {
const tab = await chrome.tabs.get(tabId);
if (tab.status === "complete") {
return true;
}
} catch {
return false;
}
return new Promise((resolve) => {
const timer = setTimeout(() => {
chrome.tabs.onUpdated.removeListener(listener);
resolve(false);
}, timeoutMs);
function listener(updatedTabId, changeInfo) {
if (updatedTabId === tabId && changeInfo.status === "complete") {
clearTimeout(timer);
chrome.tabs.onUpdated.removeListener(listener);
resolve(true);
}
}
chrome.tabs.onUpdated.addListener(listener);
});
}
async function captureFromTab(tabId, maxContentChars) {
const [result] = await chrome.scripting.executeScript({
target: { tabId },
func: deerExtractCurrentPage,
});
const page = result?.result;
if (!page?.content) {
throw new Error("当前页面没有可读取的文本。");
}
const limit = Math.max(1000, Number(maxContentChars || 200000));
if (page.content.length > limit) {
page.content = `${page.content.slice(0, limit)}\n\n[Truncated by zncm browser task limit]`;
}
return page;
}
async function postTaskResult(taskId, settings, payload) {
const apiBaseUrl = deerNormalizeBaseUrl(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
const response = await fetch(`${apiBaseUrl}/api/browser-context/tasks/${encodeURIComponent(taskId)}/result`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...deerAuthHeaders(settings),
},
body: JSON.stringify(payload),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const detail = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data);
throw new Error(detail || `HTTP ${response.status}`);
}
return data;
}
function zncmApiBaseCandidates(settings) {
const candidates = [];
const add = (value) => {
const normalized = deerNormalizeBaseUrl(value || "");
if (normalized && !candidates.includes(normalized)) {
candidates.push(normalized);
}
};
add(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
try {
const url = new URL(deerNormalizeBaseUrl(settings.apiBaseUrl || ""));
if (url.pathname && url.pathname !== "/") {
add(url.origin);
}
} catch {
// Ignore invalid configured API URL.
}
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
return candidates;
}
async function zncmFetchJson(settings, path, options) {
let lastError = null;
for (const apiBaseUrl of zncmApiBaseCandidates(settings)) {
try {
const response = await fetch(`${apiBaseUrl}${path}`, options);
const contentType = response.headers.get("content-type") || "";
const data = contentType.includes("application/json")
? await response.json().catch(() => ({}))
: { detail: await response.text().catch(() => "") };
if (response.ok) {
return { data, apiBaseUrl, status: response.status };
}
const detail = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data);
lastError = new Error(detail || `HTTP ${response.status}`);
lastError.status = response.status;
lastError.detail = detail;
lastError.apiBaseUrl = apiBaseUrl;
if (response.status !== 404) {
throw lastError;
}
} catch (error) {
lastError = error;
if (error?.status && error.status !== 404) {
throw error;
}
}
}
throw lastError || new Error("zncm 服务请求失败。");
}
function zncmMakeThreadId() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
return `zncm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
function zncmBuildPagePrompt(page, content, truncated, followUp) {
const title = page.title || "未知标题";
const url = page.url || "未知 URL";
const capturedAt = new Date().toISOString();
if (followUp) {
return [
"你是 zncm 页面问答助手。请继续基于本会话里已经读取的网页内容回答用户问题。",
"如果已有网页内容不足以回答,请说明缺少哪些信息;不要编造,也不要再尝试抓取其它网页。",
`当前页面标题:${title}`,
`当前页面 URL:${url}`,
`本轮提问时间:${capturedAt}`,
].join("\n\n");
}
return [
"你是 zncm 页面问答助手。请直接基于当前网页内容回答用户问题。",
"如果网页内容不足以回答,请说明缺少哪些信息;不要编造,也不要再尝试抓取其它网页。",
`页面标题:${title}`,
`页面 URL:${url}`,
`读取时间:${capturedAt}`,
truncated ? "说明:页面内容较长,本次已按插件限制截断。" : "说明:页面内容如下。",
"当前网页内容:",
page.contentType === "text" ? `\`\`\`text\n${content}\n\`\`\`` : content,
].join("\n\n");
}
function zncmExtractLatestAnswer(values) {
const messages = Array.isArray(values?.messages) ? values.messages : [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index] || {};
const type = String(message.type || message.role || "").toLowerCase();
if (type !== "ai" && type !== "assistant") {
continue;
}
const content = message.content;
if (typeof content === "string" && content.trim()) {
return content.trim();
}
if (Array.isArray(content)) {
const text = content
.map((part) => {
if (typeof part === "string") return part;
if (part && typeof part.text === "string") return part.text;
return "";
})
.filter(Boolean)
.join("\n")
.trim();
if (text) {
return text;
}
}
}
return "";
}
async function askZncmViaRunsWait(settings, payload, page, question) {
const contentLimit = 60000;
const rawContent = String(page.content || "");
const truncated = rawContent.length > contentLimit;
const content = truncated ? `${rawContent.slice(0, contentLimit)}\n\n[Truncated locally by zncm Web Clipper]` : rawContent;
const threadId = payload.threadId || zncmMakeThreadId();
const promptPrefix = zncmBuildPagePrompt(page, content, truncated, Boolean(payload.threadId));
const { data } = await zncmFetchJson(settings, "/api/runs/wait", {
method: "POST",
headers: {
"Content-Type": "application/json",
...deerAuthHeaders(settings),
},
body: JSON.stringify({
assistant_id: "lead_agent",
input: {
messages: [
{
type: "human",
content: question,
additional_kwargs: {
prompt_prefix: promptPrefix,
},
},
],
},
config: {
configurable: {
thread_id: threadId,
},
},
metadata: {
source: "zncm_page_assistant",
browser_context_url: page.url || "",
browser_context_title: page.title || "",
browser_context_fallback: true,
},
stream_mode: ["values"],
on_disconnect: "continue",
multitask_strategy: "reject",
}),
});
return {
success: true,
thread_id: threadId,
run_id: "",
answer: zncmExtractLatestAnswer(data) || "zncm 没有返回可显示的答案。",
truncated,
message: "zncm 页面问答已完成。",
};
}
async function askZncmAboutPage(payload) {
let settings = await getSettings();
settings = await discoverServiceBases(settings);
settings = await syncAuthToken(settings);
if (!settings.pageAssistantEnabled) {
throw new Error("页面问答框已关闭。");
}
const page = payload?.page || {};
const question = String(payload?.question || "").trim();
if (!question) {
throw new Error("请输入要询问的问题。");
}
if (!page.content) {
throw new Error("当前页面没有可读取的正文。");
}
try {
const { data } = await zncmFetchJson(settings, "/api/browser-context/ask", {
method: "POST",
headers: {
"Content-Type": "application/json",
...deerAuthHeaders(settings),
},
body: JSON.stringify({
thread_id: payload.threadId || null,
title: page.title || "",
url: page.url || "",
content: page.content || "",
content_type: page.contentType || "markdown",
question,
}),
});
return data;
} catch (error) {
const detail = String(error?.detail || error?.message || "");
if (error?.status === 404 && !/browser context integration is disabled/i.test(detail)) {
return askZncmViaRunsWait(settings, payload, page, question);
}
throw error;
}
}
async function getActSessionMap() {
try {
const stored = await chrome.storage.session.get({ actSessions: {} });
return stored.actSessions && typeof stored.actSessions === "object" ? stored.actSessions : {};
} catch {
return {};
}
}
async function setActSessionTab(sessionId, tabId) {
if (!sessionId) return;
const map = await getActSessionMap();
map[sessionId] = tabId;
await chrome.storage.session.set({ actSessions: map }).catch(() => {});
}
async function removeActSession(sessionId) {
if (!sessionId) return;
const map = await getActSessionMap();
if (sessionId in map) {
delete map[sessionId];
await chrome.storage.session.set({ actSessions: map }).catch(() => {});
}
}
async function resolveSessionTabId(sessionId) {
const map = await getActSessionMap();
const tabId = map[sessionId];
if (!tabId) return null;
try {
await chrome.tabs.get(tabId);
return tabId;
} catch {
await removeActSession(sessionId);
return null;
}
}
async function performActionInTab(tabId, action) {
const [result] = await chrome.scripting.executeScript({
target: { tabId },
func: deerPerformAction,
args: [action],
});
return result?.result || { status: "ok", message: "" };
}
async function observeTab(tabId, maxElements) {
const [result] = await chrome.scripting.executeScript({
target: { tabId },
func: deerObservePage,
args: [maxElements],
});
const observation = result?.result;
if (!observation) {
throw new Error("当前页面无法读取交互元素。");
}
return observation;
}
async function runActTask(task, settings) {
const sessionId = task.session_id || "";
const action = task.action || {};
const actionType = String(action.type || "").toLowerCase();
const maxElements = 150;
try {
if (!sessionId) {
throw new Error("act 任务缺少 session_id。");
}
if (actionType === "close") {
const tabId = await resolveSessionTabId(sessionId);
if (tabId) {
await chrome.tabs.remove(tabId).catch(() => {});
}
await removeActSession(sessionId);
await postTaskResult(task.id, settings, {
kind: "act",
observation: { action_status: "ok", message: "Tab closed.", url: "", title: "", elements: [], text: "" },
metadata: { source: "zncm-web-clipper", worker_id: settings.workerId, session_id: sessionId },
});
return;
}
let tabId = await resolveSessionTabId(sessionId);
if (actionType === "navigate") {
const url = String(action.url || task.url || "");
if (!url) {
throw new Error("navigate 动作缺少 url。");
}
if (tabId) {
await chrome.tabs.update(tabId, { url });
} else {
const tab = await chrome.tabs.create({ url, active: false });
tabId = tab.id;
await setActSessionTab(sessionId, tabId);
}
await waitForTabLoad(tabId, settings.taskPageLoadTimeoutSeconds);
await delay(800);
} else {
if (!tabId) {
throw new Error("会话标签页不存在,请先用 navigate 打开页面。");
}
const outcome = await performActionInTab(tabId, action);
// Give the page a moment to react / navigate, then settle.
await delay(400);
await waitForTabLoad(tabId, settings.taskPageLoadTimeoutSeconds);
await delay(500);
if (outcome.status === "error") {
const observation = await observeTab(tabId, maxElements).catch(() => ({ elements: [], text: "", url: "", title: "" }));
await postTaskResult(task.id, settings, {
kind: "act",
observation: { ...observation, action_status: outcome.status, message: outcome.message },
metadata: { source: "zncm-web-clipper", worker_id: settings.workerId, session_id: sessionId },
});
return;
}
}
const observation = await observeTab(tabId, maxElements);
await postTaskResult(task.id, settings, {
kind: "act",
title: observation.title || "",
url: observation.url || "",
observation: { ...observation, action_status: "ok", message: "" },
metadata: { source: "zncm-web-clipper", worker_id: settings.workerId, session_id: sessionId },
});
} catch (error) {
await postTaskResult(task.id, settings, {
kind: "act",
error: error instanceof Error ? error.message : String(error),
metadata: { source: "zncm-web-clipper", worker_id: settings.workerId, session_id: sessionId },
}).catch((submitError) => console.warn("Failed to submit zncm act task error", submitError));
}
}
async function runFetchTask(task, settings) {
let tabId = null;
try {
const tab = await chrome.tabs.create({ url: task.url, active: false });
tabId = tab.id;
await waitForTabLoad(tabId, settings.taskPageLoadTimeoutSeconds);
await delay(750);
const page = await captureFromTab(tabId, task.max_content_chars);
await postTaskResult(task.id, settings, {
kind: "fetch",
title: page.title,
url: page.url,
content: page.content,
content_type: page.contentType || "markdown",
metadata: {
source: "zncm-web-clipper",
worker_id: settings.workerId,
instruction: task.instruction || "",
},
});
} catch (error) {
await postTaskResult(task.id, settings, {
kind: "fetch",
error: error instanceof Error ? error.message : String(error),
metadata: {
source: "zncm-web-clipper",
worker_id: settings.workerId,
},
}).catch((submitError) => console.warn("Failed to submit zncm task error", submitError));
} finally {
if (tabId && settings.closeTaskTabs) {
await chrome.tabs.remove(tabId).catch(() => {});
}
}
}
async function runTask(task, settings) {
if ((task.kind || "fetch") === "act") {
await runActTask(task, settings);
return;
}
await runFetchTask(task, settings);
}
async function pollOnce() {
if (isPolling) {
return { ok: true, message: "任务轮询正在运行。" };
}
isPolling = true;
try {
let settings = await getSettings();
settings = await discoverServiceBases(settings);
settings = await syncAuthToken(settings);
if (!settings.taskPollingEnabled) {
return { ok: false, message: "任务轮询已关闭。" };
}
const apiBaseUrl = deerNormalizeBaseUrl(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
const url = `${apiBaseUrl}/api/browser-context/tasks/next?worker_id=${encodeURIComponent(settings.workerId)}`;
const response = await fetch(url, { headers: deerAuthHeaders(settings) });
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const detail = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data);
throw new Error(detail || `HTTP ${response.status}`);
}
if (!data.task) {
return { ok: true, message: "暂无待处理的 zncm 浏览器任务。" };
}
await runTask(data.task, settings);
return { ok: true, message: `已完成 zncm 浏览器任务 ${data.task.id}。` };
} finally {
isPolling = false;
await scheduleNextPoll();
}
}
async function initializePolling() {
let settings = await getSettings();
try {
settings = await discoverServiceBases(settings);
} catch (error) {
// Service discovery persists diagnostic metadata in storage.sync. Chrome can
// temporarily reject those writes after its quota is reached; polling must
// still start with the last known settings.
console.warn("zncm service discovery failed during startup", error);
}
try {
settings = await syncAuthToken(settings);
} catch (error) {
// A failed status/token write must not prevent alarms from being installed.
// pollOnce() will retry token synchronization before claiming each task.
console.warn("zncm token sync failed during startup", error);
}
await refreshAlarm(settings);
await scheduleNextPoll(1000);
}
function startup() {
if (!startupPromise) {
startupPromise = initializePolling().finally(() => {
startupPromise = null;
});
}
return startupPromise;
}
async function restartPollingSchedule() {
const settings = await getSettings();
await refreshAlarm(settings);
await scheduleNextPoll(0);
}
chrome.runtime.onInstalled.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
chrome.runtime.onStartup.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
chrome.storage.onChanged.addListener((changes, areaName) => {
if (
areaName === "sync" &&
Object.keys(changes || {}).some((key) => POLLING_SCHEDULE_SETTING_KEYS.has(key))
) {
void restartPollingSchedule().catch((error) => console.warn("zncm polling reschedule failed", error));
}
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
void pollOnce().catch((error) => console.warn("zncm alarm poll failed", error));
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === "zncm-discover-service") {
getSettings()
.then((settings) => discoverServiceBases(settings, true))
.then((settings) =>
sendResponse({
ok: Boolean(settings.apiBaseUrl && settings.webBaseUrl),
message: `服务地址:${settings.webBaseUrl};API:${settings.apiBaseUrl}`,
apiBaseUrl: settings.apiBaseUrl,
webBaseUrl: settings.webBaseUrl,
source: settings.lastServiceSyncSource || "",
}),
)
.catch((error) => sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message?.type === "zncm-sync-token") {
syncAuthToken(null, true)
.then((settings) => {
const hasToken = Boolean(settings.accessToken);
sendResponse({
ok: hasToken,
message: hasToken
? `Token 已从 ${settings.lastTokenSyncSource || "zncm"} 同步。`
: "没有找到 zncm token。请打开已登录的 zncm 页面后重试。",
source: settings.lastTokenSyncSource || "",
});
})
.catch((error) => sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message?.type === "zncm-page-agent-ask") {
askZncmAboutPage(message)
.then((result) => sendResponse({ ok: true, ...result }))
.catch((error) => sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message?.type === "zncm-open-options") {
chrome.runtime.openOptionsPage()
.then(() => sendResponse({ ok: true }))
.catch((error) => sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message?.type !== "zncm-poll-now") {
return false;
}
pollOnce()
.then((result) => sendResponse(result))
.catch((error) => sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) }));
return true;
});
void startup().catch((error) => console.warn("zncm startup failed", error));
@@ -0,0 +1,130 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const BACKGROUND_PATH = path.join(__dirname, "background.js");
function createHarness() {
let storageChangedListener;
let alarmCreateCount = 0;
const settings = {
apiBaseUrl: "http://localhost:8001",
webBaseUrl: "http://localhost:5174",
autoDiscoverServiceUrl: false,
accessToken: "",
tokenSyncEnabled: false,
taskPollingEnabled: true,
workerId: "test-worker",
taskPollIntervalSeconds: 5,
};
const chrome = {
alarms: {
clear: async () => {},
create: async () => {
alarmCreateCount += 1;
},
onAlarm: { addListener: () => {} },
},
cookies: { get: async () => null },
runtime: {
onInstalled: { addListener: () => {} },
onMessage: { addListener: () => {} },
onStartup: { addListener: () => {} },
},
storage: {
local: {
get: async (defaults) => ({ ...defaults }),
set: async () => {},
},
session: { get: async () => ({ actSessions: {} }), set: async () => {} },
sync: {
get: async () => ({ ...settings }),
set: async (updates) => Object.assign(settings, updates),
},
onChanged: {
addListener: (listener) => {
storageChangedListener = listener;
},
},
},
tabs: { query: async () => [] },
};
const context = {
chrome,
console,
fetch: async () => {
throw new Error("offline test");
},
importScripts: () => {},
setTimeout: () => 1,
clearTimeout: () => {},
URL,
DEER_CLIPPER_DEFAULTS: settings,
deerNormalizeBaseUrl: (value) => String(value || "").replace(/\/+$/, ""),
deerMakeWorkerId: () => "test-worker",
deerAuthHeaders: () => ({}),
deerExtractAuthTokenFromPage: () => ({ token: "" }),
};
context.globalThis = context;
vm.runInNewContext(fs.readFileSync(BACKGROUND_PATH, "utf8"), context, {
filename: BACKGROUND_PATH,
});
return {
emitStorageChange(changes) {
storageChangedListener(changes, "sync");
},
getAlarmCreateCount: () => alarmCreateCount,
resetAlarmCreateCount: () => {
alarmCreateCount = 0;
},
getApiCandidates: (webBase, currentApiBase) =>
vm.runInContext(`apiCandidatesForWebBase(${JSON.stringify(webBase)}, ${JSON.stringify(currentApiBase)})`, context),
};
}
const settle = () => new Promise((resolve) => setImmediate(resolve));
test("self-written sync metadata does not restart browser task polling", async () => {
const harness = createHarness();
await settle();
harness.resetAlarmCreateCount();
harness.emitStorageChange({
lastTokenSyncAt: { oldValue: "before", newValue: "after" },
});
await settle();
assert.equal(harness.getAlarmCreateCount(), 0);
});
test("polling configuration changes restart browser task polling", async () => {
const harness = createHarness();
await settle();
harness.resetAlarmCreateCount();
harness.emitStorageChange({
taskPollingEnabled: { oldValue: false, newValue: true },
});
await settle();
assert.equal(harness.getAlarmCreateCount(), 1);
});
test("service discovery prefers the API paired with the open zncm tab", async () => {
const harness = createHarness();
assert.deepEqual(
Array.from(harness.getApiCandidates("http://localhost:5174", "http://47.94.209.59:2026")),
[
"http://localhost:5174",
"http://localhost:8001",
"http://47.94.209.59:2026",
"http://localhost:8001",
].filter((value, index, values) => values.indexOf(value) === index),
);
});
@@ -0,0 +1,9 @@
// Default deployment settings for zncm Web Clipper.
//
// Change these two values before loading or packaging the extension when the
// zncm service address changes. Users can still override them in the extension
// settings page after installation.
globalThis.ZNCM_WEB_CLIPPER_CONFIG = Object.freeze({
defaultWebBaseUrl: "http://47.94.209.59:2026",
defaultApiBaseUrl: "http://47.94.209.59:2026",
});
@@ -0,0 +1,777 @@
(() => {
if (window.top !== window || window.__zncmPageAssistantLoaded) {
return;
}
window.__zncmPageAssistantLoaded = true;
const HOST_ID = "zncm-page-assistant-host";
const state = {
enabled: true,
taskPollingEnabled: true,
webBaseUrl: "",
open: false,
busy: false,
threadId: "",
pageUrl: location.href,
messages: [],
};
let host = null;
let shadow = null;
let ui = {};
function sendRuntimeMessage(message) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(response || {});
});
});
}
function makeElement(tag, className = "", text = "") {
const element = document.createElement(tag);
if (className) {
element.className = className;
}
if (text) {
element.textContent = text;
}
return element;
}
function setStatus(message) {
if (ui.status) {
ui.status.textContent = message || "";
}
}
function setBusy(busy) {
state.busy = busy;
if (ui.send) ui.send.disabled = busy;
if (ui.input) ui.input.disabled = busy;
if (ui.panel) ui.panel.classList.toggle("is-busy", busy);
}
function resetThreadIfPageChanged() {
if (state.pageUrl === location.href) {
return;
}
state.pageUrl = location.href;
state.threadId = "";
state.messages = [];
renderMessages();
setStatus("页面已变化,下一次提问会重新读取当前页面。");
}
function destroyWidget() {
if (host) {
host.remove();
}
host = null;
shadow = null;
ui = {};
}
function renderMessages() {
if (!ui.messages) {
return;
}
ui.messages.replaceChildren();
if (state.messages.length === 0) {
const empty = makeElement("div", "empty");
const emptyTitle = makeElement("strong", "", "问问当前页面");
const emptyText = makeElement("span", "", "可以提问页面里的数据、表格、段落和结论。");
empty.append(emptyTitle, emptyText);
ui.messages.appendChild(empty);
return;
}
for (const message of state.messages) {
const item = makeElement("div", `message ${message.role}`);
const avatar = makeElement("div", "avatar", message.role === "user" ? "你" : "zn");
const bubble = makeElement("div", "bubble");
const label = makeElement("div", "message-label", message.role === "user" ? "你" : "zncm");
const body = makeElement("div", "message-body", message.text);
if (message.pending) {
body.classList.add("pending");
}
bubble.append(label, body);
item.append(avatar, bubble);
ui.messages.appendChild(item);
}
ui.messages.scrollTop = ui.messages.scrollHeight;
}
function setOpen(open) {
state.open = open;
if (!ui.panel || !ui.launcher) {
return;
}
ui.panel.classList.toggle("open", open);
ui.launcher.classList.toggle("open", open);
ui.launcher.setAttribute("aria-expanded", String(open));
if (open) {
resetThreadIfPageChanged();
setTimeout(() => ui.input?.focus(), 50);
}
}
async function ask(question) {
const normalized = String(question || "").trim();
if (!normalized || state.busy) {
return;
}
resetThreadIfPageChanged();
setBusy(true);
setStatus("正在读取页面并发送给 zncm...");
state.messages.push({ role: "user", text: normalized });
const pending = { role: "assistant", text: "zncm 正在理解当前页面...", pending: true };
state.messages.push(pending);
renderMessages();
ui.input.value = "";
try {
if (typeof deerExtractCurrentPage !== "function") {
throw new Error("页面读取脚本未加载,请刷新当前页面后重试。");
}
const page = deerExtractCurrentPage();
const response = await sendRuntimeMessage({
type: "zncm-page-agent-ask",
page,
question: normalized,
threadId: state.threadId || "",
});
if (!response.ok) {
throw new Error(response.message || "zncm 没有返回答案。");
}
state.threadId = response.thread_id || state.threadId;
pending.text = response.answer || "zncm 没有返回答案。";
pending.pending = false;
setStatus(response.truncated ? "已回答。页面内容较长,本次按配置做了截断。" : "已回答。");
} catch (error) {
pending.text = error instanceof Error ? error.message : "页面问答失败。";
pending.pending = false;
setStatus("本次问答失败。");
} finally {
setBusy(false);
renderMessages();
ui.input.focus();
}
}
function buildWidget() {
host = document.createElement("div");
host.id = HOST_ID;
document.documentElement.appendChild(host);
shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = `
:host {
all: initial;
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.launcher,
.panel {
position: fixed;
z-index: 2147483647;
font: 14px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.launcher {
right: 20px;
bottom: 20px;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 8px;
height: 44px;
min-width: 112px;
border: 1px solid rgba(15, 118, 110, 0.28);
border-radius: 8px;
padding: 0 12px 0 10px;
background: #0f766e;
color: #fff;
box-shadow: 0 12px 34px rgba(15, 118, 110, 0.28), 0 2px 8px rgba(17, 24, 39, 0.12);
cursor: pointer;
font-weight: 750;
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.launcher:hover {
transform: translateY(-2px);
box-shadow: 0 16px 40px rgba(15, 118, 110, 0.32), 0 4px 12px rgba(17, 24, 39, 0.14);
}
.launcher-mark {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.18);
font-size: 12px;
letter-spacing: 0;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.launcher.open {
background: #115e59;
}
.panel {
right: 20px;
bottom: 72px;
width: min(430px, calc(100vw - 24px));
max-height: min(640px, calc(100vh - 96px));
display: none;
grid-template-rows: auto auto minmax(180px, 1fr) auto;
overflow: hidden;
border: 1px solid #d6d8dc;
border-radius: 8px;
background: #ffffff;
color: #171717;
box-shadow: 0 24px 70px rgba(17, 24, 39, 0.2), 0 1px 2px rgba(17, 24, 39, 0.08);
}
.panel.open {
display: grid;
animation: zncm-panel-in 160ms ease-out;
}
@keyframes zncm-panel-in {
from {
opacity: 0;
transform: translateY(8px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.header {
display: grid;
gap: 10px;
padding: 14px;
background: linear-gradient(180deg, #f4fffc 0%, #ffffff 84%);
border-bottom: 1px solid #e5e7eb;
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.brand {
display: flex;
align-items: center;
min-width: 0;
gap: 10px;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
flex: 0 0 auto;
border-radius: 8px;
background: #0f766e;
color: #fff;
font-weight: 800;
box-shadow: 0 8px 20px rgba(15, 118, 110, 0.18);
}
.brand-copy {
display: grid;
min-width: 0;
gap: 1px;
}
.brand-copy strong {
color: #111827;
font-size: 15px;
line-height: 1.25;
}
.brand-copy span {
overflow: hidden;
color: #6b7280;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-actions {
display: flex;
flex: 0 0 auto;
gap: 6px;
}
.icon-button {
display: grid;
place-items: center;
width: auto;
min-width: 34px;
height: 30px;
padding: 0 8px;
border: 1px solid #dfe3e8;
border-radius: 7px;
background: #fff;
color: #374151;
cursor: pointer;
font: inherit;
font-size: 12px;
white-space: nowrap;
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.icon-button:hover {
border-color: #0f766e;
color: #0f766e;
background: #f0fdfa;
}
.page-chip {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 8px;
min-width: 0;
border: 1px solid #dbeafe;
border-radius: 8px;
padding: 8px 10px;
background: #f8fbff;
color: #334155;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.page-chip-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: #0f766e;
}
.page-chip-text {
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
min-height: 20px;
padding: 9px 14px;
border-bottom: 1px solid #f0f2f5;
color: #475569;
background: #fbfcfd;
font-size: 12px;
}
.messages {
display: grid;
align-content: start;
gap: 12px;
overflow: auto;
padding: 14px;
background: linear-gradient(180deg, #f7f8fa 0%, #f3f6f8 100%);
}
.empty {
display: grid;
gap: 5px;
padding: 16px;
border: 1px solid #dfe7ed;
border-radius: 8px;
background: linear-gradient(180deg, #ffffff 0%, #fbfefd 100%);
color: #4b5563;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
}
.empty strong {
color: #111827;
font-size: 14px;
}
.empty span {
font-size: 13px;
}
.message {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 8px;
align-items: start;
}
.message.user {
grid-template-columns: minmax(0, 1fr) 28px;
}
.message.user .avatar {
grid-column: 2;
}
.message.user .bubble {
grid-column: 1;
justify-self: end;
}
.avatar {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: #e6fffb;
color: #0f766e;
font-size: 11px;
font-weight: 800;
}
.message.user .avatar {
background: #111827;
color: #fff;
}
.bubble {
display: grid;
gap: 4px;
max-width: 100%;
}
.message-label {
color: #6b7280;
font-size: 11px;
}
.message.user .message-label {
text-align: right;
}
.message-body {
max-width: 330px;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
color: #171717;
white-space: pre-wrap;
overflow-wrap: anywhere;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.message.user .message-body {
border-color: #0f172a;
background: #0f172a;
color: #fff;
}
.message-body.pending {
color: #6b7280;
}
.message-body.pending::after {
content: "";
display: inline-block;
width: 18px;
height: 4px;
margin-left: 6px;
border-radius: 999px;
background: linear-gradient(90deg, #0f766e 0 33%, transparent 33% 100%);
background-size: 9px 4px;
animation: zncm-thinking 900ms linear infinite;
vertical-align: middle;
}
@keyframes zncm-thinking {
from {
background-position: 0 0;
}
to {
background-position: 18px 0;
}
}
.composer-wrap {
display: grid;
gap: 10px;
padding: 12px 14px 14px;
background: #fff;
border-top: 1px solid #e5e7eb;
}
.quick {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
.quick button {
border: 1px solid #d6d8dc;
border-radius: 8px;
padding: 8px 10px;
background: #fbfcfd;
color: #374151;
cursor: pointer;
font: inherit;
font-size: 12px;
text-align: left;
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
}
.quick button:hover {
border-color: #0f766e;
color: #0f766e;
background: #f0fdfa;
transform: translateY(-1px);
}
.composer {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
align-items: end;
}
.composer textarea {
min-height: 46px;
max-height: 130px;
resize: vertical;
border: 1px solid #cfd6dd;
border-radius: 8px;
padding: 10px 11px;
color: #171717;
background: #fbfcfd;
font: inherit;
outline: none;
}
.composer textarea:focus {
border-color: #0f766e;
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.12);
}
.composer button {
height: 42px;
border: 0;
border-radius: 8px;
padding: 0 14px;
background: #0f766e;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 750;
box-shadow: 0 8px 18px rgba(15, 118, 110, 0.2);
transition: transform 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.composer button:hover {
background: #115e59;
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(15, 118, 110, 0.24);
}
.panel.is-busy .brand-mark,
.panel.is-busy .launcher-mark {
opacity: 0.82;
}
.composer button:disabled,
.quick button:disabled,
.icon-button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
@media (max-width: 480px) {
.launcher {
right: 12px;
bottom: 12px;
min-width: 96px;
}
.panel {
right: 12px;
bottom: 64px;
width: calc(100vw - 24px);
max-height: calc(100vh - 80px);
}
.message-body {
max-width: calc(100vw - 104px);
}
.quick {
grid-template-columns: 1fr;
}
}
`;
const launcher = makeElement("button", "launcher");
launcher.type = "button";
launcher.title = "打开 zncm 页面问答";
launcher.setAttribute("aria-expanded", "false");
launcher.append(makeElement("span", "launcher-mark", "zn"), makeElement("span", "", "页面问答"));
const panel = makeElement("section", "panel");
const header = makeElement("header", "header");
const headerTop = makeElement("div", "header-top");
const brand = makeElement("div", "brand");
const brandCopy = makeElement("div", "brand-copy");
const title = makeElement("strong", "", "zncm 页面问答");
const subtitle = makeElement("span", "", "读取当前页面并回答");
brandCopy.append(title, subtitle);
brand.append(makeElement("div", "brand-mark", "zn"), brandCopy);
const actions = makeElement("div", "header-actions");
const settings = makeElement("button", "icon-button", "设置");
settings.type = "button";
settings.title = "打开插件设置";
const minimize = makeElement("button", "icon-button", "-");
minimize.type = "button";
minimize.title = "收起";
actions.append(settings, minimize);
headerTop.append(brand, actions);
const pageChip = makeElement("div", "page-chip");
const pageTitle = makeElement("span", "page-chip-text", document.title || location.hostname);
pageChip.append(makeElement("span", "page-chip-dot"), pageTitle);
header.append(headerTop, pageChip);
const status = makeElement("div", "status", "已连接当前页面。");
const messages = makeElement("div", "messages");
const composerWrap = makeElement("div", "composer-wrap");
const quick = makeElement("div", "quick");
for (const text of ["总结此页", "提取关键数据", "列出待办", "解释表格"]) {
const chip = makeElement("button", "", text);
chip.type = "button";
chip.addEventListener("click", () => ask(text));
quick.appendChild(chip);
}
const composer = makeElement("form", "composer");
const input = makeElement("textarea");
input.placeholder = "问当前页面...";
input.rows = 2;
const send = makeElement("button", "", "发送");
send.type = "submit";
composer.append(input, send);
composerWrap.append(quick, composer);
panel.append(header, status, messages, composerWrap);
shadow.append(style, panel, launcher);
ui = { launcher, panel, subtitle, pageTitle, status, messages, input, send };
renderMessages();
launcher.addEventListener("click", () => setOpen(!state.open));
minimize.addEventListener("click", () => setOpen(false));
settings.addEventListener("click", () => {
sendRuntimeMessage({ type: "zncm-open-options" }).catch((error) => {
setStatus(error instanceof Error ? error.message : "无法打开插件设置。");
});
});
composer.addEventListener("submit", (event) => {
event.preventDefault();
ask(input.value);
});
input.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
ask(input.value);
}
});
}
function ensureWidget() {
if (!state.enabled) {
destroyWidget();
return;
}
if (document.getElementById(HOST_ID)) {
return;
}
buildWidget();
}
async function loadSettings() {
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
state.enabled = Boolean(settings.pageAssistantEnabled);
state.taskPollingEnabled = Boolean(settings.taskPollingEnabled);
state.webBaseUrl = deerNormalizeBaseUrl(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
ensureWidget();
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "sync") {
return;
}
if (changes.pageAssistantEnabled) {
state.enabled = Boolean(changes.pageAssistantEnabled.newValue);
ensureWidget();
if (state.enabled) {
setStatus("页面问答框已开启。");
}
}
if (changes.taskPollingEnabled) {
state.taskPollingEnabled = Boolean(changes.taskPollingEnabled.newValue);
}
if (changes.webBaseUrl) {
state.webBaseUrl = deerNormalizeBaseUrl(changes.webBaseUrl.newValue || DEER_CLIPPER_DEFAULTS.webBaseUrl);
}
});
function isZncmPage() {
const appPath = `${location.pathname}${location.hash}`.toLowerCase();
if (
appPath.includes("/page/workspace") ||
appPath.includes("/chats/") ||
appPath.includes("#/login")
) {
return true;
}
try {
const configured = new URL(state.webBaseUrl);
return configured.origin === location.origin;
} catch {
return false;
}
}
// MV3 service workers may be suspended between their short polling timers,
// while chrome.alarms cannot reliably run more often than every 30 seconds.
// A visible zncm tab provides a cheap wake-up signal so browser_act requests
// are normally claimed within the backend/model timeout window.
setInterval(() => {
if (!state.taskPollingEnabled || !isZncmPage()) {
return;
}
chrome.runtime.sendMessage({ type: "zncm-poll-now" }).catch(() => {});
}, 3000);
setInterval(() => {
if (!state.enabled || !ui.pageTitle) {
return;
}
if (state.pageUrl !== location.href) {
resetThreadIfPageChanged();
}
ui.pageTitle.textContent = document.title || location.hostname;
}, 1200);
void loadSettings();
})();
@@ -0,0 +1,24 @@
{
"manifest_version": 3,
"name": "zncm Web Clipper",
"version": "0.2.0",
"description": "Send pages to zncm and execute browser-backed fetch tasks.",
"permissions": ["activeTab", "alarms", "cookies", "scripting", "storage", "tabs"],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "zncm Web Clipper",
"default_popup": "popup.html"
},
"options_page": "options.html",
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["config.js", "shared.js", "content.js"],
"run_at": "document_idle",
"all_frames": false
}
]
}
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>zncm Web Clipper Settings</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="options">
<h1>zncm Web Clipper Settings</h1>
<label>
API 地址
<input id="apiBaseUrl" />
</label>
<label>
前端地址
<input id="webBaseUrl" />
</label>
<label class="checkbox">
<input id="autoDiscoverServiceUrl" type="checkbox" />
自动识别已打开 zncm 页面的服务地址
</label>
<div class="actions">
<button id="discoverService" type="button">重新识别服务地址</button>
</div>
<p id="serviceStatus" class="status"></p>
<label>
Access Token
<textarea id="accessToken" rows="5" placeholder="会自动从已登录 zncm 页面或 access_token Cookie 获取。也可以手动粘贴。"></textarea>
</label>
<label class="checkbox">
<input id="tokenSyncEnabled" type="checkbox" />
自动同步已登录 zncm 的 token
</label>
<div class="actions">
<button id="syncToken" type="button">Sync token now</button>
</div>
<p id="tokenStatus" class="status"></p>
<label class="checkbox">
<input id="pageAssistantEnabled" type="checkbox" />
启用页面问答框
</label>
<label>
默认首问
<textarea id="defaultPrompt" rows="4"></textarea>
</label>
<label class="checkbox">
<input id="taskPollingEnabled" type="checkbox" />
启用 zncm 聊天触发的浏览器抓取任务
</label>
<label>
Worker ID
<input id="workerId" placeholder="自动生成" />
</label>
<label>
任务轮询间隔秒数
<input id="taskPollIntervalSeconds" type="number" min="3" step="1" />
</label>
<label>
页面加载超时秒数
<input id="taskPageLoadTimeoutSeconds" type="number" min="5" step="1" />
</label>
<label class="checkbox">
<input id="closeTaskTabs" type="checkbox" />
抓取完成后关闭任务标签页
</label>
<button id="save" type="button">保存</button>
<p id="status" class="status"></p>
</main>
<script src="config.js"></script>
<script src="shared.js"></script>
<script src="options.js"></script>
</body>
</html>
@@ -0,0 +1,148 @@
const textFields = [
"apiBaseUrl",
"webBaseUrl",
"accessToken",
"defaultPrompt",
"workerId",
"taskPollIntervalSeconds",
"taskPageLoadTimeoutSeconds",
];
const checkboxFields = [
"autoDiscoverServiceUrl",
"tokenSyncEnabled",
"pageAssistantEnabled",
"taskPollingEnabled",
"closeTaskTabs",
];
function formatServiceStatus(settings) {
if (!settings.lastServiceSyncAt) {
return `当前服务地址:${settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl};API:${settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl}`;
}
return `当前服务地址:${settings.webBaseUrl || ""};API:${settings.apiBaseUrl || ""}\n上次识别:${settings.lastServiceSyncAt}(${settings.lastServiceSyncSource || "unknown source"})`;
}
function formatTokenStatus(settings) {
if (!settings.lastTokenSyncAt) {
return "Token 尚未自动同步。";
}
return `上次 token 同步:${settings.lastTokenSyncAt}(${settings.lastTokenSyncSource || "unknown source"})`;
}
async function loadSettings() {
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
if (!settings.workerId) {
settings.workerId = deerMakeWorkerId();
await chrome.storage.sync.set({ workerId: settings.workerId });
}
document.getElementById("apiBaseUrl").placeholder =
DEER_CLIPPER_DEFAULTS.apiBaseUrl || "在 config.js 中配置默认 API 地址";
document.getElementById("webBaseUrl").placeholder =
DEER_CLIPPER_DEFAULTS.webBaseUrl || "在 config.js 中配置默认前端地址";
for (const field of textFields) {
document.getElementById(field).value = settings[field] ?? "";
}
for (const field of checkboxFields) {
document.getElementById(field).checked = Boolean(settings[field]);
}
document.getElementById("serviceStatus").textContent = formatServiceStatus(settings);
document.getElementById("tokenStatus").textContent = formatTokenStatus(settings);
}
async function saveSettings() {
const next = {};
for (const field of textFields) {
next[field] = document.getElementById(field).value.trim();
}
for (const field of checkboxFields) {
next[field] = document.getElementById(field).checked;
}
next.apiBaseUrl = deerNormalizeBaseUrl(next.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
next.webBaseUrl = deerNormalizeBaseUrl(next.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
next.defaultPrompt = next.defaultPrompt || DEER_CLIPPER_DEFAULT_PROMPT;
next.workerId = next.workerId || deerMakeWorkerId();
next.taskPollIntervalSeconds = Math.max(3, Number(next.taskPollIntervalSeconds || DEER_CLIPPER_DEFAULTS.taskPollIntervalSeconds));
next.taskPageLoadTimeoutSeconds = Math.max(5, Number(next.taskPageLoadTimeoutSeconds || DEER_CLIPPER_DEFAULTS.taskPageLoadTimeoutSeconds));
await chrome.storage.sync.set(next);
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
document.getElementById("status").textContent = "已保存。";
document.getElementById("serviceStatus").textContent = formatServiceStatus(settings);
document.getElementById("tokenStatus").textContent = formatTokenStatus(settings);
}
function sendDiscoverServiceMessage() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: "zncm-discover-service" }, (response) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(response || { ok: false, message: "服务地址识别没有返回结果。" });
});
});
}
function sendSyncTokenMessage() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: "zncm-sync-token" }, (response) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(response || { ok: false, message: "Token 同步没有返回结果。" });
});
});
}
async function discoverServiceNow() {
const button = document.getElementById("discoverService");
button.disabled = true;
document.getElementById("serviceStatus").textContent = "正在识别服务地址...";
try {
await chrome.storage.sync.set({
autoDiscoverServiceUrl: document.getElementById("autoDiscoverServiceUrl").checked,
apiBaseUrl: deerNormalizeBaseUrl(document.getElementById("apiBaseUrl").value || DEER_CLIPPER_DEFAULTS.apiBaseUrl),
webBaseUrl: deerNormalizeBaseUrl(document.getElementById("webBaseUrl").value || DEER_CLIPPER_DEFAULTS.webBaseUrl),
});
const response = await sendDiscoverServiceMessage();
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
document.getElementById("apiBaseUrl").value = settings.apiBaseUrl || "";
document.getElementById("webBaseUrl").value = settings.webBaseUrl || "";
document.getElementById("serviceStatus").textContent = response.message || formatServiceStatus(settings);
} catch (error) {
document.getElementById("serviceStatus").textContent = error instanceof Error ? error.message : "服务地址识别失败。";
} finally {
button.disabled = false;
}
}
async function syncTokenNow() {
const button = document.getElementById("syncToken");
button.disabled = true;
document.getElementById("tokenStatus").textContent = "正在同步 zncm token...";
try {
await chrome.storage.sync.set({
tokenSyncEnabled: document.getElementById("tokenSyncEnabled").checked,
apiBaseUrl: deerNormalizeBaseUrl(document.getElementById("apiBaseUrl").value || DEER_CLIPPER_DEFAULTS.apiBaseUrl),
webBaseUrl: deerNormalizeBaseUrl(document.getElementById("webBaseUrl").value || DEER_CLIPPER_DEFAULTS.webBaseUrl),
});
const response = await sendSyncTokenMessage();
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
document.getElementById("accessToken").value = settings.accessToken || "";
document.getElementById("apiBaseUrl").value = settings.apiBaseUrl || "";
document.getElementById("webBaseUrl").value = settings.webBaseUrl || "";
document.getElementById("serviceStatus").textContent = formatServiceStatus(settings);
document.getElementById("tokenStatus").textContent = response.message || formatTokenStatus(settings);
} catch (error) {
document.getElementById("tokenStatus").textContent = error instanceof Error ? error.message : "Token 同步失败。";
} finally {
button.disabled = false;
}
}
document.getElementById("save").addEventListener("click", saveSettings);
document.getElementById("discoverService").addEventListener("click", discoverServiceNow);
document.getElementById("syncToken").addEventListener("click", syncTokenNow);
void loadSettings();
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>zncm Web Clipper</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="popup">
<h1>zncm Web Clipper</h1>
<label>
首问
<textarea id="prompt" rows="4"></textarea>
</label>
<label class="checkbox">
<input id="pageAssistantEnabled" type="checkbox" />
启用页面问答框
</label>
<div class="actions">
<button id="send" type="button">发送当前页</button>
<button id="syncToken" type="button">同步 token</button>
<button id="pollNow" type="button">检查任务</button>
<button id="options" type="button" class="secondary">设置</button>
</div>
<p id="status" class="status"></p>
</main>
<script src="config.js"></script>
<script src="shared.js"></script>
<script src="popup.js"></script>
</body>
</html>
+151
View File
@@ -0,0 +1,151 @@
function setStatus(message) {
document.getElementById("status").textContent = message;
}
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) {
throw new Error("No readable active tab found.");
}
return tab;
}
async function captureFromActiveTab(tabId) {
const [result] = await chrome.scripting.executeScript({
target: { tabId },
func: deerExtractCurrentPage,
});
if (!result?.result?.content) {
throw new Error("The current page has no readable text to send.");
}
return result.result;
}
async function submitToZncm(page, settings, prompt) {
const apiBaseUrl = deerNormalizeBaseUrl(settings.apiBaseUrl || DEER_CLIPPER_DEFAULTS.apiBaseUrl);
const response = await fetch(`${apiBaseUrl}/api/browser-context/capture`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...deerAuthHeaders(settings),
},
body: JSON.stringify({
title: page.title,
url: page.url,
content: page.content,
content_type: page.contentType,
prompt,
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const detail = typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data);
throw new Error(detail || `HTTP ${response.status}`);
}
return data;
}
async function sendCurrentPage() {
const sendButton = document.getElementById("send");
sendButton.disabled = true;
setStatus("正在读取当前页面...");
try {
await syncToken({ silent: true });
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
const prompt = document.getElementById("prompt").value.trim() || settings.defaultPrompt || DEER_CLIPPER_DEFAULT_PROMPT;
const tab = await getActiveTab();
const page = await captureFromActiveTab(tab.id);
setStatus("正在发送到 zncm...");
const result = await submitToZncm(page, settings, prompt);
const webBaseUrl = deerNormalizeBaseUrl(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
await chrome.tabs.create({ url: `${webBaseUrl}/${result.chat_url}`.replace(/\/#/g, "/#") });
setStatus("已发送,zncm 聊天页面已打开。");
} catch (error) {
setStatus(error instanceof Error ? error.message : "发送失败。");
} finally {
sendButton.disabled = false;
}
}
function sendSyncTokenMessage() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: "zncm-sync-token" }, (response) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(response || { ok: false, message: "Token 同步没有返回结果。" });
});
});
}
function sendPollNowMessage() {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: "zncm-poll-now" }, (response) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(response || { ok: true, message: "Poll requested." });
});
});
}
async function syncToken(options = {}) {
const button = document.getElementById("syncToken");
button.disabled = true;
if (!options.silent) {
setStatus("正在同步 zncm token...");
}
try {
const response = await sendSyncTokenMessage();
if (!options.silent) {
setStatus(response.message || "Token sync finished.");
}
return response;
} catch (error) {
if (!options.silent) {
setStatus(error instanceof Error ? error.message : "Token 同步失败。");
}
return { ok: false, message: error instanceof Error ? error.message : "Token 同步失败。" };
} finally {
button.disabled = false;
}
}
async function pollNow() {
const button = document.getElementById("pollNow");
button.disabled = true;
setStatus("正在检查 zncm 浏览器任务...");
try {
await syncToken({ silent: true });
const response = await sendPollNowMessage();
setStatus(response.message || "任务检查完成。");
} catch (error) {
setStatus(error instanceof Error ? error.message : "任务检查失败。");
} finally {
button.disabled = false;
}
}
async function togglePageAssistant() {
const enabled = document.getElementById("pageAssistantEnabled").checked;
await chrome.storage.sync.set({ pageAssistantEnabled: enabled });
setStatus(enabled ? "页面问答框已开启。" : "页面问答框已关闭。");
}
async function init() {
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
document.getElementById("prompt").value = settings.defaultPrompt || DEER_CLIPPER_DEFAULT_PROMPT;
document.getElementById("pageAssistantEnabled").checked = Boolean(settings.pageAssistantEnabled);
document.getElementById("send").addEventListener("click", sendCurrentPage);
document.getElementById("syncToken").addEventListener("click", () => syncToken());
document.getElementById("pollNow").addEventListener("click", pollNow);
document.getElementById("pageAssistantEnabled").addEventListener("change", togglePageAssistant);
document.getElementById("options").addEventListener("click", () => chrome.runtime.openOptionsPage());
}
void init();
@@ -0,0 +1,389 @@
const DEER_CLIPPER_DEFAULT_PROMPT =
"\u8bf7\u57fa\u4e8e\u521a\u521a\u53d1\u9001\u7684\u7f51\u9875\u5185\u5bb9\uff0c\u5148\u7528\u4e2d\u6587\u603b\u7ed3\u8981\u70b9\uff0c\u5e76\u7b49\u5f85\u6211\u7ee7\u7eed\u63d0\u95ee\u3002";
const ZNCM_EXTENSION_CONFIG = globalThis.ZNCM_WEB_CLIPPER_CONFIG || {};
const ZNCM_DEFAULT_WEB_BASE_URL = String(ZNCM_EXTENSION_CONFIG.defaultWebBaseUrl || "").trim().replace(/\/+$/, "");
const ZNCM_DEFAULT_API_BASE_URL = String(ZNCM_EXTENSION_CONFIG.defaultApiBaseUrl || "").trim().replace(/\/+$/, "");
const DEER_CLIPPER_DEFAULTS = {
apiBaseUrl: ZNCM_DEFAULT_API_BASE_URL,
webBaseUrl: ZNCM_DEFAULT_WEB_BASE_URL,
autoDiscoverServiceUrl: true,
lastServiceSyncAt: "",
lastServiceSyncSource: "",
accessToken: "",
tokenSyncEnabled: true,
lastTokenSyncAt: "",
lastTokenSyncSource: "",
defaultPrompt: DEER_CLIPPER_DEFAULT_PROMPT,
pageAssistantEnabled: true,
taskPollingEnabled: true,
workerId: "",
taskPollIntervalSeconds: 5,
taskPageLoadTimeoutSeconds: 30,
closeTaskTabs: false,
};
function deerNormalizeBaseUrl(value) {
return String(value || "").trim().replace(/\/+$/, "");
}
function deerAuthHeaders(settings) {
const token = String(settings.accessToken || "").trim();
if (!token) {
return {};
}
return {
Authorization: /^bearer\s+/i.test(token) ? token : `Bearer ${token}`,
};
}
function deerMakeWorkerId() {
return `zncm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
function deerExtractAuthTokenFromPage() {
const storageKeys = ["deerflow.auth"];
const tokenFieldNames = new Set(["access_token", "accessToken", "token", "id_token", "idToken"]);
function looksLikeToken(value) {
const text = String(value || "").trim();
return text.length > 20 && !/\s/.test(text);
}
function findToken(value, depth = 0) {
if (value == null || depth > 4) {
return "";
}
if (typeof value === "string") {
const text = value.trim();
if (!text) {
return "";
}
if ((text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"))) {
try {
return findToken(JSON.parse(text), depth + 1);
} catch {
return looksLikeToken(text) ? text : "";
}
}
return looksLikeToken(text) ? text : "";
}
if (typeof value !== "object") {
return "";
}
for (const [key, nested] of Object.entries(value)) {
if (tokenFieldNames.has(key) && looksLikeToken(nested)) {
return String(nested).trim();
}
}
for (const nested of Object.values(value)) {
const token = findToken(nested, depth + 1);
if (token) {
return token;
}
}
return "";
}
function scanStorage(storage, storageName) {
for (const key of storageKeys) {
const token = findToken(storage.getItem(key));
if (token) {
return { token, source: `${storageName}:${key}` };
}
}
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (!key || !/(auth|token|session)/i.test(key)) {
continue;
}
const token = findToken(storage.getItem(key));
if (token) {
return { token, source: `${storageName}:${key}` };
}
}
return null;
}
try {
return scanStorage(window.localStorage, "localStorage") || scanStorage(window.sessionStorage, "sessionStorage");
} catch (error) {
return { token: "", source: "", error: error instanceof Error ? error.message : String(error) };
}
}
function deerExtractCurrentPage() {
const maxLocalChars = 220000;
function cleanText(value) {
return String(value || "")
.replace(/\s+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function escapeCell(value) {
return cleanText(value).replace(/\|/g, "\\|").replace(/\n/g, " ");
}
function tableToMarkdown(table, index) {
const rows = Array.from(table.rows || []).map((row) =>
Array.from(row.cells || []).map((cell) => escapeCell(cell.innerText)),
);
if (rows.length === 0 || rows.every((row) => row.length === 0)) {
return "";
}
const width = Math.max(...rows.map((row) => row.length));
const normalized = rows.map((row) => {
const next = row.slice();
while (next.length < width) next.push("");
return next;
});
const header = normalized[0];
const divider = Array.from({ length: width }, () => "---");
const body = normalized.slice(1);
return [
`### Table ${index + 1}`,
"",
`| ${header.join(" | ")} |`,
`| ${divider.join(" | ")} |`,
...body.map((row) => `| ${row.join(" | ")} |`),
].join("\n");
}
const clone = document.body ? document.body.cloneNode(true) : null;
if (clone) {
clone.querySelectorAll("script, style, noscript, svg, canvas, iframe").forEach((node) => node.remove());
}
const title = document.title || "";
const url = location.href;
const description = document.querySelector('meta[name="description"]')?.getAttribute("content") || "";
const main = clone?.querySelector("article, main, [role='main']") || clone;
const text = cleanText(main?.innerText || document.body?.innerText || "");
const tables = Array.from(document.querySelectorAll("table"))
.map((table, index) => tableToMarkdown(table, index))
.filter(Boolean);
const parts = [];
if (description.trim()) {
parts.push(`## Meta Description\n\n${cleanText(description)}`);
}
if (text) {
parts.push(`## Page Text\n\n${text}`);
}
if (tables.length > 0) {
parts.push(`## Tables\n\n${tables.join("\n\n")}`);
}
let content = parts.join("\n\n");
if (content.length > maxLocalChars) {
content = `${content.slice(0, maxLocalChars)}\n\n[Truncated locally by zncm Web Clipper]`;
}
return { title, url, content, contentType: "markdown" };
}
// Injected into the page to build an indexed list of interactive elements plus
// the visible page text. Each interactive element is tagged with
// `data-deer-index` so a follow-up deerPerformAction call can find it by index.
function deerObservePage(maxElements) {
const limit = Math.max(10, Number(maxElements || 150));
const DEER_ATTR = "data-deer-index";
document.querySelectorAll(`[${DEER_ATTR}]`).forEach((node) => node.removeAttribute(DEER_ATTR));
function isVisible(el) {
if (!(el instanceof Element)) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 1 && rect.height <= 1) return false;
const style = window.getComputedStyle(el);
if (style.visibility === "hidden" || style.display === "none" || style.opacity === "0") return false;
if (el.disabled) return false;
return true;
}
function labelFor(el) {
const aria = el.getAttribute("aria-label");
if (aria && aria.trim()) return aria.trim();
const placeholder = el.getAttribute("placeholder");
const title = el.getAttribute("title");
const name = el.getAttribute("name");
const text = (el.innerText || el.textContent || "").trim();
if (text) return text.replace(/\s+/g, " ");
if (placeholder && placeholder.trim()) return placeholder.trim();
if (title && title.trim()) return title.trim();
if (el.value && String(el.value).trim()) return String(el.value).trim();
if (name && name.trim()) return name.trim();
const alt = el.querySelector?.("img[alt]")?.getAttribute("alt");
if (alt && alt.trim()) return alt.trim();
return "";
}
const selector = [
"a[href]",
"button",
"input:not([type=hidden])",
"select",
"textarea",
"[role=button]",
"[role=link]",
"[role=tab]",
"[role=menuitem]",
"[role=checkbox]",
"[role=radio]",
"[role=switch]",
"[contenteditable=true]",
"[onclick]",
].join(",");
const seen = new Set();
const elements = [];
let index = 0;
for (const el of document.querySelectorAll(selector)) {
if (index >= limit) break;
if (seen.has(el) || !isVisible(el)) continue;
seen.add(el);
el.setAttribute(DEER_ATTR, String(index));
const tag = el.tagName.toLowerCase();
let label = labelFor(el);
if (label.length > 200) label = label.slice(0, 200);
const entry = { index, tag, label };
const type = el.getAttribute("type") || el.getAttribute("role");
if (type) entry.type = type;
if (tag === "input" || tag === "textarea" || tag === "select") {
const value = String(el.value || "");
if (value) entry.value = value.slice(0, 80);
}
if (tag === "a") {
const href = el.getAttribute("href") || "";
if (href) entry.href = href.slice(0, 200);
}
elements.push(entry);
index += 1;
}
// Inline text extraction so this function stays self-contained when injected
// via chrome.scripting.executeScript (no dependency on other content-script fns).
let text = "";
try {
const clone = document.body ? document.body.cloneNode(true) : null;
if (clone) {
clone.querySelectorAll(`script, style, noscript, svg, canvas, iframe, [${DEER_ATTR}-skip]`).forEach((node) => node.remove());
}
const main = clone?.querySelector("article, main, [role='main']") || clone;
text = String(main?.innerText || document.body?.innerText || "")
.replace(/\s+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
} catch {
text = "";
}
return {
url: location.href,
title: document.title || "",
elements,
text,
};
}
// Injected into the page to perform a single action on the element previously
// tagged by deerObservePage (by index). Returns {status, message}.
async function deerPerformAction(action) {
const DEER_ATTR = "data-deer-index";
const type = String(action?.type || "").toLowerCase();
function findByIndex(idx) {
if (idx === undefined || idx === null || idx < 0) return null;
return document.querySelector(`[${DEER_ATTR}="${idx}"]`);
}
function fireInput(el, value) {
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
if (setter) {
setter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
try {
if (type === "observe" || type === "extract") {
return { status: "ok", message: type === "extract" ? "Page text re-read." : "Observed page." };
}
if (type === "scroll") {
const el = findByIndex(action.index);
if (el) {
el.scrollIntoView({ block: "center", behavior: "instant" });
return { status: "ok", message: "Scrolled element into view." };
}
window.scrollBy(0, Math.round(window.innerHeight * 0.85));
return { status: "ok", message: "Scrolled down one viewport." };
}
if (type === "wait") {
const selector = String(action.wait_for || "").trim();
const timeoutMs = Math.max(500, Number(action.timeout_ms || 8000));
if (!selector) {
await new Promise((resolve) => setTimeout(resolve, Math.min(timeoutMs, 3000)));
return { status: "ok", message: "Waited." };
}
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (document.querySelector(selector)) {
return { status: "ok", message: `Element ${selector} appeared.` };
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
return { status: "timeout", message: `Element ${selector} did not appear within ${timeoutMs}ms.` };
}
if (type === "click") {
const el = findByIndex(action.index);
if (!el) return { status: "error", message: `No element with index ${action.index}. Re-observe the page.` };
el.scrollIntoView({ block: "center", behavior: "instant" });
if (typeof el.focus === "function") el.focus();
el.click();
return { status: "ok", message: `Clicked element ${action.index}.` };
}
if (type === "type") {
const el = findByIndex(action.index);
if (!el) return { status: "error", message: `No element with index ${action.index}. Re-observe the page.` };
el.scrollIntoView({ block: "center", behavior: "instant" });
if (typeof el.focus === "function") el.focus();
const value = String(action.text ?? "");
if (el.isContentEditable) {
el.textContent = value;
el.dispatchEvent(new Event("input", { bubbles: true }));
} else {
fireInput(el, value);
}
if (action.submit) {
const enterInit = { bubbles: true, cancelable: true, key: "Enter", code: "Enter", keyCode: 13, which: 13 };
el.dispatchEvent(new KeyboardEvent("keydown", enterInit));
el.dispatchEvent(new KeyboardEvent("keyup", enterInit));
const form = el.form || el.closest("form");
if (form && typeof form.requestSubmit === "function") {
try {
form.requestSubmit();
} catch {
form.submit?.();
}
}
}
return { status: "ok", message: `Typed into element ${action.index}${action.submit ? " and submitted" : ""}.` };
}
return { status: "error", message: `Unsupported action type: ${type}` };
} catch (error) {
return { status: "error", message: error instanceof Error ? error.message : String(error) };
}
}
@@ -0,0 +1,96 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
color: #171717;
background: #fafafa;
font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
h1 {
margin: 0 0 14px;
font-size: 16px;
}
.popup,
.options {
display: grid;
gap: 12px;
padding: 14px;
}
.popup {
width: 340px;
}
.options {
max-width: 720px;
}
label {
display: grid;
gap: 6px;
font-weight: 600;
}
label.checkbox {
grid-template-columns: auto 1fr;
align-items: center;
font-weight: 500;
}
label.checkbox input {
width: auto;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
input,
textarea {
width: 100%;
border: 1px solid #d4d4d4;
border-radius: 6px;
padding: 8px;
background: #fff;
color: inherit;
font: inherit;
font-weight: 400;
}
textarea {
resize: vertical;
}
button {
border: 0;
border-radius: 6px;
padding: 9px 12px;
background: #111827;
color: #fff;
cursor: pointer;
font: inherit;
font-weight: 600;
}
button.secondary {
background: #e5e7eb;
color: #111827;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.status {
min-height: 20px;
margin: 0;
color: #525252;
white-space: pre-wrap;
}
+117
View File
@@ -0,0 +1,117 @@
# Wiki 数据流转与本地编码验收(2026-09-03)
## 结论
本地 BGE-M3 小批量编码已安装、配置并实际运行。已有 Wiki 的向量化、自动沉淀、
普通库文件导出、助手库异步导入、助手库文件导出、空普通库回流及两端向量检索已实测通过。
**技能自动生成 Wiki 尚未完成验收**:WeKnora 的两个生成模型都返回 HTTP 402
`Insufficient Balance`。不能把后半段通过等同于全链路通过。
本次修改运行在本地 DeerFlow 开发服务(前端 5175、后端 8001),连接用户提供的远端
WeKnora;没有把业务代码发布到服务器,没有提交 Git,也没有直接写 WeKnora 图数据库。
## 本地编码配置
- 实现:FastEmbed / ONNX Runtime CPU,官方 `BAAI/bge-m3` 权重。
- 配置名:`bge-embedding-m3`,1024 维,每批 2 条,2 个 CPU 推理线程。
- 运行依赖:FastEmbed 随标准后端依赖安装;模型必须提前放在离线目录中,仍按需加载,不影响主服务启动。
- 权重:约 2.3 GB,持久缓存 `C:\Users\ji\.cache\deerflow\embeddings`,不进入业务仓库。
- 配置:`llmwiki.local_wiki_index.embedding.provider: local`;可用
`local_model_path` 指定离线权重目录,详见后端 README。
- 查询向量在本地实时编码。已有 Wiki 在内容哈希、模型指纹匹配且向量非空时直接复用。
不同模型或维度不能混用;导入不静默重新编码。
- 原始文档分段向量与模型生成后的 Wiki 文本不同,不能相互冒用。本次新建的是 Wiki
正文的 BGE-M3 索引,不是把既有 512 维文档向量改名为 1024 维。
## 实际数据验收
来源是已有普通知识库“图片测试111”,没有改写其 Wiki 内容。
| 阶段 | Wiki | Wiki 向量 | 实体 | 引用关系 |
| --- | ---: | ---: | ---: | ---: |
| 普通知识库导出 | 19 | 101 | 19 | 108 |
| 助手知识库导入后导出 | 19 | 101 | 19 | 108 |
| 新建普通知识库回流后导出 | 19 | 101 | 19 | 108 |
| 知识梳理总库导出(7 个来源记录) | 19 | 101 | 19 | 108 |
逐项断言通过:
1. 三份包的 Wiki slug、标题、正文、摘要、页面类型、别名、分类路径、出链一致。
2. 101 条向量的原始 float32 字节、分段编号、维度和模型指纹完全一致。
3. 将导入前后实体 ID 映射回原 Wiki slug 后,108 条关系的端点及谓词集合完全一致。
4. 总库导出没有因多个来源重复同步而重复导出同一当前页面的分段向量。
5. 元数据回归测试另覆盖 tags、目录恢复、原始文档片段不计入 Wiki 数量、原生技能无
ownership 记录时可被选中、短 Wiki 不漏向量、模型指纹不兼容时不假装向量检索。
普通包约 1.01 MB,助手包约 0.79 MB,回流包约 0.83 MB。普通包还带 2 个原始文档及
108 个原始片段;助手导出提供 Wiki、Wiki 向量和关系,不重新上传原始文档触发二次生成。
### 检索
实际查询:“军方用什么工具评估极端天气带来的风险?”
- 回流普通库 `/api/llmwiki/search` 返回 `mode=vector`,首条“国防气候评估工具”。
- 助手库 `/api/assistant-knowledge/search` 返回 `retrieval_mode=vector`,首条同上;
首个命中分段余弦相似度约 0.6343。
- 浏览器实际提交总库检索,显示“向量检索 · 余弦相似度”和相同命中;不是只检查返回字段。
- 普通库采用已有页面级聚合评分,因此页面最终分数与单分段余弦值不必相等。
- 回流的向量写入 **DeerFlow 管理的本地 Wiki 索引**。未写入 WeKnora 原生向量数据库,
本次结论适用于本系统问答检索,不能声称原生 WeKnora 自身的检索也已完成该向量导入。
### 页面
普通与助手库复用同一 Wiki 阅读组件:目录树、类型/标签筛选、分类面包屑、别名标签、
正文内链、出链、反向链接及邻接关系视图。窄窗口可收起目录。
浏览器验证了“索引 → 国防气候评估工具”、展开关系视图、助手 Wiki 跳回普通库来源,
以及普通 Wiki 的“在知识梳理中查看”反向跳转。
### 验收数据位置
- 原有普通库:`48f669dc-433e-4aab-b5ea-897b6f7a3d2f`
- 知识梳理总库:`c3fd9841-0718-44dc-860e-aae3020fd485`
- 最终助手测试库:`489be905-08c8-40fe-8d60-42ac21dd945a`
- 最终普通回流测试库:`428162aa-03f6-4c61-8994-360f2f39e0c9`
新建测试库名称均以“Wiki链路验收-0903-1826”开头,保留用于复核;未删除用户已有库。
本地 `.runtime/wiki-live-result.json` 保存验收结果,`.runtime/wiki_live_acceptance.py`
保存本次验收流程,均为忽略的运行产物,不包含认证口令。
## 这次实际修复的问题
- 内置技能只有磁盘目录、没有归属记录时,被错误判定为不存在。
- 技能应上传去代码后的知识正文,让 WeKnora 生成 Wiki,而不是伪造一个 `skill` 类型页面。
- Wiki 生成等待使用实际 stats 接口;零页结果明确失败,不无期限等待。
- 向量化完成后自动导出沉淀到总库,并阻断导出回调重复触发全量同步。
- 短摘要和短正文同时存在时,旧分段阈值会将整页过滤为零向量。
- 分类路径必须先创建 WeKnora folder 并绑定 folder_id;只传 category_path 会被服务端清空。
- Wiki 的标签/分类/别名/原始引用保留;图节点绑定真实 Wiki,避免生成重复的空实体页面。
- 保留归一化 float32 原始字节;导入中不能用 `section_index or index`,因为合法编号 0 会被改写。
- 总库多个来源保留审计,但检索与导出只使用当前 Wiki 版本对应的向量,避免旧贡献污染检索。
- 包解析改为同盘临时 SQLite,上传先落盘再异步导入,导出完成后才提供文件下载。
## 阻塞与未通过的范围
1. 技能 `knowledge-base-ingest` 已送入专门创建的普通测试库,但远端
`deepseek-v4-flash` 和 `qwen3.6-flash` 均因余额不足失败。WeKnora 即使失败也可能把
文档 parse_status 标为 completed。需恢复可用生成模型后重新运行技能全链路验收。
2. 已采用磁盘暂存和有界批处理,但本次只有约 1 MB 实测,**未做数 GB 压测、断点续传、
进程中断恢复验收**。大包部署仍需磁盘容量、代理 body size 和超时配置。
3. 当前实体去重主要按类型及规范化名称;这次没有完成跨别名、歧义实体的模型对齐及
多来源事实冲突合并验收,不能视为这一历史需求已全面完成。
4. 重复上传仍会保留独立来源记录。向量导出已消除旧版本重复,但上传身份幂等、部分远端
写入失败后的无损重试、跨实例图片资源打包仍需独立验证。
5. 当前版本已补充自动增量链路:服务启动立即补扫,之后默认每 30 秒扫描全部普通
知识库;Wiki 抽取完成后无需手工点击向量化,并自动以来源快照覆盖同步到知识梳理总库。
6. 回流当前只允许新建空 Wiki 普通库,避免覆盖已有页面。普通库现有页面列表的超大规模
分页与助手问答的最终生成回答不属于这轮通过结论。
## 自动化检查
- 后端相关测试:118 passed(assistant repository、Wiki sync、package roundtrip、
WeKnora 接口、向量检索与知识库管理)。
- 修改的 Python 功能文件 Ruff 检查通过;app.py 既有启动顺序导致的 E402 不在此次改动中处理。
- `git diff --check` 通过(只有仓库换行提示)。
- 前端完整 `pnpm typecheck` 未通过:57 个既有/本轮范围外错误,涉及 nextra 缺失、Markdown
插件类型及其他模块;本轮新建/修改的 Wiki 组件、助手 API、技能面板没有出现在错误清单中。
浏览器实际页面与跳转已验证,但不能声称全项目构建通过。
@@ -0,0 +1,188 @@
# 报告协作工作台 — 交互重设计方案(v2)
> 日期:2026-09-07
> 状态:**待确认**(确认后按 §7 分期实施)
> 取代:现行「规则澄清 + 三张模板方案卡 + 画布为主」的交互
> 原则:回到最初设计 —— **协调者智能体全程与用户对话的主线**
---
## 1. 设计目标与总体形态
用户视角只有一条主线:**和一个规划师智能体对话 → 它摸清意图并规划工作流 → 确认后执行、过程在流程图上可见 → 随时插话干预、指定智能体重跑 → 最终成稿、继续对话式修改**。
```
┌─────────────────────────────────────────────────────────┐
│ 阶段1 计划模式 阶段2/3 执行+干预 阶段4 成稿 │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 规划师对话 │ 确认 │ 聊天时间线 │ 审稿过 │ 对话改稿 │ │
│ │ 需求摘要卡 │ ─────→ │ +成员Tab │ ─────→ │ 版本/diff│ │
│ │ 工作流卡 │ 冻结 │ +流程图动画│ 报告卡 │ 上游重跑 │ │
│ └───────────┘ └───────────┘ └──────────┘ │
│ ↑ 回到计划模式(replan)随时可触发 ↑ │
└─────────────────────────────────────────────────────────┘
```
与现行的核心差异:
| 维度 | 现行(做偏的) | v2 |
|------|----------------|-----|
| 需求澄清 | 规则模板问句秒回 | 规划师智能体(LLM)领域化提问 |
| 工作流来源 | 3 张确定性模板 | LLM 按意图生成自定义 DAG,对话迭代修改 |
| 执行展示 | 画布为主、聊天为辅 | **聊天时间线为主**(成员 Tab 切换保留),**执行状态在流程图上动画呈现** |
| 干预 | 命令确认卡体系 | 输入框常开 → 协调者智能体分类;支持**指定智能体重跑** |
| 报告修改 | 改写候选(纯文字层) | 意图分流:润色走改写;**事实性问题触发上游节点重跑** |
---
## 2. 阶段 1:计划模式 — 需求梳理与工作流规划
### 2.1 计划模式状态
- 用户发出第一条需求消息起进入**计划模式**,Header 显示徽章「📋 计划模式」,输入框提示语切换为「梳理需求中,你可以随时补充或纠正…」。
- 计划模式在**工作流确认冻结**时结束;执行期大改可通过 replan 回到计划模式(见 §5.4)。
### 2.2 规划师对话(LLM,替换现行规则路由)
- 必须调用独立内置智能体 `report-collaboration-intent` 的真实 LangGraph thread,并原样消费 `messages-tuple` SSE;禁止用 `ainvoke()` 等待完整 JSON 后再包装成一话。
- 流式正文、思考内容和工具步骤直接进入工作台现有 `MessageList`,与普通智能体问答采用同一套步骤条 / Reasoning 展示。
- 模型:`report_collaboration.planner_model`(回退 `coordinator_model` → 默认模型,复用现有 model_resolver 冻结机制)。
- 提问是领域化的:写市场分析就问「关注整车还是供应链 / 要不要竞品对比 / 给谁看 / 篇幅」,而非固定模板问句。
- 澄清轮次上限沿用 `max_clarification_rounds`;用户随时可以说「直接开始」按默认假设推进。
### 2.3 需求摘要卡
信息足够后,规划师在对话中输出一张**结构化需求摘要卡**(业务卡插槽渲染):主题 / 目的 / 读者 / 角度清单 / 篇幅与重点 / 默认假设。用户逐项看,可在对话里继续改(「读者改成投资人」→ 卡片更新出新版)。
### 2.4 工作流生成与迭代
- 规划师判断意图信息足够后,立即按已确认意图自动生成**一份**工作流方案(不是三选一),不再要求用户额外点击“确认意图/生成方案”;方案以工作流卡(成员构成、各节点使命、预计时长/成本)+ 右侧流程图预览呈现。
- 生成约束(保证可执行):**节点类型受词表约束** — `research / verify / analyze / write / review`;自由的是结构(几条线、并行还是串行、核验深度)与每个节点的使命/角度/工具。
- 对话式修改:「把价格策略单独拆一条线」「加一个政策风险分析」→ 规划师修订后出新版工作流卡(带变更说明),再确认。
- 「开始执行」→ 冻结需求 + 工作流(含模型/预算快照,沿用现有 freeze 机制)。
---
## 3. 阶段 2:执行 — 过程在流程图上可见
### 3.1 流程图动画规格(执行状态的可视化主载体)
节点状态 → 样式(`ReportFlowCanvas` / `ReportFlowNode`,状态源 `selectNodeStatusMap`,事件源 `node.status.changed` SSE):
| 状态 | 节点样式 | 连入边 |
|------|----------|--------|
| planned / queued | 灰色常态 | 静态 |
| **running** | 主色描边 + **呼吸脉冲动画**(ring)+ 角标 spinner | **流动动画**(@xyflow 内置 `animated: true` 的 dash 流动) |
| **completed** | 绿色 + ✓ 角标,常驻 | 恢复静态实线 |
| failed | 红色 + 错误角标 | 红色虚线 |
| awaiting_input | 橙色 + ?角标 | 橙色静态 |
| interrupted / superseded | 半透明降级 | 灰 |
- 语义:**边流动 = 数据正在流入该节点**(该节点执行中);节点完成即停动画换完成样式。
- 实现成本:@xyflow 原生支持 animated 边与自定义节点 className,纯 CSS 补呼吸动画即可,无需新依赖。
### 3.2 聊天区(保留现有 Tab 切换)
- 并行成员沿用 **AgentStageSelector Tab 切换**(维持现状,不改交错时间线)。
- 每个 Tab 内是该成员的完整流式输出:思考、工具调用卡、检索过程、正文 —— 与普通智能体问答同观感(message-adapter 已具备映射能力)。
- 协调者播报(「研究线 A 完成,核心发现 X,开始交叉核验」)进入总时间线 Tab。
---
## 4. 阶段 3:干预 — 对话即干预 + 指定重跑
### 4.1 输入框常开,协调者智能体分流
执行期输入框永远可用,所有输入先到协调者(LLM 分类,规则高精度直判兜底——复用 RC-BE-014 IntentRouter 骨架,把「规则优先」反转为「LLM 优先 + 规则快路径」):
| 用户意图 | 协调者行为 | 生效时机 |
|----------|-----------|----------|
| 提问(「查到什么了?」) | 直接回答 | 立即,不打断 |
| 修正方向(「重点看下沉市场」) | 小改:在安全点注入 overlay,并回复说明 | 下个安全点 |
| **指定重跑**(见 §4.2) | 出确认卡 → 重开节点 attempt | 确认后 |
| 大改(砍线/加线) | 提议 replan → 回计划模式 | 用户确认 |
| 停止 | 直接取消 | 立即 |
### 4.2 指定智能体重跑(新增核心交互)
**场景**:用户发现某节点传给下游的信息不对(如研究线 B 的结论有误)。
两个入口,同一后端动作:
1. **对话**:「研究线 B 的结果不对,重新查,重点看价格战」→ 协调者解析出 `node_id` + 新指令;
2. **画布**:点节点 → 抽屉(NodeInspectorDrawer)加「↻ 重新执行」按钮。
**级联规则**(复用并补全 ledger 的 targeted rework):
```
重跑节点 N(attempt+1,带用户新指令作为 mission overlay)
→ N 完成 → 所有直接/间接依赖 N 产物的下游节点标记 superseded
→ 按 DAG 依赖序逐波重跑(边动画重新流动,用户全程可见)
→ 受影响章节重新生成
```
- 确认卡必须展示**影响面**:「将级联重跑 核验B / 分析 / 写作 共 3 个节点,预计 +8 分钟」。
- 成本保护:级联重跑计入 run 预算(现有 budget 机制),预算不足时明确报因。
---
## 5. 阶段 4:报告成稿与对话式修改
### 5.1 成稿
审稿通过 → 报告卡出现在时间线(点开 ReportPreviewDialog 全文),协调者播报总结(各线核心发现 → 结论如何形成)。
### 5.2 修改意图分流(协调者 LLM 判定)
| 用户说 | 判定 | 动作 |
|--------|------|------|
| 「把第二章结论写得更笃定」 | 文字润色 | 改写候选(现有 rewrite API)→ 对比预览 → apply → 新版本 |
| 「这个数据不对 / 重新收集一下」 | **事实性问题** | 定位来源节点 → **触发该节点重跑(可带新角度)→ 级联 §4.2 → 受影响章节重生成 → 新版本** |
| 「再从供应链角度补一节」 | 结构性新增 | replan:回计划模式改工作流(只增不删已完成部分) |
| 「恢复到上一版」 | 版本回退 | 现有 restore API |
- 版本链:每次 apply / restore / 级联重跑产出均产生新 head,历史完整保留(现有机制)。
- 修改对话里协调者必须先**复述理解**(「你是想让研究线 B 换个角度重查,然后重写第三章,对吗?」)再执行,避免误触发昂贵的级联。
---
## 6. 工程改造清单
### 6.1 保留(本次联调已验证)
- 持久化 13 张表 + durable SSE + `after_seq` 断线续传;幂等键 / 乐观锁 / 预算框架。
- MessageList 复用链路(message-adapter → LangGraph Message → 业务卡插槽)。
- AgentStageSelector Tab、commands 安全点骨架(RC-BE-014)、报告版本三件套(RC-BE-015)、前端 hooks/reducer/selectors 主体。
- 「清空历史」「生成候选方案」等本次新增入口。
### 6.2 后端重做 / 新建
| 项 | 说明 |
|----|------|
| 规划师智能体 | LLM 多轮澄清(替换 `requirement/intent_router.py` 规则路由)+ 需求摘要结构化 + **工作流 LLM 生成**(受 §2.4 词表约束,替换 `planning/controller.py` 三模板)+ 对话式工作流修订 |
| 协调者智能体升级 | 干预分类 LLM 优先;修改意图分流(§5.2);执行前复述确认 |
| 指定重跑 + 级联 | ledger targeted rework 补全级联 superseded/重跑波次;确认卡带影响面计算 |
| AgentScope 适配器修复 | `count_tokens` 协议缺失(上下文压缩必崩);attempt 耗尽终态化(防僵尸 run) |
| 事件补齐 | 边流动所需的节点状态事件已有;补「级联影响预告」事件 |
### 6.3 前端重做 / 新建
| 项 | 说明 |
|----|------|
| 计划模式 UI | Header 徽章、composer 提示切换、需求摘要卡、工作流卡(替换现三张模板卡) |
| 流程图动画 | running 节点呼吸动画 + 入边 `animated`、completed ✓ 样式、级联重跑时边动画重流(§3.1) |
| 节点抽屉 | 加「重新执行」按钮 → 确认卡 |
| 干预交互 | 执行期输入框常开;意图确认卡(含影响面);修改对话复述确认 |
| 布局 | 聊天为主、画布为辅的默认比例微调(不动整体两栏结构) |
---
## 7. 实施分期
| 期 | 内容 | 验收 |
|----|------|------|
| **P0 地基** | AgentScope `count_tokens` 修复;attempt 耗尽终态化;级联重跑的 ledger 补全 | 执行阶段不崩、无僵尸 run、单节点重跑级联正确(后端测试) |
| **P1 计划模式 + 执行可视化** | 规划师 LLM 对话、需求摘要卡、工作流 LLM 生成与迭代、计划模式 UI、流程图动画 | 从新会话到确认工作流全程对话完成;执行时动画正确随状态流转 |
| **P2 干预 + 成稿修改** | 协调者 LLM 分类、指定重跑(两入口 + 影响面确认卡)、报告修改意图分流、上游级联重跑 | 四类干预各自走通;「数据不对→重跑→新章节→新版本」全链路 |
每期结束跑 RC 全量测试(当前基线 233 通过)+ 前端 26 单测,不引入对其他模块的影响。
@@ -0,0 +1,719 @@
# AgentScope 2.0 多智能体报告协作工作台——前端实施计划
> 文档状态:前端代码已落地、契约冻结;联调等待后端运行中干预与报告改写
> 编写日期:2026-09-02;进度更新日期:2026-09-06
> 目标项目:`F:\react01\deeflow-code\deerflow-server\frontend-web`
> 视觉与交互参考:`F:\react01\coze-frontend\frontend\apps\coze-studio`
> 参考页面:`http://localhost:3000/work_flow?space_id=1&workflow_id=wf_88cb9f4c6642`
> 配套后端计划:`docs/agentscope-多智能体报告协作工作台-后端实施计划.md`
> 本文只规划新增页面,不修改现有多智能体会商页面,不复用旧 Workflow Studio 执行状态。
## 0. 当前进度
`frontend-web/src/report-collaboration/` 已实现两栏工作台、会话/方案/run API 客户端、mock driver、SSE reducer、`MessageList` 适配、方案卡、画布、节点抽屉、Composer、报告预览与历史。`RC-FE-001`~`013` 的页面骨架与契约层视为完成。
后端已通规划前路径(澄清 → 出方案 → 选定 → `queued` run),见后端计划 §0。前端真实执行体验仍受阻:
- `GET /runs/{id}/stream` 已挂载(`RC-BE-011`);dispatcher/租约已落地(`RC-BE-012`),默认 `worker_enabled=false` 仍不自动领取 queued run。
- 报告改写 / 恢复接口已由后端 `RC-BE-015` 挂载。
- 质量角色已由 `QualityRoleKernel` 跑通(`RC-BE-013`);真实 AgentScope 模型/`web_search` 仍待后续票。
联调顺序:先打开 `report_collaboration.enabled` 验证规划前 REST 与 `GET /runs/{id}/stream` 重放;画布自动执行需同时打开 `worker_enabled`(默认关)。
## 1. 前端目标
新增独立的「报告协作」工作台。用户可以通过对话提出任意报告需求,查看系统生成的多个协作方案,点击方案卡片在流程图中预览协作结构,确认后观察多智能体执行,并在全过程通过统一对话框提出修改意见。
前端必须满足:
1. 页面采用“左侧对话、右侧流程画布”的固定两栏布局,画布、工具栏、弹窗和抽屉参考 Coze Studio 页面风格。
2. 消息列表必须复用 DeerFlow 现有 `MessageList`,不复制 Coze 的会话消息实现。
3. 所有执行过程、AgentScope 团队消息、智能体回复、工具调用及其原始返回、错误、协助卡、返工和最终报告入口都通过现有消息协议按时间顺序显示在左侧对话区,不设置独立执行结果面板,也不另做“阶段摘要”替代实时消息。
4. 候选方案、流程节点、对话进度和报告使用同一份后端事实,不各自推断状态。
5. 用户可在规划中、运行中、等待输入和报告完成后继续对话。
6. 用户协助卡的出现和提交都不能在前端被解释为“节点已经完成”。
7. 页面刷新、SSE 断线重连、历史会话切换后能恢复到一致状态。
暂定:
- 页面名称:`报告协作`。
- 路由:`/page/workspace/report-collaboration`。
- 会话路由:`/page/workspace/report-collaboration/:sessionId`。
- 默认候选方案数量:3。
- 点击画布节点时打开覆盖式节点说明抽屉;抽屉只展示节点职责和合同,不展示节点执行结果。
---
## 2. 范围与非目标
### 2.1 本期范围
- 新建、查看、重命名、删除协作报告会话。
- 使用对话澄清报告需求。
- 显示 2~3 个候选方案卡片。
- 卡片与右侧流程图预览联动。
- 节点说明抽屉:职责、所依据的信息类型、预期产出、工具、资料要求和验收条件。
- 实时展示节点排队、运行、校验、等待用户、完成、返工、失败和取消。
- 在左侧对话时间线中直接展示每个智能体的流式文本、团队对话、工具调用和工具返回,并展示审稿意见、报告文件和报告版本。
- 并行阶段用横向的智能体选择器切换当前查看对象;选择器只控制同一份消息流的聚焦范围,不创建另一套执行结果视图。
- 运行期间发送补充要求、重新检索、重新分析、节点重跑等指令。
- 完成后进行报告问答、章节重写、全文润色和版本恢复。
- 报告模板选择、Markdown 预览、Markdown/Word 下载。
- 响应式布局、键盘操作、加载反馈和错误恢复。
### 2.2 明确不做
- 不引入 `@coze-workflow/playground` 或 `@coze-arch/coze-design`。
- 不加载 Coze workflow definition、run 或 node run。
- 不允许前端拖拽连线后直接改变执行逻辑。
- 不新增第二套消息气泡、Markdown、引用和文件卡渲染器。
- 不展示模型原始 chain-of-thought、系统/内部 prompt、密钥和被标记为内部控制信息的字段。
- 不把用户有权查看的 AgentScope TeamSay、智能体文本、工具调用或工具返回压缩成“正在检索”等摘要卡;这些原始业务消息必须进入复用的 `MessageList`。敏感字段只能按统一脱敏策略替换,不能以摘要替代。
- 不增加第三栏、常驻详情栏、节点执行结果栏或独立执行时间线。
- 节点抽屉不展示节点生成正文、工具返回、证据列表、错误堆栈或 attempt 历史;这些过程统一在左侧 `MessageList` 的原生消息/工具步骤中查看。
- 不改造现有圆桌、多智能体会商、深度研究和 AI 写作页面。
- 不把浏览器状态作为业务事实源。
---
## 3. 参考页面行为映射
| 目标行为 | Coze 参考文件 | DeerFlow 目标组件 | 说明 |
|---|---|---|---|
| 两栏骨架 | `workflow-studio-layout.tsx` 的面板比例与分隔条 | `ReportCollaborationLayout.tsx` | 左对话、右画布;只保留一条可调整分隔线 |
| 顶栏 | `workflow-studio-header.tsx` | `ReportCollaborationHeader.tsx` | 替换为会话、模板、报告、停止等操作 |
| 候选卡片 | `workflow-proposal-cards.tsx` | `ReportPlanCards.tsx` | 点击预览,明确确认后才执行 |
| 右侧画布 | `workflow-canvas.tsx` | `ReportFlowCanvas.tsx` | 使用 `@xyflow/react` 重建,不引入 Coze Playground |
| 左侧消息区 | `run-conversation-panel.tsx` | `ReportConversationPanel.tsx` | 外层风格参考 Coze,内部复用 DeerFlow `MessageList` |
| 节点说明抽屉 | Coze 节点配置面板的视觉与抽屉行为 | `NodeInspectorDrawer.tsx` | 点击画布节点打开,只展示职责与输入输出合同 |
| 全程执行反馈 | 参考其进度和状态表达 | `AgentStageSelector` + DeerFlow `MessageList` | 选择智能体后直接显示其流式消息、TeamSay、工具调用与返回;不再合成为阶段摘要 |
| 状态栏 | `connection-status.tsx` | `ConnectionStatusBar.tsx` | 展示连接、重连和事件游标 |
| 对话历史 | `left-panel-tabs.tsx` | `ConversationSidebarTabs.tsx` | 对话与历史,不增加“节点资源”Tab |
| 异步反馈 | 规划进度、运行状态 | MessageList 步骤条 + 画布状态 | 操作超过 300ms 必须有反馈 |
主要参考源:
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-layout.tsx`
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-composition.tsx`
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-header.tsx`
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\run-conversation\workflow-proposal-cards.tsx`
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\styles.module.less`
若直接复制 Apache-2.0 源码片段或样式,必须保留版权头并维护来源说明;默认做行为映射和视觉重建。
---
## 4. 页面结构
```text
┌──────────────────────────────────────────────────────────────────────────┐
│ 返回 报告协作 / 会话标题 保存状态 模板 报告 停止/继续 更多 │
├────────────────────────────────┬─────────────────────────────────────────┤
│ 对话 / 历史 │ 协作流程图 │
│ │ │
│ DeerFlow MessageList │ @xyflow/react │
│ · 并行时:[协调者][市场][政策] │ · 节点状态与活动连线 │
│ · 用户与协调者/成员消息 │ · 缩放、适配视图、小地图 │
│ · 原生 ToolCall / ToolMessage │ · 点击节点打开覆盖式说明抽屉 │
│ · 协助卡、报告文件和版本 │ │
│ │ │
│ 统一输入框 │ │
├────────────────────────────────┴─────────────────────────────────────────┤
│ 已连接 · run_xxx · 事件 128 │
└──────────────────────────────────────────────────────────────────────────┘
```
节点抽屉覆盖在右侧画布之上,不占用第三栏:
```text
┌─────────────────────────────────────────┐
│ 节点名称 关闭 × │
│ 角色 / 当前状态 │
├─────────────────────────────────────────┤
│ 节点职责 │
│ 要根据什么:所需输入、上游类型、资料要求 │
│ 要产出什么:成果类型、格式、验收条件 │
│ 可用工具与边界 │
│ 上下游关系 │
├─────────────────────────────────────────┤
│ 在对话中对此节点提出意见 │
└─────────────────────────────────────────┘
```
抽屉明确不展示实际执行结果、生成正文、工具返回、来源明细、错误堆栈或 attempt 时间线;这些信息只在左侧原生消息流中展示。
布局默认值:
- 顶栏高度:56px。
- 左侧面板:默认 480px,最小 360px,最大 720px。
- 右侧画布:最小 480px,使用剩余空间。
- 节点说明抽屉:覆盖画布右侧,建议宽度 420px;关闭后完整归还画布空间。
- 状态栏:24~28px。
- 低于 960px:左侧对话和右侧画布改为顶部按钮切换的单面板模式;节点说明仍使用覆盖式 Drawer。
- 两栏分隔条:支持 pointer 拖动、方向键、Home、End。
### 4.1 原生消息流与并行智能体切换
“复用 `MessageList`”在本页面的含义是复用它现有的 assistant 文本、`tool_calls`、`tool` 消息、引用、文件和协助卡渲染语义;不把 AgentScope 的运行过程先解析成专用步骤条、进度摘要或阶段结果卡。
单个智能体:
- `agent.reply.started` 立即创建一条稳定的 assistant 消息;`message.delta` 持续更新同一 `messageId`,所以用户会先看到流式状态、再看到逐字回复,而不是等待节点结束。
- 工具调用创建/更新该 assistant 消息的 `tool_calls`;工具返回创建匹配 `tool_call_id` 的原生 `tool` 消息。现有 `MessageGroup` 负责把两者配对显示,不再另写工具过程渲染器。
- AgentScope TeamSay 作为带 `agentRunId` 的普通 assistant 消息进入同一流,按到达顺序显示。它不是节点完成的证据,节点状态仍由右侧画布和后端状态机决定。
并行智能体:
```text
多角度并行研究 3/4 进行中
[ 协调者 ] [ 市场规模 ✓ ] [ 品牌竞争 ● ] [ 海外市场 ● ] [ 政策风险 ! ]
└──────────────────── 当前选中智能体的原生 MessageList ────────────────────┘
```
- `AgentStageSelector` 只在并行阶段出现,横向放置在左侧对话区顶部;使用现有图标体系的头像/角色图标、名称、状态文字和非纯颜色状态标识。默认选中第一位已开始运行的成员;用户手动切换后保持选择,不因其他成员返回而抢走阅读焦点。
- 所有成员的事件都持续接收、按每个 `agentRunId` 持久化和更新,即使当前未选中;切回即可看到完整历史与正在更新的同一条消息,不丢 token、不重新请求、不把结果拼成摘要。
- 当前选择只将共享用户消息、协调者广播消息和目标成员消息组成一个 `BaseStream<AgentThreadState>` 交给同一个 `MessageList`。切换标签仅切换该视图的消息范围,不改变 run、节点或消息的后端事实。
- 某成员请求用户协助时,其标签显示“待回复”徽标,选择器显示待处理数;点击徽标切到该成员,原生协助工具消息仍由现有 `MessageList` 协助卡渲染。其他成员可继续运行。
- 节点抽屉打开某并行成员时,同步聚焦对应标签;抽屉仍只展示合同。用户从抽屉或输入框发送反馈时带 `targetNodeId`/`agentRunId`,后端在安全点生效。
这不是把完整日志另做成调试面板:对话区展示的是模型和工具的真实业务消息;只有 chain-of-thought、内部提示词及按权限必须脱敏的字段不进入该消息流。
### 4.2 页面主要状态
| 状态 | 左侧对话区 | 右侧流程画布 | 主要操作 |
|---|---|---|---|
| `empty` | 欢迎语和输入框 | 空态 | 输入需求、选模板 |
| `clarifying` | 澄清问题和协助卡 | 需求理解节点 | 回答或补充 |
| `planning` | 用户消息和规划进度 | 骨架或需求图 | 继续补充,等待安全点生效 |
| `proposal_ready` | 2~3 张方案卡 | 点击卡片预览对应图 | 切换、修改、采用 |
| `running` | 协调者与成员原生消息、工具过程和用户反馈 | 实时节点状态 | 干预、停止、在对话中要求重跑 |
| `awaiting_input` | 等待用户卡片、原因和影响 | 对应节点显示等待 | 回答、取消 |
| `reviewing` | 审稿意见和返工过程 | 评审/返工节点状态 | 接受或补充要求 |
| `completed` | 总结、报告文件卡、版本和后续对话 | 完整执行图 | 问答、改写、导出 |
| `failed` | 错误说明和恢复建议 | 失败节点 | 通过对话重试或取消 |
| `cancelled` | 已保留成果说明 | 未完成节点取消 | 从已有成果继续或新建方案 |
---
## 5. 前端目录与组件边界
建议新增:
```text
frontend-web/src/report-collaboration/
api/
client.ts
sessions.ts
plans.ts
runs.ts
reports.ts
types.ts
components/
ReportCollaborationLayout.tsx
ReportCollaborationHeader.tsx
ConversationSidebarTabs.tsx
ReportConversationPanel.tsx
AgentStageSelector.tsx
ReportComposer.tsx
ReportPlanCards.tsx
ReportFlowCanvas.tsx
ReportFlowNode.tsx
NodeInspectorDrawer.tsx
ReportPreviewDialog.tsx
InterventionCard.tsx
SessionHistoryPanel.tsx
ConnectionStatusBar.tsx
hooks/
useReportCollaborationSession.ts
useReportCollaborationStream.ts
useReportCollaborationMessages.ts
useReportPlanSelection.ts
useReportPanelLayout.ts
state/
event-normalizer.ts
event-reducer.ts
selectors.ts
message-adapter.ts
pages/
ReportCollaborationPage.tsx
styles/
report-collaboration.css
tests/
```
修改范围:
- `frontend-web/src/pages/WorkspaceRoutes.tsx`:增加页面和会话路由。
- `frontend-web/src/components/workspace/workspace-nav-chat-list.tsx`:增加导航入口。
- `frontend-web/src/components/workspace/messages/message-list.tsx`:仅允许增加一个向后兼容的通用业务卡片插槽。
- `frontend-web/src/core/messages/utils.ts`:如确需识别通用业务卡类型,只做集中式扩展。
不得把新功能放进:
- `frontend-web/src/roundtable-planning/`
- `frontend-web/src/open-canvas/`
- Coze 项目的 `workflow-standalone/`
---
## 6. 数据来源与前端状态
### 6.1 三层状态
前端状态分为:
1. 服务器快照:session、原生 messages、plans、run、nodes、artifacts、report versions;每条消息带稳定的 `agentRunId`、`nodeRunId`、`phaseId` 和可见性标记,其中 artifacts 只用于生成左侧消息卡和报告文件,不形成独立结果面板。
2. durable SSE reducer:从 `seq` 连续应用事件,更新服务器快照投影。
3. 纯展示状态:预览中的方案、选中的节点、聚焦的并行 `agentRunId`、两栏宽度、节点抽屉开关、窄屏当前视图和画布位置。
纯展示状态不能改变执行结果。点击卡片只改变 `previewPlanId`;只有点击“采用方案”才改变服务器 `selectedPlanId`。
### 6.2 前端事件包
```ts
interface CollaborationEvent<T = unknown> {
eventId: string;
sessionId: string;
runId?: string;
seq: number;
type: string;
timestamp: string;
data: T;
}
```
必须处理:
- 快照先到、事件后到。
- 事件重复。
- 事件短暂乱序。
- SSE 断线后从 `after_seq` 重放。
- 页面切换时旧连接晚到事件。
- token delta 多次更新同一消息。
- TeamSay、assistant 文本、tool-call 参数 delta、tool 返回 delta 分别更新各自稳定消息,不得等节点完成后批量生成摘要。
- 并行成员的消息可交错到达;Reducer 必须按 `agentRunId + messageId` 更新,而不是按当前被选中的标签丢弃未聚焦成员的事件。
- 节点重新打开后出现新 attempt。
Reducer 以 `sessionId + runId + seq` 去重;消息行以 `messageId`、工具结果以 `toolCallId`、标签以 `agentRunId` 使用后端稳定 ID,禁止使用数组下标。
---
## 7. 分步实施计划
### FE-0:冻结参考视觉与行为基线
任务:
1. 在 1440×900、1280×800、1024×768 打开参考页面。
2. 保存初始态、方案态、运行态和节点说明抽屉四组截图。
3. 记录顶栏、两栏比例、间距、圆角、边框、阴影、画布工具栏和状态色。
4. 形成“参考组件—目标组件—允许差异”表。
5. 明确消息列表保持 DeerFlow 原样,不纳入像素对齐。
交付物:
- 视觉基线截图。
- 视觉 token 表。
- 页面行为清单。
验收:参考的每个用户可见状态都有截图或明确说明不存在。
### FE-1:路由、菜单和静态页面骨架
任务:
1. 增加两个路由和菜单项。
2. 创建 `ReportCollaborationPage`。
3. 创建顶栏、左侧对话栏、右侧画布和底部连接栏。
4. 实现可调整宽度的两栏布局,以及“专注对话/专注画布”切换。
5. 实现窄屏单面板切换与覆盖式节点 Drawer。
验收:
- 不连接后端也能呈现完整空态。
- 375~1440px 不出现页面级横向滚动。
- 对话/画布切换不会卸载会话状态或清空输入内容。
- 键盘可以调整两栏分隔条。
### FE-2:页面设计系统与 Coze 风格重建
建立页面专用语义变量:
```text
--rc-bg-page
--rc-bg-panel
--rc-bg-canvas
--rc-border
--rc-text-primary
--rc-text-secondary
--rc-accent
--rc-running
--rc-waiting
--rc-success
--rc-danger
--rc-panel-shadow
--rc-card-radius
```
规则:
- 从 DeerFlow 主题 token 派生,不在组件中散落十六进制颜色。
- 使用 Lucide SVG 图标,不使用 emoji。
- hover/focus/展开动画控制在 150~300ms。
- 支持 `prefers-reduced-motion`。
- 交互状态不能只靠颜色表达。
- 正文对比度达到 4.5:1。
- 操作超过 300ms 显示 spinner、骨架或流式状态。
验收:除 MessageList 内部外,两栏布局、密度、卡片、画布工具栏、弹窗和节点抽屉达到参考基线。
### FE-3:API 类型和 TanStack Query 层
实现:
- session CRUD。
- messages 查询与发送。
- plan request、plans 查询、select。
- run 创建、查询、停止、重试。
- command 创建、确认、取消。
- artifacts、sources、reports、versions 查询。
每个写请求携带:
- `idempotencyKey`
- `expectedRevision`
冲突行为:
- `409`:刷新当前快照并提示用户重新确认。
- `422`:在对应操作附近显示具体错误。
- `429`:显示预算/并发限制,不自动重试。
- `5xx`:保留用户输入,允许使用同一幂等键重试。
验收:API 层不包含页面展示逻辑,类型与后端 OpenAPI 一致。
### FE-4:SSE、快照和事件 Reducer
实现 `useReportCollaborationStream`:
1. 先拉 session snapshot。
2. 使用 snapshot 的 `lastSeq` 建立 SSE。
3. 校验每个事件 session/run 身份。
4. 按 seq 应用事件并去重。
5. 出现序号缺口时暂停 live apply,补拉缺失事件。
6. 断线指数退避并支持 `Last-Event-ID`。
7. 终态后完成最后一次事件补拉再关闭连接。
Reducer 必须覆盖:
- 消息流增量。
- 计划候选和 revision。
- node/node attempt 状态。
- artifact 创建和校验。
- command 分类和执行。
- report delta 和正式版本。
- 错误、取消、接管恢复。
- AgentScope 原始可见消息和工具事件:`message.created/delta/completed`、`tool_call.created/delta/completed`、`tool_result.created/delta/completed`、`team_message.created/delta/completed`。
验收:重复事件、乱序事件和重连都不产生重复消息、重复节点或状态倒退。
### FE-5:复用 DeerFlow `MessageList`
复用:
- `frontend-web/src/components/workspace/messages/message-list.tsx`
- `frontend-web/src/components/workspace/messages/message-list-item.tsx`
- `frontend-web/src/components/workspace/messages/message-group.tsx`
- `frontend-web/src/components/workspace/messages/context.ts`
实现无损协议适配 `message-adapter.ts`:
1. 将已持久化的 `CollaborationMessage` 一对一映射为 LangGraph SDK `Message[]`,不将文本或工具结果二次概括。
2. 构造最小 `BaseStream<AgentThreadState>` 外观:`messages`、`isLoading`、`isThreadLoading`、`values`。
3. 协调者、成员 AgentScope TeamSay 和成员回复都映射为普通 assistant 消息;保留 `messageId`、文本 chunk、角色/成员名称、`agentRunId`、`nodeRunId`、`phaseId`、来源事件 ID 和完成态。
4. AgentScope 工具调用映射为该 assistant 消息的标准 `tool_calls`;调用结果映射为标准 `tool` 消息,并严格复用原有 `tool_call_id` 配对。工具参数、返回正文及增量保持原文,只有后端明确标注的脱敏字段被替换。
5. 将来源、文件、报告 Markdown delta 和正式版本继续映射为既有引用、文件卡和普通 assistant 消息;规划卡、用户确认卡和报告替换卡仍通过一个通用业务卡片插槽渲染。
6. 错误、恢复建议、命令影响范围和报告版本变化也必须进入同一时间线;错误堆栈以可读错误消息替代,但不能吞掉已发生的工具/成员消息。
7. `selectMessagesForAgentRun(allMessages, focusedAgentRunId)` 只在客户端从已同步消息中选出共享消息与目标成员消息;并行选择器不得重新拉取、重新排序或改写消息。
约束:
- 不使用 LangGraph `useThreadStream` 发送消息。
- 新页面输入框调用报告协作 API。
- 不增加多个页面专用 MessageList props。
- 普通聊天、AI 写作、深度研究等未传插槽时必须零变化。
- 不另建执行结果列表、节点结果面板或成果时间线。
- 不用“运行中”“已检索 N 条”等专用摘要替代真实消息。`MessageList` 本身的流式状态和原生工具步骤是唯一过程视图。
验收:
- 历史和流式消息使用同一行稳定更新。
- 单成员首次开始回复、每次工具启动和工具返回均在 300ms 内出现可见的原生消息行或原生工具步骤;不允许持续数秒只有静态加载圈。
- 并行标签切换后显示目标成员的完整已持久化消息和实时 delta;未选中成员持续接收,标签上的运行/待回复状态及时更新。
- 用户上翻阅读时不强制滚底。
- 用户发送新消息后滚到最新位置。
- 卡片显示成功不改变后端节点状态。
### FE-6:需求澄清和候选方案卡片
`ReportPlanCards` 展示:
- 推荐标签、策略、标题、摘要和推荐理由。
- 分析角度和参与角色。
- 关键步骤与质量门。
- 预计时间、成本等级和资料范围。
- 方案验证错误。
交互:
- 点击卡片或“查看流程”只切换预览。
- “采用此方案”调用 select API。
- “开始协作”再次校验并创建 run。
- 方案调整后显示新 revision;旧 revision 只读。
- 过期 revision 返回 409 后不静默覆盖。
验收:连续快速切换三张卡时,画布最终显示最后一次选择,不闪回旧图。
### FE-7:协作流程画布
技术:`@xyflow/react`。可以借鉴但不直接耦合:
- `frontend-web/src/components/ai-elements/canvas.tsx`
- `frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx`
节点显示:
- 节点名、角色名和分析角度。
- 状态文字、图标和颜色。
- 等待用户、返工、失败和 superseded 标记。
画布能力:
- 自动布局。
- 拖动节点只保存浏览器展示位置。
- 缩放、适配视图、方向切换、小地图。
- 活动边低强度流动动画。
- 点击节点打开覆盖在画布上的节点说明抽屉。
- 节点重跑、补充或纠偏一律通过左侧对话完成;抽屉只提供“在对话中对此节点提出意见”入口。
- 方案切换后清理不属于新方案的本地坐标。
验收:
- 30 个节点内流畅操作。
- 单个 node event 不导致整个 React Flow 重挂。
- `awaiting_input`、`reopened`、`superseded` 不与 `completed` 混淆。
### FE-8:节点说明抽屉与对话定位
点击画布节点打开 `NodeInspectorDrawer`。抽屉只展示计划合同:
1. 节点名称、角色、分析角度和当前状态标签。
2. 节点职责:负责解决什么问题。
3. 要根据什么:所需输入类型、上游节点类型、资料范围和时效要求。
4. 要产出什么:成果类型、结构要求和验收条件。
5. 可用工具、禁止事项、预算上限。
6. 上游与下游关系。
抽屉不展示:
- 节点实际生成内容。
- 工具调用返回值。
- EvidenceBundle 或其他成果物正文。
- attempt 执行时间线。
- 错误堆栈和调试信息。
- 报告正文。
交互:
- “在对话中对此节点提出意见”关闭抽屉、聚焦左侧 Composer,并绑定 `targetNodeId` 上下文。
- 若该节点对应正在并行运行的成员,同时绑定该成员的 `agentRunId` 并切换左侧选择器,使用户发送前能先看到该成员的真实对话与工具过程。
- 用户仍需在对话区输入“重新检索”“重新分析”等要求;抽屉不直接发起业务变更。
- 状态标签来自后端节点状态,但不加载节点结果接口。
- 关闭抽屉不清空 selected node;再次点击同一节点可恢复说明。
报告预览不使用常驻第三栏。最终报告、章节候选和版本先作为左侧 MessageList 中的普通消息/文件卡出现;点击文件卡时复用 `ArtifactFileDetail` 或现有 Markdown 能力打开大尺寸 Dialog,关闭后回到两栏工作台。
验收:
- 抽屉宽度不改变两栏布局,不挤压左侧对话区。
- 抽屉只读取节点合同和图结构,不请求节点执行结果。
- 所有原生成员消息、工具过程、错误和报告入口都能在左侧对话时间线找到。
### FE-9:全过程对话干预
`ReportComposer` 根据状态显示不同提示:
- 空闲:输入报告需求。
- 规划中:补充要求将在规划安全点应用。
- 运行中:协调者正在判断影响范围。
- 等待用户:优先回答等待问题。
- 完成后:可问答、重写、润色或重新研究。
发送流程:
1. 生成 idempotency key。
2. 插入 pending 用户消息;带 `targetNodeId`/`agentRunId` 时在相关成员消息流和共享上下文中引用同一条用户消息,不重复持久化。
3. 显示“正在识别修改范围”。
4. `command.classified` 后展示意图和影响节点。
5. 高成本或低置信度命令显示确认卡。
6. `command.accepted` 后显示排队或等待安全点。
7. 完成后关联新 node attempt 或 report version。
8. 失败时保留原消息和重试入口。
典型映射:
| 用户输入 | 预期操作 |
|---|---|
| “增加政策风险角度” | 新增角度节点和受影响下游 |
| “资料太旧,重新找” | 重新检索指定证据包 |
| “竞争格局分析不到位” | 复用证据,重新分析 |
| “重写第二章” | 只生成章节替换候选 |
| “语气更正式” | 新报告版本,不重新研究 |
| “为什么得出这个结论” | 只读问答,不修改报告 |
验收:
- 双击发送不创建重复命令。
- 选中“品牌竞争分析师”后发送补充要求,只影响该成员及依赖分析,其他并行成员仍保持消息流和运行状态。
- 运行中反馈不会让 run 或 node 误变 completed。
- “重写第二章”不会触发全量检索。
- “资料太旧”不会只做文字润色。
### FE-10:会话历史、报告版本与导出
- History Tab 显示标题、状态、更新时间、当前版本。
- 新建会话不删除旧会话。
- 会话切换时先关闭旧 SSE,再加载新快照。
- 报告修改先生成候选,确认后创建新版本。
- 版本恢复创建新的头版本,不删除历史。
- 下载复用现有 Markdown/Word 能力。
- 文件名来自报告标题,不暴露宿主机或沙箱路径。
验收:历史会话可完整恢复左侧消息流程、右侧节点状态和报告版本;不额外恢复节点结果面板。
### FE-11:测试、视觉回归和性能
单元测试:
- event normalizer/reducer。
- message adapter。
- plan revision 和选择状态。
- node status 和 available actions。
- command pending/idempotency。
组件测试:
- 空态、澄清、规划、候选、运行、等待、完成、失败、取消。
- 卡片选择、节点点击、只读说明 Drawer、确认弹窗和报告预览 Dialog。
- MessageList 通用业务卡插槽零回归。
- 单成员流式文本、TeamSay、工具调用与工具返回的原样重放;并行成员交错到达、切换标签、切回后不中断 delta。
- 非当前标签成员发起协助时显示待回复徽标,点击后使用原生协助卡回复到正确节点。
E2E:
1. 新建会话并输入新能源汽车报告要求。
2. 选择候选并开始。
3. 运行中增加风险角度。
4. 重新检索海外最新资料。
5. 完成后重写第二章。
6. 刷新、断网重连、切换会话。
7. 停止和重试失败节点。
性能门:
- 30 节点画布不卡顿。
- 长消息列表使用现有优化策略。
- SSE 高频 delta 合帧后再 setState。
- 画布和消息区使用独立 selector,避免互相触发整树重渲染。
- 节点抽屉只订阅节点合同和状态,不能因阶段产出更新而反复重渲染。
- 只渲染当前聚焦成员和共享消息的可视窗口;非当前成员的消息仍写入 store,但不因 token delta 重渲染当前 `MessageList`。
---
## 8. 后端接口依赖
前端开发前,后端必须冻结以下接口:
```text
POST /api/report-collaboration/sessions
GET /api/report-collaboration/sessions
GET /api/report-collaboration/sessions/{session_id}
PATCH /api/report-collaboration/sessions/{session_id}
DELETE /api/report-collaboration/sessions/{session_id}
POST /api/report-collaboration/sessions/{session_id}/messages
GET /api/report-collaboration/sessions/{session_id}/messages
POST /api/report-collaboration/sessions/{session_id}/plan-requests
GET /api/report-collaboration/sessions/{session_id}/plans
POST /api/report-collaboration/sessions/{session_id}/plans/{plan_id}/select
POST /api/report-collaboration/sessions/{session_id}/runs
GET /api/report-collaboration/runs/{run_id}
GET /api/report-collaboration/runs/{run_id}/events
GET /api/report-collaboration/runs/{run_id}/stream
POST /api/report-collaboration/runs/{run_id}/commands
POST /api/report-collaboration/runs/{run_id}/cancel
GET /api/report-collaboration/runs/{run_id}/nodes/{node_id}/contract
GET /api/report-collaboration/sessions/{session_id}/reports
POST /api/report-collaboration/sessions/{session_id}/report-rewrites
POST /api/report-collaboration/sessions/{session_id}/report-rewrites/{id}/apply
```
前后端共享枚举:
- session status。
- run status。
- node/node attempt status。
- command intent/status。
- event type/version。
- artifact type/schema version。
- report version/change type。
---
## 9. 前端开发顺序
页面与契约层已完成(代码在 `frontend-web/src/report-collaboration/`):
1. `RC-FE-001`:视觉截图与行为基线。
2. `RC-FE-002`:路由、菜单和左对话/右画布两栏空壳。
3. `RC-FE-003`:页面 token、弹窗、抽屉和响应式。
4. `RC-FE-004`:API 类型和 mock server。
5. `RC-FE-005`:SSE reducer 与快照恢复。
6. `RC-FE-006`:MessageList adapter 和通用卡片插槽。
7. `RC-FE-007`:需求澄清与候选方案卡。
8. `RC-FE-008`:流程画布和自定义节点。
9. `RC-FE-009`:只读节点说明抽屉与对话目标定位。
10. `RC-FE-010`:统一 Composer 和命令确认。
11. `RC-FE-011`:报告预览、版本和导出。
12. `RC-FE-012`:历史、重连和异常恢复。
13. `RC-FE-013`:组件/E2E/视觉/性能验收。
依赖规则:
- 契约已冻结为 `src/report-collaboration/api/types.ts`;后端 `RC-BE-001` 已对齐。
- 真实 SSE 联调:后端 `RC-BE-011` 已挂载 `GET /runs/{id}/stream`;默认无后台循环时流只会重放已入库事件。
- 报告版本操作已接后端 `RC-BE-015`(改写候选 / apply / restore)。
- 执行画布实时状态依赖打开 `worker_enabled`;质量角色内核已在 `RC-BE-013` 落地,真实成员见 `RC-BE-017`。
---
## 10. 前端完成定义
页面与 mock 联调路径已落地。真实后端可联调 `GET /runs/{id}/stream`、领取/恢复(`RC-BE-012`,默认不挂 worker)、质量角色(`RC-BE-013`)以及报告改写/恢复(`RC-BE-015`)。此前以 mock driver 覆盖交互,不以「前端未编码」计未完成。
前端只有同时满足以下条件才算完成:
1. 页面结构与 Coze 参考页面逐状态验收通过。
2. 消息区复用 DeerFlow `MessageList`,其他聊天页面无回归。
3. 候选卡、左侧对话流程、右侧流程图和报告来自同一 session/run/event 状态。
4. 点击候选只预览,明确确认后才执行。
5. 用户可以在规划、运行、完成后三个阶段继续对话。
6. 协助卡显示或提交不会导致前端误标完成。
7. 节点点击只打开说明抽屉,可查看职责、所需输入、预期输出、工具和验收条件,不展示执行结果。
8. 节点重开、返工和 superseded 状态表达正确。
9. SSE 重连、刷新和历史切换无重复消息和状态倒退。
10. 报告修改以候选和版本形式完成,不直接覆盖正式报告。
11. 页面始终只有对话与画布两个常驻区域,所有执行过程和产出入口都能在对话时间线中找到。
12. 响应式、键盘、对比度、加载反馈和错误恢复通过验收。
@@ -0,0 +1,485 @@
# 报告协作工作台 — 前端对接指南
> **适用版本:** RC-BE-000~018 已落地(2026-09-06)
> **配套文档:**
> - [前端实施计划](./agentscope-多智能体报告协作工作台-前端实施计划.md)
> - [后端实施计划](./agentscope-多智能体报告协作工作台-后端实施计划.md)
> - 模块 README:`frontend-web/src/report-collaboration/README.md`
本文说明**前端如何对接真实后端**:环境配置、API 契约、用户流程、状态机、错误处理与联调验收。前端 API 客户端与页面已实现,通常**无需再写一套 HTTP 层**。
---
## 1. 结论速览
| 问题 | 答案 |
|------|------|
| 前端还要写接口吗? | **不用**。`src/report-collaboration/api/` 已封装 REST + SSE,页面通过 `getReportCollaborationDriver()` 调用。 |
| 为什么调不通? | 最常见:`config.yaml` 里 `report_collaboration.enabled: false` → 全部 **503**;或 `worker_enabled: false` → run 不入队执行。 |
| Mock 和真后端怎么切? | **Mock 已移除**。前端永远请求真实后端(`deerflow.rc-mock` / `VITE_RC_MOCK` 开关已删除);联调需 `enabled: true`。 |
| 字段会和 MySQL 对不上吗? | 前端只认 JSON 契约(`api/types.ts`),不直连表。缺列表现为后端 500/SQL 错误,部署后需**重启 Gateway** 跑 `create_all` + 列同步。 |
| 从哪进页面? | `/page/workspace/report-collaboration`(新会话)或 `/page/workspace/report-collaboration/:sessionId`。侧边栏入口默认注释,可直接 URL 访问。 |
---
## 2. 架构:前端已有什么
```
ReportCollaborationPage
└─ useReportCollaborationSession(唯一数据入口)
├─ driver.getSnapshot() ← REST 冷启动
├─ driver.openRunStream() ← SSE 实时事件
├─ driver.sendMessage / createCommand / startRun …
└─ event-reducer ← 事件 → UI 状态
ReportConversationPanel
└─ MessageList(message-adapter 映射 CollaborationMessage → LangGraph Message)
```
### 2.1 目录结构
```text
frontend-web/src/report-collaboration/
api/
types.ts # 冻结契约(snake_case,与后端 Pydantic 一致)
client.ts # rcFetch:鉴权、幂等键、乐观锁、错误映射
sessions.ts # 会话 CRUD、消息
plans.ts # 方案请求 / 列表 / 选定
runs.ts # Run、SSE、命令、节点合同
reports.ts # 报告版本、改写、恢复
driver.ts # 真实后端统一门面(Mock 驱动已移除)
state/
event-normalizer.ts
event-reducer.ts
selectors.ts
message-adapter.ts
hooks/
useReportCollaborationSession.ts # 快照 → SSE → reducer → 动作
useReportCollaborationStream.ts # 断线重连、after_seq 续接
useReportCollaborationMessages.ts
components/ …
pages/ReportCollaborationPage.tsx
```
### 2.2 事实源原则
- **冷启动:** `GET /sessions/{id}` 返回 `SessionSnapshot`(session、messages、plans、run、nodes、agent_runs、commands、report_versions、last_seq)。
- **运行中:** `GET /runs/{id}/stream?after_seq=N` 订阅 durable SSE。
- **断线 / 序号缺口:** `GET /runs/{id}/events?after_seq=N` REST 补拉。
- **澄清 / 规划阶段(尚无 run):** 每 2.5s 轮询快照(后端事件主要在 run 流上)。
纯展示状态(预览方案、聚焦成员、面板宽度)**不**写入服务器,仅存前端。
---
## 3. 后端启用与部署
### 3.1 必改配置
文件:`offline-backend-20260512/backend/config.yaml`
```yaml
report_collaboration:
enabled: false # 联调必须改为 true
worker_enabled: false # 要看成员执行必须 true
planner_model: null # 建议填可用模型名
coordinator_model: null
research_model: null
writer_model: null
reviewer_model: null
# … token_budget、max_concurrent_runs_per_user 等见同文件
```
| 配置 | 现象 |
|------|------|
| `enabled: false` | 所有路由 **503**,`code: REPORT_COLLABORATION_DISABLED` |
| `enabled: true`,`worker_enabled: false` | REST/SSE 可用,run 入队但不自动执行(测 API 可用,无成员流式输出) |
| 两者 `true` + 模型配置完整 | 完整联调 |
### 3.2 MySQL / 表结构
生产环境 Alembic 自动升级默认关闭,启动时走 `create_all` + `_ensure_orm_columns_sync`。
- 迁移脚本(可选手动跑):`20260905_01`、`20260906_01`、`20260907_01`
- ORM:`packages/harness/deerflow/persistence/report_collaboration/model.py`(13 张表)
- **部署后务必重启 Gateway**,观察日志是否有 `Failed to auto-add column report_collaboration_*`
已有 RC-003 时代旧表会通过列同步补上 `template_snapshot_json`、`budget_snapshot_json`、`usage_json`、`budget_exhausted_reason` 等可空列,并创建 `rewrite_proposals`、`audits`。
### 3.3 前端环境变量
| 变量 | 说明 |
|------|------|
| `VITE_BACKEND_BASE_URL` | Gateway 地址,默认 `http://127.0.0.1:8001` |
请求经 `apiFetch`,自动携带登录 JWT(`credentials: "include"`)。**必须先登录 DeerFlow**。
---
## 4. API 基址与请求约定
### 4.1 Base URL
```text
{VITE_BACKEND_BASE_URL}/api/report-collaboration
```
代码常量:`REPORT_COLLABORATION_BASE()`(`api/client.ts`)。
### 4.2 请求头
| Header | 何时携带 | 说明 |
|--------|----------|------|
| `Authorization` | 全部 | 由 `apiFetch` 注入 |
| `X-Idempotency-Key` | 写操作 | 格式 `rc-{uuid}`;重复提交返回首次结果 |
| `X-Expected-Revision` | 选方案、开 run、改会话等 | 会话 `requirement_revision` 或方案 `revision` |
| `Accept: text/event-stream` | SSE | `openRunStream` |
### 4.3 错误响应格式
```json
{
"code": "REPORT_COLLABORATION_DISABLED",
"message": "报告协作工作台未启用",
"detail": { }
}
```
前端映射(`api/client.ts`):
| HTTP | 异常类 | 典型场景 |
|------|--------|----------|
| 409 | `ConflictError` | revision 过期、active run 冲突 |
| 422 | `ValidationError` | 参数/语义错误、缺幂等键 |
| 429 | `RateLimitError` | 预算耗尽、每用户并发 run 上限 |
| 5xx | `ServerError` | 可带同一幂等键重试 |
| 0 | `NetworkError` | fetch 失败 |
### 4.4 完整路由清单(与前端 client 对齐)
| 方法 | 路径 | 前端模块 |
|------|------|----------|
| POST | `/sessions` | `sessions.createSession` |
| GET | `/sessions` | `sessions.listSessions` |
| GET | `/sessions/{id}` | `sessions.getSessionSnapshot` |
| PATCH | `/sessions/{id}` | `sessions.updateSession` |
| DELETE | `/sessions/{id}` | `sessions.deleteSession` |
| GET | `/sessions/{id}/messages` | `sessions.listMessages` |
| POST | `/sessions/{id}/messages` | `sessions.sendMessage` |
| POST | `/sessions/{id}/plan-requests` | `plans.requestPlans` |
| GET | `/sessions/{id}/plans` | `plans.listPlans` |
| POST | `/sessions/{id}/plans/{planId}/select` | `plans.selectPlan` |
| POST | `/sessions/{id}/runs` | `runs.startRun` |
| GET | `/sessions/{id}/reports` | `reports.listReportVersions` |
| POST | `/sessions/{id}/report-rewrites` | `reports.createRewrite` |
| POST | `/sessions/{id}/report-rewrites/{id}/apply` | `reports.applyRewrite` |
| POST | `/sessions/{id}/report-versions/{id}/restore` | `reports.restoreReportVersion` |
| GET | `/runs/{id}` | `runs.getRun` |
| GET | `/runs/{id}/events` | `runs.listRunEvents` |
| GET | `/runs/{id}/stream` | `runs.openRunStream` |
| POST | `/runs/{id}/cancel` | `runs.cancelRun` |
| POST | `/runs/{id}/commands` | `runs.createCommand` |
| POST | `/runs/{id}/commands/{id}/confirm` | `runs.confirmCommand` |
| POST | `/runs/{id}/commands/{id}/cancel` | `runs.cancelCommand` |
| GET | `/runs/{id}/nodes/{nodeId}/contract` | `runs.getNodeContract` |
| GET | `/runs/{id}/artifacts` | (后端已暴露,前端按需接) |
| GET | `/runs/{id}/sources` | (后端已暴露,前端按需接) |
---
## 5. 用户流程与 API 时序
```mermaid
sequenceDiagram
participant U as 用户
participant FE as 前端 RC
participant API as Gateway
participant W as Worker
U->>FE: 输入需求
FE->>API: POST /sessions
FE->>API: POST /sessions/{id}/messages
API-->>FE: 澄清(快照 messages)
U->>FE: 回答澄清
FE->>API: POST /messages
FE->>API: POST /plan-requests
API-->>FE: 候选方案(快照 plans)
U->>FE: 采用方案
FE->>API: POST /plans/{id}/select
U->>FE: 开始协作
FE->>API: POST /runs
FE->>API: GET /runs/{id}/stream
W-->>API: 持久化事件
API-->>FE: SSE
U->>FE: 运行中干预
FE->>API: POST /commands → confirm
```
### 5.1 阶段 0:进入页面
| 路由 | 行为 |
|------|------|
| `/page/workspace/report-collaboration` | 空态;`POST /sessions` 后跳转带 `sessionId` |
| `/page/workspace/report-collaboration/:sessionId` | 加载快照;有 `location.state.rcInitialMessage` 时自动发首条消息 |
路由注册:`frontend-web/src/pages/WorkspaceRoutes.tsx`。
侧边栏入口已在 `workspace-nav-chat-list.tsx` 中放开(「报告协作」项),也可直接访问 URL。
### 5.2 阶段 1:需求澄清(无 run)
```http
POST /api/report-collaboration/sessions
POST /api/intent/init
POST /api/intent/stream
POST /api/report-collaboration/sessions/{sessionId}/intent-turns
```
- 报告页面固定调用内置 `report-collaboration-intent` 智能体;`/api/intent/stream` 原样透传 LangGraph `messages-tuple`,不是等待模型完成后包装响应。
- 正文 token、provider reasoning 和工具调用步骤实时写入当前 `MessageList` 气泡;终态后 `/intent-turns` 只负责持久化镜像与状态交接,不再调用模型识别意图。
- 旧 `/sessions/{sessionId}/messages` 在 `empty/clarifying` 阶段返回 `409 USE_STREAMING_INTENT`,防止退回阻塞式 `ainvoke` 路径。
- 会话状态:`empty` → `clarifying` → `planning` → `proposal_ready`。
- 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。
- 意图智能体输出 `INTENT_READY` 后,`POST /intent-turns` 自动创建并完成 `plan_request`;前端无需再展示“确认意图”或“生成候选方案”按钮。
### 5.3 阶段 2:候选方案
```http
# 正常路径:由最后一轮 POST /messages 自动触发规划
POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select
```
- `POST /plan-requests` 仅保留给“重新生成”与兼容调用,不是首次规划的必经步骤。
- 规划师尚未判定 `ready` 时直接调用该兼容接口会返回 `422 REQUIREMENT_NOT_READY`,防止跳过意图确认。
- **预览方案**(`previewPlanId`):纯前端,不调 API。
- **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。
- 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。
- 右栏 `ReportFlowCanvas` 使用 `PlanNode` / `PlanEdge` 渲染 DAG。
### 5.4 阶段 3:开始执行与 SSE
```http
POST /api/report-collaboration/sessions/{sessionId}/runs
{ "plan_id": "plan-xxx" }
GET /api/report-collaboration/runs/{runId}/stream?after_seq=0
Accept: text/event-stream
```
SSE 每条 `data:` 为 JSON:
```typescript
interface CollaborationEvent {
eventId: string;
sessionId: string;
runId?: string | null;
seq: number;
type: CollaborationEventType;
timestamp: string;
data: unknown;
}
```
常用 `type`(完整列表见 `api/types.ts`):
| 类型 | 含义 |
|------|------|
| `message.created` / `message.delta` / `message.completed` | 对话镜像流式 |
| `tool_call.*` / `tool_result.*` | 工具调用与返回 |
| `team_message.*` | 成员 / 协调者广播 |
| `plan.proposed` / `plan.selected` | 方案生命周期 |
| `run.status.changed` | Run 状态 |
| `node.status.changed` / `node.progress` | 节点执行 |
| `command.confirmation.required` | 需用户确认的干预 |
| `report.version.created` | 报告版本落盘 |
| `heartbeat` | 保活 |
### 5.5 阶段 4:运行中干预
```http
POST /api/report-collaboration/runs/{runId}/commands
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/confirm
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/cancel
POST /api/report-collaboration/runs/{runId}/cancel
```
- Run 活跃时 `send()` 自动走 `createCommand`(而非 `sendMessage`)。
- 可从节点抽屉 / `AgentStageSelector` 传入 `target_node_id`、`agent_run_id`。
- `InterventionCard` 展示 `confirmation_required` 命令;确认后调 `confirmCommand`。
`CommandIntent` 枚举(后端分类,前端展示 reason/impact):`answer_question`、`update_requirements`、`research_again`、`rewrite_section`、`rerun_node`、`replan`、`cancel` 等。
### 5.6 阶段 5:报告版本与改写
```http
GET /api/report-collaboration/sessions/{sessionId}/reports
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites/{rewriteId}/apply
POST /api/report-collaboration/sessions/{sessionId}/report-versions/{versionId}/restore
```
- `createRewrite` 只生成候选,不直接覆盖正式版本。
- `applyRewrite` / `restoreReportVersion` 产生新的 head 版本,历史保留。
- `ReportPreviewDialog` 展示 `ReportVersion.markdown`。
---
## 6. 前端状态机(接入方须知)
**不要**在页面里用零散 REST 字段拼 UI 状态。统一路径:
```text
SessionSnapshot → applySessionSnapshot()
CollaborationEvent[] → normalizeCollaborationEvent() → applyCollaborationEvents()
→ selectors(derivePagePhase、selectPlanForCanvas、…)
```
### 6.1 核心 Selector
| Selector | 用途 |
|----------|------|
| `derivePagePhase` | Composer 占位、方案卡、空态 |
| `selectPlanForCanvas` | 画布 DAG(含预览方案) |
| `selectAgentTabs` | 并行成员 Tab |
| `selectNodeStatusMap` | 节点颜色 / 状态 |
| `selectPendingConfirmationCommands` | 干预确认卡 |
| `selectHeadReportVersion` | 报告预览入口 |
| `selectStreamingAgentRunIds` | 流式成员指示 |
### 6.2 SSE 连接策略(`useReportCollaborationStream`)
- Run 非终态时连接;`completed` / `failed` / `cancelled` 后关流并 REST 最终补拉。
- 断线:指数退避重连(1s~30s),`after_seq` 取 reducer 内 `lastSeq`。
- `hasSeqGap`:暂停 live 应用,先 `listRunEvents` 回填。
### 6.3 乐观消息
用户发送时插入 `metadata.optimistic: true` 的 pending 消息;服务端镜像带相同 `idempotency_key` 时 reducer 原地替换。
---
## 7. 消息与 MessageList 复用
`useReportCollaborationMessages` 将 `CollaborationMessage[]` 转为 `@langchain/langgraph-sdk` 的 `Message[]`,并构造合成 `BaseStream` 供 `MessageList` 消费。
| 来源 | 渲染 |
|------|------|
| `human` / `ai` 文本 | 普通气泡 |
| `tool_calls` + `tool` | 现有工具卡 |
| `team_message` 事件 | 带 `name` 的成员发言 |
| 业务卡 | `businessCardSlot`(方案组、命令确认、报告可用) |
分组键:`core/messages/utils.ts` 中 `assistant:task-report-import` 同级扩展 `assistant:business-card`,判据 `additional_kwargs.report_collaboration_card`。
### 7.1 在其他页面嵌入 RC(最小集)
1. `useReportCollaborationSession(sessionId)`
2. `ThreadContext.Provider` + `MessageList` + `businessCardSlot`
3. 引入 `report-collaboration/styles/report-collaboration.css`
---
## 8. 单测
Mock 驱动(原 `api/mock.ts`)与 `deerflow.rc-mock` / `VITE_RC_MOCK` 开关已移除,前端只请求真实后端;本地演示请直接联调后端。
单测:
```bash
cd frontend-web
pnpm test:report-collaboration
```
---
## 9. 联调验收清单
### 9.1 准备
- [ ] `report_collaboration.enabled: true`
- [ ] `report_collaboration.worker_enabled: true`
- [ ] 五个 `*_model` 已配置且可用
- [ ] Gateway 已重启(MySQL 表/列就绪)
- [ ] 前端已登录(JWT 有效)
### 9.2 探活
```bash
curl -s -H "Authorization: Bearer <token>" \
http://127.0.0.1:8001/api/report-collaboration/sessions
```
期望:**200** + `{ "sessions": [...] }`,而非 503。
### 9.3 页面流程(浏览器 Network)
1. 打开 `/#/page/workspace/report-collaboration`
2. 输入需求 → `POST .../sessions` **200**
3. 跳转 → `GET .../sessions/{id}` **200**,响应含 `SessionSnapshot`
4. 首条消息 → `POST .../messages` **200**
5. 请求方案 → `POST .../plan-requests` **200**
6. 采用方案 → `POST .../plans/{id}/select` **200**
7. 开始协作 → `POST .../runs` **200**
8. `GET .../runs/{id}/stream` 持续收到 SSE(`message.delta`、`node.status.changed` 等)
9. 刷新页面 → 消息与 `last_seq` 续接正确
10. 运行中输入「重新检索」→ `POST .../commands` → 确认卡 → `confirm` **200**
### 9.4 常见失败
| 现象 | 原因 | 处理 |
|------|------|------|
| 全部 503 | `enabled: false` | 改配置并重启 |
| run 一直 queued | `worker_enabled: false` | 改为 true |
| 409 选方案 | revision 过期 | 刷新后重选(前端已自动 refresh) |
| 429 | 预算/并发上限 | 调大配置或取消其他 run |
| 500 + SQL | 表缺列 | 重启 Gateway,查列同步日志 |
---
## 10. 契约与类型维护
- **单一事实源:** `frontend-web/src/report-collaboration/api/types.ts`
- **后端镜像:** `offline-backend-20260512/backend/app/report_collaboration/contracts/`
- 修改字段须同步两份实施计划与本指南,并跑前后端相关测试。
`SessionSnapshot` 形状(摘要):
```typescript
interface SessionSnapshot {
session: ReportSession;
messages: CollaborationMessage[];
plans: ReportPlanCandidate[];
run: ReportRun | null;
nodes: CollaborationNodeState[];
agent_runs: AgentRun[];
commands: CollaborationCommand[];
report_versions: ReportVersion[];
last_seq: number;
}
```
`ReportRun` 可选字段(RC-BE-016):`usage`、`budget_exhausted_reason`。
---
## 11. 后续工作(非对接阻塞)
| 项 | 说明 |
|----|------|
| RC-BE-019 | 灰度发布与离线部署文档 |
| 侧边栏入口 | 已放开(`workspace-nav-chat-list.tsx` 的「报告协作」项) |
| iframe 嵌入 | RC 页尚未专门适配 `embed=1`,可参考现有深链模式扩展 |
| 真人盲评 | RC-BE-018 为确定性评测集,上线前仍需计划 §11.4 人工评审 |
---
## 12. 相关代码索引
| 用途 | 路径 |
|------|------|
| API 客户端 | `frontend-web/src/report-collaboration/api/` |
| 会话 Hook | `frontend-web/src/report-collaboration/hooks/useReportCollaborationSession.ts` |
| 页面 | `frontend-web/src/report-collaboration/pages/ReportCollaborationPage.tsx` |
| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
| 后端路由 | `offline-backend-20260512/backend/app/gateway/routers/report_collaboration.py` |
| 配置 | `offline-backend-20260512/backend/config.yaml` → `report_collaboration` |
| 持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/report_collaboration/` |
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
# 日常使用复查(2026-09-14)
范围:ji 分支上继续检查知识库管理、助手知识库、我的项目、项目问答和官方入口。沿用本地隔离后端数据目录 `.runtime/ji-acceptance`,浏览器检查 464×642 与 1440×900 两种尺寸;未向生产库写入验收项目。
## 发现与修复
**首屏白屏的直接业务触发点也已修复:** `LlmWikiKnowledgeSelector` 在懒加载/未加载知识目录时,每次渲染创建新的空数组,推荐问题 effect 随之回写父页状态,形成无限更新。将目录空结果和默认助手库选择稳定化后,重新从登录进入新对话成功,欢迎引导、输入框及知识空间下拉均可操作(截图 06)。仅升级依赖并不能消除这个业务循环;上述修复与依赖兼容更新共同保留。
1. **首次进入:高影响,已修复依赖。** 本轮首次登录后出现整页空白,控制台为 ScrollArea `Maximum update depth exceeded`。安装版本 1.2.10 使用不稳定引用回调;升级并锁定 1.2.18,同步 package-lock,核对源码使用稳定 setState 引用。冷启动仍复现组合引用循环,进一步发现各基础组件带入 Slot 1.2.3/1.2.4;统一锁定 Slot 1.3.3,npm 与 pnpm 均配置覆盖间接依赖。依据 [Radix 发布记录](https://www.radix-ui.com/primitives/docs/overview/releases) 的 React 19 无限重渲染及 Slot 引用修复。
2. **新建项目:已修复。** 手机表单出现两个菜单按钮(截图 01)。由外层提供菜单时不再显示内部按钮;官方入口也去掉无效的内部侧栏按钮。
3. **项目历史:已修复。** 我的项目侧栏链接跳到旧项目空间(截图 02),导致返回目的地变化。现在保留我的项目路径,普通聊天与旧项目空间保持原路由。手机点历史后菜单自动收起。官方侧栏原本把项目聊天打开为普通聊天,现按项目归属进入项目工作区;`/official/workspace/projects/groups` 根路径回到普通问答项目列表。
4. **项目设置与失败恢复:已修复。** 默认智能体更新后,同步新对话选择器;项目对话权限/归属加载失败、普通问答项目列表失败均有重试入口。
5. **助手知识检索:已修复。** 连续检索或切换库时,以递增请求序号隔离过期成功/失败/结束回调,防止旧结果覆盖新库;新检索清除旧结果;空输入禁止提交。无结果原本只有小标签(截图 03),现在说明可调整关键词并提供浏览目录按钮。输入框补充可访问名称。
6. **窄屏输入栏:已修复。** 464px 截图中发送箭头被工具栏挤出右边缘(截图 07);工具区允许换行、发送区不收缩,截图 08 确认按钮完整可见。项目问答网络错误也增加“重新加载对话”,只重建读取状态,不自动重发消息。
## 复测记录
- 具名项目创建成功;新建项目对话,通过真实模型收到“项目问答回归通过”;刷新保留消息与项目归属。
- 修复工具栏后,在窄屏实际点击发送并收到“窄屏发送通过”;首屏知识空间菜单完成加载、选择助手库后显示已选择 1 个,未再出现循环白屏。
- 手机项目历史链接确认留在 `qa/projects/groups`;桌面官方 groups 根路径跳回正确列表,官方项目历史进入相同项目。
- 官方和常规项目切换时偶发网络 `Failed to fetch`,刷新/重新挂载恢复历史消息;没有将其描述为已排除的网络问题,新增恢复按钮未进行独立网络故障注入测试。
- 助手知识库列表、阅读、检索无结果、返回入口实际操作;空查询按钮禁用。检索竞态由代码检查修复,未使用浏览器网络故障注入。
- 后端 22 个相关文件重新运行:**273 passed**,涵盖知识库管理/导入/检索、Wiki、编码与向量、项目归属隔离、策略持久化及调用边界。
- 前端项目与策略测试:**33 passed**,新增项目历史保持入口、旧项目和普通聊天/创建向导回归。
- 生产构建通过。全仓类型检查仍有 **59 个既有错误**,与上一轮错误列表一致;不宣称全仓类型检查通过。
## 证据与边界
本轮截图与日志存放在 `frontend-web/.runtime/daily-usage-audit/`:`01-create-before.png`、`02-history-before.png`、`03-knowledge-search.png`、`04-official-desktop.png`、`05-official-project-after.png`(网络失败状态)、后续 after 截图,以及 tests/build/typecheck 日志。后端日志为 `.runtime/ji-acceptance/daily-regression.txt`。
上一轮真实文件入库、向量命中、方案生成与保存的验证详见 `ji-knowledge-projects-acceptance.md`。本轮不重复付费生成完整方案;没有进行生产全量语料质量评估、负载测试、断网故障注入或完整无障碍合规审计。已检查可见导航、表单名称、窄屏显示和主要失败恢复,不能据此承诺所有模块没有缺陷。代码未提交、推送或部署。
File diff suppressed because it is too large. Load diff
+37
View File
@@ -0,0 +1,37 @@
# ji 分支知识库与我的项目验收
日期:2026-09-14。基线:`ji` / `f62a512a`,已从 origin fast-forward 更新。
## 修复范围
1. 简洁模式进入我的项目保留功能导航,覆盖列表、策略工作台和问答项目;移动端有菜单展开入口。
2. 策略结果卡片的模型思考前置;检查意图、评判、方案、展开视图保持过程在正文前。
3. 普通问答改为真实项目分组:先填写项目名称创建项目,再在项目中创建多条问答。服务端持久归属与权限校验沿用 analysis-projects;不自动迁移或标记历史普通聊天。官方入口支持对应 groups 子路由。
4. 修复方案列表要求预先选中策略导致无法进入的问题;任务变化后已有方案仍可查看;请求序号避免旧请求失败覆盖成功结果。
5. 修复知识库管理目录缓存导致上游删除后仍显示的问题;保留检索端缓存。
6. 项目编辑保留知识库绑定,助手知识库补充普通用户空态。
## 自动化
- 后端 22 个相关测试文件:273 passed(知识库、Wiki、向量、编码器、项目归属、策略配置与持久化、边界等)。
- 前端策略与项目测试:31 passed。新增用例覆盖未选择策略、已有方案变更任务、未完成评判及全部策略失败。
- `tests/test_knowledge_live_embedding.py`:真实接口 1 passed;默认跳过,设置 `KNOWLEDGE_TEST_EMBEDDING_KEY` 后运行。默认服务 SiliconFlow、模型 BAAI/bge-m3;可用 `KNOWLEDGE_TEST_EMBEDDING_URL` / `KNOWLEDGE_TEST_EMBEDDING_MODEL` 更换。
- `pnpm build`:通过。
- `pnpm typecheck`:未完全通过,59 个错误位于此次未改动文件(nextra 缺失、Markdown 插件类型、旧 JSX 类型及旧问答页等);修改文件无类型错误。不能将生产构建通过等同于全仓类型检查通过。
## 真实接口与浏览器
测试使用 `.runtime/ji-acceptance` 独立数据目录,保留测试数据便于复查,未操作业务数据库已有项目。
- BAAI/bge-m3:中英文 3 条输入,均返回 1024 维有效向量。
- 实际检索服务:相关正文排名、指定库范围、草稿排除、概览查询通过。
- 两个项目 A/B:A 创建两条对话,B 列表为零;跨项目读取 A 的对话返回 404。
- 浏览器:简洁模式完整菜单可见;新建具名普通问答项目;项目内真实发送并收到回答;第二条对话独立;回列表仍是一张项目卡,刷新后保留。
- 方案整理:真实模型返回 1539 字符正文,含 agent_done / all_done,流正常结束。
- 方案入口:完成评判但尚未勾选策略的测试项目出现在方案列表。浏览器进入后勾选策略、生成 2755 字符正文、保存成功;服务端校验 synthesisStatus=done 且 solutionSavedAt 非空,刷新后正文与导出按钮恢复。
- 官方入口:`/official/workspace/projects/groups/new` 正常展示具名问答项目表单,官方侧栏保留。
- 助手知识库:创建、重命名、列表、拒绝 .py 上传通过;上传测试文本任务 completed;生成 Wiki 页面,向量检索命中 1 条,详情 HTTP 200。
## 限制
真实模型测试使用最小合成读书活动/设备资料,未对生产全量知识语料进行质量评估。API 密钥仅用于测试进程,不提交配置。仓库原有未跟踪 docx 与 packages1.zip 保持不变。未提交、推送代码,也未执行离线发布。
@@ -0,0 +1,304 @@
# OpenVibeCoding 内网可视化应用生成平台:分步开发方案
> 文档状态:待实施
>
> 目标环境:现有 Ubuntu 22.04 Docker 服务器;对外可用端口限定为 `8020-8030`。
>
> 本文基于 2026-08-31 的只读巡检编写。未在目标服务器上安装、停止或修改任何服务。
## 1. 目标与范围
将 OpenVibeCoding 改造成一个使用公司内网模型、在公司服务器 Docker 沙箱中生成并运行可视化应用的工作台。用户通过对话生成项目,查看并修改文件,在浏览器中预览运行结果。
第一期只服务以下项目类型:
- 数据大屏、数据看板、海报和展示型页面;
- React + Vite + TypeScript 前端;
- ECharts 图表、流程图;
- 可选的轻量 Python FastAPI 接口;
- 项目文件浏览、编辑、运行日志和实时预览。
第一期明确不做:
- 腾讯云 CloudBase、SCF、TCR、CodeBuddy、云端一键部署;
- 任意语言、任意 npm/PyPI 包的无限制安装;
- 用户自定义 Dockerfile、Docker Socket 挂载、宿主机命令执行;
- 直接将沙箱暴露为独立公网端口;
- 对不可信代码提供“绝对安全”的隔离承诺。
## 2. 已确认的服务器基线
目标服务器当前具备:
| 项目 | 巡检结果 | 判断 |
|---|---:|---|
| 操作系统 | Ubuntu 22.04.5 LTS | 支持 Docker 方案 |
| CPU | 32 vCPU | 足够第一期 |
| 内存 | 125 GiB;巡检时可用约 106 GiB | 足够第一期 |
| 根磁盘 | 2 TB;巡检时可用约 1.6 TB | 足够 Harbor、镜像与工作区 |
| Docker | 29.1.3,Docker Compose 2.40.3 | 已满足 |
| 负载 | 约 0.5 | 当前有余量 |
| `8020-8030` | 巡检时无监听、无 Docker 端口映射 | 可规划使用 |
该服务器已承载 DeerFlow、Coze、LiteLLM、数据库和其他容器;`80`、`8000`、`8005`、`8006`、`3306`、`5432` 等端口已经被使用。因此,本项目不能占用现有端口、网络、数据库或容器名称。
该服务器有公网 IP,且已有多项服务绑定在 `0.0.0.0`。本方案将它视作“公网可达的共享主机”处理;云安全组和宿主机防火墙必须额外核验,不能仅凭端口监听结果假设安全。
## 3. 总体架构
```mermaid
flowchart LR
U[用户浏览器] -->|:8020| G[App Builder Gateway]
U -->|:8022 /preview/<sandboxId>/| P[Preview Router]
G --> O[OpenVibe Web / Agent Server]
O --> M[现有内网模型网关]
O --> DB[(独立 PostgreSQL)]
O --> R[(独立 Redis)]
O --> SM[Local Sandbox Manager]
SM --> D[Docker Engine]
D --> S1[visual-ui Sandbox]
D --> S2[visual-fullstack Sandbox]
P --> S1
P --> S2
D --> H[Harbor :8023]
S1 --> N[内网 pnpm/npm 源或预置离线依赖]
S2 --> N
```
设计原则:
1. 保留 OpenVibe 的对话、任务、文件树、工具卡片和 OpenCode ACP 代码代理体验。
2. 模型请求仅由 OpenVibe 服务端发往既有内网模型网关;API Key 不进入沙箱。
3. 每个项目运行于独立 Docker 容器和独立工作区;沙箱只运行项目代码,不承担平台控制逻辑。
4. 浏览器预览只经过统一预览路由,不为每个项目映射新的公网端口。
5. 先用固定模板和固定依赖清单保证稳定,后续再按白名单增加依赖。
## 4. 端口规划
端口虽然可用,但应尽量少暴露。除 8020、8022 和按需开放的 8023 外,其余服务只加入 Docker 内部网络。
| 端口 | 服务 | 绑定范围 | 用途 |
|---:|---|---|---|
| 8020 | `appbuilder-gateway` | 公网访问面或 VPN 白名单 | 控制台页面、`/api`、SSE/WebSocket |
| 8021 | 预留 | 仅 `127.0.0.1` | 平台调试;第一期不对外开放 |
| 8022 | `preview-router` | 公网访问面或 VPN 白名单 | 统一预览入口:`/preview/<sandboxId>/` |
| 8023 | Harbor | 仅沙箱节点、构建节点和管理员网段 | 私有镜像仓库 HTTPS Registry |
| 8024 | 预留 | 不对外开放 | 后续 npm 私服或运维服务 |
| 8025-8030 | 预留 | 不对外开放 | 扩展、迁移或故障切换 |
### 4.1 预览路由规则
暂时没有可用泛域名时,使用路径型预览:
```text
http(s)://<服务器地址>:8022/preview/<sandboxId>/
```
项目模板的 Vite `base` 和 HMR 路径必须由启动脚本注入为该前缀,确保图片、JS/CSS 资源及热更新不会错误请求到根路径。后续若获得 `*.preview.company.local` 内网泛域名,可切换为子域名预览,用户体验更好。
## 5. 镜像、模板与离线依赖策略
### 5.1 标准镜像
第一期维护两种版本化镜像,不允许用户自行构建 Dockerfile:
| 镜像 | 用途 | 预置内容 |
|---|---|---|
| `appbuilder/visual-ui:<version>` | 大屏、海报、纯前端 | Node.js 20、pnpm、React、Vite、TypeScript、ECharts、流程图库、统一 UI 库、Tailwind、图标与中文字体 |
| `appbuilder/visual-fullstack:<version>` | 前端加接口 | 上述前端依赖 + Python 3.12、FastAPI、Uvicorn、Pydantic |
流程图库第一期统一选择一种(建议 React Flow),避免同时生成多种流程图语法和样式。地图、背景图、字体等资源也应作为受版本控制的本地资产打包,不能依赖 Google Fonts 或公网 CDN。
### 5.2 项目模板
每次创建项目时从镜像中的模板初始化到 `/workspace`:
```text
templates/
visual-screen/ # React + ECharts + React Flow + Mock 数据
visual-screen-api/ # visual-screen + FastAPI + /api 反向代理
poster-page/ # 海报、营销展示、静态动效
```
`visual-screen-api` 约定:前端只请求相对路径 `/api/*`;预览网关将其转发到同一沙箱的 FastAPI `8000` 端口。没有后端时保留 Mock 数据模块,生成的页面仍可运行。
### 5.3 依赖预置方式
不能仅把安装命令写进 Dockerfile 后在用户创建项目时再联网执行。正确做法是:
1. 在受控构建环境中锁定 `package.json`、`pnpm-lock.yaml`、`requirements.lock`。
2. 构建镜像时执行 `pnpm fetch`,在镜像保存只读 pnpm store;Python 包保存到 `/opt/wheelhouse`。
3. 沙箱初始化使用 `pnpm install --offline --frozen-lockfile` 和 `pip install --no-index --find-links=/opt/wheelhouse`。
4. 第一期开启“固定依赖白名单”:AI 只能使用镜像已安装的依赖;新依赖先由管理员审核、加入锁文件和下一版基础镜像。
5. 第二期部署 Verdaccio/Nexus 等内部制品源,作为受控补充而非允许访问公网 npm/PyPI。
## 6. OpenVibe 改造边界
### 6.1 保留
- 前端交互:对话、任务状态、文件浏览与修改、预览、日志;
- OpenCode ACP 代码代理;
- 项目与会话领域模型;
- 代码代理的流式事件协议。
### 6.2 替换或关闭
| OpenVibe 当前能力 | 第一期开源/内网实现 |
|---|---|
| CodeBuddy | 只启用 OpenCode,配置内网 OpenAI 兼容模型网关 |
| CloudBase、SCF 沙箱 | 新增 `LocalDockerSandboxManager` |
| TCR | Harbor |
| CloudBase 环境、用户环境池 | Docker 容器、工作区卷、沙箱记录 |
| CloudBase 消息与事件持久化 | 独立 PostgreSQL;Redis 用于临时队列与状态 |
| CloudBase 存储、函数、MCP、资源管理后台 | 第一期开关关闭或移除入口 |
| CloudBase 项目模板和系统提示词 | 替换为大屏/海报/FastAPI 模板与受控依赖提示词 |
重点改造点是将 OpenVibe 的 `scf-sandbox-manager` 和 CloudBase 生命周期调用替换为本地 Docker 生命周期。为降低前端和 OpenCode 工具改动范围,新管理器应适配现有的文件、命令、日志、健康检查与工作区信息契约,而不是改写所有 Agent 工具。
## 7. 沙箱安全基线
每个沙箱容器必须满足:
- 非 root 用户运行;禁止 `privileged`,禁止 Docker Socket,禁止宿主机目录挂载;
- `--cap-drop=ALL`、`no-new-privileges`、`pids-limit`;
- 首期默认限制:`1 vCPU`、`3 GB 内存`、`10 GB 工作区`、60 分钟空闲回收;
- 使用独立 Docker network;默认无公网出口,只允许访问预置依赖、必要的内网 DNS 和平台回调;
- 数据库、Redis、Harbor 管理接口、宿主机管理端口不在沙箱网络可访问范围;
- 运行命令、容器生命周期、文件变更和预览访问写入审计日志;
- 平台服务由受限服务账号调用 Docker API;用户代码永远不直接获得 Docker 权限。
第一期为“内部可信用户的容器隔离”。若后续允许大量非可信代码或文件上传,应迁移为 Kubernetes 节点,并评估 gVisor/Kata 等更强运行时隔离。
## 8. 分步实施计划
### 阶段 0:部署前隔离与基线确认
**工作项**
1. 为本项目建立独立目录、Docker Compose Project、卷和网络,命名统一为 `appbuilder-*`。
2. 确认云安全组、iptables/nftables 规则:仅允许管理员 SSH;8020、8022、8023 仅向 VPN 或指定办公出口开放。
3. 不复用当前服务器上已有的 MySQL、PostgreSQL、Redis、Nginx 配置;建立独立实例或独立命名空间。
4. 创建内部 CA 证书或确定可信 HTTPS 证书方案;Docker 构建/运行节点信任 Harbor 证书。
5. 轮换目标服务器 root 密码,改用 SSH 密钥和受限运维账号;关闭 root 密码远程登录。
**验收**
- `8020-8030` 无端口冲突;
- 新容器、网络和数据卷均不影响既有 DeerFlow 等服务;
- 业务数据库和沙箱端口无法从非授权公网地址访问。
### 阶段 1:内网基础设施与镜像供应链
**工作项**
1. 在独立 Compose 项目中部署 Harbor,主入口为 `8023`,镜像数据使用独立持久化目录。
2. 构建并推送 `visual-ui`、`visual-fullstack` 两种基础镜像;记录镜像版本、SBOM 和依赖锁文件。
3. 完成三个模板及离线依赖初始化脚本。
4. 先不启动漏洞扫描;严格断网环境下后续通过离线包维护漏洞库。
5. 预留内部 npm/PyPI 源接入方式;第一期仅使用预置依赖。
**验收**
- Docker daemon 能从 Harbor 拉取指定版本镜像;
- 在禁止公网访问的测试网络中,三个模板均能启动并构建;
- 一个 ECharts 页面、一个流程图页面和一个 FastAPI `/api/health` 均可运行。
### 阶段 2:OpenVibe 去 CloudBase 化与内网模型接入
**工作项**
1. Fork 固定版本的 OpenVibeCoding,建立 `local-platform` 改造分支,记录上游 commit 与许可证。
2. 只保留 OpenCode Agent;将 provider 指向现有内网模型网关,模型凭证只保存在服务端环境变量或密钥服务。
3. 将基础实体、任务、消息、流式事件的持久化迁移为独立 PostgreSQL;不再调用 CloudBase 集合。
4. 关闭 CloudBase 环境、函数、存储、数据库、资源仪表盘和相关 MCP 入口。
5. 将系统提示词和项目创建默认项改为“可视化页面 + 受控模板/依赖”。
**验收**
- 不设置任何腾讯云、CloudBase、CodeBuddy、TCR 环境变量时,平台可启动;
- 用户发起“生成 ECharts 大屏”后,OpenCode 能调用内网模型并生成项目文件;
- 服务端日志和沙箱环境中均没有模型 API Key。
### 阶段 3:本地沙箱管理器与预览路由
**工作项**
1. 实现 `LocalDockerSandboxManager`:创建、启动、停止、超时回收、查询状态和删除容器。
2. 每个沙箱分配工作区卷、随机内部容器地址、模板类型和资源限制;在数据库记录 `sandbox_id`、项目、用户、镜像版本、过期时间和状态。
3. 实现或适配沙箱内 runner:文件读写、受限命令执行、日志流、健康检查、前端端口和 FastAPI 端口发现。
4. 实现 `preview-router`:由平台根据 `sandboxId` 维护路由,将 `/preview/<sandboxId>/` 代理到对应内部容器;支持 WebSocket/HMR。
5. 增加容器异常退出、端口未就绪、构建失败、超时与手动重启的可见状态。
**验收**
- 两个用户创建的项目存在于不同容器和不同工作区;
- 修改 ECharts 配置文件后,预览地址显示修改结果;
- FastAPI 模板的 `/api/health` 可通过前端相对路径访问;
- 沙箱不能访问 Docker Socket、宿主机数据库和非白名单网络;
- 空闲超时后容器被回收,项目文件仍可恢复。
### 阶段 4:产品能力收口
**工作项**
1. 在文件面板支持安全的文件查看、编辑、保存、目录树刷新与下载;限制文件大小和可编辑扩展名。
2. 在对话中增加模板选择和能力提示:大屏、流程图、海报、可选 FastAPI。
3. 为 Agent 增加依赖白名单、图表主题、中文字体、Mock 数据、接口契约等明确提示词。
4. 增加“运行/停止/重启/删除沙箱”和日志面板;仅项目所有者或管理员有相应权限。
5. 增加项目快照、模板版本记录和基础审计日志。
**验收**
- 从一句自然语言需求到可预览项目的完整链路可完成;
- 用户可查看并手动修改文件,刷新后改动保留;
- 页面覆盖 ECharts、流程图、响应式大屏和可选 FastAPI 接口;
- 非拥有者不能读取、修改或预览另一用户的项目。
### 阶段 5:上线前安全与运行验收
**工作项**
1. 压测 6 个并发沙箱,并记录 CPU、内存、磁盘、容器启动时间、首屏预览时间和模型等待时间。
2. 验证镜像保留策略、工作区配额、容器回收、数据库备份和 Harbor 备份恢复。
3. 执行越权、路径穿越、命令注入、网络探测、资源耗尽和模型密钥泄露测试。
4. 建立日常运维操作:新增基础依赖、构建镜像、发布模板、回滚镜像、清理工作区和审计查询。
**验收**
- 并发运行时既有 DeerFlow 等服务不受明显影响;
- 任一沙箱异常退出不会影响其他项目或平台控制服务;
- 备份可恢复项目元数据、工作区和 Harbor 镜像;
- 仅开放计划中的入口端口,内部组件无不必要公网监听。
## 9. 首期资源与并发建议
由于该主机已有多项业务,首期不应按理论最大值分配:
| 组件 | 首期限制 |
|---|---|
| Harbor | 8 vCPU、16 GB 内存、500 GB 数据盘配额 |
| OpenVibe 控制服务 | 4 vCPU、8-16 GB 内存 |
| 单个 UI 沙箱 | 1 vCPU、3 GB 内存、10 GB 工作区 |
| 单个 Fullstack 沙箱 | 1.5 vCPU、4 GB 内存、12 GB 工作区 |
| 沙箱并发 | 先限制为 6;稳定后再逐步上调 |
当并发超过 10 个,或要面向更广泛用户开放时,应优先增加独立沙箱节点,而不是继续在本共享主机上加容器。
## 10. 实施顺序与交付物
建议按以下提交/交付顺序推进,每一步都可独立验收和回退:
1. `docs: confirm appbuilder network, ports and security baseline`
2. `infra: add Harbor and versioned visual sandbox images`
3. `infra: add offline templates and dependency locks`
4. `feat: replace OpenVibe CloudBase persistence and model provider`
5. `feat: add local Docker sandbox manager`
6. `feat: add sandbox runner and path-based preview router`
7. `feat: add visual-screen and FastAPI project workflows`
8. `test: add sandbox isolation, preview and recovery coverage`
9. `ops: add backup, cleanup, monitoring and rollout runbook`
达到阶段 3 即可交付“内网模型驱动、生成并预览大屏/海报页面”的 PoC;达到阶段 5 后再作为受控的内部多人平台发布。
@@ -0,0 +1,139 @@
# Iframe Chat Page Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a standalone `/page/workspace/chats_iframe/*` chat page that reuses the existing thread system and history, while showing only `你好` and no input box before the first user message.
**Architecture:** Keep the existing workspace shell and chat runtime intact, then add a new route pair and a copied page component dedicated to the iframe path. The new page keeps the original chat behavior once a thread already has messages, but it owns its empty-state rendering and URL transition to `/page/workspace/chats_iframe/:thread_id`.
**Tech Stack:** React, TypeScript, React Router, Next navigation shim, existing LangGraph thread hooks
---
### Task 1: Add the iframe chat routes
**Files:**
- Modify: `frontend-web/src/pages/WorkspaceRoutes.tsx`
- [ ] **Step 1: Add `chats_iframe/new` and `chats_iframe/:thread_id` routes that render the new page inside the existing workspace layout and chat runtime**
```tsx
<Route
path="chats_iframe/new"
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<IframeChatPage />
</ChatRuntime>
</WorkspaceLayout>
}
/>
<Route
path="chats_iframe/:thread_id"
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<IframeChatPage />
</ChatRuntime>
</WorkspaceLayout>
}
/>
```
- [ ] **Step 2: Ensure the new route imports compile cleanly**
Run: `rg -n "IframeChatPage|chats_iframe" frontend-web/src/pages/WorkspaceRoutes.tsx`
Expected: `WorkspaceRoutes.tsx` contains one import for `IframeChatPage` and both `chats_iframe` route entries.
### Task 2: Add a standalone iframe chat page
**Files:**
- Create: `frontend-web/src/pages/IframeChatPage.tsx`
- Reference: `frontend-web/src/pages/ChatPage.tsx`
- [ ] **Step 1: Copy the current `ChatPage` implementation into `IframeChatPage` and preserve the existing chat runtime integration**
```tsx
export default function IframeChatPage() {
const routeBase = useRouteBase();
const router = useRouter();
const [showFollowups, setShowFollowups] = useState(false);
const [hasStartedConversation, setHasStartedConversation] = useState(false);
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
useThreadChat();
// keep the rest of the existing chat hooks and handlers aligned with ChatPage
}
```
- [ ] **Step 2: Change the thread creation URL replacement to stay on the iframe route family**
```tsx
onStart: (createdThreadId) => {
setThreadId(createdThreadId);
setIsNewThread(false);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${routeBase}/chats_iframe/${createdThreadId}`,
);
},
```
- [ ] **Step 3: Replace the default landing state with a simple `你好` view and do not render the input box while the iframe page is still a brand-new untouched chat**
```tsx
{isLandingState && mounted ? (
<div className="flex min-h-[240px] items-center justify-center">
<div className="text-2xl font-medium">你好</div>
</div>
) : null}
{!isLandingState &&
(mounted ? (
<div className="w-full -translate-y-4">
<InputBox
className="w-full"
isNewThread={false}
threadId={threadId}
autoFocus={false}
status={thread.error ? "error" : thread.isLoading ? "streaming" : "ready"}
context={settings.context}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading}
onContextChange={(context) => setSettings("context", context)}
promptPrefix={promptPrefixProp}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
</div>
) : (
<div aria-hidden="true" className="bg-background/5 h-32 w-full -translate-y-4 rounded-3xl border" />
))}
```
- [ ] **Step 4: Keep message rendering, thread title, export, token usage, todos, artifact triggers, and all post-first-message behavior identical to the original page**
Run: `rg -n "MessageList|ThreadTitle|ArtifactTrigger|TokenUsageIndicator|TodoList" frontend-web/src/pages/IframeChatPage.tsx`
Expected: The new page still renders the same core chat primitives used by `ChatPage`.
### Task 3: Verify the new page compiles
**Files:**
- Test: `frontend-web/src/pages/IframeChatPage.tsx`
- Test: `frontend-web/src/pages/WorkspaceRoutes.tsx`
- [ ] **Step 1: Run the frontend build**
Run: `cmd /c npm run build`
Expected: Build succeeds without TypeScript or bundling errors related to `IframeChatPage` or `chats_iframe`.
- [ ] **Step 2: Spot-check the changed routes in the built source**
Run: `rg -n "chats_iframe" frontend-web/src/pages`
Expected: Matches appear in `WorkspaceRoutes.tsx` and `IframeChatPage.tsx`.
- [ ] **Step 3: Commit after verification**
```bash
git add docs/superpowers/plans/2026-06-01-iframe-chat-page.md frontend-web/src/pages/WorkspaceRoutes.tsx frontend-web/src/pages/IframeChatPage.tsx
git commit -m "feat: add iframe chat page routes"
```
+28 -3
View File
@@ -1,10 +1,25 @@
# 本地开发(pnpm dev)— 与原先 .env 保持一致
# 个人覆盖请新建 .env.development.local(已 gitignore,优先级更高)
# 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。
VITE_USE_REAL_DATA=true
# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(consumer 外网不可达时调试用)。
VITE_MOCK_COP_TASK_DETAIL=true
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:true 走本地样例假数据(真实接口未就绪时调试用);联调通过后置 false。
VITE_MOCK_SQ_REPORT=false
# 深链任务详情(cop-task-detail:任务名称/描述)数据源:true 走本地假数据(真实接口未就绪时调试用);联调通过后置 false。
VITE_MOCK_COP_TASK_DETAIL=true
# Gateway / DeerFlow API
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
# CMZS后端接口
@@ -24,11 +39,21 @@ VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
# 我的笔记前端
VITE_AIPI_FRONTEND_URL= xxx
VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
# 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
# 工作室/空间前端地址(用于 auth/callback 等跳转)
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
VITE_AIPI_FRONTEND_URL=http://localhost:3000
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=https://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
VITE_AIPI_KNOWES_URL=https://ch1.b.uat.4.cn/knowes
# MaxKey 鉴权(getTokenByOnlineTicket)
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
# 可选:Canvas 独立 LangGraph 地址(不设则回退到 VITE_LANGGRAPH_BASE_URL)
# VITE_CANVAS_LANGGRAPH_BASE_URL=
@@ -15,8 +15,16 @@ VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
# 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
# 工作室/空间前端地址(用于 auth/callback 等跳转)
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
VITE_AIPI_KNOWES_URL=http://ch1.b.uat.4.cn/knowes
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
+39 -3
View File
@@ -5,25 +5,47 @@
# ├─────────────────┼──────────────────────────────────────────────────┤
# │ pnpm dev │ .env.development + .env.development.local │
# │ pnpm build │ .env.production + .env.production.local │
# │ pnpm build:network │ .env.network + .env.network.local(内网地址) │
# │ scripts/start-* │ 脚本注入的环境变量会覆盖上述文件(见下方说明) │
# └─────────────────┴──────────────────────────────────────────────────┘
#
# 推荐:
# - 日常本地开发:改 .env.development(已提交,团队默认 UAT 地址)
# - 本机连 127.0.0.1:8001:复制 .env.development.local.example → .env.development.local
# - 内网打包部署:改 .env.production 后再执行 pnpm build
# - 内网打包部署:改 .env.network 后执行 pnpm build:network(或改 .env.production 后 pnpm build)
#
# 若浏览器直连后端报 CORS,请在 Gateway 配置 CORS_ORIGINS,包含前端源站,例如:
# CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173,http://127.0.0.1:5174
# 若浏览器直连后端报 CORS,请在 Gateway 配置 GATEWAY_CORS_ORIGINS,包含前端源站;无界嵌入时还要加宿主源站,例如:
# GATEWAY_CORS_ORIGINS=http://127.0.0.1:5174,http://localhost:5174,http://127.0.0.1:5183,http://localhost:5183
# --- 开发环境(见 .env.development)---
# VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
# VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
# VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
# VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
# --- 生产打包(见 .env.production)---
# 部署前修改 .env.production 中的地址,再执行 pnpm build
# --- 任务看板(任务分析 / 三情分析)数据源开关 ---
# true=走真实接口;false 或未设=这两页使用本地假数据(演示用,无需后端)。
# .env.development / .env.production / .env.network / .env.sit 均默认 true(真实数据),
# 仅本机 .env 默认 false(假数据)。改后需重启 dev / 重新打包生效。
# VITE_USE_REAL_DATA=true
# --- 任务详情(登录深链 cop-task-detail)数据源开关 ---
# 默认真实接口(未设/false);改成 true 时该接口走本地假数据(consumer 外网不可达时调试用)。
# 与 VITE_USE_REAL_DATA 独立。改后需重启 dev / 重新打包生效。
# VITE_MOCK_COP_TASK_DETAIL=false
# VITE_POSITION_ROUNDTABLE_DEMO_MODE=false
# true=免登录直达岗位分析,完全使用前端 JSON,不请求任何后台接口。
# 仅限离线演示环境;生产环境请保持 false。
# VITE_POSITION_ROUNDTABLE_STANDALONE_MODE=false
# MaxKey 鉴权根地址(无界 onlineTicket 换 token)
# VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
# VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
# VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
# VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
# VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
@@ -32,4 +54,18 @@
# 可选
# VITE_CANVAS_LANGGRAPH_BASE_URL=
# VITE_STATIC_WEBSITE_ONLY=
# 部署级紧凑导航:true 时固定收起工作台左侧栏,并隐藏换肤与全量/简洁模式切换。
# VITE_FORCE_COLLAPSED_SIDEBAR=false
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
# VITE_COMPACT_MENU_TYPE=A
# 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。
# 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。
# VITE_TURBO_MODE_VISIBLE=false
# VITE_GITHUB_OAUTH_TOKEN=
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
# true 时,普通登录后进入同一 Vite 应用中隔离的 /official/*。
# 已指定 redirect 或 taskId/goPath 的登录仍保持原来的优先级。
# 仅 .env.development 默认 true;其余环境保持 false。
# VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
+60
View File
@@ -0,0 +1,60 @@
# 本地开发(pnpm dev)— 与原先 .env 保持一致
# 个人覆盖请新建 .env.development.local(已 gitignore,优先级更高)
# 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。
VITE_USE_REAL_DATA=true
# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。
VITE_MOCK_COP_TASK_DETAIL=false
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:默认真实接口;真实接口未就绪可改 true 走本地样例假数据。
VITE_MOCK_SQ_REPORT=false
# 深链任务详情(cop-task-detail)数据源:默认真实接口;真实接口未就绪可改 true 走本地假数据。
VITE_MOCK_COP_TASK_DETAIL=false
# Gateway / DeerFlow API
VITE_BACKEND_BASE_URL=http://10.19.200.5:2026
VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api
# 内网环境保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
# CMZS后端接口
VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm
# 佳来模型对话接口
VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt
# 多轮检索接口
VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat
VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat
VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary
VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web
VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
# 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
# 工作室/空间前端地址(用于 auth/callback 等跳转)
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
VITE_AIPI_CONSUMER_URL=http://10.19.200.5:8002/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
VITE_AIPI_KNOWES_URL=http://ch1.b.uat.4.cn/knowes
# MaxKey 鉴权(getTokenByOnlineTicket)
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
# 可选:Canvas 独立 LangGraph 地址(不设则回退到 VITE_LANGGRAPH_BASE_URL)
# VITE_CANVAS_LANGGRAPH_BASE_URL=
+25
View File
@@ -1,12 +1,26 @@
# 生产打包(pnpm build)— 部署前请改成实际内网/生产地址
# 打包后变量会写入 dist,运行时不可再改
# 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。
VITE_USE_REAL_DATA=true
# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。
VITE_MOCK_COP_TASK_DETAIL=false
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:默认真实接口;真实接口未就绪可改 true 走本地样例假数据。
VITE_MOCK_SQ_REPORT=false
# Gateway / DeerFlow API
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
# 线上保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
# CMZS后端接口
VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm
# 佳来模型对话接口
VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt
# 多轮检索接口
@@ -19,10 +33,21 @@ VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
# 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
# 我的空间前端
VITE_AIPI_FRONTEND_URL= xxx
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=https://present.a.uat.4.cn/present/domain
# B 系统知识库 iframe 根地址
VITE_AIPI_KNOWES_URL=https://ch1.b.uat.4.cn/knowes
# MaxKey 鉴权(getTokenByOnlineTicket)
VITE_AUTH_BASE_URL=https://auth.y.uat.4.cn
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
# 工作室/空间前端地址(用于 auth/callback 等跳转)
VITE_AIPI_FRONTEND_URL=https://knowledgetoolweb.b.uat.4.cn
# 可选:Canvas 独立 LangGraph 地址
# VITE_CANVAS_LANGGRAPH_BASE_URL=
@@ -3,3 +3,5 @@
# VITE_BACKEND_BASE_URL=https://your-intranet-host/deerflow
# VITE_LANGGRAPH_BASE_URL=https://your-intranet-host/deerflow/api
# VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
# VITE_AIPI_BACKENP_URL=https://your-notebook-backend-host
# VITE_AIPI_FRONTEND_URL=https://your-notebook-frontend-host
+59
View File
@@ -0,0 +1,59 @@
# SIT 环境打包(pnpm build:sit / npm run build:sit)— 接口地址为 UAT 的 sit 版本
# 打包后变量会写入 dist,运行时不可再改
# 任务看板数据源:true=真实接口(默认);改成 false 可让任务分析/三情分析两页走本地假数据。
VITE_USE_REAL_DATA=true
# 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。
VITE_MOCK_COP_TASK_DETAIL=false
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:默认真实接口;真实接口未就绪可改 true 走本地样例假数据。
VITE_MOCK_SQ_REPORT=false
# 深链任务详情(cop-task-detail)数据源:默认真实接口;真实接口未就绪可改 true 走本地假数据。
VITE_MOCK_COP_TASK_DETAIL=false
# Gateway / DeerFlow API
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
# SIT 环境保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
# CMZS后端接口
VITE_API_BASE_URL=https://ch1.b.sit.4.cn/zncm
# 佳来模型对话接口
VITE_AIPI_CHAT_URL=https://ch1.b.sit.4.cn/gpt
# 多轮检索接口
VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.sit.4.cn/jialai_b1_chat
VITE_AIPI_TASK_CHAT_URL=https://ch1.b.sit.4.cn/qq_b1_chat
VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.sit.4.cn/knowlibrary
VITE_AIPI_B1COH_URL=https://ch1.b.sit.4.cn/web
VITE_AIPI_B2ACTOR_URL=https://ty.b.sit.4.cn
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.sit.4.cn/knowes/api/iframeKnowledgeUrl
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
VITE_AIPI_MOPECHAT_URL=https://ch1.b.sit.4.cn/likun_b1_mope
# 工作室/空间后端接口(侧边栏“工作室”列表、空间详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.sit.4.cn
VITE_NOTEBOOK_API_BASE_URL=https://knowledgetoolserver.b.sit.4.cn
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
VITE_STUDIO_ENABLED=true
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
VITE_AIPI_CONSUMER_URL=https://ch1.b.sit.4.cn/consumer
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
VITE_AIPI_PRESENT_URL=http://present.a.sit.4.cn/present/domain
# B 系统知识库 iframe 根地址
VITE_AIPI_KNOWES_URL=https://ch1.b.sit.4.cn/knowes
# MaxKey 鉴权(getTokenByOnlineTicket)
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
VITE_AIPI_PGLINKTO_URL=https://pg.b.sit.4.cn
VITE_AIPI_XKLINKTO_URL=https://kz.b.sit.4.cn
# 工作室/空间前端地址(用于 auth/callback 等跳转)
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
# 可选:Canvas 独立 LangGraph 地址
# VITE_CANVAS_LANGGRAPH_BASE_URL=
+46
View File
@@ -0,0 +1,46 @@
**Comparison target**
- Source visual truth (dark intelligent-agent list): `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-75f6ce1c-cd70-4b8c-a80a-a3d100105c69.png`
- Additional source references: `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-00567484-ba8a-4595-b386-52e1467ae9cf.png` (segmented switch), `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-02808803-05ba-4f2c-be50-c72b484a9f06.png` (business-chain node card), `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-adf38fec-7baf-4d01-9c73-bdbae94b774e.png` (business-chain selector)
- Implementation screenshots: `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-light.png`, `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-dark.png`
- Full-view comparison evidence: `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-comparison.png`
- Viewport: 1280 × 720. Light route: `#/page/strategy/qa/position-roundtable`; dark embedded route: `#/page/strategy/qa/position-roundtable?embed=1&theme=dark`.
- State: local browser has no authenticated API session. The original intent API is invoked and reports `Authentication required`; business-chain and agent endpoints return no usable data.
**Findings**
- [P1] Populated agent and business-chain states cannot be compared yet.
Location: right agent list and non-planning-role chain diagram.
Evidence: reference images show populated cards and nodes; the implementation has `0 个` agents and no available business chain in the unauthenticated preview.
Impact: card density, selected-card indicator, chain node flow, and agent-detail transition cannot be truthfully verified against live data.
Fix: verify the route in an authenticated session with at least one public chain and tagged agents.
**Open Questions**
- The final business-chain stage arrangement is still pending product confirmation. The compact graph maps whatever stage configuration is returned, while intentionally omitting dependency-label text as requested.
**Implementation Checklist**
1. Left task panel is now display-only and retains its independent position-roundtable task adapter.
2. Business-chain selection uses the original roundtable card/radio/relationship-entry pattern.
3. Every position uses the original `useStep1Intent` interface and `Step1Panel` message list/input panel; only the position header changes.
4. The right switch now uses the exact `课题研究 / ai写作` segmented-control treatment (light gray track, white active state; dark blue glass equivalent).
5. The embedded layout widens the left panel to 360px and applies the deep-blue right-panel tokens used by the reference.
6. Other roles render a taller top-to-bottom card-node flow without dependency labels. The canvas supports mouse-drag panning, and each node retains only a name plus a single-line description.
**Follow-up Polish**
- [P3] If live content is unusually long, tune the compact flow node width or horizontal scrolling after seeing the production chain data.
**Comparison history**
- Iteration 1: task fields were editable and the right TDesign card tabs looked unlike the reference. Replaced the task form with display-only information and replaced the tabs with the existing `课题研究 / ai写作` segmented switch.
- Iteration 2: business-chain selection was a dropdown and the flow was vertical avatar chips. Replaced it with original-roundtable selection cards and compact business-chain configuration-style node cards with link lines but no dependency labels.
- Iteration 3: dark right-panel background differed from the supplied deep-blue reference. Applied the `#032650` / `#042b59` / `#2b8ffe` token family and captured the revised embedded dark state in the full-view comparison.
- Iteration 4: removed the custom disabled input from non-planning positions and reused the original roundtable input panel. Enlarged the left flow viewport, retained its connection lines, and removed node level/seat/status metadata.
- Iteration 5: converted the compact flow from left-to-right to top-to-bottom. Added mouse-drag panning and single-line description truncation; populated-chain visual QA remains blocked by the unauthenticated preview.
- Iteration 6: constrained the flow canvas to its card viewport, narrowed nodes from 190px to 158px, and realigned branch gaps plus connector anchors to prevent clipping and offset lines.
**Final result**
blocked
@@ -0,0 +1,253 @@
# AI 写作「四阶段内置智能体化」改造方案
> 目标:把 AI 写作现有的 **素材收集专家 / 写作大纲 / 作家写作 / 编辑审核** 四个功能,
> 从「硬编码的 LangGraph 节点 + 硬编码提示词」改造为由**内置功能型智能体**驱动——
> 每个阶段对应一个可配置的内置 agent(独立人设 SOUL、可配模型、可挂技能/工具)。
本文给出现状梳理、目标设计、以及**分步可独立交付的实现计划**。
---
## 0. 一句话结论
不推倒重来。**保留现有图的编排骨架、4 个用户干预点、流式与状态机,只把每个阶段节点的「大脑」从「硬编码 prompt 直调 LLM」替换为「加载对应内置 agent 的 SOUL+配置、在子线程上跑该 agent、按结构化契约解析其输出」。** 这样前端几乎不动,复杂逻辑(严格模式、素材不足求助、逐章流式、技能检索步骤)全部保留。
---
## 1. 现状梳理(精确到文件)
AI 写作是一条 LangGraph 图:`packages/harness/deerflow/agents/ai_writing/graph.py`
```
intent_parser → researcher → (pause 素材确认) → writer_outline → (pause 大纲确认)
→ writer_draft → (pause 草稿确认 / 素材不足求助) → editor → (pause 编辑打回) → done
```
四个待改造阶段(均为**硬编码节点**,直接调 LLM、不走 agent 运行时):
| 阶段(用户语言) | 节点文件 | 提示词 | 产出(`state.py`) |
|---|---|---|---|
| 素材收集专家 | `nodes/researcher.py` | `prompts/researcher_prompts.py` | `material_package`(keywords + materials) |
| 写作大纲 | `nodes/writer_outline.py` | `prompts/writer_prompts.py` | `current_outline`(Outline JSON) |
| 作家写作 | `nodes/writer_draft.py` | `prompts/writer_prompts.py` | `current_draft`(Markdown,逐章流式) |
| 编辑审核 | `nodes/editor.py` | `prompts/editor_prompts.py` | `review_result`(评分 + 问题列表 JSON) |
关键事实:
- **LLM 调用方式**:节点通过 `nodes/_utils.py` 的 `get_model()` + `llm_json()` **直接调 LLM**,提示词写死在 `prompts/*.py`。**没有**走 agent 人设 / 技能 / 工具体系。
- **模型**:全程单一 `state.model_name`(用户在写作表单里选的主模型),各阶段无法分别配。
- **干预点 / 流式 / 严格模式 / 素材不足求助**:都在 `graph.py` 的 pause 节点和 `writer_draft` 里,逻辑较重,需原样保留。
- **前端**:`frontend-web/src/open-canvas/contexts/AIWritingContext.tsx` 是状态机(`researching/writing_outline/writing_draft/reviewing/...`),时间线 `AIWritingTimeline.tsx` 按 `progress_events`(已带 `agent_name` 字段)渲染。
---
## 2. 可复用的现成机制(不用造轮子)
代码库里**已经有**一套成熟的「内置功能型智能体」范式——圆桌的 `roundtable-intent / roundtable-recommender / roundtable-coordinator`:
1. **资源即代码**:`app/gateway/routers/_roundtable_seed_assets/<agent-id>/`
- `config.yaml`(`id` / `name` / `description`,可扩展 `model` / `skills` / `tool_groups`)
- `SOUL.md`(人设 + 职责 + 工作流 + **结构化输出契约**)
2. **种子器**:`app/gateway/routers/_roundtable_seed.py::ensure_roundtable_functional_agents`
- 启动时 + 相关路由入口自愈:目录不存在才从资源字节级落盘到 `.deer-flow/agents/<id>/` + DB(**已存在则永不覆盖**,尊重本地编辑)。
3. **运行**:在**独立 thread** 上跑该内置 agent(见 `app/gateway/routers/intent.py`,`INTENT_AGENT_ID = "roundtable-intent"`),通过 lead agent 运行时把 SOUL.md 作系统提示,流式经 LangGraph `custom`/`messages` 通道透传。
4. **结构化输出约定**:SOUL 规定模型末尾输出固定 **marker + ```json```**(如 `[INTENT_READY]\n```json{...}```),后端用正则解析(`intent.py::_INTENT_READY_PATTERN`)。
> 我们要做的,本质就是**照搬这套范式,给 AI 写作做 4 个功能型 agent**,并让对应的图节点改成「跑这个 agent + 解析其结构化输出」。
另有 agents 通用体系可复用:`agents` 表、`/api/agents` CRUD、智能体管理页(配模型/技能/工具)——4 个写作 agent 落盘后天然出现在这里,可被 admin 编辑。
---
## 3. 目标设计
### 3.1 四个内置写作智能体(建议 id / 职责)
| Agent id | 名称 | 取代节点 | 结构化输出 marker | 产出 schema |
|---|---|---|---|---|
| `ai-writing-researcher` | 素材收集专家 | `researcher` | `[MATERIALS_READY]` | `MaterialPackage` |
| `ai-writing-outliner` | 大纲规划师 | `writer_outline` | `[OUTLINE_READY]` | `Outline` |
| `ai-writing-writer` | 作家 | `writer_draft` | `[SECTION_READY]`(逐章) | 章节 Markdown |
| `ai-writing-editor` | 编辑 | `editor` | `[REVIEW_READY]` | `ReviewResult` |
每个 agent 自带:
- `config.yaml`:`id / name / description` +(扩展)`model`(该阶段专用模型,可空=回退用户主模型)、`skills`(如 researcher 挂检索技能)、`tool_groups`。
- `SOUL.md`:由现有 `prompts/*.py` 内容迁移改写,**追加结构化输出契约**(严格 marker + JSON 格式 + 反例,仿 `roundtable-intent` SOUL 的「硬规则」写法,压制弱模型乱输出)。
### 3.2 节点改造方式(核心)
每个阶段节点保留**输入/输出契约不变**(仍读写 `state` 的同一批字段),内部从:
```
# 旧:硬编码 prompt 直调
model = get_model(config, state["model_name"])
result = llm_json(model, SYSTEM_PROMPT, USER_PROMPT.format(...))
```
改为:
```
# 新:跑对应内置 agent,按 marker 解析
result = await run_writing_agent(
agent_id="ai-writing-outliner",
state=state, config=config,
user_payload=<本阶段输入>, # 意图/素材/大纲/草稿等拼成的一段输入
marker="[OUTLINE_READY]", # 结构化输出契约
)
```
新增统一助手 `nodes/_agent_runner.py::run_writing_agent(...)`:在子 thread 上跑指定内置 agent(复用 `intent.py` 的 run/stream 机制)、把流式 chunk 透传到 `custom` 通道(前端时间线不回归)、用复用的 `_utils.parse_json` 多级容错解析 marker 后的 JSON。
### 3.3 为什么选这个方案(A)而非全多智能体重写(B)
- **方案 A(推荐)**:保留图骨架,逐节点替换大脑。前端基本不动;干预点/流式/严格模式/素材求助原样保留;可**按阶段灰度 + feature flag 回退**,风险可控。
- **方案 B(不推荐)**:仿圆桌重做成多 agent 编排,需重写编排、状态、流式、4 个干预点对接,工作量数倍、回归面巨大,收益(更"纯"的多 agent)对本场景并不必要。
---
## 4. 配置与可管理性
- **落盘**:仿 roundtable 新增 `_ai_writing_seed_assets/` + 一个 `ensure_ai_writing_functional_agents()` 种子器,在 Gateway 启动与 ai_writing 会话入口自愈。
- **管理界面**:4 个 agent 落盘后即出现在智能体管理页,admin 可编辑 **SOUL(人设)/ 模型 / 挂载技能**。可选:在「文章类型配置页」旁加一个「写作智能体配置」入口,或给这 4 个打一个 `tag` 便于筛选。
- **模型优先级**:节点跑 agent 时 `该 agent config.model > 用户写作表单选的主模型 model_name`(保留现有"用户选模型"语义,新增 per-阶段覆盖能力)。
- **技能**:researcher 现有的技能检索能力(`search/skill_search.py`)改为 `ai-writing-researcher` 的**挂载技能**,admin 可增减;检索步骤事件(`SkillStep`)形态保留给前端展示。
---
## 5. 分步实现计划(每步可独立交付 / 验收)
### Phase 0 · 契约冻结
- 定 4 个 agent 的 `id / name`、每个的 **marker + JSON schema**(与 `state.py` 的 `MaterialPackage/Outline/DraftArticle/ReviewResult` 严格对齐)。
- **验收**:本文 §3.1 / §6 的契约评审通过、字段与现有 TypedDict 一一对应。
### Phase 1 · 内置 agent 种子化(后端,纯新增,不接管流程)
- 新建 `app/gateway/routers/_ai_writing_seed_assets/<4 个 agent>/{config.yaml, SOUL.md}`;SOUL 由现有 `prompts/*.py` 迁移改写 + 追加结构化输出契约。
- 仿 `_roundtable_seed.py` 写 `ensure_ai_writing_functional_agents()`,挂到启动 lifespan + ai_writing 入口。
- **验收**:启动后 4 个 agent 出现在 `.deer-flow/agents/`、DB、`/api/agents`,能在智能体管理页查看/编辑;**此阶段不改图,写作流程行为不变**。
### Phase 2 · 节点接入「跑内置 agent」(后端,逐阶段灰度)
- 新增 `nodes/_agent_runner.py::run_writing_agent(...)`(子线程跑 agent + 流式透传 + marker 解析)。
- 加 feature flag:`config.yaml → ai_writing.use_builtin_agents.{editor,outliner,researcher,writer}`(默认 false=走旧节点)。
- **接入顺序(由易到难)**:
1. **editor**(最简单:小 JSON、无检索、无逐章流式)→ flag 打开端到端验证。
2. **outliner**(输出 Outline JSON)。
3. **researcher**(保留技能检索步骤事件、素材确认暂停)。
4. **writer**(最难:保留**逐章流式** + **严格模式素材不足求助** `blocked_sections`;建议**仍由节点控制"逐章循环"**,每章调一次 `ai-writing-writer`,`section_help` 暂停逻辑不动)。
- 每接一个,旧 prompt 与旧逻辑**保留**,节点按 flag 二选一。
- **验收**:每个阶段 flag 打开后端到端走通,产出结构与旧版一致,干预点 / 流式 / 严格模式 / 求助暂停均不回归。
### Phase 3 · 每阶段 agent 配置化
- per-agent 模型覆盖生效(`agent.config.model > model_name`)。
- researcher 的检索能力改为挂载技能,admin 可增减。
- 智能体管理页可编辑这 4 个的 SOUL / 模型 / 技能;config 热加载,新会话即生效、不重启前端。
- **验收**:admin 改某阶段 agent 的模型 / SOUL / 技能后,新会话立即按新配置运行。
### Phase 4 · 前端适配(小改)
- 时间线 / 状态文案:阶段名读对应 agent 的 `name`(`progress_events.agent_name` 已存在,基本沿用,去掉写死文案)。
- 可选:写作表单 / 设置加「查看·配置写作智能体」入口,跳智能体管理页。
- **验收**:前端展示各阶段对应的智能体名;配置入口可达。
### Phase 5 · 清理收口
- 四阶段稳定后,默认打开全部 flag、移除旧 prompt 直调路径与 flag(prompts 历史留在 git)。
- 更新 `offline-backend-20260512/backend/CLAUDE.md` 架构描述。
- **验收**:回归测试通过、冗余删除、文档同步。
---
## 6. 结构化输出契约(与现有 state 对齐)
各 agent SOUL 末尾必须输出 `marker + ```json``` 块,schema 直接对齐 `state.py`:
- `ai-writing-researcher` → `[MATERIALS_READY]`:`{ "keywords": string[], "materials": Material[] }`(`Material` 见 `state.py`,含 `id/title/content/source/relevance_score/source_type/...`)。
- `ai-writing-outliner` → `[OUTLINE_READY]`:`Outline = { "title": string, "sections": OutlineSection[] }`(`OutlineSection = { section_title, key_points: KeyPoint[], material_ids }`)。
- `ai-writing-writer` → 逐章 `[SECTION_READY]`:`{ "section_title": string, "markdown": string, "needs_more_material"?: { "reason": string } }`(`needs_more_material` 触发严格模式求助 `blocked_sections`)。
- `ai-writing-editor` → `[REVIEW_READY]`:`ReviewResult = { "verdict": "pass"|"reject", "fact_score", "logic_score", "language_score", "overall_score", "pass_threshold", "revision_notes": ReviewIssue[] }`。
解析复用 `nodes/_utils.py::parse_json`(多级容错),marker 正则仿 `intent.py::_INTENT_READY_PATTERN`。
---
## 7. 风险与注意
1. **弱模型结构化输出不稳**:SOUL 写严格格式 + 反例 + 「输出 marker 后立刻闭嘴」硬规则(仿 `roundtable-intent` SOUL §0);解析层复用 `parse_json` 多级降级。
2. **writer 的逐章流式 + 严格模式求助最复杂**:建议**循环控制权留在节点**(每章调一次 agent),不要让 agent 一次吐全文,否则 `blocked_sections` / `section_help` / 逐章流式都要重做。
3. **researcher 技能检索步骤事件**(`SkillStep`)前端有专门卡片:迁移时保留这些 `progress_events` 形态。
4. **种子器 never-overwrite**:迭代 SOUL 后**老部署不会自动更新**(与圆桌同一问题)。运维更新办法:删 `.deer-flow/agents/<id>/` 重新种子,或后续给种子器加「内置功能 agent 按版本/hash 覆盖」开关。
5. **子线程隔离**:跑写作 agent 的子 thread 与主 ai_writing thread 状态隔离,注意 checkpointer 与清理(沿用 ai_writing 既有的会话清理)。
6. **离线内网**:researcher 的检索一律走技能(不联网),与现有「离线部署禁 web_search、走 skill」策略一致。
---
## 8. 涉及文件清单(落地时对照)
**后端(主要改动)**
- 新增 `app/gateway/routers/_ai_writing_seed_assets/{ai-writing-researcher,ai-writing-outliner,ai-writing-writer,ai-writing-editor}/{config.yaml,SOUL.md}`
- 新增 `app/gateway/routers/_ai_writing_seed.py`(种子器,仿 `_roundtable_seed.py`)
- 新增 `packages/harness/deerflow/agents/ai_writing/nodes/_agent_runner.py`(`run_writing_agent`)
- 改 `nodes/{researcher,writer_outline,writer_draft,editor}.py`(按 flag 二选一)
- 改 `config.yaml`(新增 `ai_writing.use_builtin_agents.*` 开关;可选 per-agent 默认模型)
- 保留 `prompts/*.py`(回退用,Phase 5 删)
**前端(小改)**
- 改 `src/open-canvas/components/ai-writing/AIWritingTimeline.tsx`(阶段名读 agent name)
- 可选:写作表单/设置加「配置写作智能体」入口(跳 `/page/...` 智能体管理页)
**文档**
- 本文件
- Phase 5 收口时更新 `offline-backend-20260512/backend/CLAUDE.md`
---
## 9. 实施记录(2026-06-11,Phase 1-4 已落地)
### 已交付
- **Phase 1**:`_ai_writing_seed_assets/{ai-writing-researcher,ai-writing-outliner,ai-writing-writer,ai-writing-editor}/{config.yaml,SOUL.md}` + `_ai_writing_seed.py::ensure_ai_writing_functional_agents()`,挂启动 lifespan(`_sync_legacy_agents` 之前)+ `ai_writing.list_sessions / list_article_types` 入口自愈。
- **Phase 2**:`nodes/_agent_runner.py`(`stream_writing_agent` / `run_writing_agent` / `parse_marker_json` / `resolve_agent_model_name` / `use_builtin_agent`);四节点按 `config.yaml → ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` 二选一(默认全 false,config 按 mtime 热加载,改完即对新会话生效)。agent 目录缺失时节点**静默回退旧 prompt 路径**,写作流程永不因 agent 被删而中断。
- **Phase 3(部分)**:per-agent 模型覆盖已生效(优先级 `agent config.model > 节点显式回退模型(keyword_model/rank_model)> 表单主模型 model_name`);SOUL/模型在智能体管理页可编辑、按 mtime 热加载。researcher「检索能力改挂载技能」未做(仍走节点内 SkillPicker 编排)。
- **Phase 4**:时间线 agent 名由后端 `progress_events.agent_name` 动态下发(flag 开启时读 agent config 的 `name`);`TimelineStep.tsx` 补「大纲规划师」配色(未命中名字回退灰色,admin 改名不报错)。
- **测试**:`tests/test_ai_writing_seed.py`(资源完整性 / 契约 marker / never-overwrite)+ `tests/test_ai_writing_agent_runner.py`(marker 多级解析 / 模型优先级 / flag / 流式端到端 / SOUL 缺失降级),共 31 例。
### 与 §6 契约的修订(以实现为准)
为保住「逐章流式打字机」「review_chunk/review_item_done 分维度卡片」「素材确认/求助暂停」等前端形态,编排控制权全部留在节点,agent 只承担**单次结构化产出**:
| Agent | 实际契约 |
|---|---|
| `ai-writing-researcher` | 两个子任务:`[KEYWORDS_READY]` + `{"keywords": []}`;`[MATERIALS_RANKED]` + `{"ranked_ids": [], "summary": ""}`。素材本体来自节点的检索编排(agent 不联网),`MaterialPackage` 仍由节点组装 |
| `ai-writing-outliner` | `[OUTLINE_READY]` + 完整 `Outline` JSON(新规划 / 修改模式共用,payload 开头标明任务模式) |
| `ai-writing-writer` | **无 JSON marker**:每章直接输出 Markdown 正文(保流式);严格模式素材不足只输出一行 `[[SECTION_BLOCKED]] 原因`(沿用原哨兵) |
| `ai-writing-editor` | 每个维度一次调用:`[REVIEW_READY]` + `{"score": int, "issues": []}`;综合分/verdict/pass_threshold 仍由节点按 config 计算(不信任 LLM 自判通过) |
另一处架构修订:**不在子 thread 上经 lead agent 运行时跑**(§3.2 原设想复用 `intent.py` 机制,但其 loopback 在 app 层,harness 图节点不可 import;且单轮结构化调用无需 thread/checkpointer/中间件成本)。`run_writing_agent` 直接「SOUL 作 system prompt + 单条输入」调模型,规避了 §7.5 的子线程隔离/清理风险。researcher 挂技能(Phase 3 剩余项)如需完整工具体系再评估接入运行时。
### 已知存量测试失败(与本次无关)
`test_ai_writing_graph.py`(mock 已删除的 `intent_parser.llm_json`)、`test_ai_writing_word_count.py`(mock 已删除的 `writer_draft.get_model`)、`test_ai_writing_draft_headings.py`(断言三级结构升级前的 `####→###` 行为)、`test_ai_writing_outline_revise.py`(断言旧 prompt 文案),改造前即失败,待另行清理。
## 10. Phase 3 收尾:检索类型两项化 + 配置技能驱动检索(2026-06-13 已实施)
本节落地了 §9 遗留的「researcher 检索改挂载技能」,并按新需求重新设计了检索类型与 PAUSE-1 交互。
### 10.1 检索类型(material_source)收敛为两项
- 写作表单「素材来源」改名「检索类型」,只剩 `general`(通用检索,默认)/ `notebook`(我的空间);旧值 `knowledge_base` / `skill` 由 researcher 节点按 `general` 兼容(老会话 resume 不受影响)。
- 前端:`AIWritingForm.tsx`(选项/标签)、`WritingFormContext.tsx`(默认值/类型/applyProposal 旧值映射)、`types/ai-writing.ts`、`message-list.tsx`(审批卡标签兼容旧值)。
### 10.2 通用检索 = 素材收集专家配置的技能编排
- `ai-writing-researcher` agent 的 `config.yaml → skills` 是权威配置(智能体管理页可增删/替换);种子默认 `[knowledge-base-search]`。
- 内置技能 **knowledge-base-search(知识库检索)**:把原 web/intranet 关键词并发检索包装成技能。researcher 对它特殊处理——不拼 description,直接对 Step1 生成的检索词做原生并发检索(行为与旧 knowledge_base 模式一致)。其它配置技能按 `意图 + description` 拼定向 query 检索。按配置顺序执行、累积达 `max_materials` 早停(`stop_early`)。
- 技能配置为空/全部失效 → 内存兜底回退知识库检索(`researcher._load_configured_skills`),检索永不瘫痪。
- 种子:`_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` → `skills/public/`(never-overwrite);旧部署的 researcher `config.yaml` 缺 `skills` 键时就地补默认(显式 `skills: []` 不动)。`_ai_writing_seed.py::ensure_ai_writing_functional_agents()` 一并处理。
### 10.3 PAUSE-1 输入框 → 匹配技能检索并**追加**
- 用户在素材确认卡输入自然语言(如「检索 A 的数据」)→ `re_search + user_query`:配置技能 >1 个时 `SkillPicker(candidates=配置技能)` 让模型按输入匹配排序(流式打字机保留),单技能直接用;用用户原话(非知识库技能附 description)检索。
- **追加语义**:保留原素材列表与顺序,新素材按 URL 去重后追加在末尾(ID 续号),跳过 LLM 重排与 max_materials 截断(仅 100 条硬上限),summary 显示「追加检索到 X 条新素材,当前共 Y 条」。原 20 条 + 新 20 条 = 40 条。
- 「技能检索」独立按钮已删除(通用检索本来就走技能编排);后端继续兼容旧 `enable_skill_search` action(按 user_query 追加处理)。无输入的「重新搜索」仍为覆盖式重检。
- 前端:`InterventionCard.tsx`(按钮/文案,hasQuery 时按钮文案「检索并追加素材」)、`MaterialsToolbar.tsx`(提示语)、`useAIWritingStream.ts`(素材卡回到 researching 一律置 skillSearchPending,让 SkillStepsCard 占位即时挂载)。
### 10.4 测试
- `tests/test_ai_writing_material_source.py` 重写(9 例):缺省/旧值→通用检索、自定义技能 description query、user_query 追加+URL 去重+多技能 picker 匹配(candidates 断言)、无输入重检覆盖、空配置回退。
- `tests/test_ai_writing_seed.py` 扩展(+5 例):技能资产存在、researcher 种子含默认 skills、技能种子 never-overwrite、缺 skills 键就地补丁、显式 `[]` 不动。
@@ -0,0 +1,507 @@
# AI 写作「对话驱动写作台」实现方案(可落地版)
> 需求:**复制**现有 AI 写作代码,新增一个独立页面(**原页面/原路由零改动**)。在新页面里
> **大改交互流程**——去掉「素材收集 / 大纲规划 / 作家写作 / 编辑审核」四个阶段干预卡里的**输入框**,
> 只**保留原来的确认按钮**;在页面**底部新增一个统一的多行智能输入框**,具备**强意图理解**能力:
> 重新搜索、新增检索词、添加检索素材、重新规划大纲、按要求改大纲、提修改意见、审核意见、
> 素材不足继续搜集、对写作内容提问……**覆盖现有全部交互甚至更多**。
>
> 交互节奏不变:**一步一确认**——进行到某个专家阶段时,交互就针对该专家(阶段作用域);
> 用户可以**点原来的确认按钮**,也可以**在底部输入框里发「继续下一步」的意图**推进。
> 意图**拿不准时**主动向用户**发起协助/澄清**,绝不盲目触发高代价动作(定稿 / 重搜等)。
本文是**实现指引**,落地时按第 9 节路线图分阶段推进。文中所有文件路径、符号名均对齐当前代码。
---
## 1. 现状梳理(改造地基)
现有 AI 写作是一套**独立 LangGraph 子系统**(不是 lead_agent),前后端都已成型、可复用。
### 1.1 后端 `ai_writing` graph
- 图工厂:`offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py`
(`make_ai_writing_graph`,registered as `assistant_id="ai_writing"`)
- 状态:`.../ai_writing/state.py`(`AIWritingState`)
- 四个「阶段大脑」节点:
| 阶段 | 节点 | 文件 |
|------|------|------|
| 素材收集 | `researcher` | `nodes/researcher.py` |
| 大纲规划 | `writer_outline` | `nodes/writer_outline.py` |
| 作家写作 | `writer_draft` | `nodes/writer_draft.py` |
| 编辑审核 | `editor` | `nodes/editor.py` |
- **五个暂停点**(`interrupt()`,`Command(resume=payload)` 续跑)——这是对话改造的核心可复用资产:
| pause_point | 语义 | 现有 action |
|-------------|------|-------------|
| `material_confirm` | 素材确认 | `confirm` / `re_search` / `enable_skill_search` |
| `outline_confirm` | 大纲确认 | `confirm` / `re_outline` |
| `draft_confirm` | 草稿确认 | `to_editor` / `finalize` / `user_revise` |
| `review_confirm` | 编辑打回确认 | `accept_review` / `force_finalize` |
| `section_help` | 素材不足求助 | `section_help`(+`sectionDecisions`) |
- resume 载荷(`UserInterventionPayload`,`types/ai-writing.ts`)字段:`action`、`approvedMaterialIds`、
`userQuery`(自然语言补充检索)、`editedOutline`、`outlineFeedback`、`userRevisionNotes`、
`overrideVerdict`、`selectedIssueIndices`、`sectionDecisions`。
**协议已同时支持「自然语言 + 结构化 action」**——这是对话层能复用的关键。
> 结论:**状态机 / 流式 / 暂停·续跑协议 / 持久化都可复用**,无需重写引擎。改造本质是
> 「把分散在干预卡里的输入框,换成一个底部智能输入框 + 意图路由层」,再把意图翻译回**现有
> `action + payload`**;仅少量新意图(对内容提问、素材不足自动回退重搜、追加审核意见)需新增能力。
### 1.2 前端 `open-canvas` 子系统
- 主页面:`frontend-web/src/open-canvas/pages/AIWritingPage.tsx`
- 路由注册:`open-canvas/routes/OpenCanvasRoutes.tsx`(`/page/canvas/ai-writing/*`)、
`pages/WorkspaceRoutes.tsx`(`/page/workspace/ai-writing/*`)、`pages/NotebookAIWritingPage.tsx`
- 状态机 + 流式:`contexts/AIWritingContext.tsx`(`useReducer` + LangGraph `useStream`,`assistantId:'ai_writing'`)
- 事件翻译 / reducer:`hooks/useAIWritingStream.ts`
- 类型:`types/ai-writing.ts`
- **左栏面板**:`components/ai-writing/AIWritingPanel.tsx`(时间线 + 干预卡 + 状态栏;idle 时挂启动表单)
- **要改造的输入 UI**(本次重点):
- 干预卡 `components/ai-writing/InterventionCard.tsx`:内含
`MaterialConfirmCard`(素材:有 `MaterialsToolbar` 输入框)、
`OutlineConfirmCard`(大纲:有 feedback textarea)、
`DraftConfirmCard`(草稿:有「修改意见」textarea)、
`ReviewConfirmCard`(审核:有「补充修改指令」textarea)、
`SectionHelpCard`(素材不足:按钮组)。
**每张卡里都既有输入框又有确认/动作按钮**——本次要**去掉输入框、保留按钮**。
- 启动表单 `components/ai-writing/AIWritingForm.tsx`(idle 阶段的配置表单)——本次由底部输入框接管启动。
- **已存在的对话原型**(可借鉴):`components/ai-writing/WritingSetupChat.tsx`
(`useThreadStream` 起 lead_agent 线程 + `setup_writing` 审批卡回填表单;仅管启动前配置,不直接复用)。
- 底部输入框可直接复用 UI 组件:`@/components/ai-elements/prompt-input`
(`PromptInput` / `PromptInputTextarea` / `PromptInputSubmit` / `PromptInputFooter`,见 `WritingSetupChat`)。
---
## 2. 目标与范围
### 2.1 必须满足
1. **复制式新增**:新目录 / 新路由 / 新侧边栏入口,**旧页面、旧路由、旧组件零改动**(「初始的不用动」)。
2. 页面**底部单一多行智能输入框**接管全流程交互;四个阶段干预卡里的**输入框全部移除**。
3. **保留确认按钮**:每个阶段到达暂停点时,消息流里仍渲染**产物卡 + 原来的确认/动作按钮**
(素材「确认素材」、大纲「确认大纲」、草稿「让编辑审核 / 确认定稿」、审核「按编辑意见修改 / 强制定稿」、
素材不足的三选一按钮)。用户**点按钮**或**在底部输入框发自然语言意图**二选一均可推进。
4. **强意图理解**:把用户自由文本准确路由到正确动作,**覆盖现有全部交互 + 更多**(见 3.2)。
5. **阶段作用域**:意图解析必须知道「当前停在哪个专家阶段」,把同一句话按当前阶段翻译成对应 action。
6. **素材不足自动回退重搜**:修改意见需要的信息现有素材没有时,先重搜补素材、补完自动继续原意图。
7. **拿不准就协助**:低置信意图**不猜着执行**,走澄清追问(clarify)或用户协助。
8. 保留右侧草稿实时预览(可编辑)与时间线 / 进度展示。
### 2.2 明确不做(本期)
- 不改旧 `AIWritingPage` 及其 `AIWritingForm` / `InterventionCard`(这两个是要在**副本**里改的)。
- 不替换 `ai_writing` graph 四个阶段大脑逻辑,不动五个暂停点 / checkpoint / 持久化。
- 不引新状态管理库(继续 `useReducer` + `useStream`)。
### 2.3 复制策略(关键决策)
「复制一份」= **复制会被大改的表现层组件**(页面 + 干预卡 + 面板),**复用不改的引擎层**:
| 复制(新建副本,大改) | 复用(原样,不改;仅对 `types` 做**追加式**扩展) |
|------------------------|--------------------------------------------------|
| 新页面 `AIWritingChatPage.tsx` | `contexts/AIWritingContext.tsx`(状态机 + 流式) |
| 新面板 `WritingChatPanel.tsx`(消息流 + 底部输入框) | `hooks/useAIWritingStream.ts`(事件 → state) |
| 新消息卡 `chat-cards/*`(产物卡+确认按钮,**无输入框**) | `components/ai-writing/AIWritingDraftPanel.tsx`(右侧草稿预览) |
| 新意图客户端 `api/ai-writing-intent.ts` | 各流式展示子组件(`MaterialsCard`/`OutlineCard`/`ReviewCard`/`SkillStepsCard`…) |
| (可选)新 `contexts/WritingChatContext.tsx` 包一层「pending 意图 / 澄清态」 | `types/ai-writing.ts`(**只加字段不改旧字段**,保证旧页面不受影响) |
> **不复用、也不改** 的三个「输入框组件」:`AIWritingForm` / `InterventionCard` / `WritingFormContext`——
> 旧页面继续用它们,新页面完全不引用它们。这样「初始的不用动」得到硬保证。
>
> 为什么复用 `AIWritingContext` 而不复制它?它**不含任何输入框 UI**,只是「状态机 + 与后端 graph 的
> 流式桥」,复用它**不会改动旧页面的任何呈现**(旧页面照常在自己的 Provider 实例里跑)。复用能显著
> 降低维护成本、避免两套引擎漂移。若要绝对物理隔离,也可整目录复制 `AIWritingContext` +
> `useAIWritingStream` 到新目录——**推荐先复用**,仅当后续需要给对话态加大量图内新协议时再分叉。
---
## 3. 核心设计
### 3.1 三层结构
```
┌──────────────────────────────────────────────────────────────┐
│ 对话 UI 层(新) WritingChatPanel │
│ - 消息流:用户消息 + AI 阶段产物卡(含【保留的确认按钮】)+ 进度 │
│ - 底部:统一多行智能输入框(唯一自由输入入口) │
│ - 右侧:草稿实时预览(复用 AIWritingDraftPanel) │
└───────────────┬──────────────────────────────────────────────┘
│ 用户自然语言(+ 当前 status/pausePoint 上下文)
▼
┌──────────────────────────────────────────────────────────────┐
│ 意图解析层(新,核心) POST /api/ai-writing/sessions/{id}/intent │
│ 输出:{ intent, pausePoint, action, payload, │
│ needResearch, researchQuery, answer, confidence, clarify }│
└───────────────┬──────────────────────────────────────────────┘
│ 结构化 action + payload
▼
┌──────────────────────────────────────────────────────────────┐
│ 执行层(复用现有 AIWritingContext) │
│ idle → startWriting(AIWritingRequest) │
│ 暂停 → submitIntervention(UserInterventionPayload) │
│ 问答 → 旁路回答(不改写作状态) │
│ 素材不足 → 先重搜、缓存 pending、补完自动续跑 │
└──────────────────────────────────────────────────────────────┘
```
**设计原则**:意图解析层是「翻译官」——把自由文本翻译成**执行层已经懂的指令**。能复用
`action + payload` 的绝不新造协议;确认按钮点击**直接走执行层**(不经意图层,零延迟、零误判)。
### 3.2 意图分类体系(覆盖全流程 + 更多)
按「意图 → 触发时机(当前 status/pausePoint)→ 映射执行」组织,即意图解析引擎的目标 schema。
**「甚至更多」** = G(问答)、H(控制)、以及 B/C/D/E 里的「素材不足自动回退重搜」。
#### A. 启动类(status = `idle`)
| 意图 | 用户说法示例 | 映射 |
|------|-------------|------|
| `start_writing` | 「写一篇关于新能源出口的深度分析,2000 字」 | 解析成 `AIWritingRequest` → `startWriting()` |
| `start_with_outline` | 「按这个大纲写:…」 | `AIWritingRequest.userOutline` |
| `start_imitate` | 「仿照这篇样文写…」 | `writingModeType='imitate'` + `sampleText` |
#### B. 素材类(pausePoint = `material_confirm`)
| 意图 | 示例 | 映射 |
|------|------|------|
| `confirm_materials` | 「素材可以,继续 / 下一步」 | `{action:'confirm', approvedMaterialIds}` |
| `research_again` | 「重新搜索素材」 | `{action:'re_search'}`(无 userQuery=覆盖式重检) |
| `add_material`/`add_keyword` | 「再补点关于 X 的素材」「加个关键词 Y」 | `{action:'re_search', userQuery:'X/Y'}`(追加) |
| `select_materials` | 「只用第 1、3 条」 | `{action:'confirm', approvedMaterialIds:[…]}` |
| `drop_materials` | 「去掉第 2 条」 | 同上(反选后的子集) |
#### C. 大纲类(pausePoint = `outline_confirm`)
| 意图 | 示例 | 映射 |
|------|------|------|
| `confirm_outline` | 「大纲 OK,开始写」 | `{action:'confirm'}` |
| `reoutline` | 「重写大纲」 | `{action:'re_outline'}`(feedback 可空) |
| `revise_outline` | 「第二章拆成两节」「调整顺序」 | `{action:'re_outline', outlineFeedback:自然语言}` |
#### D. 写作 / 草稿类(pausePoint = `draft_confirm`)
| 意图 | 示例 | 映射 |
|------|------|------|
| `to_editor` | 「交给编辑审核」 | `{action:'to_editor'}` |
| `finalize` | 「就这样定稿」 | `{action:'finalize'}` **(高代价:低置信必澄清)** |
| `revise_draft` | 「第三段太啰嗦,精简」「补个 2024 案例」 | `{action:'user_revise', userRevisionNotes:意见}` |
| `rewrite_section` | 「重写第二章」 | `user_revise` + 章节定位写进 notes |
| `change_tone/style` | 「更口语一点 / 更正式」 | `user_revise` + notes |
| `apply_review`(若已有审核结果) | 「按编辑意见改」「只接受第 1、2 条」 | `{action:'user_revise', selectedIssueIndices:[…]}` |
#### E. 编辑审核类(pausePoint = `review_confirm`)
| 意图 | 示例 | 映射 |
|------|------|------|
| `accept_review` | 「按编辑意见改」 | `{action:'accept_review', selectedIssueIndices}` |
| `partial_accept_review` | 「只接受第 1、2 条」 | `accept_review` + `selectedIssueIndices` 子集 |
| `add_review_opinion` | 「再补一条:核查数据准确性」 | `accept_review` + `userRevisionNotes`(并入补充指令,现协议已支持) |
| `force_finalize` | 「不改了,强制定稿」 | `{action:'force_finalize', overrideVerdict:true}` **(高代价:必澄清)** |
#### F. 素材不足求助类(pausePoint = `section_help`)
| 意图 | 映射 |
|------|------|
| `section_supplement` | `{action:'section_help', sectionDecisions:[…'supplement']}` |
| `section_loose` | `sectionDecisions:[…'loose']` |
| `section_delete` | `sectionDecisions:[…'delete']` |
#### G. 问答类(**新**;任意 status;旁路,不改写作状态)
| 意图 | 示例 | 映射 |
|------|------|------|
| `ask_about_content` | 「第二章讲了啥?」「为什么用这个论点?」 | 意图节点直接带回 `answer`,对话流渲染(见 5.4) |
| `ask_meta` | 「现在到哪步了?」「用了哪些素材?」 | 读当前 state 直接回答,不走图 |
#### H. 控制类(**新 / 半新**)
| 意图 | 映射 |
|------|------|
| `next_step`(通用「继续/下一步」) | 按**当前 pausePoint** 映射到该阶段的默认确认(material/outline→`confirm`,draft→`to_editor`…;等价点确认按钮) |
| `restart_stage` | 重跑当前阶段(复用 `resumeWriting()` 空 command 续跑) |
| `run_background` | 转后台自动跑:`suspendAIWritingToBackground()` |
| `cancel` | `pauseWriting()`(`stream.stop()`) |
| `unknown/clarify` | 无法判定 → 反问澄清 / 用户协助(**防误触发硬指标**) |
### 3.3 「素材不足自动重搜」判定与回退(用户强调点)
触发:用户在**大纲/草稿/审核**阶段提出修改意见,但意见需要的信息现有素材里没有。
判定来源(组合):
1. **意图节点直接判**:把「素材摘要(keywords + 每条 title/source)」喂给意图 prompt,让它判断
「这条意见能否用现有素材满足」;不能 → `needResearch=true` + `researchQuery`。
2. **执行阶段兜底**:`writer_draft` 严格模式已有 `blocked_sections` → `section_help` 暂停点,
对话层把它翻译成「素材不足,是否重搜?」的追问。
回退动作(**首期纯前端实现,不动后端协议**):`needResearch=true` 时,前端把用户的修改
`payload` 缓存为 `pendingIntent`,先 `submitIntervention({action:'re_search', userQuery:researchQuery})`
补素材;监听到新一轮 `material_confirm`(素材追加完成)后,**自动提交** `pendingIntent`。
需处理「重搜期间用户又改主意」→ 允许覆盖 pending。
---
## 4. 意图解析引擎:方案与契约
**采用方案 A:后端 LLM 意图节点(独立 REST)**,叠加高频短句**规则快路径**优化。
理由:`needResearch` 判定需要**完整写作上下文**(素材/大纲/草稿),后端天然持有 checkpoint state;
与 `setup_writing` 一脉相承,离线模型选择策略统一;输出 schema 稳定、前端只消费。
### 4.1 后端入口
`app/gateway/routers/ai_writing.py` 新增:
```
POST /api/ai-writing/sessions/{session_id}/intent
Body: { text: string, status: string, pausePoint?: string }
Resp: IntentResult(见 4.3)
```
实现:读该 session 的 checkpoint state(素材摘要 / 大纲纲要 / 草稿字数·章节 / 审核意见)→
用 `ai_writing` 配置模型(复用 `intent_parser.py` 同套 `create_chat_model` + `parse_json` +
模型容错 `_model_fallback`)解析 → 返回结构化契约。**不改图拓扑**。
新增 prompt:`agents/ai_writing/prompts/intent_router_prompts.py`
- 输入:用户文本、`current_status`/`pause_point`、素材摘要、大纲纲要、草稿字数+章节标题、审核意见列表。
- 输出:严格 JSON(4.3 契约)。硬要求:**只在证据充分时给高 confidence,否则给 `clarify`**;
`finalize`/`force_finalize`/`re_search` 等高代价意图**必须**高置信才不澄清。
### 4.2 前端快路径(先跑规则,未命中再调后端)
`api/ai-writing-intent.ts` 里对**当前 pausePoint** 做少量确定性短语命中,直接产出 payload,省一次往返:
- 「继续/下一步/确认/可以/ok/好的」→ 当前阶段默认 `confirm`(draft 阶段=`to_editor`)。
- 「重新搜索/重搜」(material)→ `re_search`。「重写大纲」(outline)→ `re_outline`。
- 「定稿/强制定稿」→ **不进快路径**(高代价,一律走后端 + 可能澄清)。
- 命中即执行;未命中 → 调后端 `intent` 接口。
### 4.3 意图解析输出契约
```jsonc
{
"intent": "revise_draft", // 见 3.2 分类
"pausePoint": "draft_confirm", // 需要 resume 时;否则 null
"action": "user_revise", // 映射到现有 action
"payload": { // 直接可喂 submitIntervention 的字段(camelCase)
"userRevisionNotes": "第三段精简,并补一个 2024 年的案例"
},
"needResearch": true, // 素材是否不足
"researchQuery": "2024 行业案例", // needResearch 时的检索意图
"answer": null, // 问答类意图的直接回答(旁路)
"confidence": 0.86,
"clarify": null // 低置信时的反问话术(非空则不执行、只追问)
}
```
前端消费顺序:
1. `answer` 非空 → 对话流渲染回答(问答旁路,不动写作状态)。
2. `clarify` 非空 **或** `confidence < 阈值(建议 0.6)` → 渲染反问气泡,等用户澄清(**防误触发**)。
3. `needResearch=true` → 缓存 `pendingIntent=payload`,先重搜,收到新素材后自动提交。
4. 否则 → `idle` 走 `startWriting(...)`;暂停态走 `submitIntervention({action, ...payload})`。
---
## 5. 前端改造方案
### 5.1 新页面与路由(复制式)
- 新页面:`open-canvas/pages/AIWritingChatPage.tsx`
(骨架照抄 `AIWritingPage.tsx` 的 Provider 包裹与左右分栏;左栏换成 `WritingChatPanel`,右栏仍
`AIWritingDraftPanel`;idle 不再挂 `AIWritingForm`,改由底部输入框启动)。
- 路由注册(**新增,不动旧行**):
- `open-canvas/routes/OpenCanvasRoutes.tsx` 加 `path="ai-writing-chat/*"`。
- 侧边栏 `core/page-layout/sidebar-menu.ts`:在 `qa-research` 旁新增
`{ id:"qa-research-chat", label:"AI写作(对话版)", icon:FileText, path:"/page/canvas/ai-writing-chat" }`。
(或先不加、内部灰度直链验证。)
### 5.2 新面板 `WritingChatPanel.tsx`(左栏主体)
职责 = 消息流容器 + 底部输入框,替代 `AIWritingPanel` 里的「时间线 + InterventionCard + 状态栏」。
复用 `useAIWriting()` 读 state / 调 `startWriting` / `submitIntervention` / `pauseWriting` /
`suspendAIWritingToBackground`。
消息流渲染(把 `ProgressEvent` / 阶段产物适配成消息卡,复用现有展示子组件):
- 运行中:`research_progress`/`outline_chunk`/`draft_chunk`/`review_chunk` → 进度 / 流式卡
(复用 `SkillStepsCard`/`OutlineStreamingView`/`DraftGeneratingView`/`ReviewStreamingView` 的展示部分)。
- 到达暂停点:渲染 **产物卡 + 保留的确认按钮**(见 5.3),不再有输入框。
- 用户消息、AI 澄清追问、问答回答:普通气泡。
底部输入框:直接复用 `@/components/ai-elements/prompt-input`(`PromptInput`+`PromptInputTextarea`+
`PromptInputSubmit`),Enter 发送、Shift+Enter 换行;发送时读当前 `state.status`/`currentPause?.pausePoint`
交给意图路由(见 5.5)。运行中(非暂停点)可禁用发送或转「排队/提示当前在生成」。
### 5.3 新消息卡 `chat-cards/*`(**保留按钮、去掉输入框**)
把 `InterventionCard.tsx` 的五张卡各复制一份到 `open-canvas/components/ai-writing-chat/chat-cards/`,
**删掉其中的 textarea / MaterialsToolbar 输入部分,保留产物展示 + 确认/动作按钮**:
| 新卡 | 展示(复用) | 保留的按钮 | 删除的输入 |
|------|-------------|-----------|-----------|
| `MaterialConfirmChatCard` | `MaterialsCard`(勾选保留)+ `SkillStepsCard` | 「确认素材,规划大纲」「重新搜索」 | `MaterialsToolbar` 的 userQuery 输入框 |
| `OutlineConfirmChatCard` | `OutlineCard`(可点标题微调保留) | 「确认大纲,开始写作」 | feedback textarea + 发送 |
| `DraftConfirmChatCard` | `ReviewCard`(若有审核,勾选保留) | 「按编辑审核意见修改」「让编辑继续审核」「确认定稿」 | 「修改意见」textarea |
| `ReviewConfirmChatCard` | `ReviewCard`(勾选保留) | 「按编辑意见修改」「强制定稿」 | 「补充修改指令」textarea |
| `SectionHelpChatCard` | 章节 + 三选一按钮组 | 三选一 + 「提交并继续」 | (本就无自由输入,原样保留) |
> 勾选态(素材/审核意见)**保留**——它是「点按钮」路径的一部分,不算自由输入框。用户想用自然语言
> 「只用第 1、3 条」也能走底部输入框(意图层产出 `approvedMaterialIds`/`selectedIssueIndices`)。
> 按钮点击 **直接调 `submitIntervention`**,与旧卡完全一致,零改动风险。
### 5.4 问答旁路(新能力)
「对写作内容提问」不改写作 state。首期**轻量实现**:意图节点检出 `ask_*` 时读 state 直接带回 `answer`,
前端在消息流渲染为普通 AI 气泡即可(一次调用出答案)。完整版(后续)可另起只读 lead_agent 线程注入
草稿/大纲作背景,类似 `WritingSetupChat` 的 `useThreadStream`。
### 5.5 交互流程(对话版)串起来
1. **空闲**:底部输入「写一篇关于 X 的深度分析,2000 字」→ 意图 `start_writing` → 组 `AIWritingRequest`
→ `startWriting()`。(缺关键字段时先 `clarify` 追问字数/读者/类型。)
2. **运行中**:进度/流式卡渲染。
3. **到暂停点**:渲染产物卡 + 按钮 + 一句提示「你可以点上面的按钮,或直接说:确认继续 / 补充素材 / 改大纲…」。
- **点按钮** → 直接 `submitIntervention`(旧逻辑)。
- **发文字** → `resolveIntent(text, status, pausePoint)`(先快路径、后后端)→ 按 4.3 消费。
4. **问答**:任意时刻提问 → 旁路 `answer`,不打断写作。
5. **素材不足**:`needResearch` 或 `section_help` → 追问/执行「重搜 → 自动续跑 pending 意图」。
6. **拿不准**:`clarify`/低置信 → 反问气泡,不执行。
### 5.6 pending 意图 & 澄清态(新增本地状态)
在 `WritingChatPanel`(或新 `WritingChatContext`)维护:
- `pendingIntent: UserInterventionPayload | null` —— 素材不足重搜完成后自动提交。
- `awaitingClarify: { originalText: string } | null` —— 澄清态,用户下一句与上一句拼接再解析。
- pending 生命周期:设置 → 提交 `re_search` → 监听 reducer 里新一轮 `awaiting_material_confirm`
(`state.status` 从 `researching` 回到 `awaiting_material_confirm`)→ 自动 `submitIntervention(pending)` → 清空。
---
## 6. 后端改造方案
### 6.1 意图解析入口(新增,唯一后端改动主体)
- `app/gateway/routers/ai_writing.py`:新增 `POST /sessions/{id}/intent`(见 4.1)。
读 checkpoint state 用 `app.state.checkpointer` + `make_ai_writing_graph().aget_state(...)`
(参照 `ai_writing_job_executor.py` 的读法)取素材/大纲/草稿/审核摘要。
- `agents/ai_writing/prompts/intent_router_prompts.py`:新增意图 prompt(见 4.1)。
- 复用 `nodes/intent_parser.py` 的模型创建 / `parse_json` / `_model_fallback` 容错。
### 6.2 不需要改的部分
- 四个阶段大脑节点、五个暂停点、checkpoint、StreamBridge、持久化、sessions/transcript REST——**全不动**。
- 「素材不足自动续跑」「问答旁路」首期都在**前端 / 意图节点**实现,**不扩 resume 协议**。
### 6.3 可选增强(阶段 3,按需)
- `add_review_opinion` 完整版:`review_confirm` resume 增字段 `extra_review_notes`(首期已用现有
`userRevisionNotes` 降级承载,够用)。
- `resume_intent` 后端化(把「重搜完自动续跑」搬到后端,减少前端状态机复杂度)。
- undo / 回退到上一暂停点(依赖 checkpoint 回滚)。
---
## 7. 数据流时序(改稿 + 素材不足自动重搜)
```
用户(draft_confirm 暂停): "第三段补个2024年的行业案例"
│ 底部输入框 → resolveIntent(text, status='awaiting_draft_confirm', pausePoint='draft_confirm')
▼ POST /api/ai-writing/sessions/{id}/intent { text, status, pausePoint }
后端: 读 state(素材摘要) + LLM 解析
│ 返回 { intent:revise_draft, action:user_revise,
│ payload:{userRevisionNotes:"第三段…补2024案例"},
│ needResearch:true, researchQuery:"2024 行业案例", confidence:0.9 }
▼
前端: needResearch=true → pendingIntent = payload
│ submitIntervention({action:'re_search', userQuery:"2024 行业案例"})
▼ LangGraph resume → researcher 追加素材 → status 回到 awaiting_material_confirm
前端: 监听到新一轮素材确认 → 自动提交 pendingIntent
│ submitIntervention({action:'user_revise', userRevisionNotes:"第三段…补2024案例"})
▼ writer_draft 用新素材改稿 → draft_chunk… → draft_ready → draft_confirm
前端: 消息流展示新草稿卡 + "已按你的意见改写并补充了 2024 案例"
```
---
## 8. 关键实现细节与坑
1. **阶段作用域**:`resolveIntent` 必须带 `state.currentPause?.pausePoint` + `state.status`——
同一句「重新来」在 material 是重搜、在 outline 是重写大纲。`AIWritingContext` 已有这两个字段,直接读。
2. **确认按钮零改动**:新卡的按钮 `onClick` 原样调 `submitIntervention(payload)`,与旧卡逐字一致,
风险最低;**只删输入框**。
3. **防误触发**:低置信(<0.6)或 `clarify` 非空一律追问;`finalize`/`force_finalize`/`re_search`
即使命中也要求高置信,否则澄清。绝不盲目定稿/覆盖重搜。
4. **pending 意图队列**:素材不足是「先插重搜、再回原意图」,用 5.6 的小状态机;重搜完成前允许覆盖 pending。
5. **问答不污染 transcript**:问答/澄清消息在前端标记 `kind:'qa'|'clarify'`,与写作进度事件区分(
`AIWritingContext` 的 transcript 只存 progressEvents/completedInterventions,问答气泡是纯前端消息,
默认不入 transcript,避免历史回看错乱)。
6. **流式产物→消息卡**:复用 `OutlineStreamingView`/`DraftGeneratingView`/`ReviewStreamingView`/
`SkillStepsCard` 的**展示部分**,套进消息容器,不重写渲染。
7. **右侧草稿预览**:`AIWritingDraftPanel` + `DraftSync` 原样复用(`DraftSync` 依据 status/markdown
同步右侧画布)。
8. **离线内网**:意图模型走 `config.yaml → ai_writing` 配置模型;检索仍走技能/内网 ES,不联网。
9. **模型选择透传**:`submitIntervention` 的 resume 已把 `state.request?.modelName` 一并回传(见
`AIWritingContext`),新页面沿用即可。
10. **旧页面隔离验证**:改完后回归旧 `/page/canvas/ai-writing` 与 `/page/workspace/ai-writing`、
笔记本嵌入页,确认零变化(新代码不 import 旧的 `AIWritingForm`/`InterventionCard`/`WritingFormContext`)。
---
## 9. 分阶段实施路线图
**阶段 0 — 骨架(不含意图)**
- 复制 `AIWritingChatPage` + 注册新路由 + 侧边栏入口;复用 `AIWritingContext`/`useStream`/`DraftPanel`。
- 复制五张 `chat-cards`(删输入框、留按钮);底部输入框先只做 idle 直发 `start_writing`
(文本原样当 `userIntent`,其余字段取默认)+ 暂停点靠按钮推进。端到端跑通。
**阶段 1 — 意图解析(核心)**
- 后端 `POST /sessions/{id}/intent` + `intent_router_prompts.py`,返回 4.3 契约。
- 前端 `api/ai-writing-intent.ts`(快路径 + 后端)接入 `WritingChatPanel`,覆盖 A~F 类意图。
- 低置信 `clarify` 澄清态(5.6)。
**阶段 2 — 问答旁路 + 素材不足自动重搜**
- G 类问答(意图节点直接带 `answer`)。
- `needResearch` + pending 意图机制(前端实现重搜后自动续跑,5.6)。
**阶段 3 — 增强(按需)**
- `add_review_opinion` 扩协议、`resume_intent` 后端化、undo/回退、完整问答只读线程。
**阶段 4 — 打磨**
- 消息卡视觉、历史回看(transcript)、后台挂起入口、灰度到侧边栏。
---
## 10. 关键文件清单(落地对照)
### 新增(前端)
- `open-canvas/pages/AIWritingChatPage.tsx` — 新页面(复制 `AIWritingPage` 骨架改造)
- `open-canvas/components/ai-writing-chat/WritingChatPanel.tsx` — 消息流 + 底部输入框
- `open-canvas/components/ai-writing-chat/chat-cards/{MaterialConfirmChatCard,OutlineConfirmChatCard,DraftConfirmChatCard,ReviewConfirmChatCard,SectionHelpChatCard}.tsx` — 保留按钮、去掉输入框
- `open-canvas/components/ai-writing-chat/progressToMessage.ts` — `ProgressEvent` → 消息卡适配
- `open-canvas/api/ai-writing-intent.ts` — 意图解析客户端(快路径 + `POST …/intent`)
- (可选)`open-canvas/contexts/WritingChatContext.tsx` — pending 意图 / 澄清态
### 修改(前端,仅追加,不动旧逻辑)
- `open-canvas/routes/OpenCanvasRoutes.tsx` — 加一条 `ai-writing-chat/*` 路由
- `core/page-layout/sidebar-menu.ts` — 加一个侧边栏入口(可选)
- `open-canvas/types/ai-writing.ts` — **仅追加** `IntentResult` 等新类型,不改旧字段
### 新增(后端)
- `app/gateway/routers/ai_writing.py` — 加 `POST /sessions/{id}/intent`
- `agents/ai_writing/prompts/intent_router_prompts.py` — 意图 prompt
- `tests/test_ai_writing_intent.py` — 意图接口单测(TDD 强制)
### 复用(不改)
- `open-canvas/contexts/AIWritingContext.tsx`、`hooks/useAIWritingStream.ts`
- `open-canvas/components/ai-writing/{AIWritingDraftPanel,DraftSync,MaterialsCard,OutlineCard,ReviewCard,SkillStepsCard,OutlineStreamingView,DraftGeneratingView,ReviewStreamingView}.tsx`
- `agents/ai_writing/graph.py`、四个阶段节点、五个暂停点、`nodes/intent_parser.py`
### 绝不动(保证「初始的不用动」)
- `open-canvas/pages/AIWritingPage.tsx` 及其引用的
`components/ai-writing/{AIWritingForm,InterventionCard}.tsx`、`contexts/WritingFormContext.tsx`
- 旧路由 `/page/canvas/ai-writing`、`/page/workspace/ai-writing`、笔记本嵌入页
---
## 11. 待确认(默认取值已给出,可直接推进)
1. 意图引擎:**后端 LLM 节点(方案 A)+ 前端短句快路径** —— 默认采用。
2. `AIWritingContext` / `useAIWritingStream`:**复用**(不物理复制)——默认采用;需绝对隔离可改为复制。
3. 新页面入口:**侧边栏新增「AI写作(对话版)」** —— 默认加;若要先灰度可只留直链。
4. 「素材不足自动重搜」:**首期前端 pending 续跑**(不动后端 resume 协议)——默认采用。
5. 问答旁路:**首期意图节点直接带 `answer`** —— 默认采用。
> 以上默认按「A + 复用引擎 + 侧边栏入口 + 前端 pending + 意图节点带答案」推进,如需调整请指出。
@@ -0,0 +1,454 @@
# AI 写作「对话驱动 + 意图解析」改造方案
> 目标:在**不动现有 AI 写作页面**的前提下,新建一个「对话驱动」的写作工作台。
> 去掉素材收集 / 大纲规划 / 作家写作 / 编辑审核各阶段**分散的输入框/表单**,
> 用**底部一个统一的智能输入框**接管全流程交互;该输入框具备**强意图解析**能力,
> 能准确识别用户意图(重新搜索、加素材、加关键词、重写大纲、按要求改大纲、改写作、
> 改编辑、提编辑意见、对写作内容提问……),并在**素材不足**时自动回退去重新检索素材。
本文只做**方案与实现指引**,不含代码改动。落地时按第 9 节的路线图分阶段推进。
---
## 1. 现状梳理(改造的地基)
现有 AI 写作是一套**独立的 LangGraph 子系统**(不是 lead_agent),前后端都已成型。
### 1.1 后端:`ai_writing` graph
- 图工厂:`offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/graph.py`
- 状态:`.../agents/ai_writing/state.py`(`AIWritingState` + `UserInterventionPayload`)
- 注册:`offline-backend-20260512/backend/langgraph.json` → `assistant_id = "ai_writing"`
- 执行:与 lead agent **共用** RunManager + StreamBridge + checkpointer(`app/gateway/services.py` 里 `assistant_id=="ai_writing"` 分发到 `make_ai_writing_graph`)
节点拓扑(实际):
```
sample_analyzer → intent_parser → researcher
→ [cond] pause_material | writer_draft
→ writer_outline → pause_outline
→ writer_draft → [cond] pause_section_help | pause_draft | END
→ editor → [cond] pause_draft | pause_review
→ pause_review → END | writer_draft
```
四个「阶段大脑」节点:
| 阶段 | 节点 | 文件 |
|------|------|------|
| 素材收集 | `researcher` | `nodes/researcher.py` |
| 大纲规划 | `writer_outline` | `nodes/writer_outline.py` |
| 作家写作 | `writer_draft` | `nodes/writer_draft.py` |
| 编辑审核 | `editor` | `nodes/editor.py` |
五个**暂停点**(`interrupt()`,靠 `Command(resume=payload)` 续跑):
| pause_point | 语义 | 可用 action(现有) |
|-------------|------|---------------------|
| `material_confirm` | 素材确认 | `confirm` / `re_search` / `supplement_search` / `enable_skill_search` |
| `outline_confirm` | 大纲确认 | `confirm` / `re_outline` |
| `draft_confirm` | 草稿确认 | `to_editor` / `finalize` / `user_revise` / `custom_revise` |
| `review_confirm` | 编辑打回确认 | `accept_review` / `force_finalize` |
| `section_help` | 素材不足求助 | `supplement` / `loose` / `delete` |
> resume 载荷字段(`UserInterventionPayload`):`action`、`approved_material_ids`、
> `extra_keywords`、`user_query`(自然语言补充检索)、`edited_outline`、`outline_feedback`、
> `user_revision_notes`、`override_verdict`、`selected_issue_indices`、`section_decisions`。
> **关键点**:现有协议已经支持「自然语言 + 结构化 action」,这是对话改造的核心可复用资产。
REST(业务元数据,非图执行):`app/gateway/routers/ai_writing.py`,前缀 `/api/ai-writing`
(sessions CRUD、transcript 保存、article-types、后台挂起 `/background` + `/progress` 等)。
持久化:`ai_writing_sessions` 表(`transcript` 为 `PortableLongText`)
+ `ai_writing_article_types`(`deerflow/persistence/...`)。
配置:`config.yaml → ai_writing`(`keyword_model`、`rank_model`、`max_materials`、
`use_builtin_agents.{researcher,outliner,writer,editor}` 等)。
### 1.2 前端:`open-canvas` 子系统
- 主页面:`frontend-web/src/open-canvas/pages/AIWritingPage.tsx`
- 路由:`/page/canvas/ai-writing`、`/page/workspace/ai-writing`
- 状态机:`contexts/AIWritingContext.tsx`(`useReducer` + LangGraph `useStream`)
- 事件翻译/reducer:`hooks/useAIWritingStream.ts`
- 类型:`types/ai-writing.ts`(`WritingStatus`、`ProgressEvent`、`UserInterventionPayload`、`AIWritingRequest`…)
- 流式:LangGraph `useStream`,`assistantId: 'ai_writing'`,`streamMode: ['values','updates','custom','messages-tuple']`
- **要去掉的输入 UI**(改造重点):
- 启动表单 `components/ai-writing/AIWritingForm.tsx` + `WritingFormContext`
- 阶段暂停输入 `components/ai-writing/InterventionCard.tsx`(素材/大纲/草稿/审核四套内嵌表单)
- **已存在的对话原型**:`components/ai-writing/WritingSetupChat.tsx`
(用 `useThreadStream` 起一条 lead_agent 线程 + `setup_writing` 工具审批卡回填表单)——
这是「对话 → 结构化配置」的现成范式,可借鉴但**不直接复用**(它只管启动前配置)。
### 1.3 结论
- **状态机、流式、暂停/续跑协议、持久化都可复用**,无需重写引擎。
- 改造本质 = **把「分散表单」换成「一个对话输入框 + 意图路由层」**,
再把意图映射回**现有的 `action + payload`**(大部分场景),
少量新意图(如「对内容提问」「素材不足自动重搜」)需要**新增协议/节点**。
---
## 2. 目标与范围
### 2.1 必须满足
1. 新页面/新路由,**旧页面零改动**(用户要求「初始的不用动」)。
2. 底部**单一对话输入框**控制所有阶段交互,移除各阶段独立输入框。
3. **强意图解析**:准确识别用户自然语言意图并路由到正确动作,覆盖全流程 + 更多。
4. 用户给修改意见后,**若现有素材不足以满足要求 → 自动/建议重新检索素材**再继续。
5. 保留右侧草稿实时预览(可编辑)与时间线/进度展示。
### 2.2 明确不做(本期)
- 不改旧 `AIWritingPage` 及其表单/干预卡组件。
- 不替换底层 `ai_writing` graph 的四个阶段大脑逻辑(只加意图层与少量新协议)。
- 不引入新的状态管理库(继续 `useReducer` + `useStream`)。
---
## 3. 核心设计
### 3.1 总体思路:三层结构
```
┌─────────────────────────────────────────────────────────┐
│ 对话 UI 层(新) │
│ - 左:消息流(用户消息 + AI 阶段产物卡片 + 进度) │
│ - 底:统一智能输入框(唯一交互入口) │
│ - 右:草稿实时预览(复用 AIWritingDraftPanel) │
└───────────────┬─────────────────────────────────────────┘
│ 用户自然语言
▼
┌─────────────────────────────────────────────────────────┐
│ 意图解析层(新,核心) │
│ - 输入:用户文本 + 当前 WritingStatus + 上下文快照 │
│ - 输出:{ intent, action, payload, needResearch, ... } │
└───────────────┬─────────────────────────────────────────┘
│ 结构化 action + payload
▼
┌─────────────────────────────────────────────────────────┐
│ 执行层(复用现有) │
│ - 空闲态:startWriting(AIWritingRequest) │
│ - 暂停态:submitIntervention(UserInterventionPayload) │
│ - 运行中:stream.stop() → 续跑 / 新命令 │
│ - 提问态:旁路问答(不改写作状态) │
└─────────────────────────────────────────────────────────┘
```
**设计原则**:意图解析层是「翻译官」——把自由文本翻译成**现有执行层已经懂的指令**。
能复用 `action + payload` 的绝不新造协议;只有现有协议表达不了的才扩展。
### 3.2 意图分类体系(覆盖用户全流程 + 更多)
按「意图类别 → 触发时机(当前 status)→ 映射到的执行」组织。这是**意图解析引擎的目标 schema**。
#### A. 启动类(status = idle)
| 意图 | 说明 | 映射 |
|------|------|------|
| `start_writing` | 用户描述要写什么 | 解析成 `AIWritingRequest` → `startWriting()` |
| `start_with_outline` | 用户自带大纲 | `AIWritingRequest.userOutline` |
| `start_imitate` | 样文仿写 | `writingModeType=imitate` + 样文 |
#### B. 素材类(status ≈ awaiting_material_confirm / 运行中)
| 意图 | 用户说法示例 | 映射 |
|------|-------------|------|
| `confirm_materials` | 「素材可以,继续」 | `material_confirm` + `confirm`(可带勾选 ids) |
| `research_again` | 「重新搜索素材」 | `material_confirm` + `re_search` |
| `add_material` / `add_keyword` | 「补充关于 X 的素材」「加个关键词 Y」 | `material_confirm` + `supplement_search`,`user_query`=X/Y |
| `use_skill_search` | 「用 XX 技能查」 | `material_confirm` + `enable_skill_search`,`user_query`=技能/来源 |
| `select_materials` | 「只用第 1、3 条」 | `confirm` + `approved_material_ids` 子集 |
| `drop_materials` | 「去掉第 2 条」 | 同上,反选后 `approved_material_ids` |
#### C. 大纲类(status ≈ awaiting_outline_confirm)
| 意图 | 示例 | 映射 |
|------|------|------|
| `confirm_outline` | 「大纲 OK」 | `outline_confirm` + `confirm` |
| `reoutline` | 「重写大纲」 | `outline_confirm` + `re_outline`(`outline_feedback` 可空) |
| `revise_outline` | 「第二章拆成两节」「调整顺序」 | `re_outline` + `outline_feedback`=自然语言 |
| `edit_outline_manually` | 用户直接给出新大纲文本 | 解析成 `edited_outline` |
#### D. 写作/草稿类(status ≈ awaiting_draft_confirm / writing_draft)
| 意图 | 示例 | 映射 |
|------|------|------|
| `to_editor` | 「交给编辑审核」 | `draft_confirm` + `to_editor` |
| `finalize` | 「就这样定稿」 | `draft_confirm` + `finalize` |
| `revise_draft` | 「第三段太啰嗦,精简」「补个案例」 | `draft_confirm` + `user_revise`,`user_revision_notes`=意见 |
| `rewrite_section` | 「重写第二章」 | `user_revise` + 定位信息(章节)写进 notes |
| `change_tone/style` | 「更口语一点」 | `user_revise` + notes |
#### E. 编辑审核类(status ≈ awaiting_review_confirm)
| 意图 | 示例 | 映射 |
|------|------|------|
| `accept_review` | 「按编辑意见改」 | `review_confirm` + `accept_review`(可带 `selected_issue_indices`) |
| `partial_accept_review` | 「只接受第 1、2 条意见」 | `accept_review` + `selected_issue_indices` |
| `force_finalize` | 「不改了,强制定稿」 | `review_confirm` + `force_finalize` |
| `add_review_opinion` | 「你再补一条:检查数据准确性」 | 追加自定义审核意见(见 5.3 扩展) |
#### F. 素材不足求助类(status = awaiting_section_help)
| 意图 | 映射 |
|------|------|
| `section_supplement` | `section_help` + `supplement`(去重搜) |
| `section_loose` | `section_help` + `loose`(放宽续写) |
| `section_delete` | `section_help` + `delete` |
#### G. 问答类(**新**,任意 status,旁路,不改写作状态)
| 意图 | 示例 | 映射 |
|------|------|------|
| `ask_about_content` | 「第二章讲了啥?」「为什么用这个论点?」 | 旁路问答(见 5.4) |
| `ask_meta` | 「现在进行到哪一步?」「用了哪些素材?」 | 读当前 state 直接回答,不走图 |
#### H. 控制类(**新/半新**)
| 意图 | 映射 |
|------|------|
| `undo/back` | 回退到上一暂停点(依赖 checkpoint 回滚,见 8.4) |
| `restart_stage` | 重跑当前阶段 |
| `run_background` | 挂起后台自动跑 → `POST /sessions/{id}/background` |
| `cancel` | `stream.stop()` |
| `unknown/clarify` | 无法判定 → 反问澄清(对话追问,不误触发) |
> **覆盖「甚至更多」**:G(问答)、H(控制)、以及 D/E 里「素材不足自动回退重搜」都是超出原表单能力的新增。
### 3.3 「素材不足自动重搜」的判定与回退(用户强调的重点)
触发场景:用户在**大纲/草稿/审核**阶段提出修改意见,但意见需要的**信息现有素材里没有**。
判定放在**意图解析层**输出一个 `needResearch: boolean` + `researchQuery`,判定来源二选一(推荐组合):
1. **意图解析 LLM 直接判断**:把「当前素材摘要(keywords + 每条 title/来源)」喂给意图解析
prompt,让它判断「用户这条意见能否用现有素材满足」。不能 → `needResearch=true` 并给出检索词。
2. **执行阶段兜底**:`writer_draft` 严格模式本就有 `blocked_sections` → `pause_section_help`,
这是**已有的**素材不足闸门;对话层把它翻译成「素材不足,是否重新检索?」的对话追问。
回退动作:`needResearch=true` 时,先走 `material_confirm + supplement_search`(或新的
「带目标的重搜」协议,见 8.3)补素材,**补完自动继续**用户原本的修改意图(需要在对话层
记住「pending 修改意图」,重搜结束后再提交)。
---
## 4. 意图解析引擎:方案对比与推荐
| 方案 | 做法 | 优点 | 缺点 |
|------|------|------|------|
| **A. 后端 LLM 意图节点**(推荐) | 新增 `/api/ai-writing/intent/parse`(或图内新节点),后端用 LLM 把 `{text, status, contextSnapshot}` → 结构化意图 JSON | 能吃到完整 state(素材/大纲/草稿);可判 `needResearch`;prompt/模型集中可控;离线内网可用配置模型 | 多一次网络往返(可接受,交互本就等待) |
| B. 前端调模型接口分类 | 前端直接调现有 `/api/models` 聊天做分类 | 少一层后端改动 | 前端难拿全后端 state 快照;prompt 分散;鉴权/模型选择复杂 |
| C. 规则 + LLM 混合 | 常见短句走关键词规则快路径,兜底 LLM | 快、省 token | 规则易漏、维护成本高;中文表达多样命中率低 |
**推荐 A(后端 LLM 意图节点)**,并在其上**叠加 C 的快路径**做优化(如「确认/继续/定稿」等
高频短句先走轻量规则,未命中再调 LLM)。理由:
- 意图判定需要**当前写作上下文**(尤其 `needResearch` 判定),后端天然持有 state/checkpoint。
- 与现有「配置助手 `setup_writing`」一脉相承,模型选择/离线部署策略统一。
- 输出 schema 稳定,前端只消费结构化结果,UI 简单。
### 4.1 意图解析输出契约(建议)
```jsonc
{
"intent": "revise_draft", // 见第 3.2 分类
"pausePoint": "draft_confirm", // 若需 resume;否则 null
"action": "user_revise", // 映射到现有 action;新意图可为新值
"payload": { // 直接可喂 submitIntervention 的字段
"userRevisionNotes": "第三段精简,并补一个 2024 年的案例"
},
"needResearch": true, // 素材是否不足
"researchQuery": "2024 行业案例", // needResearch 时的检索意图
"answer": null, // 问答类意图的直接回答(旁路)
"confidence": 0.86,
"clarify": null // 低置信时的反问话术
}
```
前端拿到后:
- `answer` 非空 → 直接在对话流渲染回答(问答旁路,不动写作状态)。
- `clarify` 非空或 `confidence` 低 → 渲染反问,等用户澄清(**防误触发**)。
- `needResearch=true` → 先重搜,缓存 `payload` 为 pending,重搜完再提交。
- 否则 → `submitIntervention({action, ...payload})` 或 `startWriting(...)`。
---
## 5. 前端改造方案
### 5.1 新页面与路由
- 新目录:`frontend-web/src/open-canvas/pages/AIWritingChatPage.tsx`(或 `conversational/` 子目录)。
- 新路由:`/page/canvas/ai-writing-chat`(在 `OpenCanvasRoutes.tsx` 注册;旧路由保留)。
- 侧边栏:`sidebar-menu.ts` 加一个入口(如「AI 写作(对话版)」),或先不加、内部灰度。
### 5.2 复用 vs 新建
| 复用(尽量不改) | 新建 |
|------------------|------|
| `AIWritingContext` / `useAIWritingStream`(状态机 + 流式) | 统一对话输入框组件 `WritingChatComposer` |
| `AIWritingDraftPanel`(右侧草稿预览) | 消息流容器 `WritingChatThread`(渲染阶段产物为消息卡片) |
| `types/ai-writing.ts`(按需扩展) | 意图解析客户端 `api/ai-writing-intent.ts` |
| `ProgressEvent` 事件模型 | 阶段产物 → 消息卡片的适配器 `progressToMessage.ts` |
| 会话持久化 REST | 「pending 意图」暂存逻辑(重搜后续跑) |
**不复用**:`AIWritingForm` / `WritingFormContext` / `InterventionCard`(这三个就是要去掉的表单)。
### 5.3 交互流程(对话版)
1. 空闲:用户在底部输入「写一篇关于 X 的深度分析,2000 字」→ 意图解析 `start_writing`
→ 组装 `AIWritingRequest` → `startWriting()`。
2. 运行中:`ProgressEvent`(`research_progress`/`outline_chunk`/`draft_chunk`…)
→ 适配成消息流里的**进度卡片/流式卡片**(复用现有流式视图组件的展示部分)。
3. 到达暂停点:不再弹 `InterventionCard`,而是在消息流里展示**阶段产物卡片**
(素材列表 / 大纲 / 草稿 / 审核结果)+ 一句提示「你可以说:确认继续 / 补充素材 / 改大纲…」。
用户下一句自然语言 → 意图解析 → `submitIntervention`。
4. 问答:任意时刻用户提问 → 旁路回答,不打断写作状态。
5. 素材不足:意图解析 `needResearch` 或后端 `section_help` → 对话追问「素材不足,帮你重搜?」
→ 确认后重搜 → 自动续跑 pending 意图。
### 5.4 问答旁路(新能力)
「对写作内容提问」不应改写作 state。两种实现:
- **轻量(推荐先做)**:意图解析节点直接带回 `answer`(后端能读 state,一次调用出答案)。
- **完整**:新起一条**只读**问答线程(lead_agent,context 注入当前草稿/大纲作为背景),
类似 `WritingSetupChat` 的 `useThreadStream` 模式,但只读不回填。
---
## 6. 后端改造方案
### 6.1 意图解析入口(二选一)
- **方案 A1(推荐,独立 REST)**:`app/gateway/routers/ai_writing.py` 新增
`POST /api/ai-writing/sessions/{id}/intent`,入参 `{text}`,后端读该 session 的
checkpoint state(素材/大纲/草稿摘要)+ 用 `ai_writing` 配置模型解析,返回第 4.1 的契约。
- 好处:不改图拓扑;前端拿到结果后再决定调 `startWriting`/`submitIntervention`。
- 方案 A2(图内节点):在图里加 `conversation_router` 节点。改动大、状态耦合高,**不推荐**首期。
### 6.2 意图解析 prompt
新增 `agents/ai_writing/prompts/intent_router_prompts.py`(或复用/扩展 `intent_parser.py`):
- 输入:用户文本、`current_status`、素材摘要(keywords + 每条 title/source)、大纲纲要、
草稿字数/章节标题、审核意见列表。
- 输出:严格 JSON(第 4.1 契约)。要求模型**只在证据充分时给高 confidence**,
否则给 `clarify`(防误触发是硬指标)。
### 6.3 「带目标的重搜」协议扩展(可选增强)
现有 `supplement_search` 用 `user_query` 追加检索。为支持「改稿意见 → 缺素材 → 重搜 → 自动续跑」,
建议在 `material_confirm` 的 resume 里新增可选字段 `resume_intent`(重搜完成后要自动执行的原始意图)。
- 前端也可**纯前端**实现(重搜完成事件回来后再自动提交缓存的 pending payload),**首期优先前端实现**,避免动后端协议。
### 6.4 追加自定义审核意见(`add_review_opinion`)
`editor`/`pause_review` 目前只接受「接受/强制定稿」。要支持「你再补一条审核意见」,
需在 `review_confirm` resume 里接受 `extra_review_notes`,`editor` 或 `writer_draft`
修订时并入。首期可先降级为 `user_revise` + notes(把用户的审核诉求当作改稿意见),
**完整版**再扩协议。
### 6.5 不需要改的部分
- 四个阶段大脑节点(researcher/writer_outline/writer_draft/editor)逻辑不动。
- 五个暂停点、checkpoint、StreamBridge、持久化不动。
---
## 7. 数据流时序(改稿 + 素材不足自动重搜)
```
用户: "第三段补个2024年的行业案例"
│
▼ POST /api/ai-writing/sessions/{id}/intent { text }
后端: 读 state(素材摘要) + LLM 解析
│ 返回 { intent:revise_draft, action:user_revise,
│ payload:{userRevisionNotes:...},
│ needResearch:true, researchQuery:"2024 行业案例" }
▼
前端: needResearch=true → 缓存 pending=payload
│ 对话追问/直接执行: submitIntervention(material_confirm, supplement_search, user_query="2024 行业案例")
▼ LangGraph resume → researcher 补素材 → materials_ready
前端: 收到新素材 → 自动提交 pending
│ submitIntervention(draft_confirm, user_revise, userRevisionNotes=...)
▼ writer_draft 用新素材改稿 → draft_chunk... → draft_ready → pause_draft
前端: 消息流展示新草稿 + "已按你的意见改写并补充了案例"
```
---
## 8. 关键实现细节与坑
1. **暂停态识别**:对话层必须知道「现在停在哪个 pause_point」才能正确映射 action。
`AIWritingContext` 已有 `currentPause`/`status`,直接读。
2. **防误触发**:低置信意图一律走 `clarify` 反问,绝不猜着执行(尤其 `finalize`/`re_search` 这种代价大的)。
3. **pending 意图队列**:素材不足重搜是「先插一段、再回原意图」,需要一个小状态机记住 pending,
并处理「重搜后用户又改主意」的情况(重搜完成前允许覆盖 pending)。
4. **回退/undo(H 类)**:依赖 checkpoint 回滚,`runtime/runs/worker.py` 有 `rollback` 概念,
首期可**不做**或只做「重跑当前阶段」。
5. **问答旁路不污染 transcript**:问答消息可标记 `kind:'qa'`,与写作进度事件区分持久化。
6. **流式产物→消息卡片**:复用现有 `OutlineStreamingView`/`DraftStreamingPreview` 等的**展示部分**,
套进消息气泡即可,不必重写渲染。
7. **离线内网**:意图解析模型走 `config.yaml → ai_writing` 里的配置模型;检索仍走技能/内网 ES,
不联网(与现有并行席位「禁 web_search」的离线策略一致)。
---
## 9. 分阶段实施路线图
**阶段 0 — 骨架(不含意图)**
- 新路由 + 新页面 `AIWritingChatPage`,复用 `AIWritingContext`/`useStream`/`DraftPanel`。
- 底部对话框先只做「空闲 `start_writing` + 暂停点固定按钮」跑通端到端(相当于把干预卡换成消息)。
**阶段 1 — 意图解析(核心)**
- 后端 `POST /sessions/{id}/intent` + intent prompt,返回第 4.1 契约。
- 前端接入:自然语言 → 意图 → `startWriting`/`submitIntervention`,覆盖 A~F 类意图。
- 加高频短句规则快路径 + 低置信 `clarify` 反问。
**阶段 2 — 问答旁路 + 素材不足自动重搜**
- G 类问答(先用 intent 节点直接带 `answer`)。
- `needResearch` + pending 意图机制(前端实现重搜后自动续跑)。
**阶段 3 — 增强协议(按需)**
- `add_review_opinion` 扩 `review_confirm` 协议、`resume_intent` 后端化、undo/回退。
**阶段 4 — 打磨**
- 消息卡片视觉、历史回看(复用 transcript)、后台挂起入口、灰度到侧边栏。
---
## 10. 关键文件清单(落地时对照)
### 新增(前端)
- `open-canvas/pages/AIWritingChatPage.tsx` — 新页面
- `open-canvas/components/ai-writing-chat/WritingChatComposer.tsx` — 统一输入框
- `open-canvas/components/ai-writing-chat/WritingChatThread.tsx` — 消息流
- `open-canvas/components/ai-writing-chat/progressToMessage.ts` — 事件→消息适配
- `open-canvas/api/ai-writing-intent.ts` — 意图解析客户端
- 路由注册:`open-canvas/OpenCanvasRoutes.tsx`(+ 可选 `sidebar-menu.ts`)
### 新增(后端)
- `app/gateway/routers/ai_writing.py` — 加 `POST /sessions/{id}/intent`
- `agents/ai_writing/prompts/intent_router_prompts.py` — 意图 prompt
- (阶段 3)`state.py` / `nodes/*` — 扩 `review_confirm`、`resume_intent`
### 复用(尽量不改)
- `open-canvas/contexts/AIWritingContext.tsx`、`hooks/useAIWritingStream.ts`
- `open-canvas/components/ai-writing/AIWritingDraftPanel.tsx` 及各流式展示组件
- `open-canvas/types/ai-writing.ts`(按需扩字段)
- `agents/ai_writing/graph.py`、四个阶段节点、五个暂停点
### 不动
- 旧 `open-canvas/pages/AIWritingPage.tsx` 及 `AIWritingForm`/`WritingFormContext`/`InterventionCard`
---
## 11. 待确认
1. 意图解析引擎是否采用**推荐的方案 A(后端 LLM 节点)**?
2. 新页面是**独立入口**(侧边栏新增)还是先**内部灰度**(仅直链)?
3. 「素材不足自动重搜」首期是否接受**前端实现 pending 续跑**(不动后端 resume 协议)?
4. 问答旁路首期是否接受**意图节点直接带答案**(不新起只读线程)?
> 以上默认按「A / 内部灰度 / 前端 pending / 意图节点带答案」推进,如需调整请指出。
+52
View File
@@ -0,0 +1,52 @@
# 我的项目
入口:全量与简洁导航中的「我的项目」,路由 `/page/strategy/qa/projects`。旧地址 `/page/workspace/projects` 仅保留兼容跳转,不再展示独立入口或旧页面。
## 使用流程
1. 新建项目,选择普通问答、策略优选或方案生成。三类项目独立保存和展示,方案生成不会复用或标记策略优选项目。
2. 选择项目默认智能体,填写目标、约束等项目说明。新建项目对话时仍可换用另一个可访问的智能体。
3. 创建后进入项目工作区。普通问答项目可包含多条独立问答;方案项目可包含多条方案记录。最近对话根据 `analysis_project_mode` 回到普通问答分组或方案项目。
4. 方案生成可从「工作流配置 · 报告结构」选择、启停、编辑、删除结构,选择后自动填充项目报告结构配置;也可直接编辑大纲及固定/弹性约束。
5. 策略智能项目保存默认 Strategy Lens;每轮依次进行问题重构、各 Lens 独立分析、策略辩论和方案生成,并分为「意图理解」「多视角策略分析」「完整策略方案」三栏。右栏以文档形式给出执行摘要、目标原则、分阶段行动、资源配置、风险应对、评估复盘、形成依据与备选策略切换条件;原始对话和引用仍可切换查看。
6. 策略智能对话底部提供改变约束、增删视角、重新推演和挑战当前策略快捷动作;项目主页面可直接添加或删除 Lens。
模式在创建后不可修改,前后端都不提供修改入口。项目、对话归属和智能体访问权限在服务端校验;方案结构及每条对话所选智能体也以服务端配置为准。
## 代码与运行
- `src/pages/AnalysisProjectsPage.tsx`:普通问答与方案项目的新建、设置、历史和工作台布局。
- `src/strategy-selection/StrategyProjectsPage.tsx`:「我的项目」三类项目列表及创建入口。
- `src/pages/analysis-projects/ProjectConversation.tsx`:现有聊天、研究进度、方案画布的项目级组合。
- `src/core/analysis-projects/api.ts`:Gateway 项目 API 与按账号隔离的缓存。
- 后端:`/api/analysis-projects`,对话仍通过现有 `/api/threads/{thread_id}/runs/*` 流式接口发送;服务端从不可变归属读取项目配置。
启动新版 Gateway 后自动创建项目表。生产持久化依赖已配置的数据库;memory 模式中的项目随服务重启丢失。前端沿用仓库的 Vite 开发与构建命令。
## 验证范围
后端回归命令:在 `offline-backend-20260512/backend` 执行
```shell
python -m pytest tests/test_analysis_projects.py tests/test_deep_research_report_tool.py tests/test_writing_mode_report_middleware.py -q
```
涵盖内存与 SQLite 存储、跨用户隔离、禁止修改模式、运行时覆盖伪造模式与知识库、更新配置、对话归属与失败清理、固定方案模板及知识库资料进入研究管线。
部署前还需使用实际知识库和模型验证三类问答、流式断线续接及方案后续修改。接口模拟与单元测试不能替代真实模型联调。
## 2026-09-14 我的项目与知识库验收修复
日常使用复查:我的项目侧栏历史保持 `qa/projects/groups` 上下文,官方侧栏同样识别项目归属;手机选择历史后收起菜单;项目表单不重复显示侧栏按钮。设置保存后默认智能体同步到新对话选择器。读取项目对话失败提供重试,项目列表失败可原地重试。助手知识检索隔离过期响应,清空输入时禁用检索,无结果提供目录入口。ScrollArea 固定为包含 React 19 引用回调修复的 1.2.18。详细记录见 `docs/daily-usage-audit-20260914.md`。
- 简洁模式的「我的项目」列表、策略工作台和普通问答项目恢复工作区功能导航;窄屏提供展开菜单按钮。
- 普通问答先创建具名项目,再在项目内创建多个对话。列表读取 `/api/analysis-projects` 的 `qa` 项目,不再读取、标记或自动归入全局普通聊天。
- 项目设置更新保留既有知识库绑定。旧「项目空间」入口已移除并跳转「我的项目」;普通问答路由为 `qa/projects/groups/*`,方案生成路由为 `qa/projects/solutions/*`。
- 策略生成卡片统一先显示思考/执行过程,再显示正文;评判、方案及展开视图沿用相同顺序。
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步只保留候选策略和评判选择,第三步集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
- 资料库把已完成的策略优选综合方案作为独立「策略生成」来源展示,并标注具体策略项目名称;条目可回到策略项目第三步,也可直接下载 Markdown/Word。普通策略项目线程生成的文件沿用原文件下载链路并显示同类来源标识。
- 方案生成使用独立项目和固定报告结构,通过 `deep_research_report` 直接生成并打开方案画布;策略优选项目不再出现在方案生成分类中。
- 知识库管理页读取上游最新目录,避免远端删除后命中旧缓存;检索调用仍共享短期目录缓存。
- 普通用户访问尚未初始化的助手知识库时显示明确空态及刷新按钮。
验证:后端相关回归 273 项、前端策略/项目回归 31 项通过;真实 BAAI/bge-m3 编码、指定库向量检索与草稿隔离通过;本地隔离服务的问答项目创建/分组、真实问答、方案整理、助手库创建/编辑/文件入库/向量检索/详情均通过。完整验收记录见仓库 `docs/ji-knowledge-projects-acceptance.md`。不在源码或文档中保存接口密钥。
File diff suppressed because it is too large. Load diff
+436
View File
@@ -0,0 +1,436 @@
# 工作台问答 · iframe 嵌入开发说明
本文档说明如何将 DeerFlow **通用工作台问答**(与 `/page/workspace/chats/*` 同一套 LangGraph 流式能力)以**普通 iframe** 形式嵌入第三方页面,并在**父页面无法读取子 frame 内 `thread_id`** 的前提下,轮询「本次问答是否已结束」。
后端公开 API 实现见:`offline-backend-20260512/backend/app/gateway/routers/public_embed.py`
数据表:`embed_sessions`(迁移 `20260602_01`)
---
## 0. 速读
| 角色 | 要做的事 |
|------|----------|
| **父页面** | 自己生成 `sessionId`(UUID),拼进 iframe `src`;用 `sessionId` 调公开 API 轮询 `finished` |
| **iframe(DeerFlow)** | 打开 `/embed/chats/...`,自动登录、自动发问、登记 `sessionId → thread_id` |
| **不需要** | 无界(wujie)、父页面拿 `thread_id`、DeerFlow 登录态 Cookie |
**完成条件**:`GET .../sessions/{sessionId}/status` 返回 `finished === true`(且无进行中的 run,非澄清中断)。
---
## 1. 背景与约束
### 1.1 业务目标
- 第三方系统用 **iframe** 嵌 DeerFlow 问答 UI。
- 传入:**问题文案**、**用户名**(走现有 `login/username`)、**主题色**。
- 展示:**消息列表** + **沙箱/产物侧栏**(与主聊天相同的 `ChatBox`);**隐藏**页面左侧栏、Workspace 侧栏、底部输入框。
- 进入后 **自动发送** 首条用户消息并走流式问答。
### 1.2 为何需要 `sessionId`
iframe 与父页面通常 **跨域**,父页面 **不能** 读取子应用 URL 里的 `thread_id`(hash 路由在子 frame 内)。
因此约定:
1. **父页面**在打开 iframe 前生成全局唯一的 `sessionId`;
2. **子页面**在创建/确定 `thread_id` 后,调用后端 **登记接口** 写入 `session_id → thread_id`;
3. **父页面**只凭 `sessionId` 查询状态,必要时响应里会带上 `thread_id`(可选使用)。
---
## 2. 端到端流程
```mermaid
sequenceDiagram
participant Parent as 父页面
participant Iframe as DeerFlow iframe
participant API as Gateway /api/public/embed
Parent->>Parent: sessionId = crypto.randomUUID()
Parent->>Iframe: iframe src 含 sessionId、message、username、theme
Iframe->>Iframe: loginByUsername + 自动 sendMessage
Iframe->>API: POST /sessions { session_id, thread_id }
loop 每 1–3s
Parent->>API: GET /sessions/{sessionId}/status
API-->>Parent: registered, finished, thread_id
end
```
---
## 3. 前端路由与页面
### 3.1 路由(独立于 `/page/*`)
在 `App.tsx` 注册,**不经过** `PageRoutes` / `PageSidebar`,因此无外层导航与 Workspace 侧栏。
| 路由 | 组件 | 说明 |
|------|------|------|
| `/embed/chats/:thread_id` | `EmbedChatPage`(外包 `ChatRuntime`) | `thread_id` 可为 `new` 或已有 UUID |
示例(HashRouter,注意 `#`):
```
http://localhost:5173/#/embed/chats/new?sessionId=...&message=...&username=guest&theme=red
```
开发环境端口以本地 Vite 为准(如 5173 / 5174)。
### 3.2 URL 查询参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `sessionId` 或 `session_id` | **是** | 父页面生成的关联 id,8–128 位,仅字母数字 `_` `-` |
| `message` / `question` / `text` | 建议 | 首条用户问题;有则进入后自动发送 |
| `username` / `userName` | 否 | 默认 `guest`,对应 `POST /api/v1/auth/login/username` |
| `theme` | 否 | `dark-blue` → 深色;`red` 等 → 浅色(与 `LoginPage` 一致) |
缺少 `sessionId` 时,嵌入页会提示错误,不会发起登记。
### 3.3 相关源码
| 路径 | 职责 |
|------|------|
| [src/pages/EmbedChatPage.tsx](../src/pages/EmbedChatPage.tsx) | 嵌入页:登录、自动发问、登记 session、仅消息列表 + ChatBox |
| [src/core/embed/params.ts](../src/core/embed/params.ts) | 从 URL 解析嵌入参数 |
| [src/core/embed/status-api.ts](../src/core/embed/status-api.ts) | 登记 / 轮询 API 封装 |
| [src/App.tsx](../src/App.tsx) | 路由注册 |
### 3.4 UI 行为说明
- **隐藏**:`PageSidebar`、`WorkspaceSidebar`、`InputBox`、顶栏标题栏等(相对完整 `ChatPage`)。
- **保留**:`MessageList`、流式指示、澄清卡片(若 agent 触发 `ask_clarification`)、右侧 **产物/沙箱**(`ChatBox` 内 60/40 分栏逻辑与主聊天一致)。
- **线程 id**:新建会话时客户端先分配 UUID,`onStart` 后 `history.replaceState` 更新 hash 为 `/embed/chats/{真实id}`(与主聊天 `ChatPage` 相同手法,避免整页重载)。
---
## 4. 公开 HTTP API(无需鉴权)
前缀:`/api/public/embed/`(走 Gateway,与 `VITE_BACKEND_BASE_URL` 一致,默认 `http://47.88.25.99:7001/`)。
CSRF / Auth 中间件对 `/api/public/` 放行;请求 **不需要** Cookie。
### 4.1 登记映射(iframe 内自动调用)
```http
POST /api/public/embed/sessions
Content-Type: application/json
{
"session_id": "parent-generated-uuid",
"thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b"
}
```
**响应:**
```json
{
"session_id": "parent-generated-uuid",
"thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
"registered": true
}
```
同一 `session_id` 再次 POST 会 **更新** `thread_id`(例如 `onStart` 后 id 变化)。
### 4.2 轮询状态(父页面使用)
```http
GET /api/public/embed/sessions/{session_id}/status
```
**尚未登记**(iframe 仍在加载或未 POST):
```json
{
"session_id": "parent-generated-uuid",
"registered": false,
"thread_id": null,
"finished": false,
"thread_status": "pending",
"has_active_run": false,
"latest_run_status": null
}
```
**已登记且问答进行中:**
```json
{
"session_id": "parent-generated-uuid",
"registered": true,
"thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
"finished": false,
"thread_status": "idle",
"has_active_run": true,
"latest_run_status": "running"
}
```
**已登记且已结束:**
```json
{
"session_id": "parent-generated-uuid",
"registered": true,
"thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
"finished": true,
"thread_status": "idle",
"has_active_run": false,
"latest_run_status": "success"
}
```
#### 字段说明
| 字段 | 含义 |
|------|------|
| `registered` | 是否已收到 iframe 的 POST 登记 |
| `finished` | **父页面应以此为准**:无 inflight run,且线程非 running/busy,且非 `interrupted`(澄清等待用户) |
| `thread_status` | 从 checkpoint 推导:`idle` / `interrupted` / `error` 等 |
| `has_active_run` | 是否存在 pending/running 的 LangGraph run |
| `latest_run_status` | 该线程最近一次 run 的状态:`success` / `error` / … |
| `thread_id` | 解析出的 DeerFlow 线程 id(父页面可选用,非必须) |
### 4.3 获取生成结果(父页面使用)
问答结束后(建议先确认 `finished === true`),拉取本次模型输出:
```http
GET /api/public/embed/sessions/{session_id}/result
```
**尚未登记:**
```json
{
"session_id": "parent-generated-uuid",
"registered": false,
"thread_id": null,
"finished": false,
"thread_status": "pending",
"has_active_run": false,
"latest_run_status": null,
"messages": [],
"generated_text": "",
"generated_files": []
}
```
**已登记且已有内容:**
```json
{
"session_id": "parent-generated-uuid",
"registered": true,
"thread_id": "e9c7d09e-821d-421f-ae24-e5ddfe0cff5b",
"finished": true,
"thread_status": "idle",
"has_active_run": false,
"latest_run_status": "success",
"messages": [
{ "type": "human", "content": "请总结要点", "id": "..." },
{ "type": "ai", "content": "根据分析,要点如下:...", "id": "..." }
],
"generated_text": "根据分析,要点如下:...",
"generated_files": [
"/mnt/user-data/outputs/summary.md",
"/mnt/user-data/outputs/chart.png"
]
}
```
#### 结果字段说明
| 字段 | 含义 |
|------|------|
| `messages` | 完整消息列表(与 checkpoint 中序列化结构一致,含 human / ai / tool 等) |
| `generated_text` | 所有 `ai` / `assistant` 消息正文按顺序拼接(段落间空一行) |
| `generated_files` | 本次会话产物的虚拟路径列表(与 UI 产物栏 `artifacts` 一致) |
未结束时也可调用,会返回当前已有内容;`finished` 仍为 false 表示可能还会继续生成。
### 4.4 按 thread_id 查询(可选)
调试或父页面已持有 thread_id 时:
```http
GET /api/public/embed/threads/{thread_id}/status
GET /api/public/embed/threads/{thread_id}/result
```
状态接口响应不含 `messages` / `generated_text` / `generated_files`;结果接口字段与 §4.3 相同(无 `session_id` / `registered`)。
### 4.5 前端封装
```ts
import {
getEmbedSessionResult,
getEmbedSessionStatus,
registerEmbedSession,
} from "@/core/embed/status-api";
// 父页面(或你们自己的脚本)
const status = await getEmbedSessionStatus(sessionId);
if (status.registered && status.finished) {
const result = await getEmbedSessionResult(sessionId);
console.log(result.generated_text, result.generated_files);
}
// 仅 iframe 内需要
await registerEmbedSession(sessionId, threadId);
```
`fetch` 使用 `credentials: "omit"`,不携带 DeerFlow 会话。
---
## 5. 父页面集成示例
### 5.1 拼接 iframe
```html
<iframe
id="deerflow-embed"
title="DeerFlow 问答"
style="width:100%;height:600px;border:0"
></iframe>
```
```javascript
const sessionId = crypto.randomUUID();
const base = "http://localhost:5173"; // 换成你们部署的前端 origin
const params = new URLSearchParams({
sessionId,
message: "请根据附件总结要点",
username: "guest",
theme: "red",
});
document.getElementById("deerflow-embed").src =
`${base}/#/embed/chats/new?${params.toString()}`;
```
### 5.2 轮询直到结束
```javascript
const API = "http://47.88.25.99:7001/"; // VITE_BACKEND_BASE_URL
async function pollUntilFinished(sessionId) {
const res = await fetch(
`${API}/api/public/embed/sessions/${encodeURIComponent(sessionId)}/status`,
{ credentials: "omit" },
);
if (!res.ok) throw new Error(`status HTTP ${res.status}`);
const data = await res.json();
if (!data.registered) return { done: false, reason: "pending" };
if (data.finished) return { done: true, data };
return { done: false, reason: "running" };
}
const timer = setInterval(async () => {
try {
const poll = await pollUntilFinished(sessionId);
if (poll.done) {
clearInterval(timer);
const output = await fetch(
`${API}/api/public/embed/sessions/${encodeURIComponent(sessionId)}/result`,
{ credentials: "omit" },
).then((r) => r.json());
console.log("问答已结束", output.generated_text, output.generated_files);
}
} catch (e) {
console.error(e);
}
}, 2000);
```
### 5.3 跨域注意
父页面从浏览器 `fetch` Gateway(如 `http://47.88.25.99:7001/`)时,**Origin 必须是后端 CORS 白名单里的完整地址**(含协议、主机、端口),与 API 地址是否写 `localhost` / `127.0.0.1` 无关。
**本地开发**(前端默认 `5174`)请在 Gateway 环境变量中配置:
```bash
# offline-backend-20260512/.env 或启动脚本注入
GATEWAY_CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174,http://localhost:5173,http://127.0.0.1:5173
```
修改后**重启 Gateway**(`make dev` / `make gateway`)。若父页面部署在其它域名,把该域名的 origin 一并加入逗号分隔列表。
临时调试(勿用于公网生产):
```bash
GATEWAY_CORS_ALLOW_ALL=1
```
常见报错:`No 'Access-Control-Allow-Origin' header` → 当前页面的 origin 未出现在 `GATEWAY_CORS_ORIGINS` 中。
### 5.4 其它部署注意
---
## 6. 部署与数据库
1. **跑迁移**(SQL 持久化后端):
```bash
cd offline-backend-20260512/backend
# 按项目惯例执行 alembic upgrade head
```
表:`embed_sessions(session_id PK, thread_id, created_at, updated_at)`。
2. **重启 Gateway**,确认路由已挂载:`public_embed.router`(`app/gateway/app.py`)。
3. **前端构建** 后,将嵌入地址配给第三方;`VITE_BACKEND_BASE_URL` 指向可达的 Gateway。
无 SQL 时开发环境会退化为 **内存** `MemoryEmbedSessionStore`(进程重启后映射丢失);生产请使用持久化库。
---
## 7. 安全说明
- `sessionId` 与 `thread_id` 均视为 **不透明能力令牌**:知晓即可查询状态(公开 API **无登录**)。
- 建议:父系统自行生成不可猜测的 UUID;敏感场景在贵司 API 网关再加鉴权或 IP 限制。
- 登记接口为 **POST upsert**,不校验「session 是否属于某租户」——若需多租户隔离,请在业务层约定 `sessionId` 命名空间或增加网关校验。
---
## 8. 与主聊天 `/page/workspace/chats` 的差异
| 项目 | 主聊天 | iframe 嵌入 |
|------|--------|-------------|
| 路由 | `/page/workspace/chats/:id` | `/embed/chats/:id` |
| 布局 | 完整 Workspace + 输入框 | 仅消息区 + 产物栏 |
| 登录 | 常规登录流 | URL `username` 自动 `login/username` |
| 首条消息 | 用户输入 | URL `message` 自动发送 |
| 完成态查询 | 无公开 API | `sessionId` + `/api/public/embed/sessions/.../status` |
能力上仍走同一 `useThreadStream` + `lead_agent` 流式链路,沙箱与产物行为与主聊天一致。
---
## 9. 常见问题
**Q:`registered` 一直为 false?**
A:检查 iframe URL 是否带 `sessionId`;子页是否登录成功;Network 里是否有 `POST /api/public/embed/sessions` 且 200。
**Q:`finished` 一直 false?**
A:可能仍在流式生成(`has_active_run: true`),或 agent 进入澄清(`thread_status: interrupted`)。嵌入模式无底部输入框,澄清需产品侧另行处理或避免触发澄清类工具。
**Q:父页面能否不用轮询?**
A:当前标准方案为公开 GET 轮询。若同源可考虑 `postMessage` 扩展(未默认实现)。
**Q:能否继续用已有 thread_id 嵌入?**
A:可以:`#/embed/chats/{thread_id}?sessionId=...`,可选再带 `message` 追加一轮;登记会在已知 id 上 upsert。
---
## 10. 变更记录
| 日期 | 说明 |
|------|------|
| 2026-06-02 | 初版:iframe 嵌入路由、`sessionId` 映射表、公开登记与状态 API |
| 2026-06-02 | 新增 `GET /sessions/{session_id}/result`:返回 messages、generated_text、generated_files |
@@ -0,0 +1,154 @@
# 公开页 · iframe 嵌入外观对接说明(主题 / 背景色)
本文档面向**嵌入方(宿主系统)**,说明如何在用 `<iframe>` 嵌入 DeerFlow 公开页时,从外部控制页面的**整体明暗主题(light/dark)**与**背景色 / 文字色**,使其与宿主站点的视觉风格保持一致。
前端实现见:`src/core/embed/background.ts`(参数解析与 postMessage 监听)、`src/App.tsx`(主题接入 `forcedTheme`)。
---
## 0. 速读
| 想做什么 | 怎么做 |
|----------|--------|
| 嵌入时指定深色主题 | iframe `src` 加 `?theme=dark` |
| 嵌入时指定背景/文字色 | iframe `src` 加 `?bg=%23001529&text=%23ffffff`(`#` 需编码为 `%23`) |
| 宿主切主题时同步切换 | `postMessage({ type: "deerflow:set-theme", theme: "dark" }, "*")` |
| 宿主动态改背景色 | `postMessage({ type: "deerflow:set-bg", bg: "#001529", text: "#fff" }, "*")` |
| 什么都不传 | 页面使用 DeerFlow 自带默认配色,互不影响 |
---
## 1. 适用页面
以下公开页(免登录、只读)均支持本文档的全部参数:
| 页面 | 路由 |
|------|------|
| 会话分享页 | `/#/share/{shareCode}` |
| 圆桌分享页 | `/#/roundtable-share/{shareCode}`、`/#/roundtable-share/{shareCode}/report` |
| 用户排行榜 | `/#/public/leaderboard` |
| 定时任务公开页 | `/#/public/scheduled-tasks/{taskId}`(含 `/runs/{runId}/fullscreen` 全屏页) |
> 注意:站点使用 **hash 路由**,路径在 `#` 之后。
> 嵌入聊天页(`/#/embed/chats/*`、`/#/embed/roundtable`)有自己的主题约定,见 `embed-iframe-chat.md`,不在本文范围内。
---
## 2. URL 查询参数(嵌入时一次性指定)
### 2.1 参数列表
| 参数 | 别名 | 作用 | 合法值 |
|------|------|------|--------|
| `theme` | — | 切整体明暗主题 | 见 2.2 取值映射表 |
| `bg` | `background` | 页面根容器背景色 | 见 2.3 颜色格式 |
| `text` | `fg` | 页面根容器文字色 | 见 2.3 颜色格式 |
三个参数相互独立,可任意组合;不传或传非法值时该项忽略、沿用默认。
### 2.2 `theme` 取值映射
| 传入值(不区分大小写) | 实际主题 |
|------|------|
| `dark`、`dark-blue`、`深蓝`、`深色` | 深色 |
| `light`、`red`、`红白`、`浅色` | 浅色 |
| 其他 / 不传 | 忽略,沿用站内默认(浅色) |
### 2.3 颜色格式(`bg` / `text`)
颜色值经过白名单校验,仅接受以下写法,其余一律丢弃(防注入):
- **hex**:`#fff`、`#ffffffcc` 等 3/4/6/8 位(`#` 可省略;URL 里 `#` 必须编码为 `%23`)
- **函数式**:`rgb()` / `rgba()` / `hsl()` / `hsla()`,括号内仅允许数字、逗号、百分号、空格、`/`
- **命名色**:纯英文字母,如 `white`、`transparent`
### 2.4 参数位置
参数放在 hash 路由之后(推荐)或 `#` 之前都能识别;两处都传时**hash 路由内的优先**:
```
https://<host>/#/public/leaderboard?theme=dark ← 推荐
https://<host>/?theme=dark#/public/leaderboard ← 也支持
```
### 2.5 示例
```html
<!-- 深色主题 -->
<iframe src="https://<host>/#/public/leaderboard?theme=dark"></iframe>
<!-- 浅色主题 + 自定义背景与文字色(# 编码为 %23) -->
<iframe src="https://<host>/#/share/Ab3xYz?theme=light&bg=%23f5f7fa&text=%23333333"></iframe>
<!-- 主题与背景叠加:整页走深色变量,根背景用宿主品牌色 -->
<iframe src="https://<host>/#/roundtable-share/Ab3xYz?theme=dark&bg=%23001529"></iframe>
```
---
## 3. postMessage(嵌入后动态切换)
适用于宿主站点支持用户切主题、需要 iframe 实时跟随的场景。向 iframe 的 `contentWindow` 发消息即可,**无需等待子页面回执**(监听器在应用挂载时注册)。
### 3.1 消息格式
```js
const frame = document.getElementById("deerflow-frame");
// 切整体明暗主题(theme 取值同 2.2)
frame.contentWindow.postMessage({ type: "deerflow:set-theme", theme: "dark" }, "*");
// 改背景色 / 文字色(字段名与 URL 参数一致,bg/background、text/fg 均可)
frame.contentWindow.postMessage({ type: "deerflow:set-bg", bg: "#001529", text: "#fff" }, "*");
// 兼容别名:type 也可写 "deerflow:set-background"
```
说明:
- 两类消息相互独立,可只发其一;`set-bg` 里 `bg`、`text` 也可只传其一,未传的字段保持当前值。
- 非法颜色值 / 不认识的 `theme` 取值会被忽略,不会清空已有设置。
- targetOrigin 建议按需收紧(写 DeerFlow 站点的 origin 而非 `"*"`)。
### 3.2 宿主主题联动示例
```js
function syncDeerflowTheme(isDark) {
const win = document.getElementById("deerflow-frame").contentWindow;
win.postMessage({ type: "deerflow:set-theme", theme: isDark ? "dark" : "light" }, "*");
win.postMessage(
{ type: "deerflow:set-bg", bg: isDark ? "#001529" : "#ffffff" },
"*",
);
}
// iframe 首次加载用 URL 参数兜底,之后切换走 postMessage
myThemeStore.subscribe((theme) => syncDeerflowTheme(theme === "dark"));
```
---
## 4. 优先级与默认行为
同一项配置多处传入时,按以下优先级取值(高 → 低):
1. **postMessage** 动态设置的值
2. hash 路由内的 URL 参数(`#/路由?theme=...`)
3. `#` 之前的 URL 参数(`?theme=...#/路由`)
4. 都没传 → DeerFlow 默认配色(浅色主题 + 页面自带 Tailwind 配色)
其他保证:
- 外部主题只影响**当前 iframe 的渲染**,不写 localStorage——不会污染用户直接访问 DeerFlow 主站时自己选择的主题。
- `bg`/`text` 是内联样式,作用在页面根容器上,优先级高于主题变量;与 `theme` 叠加时以 `bg`/`text` 为准。
---
## 5. 常见问题排查
| 现象 | 排查 |
|------|------|
| `theme=dark` 不生效 | 确认路由属于第 1 节列出的公开页;确认参数拼在 hash 路由后(`/#/share/xx?theme=dark`),而不是写成了 `/#/share/xx#theme=dark` |
| `bg=#fff` 不生效 | URL 里 `#` 没编码会被浏览器截断,必须写 `%23fff`;或直接省略 `#` 写 `bg=fff` |
| postMessage 无反应 | 确认消息体是对象且 `type` 拼写为 `deerflow:set-theme` / `deerflow:set-bg`;确认在 iframe `load` 之后发送 |
| 颜色被忽略 | 检查是否符合 2.3 白名单格式(如 `linear-gradient(...)` 等复杂值不支持) |
| 主题切了但个别区域颜色没变 | 该区域可能使用了固定配色;把页面路由与截图反馈给 DeerFlow 维护方补充 `dark:` 样式 |
-265
View File
@@ -1,265 +0,0 @@
# 多智能体协调 API 接口文档
后端路由:`offline-backend-20260512/backend/app/gateway/routers/multi_agent.py`
路由前缀:`/api/multi-agent`
## 概览
提供一个"总控 + 多个子智能体"的协调编排接口。
- **总控(leader)** 智能体:通过 `agent_orchestration` 技能给子智能体派活
- **子智能体(special)**:执行被派到的具体任务,完成后把交付内容广播给其他线程
调用方按下面顺序使用:
1. 调 `POST /api/multi-agent/init` 一次性创建协调智能体 + 给每个子智能体建 `thread`,拿到 `thread_ids` 映射
2. 客户端把 `thread_ids` 保管好,后续每次调 `POST /api/multi-agent/run/stream` 时回传
3. 先调用 `agent_type="leader"` 拿派活清单,再针对清单里每个子智能体调用 `agent_type="special"`
## 1. 初始化多智能体集群
### `POST /api/multi-agent/init`
创建协调智能体并为列表中的每个子智能体建立独立的 `thread`,同时把"职责说明"广播到所有子智能体线程作为初始上下文。
#### 请求体
```json
{
"agent_names": ["taiwan-intelligence-gathering-agent", "us-intelligence-gathering-agent"],
"main_agent_name": "main-controller",
"model": "deepseek-chat"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `agent_names` | `string[]` | 是 | 已有子智能体的 id 列表(kebab-case,区分大小写不敏感,自动转小写) |
| `main_agent_name` | `string` | 否 | 要创建的协调智能体名,默认 `main-agent`。重名时会复用已存在的同名智能体 |
| `model` | `string` | 否 | 协调智能体使用的模型名。不传则用后端默认模型 |
#### 响应(200)
```json
{
"status": "success",
"main_agent_name": "main-controller",
"thread_ids": {
"main-controller": "thread-uuid-xxx",
"taiwan-intelligence-gathering-agent": "thread-uuid-yyy",
"us-intelligence-gathering-agent": "thread-uuid-zzz"
}
}
```
`thread_ids` 是一个 `{agent_name: thread_id}` 字典。**前端必须把这个对象完整保管下来**,后续调用 stream 接口需要原样回传。
#### 错误
| 状态码 | 触发条件 |
|---|---|
| `400` | `agent_names` 为空 |
| `401` | 未携带 `Authorization` 头 |
| `500` | 协调智能体创建失败且不是"已存在"错误 |
---
## 2. 流式调度
### `POST /api/multi-agent/run/stream`
通过 SSE(Server-Sent Events)流式驱动一个 leader 或 special 智能体跑一轮对话。
#### 请求体
```json
{
"agent_type": "leader",
"d_agent_thread_id": {
"main-controller": "thread-uuid-xxx",
"taiwan-intelligence-gathering-agent": "thread-uuid-yyy"
},
"agent_name": "main-controller",
"new_message": "请分析特朗普的支持率",
"skill_stop_names": [],
"model": "deepseek-chat"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `agent_type` | `"leader"` \| `"special"` | 否 | 默认 `leader`。`leader` 调用协调智能体,`special` 调用子智能体 |
| `d_agent_thread_id` | `object` | 是 | 由 `/init` 接口返回的 `thread_ids` 映射,原样回传 |
| `agent_name` | `string` | 是 | 本轮要驱动的智能体名(必须存在于 `d_agent_thread_id` 中) |
| `new_message` | `string` | 是 | 这轮发给智能体的用户消息 |
| `skill_stop_names` | `string[]` | 否 | 触发后中断执行的技能名列表(默认空) |
| `model` | `string` | 否 | 本轮 LLM 模型名,默认 `deepseek-chat` |
#### 响应
`Content-Type: text/event-stream` 的 SSE 长连接,按到达顺序产出多种数据:
##### a. 透传上游事件
原样转发底层 `POST /api/threads/{thread_id}/runs/stream` 的事件帧(LangGraph 协议),包含 `event:`、`data:` 等行。前端按 LangGraph SDK 既有方式渲染即可。
##### b. 最后一帧:调度状态
流结束前会额外产出一帧 `data:` 行,结构因 `agent_type` 而异:
**`agent_type == "leader"`:**
```
data: {"status": [["us-intelligence-gathering-agent", "收集美国民众对特朗普的看法"], ["taiwan-intelligence-gathering-agent", "..."]]}
```
`status` 是一个 `[agent_name, task_text]` 二元组数组,代表协调智能体本轮派出去的任务清单。**前端拿到后应当对每个元素再调用一次本接口(`agent_type="special"`)来执行子智能体**。如果数组为空,说明 leader 觉得已有信息足够,不再派活,可以直接展示 leader 的最终文本回答。
**`agent_type == "special"`:**
```
data: {"status": "子智能体xxx完成yyy工作"}
```
代表子智能体完成本轮工作并已把交付内容广播给其他线程。
#### 副作用(重要)
- **leader 调用**:自动把"总控智能体收到用户消息: …"广播到所有非自己的线程;派活后再把"总控智能体给 X 派活, 内容为: …"广播到所有非派活双方的线程
- **special 调用**:完成后把"子智能体 X 完成 Y 工作, 交付内容为: …"广播到所有非自己的线程
这意味着每个线程的对话历史里会逐步累积出全局上下文,无需前端再做合并。
#### 错误
| 状态码 | 触发条件 |
|---|---|
| `400` | `agent_name` 缺失或未在 `d_agent_thread_id` 中 |
| `401` | 未携带 `Authorization` 头 |
| `502` | 上游 `runs/stream` 返回非 200 / SSE 解析失败 |
---
## 3. 前端调用流程示例
```ts
// 1. 拿 token
const loginResp = await fetch("/api/v1/auth/login/username", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "yourname" }),
});
const { access_token } = await loginResp.json();
const authHeaders = {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
};
// 2. 初始化
const initResp = await fetch("/api/multi-agent/init", {
method: "POST",
headers: authHeaders,
body: JSON.stringify({
agent_names: [
"taiwan-intelligence-gathering-agent",
"us-intelligence-gathering-agent",
],
main_agent_name: "main-controller",
model: "deepseek-chat",
}),
});
const { thread_ids } = await initResp.json();
// 3. 流式调用 leader
async function streamLeader(message: string, model = "deepseek-chat") {
const resp = await fetch("/api/multi-agent/run/stream", {
method: "POST",
headers: authHeaders,
body: JSON.stringify({
agent_type: "leader",
d_agent_thread_id: thread_ids,
agent_name: "main-controller",
new_message: message,
skill_stop_names: [],
model,
}),
});
const reader = resp.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
let dispatched: [string, string][] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data:")) continue;
try {
const payload = JSON.parse(line.slice(5).trim());
if (Array.isArray(payload.status)) {
dispatched = payload.status;
}
// 其余事件按 LangGraph SDK 协议自行渲染
} catch {
/* 非 JSON 帧,可忽略或交给现有渲染器 */
}
}
}
return dispatched;
}
// 4. 对派活清单逐个调用 special
async function streamSpecial(agentName: string, task: string, model = "deepseek-chat") {
return fetch("/api/multi-agent/run/stream", {
method: "POST",
headers: authHeaders,
body: JSON.stringify({
agent_type: "special",
d_agent_thread_id: thread_ids,
agent_name: agentName,
new_message: task,
skill_stop_names: [],
model,
}),
});
// 同样按 SSE 解析
}
// 5. 编排:先 leader 派活 → 依次执行 special → 再让 leader 汇总
const dispatched = await streamLeader("请分析特朗普的支持率");
for (const [agentName, task] of dispatched) {
await streamSpecial(agentName, task);
}
const next = await streamLeader("根据上面收集到的信息出最终结论");
// 当 leader 返回 status: [] 时,意味着汇报完毕,可结束循环
```
---
## 4. 注意事项
- **`model` 字段的取值**:必须是后端 `config.yaml` 中 `models[]` 已配置的模型名(如 `deepseek-chat`、`glm-4`、`gpt-4o` 等)。传错会导致上游 502
- **`thread_ids` 必须每次回传**:后端不存储这个映射,是无状态接口
- **协调智能体必须装有 `agent_orchestration` 技能**:`/init` 创建时会自动指定 `skills=["agent_orchestration"]`。后端 `skills/` 目录里必须存在该技能定义,否则 leader 无法派活
- **同名协调智能体冲突**:`main_agent_name` 重名时后端不会报错("already exists" 走兼容分支),会复用既有智能体——所以同一个用户多次 init 用同一个名字是安全的,但 SOUL 不会被覆盖
- **SSE 断连**:本接口底层 `on_disconnect: "continue"`,断连后上游 run 会继续跑到结束(不会取消)
- **并发广播**:广播消息使用 `asyncio.gather` 并发更新所有线程 state,性能上不会随子智能体数线性变慢
---
## 5. 相关接口(底层依赖)
本接口内部转发以下 Gateway 原生接口,了解细节可查:
| 接口 | 路由 | 用途 |
|---|---|---|
| 创建 thread | `POST /api/threads` | 为每个智能体建立独立会话 |
| 读 thread state | `GET /api/threads/{id}/state` | 广播前取当前消息列表 |
| 写 thread state | `POST /api/threads/{id}/state` | 把广播消息追加到 messages |
| 取 agent 详情 | `GET /api/agents/{id}` | init 时读取子智能体的 name/description |
| 建 agent | `POST /api/agents` | init 时创建协调智能体 |
| 流式 run | `POST /api/threads/{id}/runs/stream` | leader/special 的实际执行 |
@@ -649,6 +649,31 @@ JSON 列在 repo(`sql.py`)里用 `json.dumps/loads` 手动序列化(与 `a
---
## 7B. 业务链条持久化(`roundtable_chains.py`,2026-06)
「业务链条」是用户在配置页编排的**可复用、命名、有序**的智能体编排模板;选中后 Step 2 由总控**按链条顺序派活**(前端编排,后端零改动)。链条持久化仿 §7A,新增一张表 + 一组 CRUD 路由。完整前后端设计见 `frontend-web/docs/multi-agent-business-chain-dev.md`。
### 7B.1 数据模型(`deerflow.persistence.roundtable_chains`)
- 表 **`roundtable_chains`**:`id`(PK str64) / `user_id`(idx) / `title`(512) / `description`(512, nullable) / `seats`(**`PortableLongText`** JSON:有序 `[{agent_id,name,description}, ...]`,`seats[0]` 先发言) / `created_at` / `updated_at`(**`BeijingDateTime`**)。
- ORM `RoundtableChainRow` 登记进 `persistence/models/__init__.py`,启动 `Base.metadata.create_all` 自动建表;store `make_roundtable_chain_store(sf)` 接到 `app.state.roundtable_chain_store`(`deps.py`)。
### 7B.2 路由(`app/gateway/routers/roundtable_chains.py`,前缀 `/api/roundtable-chains`)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `` | 当前用户全部链条(含 seats,按 `updated_at` 倒序) |
| POST | `` | 新建(可带客户端 `id`);校验席位 `2 ≤ N ≤ 8` + 去重 |
| GET | `/{id}` | 取单条;非属主 → 404 |
| PUT | `/{id}` | partial 更新(`exclude_unset`);带 seats 时同样校验 |
| DELETE | `/{id}` | 204;非属主 → 404 |
- 鉴权/属主用 `_current_user_id(request)`,跨用户一律 404,与 `roundtable_drafts` 一致。
- 席位约束 `MIN_SEATS=2 / MAX_SEATS=8` 与前端 `CHAIN_MIN/MAX_SEATS`、推荐弹窗 `MIN/MAX_SELECTED` 对齐;前端配置页保存前先校验,后端 `_validate_seats` 兜底(400)。
- 前端 `api/chains.ts` 直接打这组路由(snake↔camel 边界映射);测试 `tests/test_roundtable_chains.py`(CRUD + 部分更新 + 跨用户隔离)。
---
## 8. 内置席位 SOUL 设计要点
每个 `roundtable-*`(除 intent / recommender)遵循统一结构:角色定位、核心职责、输出风格(emoji + 简短结论)、边界(只产出本席位、等总控派活)。
@@ -0,0 +1,514 @@
# 多智能体规划 · 后台挂起执行 + 进度可视化 —— 分步实现方案
> 目标:在「多智能体规划」第二步增加**后台挂起**能力——把当前任务挂到后台,自动一路跑到
> 最后一步(Step3 结果绘制完成)为止;历史记录下拉框里进行中的任务显示加载动画;点击进行中
> 的任务弹出一个**大弹窗**,里面是**横向的智能体派活链路流程图**(节点 = 各席位智能体,图标 +
> 名称,不同颜色,正在发言的节点和它的连接线带加载动画);弹窗里有「查看」按钮,点击进入正在
> 进行中的任务实时观察。
---
## 0. 现状与架构事实(动手前必读)
研究自现有代码,决定了整套方案的形态。**关键结论:当前编排循环跑在前端,离开页面即中断。**
### 0.1 前端现状
| 事实 | 位置 |
|------|------|
| Step2 编排循环(leader → 顺序派 special → leader … 直到 `dispatched===[]` 共识 / `MAX_CYCLES=8`)**完全由前端 hook 驱动** | `src/roundtable-planning/hooks/useStep2Orchestration.ts:992-1204`(recommend)、`1228-1439`(chain) |
| 循环由 `currentStep===2` 的 `useEffect` 启停;离开 Step2 的 cleanup 会 `orchestrationCancelRef.current=true` + `abortAllInFlight()` 中断 | `useStep2Orchestration.ts:1468-1541`(尤其 cleanup `1534-1537`、`1471-1477`) |
| 共识判定:`leaderRes.dispatched.length===0` → `setHasConsensus(true)` | `useStep2Orchestration.ts:1091-1099` |
| 进入 Step3:手动点按钮,前置 `hasConsensus===true` | `Step2Panel.tsx:370-381`、`RoundtablePlanningPage.tsx:586-595` |
| Step3「结果绘制」:前端流式调 `roundtable-report` 智能体,写出 `outputs/方案总览.html`,`phase→"done"` 即完成 | `src/roundtable-planning/hooks/useStep3Report.ts:36,51,165,560-596,636-652` |
| 草稿持久化:`/api/roundtable-drafts`,含 step1/step2/step3 三段快照 + `furthestStep(1|2|3)`,**无显式 status 字段** | `src/roundtable-planning/api/drafts.ts:25-110,112,184-225` |
| 历史下拉 UI(触发按钮 + 列表 + 重命名/删除) | `RoundtablePlanningPage.tsx:771-892` |
| 自动保存触发点(Step1 回复完 / Step2 每轮气泡收尾 / Step3 报告保存 / 步骤前进) | `useDraftPersistence.ts:248-316` |
| 加载草稿恢复:`handleLoadDraft` → `getDraft` → `cancelAllStreams` → hydrate → `setCurrentStep(furthestStep)` | `useDraftPersistence.ts:308-368` |
### 0.2 后端现状
| 事实 | 位置 |
|------|------|
| `/api/multi-agent/init`:为各席位并行建 thread + 单例总控 `roundtable-coordinator`,SSE 返回 `seat_ready`/`coordinator_ready`/`init_done(thread_ids)` | `app/gateway/routers/multi_agent.py:647-823` |
| `/api/multi-agent/run/stream`:驱动**单轮** leader 或 special 的 LLM 执行,末尾 JSON 帧 `status`(派活列表 / `[]` 共识 / `clarification` / `error`) | `multi_agent.py:1076-1142` |
| `/api/multi-agent/report/init`:为 Step3 报告建 thread | `multi_agent.py`(report/init) |
| **后端不存在编排循环**——leader 派活由前端解释后再逐个发 `run/stream`;后端只做单轮执行 + 把派活/完成「广播」追加进各 thread 历史消息 | `multi_agent.py:24-29,487-502,1108-1142` |
| 派活格式:`agent_orchestration` 工具调用 `{agent_name, task}`(旧式 bash 解析兼容) | `multi_agent.py:525-552` |
### 0.3 可复用模板:AI 写作的「后台多阶段流程」
AI 写作已经把「后台跑多阶段流程 + 前端 SSE 订阅 + 可 resume + 定时清理」跑通了,**圆桌后台作业直接照搬这套**:
| 模板要素 | 位置 |
|------|------|
| LangGraph 图驱动多阶段(researching/writing_outline/writing_draft/reviewing/…),条件路由推进,`interrupt()` 暂停点等用户输入 | `packages/harness/deerflow/agents/ai_writing/graph.py`、`nodes/*`(`pause_material_confirm` 等 `45-290`) |
| Session 持久化表 `ai_writing_sessions`:`id/user_id/title/status/draft_*/transcript(LONGTEXT)/...`,`status∈{in_progress,done,error}` | `packages/harness/deerflow/persistence/ai_writing_sessions/model.py:14-39` |
| `transcript` 用 `PortableLongText`(MySQL LONGTEXT,避开 64KB),时间用 `BeijingDateTime` | 同上 `27-38` |
| 进度走 LangGraph Server 标准流 `stream_mode=["messages-tuple","values"]`,**graph 在 langgraph runtime 里跑,任意 worker 可 join stream**(这是后台健壮性的关键) | `ai_writing.py:1-17`、`langgraph.json:16-18` |
| Checkpointer 共享 `AsyncSqliteSaver`,落 `.deer-flow/data/checkpoints.db`(需挂数据卷) | `runtime/checkpointer/async_provider.py` |
| Cleanup cron(leader 文件锁、活跃保护、先删 CP 再删 DB、admin 手动触发) | `app/gateway/ai_writing_cleanup.py:26,34-59,111-135,193-215,307-367` |
| **多 worker 坑**:模块级内存 dict(`_event_queues`)每 worker 一份会丢会话;SSE 进程内直调会整段 buffer。解法:状态进 DB+checkpointer、SSE 走真 TCP loopback `127.0.0.1`(`trust_env=False` 禁代理) | `multi_agent.py:271-293`、`AIWritingPage.tsx` 文件头注释 §3/§5 |
### 0.4 决策(已与产品确认)
1. **后端真后台执行**:把编排循环搬到后端 run,刷新 / 关页 / 换设备都不中断。
2. **流程图节点 = 智能体派活链路**:横向排列的席位节点,正在发言的节点 + 连接线加载动画。
> ⚠️ **本方案最大的工作量在 Phase 2**:把目前跑在前端 TS 里的编排循环(`runOrchestration`/`runChainOrchestration`)
> 移植成一个后端 **LangGraph「圆桌总控编排图」**。这是与 AI 写作一致、可在 LangGraph runtime 里
> 后台跑且天然 checkpoint 的健壮路径。下文每个阶段都给出验收标准,可独立提测。
---
## 1. 总体架构
```
┌─────────────────────────────── 前端 (frontend-web) ───────────────────────────────┐
│ Step2Panel ──「后台挂起」按钮──▶ POST /api/roundtable-jobs (start) │
│ HistoryDropdown ── 进行中任务显示 spinner ──▶ 点击 ──▶ DispatchChainModal(大弹窗) │
│ DispatchChainModal ── 订阅 GET /api/roundtable-jobs/{id}/stream(SSE) │
│ 横向派活链路流程图(节点=席位, 活动节点+连线动画) ──「查看」──▶ 进入任务实时观察 │
└───────────────────────────────────────────────────────────────────────────────────┘
│ SSE 进度 / 快照
┌─────────────────────────────── 后端 (backend) ─────────────────────────────────────┐
│ /api/roundtable-jobs 路由层(start/get/stream/resume/cancel) ── 仿 ai_writing.py │
│ │ 启动后台 run │
│ LangGraph「圆桌总控编排图」 roundtable_orchestrator ── 仿 ai_writing graph │
│ init → leader_call → parse_dispatch → dispatch_seats(顺序) → loop ↺ │
│ → consensus → report_draw(写 HTML) → done (clarification 点 interrupt()) │
│ │ 每次状态转移持久化进度 │
│ roundtable_jobs 表(status/phase/dispatch_chain/active_speaker/...) + 共享 checkpointer│
│ cleanup cron (仿 ai_writing_cleanup.py) │
└───────────────────────────────────────────────────────────────────────────────────┘
```
**核心思路**:后台作业 = 一个 LangGraph 图的一次 run。图节点内部复用现有的单轮执行能力
(`multi_agent.py` 里 leader/special 的 run 逻辑),把「解释派活 → 顺序派活 → 回到 leader」的
循环从前端搬进图节点。前端从「驱动者」退化为「观察者」:通过 SSE 订阅进度渲染。
---
## 2. 数据契约(前后端共享,先定义后实现)
### 2.1 作业状态机 `JobStatus`
```
queued # 已创建,等待 worker 拉起
running # 编排进行中(Step2 派活循环 / Step3 绘制)
awaiting_input # 命中 clarification,等用户回答(interrupt 挂起)
done # 已跑到 Step3 绘制完成
error # 出错终止
cancelled # 用户取消
```
### 2.2 作业阶段 `JobPhase`(细化给流程图/进度条用)
```
initializing # 建 thread / 总控就绪
leader_thinking # 总控在决策派活
dispatching # 正在顺序派活给席位(配合 active_speaker)
consensus # 共识达成(dispatched=[])
report_drawing # Step3 报告绘制中
report_done # 绘制完成
```
### 2.3 派活链路节点 `DispatchChainNode`(流程图核心数据)
```ts
interface DispatchChainNode {
agentId: string; // 席位 agent_id(小写)
name: string; // 显示名
avatarType: string; // 复用 getAvatarTypeFor(agentId, index) → 决定颜色
order: number; // 横向排列次序(chain 模式=链条顺序;recommend 模式=席位/发言顺序)
state: 'pending' | 'active' | 'done'; // 节点状态(驱动颜色/动画)
lastSpokeCycle?: number; // 最近发言轮次(可选,做 tooltip)
}
```
### 2.4 进度事件 `JobProgressEvent`(SSE 推送的归一化事件)
```ts
type JobProgressEvent =
| { type: 'status'; status: JobStatus }
| { type: 'phase'; phase: JobPhase; cycle: number }
| { type: 'chain'; nodes: DispatchChainNode[] } // 派活链路全量/增量
| { type: 'active'; agentId: string | null } // 当前发言席位(null=总控/无)
| { type: 'consensus'; percentage: number }
| { type: 'dialogue'; bubble: Step2DialogueLike } // 新增/收尾的对话气泡(供「查看」实时渲染)
| { type: 'clarification'; question: string } // 命中澄清
| { type: 'done'; step3: DraftStep3Snapshot } // 终态:最终报告快照
| { type: 'error'; detail: string };
```
> SSE 通道可直接复用 LangGraph Server 的 `values` 流(图状态快照),由前端 hook 把 `values`
> 映射成上述归一化事件;也可在路由层做映射后再下发。**推荐前端映射**(少一层后端耦合)。
### 2.5 作业记录 `RoundtableJob`(持久化 + 列表展示)
```ts
interface RoundtableJob {
id: string;
draftId: string; // 关联的草稿
userId: string;
status: JobStatus;
phase: JobPhase;
cycle: number; // 当前轮次(0..MAX_CYCLES)
dispatchChain: DispatchChainNode[];
activeAgentId: string | null;
consensusPercentage: number;
pendingClarification: string | null;
threadIds: Record<string,string> | null; // 复用 ThreadIdMap
coordinatorName: string | null;
orchestrationMode: 'recommend' | 'chain';
chain?: { id: string; title: string } | null;
error: string | null;
createdAt: string;
updatedAt: string;
}
```
---
## Phase 1 —— 后端:圆桌作业持久化模型
**目标**:建一张 `roundtable_jobs` 表(仿 `ai_writing_sessions`),存作业状态/阶段/派活链路,
并在 `roundtable_drafts` 列表里能 join 出当前作业状态。
**涉及文件(新增/改)**
- 新增 `packages/harness/deerflow/persistence/roundtable_jobs/model.py`(ORM 模型,仿 `ai_writing_sessions/model.py:14-39`)
- 新增 `.../roundtable_jobs/sql.py`(Repository:`create / get / update_progress / list_by_user / list_older_than / delete`,仿 `ai_writing_sessions/sql.py`)
- 新增 alembic 迁移 `persistence/migrations/versions/<date>_create_roundtable_jobs.py`
- 改 `roundtable_drafts` 的列表查询:返回里带上 `status`/`jobId`(LEFT JOIN roundtable_jobs,或在 draft 行上冗余一个 `status` 字段,二选一,见下)
**步骤**
1. 字段设计(对齐 §2.5):`id(PK)`、`draft_id`、`user_id`、`status`、`phase`、`cycle(Int)`、
`dispatch_chain(PortableLongText/JSON)`、`active_agent_id`、`consensus_percentage(Int)`、
`pending_clarification(Text)`、`thread_ids(JSON)`、`coordinator_name`、`orchestration_mode`、
`chain(JSON)`、`error(Text)`、`created_at/updated_at(BeijingDateTime)`。
2. `dispatch_chain` / `thread_ids` / `chain` 用 `PortableLongText` 存 JSON 字符串,repo 层序列化。
3. 列表 join:给 `DraftMeta` 增加 `status?: JobStatus` 与 `jobId?: string`。
**推荐方案**:草稿与作业 1:1(一个草稿最多一个活跃作业),在 draft 列表查询 LEFT JOIN
`roundtable_jobs` 取最新作业的 `status`。
4. 索引:`(user_id, updated_at)`、`(draft_id)`、`(status)`(cleanup/列表用)。
**验收**
- 迁移可 `alembic upgrade head` 跑通,MySQL 上 `dispatch_chain` 为 `longtext`。
- 单测:create→update_progress→get 往返;`list_by_user` 按 `updated_at` 倒序;`list_older_than` 分批。
---
## Phase 2 —— 后端:圆桌总控编排 LangGraph 图(核心 / 最大工作量)
**目标**:把前端的编排循环移植成一个后端 LangGraph 图,可在 LangGraph runtime 里后台跑、天然
checkpoint,一路跑到 Step3 绘制完成。
**涉及文件(新增)**
- `packages/harness/deerflow/agents/roundtable_orchestrator/graph.py`(图装配,仿 `ai_writing/graph.py`)
- `.../roundtable_orchestrator/state.py`(图状态 schema:intent、selectedAgents、threadIds、coordinatorName、
mode/chain、dialogues、dispatchChain、activeAgentId、cycle、consensus、phase、step3 等)
- `.../roundtable_orchestrator/nodes/*.py`(各节点)
- 在 `langgraph.json` 注册新 assistant `roundtable_orchestrator`(仿 ai_writing assistant 注册,参考 `langgraph.json:16-18`)
**图节点设计(把前端循环逐段翻译过来)**
| 节点 | 职责 | 对应前端逻辑 |
|------|------|------|
| `init` | 复用 `_create_thread_direct` 批量建席位 thread + 总控就绪(或接收前端已建好的 `threadIds` 直接用) | `useStep2Orchestration.ts:1485-1531` / `multi_agent.py:647-823` |
| `leader_call` | 跑一轮总控(agent_type=leader),拿 `status`(派活列表 / `[]` / `clarification`) | 单轮逻辑 `multi_agent.py:1076-1142` |
| `route_after_leader`(条件边) | `clarification`→`interrupt`;有派活→`dispatch_seats`;`[]`→`consensus` | `useStep2Orchestration.ts:1091-1181` |
| `dispatch_seats` | **顺序**派活:对 dispatched 里每个席位依次跑 special run;每开始一个就更新 `activeAgentId` + 进度持久化 | `useStep2Orchestration.ts:1101-1181`(务必串行,勿并行,详见前端 §5.4 注释) |
| `clarification_pause` | `interrupt()` 挂起,`status→awaiting_input`,写 `pending_clarification` | 仿 ai_writing `pause_*` `45-290` |
| `consensus` | `setHasConsensus`、`consensus=100`,落库后进入 `report_draw` | `1091-1099` |
| `report_draw` | 跑 `roundtable-report`(先 report/init 建 thread),流式写 `outputs/方案总览.html`,产出 `DraftStep3Snapshot` | `useStep3Report.ts:91-149,636-652,560-596` |
| `done` | `status→done`、写 `step3` 进作业 + 回写草稿 step3 快照 | `RoundtablePlanningPage.tsx:613-617` |
**关键实现要点**
1. **复用单轮执行**:节点内部不要重写 LLM 调用,抽出 `multi_agent.py` 里 leader/special/report 的
单轮 run 为可被图节点直接 `await` 的内部函数(去掉 HTTP 层,进程内调 LangGraph)。
2. **派活广播**:保留现有把「总控给 X 派了活 / X 完成」追加进各 thread 历史的广播
(`multi_agent.py:487-502`),让席位读到全局上下文。
3. **MAX_CYCLES 兜底**:图里维护 `cycle`,≥8 强制收敛(对齐 `roundtable-constants.ts:13`)。
4. **chain 模式**:`orchestration_mode==='chain'` 时按 `chain.seats` 固定顺序派活,跳过总控自由决策的
部分(对齐 `runChainOrchestration` `1228-1439`)。
5. **进度落库**:每个节点结束 `update_progress(jobId, {phase, cycle, dispatchChain, activeAgentId,
consensus, dialogues_delta})`,前端 SSE 才有实时数据。
6. **dialogues 累积**:把每轮气泡写进图状态 `dialogues`,既供「查看」实时渲染,也供作业结束回写草稿 step2。
**验收**
- 用脚本(仿 `scripts/probe_ai_writing_assistant.py`)直接对图发起一次 run,不开前端,能从 intent +
selectedAgents 一路跑到写出 `方案总览.html`,作业 `status` 终态 `done`。
- 命中 clarification 时图 `interrupt`,`status=awaiting_input`,`pending_clarification` 有值。
- chain 模式按链条顺序派活,recommend 模式由总控自由派活,两条路径都能收敛。
---
## Phase 3 —— 后端:作业控制 + 进度 SSE 接口
**目标**:暴露 start/get/stream/resume/cancel,后台 run 走 LangGraph Server(任意 worker 可 join)。
**涉及文件(新增/改)**
- 新增 `app/gateway/routers/roundtable_jobs.py`(仿 `ai_writing.py` 的 CRUD + 控制壳)
- 在 gateway `app.py` 注册路由
- 新增 `app/gateway/roundtable_jobs_cleanup.py`(仿 `ai_writing_cleanup.py`)+ 在启动处挂 cron
**接口契约**
| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/api/roundtable-jobs` | 入参:`{draftId, intent, selectedAgents, threadIds?, coordinatorName?, model, orchestrationMode, chain?}`。建作业记录(`queued`) + 启动图 run(LangGraph Server thread),返回 `RoundtableJob` |
| `GET` | `/api/roundtable-jobs/{id}` | 作业快照(轮询兜底 / 弹窗首帧) |
| `GET` | `/api/roundtable-jobs/{id}/stream` | SSE 进度流:透传图 `values`/`messages-tuple`,或映射成 §2.4 事件下发。**走真 TCP loopback**(`multi_agent.py:271-293`),禁代理 |
| `POST` | `/api/roundtable-jobs/{id}/resume` | 入参 `{answer}`,回填 clarification → 图 resume,`status→running` |
| `POST` | `/api/roundtable-jobs/{id}/cancel` | 取消图 run,`status→cancelled` |
| `GET` | `/api/roundtable-jobs?draftId=` | 按草稿查活跃作业(页面恢复用) |
| `POST` | `/api/roundtable-jobs/admin/cleanup` | 管理员手动清理(仿 ai_writing admin cleanup `307-367`) |
**多 worker 健壮性(务必遵守,照搬 ai_writing 教训)**
- 作业状态只存 **DB + checkpointer**,**不要**用模块级内存 dict 存 per-session 状态。
- SSE 必须经真 TCP `127.0.0.1` loopback,不能 `httpx.ASGITransport(app=...)` 进程内直调(会整段 buffer)。
- 图 run 跑在 LangGraph runtime(独立于处理 HTTP 的 worker),刷新/换 worker 都能重新 join stream。
- 数据卷:`.deer-flow/data/checkpoints.db` 必须挂出,否则容器重启丢状态。
**验收**
- `POST /start` 后立刻 `GET /{id}` 能拿到 `queued/running`;关掉 SSE 再重连能继续收进度。
- 杀掉发起 start 的 worker(或多 worker 轮询),`resume`/`stream` 仍命中同一作业。
- cleanup 干跑:活跃作业永不删,先删 CP 再删 DB。
---
## Phase 4 —— 前端:作业 API 客户端 + 数据模型扩展
**目标**:前端有一层干净的作业 API 与类型,历史列表能拿到 status。
**涉及文件(新增/改)**
- 新增 `src/roundtable-planning/api/roundtable-jobs.ts`:
```ts
export async function startJob(payload: StartJobPayload): Promise<RoundtableJob>
export async function getJob(id: string): Promise<RoundtableJob>
export function streamJob(id: string, on: (e: JobProgressEvent) => void): () => void // 返回 unsubscribe
export async function resumeJob(id: string, answer: string): Promise<void>
export async function cancelJob(id: string): Promise<void>
```
- `streamJob` 复用项目现有 SSE/`streamMultiAgent` 的封装方式(`api/multi-agent.ts:403+`),把
LangGraph `values` 映射成 §2.4 的归一化事件。
- 改 `src/roundtable-planning/api/drafts.ts`:`DraftMeta` 增 `status?: JobStatus; jobId?: string`
(`drafts.ts:76-83` + 字段映射 `122-178` 加 snake_case 转换)。
- 新增 `src/roundtable-planning/lib/job-types.ts`:把 §2.1–2.5 的类型集中导出。
**验收**:`pnpm typecheck` 通过;`getJob`/`streamJob` 能拉到 Phase 3 的数据。
---
## Phase 5 —— 前端:Step2「后台挂起」按钮
**目标**:Step2 顶栏/底栏加按钮,点击把当前任务交给后端后台跑,前端停止本地循环、转为观察者。
**涉及文件(改)**
- `src/roundtable-planning/components/Step2Panel.tsx`(按钮放在现有「进入第三步」附近 `370-381`)
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx`(接线 + 观察者模式)
- `src/roundtable-planning/hooks/useStep2Orchestration.ts`(暴露「停止本地循环并交后台」的方法)
**步骤**
1. Step2Panel 加「⏼ 后台挂起」按钮,`onBackgroundSuspend` 回调。
2. 页面 `handleBackgroundSuspend`:
- `ensureDraft(2)` 拿到 `draftId`(`useDraftPersistence.ts` ensureDraft)。
- 收集当前状态:`intentReady`、`selectedAgents`、`threadIds`、`coordinatorName`、`model`、
`orchestrationMode`、`chain`(全部来自 `step2`/`step1` 现有状态)。
- `startJob(...)` → 拿到 `jobId`。
- **停止前端本地编排**:调用 `step2.stopOrchestration()` + 置一个 `backgroundedJobId` 标记,
让 `currentStep===2` 的 useEffect **不再自启本地循环**(在 `useStep2Orchestration.ts:1468-1541`
的启动条件里加 `&& !backgrounded`)。
- toast「已转入后台,可离开页面,任务会自动跑到结果绘制完成」。
- 刷新历史列表(`drafts` 重新拉,进行中项即出现 spinner)。
3. **观察者模式**:若当前停留在 Step2 且该 draft 有 running 作业,订阅 `streamJob`,把进度事件渲染成
对话气泡/活动席位(复用现有 Step2 渲染),而不是本地驱动。
**注意:避免「双跑」**——后台已接管后,前端绝不能再并行驱动同一批 thread。用 `backgroundedJobId`
互斥;加载草稿时若发现有 running 作业,直接进观察者模式。
**验收**:点「后台挂起」→ 可立即切到别的任务/路由;回来仍在跑;不会出现前后端同时派活。
---
## Phase 6 —— 前端:历史下拉框「进行中」加载动画
**目标**:历史下拉里 `status==='running'`(或 `awaiting_input`)的草稿条目显示加载动画;点击进行中
条目不直接 load,而是打开流程图弹窗。
**涉及文件(改)**
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` 的 `HistoryDropdown`(`771-892`)
**步骤**
1. 列表项左侧名称前/后,按 `d.status` 渲染状态标记:
- `running` → 旋转的 `Loader2`(`animate-spin`)+「进行中」。
- `awaiting_input` → 呼吸点 +「待澄清」(琥珀色)。
- `done`/无 → 维持现状(显示 `Step {furthestStep}`)。
- `error` → 红点 +「出错」。
2. 点击行为分流:
- `running`/`awaiting_input` → `openDispatchChainModal(d.jobId)`(Phase 7),**不**直接 `onLoad`。
- 其它 → 维持 `onLoad(id)`。
3. 列表需要 `status`,确保 Phase 4 的 `DraftMeta.status` 已就位;下拉打开时 `refresh()` 拉最新。
**验收**:后台挂起后,历史下拉对应条目转圈;点击弹出流程图弹窗。
---
## Phase 7 —— 前端:派活链路流程图弹窗(横向 / 大弹窗 / 动画)
**目标**:大弹窗,横向排列的智能体派活链路。节点 = 席位(图标 + 名称),不同颜色;正在发言的
节点和它的连接线带加载动画;含「查看」按钮。
**涉及文件(新增)**
- `src/roundtable-planning/components/DispatchChainModal.tsx`
**实现选型**:链路是「总控 → 席位序列」的线性结构 + 需要对**单个节点和单条连线**做精确动画,
**推荐手写 flex 横向布局 + CSS 动画连接线**(轻、可控)。项目已装 `@xyflow/react@12`(带 `animated`
边)作为备选,但手写对本场景的「节点 loading + 连线流动」更直接。
**结构(手写方案草图)**
```tsx
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[88vh] flex-col gap-0 sm:max-w-5xl"> {/* 大弹窗 */}
<DialogHeader>… 任务标题 + 阶段标签(leader_thinking/dispatching/report_drawing…) …</DialogHeader>
{/* 横向链路:总控 ──▶ 席位1 ──▶ 席位2 ──▶ … ──▶ [共识] ──▶ [报告绘制] */}
<div className="custom-scrollbar overflow-x-auto py-8">
<div className="flex items-center gap-0 min-w-max px-6">
<CoordinatorNode active={phase==='leader_thinking'} />
{nodes.map((n,i)=>(
<Fragment key={n.agentId}>
<Connector active={n.state==='active'} /> {/* 活动连线:流动虚线动画 */}
<ChainNode node={n} /> {/* 活动节点:彩色+脉冲光环 */}
</Fragment>
))}
<Connector active={phase==='consensus'} />
<MilestoneNode label="共识" done={consensus===100} />
<Connector active={phase==='report_drawing'} />
<MilestoneNode label="报告绘制" active={phase==='report_drawing'} done={phase==='report_done'} />
</div>
</div>
<DialogFooter className="border-t pt-3 sm:justify-between">
<span className="text-xs text-muted-foreground">轮次 {cycle}/8 · {statusLabel}</span>
<Button onClick={onView}>查看</Button> {/* Phase 8 */}
</DialogFooter>
</DialogContent>
</Dialog>
```
**节点颜色**:`avatarType = getAvatarTypeFor(agentId, order)` → `chainTagClasses(avatarType)`
(复用 `lib/business-chain.ts:56-72` + `lib/roundtable-constants.ts`),与业务链条卡片同一套色。
**节点状态样式**
- `pending`:灰色描边、低透明度。
- `active`:席位色填充 + 外圈 `animate-ping`/脉冲光环 + 图标旋转或呼吸。
- `done`:席位色实心 + 右上角 ✓。
**连接线加载动画(active 连线)**:CSS 流动虚线(marching-ants)或渐变扫光:
```css
/* roundtable-planning.css 追加 */
@keyframes chain-flow { to { background-position: 20px 0; } }
.chain-connector--active {
background-image: linear-gradient(90deg, var(--brand-500) 50%, transparent 50%);
background-size: 20px 100%;
animation: chain-flow 0.6s linear infinite;
}
```
**数据来源**:弹窗 mount 时 `getJob(jobId)` 取首帧,随后 `streamJob(jobId, onEvent)` 实时更新
`nodes`/`activeAgentId`/`phase`/`consensus`/`cycle`;`done` 事件后停止动画、全节点置 `done`。
弹窗关闭时 `unsubscribe`。
**验收**:弹窗够大、横向滚动;活动席位节点和它左侧连线在动;阶段推进时高亮顺移;`done` 后静止。
---
## Phase 8 —— 前端:「查看」进入进行中任务并实时观察
**目标**:弹窗「查看」→ 进入该任务,停在 Step2 实时看后台进度(气泡/活动席位随 SSE 刷新),
跑完自动呈现 Step3 结果。
**涉及文件(改)**
- `RoundtablePlanningPage.tsx`(接 `onView`)
- `useDraftPersistence.ts`(`handleLoadDraft` 增「带作业」分支)
- `useStep2Orchestration.ts`(观察者渲染)
**步骤**
1. `onView`:关弹窗 → `handleLoadDraft(draftId)` 恢复该草稿到 Step2(`useDraftPersistence.ts:308-368`),
但**跳过本地编排自启**(识别到有 running 作业 → 进观察者模式,对齐 Phase 5 的互斥标记)。
2. 订阅 `streamJob(jobId)`,把 `dialogue` 事件 append 进 `step2RoundtableDialogues`、`active` 事件
驱动 `activeSpeakerId` 高亮、`consensus` 更新百分比。
3. 命中 `awaiting_input`:在 Step2 弹出澄清输入(复用现有 `pendingClarification` UI),用户回答 →
`resumeJob(jobId, answer)`(而非本地 `submitClarification`)。
4. `done` 事件:用返回的 `DraftStep3Snapshot` 走 `hydrateStep3` + 允许切到 Step3 查看报告
(`useStep3Report.hydrateSaved` `708-767`);作业列表该项变「已完成」。
**验收**:从历史「查看」进入能实时看到后台在跑;澄清能在前端回答并续跑;跑完能看到最终报告。
---
## Phase 9 —— 边界、健壮性与收尾
1. **双跑互斥**:任一 draft 同一时刻只允许一个执行者(前端本地 or 后台作业)。加载草稿/进入 Step2
前先 `GET /api/roundtable-jobs?draftId=`,有 running 就进观察者模式。
2. **后台命中澄清**:`awaiting_input` 时历史下拉显示「待澄清」徽标;流程图弹窗显示「等待澄清」;
只有「查看」进去才能回答(或在弹窗里直接给一个回答框,调用 `resumeJob`)。
3. **取消**:流程图弹窗/Step2 提供「停止后台」→ `cancelJob`,`status→cancelled`,前端可恢复本地编辑。
4. **错误恢复**:`error` 事件展示 detail;允许「重试」= 重新 `startJob`(沿用已建 threadIds)。
5. **MAX_CYCLES / 预算**:后台图内兜底(对齐前端 `MAX_CYCLES=8`),到顶强制收敛并照常进 Step3。
6. **多 worker**:严格遵守 Phase 3 的「状态进 DB+checkpointer、SSE 走 loopback」;部署若多 worker
需 sticky 或全靠 DB 态(推荐后者)。
7. **清理**:`roundtable_jobs` cleanup cron(活跃保护、先删 CP 再删 DB),与 draft 生命周期对齐。
8. **历史列表性能**:draft 列表 join 作业 status 时注意 N+1;用一次 LEFT JOIN 取最新作业。
---
## 端到端验证清单(提测前逐条过)
- [ ] Step2 点「后台挂起」→ 立刻可离开页面 / 切到别的任务,后台继续跑。
- [ ] 历史下拉里该任务显示加载动画(转圈)。
- [ ] 点击进行中任务 → 大弹窗、横向派活链路、活动节点+连线在动、节点不同颜色。
- [ ] 阶段推进(leader→派活→共识→报告绘制)时弹窗高亮顺移。
- [ ] 弹窗「查看」→ 进入任务实时观察气泡/活动席位。
- [ ] 后台命中澄清 → 列表「待澄清」+ 弹窗提示 + 可回答续跑(`resumeJob`)。
- [ ] 一路自动跑到 Step3「绘制完成」,最终报告可在 Step3 查看,作业 `done`。
- [ ] 浏览器刷新 / 关页重开后,任务仍在后台跑、状态可恢复(真后台验证)。
- [ ] 取消后台 → `cancelled`,无残留双跑。
- [ ] 多 worker(或杀 worker)下 stream/resume 不丢会话。
- [ ] cleanup 干跑不误删活跃作业。
---
## 工作量与排期建议
| 阶段 | 内容 | 粗估 | 依赖 |
|------|------|------|------|
| P0 | 数据契约定型(本文 §2) | 0.5d | — |
| P1 | 后端作业表 + repo + 迁移 | 1d | P0 |
| **P2** | **后端编排图(移植循环)** | **3–5d(核心难点)** | P0,P1 |
| P3 | 后端控制 + SSE + cleanup | 1.5d | P1,P2 |
| P4 | 前端作业 API + 类型 + draft status | 0.5d | P3 |
| P5 | Step2 后台挂起按钮 + 互斥 | 1d | P4 |
| P6 | 历史下拉 loading 动画 | 0.5d | P4 |
| P7 | 派活链路流程图弹窗 | 1.5d | P4 |
| P8 | 「查看」实时观察 + 澄清续跑 | 1.5d | P5,P7 |
| P9 | 边界/健壮性/清理 | 1d | 全部 |
> 建议先打通 **P1→P2→P3** 的后端最小闭环(脚本验证图能后台跑到出报告),再做前端 P4–P8。
> P2 是整条链路成败的关键,务必先用脱离前端的探针脚本(仿 `scripts/probe_ai_writing_assistant.py`)跑通。
---
## 附:关键参考代码索引
- 前端编排循环(待移植到后端):`useStep2Orchestration.ts:992-1204,1228-1439,1468-1541`
- 共识判定 / 进 Step3:`useStep2Orchestration.ts:1091-1099`、`Step2Panel.tsx:370-381`、`RoundtablePlanningPage.tsx:586-595`
- Step3 绘制完成标志:`useStep3Report.ts:51,165,560-596,636-652`
- 草稿模型 / API / 自动保存 / 加载恢复:`api/drafts.ts:25-110,184-225`、`useDraftPersistence.ts:248-316,308-368`
- 历史下拉 UI:`RoundtablePlanningPage.tsx:771-892`
- 后端单轮执行 / 派活 / 广播:`multi_agent.py:647-823,1076-1142,525-552,487-502`、loopback `271-293`
- AI 写作模板:`agents/ai_writing/graph.py`、`ai_writing_sessions/model.py:14-39`、`ai_writing.py`、`ai_writing_cleanup.py:307-367`、`AIWritingPage.tsx` 文件头注释
- 颜色体系复用:`lib/business-chain.ts:56-72`、`lib/roundtable-constants.ts`(`getAvatarTypeFor`)
- 流程图备选库:`@xyflow/react@12`(已安装)
```
@@ -0,0 +1,400 @@
# 业务链条模式 · 需求与分步实现文档
> 面向「智能体推荐弹窗」的扩展:在现有**智能推荐**之外,新增**业务链条选择**与**业务链条配置**两块能力;选定业务链条后,Step 2 圆桌研讨**按链条内的固定顺序**逐个发言(而非现在的总控派活)。
>
> 配套阅读:
> - 前端总览:`multi-agent-frontend-dev.md`(推荐弹窗 §6.2、Step 2 编排 §5.4、草稿持久化 §5.7)
> - 后端总览:`multi-agent-backend-dev.md`(Step 2 §6、草稿/历史持久化 §7A)
>
> 本文档约定:**「席位 / 智能体」**指一个 agent;**「业务链条 / chain」**指一组有序排列的 agent 配置,是可命名、可复用、可持久化的实体。
---
## 0. 需求原文拆解
用户原始诉求逐条拆成可实现条目:
| # | 原始诉求 | 拆解 |
|---|----------|------|
| 1 | 推荐弹窗要有「模式选择」 | 弹窗顶部加模式切换(智能推荐 / 业务链条),两种模式互斥,确认后都产出**有序**的 `SelectedAgent[]` |
| 2 | 「智能推荐」就是现在这个 | 现有 `RecommendAgentsDialog` 整体收进「智能推荐」模式,行为不变 |
| 3 | 「业务链条选择」是卡片列表 | 卡片 = 标题 + 一排智能体名 tag;tag 不同颜色;tag 之间有**向左箭头**连接,表达链条次序 |
| 4 | 业务链条需要一个**配置页面** | 左侧智能体列表(复用 `GET /api/agents`),右侧有序长条卡片(名称 + 描述);支持右侧内部拖拽排序、左→右拖入指定位置 |
| 5 | 配置页用弹窗还是新页面? | **本文档决策:独立路由页面**(理由见 §2.1) |
| 6 | 选定链条后,Step 2 用链条内智能体研讨 | 复用既有 Step 2 init/stream;**总控(leader)按链条顺序逐个派活**(§6) |
| 7 | 规划成分步可实现的需求文档 | 见 §7 分阶段实现 |
---
## 1. 目标与非目标
### 1.1 目标
- 推荐弹窗成为「**席位来源选择器**」:无论智能推荐还是业务链条,最终都向 `onConfirm` 交付一个**有序的 `{ agent_id, name }[]`** + 一个 `orchestrationMode`。
- 业务链条是**用户可创建、命名、保存、复用、删除**的有序智能体编排模板。
- Step 2 支持两种编排:
- `recommend` 模式 → 现有总控**自由派活**循环(不动)。
- `chain` 模式 → 总控仍是派活方,但**被约束按链条顺序逐个派活**(新增)。
### 1.2 非目标(本期不做,留扩展点)
- 链条内每个席位的 per-seat prompt/temperature 真实覆盖(仍仅前端记录,沿用现状)。
- 链条的分支 / 条件流转(只做线性顺序链)。
- 链条跨用户共享 / 市场(先做「本人私有链条」)。
- 链条版本管理 / 协作编辑。
---
## 2. 关键设计决策
### 2.1 决策 A:配置页面用「独立路由页面」而非弹窗 ✅
**结论:业务链条的「编辑/管理」用独立路由页面 `/page/workspace/roundtable/chains`;业务链条的「选择」仍内嵌在推荐弹窗里(卡片列表)。**
理由:
| 维度 | 弹窗 | 独立页面 ✅ |
|------|------|------------|
| 双栏 + 拖拽 + 长条卡片 | 弹窗宽度(`sm:max-w-3xl`)+ `max-h-[85vh]` 拖拽空间局促,左右栏挤 | 整屏宽度,拖拽手感好 |
| 复用入口 | 只能从推荐弹窗进 | 可从侧栏、推荐弹窗「管理链条」按钮、深链接进入 |
| 可深链接/刷新 | 不可 | `/roundtable/chains` 可直达、可分享 |
| 推荐弹窗内嵌编辑器 | 弹窗套弹窗 / 模式套编辑,状态复杂 | 关注点分离:弹窗只「选」,页面只「编」 |
**分工**:
- **推荐弹窗**:模式选择 + 业务链条**选择**(只读卡片列表,可点「去配置」跳到页面)。
- **配置页面**:业务链条的**增删改**(双栏拖拽编辑器 + 链条列表管理)。
> 备选:若产品坚持不加路由,可改为**全屏 Dialog(`max-w-[90vw] h-[90vh]`)**承载编辑器,从推荐弹窗内打开。结构与页面版一致,仅外壳不同。本文档按「独立页面」实现,§7 标注了切换成本。
### 2.2 决策 B:链条持久化 —— 后端 MySQL(确定) ✅
**结论:业务链条存后端 MySQL,仿 `roundtable_drafts`(§7A)新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由,按登录用户隔离、跨设备保留。** 不走 localStorage。
理由:链条是可复用、需跨设备/会话保留的模板,与草稿/推荐历史性质一致;草稿当年正是因 localStorage 换设备即丢才迁后端(§7A),链条不重复踩坑。
落地:
- 表/路由设计见 §5.2,是「复制 §7A 草稿持久化、把字段换成 chain」的机械工作。
- 前端 `api/chains.ts` 直接打后端接口(snake_case↔camelCase 在这层收口,仿 `api/drafts.ts`),**不提供 localStorage 实现**。
- 后端表必须先于前端 UI 落地(§7 阶段 0 即包含后端表/路由)。
### 2.3 决策 C:拖拽库选型 —— `@dnd-kit`
仓库**当前无任何拖拽库**(`package.json` 无 `@dnd-kit` / `react-dnd` / `sortablejs`;`@xyflow/react` 是节点图,不适合列表排序)。
| 方案 | 取舍 |
|------|------|
| **`@dnd-kit/core` + `@dnd-kit/sortable`**(推荐) | React 19 友好、可访问性好、跨容器拖入 + 列表排序 + 插入指示器开箱即用;新增 ~30KB gzip 依赖 |
| 原生 HTML5 DnD(零依赖兜底) | 不加依赖,但跨容器插入位置指示、键盘可达性都要手写,易出 bug |
**建议**:新增 `@dnd-kit/core`、`@dnd-kit/sortable`、`@dnd-kit/utilities`。若产品方禁止新增依赖,退回原生 HTML5 DnD(§7 阶段 2 标注差异)。
### 2.4 决策 D:Step 2「总控按链条顺序派活」,后端零改动 ✅
**形态:总控(leader)仍是唯一派活方,但每一轮被前端约束「只能派给链条里的下一个席位」,逐个走完整条链,最后由总控收口。** 区别于 recommend 模式的「总控自由决定派给谁」。
为什么后端无需改动:
- Step 2 后端 `streamMultiAgent({ agentType: "leader" })` 每轮返回 `dispatched = [[agent, task], ...]`;`agentType: "special"` 单独驱动某席位跑一轮。这两个原语已足够。
- 每个 special 跑完,后端 `_broadcast` 会把「子智能体 X 完成…交付内容为:…」append 进**其它所有席位 + 总控 thread** 的历史(后端 §6.4),所以链路上下文天然累积。
- chain 模式只是**改变前端怎么提示 leader、以及怎么校验它的派活**——把每轮 leader 的提示词约束成「请向链条第 i 位【X】派活」,并对返回的 `dispatched` 做「必须命中链条下一席位」的校验/纠偏。**后端路由、SOUL、表结构都不动。**
→ chain 模式 = 在 `useStep2Orchestration` 里新增 `runChainOrchestration`:按链条游标逐位调 leader(约束派给该位)→ 拿到 task → 调 special → 下一位 → 链尾后调一次 leader 收口。详见 §6。
> 与「前端绕过 leader 直接顺序 special」的区别:本方案**保留总控在每一步的协调/串接职责**(leader 结合前序进展为下一席位编写针对性任务、并在派活说明里承上启下),更符合「总控按链条顺序派活」的语义,圆桌可视化里总控气泡也照常出现。
---
## 3. 数据模型与类型
新增前端类型,建议放 `frontend-web/src/roundtable-planning/lib/business-chain.ts`:
```ts
/** 业务链条里的一个有序席位。 */
export interface ChainSeat {
agent_id: string;
/** 落库时的快照名/描述,避免 agent 改名后链条显示错乱;展示优先用实时 agent 数据,回退到快照。 */
name: string;
description?: string;
}
/** 一条可复用的业务链条。 */
export interface BusinessChain {
id: string;
title: string;
description?: string;
/** 有序席位列表;seats[0] 是第一个发言者。 */
seats: ChainSeat[];
createdAt?: string;
updatedAt?: string;
}
/** 推荐弹窗确认后回传给主页面的统一结果。 */
export interface SeatSelectionResult {
/** 有序席位(与现有 handleConfirmAgents 入参兼容)。 */
agents: Array<{ agent_id: string; name: string }>;
/** 决定 Step 2 走哪种编排。 */
mode: "recommend" | "chain";
/** chain 模式下带上来源链条(用于草稿快照 / Step 2 展示链名)。 */
chain?: Pick<BusinessChain, "id" | "title">;
}
```
**约束**:链条席位数沿用圆桌的 `2 ≤ N ≤ 8`(`MIN_SELECTED`/`MAX_SELECTED`),配置页保存时校验。
**tag 配色**:复用 `roundtable-constants.ts::getAvatarTypeFor(agentId, index)`,得到 `blue|purple|emerald|amber|pink|human`,再映射到已有的 avatar 配色 token(与圆桌可视化、Step 2 气泡同一套色板,视觉统一)。
---
## 4. 现有代码改动点速查
| 位置 | 改动 | 说明 |
|------|------|------|
| `components/RecommendAgentsDialog.tsx` | 包一层「模式选择」外壳,现有内容收进「智能推荐」Tab | `onConfirm` 签名从 `agents[]` 升级为 `SeatSelectionResult`(或加可选第二参 `mode`,见 §7 阶段 3 的兼容策略) |
| `pages/RoundtablePlanningPage.tsx` | `handleConfirmAgents` 接收 `mode`,存到新状态 `orchestrationMode` | 透传给 `useStep2Orchestration` |
| `hooks/useStep2Orchestration.ts` | 新增 `runChainOrchestration` + `orchestrationMode` 分支 | init effect 里按 mode 选择 `runOrchestration` 或 `runChainOrchestration` |
| `lib/roundtable-constants.ts` | 复用 `getAvatarTypeFor` / `SelectedAgent` | 不改,仅引用 |
| `hooks/useDraftPersistence.ts` + `api/drafts.ts` | Step2 快照新增 `orchestrationMode` + `chain` 字段 | 草稿能记住「这次是链条模式」 |
| `pages/WorkspaceRoutes.tsx` | 注册 `roundtable/chains` 路由 | 仿 `roundtable/planning` |
| `components/page-sidebar.tsx`(如有侧栏入口) | 加「业务链条」入口(可选) | 也可只从推荐弹窗进 |
| `api/chains.ts`(新增) | 链条 CRUD 客户端 | 直接打后端 `/api/roundtable-chains` |
| 后端 `roundtable_chains` | 表 + 路由(MySQL) | 仿 `roundtable_drafts`,**必做** |
---
## 5. 链条持久化 API 设计
### 5.1 前端客户端 `api/chains.ts`(直接打后端)
```ts
listChains(): Promise<BusinessChain[]> // 当前用户全部链条
getChain(id: string): Promise<BusinessChain>
createChain(input: Omit<BusinessChain,"id"|"createdAt"|"updatedAt">): Promise<BusinessChain>
updateChain(id: string, patch: Partial<BusinessChain>): Promise<BusinessChain>
deleteChain(id: string): Promise<void>
```
- 实现用 `apiFetch` 打 `/api/roundtable-chains`(§5.2),snake_case↔camelCase 在这层收口(仿 `api/drafts.ts`)。
- 不提供 localStorage 实现;后端表是前端 UI 的硬依赖。
### 5.2 后端表 + 路由(仿 §7A `roundtable_drafts`)
- 表 `roundtable_chains`:`id`(PK) / `user_id`(idx) / `title`(512) / `description`(text) / `seats`(`PortableLongText`,存 `ChainSeat[]` JSON) / `created_at` / `updated_at`(`BeijingDateTime`)。
- 路由 `app/gateway/routers/roundtable_chains.py`,前缀 `/api/roundtable-chains`:`GET ``(列表)/ `POST ``(建)/ `GET /{id}` / `PUT /{id}`(partial,`exclude_unset`)/ `DELETE /{id}`。
- 鉴权/属主:`_current_user_id(request)`,跨用户访问一律 404,与 `roundtable_drafts` 完全一致。
- 启动 `Base.metadata.create_all` 自动建表(MySQL 生产 / sqlite 本地走同一套 SQLAlchemy 代码,`PortableLongText` 各自降级),登记进 `persistence/models/__init__.py`,store 接到 `app.state` 并 `include_router`。
> 整体是「复制 §7A 的草稿持久化、把字段换成 chain」的机械工作,风险低。
---
## 6. Step 2 链条模式编排设计(`runChainOrchestration`)
### 6.1 与现有 `runOrchestration` 的差异
| 维度 | `runOrchestration`(recommend) | `runChainOrchestration`(chain,新增) |
|------|--------------------------------|----------------------------------------|
| 谁派活 | 总控 leader | 总控 leader(**不变**) |
| 派给谁由谁定 | leader 自由决定 | 前端按链条游标**约束 leader 只能派给下一席位** |
| 发言顺序 | leader 每轮自由派 | **链条数组的固定顺序** |
| 循环结构 | `while` 直到 leader 返回 `[]` 共识 / MAX_CYCLES | 按链条游标走一趟(每位:leader 派活 → special 交付) |
| 收口 | leader 返回 `[]` 即共识 | 链尾后再调一次 leader 做总结收口,产出 `lastLeaderContent` + `hasConsensus` |
> **核心**:leader 始终在场,只是**每轮的派活目标被前端钉死成链条的下一席位**——前端在 leader 的 `newMessage` 里写明「本轮请通过 `agent_orchestration` 仅向【X】派活」,并对 `leaderRes.dispatched` 做命中校验/纠偏。
### 6.2 伪代码
```
runChainOrchestration(seedMessage, ids, coord, chainSeats):
isOrchestrating = true
cancel = false
N = chainSeats.length
for (let i = 0; i < N; i++): // 链条游标,逐位推进
if cancel: break
seat = chainSeats[i]
seatMeta = agentNameToRole[seat.agent_id]
// ── (a) 总控派活给「链条下一席位」──────────────────────────
highlightSpeaker(null) // leader 阶段不高亮任何 sub
leaderBubble = appendStreamingDialogue("总控协调", confidence:"链条派活中")
leaderTask = buildChainLeaderTask({
seedMessage, // 任务目标/约束(首轮)
position: i, total: N,
nextSeat: seat, // 「本轮请仅向【X(seat.agent_id)】派活」
isFirst: i === 0,
})
leaderRes = await streamMultiAgent(leader, ids, coord, leaderTask, model=globalModel)
// 约束/纠偏:取 dispatched 中命中 seat.agent_id 的那条 task;
// 若 leader 没派或派错(派给非 seat、或派给多个),前端兜底:
// - 命中就用它给出的 task 文本(保留总控的承上启下措辞);
// - 没命中则用 leader 的正文 content 作为兜底 task,或回退到模板 task。
task = pickChainTask(leaderRes, seat)
// leader 若返回 clarification → 暂停链条,走与现状一致的澄清条(§6.3)
finalizeDialogue(leaderBubble, leaderRes.content, "派活说明")
// ── (b) 被派席位交付(special,顺序、不并行)──────────────────
markSpeakerStreaming(seatMeta.speakerId)
seatBubble = appendStreamingDialogue(seatMeta.displayName, confidence:"调度中")
perSeatModel = agentConfigs[seatMeta.speakerId]?.model || globalModel
specRes = await streamMultiAgent(special, ids, seat.agent_id, task, model=perSeatModel)
finalizeDialogue(seatBubble, specRes.content, "子智能体交付")
markSpeakerIdle(seatMeta.speakerId)
// 后端 _broadcast 已把本席位交付注入其余 thread 历史 → 下一席位天然可见
if cancel: return
// ── (c) 总控收口:综合全链交付,产出最终结论 ──────────────────
leaderBubble = appendStreamingDialogue("总控协调", confidence:"汇总链条结论")
leaderRes = await streamMultiAgent(leader, ids, coord,
"各链条席位已按顺序完成发言,请综合所有交付,输出最终方案,不要再派活。")
finalizeDialogue(leaderBubble, leaderRes.content, "全案盖印")
setLastLeaderContent(leaderRes.content)
setHasConsensus(true); setConsensusPercentage(100)
isOrchestrating = false
```
要点:
- **派活方仍是总控**,符合「总控按链条顺序派活」的语义;圆桌可视化里每位席位前都先出现一个总控派活气泡,再出现该席位交付气泡。
- **约束手段是 prompt + 前端校验,不动后端**:`buildChainLeaderTask` 把「本轮只能派给【X】」写进 leader 的 `newMessage`;`pickChainTask` 对 `leaderRes.dispatched` 做命中校验,派错/未派时兜底,保证链条游标严格推进。
- **复用现有三段式气泡 / 高亮 / abort 基建**(`appendStreamingDialogue` / `finalizeDialogue` / `markSpeakerStreaming` / `activeAbortersRef` / `orchestrationCancelRef`),只是驱动顺序不同。
- **special 仍是顺序**(不并行),与现状不变量一致(文件顶部注释 §1)。
- **收口 leader 调用默认开**(产出 Step 3 结论);若链条末位本身是「总结陈述」类,可关掉收口,直接拿末位交付当 `lastLeaderContent`。配置位 `chainNeedsLeaderSummary`(默认 true)。
### 6.3 澄清 / 挂起 / 恢复 / 干预
- **澄清**:链条中途某轮 leader 返回 `clarification` → 与现状一致,暂停链条、底部弹澄清条;用户回答后从**当前游标**继续(需记录 `chainCursor`)。
- **挂起/恢复**:`isRoundTableRunning` toggle 复用现有机制;恢复时从 `chainCursor` 续跑。MVP 若不做断点续跑,可先只支持「整体停止 / 从头重跑」,UI 文案说明(扩展点,§7 阶段 4 标注)。
- **干预**:可在两席位之间插入一条用户指令,由下一轮 leader 派活前消化。MVP 同上可后置。
### 6.4 多轮链条(扩展点,非 MVP)
单趟遍历是 MVP。若要「链条跑完后总控判断是否再来一轮」,可在收口 leader 处复用 `dispatched===[]?` 判定:收口 leader 若仍派活则回到链条头再跑一轮,直到共识或 MAX_CYCLES。MVP 不做,避免复杂度。
### 6.5 init 复用
chain 模式的 Step 2 init **完全复用** `initMultiAgent`:`agentNames = chainSeats.map(s => s.agent_id)`(**按链条顺序**传入,使 `buildRoleTemplates` 的 seat id 顺序 = 链条顺序,且后端注入 coordinator 的「可调度席位清单」也按此序)。init effect 里按 `orchestrationMode` 决定 init 完成后调 `runOrchestration` 还是 `runChainOrchestration`。
---
## 7. 分阶段实现计划
> 每阶段都可独立交付、独立验收。建议顺序:**数据层 → 配置页 → 弹窗模式 → 链条选择 → Step2 编排 → 持久化兼容与收尾**。
### 阶段 0 · 后端链条表 + 脚手架(1.5 天) ✅ 已完成
**目标**:打地基(含后端持久化),不含前端 UI。
- [x] **后端**:仿 §7A `roundtable_drafts` 新建 `roundtable_chains` 表 + `/api/roundtable-chains` 路由(§5.2):
- [x] `persistence/roundtable_chains/`(model + sql repo + `make_roundtable_chain_store`),登记进 `persistence/models/__init__.py`。
- [x] `app/gateway/routers/roundtable_chains.py`(GET 列表 / POST / GET{id} / PUT{id} / DELETE),鉴权属主同 `roundtable_drafts`;席位 2–8 + 去重校验。
- [x] `deps.py` 接 `app.state.roundtable_chain_store`,`app.py` `include_router`。
- [x] 后端测试 `tests/test_roundtable_chains.py`:CRUD + 部分更新 + 跨用户隔离(5 用例全过)。
- [x] **前端**:新增 `lib/business-chain.ts`(`ChainSeat` / `BusinessChain` / `SeatSelectionResult`,§3)+ `api/chains.ts`(直接打后端,§5.1)。
- [x] (决策 C)安装 `@dnd-kit/core` `@dnd-kit/sortable` `@dnd-kit/utilities`。
- **验收**:✅ 后端 5 用例通过;✅ 表注册进 `Base.metadata`、路由 `/api/roundtable-chains` 加载正常;✅ ruff 0 错;✅ 前端 typecheck 新增文件 0 错。
### 阶段 1 · 业务链条配置页面(2–3 天) ✅ 已完成
**目标**:能创建/编辑/保存/删除链条。
- [x] 路由:`WorkspaceRoutes.tsx` 注册 `roundtable/chains`(套 `WorkspaceLayout`;本页**不需要** `ChatRuntime`,无沙箱依赖)。
- [x] 页面 `roundtable-planning/pages/BusinessChainEditorPage.tsx`:
- [x] 顶部:链条列表下拉(`<select>` 列出已存链条 + 新建项)/ 新建按钮 / 标题+描述输入 / 保存 / 删除(`window.confirm`)。
- [x] **左栏**:候选智能体(`listAgents()` + `filterRecommendCandidates`),竖排卡片,可拖、可点「+」快捷加入;已加入的置灰「已加入」。
- [x] **右栏**:有序长条卡片(序号 tag + 名称 + 描述 + 移除),`SortableContext` 内拖拽排序;空态有占位提示。
- [x] 拖拽用 `@dnd-kit`:左栏 `useDraggable`,右栏 `useDroppable` + `SortableContext`/`useSortable`,`onDragEnd` 计算插入 index(pool→右栏按落点插入;seat→seat `arrayMove`);`DragOverlay` 跟手卡片。
- [x] 校验:`2 ≤ seats ≤ 8`(前端禁用保存 + toast,后端 `_validate_seats` 兜底),重复 agent 去重提示。
- [x] 保存走 `api/chains.ts`(create 用 `nanoid()` 客户端 id,update 走同一 id)。
- **验收**:✅ typecheck 0 新增错误(76 个预存量错误均在无关模块)。**人工验收待跑**:新建「情报→方案→风险→总结」链保存 → 刷新后下拉重载 → 右栏拖拽排序 → 左栏拖入指定位 → 2–8 边界禁用保存。
- **切换成本备注**:若改全屏 Dialog 方案,编辑器组件原样复用,仅去掉路由、外面套 `Dialog`。
- **入口说明**:圆桌本身未在侧栏(`sidebar-menu.ts` 入口被注释),本页暂经直链 `/page/workspace/roundtable/chains` 或顶栏「返回圆桌」往返;阶段 3 推荐弹窗「去配置」按钮会导航到此。
- **视觉(后续重画)**:本页**不**套圆桌 `roundtable-planning` scope,直接遵循《浅红色/深蓝色 UI 规范》——用 app 语义 token(光 `--primary` 红 hue27 / 暗 蓝 hue259,已内置自适应)+ 规范小圆角(输入/按钮/tag/卡片内项 2px = `R_CTRL`,分栏/面板 4px = `R_CARD`)+ 克制阴影(`PANEL_SHADOW`)。筛选框复用通用 `TagSearchBar`,配色随 app 中性 token,不再被红 scope 染色。
### 阶段 2 · 推荐弹窗「模式选择」外壳(1 天) ✅ 已完成
**目标**:弹窗顶部模式切换,智能推荐行为不变。
- [x] `RecommendAgentsDialog` 顶部加**分段按钮**模式切换:`智能推荐 | 业务链条`(active 态品牌色 + 卡底;DialogDescription 随模式切换文案)。
- [x] 现有全部内容原样收进「智能推荐」面板(`{mode === "recommend" && (<>…</>)}` 包裹,**零行为变更**)。
- [x] 「业务链条」面板放占位:居中提示 + 「去配置业务链条」按钮(`handleGoConfig` → 关弹窗 + 跳 `roundtable/chains`)。
- [x] `onConfirm` 统一升级为回传 `SeatSelectionResult`(含 `mode`):智能推荐确认回传 `{ agents, mode: "recommend" }`;主页面 `handleConfirmAgents` 同步改签名取 `result.agents`(`result.mode` 预留给阶段 4)。
- **验收**:✅ typecheck 0 新增错误(总数仍 76);智能推荐分支 JSX/逻辑原样保留(流式/勾选/进入研讨未改);模式切换只切显示、`state`/`selectedIds` 不卸载 → 候选池不丢。**人工验收待跑**:切到业务链条→再切回,推荐结果与勾选仍在。
### 阶段 3 · 业务链条选择卡片列表(1.5 天) ✅ 已完成
**目标**:弹窗「业务链条」面板里选链条。新增 `components/BusinessChainPicker.tsx`,替换阶段 2 的占位。
- [x] 进入该 Tab 时 `listChains()` 拉本人链条 + `listAgents()` 取存活 id/名称,渲染**卡片列表**:
- [x] 卡片标题(链条 title)+ 「N 席」徽章 + 选中 ✓;描述行。
- [x] 标题下一排**智能体名 tag**:按链条顺序**从左到右**、不同颜色(`getAvatarTypeFor`→`chainTagClasses`),tag 之间插 `→`(`ArrowRightIcon`)箭头连接(`seats[0]` 最左、先发言)。
- [x] 底部「配置链条」按钮(跳 `roundtable/chains`)。
- [x] 空态:无链条时引导「去配置页面创建第一条业务链条」+ 按钮。
- [x] 选中一条 → 高亮(`ring-primary`);底部「进入研讨」`onConfirm({ agents: 存活席位有序, mode:"chain", chain:{id,title} })`。
- [x] 链条里有 agent 已不存在(被删)→ 该 tag 置灰删除线 + `title` 提示 + 卡片下方 amber 警告;confirm 按**存活席位**过滤;存活 < `CHAIN_MIN_SEATS` 则禁用「进入研讨」并提示。agent 列表加载失败时 `existingIds=null` → 不误判删除。
- **验收**:✅ typecheck 0 新增错误(总数仍 76)。卡片渲染标题 + 彩色 tag + ← 箭头;选链 → `onConfirm(mode:"chain")` → `handleConfirmAgents` 按链条顺序 `setSelectedAgents` → Step 2 左侧「参与角色」顺序 = 链条顺序(**按顺序派活在阶段 4**)。**人工 UI 验收待跑**。
- **箭头方向**(已确认):tag 左→右排列、`→`(`ArrowRightIcon`)连接,`seats[0]` 最左、先发言,箭头顺读即链条研讨顺序。
### 阶段 4 · Step 2 链条顺序编排(2–3 天) ✅ 已完成
**目标**:chain 模式按链顺序逐席发言。**后端零改动。**
- [x] 主页面新增 `orchestrationMode` 状态,`handleConfirmAgents` 从 `SeatSelectionResult.mode` 写入(chain 模式 toast 带链名);`handleStartNewTask` 复位为 `recommend`;透传给 `useStep2Orchestration`。chain 顺序 = `selectedAgents`(确认时已按链条顺序写入),无需单独 `chainSeats`。
- [x] `useStep2Orchestration` 新增 `runChainOrchestration`(§6.2)+ `orchestrationModeRef`/`selectedAgentsRef`;复用现有三段式气泡/高亮/abort 基建。
- [x] 新增 `runActiveOrchestration` 分发器:init / 恢复 / 干预 / 澄清回复 4 处调用点统一改走它,按 `orchestrationModeRef` 选 `runOrchestration`(recommend)或 `runChainOrchestration`(chain)。
- [x] `runChainOrchestration`:按 `selectedAgents` 游标,每位先 leader 约束派活(`buildChainLeaderTask` 钉死目标席位 + `pickChainTask` 命中校验/纠偏,派错/未派都兜底推进)→ 再对**该席位**跑 special;中途 leader 若 `clarification` 则暂停(MVP 回复后整链重跑)。
- [x] `initMultiAgent` 的 `agentNames` 即 `roundtableAgentIds`(由 `selectedAgents` 派生,已是链条顺序,§6.5)。
- [x] 收口 leader(`finishedAllSeats` 时触发),产出 `lastLeaderContent` + `hasConsensus=true` + `consensusPercentage=100` 解锁 Step 3。
- **验收**:✅ typecheck 0 新增错误(总数仍 76);✅ 唯一直接引用 `runOrchestration` 的是 dispatcher。逻辑层:每席「总控派活气泡 → 席位交付气泡」、严格顺序、单 special 在跑、`_broadcast` 注入前序上下文、leader 纠偏推进、跑完解锁 Step 3、abort 可中断。**人工 UI 验收待跑**。
- **MVP 边界**:chain 模式的「挂起→断点续跑」「逐席间干预」未做断点续跑,`clarification`/恢复/干预当前为**整链重跑**(`chainCursor` 列为扩展点)。
### 阶段 5 · 草稿持久化兼容 + 收尾(1 天) ✅ 已完成
**目标**:草稿记住模式,回归不破。
- [x] `Step2Snapshot`(`useStep2Orchestration.ts`)+ `DraftStep2Snapshot`(`api/drafts.ts`)新增 `orchestrationMode?: "recommend"|"chain"` 与 `chain?: {id,title}|null`。
- [x] 恢复路径:`orchestrationMode`/`chainMeta` 是**主页面 state**,`getStep2Snapshot` 写入;**包裹版 `hydrateStep2`** 在加载时 `setOrchestrationMode(snapshot.orchestrationMode ?? "recommend")` + `setChainMeta(snapshot.chain ?? null)` 再调 `step2.hydrateFromDraft`;老草稿无字段 → 默认 `recommend`(向后兼容)。`handleStartNewTask` 复位两者。
- [x] 文档回写:`multi-agent-frontend-dev.md` §5.7(快照形状 + `orchestrationMode/chain` 说明)、§6.2(席位来源选择器导语);`multi-agent-backend-dev.md` 新增 §7B(`roundtable_chains` 表/路由)。
- **验收**:✅ `pnpm typecheck` 0 新增错误(总数仍 76)。逻辑:chain 会话保存草稿 → 重载 `orchestrationMode="chain"` + 链信息恢复;老草稿(无字段)默认 recommend 照常加载。**人工 UI 验收 + `pnpm build` 待跑**。
### 工期粗估
| 阶段 | 估时 |
|------|------|
| 0 后端链条表 + 脚手架 | 1.5d |
| 1 配置页 | 2–3d |
| 2 弹窗模式壳 | 1d |
| 3 链条选择卡片 | 1.5d |
| 4 Step2 链编排 | 2–3d |
| 5 持久化兼容收尾 | 1d |
| **合计** | **约 9–11 人日**(含后端 `roundtable_chains` 表/路由) |
---
## 8. 验收清单(端到端)
1. 推荐弹窗顶部有「智能推荐 / 业务链条」切换;智能推荐与现状逐像素一致。
2. 配置页 `/page/workspace/roundtable/chains` 左栏列出智能体、右栏有序长条卡片;左→右拖入指定位、右栏内拖拽排序均生效;2–8 校验生效;保存/重载/删除正常。
3. 业务链条卡片:标题 + 彩色智能体 tag + ← 箭头连接,渲染正确。
4. 选定链条 → Step 2 席位顺序 = 链条顺序,并**按顺序逐个发言**,后序能看到前序交付。
5. chain 跑完解锁 Step 3;草稿记住 chain 模式并可重载。
6. 删除链条中引用的 agent 后,选择端有降级提示,不崩。
---
## 9. 风险与开放问题
| # | 项 | 说明 / 建议 |
|---|----|-------------|
| 1 | 配置页:弹窗 vs 页面 | 本文档定为**独立页面**(§2.1)。若产品要弹窗,编辑器组件可复用,仅换外壳。**请确认。** |
| 2 | 链条持久化 | **已定:后端 MySQL `roundtable_chains`**(§2.2 / §5.2),仿草稿持久化。 |
| 3 | Step 2 链编排 | **已定:总控按链条顺序派活**(§2.4 / §6),后端零改动。 |
| 4 | 新增 `@dnd-kit` 依赖 | 若禁止新增依赖,退原生 HTML5 DnD(手感与可达性下降)。**请确认。** |
| 5 | ← 箭头方向 / tag 阅读顺序 | 默认 `seats[0]` 在最左、先发言;箭头仅作视觉连接。**请确认是否要「从右往左读」。** |
| 6 | chain 模式是否需要收口 leader 总结 | 默认需要(产出 Step 3 结论)。若末位席位自带总结可关掉。 |
| 7 | leader 派活纠偏策略 | leader 派错/未派时前端兜底推进游标(§6.2 `pickChainTask`);可加 toast 提示「已按链条强制推进」。 |
| 8 | chain 模式断点续跑 / 逐席干预 | MVP 不做,列为扩展点(§6.3 / §7 阶段 4)。 |
| 9 | 链条是否私有 | 本期仅本人私有(仿草稿按 `user_id` 隔离)。共享/市场留扩展。 |
---
## 10. 路径速查(新增/改动)
| 资源 | 路径 | 状态 |
|------|------|------|
| 链条类型 | `frontend-web/src/roundtable-planning/lib/business-chain.ts` | 新增 |
| 链条 API 客户端 | `frontend-web/src/roundtable-planning/api/chains.ts` | 新增 |
| 配置页 | `frontend-web/src/roundtable-planning/pages/BusinessChainEditorPage.tsx` | 新增 |
| 链条选择面板 | `frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx` | 新增 |
| 链条卡片 | `frontend-web/src/roundtable-planning/components/BusinessChainCard.tsx` | 新增 |
| 推荐弹窗(加模式壳) | `frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx` | 改 |
| 主页面(mode 透传) | `frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx` | 改 |
| Step2 编排(加 chain) | `frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts` | 改 |
| 草稿快照(加 mode) | `hooks/useDraftPersistence.ts` + `api/drafts.ts` | 改 |
| 路由注册 | `frontend-web/src/pages/WorkspaceRoutes.tsx` | 改 |
| 后端链条持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_chains/` | 新增 |
| 后端链条路由 | `offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py` | 新增 |
| 后端 store 接线 | `offline-backend-20260512/backend/app/gateway/deps.py`(`app.state.roundtable_chain_store`)+ `app.py` | 改 |
</content>
</invoke>
@@ -439,8 +439,9 @@ isOrchestrating = false
> **重大变更**:草稿从浏览器 `localStorage` 迁到**后端 MySQL**,按登录用户隔离、跨设备/会话保留。旧 `utils/drafts.ts` 已删除;`useDraftPersistence` 仍保持对外接口不变(列表项含 `name`,由后端 `title` 映射而来),主页面无需改动。后端表/路由见 backend doc「圆桌草稿持久化」。
- **API 客户端 `api/drafts.ts`**:`listDrafts()`(轻量元数据)/ `getDraft(id)`(全量 step1+step2)/ `createDraft` / `updateDraft`(partial)/ `deleteDraft` / `deriveDraftName`。snake_case↔camelCase 映射在这一层收口(后端 `furthest_step/created_at/updated_at` ↔ 前端 `furthestStep/...`)。
- **快照形状**:`DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`DraftStep2Snapshot.{selectedAgents, threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages}`;以 JSON 整存进后端 `step1`/`step2`(`PortableLongText`/LONGTEXT,避免大对话超 64KB)。
- **快照形状**:`DraftStep1Snapshot.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`DraftStep2Snapshot.{selectedAgents, threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit, seatStubMessages, orchestrationMode?, chain?}`;以 JSON 整存进后端 `step1`/`step2`(`PortableLongText`/LONGTEXT,避免大对话超 64KB)。
- **`selectedAgents`(2026-06 修复)**:本次研讨实际选中的席位。**不存的话加载草稿后 Step 2/3 左侧「参与角色」会回退到 `DEFAULT_SELECTED_AGENTS`(默认 6 个)而非当时真正选的那几个** —— `roleTemplates`/`agentNameToRole` 都由 `selectedAgents` 派生。`useStep2Orchestration.hydrateFromDraft` 在该字段非空时 `setSelectedAgents`,老草稿(无此字段)保持当前值不强行覆盖。
- **`orchestrationMode?` / `chain?`(2026-06 业务链条)**:Step 2 编排模式(`recommend`=总控自由派活 / `chain`=按业务链条顺序派活)+ 来源链条 `{id,title}`。是**主页面 state**,由页面在 `getStep2Snapshot` 写入、在**包裹版 `hydrateStep2`** 里读出后 `setOrchestrationMode`/`setChainMeta`(step2 hook 的 `hydrateFromDraft` 不直接消费)。老草稿无字段 → 默认 `recommend`。业务链条完整设计见 `multi-agent-business-chain-dev.md`。
- `deriveDraftName({intentReady, interactiveMessages})`:优先 `intentReady.objective`,否则第一个 user 消息,否则 `未命名草稿 <时间>`,截断 40 字。**仅在 create 时写一次**(见下方 bug 修复)。
- **列表加载**:挂载时 `useEffect` 调 `listDrafts()`;增删改后 `refreshList()` 重拉。
- **自动保存策略**:仅在「前进」步骤切换时保存(1→2、2→3),后退不触发。
@@ -481,6 +482,8 @@ isOrchestrating = false
### 6.2 `RecommendAgentsDialog`
> **席位来源选择器(2026-06 业务链条)**:弹窗顶部加了**模式分段切换** `智能推荐 | 业务链条`。智能推荐 = 本节描述的现状(零行为变更,整体收进 `{mode==="recommend"}` 分支);业务链条 = `BusinessChainPicker`(选一条已配置链条,卡片标题 + 彩色席位 tag + `→` 箭头)。`onConfirm` 统一回传 `SeatSelectionResult{ agents, mode, chain? }`;`mode==="chain"` 时 Step 2 由总控**按链条顺序派活**(`useStep2Orchestration.runChainOrchestration`)。配置页与编排详见 `multi-agent-business-chain-dev.md`。
- **新增 `draftId` prop(2026-06)**:本次推荐归属的草稿/会话 id。由主页面在打开弹窗前 `await drafts.ensureDraft(1)` 拿到并传入(见 §6.6)。`null` 时退化为「直接推荐、不存历史」的旧行为。
- 打开时管线(`start(forceFresh=false)`):`listAgents() → filterRecommendCandidates()` → **若 `draftId` 有历史 → `listRecommendHistory()` 取最近一次直接展示(不重跑模型)**;否则 `streamRecommend()` 跑新推荐。
- 阶段 UI(`InternalState.phase`):
@@ -0,0 +1,404 @@
# 多智能体会商 Step2 多结果线方案
## 背景
当前「多智能体会商 / 多智能体规划」里,一个历史草稿只保存一份 `step2` 快照。用户从历史记录加载草稿后,如果再次点击「智能体推荐」并在弹窗里点击「进入研讨」,新的 Step2 研讨会继续写入同一份 `draft.step2`,结果是第一次生成内容会被第二次生成内容覆盖,或者页面只能展示最新一次结果。
产品期望是:
- 用户从历史记录进入已有草稿时,进入第二步后,左下角能看到第一次生成结果。
- 用户再次点击「智能体推荐」并点击「进入研讨」时,要开启一条新的研讨结果线。
- 第二次生成结果显示在左下角,但不能影响第一次生成结果。
- 用户之后再次查看同一个历史草稿时,可以分别查看第一次生成结果和第二次生成结果。
因此,这个需求的核心不是简单增加一个展示区域,而是把 Step2 从「单结果」升级为「一个草稿下多条研讨 run」。
## 现状
相关位置:
- `src/roundtable-planning/api/drafts.ts`
- `src/roundtable-planning/pages/RoundtablePlanningPage.tsx`
- `src/roundtable-planning/hooks/useStep2Orchestration.ts`
- `src/roundtable-planning/components/Step2Panel.tsx`
- `src/roundtable-planning/components/RecommendAgentsDialog.tsx`
当前 `DraftStep2Snapshot` 结构大致是:
```ts
export interface DraftStep2Snapshot {
selectedAgents?: Array<{ agent_id: string; name: string }>;
threadIds: ThreadIdMap | null;
coordinatorName: string | null;
step2RoundtableDialogues: unknown[];
lastLeaderContent: string;
hasConsensus: boolean;
consensusPercentage: number;
budgetLimit: number;
seatStubMessages?: Record<string, unknown[]>;
orchestrationMode?: "recommend" | "chain";
chain?: { id: string; title: string } | null;
}
```
这意味着一个草稿只有一份:
- 参会智能体
- 多智能体 thread 映射
- 研讨气泡
- 总控最终内容
- 共识状态
- 沙箱产物 stub
- 编排模式和业务链条
当前 `RoundtablePlanningPage.tsx` 里的 `handleConfirmAgents` 在用户点击「进入研讨」后会:
1. 规范化选中的 agents。
2. 写入 `step2.selectedAgents`。
3. 调用 `step2.resetInitGate()`。
4. 设置 `orchestrationMode` / `chainMeta`。
5. 切到 `currentStep = 2`。
它没有先把当前 Step2 结果归档成「第一次生成结果」,也没有为新的研讨创建独立 run。因此第二次研讨天然会复用同一个 `draft.step2` 写入位置。
## 目标模型
建议引入 Step2 run 概念:一个草稿可以有多条 Step2 结果线,每条结果线保存一份完整的 Step2 快照。
```ts
interface Step2RunSnapshot {
id: string;
title: string;
createdAt: string;
source: "initial" | "recommend" | "chain" | "manual";
selectedAgents?: Array<{ agent_id: string; name: string }>;
threadIds: ThreadIdMap | null;
coordinatorName: string | null;
step2RoundtableDialogues: unknown[];
lastLeaderContent: string;
hasConsensus: boolean;
consensusPercentage: number;
budgetLimit: number;
seatStubMessages?: Record<string, unknown[]>;
orchestrationMode?: "recommend" | "chain";
chain?: { id: string; title: string } | null;
step3?: DraftStep3Snapshot | null;
}
```
`DraftStep2Snapshot` 保留现有字段作为当前激活 run 的镜像,同时新增 `runs` 和 `activeRunId`:
```ts
export interface DraftStep2Snapshot {
activeRunId?: string;
runs?: Step2RunSnapshot[];
selectedAgents?: Array<{ agent_id: string; name: string }>;
threadIds: ThreadIdMap | null;
coordinatorName: string | null;
step2RoundtableDialogues: unknown[];
lastLeaderContent: string;
hasConsensus: boolean;
consensusPercentage: number;
budgetLimit: number;
seatStubMessages?: Record<string, unknown[]>;
orchestrationMode?: "recommend" | "chain";
chain?: { id: string; title: string } | null;
}
```
这样做的好处:
- 老代码仍然可以从 `draft.step2.step2RoundtableDialogues` 读取当前结果。
- 新 UI 可以从 `draft.step2.runs` 展示第一次、第二次、第三次生成结果。
- 老草稿没有 `runs` 时,可以在 hydrate 时把原来的 `step2` 包装成一条默认 run。
- 暂时不需要改数据库表结构,因为 `step2` 本身就是 JSON 快照。
## 行为设计
### 1. 历史记录加载
用户点击历史记录加载草稿时:
1. 拉取完整草稿。
2. 如果 `draft.step2.runs` 存在,使用 `activeRunId` 找到当前结果线。
3. 如果 `draft.step2.runs` 不存在,但旧字段里有 `step2RoundtableDialogues`,自动包装成一条 run:
```ts
{
id: "legacy-run",
title: "第一次生成结果",
createdAt: draft.updatedAt,
source: "initial",
...draft.step2
}
```
4. hydrate 当前 active run 到 Step2 runtime。
5. 左下角展示 run 列表。
### 2. 再次点击智能体推荐进入研讨
用户在已有历史草稿中再次点击「智能体推荐」并确认进入研讨时:
1. 先把当前页面里的 Step2 状态保存为当前 active run。
2. 创建一条新的 run:
```ts
{
id: crypto.randomUUID(),
title: `第 ${runs.length + 1} 次生成结果`,
createdAt: new Date().toISOString(),
source: result.mode,
selectedAgents: normalizedAgents,
threadIds: null,
coordinatorName: null,
step2RoundtableDialogues: [],
lastLeaderContent: "",
hasConsensus: false,
consensusPercentage: 0,
budgetLimit: DEFAULT_BUDGET,
seatStubMessages: {},
orchestrationMode: result.mode,
chain: result.mode === "chain" ? result.chain ?? null : null
}
```
3. 将 `activeRunId` 切到新 run。
4. 清空 Step2 runtime。
5. 设置新选中的 agents / mode / chain。
6. `resetInitGate()`,进入 Step2 后重新创建一组独立 thread。
7. 自动保存草稿。
关键点:第二次 run 必须使用新的 `threadIds`,不能沿用第一次 run 的 `threadIds`,否则后端 thread 历史会混在一起。
### 3. 左下角结果列表
左下角建议展示一个「生成结果」区域:
- 第一次生成结果
- 第二次生成结果
- 第三次生成结果
每条展示:
- 标题
- 创建时间
- 状态:研讨中 / 已达成共识 / 未完成 / 已生成报告
- 使用模式:智能体推荐 / 业务链条
点击某条结果时:
1. 保存当前 active run。
2. 切换 `activeRunId`。
3. hydrate 被点击的 run。
4. 中间 Step2 对话区显示该 run 的研讨气泡。
5. 如果该 run 已有 `step3`,进入 Step3 时展示对应报告。
### 4. Step3 结果归属
目前 `draft.step3` 是草稿级别的一份报告。如果只保留这一份,第二次结果生成报告后仍会覆盖第一次报告。
建议:
- `draft.step3` 继续作为当前 active run 的镜像,兼容现有 Step3 页面。
- 每条 `Step2RunSnapshot.step3` 保存自己的报告。
- `onReportSaved` 时同时写入当前 active run 的 `step3`。
这样用户切换到第一次生成结果时,可以看到第一次的 Step3 报告;切换到第二次生成结果时,可以看到第二次的 Step3 报告。
## 建议改动点
### 1. `api/drafts.ts`
扩展类型:
- 新增 `Step2RunSnapshot`。
- `DraftStep2Snapshot` 增加 `activeRunId?: string` 和 `runs?: Step2RunSnapshot[]`。
同时保留旧字段,避免影响现有保存、加载和 Step3。
### 2. `useStep2Orchestration.ts`
新增或暴露两个能力:
- `getCurrentRunSnapshot()`:从当前 Step2 runtime 生成一条 run 快照。
- `hydrateRunSnapshot(run)`:把某条 run 恢复到 Step2 runtime。
`hydrateFromDraft` 继续存在,但内部可以优先选择 active run 来 hydrate。
### 3. `RoundtablePlanningPage.tsx`
新增页面级状态:
```ts
const [step2Runs, setStep2Runs] = useState<Step2RunSnapshot[]>([]);
const [activeStep2RunId, setActiveStep2RunId] = useState<string | null>(null);
```
调整几个流程:
- `getStep2Snapshot` 输出 `runs` 和 `activeRunId`。
- `hydrateStep2` 兼容旧草稿并恢复 runs。
- `handleConfirmAgents` 在新建研讨前先归档当前 active run,再创建新 run。
- `onReportSaved` 把 Step3 报告写回当前 active run。
### 4. `Step2Panel.tsx`
新增左下角结果列表展示。建议作为一个独立组件,避免 Step2Panel 继续变大:
```tsx
<Step2RunSwitcher
runs={step2Runs}
activeRunId={activeStep2RunId}
onSelect={handleSelectStep2Run}
/>
```
组件职责只做展示和选择,不直接读写 draft。
### 5. `Step3Panel.tsx`
传入当前 active run 的报告:
- 如果当前 run 有 `step3`,展示它。
- 如果没有,按现有逻辑生成新报告。
- 生成完成后通过 `onReportSaved` 写回 run。
## 兼容策略
老草稿没有 `runs` 字段时,不做数据迁移,前端加载时即时包装。
判断规则:
```ts
function normalizeStep2Runs(step2: DraftStep2Snapshot): {
runs: Step2RunSnapshot[];
activeRunId: string | null;
} {
if (step2.runs?.length) {
return {
runs: step2.runs,
activeRunId: step2.activeRunId ?? step2.runs[0].id,
};
}
const hasLegacyContent =
step2.step2RoundtableDialogues?.length > 0 ||
Boolean(step2.threadIds) ||
Boolean(step2.lastLeaderContent);
if (!hasLegacyContent) {
return { runs: [], activeRunId: null };
}
const legacyRun: Step2RunSnapshot = {
id: "legacy-run",
title: "第一次生成结果",
createdAt: new Date().toISOString(),
source: step2.orchestrationMode ?? "initial",
selectedAgents: step2.selectedAgents,
threadIds: step2.threadIds,
coordinatorName: step2.coordinatorName,
step2RoundtableDialogues: step2.step2RoundtableDialogues,
lastLeaderContent: step2.lastLeaderContent,
hasConsensus: step2.hasConsensus,
consensusPercentage: step2.consensusPercentage,
budgetLimit: step2.budgetLimit,
seatStubMessages: step2.seatStubMessages,
orchestrationMode: step2.orchestrationMode,
chain: step2.chain,
};
return { runs: [legacyRun], activeRunId: legacyRun.id };
}
```
## 风险点
### 1. 自动保存覆盖
Step2 现在会在每个气泡收尾时自动保存。如果引入多 run,自动保存必须先把当前 runtime 合并回 active run,再写入 `draft.step2.runs`。
否则会出现 UI 切到第二次结果,但保存时仍更新第一次 run 的情况。
### 2. threadIds 混用
每条 run 必须拥有自己的 `threadIds`。新建 run 时要清空 `threadIds` 并让 Step2 重新 init。
如果复用旧 thread,会导致两次研讨的后端上下文互相污染。
### 3. Step3 报告覆盖
如果只保存草稿级 `step3`,多 run 的报告无法分别查看。
建议把报告写入当前 run,同时保留草稿级 `step3` 作为 active run 镜像。
### 4. 正在运行中的 run
如果配合后台任务能力,run 还需要带 job 信息:
```ts
jobId?: string | null;
jobStatus?: JobStatus | null;
```
这样左下角可以显示「第二次生成结果正在运行」,并打开派活链路弹窗。
## 分阶段实现建议
### P0:纯前端多 run 快照
目标:先解决“第一次 / 第二次结果分别可查看”。
- 扩展 `DraftStep2Snapshot` 类型。
- 页面维护 `step2Runs` / `activeStep2RunId`。
- `handleConfirmAgents` 新建 run。
- 左下角展示 run 列表。
- 切换 run 能恢复不同 Step2 对话。
不改后端表结构。
### P1:Step3 绑定 run
目标:每次研讨结果对应自己的报告。
- `Step2RunSnapshot.step3` 保存报告。
- `onReportSaved` 写回 active run。
- 切换 run 时同步 Step3 显示。
### P2:后台任务绑定 run
目标:和后台挂起执行方案打通。
- run 增加 `jobId` / `jobStatus`。
- 历史列表和左下角都能显示某条 run 正在执行。
- 点击正在执行的 run 打开派活链路弹窗。
### P3:后端 run 表
如果后续一个草稿下 run 数量很多,或者需要服务端按 run 查询、删除、重命名,再考虑新增后端表:
- `roundtable_runs`
- `draft_id`
- `run_id`
- `title`
- `status`
- `step2_snapshot`
- `step3_snapshot`
- `created_at`
- `updated_at`
短期不建议一开始就上表,先用现有 `step2` JSON 快照落地更快。
## 验收清单
- 从历史记录加载旧草稿时,左下角出现「第一次生成结果」。
- 点击「第一次生成结果」能看到原来的 Step2 研讨气泡。
- 在历史草稿中重新点击「智能体推荐」并「进入研讨」后,左下角新增「第二次生成结果」。
- 第二次研讨开始时,Step2 对话区为空或只显示新 run 初始化内容。
- 第二次 run 使用新的 `threadIds`,不会复用第一次 run 的后端线程。
- 切回「第一次生成结果」时,第一次的对话、共识状态、沙箱产物仍可查看。
- 切回「第二次生成结果」时,第二次的对话继续显示。
- 刷新页面后再次加载该历史草稿,两条结果线都还在。
- 如果两次都生成了 Step3 报告,切换不同结果线时能看到各自对应的报告。
@@ -0,0 +1,198 @@
# 后台挂起「在当前进度继续」实现方案
> 状态:**待实现**(仅方案,未改代码)
> 关联:[[multi-agent-background-run-dev.md]]、[[multi-agent-frontend-dev.md]]、memory `project-roundtable-background`
> 目标:点「后台挂起」后,后端作业**在前端当前研讨进度上接着跑**,而不是新建空线程从头重跑。
---
## 1. 现象与根因
### 现象
在第二步研讨进行中点「后台挂起」,转入后台的作业是**从头重新开始执行**(重新初始化席位、从原始 intent 重新派活),而不是接着前端已经讨论到的进度往下走。
### 根因
后端「起作业」路由 `start_job` **从不设 `is_resume`**(默认 `False`),而执行器据此**新建一套全新线程**:
```python
# offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py (_run, 约 215 行)
init = getattr(gateway, "init_threads", None)
if init is not None and not params.is_resume:
thread_ids, coordinator_name = await init() # ← 建全新空线程
```
加上前端 `startBackgroundJob` **故意不传 `threadIds`**、用 `buildInitialIntent()` 作第一轮 leader prompt,于是后台作业 = 空线程 + 原始意图 = 从头重跑。
> 这是**原设计的有意取舍**。`startBackgroundJob` 注释写明:不复用前端线程,因为「那些线程可能有活跃 run,复用会撞 409」。本方案就是要在可控前提下打开「复用前端线程续跑」这条路。
### 后端其实已支持续跑
澄清续跑路由 `resume_job`(`roundtable_jobs.py` 约 293 行)已经在做「在原线程上接着派活」:
```python
executor.start_job(StartParams(
...,
seed_message=_RESUME_PREFIX + answer, # 续跑式提示,而非原始 intent
thread_ids=row.get("thread_ids"), # 复用作业自己的线程
coordinator_name=row.get("coordinator_name") or "roundtable-coordinator",
is_resume=True, # ← 关键:跳过 init_threads,直接复用
...
))
```
执行器在 `is_resume=True` 时直接用 `params.thread_ids` + `params.coordinator_name`(不调 `init_threads`),交给 `run_orchestration` 续跑。**所以能力已存在,`start_job` 只是没走这条路。**
---
## 2. 设计目标与关键事实
- 「同进度续跑」= 后台作业复用**前端当前会话的真实线程**(总控 + 各席位),这些线程的 checkpoint 里已存着到目前为止的完整研讨上下文,续跑 = 在原线程上 post 一条「继续」消息让总控接着派活。
- 前端编排是**逐轮**的:每轮一个 `POST /api/multi-agent/run/stream`(leader 或某席位),**轮与轮之间线程空闲**。只有「某一轮在飞时」线程才被占用。
- 后端复用现成线程是**已验证路径**(`resume_job` 天天在用),唯一新增的不同点是:这次复用的是「前端会话线程」而非「上一个后台作业的线程」——但线程同属一个后端线程库,复用方式完全一致。
---
## 3. 改造点(最小集)
### 3.1 后端:让 `start_job` 支持复用线程
文件:`offline-backend-20260512/backend/app/gateway/routers/roundtable_jobs.py`
1. `StartJobRequest` 增加字段:
```python
resume: bool = False # true + 携带 threadIds ⇒ 复用这些线程续跑,不新建
```
2. `start_job` 里把它透传给 `StartParams`:
```python
is_resume = bool(body.resume and body.threadIds)
executor.start_job(StartParams(
...,
thread_ids=body.threadIds,
coordinator_name=body.coordinatorName,
is_resume=is_resume, # ← 新增
...
))
```
其余执行器逻辑**不动**(它在 `is_resume=True` 时已能复用 `params.thread_ids`)。
> 幂等闸门(同草稿未终态作业直接返回)保持不变;续跑挂起前前端应保证该草稿没有在跑的旧作业。
### 3.2 前端 API:补 `resume` 入参
文件:`frontend-web/src/roundtable-planning/lib/job-types.ts`
- `StartJobPayload` 增加 `resume?: boolean`。
文件:`frontend-web/src/roundtable-planning/api/roundtable-jobs.ts`
- `toStartBody` 增加 `resume: p.resume ?? false`。
### 3.3 前端:`startBackgroundJob` 改为复用线程续跑
文件:`frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx`(`startBackgroundJob` 约 1119 行)
当前端已有现成会话(`step2.threadIds` 且 `step2.coordinatorName` 非空,说明研讨已 init)时,改传:
```ts
const canContinue = !!(step2.threadIds && step2.coordinatorName);
await startJob({
draftId: draftId ?? undefined,
// 续跑:用「继续」式 seed,而不是 buildInitialIntent()(后者会让总控从头理解任务)
intent: buildInitialIntent(), // 仍传,作为无线程回退时的首轮 prompt
seedMessage: canContinue
? "用户已将本次研讨转入后台,请基于现有进展继续上一轮的协调与派活;若已可收口则直接综合输出结论。"
: undefined,
agents,
model: selectedModel || undefined,
orchestrationMode,
chain: chainMeta,
orchestrationPlan: orchestrationMode === "dag" ? orchestrationPlan : null,
// 关键三件套:复用前端真实线程 + 协调名 + resume 标志
threadIds: canContinue ? step2.threadIds ?? undefined : undefined,
coordinatorName: canContinue ? step2.coordinatorName ?? undefined : undefined,
resume: canContinue,
});
```
> 没有现成会话(极早期、线程未建)时 `canContinue=false`,自动回退到「新建线程 + 原 intent」的旧行为,安全。
### 3.4 前端:挂起前先停本地编排(避免交出去时线程还在飞)
文件:同上,`handleBackgroundSuspend`(约 1161 行)
当前顺序是 `startBackgroundJob()` → `resetToStep1()`(reset 里才 `stopOrchestration`)。复用线程时必须**先停再交**:
```ts
const handleBackgroundSuspend = useCallback(async () => {
if (step2.selectedAgents.length === 0) { /* toast 提示后 return */ }
// 1. 先彻底停掉前端在飞的那一轮,尽量让线程在交给后台前变空闲
step2.stopOrchestration();
step2.setIsRoundTableRunning(false);
try {
// 2. 再起后台作业(复用线程续跑)
await startBackgroundJob();
} catch (err) { /* toast 失败后 return */ }
// 3. 最后重置回第一步(后台作业继续在历史记录里跑)
resetToStep1();
triggerToast("🚀 已转入后台,将基于当前进度继续研讨…");
void drafts.refresh();
}, [...]);
```
---
## 4. 关键风险:线程占用(409)与三种处理
复用前端线程的唯一风险:**在某一轮在飞时挂起**,`stopOrchestration()` 只断了客户端 SSE,服务端那次 run 可能还在跑一小会儿;后台作业首轮 leader 向同一总控线程 post → 撞 409 / rollback。
| 方案 | 做法 | 取舍 |
|---|---|---|
| **A(推荐起步)** | 仅前端 stop + 后端 `is_resume` 复用,接受小概率 409 | 实现最简,真正同进度续跑;轮间挂起 100% 安全,仅「某轮在飞时挂起」有小概率 409 —— 走编排循环**现有 rollback/重试容错**兜底 |
| **B(最稳)** | 同 A,但后端在复用线程续跑前,**先 cancel 这些线程上可能残留的 run**(参考 takeover 里 `cancelJob` 的 `task.cancel()` 思路,落到 thread-run 级别)再 post | 几乎无 409;后端要多写「按 thread_id 取消活跃 run」的逻辑 |
| **C(不推荐)** | 不复用线程,新建空线程,但把「目前为止的研讨」塞进 seed_message 让总控续写 | 零 409,但**席位子线程上下文全丢**、不是真正同进度、leader prompt 臃肿 |
> 建议:先上 **A**,真机验证 409 命中率;若频繁再加 **B** 的后端线程级 cancel。
---
## 5. 适用范围(哪些挂起入口改续跑)
三个触发源(见 `autoSuspendRef`):
1. **手动「后台挂起」按钮**(`handleBackgroundSuspend`)——**改续跑**。能在 start 前干净 stop,最适合复用线程。
2. **路由卸载自动挂起**(切到其它页面,`fire`)——可改续跑(能走完整异步链);卸载瞬间 stop 不一定彻底,按 A 方案的容错兜底。
3. **刷新/关标签页 keepalive**(`beforeunload` 的 `beacon` → `startJobKeepalive`)——**建议保持新建线程**。关页时无法保证前端 run 已停,复用线程 409 风险最高;这里走「新建线程 + 原 intent」的安全回退即可(用户可在历史记录里点「查看」再用我们已实现的 takeover 接管回前端续跑)。
> 落地顺序建议:先只改 #1,验证稳定后再决定是否推广到 #2。
---
## 6. 与已实现「查看=接管回前端」的关系
二者是一对对称操作,复用同一套「线程同属一个后端库、可跨前后端续跑」的事实:
- **后台挂起(本方案)**:前端 → 后端。前端 stop,把前端线程交给后台 `is_resume` 续跑。
- **查看接管(已实现,见 memory)**:后端 → 前端。`cancelJob` 停后台,`hydrateFromDraft` 恢复作业真实线程,`resumeLiveTakeover()` 由前端在那些线程上续跑。
实现本方案时可直接参照 takeover 的线程复用与 `is_resume` 经验。
---
## 7. 测试
后端(`make test`):
- 新增/扩展 `tests/test_roundtable_jobs.py`:`start_job` 带 `resume=true + threadIds` ⇒ `StartParams.is_resume=True` 且**不调** `init_threads`、直接用传入线程;`resume=false` 或无 `threadIds` ⇒ 走 `init_threads` 新建(回归旧行为)。
- 回归:`tests/test_roundtable_inprocess_request.py`、`tests/test_roundtable_step3_pipeline.py`(确认续跑不破坏 Step3 双产物)。
前端(`pnpm typecheck`)+ 真机:
- 研讨进行到几轮后点「后台挂起」→ 历史记录里点「查看」→ 确认后台是**接着已有对话**往下派活,而非从第 1 轮重来。
- 轮间挂起 / 某轮在飞时挂起两种时机各验一次,观察是否出现 409(A 方案下应被 rollback 容错吸收)。
---
## 8. 落地清单(TL;DR)
- [ ] 后端 `StartJobRequest.resume` 字段 + `start_job` 透传 `is_resume`
- [ ] 前端 `StartJobPayload.resume` + `toStartBody`
- [ ] 前端 `startBackgroundJob`:有会话时传 `threadIds/coordinatorName/seedMessage/resume`
- [ ] 前端 `handleBackgroundSuspend`:调整为 **先 stop 再 start 再 reset**
- [ ] 后端测试(resume 复用 vs 新建分支)
- [ ] 真机验证「接着进度跑」+ 409 命中率
- [ ](可选)方案 B:后端复用线程前按 thread_id 取消残留 run
@@ -0,0 +1,362 @@
# 岗位会商交付闭环与内置智能体实现计划
> 页面:`/page/strategy/qa/position-roundtable`
>
> 目标:在现有岗位会商基础上补齐“产物交付、查看、驳回、方案总结、行动规划”的完整闭环,同时继续保持原多智能体会商页面不受影响。
## 实施核对(2026-07-21)
本计划已按“岗位会商独立路由、无总控、各席位直接问答”的边界完成第一轮闭环。实际流程调整为:
```text
任务信息 → 情报分析岗识别并确认意图 → 启动校验并冻结业务链
→ 分阶段席位直接问答/交付 Markdown → 全部有效交付
→ 方案总结智能体(roundtable-summary)→ 行动规划智能体(position-action-planner)
→ 可随时返回交付产物驳回 → 后续阶段及内置产物级联失效 → 重做后重新收口
```
- [x] 情报意图、业务链选择、席位岗位归属和席位智能体配置均在启动前校验;启动后保存业务链快照。
- [x] 普通席位直接调用各自分配的智能体,并注入任务、意图及已完成上游席位的问答/产物;不经过总控智能体。
- [x] 普通席位必须在 `outputs` 中交付 Markdown 才会完成;情报分析岗以确认的结构化意图完成为准。
- [x] 节点、交付卡片、流程图和大图弹窗均展示 `locked / ready / running / done / rejected / stale / error` 状态,并可回看节点会话和产物。
- [x] 驳回要求标题和理由;当前节点转为 `rejected`,后续阶段按依赖转为 `stale/locked`,总结和行动规划同时失效,历史内容保留可查看。
- [x] 被驳回或失效的普通席位必须写入一份**新的或已改写** Markdown 才能再次完成;单纯继续问答且没有新交付不会误使下游产物失效。
- [x] 方案总结复用 `roundtable-summary` 的真实流式线程;行动规划新增 `position-action-planner`,强制输出 Markdown 报告和 `action-plan-subtasks.json`。
- [x] 两个内置智能体的线程、产物快照和来源节点版本栅栏均持久化;流式期间若上游版本变化,旧运行不能覆盖最新状态。
- [x] 右侧“交付产物”可查看内置节点结果、继续同一线程问答/修改交付,并在打开 Markdown 沙箱时自动收起右栏。
设计收敛:当前版本以节点的 `artifact_manifest`、`latest_answer`、版本、驳回记录及 Session 内置快照作为唯一事实来源,已能覆盖统一产物卡片和级联逻辑,因此不新增重复的 `position_roundtable_deliverables` 表。若后续需要“单个文件独立驳回、跨会话检索或完整版本对比”,再将清单归一化为独立产物表,避免现在的双写一致性风险。
## 数据持久化与灾难恢复(2026-07-23)
岗位会商现在使用“两层持久化”,两层职责不同:
1. LangGraph checkpoint/thread 是智能体继续推理与流式问答的执行源。
2. `position_roundtable_sessions` / `position_roundtable_nodes` 是页面历史的数据库恢复源。
数据库恢复源会保存:
- 情报分析岗完整消息、步骤时间线和未发送输入草稿;
- 每个业务链岗位的完整消息和未发送输入草稿;
- 方案总结、行动规划两个收口智能体的完整消息和未发送输入草稿;
- 已确认意图、冻结任务与业务链快照、节点状态、版本、驳回记录;
- Markdown / JSON 等文本产物正文(单文件最多 400 万字符)和文件元数据。
因此即使线上重启后 checkpoint 或沙箱 outputs 卷丢失,历史页仍能从数据库展示对话与产物正文,后续收口智能体也能继续读取数据库中的有效上游交付。新任务在意图识别开始前就会创建 Session,用户不必等到“启动业务链”才获得持久化保护。历史下拉支持删除,删除会显式清理 Session 与全部节点恢复快照,不依赖 SQLite 外键开关。
对应迁移:`20260723_05_position_roundtable_conversations.py`。部署时必须执行到 Alembic head。
## 0. 核心结论
- 方案总结智能体建议复用现有会商里的 `roundtable-summary` 能力,但不要直接搬整套 Step 3 页面流程。
- 可复用:内置智能体 ID、总结报告 prompt 组织方式、Markdown 报告产物、流式对话、报告续问与文件预览能力。
- 需要改造:输入材料从“总控共识 + 各席位交付”改成“任务信息 + 意图结果 + 已通过的岗位产物 + 业务链快照”,并把结果存到岗位会商 Session 下。
- 行动规划智能体建议作为新的内置智能体新增。
- 输入:方案总结报告、各岗位最终有效产物、业务链阶段信息、任务意图。
- 输出:行动规划报告、结构化子任务清单。
- 这个模式仍然不需要总控智能体。情报分析岗负责意图识别,业务链节点直接调用各自配置的智能体,最终再由内置总结/规划智能体收口。
- 驳回不删除历史内容,只改变“当前有效版本”的状态;被驳回节点和后续节点需要重新交付,既保留可追溯性,也方便后续返回查看。
## 1. 目标业务流程
1. 情报分析岗进入页面,查看任务信息并完成意图识别。
2. 用户点击“确认意图并启动业务链”。
3. 启动前做校验:
- 未选择业务链:提示“请先选择业务链”。
- 业务链存在未配置岗位的席位:提示并引导去业务链配置页。
- 业务链席位缺少智能体:提示具体席位名称。
- 意图尚未完成:提示先完成任务意图识别。
4. 校验通过后冻结业务链快照,初始化岗位会商节点。
5. 各岗位只对分配给本岗位的智能体进行问答。
6. 节点问答完成后形成交付产物,右侧产物卡片显示“已完成 / 未完成 / 进行中 / 已驳回 / 已失效”。
7. 左侧流程图显示各节点状态,节点可点击查看该节点对话与产物。
8. 流程图右上角提供“查看”按钮,打开弹窗展示更完整的业务链流程图。
9. 当业务链中所有岗位产物都处于有效已交付状态后,开放“方案总结”。
10. 方案总结完成后开放“行动规划”。
11. 即使进入方案总结或行动规划,用户仍可返回产物列表执行驳回。
12. 若驳回早期阶段产物,后续阶段产物、方案总结、行动规划都一起失效并回到待重新完成状态。
## 2. 状态设计
### 2.1 节点状态
现有节点状态需要扩展或映射为更贴近页面的展示状态:
| 展示状态 | 建议内部状态 | 含义 |
| --- | --- | --- |
| 未完成 | `ready` / `locked` / `rejected` | 尚未有效交付,或被驳回后等待重做 |
| 进行中 | `running` | 当前智能体正在回答或产物正在生成 |
| 已完成 | `done` | 当前节点已有有效交付 |
| 已失效 | `stale` | 上游被更新或驳回,当前结果不可作为最终材料 |
| 异常 | `error` | 生成或保存失败 |
建议新增 `rejected` 状态,便于区分“从未完成”和“被驳回后待重做”。如果第一轮想少改后端,也可以先用 `ready + rejection_record` 表示被驳回。
### 2.2 产物状态
产物状态建议独立于节点状态保存,因为一个节点可能同时有回答文本和多个文件产物。
| 状态 | 含义 |
| --- | --- |
| `pending` | 还没有交付 |
| `running` | 正在生成 |
| `delivered` | 已交付且当前有效 |
| `rejected` | 被用户驳回 |
| `stale` | 受上游驳回或重做影响,已经失效 |
### 2.3 内置收口节点
在流程图中把两个内置节点追加到业务链末尾:
1. `builtin:summary`:方案总结智能体。
2. `builtin:action-plan`:行动规划智能体。
这两个节点不属于普通业务链席位,但要和普通节点一样显示状态、支持点击查看结果。
## 3. 数据模型调整
### 3.1 Session 扩展
`position_roundtable_sessions` 已使用下列持久化字段:
| 字段 | 含义 |
| --- | --- |
| `status` | `intent_pending / active / completed / archived`;总结/规划运行态由前端瞬时状态与对应快照的 `status` 表示,避免一次运行中断后留下伪运行态 |
| `summary_thread_id` | 方案总结智能体线程 |
| `summary_snapshot` | 最新有效总结报告快照 |
| `action_plan_thread_id` | 行动规划智能体线程 |
| `action_plan_snapshot` | 最新有效行动规划快照 |
| `invalidated_at` | 记录在 `summary_snapshot` / `action_plan_snapshot` 内,包含 `invalidated_by`,无需重复写 Session 字段 |
### 3.2 Node 扩展
`position_roundtable_nodes` 已使用下列字段(展示状态由前端按状态映射):
| 字段 | 含义 |
| --- | --- |
| `status` | `locked / ready / running / done / stale / rejected / error`,前端直接映射为展示状态 |
| `rejection_count` | 当前节点累计驳回次数 |
| `last_rejection` | 最新驳回标题、理由、操作者、时间 |
| `revision` | 当前有效交付版本 |
| `invalidated_by` | 导致当前节点失效的上游节点 key |
### 3.3 Deliverable 新表(后续可选)
当需要“单个文件独立驳回、跨会话检索或完整版本对比”时,再新增 `position_roundtable_deliverables`,不要只依赖 `artifact_manifest`。
| 字段 | 含义 |
| --- | --- |
| `id` | 产物 ID |
| `session_id` | 会话 ID |
| `node_key` | 来源节点 |
| `agent_id` | 来源智能体 |
| `position_id` | 来源岗位 |
| `title` | 产物名称 |
| `kind` | `answer / file / summary / action_plan` |
| `content` | 文本产物内容摘要或正文 |
| `artifact_path` | 文件产物虚拟路径 |
| `thread_id` | 所属线程 |
| `status` | `pending / running / delivered / rejected / stale` |
| `revision` | 来源节点版本 |
| `rejection_title` | 驳回标题 |
| `rejection_reason` | 驳回理由 |
| `created_at` | 创建时间 |
| `updated_at` | 更新时间 |
当前第一轮不新增这张表:右侧产物卡片、驳回记录、总结输入材料从节点 `artifact_manifest`、节点版本/驳回记录和 Session 快照聚合,避免节点表与产物表双写。后续引入该表时,应把它提升为唯一事实来源,而不是并行维护两套状态。
## 4. 驳回与级联失效规则
### 4.1 基本驳回
用户点击产物卡片的“驳回”后弹窗填写:
- 驳回标题。
- 驳回理由。
提交后:
- 当前产物状态改为 `rejected`。
- 当前节点状态改为 `rejected` 或 `ready`。
- 当前节点的 `latest_answer` 和历史文件不删除,但不再作为有效交付输入。
- 记录 `last_rejection`,用于页面展示和后续追踪。
### 4.2 级联失效
如果驳回的节点位于第 N 阶段:
- 同阶段其他节点不自动驳回,除非业务链未来明确配置了依赖关系。
- 第 N+1 阶段及之后所有节点的有效产物全部标记为 `stale`。
- 后续节点状态回到未完成展示态。
- `builtin:summary` 和 `builtin:action-plan` 如果已经生成,也标记为 `stale`。
- 页面允许用户继续查看旧报告和旧行动规划,但必须提示“上游产物已变更,需要重新生成”。
### 4.3 重新交付
被驳回节点重新问答并完成后:
- 当前节点产生新的 `revision`。
- 当前节点产物状态变为 `delivered`。
- 只有当该阶段所有必要节点都重新有效交付后,下一阶段才重新解锁。
- 方案总结和行动规划必须基于最新有效产物重新生成。
## 5. 接口规划
### 5.1 启动与校验
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `POST` | `/api/position-roundtable/sessions/{id}/validate-activation` | 校验业务链、岗位、智能体、意图状态 |
| `POST` | `/api/position-roundtable/sessions/{id}/activate` | 校验通过后冻结链条并初始化节点 |
第一轮也可以先把校验合并在 `activate` 内,前端根据后端返回的错误码提示。
### 5.2 产物
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/position-roundtable/sessions/{id}/results` | 获取右侧统一产物来源节点(前端聚合文件与内置快照) |
| `GET` | `/api/position-roundtable/sessions/{id}/nodes/{node_key}` | 获取节点详情;文件正文由既有线程产物预览接口读取 |
| `POST` | `/api/position-roundtable/sessions/{id}/nodes/{node_key}/reject` | 驳回当前节点交付并触发级联失效 |
| `POST` | `/api/position-roundtable/sessions/{id}/nodes/{node_key}/complete-turn` | 节点回答结束后同步回答与产物 |
### 5.3 方案总结与行动规划
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `POST` | `/api/position-roundtable/sessions/{id}/summary/prepare` | 服务端准备版本锁定的总结材料和线程信息 |
| `POST` | `/api/position-roundtable/sessions/{id}/summary/ask` | 服务端准备总结的同线程续问材料 |
| `POST` | `/api/position-roundtable/sessions/{id}/summary/complete` | 校验 Markdown 交付与来源版本后持久化总结快照 |
| `POST` | `/api/position-roundtable/sessions/{id}/action-plan/prepare` | 服务端准备行动规划材料和线程信息 |
| `POST` | `/api/position-roundtable/sessions/{id}/action-plan/ask` | 服务端准备行动规划同线程续问材料 |
| `POST` | `/api/position-roundtable/sessions/{id}/action-plan/complete` | 校验 Markdown + JSON 交付与来源版本后持久化行动规划快照 |
可以复用现有 `streamMultiAgent` 流式协议,但建议在岗位会商下包一层 API,由后端负责拼装岗位会商材料,前端不直接拼长 prompt。
## 6. 前端实施步骤
### 阶段 1:状态与启动校验(已完成)
- 扩展 `PositionNodeStatus` 和状态显示映射。
- “确认意图并启动业务链”前补齐校验提示。
- 后端 `activate` 返回未配置岗位、缺智能体、未选择链条等具体错误。
- 左侧流程图节点右上角添加状态小图标。
- 节点样式调整为上方智能体名称、下方岗位名称,移除左侧 logo。
验收标准:未选择业务链、岗位未配齐、意图未完成时都能准确阻止启动;启动后流程图能显示各节点状态。
### 阶段 2:流程图弹窗(已完成)
- 左侧保留小型从上到下流程图。
- 流程图右上角增加“查看”按钮。
- 点击后打开弹窗,展示更大的流程图。
- 弹窗内支持点击节点查看对话与产物,保持和主页面选中节点同步。
验收标准:小图不挤占左侧空间,大图能完整展示业务链和两个内置收口节点。
### 阶段 3:产物卡片与预览(已完成)
- 右侧新增或改造“分析结果”Tab 为“交付产物”列表。
- 卡片展示智能体名称、产物名称、岗位名称、时间、状态。
- 点击卡片打开详情弹窗。
- 文本产物直接展示正文,文件产物复用现有 `ArtifactPreviewModal`。
- 每个有效产物提供“驳回”入口。
验收标准:节点回答和文件产物能进入统一产物列表;点击可查看详情。
### 阶段 4:驳回弹窗与级联失效(已完成)
- 新增 `RejectDeliverableDialog`。
- 驳回表单包含标题和理由,均必填。
- 后端事务内完成当前产物驳回、当前节点状态更新、后续阶段级联失效、总结和行动规划失效。
- 前端刷新 Session、Node、Deliverable 和流程图状态。
- 被驳回节点在对话区显示驳回信息,提示用户重新交付。
验收标准:驳回第一阶段任意节点后,第二阶段及之后产物都显示未完成或已失效;旧内容可查看但不能作为总结输入。
### 阶段 5:方案总结智能体(已完成)
- 封装 `usePositionSummary`,内部复用 `roundtable-summary` 的线程初始化、流式输出和 Markdown 产物预览能力。
- 输入材料改为岗位会商材料:
- 任务信息。
- 情报分析岗意图结果。
- 业务链快照。
- 所有状态为 `delivered` 的岗位产物。
- 已驳回或失效产物只作为历史参考,不进入默认总结材料。
- 方案总结节点追加到流程图末尾。
- 所有产物有效交付后才允许点击“生成方案总结”。
- 总结结果写入 `summary_snapshot`,同时进入产物列表。
验收标准:总结报告使用现有方案总结能力生成 Markdown;刷新页面后可恢复;上游驳回后总结标记失效。
### 阶段 6:行动规划智能体(已完成)
- 新增内置智能体 ID,建议先命名为 `position-action-planner`。
- 新增行动规划 prompt:
- 读取方案总结报告。
- 读取各岗位最终有效产物。
- 拆解为若干子任务。
- 每个子任务包含名称、目标、输入依据、责任岗位建议、前置依赖、交付物、验收标准。
- 输出两类产物:
- `行动规划报告.md`。
- `action-plan-subtasks.json`。
- 行动规划节点追加到方案总结之后。
- 方案总结有效完成后才允许启动行动规划。
验收标准:行动规划能生成可读报告和结构化子任务;刷新可恢复;上游驳回后自动失效。
### 阶段 7:返回修改与重新生成(已完成)
- 用户在方案总结或行动规划视图中仍可切回“交付产物”。
- 驳回后自动显示需要重做的节点。
- 重做节点完成后,允许重新生成方案总结和行动规划。
- 旧总结、旧行动规划保留历史查看,但状态显示“已失效”。
验收标准:不会因为进入总结/规划阶段而锁死前序产物;前序重做后能形成新版本闭环。
### 阶段 8:测试与回归(已完成)
- 后端测试:
- activate 校验。
- 节点完成后产物入库。
- 驳回当前节点。
- 驳回第一阶段时后续阶段级联失效。
- 总结和行动规划失效。
- 前端测试:
- 启动校验提示。
- 状态图标显示。
- 产物卡片筛选和预览。
- 驳回弹窗必填校验。
- 回归验证:
- 原多智能体会商页面不受影响。
- 业务链普通配置入口不显示岗位配置。
- 岗位会商页面刷新恢复正常。
- 深色模式和嵌入模式布局不溢出。
## 7. 推荐提交拆分
1. `feat(position-roundtable): add delivery status model`
2. `feat(position-roundtable): validate chain activation`
3. `feat(position-roundtable): add workflow status graph`
4. `feat(position-roundtable): add deliverable cards and preview`
5. `feat(position-roundtable): support rejection cascade`
6. `feat(position-roundtable): add summary builtin node`
7. `feat(position-roundtable): add action planning builtin node`
8. `test(position-roundtable): cover delivery workflow`
## 8. 第一轮建议范围
第一轮优先做最小可闭环:
```text
意图识别
→ 启动校验
→ 业务链节点状态
→ 节点产物列表
→ 产物详情查看
→ 驳回与级联失效
→ 全部交付后生成方案总结
→ 基于总结生成行动规划
```
如果时间紧,行动规划可以先生成 Markdown 报告,结构化 `subtasks.json` 放到第二轮;但数据模型和 UI 入口第一轮就要预留,否则后面会返工。
@@ -0,0 +1,15 @@
# 岗位会商沙箱文档
本目录存放**岗位会商文件预览沙箱**的对接与排障说明,独立于 `src/position-roundtable/docs/`,避免与模块内其它文档互相覆盖。
| 文档 | 说明 |
| --- | --- |
| [沙箱打开关闭与步骤条.md](./沙箱打开关闭与步骤条.md) | **详细版**:打开/关闭完整时序、步骤条关系、对接接口字段说明、必守规则、历史 bug 根因、排障手册、自测清单、接线示例与代码索引 |
模块总览仍见:`frontend-web/src/position-roundtable/README.md`。
建议读法:
- **从零对接新页面**:正文第 3、5、6、7、8、12、15、16 节
- **线上排障**:第 13、14 节
- **只要一页纸**:文末「附录 A」
@@ -0,0 +1,988 @@
# 岗位会商 · 沙箱打开 / 关闭与步骤条(详细对接与排障指南)
> **文档位置**:`frontend-web/docs/position-roundtable-sandbox/`
> **读者**:下次重做或对接岗位会商页面的同事;线上「沙箱不实时 / 卡死 / 第二次不刷新」时的排障同学。
> **最后依据**:当前仓库 `PositionRoundtablePage` + `PositionNodeChat` 实现(含临时名打开、外部 store、前沿节流、按 tool-call 重挂)。
---
## 目录
1. [文档用途与读法](#1-文档用途与读法)
2. [背景:为什么岗位会商要单独做沙箱](#2-背景为什么岗位会商要单独做沙箱)
3. [用户可见的正确行为(验收标准)](#3-用户可见的正确行为验收标准)
4. [整体架构与数据流](#4-整体架构与数据流)
5. [职责划分:谁拥有什么状态](#5-职责划分谁拥有什么状态)
6. [对接接口详解](#6-对接接口详解)
7. [打开沙箱的完整步骤](#7-打开沙箱的完整步骤)
8. [关闭沙箱的完整步骤](#8-关闭沙箱的完整步骤)
9. [步骤条与沙箱的关系](#9-步骤条与沙箱的关系)
10. [流式内容如何进入沙箱(性能关键)](#10-流式内容如何进入沙箱性能关键)
11. [与普通对话页的对照](#11-与普通对话页的对照)
12. [重做页面时的必守规则](#12-重做页面时的必守规则)
13. [历史问题与根因(务必读)](#13-历史问题与根因务必读)
14. [排障手册(按症状)](#14-排障手册按症状)
15. [对接自测清单](#15-对接自测清单)
16. [最小接线示例](#16-最小接线示例)
17. [关键代码索引](#17-关键代码索引)
18. [术语表](#18-术语表)
---
## 1. 文档用途与读法
### 1.1 这份文档解决什么问题
岗位会商右侧「文件预览沙箱」看起来像普通对话页的产物预览,但实现路径完全不同。历史上这块反复出过:
- 沙箱不实时打开,要等全文写完才弹;
- 打开了但内容冻住,生成完才一口气灌进去;
- 整页卡顿,连消息列表文字都不实时;
- 同轮「重新生成」第二次写入时,步骤条变了,沙箱却不动;
- 第一次正常,关掉后再问就不实时。
这些 bug **特别容易在「重做页面 / 换布局 / 自己重写开沙箱逻辑」时复现**。本文把正确对接方式、时序、接口、禁区和排障步骤写全,方便下一位同事直接按文档接线,而不是再靠猜。
### 1.2 建议读法
| 你的目标 | 先读哪些章节 |
| --- | --- |
| 从零接一个新页面壳 | 第 3、5、6、7、8、12、15、16 节 |
| 只改对话子组件探测逻辑 | 第 7、9、12、13 节 |
| 线上排障 | 第 13、14 节,必要时回看第 10 节 |
| 理解为什么不能用页面 state | 第 2、10、13 节 |
### 1.3 对照代码(以仓库为准)
| 角色 | 路径 |
| --- | --- |
| 页面壳(唯一沙箱宿主) | `frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx` |
| 节点对话(探测 write_file、上报预览) | `frontend-web/src/position-roundtable/components/PositionNodeChat.tsx` |
| 收口对话 | `frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx` |
| 预览 UI(共享组件,勿分叉逻辑) | `frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx` |
| 流式正文读取 | `frontend-web/src/core/artifacts/loader.ts` → `loadArtifactContentFromToolCall` |
| 普通对话页对照(步骤条自动开) | `frontend-web/src/components/workspace/messages/message-group.tsx` |
| 模块总览 | `frontend-web/src/position-roundtable/README.md` |
---
## 2. 背景:为什么岗位会商要单独做沙箱
### 2.1 普通对话页怎么开沙箱
普通对话页(`ChatPage`)大致是:
```text
ArtifactsProvider(页面级)
└─ ChatBox / MessageList
└─ message-group 渲染 write_file 步骤
└─ 发现 path 且 autoOpen → artifacts.select + setOpen(true)
└─ 右侧 ArtifactFileDetail 读同一套 ThreadContext
```
要点:
- 对话和预览共享同一套 `ThreadContext`(同一个 `useThreadStream`)。
- 步骤条组件自己就能触发打开。
- 流式 `content` 本来就在当前 React 树的 thread 里,不需要「把 thread 镜像到别的地方」。
### 2.2 岗位会商为什么不能照搬
岗位会商页面同时有:
- 左侧任务 / 意图 / 流程图;
- 中间某个节点的对话(`PositionNodeChat`);
- 右侧配置抽屉;
- **以及一个页面级单例沙箱**,它是对话区的**兄弟节点**,不在 `PositionNodeChat` 的 ThreadContext 里面。
因此:
1. 沙箱拿不到节点对话内部的 `thread`,除非节点主动上报;
2. 若把每次流式 thread 写进**页面级 `useState`**,整页(任务卡、意图、流程图、对话)都会按流式频率重渲染,主线程会被打爆;
3. 页面必须自己决定「开 / 关 / 抑制自动再开 / 切节点时清理」,不能让多个子组件各自 `setOpen`。
所以岗位会商采用:
```text
对话子组件探测 + 上报
→ 页面壳统一打开唯一 ArtifactFileDetail
→ 流式 thread 走外部 store,只让沙箱子树订阅
```
### 2.3 和「多智能体会商 Step2 沙箱」的区别
仓库里还有 `frontend-web/docs/roundtable-step2-sandbox-progress.md`,那是另一套圆桌 Step2 沙箱进度文档,**不是**岗位会商这条链路。对接岗位会商时以本文为准。
---
## 3. 用户可见的正确行为(验收标准)
重做或对接完成后,下列行为必须全部成立(用户感知对齐普通对话页):
| # | 行为 | 说明 |
| --- | --- | --- |
| A | 步骤条刚出现「写入文件」时,沙箱应已打开 | **不必等**步骤条上出现文件名 |
| B | 沙箱源码边写边滚;消息列表也实时出字 | 不能整页卡死、不能「生成完一口气刷」 |
| C | content 先于 path 时:先临时名打开,path 到了再改名 | 不闪屏、不重挂 |
| D | 同轮第二次同路径 `write_file`(重新生成) | 沙箱清空并重新流式 |
| E | 整轮结束后自动切 Markdown 预览 | 「正在生成,完成后自动预览」消失 |
| F | 用户手动关闭后,同一次写入不再自动弹 | 下一轮新写入仍可自动开 |
| G | 切岗位 / 切节点 / 新会话 / 切历史 | 沙箱关掉且不串内容 |
**不要误判的正常现象:**
- 流式期间沙箱停在「源码 / 编辑」视图,结束后才切「预览」——这是 `ArtifactFileDetail` 对 markdown/html 的既定行为,与普通对话页相同,**不是**打开时机 bug。
- 步骤条长时间显示「写入文件」、没有路径,但沙箱已经打开并在滚动——content-first 模型下这是**预期**。
---
## 4. 整体架构与数据流
```text
┌──────────────────────────────────────────────────────────────────────────┐
│ PositionRoundtablePage(页面壳) │
│ │
│ state: sandboxArtifact | null │
│ ref: sandboxArtifactRef / closedSandboxKeyRef / latestSnapshotRef │
│ store: createSandboxThreadStore() ←── 禁止改成 useState │
│ │
│ handlePreviewArtifact(artifact) ←──────────────────────────────────┐ │
│ handleNodeThreadSnapshot(...) ←───────────────────────────────┐ │ │
│ closeSandbox() │ │ │
│ │ │ │
│ {sandboxOpen && ( │ │ │
│ SandboxThreadProvider(store) │ │ │
│ └─ ArtifactFileDetail key=sandboxCloseKey(...) │ │ │
│ )} │ │ │
└───────────────────────────────────────────────────────────────────│──│───┘
│ │
┌───────────────────────────────────────────────────────────────────│──│───┐
│ PositionNodeChat / PositionBuiltinAgentChat │ │ │
│ │ │ │
│ useThreadStream → thread.messages / thread.isLoading │ │ │
│ │ │ │ │
│ ├─ MessageList → 步骤条(只展示 tool call) │ │ │
│ │ │ │ │
│ ├─ latestStreamingWriteFile(messages) │ │ │
│ │ → onPreviewArtifact({..., auto:true}) ───────────────────┘ │
│ │ │
│ └─ useEffect → onThreadSnapshot({threadId, thread}) ───────────────┘
└──────────────────────────────────────────────────────────────────────────┘
```
一句话:
- **谁打开**:对话子组件决定「该不该开、开哪次交付」;
- **谁真正开面板**:页面壳;
- **内容怎么更新**:子组件持续上报 thread 快照 → 页面壳 store → 只有沙箱子树重渲染。
---
## 5. 职责划分:谁拥有什么状态
### 5.1 页面壳必须拥有
| 状态 / 能力 | 为什么必须在页面壳 |
| --- | --- |
| `sandboxArtifact` | 唯一预览目标;切节点时要清 |
| `closedSandboxKeyRef` | 用户关过后的抑制;跨子组件生命周期 |
| `sandboxThreadStore` | 流式内容通道;不能进大页面 state |
| `closeSandbox()` | 切岗位 / 切节点 / 新会话统一入口 |
| 唯一 `ArtifactFileDetail` | 避免双实例、关闭失效、深度异常 |
### 5.2 对话子组件必须拥有
| 状态 / 能力 | 作用 |
| --- | --- |
| `autoPreviewRunArmedRef`(armed) | 只有本轮用户发送后才允许自动开 |
| `autoPreviewBaselineCallKeysRef`(baseline) | 忽略发送前已有的历史 write_file |
| `activeWriteFilePreviewRef` | 记录当前打开的 previewKey / path / toolCallId |
| `autoPreviewedArtifactKeyRef` | 同一次 identity 去重,避免每 chunk 重复上报 |
| `latestStreamingWriteFile` | 从 streaming messages 探测可预览的 write |
| `onThreadSnapshot` 上报 | 把 thread 喂给页面壳 |
### 5.3 明确禁止
1. 在 `PositionNodeChat` 内再挂一套 `ArtifactFileDetail`。
2. 让步骤条组件直接 `setOpen(true)` 作为岗位会商主路径(对照普通页可以,岗位会商不行)。
3. 把 `onThreadSnapshot` 落到 `PositionRoundtablePage` 的 `useState`。
4. 用「整个 threadId」做关闭抑制。
5. 用「path 相同」复用 `previewKey`。
---
## 6. 对接接口详解
### 6.1 `PositionArtifactPreview`
```ts
type PositionArtifactPreview = {
threadId: string; // 必填:LangGraph thread id
path: string; // 必填:真实路径,或 write-file: 虚拟 URL,或临时文件名路径
name: string; // 展示名(文件名)
mimeType: string | null; // 如 text/markdown;帮助预览语言判断
previewKey?: string; // 强烈建议:一次交付身份,推荐 `${messageId}:${toolCallId}`
auto?: boolean; // true = 流式自动打开;缺省/false = 用户点击打开
};
```
字段说明:
| 字段 | 详细含义 |
| --- | --- |
| `threadId` | 沙箱用来匹配 `onThreadSnapshot` 的线程;也对关闭抑制键有贡献 |
| `path` | 传给 `ArtifactFileDetail.filepath`。流式阶段通常是 `write-file:...` 虚拟 URL;落盘后的手动打开可以是真实 artifacts 路径 |
| `name` | UI 展示用;临时名阶段可能是 `未命名文件.md` |
| `mimeType` | 当 path 还不稳定时,帮助把语言判成 markdown/html,避免落成 TEXT |
| `previewKey` | **一次交付的身份**。同一次 tool call 的 path 升级要保持不变;新的 tool call 必须变 |
| `auto` | 自动打开才走关闭抑制;手动打开应清抑制 |
### 6.2 `onPreviewArtifact(artifact)` —— 对话 → 页面
页面壳 `handlePreviewArtifact` 当前逻辑顺序:
1. 校验 `threadId`、`path`,缺则 toast 并 return。
2. 计算 `artifactKey = sandboxCloseKey(artifact)`。
3. 若 `artifact.auto === true` 且 `closedSandboxKeyRef.current === artifactKey` → **抑制,直接 return**。
4. 清 `closedSandboxKeyRef`(手动打开或新的自动打开)。
5. `ArtifactsContext.select(path, true)` + `setOpen(true)`。
6. 判断是否同一预览 / 是否仅 path upgrade:
- `isSamePreview`:threadId + path + previewKey 全同 → 不重复 setState。
- `isPathUpgrade`:previewKey 相同、path 不同 → 更新 artifact,**不要**再折叠右侧抽屉。
7. 若不是 same preview:
- `setSandboxArtifact(artifact)`;
- **同步** `sandboxArtifactRef.current = artifact`(关键!flush 会立刻读 ref);
- `sandboxSnapshotAppliedAtRef.current = 0`(允许立刻推首帧内容)。
8. 若 store 里已有别的 thread 快照,先 `publish(null)`。
9. 若 `latestNodeThreadSnapshotRef` 的 threadId 匹配,立刻 `queueSandboxThreadSnapshot`。
10. 非 path upgrade 时 `setRightPanelOpen(false)`。
`sandboxCloseKey`:
```ts
if (artifact.previewKey) {
return `${artifact.threadId}:write:${artifact.previewKey}`;
}
return `${artifact.threadId}:${normalizePreviewArtifactPath(artifact.path)}`;
```
`ArtifactFileDetail` 的 React `key` 必须用这个 key:
**新 previewKey → 组件重挂 → 视觉上「清空再生成」。**
### 6.3 `onThreadSnapshot({ threadId, thread })` —— 对话 → 页面
每次 `thread` 引用变化(消息 chunk 到达)子组件都会调:
```ts
onThreadSnapshot?.({ threadId: activeThreadId, thread });
```
页面壳应:
1. 计算 `signature = sandboxThreadSignature(thread)`(用 isLoading、messages 长度、最近 write_file 的 path/content 长度/尾部等拼签名,用于去重)。
2. 写入 `latestNodeThreadSnapshotRef`(即使沙箱还没开,打开瞬间也要用)。
3. 若当前没有打开的沙箱,或打开的沙箱 threadId 不一致 → **只更新 ref,不 publish**。
4. 若一致 → `queueSandboxThreadSnapshot`(前沿节流后 `store.publish`)。
### 6.4 虚拟 URL 与正文读取
流式阶段 path 形如:
```text
write-file:${sourcePath}?message_id=${messageId}&tool_call_id=${toolCallId}&language=markdown
```
`loadArtifactContentFromToolCall`:
1. 解析 URL 的 `message_id`、`tool_call_id`;
2. 在 `thread.messages` 里找对应 AI message 的 tool call;
3. 返回 `toolCall.args.content`。
结论:
- 沙箱跟的是 **tool call 身份**,不是磁盘文件是否已经写完;
- 没有真实 path 也能显示正文;
- 临时文件名只影响展示名和语言猜测。
### 6.5 对话子组件 → 页面壳的其它相关回调
| 回调 | 用途 |
| --- | --- |
| `onPreviewArtifact` | 打开 / 更新沙箱 |
| `onThreadSnapshot` | 推送流式 thread |
| 页面壳 `closeSandbox` 传给 `ArtifactFileDetail.onClose` | 用户点关闭 |
手动从流程图 / 产物列表打开时,也可以直接调 `handlePreviewArtifact`(通常不带 `auto`,或不带 previewKey)。
---
## 7. 打开沙箱的完整步骤
### 7.1 武装(arm):只有「本轮用户提交」才允许自动开
在 `PositionNodeChat.handleSubmit` 真正 `sendMessage` 之前:
```text
activeWriteFilePreviewRef = null
autoPreviewBaselineCallKeysRef = writeFileCallKeys(thread.messages) // 历史 write 的 messageId:toolCallId
autoPreviewRunArmedRef = true
→ sendMessage(...)
```
发送失败要把 `armed` 设回 `false`。
为什么要 baseline?
线程里往往已经有上几次问答写过的 `write_file`。如果只看「messages 里有没有 write_file」,一提问就会把旧文件再打开一次。baseline 的含义是:**只有这次提交之后新出现的 tool call 才能自动开沙箱**。
### 7.2 探测 `write_file`:不要等完整 path
`write_file` 参数是一整段 JSON 流,**键顺序由模型决定**。
| 模型吐参顺序 | 步骤条表现 | 错误做法的后果 |
| --- | --- | --- |
| path / description 先到,再 content | 很快显示描述和文件名 | 普通对话页常见;等 path 也能开 |
| **content 先到,最后才 path** | 长时间只有「写入文件」,文件名很晚才出现 | 若硬等完整 path,沙箱会晚几十秒甚至一分钟 |
岗位会商报告类 agent **经常是第二种**。
`latestStreamingWriteFile` 规则:
1. 倒序扫描 AI messages 的 `tool_calls`;
2. 工具名是 `write_file` 或 `str_replace`;
3. **有完整扩展名的 path**(`hasCompleteArtifactExtension`)→ 用真实路径;
4. **尚无可用 path,但 `write_file` 的 content 已有非空字符** → 临时文件名:
- 正文头像 HTML → `未命名文件.html`
- 否则 → `未命名文件.md`
5. `str_replace` 必须等真实 path(它改已有文件);
6. 组装:
- `key = messageId:toolCallId`
- `path = write-file:虚拟URL`(write_file)或真实 path(str_replace)
- `mimeType`:markdown 则 `text/markdown`
为什么扩展名必须完整?
流式 path 会经历 `/a/b/report.m` → `/a/b/report.md`。若在 `.m` 时就当真实文件打开,预览语言可能被永久判成 TEXT。
### 7.3 上报 effect 的四个门闩
自动上报 `onPreviewArtifact` 前必须同时满足:
1. `thread.isLoading === true`
2. 探测到 streaming artifact,且其 key **不在** baseline
3. 已有 `previewThreadId`
4. `autoPreviewRunArmedRef === true`
然后处理 previewKey:
```text
若与 activePreview 是同一次 tool call(含 tool-start 前缀兼容)
→ 复用旧 previewKey // 用于临时名 → 真实 path
否则
→ previewKey = 新的 messageId:toolCallId
```
去重键:
```text
`${previewThreadId}:${previewKey}:${sourcePath}`
```
把 `sourcePath` 算进去,是为了:**同 previewKey 下临时名变成真实 path 时,还要再报一次**(path upgrade),但不会每来一个 content chunk 都报。
### 7.4 线程绑定竞态(不要误 disarm)
首轮对话常出现:
1. 用户发送(本地 draft threadId / 稍后 SDK 创建真实 id);
2. 流已经开始、armed=true;
3. 会话 API 把 `node.threadId` 从 `null` 写成真实 id。
这是「本节点绑定自己的新线程」,**不是**切节点。若绑定 effect 无脑清空 armed / baseline,首轮会永远开不了沙箱。
正确:识别 `isOwnThreadBinding`(同 nodeKey、旧 threadId 为空、新 threadId 等于当前 boundThreadId)时 **直接 return,不清 armed**。
### 7.5 path upgrade(改名)≠ 第二次交付
同一次 tool call:
```text
T1 content 到达 → path=未命名文件.md, previewKey=msg:tc1 → 打开
T2 真实 path 到达 → path=/mnt/.../报告.md, previewKey=msg:tc1 → 只改名
```
页面壳看到相同 previewKey、不同 path → `isPathUpgrade`:
- 更新 `sandboxArtifact`;
- **不**重挂(React key 仍基于 previewKey);
- **不**再折叠右侧抽屉。
### 7.6 同路径二次写入(重新生成)
同一次问答里模型可能:
```text
1) write_file → 步骤条「写入文件」→ 打开沙箱
2) 再 write_file 同 path,description=「重新生成优化版…」
```
这是**两次 tool call**,必须两个 previewKey。
若错误地「path 相同就复用 previewKey」:
- 步骤条出现第二条;
- 沙箱 React key 不变;
- 用户看到「重新生成了,但沙箱没变化」。
正确:身份只看 tool call。
### 7.7 时序总图(content-first)
```text
T0 用户发送 → armed=true,拍 baseline
T1 步骤条出现「写入文件」(尚无 path)
T1+ args.content 首字符到达 → 临时名打开沙箱,源码开始滚动
… store 约每 160ms 推一次 thread 快照 …
T2 args.path 拼完整(含 .md)→ path upgrade,标题改真实文件名(不重挂)
T3 write 结束 / isLoading=false → 自动切 Markdown 预览
```
### 7.8 时序总图(同轮二次写入)
```text
T1 write_file#1 → previewKey=msg:tc1 → 打开沙箱 A
T2 #1 结束(可能短暂切预览)
T3 write_file#2(同 path,新 description)→ previewKey=msg:tc2
→ key 变 → 组件重挂清空 → 跟 #2 重新流式
```
---
## 8. 关闭沙箱的完整步骤
### 8.1 用户手动关闭
`closeSandbox()` 应做:
1. `closedSandboxKeyRef = sandboxCloseKey(current)` —— 抑制**这一次**自动再开;
2. 关闭 `ArtifactsContext.open`(若开着);
3. 清节流定时器、pending 快照;
4. `sandboxSnapshotAppliedAtRef = 0`;
5. `sandboxArtifactRef = null` + `setSandboxArtifact(null)`;
6. `sandboxThreadStore.publish(null)`。
抑制与 path upgrade 的关系:
- 关掉临时名面板后,同一次 tool call 升级真实 path **仍被抑制**(同一 previewKey)——符合「用户不要看这次」;
- 下一次新的 `write_file`(新 previewKey)**可以**再自动开。
### 8.2 场景切换时强制关闭
以下操作应先调用 `closeSandbox()`,避免串台:
- 切岗位;
- 切节点;
- 切收口阶段;
- 新建会话;
- 切换历史会话。
是否写入 `closedSandboxKeyRef` 可按产品决定;至少必须清面板和 store。
### 8.3 错误的抑制粒度(历史坑)
若做成「关过一次就整个 thread 不再自动开」:
- 第一次实时开正常;
- 用户关掉;
- 再问一次 → 同 thread 被永久抑制 → 看起来像「第二次不实时 / 要等生成完」。
正确粒度:**单次交付的 `sandboxCloseKey`(含 previewKey)**。
---
## 9. 步骤条与沙箱的关系
### 9.1 步骤条从哪来
步骤条由 `MessageList` → `message-group.tsx` 渲染 tool call:
- 标题:优先 `args.description`;没有则 i18n 兜底「写入文件」;
- 子行:有 `args.path` 才显示路径;
- 普通对话页还会在「有 path + autoOpen」时自己 `select/setOpen`。
岗位会商里,步骤条**仍然只负责展示**;打开沙箱走 `onPreviewArtifact`,不要让两套逻辑抢。
### 9.2 对照表
| 你看到的步骤条 | 含义 | 沙箱应处于 |
| --- | --- | --- |
| 仅「写入文件」,无路径 | tool call 已开始;path/description 可能还没流到 | **应已打开**(临时名),源码在滚 |
| 「写入文件」+ 路径 | path 已可用 | 已打开;若刚从临时名升级则只改名 |
| 带自定义 description 的标题 + 路径 | 模型给了 description | 同上 |
| 第二条「重新生成…」+ 同路径 | 新的 write_file(新 toolCallId) | **应重挂**并重新流式 |
| 顶部「正在生成,完成后自动预览」 | 仍在 code 视图 | 正常,等 isLoading 结束切预览 |
| 生成结束,出现文件卡片 / present_files | 工具结果已落 | 通常已是预览模式;可手动再点开 |
### 9.3 常见误解
**误解 1:**「步骤条出现文件名 = 打开沙箱的正确时机」
→ 错。那是 path 到达的时机。content-first 时正确打开时机更早:content 首字节 / 工具调用已开始。
**误解 2:**「步骤条还在写『写入文件』,沙箱却开了,是 bug」
→ 通常不是。两边数据源相同,但沙箱允许无 path 打开,步骤条标题可能还没 description。
**误解 3:**「等生成完从编辑切预览 = 沙箱打开晚了」
→ 错。那是 `ArtifactFileDetail` 的 viewMode 策略。
---
## 10. 流式内容如何进入沙箱(性能关键)
### 10.1 为什么不能用页面 `useState` 存线程快照
错误写法:
```ts
setSandboxThreadSnapshot(snapshot) // 页面级 useState
```
后果:
- 消息 chunk 约每 30ms 一次,节流后仍约 6 次/秒;
- 每次都重渲染整页:任务卡、意图结果、流程图、节点对话……;
- 主线程占满 → 消息列表停绘、沙箱冻住;
- 纯尾随 `setTimeout(160)` 会被饿死,十几秒不 flush。
这同时解释了两个历史现象:
1. 「沙箱打开了但内容不更新」;
2. 「修完节流后整页更卡、消息也不实时」——因为节流终于开始真正 setState 了。
### 10.2 正确做法:外部 store + 子树订阅
```ts
createSandboxThreadStore()
subscribe / getSnapshot / publish
SandboxThreadProvider
useSyncExternalStore(store.subscribe, store.getSnapshot)
→ ThreadContext.Provider value={{ thread, isMock:false }}
→ ArtifactFileDetail
```
页面壳本身**不**因 snapshot 变化而重渲染;只有 Provider 及其子树重渲染。
### 10.3 节流必须是前沿(leading-edge)
`SANDBOX_SNAPSHOT_MIN_INTERVAL_MS = 160`:
```text
queue(snapshot):
pending = snapshot
if 距上次 publish >= 160ms:
立刻 flush(同步 publish)
else:
若没有在途 timer,则 setTimeout(剩余时间) 做补尾
flush:
清 timer
校验 pending.threadId === sandboxArtifactRef.threadId
publish(pending)
```
**不要**改回:
```ts
if (timer != null) return;
timer = setTimeout(flush, 160);
```
那是纯尾随;忙主线程下会假死。
### 10.4 signature 去重
`sandboxThreadSignature` 用最近 write_file 的 path、content 长度、content 尾 32 字等拼签名。
store.publish 时若 threadId+signature 未变,则不通知订阅者,减少无效渲染。
### 10.5 打开瞬间的首帧
`handlePreviewArtifact` 里往往会立刻 `queueSandboxThreadSnapshot(latestSnapshot)`。
此时 React 的 `setSandboxArtifact` 可能还没提交,若 flush 只读旧的 `sandboxArtifactRef`,会因 threadId 不匹配把首帧丢掉。
所以打开新预览时必须:
```ts
sandboxArtifactRef.current = artifact; // 同步
sandboxSnapshotAppliedAtRef.current = 0;
```
---
## 11. 与普通对话页的对照
| 点 | 普通对话页 | 岗位会商 |
| --- | --- | --- |
| 谁触发打开 | `message-group` 内 `artifacts.select` | 子组件 `onPreviewArtifact` → 页面壳 |
| 是否要求 path | 是(有 path 才自动开) | 否(write_file + content 可临时名开) |
| thread 从哪来 | 同树 ThreadContext | `onThreadSnapshot` + 外部 store |
| 关闭抑制 | 主要靠 ArtifactsContext / 用户操作 | `closedSandboxKeyRef` 按 previewKey |
| 同 path 重写 | 依赖新的 tool call / 选择逻辑 | **必须**新 previewKey 以重挂 |
| 结束后切预览 | ArtifactFileDetail 同一套 | 同一套 |
对齐目标是**用户感知一致**,不是代码路径完全相同。
普通对话页自动开的核心条件(对照用):
```ts
artifacts && isLoading && isLast && autoOpen && autoSelect && path && !result
```
它也要求 path,所以 content-first 时普通页同样会偏晚——只是很多主聊天示例 agent 先吐 path,观感更好。岗位会商报告 agent 更常 content-first,所以必须做临时名提前开。
---
## 12. 重做页面时的必守规则
按优先级:
1. **流式 thread 快照禁止进页面级 `useState`** → 用 store + `useSyncExternalStore`。
2. **节流用前沿 + 补尾**,不要纯尾随 timer。
3. **不要等完整 path 才开**;`write_file` + content 即可临时名打开。
4. **扩展名完整前不要把 path 当最终 path**。
5. **previewKey 按 tool call,不按 path**。
6. **打开时同步写 `sandboxArtifactRef`,并重置节流时间戳**。
7. **主路径扫描 `thread.messages`,不要只靠 `onToolStart`**。
8. **path upgrade 保持 previewKey,不要当第二次交付**。
9. **关闭抑制按 sandboxCloseKey,不要按整 thread**。
10. **沙箱单例在页面壳;子组件只上报**。
11. **本节点 threadId 回写不要 disarm**。
12. **「结束后才切预览」不要当打开 bug 去「修」掉共享组件**,除非产品明确要求边生成边渲染 Markdown(需单独评估性能)。
---
## 13. 历史问题与根因(务必读)
下面每个都在真实联调里出现过。重做时若踩中,优先回来对照。
### 13.1 沙箱要等文件名出来才开(晚几十秒)
**现象:** 步骤条早就「写入文件」,沙箱很晚才开;普通对话页对比显得「实时」。
**根因:** 探测逻辑硬等完整 `path`;而该 agent 先流 `content`、最后才流 `path`。
**修复:** content 首批字符即可用临时名打开;path 到了再 upgrade。
### 13.2 沙箱打开了,内容冻住,生成完才灌满
**现象:** 面板开了,但里面一直是空/旧内容;日志里 queue 很多,flush 很少。
**根因:** 纯尾随 160ms 定时器在忙主线程下被饿死。
**修复:** 前沿节流——到期就在流回调里同步 flush。
### 13.3 修完「内容更新」后,整页卡顿、消息也不实时
**现象:** 沙箱开始更新了,但页面更卡,消息列表也「攒到最后刷」。
**根因:** flush 走的是页面 `useState`,每 160ms 整页重渲染。以前 timer 饿死时反而「不卡」。
**修复:** 快照改外部 store,只让沙箱子树订阅。
### 13.4 第二次「重新生成」步骤有了,沙箱不变
**现象:** 同轮第二条二条 write 步骤,沙箱仍显示第一次完整结果。
**根因:** 用「path 相同」复用了第一次 previewKey,React key 不变,组件不重挂。
**修复:** previewKey 只按 tool call;同 path 新 call 必须新 key。
### 13.5 第一次实时,关掉后再问就不实时
**现象:** 关过沙箱后,下一轮要等生成完才开,或不开。
**根因:** 抑制绑在整个 threadId,或把 path upgrade / 新 call 误判成同一次。
**修复:** 抑制键 = `sandboxCloseKey(previewKey)`;仅抑制那一次交付。
### 13.6 首轮永远不开
**现象:** 新建节点第一次提问,armed 后很快又不满足条件。
**根因:** `node.threadId` 从 null 回写真实 id 时,绑定 effect 误 disarm。
**修复:** `isOwnThreadBinding` 时不清 armed。
### 13.7 预览变成纯文本
**现象:** 明明是 md,却按 TEXT 打开。
**根因:** 在 `.m` 阶段就当完整 path;或临时名没有 `.md` / 没传 language、mimeType;或 path 带空格。
**修复:** `hasCompleteArtifactExtension`;临时名带扩展名;URL 加 `language=markdown`;path trim。
### 13.8 依赖 onToolStart,线上偶发不开
**现象:** 本地偶发正常,联调经常不开。
**根因:** 后端 StreamBridge / worker 可能过滤 tools/events 类 stream mode。
**修复:** 主路径必须是 messages 里渐进出现的 `tool_calls`;onToolStart 只能当补充。
---
## 14. 排障手册(按症状)
排障时建议先看 Chrome Performance / React 重渲染是否整页抖,再看消息里 tool_calls 的 args 是 path-first 还是 content-first。
### 14.1 沙箱开得很晚
检查顺序:
1. `latestStreamingWriteFile` 是否仍要求完整 path?
2. armed 是否被 threadId 回写清掉?
3. 是否命中 `closedSandboxKey`?
4. 新 call 是否被 baseline 当成历史?
5. `previewThreadId` 是否为空(线程还没绑上)?
### 14.2 打开了但内容空白 / 冻住
1. 快照是否进了页面 useState?
2. 节流是否纯尾随?
3. `SandboxThreadProvider` 是否拿到带 messages 的 thread?
4. 虚拟 URL 的 message_id / tool_call_id 是否对得上?
5. 打开时 `sandboxArtifactRef` 是否同步?(首帧是否被 drop)
6. signature 是否因实现错误一直不变,导致 publish 被去重?
### 14.3 整页卡、消息列表也不实时
1. 搜页面壳是否还有 `setSandboxThreadSnapshot` / 类似大对象 state。
2. 是否在每 chunk `console.debug` 打大对象(DevTools 打开时更卡)。
3. 是否错误地让页面壳订阅了 store(页面壳不应 `useSyncExternalStore` 这份 store;只有 Provider 订)。
### 14.4 重新生成不刷新
1. 第二次 write 的 toolCallId 是否变了?
2. previewKey 是否仍用 path 复用?
3. `ArtifactFileDetail` 的 `key=` 是否基于 sandboxCloseKey/previewKey?
### 14.5 关后再问异常
1. 打印 `closedSandboxKeyRef` 与新 artifact 的 sandboxCloseKey。
2. 确认新一轮 send 时 armed/baseline 已重置。
3. 确认抑制不是整 thread。
### 14.6 语言 / 预览模式不对
1. 看打开时的 path / mimeType / language 查询参数。
2. 确认不是在扩展名不完整时打开。
3. 确认「源码→预览」发生在 isLoading false 之后(属正常)。
---
## 15. 对接自测清单
重做页面后至少跑完:
### 功能
- [ ] 首轮:步骤条「写入文件」出现时,沙箱是否已开并开始出字?
- [ ] content-first agent:无文件名阶段是否也能开?
- [ ] path 到达后:是否只改名、不整页闪、不重挂?
- [ ] 同轮第二次同路径 write:是否清空并重新流式?
- [ ] 结束后是否自动切 Markdown 预览?
- [ ] 手动点步骤条路径 / 产物卡片:能否打开正确文件?
### 关闭与抑制
- [ ] 流式中手动关闭:本次是否不再自动弹?
- [ ] 关闭后再发一轮新问题:是否仍可自动开?
- [ ] path upgrade 是否不会绕过「用户刚关闭」的抑制?
### 场景切换
- [ ] 切节点:沙箱关闭,旧内容不残留。
- [ ] 切岗位:同上。
- [ ] 新建会话 / 切历史:同上。
### 性能
- [ ] 流式中消息列表是否实时出字?
- [ ] 页面是否明显掉帧 / 卡死?
- [ ] React DevTools 中,大页面壳是否在每 160ms 整页 update?(不应)
---
## 16. 最小接线示例
### 16.1 页面壳(示意)
```tsx
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
const closedSandboxKeyRef = useRef<string | null>(null);
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
const store = useMemo(createSandboxThreadStore, []);
// pending + timer + appliedAt 用于前沿节流(见 PositionRoundtablePage)
function handlePreviewArtifact(artifact: PositionArtifactPreview) {
if (!artifact.threadId || !artifact.path) return;
const key = sandboxCloseKey(artifact);
if (artifact.auto && closedSandboxKeyRef.current === key) return;
closedSandboxKeyRef.current = null;
artifactsCtx?.select(artifact.path, true);
artifactsCtx?.setOpen(true);
const prev = sandboxArtifactRef.current;
const isSame =
prev?.threadId === artifact.threadId &&
prev.path === artifact.path &&
prev.previewKey === artifact.previewKey;
const isPathUpgrade =
!isSame &&
Boolean(artifact.previewKey) &&
prev?.threadId === artifact.threadId &&
prev.previewKey === artifact.previewKey;
if (!isSame) {
sandboxArtifactRef.current = artifact; // 必须同步
setSandboxArtifact(artifact);
sandboxSnapshotAppliedAtRef.current = 0;
}
if (store.getSnapshot()?.threadId !== artifact.threadId) {
store.publish(null);
}
const latest = latestNodeThreadSnapshotRef.current;
if (latest?.threadId === artifact.threadId) {
queueSandboxThreadSnapshot(latest);
}
if (!isPathUpgrade) setRightPanelOpen(false);
}
function handleNodeThreadSnapshot(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) {
const next = { ...snapshot, signature: sandboxThreadSignature(snapshot.thread) };
latestNodeThreadSnapshotRef.current = next;
if (sandboxArtifactRef.current?.threadId !== snapshot.threadId) return;
queueSandboxThreadSnapshot(next);
}
function closeSandbox() {
if (sandboxArtifactRef.current) {
closedSandboxKeyRef.current = sandboxCloseKey(sandboxArtifactRef.current);
}
// clear timer / pending / appliedAt
sandboxArtifactRef.current = null;
setSandboxArtifact(null);
store.publish(null);
artifactsCtx?.setOpen(false);
}
// 渲染
{sandboxArtifact && (
<SandboxThreadProvider
store={store}
threadId={sandboxArtifact.threadId}
fallbackThread={stub}
>
<ArtifactFileDetail
key={sandboxCloseKey(sandboxArtifact)}
filepath={sandboxArtifact.path}
threadId={sandboxArtifact.threadId}
mimeType={sandboxArtifact.mimeType}
onClose={closeSandbox}
allowVirtualMarkdownEdit
/>
</SandboxThreadProvider>
)}
```
### 16.2 对话子组件(示意)
```tsx
// send 前
activeWriteFilePreviewRef.current = null;
autoPreviewBaselineCallKeysRef.current = writeFileCallKeys(thread.messages);
autoPreviewRunArmedRef.current = true;
await sendMessage(...);
// 探测
const streaming = thread.isLoading ? latestStreamingWriteFile(thread.messages) : null;
const next =
streaming && !autoPreviewBaselineCallKeysRef.current.has(streaming.key)
? streaming
: null;
useEffect(() => {
if (!thread.isLoading || !next || !previewThreadId || !autoPreviewRunArmedRef.current) return;
// previewKey:同 tool call 复用,否则用 next.key
// artifactKey = threadId:previewKey:sourcePath 去重
onPreviewArtifact?.({
threadId: previewThreadId,
path: next.path,
name: next.name,
mimeType: next.mimeType,
previewKey,
auto: true,
});
}, [next, previewThreadId, thread.isLoading, onPreviewArtifact]);
useEffect(() => {
onThreadSnapshot?.({ threadId: activeThreadId, thread });
}, [activeThreadId, thread, onThreadSnapshot]);
```
**强烈建议:** 探测 / armed / baseline / 临时名逻辑直接复用或抽取自 `PositionNodeChat`,不要从零重写。
---
## 17. 关键代码索引
| 主题 | 位置 |
| --- | --- |
| 临时名 / 完整扩展名 / latestStreamingWriteFile | `PositionNodeChat.tsx` 中 `provisionalWriteFilePath`、`hasCompleteArtifactExtension`、`latestStreamingWriteFile` |
| armed / baseline / 上报 effect | `PositionNodeChat.tsx` `handleSubmit` + streaming preview `useEffect` |
| 同 tool call 判断 / 二次写入重挂 | 同上,`isSameToolCall` / `previewKey` |
| 线程绑定保护 | `PositionNodeChat.tsx` node binding `useEffect` 中 `isOwnThreadBinding` |
| sandboxCloseKey / store / 前沿节流 | `PositionRoundtablePage.tsx` `sandboxCloseKey`、`createSandboxThreadStore`、`queueSandboxThreadSnapshot` |
| 打开 / path upgrade / 抑制 | `PositionRoundtablePage.tsx` `handlePreviewArtifact` |
| 关闭 | `PositionRoundtablePage.tsx` `closeSandbox` |
| 虚拟 URL 读 content | `core/artifacts/loader.ts` `loadArtifactContentFromToolCall` |
| 源码/预览切换 | `artifact-file-detail.tsx` 中 `viewMode` + `thread.isLoading` |
| 普通页步骤条自动开 | `message-group.tsx` `write_file` 分支 |
相关提交(背景):
- `fix(frontend): 岗位会商沙箱在 write_file 流式阶段实时打开并刷新`
— 临时名打开、外部 store、前沿节流
- `fix(frontend): 同路径二次 write_file 时重挂岗位会商沙箱`
— previewKey 按 tool call,禁止按 path 复用
---
## 18. 术语表
| 术语 | 含义 |
| --- | --- |
| armed | 本轮用户发送后,允许自动打开沙箱的开关 |
| baseline | send 前已有的 write tool call 集合,用于忽略历史产物 |
| previewKey | 一次交付的稳定身份,推荐 `messageId:toolCallId` |
| sandboxCloseKey | 关闭抑制键,同时用作 `ArtifactFileDetail` 的 React key |
| provisional path / 临时名 | 尚无真实 path 时的 `未命名文件.md` / `.html` |
| path upgrade | 同 previewKey 下临时名升级为真实 path |
| sandboxThreadStore | 流式线程外部 store,避免整页重渲染 |
| write-file: URL | 用 message_id + tool_call_id 定位流式 content 的虚拟路径 |
| leading-edge throttle / 前沿节流 | 到期立即在回调里 publish,而不是只挂尾随 timer |
| content-first / path-first | 模型流式 JSON 参数时 content 与 path 哪个先到 |
| isOwnThreadBinding | 本节点把自己的新 threadId 写回 props,不是切节点 |
---
## 附录 A:给对接同学的一页纸摘要
1. 页面壳只挂一个沙箱;子组件只调用 `onPreviewArtifact` / `onThreadSnapshot`。
2. 自动打开时机 ≈ 步骤条出现「写入文件」时,而不是出现文件名时。
3. `write_file` 可以没有 path:临时名打开,path 到了再改名。
4. 流式 thread 进外部 store,进页面 state 会卡死。
5. 节流要前沿;previewKey 看 tool call;关闭抑制看 sandboxCloseKey。
6. 同路径重新生成 = 新 previewKey = 沙箱重挂。
7. 出问题先对照第 13、14 节,再看代码索引第 17 节。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,610 @@
# 圆桌 DAG 编排 + 并行执行显示 —— 分步实现需求文档
> 目标读者:实现本功能的前端 / 后端工程师。
> 本文是**可逐步落地**的需求 + 设计文档,每个 Phase 都能独立交付、独立验收。
---
## 0. 背景与目标
### 0.1 现状
Step 2 圆桌目前有两种编排模式(`orchestrationMode`,见 `hooks/useStep2Orchestration.ts`):
- **`free`(自由模式)**:总控(leader)自由决定派活给谁,`runOrchestration` 循环驱动。
- **`chain`(线性链条)**:用户把席位排成一条**线性**链 `A → B → C`,`runChainOrchestration` 按顺序逐个派活,最后总控收口。链路用 `DispatchChainModal.tsx` 的**横向流程图**展示(总控 → 席位1 → 席位2 → … → 共识 → 报告)。
链条数据现在只是一个**有序数组** `selectedAgents`(线性),节点状态类型见 `lib/job-types.ts`(`ChainNodeState = pending | active | done`)。
### 0.2 目标(本次新增)
1. **DAG 编排**:用户可视化指定**智能体之间的直接依赖关系**——有的串行(`A → B → C`),有的并行(`A B C` 同时跑)。编排结果是一张**流程图(DAG)**。
2. **并行执行**:DAG 里同一层(互不依赖)的智能体**并行**跑,墙钟时间从「串行求和」降到「并行取最慢」。
3. **并行禁止写文件**:并行的智能体**禁用文件写入 / 沙箱命令**,避免它们同时打开沙箱互相打架。产物以消息正文交付;落文件留给串行 / 收口阶段。
4. **并行高性能显示**:并行时收起左侧、顶部头像条切换、主区同时显示 2 个智能体的消息流,其余**只接流不渲染**,避免页面卡死。
### 0.3 设计总原则
- **复用** 现有 special run / leader run / rollback 抢占 / 轻量交付状态接口(`GET /threads/{id}/last-ai-message`),不另起炉灶。
- **线性链是 DAG 的特例**:`chain` 模式可平滑迁移成"每层 1 个节点"的 DAG,老数据不破坏。
- **分层(Stage)优先于自由连线**:编辑交互首选「分层」模型(每个 stage 放若干并行席位,stage 间串行),实现成本低、覆盖 95% 的"串 / 并"诉求;自由连线画布作为后续可选增强(见 §2.3)。
### 0.4 已确认的设计决策(✅ 本期按这些实现)
> 以下三项已与需求方确认锁定,实现时直接按此,不再二选一。
1. **✅ 编排模型 = 分层 Stage**:本期只做分层(每个 stage 放若干并行席位、stage 间串行)。**自由连线画布本期不做**,仅在数据模型层预留(§1.2)以免将来返工。
2. **✅ 并行跳过 leader、前端直派**:DAG 由用户显式指定,执行时前端**直接**对 stage 内席位发起 special run,**不**每个 stage 再跑一轮总控派活;只在最后 `finalSynthesis` 跑一轮 leader 综合(即 §3.2 的方案 B)。
3. **✅ 主区固定 2 列,窄屏降 1 列**:并行时主区最多并排显示 2 个被选中智能体的消息流;窄屏(断点见 §5.3)降为 1 列 + 头像条横向滚动。不做 3+ 列。
---
## 1. 数据模型
### 1.1 DAG 的「分层」表示(推荐,主模型)
把 DAG 表示成**有序的 stage 列表**,每个 stage 内的席位并行、stage 之间串行:
```ts
// lib/dag-types.ts (新建)
/** 一个并行批次:stage 内所有席位并行执行。 */
export interface OrchestrationStage {
/** stage 稳定 id(拖拽重排 / diff 用)。 */
id: string;
/** 本 stage 并行执行的席位(agent_id 列表)。1 个 = 退化为串行节点。 */
agentIds: string[];
}
/** 用户编排出的执行计划。stages 顺序即串行顺序。 */
export interface OrchestrationPlan {
/** 编排模式标识,持久化进 step2。 */
mode: "dag";
/** 串行执行的批次;每批内并行。 */
stages: OrchestrationStage[];
/** 收口:是否在所有 stage 完成后由总控综合(默认 true)。 */
finalSynthesis: boolean;
}
```
- **串行** `A → B → C` = `stages: [{agentIds:[A]}, {agentIds:[B]}, {agentIds:[C]}]`。
- **并行** `A B C` = `stages: [{agentIds:[A,B,C]}]`。
- **混合** `A →(B C)→ D` = `stages: [{[A]}, {[B,C]}, {[D]}]`。
> 为什么用分层而不是「邻接表 / 边」:执行调度本就是「拓扑分批」,分层就是分批的结果,前端不必再做拓扑排序,也天然避免环;编辑器交互更直观(拖卡片进 stage)。真正的自由连线 DAG 见 §2.3(可选)。
### 1.2 通用 DAG 表示(§2.3 自由连线时才需要)
若将来做自由连线画布,再引入边表示,并在执行前做一次**拓扑排序 → 分层**,落回 §1.1 的 stages 结构给调度器用:
```ts
export interface DagEdge { from: string; to: string } // from 完成后才能跑 to
export interface DagGraph { nodes: string[]; edges: DagEdge[] }
// 执行前:toposortToStages(graph) -> OrchestrationStage[]
```
### 1.3 持久化
- 复用 `roundtable_drafts.step2` JSON(见后端 `app.gateway.routers` roundtable-drafts + `hooks/useStep2Orchestration.ts` 里对 `step2` 的读写)。
- 在 `step2` 增加字段:`orchestrationPlan: OrchestrationPlan | null`。
- `orchestrationMode` 扩展为 `"free" | "chain" | "dag"`;`chain` 旧数据读取时即时转换为单节点-stage 的 `dag`(兼容层,不改老 draft)。
### 1.4 校验(纯函数,可单测)
`lib/dag-types.ts` 内提供:
```ts
validatePlan(plan: OrchestrationPlan, selectedAgentIds: string[]): {
ok: boolean;
errors: string[]; // 面向用户的中文提示
}
```
校验项:
- 每个 `agentId` 必须在本次选定席位 `selectedAgents` 内(防止编排了不存在的席位 → 复用后端 `_partition_dispatch` 同思路的"必须在 roster 内")。
- 不允许空 stage;不允许同一席位出现在多个 stage(一个席位只跑一次)。
- 至少 1 个 stage。
- (可选)单 stage 并行度上限提示(如 > 6 并行给 warning:远程模型并发 + 沙箱压力)。
---
## 2. 编排编辑器(流程图构建)交互
### 2.1 入口与整体布局
- 在 Step 2 顶部模式切换里新增 **「依赖编排」**(`dag`)模式(与现有 自由 / 链条 并列)。
- 选择 `dag` 后弹出 / 进入**编排编辑器**(复用 Dialog 大弹窗,参考 `DispatchChainModal` 的尺寸 `w-[94vw] max-w-[1240px]`)。
### 2.2 推荐交互:分层(Stage)编辑器(Phase 2 实现)
```
┌ 依赖编排 ───────────────────────────────────────── [校验✓] [保存] ┐
│ 可选席位(点/拖加入某个 stage) │
│ [情报收集] [环境评估] [方案设计] [风险审查] [执行规划] [前端实现] │
│ ─────────────────────────────────────────────────────────────── │
│ Stage 1(并行) Stage 2(并行) Stage 3 │
│ ┌──────────┐ ─────▶ ┌──────────┐ ┌────────┐ ─▶ ┌──────────┐ │
│ │ 情报收集 │ │ 方案设计 │ │风险审查│ │ 执行规划 │ │
│ └──────────┘ └──────────┘ └────────┘ └──────────┘ │
│ [+ 拖席位到此] [+] [+] │
│ [+ 新建 Stage(串在最后)] │
│ ─────────────────────────────────────────────────────────────── │
│ 说明:同一 Stage 内的席位会**并行**执行;Stage 之间**串行** │
│ (后一个 Stage 能看到前面所有 Stage 的交付摘要)。 │
└───────────────────────────────────────────────────────────────────┘
```
交互要点:
- **加入 stage**:从顶部可选席位**拖拽**到某个 stage,或点席位 →「加入 Stage N」。
- **移动 / 重排**:席位可在 stage 间拖动;stage 可整体左右拖动重排(改串行顺序)。
- **删除**:席位 / 空 stage 可删。
- **并行度提示**:stage 内 ≥2 席位即标「并行」徽标;过多给 warning。
- **实时校验**:`validatePlan` 实时跑,错误在顶部红条提示,「保存」在校验通过前禁用。
- **预览流程图**:编辑区下方实时渲染只读流程图缩略(复用 §7 的 DAG 流程图组件)。
> 拖拽可用现成轻量方案(HTML5 DnD 或 `@dnd-kit`,看项目是否已有依赖;没有就先用「点选 + 加入/移出按钮」的无拖拽版,Phase 2 先上无拖拽、Phase 2.5 再加拖拽)。
### 2.3 可选增强:自由连线画布(后续,非本期必须)
- 节点自由摆放 + 拉线建边 → `DagGraph`(§1.2)。
- 实现成本高(画布 / 连线 / 命中检测 / 自动布局),且分层模型已覆盖绝大多数诉求。**本期不做**,仅在数据模型上预留 §1.2,避免将来返工。
### 2.4 验收(Phase 2)
- 能拖 / 点把席位分配到多个 stage,保存进 `step2.orchestrationPlan`。
- 刷新 / 重进 draft 能恢复编排。
- 非法编排(空 stage / 席位重复 / 席位不在 roster)有明确中文提示且无法保存。
---
## 3. 执行调度(前端编排循环 + 后端复用)
### 3.1 新增 `runDagOrchestration`(`useStep2Orchestration.ts`)
与现有 `runOrchestration` / `runChainOrchestration` 并列,逻辑:
```
for (stageIndex, stage of plan.stages):
if isStale() break // 复用现有 epoch 抢占(人工干预/暂停)
// 1) 总控按本 stage 的席位并行派活(一轮 leader,dispatched = stage.agentIds)
// —— 或跳过 leader,前端直接对 stage 内每个席位发起 special run(见 3.2)
// 2) 并行跑 stage 内所有席位
await Promise.all(stage.agentIds.map(seat =>
streamMultiAgent({ agentType:"special", agentName:seat,
newMessage: buildStageTask(seat, plan, stageIndex),
parallelNoFile: stage.agentIds.length > 1, // §4
... })))
// 3) 等本 stage 全部 ✅(Promise.all 天然 barrier)再进入下一 stage
// 4) finalSynthesis:最后一轮 leader(synthesisMode=true)综合所有 stage 交付
```
要点:
- **批内并行**:`Promise.all`(不是现在的 `for await` 串行)。
- **批间串行**:`Promise.all` 即 barrier,自动等齐。
- **依赖可见性**:进入 stage N 的席位时,把**前面所有 stage 已交付席位的摘要**拼进它的 `newMessage`(复用 §交付状态:网关 `_collect_delivery_status` / `GET last-ai-message?max_chars=N`,前端也可在 stage 边界拉一次摘要拼进 task)。这样 `(B C)` 能看到 `A` 的产出。
- **抢占 / 干预 / 暂停**:每个 stage 边界检查 `isStale()`(epoch),与现有自由 / 链条模式一致。
- **rollback**:special run 已统一 `multitask_strategy=rollback`,并行多席位各自不同 thread,互不冲突。
### 3.2 是否每 stage 都走 leader? — ✅ 已定:前端直派(方案 B)
**本期采用方案 B**:DAG 由用户**显式**指定,不需要 leader 再"决定派给谁"。前端**直接**对 stage 内每个席位发起 special run,省掉每 stage 的 leader LLM 往返(更快、更可控);只在所有 stage 完成后、`finalSynthesis` 为真时跑**一轮** leader(`synthesisMode=true`)综合所有交付。
> (备选方案 A:每 stage 跑一轮 leader 派活——更贴现有架构但每 stage 多一次 leader LLM、更慢。**本期不采用**,留档备查。)
### 3.3 后端改动
- **基本不需要新接口**:special run(`POST /api/multi-agent/run/stream` agent_type=special)、leader run、轻量交付读(`GET /threads/{id}/last-ai-message`)、轻量追加(`POST /threads/{id}/messages/append`)都已就绪。
- 仅需 §4 的「并行禁写文件」run policy 开关。
### 3.4 验收(Phase 3)
- `(B C)` 两席位**同时**出现在执行(后端日志两条 special run 时间重叠;前端两个流并行增长)。
- stage 间严格串行(stage 2 不早于 stage 1 全部完成)。
- stage N 席位的输入里能看到 stage <N 的交付摘要。
---
## 4. 并行时禁止写文件(防沙箱打架)
### 4.1 后端 run policy
`app/gateway/roundtable_run_policy.py` 现有角色:`leader` / `seat` / `report`。新增一个角色或开关:
- 方案:给 `seat` 增加一个「并行变体」`seat_parallel`,其 `excluded_tools` 在 `seat` 基础上**再禁** `write_file` / `bash` / `str_replace` / `present_files`(与 `report` 角色禁写的思路一致)。
- `_special_run`(`multi_agent.py`)按请求里的一个新 flag(如 `no_file: true`)选择 `seat_parallel` policy。
### 4.2 前端传参
- `streamMultiAgent` 请求体新增 `parallel_no_file?: boolean`(或复用 `no_file`)。
- `runDagOrchestration` 在 `stage.agentIds.length > 1`(真并行)时置 `true`;单节点 stage(实际串行)不限制,可正常写文件。
### 4.3 产物归属
- 并行席位的产物 = **消息正文**(被 `_collect_delivery_status` / 综合块读取)。
- 需要落文件 / 画图的(如 Step3 报告、`roundtable-report`)放在**串行的收口阶段**单独跑,不在并行批次里。
### 4.4 验收(Phase 4)
- 并行 stage 的席位 run 日志显示 `Excluded ... ['write_file','bash','str_replace','present_files', ...]`。
- 并行期间不再出现多个席位抢同一沙箱 / 写文件冲突。
---
## 5. 并行执行的前端显示
### 5.1 布局总览(并行进行时)
```
┌ 多智能体圆桌会商中心 ─────────────────────────────────────────────┐
│ [头像条:进行中的并行智能体] [模型选择 ▾] [暂停会商] │
│ (情报)● (方案)● (风险)● (执行) … │
│ ↑选中 ↑选中 loading hidden │
│ ───────────────────────────────────────────────────────────────── │
│ ┌─ 方案设计(选中)──────────┐ ┌─ 风险审查(选中)──────────┐ │
│ │ 一、整体架构…(流式渲染) │ │ 1. 合规风险 IDFA/OAID… │ │
│ │ … │ │ … │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
(左侧"参与角色"栏在并行时整体收起,腾出横向空间放两列消息)
```
### 5.2 关键交互(与需求逐条对齐)
1. **收起左侧**:并行执行时把左侧「参与角色 / 人工参与」栏整体折叠(一个 `isParallelRunning` 状态控制;非并行恢复)。
2. **头像条**:位置在**消息列表上方、模型选择器左侧**。展示**当前并行批次的所有进行中智能体头像**:
- **进行中** → 头像加 loading 动画(复用 `DispatchChainModal` 的 `animate-ping` 光环 + `Loader2Icon animate-spin` 角标)。
- **已完成** → 打勾(复用现有 `CheckIcon` 角标)。
- **选中** → 高亮描边(当前正在主区显示的那 2 个)。
3. **主区同时显示 2 个**:被选中的 2 个智能体的消息列表**并排两列**渲染。
4. **多于 2 个先隐藏**:未选中的智能体**不渲染 DOM**(见 §6 性能)。
5. **点头像切换**:点击未选中的头像 → 把它设为选中之一(替换当前 2 个里的一个,或按"最近点击保留 2 个"的 LRU 策略);被换下的智能体**停止渲染但继续接流**。
### 5.3 选中策略 — ✅ 固定 2 列,窄屏降 1 列
- 维护 `selectedParallelSeats: string[]`,**最多 2**(固定 2 列)。**不做 3+ 列**。
- **响应式**:宽屏 2 列并排;窄屏(建议断点 `< 1024px` / `lg`)降为**1 列**,头像条横向滚动切换;此时 `selectedParallelSeats` 仍可存 2 个,但主区只渲染当前 1 个(点头像切换哪个在前)。
- 默认选中本批次**最先开始**的(宽屏 2 个 / 窄屏 1 个)。
- 点头像:若已选中则取消;未选中则加入(超过上限时按 LRU 踢掉最早选中的)。被换下的席位**停止渲染但继续接流**(§6)。
### 5.4 验收(Phase 5)
- 并行时左侧收起、出现头像条、主区两列。
- 进行中头像有 loading;完成打勾;选中有高亮。
- 点头像能切换主区显示的智能体,且切换是**即时**的(数据已在内存,见 §6)。
---
## 6. 流式高性能策略(避免页面卡死)
> 核心矛盾:N 个并行席位同时高频 `onTextDelta`,若都 setState + Markdown 渲染会卡死。
### 6.1 全接收、惰性渲染
- **所有**并行 run 的流都正常订阅(不丢数据):每个席位的增量文本**累积进 store / ref**(`Map<agentId, { text, status, charCount }>`)。
- **只有被选中(主区显示)的席位**才把累积文本挂到 DOM 渲染(Markdown)。
- **未选中**席位:只更新内存 + 头像条上的轻量进度(字数 / loading),**不挂载消息 DOM**。
- 切换选中时:把该席位已累积的全文一次性渲染 + 继续接增量。
### 6.2 渲染节流
- 选中席位的流式渲染**节流/合帧**:用 `requestAnimationFrame` 或 ~50–100ms 批量 flush 累积的 token,而不是每个 token 一次 setState。
- Markdown 渲染对"流式中"可降级(纯文本 / 轻量渲染),**完成后**再做完整 Markdown(现有 `MessageBubble` 的 `streaming` 标志可复用:streaming 时简渲、done 后全渲)。
### 6.3 头像条进度
- 头像条只显示**廉价**信息:loading 动画 + 可选字数(`charCount`,从内存 Map 取,节流更新,例如每 500ms 刷一次)。不触发消息体渲染。
### 6.4 数据流(建议结构)
```
streamMultiAgent(seat).onTextDelta(text) →
parallelStore.append(seatId, text) // 纯内存累积,O(1)
if seatId ∈ selectedParallelSeats: // 仅选中的进 React state(节流)
scheduleFlush(seatId) // rAF / 节流 setState
else:
bumpCharCount(seatId) // 仅更新头像条进度(节流)
```
### 6.5 验收(Phase 6)
- 6+ 席位并行时页面不卡(输入 / 滚动 / 切换流畅)。
- 切到一个之前隐藏的席位,能立刻看到它**到目前为止的全部**输出(证明流没丢、只是没渲染)。
---
## 7. 流程图执行态可视化(DAG 版)
- 把现有 `DispatchChainModal.tsx`(横向**线性**流程图)扩展 / 复刻成 **DAG 流程图**:
- 按 stage 分**列**,stage 内席位**纵向并列**,stage 间用连接线。
- 节点状态复用 `ChainNodeState`(pending / active / done)+ 现有动画(`animate-ping` / `Loader2Icon` / `CheckIcon`)。
- 末尾接 `共识 → 结果绘制` 里程碑(沿用现有)。
- 用途:编辑器里的只读预览(§2.2 底部)+ 执行时的总览(可选小窗 / 顶部缩略)。
```
总控 ─▶ ┌ Stage1 ┐ ─▶ ┌ Stage2 ┐ ─▶ ┌ Stage3 ┐ ─▶ 🤝共识 ─▶ 📊报告
│ 情报● │ │ 方案✓ │ │ 执行○ │
└────────┘ │ 风险● │ └────────┘
└────────┘
```
### 7.1 验收(Phase 7)
- 编辑器底部 / 执行时能看到 DAG 流程图,节点状态随执行实时更新(active/done)。
### 7.2 实施结论(2026-06-07,✅ 已完成)
- **新组件** `components/DagFlowChart.tsx`:纯展示组件,复刻 `DispatchChainModal` 的节点 / 连线 / 里程碑视觉(`animate-ping` 光环 + `Loader2Icon` 转圈 + `CheckIcon` 打勾 + `rt-chain-connector` 连线动画),但按 **stage 分列**:总控 ─▶ ┌Stage1┐ ─▶ ┌Stage2┐ ─▶ 🤝共识 ─▶ 📊结果绘制,stage 内席位**纵向并列**。`compact` 紧凑模式给缩略图用。所有状态由 props 传入(`stages` / `resolveSeat` / `seatStates` / `coordinatorState` / `consensusState` / `reportState`);不传 `seatStates` → 全 pending(只读预览)。`aggregateStageState` 把 stage 内席位聚合成列状态(任一 active→active、全 done→done)驱动 stage 间连线。
- **编辑器只读预览**(§2.2 底部):`BusinessChainEditorPage` 底部加「编排预览」section,把 `stageGroups`(去空 stage) 映射成 `DagFlowChart` 的 stages + `resolvePreviewSeat`(席位名/配色),`compact`、全 pending、随编辑实时变。
- **执行态总览**(Step 2 顶部缩略):`useStep2Orchestration` 新增 `dagNodeStates: Record<agentId, ChainNodeState>`,`runDagOrchestration` 跨 stage 维护(进入 stage→该批 active、席位终态→done、`resetStep2State` 清空);经 `RoundtablePlanningPage` 把 `orchestrationMode/orchestrationPlan/dagNodeStates` 传到 `Step2Panel`,dag 模式下会话区顶部 `compact` 渲染 DagFlowChart。总控/共识里程碑状态由 `Step2Panel` 从节点状态 + `isOrchestrating`/`hasConsensus` 派生(中转轮时无席位 active + isOrchestrating → 总控 active)。
- 改动文件:`components/DagFlowChart.tsx`(新)、`pages/BusinessChainEditorPage.tsx`、`hooks/useStep2Orchestration.ts`、`components/Step2Panel.tsx`、`pages/RoundtablePlanningPage.tsx`。typecheck 零新增(基线 65)。
- **下次实地验收**:①编辑器底部随分层编辑实时画出流程图;②Step2 dag 模式顶部出现缩略流程图,跑起来后当前 stage 列点亮(active 转圈)、完成打勾、中转时总控亮、最后共识亮。
---
## 8. 分步实现计划(每步可独立交付 + 验收)
| Phase | 内容 | 主要文件 | 依赖 | 可独立验收 |
|---|---|---|---|---|
| **1** | DAG 数据模型 + 校验 + 持久化字段 | `lib/dag-types.ts`(新)、draft step2 读写 | — | 纯函数单测:`validatePlan` / `chain→dag` 兼容转换 |
| **2** | 分层编排编辑器(先无拖拽:点选加入/移出) | `components/DagPlanEditor.tsx`(新)、Step2 模式切换 | P1 | 能编排并保存/恢复,非法编排有提示 |
| **2.5** | 编辑器拖拽 | 同上 + `@dnd-kit`(若引入) | P2 | 拖拽分配/重排 |
| **3** | `runDagOrchestration`:批内并行 + 批间串行 + 依赖摘要注入 | `hooks/useStep2Orchestration.ts`、`api/multi-agent.ts` | P1 | 后端日志并行 special run 时间重叠;stage 严格串行 |
| **4** | 并行禁写文件 run policy | `roundtable_run_policy.py`、`multi_agent.py`、`api/multi-agent.ts` | P3 | 并行 run 的 excluded_tools 含 write_file/bash/... |
| **5** | 并行显示骨架:收起左侧 + 头像条 + 两列 + 选中切换 | `components/Step2Panel.tsx`、`components/ParallelSeatStrip.tsx`(新) | P3 | 见 §5.4 |
| **6** | 流式高性能:全接收 / 惰性渲染 / 节流 | `hooks/useStep2Orchestration.ts`(parallelStore)、`Step2Panel` | P5 | 见 §6.5(6+ 并行不卡) |
| **7** | DAG 流程图执行态可视化 | `components/DagFlowChart.tsx`(复刻自 `DispatchChainModal`) | P2、P3 | 见 §7.1 |
| **8** | 边界 / 异常 | 各处 | 全部 | 见 §9 |
> 落地顺序建议:**P1 → P3 → P4 → P5 → P6**(先把"能并行跑 + 能看 + 不卡"打通),**P2 编辑器**可与 P3 并行开发(先用一份 hardcode/简单 UI 的 plan 驱动 P3 联调),**P7 / P2.5 / 2.3** 作为增强后置。
---
## 9. 边界与异常(Phase 8)
- **某并行节点失败 / 超时**:单个席位失败不拖垮整 stage;标该节点 error,stage 其余 ✅ 仍推进;收口时把"X 失败"如实告知总控(复用交付状态 ⬜ + 失败标记)。
- **依赖未满足**:调度器保证 stage 串行,理论不会发生;防御性校验 stage N 启动前 stage <N 必须全部终态(done/failed)。
- **人工干预 / 暂停 / 恢复**:在 stage 边界检查 `isStale()`(epoch);干预指令插入后,从"下一个未开始的 stage"继续(已完成 stage 不重跑)。
- **并行禁写文件 vs 席位本想写文件**:在席位 SOUL / 派活提示里说明"本轮为并行研讨,产出请直接写在回复正文,不要写文件",降低模型困惑。
- **窄屏**:主区两列降为一列 + 头像条横向滚动。
- **单 stage 全并行(一次性 N 个)**:给并行度 warning(远程模型并发限流 / 成本)。
---
## 10. 与现有代码的衔接点速查
- 编排循环:`hooks/useStep2Orchestration.ts`(`runOrchestration` / `runChainOrchestration` 旁新增 `runDagOrchestration`;复用 `isStale()` epoch、`abortAllInFlight`、`appendStreamingDialogue` / `appendToDialogue`)。
- 流式 API:`api/multi-agent.ts`(`streamMultiAgent`,新增 `parallel_no_file` 入参;`onTextDelta(text, messageId)` 已是按席位回调,天然支持并行分流)。
- 显示:`components/Step2Panel.tsx`(消息列表 `step2RoundtableDialogues.map`;新增头像条 `ParallelSeatStrip` + 两列容器 + 折叠左侧)。
- 流程图:`components/DispatchChainModal.tsx` / `lib/job-types.ts` / `lib/business-chain.ts`(复刻成 DAG 版)。
- 常量 / 元数据:`lib/roundtable-constants.ts`(`BUILTIN_AGENT_META` 头像/中文名、`buildAgentNameToRole`)。
- 后端 run policy:`app/gateway/roundtable_run_policy.py`(新增 `seat_parallel`)、`app/gateway/routers/multi_agent.py`(`_special_run` 读 `no_file` flag)。
- 交付摘要:`GET /api/threads/{id}/last-ai-message?max_chars=N`(依赖注入 stage N 输入用)。
---
## 11. 一句话总结
把「线性链条」升级为「分层 DAG」:**编辑器**用分层 stage(拖卡片)表达串/并;**调度**用 `Promise.all` 批内并行、批间 barrier 串行,依赖摘要在 stage 边界注入;**并行禁写文件**避免沙箱打架;**显示**收起左侧、头像条切换、主区两列、未选中只接流不渲染(rAF 节流)保证不卡。全部建立在现有 special/leader run + 轻量交付接口之上,后端几乎零新接口。
---
## 12. 已知问题与排查记录(2026-06-06)
### 12.1 ❗并行席位「步骤条」不显示 —— 根因:`seat_parallel` 工具面禁得过多
**现象**:并行两列里的席位气泡看不到步骤条(`MessageStepsCard`),而串行席位有。
**排查结论(已定位,待修)**:
1. **步骤条只渲染「工具调用」步骤**。`MessageStepsCard` 的 `isToolStep` 只认 `tool_calling:<toolName>` 的步骤进折叠卡;纯阶段步骤(`connecting:` / `streaming:` 这类 reasoning)只渲染成**底部一行小灯泡**,不出大卡(见 `MessageStepsCard.tsx` L80-100)。
2. **并行席位几乎调不动任何会产生步骤的工具**。后端 `app/gateway/roundtable_run_policy.py`:
- `_SEAT_EXCLUDED = [web_search, ask_clarification, present_files, view_image, agent_orchestration]` —— **注意 `web_search` 对所有席位(串行也)就禁了**(注释「圆桌研讨里联网搜索一律禁用」)。
- `_SEAT_PARALLEL_EXCLUDED = _SEAT_EXCLUDED + [write_file, bash, str_replace]`。
- 于是并行席位可用工具几乎只剩 `read_file / ls / task / write_todos / memory / hindsight_*`,研究/分析类席位基本一个都不会调 → **没有任何 `tool_calling` 步骤** → 步骤条(大卡)不出现。
3. **对比串行为何「有步骤条」**:串行席位(`seat` policy)**能 `write_file`**,会产生 `tool_calling:write_file` 步骤 → 大卡出现。并行被禁了 write_file + 又没有 search,所以一个工具步都没有。
**用户诉求**:并行时**只禁真正会冲突的写操作**,搜索信息 / 调用技能等**必要操作不要禁**。
**关键事实(影响方案选择)**:圆桌每个席位是**独立 thread = 独立 sandbox**(后端 per-thread 隔离 `.deer-flow/users/{uid}/threads/{tid}/`)。所以并行席位写文件**在后端并不会互相冲突**;当初 §4「并行禁写文件」的「打架」其实主要是**前端单沙箱预览**(`activeArtifactThreadId` 只能同时显示一个)+ 产物归集到正文的简化考量,并非后端真冲突。→ 这意味着「并行禁写」的必要性比原设想弱,可放宽。
**✅ 决策:选 A(2026-06-06 已实施)**:把 `seat_parallel` 放宽到**只禁 `write_file / bash / str_replace`**(防前端单沙箱预览串台 + 产物走正文)+ 仍禁 `ask_clarification / present_files / view_image / agent_orchestration`(打断/越权类),但**放开 `web_search` 与技能**(搜索不碰 sandbox;技能靠 `read_file`)。改在 `app/gateway/roundtable_run_policy.py` 的 `_SEAT_PARALLEL_EXCLUDED`(**不再继承** seat 的 web_search 禁用,显式列举),测试 `tests/test_roundtable_run_policy.py` 同步更新(`test_seat_parallel_allows_web_search_and_skills`)。
→ 并行席位现在能联网搜索/用技能 → 产生 `tool_calling` 步骤 → 步骤条显示。
> 备选 B(连写文件也放开,沙箱按 thread 隔离)/ C(重新评估「圆桌串行也一律禁 web_search」)**本期不做**,留档。串行席位目前仍禁搜索(`seat` policy 不变)。
### 12.2 其它已修(本轮反馈)
- 最左侧**全局工作区侧栏**并行时收起:改用与「打开沙箱」同一路径 `useSidebarSafe()?.setOpen(false)`(之前只收了圆桌「参与角色」栏 `step2SidebarExpanded`)。并行结束**不自动展开**(与沙箱收起不回弹一致)。
- 并行两列改回**完整 Markdown**(撤销 §6.2 流式纯文本降级 `plainWhileStreaming`),与串行同款 `Step2Message`。
- 头像条**不再显示字数**(去掉 charCount 徽标 + 字段)。
- **致命 bug 已修**:用户干预 / 并行跑完后**不再重跑整张 DAG**。`dagPlanExecutedRef` 闸门 + DAG 各阶段跑完后释放锁 `await runOrchestration(handoff)` 交回总控继续派活/收口;后续干预/恢复一律走 leader 自由派活。
- **并行两列宽表溢出已修(2026-06-07)**:模型输出的 Markdown **表格**(如「法律免责条款」表)没有宽度约束 / 横向滚动容器,在并行两列的窄列里会**超出左列、盖到相邻列**。修法:`MessageBubble.sharedMarkdownComponents.table`(+ `markdown-components.tsx`)把 `<table>` 套进 `<div class="max-w-full overflow-x-auto">`——表格保持自然宽度、超出时**列内横向滚动**而非撑破列宽。`min-w-0` 链(grid `minmax(0,1fr)` → 列 `min-w-0` → 气泡 `flex-1 min-w-0`)本就齐全,只差 table 这一层。
- **关于「并行席位无步骤条」**:步骤条(`MessageStepsCard` 大卡)只在席位产生 `tool_calling` 步骤时出现(§12.1)。纯分析型席位若本轮**直接写分析正文、没调用任何工具**(web_search 等),就只显示一行「正在输出」思考指示、不出大卡——这是设计使然,非解析失败。要确认是否真调了工具:浏览器 devtools 设 `window.__MULTI_AGENT_DEBUG__=true` 看 `frame# event=messages` 里有无 tool_calls,或查后端 special run 日志。
### 12.3 ✅需求变更:Stage 间「经总控中转 + 广播」(2026-06-06 提出,2026-06-07 已实施)
**当前实现(方案 B,§3.2)**:DAG 各 stage 由前端**直接**派活,stage 间不跑 leader,靠前端把 `buildDependencySummary` 拼进下一 stage 的 task(`priorSummary`)。各 stage 跑完后才 `await runOrchestration` 交回总控收口。
**新诉求**:业务链条调用时,**每一轮 stage 之间都要经总控中转 + 广播**,而不是前端直接串下去。具体:
1. 第 1 轮并行调用 stage1 的 A、B、C;
2. A/B/C **全部完成后**,把这一批的**输出广播出来**(广播给其它席位 + 总控 thread);
3. **总控(leader)接收**这批交付 → 再**往下继续派活**(派 stage2 的 D、E);
4. 派活时把(承上的)消息**广播给下一轮的 D、E**;
5. 以此类推,直到所有 stage 跑完再收口。
> 即把 §3.2 的「方案 B 前端直派」改成 **「方案 A' = 计划约束下的总控中转」**:stage 边界插入一轮 leader,由它综合上一批交付并驱动下一批(下一批席位仍由编排计划**钉死**为 D、E,不是 leader 自由乱派——leader 负责「承上启下 + 广播上下文」,计划负责「派给谁」)。
**实施要点(下次会话)**:
- **每个 stage 完成 → 广播该批输出**:后端 `_special_run` 已对每个 seat 做 detached 后广播(「子智能体X完成…交付内容为:…」→ 其它席位 + 总控)。但它是 **detached/best-effort + 截断**;要让总控**可靠**地在下一轮前看到,需在 stage barrier 处**等广播落地**(或前端在 stage 边界用 `GET /threads/{id}/last-ai-message` 主动收齐各席位交付再喂给 leader)。
- **插入 leader 中转轮**:`runDagOrchestration` 在进入下一 stage 前,跑一轮 leader(`synthesisMode=true` 让后端注入上一批**完整**交付),让总控产出一段「承上启下」说明;把这段说明(而非机械 `priorSummary`)作为下一 stage 席位 task 的上下文 / 或广播给下一批席位 thread(复用 `POST /threads/{id}/messages/append`)。
- **下一批仍由计划钉死**:leader 中转轮**不**自由决定派给谁(计划已定 D、E);可参考 `buildChainLeaderTask` 的「约束 leader 只派给指定席位」思路,或干脆 leader 只做综合、前端照计划派 D、E 并把 leader 综合 + 广播注入其 task。
- **与「并行禁写」「§12.1 放开搜索」并存**:中转/广播不影响工具面。
- **与致命 bug 修复并存**:最后一个 stage 跑完仍走「交回总控(`runOrchestration`)收口/继续派活」;用户干预仍走 leader、不重跑 DAG。
**影响文件(预估)**:`hooks/useStep2Orchestration.ts`(`runDagOrchestration` 的 stage 循环里加 leader 中转 + 广播收齐)、可能复用后端 `_leader_run`(synthesisMode 注入全文)/ `/messages/append`(广播)/ `/last-ai-message`(收齐交付)。后端基本无需新接口。
**✅ 实施结论(2026-06-07)**:后端**零改动**,全部在前端 `runDagOrchestration` 内完成,复用既有「传摘要按需传全文」优化链路:
1. **中转轮位置**:每个**非最后**的 stage barrier 到达后(`Promise.all` 收齐本批交付),插入**一轮 leader**「中转轮」,展示为「总控协调(承上启下)」气泡。最后一个 stage 后**不**插中转,综合/收口仍交给末尾的 `runOrchestration` 接管(与致命 bug 修复一致)。
2. **可靠收齐交付(传摘要)**:中转轮跑 `streamMultiAgent({agentType:"leader", synthesisMode:false})`。后端 `_leader_run` 本就在每轮 leader 前用 `_collect_delivery_status`(走**轻量 `GET /last-ai-message?max_chars=600` 接口**)**客观核对**各席位真实交付并把**摘要**注入总控输入(`_build_seat_status_block`)——比 detached 广播可靠(直接实读各席位 thread,不依赖 best-effort 广播是否落地)。故中转轮传 `synthesisMode:false`:只综合本批、**不收口**,只注入摘要;**完整交付正文**留到末尾收口的 synthesis 轮(`_build_full_deliverables_block`,全 ✅ 才注入)按需读取——即「**传摘要按需传全文**」。
3. **承上启下驱动下一批**:中转轮产出的综合说明存进 `leaderBridge`,作为**下一 stage** 席位 task 的上下文(`buildStageTask({priorSummary: leaderBridge, priorIsBridge:true})`,措辞为「承上启下综合说明」而非机械「交付摘要」),替代原来的 `buildDependencySummary(deliveries)`。中转失败/无产出时回退机械依赖摘要,不阻断。
4. **下一批由计划钉死**:中转轮提示(`buildDagTransitTask`)明确告知总控「下一阶段席位已由编排计划钉死为 X、Y,你**不要**自行 `agent_orchestration` 派活、**不要**收口」,前端仍照计划直派下一批;即便弱模型仍派了活,前端忽略其 `dispatched`、只取 `content` 当 bridge。
5. **广播**:本批输出广播给其它席位 + 总控 = 后端 `_special_run` 既有 detached 后广播(截断摘要);承上消息给下一批 = 注入其 task `new_message`(席位 run 输入必见,比 `/messages/append` 更可靠,省一次写盘)。
6. **澄清/抢占/失败**:中转轮触发澄清 → 暂停 DAG(回复后走 leader 自由派活、不重跑 DAG,沿用 `dagPlanExecutedRef` 闸门);被 `isStale()`/abort 抢占 → 静默收尾;异常 → 该气泡标错误、清空 bridge 继续。
**纯函数**:`lib/dag-types.ts` 新增 `buildDagTransitTask`(中转轮提示)+ `buildStageTask` 加 `priorIsBridge` 形参;`lib/dag-types.test.ts` 加 4 个单测(共 27 个全过)。
> ⚠️ **§12.3 的「单独中转轮 + 前端直派」已被 §12.4 取代**(2026-06-07 同日):见下。`buildDagTransitTask` / `buildStageTask` / `buildDependencySummary` 仍保留在 lib(带单测),但 `runDagOrchestration` 不再调用它们。
### 12.4 ✅ 需求再变更:DAG 改成「总控驱动每 stage 派活」(2026-06-07,§3.2 方案 A)
**新诉求(用户原话)**:把并行(DAG)逻辑改得和串行一样——串行时总控给多个智能体派活、它们完成后汇报给总控、总控再继续往下派活;并行也改成「由总控判断要执行哪些步骤,这些步骤完成后再汇报给总控,总控继续往下派活」。
**已确认的两个关键决策**:
1. **每批「派给谁」**:✅ **计划钉死成员,但走总控派活**(不是总控自由乱派)。即 stage 成员仍由编排计划定死,但改成总控每轮**真正** `agent_orchestration` 派活给这些席位(约束只派给它们、且每个都派到)。= §3.2「方案 A」+ 批内并行。
2. **适用范围**:✅ **只改 dag 模式**(recommend 仍串行逐个、chain 不动)。
**实现(后端零改动,全在 `runDagOrchestration`)**:每个 stage 两步——
- **(a) 总控派活轮**:跑一轮 `leader`,提示 `buildDagStageLeaderTask`(首 stage 带任务背景;后续 stage 靠后端注入的此前交付摘要承上;列出本 stage 钉死席位的 `agent_name`,要求逐个派到、不派清单外、不收口)。`synthesisMode:false` → 后端 `_build_seat_status_block` 走轻量 `last-ai-message` 注入各席位**摘要**(传摘要按需传全文)。
- **(b) 批内并行**:本 stage 席位并行跑总控派给各自的 task——`pickDagSeatTask(leaderRes.dispatched, content, agentId, name)` 命中其 `agent_name`;总控漏派/写偏则回退总控正文/模板,**不借用别席位 task**(多席位并行避免张冠李戴)。完成后交付经后端 `_special_run` detached 后广播回总控;下一 stage 的派活轮即可看到。
- barrier 后进入下一 stage,总控再派下一批。全部 stage 跑完 → 交回 `runOrchestration` 收口(synthesisMode 按需注入全文)。
**相比 §12.3 的变化**:§12.3 是「前端直派 + stage 间单独插一轮只综合的中转轮」;§12.4 把**每个 stage 本身**就改成「总控派活轮 + 批内并行」——总控既承上(注入的摘要)又派活,不再需要单独的中转轮。与 chain 模式同构,区别是一次钉死一批、批内并行。
**纯函数**:`lib/dag-types.ts` 新增 `buildDagStageLeaderTask`(每 stage 派活提示)+ `pickDagSeatTask`(取某席位 task,多席位不张冠李戴);`lib/dag-types.test.ts` 加 5 个单测(共 32 个全过)。`runDagOrchestration` 不再用 `buildStageTask/buildDependencySummary/buildDagTransitTask`(保留在 lib)。
**Phase 7 流程图兼容**:总控派活轮无席位 active + `isOrchestrating` → 流程图「总控」节点显示 active(承上派活中),随后该 stage 列点亮,无缝衔接。
**下次实地验收**:①每个 stage 前出现「总控协调(派活中/承上派活中)」气泡,内容是总控对本 stage 席位的派活说明;②后端日志该轮 leader 的 `_partition_dispatch` 派给的正是本 stage 钉死席位;③这些席位并行跑(special 日志时间重叠);④完成后下一 stage 总控派活轮能看到上一批交付摘要(`[leader-roster]` 日志)。
### 12.5 ✅ 流程图弹窗化 + DAG「快速/沙箱」执行子模式(2026-06-07)
三个改动一起做:
**(1) 流程图改成弹窗**:Step2 顶部不再内联 `DagFlowChart`(挤占两列消息高度,截图 `样式优化-1.png`)。改成一条细栏「分层编排执行中 · 共 N 个阶段 + [查看编排流程图]」按钮,点开 `Dialog` 看**全尺寸**流程图(非 compact)。编辑器(`BusinessChainEditorPage`)底部预览保持内联不变。改 `Step2Panel.tsx`(加 `dagFlowOpen` state + Dialog)。
**(2) 推荐弹窗新增 DAG 执行子模式(快速/沙箱)**:`BusinessChainPicker` 在选中链条**有分层并行**(走 dag)时,「进入研讨」旁出现「执行方式:快速·双列禁写 / 沙箱·单列可写」二选一(只 dag 时出现,§已确认)。
- **快速(`fast`,默认)**:同 stage 席位**并行**、主区**双列**、`parallelNoFile=true`(seat_parallel **禁写文件**)。= 之前的行为。
- **沙箱(`sandbox`)**:同 stage 席位**仍并行**,但主区**单列**(`selectedParallelSeats` 最多 1)、`parallelNoFile=false`(seat policy **允许写文件**);点头像切换查看,沙箱跟随当前席位。
- 数据流:`SeatSelectionResult.dagExecMode` → `RoundtablePlanningPage` state(持久化进 `DraftStep2(Run)Snapshot.dagExecMode`,mirror orchestrationPlan)→ `useStep2Orchestration` option → `runDagOrchestration`。
**(3) 沙箱跟随当前显示席位(防 bug 重点,用户特别强调)**:`useStep2Orchestration`
- `sandboxFollowThreadIdRef` = 当前显示席位的 thread;`openSeatVirtualArtifact` 的 auto 守卫:sandbox 模式下,**后台并行席位**写文件时若不是当前显示席位,**不自动打开**(否则沙箱内容和主区列表错位)。
- 切换显示席位的 effect(keyed on `selectedParallelSeats` + `dagExecMode`):该席位**有产物**(stub 里有 write_file/str_replace)→ `openSeatLatestArtifact` 强制打开它最新的文件;**无产物**→ 关闭沙箱面板(`setActiveArtifactThreadId(null)` + `deselectArtifact` + `setArtifactsOpen(false)`)。
- 当前显示席位 live 写文件 → `handleArtifactToolPhase` auto-open,守卫放行(follow===该席位)。后台席位写 → 守卫拦掉、只缓存 stub;切到它时 effect 再打开。
- `toggleParallelSeat`:sandbox 单列 = 点头像切换(始终保留 1 个,不取消到空);fast 双列 = 原 LRU 2 个。
**改动文件**:`lib/business-chain.ts`(SeatSelectionResult.dagExecMode)、`components/BusinessChainPicker.tsx`(子模式 UI)、`api/drafts.ts`(快照 dagExecMode)、`pages/RoundtablePlanningPage.tsx`(state/snapshot/hydrate/confirm/reset 全链)、`hooks/useStep2Orchestration.ts`(option + parallelNoFile/单列 + 沙箱跟随 + toggle cap)、`components/Step2Panel.tsx`(流程图弹窗 + 单列 grid)。typecheck 零新增(基线 65),dag-types 32 单测全过。
**下次实地验收(沙箱跟随是重点)**:①推荐弹窗选有并行的链条 → 出现「快速/沙箱」二选一;②快速=双列禁写(无沙箱);③沙箱=单列、席位能写文件、点头像切换列表时沙箱内容**同步**切换:有产物显示、无产物关闭;④后台席位写文件不会把沙箱抢到非当前列表;⑤流程图点按钮弹窗、全尺寸、节点随执行点亮。
### 12.6 ✅ 修:并行席位「跟着派活」(2026-06-07)
**现象**(截图 `bug-1.png`):总控派活后,并行席位的气泡里显示的是**总控的整段派活叙述**(「我来调度各智能体…第1阶段任务清单:席位1…席位2…」),还去调 `skill_list` 找 `agent_orchestration`——席位在**模仿派活**。
**根因**:总控这轮**没真正调用 `agent_orchestration` 工具**(弱模型 deepseek 用文字罗列了派活计划)→ 后端解析的 `dispatched=[]` → 前端 `pickDagSeatTask` 回退把**总控整段叙述**喂给**每个**席位 → 席位照抄叙述、模仿派活。(席位本就被 run policy 禁了 `agent_orchestration`,**派不了活**,只是空转一轮叙述 + 调 `skill_list`。)
**修 v1(护栏,效果不足)**:`pickDagSeatTask` 加角色边界护栏 + 回退措辞改「挑出属于你的那份」;`buildDagStageLeaderTask` 加「必须真正调用工具」。→ **没拦住**:总控对 deepseek 仍只写文字不调工具(`dispatched` 还是空),席位拿到的**任务正文本身**就是那段「我来调度…席位1/2/3」,护栏被淹没;3/4 席位仍模仿派活,有的甚至绕道 `bash`+`python requests.get('/api/agents')` 调内部接口派活(bug-1~4)。
**✅ 修 v2(根治:回退绝不注入派活叙述)**:`pickDagSeatTask` 去掉 `leaderContent` 入参——
- **命中**总控结构化派活(真调了工具)→ `请你作为「X」,完成总控派给你的本阶段任务:<dispatched task>` + 护栏;
- **未命中 / `dispatched` 空**(总控只写文字)→ **绝不**把总控派活叙述喂给席位,改给**干净本职任务**:「请你作为「X」,从你的专业职责出发,独立完成本阶段属于你的分析与交付。任务背景与此前各阶段交付已在你的会话上下文里(后端广播注入),据此承接」+ 护栏。护栏强化到「不要用 bash/python/脚本访问内部接口(/api/agents、agent_orchestration)派活、不要复述『我来调度』总控口吻」。
- `buildDagStageLeaderTask` 的「必须真正调用工具」保留(让命中率更高、尽量走 exact-match)。
- `dag-types.test.ts` 改 2 单测(共 33 过)。typecheck 65。
> ⚠️ **残留次要污染源(若仍复发再处理)**:DAG 每个 stage 的 leader 轮,后端 `_leader_run` 会把 leader 的输入(含「请你作为总控…派活」)**预广播**进所有席位线程(`总控智能体收到用户消息:…`)。席位的**当轮任务**已干净(修 v2),这条只是历史上下文,理论上被「最新的干净任务 + 护栏」压制。若实测仍模仿派活,下一步:后端给 DAG leader 轮加 `suppress_pre_broadcast` / 或广播一条**净化**过的背景(去掉派活措辞)。
### 12.7 ✅ 修 4 项(2026-06-07):席位仍派活(v3 后端)+ 沙箱关不掉 + 并行不自动下滑 + 流程图只到 Step2
实测 §12.6 仍有席位派活(视觉设计师 `skill_manage` 找 agent-orchestration、列 dispatch all 4)——**坐实**了「残留次要污染源」:后端 leader 预广播把派活 prompt 推进席位线程,弱模型从历史里捡到派活意图。一起修了 4 项:
1. **席位仍派活(根治·后端)**:`_leader_run` 加 `suppress_pre_broadcast` 参数;DAG「每 stage 派活轮」前端传 `suppressPreBroadcast: true`(`api/multi-agent.ts` → `suppress_pre_broadcast`,run/stream 读取)。为真时:**不**把总控派活 prompt 预广播给席位、**也不**把「总控给 X 派活」detached 广播给兄弟席位。recommend/chain 不传该 flag,行为不变。配套:`pickDagSeatTask` 加 `seedMessage` 入参——预广播抑制后席位线程没有用户原始消息了,把**任务背景**直接写进席位 task(此前各阶段交付仍由 special 完成后广播注入)。→ 席位线程里**任何地方都没有派活语句**,根治模仿。
2. **沙箱关闭按钮关不掉**:沙箱跟随 effect 之前每次 render(deps 身份变)都重跑 → 把用户刚关的沙箱强制重开。加 `lastReconciledSeatRef` 守卫:**只在显示席位真正变化时**才开/关沙箱;用户手动关闭(席位没变)时 effect 早退,不再回弹。
3. **并行时自动下滑失效**:平铺列表 ↔ 网格(两列/单列)切换时 `scrollHeight` 突变、`scrollTop` 被浏览器 clamp → `useAutoScroll` 误判「用户上滚」而停跟随。`Step2Panel` 加 effect:`isParallelRunning` / `selectedParallelSeats` 变化时 `scrollStep2ToBottom()` 重新拉到底 + 恢复跟随。
4. **流程图只展示 Step2**:`DagFlowChart` 加 `showReportMilestone`(默认 true);Step2 弹窗 + 编辑器预览传 `false` —— 只到「共识」(Step2 终点),去掉「结果绘制」(那是 Step3)。
**改动**:后端 `multi_agent.py`(`_leader_run` suppress 两处广播 + run/stream 读 flag);前端 `api/multi-agent.ts`、`lib/dag-types.ts`(pickDagSeatTask + seedMessage)、`hooks/useStep2Orchestration.ts`(传 flag + seed + 沙箱跟随守卫)、`components/Step2Panel.tsx`(自动下滑 effect + showReportMilestone)、`components/DagFlowChart.tsx`、`pages/BusinessChainEditorPage.tsx`。`dag-types.test.ts` 共 **34 单测过**;前端 tsc 65 基线零新增;后端 `py_compile` OK。
### 12.8 ✅ 席位产出 md 文件 + 禁写 .py(2026-06-07)
**现象**:某次 7 席位**全都没产出 md 文件**;且一直有的问题——席位**有时写 .py 脚本**(用户要求:不写 .py,其它格式可以)。
**根因(没 md)**:两层都堵了——(a)**快速模式** `seat_parallel` policy 禁了 `write_file`(原「产物走正文」设计);(b)DAG 席位**提示词**写的是「直接输出你的交付正文」,即使沙箱模式(允许写)也不会去写文件。
**修**:
1. **后端 policy 放开写文件、统一禁 bash**(`roundtable_run_policy.py`):
- `seat_parallel`:去掉 `write_file` / `str_replace` 禁用(**可写 md**),保留禁 `bash` + 打断/越权类;仍放开 `web_search`。
- `seat`:新增禁 `bash`(原来允许)——圆桌席位是「文档产出者」,不该跑 shell/python 脚本(弱模型爱写 .py 再跑生成产物,乱且抓不到预览);改为**直接 write_file 写成品文档**。
- `report`:`bash` 已随 `seat` 禁,改为 `[*_SEAT_EXCLUDED, "str_replace"]`(集合不变)。
- 测试 `test_roundtable_run_policy.py`:`test_seats_can_write_files_but_not_run_bash`(seat / seat_parallel 都可写文件、都禁 bash),6 用例过。
2. **前端提示词**(`pickDagSeatTask`)加「产物要求」:**把完整交付写成一个 .md 文件**(write_file 到 outputs)+ 正文给摘要;**禁写 .py 或任何脚本文件**,.md/.html/.txt 等其它格式都可以,唯独不要 .py。
3. **快速模式(双列)不自动开沙箱**(`openSeatVirtualArtifact` 新增守卫):放开写文件后,双列里多席位并发写文件会让单沙箱预览反复串台——fast 模式 DAG 并行批次**完全不自动开沙箱**(文件仍写盘,Step3/文件卡可查看);沙箱模式(单列)仍按「沙箱跟随当前席位」预览。
**关于 .py 的强度**:`write_file` 是全局工具(所有 agent 共用),无法在网关层按扩展名硬拦,故 .py 禁止走**提示词**(强约束)+ **禁 bash**(写了也跑不了,脚本生成产物的动机被掐断)。若实测仍偶发写 .py 且需**硬拦**,下一步:给 lead_agent 的 `write_file` 工具加一个「按 run context 禁某些扩展名」的开关,网关 `_run_payload` 对圆桌席位置 `[".py"]`(只影响圆桌 run,不动其它 agent)。
**改动**:`roundtable_run_policy.py`、`test_roundtable_run_policy.py`、`lib/dag-types.ts`、`lib/dag-types.test.ts`(35 过)、`hooks/useStep2Orchestration.ts`。前端 tsc 65、后端 py_compile OK + 6 policy 单测过。⚠️ **改了后端,需重启 Gateway**。
### 12.9 ✅ 席位「按需取前序全文」只读工具 read_peer_delivery(2026-06-07)
**诉求**:A,B,C → D,E 时,D,E 默认拿 A,B,C 的**摘要**,但识别到需要**全文**时也能取到。之前只有总控(leader)能「摘要默认+按需全文」(跨 thread 读),下游席位**不能**(席位间 thread 隔离、只有 1500 字截断广播)。
**实现**(真·按需,全文只在调用工具时才进 LLM 上下文):
1. **新 harness 只读工具** `read_peer_delivery(seat_name)`(`packages/harness/deerflow/tools/builtins/roundtable_peers_tool.py`):从 `runtime.context["roundtable_peer_deliveries"]`(网关注入的 `[{name,content}]`)按席位名取全文;空名→列出可取席位;非圆桌场景→「无」。只读 context dict,**不跨 thread、不碰 sandbox、无网络**,纯 `deerflow.*`(不违反 harness→app 防火墙)。注册进 `tools/tools.py` 的 builtin(始终在,只读无副作用)。
2. **网关注入 context**:`_run_payload` 加 `extra_context`;`_special_run` 收 `peer_deliveries` → 注入 `context.roundtable_peer_deliveries`;run/stream 读 `peer_deliveries`(规整 `[{name,content}]`)。**不进 prompt → 不调工具就不耗 token**。
3. **run policy**:`read_peer_delivery` 从 `leader`(总控自己跨 thread 读,不需要)+ `report`(Step3 不读前序)排除;**席位(seat/seat_parallel)保留**。
4. **前端**:`runDagOrchestration` 跨 stage 累积**完整交付** `priorDeliveries`,stage>0 席位的 special run 带 `peerDeliveries`(`api/multi-agent.ts` → `peer_deliveries`);`pickDagSeatTask` 加 `hasPeers` 形参,有前序时在 task 里提示「需要某前序席位全文时调用 read_peer_delivery('<名>'),平时用摘要」。
**最终语义**:D、E 默认看 A、B、C 的**摘要**(1500 字广播);识别到不够 → 调 `read_peer_delivery('情报收集')` 取**全文**。总控仍是摘要默认+综合轮全文。**= 用户要的「摘要默认、按需全文」对席位也成立了。**
**改动**:后端 `roundtable_peers_tool.py`(新)、`tools/tools.py`、`roundtable_run_policy.py`、`multi_agent.py`;前端 `api/multi-agent.ts`、`lib/dag-types.ts`、`lib/dag-types.test.ts`(36 过)、`hooks/useStep2Orchestration.ts`。前端 tsc 65;后端 py_compile OK + 6 policy 单测过 + 工具导入/行为 smoke 通过。⚠️ **改了后端 + 新增 harness 工具,需重启 Gateway**。
### 12.10 ✅ 修:沙箱模式第一个席位不自动开沙箱(2026-06-07,前端)
**现象**:沙箱(单列)模式并行时,**第一个**席位的消息列表不自动打开沙箱(要手动开);切换到其它席位时正常自动打开。
**根因**:沙箱跟随 effect(§12.5)在选中席位**还没写产物**时走「关闭沙箱」分支,里面同时调了 `deselectArtifact()` **和** `setArtifactsOpen(false)`。后者是 context 的 **wrapped setOpen**(`context.tsx:73`):`关闭时若 autoOpen 为真 → 把 autoOpen 置 false`。于是第一个席位刚选中(还没写文件)→ autoOpen 被关 → 该席位**随后 live 写文件**时,`openSeatVirtualArtifact` 的 `!flags.autoOpen` 守卫拦掉自动打开。切换到**已写过**文件的席位能开,是因为那条路走 `openSeatLatestArtifact`(force,auto=false,绕过 autoOpen 守卫)。
**修**(一行):关闭分支只保留 `deselectArtifact()`(它走 raw `setOpen(false)`,**不**动 autoOpen),删掉多余的 `setArtifactsOpen(false)`。这样面板关掉、但 autoOpen 仍为真 → 第一个席位 live 写文件时正常自动开沙箱。(若用户**手动**关过沙箱,autoOpen 本就为 false,不会自动重开 —— 尊重用户。)改 `hooks/useStep2Orchestration.ts` 沙箱跟随 effect。tsc 65。
### 12.11 ✅ 修:快速模式又生成文件了 + Fragment 警告(2026-06-07);记录无关 404
1. **快速模式不应生成文件(回退 §12.8 的过度放开)**:§12.8 为「让席位产 md」把 `seat_parallel`(快速模式)也放开了 write_file —— 但**快速模式(双列)的设计就是产物走正文、不写文件**,写文件归**沙箱模式**(单列 + 沙箱预览)。修:
- `roundtable_run_policy.py`:`seat_parallel` 重新**禁 write_file / str_replace**(保留禁 bash);`seat`(沙箱模式)仍可写。
- `useStep2Orchestration.ts`:`parallelNoFile = dagExecMode !== "sandbox"`(快速模式一律禁写,含单节点 stage;沙箱模式放开)。
- `pickDagSeatTask` 加 `allowFiles` 形参:沙箱(true)=写 .md 文件 + 禁 .py;快速(false)=产物走正文、不写文件。hook 按 `dagExecMode === "sandbox"` 传。
- 测试:`test_sandbox_seat_writes_files_fast_parallel_does_not`(6 过);`dag-types.test.ts` 拆成沙箱/快速两个产物要求用例(37 过)。
- **语义**:快速=双列、产物走正文、不写文件;沙箱=单列、写 .md、沙箱跟随预览。
2. **`Invalid prop data-lov-id supplied to React.Fragment`**:`DagFlowChart` 的 stage 循环用了具名 `<Fragment key>`,dev 代码标注插件(lovable-tagger)给它注入 `data-lov-id`,而 Fragment 只接受 key/children → React 警告。修:换成 `<div className="contents" key=...>`(display:contents 不生成盒子,Connector/StageColumn 仍是外层 flex 直接子项,布局不变,且能接收 data-*)。移除 `Fragment` import。
3. **(无关,非本期引入)`/api/getTokenByEmail?email=…%40local.deerflow` 404 ×60**:来自 `src/core/studio/api.ts` 的 `fetchStudioToken`(studio/expert-auth 引导),用户邮箱被映射到 legacy `@local.deerflow` 域、后端 `getTokenByEmail` 不认 → 404,React Query 跨多个 `useStudioToken` 调用点重试 → 重复多次。**与圆桌 DAG 改动无关**(圆桌头像是本地 SVG,本期未新增任何前端 GET)。需要的话单独排查 studio token 链路。
---
## 13. 实施进度(截至 2026-06-06,下次会话续)
| Phase | 内容 | 状态 |
|---|---|---|
| 1 | DAG 数据模型 + 校验 + 持久化字段(`lib/dag-types.ts`,24 单测) | ✅ 完成 |
| 2 | 分层编辑器(**做进业务链条页**):链条加 `stages` 字段(前后端 + alembic `20260607_03` + 11 后端单测);`BusinessChainEditorPage` 竖列分组 Stage(DnD + 按钮);纯逻辑 `lib/chain-stages.ts`(19 单测) | ✅ 完成 |
| 3 | `runDagOrchestration`:批内 `Promise.all` 并行 + 批间 barrier 串行 + 依赖摘要注入(方案 B 前端直派) | ✅ 完成 |
| 4 | 并行 run policy `seat_parallel`(`roundtable_run_policy.py` + `multi_agent.py` 读 `parallel_no_file`,6 后端单测);**§12.1 方案 A 已放开 web_search/技能** | ✅ 完成 |
| 5 | 并行显示:收起左侧 + 头像条 + 两列 + 选中切换 | ✅ 完成(本轮修了侧栏/Markdown/字数) |
| 6 | 流式高性能:ref 全量累积 + 节流 flush(仅选中渲染) | ✅ 完成 |
| 7 | DAG 流程图执行态可视化(`DispatchChainModal` 复刻成按 stage 分列;后改成**弹窗**见 §12.5) | ✅ 完成(2026-06-07) |
**端到端闭环已通**:链条页编排分层 → 存库 → 选链条进 Step2 → `runDagOrchestration` 并行直派 → 各阶段完交回总控派活/收口。
**✅ §12.3 已实施(2026-06-07)**:见 §12.3「实施结论」。`runDagOrchestration` 在每个非末尾 stage barrier 后插入 leader 中转轮(`synthesisMode:false` 走轻量 `last-ai-message` 注入摘要、收口轮才按需全文),承上启下驱动下一批;后端零改动;`dag-types.ts` 加 `buildDagTransitTask` + `buildStageTask.priorIsBridge`,27 单测全过。
**下次会话 TODO(优先级从高到低)**:
1. **运行时实地验收 §12.3**:①每个 stage 之间出现「总控协调(承上启下)」气泡,内容是对上一批的综合;②后端日志中转轮 `[leader-roster] ... 注入席位交付状态` 显示上一批 ✅、未跑的 ⬜;③下一 stage 席位 task 里带的是总控承上说明(非机械摘要);④中转轮不应真的派活(即使派了前端也忽略)。
2. 运行时实地验收(旧):①§12.1 后并行席位能搜索、步骤条显示;②并行 special 日志时间重叠 + `Excluded` 不含 web_search 含 write_file;③用户干预后是「总控派活」而非重跑 DAG;④最左全局侧栏 + 参与角色栏并行收起且不回弹。
3. **✅ Phase 7 DAG 流程图可视化已完成(2026-06-07)**:见 §7.2。`DagFlowChart` 组件 + 编辑器只读预览 + Step2 执行态缩略(`dagNodeStates` 实时驱动)。
4. (可选)串行也确定性注入上一轮交付摘要。
**关键文件速查**:
- 前端:`hooks/useStep2Orchestration.ts`(`runDagOrchestration` / `dagPlanExecutedRef` / 并行 store+flush)、`components/Step2Panel.tsx`(两列+头像条)、`components/ParallelSeatStrip.tsx`、`lib/dag-types.ts`、`lib/chain-stages.ts`、`pages/BusinessChainEditorPage.tsx`、`pages/RoundtablePlanningPage.tsx`(侧栏收起 effect + handleConfirmAgents dag 分支)、`components/BusinessChainPicker.tsx`(链条→dag)。
- 后端:`app/gateway/roundtable_run_policy.py`(`seat_parallel`,**待放宽**)、`app/gateway/routers/multi_agent.py`(`_special_run` 读 `parallel_no_file`)、`persistence/roundtable_chains/`(`stages` 列)、`migrations/versions/20260607_03_chain_stages.py`。
- 测试:`frontend-web` 用 esbuild 转译 `.test.ts` 后 `node --test`(项目无 vitest);后端 `PYTHONPATH=. uv run pytest tests/test_roundtable_chain_stages.py tests/test_roundtable_run_policy.py`。
@@ -0,0 +1,7 @@
# 技能知识归纳与“知识梳理”助手知识库
管理员在「技能管理 → 技能知识归纳」中选择一个或多个技能、一个或多个目标,系统按“技能 × 目标”创建后台任务。界面提供总体进度、单项失败重试、实体/关系审核、绑定重归纳、自动同步和两种解绑动作。技能列表通过一次 bindings 查询展示归纳目标,避免逐技能请求。
扫描仅处理知识资料。代码、脚本、锁文件、密钥文件以及 Markdown 代码块不会进入上传、摘要、提示词或知识指纹;ZIP 只读取逐项验证通过的知识成员。自动监听只比较该知识指纹,所以纯代码改动不会产生 stale 状态。
知识库页面新增「普通知识库 / 助手知识库」并行入口。管理员可将系统默认入口设为 v1(普通 WeKnora)或 v2(全局“知识梳理”)。助手库无需 WeKnora 即可初始化,所有用户可只读查看和搜索 Wiki;管理员可上传本地文件、从普通库导入、重新导入或删除来源、查看版本并回滚。来源删除只移除对应贡献,共享实体与关系继续保留。
@@ -0,0 +1,375 @@
# 浅蓝色 UI 规范
> 本文档由设计稿截图(`1.1 全局样式-色彩`、`1.2 全局样式-色彩附录`)识别整理而成。
> 全部色值已与"色彩附录"编号表逐一核对,保证准确。
---
## 1.1 色彩体系总览
调色体系分为 **主色** 与 **辅色** 两大类。每个色相按用途分为若干阶梯:
| 阶梯 | 数量 | 用途 |
| --- | --- | --- |
| 浅色 | 3 | 用于大面积底色 |
| 中亮色 | 6 | 用于标签、图表、提示等 |
| 深色 | 4(+基准色,共 5 个深阶) | 用于小面积按钮、图标等 |
| 线性渐变色 | 1 | 用于特殊装饰性小面积底色 |
| 径向渐变色 | 1 | 用于特殊装饰性小面积底色 |
> 中性色(灰阶)略有不同:浅色用于背景 / 白色文字及分割线,中亮色用于备注文字及图标,深色用于正文,且**无渐变色**。
---
## 主色
### 主色 - 红(基准 `#CF1322`)
| 用途 | 色值 |
| --- | --- |
| 浅色 | `#FEFAFA` · `#FEF6F6` · `#FEEDED` |
| 中亮色 | `#FDDADC` · `#FCB5B9` · `#FA9196` · `#F86C73` · `#F74750` · `#F5222D` |
| 深色 | `#CF1322` · `#BC0D1E` · `#A8071A` · `#820014` · `#5C0011` |
| 线性渐变色 | `#F5222D` → `#A8071A` |
| 径向渐变色 | `#F86C73` → `#F5222D` |
### 主色 - 蓝(基准 `#0062D9`)
| 用途 | 色值 |
| --- | --- |
| 浅色 | `#F9FCFF` · `#F4F9FF` · `#EAF4FF` |
| 中亮色 | `#D4E9FF` · `#AAD2FF` · `#80BCFF` · `#55A6FE` · `#2B8FFE` · `#0079FE` |
| 深色 | `#0062D9` · `#0056C6` · `#004AB3` · `#00368C` · `#002466` |
| 线性渐变色 | `#0079FE` → `#004AB3` |
| 径向渐变色 | `#55A6FE` → `#0079FE` |
---
## 辅色
### 辅色 - 橙(基准 `#D97B00`)
| 用途 | 色值 |
| --- | --- |
| 浅色 | `#FFFCF9` · `#FFFAF4` · `#FFF6EA` |
| 中亮色 | `#FFEED4` · `#FFDDAA` · `#FFCC80` · `#FFBB55` · `#FFAA2B` · `#FF9900` |
| 深色 | `#D97B00` · `#C66D00` · `#B35F00` · `#8C4600` · `#663000` |
| 线性渐变色 | `#FF9900` → `#B35F00` |
| 径向渐变色 | `#FFBB55` → `#FF9900` |
### 辅色 - 绿(基准 `#1EB07F`)
| 用途 | 色值 |
| --- | --- |
| 浅色 | `#FAFEFD` · `#F6FDFB` · `#EEFCF7` |
| 中亮色 | `#DCF8EE` · `#BAF1DD` · `#97EBCD` · `#74E4BC` · `#52DDAB` · `#2FD69A` |
| 深色 | `#1EB07F` · `#189D72` · `#118A65` · `#07634B` · `#043D30` |
| 线性渐变色 | `#2FD69A` → `#118A65` |
| 径向渐变色 | `#74E4BC` → `#2FD69A` |
### 辅色 - 青蓝(基准 `#2F9ACC`)
| 用途 | 色值 |
| --- | --- |
| 浅色 | `#FBFDFE` · `#F7FCFE` · `#EFFAFE` |
| 中亮色 | `#E0F5FD` · `#C1EBFB` · `#A2E1F9` · `#83D7F7` · `#64CDF5` · `#45C3F3` |
| 深色 | `#2F9ACC` · `#2788B9` · `#1E76A6` · `#115580` · `#0B3959` |
| 线性渐变色 | `#45C3F3` → `#1E76A6` |
| 径向渐变色 | `#83D7F7` → `#45C3F3` |
### 中性色 - 灰(基准 `#222222`)
| 用途 | 色值 |
| --- | --- |
| 浅色(背景、白色文字及分割线) | `#FFFFFF` · `#FAFAFA` · `#F5F5F5` |
| 中亮色(备注文字及图标) | `#E8E8E8` · `#DBDBDB` · `#C1C1C1` · `#A6A6A6` · `#7F7F7F` · `#575757` |
| 深色(正文) | `#3D3D3D` · `#222222` |
---
## 1.2 色彩附录(编号对照表)
编号规则:`ZS` = 主色(主色调),`FS` = 辅色 / 中性色。`-01~-03` 为浅色,`-04~-09` 为中亮色,`-10~-13` 为深色,`-14` 为线性渐变色,`-15` 为径向渐变色;不带后缀的为该色相基准深色。
### 主色 ZS
| 编号 | 红 (ZS-001) | 编号 | 蓝 (ZS-002) |
| --- | --- | --- | --- |
| ZS-001 | `#CF1322` | ZS-002 | `#0062D9` |
| ZS-001-01 | `#FEFAFA` | ZS-002-01 | `#F9FCFF` |
| ZS-001-02 | `#FEF6F6` | ZS-002-02 | `#F4F9FF` |
| ZS-001-03 | `#FEEDED` | ZS-002-03 | `#EAF4FF` |
| ZS-001-04 | `#FDDADC` | ZS-002-04 | `#D4E9FF` |
| ZS-001-05 | `#FCB5B9` | ZS-002-05 | `#AAD2FF` |
| ZS-001-06 | `#FA9196` | ZS-002-06 | `#80BCFF` |
| ZS-001-07 | `#F86C73` | ZS-002-07 | `#55A6FE` |
| ZS-001-08 | `#F74750` | ZS-002-08 | `#2B8FFE` |
| ZS-001-09 | `#F5222D` | ZS-002-09 | `#0079FE` |
| ZS-001-10 | `#BC0D1E` | ZS-002-10 | `#0056C6` |
| ZS-001-11 | `#A8071A` | ZS-002-11 | `#004AB3` |
| ZS-001-12 | `#820014` | ZS-002-12 | `#00368C` |
| ZS-001-13 | `#5C0011` | ZS-002-13 | `#002466` |
| ZS-001-14 | `#F5222D` → `#A8071A` | ZS-002-14 | `#0079FE` → `#004AB3` |
| ZS-001-15 | `#F86C73` → `#F5222D` | ZS-002-15 | `#55A6FE` → `#0079FE` |
### 辅色 / 中性色 FS
| 编号 | 橙 (FS-001) | 绿 (FS-002) | 青蓝 (FS-003) | 中性灰 (FS-004) |
| --- | --- | --- | --- | --- |
| FS-00n | `#D97B00` | `#1EB07F` | `#2F9ACC` | `#222222` |
| -01 | `#FFFCF9` | `#FAFEFD` | `#FBFDFE` | `#FFFFFF` |
| -02 | `#FFFAF4` | `#F6FDFB` | `#F7FCFE` | `#FAFAFA` |
| -03 | `#FFF6EA` | `#EEFCF7` | `#EFFAFE` | `#F5F5F5` |
| -04 | `#FFEED4` | `#DCF8EE` | `#E0F5FD` | `#E8E8E8` |
| -05 | `#FFDDAA` | `#BAF1DD` | `#C1EBFB` | `#DBDBDB` |
| -06 | `#FFCC80` | `#97EBCD` | `#A2E1F9` | `#C1C1C1` |
| -07 | `#FFBB55` | `#74E4BC` | `#83D7F7` | `#A6A6A6` |
| -08 | `#FFAA2B` | `#52DDAB` | `#64CDF5` | `#7F7F7F` |
| -09 | `#FF9900` | `#2FD69A` | `#45C3F3` | `#575757` |
| -10 | `#C66D00` | `#189D72` | `#2788B9` | `#3D3D3D` |
| -11 | `#B35F00` | `#118A65` | `#1E76A6` | — |
| -12 | `#8C4600` | `#07634B` | `#115580` | — |
| -13 | `#663000` | `#043D30` | `#0B3959` | — |
| -14 | `#FF9900` → `#B35F00` | `#2FD69A` → `#118A65` | `#45C3F3` → `#1E76A6` | — |
| -15 | `#FFBB55` → `#FF9900` | `#74E4BC` → `#2FD69A` | `#83D7F7` → `#45C3F3` | — |
> 注:中性灰(FS-004)无渐变阶,最深至 `#222222`(基准)/`#3D3D3D`(FS-004-10),用于正文文字。
---
# 组件规范
> 以下为各组件的视觉规范与实现方式。颜色统一引用上文色值,建议在项目中先抽取为 CSS 变量 / Tailwind token,组件只引用语义化变量。
## 语义化 Token 建议
把色彩体系映射成语义变量,组件层只用语义名,便于换肤与维护:
```css
:root {
/* 品牌 / 主色(蓝) */
--color-primary: #0062D9; /* 主按钮、实底选中:ZS-002 深色基准 */
--color-primary-hover: #0079FE; /* 悬浮态:ZS-002-09 */
--color-primary-active: #0056C6; /* 按下态:ZS-002-10 */
--color-primary-bg: #EAF4FF; /* 浅底选中底色:ZS-002-03 */
--color-primary-border: #0079FE; /* 描边选中边框 */
/* 中性 */
--color-text: #3D3D3D; /* 正文:FS-004-10 */
--color-text-secondary: #A6A6A6; /* 次要 / 占位符:FS-004-07 */
--color-border: #DBDBDB; /* 默认边框:FS-004-05 */
--color-bg-disabled: #F5F5F5; /* 失效底色:FS-004-03 */
--color-fill-disabled: #A6A6A6; /* 主按钮失效底色:FS-004-07 */
--color-surface: #FFFFFF; /* 卡片 / 控件底 */
}
```
> 项目使用 Tailwind v4 + shadcn,可在 `@theme` 中声明这些变量后用 `bg-primary` / `text-primary` 等使用;下文示例同时给出等价的 Tailwind 写法。
---
## 2.1 基础 - 按钮(Button)
### 按钮尺寸
| 规格 | 高度 | 圆角 | 用途 |
| --- | --- | --- | --- |
| 小 | **28px** | 4px | 空间小的板块内部,如表格中、卡片标题等 |
| 正常 | **32px** | 4px | 常用按钮(默认尺寸) |
水平内边距约 12–16px,字号 14px,图标与文字间距 4–8px。
### 常用按钮类型
| 类型 | 视觉 | 用途 |
| --- | --- | --- |
| 主按钮 | 实底 `--color-primary`,白字 | 一个操作区域**只能有一个**主按钮 |
| 主按钮·失效 | 实底灰 `--color-fill-disabled`,白字 | 主按钮禁用态 |
| 次要按钮 | 白底 + `--color-primary` 边框 + 蓝字 | 重要度低于主按钮,高于默认按钮 |
| 默认按钮 | 白底 + `--color-border` 边框 + `--color-text` | 默认动作(取消等) |
| 默认·失效 | `--color-bg-disabled` 底 + 浅边框 + `--color-text-secondary` | 次要/默认按钮禁用态 |
| 文本按钮 | 无边框无底,蓝字(可加下划线) | 表格操作、文件下载等 |
| 图标按钮 | 仅图标,可选 描边/实底/浅底 × 方形/圆形 | 增强辨识度或节省空间 |
实现(Tailwind 示意):
```tsx
// 主按钮
<button className="h-8 px-4 rounded text-sm text-white bg-[#0062D9]
hover:bg-[#0079FE] active:bg-[#0056C6]
disabled:bg-[#A6A6A6] disabled:cursor-not-allowed">确定</button>
// 次要按钮(次按钮)
<button className="h-8 px-4 rounded text-sm text-[#0062D9] bg-white
border border-[#0062D9] hover:bg-[#EAF4FF]">点击</button>
// 默认按钮
<button className="h-8 px-4 rounded text-sm text-[#3D3D3D] bg-white
border border-[#DBDBDB] hover:border-[#0079FE] hover:text-[#0079FE]
disabled:bg-[#F5F5F5] disabled:text-[#A6A6A6] disabled:border-[#DBDBDB]">取消</button>
// 文本按钮
<button className="text-sm text-[#0062D9] hover:underline">文本按钮样式</button>
// 图标按钮(实底圆形)
<button className="size-7 grid place-items-center rounded-full text-white bg-[#0062D9]
hover:bg-[#0079FE]"><TrashIcon className="size-4" /></button>
```
图标按钮三种底 × 两种形状:
- **描边**:白底 + `--color-border` 边框,图标用 `--color-text`
- **实底**:`--color-primary` 底 + 白图标
- **浅底**:`--color-primary-bg`(`#EAF4FF`)底 + 蓝图标
- 形状:方形 `rounded`(4px)/ 圆形 `rounded-full`
### 按钮组合
| 组合 | 说明 |
| --- | --- |
| 文字 + 下拉按钮 | 按钮右侧带 `⌄` 箭头;有 默认 / 主按钮 两种变体 |
| 图标 + 文字按钮 | `+ 正常`,前置图标;有 描边 / 实底 两种变体 |
| 文本按钮 + 图标 | 蓝色文本链按钮:`更多 »` `展开 ⌄` `下一页 »|` `收起 ⌃` `|⌜ 上一页` |
| 主按钮 + 默认按钮 | `确定`(主) `取消`(默认) `返回`(默认) 并排,间距 8–12px |
### 其他状态
- **悬浮状态(hover)**:主按钮悬浮时底色变亮(`--color-primary-hover` `#0079FE`),用于 Banner / 登录 / 视觉冲击强的场景。
- **加载中(loading)**:按钮内前置旋转图标 `⟳ 加载中`,期间 `disabled` 并降低交互;用于异步操作等待反馈。
```tsx
<button className="h-8 px-4 rounded text-sm text-white bg-[#0062D9] inline-flex items-center gap-1.5" disabled>
<Spinner className="size-4 animate-spin" /> 加载中
</button>
```
---
## 5.9 输入 - 单选框(选择按钮 / Segmented Radio)
一组互斥选项,外观为按钮组。两种排布 × 三种选中样式。
### 排布方式
| 排布 | 说明 |
| --- | --- |
| 拼接式 | 选项首尾相连,仅最外侧两角圆角,相邻共用 1px 边框 |
| 分离式 | 选项之间留间距(约 8px),每个独立 4px 圆角,各自带完整边框 |
### 选中(已选)样式 × 三种
| 样式 | 已选 | 未选 |
| --- | --- | --- |
| 描边式 | 白底 + 蓝边框 `--color-primary` + 蓝字 | 白底 + `--color-border` 边框 + `--color-text` |
| 浅底式 | `--color-primary-bg`(`#EAF4FF`) 底 + 蓝字 + 蓝边框 | 同上 |
| 实底式 | `--color-primary` 实底 + 白字 | 同上 |
- **带统计**:选项文字后追加计数,如 `已选(230)` / `未选(230)`,计数用次要色 `--color-text-secondary`。
实现(拼接式 + 描边选中):
```tsx
<div className="inline-flex rounded overflow-hidden border border-[#DBDBDB] divide-x divide-[#DBDBDB]">
{options.map((o) => (
<button key={o.value}
className={cn(
"h-8 px-4 text-sm bg-white",
value === o.value
? "text-[#0062D9] ring-1 ring-inset ring-[#0079FE] z-10" // 描边选中
: "text-[#3D3D3D] hover:text-[#0079FE]"
)}>
{o.label}{o.count != null && <span className="text-[#A6A6A6]">({o.count})</span>}
</button>
))}
</div>
```
> 浅底式把选中态换成 `bg-[#EAF4FF] text-[#0062D9]`;实底式换成 `bg-[#0062D9] text-white`。
> 分离式去掉 `divide-x` / `overflow-hidden`,外层用 `gap-2`,每个按钮单独 `rounded border`。
### 按钮式页签(Tab as Button)
图标 + 文字的页签型按钮:
- **激活** 两种:描边(白底 + 蓝边框 + 蓝字)/ 实底(`--color-primary` 底 + 白字)
- **正常**:白底 + `--color-border` 边框 + `--color-text`,前置图标
- 结构与拼接式选择按钮一致,区别在每项前置一个功能图标。
---
## 5.11 输入 - 选择器(Select / Dropdown)
单项输入选择,下拉单选。
### 状态
| 状态 | 触发框 |
| --- | --- |
| 占位 | `请选择` 占位文字用 `--color-text-secondary`,灰边框 `--color-border`,右侧 `⌄` |
| 展开/聚焦 | 边框变 `--color-primary`,箭头翻转为 `⌃` |
| 已选 | 显示选中项文字(正文色),保持聚焦时蓝边框 |
### 下拉面板
- 白底、圆角 4px、投影(如 `shadow-lg`),最大高度后出现滚动条
- 选项默认 `--color-text`;**hover / 选中** 项:`--color-primary-bg`(`#EAF4FF`) 底 + `--color-primary` 文字
- **分组**:分组标题用小号灰字 `--color-text-secondary`,组之间用 1px 分割线 `--color-border`
实现示意:
```tsx
// 触发框
<button className="h-8 w-full px-3 inline-flex items-center justify-between rounded
border border-[#DBDBDB] bg-white text-sm
data-[state=open]:border-[#0062D9]">
<span className={selected ? "text-[#3D3D3D]" : "text-[#A6A6A6]"}>{selected ?? "请选择"}</span>
<ChevronDown className="size-4 text-[#A6A6A6] transition-transform data-[state=open]:rotate-180" />
</button>
// 下拉项
<li className={cn(
"h-9 px-3 flex items-center text-sm cursor-pointer rounded-sm",
active ? "bg-[#EAF4FF] text-[#0062D9]" : "text-[#3D3D3D] hover:bg-[#EAF4FF]"
)}>{label}</li>
// 分组标题
<div className="px-3 py-1.5 text-xs text-[#A6A6A6]">分组标题1</div>
```
> 推荐直接基于 Radix `Select`(项目已有 shadcn 封装)改这些类名落地。
---
## 5.14 输入 - 开关(Switch)
两种形态:
### 1. 滑块开关(多用于表单、表格中)
- **开**:胶囊底 `--color-primary`,白色圆形滑块在右,左侧白字「开」
- **关**:胶囊底灰 `--color-text-secondary`/`#C1C1C1`,滑块在左,右侧灰字「关」
- 切换时滑块带左右过渡动画
```tsx
<button role="switch" aria-checked={on}
className={cn(
"relative h-6 w-12 rounded-full transition-colors text-[11px] text-white",
on ? "bg-[#0062D9]" : "bg-[#C1C1C1]"
)}>
<span className={cn("absolute top-0.5 size-5 rounded-full bg-white transition-all",
on ? "left-[26px]" : "left-0.5")} />
<span className={cn("absolute top-1/2 -translate-y-1/2", on ? "left-2" : "right-2 text-[#7F7F7F]")}>
{on ? "开" : "关"}
</span>
</button>
```
### 2. 单选框作为开关(分段式)
- `开 | 关` 两段式:激活段 `--color-primary` 实底白字,未激活段白底灰字 + `--color-border` 边框
- 即 5.9 拼接式「实底选择按钮」的两选项特例
```tsx
<div className="inline-flex rounded overflow-hidden border border-[#DBDBDB]">
<button className={cn("h-7 px-4 text-sm", on ? "bg-[#0062D9] text-white" : "bg-white text-[#3D3D3D]")}>开</button>
<button className={cn("h-7 px-4 text-sm border-l border-[#DBDBDB]", !on ? "bg-[#0062D9] text-white" : "bg-white text-[#3D3D3D]")}>关</button>
</div>
```
+161
View File
@@ -0,0 +1,161 @@
# 需求文档 · 问答中心左侧栏改造(0616)
> 整理自 2026-06-16 需求草稿。本批需求统一围绕「问答中心」菜单下 5 个页面入口的**左侧侧边栏**进行改造,目标是让每个业务页面的左栏从「通用导航」变为「贴合该业务场景的快捷入口 + 历史记录」。
>
> 菜单结构参考:`frontend-web/src/core/page-layout/sidebar-menu.ts` 中 `qa-center`(问答中心)分组,含 5 个子项,正好对应下文 5 条需求。
## 0. 背景与共性约定
| 需求 | 菜单项 | id | 现有路由 | 对应页面/组件 |
|------|--------|----|----------|--------------|
| 1 | 通用问答 | `general-qa` | `/page/workspace/chats/new` | Workspace Chat + 左栏(`recent-chat-list.tsx` 等) |
| 2 | 多智能体会商 | `multi-agent-roundtable` | `/page/strategy/qa/multi-agent-roundtable` | `roundtable-planning/pages/RoundtablePlanningPage.tsx` |
| 3 | zzJC支持 | `qa-zzjc` | `/page/strategy/agents/6bf/chats/new` | `pages/AgentChatPage.tsx` |
| 4 | zzXD应用 | `qa-zzxd` | `/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new` | `pages/AgentChatPage.tsx` |
| 5 | 课题研究 | `qa-research` | `/page/canvas/ai-writing` | `open-canvas/pages/AIWritingPage.tsx` |
**共性约定(适用于全部 5 条,除非单条另有说明):**
- 「更多」一律为可点击入口;点击后跳转到对应的完整列表/管理页(具体目标见各条「待确认」)。
- 列表为空时的兜底:参考各条「现状」中的级联/空态规则;无内容时该区块整体隐藏或显示占位文案(待 UI 设计确认,默认隐藏,与现有 `recent-chat-list.tsx` 一致)。
- 左栏宽度、折叠交互沿用现有侧边栏体系(`SidebarGroup` 系列组件)。
- 所有「智能体」相关数据复用 `core/agents`(`Agent` 类型已含 `is_pinned` / `is_favorite` / `featured_order`,scope 支持 `mine` / `square` / `builtin`)。
---
## 1. 通用问答 — 左侧栏改造
**页面**:`/page/workspace/chats/new`
### 1.1 现状
当前左栏主要展示「最近会话」列表(`recent-chat-list.tsx`,已过滤 `scheduler` / `bootstrap` / `roundtable` 类型线程,置顶会话优先)。
### 1.2 目标布局(自上而下)
左栏从上到下依次为三块:**新建回答按钮 → 常用智能体 → 聊天记录**。
#### ① 顶部:新建回答
- 左栏最顶部放置「**新建回答**」按钮(主操作)。
- 点击 = 开启一次全新的通用问答会话(等价于跳转 `/page/workspace/chats/new`)。
#### ② 常用智能体(最多 3 个 + 更多)
- 展示「常用智能体」卡片/列表项,**最多 3 个**。
- 数据来源采用**级联兜底**逻辑(取到即用,不足再降级):
1. 若用户有**置顶智能体**(`is_pinned = true`)→ 展示置顶的;
2. 否则展示**用户自己的智能体**(`scope = "mine"`);
3. 仍没有 → 展示**默认广场智能体**(`scope = "square"` / `builtin`,即广场默认推荐)。
- 三者按上述优先级取,最终最多取 3 个。
- 下方显示「**更多**」入口。
- 点击某个常用智能体 → 进入该智能体的对话(待确认:是否为该智能体新建会话)。
#### ③ 聊天记录(最多 10 条 + 更多)
- 展示用户的问答聊天记录,**最多 10 条**(沿用现有 `useThreads` + 过滤规则)。
- 下方显示「**更多**」入口。
- 点击「更多」→ **跳转到问答记录页面**(完整聊天记录列表页)。
### 1.3
- 「常用智能体」点击后「打开该智能体主页」
- 「常用智能体」的「更多」跳转目标页 智能体管理 `/page/strategy/agents`。
- 「聊天记录」的「问答记录页面」具体路由http://localhost:5173/#/page/workspace/chats。
- 级联兜底是(置顶组够 3 个就完全不展示我的/广场)如果不够「补足到 3 个
---
## 2. 多智能体会商 — 左侧栏重做
**页面**:`/page/strategy/qa/multi-agent-roundtable`(`RoundtablePlanningPage`)
### 2.1 现状
圆桌会商页已有业务链选择、分析草稿等能力(`roundtable-planning/api/chains.ts`、`hooks/useRoundtableDrafts.ts`)。需求要求**左栏整体重做**。
### 2.2 目标布局(自上而下)
#### ① 顶部:业务链(公共,前 3 个)
- 顶部展示**业务链**,仅取**公共**链条(`listChains("public")`,即所有已发布 `is_public = true` 的链条)。
- 仅展示**前 3 个**。
- 点击某条业务链 → 以该业务链启动一次会商(沿用现有"会商自动串联圆桌"流程,见记忆 `business-chain-mapping`)。
#### ② 下方:分析记录
- 业务链下方展示「**分析记录**」列表。
- 数据来源:圆桌分析草稿/历史(`useRoundtableDrafts` / `listDrafts`)。
- 点击某条分析记录 → 打开对应的历史会商分析。
### 2.3 待确认
- 「业务链」是否需要「更多」入口跳转到业务链条配置页(`/page/strategy/business-chains`)?需求未提及,默认仅展示前 3 个、无更多。 需要更多入口
- 「分析记录」是否需要分页/「更多」?需求未提及,默认展示全部或合理上限 分析记录默认全部吧。
- 公共业务链「前 3 个」的排序口径(创建时间倒序?已有 `listChains` 默认 newest first) 按照现在默认的显示来吧,然后要支持用户单选,如果用户选择后,点击进入多智能体研讨按钮时,直接按照选择的业务链进去执行,如果用户取消勾选,或没有勾选就还是打开弹窗。
---
## 3. zzJC支持 — 左侧栏改造(按类型聚合智能体)
**页面**:`/page/strategy/agents/6bf/chats/new`(当前为单一智能体 `6bf` 的对话页)
### 3.1 现状
当前 zzJC支持直接进入某个固定智能体(id=`6bf`)的对话页(`AgentChatPage`)。
### 3.2 目标布局
- **左栏按「类型」分组**,展示该类型下的智能体列表("类型"待确认,推测为智能体 `tags` / 分类)。
- 智能体分组**下方**展示**聊天记录**。
- 左栏**宽度与通用问答(需求 1)保持一致**("左侧是问答宽")。
### 3.3 Admin 设置入口
- 在 **admin 账号权限下**,页面**右侧顶部显示「设置」图标按钮**(齿轮图标)。
- 点击设置按钮 → 打开设置面板,可对该模块进行配置。
- 非 admin 账号不显示设置按钮(沿用 `sidebar-menu.ts` 中 `adminOnly` 同款角色判断)。
### 3.4 待确认
- 「类型」的确切含义:是智能体的 `tags`,还是 zzJC 业务自定义的一组分类?
- 现 zzJC 入口是单个智能体 `6bf`;改为「按类型展示多个智能体」后,**进入该菜单时默认落地页是什么**(智能体列表?还是仍默认进 `6bf` 对话)?是否需要新建一个 zzJC 聚合页。
- Admin「设置」具体可配置项:配置「该模块纳入哪些类型/哪些智能体」?还是其他(如默认智能体、欢迎语)?需明确设置项清单。
- 「聊天记录」过滤范围:仅 zzJC 相关会话,还是全部会话?
---
## 4. zzXD应用 — 左侧栏改造
**页面**:`/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new`
### 4.1 需求
**与需求 3(zzJC支持)完全一致**:
- 左栏按类型展示该类型下的智能体 + 下方聊天记录;
- 左栏宽度同通用问答;
- admin 账号下右侧顶部显示设置图标按钮,可设置。
### 4.2 待确认
- 同需求 3 全部待确认项。
- zzXD 与 zzJC 是否共用同一套「按类型 + 设置」组件(仅数据源/默认智能体不同),还是各自独立配置?建议**抽象为同一可复用组件**,通过参数区分。
---
## 5. 课题研究 — 左侧栏改造(研究模板 + 笔记本)
**页面**:`/page/canvas/ai-writing`(`AIWritingPage`)
### 5.1 目标布局(自上而下)
#### ① 上半部分:研究模板
- 左栏上半部分展示「**研究模板**」。
- 研究模板 = **原来的「文章类型」**(`open-canvas/hooks/useArticleTypes.ts` 的 `articleTypes`,即 AI 写作表单里的「文章类型」选择项)。
- 点击某个研究模板 → 以该模板/文章类型发起课题研究(沿用现有 `articleType` 设置逻辑)。
#### ② 下半部分:笔记本
- 左栏下半部分展示「**笔记本**」。
- 笔记本内容 = **「简洁模式」左侧菜单下方展示的笔记本内容**(`useStudioNotebooks`,即 `workspace-nav-studio.tsx` 中渲染的 studio notebooks 列表)。
- 点击某个笔记本 → 打开该笔记本(沿用现有 `/page/workspace/studio/notebooks/{id}` 行为,或作为课题研究的素材来源 `materialSource = 'notebook'`,待确认)。
> 备注:草稿原文写作「简介模式」,结合代码应为「**简洁模式**」(`appearance-settings-page.tsx` 中的全局界面模式:全量 / 简洁;简洁模式下笔记本在左栏菜单展示)。
### 5.2 待确认
- 「研究模板」点击行为:仅预填表单 `articleType`,还是直接开始生成?
- 「笔记本」点击行为:跳转笔记本详情页,还是将笔记本设为当前课题研究的素材来源(`materialSource='notebook'`)?
- 上/下两部分是否各自可折叠、是否需要「更多」入口与数量上限?需求未提及。
---
## 6. 整体待确认与建议
1. **复用性**:需求 3 / 4(zzJC / zzXD)布局完全相同,建议实现为同一套「按类型分组 + 聊天记录 + admin 设置」可复用组件,通过配置区分模块。
2. **左栏宽度统一**:需求 1 / 3 / 4 都要求"问答宽",建议统一左栏宽度常量,避免各页面各写一套。
3. **「更多」目标页**:需求 1 涉及「智能体更多」「聊天记录更多→问答记录页」两个跳转目标需先确定路由是否已存在。
4. **Admin 设置项清单**:需求 3 / 4 的设置面板具体能配置什么,需要产品给出字段清单后才能落地。
5. **入口语义变更**:需求 3 / 4 把原「单一智能体对话」入口改为「按类型聚合」,菜单 `path` 与默认落地页可能需要调整,需同步确认。
+4
View File
@@ -49,6 +49,7 @@ frontend-web/
│ ├─ ScheduledTaskRunDetailPage.tsx
│ ├─ PublicSharePage.tsx 免登录分享只读
│ ├─ PublicScheduledTaskPage.tsx
│ ├─ EmbedChatPage.tsx iframe 嵌入问答(见 docs/embed-iframe-chat.md)
│ ├─ LoginPage.tsx 用户名直登(开发/内网用)
│ └─ ...
├─ core/ 「领域核心」: API 客户端、领域 hooks、类型
@@ -186,6 +187,7 @@ ReactDOM.createRoot(#root)
/login/:username LoginPage(按用户名直登)
/landing LandingPage
/share/:shareCode PublicSharePage(免登录只读)
/embed/chats/:thread_id EmbedChatPage(iframe 嵌入问答,见 docs/embed-iframe-chat.md)
/public/scheduled-tasks/:taskId PublicScheduledTaskPage(免登录)
/page/* PageRoutes(含侧边栏与 Header)
* NotFoundPage
@@ -664,6 +666,7 @@ const { models, tokenUsageEnabled, isLoading } = useModels();
- 创建分享:[components/workspace/thread-share-dialogs.tsx](../src/components/workspace/thread-share-dialogs.tsx) + `core/thread-shares/`
- 只读页 `PublicSharePage`(路由 `/share/:shareCode`)/ `PublicScheduledTaskPage`(`/public/scheduled-tasks/:taskId`),不需要登录
- iframe 嵌入问答 `EmbedChatPage`(`/embed/chats/:thread_id`):第三方用普通 iframe 嵌套,父页面用 `sessionId` 轮询公开 API 判断问答是否结束,详见 [embed-iframe-chat.md](./embed-iframe-chat.md)
- 后端会用 share token 做权限校验,前端通过 `apiFetch` 自动带
---
@@ -1138,6 +1141,7 @@ setSettings("context", { model_name: "gpt-5" }); // 仅当前线程
| `/landing` | App.tsx | LandingPage.tsx |
| `/share/:shareCode` | App.tsx | PublicSharePage.tsx |
| `/public/scheduled-tasks/:taskId` | App.tsx | PublicScheduledTaskPage.tsx |
| `/embed/chats/:thread_id` | App.tsx | EmbedChatPage.tsx([embed-iframe-chat.md](./embed-iframe-chat.md)) |
| `/page/workspace/*` | PageRoutes.tsx → WorkspaceRoutes.tsx | ChatsPage / ChatPage / AgentsPage / NewAgentPage / AgentChatPage / SkillsPage / SkillEvolutionPage / MemoryPage / ScheduledTasksPage / ScheduledTaskRunDetailPage / AdminUserHubPage / AdminScheduledTasksPage / TagsPage / RecommendedQuestionsPage / PromptPrefixSettingsPage / LlmMetricsPage |
| `/page/strategy/*` | PageRoutes.tsx → StrategyRoutes.tsx | GeneralQA + LightApp* + 复用 Workspace 页面 |
| `/page/canvas/*` | PageRoutes.tsx → open-canvas/routes/OpenCanvasRoutes.tsx | CanvasHomePage / CanvasThreadPage / AIWritingPage / ArticleTypesConfigPage |
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,822 @@
# 沙箱统一写作管线:可实现开发方案
> 状态:设计稿;本文件只定义实施方案,不包含本轮业务代码改动。
> 覆盖范围:普通沙箱 Markdown、深度研究 `report.md`、现有 Markdown 编辑器的选区/段落改写。
## 1. 目标与结论
目标不是再增加一个“AI 写作页面”,而是将现有的四类能力统一为一个安全、可流式、可追溯的**文档改写管线**:
1. Markdown 编辑器选区改写;
2. Markdown 编辑器段落改写;
3. 深度研究报告的局部候选改写;
4. 沙箱中 Markdown 文件的全文改写。
该管线负责固定原文版本、构造上下文、逐 token 输出、校验 Markdown、生成可核验 diff、按策略提交,以及在全文覆盖时保存可撤销版本。
结论:需求可行,现有工程已有足够的复用基础:
- `frontend-web/src/open-canvas/components/artifact/MarkdownAiEditor.tsx` 已具备局部改写弹窗与人工“替换/插入/重试/追问”交互。
- `frontend-web/src/open-canvas/hooks/useWritingRewrite.ts` 与 `frontend-web/src/open-canvas/api/rewrite.ts` 已具备 SSE 文本流消费。
- `offline-backend-20260512/backend/app/gateway/routers/writing.py` 已具备模型流式读取实现。
- `offline-backend-20260512/backend/app/gateway/routers/deep_research.py` 已具备报告哈希、候选内容和用户确认应用机制。
- `frontend-web/src/components/workspace/messages/message-list.tsx`、`MessageGroup.tsx` 已可承载普通对话的工具步骤条、流式消息和文件卡片。
- `frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx` 已经通过虚拟消息把研究进度接入普通 `MessageList`。
因此应该复用这些基础,建设一个执行内核;不能为全文改写另建独立聊天列表或独立写作流。
## 2. 产品边界与交互策略
### 2.1 支持范围
第一版仅对 `.md`、`.markdown` 文件显示“AI 全文改写”按钮。
不支持 PDF、DOCX、HTML、图片和二进制文件;不在改写过程中发起新的联网搜索;超大文件不静默截断后覆盖,而是提示用户选择章节改写。
### 2.2 范围、模式和提交策略
| 入口 | 文本范围 | 管线模式 | 提交策略 | 展示位置 |
| --- | --- | --- | --- | --- |
| 编辑器气泡菜单 | 当前选区 | `selection` | `preview_only` | 现有改写预览弹窗 |
| 编辑器段落菜单 | 当前段落 | `paragraph` | `preview_only` | 现有改写预览弹窗 |
| 深度研究“改写第一段/章节” | 被解析出的报告片段 | `report_section` | `manual_apply` | 普通消息列表候选稿 + 确认卡 |
| 文件右上角“AI 全文改写” | 当前完整 Markdown | `document` | `auto_commit_after_validation` | 普通消息列表步骤条 + 右侧沙箱 |
统一内核中的枚举:
~~~python
class RewriteMode(str, Enum):
SELECTION = "selection"
PARAGRAPH = "paragraph"
REPORT_SECTION = "report_section"
DOCUMENT = "document"
class CommitPolicy(str, Enum):
PREVIEW_ONLY = "preview_only"
MANUAL_APPLY = "manual_apply"
AUTO_COMMIT_AFTER_VALIDATION = "auto_commit_after_validation"
~~~
这三类策略必须严格区分:
- `preview_only`:只生成候选,前端编辑器确认后才改本地 TipTap 文档;不写真实沙箱文件。
- `manual_apply`:后端保留基准哈希和候选片段,用户点确认后才将该片段应用进研究报告。
- `auto_commit_after_validation`:全文生成完成并校验通过后,后端进行一次带哈希校验的原子覆盖;同时保存可撤销版本。
### 2.3 局部编辑器:保持当前体验
现有文件:
- `frontend-web/src/open-canvas/components/artifact/MarkdownAiEditor.tsx`
- `frontend-web/src/open-canvas/hooks/useWritingRewrite.ts`
- `frontend-web/src/open-canvas/api/rewrite.ts`
- `offline-backend-20260512/backend/app/gateway/routers/writing.py`
局部改写仍然是:
1. 选中文字或打开段落菜单;
2. 选择润色、扩写、缩写、改写等动作;
3. 在现有弹窗中流式出现候选;
4. 用户手动选择替换、插入、重试或继续追问。
不要让局部改写每次都往普通消息列表插完整步骤条,否则频繁编辑会淹没聊天历史。它只共享底层管线,不共享全文改写的消息展示方式。
需要补充本地版本保护:发起请求时保存当前完整编辑器文本哈希、选区 `from/to` 和选区文本;用户点击“替换原文”前再次检查该范围仍匹配。若用户已修改原文,提示“原文已变化,请重新生成候选稿”,禁止将过期候选替换到错误位置。
### 2.4 右上角“AI 全文改写”
宿主文件:
- `frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx`
新增独立图标按钮,位置在右上角文件操作区,靠近保存、复制、下载一类操作。它**不能**替代页面中间当前 Sparkles 编辑模式:
| 控件 | 职责 |
| --- | --- |
| 中部 Sparkles/编辑模式 | 打开 `MarkdownAiEditor`,处理选区和段落 |
| 新增右上角 AI 全文改写 | 对整个 Markdown 发起后台全文重写任务 |
建议显示条件:
~~~ts
const canUseFullDocumentRewrite =
isMarkdownFile &&
canEditArtifact &&
!isSkillFile &&
!isMock &&
!useWriteFileStreaming &&
!hasLocalEdits;
~~~
若 `hasLocalEdits` 为真,按钮置灰并提示“请先保存当前修改后再进行全文改写”。后端全文任务从真实文件读取快照,不能在用户本地还有未保存修改时读取旧版本并最终覆盖新修改。
用户体验:
1. 点击按钮,打开简洁配置弹窗。
2. 弹窗显示文件名、原文字数;用户填写改写要求、选择模型、选择风格。
3. 点击开始,左侧普通消息列表创建一组步骤条。
4. 右侧沙箱自动切到源码视图,先显示“正在准备临时草稿,原文件尚未修改”。
5. 新 Markdown 从空临时草稿开始逐 token 写入右侧代码区域。
6. 校验通过后,后端原子替换真实内容;前端再同步真实 Artifact 缓存。
7. 左侧保留最终对比总结、查看 diff 按钮和撤销按钮。
任务进行中,同一个文件的保存、直接编辑、局部编辑和再次全文改写必须禁用;其他文件不受影响。
### 2.5 深度研究报告的约束
普通 Markdown 和深度研究报告使用同一管线,但不能使用同一上下文策略:
| 能力 | 普通沙箱 Markdown | 深度研究 report.md |
| --- | --- | --- |
| 输入上下文 | 原文 + 用户指令 | 原文 + 当前会话已选资料 |
| 联网 | 不联网 | 不联网 |
| 事实约束 | 不新增无依据事实 | 只能引用已选资料,禁止编造 |
| 引用校验 | 可选 | 必须校验来源 ID 白名单 |
| 最终存储 | Artifact 文件 | 研究 session 的 `report_markdown` |
| 局部改写 | 编辑器确认 | 现有确认卡片 |
深度研究右侧的 `report.md` 是由 `ResearchSandboxPanel.tsx` 以虚拟 `write_file` 方式展示的;它不能当作普通磁盘 Artifact 直接写入。全文任务必须通过研究 session 更新 `report_markdown`,并使 `report_html` 缓存失效。
## 3. 总体架构
~~~mermaid
flowchart LR
A["MarkdownAiEditor<br/>选区 / 段落"] --> P
B["ArtifactFileDetail<br/>AI 全文改写"] --> P
C["DeepResearch<br/>局部 / 全文"] --> P
P["Document Rewrite Pipeline<br/>冻结版本 → 构造上下文 → 流式生成<br/>校验 → 差异总结 → 按策略提交"]
P --> I["InlineDocumentSource"]
P --> F["ArtifactDocumentSource"]
P --> R["DeepResearchReportSource"]
P --> E["可恢复 SSE 事件 / Job"]
E --> L["普通 MessageList"]
E --> S["右侧沙箱临时草稿"]
P --> V["内容哈希、版本快照、原子提交"]
~~~
设计原则:
1. 纯执行内核不依赖 FastAPI、路由、用户身份或操作系统路径。
2. 鉴权、线程路径解析、研究 session 获取只在 `app` 层的来源适配器中完成。
3. 完整文档任务使用持久化后台 Job;局部弹窗可维持单次 SSE,避免过度持久化。
4. 全文流式过程中只写临时草稿;真实内容只在最终 compare-and-swap 成功时改变。
不要直接把现有完整“AI 写作 Agent”塞进改写按钮。那个流程适合从主题写新文章,可能包含大纲、资料和章节生命周期;本需求是严格以当前文档为基准改写。第一版复用它的流式视觉表达和消息列表能力,但使用专门的文档改写执行内核。
## 4. 后端模块设计
### 4.1 目录与职责
建议新增:
~~~text
offline-backend-20260512/backend/
├─ packages/harness/deerflow/
│ ├─ document_rewrite/
│ │ ├─ __init__.py
│ │ ├─ models.py # 请求、事件、校验、差异领域模型
│ │ ├─ ports.py # Source / Model / Store 协议
│ │ ├─ pipeline.py # 统一编排,不导入 app.*
│ │ ├─ prompt_builder.py # 普通与深度研究提示词
│ │ ├─ validation.py # Markdown / 引用 / 截断检查
│ │ └─ diff_summary.py # 字数、标题、块、引用差异
│ └─ persistence/
│ └─ document_rewrites/
│ ├─ base.py
│ ├─ model.py
│ └─ sql.py
├─ app/gateway/
│ ├─ routers/document_rewrites.py
│ ├─ document_rewrite_sources.py
│ └─ document_rewrite_executor.py
└─ packages/harness/deerflow/persistence/migrations/versions/
└─ <next_revision>_document_rewrites.py
~~~
`packages/harness/deerflow/document_rewrite` 不能导入 `app.*`,以符合项目 harness→app import firewall。`app/gateway/document_rewrite_sources.py` 负责把已鉴权的 Artifact/研究会话包装成内核端口。
### 4.2 来源端口
~~~python
class DocumentSource(Protocol):
async def read_snapshot(self) -> DocumentSnapshot: ...
async def read_current_hash(self) -> str: ...
async def commit_if_unchanged(
self,
*,
expected_hash: str,
content: str,
metadata: CommitMetadata,
) -> CommitResult: ...
async def restore_if_unchanged(
self,
*,
expected_hash: str,
content: str,
) -> CommitResult: ...
~~~
三种实现:
| 适配器 | 使用场景 | 读取方式 | 提交方式 |
| --- | --- | --- | --- |
| `InlineDocumentSource` | 选区/段落候选 | 前端编辑器快照 | 不提交 |
| `ArtifactDocumentSource` | 普通 Markdown | 复用 Artifact 权限与虚拟路径解析 | 同目录临时文件 + `os.replace` |
| `DeepResearchReportSource` | 研究报告 | session `report_markdown` + 已选来源 | 数据库哈希条件更新 |
`ArtifactDocumentSource` 必须复用 `app/gateway/routers/artifacts.py` 中已有的线程访问校验、虚拟路径解析逻辑,绝不允许请求体传入任意本机绝对路径。
### 4.3 持久化 Job、事件和版本
全文改写使用持久化 Job,参考 `app/gateway/deep_research_job_executor.py` 的后台任务、租约、取消监听和事件重放模式。
#### document_rewrite_jobs
| 字段 | 作用 |
| --- | --- |
| `id` | `drw_xxx` Job ID |
| `user_id` | 发起人 |
| `source_kind` | `artifact` / `deep_research_report` |
| `thread_id`、`artifact_path` | 普通文件来源 |
| `research_session_id` | 研究报告来源 |
| `presentation_thread_id` | 普通消息列表归属 |
| `mode`、`commit_policy` | 范围和提交语义 |
| `instruction`、`style`、`model_name` | 用户配置 |
| `base_sha256` | 原文冻结哈希 |
| `draft_content`、`draft_sha256` | 临时流式草稿 |
| `status`、`stage` | 生命周期 |
| `validation_json`、`diff_summary_json` | 可展示结果 |
| `committed_sha256` | 成功提交后的哈希 |
| `error_message`、时间字段 | 错误、审计、取消 |
#### document_rewrite_events
至少包含 `job_id`、`seq`、`event_type`、`stage`、`payload_json`、`created_at`。`seq` 用于断线重连时通过 `after_seq` 重放。
不必将每个 token 单独入库:每 250ms 或每积累 4KB 持久化一次 `draft_content`;状态转换、警告、完成、冲突、取消必须立即写事件。重连时先发送最新草稿快照,再发送快照序号后的增量事件。
#### document_rewrite_versions
保存真实全文覆盖前的原文版本,字段包含 `job_id`、`source_identity`、`version_no`、`content`、`sha256`、`reason`、`created_at`。初版每个文档保留最近 20 个版本即可。
索引要求:
- `(user_id, presentation_thread_id, created_at DESC)`:恢复历史消息。
- 真实来源标识 + 活跃状态:防止同一文档并行两个全文改写。
### 4.4 Job 状态机
~~~mermaid
stateDiagram-v2
[*] --> queued
queued --> snapshotting
snapshotting --> generating
generating --> validating
validating --> comparing
comparing --> committing
committing --> completed
completed --> reverted
queued --> cancelled
generating --> cancelled
snapshotting --> failed
generating --> failed
validating --> failed
committing --> conflict
committing --> failed
~~~
状态约束:
- 同一真实文档只允许一个活跃 Job。
- 生成期间取消、异常、断线,真实文档绝不能变化。
- 提交前重新读取当前哈希;与 `base_sha256` 不一致时进入 `conflict`,草稿保留但不覆盖。
### 4.5 API 与 SSE 契约
创建全文任务:
~~~http
POST /api/document-rewrites
Content-Type: application/json
~~~
普通 Artifact 请求:
~~~json
{
"source": {
"kind": "artifact",
"thread_id": "thread_xxx",
"path": "/mnt/user-data/outputs/report.md"
},
"presentation_thread_id": "thread_xxx",
"mode": "document",
"instruction": "统一为正式行业分析语气,减少重复,不新增外部事实",
"style": "formal_analysis",
"model_name": "system_default"
}
~~~
深度研究请求:
~~~json
{
"source": {
"kind": "deep_research_report",
"session_id": "drs_xxx"
},
"presentation_thread_id": "collector_thread_xxx",
"mode": "document",
"instruction": "重新组织全文逻辑,保留全部可用引用",
"style": "formal_analysis",
"model_name": "system_default"
}
~~~
其他接口:
~~~text
GET /api/document-rewrites/{job_id}
GET /api/document-rewrites/{job_id}/stream?after_seq=42
POST /api/document-rewrites/{job_id}/cancel
POST /api/document-rewrites/{job_id}/undo
GET /api/document-rewrites?presentation_thread_id={thread_id}
~~~
SSE 事件:
| 事件 | 关键数据 | 前端动作 |
| --- | --- | --- |
| `job_started` | 文件名、模式 | 建立步骤条 |
| `snapshot_fixed` | 原文字数、哈希 | 完成“固定原文版本” |
| `requirements_resolved` | 模型、风格、要求 | 完成“理解改写要求” |
| `rewrite_delta` | `delta`、累计字符数 | 追加右侧临时草稿 |
| `validation_started` | 校验项 | 进入校验步骤 |
| `validation_completed` | passed、warnings | 显示校验结果 |
| `comparison_ready` | `DiffSummary` | 渲染总结数据 |
| `commit_started` | 无 | 显示安全替换中 |
| `committed` | 最终哈希、版本 ID | 刷新真实文件缓存 |
| `completed` | 完整结果 | 固化历史步骤条 |
| `conflict` / `error` / `cancelled` | 原因 | 恢复原文显示,不保存草稿 |
局部改写兼容方案:
- `POST /api/writing/rewrite` 的请求和 SSE 格式先不变。
- `writing.py` 内部逐步把 prompt、模型流、基础 Markdown 校验下沉给统一管线的 `preview_only` 分支。
- 深度研究 `/sessions/{id}/rewrite/stream` 和确认候选接口也先保持 URL、前端行为不变,再内部复用 `manual_apply` 分支。
这样可以先交付全文改写,不会一次性破坏已有编辑器和深度研究流程。
### 4.6 模型、提示词与校验
管线只依赖一个流式模型端口:
~~~python
class StreamingRewriteModel(Protocol):
async def stream_text(
self,
*,
messages: list[BaseMessage],
model_name: str | None,
) -> AsyncIterator[TextChunk]: ...
~~~
第一版可以有两个 adapter:
- 普通文档复用 `writing.py` 的 chat model `astream`;
- 深度研究复用 `deep_research.py` 使用的 `DeerFlowCompletionBackend.stream_complete`。
先统一编排,不强制立即统一所有模型配置实现。
普通 Markdown 系统约束:
~~~text
仅根据原文与用户要求改写,不联网,不补充未给出的事实。
保留有效链接、代码块、表格、标题语义和用户明确要求保留的内容。
只输出完整 Markdown 正文;不得输出解释、前缀或“改写如下”。
~~~
深度研究额外约束:
~~~text
不得联网。事实、数字、结论和引用只能来自当前 session 已选资料。
只允许使用给出的 source_id;资料不足时保持谨慎表述,禁止自行补全。
~~~
校验规则:
| 类别 | 硬失败 | 告警但允许完成 |
| --- | --- | --- |
| 正文 | 空文档、仅空白、无可见内容 | 改写后与原文完全一致 |
| Markdown | 未闭合围栏、明确截断 | 标题层级跳级、章节数明显减少 |
| 深度研究来源 | 未选来源 ID、非法引用 | 引用数量下降、可能需核验的数字断言 |
## 5. 安全提交、对比与撤销
### 5.1 临时草稿不等于真实文件
用户视觉上看到“先清空再逐 token 写入”,但真实文件的处理必须是:
1. 读取原文并保存内容哈希、版本快照。
2. 右侧只展示内存/Job 中的 `draft_content`。
3. 生成完成后做校验和 diff。
4. 提交前再次读取真实来源,并比较 `SHA-256`。
5. 哈希一致才原子替换;否则进入冲突,不覆盖。
普通 Artifact 使用同目录临时文件、刷盘和 `os.replace`;深度研究通过数据库事务内的“旧哈希条件更新”提交。任何失败、取消或断流都保留真实原文。
### 5.2 撤销
撤销流程:
1. 读取该 Job 的 `before_full_rewrite` 快照。
2. 确认当前真实内容哈希仍等于该 Job 的 `committed_sha256`。
3. 若用户在改写后又手工编辑,返回冲突,不盲目覆盖。
4. 先保存当前内容为 `before_undo` 版本。
5. 原子恢复旧版本,Job 状态改为 `reverted`,消息列表卡片同步显示“已撤销”。
### 5.3 结构化 DiffSummary
后端产出结构化差异,前端仅渲染,避免生成空泛结论:
~~~ts
type DiffSummary = {
sourceDisplayName: string;
instruction: string;
modelName: string;
originalCharCount: number;
rewrittenCharCount: number;
charChangeRatio: number;
changedBlockCount: number;
headings: {
added: string[];
removed: string[];
reordered: string[];
levelWarnings: string[];
};
citations?: {
beforeCount: number;
afterCount: number;
retainedCount: number;
removedIds: string[];
invalidIds: string[];
};
requestedOptimizations: string[];
validationWarnings: string[];
};
~~~
推荐总结展示:
~~~text
已完成《新能源汽车市场分析报告》的整体改写。
- 改写范围:全文;要求为“统一正式分析语气、减少重复”;模型:默认模型。
- 数量变化:原文 8,260 字,改写后 8,110 字,减少 1.8%;调整 14 个内容块。
- 结构变化:标题层级保持不变;未新增或删除一级标题。
- 已执行的优化:按要求合并重复表述、统一语气、调整段落衔接。
- 保留情况:保留 35 条已选来源引用,未新增外部来源。
- 风险提示:无。
[查看前后对比] [撤销到改写前版本]
~~~
“已执行的优化”仅来自用户要求和可计算的结构差异;不能无依据宣称“文章更专业、更严谨”。
## 6. 前端实现设计
### 6.1 新增模块
~~~text
frontend-web/src/core/document-rewrite/
├─ types.ts
├─ api.ts
├─ sse.ts
├─ use-document-rewrite-job.ts
├─ document-rewrite-provider.tsx
└─ document-rewrite-presentation.ts
frontend-web/src/components/workspace/document-rewrite/
├─ DocumentRewriteDialog.tsx
├─ DocumentRewriteProgressStep.tsx
├─ DocumentRewriteSummaryCard.tsx
└─ DocumentDiffDialog.tsx
~~~
职责:
| 模块 | 职责 |
| --- | --- |
| `api.ts` | 创建、查询、取消、撤销 Job |
| `sse.ts` | SSE 解析、`after_seq` 重连 |
| `use-document-rewrite-job.ts` | Job 状态、草稿、rAF 合并 token |
| Provider | 同时向文件面板与消息列表提供同一任务状态 |
| `DocumentRewriteDialog` | 改写要求、模型、风格配置 |
| ProgressStep | 普通消息列表内的五步步骤条 |
| SummaryCard | 结构化总结、diff、撤销动作 |
| DiffDialog | 原文与改写后双栏/行级对比 |
模型选择使用项目已有的 `tdesign-react` 选择组件,放在全文改写配置弹窗中;局部编辑器保留当前模型传递方式,后续再统一选择器。
### 6.2 流式平滑:每帧合并 delta
不能因为 SSE 是 token 流就每 token 调一次 `setState`。应当网络层完整读取、渲染层按动画帧合并:
~~~ts
const pendingDeltaRef = useRef("");
const frameRef = useRef<number | null>(null);
function onRewriteDelta(delta: string) {
pendingDeltaRef.current += delta;
if (frameRef.current !== null) return;
frameRef.current = requestAnimationFrame(() => {
const pending = pendingDeltaRef.current;
pendingDeltaRef.current = "";
frameRef.current = null;
setDraftContent((previous) => previous + pending);
});
}
~~~
完成、失败、取消前必须 flush 最后一段缓存。右侧代码编辑器只消费 `draftContent`;真实 Artifact 查询缓存直到 `committed` 事件才更新。
### 6.3 ArtifactFileDetail 改造
在 `artifact-file-detail.tsx` 增加:
~~~ts
type DocumentRewriteTarget =
| {
kind: "artifact";
threadId: string;
path: string;
presentationThreadId: string;
}
| {
kind: "deep_research_report";
sessionId: string;
presentationThreadId: string;
};
~~~
改造步骤:
1. 加入右上角全文改写图标按钮和 `DocumentRewriteDialog`。
2. 有活跃任务时强制 `viewMode = "code"`。
3. 内容优先级为:`activeRewrite.draftContent ?? draftContent ?? artifactContent`。
4. 活跃任务中代码只读,禁用保存、局部编辑和重复全文改写。
5. 收到 `committed` 后用现有 `useUpdateArtifactContent` 或等价 Query cache 更新,使真实文件立即同步。
6. 收到 `conflict/error/cancelled` 时废弃临时草稿显示,回到真实文件内容。
### 6.4 ResearchSandboxPanel 接入
相关文件:
- `frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx`
- `frontend-web/src/pages/deep-research/useReportSandbox.ts`
- `frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx`
改造:
1. `ResearchSandboxPanel` 为虚拟 `report.md` 传入 `kind: "deep_research_report"` target。
2. `useReportSandbox` 接受 `externalDraftOverride`:
~~~ts
const visibleReport = activeDocumentRewrite?.isActive
? activeDocumentRewrite.draftContent
: reportMarkdown;
~~~
3. 流式期间只显示临时草稿,不提前写 session 报告。
4. `committed` 后刷新/替换 session 的 `report_markdown`;失败后恢复旧报告。
5. 当前“改写第一段 → 候选消息 → 确认应用”逻辑继续保留,不因全文改写而改变。
### 6.5 普通消息列表接入
复用:
- `frontend-web/src/components/workspace/messages/message-list.tsx`
- `frontend-web/src/components/workspace/messages/MessageGroup.tsx`
- `DeepResearchWorkbench.tsx` 中的虚拟消息注入模式。
`DocumentRewritePresentationProvider` 按 `presentation_thread_id` 读取 Job 历史并投影为:
1. 用户消息:“AI 全文改写《xxx.md》”;
2. AI 工具调用:`document_rewrite_progress`;
3. AI 富消息:`document_rewrite_summary`。
`MessageGroup.tsx` 增加 `document_rewrite_progress` 分支,渲染 `DocumentRewriteProgressStep`。最终摘要渲染 `DocumentRewriteSummaryCard`,其内含“查看前后对比”和“撤销”。
全文步骤固定为:
| 步骤 | 展示内容 |
| --- | --- |
| 固定原文版本 | 文件名、原文字数、快照已保存 |
| 理解改写要求 | 改写目标、模型、风格 |
| 流式重写 Markdown | 正在写入右侧 xxx.md、累计字数 |
| 校验文档 | 非空、标题、围栏、截断;报告还含来源检查 |
| 对比原文并生成说明 | 差异、风险、撤销入口 |
步骤条必须在完成后保留,不能因为流结束被隐藏。
## 7. 核心执行伪代码
~~~python
async def run_document_rewrite(job_id: str) -> None:
job = await jobs.acquire_lease(job_id)
source = source_factory.create(job) # 已完成鉴权的来源适配器
await emit("snapshotting")
snapshot = await source.read_snapshot()
base_hash = sha256(snapshot.content)
await jobs.freeze_snapshot(job_id, snapshot, base_hash)
await emit("snapshot_fixed", char_count=count_chars(snapshot.content))
messages = prompt_builder.build(
mode=job.mode,
source_kind=job.source_kind,
original=snapshot.content,
instruction=job.instruction,
style=job.style,
research_sources=snapshot.allowed_sources,
)
await emit("requirements_resolved", ...)
draft = ""
async for chunk in model.stream_text(messages=messages, model_name=job.model_name):
if await jobs.cancel_requested(job_id):
await jobs.mark_cancelled(job_id)
await emit("cancelled")
return
draft += chunk.text
await jobs.append_draft_periodically(job_id, draft)
await emit("rewrite_delta", delta=chunk.text)
await emit("validation_started")
validation = validate_markdown(
draft,
source_kind=job.source_kind,
allowed_citations=snapshot.allowed_citation_ids,
)
if validation.has_fatal_error:
await jobs.mark_failed(job_id, validation.message)
await emit("error", ...)
return
await emit("validation_completed", ...)
diff = build_diff_summary(snapshot.content, draft, validation)
await jobs.save_diff(job_id, diff)
await emit("comparison_ready", diff)
if job.commit_policy != AUTO_COMMIT_AFTER_VALIDATION:
await jobs.mark_candidate_ready(job_id)
return
await emit("commit_started")
await versions.save_before_rewrite(job, snapshot)
result = await source.commit_if_unchanged(
expected_hash=base_hash,
content=draft,
metadata=CommitMetadata(job_id=job_id),
)
if not result.ok:
await jobs.mark_conflict(job_id)
await emit("conflict", ...)
return
await jobs.mark_completed(job_id, result.sha256)
await emit("committed", ...)
await emit("completed", diff)
~~~
## 8. 实施顺序
### 阶段 A:后端内核与安全基础
1. 新增领域模型、来源端口、校验、确定性 diff。
2. 新增 Job、事件、版本数据表与 migration。
3. 参考深度研究 executor 完成后台任务、取消、SSE 重放。
4. 实现普通 Artifact 读取、哈希比较、原子提交和撤销 API。
5. 编写接口测试。
验收:可以通过 API 对普通 Markdown 创建全文任务;取消、失败、冲突均不改原文件;成功后可撤销。
### 阶段 B:普通沙箱全文 UI
1. 新增 `core/document-rewrite` 前端状态、SSE、rAF hook。
2. 改 `ArtifactFileDetail`:右上角按钮、配置弹窗、临时草稿、禁用态。
3. 接入普通 `MessageList` 的步骤条、总结卡、diff 和撤销。
4. 支持刷新后恢复 Job 状态与已完成历史。
验收:右侧可平滑逐 token 写入;完成后替换真实 Markdown;左侧步骤和总结保留;撤销成功恢复。
### 阶段 C:深度研究接入
1. 实现 `DeepResearchReportSource`。
2. 由 `ResearchSandboxPanel` 传入报告 target。
3. `useReportSandbox` 支持外部临时草稿。
4. 对深度研究添加引用白名单校验。
5. 保持现有报告局部候选确认卡不变,再逐步下沉公共 prompt/校验逻辑。
验收:报告全文改写不触发联网搜索,不允许新来源;局部报告改写依然先确认再应用。
### 阶段 D:局部改写内核迁移
1. 保持 `/api/writing/rewrite` 契约不变。
2. 将 `writing.py` 转为 `InlineDocumentSource + preview_only` 的兼容 facade。
3. 给 `useWritingRewrite` 增加原文快照失效检查。
4. 回归替换、插入、重试、继续追问。
## 9. 测试与验收
建议新增后端测试:
~~~text
offline-backend-20260512/backend/tests/
├─ test_document_rewrite_pipeline.py
├─ test_document_rewrite_api.py
├─ test_document_rewrite_versions.py
└─ test_document_rewrite_deep_research.py
~~~
必须覆盖:
- Job 状态、SSE 顺序和断线恢复;
- 空输出、围栏不闭合、截断、非法深度研究引用;
- 同一文件并发任务;
- 提交前手工修改导致的冲突;
- 取消/异常不改真实文件;
- 正常撤销、撤销前手工编辑导致的冲突;
- 旧 `/api/writing/rewrite`、深度研究 `/rewrite/stream` 兼容回归。
前端必须覆盖:
- 多个 delta 在一帧合并,最终内容不丢失;
- `committed` 前 Artifact 缓存不被临时草稿污染;
- 失败/取消/冲突后右侧恢复原文;
- 活跃任务禁用同文件编辑、保存、重复改写;
- 刷新后历史步骤条与总结仍存在;
- 深度研究虚拟 `report.md` 可用全文改写;
- 局部编辑器原文变化后不能应用过期候选。
执行基线:
~~~powershell
cd offline-backend-20260512/backend
$env:PYTHONPATH = "."
uv run --no-sync pytest tests/test_document_rewrite_pipeline.py -v
uv run --no-sync pytest tests/test_document_rewrite_api.py -v
uv run --no-sync ruff check app packages/harness
cd ../../frontend-web
pnpm typecheck
pnpm build
~~~
## 10. 风险控制
| 风险 | 控制措施 |
| --- | --- |
| 用户误以为真实文件已经清空 | 明确显示“临时草稿,原文件尚未修改” |
| 流式显示仍然卡顿 | 网络完整读取 + `requestAnimationFrame` 合并渲染 |
| 改写中用户手动编辑 | `SHA-256` compare-and-swap,冲突即拒绝覆盖 |
| 输出 Markdown 不完整 | 非空、围栏、截断和标题检查 |
| 深度研究幻觉 | 来源白名单、禁止联网、引用 ID 校验 |
| 文档过大 | 第一版拒绝并提示按章节改写,不隐式截断 |
| 局部编辑污染聊天历史 | 局部只在编辑器弹窗展示,不创建完整步骤条 |
| 历史记录只存在浏览器内存 | 全文 Job/Event/Diff 持久化,按展示线程恢复 |
## 11. 复用点清单
| 现有实现 | 在新方案中的用途 |
| --- | --- |
| `MarkdownAiEditor.tsx` | 保留局部编辑菜单、预览弹窗、确认动作 |
| `useWritingRewrite.ts` | 保留局部流控制,增加快照失效保护 |
| `open-canvas/api/rewrite.ts` | 保持兼容,逐步接入统一内核 |
| `routers/writing.py` | 复用当前模型流式读取,抽取 prompt/流逻辑 |
| `routers/deep_research.py` | 复用报告来源、哈希候选、来源约束 |
| `deep_research_job_executor.py` | 复用后台 Job、取消、事件重放模式 |
| `artifact-file-detail.tsx` | 复用沙箱文件操作、代码/预览、缓存同步 |
| `useUpdateArtifactContent` | 完成后更新普通 Artifact 缓存 |
| `ResearchSandboxPanel.tsx` | 复用研究报告的右侧沙箱宿主 |
| `useReportSandbox.ts` | 加入临时草稿覆盖显示 |
| `MessageList.tsx / MessageGroup.tsx` | 复用普通消息步骤条、总结消息、文件卡片能力 |
| `DeepResearchWorkbench.tsx` | 复用向普通消息列表投影过程事件的模式 |
## 12. 完成定义
完成后应满足:
1. 局部编辑依旧是编辑器弹窗中的人工确认操作。
2. 全文改写在普通消息列表展示完整且不隐藏的步骤条。
3. 右侧沙箱可平滑地逐 token 展示临时 Markdown 草稿。
4. 任何中断、取消、校验失败和冲突都不损坏真实文件。
5. 完成后用户可以看到量化差异、结构变化、来源保留和风险提示。
6. 普通 Markdown 与深度研究报告都可全文改写,但研究报告绝不绕过已选来源约束。
7. 每一次真实全文覆盖都有版本快照,并且能够安全撤销。
+148
View File
@@ -0,0 +1,148 @@
# 定时任务 — 模板填充「源数据抽取」手工测试清单
> 覆盖功能:定时任务「模板数据转换器」内置 agent —— 把用户传入的源数据转换成 HTML
> 模板的 `DATA` 结构并填充生成可预览页面。**重点测「源数据抽取」**:源可能是
> ① 纯 JSON、② 含 JSON 的文本、③ 纯文本,三种形态都要正确处理。
>
> 测试环境:后端 `http://localhost:8001`(`make dev`),前端 `http://localhost:5174`(`pnpm dev`)。
> 相关代码:`deerflow/runtime/scheduler/template_fill.py`(`extract_embedded_json`)、
> `service.py`(`_run_template_fill_attempt` / `_build_conversion_prompt`)、
> `pages/ScheduledTasksPage.tsx`。
---
## 前置 — 进入表单
| # | 步骤 | 预期结果 |
|---|------|----------|
| 0.1 | 任务管理 → 新建定时任务 | 弹出「新建定时任务」对话框 |
| 0.2 | 「执行智能体」下拉选择 **模板数据转换器** | 表单出现「HTML 模板」下拉;「必须生成 HTML/Markdown」两个开关隐藏 |
| 0.3 | 「HTML 模板」下拉选择 **no.1** | 下方出现「上传 .json」按钮 + 源数据文本框;说明含「未提供源 JSON 时,将直接展示该模板的默认数据」|
---
## T1 — 源形态①:纯 JSON
| # | 步骤 / 输入 | 预期结果 |
|---|------|----------|
| 1.1 | 在源数据框粘贴纯 JSON(示例见下「样例 A」) | 文本框正常接收,无报错 |
| 1.2 | 保存任务 → 「立即执行」 | 执行记录新增一条,状态最终 succeeded |
| 1.3 | 打开执行记录 → HTML 预览 | 渲染出 no.1 大屏,**总览指标/各列表数据来自粘贴的 JSON**(而非模板默认值)|
| 1.4 | 执行记录正文 | 文案为「已根据源数据填充模板「no.1」生成页面」|
## T2 — 源形态②:含 JSON 的文本(抽取重点)
| # | 步骤 / 输入 | 预期结果 |
|---|------|----------|
| 2.1 | 粘贴「样例 B」(前后是中文说明,中间夹一段 JSON) | 文本框正常接收 |
| 2.2 | 保存 → 立即执行 → 查看 HTML | 页面数据以**夹带的 JSON 为准**填充;正文里的补充说明(如「重点突出 X」)被一并参考 |
| 2.3 | 验证抽取正确性:JSON 字符串值里含 `}`(如 `"备注": "进度 80%}尚未完成"`) | 不会被误判为 JSON 块提前结束,整段 JSON 被完整抽取 |
## T3 — 源形态③:纯文本(无 JSON)
| # | 步骤 / 输入 | 预期结果 |
|---|------|----------|
| 3.1 | 粘贴「样例 C」(一段纯中文描述,无任何 JSON) | 文本框正常接收 |
| 3.2 | 保存 → 立即执行 → 查看 HTML | 智能体**理解文本语义**后据实填充模板,产出完整可预览页面(数据可能较稀疏但结构完整)|
| 3.3 | 不应报「未返回 JSON」类硬失败(除非模型彻底失败重试耗尽)| 状态 succeeded |
## T4 — 空源 → 模板默认数据
| # | 步骤 | 预期结果 |
|---|------|----------|
| 4.1 | 源数据框留空,保存 → 立即执行 | **不调用模型**,直接用 no.1 模板自带的默认数据生成 |
| 4.2 | 执行记录正文 | 「未提供源数据,已用模板「no.1」的默认数据生成页面」|
| 4.3 | HTML 预览 | 展示模板内置的默认大屏 |
## T5 — 上传 .json 文件
| # | 步骤 | 预期结果 |
|---|------|----------|
| 5.1 | 点击「上传 .json 文件」,选一个合法 `.json` | 文件内容被读入源数据文本框 |
| 5.2 | 上传一个非法 JSON 内容的文件 | toast 提示「文件不是合法 JSON,已填入文本框请检查」,内容仍填入(可手改)|
| 5.3 | 连续上传同一个文件两次 | 第二次仍能触发读取(input 已被重置 value)|
## T6 — no.2(只展示模板)
| # | 步骤 | 预期结果 |
|---|------|----------|
| 6.1 | 「HTML 模板」下拉选 **no.2** | **隐藏**上传按钮与源数据文本框;显示「该模板暂为直接展示已有数据,无需填写」|
| 6.2 | 即使之前填过源数据,保存 → 立即执行 | 忽略源数据,**原样输出** no.2 现有页面 |
| 6.3 | 执行记录正文 | 「已展示模板「no.2」的现有数据」|
## T7 — 编辑回显
| # | 步骤 | 预期结果 |
|---|------|----------|
| 7.1 | 对已保存的模板任务点「编辑」 | 「执行智能体」「HTML 模板」「源数据」均正确回显 |
| 7.2 | 把模板从 no.1 改成 no.2 再保存 | 切到 no.2 时源数据区即时隐藏;保存后执行走只展示分支 |
---
## 样例数据(可直接复制粘贴到源数据框)
### 样例 A — 纯 JSON
```json
{
"overview": {
"核心指标": [
{ "名称": "综合区域", "数值": 7, "单位": "区", "说明": "测试数据" },
{ "名称": "组织与单位", "数值": 188, "单位": "个", "说明": "测试数据" }
]
},
"regions": [
{ "name": "测试区域甲", "scope": "本岛西岸" }
],
"equipment": [
{ "name_zh": "测试装备X", "category": "无人系统", "status": "列装" }
]
}
```
> 预期:总览第一项「综合区域」显示 **7**,「组织与单位」显示 **188**,区域/装备出现「测试区域甲」「测试装备X」。
### 样例 B — 含 JSON 的文本
```
这是本月台海态势数据,请据此填充大屏,重点突出离岛防卫:
{"overview": {"核心指标": [{"名称": "综合区域", "数值": 9, "单位": "区", "说明": "含离岛"}]}, "regions": [{"name": "离岛防区", "scope": "外岛", "备注": "进度 80%}尚未完成"}]}
以上数据来自 6 月例会,务必准确,不要臆造未提供的字段。
```
> 预期:① 数据以中间 JSON 为准(综合区域=9、出现「离岛防区」);② 字符串里的 `}`(`进度 80%}`)不破坏抽取;③ 前后的「重点突出离岛防卫」「务必准确」作为补充说明被模型参考。
### 样例 C — 纯文本
```
请整理一份台湾滨海防卫概览:覆盖本岛沿岸 9 个综合区域,组织与单位约 280 个,
装备条目近百项(含无人系统、防空、海军平台等类别),并列出若干关键节点与近期演习。
数据以离线知识为准,没有的字段留空即可。
```
> 预期:智能体理解语义后据实填充,产出结构完整的页面(具体数值取决于模型,可较稀疏),不报硬失败。
---
## 后端单元测试(自动化补充)
```bash
# 在 offline-backend-20260512/backend/ 下运行
PYTHONPATH=. uv run --no-sync pytest tests/test_template_fill.py -v # 引擎:抽取/校验/填充/注册表
PYTHONPATH=. uv run --no-sync pytest tests/test_template_fill_attempt.py -v # 调度器执行分支:三形态/默认/只展示/重试
PYTHONPATH=. uv run --no-sync pytest tests/test_template_builder_seed.py -v # 内置 agent 种子
```
抽取相关关键用例:
- `test_extract_embedded_json_pure_object` / `_pure_array` —— 纯 JSON。
- `test_extract_embedded_json_text_with_json` / `_fenced` —— 含 JSON 的文本 / ```json 围栏。
- `test_extract_embedded_json_plain_text_returns_none` —— 纯文本返回 None。
- `test_extract_embedded_json_ignores_braces_in_strings` —— 引号内 `}` 不误判。
- `test_text_with_embedded_json_surfaced_in_prompt` —— 转换 prompt 单独标注「识别到的结构化数据」。
- `test_plain_text_source_converts` —— 纯文本仍照常转换。
- `test_empty_source_uses_template_default` —— 空源用模板默认数据。
- `test_display_only_template_renders_as_is` —— no.2 原样输出。
三个文件应全部通过(0 failed)。
+5 -3
View File
@@ -13,9 +13,11 @@
} catch (e) {}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CM 助手</title>
</head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title>
<script src="/runtime-config.js" data-runtime-config></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
+10497 -10140
View File
File diff suppressed because it is too large. Load diff
+135 -111
View File
@@ -1,111 +1,135 @@
{
"name": "deer-flow-frontend-vite",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language-data": "^6.5.2",
"@langchain/core": "^1.1.15",
"@langchain/langgraph-sdk": "^1.5.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@tanstack/react-query": "^5.90.17",
"@tiptap/core": "^3.23.4",
"@tiptap/extension-bubble-menu": "^3.23.4",
"@tiptap/extension-placeholder": "^3.23.4",
"@tiptap/extension-underline": "^3.23.4",
"@tiptap/pm": "^3.23.4",
"@tiptap/react": "^3.23.4",
"@tiptap/starter-kit": "^3.23.4",
"@types/hast": "^3.0.4",
"@uiw/codemirror-theme-basic": "^4.25.4",
"@uiw/codemirror-theme-monokai": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.33",
"best-effort-json-parser": "^1.2.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"date-fns": "^4.1.0",
"dayjs": "^1.11.21",
"docx": "^9.5.1",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"file-saver": "^2.0.5",
"gsap": "^3.13.0",
"hast": "^1.0.0",
"katex": "^0.16.28",
"lucide-react": "^0.562.0",
"motion": "^12.26.2",
"nanoid": "^5.1.6",
"next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.4.1",
"react-router-dom": "^7.9.4",
"react-syntax-highlighter": "^16.1.1",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "3.15.0",
"sonner": "^2.0.7",
"streamdown": "1.4.0",
"tailwind-merge": "^3.4.0",
"tdesign-react": "^1.12.3",
"tiptap-markdown": "^0.9.0",
"tokenlens": "^1.3.1",
"unist-util-visit": "^5.0.0",
"use-stick-to-bottom": "^1.1.1",
"uuid": "^14.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.15",
"@types/file-saver": "^2.0.7",
"@types/gsap": "^3.0.0",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.2",
"vite": "^6.0.0",
"vite-plugin-source-identifier": "^1.1.2"
},
"packageManager": "pnpm@10.26.2"
}
{
"name": "deer-flow-frontend-vite",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language-data": "^6.5.2",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource/noto-sans-sc": "^5.2.9",
"@langchain/core": "^1.1.15",
"@langchain/langgraph-sdk": "^1.5.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "1.2.18",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "1.3.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@tanstack/react-query": "^5.90.17",
"@tiptap/core": "^3.23.4",
"@tiptap/extension-bubble-menu": "^3.23.4",
"@tiptap/extension-placeholder": "^3.23.4",
"@tiptap/extension-underline": "^3.23.4",
"@tiptap/pm": "^3.23.4",
"@tiptap/react": "^3.23.4",
"@tiptap/starter-kit": "^3.23.4",
"@types/hast": "^3.0.4",
"@uiw/codemirror-theme-basic": "^4.25.4",
"@uiw/codemirror-theme-monokai": "^4.25.4",
"@uiw/react-codemirror": "^4.25.4",
"@xyflow/react": "^12.10.0",
"ai": "^6.0.33",
"axios": "1.16.1",
"best-effort-json-parser": "^1.2.1",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"date-fns": "^4.1.0",
"dayjs": "^1.11.21",
"dnd-kit": "^0.0.2",
"docx": "^9.5.1",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"file-saver": "^2.0.5",
"gsap": "^3.13.0",
"hast": "^1.0.0",
"katex": "^0.16.28",
"leaflet": "^1.9.4",
"lucide-react": "^0.562.0",
"maplibre-gl": "^6.9.0",
"mermaid": "^11.15.0",
"motion": "^12.26.2",
"nanoid": "^5.1.6",
"next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.4.1",
"react-router-dom": "^7.9.4",
"react-syntax-highlighter": "^16.1.1",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "3.15.0",
"sonner": "^2.0.7",
"streamdown": "1.4.0",
"tailwind-merge": "^3.4.0",
"tdesign-react": "^1.12.3",
"tiptap-markdown": "^0.9.0",
"tokenlens": "^1.3.1",
"unist-util-visit": "^5.0.0",
"use-stick-to-bottom": "^1.1.1",
"uuid": "^14.0.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.15",
"@types/file-saver": "^2.0.7",
"@types/gsap": "^3.0.0",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.4",
"lightningcss": "^1.30.2",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.15",
"tw-animate-css": "^1.4.0",
"typescript": "^5.8.2",
"vite": "^6.0.0",
"vite-plugin-source-identifier": "^1.1.2"
},
"overrides": {
"@radix-ui/react-slot": "1.3.3"
},
"packageManager": "pnpm@10.26.2"
}
+3
View File
@@ -0,0 +1,3 @@
# Keep indirect Radix primitives on the React 19 ref-stability fix too.
overrides:
'@radix-ui/react-slot': 1.3.3
+11 -5
View File
@@ -1,5 +1,11 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
import tailwindcss from "@tailwindcss/postcss";
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs";
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs";
export default {
plugins: [
tailwindcss(),
legacyColorFallback(),
lightningCssLegacyChrome(),
],
};
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52">
<defs>
<linearGradient id="ap-add" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f5222d"/>
<stop offset="1" stop-color="#a8071a"/>
</linearGradient>
</defs>
<rect width="52" height="52" rx="8" fill="#ffcfd1"/>
<path fill="url(#ap-add)" d="M26 14a2 2 0 0 1 2 2v8h8a2 2 0 1 1 0 4h-8v8a2 2 0 1 1-4 0v-8h-8a2 2 0 1 1 0-4h8v-8a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(2.236 118.236)"><path class="b" d="M9204.336-22218.762a6.4,6.4,0,0,1-2.489-.691,6.242,6.242,0,0,1-2-1.615,6.363,6.363,0,0,1-1.206-2.283,6.357,6.357,0,0,1-.246-1.863c-.047-.033-.094-.066-.139-.1a6.4,6.4,0,0,1-2.272-3.4,6.418,6.418,0,0,1,.268-4.076,6.336,6.336,0,0,1,1.233-1.912,6.383,6.383,0,0,1-.4-1.973,6.41,6.41,0,0,1,.53-2.809,6.441,6.441,0,0,1,1.7-2.289,6.424,6.424,0,0,1,1.751-1.057,4.969,4.969,0,0,1,.343-1.289,4.965,4.965,0,0,1,1.334-1.859,5.038,5.038,0,0,1,2.016-1.082,5.037,5.037,0,0,1,2.289-.074,5.024,5.024,0,0,1,2.089.943,4.947,4.947,0,0,1,.871.861,5.147,5.147,0,0,1,.876-.861,5,5,0,0,1,2.083-.943,5.035,5.035,0,0,1,2.289.074,5.06,5.06,0,0,1,2.021,1.082,5.031,5.031,0,0,1,1.334,1.859,5.008,5.008,0,0,1,.343,1.289,6.424,6.424,0,0,1,1.751,1.057,6.441,6.441,0,0,1,1.7,2.289,6.405,6.405,0,0,1,.524,2.809,6.434,6.434,0,0,1-.4,1.973,6.315,6.315,0,0,1,1.234,1.912,6.418,6.418,0,0,1,.268,4.076,6.4,6.4,0,0,1-2.272,3.4l-.143.105a6.214,6.214,0,0,1-.248,1.859,6.26,6.26,0,0,1-1.2,2.283,6.271,6.271,0,0,1-2.01,1.615,6.354,6.354,0,0,1-2.485.691c-.14.006-.278.012-.418.012a6.213,6.213,0,0,1-2.139-.375,6.394,6.394,0,0,1-2.2-1.34,6.4,6.4,0,0,1-.915-1.066,6.415,6.415,0,0,1-.91,1.066,6.427,6.427,0,0,1-2.206,1.34,6.283,6.283,0,0,1-2.143.377C9204.609-22218.748,9204.473-22218.752,9204.336-22218.762Zm6.768-6.342a4.232,4.232,0,0,0,.346,1.652,4.123,4.123,0,0,0,.972,1.379,4.116,4.116,0,0,0,1.44.877,4.062,4.062,0,0,0,1.67.234,4.159,4.159,0,0,0,1.63-.447,4.159,4.159,0,0,0,1.313-1.061,4.227,4.227,0,0,0,.787-1.49c.02-.07.037-.139.054-.209a6.32,6.32,0,0,1-1.422.164,1.1,1.1,0,0,1-1.1-1.1,1.1,1.1,0,0,1,1.1-1.1,4.178,4.178,0,0,0,2.529-.859,4.138,4.138,0,0,0,1.485-2.221,4.216,4.216,0,0,0-.173-2.67,4.2,4.2,0,0,0-1.765-2.01,1.1,1.1,0,0,1-.4-1.5,1.1,1.1,0,0,1,1.063-.543,4.16,4.16,0,0,0,.1-.758,4.249,4.249,0,0,0-.346-1.838,4.156,4.156,0,0,0-1.117-1.5,4.078,4.078,0,0,0-.569-.408,4.928,4.928,0,0,1-.385.838,1.1,1.1,0,0,1-.955.553,1.08,1.08,0,0,1-.547-.15,1.093,1.093,0,0,1-.4-1.5,2.906,2.906,0,0,0,.374-1.229,1.022,1.022,0,0,1,.006-.145,2.841,2.841,0,0,0-.218-1.133,2.836,2.836,0,0,0-.754-1.051,2.794,2.794,0,0,0-1.139-.607,2.8,2.8,0,0,0-1.29-.045,2.782,2.782,0,0,0-1.173.535,2.84,2.84,0,0,0-.82.994,2.912,2.912,0,0,0-.3,1.262v5.254a4.426,4.426,0,0,0,.877,2.625,4.4,4.4,0,0,0,2.277,1.58,1.1,1.1,0,0,1,.748,1.361,1.1,1.1,0,0,1-1.367.748,6.6,6.6,0,0,1-2.535-1.41v6.928Zm-10.352,1.145a4.119,4.119,0,0,0,.787,1.49,4.148,4.148,0,0,0,1.313,1.061,4.105,4.105,0,0,0,1.624.447,4.076,4.076,0,0,0,1.676-.234,4.125,4.125,0,0,0,1.44-.877,4.208,4.208,0,0,0,.972-1.379,4.226,4.226,0,0,0,.34-1.652v-6.924a6.628,6.628,0,0,1-2.534,1.406,1.117,1.117,0,0,1-.308.045,1.1,1.1,0,0,1-1.055-.793,1.093,1.093,0,0,1,.748-1.361,4.393,4.393,0,0,0,2.272-1.58,4.354,4.354,0,0,0,.876-2.625v-5.254a2.906,2.906,0,0,0-.29-1.262,2.84,2.84,0,0,0-.82-.994,2.778,2.778,0,0,0-1.179-.535,2.8,2.8,0,0,0-1.29.045,2.832,2.832,0,0,0-1.139.607,2.821,2.821,0,0,0-.748,1.051,2.819,2.819,0,0,0-.212,1.271,2.821,2.821,0,0,0,.374,1.234,1.1,1.1,0,0,1-.4,1.5,1.1,1.1,0,0,1-1.5-.4,5.009,5.009,0,0,1-.386-.838,4.127,4.127,0,0,0-.568.408,4.156,4.156,0,0,0-1.117,1.5,4.249,4.249,0,0,0-.346,1.838,4.246,4.246,0,0,0,.1.758,1.106,1.106,0,0,1,1.064.543,1.1,1.1,0,0,1-.4,1.5,4.2,4.2,0,0,0-1.765,2.01,4.216,4.216,0,0,0-.173,2.67,4.138,4.138,0,0,0,1.485,2.221,4.178,4.178,0,0,0,2.529.859,1.1,1.1,0,0,1,1.1,1.1,1.1,1.1,0,0,1-1.1,1.1,6.32,6.32,0,0,1-1.422-.164C9200.714-22224.1,9200.732-22224.029,9200.752-22223.959Z" transform="translate(-9186.242 22256.754)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{opacity:0;}.b,.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18559 4825)"><rect class="b" width="42" height="42" transform="translate(-18554 -4704)"/><path class="c" d="M-9736.837-26950.139a1.008,1.008,0,0,1-.542-.889v-3.371h-.752a4.506,4.506,0,0,1-4.5-4.5v-2.629c-1.89-.08-3.366-2-3.366-4.367s1.477-4.287,3.366-4.369v-2.633a4.5,4.5,0,0,1,4.5-4.5h15.658v.129a3.676,3.676,0,0,0,.494,1.869h-16.152a2.5,2.5,0,0,0-2.5,2.5v14a2.5,2.5,0,0,0,2.5,2.5h1.741a1.006,1.006,0,0,1,1.009,1v2.432l4.544-3.244a.97.97,0,0,1,.576-.189h13.124a2.5,2.5,0,0,0,2.506-2.5v-9.441a3.524,3.524,0,0,0,1.118.189h.033a3.634,3.634,0,0,0,2.764-1.293,4.793,4.793,0,0,1,1.456,3.549,5.014,5.014,0,0,1-.982,3.041,3.23,3.23,0,0,1-2.391,1.334v2.621a4.507,4.507,0,0,1-4.5,4.5h-12.806l-5.858,4.184a1.021,1.021,0,0,1-.582.191A1.007,1.007,0,0,1-9736.837-26950.139Zm1.334-10.389a1.876,1.876,0,0,1-1.876-1.877v-7.869a1.876,1.876,0,0,1,1.876-1.877h15.739a1.876,1.876,0,0,1,1.876,1.877v7.869a1.876,1.876,0,0,1-1.876,1.877Zm10.931-5.811a1.752,1.752,0,0,0,1.754,1.746,1.751,1.751,0,0,0,1.747-1.746,1.754,1.754,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9724.572-26966.338Zm-9.617,0a1.752,1.752,0,0,0,1.748,1.746,1.751,1.751,0,0,0,1.747-1.746,1.755,1.755,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9734.189-26966.338Zm20.1-5.277-.453-1.551a4.433,4.433,0,0,0-2.946-3.006l-1.537-.467a.649.649,0,0,1-.447-.611.524.524,0,0,1,.021-.148.616.616,0,0,1,.217-.346.624.624,0,0,1,.217-.115l.284-.088,1.232-.365a4.417,4.417,0,0,0,2.96-3.02l.46-1.551a.625.625,0,0,1,.42-.434v.006a.538.538,0,0,1,.183-.033.574.574,0,0,1,.292.08.581.581,0,0,1,.3.381l.46,1.557a4.422,4.422,0,0,0,2.96,3.014l1.171.346.346.107a.647.647,0,0,1,.454.609.641.641,0,0,1-.447.611l-1.537.467a4.434,4.434,0,0,0-2.946,3.006l-.453,1.551a.631.631,0,0,1-.6.461A.636.636,0,0,1-9714.089-26971.615Zm-4.571-7.34-.183-.623a2.7,2.7,0,0,0-1.829-1.83l-.623-.189a.29.29,0,0,1-.2-.277.284.284,0,0,1,.2-.277l.61-.184a2.7,2.7,0,0,0,1.842-1.842l.183-.615a.32.32,0,0,1,.2-.2.327.327,0,0,1,.224.027.275.275,0,0,1,.143.17l.183.615a2.713,2.713,0,0,0,1.842,1.842l.609.184a.289.289,0,0,1,.21.277.3.3,0,0,1-.21.277l-.257.076-.366.113a2.725,2.725,0,0,0-1.828,1.83l-.183.623a.293.293,0,0,1-.278.2A.3.3,0,0,1-9718.66-26978.955Z" transform="translate(-8805.37 22282.029)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 3840 2160"><image width="3840" height="2160" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAADwAAAAhwCAYAAACQvstyAAAgAElEQVR4nOzdiXKjOhYAUOO8///iiZlyYhxHjloLAgScMzWV1wkGzCKElnuHcbyNFwAAAAAAAAAAAAAAAAAAYHuft8t/TgMAcHjinQBwJNfh95fxnNtWeD4AAAAAAAD421r9WlP/jX40ANhOajxF6XPa+AzOILwvXPcAp3e5jBcZgAHgzFQDAADm0dAOAAAAAADwb8anAABnZVzJOaQm7ubWh10vADzcnxxfT4Xb7XJ1UAAAAAAAAAAAAAAAAAAAoB//ORcAcEIiqwIAtDHVq0TgBAAAAAAAAADglXEl51B7fl0XAES8PiFkAAYAAAAAAAAAAAAAAAAAgI7IAAzAPKWZZFORisL1hcvLXAsAQI9EbAUAAAAAAPjNOB8AgG9HrRdN42Ryv1/tuJra47f09mrHuRtfBEABGYABAAAAAAAAAAAAAAAAAKAjwzgKsQZAA0s/TkojRAEAwJZE6gQAAAAAAM7K+B4AgGOKjYdJ1f9yP1e7/lK1mXtbbQ8Act1uMgADAAAAAAAAAAAAAAAAAEBPZAAGoC2PFQAA+CGCJwAAAAAA0JvcTGuljBsCAIAfxg0BMJcMwAAAAAAAAAAAAAAAAAAA0Jf/nA8AmpoiFYnoCQAAAAAAAAAA/ZGJDAAAlqO+DUBDMgADAAAAAAAAAAAAAAAAAEBHZAAGYBlh5CIZgTmjMbjuBxG9AOB0pnqwyJ4AAAAAAMBSSsfltO63MC4IADBekjMzLgiABZkADACwlqmBS8MWAAAAAAAAALB3Jv4CAAAALOp7AvC9EUbECQDWFEb6gjNx/QPA+Xw+nv8f1zZf3YAagHMobbeXeR4AAAjbjWrfD7Q/AexTajxC64Dlxj8AADHqCZyBhEAALG0cZQAGAAAAWI2JWQAAAAAAwN4JFAEAAACwip8JwBpk1pEa4Ft6HgwYBvZiKq+mcm6KeCTCFwAAZzLVf2+P79z7e/3W9XWRUoGzEzgCAADI1Wrcj/FDQK/C9urYuJOwXbm0nTv2+bnrbW2rcTfTc8J4HwAAzih8L9CPCyxlbjvt3PJJO/H27ufwcR6u5z4SAAAAAAAAAAAAAAAAAADQl/+cj070NjNeZglgKWG5MmU+ExkUAIAzudd/52S3nerVe4u0V5qRIJW5AYDfwueD9l0AADgv7wPAWcTajWvHocQ+19u4ltrMxp4PAABQTn0aSEmN4ystP+aOD5w7bmSv4xMPSgZgAAAAgC3cB+doIAMAAAAAAAAAAADgDzIA713rCAGhpdcP8IwM8jgSMgEDAHA293fvs0Tam1vfz/28TMHA0dRGZpUJGAAAjqt1e5BAdUAvctuBjS/J0ypT2fSccNwBADiDcNyJ/lYgRrsqK5ABGIC+3F+YDNYHAOAsDJQBAAAAAAAAAAAA4A/vGYBTmWP2llnm7FLnMeS8wnHsLcNL+HyZJgGbEAEAwNHd67y3x3dcq/4+BvXu8PdHNff71QYrCrcr6BGwtLCdRSZgAAA4jtS4DvV+YC+MB1lWq3ZomX8BADiTqR6tfQVIKZ1/lztuI1wuXD7cbmp+oHmChyADMAB9uVdAvDQBAAAAAAAAAAAAAAAAJ/aeATg1s9vM73l6ybTQ6jyKLAv922uGl2emmuD3IooCAHBEUz23NhNwmOkxVywT8N4t9d6Qe7y8twAAAAB7YRwQsJSwnTTWrjr9XrvqMmrHC8n8CwDnM1UbPP45I5l/4XiWnkeTyswbE9uv3M8rp07pfQIwAAAAAAAAAAAAAAAAwFEdLWA+AId03AnAtTPh11I6M7/XiKsiB0C/jnJ/vpXnj58ijHImubez2wLehZEpt4pUOXc/eo2wKfIntHev57bsXMitN6tfl5l7vGKfT2W60PEE1Eao1Y4LAAD7Fb4H9D4eBlhOq3bDrdqDY9vVPt1Wy8xlX30W4e+2/XoAQEO6jziz8D1KfyocT+l9PTdjcOv5f9p9eSEDMAAAAEBPvgY76VgAAAAAAAA2YmI2AABnYOIvADtw3AnArWa6bx05tfcZ+6n9UyECWntGRnmsV4cDZxBmDI2RiRPehffDUvdHaeTrpTP/isQN+zc3w2PsfT2WCUK9elu1mX3D8yZDMBBKtW9rvwUAgP61zhwB7Nfc9r9UBmHtxFxenjf6G4GtGQcF7cWqk+4zzmR6/9FPCoTmlgul7bfaeykgAzAA+6YjCgCAI5rqtzedDgAAAAAAwErGyARgAADYMwHVAdgxE4BTzKif52jHLxZZNzfibmq53L+Hco+zQePb2jrDytGuk/B+MRGYMyjNBBzK/XxKaebSpfR+u8cikaYiJdcet1broY3Y8a89L3PP59LXw16ut9pycG4G5th6SiOnpyIcl0ZAbvU8WPp4hsLv2cv5bOWv83iv81ZnAp7WF9lh9ec+5J6H1HK5GYFzt1faASYjMfSrNtKs9kQAAFjfWuMsjIeBfpS2q2nX5S9zM5m1ei4cJWNn6+9Ru77ccQcxvZ0HzY3UOMt102pcV6vtblV+bPUc6S0zbqtxEKn1qFZyBuH71V76P1P9taXzTVqL7d/c+RL6qevkzmMK5Z7H3PXWXpdz30dz53PBjpkADMC+qbABAHBk94FcJlECAAAAAABLuY+5MbEcAIAjM6EUgB0zARhepSYS1kYUyV0+NYExFhEldwKkiCj7NjciTu1E2b1cN7kZzuAIaiM+9poBtDYC4VqRF2sjS+ZG4K3NFBrjMQ9xtZljW99XqfIgVt6kyqFWmX9zy+XS49kq827u31sJv9/SmX/DfzeLLP9YoXryuUznu3QSeasMI7XbB9JK22tK2w8BAID15GboqM3kAayntD2sdvnWtBvvW+vMv7X9WJfMv+9F6+8xtx9vbj/iUlQ/oJ2t+r/XGk+WW06llkutL3d7W2U8rs3s3mo8CJzJ3HryVlLzWFLWygwcW1+r95K58xdaZZQ9itLrZ26CttyMv62Os3EOnIAJwAAAAAA9+xp8pacOAAAAAABYgEngAAAchQDlABzQMI630Wx3TmNuJIpJGGkiN1JKKoNr6d/D9ewt4q+MxN9yM/u2Ol5zr5O9nLdpP3VScASpiIPaKwDgBBpF8Fc/pidhx1sq40l4/eq4g/LnQqwdaq1I0AAAwI/c/v/W4wGMEYI07ajsQauMZvfnwv2aP2ozUOntbHwGQJkwo3CuVAbfiXJ4GbHjmns+VZfpUTh+4KgZXFv1D7fa7tz1U3c+YuaOK8s9z5PYfKfceVDAv93vpa92m5sMwAAc3PRCp4MOAIA9+xp8o5EcAAAAAABoyHgaAACOxARUAA7IBGDOpVXkiNYZUGP7lbu/pd+rNNNHqNX33ksG2aNlTk6d79z19x6xJ/yeJgLTg1SEwpjciIfRzycyqQHAlmojAq9lq4jC4XEZHs/02+PfpfXvZ/14Wr96MR2IXYex+qv6LLyrbSfS8Q0AAP2SEQPqteoXDccXhOvTvsrWWmT/fY6n2fi7rN0PE2Y4bL08AGXlaykZgdvIzfDr+LIHLerGayrNpDrX3Iyw4b9L550w77jH5J7X3OutdPup5V0f0JwJwAAcmxcNAACORCZgAAAAAABgrrGTCcAAAFDD2BkATsQEYJijdkJh7YTE1OdyM7vm/j6m18y4KbX73Uum21hkn6UzAefq/bqQ8ewcYpdf7emORXJtdZmvdbvkZsDWIALAFnp//Gy1f896yB8VkTmZgGFPWr23qefCj6UiRwMAANvRPgTv7UitxwMYX0BP5mY46yXz72StjL9zt+txC/C3rcrxs4vVT+f2i+Z+vPfzEqu+h/utmr9ve20PKc282vu4/JjYvIJU/3Tq+Jy9HWxu5t/cvwPdMQEYgHPKnRAJAAAAAAAAAABHIPMvAAB7JgA4ACdkAjAsqTZySUyrSBy5mYRr17+UtSO31GZqjsldT25km1bHYW5E0Nzf9xJxJzyvJgIfy1KnUXsBALCVsONiqrdO9Z5bYV37WR8O1gfAvrVqNwvbTfYWaRoAAPakdX90isweHFlsALj2T45mTvbf1+eAph0A2KewfrvXiZDT11hr92X+PZaj9lO2mm8SU5t5t5ft7iUzcPh9czMlx5YP/156XSx9XQGrMQEYgHNau0MdAACW8NXBZ6QOAAAAAACQYFI8AAB7I+MvAFyGcbyNJj8BsJpeIy9Nz0KdHX+bTtvcwxM7/eF6va8vo/b61oACAMurrocO38/q0np22BakHgxptfXi6f5Sr2YNuc+DVGbfVn0GtRGFZRgGAOAIlh6LM7c9CLagHRLq1Wb/DcfDaHYBgP6l6s36HZcRO+ytDvdeX4fWyqAcG6f8cV1og41pdwGgtXsb0Nfz5SYDMAAAAAAAAAAAAMAhmXwPAMDemOgOAE8mAAOwrlSGma08M9w8tq/z47fpcORG8io9nd7T1xE2iORe5+FyGlYAoL3p+VpaD/362PhTj82tV7fO8Ah7Vnv/lZIJmJ7EngOtnwsy/gIAcCa9trNo/2ENsXaVtdpd4MhqM/++mTIBB7/XXgkA/VBv3kbqsOeOn01Vq2KfDzPfxjLhbmXufuR+n+f3b1X/XYl2FwBWsJN8+ACwsvsLpE4OAAD2QkcgAAAAAADw6j4ZQf8BAAB7YNwLine truncated

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><image width="582" height="667" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIwAAAU2CAYAAAD3cC0IAAAgAElEQVR4nOy9iZPrynH1mVUAe9/u9nZJT/tqy5Zly+tnz/pHT8REzLfa48/SWLIsyZIla3l621379k4CVRNZlQUkQJAEu+/tbrDPLwIBEgRBEARJ4ODkSeMnYwLg1mPMcmvo7+hHapfcTm1uarst+/muGlf93F41q/79uev7Wz+MDFbdnjXY1vzrRLRBRNtEtCn312Ssh42Oaetqfh4yIsqJaCT3c3mNXO6PZJ5Mbo/ksVymtT9sr9Y1V8+3M7aKI6KSiCZEVMjz/YzlljJPIfNPZFp6/oU8lpY5VstN98cyX9dw3jEtzX9GRCdqHqfWVd+eNeh5wDz8im+i2/bz6O74LnlT+9tV94O7+rktu91W/fcErAQ5PkYAAAAACEYJL1p8aQ+ZEnPaw5oIRZtKFBq1hKC1DnForXV/lhhkldjTXpf2tHmC0SyRaBauh2BUKsGoaE0btwSjLlFJi0bjDhGpPS0JUeciGp2p12kPZUvUag8TNeAsBgAAAAAQjAAAAIA7jhFxZ0dcQTzeIqJdGadhWwSgJASl2+nxTSX0aBHJKhePHs+6rcda3Mla9+01+yEWCUzzjqmSoKQdPmWHE6jsGOtpXY+7DuFnLOLRqQxnIippYem0NRzJ/WNxKx3LfBCPAAAAgDsKBCMAAABgdciUaNM1zlsOopEqHUuCURqWEYySowh0Y27gmEsLQ1owOlVjPRyJUHTSEoxOWu6j9jCm6XGJ/QAAAAAYPhCMAAAAgOHDYs0+Ee3JsCvjHZm+I9O2lTC0q4QfXc41UhlBs8rRdNkXuJ0kEe9+qxxtVlnauFVOl8ZJaDoSESk5kJLAdCjT9PilDGfYNwAAAIDhAsEIAAAAuJ20M31m3d5SwtCyghESwO8GmXKfXQZ/CcHoUB4/nZHNdN7KZyru+ocEAAAA3DYgGAEAAAC3iyTy3JfxgYg/99R4XwlD20pQmlWKpkvS4AoCy2KUGFn2KEnTAlEqd0si0nM1fqHuP4MrCQAAALhdQDACAAAArodMRJ3NGa3kUx7Qfg/BaE85h4bAdDt3711Yb0NO1j9Od749L6nn1AHMpvV4bOPcfs5szFJt5HnuRW4so8aGrGlO0883xqgAb1K3+XnNx7yEbRtjG/M1n3+dJLfSxhKvqZ1H8wSjQxnOlPPoQjmSLtQYOUkAAADAa8b4yRjbGNx+zJLHxHe1p4u94rnDTW23ZT/fVeOqn9urZtW/P9e/v1kReB4S0QMZ35PbB3L7nghE26oV/XqrFE23px9d95tYEl9l5TgvreR97Ojl5b6X+6bqHla25iuViORkmhaXSvUY30zzeBnTnL3ZVx3KvJ8nHBkl7szrymZkv7IyvyUyUeixldBTt/mP4o9Vj/HzpTOcyRpd4bx0jEuPG5M35rNGZ0vd9h/TiRKALlolael+Ctx+oQSlFyIo8e2nMjwRAcr1eN1Xh1/xH8jbtge5O96k76b2t6vuB3f1c1t2u6367wlYCeAwAgAAAK5GEnc2OoZN5QZaJBjduyWna04NvhJiojBTCy3kXUvQ8UoASoJP0RKIykoocg3BqBaI0nxOLZ+FIlOtl3Yo1WOj1y0IT/MdRLUIRQsEI1LiziyMKJG2Epd8JQTZxrh+3DamxeVnIgzVAlISjKzRwlFWPe5EMGrPXy9TXtPYhvBlRbSiNK4em/c+r8qop9g56SkYvRDR6HzOgCujAAAAwCWBwwgMAziM+gGH0TCBw+h6eXX726aIPA/FHaTHD2RI5WObHWKSLkd7nSfpfWAhZyICj3THCuNSBJ+J3C+odKWaHsfO6ftlQ8Bpl5h5JQTV82lxx7XueyXq1NOM3Ha+nlaP/YIrt74q+Go+t40uKaO5gp6pStB0mRlJiRlVAlL9mFHPoYaYpO/Xz2sKUM0SNi1QJUEpJ2szcSTV48xKlzsTxZs4/4jI5HI/CVCjW5B3Vc4oSUvDmZSwvWgJSc9kzPcfi9B08UrWCA6j6wUOo5t5XTiMLgccRmAFgWAEhgEEo35AMBomEIyul/77m1G5QhuqTfmmlI7d7xCK2oLRwQ2cdJeVQyg5c6Kbpmzdl8EXjSBj5wolHCUhqZ7uWoIRqTG7huqyMb0du0jlWYvez2W+IDf1pbrMt2f+c+oTinnldTUsKAU3kghE1BKMrAhG1mqBqBaK0vQ45KpEzrZcSVm47+Uxo+a7XgoRjeYJRsmddCoiU9fQ77ODYHS9QDC6mdeFYHQ5IBiBFQSCERgGEIz6AcFomEAwul767W8sEr0hw5tE9Ejdf0N1KttQIdYbKrx645qFIh9cGCzueC8tyv045AdZugjiTuni2NCYSscC0KRRIua9a5R01aVerlGmljJ/jMzrpkrD2rSnNL09elrXfI0pZv7jfZbxeujzrann6T5J6JqoQ75nzec77tVOpFSW5imVxunSs+RMslWuErXuJ8eStSPK7Ig8rVHGopJZDyKTD/s8C01rIjytiTtp7Zrlh1K5jrqcSClw+1MZPhFR6WO5/1gEpflAMLpeIBjdzOtCMLocEIzACgLBCAwDCEb9gGA0TCAYXS/1/ralhuQkWpcSsoMFgtH+NaxzyuSJoo6pcn+ii6cZGs1C0VicQGO5PyHH7iAfhaQ0nXx8jMLjRaPz2Cxxp59Qc8e/yLeOxY6k2e4lJVQZE4O1zSgIQTx4MyJr1oKI5CkKRZkIRjwtCUc81qHdpnImZZLhlDquzQsUf5UcLhCMdCZSciGdKGcSBKPrBoLRzbwuBKPLAcEIrCAQjMAwgGDUDwhGwwSC0fVizKaUi71NRG/J+A25/UDKynZbJWh6eN3OISfCzgU5FoFkzE4hFn1cEIAuomPIFSIOlUE40qVmyTFUh0qn+7Ekrc4a0mgfSzOLp327mb/Tvg1uHv1Nbmc6+anbpmMaNRxLWUdQtlWCkG2UrvkqmHtExuaUWXYkrYvQtEaO+PY6Ed9ncSkITGvXUNJWivhzKsJQEoP49pEqbftEhKSP1PCUvD9/zet3s0Awul1AMBoWEIzACgLBCAwDCEb9gGA0TCAYvQ5sy0G0JblD22TMA3EMzRKMdl7jerkq/yd1A6NG17AUNj0hQyIKVcIRl5KNG0KSCS4hnRvUFHDmO4Mg8IBl6HYhdbuURGaSPCV2JJGIRTwObiS5z04kFpOiU2lNwrjzqmscmeRIyluOpNfF0RzB6Al5/1RcR8l5pIfrbfH/OoBgdLuAYDQsIBiBFQSCERgGEIz6AcFomEAwetXsiAD0tpSTvSO331Gdy9pCUnIQXf7DmLUfxwPCknzIVWFX0HlwKRh/XglCYXAXoUQsCka6/Kyr/XwqS0vt5qu1qMbTXb26unIB0JeurnXVXj6z65wX91EqR2u2/9dlakkgGoUSOCOOJBaTrF0nR5vhvrExK8yE8tFswffusnjlPNKC0ImUrXEp24dSzvahiEsficB0POg9CoLR7QKC0bCAYARWEAhGYBhAMOoHBKNhAsHosmyKOLQtw46Ukj0UB9E7HYLRvSuVlHXtqzEDqN1WvlQOopQplJxB5+T8eRCOWDCydB5KzaKjqFBt53X516zSL4g+YAjMKHfz7ekxrJtDtdlxxEKRNxtBNCKzKSVsG+JMWleOpKxyH9Xj2PWt+zt7lU1WSte1tmD0oeQgPRaXUnIhHcv4bBCfFASj2wUEo2EBwQisIBCMwDCAYNQPCEbDBILRsoxEFHqXiN4jos+IGPSeamW/qZxD2yrQ+tXA+2w80JuQpzPy7iwKQe6cDEUhiG/HMrLoHKodQlFYMlWYdancRF5lySTPhplyBdUCEgQjMARS+Zp2ILWzk2SeqpNbLEUjEYBqR1JeOZTYiRTK2YITaUNK2zaC0BSyyuwmmfAbkKvv7KviQrmOztT4uZS0/V6G36nxs5hPdouBYHS7gGA0LCAYgRUEghEYBhCM+gHBaJhAMJpFppxD2kX0QErOugSjg0u90uyyFhdyhXzIFtLZQ4U4hziA+iyIQ14EIx7HErNzaWE/lueYDsGHUCIGQCcd5W0twYndRFkK0OZSNRODtPk2C0ZpHMTiyokkWUg8Dr8xuZTFTXO5kzkvZWsfiOtIC0ZPZDjpcCCVt2I3gGB0u4BgNCwgGIEVBIIRGAYQjPoBwWiYQDDqYltEoCQKpXFqcb/bEpG2pcPS1Uj7YsgH4rwhfxaH4BY6C2JQyW4iKSeLzqGicgrFk754P2UPxVDrhGnlCyFXCIBpuvKSdE6S+pVSpWh1KHYKybZhbM0olrUFF1LMQiKzFfKQjImiEmciJeHo1ZzEjVuC0LEMqaV/24H0oTiWbhYIRrcLCEbDAoIRWEEgGIFhAMGoHxCMhsndFoyMiD87anwgJWddgtEbUl625KvMzB5K7qFC3EOFtLQfizjEbqHT4BgiEY/K8jzkDpVu0uhOBtcQADfFLDdS3a0tOpE2KMuiSMSZSEksymyathZKXqMLKRcXUspCmv4uX+5k70REoy7BiN1HhyIsHanx9f0qQzC6XUAwGhYQjMAKAsEIDAMIRv2AYDRM7q5gtCWOoc8R0WdlnMSh+y0H0ZaMr7axUo6JDyHUp2FgIYjcKZXuNIpEJY/HzfKzKnNIl6TpVvZwDQFwM3S7kaj6VkqAvOrSxmKQl3I0PS20+M+2gniU2S0iHoKwtBWHICa9ipM8L2LQqXIenUrGURKRfivDbyRc+3pCsyEY3S4gGA0LCEZgBYFgBIYBBKN+QDAaJndDMNJdzNLAYtFbHYLRWyHUdhm6HUSpPEyLine truncated

After

Width:  |  Height:  |  Size: 664 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="xMidYMid slice" width="139" height="765" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAAX6CAYAAAA2hEelAAAgAElEQVR4nOy9+ZMs2XXfd25mVi/vvdkxAAYgIBAESYugaEdYpG3BlIIMOUL2P2uFf7MIibQIYggMIcomTYogAVIkQCwz5Axmea+7KvMeRS636uatu2ZlVeXy/QQK/Xqp6p7uylPfe5bvEfzON2kQQoTupb6ArZ9l+4fPxiW/H3f/d+H/RACmQoa/BABgbIrBj6cUQLxyOXoE/DUBWCZQLACA0RmuWBTh3EVQ0piPeNLPAwC4OqcHluGEAg4CDAAz5dyBRQxQLC4QaACYCddULC7MQKQCir98DQCYDHpgGftoon/9ENUijLeu749AA8DEmKJicRETaBBkAJgARYKaSFMd3JWLDn0uY+VaTFyBBkEGgCtxScUydo7EFqgEAgwA16fYX3jMYysK4fj3uUD5GoCJcCnFYruoz9Xqb8vFQMUAcEH0wGKWdceB9625+uNfU83UP4c03gcAjMg1FYuNmIa61EBgPh6qSgCcGVtgMS+usRWFTcHQhZWMPnyp/zxQMgCMwLX6WHxqwaUw9PfHysPYPgYlA8CJxASWSykYMhSDUjBZINiMGWRMJcOO5C8AwMM4imW8y85evRHeY5It0A39iczHVUFFWI5wCDYAOBgSWLj/r9H7X8zvI9t/CdFd9qaKsQ0tjvkzmUcvXcVIz/0AWC1TUyz2R+9nPw65mPbj2f79w9uYo1LMT60exzwiSUPJQMUAoDFGYDl3DkYpI9YeWXYfF0SCtaOSzWrzHB3F6vuYORhpvA/AKpnTdPPRIYxEpxp4H1i4u+jZclQao5qksD2m67hkfg6AxXOOwOK6iM6tZFwdvpkjqIw1UnB8BOOjipJEcAFrYl6KxQX3LtqqeysM9SIcKkYxxoVvPq4vF4NAAxbLJQPL+XMxx99PVW2EcQv1xoyFnoshi4JBVQkskmUoFjf6BW1TLb73x5iINqtKKrBkXVAROCaBJXLNwDK2gonJmah/Vw4VQ45j0ljYAhkhuIClsXTFQh7Xfz3Jm2lKwpeHGfNn0pPKyLmARTGlwNK/sETyBT20GqXnOqqudJ0ZSuacfr0ZggtYGmtQLDGwoWD0nAxbjjDkUUKx348sgaUK3A+AWTDlwDJWP4zLjsH16OZRSXYfNVWMOZM0RNXojye1QAbArIFiORDKxbBR3XFVlWyPEfq+mRasoFrA7JljYBlaTUpTLv371Rkf2VMxaqiAKI/0jXGhvibv7oEKEZg9UCxufD4vpk9LpiWB9bJ1bBAzK1NQLWDWLCGw2F/d3XuShioX22MwkZDaEaZWNpmhZGICjK1pD6oFzBYolmGYU816EJCG+sgTghhyLWARLDmwhHIxscrF1eLffVztTdo/jDoSyS6omB29oe+Td2+RawGzxWaMBE6HjQCjBwhfgMmMGwCzZE1HoVNzKy7l4nt4PbjEdPHarB6gWsDswKvi+ZGJ9giXGCUA4KwgsJw+o9Ov+tQpl/266r1iqSK/h/oalfRNSfwCMBkQWM7PkFyL3o2LwAJmxxrLzanVINf9UocOTQ+Y0M+Qd/crE74PAJNgmYqFJ5nvNFVLSLFAtYDZsizFogeU04PLGB26+mMpxZLyO1cdvLajFACTBYrlcrhyLT501QLAbFhzS78rZ+LKf7i+zv7+viHX2teiDzG60DtxOaGyBMDVwSvhZWGjryU0oIhOXDBLFrKwjPtvR3rU7m1IuaQ+ZqWpkJjKVKb1tUC1gFkw71fBfjPaXBiSZ0GuBcyKeSoWM5hcN7ik5lqUatFniFzYOnFRHQKTB4rlOsjEGSKhBReoFjB5YPR0UBYu5eEiPtdSq5U6APZVi9RMoWKCRR6hcACYBPMJLOM2v02BSjN1CgUp9Xm9ExfHITBZ5hFYXFWfaQSY8EyRUiv9qedKq/KkeOLmWo4GwQVMknkplmUoFYXZiRtzxNGTuKfaPQBwNpAIPOC6UMe5gO2BkSPVh66C8u4FAbkWMFmmH1h4MTkVG9IwggoFC70yhBcFMFmmfRTa51boGqo/dbrZnmvx31vPtcS4xellZ3jigskywVe97uTBxtvlYvazhP4mAvNDYOpMTLHwQZ0I28qeWWKUkntCSPfE1YOL679YaEcm+LSAyTJxxbIK2OjEjfkPz7oXBbjLgUmCzttjxnKOS5mC1nMtvv3Nx9Uh5t2JPycAozMRxcLabZXIzjS7HGCngDwLmBxXVixsqfhMLriYP5DLYS7k4m+fej58rNTyJqG/izCa5cwcDQBX5QqBxRZEkHs8wQ9X9bWgExdMhgsHFlOhLOo6SHOWs3+1NKaeY6pDRad2UB0Ck+EK5/PV51N8DKkOocUfTA5UhdyYPi0moeqRKwfj+zK9OqQnZkPVIex4BpPiAoqFDwN4y++iPRUeWB3K0dMCpsR5FQtbjj3XmfsZAzZyGy5iTZtck9SVUeWJXcWaa/cF4KqcMbBoQWU9XbRjoOdYUnxaChhAgalwnsCy3AHCWNWiE8i1WFM1TGIfYHz5E3N2KNeOUQBcjTPmWJBPOQFpWFeGUIqlQCcumAJ4EoYZGiFD61P7nxe9m80T1/ZYqA6BSXJaYDmqlKJHZSRss0OhIKXPDiG4gKsyTo7FrP7Mt/IzFUzFQoHgIgzVkiGJC67JsMBiruNoelQwA3canTA5hI7ULlzSWvx3GEoE12S4YpGy7zxvKykvU5CH+lNiP95/nLrBV3SzVIeNibq7XOjYykaLf2ziF4DRGa5Y6psMvCjiaZ3AkWIhI9dSaPuEXAitOgTVAq4GZoXiiXWWi5wROvr6UCeuuY7V9jgCZWcwBeKffEqhmEcgcE702SFKqA5hdghclTjFooKKL58CQugXeewvUAUWszrkwpwdQnUIXAV3YDETs3pgAefjOHywMQOUMjukci34w4GLYg8sNoWCoGKi/0JsasT0xg19ffs71r+6Xx0qDUMn1xZE3Vku145RAFyMYl/Z0QMHFMr1sB949OpQHpEbMxULrBTARSmosjznoFBScamRFDStwm1fy+FhXZ24NoShWJDABRenCPaigCkw1FlO35aIVwpwMdDHkk5sP4tJ6Ot9F75eHUpxltNnh3AcAhcDTVTzgQfODqGnBVycAk+3kxkjv2LhaG6ItCRuFVEdIiOJi9khcDGgWOaF6dMSAi3+4CrgyTYusRe7fnN93obp4m8+lnnf+hi06W74W4OLgSfbvJDG7FAod5IZCVwALgKqQsNxbUo0Vcvwaeg6v9J04wr9c7bZIV9PS25xlgPgrCCwzI+DsxwnzQ6pwAJTYnB2CnTYXo3wtLP5t1HKhWvFwqWWZ3GhHkCvDmHaGZwdKJY5cQgHba6Fm2NRyKOFjMBS4jgEzg0SeuNxyu4TW3A4fIxZ23zQzHFxF1TM9SCu6lCmVYfQuQTODhTLnDAVyyGwZNrHbWBTIrgoCCynY5sd8iVVfR63sS5zqdUhFUz0iWfkWcDZwKvXbOjFAdZUS2y+RGirQfKIrwdgMKgKnY/YUnCkSulEhtjfRV8cXwXUp606hNkhcDagWGaDNQZIi52CD8wOgYuAJ9f4mNvxXZUifyXI/zGFzQDKVyHKMDsELgGeXPNGrw6pNay+vymqQ+AiwI/lvMTmWSigSlzIhOqQ+vgGCVxwbvCqNW/YUCwhbJaVAIwOnljj4cqlhLpxTYXh+Vpr2sZM4Ib6X8g4EkGzgtFBYJk/pvlTDCqwIIkLzgI6b6eHS730O3b9C81CXb/119x0y8y2cPAHY1OgRWqGHIcN7oJEqSVnRWB2aIPqEDgXBZovz4apNkLOcjYnOceO5+7LD5+VXWCpb7cBT13CUQicGyiWOSI0E6jW/GlIdUi3rMRQIhgVvFqNj60KNJZHS/dofHhE3hu1VANMnDJj4hmAUUBgmS1Hi/vZGEqMac5D2RmcBVSFLscpO5/d1aE+kpjVkSgL5FmEsXcIlpVgNKBYloU5OxTyw4VlJTgLUCznR1cWMYE8vjpER07+Uis76wrJlfPBtDM4C3gyLQszsMTkWHL0s4CxgWK5LL48iyt34supmKiy807Ll8TkWXQrBeRZwMngVWp5mJUhRSjXgrIzGA0olvOhZnps7v0xF7zL9Z/7H9Y/u79LZUng+r5P1s0OwQsXjAIUyzIx9w6FQIs/GBU4yF2eUD+LLaeiPiYDX0fa16nZoSKgTFmrDt0Q88MUf2lgXuDVaZlIRxLXBaadwaggx3I5UvtZTDz9LHV7fy91oysW/X4+L1x1FIIXLjgZvDpdj5ETpEdzQ+a0s0+x2KadARgMFMv5aa94ZpvaCF3s5vupmxJLEta8jOPO+9mhIiHxC8ARUCzX4RQbBUdwOHpIdqwH8aGqQzdQLeAUsLv5ergUSOiCdk87H2dSpOZre6vlT3yzQzedatmiCxcMBYplXqSqCD2JGxMkDmVnPDfACSDHcj2G+rP4p537yE556NUhn8l2rikWBBYwGASWJaEfa4WWwD1eGu8Cu53BKCCwXJ5T/VmOczN721tDyAihrwWJrQ7pwSXHziEwBASWa3AcFlKOQ3GmT2L/ZYfKEEdVhoQx7Rxa2wrAEQgsl6O7rJP7WWz054T6uxGPgwDXqoLine truncated

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 278 1530"><image width="278" height="1530" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAAX6CAYAAAA2hEelAAAgAElEQVR4nOy9+ZMs2XXfd25mVi/vvdkxAAYgIBAESYugaEdYpG3BlIIMOUL2P2uFf7MIibQIYggMIcomTYogAVIkQCwz5Axmea+7KvMeRS636uatu2ZlVeXy/QQK/Xqp6p7uylPfe5bvEfzON2kQQoTupb6ArZ9l+4fPxiW/H3f/d+H/RACmQoa/BABgbIrBj6cUQLxyOXoE/DUBWCZQLACA0RmuWBTh3EVQ0piPeNLPAwC4OqcHluGEAg4CDAAz5dyBRQxQLC4QaACYCddULC7MQKQCir98DQCYDHpgGftoon/9ENUijLeu749AA8DEmKJicRETaBBkAJgARYKaSFMd3JWLDn0uY+VaTFyBBkEGgCtxScUydo7EFqgEAgwA16fYX3jMYysK4fj3uUD5GoCJcCnFYruoz9Xqb8vFQMUAcEH0wGKWdceB9625+uNfU83UP4c03gcAjMg1FYuNmIa61EBgPh6qSgCcGVtgMS+usRWFTcHQhZWMPnyp/zxQMgCMwLX6WHxqwaUw9PfHysPYPgYlA8CJxASWSykYMhSDUjBZINiMGWRMJcOO5C8AwMM4imW8y85evRHeY5It0A39iczHVUFFWI5wCDYAOBgSWLj/r9H7X8zvI9t/CdFd9qaKsQ0tjvkzmUcvXcVIz/0AWC1TUyz2R+9nPw65mPbj2f79w9uYo1LMT60exzwiSUPJQMUAoDFGYDl3DkYpI9YeWXYfF0SCtaOSzWrzHB3F6vuYORhpvA/AKpnTdPPRIYxEpxp4H1i4u+jZclQao5qksD2m67hkfg6AxXOOwOK6iM6tZFwdvpkjqIw1UnB8BOOjipJEcAFrYl6KxQX3LtqqeysM9SIcKkYxxoVvPq4vF4NAAxbLJQPL+XMxx99PVW2EcQv1xoyFnoshi4JBVQkskmUoFjf6BW1TLb73x5iINqtKKrBkXVAROCaBJXLNwDK2gonJmah/Vw4VQ45j0ljYAhkhuIClsXTFQh7Xfz3Jm2lKwpeHGfNn0pPKyLmARTGlwNK/sETyBT20GqXnOqqudJ0ZSuacfr0ZggtYGmtQLDGwoWD0nAxbjjDkUUKx348sgaUK3A+AWTDlwDJWP4zLjsH16OZRSXYfNVWMOZM0RNXojye1QAbArIFiORDKxbBR3XFVlWyPEfq+mRasoFrA7JljYBlaTUpTLv371Rkf2VMxaqiAKI/0jXGhvibv7oEKEZg9UCxufD4vpk9LpiWB9bJ1bBAzK1NQLWDWLCGw2F/d3XuShioX22MwkZDaEaZWNpmhZGICjK1pD6oFzBYolmGYU816EJCG+sgTghhyLWARLDmwhHIxscrF1eLffVztTdo/jDoSyS6omB29oe+Td2+RawGzxWaMBE6HjQCjBwhfgMmMGwCzZE1HoVNzKy7l4nt4PbjEdPHarB6gWsDswKvi+ZGJ9giXGCUA4KwgsJw+o9Ov+tQpl/266r1iqSK/h/oalfRNSfwCMBkQWM7PkFyL3o2LwAJmxxrLzanVINf9UocOTQ+Y0M+Qd/crE74PAJNgmYqFJ5nvNFVLSLFAtYDZsizFogeU04PLGB26+mMpxZLyO1cdvLajFACTBYrlcrhyLT501QLAbFhzS78rZ+LKf7i+zv7+viHX2teiDzG60DtxOaGyBMDVwSvhZWGjryU0oIhOXDBLFrKwjPtvR3rU7m1IuaQ+ZqWpkJjKVKb1tUC1gFkw71fBfjPaXBiSZ0GuBcyKeSoWM5hcN7ik5lqUatFniFzYOnFRHQKTB4rlOsjEGSKhBReoFjB5YPR0UBYu5eEiPtdSq5U6APZVi9RMoWKCRR6hcACYBPMJLOM2v02BSjN1CgUp9Xm9ExfHITBZ5hFYXFWfaQSY8EyRUiv9qedKq/KkeOLmWo4GwQVMknkplmUoFYXZiRtzxNGTuKfaPQBwNpAIPOC6UMe5gO2BkSPVh66C8u4FAbkWMFmmH1h4MTkVG9IwggoFC70yhBcFMFmmfRTa51boGqo/dbrZnmvx31vPtcS4xellZ3jigskywVe97uTBxtvlYvazhP4mAvNDYOpMTLHwQZ0I28qeWWKUkntCSPfE1YOL679YaEcm+LSAyTJxxbIK2OjEjfkPz7oXBbjLgUmCzttjxnKOS5mC1nMtvv3Nx9Uh5t2JPycAozMRxcLabZXIzjS7HGCngDwLmBxXVixsqfhMLriYP5DLYS7k4m+fej58rNTyJqG/izCa5cwcDQBX5QqBxRZEkHs8wQ9X9bWgExdMhgsHFlOhLOo6SHOWs3+1NKaeY6pDRad2UB0Ck+EK5/PV51N8DKkOocUfTA5UhdyYPi0moeqRKwfj+zK9OqQnZkPVIex4BpPiAoqFDwN4y++iPRUeWB3K0dMCpsR5FQtbjj3XmfsZAzZyGy5iTZtck9SVUeWJXcWaa/cF4KqcMbBoQWU9XbRjoOdYUnxaChhAgalwnsCy3AHCWNWiE8i1WFM1TGIfYHz5E3N2KNeOUQBcjTPmWJBPOQFpWFeGUIqlQCcumAJ4EoYZGiFD61P7nxe9m80T1/ZYqA6BSXJaYDmqlKJHZSRss0OhIKXPDiG4gKsyTo7FrP7Mt/IzFUzFQoHgIgzVkiGJC67JsMBiruNoelQwA3canTA5hI7ULlzSWvx3GEoE12S4YpGy7zxvKykvU5CH+lNiP95/nLrBV3SzVIeNibq7XOjYykaLf2ziF4DRGa5Y6psMvCjiaZ3AkWIhI9dSaPuEXAitOgTVAq4GZoXiiXWWi5wROvr6UCeuuY7V9jgCZWcwBeKffEqhmEcgcE702SFKqA5hdghclTjFooKKL58CQugXeewvUAUWszrkwpwdQnUIXAV3YDETs3pgAefjOHywMQOUMjukci34w4GLYg8sNoWCoGKi/0JsasT0xg19ffs71r+6Xx0qDUMn1xZE3Vku145RAFyMYl/Z0QMHFMr1sB949OpQHpEbMxULrBTARSmosjznoFBScamRFDStwm1fy+FhXZ24NoShWJDABRenCPaigCkw1FlO35aIVwpwMdDHkk5sP4tJ6Ot9F75eHUpxltNnh3AcAhcDTVTzgQfODqGnBVycAk+3kxkjv2LhaG6ItCRuFVEdIiOJi9khcDGgWOaF6dMSAi3+4CrgyTYusRe7fnN93obp4m8+lnnf+hi06W74W4OLgSfbvJDG7FAod5IZCVwALgKqQsNxbUo0Vcvwaeg6v9J04wr9c7bZIV9PS25xlgPgrCCwzI+DsxwnzQ6pwAJTYnB2CnTYXo3wtLP5t1HKhWvFwqWWZ3GhHkCvDmHaGZwdKJY5cQgHba6Fm2NRyKOFjMBS4jgEzg0SeuNxyu4TW3A4fIxZ23zQzHFxF1TM9SCu6lCmVYfQuQTODhTLnDAVyyGwZNrHbWBTIrgoCCynY5sd8iVVfR63sS5zqdUhFUz0iWfkWcDZwKvXbOjFAdZUS2y+RGirQfKIrwdgMKgKnY/YUnCkSulEhtjfRV8cXwXUp606hNkhcDagWGaDNQZIi52CD8wOgYuAJ9f4mNvxXZUifyXI/zGFzQDKVyHKMDsELgGeXPNGrw6pNay+vymqQ+AiwI/lvMTmWSigSlzIhOqQ+vgGCVxwbvCqNW/YUCwhbJaVAIwOnljj4cqlhLpxTYXh+Vpr2sZM4Ib6X8g4EkGzgtFBYJk/pvlTDCqwIIkLzgI6b6eHS730O3b9C81CXb/119x0y8y2cPAHY1OgRWqGHIcN7oJEqSVnRWB2aIPqEDgXBZovz4apNkLOcjYnOceO5+7LD5+VXWCpb7cBT13CUQicGyiWOSI0E6jW/GlIdUi3rMRQIhgVvFqNj60KNJZHS/dofHhE3hu1VANMnDJj4hmAUUBgmS1Hi/vZGEqMac5D2RmcBVSFLscpO5/d1aE+kpjVkSgL5FmEsXcIlpVgNKBYloU5OxTyw4VlJTgLUCznR1cWMYE8vjpER07+Uis76wrJlfPBtDM4C3gyLQszsMTkWHL0s4CxgWK5LL48iyt34supmKiy807Ll8TkWXQrBeRZwMngVWp5mJUhRSjXgrIzGA0olvOhZnps7v0xF7zL9Z/7H9Y/u79LZUng+r5P1s0OwQsXjAIUyzIx9w6FQIs/GBU4yF2eUD+LLaeiPiYDX0fa16nZoSKgTFmrDt0Q88MUf2lgXuDVaZlIRxLXBaadwaggx3I5UvtZTDz9LHV7fy91oysW/X4+L1x1FIIXLjgZvDpdj5ETpEdzQ+a0s0+x2KadARgMFMv5aa94ZpvaCF3s5vupmxJLEta8jOPO+9mhIiHxC8ARUCzX4RQbBUdwOHpIdqwH8aGqQzdQLeAUsLv5ergUSOiCdk87H2dSpOZre6vlT3yzQzedatmiCxcMBYplXqSqCD2JGxMkDmVnPDfACSDHcj2G+rP4p537yE556NUhn8l2rikWBBYwGASWJaEfa4WWwD1eGu8Cu53BKCCwXJ5T/VmOczN721tDyAihrwWJrQ7pwSXHziEwBASWa3AcFlKOQ3GmT2L/ZYfKEEdVhoQx7Rxa2wrAEQgsl6O7rJP7WWz054T6uxGPgwDXqoNVi/9G+56ugKE7y1VQLSAV7BW6Bv3VY+fpF+n/XLine truncated

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="273" height="323" viewBox="0 0 273 323"><image width="273" height="323" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAiIAAAKGCAYAAAB3OfDSAAAgAElEQVR4nOy9abMlO3YdtpDDme5YVW/ufv16YLNp0gzJCsuOsBz+YDkUIf1ef/QnR9AWJYeowSRlNalmz/3mV1V3OFMmHBsJZO7cB8jhDLdu3cKuypsnkUgkcgIW1h6g/ve/1IgSJcrTk6IElAKuFsDH19qsabtKV0hUtc3FbmcAJgCeAfgMwI8B/ATADwB8YtOvAJwBmNFhANYA7gG8BvBSa3wB4HcAfgHg7zXwdxr4Qmvcao01tNbU8tCfUgO6BEpU66qOGjdL4OsbhZulMvnSJL6kUaI8RcniU40SJQoTAhXXAD6woINAyGca+CGA7yngQ9qvgUsAc1Xlhx3OrLXGjQJea21ACi1TAitaGcDyGwB/AAxIuYs3PUqUKIhAJEqUKEI+BfCPAPwPAP5Ua8OCXGrgwgALhQVvNxyfSoyFBiZa47lhTLQBMj/WwJ+piiX5yjAjGv8OwP8D4G8A3MSbHyVKlAhEokSJkgL4yKph/lxr/BMLRH5qWY1KlF2aFVqKXV0tmnZrzIkxIWCimwN+ZlmWj1S1/lvLkty+808gSpR3WCIQiRIlCgGQ/wXA/6Q1/rwkkKDxoSK1ikJlS4IKTKgAELEYBGUCKF3Ze7idpQMoGh8rpUlV82Ol8N8D+L+0xv8B4D9C4Q7RXC1KlHdSIhCJEuXdlbm1AflnWuNfEAuiNT7VyoKIBLVBaw1AxFpb8GCwhqpAiLbAoyxR79S6Vt88B6lvFH6oYNQ4U6vu+U9WfRPhSJQo75hEIBIlyjsmzjNGA38CjX+lgX+uNf5Mk4GqtvsZAJELOCPCAIkFGoYNUQHQorUynjFaYaqAP1dKX5IBrNbG+PXfQOH3EYxEifJuSQQiUaK8e0IeLH+sNf65Bv4l2YToih0xQrghsWyIWZIqMelhRErLiMAxIUnljkvr0rrlOrBigcsCCv9NooxnTQJlTvEXAD6P72SUKO+ORCASJco7JlrjR4D+lwD+VanVP9KVGy5U0jAZnYtgRAwW0UBiQUYrjxJl2jS4+CFaEYD5LFH6Xygg1Rp3SuEvofBtfC+jRHk3JAKRKFGeuOjGvZa+9w808E+h8b9qjf+u1DgHAwkcMCi004kZ2QEirHxjE2JVO0rXeGNHeLplRlIofKoU/mcAXxvD1crFN8YaiRLlHZAIRKJEeeJCnX1BkVY1XiSVZwyBEHLTvdC6HV21lw0RKhqtG01MbVsiAIzSAuCwtTNkLao8nyml/7dEGw+arwH11/HdjBLl6UsEIlGivAOiNTKt8X0N/LOyNN4xLyQI6ZLELs5+pGUjUjYMRzHyVipu6KqQK4U/KRW+1MBfa43flxqvtMY2vqNRojxdiUAkSpTHKQMhQqe4WGKEId7XGn9awqhjfqS1mlQGHeJETtci2A0nNSti533hRqhgDEhQLxMQ400Dc+y0UPgjKP0/augvAPUfFPCNK/5Y9yRKlCiPRyIQiRLliUrtTqtxpjV+Co1/rDV+WJaYMayxl5ykN7cGrFB4rhT+aaLw+2mGXxc5vtls6yxHQSNRokR5PBLns4wS5TSiDlz2lea8DdvxPoB/Umr8k0KrD0pdzWbbe5a2q209U25pDVPNUnu/tGOJuPVgUU2ckaLAeVGq/zZN8I8WE/3xYqKRp7oOlHYgFnlTzyVKlCgBiYxIlChPTBzIyBJgkkHNJ/ggTfBnZYmfamBKACIZOASpQ7fbDWUtU1vGqmVjtKoFWyK3e89XgRkKX3KdJvhsmuPTosT1coPXKBxhEiVKlKckEYhEiTJMTt3/HbN8TR36LAcu5np+McdHWaJ/VGgTwTTRlhGR7IU1GG3tA2M23LosRLRUwZoctFh7EdootSYm54+Uwo+UUj9ns/Uek50Yq2Uae95okxIlSo9EIBIlyhMTraFJMZOlyOYTPJ9NzGy3z7clMl22r1XLpbErMaJsZFQFFiNEhm1nbrycIdkBO06tM4ApsfPUnGuFH1IANg0T+v2m45AoUaK8pRKBSJR3XU7FdOxb7qHHOU8Z6voXSmly2f0UWl1UAccUs8ewBxBwSBhogGBCWBh3SAChmzxdzIjXloSdT+aFAUPqHNCfao0fmBl629cqJwAeK5qVNUZOxaBE5iTKOysRiESJ8rSk7vg0cFZqfFIW+Ijml3EdfMJAQ81sWEBSqsbfl6b0T5iLbsvVV3SbWgAN87u0C9CmQXSbeSnRLLwuiubESfChBj4EzAy9TqKZSJQoT0giEIny1OXYndbY8k6VXzICSu7RGnOtTfyQ98lkpDVBnf2hArYgBBCS0s6uq+zEdcxSVKn2ca4cXQowss9SOjsRTFDi/bK6hnpSvsA9kmnHZhjGMijHZk4iYxLlyUoEIlGiPA45hSpnojWuyhKXGpgYbxm0IYxkSWBn0SUQUibNCVQpApsJ4KI8qhjDsDDgUzI1TE1/lP4u1oIcpRUuAFzRtexx/V1yqGonSpQoR5IIRKK87XIsxuPUzMWh+2WHqXr2k+S6CmZGS+5UL6rp6GsAUhunwoIQ1YANM1dM0swZ45OSMyIem49eZsQdwxgVVOXNzcR8Foioppp99+dY+8fKUObkWIxJBFJR3nqJQCRKlLdD9gE0Galkykotk2o3I66z9dCNXQiRH9qFbRcsiFnK3XDvXLzGqiIOiQMkNUsijGN3yqxWVKspgLTnWofcjyhRojxCiUAkytsiD818HIvJCKXLkfNQ5gMiXyg0mTLgoySVjM7h4ocILxhtC3DMhWIFun3uR1cwsZYHjnDn9bIi0sXXw6S4gsuq7gmbv8Z3zcIxeW+mZGz6WDk2YxJtS6K89RKBSJQob0b6gEpo20lfbFRltSptI1T7g6Mb4ynDGQ/VrMHYEfQwIm6t4QcfrQisuqlPzZyUgTgjpfe+hO5HCFj0SWRQokR5QxKBSJTHJod2CKdmPMYyHaF8fczI0PJDI/XE9P0am7LE2mhCrM7F2H8wQ1RuE5Jw1Yy2IIXyMG8Zn7SAiLI2J7q93oktwgALOEPCbFbssQUzYZHEzLGYjbGMx6mYk4diTCJTEuXRSAQiUaIcV4YyGnL/UOATYkJ4utOwUH++pAUltpp7njivF9W2CXFsSL2w/BgCRDi4YGvOkJQin1PLlCzwGVMHFQBWFh/RNRXiWvuAWV96SEKqsMicRIlyZIlAJMqbklMzH6diPPrKkx2eBA4yva+DC+ULdqzW9pRsRDZa40aXuAExI1YlU+tsnCcMRy5g3jGqYUI05Fn8V+UAScm2ue1HGVDdlDs2Ihqq1FSfm1LjNdWfARFfLQ5lKFy+UmwjkD60vGMxJkNVTpEpifLWSQQiUaIcJmOBiwQmcjvEqPSl8/2ZLZeAyMsSeAlgXXunWOWGi+ehVTtyag1UhJ1I51WFgAgkwGDMiFXHgIETLoRDtDJ1/47qb68pCTAiUsaquEIAse/Kj2XEGiXKOysRiEQ5tbwtzMe+5ciOq297KNAYW0+enkKRpwlWpVZflhpfKo17XSORKqsu2126ZtYX2lpjaDHJXUgaL5emrJaHDrP98MYRKRtD2sZWRK+01l+WpfpKAysFpNbxp6s2Q5mIQ5mS0PZQOTaA6WPWIlMS5dFKBCJRonTLPkAABzAfx9jOrLHqfVHic1oSpZdAFTq9Vr0kLGKqaoKdmYKYTUgLiPTYiOwAEbbf58pbckbEedWY6O70D/cAPrfLPYsl0sWIhDrksaqUPqak73mNBTxRoryzEoFIlGPJ2858HJvxGKtiObS+iv1KybxDa7XcFvoPidK/S1P9WlkQ4gKbgduMtDUxrd/ae5K2+IBIiba6ZQeIlIwxacUV0RbE6BsN/Fprs9xZIKIOfNf6AMdY4HAoY3Lo+YcCr2MzJRFYRTmaRCAS5anK0M7q2IzHqQDImG3TWZcltluFV0mBL5XCV4nS99BqLoFI4jxUWAh3x4LUhQ70mvExIk4jtOOeK71qWBFlxYi8Lkv8ugR+o4Fb216VAxmRnSqK9VAA0JfvUMYkSpR3XiIQibKv7DsqfWzMx6kYj32BxMGApO4pNdS2VJsE6qskwX9VCX6stf4BNIVMV+3p/Us2o64DIp65ZYa477ofWqYLRgRs3XjTaANCSmBbQn1Ravyy1OoPxO4AmDmPoAMYjbG2IceyKZHH9dmUHMqUhKSPKYm2JFEeXCIQifK2y7GYj1MxHmMBxtGYEQ2Y0O6FVt+WJf6mhP5UQX8AqCnAVDQMeLgExe1FeOGBbobbgrTSJSDhbrpl27vGrTV0UZb4vAT+odTqcw11twcTMnQ7lG8sU9LHkEg5FdCIEuWtkwhEogyVh2ZADmU+hjIdsgMIzt0SWIf2nyp9+LaqAphprb4rNP5aaf1JovTPgPJK6cRkgAMJci4Z3XjOtC5uJCNSpzEGxAGRmhmpjyN/XcOIfKc1/pOG+k9ak8eMOWsunuFYW4+hgOLQ9LFMx1jvm1MzJdGWJMqDSwQiUd4WORXz4STk1XIo43FsgDJmO7dYYlmU+K0C/gtIRQP9EaAvdKlaqhkocXLOkLj0PkaEJWiW4J0UT7ePr9JMJPovSqj/oKH+GsArVBFhtZufT9yLUzEhofR9bUVC5fflj0xJlCcvEYhEkfK2MR99KhXZscsGfkjIdF85ofSHBizB7fqCNdZFqW6h1S8B/PtE6WcK+k+h9bmyRUnVjHYARNytvt4wyIzwOCJobEOcGLsQXRIj8nUJ9XOt1f+roX5h1TK55xr7gEMoXx8w6Ov4hwKDsQDiTTElISbkVLYkEVBLine truncated

After

Width:  |  Height:  |  Size: 193 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(255,173,32,0.15);stroke-width:5px;stroke-dasharray:7 5;}.e{stroke:none;}.g{filter:url(#e);}.h{filter:url(#b);}</style><linearGradient id="a" x1="0.761" y1="0.205" x2="0.284" y2="0.581" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#ffd2ab"/><stop offset="1" stop-color="#ff7855"/></linearGradient><filter id="b" x="88.068" y="60.324" width="217.004" height="181.59" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="c"/><feFlood flood-color="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.237" y1="0.761" x2="0.718" y2="0.394" xlink:href="#a"/><filter id="e" x="424.041" y="484.359" width="215.309" height="181.689" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="f"/><feFlood flood-color="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="f"/><feComposite in="SourceGraphic"/></filter></defs><g transform="translate(4 4)"><g transform="translate(-126.998 -127.002)"><g class="h" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="a" d="M18115.566-21172.494q4.919-8.443,10.389-16.543a294.531,294.531,0,0,1,35.713-43.289,294.545,294.545,0,0,1,43.293-35.717,292.5,292.5,0,0,1,49.682-26.969,290.614,290.614,0,0,1,31.271-11.162l1.656,5.506a288.092,288.092,0,0,0-166.973,131.084Z" transform="translate(-18005 21389)"/></g><g class="g" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="b" d="M18451.539-20750.967a288.209,288.209,0,0,0,165.279-131.172l5.029,2.92q-5.1,8.815-10.8,17.258a294.659,294.659,0,0,1-35.717,43.287,294.647,294.647,0,0,1-43.293,35.717,292.2,292.2,0,0,1-49.686,26.971,291.1,291.1,0,0,1-29.178,10.537Z" transform="translate(-18005 21389)"/></g><circle class="c" cx="359.5" cy="359.5" r="359.5" transform="translate(126.998 127.002)"/></g><g class="d" transform="translate(98 98)"><circle class="e" cx="261.5" cy="261.5" r="261.5"/><circle class="f" cx="261.5" cy="261.5" r="259"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(255,173,32,0.15);stroke-width:5px;stroke-dasharray:7 5;}.e{stroke:none;}.g{filter:url(#e);}.h{filter:url(#b);}</style><linearGradient id="a" x1="0.761" y1="0.205" x2="0.284" y2="0.581" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#ffd2ab"/><stop offset="1" stop-color="#ff7855"/></linearGradient><filter id="b" x="88.068" y="60.324" width="217.004" height="181.59" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="c"/><feFlood flood-color="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.237" y1="0.761" x2="0.718" y2="0.394" xlink:href="#a"/><filter id="e" x="424.041" y="484.359" width="215.309" height="181.689" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="f"/><feFlood flood-color="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="f"/><feComposite in="SourceGraphic"/></filter></defs><g transform="translate(4 4)"><g transform="translate(-126.998 -127.002)"><g class="h" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="a" d="M18115.566-21172.494q4.919-8.443,10.389-16.543a294.531,294.531,0,0,1,35.713-43.289,294.545,294.545,0,0,1,43.293-35.717,292.5,292.5,0,0,1,49.682-26.969,290.614,290.614,0,0,1,31.271-11.162l1.656,5.506a288.092,288.092,0,0,0-166.973,131.084Z" transform="translate(-18005 21389)"/></g><g class="g" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="b" d="M18451.539-20750.967a288.209,288.209,0,0,0,165.279-131.172l5.029,2.92q-5.1,8.815-10.8,17.258a294.659,294.659,0,0,1-35.717,43.287,294.647,294.647,0,0,1-43.293,35.717,292.2,292.2,0,0,1-49.686,26.971,291.1,291.1,0,0,1-29.178,10.537Z" transform="translate(-18005 21389)"/></g><circle class="c" cx="359.5" cy="359.5" r="359.5" transform="translate(126.998 127.002)"/></g><g class="d" transform="translate(98 98)"><circle class="e" cx="261.5" cy="261.5" r="261.5"/><circle class="f" cx="261.5" cy="261.5" r="259"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="458" height="113.588" viewBox="0 0 458 113.588"><defs><style>.a{opacity:0.78;}.b{fill:url(#a);}.c{fill:#fff;}.d{filter:url(#d);}.e{filter:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#fff9c1"/></linearGradient><filter id="b" x="104.221" y="19.443" width="226.559" height="61.289" filterUnits="userSpaceOnUse"><feOffset dy="1" input="SourceAlpha"/><feGaussianBlur stdDeviation="5.5" result="c"/><feFlood flood-color="#fe0" flood-opacity="0.71"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><filter id="d" x="104.221" y="19.443" width="226.559" height="61.289" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="2" result="e"/><feFlood flood-color="#fff" result="f"/><feComposite operator="out" in="SourceGraphic" in2="e"/><feComposite operator="in" in="f"/><feComposite operator="in" in2="SourceGraphic"/></filter></defs><g transform="translate(-731 -564.412)"><image class="a" width="458" height="113.588" transform="translate(731 564.412)" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAADEEAAAMKCAYAAADQmruwAAAgAElEQVR4nOy9W5Mkx3kleDITWZVdfaluNLpxI0EAFEAQ4AUUKRHgVTeOxsZsH+dBL2uyeVrbB/2ctX1aW9u1fZqHfZDt2tiSo5FEzYgakhJJiSAIUCAIEsSlCaIbDTSyMjoz1yIz3DvzeJ763COzquvyHTOgOiP88vk1Itz9O6cz+bO/wGpUzdV+87f53R9Q6PHyzxitt3y916Qzrlb/DvlQNGBL2Dei+/xbodtkN1m2N1zHcHU5q8lyPvE+55drRyF63dXhx41d6v7RRLepoEcB/D6A3wPwOwB2m3vVIVtdd+LbTb6TjPAOh8PhcDgcDofD4XA4HA7HUUansW3ULHgOm7WvsJjbubNGN1vo3G7W67rNvakoW4jXa/7rLlyfNn9voY9Xgd4/Y7D7Cra6N4D+WKS3Kv1ps1Y3av6eZIR2GjftU+1T9ycBoX1DG99uyr6wJsv7Egs9ucaWuB/RdMnRh0347eV0R008uXxP6XO+Zvx9MW3Sn+ryCXtGYtk6li+zPDG/vcz4AaFeJ8JOme509X3az+B6Vds/ENe5X6jyWdtKyr6IUI+qPqqmvP3mr+o3Yv8t6Xecv9X/1dZKqIc9cT9A5L//ttfUtovsi/XB9op+GiD7q8q/ZT3v3y+mGDXtm7QnlUPWW+hfeyJ8uC/GfTIvhX64TfbS/JfUA6dL6cTre9Nlu0R9qnpQ9gfIccHhaXya/ciat43+kTyPKR9VXyp8jCfqheshtnPmuJL9LqvfT5vr0+L6VP0t1o/Vbwhy/g7XzzQX9pbtCWC7wvy3lbwf0PMp/MMoD8gu872E0rfqN7muwM8h1T4x31De6XI4iqdgltN6f0Pe9YiF5/A7r98DVL2F9+Y8jCfTeH6Cz1OMxfxq3Qe9H4RzLUl49R5hHUPoGukKO/mciBVPwSq3ssustxmmd+plTbtjOKu++HmaWb9JPOqwsrxWxxb2cH2MKd8enVvi+wj3c8sDym9I4cWADXbEL3tR/hgupEvpFY9Do556dH9M6cwxTcoZ7czsN/K3gWgvluMl9U8Tt1nfArF/WwFF/+J65HN0XG+qPVW5k/ui/HG8kXkxPfMDcf98pH3Gg0v2GxqPveZ5Wz+LsKpdMs8VJv1bjQcuB+cT0rPsD8hd8KD+IR/4ap6y4lvhLLuzP/gy0wnlsOzgcDOE9bBxfB+rxLjuW+c/+bwro7Q9OTzDWKgZS0PyUIVzuv3V5Vo8f1ytmtys9YBcjFcXkzES6U7Itm5v9XWJQnurcH55a/X11MD5n8e/2oRrLsd6DxeCvWTPUjFW9N2K3ycCyB7uLyvbNOS/cI/rfaleRd3Fy6Pm+3Ckx+1kMp9quGjNcO785TeFnY6jgnu8JRx3AWcBPATgGQCfBfDpxgniQvPgP+zNTXeCcDgcDofD4XA4HA6Hw+FwnCSEQ0IVOUEs7tz3m5NcwQGit8+JnUU0ThDVogPEghNEv87rKoDLqKqPAf1r2ML7hYf7bzen0MI6Ydmhp+MDdoK4fcLXJ5UTxEl2/AiYNm07OQXlDf16svDfacB0oZ2np6RfY0U7n9T5OuC09OtFh8jTMnfhFJZ58bl8Wp7HOEXPZB7H02P3fBp9uI1+7wIqnGvOtXSy59/ZAdRq3s58iN5yBigln8wOv+F05/cX37uOchuftvcFx9HAlPrdaXnOOe4+uO85HJ3mffsDAB+e+tpwOByODcOdIByHjVrt4TMAvg7gOQBPAbjSOEA4HA6Hw+FwOBwOh8PhcDgcjs1h0myyjemgV+eO00PVXXBkyMU+YaspKtwH4Ang1i30d29itHcLW93VVFSrGZon8dBSRC6Tt8UsTuEl8y8x7PJ9ZoxPmLiFXctMu50m3Qm2tldvkCfM9JuqB4u5WSko5DK+q3LM0q2ZYJtDAZPTclh8UeFkUZHlJGLRuScoupyGQ6aTBcKlk17mgOmCis1p6de3qV+f5PKGfn3anNYWx/HklLQzK3CdFufTk67AFbBKgevo9+vR3jaq6iMAPtY4WO8sHCC0sOjccxqc1rDwHpI6feQiN54Kp+Mv9sHb0hmkLP8pOVakYzkSRzNTupFfEo6Z8kX8sVA8UOmzfRyP07sTe3yKnEnajt/F95hqxXqIw3GQWHyHrhaeSe78dToR2r3TLCz+GsDPAFw/7RXjcDgcm4Q7QTgOC7WTw4MAnm6cH2oniE81qhAOh8PhcDgcDofD4XA4HA6HY/PoLqg+HBY683Xn6h5U9WGlG/dhsDs/XL/V8kCOw3FyMHUniBMLd4KYw50gTgZWHR50J4iTh9PqBHGbDiaedBw/J4jR3gCYXMb41kdQja8C1YXG1TdfCeI4Kl+sj6N8yJsP728C06X/TOWMzByVc4IZb00nkrxwdTmbdp45ZyyMYytd5XwinDwi6L6sHw5npJundLKgsmYpunQX7WMFwjF6g9XzgbRDpC9/G5BOL1xPAyzZ2bo/WgFU+yzVow6fq7Bj2T9vF9wps9VvhD3Kzjv5ZNqXmW5S3qX7d/per3t74Vqnya8ev4XPJ24Xrn9xXdZHZrhsrIzfWejP09X2lea/crwUhN9UOvv+XujTA2AcH3sd9GbkMG8DeAn9wcs4u/02+oP3Y0wmVQlkLHw/ITFhEpeAEH5v+fdWJcJnYjUZzASjvUlD8jIvb0I6o0hkjHIyVLkleQz2r6+k3tnulfWuleXYDi7fcrjOQjvU70nv4+Vv3VwdweFw7Ad3gnAcBmr1h2cBfAnA8wA+2ThEuAOEw+FwOBwOh8PhcDgcDofDcZgIG3BtGUKLUAFV/d8YGOw0G4NNvmETaSts9vAmHG9e8aYRbeIx4maW2twL6YfrvOnVqNPHzbHmstq8YsRNr73l/BNlCKG4kJSX8me7YnjVrlwPQtEi5hP+wXZTegqJogQj1AsrTIh8onIHtWssx4fL0WJ5eHM4pMPKHiGisEfVs+xX1I536nfuJBTTY2WNzM3oRJGE6jnpJ/Rb1g/3g1AfodxNuKg80qVwS/ndccDaOrM63di+rDhC5Ygg+2I41W5c7nBDKaMYhwd4PuHyg/snVsfjwwjcfiqfaK8YF6D5JJaXyiM3+8V8kIy/WE/Nxn+j7DIP15EKNqpfs+JOzJcPP6hDDNZhkEwlH/U8ifFm6d9RKlL9YIvbhxWPwjiieYmVdlQ9crrJ84LGtzoEEsCHPWL/msWbYqtp33l76+cTRL0zknET8hXPyfib208pIvF421u+D54PluafKbaq5qBWf/lAC/c3NU/JQy9GvOT5PaFw/N7CsPp5sG/lc+YOY/6oObQU04OIz/61mc8va7yrccvjU+YfxgnPH0vPqSlGzaGlLXFoST035fwhDpfFdLF8P3fe4/rcoveeZL5eyrfTKG7V/WeycFhrrty2eOiK31u5/464XOJQV/I+p+Yz9T4xu96dfTQMb+ygGp8Bqn5zrdN8x6SH3cdUT5FJP/zmcGRHDG/03yQcHUq14qv0Asz4wm5wuSg9tjspRyIpJ+wbinDhPpmb5NuMN0vLQ7afyF/Nj1a75rZ7TF+V36pPftAmE6thz2L5RtM0PNsl2jMbqwUcU/sDrPyM+2Y7hHHW3B+r9FS9by0fsi9u98J5ojWWxts0GdfJOLPWN6zrm7rfwJxnOD8uR3ealU+C3P6+6XA8LwQsxZsu9btZXXQX77Vor03hsPIJ6JKzS2G+piCpeh4Line truncated

After

Width:  |  Height:  |  Size: 625 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{opacity:0;fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.405" x2="1" xlink:href="#a"/></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-14540.154 22987.455)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="188" height="241" viewBox="0 0 188 241"><defs><style>.a{fill:url(#a);}.b{fill:#fff;opacity:0.47;}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 376 482"><image width="376" height="482" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXgAAAHiCAYAAADiYNeDAAAgAElEQVR4nOy993csOZYmdoFIw6R73pav6qrqrvY9flatmdldrWa1+nv1m46OjrSr7ZnZdtW2ulxX1Xt8ho+eTBcB6CACQFzcQEQaMskkeb938iUzvP3w4cPFhdDpGK41hGg+ez3jxZm0veg69r85Vj0zzHqe/thnOmgJAIn9xh8zrQsAqwBwAwA2ybf7rNqPWbaH1nG/V+zfCZRX02y/BQBtO90gAwDz4KcAoOw0bacPAWAAAH3794n9xr/NZx99DiD8dutkdvv4k6F9Toae88Zc+LOk53umZj3fSec57/W7Imhd67NnnCc6loAdETsy7iKiXgOADQBYtyTvvjft9B4i9hW7Tfy7a6dhghcNNKBR0eYIfoRI3hH7iPw2n0NL7OZzhL7N9GNE8kO0PVdwDOw2GYyFggmesWiYZ+wmADwEgHsAcB8AbgPAHft9Cyl2R9DtyHfbbqtlCRx/5JznQMk/sftdt7+d4saf1H7G9jOKfA+Rot8FgB0AeGW/XwDASwB4BgB7dlsMxkLABM84K6zZzyr53LREPongO0t4J5yN1J5j3dEUBL9jSf6EfI7th8E4FdiDZw++wPwefMcS9+sA8BoAPAaABwDwyJL4LUT4MYsmmWIvlxlZjUXjSH3Xkv8WADwHgKcA8AQAvskLAq3ns3LYg59ve1cMrOAZs2DN2hfryCu/awk9RvC3llSZnycSVMDFMLIkHyN483sbeftH9sPqnjEVWMGzgi/QfJ7SEvYbAPCW/X7TEvl9a7GsEZume34HfyXgInSO0efAWjqG6L8CgK8B4M/2e6sxGocV/Hzbu2JggmeCLxCe5xoJVbxrSR0T/BuW4Ncu7qCvBY4twX9NCP5rq+5xiGap7Jng59veFQMTPBN8Ae2P5JFV5+8BwLcA4B077S6yZ9xn3ugVxmxQyJ5xn1fWzvkCAD4FgM+syt/K7yYT/Hzbu2JggmeCv5V/dB7tctcq9LcIwd+5kCNjTMIrQvB/tp9tEHl0zq79nC+Y4JcGTPDXl+DbVpl/GwC+CwAf2obSB6hz0Q0b9cJYXvSRRXNg7RzTQPtHAPgtAPzBqvrze9GZ4JcGTPDXi+BvothzZ8Vggn/AkVWXHqkleUzwzrrZRbH3iwMT/NKACf56ELywZP4hIvN3bPz6Lfu5sbC9My4C+8iieWmtHEf6f7Skvxj2Y4JfGjDBX12C71jv/I5V5h8AwHcswX9gY9YvsimOcX7QNrb+E0vwv7d/P7c+/qszzY3DBL80YIK/mgTfsw2kP7Af02D6trVnbrNav7bYtxaN+XxpG2Z/bT+fWj//9GCCXxowwV8dgu/ZKJh71o75DiL4tzhenUFwbCNuHMH/3to2L218/fxkzwS/NGCCvxoEv2oJ/ScA8GOr3h8ji2aeZFmMq48xsmieWhX/CwD4uW2cnS8lAhP80oAJ/vISfBcpdmO//NAS/I9suCN3QmLMAmXDK39pCf7XtmHWKfrh1Ntigl8aMMFfToJfsQ2lP7GfD23qgHuW9BmMebFtSf1rG23zc6vqP5natmGCXxowwV8egpcoIuZt6607S+ataNrd6/1sM6ZF/LnLrEf/C/txit5F3tQnOmOCXxowwV8OgheW1I398jcA8D0bx37XfkI7RgOzO2NGRJ8/ZRX9tiX33wDAz6yN82XtQ8YEvzTgXovLjRWr2E1UzPcB4C8swX9rqnS8zPGMaVBPktKmg76PQm3v2V7QH1sb55kdwISxhGAFv7wKftX67H8PAH9pOyi5wTTCqBiq2JnYGfMgeP6iin6MBiUxHab+DQD+P+vVn/ilWMEvDVjBLx82LIl/aP31v7fWzKPokeo5XyQGo/IsRX6EgqVtI7Ret436D2270GuW5J/ZkacYSwJW8Mul4Dcsmf8tAPydTQT2wPY+Jcely+Njz51x5rAK3j3O8ed6xyp6EzP/362a/xVok9WSFfwygBX8cmAzt190Tuh/Z332H+fT6QOMiR34AWYsCo6g3QMYVfS3Qefi4zXUqe6u7RW7ZVMjMC4QrOAvXsHfsSGPPwWRe+3fRjnZ64+HiZ1xnnDPdf3jfWgtmj+Chn8FgP/XhlZuz3SUrODPFKzgLw43rPL5nlXsPwUNH03MGcPEzrgI+OeullE37McFAty0ETcf20yWrOYvAKzgL0bB37Ahj39vPx/YRtTV6v6Z0BlLjPjzfuLVPMB/s5//MRXJs4I/U7CCP1/0rMIxqv1/tuT+gyixMxiXF+Z5ftdG2bgBZTZtR6knZ5aWmDERrODPT8EbcjcWzD9aYv++Jfu4JcPKnXGZUP/cH9sG199YJf9/2b/jnaNYwZ8pWMEvHm3bE/A7ltj/yWZ+vHXVT5zBsALmW2h4yFXrz5tImxfnOhj4NQQr+MUq+Jbt4m3i2v+97ZH6ejxChhU74yoh+l4d2vQGxo//P21em0/tQOEFWMGfKVjBLw63rQ9pyP0frHp/fFVPlsGYAhvWprxlLctNO+0z22mKccZgBb8YBX/bhj6ahtR/Z9MO3A+36zqOzLh9BuMywUdXVh70FzbH/H8FgP8bAP4lT0PMCv5MwQr+bNG2se0m3cB/sOT+0VSZHxmM64X7Nlx43fr0Kzbv/BP25c8OTPBnB5E3Jmn9H6wl82NryYTk7hUHSTnAYFwHhM971/bc3rD9QF6z3vwf+M04GzDBnw3MA/q+tWT+2XZiah46j6T4YDCuJCZbkCs2EMGp+Zb15//EmSlPDyb402PNhj3+bzYE8sNoCKSoYXL24BnXAqTNSVcefCOI/toSvbFv/g9r2Rzz8zE/mOBPh7vIb/9fbeclck01p/NlMCg0SpNd4pYNJe7aGSt2eMDZEpYxPJjg54fx1/8KAP5LniisGM6suJ5epWjQ+YAcPCgHgxGiYPg8uMZF2BTvSMt2CuzZTKumhvyvdhQpxoxggp8dLlLGhEH+R9uB6d18K1iMeGLX1z5Ui8GYCCFwu9SKbXztICX/M46wmR1M8LOhZcn8761y/+to56VAuTO5MxhRmPfETi/cmoov/5a1Pl2Kg/9mO0WlfEGnAxP89FixNoyJlPlfAOB/snmvS/jRlpjcGYypEH1HPNEnduzXjjVw2nbml7XJyhgBmOCnQ9fmbP+pVe4/CcjdKRGtUXw7kzuDMRWC90f44WCRon9g3701S/L/j42VH/EFbgYT/GSs2Tjdf7Ke+18FMe4Vtc7EzmDMDCyIzN9+iMCA5I0lmiHe+ozDKJvBBN+Mnm3R/0fbgemHlNwDr51j2hmMU0KXkcVVb/6OTd7Xs9xlrJvf8gAi9WCCr8ea7Z36T9Zz/4s8j7UAFNfOXjuDsRBUkvFZ30bnAsu8iwoApFX0n7CSj4MJPo5VG6b1TyDyHqo/tIMUWCDlDqzcGYwzBcrX5HnedYoq5t20HaI6dkHTGPs7OxYsA4EJvooVOwLNP1rl/qOA3B2xK0XqkWy/MxhnAvw+5danBpCymFbaNbfsuzm0S46skufoGgQm+BBda8uYbJD/2WaEDMndELvSIJxRKJjYGYwzBX6f3N+ZApCiIPqS5G/YoIcEEf0n9u9rD2CCD2Cqe++gOPcf2YE7CuTkbj9Bi/8FHzWDcdXh2ryU/ZZRJX9ifXnT0/VzDqEswARfoG3J/ac2K+Rf2Rb7Aki5c4Mqg3FBcO8hVJS8i65JLLFrS/LXPq0BE3xhsjy2oy/9J9tCf8/P9cpdlR0xGAzGxUAboSUsyQdK/p59d09sZI3x4r+67nVsJniAh1ax/2er4MuxU51iMP4fcKw7g3HhcDXoLCuiJEMlf9++wwYHVs1vXeebdt0J/o4l9/9ke8kV6Qe854eVO1szDMZSwJG8t2vAhVAK1ON11xL8f80H876muM4Ev5E3zmj9v9tG1df8HPP8qKwgeCDkzh48g7EEsFFsRsmb0aFkgg/pNftOg+0A9S/Xdfi/60rwa7bl3WWFfNvPCTx3YOXOYCwrvJK3bWMmjLKwaxL7Tpt3e8cq+Z9fx96u15HgE9tL9b/YiJl3/RyOlmEwLhmsko9H17xr33Gwja+/tA2w1wbXjeDbNjPkP9gcM98JrkHeS1UxuTMYlw35eyuKT0nwXfuOm2RkL62C/+w6hU9eN4J/3Q6Q/c8BueOqHm1QrY4yw2AwlgW006GyqQ3Kd9eN8frPtoer+XxxXe5f60p6zP6UfHIiYSNmfmzHUP2RbWQtkVfx1AUdMIPBOBsom2fS5q4psGHf+UMbNnloI2t00M52BbVc68q2IebtLi5fjLhtwyEdud8LlnPWjELqnZU7g7H8cO+pH5fBkLu1a0Bg0r5r3/0ta9n8DLQuwievcE6pq6vg89PKb/CKbWwx2SH/znZsssshYqfZIbnTKoNxORAMpqZRVI0uOkOFo0IZDjjynryGQTX3/NXBVffguzbHzN/aVAQf2lzvtvXd9lJVqmyNd+AskQzG5YEm728eHy+LmDkhHXuvWg4wETXfWKL//Cpnn7zqBH8HtOnVpv+xGDRbrBWTrcTPQyKzOJGzgmcwLgfo++uVfFa8w17BO5LXH+Q1ep178SalwZOreqevLsELWAWtv1tEzYh/VzSy2ichL+1tT1WqLine truncated

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68">
<defs>
<linearGradient id="ap-idea" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f3212c"/>
<stop offset="1" stop-color="#aa071a"/>
</linearGradient>
</defs>
<g fill="url(#ap-idea)" transform="translate(12 10)">
<path d="M22 2.2A12.8 12.8 0 0 0 9.2 15c0 5.1 2.9 9.5 7.1 11.6v3.8a2.4 2.4 0 0 0 2.4 2.4h6.6a2.4 2.4 0 0 0 2.4-2.4v-3.8A12.8 12.8 0 0 0 34.8 15 12.8 12.8 0 0 0 22 2.2Zm3.3 26.8h-6.6v-2.1h6.6Zm-3.3-26.8A10.4 10.4 0 0 1 32.4 15a10.4 10.4 0 0 1-6.3 9.5 2.4 2.4 0 0 0-1.3 2.1v.3h-5.6v-.3a2.4 2.4 0 0 0-1.3-2.1A10.4 10.4 0 0 1 11.6 15 10.4 10.4 0 0 1 22 2.2Z"/>
<path d="M17.6 39.2a2 2 0 0 0 2 2h4.8a2 2 0 0 0 2-2v-1.4h-8.8Zm-1.8-3.8h12.4a1.6 1.6 0 1 0 0-3.2H15.8a1.6 1.6 0 1 0 0 3.2Z"/>
<path d="M8.4 22.6a1.6 1.6 0 0 0 0-3.2H3.8a1.6 1.6 0 1 0 0 3.2Zm31.8 0a1.6 1.6 0 0 0 0-3.2h-4.6a1.6 1.6 0 1 0 0 3.2ZM7.2 10.8a1.6 1.6 0 0 0 2.3-2.3L6.2 5.3A1.6 1.6 0 0 0 4 7.6Zm29.3-2.3a1.6 1.6 0 0 0 2.3 2.3l3.2-3.2A1.6 1.6 0 1 0 39.7 5.3Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Loaded 100 of 3294 files, more files were not shown because too many files have changed in this diff. Show more