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/ 可选),
新增与编辑都要能管附属文件;查看详情要按文件树并能看文件内容。
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 个全绿。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 写法问题未动)。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 写法未动)。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 内部、会把该行撑高,不会压到下一行。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,避免只读控件看起来像能编辑。readonly 输入框默认仍是白底(只有 disabled 才自动变灰),
凡是「只读展示」的 a-input / a-textarea 都要自己压底色;底色取 --ai-bg-subtle
(= @cb-vscode-input-background,就是项目里输入框的底色 token)。需求:「工具界面工具名都是英文,能不能做成中文;会不会影响 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()。__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 查询」)全部删除。vue-tsc 的 src/ai 零错误。core/logics/workbenchMode.ts 的 setMode 固定跳 WORKBENCH_PATH / BASE_HOME;/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 是新的、会话还没选,那种情况仍交给默认落点选一条,免得落在一屏空白欢迎页。useChatStreamStore 是 Pinia 全局单例(defineStore('ai-chat-stream')),
组件卸载不销毁;且 store.init 有幂等保护(initialized && workspaceId 相同 直接 return),
所以切回工作台时会话选择与消息都还在。vue-tsc 的 src/ai 零错误,src/core 仅 3 个既有错误
(BasicButton 的 Fn、TableAction 的 ActionItem、useECharts 的恒真判断),与本次改动无关。1f7ae24(分支 dev_4)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 的输出为准,别被这行吓到。