diff --git a/.gitignore b/.gitignore index e22a4ef0..34a6d1fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ # Workspace/editor -.vscode/* -!.vscode/launch.json -!.vscode/tasks.json -!.vscode/extensions.json -!.vscode/settings.json -frontend-web/.vscode/ +.vscode/* +!.vscode/launch.json +!.vscode/tasks.json +!.vscode/extensions.json +!.vscode/settings.json +frontend-web/.vscode/ node_modules/ # Claude Code local-only settings (per-user permissions, not shared) .claude/settings.local.json @@ -56,9 +56,9 @@ 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/.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/ @@ -66,14 +66,14 @@ 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 +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/ @@ -84,22 +84,23 @@ 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/ +skills/ +hermes-agent-main/ .gitignore -offline-backend-20260512/backend/tests/* -!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py -!offline-backend-20260512/backend/tests/test_health_auth_bypass.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 -# 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 -deploy +offline-backend-20260512/backend/tests/* +!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py +!offline-backend-20260512/backend/tests/test_health_auth_bypass.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 +# 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 +deploy .history # Cloned third-party repos (local only) @@ -108,22 +109,22 @@ 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 +# 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 @@ -140,4 +141,4 @@ offline-backend-20260512/backend/uv.lock __pycache__/ # misc -.DS_Store +.DS_Store diff --git a/AGENTS.md b/AGENTS.md index 5cbfebb5..81ab06d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,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) - **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat`, sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` reuses the engine (`AIWritingContext` + `useStream` + `AIWritingDraftPanel`); **idle keeps the classic setup layout** (`WritingSetupChat` + collapsible form) and switches to the chat workbench once writing starts; left panel `components/ai-writing-chat/WritingChatPanel.tsx` = **unified chronological feed** (`ChatFlowTimeline.tsx`, chat bubbles anchor-interleaved into the progress timeline so triggered events render below the user's message) + button-only intervention cards (`ChatInterventionCards.tsx`, inline textareas removed) + bottom unified smart composer. Intent resolution: frontend fast path + `POST /api/ai-writing/sessions/{id}/intent` → `deerflow/agents/ai_writing/intent_router.py` (per-pause action whitelist, high-cost confidence gates, payload cleaning); supports QA bypass, clarify follow-ups, control intents, and material-stage auto re-search + pending-intent resume. Tests `tests/test_ai_writing_intent.py`; plan doc `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`. - **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes). -- **菜单管理 (Menu Management, admin-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-`). 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). +- **菜单管理 (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-`). 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`. - **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答直连外部 AG-UI 网关(POST SSE),不经过 LangGraph,会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`),核心模块 `src/core/sentiment-agent/`。 - **敏感词管理 (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/`。 diff --git a/CLAUDE.md b/CLAUDE.md index c0c669d4..a62332da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **定时任务「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`. - **轻应用中心 (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-`). 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). +- **菜单管理 (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-`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** left-nav is a separate overlay: `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`. - **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`. - **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels. - **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。 diff --git a/frontend-web/.env.example b/frontend-web/.env.example index 68e79a97..415c1a07 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -56,4 +56,7 @@ # VITE_STATIC_WEBSITE_ONLY= # 部署级紧凑导航:true 时固定收起工作台左侧栏,并隐藏换肤与全量/简洁模式切换。 # VITE_FORCE_COLLAPSED_SIDEBAR=false +# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。 +# 不填或留空时按类型 A,继续使用原来的简介菜单数据。 +# VITE_COMPACT_MENU_TYPE=A # VITE_GITHUB_OAUTH_TOKEN= diff --git a/frontend-web/docs/angular-智能体对话对接文档.md b/frontend-web/docs/angular-智能体对话对接文档.md index 34f0b92b..05bc4855 100644 --- a/frontend-web/docs/angular-智能体对话对接文档.md +++ b/frontend-web/docs/angular-智能体对话对接文档.md @@ -12,6 +12,12 @@ http://localhost:5173/#/page/workspace/agents/roundtable-structure/chats/new 本文覆盖:**登录拿 Token → 列模型/切模型 → 建会话/存聊天记录 → 流式对话 → 思考内容 → 步骤条(含创建脚本/执行脚本/写入文件及全部工具)→ 用户协助卡片 → 消息列表渲染 → 停止/重连/上传/产物**。 **不包含**智能体配置(创建/编辑智能体、技能绑定等)。 +路径约定(均相对本仓库根目录 `deerflow-server/`): + +- 前端:`frontend-web/src/…` +- 后端:`offline-backend-20260512/backend/…`(下文若写 `app/`、`deerflow/`,都在这个 backend 目录下) +- 每节末尾有 **参考代码**;文末第 20 节是总索引。 + --- ## 0. 先看这一页 @@ -45,6 +51,17 @@ Content-Type: application/json 用 Bearer **不必**再带 CSRF。不要用 cookie 登录(`/login/local` 只把 JWT 写进 HttpOnly cookie,不适合 Angular 跨域 API 客户端)。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 前端两个 Base URL | `frontend-web/src/core/config/index.ts`(`getBackendBaseURL` / `getLangGraphBaseURL`) | +| 环境变量注入 | `frontend-web/src/env.ts` | +| LangGraph SDK 客户端(自动带 Bearer) | `frontend-web/src/core/api/api-client.ts` | +| Gateway fetch 自动带 Authorization | `frontend-web/src/core/api/fetch-client.ts` | +| CSRF:有 Bearer 则跳过 | `offline-backend-20260512/backend/app/gateway/csrf_middleware.py` | +| 登录接口白名单 | `offline-backend-20260512/backend/app/gateway/auth_middleware.py` | + --- ## 1. 架构与端到端流程 @@ -77,6 +94,19 @@ sequenceDiagram 2. `context.agent_id = "roundtable-structure"` 3. 会话 `metadata.agent_id = "roundtable-structure"`(历史列表按此过滤) +**参考代码** + +| 说明 | 路径 | +|------|------| +| 路由挂载 AgentChatPage | `frontend-web/src/pages/WorkspaceRoutes.tsx`(`agents/:agent_id/chats/:thread_id`) | +| 对话页:`assistantId` + `context.agent_id` | `frontend-web/src/pages/AgentChatPage.tsx` | +| 聊天气泡壳 | `frontend-web/src/components/workspace/chats/chat-box.tsx` | +| 发消息时写入 metadata.agent_id | `frontend-web/src/core/threads/hooks.ts`(`onCreated` / `thread.submit`) | +| 非内置 assistant_id → agent_id | `offline-backend-20260512/backend/app/gateway/services.py`(`build_run_config`) | +| Lead Agent 工厂 | `offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py` | +| 智能体种子(id / 中文名 / 技能) | `offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/config.yaml` | +| 智能体 SOUL | `offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md` | + --- ## 2. CORS / 跨域(Angular 必读) @@ -94,6 +124,8 @@ GATEWAY_CORS_ORIGINS=http://localhost:4200,http://127.0.0.1:4200,https://your-an 或开发期临时 `GATEWAY_CORS_ALLOW_ALL=1`(勿用于生产)。 +**参考代码**:`offline-backend-20260512/backend/app/gateway/app.py`(搜 `GATEWAY_CORS_ORIGINS` / `local_vite_origins` / `cors_expose_headers`)。回归:`offline-backend-20260512/backend/tests/test_taskcop_cors.py`。 + 浏览器跨域读 SSE 还需要暴露这些响应头(Gateway 已配): - `Content-Location`(从中解析 `run_id`,取消/重连要用) @@ -210,6 +242,20 @@ function authHeader(): HttpHeaders { } ``` +**参考代码** + +| 说明 | 路径 | +|------|------| +| 前端登录 API | `frontend-web/src/core/auth/api.ts`(`loginByUsername` / `loginByToken`) | +| Token 存取、拼 Bearer | `frontend-web/src/core/auth/index.ts`(`getAuthorizationHeaderValue` / `setStoredAuth`) | +| 类型 `LoginByUsernameResponse` | `frontend-web/src/core/auth/index.ts` | +| 登录页(含 token 换票) | `frontend-web/src/pages/LoginPage.tsx` | +| 后端登录路由 | `offline-backend-20260512/backend/app/gateway/routers/auth.py`(`/login/username`、`/login/token`、`/me`) | +| JWT 签发 | `offline-backend-20260512/backend/app/gateway/auth/jwt.py` | +| 请求里解析 cookie / Bearer | `offline-backend-20260512/backend/app/gateway/deps.py`(`get_current_user`) | +| 登录配置(口令门 / token_login) | `offline-backend-20260512/backend/config.yaml` → `auth_login`;schema:`deerflow/config/login_config.py` | +| 登录契约说明 | `offline-backend-20260512/backend/docs/AUTH_LOGIN.md` | + --- ## 4. 模型列表与切换 @@ -266,6 +312,20 @@ Authorization: Bearer 每轮都可以换模型;换了只影响 **下一轮**,不会改写历史。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 前端拉模型 | `frontend-web/src/core/models/api.ts`、`frontend-web/src/core/models/types.ts` | +| 输入框模型下拉 + 档位 | `frontend-web/src/components/workspace/input-box.tsx`(`InputMode`、`ModelSelector`) | +| 档位写入 context | `frontend-web/src/core/threads/hooks.ts`(`thread.submit` 的 `context`:`thinking_enabled` / `is_plan_mode` / `subagent_enabled`) | +| 本地记住所选模型 | `frontend-web/src/core/settings/local.ts`(`deerflow.thread-model.{threadId}`) | +| 后端模型列表 | `offline-backend-20260512/backend/app/gateway/routers/models.py` | +| context 白名单 | `offline-backend-20260512/backend/app/gateway/services.py`(`_CONTEXT_CONFIGURABLE_KEYS`) | +| 本轮实际用哪个模型 | `offline-backend-20260512/backend/app/gateway/services.py`(`_resolve_effective_run_model_name`) | +| 创建 ChatModel、关思考 | `offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py` | +| 模型配置源 | `offline-backend-20260512/backend/config.yaml` → `models[]` | + --- ## 5. 会话(Thread)= 聊天记录的存储单位 @@ -393,6 +453,20 @@ DELETE {LG}/threads/{thread_id} `status`:`idle` | `busy` | `interrupted` | `error` 正在跑时为 `busy`。同一会话再发会被 `multitask_strategy: "reject"` 打回 **HTTP 409**。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 线程类型 / context 类型 | `frontend-web/src/core/threads/types.ts` | +| 列表、标题、路由 | `frontend-web/src/core/threads/utils.ts`(`titleOfThread` / `pathOfThread`) | +| 分页搜索 | `frontend-web/src/core/threads/hooks.ts`(`useThreadsPage`) | +| 智能体最近对话 | `frontend-web/src/components/workspace/agents/agent-recent-chats.tsx` | +| 工作区会话列表 UI | `frontend-web/src/components/workspace/workspace-nav-chat-list.tsx` | +| 建/搜/改/删/state/history | `offline-backend-20260512/backend/app/gateway/routers/threads.py` | +| ThreadResponse / Search 模型 | 同上文件内 `ThreadResponse`、`ThreadCreateRequest`、`ThreadSearchRequest` | +| 删除时清本地目录 | `frontend-web/src/core/threads/hooks.ts`(delete 后再调 Gateway DELETE) | +| 409 中文文案 | `offline-backend-20260512/backend/app/gateway/services.py`(`create_or_reject` / RunManager) | + --- ## 6. 发起智能体对话(核心) @@ -511,6 +585,21 @@ Last-Event-ID: <上一帧 id> 心跳是注释行:`: heartbeat`,忽略即可。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 前端 `useStream` + `thread.submit` | `frontend-web/src/core/threads/hooks.ts`(`useThreadStream`、构造 `messages` / `context`) | +| SDK 包装 | `frontend-web/src/core/api/api-client.ts` | +| stream_mode 规范化 | `frontend-web/src/core/api/stream-mode.ts` | +| 停止:`thread.stop` / `runs.cancel` | `frontend-web/src/core/threads/hooks.ts`(`stopThreadRun`) | +| 输入框停止按钮 | `frontend-web/src/components/workspace/input-box.tsx` | +| 后端 stream / cancel / join | `offline-backend-20260512/backend/app/gateway/routers/thread_runs.py`(`RunCreateRequest`、`stream_run`、`cancel_run`、`join_run`) | +| 真正起跑 | `offline-backend-20260512/backend/app/gateway/services.py`(`start_run`) | +| 后台跑图 + 发 SSE | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py`(`run_agent`) | +| Run 生命周期 | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/manager.py` | +| 流式设计说明 | `offline-backend-20260512/backend/docs/STREAMING.md` | + --- ## 7. SSE 事件协议(Angular 必须自己解析 POST 流) @@ -655,6 +744,18 @@ async function streamRun( } ``` +**参考代码** + +| 说明 | 路径 | +|------|------| +| SSE 组帧(`event:` / `data:` / `id:`) | `offline-backend-20260512/backend/app/gateway/services.py`(`format_sse`、`sse_consumer`) | +| 心跳 / 订阅 | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/stream_bridge.py` | +| 首帧 `event: metadata` | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py`(`bridge.publish(..., "metadata", {run_id, thread_id})`) | +| `messages-tuple` → 内部 `messages` | 同上 `run_agent`;SSE 事件名 `_lg_mode_to_sse_event` | +| chunk 序列化成 `[chunk, meta]` | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py`(`serialize_messages_tuple` / `serialize_channel_values`) | +| 前端消费:onCustomEvent / onUpdateEvent | `frontend-web/src/core/threads/hooks.ts` | +| 协议说明 | `offline-backend-20260512/backend/docs/STREAMING.md` | + --- ## 8. 消息数据结构(历史 + 实时共用) @@ -707,6 +808,14 @@ async function streamRun( | `ai` 有 `tool_calls` 或仅有思考 | **不单独当最终气泡**,进入步骤条 | | `tool` | **几乎不单独成气泡**,并进步骤条 / 协助卡片 | +**参考代码** + +| 说明 | 路径 | +|------|------| +| 分组、抽文本、抽思考、隐藏消息 | `frontend-web/src/core/messages/utils.ts`(`groupMessages` / `extractContentFromMessage` / `extractReasoningContentFromMessage` / `isHiddenFromUIMessage`) | +| 检索结果识别 | `frontend-web/src/core/messages/search-results.ts` | +| 序列化后的 message dict | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py` | + --- ## 9. 消息列表如何分组渲染 @@ -765,6 +874,20 @@ async function streamRun( `busy` 且还没有最终 `assistant` 气泡时,在列表底部显示跳动的「正在生成」。 上传文件未完成时可插一条 mock AI:`additional_kwargs.element === "task"`,文案「文件上传中,请稍候...」。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 列表入口、分组渲染 | `frontend-web/src/components/workspace/messages/message-list.tsx` | +| 单条用户/助手气泡 | `frontend-web/src/components/workspace/messages/message-list-item.tsx` | +| Markdown | `frontend-web/src/components/workspace/messages/markdown-content.tsx` | +| 气泡/会话滚动容器 | `frontend-web/src/components/ai-elements/message.tsx`、`conversation.tsx` | +| 跳动「正在生成」 | `frontend-web/src/components/workspace/streaming-indicator.tsx` | +| 技能来源展示 | `frontend-web/src/core/skill-display/runtime.ts` | +| 导出 | `frontend-web/src/core/threads/export.ts`;Word:`frontend-web/src/components/workspace/messages/download-word-button.tsx` | +| 删除消息 | `frontend-web/src/components/workspace/messages/delete-message-button.tsx` | +| 问题跳转 | `frontend-web/src/components/workspace/question-navigator.tsx` | + --- ## 10. 思考内容如何对接 @@ -809,6 +932,17 @@ function splitThink(content: string): { answer: string; thinking: string | null `thinking_enabled: false` 时模型不应再吐思考;若仍出现 ``,继续剥掉以免露标签。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 抽思考 / 剥 ``(含未闭合) | `frontend-web/src/core/messages/utils.ts`(`extractReasoningContentFromMessage`、`splitInlineReasoning`) | +| 步骤条里的思考步 | `frontend-web/src/components/workspace/messages/message-group.tsx`(`convertToSteps` 的 `reasoning`) | +| 独立思考折叠块 | `frontend-web/src/components/ai-elements/reasoning.tsx` | +| 文案「思考」 | `frontend-web/src/core/i18n/locales/zh-CN.ts`(`t.common.thinking`) | +| 模型思考流字段 | `offline-backend-20260512/backend/packages/harness/deerflow/models/vllm_provider.py` | +| 关思考注入 extra_body | `offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py` | + --- ## 11. 步骤条(Chain of Thought)— 请做全 @@ -925,6 +1059,23 @@ interface WebFetchArgs { url: string } 字段名不统一(`content` / `page_content` / `snippet` / `m_title` / `name`),展示时做并集兜底。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| **步骤条主实现(必看)** | `frontend-web/src/components/workspace/messages/message-group.tsx`(`convertToSteps`、按 `name` 分支、`ArtifactWriteToolCall`) | +| ChainOfThought 组件 | `frontend-web/src/components/ai-elements/chain-of-thought.tsx` | +| 中文底稿 | `frontend-web/src/core/i18n/locales/zh-CN.ts` → `toolCalls`;类型 `frontend-web/src/core/i18n/locales/types.ts` | +| 工具一句话说明 | `frontend-web/src/core/tools/utils.ts`(`explainToolCall`) | +| 检索结果卡片 | `frontend-web/src/core/messages/search-results.ts` | +| 写文件实时预览 | `frontend-web/src/core/artifacts/streaming-write-preview.ts` | +| bash / ls / read / write / glob / grep | `offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py`(参数都要求先填 `description`) | +| 工具装配(含 MCP、内置) | `offline-backend-20260512/backend/packages/harness/deerflow/tools/tools.py` | +| present_files | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/present_file_tool.py` | +| skill_list / skill_view / search_skills | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/skill_tools.py` | +| 启用哪些工具 | `offline-backend-20260512/backend/config.yaml` → `tools[]` | +| 会商页步骤条(对照用,对话页以 message-group 为准) | `frontend-web/src/roundtable-planning/lib/step-display.tsx` | + --- ## 12. 用户协助卡片(ask_clarification) @@ -1007,6 +1158,17 @@ input.messages = [{ type: "human", content: "<上面拼好的文本>" }] 不要调 `/state` 的 resume,也不要自己造 ToolMessage。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 卡片 UI、拼「我选择:」、多卡合并提交 | `frontend-web/src/components/workspace/messages/message-list.tsx`(`ClarificationCard`、`handleSubmitAllClarifications`) | +| 识别 tool 消息 | `frontend-web/src/core/messages/utils.ts`(`isClarificationToolMessage`) | +| 工具定义 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_tool.py` | +| 截住工具、写 `additional_kwargs.clarification`、goto END | `offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py`(`_build_payload`) | +| `allow_multiple` 解析 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_utils.py` | +| 提交后又走同一套 sendMessage | `frontend-web/src/pages/AgentChatPage.tsx`、`frontend-web/src/core/threads/hooks.ts`(**不是** Command resume) | + --- ## 13. 其它卡片与面板 @@ -1056,6 +1218,18 @@ interface Todo { content?: string; status?: 'pending' | 'in_progress' | 'complet `setup_agent` / `setup_writing` 带 approval kwargs。普通 `roundtable-structure` 对话 **不会出现**,可忽略。 Deep Research 的 `setup_research_approval` 同理。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| present_files 分组渲染 | `frontend-web/src/components/workspace/messages/message-list.tsx`(`assistant:present-files`) | +| 产物 API | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | +| 产物前端 | `frontend-web/src/components/workspace/artifacts/`(如 `artifact-file-detail.tsx`) | +| 子任务卡 | `frontend-web/src/components/workspace/messages/subtask-card.tsx` | +| `task` 工具 / 子代理 | `offline-backend-20260512/backend/packages/harness/deerflow/subagents/` | +| Todo 面板 | `frontend-web/src/components/workspace/todo-list.tsx`;类型 `frontend-web/src/core/todos/types.ts` | +| write_todos 中间件装配 | `offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py`(`_build_middlewares`) | + --- ## 14. 标题、追问、错误态 @@ -1095,6 +1269,18 @@ POST {GATEWAY}/api/threads/{thread_id}/suggestions | 上下文压缩 | `custom.context_compacting` | 「正在压缩上下文…」 | | 会话不属于当前用户 | 403/404 | 回到新建 | +**参考代码** + +| 说明 | 路径 | +|------|------| +| 标题展示 | `frontend-web/src/components/workspace/thread-title.tsx` | +| 列表用 title | `frontend-web/src/core/threads/utils.ts`(`titleOfThread`) | +| 临时标题 + LLM 标题 | `offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py` | +| 追问 API 调用 | `frontend-web/src/components/workspace/input-box.tsx`(`/api/threads/${threadId}/suggestions`) | +| 追问后端 | `offline-backend-20260512/backend/app/gateway/routers/suggestions.py` | +| 409 toast | `frontend-web/src/core/threads/hooks.ts`(`isActiveRunConflictError` / `onError`) | +| 压缩 toast | `offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py`(`custom.context_compacting`) | + --- ## 15. 文件上传(用户侧) @@ -1138,6 +1324,16 @@ PDF/PPT/Excel/Word 会转成 Markdown 一并放入 uploads。 限额:`GET /api/threads/{thread_id}/uploads/limits`。 列表/删除:`GET …/uploads/list`,`DELETE …/uploads/{filename}`。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 前端上传 | `frontend-web/src/components/workspace/input-box.tsx`;预览 `frontend-web/src/components/workspace/messages/uploaded-file-preview-dialog.tsx` | +| 后端上传路由 | `offline-backend-20260512/backend/app/gateway/routers/uploads.py` | +| 目录与虚拟路径 | `offline-backend-20260512/backend/packages/harness/deerflow/uploads/manager.py` | +| 文档转 Markdown | `offline-backend-20260512/backend/packages/harness/deerflow/utils/file_conversion.py` | +| 说明文档 | `offline-backend-20260512/backend/docs/FILE_UPLOAD.md` | + --- ## 16. 推荐的页面结构(对标 AgentChatPage) @@ -1162,6 +1358,20 @@ PDF/PPT/Excel/Word 会转成 Markdown 一并放入 uploads。 `roundtable-structure` 是「结构化抽取」智能体:给它方案报告或席位交付,产出 flow-json 流程图数据,常伴随写文件 + 展示文件。不要当成多智能体会商编排页(`/roundtable` 是另一套 API)。 +**参考代码** + +| 说明 | 路径 | +|------|------| +| 对话页整页 | `frontend-web/src/pages/AgentChatPage.tsx` | +| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` | +| ChatBox(标题栏 + 列表 + 输入) | `frontend-web/src/components/workspace/chats/chat-box.tsx` | +| 输入框 | `frontend-web/src/components/workspace/input-box.tsx` | +| 左栏最近会话 | `frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx`、`agent-recent-chats.tsx` | +| 欢迎/推荐问题 | `frontend-web/src/components/workspace/agent-welcome.tsx`、`agents/agent-question-chips.tsx` | +| 智能体种子 | `offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/` | +| 会商编排(不要和本页搞混) | `frontend-web/src/roundtable-planning/`、`offline-backend-20260512/backend/app/gateway/routers/multi_agent.py` | +| iframe 嵌现网 UI(备选) | `frontend-web/docs/embed-iframe-chat.md` | + --- ## 17. 最小对接顺序(验收清单) @@ -1240,22 +1450,95 @@ Base:`{GATEWAY} = http://host:8001`,LangGraph `{LG} = {GATEWAY}/api`。 --- -## 20. 参考实现位置(本仓库) +## 20. 参考实现总索引 -| 主题 | 文件 | +路径均相对仓库根 `deerflow-server/`。后端根目录 = `offline-backend-20260512/backend/`。 +各节正文末尾也有对应表;这里按主题汇总,加粗的是对接时优先打开的文件。 + +### 20.1 前端:页面与 UI + +| 主题 | 路径 | |------|------| -| 对话页 | `frontend-web/src/pages/AgentChatPage.tsx` | -| 发消息 / SSE | `frontend-web/src/core/threads/hooks.ts` | -| 消息分组 | `frontend-web/src/core/messages/utils.ts` | -| 步骤条 | `frontend-web/src/components/workspace/messages/message-group.tsx` | -| 协助卡片 | `frontend-web/src/components/workspace/messages/message-list.tsx` | -| 中文步骤文案 | `frontend-web/src/core/i18n/locales/zh-CN.ts` → `toolCalls` | -| 登录 | `frontend-web/src/core/auth/api.ts` | -| 模型 | `GET /api/models` ← `app/gateway/routers/models.py` | -| 会话 / 状态 | `app/gateway/routers/threads.py` | -| 流式 | `app/gateway/routers/thread_runs.py` | -| SSE 帧 | `app/gateway/services.py` `format_sse` | -| 协助卡 payload | `deerflow/agents/middlewares/clarification_middleware.py` | -| 沙箱工具 description | `deerflow/sandbox/tools.py`(bash / write_file / …) | +| **智能体对话页** | `frontend-web/src/pages/AgentChatPage.tsx` | +| 路由 `agents/:agent_id/chats/:thread_id` | `frontend-web/src/pages/WorkspaceRoutes.tsx` | +| 对话壳(标题 / 列表 / 输入 / 产物入口) | `frontend-web/src/components/workspace/chats/chat-box.tsx` | +| **消息列表 + 协助卡片** | `frontend-web/src/components/workspace/messages/message-list.tsx` | +| 用户/助手气泡 | `frontend-web/src/components/workspace/messages/message-list-item.tsx` | +| **步骤条(创建脚本/执行脚本/写入文件等)** | `frontend-web/src/components/workspace/messages/message-group.tsx` | +| Markdown | `frontend-web/src/components/workspace/messages/markdown-content.tsx` | +| 子任务卡 | `frontend-web/src/components/workspace/messages/subtask-card.tsx` | +| 思考折叠块 | `frontend-web/src/components/ai-elements/reasoning.tsx` | +| ChainOfThought 控件 | `frontend-web/src/components/ai-elements/chain-of-thought.tsx` | +| 输入框(模型、档位、发送、停止、追问、上传) | `frontend-web/src/components/workspace/input-box.tsx` | +| Todo 条 | `frontend-web/src/components/workspace/todo-list.tsx` | +| 会话标题 | `frontend-web/src/components/workspace/thread-title.tsx` | +| 正在生成 | `frontend-web/src/components/workspace/streaming-indicator.tsx` | +| 会话列表 UI | `frontend-web/src/components/workspace/workspace-nav-chat-list.tsx` | +| 该智能体最近对话 | `frontend-web/src/components/workspace/agents/agent-recent-chats.tsx` | +| 左栏 | `frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx` | +| 产物预览 | `frontend-web/src/components/workspace/artifacts/` | +| 中文步骤文案 `toolCalls.*` | `frontend-web/src/core/i18n/locales/zh-CN.ts` | -智能体种子:`offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/`(`id: roundtable-structure`,`name: 结构化抽取`)。 +### 20.2 前端:协议与状态 + +| 主题 | 路径 | +|------|------| +| **发消息 / useStream / cancel / 409** | `frontend-web/src/core/threads/hooks.ts` | +| 线程类型 | `frontend-web/src/core/threads/types.ts` | +| 标题与路由 | `frontend-web/src/core/threads/utils.ts` | +| **消息分组、思考、隐藏** | `frontend-web/src/core/messages/utils.ts` | +| 检索结果归一化 | `frontend-web/src/core/messages/search-results.ts` | +| **登录 API** | `frontend-web/src/core/auth/api.ts` | +| Bearer 存取 | `frontend-web/src/core/auth/index.ts` | +| 模型 API / 类型 | `frontend-web/src/core/models/api.ts`、`frontend-web/src/core/models/types.ts` | +| LangGraph Client | `frontend-web/src/core/api/api-client.ts` | +| Gateway fetch + 401 | `frontend-web/src/core/api/fetch-client.ts` | +| Base URL | `frontend-web/src/core/config/index.ts`、`frontend-web/src/env.ts` | +| 写文件流式预览 | `frontend-web/src/core/artifacts/streaming-write-preview.ts` | +| 工具一句话 | `frontend-web/src/core/tools/utils.ts` | + +### 20.3 后端:HTTP + +| 主题 | 路径 | +|------|------| +| **登录 /me** | `offline-backend-20260512/backend/app/gateway/routers/auth.py` | +| JWT | `offline-backend-20260512/backend/app/gateway/auth/jwt.py` | +| **CORS** | `offline-backend-20260512/backend/app/gateway/app.py` | +| 鉴权中间件 | `offline-backend-20260512/backend/app/gateway/auth_middleware.py` | +| **模型列表** | `offline-backend-20260512/backend/app/gateway/routers/models.py` | +| **会话 CRUD / search / state** | `offline-backend-20260512/backend/app/gateway/routers/threads.py` | +| **runs/stream / cancel / join** | `offline-backend-20260512/backend/app/gateway/routers/thread_runs.py` | +| start_run、SSE、context 白名单 | `offline-backend-20260512/backend/app/gateway/services.py` | +| 上传 | `offline-backend-20260512/backend/app/gateway/routers/uploads.py` | +| 产物 | `offline-backend-20260512/backend/app/gateway/routers/artifacts.py` | +| 追问 | `offline-backend-20260512/backend/app/gateway/routers/suggestions.py` | +| 简化开放聊天(不要用来复刻本页) | `offline-backend-20260512/backend/app/gateway/routers/open_chat.py` | + +### 20.4 后端:运行时与工具 + +| 主题 | 路径 | +|------|------| +| **跑图 + 发 SSE 事件** | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/worker.py` | +| RunManager | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/runs/manager.py` | +| StreamBridge | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/stream_bridge.py` | +| 消息序列化 | `offline-backend-20260512/backend/packages/harness/deerflow/runtime/serialization.py` | +| Lead Agent | `offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py` | +| **协助卡中间件** | `offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/clarification_middleware.py` | +| 协助工具 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/clarification_tool.py` | +| **bash / write_file 等(description)** | `offline-backend-20260512/backend/packages/harness/deerflow/sandbox/tools.py` | +| 工具总装 | `offline-backend-20260512/backend/packages/harness/deerflow/tools/tools.py` | +| present_files | `offline-backend-20260512/backend/packages/harness/deerflow/tools/builtins/present_file_tool.py` | +| 标题 | `offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py` | +| 模型工厂 | `offline-backend-20260512/backend/packages/harness/deerflow/models/factory.py` | + +### 20.5 配置、种子、文档 + +| 主题 | 路径 | +|------|------| +| **roundtable-structure 种子** | `offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/config.yaml` | +| 同上 SOUL | `offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-structure/SOUL.md` | +| 模型 / 工具开关 | `offline-backend-20260512/backend/config.yaml` | +| 流式协议 | `offline-backend-20260512/backend/docs/STREAMING.md` | +| 登录契约 | `offline-backend-20260512/backend/docs/AUTH_LOGIN.md` | +| 上传 | `offline-backend-20260512/backend/docs/FILE_UPLOAD.md` | +| iframe 嵌入备选 | `frontend-web/docs/embed-iframe-chat.md` | diff --git a/frontend-web/public/runtime-config.js b/frontend-web/public/runtime-config.js index e24ea0c7..24cc025a 100644 --- a/frontend-web/public/runtime-config.js +++ b/frontend-web/public/runtime-config.js @@ -52,7 +52,11 @@ window.__CM_RUNTIME_CONFIG__ = { // 8BF 嵌入页专用任务系统接口。部署到其他系统时只需修改这一项, // 不影响原多智能体会商使用的任务详情接口。 - VITE_8BF_TASK_API_BASE_URL: "https://ch1.b.uat.4.cn/zncm", + VITE_8BF_TASK_API_BASE_URL: "https://ch1.b.uat.4.cn/zl_consumer", + + // 8BF 会商页右上角「返回」按钮的跳转地址(当前窗口 window.location.href)。 + // 部署后直接改 dist/runtime-config.js 即可,无需重新打包。留空则点击时提示未配置。 + VITE_8BF_ROUNDTABLE_BACK_URL: "", // 佳来模型对话接口。 VITE_AIPI_CHAT_URL: "https://ch1.b.uat.4.cn/gpt", @@ -134,6 +138,11 @@ window.__CM_RUNTIME_CONFIG__ = { // 隐藏系统 Logo、换肤按钮以及“全量/简洁模式”切换。修改后刷新页面即可生效。 VITE_FORCE_COLLAPSED_SIDEBAR: "false", + // 简介模式左侧菜单类型:与菜单管理页「简介模式」里的类型 A / B 对应。 + // 两套网页部署时分别填 "A" 或 "B"。不填、留空、或旧配置没有这一项,都按类型 A, + // 继续读取原来的简介菜单数据,无需改旧页面。 + VITE_COMPACT_MENU_TYPE: "A", + // GitHub OAuth Token;不使用 GitHub 相关能力时留空。 VITE_GITHUB_OAUTH_TOKEN: "", diff --git a/frontend-web/src/assets/eightbf-roundtable/back-arrow.svg b/frontend-web/src/assets/eightbf-roundtable/back-arrow.svg new file mode 100644 index 00000000..801cd281 --- /dev/null +++ b/frontend-web/src/assets/eightbf-roundtable/back-arrow.svg @@ -0,0 +1,3 @@ + diff --git a/frontend-web/src/components/ai-elements/reasoning.tsx b/frontend-web/src/components/ai-elements/reasoning.tsx index 2845edf6..d9c63e2a 100644 --- a/frontend-web/src/components/ai-elements/reasoning.tsx +++ b/frontend-web/src/components/ai-elements/reasoning.tsx @@ -165,7 +165,7 @@ export const ReasoningTrigger = memo( export type ReasoningContentProps = ComponentProps< typeof CollapsibleContent > & { - children: string; + children: ReactNode; }; export const ReasoningContent = memo( @@ -178,7 +178,11 @@ export const ReasoningContent = memo( )} {...props} > - {children} + {typeof children === "string" ? ( + {children} + ) : ( + children + )} ), ); diff --git a/frontend-web/src/components/analysis-results/AnalysisResultCardList.tsx b/frontend-web/src/components/analysis-results/AnalysisResultCardList.tsx new file mode 100644 index 00000000..2788cf70 --- /dev/null +++ b/frontend-web/src/components/analysis-results/AnalysisResultCardList.tsx @@ -0,0 +1,104 @@ +import { FileOutput } from "lucide-react"; +import { toast } from "sonner"; + +import responsibilityIcon from "@/assets/position-roundtable/责任岗位.svg"; +import resultTimeIcon from "@/assets/position-roundtable/时间.svg"; +import { desensitize } from "@/lib/desensitize"; +import { cn } from "@/lib/utils"; + +import "./analysis-results.css"; +import type { AnalysisResultCardItem } from "./types"; + +function formatResultTime(value: string | undefined, fallback: string): string { + if (!value) return fallback; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(date); +} + +export function AnalysisResultCardList({ + items, + emptyTitle = "暂无交付产物", + emptyDescription = "第二步研讨与第三步总结生成 Markdown 后,产物会显示在这里。", + padded = true, + onOpen, +}: { + items: AnalysisResultCardItem[]; + emptyTitle?: string; + emptyDescription?: string; + padded?: boolean; + onOpen: (item: AnalysisResultCardItem) => void; +}) { + if (items.length === 0) { + return ( +
+ +

{emptyTitle}

+

+ {emptyDescription} +

+
+ ); + } + + return ( +
+ {items.map((item, index) => { + const displayedTime = formatResultTime( + item.updatedAt, + item.fallbackTime || "刚刚", + ); + const displayedProductName = item.delivered ? item.productName : "未交付"; + return ( +