diff --git a/TOP_CODING_RULES.md b/TOP_CODING_RULES.md
index a3ad349..6fbe4f3 100644
--- a/TOP_CODING_RULES.md
+++ b/TOP_CODING_RULES.md
@@ -175,6 +175,7 @@
- 文件扩展名白名单校验(仅允许 ppt/pptx/pdf/doc/docx/mp4/png/jpg/jpeg)
- 文件名重命名为存储 UUID,杜绝路径穿越
- 上传目录对静态预览只读,禁止直接执行
+5. **开发阶段路由调整直接收口到新结构**:在开发阶段,信息架构或导航结构调整时,默认**不需要保留老路由**。除非用户明确要求兼容、灰度、外链保持可用,否则应直接删除旧路由与旧入口,避免同时维护新旧两套路由造成漂移与误判。
---
@@ -565,4 +566,4 @@
---
-*注:本准则放置于项目根目录,作为全局 Rule 永久锁定。*
\ No newline at end of file
+*注:本准则放置于项目根目录,作为全局 Rule 永久锁定。*
diff --git a/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY17_工作台界面线框稿.md b/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY17_工作台界面线框稿.md
index 3b74492..76ac621 100644
--- a/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY17_工作台界面线框稿.md
+++ b/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY17_工作台界面线框稿.md
@@ -164,7 +164,7 @@
| [我的待办] [我的业务流] [最近交付物] [高风险事项] |
+--------------------------------------------------------------------------------------------------+
| +-------------------------+ +-------------------------+ +-------------------------+ |
-| | 公众号专员 | | 售前方案专员 | | 合同审查专员 | |
+| | 公众号创作专员 | | 售前方案专员 | | 合同审查专员 | |
| | 通用专员 / 内容运营 | | 行业专员 / 售前 | | 行业专员 / 法务 | |
| | 3 个待办 / 1 篇待发布 | | 2 个待审批 / 1 个高风险 | | 5 个进行中 | |
| | [进入应用] | | [进入应用] | | [进入应用] | |
diff --git a/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY23_专员规则文件与技能绑定方案.md b/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY23_专员规则文件与技能绑定方案.md
index 3e2e697..ffdc35c 100644
--- a/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY23_专员规则文件与技能绑定方案.md
+++ b/docs/01_System_Overall/History_And_Retrospectives/系统演进旧稿/SY23_专员规则文件与技能绑定方案.md
@@ -299,7 +299,7 @@ var ValidSkillKeys = map[string]bool{
| `openclaw-setup`、`aionui-assistant`、`aionui-*` 技能 | AionUi 自身的安装运维助手,与业务无关 |
| `moltbook` | 面向其自有平台的发布助手 |
| `game-3d`、`story-roleplay`、`human-3-coach`、`ui-ux-pro-max` | 游戏 / 角色扮演 / 个人成长 / 设计工具,与企业办公无关 |
-| `weixin-file-send` | 是「把文件发到微信」的客户端脚本,依赖其本地 Electron 环境,不可移植(公众号专员要的是发文,不是发文件) |
+| `weixin-file-send` | 是「把文件发到微信」的客户端脚本,依赖其本地 Electron 环境,不可移植(公众号创作专员要的是发文,不是发文件) |
### 前置依赖:OfficeCLI 未下载
diff --git a/docs/01_System_Overall/SY25_业务逻辑拆解与数字员工抽取方法.md b/docs/01_System_Overall/SY25_业务逻辑拆解与数字员工抽取方法.md
index e830ff5..4fde3f4 100644
--- a/docs/01_System_Overall/SY25_业务逻辑拆解与数字员工抽取方法.md
+++ b/docs/01_System_Overall/SY25_业务逻辑拆解与数字员工抽取方法.md
@@ -263,7 +263,7 @@ skill 是能力片段,不是岗位。
错误示例:
- `客户跟进专员`
-- `招聘筛选专员`
+- `简历筛选与招聘专员`
问题:
diff --git a/docs/02_Architecture/AR05_工作台架构约定.md b/docs/02_Architecture/AR05_工作台架构约定.md
index 949bbe7..fe58a64 100644
--- a/docs/02_Architecture/AR05_工作台架构约定.md
+++ b/docs/02_Architecture/AR05_工作台架构约定.md
@@ -3,7 +3,7 @@
> 落盘日期:2026-09-16
> 状态:架构合同
> 作用:作为后续前端重构的强约束,先定模型,再做代码迁移
-> 关联文档:`AR08_角色交互设计.md`
+> 关联文档:`AR08_角色交互设计.md`、`AR12_对话驱动与结构化交互架构.md`
---
@@ -275,6 +275,33 @@ type TaskRuntimeContext = {
3. 工具焦点时,展示工具级执行流与产物
4. 右栏不允许只支持专员、不支持工具
+### 6.6 关键分叉节点交互
+
+Workbench 采用混合交互架构:
+
+- 对话负责发起任务与局部修订
+- 结构化 UI 负责关键分叉节点接管
+- 右栏负责持续展示阶段、结果与回退点
+
+因此,以下节点不允许只靠消息流向下滚动:
+
+1. 选题
+2. 标题
+3. 路由
+4. Provider
+5. 发布
+6. 删除
+7. 覆盖已有结果
+8. 切换当前主责对象
+
+这些节点一律视为关键分叉节点,必须满足:
+
+1. 显式列出候选项
+2. 提供一键确认入口
+3. 允许用户手工改写,而不是只能选现有项
+4. 标明当前结果是“用户确认”还是“系统默认”
+5. 未确认前,不得静默继续执行下游步骤
+
---
## 7. 对象生命周期
diff --git a/docs/02_Architecture/AR12_对话驱动与结构化交互架构.md b/docs/02_Architecture/AR12_对话驱动与结构化交互架构.md
new file mode 100644
index 0000000..35ceae8
--- /dev/null
+++ b/docs/02_Architecture/AR12_对话驱动与结构化交互架构.md
@@ -0,0 +1,259 @@
+# AR12 — 对话驱动与结构化交互架构
+
+> 状态:架构讨论点 + 后续 Workbench 交互约束来源
+> 最后更新:2026-09-23
+>
+> 关联文档:
+> - `AR02_前端架构.md`
+> - `AR05_工作台架构约定.md`
+> - `docs/01_System_Overall/SY22_角色技能应用统一任务架构.md`
+
+---
+
+## 1. 背景
+
+EAI 平台当前正在从传统页面式软件,转向“主对话入口 + 任务上下文 + 对象挂载”的新工作台形态。
+
+这个转向带来一个新的核心问题:
+
+**用户应该通过自然语言持续推进任务,还是在关键节点切换到结构化 UI 做选择。**
+
+这个问题不是前端组件样式问题,而是下一代 AI 驱动软件的主交互范式问题。
+
+如果这条架构判断不明确,后续专员、技能、xApp、右栏工作流、操作按钮都会出现各自实现、口径分裂的问题。
+
+---
+
+## 2. 问题定义
+
+对话驱动式工作方式有明确优势:
+
+1. 用户可以一句话启动任务
+2. 用户不必先理解系统表单结构
+3. 用户可以用自然语言表达模糊目标
+4. AI 可以在早期帮助补全需求
+
+对话驱动式工作方式也有明确缺点:
+
+1. 信息天然按时间线滚动,分叉选择容易被淹没
+2. 用户很难判断系统是在“建议”,还是已经“替用户决定”
+3. 关键状态与可选项不够显式,可控性下降
+4. 过程虽然看起来流畅,但审计性、回退性、对比性不足
+
+传统 UI 的优劣恰好相反:
+
+1. 它强在并列展示、状态显式、可比较、可回退
+2. 它弱在启动成本高、表单感强、难以承接模糊需求
+
+因此,EAI 不应在“纯对话”与“纯页面表单”之间二选一。
+
+---
+
+## 3. 架构结论
+
+EAI 的正式交互结论是:
+
+**系统采用“对话发起 + 结构化决策接管 + 对话继续推进”的混合交互架构。**
+
+换句话说:
+
+1. **对话**负责表达目标、补充上下文、提出修改意见
+2. **结构化 UI**负责承接关键分叉、显式展示备选项、确认后果
+3. **工作流状态面板**负责持续展示当前阶段、可回退点、执行结果
+
+这不是“聊天框旁边加几个按钮”。
+
+这是把 AI 软件拆成三层协作:
+
+1. **对话层**:负责意图生成
+2. **决策层**:负责关键节点接管
+3. **执行层**:负责自动推进与结果回写
+
+---
+
+## 4. 交互分层原则
+
+### 4.1 对话适用的阶段
+
+以下阶段优先使用对话:
+
+1. 用户第一次描述任务目标
+2. 用户补充背景、约束、材料
+3. 用户对已生成结果提出局部修改意见
+4. 用户要求 AI 解释建议原因
+5. 用户在开放空间中重新定义问题
+
+典型例子:
+
+- “围绕这个热点写一篇面向医院管理层的文章”
+- “第三段太硬了,改得更像公众号”
+- “把语气改得更专业,但不要太官腔”
+
+### 4.2 结构化 UI 适用的阶段
+
+以下阶段必须由结构化 UI 接管:
+
+1. 存在有限个备选项,且用户必须做一次显式决定
+2. 每个选项存在不同后果、成本或执行路径
+3. 系统需要记录这次选择是“用户确认”还是“系统默认”
+4. 后续步骤会基于该选择继续自动推进
+
+典型例子:
+
+- 选题候选
+- 标题候选
+- 路由选择
+- Provider 选择
+- 执行方式选择
+- 是否覆盖已有结果
+- 是否发布 / 删除 / 切换对象
+
+---
+
+## 5. 关键分叉点规则
+
+Workbench 中凡是满足“改变后续执行路径”的节点,都属于关键分叉点。
+
+关键分叉点必须满足以下规则:
+
+1. 系统必须显式列出候选项
+2. 每个候选项必须展示最小必要解释
+3. 用户必须能一键确认,而不是被迫输入“选第二个”
+4. 系统必须记录当前结果是:
+ - 用户显式选择
+ - 系统自动默认
+ - 用户手工输入新方案
+5. 用户必须能看到“继续生成”和“返回修改”的边界
+
+这条规则直接适用于专员工作流。
+
+例如公众号创作专员在“选题”和“标题”阶段,不能只在消息流里丢出一大段文本然后自动往下跑。
+
+它必须提供:
+
+1. 候选卡片
+2. 选择按钮
+3. 重生成动作
+4. 手工改写入口
+5. 当前默认项标识
+
+---
+
+## 6. 问答式与按钮式的正式判断
+
+正式判断不是“问答式更先进”或“按钮式更稳定”。
+
+正式判断是:
+
+**开放问题用问答式,有限决策用按钮式或卡片式。**
+
+进一步细化如下:
+
+### 6.1 优先使用问答式的情况
+
+1. 目标尚未清晰
+2. 用户需要补充背景
+3. 用户想用自然语言直接改结果
+4. 系统无法预先枚举所有合理选项
+
+### 6.2 优先使用按钮 / 卡片式的情况
+
+1. 系统已经生成有限候选集
+2. 用户需要从候选集中选一个继续
+3. 系统需要明确记录选中的那一项
+4. 不同选项会触发不同下游动作
+
+### 6.3 必须同时提供两者的情况
+
+当系统给出候选项时,必须同时允许:
+
+1. 直接点选现有候选
+2. 手工输入“都不要,我自己改一个”
+
+因此,最佳形态不是单选按钮,也不是纯聊天。
+
+最佳形态是:
+
+**候选卡片 + 一键选择 + 手工改写入口。**
+
+---
+
+## 7. 对 Workbench 的直接影响
+
+这条架构结论会直接改写 Workbench 的交互职责。
+
+### 7.1 主对话区
+
+主对话区不再只负责消息流。
+
+它还必须承担:
+
+1. 意图发起
+2. 阶段性建议解释
+3. 结构化候选项承接
+4. 人工修改后的继续推进
+
+### 7.2 右栏工作流
+
+右栏工作流不只是“执行日志”。
+
+它还必须承担:
+
+1. 当前阶段展示
+2. 待用户选择节点提示
+3. 当前默认项显示
+4. 历史选择与回退入口
+
+### 7.3 对象工作流定义
+
+后续每个专员 / 技能 / xApp 的工作流定义,不仅要写“执行步骤”,还要写清楚:
+
+1. 哪些步骤是开放输入
+2. 哪些步骤是候选选择
+3. 哪些步骤允许自动默认
+4. 哪些步骤禁止 AI 静默跨过
+
+---
+
+## 8. 系统禁止事项
+
+以下做法禁止作为正式交互方案:
+
+1. 系统给出多个候选项,但没有显式选择入口
+2. 系统明明处在关键分叉点,却继续自动执行下游步骤
+3. 用户只能靠回复“第 2 个”“用第三个”完成选择
+4. 工作流已经进入“待确认”状态,但界面没有任何结构化承接
+5. 系统默认选择了某项,但没有标注“这是默认,不是用户确认”
+
+这些行为会让系统看起来更像一个会说话的黑箱,而不是可控的智能工作台。
+
+---
+
+## 9. 当前产品阶段的指导结论
+
+EAI 当前阶段的正式判断如下:
+
+1. 一句话启动任务,继续保留
+2. 关键分叉显式选择,必须补齐
+3. 执行过程持续可见,必须通过工作流面板承接
+4. 局部修改回到对话,继续保留
+5. AI 不得静默跨过高价值决策点
+
+其中“高价值决策点”至少包括:
+
+1. 选题
+2. 标题
+3. 路由
+4. Provider
+5. 发布
+6. 删除
+7. 覆盖已有结果
+8. 切换当前主责对象
+
+---
+
+## 10. 一句话结论
+
+**EAI 不是用聊天框取代 UI。EAI 是用对话降低启动门槛,用结构化交互接管关键决策,用工作流面板维持持续可控。**
+
+这条结论从现在开始,作为后续 Workbench、专员工作流、技能运行面、xApp 承接面的共同交互依据。
diff --git a/docs/02_Architecture/目录说明.md b/docs/02_Architecture/目录说明.md
index e40d98f..6fbaafc 100644
--- a/docs/02_Architecture/目录说明.md
+++ b/docs/02_Architecture/目录说明.md
@@ -9,6 +9,7 @@
> 当前产品导航与工作台口径,请以 `docs/01_System_Overall/SY22_角色技能应用统一任务架构.md` 为准:
> `新建任务 / 项目 / 专员·技能·APP·连接器 / 长程APP / 知识库 / 后台管理 / 我的`
> **`AR09` 是规范性文件(对象命名规范),不是设计稿。** 它约束新增代码的命名,并登记了当前已核实的命名问题与修复流程;与 `TOP_CODING_RULES.md` 的 G03 配套使用。
+> **`AR12` 是当前交互范式讨论的主文档。** 它定义“对话发起 + 结构化决策接管 + 对话继续推进”的混合交互架构,后续 Workbench 与专员工作流交互以此为依据。
> 当前正式对象目录请以 `docs/05_Object_Catalog/` 为准;本目录负责跨层架构契约,不再承担对象清单主仓职责。
## 文件清单
@@ -29,4 +30,5 @@
| `AR09_对象命名规范.md` | 对象命名规范(**规范性文件**:原理 / 判据 / 术语表 / 分层规范 / 检查守卫 / 修复流程 / 现状问题登记 / 反例库) |
| `AR10_应用可删除封装规范.md` | XApp 可删除封装规范(面向一级业务包,目标是“删目录 + 删注册 + 跑卸载”) |
| `AR11_技能专员连接器可删除封装规范.md` | Skill / Specialist / Connector 可删除封装规范(复用 AR10 思想,但区分能力包、策略定义包、连接插件包的边界) |
+| `AR12_对话驱动与结构化交互架构.md` | Workbench 交互范式主文档(对话发起 + 结构化决策接管 + 对话继续推进) |
| `Historical_Records/目录说明.md` | 架构迁移说明、历史清单、旧版 AR01–AR03 与阶段性记录索引 |
diff --git a/docs/05_Object_Catalog/specialists/目录.md b/docs/05_Object_Catalog/specialists/目录.md
index f847ed7..fe8d8e6 100644
--- a/docs/05_Object_Catalog/specialists/目录.md
+++ b/docs/05_Object_Catalog/specialists/目录.md
@@ -13,9 +13,9 @@
| `专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` | `wechat-official-account` | 公众号创作专员 | 选题、提纲、正文、发布准备 | active | 继续包化公众号工作流与模型 |
| `专05` | `EAI-S-05` | `customer-followup` | 客户跟进专员 | 客户资料整理、意向分层、联系推进、会议预约 | active | 作为新口径继续打磨;详见 [客户跟进专员.md](./客户跟进专员.md) |
-| `专06` | `EAI-S-06` | `resume-processor` | 招聘筛选专员 | 招聘入口归档、候选人筛选、面试推进 | active | 继续观察是否需要补独立招聘连接器 |
+| `专06` | `EAI-S-06` | `resume-processor` | 简历筛选与招聘专员 | 招聘入口归档、候选人筛选、面试推进 | active | 继续观察是否需要补独立招聘连接器 |
## 已退出当前目录但保留历史意义的对象
diff --git a/docs/06_Product_Lines/PL04_云趣能力追平路线图.md b/docs/06_Product_Lines/PL04_云趣能力追平路线图.md
index 2392224..7e6315f 100644
--- a/docs/06_Product_Lines/PL04_云趣能力追平路线图.md
+++ b/docs/06_Product_Lines/PL04_云趣能力追平路线图.md
@@ -103,9 +103,9 @@
- `知识运营专员`
- `流程推进专员`
- `报告生成专员`
-- `公众号专员`
+- `公众号创作专员`
- `客户跟进专员`
-- `招聘筛选专员`
+- `简历筛选与招聘专员`
### 4.2 已有执行层基础
diff --git a/docs/20_Global_Workbench_Evolution/GW01_全球主流软件工作台历史演进.md b/docs/20_Global_Workbench_Evolution/GW01_全球主流软件工作台历史演进.md
new file mode 100644
index 0000000..78286fd
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/GW01_全球主流软件工作台历史演进.md
@@ -0,0 +1,326 @@
+# GW01_全球主流软件工作台历史演进
+
+> 状态:研究主文
+> 最后更新:2026-09-23
+> 关联文档:
+> - `docs/02_Architecture/AR12_对话驱动与结构化交互架构.md`
+> - `GW02_对话式AI工作台与传统UI分界判断.md`
+
+---
+
+## 1. 背景
+
+“工作台”不是一个固定不变的 UI 样式,而是软件在不同时代对同一个问题的回答:
+
+**人如何向系统发起任务、查看状态、做关键选择、协调资源,并把结果沉淀为持续可复用的工作资产。**
+
+如果只把工作台理解成一个页面布局,就会误判今天的 AI 工作台。
+真正的历史主线不是“按钮变聊天”,而是:
+
+1. 从命令执行界面,走向图形化操作面
+2. 从孤立应用,走向集成式工作面
+3. 从个人文件处理,走向协同式共享空间
+4. 从信息显示与流程填写,走向角色化任务入口
+5. 从辅助建议,走向 AI 参与执行
+6. 从单代理对话,走向多代理编排与人工接管
+
+---
+
+## 2. 核心判断
+
+全球主流软件工作台形态的演进,并不是线性替换,而是不断叠层。
+
+旧层没有消失,新层只是接管了更高价值的控制面:
+
+1. CLI 没有死,它变成了专家层和自动化层
+2. GUI 没有死,它变成了大众软件的主操作面
+3. 表单和事务界面没有死,它们在 ERP、CRM、后台系统里仍是高密度输入面
+4. 协同工作台没有死,它变成了组织知识和任务的主枢纽
+5. 对话式 AI 没有吞掉 UI,它只是新增了一个极低门槛的意图输入层
+6. Agent Workbench 不是“聊天升级版”,而是新的协调层
+
+所以今天讨论 AI 工作台时,不能把问题简化成“要不要聊天框”。
+真正的问题是:
+
+**哪一层负责发起,哪一层负责决策,哪一层负责执行,哪一层负责审计。**
+
+---
+
+## 3. 历史分期
+
+### 3.1 第一阶段:批处理与命令行工作面
+
+这一阶段的核心不是“界面美观”,而是“机器终于允许人直接发出可执行指令”。
+
+早期交互从打孔卡、批处理到终端命令,解决的是“如何让机器接收任务”。
+CLI 带来的关键变化是:
+
+1. 首次形成实时往返的人机回路
+2. 用户开始直接操控系统,而不是通过操作员间接提交任务
+3. 任务执行以命令为中心,而不是以可见对象为中心
+
+这一阶段的工作台特点是:
+
+1. 强表达力
+2. 强组合性
+3. 低可见性
+4. 高学习门槛
+
+CLI 的历史地位不是“过时的输入法”,而是软件工作台第一次拥有“可编排执行”能力。
+今天 AI Agent 的工具调用、命令执行和脚本编排,本质上都继承了这条线路。
+
+### 3.2 第二阶段:GUI 与桌面隐喻
+
+GUI 的意义不是“更好看”,而是把机器操作从语法记忆,转移到视觉识别与对象操作。
+
+从 Sketchpad、Engelbart 的演示,到 Xerox PARC、桌面隐喻、窗口和鼠标,软件开始把“命令”翻译成“对象 + 动作”的关系。
+这一步把软件工作台从专家工具推进到大众工具。
+
+这一阶段奠定了三个长期结构:
+
+1. 窗口是工作上下文容器
+2. 图标 / 文档 / 文件夹是可见对象
+3. 菜单 / 按钮 / 拖拽是标准动作
+
+GUI 最大的贡献不是替代 CLI,而是让“状态可见”和“操作可发现”成为主流软件的基本要求。
+
+### 3.3 第三阶段:集成开发工作台与专业工作面
+
+IDE 是现代工作台思想第一次被做成高完成度产品。
+
+IDE 不只是把编辑器、编译器、调试器放在一起,它定义了“一个工作面如何承接完整任务闭环”:
+
+1. 代码编写
+2. 运行与调试
+3. 错误定位
+4. 项目导航
+5. 构建与发布
+
+这一步的关键不是视觉变化,而是:
+
+**从单功能工具,升级为围绕任务闭环组织的统一工作面。**
+
+这条线后来影响了许多企业软件和内容生产软件。
+“左侧导航 + 中央主区 + 底部状态/日志 + 右侧属性/上下文”的经典工作台骨架,很大程度上就是专业工作面的产物。
+
+### 3.4 第四阶段:企业事务系统与角色化工作面
+
+当 ERP、CRM、ITSM、BPM 系统进入企业核心流程后,工作台开始从“工具整合”走向“角色整合”。
+
+这类系统解决的是:
+
+1. 谁在什么岗位上处理什么事务
+2. 谁能看到哪些数据和动作
+3. 哪些流程必须被审批、留痕、追责
+
+这一阶段的典型变化是:
+
+1. 从功能菜单走向角色入口
+2. 从自由探索走向事务驱动
+3. 从单用户文件处理走向组织流程处理
+
+CRM 的演进很有代表性:它从纸质联系人和 Rolodex,发展到 ACT!、SFA、Siebel,再到 Salesforce 的云化,再到 AI 和 agentic CRM,整个演进始终围绕“组织如何围绕客户任务协同”展开,而不只是数据录入 ([TechTarget, 2024](https://www.techtarget.com/searchcustomerexperience/infographic/The-history-and-evolution-of-CRM))。
+
+SAP 的演进也说明了同一件事:传统 SAP GUI 的强项是事务深度和专家效率,但 Fiori 明确把方向改成角色化、移动化和 launchpad 化,也就是从“复杂事务屏”走向“角色工作台” ([SAP PRESS, 2026](https://learning.sap-press.com/sap-fiori-overview))。
+
+### 3.5 第五阶段:Web / SaaS 工作台
+
+SaaS 时代最大的变化,不只是部署方式变成浏览器,而是:
+
+1. 工作台变成持续在线的服务入口
+2. 组织成员开始共享同一个实时系统
+3. 集成能力成为产品竞争核心
+
+这使工作台从本地软件壳,变成组织级云端操作面。
+这一步之后,“一个页面就是一个应用”的理解开始失效,取而代之的是:
+
+1. 首页总览
+2. 模块导航
+3. 搜索入口
+4. 角色仪表盘
+5. 跨应用跳转
+6. 消息与通知中心
+
+也就是说,SaaS 重新定义了工作台的边界:
+工作台不再只是当前功能页,而是整个平台的统一入口层。
+
+### 3.6 第六阶段:协同工作台与共享上下文
+
+Lotus Notes、Google Docs、Slack、Teams、Notion、Figma 这一线,给工作台带来的最大变化是:
+
+**工作对象从“文件副本”变成“共享上下文”。**
+
+这一步至少有四个决定性变化:
+
+1. 文档从静态文件变成共享空间
+2. 消息从即时通知变成可搜索知识
+3. 任务不再附着于单人界面,而是附着于团队上下文
+4. 工具集成开始被工作台统一编排
+
+Google Docs 的历史意义,不是“在线版 Word”,而是把文档从附件制、版本制,改成单一实时源 ([Google, 2021](https://blog.google/products-and-platforms/products/workspace/happy-15-years-google-docs/))。
+
+Slack 的历史意义,也不是“企业 IM”,而是把人、知识、工具和工作流集中到同一环境里,并明确提出自己是 “operating system for work” ([Slack, 2026](https://slack.com/resources/why-use-slack/what-is-slack-and-how-does-it-work))。
+
+Teams 则把协同工作台继续往“整套生产力套件前台”推进,把聊天、会议、文件、日程、自动化和权限体系统一挂到 Microsoft 365 上,形成真正的平台级工作枢纽 ([m.io, 2026](https://www.m.io/blog/history-of-microsoft-teams))。
+
+从这一阶段开始,工作台的核心不再只是“我如何操作软件”,而是:
+
+**团队如何围绕同一个任务上下文持续协作。**
+
+### 3.7 第七阶段:Copilot 与对话入口
+
+ChatGPT 以及随后进入各种软件的 Copilot,把软件工作台再次向前推了一步:
+
+1. 自然语言成为新的启动入口
+2. 用户可以先表达目标,再让系统反推动作
+3. 软件第一次在大众层面拥有“解释、建议、草拟、改写”的统一入口
+
+这一阶段真正被验证的,不是“聊天界面很新鲜”,而是:
+
+**自然语言把复杂软件的启动门槛显著降低了。**
+
+这也是为什么 2023 之后,大量成熟软件不是先重写全部 UI,而是先把 Copilot / Assistant 插到现有工作台里。
+因为它们看见的是一个新增入口层,而不是一次性替换全部交互层。
+
+### 3.8 第八阶段:Agent Workbench 与控制面升级
+
+2025 之后的新变化,不是“聊天更像人”,而是软件开始出现真正的 agent orchestration layer。
+
+这一阶段的工作台,已经不只是一个对话框,而是一个控制面:
+
+1. 用户发起任务
+2. 系统拆解任务
+3. Agent 调用工具执行
+4. 人在关键节点审阅、接管、回退
+5. 平台记录过程、产物与状态
+
+GitHub Copilot 的 agent mode 已经明确越过“回答问题”阶段,开始自动识别子任务、跨文件修改、建议或调用工具,并进入自修复循环 ([GitHub Blog, 2025](https://github.blog/news-insights/product-news/github-copilot-agent-mode-activated/))。
+
+开发类产品还进一步出现了 agent command center、parallel sessions、agent fleet management 等结构,说明工作台的重心正在从“单轮交互”转向“多执行体编排与治理”。
+
+这一步意味着:
+
+**AI 工作台的主价值,不再只是生成内容,而是协调执行。**
+
+---
+
+## 4. 代表性工作台类型
+
+站在 2026 年看,全球主流软件工作台大致可分为六类:
+
+1. **命令工作台**
+ - 代表:CLI、终端、脚本环境
+ - 强项:表达力、组合性、自动化
+
+2. **对象工作台**
+ - 代表:桌面 GUI、文件管理器、传统设计软件
+ - 强项:状态可见、对象直观、通用性高
+
+3. **专业工作台**
+ - 代表:IDE、分析平台、工程软件
+ - 强项:围绕专业闭环组织能力
+
+4. **事务工作台**
+ - 代表:ERP、CRM、后台系统、ITSM
+ - 强项:角色化、审批化、留痕化
+
+5. **协同工作台**
+ - 代表:Docs、Slack、Teams、Notion、Figma
+ - 强项:共享上下文、多人协作、知识沉淀
+
+6. **Agent 工作台**
+ - 代表:Copilot Workspace、AI-native IDE、Agent Control Plane
+ - 强项:自然语言发起、工具调用、自动推进、人工接管
+
+注意,这六类不是相互排斥。
+今天最有竞争力的产品,往往恰恰是多层叠加:
+
+1. 用对话发起
+2. 用事务面控制权限与状态
+3. 用协同面承接共享上下文
+4. 用 Agent 层接管执行
+
+---
+
+## 5. 对当前 AI 工作台讨论的直接启示
+
+### 5.1 聊天框不是工作台的全部
+
+聊天框只解决了“怎么开始说”,没有天然解决:
+
+1. 过程状态可见
+2. 候选项并列比较
+3. 默认与确认边界
+4. 回退与审计
+5. 多对象、多工具、多步骤协同
+
+### 5.2 现代工作台的核心价值已经变成“协调”
+
+从 Slack 到 Teams,再到 Agent Control Plane,最值钱的都不是单一功能,而是协调层:
+
+1. 协调人和人
+2. 协调人和文档
+3. 协调任务和工具
+4. 协调模型和执行体
+
+### 5.3 AI 没有取消 UI,AI 把 UI 重新分层了
+
+现在真正发生的事不是“界面被对话吞掉”,而是界面被重新分成四层:
+
+1. 意图输入层
+2. 决策接管层
+3. 执行编排层
+4. 状态审计层
+
+哪怕在最先进的 AI 工作台里,这四层也都存在,只是表达方式不同。
+
+### 5.4 新一代产品竞争点是“工作台控制权”
+
+未来产品竞争,不只是谁的模型更强,而是谁控制:
+
+1. 任务入口
+2. 上下文组织
+3. 工具调用
+4. 审批回路
+5. 结果回写
+
+也就是说,真正的战略高地不是单个 Agent,而是 Agent 所附着的工作台控制面。
+
+---
+
+## 6. 结论
+
+全球主流软件工作台的历史演进,说明了一个非常稳定的规律:
+
+**每一代工作台的胜出,不是因为它删掉了上一代,而是因为它把“发起任务、做决策、调动资源、沉淀结果”的闭环做得更完整。**
+
+所以,今天讨论对话式 AI 软件时,真正的问题不是:
+
+- 要不要聊天框
+- 要不要按钮
+- 要不要 Tab
+
+真正的问题是:
+
+1. 对话负责什么
+2. 结构化界面负责什么
+3. Agent 负责什么
+4. 人工确认点放在哪里
+5. 过程如何保持可见、可接管、可回退
+
+这也是为什么下一代 AI 工作台,不会回到传统 GUI,也不会停在纯聊天界面。
+它会走向一种新的混合形态:
+
+**对话负责发起,结构化界面负责关键接管,Agent 负责执行推进,工作流面板负责持续可见。**
+
+---
+
+## 7. 参考来源
+
+1. The History of User Interfaces, https://www.historyofui.com/
+2. TechTarget, The history and evolution of CRM, https://www.techtarget.com/searchcustomerexperience/infographic/The-history-and-evolution-of-CRM
+3. Slack, What is Slack and how does it work?, https://slack.com/resources/why-use-slack/what-is-slack-and-how-does-it-work
+4. Google, 15 milestones, moments and more for Google Docs’ 15th birthday, https://blog.google/products-and-platforms/products/workspace/happy-15-years-google-docs/
+5. SAP PRESS, SAP Fiori Overview: UX, App Types, and Deployment Models, https://learning.sap-press.com/sap-fiori-overview
+6. m.io, The History of Microsoft Teams, https://www.m.io/blog/history-of-microsoft-teams
+7. GitHub Blog, Vibe coding with GitHub Copilot: Agent mode and MCP support rolling out to all VS Code users, https://github.blog/news-insights/product-news/github-copilot-agent-mode-activated/
diff --git a/docs/20_Global_Workbench_Evolution/GW02_对话式AI工作台与传统UI分界判断.md b/docs/20_Global_Workbench_Evolution/GW02_对话式AI工作台与传统UI分界判断.md
new file mode 100644
index 0000000..6299bdf
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/GW02_对话式AI工作台与传统UI分界判断.md
@@ -0,0 +1,361 @@
+# GW02_对话式AI工作台与传统UI分界判断
+
+> 状态:专题判断
+> 最后更新:2026-09-23
+> 关联文档:
+> - `GW01_全球主流软件工作台历史演进.md`
+> - `docs/02_Architecture/AR12_对话驱动与结构化交互架构.md`
+
+---
+
+## 1. 问题定义
+
+新一代 AI 驱动软件,最大的误判之一,就是把问题理解成:
+
+- “聊天框会不会取代传统 UI”
+- “问答式是不是比按钮式先进”
+- “有了自然语言之后,页面结构是不是就不重要了”
+
+这些提法都不够准确。
+
+真正的问题是:
+
+**当软件具备自然语言理解、内容生成、工具调用和阶段性自动执行能力之后,传统 UI 的职责如何被重排。**
+
+也就是说,我们讨论的不是“聊天 vs 页面”,而是:
+
+1. 哪些环节适合开放表达
+2. 哪些环节必须显式选择
+3. 哪些环节可以自动执行
+4. 哪些环节必须由人工接管
+
+---
+
+## 2. 结论先行
+
+结论非常明确:
+
+**对话式 AI 工作台不会消灭传统 UI,但会把传统 UI 从“唯一入口层”改造成“结构化接管层和可见性层”。**
+
+所以,新一代 AI 工作台最合理的主形态不是:
+
+1. 纯聊天
+2. 纯表单
+3. 聊天框旁边随便贴几个按钮
+
+而是:
+
+**对话发起 + 结构化决策接管 + 持续可见的工作流状态面。**
+
+---
+
+## 3. 为什么纯对话不够
+
+### 3.1 对话擅长启动,不擅长稳定决策
+
+用户在任务开始时,经常只能说出目标,说不清完整参数。
+
+例如:
+
+- “帮我围绕这个热点写一篇公众号稿”
+- “给我配一条 AI 路由策略”
+- “把组织配置整理成更适合后台管理的结构”
+
+这时候对话很强,因为它允许模糊表达、逐步澄清和低门槛启动。
+
+但一旦进入关键分叉点,例如:
+
+1. 选哪个选题
+2. 选哪个标题
+3. 走哪个 Provider
+4. 是否继续发布
+5. 是否覆盖已有结果
+
+纯对话就会开始失控。
+
+原因很简单:
+
+1. 候选项会被消息流淹没
+2. 用户不容易对比多个候选
+3. 用户不清楚系统是“建议”还是“已经默认执行”
+4. 历史决策边界不清楚
+
+### 3.2 对话天然是线性的,任务天然是分叉的
+
+聊天是时间线结构。
+工作流是状态机结构。
+
+这两者在关键节点上天然冲突。
+
+只靠聊天往下滚,会出现三个问题:
+
+1. 分叉点不可见
+2. 默认选择不可见
+3. 回退路径不可见
+
+所以只要任务不是单轮问答,而是持续推进的工作闭环,对话就必须让位给某种结构化承接。
+
+### 3.3 对话会放大“黑箱感”
+
+当系统既能理解自然语言,又能自动往下执行时,如果没有显式状态承接,用户会产生强烈的不确定感:
+
+1. AI 为什么选了这个方案
+2. 还有哪些备选没展示
+3. 这一步是不是已经不可逆
+4. 是我确认的,还是系统默认的
+
+这会直接伤害信任。
+
+所以对话式 AI 产品如果不补足结构化接管层,看起来很智能,实际会越来越难控。
+
+---
+
+## 4. 为什么纯传统 UI 也不够
+
+传统 UI 的强项也很明确:
+
+1. 候选项并列展示
+2. 状态稳定
+3. 可见性强
+4. 回退路径清楚
+5. 权限和审计友好
+
+但传统 UI 有三个天然短板:
+
+1. 启动成本高
+2. 表单感强
+3. 难以承接模糊目标
+
+用户在任务开始时,往往根本不知道该先填哪一项。
+如果系统一上来就是大表单和流程页,用户会把认知负担提前承担掉。
+
+所以传统 UI 很适合“确认和控制”,不适合“生成需求和探索方向”。
+
+---
+
+## 5. 正式分界:开放表达与有限决策
+
+这一代 AI 工作台最关键的交互边界,可以用一句话概括:
+
+**开放问题用对话,有限决策用结构化 UI。**
+
+### 5.1 适合对话的环节
+
+以下环节优先用自然语言:
+
+1. 用户第一次描述目标
+2. 用户补充背景和约束
+3. 用户要求 AI 解释建议
+4. 用户对结果做局部修改
+5. 用户重新定义问题
+
+### 5.2 适合结构化 UI 的环节
+
+以下环节必须有显式承接:
+
+1. 系统已经形成有限候选集
+2. 用户必须在候选中选一个继续
+3. 不同选项会导致不同下游动作
+4. 系统需要记录“用户确认”还是“默认沿用”
+5. 一旦继续执行,会产生不可忽略的时间、资源或结果成本
+
+### 5.3 两者必须共存的环节
+
+最理想的形态不是按钮替代对话,而是:
+
+1. 给出候选项
+2. 提供一键选择
+3. 同时允许用户说“都不要,我自己改一个”
+
+所以最佳交互单元不是单纯按钮,也不是单纯问答。
+
+最佳交互单元是:
+
+**候选卡片 + 一键确认 + 手工改写入口。**
+
+---
+
+## 6. 对话式 AI 软件和传统软件的根本差异
+
+传统软件大多要求:
+
+1. 用户先学会系统结构
+2. 用户再进入正确页面
+3. 用户再按流程填写和点击
+
+对话式 AI 软件把这件事倒过来了:
+
+1. 用户先说目标
+2. 系统再帮助补全结构
+3. 再在关键节点要求用户接管
+
+这带来两个本质变化:
+
+### 6.1 入口从“功能入口”变成“意图入口”
+
+传统软件的入口是模块和菜单。
+AI 软件的入口可以是目标和需求。
+
+这让系统第一次可以从“我要做什么”而不是“我要进哪个页面”开始。
+
+### 6.2 软件开始承担“中间推理”
+
+传统 UI 默认把中间推理留给用户。
+AI 工作台开始替用户做一部分:
+
+1. 补参数
+2. 提候选
+3. 生成草稿
+4. 给建议
+5. 编排执行路径
+
+但中间推理越强,越需要“中间可见”。
+
+这就是为什么 AI 软件不可能只有聊天框。
+因为它承担了更多中间推理,所以更需要中间状态可视化。
+
+---
+
+## 7. 对工作台设计的具体要求
+
+### 7.1 必须区分三类状态
+
+所有 AI 工作台都必须明确区分:
+
+1. `用户输入`
+2. `系统建议`
+3. `系统已执行`
+
+如果这三类状态在界面上混在一起,用户很快就会失去控制感。
+
+### 7.2 必须标明默认值与确认值
+
+很多系统喜欢“先帮你默认选一个再继续跑”,短期看流畅,长期看一定出问题。
+
+因为用户会分不清:
+
+1. 这是我选的
+2. 这是系统代我选的
+3. 这是系统只是先暂存的
+
+所以默认项必须显式标注。
+
+### 7.3 必须有待接管状态
+
+一旦系统来到关键分叉点,不能继续伪装成自然流畅对话。
+
+它必须明确进入一种待接管状态,例如:
+
+1. 待选题
+2. 待选标题
+3. 待确认发布
+4. 待选择路由
+
+这一步是 AI 工作台和“会聊天的功能插件”之间的分水岭。
+
+### 7.4 必须允许回到对话局部修改
+
+结构化接管不是把系统重新做回旧表单。
+
+用户在接管后,仍然应该能立刻回到对话里说:
+
+- “第三个标题好一点,但再稳一点”
+- “不要这些候选,我自己写一个”
+- “就按这个选题,但换个更像行业观察的语气”
+
+也就是说:
+
+**结构化接管负责做决定,对话负责做微调。**
+
+---
+
+## 8. 对 EAI 这类系统的直接启示
+
+EAI 这类“任务中心 + 对象中心”的平台,不应该模仿传统后台,也不应该停在单一聊天产品形态。
+
+它更接近一种“可控的 AI 工作台”。
+
+所以 EAI 的正确方向应当是:
+
+1. 一句话发起任务
+2. 系统自动形成候选与工作流初稿
+3. 在关键节点由用户显式接管
+4. 系统继续自动推进
+5. 全过程在工作流面板中保持可见
+
+这意味着:
+
+### 8.1 专员工作流不能只靠消息流表达
+
+例如公众号创作专员的:
+
+1. 选题
+2. 标题
+3. 提纲
+4. 配图
+5. 发布准备
+
+其中至少前两步就必须有结构化承接。
+
+### 8.2 技能执行不能只显示“开始执行 / 执行完成”
+
+技能如果涉及多候选、多配置、多路由选择,也必须有中间状态和待确认点。
+
+### 8.3 后台治理界面仍然必须保留高度结构化
+
+AI 调用量、路由策略、Provider 配置、权限与审计,这些都不是适合纯对话承接的主面。
+这些地方仍然需要明确的表格、Tab、对比视图和可追责记录。
+
+所以 AI 平台永远不会完全取消“传统后台”,它只会改变前台任务发起和工作流协作方式。
+
+---
+
+## 9. 一句话判断
+
+**对话式 AI 工作台最大的创新,不是让软件变成聊天,而是让用户可以先说目标,再在关键节点低成本接管系统。**
+
+这也是它与传统 UI 的根本区别:
+
+1. 传统 UI 要求用户先理解结构再操作
+2. AI 工作台允许用户先表达目标再逐步接管
+
+但这条路成立的前提是:
+
+**系统必须把关键决策点重新显式化。**
+
+没有这一步,对话式 AI 软件就会沦为:
+
+1. 启动很惊艳
+2. 中途很混乱
+3. 结果很难控
+
+---
+
+## 10. 结论
+
+新一代对话式 AI 软件和传统 UI 之间的重大区别,不在于有没有聊天框,而在于:
+
+**系统把“需求形成”前移给对话,把“关键决策”交还给结构化界面,把“持续推进”交给 Agent,把“状态与审计”交给工作流控制面。**
+
+这四层缺一不可。
+
+因此,真正成熟的 AI 工作台不会长成纯聊天产品,也不会回退成纯表单后台。
+它会长成一种混合形态:
+
+1. 对话负责发起
+2. 卡片 / 按钮 / Tab 负责接管
+3. Agent 负责执行
+4. 工作流面板负责可见性和回退
+
+这不是传统 UI 的终结。
+这是传统 UI 被重新分工。
+
+---
+
+## 11. 参考来源
+
+1. The History of User Interfaces, https://www.historyofui.com/
+2. Slack, What is Slack and how does it work?, https://slack.com/resources/why-use-slack/what-is-slack-and-how-does-it-work
+3. Google, 15 milestones, moments and more for Google Docs’ 15th birthday, https://blog.google/products-and-platforms/products/workspace/happy-15-years-google-docs/
+4. SAP PRESS, SAP Fiori Overview: UX, App Types, and Deployment Models, https://learning.sap-press.com/sap-fiori-overview
+5. GitHub Blog, Vibe coding with GitHub Copilot: Agent mode and MCP support rolling out to all VS Code users, https://github.blog/news-insights/product-news/github-copilot-agent-mode-activated/
diff --git a/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md b/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md
new file mode 100644
index 0000000..028e04c
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/GW03_专员工作交互形式分析.md
@@ -0,0 +1,554 @@
+# GW03_专员工作交互形式分析
+
+> 状态:专题分析
+> 最后更新:2026-09-23
+> 关联文档:
+> - `GW01_全球主流软件工作台历史演进.md`
+> - `GW02_对话式AI工作台与传统UI分界判断.md`
+> - `docs/05_Object_Catalog/specialists/`(各专员详细定义)
+
+---
+
+## 1. 分析目标
+
+GW02 给出了"对话 vs 结构化 UI"的分界原则。本文件把它落到具体对象上:
+
+**平台中每个专员,它的启动、决策、执行、反馈分别应该走对话还是走 UI 选择。**
+
+分析维度统一为四项:
+
+| 维度 | 说明 |
+|---|---|
+| **启动方式** | 用户如何开始与该专员的交互 |
+| **决策方式** | 系统给出候选后,用户如何选择 |
+| **执行方式** | 系统自动推进还是人工辅助 |
+| **反馈/审计方式** | 结果如何展示、如何回查 |
+
+所有分析基于以下分界标准(来自 GW02):
+
+1. 开放问题用对话
+2. 有限决策用结构化 UI
+3. 候选卡片 + 一键确认 + 手工改写入口 = 最佳交互单元
+
+---
+
+## 2. 通用级专员分析
+
+### 2.1 通用助手(general-assistant)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话**。用户直接说目标,不进入任何应用路由。这是系统唯一入口。 |
+| **决策方式** | **对话为主,路由选择用结构化**。通用助手不自己做领域决策,它的核心职责是"判断意图 → 推荐专员"。当它推荐多个专员时,需要用结构化卡片展示选项,而不是纯对话列表。 |
+| **执行方式** | **dw(默认工作站)**。它可以自主执行简单任务(闲聊、查资料、任务拆解),但遇到领域任务时必须路由到其他专员。 |
+| **反馈/审计** | **对话流 + 任务记录**。简单任务直接在对话中反馈;路由决策需要记录"用户选择了哪个专员、从什么入口进入"。 |
+
+**交互关键判断:**
+
+通用助手不应该只是一个"会聊天的功能插件"。它是平台的路由中枢。它的关键交互特征是:
+
+1. 用户说"帮我写一篇公众号稿" → 对话启动
+2. 通用助手判断意图 → 对话分析
+3. 系统展示"公众号创作专员 / 汇报专员 / 通用任务"等候选卡片 → **结构化选择**
+4. 用户选"公众号创作专员" → 进入公众号创作专员的工作流
+
+所以通用助手的"对话"只是入口层,它的真正价值在"路由决策的透明化"。
+
+---
+
+### 2.2 公众号创作专员(wechat-official-account)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话**。用户说"帮我起一个公众号选题"或"围绕这个热点写一篇文章"。 |
+| **决策方式** | **必须结构化接管**。公众号创作专员有四步工作流:选题 → 标题 → 提纲 → 正文。每一步产出多个候选后,都必须用结构化卡片展示,让用户做选择。不能只在聊天流里说"我生成了3个选题"让用户回复序号。 |
+| **执行方式** | **dw(默认工作站)**。内容生成可自主执行,但每个候选都需要用户确认后才进入下一步。 |
+| **反馈/审计** | **对话流 + 工作流面板**。生成过程在对话中展示,但选题、标题等关键决策必须在工作流面板中可见,支持回退和修改。 |
+
+**交互关键判断:**
+
+公众号创作专员是平台中工作流最清晰的专员之一。它的工作流四步不是四段对话,而是**四次决策分叉**:
+
+```
+启动对话 → 生成3个选题 → [用户选第2个] → 生成3个标题 → [用户选第1个]
+→ 生成提纲 → [用户修改后确认] → 生成正文 → [用户编辑后发布准备]
+```
+
+四次分叉,四次结构化接管。
+
+如果只做纯对话,会出现以下问题:
+
+1. 三次选题在聊天流中被淹没,用户找不到"我到底选了哪个"
+2. 用户无法回退到"换个标题"而不重写整篇文章
+3. 用户不知道系统已经执行到哪一步,哪些已确认、哪些是默认
+
+所以公众号创作专员的正确交互形态是:
+
+**对话启动 → 候选卡片选择 → 候选卡片选择 → 候选卡片选择 → 候选卡片选择 → 最终编辑**
+
+每一步都是"对话生成 + 卡片选择"的闭环。
+
+---
+
+### 2.3 汇报专员(presentation-briefing)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话**。用户说"帮我做一版 8 页管理层汇报 PPT"。 |
+| **决策方式** | **混合。页数/结构用结构化,内容用对话微调**。汇报专员的输出是 PPT 结构,这是一个强结构化的文档类型。用户应该先选择汇报类型(经营汇报/方案汇报/培训课件/项目复盘),然后系统生成大纲结构,用户确认或修改。 |
+| **执行方式** | **dw(默认工作站)**。PPT 生成可自主执行,但大纲确认后才能生成具体内容。 |
+| **反馈/审计** | **结构化预览 + 对话微调**。PPT 大纲需要结构化预览,生成后的内容允许用户在对话中说"第三页太长了,精简一下"。 |
+
+**交互关键判断:**
+
+汇报专员和公众号创作专员不同。公众号是"从零创作",汇报专员是"基于已有材料整理"。所以它的启动阶段可能需要用户上传文件或粘贴文本,这本身就是结构化输入。
+
+关键决策点:
+
+1. 选择汇报类型 → **结构化卡片**
+2. 上传/粘贴材料 → **结构化文件上传**
+3. 生成大纲 → **结构化预览 + 确认**
+4. 生成内容 → **对话微调(逐页说)**
+5. 最终导出 → **结构化确认**
+
+---
+
+## 3. 行业级专员分析
+
+### 3.1 合同审查专员(contract-review)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话 + 文件上传**。用户说"审查这份采购合同",同时上传合同文件。 |
+| **决策方式** | **必须结构化接管**。合同审查的输出是风险分级(critical/high/medium/low),这是一个高度结构化的结果。用户必须能看到完整的条款列表、风险等级、红线建议,并逐项确认。纯对话无法承载这种复杂信息。 |
+| **执行方式** | **adw(辅助默认工作站)**。合同审查涉及法律合规,必须人工确认每个风险点。系统可以自动识别条款和标记风险等级,但用户必须逐项审核。 |
+| **反馈/审计** | **高度结构化**。审查结果需要以表格形式展示:条款名、风险等级、建议、原始条款位置。这是审计友好的界面。 |
+
+**交互关键判断:**
+
+合同审查专员是"adw 模式"的典型代表。它的工作流程是:
+
+```
+上传合同 → [系统自动分析] → 结构化展示风险表格 → [用户逐项确认/修改] → 生成审查报告 → [用户确认/导出]
+```
+
+每一步都不能用纯对话替代。原因:
+
+1. 风险分级是四级结构化数据,聊天流无法清晰展示
+2. 用户需要能"只看 critical 和 high"、"按条款类型筛选"
+3. 审查结果是法律级文件,必须有完整的审计追踪
+
+所以合同审查专员的"对话"只发生在启动阶段(说一句话 + 上传文件),后续全部是结构化交互。
+
+---
+
+### 3.2 售前方案专员(solution-proposal)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话 + 结构化输入**。用户说"帮我起一版智能培训升级方案",系统追问客户背景、行业、规模、预算等关键参数。 |
+| **决策方式** | **必须结构化接管**。售前方案的核心输出是"方案结构 + 需求澄清 + 风险边界",这些都是高度结构化的内容。 |
+| **执行方式** | **adw(辅助默认工作站)**。售前方案涉及客户关系,需要人工确认哪些是"已确认需求"、哪些是"我方假设"。 |
+| **反馈/审计** | **结构化方案预览 + 对话微调**。方案大纲需要结构化展示,内容细节允许对话微调。 |
+
+**交互关键判断:**
+
+售前方案专员的交互关键在"需求澄清"环节。系统不能直接生成方案,而是应该:
+
+```
+用户说需求 → [系统追问关键参数] → 生成需求清单 → [用户确认需求]
+→ 生成方案大纲 → [用户确认大纲] → 生成方案内容 → [用户修改后确认]
+```
+
+其中"系统追问关键参数"阶段是**对话引导的结构化采集**——系统用对话问用户,但用户回答的内容是结构化的(下拉选择、数字输入、多选)。
+
+---
+
+### 3.3 客户跟进专员(customer-followup)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话 + 数据导入**。用户说"帮我跟进这些客户",系统导入客户数据或用户粘贴客户信息。 |
+| **决策方式** | **混合。意向分级用结构化,联系策略用对话生成**。客户意向分级(A/B/C/D)是结构化判断,但首轮联系话术可以先生成多条供用户选择。 |
+| **执行方式** | **adw(辅助默认工作站)**。客户跟进涉及对外沟通,需要人工确认每条联系消息后再发送。 |
+| **反馈/审计** | **结构化客户列表 + 对话生成的话术**。客户状态需要结构化表格展示,话术可以对话生成。 |
+
+**交互关键判断:**
+
+客户跟进专员是"数据驱动型"专员。它的工作流程是:
+
+```
+导入客户数据 → [系统自动分级] → 结构化展示客户列表和意向等级 → [用户确认分级]
+→ 生成首轮联系话术 → [用户选择/修改] → 预约建议 → [用户确认] → 状态回写
+```
+
+关键决策点:
+
+1. 客户分级 → **结构化(系统建议等级 + 用户确认)**
+2. 联系话术 → **对话生成 + 卡片选择**
+3. 预约时间 → **结构化日历选择**
+4. 状态回写 → **结构化确认**
+
+---
+
+### 3.4 简历筛选与招聘专员(resume-processor)
+
+| 维度 | 分析 |
+|---|---|
+| **启动方式** | **对话 + 批量导入**。用户说"帮我筛这批简历",系统导入简历文件或用户粘贴候选人信息。 |
+| **决策方式** | **必须结构化接管**。招聘筛选的核心输出是"候选人排序 + 匹配度评分 + 推荐理由",这是一个典型的排序决策问题。聊天流无法清晰展示排名和对比。 |
+| **执行方式** | **dw(默认工作站)**。简历筛选可自主执行,但用户必须能看到完整的候选人列表、匹配度、筛选理由,并手动调整排序。 |
+| **反馈/审计** | **高度结构化表格**。候选人列表必须按匹配度排序展示,支持按技能、经验、学历等维度筛选。这是招聘场景的刚需。 |
+
+**交互关键判断:**
+
+简历筛选与招聘专员是"数据驱动型"专员中最需要结构化 UI 的一个。它的工作流程:
+
+```
+导入简历 → [系统自动解析和评分] → 结构化展示候选人排名表格 → [用户调整排序/标记面试/淘汰]
+→ 生成面试建议 → [用户确认] → 面试安排 → [用户确认]
+```
+
+关键决策点:
+
+1. 简历解析 → **系统自动执行 + 结构化展示**
+2. 匹配度评分 → **系统建议 + 用户调整**
+3. 面试/淘汰标记 → **结构化按钮操作**
+4. 面试建议 → **对话生成 + 卡片选择**
+5. 面试安排 → **结构化日历选择**
+
+这个专员有一个特殊特征:**排序决策**。聊天流完全不适合做排序和对比。用户需要能看到 Top 10 候选人的横向对比。
+
+#### 3.4.1 功能拆解:它真正要负责什么
+
+这个专员不能只做“筛简历”这个单点动作,它应该负责一条完整但边界清晰的招聘推进链:
+
+| 功能层 | 必须覆盖的能力 | 不能缺的输出 |
+|---|---|---|
+| **招聘入口归档** | 拉取招聘邮箱、上传附件、识别简历、去重合并、保留来源 | 候选人入口清单、重复投递合并记录 |
+| **岗位要求建模** | 读取 JD、拆出硬门槛、加分项、淘汰项、面试关注项 | 结构化筛选规则卡、评分维度表 |
+| **候选人解析** | OCR、字段抽取、技能识别、项目经历拆解、年限估算 | 候选人标准档案 |
+| **匹配度评分** | 按岗位维度打分、给排序理由、标不确定项 | 排名表、评分证据、待确认问题 |
+| **推进建议** | 推荐进入面试/淘汰/保留池、给面试重点、生成沟通建议 | 推荐清单、面试建议卡、淘汰原因 |
+| **招聘协同** | 面试排期、状态回写、和招聘经理确认 | 面试安排单、状态流转记录 |
+
+这里最关键的一点是:**它的主价值不是“帮你读一份简历”,而是“把一批候选人变成一张可推进的决策清单”。**
+
+如果只做解析或摘要,它只是技能;如果能把入口、评分、排序、推进连起来,它才配叫专员。
+
+#### 3.4.2 好用的目标工作流:不是线性表单,而是批处理决策流
+
+这个专员的理想工作流应该分成 6 段,每一段都有清晰输入、系统动作和人工确认点:
+
+1. **创建招聘任务**
+ 用户输入岗位名称、JD、招聘邮箱或上传简历包,选择本轮筛选目标。
+
+2. **建立筛选规则**
+ 系统把 JD 拆成:
+ - 硬门槛:必须满足
+ - 加分项:满足越多越靠前
+ - 风险项:信息缺失或冲突
+ - 面试关注项:需要面谈验证
+
+3. **批量解析候选人**
+ 系统读取邮件、附件、PDF、图片简历,抽取候选人结构化字段,并做重复投递识别。
+
+4. **自动评分与排序**
+ 系统按统一评分卡打分,输出总分、分项分、排名、理由、不确定项。
+
+5. **人工校正与分桶**
+ 用户直接在表格里做四类动作:
+ - 调整排序
+ - 标记进入面试
+ - 标记淘汰
+ - 标记待确认
+
+6. **生成推进动作**
+ 系统批量生成:
+ - 推荐候选人清单
+ - 面试重点摘要
+ - 邀约邮件/消息草稿
+ - 面试安排建议
+
+真正影响可用性的,不是第 3 步“AI 能不能抽字段”,而是第 5 步“用户能不能极快地改、看、比、确认”。
+
+#### 3.4.3 UI 结构:主界面必须围绕“候选人排名表”展开
+
+这个专员最适合的不是纯聊天页,而是一个 **1 个主表 + 2 个侧栏 + 1 个底部审计区** 的工作台。
+
+建议布局:
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ 顶部任务条:岗位 / 当前轮次 / 待处理数 / 面试推荐数 / 风险数 │
+├──────────────┬──────────────────────────────┬───────────────┤
+│ 左侧规则栏 │ 中央候选人主表 │ 右侧详情栏 │
+│ │ │ │
+│ JD 规则卡 │ 排名 / 候选人 / 总分 / 分项分 │ 候选人详情 │
+│ 硬门槛 │ 来源 / 风险 / 推荐动作 │ 简历原文片段 │
+│ 加分项 │ [筛选][排序][批量操作] │ 评分证据 │
+│ 面试关注项 │ │ 待确认点 │
+│ │ │ 操作记录 │
+├──────────────┴──────────────────────────────┴───────────────┤
+│ 底部审计区:时间线 / 回放 / 对话微调 / 导出结果 │
+└──────────────────────────────────────────────────────────────┘
+```
+
+四个关键区块一个都不能少:
+
+1. **左侧规则栏**
+ 让用户始终看到“系统按什么筛人”。没有这个区块,评分就会变成黑箱。
+
+2. **中央候选人主表**
+ 这是整个页面的核心。列建议固定为:
+ - 排名
+ - 候选人
+ - 当前阶段
+ - 总分
+ - 核心维度分
+ - 来源
+ - 风险
+ - 推荐动作
+ - 人工最终动作
+
+3. **右侧详情栏**
+ 点击任一候选人后展开,展示:
+ - 基本信息
+ - 匹配证据
+ - 简历原文引用
+ - 缺失信息
+ - 推荐理由
+ - 面试重点
+
+4. **底部审计区**
+ 不只是“日志”。它应该包含:
+ - 时间线
+ - 回放节点
+ - 对话微调入口
+ - 导出记录
+
+#### 3.4.4 什么叫“能用好用”:要把高频动作压缩成一屏完成
+
+招聘同学的真实高频动作不是聊天,而是以下几类:
+
+- 先看前 10 名
+- 看某人为什么排这里
+- 快速标记进面 / 淘汰 / 待确认
+- 调整某个维度权重
+- 找出“学历不满足但经验很强”的例外
+- 导出一版给招聘经理确认
+
+所以“好用”的标准不是页面好看,而是下面这些动作能不能极快完成:
+
+| 目标动作 | 好用的交互方式 | 不好用的交互方式 |
+|---|---|---|
+| 看前 10 名 | 默认按总分排序,首屏直出 | 让用户先问一句“谁最合适” |
+| 看排序理由 | 行内展开 + 右栏证据 | 只给一句文本总结 |
+| 标记进面/淘汰 | 表格行内按钮 + 批量操作 | 打开详情页再二次确认 |
+| 改筛选规则 | 左栏直接改权重或门槛 | 回到聊天里重新描述 |
+| 看信息缺口 | 风险标签 + 缺失字段提示 | 把问题埋在长段解释里 |
+| 导出决策单 | 一键导出推荐清单 | 手工复制聊天内容 |
+
+一句话:**这个专员的“好用”来自可批量处理、可横向比较、可快速确认,而不是来自会聊天。**
+
+#### 3.4.5 对话在这里仍然重要,但只能做三件事
+
+这个专员不是不要对话,而是要把对话压缩到最有价值的三个位置:
+
+1. **启动任务**
+ 例如:“帮我按这个 JD 先筛今天邮箱里收到的简历。”
+
+2. **局部调参**
+ 例如:“这轮更看重 ToB 销售经验,学历权重降一点。”
+
+3. **生成解释与沟通文本**
+ 例如:“帮我把前 3 名的推荐理由整理给招聘经理。”
+
+除此之外的核心决策,都应该回到结构化 UI。
+
+#### 3.4.6 当前实现与目标状态之间的差距
+
+结合当前仓库状态,这个专员已经有了对象定义、工作台样例数据和基础话术,但距离“能用好用”还有几处硬缺口:
+
+1. **当前入口仍是通用工作台入口**
+ 前端专员 manifest 默认 `objectEntryRoute = '/home'`,说明它目前是“挂载到通用工作台”的对象,不是独立招聘工作区。
+
+2. **当前工作台主要还是静态场景稿**
+ `workbench.js` 里已经有“候选人列表、时间线、回放”等结构,但本质还是演示态数据,不是招聘域对象驱动的真实 UI。
+
+3. **缺少岗位规则建模面**
+ 现在定义里强调了“按岗位要求评分”,但没有看到专门承载 JD 拆解、权重调节、硬门槛确认的页面结构。
+
+4. **缺少候选人证据面**
+ 真正让招聘经理敢用的不是分数,而是“分数背后的证据引用”。这一层目前还不够具体。
+
+5. **缺少批量动作面**
+ 招聘场景的效率来自批处理:批量标记、批量淘汰、批量发起邀约。没有批量动作,页面再结构化也不够好用。
+
+#### 3.4.7 产品落地优先级:先把什么做出来
+
+如果按“最快形成可用闭环”排序,我建议按下面三层推进:
+
+**P0:先把最小可用闭环做出来**
+
+- 独立招聘任务页
+- JD 结构化规则卡
+- 候选人主表
+- 候选人详情侧栏
+- 进面 / 淘汰 / 待确认 三类动作
+- 推荐清单导出
+
+**P1:把它做顺手**
+
+- 权重调节
+- 缺失信息高亮
+- 例外候选人提示
+- 批量操作
+- 面试重点自动生成
+- 邀约草稿生成
+
+**P2:把它做成真正的招聘工作台**
+
+- 招聘邮箱实时同步
+- 多岗位并行筛选
+- 面试反馈回写
+- 录用结果反哺评分模型
+- “为什么这次排序和上次不同”的版本对比
+
+结论很明确:**这个专员不应该继续停留在“聊天入口 + 几段说明文案”的层级,它必须升级成一个围绕候选人排序表运转的招聘工作台。**
+
+## 4. 综合分析
+
+### 4.1 按交互模式分类
+
+将所有专员按"对话 vs 结构化"的比例排序:
+
+| 专员 | 对话占比 | 结构化占比 | 模式 |
+|---|---|---|---|
+| **通用助手** | 70% | 30% | 对话入口 + 结构化路由 |
+| **公众号创作专员** | 40% | 60% | 对话生成 + 四次结构化选择 |
+| **汇报专员** | 40% | 60% | 对话生成 + 结构化大纲 |
+| **简历筛选与招聘专员** | 20% | 80% | 对话启动 + 高度结构化表格 |
+| **合同审查专员** | 20% | 80% | 对话启动 + 高度结构化表格 |
+| **售前方案专员** | 30% | 70% | 对话引导 + 结构化方案 |
+| **客户跟进专员** | 30% | 70% | 对话启动 + 结构化客户列表 |
+
+### 4.2 关键发现
+
+**发现 1:对话占比最高的不是"对话式 AI",而是路由层**
+
+通用助手对话占比 70%,但它不做领域决策。真正做领域决策的专员,对话占比都在 20%-40%。这说明:
+
+> 对话的最大价值在"启动和路由",不在"执行和决策"。
+
+**发现 2:所有涉及排序、对比、分级的任务必须用结构化 UI**
+
+招聘筛选(候选人排序)、合同审查(风险分级)、客户跟进(意向分级)——这三个专员的共同特征是"多选项对比决策"。聊天流无法承载这种决策需求。
+
+**发现 3:adw 模式的专员一定有更强烈的结构化需求**
+
+合同审查和售前方案都是 adw 模式,它们的共同特征是"涉及合规/风险/客户关系",所以每一步决策都需要人工确认,这也意味着需要更丰富的结构化界面来支撑确认过程。
+
+**发现 4:工作流越清晰的专员,越不适合纯对话**
+
+公众号创作专员有四步明确工作流(选题→标题→提纲→正文),每一步都需要用户确认。这种"多阶段决策"是结构化 UI 的强项,不是聊天流的强项。
+
+---
+
+## 5. 对 EAI 平台的交互设计建议
+
+基于以上分析,对平台交互设计提出以下建议:
+
+### 5.1 通用助手:对话入口 + 结构化路由卡片
+
+通用助手的核心价值是路由。当它推荐多个专员时,必须用结构化卡片展示:
+
+```
+┌─────────────────────────────────────┐
+│ 你可以根据需要选择: │
+│ │
+│ [📝 公众号创作专员] 内容创作 │
+│ [📊 汇报专员] 演示汇报 │
+│ [📋 合同审查专员] 法务审查 │
+│ │
+│ 或者继续用通用助手聊天 │
+└─────────────────────────────────────┘
+```
+
+### 5.2 公众号创作专员:对话生成 + 卡片选择 + 工作流面板
+
+公众号创作专员的四步工作流应该在工作流面板中持续可见:
+
+```
+┌──────────────────┬──────────────────────────┐
+│ 工作流面板 │ 对话区 │
+│ │ │
+│ ① 选题 ● 已选 │ 生成了3个选题: │
+│ ② 标题 ○ 待选 │ [卡片A] [卡片B] [卡片C] │
+│ ③ 提纲 ○ 待选 │ 请选择一个 │
+│ ④ 正文 ○ 待选 │ │
+│ │ │
+│ 已选:选题2 │ │
+│ │ │
+│ [← 上一步] [下一步 →] │
+└──────────────────┴──────────────────────────┘
+```
+
+### 5.3 合同审查/招聘筛选:高度结构化表格 + 对话辅助
+
+这些专员的"主界面"应该是表格:
+
+```
+┌──────┬──────────────┬────────┬──────────────────┐
+│ 排名 │ 候选人/条款 │ 等级 │ 建议/理由 │
+├──────┼──────────────┼────────┼──────────────────┤
+│ 1 │ 张三 │ CRITICAL│ 赔偿条款无限额 │
+│ 2 │ 李四 │ HIGH │ 缺少保密条款 │
+│ 3 │ 王五 │ MEDIUM │ 工作时间不合规 │
+│ ... │ ... │ ... │ ... │
+└──────┴──────────────┴────────┴──────────────────┘
+
+[筛选: 全部 | CRITICAL | HIGH | MEDIUM]
+[导出审查报告] [确认并继续]
+```
+
+在表格下方或侧边保留对话入口,允许用户说"第2条再严格一点"。
+
+### 5.4 所有专员的共同模式
+
+无论哪个专员,都应该遵循以下统一模式:
+
+```
+对话启动 → 系统分析 → 结构化展示候选 → 用户选择/修改
+ → 系统执行 → 结构化展示结果 → 用户确认/修改
+ → 继续或结束
+```
+
+关键原则:
+
+1. **启动用对话**(低门槛进入)
+2. **候选用卡片**(多选项并列展示)
+3. **决策用表格**(排序/对比/分级)
+4. **微调用对话**(局部修改不需要回到表格)
+5. **确认用按钮**(明确用户确认动作)
+6. **状态永远可见**(工作流面板/进度条)
+
+---
+
+## 6. 一句话总结
+
+**所有专员的交互形式可以归结为一句话:**
+
+> 对话负责"说清楚要什么",结构化 UI 负责"从系统中选出来",表格和卡片负责"看清楚选对了没有"。
+
+这三层缺一不可,也不能互相替代。
+
+---
+
+## 8. 参考来源
+
+1. GW01_全球主流软件工作台历史演进.md
+2. GW02_对话式AI工作台与传统UI分界判断.md
+3. SAP PRESS, SAP Fiori Overview: UX, App Types, and Deployment Models, https://learning.sap-press.com/sap-fiori-overview(Fiori 的 1-1-3 规则)
+4. GitHub Blog, Vibe coding with GitHub Copilot: Agent mode and MCP support, https://github.blog/news-insights/product-news/github-copilot-agent-mode-activated/
+5. 各专员详细定义:`docs/05_Object_Catalog/specialists/`
diff --git a/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md b/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md
new file mode 100644
index 0000000..5cbf333
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/GW04_公众号创作专员可用性改进分析.md
@@ -0,0 +1,519 @@
+# GW04_公众号创作专员可用性改进分析
+
+> 状态:专题分析
+> 最后更新:2026-09-23
+> 关联文档:
+> - `GW02_对话式AI工作台与传统UI分界判断.md`
+> - `GW03_专员工作交互形式分析.md`
+> - `docs/05_Object_Catalog/specialists/wechat-official-account/`(对象目录)
+
+---
+
+## 1. 核心结论
+
+**公众号创作专员的后端已经 100% 实现,前端只有骨架没有血肉。**
+
+后端有完整的工作流引擎(9步)、热点抓取(6个业务域、多数据源)、AI集成(选题/标题/提纲/正文生成)、图片生成、预览导出、DB持久化、API路由全部到位。前端只有 manifest 定义和 API 封装函数,没有任何组件、页面、路由或交互。
+
+要让它"真正可用",需要补充的是**整个前端工作流 UI 层**——这是唯一缺失的部分。
+
+---
+
+## 2. 现状全貌
+
+### 2.1 已实现(后端 100%)
+
+| 层级 | 组件 | 文件数 | 行数 |
+|------|------|--------|------|
+| Manifest + 岗位说明书 | 专员定义、技能绑定、边界规则 | 1 | 30 |
+| 工作流引擎 | 9步工作流、步骤执行器、错误处理 | 1 | 1623 |
+| 文章服务 | 文章状态CRUD、选题候选构建 | 1 | 357 |
+| 热点服务 | RSS/Web抓取、评分、缓存、降级 | 1 | 507 |
+| 交付/导出 | 配图、预览、分页、3种导出格式 | 1 | 619 |
+| 图片服务 | base64持久化、静态服务 | 1 | 176 |
+| 业务域配置 | 6个业务域、热点源、关键词 | 1 | 239 |
+| 图片生成 | media 持久化、静态路由 | 1 | 176 |
+| Runtime 辅助 | 常量、压缩、底部面板 | 1 | 58 |
+| 数据模型 | 文章表(26字段)、热点表(14字段) | 2 | 61 |
+| DAO | 文章DAO、热点DAO、包级注册 | 3 | 133 |
+| Router 注册 | 7个API端点 | 1 | 7 |
+| DB 迁移 | 两张表 AutoMigrate | 1 | 2 |
+| Seed 数据 | 专员种子数据 | 1 | 26 |
+| Task Runtime 集成 | 产物压缩、运行记录压缩 | 1 | 2 |
+| Core 注册 | builtinRegistry 注册为第一专员 | 1 | 6 |
+| 测试 | 专员 prompt 测试 | 1 | 6 |
+
+**后端总计约 4300+ 行有效代码,覆盖完整。**
+
+### 2.2 已实现(前端骨架)
+
+| 层级 | 组件 | 说明 |
+|------|------|------|
+| Manifest | manifest.js (26行) | 专员定义 + InteractionCard |
+| API 封装 | officialAccount.js (33行) | 6个API函数 |
+| 内置注册 | builtin.js 注册 | 作为第4个内置专员注册 |
+| 项目模板 | content-operations 绑定 | 绑定公众号创作专员 |
+
+### 2.3 完全缺失(前端)
+
+| 缺失项 | 级别 | 说明 |
+|--------|------|------|
+| **专属路由页面** | 必须 | `/apps/wechat-official-account` 无对应前端页面 |
+| **工作流配置页** | 必须 | 创建任务时输入业务域、关键词、受众、目标、语气等 |
+| **工作流步骤面板** | 必须 | 选题候选卡片选择、标题选择、提纲编辑、正文编辑 |
+| **配图管理 UI** | 必须 | 配图提示词查看、图片预览、图片重生成 |
+| **预览/导出 UI** | 必须 | Markdown/HTML 预览、导出按钮 |
+| **底部面板** | 可选 | timeline / replay / trace / logs 展示 |
+| **状态指示器** | 可选 | 当前步骤进度、完成步骤标记 |
+| **错误处理 UI** | 可选 | 步骤执行失败的错误展示 |
+
+---
+
+## 3. 公众号创作专员完整工作流(后端已有,前端未实现)
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Step 0: 创建任务(前端缺失) │
+│ 输入:业务域、关键词、受众、目标、语气、正文字数、提纲字数、配图数 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 1: 推荐热点选题(后端完整,前端缺失) │
+│ 1) 从热点源抓取(RSS 36kr/IT之家等 + 网页源 国家医保局等) │
+│ 2) 评分过滤(域名词 + 业务词 + 加分词) │
+│ 3) AI 增强生成 3-5 个选题候选 │
+│ 4) 每个选题含:标题、角度、推荐理由、热度等级(爆热/高/中高/中) │
+│ 前端需做:展示候选卡片 → 用户选一个 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 2: 标题生成(后端完整,前端缺失) │
+│ 1) 基于选定选题,AI 生成 3 个标题 │
+│ 2) 兜底模板生成(无AI也能工作) │
+│ 前端需做:展示候选卡片 → 用户选一个 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 3: 提纲生成(后端完整,前端缺失) │
+│ 1) 基于选定标题,AI 生成结构化学术型/行业观察型提纲 │
+│ 2) 兜底模板生成 │
+│ 前端需做:展示提纲 → 用户可编辑/修改 → 确认 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 4: 正文创作(后端完整,前端缺失) │
+│ 1) 基于提纲,AI 生成长文(1400字默认) │
+│ 2) 口语化但不油腻,观点有依据 │
+│ 3) 兜底模板生成 │
+│ 前端需做:展示正文 → 用户可编辑/修改 → 确认 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 5: 配图提示词(后端完整,前端缺失) │
+│ 1) 从正文自动提取配图位置(封面图 + 每个章节配图) │
+│ 2) 为每个配图生成 AI 提示词 │
+│ 前端需做:展示提示词列表 → 用户可编辑/删除 → 确认 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 6: 图片生成(后端完整,前端缺失) │
+│ 1) 调用 image_gen 路由的图片生成 API │
+│ 2) 每个配图生成后持久化 base64 到文件系统 │
+│ 3) 返回图片 URL │
+│ 前端需做:展示生成结果 → 用户可预览 → 对某张图点"重生成" │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 7: 预览与导出(后端完整,前端缺失) │
+│ 1) 生成 Markdown 预览稿(标题 + 正文 + 配图) │
+│ 2) 生成 HTML 预览稿(简易排版) │
+│ 3) 生成分页 Markdown(每章节一页,带 `` 分隔)│
+│ 前端需做:展示预览 → 提供3种导出格式按钮 │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 8: 分页 MD(后端完整,前端缺失) │
+│ 1) 解析分页内容,生成结构化分页数据 │
+│ 2) 每页含:页码、页标题、页面内容 │
+│ 前端需做:展示分页数据(可选,已完成的元数据步骤) │
+│ ─────────────────────────────────────────────────────────────── │
+│ Step 9: 完成 │
+│ 工作流结束,文章状态标记为完成 │
+│ 前端需做:展示完成状态、可导出 │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 4. 改进方案:从"可用"到"好用"
+
+### 4.1 第一层:可用(核心工作流跑通)
+
+**目标:用户能完整走完一遍工作流,产出可发布的 Markdown 预览稿。**
+
+需要实现的前端页面结构:
+
+```
+/apps/wechat-official-account
+├── 创建页:输入业务域、关键词、受众、目标、语气、字数要求
+├── 工作流页(主页面)
+│ ├── 左侧:步骤导航栏(9步进度条)
+│ ├── 中间:当前步骤的交互区
+│ └── 右侧:预览面板(最终稿预览)
+│
+│ Step 1 交互:热点选题候选卡片 → 选一个
+│ Step 2 交互:标题候选卡片 → 选一个
+│ Step 3 交互:提纲文本编辑 → 确认
+│ Step 4 交互:正文文本编辑 → 确认
+│ Step 5 交互:配图提示词列表 → 编辑/删除 → 确认
+│ Step 6 交互:图片预览网格 → 预览/重生成
+│ Step 7 交互:Markdown/HTML 预览 + 导出按钮
+│ Step 8 交互:分页数据展示(只读)
+│ Step 9 交互:完成状态
+└── 导出下载功能
+```
+
+**关键 UI 组件:**
+
+1. **创建表单组件** — 输入业务域、关键词、受众、目标、语气、正文字数、提纲风格、最大配图数
+2. **步骤导航组件** — 9步进度条,显示每步状态(pending/running/completed/error)
+3. **选题候选卡片组件** — 每个选题一张卡片,含标题、角度、热度标签、推荐理由,支持点击选择
+4. **标题候选卡片组件** — 3个标题卡片,点击选择
+5. **文本编辑器组件** — 提纲和正文的编辑区,支持 Markdown 编辑
+6. **配图管理组件** — 提示词列表 + 图片预览网格 + 重生成按钮
+7. **预览面板组件** — Markdown/HTML 双栏预览
+8. **导出按钮组** — 3个导出格式按钮
+
+### 4.2 第二层:好用(符合 GW02 的交互原则)
+
+基于 GW02 的分析,公众号创作专员的交互应该遵循:
+
+> **对话发起 + 结构化决策接管 + 工作流可见性**
+
+具体改进:
+
+**a) 创建任务时,对话式启动**
+
+用户不说"我要创建一个公众号创作专员任务,业务域是AI,关键词是智能体",而是直接说:
+
+> "帮我围绕智能体趋势写一篇文章"
+
+系统自动识别业务域 = AI,关键词 = 智能体,弹出表单确认:
+
+```
+确认以下设置:
+业务域:AI / 智能体(自动识别)
+关键词:智能体
+受众:公众号读者(默认,可改)
+目标:输出一篇可发布的公众号文章(默认,可改)
+语气:专业但好懂(默认,可改)
+正文字数:1400字(默认,可改)
+
+[确认] [修改]
+```
+
+**b) 选题决策用卡片,不是列表**
+
+GW02 的核心判断:候选项在消息流中容易被淹没。
+
+```
+┌─────────────────────────────────────────────────────┐
+│ 已抓取 42 条热点,AI 帮你选出了 3 个选题 │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ 🔥 爆热 │ AI Agent 正在替代客服:真的还是炒作? │ │
+│ │ │ 角度:热点追踪 · 推荐理由:今日有3家大厂│ │
+│ │ │ 宣布客服转型 Agent │ │
+│ │ │ 热度:████████░░ 12分 │ │
+│ └─────────────────────────────────────────────────┘ │
+│ [ 选择这个 ] [ 再看 ] │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ ⚡ 高 │ 中小企业为什么不敢做 AI Agent 转型? │ │
+│ │ │ 角度:产业动向 · 推荐理由:痛点明确, │ │
+│ │ │ 读者共鸣度高 │ │
+│ │ │ 热度:███████░░░ 8分 │ │
+│ └─────────────────────────────────────────────────┘ │
+│ [ 选择这个 ] [ 再看 ] │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ 🔸 中 │ Agent 开发工具链盘点:谁在跑在前面? │ │
+│ │ │ 角度:技术盘点 · 推荐理由:工具盘点类 │ │
+│ │ │ 内容长期引流效果好 │ │
+│ │ │ 热度:████░░░░░░ 5分 │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+│ [ 都不要,我自己写一个 ] [ 刷新热点 ] │
+└─────────────────────────────────────────────────────┘
+```
+
+**c) 正文编辑允许对话微调**
+
+用户在编辑区改完后,可以在侧边对话中说:
+
+> "第三段太技术了,换成更通俗的说法"
+> "加一段关于 Agent 安全风险的讨论"
+> "结尾要更有力,给一个行动号召"
+
+**d) 底部面板始终可见工作流状态**
+
+```
+┌─────────────────────────────────────────────┐
+│ 时间线:热点抓取完成(09:20) → 选题确认(09:25) │
+│ 回放节点:1 2 3 4 5 6 7 8 │
+│ 追踪:plugin.hotspot.fetch() → plugin.ai.topic() │
+│ 日志:[INFO] 抓取42条热点 → [INFO] 过滤6条 │
+└─────────────────────────────────────────────┘
+```
+
+### 4.3 第三层:好用(差异化体验)
+
+**a) 业务域模板**
+
+每个业务域(AI/医疗/保险/HR/法务/通用)提供预设模板:
+
+| 业务域 | 推荐受众 | 推荐语气 | 推荐字数 | 推荐提纲风格 |
+|--------|----------|----------|----------|-------------|
+| AI/智能体 | 技术从业者 | 专业但好懂 | 1400 | 问题拆解型 |
+| 医疗/健康 | 普通用户 | 通俗温暖 | 1200 | 场景故事型 |
+| 保险/金融 | 投资者/消费者 | 客观严谨 | 1600 | 数据对比型 |
+| HR/招聘 | HR从业者 | 实用专业 | 1300 | 方法流程型 |
+| 法务/合规 | 法务从业者 | 严谨准确 | 1500 | 条款分析型 |
+| 通用 | 公众号读者 | 专业但好懂 | 1400 | 问题拆解型 |
+
+**b) 热点趋势趋势面板**
+
+展示该业务域最近7天的热点趋势图:
+
+```
+热点趋势(近7天)
+[████████] 周一 32条
+[███████░] 周二 28条
+[█████░░░] 周三 21条
+[████████] 周四 35条 ← 今天
+[█████░░░] 周五 22条
+```
+
+**c) 选题热度预测**
+
+AI 不仅给热度标签(爆热/高/中高/中),还给出预估阅读量范围:
+
+```
+选题1:AI Agent 正在替代客服
+ 热度:爆热 · 预估阅读:5000-15000
+选题2:中小企业 AI 转型
+ 热度:高 · 预估阅读:3000-8000
+```
+
+**d) 多版本草稿对比**
+
+支持并行生成多版本:
+
+```
+版本 A(选定):智能体替代客服
+版本 B(草稿):中小企业 AI 转型
+版本 C(草稿):Agent 工具链盘点
+
+[ A ] [ B ] [ C ] [+ 新增版本]
+```
+
+---
+
+## 5. 已知 Bug
+
+### 5.1 `isOfficialAccountStep` 缺少 `page_markdown` case
+
+后端工作流引擎的 `isOfficialAccountStep` 函数(约第 919-924 行)的 switch 语句中缺少对 `officialAccountStepKeyPageMD` 的检查,而 `officialAccountNextStep` 函数(约第 950-968 行)会将其作为 `preview_export` 的下一步。这意味着执行分页 MD 步骤时会返回 "工作流步骤不存在" 错误。
+
+**修复方法:** 在 `isOfficialAccountStep` 的 switch 中添加:
+
+```go
+case officialAccountStepKeyPageMD:
+ return true
+```
+
+---
+
+## 6. 前端实现方案
+
+### 6.1 目录结构
+
+```
+frontend/src/specialists/packages/wechat-official-account/
+├── manifest.js ← 已存在,不需改
+├── views/
+│ ├── OfficialAccountCreatePage.vue ← 创建任务页
+│ ├── OfficialAccountWorkbenchPage.vue ← 工作流主页面(核心)
+│ └── OfficialAccountPreviewPage.vue ← 预览页(可选,可嵌入 Workbench)
+├── components/
+│ ├── WorkflowStepNav.vue ← 步骤导航(9步进度条)
+│ ├── TopicCandidateCard.vue ← 选题候选卡片
+│ ├── TitleCandidateCard.vue ← 标题候选卡片
+│ ├── OutlineEditor.vue ← 提纲编辑器
+│ ├── ContentEditor.vue ← 正文编辑器
+│ ├── ImagePromptList.vue ← 配图提示词管理
+│ ├── ImageGallery.vue ← 图片预览网格
+│ ├── PreviewPanel.vue ← 预览面板
+│ └── BottomPanel.vue ← 底部面板
+├── composables/
+│ ├── useOfficialAccountWorkflow.js ← 工作流状态管理
+│ └── useOfficialAccountExport.js ← 导出逻辑
+└── api/
+ └── officialAccount.js ← 已存在,不需改
+```
+
+### 6.2 核心交互流程伪代码
+
+```javascript
+// OfficialAccountWorkbenchPage.vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 6.3 创建任务流程
+
+```javascript
+// 1. 用户从专员市场或通用助手点击"公众号创作专员"
+// 2. 跳转到 /apps/wechat-official-account
+// 3. 展示创建表单(或对话式启动)
+// 4. 填写表单 → 创建任务 → 跳转到工作流页
+
+// 表单提交
+const task = await createOfficialAccountTask({
+ business_domain: 'ai',
+ keyword: '智能体',
+ audience: '技术从业者',
+ goal: '输出一篇可发布的公众号文章',
+ tone: '专业但好懂',
+ content_target_words: 1400,
+ max_content_images: 3,
+})
+
+// 5. 自动执行 Step 1(热点抓取 + 选题生成)
+await fetchWorkflow(task.id)
+// 返回的 workflow.steps['topic_recommendation'].status = 'pending'
+// workflow.shared.topicCandidates = [候选1, 候选2, 候选3]
+```
+
+### 6.4 步骤执行流程
+
+```javascript
+// 用户选择选题后,自动进入下一步
+async function handleSelectTopic(topic) {
+ // 保存选题
+ await updateOfficialAccountTask(taskId, { selected_topic: topic })
+ // 执行下一步:标题生成
+ await executeStep(taskId, 'title_generation', { topic })
+ // 刷新工作流
+ await fetchWorkflow(taskId)
+}
+
+// 每个步骤的交互模式:
+// - 有候选项:展示卡片 → 用户选一个 → 自动进入下一步
+// - 有文本内容:展示编辑器 → 用户编辑 → 点击确认 → 自动进入下一步
+// - 有图片:展示预览 → 用户查看 → 可重生成 → 自动进入下一步
+// - 最后一步:展示预览 + 导出按钮 → 完成
+```
+
+---
+
+## 7. 工作量评估(按优先级)
+
+### P0 — 核心可用(必须做)
+
+| 组件 | 预估工作量 | 说明 |
+|------|-----------|------|
+| 创建表单 | 1天 | 业务域、关键词、受众、目标、语气、字数 |
+| 路由配置 | 0.5天 | `/apps/wechat-official-account` 路由 |
+| 步骤导航 | 1天 | 9步进度条,状态指示 |
+| 选题卡片 | 1.5天 | 候选展示、选择、刷新热点 |
+| 标题卡片 | 0.5天 | 3个标题候选、选择 |
+| 提纲编辑器 | 1天 | Markdown 编辑器、确认 |
+| 正文编辑器 | 1天 | Markdown 编辑器、确认 |
+| 配图提示词 | 1天 | 提示词列表、编辑、确认 |
+| 图片预览 | 1天 | 图片网格、预览、重生成 |
+| 预览面板 | 1天 | Markdown/HTML 双栏预览 |
+| 导出功能 | 0.5天 | 3种格式下载 |
+| **合计** | **~10天** | 一个前端开发可以完成 |
+
+### P1 — 提升体验(建议做)
+
+| 组件 | 预估工作量 | 说明 |
+|------|-----------|------|
+| 对话式启动 | 1.5天 | 自然语言解析业务域/关键词 |
+| 底部面板 | 1天 | timeline / replay / trace / logs |
+| 对话微调 | 1天 | 编辑区侧边对话、局部修改 |
+| 业务域模板 | 0.5天 | 预设模板选择 |
+| **合计** | **~4天** | 在 P0 基础上 |
+
+### P2 — 差异化功能(可选)
+
+| 组件 | 预估工作量 | 说明 |
+|------|-----------|------|
+| 热点趋势图 | 1天 | 折线图展示热点趋势 |
+| 选题热度预测 | 1天 | 预估阅读量范围 |
+| 多版本草稿 | 2天 | 并行生成、对比切换 |
+| **合计** | **~4天** | 在 P1 基础上 |
+
+---
+
+## 8. 总结
+
+公众号创作专员是平台中**后端实现最完整**的专员——从工作流引擎、热点抓取、AI生成、图片生成、预览导出到DB持久化全部到位。唯一缺失的是前端 UI 层。
+
+要让它"真正可用",核心工作就是做一个完整的工作流页面。这个页面的设计原则应该遵循 GW02 的结论:
+
+1. **启动用对话/表单** — 用户输入目标,系统自动补全结构
+2. **选择用卡片** — 热点选题、标题都是多候选场景,卡片并列展示
+3. **编辑用文本区** — 提纲、正文允许用户自由修改
+4. **预览用双栏** — Markdown 编辑 + HTML 实时预览
+5. **状态永远可见** — 步骤导航 + 底部面板
+
+预计 **10天核心实现 + 4天体验优化 + 4天差异化功能**,一个有经验的 Vue 前端工程师可以独立完成。
diff --git a/docs/20_Global_Workbench_Evolution/GW05_公众号创作专员与对话框交互兼容分析.md b/docs/20_Global_Workbench_Evolution/GW05_公众号创作专员与对话框交互兼容分析.md
new file mode 100644
index 0000000..54adba9
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/GW05_公众号创作专员与对话框交互兼容分析.md
@@ -0,0 +1,485 @@
+# GW05_公众号创作专员与对话框交互兼容分析
+
+> 状态:专题分析
+> 最后更新:2026-09-23
+> 关联文档:
+> - `GW02_对话式AI工作台与传统UI分界判断.md`
+> - `GW04_公众号创作专员可用性改进分析.md`
+
+---
+
+## 1. 问题定义
+
+公众号创作专员的工作流 UI(选题卡片、标题选择、正文编辑、配图管理、预览导出)如何与当前的**对话框布局**兼容?
+
+当前架构的核心矛盾:
+
+- **对话框是主交互面**:用户通过 SmartAssistantPage 的 ChatLayout 发起一切
+- **工作流是右栏被动面板**:SpecialistPanel 只展示已完成的步骤,不提供交互式步骤执行
+- **公众号创作专员需要完整的前端交互**:选题选择、标题选择、文本编辑、配图管理等操作需要主动输入
+
+所以问题不是"怎么加一个页面",而是:**这个工作流交互应该长在对话框上,还是长在对话框旁边?**
+
+---
+
+## 2. 当前架构分析
+
+### 2.1 SmartAssistantPage 布局
+
+```
+┌────────────────────────────────────────────────────────────────────┐
+│ ChatLayout(全屏对话布局) │
+│ ┌──────────┬──────────────────────────────────────────────────────┐│
+│ │ 左侧 │ 中间区域 ││
+│ │ 任务列表 │ ┌────────────────────────────────────────────────┐ ││
+│ │ │ │ 消息流 │ ││
+│ │ 任务A │ │ 用户:围绕AI趋势写一篇公众号文章 │ ││
+│ │ 任务B │ │ 助手:好的,我帮你起草 │ ││
+│ │ 任务C │ │ · 热点抓取 → 选题推荐 → 标题生成 │ ││
+│ │ │ │ · 3 个选题候选 │ ││
+│ │ │ │ 🔥 选题1:AI Agent 正在替代客服 · 爆热 │ ││
+│ │ │ │ ⚡ 选题2:中小企业 AI 转型 · 高 │ ││
+│ │ │ │ 🔸 选题3:Agent 工具链盘点 · 中 │ ││
+│ │ │ │ 请选择一个选题 │ ││
+│ │ │ ├────────────────────────────────────────────────┤ ││
+│ │ 右侧面板 │ │ 输入框 + 技能芯片 + 发送按钮 │ ││
+│ │ │ └────────────────────────────────────────────────┘ ││
+│ │ SpecialistPanel │ ││
+│ │ ┌──────────────────────────┐ │ ││
+│ │ │ 工作流 上传物与产物 │ │ ││
+│ │ ├──────────────────────────┤ │ ││
+│ │ │ 1. ✓ 更新任务配置 │ │ ││
+│ │ │ 2. ✓ 推荐热点选题 │ │ ││
+│ │ │ 3. 4. 标题/提纲生成 │ │ ││
+│ │ │ 5. 正文创作 │ │ ││
+│ │ │ 6. 7. 配图生成 │ │ ││
+│ │ │ 8. 预览导出 │ │ ││
+│ │ │ 9. 分页 MD │ │ ││
+│ │ └──────────────────────────┘ │ ││
+│ └──────────┴──────────────────────────────────────────────────────┘│
+└────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 SpecialistPanel 的现状
+
+SpecialistPanel 是一个**只读面板**,它的职责是:
+
+1. 从 `action_records_json` 解析已执行的步骤(后端工作流引擎每执行一步就写入此字段)
+2. 从 `task_artifact` 读取步骤产物
+3. 展示步骤进度(`doneCount / steps.length`)
+4. 点击产物卡片跳到产物 tab
+
+**它不做什么:**
+
+- 不提供步骤执行按钮(执行由后端自动推进)
+- 不提供交互选择(选题选择、标题选择等)
+- 不提供文本编辑(提纲编辑、正文编辑)
+- 不提供配图管理
+
+### 2.3 当前工作流推进方式
+
+当前工作流(包括公众号创作专员)的推进方式是:
+
+1. 用户在对话框输入"帮我写一篇公众号文章"
+2. 后端解析出要创建公众号创作专员任务
+3. 后端自动执行所有工作流步骤(自动抓取热点、自动选题、自动标题、自动正文……)
+4. 每一步完成后在对话消息中展示产出
+5. 用户被动阅读
+
+**没有交互选择的余地。** 用户在对话框里只能看、不能选。
+
+### 2.4 现有交互模式的例外
+
+目前唯一的"结构化选择"发生在**技能选择**时:
+
+- 用户在技能面板选择一个技能(如"公文辅助写作")
+- 技能面板展示技能的输入表单
+- 用户填写后点击"执行"
+
+但这种交互发生在**任务创建之前**(选择要用的技能),而不是任务执行中间。
+
+---
+
+## 3. 公众号创作专员需要什么样的交互?
+
+根据 GW02 的分析,公众号创作专员有多个**多候选决策点**:
+
+| 步骤 | 决策类型 | 候选数 | 对话适合度 |
+|------|---------|--------|-----------|
+| 选题推荐 | 候选选择 | 3-5 个 | 差:消息流中候选容易淹没 |
+| 标题选择 | 候选选择 | 3 个 | 差:同上 |
+| 提纲编辑 | 文本修改 | 一个 | 中:对话可以说"改第三段"但不够精确 |
+| 正文编辑 | 文本修改 | 一个 | 中:同上 |
+| 配图提示词 | 编辑管理 | 3-5 个 | 差:需要看到所有提示词的完整内容 |
+| 图片生成 | 预览选择 | 3-5 张 | 中:对话里能看缩略图但不够清晰 |
+| 预览导出 | 查看导出 | 一个 | 好:直接下载即可 |
+
+**结论:公众号创作专员的工作流中有 4 个决策点(选题、标题、提纲、配图)不适合纯对话交互,需要结构化 UI 接管。**
+
+---
+
+## 4. 兼容方案
+
+### 方案 A:扩展 SpecialistPanel(推荐)
+
+**思路:** 不新增页面,在右侧 SpecialistPanel 中嵌入交互式步骤组件。
+
+```
+┌────────────────────────────────────────────────────────────────────┐
+│ 对话区域(不变) │
+│ ┌──────────┬──────────────────────────────────────────────────────┐│
+│ │ 左侧 │ 中间区域 ││
+│ │ 任务列表 │ 消息流(同上) ││
+│ └──────────┴──────────────────────────────────────────────────────┘│
+│ ┌─────────────────────────────────────────────────────────────────┐│
+│ │ 右侧面板(扩展为交互式) ││
+│ │ ┌──────────────────────────────────────────────────────────┐ ││
+│ │ │ 工作流 产物 预览 │ 公众号创作专员 ││ ││
+│ │ ├──────────────────────────────────────────────────────────┤ ││
+│ │ │ [1] ✓ 热点抓取 │ ││
+│ │ │ [2] ✓ 选题推荐 │ ││
+│ │ │ [3] ⚡ 标题生成(进行中) │ ││
+│ │ │ [4] ○ 提纲生成 │ ││
+│ │ │ [5] ○ 正文创作 │ ││
+│ │ │ [6] ○ 配图生成 │ ││
+│ │ │ [7] ○ 预览导出 │ ││
+│ │ ├──────────────────────────────────────────────────────────┤ ││
+│ │ │ │ ││
+│ │ │ 当前步骤:标题生成 │ ││
+│ │ │ ┌────────────────────────────────────────────────────┐ │ ││
+│ │ │ │ 📰 基于选题:"AI Agent 正在替代客服" │ │ ││
+│ │ │ │ │ │ ││
+│ │ │ │ ① 客服革命还是噱头? │ │ ││
+│ │ │ │ 热度:爆热 · 点击选择 │ │ ││
+│ │ │ │ │ │ ││
+│ │ │ │ ② 中小企业不敢做 AI 客服的三个原因 │ │ ││
+│ │ │ │ 热度:高 · 点击选择 │ │ ││
+│ │ │ │ │ │ ││
+│ │ │ │ ③ 2026 客服 Agent 行业盘点 │ │ ││
+│ │ │ │ 热度:中 · 点击选择 │ │ ││
+│ │ │ │ │ │ ││
+│ │ │ │ [ 重新生成 ] [ 我不要这些,换个热点 ] │ │ ││
+│ │ │ └────────────────────────────────────────────────────┘ │ ││
+│ │ │ │ ││
+│ │ │ 底部面板: │ ││
+│ │ │ 时间线 / 回放 / 追踪 / 日志 │ ││
+│ │ │ │ ││
+│ │ └──────────────────────────────────────────────────────────┘ ││
+│ └─────────────────────────────────────────────────────────────────┘│
+└────────────────────────────────────────────────────────────────────┘
+```
+
+**优点:**
+- 不破坏现有的对话架构
+- 与现有 SpecialistPanel 的只读模式自然融合——新增"交互模式"
+- 用户始终在同一页面内操作,不需要跳转
+- 对话框和交互面板并行展示,信息不冲突
+- 最小改动,复用现有的 SpecialistPanel 组件结构
+- 对话消息和工作流步骤的状态同步已有基础(通过 `taskRuntime.loadTaskDetail`)
+
+**缺点:**
+- 面板宽度有限,复杂的编辑区可能放不下
+- 需要大幅扩展 SpecialistPanel 的渲染逻辑
+
+**实施建议:**
+- SpecialistPanel 根据 `currentStep` 动态渲染不同组件
+- 对话消息中展示工作流摘要
+- 面板中展示完整交互
+- 两个区域的同步通过 `fetchOfficialAccountWorkflow` API + 定时刷新
+
+---
+
+### 方案 B:独立工作台页面
+
+**思路:** 用户从对话框点击"用公众号创作专员处理"后,跳转到 `/apps/wechat-official-account` 独立页面。
+
+```
+┌────────────────────────────────────────────────────────────────────┐
+│ 独立工作页面 │
+│ ┌─────────────────────────────────────────────────────────────────┐│
+│ │ 公众号创作专员工作台 ││
+│ │ ┌──────────┬──────────────────────────────────────────────────┐││
+│ │ │ 步骤导航 │ 主交互区 │││
+│ │ │ 1. ✓ │ 选题候选卡片(完整版) │││
+│ │ │ 2. ✓ │ [ 选择这个 ] [ 刷新热点 ] [ 换选题 ] │││
+│ │ │ 3. ⚡ │ │││
+│ │ │ 4. ○ │ ┌────────────────────────────────────────────┐ │││
+│ │ │ 5. ○ │ │ 编辑区:基于选题写提纲 │ │││
+│ │ │ 6. ○ │ │ · 第一段:AI 客服的现状 │ │││
+│ │ │ 7. ○ │ │ · 第二段:中小企业的顾虑 │ │││
+│ │ │ 8. ○ │ │ · 第三段:未来趋势 │ │││
+│ │ │ │ │ │ │ │ · 第四段:行业预测 │ │││
+│ │ │ │ │ │ │ └────────────────────────────────────────────┘ │││
+│ │ │ │ │ │ │ [ 保存草稿 ] [ 确认 ] [ 重新生成 ] │││
+│ │ │ │ │ │ │ │││
+│ │ │ └──────────┴──────────────────────────────────────────────────┘││
+│ │ │ ┌──────────────────────────────────────────────────────────┐ │││
+│ │ │ │ 底部面板:时间线 / 回放 / 追踪 / 日志 │ │││
+│ │ │ └──────────────────────────────────────────────────────────┘ │││
+│ └─────────────────────────────────────────────────────────────────┘│
+└────────────────────────────────────────────────────────────────────┘
+```
+
+**优点:**
+- 工作流交互可以完整展示,不受面板宽度限制
+- 适合复杂编辑场景(正文编辑、配图管理)
+- 与"工作流页面"的概念更匹配
+
+**缺点:**
+- 用户从对话页面跳转到工作台页面,上下文不连续
+- 对话消息中的上下文(上传的文件、上传的图片、前文)需要重新获取
+- 用户可能不知道工作台页面的存在(对话框是唯一的入口心智)
+- 与现有"对话即工作台"的产品定位冲突
+
+---
+
+### 方案 C:对话内嵌卡片 + 侧栏扩展(混合模式)
+
+**思路:** 结合方案 A 和 B 的优点。
+
+**场景 1:简单决策(选题、标题)**
+
+在对话消息中直接展示卡片组件,用户直接在对话消息里选择:
+
+```
+助手:已生成 3 个选题候选,请选择:
+
+┌────────────────────────────────────────┐
+│ 🔥 爆热 │ AI Agent 正在替代客服 │
+│ │ 角度:热点追踪 │
+│ │ 热度:████████░░ 12分 │
+│ │ [ 选择这个 ] [ 不看 ] │
+└────────────────────────────────────────┘
+┌────────────────────────────────────────┐
+│ ⚡ 高 │ 中小企业 AI 转型的三个原因 │
+│ │ 角度:产业动向 │
+│ │ 热度:███████░░░ 8分 │
+│ │ [ 选择这个 ] [ 不看 ] │
+└────────────────────────────────────────┘
+```
+
+选择后自动执行下一步,新的卡片出现在对话流中。
+
+**场景 2:复杂编辑(正文、配图)**
+
+当工作流进入正文创作阶段,在 SpecialistPanel 中自动切换到"编辑模式":
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 右侧面板自动切换为编辑模式 ││
+│ ┌──────────────────────────────────────────────────────────┐ ││
+│ │ 编辑区(正文创作) │ ││
+│ │ ┌────────────────────────────────────────────────────┐ │ ││
+│ │ │ # AI 客服的现状 │ │ ││
+│ │ │ │ │ ││
+│ │ │ 2026 年,越来越多的企业开始尝试用 AI 替代人工客服 │ │ ││
+│ │ │ ... │ │ ││
+│ │ │ │ │ ││
+│ │ │ [ 粗体 ] [ 斜体 ] [ 链接 ] [ 插入图片 ] [ Markdown ]│ │ ││
+│ │ └────────────────────────────────────────────────────┘ │ ││
+│ │ [ 保存草稿 ] [ 确认并继续 ] [ 对话微调 ] │ ││
+│ │ │ ││
+│ │ 对话微调面板(可选展开) │ ││
+│ │ ┌────────────────────────────────────────────────────┐ │ ││
+│ │ │ 输入修改建议:第三段太技术了,换成更通俗的说法 │ │ ││
+│ │ │ [ 执行 ] │ │ ││
+│ │ └────────────────────────────────────────────────────┘ │ ││
+│ └──────────────────────────────────────────────────────────┘ ││
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**优点:**
+- 简单决策在对话中直接完成,不打断流程
+- 复杂编辑在工作台区域完成,有足够空间
+- 对话和工作台并行,上下文不断开
+- 符合 GW02 的"对话发起 + 结构化决策接管"原则
+
+**缺点:**
+- 实现复杂度最高
+- 需要设计对话消息中卡片组件与面板编辑组件的切换逻辑
+- 需要在 SpecialistPanel 中嵌入多种不同的组件
+
+---
+
+## 5. 推荐方案与演进路径
+
+### 阶段 1:方案 A 为基础(快速可用)
+
+**目标:10 天内让公众号创作专员核心工作流可用。**
+
+实现方式:
+1. 在 SpecialistPanel 中新增"交互渲染区"
+2. 根据 `currentStep` 动态渲染对应组件
+3. 简单步骤(选题、标题)在小面板内展示卡片
+4. 复杂步骤(正文编辑)在面板内展示精简编辑器
+5. 预览导出直接展示结果
+
+这是最省力的路径,与现有代码改动最小。
+
+### 阶段 2:对话消息内嵌卡片(提升体验)
+
+**目标:让用户在对话框中就能看到工作流产出,不切到面板。**
+
+实现方式:
+1. 工作流引擎每完成一个步骤,自动在对话消息中展示结果卡片
+2. 选题卡片直接渲染在对话消息中(`msg.skillResultCard` 机制复用)
+3. 用户在对话消息中直接点击选择
+4. 选择后自动触发下一步,新的卡片出现在消息流中
+
+这需要:
+- 工作流引擎在完成每一步时自动向对话消息推送结果
+- 对话消息支持可交互卡片(不只是展示)
+
+### 阶段 3:完整独立工作台(可选)
+
+当公众号创作专员的使用量增长后,如果方案 A/B 的空间不够用(比如需要并排查看热点趋势图、选题对比、多版本草稿),再考虑完整独立工作台页面。
+
+这个页面应该是"对话 + 工作台"的双栏布局:
+- 左侧:对话流(作为上下文入口和交互补充)
+- 右侧:工作流面板(完整编辑区)
+- 底部:状态栏(时间线、回放、追踪)
+
+---
+
+## 6. 关键设计原则
+
+综合 GW02 的结论,公众号创作专员的交互设计必须遵循:
+
+### 6.1 对话不代替 UI,对话引导 UI
+
+对话的作用是:
+- 告诉系统"要做什么"("围绕 AI 趋势写文章")
+- 在编辑阶段进行局部修改("第三段太技术")
+
+对话**不应该**用来:
+- 展示所有选题候选让用户体验
+- 逐个询问标题选择
+- 展示配图效果
+
+### 6.2 结构化决策必须有 UI 空间
+
+所有需要**比较、选择、编辑**的操作必须在一个稳定的 UI 区域内完成:
+- 选题卡片:并排展示,一目了然
+- 标题选择:三个并排,点击即可
+- 正文编辑:完整的文本编辑区
+- 配图管理:网格布局,点击放大
+
+### 6.3 可见性层始终在线
+
+用户需要始终知道:
+- 当前走到哪一步了(步骤导航)
+- 已完成什么(步骤列表标记)
+- 下一步是什么(自动高亮当前步骤)
+- 产出是什么(产物 tab)
+
+### 6.4 对话与 UI 的同步
+
+- 对话消息中展示工作流摘要
+- 工作流 UI 中展示对话上下文
+- 用户在对话中的选择应该实时反映在工作流 UI 中
+- 用户在工作流 UI 中的操作应该在对话中留下记录
+
+---
+
+## 7. 具体实现建议
+
+### 7.1 SpecialistPanel 改造
+
+现有 SpecialistPanel 的 `tab` 结构已经可以扩展为"交互模式":
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
...
+
+
+
+
+
+
...
+
+
+
+```
+
+### 7.2 对话消息中的可交互卡片
+
+复用现有的 `msg.skillResultCard` 机制:
+
+```javascript
+// 工作流引擎完成选题步骤后,在对话消息中自动插入可交互卡片
+messages.value.push({
+ role: 'assistant',
+ content: '已生成 3 个选题候选,请选择:',
+ skillResult: { topicCandidates: [...] },
+ skillResultCard: markRaw(TopicCandidateCard), // 可交互的 Vue 组件
+ timestamp: new Date().toLocaleTimeString(),
+})
+```
+
+当用户点击卡片中的"选择这个"时:
+1. 调用 `executeOfficialAccountWorkflowStep(stepKey, { topic: selectedTopic })`
+2. 工作流自动推进到下一步
+3. 新的结果卡片出现在消息流中
+
+### 7.3 数据流
+
+```
+用户操作(对话 / 面板)
+ ↓
+调用专用 API(create / update / executeStep)
+ ↓
+后端工作流引擎执行
+ ↓
+更新 action_records_json + task_artifact
+ ↓
+前端轮询 / SSE / WebSocket 刷新
+ ↓
+更新 SpecialistPanel 状态
+ ↓
+如有需要,向对话消息中插入结果卡片
+```
+
+---
+
+## 8. 总结
+
+公众号创作专员与对话框的兼容,核心不在于"有没有独立页面",而在于**如何把结构化决策自然地嵌入对话工作流**。
+
+**推荐路径:**
+1. 先实现 SpecialistPanel 扩展(方案 A),让工作流可交互
+2. 再实现对话消息中嵌入卡片(方案 C 的部分),让用户不切面板也能操作
+3. 如果确实需要更大编辑空间,再补充完整独立工作台页面
+
+**关键原则:**
+- 对话引导,UI 执行
+- 决策点用卡片,编辑区用面板
+- 可见性层始终在线
+- 对话与 UI 双向同步
+
+这不是"对话还是 UI"的二选一,而是**对话和 UI 各司其职的协作系统**。
diff --git a/docs/20_Global_Workbench_Evolution/目录说明.md b/docs/20_Global_Workbench_Evolution/目录说明.md
new file mode 100644
index 0000000..159c1be
--- /dev/null
+++ b/docs/20_Global_Workbench_Evolution/目录说明.md
@@ -0,0 +1,41 @@
+# 20_Global_Workbench_Evolution 目录说明
+
+本目录专门讨论全球主流软件工作台形态的历史演进,不承担当前系统架构合同职责,也不直接替代 `02_Architecture` 中的正式架构定义。
+
+本目录的作用有三项:
+
+1. 为 EAI 的交互与工作台设计提供跨时代参照系
+2. 把“对话式 AI 工作台”和传统 GUI、SaaS、协同工作台放在同一条历史线上比较
+3. 为后续 Workbench、专员工作流、技能运行面、xApp 承接面提供外部样本库
+
+## 当前文件
+
+- `GW01_全球主流软件工作台历史演进.md`
+ - 总时间线文档
+ - 讨论从 CLI / GUI 到 SaaS / 协同工作台,再到 Copilot / Agent Workbench 的连续演进
+
+- `GW02_对话式AI工作台与传统UI分界判断.md`
+ - 专题判断文档
+ - 讨论"对话发起 + 结构化接管 + 工作流可见性"为什么会成为新一代 AI 工作台的主交互模式
+
+- `GW03_专员工作交互形式分析.md`
+ - 专题分析文档
+ - 对平台中所有专员(通用助手、公众号创作专员、合同审查专员、售前方案专员、客户跟进专员、简历筛选与招聘专员、汇报专员等)逐项分析其启动方式、决策方式、执行方式、反馈方式
+ - 给出每个专员"对话 vs 结构化 UI"的比例判断和交互设计建议
+
+- `GW04_公众号创作专员可用性改进分析.md`
+ - 专题分析文档
+ - 分析公众号创作专员后端 100% 已实现、前端约 40% 的现状
+ - 给出完整的前端工作流 UI 实现方案、交互设计、组件结构和工作量评估
+
+- `GW05_公众号创作专员与对话框交互兼容分析.md`
+ - 专题分析文档
+ - 分析公众号创作专员工作流 UI 与当前对话框架构的兼容关系
+ - 对比三种方案:扩展 SpecialistPanel、独立工作台页面、对话内嵌卡片 + 侧栏扩展
+ - 给出分阶段实施路径:先让 SpecialistPanel 可交互 → 再在对话消息中嵌入卡片 → 可选完整独立页
+
+## 使用原则
+
+1. 本目录优先记录跨产品、跨代际、跨品类的演进判断
+2. 当前 EAI 的正式架构结论,仍以 `02_Architecture` 中的正式文档为准
+3. 当研究判断足够稳定时,可回写到 `02_Architecture` 形成正式合同
diff --git a/docs/目录说明.md b/docs/目录说明.md
index 422ea52..bcc7e69 100644
--- a/docs/目录说明.md
+++ b/docs/目录说明.md
@@ -15,6 +15,7 @@
- `06_Product_Lines`:商务资料、正式产品路线与历史 PRD
- `08_Design_Rules`:设计规则与交互约定
- `09_Research`:研究判断、平台对标、项目比较与外部观察
+- `20_Global_Workbench_Evolution`:全球主流软件工作台形态演进研究,对话式 AI 工作台与传统 UI 分界判断,专员工作交互形式分析
## 当前组织原则
@@ -31,3 +32,4 @@
- `05_Object_Catalog` 负责系统对象清单、对象详情与对象边界
- `06_Product_Lines` 只保留业务材料和正式产品路线,不再承担正式对象定义职责
- `09_Research` 保留研究判断,不直接承担现行架构定义职责
+- `20_Global_Workbench_Evolution` 负责跨代际工作台演进研究,为架构与交互判断提供长期外部参照
diff --git a/eai_agentplatform/backend-go/config/ai_config.json b/eai_agentplatform/backend-go/config/ai_config.json
index fa2ca0d..15cf7ef 100644
--- a/eai_agentplatform/backend-go/config/ai_config.json
+++ b/eai_agentplatform/backend-go/config/ai_config.json
@@ -1 +1,172 @@
-{"agent_routes": {"embed_gen": "embed_route_openrouter_text_v3", "image_gen": "image_route_openrouter_gpt_image_2", "path_coach": "chat_route_openrouter_deepseek_v4_flash", "title_gen": "chat_route_openrouter_deepseek_v4_flash"}, "chat_routes": {"chat_route_ollama_qwen": {"description": "本地 Ollama Qwen2.5:7b", "endpoint": "/chat/completions", "max_tokens": 2048, "model": "qwen2.5:7b", "provider": "ollama", "short_model_name": "Qwen", "short_route_name": "本地", "temperature": 0.7}, "chat_route_ollama_qwen_fast": {"description": "本地快速对话 (Ollama Qwen2.5:7b 轻量)", "endpoint": "/chat/completions", "max_tokens": 512, "model": "qwen2.5:7b", "provider": "ollama", "short_model_name": "Qwen", "short_route_name": "快聊", "temperature": 0.3}, "chat_route_openrouter_deepseek_v3": {"description": "DeepSeek V3.2 (OpenRouter)", "endpoint": "/chat/completions", "max_tokens": 4096, "model": "deepseek/deepseek-v3.2", "provider": "openrouter", "short_model_name": "DSV3", "short_route_name": "均衡", "temperature": 0.6}, "chat_route_openrouter_deepseek_v4_flash": {"description": "DeepSeek V4 Flash (OpenRouter)", "endpoint": "/chat/completions", "max_tokens": 4096, "model": "deepseek/deepseek-v4-flash", "provider": "openrouter", "short_model_name": "DS4F", "short_route_name": "默认", "temperature": 0.7}, "chat_route_openrouter_gpt_5": {"description": "GPT 5.4 (OpenRouter)", "endpoint": "/chat/completions", "max_tokens": 8192, "model": "openai/gpt-5.4", "provider": "openrouter", "short_model_name": "GPT5", "short_route_name": "全能", "temperature": 0.7}, "chat_route_openrouter_grok_4": {"description": "Grok 4.20 (OpenRouter)", "endpoint": "/chat/completions", "max_tokens": 4096, "model": "x-ai/grok-4.20", "provider": "openrouter", "short_model_name": "Grok4", "short_route_name": "推理", "temperature": 0.6}}, "default_embed_route": "embed_route_openrouter_text_v3", "default_route": "chat_route_openrouter_deepseek_v4_flash", "description": "eai_agentplatform AI 路由配置 — 按场景分类定义多条路由,支持 provider/模型切换", "embed_routes": {"embed_route_ollama_bge_m3": {"description": "本地 Ollama bge-m3", "endpoint": "/embeddings", "model": "bge-m3", "provider": "ollama", "short_model_name": "BGE", "short_route_name": "本地"}, "embed_route_openrouter_text_v3": {"description": "OpenAI text-embedding-3-small (OpenRouter)", "endpoint": "/embeddings", "model": "openai/text-embedding-3-small", "provider": "openrouter", "short_model_name": "Emb3", "short_route_name": "默认"}}, "fallback_routes": {"chat_route_openrouter_deepseek_v3": ["chat_route_openrouter_grok_4", "chat_route_ollama_qwen"], "chat_route_openrouter_deepseek_v4_flash": ["chat_route_openrouter_deepseek_v3", "chat_route_openrouter_grok_4", "chat_route_ollama_qwen"], "chat_route_openrouter_grok_4": ["chat_route_ollama_qwen"], "embed_route_ollama_bge_m3": ["embed_route_openrouter_text_v3"], "embed_route_openrouter_text_v3": ["embed_route_ollama_bge_m3"]}, "image_routes": {"image_route_ollama_llava": {"description": "本地 Ollama LLaVA", "endpoint": "/chat/completions", "max_tokens": 2048, "model": "llava", "provider": "ollama", "short_model_name": "LLaVA", "short_route_name": "本地", "temperature": 0.7}, "image_route_openrouter_flux": {"description": "FLUX 1.1 Pro (OpenRouter)", "endpoint": "/images/generations", "max_tokens": 1024, "model": "black-forest-labs/flux-1.1-pro", "provider": "openrouter", "short_model_name": "FLUX", "short_route_name": "绘图", "temperature": 1}, "image_route_openrouter_gpt_image_2": {"description": "GPT 5.4 Image 2 (OpenRouter)", "endpoint": "/images/generations", "max_tokens": 4096, "model": "openai/gpt-5.4-image-2", "provider": "openrouter", "short_model_name": "G图2", "short_route_name": "默认", "temperature": 1}}, "version": "2.1.0"}
+{
+ "agent_routes": {
+ "embed_gen": "embed_route_openrouter_text_v3",
+ "image_gen": "image_route_openrouter_gpt_image_2",
+ "path_coach": "chat_route_lmuai_deepseek_flash",
+ "title_gen": "chat_route_lmuai_deepseek_flash"
+ },
+ "chat_routes": {
+ "chat_route_lmuai_deepseek_flash": {
+ "description": "LMUAI / DeepSeek Flash",
+ "endpoint": "/chat/completions",
+ "max_tokens": 4096,
+ "model": "deepseek-flash",
+ "provider": "lmuai",
+ "short_model_name": "DeepSeek Flash",
+ "short_route_name": "LMUAI",
+ "temperature": 0.7
+ },
+ "chat_route_siliconflow_deepseek_v4_flash": {
+ "description": "SiliconFlow / DeepSeek V4 Flash",
+ "endpoint": "/chat/completions",
+ "max_tokens": 4096,
+ "model": "deepseek-ai/DeepSeek-V4-Flash",
+ "provider": "siliconflow",
+ "short_model_name": "DeepSeek V4 Flash",
+ "short_route_name": "SiliconFlow",
+ "temperature": 0.7
+ },
+ "chat_route_ollama_qwen": {
+ "description": "本地对话 · llama.cpp / Qwen3.5 4B",
+ "endpoint": "/chat/completions",
+ "max_tokens": 2048,
+ "model": "Qwen3.5-4B-Q4_K_M.gguf",
+ "provider": "llamacpp",
+ "short_model_name": "Qwen3.5 4B",
+ "short_route_name": "本地对话",
+ "temperature": 0.7
+ },
+ "chat_route_ollama_qwen_fast": {
+ "description": "本地快聊 · llama.cpp / Qwen3.5 4B",
+ "endpoint": "/chat/completions",
+ "max_tokens": 512,
+ "model": "Qwen3.5-4B-Q4_K_M.gguf",
+ "provider": "llamacpp",
+ "short_model_name": "Qwen3.5 4B",
+ "short_route_name": "本地快聊",
+ "temperature": 0.3
+ },
+ "chat_route_openrouter_deepseek_v3": {
+ "description": "OpenRouter / DeepSeek V3.2",
+ "endpoint": "/chat/completions",
+ "max_tokens": 4096,
+ "model": "deepseek/deepseek-v3.2",
+ "provider": "openrouter",
+ "short_model_name": "DeepSeek V3.2",
+ "short_route_name": "OpenRouter",
+ "temperature": 0.6
+ },
+ "chat_route_openrouter_deepseek_v4_flash": {
+ "description": "OpenRouter / DeepSeek V4 Flash 0731",
+ "endpoint": "/chat/completions",
+ "max_tokens": 4096,
+ "model": "deepseek/deepseek-v4-flash-0731",
+ "provider": "openrouter",
+ "short_model_name": "DeepSeek V4 Flash 0731",
+ "short_route_name": "OpenRouter",
+ "temperature": 0.7
+ },
+ "chat_route_openrouter_gpt_5": {
+ "description": "OpenRouter / GPT 5.4",
+ "endpoint": "/chat/completions",
+ "max_tokens": 8192,
+ "model": "openai/gpt-5.4",
+ "provider": "openrouter",
+ "short_model_name": "GPT 5.4",
+ "short_route_name": "OpenRouter",
+ "temperature": 0.7
+ },
+ "chat_route_openrouter_grok_4": {
+ "description": "OpenRouter / Grok 4.20",
+ "endpoint": "/chat/completions",
+ "max_tokens": 4096,
+ "model": "x-ai/grok-4.20",
+ "provider": "openrouter",
+ "short_model_name": "Grok 4.20",
+ "short_route_name": "OpenRouter",
+ "temperature": 0.6
+ }
+ },
+ "default_embed_route": "embed_route_openrouter_text_v3",
+ "default_route": "chat_route_lmuai_deepseek_flash",
+ "description": "eai_agentplatform AI 路由配置 — 按场景分类定义多条路由,支持 provider/模型切换",
+ "embed_routes": {
+ "embed_route_ollama_bge_m3": {
+ "description": "本地向量 · Ollama / BGE-M3",
+ "endpoint": "/embeddings",
+ "model": "bge-m3",
+ "provider": "ollama",
+ "short_model_name": "BGE-M3",
+ "short_route_name": "本地向量"
+ },
+ "embed_route_openrouter_text_v3": {
+ "description": "OpenRouter / text-embedding-3-small",
+ "endpoint": "/embeddings",
+ "model": "openai/text-embedding-3-small",
+ "provider": "openrouter",
+ "short_model_name": "text-embedding-3-small",
+ "short_route_name": "OpenRouter"
+ }
+ },
+ "fallback_routes": {
+ "chat_route_lmuai_deepseek_flash": [
+ "chat_route_ollama_qwen_fast",
+ "chat_route_ollama_qwen"
+ ],
+ "chat_route_siliconflow_deepseek_v4_flash": [
+ "chat_route_ollama_qwen_fast",
+ "chat_route_ollama_qwen"
+ ],
+ "chat_route_openrouter_deepseek_v3": [
+ "chat_route_openrouter_grok_4",
+ "chat_route_ollama_qwen"
+ ],
+ "chat_route_openrouter_deepseek_v4_flash": [
+ "chat_route_openrouter_deepseek_v3",
+ "chat_route_openrouter_grok_4",
+ "chat_route_ollama_qwen"
+ ],
+ "chat_route_openrouter_grok_4": [
+ "chat_route_ollama_qwen"
+ ],
+ "embed_route_ollama_bge_m3": [
+ "embed_route_openrouter_text_v3"
+ ],
+ "embed_route_openrouter_text_v3": [
+ "embed_route_ollama_bge_m3"
+ ]
+ },
+ "image_routes": {
+ "image_route_ollama_llava": {
+ "description": "本地图像 · Ollama / LLaVA",
+ "endpoint": "/chat/completions",
+ "max_tokens": 2048,
+ "model": "llava",
+ "provider": "ollama",
+ "short_model_name": "LLaVA",
+ "short_route_name": "本地图像",
+ "temperature": 0.7
+ },
+ "image_route_openrouter_flux": {
+ "description": "OpenRouter / FLUX 1.1 Pro",
+ "endpoint": "/images/generations",
+ "max_tokens": 1024,
+ "model": "black-forest-labs/flux-1.1-pro",
+ "provider": "openrouter",
+ "short_model_name": "FLUX 1.1 Pro",
+ "short_route_name": "OpenRouter",
+ "temperature": 1
+ },
+ "image_route_openrouter_gpt_image_2": {
+ "description": "OpenRouter / GPT 5.4 Image 2",
+ "endpoint": "/images/generations",
+ "max_tokens": 4096,
+ "model": "openai/gpt-5.4-image-2",
+ "provider": "openrouter",
+ "short_model_name": "GPT 5.4 Image 2",
+ "short_route_name": "OpenRouter",
+ "temperature": 1
+ }
+ },
+ "version": "2.1.0"
+}
diff --git a/eai_agentplatform/backend-go/config/ai_secrets.example.json b/eai_agentplatform/backend-go/config/ai_secrets.example.json
index 4f94f91..408ece8 100644
--- a/eai_agentplatform/backend-go/config/ai_secrets.example.json
+++ b/eai_agentplatform/backend-go/config/ai_secrets.example.json
@@ -1,6 +1,7 @@
{
"VECTORENGINE_API_KEY": "sk-...",
"OPENROUTER_API_KEY": "sk-or-...",
+ "SILICONFLOW_API_KEY": "sk-...",
"VOLCES_API_KEY": "",
"LMUAI_API_KEY": "sk-...",
"ALIYUN_API_KEY": "sk-...",
@@ -9,4 +10,4 @@
"ANTHROPIC_API_KEY": "",
"OPENAI_API_KEY": "",
"BRAVE_SEARCH_API_KEY": "BSA..."
-}
\ No newline at end of file
+}
diff --git a/eai_agentplatform/backend-go/deploy/eai_agentplatform.env b/eai_agentplatform/backend-go/deploy/eai_agentplatform.env
index 08f4103..f2ec404 100644
--- a/eai_agentplatform/backend-go/deploy/eai_agentplatform.env
+++ b/eai_agentplatform/backend-go/deploy/eai_agentplatform.env
@@ -12,9 +12,9 @@ JWT_SECRET=__CHANGE_ME__GENERATE_48_CHARS_RANDOM__
JWT_EXPIRE_MINUTES=480
# 内网 LLM(OpenAI 兼容接口)
-LLM_BASE_URL=http://127.0.0.1:11434/v1
-LLM_API_KEY=ollama
-LLM_MODEL=qwen2.5:7b
+LLM_BASE_URL=http://127.0.0.1:8080/v1
+LLM_API_KEY=local
+LLM_MODEL=Qwen3.5-4B-Q4_K_M.gguf
EMBED_MODEL=bge-m3
# 数据目录
diff --git a/eai_agentplatform/backend-go/internal/api/project.go b/eai_agentplatform/backend-go/internal/api/project.go
index babd844..bea8b03 100644
--- a/eai_agentplatform/backend-go/internal/api/project.go
+++ b/eai_agentplatform/backend-go/internal/api/project.go
@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
+ "eai_agentplatform/backend/internal/dal"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
specialistruntime "eai_agentplatform/backend/internal/specialists/runtime"
@@ -89,6 +90,10 @@ func CreateProject(c *gin.Context) {
web.Fail(c, web.NewNotFoundError(err.Error()))
return
}
+ if err := validateSkillKeys(req.SkillKeys); err != nil {
+ web.Fail(c, web.NewNotFoundError(err.Error()))
+ return
+ }
instruction := ""
if req.Instruction != nil {
@@ -163,6 +168,10 @@ func UpdateProject(c *gin.Context) {
project.SpecialistKeys = encodeKeys(req.SpecialistKeys)
}
if req.SkillKeys != nil {
+ if err := validateSkillKeys(req.SkillKeys); err != nil {
+ web.Fail(c, web.NewNotFoundError(err.Error()))
+ return
+ }
project.SkillKeys = encodeKeys(req.SkillKeys)
}
if req.ConnectorKeys != nil {
@@ -246,3 +255,17 @@ func validateSpecialistKeys(keys []string) error {
}
return nil
}
+
+func validateSkillKeys(keys []string) error {
+ dao := dal.SkillDefinitionDAO{}
+ for _, key := range keys {
+ trimmed := strings.TrimSpace(key)
+ if trimmed == "" {
+ continue
+ }
+ if _, found := dao.GetByKey(trimmed); !found {
+ return errors.New("技能不存在:" + trimmed)
+ }
+ }
+ return nil
+}
diff --git a/eai_agentplatform/backend-go/internal/api/router.go b/eai_agentplatform/backend-go/internal/api/router.go
index ebce57c..4479158 100644
--- a/eai_agentplatform/backend-go/internal/api/router.go
+++ b/eai_agentplatform/backend-go/internal/api/router.go
@@ -11,8 +11,8 @@ import (
"eai_agentplatform/backend/internal/middleware"
skillapi "eai_agentplatform/backend/internal/skills/api"
specialistapi "eai_agentplatform/backend/internal/specialists/api"
- "eai_agentplatform/backend/internal/specialists/runtime"
wechatofficialaccountapi "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/api"
+ "eai_agentplatform/backend/internal/specialists/runtime"
xappapi "eai_agentplatform/backend/internal/xapps/api"
)
@@ -206,12 +206,15 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
admin.DELETE("/positions/:id", DeletePosition)
admin.POST("/specialists", specialistapi.CreateSpecialist)
admin.PUT("/specialists/:id", specialistapi.UpdateSpecialist)
+ admin.GET("/specialists/:id/remove-plan", specialistapi.PreviewSpecialistRemovePlan)
admin.DELETE("/specialists/:id", specialistapi.DeleteSpecialist)
admin.POST("/skills", skillapi.CreateSkillDefinition)
admin.PUT("/skills/:id", skillapi.UpdateSkillDefinition)
+ admin.GET("/skills/:id/remove-plan", skillapi.PreviewSkillRemovePlan)
admin.DELETE("/skills/:id", skillapi.DeleteSkillDefinition)
admin.POST("/xapps", xappapi.CreateXAppDefinition)
admin.PUT("/xapps/:id", xappapi.UpdateXAppDefinition)
+ admin.GET("/xapps/:id/remove-plan", xappapi.PreviewXAppRemovePlan)
admin.DELETE("/xapps/:id", xappapi.DeleteXAppDefinition)
admin.POST("/actions", CreateActionDefinition)
admin.PUT("/actions/:id", UpdateActionDefinition)
diff --git a/eai_agentplatform/backend-go/internal/api/specialist_prompt.go b/eai_agentplatform/backend-go/internal/api/specialist_prompt.go
index 88fa595..411d72f 100644
--- a/eai_agentplatform/backend-go/internal/api/specialist_prompt.go
+++ b/eai_agentplatform/backend-go/internal/api/specialist_prompt.go
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
- skillcore "eai_agentplatform/backend/internal/skills/core"
+ specialistcore "eai_agentplatform/backend/internal/specialists/core"
specialistmodel "eai_agentplatform/backend/internal/specialists/model"
)
@@ -63,7 +63,8 @@ func resolveSpecialist(req ChatMessageRequest) *specialistmodel.Specialist {
// 专员为 nil 或什么都没配时返回空串,调用方不做任何拼接——
// 这样「没选专员」与「选了但没配说明书」都不改变原有行为。
func specialistPromptSection(s *specialistmodel.Specialist) string {
- if s == nil {
+ ctx := specialistcore.PromptContextFor(s)
+ if ctx.Key == "" {
return ""
}
@@ -71,27 +72,27 @@ func specialistPromptSection(s *specialistmodel.Specialist) string {
// 身份
b.WriteString("【当前专员】\n")
- label := strings.TrimSpace(s.Label)
+ label := ctx.Label
if label == "" {
- label = s.Key
+ label = ctx.Key
}
- fmt.Fprintf(&b, "%s(%s)\n", label, s.Key)
- if summary := strings.TrimSpace(s.Summary); summary != "" {
+ fmt.Fprintf(&b, "%s(%s)\n", label, ctx.Key)
+ if summary := ctx.Summary; summary != "" {
b.WriteString(summary + "\n")
}
- if workStatus := strings.TrimSpace(s.WorkStatus); workStatus != "" {
+ if workStatus := ctx.WorkStatus; workStatus != "" {
b.WriteString("当前状态:" + workStatus + "\n")
}
// 可用技能
- if skills := skillcore.ParseAllowedSkills(s.AllowedSkills); len(skills) > 0 {
+ if len(ctx.AllowedSkills) > 0 {
b.WriteString("\n【可用技能】\n")
- b.WriteString(strings.Join(skills, "、"))
+ b.WriteString(strings.Join(ctx.AllowedSkills, "、"))
b.WriteString("\n首个是主技能。只在这些技能范围内承诺能力,范围外的需求如实说做不了。\n")
}
// 岗位说明书
- if rule := strings.TrimSpace(s.RuleFileMarkdown); rule != "" {
+ if rule := ctx.RuleFileMarkdown; rule != "" {
b.WriteString("\n【岗位说明书】\n")
b.WriteString(rule)
b.WriteString("\n")
diff --git a/eai_agentplatform/backend-go/internal/api/specialist_prompt_test.go b/eai_agentplatform/backend-go/internal/api/specialist_prompt_test.go
index 6e16254..1af1d0c 100644
--- a/eai_agentplatform/backend-go/internal/api/specialist_prompt_test.go
+++ b/eai_agentplatform/backend-go/internal/api/specialist_prompt_test.go
@@ -133,7 +133,7 @@ func setupAPITestDB(t *testing.T) {
for _, s := range []specialistmodel.Specialist{
{Key: "contract-review", Label: "合同审查专员", State: "active", Tier: "industry", ObjectEntryRoute: "/apps/contract-review"},
- {Key: "report-generation", Label: "报告生成专员", State: "active", Tier: "generic", ObjectEntryRoute: "/apps/report-generation"},
+ {Key: "wechat-official-account", Label: "公众号创作专员", State: "active", Tier: "generic", ObjectEntryRoute: "/apps/wechat-official-account"},
{Key: "retired-one", Label: "已下线专员", State: "inactive", Tier: "generic", ObjectEntryRoute: "/apps/retired"},
{Key: "general-assistant", Label: "通用助手", State: "system", Tier: "generic", ObjectEntryRoute: "/home"},
} {
@@ -168,7 +168,7 @@ func TestResolveSpecialistTaskWinsOverRequest(t *testing.T) {
t.Fatalf("建任务失败: %v", err)
}
- got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "report-generation"})
+ got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "wechat-official-account"})
if got == nil || got.Key != "contract-review" {
t.Fatalf("任务上的专员应优先,实际 %v", got)
}
@@ -178,8 +178,8 @@ func TestResolveSpecialistTaskWinsOverRequest(t *testing.T) {
func TestResolveSpecialistFallsBackToRequest(t *testing.T) {
setupAPITestDB(t)
- got := resolveSpecialist(ChatMessageRequest{SpecialistKey: "report-generation"})
- if got == nil || got.Key != "report-generation" {
+ got := resolveSpecialist(ChatMessageRequest{SpecialistKey: "wechat-official-account"})
+ if got == nil || got.Key != "wechat-official-account" {
t.Fatalf("应回退到请求里的 specialist_key,实际 %v", got)
}
}
@@ -228,8 +228,8 @@ func TestResolveSpecialistTaskWithoutSpecialistFallsBack(t *testing.T) {
t.Fatalf("建任务失败: %v", err)
}
- got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "report-generation"})
- if got == nil || got.Key != "report-generation" {
+ got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "wechat-official-account"})
+ if got == nil || got.Key != "wechat-official-account" {
t.Fatalf("任务专员失效时应回退到请求里的 key,实际 %v", got)
}
}
diff --git a/eai_agentplatform/backend-go/internal/config/config.go b/eai_agentplatform/backend-go/internal/config/config.go
index 355f1c2..47c92c7 100644
--- a/eai_agentplatform/backend-go/internal/config/config.go
+++ b/eai_agentplatform/backend-go/internal/config/config.go
@@ -58,9 +58,9 @@ func Load() *Config {
JWTSecret: getenv("JWT_SECRET", "change-this-to-a-strong-secret-in-production"),
JWTExpireMin: getenvInt("JWT_EXPIRE_MINUTES", 480),
- LLMBaseURL: getenv("LLM_BASE_URL", "http://127.0.0.1:11434/v1"),
- LLMAPIKey: getenv("LLM_API_KEY", "ollama"),
- LLMModel: getenv("LLM_MODEL", "qwen2.5:7b"),
+ LLMBaseURL: getenv("LLM_BASE_URL", "http://127.0.0.1:8080/v1"),
+ LLMAPIKey: getenv("LLM_API_KEY", "local"),
+ LLMModel: getenv("LLM_MODEL", "Qwen3.5-4B-Q4_K_M.gguf"),
EmbedModel: getenv("EMBED_MODEL", "bge-m3"),
AssetRootDir: assetRootDir,
diff --git a/eai_agentplatform/backend-go/internal/config/json_loader.go b/eai_agentplatform/backend-go/internal/config/json_loader.go
index 9790444..92fe5aa 100644
--- a/eai_agentplatform/backend-go/internal/config/json_loader.go
+++ b/eai_agentplatform/backend-go/internal/config/json_loader.go
@@ -63,6 +63,7 @@ type AIConfig struct {
type AISecrets struct {
VECTORENGINE_API_KEY string `json:"VECTORENGINE_API_KEY,omitempty"`
OPENROUTER_API_KEY string `json:"OPENROUTER_API_KEY,omitempty"`
+ SILICONFLOW_API_KEY string `json:"SILICONFLOW_API_KEY,omitempty"`
VOLCES_API_KEY string `json:"VOLCES_API_KEY,omitempty"`
LMUAI_API_KEY string `json:"LMUAI_API_KEY,omitempty"`
ALIYUN_API_KEY string `json:"ALIYUN_API_KEY,omitempty"`
@@ -91,6 +92,7 @@ type PlatformConfig struct {
var ProviderSecretKey = map[string]string{
"vectorengine": "VECTORENGINE_API_KEY",
"openrouter": "OPENROUTER_API_KEY",
+ "siliconflow": "SILICONFLOW_API_KEY",
"volces": "VOLCES_API_KEY",
"lmuai": "LMUAI_API_KEY",
"aliyun": "ALIYUN_API_KEY",
@@ -104,7 +106,9 @@ var ProviderSecretKey = map[string]string{
// ProviderDefaultBaseURL provider 默认 base_url(当 secrets 未提供时)
var ProviderDefaultBaseURL = map[string]string{
"ollama": "http://127.0.0.1:11434/v1",
+ "llamacpp": "http://127.0.0.1:8080/v1",
"openrouter": "https://openrouter.ai/api/v1",
+ "siliconflow": "https://api.siliconflow.cn/v1",
"openai": "https://api.openai.com/v1",
"vectorengine": "https://api.vectorengine.ai/v1",
"volces": "https://ark.cn-beijing.volces.com/api/v3",
@@ -362,6 +366,8 @@ func getSecretByField(s *AISecrets, field string) string {
return s.VECTORENGINE_API_KEY
case "OPENROUTER_API_KEY":
return s.OPENROUTER_API_KEY
+ case "SILICONFLOW_API_KEY":
+ return s.SILICONFLOW_API_KEY
case "VOLCES_API_KEY":
return s.VOLCES_API_KEY
case "LMUAI_API_KEY":
diff --git a/eai_agentplatform/backend-go/internal/dal/product.go b/eai_agentplatform/backend-go/internal/dal/product.go
index 22d3d54..5a09d04 100644
--- a/eai_agentplatform/backend-go/internal/dal/product.go
+++ b/eai_agentplatform/backend-go/internal/dal/product.go
@@ -107,7 +107,7 @@ func (r ProductDAO) NamesByIDs(ids []uint) map[uint]string {
// ProductsByStatus 按状态筛选产品列表。
func (r ProductDAO) ProductsByStatus(status string, filter map[string]string) []model.Product {
q := r.Type(&model.Product{})
- if cat, ok := filter["category"]; ok {
+ if cat, ok := filter["category"]; ok && cat != "" {
q = q.Where("category = ?", cat)
}
switch status {
diff --git a/eai_agentplatform/backend-go/internal/objectrefs/runtime.go b/eai_agentplatform/backend-go/internal/objectrefs/runtime.go
new file mode 100644
index 0000000..84b339f
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/objectrefs/runtime.go
@@ -0,0 +1,209 @@
+package objectrefs
+
+import (
+ "encoding/json"
+ "sort"
+ "strings"
+
+ "eai_agentplatform/backend/internal/dal"
+ "eai_agentplatform/backend/internal/model"
+ specialistmodel "eai_agentplatform/backend/internal/specialists/model"
+ xappmodel "eai_agentplatform/backend/internal/xapps/model"
+)
+
+type SpecialistRuntimeRefs struct {
+ XAppKeys []string
+ ProjectIDs []uint
+ TaskCount int64
+ TaskRunCount int64
+ ArtifactCount int64
+ AICallCount int64
+}
+
+type SkillRuntimeRefs struct {
+ SpecialistKeys []string
+ XAppKeys []string
+ ProjectIDs []uint
+}
+
+type XAppRuntimeRefs struct {
+ FavoriteUserIDs []uint
+ RecentUserIDs []uint
+ CustomUserIDs []uint
+}
+
+func ParseKeyList(raw string) []string {
+ trimmed := strings.TrimSpace(raw)
+ if trimmed == "" {
+ return nil
+ }
+ var keys []string
+ if err := json.Unmarshal([]byte(trimmed), &keys); err != nil {
+ return nil
+ }
+ out := make([]string, 0, len(keys))
+ seen := make(map[string]bool, len(keys))
+ for _, key := range keys {
+ key = strings.TrimSpace(key)
+ if key == "" || seen[key] {
+ continue
+ }
+ seen[key] = true
+ out = append(out, key)
+ }
+ return out
+}
+
+func listProjectIDsByKeyField(field string, key string) []uint {
+ key = strings.TrimSpace(key)
+ if key == "" || dal.DB == nil {
+ return nil
+ }
+ var projects []model.Project
+ if err := dal.DB.Select("id, " + field).Find(&projects).Error; err != nil {
+ return nil
+ }
+ ids := make([]uint, 0)
+ for _, project := range projects {
+ var raw string
+ switch field {
+ case "specialist_keys":
+ raw = project.SpecialistKeys
+ case "skill_keys":
+ raw = project.SkillKeys
+ default:
+ continue
+ }
+ for _, item := range ParseKeyList(raw) {
+ if item == key {
+ ids = append(ids, project.ID)
+ break
+ }
+ }
+ }
+ sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
+ return ids
+}
+
+func listXAppKeysByField(field string, key string) []string {
+ key = strings.TrimSpace(key)
+ if key == "" || dal.DB == nil {
+ return nil
+ }
+ var items []xappmodel.XAppDefinition
+ if err := dal.DB.Select("key, "+field).Where(field+" = ?", key).Find(&items).Error; err != nil {
+ return nil
+ }
+ out := make([]string, 0, len(items))
+ for _, item := range items {
+ if trimmed := strings.TrimSpace(item.Key); trimmed != "" {
+ out = append(out, trimmed)
+ }
+ }
+ sort.Strings(out)
+ return out
+}
+
+func listSpecialistKeysBindingSkill(skillKey string) []string {
+ skillKey = strings.TrimSpace(skillKey)
+ if skillKey == "" || dal.DB == nil {
+ return nil
+ }
+ var items []specialistmodel.Specialist
+ if err := dal.DB.Select("key, allowed_skills").Find(&items).Error; err != nil {
+ return nil
+ }
+ out := make([]string, 0)
+ for _, item := range items {
+ for _, key := range ParseKeyList(item.AllowedSkills) {
+ if key == skillKey {
+ out = append(out, strings.TrimSpace(item.Key))
+ break
+ }
+ }
+ }
+ sort.Strings(out)
+ return out
+}
+
+func CollectSpecialistRuntimeRefs(key string) SpecialistRuntimeRefs {
+ key = strings.TrimSpace(key)
+ refs := SpecialistRuntimeRefs{
+ XAppKeys: listXAppKeysByField("specialist_key", key),
+ ProjectIDs: listProjectIDsByKeyField("specialist_keys", key),
+ }
+ if key == "" || dal.DB == nil {
+ return refs
+ }
+ dal.DB.Model(&model.TaskRecord{}).Where("specialist_key = ?", key).Count(&refs.TaskCount)
+ dal.DB.Model(&model.TaskRun{}).Where("specialist_key = ?", key).Count(&refs.TaskRunCount)
+ dal.DB.Model(&model.TaskArtifact{}).Where("specialist_key = ?", key).Count(&refs.ArtifactCount)
+ dal.DB.Model(&model.AiCallLog{}).Where("specialist_key = ?", key).Count(&refs.AICallCount)
+ return refs
+}
+
+func CollectSkillRuntimeRefs(key string) SkillRuntimeRefs {
+ key = strings.TrimSpace(key)
+ return SkillRuntimeRefs{
+ SpecialistKeys: listSpecialistKeysBindingSkill(key),
+ XAppKeys: listXAppKeysByField("skill_key", key),
+ ProjectIDs: listProjectIDsByKeyField("skill_keys", key),
+ }
+}
+
+func listUserIDsByXAppCenterField(field string, key string) []uint {
+ key = strings.TrimSpace(key)
+ if key == "" || dal.DB == nil {
+ return nil
+ }
+ var rows []xappmodel.UserXAppCenter
+ if err := dal.DB.Select("user_id, favorite_keys, recent_keys, custom_xapps").Find(&rows).Error; err != nil {
+ return nil
+ }
+ userIDs := make([]uint, 0)
+ for _, row := range rows {
+ var matched bool
+ switch field {
+ case "favorite_keys":
+ for _, item := range ParseKeyList(row.FavoriteKeys) {
+ if item == key {
+ matched = true
+ break
+ }
+ }
+ case "recent_keys":
+ for _, item := range ParseKeyList(row.RecentKeys) {
+ if item == key {
+ matched = true
+ break
+ }
+ }
+ case "custom_xapps":
+ var items []struct {
+ Key string `json:"key"`
+ }
+ if err := json.Unmarshal([]byte(strings.TrimSpace(row.CustomXApps)), &items); err == nil {
+ for _, item := range items {
+ if strings.TrimSpace(item.Key) == key {
+ matched = true
+ break
+ }
+ }
+ }
+ }
+ if matched {
+ userIDs = append(userIDs, row.UserID)
+ }
+ }
+ sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
+ return userIDs
+}
+
+func CollectXAppRuntimeRefs(key string) XAppRuntimeRefs {
+ key = strings.TrimSpace(key)
+ return XAppRuntimeRefs{
+ FavoriteUserIDs: listUserIDsByXAppCenterField("favorite_keys", key),
+ RecentUserIDs: listUserIDsByXAppCenterField("recent_keys", key),
+ CustomUserIDs: listUserIDsByXAppCenterField("custom_xapps", key),
+ }
+}
diff --git a/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go b/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go
index 236f141..420a700 100644
--- a/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go
+++ b/eai_agentplatform/backend-go/internal/skills/api/admin_handlers.go
@@ -6,6 +6,8 @@ import (
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/dal"
+ "eai_agentplatform/backend/internal/objectrefs"
+ skillcore "eai_agentplatform/backend/internal/skills/core"
skillmodel "eai_agentplatform/backend/internal/skills/model"
"eai_agentplatform/backend/internal/web"
)
@@ -117,6 +119,10 @@ func DeleteSkillDefinition(c *gin.Context) {
web.Fail(c, web.NewNotFoundError("技能定义不存在"))
return
}
+ if plan, ok := skillcore.BuildRemovePlanByKey(item.Key); ok && plan.Blocked {
+ web.Fail(c, web.NewConflictError("技能仍被专员、应用或项目引用,请先处理 remove-plan 中的关联项"))
+ return
+ }
if !skillDefinitionDAO.Delete(&item) {
web.Fail(c, web.NewBadRequest("删除技能定义失败"))
return
@@ -124,6 +130,52 @@ func DeleteSkillDefinition(c *gin.Context) {
web.OK(c, gin.H{"id": id, "deleted": true})
}
+// PreviewSkillRemovePlan GET /api/admin/skills/:id/remove-plan (admin)
+func PreviewSkillRemovePlan(c *gin.Context) {
+ id, ok := parseID(c, "id")
+ if !ok {
+ return
+ }
+
+ item, found := skillDefinitionDAO.GetByID(id)
+ if !found {
+ web.Fail(c, web.NewNotFoundError("技能定义不存在"))
+ return
+ }
+
+ if plan, ok := skillcore.BuildRemovePlanByKey(item.Key); ok {
+ web.OK(c, gin.H{
+ "id": item.ID,
+ "key": item.Key,
+ "label": item.Label,
+ "source": item.Source,
+ "state": item.State,
+ "is_builtin": true,
+ "plan": plan,
+ })
+ return
+ }
+
+ refs := objectrefs.CollectSkillRuntimeRefs(item.Key)
+ web.OK(c, gin.H{
+ "id": item.ID,
+ "key": item.Key,
+ "label": item.Label,
+ "source": item.Source,
+ "state": item.State,
+ "is_builtin": false,
+ "plan": gin.H{
+ "key": item.Key,
+ "label": item.Label,
+ "definition_keys": []string{},
+ "catalog_entries": []string{},
+ "policy": "",
+ "runtime_refs": refs,
+ "blocked": false,
+ },
+ })
+}
+
func parseID(c *gin.Context, name string) (uint, bool) {
id, err := strconv.ParseUint(c.Param(name), 10, 64)
if err != nil || id == 0 {
diff --git a/eai_agentplatform/backend-go/internal/skills/core/allowed_skills.go b/eai_agentplatform/backend-go/internal/skills/core/allowed_skills.go
index 7abd052..8d03ce6 100644
--- a/eai_agentplatform/backend-go/internal/skills/core/allowed_skills.go
+++ b/eai_agentplatform/backend-go/internal/skills/core/allowed_skills.go
@@ -5,11 +5,15 @@ import (
"fmt"
"sort"
"strings"
+
+ "eai_agentplatform/backend/internal/dal"
+ skillmodel "eai_agentplatform/backend/internal/skills/model"
)
-var ValidSkillKeys = buildValidSkillKeys()
+var ValidSkillKeys = buildBuiltinSkillKeys()
+var allowedSkillKeyProvider = buildAllowedSkillKeys
-func buildValidSkillKeys() map[string]bool {
+func buildBuiltinSkillKeys() map[string]bool {
keys := make(map[string]bool)
for _, manifest := range builtinRegistry.Manifests() {
if strings.TrimSpace(manifest.Key) == "" {
@@ -21,8 +25,31 @@ func buildValidSkillKeys() map[string]bool {
}
func SortedValidSkillKeys() []string {
- keys := make([]string, 0, len(ValidSkillKeys))
+ return sortedSkillKeys(ValidSkillKeys)
+}
+
+func buildAllowedSkillKeys() map[string]bool {
+ keys := make(map[string]bool, len(ValidSkillKeys))
for k := range ValidSkillKeys {
+ keys[k] = true
+ }
+ if dal.DB == nil {
+ return keys
+ }
+ if !dal.DB.Migrator().HasTable(&skillmodel.SkillDefinition{}) {
+ return keys
+ }
+ for _, item := range (dal.SkillDefinitionDAO{}).List("", nil) {
+ if key := strings.TrimSpace(item.Key); key != "" {
+ keys[key] = true
+ }
+ }
+ return keys
+}
+
+func sortedSkillKeys(set map[string]bool) []string {
+ keys := make([]string, 0, len(set))
+ for k := range set {
keys = append(keys, k)
}
sort.Strings(keys)
@@ -59,14 +86,16 @@ func MarshalAllowedSkills(keys []string) (string, error) {
}
func ValidateAllowedSkills(keys []string) error {
+ validKeys := allowedSkillKeyProvider()
+ sortedKeys := sortedSkillKeys(validKeys)
seen := make(map[string]bool, len(keys))
for _, k := range keys {
k = strings.TrimSpace(k)
if k == "" {
continue
}
- if !ValidSkillKeys[k] {
- return fmt.Errorf("技能 key %q 非法,可选值:%s", k, strings.Join(SortedValidSkillKeys(), " / "))
+ if !validKeys[k] {
+ return fmt.Errorf("技能 key %q 非法,可选值:%s", k, strings.Join(sortedKeys, " / "))
}
if seen[k] {
return fmt.Errorf("技能 key %q 重复绑定", k)
diff --git a/eai_agentplatform/backend-go/internal/skills/core/allowed_skills_test.go b/eai_agentplatform/backend-go/internal/skills/core/allowed_skills_test.go
index 7ead923..fe84217 100644
--- a/eai_agentplatform/backend-go/internal/skills/core/allowed_skills_test.go
+++ b/eai_agentplatform/backend-go/internal/skills/core/allowed_skills_test.go
@@ -110,6 +110,11 @@ func TestMarshalAllowedSkills(t *testing.T) {
}
func TestValidateAllowedSkills(t *testing.T) {
+ originalProvider := allowedSkillKeyProvider
+ t.Cleanup(func() {
+ allowedSkillKeyProvider = originalProvider
+ })
+
t.Run("合法列表通过", func(t *testing.T) {
if err := ValidateAllowedSkills([]string{"contract-review", "batch-extract"}); err != nil {
t.Errorf("不应报错: %v", err)
@@ -138,6 +143,21 @@ func TestValidateAllowedSkills(t *testing.T) {
t.Fatal("重复绑定应报错(首个为主技能的语义会变得不确定)")
}
})
+
+ t.Run("运行时技能定义可放宽内置白名单", func(t *testing.T) {
+ allowedSkillKeyProvider = func() map[string]bool {
+ return map[string]bool{
+ "contract-review": true,
+ "runtime-skill": true,
+ }
+ }
+ if err := ValidateAllowedSkills([]string{"runtime-skill"}); err != nil {
+ t.Fatalf("运行时技能应允许绑定: %v", err)
+ }
+ if ValidSkillKeys["runtime-skill"] {
+ t.Fatal("内置技能清单不应被运行时定义污染")
+ }
+ })
}
func readFrontendFile(t *testing.T, relPath string) []byte {
diff --git a/eai_agentplatform/backend-go/internal/skills/core/ownership.go b/eai_agentplatform/backend-go/internal/skills/core/ownership.go
new file mode 100644
index 0000000..dc91188
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/skills/core/ownership.go
@@ -0,0 +1,38 @@
+package skillcore
+
+import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
+
+type OwnershipInfo struct {
+ Key string
+ Label string
+ DefinitionKeys []string
+ CatalogEntries []string
+ Policy skillcontracts.SharedReferencePolicy
+}
+
+func OwnershipCatalog() map[string]OwnershipInfo {
+ items := make(map[string]OwnershipInfo)
+ for _, manifest := range builtinRegistry.Manifests() {
+ items[manifest.Key] = ownershipFromManifest(manifest)
+ }
+ return items
+}
+
+func OwnershipByKey(key string) (OwnershipInfo, bool) {
+ manifest, ok := builtinRegistry.ByKey(key)
+ if !ok {
+ return OwnershipInfo{}, false
+ }
+ return ownershipFromManifest(manifest), true
+}
+
+func ownershipFromManifest(manifest skillcontracts.Manifest) OwnershipInfo {
+ meta := manifest.UninstallMeta()
+ return OwnershipInfo{
+ Key: manifest.Key,
+ Label: manifest.Label,
+ DefinitionKeys: meta.OwnedDefinitionKeys,
+ CatalogEntries: meta.OwnedCatalogEntries,
+ Policy: meta.ReferencePolicy,
+ }
+}
diff --git a/eai_agentplatform/backend-go/internal/skills/core/uninstall.go b/eai_agentplatform/backend-go/internal/skills/core/uninstall.go
index 15593d8..524c943 100644
--- a/eai_agentplatform/backend-go/internal/skills/core/uninstall.go
+++ b/eai_agentplatform/backend-go/internal/skills/core/uninstall.go
@@ -1,6 +1,10 @@
package skillcore
-import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
+import (
+ skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
+
+ "eai_agentplatform/backend/internal/objectrefs"
+)
type UninstallPreview struct {
DefinitionKeys []string
@@ -8,6 +12,16 @@ type UninstallPreview struct {
Policy skillcontracts.SharedReferencePolicy
}
+type RemovePlan struct {
+ Key string
+ Label string
+ DefinitionKeys []string
+ CatalogEntries []string
+ Policy skillcontracts.SharedReferencePolicy
+ RuntimeRefs objectrefs.SkillRuntimeRefs
+ Blocked bool
+}
+
func BuildUninstallPreview(manifest skillcontracts.Manifest) UninstallPreview {
meta := manifest.UninstallMeta()
return UninstallPreview{
@@ -16,3 +30,22 @@ func BuildUninstallPreview(manifest skillcontracts.Manifest) UninstallPreview {
Policy: meta.ReferencePolicy,
}
}
+
+func BuildRemovePlanByKey(key string) (RemovePlan, bool) {
+ ownership, ok := OwnershipByKey(key)
+ if !ok {
+ return RemovePlan{}, false
+ }
+ refs := objectrefs.CollectSkillRuntimeRefs(key)
+ blocked := ownership.Policy == skillcontracts.SharedReferenceBlockUninstall &&
+ (len(refs.SpecialistKeys) > 0 || len(refs.XAppKeys) > 0 || len(refs.ProjectIDs) > 0)
+ return RemovePlan{
+ Key: ownership.Key,
+ Label: ownership.Label,
+ DefinitionKeys: ownership.DefinitionKeys,
+ CatalogEntries: ownership.CatalogEntries,
+ Policy: ownership.Policy,
+ RuntimeRefs: refs,
+ Blocked: blocked,
+ }, true
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/api/admin_handlers.go b/eai_agentplatform/backend-go/internal/specialists/api/admin_handlers.go
index ad6527a..cc19b00 100644
--- a/eai_agentplatform/backend-go/internal/specialists/api/admin_handlers.go
+++ b/eai_agentplatform/backend-go/internal/specialists/api/admin_handlers.go
@@ -2,10 +2,13 @@ package specialistapi
import (
"strconv"
+ "strings"
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/dal"
+ "eai_agentplatform/backend/internal/objectrefs"
+ specialistcore "eai_agentplatform/backend/internal/specialists/core"
specialistmodel "eai_agentplatform/backend/internal/specialists/model"
specialistruntime "eai_agentplatform/backend/internal/specialists/runtime"
"eai_agentplatform/backend/internal/web"
@@ -150,10 +153,19 @@ func DeleteSpecialist(c *gin.Context) {
if !ok {
return
}
- if _, found := specialistDAO.GetByID(id); !found {
+ item, found := specialistDAO.GetByID(id)
+ if !found {
web.Fail(c, web.NewNotFoundError("专员不存在"))
return
}
+ if strings.TrimSpace(item.Key) == "general-assistant" || strings.TrimSpace(item.State) == "system" {
+ web.Fail(c, web.NewConflictError("通用专员是系统保留专员,不能删除"))
+ return
+ }
+ if plan, ok := specialistcore.BuildRemovePlanByKey(item.Key); ok && plan.Blocked {
+ web.Fail(c, web.NewConflictError("专员仍被运行时对象引用,请先处理 remove-plan 中的关联项"))
+ return
+ }
if !specialistDAO.Delete(id) {
web.Fail(c, web.NewBadRequest("删除专员失败"))
return
@@ -161,6 +173,49 @@ func DeleteSpecialist(c *gin.Context) {
web.OK(c, gin.H{"id": id, "deleted": true})
}
+// PreviewSpecialistRemovePlan GET /api/admin/specialists/:id/remove-plan (admin)
+func PreviewSpecialistRemovePlan(c *gin.Context) {
+ id, ok := parseID(c, "id")
+ if !ok {
+ return
+ }
+ item, found := specialistDAO.GetByID(id)
+ if !found {
+ web.Fail(c, web.NewNotFoundError("专员不存在"))
+ return
+ }
+
+ if plan, ok := specialistcore.BuildRemovePlanByKey(item.Key); ok {
+ web.OK(c, gin.H{
+ "id": item.ID,
+ "key": item.Key,
+ "label": item.Label,
+ "state": item.State,
+ "is_builtin": true,
+ "plan": plan,
+ })
+ return
+ }
+
+ refs := objectrefs.CollectSpecialistRuntimeRefs(item.Key)
+ web.OK(c, gin.H{
+ "id": item.ID,
+ "key": item.Key,
+ "label": item.Label,
+ "state": item.State,
+ "is_builtin": false,
+ "plan": gin.H{
+ "key": item.Key,
+ "label": item.Label,
+ "definition_keys": []string{},
+ "catalog_entries": []string{},
+ "policy": "",
+ "runtime_refs": refs,
+ "blocked": false,
+ },
+ })
+}
+
func parseID(c *gin.Context, name string) (uint, bool) {
id, err := strconv.ParseUint(c.Param(name), 10, 64)
if err != nil || id == 0 {
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/apply.go b/eai_agentplatform/backend-go/internal/specialists/core/apply.go
index c9ded28..faec4fc 100644
--- a/eai_agentplatform/backend-go/internal/specialists/core/apply.go
+++ b/eai_agentplatform/backend-go/internal/specialists/core/apply.go
@@ -22,8 +22,8 @@ func RuleFileDrafts() map[string]string {
func SkillBindings() map[string][]string {
items := make(map[string][]string)
for _, manifest := range builtinRegistry.Manifests() {
- if len(manifest.AllowedSkills) > 0 {
- items[manifest.Key] = append([]string(nil), manifest.AllowedSkills...)
+ if skills := DefaultAllowedSkillsForKey(manifest.Key); len(skills) > 0 {
+ items[manifest.Key] = skills
}
}
return items
@@ -38,13 +38,14 @@ func ApplyRuleFiles(items []specialistmodel.Specialist) error {
if manifest.RuleFileMarkdown != "" {
items[i].RuleFileMarkdown = manifest.RuleFileMarkdown
}
- if len(manifest.AllowedSkills) == 0 {
+ skills := DefaultAllowedSkillsForKey(items[i].Key)
+ if len(skills) == 0 {
continue
}
- if err := skillcore.ValidateAllowedSkills(manifest.AllowedSkills); err != nil {
+ if err := skillcore.ValidateAllowedSkills(skills); err != nil {
return fmt.Errorf("专员 %s 的技能绑定非法: %w", items[i].Key, err)
}
- raw, err := skillcore.MarshalAllowedSkills(manifest.AllowedSkills)
+ raw, err := MarshalAllowedSkillBindingsFor(&items[i])
if err != nil {
return fmt.Errorf("专员 %s 的技能绑定序列化失败: %w", items[i].Key, err)
}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/binding_provider.go b/eai_agentplatform/backend-go/internal/specialists/core/binding_provider.go
new file mode 100644
index 0000000..6a9ee96
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/specialists/core/binding_provider.go
@@ -0,0 +1,37 @@
+package specialistcore
+
+import (
+ "strings"
+
+ skillcore "eai_agentplatform/backend/internal/skills/core"
+ specialistmodel "eai_agentplatform/backend/internal/specialists/model"
+)
+
+// DefaultAllowedSkillsForKey 返回包定义里的默认技能绑定。
+//
+// 它表达的是 specialist 包默认 own 的绑定,不看运行时数据库。
+func DefaultAllowedSkillsForKey(key string) []string {
+ manifest, ok := builtinRegistry.ByKey(strings.TrimSpace(key))
+ if !ok || len(manifest.AllowedSkills) == 0 {
+ return nil
+ }
+ return append([]string(nil), manifest.AllowedSkills...)
+}
+
+// AllowedSkillBindingsFor 返回专员当前有效的技能绑定。
+//
+// 运行时持久化层优先,包内 manifest 作为兜底默认值。
+func AllowedSkillBindingsFor(s *specialistmodel.Specialist) []string {
+ if s == nil {
+ return nil
+ }
+ if skills := skillcore.ParseAllowedSkills(s.AllowedSkills); len(skills) > 0 {
+ return skills
+ }
+ return DefaultAllowedSkillsForKey(s.Key)
+}
+
+// MarshalAllowedSkillBindingsFor 返回专员当前有效技能绑定的持久化表示。
+func MarshalAllowedSkillBindingsFor(s *specialistmodel.Specialist) (string, error) {
+ return skillcore.MarshalAllowedSkills(AllowedSkillBindingsFor(s))
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/binding_provider_test.go b/eai_agentplatform/backend-go/internal/specialists/core/binding_provider_test.go
new file mode 100644
index 0000000..92f3c37
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/specialists/core/binding_provider_test.go
@@ -0,0 +1,52 @@
+package specialistcore
+
+import (
+ "reflect"
+ "testing"
+
+ specialistmodel "eai_agentplatform/backend/internal/specialists/model"
+)
+
+func TestDefaultAllowedSkillsForKey(t *testing.T) {
+ got := DefaultAllowedSkillsForKey("contract-review")
+ want := []string{"contract-review", "contract-brief", "batch-extract"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("默认技能绑定不对,实际 %v,期望 %v", got, want)
+ }
+}
+
+func TestAllowedSkillBindingsForPrefersPersistedValues(t *testing.T) {
+ s := &specialistmodel.Specialist{
+ Key: "contract-review",
+ AllowedSkills: `["runtime-skill","contract-review"]`,
+ }
+
+ got := AllowedSkillBindingsFor(s)
+ want := []string{"runtime-skill", "contract-review"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("应优先使用数据库技能绑定,实际 %v,期望 %v", got, want)
+ }
+}
+
+func TestAllowedSkillBindingsForFallsBackToManifest(t *testing.T) {
+ s := &specialistmodel.Specialist{Key: "contract-review"}
+
+ got := AllowedSkillBindingsFor(s)
+ want := []string{"contract-review", "contract-brief", "batch-extract"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("数据库技能绑定为空时应回退 manifest,实际 %v,期望 %v", got, want)
+ }
+}
+
+func TestMarshalAllowedSkillBindingsFor(t *testing.T) {
+ s := &specialistmodel.Specialist{Key: "contract-review"}
+
+ got, err := MarshalAllowedSkillBindingsFor(s)
+ if err != nil {
+ t.Fatalf("序列化默认技能绑定失败: %v", err)
+ }
+ want := `["contract-review","contract-brief","batch-extract"]`
+ if got != want {
+ t.Fatalf("序列化结果不对,实际 %s,期望 %s", got, want)
+ }
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/ownership.go b/eai_agentplatform/backend-go/internal/specialists/core/ownership.go
new file mode 100644
index 0000000..803bda5
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/specialists/core/ownership.go
@@ -0,0 +1,38 @@
+package specialistcore
+
+import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
+
+type OwnershipInfo struct {
+ Key string
+ Label string
+ DefinitionKeys []string
+ CatalogEntries []string
+ Policy specialistcontracts.SharedReferencePolicy
+}
+
+func OwnershipCatalog() map[string]OwnershipInfo {
+ items := make(map[string]OwnershipInfo)
+ for _, manifest := range builtinRegistry.Manifests() {
+ items[manifest.Key] = ownershipFromManifest(manifest)
+ }
+ return items
+}
+
+func OwnershipByKey(key string) (OwnershipInfo, bool) {
+ manifest, ok := builtinRegistry.ByKey(key)
+ if !ok {
+ return OwnershipInfo{}, false
+ }
+ return ownershipFromManifest(manifest), true
+}
+
+func ownershipFromManifest(manifest specialistcontracts.Manifest) OwnershipInfo {
+ meta := manifest.UninstallMeta()
+ return OwnershipInfo{
+ Key: manifest.Key,
+ Label: manifest.Label,
+ DefinitionKeys: meta.OwnedDefinitionKeys,
+ CatalogEntries: meta.OwnedCatalogEntries,
+ Policy: meta.ReferencePolicy,
+ }
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider.go b/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider.go
new file mode 100644
index 0000000..eafd09b
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider.go
@@ -0,0 +1,52 @@
+package specialistcore
+
+import (
+ "strings"
+
+ specialistmodel "eai_agentplatform/backend/internal/specialists/model"
+)
+
+// PromptContext 是平台层真正需要的专员提示词上下文。
+//
+// 平台不必知道 rule_file_markdown / allowed_skills 落在哪个字段,
+// 只消费这里收口后的结果。
+type PromptContext struct {
+ Key string
+ Label string
+ Summary string
+ WorkStatus string
+ AllowedSkills []string
+ RuleFileMarkdown string
+}
+
+// PromptContextFor 返回专员当前有效的提示词上下文。
+//
+// 运行时持久化层优先,包内 manifest 作为兜底默认值:
+// 1. 管理员在线编辑后的岗位说明书 / 技能绑定以数据库为准
+// 2. 数据库为空时,再回退到内置专员 manifest
+func PromptContextFor(s *specialistmodel.Specialist) PromptContext {
+ if s == nil {
+ return PromptContext{}
+ }
+
+ ctx := PromptContext{
+ Key: strings.TrimSpace(s.Key),
+ Label: strings.TrimSpace(s.Label),
+ Summary: strings.TrimSpace(s.Summary),
+ WorkStatus: strings.TrimSpace(s.WorkStatus),
+ AllowedSkills: AllowedSkillBindingsFor(s),
+ RuleFileMarkdown: strings.TrimSpace(s.RuleFileMarkdown),
+ }
+
+ manifest, ok := builtinRegistry.ByKey(ctx.Key)
+ if !ok {
+ return ctx
+ }
+ if ctx.Label == "" {
+ ctx.Label = strings.TrimSpace(manifest.Label)
+ }
+ if ctx.RuleFileMarkdown == "" {
+ ctx.RuleFileMarkdown = strings.TrimSpace(manifest.RuleFileMarkdown)
+ }
+ return ctx
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider_test.go b/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider_test.go
new file mode 100644
index 0000000..1e1e2a9
--- /dev/null
+++ b/eai_agentplatform/backend-go/internal/specialists/core/prompt_provider_test.go
@@ -0,0 +1,58 @@
+package specialistcore
+
+import (
+ "reflect"
+ "testing"
+
+ specialistmodel "eai_agentplatform/backend/internal/specialists/model"
+)
+
+func TestPromptContextForPrefersPersistedValues(t *testing.T) {
+ s := &specialistmodel.Specialist{
+ Key: "contract-review",
+ Label: "合同审查专员(自定义)",
+ Summary: "线上编辑后的摘要",
+ WorkStatus: "2 份待复核",
+ AllowedSkills: `["runtime-skill","contract-review"]`,
+ RuleFileMarkdown: "# 自定义岗位说明书",
+ }
+
+ got := PromptContextFor(s)
+
+ if got.Label != "合同审查专员(自定义)" {
+ t.Fatalf("应优先使用数据库 label,实际 %q", got.Label)
+ }
+ if got.RuleFileMarkdown != "# 自定义岗位说明书" {
+ t.Fatalf("应优先使用数据库岗位说明书,实际 %q", got.RuleFileMarkdown)
+ }
+ wantSkills := []string{"runtime-skill", "contract-review"}
+ if !reflect.DeepEqual(got.AllowedSkills, wantSkills) {
+ t.Fatalf("应优先使用数据库技能绑定,实际 %v,期望 %v", got.AllowedSkills, wantSkills)
+ }
+}
+
+func TestPromptContextForFallsBackToManifest(t *testing.T) {
+ s := &specialistmodel.Specialist{
+ Key: "contract-review",
+ Label: "",
+ }
+
+ got := PromptContextFor(s)
+
+ if got.Label != "合同审查专员" {
+ t.Fatalf("数据库 label 为空时应回退 manifest,实际 %q", got.Label)
+ }
+ if got.RuleFileMarkdown == "" {
+ t.Fatal("数据库岗位说明书为空时应回退 manifest")
+ }
+ wantSkills := []string{"contract-review", "contract-brief", "batch-extract"}
+ if !reflect.DeepEqual(got.AllowedSkills, wantSkills) {
+ t.Fatalf("数据库技能绑定为空时应回退 manifest,实际 %v,期望 %v", got.AllowedSkills, wantSkills)
+ }
+}
+
+func TestPromptContextForNil(t *testing.T) {
+ if got := PromptContextFor(nil); !reflect.DeepEqual(got, PromptContext{}) {
+ t.Fatalf("nil 专员应返回空上下文,实际 %#v", got)
+ }
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/core/uninstall.go b/eai_agentplatform/backend-go/internal/specialists/core/uninstall.go
index 882e969..7178cff 100644
--- a/eai_agentplatform/backend-go/internal/specialists/core/uninstall.go
+++ b/eai_agentplatform/backend-go/internal/specialists/core/uninstall.go
@@ -1,6 +1,9 @@
package specialistcore
-import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
+import (
+ "eai_agentplatform/backend/internal/objectrefs"
+ specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
+)
type UninstallPreview struct {
DefinitionKeys []string
@@ -8,6 +11,16 @@ type UninstallPreview struct {
Policy specialistcontracts.SharedReferencePolicy
}
+type RemovePlan struct {
+ Key string
+ Label string
+ DefinitionKeys []string
+ CatalogEntries []string
+ Policy specialistcontracts.SharedReferencePolicy
+ RuntimeRefs objectrefs.SpecialistRuntimeRefs
+ Blocked bool
+}
+
func BuildUninstallPreview(manifest specialistcontracts.Manifest) UninstallPreview {
meta := manifest.UninstallMeta()
return UninstallPreview{
@@ -16,3 +29,22 @@ func BuildUninstallPreview(manifest specialistcontracts.Manifest) UninstallPrevi
Policy: meta.ReferencePolicy,
}
}
+
+func BuildRemovePlanByKey(key string) (RemovePlan, bool) {
+ ownership, ok := OwnershipByKey(key)
+ if !ok {
+ return RemovePlan{}, false
+ }
+ refs := objectrefs.CollectSpecialistRuntimeRefs(key)
+ blocked := ownership.Policy == specialistcontracts.SharedReferenceBlockUninstall &&
+ (len(refs.XAppKeys) > 0 || len(refs.ProjectIDs) > 0 || refs.TaskCount > 0 || refs.TaskRunCount > 0 || refs.ArtifactCount > 0 || refs.AICallCount > 0)
+ return RemovePlan{
+ Key: ownership.Key,
+ Label: ownership.Label,
+ DefinitionKeys: ownership.DefinitionKeys,
+ CatalogEntries: ownership.CatalogEntries,
+ Policy: ownership.Policy,
+ RuntimeRefs: refs,
+ Blocked: blocked,
+ }, true
+}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/knowledge_operations/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/knowledge_operations/manifest.go
deleted file mode 100644
index 87bbf4c..0000000
--- a/eai_agentplatform/backend-go/internal/specialists/packages/knowledge_operations/manifest.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package knowledgeoperations
-
-import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
-
-var Manifest = specialistcontracts.Manifest{
- Key: "knowledge-operations",
- Label: "知识运营专员",
- RuleFileMarkdown: `# 知识运营专员
-
-## 你是谁
-你负责知识资产的整理:对象映射、证据抽取、规则同步、发布检查。
-你的产出要能被人复核,所以每条结论都必须带回原文出处。
-
-## 你怎么推进
-1. 先确认这次处理的是哪份知识资产、目标对象字典是哪一版。
-2. 抽取时逐条给出「字段 / 取值 / 证据片段 / 出处位置」,拿不准的单独列成「待确认」。
-3. 收尾做发布检查:字段有没有缺、同一字段前后取值是否冲突、有没有遗留占位符。
-
-## 输出要求
-- 证据片段必须原文照抄,不要改写、不要概括。
-- 冲突项要给出两个取值各自的出处,让管理员自己判。
-
-## 边界
-- 不自作主张删改知识资产原文;发现该改的,列成建议交管理员。
-- 映射不确定时宁可标「待确认」,不要猜一个填上。`,
- AllowedSkills: []string{"batch-extract", "table-cleanup", "policy-rewrite"},
- OwnedDefinitionKeys: []string{"specialist.knowledge-operations.definition"},
- OwnedCatalogEntries: []string{"catalog.specialist.knowledge-operations"},
- ReferencePolicy: specialistcontracts.SharedReferenceReadonlyHistory,
-}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/presentation_briefing/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/presentation_briefing/manifest.go
deleted file mode 100644
index 930549c..0000000
--- a/eai_agentplatform/backend-go/internal/specialists/packages/presentation_briefing/manifest.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package presentationbriefing
-
-import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
-
-var Manifest = specialistcontracts.Manifest{
- Key: "presentation-briefing",
- Label: "汇报专员",
- RuleFileMarkdown: `# 汇报专员
-
-## 你是谁
-你负责把零散材料收成能讲、能交、能继续细化的汇报稿:经营汇报、方案汇报、培训课件、项目复盘。
-你面对的是马上要上台讲的人,他要的是一版结构清楚、页数合适、重点能讲明白的演示稿。
-
-## 你怎么推进
-1. 先确认汇报对象、场景和页数限制:给管理层、给客户、给培训学员,写法完全不同。
-2. 先出结构,不急着写满内容:按「开场结论 → 现状 / 问题 → 方案 / 进展 → 风险 / 下一步」组织。
-3. 每页只保留一层主结论,宁可留白,不要把 Word 直接搬进 PPT。
-4. 最后补讲稿备注:每页写清楚要讲什么、强调什么、哪些数字必须口径一致。
-
-## 输出要求
-- 先给页纲,再给每页要点,再补讲稿备注。
-- 所有数字、时间、范围要带口径;没有依据的地方写「待补」,不要自己补齐。
-- 如果页数明显超了,优先帮用户压缩,而不是机械地继续加页。
-
-## 边界
-- 不编造数据、案例、客户反馈。
-- 不把未确认方案写成既定事实。
-- 视觉美化可以建议,但当前重点是结构与表达,不承诺设计成品。`,
- AllowedSkills: []string{"ppt-generation", "mind-map", "progress-report"},
- OwnedDefinitionKeys: []string{"specialist.presentation-briefing.definition"},
- OwnedCatalogEntries: []string{"catalog.specialist.presentation-briefing"},
- ReferencePolicy: specialistcontracts.SharedReferenceReadonlyHistory,
-}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/process_coordination/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/process_coordination/manifest.go
deleted file mode 100644
index 256d79b..0000000
--- a/eai_agentplatform/backend-go/internal/specialists/packages/process_coordination/manifest.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package processcoordination
-
-import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
-
-var Manifest = specialistcontracts.Manifest{
- Key: "process-coordination",
- Label: "流程推进专员",
- RuleFileMarkdown: `# 流程推进专员
-
-## 你是谁
-你负责跨部门事项的推进:催办、checkpoint 跟进、阻塞清理、责任路由。
-你面对的是被卡住的事,用户要的是「下一步谁做什么」。
-
-## 你怎么推进
-1. 先定位卡在哪一个 checkpoint、卡在谁那里、卡了多久。
-2. 判断这是「没看到」「没时间」还是「有分歧」——三者的催法完全不同。
-3. 给出升级路径:本级催办无效时,下一步找谁、以什么理由。
-
-## 输出要求
-- 每条待办写全「事项 / 责任人 / 当前卡点 / 建议动作 / 建议时限」。
-- 催办话术要能直接复制发出去,不要写成「请尽快处理」这种空话。
-
-## 边界
-- 只起草催办与升级建议,不代替用户对外发送。
-- 不评价同事的工作态度,只陈述事项状态与时间事实。`,
- AllowedSkills: []string{"progress-report"},
- OwnedDefinitionKeys: []string{"specialist.process-coordination.definition"},
- OwnedCatalogEntries: []string{"catalog.specialist.process-coordination"},
- ReferencePolicy: specialistcontracts.SharedReferenceReadonlyHistory,
-}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/report_generation/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/report_generation/manifest.go
deleted file mode 100644
index 40002fc..0000000
--- a/eai_agentplatform/backend-go/internal/specialists/packages/report_generation/manifest.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package reportgeneration
-
-import specialistcontracts "eai_agentplatform/backend/internal/specialists/contracts"
-
-var Manifest = specialistcontracts.Manifest{
- Key: "report-generation",
- Label: "报告生成专员",
- RuleFileMarkdown: `# 报告生成专员
-
-## 你是谁
-你负责把散落的进展整理成能交上去的报告:日报、周报、复盘摘要、交付说明。
-你面对的是要拿这份报告去汇报的人。
-
-## 你怎么推进
-1. 先问清报告的读者和用途——给客户看和给自己团队看,写法完全不同。
-2. 按「结论 → 依据 → 下一步」组织,结论放最前面。
-3. 数据只用手上有出处的,缺的地方写「待补」,不要用估计值填空。
-
-## 输出要求
-- 一份报告一个主线,不要把三件事揉成一段。
-- 风险与未完成项不要藏,单独成节。
-
-## 边界
-- 不编造未发生的进展,不把「计划做」写成「已做」。
-- 封面/落款等信息缺就问,不要自己拟一个。`,
- AllowedSkills: []string{"report-generation", "progress-report", "ppt-generation", "meeting-minutes"},
- OwnedDefinitionKeys: []string{"specialist.report-generation.definition"},
- OwnedCatalogEntries: []string{"catalog.specialist.report-generation"},
- ReferencePolicy: specialistcontracts.SharedReferenceReadonlyHistory,
-}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/resume_processor/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/resume_processor/manifest.go
index 721d4a8..3ac3dd7 100644
--- a/eai_agentplatform/backend-go/internal/specialists/packages/resume_processor/manifest.go
+++ b/eai_agentplatform/backend-go/internal/specialists/packages/resume_processor/manifest.go
@@ -4,8 +4,8 @@ import specialistcontracts "eai_agentplatform/backend/internal/specialists/contr
var Manifest = specialistcontracts.Manifest{
Key: "resume-processor",
- Label: "招聘筛选专员",
- RuleFileMarkdown: `# 招聘筛选专员
+ Label: "简历筛选与招聘专员",
+ RuleFileMarkdown: `# 简历筛选与招聘专员
## 你是谁
你负责从招聘入口整理到候选人筛选的闭环:邮件归档、候选人抽取、评分排序、跟进标记、面试安排。
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go
index 5ea158a..eb4eefb 100644
--- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go
+++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go
@@ -194,7 +194,7 @@ func resetOfficialAccountArticle(article *officialaccountmodel.OfficialAccountAr
func buildOfficialAccountTopicCandidatesFromHotspots(form officialAccountForm, hotspots []officialaccountmodel.OfficialAccountHotspot) []officialAccountTopicCandidate {
domainLabel := officialAccountBusinessDomainLabel(form.BusinessDomain)
- candidates := make([]officialAccountTopicCandidate, 0, 4)
+ candidates := make([]officialAccountTopicCandidate, 0, 3)
for _, item := range hotspots {
topic := deriveOfficialAccountTopic(form.Keyword, item.Title)
if topic == "" {
@@ -206,7 +206,7 @@ func buildOfficialAccountTopicCandidatesFromHotspots(form officialAccountForm, h
Reason: firstNonEmpty(item.Summary, "来自公开"+domainLabel+"资讯源,已按业务域相关性筛选。"),
Heat: firstNonEmpty(item.HeatLabel, "中高"),
})
- if len(candidates) >= 4 {
+ if len(candidates) >= 3 {
break
}
}
@@ -273,7 +273,7 @@ func marshalOfficialAccountHotspots(hotspots []officialaccountmodel.OfficialAcco
func buildOfficialAccountHotspotPrompt(form officialAccountForm, hotspots []officialaccountmodel.OfficialAccountHotspot) string {
cfg := getOfficialAccountBusinessDomainConfig(form.BusinessDomain)
lines := []string{
- fmt.Sprintf("你是公众号选题策划编辑,请基于下面的%s热点,为公众号生成 4 个值得写的热点选题。", cfg.PromptLabel),
+ fmt.Sprintf("你是公众号选题策划编辑,请基于下面的%s热点,为公众号生成 3 个值得写的热点选题。", cfg.PromptLabel),
"要求:",
fmt.Sprintf("1. 主题必须适合%s方向的公众号写作;", cfg.PromptLabel),
"2. 要能写成公众号文章,不是简单复述新闻;",
@@ -284,6 +284,7 @@ func buildOfficialAccountHotspotPrompt(form officialAccountForm, hotspots []offi
fmt.Sprintf("面向人群:%s", firstNonEmpty(form.Audience, "公众号读者")),
fmt.Sprintf("内容目标:%s", firstNonEmpty(form.Goal, "输出一篇可发布的公众号文章")),
fmt.Sprintf("语气:%s", firstNonEmpty(form.Tone, "专业但好懂")),
+ fmt.Sprintf("额外要求:%s", firstNonEmpty(form.Requirements, "无")),
"热点列表:",
}
for idx, item := range hotspots {
@@ -302,6 +303,7 @@ func buildOfficialAccountTopicEvalPrompt(form officialAccountForm, topic string,
fmt.Sprintf("选题:%s", topic),
fmt.Sprintf("业务域:%s", cfg.Label),
fmt.Sprintf("关键词:%s", firstNonEmpty(form.Keyword, cfg.DefaultKeyword)),
+ fmt.Sprintf("额外要求:%s", firstNonEmpty(form.Requirements, "无")),
"参考热点:",
}
for idx, item := range hotspots {
@@ -327,6 +329,7 @@ func buildOfficialAccountTopicBatchEvalPrompt(form officialAccountForm, candidat
fmt.Sprintf("关键词:%s", firstNonEmpty(form.Keyword, cfg.DefaultKeyword)),
fmt.Sprintf("面向人群:%s", firstNonEmpty(form.Audience, "公众号读者")),
fmt.Sprintf("内容目标:%s", firstNonEmpty(form.Goal, "输出一篇可发布的公众号文章")),
+ fmt.Sprintf("额外要求:%s", firstNonEmpty(form.Requirements, "无")),
"待评估选题:",
}
for idx, item := range candidates {
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_delivery.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_delivery.go
index d37c99c..becabba 100644
--- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_delivery.go
+++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_delivery.go
@@ -573,7 +573,7 @@ func ExportOfficialAccountDocument(c *gin.Context) {
return
}
if task.SpecialistKey != officialAccountSpecialistKey {
- web.Fail(c, web.NewNotFoundError("当前事项不属于公众号专员"))
+ web.Fail(c, web.NewNotFoundError("当前事项不属于公众号创作专员"))
return
}
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go
index 1f6701c..9017a5f 100644
--- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go
+++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go
@@ -125,6 +125,11 @@ type officialAccountTopicCandidate struct {
Heat string `json:"heat"`
}
+type officialAccountTitleChoice struct {
+ Title string `json:"title"`
+ Positioning string `json:"positioning"`
+}
+
type officialAccountImagePrompt struct {
Key string `json:"key"`
Section string `json:"section"`
@@ -186,7 +191,7 @@ func CreateOfficialAccountTask(c *gin.Context) {
// 任务挂在已下线的专员上,历史记录仍要解释得通。
specialist, found := specialistDAO.GetByKey(officialAccountSpecialistKey)
if !found {
- web.Fail(c, web.NewNotFoundError("公众号专员尚未配置"))
+ web.Fail(c, web.NewNotFoundError("公众号创作专员尚未配置"))
return
}
@@ -230,7 +235,7 @@ func GetOfficialAccountWorkflow(c *gin.Context) {
return
}
if task.SpecialistKey != officialAccountSpecialistKey {
- web.Fail(c, web.NewNotFoundError("当前事项不属于公众号专员"))
+ web.Fail(c, web.NewNotFoundError("当前事项不属于公众号创作专员"))
return
}
respondOfficialAccountWorkflow(c, task)
@@ -243,7 +248,7 @@ func UpdateOfficialAccountTask(c *gin.Context) {
return
}
if task.SpecialistKey != officialAccountSpecialistKey {
- web.Fail(c, web.NewNotFoundError("当前事项不属于公众号专员"))
+ web.Fail(c, web.NewNotFoundError("当前事项不属于公众号创作专员"))
return
}
@@ -393,7 +398,7 @@ func ExecuteOfficialAccountWorkflowStep(c *gin.Context) {
return
}
if task.SpecialistKey != officialAccountSpecialistKey {
- web.Fail(c, web.NewNotFoundError("当前事项不属于公众号专员"))
+ web.Fail(c, web.NewNotFoundError("当前事项不属于公众号创作专员"))
return
}
@@ -524,7 +529,7 @@ func RegenerateOfficialAccountImage(c *gin.Context) {
return
}
if task.SpecialistKey != officialAccountSpecialistKey {
- web.Fail(c, web.NewNotFoundError("当前事项不属于公众号专员"))
+ web.Fail(c, web.NewNotFoundError("当前事项不属于公众号创作专员"))
return
}
@@ -1072,11 +1077,15 @@ func runOfficialAccountStep(stepKey string, workflow *officialAccountWorkflowSta
}
func executeOfficialAccountTopicStep(workflow *officialAccountWorkflowState, article *officialaccountmodel.OfficialAccountArticle, user *model.User, now time.Time) officialAccountStepExecution {
- hotspots, hotspotLogs, _ := ensureOfficialAccountHotspots(workflow.Form, false)
+ hotspots, hotspotLogs, hotspotErr := ensureOfficialAccountHotspots(workflow.Form, false)
hotspots = sortOfficialAccountHotspots(hotspots)
+ fallbackUsed := hotspotErr != nil || len(hotspots) == 0
candidates := buildOfficialAccountTopicCandidatesFromHotspots(workflow.Form, hotspots)
- _ = tryFillOfficialAccountTopicsWithAI(workflow.Form, hotspots, user, &candidates)
- evaluatedCount := evaluateOfficialAccountTopics(workflow.Form, hotspots, user, &candidates)
+ evaluatedCount := 0
+ if !fallbackUsed {
+ _ = tryFillOfficialAccountTopicsWithAI(workflow.Form, hotspots, user, &candidates)
+ evaluatedCount = evaluateOfficialAccountTopics(workflow.Form, hotspots, user, &candidates)
+ }
if workflow.Shared.SelectedTopic == "" && len(candidates) > 0 {
workflow.Shared.SelectedTopic = candidates[0].Topic
}
@@ -1090,20 +1099,30 @@ func executeOfficialAccountTopicStep(workflow *officialAccountWorkflowState, art
"candidates": candidates,
"selected_topic": workflow.Shared.SelectedTopic,
"hotspots": hotspots,
+ "fallback_used": fallbackUsed,
+ }
+ summary := fmt.Sprintf("已完成热点选题推荐,当前推荐题目为「%s」。", workflow.Shared.SelectedTopic)
+ logs := append([]string{
+ fmt.Sprintf("%s 已基于关键词「%s」生成候选选题", now.Format("15:04:05"), workflow.Form.Keyword),
+ fmt.Sprintf("%s 已完成 %d 个选题的热度与可写性评判", now.Format("15:04:05"), evaluatedCount),
+ fmt.Sprintf("%s 默认选中「%s」进入下一步", now.Format("15:04:05"), workflow.Shared.SelectedTopic),
+ }, hotspotLogs...)
+ if fallbackUsed {
+ summary = fmt.Sprintf("热点源暂不可用,已回退为通用选题建议;当前推荐题目为「%s」。", workflow.Shared.SelectedTopic)
+ output["hotspot_error"] = errorMessage(hotspotErr)
+ logs = append(logs,
+ fmt.Sprintf("%s 未获取到可用热点,已回退为通用选题模板。", now.Format("15:04:05")),
+ )
}
return officialAccountStepExecution{
- Summary: fmt.Sprintf("已完成热点选题推荐,当前推荐题目为「%s」。", workflow.Shared.SelectedTopic),
+ Summary: summary,
TaskStatus: taskStatusInProgress,
ArtifactTitle: "公众号选题建议",
ArtifactType: "topics",
ArtifactStatus: artifactStatusDraft,
ArtifactContent: buildTopicArtifactContent(candidates, workflow.Shared.SelectedTopic),
Output: output,
- Logs: append([]string{
- fmt.Sprintf("%s 已基于关键词「%s」生成候选选题", now.Format("15:04:05"), workflow.Form.Keyword),
- fmt.Sprintf("%s 已完成 %d 个选题的热度与可写性评判", now.Format("15:04:05"), evaluatedCount),
- fmt.Sprintf("%s 默认选中「%s」进入下一步", now.Format("15:04:05"), workflow.Shared.SelectedTopic),
- }, hotspotLogs...),
+ Logs: logs,
}
}
@@ -1122,7 +1141,7 @@ func executeOfficialAccountTitleStep(workflow *officialAccountWorkflowState, art
}
output := map[string]any{
- "titles": titles,
+ "titles": buildOfficialAccountTitleChoices(titles),
"selected_title": workflow.Shared.SelectedTitle,
"selected_topic": workflow.Shared.SelectedTopic,
}
@@ -1227,12 +1246,6 @@ func buildOfficialAccountTopicCandidates(form officialAccountForm) []officialAcc
Reason: "更贴近业务落地,适合输出步骤化内容。",
Heat: "中高",
},
- {
- Topic: fmt.Sprintf("%s这件事,外行和内行到底差在哪?", keyword),
- Angle: "认知对比",
- Reason: "利于写出观点型文章,适合做公众号风格表达。",
- Heat: "中",
- },
}
}
@@ -1242,8 +1255,6 @@ func buildOfficialAccountTitles(form officialAccountForm, topic string) []string
fmt.Sprintf("%s最近又火了,但大多数人根本没看懂", topic),
fmt.Sprintf("别急着谈%s,先把这 3 件事看明白", keyword),
fmt.Sprintf("%s最容易踩的坑,我一次讲透", keyword),
- fmt.Sprintf("为什么说%s不是不能做,而是你做错了顺序", keyword),
- fmt.Sprintf("关于%s,外行最爱问的 5 个问题", keyword),
}
}
@@ -1251,11 +1262,13 @@ func buildOfficialAccountOutline(form officialAccountForm, title string) string
keyword := firstNonEmpty(form.Keyword, "这个主题")
outlineStyle := normalizeOfficialAccountOutlineStyle(form.OutlineStyle)
outlineWords := normalizeOfficialAccountOutlineWords(form.OutlineWords)
+ requirements := firstNonEmpty(form.Requirements, "无")
return strings.TrimSpace(fmt.Sprintf(
- "# 标题\n%s\n\n> 提纲风格:%s | 目标字数:约 %d 字\n\n## 一、为什么现在值得聊%s\n- 当前背景和触发原因\n- 为什么读者现在会关心\n\n## 二、大多数人最容易误解的地方\n- 常见认知偏差 1\n- 常见认知偏差 2\n- 常见认知偏差 3\n\n## 三、真正可落地的做法\n- 第一步:先明确目标\n- 第二步:先从小范围验证\n- 第三步:沉淀方法和复用模板\n\n## 四、最后给读者的建议\n- 什么情况应该立刻开始\n- 什么情况应该先补前置条件\n- 一句话总结\n",
+ "# 标题\n%s\n\n> 提纲风格:%s | 目标字数:约 %d 字\n> 额外要求:%s\n\n## 一、为什么现在值得聊%s\n- 当前背景和触发原因\n- 为什么读者现在会关心\n\n## 二、大多数人最容易误解的地方\n- 常见认知偏差 1\n- 常见认知偏差 2\n- 常见认知偏差 3\n\n## 三、真正可落地的做法\n- 第一步:先明确目标\n- 第二步:先从小范围验证\n- 第三步:沉淀方法和复用模板\n\n## 四、最后给读者的建议\n- 什么情况应该立刻开始\n- 什么情况应该先补前置条件\n- 一句话总结\n",
title,
outlineStyle,
outlineWords,
+ requirements,
keyword,
))
}
@@ -1265,10 +1278,12 @@ func buildOfficialAccountContent(form officialAccountForm, title, outline string
audience := firstNonEmpty(form.Audience, "公众号读者")
tone := firstNonEmpty(form.Tone, "专业但好懂")
targetWords := normalizeOfficialAccountContentTargetWords(form.ContentTargetWords)
+ requirements := firstNonEmpty(form.Requirements, "无")
return strings.TrimSpace(fmt.Sprintf(
- "# %s\n\n> 正文字数目标:约 %d 字\n\n很多%s最近都在关注“%s”,但真正把这件事讲清楚的人并不多。今天这篇文章,我想用尽量%s的方式,把这个问题讲透。\n\n## 为什么现在值得聊\n\n第一,是这个话题本身已经进入更多人的工作和决策场景;第二,是不少人已经开始行动,但做法还比较碎;第三,大家更需要的不是空泛观点,而是能拿去用的方法。\n\n## 最容易踩的坑\n\n第一个坑,是一上来就追求大而全,结果什么都想做,最后什么都落不下去。\n\n第二个坑,是只看表面热度,不看自己真实业务是否适配。\n\n第三个坑,是做完一次就结束,没有形成可复用的内容资产和方法模板。\n\n## 真正可落地的做法\n\n先明确目标:你是要做传播、做转化,还是做认知教育?目标不同,内容结构完全不同。\n\n再做小范围验证:不要一开始就上最重的版本,先验证选题、标题和读者反馈。\n\n最后沉淀模板:把好用的标题结构、提纲结构和正文风格保留下来,后面才能越做越快。\n\n## 最后想说\n\n如果你也在做%s,别急着一次把所有事情做完。先抓住一个明确问题,把它讲明白、讲透、讲到读者愿意继续往下看,这篇文章就已经成功了一半。\n\n这篇内容可以继续细化成你自己的案例版、行业版、客户版。后面只要沿着这个骨架迭代,文章生产就会越来越稳。\n",
+ "# %s\n\n> 正文字数目标:约 %d 字\n> 额外要求:%s\n\n很多%s最近都在关注“%s”,但真正把这件事讲清楚的人并不多。今天这篇文章,我想用尽量%s的方式,把这个问题讲透。\n\n## 为什么现在值得聊\n\n第一,是这个话题本身已经进入更多人的工作和决策场景;第二,是不少人已经开始行动,但做法还比较碎;第三,大家更需要的不是空泛观点,而是能拿去用的方法。\n\n## 最容易踩的坑\n\n第一个坑,是一上来就追求大而全,结果什么都想做,最后什么都落不下去。\n\n第二个坑,是只看表面热度,不看自己真实业务是否适配。\n\n第三个坑,是做完一次就结束,没有形成可复用的内容资产和方法模板。\n\n## 真正可落地的做法\n\n先明确目标:你是要做传播、做转化,还是做认知教育?目标不同,内容结构完全不同。\n\n再做小范围验证:不要一开始就上最重的版本,先验证选题、标题和读者反馈。\n\n最后沉淀模板:把好用的标题结构、提纲结构和正文风格保留下来,后面才能越做越快。\n\n## 最后想说\n\n如果你也在做%s,别急着一次把所有事情做完。先抓住一个明确问题,把它讲明白、讲透、讲到读者愿意继续往下看,这篇文章就已经成功了一半。\n\n这篇内容可以继续细化成你自己的案例版、行业版、客户版。后面只要沿着这个骨架迭代,文章生产就会越来越稳。\n",
firstNonEmpty(title, keyword),
targetWords,
+ requirements,
audience,
keyword,
tone,
@@ -1289,11 +1304,12 @@ func tryFillOfficialAccountTopicsWithAI(form officialAccountForm, hotspots []off
func tryFillOfficialAccountTitlesWithAI(form officialAccountForm, topic string, user *model.User, target *[]string) bool {
var items []string
prompt := fmt.Sprintf(
- "请基于选题“%s”为公众号生成 5 个标题候选,关键词是“%s”,面向%s,语气%s。只返回 JSON 数组字符串,不要输出 markdown。",
+ "请基于选题“%s”为公众号生成 3 个标题候选,分别覆盖偏点击、偏稳重、偏平衡三种取向。关键词是“%s”,面向%s,语气%s,额外要求:%s。只返回 JSON 数组字符串,不要输出 markdown。",
topic,
form.Keyword,
form.Audience,
form.Tone,
+ firstNonEmpty(form.Requirements, "无"),
)
if !generateOfficialAccountJSON(prompt, user, &items) || len(items) == 0 {
return false
@@ -1304,24 +1320,26 @@ func tryFillOfficialAccountTitlesWithAI(form officialAccountForm, topic string,
func tryGenerateOfficialAccountOutlineWithAI(form officialAccountForm, title string, user *model.User) (string, bool) {
prompt := fmt.Sprintf(
- "请基于公众号标题“%s”生成一份适合中文公众号的文章提纲,输出 markdown。要求有 4-6 个二级部分,每部分带 2-4 个要点,语气%s,受众%s,提纲风格%s,目标提纲字数约%d字。",
+ "请基于公众号标题“%s”生成一份适合中文公众号的文章提纲,输出 markdown。要求有 4-6 个二级部分,每部分带 2-4 个要点,语气%s,受众%s,提纲风格%s,目标提纲字数约%d字。额外要求:%s",
title,
form.Tone,
form.Audience,
normalizeOfficialAccountOutlineStyle(form.OutlineStyle),
normalizeOfficialAccountOutlineWords(form.OutlineWords),
+ firstNonEmpty(form.Requirements, "无"),
)
return generateOfficialAccountText(prompt, user)
}
func tryGenerateOfficialAccountContentWithAI(form officialAccountForm, title, outline string, user *model.User) (string, bool) {
prompt := fmt.Sprintf(
- "请根据以下公众号文章信息输出一篇可发布的中文正文草稿。\n标题:%s\n受众:%s\n目标:%s\n语气:%s\n提纲:\n%s\n要求:约%d字,语言自然,不要编造成数据,不要输出多余解释。",
+ "请根据以下公众号文章信息输出一篇可发布的中文正文草稿。\n标题:%s\n受众:%s\n目标:%s\n语气:%s\n提纲:\n%s\n额外要求:%s\n要求:约%d字,语言自然,不要编造成数据,不要输出多余解释。",
title,
form.Audience,
form.Goal,
form.Tone,
outline,
+ firstNonEmpty(form.Requirements, "无"),
normalizeOfficialAccountContentTargetWords(form.ContentTargetWords),
)
return generateOfficialAccountText(prompt, user)
@@ -1519,8 +1537,8 @@ func buildTopicArtifactContent(candidates []officialAccountTopicCandidate, selec
func buildTitleArtifactContent(titles []string, selected string) string {
lines := []string{"# 标题候选", ""}
- for idx, item := range titles {
- lines = append(lines, fmt.Sprintf("%d. %s", idx+1, item))
+ for idx, item := range buildOfficialAccountTitleChoices(titles) {
+ lines = append(lines, fmt.Sprintf("%d. [%s] %s", idx+1, item.Positioning, item.Title))
}
lines = append(lines, "", fmt.Sprintf("当前默认标题:%s", selected))
return strings.Join(lines, "\n")
@@ -1531,6 +1549,9 @@ func buildOfficialAccountSourceRefs(workflow officialAccountWorkflowState, stepK
"关键词:" + firstNonEmpty(workflow.Form.Keyword, "待补充"),
"受众:" + firstNonEmpty(workflow.Form.Audience, "公众号读者"),
}
+ if workflow.Form.Requirements != "" {
+ refs = append(refs, "写作要求:"+workflow.Form.Requirements)
+ }
if workflow.Shared.SelectedTopic != "" {
refs = append(refs, "选题:"+workflow.Shared.SelectedTopic)
}
@@ -1543,6 +1564,22 @@ func buildOfficialAccountSourceRefs(workflow officialAccountWorkflowState, stepK
return refs
}
+func buildOfficialAccountTitleChoices(titles []string) []officialAccountTitleChoice {
+ labels := []string{"偏点击", "偏稳重", "偏平衡"}
+ choices := make([]officialAccountTitleChoice, 0, len(titles))
+ for idx, item := range titles {
+ positioning := "候选"
+ if idx < len(labels) {
+ positioning = labels[idx]
+ }
+ choices = append(choices, officialAccountTitleChoice{
+ Title: item,
+ Positioning: positioning,
+ })
+ }
+ return choices
+}
+
func cloneOutputMap(source map[string]any) map[string]any {
result := map[string]any{}
for key, value := range source {
diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/manifest.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/manifest.go
index 7391d1e..9904a9f 100644
--- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/manifest.go
+++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/manifest.go
@@ -4,8 +4,8 @@ import specialistcontracts "eai_agentplatform/backend/internal/specialists/contr
var Manifest = specialistcontracts.Manifest{
Key: "wechat-official-account",
- Label: "公众号专员",
- RuleFileMarkdown: `# 公众号专员
+ Label: "公众号创作专员",
+ RuleFileMarkdown: `# 公众号创作专员
## 你是谁
你负责公众号内容从选题到成稿:热点选题、标题、提纲、正文。
diff --git a/eai_agentplatform/backend-go/internal/specialists/seeding/seed_specialists.go b/eai_agentplatform/backend-go/internal/specialists/seeding/seed_specialists.go
index c62c730..324e94f 100644
--- a/eai_agentplatform/backend-go/internal/specialists/seeding/seed_specialists.go
+++ b/eai_agentplatform/backend-go/internal/specialists/seeding/seed_specialists.go
@@ -38,81 +38,9 @@ func SeedSpecialists(db *gorm.DB) error {
State: "system",
SortOrder: 0,
},
- {
- Key: "knowledge-operations",
- Label: "知识运营专员",
- Tier: "generic",
- SpecialistMode: "dw",
- ObjectEntryRoute: "/apps/knowledge-operations",
- Summary: "对象字典、证据索引与规则同步",
- WorkStatus: "4 个待整理",
- RiskLabel: "1 个映射待确认",
- Color: "#8e6cf2",
- Stage: "证据整理",
- Progress: 63,
- MarketTag: "已安装",
- Version: "v1.0",
- ConnectorScope: "连接知识资产 / 文档系统",
- PermissionScope: "知识资产读取、规则发布建议、证据索引维护",
- ResourceBindings: "知识库、文档系统、规则字典、对象映射表",
- InfoSources: "对象字典、规则文档、证据片段、变更记录",
- BaseSkills: "对象映射、证据抽取、规则同步、发布检查",
- AIAssistance: "自动识别字段冲突并给出映射建议",
- GeneratedSkills: "把高频清洗与映射动作沉淀为新规则技能",
- State: "active",
- SortOrder: 20,
- },
- {
- Key: "process-coordination",
- Label: "流程推进专员",
- Tier: "generic",
- SpecialistMode: "adw",
- ObjectEntryRoute: "/apps/process-coordination",
- Summary: "跨部门催办、checkpoint 跟进与阻塞升级",
- WorkStatus: "6 个待跟进",
- RiskLabel: "2 个阻塞",
- Color: "#14b8a6",
- Stage: "阻塞清理",
- Progress: 57,
- MarketTag: "可升级",
- Version: "v1.0",
- ConnectorScope: "连接审批流 / 企微 / 项目事项",
- PermissionScope: "事项读写、催办通知、阻塞升级、审批触发",
- ResourceBindings: "审批流、项目事项、企微、日程提醒",
- InfoSources: "任务看板、流程 checkpoint、阻塞日志、责任人状态",
- BaseSkills: "催办升级、checkpoint 跟进、阻塞清理、责任路由",
- AIAssistance: "自动判断升级路径并规划下一步协同动作",
- GeneratedSkills: "把成功的跨部门推进套路沉淀为自动推进技能",
- State: "active",
- SortOrder: 30,
- },
- {
- Key: "report-generation",
- Label: "报告生成专员",
- Tier: "generic",
- SpecialistMode: "dw",
- ObjectEntryRoute: "/apps/report-generation",
- Summary: "日报、周报、复盘和交付说明自动生成",
- WorkStatus: "3 个待输出",
- RiskLabel: "0 个异常",
- Color: "#ec4899",
- Stage: "报告汇总",
- Progress: 76,
- MarketTag: "可升级",
- Version: "v1.0",
- ConnectorScope: "连接交付物 / 知识库 / 模板中心",
- PermissionScope: "交付物读取、模板套用、报告产出",
- ResourceBindings: "交付模板、知识库、任务结果、证据引用",
- InfoSources: "日报数据、周进度、复盘纪要、交付清单",
- BaseSkills: "日报生成、周报汇总、复盘摘要、交付说明编排",
- AIAssistance: "自动抽取重点变化并生成适配受众的版本",
- GeneratedSkills: "把高质量报告结构沉淀为新模板技能",
- State: "active",
- SortOrder: 40,
- },
{
Key: "wechat-official-account",
- Label: "公众号专员",
+ Label: "公众号创作专员",
Tier: "generic",
SpecialistMode: "dw",
ObjectEntryRoute: "/apps/wechat-official-account",
@@ -208,7 +136,7 @@ func SeedSpecialists(db *gorm.DB) error {
},
{
Key: "resume-processor",
- Label: "招聘筛选专员",
+ Label: "简历筛选与招聘专员",
Tier: "industry",
SpecialistMode: "dw",
ObjectEntryRoute: "/apps/resume-processor",
@@ -230,30 +158,6 @@ func SeedSpecialists(db *gorm.DB) error {
State: "active",
SortOrder: 80,
},
- {
- Key: "presentation-briefing",
- Label: "汇报专员",
- Tier: "generic",
- SpecialistMode: "dw",
- ObjectEntryRoute: "/apps/presentation-briefing",
- Summary: "经营汇报、方案汇报与培训课件统一出稿",
- WorkStatus: "2 个待成稿",
- RiskLabel: "1 个页数待压缩",
- Color: "#4f46e5",
- Stage: "演示编排",
- Progress: 68,
- MarketTag: "已安装",
- Version: "v1.0",
- ConnectorScope: "连接任务结果 / 知识库 / 模板中心",
- PermissionScope: "读取材料、生成演示稿、导出讲稿备注",
- ResourceBindings: "演示模板、知识库、任务产物、汇报素材",
- InfoSources: "主题目标、受众、汇报材料、页数限制",
- BaseSkills: "演示结构规划、页面要点提炼、讲稿备注生成、口径统一",
- AIAssistance: "自动压缩材料并给出适合管理层、客户或培训场景的页面结构",
- GeneratedSkills: "把高质量汇报结构沉淀为可复用的演示模板",
- State: "active",
- SortOrder: 100,
- },
})
// 岗位说明书与技能绑定按 key 挂载(见 specialists/core.RuleFileDrafts / SkillBindings)
if err := specialistcore.ApplyRuleFiles(items); err != nil {
@@ -327,23 +231,6 @@ func SeedSpecialists(db *gorm.DB) error {
}
}
- if err := deleteSpecialists(db, []string{
- "training-delivery",
- }); err != nil {
- return err
- }
-
- if err := retireSpecialists(db, []string{
- "knowledge-operations",
- "process-coordination",
- "report-generation",
- "presentation-briefing",
- "hr-email-sorter",
- "logistics-fulfillment",
- }); err != nil {
- return err
- }
-
specialistcore.LogRuleFileCoverage(db)
log.Println("[OK] 专员目录种子已导入")
return nil
@@ -366,20 +253,3 @@ func applySpecialistCodes(items []specialistmodel.Specialist) []specialistmodel.
}
return items
}
-
-func deleteSpecialists(db *gorm.DB, keys []string) error {
- if len(keys) == 0 {
- return nil
- }
- return db.Where("key IN ?", keys).Delete(&specialistmodel.Specialist{}).Error
-}
-
-func retireSpecialists(db *gorm.DB, keys []string) error {
- if len(keys) == 0 {
- return nil
- }
- return db.Model(&specialistmodel.Specialist{}).
- Where("key IN ?", keys).
- Where("state = ?", "active").
- Update("state", "retired").Error
-}
diff --git a/eai_agentplatform/backend-go/internal/store/seed.go b/eai_agentplatform/backend-go/internal/store/seed.go
index bb2523f..d6b9084 100644
--- a/eai_agentplatform/backend-go/internal/store/seed.go
+++ b/eai_agentplatform/backend-go/internal/store/seed.go
@@ -66,9 +66,9 @@ func SeedDefaults() error {
{"password_min_length", "6", "密码最小长度"},
{"session_timeout_minutes", "480", "会话超时分钟数(不活动自动退出)"},
// 7.2.4 AI 配置
- {"llm_base_url", "http://localhost:11434/v1", "LLM 服务地址"},
- {"llm_api_key", "sk-xxx", "LLM API Key(敏感信息,建议写入 ai_secrets.json)"},
- {"llm_model", "qwen2.5:7b", "LLM 模型名"},
+ {"llm_base_url", "http://localhost:8080/v1", "LLM 服务地址"},
+ {"llm_api_key", "local", "LLM API Key(本地服务可留占位值,远程服务建议写入 ai_secrets.json)"},
+ {"llm_model", "Qwen3.5-4B-Q4_K_M.gguf", "LLM 模型名"},
{"embed_model", "bge-m3", "Embedding 模型名"},
{"llm_max_tokens", "2048", "LLM 最大输出 Token 数"},
{"llm_temperature", "0.7", "LLM 温度参数(0.0~1.0)"},
@@ -91,6 +91,18 @@ func SeedDefaults() error {
if err := seedCoursesFromJSON(); err != nil {
return err
}
+ if err := seedProductsFromJSON(); err != nil {
+ return err
+ }
+ if err := seedCompanyIntroContent(); err != nil {
+ return err
+ }
+ if err := seedQuestionsFromJSON(); err != nil {
+ return err
+ }
+ if err := seedDefaultExamPapers(); err != nil {
+ return err
+ }
if err := seedTrainingMedia(); err != nil {
return err
}
@@ -135,6 +147,40 @@ type courseSeedFile struct {
Data []courseSeedItem `json:"data"`
}
+type productSeedFile struct {
+ Data []productSeedItem `json:"data"`
+}
+
+type productSeedItem struct {
+ Code string `json:"code"`
+ Name string `json:"name"`
+ Category string `json:"category"`
+ Tags string `json:"tags"`
+ Description string `json:"description"`
+ Pricing string `json:"pricing"`
+ CommissionRecommend string `json:"commission_recommend"`
+ CommissionNegotiate string `json:"commission_negotiate"`
+ PublicCourseBonus string `json:"public_course_bonus"`
+ VersionRisk string `json:"version_risk"`
+ ReportRules string `json:"report_rules"`
+ Status string `json:"status"`
+}
+
+type questionSeedFile struct {
+ Data []questionSeedItem `json:"data"`
+}
+
+type questionSeedItem struct {
+ Domain string `json:"domain"`
+ CourseID *uint `json:"course_id"`
+ Type string `json:"type"`
+ Stem string `json:"stem"`
+ Options []model.QuestionOption `json:"options"`
+ Answer []string `json:"answer"`
+ Explanation string `json:"explanation"`
+ Status string `json:"status"`
+}
+
type courseSeedItem struct {
Code string `json:"code"`
Name string `json:"name"`
@@ -197,6 +243,172 @@ func seedCoursesFromJSON() error {
return nil
}
+func seedProductsFromJSON() error {
+ var count int64
+ DB.Model(&model.Product{}).Count(&count)
+ if count > 0 {
+ return nil
+ }
+
+ cfg := config.Load()
+ path := filepath.Join(cfg.TrainingMaterialsDir, "work", "products.json")
+ if _, err := os.Stat(path); err != nil {
+ return nil
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ var payload productSeedFile
+ if err := json.Unmarshal(data, &payload); err != nil {
+ return err
+ }
+
+ for _, item := range payload.Data {
+ if item.Code == "" || item.Name == "" || item.Category == "" {
+ continue
+ }
+ status := item.Status
+ if strings.TrimSpace(status) == "" {
+ status = "active"
+ }
+ product := model.Product{
+ Code: item.Code,
+ Name: item.Name,
+ Category: item.Category,
+ Tags: item.Tags,
+ Description: item.Description,
+ Pricing: item.Pricing,
+ CommissionRecommend: item.CommissionRecommend,
+ CommissionNegotiate: item.CommissionNegotiate,
+ PublicCourseBonus: item.PublicCourseBonus,
+ VersionRisk: item.VersionRisk,
+ ReportRules: item.ReportRules,
+ Status: status,
+ }
+ if err := DB.Create(&product).Error; err != nil {
+ return err
+ }
+ }
+
+ log.Println("[OK] 产品知识种子已导入")
+ return nil
+}
+
+func seedCompanyIntroContent() error {
+ var cfg model.SystemConfig
+ if err := DB.Where("config_key = ?", "company_intro").First(&cfg).Error; err != nil {
+ return nil
+ }
+ if strings.TrimSpace(cfg.ConfigValue) != "" {
+ return nil
+ }
+ cfg.ConfigValue = strings.Join([]string{
+ "博昇,聚焦企业增长与 AI 落地的综合服务商。",
+ "",
+ "我们围绕企业从资本运作到资质认定,再到 AI 转型的完整成长链路,提供四大板块服务:",
+ "1. 资本咨询:股权规划、融资顾问、IPO 辅导,帮助企业走通资本路径。",
+ "2. 资质认定辅导:高新技术企业、专精特新、专精特新小巨人的申报陪跑。",
+ "3. AI 咨询与实施:AI 公开课、落地诊断、软件授权与定制开发。",
+ "4. 企业级 AI 工具与平台:按行业匹配知识库、办公智能体、工业/医疗 AI、内容生成与电商运营方案。",
+ "",
+ "本培训平台面向新员工入职培训与内部持续学习,统一承接公司认知、产品知识、销售方法与考试验收。",
+ }, "\n")
+ return DB.Save(&cfg).Error
+}
+
+func seedQuestionsFromJSON() error {
+ var count int64
+ DB.Model(&model.Question{}).Count(&count)
+ if count > 0 {
+ return nil
+ }
+
+ cfg := config.Load()
+ path := filepath.Join(cfg.TrainingMaterialsDir, "work", "questions.json")
+ if _, err := os.Stat(path); err != nil {
+ return nil
+ }
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ var payload questionSeedFile
+ if err := json.Unmarshal(data, &payload); err != nil {
+ return err
+ }
+
+ for _, item := range payload.Data {
+ if item.Domain == "" || item.Type == "" || item.Stem == "" || len(item.Answer) == 0 {
+ continue
+ }
+ status := item.Status
+ if strings.TrimSpace(status) == "" {
+ status = "active"
+ }
+ question := model.Question{
+ Domain: item.Domain,
+ CourseID: item.CourseID,
+ Type: item.Type,
+ Stem: item.Stem,
+ Options: mustJSON(item.Options),
+ Answer: mustJSON(item.Answer),
+ Explanation: item.Explanation,
+ Status: status,
+ }
+ if err := DB.Create(&question).Error; err != nil {
+ return err
+ }
+ }
+
+ log.Println("[OK] 考试题库种子已导入")
+ return nil
+}
+
+func seedDefaultExamPapers() error {
+ var count int64
+ DB.Model(&model.ExamPaper{}).Count(&count)
+ if count > 0 {
+ return nil
+ }
+
+ papers := []model.ExamPaper{
+ {
+ Name: "内部培训综合自测",
+ Type: "self_test",
+ Domain: "company,product,sales",
+ QuestionCount: 20,
+ TotalScore: 100,
+ PassScore: 60,
+ DurationMinutes: 30,
+ Randomize: true,
+ Status: "active",
+ },
+ {
+ Name: "内部培训综合考试",
+ Type: "formal",
+ Domain: "company,product,sales",
+ QuestionCount: 30,
+ TotalScore: 100,
+ PassScore: 80,
+ DurationMinutes: 45,
+ Randomize: true,
+ Status: "active",
+ },
+ }
+ for _, paper := range papers {
+ if err := DB.Create(&paper).Error; err != nil {
+ return err
+ }
+ }
+ log.Println("[OK] 默认考试配置已导入")
+ return nil
+}
+
type mediaSeed struct {
filename string
sourcePath string
@@ -345,8 +557,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "直接告诉我目标,或者把材料贴给我,我来帮你起步。",
"starter_prompts": []string{"帮我拆解这个任务,并列出下一步", "把这段产品介绍整理成正式文案", "先帮我判断这个问题该找哪个专员或技能"},
}),
- State: "active",
- SortOrder: 10,
+ State: "active",
+ SortOrder: 10,
},
{
Key: "document-translate",
@@ -373,8 +585,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "把原文贴给我,并说明目标语言、受众和语气要求;如果有术语表,也一起给我。",
"starter_prompts": []string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"},
}),
- State: "active",
- SortOrder: 20,
+ State: "active",
+ SortOrder: 20,
},
{
Key: "copy-proofreading",
@@ -401,8 +613,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "把文案贴给我,并告诉我这段内容要发给谁、希望保留什么语气。",
"starter_prompts": []string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"},
}),
- State: "active",
- SortOrder: 30,
+ State: "active",
+ SortOrder: 30,
},
{
Key: "audio-transcribe",
@@ -429,8 +641,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "把音频内容发给我,或者告诉我语言、场景和你最终想拿到什么结果。",
"starter_prompts": []string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"},
}),
- State: "active",
- SortOrder: 40,
+ State: "active",
+ SortOrder: 40,
},
{
Key: "batch-extract",
@@ -457,8 +669,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "告诉我想抽哪些字段、输出成什么格式、哪些情况需要标记为异常。",
"starter_prompts": []string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"},
}),
- State: "active",
- SortOrder: 50,
+ State: "active",
+ SortOrder: 50,
},
{
Key: "contract-review",
@@ -485,8 +697,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "把合同发给我,并告诉我这次重点想看哪些条款,或者你站在哪一方立场。",
"starter_prompts": []string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"},
}),
- State: "active",
- SortOrder: 60,
+ State: "active",
+ SortOrder: 60,
},
{
Key: "report-generation",
@@ -513,8 +725,8 @@ func seedSkillDefinitions() error {
"opening_prompt": "把材料给我,并告诉我报告类型、受众、周期和你最想突出哪些重点。",
"starter_prompts": []string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"},
}),
- State: "active",
- SortOrder: 70,
+ State: "active",
+ SortOrder: 70,
},
})
@@ -894,9 +1106,9 @@ func seedXAppDefinitions() error {
Color: "#4f46e5",
IconText: "P",
CoverTone: "linear-gradient(135deg, #4338ca 0%, #818cf8 100%)",
- Summary: "直接进入汇报 PPT 应用,默认挂上汇报专员,快速拿到页面结构、讲稿备注和演示骨架。",
- Description: "适合经营汇报、方案汇报、培训课件和项目复盘。打开就进入“汇报专员 + PPT 技能”的样板链路。",
- SpecialistKey: "presentation-briefing",
+ Summary: "直接进入汇报 PPT 应用,快速拿到页面结构、讲稿备注和演示骨架。",
+ Description: "适合经营汇报、方案汇报、培训课件和项目复盘。",
+ SpecialistKey: "",
SkillKey: "ppt-generation",
DefaultPrompt: "帮我做一版 8 页管理层汇报 PPT",
PromptsJSON: mustJSON([]string{"帮我做一版 8 页管理层汇报 PPT", "按汇报对象输出页面大纲和讲稿备注", "把这批材料压成一版培训课件骨架"}),
diff --git a/eai_agentplatform/backend-go/internal/xapps/api/handlers.go b/eai_agentplatform/backend-go/internal/xapps/api/handlers.go
index efd122c..c96d9cc 100644
--- a/eai_agentplatform/backend-go/internal/xapps/api/handlers.go
+++ b/eai_agentplatform/backend-go/internal/xapps/api/handlers.go
@@ -13,6 +13,7 @@ import (
"eai_agentplatform/backend/internal/jsonutil"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/web"
+ xappcore "eai_agentplatform/backend/internal/xapps/core"
xappdefs "eai_agentplatform/backend/internal/xapps/model"
)
@@ -132,6 +133,16 @@ func validateDefinitionReq(req *definitionReq) *web.AppError {
if !jsonutil.ValidateStringArrayJSON(req.TagsJSON) {
return web.NewBadRequest("tags_json 必须是字符串数组")
}
+ if req.SpecialistKey != "" {
+ if _, found := (dal.SpecialistDAO{}).GetByKey(req.SpecialistKey); !found {
+ return web.NewBadRequest("specialist_key 指向的专员不存在")
+ }
+ }
+ if req.SkillKey != "" {
+ if _, found := (dal.SkillDefinitionDAO{}).GetByKey(req.SkillKey); !found {
+ return web.NewBadRequest("skill_key 指向的技能不存在")
+ }
+ }
return nil
}
@@ -270,6 +281,10 @@ func DeleteXAppDefinition(c *gin.Context) {
web.Fail(c, web.NewNotFoundError("应用定义不存在"))
return
}
+ if plan := xappcore.BuildRemovePlan(item); plan.Blocked {
+ web.Fail(c, web.NewConflictError("应用仍被运行时对象引用,请先处理 remove-plan 中的关联项"))
+ return
+ }
if !xAppDefinitionDAO.Delete(&item) {
web.Fail(c, web.NewBadRequest("删除应用定义失败"))
return
@@ -277,6 +292,28 @@ func DeleteXAppDefinition(c *gin.Context) {
web.OK(c, gin.H{"id": id, "deleted": true})
}
+func PreviewXAppRemovePlan(c *gin.Context) {
+ id, ok := parseID(c, "id")
+ if !ok {
+ return
+ }
+ item, found := xAppDefinitionDAO.GetByID(id)
+ if !found {
+ web.Fail(c, web.NewNotFoundError("应用定义不存在"))
+ return
+ }
+ plan := xappcore.BuildRemovePlan(item)
+ web.OK(c, gin.H{
+ "id": item.ID,
+ "key": item.Key,
+ "label": item.Label,
+ "source": item.Source,
+ "state": item.State,
+ "is_builtin": false,
+ "plan": plan,
+ })
+}
+
const (
maxFavoriteXAppCount = 24
maxRecentXAppCount = 8
diff --git a/eai_agentplatform/backend-go/internal/xapps/core/uninstall.go b/eai_agentplatform/backend-go/internal/xapps/core/uninstall.go
index 7f4e6fc..8e4d3f2 100644
--- a/eai_agentplatform/backend-go/internal/xapps/core/uninstall.go
+++ b/eai_agentplatform/backend-go/internal/xapps/core/uninstall.go
@@ -1,11 +1,26 @@
package xappcore
+import (
+ "eai_agentplatform/backend/internal/objectrefs"
+ xappmodel "eai_agentplatform/backend/internal/xapps/model"
+)
+
type UninstallPreview struct {
DefinitionKeys []string
CatalogEntries []string
Policy SharedReferencePolicy
}
+type RemovePlan struct {
+ Key string
+ Label string
+ DefinitionKeys []string
+ CatalogEntries []string
+ Policy SharedReferencePolicy
+ RuntimeRefs objectrefs.XAppRuntimeRefs
+ Blocked bool
+}
+
func BuildUninstallPreview(manifest Manifest) UninstallPreview {
meta := manifest.UninstallMeta()
return UninstallPreview{
@@ -14,3 +29,21 @@ func BuildUninstallPreview(manifest Manifest) UninstallPreview {
Policy: meta.ReferencePolicy,
}
}
+
+func BuildRemovePlan(item xappmodel.XAppDefinition) RemovePlan {
+ definitionKeys := []string{}
+ catalogEntries := []string{}
+ if item.Key != "" {
+ definitionKeys = []string{"xapp." + item.Key + ".definition"}
+ catalogEntries = []string{"catalog.xapp." + item.Key}
+ }
+ return RemovePlan{
+ Key: item.Key,
+ Label: item.Label,
+ DefinitionKeys: definitionKeys,
+ CatalogEntries: catalogEntries,
+ Policy: "",
+ RuntimeRefs: objectrefs.CollectXAppRuntimeRefs(item.Key),
+ Blocked: false,
+ }
+}
diff --git a/eai_agentplatform/frontend/src/components/SectionTabs.vue b/eai_agentplatform/frontend/src/components/SectionTabs.vue
index d039d9e..5910fed 100644
--- a/eai_agentplatform/frontend/src/components/SectionTabs.vue
+++ b/eai_agentplatform/frontend/src/components/SectionTabs.vue
@@ -5,26 +5,26 @@
@@ -40,6 +40,7 @@ const props = defineProps({
groups: { type: Array, default: () => [] },
// 当前路由落在哪个组
groupKey: { type: String, default: '' },
+ sectionLabel: { type: String, default: '' },
})
const route = useRoute()
@@ -67,6 +68,13 @@ function openGroup(group) {
if (target) router.push(target.pageRoute)
}
+function formatIndexedLabel(label, index) {
+ const sectionIndex = Number(String(props.sectionLabel || '').match(/^(\d+)\./)?.[1] || 0)
+ const normalizedLabel = String(label || '').replace(/^\d+(?:\.\d+)?\s+/, '')
+ if (!sectionIndex) return normalizedLabel
+ return `${sectionIndex}.${index + 1} ${normalizedLabel}`
+}
+
function isCurrent(path) {
return route.path === path || route.path.startsWith(`${path}/`)
}
diff --git a/eai_agentplatform/frontend/src/components/chat/ChatLayout.vue b/eai_agentplatform/frontend/src/components/chat/ChatLayout.vue
index 4622969..7802921 100644
--- a/eai_agentplatform/frontend/src/components/chat/ChatLayout.vue
+++ b/eai_agentplatform/frontend/src/components/chat/ChatLayout.vue
@@ -22,11 +22,11 @@
🧠
- 专员·技能·APP·连接器
+ 专员·技能·xApp·连接器
🧩
- 长程APP
+ 长程 xApp
📚
diff --git a/eai_agentplatform/frontend/src/components/chat/CurrentObjectChip.vue b/eai_agentplatform/frontend/src/components/chat/CurrentObjectChip.vue
index 0701af0..500b43f 100644
--- a/eai_agentplatform/frontend/src/components/chat/CurrentObjectChip.vue
+++ b/eai_agentplatform/frontend/src/components/chat/CurrentObjectChip.vue
@@ -78,7 +78,7 @@ const visible = computed(() => Boolean(kind.value))
const label = computed(() => {
if (kind.value === 'specialist') {
- if (isGeneralAssistant.value) return '通用助手'
+ if (isGeneralAssistant.value) return '通用专员'
return currentSpecialist.value?.label || specialistEntry.value?.label || '已选专员'
}
return skillEntry.value?.label || ''
@@ -88,7 +88,7 @@ const badge = computed(() => (kind.value === 'specialist' ? '专' : '技'))
const hint = computed(() => {
if (kind.value === 'specialist') {
- if (isGeneralAssistant.value) return '当前专员:通用助手'
+ if (isGeneralAssistant.value) return '当前专员:通用专员'
return specialistEntry.value ? `当前专员:${label.value} · 点开它的工作台` : `当前专员:${label.value}`
}
return skillEntry.value ? `当前技能:${label.value}` : label.value
diff --git a/eai_agentplatform/frontend/src/components/chat/LlmRoutePicker.vue b/eai_agentplatform/frontend/src/components/chat/LlmRoutePicker.vue
index 03598a5..c4ae813 100644
--- a/eai_agentplatform/frontend/src/components/chat/LlmRoutePicker.vue
+++ b/eai_agentplatform/frontend/src/components/chat/LlmRoutePicker.vue
@@ -67,7 +67,7 @@ onMounted(() => {
}
.llm-route-select {
- width: 92px;
+ width: 176px;
border: none;
outline: none;
background: transparent;
@@ -76,4 +76,8 @@ onMounted(() => {
color: #303133;
cursor: pointer;
}
+
+.llm-route-picker.compact .llm-route-select {
+ width: 156px;
+}
diff --git a/eai_agentplatform/frontend/src/components/chat/OfficialAccountSpecialistPanel.vue b/eai_agentplatform/frontend/src/components/chat/OfficialAccountSpecialistPanel.vue
new file mode 100644
index 0000000..f4ff27f
--- /dev/null
+++ b/eai_agentplatform/frontend/src/components/chat/OfficialAccountSpecialistPanel.vue
@@ -0,0 +1,1406 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
公众号创作专员 · 工作流
+
{{ headSub }}
+
+
+ {{ taskStatusLabel(workflowTask.status) }}
+ {{ workflowTask.id }}
+
+
+
+
+ 当前还没有可展示的流程步骤。
+
+
+
+
{{ step.status === 'completed' ? '✓' : index + 1 }}
+
+
+ {{ step.title }}
+ {{ stepStatusLabel(step.status) }}
+ {{ hhmm(step.finished_at) }}
+
+
{{ step.progress_text || '等待执行' }}
+
{{ stepLatestLog(step) }}
+
+
+ 产出
+ {{ art.title }}
+ {{ artifactTypeLabel(art.artifact_type || art.artifactType) }}
+
+
+
+
+
+
+
+
+
公众号创作专员 · 产物
+
{{ uploadItems.length }} 个上传物 · {{ artifactItems.length }} 个产物
+
+
+
+
+
+ 上传物
+ {{ uploadItems.length }} 个
+
+
+ 当前任务还没有记录上传物。
+
+
+
+ {{ item.title }}
+ {{ item.status }}
+
+
{{ item.detail }}
+
{{ item.meta }}
+
+
+
+
+
+
+
+ 产物
+ {{ artifactItems.length }} 个
+
+
+ 当前任务还没有产物。
+
+
+
+ {{ item.title }}
+ {{ item.status }}
+
+
{{ item.detail }}
+
{{ item.meta }}
+
+
+
+
+
+
+
+
公众号创作专员 · 互动窗
+
在对话右栏里完成配置、选择和生成,不跳独立工作台。
+
+
+ {{ taskStatusLabel(workflowTask.status) }}
+ {{ workflowTask.id }}
+
+
+
+
+
+
+
+
创作流程
+
+
+ {{ item.index }}. {{ item.short }}
+
+
+
+
+
+
+
+
1. 选题
+
{{ stepProgressText('topic_recommendation', '先生成一组可选选题。') }}
+
+
+
+
+
+
+
+
生成选题后,这里会出现 3 个候选。
+
+
+
+
+
+
2. 标题
+
{{ stepProgressText('title_generation', '基于当前选题生成 3 个标题。') }}
+
+
+
+
+
+
+
+
先选题,再生成标题。
+
+
+
+
+
+
3. 提纲
+
{{ stepProgressText('outline_generation', '提纲生成后可直接微调。') }}
+
+
+
+
+
+
+
+
+
+
+
4. 正文
+
{{ stepProgressText('content_creation', '正文生成后可继续做配图和导出。') }}
+
+
+
+
+
+
+
+
+
+
+
交付整理
+
+
+
+
+
+
+
+
+
+
配图提示词
+
+
+
{{ item.section }}
+
{{ item.prompt }}
+
+
+
+
+
+
+
+
+
+
分页 MD
+
{{ pageMarkdown }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eai_agentplatform/frontend/src/components/chat/PlusMenu.vue b/eai_agentplatform/frontend/src/components/chat/PlusMenu.vue
index 49b8894..a2450f7 100644
--- a/eai_agentplatform/frontend/src/components/chat/PlusMenu.vue
+++ b/eai_agentplatform/frontend/src/components/chat/PlusMenu.vue
@@ -121,7 +121,6 @@ import { ElMessageBox } from 'element-plus'
import {
ArrowRight, Avatar, Check, Connection, EditPen, MagicStick, Plus, Search, Setting, UploadFilled,
} from '@element-plus/icons-vue'
-import { getStaticSkillByKey } from '@/skills/core/builtin'
import { listConnectors } from '@/connectors/api/connector'
import { useAuthStore } from '@/store/auth'
import { useSkillCatalogStore } from '@/skills/store/skillCatalog'
@@ -179,19 +178,6 @@ function routable(path) {
}
}
-const generalAssistantItem = computed(() => {
- const item = skillCatalog.getByKey('smart-assistant') || getStaticSkillByKey('smart-assistant')
- if (!item) return null
- return {
- key: GENERAL_ASSISTANT_KEY,
- label: item.label,
- caption: item.caption,
- display_code: '专00',
- eailogic_code: 'EAI-S-00',
- objectEntryRoute: item.objectEntryRoute,
- }
-})
-
const entries = computed(() => {
const list = []
if (props.modes.length) {
@@ -211,20 +197,16 @@ const entries = computed(() => {
type: 'resource',
searchPlaceholder: '搜索专员',
empty: '暂无可用专员',
- items: [
- ...(generalAssistantItem.value ? [generalAssistantItem.value] : []),
- ...specialistCatalog.specialists
- .filter((specialist) => routable(specialist.objectEntryRoute))
- // key 要在切换时用:选了专员就等于把当前任务改挂到它名下。
- .map((specialist) => ({
- key: specialist.key,
- label: specialist.label,
- caption: specialist.summary,
- display_code: specialist.display_code,
- eailogic_code: specialist.eailogic_code,
- objectEntryRoute: specialist.objectEntryRoute,
- })),
- ],
+ items: specialistCatalog.specialists
+ // key 要在切换时用:选了专员就等于把当前任务改挂到它名下。
+ .map((specialist) => ({
+ key: specialist.key,
+ label: specialist.label,
+ caption: specialist.summary,
+ display_code: specialist.display_code,
+ eailogic_code: specialist.eailogic_code,
+ objectEntryRoute: specialist.objectEntryRoute,
+ })),
manage: { label: '专员市场', pageRoute: '/market' },
})
}
diff --git a/eai_agentplatform/frontend/src/components/chat/SpecialistPanel.vue b/eai_agentplatform/frontend/src/components/chat/SpecialistPanel.vue
index 39908cb..bc5e0e8 100644
--- a/eai_agentplatform/frontend/src/components/chat/SpecialistPanel.vue
+++ b/eai_agentplatform/frontend/src/components/chat/SpecialistPanel.vue
@@ -11,7 +11,11 @@
靠推断(见 utils/specialistFlow.js 的 markFlowState),并且推断出来的那一步
在界面上要标出来,不能让它冒充实测进度。
-->
-
+
+
@@ -93,7 +166,7 @@ async function load() {
const exams = res.data || []
items.value = exams
.filter((p) => p.type === 'formal')
- .slice((page.value - 1) * size.value, (page.value.value * size.value))
+ .slice((page.value - 1) * size.value, page.value * size.value)
total.value = exams.filter((p) => p.type === 'formal').length
} catch (e) {
items.value = []
@@ -118,6 +191,7 @@ async function startExam(p) {
try {
const res = await examStart(p.id)
const d = res.data
+ result.value = null
sessionId.value = d.session_id
currentPaper.value = { name: d.exam_name }
questions.value = d.questions || []
@@ -150,6 +224,11 @@ async function submit() {
}
}
+function closeDialog() {
+ quizVisible.value = false
+ result.value = null
+}
+
function formatAnswer(correct) {
if (!correct) return '—'
if (correct.length === 1 && (correct[0] === 'true' || correct[0] === 'false')) {
@@ -158,6 +237,13 @@ function formatAnswer(correct) {
return correct.join('、')
}
+function formatUserAnswer(ans) {
+ if (ans == null || ans === '') return '未作答'
+ if (Array.isArray(ans)) return ans.join('、')
+ if (ans === 'true' || ans === 'false') return ans === 'true' ? '正确' : '错误'
+ return String(ans)
+}
+
onMounted(load)
@@ -175,4 +261,39 @@ onMounted(load)
display: flex;
justify-content: flex-start;
}
+.paper-meta,
+.result-summary {
+ margin-bottom: 12px;
+}
+.result-meta {
+ margin-top: 8px;
+ color: #606266;
+}
+.question-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+}
+.question-item:last-child {
+ border-bottom: none;
+}
+.question-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 500;
+ margin-bottom: 10px;
+}
+.question-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.detail-line {
+ font-size: 13px;
+ color: #606266;
+ margin-bottom: 4px;
+}
+.detail-line.explain {
+ color: #909399;
+}
diff --git a/eai_agentplatform/frontend/src/views/exam/ExamMyRecordPage.vue b/eai_agentplatform/frontend/src/views/exam/ExamMyRecordPage.vue
index 5f20507..a496cd5 100644
--- a/eai_agentplatform/frontend/src/views/exam/ExamMyRecordPage.vue
+++ b/eai_agentplatform/frontend/src/views/exam/ExamMyRecordPage.vue
@@ -110,7 +110,7 @@ async function load() {
const records = res.data || []
items.value = records
.filter((r) => !filters.examName || (r.exam_name || '').includes(filters.examName))
- .slice((page.value - 1) * size.value, page.value.value * size.value)
+ .slice((page.value - 1) * size.value, page.value * size.value)
total.value = records.length
} catch (e) {
items.value = []
diff --git a/eai_agentplatform/frontend/src/views/exam/ExamSelfTestPage.vue b/eai_agentplatform/frontend/src/views/exam/ExamSelfTestPage.vue
index 8e52f37..5f13f49 100644
--- a/eai_agentplatform/frontend/src/views/exam/ExamSelfTestPage.vue
+++ b/eai_agentplatform/frontend/src/views/exam/ExamSelfTestPage.vue
@@ -60,6 +60,79 @@
/>
+
+
+
+
+
+ {{ result.score }} / {{ result.total_score }} 分
+
+
+ {{ result.passed ? '已通过' : '未通过' }},答对 {{ result.correct_count }} 题,答错 {{ result.wrong_count }} 题
+
+
+
+ 答题解析
+
+
+ {{ idx + 1 }}. {{ q.stem }}
+
+ {{ q.is_correct ? '正确' : '错误' }}
+
+
+
你的答案:{{ formatUserAnswer(q.user_answer) }}
+
正确答案:{{ formatAnswer(q.correct_answer) }}
+
解析:{{ q.explanation }}
+
+
+
+
+
+ 共 {{ questions.length }} 题,请完成后提交。
+
+
+
{{ idx + 1 }}. {{ q.stem }}
+
+
+
+ {{ opt.key }}. {{ opt.text }}
+
+
+
+
+
+ {{ opt.key }}. {{ opt.text }}
+
+
+
+
+ 正确
+ 错误
+
+
+
+
+
+
+
+
+
+
@@ -93,7 +166,7 @@ async function load() {
const exams = res.data || []
items.value = exams
.filter((p) => p.type === 'self_test')
- .slice((page.value - 1) * size.value, page.value.value * size.value)
+ .slice((page.value - 1) * size.value, page.value * size.value)
total.value = exams.filter((p) => p.type === 'self_test').length
} catch (e) {
items.value = []
@@ -118,6 +191,7 @@ async function startExam(p) {
try {
const res = await examStart(p.id)
const d = res.data
+ result.value = null
sessionId.value = d.session_id
currentPaper.value = { name: d.exam_name }
questions.value = d.questions || []
@@ -150,6 +224,11 @@ async function submit() {
}
}
+function closeDialog() {
+ quizVisible.value = false
+ result.value = null
+}
+
function formatAnswer(correct) {
if (!correct) return '—'
if (correct.length === 1 && (correct[0] === 'true' || correct[0] === 'false')) {
@@ -158,6 +237,13 @@ function formatAnswer(correct) {
return correct.join('、')
}
+function formatUserAnswer(ans) {
+ if (ans == null || ans === '') return '未作答'
+ if (Array.isArray(ans)) return ans.join('、')
+ if (ans === 'true' || ans === 'false') return ans === 'true' ? '正确' : '错误'
+ return String(ans)
+}
+
onMounted(load)
@@ -175,4 +261,39 @@ onMounted(load)
display: flex;
justify-content: flex-start;
}
+.paper-meta,
+.result-summary {
+ margin-bottom: 12px;
+}
+.result-meta {
+ margin-top: 8px;
+ color: #606266;
+}
+.question-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+}
+.question-item:last-child {
+ border-bottom: none;
+}
+.question-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 500;
+ margin-bottom: 10px;
+}
+.question-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.detail-line {
+ font-size: 13px;
+ color: #606266;
+ margin-bottom: 4px;
+}
+.detail-line.explain {
+ color: #909399;
+}
diff --git a/eai_agentplatform/frontend/src/views/exam/LeaderboardPage.vue b/eai_agentplatform/frontend/src/views/exam/LeaderboardPage.vue
index f03b80f..6259704 100644
--- a/eai_agentplatform/frontend/src/views/exam/LeaderboardPage.vue
+++ b/eai_agentplatform/frontend/src/views/exam/LeaderboardPage.vue
@@ -96,9 +96,9 @@ async function load() {
if (filters.department) {
items.value = allItems
.filter((r) => r.department === filters.department)
- .slice((page.value - 1) * size.value, page.value.value * size.value)
+ .slice((page.value - 1) * size.value, page.value * size.value)
} else {
- items.value = allItems.slice((page.value - 1) * size.value, page.value.value * size.value)
+ items.value = allItems.slice((page.value - 1) * size.value, page.value * size.value)
}
myRank.value = d.my_rank || 0
total.value = d.total || allItems.length
diff --git a/eai_agentplatform/frontend/src/views/exam/MyCertificatesPage.vue b/eai_agentplatform/frontend/src/views/exam/MyCertificatesPage.vue
index 3bd4526..1f3fa7d 100644
--- a/eai_agentplatform/frontend/src/views/exam/MyCertificatesPage.vue
+++ b/eai_agentplatform/frontend/src/views/exam/MyCertificatesPage.vue
@@ -116,7 +116,7 @@ async function load() {
const records = res.data || []
items.value = records
.filter((c) => !filters.examName || (c.exam_name || '').includes(filters.examName))
- .slice((page.value - 1) * size.value, page.value.value * size.value)
+ .slice((page.value - 1) * size.value, page.value * size.value)
total.value = records.length
} catch (e) {
items.value = []
diff --git a/eai_agentplatform/frontend/src/views/exam/MyMistakesPage.vue b/eai_agentplatform/frontend/src/views/exam/MyMistakesPage.vue
index 36058b8..7e674a7 100644
--- a/eai_agentplatform/frontend/src/views/exam/MyMistakesPage.vue
+++ b/eai_agentplatform/frontend/src/views/exam/MyMistakesPage.vue
@@ -112,7 +112,7 @@ async function load() {
if (filters.resolved !== null && m.resolved !== filters.resolved) return false
return true
})
- .slice((page.value - 1) * size.value, page.value.value * size.value)
+ .slice((page.value - 1) * size.value, page.value * size.value)
total.value = records.length
} catch (e) {
items.value = []
diff --git a/eai_agentplatform/frontend/src/views/me/MySettingsPage.vue b/eai_agentplatform/frontend/src/views/me/MySettingsPage.vue
index d7cf8dd..25c9565 100644
--- a/eai_agentplatform/frontend/src/views/me/MySettingsPage.vue
+++ b/eai_agentplatform/frontend/src/views/me/MySettingsPage.vue
@@ -3,7 +3,7 @@
@@ -31,7 +31,7 @@
{{ identityInfo.hireBatch }}
{{ identityInfo.identityHint }}
- 组织身份用于项目、知识库、长程 APP 的权限与默认上下文判断。
+ 组织身份用于项目、知识库、长程 xApp 的权限与默认上下文判断。
@@ -42,7 +42,7 @@
-
+
diff --git a/eai_agentplatform/frontend/src/views/system/AdminOverviewPage.vue b/eai_agentplatform/frontend/src/views/system/AdminOverviewPage.vue
deleted file mode 100644
index af0785d..0000000
--- a/eai_agentplatform/frontend/src/views/system/AdminOverviewPage.vue
+++ /dev/null
@@ -1,504 +0,0 @@
-
-
-
- Governance Center
- 后台管理与任务治理
-
-
-
-
-
-
-
-
待处理任务
-
把现在最该动手的事项放到前面。
-
-
-
-
-
-
-
-
-
治理入口
-
按任务语义进入,不再记后台菜单。
-
-
-
-
-
-
-
-
-
-
-
风险提醒
-
只保留需要你尽快关注的几项。
-
-
-
-
部门
-
-
-
-
{{ item.department }}
-
{{ item.employee_count }} 人 · 已读 {{ item.company_rate }}%
-
-
- {{ item.pass_rate }}%
-
-
-
-
暂无部门风险项
-
-
-
-
岗位
-
-
-
-
{{ item.position_name }}
-
{{ item.employee_count }} 人 · 参考 {{ item.taken_count }} 次
-
-
- {{ item.pass_rate }}%
-
-
-
-
暂无岗位风险项
-
-
-
-
-
-
-
- 连接器 {{ liveConnectorCount }}
- AI 用户 {{ aiUsageSummary.userCount }}
- 题目 {{ dashboard.question_count }}
- 考试 {{ dashboard.formal_paper_count }}
-
-
-
-
-
-
{{ item.name }}
-
{{ item.desc }}
-
-
{{ item.status }}
-
-
- 暂无连接器状态
-
-
-
-
-
-
-
-
diff --git a/eai_agentplatform/frontend/src/views/system/AiAdminPage.vue b/eai_agentplatform/frontend/src/views/system/AiAdminPage.vue
new file mode 100644
index 0000000..cd64812
--- /dev/null
+++ b/eai_agentplatform/frontend/src/views/system/AiAdminPage.vue
@@ -0,0 +1,513 @@
+
+
+
+
+
6.4 AI后台管理
+
+
+
总调用
+
{{ usageSummary.total_calls }}
+
当前筛选时间窗
+
+
+
+
+
+
+
+
+
+ 对话路由
+ {{ aiChatRoutes.length }}
+ 可用于 Agent 与对话场景
+
+
+ Embedding 路由
+ {{ aiEmbedRoutes.length }}
+ 可用于检索与向量任务
+
+
+ Provider 数
+ {{ providerCount }}
+ 已纳入密钥状态管理
+
+
+ 默认对话路由
+ {{ aiConfig.default_route || '未设置' }}
+ 保存后热生效
+
+
+
+
+
+
+
Agent → 路由映射
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Provider 密钥状态
+
+
+ {{ ok ? '已配置' : '未配置' }}
+ {{ provider }}
+
+
+
密钥明文不回显;配置见 config/ai_secrets.json(仅示例文件入库,真实密钥不入库)
+
+
+
+
+
+
+
+
+
+ 热重载
+ 保存 AI 配置
+
+
+
+
+
+
+
+ 时间窗
+
+ 近 30 天
+ 近 90 天
+ 近 365 天
+
+ 分组
+
+ 按月
+ 按类型
+ 按 Provider
+
+
+
+
+
+
+ 总调用
+ {{ usageSummary.total_calls }}
+ 所有请求次数
+
+
+ 成功
+ {{ usageSummary.success_calls }}
+ 成功响应次数
+
+
+ 失败
+ {{ usageSummary.failed_calls }}
+ 需要排查的失败调用
+
+
+ 总消耗点数
+ {{ usageSummary.total_credits }}
+ 组织级成本观察视角
+
+
+
+
+
+
+ 按日调用趋势
+
+
+
+
+
+ 成功 / 失败占比
+
+
+
+
+
+ 类型分布
+
+
+
+
+
+
+ 分组明细({{ groupLabel }})
+
+
+ {{ bucketLabel(row.bucket) }}
+
+
+
+
+
+
+
+
+
+
+ 按用户用量与剩余点数
+
+
+
+
+
+
+ {{ row.role === 'admin' ? '管理员' : '员工' }}
+
+
+
+
+
+
+
+
+ {{ row.ai_points }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eai_agentplatform/frontend/src/views/system/AiUsagePage.vue b/eai_agentplatform/frontend/src/views/system/AiUsagePage.vue
index fbe2f74..e9c2e96 100644
--- a/eai_agentplatform/frontend/src/views/system/AiUsagePage.vue
+++ b/eai_agentplatform/frontend/src/views/system/AiUsagePage.vue
@@ -3,11 +3,7 @@
运行治理
-
AI 用量
-
- 统一查看组织级 AI 调用量、成功率、用户消耗和 Provider 分布。
- 这页服务于资源治理、路由优化和异常排查,而不只是简单报表。
-
+
AI 用量分析
总调用
diff --git a/eai_agentplatform/frontend/src/views/system/CompanyConfigPage.vue b/eai_agentplatform/frontend/src/views/system/CompanyConfigPage.vue
deleted file mode 100644
index c0e63ae..0000000
--- a/eai_agentplatform/frontend/src/views/system/CompanyConfigPage.vue
+++ /dev/null
@@ -1,164 +0,0 @@
-
-
-
-
-
系统治理
-
公司配置
-
- 这里维护组织级基础信息,用于新员工 onboarding、公司介绍 APP、通知触达与系统默认展示。
- 它不是孤立配置表,而是平台组织身份的一部分。
-
-
- 保存配置
-
-
-
-
配置项
-
{{ companyItems.length }}
-
组织级基础参数
-
-
-
-
-
- 公司名称
- {{ values.company_name || '未设置' }}
- 对外展示的组织名称
-
-
- 管理员邮箱
- {{ values.admin_email || '未设置' }}
- 用于通知和运营联系
-
-
- 公司介绍长度
- {{ introLength }}
- 字符数,用于公司介绍 APP 内容基础
-
-
- 内容状态
- {{ introLength ? '已配置' : '待完善' }}
- 建议补齐 onboarding 主文案
-
-
-
-
-
-
-
组织基础信息
-
这些信息会影响公司介绍、通知触达和组织身份展示。
-
-
-
-
-
-
-
-
-
- {{ item.config_key }}
-
-
-
- {{ item.description }}
-
-
-
-
- 保存配置
-
-
-
-
-
-
-
-
-
-
-
diff --git a/eai_agentplatform/frontend/src/views/system/OrganizationManagementPage.vue b/eai_agentplatform/frontend/src/views/system/OrganizationManagementPage.vue
new file mode 100644
index 0000000..0b4299b
--- /dev/null
+++ b/eai_agentplatform/frontend/src/views/system/OrganizationManagementPage.vue
@@ -0,0 +1,789 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.description || '—' }}
+
+
+
+
+
+ {{ row.status === 'active' ? '启用' : '停用' }}
+
+
+
+
+
+ 编辑
+
+ {{ row.status === 'active' ? '停用' : '启用' }}
+
+ 删除
+
+
+
+
+
+
+
+
+ 按部门学情统计(员工 {{ statsTotalEmployees }} 人)
+
+
+
+
+
+ {{ row.company_rate }}%
+
+
+ {{ row.product_avg }} / {{ row.course_avg }}
+
+
+ {{ row.formal_passed }}/{{ row.formal_count }} 通过
+
+
+
+
+ {{ row.pass_rate }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ positionKnowledgeCount[row.id] || 0 }} 条
+
+
+
+
+
+
+ {{ labelOf(POSITION_STATUS, row.status) }}
+
+
+
+
+
+ 编辑
+ 知识映射
+ 考试蓝图
+ 停用
+
+
+
+
+
+
+
+
+
+
+
+ 公司名称
+ {{ companyValues.company_name || '未设置' }}
+ 对外展示的组织名称
+
+
+ 管理员邮箱
+ {{ companyValues.admin_email || '未设置' }}
+ 用于通知和运营联系
+
+
+ 公司介绍长度
+ {{ introLength }}
+ 字符数,用于公司介绍 xApp 内容基础
+
+
+ 内容状态
+ {{ introLength ? '已配置' : '待完善' }}
+ 建议补齐 onboarding 主文案
+
+
+
+
+
+
+
+
+
+ {{ item.config_key }}
+
+
+
+ {{ item.description }}
+
+
+
+
+ 保存配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 启用
+ 停用
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+ 圈定该岗位应掌握的知识范围:至少配置一条「知识域」;可细化到具体课程/产品;级别 L1-L4 用于分级标注。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+ 取消
+ 保存映射
+
+
+
+
+
+ 蓝图规定岗位考试的题型构成。配置蓝图后,该岗位的考试优先按蓝图组卷。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+ 取消
+ 保存蓝图
+
+
+
+
+
+
+
+
diff --git a/eai_agentplatform/frontend/src/views/system/SystemConfigPage.vue b/eai_agentplatform/frontend/src/views/system/SystemConfigPage.vue
index 3d8e469..a8d7935 100644
--- a/eai_agentplatform/frontend/src/views/system/SystemConfigPage.vue
+++ b/eai_agentplatform/frontend/src/views/system/SystemConfigPage.vue
@@ -2,31 +2,22 @@
-
系统治理
-
系统配置
-
维护文件存储、身份会话、AI 路由与 Provider 密钥状态。
+
6.3 系统配置
-
-
-
+
-
-
+
+
@@ -38,7 +29,7 @@
-
+
@@ -49,77 +40,9 @@
-
-
-
-
-
-
-
-
Agent → 路由映射
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Provider 密钥状态
-
-
- {{ ok ? '已配置' : '未配置' }}
- {{ provider }}
-
-
-
密钥明文不回显;配置见 config/ai_secrets.json(仅示例文件入库,真实密钥不入库)
-
-
-
-
-
-
-
-
-
-
- 热重载
- 保存 AI 配置
-
-
-
-
+
保存配置
@@ -132,9 +55,7 @@
@@ -288,42 +134,6 @@ onMounted(() => {
line-height: 1.5;
margin-top: 4px;
}
-.route-selector-section {
- background: #fafafa;
- border-radius: 6px;
- padding: 16px 20px;
- margin-bottom: 12px;
-}
-.section-label {
- font-size: 13px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 12px;
-}
-.secret-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 12px 24px;
- margin-bottom: 8px;
-}
-.secret-item {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.secret-name {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 13px;
- color: #303133;
-}
-.json-collapse {
- margin-top: 4px;
-}
-.json-editor :deep(textarea) {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- line-height: 1.6;
-}
.save-bar {
margin-top: 20px;
display: flex;
diff --git a/eai_agentplatform/frontend/src/views/system/UserManagementPage.vue b/eai_agentplatform/frontend/src/views/system/UserManagementPage.vue
index d3c18f8..08c6979 100644
--- a/eai_agentplatform/frontend/src/views/system/UserManagementPage.vue
+++ b/eai_agentplatform/frontend/src/views/system/UserManagementPage.vue
@@ -2,26 +2,13 @@
-
组织治理
-
用户管理
-
维护组织成员、岗位归属和学习推进。
-
- 新增用户
- 查看学习进度
-
+
6.1 用户管理
-
-
-
-
-
+
+
+
@@ -130,7 +117,7 @@
-
+
diff --git a/eai_agentplatform/frontend/src/views/workbench/DocumentCenterPage.vue b/eai_agentplatform/frontend/src/views/workbench/DocumentCenterPage.vue
index ab37134..48c6722 100644
--- a/eai_agentplatform/frontend/src/views/workbench/DocumentCenterPage.vue
+++ b/eai_agentplatform/frontend/src/views/workbench/DocumentCenterPage.vue
@@ -221,7 +221,7 @@ function parseContext(value) {
function lookupSpecialistLabel(key) {
const normalized = String(key || '').trim()
- if (!normalized) return '通用助手'
+ if (!normalized) return '通用专员'
const skill = skillCatalog.getByKey(normalized)
if (skill) return skill.label
const specialist = specialistCatalog.getByKey(normalized)
diff --git a/eai_agentplatform/frontend/src/views/workbench/MarketPage.vue b/eai_agentplatform/frontend/src/views/workbench/MarketPage.vue
index b60043d..3aaf552 100644
--- a/eai_agentplatform/frontend/src/views/workbench/MarketPage.vue
+++ b/eai_agentplatform/frontend/src/views/workbench/MarketPage.vue
@@ -133,7 +133,7 @@
{{ row.state === 'active' ? '停用' : '启用' }}
- 删除
+ 删除
@@ -146,7 +146,7 @@
-
+
@@ -480,12 +480,14 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import {
createSpecialist,
deleteSpecialist,
+ getSpecialistRemovePlan,
getSpecialistSummary,
listSpecialists,
updateSpecialist,
} from '@/specialists/api/specialist'
+import { staticSpecialistCatalog } from '@/specialists/core/builtin'
import { useAuthStore } from '@/store/auth'
-import { useTaskRuntimeStore } from '@/store/taskRuntime'
+import { GENERAL_ASSISTANT_KEY, useTaskRuntimeStore } from '@/store/taskRuntime'
import { openSpecialistInWorkbench } from '@/utils/workbenchNavigation'
import { WORKBENCH_ROUTE } from '@/config/workbench'
@@ -545,9 +547,15 @@ const form = reactive({
sort_order: 0,
})
+const isSystemGeneralSpecialist = computed(() => form.key === GENERAL_ASSISTANT_KEY)
+const builtinSpecialistLabelMap = new Map(
+ staticSpecialistCatalog.map((item) => [item.key, item.label])
+)
+
const tableItems = computed(() => remoteItems.value.map(item => ({
...item,
- object_entry_route: item.object_entry_route || WORKBENCH_ROUTE,
+ label: displaySpecialistLabel(item),
+ object_entry_route: item.object_entry_route || WORKBENCH_ROUTE,
tierLabel: item.tier === 'generic' ? '通用' : '行业',
specialistModeLabel: item.specialist_mode === 'adw' ? 'ADW' : 'DW',
})))
@@ -559,7 +567,7 @@ function normalizeSpecialistCard(item) {
return {
id: item.id,
key: item.key,
- title: item.label,
+ title: displaySpecialistLabel(item),
tag: item.market_tag,
desc: item.summary,
meta: `${item.version || 'v1.0'} · ${item.connector_scope || `${item.stage} · ${item.work_status}`}`,
@@ -573,6 +581,22 @@ function normalizeSpecialistCard(item) {
}
}
+function displaySpecialistLabel(item) {
+ return builtinSpecialistLabelMap.get(item?.key) || item?.label
+}
+
+function summarizeSpecialistRemovePlan(planPayload) {
+ const payload = planPayload?.data || planPayload || {}
+ const plan = payload?.plan || {}
+ const refs = plan?.RuntimeRefs || {}
+ return [
+ `删除对象:${payload?.label || plan?.Label || payload?.key || '-'}`,
+ `对象类型:${payload?.is_builtin ? '内置专员' : '运行时专员'}`,
+ `引用情况:xApp ${refs?.XAppKeys?.length || 0},项目 ${refs?.ProjectIDs?.length || 0},任务 ${refs?.TaskCount || 0},运行 ${refs?.TaskRunCount || 0},工件 ${refs?.ArtifactCount || 0},调用日志 ${refs?.AICallCount || 0}`,
+ `退场策略:${plan?.Policy || '未设置'}`,
+ ].join('\n')
+}
+
function resetForm() {
form.id = null
form.key = ''
@@ -627,7 +651,7 @@ function openSpecialistWorkspace(row) {
function openEdit(row) {
form.id = row.id
form.key = row.key
- form.label = row.label
+ form.label = displaySpecialistLabel(row)
form.tier = row.tier
form.specialist_mode = row.specialist_mode || 'dw'
form.object_entry_route = row.object_entry_route || WORKBENCH_ROUTE
@@ -748,8 +772,18 @@ async function toggleStatus(row) {
}
async function remove(row) {
+ if (row.key === GENERAL_ASSISTANT_KEY) {
+ ElMessage.warning('通用专员是系统保留专员,不能删除')
+ return
+ }
try {
- await ElMessageBox.confirm(`确定删除专员「${row.label}」吗?删除后将不再出现在市场和首页。`, '删除专员', { type: 'warning' })
+ const planRes = await getSpecialistRemovePlan(row.id)
+ const summaryText = summarizeSpecialistRemovePlan(planRes)
+ if (planRes?.data?.plan?.Blocked) {
+ await ElMessageBox.alert(`${summaryText}\n\n当前仍有运行时引用,暂时不能删除。`, '删除前检查', { type: 'warning' })
+ return
+ }
+ await ElMessageBox.confirm(`${summaryText}\n\n确定删除专员「${displaySpecialistLabel(row)}」吗?删除后将不再出现在市场和首页。`, '删除专员', { type: 'warning' })
await deleteSpecialist(row.id)
ElMessage.success('专员已删除')
await loadAll()
diff --git a/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogDetailPage.vue b/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogDetailPage.vue
index aa53dbc..2e80334 100644
--- a/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogDetailPage.vue
+++ b/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogDetailPage.vue
@@ -126,22 +126,22 @@ const detail = computed(() => {
if (resolvedCatalogType.value === 'xapps') {
const xapp = xappCatalog.getByKey(resolvedObjectKey.value)
- if (!xapp) return notFoundDetail('APP')
+ if (!xapp) return notFoundDetail('xApp')
const isOrchestrated = xapp.xappMode === 'orchestrated'
return {
title: xapp.label,
display_code: xapp.display_code,
eailogic_code: xapp.eailogic_code,
subtitle: isOrchestrated ? '更偏向组织流程与协同推进' : '更偏向直接执行具体内容',
- metaText: `${xapp.marketTag} · ${isOrchestrated ? '编排型 APP' : '执行型 APP'}`,
- badge: 'APP',
+ metaText: `${xapp.marketTag} · ${isOrchestrated ? '编排型 xApp' : '执行型 xApp'}`,
+ badge: 'xApp',
avatar: xapp.label.slice(0, 1),
color: xapp.color || '#7c8cff',
description: xapp.description,
tags: normalizeArray(xapp.tags),
sceneTitle: '适合什么时候用',
sceneText: xapp.summary,
- promptTitle: 'APP 帮你做',
+ promptTitle: 'xApp 帮你做',
prompts: normalizeArray(xapp.prompts),
}
}
diff --git a/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogPage.vue b/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogPage.vue
index 1cb1854..285261a 100644
--- a/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogPage.vue
+++ b/eai_agentplatform/frontend/src/views/workbench/ObjectCatalogPage.vue
@@ -59,6 +59,14 @@
+
+
+
+
@@ -187,12 +204,15 @@
diff --git a/eai_agentplatform/frontend/src/xapps/views/TrainingXAppShell.vue b/eai_agentplatform/frontend/src/xapps/views/TrainingXAppShell.vue
index 38d9f3d..0f6b60f 100644
--- a/eai_agentplatform/frontend/src/xapps/views/TrainingXAppShell.vue
+++ b/eai_agentplatform/frontend/src/xapps/views/TrainingXAppShell.vue
@@ -2,9 +2,9 @@
-
长程 APP
-
内部培训APP
-
把销售培训、产品知识和公司介绍收在同一个 xapp 下,不再作为并行页面散落在主架构外侧。
+
长程 xApp
+
内部培训 xApp
+
把销售培训、产品知识和公司介绍收在同一个 xApp 里,让培训入口、内容状态和学习动作都落在同一运行面。
EAI-X
@@ -23,31 +23,166 @@
-
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+ {{ item.hint }}
+
+
+
+
+
+ 模块总览
+
+
+
+
+ 当前状态
+
+
+
{{ item.label }}
+
{{ item.value }}
+
+
+ {{ statusTip }}
+
+
+
+
diff --git a/eai_agentplatform/frontend/src/xapps/views/XAppDirectoryPage.vue b/eai_agentplatform/frontend/src/xapps/views/XAppDirectoryPage.vue
index b32f22c..efd6718 100644
--- a/eai_agentplatform/frontend/src/xapps/views/XAppDirectoryPage.vue
+++ b/eai_agentplatform/frontend/src/xapps/views/XAppDirectoryPage.vue
@@ -2,12 +2,12 @@
-
APP 目录
-
APP目录
+
xApp 目录
+
xApp目录
{{ xappCards.length }}
- 个已接入 APP
+ 个已接入 xApp
@@ -45,7 +45,7 @@
- 进入 APP
+ 进入 xApp
查看运行面
@@ -62,10 +62,10 @@ const router = useRouter()
const xappCards = [
{
key: 'internal-training',
- title: '内部培训APP',
+ title: '内部培训 xApp',
subtitle: '销售培训 / 产品知识 / 公司介绍',
badge: '已接入',
- desc: '面向内部培训与知识导览的统一 APP,下面承接销售培训、产品知识和公司介绍三个运行模块。',
+ desc: '面向内部培训与知识导览的统一 xApp,下面承接销售培训、产品知识和公司介绍三个运行模块。',
pageRoute: INTERNAL_TRAINING_XAPP_ROOT,
color: '#5b7cff',
meta: ['统一培训入口', '多模块', '持续学习'],
@@ -77,7 +77,7 @@ const xappCards = [
},
{
key: 'internal-exam',
- title: '内部考试APP',
+ title: '内部考试 xApp',
subtitle: '练习 / 正式考试 / 记录 / 证书',
badge: '已接入',
desc: '面向内部培训后的考核与评估场景,统一承接练习、自测、正式考试、成绩记录和证书结果。',