From 70da7c56491f627bffe447cebb1757d27c699e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sat, 15 Aug 2026 12:51:58 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gpt-researcher-deerflow-集成开发方案.md | 62 +- .../workspace/workspace-nav-chat-list.tsx | 14 +- .../src/core/page-layout/sidebar-menu.ts | 7 + frontend-web/src/pages/DeepResearchPage.tsx | 1378 +++++++++++++++++ frontend-web/src/pages/StrategyRoutes.tsx | 2 + frontend-web/src/pages/WorkspaceRoutes.tsx | 11 + .../deep-research/DeepResearchWorkbench.tsx | 1124 ++++++++++++++ .../strategy-components/api/deep-research.ts | 639 ++++++++ .../config.20260814T160622756141Z.yaml | 582 +++++++ offline-backend-20260512/backend/CLAUDE.md | 63 +- offline-backend-20260512/backend/README.md | 108 +- .../backend/app/gateway/app.py | 228 +-- .../gateway/deep_research_job_dispatcher.py | 158 ++ .../app/gateway/deep_research_job_executor.py | 558 +++++++ .../app/gateway/deep_research_live_hub.py | 60 + .../backend/app/gateway/deps.py | 56 + .../app/gateway/routers/deep_research.py | 948 ++++++++++++ offline-backend-20260512/backend/config.yaml | 26 + .../deerflow/agents/deep_research/UPSTREAM.md | 56 + .../deerflow/agents/deep_research/__init__.py | 26 + .../agents/deep_research/adapters/__init__.py | 19 + .../deep_research/adapters/artifacts.py | 159 ++ .../agents/deep_research/adapters/context.py | 130 ++ .../deep_research/adapters/event_sink.py | 152 ++ .../agents/deep_research/adapters/image.py | 233 +++ .../agents/deep_research/adapters/llm.py | 472 ++++++ .../agents/deep_research/adapters/material.py | 75 + .../adapters/material_provider.py | 541 +++++++ .../agents/deep_research/cancellation.py | 135 ++ .../deerflow/agents/deep_research/config.py | 173 +++ .../deerflow/agents/deep_research/engine.py | 63 + .../deerflow/agents/deep_research/events.py | 157 ++ .../agents/deep_research/runners/__init__.py | 19 + .../agents/deep_research/runners/basic.py | 439 ++++++ .../agents/deep_research/runners/deep.py | 407 +++++ .../agents/deep_research/runners/detailed.py | 435 ++++++ .../agents/deep_research/runners/images.py | 167 ++ .../deep_research/runners/multi_agent.py | 434 ++++++ .../deep_research/runners/report_stream.py | 87 ++ .../deerflow/agents/deep_research/types.py | 180 +++ .../deep_research_events/__init__.py | 33 + .../persistence/deep_research_events/model.py | 43 + .../persistence/deep_research_events/sql.py | 143 ++ .../deep_research_jobs/__init__.py | 48 + .../persistence/deep_research_jobs/model.py | 89 ++ .../persistence/deep_research_jobs/sql.py | 506 ++++++ .../deep_research_messages/__init__.py | 33 + .../deep_research_messages/model.py | 35 + .../persistence/deep_research_messages/sql.py | 105 ++ .../deep_research_sessions/__init__.py | 40 + .../deep_research_sessions/model.py | 62 + .../persistence/deep_research_sessions/sql.py | 222 +++ .../deep_research_sources/__init__.py | 33 + .../deep_research_sources/model.py | 61 + .../persistence/deep_research_sources/sql.py | 166 ++ .../versions/20260620_01_placeholder.py | 34 + .../20260620_02_chain_gather_first.py | 4 +- .../deerflow/persistence/models/__init__.py | 10 + .../persistence/task_buttons/model.py | 6 +- 59 files changed, 12108 insertions(+), 148 deletions(-) create mode 100644 frontend-web/src/pages/DeepResearchPage.tsx create mode 100644 frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx create mode 100644 frontend-web/src/strategy-components/api/deep-research.ts create mode 100644 offline-backend-20260512/backend/.model-config-backups/config.20260814T160622756141Z.yaml create mode 100644 offline-backend-20260512/backend/app/gateway/deep_research_job_dispatcher.py create mode 100644 offline-backend-20260512/backend/app/gateway/deep_research_job_executor.py create mode 100644 offline-backend-20260512/backend/app/gateway/deep_research_live_hub.py create mode 100644 offline-backend-20260512/backend/app/gateway/routers/deep_research.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/UPSTREAM.md create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/artifacts.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/context.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/event_sink.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/image.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/llm.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/material.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/adapters/material_provider.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/cancellation.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/config.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/engine.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/events.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/basic.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/deep.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/detailed.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/images.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/multi_agent.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/runners/report_stream.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/types.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_events/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_events/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_events/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_jobs/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_jobs/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_jobs/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_messages/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_messages/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_messages/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sessions/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sessions/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sessions/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sources/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sources/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/deep_research_sources/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260620_01_placeholder.py diff --git a/docs/gpt-researcher-deerflow-集成开发方案.md b/docs/gpt-researcher-deerflow-集成开发方案.md index 8491e367..8bef24a9 100644 --- a/docs/gpt-researcher-deerflow-集成开发方案.md +++ b/docs/gpt-researcher-deerflow-集成开发方案.md @@ -1,6 +1,6 @@ # GPT Researcher 接入 DeerFlow 的可实施开发方案 -> 文档状态:设计稿,可进入任务拆分与开发评审 +> 文档状态:持续实现中;本次已补齐报告实时写作、对话工作台与浏览器端 Word 导出 > 编写日期:2026-08-14 > GPT Researcher 核对基线:[`5d84d2f5553e70a2765a8ff3a0d2672d60437ce8`](https://github.com/assafelovic/gpt-researcher/tree/5d84d2f5553e70a2765a8ff3a0d2672d60437ce8) > 上游本地代码:`F:\react01\deeflow-code\gpt-researcher`(当前 HEAD 与上述基线一致) @@ -51,7 +51,7 @@ GPT Researcher 当前真正直接使用 LangGraph 的生产代码主要在: 2. 保持上游研究流程和报告生成方法的主体结构,减少功能偏差。 3. 在四个边界上使用 DeerFlow Adapter:模型、研究材料、事件、产物。 4. 搜索结果一律由 DeerFlow 提供完整正文和元数据,使 GPT Researcher 不再自行联网、抓取或调用 MCP。 -5. 新建独立前端入口 `/page/deep-research`,不进入 `open-canvas`,不修改 AI 写作状态机。 +5. 新建独立前端入口 `/page/strategy/qa/deep-research`,不进入 `open-canvas`,不修改 AI 写作状态机。 ### 2.3 第一版的能力范围 @@ -66,7 +66,7 @@ GPT Researcher 当前真正直接使用 LangGraph 的生产代码主要在: - 后台执行、刷新恢复、断线重连、取消、重试、人工审核后继续。 - 历史会话、报告追问、产物下载。 -PDF、DOCX 自动导出建议做成可选能力。原因不是 LangGraph,而是 DeerFlow 当前依赖中没有上游导出使用的全部库。核心研究功能可以保持零 LangGraph 依赖变更;如果部署方需要服务端 PDF/DOCX,再启用独立的 `deep-research-export` 可选依赖组。 +PDF 自动导出仍建议做成可选能力。DOCX 已改为复用前端现有 `docx` 依赖在浏览器本地生成:完成态报告导出为 Word 时会附带收集到的参考来源,不新增 Python 或 LangGraph 依赖。私有报告配图暂不嵌入 Word;若后续需要嵌图,应通过鉴权产物接口取得 Blob 后写入 `ImageRun`。 --- @@ -104,7 +104,7 @@ PDF、DOCX 自动导出建议做成可选能力。原因不是 LangGraph,而 | 详细报告 | 总体研究后按子主题并行展开 | `DetailedReport` | 子主题任务并行,最终合并引言和结论 | | 深度研究 | 按 breadth/depth 递归探索 | `DeepResearchSkill` | 有界递归、分支事件、统一去重和取消 | | 多智能体编审 | 有计划、复核、修订、事实核查 | `ChiefEditor` + `EditorAgent` | LangGraph 1.x 图,支持暂停和恢复 | -| 自定义报告 | 用户给定报告结构或提示 | upstream custom report/prompt | 配置白名单与模板变量,不允许任意系统指令覆盖 | +| 自定义报告 | 用户给定报告结构或提示 | upstream custom report/prompt | 已支持:受限 `custom_outline` 保存到配置快照;标题/编号列表决定详细报告和多智能体章节,其他模式作为受保护的写作结构约束 | ### 4.2 用户可见能力 @@ -113,9 +113,9 @@ PDF、DOCX 自动导出建议做成可选能力。原因不是 LangGraph,而 3. 查看自动生成的研究计划。 4. 在开启人工审核时,修改或批准研究计划。 5. 实时查看:当前阶段、搜索问题、材料数量、子主题、递归分支、写作和核查进度。 -6. 查看来源列表、来源正文摘要、来源类型、时间、相关度和被引用情况。 +6. 查看来源列表、来源正文摘要、来源类型、时间、相关度和被引用情况;研究停止后可调整后续追问使用的来源集合。 7. 查看流式报告预览。 -8. 下载 Markdown、HTML,以及部署已启用时的 PDF、DOCX。 +8. 下载 Markdown、HTML 和浏览器本地生成的 Word;PDF 仍按部署能力决定是否开放。 9. 研究完成后继续针对本次报告和材料追问。 10. 取消正在运行的任务;刷新页面后恢复状态;服务重启后自动续跑或重新领取。 11. 查看、重命名、删除历史研究会话。 @@ -179,6 +179,8 @@ F:\react01\deeflow-code\gpt-researcher 当前版本固定为 `5d84d2f5553e70a2765a8ff3a0d2672d60437ce8`。本章所有 `gpt_researcher/...`、`multi_agents/...` 和 `backend/...` 路径均相对于该目录;实施时以该本地目录作为 vendoring 的唯一来源,不从浮动分支重新下载文件。 +当前实现的可执行代码是对这些控制流的 DeerFlow 适配,而非直接 import 上游包(上游 Provider、检索/抓取和 MCP 栈不能越过 DeerFlow 的安全边界)。固定 commit、许可证、逐文件映射、不可突破的适配约束和后续直接 vendoring 的补充义务,见 `offline-backend-20260512/backend/packages/harness/deerflow/agents/deep_research/UPSTREAM.md`。 + 最关键的源码位置如下: | 用途 | 本地文件 | @@ -365,6 +367,17 @@ offline-backend-20260512/backend/app/gateway/ ### 7.4 前端 +当前 Phase 2 已落地为与现有 Vite + React 页面目录一致的轻量实现: + +```text +frontend-web/src/pages/DeepResearchPage.tsx +frontend-web/src/strategy-components/api/deep-research.ts +frontend-web/src/pages/StrategyRoutes.tsx +frontend-web/src/core/page-layout/sidebar-menu.ts +``` + +其中 `DeepResearchPage` 同时承载创建表单、历史会话、对话式 SSE 研究过程、流式报告草稿、来源库、报告预览和 MD/HTML/Word 下载。后续在页面复杂度明显增加时,可按下方的目标目录拆分;不能为了目录形式而重复实现另一套状态或 API client。 + ```text frontend-web/src/deep-research/ ├── routes/DeepResearchRoutes.tsx @@ -389,10 +402,10 @@ frontend-web/src/deep-research/ 路由修改: -- 在 `frontend-web/src/pages/PageRoutes.tsx` 增加: +- 当前实现已在 `frontend-web/src/pages/StrategyRoutes.tsx` 增加: ```tsx -} /> +} /> ``` - 在 `frontend-web/src/core/page-layout/sidebar-menu.ts` 的「问答管理」下增加: @@ -401,8 +414,8 @@ frontend-web/src/deep-research/ { id: "qa-deep-research", label: "深度研究", - icon: Search, - path: "/page/deep-research", + icon: Telescope, + path: "/page/strategy/qa/deep-research", } ``` @@ -1207,17 +1220,13 @@ resume 请求示例: ### 17.1 路由 ```text -/page/deep-research 创建页 + 历史列表 -/page/deep-research/:sessionId 研究工作台 +/page/strategy/qa/deep-research 创建页、历史列表和研究工作台 ``` -`DeepResearchRoutes.tsx`: +当前版本使用单页会话选择,不需要再维护一套嵌套路由: ```tsx - - } /> - } /> - +} /> ``` ### 17.2 创建页 @@ -1519,6 +1528,8 @@ tests/test_deep_research_user_isolation.py ### Phase 1:标准报告后端 MVP +实现状态(2026-08-14):已完成领域配置、材料/LLM/上下文/产物 Adapter、Basic runner、五类持久化表、租约式 dispatcher/executor、Gateway API 和定向测试。当前 Basic runner 是 DeerFlow 原生实现;上游 GPT Researcher 的选定源码尚未 vendoring,仍应在 Phase 3/4 的详细报告、递归研究和多智能体实现中按本文第 6 章从本地固定 commit 引入。 + 任务: 1. 新增 config/types/cancellation/events。 @@ -1538,6 +1549,8 @@ tests/test_deep_research_user_isolation.py ### Phase 2:独立前端页面 +实现状态(2026-08-14):已完成路由与菜单入口、创建表单(将所选 mode 写入后端 config)、会话历史、作业 SSE、取消/重新开始、实时来源刷新、报告预览和 MD/HTML 下载。本轮将右侧工作区改为对话式:原始课题、研究过程、流式报告草稿、最终报告与报告追问按对话顺序展示;报告 `report_chunk` 依赖持久化 SSE 重放,刷新时不重新调用模型。页面与 AI 写作页面、Context 和路由完全隔离。 + 任务: 1. 增加路由和菜单。 @@ -1553,12 +1566,14 @@ tests/test_deep_research_user_isolation.py ### Phase 3:详细报告与 Deep Research +实现状态(2026-08-14):已完成 `DetailedRunner` 和 `DeepRunner`,并由 `DeepResearchEngine` 分发;Gateway capabilities 与创建校验已开放 `detailed`、`deep`,前端模式选择与递归阶段事件也已接入。详细报告按本地上游 `gpt-researcher/backend/report_type/detailed_report/detailed_report.py` 的“子主题规划 → 分主题研究 → 章节汇编”控制流做 Adapter 化实现;递归模式按 `gpt-researcher/gpt_researcher/skills/deep_research.py` 的“分支 → learning → follow-up”控制流适配。二者不直接调用上游的模型、检索器或抓取器,而是只通过 DeerFlow 的 `AdapterBundle` 复用现有材料收集和 LangGraph 1.x 运行时。 + 任务: 1. 接入 Detailed runner。 2. 接入 Deep runner并修复已知空查询问题。 3. 增加全局预算、分支事件和来源去重。 -4. 增加高级配置 UI。 +4. 增加高级配置 UI。(已完成:仅暴露已由运行器使用的语言、语气、预算、并发、模式专属参数和 `custom_outline`。) 验收: @@ -1569,6 +1584,8 @@ tests/test_deep_research_user_isolation.py ### Phase 4:LangGraph 多智能体与人工审核 +实现状态(2026-08-14):已完成 `MultiAgentRunner`。它按本地上游 `gpt-researcher/multi_agents/agents/orchestrator.py` 的 editor → researcher → writer → reviewer 结构,用当前已安装的 LangGraph 1.x `StateGraph` 编排,不引入上游的 retriever、scraper 或额外 agent 框架。初次运行产生 plan-review `awaiting_input` 事件;`RecordingEventSink` 将 interrupt、计划和租约状态写入既有 durable job,前端可批准或提交修订,`POST /api/deep-research/jobs/{id}/resume` 重新入队后从事件日志恢复原计划并继续执行。审阅不通过时按 `max_revision_loops` 有界回到改写节点。 + 任务: 1. vendor LangGraph 多智能体选定代码。 @@ -1585,10 +1602,12 @@ tests/test_deep_research_user_isolation.py ### Phase 5:追问、图片和完整导出 +实现状态(2026-08-14):图片生成和报告追问子项已完成。由于当前 DeerFlow 部署本身只有识图、没有可直接复用的文生图 Provider,深度研究新增了部署级 `deep_research.image_generation`(OpenAI 兼容 `POST {base_url}/images/generations`)适配器;默认关闭且不保存任何密钥到 session/job。开启后 capabilities 才向前端开放「为报告生成配图」开关。四种 Runner 在文本报告完成后共用 `runners/images.py`:最多四张、优先报告二级章节、请求 `b64_json`,图片保存为隐藏 runtime thread 的受限 `research-image-01..04.(png|jpg|webp)` 产物,并以 `deep-research://` 私有 URI 插入 Markdown。前端经携带现有鉴权头的会话产物接口下载成 Blob 再预览,因此不会泄露 hidden thread id,也不依赖可能过期的第三方 URL;图片服务未配或单张失败只产生可恢复 warning,文字报告照常完成。完成态报告还支持 owner-scoped `GET /messages`、`POST /chat`:问题与回答写入既有消息表,模型仅接收本报告、已选来源和近期消息;回答中的 `[[source:id]]` 仅在 id 属于该会话来源时保留并写入 citation ids。用户可在研究任务停止后以 `PATCH /sessions/{id}/sources/{source_id}` 调整“已选来源”,该集合会立即约束后续追问但不会倒改已完成的报告;运行中返回 409,以免与自动策展写入竞态。`allow_new_research=true` 明确返回 422,不会暗中重新搜集信息。DOCX 已通过浏览器端 `docx` 生成,无需部署新离线 wheel;PDF 与含私有图片嵌入的服务端文档导出仍是后续可选子项。 + 任务: -1. 报告追问与消息持久化。 -2. DeerFlow ImageProvider 适配和图片插入。 +1. 报告追问与消息持久化。(已完成) +2. DeerFlow ImageProvider 适配和图片插入。(已完成) 3. 可选 PDF/DOCX 导出依赖与 capability。 4. 完善配额、监控和管理配置。 @@ -1647,7 +1666,8 @@ tests/test_deep_research_user_isolation.py | 事实核查回环 | 复用,增加最大回环和来源约束 | | 引言、正文、结论 | ReportGenerator 复用 | | 引用和参考链接 | Markdown processing 复用并绑定 source id | -| 图片生成与插入 | 后续接 DeerFlow ImageProvider | +| 报告完成后的追问 | 已支持:仅报告/已选来源/近期消息,持久化 source-id 引用,不会隐式重新搜索;停止后可人工调整已选来源 | +| 图片生成与插入 | 已支持:部署级 OpenAI 兼容 ImageProvider,可选开关、受鉴权产物、报告内预览;未配置时优雅跳过 | | Markdown | 核心支持 | | HTML | 核心支持并清洗 | | PDF/DOCX | 可选依赖能力 | diff --git a/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx b/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx index 9957103c..fc6e08af 100644 --- a/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx +++ b/frontend-web/src/components/workspace/workspace-nav-chat-list.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/sidebar"; import { useI18n } from "@/core/i18n/hooks"; // import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react"; -import { BookOpen, BotIcon, BrainIcon, BriefcaseBusiness, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Users } from "lucide-react"; +import { BookOpen, BotIcon, BrainIcon, BriefcaseBusiness, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react"; import { KnowledgeRagToggle } from "./knowledge-rag-toggle"; import { SidebarCollapseButton } from "./workspace-header"; @@ -172,6 +172,18 @@ export function WorkspaceNavChatList({ + + + + + 深度研究 + + + = { + initializing: "初始化", + planning: "规划子问题", + collecting: "收集材料", + curating: "筛选素材", + compressing: "压缩上下文", + deepening: "递归深挖", + writing: "撰写报告", + reviewing: "审查报告", + illustrating: "生成报告配图", + exporting: "导出结果", + done: "完成", +}; + +const STATUS_BADGES: Record = { + draft: { label: "草稿", variant: "outline" }, + running: { label: "进行中", variant: "default" }, + awaiting_input: { label: "等待输入", variant: "secondary" }, + completed: { label: "已完成", variant: "default" }, + failed: { label: "失败", variant: "destructive" }, + cancelled: { label: "已取消", variant: "outline" }, +}; + +const DEFAULT_AVAILABLE_MODES: ResearchMode[] = ["basic", "quick", "detailed", "deep", "multi_agent"]; + +type AdvancedResearchOptions = { + language: "zh-CN" | "en-US"; + tone: ResearchTone; + maxSearchResults: string; + maxIterations: string; + maxConcurrency: string; + maxContextWords: string; + maxSubtopics: string; + deepBreadth: string; + deepDepth: string; + maxRevisionLoops: string; + maxGeneratedImages: string; + thinkingEnabled: boolean; + customOutline: string; +}; + +const DEFAULT_ADVANCED_OPTIONS: AdvancedResearchOptions = { + language: "zh-CN", + tone: "objective", + maxSearchResults: "8", + maxIterations: "3", + maxConcurrency: "4", + maxContextWords: "24000", + maxSubtopics: "8", + deepBreadth: "4", + deepDepth: "2", + maxRevisionLoops: "2", + maxGeneratedImages: "2", + thinkingEnabled: false, + customOutline: "", +}; + +function boundedInteger(value: string, fallback: number, min: number, max: number): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); +} + +function buildResearchConfig( + mode: ResearchMode, + generateImages: boolean, + options: AdvancedResearchOptions, +): Record { + const config: Record = { + mode, + generate_images: generateImages, + language: options.language, + tone: options.tone, + thinking_enabled: options.thinkingEnabled, + max_search_results_per_query: boundedInteger(options.maxSearchResults, 8, 1, 20), + max_iterations: boundedInteger(options.maxIterations, 3, 1, 10), + max_concurrency: boundedInteger(options.maxConcurrency, 4, 1, 8), + max_context_words: boundedInteger(options.maxContextWords, 24_000, 500, 120_000), + }; + + if (mode === "detailed" || mode === "multi_agent") { + config.max_subtopics = boundedInteger(options.maxSubtopics, 8, 1, 20); + } + if (mode === "deep") { + config.deep_breadth = boundedInteger(options.deepBreadth, 4, 1, 8); + config.deep_depth = boundedInteger(options.deepDepth, 2, 1, 4); + } + if (mode === "multi_agent") { + config.max_revision_loops = boundedInteger(options.maxRevisionLoops, 2, 0, 5); + } + if (generateImages) { + config.max_generated_images = boundedInteger(options.maxGeneratedImages, 2, 1, 4); + } + if (options.customOutline.trim()) { + config.custom_outline = options.customOutline.trim().slice(0, 4_000); + } + return config; +} + +function researchExportBaseName(session: DeepResearchSession): string { + const title = (session.title || session.query || "研究报告") + .replace(/[\\/:*?"<>|]/g, "-") + .replace(/\s+/g, " ") + .trim(); + return `深度研究-${title.slice(0, 80) || "研究报告"}`; +} + +function buildResearchWordMarkdown(session: DeepResearchSession, sources: DeepResearchSource[]): string { + const references = sources + .map((source, index) => { + const title = source.title?.trim() || `来源 ${index + 1}`; + return source.url ? `${index + 1}. ${title}\n ${source.url}` : `${index + 1}. ${title}`; + }) + .join("\n"); + const metadata = `> 研究课题:${session.query}\n> 导出时间:${new Date().toLocaleString("zh-CN")}\n`; + return [metadata, session.reportMarkdown, references ? `## 参考来源\n\n${references}` : ""].filter(Boolean).join("\n\n"); +} + +// ── main component ────────────────────────────────────────────────────────── + +/** + * Kept temporarily as the implementation reference for older direct links. + * The actual page is now the conversation workbench exported below. + */ +function LegacyDeepResearchPage() { + const [selectedId, setSelectedId] = useState(null); + const [query, setQuery] = useState(""); + const [mode, setMode] = useState("basic"); + const [generateImages, setGenerateImages] = useState(false); + const [advancedOptions, setAdvancedOptions] = useState( + DEFAULT_ADVANCED_OPTIONS, + ); + + const sessions = useResearchSessions(); + const session = useResearchSession(selectedId); + const capabilities = useCapabilities(); + const actions = useResearchActions(); + + const availableModes = useMemo( + () => capabilities.data?.modes ?? DEFAULT_AVAILABLE_MODES, + [capabilities.data?.modes], + ); + + useEffect(() => { + if (!availableModes.includes(mode)) { + setMode(availableModes[0] ?? "basic"); + } + }, [availableModes, mode]); + + const imageGenerationAvailable = capabilities.data?.imageGeneration?.available === true; + + useEffect(() => { + if (!imageGenerationAvailable) setGenerateImages(false); + }, [imageGenerationAvailable]); + + const handleCreate = useCallback(async () => { + const trimmed = query.trim(); + if (!trimmed) return; + try { + const created = await actions.create.mutateAsync({ + query: trimmed, + config: buildResearchConfig(mode, generateImages, advancedOptions), + }); + setQuery(""); + setSelectedId(created.id); + // Auto-start the research job. + await actions.startJob.mutateAsync(created.id); + toast.success("深度研究已启动"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "启动研究失败"); + } + }, [query, mode, generateImages, advancedOptions, actions]); + + const handleStart = useCallback( + async (sessionId: string) => { + try { + await actions.startJob.mutateAsync(sessionId); + toast.success("深度研究已启动"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "启动研究失败"); + } + }, + [actions], + ); + + const handleDelete = useCallback( + async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + try { + await actions.delete.mutateAsync(id); + if (selectedId === id) setSelectedId(null); + toast.success("已删除"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "删除失败"); + } + }, + [actions, selectedId], + ); + + return ( +
+ {/* ── left panel: form + history ───────────────────────────────── */} +
+
+
+ +

深度研究

+
+