Files
pj0235-eai_agentplatform/docs/02_Architecture/AR09_Object_Naming_Standard.md
T
eaiadminandClaude Code e8aedd50d2 refactor: 后端仓库层更名为数据访问层(internal/repository → internal/dal)
「仓库层」是 repository 的直译,中文里与「代码仓库 / git 仓库」同词,
而这一层做的事就是数据访问。名字改成它实际在做的事。

改名口径(纯机械替换,无逻辑改动):
- 包:internal/repository → internal/dal(package repository → package dal)
- 类型:XxxRepo → XxxDAO(TaskRecordDAO / SpecialistDAO / PositionDAO …)
- 变量:xxxRepo → xxxDAO
- import 路径、包限定符、日志前缀 [repository] → [dal] 同步
- 注释里的「仓库层」→「数据访问层」;core.go 包注释补上 DAL/DAO 全称

命名规范补登(AR09 是命名问题的最高依据,改了名就得回去登记):
- AR09 §3.1 术语表新增「数据访问层 dal / DAO」一行
- AR09 §5.6 缩写表新增 DAO / dal —— 原文是「只有下表内的缩写允许使用」,
  不登记就是自己破自己的规矩
- PROJECT_STATE.md 新增 D27 记录本次更名决策

验证:全部在 db 副本上做,生产库 data/eai_agentplatform.db 未触碰。
- 等价性对照:拿 HEAD 源码 + 仅改名 造出第二棵树,两棵树各自起
  httptest 服务跑同一份探针(60 个 GET + 13 个写/回读,覆盖专员/技能/应用/
  任务/交付物/项目/岗位/考试/知识/积分/管理端只读等),逐端点比对响应体:
  73 项里 52 项字节完全一致、21 项仅运行期时间戳不同、内容差异 0。
- 探针非空:往改名后的树注入「SpecialistDAO.List 限 3 条」变异,
  /api/specialists 立刻被抓出 —— 证明上面那个 0 不是没测到。
- 暂存区自洽:把索引整个导出成源码树,go build / go vet / go test ./... 全绿。
- gofmt:因 import 排序变化而错位的 19 个文件已修;另 2 个文件(skill_definition.go、
  seed.go)的格式问题是工作区里别人的在制品带来的,未替其改动。

未纳入本次提交:工作区里正在进行中的「文生语音技能 + 技能展示色/交互卡」
(tts_handlers.go、text_to_speech/manifest.go、skillCatalog.js 等),
以及 router.go / skill_definition.go / seed.go 三个文件里属于该在制品的改动 ——
这三个文件只把「改名那一版」放进索引,工作区原样保留。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 09:19:21 +08:00

49 KiB
Raw Blame History

AR09 对象命名规范(Object Naming Standard)

版本:V1.1 日期:2026-09-18 性质:规范性文件(normative)。「必须 / 禁止」是硬约束,「应当」是默认做法,「建议」可依场景取舍。 与 TOP_CODING_RULES.md 的关系:G03(命名锚定,10 条)给的是硬约束底线;本文件是它的展开与可执行化。 其中 §5.7 前缀规范 与 G03 第 5-10 条互为详略 —— 准则记硬约束,本文件记判据、检查与修复流程。 冲突时以 TOP_CODING_RULES.md 为准,本文件不得放宽 G03。 适用范围:后端 Go、数据库、API、前端 Vue、注释与文档。 读者:写代码的人 + 执行命名清理的 AI。


一、原理:为什么命名值得单独立一份规范

1.1 命名是唯一零成本的文档

注释会过期,文档会失联,只有名字跟着每一次调用出现在读者眼前。 一个名字读错,读者付出的不是"多看一眼",而是先建立错误的心智模型,再用后续所有代码去纠正它。

1.2 名称即契约

大多数命名失误只是难读,但有一类会静默断链:当某个标识符是靠名字派生、靠名字配对、靠名字跨进程传递时,改名就等于改协议,而编译器与测试都不会拦。

本项目已经存在的这类协议:

靠名字建立的协议 位置 改名后果
action_key 由 action_title(中文)派生,前后端靠它配对 specialistFlow.js 的 findLatestRun 改文案 → 配对静默失效,UI 永远显示"未执行"
路由 query 参数 xapp_specialist / xapp_skill / xapp_prompt xappCatalog.js → SmartAssistantPage.vue 写读不同名 → 参数被静默丢弃(2026-09-17 真实发生过,见 §8.1)
专员 / 技能的 key specialist.key、skill_definition.key、ValidSkillKeys 改 key → 任务挂载、技能绑定、种子数据全部对不上
种子数据的 key seedSpecialistRuleFiles 等 拼错 → 启动时只能靠显式校验挡住,否则静默漏种

所以:改名前必须先问"这个名字有没有被别人当协议用"。 这是本文件第七节的由来。

1.3 认知债会复利

一个失真的名字不会自己消失。它会被复制(新代码照着写)、被引用(更多地方依赖这个错名)、被解释(文档里加一段"注意这里其实是指……")。 三个月后,改它的成本是今天的十倍。失真名字的唯一低成本修复窗口,是它刚被写下的那一刻。

1.4 命名错误逃得过编译器和测试

拼写错误的变量名编译不过,语义错误的名字编译得过。 const app = specialistCatalog.getByKey(key) 一切正常,只是每个读它的人都要在脑子里纠正一次。 这类错误没有任何自动化工具能替你发现——除非专门写一个(见第六节)。

1.5 所以要在"新增"那道口子上拦

追改历史代码性价比低。命名规范的绝大部分价值,发生在新代码被写下的那五秒钟:选名字时对照一次术语表。


二、判据:一个名字好不好,用这五条量

判据 含义 反例
准确性 名字指向的对象,和它实际装的东西是同一个 businessApps 装的是专员
唯一性 一个概念全项目只有一个名字;一个名字只指一个概念 contract-review 既是专员 key 又是技能 key
一致性 同类东西的写法统一(ID 就永远 ID) UserID 与 mediaId 并存
可检索性 能被 grep 精确找到 id、data、res、info
稳定性 不会因为阶段变化而变成谎言 newUser、tempData、userList2

冲突时的优先级

外部标准  >  一致性  >  准确性  >  书写简短
  • 外部标准最高:第三方规范定义的字段名(金蝶的 FCustId、OOXML 的 sldId、HTTP 头)必须按字符照抄,不许"美化"。美化等于自建一层翻译,且翻译层迟早漏项。
  • 一致性高于准确性:ID 和 Id 哪个更好看无关紧要,全项目只能有一个。两种写法并存时,读代码的人每次都要停下确认"这是同一个东西吗"。
  • 准确性高于简短:宁可名字长一点,也不要让人误判对象边界。

三、对象术语表

3.1 正式术语(必须使用)

对象 正式名 代码落点 说明
专员 specialist model/specialist.go 可挂载到任务的数字员工
技能 skill model/skill_definition.go 任务级能力定义
应用 xapp model/xapp_definition.go 长程任务运行壳
连接器 connector internal/connector/* 外部系统接入
动作 action model/action_definition.go 技能调用的底层执行单元
任务 task model/task_record.go 工作实例容器
数据访问层 dal / DAO internal/dal/*.go 统一的数据访问层:包名 dal,每个实体的访问对象叫 XxxDAO(TaskRecordDAO、SpecialistDAO…)。所有 handler 必须经它访问数据,禁止直接调用 store.DB。2026-09-19 由 internal/repository + XxxRepo 更名而来

3.2 兼容术语(限制使用,禁止扩散)

历史词 现状 允许出现的位置 禁止
role 旧心智残留 无。已无正式对象叫 role 不得用于新变量、新文件名、新字段
assistant 只指"默认通用助手"这一件事 响应消息角色 role: "assistant"、smart-assistant(默认技能 key)、general-assistant(默认专员 key) 不得用来指代专员、技能或对象类型。工作台主对话现走 POST /api/chat/message 与 sendChatMessage,不得再回退到旧 assistant 对话命名
capability 泛能力总称 仅限口语与文档泛指 不得充当对象级文件名或字段名。已执行:capability_definition.go → skill_action_definition.go
businessApps 已删除(2026-09-18,d0d7b35) 无 不许复活
availableSkills 已更名 staticSkillCatalog(2026-09-18) 作为来源前缀使用,规则见 §5.7 不得再用旧名;不得跨模块引用(§5.7.5)
worker 历史运行时术语 仅允许出现在迁移逻辑、迁移测试、历史说明中 不得回流到业务表名、API 路径、模块名或运行时对象名

3.3 共名冲突登记

同一个字符串同时是两个对象的标识符 —— 这是既成事实,不强行改,但要靠注释和测试守住,并且不再新增:

字符串 身份一 身份二
contract-review 专员 key 技能 key
report-generation 专员 key 技能 key

处理要求:

  1. model/skill_keys.go 必须保留说明注释(已有)。
  2. 任何"按 key 查对象"的代码,必须写明查的是哪一类,不允许出现 getByKey(key) 这种不区分对象类型的调用。
  3. skill_keys_test.go 那种"前后端 key 集合双向对齐"的测试必须保留 —— 它是这两个身份不互相污染的唯一保障。
  4. 跨对象提及同一字符串时,必须带类型前缀:specialist:contract-review / skill:contract-review。

第 4 条已经写在 skill_keys.go:24-25 的注释里("日志里请带类型前缀")—— 这是 §5.7 前缀思想在本项目最早的一次自觉使用:同一个字符串在两个对象间共名时,用前缀消歧。 它当时只写在注释里、只覆盖日志一处;§5.7 把它推广成通则。保留这条注释,别删。

3.4 worker 命名空间已退出业务主命名

当前运行时业务命名已经完成收口:

  • 表 / 模型:task_record、task_run、task_artifact
  • API 路径:/api/tasks、/api/my/tasks、/api/artifacts/*
  • 前端模块:api/taskRuntime.js、store/taskRuntime.js

worker 仅保留在迁移逻辑、迁移测试、历史说明里,用来识别旧库与旧代码路径;它不再是正式术语,也不进入新白名单。


四、分层规范

4.1 后端 Go

项 规范 禁止
类型名 CamelCase,对象名 + 用途后缀:Specialist、SkillDefinition、TaskRecord Data、Info、Manager、Helper 这类空词
字段名 CamelCase 业务字段用 Id/Data/Res 这类模糊名
ID 写法 一律 ID:UserID、TaskID、RouteID、AIRouteID UserId、mediaId、sourceId
json tag 一律 snake_case:json:"specialist_key" json:"skillKey"、json:"createdAt"
外部标准字段 原样保留,并在字段注释里写明来源系统 美化、改大小写、加前缀
常量 CamelCase,同族常量同前缀 裸数字、魔法字符串
接收器 与类型同义的单字母或短名(s、req) 无意义的 this、obj

允许的例外(白名单,只有这些):

  • 第三方 API 字段:FCustId、FUseOrgId、FStockId 等 ERP 字段
  • 文档格式规范属性:OOXML 的 sldId、styleId

例外必须在字段旁注明来源,否则下一个人会以为是漂移。

4.2 数据库

项 规范
表名 单数 snake_case:specialist、skill_definition、task_record
表名前缀 一级对象用裸名(specialist / project / product);归属或复合概念必须带前缀(task_record / knowledge_chunk / position_exam_blueprint)。详见 §5.7.3
列名 snake_case,与 Go 字段的 json tag 一致
外键 <对象>_id:specialist_id、task_id
时间 <动词>_at:created_at、finished_at
JSON 列 后缀 _json,并在字段注释写明结构(如 allowed_skills 存的是字符串数组)
布尔列 语义正向:enabled、pinned;禁止 not_xxx
改名 AutoMigrate 不会改列名,必须手写迁移(P06.1)

4.3 API

项 规范 现状
路径 复数资源:/api/specialists、/api/skills、/api/xapps ✅ 已一致,保持不回退
路径前缀 一级对象裸复数(/api/specialists);子系统带前缀(/api/tasks、/api/knowledge/*)。与表名规律同构 ✅ 已一致,保持不回退(§5.7.3)
路径参数 snake_case:/api/knowledge/audit/{source_id} ✅ 已收口为 snake_case
query 参数 snake_case,且跨层同名 ✅ 持续守住
入口协议 对象入口 query 必须定义在一处常量,写读都引它 ✅ 已抽到共享常量

入口协议强制要求:任何"从 A 页面带参数打开 B 页面"的协议,参数名必须是共享常量,不允许两边各写字符串字面量。这是 §8.1 事故的直接教训。

4.4 前端

项 规范
文件名 store:<对象>Catalog.js(如 specialistCatalog.js / skillCatalog.js / xappCatalog.js);API:<对象>.js(如 specialist.js / taskRuntime.js)
store 文件名后缀 不带 Store(Pinia 的 useXxxStore 已在导出名上表达)。现仅 projectStore.js 一个孤例带后缀 → 改为 project.js(§8.12-4)
组件文件名 与对象相关的组件必须带对象前缀:SpecialistChip.vue / SkillStrip.vue;工作台布局件用 surface / workspace 语义(如 SurfaceChatRail.vue);纯布局件可用通用名(ChatInputBar.vue / PlusMenu.vue)
catalog store 模板 normalize<Object> → hydrate → getByKey / getByPath(specialistCatalog.js 为标准范本)
computed current<Object>Presentation,对象名必须准确
局部变量 必须与对象一致。const app = specialistCatalog.getByKey() 是禁止的
布尔 is<X> / has<X> / can<X>,禁止否定式
容器 单个 item,集合 items,映射 <name>Map,列表 <name>List

4.5 注释与文档

  • 历史兼容字段必须在注释里写明"历史兼容 / 待迁移",否则会被当成正式命名复制。
  • 文档里的文件引用用相对路径,禁止 file:///home/<某人的用户名>/... 这类绝对路径 —— 别人 clone 下来全是死链(违反 G18)。

五、命名模式库(做法)

5.1 后缀表

后缀 含义 例
_id 主键 task_id
_key 业务标识(字符串,可读) specialist_key、action_key
_at 时间点 finished_at
_count 计数 retry_count
_json 存 JSON 的文本列 allowed_skills、input_json
_ms / _bytes 单位必须进名字 timeout_ms、size_bytes

单位不写进名字是隐性 bug 源:timeout 是秒还是毫秒?读的人只能翻实现。

5.2 布尔

is / has / can / should + 正向语义。禁止 flag、status(名词当布尔用)、notXxx(双重否定)。

5.3 容器

item(单) / items(集) / xxxMap(映射) / xxxList(有序)。让读者不点进定义就知道能不能 .map()。

5.4 函数动词表

同一件事只用一个动词,禁止同义混用:

动词 职责 禁止替代
normalize<X> 把外部来的数据整理成内部统一形状 format / convert / adapt
hydrate 把目录/缓存数据装进运行时 load / init
resolve<X> 按规则算出一个结果(可能多来源) get / find
load<X> 从存储读 fetch / read
build<X> 组装(无 IO) create / make
ensure<X> 没有就补上(幂等) check / init
apply<X> 把某份数据/规则作用到目标上(幂等) update / set
parse<X> 字符串 → 结构 decode / read

5.5 禁止词表

作为业务标识符禁止使用(框架/标准库要求时除外):

id、data、info、res、req(局部除外)、tmp、temp、obj、item(无上下文时)、list(无前缀时)、util、common、helper、manager、handler(无对象前缀时)、flag、value

理由:不是难看,是搜不到。grep 出几百条结果等于没有结果(G03 的最小长度约束就是这个意思)。

5.6 缩写表

只有下表内的缩写允许使用,其余一律写全:

允许 全称
ID identifier
URL uniform resource locator
API application programming interface
JSON JavaScript object notation
AI artificial intelligence
OCR optical character recognition
PPT / PDF 文件格式
DAO data access object(数据访问对象,见 §3.1)
dal data access layer(数据访问层包名,Go 包名一律小写)
req / res 仅限 HTTP 处理函数的局部变量

禁止:usr、mgr、svc、num、str、val。(HTTP 处理函数的局部 req / res、Go 的 ctx、局部配置 cfg 属公认短名,不算违规。)


5.7 前缀规范(对象 / 来源 / 作用域)

前缀是本项目用得最重、也最容易失控的命名手段:34 张表、18 组 API 路径、全部 store 与组件都在用。 本节回答四件事:什么时候必须加、加哪一类、什么时候必须去掉、怎么检查。

与 TOP_CODING_RULES.md G03 的关系:G03 第 5-10 条是这一节的硬约束摘要(准则层,必读); 本节是它的完整展开 —— 判据表、三类前缀的对照、机器守卫(§6.2 守卫 G–J)、改名的额外要求(§7.6)、现状登记(§8.12)。 两者冲突时以 G03 为准。

5.7.1 原理:前缀只在"平铺且无类型"的命名空间里才必要

前缀有成本:让名字变长、会随事实过期、被复制后极难收回。 所以加前缀的唯一正当理由,是去掉它之后同一个命名空间里会出现两个可能同名的东西。

判据落在命名空间的形状上,而不是被命名对象的重要性上:

命名空间 形状 前缀
SQLite 表名 一个库内全平铺,无层级 必要:task_record 不能叫 task
表内列名 一张表内全平铺 必要:object_entry_route 的 object_
URL query 无 schema 的字符串袋 必要:xapp_specialist(§8.1 事故的根源就在这里)
JSON key 跨语言,接收端不做类型检查 必要:全站 snake_case
目录内文件名 平铺 必要:SkillStrip.vue
shell 变量 一个 shell 内共享(start_dev 同时跑两个服务) 必要:BACKEND_PORT / FRONTEND_PORT
Go 包内标识符 有包作用域 冗余但可接受(跨包引用时才真正需要)
结构体字段 有 struct 作用域 不需要:SkillDefinition.Key 不必叫 skill_key

一句话:编译器或运行时能替你消歧的地方,前缀是噪音;不能的地方,前缀是唯一的消歧手段。

本项目一个现成的正面例子:deploy/eai_agentplatform.env 里是裸名 PORT、DB_PATH(一个 systemd unit 独占一份进程环境,不会冲突),而 start_dev_10231_10232.sh:56-57 里是 BACKEND_PORT / FRONTEND_PORT(同一个 shell 里跑两个服务,必须区分)。 同一个"端口"概念,在两层用了两种写法 —— 这不是不一致,这是对命名空间边界的正确判断,应当保持。

5.7.2 三类前缀,各有各的生命周期

类别 标记什么 本项目实例 生命周期
对象前缀 属于哪个一级对象 / 子系统 skill_definition、worker_task、knowledge_space 永久(对象在,前缀在)
来源前缀 这份数据的来路 staticSkillCatalog、normalizeCustomApp、normalizeRemoteApp、(已退役)legacy_* 必须写退出条件(§5.7.6)
作用域前缀 从哪个入口来 / 属于谁 query 的 app_、API 的 my_ 随接口一起设计,不单独退役

对象前缀的白名单 = §3.1 术语表 + §5.7.3 的子系统清单。 不在这两张表里的前缀不许发明 —— 这与 §5.6 缩写表是同一个思路:能用的集合必须封闭,否则每个人都会造自己的。

5.7.3 已收敛的两条规律(DB 层与 API 层各自独立长成了同一条)

34 张表的前缀乍看是随手加的,实际有规律:

一级对象   →  裸名        specialist / project / product / position / course / user
归属或复合  →  带前缀      worker_task / knowledge_chunk / position_exam_blueprint / ai_call_log

API 路径独立地收敛到了同一条:

一级对象   →  裸复数      /api/specialists /api/skills /api/xapps /api/actions /api/products
子系统     →  带前缀      /api/tasks /api/knowledge/* /api/system/* /api/exam/* /api/ai/*

两层没有互相参照却选了一样的分法,说明这条规则是自然的,不是硬塞的。写进规范,守住不回退。

子系统前缀白名单(现有;新增须先登记到本表):

task · knowledge · exam · official_account · ai · media · system · user · position

已知的一处不同构(登记,不强制回改):specialist 是裸名,而它的同位对象是 skill_definition / xapp_definition / action_definition。 _definition 后缀标记的是"对象定义表",按此语义 specialist 应属同一族。但改表名牵动迁移与所有引用,收益不抵成本 —— 按 §6.4 的 P3 纪律先登记,不顺手改。

5.7.4 硬约束

  1. 一个标识符最多带一个类型前缀。 task_record ✅ / xapp_specialist_skill_key ❌。需要两层信息时用组合词,不要叠加前缀。
  2. 次序固定:前缀 + 核心词 + 后缀。 skill_definition ✅ / definition_skill ❌。
  3. 前缀必须写全,禁止缩写。 specialist_ ✅ / sp_、sk_、wr_ ❌(§5.6)。
  4. 禁止拼音前缀。 前缀取自 §3.1 的英文术语表;中文是对外展示层的事,不进标识符。
  5. 过渡前缀不得嵌套。 legacy_ 之上不许再叠一层,理由见 §5.7.6。
  6. 来源前缀不得跨模块引用(§5.7.5)。
  7. 作用域前缀不得进入模型、表、字段名。 my_ 只能出现在 API 路径与 query。 现状是对的:API 是 api/my_xapp_center.go,模型是 model/user_xapp_center.go —— 保持这个分层。 附带提醒:my- 命名的是视图不是资源(/api/my/tasks 与 /api/tasks 并存)。当出现第三种视图(管理员看某个用户的)时它会没有名字 —— 届时改用 ?owner= 参数,不要再加 their-tasks 这种前缀。

5.7.5 来源前缀不得泄漏到调用点

这是本节最实用的一条,也是本项目正在发生的一个问题。

staticSkillCatalog 的定义位置是对的(config/workbench.js),但它现在有 6 处引用,其中 5 处是同一种兜底写法:

skillCatalog.getByKey(key) || staticSkillCatalog.find((item) => item.key === key)

分布在 SmartAssistantPage.vue(3 处)、PlusMenu.vue、CurrentObjectChip.vue。

问题不是"不该加 static 前缀",而是前缀泄漏到了调用点:调用方本来不该知道"这份数据可能是静态兜底的"。 每多一个调用点,就多一处将来要同步修改的地方;而且哪一处漏了不会有任何提示。

→ 做法:把兜底收进 skillCatalog,调用点只写一个名字:

// store/skillCatalog.js —— 前缀留在声明处,不出模块
function resolve(key) {
  return getByKey(key) || staticSkills.find((item) => item.key === key) || null
}

(skillCatalog.js 目前只有 hydrate / getByKey,尚无 resolve —— 这是待办,见 §8.12-2。)

推广成规则:任何带来源前缀的符号(static* / legacy* / custom* / remote*),被声明模块之外的文件直接引用,即为泄漏。 判断不需要读实现 —— 看 import 就够。

5.7.6 来源前缀必须带退出条件

static* / legacy* / tmp* / old* / deprecated* 描述的是过程状态,而过程会结束。 没有退出条件,它们就会永久化,代码变成考古现场。

本项目在同一张表上同时有一个反例和一个正例。

反例 —— skill_definition 的五代列名:

entry_route  →  route  →  legacy_entry_route / legacy_route
             →  legacy_object_entry_route  →  (已删除)

legacy_object_entry_route 的字面意思是"遗留的 · 对象入口路由" —— legacy_ 叠在已经 legacy 的概念上。 过渡前缀出现第二层,就是上一次迁移没有退出条件的证据。

正例 —— 同一个字段的收尾: 上面那批 legacy 列的 drop 逻辑(db.go:148-158)与 RoleKind → ObjectKind 的迁移写在同一笔提交(d0d7b35)里,而不是"先留着以后再说"。这是正确做法,值得照抄。

→ 要求:写下来源前缀的同一行注释或相邻 TODO,必须写清"什么时候可以去掉"。 现存待办示例:staticSkillCatalog 的退出条件 = workbench.js 的静态兜底被确认可由 skillCatalog 完全覆盖后删除。

5.7.7 前缀与分隔符

分隔符由所在层决定,不由个人喜好:

层 分隔符 例
Go 标识符 CamelCase 连写,前缀是完整单词、不加分隔符 staticSkillCatalog、normalizeRemoteApp
表名 / 列名 / JSON key snake_case task_record、xapp_specialist
URL 路径 / query snake_case /api/tasks、xapp_skill
文件名 组件 CamelCase.vue、模块 camelCase.js SpecialistChip.vue、skillCatalog.js

同一层内不得混用。模型层当前 237 : 0 全是 snake_case,是一条干净基线,加守卫防回退(§6.2 守卫 I)。


六、如何检查

6.1 人工五问(写名字时问自己,review 时问作者)

  1. 这个名字指的是哪个对象?能一句话说出它是 specialist / skill / app / connector / action / task 中的哪一个吗?
  2. 换个人读,会不会理解成另一个对象?
  3. 全项目 grep 一下,是不是只有这一种写法?
  4. 改它会不会断掉靠名字派生的协议(§1.2 的表)?
  5. 它是外部标准吗?是 → 按字符照抄,一个字都不许动。

6.2 机器守卫(建议落成一个 Go test,与 skill_keys_test.go 同一位置)

已有先例:model/skill_keys_test.go 读 frontend/src/config/workbench.js,把前端技能 key 与后端 ValidSkillKeys(22 个)双向对齐。 命名守卫照这个模式写,每条都要带白名单,否则会被误报淹没:

# 查什么 正则 白名单
A Go 里 Id 写法 \b[A-Za-z]+Id\b ERP 字段(FCustId 等)、OOXML(sldId、styleId)
B camelCase json tag json:"[a-z]+[A-Z] 无
C camelCase 路径参数 \{[a-z]+[A-Z][A-Za-z]*\} 无
D 禁用词当业务标识符 按 §5.5 词表 框架约定(req 局部)
E 前端 skill key ↔ 后端 ValidSkillKeys 已有的 skill_keys_test.go —
F 入口协议写读对称 见下 —
G 前缀白名单 diff:从表名、API 路径、store/组件文件名、query 参数中抽取前缀,与 §3.1 术语表 + §5.7.3 子系统清单求差集 见下 两张表本身就是白名单
H 过渡前缀缺退出条件:legacy_ / legacy[A-Z] / static[A-Z] / tmp_ / old[A-Z] / deprecated 每一处都必须有说明移除条件的注释或 TODO `(legacy_ static[A-Z]
I 分隔符不混用:模型层 JSON tag 必须 100% snake_case(基线 237:0) json:"[a-z0-9_]*[A-Z] 无(my_app_center.go 已修)
J 来源前缀跨模块引用:带来源前缀的符号不得被声明它的模块之外的文件引用 见下 声明模块自身

守卫 G 的做法:表名与路径前缀可以直接从 model/*.go 的 TableName() 和 router.go 的注册里抽; 抽出的前缀集合与白名单求差集,多出来的就是"某人新造的前缀",需要先登记再放行。 这条守卫的价值在于把"前缀"从个人习惯变成受控词汇表 —— 与 §5.6 缩写表同一个机制。

守卫 J 的做法:static* / legacy* / custom* / remote* 开头的导出符号,grep 其被 import 的位置, 凡出现在声明模块之外即为违规。当前 staticSkillCatalog 有 5 处这类引用(§5.7.5),是这条守卫的首批命中项。 不需要读实现,看 import 就够 —— 这是一条几乎零成本、却能挡住"兜底逻辑被抄 N 遍"的守卫。

守卫 F 的做法(最重要,也最难自动化): 入口协议不要靠扫描源码配对,而是把参数名提成共享常量(如 frontend/src/config/objectEntry.js 导出 XAPP_ENTRY_QUERY_KEYS = { specialist: 'xapp_specialist', ... }),写端读端都引它。 这样 F 就从"看不见的约定"变成了"编译器能查的引用",守卫 F 也就不需要了 —— 能用结构消除的检查,不要用扫描去补。

6.3 跨层对齐检查

任何"同一份清单在前后端各写一遍"的东西,都必须有双向对齐测试:

  • 技能 key:前端 staticSkillCatalog ↔ 后端 ValidSkillKeys ✅ 已有(skill_keys_test.go,22 个 key 双向对齐)
  • 专员 key:前端静态目录 ↔ 后端种子数据 ⬜ 建议补
  • 入口协议参数名:✅ 已做。写端与读端都改为引用 frontend/src/config/objectEntry.js 的共享常量,不再各写各的字面量。

6.4 检查结果的严重性分级

级别 判据 处理
P0 名字不一致导致功能已断(写读不同名、配对失效) 当 bug 修,立即
P1 跨层漂移,会被新代码复制(json tag、路径参数、失真局部变量) 本迭代内
P2 名字带旧词但语义尚可推断(RoleKind、currentRolePresentation) 排期做,别急
P3 死代码 / 无人引用的历史变量 先判生死,再决定改名还是删除

注意 P3 的陷阱:给一份没人读的静态数据改名,等于给它续命。 先确认引用数,是 0 就删(见 §8.9,businessApps 已按此原则删除)。

P3 同样适用于前缀:给死代码补一个语义正确的前缀,只是让它死得更体面(§5.7)。


七、如何修复

7.1 迁移顺序:由内到外,最后才碰数据库

① 局部变量
② 前端协议 / 入口参数
③ 文件名
④ 字段名 / 数据库列名

理由:

  • ① 影响面最小、可读性收益最直接、无跨进程风险。
  • ② 优先于 ③ —— 协议不统一会持续产生新的分叉,不先堵住,你改完还会被新代码重新搞乱。
  • ④ 放最后:牵动数据库,而 SQLite 的列改名 AutoMigrate 不管,必须手写迁移,风险最高、回滚最难(G05:大规模操作前先备份)。

7.2 改名六步法

  1. 定名:对照 §3 术语表 + §5 模式库,写下新名字,并回答 §6.1 的五问。
  2. 查引用:grep -rn "<旧名>",范围必须包含
    • 源码 / 模板 / 样式
    • 字符串字面量(路由、query key、事件名、localStorage key)
    • JSON tag、SQL 语句、迁移脚本
    • 配置、种子数据、测试、e2e、文档、注释
  3. 判协议:这个旧名是否出现在 §1.2 那张表里?是 → 这不是改名,是改协议,写下来通知所有相关方(含前端)。
  4. 一次改完:不留双写法。兼容层是唯一例外,且必须带删除条件("等 X 上线后删除"),否则兼容层会永久化。
  5. 验证(按 G04,不靠感觉):
    • go build ./... + go test ./... exit 0
    • npm run build exit 0
    • 真起 dev server,在浏览器里走一遍受影响路径 —— 编译通过不代表功能没断(§8.1 就是编译全绿但功能已断)
  6. 提交:一次提交只装一件事(G14);提交信息写为什么改名,不只写改了什么。

7.3 会静默断链的改名(动手前必读)

改名对象 为什么危险 必须同时检查
action_key / action_title key 由中文标题派生,前后端靠名字配对 specialistFlow.js 的 findLatestRun;任何改文案的地方
路由 query 参数 写端读端是两处独立字面量,编译不报错 全链路 grep,或改成共享常量
专员 / 技能的 key 任务挂载、技能绑定、审计日志全按 key 存 worker_task.specialist_key、ai_call_log.specialist_key、种子数据
种子数据的 key 拼错或改名 → 与库中现有记录对不上 启动时校验(applySpecialistRuleFiles 已有)
外键列名 老库有数据,改列名要迁移 手写迁移 + 备份 + 副本验证(P06.1/P06.7)

判断口诀:这个名字如果在两个不同的进程/两次运行之间传递过,它就是协议,不是名字。

7.4 各级的具体做法

① 局部变量 直接改,改完读一遍上下文确认没有同名遮蔽(shadowing)。

② 前端协议 / 入口参数 不要只是"改一致",而是提成共享常量:

// frontend/src/config/objectEntry.js
export const XAPP_ENTRY_QUERY_KEYS = {
  specialist: 'xapp_specialist',
  skill: 'xapp_skill',
  prompt: 'xapp_prompt',
}

写端 params.set(ENTRY_PARAMS.specialist, ...),读端 route.query[ENTRY_PARAMS.specialist]。 这样下一个参数不会再各写各的。

③ 文件名

  • 只改名:git mv,保持历史可追溯。
  • 一文件两职责:先拆文件,再改名,同时改注册点(router.go 等)。只拆一半会留下一个名实仍不符的残文件,比不改更糟。

④ 字段名 / 列名

  1. 先备份(data/backups/ 内置 VACUUM INTO,D24)。
  2. 在库副本上验证新列/新列名能被正确读写(P06.1)。
  3. 写一次性迁移;老列在确认无引用前不删。
  4. 同步改 json tag、前端读取点、文档字段表。
  5. 改完立即跑一遍 P06.2 的做法:探测脚本用完即删,别留成断言"升级前状态"的测试。

7.5 回滚与提交

  • 一次提交一件事:改名与功能改动不要混在一个提交里(否则回滚会带走功能)。
  • 改名列必须写明"旧名 → 新名",方便 git log --follow 与后来人搜索。
  • 如果改名波及数据库,提交信息里写明迁移步骤与验证结果。

7.6 前缀改名的额外要求

前缀改名与普通改名有一个根本区别:前缀几乎总是活在字符串里(表名、列名、JSON tag、URL 参数、env 变量名), 而字符串改名编译器一个都管不着。在 §7.2 六步法之上,额外三条:

① 必须按字符串 grep,不是按符号 grep。 查 LegacyObjectEntryRoute 是不够的,必须查 legacy_object_entry_route,并且覆盖: JSON tag、SQL 语句与迁移脚本、前端字面量、query 参数、文档。范围与 §7.2 第 2 步相同,但起点是字符串。

② 禁止子串替换、禁止正则通配,必须逐标识符精确锚定。

本项目有一个现成的雷:role_kind(正确新名 object_kind)与 role_card_json(正确新名 interaction_card_json) 都以 role_ 开头,但去向完全不同。 一句 sed 's/role_/object_/g' 会把 role_card_json 误伤成 object_card_json —— 而它本该叫 interaction_card_json。

这不是假设:db.go:171 与 :184 两条迁移的目标名确实不同,证明了批量替换必然出错。 结论:前缀改名只能一个标识符一个标识符地改,宁可慢。

③ 本项目不需要兼容窗口,但持久化的名字除外。

前后端同版本发布(单二进制 + 内网部署,无外部消费者),所以 URL query、JSON key 这类字符串边界 一次改完即可,不需要新旧并存的过渡期 —— 这是本项目相对一般工程的一个有利条件,可以放心用。

唯一例外:任何被持久化的名字。浏览器 localStorage 里的 key、已发出的书签 URL、磁盘上的配置文件 —— 这些在改名后仍然存在,读端需要容旧或做一次性迁移。


八、本项目现状

2026-09-17 实测:以下每条都在代码里核实过,标了行号,级别按 §6.4。 2026-09-18 更新:§8.1–8.10 已在提交 d0d7b35 执行完毕。原文保留是为了留下判断依据(§9 反例库直接依赖它); 每条的『现状』行是当下事实,冲突时以它为准。 新增 §8.12(前缀维度)、§8.13(未根治项)。

8.1 【曾为 P0|09-18 已修,但未根治】对象入口协议写读不对称

  • 写端:xappCatalog.js:143-146 拼 xapp_specialist / xapp_skill / xapp_prompt
  • 读端:SmartAssistantPage.vue:604、605、606 读 xapp_specialist / xapp_skill / xapp_prompt
  • 全仓库核实:没有任何地方读不带前缀的,也没有任何地方写带前缀的
  • 后果:从能力目录点应用进工作台,预置的专员 / 技能 / 提示词三个参数全部被静默丢弃,不报错。用户看到的是"点进去还是空的"
  • 注意:这是 bug,不是风格问题。按 §7.4② 提成共享常量一起修
  • 现状(09-18):✅ 功能与根因都已修 —— 两侧统一为 xapp_specialist / xapp_skill / xapp_prompt,并提成 frontend/src/config/objectEntry.js 共享常量;读端另有 3 个 watch(SmartAssistantPage.vue:700/707/714),使二次打开另一个应用时也能切换。

8.2 【P1】camelCase json tag

my_app_center.go:36-41 六处:installState、skillKey、createdAt、iconText、coverTone、isCustomApp,是全站 snake_case 体系里仅有的例外,且集中在同一个结构体。 → 改 snake_case,前端同步。 现状(09-18):⬜ 未动。模型层基线仍为 237 : 0 全 snake_case,违规仍集中在这一处。

8.3 【P1】URL 路径参数 camelCase

api/knowledge.go:97 的 {sourceId}、api/exam.go:975 的 {recordId}。 → 改 snake_case({source_id}、{record_id}),同步前端调用处。 现状(09-18):⬜ 未动(knowledge.go:94/160、exam.go:972 路径仍为 camelCase)。

8.4 【P1】局部变量失真

SmartAssistantPage.vue:314:const app = specialistCatalog.getByKey(key) —— 拿到的是专员,却叫 app。 注:它所在的 computed 名字是对的(currentSpecialistPresentation,kind: 'specialist'),失真仅限这一个局部变量。 → 改 specialist。P1 原因:它会被后续代码照抄。 现状(09-18):✅ 已修(const app = 在 SmartAssistantPage.vue 中已无匹配)。

8.5 【P2】role 心智残留

SmartAssistantPage.vue:354 的 currentRolePresentation,把 assistant / specialist / skill 三类收在一个 role 概念下。 → 收敛为 currentObjectPresentation,或按 §3.1 拆成三份。 现状(09-18):✅ 已修(全仓已无 currentRolePresentation)。

8.6 【P2】RoleKind 字段名带旧词

model/skill_definition.go:13:旧 RoleKind,早期注释曾写 assistant / specialist / skill。

  • 澄清:这个字段表达的是"这条定义属于哪类对象"。当前正式值已收口为 specialist / skill,不再把 assistant 作为对象分类值保留。
  • 改名方向:object_kind(准确)。
  • 成本提醒:改字段名要同时动 ① 数据库列 role_kind ② json tag ③ 前端读取点。SQLite 改列名 AutoMigrate 不管,得手写迁移。
  • 建议:默认方案是保留字段名,在注释与文档里标注语义;真要改,按 §7.4④ 走完整流程,不要顺手改。
  • 现状(09-18):✅ 已按完整流程改名 ObjectKind(列 object_kind),迁移写在 migrateSkillObjectKindColumn(先拷数据再 drop 旧列,未踩 AutoMigrate 只加不删的坑)。 印证 §5.7.6:旧列与新列的迁移逻辑同批删除,未留考古层。 ⚠️ 全仓仅剩的 role_kind 在 db_migration_test.go:186-201 —— 这是正确用法:测试必须重建升级前的旧库才能验证迁移。 不要把这类旧名当成待清理项。

8.7 【P2|09-18 已改】capability_definition.go 名实不符

internal/api/capability_definition.go 实际装了两套东西:skillDefinitionReq 5 个 handler + actionDefinitionReq 5 个 handler(共 15 个函数)。

  • 不能只改名为 skill_definition.go —— action 的 handler 还在里面,改完名实仍然不符。
  • 两个正确方案:① 拆成 skill_definition.go + action_definition.go,同时改 router.go 注册;② 不拆,改名 skill_and_action_definition.go。
  • 要害:capability 不是正式对象,让它当文件名会持续制造一个不存在的一级概念。
  • 现状(09-18):✅ 已改为 api/skill_action_definition.go(采用方案②,未拆分),router.go 注册已同步,构建通过。 ⚠️ 遗留观察:该文件名与 §5.7.4 第 1 条不冲突,但它是双对象文件;若将来 action 的 handler 变多,仍应拆成两文件(拆分时必须同时改 router.go 注册)。

8.8 【P1|09-18 已完成】worker 命名空间已退出业务路径

已完成的收口:

  • /api/worker/tasks → /api/tasks
  • worker_task.go / worker_run.go / worker_artifact.go → task_record.go / task_run.go / task_artifact.go
  • api/worker.js / store/workerRuntime.js → api/taskRuntime.js / store/taskRuntime.js

现状(09-18):✅ 业务代码已完成去兼容;worker_* 仅保留在迁移逻辑、迁移测试与历史说明中。

8.9 【P3|09-18 已删除】businessApps 死代码

  • 定义:config/workbench.js:1434,内容是专员目录(第一条即 contract-review 合同审查专员,带 tier / workerType / roleCard),名字与内容相反
  • 引用情况:模块外零引用;仅 workbench.js 内部 6 处自用(2232 按 legacy 路由查、2236 按 tier 筛、2240 数量、2244 可升级数、2252-2253 dw/adw 统计)
  • 建议:先确认那几个统计入口是否还有页面在用 → 没有就整块删除;有就随使用者一起迁到 specialistCatalog
  • 不要改名成 staticSpecialistCatalog —— 那等于给一份没人读的静态副本续命
  • 现状(09-18):✅ 已整块删除,按建议路径处理。 留下一条经验:判断"改还是删"要看引用数,不是看名字难不难看。 这一条最初被外部清单列为"建议改名为 staticSpecialistCatalog",实际核实后是模块外零引用的死变量 —— 按原名改就给它续了命。

8.10 【P3|09-18 已更名,但只解决一半】availableSkills 静态 fallback

config/workbench.js:105,22 条技能定义,被 4 处用于兜底:store/skillCatalog.js:4、components/chat/PlusMenu.vue:183、config/projectTemplates.js:19、components/chat/CurrentObjectChip.vue:55。 → 确认 skillCatalog 覆盖完整后,改名 staticSkillCatalog(语义准确),或一并删除。 现状(09-18):✅ 已更名 staticSkillCatalog,未删除(skillCatalog 尚未完全覆盖,兜底仍有实际作用)。 ⚠️ 但改名只解决了一半 —— 兜底逻辑现在被抄到了 5 个调用点,见 §5.7.5 与 §8.12-2。

8.11 【已守住,勿回退】

  • 后端对象 API 命名已清楚:/api/specialists、/api/skills、/api/xapps、/api/actions(router.go:43-50)
  • 前端 catalog store 命名已规范:specialistCatalog.js(normalizeSpecialist / getByKey / getByPath)是标准范本
  • 技能 key 前后端对齐测试已有(model/skill_keys_test.go,22 个 key)

8.12 【2026-09-18 新增】前缀维度现状

8.12-1 【已成立,守住】表名与 API 路径的前缀规律

34 张表与 18 组 API 路径各自独立收敛到了同一条分法(一级对象裸名、归属复合带前缀,详见 §5.7.3)。 两层没有互相参照却选了一样的写法 —— 这是自然规律,写进规范是为了守住,不是为了改造。

8.12-2 【P1】staticSkillCatalog 的前缀泄漏到 5 个调用点

  • 定义:config/workbench.js(原名 availableSkills,09-18 更名)
  • 引用:共 6 处,其中 5 处是同一种兜底写法 skillCatalog.getByKey(k) || staticSkillCatalog.find(...) —— SmartAssistantPage.vue:299/340/408、PlusMenu.vue:183、CurrentObjectChip.vue:55
  • 根因:store/skillCatalog.js 只有 hydrate / getByKey,没有 resolve,所以每个调用方都得自己拼兜底
  • 修法:加 resolve(key) 把兜底收进模块(§5.7.5),调用点只写 skillCatalog.resolve(key)
  • 为什么是 P1 而不是 P2:这 5 处是将来删除静态目录时必然踩到的地雷 —— 漏掉任何一处不会有任何提示

8.12-3 【登记,不强制回改】specialist 是裸名,同位对象却带 _definition

specialist 与 skill_definition / xapp_definition / action_definition 是同级对象,命名却不同族。 按 §6.4 的 P3 纪律:先登记,不顺手改(改表名牵动迁移与全部引用,收益不抵成本)。

8.12-4 【P3】projectStore.js 是 12 个 store 里唯一带 Store 后缀的

frontend/src/store/ 下 12 个文件,只有 projectStore.js 带后缀(Pinia 的 useProjectStore 已经在导出名上表达了这层意思)。 → 改名 project.js,与 xappCatalog.js / skillCatalog.js 等同构。

8.12-5 【P3】docs/10-eaiintro/ 与其余 7 个目录不同构,且混入 Office 锁文件

  • 目录名:10-eaiintro 用连字符且无文档前缀,而其余是 01_System_Overall/(SY)、02_Architecture/(AR)…… 09_Research/(RS)
  • 内容:27 个跟踪文件里混着 pptx / jpg 等二进制素材,性质上不是"文档目录"而是"素材目录"
  • 顺带发现:~$梅奥心磁_….pptx 等 3 个 Office 锁文件被 git 跟踪了(~$ 是 Office 打开文件时产生的临时锁文件,本不该入库)
  • 建议:先决定它是"文档目录"还是"素材目录" —— 前者按 11_<Name>/ 纳入编号体系并配文档前缀,后者移出 docs/;锁文件从版本库删除并加进 .gitignore

8.12-6 【09-18 已收口】工作台主对话接口已统一为 chat/message

当前前端接口文件是 api/chatMessage.js,主入口是 sendChatMessage,后端对应 POST /api/chat/message。

  • 默认助手语义仍只存在于 smart-assistant / general-assistant 这组默认对象身份里
  • 工作台主对话入口则使用中性命名 chat/message
  • 因此,assistant 现在是对象身份语义,不是主对话 API 前缀

后续若再新增聊天接口,也应守住这条分层:对象身份不要回流到总入口路径名。

8.12-7 【已合规,保持】my_ 停留在 API 层

  • API:api/my_xapp_center.go、/api/my/xapp-center、/api/my/tasks
  • 模型:model/user_xapp_center.go(不是 my_)

这正是 §5.7.4 第 7 条要求的分层,现状正确,不要"统一"成 my_。

8.13 【P1|09-18 已修】入口协议已抽成共享常量

§8.1 的功能和根因都已修:xappCatalog.js 与 SmartAssistantPage.vue 现通过 frontend/src/config/objectEntry.js 共享 XAPP_ENTRY_QUERY_KEYS,不再各写各的 'xapp_specialist' / 'xapp_skill' / 'xapp_prompt' 字面量。

  • 收益:入口协议改名只需要动一处
  • 守则:能用结构消除的检查,不要靠扫描补漏

九、反例库(真实事故)

症状 根因 正确做法
从应用目录点进去,工作台是空的(专员/技能/提示词都没带上) 写端读端各写一套 query 字面量,编译不报错 参数名提成共享常量(§7.4②)
const app = specialistCatalog.getByKey() 读起来像在查应用 局部变量照抄了旧的"应用"心智 变量名必须与对象一致
businessApps 里装的全是专员 历史命名未随对象迁移 名实不符时优先删,其次改名
capability_definition.go 里装的是 skill 和 action 中间态概念被固化成了文件名 让已经消失的概念从文件名里消失
RoleKind 字段名比模型名旧一代 模型改名了,字段没跟上 一套迁移要改到底,不能只改外层
改了任务步骤的中文文案,UI 就永远显示"未执行" action_key 由中文标题派生,名字成了协议 能不派生就不派生;非派生不可就加配对检查
contract-review 查出来两条不同对象的记录 同一字符串被两个对象共用 共名要登记 + 用测试守住
差点把 ERP 的 FCustId "美化"成 FCustId→CustID 误以为一致性高于一切 外部标准优先级最高,照抄
文档里写 file:///home/<用户名>/... 链接 本机可点,他人全断 文档引用一律相对路径
skill_definition 一个字段先后有 5 代列名,最后一代叫 legacy_object_entry_route 上一次迁移没有退出条件,只好再叠一层 legacy_ 过渡前缀必须写退出条件,且禁止嵌套(§5.7.6)
同一个兜底判断在 5 个调用点被抄了 5 遍 static 前缀泄漏到调用点,调用方被迫知道数据来路 来源前缀不出模块,收进 resolve()(§5.7.5)
sed 's/role_/object_/g' 会把 role_card_json 误伤成 object_card_json 前缀改名用了子串替换 逐标识符精确锚定,禁止通配(§7.6②)
3 个 ~$*.pptx Office 锁文件进了版本库 素材与文档混放,无 .gitignore 守卫 目录按性质分离;临时前缀文件不入库(§8.12-5)
差点把 businessApps "改名"成 staticSpecialistCatalog 只看了名字难看,没数引用数 改还是删,看引用数(§6.4 / §8.9)

十、修订记录

版本 日期 变更
V1.0 2026-09-17 首版。原理 / 判据 / 术语表 / 分层规范 / 模式库 / 检查 / 修复 / 现状 / 反例库
V1.1 2026-09-18 ① 新增 §5.7 前缀规范(原理 / 三类前缀 / 两条已收敛规律 / 硬约束 / 泄漏 / 退出条件 / 分隔符);② §6.2 新增守卫 G–J;③ 新增 §7.6 前缀改名的额外要求(字符串 grep、禁止子串替换、无兼容窗口的例外);④ §8 全面刷新 —— §8.1–8.10 已在 d0d7b35 执行完毕,逐条标注『现状』,新增 §8.12(前缀维度)与 §8.13(入口协议未根治);⑤ §1.2 / §3.2 / §4.2 / §4.3 / §4.4 / §6.3 / §6.4 同步当下事实;⑥ §9 反例库补 5 条前缀类事故;⑦ 同步 TOP_CODING_RULES.md V1.2 —— 该文件 G03 新增第 5-10 条承载同样的硬约束,本文件 §5.7 是其完整展开,两者互为详略

注:本文件是命名问题的最高依据,与 TOP_CODING_RULES.md 配套使用。术语表(§3)变更属于架构级决策,改前先更新 PROJECT_STATE.md 的已拍板决策表。