refactor: 后端仓库层收口(A1:课程/产品/素材)+ 收进工作区既有对象化重构

本提交含两部分。第一部分是本轮工作;第二部分是此前一直留在工作区、
从未提交的对象化重构,与第一部分在文件上互相咬合(internal/repository
整个包都是未跟踪状态,且 api 层已有文件引用它),无法拆成两个可编译的提交。

一、仓库层收口 A1 批(本轮工作)

把 api 层手写的 store.DB 查询收进具名仓库方法,只给真正获益的对象做方法,
不机械包裹全量。本批迁移 22 处裸查询(courses.go 9 / media.go 12 / products.go 1),
新增方法:

- MediaFileRepo.ListByBind / ListForAudit / MarkExtracted
- KnowledgeChunkRepo.CountByMediaFile
- ProductRepo.GetVisibleByID

两条业务口径改由仓库单点持有,避免各处手写漂移:
「只有 approved 素材出现在课程详情」与「已停用产品不在课程详情露出」。

修掉两个真实缺陷:
- ProductRepo.GetByID 缺 Where 条件。此前 GET /api/products/{id} 对任意 id 都返回
  第一条产品、对不存在的 id 返回 200,且 PUT /api/products/{id} 会覆盖第一条产品
  —— 数据损坏级。全仓扫描确认这是唯一一处同型写法。
- ProductRepo.Delete 写 status="deleted",而 DELETE 处理器文档与回包都声称
  "inactive",接口在说谎;管理员用 status=all 拉列表会看到前端不认识的状态。
  已对齐为 inactive(与 CourseRepo.Delete 一致)。

删除 8 个零调用且列名不存在的死方法(一调即 SQL 报错):
- media_file 上的 file_path / file_type / approval_status 三列并不存在,
  GetByPath / ListByType / UpdateStatus 全废
- knowledge_chunk 上的 space_id 列不存在(模型早已改为 knowledge_space_key),
  List / Total / ListBySpaceIDs / DeleteBySpace / SearchByVector 全废
取舍边界:能对当前 schema 跑通的死方法保留,跑不通的删或修。

CourseRepo.List 补齐 status=all 档(此前传给它会当作 status='all' 过滤出空列表)。
该方法此前零调用,现与产品列表语义对齐。

验证:go build ./... 与 go test ./... 全绿;另用真实 HTTP 请求验证 34 项
(课程 17 / 产品 3 / 素材 14),跑在数据库副本与独立 KB_DATA_DIR 上,
含 multipart 真上传 → 审批 → pdftotext 提取 → 分片入库的完整链路。

二、此前未提交的对象化重构(非本轮工作)

- 新增 internal/repository 仓库层、connectors、skills、specialists、xapps、jsonutil,
  model/task_record|task_run|task_artifact、api/task_runtime|action_definition|chat_message
- 删除 api/app_definition、connectors、my_app_center、notification、office_skill、
  export_docx|pptx|xlsx、official_account_* 等,随 XApp/Skill/Specialist/Connector
  可插拔打包方向(AR10/AR11)调整
- 资产目录归位:backend-go/knowledge_source → assets/knowledge/source、
  training_materials → assets/training/materials;README 内相对路径同步加深两级;
  deploy env 补 ASSET_ROOT_DIR 并改 KNOWLEDGE_SOURCE_DIR / TRAINING_MATERIALS_DIR
- 前端新增 skills/ specialists/ connectors/ xapps/ 目录与对应页面

验证:前端 npm run build 通过(7.26s)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-19 01:23:51 +08:00
co-authored by Claude Code
parent ddd2d2cbd8
commit 14f303459e
448 changed files with 18792 additions and 17027 deletions
@@ -14,7 +14,7 @@
> | 对象不跳页,都在同一工作面里挂载 | `AR05_Workbench_Architecture_Contract.md` |
> | 专员 / 工具不应该是独立 Vue 页面 | 已实现:`views/workbench/` 无独立工具页 |
> | `+` 菜单作为统一的对象选择入口 | 已实现:`components/chat/PlusMenu.vue` |
> | 切换对象 = 更新任务上下文,不跳路由 | 已实现:`store/workerRuntime.js` |
> | 切换对象 = 更新任务上下文,不跳路由 | 已实现:`store/taskRuntime.js` |
>
> ### 已废弃(不要照做)
>
@@ -1,9 +1,10 @@
# AR09 对象命名规范(Object Naming Standard)
> **版本**:V1.0
> **日期**:2026-09-17
> **版本**:V1.1
> **日期**:2026-09-18
> **性质**:规范性文件(normative)。「必须 / 禁止」是硬约束,「应当」是默认做法,「建议」可依场景取舍。
> **与 TOP_CODING_RULES.md 的关系**:G03(变量命名锚定)给的是四条底线;本文件是它的展开与可执行化。
> **与 TOP_CODING_RULES.md 的关系**:G03(命名锚定,10 条)给的是硬约束底线;本文件是它的展开与可执行化。
> 其中 **§5.7 前缀规范 与 G03 第 5-10 条互为详略** —— 准则记硬约束,本文件记判据、检查与修复流程。
> **冲突时以 TOP_CODING_RULES.md 为准**,本文件不得放宽 G03。
> **适用范围**:后端 Go、数据库、API、前端 Vue、注释与文档。
> **读者**:写代码的人 + 执行命名清理的 AI。
@@ -26,7 +27,7 @@
| 靠名字建立的协议 | 位置 | 改名后果 |
|---|---|---|
| `action_key` 由 `action_title`(中文)派生,前后端靠它配对 | `specialistFlow.js` 的 `findLatestRun` | 改文案 → 配对静默失效,UI 永远显示"未执行" |
| 路由 query 参数 `specialist/skill/prompt` | `appCatalog.js` → `SmartAssistantPage.vue` | 写读不同名 → 参数被静默丢弃(见 §8.1) |
| 路由 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` 等 | 拼错 → 启动时只能靠显式校验挡住,否则静默漏种 |
@@ -79,21 +80,21 @@
|---|---|---|---|
| 专员 | `specialist` | `model/specialist.go` | 可挂载到任务的数字员工 |
| 技能 | `skill` | `model/skill_definition.go` | 任务级能力定义 |
| 应用 | `app` | `model/app_definition.go` | 长程任务运行壳 |
| 应用 | `xapp` | `model/xapp_definition.go` | 长程任务运行壳 |
| 连接器 | `connector` | `internal/connector/*` | 外部系统接入 |
| 动作 | `action` | `model/action_definition.go` | 技能调用的底层执行单元 |
| 任务 | `task` | `model/worker_task.go` | 工作实例容器 |
| 任务 | `task` | `model/task_record.go` | 工作实例容器 |
### 3.2 兼容术语(限制使用,禁止扩散)
| 历史词 | 现状 | 允许出现的位置 | 禁止 |
|---|---|---|---|
| `role` | 旧心智残留 | 无。已无正式对象叫 role | 不得用于新变量、新文件名、新字段 |
| `assistant` | 只指"默认通用助手"这一件事 | `assistant.js`(默认聊天接口)、`smart-assistant`(默认技能 key)、`general-assistant`(默认专员 key) | 不得用来指代专员、技能或对象类型 |
| `capability` | 泛能力总称 | 仅限口语与文档泛指 | 不得充当对象级文件名或字段名 |
| `businessApps` | 历史静态变量,装的是专员 | 现有代码 | 禁止新增引用;建议整块删除(见 §8) |
| `availableSkills` | 静态 fallback 技能源 | 现有代码 | 禁止新增引用;建议改名 `staticSkillCatalog` |
| `worker` | 任务执行子系统前缀 | `worker_task` / `worker_run` / `worker_artifact` / `/api/worker/*` / `workerRuntime.js` | **待拍板**(见 §3.4) |
| `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 共名冲突登记
@@ -108,17 +109,21 @@
1. `model/skill_keys.go` 必须保留说明注释(已有)。
2. 任何"按 key 查对象"的代码,必须写明查的是哪一类,不允许出现 `getByKey(key)` 这种不区分对象类型的调用。
3. `skill_keys_test.go` 那种"前后端 key 集合双向对齐"的测试必须保留 —— 它是这两个身份不互相污染的唯一保障。
4. **跨对象提及同一字符串时,必须带类型前缀**:`specialist:contract-review` / `skill:contract-review`。
### 3.4 待拍板:`worker` 这个命名空间
第 4 条已经写在 `skill_keys.go:24-25` 的注释里("日志里请带类型前缀")——
**这是 §5.7 前缀思想在本项目最早的一次自觉使用**:同一个字符串在两个对象间共名时,用前缀消歧。
它当时只写在注释里、只覆盖日志一处;§5.7 把它推广成通则。保留这条注释,别删。
现状:`/api/worker/tasks`、`model/worker_task.go`、`worker_run.go`、`worker_artifact.go`、`api/worker.js`、`store/workerRuntime.js`。
### 3.4 `worker` 命名空间已退出业务主命名
`worker` 不在正式术语表里,但已经形成一整套一致的前缀。两个选择:
当前运行时业务命名已经完成收口:
- **A. 收编为正式术语**:定义为"任务执行子系统",写进 §3.1。成本为零,立刻合法。
- **B. 重命名**:把 `worker_*` 收敛为 `task_*`。语义更准,但要动表名、API 路径、前端模块,成本高。
- 表 / 模型:`task_record`、`task_run`、`task_artifact`
- API 路径:`/api/tasks`、`/api/my/tasks`、`/api/artifacts/*`
- 前端模块:`api/taskRuntime.js`、`store/taskRuntime.js`
**建议 A**。理由:它已经自洽,且不与任何人抢名字;真正会误导的是 §8 里那些"名实相反"的(`businessApps` 装专员、`capability` 装 skill),而不是一个自洽的子系统前缀。
`worker` 仅保留在**迁移逻辑、迁移测试、历史说明**里,用来识别旧库与旧代码路径;它不再是正式术语,也不进入新白名单。
---
@@ -128,7 +133,7 @@
| 项 | 规范 | 禁止 |
|---|---|---|
| 类型名 | `CamelCase`,对象名 + 用途后缀:`Specialist`、`SkillDefinition`、`WorkerTask` | `Data`、`Info`、`Manager`、`Helper` 这类空词 |
| 类型名 | `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"` |
@@ -147,7 +152,8 @@
| 项 | 规范 |
|---|---|
| 表名 | 单数 snake_case:`specialist`、`skill_definition`、`worker_task` |
| 表名 | 单数 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` |
@@ -159,10 +165,11 @@
| 项 | 规范 | 现状 |
|---|---|---|
| 路径 | 复数资源:`/api/specialists`、`/api/skills`、`/api/apps` | ✅ 已一致,保持不回退 |
| 路径参数 | snake_case:`/api/knowledge/audit/{source_id}` | ❌ 现在是 `{sourceId}`、`{recordId}` |
| query 参数 | snake_case,且**跨层同名** | ❌ 见 §8.1 |
| 入口协议 | 对象入口 query 必须**定义在一处常量**,写读都引它 | ❌ 现在两边各写各的 |
| 路径 | 复数资源:`/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 事故的直接教训。
@@ -170,7 +177,9 @@
| 项 | 规范 |
|---|---|
| 文件名 | store:`<对象>Catalog.js`(如 `specialistCatalog.js` / `skillCatalog.js` / `appCatalog.js`);API:`<对象>.js`(如 `specialist.js` / `worker.js`) |
| 文件名 | 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()` 是禁止的 |
@@ -249,6 +258,152 @@
---
### 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`。
问题不是"不该加 `static` 前缀",而是**前缀泄漏到了调用点**:调用方本来不该知道"这份数据可能是静态兜底的"。
每多一个调用点,就多一处将来要同步修改的地方;而且**哪一处漏了不会有任何提示**。
→ **做法**:把兜底收进 `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 时问作者)
@@ -272,18 +427,30 @@
| 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` 导出 `ENTRY_PARAMS = { specialist: 'app_specialist', ... }`),写端读端都引它。
入口协议不要靠扫描源码配对,而是**把参数名提成共享常量**(如 `frontend/src/config/objectEntry.js` 导出 `XAPP_ENTRY_QUERY_KEYS = { specialist: 'xapp_specialist', ... }`),写端读端都引它。
这样 F 就从"看不见的约定"变成了"编译器能查的引用",守卫 F 也就不需要了 —— **能用结构消除的检查,不要用扫描去补**。
### 6.3 跨层对齐检查
任何"同一份清单在前后端各写一遍"的东西,都必须有双向对齐测试:
- 技能 key:前端 `availableSkills` ↔ 后端 `ValidSkillKeys` ✅ 已有
- 技能 key:前端 `staticSkillCatalog` ↔ 后端 `ValidSkillKeys` ✅ 已有(`skill_keys_test.go`,22 个 key 双向对齐)
- 专员 key:前端静态目录 ↔ 后端种子数据 ⬜ 建议补
- 入口协议参数名:⬜ 建议补(提到共享常量后自然消解)
- 入口协议参数名:✅ **已做**。写端与读端都改为引用 `frontend/src/config/objectEntry.js` 的共享常量,不再各写各的字面量。
### 6.4 检查结果的严重性分级
@@ -295,7 +462,9 @@
| **P3** | 死代码 / 无人引用的历史变量 | **先判生死,再决定改名还是删除** |
**注意 P3 的陷阱**:给一份没人读的静态数据改名,等于给它续命。
先确认引用数,是 0 就删(见 §8.9)。
先确认引用数,是 0 就删(见 §8.9,`businessApps` 已按此原则删除)。
**P3 同样适用于前缀**:给死代码补一个语义正确的前缀,只是让它死得更体面(§5.7)。
---
@@ -352,10 +521,10 @@
不要只是"改一致",而是**提成共享常量**:
```js
// frontend/src/config/objectEntry.js
export const ENTRY_PARAMS = {
specialist: 'app_specialist',
skill: 'app_skill',
prompt: 'app_prompt',
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]`。
@@ -378,81 +547,187 @@ export const ENTRY_PARAMS = {
- 改名列必须写明"旧名 → 新名",方便 `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-17 实测**:以下每条都在代码里核实过,标了行号,级别按 §6.4。
> **2026-09-18 更新**:§8.1–8.10 已在提交 `d0d7b35` 执行完毕。原文保留是为了留下**判断依据**(§9 反例库直接依赖它);
> **每条的『现状』行是当下事实,冲突时以它为准。** 新增 §8.12(前缀维度)、§8.13(未根治项)。
### 8.1 【P0|功能已断】对象入口协议写读不对称
### 8.1 【曾为 P0|09-18 已修,但未根治】对象入口协议写读不对称
- **写端**:[appCatalog.js:140-142](../../eai_agentplatform/frontend/src/store/appCatalog.js#L140-L142) 拼 `specialist` / `skill` / `prompt`
- **读端**:`SmartAssistantPage.vue:603`、`604`、`619` 读 `app_specialist` / `app_skill` / `app_prompt`
- **写端**:[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`。
`model/skill_definition.go:13`:旧 `RoleKind`,早期注释曾写 `assistant / specialist / skill`。
- **澄清**:这个字段本身不是垃圾,语义是"这条定义属于哪类对象"(`skill_definition` 表同时装 assistant / specialist / skill 三类记录)。问题只在名字里的 `role` 让人误读成"角色"。
- **澄清**:这个字段表达的是"这条定义属于哪类对象"。当前正式值已收口为 `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】`capability_definition.go` 名实不符
### 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】`worker` 命名空间待拍板
### 8.8 【P1|09-18 已完成】`worker` 命名空间已退出业务路径
`/api/worker/tasks`、`worker_task.go`、`worker_run.go`、`worker_artifact.go`、`api/worker.js`、`store/workerRuntime.js`。
→ 见 §3.4,建议收编为正式术语,成本为零。
已完成的收口:
### 8.9 【P3】`businessApps` 疑似死代码 —— 建议删,不建议改名
- `/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】`availableSkills` 静态 fallback
### 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/apps`、`/api/actions`(`router.go:43-50`)
- 后端对象 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'` 字面量。
- **收益**:入口协议改名只需要动一处
- **守则**:能用结构消除的检查,不要靠扫描补漏
---
## 九、反例库(真实事故)
@@ -468,6 +743,11 @@ export const ENTRY_PARAMS = {
| `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) |
---
@@ -476,6 +756,7 @@ export const ENTRY_PARAMS = {
| 版本 | 日期 | 变更 |
|---|---|---|
| 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 是其完整展开,两者互为详略 |
---
@@ -0,0 +1,747 @@
# AR10 XApp Removable Packaging Specification
> **版本**:V1.2
> **日期**:2026-09-18
> **性质**:规范性文件(normative)
> **适用对象**:`xapp`,即一级业务包
> **关联文档**:
> - `AR05_Workbench_Architecture_Contract.md`
> - `AR09_Object_Naming_Standard.md`
> - `AR11_Skill_Specialist_Connector_Removable_Packaging_Specification.md`
## 1. 目标
本文定义 `xapp` 的封装方式,目标不是“前端入口看起来像 APP”,而是让一个 `xapp` 在工程上达到如下删除标准:
1. 删除一个后端目录
2. 删除一个前端目录
3. 删除一条注册项
4. 不需要全仓手工搜改业务代码
5. 重新构建后不出现编译错误
6. 运行卸载脚本后,不残留该 `xapp` 自有表、路由、菜单、任务、通知、前端状态
本文中的“可删除”指:
- 删一个目录 + 删一条注册项 + 跑一次卸载 = 安全移除一个 `xapp`
严格意义上,“删一个文件就彻底删除”只适用于插件二进制或代码生成包;在当前单仓工程内,合理目标应为“删一个目录”。
## 2. 范围与非目标
### 2.1 本文覆盖
本文覆盖 `xapp` 的以下封装维度:
1. manifest
2. registry
3. 前后端路由注册
4. provider 暴露
5. 数据边界
6. schema 与 seed
7. 卸载协议
8. 定义条目所有权
9. 共享引用降级协议
10. 守卫与验收标准
### 2.2 本文不覆盖
本文不讨论以下内容的具体业务实现:
1. 某个 `xapp` 内部页面应该长什么样
2. 某个 `xapp` 的领域模型如何详细设计
3. 某个 `xapp` 的 UI 风格如何命名
这些属于对象内部实现,不属于封装边界规范本身。
## 3. 当前问题
当前仓库已经存在 `xapp_definition`、前端 `/xapps/...` 路由以及若干 `XAppShell`,但多数业务 `xapp` 仍停留在“入口像 xapp,底层仍是平台散装模块”的阶段。典型表现如下:
1. 平台总路由中仍手写某个 `xapp` 的业务路由
2. 平台总导航中仍手写某个 `xapp` 的菜单
3. 平台层 API 直接查询某个 `xapp` 的表
4. `xapp` 的核心对象仍放在通用 `model` 包,而非 `xapp` 自己的域包
5. 持久化模型直接兼任 API DTO 和领域对象
6. 统计、画像、后台管理直接耦合业务表,而不是通过 provider 聚合
7. `xapp` 的任务、通知、缓存、前端状态未形成 own 命名空间
8. `xapp_definition`、目录条目、seed 定义记录等定义层资源尚未进入卸载协议
9. 历史任务、收藏、通知历史等共享引用在对象删除后的处理策略未定义
这意味着 `xapp` 仍然不是一个可插拔业务包,而只是“挂了 xapp 皮肤的并行模块”。
## 4. 定义
### 4.1 什么是 xapp
在本项目中,`xapp` 的定义是:
1. 一个可注册的一级业务包
2. 一个有 manifest 的对象
3. 一个有自有 schema 的领域边界
4. 一个只通过 contract 暴露能力的模块
5. 一个可以通过“删目录 + 删注册 + 跑卸载”安全移除的工程单元
### 4.2 什么不是 xapp
以下对象不应按 `xapp` 方式建模:
1. 单个技能
2. 单个专员
3. 单个连接器
4. 一张单页配置页
5. 一个普通 store 或 API 文件
这些对象的封装规则见 `AR11`。
## 5. 封装目标
每个 `xapp` 必须完整拥有以下六类内容:
1. Manifest
2. Frontend shell
3. Backend module
4. Domain model / repo / service
5. Owned schema
6. Integration adapters
平台层只负责:
1. 注册 `xapp`
2. 聚合 `xapp` 暴露的能力
3. 为 `xapp` 提供公共底座能力
平台层不负责:
1. 直接进入 `xapp` 内部目录取对象
2. 直接查询 `xapp` 自有表
3. 直接 hardcode 某个 `xapp` 的页面、菜单、统计、通知
4. 直接依赖 `xapp` 内部 repo / service / views
## 6. 依赖方向
`xapp` 封装能否成立,核心不在于目录是否漂亮,而在于依赖方向是否单一。
正确依赖方向应为:
```text
platform core
-> xapps/core/contracts
-> xapps/core/registry
-> xapp manifest
-> xapp module install / register
xapp internal
-> xapp domain / repo / service / dto / schema
```
错误依赖方向包括:
1. `platform api -> xapp repo`
2. `platform stats -> xapp table`
3. `platform nav -> xapp views path literal`
4. `platform seed -> xapp domain object`
一句话:**平台层可以认识“这个 xapp 存在”,但不能认识“这个 xapp 里面具体有什么文件”。**
## 7. 推荐目录结构
### 7.1 后端
```text
backend-go/internal/xapps/
core/
contracts.go
manifest.go
registry.go
uninstall.go
apps/
internal_exam/
manifest.go
module.go
api/
student.go
admin.go
domain/
paper.go
record.go
mistake.go
repo/
paper_repo.go
record_repo.go
mistake_repo.go
service/
exam_service.go
stats_service.go
dto/
request.go
response.go
schema/
migrations.go
uninstall.go
assets/
tests/
internal_training/
manifest.go
module.go
api/
domain/
repo/
service/
dto/
schema/
assets/
tests/
```
### 7.2 前端
```text
frontend/src/xapps/
core/
contracts.js
registry.js
installer.js
uninstall.js
apps/
internal-exam/
manifest.js
routes.js
nav.js
providers.js
api/
store/
views/
components/
assets/
internal-training/
manifest.js
routes.js
nav.js
providers.js
api/
store/
views/
components/
assets/
```
## 8. 核心硬规则
### 8.0 一对象一目录是最终形态
对 `xapp` 而言,最终封装形态必须满足:
1. 一个 `xapp` 对应一个后端目录
2. 一个 `xapp` 对应一个前端目录
3. 该 `xapp` 的业务定义、路由定义、provider 定义、schema 定义都落在自己的目录中
因此:
1. **允许中心化注册**
2. **不允许中心化定义**
这里的“中心化注册”指:
1. registry 统一列出有哪些 `xapp manifest`
2. installer 统一执行 install / uninstall
3. platform core 统一聚合 provider
这里的“中心化定义”指:
1. 在一个平台公共文件里手写多个 `xapp` 的业务路由
2. 在一个平台公共文件里手写多个 `xapp` 的菜单项
3. 在一个平台公共文件里手写多个 `xapp` 的 schema / provider / page path
前者是允许的,后者不是最终封装形态。
### 8.0.1 允许中心化 / 禁止中心化
| 类型 | 是否允许 | 说明 |
|---|---|---|
| `xapps/core/registry` 集中列出 manifest | 允许 | 这是注册中心,不是定义中心 |
| `xapps/core/installer` 统一安装路由和导航 | 允许 | 这是装配层 |
| 平台总路由手写 `internal-exam` 业务路径 | 禁止 | 这是业务定义泄漏 |
| 平台总导航手写 `internal-training` 菜单细节 | 禁止 | 这是业务定义泄漏 |
| 平台公共文件集中维护多个 `xapp` 的 page path / provider key / table 名 | 禁止 | 这会破坏目录级删除 |
### 8.1 平台层不能直接 import xapp 内部实现
平台层只能依赖:
1. `xapps/core/registry`
2. `manifest`
3. `contracts/interface`
平台层不能依赖:
1. `internal_exam/domain/*`
2. `internal_exam/repo/*`
3. `internal_exam/service/*`
4. `internal_exam/views/*`
5. 任何其他 `xapp` 内部文件
这条规则的目的,是保证删除某个 `xapp` 目录后,不会炸掉全局依赖图。
### 8.2 路由必须由 xapp 自己注册
总路由不得手写某个 `xapp` 的业务路径,例如:
1. `/api/exam/*`
2. `/xapps/internal-exam/*`
3. `/courses/*`
4. `/products/*`
正确做法:
1. 平台加载 `xapp registry`
2. `xapp` 在 `module.go` / `manifest.js` 中自注册前后端路由
后端示意:
```go
xappRegistry.Register(internalexam.Manifest)
```
前端示意:
```js
xappRegistry.register(internalExamManifest)
```
### 8.3 数据表必须归属到 xapp 命名空间
为了实现“删目录即可删表”,表名必须体现归属。
不推荐:
1. `exam_paper`
2. `exam_record`
3. `mistake_record`
4. `learning_progress`
推荐:
1. `xapp_exam_paper`
2. `xapp_exam_record`
3. `xapp_exam_mistake`
4. `xapp_training_progress`
如果某张表是平台共享能力,而不是某个 `xapp` 私有数据,则应放入 shared 域,不允许伪装成 `xapp` 私有对象。
### 8.4 核心对象不能继续停留在通用 model 总包
`xapp` 私有业务对象必须放入自己的域目录,例如:
```text
internal/xapps/apps/internal_exam/domain/
```
平台 `internal/model` 中只保留:
1. 平台公共对象
2. 跨 `xapp` 共享对象
3. 公共底座对象
### 8.5 持久化模型不能直接充当 API DTO
必须分层:
1. `domain`:领域对象
2. `repo entity`:持久化对象
3. `dto request/response`:接口契约
禁止继续使用如下模式:
1. `ShouldBindJSON(&model.ExamPaper{})`
2. API 直接返回 ORM entity
3. 统计逻辑直接依赖表字段细节
### 8.6 统计、画像、后台管理只能通过 provider 聚合
平台层不得直接查询某个 `xapp` 的私有表来构建:
1. 首页统计
2. 学员画像
3. 部门统计
4. 后台管理报表
应改为由 `xapp` 对外暴露 provider:
```go
type XAppStatsProvider interface {
BuildAdminStats(ctx context.Context) (any, error)
}
type XAppProfileProvider interface {
BuildUserProfile(ctx context.Context, userID uint) (any, error)
}
```
平台只聚合 provider 输出,不碰 `xapp` 内部表结构。
### 8.7 菜单、通知、任务、前端状态必须归 xapp 自己管理
不得把以下内容散落在平台全局文件中:
1. 导航项
2. 页面路由 path 判断
3. 通知跳转链接
4. 定时任务
5. seed 数据入口
6. localStorage key
7. 前端 layout 的特殊 case
这些都必须由 `xapp manifest` 暴露。
### 8.8 定义条目也属于 xapp 的 own boundary
`xapp` 的封装边界不仅包括代码、路由、表和缓存,也包括“定义层资源”。
定义层资源至少包括:
1. `xapp_definition` 中属于该 `xapp` 的定义记录
2. 对象目录、市场目录、对象中心中的条目
3. seed 自动生成的对象定义记录
4. 前端目录页、治理页中依赖定义中心生成的条目
如果一个 `xapp` 删除后,这些定义条目仍然残留,那么:
1. 用户仍可能看到僵尸入口
2. 后台仍可能显示无效定义
3. 平台仍可能尝试按已删除对象做跳转或查询
因此,定义条目必须被视为 `owned resource`,纳入 manifest 和卸载协议。
## 9. Manifest 与 Registry 设计
## 9.1 后端 manifest
```go
type XAppManifest interface {
Key() string
Meta() XAppMeta
RegisterRoutes(r gin.IRouter)
RegisterProviders(reg ProviderRegistry)
RegisterMigrations(reg MigrationRegistry)
RegisterSeeds(reg SeedRegistry)
RegisterJobs(reg JobRegistry)
RegisterNotifications(reg NotificationRegistry)
OwnedTables() []string
OwnedStorageKeys() []string
OwnedJobKeys() []string
OwnedNotificationKeys() []string
OwnedDefinitionKeys() []string
OwnedCatalogEntries() []string
SharedReferencePolicy() SharedReferencePolicy
}
```
后端 manifest 至少负责:
1. 路由注册
2. provider 注册
3. migration 注册
4. seed 注册
5. job 注册
6. 通知注册
7. 定义条目归属声明
8. 共享引用处理策略声明
### 9.2 前端 manifest
```js
export default {
key: 'internal-exam',
label: '内部考试APP',
baseRoute: '/xapps/internal-exam',
install({ navRegistry, routeRegistry, providerRegistry, storeRegistry }) {},
uninstallMeta: {
ownedStorageKeys: ['xapp:internal-exam:*'],
ownedRouteNames: ['InternalExam*'],
ownedNavKeys: ['xapp.internal-exam.*'],
ownedDefinitionKeys: ['xapp.internal-exam.definition'],
ownedCatalogEntries: ['catalog.xapp.internal-exam'],
},
}
```
前端 manifest 至少负责:
1. 页面路由注册
2. 菜单注册
3. store 注册
4. provider 注册
5. 前端状态资源声明
6. 前端定义条目资源声明
### 9.3 Registry 的职责
registry 只做两件事:
1. 收集 manifest
2. 统一安装 / 卸载 / 枚举
registry 不应承担:
1. 业务逻辑
2. repo 查询
3. 页面渲染
4. 领域对象转换
## 10. 卸载协议
“可删除”不是只删代码,还必须定义资源清理协议。
### 10.1 卸载输入
卸载协议至少应支持以下输入:
1. `xapp key`
2. `dry_run`
3. `drop_tables`
4. `clear_storage`
5. `clear_jobs`
6. `clear_notifications`
### 10.2 卸载输出
卸载结果至少应返回:
1. 实际清理的表
2. 实际清理的 job key
3. 实际清理的 notification key
4. 实际清理的 storage key
5. 未清理成功的残留项
6. 实际清理的定义条目
7. 被共享引用阻塞或降级处理的项
### 10.3 卸载原则
卸载必须遵守以下原则:
1. 只清理 manifest 声明过的 owned 资源
2. 默认支持 `dry_run`
3. 禁止越权删除共享资源
4. 共享表必须由 shared 域自己维护,不得被 xapp 卸载误删
### 10.4 定义条目清理协议
卸载 `xapp` 时,必须同时清理或冻结以下定义层资源:
1. `xapp_definition` 中的定义记录
2. 对象目录或市场目录中的条目
3. seed 自动生成的默认定义条目
允许的处理方式包括:
1. 直接删除
2. 标记为 tombstone
3. 标记为 disabled 且不再对用户可见
但无论采用哪种方式,都必须保证:
1. 平台不会再把它当成可安装、可进入、可查询的有效 `xapp`
2. 前后端目录与导航中不再出现死入口
### 10.5 Shared Reference Policy
`xapp` 删除时,除 own resource 外,还必须处理共享域对它的历史引用。
典型共享引用包括:
1. 历史任务记录
2. 项目绑定关系
3. 收藏 / 最近使用
4. 通知历史
5. 审计日志
6. 历史发布版本
规范允许以下四类处理策略:
1. `block_uninstall`:仍存在强引用时禁止卸载
2. `convert_to_tombstone`:转为“对象已移除”墓碑态
3. `detach_reference`:解除引用但保留历史记录
4. `readonly_history`:保留只读历史,不允许继续进入对象
`xapp manifest` 必须声明自己的共享引用处理策略,平台卸载器必须在 `dry_run` 结果中明确报告:
1. 哪些引用会被阻塞
2. 哪些引用会被降级
3. 哪些引用会被直接解除
### 10.6 Dry-run Gate 与执行守卫
卸载协议不仅要能执行,还必须能被平台守卫。
至少应具备以下守卫:
1. uninstall `dry_run` 报告
2. owned resource audit
3. 定义条目完整性检查
4. 共享引用阻塞检查
5. CI 中的封装边界检查
## 11. Owned 资源矩阵
一个 `xapp` 的 own boundary 至少应覆盖以下资源:
| 资源类型 | 是否必须声明 | 典型例子 |
|---|---|---|
| 后端目录 | 必须 | `backend-go/internal/xapps/apps/internal_exam/` |
| 前端目录 | 必须 | `frontend/src/xapps/apps/internal-exam/` |
| 后端路由 | 必须 | `/api/xapps/internal-exam/*` |
| 前端路由 | 必须 | `/xapps/internal-exam/*` |
| owned tables | 必须 | `xapp_exam_record` |
| owned storage keys | 必须 | `xapp:internal-exam:*` |
| definition keys | 必须 | `xapp.internal-exam.definition` |
| catalog entries | 必须 | `catalog.xapp.internal-exam` |
| provider keys | 必须 | `xapp.internal-exam.stats` |
| job keys | 视需要 | `xapp.internal-exam.daily-sync` |
| notification keys | 视需要 | `xapp.internal-exam.record-published` |
| nav keys | 必须 | `xapp.internal-exam.entry` |
## 12. 当前培训 / 考试域的拆分建议
## 12.1 应进入 `internal_exam` xapp 的内容
应归入:
1. 考试配置
2. 考试记录
3. 错题本
4. 发证逻辑
5. 学员考试流程
6. 考试统计 provider
7. 考试后台管理接口
8. 考试前端 views / api / store
当前对象对应:
1. `ExamPaper`
2. `ExamRecord`
3. `MistakeRecord`
## 12.2 应进入 `internal_training` xapp 的内容
应归入:
1. 课程
2. 产品知识
3. 公司介绍
4. 培训入口
5. 培训前端 views / api / store
6. 培训进度统计 provider
## 12.3 `LearningProgress` 的边界
`LearningProgress` 当前记录的是:
1. `company`
2. `product`
3. `course`
因此它不应进入 `internal_exam`。
它有两种合理归属:
1. 归入 `internal_training`
2. 归入 shared learning 域
选择标准如下:
1. 如果未来只有培训 `xapp` 使用,则归 `internal_training`
2. 如果未来知识库、认证、课程中心等多个 `xapp` 都会消费,则归 shared learning
## 13. 迁移顺序
推荐按以下顺序实施,避免边搬边炸:
1. 先建立 `xapps/core/contracts`
2. 建立 registry
3. 让 `exam / training` 先改为 manifest 注册
4. 将平台直查表的逻辑改为 provider 调用
5. 再搬迁 `domain / repo / service`
6. 再把 DTO 与持久化对象分层
7. 最后再改表名命名空间
8. 最后补卸载脚本
这个顺序的核心原因是:
1. 先修正依赖方向
2. 再修正代码落点
3. 再修正对象边界
4. 最后修正资源所有权
## 14. 判定红线
如果一个 `xapp` 还存在以下任意一条,则不算完成封装:
1. 总路由中仍手写它的业务路径
2. 总导航中仍手写它的业务菜单
3. 平台 `api/*.go` 仍直接查询它的私有表
4. 平台 `model` 总包仍承载它的核心业务对象
5. 别的模块还能直接 import 它的内部包
6. 数据表名未进入它 own 的命名空间
7. 统计、画像、后台管理未通过 provider 输出
8. 前端主 layout 仍有针对它的硬编码 path 判断
9. 卸载协议无法枚举它的 owned 资源
10. 删除目录后仍需手工搜索多处散装注册点
11. 一个公共定义文件里仍集中维护多个 `xapp` 的业务路由、菜单、provider、schema 或 path
12. `xapp_definition`、目录条目或 seed 定义记录未进入 own boundary
13. 历史任务、收藏、通知历史等共享引用没有明确降级策略
14. 缺少 `dry_run`、边界检查或资源审计守卫
### 14.1 迁移期兼容层的限制
本规范允许迁移期存在少量兼容层,但必须满足以下条件:
1. 兼容层只能做转发、注册、兼容映射
2. 兼容层不得继续承载多个 `xapp` 的业务定义
3. 兼容层必须有明确退场目标,不能成为长期正式结构
换句话说:
1. `registry` 可以中心化
2. `compat adapter` 可以暂存
3. `definition hub` 不允许长期存在
### 14.2 Enforcement / Guard
为了防止封装边界回退,平台至少应建立以下自动守卫:
1. import boundary lint:禁止平台层直接 import `xapp` 内部实现
2. registry completeness check:已注册对象必须能完整枚举 own 资源
3. owned resource audit:检查 manifest 声明和实际资源是否一致
4. uninstall dry-run gate:卸载前必须可生成风险报告
5. CI fail 条件:若出现未声明资源、未处理共享引用或越权依赖,则直接失败
## 15. 验收清单
一个 `xapp` 只有满足以下条件,才算达到“整包可删除”:
1. 删除 `backend-go/internal/xapps/apps/<xapp>/`
2. 删除 `frontend/src/xapps/apps/<xapp>/`
3. 删除 registry 里的注册项
4. 后端构建通过
5. 前端构建通过
6. 没有平台层 import 残留
7. 没有平台级 hardcoded path 残留
8. 运行卸载脚本后,该 `xapp` 的 owned tables 被清理
9. 前端导航、页面、缓存、localStorage 全部消失
10. 平台统计只少这一个 `xapp` 的 provider 结果,不出现空指针或编译错误
11. job / notification / provider key 不残留
12. 共享域不被误删
13. `xapp_definition`、目录条目、seed 定义条目不会残留死入口
14. `dry_run` 报告能正确列出 shared reference 的阻塞与降级结果
## 16. 最终标准
`xapp` 的最终定义应为:
1. 一个可注册的业务包
2. 一个有 manifest 的对象
3. 一个有自有 schema 的领域边界
4. 一个只通过 contract 暴露能力的模块
5. 一个可以通过“删目录 + 删注册 + 跑卸载”安全移除的工程单元
只有达到这个标准,`xapp` 才不是“挂在平台上的专题页面”,而是真正的一级对象。
+2
View File
@@ -23,3 +23,5 @@
| `AR07_Architecture_Alignment_Audit.md` | 架构对齐确认与本轮修复范围(收敛两套并行模型的确认记录) |
| `AR08_Role_Interaction_Design.md` | AI 角色与工具统一交互设计(**部分被取代**:不跳页结论已采纳,「数字技术员」对象已废弃,见文首补注) |
| `AR09_Object_Naming_Standard.md` | 对象命名规范(**规范性文件**:原理 / 判据 / 术语表 / 分层规范 / 检查守卫 / 修复流程 / 现状问题登记 / 反例库) |
| `AR10_XApp_Removable_Packaging_Specification.md` | XApp 可删除封装规范(面向一级业务包,目标是“删目录 + 删注册 + 跑卸载”) |
| `AR11_Skill_Specialist_Connector_Removable_Packaging_Specification.md` | Skill / Specialist / Connector 可删除封装规范(复用 AR10 思想,但区分能力包、策略定义包、连接插件包的边界) |