# 2026-10-09 ## 启动前后端 - 前端(`ai-electron/frontend`):`./node_modules/.bin/vite dev --port 3100 --strictPort` → http://localhost:3100/ - 后端(`ai-server`):`mvn -pl ai-server spring-boot:run -Dspring-boot.run.workingDirectory=E:/workspace/zsjz-ai` → http://127.0.0.1:8980/js/a (`Started App in 5.6s`,`/sys/health` 返回 200) - `/tmp/mvnw.sh` 已重建(本机 mvn 包装脚本,见 skill §1)。 ## ★ 环境陷阱:宿主注入的 `SERVER__PORT` 会污染 Spring Boot 端口 首次启动报 `PortInUseException: Port 51536 is already in use`,而 `application.yaml` 明明写的是 `8980`。 根因:WorkBuddy 宿主 shell 环境里有 `SERVER__PORT=51536` / `SERVER__HOST=127.0.0.1`。 Spring Boot 的松散绑定把它解析成 `server.port` / `server.address`, **操作系统环境变量优先级高于 application.yaml**,于是后端去绑 51536 —— 而那个端口正是宿主进程自己在监听。 解法(二选一): - `env -u SERVER__PORT -u SERVER__HOST bash /tmp/mvnw.sh ...`(推荐,恢复配置文件原行为) - 或 `-Dspring-boot.run.arguments=--server.port=8980`(命令行参数优先级最高) ⚠️ 同类隐患:任何从本 shell 启动的 Spring Boot / Node 服务,都要先确认没有 `SERVER__*` 之类的宿主变量覆盖配置。 ## 智能工作台「插件与技能」:技能新增/编辑改造(按技能目录规范) 需求:技能按规范落地(`SKILL.md` 必需 + `scripts/` `references/` `assets/` `testcases/` 可选), 新增与编辑都要能管附属文件;查看详情要按**文件树**并能**看文件内容**。 ### 后端(ai-server) - `SkillStore`: - `save(...)` 新增 7 参重载(`files` / `deletePaths`),**overwrite 时不再整目录删重建** —— 旧行为会让编辑一次就丢光附属文件(既有隐患,本次一并修正); - 新增 `readFile` 读单个文件:UTF-8 严格解码判文本/二进制、2MB 预览上限、路径穿越防护; - 新增 `pruneEmptyDirs`(删空文件后不留空目录);旧 5 参重载保留,测试零改动。 - 新增 `SkillFileContentVO`;`SaveSkillDTO` 加 `files`/`deletePaths`; `AgentCapabilityService(+Impl)` 的 createSkill/updateSkill 扩参并新增 `readSkillFile`; Controller 新增 `GET /capability/skills/{name}/file?path=`。 - `SkillStoreTest` 补 4 个用例(附属文件写入/读回、路径穿越与 SKILL.md 拒绝、编辑保留未提交文件、 二进制 base64),15 个全绿。 ### 前端(ai-electron/frontend) - `src/ai/components/SkillEditorModal.vue` 重写为**新增/编辑共用**:按 4 个规范目录分页签管理文件 (增/改/删/改名),二进制文件只读且保存时原样保留;编辑模式读磁盘真实内容预填。 - `src/ai/views/aiPlugin/index.vue`:技能详情抽屉改「左文件树 + 右内容预览」,点任意节点按需拉内容 (图片走 base64 预览);技能卡片加「编辑」入口。 - `capabilityApi.readSkillFile` + `types.ts` 的 `SaveSkillFile` / `SkillFileContent`。 ### 验证 - 后端:`mvn -pl ai-server test -Dtest=SkillStoreTest` 15/15 通过;已重启服务(8980)。 - 前端:`vue-tsc` 对 `src/ai/**` 零错误(core 的 3 个是既有问题);eslint 干净; stylelint 对比 HEAD 无新增错误(aiPlugin 既有 28 处 rgba/alpha 写法问题未动)。 - **未验证**:UI 端到端 —— 接口需登录态,curl 只能确认路由已注册(返回业务码 401)。 ### 补充:技能编辑器补 label(用户反馈「不知道要填什么」) - `SkillEditorModal.vue`:技能名 / 描述 / 正文 / 附属文件名 / 附属文件内容 全部改成 「label 在上、控件在下」(`__field` + `__label` + `__req` 必填星号),placeholder 只留示例。 - `AgentFormModal.vue` 专家弹窗里的技能子面板同样补 label:用 `__sfield` / `__slabel`, **不能复用该文件的 `__label`** —— 那是横向表单行用的(`width:96px; text-align:right`), 而 `__seditor` 是纵向 flex 布局。 - 验证:`vue-tsc` 对 `src/ai` 零错误;eslint 干净;stylelint 对比 HEAD 无新增(AgentFormModal 既有 6 处 rgba 写法未动)。 ### 补充:技能详情抽屉宽度回退 - 用户反馈「查看详情侧边栏太宽」→ 抽屉从 1000 回到原来的 **720**,左文件树从 340 收到 **240**(两栏内容区约 424px)。 - 结论:**详情抽屉宽度沿用 720 这个既有口径**,别再自行放大;两栏布局时左树固定 240px。 ### 补充:技能编辑弹框限高 - 用户反馈「新增/编辑弹框不能一直增高,要用滚动条」→ `SkillEditorModal` 的 BasicModal 加 `:body-style="{ maxHeight: '64vh', overflowY: 'auto' }"`。 - **口径**:本项目的长内容弹框统一用 `maxHeight: 64vh + overflowY: auto` (aiPlugin 的工具包弹框、AgentFormModal、ArtifactPreviewModal 都是这套),新弹框照抄即可。 ### 技能详情改为只读弹框(与编辑弹框同构) - 用户要求「查看详情也用新增编辑一样的弹框,只读即可」→ 新增 `src/ai/components/SkillDetailModal.vue`: 900 宽 BasicModal、`:footer="null"`、bodyStyle 64vh 限高、SKILL.md + 四个规范目录页签全只读; 打开时读详情并**并发**拉全部附属文件内容(文本 content / 图片 base64)。 - 抽 `src/ai/utils/skill.ts`:`SKILL_STD_DIRS` / `SKILL_NAME_RE` / `formatFileSize` / `splitSkillMd` / `topDirOf` / `leafPath` / `groupSkillFiles` / `isImagePath` / `imageMimeOf` —— 详情与编辑弹框共用,避免两边各写一份后漂移。 - `aiPlugin/index.vue` 大幅瘦身:删掉详情抽屉(a-drawer)、`skillTree`、`openDirs/isDirOpen/toggleDir/ dirTag/leafName/formatSize`、`openSkillFile`、`IMAGE_MIME/fileExt/isImageFile/imageMime`、 `readSkill/readSkillFile` 引用、`Drawer` import、`skillLoading`;改为 ``。 - ★ 顺手修掉上一轮引入的样式冲突:详情抽屉的 `&__detail` 与**工具详情弹框**既有的 `&__detail` 同名, LESS 会把两条规则合并(工具的详情布局被带偏)—— 已随抽屉样式一起删除。 - 验证:`vue-tsc` 对 `src/ai` 零错误;eslint 干净;stylelint 26 处(比改动前少 1,全是既有 rgba 写法)。 ### 研判方案「新建/编辑方案」弹框行距修正 - 用户反馈「表单间距太大」→ 根因:`a-form-item` 默认带 `margin-bottom: 24px`(antd 给校验提示留位), 而这个弹框的字段名是**自己画的 label(在 form-item 外面)**,再叠 24px 会把行距从设计意图的 20px(gap 12 + row margin 8)顶成 44px。 - 修法:`&__form :deep(.ant-form-item) { margin-bottom: 0; }`,行距交回 `__row` 统一控制。 校验失败时 explain 在 form-item 内部、会把该行撑高,不会压到下一行。 - **同类坑**:凡是「自绘 label + a-form-item 只包控件」的表单,都要压平 form-item 的 24px 下边距。 - 验证:eslint 干净;stylelint 6 处(与改动前一致,全是既有 rgba 写法)。 ### 研判方案弹框:移除「改技能」入口 - 用户要求「弹框里选择技能那里不可以去编辑技能」→ `AgentFormModal.vue` 删除: skillEditor 子面板(模板 + reactive 状态 + `openSkillEditor/closeSkillEditor/prefillSkill/saveSkill/ splitSkillMd/reloadSkills`)、技能卡片上的铅笔按钮(`__card-ops` / `__iconbtn` 及样式); 顺带清掉不再使用的 `Button` import 与 `SaveSkillParams` 类型引用。 - 文案改为「技能的新增与编辑在『插件与技能 → 技能』页进行」,卡片只剩勾选语义。 - **口径**:研判方案弹框里的技能/工具包一律只做勾选,**任何技能编辑能力都归「插件与技能」页**。 - 验证:`vue-tsc` 对 `src/ai` 零错误;eslint 干净;stylelint 6 处(与改动前一致)。 ### 技能详情弹框:只读控件底纹置灰 - 用户要求「查看技能的时候输入框这些底纹置灰色」→ `SkillDetailModal.vue` 加: `:deep(.ant-input) { &[readonly] { background: var(--ai-bg-subtle, #f5f5f5); ... } }`, hover/focus 一并覆盖为同色 + `box-shadow: none`,避免只读控件看起来像能编辑。 - **口径**:antd 的 `readonly` 输入框默认仍是白底(只有 `disabled` 才自动变灰), 凡是「只读展示」的 a-input / a-textarea 都要自己压底色;底色取 `--ai-bg-subtle` (= `@cb-vscode-input-background`,就是项目里输入框的底色 token)。 - 验证:eslint 干净;stylelint 干净。**视觉未在真实页面确认**(需登录态)。 ### 工具界面中文化(展示名与调用名分离) 需求:「工具界面工具名都是英文,能不能做成中文;会不会影响 agent 调用;有影响就加表字段」。 - **结论:不影响调用,因此未加表字段。** - 调用名是 `@Tool(name)` → `ToolSchema.getName()` → 模型 function name,与页面显示完全无关。 - 反过来,**直接改 `@Tool(name)` 为中文会炸**:OpenAI/兼容 API 的 function name 只接受 `[a-zA-Z0-9_-]{1,64}`;且 `agent_tool_pack.tool_names`、MCP `ToolDefinition.name`、 历史消息里的工具名都按英文名引用。 - **依据(已实测)**:`ai-electron/frontend/src/ai/utils/constants.ts` 的 `TOOL_LABELS` (上个提交 df03094 引入,聊天时间线 / 状态栏 / 产物栏在用)**已完整覆盖**后端全部 64 个 `@Tool`—— 用 grep 提取 `@Tool(name=...)` 与 `TOOL_LABELS` 的 key 做 `comm` 差集,结果为空; 前端多出的 5 条是 MCP 案件工具(list_cases/open_case/current_case/close_case)与框架内置 (reset_equipped_tools),它们不进 `agent_tool` 表。所以工具页复用这张表即可 100% 中文,零后端/DB 改动。 - 改动(全前端): - `aiPlugin/index.vue`:工具卡片主标题、工具包编辑器卡片、工具包详情弹框的工具名 → `toolLabel()`; 英文调用名降为小字等宽副信息(新增 `__tool-code` / `__pack-card-code` / `__pack-tool-code`, 并给 `__card-meta` 加 flex 布局);工具详情弹框标题改中文并新增「调用名(模型按它调用工具)」块; 工具页与工具包编辑器两处搜索框都支持**中文名和英文名同时命中**;placeholder 补中文示例。 - `AgentFormModal.vue`:研判方案弹框里工具包卡片的工具 chips 同样走 `toolLabel()`。 - 验证:eslint 干净;stylelint 与 HEAD 基线**完全一致**(aiPlugin 仅剩既有 8 处 rgba + 1 处 comment-empty-line-before + 1 处 `__mono` 的 font-family 引号;AgentFormModal 仅既有 2 处 rgba); `vue-tsc` 全量 87 个错误全在 call/case/core/graph/otg/person/trans,**`src/ai` 零错误**。 - **遗留隐患**:中文名表在前端,后端新增工具必须同步补 `TOOL_LABELS`(constants.ts 里已写明)。 若日后要「后端权威」,需在 `agent_tool` 加 `display_name` 列(固定表名 DDL 只进 `sql/agent_tool.sql`, 由人工执行 ALTER),但**省不掉**前端那份表 —— 聊天时间线是流式渲染,SSE 事件里只有 name。 ### 补充:工具界面只留中文(撤掉英文原名) - 用户要求「工具列表上不展示英文名称,只展示中文」→ 撤掉上一轮加的英文副信息: `__tool-code` / `__pack-card-code` / `__pack-tool-code` 三处模板节点与样式、工具详情弹框的 「调用名」块、工具卡片的 `:title="toolName"`、工具包 chips 的 `:title`,以及 placeholder / hint 里的英文示例(`stat_trans_big` → 「大额交易」,`execute_sql` → 「执行 SQL 查询」)全部删除。 - **口径**:工具界面上不出现任何英文工具名(含悬停提示与占位文案)。 但搜索仍**同时匹配中英文**——能力保留,只是不再提示;从日志里复制英文名来搜是常见用法。 - 验证:eslint 干净;stylelint 与基线一致(8 rgba / 1 comment / 1 quotes,全既有); `vue-tsc` 的 `src/ai` 零错误。 ### 自主 ⇄ 智能 模式切换:各模式记住自己的停留位置 - 问题:来回切每次都掉回默认落点(切回自主 → 首页;切回智能 → 工作台默认视图)。 - 根因两处: 1. `core/logics/workbenchMode.ts` 的 `setMode` 固定跳 `WORKBENCH_PATH` / `BASE_HOME`; 2. 智能工作台是**单路由全屏壳**(`/workbench`,内部 `view` 不落 URL),切走即卸载,视图回到默认落点。 - 改法: - `core/enums/cacheEnum.ts` 新增 `APP_WORKBENCH_PATH_KEY` / `APP_WORKBENCH_VIEW_KEY`。 - `core/logics/workbenchMode.ts` 新增 `get/setLastTraditionalPath`、`get/setLastWorkbenchView`(**sessionStorage**, 读写带 try/catch 兜底);`setMode` 改为「切走前记下当前页 → 切回时跳上次停留处,没记忆才回默认落点」。 切到 traditional 仍只在「当前在工作台」时才跳 —— 在传统壳点自己那一侧只对齐偏好,不该把人挪走。 - `ai/views/aiWorkbench/index.vue`:`view` 初值 = `?view=` 参数 ?? 上次停留视图 ?? `'chat'`; `watch(view)` 持续写回;`userChoseView` 初值 = 是否有记忆视图;`applyDefaultView` 条件改为 `userChoseView && (view !== 'chat' || hasConversation)` —— 「对话」要靠会话上下文才成立, 刷新后 store 是新的、会话还没选,那种情况仍交给默认落点选一条,免得落在一屏空白欢迎页。 - **为什么用 sessionStorage 而非 localStorage**:只在本次运行内有效,重启后各模式回默认落点, 免得上次遗留的页面在案件上下文变化后变成一次莫名其妙的跳转。 - **会话/消息不用另外存**:`useChatStreamStore` 是 Pinia 全局单例(`defineStore('ai-chat-stream')`), 组件卸载不销毁;且 `store.init` 有幂等保护(`initialized && workspaceId 相同` 直接 return), 所以切回工作台时会话选择与消息都还在。 - 验证:eslint / stylelint 干净;`vue-tsc` 的 `src/ai` 零错误,`src/core` 仅 3 个**既有**错误 (BasicButton 的 `Fn`、TableAction 的 `ActionItem`、useECharts 的恒真判断),与本次改动无关。 - **未验证**:真实来回点击(需登录态 + 案件上下文)。 ### 提交 `1f7ae24`(分支 dev_4) - 范围:本轮工作 6 个文件(SkillDetailModal / aiPlugin / AgentFormModal / cacheEnum / workbenchMode / aiWorkbench)+ 在途的工作台改动。 - `aiWorkbench/index.vue` 里我的改动与**在途改动**(移除「文件管理」视图、去掉顶栏会话 chip) 交织在同一批 hunk 里,无法按路径拆开,因此连配套的 `ai/views/aiCaseData/` 两个已 staged 删除 一起提交(否则会留下两个没人引用的孤儿文件)。commit message 末尾已注明这一点。 - 未提交:`ChatComposer.vue`、`aiExpert/index.vue`(纯 UI 元素顺序调整,与本轮无关)、 `.workbuddy-ai/memory/2026-10-09.md`(本地工作记忆,不入库)。 - ⚠️ 提交命令末尾会出现 `PROGRAM BLOCKED BY SECURITY POLICY`(git 内部调用 `sc.exe` / `reg.exe` 被沙箱黑名单拦下)—— **提交本身已成功**,看 `git log` 的输出为准,别被这行吓到。