# 「专家页面」设计与实施计划 v2(含技能、工具配置) ## 一、已探明的关键事实 - `agent` 表实体有 `skills_allow_json` / `tools_allow_json` / `skills_deny_json` 字段但**全部无人消费**——正是为本需求预留的;缺的只有 `user_id`(隔离)与 `is_builtin`(内置标记)。 - 智能体**没有任何 CRUD 接口**,照抄 `/models` 模板(AgentModelController + Result 包装 + 前端 modelApi)。 - 会话绑定天然满足:`agent_chat_session.agent_id` 创建时写入(兜底默认 row_id=1),后端无任何修改通道 →「绑定后不能更换」。 - **技能机制**(关键):技能=磁盘上含 `SKILL.md` 的目录,装在 `.agentscope/users/{userId}/skills`(天然按用户隔离);运行时模型只见「名称+描述」目录、正文用 `load_skill_through_path` 按需加载;**`b.skillFilter(SkillFilter.only(名…))` 就是 builder 级按名白名单**,与现有「用户停用技能」overlay 正确叠加。候选技能列表接口 `GET /capability/skills` 已有(前端 capabilityApi.listSkills ✓)。 - **工具机制**:`AgentToolRegistry.registerBusinessTools(toolkit, activeGroups)` 只控制**初始装备**,6 个组全部注册、模型可用 `reset_equipped_tools` 随时换组 → 不构成限制。要做真限制需「按组注册」:只注册专家启用的组(组工具不注册=模型不可见、切不过去)。基础设施工具(execute_sql/render_chart/render_graph 等)始终可用,不纳入配置(非目标)。 - 组元数据已有:`GROUP_LABELS(groupLabel)`、组常量 6 个(person/call/trans/track/graph/file,otg 未注册)、`GET /capability/tools` 每条带 groupCode/groupLabel/groupDescription/defaultActive → **前端工具组勾选无需新接口**。 - ⚠️ AgentService 实例池无失效机制:任何专家配置变更必须 `evictAgentPool(rowId)`。 - 表 DDL 不在仓库(无 Flyway):新列用手写幂等 SQL 放 sql/,用 jshell+postgres 驱动执行。 **默认决策**(沿用上轮推荐):内置专家不可编辑不可删;智能体只从专家页选择,输入框只读显示。 ## 二、数据与后端 ### SQL `sql/agent_expert.sql`(幂等) ```sql ALTER TABLE agent ADD COLUMN IF NOT EXISTS user_id BIGINT; ALTER TABLE agent ADD COLUMN IF NOT EXISTS is_builtin SMALLINT NOT NULL DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_agent_user_id ON agent(user_id); UPDATE agent SET is_builtin = 1, user_id = COALESCE(user_id, 1) WHERE user_id IS NULL; -- 存量→内置 ``` (`skills_allow_json`/`tools_allow_json` 列按实体已存在,脚本里附注释核对:`ALTER TABLE agent ADD COLUMN IF NOT EXISTS skills_allow_json TEXT;` 同 tools_allow_json 兜底) ### 接口 `AgentExpertController`(`/agents`,Result 包装,仿 /models) | 方法 | 语义 | |---|---| | `GET /agents` | 列表:`is_builtin=1 OR user_id=me`;VO 带 `builtin`/`mine` + `skillNames[]` + `toolGroups[]` | | `POST /agents` | 新建(user_id=me, is_builtin=0);body=SaveExpertDTO | | `PUT /agents/{rowId}` | 编辑;**内置拒绝**(400「内置专家不可编辑」);校验归属 | | `DELETE /agents/{rowId}` | 删除;**内置拒绝**(400「内置专家不可删除」);仅物理删自建 | - **SaveExpertDTO**:name(@NotBlank ≤100)、description(≤500)、sysPrompt(@NotBlank)、`skillNames: List`、`toolGroups: List`(校验 toolGroups ⊆ 6 个已注册组名;skillNames 校验存在于用户技能目录,复用 SkillStore.resolveDir)。 - userId 由 controller 取 `StpUtil` 后传 service(便于单测)。 ### AgentService 改动 1. `buildAgent`:读 `entry.getToolsAllowJson()` → 新增 `registerBusinessTools(toolkit, enabledGroups, activeGroups)` 重载(**只注册** enabledGroups 的工具 + createGroup,active=enabled;enabled=null 走旧全注册路径,InsightAgentFactory/MCP/能力枚举零改动);读 `entry.getSkillsAllowJson()` 非空 → `b.skillFilter(SkillFilter.only(名…))`。 2. 新增 `evictAgentPool(Long agentRowId)`:`agentPool.keySet().removeIf(k -> k.contains("-a" + rowId + "-"))`;create/update/delete 专家后调用。 3. `getOrCreateAgent` 加 agent==null 兜底 → 404「智能体不存在或已删除」(被删专家的旧会话再对话得到明确报错)。 ## 三、前端(src/ai) | 文件 | 内容 | |---|---| | 新 `api/agentApi.ts` | listExperts/createExpert/updateExpert/deleteExpert(Result 族,仿 modelApi;雪花 ID 一律字符串) | | `api/types.ts` | +`AiExpert`(含 builtin/mine/skillNames/toolGroups)、`SaveExpertParams` | | 新 `views/aiExpert/index.vue` | 卡片网格页(照 aiModel 骨架:ai-page 壳 + grid + a-spin + 空态 + Modal.confirm 删除);两个分组:**内置专家**(只读卡片:去对话)/**我的专家**(去对话、编辑、删除);删除确认提示「绑定它的会话将无法继续对话」 | | 新 `components/AgentFormModal.vue` | 新建/编辑弹窗(照 ModelFormModal):名称(必填)、描述、系统提示词(必填 textarea)、**工具**(6 业务组勾选:从 `capabilityApi.listTools()` 按 groupCode 聚合,显示中文名/描述/数量;默认勾 person)、**技能**(从 `capabilityApi.listSkills()` 勾选,默认全选=不限制;空数组=不限制语义) | | `views/aiWorkbench/index.vue` | WorkbenchView +`'agent'`;RAIL_ITEMS +「专家」;分支(plugin 的裸 v-else 改 v-else-if);专家页 `@chat` → `view='chat'` + `store.setPendingExpert(expert)` | | `store/chatStream.ts` | +`pendingExpertId/setPendingExpert`;`createSession` 透传 `agentId`(会话就此绑定);绑定后清 pending | | `composables/useAiChatPage.ts` | +experts 加载、`expertName` 计算(已绑定会话→绑定的;否则 pending;兜底「默认专家」) | | `components/ChatComposer.vue` + `ChatPanel.vue` | 输入框加**只读专家 chip**「专家:xxx」(会话已绑定=固定,不可点)——体现绑定后不可更换 | **「去对话」**:同工作台内不走路由,emit → `view='chat'` + pendingExpert(首条消息创建会话时带上 agentId);不动当前会话。 ## 四、交互口径(成文) 1. 卡片操作:**去对话**(预选该专家,下一条消息创建绑定它的新会话)/ **编辑** / **删除**(后两者仅自建)。 2. 会话创建即绑定智能体,UI 无更换入口;后端本就没有改 agent_id 的接口。 3. 内置专家:全局可见、只有「去对话」,不可编辑/删除(后端双重校验,不靠前端隐藏)。 4. 删除自建专家:确认提示绑定它的会话将无法继续对话;之后这些会话再对话 → 404「智能体不存在或已删除」。 5. 工具配置=组级启用(选中的组注册并初始装备,未选的组模型不可见也切不出去);技能配置=名称白名单(不勾=不限制)。编辑保存后 evict 池,新对话立即生效。 ## 五、实施步骤 1. SQL + `AgentEntity` 加列 → jshell 执行 DDL 并核对既有两列。 2. 后端:DTO/VO/Controller + AgentService(CRUD/隔离/内置校验/evict/两处 buildAgent 配置消费)+ `registerBusinessTools` 三参重载 → 编译。 3. 后端单测:`AgentExpertTest`(隔离过滤:内置+自己的;内置拒改拒删;归属校验;toolGroups/skillNames 校验;evict 生效;被删专家 getOrCreateAgent 兜底)——userId 走参数不依赖 sa-token。 4. 前端:agentApi/types → AgentFormModal(含工具组+技能勾选)→ aiExpert 卡片页 → 工作台接入 + composer 只读 chip → tsc/eslint。 5. E2E(浏览器):新建专家(勾 call 组 + 2 个技能)→ 卡片在「我的」分组 → 去对话 → 提问 → 会话绑定该专家(chip 固置)→ 编辑提示词 → 新会话验证 evict 生效 → 内置专家无编辑/删除 → 删除自建专家 → 旧会话对话报「智能体不存在或已删除」。 ## 六、验收 - 内置:全局可见、仅去对话;自建:仅本人可见(后端过滤)可编辑可删。 - 会话绑定后无任何更换入口;被删专家的会话报错明确。 - 专家配置的工具组/技能在新对话真实生效(未启用组模型不可见;技能目录只见白名单),保存后立即生效(evict)。 - 卡片列表 + 内置/我的分组展示;tsc/eslint/后端测试全绿(存量 8 个失败除外)。