Merge branch 'master-nj' into 'master'
fix(buttons): 行动分析宿主路由支持切换任务id/行动id See merge request knowledge-base-bj/deerflow-code!39
This commit is contained in:
commit
4f7f07504a
17 files changed
+542
-45
No files matched your search
@@ -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/`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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],
|
||||||
@@ -260,6 +264,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
|
||||||
|
|||||||
@@ -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 ${
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+324
@@ -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]` 块**严格按格式**,后端用正则解析,任何变形都会让前端进入降级路径
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
description: 8BF 多智能体会商第一步的任务理解智能体,通过 2-5 轮简短追问把用户模糊需求转化成结构化的「任务目标+约束+关键假设」,为 8BF 圆桌研讨准备输入。与通用 roundtable-intent 功能一致,提示词独立可单独调优
|
||||||
|
id: eightbf-roundtable-intent
|
||||||
|
name: 8BF 任务理解
|
||||||
@@ -72,6 +72,7 @@ from app.gateway.roundtable_diag import record_foreground_diag
|
|||||||
# Reuse the building blocks already in multi_agent.py — keeps both routers
|
# Reuse the building blocks already in multi_agent.py — keeps both routers
|
||||||
# identical in connection handling, payload shape, and error mapping.
|
# identical in connection handling, payload shape, and error mapping.
|
||||||
from app.gateway.routers._roundtable_seed import (
|
from app.gateway.routers._roundtable_seed import (
|
||||||
|
EIGHTBF_INTENT_AGENT_ID,
|
||||||
POSITION_INTENT_AGENT_ID,
|
POSITION_INTENT_AGENT_ID,
|
||||||
ensure_roundtable_functional_agents,
|
ensure_roundtable_functional_agents,
|
||||||
)
|
)
|
||||||
@@ -127,7 +128,11 @@ def _get_intent_turn_store(request: Request):
|
|||||||
|
|
||||||
# 内置 agent 目录名,须与 ``.deer-flow/agents/roundtable-intent/`` 一致。
|
# 内置 agent 目录名,须与 ``.deer-flow/agents/roundtable-intent/`` 一致。
|
||||||
INTENT_AGENT_ID = "roundtable-intent"
|
INTENT_AGENT_ID = "roundtable-intent"
|
||||||
_SUPPORTED_INTENT_AGENT_IDS = frozenset({INTENT_AGENT_ID, POSITION_INTENT_AGENT_ID})
|
# 8BF 独立会商页第一步专用的内置意图智能体:提示词与 roundtable-intent 同源,
|
||||||
|
# 但目录独立、可单独调优,不改变通用会商与岗位会商的契约。
|
||||||
|
_SUPPORTED_INTENT_AGENT_IDS = frozenset(
|
||||||
|
{INTENT_AGENT_ID, POSITION_INTENT_AGENT_ID, EIGHTBF_INTENT_AGENT_ID}
|
||||||
|
)
|
||||||
|
|
||||||
# SOUL 规定的交接信号:``[INTENT_READY]`` + markdown json 代码块。
|
# SOUL 规定的交接信号:``[INTENT_READY]`` + markdown json 代码块。
|
||||||
# group(1) 解析为 IntentSummary;解析失败则继续走澄清/asking 分支。
|
# group(1) 解析为 IntentSummary;解析失败则继续走澄清/asking 分支。
|
||||||
@@ -143,7 +148,9 @@ def _requested_intent_agent_id(payload: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
Existing multi-agent callers omit the field and therefore keep using
|
Existing multi-agent callers omit the field and therefore keep using
|
||||||
``roundtable-intent``. Position roundtable sends the dedicated ID so its
|
``roundtable-intent``. Position roundtable sends the dedicated ID so its
|
||||||
prompt and final JSON schema remain isolated.
|
prompt and final JSON schema remain isolated. The 8BF roundtable page
|
||||||
|
sends ``eightbf-roundtable-intent`` so its Step-1 prompt can be tuned
|
||||||
|
independently while keeping the generic hand-off schema.
|
||||||
"""
|
"""
|
||||||
requested = str(payload.get("intent_agent_id") or "").strip()
|
requested = str(payload.get("intent_agent_id") or "").strip()
|
||||||
if not requested:
|
if not requested:
|
||||||
|
|||||||
@@ -33,16 +33,6 @@ concurrency:
|
|||||||
fail_open: true
|
fail_open: true
|
||||||
|
|
||||||
models:
|
models:
|
||||||
- name: MiniMax-M2.7
|
|
||||||
display_name: MiniMax-M2.7
|
|
||||||
use: langchain_openai:ChatOpenAI
|
|
||||||
model: MiniMax-M2.7
|
|
||||||
api_key: sk-cp-mrsAGtzDpUl0TpOBPVoiRG_pWF1kDDN9_mDAnONBs6HIPcNUexSKtSbx2QOxfKBvJT6Xw6hDTV6hdkJYFWfNA8UtHQ08TiGJiosdRoW6q1sGb6Cb7D85NHk
|
|
||||||
base_url: https://api.minimaxi.com/v1
|
|
||||||
max_tokens: 4096
|
|
||||||
temperature: 1.0
|
|
||||||
supports_vision: true
|
|
||||||
supports_thinking: true
|
|
||||||
- name: MiniMax-M2.7-fash
|
- name: MiniMax-M2.7-fash
|
||||||
display_name: MiniMax-M2.7-fash
|
display_name: MiniMax-M2.7-fash
|
||||||
use: langchain_openai:ChatOpenAI
|
use: langchain_openai:ChatOpenAI
|
||||||
@@ -605,7 +595,7 @@ llmwiki:
|
|||||||
enabled: true
|
enabled: true
|
||||||
# 免登录 POST /api/knowledge/vector-search 返回的 Wiki 一键跳转链接前缀。
|
# 免登录 POST /api/knowledge/vector-search 返回的 Wiki 一键跳转链接前缀。
|
||||||
frontend_base_url: "http://127.0.0.1:5174"
|
frontend_base_url: "http://127.0.0.1:5174"
|
||||||
auto_sync: true
|
auto_sync: false
|
||||||
sync_interval_seconds: 30
|
sync_interval_seconds: 30
|
||||||
database_batch_size: 50
|
database_batch_size: 50
|
||||||
embedding:
|
embedding:
|
||||||
|
|||||||
Reference in new issue
Block a user