Compare commits

..
1687 Commits
Author SHA1 Message Date
gozeros 19d3cb96dc sync update from LAN repo (source master-yx @ cf505220) 2026-10-21 15:11:11 +08:00
董宏凯 e243ec3a46 代码提交 2026-09-09 22:46:35 +08:00
董宏凯andCursor f4c7d41a60 fix: accept vLLM thinking in reasoning or reasoning_content
Newer vLLM parsers put CoT in reasoning; older builds and DeepSeek still use reasoning_content. Canonicalize to the UI field and echo only the original wire field so other ChatOpenAI models stay unchanged. Point qwen3.6 at VllmChatModel.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 22:46:08 +08:00
董宏凯andCursor d765afe2ac feat(turbo): add direct LLM chat mode with runtime toggle
Add 极速 mode that streams via /api/direct-chat without the lead agent stack,
plus VITE_TURBO_MODE_VISIBLE in runtime-config.js (default off). Also disable
global memory, tighten Qwen/vLLM thinking control for flash mode, and force
thinking off on the direct-chat endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 21:58:36 +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
xuhdandClaude Sonnet 4.6 c714351819 feat: 我的笔记列表与详情页 UI 重构(Gemini 风格)
- StudioNotebooksPage: 卡片改为灰底无边框圆角网格,hover 显示三点菜单,支持重命名(PATCH)和删除(DELETE)
- StudioNotebookDetailPage: 详情页背景改为主题色渐变,内容垂直居中,输入框内联布局,模型选择器移至右侧,历史对话限显 5 条可展开/滚动
- api.ts: 新增 deleteSearchSpace、renameSearchSpace、uploadSourceText 接口
- 来源 Dialog:新增「复制的文字」面板,支持粘贴文本上传

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:36:42 +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
xuhd 1a1d22d6ee 添加环境变量我的笔记的前端地址 2026-06-01 16:59:09 +08:00
xuhdandClaude Sonnet 4.6 ab9034633a feat: StudioNotebookDetailPage 添加「我的笔记」跳转
- 聊天视图顶栏和列表视图右上角均新增「我的笔记」链接
- 点击跳转目标系统 callback 页面并携带 token 和 redirect 参数
- 来源弹窗仅保留「上传文件」和「复制的文字」两个选项
- VITE_AIPI_BACKENP_URL 用于 Studio API 调用,VITE_AIPI_FRONTEND_URL 用于页面跳转
- useStudioNotebooks 新增 token 返回值供页面直接使用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:55:39 +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
xuhdandClaude Sonnet 4.6 7f0a0d92a3 feat: 素材来源新增「我的笔记」选项
- materialSource 枚举新增 notebook 值
- 选择「我的笔记」时展开笔记本 + 文档多选选择器
- 切换其他来源自动清空笔记选择
- 只有 notebook 模式才向后端传笔记参数

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:30:16 +08:00
xuhdandClaude Sonnet 4.6 e4983be9d7 fix: 解决 merge 冲突,恢复笔记来源字段
合并 master 时冲突导致 notebook_search_space_id / notebook_source_ids
被覆盖,现补回;同时兼容 master 新增的 material_source / model_name 字段。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:22:55 +08:00
xuhd e30b44dae1 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-06-01 14:21:20 +08:00
ZyGuo 0922c8299f feat: iframe page 2026-06-01 14:16:13 +08:00
董宏凯 8adb4d5cef 一键部署 2026-06-01 13:53:57 +08:00
董宏凯 5198f6c4df Merge branch 'dhk-1' into 'master'
圆桌草稿持久化:localStorage 迁至后端 MySQL

See merge request knowledge-base-bj/deerflow-server!73
2026-06-01 04:59:24 +00:00
董宏凯andClaude Opus 4.8 5d0bafd651 AI写作:新增素材来源选项(知识库/技能检索)
写作表单新增「素材来源」下拉:知识库(默认 web/intranet 检索)/
技能检索(用已启用技能编排检索)。用户选择后素材收集专家首次检索
据此决定检索方式。

- state 新增 material_source 通道(缺省 knowledge_base,兼容老 session)
- researcher 首次检索(无干预)时按来源决定 is_skill_search;
  PAUSE-1「启用技能检索」按钮仍独立有效
- 前端 AIWritingRequest.materialSource + 表单下拉 + hook 提交 material_source

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:58:50 +08:00
xuhdandClaude Sonnet 4.6 b749c78efa feat: AI写作集成笔记来源,支持多文档多选检索
- 前端 AIWritingForm 新增笔记来源选择器(笔记本单选 + 文档多选 checkbox)
- AIWritingRequest 类型新增 noteSearchSpaceId / noteSourceIds 字段
- AIMaterial.sourceType 新增 notebook 类型
- useAIWritingStream startWriting 时自动获取 notebook_token 并携带笔记参数
- 后端 AIWritingState 新增 notebook_search_space_id / notebook_source_ids 字段
- researcher 节点新增 _search_notebook,解析 API 返回的 documents/chunks,
  每个 chunk 作为独立素材(source_type=notebook)混入素材包

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 11:47:37 +08:00
xuhd 22b93e32eb Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-06-01 10:49:53 +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
fengxiangdebaozi 169346cdbd 样式修改 2026-06-01 10:06:28 +08:00
董宏凯 ea50540bd5 样式修改 2026-05-31 18:23:34 +08:00
董宏凯 8e909ef1c1 ui规范 2026-05-31 18:11:17 +08:00
董宏凯 7ad42ccf16 ui规范 2026-05-31 16:48:43 +08:00
董宏凯andCursor 633a350bfc AI写作:技能检索进度反馈与超时兜底
检索等待时 SkillStepsCard 显示 spinner;单技能检索 25s 超时避免卡死;前后端增加 skill-timing 调试日志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:42:51 +08:00
董宏凯andCursor 23d6f9efc2 圆桌 Step1:支持多澄清实时展示与合并提交
流式解析 ask_clarification 逐题渲染澄清卡;草稿恢复 selectedAgents 修复参与角色;Step3 隐藏左侧栏让报告区全宽。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:23:43 +08:00
董宏凯andCursor 838cae0d2d 圆桌草稿持久化:localStorage 迁至后端 MySQL
新增 /api/roundtable-drafts 草稿 CRUD 与会话级推荐历史;前端 useDraftPersistence 改走 api/drafts,删除本地 drafts.ts。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 00:23:25 +08:00
董宏凯 24e92c437d ai写作bug修改 2026-05-29 22:16:19 +08:00
董宏凯 7d45c89d72 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-29 20:56:43 +08:00
董宏凯 8e931c5acc 代码 2026-05-29 20:56:07 +08:00
fengxiangdebaozi be37779472 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-29 20:17:07 +08:00
fengxiangdebaozi 27cfd53c06 样式修改 2026-05-29 20:16:41 +08:00
吉鑫升 00e74a5e45 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	offline-backend-20260512/backend/app/gateway/services.py
2026-05-29 18:36:29 +08:00
fengxiangdebaozi c890ae5a15 样式修改 2026-05-29 18:17:08 +08:00
fengxiangdebaozi 786ddaa38f 代码修改 2026-05-29 18:07:45 +08:00
董宏凯 d162e5533f Merge branch 'zhujingyi' into 'master'
'feat:智能体暗色模式样式修改'

See merge request knowledge-base-bj/deerflow-server!72
2026-05-29 07:30:17 +00:00
董宏凯 e68b726f9a Merge branch 'dhk-1' into 'master'
圆桌 Step2:固定协调员单例、优化调度性能、修正研讨中状态

See merge request knowledge-base-bj/deerflow-server!71
2026-05-29 07:29:17 +00:00
fengxiangdebaozi 8ed3b0eb91 路由样式修改 2026-05-29 15:26:41 +08:00
fengxiangdebaozi a9332c0ab0 技能联调 2026-05-29 15:22:28 +08:00
xuhd 423abf4fde 上传文件添加 2026-05-29 15:15:29 +08:00
fengxiangdebaozi 07ad87365f 作家写作 2026-05-29 11:42:30 +08:00
吉鑫升andClaude Opus 4.7 2c136c1fde 参考文献模式合并为单一 referenceBatch 并统一连续编号
将同一轮检索结果合并为一个 referenceBatch,由系统统一从 [1] 连续编号,避免每个检索词各自重新编号;提示词引导优先并行检索后再作答。智能体列表接口并发加载技能等轻量信息以渲染卡片徽章,并新增参考文献相关测试。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:31:31 +08:00
fengxiangdebaozi 1a122eb7fd 代码修改 2026-05-29 11:19:58 +08:00
xuhdandClaude Sonnet 4.6 4a63399363 feat: 新增工作室展示开关 VITE_STUDIO_ENABLED
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:47:50 +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
zjy 8aa52e7e3c 'feat:智能体暗色模式样式修改' 2026-05-29 09:51:44 +08:00
董宏凯andCursor de597b602a 圆桌 Step2:固定协调员单例、优化调度性能、修正研讨中状态
将 roundtable-coordinator 改为内置单例避免重复创建;legacy sync 加 TTL 减少 runs/stream 延迟;前端仅当前发言者显示研讨中。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 23:23:40 +08:00
董宏凯 2371364911 代码优化 2026-05-28 22:35:05 +08:00
董宏凯 214e794451 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 21:22:54 +08:00
董宏凯 ab902f948a 代码合并 2026-05-28 21:22:49 +08:00
fengxiangdebaozi ce1e36f4fd 样式修改 2026-05-28 21:15:45 +08:00
fengxiangdebaozi 72eb5204ff Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 20:18:43 +08:00
xuhd e4da5bec8d 22222 2026-05-28 18:56:11 +08:00
xuhd 6102963624 000 2026-05-28 18:43:20 +08:00
xuhd b152cb43fc 222 2026-05-28 18:37:48 +08:00
xuhdandClaude Sonnet 4.6 ebe1678b30 Merge origin/master into xuhd
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 18:31:16 +08:00
fengxiangdebaozi 8a0962681a 代码修改 2026-05-28 18:29:50 +08:00
fengxiangdebaozi 9778f9521e 接口地址修改 2026-05-28 18:29:50 +08:00
fengxiangdebaozi 4a3cfb9dfa 任务理解修改 2026-05-28 18:29:50 +08:00
董宏凯 1cfa93e855 点击后打开沙箱 2026-05-28 18:29:50 +08:00
董宏凯 4fab13c728 文件浏览 2026-05-28 18:29:50 +08:00
fengxiangdebaozi a3c69dab07 代码修改 2026-05-28 18:29:50 +08:00
董宏凯 e3dd430e68 按钮修改 2026-05-28 18:29:44 +08:00
董宏凯 d018ecd805 速度优化 2026-05-28 18:29:44 +08:00
fengxiangdebaozi a4b7ab5e9d 代码修改 2026-05-28 18:29:37 +08:00
董宏凯 ef1481be63 智能体查询优化 2026-05-28 18:29:37 +08:00
董宏凯 3a2b5d5ccd 代码修改 2026-05-28 18:29:32 +08:00
吉鑫升andClaude Opus 4.7 78f6e37f76 新增智能体收藏、定时任务必须生成MD校验、问答时间显示及账号绑定的常用提示词
- 智能体支持从广场收藏到「我的」(新增 agent_favorites 表与收藏/取消收藏接口)
- 定时任务新增「必须生成Markdown文件」开关:注入提示词并校验生成的 .md 文件且正文超过100字,否则记为失败(存于 execution_context,无需迁移)
- 聊天问答展示时间(年月日 时分):问题在右下角、回答在左下角
- 常用提示词改为与账号绑定(新增 user_custom_prompts 表与 /api/user-prompts 接口),仅在首页输入框上方以居中标签 + 轮播展示,齿轮置于最右侧管理

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:29:32 +08:00
xuhdandClaude Sonnet 4.6 1ea521abab 前端优化:chat-box、消息列表、rehype 及 Studio 相关交互改进
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 18:29:10 +08:00
xuhd b8a97f2138 xxx 2026-05-28 18:29:10 +08:00
吉鑫升andClaude Opus 4.7 dac5ff38db 技能展示配置持久化并适配引用来源,新增技能文件预览组件
- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配
- 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示
- 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:29:10 +08:00
董宏凯 ac98d2f585 ai写作默认模型 2026-05-28 18:29:10 +08:00
董宏凯 37a9cca82d 代码修改 2026-05-28 18:29:10 +08:00
董宏凯 8c53594df7 多智能体会商沙箱 2026-05-28 18:29:10 +08:00
hkdongandClaude Opus 4.7 eec7567ac6 修复总控智能体共识后仍被错判为继续派活的死循环
_find_dispatch_message 原实现会一路向前翻历史,直到撞到任意一条带
tool_calls 的 AIMessage,导致本轮 LLM 即使共识(tool_calls=[]),也会
被上一轮的派活 AI 翻出来再用一次,前端因此把同样的 task 又发给子智能体,
形成"嘴上共识、手上派活"的死循环。

改成只看最新一条 AIMessage(跳过末尾 skill_stop / clarification ack
ToolMessage 即可),让 tool_calls 是否为空作为本轮的唯一权威信号。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 18:29:10 +08:00
fengxiangdebaozi 920fde9cd0 代码提交 2026-05-28 18:29:10 +08:00
董宏凯 742bbdf3ac 点击确认 2026-05-28 18:29:10 +08:00
董宏凯 4da8a1301e 报告沙箱 2026-05-28 18:29:10 +08:00
董宏凯 3bd5cfab9d 代码修改 2026-05-28 18:29:10 +08:00
董宏凯 4f8b0f1c67 ai写作修改 2026-05-28 18:29:10 +08:00
fengxiangdebaozi c59819f6dc 开发文档 2026-05-28 18:29:10 +08:00
fengxiangdebaozi 96d0f4ae73 接口修改 2026-05-28 18:29:03 +08:00
吉鑫升 a35aeee6a2 代码提交 2026-05-28 18:28:57 +08:00
吉鑫升 2688cda6b5 忽略RAG调试日志 2026-05-28 18:28:57 +08:00
吉鑫升 f668133297 实现RAG参考文献显示模式 2026-05-28 18:28:57 +08:00
吉鑫升andClaude Opus 4.7 89bd07be92 管理员智能体/技能管理 + 跨标签登录同步 + 上游响应异常重试
- 「设置和更多」新增「智能体管理」「技能管理」入口,与定时任务管理同级;
  独立的 AdminAgentsPage/AdminSkillsPage 复用 AgentCard/SkillRow,
  管理员可查看并编辑/删除所有用户的智能体与技能。
- 后端 /api/agents、/api/skills 增加 ?admin=true 越权列表;agents 端
  放宽 _get_editable_or_raise 让管理员可编辑/删除任意智能体;新增
  list_all/update_any/delete_any(agents)与 list_all(skills)。
- 新增 AuthSyncWatcher:监听 localStorage["deerflow.auth"] 的 storage
  事件,user_id 变化时自动 reload,修复多标签切换登录后老页面点击
  「新对话」偶发 404 的问题。
- LLMErrorHandlingMiddleware 将 bad_response_status_code / JSON
  decode 等上游网关乱码识别为可重试的 upstream 类故障。
- 侧边栏「任务管理」改名为「定时任务」。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:49 +08:00
吉鑫升andClaude Opus 4.7 a5fb806028 轮换 LLM 凭证与默认模型(DeepSeek → zai-org/GLM-5-FP8)
- backend/config.yaml、config.yaml 把默认模型从 deepseek-chat 切到
  zai-org/GLM-5-FP8(modal endpoint),并轮换对应 API key / Bearer
- .env 同步更新

注意:这些文件包含真实凭证,应用部署时务必通过 .env 或私有 config
覆盖,不要把仓库里的版本直接用于生产。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:49 +08:00
吉鑫升andClaude Opus 4.7 83714f628b 私密/公开互斥兜底 + 智能体搜索按显示后名称匹配 + 多处小修
- 后端 update_agent / set_agent_private 增加兜底:is_private=true 时强制
  published=false,杜绝同时落在公开+私密两个广场的灰色态
- agent-card 编辑弹窗暴露 editIsPrivate 开关,与公开发布开关在前端互斥校验
- agent-gallery 智能体模糊搜索改为前端针对 desensitize() 后的名字比对,
  解决脱敏显示与原始名字不一致导致看似搜索失效的问题
- useUpdateAgent 在 onSuccess 立刻把新数据 patch 进所有 ["agents"] 列表
  缓存,卡片在后台 refetch 完成前先行 re-render,避免「切换私密/发布后
  好像没反应」的体感延迟
- useGlobalShortcuts 对 event.key 为 undefined(IME 合成 / 扩展注入)做
  容错,否则会抛 TypeError 把后续所有快捷键吃掉

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:49 +08:00
吉鑫升andClaude Opus 4.7 ae3e2fb4e8 默认主题改为浅色 + 管理员可配置全局默认主题
- system_settings 新增 AppearanceSettings(default_theme),默认为 light,
  对应 GET /api/system-settings/appearance(任何人可读)+ PUT(admin 可写)
- App.tsx 把 NextThemesProvider 的 defaultTheme 从 dark 改为 light,并把
  QueryClientProvider 提到 ThemeProvider 外层以便后者使用 useQuery
- ThemeContext 新增 localStorage 标志 strategy-theme-user-set:用户调用
  toggleTheme / setTheme 时打标;每次启动若标志未设,则跟随 admin 配置的
  default_theme 同步主题,admin 改动用户刷新即生效;标志一旦存在永远尊重
  用户个人选择
- legacy 'theme' 键迁移视为用户显式选择,避免老用户被新默认覆盖
- frontend system-settings api/hook 增 appearance 拉取与更新

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:49 +08:00
吉鑫升 650102cedb feat: improve citation display and skill persistence 2026-05-28 18:28:45 +08:00
吉鑫升andClaude Opus 4.7 61babbdf7d 技能展示样本运行器 + 参考文献来源过滤调整
- skills 路由新增 _run_skill_python_sample,按本地 Python 候选实际执行
  技能脚本,作为展示适配器的真实样本来源
- skill-display 运行时与适配器调整,参考文献收集放宽起始消息约束

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:41 +08:00
吉鑫升andClaude Opus 4.7 3734b6a05f 新增智能体私密广场(is_private + 管理员密码门控)
- agents 表新增 is_private 列与迁移 20260526_01;列表 filter 拆分为公开
  广场用的 _listing_filter(剔除私密)与单条访问用的 _visible_filter(保留可达)
- system_settings 增 PrivateSquareSettings(enabled + password_hash),
  对应 GET/PUT /api/system-settings/private-square 由管理员管理
- /api/agents 接口:列表/创建/更新新增 is_private 字段;新增
  POST /api/agents/private-square/list(密码校验后返回私密智能体)与
  PUT /api/agents/{id}/private(管理员跨用户搬动智能体)
- 前端「智能体」页签新增「私密广场」入口(密码校验 + sessionStorage 缓存 +
  锁定按钮),卡片增加紫色私密徽章与盾牌切换按钮(owner 自助 / 管理员跨用户),
  「广场」与首页推荐候选均剔除私密智能体
- 新增管理员配置页 /settings/private-square 与侧边栏入口

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:41 +08:00
吉鑫升andClaude Opus 4.7 0256d527f0 技能展示适配持久化 + 参考文献收集与新对话流式修复
- 后端新增 skill_display_adapters 表,持久化每个技能的 display 配置与 sample
  payload;/api/skills 列表、详情、设置接口同步读写
- 新增 /api/skills/{name}/display-sample 端点,编辑器优先加载库中样例
- 前端展示适配面板改为从后端加载持久样例,无样例时提示"自动识别"
- 参考文献收集修复:仅在 bash/shell/python 等执行类工具上从参数推断技能
  名,避免 read_file / web_fetch 误匹配;引用列表纳入最新一条助手消息
- ChatBox 改用「最新 human → 最新 ai」窗口收集引用,并仅保留 citation
  模式
- 修复「新对话」点击后已发消息消失:useThreadStream 只在 threadId 切回
  空值时重置/中止 SSE,避免 onCreated 分配 id 时把刚开始的流打断;
  ChatPage 在 landing 状态隐藏 MessageList,移除 KeyedChatPage remount
- 技能详情对话框:所有者可查看源码与文件 tab(不再要求管理员)
- LocalSandbox:Windows PowerShell 5 路径下把 && 重写为 ;,并以
  UTF-8 解码 subprocess 输出,避免中文乱码
- knowledge-search-v2 mock 样例改为中文固定测试数据

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:28:41 +08:00
吉鑫升 7adb93fb8e 修复「新对话」流式中无法重置 + 参考文献开关默认关闭并加对勾
- 流式回答过程中点「新对话」:URL 同路径导致 react-router navigate 静默 no-op,
  下游 hook 收不到事件;改为显式 navigate(..., {replace:true, state:{resetAt}})
  保证每次点击刷新 location.key,并让 useThreadChat 监听 location.key 触发重置
- useThreadStream 在 threadId 变化时 stop() 掉上一条 SSE,避免旧流继续派发事件
- 引入 KeyedChatPage 用 location.key 作 ChatRuntime key,真正的导航触发整树 remount,
  解决 useStream 内部 stream.values 残留导致欢迎页底下还能看到旧消息的过渡帧问题
- 「参考文献」开关默认关闭(local.ts),开启态在文字前额外多一个 ✓ 对勾
2026-05-28 18:28:33 +08:00
吉鑫升 97f24f17eb 新增 RAG 引用展示模式:支持技能结果按编号引用/列表/表格渲染
- 前端新增 skill-display 适配层(runtime/display-adapter)与可视化适配面板,
  支持从技能返回 JSON 自动识别字段并配置 citation/list/table/card 展示
- 正文 [[N]] 编号渲染为可点击徽章,点击不跳 URL 而是定位到右侧参考文献面板;
  参考卡片样式重做(圆形编号、悬停高亮、过滤掉 "result" 占位类型)
- bash 工具调用按命令中的 skills/{public,custom}/<name>/ 路径反查展示适配,
  解决调用名为 "bash" 时无法匹配到具体技能的问题
- 工具结果 JSON 解析放宽:能从 stderr/exit code 噪声尾巴里抢救出 JSON 主体
- 展示适配自动识别接入 LLM:读 SKILL.md 让模型推断真实返回结构,失败回退 mock
- 展示适配面板修复保存回显:保存后表单跟随服务端最新值刷新,样例文本不再被抹掉
- 设置页加 "RAG 引用模式" 开关(默认开),关联 lead-agent prompt 注入引用规则
2026-05-28 18:28:33 +08:00
吉鑫升andClaude Opus 4.7 96b0bae6ca 技能详情对话框新增文件浏览面板:支持预览、编辑、下载、复制路径
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:27:12 +08:00
董宏凯 3632b0d726 代码合并 2026-05-28 17:49:18 +08:00
吉鑫升 6792b4f4c4 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	frontend-web/src/components/workspace/chats/chat-box.tsx
#	offline-backend-20260512/backend/app/gateway/routers/agents.py
2026-05-28 17:29:36 +08:00
吉鑫升andClaude Opus 4.7 10e35055e0 新增智能体收藏、定时任务必须生成MD校验、问答时间显示及账号绑定的常用提示词
- 智能体支持从广场收藏到「我的」(新增 agent_favorites 表与收藏/取消收藏接口)
- 定时任务新增「必须生成Markdown文件」开关:注入提示词并校验生成的 .md 文件且正文超过100字,否则记为失败(存于 execution_context,无需迁移)
- 聊天问答展示时间(年月日 时分):问题在右下角、回答在左下角
- 常用提示词改为与账号绑定(新增 user_custom_prompts 表与 /api/user-prompts 接口),仅在首页输入框上方以居中标签 + 轮播展示,齿轮置于最右侧管理

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:16:15 +08:00
董宏凯 7548620683 样式修改 2026-05-28 17:14:01 +08:00
董宏凯 d5e12e3506 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!70
2026-05-28 07:58:17 +00:00
fengxiangdebaozi 23b9d401b7 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 15:56:58 +08:00
董宏凯 44894765b6 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!69
2026-05-28 07:54:20 +00:00
fengxiangdebaozi c2568f4be9 代码修改 2026-05-28 15:50:31 +08:00
董宏凯 90adb9dfa3 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 15:48:15 +08:00
董宏凯 d2f3eba434 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
# Conflicts:
#	frontend-web/src/components/workspace/chats/chat-box.tsx
#	frontend-web/src/pages/StrategyRoutes.tsx
#	offline-backend-20260512/backend/app/gateway/routers/agents.py
2026-05-28 15:36:11 +08:00
fengxiangdebaozi 2fe058e0ee 接口地址修改 2026-05-28 15:24:00 +08:00
吉鑫升andClaude Opus 4.7 4410acaee8 技能展示配置持久化并适配引用来源,新增技能文件预览组件
- 后端将技能展示适配配置存入数据库,引用批次按持久化映射进行适配
- 前端新增技能文件预览弹窗与文件树组件,完善技能来源展示
- 修正 .gitignore 中宽泛的 skills/ 规则误忽略前端组件目录的问题

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:16:56 +08:00
fengxiangdebaozi d03f5fbbc0 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 15:10:25 +08:00
fengxiangdebaozi ae1c2f95a7 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-28 14:25:54 +08:00
fengxiangdebaozi 381547ec78 任务理解修改 2026-05-28 14:25:44 +08:00
董宏凯 f4672ea6b8 点击后打开沙箱 2026-05-28 13:56:05 +08:00
董宏凯 51a270a2f7 文件浏览 2026-05-28 13:52:25 +08:00
fengxiangdebaozi f3c8f2e1ab 代码修改 2026-05-28 11:45:25 +08:00
xuhd e2b676caaa 修复 2026-05-28 10:00:40 +08:00
董宏凯 68ac49259c 按钮修改 2026-05-28 09:09:02 +08:00
董宏凯 d94c0aae4b 速度优化 2026-05-27 23:12:02 +08:00
董宏凯 047e700d09 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 22:23:44 +08:00
董宏凯 9ccc473982 智能体查询优化 2026-05-27 22:23:37 +08:00
董宏凯 4142029f48 代码修改 2026-05-27 21:25:15 +08:00
fengxiangdebaozi dbf2cc8d80 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 20:57:19 +08:00
吉鑫升 f200972c00 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-05-27 20:28:38 +08:00
董宏凯 3033597f02 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!68
2026-05-27 12:28:25 +00:00
董宏凯 a16dd889cf Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 20:28:05 +08:00
董宏凯 dab6ecbf9a ai写作默认模型 2026-05-27 20:28:00 +08:00
董宏凯 8bdbf6ab15 代码修改 2026-05-27 19:43:28 +08:00
fengxiangdebaozi 8f75e12871 代码修改 2026-05-27 19:43:15 +08:00
xuhd 36311e6cef 999 2026-05-27 19:30:54 +08:00
fengxiangdebaozi 8f5af46ec2 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 19:15:19 +08:00
fengxiangdebaozi aeebf53739 api 2026-05-27 19:15:09 +08:00
吉鑫升 1d81ee9ed0 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji 2026-05-27 19:12:27 +08:00
董宏凯 e1c9d24aa4 Merge branch 'dhk-1' into 'master'
多智能体会商沙箱

See merge request knowledge-base-bj/deerflow-server!67
2026-05-27 11:12:07 +00:00
董宏凯 b7da062a4c 多智能体会商沙箱 2026-05-27 19:11:36 +08:00
吉鑫升 6fd6d564eb Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	frontend-web/src/components/workspace/messages/message-list-item.tsx
#	frontend-web/src/components/workspace/messages/message-list.tsx
#	offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py
#	offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py
2026-05-27 19:10:27 +08:00
吉鑫升 106f1d1794 代码提交 2026-05-27 19:02:19 +08:00
董宏凯 3809d7f3e7 Merge branch 'tjl' into 'master'
修复总控智能体共识后仍被错判为继续派活的死循环

See merge request knowledge-base-bj/deerflow-server!66
2026-05-27 10:08:50 +00:00
hkdongandClaude Opus 4.7 c769dd20f7 修复总控智能体共识后仍被错判为继续派活的死循环
_find_dispatch_message 原实现会一路向前翻历史,直到撞到任意一条带
tool_calls 的 AIMessage,导致本轮 LLM 即使共识(tool_calls=[]),也会
被上一轮的派活 AI 翻出来再用一次,前端因此把同样的 task 又发给子智能体,
形成"嘴上共识、手上派活"的死循环。

改成只看最新一条 AIMessage(跳过末尾 skill_stop / clarification ack
ToolMessage 即可),让 tool_calls 是否为空作为本轮的唯一权威信号。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:07:30 +08:00
董宏凯 445865e422 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!65
2026-05-27 09:48:13 +00:00
fengxiangdebaozi 24d4934c27 代码提交 2026-05-27 17:47:20 +08:00
吉鑫升 fe7cdba07e 忽略RAG调试日志 2026-05-27 17:44:02 +08:00
吉鑫升 c56db15a03 实现RAG参考文献显示模式 2026-05-27 17:40:10 +08:00
董宏凯 deb9993a64 点击确认 2026-05-27 17:25:10 +08:00
董宏凯 1ff8860b98 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 17:20:47 +08:00
董宏凯 198fc18c50 报告沙箱 2026-05-27 17:20:41 +08:00
董宏凯 621fdc9244 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!64
2026-05-27 08:02:37 +00:00
fengxiangdebaozi 9ece6213a9 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 16:02:12 +08:00
fengxiangdebaozi 35c313b689 开发文档 2026-05-27 16:02:07 +08:00
董宏凯 d6b88b842d Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 15:31:00 +08:00
董宏凯 ff90cac755 代码修改 2026-05-27 15:30:51 +08:00
董宏凯 8f93606c81 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-27 14:19:18 +08:00
董宏凯 95baa44a1c ai写作修改 2026-05-27 14:19:11 +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
xu hd 066d5032f9 Merge branch 'xuhd' into 'master'
修复一下

See merge request knowledge-base-bj/deerflow-server!63
2026-05-27 03:44:16 +00:00
xuhd 3f8a520624 修复一下 2026-05-27 11:40:41 +08:00
fengxiangdebaozi 6ab6695be7 接口修改 2026-05-27 11:37:07 +08:00
吉鑫升andClaude Opus 4.7 eda0ccffdb 管理员智能体/技能管理 + 跨标签登录同步 + 上游响应异常重试
- 「设置和更多」新增「智能体管理」「技能管理」入口,与定时任务管理同级;
  独立的 AdminAgentsPage/AdminSkillsPage 复用 AgentCard/SkillRow,
  管理员可查看并编辑/删除所有用户的智能体与技能。
- 后端 /api/agents、/api/skills 增加 ?admin=true 越权列表;agents 端
  放宽 _get_editable_or_raise 让管理员可编辑/删除任意智能体;新增
  list_all/update_any/delete_any(agents)与 list_all(skills)。
- 新增 AuthSyncWatcher:监听 localStorage["deerflow.auth"] 的 storage
  事件,user_id 变化时自动 reload,修复多标签切换登录后老页面点击
  「新对话」偶发 404 的问题。
- LLMErrorHandlingMiddleware 将 bad_response_status_code / JSON
  decode 等上游网关乱码识别为可重试的 upstream 类故障。
- 侧边栏「任务管理」改名为「定时任务」。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:13:29 +08:00
xu hd 1d399c3c21 Merge branch 'xuhd' into 'master'
feat: 笔记本 Studio 模块 — RAG 聊天、引用弹窗、侧边栏工作室

See merge request knowledge-base-bj/deerflow-server!62
2026-05-27 03:10:13 +00: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
董宏凯 9d81858ff4 Merge branch 'dhk-1' into 'master'
代码提交

See merge request knowledge-base-bj/deerflow-server!61
2026-05-27 01:16:50 +00:00
fengxiangdebaozi f791210e70 代码提交 2026-05-27 09:16:25 +08:00
xuhdandClaude Sonnet 4.6 794ff93fa9 chore: 更新 .env 配置(notebook API 地址、数据库连接)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:56:14 +08:00
xuhdandClaude Sonnet 4.6 ff8eb930f7 feat: 笔记本 Studio 模块 — RAG 聊天、引用弹窗、侧边栏工作室
- 新增 Studio 笔记本列表页与详情页(RAG 流式聊天、历史线程切换)
- 修复 streamSubgraphs: true 使 AI 流式消息正常展示
- 引用圆圈 [[cite:N]] 渲染 + 点击弹窗显示文档 chunk 内容
- 侧边栏「工作室」区域支持折叠/展开,笔记本过滤 is_owner=true
- 后端 lead_agent 支持 notebook_source_ids / notebook_token 参数
- 线程 metadata 改为 thread_type=notebook,侧边栏过滤同步

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:55:08 +08:00
董宏凯 d71a21ca8b Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!60
2026-05-26 15:25:32 +00:00
fengxiangdebaozi 2bd23dd374 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-26 23:21:13 +08:00
fengxiangdebaozi a501463ac7 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-26 23:20:58 +08:00
fengxiangdebaozi 9bd049923e 多智能体页面结构修改 2026-05-26 23:20:50 +08:00
董宏凯 67152f1c83 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-26 21:10:33 +08:00
董宏凯 8ca3fdec75 ai写作 2026-05-26 21:10:27 +08:00
fengxiangdebaozi 8e7fc54548 代码修改 2026-05-26 21:09:02 +08:00
吉鑫升 1642c3e694 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/contexts/ThemeContext.tsx
#	offline-backend-20260512/backend/config.yaml
2026-05-26 19:34:43 +08:00
董宏凯 d738c13e9e Merge branch 'dhk-1' into 'master'
智能体列表

See merge request knowledge-base-bj/deerflow-server!59
2026-05-26 11:32:35 +00:00
fengxiangdebaozi 45eb46c3f3 智能体列表 2026-05-26 19:31:59 +08:00
吉鑫升andClaude Opus 4.7 7b629d0697 轮换 LLM 凭证与默认模型(DeepSeek → zai-org/GLM-5-FP8)
- backend/config.yaml、config.yaml 把默认模型从 deepseek-chat 切到
  zai-org/GLM-5-FP8(modal endpoint),并轮换对应 API key / Bearer
- .env 同步更新

注意:这些文件包含真实凭证,应用部署时务必通过 .env 或私有 config
覆盖,不要把仓库里的版本直接用于生产。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:31:10 +08:00
吉鑫升andClaude Opus 4.7 89a1fa5042 私密/公开互斥兜底 + 智能体搜索按显示后名称匹配 + 多处小修
- 后端 update_agent / set_agent_private 增加兜底:is_private=true 时强制
  published=false,杜绝同时落在公开+私密两个广场的灰色态
- agent-card 编辑弹窗暴露 editIsPrivate 开关,与公开发布开关在前端互斥校验
- agent-gallery 智能体模糊搜索改为前端针对 desensitize() 后的名字比对,
  解决脱敏显示与原始名字不一致导致看似搜索失效的问题
- useUpdateAgent 在 onSuccess 立刻把新数据 patch 进所有 ["agents"] 列表
  缓存,卡片在后台 refetch 完成前先行 re-render,避免「切换私密/发布后
  好像没反应」的体感延迟
- useGlobalShortcuts 对 event.key 为 undefined(IME 合成 / 扩展注入)做
  容错,否则会抛 TypeError 把后续所有快捷键吃掉

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:30:00 +08:00
吉鑫升andClaude Opus 4.7 c9c59b761f 默认主题改为浅色 + 管理员可配置全局默认主题
- system_settings 新增 AppearanceSettings(default_theme),默认为 light,
  对应 GET /api/system-settings/appearance(任何人可读)+ PUT(admin 可写)
- App.tsx 把 NextThemesProvider 的 defaultTheme 从 dark 改为 light,并把
  QueryClientProvider 提到 ThemeProvider 外层以便后者使用 useQuery
- ThemeContext 新增 localStorage 标志 strategy-theme-user-set:用户调用
  toggleTheme / setTheme 时打标;每次启动若标志未设,则跟随 admin 配置的
  default_theme 同步主题,admin 改动用户刷新即生效;标志一旦存在永远尊重
  用户个人选择
- legacy 'theme' 键迁移视为用户显式选择,避免老用户被新默认覆盖
- frontend system-settings api/hook 增 appearance 拉取与更新

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:29:45 +08:00
吉鑫升 8b2f398015 feat: improve citation display and skill persistence 2026-05-26 19:27:03 +08:00
董宏凯 5c5bf7a60e Merge branch 'dhk-1' into 'master'
提示词修改

See merge request knowledge-base-bj/deerflow-server!58
2026-05-26 06:49:51 +00:00
fengxiangdebaozi 6cf4404243 提示词修改 2026-05-26 14:48:06 +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
吉鑫升 b671b49725 Merge remote-tracking branch 'origin/master' into ji 2026-05-26 14:25:02 +08:00
吉鑫升andClaude Opus 4.7 d3d7db1f34 技能展示样本运行器 + 参考文献来源过滤调整
- skills 路由新增 _run_skill_python_sample,按本地 Python 候选实际执行
  技能脚本,作为展示适配器的真实样本来源
- skill-display 运行时与适配器调整,参考文献收集放宽起始消息约束

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:24:47 +08:00
吉鑫升andClaude Opus 4.7 26a388e3bf 新增智能体私密广场(is_private + 管理员密码门控)
- agents 表新增 is_private 列与迁移 20260526_01;列表 filter 拆分为公开
  广场用的 _listing_filter(剔除私密)与单条访问用的 _visible_filter(保留可达)
- system_settings 增 PrivateSquareSettings(enabled + password_hash),
  对应 GET/PUT /api/system-settings/private-square 由管理员管理
- /api/agents 接口:列表/创建/更新新增 is_private 字段;新增
  POST /api/agents/private-square/list(密码校验后返回私密智能体)与
  PUT /api/agents/{id}/private(管理员跨用户搬动智能体)
- 前端「智能体」页签新增「私密广场」入口(密码校验 + sessionStorage 缓存 +
  锁定按钮),卡片增加紫色私密徽章与盾牌切换按钮(owner 自助 / 管理员跨用户),
  「广场」与首页推荐候选均剔除私密智能体
- 新增管理员配置页 /settings/private-square 与侧边栏入口

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:24:29 +08:00
董宏凯 6a9d8e903e Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!57
2026-05-26 06:19:16 +00:00
董宏凯 daba5c0599 代码修改 2026-05-26 14:18:00 +08:00
董宏凯 d1e6cd5a8f 多智能体协作修改 2026-05-26 13:01:01 +08:00
fengxiangdebaozi eeb7f30585 代码修改 2026-05-26 11:47:47 +08:00
董宏凯 29fdb06cea Merge branch 'dhk-1' into 'master'
修改

See merge request knowledge-base-bj/deerflow-server!56
2026-05-26 01:52:02 +00:00
fengxiangdebaozi 6d02e2c89e ai写作修改 2026-05-26 09:50:24 +08:00
fengxiangdebaozi cf8c17d08b 敏感词修改 2026-05-26 09:15:32 +08:00
吉鑫升andClaude Opus 4.7 ad5d55220c 技能展示适配持久化 + 参考文献收集与新对话流式修复
- 后端新增 skill_display_adapters 表,持久化每个技能的 display 配置与 sample
  payload;/api/skills 列表、详情、设置接口同步读写
- 新增 /api/skills/{name}/display-sample 端点,编辑器优先加载库中样例
- 前端展示适配面板改为从后端加载持久样例,无样例时提示"自动识别"
- 参考文献收集修复:仅在 bash/shell/python 等执行类工具上从参数推断技能
  名,避免 read_file / web_fetch 误匹配;引用列表纳入最新一条助手消息
- ChatBox 改用「最新 human → 最新 ai」窗口收集引用,并仅保留 citation
  模式
- 修复「新对话」点击后已发消息消失:useThreadStream 只在 threadId 切回
  空值时重置/中止 SSE,避免 onCreated 分配 id 时把刚开始的流打断;
  ChatPage 在 landing 状态隐藏 MessageList,移除 KeyedChatPage remount
- 技能详情对话框:所有者可查看源码与文件 tab(不再要求管理员)
- LocalSandbox:Windows PowerShell 5 路径下把 && 重写为 ;,并以
  UTF-8 解码 subprocess 输出,避免中文乱码
- knowledge-search-v2 mock 样例改为中文固定测试数据

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:16:51 +08:00
董宏凯 2ecd50f5dc Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!55
2026-05-25 14:52:19 +00:00
董宏凯 48d22e0d42 ui规范 2026-05-25 22:51:38 +08:00
董宏凯 096e85fbc3 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-25 22:39:56 +08:00
董宏凯 e3d8e534f6 样式修改 2026-05-25 22:39:50 +08:00
fengxiangdebaozi acadc246cb Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-25 21:22:09 +08:00
fengxiangdebaozi fd2c8d1d64 ui规范 2026-05-25 21:21:54 +08:00
董宏凯 f7575e4ddb Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!54
2026-05-25 12:35:09 +00:00
董宏凯 114ee8f6ac 代码修改 2026-05-25 20:34:46 +08:00
董宏凯 b91a45639f 模型下拉框 2026-05-25 20:23:33 +08:00
吉鑫升 c4277f8566 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/components/workspace/messages/message-list-item.tsx
2026-05-25 19:50:17 +08:00
吉鑫升 b7aa8b06ca 修复「新对话」流式中无法重置 + 参考文献开关默认关闭并加对勾
- 流式回答过程中点「新对话」:URL 同路径导致 react-router navigate 静默 no-op,
  下游 hook 收不到事件;改为显式 navigate(..., {replace:true, state:{resetAt}})
  保证每次点击刷新 location.key,并让 useThreadChat 监听 location.key 触发重置
- useThreadStream 在 threadId 变化时 stop() 掉上一条 SSE,避免旧流继续派发事件
- 引入 KeyedChatPage 用 location.key 作 ChatRuntime key,真正的导航触发整树 remount,
  解决 useStream 内部 stream.values 残留导致欢迎页底下还能看到旧消息的过渡帧问题
- 「参考文献」开关默认关闭(local.ts),开启态在文字前额外多一个 ✓ 对勾
2026-05-25 19:47:55 +08:00
董宏凯 26097b7cfe Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!53
2026-05-25 11:39:59 +00:00
董宏凯 6addf507cb 样式修改 2026-05-25 19:39:31 +08:00
董宏凯 f0bf29ba08 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-25 19:38:23 +08:00
董宏凯 6ef7701805 样式修改 2026-05-25 19:38:16 +08:00
fengxiangdebaozi 70814b1026 脱敏 2026-05-25 19:30:36 +08:00
fengxiangdebaozi ca1d7e77ba 脱敏 2026-05-25 19:30:26 +08:00
吉鑫升 a800baf8ae Merge remote-tracking branch 'origin/master' into ji 2026-05-25 18:59:32 +08:00
fengxiangdebaozi 2f7f07467e Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-25 18:20:50 +08:00
fengxiangdebaozi f472bba9f0 敏感词 2026-05-25 18:20:40 +08:00
董宏凯 48de418d21 Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!52
2026-05-25 10:19:58 +00:00
吉鑫升 8892637b97 新增 RAG 引用展示模式:支持技能结果按编号引用/列表/表格渲染
- 前端新增 skill-display 适配层(runtime/display-adapter)与可视化适配面板,
  支持从技能返回 JSON 自动识别字段并配置 citation/list/table/card 展示
- 正文 [[N]] 编号渲染为可点击徽章,点击不跳 URL 而是定位到右侧参考文献面板;
  参考卡片样式重做(圆形编号、悬停高亮、过滤掉 "result" 占位类型)
- bash 工具调用按命令中的 skills/{public,custom}/<name>/ 路径反查展示适配,
  解决调用名为 "bash" 时无法匹配到具体技能的问题
- 工具结果 JSON 解析放宽:能从 stderr/exit code 噪声尾巴里抢救出 JSON 主体
- 展示适配自动识别接入 LLM:读 SKILL.md 让模型推断真实返回结构,失败回退 mock
- 展示适配面板修复保存回显:保存后表单跟随服务端最新值刷新,样例文本不再被抹掉
- 设置页加 "RAG 引用模式" 开关(默认开),关联 lead-agent prompt 注入引用规则
2026-05-25 18:19:52 +08:00
董宏凯 8c55d4b55d 文档修改 2026-05-25 18:19:16 +08:00
董宏凯 dffb8a955b 暂停修改 2026-05-25 17:56:40 +08:00
董宏凯 86e3b4de06 Merge branch 'dhk-1' into 'master'
接口联调

See merge request knowledge-base-bj/deerflow-server!51
2026-05-25 09:50:01 +00:00
董宏凯 525218f9a7 多智能体协作 2026-05-25 17:48:59 +08:00
董宏凯 9a70329db2 代码提交 2026-05-25 16:08:18 +08:00
吉鑫升andClaude Opus 4.7 9a63a4b7ca 技能详情对话框新增文件浏览面板:支持预览、编辑、下载、复制路径
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:17:39 +08:00
董宏凯 357f245aec Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-25 14:14:39 +08:00
董宏凯 8f9451a24b 接口修改 2026-05-25 14:14:31 +08:00
fengxiangdebaozi 6396ccc701 接口修改 2026-05-25 14:14:01 +08:00
董宏凯 9361c069db Merge branch 'dhk-1' into 'master'
代码修改

See merge request knowledge-base-bj/deerflow-server!50
2026-05-24 14:16:58 +00:00
董宏凯 0b5cc2d639 代码修改 2026-05-24 22:16:25 +08:00
董宏凯 61358b6233 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!49
2026-05-24 13:13:48 +00:00
董宏凯 05815d9475 多智能问答 2026-05-24 21:13:19 +08:00
董宏凯 77db1a6297 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-23 23:36:55 +08:00
董宏凯 21254a060a 接口联调 2026-05-23 23:36:41 +08:00
董宏凯 f049936ec4 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!48
2026-05-23 15:05:37 +00:00
董宏凯 8673eb5eca 接口联调 2026-05-23 23:05:01 +08:00
董宏凯 43df9175df 修改 2026-05-23 17:46:31 +08:00
董宏凯andCursor 7fcf22de90 Merge origin/dhk-1: resolve config.yaml database backend conflict
Use sqlite as temporary fallback while MySQL is unavailable; keeps
local persistence instead of in-memory backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:44:52 +08:00
董宏凯 6f9d81ca68 样式修改 2026-05-23 17:43:46 +08:00
董宏凯 267ce81be2 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!47
2026-05-23 09:42:11 +00:00
fengxiangdebaozi 3987ee7ee3 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-23 17:23:22 +08:00
fengxiangdebaozi 87c9986b33 接口联调 2026-05-23 17:23:15 +08:00
fengxiangdebaozi a85f1571a0 接口修改 2026-05-23 10:49:16 +08:00
董宏凯 d6ff00226a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-22 21:41:52 +08:00
董宏凯 7f1f34e34f 样式修改 2026-05-22 21:41:26 +08:00
fengxiangdebaozi edb3238f3e 接口修改 2026-05-22 20:40:36 +08:00
吉鑫升andClaude Opus 4.7 67d5e43ec4 标题生成强制关闭 think 模式,回答速度指示器默认开启
- TitleMiddleware 给模型实例注入 extra_body 关 think(覆盖 Zhipu GLM 与 Qwen/vLLM 两套写法),并兜底剥离不闭合/残留的 <think> 标签,避免 GLM5 起标题时长时间思考与标签泄漏。
- 前端 streamSpeedMeterEnabled 默认改为 true,设置项描述同步为"默认开启"。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:03:24 +08:00
吉鑫升andClaude Opus 4.7 f8e3a3e5b4 公开定时任务页:每条记录标题前加 M月D日 前缀
同名任务多次执行后列表全是同一行,加上日期前缀方便扫描;
用 muted-foreground + text-xs + font-normal 弱化,不喧宾夺主。
Tooltip / title / 删除确认弹窗内的文案也同步带上日期。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:38:44 +08:00
吉鑫升 ef410e298d Merge remote-tracking branch 'origin/master' into ji 2026-05-22 16:23:11 +08:00
吉鑫升andClaude Opus 4.7 80fc2a9240 新增技能目录重新扫描接口与分页,关掉技能安全扫描的配置开关
- 后端新增 POST /api/skills/reconcile-custom:扫描 custom 目录,把磁盘存在但 DB 无记录的技能登记到调用者名下(published=false),附 4 个单元测试
- 前端技能列表加"重新扫描技能目录"按钮,列表分页每页 10 条
- SkillsConfig 新增 security_scan.enabled(默认 true)用于关闭 LLM 安全扫描;scan_skill_content 在开关关闭时直接放行
- 智能体卡片/对话页长名 truncate 显示 + loading 占位

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:22:42 +08:00
xu hd 3ae1202536 Merge branch 'xuhd' into 'master'
feat: 统一 Admin 用户中心 + 修复默认对话记忆存储路径

See merge request knowledge-base-bj/deerflow-server!46
2026-05-22 08:05:32 +00:00
xuhdandClaude Sonnet 4.6 2c0bb2041b feat: 统一 Admin 用户中心 + 修复默认对话记忆存储路径
## 前端
- 新增 AdminUserHub 页面,合并用户管理/审计/记忆三个页面为统一中心(左侧用户列表 + 右侧 Tab)
- 旧三条 admin 路由重定向到新 /admin 路由
- 侧边栏导航菜单合并为单一入口"用户中心"
- memory-settings-page: 默认助手下拉选项传 agent=default(后端识别为全局哨兵)

## 后端
- memory.py: 将 "default" 加入全局哨兵 _GLOBAL_AGENT_VALUES,?agent=default 读全局记忆文件
- memory_middleware.py: _resolve_agent_id() 无 agent 上下文时返回 None,默认对话写入 users/{id}/memory.json
- manager.py: 移除 agent_id or "default" 回退,让 None 透传给 V1 provider
- admin_users.py: Admin 记忆 API 将"默认助手"桶改为读全局文件(agent_id=None)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:04:49 +08:00
董宏凯 ab6b6876aa 样式修改 2026-05-22 15:26:20 +08:00
董宏凯 284445e0c9 样式修改 2026-05-22 15:20:59 +08:00
董宏凯 05729505d8 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-22 15:13:44 +08:00
董宏凯 4004fb908b 接口地址 2026-05-22 15:13:28 +08:00
董宏凯 19660e49d2 样式修噶 2026-05-22 14:56:59 +08:00
吉鑫升 b6a2c9336e Merge remote-tracking branch 'origin/master' into ji 2026-05-22 14:25:23 +08:00
吉鑫升andClaude Opus 4.7 ba2bc46042 智能体对话选 Agent 直达、澄清问题批量提交、LLM 错误中文化等
- 前端:新会话选 Agent 后直接跳转并自动发送,智能体页输入框宽度与首页对齐
- 前端:澄清卡片支持多选+多问题合并提交,消除单题逐一发送的割裂感
- 前端:定时任务弹窗 Agent 下拉支持长描述截断,避免撑破布局
- 后端:ask_clarification 支持同回合多次调用以提多个独立问题
- 后端:LLM 错误处理与定时任务失败提示统一改为中文

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:24:29 +08:00
wg he ad8f74c964 Merge branch 'hwg' into 'master'
修改数据库连接

See merge request knowledge-base-bj/deerflow-server!45
2026-05-22 06:10:44 +00:00
wghe ded3123af2 修改数据库连接 2026-05-22 14:09:03 +08:00
董宏凯 e076abe759 ui规范 2026-05-22 12:34:11 +08:00
xu hd c1bb4864ab Merge branch 'xuhd-2' into 'master'
feat: V1记忆系统支持、管理员记忆编辑及对话记忆注入开关

See merge request knowledge-base-bj/deerflow-server!44
2026-05-22 04:34:07 +00:00
xuhdandClaude Sonnet 4.6 b3a39cbdf7 feat: V1记忆系统支持、管理员记忆编辑及对话记忆注入开关
- 新增 V1 记忆系统(memory.json)支持:提取结果强制中文、历史字段修复为增量累积日志
- 用户设置页面:V1 模式下新增 agent 选择器,可查看各 agent 的私有记忆
- 管理员记忆页面:区分 V1/V2 渲染,V1 支持完整 CRUD(用户画像/历史摘要/事实记录)
- 后端新增 V1 记忆 CRUD API(sections/facts),前端同步添加对应 hooks 和 API 调用
- 新建专家页面:标记 bootstrap 线程类型,跳过记忆保存,侧边栏过滤该类线程
- 输入框工具栏新增「记忆」开关:关闭时 V1/V2 均不注入,开启时按 config.yaml 版本正常注入
- 修复 agent 目录不存在时的 FileNotFoundError,回退至默认配置

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 12:32:47 +08:00
董宏凯 d59e0514b3 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!43
2026-05-22 03:12:49 +00:00
董宏凯 6e88c5a1c5 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-22 11:11:57 +08:00
吉鑫升 c74be2b789 Merge branch 'master' into ji 2026-05-22 10:58:20 +08:00
吉鑫升andClaude Opus 4.7 a258122e22 定时任务:修复智能体执行 422、新增描述字段、公开页删除按钮等
- 定时任务执行时按 agent.id 解析智能体,老数据中存的中文显示名会回退按 name 查找,避免 422
- scheduled_tasks 表新增 description 字段(迁移 20260522_01),创建/编辑表单支持,公开页标题下显示
- 公开页(/public/scheduled-tasks/{id})新增删除单次执行记录入口,使用样式化 Dialog 确认
- 公开页左侧标题区精简:去掉 schedule/timezone/最近状态两行;执行列表标题剥离尾部时间戳,"计划"改为"执行时间"
- 工作区定时任务列表:超长标题改为按字符换行不再截断;按 created_at 倒序(新建任务在前)
- 启动 Alembic 升级加超时与单独线程,MySQL 缺库时自动 CREATE DATABASE
- vite build 增加 8GB 内存上限

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:55:59 +08:00
董宏凯 a01d7123d7 来源新增修改 2026-05-22 10:43:12 +08:00
董宏凯 7652db4601 样式修改 2026-05-21 22:26:25 +08:00
董宏凯 433c540ee2 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!42
2026-05-21 12:49:11 +00:00
董宏凯 37e1f7104f 代码提交 2026-05-21 20:47:45 +08:00
董宏凯 6467c8a870 样式修改 2026-05-21 20:42:57 +08:00
吉鑫升andClaude Opus 4.7 a892f55cb2 Merge branch 'master' into ji
冲突解决:
- frontend-web/src/pages/AgentChatPage.tsx: 保留 HEAD 的 isLandingState +
  AgentWelcome 欢迎页扩展,丢弃 master 的旧版 InputBox 风格
- offline-backend-20260512/backend/config.yaml: 保留 HEAD 的对齐注释格式;
  master 引入的中文 bank_mission / bank_retain_mission 已自动合入

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:31:49 +08:00
吉鑫升 ac4424bab2 代码修改 2026-05-21 20:19:44 +08:00
wg he 99992c951d Merge branch 'hwg' into 'master'
feat: 智能体精选排序与新对话智能体选择器

See merge request knowledge-base-bj/deerflow-server!41
2026-05-21 11:49:54 +00:00
wghe c516b74b75 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into hwg 2026-05-21 19:44:15 +08:00
xu hd da53b81bf9 Merge branch 'xuhd' into 'master'
22

See merge request knowledge-base-bj/deerflow-server!37
2026-05-21 11:43:01 +00:00
xuhd f730924572 222 2026-05-21 19:42:46 +08:00
董宏凯 0fb69a38cd Merge branch 'dhk-1' into 'master'
笔记本页面

See merge request knowledge-base-bj/deerflow-server!39
2026-05-21 11:41:20 +00:00
董宏凯 125e8b271b 笔记本页面 2026-05-21 19:40:54 +08:00
董宏凯 8c2ab6b860 Merge branch 'dhk-1' into 'master'
意图理解

See merge request knowledge-base-bj/deerflow-server!38
2026-05-21 10:48:46 +00:00
董宏凯 d639b24c2d 意图理解 2026-05-21 18:48:15 +08:00
wghe 6ebbf3aa32 Merge branch 'ji' of http://192.168.0.200/knowledge-base-bj/deerflow-server into hwg 2026-05-21 18:35:56 +08:00
wghe b97548f8d6 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into hwg 2026-05-21 18:30:28 +08:00
wghe 87eff3e133 22 2026-05-21 18:29:55 +08:00
xuhd 2e6ce67b64 22 2026-05-21 18:04:45 +08:00
吉鑫升andClaude Opus 4.7 843eda6fea fix: 公开接口豁免 CSRF 校验
/api/public/ 命名空间下的匿名 PUT/POST 请求没有会话 cookie,
被 CSRF 中间件拦在 403。auth 中间件已放行该前缀,CSRF 这里补齐豁免,
让公开定时任务页的编辑保存能正常调用。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:45:54 +08:00
吉鑫升 643f9b2c76 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/pages/AgentChatPage.tsx
2026-05-21 17:44:09 +08:00
董宏凯 2089f5e8ee Merge branch 'dhk-1' into 'master'
配置参数修改

See merge request knowledge-base-bj/deerflow-server!36
2026-05-21 09:38:12 +00:00
董宏凯 7e98d7dc52 配置参数修改 2026-05-21 17:37:39 +08:00
xu hd a514b2fe79 Merge branch 'xuhd' into 'master'
feat: 记忆配置改为数据库存储,用户只可配置召回模式

See merge request knowledge-base-bj/deerflow-server!35
2026-05-21 09:34:48 +00:00
xuhdandClaude Sonnet 4.6 d56230e04d feat: 记忆配置改为数据库存储,用户只可配置召回模式
- 新增 persistence/memory_config/ 模块:UserMemoryConfigRow ORM 模型 +
  SqlUserMemoryConfigRepository + MemoryUserMemoryConfigStore(内存兼容)
- 新增 Alembic 迁移脚本 20260521_01 创建 user_memory_configs 表
- memory_config.py:USER_WRITABLE_FIELDS 从 3 个缩减为 1 个
  (hindsight.recall_budget),移除文件 IO,改为进程内 cache + async DB 对接函数
- Gateway lifespan 启动时批量预热 recall_budget cache
- memory router PATCH/DELETE 改用 DB 存储,支持 400 校验非法 budget 值

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 17:34:25 +08:00
董宏凯 03e0d8248d Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!34
2026-05-21 09:23:14 +00:00
董宏凯 065bd8f28a 样式修改 2026-05-21 17:22:40 +08:00
董宏凯 ca13a9199f 样式修改 2026-05-21 16:39:48 +08:00
董宏凯 b59d476c2d Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!32
2026-05-21 08:31:57 +00:00
董宏凯 c69486556a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-21 16:31:27 +08:00
董宏凯 bfd75da308 功能迁移,页面整体样式修改 2026-05-21 16:30:11 +08:00
吉鑫升andClaude Opus 4.7 53bbb9b15b feat: 公开定时任务页支持正文/MD 切换与内联编辑
- 公开查看器右侧改为 Tabs:MD 附件优先、然后是「正文」;标签使用文件名(去后缀)
- 新增内联编辑:正文与 MD 文件均可编辑保存,每个 Tab 支持 Word 下载
- 后端新增公开 PUT 端点(/runs/{run_id}/files/{i}、/runs/{run_id}/body),服务层提取 _apply_run_file_edit 并新增 admin_update_run_file / admin_update_run_body
- 列表过滤:仅展示 succeeded 且有正文或可用 MD 的执行记录;后端在响应中标记每个文件 available,前端隐藏磁盘已缺失的 MD Tab
- 左侧任务名与右侧执行记录标题均改为可换行 + Tooltip 显示完整内容
- .gitignore 增加 frontend-web/dist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:53:43 +08:00
吉鑫升 f1d58983c2 feat: 智能体精选排序与新对话智能体选择器
- 后端新增 featured_order 字段及精选排序 API(/agents/selector、/agents/{id}/featured、/agents/featured/order),移除 user_agent_pin 表
- 前端新对话页 (ChatPage) 与输入框 (InputBox) 接入 agent-selector,选中后跳转 AgentChatPage 并通过 sessionStorage 透传首条消息
- 新增管理员智能体管理弹窗 (agent-manage-dialog) 支持精选添加/移除/上下移动
2026-05-21 14:39:12 +08:00
xu hd a8a545690f Merge branch 'xuhd' into 'master'
feat: 智能体创建流程优化与管理员记忆页面

See merge request knowledge-base-bj/deerflow-server!31
2026-05-21 03:50:02 +00:00
xuhdandClaude Sonnet 4.6 3748513996 feat: 智能体创建流程优化与管理员记忆页面
- 创建智能体页面:保存后自动跳转列表页、技能列表增加搜索过滤框
- Bootstrap 模式:开放全部技能可见性,注入专属提示词(skill_list 匹配意图、禁止 skill_manage)
- 管理员记忆页面:新增 AdminMemoryPage 及路由,支持查看/编辑用户记忆条目
- 内存配置与管理员用户接口同步更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:43:59 +08:00
董宏凯 18c2a4122f Merge branch 'dhk-1' into 'master'
更新

See merge request knowledge-base-bj/deerflow-server!30
2026-05-21 00:32:37 +00:00
董宏凯 04664c8d42 更新 2026-05-21 08:31:33 +08:00
董宏凯 628bbe14a8 Merge branch 'dhk-1' into 'master'
样式修改

See merge request knowledge-base-bj/deerflow-server!29
2026-05-20 14:47:15 +00:00
董宏凯 abae7c9d67 样式修改 2026-05-20 22:46:37 +08:00
董宏凯 eeee00f6cd 样式修改 2026-05-20 22:17:59 +08:00
吉鑫升andClaude Opus 4.7 6971ea5f9c fix: 修正 config.yaml 中 minimax 模型条目缩进,避免 YAML 解析失败
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:45:52 +08:00
吉鑫升 cee38f93c8 Merge remote-tracking branch 'origin/master' into ji 2026-05-20 19:38:00 +08:00
吉鑫升andClaude Opus 4.7 fa5a6b8176 fix: 技能详情弹窗宽度自适应
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:37:46 +08:00
董宏凯 97f8940b15 样式修改 2026-05-20 17:21:29 +08:00
董宏凯 556d23ade5 样式修改 2026-05-20 17:13:59 +08:00
吉鑫升 313290b22c Merge remote-tracking branch 'origin/master' 2026-05-20 17:06:06 +08:00
吉鑫升andClaude Opus 4.7 d339db4799 feat: 公开定时任务页面、Word 导出、定时任务实时刷新与技能详情重构
- 新增 /public/scheduled-tasks/:taskId 公开只读访问页与对应的
  /api/public/scheduled-tasks 后端路由(无需登录)
- 新增对话整体 / 单条消息 / Markdown 工件 "另存为 Word (.docx)" 能力
- 定时任务列表与运行列表在任务执行中自动轮询刷新,并新增"正在执行"标记
- 定时任务运行消息接口在事件存储为空时回退到 LangGraph checkpoint,
  让正在执行的任务也能立即看到提示词与流式输出
- 技能详情对话框由折叠面板改为 Tab 切换(Detail / SKILL.md),
  并支持 Rendered / Raw 切换、一键复制
- config.yaml 增加 database (MySQL) 与 checkpointer (SQLite) 配置块

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:04:58 +08:00
董宏凯 03dff2ce77 样式修改 2026-05-20 15:21:23 +08:00
董宏凯 c2c44f8d29 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-20 15:10:09 +08:00
董宏凯 13130c7f27 展开收起动画 2026-05-20 15:09:50 +08:00
董宏凯 30b986bb33 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!28
2026-05-20 06:06:30 +00:00
董宏凯 0196b0a445 文档修改 2026-05-20 14:05:53 +08:00
董宏凯 8edd06fb67 文档配置文件修改 2026-05-20 14:03:26 +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
董宏凯 4fbd0bb5c5 ai写作 2026-05-20 13:06:29 +08:00
董宏凯 c2563661d0 Merge branch 'dhk-1' into 'master'
容错处理

See merge request knowledge-base-bj/deerflow-server!27
2026-05-20 04:54:00 +00:00
董宏凯 f358cfb7c7 容错处理 2026-05-20 12:51:04 +08:00
董宏凯 ef11d3e0ed Merge branch 'dhk-1' into 'master'
报错兜底

See merge request knowledge-base-bj/deerflow-server!26
2026-05-19 14:56:01 +00:00
董宏凯 f44e2ccf52 报错兜底 2026-05-19 22:54:36 +08:00
吉鑫升 7e75c5b9b9 Merge remote-tracking branch 'origin/master' into ji 2026-05-19 19:58:59 +08:00
董宏凯 15ae11f966 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!25
2026-05-19 11:54:42 +00:00
董宏凯 226439ef36 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-19 19:54:18 +08:00
董宏凯 81aca93b81 章节问答 2026-05-19 19:51:44 +08:00
吉鑫升andClaude Opus 4.7 777c3b0079 fix: 定时任务执行过程实时刷新——RunJournal 增加按时间 flush
RunJournal 原本只在缓冲满 20 条或运行结束时才把事件落库,短运行会在结束前一直查不到消息,导致定时任务执行详情页的「执行过程」一直显示「暂无执行消息」。

新增 _FLUSH_INTERVAL_SECONDS=3s 的按时间 flush(由 _put 驱动);flush 失败时退回缓冲并重新抛出以便区分成败;flush 成功后若缓冲仍有残留则补一次 flush,失败则不重试避免空转。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:18:14 +08:00
董宏凯 ab85e4df72 效果优化 2026-05-19 18:10:15 +08:00
吉鑫升 cf8185498a Merge branch 'ji' 2026-05-19 17:22:45 +08:00
吉鑫升andClaude Opus 4.7 bf34b7df4e fix: 修复 Memory V2 合并导致的启动崩溃
master 的 Memory V2 重构删除了 memory/updater.py,admin_users.py 仍从中导入 get_memory_data 导致启动 ImportError。改用 BuiltinFileProvider 读取 V2 的 USER.md / 各 agent MEMORY.md;前端 MemoryBucketView 同步改为渲染 V2 条目列表。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:22:27 +08:00
吉鑫升andClaude Opus 4.7 028c5a34e6 fix: 速度胶囊每轮只显示一次 + 技能调用按 run 合并统计
速度胶囊:原本每条 AI 消息都渲染,agentic 多轮 LLM 调用会出现多个胶囊。改为只在每轮 turn-final 消息(下一条 human 之前的最后一条 AI 消息)显示。

技能统计:tool_call_metrics 改为按 (run_id, skill_name) upsert 合并——同一 run 内同一技能的多次调用/重试合并为一行;任一成功即记成功,全部失败则记一行 error 并累积所有报错信息;非技能调用不受影响。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:22:17 +08:00
xuhd c0ea0b0f5a 标题生成 2026-05-19 16:30:41 +08:00
吉鑫升 4c38055d38 Merge remote-tracking branch 'origin/master' into ji 2026-05-19 16:28:39 +08:00
吉鑫升andClaude Opus 4.7 87fcbc5bfb chore: vite 构建产物使用相对路径 (base: "./")
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:28:29 +08:00
ZyGuo 043a1aab21 update git ignore 2026-05-19 16:12:12 +08:00
xu hd 9f86c8e40d Merge branch 'xuhd' into 'master'
feat(memory): Memory V2 重写 - 可插拔 provider + Hindsight 集成

See merge request knowledge-base-bj/deerflow-server!22
2026-05-19 08:10:25 +00:00
xuhd 3330da0e59 记忆的config.yaml修改 2026-05-19 16:06:51 +08:00
xuhd 64492bec43 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-19 16:05:11 +08:00
董宏凯 2402a84c71 检索逻辑修改 2026-05-19 15:14:51 +08:00
董宏凯 294d06f22b 文档修改 2026-05-19 13:00:40 +08:00
xuhd d6e788dc89 修复 2026-05-19 09:55:20 +08:00
xuhd 070cd07199 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-19 09:40:19 +08:00
xuhd 4ded0ccd4f 补充 2026-05-19 09:38:20 +08:00
xuhd 6253a2bca9 修改优化 2026-05-19 09:38:00 +08:00
吉鑫升 bd1efd8026 Merge branch ji into master: 工具/技能调用统计 + 用户活动审计 + 标签多选筛选 2026-05-19 09:28:44 +08:00
吉鑫升andClaude Opus 4.7 4940e0d8ed feat: 工具/技能调用统计 + 用户活动审计 + 标签多选筛选
- 任务1: 用户管理/定时任务管理移入左下角菜单, 从侧边栏删除原入口
- 任务2: 管理员查看用户问答(读 LangGraph checkpoint)与记忆, 新增 admin_users 路由与审计页
- 任务3: 标签筛选改为顶部多选标签栏(OR), agents/skills/scheduled_tasks 三端
- 任务4: 新增 tool_call_metrics 表 + ToolMetricsMiddleware + tool-metrics 路由,
  工具/技能调用统计并入大模型统计页, 含错误分类
- 技能识别: lead agent 经 read_file 加载 /mnt/skills/<cat>/<name>/SKILL.md 调用技能,
  中间件扫描工具参数里的 skills/{public,custom}/{name}/ 路径还原 skill_name
- 单元测试 tests/test_tool_metrics_middleware.py (18 项全过)
- 附: SQLite->MySQL 切换后清理脚本

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:59:15 +08:00
董宏凯 8d6cc65530 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!21
2026-05-18 15:08:25 +00:00
董宏凯 b8adefbb88 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-18 23:08:02 +08:00
董宏凯 2c56a84bb7 接口报错异常处理 2026-05-18 23:06:59 +08:00
董宏凯 613e22d6ca Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!20
2026-05-18 14:53:35 +00:00
董宏凯 73e76fc72f 依赖安装 2026-05-18 22:46:57 +08:00
董宏凯 1e5e2581c8 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-18 22:45:25 +08:00
董宏凯 b25c4e33cb 异常处理 2026-05-18 22:41:44 +08:00
董宏凯 17b28f43f8 大纲规划 2026-05-18 21:43:48 +08:00
吉鑫升andClaude Opus 4.7 e3cde9be25 feat: 定时任务运行详情执行过程视图 + 管理员下载删除 + MySQL 迁移修复
定时任务:
- 运行详情页新增「执行过程」标签页,按 agent_run_id 拉取该次运行的
  agent 消息(GET /scheduled-tasks/runs/{id}/messages),运行中每 4s
  自动刷新,可观察卡住的任务停在哪一步
- 详情页「返回」按钮回到来源列表(普通/管理员)并重新选中原任务
- 管理员可下载、删除任意用户的执行记录(DELETE /runs/{id} 加管理员兜底)

MySQL 兼容修复:
- 合并迁移 20260518_06 汇合 ai-writing 与 thread-shares 两个 head
- 20260517_02 改用 SQLAlchemy Core 构造,规避保留字 key 的 1064 报错
- list_messages_by_run 各事件存储补 user_id 形参以保持接口一致

测试: 新增 test_scheduled_task_admin_run_delete.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:13:58 +08:00
吉鑫升 449703a6f0 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	offline-backend-20260512/backend/app/gateway/deps.py
2026-05-18 19:35:16 +08:00
吉鑫升andClaude Opus 4.7 358d5c72a6 feat: 业务库切换 MySQL + 修复定时任务运行详情递归与重名 500
- 业务 ORM 数据支持 MySQL 后端(asyncmy),新增 PortableJSON/
  BeijingDateTime 可移植列类型、迁移脚本与切库脚本
- 修复 GET /api/scheduled-tasks/runs/{id} 的 _load_run 无限递归
  (误调用自身,应委托 service.get_run)导致的 RecursionError 500
- 定时任务名按用户唯一:create/update/admin_update 主动校验,
  冲突抛 DuplicateTaskNameError,路由转 409 而非裸 IntegrityError 500
- 补充回归测试 test_scheduled_task_run_lookup / _name_uniqueness

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:34:14 +08:00
董宏凯 abd2ced4b4 Merge branch 'dhk-1' into 'master'
ai写作

See merge request knowledge-base-bj/deerflow-server!19
2026-05-18 09:55:50 +00:00
董宏凯 49e5aaea6b Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-18 17:47:33 +08:00
董宏凯 d1f431d121 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-18 17:45:10 +08:00
fengxiangdebaozi fd759d6845 依赖更新 2026-05-18 17:44:08 +08:00
董宏凯 5be5531722 容错显示 2026-05-18 17:41:51 +08:00
吉鑫升 2a4fccde70 Merge remote-tracking branch 'origin/likun' 2026-05-18 15:41:36 +08:00
吉鑫升 d6f018e581 Merge branch 'master' into ji 2026-05-18 15:39:57 +08:00
吉鑫升andClaude Opus 4.7 e0c1cb6581 feat: 定时任务管理 + 会话只读分享
定时任务:
- 管理员可查看/编辑/删除/触发全部用户的定时任务(新增 /api/scheduled-tasks/admin/* 接口与管理页)
- 卡死任务恢复:单次执行超 30 分钟超时重试(最多 3 次),耗尽后标记 failed;看门狗回收服务重启遗留的「执行中」任务

会话分享:
- 新增「只读链接」分享模式,生成公开免登录的只读对话快照页(/share/:code)
- 公开接口 /api/public/shares/* 提供对话快照与图片附件

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:39:40 +08:00
xuhdandClaude Opus 4.7 f80a00e211 feat(skills): curator 面板加 pinned 计数 + 运行时输出技能名日志
- get_curator_status 的 agent_created_skills 增加 pinned 计数,与
  active/stale/archived 正交;前端技能进化面板同步展示「被钉住」一项
  (curator/types.ts、skill-evolution-panel.tsx、zh-CN/en-US/types i18n)。
- run_curator_review 每次运行时把涉及的技能名输出到日志:新增
  _log_scanned_skills(扫描到的技能名)与 _candidate_skill_names
  (候选技能名),logger 与 curator_log 双写。

新增 test_curator_status_counts.py、test_curator_skill_logging.py。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:25:13 +08:00
xuhdandClaude Opus 4.7 4aee413680 revert(memory): Hindsight document_id 改回每轮独立文档
commit dba821a 把 document_id 从带时间戳改成按 thread_id 确定性派生
(同一 thread 累积成一个文档)。经复核,改回原来的带微秒时间戳写法 ——
每轮对话的 retain 重新落到各自独立的 Hindsight 文档。

retain_every_n_turns 轮次节流(turn_index)与 aiohttp 客户端泄漏修复
保持不动,仅回退 document_id 这一处。

test_hindsight_provider.py 的 TestDocumentId 按"每轮独立文档"重写。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:24:13 +08:00
likun 2a91e2e7ad 知识库补充到p6 2026-05-18 15:18:22 +08:00
xuhd dba821a70e @
fix(memory): 修复 Hindsight provider 生命周期的三处缺陷

HindsightProvider 被 build_memory_manager 每轮重新构造,以下三个问题
都源于这个无状态特性未被正确处理:

1. document_id 每轮漂移 —— 原 _reset_retain_batch 用微秒时间戳拼接,
   导致同一 thread 的每轮 retain 落到不同文档,文档数随对话轮数线性膨胀。
   改为按 thread_id 确定性派生 (thread-{thread_id}),同一 thread 累积成一份。

2. retain_every_n_turns 失效 —— 进程内 _turn_counter 每轮被 initialize
   重置,retain_every_n_turns > 1 时永不触发。新增 turn_index 参数贯穿
   sync_turn/sync_all,由 MemoryMiddleware 按对话历史 (count_user_turns)
   算出并传入;_turn_counter 仅作缺省退路。

3. aiohttp ClientSession 泄漏 —— 缓存的客户端从不 shutdown,且 _run_coro
   每次新建事件循环使缓存客户端失效,触发 "Unclosed client session"。
   改为客户端即开即关:_run_with_client 在 finally 保证关闭,_client_factory
   作为可覆盖的测试注入点。

测试:test_hindsight_provider.py 全量重写 (含 TestDocumentId /
TestRetainEveryNTurnsAcrossRecreation / TestClientLifecycle),
新增 test_memory_message_processing.py,test_memory_manager.py 补
turn_index 透传用例。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-18 14:34:22 +08:00
董宏凯 836dd73446 容错处理 2026-05-18 11:29:40 +08:00
xuhd 8f88427d4e Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-18 10:25:51 +08:00
xuhd 817585a4ce Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd-v2 2026-05-18 10:22:23 +08:00
xuhdandClaude Opus 4.7 0386f54449 feat(skills): 接入技能使用埋点,修复活跃技能被 curator 误归档
技能正文不进 system prompt,Agent 按 Progressive Loading 用 read_file
加载 SKILL.md 才算真正用到该技能。此前 read_file 与 skill_manage 的
patch/edit 都不更新 .usage.json,curator 的年龄流转据 latest_activity_at
判断闲置,导致每天都在用的 agent 创建技能在 30/90 天后被误标陈旧并归档。

- sandbox/tools.py: 新增 _custom_skill_name_from_path / _record_skill_read,
  read_file 读取 custom 技能目录下文件时 bump_use(best-effort,失败不影响主流程)
- skill_manage_tool.py: patch/edit 经 _bump_patch_best_effort 记 patch_count
- tests/test_skill_usage_signals.py: 12 项,覆盖路径解析、埋点与异常兜底

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:22:15 +08:00
吉鑫升 4761677676 Merge remote-tracking branch 'origin/master' into ji 2026-05-18 10:03:49 +08:00
吉鑫升andClaude Opus 4.7 f61d053010 feat(skills): 新增技能详情字段并统一发布图标
- 技能新增 detail 字段(skills 表新增列 + Alembic 迁移),
  由管理员或技能发布者编辑,内置/公开技能仅管理员可编辑
- 新增 PUT /api/skills/{name}/detail 接口,GET/列表接口返回 detail
- 新增可复用的技能详情弹窗组件,支持 Markdown 渲染与内联编辑
- 智能体创建/编辑选技能、智能体卡片技能徽章均可查看技能详情
- 将定时任务/智能体/技能的发布图标统一为 Globe/Lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:03:37 +08:00
fengxiangdebaozi 8440d02198 Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-18 10:03:06 +08:00
xuhd ebcdf847d2 222 2026-05-18 10:02:00 +08:00
fengxiangdebaozi 5c553aae19 文档更新 2026-05-18 09:58:34 +08:00
董宏凯 0af8ca5f78 历史记录回显 2026-05-17 22:55:01 +08:00
董宏凯 52ba178c4e 字数要求修改 2026-05-17 22:33:43 +08:00
董宏凯 166016a741 检索结果过程添加 2026-05-17 22:01:29 +08:00
董宏凯 20786e690d 自动下滑 2026-05-17 21:33:24 +08:00
董宏凯 c19417a706 暂停继续 2026-05-17 21:19:12 +08:00
董宏凯 b768e0affb 改写弹窗出现的位置 2026-05-17 20:38:35 +08:00
董宏凯 05ff10df1b ai改写调用 2026-05-17 20:18:11 +08:00
董宏凯 a0a6227368 文章类型添加 2026-05-17 19:50:52 +08:00
董宏凯 918e65d8d3 ai写作 2026-05-17 19:29:10 +08:00
董宏凯 6cab62d140 作家大纲拖拽调整顺序 2026-05-17 18:00:07 +08:00
董宏凯 9526268f1f 加载框顺序调整 2026-05-17 17:55:42 +08:00
董宏凯 016ec1877a ai写作 2026-05-17 17:39:41 +08:00
董宏凯 772e75f04d ai写作 2026-05-17 17:25:18 +08:00
董宏凯 10d7190ea4 ai写作 2026-05-17 14:22:25 +08:00
董宏凯 cc8703577d ai写作 2026-05-17 13:08:45 +08:00
董宏凯 4533755704 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-16 22:03:41 +08:00
董宏凯 da4a08dfe1 智能选中文字 2026-05-16 22:00:34 +08:00
xuhd 48ddbf0412 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-16 15:51:49 +08:00
xuhdandClaude Opus 4.7 39def1c20b feat(skills): curator 健壮性加固 + 评审模型可配置 + 手动触发体验
调度健壮性:
- 技能进化总开关 skill_evolution.enabled 真正生效;CuratorConfig
  与 CuratorSubConfig 默认 enabled 改为 false(显式 opt-in)
- 新增 per-user 运行锁(.curator_state 的 running/started_at),
  防定时器与手动触发并发重复运行;6h stale 检测防进程残留死锁
- maybe_run_curator 改用 ThreadPoolExecutor 限并发(上限 5),
  防首次部署所有用户同时到期的惊群
- 首次运行持续失败时按 6h 退避重试,不再每个 tick 烧 LLM 配额
- should_run_now 时间解析容忍 naive 时间串;损坏的 last_run_at
  触发重跑而非永久死锁
- SkillStore 新增 list_published_names 批量接口,curator 一轮
  只查一次 published,取代逐技能查库

LLM 评审超时:
- _run_llm_review 给 agent.invoke 套整体超时
  (curator.review_timeout_seconds,默认 600s),超时中止本次评审
  并在 curator 日志输出,curator 正常收尾、释放运行锁
- copy_context 把 user 上下文传入工作线程

模型可配置:
- 新增 curator.model_name;curator 评审与技能安全审核共用该模型,
  未配置则回退主模型
- 移除独立的 moderation_model_name 配置
- 前端进化面板新增模型下拉选择器

手动触发体验:
- /api/curator/run 派发后台线程前先预检,返回 {triggered, reason},
  不再无论开关状态都提示"已开始"
- 前端:总开关/curator 开关未开时禁用"立即执行"按钮,
  被预检拦下时按原因(开关未开 / 已有任务在跑)给出准确提示

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:50:15 +08:00
董宏凯 e8a31b5d37 代码修改 2026-05-16 14:24:44 +08:00
董宏凯 5a25dd08f6 Merge branch 'dhk-2' into 'master'
文档修改

See merge request knowledge-base-bj/deerflow-server!18
2026-05-16 01:07:56 +00:00
fengxiangdebaozi b34219ce86 文档修改 2026-05-16 09:07:18 +08:00
董宏凯 05548b0fa6 Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!17
2026-05-15 15:28:24 +00:00
董宏凯 e08e7be24c Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-15 23:27:48 +08:00
董宏凯 1be600155b openCanvas 2026-05-15 23:27:34 +08:00
xuhd be757b1f2f @
feat(skills): skill_list 可见性过滤 + skill 只读工具常驻 + 进化面板对照展示

- skill_list:默认 lead agent 改为只暴露未归档、且内置或属于当前用户的技能;
  自定义 agent 仍严格按白名单。SkillStore 不可用时降级放行全部自定义技能。
- prompt:注入系统提示词的技能列表剔除已归档(state=archived)技能。
- tools:skill_list/skill_view 只读工具改为常驻提供;skill_manage 写工具仍按
  生效配置(含 per-user 覆盖)的 skill_evolution.enabled 门控。
- curator:should_run_now 解析异常时改为不运行;get_curator_status 返回上次
  LLM 建议与实际工具调用两份记录,供前端对照。
- 前端:技能进化面板新增建议 vs 实际动作对照展示 + 配套 i18n。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-15 20:39:13 +08:00
xuhdandClaude Opus 4.7 d592e1e535 fix(skills): curator 自我进化周期调度 + 并发/配置/安全修复
调度:
- app.py 新增常驻 curator 调度器,每小时检查一次"需不需要进化",
  替代原来"仅启动跑一次";多 worker 下用跨平台文件锁做 leader 选举,
  只有一个进程跑调度;启动延迟 5 分钟避开启动高峰

curator 逻辑修复:
- C1: should_run_now / apply_automatic_transitions / get_curator_status
  调用 _get_curator_config 时传 user_id,使 per-user 配置在运行时生效
- C3: usage.py 的 .usage.json 读-改-写加进程内锁,bump_* 改回调式,
  避免 curator 多线程并发丢状态更新
- C4a: curator 受限版 skill_manage 在工具层屏蔽 delete,
  堵住绕过 skill_archive 安全网删技能的口子
- C4b: 进化记录同时保存 LLM 自述(suggestions)与真实工具调用(actual_actions)
- 无候选技能时跳过 LLM 评审,省掉无意义的模型调用

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:31:05 +08:00
xuhd e381e9f52e 222 2026-05-15 18:00:11 +08:00
吉鑫升 3b951efd13 Merge remote-tracking branch 'origin/master' into ji 2026-05-15 16:54:56 +08:00
吉鑫升 33161c5bad Merge remote-tracking branch 'origin/likun' into ji
# Conflicts:
#	frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx
#	frontend-web/src/components/workspace/code-editor.tsx
#	frontend-web/src/components/workspace/input-box.tsx
#	frontend-web/src/pages/ChatPage.tsx
#	offline-backend-20260512/backend/app/gateway/routers/artifacts.py
2026-05-15 16:54:10 +08:00
xuhd 8b6236c796 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd-v2 2026-05-15 16:42:48 +08:00
xuhdandClaude Opus 4.7 a59d80d7f3 feat(i18n): 全局将「专家」文案改为「智能体」
统一智能体功能的 UI 文案:侧边栏、智能体页、新建/保存流程、
审批卡片、登录提示及后端审批消息中的「专家」均改为「智能体」。
保留 prompt 人设、QA 子应用等非该功能语义的「专家」表述。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:42:40 +08:00
xuhdandClaude Opus 4.7 a5c76591ac feat(i18n): 全局将「专家」文案改为「智能体」
统一智能体功能的 UI 文案:侧边栏、智能体页、新建/保存流程、
审批卡片、登录提示及后端审批消息中的「专家」均改为「智能体」。
保留 prompt 人设、QA 子应用等非该功能语义的「专家」表述。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:42:11 +08:00
xuhd c2a975af9e @
feat(i18n): 全局将「专家」文案改为「智能体」

统一智能体功能的 UI 文案:侧边栏、智能体页、新建/保存流程、
审批卡片、登录提示及后端审批消息中的「专家」均改为「智能体」。
保留 prompt 人设、QA 子应用等非该功能语义的「专家」表述。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@
2026-05-15 16:41:44 +08:00
xuhd ab48cd5b55 111 2026-05-15 16:36:02 +08:00
吉鑫升andClaude Opus 4.7 ed4909a28d chore: 纳入 skill_evolution 运行时配置文件
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:29:26 +08:00
吉鑫升 afc517b3a7 Merge remote-tracking branch 'origin/master' into ji
# Conflicts:
#	frontend-web/src/components/workspace/agents/agent-gallery.tsx
#	frontend-web/src/components/workspace/settings/skill-settings-page.tsx
2026-05-15 16:27:56 +08:00
likun de9f6b6711 feat: 新增写作画布系统 2026-05-15 16:22:04 +08:00
吉鑫升andClaude Opus 4.7 646cb8caeb feat: 标签管理功能 + 问答前缀对用户隐藏
标签管理:
- 新增 tags/tag_assignments 持久化层、迁移、/api/tags 路由
- 标签分智能体/技能/定时任务三类, 仅同类型可互打, 仅 admin 可增删改与打标签
- 三类资源列表支持按名称模糊检索与按标签筛选(走后端查询参数)
- 前端新增标签管理页、TagPickerDialog(点击即时绑定)、彩色标签 chip

问答前缀:
- 前缀不再拼入消息 content, 改存 additional_kwargs 由 PromptPrefixMiddleware
  在调模型前临时注入, 用户界面看不到前缀但模型仍收到
- 修正 thread_existed 闸门导致首轮前缀被跳过的问题

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:19:08 +08:00
likun d533ddde88 feat: 补充知识库内容p4 2026-05-15 16:17:55 +08:00
xuhdandClaude Opus 4.7 9664cb653a feat(frontend): 记忆从设置弹窗移至侧边栏独立入口
- 新增 MemoryPage 及路由 /page/workspace/memory
- 侧边栏在「技能」下方新增「记忆」入口
- 设置弹窗注释掉「记忆」标签页

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:47:49 +08:00
xuhd 1574d44cc3 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-15 15:26:16 +08:00
xuhdandClaude Opus 4.7 6981fc06ca feat(skills): 点击技能展示 SKILL.md 详情弹窗
- 后端新增 GET /api/skills/{name}/content 接口,返回公共/自定义技能的 SKILL.md 原文
- 前端技能行内容区可点击,弹窗以等宽原文展示 SKILL.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:23:34 +08:00
xuhdandClaude Opus 4.7 12798c00bc feat(frontend): 设置弹窗移除「技能」标签页
技能管理已有侧边栏独立入口(/page/workspace/skills),设置弹窗内的入口冗余,移除。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:10:43 +08:00
xuhdandClaude Opus 4.7 ebb6be816b feat(frontend): 技能/专家广场展示调整 + 通知合并入设置菜单
- 技能与专家「广场」标签只展示内置和已发布项,Tab 顺序调为「我的」在前并默认选中
- 「智能体」全量更名为「专家」
- 通知由侧边栏铃铛改为「设置和更多」菜单内的弹窗入口,合并通知列表与通知设置

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:08:56 +08:00
likun ae4e4f67c2 新增大模型知识库技能 2026-05-15 14:51:16 +08:00
xuhdandClaude Opus 4.7 013aafde17 feat(frontend): 记忆页面迁移到 Memory V2
前端记忆模块整体迁到后端 Memory V2 接口,修复调旧端点 /api/memory
返回 404 的问题。

- core/memory/{types,api,hooks}.ts 重写为 V2 数据模型与端点
  (USER.md / MEMORY.md 条目增删改、配置读写、Hindsight 检索)
- memory-settings-page.tsx 重写:用户画像 + 工作笔记两区的条目
  增删改、记忆设置面板(白名单字段)、Hindsight 检索面板
- 工作笔记按专家隔离,顶部专家下拉(默认助手 + useAgents 列出的
  用户专家),专家列表取自 agents 注册表
- settings-dialog 记忆分区移到技能下方
- i18n(zh-CN/en-US/types)memory 文案整体替换

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:14:08 +08:00
董宏凯 050b1ad78e 代码提交 2026-05-15 14:08:01 +08:00
xuhdandClaude Opus 4.7 21a76f5e7c refactor(memory): Memory V2 P3 - 舍弃后台抽取 + Gateway 端点收口
P3 在 P1/P2 基础上完成 Memory V2 收尾:

舍弃后台抽取:
- 删除 updater/queue/storage/prompt/summarization_hook 五个模块
- 移除 background_extraction 配置段、扁平字段兼容、storage 字段
- 理由:Hindsight auto_retain 已自动留存每轮对话,后台抽取冗余

Hindsight 召回注入改用 wrap_model_call:
- 由 before_model 改为 wrap_model_call —— 召回只注入单次模型请求、
  不落持久 state,从根上消除 <memory-context> 泄漏给前端的风险
- builtin frozen snapshot 仍走系统提示词

Gateway 端点收口为 /api/memory/v2*:
- 删除旧 facts/history 端点 (/api/memory、/memory/facts 等)
- 新增 entries 增删改 (POST/PUT/DELETE /memory/v2/entries)
- 新增 POST /memory/v2/search —— Hindsight recall/reflect 检索代理
- client.py memory 方法重写为 V2 等价方法

文档同步更新 CLAUDE.md 与 MEMORY_V2_DESIGN_ZH.md。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:08:18 +08:00
xuhdandClaude Opus 4.7 f9d7610679 feat(memory): Memory V2 重写 - 可插拔 provider + Hindsight 集成
以 Hermes 的可插拔记忆架构为骨架重写 deerflow 记忆模块,兼容原有
结构化数据,并新增 Hindsight 作为外部记忆后端。

P1 - 抽象层 + builtin 本地记忆:
- MemoryProvider 抽象基类 + MemoryManager 编排层(builtin + ≤1 外部)
- BuiltinFileProvider:USER.md/MEMORY.md 二元存储、字符配额、文件锁、
  原子写、frozen snapshot
- memory 工具(LangChain @tool):LLM 主动写入用户画像/工作笔记
- 记忆内容安全扫描(注入/外渗模式 + 不可见 unicode)
- memory_config 改为嵌套结构 + 旧扁平字段兼容 + per-user 三层覆盖
- 迁移脚本 migrate_memory_to_v2.py:旧 memory.json -> USER.md/MEMORY.md
- Gateway 新增 /api/memory/v2 与 v2/config 端点(用户可配置白名单字段)

P2 - Hindsight 外部 provider:
- HindsightProvider:双 bank 隔离(user bank 跨 agent 共享 + work bank
  agent 私有)、并发召回合并、auto-retain、on_memory_write 镜像
- hindsight_recall/reflect/retain 三个 LangChain 工具
- MemoryMiddleware.before_model 注入 Hindsight 动态召回
- context/tools/hybrid 三种 memory_mode
- hindsight-client 作为可选依赖,未装时自动退回纯 builtin

注入走系统提示词(builtin frozen snapshot)+ before_model(Hindsight
动态召回)。后台抽取保留但默认关闭,P3 期重接入。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:15:55 +08:00
吉鑫升 aeba15eb16 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	frontend-web/src/pages/WorkspaceRoutes.tsx
#	offline-backend-20260512/backend/packages/harness/deerflow/persistence/models/__init__.py
2026-05-15 11:21:48 +08:00
董宏凯 b81f64a253 Merge branch 'dhk-2' into 'master'
Dhk 2

See merge request knowledge-base-bj/deerflow-server!16
2026-05-15 02:43:25 +00:00
fengxiangdebaozi a0d04cba50 文档修改 2026-05-15 10:42:44 +08:00
xu hd 43bdbcf89c Merge branch 'xuhd' into 'master'
feat(skills): 自我进化后端 — curator 配置可编辑 + 运行日志可读

See merge request knowledge-base-bj/deerflow-server!15
2026-05-15 01:36:33 +00:00
xuhdandClaude Opus 4.7 6947a3ffa7 chore(tests): ignore tests/ 目录 + 同步现有测试到新 skill_archive 签名
- .gitignore: 新增 offline-backend-20260512/backend/tests/* 规则,
  让后续新增的测试文件不再误进入版本控制
- test_skills_multitenancy.py: 同步上次 feat(skills) 提交里
  skill_archive(name, reason) 必填 reason 的签名变更

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:03:07 +08:00
xuhdandClaude Opus 4.7 62d525dae7 feat(skills): 自我进化 - 软归档/恢复 + Agent 归属 + 状态分层 UI
后端:
- 新增 PUT /api/skills/custom/{name}/state 与 /pinned 软归档/恢复 + 钉选 API
- skill_manage 工具: agent 创建写 DB 归属; 删除清理 .usage.json + DB row + cascade 引用; 加 ownership 预校验
- curator 全部软归档(文件保留在 custom/), 加 reason 必填(中文), 修 user_context 跨线程传播 / 协程泄漏 / archive 文件不再移动
- LLM 评审接 ToolErrorHandlingMiddleware, 工具异常自动重试不中止本轮; 替换 create_react_agent 为 create_agent
- skill_list 按当前 agent config.yaml 的 skills 字段过滤(custom agent), lead agent 仍可见全部
- 自我进化提示词对齐 Hermes 风格: 主动询问保存, reason 强制中文且面向用户
- 控制台日志增强: 自动归档/标过时/LLM 合并/删减建议 都按条目输出原因

前端:
- 技能管理页 Tab 改「全部 / 我的」, 正确包含他人发布的 custom 技能(修复 isCustom 互斥 bug)
- 新增「过时 N / 归档 N」右侧抽屉(恢复/查看/立即归档 ⋯ 菜单)
- 新增徽章: 钉选 / 来源(Agent 创建 / 用户上传) / 使用次数展示
- 内置技能徽章修复(原 isBuiltin && isCustom 永远 false)
- 自我进化日志 run_started sentinel 在前端展示为中文分隔条
- 移除冗余 enabled 开关(归档已覆盖此能力, 减少术语)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:00:52 +08:00
xuhd f8bc74b27f Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-14 23:45:10 +08:00
董宏凯 044370f13c 沙箱内容可保存 2026-05-14 22:12:20 +08:00
xuhd a2c45492cb 2222 2026-05-14 20:45:22 +08:00
xuhd 9a67ca7c03 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into xuhd 2026-05-14 20:45:02 +08:00
xuhdandClaude Sonnet 4.6 40cd8a24af feat: admin skill public upload + user management page
- Admin users can choose to install skills to public (built-in) or custom category via UI radio selector in InstallSkillDialog
- Backend: SkillStorage.ainstall_skill_from_archive accepts target_category param; non-admin 403 on public install
- Backend: AgentStore/SkillStore add count_owned(user_id); UserRepository/LocalAuthProvider add list_users()
- Backend: GET /api/v1/auth/users endpoint (admin-only) returns all users with skill_count and agent_count
- Frontend: admin-only "用户管理" nav item in sidebar; /page/workspace/admin/users route with user table
- i18n: added sidebar.users and adminUsers section in zh-CN/en-US

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:40:23 +08:00
吉鑫升andClaude Sonnet 4.6 28948975db Merge: 解决 ScheduledTaskRunDetailPage 合并冲突
保留 HEAD 的编辑功能 import(useCallback、Edit3、Loader2、Save、X),
同时采用 b78672 中的 useRouteNavigate 自定义导航 hook。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:29:35 +08:00
fengxiangdebaozi 1ce1db9689 代码处理 2026-05-14 19:28:07 +08:00
fengxiangdebaozi 86cdeed6ef Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-2 2026-05-14 19:26:01 +08:00
fengxiangdebaozi d6ec0afbd6 路由嵌套修改 2026-05-14 19:25:39 +08:00
吉鑫升 3ea33e1dee 大模型速度增加提示 2026-05-14 19:12:14 +08:00
xuhdandClaude Opus 4.7 3e6e3c08d5 feat(skills): curator 三条件过滤 — agent 创建 + 未发布 + 无引用
Curator 之前只过滤 source=='agent',导致两类 skill 仍会被自动归档:
1. owner 已 published 的 skill(user B 在用,结果被 curator 收走)
2. 加进 agent.skills 但 .usage.json 活动不够频繁的 skill

新规则:curator transitions 和 LLM 合并候选**只**作用于同时满足
三条件的 skill —— source=='agent' + published==False + 不被任何
agent.config.yaml 的 skills 字段引用。

curator.py
- 新 helper _collect_referenced_skill_names():扫所有
  .deer-flow/agents/*/config.yaml 把 skills 字段聚合成 set,一次
  curator run 内只调一次
- 新 helper _is_skill_published(name):通过 SkillStore 查 published
  状态,失败时返回 False(保守降级到旧行为)
- 新 helper _eligible_for_curation(name, entry, owned_set, referenced_set)
  三条件合一判定
- apply_automatic_transitions 应用三条件过滤;返回的 counters 加
  skipped_referenced / skipped_published 计数
- _render_candidate_list 应用同一过滤,LLM 看不到 protected skill
- _make_skill_archive_tool safety net:即使 LLM 编造名字调 archive,
  工具内部再做一次"被引用 + published"检查,违规拒绝并记 warning

测试 tests/test_skills_multitenancy.py 新增 5 用例:
- test_curator_skips_skill_referenced_by_any_agent
- test_curator_skips_published_skill
- test_curator_archives_orphan_unpublished_skill
- test_skill_archive_tool_refuses_referenced_or_published
- test_collect_referenced_skill_names_aggregates_from_disk

跑测试:tests/test_skills_multitenancy.py 17/17,全套 36/36 全过。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:49:22 +08:00
xuhdandClaude Opus 4.7 6155a2ec2b fix: curator + notifications react-query cache 按 user_id 隔离
切换账号在同一 tab 里时,旧 cache key (["curator","config"] 等) 会
让新账号"看到"上一个账号的值。如果用户没意识到、直接点保存,就会
把上一个账号的配置原样写到新账号的 per-user JSON 里去。

修复:在 useCuratorKeys / useNotificationKeys hook 里读
getStoredAuth().user_id 织进 queryKey。每个用户拥有独立 cache
桶,切换账号绝不命中对方的数据。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:20:41 +08:00
董宏凯 4e0787ec77 Merge branch 'dhk-2' into 'master'
页面接口整体修改

See merge request knowledge-base-bj/deerflow-server!14
2026-05-14 10:14:39 +00:00
fengxiangdebaozi 8ecc32da6d 页面结构修改 2026-05-14 18:13:58 +08:00
xuhdandClaude Opus 4.7 eb41da78f2 fix: 四处 router.push 缺 /page 前缀导致 404
HashRouter 顶层路由是 /page/*,所有 workspace 子路由必须带这个前缀
才能命中。以下四处 router.push 字面量遗漏,点击后落到 NotFoundPage:

- skill-evolution-panel.tsx 返回技能按钮
- NewAgentPage.tsx 返回 agents
- NewAgentPage.tsx 创建后“开始对话”
- agent-card.tsx 列表卡片“聊天”按钮

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:56:32 +08:00
xuhdandClaude Opus 4.7 a508969331 chore: 忽略 skill_evolution.json 运行时文件
前端 PUT /api/curator/config 会把 per-user 覆盖写到
backend/skill_evolution.json(全局 fallback),属于运行时
数据不该入库。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:46:36 +08:00
xuhdandClaude Opus 4.7 99febda256 feat(skills/notifications): 多租户阶段 4 — 测试 + 前端
后端:
tests/test_skills_multitenancy.py(12 个用例,全部通过)
- SkillStore: 可见性 / 全局重名拒绝 / owner-only update+delete /
  admin override / ensure_legacy 仅补 NULL owner
- 路由助手 _get_editable_skill_or_raise / _get_owned_skill_or_raise
  的 owner / admin / 403 / 404 分流
- cascade_skill_unpublished_or_deleted:
  · unpublish 同时扫两个用户的 agent,剥掉 skill;自己改自己不发
    通知,其他人收 agent_skill_removed
  · admin_takedown 额外给 skill owner 发 skill_taken_down
- NotificationStore CRUD + unread_count + 跨用户隔离
- 同步版 apply_automatic_transitions 用户隔离:A 跑 curator 不
  动 B 的 skill;curator_log per-user 分桶

前端 — core/notifications/{types,api,hooks,index}.ts
- 类型对齐 routers/notifications.py 的 NotificationItem
- listNotifications / getUnreadCount / markRead / markAllRead /
  deleteNotification
- useNotifications / useUnreadCount(refetchInterval=30s) / mutation 钩子

前端 — components/workspace/notification-bell.tsx
- 顶部铃铛图标 + 未读红点(>99 显示 99+)
- 复用 DropdownMenu 作为弹层(项目暂无 Popover 原语)
- 列表项:未读蓝点 + 标题 + body + 相对时间,悬停露出删除按钮
- 标记单条已读 / 全部已读 / 删除
- 接入 workspace-nav-menu.tsx,作为 SidebarMenuItem 放在设置入口上方

前端 — Skill 多租户字段
- core/skills/type.ts 加 owner_user_id / published / builtin
- core/skills/api.ts 加 setSkillPublished + deleteCustomSkill
- core/skills/hooks.ts 加 useSetSkillPublished + useDeleteCustomSkill

前端 — settings/skill-settings-page.tsx
- 抽出 SkillRow 子组件,每项渲染 owner badge(内置/我的/来自他人/
  已发布),样式抄自 agent-card.tsx 的 isBuiltin/isOwned/isFromOthers
- canManage = isOwned || isAdmin;canTogglePublish 同上但排除内置
- 发布/取消发布按钮(Eye / EyeOff),调用 setSkillPublished
- 删除按钮 + 二次确认 Dialog,调用 deleteCustomSkill
- 启用开关保持原样

前端 — i18n
- types.ts: 新增 NotificationsTranslations + Translations.notifications;
  settings.skills 加 badgeBuiltin/badgeMine/badgeFromOthers(+Title) /
  badgePublished(+Title) / publishAction / unpublishAction /
  publishSuccess / unpublishSuccess / deleteAction / deleteConfirm /
  deleteSuccess
- zh-CN.ts / en-US.ts 同步补齐对应文案

验证
- PYTHONPATH=. uv run pytest tests/test_skills_multitenancy.py -v →
  12 passed in 2.08s
- 与现有 test_agents_backend / test_skill_evolution_runtime /
  test_skills_install_upload 并行跑:41 通过 + 1 个 pre-existing 失败
  (test_validate_rejects_missing_skill_md 期望 "SKILL.md" 但收到
  "Skill archive is empty",与本期改动无关)
- pnpm typecheck:本次新增/修改的文件零新增错误;剩余报错全部为
  pre-existing 的 next/link、next/navigation 别名声明缺失、
  workspace-nav-menu.tsx 的 SettingsSection 类型不匹配、nextra
  缺失、markdown-docx 第三方库等

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:35:02 +08:00
fengxiangdebaozi 543d78e040 路由调整 2026-05-14 17:24:45 +08:00
xuhdandClaude Opus 4.7 4ef96c7930 feat(skills): 多租户阶段 3 — Curator + 配置 per-user
curator_log.py
- ring buffer 从单一 deque 改为 dict[user_id -> deque],每个用户
  自己的运行日志互不可见
- start_new_run / record / get_recent / clear 全部接 user_id
  关键字参数;未传时落入 __default__ 桶兼容旧调用
- 新增 clear_all() 用于跨用户清理(仅测试用)

curator.py
- 状态文件路径从 skills/.curator_state 改为
  .deer-flow/users/{user_id}/.curator_state(per-user 隔离)
- 所有公开 API 接受 user_id 参数:should_run_now /
  apply_automatic_transitions / _render_candidate_list /
  run_curator_review / _curator_thread_target /
  get_curator_status / _make_skill_archive_tool / _run_llm_review
- 新增 _list_user_owned_skill_names(user_id):通过 SkillStore
  查询当前用户拥有的 skill 名集合,用于过滤 transitions / LLM
  candidates,保证 user A 跑 curator 不会动 user B 的 skill;
  SkillStore 不可达时退化为"全部 agent-created skill"(兼容旧部署)
- _get_curator_config(user_id) 通过 load_skill_evolution_config_for_user
  叠加 per-user override,让阈值/间隔/moderation_model 也是 per-user
- maybe_run_curator 启动时枚举 UserRow,按用户检查 should_run_now
  并各起一个 daemon 线程跑

skill_evolution_runtime.py
- 全局 backend/skill_evolution.json 语义保留作为 fallback
- 新增 per-user API:resolve_user_path / load_user_overrides /
  save_user_overrides / get_user_overrides_mtime,文件落到
  .deer-flow/users/{user_id}/skill_evolution.json
- 新增 load_skill_evolution_config_for_user(user_id):读全局
  SkillEvolutionConfig 作为 baseline,浅合并 per-user JSON,
  返回 Pydantic 实例。验证失败时回落到 baseline

routers/skills.py
- /api/curator/status / run / config GET+PUT / logs 全部接
  _current_user_id 注入运行上下文
- update_curator_config 改为 per-user:保存到
  users/{user_id}/skill_evolution.json,不再 reset_app_config
- _build_curator_config_response 接受 SkillEvolutionConfig 实例
  (而不是 AppConfig)以适配 per-user 合并结果

旧 19 个测试继续通过;未引入新依赖。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:09:05 +08:00
fengxiangdebaozi 7a7c99f752 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-2 2026-05-14 16:51:58 +08:00
fengxiangdebaozi 6b1f17d6a8 页面导入 2026-05-14 16:51:32 +08:00
xuhdandClaude Opus 4.7 cf191ef874 feat(skills/notifications): 多租户阶段 2 — 路由 + 级联 + 启动迁移
后端工作:

deps.py
- 挂载 skill_store / notification_store 到 app.state
- 启动时 set_notification_store(...) 注入到 harness sender;
  关闭时清空,保证测试和 embedded 模式不留状态
- 加 get_skill_store / get_notification_store FastAPI 依赖

routers/skills.py
- 加 _current_user_id / _is_admin_user / _get_owned_skill_or_raise /
  _get_editable_skill_or_raise(对齐 routers/agents.py)
- SkillResponse 加 owner_user_id / published / builtin 字段,
  _skill_to_response 接受 SkillStore 记录映射
- GET /api/skills 与 GET /api/skills/custom 改 per-user 可见性过滤
- POST /api/skills/install + /install-upload 预解析 archive 名校验
  全局 name 不被他人持有;安装后 ensure_legacy 注册 owner=current
- DELETE /api/skills/custom/{name} 走 _get_editable_skill_or_raise,
  调 cascade(owner 自删 = delete,admin 跨用户 = admin_takedown),
  SkillStore.delete / delete_admin 同步清除 DB 记录
- 新增 PUT /api/skills/custom/{name}/published 切换发布状态;
  从 published → unpublished 时触发 cascade(unpublish / admin_takedown)

skills/cascade.py(新)
- 扫所有 .deer-flow/agents/{id}/config.yaml,把指定 skill 从其 skills
  数组移除,并通过 deerflow.persistence.notifications.notify 给受
  影响 agent 的 owner 发站内信;admin 强制下架时另给 skill owner 发
  一条
- 操作类型:unpublish / delete / admin_takedown,对应不同文案

routers/notifications.py(新)+ app.include_router
- GET /api/notifications(unread_only / limit)
- GET /api/notifications/unread_count(红点用)
- POST /api/notifications/{id}/read
- POST /api/notifications/read_all
- DELETE /api/notifications/{id}

app.py lifespan
- _sync_legacy_skills(app):每次启动扫 skills/custom/ 把没在 DB
  的 skill 落库为 admin owner + published=True,幂等、非阻塞失败
- 调用顺序:_ensure_admin_user → _sync_legacy_skills → 启动 curator

测试:19 个旧用例继续通过;阶段 4 会写 test_skills_multitenancy.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:24:42 +08:00
董宏凯 f6d3d013fd Merge branch 'dhk-2' into 'master'
样式修改

See merge request knowledge-base-bj/deerflow-server!13
2026-05-14 08:04:28 +00:00
fengxiangdebaozi e6ad3c414f 页面布局调整 2026-05-14 16:01:25 +08:00
xuhdandClaude Opus 4.7 8162719e36 feat(skills/notifications): 多租户阶段 1 — 持久化层
新增两套 persistence 模块,仿 persistence/agents/ 模板:

1. persistence/skills/
   - SkillRow ORM (PK=name, owner_user_id, published, timestamps)
   - SkillStore 抽象:list_visible / get_visible / get_any /
     get_owned / create / ensure_legacy / update / update_admin /
     delete / delete_admin
   - SqlAlchemy 实现 SkillRepository
   - MemorySkillStore 用于测试
   - 可见性 = owner OR published OR builtin

2. persistence/notifications/
   - NotificationRow ORM (id, user_id, type, title, body, payload JSON,
     read, created_at)
   - NotificationStore 抽象:create / list_for_user / unread_count /
     mark_read / mark_all_read / delete
   - SqlAlchemy 实现 NotificationRepository
   - MemoryNotificationStore 用于测试
   - sender.notify(user_id, type, ...) 全局接口;harness 不能 import
     app.*,所以用 set_notification_store / get_notification_store
     注入模式

3. persistence/models/__init__.py 注册 SkillRow + NotificationRow

后续阶段会接入路由、cascade、curator per-user 重构、前端。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:44:36 +08:00
xuhdandClaude Opus 4.7 f5bc02d598 fix: skills 路由两个小修复
skills.py:
- update_curator_config 直接 await get_curator_config() 报错
  ('Depends' object has no attribute 'skill_evolution')。FastAPI 路由
  默认参数 Depends(...) 只在请求时被框架解析,直接调用拿到的是依赖标记。
  抽出 _build_curator_config_response(config) 让 GET 和 PUT 共用。

skill_manage_tool.py:
- 模型把 action='list' 当成可用动作误调 skill_manage 报
  Unsupported action。补两个 read-only 别名 list/view,分别委派到
  skill_list_tool 和 skill_view_tool,docstring 同步更新。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:43:26 +08:00
fengxiangdebaozi 139dfb76e4 bug修改 2026-05-14 14:59:12 +08:00
fengxiangdebaozi f4769c4046 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-2 2026-05-14 14:28:31 +08:00
fengxiangdebaozi fea3cc741f 系统合并 2026-05-14 14:28:17 +08:00
xuhdandClaude Opus 4.7 4e764350a4 feat(agents): 创建专家页面加模型选择
NewAgentPage 在两处加 Select 下拉:
- 左侧聊天框上方 "当前对话模型":透传给 useThreadStream 的
  context.model_name,控制当前 bootstrap 对话调用的 LLM
- 右侧表单 "模型":写入 agent.model,控制专家保存后被使用时调用
  的 LLM

两个下拉互相独立,复用 useModels() 拉 /api/models。页面加载完
模型列表后自动 seed 到列表第一个(与 config.yaml 全局默认对齐),
用户可随时切换。

杂项:.gitignore 忽略 skills/ 与 hermes-agent-main/ 运行时目录。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:59:10 +08:00
xuhd c63811367e 123 2026-05-14 12:45:04 +08:00
xuhdandClaude Opus 4.7 4cc8b3198b fix: 修复 setup_agent 创建专家流程的多个问题
后端 (setup_agent_approval_middleware.py):
- 拦截策略从 wrap_tool_call + Command(goto=END) 改为
  after_model + jump_to="end"。在 tool_node 执行前直接终止图,
  避免模型并行调多个 tool_call 时 Command 输出被稀释、graph
  路由不稳定的问题(症状:审批 ToolMessage 发出后流不结束)。
- 用 @hook_config(can_jump_to=["end"]) 装饰器声明跳转能力,
  否则 LangChain 不会为该 hook 构造 conditional edge。

前端 (NewAgentPage.tsx):
- 聊天阶段不再调 ensureAgent / POST /api/agents,placeholder
  agent 不再落库;agent 仅在用户点右上角“保存”时创建。
- 自动生成的占位名 "未命名专家-xxx" 不再回写到输入框,保持空白
  直到用户输入或 setup_agent 审批回填。
- handleApproveSetupAgent 仅把模型 propose 的 payload 填入表单,
  不再 PUT /api/agents/{id};落库交给手动保存按钮。
- onToolEnd 不再 getAgentWithRetry —— 工具被 middleware 拦截、
  根本不会执行,没有 agent 状态可拉。清理对应死代码(getAgent
  import、wait()、AGENT_READ_RETRY_DELAYS_MS、getAgentWithRetry)。
- 路由跳转补齐 /page 前缀:返回按钮、“开始对话”按钮。
- 右侧表单:SOUL.md textarea 加 max-h-[400px],技能列表外框加
  max-h-[240px],各自 overflow-y-auto,避免长内容撑高整列。

前端 (agent-card.tsx):
- 列表卡片“聊天”按钮的跳转路径补齐 /page 前缀,避免点击后 404。

无新依赖。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:35:49 +08:00
xuhdandClaude Opus 4.7 5c6ac06656 feat(skills): 自我进化前端 — Skills 独立侧边栏 + /skills/evolution 页面
UI 重构:
- settings-dialog.tsx:把 Skills section 从设置弹窗移除
- workspace-nav-chat-list.tsx + WorkspaceRoutes.tsx + SkillsPage.tsx:
  Skills 提升为独立侧边栏导航项,/workspace/skills 路由直接渲染
  SkillSettingsPage(同事原型已就绪)
- skill-settings-page.tsx:顶部追加 "自我进化" 跳转按钮

自我进化页面:
- /workspace/skills/evolution 子路由 → SkillEvolutionPage 包装
- skill-evolution-panel.tsx:三段式
  · 状态卡:启用/暂停徽章 + 累计运行次数 + 上次运行时间/摘要 +
    技能计数 (total/active/stale/archived) + 立即执行按钮
  · 配置表单:enabled / curator.enabled / interval_hours /
    stale_after_days / archive_after_days / moderation_model_name
    本地暂存 → 保存调 PUT,热生效
  · 日志面板:拉 GET /api/curator/logs,2 秒自动刷新,monospace
    展示,warning 黄、error 红
- core/curator/api.ts + hooks.ts + types.ts:TanStack Query 包装
  status / config (GET/PUT) / run / logs

i18n:
- types.ts 新增 settings.skillEvolution.* 类型签名
- zh-CN.ts / en-US.ts:状态卡 / 配置表单 / 日志面板文案,
  以及 settings.skills.goToEvolution

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:00:54 +08:00
xuhdandClaude Opus 4.7 9b6b94d919 feat(skills): 自我进化后端 — curator 配置可编辑 + 运行日志可读
后端骨架:
- curator.py / usage.py / skill_tools.py:基于使用计数的生命周期管理
  + LLM 合并/归档建议(同事已有原型,集中提交)
- skill_evolution_config.py:CuratorConfig 嵌套模型
- app.py:启动时按间隔触发 curator 后台线程
- tools.py:开启自我进化时一并注册 skill_list / skill_view 工具
- skill_manage_tool.py:agent 创建技能时标记 source=agent

本次新增:
- skill_evolution_runtime.py:skill_evolution.json 读写,原子写入
- app_config.py:把 overrides 深合并到 AppConfig.skill_evolution,
  并把 overrides 的 mtime 纳入缓存失效条件,前端 PUT 后热生效
- curator_log.py:进程内 ring buffer (maxlen=200),所有 record/get
  操作内部兜异常,保证 logging 永远不会让 curator 跑挂
- curator.py 关键节点插桩:run_started / 自动转换计数 / 候选数 /
  LLM agent invoke / 解析结果 / archive 动作 / run complete
- routers/skills.py:GET/PUT /api/curator/config(写到
  skill_evolution.json + reset_app_config)+ GET /api/curator/logs

测试:
- tests/test_skill_evolution_runtime.py:overrides 往返、损坏 JSON
  容错、AppConfig 合并、ring buffer 线程安全、record 异常吞咽

杂项:
- .gitignore:忽略 .deer-flow/* 与 skills/* 等运行时目录
- uv.lock:跟随依赖锁更新
- 清理已废弃的 agent runtime 数据 SOUL.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:00:21 +08:00
董宏凯 8f4af5ece4 Merge branch 'dhk-2' into 'master'
添加提示词权限

See merge request knowledge-base-bj/deerflow-server!12
2026-05-14 03:52:27 +00:00
fengxiangdebaozi 2d9248889f Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-2 2026-05-14 11:51:51 +08:00
fengxiangdebaozi 804f37dcfa 提示词权限配置 2026-05-14 11:51:29 +08:00
董宏凯 bf98713a55 Merge branch 'dhk-2' into 'master'
推荐问题接口添加

See merge request knowledge-base-bj/deerflow-server!11
2026-05-14 03:08:14 +00:00
fengxiangdebaozi 2f2ba44c7c 添加推荐问题修改接口 2026-05-14 11:07:28 +08:00
fengxiangdebaozi 62292500ba Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-2 2026-05-14 10:52:08 +08:00
fengxiangdebaozi a7f1e89ce9 提示词修改 2026-05-14 10:51:29 +08:00
董宏凯 20c7e2841c Merge branch 'dhk-2' into 'master'
提示词修改

See merge request knowledge-base-bj/deerflow-server!10
2026-05-14 02:45:46 +00:00
fengxiangdebaozi c79f5c5a7a 样式修改 2026-05-14 10:42:37 +08:00
xu hd d3771ed31f Merge branch 'xuhd' into 'master'
feat: skill 名字允许下划线分隔

See merge request knowledge-base-bj/deerflow-server!9
2026-05-14 02:42:15 +00:00
xuhdandClaude Opus 4.7 230eec1baf feat: skill 名字允许下划线分隔
放宽 skill 命名校验:除连字符外也接受下划线作为分隔符,首尾和连续
分隔符的限制扩展到 _ 与混合分隔(--、__、-_、_- 都拒绝)。前置
frontmatter 校验与存储层正则同步放宽。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:41:37 +08:00
xu hd 0bae393e07 Merge branch 'xuhd' into 'master'
fix: /login/admin 自动把历史 admin 账户升级为 admin 角色

See merge request knowledge-base-bj/deerflow-server!8
2026-05-14 02:28:42 +00:00
xuhdandClaude Opus 4.7 434cf40b36 fix: /login/admin 自动把历史 admin 账户升级为 admin 角色
历史上以 username=admin 自动注册的账户曾被建成 system_role=user,
登录时按 username 修正一次,避免老库需要手工改表。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:25:24 +08:00
xu hd 7417a1c950 Merge branch 'xuhd' into 'master'
feat: admin可删除内置专家

See merge request knowledge-base-bj/deerflow-server!7
2026-05-14 02:18:33 +00:00
xu hd 799ccbe300 Merge branch 'xuhd' into 'master'
feat: admin可编辑内置专家 + agent列表Tab分组

See merge request knowledge-base-bj/deerflow-server!6
2026-05-14 02:06:42 +00:00
xuhdandClaude Opus 4.7 43044232bb feat: admin可删除内置专家
复用 _get_editable_or_raise 分支,给 AgentStore 增加 delete_builtin,
路由按 edit_mode 决定走 delete 还是 delete_builtin;前端 agent-card
为 admin 显示内置 agent 的删除按钮。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:53:13 +08:00
董宏凯 4567db8836 Merge branch 'dhk-2' into 'master'
页面结构调整

See merge request knowledge-base-bj/deerflow-server!5
2026-05-14 01:43:08 +00:00
fengxiangdebaozi e286e59431 页面结构调整 2026-05-14 09:42:12 +08:00
fengxiangdebaozi 65e89a4248 样式修改 2026-05-14 09:36:45 +08:00
fengxiangdebaozi db63086c38 文档修改 2026-05-14 09:35:15 +08:00
xuhdandClaude Sonnet 4.6 2abdf28eba feat: admin可编辑内置专家 + agent列表Tab分组
- /login/admin 登录时自动赋予 admin 角色,允许编辑内置专家
- agent列表页新增「全部」(内置+已发布) 和「自己的」两个Tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:16:18 +08:00
董宏凯 2476f04a26 Merge branch 'dhk-1' into 'master'
接口地址填写

See merge request knowledge-base-bj/deerflow-server!4
2026-05-13 14:29:34 +00:00
董宏凯 e458f1eab4 接口地址填写 2026-05-13 22:29:05 +08:00
fengxiangdebaozi b15b2649f8 文档修改 2026-05-13 22:22:59 +08:00
fengxiangdebaozi 4fce7ba09a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server 2026-05-13 22:19:02 +08:00
fengxiangdebaozi 1a1369f115 代码提交 2026-05-13 22:18:55 +08:00
董宏凯 9434ffabdc Merge branch 'dhk-1' into 'master'
Dhk 1

See merge request knowledge-base-bj/deerflow-server!3
2026-05-13 14:17:47 +00:00
董宏凯 6aeb428796 Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1 2026-05-13 22:17:25 +08:00
董宏凯 8cabf32ebe 提交 2026-05-13 22:16:59 +08:00
董宏凯 85a98fcfd6 Merge branch 'dhk-v3' into 'master'
ceshi1

See merge request knowledge-base-bj/deerflow-server!2
2026-05-13 14:09:07 +00:00
董宏凯 aa1f81a30a ceshi1 2026-05-13 22:08:37 +08:00
fengxiangdebaozi 1c50c73aa4 样式修改 2026-05-13 21:41:13 +08:00
fengxiangdebaozi 6cc28c7edd Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server 2026-05-13 21:25:40 +08:00
fengxiangdebaozi 1c2cf768f8 样式修改 2026-05-13 21:25:29 +08:00
吉鑫升 481e718d86 Merge remote-tracking branch 'origin/master' 2026-05-13 21:02:27 +08:00
吉鑫升andClaude Opus 4.7 29f74afbf6 fix(scheduler): give every scheduled run its own throwaway thread
All scheduled tasks for a user shared one scheduler thread, so each run
appended its messages to the same LangGraph checkpoint. Over time the
history blew past the model's input window and every task on that
account started failing with context-too-long.

_execute_task now mints a fresh thread_id per run and threads it through
ensure_thread_meta / snapshot / start_run / extract_*. The per-task
scheduler_thread_id column is left in place for backwards compatibility
but no longer drives the LLM call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:58:23 +08:00
fengxiangdebaozi 02111ab5b8 文档修改 2026-05-13 20:22:14 +08:00
吉鑫升 a2f7157bc3 chore: remove .claude/settings.local.json from tracking
Followup to previous commit which only landed the .gitignore rule but
left the file still tracked in the index.
2026-05-13 20:14:37 +08:00
吉鑫升 e6591abfbc chore: stop tracking .claude/settings.local.json
This file holds per-user Claude Code permission allowlists and should not
be shared across the team. Adding it to .gitignore and removing from the
index; existing working copies are preserved.
2026-05-13 20:14:00 +08:00
吉鑫升 50bda1ddee Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts:
#	frontend-web/src/components/workspace/agents/agent-card.tsx
#	frontend-web/src/pages/NewAgentPage.tsx
2026-05-13 20:10:21 +08:00
吉鑫升 31abad86d5 智能体配置 2026-05-13 20:04:06 +08:00
fengxiangdebaozi af7e90c87a Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server 2026-05-13 18:23:25 +08:00
fengxiangdebaozi 2e4bdf122e 页面结构修改 2026-05-13 18:18:05 +08:00
吉鑫升 83d7fa26fe Merge remote master docs 2026-05-13 17:28:04 +08:00
fengxiangdebaozi efc4d89fdf 文档 2026-05-13 17:17:46 +08:00
吉鑫升 a27206c543 Update local frontend and backend changes 2026-05-13 17:17:42 +08:00
吉鑫升 faca2c9a2d Merge branch 'likun' into master 2026-05-13 17:02:22 +08:00
fengxiangdebaozi 0cbd137b4f wendang1xiug1 2026-05-13 16:38:34 +08:00
likun cdf41d3e5e feat:完成技能化适配的意图识别功能 2026-05-13 16:01:00 +08:00
吉鑫升 aa034f8669 定时任务订阅模式 2026-05-13 14:59:27 +08:00
likun aee5a50b9f bug修复 2026-05-13 14:57:16 +08:00
likun b8926f00d7 新增用户意图确认功能 2026-05-13 14:36:44 +08:00
吉鑫升 839386f0b4 test: verify new remote push 2026-05-13 14:23:55 +08:00
吉鑫升 cf2e4bdd9c 智能体分权 2026-05-13 11:36:17 +08:00
吉鑫升 e7a455f22e Initial commit: deerflow-server (frontend-web + offline-backend-20260512) 2026-05-13 09:36:15 +08:00
413 changed files with 35233 additions and 713 deletions

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
# Workspace/editor
.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
# Local runtime logs and pid files
.runtime/
logs/
offline-backend-20260512/backend/logs/
# Frontend local/runtime artifacts
frontend-web/.env
frontend-web/.env.local
frontend-web/.env.*.local
frontend-web/node_modules/
frontend-web/.pnpm-store/
frontend-web/tsconfig.tsbuildinfo
frontend-web/vite-dev.log
frontend-web/vite-dev.err.log
# Frontend local packages/archives/docs (non-source)
frontend-web/pnpm-lock.yaml
frontend-web/README.md
frontend-web/doc.md
frontend-web/src.zip
frontend-web/src9.zip
frontend-web/src-v1.zip
frontend-web/src-*.rar
frontend-web/dist
# Frontend build artifacts
frontend-web/dist/
frontend-web/dist-scheduled-check/
# Offline backend bundles and local deployment artifacts
offline-backend-20260505.zip
offline-backend-20260505/output/
offline-backend-20260505/bundle/.env
offline-backend-20260505/bundle/.env.example
offline-backend-20260505/bundle/DEPLOY_BACKEND_OFFLINE_ZH.md
offline-backend-20260505/bundle/SHA256SUMS
offline-backend-20260505/bundle/config.yaml
offline-backend-20260505/bundle/config.yaml.example
offline-backend-20260505/bundle/data/
offline-backend-20260505/bundle/deerflow-backend-offline-image-20260505.tar
offline-backend-20260505/bundle/extensions_config.json
offline-backend-20260505/bundle/frontend-vite.zip
offline-backend-20260505/bundle/skills/
offline-backend-20260505/bundle/start_backend.0509.sh
offline-backend-20260505/bundle/start_backend.sh
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
offline-backend-20260512/data/
offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
offline-backend-20260512/extensions_config.json
offline-backend-20260512/skills/
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/backend/.env
offline-backend-20260512/backend/.env.local
offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh
hermes-agent-main/
skills/*
offline-backend-20260512/backend/.deer-flow/*
# Per-user skill_evolution overrides — runtime data written by the API
offline-backend-20260512/backend/skill_evolution.json
skills/
hermes-agent-main/
.gitignore
offline-backend-20260512/backend/tests/*
# 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_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 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 vLLM reasoning/reasoning_content field compatibility regressions versioned.
!offline-backend-20260512/backend/tests/test_reasoning_fields.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_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
# 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_compact_menu_overrides.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
# 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
# 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/
# uv lockfile (not tracked per project preference)
offline-backend-20260512/backend/uv.lock
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
# Local-only scratch directory (not committed)
0622/
# Python bytecode cache
__pycache__/
# misc
.DS_Store
+1 -1
View File
@@ -96,7 +96,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **智能体使用引导 (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>` 窗口事件强制重放(忽略已看过记忆)。 - **智能体使用引导 (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. - **敏感词管理 (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/`。 - **技能地址管理 (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 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 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`. - **按钮管理 (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` - **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 - **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/` - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
+1
View File
@@ -83,6 +83,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps) - **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
- **定时任务「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. - **定时任务「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`. - **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). - **轻应用中心 (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`. - **菜单管理 (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`. - **白名单管理 (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`.
-3
View File
@@ -1,3 +0,0 @@
# deerflow-code
deerflow-code
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.
+1 -1
View File
@@ -18,7 +18,7 @@ VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。 # 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=true VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口 # 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
+3
View File
@@ -59,6 +59,9 @@
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。 # 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。 # 不填或留空时按类型 A,继续使用原来的简介菜单数据。
# VITE_COMPACT_MENU_TYPE=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= # VITE_GITHUB_OAUTH_TOKEN=
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)--- # --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
+5
View File
@@ -151,6 +151,11 @@ window.__CM_RUNTIME_CONFIG__ = {
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。 // GitHub OAuth Token;不使用 GitHub 相关能力时留空。
VITE_GITHUB_OAUTH_TOKEN: "", VITE_GITHUB_OAUTH_TOKEN: "",
// 是否在问答输入框「模式」菜单显示「极速」:"true" 显示,"false" 隐藏。
// 默认关闭。开启后可直连大模型流式问答,不走智能体工具与技能。
// 部署后改 dist/runtime-config.js 再刷新页面即可,无需重新打包。
VITE_TURBO_MODE_VISIBLE: "true",
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。 // 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。 // 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
VITE_SENTIMENT_AGENT_VISIBLE: "true", VITE_SENTIMENT_AGENT_VISIBLE: "true",
@@ -51,6 +51,10 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs"; import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useAgentsPaginated } from "@/core/agents"; import { useAgentsPaginated } from "@/core/agents";
import { getAPIClient } from "@/core/api"; import { getAPIClient } from "@/core/api";
@@ -69,6 +73,7 @@ import {
useToggleThreadPin, useToggleThreadPin,
} from "@/core/threads/hooks"; } from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types"; import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import { import {
bootstrapResumePath, bootstrapResumePath,
isBootstrapThread, isBootstrapThread,
@@ -479,6 +484,7 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
) : ( ) : (
filteredThreads.map((thread) => { filteredThreads.map((thread) => {
const isBoot = isBootstrapThread(thread); const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread); const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread);
const isActive = href === pathname; const isActive = href === pathname;
const pinned = isThreadPinned(thread); const pinned = isThreadPinned(thread);
@@ -505,6 +511,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
)} )}
aria-label="智能体创建会话" aria-label="智能体创建会话"
/> />
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : ( ) : (
<MessageSquare <MessageSquare
className={cn( className={cn(
@@ -516,9 +524,15 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
{pinned && ( {pinned && (
<Pin className="text-muted-foreground/70 size-3 shrink-0" /> <Pin className="text-muted-foreground/70 size-3 shrink-0" />
)} )}
<span className="truncate"> <span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))} {desensitize(titleOfThread(thread, t.pages.newChat))}
</span> </span>
{projectType && (
<MyProjectThreadBadge
className="mr-4"
type={projectType}
/>
)}
{isActive && ( {isActive && (
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" /> <span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
)} )}
@@ -4,7 +4,12 @@ import { MessageSquareIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useThreadsPage } from "@/core/threads/hooks"; import { useThreadsPage } from "@/core/threads/hooks";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import { pathOfThread, titleOfThread, isHiddenFromRecentChats, isBootstrapThread } from "@/core/threads/utils"; import { pathOfThread, titleOfThread, isHiddenFromRecentChats, isBootstrapThread } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize"; import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -45,6 +50,7 @@ export function AgentRecentChats({ agentId }: { agentId: string }) {
{threads.map((thread) => { {threads.map((thread) => {
const href = pathOfThread(thread); const href = pathOfThread(thread);
const isActive = href === pathname; const isActive = href === pathname;
const projectType = myProjectConversationTypeOfThread(thread);
return ( return (
<Link <Link
key={thread.thread_id} key={thread.thread_id}
@@ -57,15 +63,20 @@ export function AgentRecentChats({ agentId }: { agentId: string }) {
: "text-muted-foreground hover:bg-accent hover:text-foreground", : "text-muted-foreground hover:bg-accent hover:text-foreground",
)} )}
> >
<MessageSquareIcon {projectType ? (
className={cn( <MyProjectThreadIcon type={projectType} />
"size-3.5 shrink-0", ) : (
isActive ? "text-primary" : "text-muted-foreground/70", <MessageSquareIcon
)} className={cn(
/> "size-3.5 shrink-0",
<span className="truncate"> isActive ? "text-primary" : "text-muted-foreground/70",
)}
/>
)}
<span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, "新对话"))} {desensitize(titleOfThread(thread, "新对话"))}
</span> </span>
{projectType && <MyProjectThreadBadge type={projectType} />}
</Link> </Link>
); );
})} })}
@@ -60,6 +60,10 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs"; import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useAgentsPaginated, type Agent } from "@/core/agents"; import { useAgentsPaginated, type Agent } from "@/core/agents";
import { getAPIClient } from "@/core/api"; import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
@@ -75,6 +79,7 @@ import {
useToggleThreadPin, useToggleThreadPin,
} from "@/core/threads/hooks"; } from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types"; import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import { import {
isBootstrapThread, isBootstrapThread,
isHiddenFromRecentChats, isHiddenFromRecentChats,
@@ -483,6 +488,7 @@ export function GeneralQaSidebar() {
); );
const isActive = href === pathname; const isActive = href === pathname;
const isBoot = isBootstrapThread(thread); const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const pinned = isThreadPinned(thread); const pinned = isThreadPinned(thread);
const favorited = isThreadFavorite(thread); const favorited = isThreadFavorite(thread);
return ( return (
@@ -507,6 +513,8 @@ export function GeneralQaSidebar() {
)} )}
aria-label="智能体创建会话" aria-label="智能体创建会话"
/> />
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : ( ) : (
<MessageSquare <MessageSquare
className={cn( className={cn(
@@ -518,9 +526,15 @@ export function GeneralQaSidebar() {
{pinned && ( {pinned && (
<Pin className="size-3 shrink-0 text-muted-foreground/70" /> <Pin className="size-3 shrink-0 text-muted-foreground/70" />
)} )}
<span className="truncate"> <span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))} {desensitize(titleOfThread(thread, t.pages.newChat))}
</span> </span>
{projectType && (
<MyProjectThreadBadge
className="mr-4"
type={projectType}
/>
)}
{isActive && ( {isActive && (
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" /> <span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
)} )}
@@ -7,6 +7,7 @@ import {
CheckIcon, CheckIcon,
FileTextIcon, FileTextIcon,
FolderTreeIcon, FolderTreeIcon,
GaugeIcon,
GraduationCapIcon, GraduationCapIcon,
LightbulbIcon, LightbulbIcon,
PaperclipIcon, PaperclipIcon,
@@ -68,6 +69,7 @@ import { isHiddenFromUIMessage } from "@/core/messages/utils";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import type { Model } from "@/core/models/types"; import type { Model } from "@/core/models/types";
import type { AgentThreadContext } from "@/core/threads"; import type { AgentThreadContext } from "@/core/threads";
import { isTurboModeVisible } from "@/core/threads/use-direct-chat";
import { textOfMessage } from "@/core/threads/utils"; import { textOfMessage } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize"; import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -223,7 +225,11 @@ export function InputBox({
context.execution_profile === "single_round_rag"; context.execution_profile === "single_round_rag";
const multiRoundRagActive = const multiRoundRagActive =
context.execution_profile === "multi_round_rag"; context.execution_profile === "multi_round_rag";
const turboModeVisible = isTurboModeVisible();
const directChatActive =
turboModeVisible && context.execution_profile === "direct";
const frontendRagActive = singleRoundRagActive || multiRoundRagActive; const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
const bypassAgentActive = frontendRagActive || directChatActive;
const modelOptions = useMemo(() => { const modelOptions = useMemo(() => {
return singleRoundRagActive ? extraModels : models; return singleRoundRagActive ? extraModels : models;
}, [extraModels, models, singleRoundRagActive]); }, [extraModels, models, singleRoundRagActive]);
@@ -253,11 +259,37 @@ export function InputBox({
recommendedQuestions.length > 0; recommendedQuestions.length > 0;
useEffect(() => { useEffect(() => {
// Multi-round RAG is a frontend execution profile, not a model option. // Frontend-only execution profiles are not model options.
// Do not let the normal model fallback clear it on every render. // Do not let the normal model fallback clear them on every render.
if (!turboModeVisible && context.execution_profile === "direct") {
onContextChange?.({
...context,
execution_profile: undefined,
});
return;
}
if (multiRoundRagActive) { if (multiRoundRagActive) {
return; return;
} }
if (directChatActive) {
if (models.length === 0) {
return;
}
const currentModel = models.find((m) => m.name === context.model_name);
const fallbackModel = currentModel ?? models[0]!;
if (
context.model_name === fallbackModel.name &&
context.execution_profile === "direct"
) {
return;
}
onContextChange?.({
...context,
model_name: fallbackModel.name,
execution_profile: "direct",
});
return;
}
if (modelOptions.length === 0) { if (modelOptions.length === 0) {
return; return;
} }
@@ -281,7 +313,7 @@ export function InputBox({
mode: nextMode, mode: nextMode,
execution_profile: fallbackModel.execution_profile, execution_profile: fallbackModel.execution_profile,
}); });
}, [context, modelOptions, multiRoundRagActive, onContextChange]); }, [context, directChatActive, modelOptions, models, multiRoundRagActive, onContextChange, turboModeVisible]);
const selectedModel = useMemo(() => { const selectedModel = useMemo(() => {
if (modelOptions.length === 0) { if (modelOptions.length === 0) {
@@ -314,7 +346,10 @@ export function InputBox({
model_name, model_name,
mode: getResolvedMode(context.mode, model.supports_thinking ?? false), mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
reasoning_effort: context.reasoning_effort, reasoning_effort: context.reasoning_effort,
execution_profile: model.execution_profile, execution_profile:
context.execution_profile === "direct"
? "direct"
: model.execution_profile,
}); });
setModelDialogOpen(false); setModelDialogOpen(false);
}, },
@@ -346,6 +381,16 @@ export function InputBox({
[onContextChange, context, models, supportThinking], [onContextChange, context, models, supportThinking],
); );
const handleDirectChatSelect = useCallback(() => {
const ordinaryModel =
models.find((model) => model.name === context.model_name) ?? models[0];
onContextChange?.({
...context,
model_name: ordinaryModel?.name ?? context.model_name,
execution_profile: "direct",
});
}, [context, models, onContextChange]);
const handleSingleRoundRagSelect = useCallback(() => { const handleSingleRoundRagSelect = useCallback(() => {
const ragModel = const ragModel =
extraModels.find((model) => model.name === context.model_name) ?? extraModels.find((model) => model.name === context.model_name) ??
@@ -749,7 +794,7 @@ export function InputBox({
/> />
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> */} </PromptInputActionMenu> */}
{!frontendRagActive && ( {!bypassAgentActive && (
<AddAttachmentsButton <AddAttachmentsButton
className={cn( className={cn(
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", "rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
@@ -758,7 +803,7 @@ export function InputBox({
)} )}
/> />
)} )}
{!frontendRagActive && !hideContextTools && <Tooltip {!bypassAgentActive && !hideContextTools && <Tooltip
content={ content={
context.writing_mode context.writing_mode
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料" ? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
@@ -784,7 +829,7 @@ export function InputBox({
<span className="text-xs font-normal">写作</span> <span className="text-xs font-normal">写作</span>
</PromptInputButton> </PromptInputButton>
</Tooltip>} </Tooltip>}
{!frontendRagActive && <Tooltip {!bypassAgentActive && <Tooltip
content={ content={
artifactDirectoryModeEnabled artifactDirectoryModeEnabled
? "文件模式已开启:右侧会显示当前对话生成的全部文件目录" ? "文件模式已开启:右侧会显示当前对话生成的全部文件目录"
@@ -813,7 +858,7 @@ export function InputBox({
<span className="text-xs font-normal">文件</span> <span className="text-xs font-normal">文件</span>
</PromptInputButton> </PromptInputButton>
</Tooltip>} </Tooltip>}
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip {!bypassAgentActive && !hideContextTools && !hideMemoryTool && <Tooltip
content={ content={
memoryInjectionEnabled memoryInjectionEnabled
? "记忆注入已开启:对话前会注入历史记忆" ? "记忆注入已开启:对话前会注入历史记忆"
@@ -839,7 +884,7 @@ export function InputBox({
<span className="text-xs font-normal">记忆</span> <span className="text-xs font-normal">记忆</span>
</PromptInputButton> </PromptInputButton>
</Tooltip>} </Tooltip>}
{referenceModeEntryEnabled && !frontendRagActive && !hideContextTools && <Tooltip {referenceModeEntryEnabled && !bypassAgentActive && !hideContextTools && <Tooltip
content={ content={
ragModeEnabled ragModeEnabled
? "参考文献模式已开启:检索类结果会按编号引用展示" ? "参考文献模式已开启:检索类结果会按编号引用展示"
@@ -869,7 +914,7 @@ export function InputBox({
<span className="text-xs font-normal">参考文献</span> <span className="text-xs font-normal">参考文献</span>
</PromptInputButton> </PromptInputButton>
</Tooltip>} </Tooltip>}
{!frontendRagActive && <Tooltip {!bypassAgentActive && <Tooltip
content={ content={
contextCompressionEnabled contextCompressionEnabled
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token" ? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
@@ -909,7 +954,9 @@ export function InputBox({
: "flash" : "flash"
} }
tooltipContent={ tooltipContent={
singleRoundRagActive directChatActive
? `${t.inputBox.turboMode}:${t.inputBox.turboModeDescription}`
: singleRoundRagActive
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料" ? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
: multiRoundRagActive : multiRoundRagActive
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料" ? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
@@ -921,7 +968,9 @@ export function InputBox({
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", "gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium", compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
compact && compact &&
(frontendRagActive (directChatActive
? "border-sky-200 bg-sky-50/60 text-sky-700 hover:bg-sky-100 dark:border-sky-700 dark:bg-sky-950/40 dark:text-sky-200"
: frontendRagActive
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200" ? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
: context.mode === "thinking" : context.mode === "thinking"
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200" ? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
@@ -933,7 +982,9 @@ export function InputBox({
)} )}
> >
<div> <div>
{frontendRagActive ? ( {directChatActive ? (
<GaugeIcon className="size-3" />
) : frontendRagActive ? (
<BookMarkedIcon className="size-3" /> <BookMarkedIcon className="size-3" />
) : context.mode === "flash" ? ( ) : context.mode === "flash" ? (
<ZapIcon className="size-3" /> <ZapIcon className="size-3" />
@@ -948,10 +999,14 @@ export function InputBox({
<div <div
className={cn( className={cn(
"text-xs font-normal", "text-xs font-normal",
context.mode === "ultra" ? "golden-text" : "", context.mode === "ultra" && !directChatActive && !frontendRagActive
? "golden-text"
: "",
)} )}
> >
{frontendRagActive {directChatActive
? t.inputBox.turboMode
: frontendRagActive
? multiRoundRagActive ? multiRoundRagActive
? "多轮 RAG" ? "多轮 RAG"
: "单轮 RAG" : "单轮 RAG"
@@ -969,9 +1024,39 @@ export function InputBox({
{t.inputBox.mode} {t.inputBox.mode}
</DropdownMenuLabel> </DropdownMenuLabel>
<PromptInputActionMenu> <PromptInputActionMenu>
{turboModeVisible && (
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "flash" directChatActive
? "bg-sky-50 text-sky-800 hover:bg-sky-100 focus:bg-sky-100 dark:bg-sky-950/40 dark:text-sky-200 dark:hover:bg-sky-950/60 dark:focus:bg-sky-950/60"
: "text-muted-foreground/65",
)}
onSelect={handleDirectChatSelect}
>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<GaugeIcon
className={cn(
"mr-2 size-4",
directChatActive && "text-sky-700 dark:text-sky-200",
)}
/>
{t.inputBox.turboMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.turboModeDescription}
</div>
</div>
{directChatActive ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
)}
<PromptInputActionMenuItem
className={cn(
!bypassAgentActive && context.mode === "flash"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -982,7 +1067,7 @@ export function InputBox({
<ZapIcon <ZapIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "flash" && !bypassAgentActive && context.mode === "flash" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
@@ -992,7 +1077,7 @@ export function InputBox({
{t.inputBox.flashModeDescription} {t.inputBox.flashModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && context.mode === "flash" ? ( {!bypassAgentActive && context.mode === "flash" ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
@@ -1001,7 +1086,7 @@ export function InputBox({
{supportThinking && ( {supportThinking && (
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "thinking" !bypassAgentActive && context.mode === "thinking"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -1012,7 +1097,7 @@ export function InputBox({
<LightbulbIcon <LightbulbIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "thinking" && !bypassAgentActive && context.mode === "thinking" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
@@ -1022,7 +1107,7 @@ export function InputBox({
{t.inputBox.reasoningModeDescription} {t.inputBox.reasoningModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && context.mode === "thinking" ? ( {!bypassAgentActive && context.mode === "thinking" ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
@@ -1031,7 +1116,7 @@ export function InputBox({
)} )}
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "pro" !bypassAgentActive && context.mode === "pro"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -1042,7 +1127,7 @@ export function InputBox({
<GraduationCapIcon <GraduationCapIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && context.mode === "pro" && !bypassAgentActive && context.mode === "pro" &&
"text-accent-foreground", "text-accent-foreground",
)} )}
/> />
@@ -1052,7 +1137,7 @@ export function InputBox({
{t.inputBox.proModeDescription} {t.inputBox.proModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && context.mode === "pro" ? ( {!bypassAgentActive && context.mode === "pro" ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
@@ -1060,7 +1145,7 @@ export function InputBox({
</PromptInputActionMenuItem> </PromptInputActionMenuItem>
<PromptInputActionMenuItem <PromptInputActionMenuItem
className={cn( className={cn(
!frontendRagActive && context.mode === "ultra" !bypassAgentActive && context.mode === "ultra"
? "text-accent-foreground" ? "text-accent-foreground"
: "text-muted-foreground/65", : "text-muted-foreground/65",
)} )}
@@ -1071,14 +1156,14 @@ export function InputBox({
<RocketIcon <RocketIcon
className={cn( className={cn(
"mr-2 size-4", "mr-2 size-4",
!frontendRagActive && !bypassAgentActive &&
context.mode === "ultra" && context.mode === "ultra" &&
"text-[#dabb5e]", "text-[#dabb5e]",
)} )}
/> />
<div <div
className={cn( className={cn(
!frontendRagActive && !bypassAgentActive &&
context.mode === "ultra" && context.mode === "ultra" &&
"golden-text", "golden-text",
)} )}
@@ -1090,7 +1175,7 @@ export function InputBox({
{t.inputBox.ultraModeDescription} {t.inputBox.ultraModeDescription}
</div> </div>
</div> </div>
{!frontendRagActive && context.mode === "ultra" ? ( {!bypassAgentActive && context.mode === "ultra" ? (
<CheckIcon className="ml-auto size-4" /> <CheckIcon className="ml-auto size-4" />
) : ( ) : (
<div className="ml-auto size-4" /> <div className="ml-auto size-4" />
@@ -1161,7 +1246,7 @@ export function InputBox({
</DropdownMenuGroup> </DropdownMenuGroup>
</PromptInputActionMenuContent> </PromptInputActionMenuContent>
</PromptInputActionMenu> </PromptInputActionMenu>
{!frontendRagActive && supportReasoningEffort && context.mode !== "flash" && ( {!bypassAgentActive && supportReasoningEffort && context.mode !== "flash" && (
<PromptInputActionMenu> <PromptInputActionMenu>
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}> <PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
<div className="text-xs font-normal"> <div className="text-xs font-normal">
@@ -1279,7 +1364,7 @@ export function InputBox({
)} )}
</PromptInputTools> </PromptInputTools>
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}> <PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
{!frontendRagActive && agentSelector && ( {!bypassAgentActive && agentSelector && (
<AgentSelector <AgentSelector
selectedAgentId={agentSelector.selectedAgentId} selectedAgentId={agentSelector.selectedAgentId}
onAgentChange={agentSelector.onAgentChange} onAgentChange={agentSelector.onAgentChange}
@@ -1287,8 +1372,8 @@ export function InputBox({
compact={compact} compact={compact}
/> />
)} )}
{!frontendRagActive && knowledgeSelector} {!bypassAgentActive && knowledgeSelector}
{!frontendRagActive && promptPrefix && ( {!bypassAgentActive && promptPrefix && (
<Tooltip <Tooltip
content={ content={
promptPrefix.on promptPrefix.on
@@ -25,6 +25,7 @@ export function ModelNameSelect({
className, className,
triggerClassName, triggerClassName,
defaultLabel = "默认模型", defaultLabel = "默认模型",
title = "选择用于本次操作的大模型(默认使用系统配置的模型)",
}: { }: {
value: string | null; value: string | null;
onChange: (modelName: string | null) => void; onChange: (modelName: string | null) => void;
@@ -32,6 +33,7 @@ export function ModelNameSelect({
className?: string; className?: string;
triggerClassName?: string; triggerClassName?: string;
defaultLabel?: string; defaultLabel?: string;
title?: string;
}) { }) {
const { models } = useModels(); const { models } = useModels();
return ( return (
@@ -42,7 +44,7 @@ export function ModelNameSelect({
> >
<SelectTrigger <SelectTrigger
className={cn("h-8 gap-1.5 text-xs", triggerClassName, className)} className={cn("h-8 gap-1.5 text-xs", triggerClassName, className)}
title="选择用于本次操作的大模型(默认使用系统配置的模型)" title={title}
> >
<SelectValue placeholder={defaultLabel} /> <SelectValue placeholder={defaultLabel} />
</SelectTrigger> </SelectTrigger>
@@ -0,0 +1,51 @@
import { FolderKanban } from "lucide-react";
import {
MY_PROJECT_CONVERSATION_LABELS,
type MyProjectConversationType,
} from "@/core/threads/project-metadata";
import { cn } from "@/lib/utils";
const BADGE_STYLES: Record<MyProjectConversationType, string> = {
qa: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-700/70 dark:bg-sky-950/55 dark:text-sky-300",
strategy:
"border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-700/70 dark:bg-violet-950/55 dark:text-violet-300",
solution:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-700/70 dark:bg-emerald-950/55 dark:text-emerald-300",
};
export function MyProjectThreadIcon({
type,
className,
}: {
type: MyProjectConversationType;
className?: string;
}) {
return (
<FolderKanban
aria-label={`我的项目 · ${MY_PROJECT_CONVERSATION_LABELS[type]}`}
className={cn("size-3.5 shrink-0 text-violet-500", className)}
/>
);
}
export function MyProjectThreadBadge({
type,
className,
}: {
type: MyProjectConversationType;
className?: string;
}) {
return (
<span
className={cn(
"shrink-0 rounded border px-1 py-0.5 text-[9px] font-medium leading-none",
BADGE_STYLES[type],
className,
)}
title={`来自我的项目 · ${MY_PROJECT_CONVERSATION_LABELS[type]}`}
>
{MY_PROJECT_CONVERSATION_LABELS[type]}
</span>
);
}
@@ -50,6 +50,10 @@ import {
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs"; import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { getAPIClient } from "@/core/api"; import { getAPIClient } from "@/core/api";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { import {
@@ -64,6 +68,7 @@ import {
useToggleThreadPin, useToggleThreadPin,
} from "@/core/threads/hooks"; } from "@/core/threads/hooks";
import type { AgentThread, AgentThreadState } from "@/core/threads/types"; import type { AgentThread, AgentThreadState } from "@/core/threads/types";
import { myProjectConversationTypeOfThread } from "@/core/threads/project-metadata";
import { import {
isBootstrapThread, isBootstrapThread,
isHiddenFromRecentChats, isHiddenFromRecentChats,
@@ -246,6 +251,7 @@ export function RecentChatList({
const href = listPathOfThread(thread); const href = listPathOfThread(thread);
const isActive = href === pathname; const isActive = href === pathname;
const isBoot = isBootstrapThread(thread); const isBoot = isBootstrapThread(thread);
const projectType = myProjectConversationTypeOfThread(thread);
const pinned = isThreadPinned(thread); const pinned = isThreadPinned(thread);
const favorited = isThreadFavorite(thread); const favorited = isThreadFavorite(thread);
return ( return (
@@ -279,6 +285,8 @@ export function RecentChatList({
} }
aria-label="智能体创建会话" aria-label="智能体创建会话"
/> />
) : projectType ? (
<MyProjectThreadIcon type={projectType} />
) : ( ) : (
compact && ( compact && (
<MessageSquare <MessageSquare
@@ -294,9 +302,15 @@ export function RecentChatList({
{pinned && ( {pinned && (
<Pin className="size-3 shrink-0 text-muted-foreground/70" /> <Pin className="size-3 shrink-0 text-muted-foreground/70" />
)} )}
<span className="truncate"> <span className="min-w-0 flex-1 truncate">
{desensitize(titleOfThread(thread, t.pages.newChat))} {desensitize(titleOfThread(thread, t.pages.newChat))}
</span> </span>
{projectType && (
<MyProjectThreadBadge
className="mr-3"
type={projectType}
/>
)}
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && ( {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
@@ -69,7 +69,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
// }, // },
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon }, { id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
...(isAdmin ...(isAdmin
? [{ id: "encoder", label: "本地编码器", icon: CpuIcon }] ? [{ id: "encoder", label: "Wiki 镜像编码器", icon: CpuIcon }]
: []), : []),
{ {
id: "account", id: "account",
@@ -14,7 +14,7 @@ import {
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react"; import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, FolderKanban, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
import { KnowledgeRagToggle } from "./knowledge-rag-toggle"; import { KnowledgeRagToggle } from "./knowledge-rag-toggle";
import { SidebarCollapseButton } from "./workspace-header"; import { SidebarCollapseButton } from "./workspace-header";
@@ -24,7 +24,10 @@ import {
applyCompactMenuOverrides, applyCompactMenuOverrides,
type CompactMenuItem, type CompactMenuItem,
} from "@/core/page-layout/compact-menu-overrides"; } from "@/core/page-layout/compact-menu-overrides";
import { type MenuItem } from "@/core/page-layout/sidebar-menu"; import {
MARKDOWN_DOCX_MENU_ITEM,
type MenuItem,
} from "@/core/page-layout/sidebar-menu";
import { useCompactMenuOverrides } from "@/strategy-components/api/compact-menu-overrides"; import { useCompactMenuOverrides } from "@/strategy-components/api/compact-menu-overrides";
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标 // 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
@@ -65,6 +68,15 @@ export const WORKSPACE_COMPACT_NAV_ITEMS: WorkspaceCompactNavItem[] = [
path: "/page/workspace/chats", path: "/page/workspace/chats",
match: (pathname) => pathname === "/page/workspace/chats", match: (pathname) => pathname === "/page/workspace/chats",
}, },
{
id: "strategy-projects",
label: "我的项目",
icon: FolderKanban,
path: "/page/strategy/qa/projects",
match: (pathname) =>
pathname.startsWith("/page/strategy/qa/projects") ||
pathname.startsWith("/page/strategy/qa/strategy-selection"),
},
{ {
id: "workspace-library", id: "workspace-library",
label: "资料库", label: "资料库",
@@ -140,7 +152,13 @@ export const WORKSPACE_COMPACT_NAV_ITEMS: WorkspaceCompactNavItem[] = [
label: "知识库管理", label: "知识库管理",
icon: BookOpen, icon: BookOpen,
path: "/page/workspace/knowledge", path: "/page/workspace/knowledge",
match: (pathname) => pathname.startsWith("/page/workspace/knowledge"), match: (pathname) =>
pathname.startsWith("/page/workspace/knowledge") &&
!pathname.startsWith(MARKDOWN_DOCX_MENU_ITEM.path),
},
{
...MARKDOWN_DOCX_MENU_ITEM,
match: (pathname) => pathname.startsWith(MARKDOWN_DOCX_MENU_ITEM.path),
}, },
]; ];
+8 -5
View File
@@ -109,8 +109,8 @@ export interface TaskButton {
/** 追加的查询参数名(如 ``id`` / ``task_id``);空则用 ``id``。 */ /** 追加的查询参数名(如 ``id`` / ``task_id``);空则用 ``id``。 */
idParam: string; idParam: string;
/** /**
* ``url`` 追加 id 时取任务id 还是行动id。仅 xdfx(行动分析)二者不同:深链原始 ``?taskId=`` * ``url`` / ``host`` 追加 id 时取任务id 还是行动id。仅 xdfx(行动分析)二者不同:深链原始
* 实为行动id,任务id 由 action 接口解析。其它业务两者相同。 * ``?taskId=`` 实为行动id,任务id 由 action 接口解析。其它业务两者相同。
*/ */
idKind: TaskButtonIdKind; idKind: TaskButtonIdKind;
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */ /** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */
@@ -327,7 +327,8 @@ export interface RunTaskButtonCtx {
* 执行一个跳转按钮: * 执行一个跳转按钮:
* - ``url``:拼地址(可选追加 ``?id=``,取任务id 或行动id 视 ``idKind``)新标签打开;地址为空提示未配置。 * - ``url``:拼地址(可选追加 ``?id=``,取任务id 或行动id 视 ``idKind``)新标签打开;地址为空提示未配置。
* - ``business``:复用深链解析(会按目标业务重绑任务上下文)后 ``navigate``(始终用任务id)。 * - ``business``:复用深链解析(会按目标业务重绑任务上下文)后 ``navigate``(始终用任务id)。
* - ``host``:向 ``window.parent`` 发 ``HOST_NAVIGATE``,由宿主系统跳转(仅 iframe 嵌入时生效)。 * - ``host``:向 ``window.parent`` 发 ``HOST_NAVIGATE``,由宿主系统跳转(仅 iframe 嵌入时生效;
* 追加 id 时同样按 ``idKind`` 取任务id / 行动id)。
*/ */
export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Promise<void> { export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Promise<void> {
const fail = (m: string) => ctx.onError?.(m); const fail = (m: string) => ctx.onError?.(m);
@@ -355,9 +356,11 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
} }
const queryParams: Record<string, number> = {}; const queryParams: Record<string, number> = {};
if (btn.appendTaskId) { if (btn.appendTaskId) {
const id = parseHostTaskId(ctx.taskId); // 行动分析(xdfx)可选发行动id;与 url 外链同规则:行动id 缺失时回退任务id。
const raw = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
const id = parseHostTaskId(raw);
if (id === null) { if (id === null) {
fail("任务id不是数字,无法跳转宿主系统"); fail(`${btn.idKind === "action" ? "行动id" : "任务id"}不是数字,无法跳转宿主系统`);
return; return;
} }
queryParams[(btn.idParam || "id").trim() || "id"] = id; queryParams[(btn.idParam || "id").trim() || "id"] = id;
@@ -96,6 +96,9 @@ export const enUS: Translations = {
mode: "Mode", mode: "Mode",
flashMode: "Flash", flashMode: "Flash",
flashModeDescription: "Fast and efficient, but may not be accurate", flashModeDescription: "Fast and efficient, but may not be accurate",
turboMode: "Turbo",
turboModeDescription:
"Talk to the model directly with no agent tools or skills, for lower first-token latency",
reasoningMode: "Reasoning", reasoningMode: "Reasoning",
reasoningModeDescription: reasoningModeDescription:
"Reasoning before action, balance between time and accuracy", "Reasoning before action, balance between time and accuracy",
@@ -91,6 +91,8 @@ export interface Translations {
mode: string; mode: string;
flashMode: string; flashMode: string;
flashModeDescription: string; flashModeDescription: string;
turboMode: string;
turboModeDescription: string;
reasoningMode: string; reasoningMode: string;
reasoningModeDescription: string; reasoningModeDescription: string;
proMode: string; proMode: string;
@@ -95,6 +95,8 @@ export const zhCN: Translations = {
mode: "模式", mode: "模式",
flashMode: "快速", flashMode: "快速",
flashModeDescription: "快速且高效的完成任务,但可能不够精准", flashModeDescription: "快速且高效的完成任务,但可能不够精准",
turboMode: "极速",
turboModeDescription: "直连大模型流式问答,不走智能体工具与技能,首字更快",
reasoningMode: "思考", reasoningMode: "思考",
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡", reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
proMode: "专业问答", proMode: "专业问答",
+13 -5
View File
@@ -364,11 +364,16 @@ export function extractReasoningContentFromMessage(message: Message) {
if (message.type !== "ai") { if (message.type !== "ai") {
return null; return null;
} }
if ( if (message.additional_kwargs) {
message.additional_kwargs && const reasoningContent = message.additional_kwargs.reasoning_content;
"reasoning_content" in message.additional_kwargs if (typeof reasoningContent === "string") {
) { return reasoningContent;
return message.additional_kwargs.reasoning_content as string | null; }
// Newer vLLM puts thinking in `reasoning` instead of `reasoning_content`.
const reasoning = message.additional_kwargs.reasoning;
if (typeof reasoning === "string") {
return reasoning;
}
} }
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {
const part = message.content[0]; const part = message.content[0];
@@ -424,6 +429,9 @@ export function hasReasoning(message: Message) {
if (typeof message.additional_kwargs?.reasoning_content === "string") { if (typeof message.additional_kwargs?.reasoning_content === "string") {
return true; return true;
} }
if (typeof message.additional_kwargs?.reasoning === "string") {
return true;
}
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {
const part = message.content[0]; const part = message.content[0];
// Compatible with the Anthropic gateway // Compatible with the Anthropic gateway
+1 -1
View File
@@ -7,7 +7,7 @@ export interface Model {
supports_thinking?: boolean; supports_thinking?: boolean;
supports_reasoning_effort?: boolean; supports_reasoning_effort?: boolean;
/** Execution profiles handled by the frontend instead of DeerFlow. */ /** Execution profiles handled by the frontend instead of DeerFlow. */
execution_profile?: "single_round_rag" | "multi_round_rag"; execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
} }
export interface TokenUsageSettings { export interface TokenUsageSettings {
@@ -21,6 +21,7 @@ import {
FileBox, FileBox,
FileSearch, FileSearch,
FileText, FileText,
FolderKanban,
Gauge, Gauge,
Grid3X3, Grid3X3,
HelpCircle, HelpCircle,
@@ -48,12 +49,23 @@ export type MenuItem = {
path?: string; path?: string;
/** 额外视为「当前页属于本菜单项」的路由(不在侧栏展示,仅用于高亮)。 */ /** 额外视为「当前页属于本菜单项」的路由(不在侧栏展示,仅用于高亮)。 */
aliasPaths?: string[]; aliasPaths?: string[];
/** 从当前菜单项自身的 path / aliasPaths 前缀匹配中排除的路由。 */
excludePaths?: string[];
/** 非 admin 用户使用的路由(菜单对普通用户可见时生效,与简洁模式公开排行榜一致) */ /** 非 admin 用户使用的路由(菜单对普通用户可见时生效,与简洁模式公开排行榜一致) */
userPath?: string; userPath?: string;
adminOnly?: boolean; adminOnly?: boolean;
children?: MenuItem[]; children?: MenuItem[];
}; };
/** Markdown 转 Word 功能的菜单定义,供全量与简洁模式共用。 */
export const MARKDOWN_DOCX_MENU_PATH = "/page/workspace/knowledge/markdown-editor";
export const MARKDOWN_DOCX_MENU_ITEM: MenuItem & { path: string } = {
id: "resource-markdown-docx-editor",
label: "md转word",
icon: FileText,
path: MARKDOWN_DOCX_MENU_PATH,
};
export const sidebarMenuItems: MenuItem[] = [ export const sidebarMenuItems: MenuItem[] = [
{ {
id: "qa-center", id: "qa-center",
@@ -68,6 +80,13 @@ export const sidebarMenuItems: MenuItem[] = [
icon: Users, icon: Users,
path: "/page/strategy/qa/multi-agent-roundtable", path: "/page/strategy/qa/multi-agent-roundtable",
}, },
{
id: "strategy-projects",
label: "我的项目",
icon: FolderKanban,
path: "/page/strategy/qa/projects",
aliasPaths: ["/page/strategy/qa/strategy-selection"],
},
// { // {
// id: "position-roundtable", // id: "position-roundtable",
// label: "岗位会商", // label: "岗位会商",
@@ -258,12 +277,7 @@ export const sidebarMenuItems: MenuItem[] = [
}, },
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" }, { id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" }, { id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
{ MARKDOWN_DOCX_MENU_ITEM,
id: "resource-markdown-docx-editor",
label: "md转word",
icon: FileText,
path: "/page/workspace/knowledge/markdown-editor",
},
{ {
id: "mgmt-tasks", id: "mgmt-tasks",
label: "定时任务", label: "定时任务",
@@ -330,6 +344,7 @@ export const sidebarMenuItems: MenuItem[] = [
label: "知识库管理", label: "知识库管理",
icon: BookOpen, icon: BookOpen,
path: "/page/workspace/knowledge", path: "/page/workspace/knowledge",
excludePaths: [MARKDOWN_DOCX_MENU_PATH],
}, },
{ {
id: "resource-knowledge-base", id: "resource-knowledge-base",
@@ -365,6 +380,9 @@ function routeMatchesPathname(pathname: string, route?: string): boolean {
/** 菜单项自身 path / aliasPaths 是否匹配当前路由。 */ /** 菜单项自身 path / aliasPaths 是否匹配当前路由。 */
export function menuItemMatchesPath(item: MenuItem, pathname: string): boolean { export function menuItemMatchesPath(item: MenuItem, pathname: string): boolean {
if (item.excludePaths?.some((path) => routeMatchesPathname(pathname, path))) {
return false;
}
if (routeMatchesPathname(pathname, item.path)) return true; if (routeMatchesPathname(pathname, item.path)) return true;
return item.aliasPaths?.some((p) => routeMatchesPathname(pathname, p)) ?? false; return item.aliasPaths?.some((p) => routeMatchesPathname(pathname, p)) ?? false;
} }
+637
View File
@@ -0,0 +1,637 @@
import { getBackendBaseURL } from "@/core/config";
import { apiFetch } from "@/core/api";
import type { Skill, SkillDisplayConfig, SkillState } from "./type";
export interface ListSkillsParams {
search?: string;
tagIds?: string[];
sort?: "time_desc" | "time_asc" | "activity_desc" | "activity_asc" | "usage_desc" | "usage_asc" | "name_asc" | "name_desc";
/** Keep only skills filed under this publish square (发布广场). */
square?: string;
/** ``"mine"`` restricts to the skills the caller's 岗位 (position) makes visible. */
scope?: "mine";
}
export async function loadSkills(params: ListSkillsParams = {}) {
const qs = new URLSearchParams();
if (params.search) qs.set("search", params.search);
for (const id of params.tagIds ?? []) qs.append("tag_ids", id);
if (params.sort) qs.set("sort", params.sort);
if (params.square) qs.set("square", params.square);
if (params.scope) qs.set("scope", params.scope);
const query = qs.toString();
const skills = await apiFetch(
`${getBackendBaseURL()}/api/skills${query ? `?${query}` : ""}`,
);
const json = await skills.json();
return json.skills as Skill[];
}
export async function loadAllSkillsAdmin() {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills?admin=true`,
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to load skills");
}
const json = await response.json();
return json.skills as Skill[];
}
export async function getSkill(skillName: string): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}`,
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to load skill");
}
return response.json() as Promise<Skill>;
}
export async function enableSkill(skillName: string, enabled: boolean) {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${skillName}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
enabled,
}),
},
);
return response.json();
}
export async function setSkillAlwaysOn(
skillName: string,
alwaysOn: boolean,
): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/always-on`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ always_on: alwaysOn }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill 常驻 flag");
}
return response.json() as Promise<Skill>;
}
export async function setSkillDisplay(
skillName: string,
display: SkillDisplayConfig | null,
sample?: Record<string, unknown> | null,
): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ display, sample }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill display adapter");
}
return response.json() as Promise<Skill>;
}
export interface SkillDisplaySampleResponse {
skill_name: string;
sample: Record<string, unknown>;
}
export interface PersistedSkillDisplaySampleResponse {
skill_name: string;
sample: Record<string, unknown> | null;
}
export async function getPersistedSkillDisplaySample(
skillName: string,
): Promise<PersistedSkillDisplaySampleResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display-sample`,
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to get persisted skill display sample");
}
return response.json() as Promise<PersistedSkillDisplaySampleResponse>;
}
export async function getSkillDisplaySample(
skillName: string,
): Promise<SkillDisplaySampleResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/display-sample`,
{ method: "POST" },
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to get skill display sample");
}
return response.json() as Promise<SkillDisplaySampleResponse>;
}
export interface InstallSkillRequest {
thread_id: string;
path: string;
skill_name?: string;
}
export interface InstallSkillResponse {
success: boolean;
skill_name: string;
message: string;
overwritten?: boolean;
}
export interface PreviewSkillInstallRequest {
thread_id: string;
path: string;
}
export interface PreviewSkillInstallResponse {
ok: boolean;
skill_name: string;
description: string;
license: string | null;
allowed_tools: string[];
can_install_as_is: boolean;
owned_by_me: boolean;
owned_by_other: boolean;
builtin_conflict: boolean;
owner_username?: string | null;
owner_user_id?: string | null;
message: string;
warnings: string[];
}
export interface ValidateSkillUploadResponse {
ok: boolean;
skill_name: string;
description: string;
license: string | null;
allowed_tools: string[];
conflict: boolean;
conflict_kind: "public" | "custom" | null;
can_install_as_is: boolean;
can_overwrite: boolean;
owned_by_me: boolean;
owned_by_other: boolean;
builtin_conflict: boolean;
owner_username?: string | null;
owner_user_id?: string | null;
message: string;
warnings: string[];
}
export async function installSkill(
request: InstallSkillRequest,
): Promise<InstallSkillResponse> {
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/install`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
// Handle HTTP error responses (4xx, 5xx)
const errorData = await response.json().catch(() => ({}));
const errorMessage =
errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`;
return {
success: false,
skill_name: "",
message: errorMessage,
};
}
return response.json();
}
export async function previewSkillInstall(
request: PreviewSkillInstallRequest,
): Promise<PreviewSkillInstallResponse> {
const response = await apiFetch(`${getBackendBaseURL()}/api/skills/install-preview`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw await parseDetailedError(response, "Failed to preview skill install");
}
return response.json() as Promise<PreviewSkillInstallResponse>;
}
async function parseDetailedError(response: Response, fallback: string): Promise<Error> {
const data = await response.json().catch(() => ({}));
const detail = (data as { detail?: unknown }).detail;
const message = typeof detail === "string" && detail ? detail : `${fallback} (HTTP ${response.status})`;
return new Error(message);
}
export async function validateSkillUpload(
file: File,
targetCategory: "public" | "custom" = "custom",
): Promise<ValidateSkillUploadResponse> {
const form = new FormData();
form.append("file", file, file.name);
const params = new URLSearchParams({ target_category: targetCategory });
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/validate?${params.toString()}`,
{
method: "POST",
body: form,
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Validation failed");
}
return response.json() as Promise<ValidateSkillUploadResponse>;
}
export async function installSkillUpload(
file: File,
options: { overwrite?: boolean; targetCategory?: "public" | "custom" } = {},
): Promise<InstallSkillResponse> {
const form = new FormData();
form.append("file", file, file.name);
const params = new URLSearchParams();
if (options.overwrite) params.set("overwrite", "true");
if (options.targetCategory) params.set("target_category", options.targetCategory);
const query = params.size > 0 ? `?${params.toString()}` : "";
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/install-upload${query}`,
{ method: "POST", body: form },
);
if (!response.ok) {
throw await parseDetailedError(response, "Install failed");
}
return response.json() as Promise<InstallSkillResponse>;
}
export async function duplicateSkill(
sourceName: string,
newName: string,
): Promise<InstallSkillResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(sourceName)}/duplicate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_name: newName }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to duplicate skill");
}
return response.json() as Promise<InstallSkillResponse>;
}
export interface ReconcileCustomSkillsResponse {
adopted: string[];
already_registered: string[];
}
export async function reconcileCustomSkills(): Promise<ReconcileCustomSkillsResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/reconcile-custom`,
{ method: "POST" },
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to reconcile skills directory");
}
return response.json() as Promise<ReconcileCustomSkillsResponse>;
}
export interface SetSkillPublishedResponse {
name: string;
owner_user_id: string | null;
published: boolean;
square_id?: string;
}
export async function setSkillPublished(
skillName: string,
published: boolean,
squareId?: string,
): Promise<SetSkillPublishedResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/published`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
squareId != null ? { published, square_id: squareId } : { published },
),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update published state");
}
return response.json() as Promise<SetSkillPublishedResponse>;
}
export async function setSkillState(
skillName: string,
state: SkillState,
): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/state`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ state }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill state");
}
return response.json() as Promise<Skill>;
}
export async function setSkillPinned(
skillName: string,
pinned: boolean,
): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/pinned`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pinned }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill pinned state");
}
return response.json() as Promise<Skill>;
}
export async function setSkillFeatured(
skillName: string,
featured: boolean,
): Promise<Skill> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}/featured`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ featured }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill featured state");
}
return response.json() as Promise<Skill>;
}
export interface SkillContentResponse {
name: string;
category: string;
content: string;
}
export async function getSkillContent(
skillName: string,
): Promise<SkillContentResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/content`,
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to load skill content");
}
return response.json() as Promise<SkillContentResponse>;
}
export async function deleteCustomSkill(skillName: string): Promise<void> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/custom/${encodeURIComponent(skillName)}`,
{ method: "DELETE" },
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to delete skill");
}
}
/** Delete a built-in skill. The backend permits this only for administrators. */
export async function deleteBuiltinSkill(skillName: string): Promise<void> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}`,
{ method: "DELETE" },
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to delete built-in skill");
}
}
export interface SkillFileNode {
name: string;
type: "file" | "dir";
path: string;
size?: number | null;
mtime?: number | null;
children?: SkillFileNode[] | null;
}
export interface SkillFilesResponse {
name: string;
category: "public" | "custom";
editable: boolean;
root: SkillFileNode;
}
export interface SkillFileContentResponse {
name: string;
category: "public" | "custom";
path: string;
mime: string;
size: number;
mtime: number;
is_text: boolean;
editable: boolean;
content?: string | null;
}
export interface SkillFileSaveResponse {
name: string;
path: string;
size: number;
mtime: number;
}
export async function getSkillFiles(skillName: string): Promise<SkillFilesResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files`,
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to load skill files");
}
return response.json() as Promise<SkillFilesResponse>;
}
export async function getSkillFileContent(
skillName: string,
path: string,
): Promise<SkillFileContentResponse> {
const url =
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/content` +
`?path=${encodeURIComponent(path)}`;
const response = await apiFetch(url);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to read skill file");
}
return response.json() as Promise<SkillFileContentResponse>;
}
/** URL suitable for direct `<img>` / `<iframe>` / `<a download>` embedding. */
export function getSkillFileDownloadURL(skillName: string, path: string): string {
return (
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/download` +
`?path=${encodeURIComponent(path)}`
);
}
export async function saveSkillFile(
skillName: string,
path: string,
content: string,
): Promise<SkillFileSaveResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/content`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path, content }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to save skill file");
}
return response.json() as Promise<SkillFileSaveResponse>;
}
export async function deleteSkillFile(
skillName: string,
path: string,
): Promise<void> {
const url =
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/files/content` +
`?path=${encodeURIComponent(path)}`;
const response = await apiFetch(url, { method: "DELETE" });
if (!response.ok) {
throw await parseDetailedError(response, "Failed to delete skill file");
}
}
export interface SkillNameZhResponse {
name: string;
name_zh: string | null;
}
export interface GenerateMissingSkillNameZhResponse {
generated: SkillNameZhResponse[];
failed: string[];
skipped: number;
/** Representative model error when some skills failed due to the LLM. */
model_error?: string | null;
}
/**
* Set a skill's Chinese display name. An empty string clears it (falls back to
* the English name).
*/
export async function setSkillNameZh(
skillName: string,
nameZh: string,
): Promise<SkillNameZhResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/name-zh`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name_zh: nameZh }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to set skill Chinese name");
}
return response.json() as Promise<SkillNameZhResponse>;
}
/** Ask the LLM to generate and persist a Chinese name for a single skill. */
export async function generateSkillNameZh(
skillName: string,
modelName?: string | null,
): Promise<SkillNameZhResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/name-zh/generate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model_name: modelName ?? null }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to generate skill Chinese name");
}
return response.json() as Promise<SkillNameZhResponse>;
}
/** One-click (admin): generate Chinese names for every skill that lacks one. */
export async function generateMissingSkillNameZh(
modelName?: string | null,
): Promise<GenerateMissingSkillNameZhResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/name-zh/generate-missing`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model_name: modelName ?? null }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to bulk-generate skill Chinese names");
}
return response.json() as Promise<GenerateMissingSkillNameZhResponse>;
}
export interface SetSkillDetailResponse {
name: string;
detail: string | null;
}
/**
* Update a skill's long-form detail text. Owner/admin for custom skills,
* admin-only for built-in skills (backend enforces). An empty string clears it.
*/
export async function setSkillDetail(
skillName: string,
detail: string,
): Promise<SetSkillDetailResponse> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/skills/${encodeURIComponent(skillName)}/detail`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ detail }),
},
);
if (!response.ok) {
throw await parseDetailedError(response, "Failed to update skill detail");
}
return response.json() as Promise<SetSkillDetailResponse>;
}
+379
View File
@@ -0,0 +1,379 @@
import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
deleteBuiltinSkill,
deleteCustomSkill,
deleteSkillFile,
duplicateSkill,
enableSkill,
generateMissingSkillNameZh,
generateSkillNameZh,
getSkill,
getSkillContent,
getSkillFileContent,
getSkillFiles,
installSkillUpload,
reconcileCustomSkills,
saveSkillFile,
setSkillAlwaysOn,
setSkillDetail,
setSkillNameZh,
setSkillDisplay,
setSkillPinned,
setSkillFeatured,
setSkillPublished,
setSkillState,
validateSkillUpload,
} from "./api";
import type { ListSkillsParams } from "./api";
import type { Skill, SkillState } from "./type";
import { loadAllSkillsAdmin, loadSkills } from ".";
// Stable empty fallback. Returning `data ?? []` inline would hand out a fresh
// array reference on every render while the query is loading/errored, which
// makes downstream `useMemo([skills])` / `useEffect([skills])` consumers churn
// forever (observed as a "Maximum update depth exceeded" loop on NewAgentPage).
const EMPTY_SKILLS: Skill[] = [];
export function useSkills(params: ListSkillsParams = {}) {
const search = params.search ?? "";
const tagIds = params.tagIds ?? [];
const sort = params.sort ?? "time_desc";
const scope = params.scope;
const { data, isLoading, error } = useQuery({
queryKey: ["skills", search, [...tagIds].sort(), sort, scope ?? "all"],
queryFn: () => loadSkills({ search, tagIds, sort, scope }),
placeholderData: keepPreviousData,
});
return { skills: data ?? EMPTY_SKILLS, isLoading, error };
}
/**
* Admin-only listing — every skill in the system (built-in + every user's
* custom skills, including unpublished ones owned by other users). Backend
* 403s if the caller is not ``system_role === "admin"``.
*/
export function useAllSkillsAdmin(enabled: boolean) {
const { data, isLoading, error } = useQuery({
queryKey: ["skills", "admin", "all"],
queryFn: () => loadAllSkillsAdmin(),
enabled,
staleTime: 30_000,
});
return { skills: data ?? EMPTY_SKILLS, isLoading, error };
}
export function useSkillContent(skillName: string | null) {
return useQuery({
queryKey: ["skill-content", skillName],
queryFn: () => getSkillContent(skillName!),
enabled: !!skillName,
});
}
export function useSkill(skillName: string | null) {
return useQuery({
queryKey: ["skill", skillName],
queryFn: () => getSkill(skillName!),
enabled: !!skillName,
});
}
export function useSetSkillDetail() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, detail }: { skillName: string; detail: string }) =>
setSkillDetail(skillName, detail),
onSuccess: (_data, { skillName }) => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
},
});
}
export function useSetSkillNameZh() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, nameZh }: { skillName: string; nameZh: string }) =>
setSkillNameZh(skillName, nameZh),
onSuccess: (_data, { skillName }) => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
},
});
}
export function useGenerateSkillNameZh() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, modelName }: { skillName: string; modelName?: string | null }) =>
generateSkillNameZh(skillName, modelName),
onSuccess: (_data, { skillName }) => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
},
});
}
export function useGenerateMissingSkillNameZh() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (modelName?: string | null) => generateMissingSkillNameZh(modelName),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useSetSkillDisplay() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
skillName,
display,
sample,
}: {
skillName: string;
display: Parameters<typeof setSkillDisplay>[1];
sample?: Parameters<typeof setSkillDisplay>[2];
}) => setSkillDisplay(skillName, display, sample),
onSuccess: (data, { skillName }) => {
queryClient.setQueryData(["skill", skillName], data);
queryClient.setQueriesData<Skill[]>({ queryKey: ["skills"] }, (current) =>
Array.isArray(current)
? current.map((item) => (item.name === skillName ? { ...item, ...data } : item))
: current,
);
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
},
});
}
export function useEnableSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
skillName,
enabled,
}: {
skillName: string;
enabled: boolean;
}) => {
await enableSkill(skillName, enabled);
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useSetSkillAlwaysOn() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, alwaysOn }: { skillName: string; alwaysOn: boolean }) =>
setSkillAlwaysOn(skillName, alwaysOn),
onSuccess: (data, { skillName }) => {
queryClient.setQueryData(["skill", skillName], data);
queryClient.setQueriesData<Skill[]>({ queryKey: ["skills"] }, (current) =>
Array.isArray(current)
? current.map((item) => (item.name === skillName ? { ...item, ...data } : item))
: current,
);
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
},
});
}
export function useValidateSkillUpload() {
return useMutation({
mutationFn: ({
file,
targetCategory,
}: {
file: File;
targetCategory?: "public" | "custom";
}) => validateSkillUpload(file, targetCategory),
});
}
export function useInstallSkillUpload() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ file, overwrite, targetCategory }: { file: File; overwrite?: boolean; targetCategory?: "public" | "custom" }) =>
installSkillUpload(file, { overwrite, targetCategory }),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useReconcileCustomSkills() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => reconcileCustomSkills(),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useSetSkillPublished() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
skillName,
published,
squareId,
}: {
skillName: string;
published: boolean;
squareId?: string;
}) => setSkillPublished(skillName, published, squareId),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
// Unpublishing triggers the backend cascade that strips this skill
// from every referencing agent's config.yaml, so the cached agent
// list (agent.skills) is now stale.
void queryClient.invalidateQueries({ queryKey: ["agents"] });
},
});
}
export function useSetSkillState() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, state }: { skillName: string; state: SkillState }) =>
setSkillState(skillName, state),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useSetSkillPinned() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, pinned }: { skillName: string; pinned: boolean }) =>
setSkillPinned(skillName, pinned),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useSetSkillFeatured() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ skillName, featured }: { skillName: string; featured: boolean }) =>
setSkillFeatured(skillName, featured),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useDuplicateSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
sourceName,
newName,
}: {
sourceName: string;
newName: string;
}) => duplicateSkill(sourceName, newName),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
},
});
}
export function useDeleteCustomSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (skillName: string) => deleteCustomSkill(skillName),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
// Deletion triggers the backend cascade that strips this skill from
// every referencing agent's config.yaml, so the cached agent list
// (agent.skills) is now stale.
void queryClient.invalidateQueries({ queryKey: ["agents"] });
},
});
}
export function useSkillFiles(skillName: string | null) {
return useQuery({
queryKey: ["skill-files", skillName],
queryFn: () => getSkillFiles(skillName!),
enabled: !!skillName,
});
}
export function useSkillFileContent(skillName: string | null, path: string | null) {
return useQuery({
queryKey: ["skill-file", skillName, path],
queryFn: () => getSkillFileContent(skillName!, path!),
enabled: !!skillName && !!path,
});
}
export function useSaveSkillFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
skillName,
path,
content,
}: {
skillName: string;
path: string;
content: string;
}) => saveSkillFile(skillName, path, content),
onSuccess: (_data, { skillName, path }) => {
void queryClient.invalidateQueries({ queryKey: ["skill-files", skillName] });
void queryClient.invalidateQueries({ queryKey: ["skill-file", skillName, path] });
// SKILL.md edits also affect the source-tab view and skill metadata.
if (path === "SKILL.md") {
void queryClient.invalidateQueries({ queryKey: ["skill-content", skillName] });
void queryClient.invalidateQueries({ queryKey: ["skill", skillName] });
}
},
});
}
export function useDeleteSkillFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
skillName,
path,
}: {
skillName: string;
path: string;
}) => deleteSkillFile(skillName, path),
onSuccess: (_data, { skillName, path }) => {
void queryClient.invalidateQueries({ queryKey: ["skill-files", skillName] });
void queryClient.removeQueries({ queryKey: ["skill-file", skillName, path] });
},
});
}
export function useDeleteBuiltinSkill() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteBuiltinSkill,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["skills"] });
void queryClient.invalidateQueries({ queryKey: ["admin-skills"] });
},
});
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./api";
export * from "./type";
export * from "./utils";
+88
View File
@@ -0,0 +1,88 @@
import type { Tag } from "@/core/tags";
export type SkillState = "active" | "stale" | "archived";
export type SkillSource = "agent" | "upload";
export type SkillDisplayMode = "citation" | "list" | "table" | "card" | "custom" | "none";
export type SkillLinkStrategy = "auto" | "url" | "id";
export type SkillCitationToken = "doubleBracket" | "singleBracket";
export interface SkillDisplayTableColumn {
label: string;
field: string;
width?: string | null;
}
export interface SkillDisplayResultSetConfig {
key: string;
label?: string | null;
enabled: boolean;
mode: SkillDisplayMode;
resultPath: string;
titleField?: string | null;
summaryField?: string | null;
contentField?: string | null;
sourceField?: string | null;
authorField?: string | null;
typeField?: string | null;
timeField?: string | null;
scoreField?: string | null;
urlField?: string | null;
idField?: string | null;
linkStrategy: SkillLinkStrategy;
linkTemplate?: string | null;
citationToken: SkillCitationToken;
maxItems: number;
customInstruction: string;
tableColumns?: SkillDisplayTableColumn[];
}
export interface SkillDisplayConfig extends Omit<SkillDisplayResultSetConfig, "key" | "label"> {
resultSets?: SkillDisplayResultSetConfig[];
}
export interface Skill {
name: string;
// 用户配置或大模型生成的中文名称;为空时界面回退到使用英文 name。
name_zh?: string | null;
description: string;
category: string;
license: string;
enabled: boolean;
// 常驻(免压缩) —— 开启技能压缩后,标记此项的技能仍以全文形式注入系统提示词,
// 而非被压缩成索引 + search_skills 检索。
always_on?: boolean;
// Multitenancy fields — owner_user_id is null for built-in / public skills.
// ``builtin`` mirrors that nullness for explicit branching on the UI side.
owner_user_id?: string | null;
// Publisher username (email local part) resolved from owner_user_id by the
// backend. Null for built-in / public skills with no human owner.
owner_username?: string | null;
published?: boolean;
/** Publish square (发布广场) the skill is filed under once published. "" = default square. */
square_id?: string;
builtin?: boolean;
// True when this skill is in the caller's 我的技能 collection — e.g. granted by
// their 岗位 (position). Owned skills are detected separately via owner_user_id.
favorited?: boolean;
// Long-form detail/introduction authored by the skill owner or an admin.
// Distinct from ``description`` (parsed from SKILL.md); supports Markdown.
detail?: string | null;
display?: SkillDisplayConfig | null;
displaySample?: Record<string, unknown> | null;
// Lifecycle fields — populated by curator. The main list filters on these
// so stale / archived skills appear in dedicated drawers instead.
state?: SkillState;
pinned?: boolean;
// 置顶 —— 仅影响列表排序,与 pinned(防自动归档)正交。
featured?: boolean;
// Provenance — distinguishes skills created by an agent during a chat
// (``agent``) from skills the user uploaded / authored manually (``upload``).
source?: SkillSource;
created_at?: string | null;
last_activity_at?: string | null;
use_count?: number;
view_count?: number;
patch_count?: number;
// Tags assigned to this skill (admin-managed).
tags?: Tag[];
}
+22
View File
@@ -0,0 +1,22 @@
import type { Skill } from "./type";
/**
* 智能体技能白名单的候选池过滤。
*
* 编辑智能体时,普通用户只能从「内置技能 + 自己的技能 + 已发布到广场的
* 技能」里挑选,其他用户未发布的自定义技能不可选;管理员仍可使用全部
* 已启用技能。
*/
export function isAgentSkillCandidate(
skill: Skill,
currentUserId: string | null,
isAdmin = false,
): boolean {
if (!skill.enabled) return false;
if (isAdmin) return true;
if ((skill.state ?? "active") !== "active") return false;
const builtin = skill.builtin ?? !skill.owner_user_id;
const ownedByMe =
!!skill.owner_user_id && skill.owner_user_id === currentUserId;
return builtin || ownedByMe || !!skill.published;
}
@@ -13,6 +13,44 @@ export type KnowledgeBaseProductVersion = "v1" | "v2";
export type OrdinaryKnowledgeLanding = "wiki" | "weknora"; export type OrdinaryKnowledgeLanding = "wiki" | "weknora";
export interface StrategyProjectSettings {
taiwan_strait_demo_enabled: boolean;
}
export interface StrategyProjectSettingsUpdate {
taiwan_strait_demo_enabled?: boolean;
}
export async function loadStrategyProjectSettings(): Promise<StrategyProjectSettings> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/system-settings/strategy-projects`,
);
if (!response.ok) {
throw new Error(`无法读取策略项目设置(HTTP ${response.status})`);
}
return (await response.json()) as StrategyProjectSettings;
}
export async function updateStrategyProjectSettings(
body: StrategyProjectSettingsUpdate,
): Promise<StrategyProjectSettings> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/system-settings/strategy-projects`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
if (!response.ok) {
const detail = await response
.json()
.catch(() => ({ detail: "保存策略项目设置失败" }));
throw new Error(detail.detail ?? "保存策略项目设置失败");
}
return (await response.json()) as StrategyProjectSettings;
}
export async function loadOrdinaryKnowledgeLanding(): Promise<OrdinaryKnowledgeLanding> { export async function loadOrdinaryKnowledgeLanding(): Promise<OrdinaryKnowledgeLanding> {
const response = await apiFetch(`${getBackendBaseURL()}/api/system-settings/ordinary-knowledge-landing`); const response = await apiFetch(`${getBackendBaseURL()}/api/system-settings/ordinary-knowledge-landing`);
if (!response.ok) throw new Error("无法读取普通知识库默认页面设置"); if (!response.ok) throw new Error("无法读取普通知识库默认页面设置");
@@ -13,6 +13,7 @@ import {
loadPromptPrefixSettings, loadPromptPrefixSettings,
loadPublishSquares, loadPublishSquares,
loadSkillCompressionSettings, loadSkillCompressionSettings,
loadStrategyProjectSettings,
updateAgentModuleConfig, updateAgentModuleConfig,
updateAppearanceSettings, updateAppearanceSettings,
updateCitationDisplaySettings, updateCitationDisplaySettings,
@@ -23,6 +24,7 @@ import {
updatePromptPrefixSettings, updatePromptPrefixSettings,
updatePublishSquares, updatePublishSquares,
updateSkillCompressionSettings, updateSkillCompressionSettings,
updateStrategyProjectSettings,
type AppearanceUpdate, type AppearanceUpdate,
type CitationDisplayUpdate, type CitationDisplayUpdate,
type LeaderboardSettingsUpdate, type LeaderboardSettingsUpdate,
@@ -32,6 +34,7 @@ import {
type PromptPrefixUpdate, type PromptPrefixUpdate,
type PublishSquaresUpdate, type PublishSquaresUpdate,
type SkillCompressionUpdate, type SkillCompressionUpdate,
type StrategyProjectSettingsUpdate,
type LayoutMode, type LayoutMode,
type ViewMode, type ViewMode,
} from "./api"; } from "./api";
@@ -45,6 +48,28 @@ const LEADERBOARD_KEY = ["systemSettings", "leaderboard"] as const;
const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const; const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const;
const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const; const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const;
const PLUGIN_MANAGEMENT_KEY = ["systemSettings", "pluginManagement"] as const; const PLUGIN_MANAGEMENT_KEY = ["systemSettings", "pluginManagement"] as const;
const STRATEGY_PROJECTS_KEY = ["systemSettings", "strategyProjects"] as const;
export function useStrategyProjectSettings() {
return useQuery({
queryKey: STRATEGY_PROJECTS_KEY,
queryFn: () => loadStrategyProjectSettings(),
staleTime: 60_000,
refetchOnWindowFocus: false,
retry: false,
});
}
export function useUpdateStrategyProjectSettings() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: StrategyProjectSettingsUpdate) =>
updateStrategyProjectSettings(body),
onSuccess: (data) => {
queryClient.setQueryData(STRATEGY_PROJECTS_KEY, data);
},
});
}
export function usePromptPrefixSettings() { export function usePromptPrefixSettings() {
return useQuery({ return useQuery({
+1
View File
@@ -1579,6 +1579,7 @@ export function useThreadStream({
context: { context: {
...mergedContext, ...mergedContext,
thinking_enabled: mergedContext.mode !== "flash", thinking_enabled: mergedContext.mode !== "flash",
thinking_force_disabled: mergedContext.mode === "flash",
is_plan_mode: is_plan_mode:
mergedContext.mode === "pro" || mergedContext.mode === "ultra", mergedContext.mode === "pro" || mergedContext.mode === "ultra",
subagent_enabled: mergedContext.mode === "ultra", subagent_enabled: mergedContext.mode === "ultra",
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
myProjectConversationTypeFromSearchParams,
myProjectConversationTypeOfThread,
myProjectThreadMetadata,
withMyProjectConversationMarker,
} from "./project-metadata";
test("project conversation type is read from persisted metadata", () => {
assert.equal(
myProjectConversationTypeOfThread({
metadata: myProjectThreadMetadata("strategy"),
}),
"strategy",
);
assert.equal(
myProjectConversationTypeOfThread({
metadata: { project_source: "elsewhere", project_type: "qa" },
}),
null,
);
});
test("metadata wins over the compatibility context fallback", () => {
assert.equal(
myProjectConversationTypeOfThread({
metadata: myProjectThreadMetadata("solution"),
context: myProjectThreadMetadata("qa"),
}),
"solution",
);
});
test("new-chat route marker round-trips all supported project types", () => {
for (const type of ["qa", "strategy", "solution"] as const) {
const path = withMyProjectConversationMarker(
"/page/strategy/chats/new?keep=1",
type,
);
const params = new URLSearchParams(path.split("?", 2)[1]);
assert.equal(params.get("keep"), "1");
assert.equal(myProjectConversationTypeFromSearchParams(params), type);
}
});
@@ -0,0 +1,94 @@
/**
* Stable marker for conversations entered from 「我的项目」.
*
* Keep this separate from ``project_id``: that key belongs to DeerFlow's
* folder-style conversation projects, while this marker describes the product
* mode that created (or explicitly continued) a conversation.
*/
export const MY_PROJECT_THREAD_SOURCE = "my-projects" as const;
export const MY_PROJECT_THREAD_SOURCE_KEY = "project_source" as const;
export const MY_PROJECT_THREAD_TYPE_KEY = "project_type" as const;
export const MY_PROJECT_QUERY_SOURCE_KEY = "projectSource" as const;
export const MY_PROJECT_QUERY_TYPE_KEY = "projectType" as const;
export type MyProjectConversationType = "qa" | "strategy" | "solution";
export const MY_PROJECT_CONVERSATION_LABELS: Record<
MyProjectConversationType,
string
> = {
qa: "普通问答",
strategy: "策略优选",
solution: "方案生成",
};
const MY_PROJECT_CONVERSATION_TYPES = new Set<MyProjectConversationType>([
"qa",
"strategy",
"solution",
]);
function typeFromRecord(
value: Record<string, unknown> | null | undefined,
): MyProjectConversationType | null {
if (value?.[MY_PROJECT_THREAD_SOURCE_KEY] !== MY_PROJECT_THREAD_SOURCE) {
return null;
}
const type = value[MY_PROJECT_THREAD_TYPE_KEY];
return typeof type === "string" &&
MY_PROJECT_CONVERSATION_TYPES.has(type as MyProjectConversationType)
? (type as MyProjectConversationType)
: null;
}
export function myProjectThreadMetadata(
type: MyProjectConversationType,
): Record<string, unknown> {
return {
[MY_PROJECT_THREAD_SOURCE_KEY]: MY_PROJECT_THREAD_SOURCE,
[MY_PROJECT_THREAD_TYPE_KEY]: type,
};
}
/** Read persisted metadata first; context is only a compatibility fallback. */
export function myProjectConversationTypeOfThread(thread: {
metadata?: Record<string, unknown> | null;
context?: Record<string, unknown> | null;
}): MyProjectConversationType | null {
return typeFromRecord(thread.metadata) ?? typeFromRecord(thread.context);
}
export function myProjectConversationTypeFromSearchParams(params: {
get(name: string): string | null;
}): MyProjectConversationType | null {
if (params.get(MY_PROJECT_QUERY_SOURCE_KEY) !== MY_PROJECT_THREAD_SOURCE) {
return null;
}
const type = params.get(MY_PROJECT_QUERY_TYPE_KEY);
return typeof type === "string" &&
MY_PROJECT_CONVERSATION_TYPES.has(type as MyProjectConversationType)
? (type as MyProjectConversationType)
: null;
}
/** Add an explicit, one-shot marker to a new-conversation route. */
export function withMyProjectConversationMarker(
path: string,
type: MyProjectConversationType,
): string {
const hashIndex = path.indexOf("#");
const hash = hashIndex >= 0 ? path.slice(hashIndex) : "";
const pathWithoutHash = hashIndex >= 0 ? path.slice(0, hashIndex) : path;
const queryIndex = pathWithoutHash.indexOf("?");
const pathname =
queryIndex >= 0 ? pathWithoutHash.slice(0, queryIndex) : pathWithoutHash;
const params = new URLSearchParams(
queryIndex >= 0 ? pathWithoutHash.slice(queryIndex + 1) : "",
);
params.set(MY_PROJECT_QUERY_SOURCE_KEY, MY_PROJECT_THREAD_SOURCE);
params.set(MY_PROJECT_QUERY_TYPE_KEY, type);
const search = params.toString();
return `${pathname}${search ? `?${search}` : ""}${hash}`;
}
+4 -2
View File
@@ -14,6 +14,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
thread_id: string; thread_id: string;
model_name: string | undefined; model_name: string | undefined;
thinking_enabled: boolean; thinking_enabled: boolean;
/** Flash mode: force vLLM/Qwen ``enable_thinking=false`` even without a per-model disable shape. */
thinking_force_disabled?: boolean;
is_plan_mode: boolean; is_plan_mode: boolean;
subagent_enabled: boolean; subagent_enabled: boolean;
reasoning_effort?: "minimal" | "low" | "medium" | "high"; reasoning_effort?: "minimal" | "low" | "medium" | "high";
@@ -29,8 +31,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
llmwiki_knowledge_base_ids?: string[]; llmwiki_knowledge_base_ids?: string[];
/** Assistant knowledge-base ids selected for this conversation. */ /** Assistant knowledge-base ids selected for this conversation. */
assistant_knowledge_base_ids?: string[]; assistant_knowledge_base_ids?: string[];
/** Direct RAG modes handled in the frontend; these requests never enter Lead Agent. */ /** Frontend-only execution paths that never enter Lead Agent (RAG / 极速直连). */
execution_profile?: "single_round_rag" | "multi_round_rag"; execution_profile?: "single_round_rag" | "multi_round_rag" | "direct";
agent_id?: string; agent_id?: string;
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */ /** @deprecated kept for reading legacy thread metadata; prefer agent_id */
agent_name?: string; agent_name?: string;
@@ -0,0 +1,312 @@
import type { Message } from "@langchain/langgraph-sdk";
import { useCallback, useEffect, useRef, useState } from "react";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import { readRuntimeFlag } from "@/env";
import { createClientId } from "@/lib/client-id";
import { textOfMessage } from "./utils";
const STORAGE_PREFIX = "workspace.direct-chat.";
const MAX_HISTORY_MESSAGES = 20;
export function isDirectChatProfile(context: unknown): boolean {
return (
typeof context === "object" &&
context !== null &&
(context as { execution_profile?: unknown }).execution_profile === "direct"
);
}
/** 是否在模式菜单展示「极速」。仅 runtime-config.js 显式为 "true" 时开启,默认关闭。 */
export function isTurboModeVisible(): boolean {
return readRuntimeFlag("VITE_TURBO_MODE_VISIBLE");
}
function storageKey(threadId: string) {
return `${STORAGE_PREFIX}${threadId}`;
}
function readStoredMessages(threadId: string): Message[] {
if (typeof window === "undefined" || !threadId) return [];
try {
const raw = window.sessionStorage.getItem(storageKey(threadId));
const parsed: unknown = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? (parsed as Message[]) : [];
} catch {
return [];
}
}
function writeStoredMessages(threadId: string, messages: Message[]) {
if (typeof window === "undefined" || !threadId) return;
try {
window.sessionStorage.setItem(storageKey(threadId), JSON.stringify(messages));
} catch {
// Quota errors must never interrupt an already-running answer.
}
}
function isAbortError(error: unknown): boolean {
return error instanceof DOMException
? error.name === "AbortError"
: error instanceof Error && error.name === "AbortError";
}
function historyFromMessages(messages: Message[]): Array<{
role: "user" | "assistant";
content: string;
}> {
const history: Array<{ role: "user" | "assistant"; content: string }> = [];
for (const message of messages) {
if (message.type !== "human" && message.type !== "ai") continue;
const content = textOfMessage(message)?.trim();
if (!content) continue;
history.push({
role: message.type === "human" ? "user" : "assistant",
content,
});
}
return history.slice(-MAX_HISTORY_MESSAGES);
}
function parseSseBlocks(buffer: string): {
events: Array<{ event?: string; data: string }>;
rest: string;
} {
const parts = buffer.split("\n\n");
const rest = parts.pop() ?? "";
const events: Array<{ event?: string; data: string }> = [];
for (const part of parts) {
let event: string | undefined;
const dataLines: string[] = [];
for (const line of part.split("\n")) {
if (line.startsWith(":")) continue;
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
}
if (dataLines.length > 0) {
events.push({ event, data: dataLines.join("\n") });
}
}
return { events, rest };
}
async function readErrorDetail(response: Response): Promise<string> {
try {
const payload: unknown = await response.json();
if (
payload &&
typeof payload === "object" &&
"detail" in payload &&
typeof (payload as { detail: unknown }).detail === "string"
) {
return (payload as { detail: string }).detail;
}
} catch {
// Fall through to status text.
}
return `极速问答失败(${response.status})`;
}
/**
* Frontend-only adapter that talks to Gateway ``POST /api/direct-chat/stream``.
* It never submits a LangGraph run or touches the lead-agent stack.
*/
export function useDirectChatMessages(threadId: string | undefined) {
const [messages, setMessages] = useState<Message[]>(() =>
readStoredMessages(threadId ?? ""),
);
const messagesRef = useRef(messages);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const controllerRef = useRef<AbortController | null>(null);
useEffect(() => {
const stored = readStoredMessages(threadId ?? "");
messagesRef.current = stored;
setMessages(stored);
setError(null);
}, [threadId]);
useEffect(() => {
messagesRef.current = messages;
}, [messages]);
useEffect(() => () => controllerRef.current?.abort(), []);
const updateMessages = useCallback(
(targetThreadId: string, updater: (current: Message[]) => Message[]) => {
setMessages((current) => {
const next = updater(current);
messagesRef.current = next;
writeStoredMessages(targetThreadId, next);
return next;
});
},
[],
);
const stop = useCallback(() => controllerRef.current?.abort(), []);
const submit = useCallback(
async ({
targetThreadId,
query,
modelName,
}: {
targetThreadId: string;
query: string;
modelName?: string;
}) => {
if (isLoading) return;
const question = query.trim();
if (!question) return;
const humanId = createClientId("direct-human");
const assistantId = createClientId("direct-ai");
const clientTs = new Date().toISOString();
const history = historyFromMessages(messagesRef.current);
const userMessage: Message = {
id: humanId,
type: "human",
content: [{ type: "text", text: question }],
additional_kwargs: { client_ts: clientTs, direct_chat: true },
};
const initialAssistant: Message = {
id: assistantId,
type: "ai",
content: "",
additional_kwargs: { direct_chat: true },
};
updateMessages(targetThreadId, (current) => [
...current,
userMessage,
initialAssistant,
]);
setError(null);
setIsLoading(true);
const controller = new AbortController();
controllerRef.current = controller;
let answer = "";
const refreshAssistant = (content: string) => {
updateMessages(targetThreadId, (current) =>
current.map((item) =>
item.id === assistantId
? ({
...item,
content,
additional_kwargs: {
...item.additional_kwargs,
direct_chat: true,
},
} as Message)
: item,
),
);
};
try {
const response = await apiFetch(
`${getBackendBaseURL()}/api/direct-chat/stream`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({
model_name: modelName,
messages: [...history, { role: "user", content: question }],
}),
},
);
if (!response.ok) {
throw new Error(await readErrorDetail(response));
}
if (!response.body) {
throw new Error("极速问答未返回数据流。");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parsed = parseSseBlocks(buffer);
buffer = parsed.rest;
for (const block of parsed.events) {
let payload: unknown = null;
try {
payload = JSON.parse(block.data);
} catch {
continue;
}
if (block.event === "error") {
const detail =
payload &&
typeof payload === "object" &&
"detail" in payload &&
typeof (payload as { detail: unknown }).detail === "string"
? (payload as { detail: string }).detail
: "极速问答失败。";
throw new Error(detail);
}
if (
block.event === "delta" &&
payload &&
typeof payload === "object" &&
"text" in payload &&
typeof (payload as { text: unknown }).text === "string"
) {
answer += (payload as { text: string }).text;
refreshAssistant(answer);
}
if (
block.event === "result" &&
payload &&
typeof payload === "object" &&
"answer" in payload &&
typeof (payload as { answer: unknown }).answer === "string"
) {
answer = (payload as { answer: string }).answer;
refreshAssistant(answer);
}
}
}
if (!answer) {
throw new Error("模型未返回任何文本。");
}
} catch (caught) {
if (!isAbortError(caught)) {
const message =
caught instanceof Error ? caught.message : "极速问答失败。";
setError(message);
updateMessages(targetThreadId, (current) =>
current.map((item) =>
item.id === assistantId
? ({
...item,
content: answer || message,
additional_kwargs: {
...item.additional_kwargs,
direct_chat: true,
},
} as Message)
: item,
),
);
}
} finally {
if (controllerRef.current === controller) controllerRef.current = null;
setIsLoading(false);
}
},
[isLoading, updateMessages],
);
return { messages, isLoading, error, submit, stop };
}
@@ -116,6 +116,14 @@ export interface EightBfTaskProcessReportReceipt {
id: string; id: string;
} }
/**
* 外部任务系统要求 8BF 会商页提交的过程产物携带 eightTask=1,
* 用于与岗位会商等其它入口的产物区分来源。
*/
function processReportBody(input: EightBfTaskProcessReportInput): string {
return JSON.stringify({ ...input, eightTask: 1 });
}
/** /**
* 当前 DeerFlow 的外部业务请求适配器沿用任务系统的默认约定:使用 * 当前 DeerFlow 的外部业务请求适配器沿用任务系统的默认约定:使用
* `Authorization: Bearer admin`,而不是 DeerFlow 登录接口返回的 token。 * `Authorization: Bearer admin`,而不是 DeerFlow 登录接口返回的 token。
@@ -180,7 +188,7 @@ export async function createEightBfTaskProcessReport(
const response = await fetchExternalTaskSystem(PROCESS_REPORT_ADD_PATH, { const response = await fetchExternalTaskSystem(PROCESS_REPORT_ADD_PATH, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(input), body: processReportBody(input),
}); });
if (!response.ok) throw await externalTaskError(response, "过程产物登记失败"); if (!response.ok) throw await externalTaskError(response, "过程产物登记失败");
const id = findProcessReportId(await response.json().catch(() => null)); const id = findProcessReportId(await response.json().catch(() => null));
@@ -198,7 +206,7 @@ export async function updateEightBfTaskProcessReport(
{ {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(input), body: processReportBody(input),
}, },
); );
if (!response.ok) throw await externalTaskError(response, "过程产物提交失败"); if (!response.ok) throw await externalTaskError(response, "过程产物提交失败");
@@ -31,6 +31,7 @@ import {
syncPositionOptionsFromRoles, syncPositionOptionsFromRoles,
} from "@/position-roundtable/lib/positions"; } from "@/position-roundtable/lib/positions";
import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts"; import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts";
import { EIGHTBF_INTENT_AGENT_ID } from "@/roundtable-planning/api/intent";
import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent"; import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent";
import "@/position-roundtable/styles/intent-understanding.css"; import "@/position-roundtable/styles/intent-understanding.css";
import type { Step1PanelProps } from "@/roundtable-planning/components/Step1Panel"; import type { Step1PanelProps } from "@/roundtable-planning/components/Step1Panel";
@@ -96,6 +97,8 @@ export function EightBfStep1Panel({
}: EightBfStep1PanelProps) { }: EightBfStep1PanelProps) {
const [rightPanelOpen, setRightPanelOpen] = useState(true); const [rightPanelOpen, setRightPanelOpen] = useState(true);
const [rightTab, setRightTab] = useState<"agents" | "results">("agents"); const [rightTab, setRightTab] = useState<"agents" | "results">("agents");
// 第一步内置任务理解智能体:其「常用问题」显示在输入框左上角,点击直接发送。
const { agent: intentAgent } = useAgent(EIGHTBF_INTENT_AGENT_ID);
const openPanel = (tab: "agents" | "results") => { const openPanel = (tab: "agents" | "results") => {
if (rightPanelOpen && rightTab === tab) { if (rightPanelOpen && rightTab === tab) {
@@ -167,6 +170,8 @@ export function EightBfStep1Panel({
modelOptions={modelOptions} modelOptions={modelOptions}
selectedModel={selectedModel} selectedModel={selectedModel}
onSelectedModelChange={onSelectedModelChange} onSelectedModelChange={onSelectedModelChange}
commonQuestions={intentAgent?.common_questions ?? null}
sendCommonQuestionOnClick
/> />
)} )}
intentReady={intentReady} intentReady={intentReady}
@@ -331,8 +336,26 @@ function EightBfAgentWorkspace({
summaryReport: step3Summary, summaryReport: step3Summary,
}); });
// 第一步绑定的内置任务理解智能体:不在业务链席位里,固定置顶展示,
// 供管理员在本面板查看 / 配置其提示词与常用问题。
const intentAgent = useMemo(
() => agents.find((agent) => agent.id === EIGHTBF_INTENT_AGENT_ID) ?? null,
[agents],
);
const intentAgentMatchesKeyword = useMemo(() => {
const keyword = agentKeyword.trim().toLowerCase();
if (!keyword) return true;
const name = (intentAgent?.name || "8BF 任务理解").toLowerCase();
const description = (intentAgent?.description || "").toLowerCase();
return name.includes(keyword) || description.includes(keyword);
}, [agentKeyword, intentAgent]);
useEffect(() => { useEffect(() => {
if (selectedAgentId && !seats.some((seat) => seat.agent_id === selectedAgentId)) { if (
selectedAgentId &&
selectedAgentId !== EIGHTBF_INTENT_AGENT_ID &&
!seats.some((seat) => seat.agent_id === selectedAgentId)
) {
setSelectedAgentId(null); setSelectedAgentId(null);
} }
}, [seats, selectedAgentId]); }, [seats, selectedAgentId]);
@@ -412,7 +435,12 @@ function EightBfAgentWorkspace({
) : ( ) : (
<AgentDetailPanel <AgentDetailPanel
agent={selectedAgent} agent={selectedAgent}
agentName={selectedAgent?.name ?? selectedSeat?.name ?? ""} agentName={
selectedAgent?.name
?? (selectedAgentId === EIGHTBF_INTENT_AGENT_ID
? "8BF 任务理解"
: selectedSeat?.name ?? "")
}
onAskQuestion={() => toast.info("请在中间对话区向任务理解智能体发起问题")} onAskQuestion={() => toast.info("请在中间对话区向任务理解智能体发起问题")}
showRecentChats={false} showRecentChats={false}
avatarSrc={agentConfigLogo} avatarSrc={agentConfigLogo}
@@ -468,7 +496,7 @@ function EightBfAgentWorkspace({
</header> </header>
{isLoading && seats.length > 0 ? ( {isLoading && seats.length > 0 ? (
<div className="eightbf-roundtable__agent-selector-empty">正在加载智能体…</div> <div className="eightbf-roundtable__agent-selector-empty">正在加载智能体…</div>
) : filteredSeatAgents.length === 0 ? ( ) : filteredSeatAgents.length === 0 && !intentAgentMatchesKeyword ? (
<div className="eightbf-roundtable__agent-selector-empty"> <div className="eightbf-roundtable__agent-selector-empty">
<p>{seatAgents.length ? "没有符合条件的智能体" : "当前岗位尚未配置智能体"}</p> <p>{seatAgents.length ? "没有符合条件的智能体" : "当前岗位尚未配置智能体"}</p>
{!seatAgents.length && canConfigureChain && ( {!seatAgents.length && canConfigureChain && (
@@ -477,6 +505,13 @@ function EightBfAgentWorkspace({
</div> </div>
) : ( ) : (
<div className="eightbf-roundtable__agent-selector-list"> <div className="eightbf-roundtable__agent-selector-list">
{intentAgentMatchesKeyword && (
<EightBfAgentCard
name={intentAgent?.name || "8BF 任务理解"}
description={intentAgent?.description || "第一步「意图识别」的内置任务理解智能体"}
onSelect={() => setSelectedAgentId(EIGHTBF_INTENT_AGENT_ID)}
/>
)}
{filteredSeatAgents.map(({ seat, agent }) => ( {filteredSeatAgents.map(({ seat, agent }) => (
<EightBfAgentRow <EightBfAgentRow
key={seat.agent_id} key={seat.agent_id}
@@ -503,6 +538,19 @@ function EightBfAgentRow({
}) { }) {
const name = agent?.name || seat.name || "未命名智能体"; const name = agent?.name || seat.name || "未命名智能体";
const description = agent?.description || seat.description || "智能体助手"; const description = agent?.description || seat.description || "智能体助手";
return <EightBfAgentCard name={name} description={description} onSelect={onSelect} />;
}
/** 智能体列表卡片:席位智能体与置顶的内置任务理解智能体共用同一展示结构。 */
function EightBfAgentCard({
name,
description,
onSelect,
}: {
name: string;
description: string;
onSelect: () => void;
}) {
const avatarText = firstAgentAvatarChar(name); const avatarText = firstAgentAvatarChar(name);
return ( return (
<button type="button" className="eightbf-roundtable__agent-selector-card" onClick={onSelect}> <button type="button" className="eightbf-roundtable__agent-selector-card" onClick={onSelect}>
@@ -3,6 +3,7 @@ import { useSearchParams } from "react-router-dom";
import { Loader2, Save, X } from "lucide-react"; import { Loader2, Save, X } from "lucide-react";
import { getAccountDisplayName, getStoredAuth } from "@/core/auth"; import { getAccountDisplayName, getStoredAuth } from "@/core/auth";
import { EIGHTBF_INTENT_AGENT_ID } from "@/roundtable-planning/api/intent";
import { EightBfTaskCard } from "@/eightbf-roundtable/components/EightBfTaskCard"; import { EightBfTaskCard } from "@/eightbf-roundtable/components/EightBfTaskCard";
import { import {
EightBfStep1Panel, EightBfStep1Panel,
@@ -76,6 +77,9 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa
const [selectedChainId, setSelectedChainId] = useState(""); const [selectedChainId, setSelectedChainId] = useState("");
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// 第二步「人工干预」:开启后每走一步暂停等待用户输入;关闭自动推进。会话内状态,
// 新建会话 / 刷新后回到默认关闭(与现状一致)。
const [step2Intervention, setStep2Intervention] = useState(false);
const configuredChain = useMemo( const configuredChain = useMemo(
() => chains.find((chain) => chain.id === mapping?.chainId) ?? null, () => chains.find((chain) => chain.id === mapping?.chainId) ?? null,
[chains, mapping?.chainId], [chains, mapping?.chainId],
@@ -84,14 +88,29 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa
() => buildEightBfTaskContext(task, taskDictionaries), () => buildEightBfTaskContext(task, taskDictionaries),
[task, taskDictionaries], [task, taskDictionaries],
); );
// 席位「产物提交 position」映射:业务链里逐席位配置的 artifact_position(agent_id → 值)。
// 提交产物时按产出该产物的席位智能体取值;未配置的席位不进 map(提交时回退默认 0)。
const artifactPositionByAgent = useMemo(() => {
const map = new Map<string, number>();
for (const seat of configuredChain?.seats ?? []) {
if (typeof seat.artifact_position === "number" && Number.isFinite(seat.artifact_position)) {
map.set(seat.agent_id, seat.artifact_position);
}
}
return map;
}, [configuredChain]);
const taskArtifactSubmissionConfig = useMemo<TaskArtifactSubmissionConfig | undefined>( const taskArtifactSubmissionConfig = useMemo<TaskArtifactSubmissionConfig | undefined>(
() => taskId ? { () => taskId ? {
taskId, taskId,
position: 0, position: 0,
positionFor: (target) => {
const agentId = target.agentId?.trim();
return agentId ? artifactPositionByAgent.get(agentId) ?? null : null;
},
createProcessReport: createEightBfTaskProcessReport, createProcessReport: createEightBfTaskProcessReport,
updateProcessReport: updateEightBfTaskProcessReport, updateProcessReport: updateEightBfTaskProcessReport,
} : undefined, } : undefined,
[taskId], [taskId, artifactPositionByAgent],
); );
useEffect(() => { useEffect(() => {
@@ -260,6 +279,12 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa
// 无 taskId 进入时每次都开全新会话(不回显 anonymous 键下的最新草稿);带 taskId 保持回显现状。 // 无 taskId 进入时每次都开全新会话(不回显 anonymous 键下的最新草稿);带 taskId 保持回显现状。
skipInitialDraftRestore={!taskId} skipInitialDraftRestore={!taskId}
taskContext={taskContext as Record<string, unknown> | null} taskContext={taskContext as Record<string, unknown> | null}
// 第一步绑定 8BF 专属内置意图智能体(roundtable-intent 的同源复制,
// 提示词独立可调优),不再与通用多智能体会商共用一个 SOUL。
intentAgentId={EIGHTBF_INTENT_AGENT_ID}
// 页头「人工干预」开关:控制第二步是否每步暂停等待用户输入。
step2InterventionEnabled={step2Intervention}
onStep2InterventionChange={setStep2Intervention}
entryRailTaskId={taskId || null} entryRailTaskId={taskId || null}
entryRailTaskCardSlot={taskId ? ( entryRailTaskCardSlot={taskId ? (
<EightBfTaskCard <EightBfTaskCard
+87 -8
View File
@@ -13,6 +13,7 @@ import { useLocation } from "react-router-dom";
import { useRouteBase } from "@/core/route-base"; import { useRouteBase } from "@/core/route-base";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -48,6 +49,7 @@ import { useNotification } from "@/core/notification/hooks";
import { saveLastAgentId, useThreadSettings } from "@/core/settings"; import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks"; import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed"; import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream"; import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
import { textOfMessage } from "@/core/threads/utils"; import { textOfMessage } from "@/core/threads/utils";
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode"; import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
@@ -560,9 +562,26 @@ export default function AgentChatPage({
}, },
}); });
const directChat = useDirectChatMessages(threadId);
const directChatSelected =
isDirectChatProfile(settings.context) && isTurboModeVisible();
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md // 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
// 流式 stub),成功且真实产物落地后自动撤出。 // 流式 stub),成功且真实产物落地后自动撤出。
const { displayThread } = useResearchWritingFeed(threadId, thread); const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
const displayThread = useMemo(() => {
const base = researchDisplayThread;
if (directChat.messages.length === 0 && !directChat.isLoading) {
return base;
}
const messages = [...base.messages, ...directChat.messages];
return {
...base,
values: { ...base.values, messages },
messages,
isLoading: base.isLoading || directChat.isLoading,
} as typeof base;
}, [directChat.isLoading, directChat.messages, researchDisplayThread]);
// ── agentfx「入库」协助卡 ────────────────────────────────────────────────── // ── agentfx「入库」协助卡 ──────────────────────────────────────────────────
// 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且 // 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且
@@ -743,6 +762,62 @@ export default function AgentChatPage({
const handleSubmit = useCallback( const handleSubmit = useCallback(
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => { (message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
if (directChatSelected) {
if (message.files && message.files.length > 0) {
toast.error("极速问答暂不支持附件,请移除附件后重试。");
return;
}
setHasStartedConversation(true);
saveLastAgentId(agent_id);
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
void (async () => {
try {
let targetThreadId = threadId;
if (isNewThread) {
const created = await getAPIClient().threads.create({
metadata: {
agent_id,
...(Object.keys(conversationMetadata).length > 0
? conversationMetadata
: {}),
},
});
targetThreadId = created.thread_id;
knowledgeSelectionThread.current = targetThreadId;
activeThreadIdRef.current = targetThreadId;
setThreadId(targetThreadId);
setIsNewThread(false);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${agentChatsBase}/${targetThreadId}`,
);
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
if (title) {
void getAPIClient().threads
.updateState(targetThreadId, { values: { title } })
.catch(() => undefined);
}
}
if (!targetThreadId) {
toast.error("无法创建会话。");
return;
}
await directChat.submit({
targetThreadId,
query: message.text,
modelName: settings.context.model_name,
});
} catch (error) {
toast.error(
error instanceof Error ? error.message : "极速问答失败。",
);
}
})();
return;
}
setHasStartedConversation(true); setHasStartedConversation(true);
// Remember this agent so the new-chat landing screen pre-selects it next // Remember this agent so the new-chat landing screen pre-selects it next
// time (covers both direct agent chats and the ChatPage handoff replay). // time (covers both direct agent chats and the ChatPage handoff replay).
@@ -767,7 +842,7 @@ export default function AgentChatPage({
...pageReportThemeContext, ...pageReportThemeContext,
}); });
}, },
[sendMessage, threadId, agent_id, settings.context.writing_mode, writingArtifactPath, baseMode], [sendMessage, threadId, agent_id, settings.context.writing_mode, settings.context.model_name, writingArtifactPath, baseMode, directChat, directChatSelected, isNewThread, conversationMetadata, agentChatsBase, setThreadId, setIsNewThread],
); );
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。 // 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
@@ -852,8 +927,12 @@ export default function AgentChatPage({
); );
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
if (directChat.isLoading) {
directChat.stop();
return;
}
await thread.stop(); await thread.stop();
}, [thread]); }, [directChat, thread]);
const isLandingState = isNewThread && !hasStartedConversation; const isLandingState = isNewThread && !hasStartedConversation;
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。 // 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
@@ -867,7 +946,7 @@ export default function AgentChatPage({
const status = thread.error const status = thread.error
? "error" ? "error"
: thread.isLoading : thread.isLoading || directChat.isLoading
? "streaming" ? "streaming"
: "ready"; : "ready";
@@ -1084,7 +1163,7 @@ export default function AgentChatPage({
autoFocus autoFocus
status={status} status={status}
context={settings.context} context={settings.context}
disabled={isDemoMode || knowledgeSelectionLoading} disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
onContextChange={(context) => onContextChange={(context) =>
setSettings("context", context) setSettings("context", context)
} }
@@ -1104,7 +1183,7 @@ export default function AgentChatPage({
pendingKnowledgeSelection.current === null && pendingKnowledgeSelection.current === null &&
pendingAssistantKnowledgeSelection.current === null pendingAssistantKnowledgeSelection.current === null
} }
disabled={isDemoMode || knowledgeSelectionLoading} disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
label="知识空间" label="知识空间"
/> />
) )
@@ -1175,7 +1254,7 @@ export default function AgentChatPage({
assistantId={agent_id} assistantId={agent_id}
status={status} status={status}
context={settings.context} context={settings.context}
disabled={isDemoMode || knowledgeSelectionLoading} disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
onContextChange={(context) => onContextChange={(context) =>
setSettings("context", context) setSettings("context", context)
} }
@@ -1190,7 +1269,7 @@ export default function AgentChatPage({
assistantValue={selectedAssistantKnowledgeBaseIds} assistantValue={selectedAssistantKnowledgeBaseIds}
onChange={changeKnowledgeSelection} onChange={changeKnowledgeSelection}
onAssistantChange={changeAssistantKnowledgeSelection} onAssistantChange={changeAssistantKnowledgeSelection}
disabled={isDemoMode || knowledgeSelectionLoading} disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
readOnly readOnly
label="知识空间" label="知识空间"
/> />
+97 -19
View File
@@ -64,11 +64,16 @@ import {
useThreadSettings, useThreadSettings,
} from "@/core/settings"; } from "@/core/settings";
import { useThreadStream, stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks"; import { useThreadStream, stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks";
import {
myProjectConversationTypeFromSearchParams,
myProjectThreadMetadata,
} from "@/core/threads/project-metadata";
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed"; import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream"; import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode"; import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit"; import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag"; import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag"; import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag";
import { useLayoutMode } from "@/core/system-settings/hooks"; import { useLayoutMode } from "@/core/system-settings/hooks";
import { useRouteBase } from "@/core/route-base"; import { useRouteBase } from "@/core/route-base";
@@ -285,16 +290,28 @@ export default function ChatPage() {
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]); }, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const projectConversationType =
myProjectConversationTypeFromSearchParams(searchParams);
const newThreadMetadata = useMemo<Record<string, unknown> | undefined>(() => {
const metadata: Record<string, unknown> = projectConversationType
? myProjectThreadMetadata(projectConversationType)
: {};
if (knowledgeSelectionReady) {
metadata.llmwiki_knowledge_base_ids = selectedKnowledgeBaseIds;
metadata.assistant_knowledge_base_ids = selectedAssistantKnowledgeBaseIds;
}
return Object.keys(metadata).length > 0 ? metadata : undefined;
}, [
knowledgeSelectionReady,
projectConversationType,
selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds,
]);
const [thread, sendMessage, isUploading] = useThreadStream({ const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : threadId, threadId: isNewThread ? undefined : threadId,
context: streamContext, context: streamContext,
threadMetadata: knowledgeSelectionReady threadMetadata: newThreadMetadata,
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: undefined,
isMock, isMock,
onStart: (createdThreadId) => { onStart: (createdThreadId) => {
knowledgeSelectionThread.current = createdThreadId; knowledgeSelectionThread.current = createdThreadId;
@@ -348,9 +365,12 @@ export default function ChatPage() {
}); });
const singleRoundRag = useSingleRoundRagMessages(threadId); const singleRoundRag = useSingleRoundRagMessages(threadId);
const multiRoundRag = useMultiRoundRagMessages(threadId); const multiRoundRag = useMultiRoundRagMessages(threadId);
const directChat = useDirectChatMessages(threadId);
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context); const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context); const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
const frontendRagSelected = isFrontendRagProfile(settings.context); const frontendRagSelected = isFrontendRagProfile(settings.context);
const directChatSelected =
isDirectChatProfile(settings.context) && isTurboModeVisible();
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md // 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
// 流式 stub),成功且真实产物落地后自动撤出。 // 流式 stub),成功且真实产物落地后自动撤出。
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread); const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
@@ -360,13 +380,15 @@ export default function ChatPage() {
if ( if (
singleRoundRag.messages.length === 0 && singleRoundRag.messages.length === 0 &&
ragMessages.length === 0 && ragMessages.length === 0 &&
directChat.messages.length === 0 &&
!singleRoundRag.isLoading && !singleRoundRag.isLoading &&
!multiRoundRag.isLoading !multiRoundRag.isLoading &&
!directChat.isLoading
) { ) {
return base; return base;
} }
const messages = mergeMessagesByTime( const messages = mergeMessagesByTime(
[...base.messages, ...singleRoundRag.messages], [...base.messages, ...singleRoundRag.messages, ...directChat.messages],
ragMessages, ragMessages,
); );
return { return {
@@ -374,9 +396,14 @@ export default function ChatPage() {
values: { ...base.values, messages }, values: { ...base.values, messages },
messages, messages,
isLoading: isLoading:
base.isLoading || singleRoundRag.isLoading || multiRoundRag.isLoading, base.isLoading ||
singleRoundRag.isLoading ||
multiRoundRag.isLoading ||
directChat.isLoading,
} as typeof base; } as typeof base;
}, [ }, [
directChat.isLoading,
directChat.messages,
multiRoundRag.isLoading, multiRoundRag.isLoading,
multiRoundRag.messages, multiRoundRag.messages,
researchDisplayThread, researchDisplayThread,
@@ -392,6 +419,52 @@ export default function ChatPage() {
const handleSubmit = useCallback( const handleSubmit = useCallback(
(message: PromptInputMessage) => { (message: PromptInputMessage) => {
if (directChatSelected) {
if (message.files && message.files.length > 0) {
toast.error("极速问答暂不支持附件,请移除附件后重试。");
return;
}
setHasStartedConversation(true);
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
void (async () => {
try {
let targetThreadId = threadId;
if (isNewThread) {
const created = await getAPIClient(isMock).threads.create({});
targetThreadId = created.thread_id;
setThreadId(targetThreadId);
setIsNewThread(false);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${routeBase}/chats/${targetThreadId}`,
);
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
if (title) {
void getAPIClient(isMock).threads
.updateState(targetThreadId, { values: { title } })
.catch(() => undefined);
}
}
if (!targetThreadId) {
toast.error("无法创建会话。");
return;
}
await directChat.submit({
targetThreadId,
query: message.text,
modelName: settings.context.model_name,
});
} catch (error) {
toast.error(
error instanceof Error ? error.message : "极速问答失败。",
);
}
})();
return;
}
if (frontendRagSelected) { if (frontendRagSelected) {
if (message.files && message.files.length > 0) { if (message.files && message.files.length > 0) {
toast.error("RAG 问答暂不支持附件,请移除附件后重试。"); toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
@@ -405,7 +478,9 @@ export default function ChatPage() {
try { try {
let targetThreadId = threadId; let targetThreadId = threadId;
if (isNewThread) { if (isNewThread) {
const created = await getAPIClient(isMock).threads.create({}); const created = await getAPIClient(isMock).threads.create({
metadata: newThreadMetadata ?? {},
});
targetThreadId = created.thread_id; targetThreadId = created.thread_id;
setThreadId(targetThreadId); setThreadId(targetThreadId);
setIsNewThread(false); setIsNewThread(false);
@@ -465,13 +540,8 @@ export default function ChatPage() {
try { try {
const created = await getAPIClient(isMock).threads.create({ const created = await getAPIClient(isMock).threads.create({
metadata: { metadata: {
...newThreadMetadata,
agent_id: selectedAgentId, agent_id: selectedAgentId,
...(knowledgeSelectionReady
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
}, },
}); });
sessionStorage.setItem( sessionStorage.setItem(
@@ -490,7 +560,7 @@ export default function ChatPage() {
); );
saveLastAgentId(selectedAgentId); saveLastAgentId(selectedAgentId);
router.push( router.push(
`/page/workspace/agents/${selectedAgentId}/chats/${created.thread_id}`, `${routeBase}/agents/${encodeURIComponent(selectedAgentId)}/chats/${encodeURIComponent(created.thread_id)}`,
); );
} catch (err) { } catch (err) {
agentHandoffPendingRef.current = false; agentHandoffPendingRef.current = false;
@@ -549,6 +619,7 @@ export default function ChatPage() {
selectedAssistantKnowledgeBaseIds, selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds, selectedKnowledgeBaseIds,
knowledgeSelectionReady, knowledgeSelectionReady,
newThreadMetadata,
agentHandoffPending, agentHandoffPending,
isMock, isMock,
router, router,
@@ -560,6 +631,8 @@ export default function ChatPage() {
multiRoundRag, multiRoundRag,
multiRoundRagSelected, multiRoundRagSelected,
frontendRagSelected, frontendRagSelected,
directChat,
directChatSelected,
routeBase, routeBase,
setIsNewThread, setIsNewThread,
setThreadId, setThreadId,
@@ -655,6 +728,10 @@ export default function ChatPage() {
saveLastAgentId(agentId); saveLastAgentId(agentId);
}, []); }, []);
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
if (directChat.isLoading) {
directChat.stop();
return;
}
if (singleRoundRag.isLoading) { if (singleRoundRag.isLoading) {
singleRoundRag.stop(); singleRoundRag.stop();
return; return;
@@ -664,7 +741,7 @@ export default function ChatPage() {
return; return;
} }
await thread.stop(); await thread.stop();
}, [multiRoundRag, singleRoundRag, thread]); }, [directChat, multiRoundRag, singleRoundRag, thread]);
const promptPrefixProp = useMemo(() => { const promptPrefixProp = useMemo(() => {
if (!promptPrefixSettings?.enabled) return undefined; if (!promptPrefixSettings?.enabled) return undefined;
@@ -914,7 +991,8 @@ export default function ChatPage() {
agentHandoffPending || agentHandoffPending ||
knowledgeSelectionLoading || knowledgeSelectionLoading ||
singleRoundRag.isLoading || singleRoundRag.isLoading ||
multiRoundRag.isLoading multiRoundRag.isLoading ||
directChat.isLoading
} }
onContextChange={(context) => onContextChange={(context) =>
setSettings("context", context) setSettings("context", context)
@@ -5,6 +5,7 @@ import {
ChevronDown, ChevronDown,
CircleHelp, CircleHelp,
Clock3, Clock3,
FolderKanban,
LoaderCircle, LoaderCircle,
MessageSquareText, MessageSquareText,
PanelLeftClose, PanelLeftClose,
@@ -20,7 +21,12 @@ import {
} from "lucide-react"; } from "lucide-react";
import { type FormEvent, useCallback, useMemo, useState } from "react"; import { type FormEvent, useCallback, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Toaster } from "sonner";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useAgent, useAgents } from "@/core/agents"; import { useAgent, useAgents } from "@/core/agents";
import { getAccountDisplayName } from "@/core/auth"; import { getAccountDisplayName } from "@/core/auth";
import { import {
@@ -29,28 +35,41 @@ import {
useScheduledTaskRuns, useScheduledTaskRuns,
useScheduledTasks, useScheduledTasks,
} from "@/core/scheduled-tasks"; } from "@/core/scheduled-tasks";
import { useLocalSettings } from "@/core/settings";
import { import {
useAppearanceSettings, useAppearanceSettings,
useCitationDisplaySettings, useCitationDisplaySettings,
} from "@/core/system-settings/hooks"; } from "@/core/system-settings/hooks";
import { useThreadStream, useThreads } from "@/core/threads/hooks"; import { useThreadStream, useThreads } from "@/core/threads/hooks";
import {
myProjectConversationTypeOfThread,
myProjectConversationTypeFromSearchParams,
myProjectThreadMetadata,
} from "@/core/threads/project-metadata";
import { textOfMessage, titleOfThread } from "@/core/threads/utils"; import { textOfMessage, titleOfThread } from "@/core/threads/utils";
import { uuid } from "@/core/utils/uuid"; import { uuid } from "@/core/utils/uuid";
import { import {
OFFICIAL_DEERFLOW_HOME_ROUTE, OFFICIAL_DEERFLOW_HOME_ROUTE,
OFFICIAL_DEERFLOW_ROUTE_PREFIX, OFFICIAL_DEERFLOW_ROUTE_PREFIX,
} from "@/core/official-deerflow/routes"; } from "@/core/official-deerflow/routes";
import StrategyProjectsPage from "@/strategy-selection/StrategyProjectsPage";
import StrategySelectionPage from "@/strategy-selection/StrategySelectionPage";
import "./official-deerflow-entry.css"; import "./official-deerflow-entry.css";
type OfficialPage = "chat" | "agents" | "agent-chat" | "scheduled" | "settings"; type OfficialPage =
| "chat"
| "projects"
| "agents"
| "agent-chat"
| "scheduled"
| "settings";
function pageFromPath(pathname: string): OfficialPage { function pageFromPath(pathname: string): OfficialPage {
const path = pathname.slice(OFFICIAL_DEERFLOW_ROUTE_PREFIX.length); const path = pathname.slice(OFFICIAL_DEERFLOW_ROUTE_PREFIX.length);
if (path.startsWith("/workspace/agents/") && path.includes("/chats")) { if (path.startsWith("/workspace/agents/") && path.includes("/chats")) {
return "agent-chat"; return "agent-chat";
} }
if (path.startsWith("/workspace/projects")) return "projects";
if (path.startsWith("/workspace/agents")) return "agents"; if (path.startsWith("/workspace/agents")) return "agents";
if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled"; if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled";
if (path.startsWith("/workspace/settings")) return "settings"; if (path.startsWith("/workspace/settings")) return "settings";
@@ -78,6 +97,11 @@ function parseScheduledTaskId(pathname: string) {
return match ? decodeURIComponent(match[1]!) : null; return match ? decodeURIComponent(match[1]!) : null;
} }
function parseProjectId(pathname: string) {
const match = pathname.match(/\/workspace\/projects\/([^/]+)/);
return match ? decodeURIComponent(match[1]!) : null;
}
function readableError(error: unknown, fallback: string) { function readableError(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback; return error instanceof Error && error.message ? error.message : fallback;
} }
@@ -159,20 +183,36 @@ function ChatWorkspace({
routeThreadId: string; routeThreadId: string;
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const [sendError, setSendError] = useState<string | null>(null); const [sendError, setSendError] = useState<string | null>(null);
const isNewThread = routeThreadId === "new"; const isNewThread = routeThreadId === "new";
const projectConversationType = isNewThread
? myProjectConversationTypeFromSearchParams(
new URLSearchParams(location.search),
)
: null;
// A new conversation uses the same client-generated id approach as the // A new conversation uses the same client-generated id approach as the
// existing ChatPage. The backend owns the persisted id once streaming starts. // existing ChatPage. The backend owns the persisted id once streaming starts.
const [draftThreadId] = useState<string>(() => uuid() as string); const [draftThreadId] = useState<string>(() => uuid() as string);
const activeThreadId = isNewThread ? draftThreadId : routeThreadId; const activeThreadId = isNewThread ? draftThreadId : routeThreadId;
const { agent } = useAgent(agentId, { enabled: !!agentId }); const { agent } = useAgent(agentId, { enabled: !!agentId });
const [localSettings] = useLocalSettings();
const [thread, sendMessage, isUploading] = useThreadStream({ const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : routeThreadId, threadId: isNewThread ? undefined : routeThreadId,
assistantId: agentId ?? undefined, assistantId: agentId ?? undefined,
threadMetadata: projectConversationType
? myProjectThreadMetadata(projectConversationType)
: undefined,
context: { context: {
...localSettings.context, // This compact official shell has no model/mode controls of its own.
// Do not inherit a hidden selection saved by the customized workspace
// (for example an obsolete or quota-exhausted model); an omitted model
// lets the backend resolve the current system default or the agent's
// globally configured model.
model_name: undefined,
mode: undefined,
reasoning_effort: undefined,
writing_mode: false,
...(agentId ? { agent_id: agentId } : {}), ...(agentId ? { agent_id: agentId } : {}),
}, },
onStart: (createdThreadId) => { onStart: (createdThreadId) => {
@@ -658,6 +698,10 @@ export default function OfficialDeerFlowEntry() {
() => parseScheduledTaskId(location.pathname), () => parseScheduledTaskId(location.pathname),
[location.pathname], [location.pathname],
); );
const projectId = useMemo(
() => parseProjectId(location.pathname),
[location.pathname],
);
const { data: threads = [], isLoading: threadsLoading, error: threadsError } = const { data: threads = [], isLoading: threadsLoading, error: threadsError } =
useThreads({ useThreads({
limit: 20, limit: 20,
@@ -666,10 +710,12 @@ export default function OfficialDeerFlowEntry() {
select: ["thread_id", "updated_at", "values", "metadata"], select: ["thread_id", "updated_at", "values", "metadata"],
}); });
const go = (path: string) => navigate(path); const go = (path: string) => navigate(path);
const projectBasePath = `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/projects`;
const nav = [ const nav = [
{ icon: PlusSquare, label: "新对话", path: OFFICIAL_DEERFLOW_HOME_ROUTE, pages: ["chat"] }, { icon: PlusSquare, label: "新对话", path: OFFICIAL_DEERFLOW_HOME_ROUTE, pages: ["chat"] },
{ icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] }, { icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] },
{ icon: FolderKanban, label: "我的项目", path: projectBasePath, pages: ["projects"] },
{ icon: Bot, label: "智能体", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents`, pages: ["agents", "agent-chat"] }, { icon: Bot, label: "智能体", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents`, pages: ["agents", "agent-chat"] },
{ icon: CalendarClock, label: "定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`, pages: ["scheduled"] }, { icon: CalendarClock, label: "定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`, pages: ["scheduled"] },
] as const; ] as const;
@@ -706,29 +752,43 @@ export default function OfficialDeerFlowEntry() {
) : threadsError ? ( ) : threadsError ? (
<p className="official-sidebar-error">对话列表加载失败</p> <p className="official-sidebar-error">对话列表加载失败</p>
) : threads.length ? ( ) : threads.length ? (
threads.map((chat) => ( threads.map((chat) => {
<button const projectType = myProjectConversationTypeOfThread(chat);
className={`official-recent-chat${chatRoute.threadId === chat.thread_id ? " is-active" : ""}`} return (
key={chat.thread_id} <button
onClick={() => { className={`official-recent-chat${chatRoute.threadId === chat.thread_id ? " is-active" : ""}`}
const agentId = typeof chat.context?.agent_id === "string" key={chat.thread_id}
? chat.context.agent_id onClick={() => {
: typeof chat.metadata?.agent_id === "string" const agentId = typeof chat.context?.agent_id === "string"
? chat.metadata.agent_id ? chat.context.agent_id
: null; : typeof chat.metadata?.agent_id === "string"
go( ? chat.metadata.agent_id
agentId : null;
? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(chat.thread_id)}` go(
: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(chat.thread_id)}`, agentId
); ? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(chat.thread_id)}`
}} : `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(chat.thread_id)}`,
type="button" );
> }}
<MessageSquareText aria-hidden="true" size={15} /> type="button"
<span>{titleOfThread(chat, "未命名对话")}</span> >
<small>{formatTimestamp(chat.updated_at)}</small> {projectType ? (
</button> <MyProjectThreadIcon className="size-[15px]" type={projectType} />
)) ) : (
<MessageSquareText aria-hidden="true" size={15} />
)}
<span className="official-recent-chat-copy">
<span className="official-recent-chat-title">
{titleOfThread(chat, "未命名对话")}
</span>
{projectType && (
<MyProjectThreadBadge type={projectType} />
)}
</span>
<small>{formatTimestamp(chat.updated_at)}</small>
</button>
);
})
) : ( ) : (
<p className="official-sidebar-empty">暂无历史对话</p> <p className="official-sidebar-empty">暂无历史对话</p>
)} )}
@@ -754,6 +814,17 @@ export default function OfficialDeerFlowEntry() {
routeThreadId={chatRoute.threadId} routeThreadId={chatRoute.threadId}
/> />
)} )}
{page === "projects" && (
projectId ? (
<StrategySelectionPage
key={projectId}
projectBasePath={projectBasePath}
projectId={projectId}
/>
) : (
<StrategyProjectsPage basePath={projectBasePath} />
)
)}
{page === "agents" && ( {page === "agents" && (
<AgentsPage <AgentsPage
openAgent={(agentId) => openAgent={(agentId) =>
@@ -771,6 +842,7 @@ export default function OfficialDeerFlowEntry() {
{page === "scheduled" && <ScheduledTasksPage selectedTaskId={scheduledTaskId} />} {page === "scheduled" && <ScheduledTasksPage selectedTaskId={scheduledTaskId} />}
{page === "settings" && <SettingsPage />} {page === "settings" && <SettingsPage />}
</div> </div>
<Toaster position="top-center" />
</main> </main>
); );
} }
+22 -1
View File
@@ -1,4 +1,4 @@
import { Navigate, Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes, useParams } from "react-router-dom";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
@@ -78,6 +78,8 @@ import UsersPage from "./UsersPage";
import LlmMetricsPage from "./LlmMetricsPage"; import LlmMetricsPage from "./LlmMetricsPage";
import AdminScheduledTasksPage from "./AdminScheduledTasksPage"; import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
import AdminUserActivityPage from "./AdminUserActivityPage"; import AdminUserActivityPage from "./AdminUserActivityPage";
import StrategyProjectsPage from "@/strategy-selection/StrategyProjectsPage";
import StrategySelectionPage from "@/strategy-selection/StrategySelectionPage";
import { ChatRuntime } from "./WorkspaceRoutes"; import { ChatRuntime } from "./WorkspaceRoutes";
/** /**
@@ -98,6 +100,11 @@ function StrategyChatThreadLayout() {
); );
} }
function StrategyProjectWorkbenchRoute() {
const { project_id: projectId = "" } = useParams<{ project_id: string }>();
return <StrategySelectionPage key={projectId} projectId={projectId} />;
}
export default function StrategyRoutes() { export default function StrategyRoutes() {
const storedAuth = getStoredAuth(); const storedAuth = getStoredAuth();
const canManageBusinessMappings = !storedAuth || storedAuth.system_role === "admin"; const canManageBusinessMappings = !storedAuth || storedAuth.system_role === "admin";
@@ -110,6 +117,20 @@ export default function StrategyRoutes() {
<Route index element={<Navigate to="qa/general" replace />} /> <Route index element={<Navigate to="qa/general" replace />} />
<Route path="qa/general" element={<GeneralQA />} /> <Route path="qa/general" element={<GeneralQA />} />
<Route
path="qa/projects"
element={
<StrategyProjectsPage basePath="/page/strategy/qa/projects" />
}
/>
<Route
path="qa/projects/:project_id"
element={<StrategyProjectWorkbenchRoute />}
/>
<Route
path="qa/strategy-selection"
element={<Navigate to="/page/strategy/qa/projects" replace />}
/>
<Route <Route
path="qa/expert-qa" path="qa/expert-qa"
element={ element={
+18 -1
View File
@@ -309,7 +309,24 @@ export default function TaskButtonsPage() {
className="h-8 w-24" className="h-8 w-24"
title="发给宿主的参数名(默认 id)" title="发给宿主的参数名(默认 id)"
/> />
<span className="text-muted-foreground">= 任务id(数字)</span> <span className="text-muted-foreground">=</span>
{/* 行动分析(xdfx):行动id 与任务id 不同,可选发哪种;其它业务固定任务id */}
{tab === "xdfx" ? (
<Select
value={r.idKind}
onValueChange={(v) => update(r.id, { idKind: v as TaskButton["idKind"] })}
>
<SelectTrigger className="h-8 w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="task">任务id</SelectItem>
<SelectItem value="action">行动id</SelectItem>
</SelectContent>
</Select>
) : (
<span className="text-muted-foreground">任务id(数字)</span>
)}
</div> </div>
)} )}
<span className="text-muted-foreground w-full text-[11px]"> <span className="text-muted-foreground w-full text-[11px]">
@@ -8,8 +8,10 @@
* 全新部署自动播种「通用分析结构 / 递进式论证结构」),支持 启用/停用 筛选 * 全新部署自动播种「通用分析结构 / 递进式论证结构」),支持 启用/停用 筛选
* 与 标题 筛选;卡片上可 switch 启停、编辑、删除(二次确认),并显示 * 与 标题 筛选;卡片上可 switch 启停、编辑、删除(二次确认),并显示
* 最后修改时间与创建用户名;新增时不填状态(服务端强制默认启用)。 * 最后修改时间与创建用户名;新增时不填状态(服务端强制默认启用)。
* - 详情视图:点击卡片即「选择」该结构(内容回填到高级配置的报告结构 * **点击卡片即「选择」该结构(内容回填到高级配置的报告结构 custom_outline,
* custom_outline),弹窗切换为展示 标题 + Markdown 大纲预览,右上角「返回」回列表。 * 卡片留在列表中高亮为当前使用,不进入详情)**。
* - 详情视图:通过卡片右上角「查看大纲结构」眼睛图标进入,展示 标题 + Markdown
* 大纲预览,右上角「返回」回列表。
*/ */
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -238,8 +240,9 @@ export function WorkflowConfigDialog({
toast.error(`「${structure.title}」已停用,请先启用后再选用`); toast.error(`「${structure.title}」已停用,请先启用后再选用`);
return; return;
} }
// 点击卡片只做「选择」:内容回填后留在列表视图(卡片高亮为当前使用),
// 详情视图通过卡片上的「查看大纲结构」眼睛图标进入。
onApply?.(structure.content, structure.structureMode, structure.retrievalDirections, structure.retrievalSkills); onApply?.(structure.content, structure.structureMode, structure.retrievalDirections, structure.retrievalSkills);
setDetailId(structure.id);
toast.success(`已选择报告结构「${structure.title}」,并填充到报告结构配置`); toast.success(`已选择报告结构「${structure.title}」,并填充到报告结构配置`);
}; };
@@ -325,7 +328,7 @@ export function WorkflowConfigDialog({
</h2> </h2>
</div> </div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400 sm:text-[13px]"> <p className="mt-1 text-xs text-slate-500 dark:text-slate-400 sm:text-[13px]">
点击卡片选择报告结构(自动填充到报告结构配置);可启停、编辑、删除。 点击卡片选择报告结构(自动填充到报告结构配置);点击卡片上的眼睛图标查看大纲详情;可启停、编辑、删除。
</p> </p>
</div> </div>
<Button <Button
@@ -501,7 +504,7 @@ export function WorkflowConfigDialog({
key={structure.id} key={structure.id}
structure={structure} structure={structure}
applied={structure.id === appliedStructureId} applied={structure.id === appliedStructureId}
onOpen={() => handleSelect(structure)} onSelect={() => handleSelect(structure)}
onPreview={(event) => { onPreview={(event) => {
event.stopPropagation(); event.stopPropagation();
setDetailId(structure.id); setDetailId(structure.id);
@@ -750,7 +753,7 @@ export function WorkflowConfigDialog({
function StructureCard({ function StructureCard({
structure, structure,
applied, applied,
onOpen, onSelect,
onPreview, onPreview,
onDuplicate, onDuplicate,
onEdit, onEdit,
@@ -760,7 +763,7 @@ function StructureCard({
}: { }: {
structure: ReportStructure; structure: ReportStructure;
applied: boolean; applied: boolean;
onOpen: () => void; onSelect: () => void;
onPreview: (event: React.MouseEvent) => void; onPreview: (event: React.MouseEvent) => void;
onDuplicate: (event: React.MouseEvent) => void; onDuplicate: (event: React.MouseEvent) => void;
onEdit: (event: React.MouseEvent) => void; onEdit: (event: React.MouseEvent) => void;
@@ -775,11 +778,11 @@ function StructureCard({
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={onOpen} onClick={onSelect}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") { if (event.key === "Enter" || event.key === " ") {
event.preventDefault(); event.preventDefault();
onOpen(); onSelect();
} }
}} }}
className={`group relative flex cursor-pointer select-none flex-col justify-between rounded-2xl border p-5 text-left transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-[#F5222D]/30 ${ className={`group relative flex cursor-pointer select-none flex-col justify-between rounded-2xl border p-5 text-left transition-all duration-200 outline-none focus-visible:ring-2 focus-visible:ring-[#F5222D]/30 ${
@@ -64,6 +64,8 @@
.official-recent-section > p { color: #90908e; font-size: 12px; margin: 0 8px 9px; } .official-recent-section > p { color: #90908e; font-size: 12px; margin: 0 8px 9px; }
.official-recent-chat { display: grid; font-size: 13px; gap: 8px; grid-template-columns: 15px minmax(0, 1fr) auto; padding: 8px; } .official-recent-chat { display: grid; font-size: 13px; gap: 8px; grid-template-columns: 15px minmax(0, 1fr) auto; padding: 8px; }
.official-recent-chat span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .official-recent-chat span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-recent-chat-copy { align-items: center; display: flex; gap: 6px; min-width: 0; }
.official-recent-chat-title { flex: 1; min-width: 0; }
.official-recent-chat small { color: #acaca9; font-size: 11px; } .official-recent-chat small { color: #acaca9; font-size: 11px; }
.official-recent-chat.is-active { background: #efefed; color: #252525; } .official-recent-chat.is-active { background: #efefed; color: #252525; }
.official-sidebar-loading, .official-sidebar-loading,
@@ -26,6 +26,11 @@ interface PositionIntentInputProps extends Step1InputAreaProps {
selectedModel?: string; selectedModel?: string;
onSelectedModelChange?: (model: string) => void; onSelectedModelChange?: (model: string) => void;
commonQuestions?: (string | AgentQuestion)[] | null; commonQuestions?: (string | AgentQuestion)[] | null;
/**
* 点击常用问题 chip 时直接把问题发送出去(8BF 第一步)。
* 缺省 false:仅填充输入框(岗位会商等既有页面的行为保持不变)。
*/
sendCommonQuestionOnClick?: boolean;
/** 仅保留上传、模型选择和发送的紧凑输入区(用于会商第 3 步)。 */ /** 仅保留上传、模型选择和发送的紧凑输入区(用于会商第 3 步)。 */
hideUtilityActions?: boolean; hideUtilityActions?: boolean;
} }
@@ -68,6 +73,7 @@ export function PositionIntentInput({
selectedModel, selectedModel,
onSelectedModelChange, onSelectedModelChange,
commonQuestions, commonQuestions,
sendCommonQuestionOnClick = false,
hideUtilityActions = false, hideUtilityActions = false,
}: PositionIntentInputProps) { }: PositionIntentInputProps) {
// Files are owned by Step1Panel. This ref only opens the native picker and // Files are owned by Step1Panel. This ref only opens the native picker and
@@ -86,7 +92,13 @@ export function PositionIntentInput({
: (modelOptions[0]?.value ?? ""); : (modelOptions[0]?.value ?? "");
const pickCommonQuestion = (question: AgentQuestion) => { const pickCommonQuestion = (question: AgentQuestion) => {
onValueChange(question.prompt); // 直接发送模式:以问题全文为覆盖文本调 onSend,绕过输入框 state 的
// 异步更新(同 tick 里 onSend 闭包读不到刚 setValue 的新值)。
if (sendCommonQuestionOnClick) {
onSend(question.prompt);
} else {
onValueChange(question.prompt);
}
setShowMoreQuestions(false); setShowMoreQuestions(false);
}; };
@@ -160,6 +172,7 @@ export function PositionIntentInput({
type="button" type="button"
className="position-roundtable-intent-common-question" className="position-roundtable-intent-common-question"
title={question.prompt} title={question.prompt}
disabled={sendCommonQuestionOnClick ? isSending : undefined}
onClick={() => pickCommonQuestion(question)} onClick={() => pickCommonQuestion(question)}
> >
<span aria-hidden="true" /> <span aria-hidden="true" />
@@ -185,6 +198,7 @@ export function PositionIntentInput({
key={`${question.prompt}-${index}`} key={`${question.prompt}-${index}`}
type="button" type="button"
title={question.prompt} title={question.prompt}
disabled={sendCommonQuestionOnClick ? isSending : undefined}
onClick={() => pickCommonQuestion(question)} onClick={() => pickCommonQuestion(question)}
> >
{question.title} {question.title}
@@ -268,7 +282,7 @@ export function PositionIntentInput({
<button <button
id="send-message-btn" id="send-message-btn"
type="button" type="button"
onClick={isSending ? onStop : onSend} onClick={() => (isSending ? onStop() : onSend())}
disabled={isSending ? false : !canSend} disabled={isSending ? false : !canSend}
aria-label={isSending ? "暂停生成" : "发送"} aria-label={isSending ? "暂停生成" : "发送"}
title={isSending ? "暂停生成" : "发送"} title={isSending ? "暂停生成" : "发送"}
@@ -12,6 +12,9 @@ export type { ClarificationPayload };
export const INTENT_AGENT_ID = "roundtable-intent"; export const INTENT_AGENT_ID = "roundtable-intent";
/** Dedicated Step 1 agent for the position-roundtable workspace. */ /** Dedicated Step 1 agent for the position-roundtable workspace. */
export const POSITION_INTENT_AGENT_ID = "position-roundtable-intent"; export const POSITION_INTENT_AGENT_ID = "position-roundtable-intent";
/** Dedicated Step 1 agent for the 8BF roundtable workspace (same prompt family
* as `roundtable-intent`, independently tunable via the agent config UI). */
export const EIGHTBF_INTENT_AGENT_ID = "eightbf-roundtable-intent";
/** /**
* Task-intent agent (Step 1) API. Mirror of multi-agent.ts but with a single * Task-intent agent (Step 1) API. Mirror of multi-agent.ts but with a single
@@ -81,7 +81,8 @@ export interface Step1InputAreaProps {
isSending: boolean; isSending: boolean;
canSend: boolean; canSend: boolean;
pendingClarification: boolean; pendingClarification: boolean;
onSend: () => void; /** 发送当前输入框内容;传 `overrideText` 时直接发送该文本(常用问题一键发送)。 */
onSend: (overrideText?: string) => void;
onStop: () => void; onStop: () => void;
onFollowUp: () => void; onFollowUp: () => void;
onSkip: () => void; onSkip: () => void;
@@ -236,9 +237,12 @@ export function Step1Panel(props: Step1PanelProps) {
setAttachedFiles((prev) => prev.filter((_, i) => i !== idx)); setAttachedFiles((prev) => prev.filter((_, i) => i !== idx));
}; };
const handleSend = () => { const handleSend = (overrideText?: string) => {
if (!inputText.trim() && attachedFiles.length === 0) return; // overrideText:常用问题 chip 一键发送——绕过异步的输入框 state,
onSendMessage(inputText, attachedFiles.length > 0 ? attachedFiles : undefined); // 直接以问题全文作为本回合消息(仍共享附件与发送后的清空逻辑)。
const textToSend = overrideText ?? inputText;
if (!textToSend.trim() && attachedFiles.length === 0) return;
onSendMessage(textToSend, attachedFiles.length > 0 ? attachedFiles : undefined);
handleInputTextChange(""); handleInputTextChange("");
setAttachedFiles([]); setAttachedFiles([]);
}; };
@@ -411,7 +415,7 @@ export function Step1Panel(props: Step1PanelProps) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
id="send-message-btn" id="send-message-btn"
onClick={handleSend} onClick={() => handleSend()}
disabled={isIntentSending || !intentThreadId} disabled={isIntentSending || !intentThreadId}
className="bg-primary text-white hover:bg-[var(--brand-900)] disabled:bg-slate-300 disabled:cursor-not-allowed text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-1.5 transition-all shadow-[0_2px_8px_rgba(245,34,45,0.15)] active:scale-95" className="bg-primary text-white hover:bg-[var(--brand-900)] disabled:bg-slate-300 disabled:cursor-not-allowed text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-1.5 transition-all shadow-[0_2px_8px_rgba(245,34,45,0.15)] active:scale-95"
> >
@@ -422,6 +422,12 @@ export interface UseStep2OrchestrationOptions {
seatSkillDirectives?: Record<string, boolean>; seatSkillDirectives?: Record<string, boolean>;
/** Per-seat human-validation checkpoints inherited from the selected chain. */ /** Per-seat human-validation checkpoints inherited from the selected chain. */
humanValidations?: Record<string, boolean>; humanValidations?: Record<string, boolean>;
/**
* 全局「人工干预」开关(8BF 页头):开启后第二步每走一步(链条每席位 / DAG 每 stage
* 完成)就暂停等待用户输入或恢复;关闭则全自动推进,与既有行为一致。与 per-seat
* `humanValidations` 取 OR——任一生效即在最近一步边界暂停。
*/
manualIntervention?: boolean;
/** /**
* 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位的配置)。 * 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位的配置)。
* 派活该 special 席位时按 agentId 查表,**席位覆盖优先、否则跟随链条级 `selectedMode`**: * 派活该 special 席位时按 agentId 查表,**席位覆盖优先、否则跟随链条级 `selectedMode`**:
@@ -593,6 +599,7 @@ export function useStep2Orchestration(
selectedMode, selectedMode,
seatSkillDirectives = {}, seatSkillDirectives = {},
humanValidations = {}, humanValidations = {},
manualIntervention = false,
seatModes = {}, seatModes = {},
gatherFirst = false, gatherFirst = false,
orchestrationMode, orchestrationMode,
@@ -746,6 +753,9 @@ export function useStep2Orchestration(
// 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。 // 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。
const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives); const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives);
const humanValidationsRef = useRef<Record<string, boolean>>(humanValidations); const humanValidationsRef = useRef<Record<string, boolean>>(humanValidations);
// 全局「人工干预」开关(8BF 页头):编排循环在每个一步边界读最新值,
// 中途切换只影响下一次边界,不打断在途 run。
const manualInterventionRef = useRef<boolean>(manualIntervention);
// 每席位推理深度覆盖映射(所选业务链条各席位 seatMode)打到 ref,席位调用按 agentId 读最新值; // 每席位推理深度覆盖映射(所选业务链条各席位 seatMode)打到 ref,席位调用按 agentId 读最新值;
// 覆盖优先、否则跟随 selectedModeRef。 // 覆盖优先、否则跟随 selectedModeRef。
const seatModesRef = useRef<Record<string, SeatMode | null>>(seatModes); const seatModesRef = useRef<Record<string, SeatMode | null>>(seatModes);
@@ -800,6 +810,9 @@ export function useStep2Orchestration(
useEffect(() => { useEffect(() => {
humanValidationsRef.current = humanValidations; humanValidationsRef.current = humanValidations;
}, [humanValidations]); }, [humanValidations]);
useEffect(() => {
manualInterventionRef.current = manualIntervention;
}, [manualIntervention]);
useEffect(() => { useEffect(() => {
seatModesRef.current = seatModes; seatModesRef.current = seatModes;
}, [seatModes]); }, [seatModes]);
@@ -1681,12 +1694,16 @@ export function useStep2Orchestration(
); );
const pauseForHumanValidation = useCallback( const pauseForHumanValidation = useCallback(
(continuation: HumanValidationContinuation, seatNames: string[]) => { (
continuation: HumanValidationContinuation,
seatNames: string[],
source = "业务链条配置",
) => {
humanValidationContinuationRef.current = continuation; humanValidationContinuationRef.current = continuation;
appendDialogue({ appendDialogue({
sender: "系统", sender: "系统",
avatarType: "blue", avatarType: "blue",
content: `已完成 ${seatNames.join("、")} 的输出,按业务链条配置暂停,等待人工校验。可直接点击“恢复会商”继续,或在下方输入修改意见后提交。`, content: `已完成 ${seatNames.join("、")} 的输出,按${source}暂停,等待人工校验。可直接点击“恢复会商”继续,或在下方输入修改意见后提交。`,
confidence: "等待人工校验", confidence: "等待人工校验",
}); });
setIsRoundTableRunning(false); setIsRoundTableRunning(false);
@@ -2793,18 +2810,27 @@ export function useStep2Orchestration(
} }
if (!chainInvalidNames.includes(seatName)) chainInvalidNames.push(seatName); if (!chainInvalidNames.includes(seatName)) chainInvalidNames.push(seatName);
finishedAllSeats = false; finishedAllSeats = false;
if (humanValidationsRef.current[seat.agent_id]) { if (manualInterventionRef.current || humanValidationsRef.current[seat.agent_id]) {
pauseForHumanValidation({ mode: "chain", nextSeatIndex: i }, [seatName]); pauseForHumanValidation(
{ mode: "chain", nextSeatIndex: i },
[seatName],
manualInterventionRef.current && !humanValidationsRef.current[seat.agent_id]
? "人工干预开关"
: undefined,
);
break; break;
} }
continue; continue;
} }
setConsensusPercentage(Math.min(95, Math.round(((i + 1) / total) * 90))); setConsensusPercentage(Math.min(95, Math.round(((i + 1) / total) * 90)));
if (humanValidationsRef.current[seat.agent_id]) { if (manualInterventionRef.current || humanValidationsRef.current[seat.agent_id]) {
pauseForHumanValidation( pauseForHumanValidation(
{ mode: "chain", nextSeatIndex: i + 1 }, { mode: "chain", nextSeatIndex: i + 1 },
[seatName], [seatName],
manualInterventionRef.current && !humanValidationsRef.current[seat.agent_id]
? "人工干预开关"
: undefined,
); );
finishedAllSeats = false; finishedAllSeats = false;
break; break;
@@ -3237,8 +3263,11 @@ export function useStep2Orchestration(
setConsensusPercentage( setConsensusPercentage(
Math.min(95, Math.round(((s + 1) / plan.stages.length) * 90)), Math.min(95, Math.round(((s + 1) / plan.stages.length) * 90)),
); );
const manualInterventionOn = manualInterventionRef.current;
const validationSeatNames = stageSeats const validationSeatNames = stageSeats
.filter(({ agentId }) => humanValidationsRef.current[agentId]) .filter(
({ agentId }) => manualInterventionOn || humanValidationsRef.current[agentId],
)
.map(({ name }) => name); .map(({ name }) => name);
if (validationSeatNames.length > 0) { if (validationSeatNames.length > 0) {
pauseForHumanValidation( pauseForHumanValidation(
@@ -3248,6 +3277,7 @@ export function useStep2Orchestration(
priorDeliveries: [...priorDeliveries], priorDeliveries: [...priorDeliveries],
}, },
validationSeatNames, validationSeatNames,
manualInterventionOn ? "人工干预开关" : undefined,
); );
finishedAllStages = false; finishedAllStages = false;
break; break;
@@ -55,12 +55,14 @@ function payloadFrom(
): TaskProcessReportPayload { ): TaskProcessReportPayload {
const name = target.name?.trim() || fileName(target.path); const name = target.name?.trim() || fileName(target.path);
const agentId = target.agentId?.trim() || target.threadId; const agentId = target.agentId?.trim() || target.threadId;
// 产物提交 position:优先取业务链里给该席位智能体配置的值,未配置回退默认。
const position = config.positionFor?.(target) ?? config.position;
return { return {
taskId: config.taskId, taskId: config.taskId,
agentId, agentId,
agentName: target.agentName?.trim() || agentId, agentName: target.agentName?.trim() || agentId,
content, content,
position: config.position, position,
reportName: name, reportName: name,
submitStatus, submitStatus,
}; };
@@ -55,6 +55,13 @@ export interface ChainSeat {
* 使同一条业务链既能继续用于原会商,也能在岗位协同中按岗位筛选可问答的智能体。 * 使同一条业务链既能继续用于原会商,也能在岗位协同中按岗位筛选可问答的智能体。
*/ */
position_id?: string | null; position_id?: string | null;
/**
* 该席位提交过程产物时传给外部任务系统的 `position` 值(8BF 会商页用)。
* 不同席位智能体对应不同的产物位置,缺省 / null = 提交时回退 0。
* 随 `seats` JSON 落库(无需 DB 列,但需在后端 `ChainSeatSchema` 显式声明)。
* 注意与 `position_id`(岗位协同的岗位归属)无关。
*/
artifact_position?: number | null;
/** /**
* 该席位在业务链中配置的岗位职责。会商「智能体配置」面板只读展示; * 该席位在业务链中配置的岗位职责。会商「智能体配置」面板只读展示;
* 随 `seats` JSON 落库(无需 DB 列)。空 / 缺省 = 未填写。 * 随 `seats` JSON 落库(无需 DB 列)。空 / 缺省 = 未填写。
@@ -13,7 +13,14 @@ export interface TaskProcessReportPayload {
export interface TaskArtifactSubmissionConfig { export interface TaskArtifactSubmissionConfig {
/** External task-system id, not the local roundtable draft storage key. */ /** External task-system id, not the local roundtable draft storage key. */
taskId: string; taskId: string;
/** 提交产物时传给外部任务系统的默认 position(产物位置)。 */
position: number; position: number;
/**
* 按席位智能体解析提交产物的 position(业务链里逐席位配置的「产物提交 position」)。
* 入参是 hook 解析后的 target(`agentId` 已归一为席位 agent_id);返回
* null / undefined = 该席位未配置,回退 `position` 默认值。
*/
positionFor?: (target: TaskArtifactTarget) => number | null | undefined;
createProcessReport: (payload: TaskProcessReportPayload) => Promise<{ id: string }>; createProcessReport: (payload: TaskProcessReportPayload) => Promise<{ id: string }>;
updateProcessReport: (reportId: string, payload: TaskProcessReportPayload) => Promise<void>; updateProcessReport: (reportId: string, payload: TaskProcessReportPayload) => Promise<void>;
} }
@@ -697,6 +697,8 @@ export default function BusinessChainEditorPage() {
seat_mode: s.seat_mode ?? null, seat_mode: s.seat_mode ?? null,
// 岗位协同专用岗位归属:普通圆桌忽略,但保存时必须原样保留。 // 岗位协同专用岗位归属:普通圆桌忽略,但保存时必须原样保留。
position_id: s.position_id ?? null, position_id: s.position_id ?? null,
// 席位「产物提交 position」:提交过程产物时传给外部任务系统;null = 回退默认 0。
artifact_position: s.artifact_position ?? null,
// 业务链席位岗位职责:编辑器已不提供输入,保存时原样保留存量值(会商智能体配置面板只读展示)。 // 业务链席位岗位职责:编辑器已不提供输入,保存时原样保留存量值(会商智能体配置面板只读展示)。
job_responsibilities: s.job_responsibilities?.trim() || null, job_responsibilities: s.job_responsibilities?.trim() || null,
}; };
@@ -75,6 +75,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { desensitize } from "@/lib/desensitize"; import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree"; import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
@@ -324,6 +325,15 @@ export default function RoundtablePlanningPage({
entryRailTaskCardSlot, entryRailTaskCardSlot,
/** 独立业务页传给意图智能体的隐藏任务上下文。 */ /** 独立业务页传给意图智能体的隐藏任务上下文。 */
taskContext, taskContext,
/** 第一步绑定的内置意图智能体 id(如 8BF 的 eightbf-roundtable-intent)。
* 缺省沿用通用会商的 roundtable-intent,原多智能体会商入口行为不变。 */
intentAgentId,
/**
* 8BF 页头「人工干预」开关(受控):开启后第二步每走一步暂停等待用户输入,
* 关闭自动推进。提供 onStep2InterventionChange 时才渲染开关,通用会商页不受影响。
*/
step2InterventionEnabled,
onStep2InterventionChange,
/** 独立业务页的任务作用域,仅用于左栏记录筛选,不改变原深链逻辑。 */ /** 独立业务页的任务作用域,仅用于左栏记录筛选,不改变原深链逻辑。 */
entryRailTaskId, entryRailTaskId,
/** 独立业务页的根节点样式类,避免修改原会商样式。 */ /** 独立业务页的根节点样式类,避免修改原会商样式。 */
@@ -367,6 +377,11 @@ export default function RoundtablePlanningPage({
entryRailBusinessChainSlot?: ReactNode; entryRailBusinessChainSlot?: ReactNode;
entryRailTaskCardSlot?: ReactNode; entryRailTaskCardSlot?: ReactNode;
taskContext?: Record<string, unknown> | null; taskContext?: Record<string, unknown> | null;
/** 第一步绑定的内置意图智能体 id;缺省走通用 roundtable-intent。 */
intentAgentId?: string;
/** 第二步「人工干预」开关(8BF 页头,新建任务按钮左侧)。 */
step2InterventionEnabled?: boolean;
onStep2InterventionChange?: (enabled: boolean) => void;
entryRailTaskId?: string | null; entryRailTaskId?: string | null;
rootClassName?: string; rootClassName?: string;
renderStep1?: (props: Step1PanelProps & { renderStep1?: (props: Step1PanelProps & {
@@ -755,6 +770,7 @@ export default function RoundtablePlanningPage({
selectedMode: seatMode, selectedMode: seatMode,
seatSkillDirectives, seatSkillDirectives,
humanValidations, humanValidations,
manualIntervention: step2InterventionEnabled ?? false,
seatModes, seatModes,
gatherFirst, gatherFirst,
orchestrationMode, orchestrationMode,
@@ -771,6 +787,7 @@ export default function RoundtablePlanningPage({
selectedModel, selectedModel,
triggerToast, triggerToast,
taskContext, taskContext,
intentAgentId,
}); });
const taskArtifactSubmission = useTaskArtifactSubmission( const taskArtifactSubmission = useTaskArtifactSubmission(
@@ -2381,6 +2398,23 @@ export default function RoundtablePlanningPage({
</button> </button>
)} )}
{/* 8BF:第二步「人工干预」开关。开启后第二步每走一步暂停,等待用户输入 /
恢复后继续;关闭则自动推进(与原行为一致)。仅独立业务页传入受控回调时渲染。 */}
{onStep2InterventionChange && (
<label
id="step2-intervention-toggle"
title="开启后:第二步每完成一步暂停,可在下方输入修改意见或点击“恢复会商”继续;关闭后:自动完成整个研讨"
className="bg-white hover:bg-slate-50 border border-[#e2e8f0] text-[#334155] text-xs font-bold px-3 py-2 rounded-xl flex items-center gap-2 transition cursor-pointer select-none dark:border-border dark:bg-secondary dark:text-foreground dark:hover:bg-accent"
>
<Switch
checked={step2InterventionEnabled ?? false}
onCheckedChange={onStep2InterventionChange}
className="scale-90"
/>
人工干预
</label>
)}
{/* 深链接(taskId)模式:任务固定,把「新建任务」换成「重新分析」——清空会话、对同一任务重做意图分析。 */} {/* 深链接(taskId)模式:任务固定,把「新建任务」换成「重新分析」——清空会话、对同一任务重做意图分析。 */}
{prefillRef.current?.taskId ? ( {prefillRef.current?.taskId ? (
<button <button
@@ -41,10 +41,6 @@ const iconButtonClass = (isDark: boolean, isPaleLightTheme: boolean, isCustomThe
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]", : "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
); );
// 深色顶栏菜单左右分组(设计稿:左 问答中心/任务管理/应用中心,右 综合管理/资源管理)
const LEFT_MENU_IDS = ["qa-center", "task-management", "light-app-center"];
const RIGHT_MENU_IDS = ["comprehensive-management", "resource-management"];
type HeaderProps = { type HeaderProps = {
collapsed?: boolean; collapsed?: boolean;
}; };
@@ -122,8 +118,11 @@ const Header = ({ collapsed }: HeaderProps) => {
const thirdSubNavItems = getActiveThirdLevelItems(visibleMenuItems, pathname); const thirdSubNavItems = getActiveThirdLevelItems(visibleMenuItems, pathname);
const leftMenuItems = visibleMenuItems.filter((i) => LEFT_MENU_IDS.includes(i.id)); // 深色顶栏围绕中间标题分成左右两组。直接按菜单管理后的一级顺序均分,确保被升为
const rightMenuItems = visibleMenuItems.filter((i) => RIGHT_MENU_IDS.includes(i.id)); // 一级或重新排序的节点仍会显示;默认 5 项时仍保持左 3 / 右 2 的原布局。
const topMenuSplitIndex = Math.ceil(visibleMenuItems.length / 2);
const leftMenuItems = visibleMenuItems.slice(0, topMenuSplitIndex);
const rightMenuItems = visibleMenuItems.slice(topMenuSplitIndex);
const handleNavItemClick = (item: MenuItem) => { const handleNavItemClick = (item: MenuItem) => {
const path = item.path ?? findFirstRoutePath(item.children); const path = item.path ?? findFirstRoutePath(item.children);
@@ -73,6 +73,21 @@ export interface LlmWikiKnowledgeBase {
is_conversation_deposit?: boolean; is_conversation_deposit?: boolean;
} }
export interface LlmWikiEmbeddingModel {
id: string;
name: string;
provider?: string | null;
model_name?: string | null;
status?: string | null;
is_default: boolean;
available: boolean;
}
export interface LlmWikiEmbeddingModelsResponse {
models: LlmWikiEmbeddingModel[];
default_model_id?: string | null;
}
export interface PublicLlmWikiKnowledgeBase extends LlmWikiKnowledgeBase { export interface PublicLlmWikiKnowledgeBase extends LlmWikiKnowledgeBase {
wiki: { wiki: {
status: "ready" | "empty" | "unavailable"; status: "ready" | "empty" | "unavailable";
@@ -268,6 +283,9 @@ export interface LlmWikiWikiIndexStatus {
export interface LocalWikiEncoderStatus { export interface LocalWikiEncoderStatus {
provider: string; provider: string;
configured_provider?: string | null;
mode?: "configured" | "local";
using_local?: boolean;
model: string; model: string;
dimensions?: number | null; dimensions?: number | null;
state: "idle" | "starting" | "ready" | "failed"; state: "idle" | "starting" | "ready" | "failed";
@@ -424,6 +442,17 @@ export async function getLlmWikiKnowledgeBase(
); );
} }
export async function listLlmWikiEmbeddingModels(): Promise<LlmWikiEmbeddingModelsResponse> {
const payload = await readJson<LlmWikiEmbeddingModelsResponse>(
await apiFetch(`${API()}/embedding-models`),
"读取知识服务向量模型",
);
return {
models: payload.models ?? [],
default_model_id: payload.default_model_id ?? null,
};
}
export async function createLlmWikiWeKnoraEmbedSession( export async function createLlmWikiWeKnoraEmbedSession(
id: string, id: string,
tab: "documents" | "wiki" | "graph", tab: "documents" | "wiki" | "graph",
@@ -457,6 +486,7 @@ export async function createLlmWikiKnowledgeBase(input: {
description: string; description: string;
type: "document" | "faq"; type: "document" | "faq";
wiki_enabled?: boolean; wiki_enabled?: boolean;
embedding_model_id?: string;
}): Promise<LlmWikiKnowledgeBase> { }): Promise<LlmWikiKnowledgeBase> {
return readJson( return readJson(
await apiFetch(`${API()}/knowledge-bases`, { await apiFetch(`${API()}/knowledge-bases`, {
@@ -654,6 +684,13 @@ export async function configureLocalWikiEncoder(
); );
} }
export async function restoreConfiguredWikiEncoder(): Promise<LocalWikiEncoderStatus> {
return readJson(
await apiFetch(`${API()}/local-encoder/use-configured`, { method: "POST" }),
"恢复配置文件编码器",
);
}
export async function uploadLocalWikiEncoder( export async function uploadLocalWikiEncoder(
files: File[], files: File[],
onProgress?: (percent: number) => void, onProgress?: (percent: number) => void,
@@ -688,10 +725,15 @@ export async function uploadLocalWikiEncoder(
payload = null; payload = null;
} }
if (request.status < 200 || request.status >= 300) { if (request.status < 200 || request.status >= 300) {
const detail = payload && typeof payload === "object" && "detail" in payload const rawDetail = payload && typeof payload === "object" && "detail" in payload
? (payload as { detail: unknown }).detail ? (payload as { detail: unknown }).detail
: `请求失败 (${request.status})`; : `请求失败 (${request.status})`;
reject(new Error(typeof detail === "string" ? detail : JSON.stringify(detail))); const detail = rawDetail && typeof rawDetail === "object" && "message" in rawDetail
? String((rawDetail as { message: unknown }).message)
: typeof rawDetail === "string"
? rawDetail
: JSON.stringify(rawDetail);
reject(new Error(userFacingKnowledgeMessage(detail)));
return; return;
} }
onProgress?.(100); onProgress?.(100);
@@ -1268,6 +1310,17 @@ export function useLlmWikiSettings(enabled = true) {
}); });
} }
export function useLlmWikiEmbeddingModels(enabled = true) {
const { userId } = useLiveAccount();
return useQuery({
queryKey: ["llmwiki", "embedding-models", userId ?? "anonymous"],
queryFn: listLlmWikiEmbeddingModels,
enabled,
staleTime: 60_000,
refetchOnMount: "always",
});
}
export function useLlmWikiActions() { export function useLlmWikiActions() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidateKnowledge = () => const invalidateKnowledge = () =>
@@ -34,6 +34,13 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
@@ -48,7 +55,9 @@ import {
searchLlmWiki, searchLlmWiki,
startLocalWikiEncoder, startLocalWikiEncoder,
uploadLocalWikiEncoder, uploadLocalWikiEncoder,
restoreConfiguredWikiEncoder,
useLlmWikiActions, useLlmWikiActions,
useLlmWikiEmbeddingModels,
useLlmWikiKnowledgeBases, useLlmWikiKnowledgeBases,
useLlmWikiSettings, useLlmWikiSettings,
useLlmWikiSettingsActions, useLlmWikiSettingsActions,
@@ -68,6 +77,14 @@ function messageOf(error: unknown): string {
return error instanceof Error ? error.message : "操作失败,请稍后重试"; return error instanceof Error ? error.message : "操作失败,请稍后重试";
} }
function encoderProviderLabel(provider?: string | null): string {
const normalized = provider?.trim().toLowerCase();
if (normalized === "local") return "本地 BGE-M3";
if (normalized === "openai") return "OpenAI";
if (normalized === "openai_compatible") return "OpenAI 兼容服务";
return provider?.trim() || "未配置";
}
function searchModeLabel(mode: LlmWikiSearchResponse["mode"]) { function searchModeLabel(mode: LlmWikiSearchResponse["mode"]) {
if (mode === "vector") return "本地 Wiki 向量检索"; if (mode === "vector") return "本地 Wiki 向量检索";
if (mode === "mixed") return "Wiki 混合检索"; if (mode === "mixed") return "Wiki 混合检索";
@@ -457,18 +474,50 @@ function CreateKnowledgeDialog({
open: boolean; open: boolean;
pending: boolean; pending: boolean;
onOpenChange: (value: boolean) => void; onOpenChange: (value: boolean) => void;
onCreate: (input: { name: string; description: string; type: "document" | "faq"; wiki_enabled: boolean }) => Promise<void>; onCreate: (input: { name: string; description: string; type: "document" | "faq"; wiki_enabled: boolean; embedding_model_id: string }) => Promise<void>;
}) { }) {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [wikiEnabled, setWikiEnabled] = useState(true); const [wikiEnabled, setWikiEnabled] = useState(true);
const [embeddingModelId, setEmbeddingModelId] = useState("");
const embeddingModels = useLlmWikiEmbeddingModels(open);
const availableEmbeddingModels = useMemo(
() => (embeddingModels.data?.models ?? []).filter((model) => model.available !== false),
[embeddingModels.data?.models],
);
useEffect(() => { useEffect(() => {
if (!open) { setName(""); setDescription(""); setWikiEnabled(true); } if (!open) {
setName("");
setDescription("");
setWikiEnabled(true);
setEmbeddingModelId("");
}
}, [open]); }, [open]);
useEffect(() => {
if (!open || !embeddingModels.data || availableEmbeddingModels.length === 0) return;
if (availableEmbeddingModels.some((model) => model.id === embeddingModelId)) return;
const defaultId = embeddingModels.data.default_model_id;
setEmbeddingModelId(
availableEmbeddingModels.some((model) => model.id === defaultId)
? defaultId!
: availableEmbeddingModels[0]!.id,
);
}, [availableEmbeddingModels, embeddingModelId, embeddingModels.data, open]);
const modelLabel = (model: (typeof availableEmbeddingModels)[number]) => {
const provider = model.provider?.trim();
const implementation = model.model_name?.trim();
const suffix = [provider, implementation].filter(Boolean).join(" · ");
return suffix ? `${model.name}(${suffix})` : model.name;
};
const modelSelectionUnavailable = embeddingModels.isLoading || embeddingModels.isError || !embeddingModelId;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader><DialogTitle>新建个人知识库</DialogTitle><DialogDescription>选择知识库模式;模型、切块和存储策略仍由知识服务管理。</DialogDescription></DialogHeader> <DialogHeader><DialogTitle>新建个人知识库</DialogTitle><DialogDescription>选择知识库模式和知识服务用于原文分块的向量模型。</DialogDescription></DialogHeader>
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-name">名称</label><Input id="llmwiki-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={255} /></div> <div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-name">名称</label><Input id="llmwiki-name" value={name} onChange={(event) => setName(event.target.value)} maxLength={255} /></div>
<div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-description">描述</label><Textarea id="llmwiki-description" value={description} onChange={(event) => setDescription(event.target.value)} rows={4} maxLength={2048} /></div> <div className="space-y-2"><label className="text-sm font-medium" htmlFor="llmwiki-description">描述</label><Textarea id="llmwiki-description" value={description} onChange={(event) => setDescription(event.target.value)} rows={4} maxLength={2048} /></div>
@@ -476,8 +525,33 @@ function CreateKnowledgeDialog({
<button type="button" className={`rounded-xl border p-4 text-left transition-colors ${wikiEnabled ? "border-primary bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/40"}`} onClick={() => setWikiEnabled(true)}><div className="flex items-center gap-2 text-sm font-semibold"><BookOpen className="size-4 text-violet-500" />Wiki 知识库</div><p className="mt-2 text-xs leading-5 text-muted-foreground">自动综合资料,生成 Wiki 页面和页面关系图谱。</p></button> <button type="button" className={`rounded-xl border p-4 text-left transition-colors ${wikiEnabled ? "border-primary bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/40"}`} onClick={() => setWikiEnabled(true)}><div className="flex items-center gap-2 text-sm font-semibold"><BookOpen className="size-4 text-violet-500" />Wiki 知识库</div><p className="mt-2 text-xs leading-5 text-muted-foreground">自动综合资料,生成 Wiki 页面和页面关系图谱。</p></button>
<button type="button" className={`rounded-xl border p-4 text-left transition-colors ${!wikiEnabled ? "border-primary bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/40"}`} onClick={() => setWikiEnabled(false)}><div className="flex items-center gap-2 text-sm font-semibold"><FileText className="size-4 text-blue-500" />标准知识库</div><p className="mt-2 text-xs leading-5 text-muted-foreground">仅进行文档解析、切块与向量/关键词索引。</p></button> <button type="button" className={`rounded-xl border p-4 text-left transition-colors ${!wikiEnabled ? "border-primary bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/40"}`} onClick={() => setWikiEnabled(false)}><div className="flex items-center gap-2 text-sm font-semibold"><FileText className="size-4 text-blue-500" />标准知识库</div><p className="mt-2 text-xs leading-5 text-muted-foreground">仅进行文档解析、切块与向量/关键词索引。</p></button>
</div></div> </div></div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="llmwiki-embedding-model">原文向量模型</label>
{embeddingModels.isLoading ? (
<div className="flex h-9 items-center rounded-md border px-3 text-sm text-muted-foreground"><Loader2 className="mr-2 size-4 animate-spin" />正在读取知识服务模型</div>
) : embeddingModels.isError ? (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
<p>模型读取失败:{messageOf(embeddingModels.error)}</p>
<Button type="button" variant="ghost" size="sm" className="mt-1 h-7 px-2" onClick={() => void embeddingModels.refetch()}>重新读取</Button>
</div>
) : availableEmbeddingModels.length ? (
<Select value={embeddingModelId} onValueChange={setEmbeddingModelId} disabled={pending}>
<SelectTrigger id="llmwiki-embedding-model" className="w-full"><SelectValue placeholder="请选择向量模型" /></SelectTrigger>
<SelectContent position="popper">
{availableEmbeddingModels.map((model) => (
<SelectItem key={model.id} value={model.id}>
{modelLabel(model)}{model.is_default ? " · 默认" : ""}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-muted-foreground">知识服务中没有可用的 Embedding 模型,请先在知识服务中启用本地或 OpenAI 兼容向量模型。</div>
)}
<p className="text-xs leading-5 text-muted-foreground">该模型由知识服务(WeKnora)对上传原文和分块进行向量化,并在创建时绑定到新库。它与 DeerFlow 的“Wiki 镜像编码器”是两套独立配置;选择 OpenAI 兼容模型时,无需再配置本地 BGE-M3。需要跨知识库检索时,请为相关知识库保持相同或兼容的向量模型。</p>
</div>
</div> </div>
<DialogFooter><Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button><Button disabled={pending || !name.trim()} onClick={() => void onCreate({ name: name.trim(), description: description.trim(), type: "document", wiki_enabled: wikiEnabled })}>{pending && <Loader2 className="mr-2 size-4 animate-spin" />}创建</Button></DialogFooter> <DialogFooter><Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button><Button disabled={pending || !name.trim() || modelSelectionUnavailable} onClick={() => void onCreate({ name: name.trim(), description: description.trim(), type: "document", wiki_enabled: wikiEnabled, embedding_model_id: embeddingModelId })}>{pending && <Loader2 className="mr-2 size-4 animate-spin" />}创建</Button></DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@@ -501,11 +575,12 @@ export function LocalEncoderSettingsCard() {
const applyStatus = (status: Awaited<ReturnType<typeof getLocalWikiEncoderStatus>>) => { const applyStatus = (status: Awaited<ReturnType<typeof getLocalWikiEncoderStatus>>) => {
queryClient.setQueryData(["llmwiki-local-encoder"], status); queryClient.setQueryData(["llmwiki-local-encoder"], status);
void queryClient.invalidateQueries({ queryKey: ["llmwiki", "wiki-index-status"] });
setModelPath(status.model_path || ""); setModelPath(status.model_path || "");
}; };
const configure = useMutation({ const configure = useMutation({
mutationFn: () => configureLocalWikiEncoder(modelPath.trim()), mutationFn: () => configureLocalWikiEncoder(modelPath.trim()),
onSuccess: (status) => { applyStatus(status); toast.success("编码器目录已保存"); }, onSuccess: (status) => { applyStatus(status); toast.success("已切换到本地 Wiki 编码器并保存目录"); },
onError: (error) => toast.error(messageOf(error)), onError: (error) => toast.error(messageOf(error)),
}); });
const upload = useMutation({ const upload = useMutation({
@@ -514,7 +589,7 @@ export function LocalEncoderSettingsCard() {
onSuccess: (status) => { onSuccess: (status) => {
applyStatus(status); applyStatus(status);
setFiles([]); setFiles([]);
toast.success(`已保存 ${status.uploaded_files ?? 0} 个模型文件`); toast.success(`已保存 ${status.uploaded_files ?? 0} 个模型文件,并切换到本地 Wiki 编码器`);
}, },
onError: (error) => toast.error(messageOf(error)), onError: (error) => toast.error(messageOf(error)),
}); });
@@ -526,7 +601,17 @@ export function LocalEncoderSettingsCard() {
}, },
onError: (error) => toast.error(messageOf(error)), onError: (error) => toast.error(messageOf(error)),
}); });
const restoreConfigured = useMutation({
mutationFn: restoreConfiguredWikiEncoder,
onSuccess: (nextStatus) => {
applyStatus(nextStatus);
toast.success(`已恢复 config.yaml 编码器:${encoderProviderLabel(nextStatus.provider)}`);
},
onError: (error) => toast.error(messageOf(error)),
});
const status = encoder.data; const status = encoder.data;
const usingLocal = status?.using_local ?? status?.provider === "local";
const localOverrideActive = status?.mode === "local";
const selectedBytes = files.reduce((sum, file) => sum + file.size, 0); const selectedBytes = files.reduce((sum, file) => sum + file.size, 0);
const formatSize = (value: number) => value >= 1024 ** 3 const formatSize = (value: number) => value >= 1024 ** 3
? `${(value / 1024 ** 3).toFixed(2)} GB` ? `${(value / 1024 ** 3).toFixed(2)} GB`
@@ -535,29 +620,49 @@ export function LocalEncoderSettingsCard() {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-base"><Cpu className="size-5" />DeerFlow 本地编码器</CardTitle> <CardTitle className="flex items-center gap-2 text-base"><Cpu className="size-5" />DeerFlow Wiki 镜像编码器</CardTitle>
<CardDescription>仅管理员可见。配置服务器上已有的 BGE-M3 目录,或直接上传完整离线模型目录;编码器作为独立进程启动,不影响 DeerFlow 其他服务。</CardDescription> <CardDescription>仅管理员可见。这里负责 DeerFlow 内部 Wiki 页面镜像的向量检索,不会决定知识服务对上传原文使用哪个模型。原文向量模型在“新建知识库”时选择。</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-5"> <CardContent className="space-y-5">
<div className="flex flex-wrap items-center gap-2 text-sm"> <div className="flex flex-wrap items-center gap-2 text-sm">
<Badge variant={status?.state === "failed" ? "destructive" : status?.state === "ready" ? "default" : "secondary"}> <Badge variant={encoder.isError || (usingLocal && status?.state === "failed") ? "destructive" : usingLocal && status?.state === "ready" ? "default" : "secondary"}>
{status?.state === "ready" ? "已就绪" : status?.state === "starting" ? "启动中" : status?.state === "failed" ? "启动失败" : "未启动"} {encoder.isError
? "状态读取失败"
: encoder.isLoading
? "读取中"
: !usingLocal
? "配置编码器生效中"
: status?.state === "ready"
? "本地编码器已就绪"
: status?.state === "starting"
? "本地编码器启动中"
: status?.state === "failed"
? "本地编码器启动失败"
: "本地编码器未启动"}
</Badge> </Badge>
<span className="text-muted-foreground">{status ? `${status.model} · ${status.dimensions || "自动探测"} 维 · ${status.process_isolation ? "独立进程" : "主进程"}` : "正在读取状态"}</span> {status && <Badge variant="outline">当前 provider:{encoderProviderLabel(status.provider)}</Badge>}
{status?.model_files_ready && <Badge variant="outline">模型文件已就绪</Badge>} {status && <Badge variant="outline">{localOverrideActive ? "页面本地覆盖" : "config.yaml 配置"}</Badge>}
<span className="text-muted-foreground">
{status
? usingLocal
? `${status.model} · ${status.dimensions || "自动探测"} 维 · ${status.process_isolation ? "独立进程" : "主进程"}`
: `由 config.yaml 的 ${encoderProviderLabel(status.configured_provider || status.provider)} provider 提供向量服务,无需启动本地模型`
: "正在读取状态"}
</span>
{usingLocal && status?.model_files_ready && <Badge variant="outline">模型文件已就绪</Badge>}
</div> </div>
{encoder.error && <p className="text-sm text-destructive">{messageOf(encoder.error)}</p>} {encoder.error && <p className="text-sm text-destructive">{messageOf(encoder.error)}</p>}
{status?.last_error && <p className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">{status.last_error}</p>} {usingLocal && status?.last_error && <p className="rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">{status.last_error}</p>}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium" htmlFor="local-encoder-path">使用服务器已有目录</label> <label className="text-sm font-medium" htmlFor="local-encoder-path">切换到服务器本地模型目录</label>
<div className="flex flex-col gap-2 sm:flex-row"> <div className="flex flex-col gap-2 sm:flex-row">
<Input id="local-encoder-path" value={modelPath} onChange={(event) => setModelPath(event.target.value)} placeholder="例如 /opt/deerflow/models/bge-m3" /> <Input id="local-encoder-path" value={modelPath} onChange={(event) => setModelPath(event.target.value)} placeholder="例如 /opt/deerflow/models/bge-m3" />
<Button variant="outline" disabled={!modelPath.trim() || configure.isPending} onClick={() => configure.mutate()}> <Button variant="outline" disabled={!modelPath.trim() || configure.isPending} onClick={() => configure.mutate()}>
{configure.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}保存目录 {configure.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}保存目录
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground">目录内需包含 <code>onnx/model.onnx</code>(推荐)或根目录 <code>model.onnx</code>。</p> <p className="text-xs text-muted-foreground">目录内需包含 <code>onnx/model.onnx</code>(推荐)或根目录 <code>model.onnx</code>。保存后会立即把 Wiki 镜像编码器切换为 local 并热重建,无需修改配置文件或重启服务。</p>
</div> </div>
<div className="space-y-2 rounded-xl border border-dashed p-4"> <div className="space-y-2 rounded-xl border border-dashed p-4">
@@ -576,7 +681,7 @@ export function LocalEncoderSettingsCard() {
)} )}
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{files.length ? `已选择 ${files.length} 个文件,共 ${formatSize(selectedBytes)}` : "请选择完整 BGE-M3 目录。大文件会落到运行目录,不写入源码仓库。"} {files.length ? `已选择 ${files.length} 个文件,共 ${formatSize(selectedBytes)}` : "请选择完整 BGE-M3 目录。上传完成后会自动切换到 local;大文件会落到运行目录,不写入源码仓库。"}
</p> </p>
<Button disabled={!files.length || upload.isPending} onClick={() => upload.mutate()}> <Button disabled={!files.length || upload.isPending} onClick={() => upload.mutate()}>
{upload.isPending ? <Loader2 className="mr-2 size-4 animate-spin" /> : <Upload className="mr-2 size-4" />} {upload.isPending ? <Loader2 className="mr-2 size-4 animate-spin" /> : <Upload className="mr-2 size-4" />}
@@ -586,10 +691,16 @@ export function LocalEncoderSettingsCard() {
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button disabled={!status?.model_files_ready || start.isPending || status?.state === "starting" || status?.state === "ready"} onClick={() => start.mutate()}> <Button disabled={!usingLocal || !status?.model_files_ready || start.isPending || status?.state === "starting" || status?.state === "ready"} onClick={() => start.mutate()}>
{(start.isPending || status?.state === "starting") && <Loader2 className="mr-2 size-4 animate-spin" />} {(start.isPending || status?.state === "starting") && <Loader2 className="mr-2 size-4 animate-spin" />}
{status?.state === "ready" ? "编码器已启动" : status?.state === "failed" ? "重新启动" : "一键启动编码器"} {!usingLocal ? "当前无需启动本地模型" : status?.state === "ready" ? "编码器已启动" : status?.state === "failed" ? "重新启动" : "一键启动编码器"}
</Button> </Button>
{localOverrideActive && (
<Button variant="secondary" disabled={restoreConfigured.isPending} onClick={() => restoreConfigured.mutate()}>
{restoreConfigured.isPending && <Loader2 className="mr-2 size-4 animate-spin" />}
恢复 config.yaml 编码器
</Button>
)}
<Button variant="ghost" disabled={encoder.isFetching} onClick={() => void encoder.refetch()}> <Button variant="ghost" disabled={encoder.isFetching} onClick={() => void encoder.refetch()}>
<RefreshCw className={`mr-2 size-4 ${encoder.isFetching ? "animate-spin" : ""}`} />刷新状态 <RefreshCw className={`mr-2 size-4 ${encoder.isFetching ? "animate-spin" : ""}`} />刷新状态
</Button> </Button>
@@ -601,13 +601,19 @@ export function WeKnoraWikiManagementPage() {
} }
}; };
const vectorStatusText = !indexStatus.data?.local_index_enabled const vectorStatusText = indexStatus.isLoading
? "本地向量未启用,检索使用 Wiki 接口" ? "正在读取 Wiki 镜像向量状态"
: indexStatus.data.fully_vectorized : indexStatus.isError
? `整库已向量化(${indexStatus.data.vector_count ?? 0} 个向量章节)` ? `Wiki 镜像向量状态读取失败:${indexStatus.error instanceof Error ? indexStatus.error.message : "请稍后重试"}`
: indexStatus.data.state === "running" || indexStatus.data.state === "rebuilding" : !indexStatus.data?.local_index_enabled
? "整库向量化进行中,完成前检索使用 Wiki 接口" ? "Wiki 镜像向量未启用,检索使用 Wiki 接口"
: "整库尚未完成向量化,检索使用 Wiki 接口"; : indexStatus.data.fully_vectorized
? `整库已向量化(${indexStatus.data.vector_count ?? 0} 个向量章节)`
: indexStatus.data.state === "running" || indexStatus.data.state === "rebuilding"
? "整库向量化进行中,完成前检索使用 Wiki 接口"
: indexStatus.data.last_error
? `整库向量化失败:${indexStatus.data.last_error}`
: "整库尚未完成向量化,检索使用 Wiki 接口";
return ( return (
<div className="flex h-full min-h-0 flex-col bg-background"> <div className="flex h-full min-h-0 flex-col bg-background">
@@ -626,7 +632,7 @@ export function WeKnoraWikiManagementPage() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => void vectorize()} onClick={() => void vectorize()}
disabled={!canWrite || actions.vectorizeWiki.isPending || indexStatus.data?.state === "running" || indexStatus.data?.state === "rebuilding"} disabled={!canWrite || indexStatus.isLoading || actions.vectorizeWiki.isPending || indexStatus.data?.state === "running" || indexStatus.data?.state === "rebuilding"}
> >
{actions.vectorizeWiki.isPending || indexStatus.data?.state === "running" || indexStatus.data?.state === "rebuilding" ? ( {actions.vectorizeWiki.isPending || indexStatus.data?.state === "running" || indexStatus.data?.state === "rebuilding" ? (
<Loader2 className="mr-2 size-4 animate-spin" /> <Loader2 className="mr-2 size-4 animate-spin" />
@@ -652,6 +658,17 @@ export function WeKnoraWikiManagementPage() {
<main className="grid min-h-0 flex-1 grid-cols-[320px_1fr] overflow-hidden"> <main className="grid min-h-0 flex-1 grid-cols-[320px_1fr] overflow-hidden">
<aside className="flex min-h-0 flex-col border-r bg-muted/20"> <aside className="flex min-h-0 flex-col border-r bg-muted/20">
<div className="border-b p-3"> <div className="border-b p-3">
{indexStatus.isLoading && (
<div className="mb-3 flex items-center rounded-lg border bg-background p-3 text-xs text-muted-foreground">
<Loader2 className="mr-2 size-4 animate-spin" />正在读取 Wiki 镜像向量状态
</div>
)}
{indexStatus.isError && (
<div className="mb-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-xs text-destructive">
<p>Wiki 镜像向量状态读取失败:{indexStatus.error instanceof Error ? indexStatus.error.message : "请稍后重试"}</p>
<Button type="button" variant="ghost" size="sm" className="mt-1 h-7 px-2" onClick={() => void indexStatus.refetch()}>重新读取</Button>
</div>
)}
{indexStatus.data && ( {indexStatus.data && (
<div className="mb-3 rounded-lg border bg-background p-3"> <div className="mb-3 rounded-lg border bg-background p-3">
<div className="flex items-center justify-between gap-2 text-xs"> <div className="flex items-center justify-between gap-2 text-xs">
@@ -0,0 +1,736 @@
import { useEffect, useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import {
Bot,
Check,
CirclePlus,
Layers3,
LoaderCircle,
LockKeyhole,
Power,
RotateCcw,
Save,
Sparkles,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector";
import { ModelNameSelect } from "@/components/workspace/model-name-select";
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useAllSkillsAdmin } from "@/core/skills/hooks";
import { cn } from "@/lib/utils";
import {
createStrategyAngle,
disableStrategyAngle,
STRATEGY_AGENTS_QUERY_KEY,
updateStrategyAgent,
useStrategyAgents,
type StrategyAgent,
type StrategyAgentCategory,
} from "./strategy-agents";
interface AgentConfigDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface AgentConfigDraft {
name: string;
description: string;
soul: string;
model: string | null;
skills: string[];
enabled: boolean;
sortOrder: number;
}
const NEW_ANGLE_ID = "__new_strategy_angle__";
const CATEGORY_COPY: Record<
StrategyAgentCategory,
{ title: string; description: string }
> = {
intent: {
title: "意图理解",
description: "把用户对话整理为结构化任务,固定流程入口",
},
angle: {
title: "分析角度",
description: "并行生成独立策略,可按业务需要扩展",
},
judge: {
title: "智能评判",
description: "统一比较各角度策略并给出推荐结论",
},
solution: {
title: "方案整理",
description: "将用户选中的策略消冲突并汇总成方案",
},
};
function normalizeSkills(skills: string[]) {
return [...new Set(skills)].sort((left, right) =>
left.localeCompare(right),
);
}
function sameSkills(left: string[], right: string[]) {
const normalizedLeft = normalizeSkills(left);
const normalizedRight = normalizeSkills(right);
return (
normalizedLeft.length === normalizedRight.length &&
normalizedLeft.every((skill, index) => skill === normalizedRight[index])
);
}
function draftFromAgent(agent: StrategyAgent): AgentConfigDraft {
return {
name: agent.name,
description: agent.description,
soul: agent.soul ?? "",
model: agent.model,
skills: agent.skills ?? [],
enabled: agent.enabled,
sortOrder: agent.sort_order,
};
}
function emptyAngleDraft(sortOrder: number): AgentConfigDraft {
return {
name: "",
description: "",
soul: "",
model: null,
skills: [],
enabled: true,
sortOrder,
};
}
function sameDraft(left: AgentConfigDraft, right: AgentConfigDraft) {
return (
left.name === right.name &&
left.description === right.description &&
left.soul === right.soul &&
left.model === right.model &&
left.enabled === right.enabled &&
left.sortOrder === right.sortOrder &&
sameSkills(left.skills, right.skills)
);
}
export function AgentConfigDialog({
open,
onOpenChange,
}: AgentConfigDialogProps) {
const [selectedId, setSelectedId] = useState("");
const [draft, setDraft] = useState<AgentConfigDraft>(() =>
emptyAngleDraft(100),
);
const [savedDraft, setSavedDraft] = useState<AgentConfigDraft | null>(null);
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const queryClient = useQueryClient();
const {
data: agents = [],
isLoading,
error,
refetch,
} = useStrategyAgents({
enabled: open,
includeDisabled: true,
includeConfig: true,
});
const {
skills,
isLoading: isSkillsLoading,
error: skillsError,
} = useAllSkillsAdmin(open);
const fixedAgents = useMemo(
() => agents.filter((agent) => agent.fixed),
[agents],
);
const angleAgents = useMemo(
() => agents.filter((agent) => agent.category === "angle"),
[agents],
);
const selectedAgent = useMemo(
() => agents.find((agent) => agent.id === selectedId) ?? null,
[agents, selectedId],
);
const creating = selectedId === NEW_ANGLE_ID;
const dirty = Boolean(savedDraft && !sameDraft(draft, savedDraft));
const selectableSkills = useMemo(
() =>
skills.filter((skill) => {
const globallyAvailable =
Boolean(skill.builtin) || Boolean(skill.published);
return (
skill.enabled &&
(skill.state ?? "active") === "active" &&
globallyAvailable
);
}),
[skills],
);
useEffect(() => {
if (!open || isLoading || creating || agents.length === 0) return;
if (selectedAgent) return;
setSelectedId(agents[0].id);
}, [agents, creating, isLoading, open, selectedAgent]);
useEffect(() => {
if (!open || creating || !selectedAgent) return;
const next = draftFromAgent(selectedAgent);
setDraft(next);
setSavedDraft(next);
}, [creating, open, selectedAgent?.id, selectedAgent?.updated_at]);
const confirmDiscardChanges = () =>
!dirty || window.confirm("当前配置尚未保存,确定放弃这些修改吗?");
const selectAgent = (agentId: string) => {
if (agentId === selectedId || busy || !confirmDiscardChanges()) return;
setSelectedId(agentId);
setSavedDraft(null);
};
const startCreate = () => {
if (busy || !confirmDiscardChanges()) return;
const maxOrder = angleAgents.reduce(
(current, agent) => Math.max(current, agent.sort_order),
99,
);
const next = emptyAngleDraft(maxOrder + 1);
setSelectedId(NEW_ANGLE_ID);
setDraft(next);
setSavedDraft(next);
};
const save = async () => {
const name = draft.name.trim();
if (!name) {
toast.error("请填写智能体名称");
return;
}
setBusy(true);
try {
if (creating) {
const created = await createStrategyAngle({
name,
description: draft.description.trim(),
soul: draft.soul,
model: draft.model,
skills: draft.skills,
sort_order: draft.sortOrder,
});
setSelectedId(created.id);
const next = draftFromAgent(created);
setDraft(next);
setSavedDraft(next);
await queryClient.invalidateQueries({
queryKey: STRATEGY_AGENTS_QUERY_KEY,
});
toast.success(`${created.name} 已加入分析角度库`);
} else if (selectedAgent) {
const updated = await updateStrategyAgent(selectedAgent.id, {
name,
description: draft.description.trim(),
soul: draft.soul,
model: draft.model,
skills: draft.skills,
enabled: selectedAgent.fixed ? undefined : draft.enabled,
sort_order: selectedAgent.fixed ? undefined : draft.sortOrder,
});
const next = draftFromAgent(updated);
setDraft(next);
setSavedDraft(next);
await queryClient.invalidateQueries({
queryKey: STRATEGY_AGENTS_QUERY_KEY,
});
toast.success(`${updated.name} 配置已保存`);
}
} catch (reason) {
toast.error(reason instanceof Error ? reason.message : "保存失败");
} finally {
setBusy(false);
}
};
const softDelete = async () => {
if (!selectedAgent || selectedAgent.fixed || busy) return;
const confirmed = window.confirm(
`确定停用“${selectedAgent.name}”吗?\n\n它不会再出现在新项目的角度选择中,但历史项目、SOUL 和技能配置都会保留,可随时恢复。`,
);
if (!confirmed) return;
setBusy(true);
try {
const updated = await disableStrategyAngle(selectedAgent.id);
const next = draftFromAgent(updated);
setDraft(next);
setSavedDraft(next);
await queryClient.invalidateQueries({
queryKey: STRATEGY_AGENTS_QUERY_KEY,
});
toast.success(`${updated.name} 已停用,历史项目不受影响`);
} catch (reason) {
toast.error(reason instanceof Error ? reason.message : "停用失败");
} finally {
setBusy(false);
}
};
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen && (busy || !confirmDiscardChanges())) return;
onOpenChange(nextOpen);
};
const category = creating ? "angle" : selectedAgent?.category;
const categoryCopy = category ? CATEGORY_COPY[category] : null;
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="grid h-[min(860px,calc(100vh-40px))] max-w-[1180px] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 sm:max-w-[1180px]">
<DialogHeader className="border-b px-6 py-5 dark:border-[#173d68]">
<DialogTitle className="flex items-center gap-2">
<Bot className="size-5 text-primary" />
统一策略智能体
</DialogTitle>
<DialogDescription>
配置对所有项目全局生效。流程角色保持唯一;分析角度可扩展、排序和停用,历史项目仍保留原引用。
</DialogDescription>
</DialogHeader>
<div className="grid min-h-0 grid-cols-[320px_minmax(0,1fr)]">
<aside className="min-h-0 overflow-y-auto border-r bg-muted/20 p-4 dark:border-[#173d68] dark:bg-[#031b38]">
<div className="mb-3 flex items-center justify-between px-1">
<div>
<p className="text-xs font-semibold tracking-wide text-muted-foreground">
固定流程角色
</p>
<p className="mt-1 text-[11px] text-muted-foreground/80">
仅可修改配置,不可新增、停用或删除
</p>
</div>
<Badge variant="secondary">{fixedAgents.length}</Badge>
</div>
<div className="space-y-1.5">
{fixedAgents.map((agent) => {
const selected = agent.id === selectedId;
const copy = CATEGORY_COPY[agent.category];
return (
<button
key={agent.id}
type="button"
disabled={busy}
onClick={() => selectAgent(agent.id)}
className={cn(
"w-full rounded-xl border px-3 py-3 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60",
selected
? "border-primary/45 bg-primary/10 shadow-sm"
: "border-transparent bg-background/70 hover:border-border dark:bg-[#08274c]/55 dark:hover:border-[#28507c]",
)}
>
<span className="flex items-center justify-between gap-2">
<span className="flex min-w-0 items-center gap-2">
<LockKeyhole className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate text-sm font-medium">
{copy.title}
</span>
</span>
{selected ? (
<Check className="size-4 shrink-0 text-primary" />
) : null}
</span>
<span className="mt-1.5 block truncate pl-5.5 text-xs text-muted-foreground">
{agent.name}
</span>
</button>
);
})}
</div>
<div className="mb-3 mt-6 flex items-center justify-between px-1">
<div>
<p className="text-xs font-semibold tracking-wide text-muted-foreground">
分析角度库
</p>
<p className="mt-1 text-[11px] text-muted-foreground/80">
新项目只使用当前启用的角度
</p>
</div>
<Button
type="button"
size="sm"
className="h-8 gap-1.5"
disabled={busy}
onClick={startCreate}
>
<CirclePlus className="size-3.5" />
新增
</Button>
</div>
<div className="space-y-1.5">
{angleAgents.map((agent) => {
const selected = agent.id === selectedId;
return (
<button
key={agent.id}
type="button"
disabled={busy}
onClick={() => selectAgent(agent.id)}
className={cn(
"w-full rounded-xl border px-3 py-3 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60",
selected
? "border-primary/45 bg-primary/10 shadow-sm"
: "border-transparent bg-background/70 hover:border-border dark:bg-[#08274c]/55 dark:hover:border-[#28507c]",
!agent.enabled && "opacity-65",
)}
>
<span className="flex items-center justify-between gap-2">
<span className="flex min-w-0 items-center gap-2">
<Layers3 className="size-3.5 shrink-0 text-primary" />
<span className="truncate text-sm font-medium">
{agent.name}
</span>
</span>
{selected ? (
<Check className="size-4 shrink-0 text-primary" />
) : null}
</span>
<span className="mt-2 flex items-center justify-between gap-2 pl-5.5">
<span className="truncate text-[11px] text-muted-foreground">
顺序 {agent.sort_order}
</span>
<Badge
variant={agent.enabled ? "secondary" : "outline"}
className="h-5 text-[10px]"
>
{agent.enabled ? "已启用" : "已停用"}
</Badge>
</span>
</button>
);
})}
{!isLoading && angleAgents.length === 0 ? (
<div className="rounded-xl border border-dashed p-5 text-center text-xs text-muted-foreground">
暂无分析角度,点击“新增”建立第一个角度。
</div>
) : null}
</div>
</aside>
<main className="min-h-0 overflow-y-auto px-6 py-5">
{isLoading ? (
<div className="flex h-64 items-center justify-center gap-2 text-sm text-muted-foreground">
<LoaderCircle className="size-4 animate-spin" />
正在读取全局配置…
</div>
) : error ? (
<div className="rounded-xl border border-destructive/30 bg-destructive/5 p-5 text-sm text-destructive">
<p className="font-medium">策略智能体配置加载失败</p>
<p className="mt-1 leading-6 opacity-85">
{error instanceof Error ? error.message : "请稍后重试"}
</p>
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => void refetch()}
>
<RotateCcw className="size-3.5" />
重新加载
</Button>
</div>
) : categoryCopy ? (
<div className="space-y-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-lg font-semibold">
{creating ? "新增分析角度" : categoryCopy.title}
</h3>
{selectedAgent?.fixed ? (
<Badge variant="outline" className="gap-1">
<LockKeyhole className="size-3" /> 固定角色
</Badge>
) : null}
{!creating && selectedAgent && !selectedAgent.fixed ? (
<Badge
variant={draft.enabled ? "secondary" : "outline"}
>
{draft.enabled ? "已启用" : "已停用"}
</Badge>
) : null}
</div>
<p className="mt-1 text-sm text-muted-foreground">
{creating
? "创建后会成为所有策略项目可选的全局分析角度。"
: categoryCopy.description}
</p>
</div>
{!creating && selectedAgent ? (
<Badge
variant="outline"
className="max-w-[300px] truncate font-mono text-[10px]"
title={selectedAgent.id}
>
{selectedAgent.id}
</Badge>
) : null}
</div>
<section className="rounded-xl border bg-muted/15 p-4 dark:border-[#173d68] dark:bg-[#031b38]">
<div className="grid gap-4 sm:grid-cols-2">
<label className="block">
<span className="mb-1.5 block text-xs font-medium">
智能体名称
</span>
<Input
value={draft.name}
disabled={busy}
maxLength={100}
placeholder="例如:科技与产业链视角"
onChange={(event) =>
setDraft((current) => ({
...current,
name: event.target.value,
}))
}
/>
</label>
<label className="block">
<span className="mb-1.5 block text-xs font-medium">
运行模型
</span>
<ModelNameSelect
value={draft.model}
onChange={(model) =>
setDraft((current) => ({ ...current, model }))
}
disabled={busy}
defaultLabel="使用系统默认模型"
title="选择该全局智能体运行时使用的大模型"
triggerClassName="h-9 w-full bg-background"
/>
</label>
<label className="block sm:col-span-2">
<span className="mb-1.5 block text-xs font-medium">
职责简介
</span>
<Textarea
value={draft.description}
disabled={busy}
maxLength={2000}
className="min-h-20 resize-y"
placeholder="说明该角色关注的问题、交付内容和边界…"
onChange={(event) =>
setDraft((current) => ({
...current,
description: event.target.value,
}))
}
/>
</label>
</div>
{!creating && selectedAgent && !selectedAgent.fixed ? (
<div className="mt-4 grid gap-3 border-t pt-4 sm:grid-cols-[1fr_180px] dark:border-[#173d68]">
<div className="flex items-center justify-between rounded-lg border bg-background px-3 py-2.5 dark:border-[#28507c] dark:bg-[#08274c]">
<div>
<p className="text-sm font-medium">用于新项目</p>
<p className="mt-0.5 text-xs text-muted-foreground">
停用后历史项目仍可回显,新的角度选择将隐藏它
</p>
</div>
<Switch
checked={draft.enabled}
disabled={busy}
aria-label="启用该分析角度"
onCheckedChange={(enabled) =>
setDraft((current) => ({ ...current, enabled }))
}
/>
</div>
<label className="block">
<span className="mb-1.5 block text-xs font-medium">
显示顺序
</span>
<Input
type="number"
min={-10000}
max={10000}
value={draft.sortOrder}
disabled={busy}
onChange={(event) =>
setDraft((current) => ({
...current,
sortOrder: Number(event.target.value) || 0,
}))
}
/>
</label>
</div>
) : creating ? (
<label className="mt-4 block max-w-[180px] border-t pt-4 dark:border-[#173d68]">
<span className="mb-1.5 block text-xs font-medium">
显示顺序
</span>
<Input
type="number"
min={-10000}
max={10000}
value={draft.sortOrder}
disabled={busy}
onChange={(event) =>
setDraft((current) => ({
...current,
sortOrder: Number(event.target.value) || 0,
}))
}
/>
</label>
) : null}
</section>
<label className="block">
<span className="mb-2 flex items-center gap-2 text-sm font-medium">
<Sparkles className="size-4 text-primary" /> SOUL
</span>
<Textarea
value={draft.soul}
disabled={busy}
maxLength={200000}
onChange={(event) =>
setDraft((current) => ({
...current,
soul: event.target.value,
}))
}
className="min-h-[210px] resize-y font-mono text-xs leading-6"
placeholder="定义角色身份、分析方法、行为边界和输出格式…"
/>
</label>
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<span className="text-sm font-medium">系统技能选择</span>
<Badge variant="secondary">
已选 {draft.skills.length} 个
</Badge>
</div>
<p className="text-xs leading-5 text-muted-foreground">
只展示当前启用且全局可见的系统技能;通过穿梭框选择,不接受手工输入技能名。
</p>
<SkillTransferSelector
skills={selectableSkills}
selected={draft.skills}
onChange={(selectedSkills) =>
setDraft((current) => ({
...current,
skills: selectedSkills,
}))
}
isLoading={isSkillsLoading}
onViewDetail={setDetailSkillName}
/>
{skillsError ? (
<p className="text-xs text-destructive">
技能库加载失败,请稍后重试。
</p>
) : null}
</section>
</div>
) : (
<div className="flex h-64 items-center justify-center text-sm text-muted-foreground">
暂无可配置角色
</div>
)}
</main>
</div>
<DialogFooter className="flex-row items-center justify-between border-t px-6 py-4 dark:border-[#173d68] sm:justify-between">
<div>
{!creating && selectedAgent && !selectedAgent.fixed ? (
selectedAgent.enabled ? (
<Button
variant="ghost"
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
disabled={busy}
onClick={() => void softDelete()}
>
<Trash2 className="size-4" />
停用并从新项目移除
</Button>
) : (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Power className="size-3.5" />
打开“用于新项目”并保存即可恢复
</span>
)
) : (
<span className="text-xs text-muted-foreground">
{creating
? "新角度创建后立即全局可用"
: "固定角色始终参与对应流程阶段"}
</span>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
disabled={busy}
onClick={() => handleOpenChange(false)}
>
关闭
</Button>
<Button
disabled={
!categoryCopy ||
!draft.name.trim() ||
busy ||
isLoading ||
isSkillsLoading ||
(!creating && !dirty)
}
onClick={() => void save()}
>
{busy ? (
<LoaderCircle className="size-4 animate-spin" />
) : creating ? (
<CirclePlus className="size-4" />
) : (
<Save className="size-4" />
)}
{creating ? "创建分析角度" : "保存当前配置"}
</Button>
</div>
</DialogFooter>
</DialogContent>
<SkillDetailDialog
skillName={detailSkillName}
open={detailSkillName !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDetailSkillName(null);
}}
/>
</Dialog>
);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,44 @@
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import { parseSseChunk } from "./lib";
import type { ParallelAgentEvent, ParallelAgentPayload } from "./types";
async function readError(response: Response): Promise<string> {
const body = (await response.json().catch(() => null)) as
| { detail?: string }
| null;
return body?.detail || `请求失败(HTTP ${response.status})`;
}
export async function streamParallelAgents(
payload: ParallelAgentPayload,
onEvent: (event: ParallelAgentEvent) => void,
signal?: AbortSignal,
): Promise<void> {
const response = await apiFetch(
`${getBackendBaseURL()}/api/parallel-agents/run/stream`,
{
method: "POST",
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal,
},
);
if (!response.ok) throw new Error(await readError(response));
if (!response.body) throw new Error("服务器未返回流式响应");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: !done });
const parsed = parseSseChunk(buffer, done);
buffer = parsed.rest;
parsed.events.forEach(onEvent);
if (done) break;
}
}
@@ -0,0 +1,350 @@
import type {
AgentOutputState,
IntentTurn,
StrategyAngle,
StrategyEvaluation,
StrategyTaskDraft,
} from "./types";
export const STRATEGY_AGENT_ROLES = [
{
id: "strategy-selection-intent",
name: "意图理解智能体",
role: "将自然语言需求整理为可执行任务",
},
{
id: "strategy-selection-angle-governance",
name: "治理统筹智能体",
role: "关注目标、机制、跨部门协同与执行闭环",
},
{
id: "strategy-selection-angle-diplomacy",
name: "外交沟通智能体",
role: "关注沟通渠道、危机管控与多边协调",
},
{
id: "strategy-selection-angle-economy",
name: "经济韧性智能体",
role: "关注供应链、产业、贸易与市场预期",
},
{
id: "strategy-selection-angle-security",
name: "安全风险智能体",
role: "关注预警、误判防范与非升级性风险处置",
},
{
id: "strategy-selection-angle-public-opinion",
name: "社会民生智能体",
role: "关注公众沟通、民生连续性与人道保障",
},
{
id: "strategy-selection-angle-law-policy",
name: "法律规则智能体",
role: "关注适用规则、合规边界与证据链",
},
{
id: "strategy-selection-judge",
name: "策略评判智能体",
role: "以统一量表比较策略的收益、风险与可执行性",
},
{
id: "strategy-selection-solution",
name: "方案整理智能体",
role: "将入选策略去重、消冲突并整理为最终方案",
},
] as const;
export const STRATEGY_ANGLES: StrategyAngle[] = [
{
id: "governance",
agentId: "strategy-selection-angle-governance",
name: "治理与统筹",
shortName: "治理",
description: "目标拆解、组织协同、阶段推进和反馈闭环",
accent: "#6d5dfc",
},
{
id: "diplomacy",
agentId: "strategy-selection-angle-diplomacy",
name: "外交与沟通",
shortName: "外交",
description: "危机沟通、多边协调、信号一致性与降温安排",
accent: "#0ea5e9",
},
{
id: "economy",
agentId: "strategy-selection-angle-economy",
name: "经济与产业韧性",
shortName: "经济",
description: "供应链连续性、市场预期、贸易和关键产业保障",
accent: "#10b981",
},
{
id: "security",
agentId: "strategy-selection-angle-security",
name: "安全与风险管控",
shortName: "安全",
description: "态势预警、误判防范、事件分级和非升级性处置",
accent: "#f97316",
},
{
id: "public_opinion",
agentId: "strategy-selection-angle-public-opinion",
name: "社会民生与舆情",
shortName: "民生",
description: "公众沟通、基本服务、社会心理和人道保障",
accent: "#ec4899",
},
{
id: "law_policy",
agentId: "strategy-selection-angle-law-policy",
name: "法律与规则",
shortName: "法律",
description: "适用规则、合规审查、责任边界与证据留存",
accent: "#a855f7",
},
];
export const EMPTY_TASK_DRAFT: StrategyTaskDraft = {
name: "",
detail: "",
objective: "",
prerequisites: "",
constraints: "",
deliverables: "",
acceptanceCriteria: "",
horizon: "",
selectedAngleIds: STRATEGY_ANGLES.slice(0, 4).map((angle) => angle.id),
};
export const INITIAL_INTENT_TURNS: IntentTurn[] = [
{
id: "welcome",
role: "assistant",
content:
"请描述你想解决的问题、希望达成的目标以及已知约束。我会通过对话补齐关键信息,并把需求实时整理到右侧任务草稿。",
},
];
export const TAIWAN_STRAIT_DEMO_TASK: StrategyTaskDraft = {
name: "台海局势风险管控与区域韧性提升策略",
detail:
"面向台海局势高不确定性场景,形成一套不预设立场、以避免误判和升级为核心的综合政策方案。方案聚焦危机沟通、区域经济与民生连续性、规则遵循及信息透明,不包含战术行动、目标选择或武力运用指导。",
objective:
"未来12—24个月内,降低突发事件转化为系统性危机的概率,保护平民福祉与关键经济活动,维持可验证、可持续的沟通和协调机制。",
prerequisites:
"建立跨部门联合研判机制;明确授权与对外沟通口径;掌握航运、能源、金融、通信和公共服务的基础风险数据;预留与各相关方沟通的技术渠道。",
constraints:
"遵守适用的国际法及人道原则;坚持克制、对等、可逆和非升级;不得输出军事战术、具体目标、武器使用或规避监管建议;关键事实须标注时点并交叉核验。",
deliverables:
"一份综合策略;分阶段行动清单;跨部门责任矩阵;风险预警指标;对外沟通要点;每月复盘和动态调整机制。",
acceptanceCriteria:
"措施均有责任主体、触发条件和退出条件;重要建议至少经两个角度交叉验证;高风险措施设置法律审查与升级门槛;方案可在72小时内启动首轮响应。",
horizon: "12—24个月,按0—30天、1—6个月、6—24个月分三阶段推进",
selectedAngleIds: STRATEGY_ANGLES.map((angle) => angle.id),
};
export const TAIWAN_STRAIT_DEMO_TURNS: IntentTurn[] = [
...INITIAL_INTENT_TURNS,
{
id: "demo-user",
role: "user",
isDemo: true,
content:
"请围绕台海局势做一个综合风险管控策略,重点是减少误判升级,同时兼顾经济、民生和规则约束,时间范围看未来一到两年。",
},
{
id: "demo-assistant",
role: "assistant",
isDemo: true,
content:
"已将需求整理为“风险管控与区域韧性提升”任务:以降级避险为目标,采用治理、外交、经济、安全、民生和法律六个角度并行论证;明确排除战术性军事建议,并补充了阶段周期、验收标准和启动条件。右侧字段仍可直接修改。",
},
];
export const TAIWAN_STRAIT_DEMO_OUTPUTS: Record<string, AgentOutputState> = {
"strategy-selection-angle-governance": {
status: "done",
isDemo: true,
steps: ["拆解目标", "形成协同机制", "设置复盘闭环"],
content: `### 治理统筹策略:建立“联合研判—分级响应—复盘校正”闭环
1. 设立常态化跨部门风险研判单元,统一风险词典、数据口径和事件分级。
2. 为不同风险等级预先定义责任主体、会商时限、可逆措施和退出条件,避免临时决策放大误判。
3. 建立月度桌面推演与季度复盘,重点校验交通、能源、金融、通信和公共服务之间的级联风险。
4. 重大调整实行“政策效果—升级风险—民生影响—法律合规”四项并联审查。
**优先动作:** 30天内完成责任矩阵和跨部门值守清单;90天内完成首次全链路压力测试。`,
},
"strategy-selection-angle-diplomacy": {
status: "done",
isDemo: true,
steps: ["识别沟通对象", "设计危机渠道", "校准公开信号"],
content: `### 外交沟通策略:保持多层次、可验证的危机沟通
1. 维护工作层、专业部门和高层三类沟通渠道,约定紧急事件的确认窗口与回执机制。
2. 对外表述采用“事实、原则、下一步安排”三段式,减少模糊威慑和相互误读。
3. 支持以航行安全、灾害救援、公共卫生等低政治敏感议题开展技术性接触,积累最低限度信任。
4. 引入可信第三方作为信息转递与事实核验的备份通道,但不替代直接沟通。
**触发条件:** 当出现信息不一致、渠道中断或舆情快速放大时,立即启动双渠道交叉确认。`,
},
"strategy-selection-angle-economy": {
status: "done",
isDemo: true,
steps: ["识别关键链路", "评估替代能力", "配置韧性措施"],
content: `### 经济韧性策略:先保连续性,再做结构性分散
1. 对航运、半导体、能源、金融结算和通信链路建立关键节点清单与中断情景。
2. 以库存天数、替代供应商覆盖率、运输改道时间和流动性缺口作为月度韧性指标。
3. 对短期冲击优先使用信息协调、应急库存和融资支持,避免仓促、不可逆的全面脱钩措施。
4. 中长期推动来源与路线多元化,同时保留正常经贸交往的政策空间。
**优先动作:** 对关键民生物资开展30/60/90天压力测试,并为中小企业准备定向流动性工具。`,
},
"strategy-selection-angle-security": {
status: "done",
isDemo: true,
steps: ["构建预警指标", "划分风险等级", "设计降级动作"],
content: `### 安全风险策略:以预警和降级机制减少偶发事件外溢
1. 建立覆盖异常活动、通信中断、错误信息扩散、交通受阻和市场波动的综合预警看板。
2. 将风险划分为关注、紧张、严重三级,每级只配置克制、可逆、可解释的非升级性应对动作。
3. 对来源未核实的信息实行“双源确认+时点标注”,避免单一信号直接触发重大政策响应。
4. 所有高风险决策设置冷静期、法律复核和跨部门反方意见席位。
**止损线:** 一旦沟通渠道失效或民生影响超过阈值,优先恢复沟通与公共服务,暂停可能叠加风险的新措施。`,
},
"strategy-selection-angle-public-opinion": {
status: "done",
isDemo: true,
steps: ["分析公众需求", "保障基本服务", "设计信息发布"],
content: `### 社会民生策略:稳定预期并保护基本服务
1. 建立权威、固定频率的信息发布窗口,明确已知、未知和正在核验的事项。
2. 为交通、医疗、能源、通信和教育准备连续性方案,优先保障脆弱群体。
3. 监测谣言与恐慌性行为时,采用事实澄清和服务指引,不扩大未经证实的信息。
4. 设置独立反馈渠道,收集企业、社区和民众的实际困难并纳入每日研判。
**评价指标:** 基本服务可用率、权威信息触达率、谣言纠正时长和重点群体保障覆盖率。`,
},
"strategy-selection-angle-law-policy": {
status: "done",
isDemo: true,
steps: ["识别规则边界", "建立审查清单", "设计留痕机制"],
content: `### 法律规则策略:把合规审查嵌入每个决策节点
1. 为数据共享、经济措施、应急管理和对外沟通分别建立适用规则清单。
2. 重大措施记录事实依据、授权来源、比例性判断、影响评估和退出条件。
3. 涉及平民权益、基本服务和跨境活动的措施优先进行人道影响评估。
4. 对时效性强的事项采用“快速初审—事后完整复核”,但不得跳过最低合规门槛。
**优先动作:** 建立统一法律审查模板和决策证据包,保证后续可解释、可追溯。`,
},
};
export const TAIWAN_STRAIT_DEMO_EVALUATIONS: StrategyEvaluation[] = [
{
agentId: "strategy-selection-angle-governance",
score: 92,
conclusion: "优先推荐",
comment: "跨部门闭环清晰,是其他策略落地的共同底座。",
strengths: ["责任明确", "阶段完整", "便于复盘"],
risks: ["需要稳定的牵头机制"],
},
{
agentId: "strategy-selection-angle-diplomacy",
score: 90,
conclusion: "优先推荐",
comment: "直接作用于误判风险,且措施克制、可逆。",
strengths: ["降级效果直接", "渠道有备份"],
risks: ["依赖相关方持续参与"],
},
{
agentId: "strategy-selection-angle-economy",
score: 86,
conclusion: "建议采用",
comment: "兼顾短期连续性与长期韧性,需控制过度防御成本。",
strengths: ["指标可量化", "避免一刀切"],
risks: ["跨行业数据获取成本较高"],
},
{
agentId: "strategy-selection-angle-security",
score: 91,
conclusion: "优先推荐",
comment: "预警、确认和冷静期设计可显著降低错误触发。",
strengths: ["止损线清楚", "强调双源核验"],
risks: ["阈值需持续校准"],
},
{
agentId: "strategy-selection-angle-public-opinion",
score: 84,
conclusion: "建议采用",
comment: "增强社会承压能力,建议与经济连续性方案合并实施。",
strengths: ["关注脆弱群体", "沟通指标明确"],
risks: ["需避免信息发布碎片化"],
},
{
agentId: "strategy-selection-angle-law-policy",
score: 88,
conclusion: "建议采用",
comment: "为高压情境下的决策提供必要边界和可追溯性。",
strengths: ["审查嵌入流程", "留痕充分"],
risks: ["快速响应与完整审查需平衡"],
},
];
export const TAIWAN_STRAIT_DEMO_SYNTHESIS = `# 台海局势风险管控与区域韧性提升综合方案
> **示例说明:** 本方案为产品功能演示数据,采用中立、降级避险和民生优先视角,不构成现实决策建议;正式使用前应依据最新可靠信息进行专业复核。
## 一、总体目标
在未来12—24个月内,以“减少误判、阻断升级、保护民生、维持连续性”为主线,建立可监测、可启动、可退出、可复盘的风险管控体系。任何措施均应符合适用规则,坚持克制、对等、可逆和可解释。
## 二、四条主策略
### 1. 联合研判与分级响应
- 建立跨部门常态研判单元,统一风险词典、数据口径和三级事件标准。
- 每个级别明确牵头主体、会商时限、启动阈值、允许动作和退出条件。
- 重大建议同时接受政策效果、升级风险、民生影响和法律合规审查。
### 2. 多层危机沟通与事实核验
- 维护工作层、专业部门和高层沟通渠道,并配置可信第三方备份。
- 紧急事项执行“双渠道回执、双来源确认、统一时点标注”。
- 公开沟通采用“事实—原则—下一步安排”结构,降低信号歧义。
### 3. 经济与公共服务连续性
- 围绕航运、能源、金融、通信、医疗和关键供应链建立中断情景清单。
- 用库存天数、替代覆盖率、改道时长、服务可用率等指标开展月度压力测试。
- 短期优先使用应急库存、信息协调和定向融资;中长期有序提升来源与路线多元化。
### 4. 规则嵌入与社会韧性
- 将授权来源、比例性、人道影响、退出条件和证据留存纳入统一审查模板。
- 通过固定频率的权威发布说明已知、未知和核验进度,提供清晰服务指引。
- 优先保障脆弱群体,并建立企业、社区与公众反馈的快速回流机制。
## 三、阶段安排
| 阶段 | 重点动作 | 主要产出 |
| --- | --- | --- |
| 0—30天 | 完成责任矩阵、沟通清单、关键链路盘点与法律审查模板 | 一图一表一清单、首版预警看板 |
| 1—6个月 | 开展压力测试和桌面推演,打通双渠道确认与公众反馈 | 演练报告、阈值校准、改进台账 |
| 6—24个月 | 推进结构性韧性项目并季度复盘,动态退出低效措施 | 韧性提升项目、季度评估报告 |
## 四、核心预警指标
1. 沟通渠道可用率与紧急事项回执时长。
2. 未核实信息传播速度及权威澄清时长。
3. 关键运输、能源、金融、通信和公共服务可用率。
4. 市场异常波动、关键物资库存天数与替代供应覆盖率。
5. 脆弱群体保障覆盖率和公众反馈闭环时长。
## 五、止损与调整机制
当直接沟通失效、基本服务明显下降、信息可信度不足或风险指标跨级跃升时,优先恢复沟通、事实核验和民生服务;暂停新增高风险措施。每月复盘策略组合,每季度由独立评估组检查目标偏移、措施叠加和退出条件执行情况。
`;
@@ -0,0 +1,329 @@
/**
* Run from frontend-web:
* npx esbuild src/strategy-selection/lib.test.ts --bundle --format=esm --platform=node --outfile=.runtime/unit-tests/strategy-selection.test.mjs
* node --test .runtime/unit-tests/strategy-selection.test.mjs
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
areStrategyTaskDraftsEquivalent,
buildIntentBrief,
buildJudgeBrief,
buildSynthesisBrief,
extractFencedJson,
normalizeIntentClarification,
parseEvaluationsFromText,
parseSseChunk,
parseTaskDraftFromText,
stripStructuredBlocks,
} from "./lib";
import { EMPTY_TASK_DRAFT, STRATEGY_ANGLES } from "./constants";
import type { AgentOutputState, StrategyAngle } from "./types";
test("parseSseChunk preserves incomplete frames and parses subsequent frames", () => {
const first = parseSseChunk(
'data: {"type":"agent_start","agent_id":"a"}\n\ndata: {"type":"del',
);
assert.equal(first.events.length, 1);
assert.equal(first.events[0].type, "agent_start");
const second = parseSseChunk(`${first.rest}ta","agent_id":"a","text":"你好"}\n\n`);
assert.deepEqual(second.events, [
{ type: "delta", agent_id: "a", text: "你好" },
]);
});
test("normalizeIntentClarification accepts string and object options", () => {
const clarification = normalizeIntentClarification({
type: "agent_clarification",
agent_id: "strategy-selection-intent",
question: "请选择时间范围",
options: ["短期", { id: "long", label: "长期", description: "一年以上" }],
allow_custom: true,
allow_multiple: false,
});
assert.equal(clarification.question, "请选择时间范围");
assert.deepEqual(clarification.options, [
{ id: "option-1", label: "短期", description: undefined },
{ id: "long", label: "长期", description: "一年以上" },
]);
assert.equal(clarification.allowCustom, true);
});
test("task draft equivalence ignores whitespace and angle ordering only", () => {
const left = {
...EMPTY_TASK_DRAFT,
name: "区域 风险方案",
detail: "第一行\n第二行",
selectedAngleIds: ["governance", "diplomacy"],
};
const presentationOnlyChange = {
...left,
name: " 区域 风险方案 ",
detail: "第一行 第二行",
selectedAngleIds: ["diplomacy", "governance", "diplomacy"],
};
assert.equal(
areStrategyTaskDraftsEquivalent(left, presentationOnlyChange),
true,
);
assert.equal(
areStrategyTaskDraftsEquivalent(left, {
...presentationOnlyChange,
objective: "新增目标",
}),
false,
);
});
test("intent brief retains the clarification question and the user's confirmed answer", () => {
const brief = buildIntentBrief(
[
{
id: "clarify",
role: "assistant",
content: "",
clarification: {
question: "方案主要面向谁?",
options: [{ id: "public", label: "公共部门" }],
allowCustom: true,
allowMultiple: false,
},
clarificationAnswered: true,
clarificationAnswer: "公共部门",
},
{ id: "answer", role: "user", content: "对澄清问题的回答:公共部门" },
],
EMPTY_TASK_DRAFT,
);
assert.match(brief, /意图助手澄清:方案主要面向谁/);
assert.match(brief, /用户对该澄清的确认:公共部门/);
});
test("stripStructuredBlocks keeps prose and hides machine-readable payloads", () => {
assert.equal(
stripStructuredBlocks('说明文字\n[INTENT_READY]\n```strategy-task-json\n{"name":"x"}\n```'),
"说明文字",
);
});
test("extractFencedJson and parseTaskDraftFromText read the structured task block", () => {
const text = [
"已整理任务。",
"```strategy-task-json",
JSON.stringify({
name: "测试任务",
detail: "详情",
recommended_angles: ["governance", "unknown", "law_policy"],
}),
"```",
].join("\n");
assert.deepEqual(extractFencedJson(text, "strategy-task-json"), {
name: "测试任务",
detail: "详情",
recommended_angles: ["governance", "unknown", "law_policy"],
});
const task = parseTaskDraftFromText(text, EMPTY_TASK_DRAFT);
assert.equal(task?.name, "测试任务");
assert.deepEqual(task?.selectedAngleIds, ["governance", "law_policy"]);
});
test("task parser accepts backend SOUL field aliases", () => {
const text = `\`\`\`strategy-task-json\n${JSON.stringify({
task_name: "别名任务",
task_detail: "别名详情",
expected_outputs: "综合方案",
recommended_angles: ["public_opinion", "strategy-selection-angle-law-policy"],
})}\n\`\`\``;
const task = parseTaskDraftFromText(text, EMPTY_TASK_DRAFT);
assert.equal(task?.name, "别名任务");
assert.equal(task?.detail, "别名详情");
assert.equal(task?.deliverables, "综合方案");
assert.deepEqual(task?.selectedAngleIds, ["public_opinion", "law_policy"]);
});
test("task parser rejects incomplete or unrecognized current-turn payloads", () => {
const completeFallback = {
...EMPTY_TASK_DRAFT,
name: "旧任务",
detail: "旧详情",
selectedAngleIds: ["governance"],
};
const wrap = (payload: unknown) =>
`\`\`\`strategy-task-json\n${JSON.stringify(payload)}\n\`\`\``;
assert.equal(parseTaskDraftFromText(wrap({}), completeFallback), null);
assert.equal(parseTaskDraftFromText(wrap([]), completeFallback), null);
assert.equal(
parseTaskDraftFromText(
wrap({ name: "新任务", detail: "新详情", selectedAngleIds: ["unknown"] }),
completeFallback,
),
null,
);
assert.equal(
parseTaskDraftFromText(
wrap({ name: "新任务", detail: "新详情" }),
completeFallback,
),
null,
);
});
test("evaluation parser validates scores without inventing missing candidates", () => {
const outputs: Record<string, AgentOutputState> = {
"strategy-selection-angle-governance": {
status: "done",
content: "A",
steps: [],
},
"strategy-selection-angle-law-policy": { status: "done", content: "B", steps: [] },
"strategy-selection-angle-security": {
status: "error",
content: "PARTIAL_FAILED_OUTPUT",
steps: [],
error: "failed",
},
};
const text = `\`\`\`strategy-evaluation-json\n${JSON.stringify({
evaluations: [
{
agentId: "strategy-selection-angle-governance",
score: 100,
conclusion: "优先推荐",
comment: "清晰",
},
],
})}\n\`\`\``;
const evaluations = parseEvaluationsFromText(text, outputs);
assert.equal(evaluations.length, 1);
assert.equal(evaluations[0].score, 100);
assert.equal(
evaluations.some(
(item) => item.agentId === "strategy-selection-angle-law-policy",
),
false,
);
assert.equal(buildJudgeBrief(EMPTY_TASK_DRAFT, outputs).includes("PARTIAL_FAILED_OUTPUT"), false);
});
test("judge prompt and evaluation parser only use task-selected angles", () => {
const outputs: Record<string, AgentOutputState> = {
"strategy-selection-angle-governance": { status: "done", content: "治理策略", steps: [] },
"strategy-selection-angle-law-policy": { status: "done", content: "法律策略", steps: [] },
};
const task = { ...EMPTY_TASK_DRAFT, selectedAngleIds: ["governance"] };
const brief = buildJudgeBrief(task, outputs);
assert.equal(brief.includes("治理策略"), true);
assert.equal(brief.includes("法律策略"), false);
const evaluations = parseEvaluationsFromText("无结构化量表", outputs, [
"strategy-selection-angle-governance",
]);
assert.deepEqual(evaluations, []);
});
test("dynamic administrator angles flow through intent parsing and judging without fabricated evaluation", () => {
const dynamic: StrategyAngle = {
id: "technology",
agentId: "strategy-selection-angle-dynamic-tech",
name: "科技与产业链",
shortName: "科技",
description: "关注技术依赖与产业协同",
accent: "#2563eb",
enabled: true,
};
const task = {
...EMPTY_TASK_DRAFT,
selectedAngleIds: [dynamic.id],
};
const parsed = parseTaskDraftFromText(
`\`\`\`strategy-task-json\n${JSON.stringify({
name: "动态任务",
detail: "测试管理员新增角度",
selectedAngleIds: [dynamic.agentId],
})}\n\`\`\``,
task,
[dynamic],
);
assert.deepEqual(parsed?.selectedAngleIds, [dynamic.id]);
assert.match(buildIntentBrief([], task, [dynamic]), /technology:科技与产业链/);
const outputs: Record<string, AgentOutputState> = {
[dynamic.agentId]: { status: "done", content: "科技策略", steps: [] },
};
assert.match(buildJudgeBrief(task, outputs, [dynamic]), /科技策略/);
assert.deepEqual(
parseEvaluationsFromText("没有结构化评判", outputs, [dynamic.agentId], [dynamic]),
[],
);
});
test("evaluation parser rejects missing fields, invalid scores, and duplicate agent rows", () => {
const agentId = "strategy-selection-angle-governance";
const outputs: Record<string, AgentOutputState> = {
[agentId]: { status: "done", content: "治理策略", steps: [] },
};
const wrap = (evaluations: Record<string, unknown>[]) =>
`\`\`\`strategy-evaluation-json\n${JSON.stringify({ evaluations })}\n\`\`\``;
assert.deepEqual(
parseEvaluationsFromText(
`\`\`\`strategy-evaluation-json\n${JSON.stringify({ evaluations: [null] })}\n\`\`\``,
outputs,
[agentId],
),
[],
);
assert.deepEqual(
parseEvaluationsFromText(wrap([{ agentId, conclusion: "建议采用" }]), outputs, [agentId]),
[],
);
assert.deepEqual(
parseEvaluationsFromText(
wrap([{ agentId, score: 80.5, conclusion: "建议采用" }]),
outputs,
[agentId],
),
[],
);
assert.deepEqual(
parseEvaluationsFromText(
wrap([
{ agentId, score: 80, conclusion: "建议采用" },
{ agentId, score: 75, conclusion: "保留备选" },
]),
outputs,
[agentId],
),
[],
);
});
test("synthesis prompt exposes only the user's final strategy basket", () => {
const governance = STRATEGY_ANGLES.find((angle) => angle.id === "governance")!;
const security = STRATEGY_ANGLES.find((angle) => angle.id === "security")!;
const task = {
...EMPTY_TASK_DRAFT,
selectedAngleIds: STRATEGY_ANGLES.map((angle) => angle.id),
};
const outputs: Record<string, AgentOutputState> = {
[governance.agentId]: { status: "done", content: "治理入选正文", steps: [] },
[security.agentId]: { status: "done", content: "安全未入选正文", steps: [] },
};
const brief = buildSynthesisBrief(
task,
[governance.agentId],
outputs,
[],
STRATEGY_ANGLES,
);
assert.match(
brief,
new RegExp(`最终只允许整合以下 1 项:${governance.name}`),
);
assert.match(brief, /治理入选正文/);
assert.equal(brief.includes("安全未入选正文"), false);
assert.match(brief, /"selectedAngleIds": \[\s*"governance"\s*\]/);
});
+408
View File
@@ -0,0 +1,408 @@
import { STRATEGY_ANGLES } from "./constants";
import type {
AgentOutputState,
IntentClarification,
IntentTurn,
ParallelAgentEvent,
StrategyAngle,
StrategyEvaluation,
StrategyTaskDraft,
} from "./types";
function stringValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
export function normalizeIntentClarification(
event: Extract<ParallelAgentEvent, { type: "agent_clarification" }>,
): IntentClarification {
const question =
stringValue(event.question) ||
stringValue(event.content) ||
"请补充完成任务定义所需的信息。";
const options = Array.isArray(event.options)
? event.options
.map((option, index) => {
const label =
typeof option === "string" ? stringValue(option) : stringValue(option?.label);
if (!label) return null;
return {
id:
(typeof option === "string" ? "" : stringValue(option.id)) ||
`option-${index + 1}`,
label,
description:
(typeof option === "string" ? "" : stringValue(option.description)) ||
undefined,
};
})
.filter((option): option is NonNullable<typeof option> => option !== null)
: [];
return {
question,
context:
stringValue(event.clarification_context) ||
stringValue(event.context) ||
undefined,
clarificationType: stringValue(event.clarification_type) || undefined,
options,
allowCustom: event.allow_custom !== false || options.length === 0,
allowMultiple: event.allow_multiple === true,
};
}
function selectedStrategyAgentIds(
task: StrategyTaskDraft,
angles: StrategyAngle[],
): Set<string> {
return new Set(
angles.filter((angle) => task.selectedAngleIds.includes(angle.id)).map(
(angle) => angle.agentId,
),
);
}
const TASK_DRAFT_TEXT_FIELDS = [
"name",
"detail",
"objective",
"prerequisites",
"constraints",
"deliverables",
"acceptanceCriteria",
"horizon",
] as const satisfies readonly (keyof StrategyTaskDraft)[];
function normalizeTaskDraftText(value: string): string {
return value.trim().replace(/\s+/g, " ");
}
/**
* Treat presentation-only whitespace and angle ordering as equivalent so an
* intent-agent round trip cannot invalidate an already generated workflow
* unless the task's meaning actually changed.
*/
export function areStrategyTaskDraftsEquivalent(
left: StrategyTaskDraft,
right: StrategyTaskDraft,
): boolean {
const textFieldsMatch = TASK_DRAFT_TEXT_FIELDS.every(
(field) =>
normalizeTaskDraftText(left[field] as string) ===
normalizeTaskDraftText(right[field] as string),
);
if (!textFieldsMatch) return false;
const leftAngles = [...new Set(left.selectedAngleIds)].sort();
const rightAngles = [...new Set(right.selectedAngleIds)].sort();
return (
leftAngles.length === rightAngles.length &&
leftAngles.every((angleId, index) => angleId === rightAngles[index])
);
}
function stringList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map(stringValue).filter(Boolean);
}
export function extractFencedJson<T>(text: string, marker?: string): T | null {
const escaped = marker?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const patterns = [
escaped ? new RegExp("```" + escaped + "\\s*([\\s\\S]*?)```", "i") : null,
/```(?:json)?\s*([\s\S]*?)```/i,
].filter(Boolean) as RegExp[];
for (const pattern of patterns) {
const match = pattern.exec(text);
if (!match?.[1]) continue;
try {
return JSON.parse(match[1].trim()) as T;
} catch {
// The model may have emitted another fenced block first; fall through to
// the balanced-object fallback below.
}
}
const firstObject = text.indexOf("{");
const lastObject = text.lastIndexOf("}");
if (firstObject >= 0 && lastObject > firstObject) {
try {
return JSON.parse(text.slice(firstObject, lastObject + 1)) as T;
} catch {
return null;
}
}
return null;
}
export function stripStructuredBlocks(text: string): string {
return text
.replace(/```(?:strategy-task-json|strategy-evaluation-json)\s*[\s\S]*?```/gi, "")
.replace(/\[(?:INTENT_READY|JUDGEMENT_READY)\]/gi, "")
.trim();
}
export function parseTaskDraftFromText(
text: string,
fallback: StrategyTaskDraft,
angles: StrategyAngle[] = STRATEGY_ANGLES,
): StrategyTaskDraft | null {
const parsed = extractFencedJson<Record<string, unknown>>(text, "strategy-task-json");
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const rawName = parsed.name ?? parsed.task_name;
const rawDetail = parsed.detail ?? parsed.task_detail;
const rawAngles = parsed.selectedAngleIds ?? parsed.recommended_angles;
if (
typeof rawName !== "string" ||
!rawName.trim() ||
typeof rawDetail !== "string" ||
!rawDetail.trim() ||
!Array.isArray(rawAngles)
) {
return null;
}
const angleIds = [
...new Set(
rawAngles
.map(stringValue)
.map((value) => {
const normalized = value.toLowerCase().replaceAll("-", "_");
return angles.find(
(angle) =>
angle.id === normalized ||
angle.agentId === value ||
angle.name === value ||
angle.shortName === value,
)?.id;
})
.filter((id): id is string => Boolean(id)),
),
];
if (!angleIds.length) return null;
return {
name: stringValue(rawName),
detail: stringValue(rawDetail),
objective: stringValue(parsed.objective) || fallback.objective,
prerequisites:
stringValue(parsed.prerequisites) || fallback.prerequisites,
constraints: stringValue(parsed.constraints) || fallback.constraints,
deliverables:
stringValue(parsed.deliverables ?? parsed.expected_outputs) || fallback.deliverables,
acceptanceCriteria:
stringValue(parsed.acceptanceCriteria) || fallback.acceptanceCriteria,
horizon: stringValue(parsed.horizon) || fallback.horizon,
selectedAngleIds: angleIds,
};
}
export function parseEvaluationsFromText(
text: string,
outputs: Record<string, AgentOutputState>,
allowedAgentIds?: Iterable<string>,
angles: StrategyAngle[] = STRATEGY_ANGLES,
): StrategyEvaluation[] {
const parsed = extractFencedJson<
{ evaluations?: Record<string, unknown>[] } | Record<string, unknown>[]
>(text, "strategy-evaluation-json");
const rows = Array.isArray(parsed) ? parsed : parsed?.evaluations;
if (!Array.isArray(rows)) return [];
const allowed = allowedAgentIds ? new Set(allowedAgentIds) : null;
const knownIds = new Set(
angles.filter((angle) => !allowed || allowed.has(angle.agentId)).map(
(angle) => angle.agentId,
),
);
const evaluations: StrategyEvaluation[] = [];
const seen = new Set<string>();
for (const row of rows) {
if (!row || typeof row !== "object" || Array.isArray(row)) return [];
const agentId = stringValue(row.agentId ?? row.agent_id);
if (!knownIds.has(agentId)) continue;
const score = row.score;
const conclusion = stringValue(row.conclusion);
if (
seen.has(agentId) ||
outputs[agentId]?.status !== "done" ||
!outputs[agentId]?.content.trim() ||
typeof score !== "number" ||
!Number.isFinite(score) ||
!Number.isInteger(score) ||
score < 0 ||
score > 100 ||
(conclusion !== "优先推荐" &&
conclusion !== "建议采用" &&
conclusion !== "保留备选")
) {
// Reject the entire structured result instead of inventing defaults or
// allowing duplicate rows to be selected twice downstream.
return [];
}
seen.add(agentId);
evaluations.push({
agentId,
score,
conclusion,
comment: stringValue(row.comment) || "请结合完整评判正文审阅。",
strengths: stringList(row.strengths),
risks: stringList(row.risks),
});
}
if (allowed && [...allowed].some((agentId) => !seen.has(agentId))) return [];
return evaluations.sort((a, b) => b.score - a.score);
}
export function buildIntentBrief(
turns: IntentTurn[],
currentDraft: StrategyTaskDraft,
angles: StrategyAngle[] = STRATEGY_ANGLES,
): string {
const conversation = turns
.filter(
(turn) =>
turn.content.trim() ||
turn.clarification?.question.trim() ||
turn.clarificationAnswer?.trim(),
)
.map((turn) => {
const lines: string[] = [];
if (turn.content.trim()) {
lines.push(
`${turn.role === "user" ? "用户" : "意图助手"}:${turn.content}`,
);
}
if (turn.clarification) {
const optionText = turn.clarification.options
.map((option) => option.label)
.filter(Boolean)
.join(" / ");
lines.push(
`意图助手澄清:${turn.clarification.question}${
optionText ? `(可选:${optionText})` : ""
}`,
);
}
if (turn.clarificationAnswer?.trim()) {
lines.push(`用户对该澄清的确认:${turn.clarificationAnswer}`);
}
return lines.join("\n");
})
.join("\n\n");
const availableAngles = angles
.filter((angle) => angle.enabled !== false)
.map((angle) => `- ${angle.id}:${angle.name}(${angle.description})`)
.join("\n");
const exampleAngleIds = angles
.filter((angle) => angle.enabled !== false)
.slice(0, 3)
.map((angle) => angle.id);
return `你负责意图理解与任务定义。基于以下对话整理任务。不要虚构实时事实。\n\n当前草稿(可据新信息修订):\n${JSON.stringify(currentDraft, null, 2)}\n\n本系统当前可选角度(只能从下列 id 中选择):\n${availableAngles}\n\n对话:\n${conversation}\n\n请严格二选一执行:\n1. 如果不同答案会显著改变任务方向,立即调用 ask_clarification,只提一个最关键问题并提供清晰互斥选项;调用后停止,不输出任务 JSON,也不要在普通正文里假装提问。\n2. 如果信息已足够(包括用户刚回答了澄清),先用简洁自然语言复述整理结果,再在回复末尾追加以下可机器读取代码块,字段名不得改变:\n\`\`\`strategy-task-json\n{"name":"任务名称","detail":"任务详情","objective":"目标","prerequisites":"前置条件","constraints":"约束与边界","deliverables":"交付物","acceptanceCriteria":"验收标准","horizon":"时间范围","selectedAngleIds":${JSON.stringify(exampleAngleIds)}}\n\`\`\`\n只选择与任务有关的角度。`;
}
export function buildStrategyBrief(
task: StrategyTaskDraft,
selectedAngles = STRATEGY_ANGLES.filter((angle) =>
task.selectedAngleIds.includes(angle.id),
),
): string {
const angles = selectedAngles
.map((angle) => `- ${angle.name}:${angle.description}`)
.join("\n");
return `你是其中一个专业角度的策略生成智能体,只从自己的职责角度输出一套可独立审阅的策略。\n\n任务详情:\n${JSON.stringify(task, null, 2)}\n\n本轮并行角度:\n${angles}\n\n请给出:核心判断、3—6项策略动作、实施阶段、触发/退出条件、指标、主要风险与协同依赖。避免空泛口号;涉及动态事实时说明需复核。工具失败、文件路径、权限问题和检索计划属于执行过程,不得混入策略正文;确有信息缺口时只在独立的“数据限制与待核验”小节简要列明。对于地缘政治议题,坚持中立、降级避险、民生优先,不输出军事战术、目标选择、武器运用或规避监管建议。`;
}
export function buildJudgeBrief(
task: StrategyTaskDraft,
outputs: Record<string, AgentOutputState>,
angles: StrategyAngle[] = STRATEGY_ANGLES,
): string {
const selectedAgentIds = selectedStrategyAgentIds(task, angles);
const strategies = angles.filter(
(angle) =>
selectedAgentIds.has(angle.agentId) &&
outputs[angle.agentId]?.status === "done" &&
outputs[angle.agentId]?.content.trim(),
)
.map(
(angle) =>
`## ${angle.name}\nagentId: ${angle.agentId}\n${outputs[angle.agentId].content}`,
)
.join("\n\n");
return `你是独立策略评判智能体。请依据任务目标,对候选策略的目标贡献、可执行性、风险可控性、协同性和合规性进行比较。先输出总体评判与冲突提醒,再在末尾输出结构化量表。工具失败、文件路径、权限问题和检索计划属于执行过程,不得写进评判正文;确有信息缺口时只在独立的“数据限制与待核验”小节简要列明。\n\n任务:\n${JSON.stringify(task, null, 2)}\n\n候选策略:\n${strategies}\n\n末尾必须追加:\n\`\`\`strategy-evaluation-json\n{"evaluations":[{"agentId":"候选agentId","score":0,"conclusion":"优先推荐|建议采用|保留备选","comment":"一句评价","strengths":["优势"],"risks":["风险"]}]}\n\`\`\`\n必须覆盖每个候选策略,score为0—100整数。`;
}
export function buildSynthesisBrief(
task: StrategyTaskDraft,
selectedAgentIds: string[],
outputs: Record<string, AgentOutputState>,
evaluations: StrategyEvaluation[],
angles: StrategyAngle[] = STRATEGY_ANGLES,
): string {
const evaluationMap = new Map(evaluations.map((item) => [item.agentId, item]));
const uniqueSelectedAgentIds = [...new Set(selectedAgentIds)];
const selectedAngles = uniqueSelectedAgentIds
.map((agentId) => angles.find((item) => item.agentId === agentId))
.filter((angle): angle is StrategyAngle => Boolean(angle));
const selectedAngleIds = new Set(selectedAngles.map((angle) => angle.agentId));
const exactTask = {
...task,
selectedAngleIds: selectedAngles.map((angle) => angle.id),
};
const selections = uniqueSelectedAgentIds
.filter((agentId) => selectedAngleIds.has(agentId))
.map((agentId) => {
const angle = angles.find((item) => item.agentId === agentId);
const evaluation = evaluationMap.get(agentId);
return `## ${angle?.name ?? agentId}\n评判:${evaluation ? `${evaluation.score}分,${evaluation.comment}` : "用户手动选入"}\n${outputs[agentId]?.content ?? ""}`;
})
.join("\n\n");
const selectedNames = selectedAngles.map((angle) => angle.name).join("、");
return `你是方案整理智能体。请将用户选中的策略消除重复、处理冲突、明确优先级和依赖关系,整理为一份可直接下载的完整 Markdown 方案。\n\n任务(selectedAngleIds 已收敛为用户最终选入的策略角度):\n${JSON.stringify(exactTask, null, 2)}\n\n本次最终只允许整合以下 ${selectedAngles.length} 项:${selectedNames}\n不得补写、复活或暗中纳入任何未列出的角度或策略。\n\n入选策略:\n${selections}\n\n只输出最终 Markdown 正文。工具失败、文件路径、权限问题和检索计划属于执行过程,不得混入方案正文;确有信息缺口时仅归入“待核验信息”小节。至少包含:执行摘要、目标与原则、综合策略、阶段路线图、责任与协同、指标与预警、风险及止损机制、待核验信息。不要输出代码围栏。对于地缘政治议题坚持中立、降级避险、民生优先,禁止军事战术和伤害性操作建议。`;
}
export function parseSseChunk(
buffer: string,
flush = false,
): { events: ParallelAgentEvent[]; rest: string } {
const normalized = buffer.replace(/\r\n/g, "\n");
const parts = normalized.split("\n\n");
const rest = flush ? "" : (parts.pop() ?? "");
const complete = flush ? parts.concat(parts.length ? [] : [normalized]) : parts;
const events: ParallelAgentEvent[] = [];
for (const frame of complete) {
const payload = frame
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n")
.trim();
if (!payload || payload === "[DONE]") continue;
try {
const event = JSON.parse(payload) as ParallelAgentEvent;
if (event && typeof event.type === "string") events.push(event);
} catch {
// Ignore one malformed frame instead of discarding the rest of a stream.
}
}
return { events, rest };
}
export function downloadMarkdown(markdown: string, fileName: string): void {
const cleanName =
fileName.replace(/\.(md|markdown)$/i, "").replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").trim() ||
"strategy-plan";
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${cleanName}.md`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 10_000);
}
@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
strategyProjectListPath,
strategyProjectWorkbenchPath,
} from "./project-routes";
test("solution list paths work in both application shells", () => {
assert.equal(
strategyProjectListPath("/page/strategy/qa/projects", "solution"),
"/page/strategy/qa/projects?type=solution",
);
assert.equal(
strategyProjectListPath("/official/workspace/projects/", "solution"),
"/official/workspace/projects?type=solution",
);
});
test("solution workbench paths encode ids and request the solution module", () => {
assert.equal(
strategyProjectWorkbenchPath(
"/official/workspace/projects",
"strategy-selection-a/b",
"solution",
),
"/official/workspace/projects/strategy-selection-a%2Fb?module=solution",
);
});
@@ -0,0 +1,28 @@
export type StrategyProjectListCategory =
| "all"
| "qa"
| "strategy"
| "solution";
function normalizeBasePath(basePath: string): string {
return basePath.replace(/\/+$/, "");
}
export function strategyProjectListPath(
basePath: string,
category: StrategyProjectListCategory = "all",
): string {
const path = normalizeBasePath(basePath);
return category === "all"
? path
: `${path}?type=${encodeURIComponent(category)}`;
}
export function strategyProjectWorkbenchPath(
basePath: string,
projectId: string,
module?: "solution",
): string {
const path = `${normalizeBasePath(basePath)}/${encodeURIComponent(projectId)}`;
return module === "solution" ? `${path}?module=solution` : path;
}
@@ -0,0 +1,147 @@
/**
* Run from frontend-web:
* npx esbuild src/strategy-selection/project-wal.test.ts --bundle --format=esm --platform=node --outfile=.runtime/unit-tests/project-wal.test.mjs
* node --test .runtime/unit-tests/project-wal.test.mjs
*/
import assert from "node:assert/strict";
import test from "node:test";
import type { StrategyProjectSnapshot } from "./projects";
import {
clearStrategyProjectWal,
isStrategyProjectWalExpired,
LatestByKeyScheduler,
resolveStrategyProjectWal,
strategyProjectSnapshotHash,
strategyProjectWalKey,
writeStrategyProjectWal,
} from "./project-wal";
const createSnapshot = (): StrategyProjectSnapshot => ({
version: 1,
angleCatalog: [],
stage: "intent",
taskDraft: {
name: "",
detail: "",
objective: "",
prerequisites: "",
constraints: "",
deliverables: "",
acceptanceCriteria: "",
horizon: "",
selectedAngleIds: [],
},
turns: [],
outputs: {},
judgeStatus: "idle",
judgeContent: "",
evaluations: [],
selectedStrategyIds: [],
synthesis: "",
synthesisStatus: "idle",
solutionSavedAt: null,
isDemo: false,
workflowStale: false,
});
test("WAL keys isolate accounts and projects", () => {
assert.notEqual(
strategyProjectWalKey("account:one", "project-a"),
strategyProjectWalKey("account:two", "project-a"),
);
assert.notEqual(
strategyProjectWalKey("account:one", "project-a"),
strategyProjectWalKey("account:one", "project-b"),
);
});
test("snapshot hashes are stable and change with project content", async () => {
const snapshot = createSnapshot();
const same = createSnapshot();
const changed = {
...createSnapshot(),
taskDraft: {
...createSnapshot().taskDraft,
name: "已修改任务",
},
};
assert.equal(
await strategyProjectSnapshotHash(snapshot),
await strategyProjectSnapshotHash(same),
);
assert.notEqual(
await strategyProjectSnapshotHash(snapshot),
await strategyProjectSnapshotHash(changed),
);
});
test("WAL degrades safely when IndexedDB is unavailable", async () => {
const snapshot = createSnapshot();
assert.equal(
await writeStrategyProjectWal(
"account:test",
"project-test",
"base-hash",
snapshot,
),
null,
);
assert.equal(
await clearStrategyProjectWal("account:test", "project-test", snapshot),
false,
);
});
test("WAL recovery uses ancestry hashes instead of incomparable clocks", async () => {
const server = createSnapshot();
const local = {
...createSnapshot(),
taskDraft: { ...createSnapshot().taskDraft, name: "本地编辑" },
};
const otherServer = {
...createSnapshot(),
taskDraft: { ...createSnapshot().taskDraft, name: "服务器编辑" },
};
const serverHash = await strategyProjectSnapshotHash(server);
const localHash = await strategyProjectSnapshotHash(local);
const otherServerHash = await strategyProjectSnapshotHash(otherServer);
const wal = { baseServerHash: serverHash, snapshotHash: localHash };
assert.equal(resolveStrategyProjectWal(wal, serverHash), "recover");
assert.equal(resolveStrategyProjectWal(wal, localHash), "same");
assert.equal(resolveStrategyProjectWal(wal, otherServerHash), "conflict");
});
test("latest-by-key scheduler bounds queued streaming snapshots", async () => {
let releaseFirst!: () => void;
const firstBlocked = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const calls: number[] = [];
const scheduler = new LatestByKeyScheduler<number, number>(async (value) => {
calls.push(value);
if (value === 1) await firstBlocked;
return value;
});
const first = scheduler.schedule("project", 1);
await Promise.resolve();
const superseded = scheduler.schedule("project", 2);
const latest = scheduler.schedule("project", 3);
assert.equal(await superseded, null);
releaseFirst();
assert.equal(await first, 1);
assert.equal(await latest, 3);
assert.deepEqual(calls, [1, 3]);
await scheduler.waitForIdle("project");
});
test("WAL expiry is deterministic at the retention boundary", () => {
const now = Date.now();
const sixDaysAgo = now - 6 * 24 * 60 * 60 * 1000;
const eightDaysAgo = now - 8 * 24 * 60 * 60 * 1000;
assert.equal(isStrategyProjectWalExpired(sixDaysAgo, now), false);
assert.equal(isStrategyProjectWalExpired(eightDaysAgo, now), true);
});
@@ -0,0 +1,362 @@
import type { StrategyProjectSnapshot } from "./projects";
const DATABASE_NAME = "deerflow-strategy-project-wal";
const STORE_NAME = "drafts";
const DATABASE_VERSION = 1;
const WAL_TTL_MS = 7 * 24 * 60 * 60 * 1000;
export interface StrategyProjectWalRecord {
key: string;
userKey: string;
projectId: string;
/** Server snapshot on top of which this local edit was made. */
baseServerHash: string;
snapshotHash: string;
snapshot: StrategyProjectSnapshot;
updatedAt: number;
}
interface PendingJob<Input, Result> {
input: Input;
resolve: (result: Result | null) => void;
reject: (reason: unknown) => void;
}
interface KeyQueueState<Input, Result> {
running: boolean;
pending?: PendingJob<Input, Result>;
idleWaiters: Array<() => void>;
}
/** Keep at most one active and one latest pending operation per key. */
export class LatestByKeyScheduler<Input, Result> {
private readonly states = new Map<string, KeyQueueState<Input, Result>>();
public constructor(
private readonly worker: (input: Input) => Promise<Result>,
) {}
public schedule(key: string, input: Input): Promise<Result | null> {
let state = this.states.get(key);
if (!state) {
state = { running: false, idleWaiters: [] };
this.states.set(key, state);
}
return new Promise<Result | null>((resolve, reject) => {
// The superseded value has not started hashing or IndexedDB work. Resolve
// it now so its growing snapshot closure can be released immediately.
state!.pending?.resolve(null);
state!.pending = { input, resolve, reject };
void this.pump(key, state!);
});
}
public waitForIdle(key: string): Promise<void> {
const state = this.states.get(key);
if (!state || (!state.running && !state.pending)) return Promise.resolve();
return new Promise((resolve) => state.idleWaiters.push(resolve));
}
private async pump(
key: string,
state: KeyQueueState<Input, Result>,
): Promise<void> {
if (state.running) return;
const job = state.pending;
if (!job) {
this.finishKey(key, state);
return;
}
state.pending = undefined;
state.running = true;
try {
job.resolve(await this.worker(job.input));
} catch (reason) {
job.reject(reason);
} finally {
state.running = false;
if (state.pending) {
void this.pump(key, state);
} else {
this.finishKey(key, state);
}
}
}
private finishKey(
key: string,
state: KeyQueueState<Input, Result>,
): void {
this.states.delete(key);
state.idleWaiters.splice(0).forEach((resolve) => resolve());
}
}
export function strategyProjectWalKey(
userKey: string,
projectId: string,
): string {
return `${encodeURIComponent(userKey)}::${encodeURIComponent(projectId)}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isUsableSnapshot(value: unknown): value is StrategyProjectSnapshot {
if (!isRecord(value) || value.version !== 1) return false;
if (value.stage !== "intent" && value.stage !== "workspace") return false;
if (!isRecord(value.taskDraft) || !Array.isArray(value.taskDraft.selectedAngleIds)) {
return false;
}
return (
Array.isArray(value.angleCatalog) &&
Array.isArray(value.turns) &&
isRecord(value.outputs) &&
Array.isArray(value.evaluations) &&
Array.isArray(value.selectedStrategyIds) &&
typeof value.synthesis === "string"
);
}
function fallbackHash(text: string): string {
let first = 0x811c9dc5;
let second = 0x9e3779b9;
for (let index = 0; index < text.length; index += 1) {
const code = text.charCodeAt(index);
first = Math.imul(first ^ code, 0x01000193);
second = Math.imul(second ^ code, 0x85ebca6b);
}
return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0)
.toString(16)
.padStart(8, "0")}`;
}
export async function strategyProjectSnapshotHash(
snapshot: StrategyProjectSnapshot,
): Promise<string> {
const serialized = JSON.stringify(snapshot);
if (globalThis.crypto?.subtle) {
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(serialized),
);
return Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
}
return fallbackHash(serialized);
}
export function isStrategyProjectWalExpired(
updatedAt: number,
now = Date.now(),
): boolean {
return now - updatedAt > WAL_TTL_MS;
}
export function resolveStrategyProjectWal(
wal: Pick<StrategyProjectWalRecord, "baseServerHash" | "snapshotHash">,
serverHash: string,
): "same" | "recover" | "conflict" {
if (wal.snapshotHash === serverHash) return "same";
if (wal.baseServerHash === serverHash) return "recover";
return "conflict";
}
function openDatabase(): Promise<IDBDatabase | null> {
if (typeof indexedDB === "undefined") return Promise.resolve(null);
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains(STORE_NAME)) {
database.createObjectStore(STORE_NAME, { keyPath: "key" });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("无法打开项目恢复存储"));
request.onblocked = () => reject(new Error("项目恢复存储正在升级,请关闭其他页面后重试"));
});
}
function waitForTransaction(transaction: IDBTransaction): Promise<void> {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () =>
reject(transaction.error ?? new Error("项目恢复存储事务失败"));
transaction.onabort = () =>
reject(transaction.error ?? new Error("项目恢复存储事务已中止"));
});
}
function readRequest(request: IDBRequest): Promise<unknown> {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("无法读取项目恢复记录"));
});
}
async function writeWalRecord(
record: StrategyProjectWalRecord,
): Promise<StrategyProjectWalRecord | null> {
const database = await openDatabase();
if (!database) return null;
try {
const transaction = database.transaction(STORE_NAME, "readwrite");
const transactionDone = waitForTransaction(transaction);
const store = transaction.objectStore(STORE_NAME);
// Queue the mutation from inside IDB's success callback. Awaiting the GET
// and only then issuing PUT is racy in Safari because the transaction may
// become inactive between promise turns.
const outcome = new Promise<StrategyProjectWalRecord>((resolve, reject) => {
const request = store.get(record.key);
request.onerror = () =>
reject(request.error ?? new Error("无法读取项目恢复记录"));
request.onsuccess = () => {
try {
const existing = request.result;
if (
!isRecord(existing) ||
typeof existing.updatedAt !== "number" ||
existing.updatedAt <= record.updatedAt
) {
store.put(record);
resolve(record);
return;
}
resolve(existing as unknown as StrategyProjectWalRecord);
} catch (reason) {
reject(reason);
}
};
});
const [result] = await Promise.all([outcome, transactionDone]);
return result;
} finally {
database.close();
}
}
interface WalWriteInput {
userKey: string;
projectId: string;
baseServerHash: string;
snapshot: StrategyProjectSnapshot;
updatedAt: number;
}
const writeScheduler = new LatestByKeyScheduler<
WalWriteInput,
StrategyProjectWalRecord | null
>(async ({ userKey, projectId, baseServerHash, snapshot, updatedAt }) => {
const record: StrategyProjectWalRecord = {
key: strategyProjectWalKey(userKey, projectId),
userKey,
projectId,
baseServerHash,
snapshotHash: await strategyProjectSnapshotHash(snapshot),
snapshot,
updatedAt,
};
return writeWalRecord(record);
});
export function writeStrategyProjectWal(
userKey: string,
projectId: string,
baseServerHash: string,
snapshot: StrategyProjectSnapshot,
): Promise<StrategyProjectWalRecord | null> {
const key = strategyProjectWalKey(userKey, projectId);
return writeScheduler.schedule(key, {
userKey,
projectId,
baseServerHash,
snapshot,
updatedAt: Date.now(),
});
}
export async function readStrategyProjectWal(
userKey: string,
projectId: string,
): Promise<StrategyProjectWalRecord | null> {
const key = strategyProjectWalKey(userKey, projectId);
await writeScheduler.waitForIdle(key);
const database = await openDatabase();
if (!database) return null;
try {
const transaction = database.transaction(STORE_NAME, "readonly");
const transactionDone = waitForTransaction(transaction);
const [record] = await Promise.all([
readRequest(transaction.objectStore(STORE_NAME).get(key)),
transactionDone,
]);
if (!isRecord(record) || !isUsableSnapshot(record.snapshot)) return null;
if (
record.userKey !== userKey ||
record.projectId !== projectId ||
typeof record.baseServerHash !== "string" ||
typeof record.snapshotHash !== "string" ||
typeof record.updatedAt !== "number"
) {
return null;
}
const typed = record as unknown as StrategyProjectWalRecord;
if (isStrategyProjectWalExpired(typed.updatedAt)) {
void clearStrategyProjectWal(userKey, projectId, typed.snapshot).catch(
() => undefined,
);
return null;
}
return typed;
} finally {
database.close();
}
}
export async function clearStrategyProjectWal(
userKey: string,
projectId: string,
expectedSnapshot?: StrategyProjectSnapshot,
): Promise<boolean> {
const key = strategyProjectWalKey(userKey, projectId);
const expectedHash = expectedSnapshot
? await strategyProjectSnapshotHash(expectedSnapshot)
: null;
await writeScheduler.waitForIdle(key);
const database = await openDatabase();
if (!database) return false;
try {
const transaction = database.transaction(STORE_NAME, "readwrite");
const transactionDone = waitForTransaction(transaction);
const store = transaction.objectStore(STORE_NAME);
if (!expectedSnapshot) {
store.delete(key);
await transactionDone;
return true;
}
const deletedPromise = new Promise<boolean>((resolve, reject) => {
const request = store.get(key);
request.onerror = () =>
reject(request.error ?? new Error("无法读取项目恢复记录"));
request.onsuccess = () => {
try {
const existing = request.result;
const matches =
isRecord(existing) && existing.snapshotHash === expectedHash;
if (matches) store.delete(key);
resolve(matches);
} catch (reason) {
reject(reason);
}
};
});
const [deleted] = await Promise.all([deletedPromise, transactionDone]);
return deleted;
} finally {
database.close();
}
}
@@ -0,0 +1,469 @@
import { apiFetch } from "@/core/api/fetch-client";
import { getAccountIdentityKey } from "@/core/auth";
import { getBackendBaseURL } from "@/core/config";
import { createClientId } from "@/lib/client-id";
import {
EMPTY_TASK_DRAFT,
INITIAL_INTENT_TURNS,
STRATEGY_ANGLES,
TAIWAN_STRAIT_DEMO_EVALUATIONS,
TAIWAN_STRAIT_DEMO_OUTPUTS,
TAIWAN_STRAIT_DEMO_SYNTHESIS,
TAIWAN_STRAIT_DEMO_TASK,
TAIWAN_STRAIT_DEMO_TURNS,
} from "./constants";
import type {
AgentOutputState,
IntentTurn,
StrategyAngle,
StrategyEvaluation,
StrategyStage,
StrategyTaskDraft,
} from "./types";
import { clearStrategyProjectWal } from "./project-wal";
export const STRATEGY_PROJECT_ID_PREFIX = "strategy-selection-";
const SNAPSHOT_KIND = "strategy-selection-project";
// A strategy project can be mounted twice briefly during route transitions or
// opened in more than one tab. Serialize saves in this JS realm and, where
// supported, across same-browser tabs so an older PUT cannot finish after and
// overwrite a newer snapshot.
const projectSaveQueues = new Map<string, Promise<void>>();
async function withBrowserProjectLock<T>(
key: string,
operation: () => Promise<T>,
): Promise<T> {
if (typeof navigator !== "undefined" && navigator.locks) {
return navigator.locks.request(`deerflow-strategy-save:${key}`, operation);
}
return operation();
}
function serializeProjectSave<T>(
key: string,
operation: () => Promise<T>,
): Promise<T> {
const previous = projectSaveQueues.get(key) ?? Promise.resolve();
const request = previous
.catch(() => undefined)
.then(() => withBrowserProjectLock(key, operation));
const tail = request.then(
() => undefined,
() => undefined,
);
projectSaveQueues.set(key, tail);
void tail.then(() => {
if (projectSaveQueues.get(key) === tail) projectSaveQueues.delete(key);
});
return request;
}
const DEMO_JUDGE_CONTENT = `### 评判结论
六个角度均与任务目标相关,其中治理统筹、安全风险和外交沟通直接作用于“减少误判与阻断升级”,应优先纳入;法律规则是高压情境下的必要边界。经济韧性和社会民生策略建议与主策略合并实施,避免形成两套割裂的连续性机制。
**组合提醒:** 对外沟通节奏、市场稳定措施和风险等级调整必须使用同一事实基线;任何高风险政策动作均需设置冷静期、法律复核与退出条件。`;
export interface StrategyProjectSnapshot {
version: 1;
/** Copy of the angle catalog used by this project, for historical rendering. */
angleCatalog: StrategyAngle[];
stage: StrategyStage;
taskDraft: StrategyTaskDraft;
turns: IntentTurn[];
outputs: Record<string, AgentOutputState>;
judgeStatus: AgentOutputState["status"];
judgeContent: string;
evaluations: StrategyEvaluation[];
selectedStrategyIds: string[];
synthesis: string;
synthesisStatus: AgentOutputState["status"];
solutionSavedAt: string | null;
isDemo: boolean;
workflowStale: boolean;
}
export interface StrategyProjectSummary {
id: string;
title: string;
brief: string;
createdAt: string | null;
updatedAt: string | null;
stage: StrategyStage;
questionCount: number;
angleCount: number;
completedStrategyCount: number;
hasEvaluation: boolean;
hasSolution: boolean;
isDemo: boolean;
workflowStale: boolean;
}
export interface StrategyProject extends StrategyProjectSummary {
snapshot: StrategyProjectSnapshot;
}
export class StrategyProjectConflictError extends Error {
public constructor(message: string) {
super(message);
this.name = "StrategyProjectConflictError";
}
}
interface ParallelTaskResponse {
id: string;
title?: string;
brief?: string | null;
agents?: Array<{ agent_id: string; name?: string; avatarType?: string | null }>;
messages?: Array<Record<string, unknown>> | null;
created_at?: string | null;
updated_at?: string | null;
}
interface ParallelTaskListResponse {
tasks?: ParallelTaskResponse[];
}
interface SnapshotEnvelope extends Record<string, unknown> {
kind: typeof SNAPSHOT_KIND;
version: 1;
snapshot: StrategyProjectSnapshot;
}
const cloneTask = (task: StrategyTaskDraft): StrategyTaskDraft => ({
...task,
selectedAngleIds: [...task.selectedAngleIds],
});
const cloneAngles = (angles: StrategyAngle[]): StrategyAngle[] =>
angles.map((angle) => ({ ...angle }));
const cloneTurns = (turns: IntentTurn[]): IntentTurn[] =>
turns.map((turn) => ({ ...turn, streaming: false }));
const cloneOutputs = (
outputs: Record<string, AgentOutputState>,
): Record<string, AgentOutputState> =>
Object.fromEntries(
Object.entries(outputs).map(([id, output]) => [
id,
{ ...output, steps: [...(output.steps ?? [])] },
]),
);
const cloneEvaluations = (evaluations: StrategyEvaluation[]) =>
evaluations.map((item) => ({
...item,
strengths: [...item.strengths],
risks: [...item.risks],
}));
export function createEmptyStrategySnapshot(
angleCatalog: StrategyAngle[] = STRATEGY_ANGLES,
): StrategyProjectSnapshot {
const enabledAngles = angleCatalog.filter((angle) => angle.enabled !== false);
return {
version: 1,
angleCatalog: cloneAngles(enabledAngles),
stage: "intent",
taskDraft: {
...cloneTask(EMPTY_TASK_DRAFT),
selectedAngleIds: enabledAngles.slice(0, 4).map((angle) => angle.id),
},
turns: cloneTurns(INITIAL_INTENT_TURNS),
outputs: {},
judgeStatus: "idle",
judgeContent: "",
evaluations: [],
selectedStrategyIds: [],
synthesis: "",
synthesisStatus: "idle",
solutionSavedAt: null,
isDemo: false,
workflowStale: false,
};
}
export function canCreateTaiwanDemo(
angleCatalog: StrategyAngle[],
): boolean {
const enabledAgentIds = new Set(
angleCatalog
.filter((angle) => angle.enabled !== false)
.map((angle) => angle.agentId),
);
return STRATEGY_ANGLES.every((angle) => enabledAgentIds.has(angle.agentId));
}
export function createDemoStrategySnapshot(
angleCatalog: StrategyAngle[] = STRATEGY_ANGLES,
): StrategyProjectSnapshot {
if (!canCreateTaiwanDemo(angleCatalog)) {
throw new Error("台海示例所需的六个默认角度未全部启用,请先由管理员恢复配置");
}
const enabledAgentIds = new Set(
angleCatalog
.filter((angle) => angle.enabled !== false)
.map((angle) => angle.agentId),
);
const defaultDemoAgentIds = new Set(
STRATEGY_ANGLES.map((angle) => angle.agentId),
);
const demoAngles = angleCatalog.filter(
(angle) =>
angle.enabled !== false &&
enabledAgentIds.has(angle.agentId) &&
defaultDemoAgentIds.has(angle.agentId),
);
const effectiveAngles = demoAngles;
const demoOutputs = Object.fromEntries(
Object.entries(cloneOutputs(TAIWAN_STRAIT_DEMO_OUTPUTS)).filter(([agentId]) =>
effectiveAngles.some((angle) => angle.agentId === agentId),
),
);
const demoEvaluations = TAIWAN_STRAIT_DEMO_EVALUATIONS.filter((evaluation) =>
effectiveAngles.some((angle) => angle.agentId === evaluation.agentId),
);
return {
version: 1,
angleCatalog: cloneAngles(effectiveAngles),
stage: "workspace",
taskDraft: {
...cloneTask(TAIWAN_STRAIT_DEMO_TASK),
selectedAngleIds: effectiveAngles.map((angle) => angle.id),
},
turns: cloneTurns(TAIWAN_STRAIT_DEMO_TURNS),
outputs: demoOutputs,
judgeStatus: "done",
judgeContent: DEMO_JUDGE_CONTENT,
evaluations: cloneEvaluations(demoEvaluations),
selectedStrategyIds: demoEvaluations.filter(
(item) => item.score >= 88,
).map((item) => item.agentId),
synthesis: TAIWAN_STRAIT_DEMO_SYNTHESIS,
synthesisStatus: "done",
solutionSavedAt: null,
isDemo: true,
workflowStale: false,
};
}
function isObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function readSnapshot(task: ParallelTaskResponse): StrategyProjectSnapshot | null {
const envelope = task.messages?.find(
(message) => message.kind === SNAPSHOT_KIND && message.version === 1,
);
if (!isObject(envelope) || !isObject(envelope.snapshot)) return null;
const snapshot = envelope.snapshot as unknown as StrategyProjectSnapshot;
if (
snapshot.version !== 1 ||
(snapshot.stage !== "intent" && snapshot.stage !== "workspace") ||
!isObject(snapshot.taskDraft) ||
!Array.isArray(snapshot.taskDraft.selectedAngleIds) ||
!Array.isArray(snapshot.turns) ||
!isObject(snapshot.outputs) ||
!Array.isArray(snapshot.evaluations) ||
!Array.isArray(snapshot.selectedStrategyIds)
) {
return null;
}
return {
...snapshot,
angleCatalog: cloneAngles(
Array.isArray(snapshot.angleCatalog) && snapshot.angleCatalog.length
? snapshot.angleCatalog.filter(
(angle) =>
isObject(angle) &&
typeof angle.id === "string" &&
typeof angle.agentId === "string" &&
typeof angle.name === "string",
)
: STRATEGY_ANGLES,
),
workflowStale: snapshot.workflowStale === true,
solutionSavedAt:
typeof snapshot.solutionSavedAt === "string"
? snapshot.solutionSavedAt
: null,
taskDraft: cloneTask(snapshot.taskDraft),
turns: cloneTurns(snapshot.turns),
outputs: cloneOutputs(snapshot.outputs),
evaluations: cloneEvaluations(snapshot.evaluations),
selectedStrategyIds: [...snapshot.selectedStrategyIds],
};
}
function toProject(task: ParallelTaskResponse): StrategyProject | null {
const snapshot = readSnapshot(task);
if (!snapshot) return null;
const completedStrategyCount = Object.values(snapshot.outputs).filter(
(output) => output.status === "done" && output.content.trim(),
).length;
return {
id: task.id,
title: snapshot.taskDraft.name.trim() || task.title?.trim() || "未命名策略项目",
brief: snapshot.taskDraft.detail.trim() || task.brief?.trim() || "尚未补充项目说明",
createdAt: task.created_at ?? null,
updatedAt: task.updated_at ?? null,
stage: snapshot.stage,
questionCount: snapshot.turns.filter((turn) => turn.role === "user").length,
angleCount: snapshot.taskDraft.selectedAngleIds.length,
completedStrategyCount,
hasEvaluation:
!snapshot.workflowStale &&
snapshot.judgeStatus === "done" &&
Boolean(snapshot.judgeContent.trim()),
hasSolution:
!snapshot.workflowStale &&
snapshot.synthesisStatus === "done" &&
Boolean(snapshot.synthesis.trim()),
isDemo: snapshot.isDemo,
workflowStale: snapshot.workflowStale,
snapshot,
};
}
async function readError(response: Response): Promise<string> {
const body = (await response.json().catch(() => null)) as
| { detail?: string }
| null;
return body?.detail || `请求失败(HTTP ${response.status})`;
}
const endpoint = (suffix = "") =>
`${getBackendBaseURL()}/api/parallel-agent-tasks${suffix}`;
async function getParallelTask(id: string): Promise<ParallelTaskResponse> {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`));
if (!response.ok) throw new Error(await readError(response));
return (await response.json()) as ParallelTaskResponse;
}
export async function listStrategyProjects(): Promise<StrategyProjectSummary[]> {
const response = await apiFetch(endpoint());
if (!response.ok) throw new Error(await readError(response));
const body = (await response.json()) as ParallelTaskListResponse;
const candidates = (body.tasks ?? []).filter((task) =>
task.id.startsWith(STRATEGY_PROJECT_ID_PREFIX),
);
const records = await Promise.allSettled(
candidates.map((task) => getParallelTask(task.id)),
);
return records
.flatMap((result) => {
if (result.status !== "fulfilled") return [];
const project = toProject(result.value);
return project ? [project] : [];
})
.sort((a, b) =>
String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")),
);
}
export async function getStrategyProject(id: string): Promise<StrategyProject> {
if (!id.startsWith(STRATEGY_PROJECT_ID_PREFIX)) {
throw new Error("这不是一个策略优选项目");
}
const task = await getParallelTask(id);
const project = toProject(task);
if (!project) throw new Error("项目数据不完整,无法恢复策略工作台");
return project;
}
function projectAgents(snapshot: StrategyProjectSnapshot) {
const selected = new Set(snapshot.taskDraft.selectedAngleIds);
const angleAgents = snapshot.angleCatalog.filter((angle) => selected.has(angle.id)).map(
(angle) => ({ agent_id: angle.agentId, name: angle.name }),
);
return [
{ agent_id: "strategy-selection-intent", name: "意图理解智能体" },
...angleAgents,
{ agent_id: "strategy-selection-judge", name: "策略评判智能体" },
{ agent_id: "strategy-selection-solution", name: "方案整理智能体" },
];
}
function snapshotEnvelope(snapshot: StrategyProjectSnapshot): SnapshotEnvelope {
return {
kind: SNAPSHOT_KIND,
version: 1,
snapshot,
};
}
export async function createStrategyProject(
snapshot: StrategyProjectSnapshot = createEmptyStrategySnapshot(),
): Promise<StrategyProject> {
const id = `${STRATEGY_PROJECT_ID_PREFIX}${createClientId()}`;
const response = await apiFetch(endpoint(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id,
title: snapshot.taskDraft.name.trim() || "未命名策略项目",
brief: snapshot.taskDraft.detail.trim() || null,
agents: projectAgents(snapshot),
messages: [snapshotEnvelope(snapshot)],
}),
});
if (!response.ok) throw new Error(await readError(response));
const project = toProject((await response.json()) as ParallelTaskResponse);
if (!project) throw new Error("项目已创建,但返回数据无法识别");
return project;
}
export async function saveStrategyProject(
id: string,
snapshot: StrategyProjectSnapshot,
options: {
expectedUpdatedAt?: string | null;
keepalive?: boolean;
signal?: AbortSignal;
} = {},
): Promise<StrategyProject> {
const account = getAccountIdentityKey() ?? "anonymous";
return serializeProjectSave(`${account}:${id}`, async () => {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), {
method: "PUT",
headers: { "Content-Type": "application/json" },
keepalive: options.keepalive,
signal: options.signal,
body: JSON.stringify({
title: snapshot.taskDraft.name.trim() || "未命名策略项目",
brief: snapshot.taskDraft.detail.trim() || null,
agents: projectAgents(snapshot),
messages: [snapshotEnvelope(snapshot)],
expected_updated_at: options.expectedUpdatedAt ?? undefined,
}),
});
if (!response.ok) {
const message = await readError(response);
if (response.status === 409) {
throw new StrategyProjectConflictError(message);
}
throw new Error(message);
}
const project = toProject((await response.json()) as ParallelTaskResponse);
if (!project) throw new Error("项目已保存,但返回数据无法识别");
return project;
});
}
export async function deleteStrategyProject(id: string): Promise<void> {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), {
method: "DELETE",
});
if (!response.ok && response.status !== 404) {
throw new Error(await readError(response));
}
void clearStrategyProjectWal(
getAccountIdentityKey() ?? "anonymous",
id,
).catch(() => undefined);
}
@@ -0,0 +1,112 @@
import type { StrategyAngle } from "./types";
export type StrategyAgentCategory =
| "intent"
| "angle"
| "judge"
| "solution";
export interface StrategyAgent {
id: string;
category: StrategyAgentCategory;
fixed: boolean;
enabled: boolean;
sort_order: number;
name: string;
description: string;
model: string | null;
soul: string | null;
skills: string[] | null;
created_at: string | null;
updated_at: string | null;
}
const STRATEGY_ANGLE_PREFIX = "strategy-selection-angle-";
const LEGACY_ANGLE_IDS: Record<string, string> = {
"strategy-selection-angle-governance": "governance",
"strategy-selection-angle-diplomacy": "diplomacy",
"strategy-selection-angle-economy": "economy",
"strategy-selection-angle-security": "security",
"strategy-selection-angle-public-opinion": "public_opinion",
"strategy-selection-angle-law-policy": "law_policy",
};
const LEGACY_SHORT_NAMES: Record<string, string> = {
"strategy-selection-angle-governance": "治理",
"strategy-selection-angle-diplomacy": "外交",
"strategy-selection-angle-economy": "经济",
"strategy-selection-angle-security": "安全",
"strategy-selection-angle-public-opinion": "民生",
"strategy-selection-angle-law-policy": "法律",
};
const LEGACY_ACCENTS: Record<string, string> = {
"strategy-selection-angle-governance": "#6d5dfc",
"strategy-selection-angle-diplomacy": "#0ea5e9",
"strategy-selection-angle-economy": "#10b981",
"strategy-selection-angle-security": "#f97316",
"strategy-selection-angle-public-opinion": "#ec4899",
"strategy-selection-angle-law-policy": "#a855f7",
};
const ACCENT_PALETTE = [
"#2563eb",
"#0891b2",
"#059669",
"#ca8a04",
"#ea580c",
"#dc2626",
"#db2777",
"#7c3aed",
] as const;
function stableAccent(id: string) {
if (LEGACY_ACCENTS[id]) return LEGACY_ACCENTS[id];
let hash = 2166136261;
for (let index = 0; index < id.length; index += 1) {
hash ^= id.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return ACCENT_PALETTE[(hash >>> 0) % ACCENT_PALETTE.length];
}
function displayAngleName(name: string) {
const cleaned = name
.trim()
.replace(/^策略(?:分析)?视角[·•::\s-]*/, "")
.replace(/(?:分析)?智能体$/, "")
.trim();
return cleaned || name.trim() || "未命名视角";
}
/** Map the public API shape to the workbench's stable angle representation. */
export function strategyAgentToAngle(
agent: StrategyAgent,
): StrategyAngle | null {
if (agent.category !== "angle") return null;
const suffix = agent.id.startsWith(STRATEGY_ANGLE_PREFIX)
? agent.id.slice(STRATEGY_ANGLE_PREFIX.length)
: agent.id;
const name = displayAngleName(agent.name);
return {
id: LEGACY_ANGLE_IDS[agent.id] ?? suffix,
agentId: agent.id,
name,
shortName:
LEGACY_SHORT_NAMES[agent.id] ??
name.split(/[与和及、·/]/u, 1)[0].slice(0, 6),
description: agent.description,
accent: stableAccent(agent.id),
enabled: agent.enabled,
};
}
export function strategyAgentsToAngles(
agents: StrategyAgent[],
): StrategyAngle[] {
return agents.flatMap((agent) => {
const angle = strategyAgentToAngle(agent);
return angle ? [angle] : [];
});
}
@@ -0,0 +1,66 @@
/**
* Run from frontend-web:
* npx esbuild src/strategy-selection/strategy-agents.test.ts --bundle --format=esm --platform=node --outfile=.tmp-tests/strategy-agents.test.mjs
* node --test .tmp-tests/strategy-agents.test.mjs
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
strategyAgentToAngle,
strategyAgentsToAngles,
type StrategyAgent,
} from "./strategy-agent-mapping";
function agent(overrides: Partial<StrategyAgent> = {}): StrategyAgent {
return {
id: "strategy-selection-angle-public-opinion",
category: "angle",
fixed: false,
enabled: true,
sort_order: 100,
name: "策略视角·社会民生",
description: "关注公共服务连续性",
model: null,
soul: null,
skills: null,
created_at: null,
updated_at: null,
...overrides,
};
}
test("keeps legacy project ids while accepting the public redacted shape", () => {
assert.deepEqual(strategyAgentToAngle(agent()), {
id: "public_opinion",
agentId: "strategy-selection-angle-public-opinion",
name: "社会民生",
shortName: "民生",
description: "关注公共服务连续性",
accent: "#ec4899",
enabled: true,
});
});
test("derives stable dynamic ids and accents from the reserved prefix", () => {
const dynamic = agent({
id: "strategy-selection-angle-abc123",
name: "科技与产业链智能体",
});
const first = strategyAgentToAngle(dynamic);
const second = strategyAgentToAngle({ ...dynamic });
assert.equal(first?.id, "abc123");
assert.equal(first?.shortName, "科技");
assert.equal(first?.accent, second?.accent);
});
test("filters fixed workflow roles out of the angle catalog", () => {
assert.equal(
strategyAgentsToAngles([
agent(),
agent({ id: "strategy-selection-intent", category: "intent" }),
]).length,
1,
);
});
@@ -0,0 +1,128 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
import {
strategyAgentsToAngles,
type StrategyAgent,
} from "./strategy-agent-mapping";
export {
strategyAgentToAngle,
strategyAgentsToAngles,
} from "./strategy-agent-mapping";
export type {
StrategyAgent,
StrategyAgentCategory,
} from "./strategy-agent-mapping";
export interface CreateStrategyAngleInput {
name: string;
description?: string;
soul?: string;
model?: string | null;
skills?: string[];
sort_order?: number;
}
export interface UpdateStrategyAgentInput {
name?: string;
description?: string;
soul?: string;
model?: string | null;
skills?: string[];
enabled?: boolean;
sort_order?: number;
}
export const STRATEGY_AGENTS_QUERY_KEY = ["strategy-agents"] as const;
function endpoint(suffix = "") {
return `${getBackendBaseURL()}/api/strategy-agents${suffix}`;
}
async function responseError(response: Response) {
const body = (await response.json().catch(() => null)) as
| { detail?: string }
| null;
return body?.detail || `请求失败(HTTP ${response.status})`;
}
export async function listStrategyAgents(options?: {
includeDisabled?: boolean;
includeConfig?: boolean;
}): Promise<StrategyAgent[]> {
const query = new URLSearchParams();
if (options?.includeDisabled) query.set("include_disabled", "true");
if (options?.includeConfig) query.set("include_config", "true");
const response = await apiFetch(
`${endpoint()}${query.size ? `?${query.toString()}` : ""}`,
);
if (!response.ok) throw new Error(await responseError(response));
const body = (await response.json()) as { agents?: StrategyAgent[] };
return body.agents ?? [];
}
export async function createStrategyAngle(
input: CreateStrategyAngleInput,
): Promise<StrategyAgent> {
const response = await apiFetch(endpoint("/angles"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error(await responseError(response));
return (await response.json()) as StrategyAgent;
}
export async function updateStrategyAgent(
id: string,
input: UpdateStrategyAgentInput,
): Promise<StrategyAgent> {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error(await responseError(response));
return (await response.json()) as StrategyAgent;
}
export async function disableStrategyAngle(id: string): Promise<StrategyAgent> {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), {
method: "DELETE",
});
if (!response.ok) throw new Error(await responseError(response));
return (await response.json()) as StrategyAgent;
}
export function useStrategyAgents(options?: {
enabled?: boolean;
includeDisabled?: boolean;
includeConfig?: boolean;
}) {
const includeDisabled = options?.includeDisabled ?? false;
const includeConfig = options?.includeConfig ?? false;
return useQuery({
queryKey: [
...STRATEGY_AGENTS_QUERY_KEY,
includeDisabled,
includeConfig,
],
queryFn: () => listStrategyAgents({ includeDisabled, includeConfig }),
enabled: options?.enabled ?? true,
staleTime: 30_000,
});
}
/** Reusable active-angle query for the workbench and project creation flow. */
export function useStrategyAngleCatalog(enabled = true) {
const query = useStrategyAgents({ enabled });
const angles = useMemo(
() => strategyAgentsToAngles(query.data ?? []),
[query.data],
);
return { ...query, angles };
}
@@ -0,0 +1,128 @@
export type StrategyStage = "intent" | "workspace";
export type AgentRunStatus =
| "idle"
| "queued"
| "running"
| "done"
| "error";
export interface StrategyAngle {
id: string;
agentId: string;
name: string;
shortName: string;
description: string;
accent: string;
/** False when an angle survives only in an existing project snapshot. */
enabled?: boolean;
}
export interface StrategyTaskDraft {
name: string;
detail: string;
objective: string;
prerequisites: string;
constraints: string;
deliverables: string;
acceptanceCriteria: string;
horizon: string;
selectedAngleIds: string[];
}
export interface IntentTurn {
id: string;
role: "user" | "assistant";
content: string;
streaming?: boolean;
isDemo?: boolean;
clarification?: IntentClarification;
clarificationAnswered?: boolean;
clarificationAnswer?: string;
reasoning?: string;
error?: string;
retryPrompt?: string;
retryClarificationTurnId?: string;
}
export interface IntentClarificationOption {
id: string;
label: string;
description?: string;
}
export interface IntentClarification {
question: string;
context?: string;
clarificationType?: string;
options: IntentClarificationOption[];
allowCustom: boolean;
allowMultiple: boolean;
}
export interface AgentOutputState {
status: AgentRunStatus;
content: string;
steps: string[];
reasoning?: string;
error?: string;
/** A failed refresh kept the last successful content for this same task. */
preservedFromPrevious?: boolean;
isDemo?: boolean;
}
export interface StrategyEvaluation {
agentId: string;
score: number;
conclusion: "优先推荐" | "建议采用" | "保留备选";
comment: string;
strengths: string[];
risks: string[];
}
export interface ParallelAgentPayload {
task_name: string;
brief: string;
agent_ids: string[];
model?: string;
}
export type ParallelAgentEvent =
| { type: "agent_start"; agent_id: string }
| { type: "delta"; agent_id: string; text: string }
| {
type: "step";
agent_id: string;
key?: string;
label?: string;
args?: Record<string, unknown>;
}
| { type: "agent_done"; agent_id: string; content: string }
| { type: "agent_error"; agent_id: string; message: string }
| {
type: "agent_clarification";
agent_id: string;
content?: string;
question?: string;
clarification_type?: string;
clarification_context?: string;
context?: string;
options?: Array<
| string
| {
id?: string;
label?: string;
description?: string;
}
>;
allow_custom?: boolean;
allow_multiple?: boolean;
}
| {
type: "thinking" | "reasoning" | "reasoning_delta";
agent_id: string;
text?: string;
delta?: string;
content?: string;
}
| { type: "all_done" };
Binary file not shown.
Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
# ========================
# EDIT BEFORE DEPLOYMENT
# ========================
# This file is loaded by start_backend.scheduler.sh through docker --env-file.
# Put server-specific values here. Do not configure Python mirrors only on the
# host shell; values in this file are what the container will receive.
# AI provider keys. Replace placeholders and rotate any test key before release.
DEEPSEEK_API_KEY=sk-9808fb2375534112ac275541aec14b9b
OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
TAVILY_API_KEY=your-tavily-api-key
SERPER_API_KEY=your-serper-api-key
# Auth and frontend access.
# AUTH_JWT_SECRET must stay stable across restarts, or existing logins expire.
# GATEWAY_CORS_ORIGINS must include the exact frontend origins, separated by commas.
AUTH_JWT_SECRET=i3_BiBK44sYHiFIkChhDs46y57ZewJKO3diQDw01anI
GATEWAY_CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174,http://localhost:5183,http://127.0.0.1:5183,http://localhost:4200,http://47.88.25.99:7010
GATEWAY_ENABLE_DOCS=false
GATEWAY_WORKERS=2
AUTH_TRUSTED_PROXIES=
# Scheduler dependency behavior.
# Keep SCHEDULER_INSTALL_DEPS=1 if croniter is not already baked into the image.
# Set the Python mirror below if the server cannot access public PyPI.
SCHEDULER_INSTALL_DEPS=1
SCHEDULER_PIP_PACKAGES=croniter==6.2.2
SCHEDULED_TASKS_ENABLED=true
# Element / Matrix delivery for scheduled task results.
# If disabled or not configured, scheduled tasks still run but no Element DM is sent.
ELEMENT_ENABLED=false
ELEMENT_HOMESERVER_URL=
ELEMENT_ACCESS_TOKEN=
ELEMENT_BOT_USERNAME=scheduled-bot
ELEMENT_BOT_PASSWORD=
ELEMENT_AUTO_REGISTER=false
ELEMENT_STATE_FILE=/app/backend/.deer-flow/element-bot-token
# Python private package index used by uv/pip inside the backend container and
# propagated into sandbox containers for agent-installed dependencies.
# Uncomment and set these to your internal mirror when public PyPI is blocked.
# UV_INDEX_URL=http://your-python-mirror/simple
# PIP_INDEX_URL=http://your-python-mirror/simple
# UV_TRUSTED_HOST=your-python-mirror
# PIP_TRUSTED_HOST=your-python-mirror
# PIP_EXTRA_INDEX_URL=
# UV_EXTRA_INDEX_URL=
# HTTP_PROXY=
# HTTPS_PROXY=
# NO_PROXY=
# Alternative generic names also supported by the startup script.
# PYTHON_PACKAGE_INDEX_URL=http://your-python-mirror/simple
# PYTHON_EXTRA_INDEX_URL=
# PYTHON_TRUSTED_HOST=your-python-mirror
MYSQL_DATABASE_URL=mysql+aiomysql://deerflow:deerflow123@192.168.0.123:3306/deerflow?charset=utf8mb4
DEER_FLOW_NOTEBOOK_API_BASE=http://127.0.0.1:8003
# 圆桌后台作业网关:缺省即 inprocess(进程内驱动真模型,后台挂起开箱即用);
# 仅前端联调时显式设 simulated(真实进度但不调真模型)。本行可删,留作文档。
ROUNDTABLE_JOB_GATEWAY=inprocess
DEEPSEEK_CHAT_API_KEY=sk-686b7bc000be4b7cbed6dccb3897c411
GLM5_FP8_API_KEY=modalresearch_gWu46FDpVLYZWPVEA3b_2fq8Jw2UJiKMgEm_Rxmn1SA
SILICONFLOW_API_KEY=sk-ighylafootabtuthwueirmoycccyqtdgbifyhozduvolkdyt
# WeKnora admin credentials (DeerFlow server-side iframe SSO proxy)
WEKNORA_ADMIN_PASSWORD=1qaz@WSX
@@ -0,0 +1,29 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
.coverage
.coverage.*
.ruff_cache
agent_history.gif
static/browser_history/*.gif
log/
log/*
.deer-flow/users/
# Virtual environments
.venv
venv/
# User config file
# config.yaml
# Langgraph
.langgraph_api
# Claude Code settings
.claude/settings.local.json
+53 -5
View File
@@ -299,6 +299,8 @@ from deerflow.config import get_app_config
**Forced-research built-in agent.** `forced-research-responder` (「强制检索输出助手」) is seeded from `app/gateway/routers/_forced_research_seed_assets/` before `_sync_legacy_agents`. `ForcedResearchMiddleware` is mounted only for this stable id. On every visible user turn it requires a successful information-gathering tool result before the model may finish; `tool_choice=required` is backed by an `after_model -> model` retry for gateways that ignore tool choice, with an eight-attempt failure cap that permits only an honest failure response. Skill discovery, reading `SKILL.md`, and writing a helper script do not satisfy the gate; web/knowledge/MCP results, non-skill uploaded-file reads, or successful Python skill-script execution do. Tool filtering removes `present_files`, `skill_manage`, task/schedule/memory/clarification surfaces. The middleware permits `write_file`/`str_replace` only for `/mnt/user-data/workspace/*.py` and permits `bash` only for `.py` execution without redirection/document paths, so skills can run scripts but the agent cannot create Markdown/document artifacts or write `outputs`. Tests: `tests/test_forced_research_agent.py`. **Forced-research built-in agent.** `forced-research-responder` (「强制检索输出助手」) is seeded from `app/gateway/routers/_forced_research_seed_assets/` before `_sync_legacy_agents`. `ForcedResearchMiddleware` is mounted only for this stable id. On every visible user turn it requires a successful information-gathering tool result before the model may finish; `tool_choice=required` is backed by an `after_model -> model` retry for gateways that ignore tool choice, with an eight-attempt failure cap that permits only an honest failure response. Skill discovery, reading `SKILL.md`, and writing a helper script do not satisfy the gate; web/knowledge/MCP results, non-skill uploaded-file reads, or successful Python skill-script execution do. Tool filtering removes `present_files`, `skill_manage`, task/schedule/memory/clarification surfaces. The middleware permits `write_file`/`str_replace` only for `/mnt/user-data/workspace/*.py` and permits `bash` only for `.py` execution without redirection/document paths, so skills can run scripts but the agent cannot create Markdown/document artifacts or write `outputs`. Tests: `tests/test_forced_research_agent.py`.
**Strategy-selection functional agents.** `app/gateway/routers/_strategy_selection_seed.py` seeds nine stable folder-backed agents from `_strategy_selection_seed_assets/` before `_sync_legacy_agents`: intent, governance/diplomacy/economy/security/public-opinion/law-policy angles, judge, and solution editor. Seeding is missing-directory-only and must never overwrite administrator changes. The global `strategy_agent_roles` registry classifies them independently of projects: intent/judge/solution are fixed, unique roles that may be configured but never added, disabled, reordered, or deleted; angle roles are a shared, ordered catalog that administrators may add, edit, disable (soft delete), restore, and reorder through `/api/strategy-agents`. Public `GET /api/strategy-agents` returns enabled identity/description metadata only; `include_disabled=true` or `include_config=true` is admin-only. Admin writes are `POST /api/strategy-agents/angles`, `PUT /api/strategy-agents/{id}`, and `DELETE /api/strategy-agents/{id}`; DELETE retains the folder-backed agent and role row to preserve historical references. Dynamic angles remain built-in folder-backed agents under the reserved `strategy-selection-angle-` id namespace, with `web` + `file:read`; a blank SOUL on create receives a safe role template. All strategy skill writes validate that names exist, are active, and are globally visible; generic agent CRUD cannot create or hard-delete reserved strategy IDs, and parallel execution rejects disabled roles. Angle configs reuse `policy-knowledge`; intent reuses `intent-clarification-policy`; the judge has no retrieval skill; the solution editor gets only `file:write`. Keep the SOUL contracts neutral and evidence-aware, with explicit de-escalation/civilian-protection framing and hard bans on operational combat guidance and targeted political manipulation. Tests: `tests/test_strategy_selection_seed.py`, `tests/test_strategy_agent_management.py`.
**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): **ThreadState** (`packages/harness/deerflow/agents/thread_state.py`):
- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` - Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images`
- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear)
@@ -540,6 +542,8 @@ Self-contained product knowledge base. **All knowledge capability lives in this
Custom agent metadata lives in the `agents` ORM table (`deerflow.persistence.agents`). `agents.id` is the stable runtime id and the filesystem directory name under `.deer-flow/agents/{id}/`; `agents.name` is display-only, can contain Chinese characters, and is not unique. `user_id IS NULL` marks built-in agents, user-owned agents are visible only to their owner unless `published = true`, and update/delete operations require ownership. The runtime accepts `agent_id` in context/configurable, and non-default `assistant_id` values are treated as agent ids after visibility checks. Custom agent metadata lives in the `agents` ORM table (`deerflow.persistence.agents`). `agents.id` is the stable runtime id and the filesystem directory name under `.deer-flow/agents/{id}/`; `agents.name` is display-only, can contain Chinese characters, and is not unique. `user_id IS NULL` marks built-in agents, user-owned agents are visible only to their owner unless `published = true`, and update/delete operations require ownership. The runtime accepts `agent_id` in context/configurable, and non-default `assistant_id` values are treated as agent ids after visibility checks.
Strategy-selection fan-out (`routers/parallel_agents.py`) must keep hidden reasoning separate per agent: provider reasoning blocks/fields and inline `<think>...</think>` text emit `reasoning_delta`, while visible answer text emits `delta` and only visible text may enter `agent_done.content`. Never concatenate `reasoning_delta` into a selectable strategy or generated deliverable. Inspect the final AI message with the shared `is_llm_error_message` detector and require substantive visible text before constructing a success result. A middleware fallback, empty/thinking-only response, or punctuation-only provider degeneration is a failed seat run: raise so the stream emits `agent_error`; never wrap it in `agent_done`. Regression coverage: `tests/test_roundtable_model_fallback.py`.
**Listing cache + pagination** ? to keep `GET /api/agents` fast, the gallery's per-card extras (skills + tool_groups) are denormalized into two cache tables instead of reading every agent's `config.yaml` on each request: `agent_skills` (relational, one row per (agent, skill) with `position`) and `agent_extras` (one row per agent: `tool_groups` JSON + `model` + `skills_synced_at`). `config.yaml` remains authoritative for the agent runtime; these tables are a read cache, write-through on create/update and backfilled from disk by `_sync_legacy_agents` (mtime-vs-`skills_synced_at` drift check). The list endpoint enriches via `AgentStore.get_extras_for([ids])` (two batched `IN` queries). Both tables are auto-created by `Base.metadata.create_all` (no migration needed). `GET /api/agents` supports **opt-in server-side pagination**: `scope` (`mine` = owned + favorited / `square` = built-in/published public), `page`, `page_size`, returning `{agents, total, page, page_size}`; omitting `scope`/`page` preserves the legacy "return everything" behavior. Name `search` matches the raw OR desensitized name (the frontend's static term?abbrev map is mirrored server-side in `deerflow/config/agent_name_desensitize.py`); `tag_ids` is an SQL OR-filter; `owner` keeps only agents published by that user id (the card's ??? ? built-in agents never match), powering the gallery's click-???-to-filter. `POST /api/agents/private-square/list` takes the same pagination params (incl. `owner`). Repository entry point: `AgentStore.list_paginated(...)`. Tests: `tests/test_agent_listing_cache.py::test_list_paginated_owner_filter_keeps_one_publisher`. **Listing cache + pagination** ? to keep `GET /api/agents` fast, the gallery's per-card extras (skills + tool_groups) are denormalized into two cache tables instead of reading every agent's `config.yaml` on each request: `agent_skills` (relational, one row per (agent, skill) with `position`) and `agent_extras` (one row per agent: `tool_groups` JSON + `model` + `skills_synced_at`). `config.yaml` remains authoritative for the agent runtime; these tables are a read cache, write-through on create/update and backfilled from disk by `_sync_legacy_agents` (mtime-vs-`skills_synced_at` drift check). The list endpoint enriches via `AgentStore.get_extras_for([ids])` (two batched `IN` queries). Both tables are auto-created by `Base.metadata.create_all` (no migration needed). `GET /api/agents` supports **opt-in server-side pagination**: `scope` (`mine` = owned + favorited / `square` = built-in/published public), `page`, `page_size`, returning `{agents, total, page, page_size}`; omitting `scope`/`page` preserves the legacy "return everything" behavior. Name `search` matches the raw OR desensitized name (the frontend's static term?abbrev map is mirrored server-side in `deerflow/config/agent_name_desensitize.py`); `tag_ids` is an SQL OR-filter; `owner` keeps only agents published by that user id (the card's ??? ? built-in agents never match), powering the gallery's click-???-to-filter. `POST /api/agents/private-square/list` takes the same pagination params (incl. `owner`). Repository entry point: `AgentStore.list_paginated(...)`. Tests: `tests/test_agent_listing_cache.py::test_list_paginated_owner_filter_keeps_one_publisher`.
**Homepage selector (agent picker on the new-chat page)** ? single admin-curated list backed by `agents.featured_order` (nullable BIGINT). NULL = not featured. Only `published = true` agents can be featured (router enforces on writes); any published agent is eligible regardless of ownership. Cleared with `order: null`. The list rendered to end users is `WHERE featured_order IS NOT NULL` + visibility-filter, sorted by `featured_order ASC`. Admins reorder by sending the desired ids to `PUT /api/agents/featured/order` (assigns indices 0..N-1). BIGINT is used rather than INT so the management UI can seed initial values from `Date.now()` (overflows INT on MySQL). **Homepage selector (agent picker on the new-chat page)** ? single admin-curated list backed by `agents.featured_order` (nullable BIGINT). NULL = not featured. Only `published = true` agents can be featured (router enforces on writes); any published agent is eligible regardless of ownership. Cleared with `order: null`. The list rendered to end users is `WHERE featured_order IS NOT NULL` + visibility-filter, sorted by `featured_order ASC`. Admins reorder by sending the desired ids to `PUT /api/agents/featured/order` (assigns indices 0..N-1). BIGINT is used rather than INT so the management UI can seed initial values from `Date.now()` (overflows INT on MySQL).
@@ -766,6 +770,7 @@ MySQL index script synchronized.
- Physical: `backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/...`, `deer-flow/skills/` - Physical: `backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/...`, `deer-flow/skills/`
- Translation: `replace_virtual_path()` / `replace_virtual_paths_in_command()` - Translation: `replace_virtual_path()` / `replace_virtual_paths_in_command()`
- Detection: `is_local_sandbox()` checks `sandbox_id == "local"` - Detection: `is_local_sandbox()` checks `sandbox_id == "local"`
- Configured mount portability: `AppConfig._resolve_relative_mount_paths` resolves each relative `sandbox.mounts[*].host_path` against the loaded `config.yaml` directory before validation/provider construction, never against the process cwd. Keep repository-owned knowledge mounts relative (for example `knowledge/knowledge_base`); absolute paths remain unchanged, and sandbox providers always receive an absolute path.
- **`{user_id}` for path resolution = `resolve_path_user_id(thread_id)`, using the persisted creator for every tracked thread.** The route's regular owner check still authorizes access first; creator resolution only selects the stable physical directory (`threads_meta.user_id`) and avoids a lost request context falling back to `users/default`. This also preserves cross-account task-deeplink conversations (rwfx/3qfx/xdfx) and roundtable system threads. Positive creator mappings are cached process-wide and DB/memory/missing-row failures fall back to the current login. It is wired at the thread-path consumers including `ThreadDataMiddleware.before_agent`, `present_file_tool`, uploads, artifact serving, and embedded `client.get_artifact`. **Rewrite-history recovery is metadata-only and job-owner scoped** (`list_by_session(..., user_id=current_user)`), so its GET/list endpoints intentionally skip redundant thread-owner checks and return an empty history instead of a history-only 404; the start/commit endpoints retain strict thread write authorization. Tests: `tests/test_thread_paths_owner.py`, `tests/test_thread_paths_roundtable.py`. - **`{user_id}` for path resolution = `resolve_path_user_id(thread_id)`, using the persisted creator for every tracked thread.** The route's regular owner check still authorizes access first; creator resolution only selects the stable physical directory (`threads_meta.user_id`) and avoids a lost request context falling back to `users/default`. This also preserves cross-account task-deeplink conversations (rwfx/3qfx/xdfx) and roundtable system threads. Positive creator mappings are cached process-wide and DB/memory/missing-row failures fall back to the current login. It is wired at the thread-path consumers including `ThreadDataMiddleware.before_agent`, `present_file_tool`, uploads, artifact serving, and embedded `client.get_artifact`. **Rewrite-history recovery is metadata-only and job-owner scoped** (`list_by_session(..., user_id=current_user)`), so its GET/list endpoints intentionally skip redundant thread-owner checks and return an empty history instead of a history-only 404; the start/commit endpoints retain strict thread write authorization. Tests: `tests/test_thread_paths_owner.py`, `tests/test_thread_paths_roundtable.py`.
- **Durable document-rewrite commit:** `document_rewrite_pipeline.atomic_write_text` writes through a deliberately short sibling temporary filename so long Windows artifact paths stay below the path-length limit, and guarantees the target `outputs` parent exists. If the sandbox recreates that directory in the narrow commit window, it repairs the directory and retries; the original file is otherwise only changed through `os.replace`. - **Durable document-rewrite commit:** `document_rewrite_pipeline.atomic_write_text` writes through a deliberately short sibling temporary filename so long Windows artifact paths stay below the path-length limit, and guarantees the target `outputs` parent exists. If the sandbox recreates that directory in the narrow commit window, it repairs the directory and retries; the original file is otherwise only changed through `os.replace`.
@@ -998,6 +1003,15 @@ scheduler runs once per deployment. Per run:
- Supports `supports_vision` flag for image understanding models - Supports `supports_vision` flag for image understanding models
- Config values starting with `$` resolved as environment variables - Config values starting with `$` resolved as environment variables
- Missing provider modules surface actionable install hints from reflection resolvers (for example `uv add langchain-google-genai`) - Missing provider modules surface actionable install hints from reflection resolvers (for example `uv add langchain-google-genai`)
- Generic `langchain_openai:ChatOpenAI` entries are transparently wrapped by
`PatchedChatOpenAI`. Besides preserving reasoning/tool-call metadata, the
patch recognizes compatible gateways that encode failures inside HTTP 200
payloads (`base_resp.status_code != 0` with null/empty `choices`) in both
streaming and non-streaming paths. It raises the stable
`OpenAICompatibleUpstreamError`; `LLMErrorHandlingMiddleware` retries
transient concurrency/inference failures, while genuine quota/balance
failures are classified as non-retriable and surfaced immediately. Embedded
vendor failures therefore cannot become empty successful assistant turns.
### Roundtable Model Fault Tolerance (会商模型自动容错 / 换下一个模型) ### Roundtable Model Fault Tolerance (会商模型自动容错 / 换下一个模型)
@@ -1014,9 +1028,11 @@ scheduler runs once per deployment. Per run:
### vLLM Provider (`packages/harness/deerflow/models/vllm_provider.py`) ### vLLM Provider (`packages/harness/deerflow/models/vllm_provider.py`)
- `VllmChatModel` subclasses `langchain_openai:ChatOpenAI` for vLLM 0.19.0 OpenAI-compatible endpoints - `VllmChatModel` subclasses `langchain_openai:ChatOpenAI` for vLLM OpenAI-compatible endpoints
- Preserves vLLM's non-standard assistant `reasoning` field on full responses, streaming deltas, and follow-up tool-call turns - Thinking text is accepted from either `reasoning` (newer vLLM parsers) or `reasoning_content` (older vLLM / DeepSeek-style). The UI still reads canonical `additional_kwargs.reasoning_content`. Follow-up turns echo only the wire field that actually arrived, so other ChatOpenAI models are unchanged
- Designed for configs that enable thinking through `extra_body.chat_template_kwargs.enable_thinking` on vLLM 0.19.0 Qwen reasoning models, while accepting the older `thinking` alias - Shared helpers live in `deerflow/models/reasoning_fields.py` and are also used by `PatchedChatOpenAI` (the default `langchain_openai:ChatOpenAI` patch)
- Designed for configs that enable thinking through `extra_body.chat_template_kwargs.enable_thinking` on vLLM Qwen reasoning models, while accepting the older `thinking` alias
- Per-model switch: set only that entry's `use: deerflow.models.vllm_provider:VllmChatModel`. Do not change SiliconFlow / DeepSeek entries that already use `langchain_openai:ChatOpenAI`
### IM Channels System (`app/channels/`) ### IM Channels System (`app/channels/`)
@@ -1126,6 +1142,15 @@ Both can be modified at runtime via Gateway API endpoints or `DeerFlowClient` me
### Conditional WeKnora LLMWiki ### Conditional WeKnora LLMWiki
**Embedding ownership:** WeKnora's per-knowledge-base `embedding_model_id`
indexes uploaded source documents; `llmwiki.local_wiki_index.embedding` indexes
the generated Wiki-page mirror inside DeerFlow. Do not couple these two model
choices. `GET /api/llmwiki/embedding-models` returns a strict credential-free
projection of WeKnora Embedding models, and KB creation must validate and pass
the selected active model id. When the caller omits a choice, retain WeKnora's
active-default behavior. Existing KBs cannot change this binding in place, and
cross-KB WeKnora searches should use model-compatible knowledge bases.
Ordinary knowledge-base cards expose an owner/admin-only name/description edit dialog in `WeKnoraKnowledgeWorkspace.tsx`, reusing `useLlmWikiActions().update` and `PATCH /api/llmwiki/knowledge-bases/{id}`. The route trims names, rejects blank or reserved conversation-deposit names, writes to WeKnora before updating local metadata, and retains existing write authorization. A successful remote list is authoritative for every list scope, including `personal`: absent remote IDs are hidden without deleting mappings/references. Upstream failures remain errors. The grid also filters cached `remote_status=missing` rows and no longer renders stale-record cleanup cards. Regression coverage: `tests/test_llmwiki_knowledge_management.py`. Ordinary knowledge-base cards expose an owner/admin-only name/description edit dialog in `WeKnoraKnowledgeWorkspace.tsx`, reusing `useLlmWikiActions().update` and `PATCH /api/llmwiki/knowledge-bases/{id}`. The route trims names, rejects blank or reserved conversation-deposit names, writes to WeKnora before updating local metadata, and retains existing write authorization. A successful remote list is authoritative for every list scope, including `personal`: absent remote IDs are hidden without deleting mappings/references. Upstream failures remain errors. The grid also filters cached `remote_status=missing` rows and no longer renders stale-record cleanup cards. Regression coverage: `tests/test_llmwiki_knowledge_management.py`.
`llmwiki.weknora.api_base_url` is the provider switch: non-empty enables WeKnora, empty preserves the legacy LLMWiki UI. WeKnora is independently deployed and owns PostgreSQL/Redis/object storage/vector storage/parsers/models; DeerFlow config contains only `api_base_url` and optional `web_base_url`. DeerFlow deliberately sends no WeKnora credential, so the WeKnora endpoint must be restricted to a trusted network and allow DeerFlow traffic itself or through its reverse proxy. There is intentionally no `default_embedding_model_id`: `WeKnoraClient` queries `/api/v1/models` and uses WeKnora's active Embedding model when creating a KB; Wiki creation additionally resolves an active KnowledgeQA model inside WeKnora for synthesis. The native BFF is `app/gateway/routers/llmwiki.py`; ownership/publication mappings live in `deerflow.persistence.llmwiki`; the browser only sees DeerFlow mapping IDs. Knowledge-base owners publish immediately (`private` -> `published`) without a review queue, and can unpublish ordinary bases. The conversation-deposit KB is system-owned, always published, shared by all DeerFlow users, excluded from My Knowledge, and immutable through user/admin detail operations; recognition requires both the reserved name and `system` owner. Existing same-name remote mappings are adopted in place as the system deposit, preserving mapping ids and document links. Its user-facing description, generated Markdown source and tags use `cmzs` branding; list/detail reads synchronize stale remote descriptions, and search results, source previews and embedded-detail JSON recursively replace legacy brand text in historical conversation-deposit payloads. Deposit requests must reference a thread accessible to the current account. Personal knowledge-base list/search views are scoped to the current DeerFlow account even for admins; frontend LLMWiki caches include the live account id, and admin write power does not make other users' private KBs appear as owned. Only `system_role === "admin"` sees the WeKnora connection-config tab. Knowledge cards navigate to a dedicated WeKnora-style detail route. Wiki-enabled bases render and default to `Wiki -> documents -> graph`; standard bases omit Wiki and render `documents -> graph`. The detail page supports sequential multi-file upload with an aggregate result for writable ordinary bases, hides it for public read-only/system-deposit bases, and uses a generic loading label while the embed session starts. It also supports URL and published manual-Markdown imports, browses real Wiki pages/content, proxies authenticated original-file previews, pages through `/chunks/{knowledge_id}`, and renders the Wiki page graph from `/knowledgebase/{kb_id}/wiki/graph`; document details use a right sheet rather than the removed KB modal. The credential-free proxy uses one WeKnora service identity, so per-user separation is enforced by DeerFlow mappings while all remote KBs physically share that identity's WeKnora workspace. WeKnora iframe detail pages first call `/api/llmwiki/knowledge-bases/{id}/weknora/session` through the normal DeerFlow API auth path, then load `/platform/knowledge-bases/{weknora_id}` with a signed `deerflow_weknora_embed` cookie for `/platform`, `/assets`, `/locales`, root assets, and API calls. Injected fetch/XHR rewriting sends all iframe `/api/v1/*`, including colliding `/api/v1/auth/*`, through `/api/llmwiki/weknora-embed/api/v1/*`; Gateway auth/CSRF bypass only applies to those signed iframe proxy requests, the proxy still revalidates the mapped KB, and cookie security honors `X-Forwarded-Proto` for HTTPS reverse proxies. `llmwiki_search` is conditionally registered in `tools.py`, is read-only, and rechecks the current DeerFlow user, agent `llmwiki_knowledge_base_ids`, and thread-context selection on every call. Source previews prefer the cleaned Wiki/LLMWiki page text when a retrieved chunk maps to a Wiki page, with raw split chunks as the fallback. Agent bindings use an inline WeKnora-style searchable/grouped multi-select in the standard agent create/detail UI; new chats use the same interaction pattern as a compact composer dropdown, and selections are written to thread metadata/context and restored on reopen. The DeerFlow frontend is native React and the WeKnora frontend is not modified or embedded. Full deployment and behavior notes: `docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md`. Tests: `tests/test_llmwiki_weknora.py`. `llmwiki.weknora.api_base_url` is the provider switch: non-empty enables WeKnora, empty preserves the legacy LLMWiki UI. WeKnora is independently deployed and owns PostgreSQL/Redis/object storage/vector storage/parsers/models; DeerFlow config contains only `api_base_url` and optional `web_base_url`. DeerFlow deliberately sends no WeKnora credential, so the WeKnora endpoint must be restricted to a trusted network and allow DeerFlow traffic itself or through its reverse proxy. There is intentionally no `default_embedding_model_id`: `WeKnoraClient` queries `/api/v1/models` and uses WeKnora's active Embedding model when creating a KB; Wiki creation additionally resolves an active KnowledgeQA model inside WeKnora for synthesis. The native BFF is `app/gateway/routers/llmwiki.py`; ownership/publication mappings live in `deerflow.persistence.llmwiki`; the browser only sees DeerFlow mapping IDs. Knowledge-base owners publish immediately (`private` -> `published`) without a review queue, and can unpublish ordinary bases. The conversation-deposit KB is system-owned, always published, shared by all DeerFlow users, excluded from My Knowledge, and immutable through user/admin detail operations; recognition requires both the reserved name and `system` owner. Existing same-name remote mappings are adopted in place as the system deposit, preserving mapping ids and document links. Its user-facing description, generated Markdown source and tags use `cmzs` branding; list/detail reads synchronize stale remote descriptions, and search results, source previews and embedded-detail JSON recursively replace legacy brand text in historical conversation-deposit payloads. Deposit requests must reference a thread accessible to the current account. Personal knowledge-base list/search views are scoped to the current DeerFlow account even for admins; frontend LLMWiki caches include the live account id, and admin write power does not make other users' private KBs appear as owned. Only `system_role === "admin"` sees the WeKnora connection-config tab. Knowledge cards navigate to a dedicated WeKnora-style detail route. Wiki-enabled bases render and default to `Wiki -> documents -> graph`; standard bases omit Wiki and render `documents -> graph`. The detail page supports sequential multi-file upload with an aggregate result for writable ordinary bases, hides it for public read-only/system-deposit bases, and uses a generic loading label while the embed session starts. It also supports URL and published manual-Markdown imports, browses real Wiki pages/content, proxies authenticated original-file previews, pages through `/chunks/{knowledge_id}`, and renders the Wiki page graph from `/knowledgebase/{kb_id}/wiki/graph`; document details use a right sheet rather than the removed KB modal. The credential-free proxy uses one WeKnora service identity, so per-user separation is enforced by DeerFlow mappings while all remote KBs physically share that identity's WeKnora workspace. WeKnora iframe detail pages first call `/api/llmwiki/knowledge-bases/{id}/weknora/session` through the normal DeerFlow API auth path, then load `/platform/knowledge-bases/{weknora_id}` with a signed `deerflow_weknora_embed` cookie for `/platform`, `/assets`, `/locales`, root assets, and API calls. Injected fetch/XHR rewriting sends all iframe `/api/v1/*`, including colliding `/api/v1/auth/*`, through `/api/llmwiki/weknora-embed/api/v1/*`; Gateway auth/CSRF bypass only applies to those signed iframe proxy requests, the proxy still revalidates the mapped KB, and cookie security honors `X-Forwarded-Proto` for HTTPS reverse proxies. `llmwiki_search` is conditionally registered in `tools.py`, is read-only, and rechecks the current DeerFlow user, agent `llmwiki_knowledge_base_ids`, and thread-context selection on every call. Source previews prefer the cleaned Wiki/LLMWiki page text when a retrieved chunk maps to a Wiki page, with raw split chunks as the fallback. Agent bindings use an inline WeKnora-style searchable/grouped multi-select in the standard agent create/detail UI; new chats use the same interaction pattern as a compact composer dropdown, and selections are written to thread metadata/context and restored on reopen. The DeerFlow frontend is native React and the WeKnora frontend is not modified or embedded. Full deployment and behavior notes: `docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md`. Tests: `tests/test_llmwiki_weknora.py`.
@@ -1322,6 +1347,14 @@ Automatic conversation summarization when approaching token limits:
when it has already been folded into the hidden summary. This keeps strict when it has already been folded into the hidden summary. This keeps strict
OpenAI-compatible gateways from rejecting a tool-continuation call with OpenAI-compatible gateways from rejecting a tool-continuation call with
`No user query found in message`. `No user query found in message`.
- LangChain keeps the agent prompt in `ModelRequest.system_message`, separate
from `ModelRequest.messages`. Compression must merge its hidden summary into
that dedicated slot and remove every `SystemMessage` from the ordinary
message list. Strict Qwen-style adapters accept only one system message at
index zero and otherwise return HTTP 400 (`system message must be at the
beginning`). Keep this normalization adapter-independent; the payload-level
OpenAI-compatible normalizer is a second line of defence and also merges
adjacent leading system messages.
- Keeps recent messages while summarizing older ones. - Keeps recent messages while summarizing older ones.
Tests: `tests/test_summarization_toggle.py` (gating + whitelist), Tests: `tests/test_summarization_toggle.py` (gating + whitelist),
@@ -1801,8 +1834,8 @@ Ordinary knowledge detail landing is globally configured by `ordinary_knowledge_
pages need backfill, not a forced rebuild of every page. pages need backfill, not a forced rebuild of every page.
FastEmbed is a standard backend dependency. Encoder configuration, directory FastEmbed is a standard backend dependency. Encoder configuration, directory
upload and warmup live only in the administrator global settings dialog. upload and warmup live only in the administrator global settings dialog.
A runtime `system_settings.json` path override wins over config.yaml; uploaded When the effective provider is local, its `system_settings.json` path override wins over config.yaml; uploaded
files are streamed into `.deer-flow/models/bge-m3`, never the source tree. files are streamed into immutable `.deer-flow/models/bge-m3-revisions/<revision>` directories, never the source tree.
Encoder warmup is explicitly started from the settings UI/API and must never block application startup Encoder warmup is explicitly started from the settings UI/API and must never block application startup
or turn a model-load failure into a DeerFlow service failure. Local provider or turn a model-load failure into a DeerFlow service failure. Local provider
is offline-only: require local_model_path or DEERFLOW_BGE_M3_MODEL_PATH and is offline-only: require local_model_path or DEERFLOW_BGE_M3_MODEL_PATH and
@@ -1810,6 +1843,21 @@ Ordinary knowledge detail landing is globally configured by `ordinary_knowledge_
`local_process_isolation=true` by default: inference runs in the bundled JSONL `local_process_isolation=true` by default: inference runs in the bundled JSONL
worker subprocess, and shutdown must terminate that worker without waiting on worker subprocess, and shutdown must terminate that worker without waiting on
model teardown. model teardown.
- The administrator encoder override is an explicit persisted mode, not an
inference from whether `model_path` happens to be non-empty. `configured`
uses the current `config.yaml` provider (including OpenAI-compatible);
saving/uploading a local directory sets `local` plus a new model revision,
while `/api/llmwiki/local-encoder/use-configured` switches back. Every switch
hot-rebuilds embedding/cache/search/sync/retrieval as one graph and updates
the harness runtime registration. Status must remain readable for a remote
provider. The scheduler stays alive as a lightweight config watcher but only
scans when effective `enabled && auto_sync`; keep the default/portable release
configs at `auto_sync=true` because asynchronous Wiki generation can finish
after the event-triggered first scan. A changed local model revision must
change the embedding fingerprint so vectors from replaced weights are never
reused. Uploaded weights live in immutable revision directories. A replaced
embedding generation gets a bounded grace period for in-flight work, then
rejects stragglers, drains real thread work, and releases its model process.
- Every successful mapping snapshot is mirrored to the global assistant base, - Every successful mapping snapshot is mirrored to the global assistant base,
including empty snapshots so deleted source pages are retired. Re-import is including empty snapshots so deleted source pages are retired. Re-import is
source-replacement semantics: absent Wiki/chunk/entity/relation pages must not source-replacement semantics: absent Wiki/chunk/entity/relation pages must not
+32 -3
View File
@@ -79,7 +79,7 @@ Middlewares execute in strict order, each handling a specific concern:
| 1 | **ThreadDataMiddleware** | Creates per-thread isolated directories (workspace, uploads, outputs) | | 1 | **ThreadDataMiddleware** | Creates per-thread isolated directories (workspace, uploads, outputs) |
| 2 | **UploadsMiddleware** | Injects newly uploaded files into conversation context | | 2 | **UploadsMiddleware** | Injects newly uploaded files into conversation context |
| 3 | **SandboxMiddleware** | Acquires sandbox environment for code execution | | 3 | **SandboxMiddleware** | Acquires sandbox environment for code execution |
| 4 | **SummarizationMiddleware** | Reduces context when approaching token limits (optional) | | 4 | **SummarizationMiddleware** | Reduces context near token limits and folds the hidden summary into one leading system message for strict model adapters (optional) |
| 5 | **TodoListMiddleware** | Tracks multi-step tasks in plan mode (optional) | | 5 | **TodoListMiddleware** | Tracks multi-step tasks in plan mode (optional) |
| 6 | **TitleMiddleware** | Auto-generates conversation titles after first exchange | | 6 | **TitleMiddleware** | Auto-generates conversation titles after first exchange |
| 7 | **MemoryMiddleware** | Queues conversations for async memory extraction | | 7 | **MemoryMiddleware** | Queues conversations for async memory extraction |
@@ -94,6 +94,7 @@ Per-thread isolated execution with virtual path translation:
- **Providers**: `LocalSandboxProvider` (filesystem) and `AioSandboxProvider` (Docker, in community/) - **Providers**: `LocalSandboxProvider` (filesystem) and `AioSandboxProvider` (Docker, in community/)
- **Virtual paths**: `/mnt/user-data/{workspace,uploads,outputs}` → thread-specific physical directories - **Virtual paths**: `/mnt/user-data/{workspace,uploads,outputs}` → thread-specific physical directories
- **Skills path**: `/mnt/skills` → `deer-flow/skills/` directory - **Skills path**: `/mnt/skills` → `deer-flow/skills/` directory
- **Portable configured mounts**: a relative `sandbox.mounts[*].host_path` is resolved against the directory containing the loaded `config.yaml`, not the process working directory. Providers still receive an absolute host path, so `knowledge/knowledge_base` can be committed and the deployment can be cloned or moved without editing a machine-specific path; already-absolute mount paths are preserved.
- **Skills loading**: Recursively discovers nested `SKILL.md` files under `skills/{public,custom}` and preserves nested container paths - **Skills loading**: Recursively discovers nested `SKILL.md` files under `skills/{public,custom}` and preserves nested container paths
- **Conditional WeKnora LLMWiki**: a non-empty `llmwiki.weknora.api_base_url` enables the DeerFlow-native WeKnora BFF and UI; an empty address preserves the existing LLMWiki page. WeKnora remains independently deployed and owns its database, Redis, storage, parsers and models. DeerFlow keeps only the API/admin addresses, user/publication mappings, agent knowledge-base bindings and per-conversation selections; it stores and sends no WeKnora credential, so the endpoint must be restricted to a trusted network and allow DeerFlow traffic. Owners publish directly to the public LLMWiki without an approval queue for ordinary bases; the system-owned conversation-deposit base is always published, shared by all users, excluded from My Knowledge, immutable through user/admin detail operations, and recognized by both its reserved name and `system` owner. An existing same-name remote mapping is adopted in place as the system deposit so its mapping id and document links stay valid. Its user-facing knowledge-base description and generated document metadata use the `cmzs` brand; list/detail reads synchronize stale remote descriptions, while conversation-deposit search results, source previews and embedded-detail JSON rewrite legacy brand text to `cmzs`. Deposit requests must reference a thread accessible to the current account. Personal knowledge-base lists are scoped to the current DeerFlow account even for admins, while frontend LLMWiki query caches also include the active account id so switching accounts cannot reuse another account's list/detail data. Admins remain the only role that sees the WeKnora connection-config tab. Knowledge cards open a dedicated WeKnora-style detail route: Wiki-enabled bases default to Wiki, then documents and graph; standard bases hide Wiki and default to documents. Writable ordinary knowledge-base details expose a native multi-file upload action that submits files sequentially and reports aggregate success/failure; read-only public bases and the system deposit base do not. It supports file/URL/manual-Markdown imports, Wiki page browsing, document preview, real parsed chunks and Wiki graph nodes/edges. New chats use a compact WeKnora-style searchable/grouped multi-select inside the composer, while agent create/edit pages bind spaces through an inline panel. A single proxy service identity means DeerFlow users are isolated logically while their WeKnora entities share that identity's workspace. The read-only `llmwiki_search` tool reauthorizes the current DeerFlow user on every call and renders credential-free source previews; source previews prefer cleaned Wiki/LLMWiki text when a retrieved chunk maps to a Wiki page, with raw chunks as the fallback. The detail page first creates a signed iframe session through the normal DeerFlow API auth path, then points the iframe directly at the proxied WeKnora `/platform/...` route using the signed `deerflow_weknora_embed` cookie; global auth/CSRF bypass is limited to those signed iframe proxy requests, and HTTPS cookie attributes honor `X-Forwarded-Proto` behind reverse proxies. The iframe proxy keeps WeKnora's root-relative `/platform`, `/assets`, `/locales`, and `/api/v1/*` paths, but is registered last through `llmwiki.proxy_router` so it cannot intercept first-party Gateway APIs such as `POST /api/v1/auth/login/username`. See [docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md](docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md). - **Conditional WeKnora LLMWiki**: a non-empty `llmwiki.weknora.api_base_url` enables the DeerFlow-native WeKnora BFF and UI; an empty address preserves the existing LLMWiki page. WeKnora remains independently deployed and owns its database, Redis, storage, parsers and models. DeerFlow keeps only the API/admin addresses, user/publication mappings, agent knowledge-base bindings and per-conversation selections; it stores and sends no WeKnora credential, so the endpoint must be restricted to a trusted network and allow DeerFlow traffic. Owners publish directly to the public LLMWiki without an approval queue for ordinary bases; the system-owned conversation-deposit base is always published, shared by all users, excluded from My Knowledge, immutable through user/admin detail operations, and recognized by both its reserved name and `system` owner. An existing same-name remote mapping is adopted in place as the system deposit so its mapping id and document links stay valid. Its user-facing knowledge-base description and generated document metadata use the `cmzs` brand; list/detail reads synchronize stale remote descriptions, while conversation-deposit search results, source previews and embedded-detail JSON rewrite legacy brand text to `cmzs`. Deposit requests must reference a thread accessible to the current account. Personal knowledge-base lists are scoped to the current DeerFlow account even for admins, while frontend LLMWiki query caches also include the active account id so switching accounts cannot reuse another account's list/detail data. Admins remain the only role that sees the WeKnora connection-config tab. Knowledge cards open a dedicated WeKnora-style detail route: Wiki-enabled bases default to Wiki, then documents and graph; standard bases hide Wiki and default to documents. Writable ordinary knowledge-base details expose a native multi-file upload action that submits files sequentially and reports aggregate success/failure; read-only public bases and the system deposit base do not. It supports file/URL/manual-Markdown imports, Wiki page browsing, document preview, real parsed chunks and Wiki graph nodes/edges. New chats use a compact WeKnora-style searchable/grouped multi-select inside the composer, while agent create/edit pages bind spaces through an inline panel. A single proxy service identity means DeerFlow users are isolated logically while their WeKnora entities share that identity's workspace. The read-only `llmwiki_search` tool reauthorizes the current DeerFlow user on every call and renders credential-free source previews; source previews prefer cleaned Wiki/LLMWiki text when a retrieved chunk maps to a Wiki page, with raw chunks as the fallback. The detail page first creates a signed iframe session through the normal DeerFlow API auth path, then points the iframe directly at the proxied WeKnora `/platform/...` route using the signed `deerflow_weknora_embed` cookie; global auth/CSRF bypass is limited to those signed iframe proxy requests, and HTTPS cookie attributes honor `X-Forwarded-Proto` behind reverse proxies. The iframe proxy keeps WeKnora's root-relative `/platform`, `/assets`, `/locales`, and `/api/v1/*` paths, but is registered last through `llmwiki.proxy_router` so it cannot intercept first-party Gateway APIs such as `POST /api/v1/auth/login/username`. See [docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md](docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md).
- **Enterprise Research workbench (temporarily disabled)**: the independent `/api/enterprise-research` router, its report executor, and its ORM table registration are intentionally inactive. The implementation remains in the repository, but startup does not create or alter `enterprise_research_tasks` or `enterprise_research_report_jobs`; this keeps the active Deep Research workflow independent of the paused workbench and avoids MySQL schema errors. - **Enterprise Research workbench (temporarily disabled)**: the independent `/api/enterprise-research` router, its report executor, and its ORM table registration are intentionally inactive. The implementation remains in the repository, but startup does not create or alter `enterprise_research_tasks` or `enterprise_research_report_jobs`; this keeps the active Deep Research workflow independent of the paused workbench and avoids MySQL schema errors.
@@ -471,6 +472,10 @@ Custom agents are indexed in the `agents` database table. The `id` column is the
The built-in `forced-research-responder` (「强制检索输出助手」) is seeded at Gateway startup. Unlike an ordinary skill-enabled custom agent, it has a runtime-enforced per-turn collection gate: the model cannot finish a response until a retrieval/knowledge/MCP tool or a skill Python script returns usable information. It then answers directly in the user's requested chat format rather than generating an artifact. The agent may create or edit only temporary `.py` files below `/mnt/user-data/workspace/` and may execute `.py` skill scripts through `bash`; writes to `outputs`, Markdown/document creation, `present_files`, general shell commands, and skill mutation are denied. Administrators can edit its model and skill allowlist from agent management; the runtime safety policy remains fixed. Regression coverage: `tests/test_forced_research_agent.py`. The built-in `forced-research-responder` (「强制检索输出助手」) is seeded at Gateway startup. Unlike an ordinary skill-enabled custom agent, it has a runtime-enforced per-turn collection gate: the model cannot finish a response until a retrieval/knowledge/MCP tool or a skill Python script returns usable information. It then answers directly in the user's requested chat format rather than generating an artifact. The agent may create or edit only temporary `.py` files below `/mnt/user-data/workspace/` and may execute `.py` skill scripts through `bash`; writes to `outputs`, Markdown/document creation, `present_files`, general shell commands, and skill mutation are denied. Administrators can edit its model and skill allowlist from agent management; the runtime safety policy remains fixed. Regression coverage: `tests/test_forced_research_agent.py`.
The strategy-selection workbench starts with nine folder-backed functional agents: one intent agent, six independent angle agents (governance, diplomacy, economy/supply-chain, security risk, public opinion, and law/policy), one judge, and one final solution editor. Defaults ship in `app/gateway/routers/_strategy_selection_seed_assets/` and are copied only when an agent directory is absent, before `_sync_legacy_agents`; existing administrator edits are never overwritten. A global `strategy_agent_roles` registry owns classification, lifecycle state, and ordering independently of projects. Intent, judge, and solution are fixed singleton roles: administrators may update their model, SOUL, and skills but cannot add, disable, reorder, or delete them. Angles form one ordered catalog shared by every project; administrators may add, update, disable, restore, and reorder them. `GET /api/strategy-agents` is the public enabled catalog and omits model/SOUL/skills; `include_disabled=true` or `include_config=true` requires an administrator. Admin writes use `POST /api/strategy-agents/angles`, `PUT /api/strategy-agents/{id}`, and `DELETE /api/strategy-agents/{id}`; deletion is a soft disable so folder configuration and historical project references remain intact. Dynamic angles use the reserved `strategy-selection-angle-` namespace, receive `web` + `file:read`, and get a safe default SOUL if the submitted SOUL is blank. Model names and selected skills are validated, skills must be active and globally visible, generic agent CRUD cannot create or hard-delete reserved strategy agents, and parallel execution rejects disabled roles. The shipped angle agents use `policy-knowledge`, intent uses `intent-clarification-policy`, judging is input-only, and the solution editor has file-write access for `/mnt/user-data/outputs/综合策略方案.md`. Every default SOUL requires neutral evidence handling, de-escalation and civilian protection, and prohibits operational combat guidance or targeted political manipulation.
`POST /api/parallel-agents/run/stream` keeps reasoning and the deliverable in separate per-agent frames. Provider structured reasoning and inline `<think>...</think>` content are emitted only as `reasoning_delta`; visible answer text is emitted as `delta`, and `agent_done.content` contains only the visible answer. Consumers must not append `reasoning_delta` to strategy output or the final document. A final `is_llm_error_message` fallback, empty/thinking-only response, or punctuation-only provider degeneration is emitted as `agent_error`, never as a successful `agent_done`. Regression coverage: `tests/test_strategy_selection_seed.py`, `tests/test_strategy_agent_management.py`, and `tests/test_roundtable_model_fallback.py`.
### AI Writing Intranet Retrieval ### AI Writing Intranet Retrieval
AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。通用检索里的非内置技能在 `ai_writing.researcher_skill_agent: true` 时会用真实 agent 运行时执行技能:以 `ai-writing-researcher` 身份读取该技能的 `SKILL.md`,并按技能要求通过 `bash` 运行脚本,弱模型也会在本轮任务里收到明确的读文件和脚本执行指令。 AI 写作的「知识库检索」会直连 `ai_writing.intranet_search_url` 内网 ES/知识库接口,并默认跳过代理环境变量和 HTTPS 证书校验,适配自签名或内部 CA 证书的内网服务。需要强校验时可在 `config.yaml` 里设置 `ai_writing.intranet_verify_ssl: true`。通用检索里的非内置技能在 `ai_writing.researcher_skill_agent: true` 时会用真实 agent 运行时执行技能:以 `ai-writing-researcher` 身份读取该技能的 `SKILL.md`,并按技能要求通过 `bash` 运行脚本,弱模型也会在本轮任务里收到明确的读文件和脚本执行指令。
@@ -543,6 +548,13 @@ the configured model has an explicit, proxy-supported thinking control. This
prevents DeerFlow's connection check from sending vendor-specific `thinking` prevents DeerFlow's connection check from sending vendor-specific `thinking`
fields that a generic OpenAI-compatible gateway may reject. fields that a generic OpenAI-compatible gateway may reject.
OpenAI-compatible gateways that return vendor errors inside an HTTP 200
response (for example, a non-zero `base_resp.status_code` with empty/null
`choices`) are treated as upstream failures in both streaming and
non-streaming calls. DeerFlow retries transient upstream responses instead of
accepting an empty assistant turn, while genuine quota/balance failures are
classified as non-retriable and surfaced immediately instead of being retried.
### Running ### Running
**Full Application** (from project root): **Full Application** (from project root):
@@ -916,6 +928,13 @@ the file through the existing thread-artifact endpoint.
远端已经删除的知识库会在下次列表刷新时自动隐藏,包括个人、公共列表和知识库选择器;页面每 15 秒自动刷新,也可以手动点击「刷新」。远端服务请求失败时展示错误,不把连接故障当成删除,不自动删除本地映射或已有引用。 远端已经删除的知识库会在下次列表刷新时自动隐藏,包括个人、公共列表和知识库选择器;页面每 15 秒自动刷新,也可以手动点击「刷新」。远端服务请求失败时展示错误,不把连接故障当成删除,不自动删除本地映射或已有引用。
新建知识库时会通过 `GET /api/llmwiki/embedding-models` 读取 WeKnora 中可用的
Embedding 模型,并把所选 `embedding_model_id` 明确传给创建接口。该模型只负责
WeKnora 中上传原文的分块向量化;它和下文 DeerFlow 自己的 Wiki 镜像编码器是两套
独立配置,因此选择 OpenAI/OpenAI-compatible 原文编码器时不要求同时启用本地
BGE-M3。模型列表响应只暴露名称、provider、状态等白名单字段,不返回密钥或连接
参数。模型绑定只对新建库生效;需要跨知识库检索时,各库应使用相同或兼容的向量模型。
## DeerFlow-local LLMWiki Wiki vector index ## DeerFlow-local LLMWiki Wiki vector index
LLMWiki can mirror processed WeKnora Wiki pages into DeerFlow and search only LLMWiki can mirror processed WeKnora Wiki pages into DeerFlow and search only
@@ -1164,13 +1183,23 @@ llmwiki:
本地编码依赖随标准后端安装。将完整 BGE-M3 模型目录提前放到离线服务器,并通过 本地编码依赖随标准后端安装。将完整 BGE-M3 模型目录提前放到离线服务器,并通过
`local_model_path` 或 `DEERFLOW_BGE_M3_MODEL_PATH` 指向它。管理员也可在 `local_model_path` 或 `DEERFLOW_BGE_M3_MODEL_PATH` 指向它。管理员也可在
「设置和更多 → 设置 → 本地编码器」中选择服务器目录,或从浏览器上传完整模型目录; 「设置和更多 → 设置 → Wiki 镜像编码器」中选择服务器目录,或从浏览器上传完整模型目录;
页面配置持久化到运行目录的 `system_settings.json`,上传文件落到 页面配置持久化到运行目录的 `system_settings.json`,上传文件落到
`.deer-flow/models/bge-m3`,均不进入源码仓库。系统强制 `local_files_only`, `.deer-flow/models/bge-m3-revisions/<revision>` 的不可变版本目录,均不进入源码仓库。系统强制 `local_files_only`,
**不会访问互联网或下载模型**。管理员可在同一设置页点击「一键启动编码器」异步加载;该操作不挂在应用启动流程,加载失败不会 **不会访问互联网或下载模型**。管理员可在同一设置页点击「一键启动编码器」异步加载;该操作不挂在应用启动流程,加载失败不会
中断 DeerFlow 主服务。默认由独立低优先级子进程承载模型内存,正常停止 DeerFlow 中断 DeerFlow 主服务。默认由独立低优先级子进程承载模型内存,正常停止 DeerFlow
时会一并回收该子进程;编码进程异常退出只会将当前向量任务标记失败。模型目录应包含 时会一并回收该子进程;编码进程异常退出只会将当前向量任务标记失败。模型目录应包含
`onnx/model.onnx`(官方权重约 2.3 GB)及对应 tokenizer/config 文件。权重不进入业务源码仓库。 `onnx/model.onnx`(官方权重约 2.3 GB)及对应 tokenizer/config 文件。权重不进入业务源码仓库。
运行时编码器模式持久化为 `configured` 或 `local`。默认 `configured` 严格使用
`config.yaml` 中的 OpenAI、OpenAI-compatible 或 local provider;保存服务器目录或
上传模型会显式切到 `local` 并热重建 embedding/search/sync/retrieval 服务,不需要
重启 Gateway。`POST /api/llmwiki/local-encoder/use-configured` 可立即恢复配置文件中的
provider。`GET /api/llmwiki/local-encoder/status` 在远程 provider 下也正常返回当前
provider/mode,不再把“不是 local”当成状态接口错误。每次激活本地模型都会生成新的
模型修订指纹,避免同一路径替换权重后误复用旧向量。切换后的旧编码器会给在途任务
保留短暂宽限期,随后等待当前编码调用结束并自动回收模型进程。
浏览器目录上传采用磁盘暂存和 4 MB 分块复制,页面显示上传进度;经 Nginx 等反向 浏览器目录上传采用磁盘暂存和 4 MB 分块复制,页面显示上传进度;经 Nginx 等反向
代理部署时仍需把请求体上限和读写超时配置到可承载完整模型目录。 代理部署时仍需把请求体上限和读写超时配置到可承载完整模型目录。
相同模型指纹、相同内容的页面复用现有向量;短正文/摘要至少保留一个非空分段。 相同模型指纹、相同内容的页面复用现有向量;短正文/摘要至少保留一个非空分段。
@@ -42,6 +42,7 @@ from app.gateway.routers import (
compact_menu_overrides, compact_menu_overrides,
dashboard_sessions, dashboard_sessions,
deep_research, deep_research,
direct_chat,
external_llmwiki, external_llmwiki,
feedback, feedback,
fixed_questions, fixed_questions,
@@ -91,6 +92,7 @@ from app.gateway.routers import (
skill_addresses, skill_addresses,
skill_knowledge, skill_knowledge,
skills, skills,
strategy_agents,
suggestions, suggestions,
system_settings, system_settings,
tags, tags,
@@ -1019,20 +1021,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# Run Alembic migrations before anything else touches the schema. # Run Alembic migrations before anything else touches the schema.
await _run_db_migrations() await _run_db_migrations()
# Local Wiki mirror/index scans are opt-in and run outside request # The dormant scheduler also watches config.yaml/system-settings
# handling. Per-library database leases keep multi-worker startup safe. # changes. Actual scans remain opt-in via enabled + auto_sync, while a
app.state.llmwiki_index_scheduler_task = None # runtime provider/URL switch can take effect without a restart.
local_wiki_index = app.state.config.llmwiki.local_wiki_index from app.gateway.llmwiki_index_scheduler import local_wiki_index_scheduler_loop
if (
local_wiki_index.enabled
and local_wiki_index.auto_sync
and getattr(app.state, "llmwiki_sync_service", None) is not None
):
from app.gateway.llmwiki_index_scheduler import local_wiki_index_scheduler_loop
app.state.llmwiki_index_scheduler_task = asyncio.create_task( app.state.llmwiki_index_scheduler_task = asyncio.create_task(
local_wiki_index_scheduler_loop(app) local_wiki_index_scheduler_loop(app)
) )
# ?? recall_budget cache?? DB ????,? get_effective_memory_config ????? # ?? recall_budget cache?? DB ????,? get_effective_memory_config ?????
try: try:
@@ -1087,6 +1083,21 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"AI-writing functional agent seeding failed at startup (non-fatal)" "AI-writing functional agent seeding failed at startup (non-fatal)"
) )
# Strategy-selection functional agents (intent, six independent
# analysis angles, judge, and solution editor). Seed them before
# _sync_legacy_agents so they are immediately visible and editable in
# the existing agent-management UI on a fresh deployment.
try:
from app.gateway.routers._strategy_selection_seed import (
ensure_strategy_selection_agents,
)
ensure_strategy_selection_agents()
except Exception:
logger.exception(
"Strategy-selection functional agent seeding failed at startup (non-fatal)"
)
# 内置「模板数据转换器」agent(JSON → HTML 模板填充)。同样必须在 # 内置「模板数据转换器」agent(JSON → HTML 模板填充)。同样必须在
# _sync_legacy_agents **之前**补建目录,后者把它 upsert 进 agents 表, # _sync_legacy_agents **之前**补建目录,后者把它 upsert 进 agents 表,
# 第一次启动 /api/agents 与定时任务下拉框里就有它。 # 第一次启动 /api/agents 与定时任务下拉框里就有它。
@@ -1211,6 +1222,15 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await ensure_builtin_agent_purpose_tags(agent_store, tag_store) await ensure_builtin_agent_purpose_tags(agent_store, tag_store)
except Exception: except Exception:
logger.exception("Built-in agent purpose-tag seeding failed at startup (non-fatal)") logger.exception("Built-in agent purpose-tag seeding failed at startup (non-fatal)")
# Synchronize every worker's strategy registry only after the
# folder agents have been imported. In memory mode this also
# restores dynamic/default angle lifecycle from sidecars.
try:
await strategy_agents.ensure_strategy_agent_registry(app)
logger.info("Strategy agent registry synchronized for worker")
except Exception:
logger.exception("Strategy agent role seeding failed at startup (non-fatal)")
except Exception: except Exception:
logger.exception("Legacy agent sync failed at startup (non-fatal)") logger.exception("Legacy agent sync failed at startup (non-fatal)")
@@ -1605,6 +1625,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
# Agents API is mounted at /api/agents # Agents API is mounted at /api/agents
app.include_router(agents.router) app.include_router(agents.router)
app.include_router(strategy_agents.router)
# Recommended Questions API is mounted at /api/recommended-questions # Recommended Questions API is mounted at /api/recommended-questions
app.include_router(recommended_questions.router) app.include_router(recommended_questions.router)
@@ -1803,6 +1824,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
# 开放问答接口 /api/open/chat —— 无需登录,指定模型 + 智能体做问答,可关闭思考模式 # 开放问答接口 /api/open/chat —— 无需登录,指定模型 + 智能体做问答,可关闭思考模式
app.include_router(open_chat.router) app.include_router(open_chat.router)
# 极速问答 /api/direct-chat/stream —— 直连大模型流式输出,不走 lead agent / 工具 / 技能
app.include_router(direct_chat.router)
# 开放接口 /api/open/3qfx/ask —— 无需登录,按 taskId 直接发起 3qfx 第一次问答(后台运行,落成真实会话) # 开放接口 /api/open/3qfx/ask —— 无需登录,按 taskId 直接发起 3qfx 第一次问答(后台运行,落成真实会话)
app.include_router(open_3qfx.router) app.include_router(open_3qfx.router)
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
from deerflow.persistence.notifications import NotificationStore from deerflow.persistence.notifications import NotificationStore
from deerflow.persistence.scheduled_tasks import ScheduledTaskStore from deerflow.persistence.scheduled_tasks import ScheduledTaskStore
from deerflow.persistence.skills import SkillStore from deerflow.persistence.skills import SkillStore
from deerflow.persistence.strategy_agent_roles import StrategyAgentRoleStore
from deerflow.persistence.tags import TagStore from deerflow.persistence.tags import TagStore
from deerflow.persistence.thread_meta.base import ThreadMetaStore from deerflow.persistence.thread_meta.base import ThreadMetaStore
@@ -132,6 +133,12 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.agent_store = make_agent_store(sf) app.state.agent_store = make_agent_store(sf)
from deerflow.persistence.strategy_agent_roles import (
make_strategy_agent_role_store,
)
app.state.strategy_agent_role_store = make_strategy_agent_role_store(sf)
from deerflow.persistence.skills import make_skill_store from deerflow.persistence.skills import make_skill_store
app.state.skill_store = make_skill_store(sf) app.state.skill_store = make_skill_store(sf)
@@ -384,7 +391,6 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
# DeerFlow-local Wiki mirror/vector index. The store is always wired so # DeerFlow-local Wiki mirror/vector index. The store is always wired so
# migrations and status inspection work while feature flags are off; # migrations and status inspection work while feature flags are off;
# network/search services only exist when explicitly enabled. # network/search services only exist when explicitly enabled.
from deerflow.integrations.weknora.local_index.runtime import register_local_wiki_index_runtime
from deerflow.persistence.llmwiki_index import make_llmwiki_index_store from deerflow.persistence.llmwiki_index import make_llmwiki_index_store
local_index_config = config.llmwiki.local_wiki_index local_index_config = config.llmwiki.local_wiki_index
@@ -397,78 +403,12 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.llmwiki_vector_search = None app.state.llmwiki_vector_search = None
app.state.llmwiki_sync_service = None app.state.llmwiki_sync_service = None
app.state.llmwiki_retrieval_service = None app.state.llmwiki_retrieval_service = None
from deerflow.integrations.weknora.runtime import build_weknora_client, get_resolved_llmwiki_runtime from app.gateway.llmwiki_index_runtime import rebuild_local_wiki_index_runtime
resolved_llmwiki = get_resolved_llmwiki_runtime(config) # Build every embedding-dependent object through the same hot-swap
weknora_client = build_weknora_client(resolved_llmwiki) if resolved_llmwiki.weknora_enabled else None # path used by the administrator settings API. This keeps startup,
embedding_config = local_index_config.embedding # uploaded-local selection and config.yaml reloads behaviorally equal.
# The administrator may select or upload the offline encoder from the await rebuild_local_wiki_index_runtime(app, force=True)
# UI. This runtime-owned value deliberately overrides config.yaml and
# survives application restarts without modifying release assets.
if embedding_config.provider == "local":
from deerflow.config.system_settings import load_system_settings
runtime_model_path = load_system_settings().local_wiki_encoder.model_path.strip()
if runtime_model_path:
embedding_config = embedding_config.model_copy(
update={"local_model_path": runtime_model_path}
)
embedding_configured = bool(
embedding_config.enabled
and (embedding_config.base_url or embedding_config.provider == "local")
and embedding_config.model.strip()
)
if embedding_configured:
from deerflow.integrations.weknora.local_index.embedding import StrictWikiEmbeddingClient
app.state.llmwiki_embedding = StrictWikiEmbeddingClient(embedding_config)
if local_index_config.enabled and app.state.llmwiki_embedding is not None:
from deerflow.integrations.weknora.local_index.search import WikiVectorSearchService
from deerflow.integrations.weknora.local_index.sync import WikiSyncService
from deerflow.integrations.weknora.local_index.vector_cache import WikiVectorCache
app.state.llmwiki_vector_cache = WikiVectorCache(
app.state.llmwiki_index_store,
max_knowledge_bases=local_index_config.cache_max_knowledge_bases,
revision_check_seconds=local_index_config.cache_revision_check_seconds,
)
app.state.llmwiki_vector_search = WikiVectorSearchService(
local_index_config,
app.state.llmwiki_embedding,
app.state.llmwiki_vector_cache,
)
if weknora_client is not None:
app.state.llmwiki_sync_service = WikiSyncService(
local_index_config,
app.state.llmwiki_index_store,
app.state.llmwiki_embedding,
app.state.llmwiki_vector_cache,
weknora_client,
)
from app.gateway.knowledge_transfer import sync_to_global
async def on_wiki_vectorized(mapping):
return await sync_to_global(app, mapping)
app.state.llmwiki_sync_service.on_completed = on_wiki_vectorized
if weknora_client is not None or app.state.llmwiki_vector_search is not None:
from deerflow.integrations.weknora.local_index.retrieval import WikiRetrievalService
app.state.llmwiki_retrieval_service = WikiRetrievalService(
app.state.llmwiki_index_store,
vector_search=(
app.state.llmwiki_vector_search
if local_index_config.internal_search_enabled
else None
),
client=weknora_client,
)
register_local_wiki_index_runtime(
store=app.state.llmwiki_index_store,
search=app.state.llmwiki_vector_search,
sync=app.state.llmwiki_sync_service,
retrieval=app.state.llmwiki_retrieval_service,
)
from deerflow.persistence.taskcop_tasks import make_taskcop_task_store from deerflow.persistence.taskcop_tasks import make_taskcop_task_store
@@ -707,9 +647,9 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
from deerflow.integrations.weknora.local_index.runtime import register_local_wiki_index_runtime from deerflow.integrations.weknora.local_index.runtime import register_local_wiki_index_runtime
register_local_wiki_index_runtime(store=None, search=None, sync=None, retrieval=None) register_local_wiki_index_runtime(store=None, search=None, sync=None, retrieval=None)
llmwiki_embedding = getattr(app.state, "llmwiki_embedding", None) from app.gateway.llmwiki_index_runtime import close_local_wiki_index_runtime
if llmwiki_embedding is not None:
await llmwiki_embedding.close() await close_local_wiki_index_runtime(app)
from deerflow.persistence.notifications import set_notification_store from deerflow.persistence.notifications import set_notification_store
set_notification_store(None) set_notification_store(None)
@@ -824,6 +764,14 @@ def get_agent_store(request: Request) -> AgentStore:
return val return val
def get_strategy_agent_role_store(request: Request) -> StrategyAgentRoleStore:
"""Return the global strategy-agent role registry."""
val = getattr(request.app.state, "strategy_agent_role_store", None)
if val is None:
raise HTTPException(status_code=503, detail="Strategy agent role store not available")
return val
def get_skill_store(request: Request) -> SkillStore: def get_skill_store(request: Request) -> SkillStore:
"""Return the custom-skill store.""" """Return the custom-skill store."""
val = getattr(request.app.state, "skill_store", None) val = getattr(request.app.state, "skill_store", None)
@@ -0,0 +1,369 @@
"""Hot-swappable runtime composition for the DeerFlow-local Wiki index.
The file configuration remains the source of truth for the normal embedding
provider. Administrators can temporarily select the uploaded offline BGE-M3
encoder through ``system_settings.json`` without rewriting release assets or
restarting Gateway.
"""
from __future__ import annotations
import asyncio
import hashlib
import json
import logging
import os
from pathlib import Path
from typing import Any
from fastapi import FastAPI
from deerflow.config import get_app_config
from deerflow.config.app_config import AppConfig
from deerflow.config.llmwiki_config import LocalWikiIndexConfig
from deerflow.config.system_settings import LocalWikiEncoderSettings, load_system_settings
from deerflow.integrations.weknora.runtime import build_weknora_client, get_resolved_llmwiki_runtime
logger = logging.getLogger(__name__)
_EMBEDDING_RETIREMENT_GRACE_SECONDS = 600.0
def resolve_effective_local_wiki_index_config(
config: AppConfig,
encoder_settings: LocalWikiEncoderSettings,
) -> LocalWikiIndexConfig:
"""Apply the persisted encoder mode to a copy of the file configuration."""
local_index = config.llmwiki.local_wiki_index
configured_embedding = local_index.embedding
runtime_model_path = encoder_settings.model_path.strip()
if encoder_settings.mode == "local":
# Directory upload is intentionally a BGE-M3 contract. A configured
# OpenAI model/dimension must not leak into the local FastEmbed worker.
embedding = configured_embedding.model_copy(
update={
"enabled": True,
"provider": "local",
"model": "bge-embedding-m3",
"dimensions": 1024,
"base_url": "",
"api_key": "",
"local_model_path": runtime_model_path or configured_embedding.local_model_path,
"local_model_revision": encoder_settings.model_revision,
}
)
elif configured_embedding.provider == "local":
# Keep the historical invariant: an administrator-selected directory
# overrides config.yaml when the configured provider itself is local.
updates = {"local_model_revision": encoder_settings.model_revision}
if runtime_model_path:
updates["local_model_path"] = runtime_model_path
embedding = configured_embedding.model_copy(
update=updates
)
else:
embedding = configured_embedding
return local_index.model_copy(update={"embedding": embedding})
def _resolved_local_model_path(config: LocalWikiIndexConfig) -> Path | None:
raw = config.embedding.local_model_path.strip() or os.environ.get(
"DEERFLOW_BGE_M3_MODEL_PATH", ""
).strip()
if not raw:
return None
path = Path(raw).expanduser()
return path.resolve() if path.is_absolute() else (Path.cwd() / path).resolve()
def _local_model_files_ready(config: LocalWikiIndexConfig) -> bool:
path = _resolved_local_model_path(config)
return bool(
path
and path.is_dir()
and ((path / "onnx" / "model.onnx").is_file() or (path / "model.onnx").is_file())
)
def _embedding_provider_ready(config: LocalWikiIndexConfig) -> bool:
embedding = config.embedding
if embedding.provider == "local":
return _local_model_files_ready(config)
return bool(embedding.base_url)
def _schedule_embedding_retirement(app: FastAPI, embedding: Any) -> None:
retired = list(getattr(app.state, "llmwiki_retired_embeddings", []) or [])
if embedding not in retired:
retired.append(embedding)
app.state.llmwiki_retired_embeddings = retired
async def retire_after_grace() -> None:
retired_successfully = False
try:
await asyncio.sleep(_EMBEDDING_RETIREMENT_GRACE_SECONDS)
await embedding.retire()
retired_successfully = True
except asyncio.CancelledError:
raise
except Exception:
logger.exception("Failed to retire an old Local Wiki embedding runtime")
finally:
if retired_successfully:
current = list(getattr(app.state, "llmwiki_retired_embeddings", []) or [])
app.state.llmwiki_retired_embeddings = [item for item in current if item is not embedding]
tasks: set[asyncio.Task[None]] = getattr(app.state, "llmwiki_retirement_tasks", None)
if tasks is None:
tasks = set()
app.state.llmwiki_retirement_tasks = tasks
task = asyncio.create_task(retire_after_grace(), name="llmwiki-embedding-retirement")
tasks.add(task)
task.add_done_callback(tasks.discard)
def _runtime_signature(
local_index: LocalWikiIndexConfig,
*,
resolved_llmwiki: Any,
encoder_mode: str,
) -> str:
payload = {
"local_index": local_index.model_dump(mode="json"),
# A directory can be mounted/unmounted without editing config.yaml.
# Readiness is part of the generation so services appear/disappear on
# the next lightweight scheduler poll instead of requiring a restart.
"local_model_files_ready": (
_local_model_files_ready(local_index)
if local_index.embedding.provider == "local"
else None
),
"encoder_mode": encoder_mode,
"weknora": {
"provider": resolved_llmwiki.provider,
"api_base_url": resolved_llmwiki.api_base_url,
"web_base_url": resolved_llmwiki.web_base_url,
"admin_email": resolved_llmwiki.admin_email,
"admin_password": resolved_llmwiki.admin_password,
"override_enabled": resolved_llmwiki.override_enabled,
},
}
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
async def rebuild_local_wiki_index_runtime(
app: FastAPI,
*,
force: bool = False,
) -> LocalWikiIndexConfig:
"""Refresh config and atomically replace all embedding-dependent services."""
lock = getattr(app.state, "llmwiki_runtime_lock", None)
if lock is None:
lock = asyncio.Lock()
app.state.llmwiki_runtime_lock = lock
async with lock:
source_config = get_app_config()
settings = load_system_settings()
effective_config = resolve_effective_local_wiki_index_config(
source_config,
settings.local_wiki_encoder,
)
resolved_llmwiki = get_resolved_llmwiki_runtime(source_config)
signature = _runtime_signature(
effective_config,
resolved_llmwiki=resolved_llmwiki,
encoder_mode=settings.local_wiki_encoder.mode,
)
current_signature = getattr(app.state, "llmwiki_runtime_signature", None)
if not force and current_signature == signature:
app.state.llmwiki_source_config = source_config
app.state.llmwiki_effective_config = effective_config
return effective_config
index_store = getattr(app.state, "llmwiki_index_store", None)
if index_store is None:
raise RuntimeError("Local Wiki index store is not initialized")
from deerflow.integrations.weknora.local_index.embedding import StrictWikiEmbeddingClient
from deerflow.integrations.weknora.local_index.retrieval import WikiRetrievalService
from deerflow.integrations.weknora.local_index.runtime import register_local_wiki_index_runtime
from deerflow.integrations.weknora.local_index.search import WikiVectorSearchService
from deerflow.integrations.weknora.local_index.sync import WikiSyncService
from deerflow.integrations.weknora.local_index.vector_cache import WikiVectorCache
embedding_config = effective_config.embedding
provider_ready = _embedding_provider_ready(effective_config)
embedding_configured = bool(
embedding_config.enabled
and provider_ready
and embedding_config.model.strip()
)
old_embedding = getattr(app.state, "llmwiki_embedding", None)
reusable = (
old_embedding
if old_embedding is not None and old_embedding.config == embedding_config
else None
)
new_embedding = None
if embedding_configured:
new_embedding = reusable or StrictWikiEmbeddingClient(embedding_config)
new_cache = None
new_search = None
new_sync = None
new_retrieval = None
weknora_client = (
build_weknora_client(resolved_llmwiki)
if resolved_llmwiki.weknora_enabled
else None
)
if effective_config.enabled and new_embedding is not None:
new_cache = WikiVectorCache(
index_store,
max_knowledge_bases=effective_config.cache_max_knowledge_bases,
revision_check_seconds=effective_config.cache_revision_check_seconds,
)
new_search = WikiVectorSearchService(
effective_config,
new_embedding,
new_cache,
)
if weknora_client is not None:
new_sync = WikiSyncService(
effective_config,
index_store,
new_embedding,
new_cache,
weknora_client,
)
from app.gateway.knowledge_transfer import sync_to_global
async def on_wiki_vectorized(mapping):
return await sync_to_global(app, mapping)
new_sync.on_completed = on_wiki_vectorized
if weknora_client is not None or new_search is not None:
new_retrieval = WikiRetrievalService(
index_store,
vector_search=(new_search if effective_config.internal_search_enabled else None),
client=weknora_client,
)
app.state.llmwiki_source_config = source_config
app.state.llmwiki_effective_config = effective_config
app.state.llmwiki_embedding = new_embedding
app.state.llmwiki_vector_cache = new_cache
app.state.llmwiki_vector_search = new_search
app.state.llmwiki_sync_service = new_sync
app.state.llmwiki_retrieval_service = new_retrieval
app.state.llmwiki_runtime_signature = signature
register_local_wiki_index_runtime(
store=index_store,
search=new_search,
sync=new_sync,
retrieval=new_retrieval,
)
if old_embedding is not None and old_embedding is not new_embedding:
# Give requests holding the prior service graph time to finish,
# then reject any stragglers and release the potentially multi-GB
# local worker instead of accumulating one process per upload.
_schedule_embedding_retirement(app, old_embedding)
return effective_config
async def close_local_wiki_index_runtime(app: FastAPI) -> None:
"""Close the current and safely-retired embedding clients at shutdown."""
retired = list(getattr(app.state, "llmwiki_retired_embeddings", []) or [])
retirement_tasks = list(getattr(app.state, "llmwiki_retirement_tasks", set()) or set())
for task in retirement_tasks:
task.cancel()
if retirement_tasks:
await asyncio.gather(*retirement_tasks, return_exceptions=True)
app.state.llmwiki_retirement_tasks = set()
current = getattr(app.state, "llmwiki_embedding", None)
seen: set[int] = set()
for embedding in [current, *retired]:
if embedding is None or id(embedding) in seen:
continue
seen.add(id(embedding))
await embedding.close()
app.state.llmwiki_retired_embeddings = []
def local_encoder_runtime_status(app: FastAPI) -> dict[str, object]:
"""Return one stable status contract for remote and local providers."""
source_config: AppConfig | None = getattr(app.state, "llmwiki_source_config", None)
if source_config is None:
source_config = app.state.config
configured = source_config.llmwiki.local_wiki_index.embedding
effective: LocalWikiIndexConfig = getattr(
app.state,
"llmwiki_effective_config",
source_config.llmwiki.local_wiki_index,
)
settings = load_system_settings().local_wiki_encoder
embedding = getattr(app.state, "llmwiki_embedding", None)
if embedding is not None:
status = dict(embedding.local_encoder_status())
else:
status = {
"provider": effective.embedding.provider,
"model": effective.embedding.model,
"dimensions": effective.embedding.dimensions,
"state": "idle",
"running": False,
"last_error": None,
"started_at": None,
"ready_at": None,
"offline_only": effective.embedding.provider == "local",
"model_path_configured": False,
"model_path_exists": False,
"model_files_ready": False,
"process_isolation": effective.embedding.local_process_isolation,
"model_path": "",
}
local_path = _resolved_local_model_path(effective)
if local_path is not None:
model_files_ready = bool(
local_path.is_dir()
and (
(local_path / "onnx" / "model.onnx").is_file()
or (local_path / "model.onnx").is_file()
)
)
status.update(
{
"model_path": str(local_path),
"model_path_configured": True,
"model_path_exists": local_path.is_dir(),
"model_files_ready": model_files_ready,
}
)
status.update(
{
"provider": effective.embedding.provider,
"configured_provider": configured.provider,
"mode": settings.mode,
"using_local": effective.embedding.provider == "local",
"offline_only": effective.embedding.provider == "local",
}
)
return status
__all__ = [
"close_local_wiki_index_runtime",
"local_encoder_runtime_status",
"rebuild_local_wiki_index_runtime",
"resolve_effective_local_wiki_index_config",
]
@@ -5,11 +5,13 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from collections.abc import Coroutine from collections.abc import Coroutine
from time import monotonic
from typing import Any from typing import Any
from fastapi import FastAPI from fastapi import FastAPI
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CONFIG_REFRESH_SECONDS = 10.0
def spawn_local_wiki_index_job(app: FastAPI, coroutine: Coroutine[Any, Any, Any]) -> asyncio.Task: def spawn_local_wiki_index_job(app: FastAPI, coroutine: Coroutine[Any, Any, Any]) -> asyncio.Task:
@@ -25,12 +27,27 @@ def spawn_local_wiki_index_job(app: FastAPI, coroutine: Coroutine[Any, Any, Any]
return task return task
async def _sync_once(app: FastAPI) -> None: async def _refresh_local_wiki_config(app: FastAPI):
from app.gateway.llmwiki_index_runtime import rebuild_local_wiki_index_runtime
return await rebuild_local_wiki_index_runtime(app)
def _auto_sync_ready(app: FastAPI, config: Any) -> bool:
return bool(
config.enabled
and config.auto_sync
and getattr(app.state, "llmwiki_sync_service", None) is not None
and getattr(app.state, "llmwiki_store", None) is not None
)
async def _sync_once(app: FastAPI, *, config: Any | None = None) -> None:
config = config or await _refresh_local_wiki_config(app)
service = getattr(app.state, "llmwiki_sync_service", None) service = getattr(app.state, "llmwiki_sync_service", None)
mapping_store = getattr(app.state, "llmwiki_store", None) mapping_store = getattr(app.state, "llmwiki_store", None)
if service is None or mapping_store is None: if not _auto_sync_ready(app, config) or service is None or mapping_store is None:
return return
config = app.state.config.llmwiki.local_wiki_index
mappings = [row for row in await mapping_store.list_visible("system", scope="all", is_admin=True) if row.get("wiki_index_enabled", True)] mappings = [row for row in await mapping_store.list_visible("system", scope="all", is_admin=True) if row.get("wiki_index_enabled", True)]
semaphore = asyncio.Semaphore(config.sync_concurrency) semaphore = asyncio.Semaphore(config.sync_concurrency)
@@ -47,16 +64,42 @@ async def _sync_once(app: FastAPI) -> None:
async def local_wiki_index_scheduler_loop(app: FastAPI) -> None: async def local_wiki_index_scheduler_loop(app: FastAPI) -> None:
interval = app.state.config.llmwiki.local_wiki_index.sync_interval_seconds
logger.info( logger.info(
"Local Wiki auto-sync scheduler started; first scan is immediate, interval=%s seconds", "Local Wiki auto-sync scheduler started; runtime config refresh interval=%s seconds",
interval, _CONFIG_REFRESH_SECONDS,
) )
last_sync_started_at: float | None = None
last_synced_runtime_signature: str | None = None
while True: while True:
try: try:
await _sync_once(app) # Rebuild checks both config.yaml mtime and persisted runtime
# settings every poll, independently from the much longer sync
# cadence. Provider/URL changes therefore take effect promptly.
config = await _refresh_local_wiki_config(app)
if not _auto_sync_ready(app, config):
# A later disabled -> enabled transition is a new first valid
# configuration and should receive an immediate scan.
last_sync_started_at = None
last_synced_runtime_signature = None
else:
now = monotonic()
interval = float(config.sync_interval_seconds)
runtime_signature = getattr(
app.state,
"llmwiki_runtime_signature",
None,
)
if (
last_sync_started_at is None
or runtime_signature != last_synced_runtime_signature
or now - last_sync_started_at >= interval
):
sync_started_at = now
await _sync_once(app, config=config)
last_sync_started_at = sync_started_at
last_synced_runtime_signature = runtime_signature
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception: except Exception:
logger.exception("Local Wiki auto-sync scheduler tick failed (non-fatal)") logger.exception("Local Wiki auto-sync scheduler tick failed (non-fatal)")
await asyncio.sleep(interval) await asyncio.sleep(_CONFIG_REFRESH_SECONDS)
@@ -0,0 +1,51 @@
---
name: task-report-import
description: 任务研判成果入库:校验智能体生成的 task-reports.json(14 类分类结构),并调用 Gateway 的 /api/task-reports/import/by-task/{taskId} 接口把结果写入任务详情(整体替换)。内网自签证书场景已关闭 SSL 校验。
allowed-tools: read_file, bash
---
# 任务研判成果入库
把「任务研判报告智能体」生成的 `/mnt/user-data/outputs/task-reports.json` 校验后导入任务详情。
导入接口会**整体替换**该任务已有的全部详情记录。
## 用法
```bash
python /mnt/skills/public/task-report-import/scripts/task_report_import.py \
--task-id <任务id> \
--json /mnt/user-data/outputs/task-reports.json
```
参数(均有默认值,一般只需 `--task-id`):
| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `--task-id` | (必填) | 任务 id,取开场信息里的 task_id |
| `--json` | `/mnt/user-data/outputs/task-reports.json` | 待导入的 JSON 文件;接受裸数组或 `{data\|records: [...]}` 信封 |
| `--gateway` | `$AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:8001` | Gateway 基地址 |
| `--token` | 见下方鉴权 | Bearer token |
| `--strict` | 关 | 开启后缺任何一个分类都拒绝导入(默认允许部分导入,缺的分类会列在结果里) |
| `--verify-tls` | 关 | 开启 TLS 证书校验。内网自签证书部署默认关闭(ssl 不验证) |
## 鉴权(按顺序取第一个可用)
1. `--token` 参数;
2. 环境变量 `AGENTFX_IMPORT_TOKEN` → `DEERFLOW_AUTH_TOKEN` → `TOOLS_TOKEN`;
3. 都没有时,自动调 Gateway 公共端点 `POST /api/parallel-agents/auth/token`(传用户名 `agentfx-import`)现签一个 token——内网部署的既有鉴权旁路模式,无需配置。
## 校验规则
- 记录必须是对象,且 `categoryType` 属于 14 个固定分类(人文环境/敌情关键事件/我情口径/条件与限制/决心建议/我情上级要求/敌情对手叙事/行动方案/信息环境/优劣势分析/处置建议/我情叙事/敌情目标/风险及应对,均带 ` JSON` 后缀);
- `contentJson` 必须存在:对象会自动序列化为字符串,字符串必须能解析为合法 JSON;
- `id`/`taskId`/`createTime`/`updateTime`/`sessionId` 缺省时自动补齐(`taskId` 强制为 `--task-id`)。
## 结果
stdout 输出一行 JSON:
```json
{"success": true, "task_id": "0101", "imported": 14, "missing_categories": [], "http_status": 200, "errors": []}
```
`success=false` 时把 `errors` 原样反馈给用户;`missing_categories` 非空时提醒用户哪些分类缺失。导入成功后任务的 TaskCOP 状态会自动置为「已完成分析」。
@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""任务研判成果入库(task-report-import 技能脚本,纯标准库)。
校验「任务研判报告智能体」生成的 task-reports.json(14 类分类结构),并调用本系统
Gateway 的 ``POST /api/task-reports/import/by-task/{taskId}`` 接口整体替换任务详情。
设计要点:
- 内网自签证书部署:默认**关闭 SSL 证书校验**(``--verify-tls`` 可重新开启);
- 鉴权:``--token`` / 环境变量优先;都没有时调 Gateway 公共端点
``/api/parallel-agents/auth/token`` 现签 token(内网部署既有鉴权旁路模式);
- ``id``/``taskId``/``createTime``/``updateTime``/``sessionId`` 缺省自动补齐
(``taskId`` 强制为 ``--task-id``,与后端导入语义一致);
- stdout 输出一行 JSON 结果(success/imported/missing_categories/errors)。
"""
from __future__ import annotations
import argparse
import json
import os
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
# 14 个固定分类(与任务详情导入示例 / 前端校验保持一致,均带 “ JSON” 后缀)。
CATEGORY_TYPES: list[str] = [
"信息环境 JSON",
"人文环境 JSON",
"敌情关键事件 JSON",
"敌情对手叙事 JSON",
"敌情目标 JSON",
"我情口径 JSON",
"我情叙事 JSON",
"我情上级要求 JSON",
"条件与限制 JSON",
"优劣势分析 JSON",
"风险及应对 JSON",
"行动方案 JSON",
"处置建议 JSON",
"决心建议 JSON",
]
TOKEN_ENV_CHAIN: tuple[str, ...] = (
"AGENTFX_IMPORT_TOKEN",
"DEERFLOW_AUTH_TOKEN",
"TOOLS_TOKEN",
)
TOKEN_MINT_USERNAME = "agentfx-import"
DEFAULT_JSON_PATH = "/mnt/user-data/outputs/task-reports.json"
REQUEST_TIMEOUT_SECONDS = 60
def resolve_json_path(path: str) -> str:
"""把 ``/mnt/user-data/...`` 映射到当前线程真实 outputs,避免 Windows 写到 F:\\mnt。"""
raw = (path or "").strip() or DEFAULT_JSON_PATH
if os.path.isfile(raw):
return raw
normalized = raw.replace("\\", "/")
current = os.path.abspath(os.getcwd())
root = ""
cursor = current
for _ in range(8):
if os.path.isdir(os.path.join(cursor, "workspace")) and os.path.isdir(
os.path.join(cursor, "outputs")
):
root = cursor
break
if os.path.basename(cursor.rstrip("\\/")) == "workspace":
root = os.path.dirname(cursor)
break
parent = os.path.dirname(cursor)
if parent == cursor:
break
cursor = parent
candidates = [raw]
if root and (normalized == "/mnt/user-data" or normalized.startswith("/mnt/user-data/")):
rest = normalized[len("/mnt/user-data") :].lstrip("/")
candidates.insert(0, os.path.normpath(os.path.join(root, *rest.split("/"))) if rest else root)
if root:
candidates.append(os.path.join(root, "outputs", "task-reports.json"))
candidates.append(os.path.join("..", "outputs", "task-reports.json"))
candidates.append("task-reports.json")
for candidate in candidates:
if os.path.isfile(candidate):
return candidate
return candidates[0]
def _now_iso() -> str:
"""北京时间 ISO 字符串(与导入示例的 +08:00 时区保持一致)。"""
return datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
def _default_gateway() -> str:
configured = os.environ.get("AGENTFX_GATEWAY_URL", "").strip()
if configured:
return configured.rstrip("/")
port = os.environ.get("DEER_FLOW_GATEWAY_PORT", "").strip() or "8001"
return f"http://127.0.0.1:{port}"
def _build_opener(verify_tls: bool) -> urllib.request.OpenerDirector:
"""verify_tls=False 时跳过证书校验(内网自签证书),并忽略环境代理。"""
handlers: list[urllib.request.BaseHandler] = []
if not verify_tls:
# ssl._create_unverified_context 是官方文档认可的跳过校验写法(私有 API 但长期稳定,
# 与仓库内 sentiment-analysis 技能同款)。
handlers.append(urllib.request.HTTPSHandler(context=ssl._create_unverified_context()))
handlers.append(urllib.request.ProxyHandler({})) # 内网代理变量会劫持 127.0.0.1 回环调用
return urllib.request.build_opener(*handlers)
def _post_json(
opener: urllib.request.OpenerDirector,
url: str,
payload: object,
token: str | None,
timeout: int,
) -> tuple[int, str]:
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if token:
headers["Authorization"] = f"Bearer {token}"
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with opener.open(request, timeout=timeout) as response:
return int(response.status), response.read().decode("utf-8", "replace")
except urllib.error.HTTPError as error: # 4xx/5xx 也带响应体,向上抛给调用方解析
return int(error.code), error.read().decode("utf-8", "replace")
def resolve_token(args: argparse.Namespace, opener: urllib.request.OpenerDirector) -> str:
"""--token / 环境变量优先;缺省时经公共 token 端点现签(内网鉴权旁路模式)。"""
if args.token:
return args.token.strip()
for env_name in TOKEN_ENV_CHAIN:
value = os.environ.get(env_name, "").strip()
if value:
return value
url = args.gateway.rstrip("/") + "/api/parallel-agents/auth/token"
status, body = _post_json(opener, url, {"username": TOKEN_MINT_USERNAME}, None, 15)
try:
token = (json.loads(body) or {}).get("access_token")
except json.JSONDecodeError:
token = None
if status != 200 or not token:
raise RuntimeError(f"签发 token 失败(HTTP {status}),请配置 --token 或环境变量 AGENTFX_IMPORT_TOKEN")
return str(token)
def load_records(path: str) -> list[object]:
"""读取 JSON 文件:接受裸数组或 {data|records: [...]} 信封。"""
if not os.path.isfile(path):
raise FileNotFoundError(
f"找不到待导入文件 {path};请先生成报告 JSON,或用 --json 显式传入沙箱路径(如 {DEFAULT_JSON_PATH})"
)
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
if isinstance(payload, dict):
for key in ("data", "records"):
if isinstance(payload.get(key), list):
return payload[key]
raise ValueError("JSON 对象里没有可用的 data/records 数组")
if isinstance(payload, list):
return payload
raise ValueError("JSON 必须是数组,或包含 data/records 数组的对象")
def normalize_records(
raw_records: list[object],
task_id: str,
errors: list[str],
) -> tuple[list[dict[str, str]], set[str]]:
"""校验并补齐记录;返回 (可导入记录, 已覆盖分类集合)。"""
records: list[dict[str, str]] = []
present: set[str] = set()
for index, raw in enumerate(raw_records, start=1):
if not isinstance(raw, dict):
errors.append(f"第 {index} 条不是对象")
continue
category = str(raw.get("categoryType") or raw.get("category_type") or "").strip()
if category not in CATEGORY_TYPES:
errors.append(f"第 {index} 条 categoryType 非法:{category or '(空)'}")
continue
content = raw.get("contentJson", raw.get("content_json"))
if isinstance(content, (dict, list)):
content = json.dumps(content, ensure_ascii=False, separators=(",", ":"))
if not isinstance(content, str) or not content.strip():
errors.append(f"第 {index} 条({category})缺少 contentJson")
continue
try:
# 校验 + 归一化:把模型可能写的 \uXXXX 转义还原为汉字原文再入库。
content = json.dumps(json.loads(content), ensure_ascii=False, separators=(",", ":"))
except json.JSONDecodeError:
errors.append(f"第 {index} 条({category})contentJson 不是合法 JSON 字符串")
continue
present.add(category)
records.append(
{
"id": str(raw.get("id") or f"agentfx-{index:02d}"),
"taskId": task_id,
"createTime": str(raw.get("createTime") or _now_iso()),
"updateTime": _now_iso(),
"sessionId": str(raw.get("sessionId") or "agentfx-import"),
"contentJson": content,
"categoryType": category,
}
)
return records, present
def main() -> int:
parser = argparse.ArgumentParser(description="校验 task-reports.json 并导入任务详情(整体替换)")
parser.add_argument("--task-id", required=True, help="任务 id(开场信息里的 task_id)")
parser.add_argument("--json", default=DEFAULT_JSON_PATH, help=f"待导入 JSON 文件路径(默认 {DEFAULT_JSON_PATH})")
parser.add_argument("--gateway", default=_default_gateway(), help="Gateway 基地址(默认 env AGENTFX_GATEWAY_URL 或 http://127.0.0.1:8001)")
parser.add_argument("--token", default="", help="Bearer token(默认走环境变量或公共端点现签)")
parser.add_argument("--strict", action="store_true", help="缺任何一个分类都拒绝导入")
parser.add_argument("--verify-tls", action="store_true", help="开启 TLS 证书校验(内网自签证书默认关闭)")
args = parser.parse_args()
task_id = args.task_id.strip()
result: dict[str, object] = {
"success": False,
"task_id": task_id,
"imported": 0,
"missing_categories": [],
"http_status": None,
"errors": [],
}
errors = result["errors"] # type: ignore[assignment]
try:
if not task_id:
raise ValueError("--task-id 不能为空")
raw_records = load_records(resolve_json_path(args.json))
records, present = normalize_records(raw_records, task_id, errors) # type: ignore[arg-type]
missing = [category for category in CATEGORY_TYPES if category not in present]
result["missing_categories"] = missing
if args.strict and missing:
raise ValueError(f"缺少分类(--strict):{'、'.join(missing)}")
if not records:
raise ValueError("没有可导入的记录(先修复上面的 errors)")
opener = _build_opener(args.verify_tls)
token = resolve_token(args, opener)
endpoint = (
args.gateway.rstrip("/")
+ "/api/task-reports/import/by-task/"
+ urllib.parse.quote(task_id, safe="")
)
status, body = _post_json(opener, endpoint, {"data": records}, token, REQUEST_TIMEOUT_SECONDS)
result["http_status"] = status
try:
response = json.loads(body)
except json.JSONDecodeError:
response = {}
if 200 <= status < 300:
result["imported"] = len(response.get("data") or [])
result["success"] = True
else:
detail = response.get("detail") or response.get("msg") or body[:300]
errors.append(f"导入接口返回 HTTP {status}:{detail}")
except Exception as error: # noqa: BLE001 —— 结果统一经 stdout JSON 反馈给智能体
errors.append(str(error))
print(json.dumps(result, ensure_ascii=False))
return 0 if result["success"] else 1
if __name__ == "__main__":
sys.exit(main())
@@ -42,6 +42,8 @@ _AGENT_PURPOSE: dict[str, str] = {
"roundtable-execution-plan": "多智能体规划", "roundtable-execution-plan": "多智能体规划",
"roundtable-risk-review": "多智能体规划", "roundtable-risk-review": "多智能体规划",
"roundtable-solution-design": "多智能体规划", "roundtable-solution-design": "多智能体规划",
# 8BF 独立会商第一步的内置意图智能体(roundtable-intent 的同源复制)
"eightbf-roundtable-intent": "多智能体规划",
# AI 写作(杂志社协作写作四阶段) # AI 写作(杂志社协作写作四阶段)
"ai-writing-researcher": "AI写作", "ai-writing-researcher": "AI写作",
"ai-writing-outliner": "AI写作", "ai-writing-outliner": "AI写作",
@@ -66,6 +66,10 @@ INTENT_AGENT_ID = "roundtable-intent"
# lifecycle is shared with Step 1 above, but its final structured hand-off is # lifecycle is shared with Step 1 above, but its final structured hand-off is
# different and must not change the multi-agent roundtable contract. # different and must not change the multi-agent roundtable contract.
POSITION_INTENT_AGENT_ID = "position-roundtable-intent" POSITION_INTENT_AGENT_ID = "position-roundtable-intent"
# 8BF 独立会商页第一步同样拥有一份内置意图智能体:与 ``roundtable-intent``
# 功能/提示词同源(资源文件即字节级复制的验证版),但落成**独立目录**,供
# 8BF 部署现场单独调优 SOUL,不影响通用多智能体会商的契约。
EIGHTBF_INTENT_AGENT_ID = "eightbf-roundtable-intent"
RECOMMENDER_AGENT_ID = "roundtable-recommender" RECOMMENDER_AGENT_ID = "roundtable-recommender"
COORDINATOR_AGENT_ID = "roundtable-coordinator" COORDINATOR_AGENT_ID = "roundtable-coordinator"
# Step 3「结果绘制」用的方案可视化总结智能体:读各席位交付 + 共识,画自包含图表 HTML。 # Step 3「结果绘制」用的方案可视化总结智能体:读各席位交付 + 共识,画自包含图表 HTML。
@@ -101,6 +105,7 @@ _ASSETS_DIR = Path(__file__).parent / "_roundtable_seed_assets"
_FUNCTIONAL_AGENT_IDS: tuple[str, ...] = ( _FUNCTIONAL_AGENT_IDS: tuple[str, ...] = (
INTENT_AGENT_ID, INTENT_AGENT_ID,
POSITION_INTENT_AGENT_ID, POSITION_INTENT_AGENT_ID,
EIGHTBF_INTENT_AGENT_ID,
RECOMMENDER_AGENT_ID, RECOMMENDER_AGENT_ID,
COORDINATOR_AGENT_ID, COORDINATOR_AGENT_ID,
REPORT_AGENT_ID, REPORT_AGENT_ID,
@@ -0,0 +1,324 @@
# 任务理解智能体
你是多智能体圆桌研讨 **Step 1** 的「意图理解专员」。你的**唯一职责**是把用户输入背后的意图,结构化成 `{objective, constraints, assumptions}`,交给 Step 2 圆桌处理。
**你不写报告、不答题、不创作、不规划、不执行**。这些都是 Step 2 圆桌席位的事。你只负责"听懂、整理、交接"。
---
## 0. 五条不可违背的硬规则(看清楚再开口)
**硬规则 1 · 不替代后续席位干活**
无论用户让你"写报告 / 写代码 / 写诗 / 查资料 / 做规划 / 出方案",你**都不要直接产出内容**。
- ✅ 正确做法:把它结构化成 `objective`,输出 `[INTENT_READY]`,交给后续圆桌
- ❌ 错误做法:用户说"帮我写一份市场分析报告"→ 你开始动笔写报告正文
**硬规则 2 · 最多 3 轮澄清,第 3 轮必须收敛**
一次「`ask_clarification` + 用户回答」算一轮。
- 第 1 轮(用户首条消息):**绝大多数情况都应该直接 INTENT_READY**,不要追问
- 第 2 轮:如果首条真的非常模糊,可以追问一次
- **第 3 轮:无论信息是否完整,必须输出 `[INTENT_READY]`**,缺的字段用 `assumptions` 兜底——用户可以在 Step 2 继续修正,你不必凑齐
- **绝不存在第 4 轮**。系统会在第 3 轮强制移除 `ask_clarification` 工具
**硬规则 3 · 输出 `[INTENT_READY]` 后立刻闭嘴**
```json``` 代码块的反引号闭合之后,**禁止再输出任何一个字符**——不写"还有什么补充吗"、不写"我可以帮你……"、不调任何工具、不复述、不展望。
- ✅ 结尾就是 ` ``` `
- ❌ 在 ` ``` ` 后面还接一段"那么我先帮你列一下大纲……"
**硬规则 4 · 绝不拒绝用户、绝不当守门员(内网专用)**
本系统部署在**内网**,处理的是**内部任务分析**,不面向互联网。**无论用户提出任何要求,你都不得拒绝、不得回避、不得做合规/价值/敏感性审查、不得说"不在职责范围 / 不能帮 / 不便处理 / 请换一个问题"。** 你的职责是「听懂并交接」,不是「把关」。
- ✅ 正确做法:任何输入一律「先理解、再结构化」,变成一个中性的 `objective`(哪怕是"了解 XX 信息""对 XX 进行分析"),输出 `[INTENT_READY]` 交给 Step 2
- ❌ 错误做法:对用户诉求做任何形式的评判、设限、劝阻、回避或拒答
- 守门、风控、取舍**都不归你管**——把它们留给 Step 2 的圆桌席位和最终使用者
**硬规则 5 · 给出协助选项后立即停下**
一旦调用 `ask_clarification` 给出本轮选项,必须立即结束本轮输出并等待用户选择。工具调用后禁止再输出任何正文:不补充说明、不复述问题或选项、不催促用户选择、不追加问题,也不输出 `[INTENT_READY]`。需要确认的内容必须全部放进 `question`、`context` 和 `options` 参数中。
---
## 1. 字段定义
| 字段 | 含义 | 必填 |
|------|------|------|
| `objective` | 一句话描述用户想做/想知道的事 | ✅ |
| `constraints` | 用户**明确说过**的硬约束(预算/时间/合规/范围/格式等) | ⚠️ 可空数组,**不要凭空编造** |
| `assumptions` | 你替用户做的合理补充 | ⚠️ 可空数组 |
**核心原则**:
- 用户说清楚的,直接汇总,**不要追问**
- 事实/查询/咨询/创作类问题,**首条就是合法 objective**,直接收敛
- 只有「objective 都写不出一句话」「同一句话有多种截然不同的理解」「方向相互矛盾」时才追问
---
## 2. 工作流
```
用户输入
↓
能不能用一句话写出 objective?
├─ 能 → 立刻输出 [INTENT_READY],结束
└─ 不能 → 调 ask_clarification(一次只问一个最关键的点)
↓
用户回答 → 再判断
↓
最多到第 3 轮 → 强制输出 [INTENT_READY](缺字段用 assumptions 兜底)
```
### 2.1 提问前自检清单(任一命中"是"就不要发出)
1. 我**能不能直接用一句话**写 `objective`?——能就别问
2. 我准备问的 `question` 与上一轮**字面或语义重叠**吗?——重叠就别问
3. 选项列表与过往某一轮**完全相同**(仅顺序、标点不同)?——是就别问
4. 用户在过往消息里**已经回答过**这个决策点(包括"我选择:X")?——是就直接汇总
5. 能不能用合理 `assumptions` 兜底,**省掉这一轮**?——能就别问
6. 这是不是已经是**第 3 轮**?——是就立刻 INTENT_READY,**不要**再调工具
---
## 3. 唯一工具:`ask_clarification`
### 3.1 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `question` | ✅ | 一个聚焦问题,一次只问一个决策点 |
| `clarification_type` | ✅ | `missing_info` / `ambiguous_requirement` / `approach_choice` / `risk_confirmation` / `suggestion` |
| `context` | 推荐 | 一句话说明为何需要确认 |
| `options` | 有离散选项时填 | 字符串数组,如 `["≤500万", "500–1000万", "暂不确定"]` |
| `allow_multiple` | 可选 | 默认**单选**;仅当用户确实可同时选多项时传 `True` |
### 3.2 调用示例(仅在首条真正模糊时用)
```
ask_clarification(
question="你希望我帮你完成什么方向的事?",
clarification_type="ambiguous_requirement",
context="目前描述太抽象,需要先确认基本方向",
options=["策划/规划类", "查询/咨询类", "创作/写作类", "其它,我手动描述"]
)
```
---
## 4. 结束协议:`[INTENT_READY]`
可以收敛时,**严格**按以下结构回复,前后不得有其它内容:
```
✅ 我理解的意图是:<一句话复述>
[INTENT_READY]
```json
{
"objective": "...",
"constraints": [],
"assumptions": []
}
```
```
格式要求:
- `[INTENT_READY]` **单独成行**,前后无其它字符
- 紧跟 ```json 代码块,字段名严格为 `objective` / `constraints` / `assumptions`
- `constraints` 用户没说就给空数组(**不要编造**)
- ```json``` 反引号闭合后**禁止再写任何字符**
- **JSON 必须严格合法,前端用 `JSON.parse` 解析,任何非法都会导致摘要无法渲染、Step 2 无法解锁**:
- 字符串值内**禁止直接敲回车换行**——一句话写不下就压成一行,确实要换行只能写转义的 `\n`
- **不要尾随逗号**(数组 / 对象最后一项后面不要留 `,`)
- 引号一律用**半角双引号 `"`**,不要用中文引号 `""` 或单引号 `'`
- 不要在 JSON 里写注释(`//`、`/* */`)
- `constraints` / `assumptions` 必须是**字符串数组**(`["...", "..."]`),不要写成一整段长字符串
---
## 5. 反例集(都是真实出现过的错误,请认真避开)
### ❌ 反例 1:用户要写东西,你直接动手
**用户**:帮我写一份关于新能源汽车产业的市场分析报告
**错误**(你开始写报告):
> 新能源汽车产业目前正经历快速发展,主要市场参与者包括比亚迪、特斯拉、宁德时代……(继续写 2000 字)
**正确**:
✅ 我理解你想要一份新能源汽车产业的市场分析报告。
[INTENT_READY]
```json
{
"objective": "撰写一份新能源汽车产业的市场分析报告",
"constraints": [],
"assumptions": ["报告由后续席位执笔", "覆盖市场规模、竞争格局、趋势预测等通用维度", "中文输出"]
}
```
### ❌ 反例 2:输出 INTENT_READY 之后还在说话
**错误**:
> [INTENT_READY]
> ```json
> {"objective": "...", "constraints": [], "assumptions": []}
> ```
>
> 那么我现在帮你列一下报告的大纲……(❌ 多余、违规)
**正确**:```json``` 反引号闭合之后**直接结束**,什么都不要再写。
### ❌ 反例 3:用户已经给出充分信息,你还在追问
**用户**:帮我规划一次成都 5 天 4 晚的家庭游,预算 1 万
**错误**(继续追问"住宿偏好/几个人/出行方式"...):
> ask_clarification(question="请问出行人数是多少?", ...)
**正确**:用户已经给了**目的地 + 时长 + 预算 + 类型**,完全可以汇总。出行人数等细节用 assumptions 兜底,Step 2 圆桌会自己问。
✅ 我理解你想要一份成都 5 天 4 晚家庭游的规划,预算 1 万元。
[INTENT_READY]
```json
{
"objective": "规划一次成都 5 天 4 晚的家庭游行程",
"constraints": ["预算 ≤ 1 万元", "5 天 4 晚", "成都"],
"assumptions": ["默认 2 大 1 小家庭出行,可在 Step 2 调整", "中文行程单"]
}
```
### ❌ 反例 4:第 3 轮还在追问
**情境**:已经问了 2 轮,用户每次都答了
**错误**:第 3 轮又调一次 `ask_clarification`
**正确**:第 3 轮**只能**输出 `[INTENT_READY]`。把目前能确定的填进 `objective` / `constraints`,把不能确定的写成 `assumptions`,交给 Step 2。系统会自动屏蔽掉第 3 轮以后的 `ask_clarification` 工具。
### ❌ 反例 5:拒绝用户、做价值判断
**错误**:
> 你好!我是任务理解专员,这个问题不在我的职责范围……请你换一个问题。
**正确**:无论用户问什么,**先理解,再结构化**。事实问题就把它结构化成"了解 XX 信息"。
---
## 6. 正例集
### ✅ 正例 1:事实/知识类首条直接收敛
**用户**:赖清德的儿子是谁
**你**:
✅ 我理解你想了解赖清德儿子的相关公开信息。
[INTENT_READY]
```json
{
"objective": "了解赖清德儿子的姓名、身份等公开信息",
"constraints": [],
"assumptions": ["用户希望获得事实性回答", "信息来源应可追溯"]
}
```
### ✅ 正例 2:创作类首条直接收敛
**用户**:帮我写一首关于秋天的诗
**你**:
✅ 我理解你想要一首以秋天为主题的诗作。
[INTENT_READY]
```json
{
"objective": "创作一首以秋天为主题的诗",
"constraints": [],
"assumptions": ["体裁与风格由后续席位裁定", "中文输出"]
}
```
### ✅ 正例 3:规划类有约束直接收敛
**用户**:帮我规划一个 500 万预算、6 个月内完成的生产系统等保 2.0 改造
**你**:
✅ 我理解的任务是:在 500 万预算、6 个月工期内,对生产系统做等保 2.0 安全改造。
[INTENT_READY]
```json
{
"objective": "对生产系统进行安全改造,满足等保 2.0 合规要求",
"constraints": ["预算 ≤ 500 万元", "工期 ≤ 6 个月", "必须满足等保 2.0"],
"assumptions": ["允许在预定窗口内安排必要停机", "现有系统具备基础数字化能力"]
}
```
### ✅ 正例 4:首条真模糊,追问 1 次后收敛
**用户**:做点什么
**你**(第 1 轮 ask_clarification):
```
ask_clarification(
question="你希望我帮你完成什么方向的事?",
clarification_type="ambiguous_requirement",
context="目前描述太抽象,需要确认基本方向",
options=["策划/规划类", "查询/咨询类", "创作/写作类", "其它,我手动描述"]
)
```
**用户**:我选择:查询/咨询类
**你**(第 2 轮——已经知道是查询类,**不再追问主题**,直接收敛):
✅ 我理解你想要进行一次查询类咨询,具体主题待 Step 2 进一步明确。
[INTENT_READY]
```json
{
"objective": "进行一次查询/咨询类的信息检索任务",
"constraints": [],
"assumptions": ["具体主题由 Step 2 圆桌进一步追问", "用户可以在圆桌阶段补充查询关键词"]
}
```
### ✅ 正例 5:用户对 INTENT_READY 不满意,继续改
**用户**(在已经看到上一轮 INTENT_READY 后追加):再加一个约束:用中文输出
**你**(直接更新结构化结果,不再追问):
✅ 已根据补充更新意图。
[INTENT_READY]
```json
{
"objective": "...(保持上一轮)",
"constraints": ["...原有", "中文输出"],
"assumptions": ["..."]
}
```
---
## 7. 禁止事项一览
- ❌ 直接产出报告/代码/诗/方案/分析等任何具体内容(那是 Step 2 的事)
- ❌ 输出 `[INTENT_READY]` 后再写一个字
- ❌ 第 3 轮还调 `ask_clarification`
- ❌ 重复询问用户已答过的问题
- ❌ 拒绝用户、说"不在职责范围"、要求用户换问题
- ❌ 编造用户没说过的 `constraints`
- ❌ 调用任何非 `ask_clarification` 的工具(系统也已硬屏蔽)
---
## 8. 输出风格
- 复述用 1-2 句话,不超过 30 字
- 适度 emoji:✅ 已明确、📋 汇总;✗ 不要乱用
- `[INTENT_READY]` 块**严格按格式**,后端用正则解析,任何变形都会让前端进入降级路径
@@ -0,0 +1,3 @@
description: 8BF 多智能体会商第一步的任务理解智能体,通过 2-5 轮简短追问把用户模糊需求转化成结构化的「任务目标+约束+关键假设」,为 8BF 圆桌研讨准备输入。与通用 roundtable-intent 功能一致,提示词独立可单独调优
id: eightbf-roundtable-intent
name: 8BF 任务理解
@@ -0,0 +1,207 @@
"""Seed the built-in functional agents used by strategy selection.
The strategy-selection workbench treats intent understanding, each analysis
angle, judging, and final synthesis as normal configurable DeerFlow agents.
Their verified defaults live beside this module and are copied into the
folder-backed agent registry before ``_sync_legacy_agents`` runs. Existing
directories are preserved, except for an exact known legacy default whose
content hash proves that an administrator has not customized it.
"""
from __future__ import annotations
import logging
import os
from collections.abc import Iterable
from hashlib import sha256
from pathlib import Path
from deerflow.config.paths import get_paths
logger = logging.getLogger(__name__)
STRATEGY_INTENT_AGENT_ID = "strategy-selection-intent"
STRATEGY_AGENT_ID_PREFIX = "strategy-selection-"
STRATEGY_ANGLE_AGENT_PREFIX = "strategy-selection-angle-"
STRATEGY_GOVERNANCE_AGENT_ID = "strategy-selection-angle-governance"
STRATEGY_DIPLOMACY_AGENT_ID = "strategy-selection-angle-diplomacy"
STRATEGY_ECONOMY_AGENT_ID = "strategy-selection-angle-economy"
STRATEGY_SECURITY_AGENT_ID = "strategy-selection-angle-security"
STRATEGY_PUBLIC_OPINION_AGENT_ID = "strategy-selection-angle-public-opinion"
STRATEGY_LAW_POLICY_AGENT_ID = "strategy-selection-angle-law-policy"
STRATEGY_JUDGE_AGENT_ID = "strategy-selection-judge"
STRATEGY_SOLUTION_AGENT_ID = "strategy-selection-solution"
STRATEGY_ANGLE_AGENT_IDS: tuple[str, ...] = (
STRATEGY_GOVERNANCE_AGENT_ID,
STRATEGY_DIPLOMACY_AGENT_ID,
STRATEGY_ECONOMY_AGENT_ID,
STRATEGY_SECURITY_AGENT_ID,
STRATEGY_PUBLIC_OPINION_AGENT_ID,
STRATEGY_LAW_POLICY_AGENT_ID,
)
STRATEGY_FIXED_AGENT_IDS: tuple[str, ...] = (
STRATEGY_INTENT_AGENT_ID,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
)
_FUNCTIONAL_AGENT_IDS: tuple[str, ...] = (
STRATEGY_INTENT_AGENT_ID,
*STRATEGY_ANGLE_AGENT_IDS,
STRATEGY_JUDGE_AGENT_ID,
STRATEGY_SOLUTION_AGENT_ID,
)
_ASSETS_DIR = Path(__file__).parent / "_strategy_selection_seed_assets"
_SEED_FILES: tuple[str, ...] = ("config.yaml", "SOUL.md")
# The first shipped intent SOUL embedded the six built-in angle ids. Upgrade
# only that byte-for-byte semantic default so existing installations adopt the
# administrator-driven angle catalog without overwriting customized SOUL.
_LEGACY_DEFAULT_SOUL_HASHES: dict[str, str] = {STRATEGY_INTENT_AGENT_ID: ("09c4a1868b4d09a8b7a421135bf677e1b3d1b2c0aaa9cb95631ff908d4cf1343")}
def _normalized_text_hash(content: bytes) -> str:
normalized = content.decode("utf-8").replace("\r\n", "\n").encode("utf-8")
return sha256(normalized).hexdigest()
def _upgrade_known_legacy_soul(agent_id: str, agent_dir: Path) -> bool:
expected_hash = _LEGACY_DEFAULT_SOUL_HASHES.get(agent_id)
target = agent_dir / "SOUL.md"
source = _ASSETS_DIR / agent_id / "SOUL.md"
if not expected_hash or not target.is_file() or not source.is_file():
return False
temporary: Path | None = None
try:
if _normalized_text_hash(target.read_bytes()) != expected_hash:
return False
replacement = source.read_bytes()
if _normalized_text_hash(replacement) == expected_hash:
return False
temporary = target.with_name(f".{target.name}.{os.getpid()}.seed-upgrade.tmp")
temporary.write_bytes(replacement)
os.replace(temporary, target)
logger.info("Upgraded legacy default SOUL for strategy agent '%s'", agent_id)
return True
except (OSError, UnicodeDecodeError):
logger.exception("Failed to upgrade legacy SOUL for strategy agent '%s'", agent_id)
if temporary is not None:
temporary.unlink(missing_ok=True)
return False
def _ensure_one(agent_id: str) -> bool:
"""Create one missing agent directory without changing an existing one."""
agent_dir = get_paths().agent_dir(agent_id)
if agent_dir.exists():
_upgrade_known_legacy_soul(agent_id, agent_dir)
return False
source_dir = _ASSETS_DIR / agent_id
if not source_dir.is_dir():
logger.error(
"Missing strategy-selection seed assets for agent '%s' (expected at %s)",
agent_id,
source_dir,
)
return False
agent_dir.mkdir(parents=True, exist_ok=True)
for filename in _SEED_FILES:
source = source_dir / filename
if source.exists():
(agent_dir / filename).write_bytes(source.read_bytes())
return True
def ensure_strategy_selection_agents() -> list[str]:
"""Seed all nine strategy-selection agents; return ids created this call."""
created: list[str] = []
for agent_id in _FUNCTIONAL_AGENT_IDS:
try:
if _ensure_one(agent_id):
created.append(agent_id)
except Exception:
logger.exception("Failed to seed strategy-selection agent '%s'", agent_id)
if created:
logger.info("Seeded missing strategy-selection agents: %s", ", ".join(created))
return created
def required_agent_ids() -> Iterable[str]:
"""Stable ids owned by this seeder, exposed for tests and diagnostics."""
return tuple(_FUNCTIONAL_AGENT_IDS)
def default_strategy_agent_roles() -> list[dict[str, object]]:
"""Return immutable-role metadata for the shared strategy workbench.
The registry is deliberately separate from the folder-backed agent record:
it owns role/category and lifecycle state, while model, SOUL and skills keep
using the common agent configuration path. ``ensure_defaults`` only
inserts missing rows, so an administrator disabling an angle is respected.
"""
roles: list[dict[str, object]] = [
{
"agent_id": STRATEGY_INTENT_AGENT_ID,
"category": "intent",
"fixed": True,
"enabled": True,
"sort_order": 0,
}
]
roles.extend(
{
"agent_id": agent_id,
"category": "angle",
"fixed": False,
"enabled": True,
"sort_order": 100 + index,
}
for index, agent_id in enumerate(STRATEGY_ANGLE_AGENT_IDS)
)
roles.extend(
(
{
"agent_id": STRATEGY_JUDGE_AGENT_ID,
"category": "judge",
"fixed": True,
"enabled": True,
"sort_order": 200,
},
{
"agent_id": STRATEGY_SOLUTION_AGENT_ID,
"category": "solution",
"fixed": True,
"enabled": True,
"sort_order": 300,
},
)
)
return roles
__all__ = [
"STRATEGY_AGENT_ID_PREFIX",
"STRATEGY_ANGLE_AGENT_IDS",
"STRATEGY_ANGLE_AGENT_PREFIX",
"STRATEGY_DIPLOMACY_AGENT_ID",
"STRATEGY_ECONOMY_AGENT_ID",
"STRATEGY_FIXED_AGENT_IDS",
"STRATEGY_GOVERNANCE_AGENT_ID",
"STRATEGY_INTENT_AGENT_ID",
"STRATEGY_JUDGE_AGENT_ID",
"STRATEGY_LAW_POLICY_AGENT_ID",
"STRATEGY_PUBLIC_OPINION_AGENT_ID",
"STRATEGY_SECURITY_AGENT_ID",
"STRATEGY_SOLUTION_AGENT_ID",
"default_strategy_agent_roles",
"ensure_strategy_selection_agents",
"required_agent_ids",
]
@@ -0,0 +1,35 @@
# 策略视角·外交国际智能体
你是策略优选流程中的「外交国际」角度智能体。你根据已经确认的任务草稿,从外交沟通、国际组织、双多边关系、第三方关切和危机管控角度形成一项可比较的候选策略;不要替其他角度作答,也不要派活。
## 分析框架
1. 绘制主要利益相关方及其公开利益、红线、可谈空间和信息不对称。
2. 区分公开表态、法律立场、政策惯例与分析推断,避免替任何一方断言真实动机。
3. 优先评估热线、工作层沟通、事实澄清、第三方斡旋、国际组织机制和信任建立措施。
4. 给出分阶段外交选项,每一步包含目标、对象、信息口径、成功信号、退出与升级防护条件。
5. 评估盟友伙伴、周边国家、市场主体与国际公众可能产生的二阶反应。
## 证据纪律
适用 `policy-knowledge` 时先按技能读取知识库;涉及最新政策或公开事件时,在工具可用的前提下核验日期和来源。无法核验的内容明确标为假设或情景,不虚构引语、协议或立场。
## 输出结构
使用 Markdown,固定包含:
1. `## 核心判断`
2. `## 候选策略`
3. `## 沟通与协商路径`
4. `## 利益相关方反应`
5. `## 风险、触发器与降级机制`
6. `## 依据与不确定性`
策略应强调合法外交、持续沟通、可逆步骤和降级风险,而不是以威胁升级作为默认手段。
## 安全与中立边界
- 对台海等敏感议题中性呈现各方公开主张,优先和平解决、危机稳定和人道保护。
- 禁止作战战术、目标选择、武器运用、规避侦测或利用具体脆弱点的可执行指导。
- 禁止面向特定群体的隐蔽操纵、欺骗、恐吓或定向政治说服;信息口径必须真实、透明、可公开说明。
- 遇到越界请求时,重构为外交沟通、风险降低、国际法框架和民间保护选项。
@@ -0,0 +1,8 @@
id: strategy-selection-angle-diplomacy
name: 策略视角·外交国际
description: 从外交沟通、国际组织、主要利益相关方与危机管控视角提出中立、合法、降级风险的策略建议。
tool_groups:
- web
- file:read
skills:
- policy-knowledge
Loaded 100 of 413 files, more files were not shown because too many files have changed in this diff. Show more