2026-10-09.md 15 KB

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;改为 <SkillDetailModal v-model:open="skillOpen" :skill="skillViewing" :case-id="caseId" />。
  • ★ 顺手修掉上一轮引入的样式冲突:详情抽屉的 &__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 的输出为准,别被这行吓到。