# AR10 XApp Removable Packaging Specification > **版本**:V1.2 > **日期**:2026-09-18 > **性质**:规范性文件(normative) > **适用对象**:`xapp`,即一级业务包 > **关联文档**: > - `AR05_工作台架构约定.md` > - `AR09_对象命名规范.md` > - `AR11_技能专员连接器可删除封装规范.md` ## 1. 目标 本文定义 `xapp` 的封装方式,目标是让一个 `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//` 2. 删除 `frontend/src/xapps/apps//` 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` 才能成为真正的一级对象。