diff --git a/frontend-web/src/strategy-components/components/task-components/KanbanColumn.tsx b/frontend-web/src/strategy-components/components/task-components/KanbanColumn.tsx index 237c30d3..d59ede02 100644 --- a/frontend-web/src/strategy-components/components/task-components/KanbanColumn.tsx +++ b/frontend-web/src/strategy-components/components/task-components/KanbanColumn.tsx @@ -37,10 +37,10 @@ export const KanbanColumn: React.FC = ({ const columnClass = isDark ? 'border border-[rgba(67,89,124,0.46)] bg-[linear-gradient(180deg,rgba(18,36,67,0.96),rgba(12,28,54,0.96))] shadow-[0_18px_40px_rgba(3,10,22,0.18)]' - : 'border border-[#E8E8E8] bg-[linear-gradient(180deg,#FFFFFF_0%,#FFF9F9_100%)] shadow-[0_14px_28px_rgba(245,34,45,0.06)]'; + : 'border border-[#E8E8E8] bg-[linear-gradient(180deg,#FFFFFF_0%,#FFF9F9_100%)] shadow-[0_2px_8px_rgba(34,34,34,0.08)]'; return ( -
+
@@ -53,7 +53,7 @@ export const KanbanColumn: React.FC = ({ {title} @@ -76,14 +76,14 @@ export const KanbanColumn: React.FC = ({ />
-

+

按时间倒序展示,支持分页切换。

-
+
{tasks.length === 0 ? (
= ({ title, category, subCa const toolbarClass = isDark ? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.94)] text-white shadow-[0_16px_36px_rgba(3,10,22,0.2)]' - : 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_14px_30px_rgba(245,34,45,0.06)]'; + : 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]'; const metricClass = isDark ? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)]' @@ -237,31 +237,31 @@ const SentimentAnalyzes: React.FC = ({ title, category, subCa : 'border-[#E8E8E8] bg-white text-[#222222] placeholder:text-[#A6A6A6]'; const metrics = [ - { label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal }, - { label: '未分析', value: unanalyzedTotal }, - { label: '分析中', value: runningTotal }, - { label: '已完成', value: completedTotal }, + { label: '全部任务', value: unanalyzedTotal + runningTotal + completedTotal, icon: Layers, fg: '#222222', chipBg: '#F5F5F5', chipFg: '#7F7F7F' }, + { label: '未分析', value: unanalyzedTotal, icon: Inbox, fg: '#F5222D', chipBg: '#FEEDED', chipFg: '#F5222D' }, + { label: '分析中', value: runningTotal, icon: Loader, fg: '#1677FF', chipBg: '#EAF4FF', chipFg: '#1677FF' }, + { label: '已完成', value: completedTotal, icon: CircleCheck, fg: '#52C41A', chipBg: '#EEFCF7', chipFg: '#189D72' }, ]; return (
-
+
-
-
- +
+
+ {category} - - + + {subCategory}
-

{title}

-

+

{title}

+

面向规划任务的情绪与倾向分析看板,支持快速检索、会商启动与结果回看。

@@ -269,7 +269,7 @@ const SentimentAnalyzes: React.FC = ({ title, category, subCa
-
- {metrics.map((item, index) => ( -
-
-
-

{item.label}

-

- {item.value} -

-
-
- + {metrics.map((item) => { + const Icon = item.icon; + return ( +
+
+
+

{item.label}

+

+ {item.value} +

+
+
+ +
-
- ))} + ); + })}
@@ -375,7 +385,7 @@ const SentimentAnalyzes: React.FC = ({ title, category, subCa {showAnalysisModal && selectedTask ? (
-
+

选择分析模式

@@ -386,7 +396,7 @@ const SentimentAnalyzes: React.FC = ({ title, category, subCa @@ -396,7 +406,7 @@ const SentimentAnalyzes: React.FC = ({ title, category, subCa
-
+
- {metrics.map(({ label, value, icon: Icon, accent }) => ( -
-
-
-

{label}

-

- {value} -

-
-
- + {metrics.map((item) => { + const Icon = item.icon; + return ( +
+
+
+

{item.label}

+

+ {item.value} +

+
+
+ +
-
- ))} + ); + })}
@@ -342,7 +332,7 @@ const TaskAnalyzes: React.FC = ({ title, category, subCategor = ({ title, category, subCategor = ({ title, category, subCategor = ({ title, category, subCategor
{showAnalysisModal && selectedTask ? ( -
-
+
+

选择分析模式

-

+

当前任务:{selectedTask.taskName}

@@ -409,15 +399,18 @@ const TaskAnalyzes: React.FC = ({ title, category, subCategor diff --git a/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py b/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py index 39873369..26adaa85 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py +++ b/offline-backend-20260512/backend/app/gateway/routers/parallel_agents.py @@ -7,12 +7,10 @@ one stream and never parses LangGraph internals. Each fan-out agent runs as a pure **executing seat**: ``excluded_tools`` = ``_EXCLUDED_TOOLS`` (``write_file`` / ``str_replace`` / ``agent_orchestration`` / -``bash`` / ``ask_clarification``) so a seat can neither write files (parallel -writes would clash), dispatch / run scripts (a weak model fed "总控 / 业务链条" -framing would otherwise mimic the controller and "派活"), nor pause to ask the -user for help (this panel is an unattended parallel fan-out — a seat short on -info makes a reasonable assumption and states it in its answer instead of -interrupting). Read-only tools stay on, so the step bar still works. +``bash``) so a seat can neither write files (parallel writes would clash) nor +dispatch / run scripts (a weak model fed "总控 / 业务链条" framing would otherwise +mimic the controller and "派活"). Read-only tools + ``ask_clarification`` stay on, +so the step bar and clarification cards still work. Unified event protocol (one JSON object per ``data:`` frame):: @@ -60,21 +58,18 @@ router = APIRouter(prefix="/api/parallel-agents", tags=["parallel-agents"]) # Soft cap on parallel degree — protects remote model concurrency + sandbox. MAX_PARALLEL_AGENTS = 8 -# 禁三类工具:①**写文件**(write_file / str_replace)—— 并行席位互相写文件会打架; +# 禁两类工具:①**写文件**(write_file / str_replace)—— 并行席位互相写文件会打架; # ②**派活 / 脚本逃逸**(agent_orchestration / bash)—— 本面板里每个智能体只做自己那 # 份分析交付,绝不该扮演「总控」去调度别人。弱模型(deepseek 等)拿到带「总控 / 业务 # 链条」语气的任务后,会概率性地用 agent_orchestration 真去派活,或绕道 bash 跑脚本 # 调内部接口模仿总控调度(bug:单步单智能体链条里子智能体输出「总控调度指令…」并用 -# glob / bash 翻找派活脚本)。③**用户协助**(ask_clarification)—— 本面板是「无人值守」 -# 的并行扇出,没有让单个席位中途打断、向用户要澄清的交互位;禁掉后席位遇到信息不足时 -# 自行做合理假设并把假设写进正文交付,而不是挂起等人。物理禁掉这三类即可根治。其余只读 -# 工具(ls / read_file / 检索)保留:既不互相打架,又能正常出步骤条。 +# glob / bash 翻找派活脚本)。物理禁掉这两类即可根治。其余只读工具(ls / read_file / +# 检索)与 ask_clarification(协作卡片)保留:既不互相打架,又能正常出步骤条 / 要澄清。 _EXCLUDED_TOOLS: list[str] = [ "write_file", # 并行不互相写文件打架 "str_replace", "agent_orchestration", # 子智能体不派活 / 不调度(防模仿总控) "bash", # 禁跑脚本(防绕道脚本调内部接口模仿派活) - "ask_clarification", # 禁「用户协助」:并行无人值守,不向用户要澄清,自己合理假设并交付 ] @@ -281,13 +276,12 @@ def _build_task_message(task_name: str, brief: str) -> str: "调度对象 / 分配给『…』的任务 / agent_name」之类调度内容,严禁罗列或转派其他席位的任务,严禁调用" "任何派活 / 调度类工具,也不要用脚本(bash / python)去调用内部接口或翻找派活脚本。" ) - # 无人值守护栏:本轮并行执行没有让你中途向用户要澄清的交互位(已禁用用户协助)。遇到 - # 信息不足时,请基于已有信息做出**合理假设**并在正文中**注明你的假设**,照常给出交付, - # 不要反问用户、不要把交付挂起等待补充。 + # 澄清护栏:要向用户确认时,必须用 ask_clarification 工具发起(前端据此渲染可点击的协作 + # 卡片),不能只把问题写在正文里 —— 否则用户看不到卡片、无法回应。能合理假设就先假设并标注。 parts.append( - "【无人值守】本轮为并行无人值守执行,没有与你来回澄清的环节。若信息不足,请自行做出" - "**合理假设**并在交付中**明确写出你假设了什么**,然后照常完成分析;不要反问用户、不要" - "要求补充信息后再继续,也不要把结论挂起等待确认。" + "【需要向用户确认时】如果确实缺少完成分析所必需的关键信息,请**调用 `ask_clarification` 工具**" + "发起确认(这样用户会看到一张可点击的协作卡片来回答);**不要只把问题写在正文里**,否则用户无法回应。" + "能基于已有信息合理假设的,就先假设并在正文中标注你的假设;只有当不同假设会显著改变结论时,才发起确认。" ) return "\n\n".join(parts) diff --git a/parallel-agents-vue/docs/08-开发文档.md b/parallel-agents-vue/docs/08-开发文档.md index e57b2f37..ea547043 100644 --- a/parallel-agents-vue/docs/08-开发文档.md +++ b/parallel-agents-vue/docs/08-开发文档.md @@ -51,6 +51,8 @@ src/ ├─ parallel-agents.css 作用域深色 token(--pa-*) + .pa-* 语义类 + Markdown 样式 + 工具类子集(无 Tailwind) ├─ ParallelAgentsPanel.vue 主组件:席位条/会话区 + 全部状态编排(表单/按钮在宿主侧) ├─ ParallelSeatStrip.vue 席位状态条(头像 + running/done/clarify/error 角标,点击切换单显) + ├─ AutoScrollPane.vue 自动下滑滚动窗格:包住单席位消息,超高出滚动条 + 流式自动钉底 + + │ 用户滚动暂停 + 半透明「自动滚动」浮动按钮(每席位一实例,互不影响) ├─ MessageBubble.vue 单条气泡:头像/发言人/徽标/时间/步骤条/正文/光标/协作卡片 ├─ MessageStepsCard.vue 步骤条(ChainOfThought 结构,1:1 对齐 Step2) ├─ StepIcon.vue 步骤图标(lucide 风格 inline SVG,按 IconKey) @@ -100,7 +102,7 @@ panel emit "completed"(AgentResult[]);可点「保存」(按 taskId upsert) / | `agent_start` | `agent_id` | 该席位置 running | | `delta` | `agent_id, text` | 追加正文(节流) | | `step` | `agent_id, key(=`tool_calling:`), label, args` | 步骤条加一步(含完整 args) | -| `agent_clarification`〔已禁用〕 | `agent_id, content, question, clarification_type, clarification_context, options, allow_*` | 设 `dialogue.clarification` → 卡片;席位置 **clarify**(待协助,非 done)。**注**:后端已把 `ask_clarification` 加入 `_EXCLUDED_TOOLS`,本面板**无人值守**不再发该事件;前端协作卡片相关代码保留(兜底,正常不触发) | +| `agent_clarification` | `agent_id, content, question, clarification_type, clarification_context, options, allow_*` | 设 `dialogue.clarification` → 卡片;席位置 **clarify**(待协助,非 done)。澄清卡片**任何时候都可点**(即便整批仍在跑),用户答完 → `panel.onClarify` → `rerunAgent` 重跑该席位 | | `agent_done` | `agent_id, content` | 收口,正文以终态为准 | | `agent_error` | `agent_id, message` | 该席位置 error | | `all_done` | — | 全部结束 | @@ -122,17 +124,30 @@ panel emit "completed"(AgentResult[]);可点「保存」(按 taskId upsert) / ``` ParallelAgentsPanel ├─ ParallelSeatStrip ── Avatar (席位条,点头像切换单显) -└─ MessageBubble (当前选中席位) - ├─ Avatar - ├─ MessageStepsCard ── StepIcon (步骤条;getStepDisplay/getStepArgs 翻译 key+args) - ├─ MarkdownContent (markdown-it) - └─ ClarificationCard (msg.clarification 存在时;提交→panel.onClarify→rerunAgent) +└─ AutoScrollPane (当前选中席位;限高滚动 + 流式自动下滑 + 浮动「自动滚动」按钮) + └─ MessageBubble + ├─ Avatar + ├─ MessageStepsCard ── StepIcon (步骤条;getStepDisplay/getStepArgs 翻译 key+args) + ├─ MarkdownContent (markdown-it) + └─ ClarificationCard (msg.clarification 存在时;提交→panel.onClarify→rerunAgent) ``` +> **自动下滑 / 高度自适应**(`AutoScrollPane.vue`):会话区只负责限高(`min-h-0 flex-1 overflow-hidden`), +> 真正的滚动与自动下滑下放到**每个席位卡片内部**的窗格 —— 父元素不够高时窗格内出现滚动条;模型流式输出时 +> 用 `ResizeObserver` 监听内容增高并钉到底部;用户向上滚动(离开底部)即暂停自动下滑、滚回底部自动恢复; +> 暂停且内容可滚动时底部居中浮出半透明「自动滚动」按钮(`.pa-autoscroll-btn`),点击恢复并滚到底。 +> 每个席位各一个实例(`v-for :key=agentId`),autoScroll / 滚动位置都在实例内,**多智能体互不影响**。 +> 席位卡片靠 `.pa-rows-fill`(`grid-auto-rows: minmax(0,1fr)`)受限于会话区高度,从而内部滚动而非撑破容器。 + `ParallelAgentsPanel` 的对外接口(详见 [07-使用说明.md](./07-使用说明.md)): - props:`agents`(必填) / `username` / `baseUrl` / `token` / `getToken` / `taskId` / `model` / `dark` -- emits:`saved(SavedTask)` / `update:taskId(string)` / `completed(AgentResult[])` -- `defineExpose`:`getResults()` / `getResult(id)` / `start()` / `cancel()` +- emits: + - `agent-completed(AgentReply)` —— **单个**智能体**真正完成(done)**时各触发一次;`AgentReply = {agent_id, name, content}` + (智能体名称 + 模型返回结果,**不含 status**)。待用户协助(clarify)/ 出错(error)**不触发**。 + - `completed(AgentReply[])` —— **所有**智能体都完成(done/error、**没有任何 clarify 在等用户**)后,整批触发一次; + 若有席位在等用户协助,本轮不发,待其答完重跑完成后再发。 + - `saved(SavedTask)` / `update:taskId(string)` / `update:taskName(string)` / `update:brief(string)` +- `defineExpose`:`getResults()`(返回 `AgentResult[]`,含 status) / `getResult(id)` / `start()` / `reanalyze()` / `save()` / `cancel()` / `loadTask(id)` --- @@ -160,16 +175,20 @@ ParallelAgentsPanel | 模型列表 | `GET /api/models` → `{models:[{name, display_name}]}` | **后端运行策略(重要)**:每个并行席位是「执行席位」,不是总控。 -- **禁用工具** `_EXCLUDED_TOOLS = [write_file, str_replace, agent_orchestration, bash, ask_clarification]`: - ①禁写文件(并行不互相打架);②禁 `agent_orchestration`/`bash`(防弱模型模仿总控**派活**或绕道脚本逃逸); - ③禁 `ask_clarification`(**禁用用户协助**:本面板是并行**无人值守**扇出,没有让单席位中途打断、向用户 - 要澄清的交互位 —— 席位信息不足时自行做**合理假设**并写进正文交付,而非挂起等人)。 -- **保留** 只读 / 检索工具 → 步骤条正常。 -- 任务消息带**角色边界护栏**(「执行席位、不要模仿总控调度 / 派活」)+ **无人值守护栏**(「信息不足时 - 自行合理假设并注明,不要反问用户 / 不要挂起等待补充」)。 +- **禁用工具** `_EXCLUDED_TOOLS = [write_file, str_replace, agent_orchestration, bash]`:①禁写文件(并行 + 不互相打架);②禁 `agent_orchestration`/`bash`(防弱模型模仿总控**派活**或绕道脚本逃逸)。 +- **保留** 只读 / 检索工具 + `ask_clarification` → 步骤条、协作卡片正常(**用户协助开启**)。 +- 任务消息带**角色边界护栏**(「执行席位、不要模仿总控调度 / 派活」)。 - 步骤 `args` 通过累积 `tool_call_chunks` + 终态全量 `tool_calls` 补全(流式期间空 args 不发,防钉死)。 -- `ask_clarification` 已被禁用,正常不再产生 `agent_clarification` 事件;`_clarification_from_messages` - 仅作终态兜底(工具被禁后恒返回 `None`),不影响其他业务路由(圆桌 / 普通问答的澄清照常)。 +- `ask_clarification` 从终态 messages 抽成结构化 `agent_clarification` 事件。 + +**前端澄清(用户协助)交互要点**(`useParallelRun` / `ClarificationCard` / `ParallelAgentsPanel`): +- 协作卡片**不再因 `submitting` 禁用**:整批仍在跑时也能立刻回答某个已澄清的席位。 +- `rerunAgent` 放开了「整批 running 时禁止重跑」的限制(澄清是该席位合并流里的终态,不会再抢气泡), + 只拦同一席位的并发重跑;收尾循环(`start` 的 finally)**跳过 busy 席位**,交给 `rerunAgent` 自己结算。 +- 答澄清后**不立刻清空**正文/步骤:先移除卡片、置流式并标记 `rerunPending`,等重跑产出**第一条新事件** + 时(`maybeStartRerun`)再清空替换,避免单选视图整列空白闪烁。 +- 「单席位结算回传」`onAgentSettled`:**`clarify` 不回传**(待用户协助≠模型完成),等重跑出 `done/error` 再回传。 --- @@ -223,6 +242,7 @@ pnpm build:lib # 组件库:dist/parallel-agents-vue.{js,umd.cjs,css} | 改布局类 / 加新工具类 | `parallel-agents.css` 末尾的「工具类子集」补规则,或组件 scoped `