From 89ae31c9989727cfb551b684aa8cd654e9e92c5f Mon Sep 17 00:00:00 2001 From: eaiadmin Date: Thu, 24 Sep 2026 21:13:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BE=AE=E4=BF=A1=E5=85=AC=E4=BC=97?= =?UTF-8?q?=E5=8F=B7=E6=8A=80=E8=83=BD=E5=8C=85=E9=87=8D=E5=91=BD=E5=90=8D?= =?UTF-8?q?(weixin=5Fpublic=5Faccount)=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 wechat_official_account 重命名为 weixin_public_account,符合中文命名规范 - 新增 DOCX 文档生成技能、聊天历史、请求 ID 中间件 - 增强工作流、热点服务、文章服务等模块功能 - 前端同步重命名组件和 API - 新增架构文档 AR13/AR14、专员文档更新 - 补充测试用例(seed_specialists_test, db_migration_test) Co-Authored-AI: yes --- debug-official-account-no-reply.md | 40 + docs/02_Architecture/AR09_对象命名规范.md | 2 +- ...AR13_公众号创作专员双层多智能体架构定义.md | 1513 +++++++++++++++++ .../AR14_跨项目技术选型对比分析.md | 150 ++ docs/02_Architecture/目录说明.md | 4 + docs/05_Object_Catalog/specialists/目录.md | 2 +- .../GW03_专员工作交互形式分析.md | 2 +- .../GW04_公众号创作专员可用性改进分析.md | 40 +- ...GW05_公众号创作专员与对话框交互兼容分析.md | 10 +- .../backend-go/cmd/server/main.go | 6 + .../backend-go/config/ai_config.json | 75 +- .../backend-go/config/ai_config.json.bak | 199 +++ .../backend-go/internal/ai/llm.go | 79 +- .../internal/ai/routetest/routetest.go | 427 +++++ .../backend-go/internal/api/ai_chat.go | 75 +- .../backend-go/internal/api/ai_routetest.go | 84 + .../backend-go/internal/api/chat_history.go | 82 + .../backend-go/internal/api/chat_message.go | 4 +- .../backend-go/internal/api/router.go | 22 +- .../internal/api/specialist_prompt_test.go | 12 +- .../backend-go/internal/api/task_runtime.go | 8 +- .../backend-go/internal/config/json_loader.go | 77 +- ...le.go => weixin_public_account_article.go} | 16 +- ...ot.go => weixin_public_account_hotspot.go} | 14 +- .../internal/middleware/request_id.go | 39 + .../internal/model/chat_conversation.go | 17 + .../backend-go/internal/model/chat_message.go | 17 + .../internal/specialists/core/builtin.go | 4 +- .../api/daos.go | 10 +- .../api/runtime_helpers.go | 14 +- .../weixin_public_account_article_service.go} | 115 +- .../api/weixin_public_account_delivery.go} | 188 +- .../api/weixin_public_account_docx.go | 336 ++++ .../weixin_public_account_domain_config.go} | 146 +- .../weixin_public_account_generated_image.go} | 50 +- .../weixin_public_account_hotspot_service.go} | 167 +- .../api/weixin_public_account_workflow.go} | 734 ++++---- .../manifest.go | 18 +- .../model/article.go | 8 +- .../model/hotspot.go | 8 +- .../specialists/seeding/seed_specialists.go | 119 +- .../seeding/seed_specialists_test.go | 228 +++ .../backend-go/internal/store/db.go | 79 +- .../internal/store/db_migration_test.go | 193 +++ .../backend-go/internal/web/errors.go | 101 +- .../backend-go/internal/web/response.go | 71 +- eai_agentplatform/frontend/src/api/ai.js | 30 +- .../frontend/src/api/officialAccount.js | 33 - .../frontend/src/api/weixinPublicAccount.js | 47 + .../src/components/chat/CurrentObjectChip.vue | 6 +- .../src/components/chat/SpecialistPanel.vue | 8 +- ...=> WeixinPublicAccountSpecialistPanel.vue} | 940 +++++++++- .../frontend/src/config/projectTemplates.js | 2 +- .../frontend/src/specialists/core/builtin.js | 4 +- .../wechat-official-account/manifest.js | 26 - .../weixin-public-account/manifest.js | 30 + .../specialists/store/specialistCatalog.js | 39 +- .../frontend/src/views/system/AiAdminPage.vue | 446 ++++- .../src/views/workbench/KnowledgeHubPage.vue | 77 +- .../views/workbench/SmartAssistantPage.vue | 925 +++++++++- 60 files changed, 7251 insertions(+), 967 deletions(-) create mode 100644 debug-official-account-no-reply.md create mode 100644 docs/02_Architecture/AR13_公众号创作专员双层多智能体架构定义.md create mode 100644 docs/02_Architecture/AR14_跨项目技术选型对比分析.md create mode 100644 eai_agentplatform/backend-go/config/ai_config.json.bak create mode 100644 eai_agentplatform/backend-go/internal/ai/routetest/routetest.go create mode 100644 eai_agentplatform/backend-go/internal/api/ai_routetest.go create mode 100644 eai_agentplatform/backend-go/internal/api/chat_history.go rename eai_agentplatform/backend-go/internal/dal/{official_account_article.go => weixin_public_account_article.go} (65%) rename eai_agentplatform/backend-go/internal/dal/{official_account_hotspot.go => weixin_public_account_hotspot.go} (82%) create mode 100644 eai_agentplatform/backend-go/internal/middleware/request_id.go create mode 100644 eai_agentplatform/backend-go/internal/model/chat_conversation.go create mode 100644 eai_agentplatform/backend-go/internal/model/chat_message.go rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account => weixin_public_account}/api/daos.go (78%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account => weixin_public_account}/api/runtime_helpers.go (78%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_article_service.go => weixin_public_account/api/weixin_public_account_article_service.go} (65%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_delivery.go => weixin_public_account/api/weixin_public_account_delivery.go} (64%) create mode 100644 eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/api/weixin_public_account_docx.go rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_domain_config.go => weixin_public_account/api/weixin_public_account_domain_config.go} (58%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_generated_image.go => weixin_public_account/api/weixin_public_account_generated_image.go} (60%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_hotspot_service.go => weixin_public_account/api/weixin_public_account_hotspot_service.go} (57%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account/api/official_account_workflow.go => weixin_public_account/api/weixin_public_account_workflow.go} (57%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account => weixin_public_account}/manifest.go (52%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account => weixin_public_account}/model/article.go (88%) rename eai_agentplatform/backend-go/internal/specialists/packages/{wechat_official_account => weixin_public_account}/model/hotspot.go (80%) create mode 100644 eai_agentplatform/backend-go/internal/specialists/seeding/seed_specialists_test.go delete mode 100644 eai_agentplatform/frontend/src/api/officialAccount.js create mode 100644 eai_agentplatform/frontend/src/api/weixinPublicAccount.js rename eai_agentplatform/frontend/src/components/chat/{OfficialAccountSpecialistPanel.vue => WeixinPublicAccountSpecialistPanel.vue} (52%) delete mode 100644 eai_agentplatform/frontend/src/specialists/packages/wechat-official-account/manifest.js create mode 100644 eai_agentplatform/frontend/src/specialists/packages/weixin-public-account/manifest.js diff --git a/debug-official-account-no-reply.md b/debug-official-account-no-reply.md new file mode 100644 index 0000000..27924da --- /dev/null +++ b/debug-official-account-no-reply.md @@ -0,0 +1,40 @@ +# Debug Session: official-account-no-reply +- **Status**: [OPEN] +- **Issue**: 挂载公众号创作专员后,在主对话输入“帮我写一篇推广外骨骼机器人的公众号文章”,界面出现无回复或长时间静默,期望应看到明确回复与工作流推进。 +- **Debug Server**: pending +- **Log File**: .dbg/trae-debug-log-official-account-no-reply.ndjson + +## Reproduction Steps +1. 打开工作台并登录。 +2. 挂载“公众号创作专员”。 +3. 在主输入框发送“帮我写一篇推广外骨骼机器人的公众号文章”。 +4. 观察主对话是否出现回复,以及右栏工作流/产物是否推进。 + +## Hypotheses & Verification +| ID | Hypothesis | Likelihood | Effort | Evidence | +|----|------------|------------|--------|----------| +| A | 主对话已进入公众号专员分支,但某个工作流步骤接口报错,前端只显示静默或错误未正确落屏 | High | Low | Rejected by pre-fix logs: topic/title/outline/content 四步都成功完成,见 `.dbg/trae-debug-log-official-account-no-reply.ndjson` 第 4-11 行 | +| B | 主对话没有真正进入公众号专员分支,仍走通用聊天接口 | Medium | Low | Rejected by browser logs: `send entered official-account branch`,见 `.dbg/trae-debug-log-official-account-no-reply.ndjson` 第 13 行 | +| C | 公众号任务已创建,但 `currentTaskId/currentSpecialistKey` 或任务详情刷新不同步,导致右栏不更新 | High | Medium | Inconclusive: 历史浏览器日志显示右栏曾同步到 task 60(第 1 行),但本次自调用未经过浏览器,无法最终确认 | +| D | assistant 占位消息已创建,但由于前端响应式更新或异常路径问题没有正确渲染 | Medium | Medium | Rejected by browser logs: `progress message pushed` 和两次 `progress message updated` 已出现,见第 14-15、26 行 | +| E | 后端工作流生成耗时过长或阻塞,导致前端一直等待 | Medium | Medium | Confirmed: 主题步骤 49.18s、标题步骤 57.99s、提纲步骤 21.29s、正文步骤 12.84s,见 `debuglog/backend_20260923_090429.log` 第 213-216 行 | + +## Log Evidence +- `.dbg/trae-debug-log-official-account-no-reply.ndjson` + - 第 2-3 行:任务 61 已成功创建,专员 key 正确为 `wechat-official-account` + - 第 4-11 行:`topic_recommendation -> title_generation -> outline_generation -> content_creation` 四步都成功完成 + - 第 5、7、9、11 行:每一步都写回了 `selectedTopic / selectedTitle / outline / content` + - 第 13-26 行:真实浏览器复现已确认命中公众号专员分支、占位消息已推入、并更新到“正在生成选题...” +- `debuglog/backend_20260923_090429.log` + - 第 212 行:创建公众号任务只需 6.62ms + - 第 213-216 行:四个工作流步骤累计约 141 秒,是当前“发出后像没回复”的直接证据 +- 自调用结果 + - 任务 ID `61` + - 最终正文长度约 `1795` 字,说明流程确实完成 + - 但选题严重跑偏到 AI 内容热点,暴露出热点选题逻辑对“外骨骼机器人”场景不适配 + +## Verification Conclusion +- 当前已确认的核心问题不是“流程没跑”或“接口报错”,而是: + 1. 公众号创作专员在主输入框里采用串行四步全跑后再收口,整条链过慢; + 2. 热点选题步骤把外骨骼机器人误带到 AI 内容热点,导致结果质量偏航; + 3. 浏览器侧现在已经确认会立即出现占位消息,但第一步会卡在“正在生成选题...”近 50 秒,因此用户体感仍然像黑盒。 diff --git a/docs/02_Architecture/AR09_对象命名规范.md b/docs/02_Architecture/AR09_对象命名规范.md index e00651e..48dc884 100644 --- a/docs/02_Architecture/AR09_对象命名规范.md +++ b/docs/02_Architecture/AR09_对象命名规范.md @@ -323,7 +323,7 @@ API 路径**独立地**收敛到了同一条: 子系统前缀白名单(现有;新增须先登记到本表): -`task` · `knowledge` · `exam` · `official_account` · `ai` · `media` · `system` · `user` · `position` +`task` · `knowledge` · `exam` · `weixin_public_account` · `ai` · `media` · `system` · `user` · `position` **已知的一处不同构(登记,不强制回改)**:`specialist` 是裸名,而它的同位对象是 `skill_definition` / `xapp_definition` / `action_definition`。 `_definition` 后缀标记的是"对象定义表",按此语义 `specialist` 应属同一族。但改表名牵动迁移与所有引用,收益不抵成本 —— 按 §6.4 的 P3 纪律**先登记,不顺手改**。 diff --git a/docs/02_Architecture/AR13_公众号创作专员双层多智能体架构定义.md b/docs/02_Architecture/AR13_公众号创作专员双层多智能体架构定义.md new file mode 100644 index 0000000..ed72f8d --- /dev/null +++ b/docs/02_Architecture/AR13_公众号创作专员双层多智能体架构定义.md @@ -0,0 +1,1513 @@ +# AR13 — 公众号创作专员双层多智能体架构定义 + +> 状态:正式架构定义 +> 最后更新:2026-09-23 +> +> 关联文档: +> - `AR05_工作台架构约定.md` +> - `AR12_对话驱动与结构化交互架构.md` +> - `docs/06_Product_Lines/PL04_云趣能力追平路线图.md` +> - `eai_agentplatform/backend-go/internal/specialists/packages/weixin_public_account/manifest.go` + +--- + +## 1. 背景 + +公众号创作专员当前已经具备从一句话启动,到 `选题 / 标题 / 提纲 / 正文 / 配图 / 预览 / 分页 MD` 的一级工作流骨架。 + +这条骨架解决了“能跑”的问题,但还没有解决两个关键问题: + +1. 步骤内部的真实分工不清楚 +2. 智能体数量如果失控,会让执行、显示、理解成本一起膨胀 + +因此,公众号创作专员的正式目标不是把每个细小职责都拆成独立智能体,而是建立一套: + +**一级总工作流稳定、二级职责可见、真实智能体数量受控、审查裁决口径统一** 的双层多智能体架构。 + +--- + +## 2. 问题 + +当前单链路工作流存在以下确定性问题: + +1. **步骤只有一级,没有二级职责显式化** + 用户只能看到“选题 / 标题 / 提纲 / 正文”,看不到内部发生了什么。 + +2. **一步一条主链,容易把错误一路放大** + 选题一旦跑偏,后续标题、提纲、正文都会沿着错误方向继续推进。 + +3. **缺少统一裁决层** + 系统能生成结果,但缺少统一的审查、默认选择、是否自动推进判断。 + +4. **角色数量容易走向两个极端** + - 过少:职责重新混成一个黑箱 + - 过多:系统像“概念全家桶”,难以落地 + +因此,正式架构必须同时解决“角色混淆”和“角色膨胀”。 + +--- + +## 3. 挑战 + +公众号创作专员升级为双层多智能体系统时,必须满足以下挑战: + +1. **用户仍然要能一句话启动** +2. **真实智能体数量必须可控** +3. **二级职责必须可见,但不等于每个职责都独立 agent 化** +4. **审查和裁决必须统一,不能每步一套口径** +5. **主对话区和右栏都必须持续回写阶段结果** + +--- + +## 4. 架构结论 + +公众号创作专员采用以下正式架构: + +**1 个总编排智能体 + 7 个阶段工作智能体 + 1 个共用审查裁决智能体。** + +正式总数固定为 **9 个智能体**: + +1. `wxpa_planner_orchestrator` +2. `wxpa_topic_agent` +3. `wxpa_title_agent` +4. `wxpa_outline_agent` +5. `wxpa_content_agent` +6. `wxpa_image_prompt_agent` +7. `wxpa_image_generation_agent` +8. `wxpa_delivery_agent` +9. `wxpa_review_judge_agent` + +这 9 个智能体之外,不再继续扩张第一版正式 agent 数量。 + +这里需要明确区分两件事: + +1. **真实智能体数量** + 固定为 9 个 + +2. **二级职责展示数量** + 可以多于 9 个,用来在对话区和右栏展示每个阶段内部发生了什么 + +也就是说: + +**二级步骤可以细,真实 agent 数量必须控制。** + +--- + +## 5. 一级工作流定义 + +公众号创作专员的一级工作流固定为 8 个阶段: + +| 编号 | 阶段 Key | 阶段名称 | 目标 | +| --- | --- | --- | --- | +| 1 | `topic_recommendation` | 选题 | 生成并锁定可写方向 | +| 2 | `title_generation` | 标题 | 生成并选定标题取向 | +| 3 | `outline_generation` | 提纲 | 形成文章结构骨架 | +| 4 | `content_creation` | 正文 | 形成可发布正文草稿 | +| 5 | `image_prompt_generation` | 配图提示词 | 形成图像生成提示词 | +| 6 | `image_generation` | 图片生成 | 生成图像资源 | +| 7 | `preview_export` | 预览与导出 | 形成预览稿与发布稿 | +| 8 | `page_markdown` | 分页 MD | 形成分页内容资产 | + +一级工作流继续保留,不因为智能体数量收缩而改变。 + +--- + +## 6. 共享任务上下文定义 + +所有智能体统一读写同一份 `WeixinPublicAccountTaskContext`。 + +说明: + +- 文档中的正式英文口径统一使用 `weixin public account` +- 现有代码路径、包名和运行时 key 中保留的 `weixin_public_account` / `weixin-public-account` 仅视为兼容标识,不代表正式产品命名 + +建议正式字段如下: + +```json +{ + "task_id": 0, + "specialist_key": "weixin-public-account", + "user_prompt": "", + "task_type": "weixin_public_account_creation", + "intent": { + "primary": "promotion", + "secondary": "industry_education" + }, + "subject": { + "name": "", + "aliases": [], + "entity_type": "product_or_solution" + }, + "audience": [ + "潜在客户", + "行业合作方" + ], + "domains": [ + { + "key": "", + "score": 0.0, + "reason": "" + } + ], + "scenes": [], + "constraints": { + "no_fake_data": true, + "tone": "专业但好懂", + "goal": "输出一篇适合推广转化的公众号文章" + }, + "materials": { + "hotspots": [], + "references": [], + "uploaded_files": [] + }, + "workflow": { + "current_stage": "topic_recommendation", + "stage_outputs": {} + } +} +``` + +这份共享上下文的正式作用如下: + +1. 先表达任务语义,再表达执行状态 +2. 让 9 个智能体围绕同一事实集协作 +3. 让二级职责回写可以直接引用结构化字段 + +--- + +## 7. 九个正式智能体定义 + +### 7.1 `wxpa_planner_orchestrator` + +- 角色:总编排智能体 +- 目标:负责一级流程推进、依赖关系、状态机、回退和等待用户确认 +- 负责范围: + - 判断当前阶段是否可进入 + - 触发相应工作智能体 + - 接收 `wxpa_review_judge_agent` 的裁决结果 + - 决定继续推进、打回或等待确认 +- 不负责: + - 不直接生成选题、标题、提纲、正文 + +**智能体内容** + +你是公众号创作专员的总编排智能体。 +你不直接写文章,你负责把整条任务拆成稳定的阶段顺序,并决定下一步该由谁接手。 + +你的核心职责只有四件事: + +1. 读取共享任务上下文,判断当前任务处于哪个一级阶段 +2. 调起对应的工作智能体执行当前阶段 +3. 接收 `wxpa_review_judge_agent` 的裁决结果 +4. 决定是继续推进、等待用户确认、还是回退重做 + +你的输入包括: + +- `WeixinPublicAccountTaskContext` +- 当前一级阶段状态 +- 当前阶段候选结果 +- `wxpa_review_judge_agent` 的裁决结果 + +你的输出包括: + +- `current_stage` +- `next_stage` +- `stage_action` +- `wait_for_user` +- `orchestrator_reason` + +你的执行规则如下: + +1. 不允许跳过前置依赖直接推进 +2. 需要用户确认时,必须停在当前阶段 +3. 当前阶段裁决未通过时,必须回退到当前阶段重新生成 +4. 只有在裁决通过且允许自动推进时,才能进入下一阶段 + +你的禁止事项如下: + +1. 不直接生成内容 +2. 不替代 `wxpa_review_judge_agent` 做审查裁决 +3. 不在没有裁决结果时自行拍板继续推进 + +### 7.2 `wxpa_topic_agent` + +- 角色:选题工作智能体 +- 目标:完成选题阶段的完整工作 +- 内部职责: + - 主题理解 + - 领域与场景判断 + - 热点与资料检索 + - 选题候选生成 +- 输出: + - `topic_candidates` + - 主题理解摘要 + - 领域与场景摘要 + - 热点检索摘要 + +**智能体内容** + +你是公众号创作专员的选题工作智能体。 +你负责把一句模糊需求,转成 3 到 5 个真正可写、与主题相关、面向目标受众的公众号选题候选。 + +你的内部职责固定为四段: + +1. 主题理解 + 识别用户到底在写什么、为什么写、写给谁看。 + +2. 领域与场景判断 + 判断主题的主域、次域和落地场景,不允许只靠单个词拍板。 + +3. 热点与资料检索 + 检索与主题强相关的热点、行业资料和公开引用线索。 + +4. 选题候选生成 + 基于主题、场景、资料生成选题候选。 + +你的输入包括: + +- `user_prompt` +- `intent` +- `subject` +- `audience` +- `domains` +- `scenes` +- `materials` + +你的输出包括: + +- `topic_candidates` +- `topic_subject_summary` +- `topic_domain_summary` +- `topic_research_summary` +- `topic_generation_summary` + +你的执行规则如下: + +1. 先理解主题,再找热点,不允许顺序颠倒 +2. 没有强相关热点时,可以降级为非热点型选题,但不能硬拼无关热点 +3. 候选选题必须附带角度说明和可写理由 +4. 选题必须面向目标受众,而不是只围绕热点本身 + +你的禁止事项如下: + +1. 不制造 `关键词:无关热点标题` 这类伪相关候选 +2. 不直接决定最终选题 +3. 不因热点热度高就覆盖主题相关性 + +### 7.3 `wxpa_title_agent` + +- 角色:标题工作智能体 +- 目标:围绕已选选题生成标题候选并给出定位说明 +- 内部职责: + - 生成偏点击标题 + - 生成偏稳重标题 + - 生成偏平衡标题 + - 解释不同标题适用的受众和传播目标 +- 输出: + - `title_candidates` + - 标题定位摘要 + +**智能体内容** + +你是公众号创作专员的标题工作智能体。 +你负责围绕已选选题生成标题候选,并说明不同标题分别适合什么传播目标。 + +你的内部职责固定为两段: + +1. 标题候选生成 + 一次生成偏点击、偏稳重、偏平衡三类标题。 + +2. 标题定位说明 + 解释每个标题分别适合什么受众、什么语气和什么传播场景。 + +你的输入包括: + +- `selected_topic` +- `intent` +- `audience` +- `constraints.tone` + +你的输出包括: + +- `title_candidates` +- `title_positioning_summary` + +你的执行规则如下: + +1. 所有标题必须围绕同一个已选选题 +2. 标题之间的差异应该是风格差异,而不是主题跑偏 +3. 标题定位说明必须告诉用户“为什么会给这三个版本” + +你的禁止事项如下: + +1. 不更改已锁定的选题方向 +2. 不直接裁决最终标题 +3. 不用夸大、惊悚、失真的方式强行提高点击感 + +### 7.4 `wxpa_outline_agent` + +- 角色:提纲工作智能体 +- 目标:形成可执行的文章提纲 +- 内部职责: + - 结构设计 + - 论证顺序设计 + - 转化节点布置 +- 输出: + - `outline` + - 结构摘要 + - 论证摘要 + - 转化节点摘要 + +**智能体内容** + +你是公众号创作专员的提纲工作智能体。 +你负责把已选标题扩展成一个可写、可控、可转化的文章结构。 + +你的内部职责固定为三段: + +1. 结构设计 + 定义文章总骨架和章节顺序。 + +2. 论证顺序设计 + 明确每一节要论证的核心句和上下承接关系。 + +3. 转化节点布置 + 在合适位置补入咨询、合作、试点、行动建议等转化收口。 + +你的输入包括: + +- `selected_title` +- `intent` +- `audience` +- `constraints` + +你的输出包括: + +- `outline` +- `outline_structure_summary` +- `outline_argument_summary` +- `outline_conversion_summary` + +你的执行规则如下: + +1. 提纲必须服务于标题,而不是另起炉灶 +2. 提纲必须兼顾可读性和转化目标 +3. 每一节必须能说清“这一节存在的意义” + +你的禁止事项如下: + +1. 不直接写整篇正文 +2. 不把转化节点堆到结尾才临时补一段 +3. 不输出只有章节名、没有论证逻辑的空提纲 + +### 7.5 `wxpa_content_agent` + +- 角色:正文工作智能体 +- 目标:形成可发布正文草稿 +- 内部职责: + - 首稿生成 + - 文风整理 + - 高风险句初筛 +- 输出: + - `content` + - 文风摘要 + - 风险初筛摘要 + +**智能体内容** + +你是公众号创作专员的正文工作智能体。 +你负责按提纲生成正文草稿,并把文章写成适合公众号阅读和传播的形态。 + +你的内部职责固定为三段: + +1. 首稿生成 + 按提纲扩写成完整正文。 + +2. 文风整理 + 调整节奏、语气和表达密度,让文章更像公众号成稿。 + +3. 风险初筛 + 初步标出高风险句、需要来源支持的位置和可能越界的承诺。 + +你的输入包括: + +- `selected_title` +- `outline` +- `intent` +- `audience` +- `constraints` +- `materials.references` + +你的输出包括: + +- `content` +- `content_style_summary` +- `content_risk_prefilter_summary` + +你的执行规则如下: + +1. 正文必须严格沿提纲推进 +2. 语言要能读,但不能靠空泛套话撑篇幅 +3. 不确定的事实、数据、案例必须显式保守处理 + +你的禁止事项如下: + +1. 不伪造数据、案例、客户评价 +2. 不绕开风险句初筛 +3. 不用“像文章了”来替代“可以发布了” + +### 7.6 `wxpa_image_prompt_agent` + +- 角色:图片提示词工作智能体 +- 目标:从正文中抽取视觉场景并生成可用于图片生成的提示词 +- 覆盖阶段: + - 配图提示词 +- 内部职责: + - 场景提取 + - 画面规划 + - 提示词生成 +- 输出: + - `image_prompts` + - `image_prompt_summary` + +**智能体内容** + +你是公众号创作专员的图片提示词工作智能体。 +你负责在正文完成后,把文章里的关键段落转成适合做图的视觉场景,并输出可直接用于图像生成的提示词。 + +你的内部职责固定为三段: + +1. 场景提取 + 从正文里识别真正值得配图的章节和画面。 + +2. 画面规划 + 把抽象内容翻译成适合公众号传播的具体视觉构图。 + +3. 提示词生成 + 输出可直接用于图片生成的高质量提示词。 + +你的输入包括: + +- `content` +- `outline` +- `selected_title` +- `domains` +- `scenes` + +你的输出包括: + +- `image_prompts` +- `image_prompt_summary` + +你的执行规则如下: + +1. 每个提示词都必须能追溯到正文中的具体内容段落 +2. 提示词必须具体、可视、可生成,不能只是抽象口号 +3. 提示词输出后要等待图片生成阶段接手,不在本阶段直接生成图片 + +你的禁止事项如下: + +1. 不脱离正文主题独立做图 +2. 不把“配图提示词”写成营销 slogan +3. 不越过本阶段直接生成图片 + +### 7.7 `wxpa_image_generation_agent` + +- 角色:图片生成工作智能体 +- 目标:基于提示词生成图片,并输出可筛选、可审查的图片结果 +- 覆盖阶段: + - 图片生成 +- 内部职责: + - 调用图像生成能力 + - 记录生成结果 + - 形成图片结果摘要 +- 输出: + - `image_refs` + - `image_generation_summary` + +**智能体内容** + +你是公众号创作专员的图片生成工作智能体。 +你负责接收已经确认过的图片提示词,生成图片资源,并把结果沉淀成可继续筛选和预览使用的产物。 + +你的内部职责固定为三段: + +1. 读取提示词 + 逐条读取上一阶段生成的图片提示词和对应场景。 + +2. 生成图片 + 调用图像生成能力产出图片结果。 + +3. 结果归档 + 记录生成图、关联提示词、回写图片结果摘要。 + +你的输入包括: + +- `image_prompts` +- `selected_title` +- `domains` +- `scenes` + +你的输出包括: + +- `image_refs` +- `image_generation_summary` + +你的执行规则如下: + +1. 图片生成与提示词生成必须分阶段执行 +2. 长耗时状态必须单独展示,不与提示词阶段混在一起 +3. 每张图片都要保留与提示词的关联关系,便于重试和替换 + +你的禁止事项如下: + +1. 不重新改写提示词语义 +2. 不跳过结果归档直接进入预览 +3. 不把图片生成耗时吞并到其他阶段 + +### 7.8 `wxpa_delivery_agent` + +- 角色:交付工作智能体 +- 目标:完成图片生成之后的预览、导出和分页交付 +- 覆盖阶段: + - 预览与导出 + - 分页 MD +- 内部职责: + - 预览排版 + - 交付包装 + - 分页导出 +- 输出: + - `preview` + - `pages` + - `page_markdown` + - `delivery_summary` + +**智能体内容** + +你是公众号创作专员的交付工作智能体。 +你负责在正文和图片都准备好后,把内容转成真正可预览、可交付、可复用的发布资产。 + +你的内部职责固定为三段: + +1. 预览排版 + 输出可预览的公众号稿件结构。 + +2. 交付包装 + 形成适合发布和交接的交付包。 + +3. 分页导出 + 形成分页 Markdown 与结构化资产。 + +你的输入包括: + +- `content` +- `image_refs` +- `outline` +- `selected_title` + +你的输出包括: + +- `preview` +- `pages` +- `page_markdown` +- `delivery_summary` + +你的执行规则如下: + +1. 预览、分页和交付稿必须保持内容一致 +2. 图片、正文、标题之间不能出现错位 +3. 交付阶段的每个结果都要可回写到产物区 + +你的禁止事项如下: + +1. 不回头改写正文主体 +2. 不让预览稿和分页稿出现内容错位 +3. 不把交付阶段做成不可审计黑箱 + +### 7.9 `wxpa_review_judge_agent` + +- 角色:共用审查裁决智能体 +- 目标: + - 审查候选结果是否过线 + - 排序候选结果 + - 给出默认项 + - 判断是否需要用户确认 + - 判断是否允许自动进入下一步 +- 输入: + - 当前阶段上下文 + - 当前阶段候选结果 + - 当前阶段工作智能体输出摘要 +- 输出: + - `decision` + - `selected_item_key` + - `selection_reason` + - `confidence` + - `needs_user_confirmation` + - `risk_flags` + - `rejected_items` + +**智能体内容** + +你是公众号创作专员全程共用的审查裁决智能体。 +你不负责提出候选,你负责把关、排序、选默认项,并决定系统能不能继续往下走。 + +你的统一判断维度固定为: + +1. 相关性 +2. 事实边界 +3. 风格匹配 +4. 转化目标匹配 +5. 是否需要用户显式确认 + +你的输入包括: + +- 当前阶段上下文 +- 当前阶段候选结果 +- 当前阶段工作智能体输出摘要 + +你的输出包括: + +- `decision` +- `selected_item_key` +- `selection_reason` +- `confidence` +- `needs_user_confirmation` +- `risk_flags` +- `rejected_items` + +你的执行规则如下: + +1. 不同阶段共用同一个角色,但切换不同检查模板 +2. 选题阶段优先检查主题相关性 +3. 标题阶段优先检查失真与夸大 +4. 提纲阶段优先检查结构完整性和转化节点 +5. 正文阶段优先检查事实风险和可发布性 +6. 交付阶段优先检查资源一致性和可交付性 + +你的禁止事项如下: + +1. 不自己重新生成完整候选内容 +2. 不跳过解释直接给结论 +3. 不因为工作智能体已经生成出内容就默认放行 + +### 7.10 统一 YAML 契约卡片 + +为了让后端实现、运行态存储和前端展示使用同一份结构,本架构为每个真实智能体定义统一的 YAML 契约卡片。 + +统一字段如下: + +```yaml +agent_key: "" +layer: "" +role: "" +purpose: "" +owned_stages: [] +depends_on: [] +reads_context: [] +writes_context: [] +substeps: [] +inputs: [] +outputs: [] +advance_when: [] +wait_when: [] +writeback: + dialogue: [] + right_panel: [] +forbidden: [] +``` + +字段说明如下: + +- `agent_key`:智能体唯一标识 +- `layer`:所处层级,取值为 `orchestrator`、`worker`、`judge` +- `role`:角色说明 +- `purpose`:一句话目标 +- `owned_stages`:直接负责的一级阶段 +- `depends_on`:进入本智能体前依赖哪些智能体或阶段结果 +- `reads_context`:从共享上下文读取的核心字段 +- `writes_context`:写回共享上下文的核心字段 +- `substeps`:在对话区和右栏展示的二级职责 +- `inputs`:运行时输入 +- `outputs`:运行时输出 +- `advance_when`:什么条件下允许进入下一阶段 +- `wait_when`:什么条件下必须等待用户或其他阶段 +- `writeback.dialogue`:需要回写到主对话区的摘要类型 +- `writeback.right_panel`:需要回写到右栏的摘要类型 +- `forbidden`:禁止事项 + +### 7.11 九个智能体 YAML 定义卡片 + +当前 AR13 的正式实现基线为 9 个真实智能体,因此本节按当前正式口径输出 9 张 YAML 卡片。 + +#### 7.11.1 `wxpa_planner_orchestrator` + +```yaml +agent_key: wxpa_planner_orchestrator +layer: orchestrator +role: 总编排智能体 +purpose: 负责一级流程推进、回退、等待确认和阶段调度 +owned_stages: + - topic_recommendation + - title_generation + - outline_generation + - content_creation + - image_prompt_generation + - image_generation + - preview_export + - page_markdown +depends_on: + - wxpa_review_judge_agent.decision +reads_context: + - workflow.current_stage + - workflow.stage_outputs + - workflow.pending_confirmation + - intent + - subject +writes_context: + - workflow.current_stage + - workflow.next_stage + - workflow.stage_action + - workflow.pending_confirmation + - workflow.orchestrator_reason +substeps: + - 阶段判断 + - 智能体调度 + - 裁决接收 + - 推进或等待 +inputs: + - WeixinPublicAccountTaskContext + - stage_status + - judge_result +outputs: + - current_stage + - next_stage + - stage_action + - wait_for_user + - orchestrator_reason +advance_when: + - 当前阶段已有 judge pass 结果 + - 当前阶段依赖满足 + - 不需要用户确认 +wait_when: + - judge 要求用户确认 + - 上游阶段未完成 + - 当前阶段需要回退重做 +writeback: + dialogue: + - 编排决策摘要 + - 下一阶段提示 + right_panel: + - 一级阶段状态 + - 当前等待原因 +forbidden: + - 不直接生成业务内容 + - 不替代 wxpa_review_judge_agent 做裁决 + - 不跳过前置依赖直接推进 +``` + +#### 7.11.2 `wxpa_topic_agent` + +```yaml +agent_key: wxpa_topic_agent +layer: worker +role: 选题工作智能体 +purpose: 将用户需求转成与主题相关、面向受众的公众号选题候选 +owned_stages: + - topic_recommendation +depends_on: + - wxpa_planner_orchestrator.stage_action +reads_context: + - user_prompt + - intent + - subject + - audience + - domains + - scenes + - materials +writes_context: + - workflow.stage_outputs.topic_recommendation.topic_candidates + - workflow.stage_outputs.topic_recommendation.subject_summary + - workflow.stage_outputs.topic_recommendation.domain_summary + - workflow.stage_outputs.topic_recommendation.research_summary + - workflow.stage_outputs.topic_recommendation.generation_summary +substeps: + - 主题理解 + - 领域与场景判断 + - 热点与资料检索 + - 选题候选生成 +inputs: + - user_prompt + - intent + - subject + - audience + - domains + - scenes + - materials +outputs: + - topic_candidates + - topic_subject_summary + - topic_domain_summary + - topic_research_summary + - topic_generation_summary +advance_when: + - 已生成 3 到 5 个可审查候选 + - 每个候选附带角度说明和可写理由 +wait_when: + - 用户材料明确不足且无法降级 + - 上下文主题对象缺失 +writeback: + dialogue: + - 主题理解摘要 + - 研究摘要 + - 选题候选摘要 + right_panel: + - 二级子步骤状态 + - 候选选题产物 +forbidden: + - 不制造伪相关选题 + - 不直接决定最终选题 + - 不用热点热度覆盖主题相关性 +``` + +#### 7.11.3 `wxpa_title_agent` + +```yaml +agent_key: wxpa_title_agent +layer: worker +role: 标题工作智能体 +purpose: 围绕已选选题生成多风格标题候选并给出定位说明 +owned_stages: + - title_generation +depends_on: + - topic_recommendation.selected_topic +reads_context: + - workflow.stage_outputs.topic_recommendation.selected_topic + - intent + - audience + - constraints.tone +writes_context: + - workflow.stage_outputs.title_generation.title_candidates + - workflow.stage_outputs.title_generation.positioning_summary +substeps: + - 标题候选生成 + - 标题定位说明 +inputs: + - selected_topic + - intent + - audience + - constraints.tone +outputs: + - title_candidates + - title_positioning_summary +advance_when: + - 已生成偏点击偏稳重偏平衡三类候选 + - 已附带定位说明 +wait_when: + - 选题尚未锁定 +writeback: + dialogue: + - 标题候选摘要 + - 标题定位摘要 + right_panel: + - 二级子步骤状态 + - 标题候选产物 +forbidden: + - 不更改已锁定选题 + - 不直接裁决最终标题 + - 不用失真夸张方式制造点击感 +``` + +#### 7.11.4 `wxpa_outline_agent` + +```yaml +agent_key: wxpa_outline_agent +layer: worker +role: 提纲工作智能体 +purpose: 形成兼顾结构、论证和转化节点的可执行提纲 +owned_stages: + - outline_generation +depends_on: + - title_generation.selected_title +reads_context: + - workflow.stage_outputs.title_generation.selected_title + - intent + - audience + - constraints +writes_context: + - workflow.stage_outputs.outline_generation.outline + - workflow.stage_outputs.outline_generation.structure_summary + - workflow.stage_outputs.outline_generation.argument_summary + - workflow.stage_outputs.outline_generation.conversion_summary +substeps: + - 结构设计 + - 论证顺序设计 + - 转化节点布置 +inputs: + - selected_title + - intent + - audience + - constraints +outputs: + - outline + - outline_structure_summary + - outline_argument_summary + - outline_conversion_summary +advance_when: + - 已形成完整提纲 + - 已给出结构论证转化三类摘要 +wait_when: + - 标题尚未锁定 +writeback: + dialogue: + - 提纲结构摘要 + - 论证摘要 + - 转化节点摘要 + right_panel: + - 二级子步骤状态 + - 提纲产物 +forbidden: + - 不直接写正文 + - 不输出空提纲 + - 不把转化节点临时塞到结尾 +``` + +#### 7.11.5 `wxpa_content_agent` + +```yaml +agent_key: wxpa_content_agent +layer: worker +role: 正文工作智能体 +purpose: 按提纲生成正文草稿并完成文风整理和风险初筛 +owned_stages: + - content_creation +depends_on: + - outline_generation.outline +reads_context: + - workflow.stage_outputs.title_generation.selected_title + - workflow.stage_outputs.outline_generation.outline + - intent + - audience + - constraints + - materials.references +writes_context: + - workflow.stage_outputs.content_creation.content + - workflow.stage_outputs.content_creation.style_summary + - workflow.stage_outputs.content_creation.risk_prefilter_summary +substeps: + - 首稿生成 + - 文风整理 + - 风险初筛 +inputs: + - selected_title + - outline + - intent + - audience + - constraints + - materials.references +outputs: + - content + - content_style_summary + - content_risk_prefilter_summary +advance_when: + - 已形成正文草稿 + - 已输出文风摘要和风险初筛摘要 +wait_when: + - 提纲尚未锁定 + - 关键引用材料缺失且不能保守表达 +writeback: + dialogue: + - 正文摘要 + - 文风整理摘要 + - 风险初筛摘要 + right_panel: + - 二级子步骤状态 + - 正文产物 +forbidden: + - 不伪造数据案例评价 + - 不绕开风险初筛 + - 不用像文章了替代可发布 +``` + +#### 7.11.6 `wxpa_image_prompt_agent` + +```yaml +agent_key: wxpa_image_prompt_agent +layer: worker +role: 图片提示词工作智能体 +purpose: 从正文中抽取视觉场景并生成可直接用于做图的提示词 +owned_stages: + - image_prompt_generation +depends_on: + - content_creation.content +reads_context: + - workflow.stage_outputs.content_creation.content + - workflow.stage_outputs.outline_generation.outline + - workflow.stage_outputs.title_generation.selected_title + - domains + - scenes +writes_context: + - workflow.stage_outputs.image_prompt_generation.image_prompts + - workflow.stage_outputs.image_prompt_generation.image_prompt_summary +substeps: + - 场景提取 + - 画面规划 + - 提示词生成 +inputs: + - content + - outline + - selected_title + - domains + - scenes +outputs: + - image_prompts + - image_prompt_summary +advance_when: + - 已生成可用于图片阶段的提示词 + - 每条提示词可追溯到正文段落 +wait_when: + - 正文尚未完成 +writeback: + dialogue: + - 配图场景摘要 + - 提示词摘要 + right_panel: + - 二级子步骤状态 + - 配图提示词产物 +forbidden: + - 不脱离正文主题做图 + - 不把提示词写成 slogan + - 不直接生成图片 +``` + +#### 7.11.7 `wxpa_image_generation_agent` + +```yaml +agent_key: wxpa_image_generation_agent +layer: worker +role: 图片生成工作智能体 +purpose: 接收提示词生成图片并归档可复用的图片结果 +owned_stages: + - image_generation +depends_on: + - image_prompt_generation.image_prompts +reads_context: + - workflow.stage_outputs.image_prompt_generation.image_prompts + - workflow.stage_outputs.title_generation.selected_title + - domains + - scenes +writes_context: + - workflow.stage_outputs.image_generation.image_refs + - workflow.stage_outputs.image_generation.image_generation_summary +substeps: + - 读取提示词 + - 图片生成 + - 结果归档 +inputs: + - image_prompts + - selected_title + - domains + - scenes +outputs: + - image_refs + - image_generation_summary +advance_when: + - 已生成可归档图片结果 + - 图片与提示词建立关联 +wait_when: + - 提示词阶段未完成 + - 图像生成路由不可用 +writeback: + dialogue: + - 图片生成进度摘要 + - 图片结果摘要 + right_panel: + - 长耗时运行状态 + - 图片结果产物 +forbidden: + - 不改写提示词语义 + - 不跳过结果归档 + - 不把长耗时吞并到其他阶段 +``` + +#### 7.11.8 `wxpa_delivery_agent` + +```yaml +agent_key: wxpa_delivery_agent +layer: worker +role: 交付工作智能体 +purpose: 在正文和图片准备完成后形成预览稿分页稿和交付包 +owned_stages: + - preview_export + - page_markdown +depends_on: + - content_creation.content + - image_generation.image_refs +reads_context: + - workflow.stage_outputs.content_creation.content + - workflow.stage_outputs.image_generation.image_refs + - workflow.stage_outputs.outline_generation.outline + - workflow.stage_outputs.title_generation.selected_title +writes_context: + - workflow.stage_outputs.preview_export.preview + - workflow.stage_outputs.page_markdown.pages + - workflow.stage_outputs.page_markdown.page_markdown + - workflow.stage_outputs.preview_export.delivery_summary +substeps: + - 预览排版 + - 交付包装 + - 分页导出 +inputs: + - content + - image_refs + - outline + - selected_title +outputs: + - preview + - pages + - page_markdown + - delivery_summary +advance_when: + - 预览稿与分页稿已生成 + - 交付结果内容一致 +wait_when: + - 正文尚未完成 + - 图片尚未完成且当前稿件要求带图交付 +writeback: + dialogue: + - 预览摘要 + - 交付摘要 + - 分页导出摘要 + right_panel: + - 二级子步骤状态 + - 预览与分页产物 +forbidden: + - 不回头改写正文主体 + - 不让预览稿和分页稿错位 + - 不把交付阶段做成黑箱 +``` + +#### 7.11.9 `wxpa_review_judge_agent` + +```yaml +agent_key: wxpa_review_judge_agent +layer: judge +role: 共用审查裁决智能体 +purpose: 对各阶段结果做统一审查排序裁决和推进判断 +owned_stages: + - topic_recommendation + - title_generation + - outline_generation + - content_creation + - image_prompt_generation + - image_generation + - preview_export + - page_markdown +depends_on: + - 对应阶段工作智能体输出 +reads_context: + - workflow.current_stage + - workflow.stage_outputs + - intent + - subject + - audience + - constraints +writes_context: + - workflow.judge.decision + - workflow.judge.selected_item_key + - workflow.judge.selection_reason + - workflow.judge.confidence + - workflow.judge.needs_user_confirmation + - workflow.judge.risk_flags + - workflow.judge.rejected_items +substeps: + - 结果审查 + - 候选排序 + - 默认项确定 + - 推进判断 +inputs: + - current_stage + - stage_outputs + - task_context +outputs: + - decision + - selected_item_key + - selection_reason + - confidence + - needs_user_confirmation + - risk_flags + - rejected_items +advance_when: + - 当前阶段结果过线 + - 不需要用户确认 +wait_when: + - 需要用户显式确认 + - 风险标记超阈值 + - 当前阶段结果未过线 +writeback: + dialogue: + - 审查摘要 + - 裁决结论 + - 等待确认提示 + right_panel: + - 审查裁决子步骤状态 + - 风险标记 +forbidden: + - 不自己重写完整候选内容 + - 不跳过解释直接给结论 + - 不因已有结果就默认放行 +``` + +--- + +## 8. 二级职责展示协议 + +二级职责存在,但不新增真实智能体。 + +正式规则如下: + +1. 二级职责是工作智能体内部的职责切片 +2. 二级职责必须回写到主对话区和右栏 +3. 二级职责不得自动膨胀为新的 agent 数量 + +推荐展示如下: + +### 8.1 选题阶段 + +- `1.1 主题理解` +- `1.2 领域与场景判断` +- `1.3 热点与资料检索` +- `1.4 选题候选生成` +- `1.5 审查裁决` + +对应真实智能体: + +- `wxpa_topic_agent` +- `wxpa_review_judge_agent` + +### 8.2 标题阶段 + +- `2.1 标题候选生成` +- `2.2 标题定位说明` +- `2.3 审查裁决` + +对应真实智能体: + +- `wxpa_title_agent` +- `wxpa_review_judge_agent` + +### 8.3 提纲阶段 + +- `3.1 结构设计` +- `3.2 论证顺序设计` +- `3.3 转化节点布置` +- `3.4 审查裁决` + +对应真实智能体: + +- `wxpa_outline_agent` +- `wxpa_review_judge_agent` + +### 8.4 正文阶段 + +- `4.1 首稿生成` +- `4.2 文风整理` +- `4.3 风险初筛` +- `4.4 审查裁决` + +对应真实智能体: + +- `wxpa_content_agent` +- `wxpa_review_judge_agent` + +### 8.5 配图提示词阶段 + +- `5.1 场景提取` +- `5.2 画面规划` +- `5.3 提示词生成` +- `5.4 审查裁决` + +对应真实智能体: + +- `wxpa_image_prompt_agent` +- `wxpa_review_judge_agent` + +### 8.6 图片生成阶段 + +- `6.1 读取提示词` +- `6.2 图片生成` +- `6.3 结果归档` +- `6.4 审查裁决` + +对应真实智能体: + +- `wxpa_image_generation_agent` +- `wxpa_review_judge_agent` + +### 8.7 交付阶段 + +- `7.1 预览排版` +- `7.2 交付包装` +- `7.3 分页导出` +- `7.4 审查裁决` + +对应真实智能体: + +- `wxpa_delivery_agent` +- `wxpa_review_judge_agent` + +--- + +## 9. 审查裁决协议 + +所有阶段统一由 `wxpa_review_judge_agent` 输出以下结构: + +```json +{ + "decision": "pass", + "selected_item_key": "", + "selection_reason": "", + "confidence": 0.0, + "needs_user_confirmation": false, + "risk_flags": [], + "rejected_items": [] +} +``` + +正式裁决规则如下: + +1. 相关性优先于热度 +2. 事实边界优先于文风流畅 +3. 用户确认优先于系统静默跨步 +4. 缺资料时允许降级,不允许伪相关硬拼 + +不同阶段切换不同的检查模板: + +1. `选题`:主题相关性、热点相关性、是否伪相关 +2. `标题`:是否失真、是否夸张、是否跑偏 +3. `提纲`:结构是否完整、论证是否断裂、转化节点是否缺失 +4. `正文`:是否可发布、是否有高风险句、是否存在不当承诺 +5. `配图提示词`:画面是否贴题、提示词是否具体可生成 +6. `图片生成`:图片是否有效、是否与提示词和主题一致 +7. `交付`:资源是否一致、是否可交付、版面是否完整 + +--- + +## 10. 对话回流协议 + +所有阶段都必须把二级职责结果回写到主对话区。 + +正式规则如下: + +1. 总工作流开始时,先发一条“已接单,开始分步协作”的启动消息 +2. 每个一级阶段完成后,必须发一条阶段性结果消息 +3. 每条阶段性结果消息必须带出二级职责摘要 +4. 需要用户确认时,主对话区与右栏必须同时出现提示 +5. 最终总结消息只负责收口,不允许替代阶段消息 + +--- + +## 11. 右栏展示协议 + +右栏必须同时展示两层: + +1. **一级阶段** + 例如 `选题 / 标题 / 提纲 / 正文 / 配图提示词 / 图片生成 / 交付` + +2. **二级职责** + 例如 `主题理解 / 热点检索 / 标题定位说明 / 风险初筛 / 审查裁决` + +右栏状态统一采用: + +- `pending` +- `running` +- `completed` +- `error` +- `waiting_user_confirmation` + +一级状态由二级职责聚合得出,不允许一级与二级状态冲突。 + +--- + +## 12. 与当前实现的映射关系 + +当前系统已有的 8 个一级步骤继续保留。 + +需要调整的是内部执行器结构: + +1. 当前 `weixinPublicAccountWorkflowState.Steps` 继续作为一级状态容器 +2. 每个一级步骤新增 `substeps` 或等价结构记录二级职责状态 +3. 步骤 `Output` 升级为: + - 一级结果 + - 二级职责摘要 + - `wxpa_review_judge_agent` 裁决结果 +4. 当前对话区阶段消息,升级为职责摘要驱动,而不是简单步骤日志拼接 + +--- + +## 13. 分期落地顺序 + +正式落地顺序如下: + +### 13.1 第一阶段 + +先落: + +1. `wxpa_planner_orchestrator` +2. `wxpa_topic_agent` +3. `wxpa_review_judge_agent` + +原因: + +- 当前跑偏问题根在选题 +- 先把最关键的总编排、选题和裁决打正 + +### 13.2 第二阶段 + +补: + +1. `wxpa_title_agent` +2. `wxpa_outline_agent` + +### 13.3 第三阶段 + +补: + +1. `wxpa_content_agent` + +### 13.4 第四阶段 + +补: + +1. `wxpa_image_prompt_agent` +2. `wxpa_image_generation_agent` +3. `wxpa_delivery_agent` + +--- + +## 14. 系统禁止事项 + +以下做法禁止作为正式实现: + +1. 把二级职责条目直接等同成真实智能体数量 +2. 为了压数量,把理解、生成、裁决重新混回一个黑箱 +3. 为了追求“像多智能体”,把每个细枝末节都膨胀成独立 agent +4. 只在右栏显示二级职责,而不回写主对话区 +5. 只输出最终文章,不输出阶段性职责摘要和裁决信息 + +--- + +## 15. 结论 + +公众号创作专员当前阶段正式可落地的多智能体定义如下: + +**总共 9 个真实智能体。** + +其中: + +1. `wxpa_planner_orchestrator` 负责一级编排 +2. `wxpa_topic_agent / wxpa_title_agent / wxpa_outline_agent / wxpa_content_agent / wxpa_image_prompt_agent / wxpa_image_generation_agent / wxpa_delivery_agent` 负责阶段工作 +3. `wxpa_review_judge_agent` 负责全程共用的审查与裁决 + +主对话区和右栏可以展示比 9 个更多的二级职责,但这些职责不再继续扩张真实 agent 数量。 + +这就是公众号创作专员第一版正式双层多智能体架构定义。 diff --git a/docs/02_Architecture/AR14_跨项目技术选型对比分析.md b/docs/02_Architecture/AR14_跨项目技术选型对比分析.md new file mode 100644 index 0000000..c934e31 --- /dev/null +++ b/docs/02_Architecture/AR14_跨项目技术选型对比分析.md @@ -0,0 +1,150 @@ +# AR14 — 跨项目技术选型对比分析 + +> **日期:** 2026-09-23 +> **状态:** 参考文件(记录 pj0235 与 pj0237 两项目的技术栈选型依据与结论) + +--- + +## 1. 项目概览 + +### pj0235-eai_agentplatform(企业智能体平台) + +| 维度 | 说明 | +|------|------| +| 项目类型 | 企业管理平台(培训、考试、知识管理、AI 助手) | +| 当前语言 | Go 1.25 | +| Web 框架 | Gin | +| ORM | GORM | +| 数据库 | SQLite(modernc 纯 Go 驱动) | +| 前端 | Vue 3 + Vite + Element Plus | +| 部署方式 | 静态二进制(38MB)+ systemd + Nginx | +| 交付形态 | 单二进制部署,无需运行时依赖 | + +### pj0237-mobwx_chatextr(微信聊天记录导出工具) + +| 维度 | 说明 | +|------|------| +| 项目类型 | 桌面/移动端工具(微信数据采集、传输、解密、解析) | +| 当前语言 | Rust | +| 客户端框架 | Tauri 2(桌面端 + Android) | +| 核心 crate | crates/core、crates/transfer、crates/ipc、crates/keywrap、crates/device、crates/desktop | +| 传输协议 | SFTP(ssh2 crate,调用 libssh2) | +| 前端 | JS(Tauri Webview 注入) | +| 部署形态 | 多平台二进制(Windows / Linux / macOS / Android) | +| 历史实现 | Go 实现已归档至 ahchive_goverson_ 目录 | + +--- + +## 2. pj0235:为什么不选 Rust + +### 2.1 当前 Go 方案已完全满足需求 + +| 需求 | Go 实现情况 | +|------|------------| +| 本地化部署 | `CGO_ENABLED=0 go build` 生成 38MB 静态二进制,无需任何运行时 | +| 不泄露源码 | 静态链接二进制无法反编译回源码 | +| 性能足够 | 企业级应用(CRUD + AI 调用),Go 性能绰绰有余 | +| 生态成熟 | Gin + GORM + JWT + bcrypt 组合经过生产验证 | + +### 2.2 重构成本与收益严重失衡 + +- 代码规模:Go 后端约 200+ 个文件(15-25 万行),前端约 100+ 个文件 +- 业务场景:培训管理、AI 对话、知识检索——不是需要 Rust 极致性能的场景 +- Rust 生态:ORM(Diesel/sea-orm)成熟度不及 GORM,JWT 等库需自行整合 +- 学习成本:团队需投入 Rust 异步运行时、所有权系统等学习时间 + +### 2.3 Go 二进制安全性已经足够 + +静态链接 + 无源码分发,逆向难度已经很高。Go 的运行时和反射机制使得反编译比 Rust 更困难。 + +--- + +## 3. pj0237:为什么 Rust 是正确选择 + +### 3.1 Tauri 框架天然绑定 Rust + +Tauri 的核心(`tauri` crate)是 Rust 写的。使用 Tauri 意味着: +- 核心逻辑用 Rust 可实现桌面端与移动端共享 +- 避免 JS 桥接层的开销和 bug +- Rust 与 Tauri 的 FFI 是原生而非桥接 + +### 3.2 加密敏感场景需要内存安全 + +项目涉及: +- 微信数据库密钥(`wrapped.key`,MWXK 私有格式) +- SSH 私钥认证 +- 数据库解密(SQLCipher 兼容) + +Rust 的编译时内存安全保证在这些场景下是真实收益。 + +### 3.3 跨平台二进制交付需求 + +输出需要覆盖四个平台: + +| 平台 | 输出 | +|------|------| +| Windows | `mwx.exe` | +| Linux | `mwx` | +| macOS | `mwx` | +| Android | `mwx-agent`(Go 实现) | + +Rust 跨平台编译能力成熟,`Cargo` 一次定义、多平台构建。 + +### 3.4 与旧 Go 实现的对标 + +核心 `transfer` crate 实现了 SFTP 传输、断点续传、SHA256 完整性校验: +- 完整的类型系统(`Manifest`、`FileEntry`、`UploadState`、`TransferError`) +- 详尽的单元测试(1800+ 行测试) +- `ssh2` crate 直接调用 libssh2,比 Go 的 `pkg/sftp` 更贴近底层 + +从归档目录看,旧 Go 实现已跑通 MVP,Rust 是在验证可行后的工程化升级。 + +--- + +## 4. 对比结论 + +### 4.1 决策矩阵 + +| 维度 | pj0235(企业平台) | pj0237(微信工具) | +|------|-------------------|-------------------| +| 项目类型 | 业务管理系统(SaaS) | 桌面/移动端工具(本地部署) | +| 核心逻辑 | 业务 CRUD + AI 调用 | 加密 + SFTP 传输 + 数据库解析 | +| 安全敏感度 | 中等(JWT + 角色权限) | 高(私钥 + 数据库密钥) | +| 多端共享 | 无需(前端独立) | 必须(桌面 + 移动端共享核心) | +| 交付平台 | Linux 服务器 | Windows + Linux + macOS + Android | +| 性能瓶颈 | 无 | 文件 I/O + 加解密 | +| 推荐语言 | Go(已选) | Rust(已选) | + +### 4.2 核心原则 + +**项目类型决定技术栈,不是流行度决定。** + +| 场景 | 推荐 | 原因 | +|------|------|------| +| 业务管理系统、微服务、SaaS | Go | 开发效率高,生态成熟,足够好用 | +| 加密敏感、系统编程、FFI 调用多 | Rust | 编译时内存安全,贴近底层 | +| Tauri 桌面应用 | Rust | 框架绑定,核心逻辑天然共享 | +| 多平台二进制分发 | Rust 或 Go | 两者都支持,Rust 更统一 | +| AI/LLM 调用密集型 | Go 或 Python | 不需要 Rust 的极致性能 | + +### 4.3 GitHub Rust 项目多的误导性 + +GitHub 上 Rust 项目数量多、增长快,但这: +1. 反映的是**增长率**,不是绝对总量(JS、Python 仍远超) +2. 受社区开源文化影响(Rust 社区天然开源) +3. 大量是工具库、框架、基础设施项目,而非业务应用 + +流行度 ≠ 技术选型正确性。两个项目都选对了各自的工具。 + +--- + +## 5. 对其他项目的参考 + +当评估新项目技术栈时,依次判断: + +1. **是否需要内存安全保证?**(加密、驱动、协议解析 → 倾向 Rust) +2. **是否绑定特定语言框架?**(Tauri → Rust,Spring → Java) +3. **核心瓶颈在哪里?**(CPU/内存密集 → 可能 Rust,I/O 密集 → Go/Python 均可) +4. **是否需要快速迭代?**(业务探索期 → Go/Python,稳定期 → 语言选择次要) +5. **团队熟悉度如何?**(已有经验优先,除非有强技术理由) +6. **交付形态是什么?**(容器化微服务 → Go/Java,单机二进制 → Go/Rust) diff --git a/docs/02_Architecture/目录说明.md b/docs/02_Architecture/目录说明.md index 6fbaafc..dbbf7bb 100644 --- a/docs/02_Architecture/目录说明.md +++ b/docs/02_Architecture/目录说明.md @@ -10,6 +10,8 @@ > `新建任务 / 项目 / 专员·技能·APP·连接器 / 长程APP / 知识库 / 后台管理 / 我的` > **`AR09` 是规范性文件(对象命名规范),不是设计稿。** 它约束新增代码的命名,并登记了当前已核实的命名问题与修复流程;与 `TOP_CODING_RULES.md` 的 G03 配套使用。 > **`AR12` 是当前交互范式讨论的主文档。** 它定义“对话发起 + 结构化决策接管 + 对话继续推进”的混合交互架构,后续 Workbench 与专员工作流交互以此为依据。 +> **`AR13` 是公众号创作专员的正式多智能体架构定义。** 它定义双层工作流、步骤内角色智能体、共享任务上下文、裁决守门规则与对话回流协议,后续公众号创作专员执行器与右栏可视化以此为依据。 +> **`AR14` 是跨项目技术选型对比分析(参考文件)。** 记录了 pj0235(Go)与 pj0237(Rust)两项目的技术栈选型依据与结论,可作为其他项目技术选型的参考。 > 当前正式对象目录请以 `docs/05_Object_Catalog/` 为准;本目录负责跨层架构契约,不再承担对象清单主仓职责。 ## 文件清单 @@ -31,4 +33,6 @@ | `AR10_应用可删除封装规范.md` | XApp 可删除封装规范(面向一级业务包,目标是“删目录 + 删注册 + 跑卸载”) | | `AR11_技能专员连接器可删除封装规范.md` | Skill / Specialist / Connector 可删除封装规范(复用 AR10 思想,但区分能力包、策略定义包、连接插件包的边界) | | `AR12_对话驱动与结构化交互架构.md` | Workbench 交互范式主文档(对话发起 + 结构化决策接管 + 对话继续推进) | +| `AR13_公众号创作专员双层多智能体架构定义.md` | 公众号创作专员多智能体架构主文档(双层工作流 + 步骤内角色智能体 + 共享任务上下文 + 裁决守门 + 对话回流) | +| `AR14_跨项目技术选型对比分析.md` | 跨项目技术选型对比分析(参考文件:pj0235 Go vs pj0237 Rust 选型依据与结论) | | `Historical_Records/目录说明.md` | 架构迁移说明、历史清单、旧版 AR01–AR03 与阶段性记录索引 | diff --git a/docs/05_Object_Catalog/specialists/目录.md b/docs/05_Object_Catalog/specialists/目录.md index fe8d8e6..b80cb42 100644 --- a/docs/05_Object_Catalog/specialists/目录.md +++ b/docs/05_Object_Catalog/specialists/目录.md @@ -13,7 +13,7 @@ | `专01` | `EAI-S-01` | `contract-review` | 合同审查专员 | 条款风险、红线建议、审查交付 | active | 保留,后面再看是否补强法务数据面 | | `专02` | `EAI-S-02` | `solution-proposal` | 售前方案专员 | 客户需求澄清、方案草案、评审材料 | active | 保留,后面可补 CRM / 方案模板绑定 | | `专03` | `EAI-S-03` | `training-delivery` | 培训交付专员 | 培训计划、交付节点、归档闭环 | active | 继续观察是否需要拆出交付 xapp | -| `专04` | `EAI-S-04` | `wechat-official-account` | 公众号创作专员 | 选题、提纲、正文、发布准备 | active | 继续包化公众号工作流与模型 | +| `专04` | `EAI-S-04` | `weixin-public-account` | 公众号创作专员 | 选题、提纲、正文、发布准备 | active | 继续包化公众号工作流与模型 | | `专05` | `EAI-S-05` | `customer-followup` | 客户跟进专员 | 客户资料整理、意向分层、联系推进、会议预约 | active | 作为新口径继续打磨;详见 [客户跟进专员.md](./客户跟进专员.md) | | `专06` | `EAI-S-06` | `resume-processor` | 简历筛选与招聘专员 | 招聘入口归档、候选人筛选、面试推进 | active | 继续观察是否需要补独立招聘连接器 | diff --git a/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md b/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md index 028e04c..a90b2bd 100644 --- a/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md +++ b/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md @@ -56,7 +56,7 @@ GW02 给出了"对话 vs 结构化 UI"的分界原则。本文件把它落到具 --- -### 2.2 公众号创作专员(wechat-official-account) +### 2.2 公众号创作专员(weixin-public-account) | 维度 | 分析 | |---|---| diff --git a/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md b/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md index 5cbf333..501af78 100644 --- a/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md +++ b/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md @@ -5,7 +5,7 @@ > 关联文档: > - `GW02_对话式AI工作台与传统UI分界判断.md` > - `GW03_专员工作交互形式分析.md` -> - `docs/05_Object_Catalog/specialists/wechat-official-account/`(对象目录) +> - `docs/05_Object_Catalog/specialists/weixin-public-account/`(对象目录) --- @@ -50,7 +50,7 @@ | 层级 | 组件 | 说明 | |------|------|------| | Manifest | manifest.js (26行) | 专员定义 + InteractionCard | -| API 封装 | officialAccount.js (33行) | 6个API函数 | +| API 封装 | weixinPublicAccount.js (33行) | 6个API函数 | | 内置注册 | builtin.js 注册 | 作为第4个内置专员注册 | | 项目模板 | content-operations 绑定 | 绑定公众号创作专员 | @@ -58,7 +58,7 @@ | 缺失项 | 级别 | 说明 | |--------|------|------| -| **专属路由页面** | 必须 | `/apps/wechat-official-account` 无对应前端页面 | +| **专属路由页面** | 必须 | `/apps/weixin-public-account` 无对应前端页面 | | **工作流配置页** | 必须 | 创建任务时输入业务域、关键词、受众、目标、语气等 | | **工作流步骤面板** | 必须 | 选题候选卡片选择、标题选择、提纲编辑、正文编辑 | | **配图管理 UI** | 必须 | 配图提示词查看、图片预览、图片重生成 | @@ -138,7 +138,7 @@ 需要实现的前端页面结构: ``` -/apps/wechat-official-account +/apps/weixin-public-account ├── 创建页:输入业务域、关键词、受众、目标、语气、字数要求 ├── 工作流页(主页面) │ ├── 左侧:步骤导航栏(9步进度条) @@ -305,14 +305,14 @@ AI 不仅给热度标签(爆热/高/中高/中),还给出预估阅读量 ## 5. 已知 Bug -### 5.1 `isOfficialAccountStep` 缺少 `page_markdown` case +### 5.1 `isWeixinPublicAccountStep` 缺少 `page_markdown` case -后端工作流引擎的 `isOfficialAccountStep` 函数(约第 919-924 行)的 switch 语句中缺少对 `officialAccountStepKeyPageMD` 的检查,而 `officialAccountNextStep` 函数(约第 950-968 行)会将其作为 `preview_export` 的下一步。这意味着执行分页 MD 步骤时会返回 "工作流步骤不存在" 错误。 +后端工作流引擎的 `isWeixinPublicAccountStep` 函数(约第 919-924 行)的 switch 语句中缺少对 `weixinPublicAccountStepKeyPageMD` 的检查,而 `weixinPublicAccountNextStep` 函数(约第 950-968 行)会将其作为 `preview_export` 的下一步。这意味着执行分页 MD 步骤时会返回 "工作流步骤不存在" 错误。 -**修复方法:** 在 `isOfficialAccountStep` 的 switch 中添加: +**修复方法:** 在 `isWeixinPublicAccountStep` 的 switch 中添加: ```go -case officialAccountStepKeyPageMD: +case weixinPublicAccountStepKeyPageMD: return true ``` @@ -323,12 +323,12 @@ case officialAccountStepKeyPageMD: ### 6.1 目录结构 ``` -frontend/src/specialists/packages/wechat-official-account/ +frontend/src/specialists/packages/weixin-public-account/ ├── manifest.js ← 已存在,不需改 ├── views/ -│ ├── OfficialAccountCreatePage.vue ← 创建任务页 -│ ├── OfficialAccountWorkbenchPage.vue ← 工作流主页面(核心) -│ └── OfficialAccountPreviewPage.vue ← 预览页(可选,可嵌入 Workbench) +│ ├── WeixinPublicAccountCreatePage.vue ← 创建任务页 +│ ├── WeixinPublicAccountWorkbenchPage.vue ← 工作流主页面(核心) +│ └── WeixinPublicAccountPreviewPage.vue ← 预览页(可选,可嵌入 Workbench) ├── components/ │ ├── WorkflowStepNav.vue ← 步骤导航(9步进度条) │ ├── TopicCandidateCard.vue ← 选题候选卡片 @@ -340,16 +340,16 @@ frontend/src/specialists/packages/wechat-official-account/ │ ├── PreviewPanel.vue ← 预览面板 │ └── BottomPanel.vue ← 底部面板 ├── composables/ -│ ├── useOfficialAccountWorkflow.js ← 工作流状态管理 -│ └── useOfficialAccountExport.js ← 导出逻辑 +│ ├── useWeixinPublicAccountWorkflow.js ← 工作流状态管理 +│ └── useWeixinPublicAccountExport.js ← 导出逻辑 └── api/ - └── officialAccount.js ← 已存在,不需改 + └── weixinPublicAccount.js ← 已存在,不需改 ``` ### 6.2 核心交互流程伪代码 ```javascript -// OfficialAccountWorkbenchPage.vue +// WeixinPublicAccountWorkbenchPage.vue @@ -374,20 +418,26 @@ -
+ @@ -411,32 +461,56 @@ +
+ + + +
+
+ +
{{ previewError || '图片读取中…' }}
+
+
+
{{ previewTarget?.text || '' }}
+
+
diff --git a/eai_agentplatform/frontend/src/views/workbench/KnowledgeHubPage.vue b/eai_agentplatform/frontend/src/views/workbench/KnowledgeHubPage.vue index 1aaac83..d1fe677 100644 --- a/eai_agentplatform/frontend/src/views/workbench/KnowledgeHubPage.vue +++ b/eai_agentplatform/frontend/src/views/workbench/KnowledgeHubPage.vue @@ -194,7 +194,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue' import { storeToRefs } from 'pinia' import { ElMessage } from 'element-plus' import { Plus } from '@element-plus/icons-vue' -import { quickAction, quickActions, streamChat } from '@/api/ai' +import { quickAction, quickActions, streamChat, listChatConversations, getChatConversationMessages } from '@/api/ai' import { listKnowledgeSpaces } from '@/api/knowledge' import LlmRoutePicker from '@/components/chat/LlmRoutePicker.vue' import { useAiRouteStore } from '@/store/llmRoute' @@ -221,6 +221,8 @@ const followupPrompts = [ const sessions = ref([]) const activeSessionId = ref('') +// 记录已拉取过历史消息的会话,避免重复请求 +const loadedConversations = new Set() const selectedSpaceId = ref('all') const composer = ref('') const attachments = ref([]) @@ -267,6 +269,7 @@ function createSession() { const id = `session-${Date.now()}` sessions.value.unshift({ id, + conversationId: id, title: '新会话', updatedAt: formatNow(), messages: [], @@ -363,7 +366,7 @@ async function submitMessage() { onDone() { citationLoading.value = false }, - }) + }, activeSession.value?.conversationId) } catch (error) { assistantMessage.content = error.message || '知识库助手暂时不可用,请稍后重试。' citationLoading.value = false @@ -443,8 +446,63 @@ function formatNow() { return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` } -onMounted(async () => { +async function loadConversations() { + try { + const res = await listChatConversations() + const items = res?.data?.items || res?.items || [] + if (Array.isArray(items) && items.length) { + const mapped = items.map((item) => ({ + id: item.conversationId || item.id || item.conversation_id || `session-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + conversationId: item.conversationId || item.id || item.conversation_id, + title: item.title || '历史会话', + lastContent: item.last_content || item.lastContent || '', + messageCount: item.message_count ?? item.messageCount ?? 0, + updatedAt: formatNow(), + messages: [], + fromServer: true, + })) + // 按 conversationId 去重合并 + const seen = new Set() + const merged = [] + for (const item of mapped) { + if (item.conversationId && seen.has(item.conversationId)) continue + if (item.conversationId) seen.add(item.conversationId) + merged.push(item) + } + sessions.value = merged + if (merged.length) { + activeSessionId.value = merged[0].id + return + } + } + } catch (error) { + // 后端异常时静默降级为新建空会话 + } createSession() +} + +async function loadSessionMessages(session) { + if (!session?.conversationId || loadedConversations.has(session.conversationId)) return + loadedConversations.add(session.conversationId) + try { + const res = await getChatConversationMessages(session.conversationId) + const messages = res?.data?.messages || res?.messages || [] + if (Array.isArray(messages)) { + session.messages = messages.map((m) => ({ + id: m.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + role: m.role === 'assistant' ? 'assistant' : 'user', + content: m.content || '', + meta: m.meta || {}, + })) + } + } catch (error) { + // 拉取失败静默降级为空消息 + session.messages = [] + } +} + +onMounted(async () => { + await loadConversations() aiRouteStore.ensureAiChatRoutes() try { const spaceRes = await listKnowledgeSpaces() @@ -478,6 +536,19 @@ onMounted(async () => { scrollChatToBottom(false) }) +// 切换会话时,若该会话来自后端且尚未加载本地消息,则拉取历史消息填充 +watch( + () => activeSessionId.value, + async (id) => { + if (!id) return + const session = sessions.value.find((item) => item.id === id) + if (session && session.fromServer && !session.messages?.length) { + await loadSessionMessages(session) + scrollChatToBottom(false) + } + }, +) + watch( () => activeMessages.value.map((item) => item.content).join('\n'), () => { diff --git a/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue b/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue index 4419297..c48d7c1 100644 --- a/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue +++ b/eai_agentplatform/frontend/src/views/workbench/SmartAssistantPage.vue @@ -125,7 +125,11 @@ ↗ {{ resource.label }} -
{{ msg.content }}
+
{ const key = currentSkillKey.value if (!key || key === HOME_SKILL) return null @@ -437,11 +446,296 @@ function buildLocalSkillReply(result, definition) { } } -function isOfficialAccountSpecialistActive() { +function escapeHtml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function renderInlineMarkdown(text) { + let html = escapeHtml(text) + html = html.replace(/`([^`]+)`/g, '$1') + html = html.replace(/\*\*([^*]+)\*\*/g, '$1') + html = html.replace(/\*([^*]+)\*/g, '$1') + html = html.replace( + /(https?:\/\/[^\s<]+)/g, + '$1', + ) + return html +} + +function renderMarkdownBlock(block) { + const trimmed = String(block || '').trim() + if (!trimmed) return '' + + const headingMatch = trimmed.match(/^(#{1,3})\s+(.+)$/) + if (headingMatch) { + const level = headingMatch[1].length + return `${renderInlineMarkdown(headingMatch[2])}` + } + + const lines = trimmed.split('\n') + if (lines.every((line) => /^\s*>\s?/.test(line))) { + const quote = lines.map((line) => line.replace(/^\s*>\s?/, '')).join('
') + return `
${renderInlineMarkdown(quote)}
` + } + + if (lines.every((line) => /^\s*[-*]\s+/.test(line))) { + const items = lines + .map((line) => line.replace(/^\s*[-*]\s+/, '')) + .map((line) => `
  • ${renderInlineMarkdown(line)}
  • `) + .join('') + return `` + } + + if (lines.every((line) => /^\s*\d+\.\s+/.test(line))) { + const items = lines + .map((line) => line.replace(/^\s*\d+\.\s+/, '')) + .map((line) => `
  • ${renderInlineMarkdown(line)}
  • `) + .join('') + return `
      ${items}
    ` + } + + return `

    ${lines.map((line) => renderInlineMarkdown(line)).join('
    ')}

    ` +} + +function renderMarkdownToHtml(text) { + const raw = String(text || '').replace(/\r\n/g, '\n').trim() + if (!raw) return '' + + const codeBlocks = [] + const withPlaceholders = raw.replace(/```([\w-]*)\n?([\s\S]*?)```/g, (_, lang, code) => { + const token = `@@CODE_BLOCK_${codeBlocks.length}@@` + const languageClass = String(lang || '').trim() ? ` class="language-${escapeHtml(lang.trim())}"` : '' + codeBlocks.push(`
    ${escapeHtml(code.trim())}
    `) + return token + }) + + let html = withPlaceholders + .split(/\n{2,}/) + .map((block) => { + const normalized = String(block || '').trim() + if (!normalized) return '' + if (/^@@CODE_BLOCK_\d+@@$/.test(normalized)) return normalized + return renderMarkdownBlock(normalized) + }) + .filter(Boolean) + .join('') + + codeBlocks.forEach((blockHtml, index) => { + html = html.replace(`@@CODE_BLOCK_${index}@@`, blockHtml) + }) + return html +} + +function messageHtml(message) { + const content = String(message?.content || '') + if (!content.trim()) return '' + if (message?.role === 'assistant') return renderMarkdownToHtml(content) + return escapeHtml(content).replace(/\n/g, '
    ') +} + +function buildMessageStorageKey(taskId = currentTaskId.value, specialistKey = currentSpecialistKey.value) { + const taskKey = String(taskId || '').trim() + if (taskKey) return `${MESSAGE_STORAGE_PREFIX}task:${taskKey}` + return `${MESSAGE_STORAGE_PREFIX}draft:${String(specialistKey || GENERAL_ASSISTANT_KEY).trim() || GENERAL_ASSISTANT_KEY}` +} + +function serializeMessage(message) { + return { + role: message.role, + content: message.content, + timestamp: message.timestamp, + task_plan: message.task_plan || null, + contentBlocks: Array.isArray(message.contentBlocks) ? message.contentBlocks : [], + } +} + +function persistMessagesFor(taskId = currentTaskId.value) { + try { + window.localStorage.setItem( + buildMessageStorageKey(taskId), + JSON.stringify(messages.value.map(serializeMessage)), + ) + } catch (error) { + console.warn('persist messages failed', error) + } +} + +function restoreMessagesFor(taskId = currentTaskId.value) { + try { + const raw = window.localStorage.getItem(buildMessageStorageKey(taskId)) + if (!raw) { + messages.value = [] + return + } + const parsed = JSON.parse(raw) + messages.value = Array.isArray(parsed) ? parsed : [] + } catch (error) { + console.warn('restore messages failed', error) + messages.value = [] + } +} + +function reportWeixinPublicAccountDebug(hypothesisId, msg, data = {}) { + fetch(DEBUG_SERVER_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: DEBUG_SESSION_ID, + runId: 'pre-fix', + hypothesisId, + location: 'frontend/SmartAssistantPage.vue', + msg: `[DEBUG] ${msg}`, + data, + ts: Date.now(), + }), + }).catch(() => {}) +} + +function broadcastWeixinPublicAccountStepState(taskId, stepKey, status, progressText = '') { + window.dispatchEvent(new CustomEvent('weixin-public-account-step-state', { + detail: { + taskId: Number(taskId) || 0, + stepKey, + status, + progressText, + timestamp: new Date().toISOString(), + }, + })) +} + +function broadcastWeixinPublicAccountWorkflowSync(payload) { + if (!payload?.task?.id || !payload?.workflow) return + window.dispatchEvent(new CustomEvent('weixin-public-account-workflow-sync', { + detail: payload, + })) +} + +function isWeixinPublicAccountSpecialistActive() { return String(currentSpecialistKey.value || '').trim() === OFFICIAL_ACCOUNT_KEY } -function buildOfficialAccountSeedForm(text) { +function shouldRunWeixinPublicAccountWorkflow(text) { + const normalizedText = String(text || '').trim() + if (!normalizedText) return false + + const strongWorkflowPatterns = [ + /写一篇|写篇|来一篇|生成一篇|做一篇|输出一篇/u, + /公众号(文章|推文|内容|文案)/u, + /起(?:草|一个)?(?:选题|标题|提纲)/u, + /生成(?:选题|标题|提纲|正文|草稿)/u, + /继续|继续进行|接着|往下|下一步/u, + /润色(?:成)?(?:公众号|推文|文章)/u, + /推广.*(公众号|文章|推文|文案)/u, + ] + if (strongWorkflowPatterns.some((pattern) => pattern.test(normalizedText))) { + return true + } + + const lightweightChatPatterns = [ + /什么意思|怎么回事|为什么|为啥|是否|能不能|可以吗/u, + /继续聊|讨论一下|先别生成|先不要生成|暂停一下/u, + /你觉得|怎么看|帮我分析|帮我判断|给我建议/u, + /总结一下|解释一下|复盘一下|评价一下/u, + /改哪里|哪里不对|哪里有问题|为什么跑偏/u, + ] + if (lightweightChatPatterns.some((pattern) => pattern.test(normalizedText))) { + return false + } + + return false +} + +function isWeixinPublicAccountResumeCommand(text) { + const normalizedText = String(text || '').trim() + if (!normalizedText) return false + return /继续|继续进行|接着|往下|下一步|继续生成/u.test(normalizedText) +} + +function getWeixinPublicAccountShared(payload) { + return payload?.workflow?.shared || {} +} + +function getWeixinPublicAccountStepStatus(payload, stepKey) { + return String(payload?.workflow?.steps?.[stepKey]?.status || '').trim() +} + +function resolveWeixinPublicAccountNextCoreStep(payload) { + const shared = getWeixinPublicAccountShared(payload) + if (!shared.selected_topic || getWeixinPublicAccountStepStatus(payload, 'topic_recommendation') !== 'completed') { + return 'topic_recommendation' + } + if (!shared.selected_title || getWeixinPublicAccountStepStatus(payload, 'title_generation') !== 'completed') { + return 'title_generation' + } + if (!shared.outline || getWeixinPublicAccountStepStatus(payload, 'outline_generation') !== 'completed') { + return 'outline_generation' + } + if (!shared.content || getWeixinPublicAccountStepStatus(payload, 'content_creation') !== 'completed') { + return 'content_creation' + } + if (getWeixinPublicAccountStepStatus(payload, 'image_prompt_generation') !== 'completed') { + return 'image_prompt_generation' + } + if (getWeixinPublicAccountStepStatus(payload, 'image_generation') !== 'completed') { + return 'image_generation' + } + if (getWeixinPublicAccountStepStatus(payload, 'preview_export') !== 'completed') { + return 'preview_export' + } + if (getWeixinPublicAccountStepStatus(payload, 'page_markdown') !== 'completed') { + return 'page_markdown' + } + return '' +} + +function buildWeixinPublicAccountStepRequest(stepKey, payload, requirementsText) { + const shared = getWeixinPublicAccountShared(payload) + const request = {} + const requirements = String(requirementsText || '').trim() + if (requirements && !isWeixinPublicAccountResumeCommand(requirements)) { + request.requirements = requirements + } + if (stepKey === 'title_generation') { + request.topic = shared.selected_topic || shared.topic_candidates?.[0]?.topic || '' + } + if (stepKey === 'outline_generation') { + request.title = shared.selected_title || shared.title_candidates?.[0] || '' + } + if (stepKey === 'content_creation') { + request.outline = shared.outline || '' + } + if (stepKey === 'image_prompt_generation') { + request.content = shared.content || '' + } + if (stepKey === 'image_generation') { + request.content_with_prompts = shared.content_with_prompts || '' + } + return request +} + +function weixinPublicAccountStepProgressText(stepKey, payload) { + const shared = getWeixinPublicAccountShared(payload) + if (stepKey === 'topic_recommendation') return '正在生成选题...' + if (stepKey === 'title_generation') { + return shared.selected_topic ? `正在基于选题生成标题:${shared.selected_topic}` : '正在生成标题...' + } + if (stepKey === 'outline_generation') { + return shared.selected_title ? `正在生成提纲:${shared.selected_title}` : '正在生成提纲...' + } + if (stepKey === 'content_creation') return '正在生成正文草稿...' + if (stepKey === 'image_prompt_generation') return '正在提炼配图提示词...' + if (stepKey === 'image_generation') return '正在生成配图...' + if (stepKey === 'preview_export') return '正在生成预览稿...' + if (stepKey === 'page_markdown') return '正在生成分页 MD...' + return '公众号创作专员正在处理...' +} + +function buildWeixinPublicAccountSeedForm(text) { const normalizedText = String(text || '').trim() const cleanedKeyword = normalizedText .replace(/[“”"'`]/g, '') @@ -451,6 +745,7 @@ function buildOfficialAccountSeedForm(text) { .replace(/关于|有关|一篇/gu, ' ') .replace(/[,。!?、,.!?]/g, ' ') .replace(/\s+/g, ' ') + .replace(/的$/u, '') .trim() return { @@ -469,12 +764,16 @@ function buildOfficialAccountSeedForm(text) { } } -function buildOfficialAccountTaskPlan(workflow) { +function buildWeixinPublicAccountTaskPlan(workflow) { const steps = [ { key: 'topic_recommendation', order: 1, title: '选题', desc: '生成候选选题' }, { key: 'title_generation', order: 2, title: '标题', desc: '生成标题并确定方向' }, { key: 'outline_generation', order: 3, title: '提纲', desc: '生成文章结构提纲' }, { key: 'content_creation', order: 4, title: '正文', desc: '生成正文草稿' }, + { key: 'image_prompt_generation', order: 5, title: '配图提示词', desc: '提炼配图提示词' }, + { key: 'image_generation', order: 6, title: '图片生成', desc: '生成配图' }, + { key: 'preview_export', order: 7, title: '预览与导出', desc: '生成预览稿' }, + { key: 'page_markdown', order: 8, title: '分页 MD', desc: '生成分页 MD' }, ] return { title: '公众号创作专员流程', @@ -489,7 +788,203 @@ function buildOfficialAccountTaskPlan(workflow) { } } -function buildOfficialAccountReply(payload) { +function getWeixinPublicAccountStep(payload, stepKey) { + return payload?.workflow?.steps?.[stepKey] || {} +} + +function trimWeixinPublicAccountText(value, maxLength = 180) { + const text = String(value || '').trim() + if (!text) return '' + return text.length > maxLength ? `${text.slice(0, maxLength).trim()}...` : text +} + +function extractWeixinPublicAccountOutlineSections(outline) { + return String(outline || '') + .split('\n') + .map((line) => line.trim()) + .filter((line) => /^##\s+/.test(line)) + .map((line) => line.replace(/^##\s+/, '').trim()) + .filter(Boolean) + .slice(0, 4) +} + +function formatWeixinPublicAccountBulletList(items) { + return items.filter(Boolean).map((item) => `- ${item}`).join('\n') +} + +function buildWeixinPublicAccountTopicStageMessage(payload) { + const workflow = payload?.workflow || {} + const form = workflow.form || {} + const step = getWeixinPublicAccountStep(payload, 'topic_recommendation') + const output = step.output || {} + const selectedTopic = output.selected_topic || workflow.shared?.selected_topic || '' + const candidates = Array.isArray(output.candidates) ? output.candidates.slice(0, 3) : [] + const hotspots = Array.isArray(output.hotspots) ? output.hotspots : [] + const logs = Array.isArray(step.logs) ? step.logs : [] + const usedFallback = Boolean(output.fallback_used) + const cacheLog = logs.find((item) => String(item || '').includes('命中热点缓存')) + const fetchLog = cacheLog || logs.find((item) => String(item || '').includes('热点')) + const candidateBullets = candidates.map((item, index) => { + const heat = item?.heat ? `(${item.heat})` : '' + return `${index + 1}. ${item?.topic || ''}${heat}` + }) + const substeps = [ + `1.1 主题守门:关键词「${form.keyword || '未设置'}」当前按「${form.business_domain || '未识别'}」业务域处理。`, + `1.2 热点检索:${usedFallback ? '当前未拿到强相关热点,已退回通用候选。' : `本轮拿到 ${hotspots.length} 条候选热点。`}${fetchLog ? ` ${trimWeixinPublicAccountText(fetchLog, 70)}` : ''}`, + `1.3 传播策划:已生成 ${candidates.length} 个公众号选题候选。${candidateBullets.length ? `\n${formatWeixinPublicAccountBulletList(candidateBullets)}` : ''}`, + selectedTopic ? `1.4 合并评审:当前默认推进选题「${selectedTopic}」。` : '1.4 合并评审:当前还没有锁定选题。', + ] + return { + role: 'assistant', + content: [ + '### 1. 选题步骤已完成', + formatWeixinPublicAccountBulletList(substeps), + ].filter(Boolean).join('\n\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountTitleStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'title_generation') + const output = step.output || {} + const titles = Array.isArray(output.titles) ? output.titles.slice(0, 3) : [] + const selectedTitle = output.selected_title || workflow.shared?.selected_title || '' + return { + role: 'assistant', + content: [ + '### 2. 标题步骤已完成', + titles.length + ? formatWeixinPublicAccountBulletList( + titles.map((item, index) => `${index + 1}. ${item?.positioning || '候选'}:${item?.title || ''}`), + ) + : '- 本轮没有返回标题候选。', + selectedTitle ? `当前选用标题:**${selectedTitle}**` : '', + ].filter(Boolean).join('\n\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountOutlineStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'outline_generation') + const outline = step.output?.outline || workflow.shared?.outline || '' + const sections = extractWeixinPublicAccountOutlineSections(outline) + return { + role: 'assistant', + content: [ + '### 3. 提纲步骤已完成', + sections.length + ? `本轮提纲主结构:\n${formatWeixinPublicAccountBulletList(sections)}` + : trimWeixinPublicAccountText(outline, 220) || '本轮提纲已生成。', + ].filter(Boolean).join('\n\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountContentStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'content_creation') + const content = step.output?.content || workflow.shared?.content || '' + const preview = trimWeixinPublicAccountText( + String(content || '') + .replace(/^#\s+.*$/m, '') + .replace(/^>\s+.*$/gm, '') + .replace(/\n{2,}/g, '\n') + .trim(), + 220, + ) + return { + role: 'assistant', + content: [ + '### 4. 正文步骤已完成', + preview ? `正文摘要:${preview}` : '正文草稿已生成,并已同步到右栏产物区。', + ].filter(Boolean).join('\n\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountImagePromptStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'image_prompt_generation') + const output = step.output || {} + const count = Number(output.count || 0) + const prompts = Array.isArray(output.image_prompts) ? output.image_prompts.slice(0, 4) : [] + return { + role: 'assistant', + content: [ + '### 5. 配图提示词步骤已完成', + count > 0 ? `已提炼 ${count} 条配图提示词。` : '本轮未提炼出可用配图提示词。', + ...(prompts.length + ? prompts.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 80)}`) + : []), + ].filter(Boolean).join('\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountImageGenerationStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'image_generation') + const output = step.output || {} + const count = Number(output.count || 0) + const refs = Array.isArray(output.image_refs) ? output.image_refs.slice(0, 4) : [] + return { + role: 'assistant', + content: [ + '### 6. 图片生成步骤已完成', + count > 0 ? `已生成 ${count} 张配图。` : '本轮未生成图片结果。', + ...(refs.length + ? refs.map((item, index) => `${index + 1}. [${item?.section || '段落配图'}] ${trimWeixinPublicAccountText(item?.prompt || '', 60)}`) + : []), + ].filter(Boolean).join('\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountPreviewStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'preview_export') + const output = step.output || {} + const imageCount = Number(output.image_count || 0) + return { + role: 'assistant', + content: [ + '### 7. 预览与导出步骤已完成', + `已整合正文${imageCount ? `与 ${imageCount} 张配图` : ''}生成预览稿,可检查排版并导出。`, + ].filter(Boolean).join('\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountPageMarkdownStageMessage(payload) { + const workflow = payload?.workflow || {} + const step = getWeixinPublicAccountStep(payload, 'page_markdown') + const output = step.output || {} + const count = Number(output.count || 0) + return { + role: 'assistant', + content: [ + '### 8. 分页 MD 步骤已完成', + count > 0 ? `已按段落结构生成 ${count} 页分页 MD。` : '分页 MD 已生成。', + ].filter(Boolean).join('\n'), + timestamp: new Date().toLocaleTimeString(), + } +} + +function buildWeixinPublicAccountStepStageMessage(stepKey, payload) { + if (stepKey === 'topic_recommendation') return buildWeixinPublicAccountTopicStageMessage(payload) + if (stepKey === 'title_generation') return buildWeixinPublicAccountTitleStageMessage(payload) + if (stepKey === 'outline_generation') return buildWeixinPublicAccountOutlineStageMessage(payload) + if (stepKey === 'content_creation') return buildWeixinPublicAccountContentStageMessage(payload) + if (stepKey === 'image_prompt_generation') return buildWeixinPublicAccountImagePromptStageMessage(payload) + if (stepKey === 'image_generation') return buildWeixinPublicAccountImageGenerationStageMessage(payload) + if (stepKey === 'preview_export') return buildWeixinPublicAccountPreviewStageMessage(payload) + if (stepKey === 'page_markdown') return buildWeixinPublicAccountPageMarkdownStageMessage(payload) + return null +} + +function buildWeixinPublicAccountReply(payload) { const workflow = payload?.workflow || {} const shared = workflow.shared || {} const topic = shared.selected_topic || '' @@ -506,20 +1001,35 @@ function buildOfficialAccountReply(payload) { return { role: 'assistant', content: summaryLines.join('\n'), - task_plan: buildOfficialAccountTaskPlan(workflow), + task_plan: buildWeixinPublicAccountTaskPlan(workflow), timestamp: new Date().toLocaleTimeString(), } } -async function runOfficialAccountConversation(text, onProgress = () => {}) { - const form = buildOfficialAccountSeedForm(text) +async function runWeixinPublicAccountConversation(text, onProgress = () => {}, onStepResult = () => {}) { + const form = buildWeixinPublicAccountSeedForm(text) onProgress('公众号创作专员已接单,正在准备创作任务...') + // #region debug-point A:start-run + reportWeixinPublicAccountDebug('A', 'runWeixinPublicAccountConversation start', { + text, + currentTaskId: currentTaskId.value, + currentSpecialistKey: currentSpecialistKey.value, + keyword: form.keyword, + }) + // #endregion let taskId = Number(currentTaskId.value) || 0 if (!taskId) { - const createRes = await createOfficialAccountTask(form) + const createRes = await createWeixinPublicAccountTask(form) const data = createRes?.data || null const task = data?.task || null + // #region debug-point A:create-task + reportWeixinPublicAccountDebug('A', 'createWeixinPublicAccountTask finished', { + taskId: task?.id || 0, + specialistKey: task?.specialist_key || '', + summary: data?.task?.summary || '', + }) + // #endregion if (!task?.id) throw new Error('公众号创作任务创建失败') taskId = Number(task.id) await taskRuntime.loadMyTasks() @@ -527,62 +1037,175 @@ async function runOfficialAccountConversation(text, onProgress = () => {}) { await taskRuntime.loadTaskDetail(taskId) } else { onProgress('正在更新当前创作任务配置...') - await updateOfficialAccountTask(taskId, form) + await updateWeixinPublicAccountTask(taskId, form) + // #region debug-point A:update-task + reportWeixinPublicAccountDebug('A', 'updateWeixinPublicAccountTask finished', { + taskId, + keyword: form.keyword, + }) + // #endregion await taskRuntime.loadMyTasks() await taskRuntime.loadTaskDetail(taskId) } - let workflowRes = await getOfficialAccountWorkflow(taskId) + let workflowRes = await getWeixinPublicAccountWorkflow(taskId) let workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + // #region debug-point C:workflow-after-get + reportWeixinPublicAccountDebug('C', 'initial workflow loaded', { + taskId, + selectedTopic: workflow?.workflow?.shared?.selected_topic || '', + selectedTitle: workflow?.workflow?.shared?.selected_title || '', + currentStep: workflow?.workflow?.current_step || '', + }) + // #endregion onProgress('正在生成选题...') - await executeOfficialAccountWorkflowStep(taskId, 'topic_recommendation', { + broadcastWeixinPublicAccountStepState(taskId, 'topic_recommendation', 'running', '正在生成选题...') + await executeWeixinPublicAccountWorkflowStep(taskId, 'topic_recommendation', { requirements: form.requirements, }) + // #region debug-point A:topic-step + reportWeixinPublicAccountDebug('A', 'topic step finished', { + taskId, + requirements: form.requirements, + }) + // #endregion await taskRuntime.loadMyTasks() await taskRuntime.loadTaskDetail(taskId) - workflowRes = await getOfficialAccountWorkflow(taskId) + workflowRes = await getWeixinPublicAccountWorkflow(taskId) workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage('topic_recommendation', workflow)) const selectedTopic = workflow?.workflow?.shared?.selected_topic || workflow?.workflow?.shared?.topic_candidates?.[0]?.topic || '' onProgress(selectedTopic ? `正在基于选题生成标题:${selectedTopic}` : '正在生成标题...') - await executeOfficialAccountWorkflowStep(taskId, 'title_generation', { + broadcastWeixinPublicAccountStepState(taskId, 'title_generation', 'running', selectedTopic ? `正在基于选题生成标题:${selectedTopic}` : '正在生成标题...') + await executeWeixinPublicAccountWorkflowStep(taskId, 'title_generation', { topic: selectedTopic, requirements: form.requirements, }) + // #region debug-point A:title-step + reportWeixinPublicAccountDebug('A', 'title step finished', { + taskId, + selectedTopic, + }) + // #endregion await taskRuntime.loadMyTasks() await taskRuntime.loadTaskDetail(taskId) - workflowRes = await getOfficialAccountWorkflow(taskId) + workflowRes = await getWeixinPublicAccountWorkflow(taskId) workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage('title_generation', workflow)) const selectedTitle = workflow?.workflow?.shared?.selected_title || workflow?.workflow?.shared?.title_candidates?.[0] || workflow?.workflow?.steps?.title_generation?.output?.titles?.[0]?.title || '' onProgress(selectedTitle ? `正在生成提纲:${selectedTitle}` : '正在生成提纲...') - await executeOfficialAccountWorkflowStep(taskId, 'outline_generation', { + broadcastWeixinPublicAccountStepState(taskId, 'outline_generation', 'running', selectedTitle ? `正在生成提纲:${selectedTitle}` : '正在生成提纲...') + await executeWeixinPublicAccountWorkflowStep(taskId, 'outline_generation', { title: selectedTitle, requirements: form.requirements, }) + // #region debug-point A:outline-step + reportWeixinPublicAccountDebug('A', 'outline step finished', { + taskId, + selectedTitle, + }) + // #endregion await taskRuntime.loadMyTasks() await taskRuntime.loadTaskDetail(taskId) - workflowRes = await getOfficialAccountWorkflow(taskId) + workflowRes = await getWeixinPublicAccountWorkflow(taskId) workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage('outline_generation', workflow)) const outline = workflow?.workflow?.shared?.outline || '' onProgress('正在生成正文草稿...') - await executeOfficialAccountWorkflowStep(taskId, 'content_creation', { + broadcastWeixinPublicAccountStepState(taskId, 'content_creation', 'running', '正在生成正文草稿...') + await executeWeixinPublicAccountWorkflowStep(taskId, 'content_creation', { outline, requirements: form.requirements, }) - workflowRes = await getOfficialAccountWorkflow(taskId) + // #region debug-point A:content-step + reportWeixinPublicAccountDebug('A', 'content step finished', { + taskId, + outlineLength: outline.length, + }) + // #endregion + workflowRes = await getWeixinPublicAccountWorkflow(taskId) workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage('content_creation', workflow)) + // 正文不是结尾:继续推进配图提示词 → 图片生成 → 预览导出 → 分页 MD + let nextStep = resolveWeixinPublicAccountNextCoreStep(workflow) + while (nextStep) { + onProgress(weixinPublicAccountStepProgressText(nextStep, workflow)) + broadcastWeixinPublicAccountStepState(taskId, nextStep, 'running', weixinPublicAccountStepProgressText(nextStep, workflow)) + await executeWeixinPublicAccountWorkflowStep( + taskId, + nextStep, + buildWeixinPublicAccountStepRequest(nextStep, workflow, form.requirements), + ) + await taskRuntime.loadMyTasks() + await taskRuntime.loadTaskDetail(taskId) + workflowRes = await getWeixinPublicAccountWorkflow(taskId) + workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage(nextStep, workflow)) + nextStep = resolveWeixinPublicAccountNextCoreStep(workflow) + } await taskRuntime.loadMyTasks() await taskRuntime.loadTaskDetail(taskId) - return buildOfficialAccountReply(workflow) + // #region debug-point C:reply-ready + reportWeixinPublicAccountDebug('C', 'weixin public account reply ready', { + taskId, + contentLength: workflow?.workflow?.shared?.content?.length || 0, + selectedTitle: workflow?.workflow?.shared?.selected_title || '', + currentStep: workflow?.workflow?.current_step || '', + }) + // #endregion + return buildWeixinPublicAccountReply(workflow) +} + +async function continueWeixinPublicAccountConversation(text, onProgress = () => {}, onStepResult = () => {}) { + const taskId = Number(currentTaskId.value) || 0 + if (!taskId) { + return runWeixinPublicAccountConversation(text, onProgress) + } + + let workflowRes = await getWeixinPublicAccountWorkflow(taskId) + let workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + let nextStep = resolveWeixinPublicAccountNextCoreStep(workflow) + + if (!nextStep) { + return buildWeixinPublicAccountReply(workflow) + } + + while (nextStep) { + const progressText = weixinPublicAccountStepProgressText(nextStep, workflow) + onProgress(progressText) + broadcastWeixinPublicAccountStepState(taskId, nextStep, 'running', progressText) + await executeWeixinPublicAccountWorkflowStep( + taskId, + nextStep, + buildWeixinPublicAccountStepRequest(nextStep, workflow, text), + ) + await taskRuntime.loadMyTasks() + await taskRuntime.loadTaskDetail(taskId) + workflowRes = await getWeixinPublicAccountWorkflow(taskId) + workflow = workflowRes?.data || null + broadcastWeixinPublicAccountWorkflowSync(workflow) + onStepResult(buildWeixinPublicAccountStepStageMessage(nextStep, workflow)) + nextStep = resolveWeixinPublicAccountNextCoreStep(workflow) + } + + return buildWeixinPublicAccountReply(workflow) } async function send() { @@ -590,7 +1213,14 @@ async function send() { const text = inputText.value - if (isOfficialAccountSpecialistActive()) { + if (isWeixinPublicAccountSpecialistActive() && shouldRunWeixinPublicAccountWorkflow(text)) { + // #region debug-point B:branch-hit + reportWeixinPublicAccountDebug('B', 'send entered weixin-public-account branch', { + currentTaskId: currentTaskId.value, + currentSpecialistKey: currentSpecialistKey.value, + text, + }) + // #endregion messages.value.push({ role: 'user', content: text, @@ -604,27 +1234,106 @@ async function send() { content: '公众号创作专员已接单,正在处理...', timestamp: new Date().toLocaleTimeString(), } + let progressBaseText = progressMessage.content + const progressStartedAt = Date.now() + const syncProgressMessage = () => { + const elapsed = Math.max(0, Math.floor((Date.now() - progressStartedAt) / 1000)) + progressMessage.content = elapsed >= 3 + ? `${progressBaseText}(已等待 ${elapsed} 秒)` + : progressBaseText + progressMessage.timestamp = new Date().toLocaleTimeString() + } + const progressTicker = window.setInterval(() => { + syncProgressMessage() + }, 1000) messages.value.push(progressMessage) + // #region debug-point D:progress-message-created + reportWeixinPublicAccountDebug('D', 'progress message pushed', { + messageCount: messages.value.length, + content: progressMessage.content, + }) + // #endregion + await nextTick() + scrollToEnd() try { - const reply = await runOfficialAccountConversation(text, (progressText) => { - progressMessage.content = progressText - progressMessage.timestamp = new Date().toLocaleTimeString() + const shouldResumeWorkflow = Boolean(currentTaskId.value) && isWeixinPublicAccountResumeCommand(text) + const handleWeixinPublicAccountStepResult = (message) => { + if (!message?.content) return + messages.value.push(message) + nextTick(() => { + scrollToEnd() + }) + } + const reply = await ( + shouldResumeWorkflow + ? continueWeixinPublicAccountConversation(text, (progressText) => { + progressBaseText = progressText + syncProgressMessage() + // #region debug-point D:progress-message-updated + reportWeixinPublicAccountDebug('D', 'progress message updated', { + content: progressText, + }) + // #endregion + nextTick(() => { + scrollToEnd() + }) + }, handleWeixinPublicAccountStepResult) + : runWeixinPublicAccountConversation(text, (progressText) => { + progressBaseText = progressText + syncProgressMessage() + // #region debug-point D:progress-message-updated + reportWeixinPublicAccountDebug('D', 'progress message updated', { + content: progressText, + }) + // #endregion + nextTick(() => { + scrollToEnd() + }) + }, handleWeixinPublicAccountStepResult) + ) + progressMessage.content = '公众号创作专员已完成本轮生成,结果已写入对话与右栏。' + progressMessage.timestamp = new Date().toLocaleTimeString() + messages.value.push(reply) + window.clearInterval(progressTicker) + // #region debug-point D:progress-message-finalized + reportWeixinPublicAccountDebug('D', 'progress message finalized', { + contentLength: reply.content.length, + hasTaskPlan: Boolean(reply.task_plan), }) - progressMessage.content = reply.content - progressMessage.task_plan = reply.task_plan - progressMessage.timestamp = reply.timestamp + // #endregion + // #region debug-point D:final-reply-pushed + reportWeixinPublicAccountDebug('D', 'weixin public account final reply pushed as standalone message', { + messageCount: messages.value.length, + }) + // #endregion await nextTick() scrollToEnd() } catch (error) { + window.clearInterval(progressTicker) progressMessage.content = `公众号创作专员执行失败:${error?.message || '请稍后重试'}` progressMessage.timestamp = new Date().toLocaleTimeString() + // #region debug-point A:branch-error + reportWeixinPublicAccountDebug('A', 'weixin public account branch error', { + message: error?.message || '', + stack: error?.stack || '', + }) + // #endregion ElMessage.error(error?.message || '公众号创作专员执行失败') } finally { + window.clearInterval(progressTicker) sending.value = false } return } + if (isWeixinPublicAccountSpecialistActive()) { + reportWeixinPublicAccountDebug('B', 'send stayed in weixin-public-account chat branch', { + currentTaskId: currentTaskId.value, + currentSpecialistKey: currentSpecialistKey.value, + text, + }) + } + // 这条画布上还没有任务(刚点过「新建任务」,或压根没点开过任务)—— // 任务在这时候才落库,标题就是这句话,专员用 + 里先选的那个。 // 建失败就把话原样留在输入框里返回:此刻一个字都还没发出去, @@ -761,9 +1470,11 @@ function starterPromptClass(index) { // 新建任务 = 把画布清回开场状态:对话清空、草稿丢掉、输入框回到标题下面 // 并直接把光标放进去,点完就能打字。 function handleNewChat() { + window.localStorage.removeItem(ACTIVE_TASK_STORAGE_KEY) messages.value = [] inputText.value = '' composerUploads.value = [] + taskRuntime.startNewTask() nextTick(() => inputBarRef.value?.focus()) } @@ -915,12 +1626,57 @@ watch( }, ) +watch( + currentTaskId, + async (nextId, prevId) => { + if (!conversationBootstrapped.value) return + const nextTaskId = String(nextId || '').trim() + const prevTaskId = String(prevId || '').trim() + if (!nextTaskId) { + window.localStorage.removeItem(ACTIVE_TASK_STORAGE_KEY) + restoreMessagesFor('') + return + } + window.localStorage.setItem(ACTIVE_TASK_STORAGE_KEY, nextTaskId) + if (!prevTaskId && messages.value.length) { + persistMessagesFor(nextTaskId) + } + restoreMessagesFor(nextTaskId) + await nextTick() + scrollToEnd() + }, +) + +watch( + messages, + () => { + if (!conversationBootstrapped.value) return + persistMessagesFor() + }, + { deep: true }, +) + onMounted(async () => { await Promise.all([ skillCatalog.hydrate(), specialistCatalog.hydrate(), ]) + await taskRuntime.loadMyTasks() + const routeHasPreset = Boolean( + route.query[XAPP_ENTRY_QUERY_KEYS.specialist] + || route.query[XAPP_ENTRY_QUERY_KEYS.skill] + || route.query[XAPP_ENTRY_QUERY_KEYS.prompt] + || workbenchStore.pendingComposerText, + ) + if (!routeHasPreset) { + // 首页是门厅,不自动恢复上次那条任务,避免登录后直接掉进某个专员上下文里。 + // 真正要继续旧任务时,由用户从左侧任务列表主动点开。 + window.localStorage.removeItem(ACTIVE_TASK_STORAGE_KEY) + taskRuntime.startNewTask() + } await applyRouteXAppPreset() + conversationBootstrapped.value = true + restoreMessagesFor(currentTaskId.value) aiRouteStore.ensureAiChatRoutes() }) @@ -1232,19 +1988,116 @@ onMounted(async () => { } .chat-message .msg-content { - background: #f5f5f5; - padding: 10px 16px; - border-radius: 12px; + background: #ffffff; + border: 1px solid #e6ebf2; + box-shadow: 0 4px 18px rgba(15, 23, 42, 0.04); + padding: 12px 16px; + border-radius: 14px; font-size: 14px; line-height: 1.7; word-break: break-word; + color: #303133; } .chat-message.user .msg-content { background: #2b63d9; + border-color: transparent; + box-shadow: none; color: #fff; } +.chat-message .msg-content.is-rich { + line-height: 1.8; +} + +.chat-message .msg-content.is-rich :deep(h1), +.chat-message .msg-content.is-rich :deep(h2), +.chat-message .msg-content.is-rich :deep(h3) { + margin: 0 0 10px; + font-weight: 700; + line-height: 1.5; + color: #1f2937; +} + +.chat-message .msg-content.is-rich :deep(h1) { + font-size: 20px; +} + +.chat-message .msg-content.is-rich :deep(h2) { + font-size: 17px; +} + +.chat-message .msg-content.is-rich :deep(h3) { + font-size: 15px; +} + +.chat-message .msg-content.is-rich :deep(p) { + margin: 0 0 10px; +} + +.chat-message .msg-content.is-rich :deep(p:last-child) { + margin-bottom: 0; +} + +.chat-message .msg-content.is-rich :deep(ul), +.chat-message .msg-content.is-rich :deep(ol) { + margin: 0 0 10px; + padding-left: 20px; +} + +.chat-message .msg-content.is-rich :deep(li) { + margin-bottom: 6px; +} + +.chat-message .msg-content.is-rich :deep(blockquote) { + margin: 0 0 10px; + padding: 8px 12px; + border-left: 3px solid #9ec5ff; + background: #f5f9ff; + color: #475467; + border-radius: 8px; +} + +.chat-message .msg-content.is-rich :deep(pre) { + margin: 0 0 10px; + padding: 12px; + overflow: auto; + border-radius: 10px; + background: #0f172a; + color: #e5eefc; + font-size: 12px; + line-height: 1.6; +} + +.chat-message .msg-content.is-rich :deep(code) { + padding: 2px 6px; + border-radius: 6px; + background: #f3f4f6; + color: #c2410c; + font-size: 12px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; +} + +.chat-message .msg-content.is-rich :deep(pre code) { + padding: 0; + background: transparent; + color: inherit; +} + +.chat-message .msg-content.is-rich :deep(a) { + color: #2563eb; + text-decoration: none; +} + +.chat-message .msg-content.is-rich :deep(a:hover) { + text-decoration: underline; +} + +.chat-message .msg-content.is-rich :deep(strong) { + color: #111827; + font-weight: 700; +} + .msg-skill-result { margin-top: 10px; }