Files
pj0235-eai_agentplatform/docs/02_Architecture/AR09_对象命名规范.md
T
eaiadmin 90031b75f3 docs: 重构仓库文档目录并迁移训练素材
按当前架构重组 docs 目录,统一中文命名与目录分层,并将训练原材料迁移到独立目录以保持架构文档边界清晰。
2026-09-22 23:23:16 +08:00

767 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` |
### 冲突时的优先级
```text
外部标准 > 一致性 > 准确性 > 书写简短
```
- **外部标准最高**:第三方规范定义的字段名(金蝶的 `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 张表的前缀乍看是随手加的,实际有规律:
```text
一级对象 → 裸名 specialist / project / product / position / course / user
归属或复合 → 带前缀 worker_task / knowledge_chunk / position_exam_blueprint / ai_call_log
```
API 路径**独立地**收敛到了同一条:
```text
一级对象 → 裸复数 /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 处是同一种兜底写法**:
```js
skillCatalog.getByKey(key) || staticSkillCatalog.find((item) => item.key === key)
```
分布在 `SmartAssistantPage.vue`(3 处)、`PlusMenu.vue`、`CurrentObjectChip.vue`。
问题在于**前缀泄漏到了调用点**:调用方本来不该知道"这份数据可能是静态兜底的"。
每多一个调用点,就多一处将来要同步修改的地方;而且**哪一处漏了不会有任何提示**。
→ **做法**:把兜底收进 `skillCatalog`,调用点只写一个名字:
```js
// 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` 的五代列名:**
```text
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]|tmp_|old[A-Z]|deprecated)` | 迁移脚本内部的一次性变量 |
| 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 迁移顺序:由内到外,最后才碰数据库
```text
① 局部变量
② 前端协议 / 入口参数
③ 文件名
④ 字段名 / 数据库列名
```
理由:
- ① 影响面最小、可读性收益最直接、无跨进程风险。
- ② 优先于 ③ —— 协议不统一会**持续产生新的分叉**,不先堵住,你改完还会被新代码重新搞乱。
- ④ 放最后:牵动数据库,而 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)。
**② 前端协议 / 入口参数**
不要只是"改一致",而是**提成共享常量**:
```js
// 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](../../eai_agentplatform/backend-go/internal/store/db.go#L171) 与 [:184](../../eai_agentplatform/backend-go/internal/store/db.go#L184)
两条迁移的目标名确实不同,**证明了批量替换必然出错**。
结论:前缀改名**只能一个标识符一个标识符地改**,宁可慢。
**③ 本项目不需要兼容窗口,但持久化的名字除外。**
前后端同版本发布(单二进制 + 内网部署,无外部消费者),所以 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](../../eai_agentplatform/frontend/src/store/xappCatalog.js#L143-L146) 拼 `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` 的已拍板决策表。*