openxiangda-skill-kit 2.0.0-alpha.18 → 2.0.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,616 @@
1
+ # OpenXiangda Admin Shell v2 架构设计
2
+
3
+ 状态:实现门禁待确认;2026-08-13 已完成源码、模板、测试、Ant Design 6、身份作用域与退出链路证据审计
4
+
5
+ 适用范围:OpenXiangda 2.0 新应用,不兼容 1.x 运行时
6
+
7
+ 决策对象:`openxiangda-admin`、官方应用模板、参考应用,以及必要的平台身份协议
8
+
9
+ ## 1. 决策摘要
10
+
11
+ OpenXiangda 2.0 已经具备可运行的 Admin 基础,不应再引入一套新的后台框架重写。后续建设采用以下方向:
12
+
13
+ 1. 保留 `openxiangda-admin` 作为平台官方 React Admin 内核,应用只声明静态路由、菜单、数据资源和业务扩展。
14
+ 2. 保留 Ant Design 6.4.2,不引入 `@ant-design/pro-components` 运行时。当前 Pro Components 2.8.10 只声明兼容 Ant Design 4/5,直接安装会形成未声明兼容和双重状态模型。借鉴 ProLayout、ProTable、ProForm 的成熟交互,但将协议、查询、权限和缓存能力落在 OpenXiangda 自己的组件中。
15
+ 3. 身份、角色会话、权限解释和环境边界由平台协议负责;路由隐藏、按钮隐藏仅负责用户体验,不能替代后端授权。
16
+ 4. 路由与导航来自应用仓库中的静态、可校验 manifest。平台后端不能下发任意前端组件或可执行代码。
17
+ 5. 标签页存在状态、页面组件保活状态、数据查询状态是三种不同状态,必须分别建模。角色、用户、租户或环境变化时,旧身份的页面实例和数据状态必须立即失效。
18
+ 6. 标准数据管理页继续基于 Data API,拆成无头查询控制器和标准 Ant Design 6 界面两层,不再增加第二套 ProTable 查询协议。
19
+ 7. 个人中心由 Shell 承担基础身份信息、角色切换、会话状态和退出登录;流程代理等领域能力改为显式扩展,不再硬编码到 Shell 核心。
20
+ 8. 先用本地 Mock 和新建应用验证,再发布 npm alpha 包;同一不可变应用版本先进入预发,验收通过后晋级正式环境。
21
+
22
+ ## 2. 现状审计
23
+
24
+ ### 2.1 已完成能力
25
+
26
+ 当前 `openxiangda-admin` 已经包含:
27
+
28
+ - Principal 与 RoleSession 初始化、恢复、续期和失败重试;
29
+ - 多角色首次选择、稳定单角色使用和主动切换;
30
+ - 批量 capability explain、应用管理员绕过和页面访问边界;
31
+ - 响应式侧栏、移动端抽屉导航、面包屑、404 和路由级错误边界;
32
+ - 固定/可关闭标签、标签恢复、页面保活和当前页刷新;
33
+ - 个人中心、身份范围展示和流程代理入口;
34
+ - Data API 服务端分页、明确字段筛选、服务端排序、列设置、密度、跨页选择、CRUD、详情、审计、导入导出和文件字段;
35
+ - Workflow Kernel v2、运维诊断和凭据管理标准页面;
36
+ - 本地平台 Mock、组件测试和真实 Chromium E2E。
37
+
38
+ 因此,本阶段不是“换后台模板”,而是把已有能力从可用组件提升为稳定、可扩展、可验证的框架协议。
39
+
40
+ ### 2.2 需要修正的架构缺口
41
+
42
+ | 缺口 | 当前表现 | 架构风险 |
43
+ | --- | --- | --- |
44
+ | 身份展示资料缺失 | Principal 只有 `userId`,个人中心和头像只能显示 ID | 每个应用自行调用旧用户 API,产生权限、隐私和实现分叉 |
45
+ | 本地偏好作用域不完整 | 标签和数据页主要按 `appCode + roleAssignmentId` 保存 | 同浏览器切换租户、用户或环境时可能复用错误偏好或路径 |
46
+ | 页面保活默认过宽 | 除 `cache: false` 外,打开标签通常保持挂载 | 长时间使用后内存增长;旧身份组件可能短暂存活 |
47
+ | 菜单协议较弱 | 单 capability、无 manifest 校验、父菜单空分支语义不完整 | 复杂权限组合和错误路由只能在应用代码中补丁化处理 |
48
+ | Shell 与领域能力耦合 | 个人中心直接包含流程代理 | 不使用 Workflow 的应用仍承担领域依赖和产品假设 |
49
+ | 主题硬编码 | Provider 固定绿色主色 | 品牌、暗色、紧凑密度只能由应用覆盖 CSS |
50
+ | 数据页文件过大 | 查询、偏好、筛选、表格、表单和导入导出集中在单文件 | 修改一个交互容易影响权限和查询语义,难以做精确测试 |
51
+ | 查询生命周期分散 | 每个页面自行依赖 roleSession id 和序列号处理陈旧请求 | 身份切换时缺少统一的失效协议和扩展点 |
52
+ | 退出登录非标准能力 | Shell 仅接受可选 `onLogout` | 新应用可能忘记接入退出并形成半成品 |
53
+
54
+ ### 2.3 2026-08-12 实现证据审计
55
+
56
+ 本次审计不以文件存在或测试名称作为完成证据,而是同时核对协议、实现、模板消费和黑盒场景。结论是:现有 Admin 应继续演进,不能重写;A-D 阶段仍有真实协议缺口,但不需要重新制作已有页面能力。
57
+
58
+ | 维度 | 已有可复用证据 | 尚未满足的设计不变量 | 处理决定 |
59
+ | --- | --- | --- | --- |
60
+ | 身份 Provider | bootstrap 校验 `appCode + environmentKey`;切换失败保留旧会话;权限批量查询、缓存清理、提前续期和页面恢复均已实现 | Context 没有完整 `identityScope`/`identityEpoch`;bootstrap 没有 `subjectProfile` | 扩展现有 Provider 和 additive bootstrap,不建立第二套身份容器 |
61
+ | 路由与菜单 | 静态 `AdminRouteObject[]`、参数匹配、面包屑、直接路由 403、404 和路由错误边界已经存在 | 仅支持一个 capability;无 manifest 负向校验;父级访问继承和纯分组节点未建模 | 在现有路由协议上加入结构化访问表达式和确定性校验,不更换路由框架 |
62
+ | 标签与页面实例 | 固定/关闭/关闭其他/刷新/会话恢复和最多 12 个标签已实现 | `cache` 同时控制恢复与挂载;除 `cache:false` 外页面全部保活;无最多 6 个保活页和 LRU;存储键缺 tenant/user/environment | 拆分 tab persistence 与 keepAlive,身份 epoch 改变时同步卸载旧实例 |
63
+ | 个人中心 | 当前角色、业务范围、角色切换和 Workflow 代理均可用 | 姓名和头像退化为 userId;核心包直接依赖 Workflow;退出登录仍是可选回调 | 增加只读资料协议,把 Workflow 代理改成显式贡献,并提供默认安全退出适配器 |
64
+ | 主题与视觉 | 响应式侧栏、390px 抽屉导航、高密度列表和 Ant Design 状态组件已经稳定 | Provider 硬编码绿色;应用无法声明 light/dark/system 和 compact | 只增加 token/algorithm 主题协议,不改现有布局语义 |
65
+ | 数据管理 | Data API 服务端分页、明确字段搜索、排序、字段权限、CRUD、详情、审计、CSV、文件字段、慢请求序列保护和角色切换清选择均已实现 | 偏好键仍是 `appCode + roleAssignmentId`;查询生命周期未形成可复用 controller;单文件约 1700 行 | 先接入 identity epoch,再做保持外部行为的 controller/UI 拆分 |
66
+ | 可选模块 | `workflow`、`operations`、`credentials` 已有独立包入口,模板使用静态 lazy route | 个人中心仍静态导入 Workflow;无 Workflow 应用缺少 bundle 负向证据 | Core 不再 import Workflow;模板显式装配贡献,构建门禁验证无意外 chunk |
67
+ | 模板与黑盒 | 模板已经覆盖桌面 Shell、移动导航、列表操作、身份失败恢复及两条 Workflow 浏览器链路;生产 chunk 预算通过 | 缺跨 tenant/user/environment 的偏好隔离、keepAlive 淘汰、标准退出和路由 manifest 负向场景 | 保留已有 E2E,补充针对新协议的可证伪场景,不重写业务演示 |
68
+ | Ant Design 6 | 6.4.2 + React 19.2.8;离线扫描 37 类组件、143 处导入 | 本次 `antd lint` 无 deprecated、a11y、usage、performance 告警 | 不引入 Pro Components 运行时;后续修改继续按精确 6.4.2 API 查询和 lint |
69
+
70
+ 平台侧 `users` 已经具备 `name`、`avatar`、`jobNumber` 和主部门关系,因此 `SubjectProfile` 不需要数据库迁移。资料必须通过当前 tenant 与 userId 联合查询,主部门也必须再次校验 tenant;不得把手机号、邮箱、密码、第三方账号或组织管理能力带入 bootstrap。
71
+
72
+ ## 3. 架构不变量
73
+
74
+ 每次实现和评审都必须满足以下不变量:
75
+
76
+ 1. **后端授权权威**:前端菜单、路由和按钮裁剪不能扩大 Data API、App API、Workflow 或控制面的授权结果。
77
+ 2. **环境来自可信入口**:前端不能通过业务参数覆盖 `environmentKey`;环境由平台托管页面元数据和已验证会话确定。
78
+ 3. **单一活动角色**:一个 RoleSession 只有一个 `activeRoleAssignmentId`,不隐式合并多角色权限。
79
+ 4. **身份作用域隔离**:所有 UI 偏好、页面保活、查询和授权缓存至少绑定 `tenantId + appCode + environmentKey + userId + roleAssignmentId`。
80
+ 5. **身份改变即失效**:角色、用户、租户、环境或 `authzVersion` 改变时,旧作用域的授权缓存、查询、选择项和页面实例不得继续使用。
81
+ 6. **静态前端代码**:平台只提供数据与授权协议,不向浏览器下发任意 React 组件或可执行业务代码。
82
+ 7. **显式服务端查询**:列表必须分页,并使用明确字段的过滤与排序;禁止拉取大页后在浏览器筛选。
83
+ 8. **字段策略闭环**:前端按字段策略裁剪显示、搜索、表单和传输,后端仍独立执行字段读写授权。
84
+ 9. **组件状态有界**:标签数量、保活页面数量、查询并发、导入导出规模都必须有明确上限。
85
+ 10. **失败可恢复**:身份刷新、页面渲染、查询、保存和角色切换失败时保留最后一个仍有效状态,并提供显式重试或回滚。
86
+
87
+ ### 3.1 每轮实现前的架构决策门禁
88
+
89
+ 本文后续每一轮修改都必须先形成可检查的决策记录。决策没有通过时只能继续审计和修订设计,不能用局部补丁提前改运行时代码。
90
+
91
+ | 检查项 | 必须回答的问题 | 不通过时的处理 |
92
+ | --- | --- | --- |
93
+ | 问题与证据 | 问题能否由源码、协议、日志或可复现测试证明,而不是由页面现象猜测 | 继续只读定位,不改代码 |
94
+ | 能力所有权 | 平台、Admin Core、可选模块、应用前端或 NestJS 后端中,谁是唯一事实来源 | 先收敛所有权,禁止新建第二套状态或接口 |
95
+ | 稳定不变量 | 身份、权限、环境、数据、发布和 1.x 隔离边界是否仍成立 | 修改设计或缩小范围 |
96
+ | 上下游契约 | 调用方、被调用方、缓存键、存储、模板、CLI 和发布门禁会受到什么影响 | 列出 additive/breaking 变化和迁移顺序 |
97
+ | 失败与并发 | 超时、重试、重复请求、切换身份、旧响应、部分成功和多会话写入如何处理 | 先定义幂等、失效和恢复语义 |
98
+ | 安全与资源 | 是否扩大授权、泄露资料、形成开放跳转、无界缓存或额外常驻资源 | fail closed,并给出上限和清理策略 |
99
+ | 回滚边界 | 是否能单独回滚;是否需要数据回滚;现网旧应用是否受影响 | 拆分提交和发布单元,禁止混合不可逆变化 |
100
+ | 可证伪验收 | 哪些 unit、contract、HTTP、浏览器和新应用场景能证明决定正确 | 先写验证矩阵,再进入实现 |
101
+
102
+ 每轮提交只解决已声明的架构主题。审计过程中发现的相邻问题进入后续决策记录;除非它直接破坏本轮不变量,否则不顺手扩大修改范围。
103
+
104
+ ## 4. 总体分层
105
+
106
+ ```mermaid
107
+ flowchart TD
108
+ A["应用仓库:路由、页面、资源、业务动作"] --> B["openxiangda-admin 协议组件"]
109
+ B --> C["openxiangda-devkit-core 浏览器客户端"]
110
+ C --> D["平台同源网关"]
111
+ D --> E["Principal / RoleSession / Authz"]
112
+ D --> F["Data API / Directory / Files"]
113
+ D --> G["Workflow / Events / Operations"]
114
+ D --> H["应用 NestJS App API"]
115
+
116
+ B --> I["Shell:导航、标签、个人中心、错误边界"]
117
+ B --> J["Data Workbench:查询、表格、表单、详情"]
118
+ B --> K["可选模块:Workflow、Operations、Credentials"]
119
+ ```
120
+
121
+ ### 4.1 所有权边界
122
+
123
+ | 层 | 拥有内容 | 不拥有内容 |
124
+ | --- | --- | --- |
125
+ | 平台 | 登录态、用户身份、RoleSession、授权解释、环境、Data API、Workflow 协议 | 应用页面结构和业务 React 组件 |
126
+ | `openxiangda-admin/core` | Provider、Shell、路由协议、导航、标签、身份切换、个人中心基础框架 | 业务状态机、业务字段和领域操作 |
127
+ | `openxiangda-admin/data` | Data API 查询控制器、标准列表/搜索/详情/编辑协议 | 应用特有字段语义和敏感后端业务动作 |
128
+ | 可选 Admin 模块 | Workflow、运维、凭据、Dashboard 的标准页面 | Shell 基础生命周期 |
129
+ | 应用仓库 | 路由 manifest、业务页面、列/筛选定义、业务动作与视觉品牌 | 身份协议、授权判断、环境推导和通用查询实现 |
130
+ | NestJS 后端 | 可复用业务服务、幂等事务、外部 API、事件消费 | 浏览器页面状态和菜单裁剪 |
131
+
132
+ ## 5. 身份、角色与个人中心
133
+
134
+ ### 5.1 平台协议补充
135
+
136
+ `Principal` 保持纯授权主体,不塞入可变化的个人资料。RoleSession bootstrap 增加只读的 `subjectProfile`:
137
+
138
+ ```ts
139
+ interface SubjectProfile {
140
+ schemaVersion: "openxiangda.subject-profile/v2";
141
+ userId: string;
142
+ displayName: string;
143
+ avatarUrl?: string | null;
144
+ jobNumber?: string | null;
145
+ affiliatedDepartment?: {
146
+ id: string;
147
+ name: string;
148
+ } | null;
149
+ }
150
+
151
+ interface RoleSessionBootstrapResult {
152
+ state: "active" | "selection_required" | "unassigned";
153
+ assignments: RoleAssignment[];
154
+ roleSession: RoleSession | null;
155
+ principal: Principal | null;
156
+ subjectProfile: SubjectProfile;
157
+ identityScope: string | null;
158
+ }
159
+ ```
160
+
161
+ 该资料由平台根据当前已认证用户生成,不要求应用角色具备 Directory 权限,也不包含手机号、邮箱、密钥或第三方账号信息。未来若建设个人资料编辑,使用独立接口和权限,不扩张 bootstrap 写能力。
162
+
163
+ 平台同时在 capabilities 中声明 `authz.role-session-context`。该名称描述平台拥有的 RoleSession 上下文协议,不按首个消费者 Admin UI 命名,避免平台协议反向依赖前端产品概念。2.0 编译器必须把它作为基础能力自动写入 AppPackage 的 `compatibility.requiredCapabilities`,应用模板和开发者不手工声明:当前 2.0 AppPackage 固定包含前端,并已把 RoleSession 与 `authz.batch-explain` 作为基础身份协议。旧客户端可以忽略 additive 字段;新版 Admin 则把 `subjectProfile` 缺失视为协议不完整,不允许自行计算 scope 或退回 userId-only 半成品。部署客户端必须在上传制品前拒绝不支持该能力的平台;运行时仍对缺字段响应 fail closed,并展示“平台版本不兼容”,不能落成白屏。
164
+
165
+ #### 5.1.1 A1 实现所有权与失败语义
166
+
167
+ | 位置 | 唯一职责 | 明确不做 |
168
+ | --- | --- | --- |
169
+ | `openxiangda-contracts` | 定义 `SubjectProfile`、schema version 和可复用 schema/type | 不承载登录、查询或缓存逻辑 |
170
+ | 平台 `OpenXiangdaAuthorizationV2Service` | 在现有 bootstrap/switch 响应中读取当前主体资料并生成 `identityScope` | 不新建身份接口、表或另一套当前角色状态 |
171
+ | 平台 application capabilities | 宣告并校验 `authz.role-session-context` | 不根据具体 Admin 页面推测能力 |
172
+ | `openxiangda-devkit-core` | 让 `RoleSessionBootstrapResult` 引用共享 `SubjectProfile` 类型 | 不从 JWT、userId 或 RoleSession 自行补资料/scope |
173
+ | 2.0 compiler | 将该能力作为基础 required capability 自动封装进制品 | 不要求模板或应用作者重复配置 |
174
+ | 本地 platform Mock | 严格模拟同一响应协议,供新应用离线验收 | 不形成比真实平台更宽松的测试专用协议 |
175
+ | `openxiangda-admin` | A2 开始消费响应并管理 identity epoch | A1 不提前重构 Shell、标签或 Data Workbench |
176
+
177
+ 平台资料查询必须同时使用认证上下文中的 `tenantId + userId`,并只选择 `id/name/avatar/jobNumber/affiliatedDepartmentId` 及主部门的 `id/name/tenantId`。`displayName` 只按 `trim(name) || userId` 回退;手机号、邮箱、username、部门全集、第三方账号和认证字段均不进入响应。用户记录不存在时返回 `AUTHZ_V2_SUBJECT_NOT_FOUND`;主部门存在但 tenant 不一致时返回 `AUTHZ_V2_SUBJECT_DEPARTMENT_TENANT_MISMATCH`,不能静默串联或使用 JWT 中的旧资料兜底。
178
+
179
+ `sessionBootstrapResult` 是 active、selection_required、unassigned 和 switch 的唯一序列化出口,资料与 scope 必须在这里一次性装配。identityScope 只从已经通过 tenant/app/environment/user/assignment 校验的活动 session 上下文生成;非 active 状态固定为 `null`。同一活动 assignment 的 RoleSession 续期和 authzVersion 变化不得改变 scope,角色、用户、租户、应用或环境变化必须改变。任何入口不得接受客户端提交的 scope。
180
+
181
+ A1 不修改应用 OAuth `App Auth` DTO、平台 JWT payload、`Principal`、登录协议或数据库 schema。应用 OAuth 负责“应用如何认证用户”,RoleSession bootstrap 负责“已登录平台用户当前以哪个应用角色工作”,两条身份流禁止因页面展示资料而合并。
182
+
183
+ ### 5.2 稳定角色语义
184
+
185
+ - 多角色用户首次没有历史选择时显示角色选择页。
186
+ - 选择后平台保持该角色;刷新、关闭后重开仍恢复同一可用角色。
187
+ - 用户主动切换角色时创建新的 RoleSession,旧会话被替代。
188
+ - 切换失败继续使用旧的有效会话,不清空当前页面。
189
+ - 已选角色被撤销、过期或授权版本变化时,重新 bootstrap;不能回退为多角色权限合并。
190
+ - 应用管理员仍通过平台 Principal 的 `isAppSuperAdmin` 获得后端绕过,前端只消费该声明,不自行按角色名判断。
191
+
192
+ ### 5.3 统一身份作用域
193
+
194
+ `identityScope` 由平台在 bootstrap 中生成并作为 opaque 字符串返回,浏览器和应用代码不得各自拼接或散列身份字段。平台内部采用带版本前缀的确定性摘要:
195
+
196
+ ```ts
197
+ identityScope = base64url(sha256(
198
+ "openxiangda.role-session-context/v2\0" +
199
+ tenantId + "\0" + appCode + "\0" + environmentKey + "\0" +
200
+ userId + "\0" + activeRoleAssignmentId
201
+ ))
202
+ ```
203
+
204
+ 该值只用于 UI 缓存与持久化隔离,不是 Token、授权凭据或防篡改证明,后端不得信任客户端回传的 `identityScope` 进行授权。只有 `state: "active"` 才返回非空值;`selection_required` 和 `unassigned` 返回 `null`,避免在没有活动角色时恢复业务页面状态。
205
+
206
+ 现有 Provider 中用于合并 bootstrap 请求、丢弃跨应用/环境旧响应的 `appCode + environmentKey` 局部键必须改名为 `bootstrapRequestScope`。它只描述一次身份加载请求的目标,不包含 tenant、user 或 active assignment,禁止暴露给页面、禁止用于标签/查询偏好,也不能与平台返回的 `identityScope` 共用类型或变量名。
207
+
208
+ Admin Provider 另外拥有只存在于当前页面生命周期的单调 `identityEpoch`。它不由平台持久化,也不进入浏览器存储:
209
+
210
+ | 转换 | `identityScope` | `identityEpoch` | 状态处理 |
211
+ | --- | --- | --- | --- |
212
+ | 首次得到 active bootstrap | 使用平台值 | 初始化为 1 | 建立当前身份状态 |
213
+ | 同一 bootstrap 被重复提交 | 不变 | 不变 | 不制造无意义重挂载 |
214
+ | RoleSession id 或 `authzVersion` 改变 | assignment 未变时可不变 | 增加 | 清理授权、查询、选择项和页面实例 |
215
+ | tenant/app/environment/user/assignment 改变 | 必须改变 | 增加 | 先卸载旧 scope,再恢复新 scope 的非敏感偏好 |
216
+ | bootstrap、刷新或切换失败 | 保留原值 | 不变 | 继续使用最后一个仍有效身份并显示可重试错误 |
217
+ | 退出登录成功 | 置空 | 增加 | 清除当前会话标签、授权、查询和页面实例 |
218
+
219
+ RoleSession 续期但 id 和 `authzVersion` 均未改变时不增加 epoch。浏览器存储只使用平台返回的 scope 作为键的一部分,不保存 Token、权限结果、业务响应或表单草稿;主题、密度、列偏好等非敏感设置可以按版本化 scope 保留。
220
+
221
+ ### 5.4 个人中心内容
222
+
223
+ 核心区域固定包含:
224
+
225
+ - 头像、显示姓名和用户标识;
226
+ - 当前应用、环境、角色和会话有效期;
227
+ - 当前角色的业务数据范围摘要;
228
+ - 切换角色、刷新身份和退出登录;
229
+ - 非生产环境的明确标识。
230
+
231
+ 领域扩展通过 `PersonalCenterSectionContribution` 注册:
232
+
233
+ ```ts
234
+ interface PersonalCenterSectionContribution {
235
+ key: string;
236
+ order?: number;
237
+ title: ReactNode;
238
+ capabilities?: AccessExpression;
239
+ render: () => ReactNode;
240
+ }
241
+ ```
242
+
243
+ 流程代理迁移为 Workflow 模块贡献。未启用 Workflow 的应用不加载该代码块。
244
+
245
+ 退出登录由 Provider 的标准 `authAdapter` 提供,不允许默认缺失:
246
+
247
+ 1. 浏览器使用同源凭据调用现有 `POST /openxiangda-api/v2/auth/logout`。会话撤销、冒用身份清理和 HttpOnly Cookie 清理由平台 `AuthService` 负责;Admin 不建立第二套登出接口。
248
+ 2. 服务端登出接口不接收 return URL,避免把导航输入带进身份撤销边界。
249
+ 3. 服务端成功后,Provider 清除当前会话的标签、授权、查询和页面实例,再使用 `window.location.replace` 进入平台登录代理 `/platform/login`。代理的 `callback` 默认是当前应用 runtime base 的绝对 URL,不携带 query/hash。
250
+ 4. 应用可以指定登录后的同应用路径,但必须经过统一校验:仅允许 `http/https`、当前 origin,且 pathname 等于 runtime base 或位于其下;用户名密码型 URL、跨域 URL、协议相对 URL 和其他应用路径全部拒绝。Admin 生成端执行同应用约束;通用平台登录页消费端执行同源约束。后者还服务平台管理端、流程设计器和 CLI,不能在缺少可信 app 上下文时假装执行同应用校验。
251
+ 5. Admin 不直接调用租户 SSO/OAuth2 地址。平台登录代理根据当前域名和租户配置选择 OAuth2/SSO/普通登录,并在认证成功后返回已校验的 callback;本阶段只收紧 OpenXiangda v2 同应用返回路径,不顺带改变 1.x 或其他平台登录场景的导航契约。
252
+ 6. 服务端登出失败时不伪装成功、不跳转、不清空仍可能有效的当前身份;界面保留可重试错误。这样不会出现“前端看似退出、服务端会话仍有效”的分裂状态。
253
+
254
+ ### 5.5 通用登录 callback 审计与 B0 安全阶段
255
+
256
+ 全仓证据表明 `/platform/login?callback=...` 是共享入口,不是 Admin 专用代理:`sy-lowcode-view`、流程设计器和 CLI 授权都会产生 callback;平台登录页的密码、游客、钉钉免登、CAS/SSO 和第三方登录分支都会消费它。当前平台登录页存在多处直接 `window.location.href = callback`,第三方回调还会再次 `decodeURIComponent`;CAS 回调服务端直接用传入的 `redirectUri` 构造 URL 并重定向。不能只修 Admin 的退出按钮,否则其他入口仍保留不同的开放跳转和编码语义。
257
+
258
+ 因此在 B1 前增加独立的 **B0 通用登录返回目标收敛**,其所有权和策略如下:
259
+
260
+ | 边界 | 唯一事实来源 | 允许范围 | 失败语义 |
261
+ | --- | --- | --- | --- |
262
+ | Admin 退出目标 | `openxiangda-admin` 纯函数 | 当前 origin、当前 `/view/:appType` runtime base 及其子路径 | 拒绝应用覆盖;默认回到当前应用根路径 |
263
+ | 平台登录页 callback | `sy-lowcode-platform` 纯函数 | 根相对路径或 `http/https` 当前 origin 绝对地址 | 丢弃 callback 并回平台首页,不导航到原值 |
264
+ | CAS login-url/callback | 平台服务端共享校验器 | 当前租户配置 origin;回调前再次校验 | 返回 400 或安全错误页,不重定向到原值 |
265
+ | CLI 登录 continuation | `OpenXiangdaCliAuthService` 生成的同源一次性 session URL | 继续由通用同源策略接受 | session 自身仍按 ticket/TTL/一次性语义验证 |
266
+
267
+ 通用策略不能只允许 `/view`,否则会破坏 `/platform`、流程设计器和 CLI session;也不能接受“任意可解析 URL”。校验必须拒绝跨 origin、非 HTTP(S)、用户名/密码 URL、协议相对地址、反斜杠、控制字符和多重编码绕过。URLSearchParams 已完成一次解码,消费端不得再无条件 `decodeURIComponent`。密码、游客、钉钉免登、SSO 启动/成功和第三方回调必须只消费同一个 `safeCallback` 结果,不能各自解析。
268
+
269
+ B0 是单独的平台安全提交:不修改登录方式、Cookie、租户 SSO 配置、1.x 页面路径或 CLI session 协议。验收使用同一组正反测试向量覆盖前端与服务端,包括平台、流程、同应用 view、CLI session、跨域、`//host`、带凭据 URL、反斜杠、控制字符和双重编码样本。先证明所有现有合法生产者仍可返回,再进入 Admin 默认退出接入。
270
+
271
+ ## 6. 路由、导航与访问协议
272
+
273
+ ### 6.1 静态 manifest
274
+
275
+ 应用继续在仓库中声明 `AdminRouteObject[]`,构建时校验:
276
+
277
+ - `key` 和规范化 `path` 全局唯一;
278
+ - 参数路由不能设为固定入口或固定标签;
279
+ - 菜单父节点在没有任何可见子项且自身不可进入时自动隐藏;
280
+ - 入口路由必须可解析;
281
+ - 路由 chunk 必须使用静态 import target,不能从平台字符串动态执行;
282
+ - 404、403 和错误 fallback 必须存在。
283
+
284
+ ### 6.2 访问表达式
285
+
286
+ 保留 `capability` 作为简写,增加结构化访问表达式:
287
+
288
+ ```ts
289
+ type AccessExpression =
290
+ | { capability: string }
291
+ | { anyOf: AccessExpression[] }
292
+ | { allOf: AccessExpression[] };
293
+ ```
294
+
295
+ 首期不在路由访问表达式中加入业务行数据。行级和字段级授权继续由 Data API 或 App API 在具体操作时执行,避免把大量记录权限预取到导航阶段。
296
+
297
+ ### 6.3 导航状态
298
+
299
+ - 菜单权限使用一次批量 explain,不为每个菜单发单独请求。
300
+ - 权限加载中不先显示后隐藏敏感入口;使用稳定骨架或保持上一次相同 identityEpoch 的结果。
301
+ - 直接访问无权限路由返回 403,而不是跳到首页掩盖问题。
302
+ - 浏览器导航由内部 `AdminNavigator` 适配器管理;默认使用 History API,保留以后接入 React Router 的适配能力,但本阶段不为了路由库替换现有稳定协议。
303
+ - 路由位置内部保留 `pathname/search/hash`。Shell 只用 pathname 做匹配,应用可以显式使用 query 作为筛选上下文,但 query 不能授予权限。
304
+
305
+ ## 7. 标签、保活和页面状态
306
+
307
+ 标签与缓存拆为三个概念:
308
+
309
+ | 概念 | 作用 | 默认值 | 失效条件 |
310
+ | --- | --- | --- | --- |
311
+ | `tab` | 是否显示可关闭标签 | 页面路由开启 | 路由删除或用户关闭 |
312
+ | `tabPersistence` | 刷新浏览器后是否恢复路径 | `session` | identityScope 变化或会话结束 |
313
+ | `keepAlive` | 切换标签后是否保留组件实例 | `none` | identityEpoch 变化、关闭、超时或容量淘汰 |
314
+
315
+ 规则:
316
+
317
+ - 列表、工作台可以显式设置 `keepAlive: "memory"`;动态详情、审批任务、创建/编辑页默认不保活。
318
+ - 最多 12 个标签,最多 6 个保活页面;所有 memory 实例使用最近最少访问策略淘汰。标签的 `pinned` 只影响关闭和标签淘汰,不豁免内存上限;固定标签的页面实例也可以被 LRU 卸载,重新访问时重新挂载。
319
+ - 标签存储键必须使用版本号和完整 identityScope。
320
+ - 身份切换同步卸载全部旧作用域页面,然后恢复新身份自己的标签路径;不能让旧组件等到下一次请求才发现身份变化。
321
+ - “刷新当前页”增加该页面实例的 generation,只重建当前页,不清空其他标签。
322
+ - 页面保活不缓存权限结果或 Data API 响应到持久化存储。
323
+
324
+ ## 8. 页面框架与视觉系统
325
+
326
+ ### 8.1 标准页面骨架
327
+
328
+ 新增 `AdminPage` 作为所有标准页面的基础表面:
329
+
330
+ - 页面标题、说明、状态和主要操作;
331
+ - 面包屑和返回动作;
332
+ - loading、empty、error、refresh 和 permission denied 状态;
333
+ - 可选页脚或危险操作区;
334
+ - 桌面 1440px 的高密度布局与 390px 移动端退化布局。
335
+
336
+ 数据详情、编辑和状态操作默认使用 Drawer/Modal,不使用永久右栏压缩表格。
337
+
338
+ ### 8.2 主题协议
339
+
340
+ `OpenXiangdaAdminProvider` 接受 `theme`:
341
+
342
+ - 应用品牌 seed:`colorPrimary`、logo、名称;
343
+ - 用户模式:`light | dark | system`;
344
+ - 密度:`default | compact`;
345
+ - Ant Design 6 的 `defaultAlgorithm`、`darkAlgorithm`、`compactAlgorithm` 组合。
346
+
347
+ 默认主题不硬编码应用颜色。CSS 只消费 Ant Design token 或 Admin CSS variables,不在业务组件散布固定颜色、阴影和圆角。
348
+
349
+ ### 8.3 视觉设计决定
350
+
351
+ 本阶段采用统一、克制、高密度的标准 B 端视觉,不单独做品牌型高保真探索。重点是清晰层级、稳定状态、键盘可达、移动端可用和错误可恢复。应用首页和业务 Dashboard 可以在此基础上单独做视觉设计,但不能改变 Shell 的交互语义。
352
+
353
+ ## 9. 标准数据管理页
354
+
355
+ ### 9.1 两层架构
356
+
357
+ 现有 `DataListPage` 保留外部能力,内部拆成:
358
+
359
+ 1. `useDataWorkbenchController`:拥有服务端查询、分页、排序、筛选、并发淘汰、选择、刷新和身份失效。
360
+ 2. `DataManagementList`:拥有 Ant Design 6 的搜索区、工具栏、表格、列设置、密度、空态和错误态。
361
+ 3. `DataRecordForm`、`DataRecordDetailDrawer`、导入导出、文件字段保持独立模块。
362
+
363
+ 应用既可直接使用完整标准页,也可使用无头控制器构建特殊工作台。二者共享同一查询和权限协议。
364
+
365
+ ### 9.2 查询协议
366
+
367
+ - 默认 `pageSize=20`,可选 10/20/50/100,禁止无界查询。
368
+ - 文本搜索必须声明目标字段与操作符;不提供默认的全字段模糊搜索。
369
+ - 多字段模糊搜索由明确的 OR filter group 表达,不在浏览器中过滤。
370
+ - 排序字段必须在资源声明中允许,并传给服务端。
371
+ - 角色或 identityEpoch 变化时取消或淘汰旧请求;旧响应不能覆盖新查询。
372
+ - 刷新保留当前数据并显示局部 loading,首次加载和失败使用独立状态。
373
+ - 查询条件可以选择同步到 URL,但默认只保存非敏感、可分享字段;敏感筛选不写 URL 或浏览器持久化。
374
+
375
+ ### 9.3 权限闭环
376
+
377
+ - 列、搜索字段、详情字段、编辑字段和导入导出字段统一消费字段访问结果。
378
+ - create/update/delete/export/import 和业务动作分别声明 capability,不通过角色名判断。
379
+ - 应用管理员消费后端 `isAppSuperAdmin` 绕过结果。
380
+ - UI 隐藏只是体验;Data API/App API 仍按角色会话、数据策略、字段策略和 revision 校验。
381
+ - 身份变化清空跨页选择,避免用新角色对旧角色选中的 ID 执行批量操作。
382
+
383
+ ### 9.4 扩展点
384
+
385
+ 标准数据页支持:
386
+
387
+ - 自定义列 render 和服务端 sortField;
388
+ - 明确类型的搜索字段;
389
+ - 行操作、批量操作和工具栏贡献;
390
+ - 自定义详情和编辑表面;
391
+ - Data API 之外的 App API 数据适配器;
392
+ - 列/搜索配置的版本化偏好迁移。
393
+
394
+ 扩展点接收受限上下文,不允许直接修改内部查询状态。敏感业务写操作应调用 NestJS App API,并由后端校验 Principal、RoleSession、scope、revision 和 idempotency key。
395
+
396
+ ### 9.5 导入导出边界
397
+
398
+ - 小规模 CSV 导出必须有明确上限,并沿用当前服务端查询与字段权限。
399
+ - 大规模或复杂 Excel 导出必须进入平台异步导出任务或 App API 后台任务,不能在浏览器拉取全部数据。
400
+ - 导入继续使用预检、受限事务分批、幂等键和错误行报告;不能以一个超大事务阻塞平台。
401
+
402
+ ## 10. 包与源码组织
403
+
404
+ 保持一个 `openxiangda-admin` npm 包和稳定子入口,避免应用安装多个相互漂移的 Admin 包:
405
+
406
+ ```text
407
+ packages/admin/src/
408
+ core/
409
+ provider/
410
+ identity/
411
+ access/
412
+ routing/
413
+ shell/
414
+ tabs/
415
+ preferences/
416
+ personal-center/
417
+ data/
418
+ controller/
419
+ query/
420
+ preferences/
421
+ list/
422
+ record-form/
423
+ record-detail/
424
+ transfer/
425
+ files/
426
+ dashboard/
427
+ workflow/
428
+ operations/
429
+ credentials/
430
+ ```
431
+
432
+ 公开子入口继续是:
433
+
434
+ - `openxiangda-admin/core`
435
+ - `openxiangda-admin/data`
436
+ - `openxiangda-admin/dashboard`
437
+ - `openxiangda-admin/workflow`
438
+ - `openxiangda-admin/operations`
439
+ - `openxiangda-admin/credentials`
440
+
441
+ 根入口只用于兼容测试,不作为新应用推荐入口。内部拆文件先保持导出兼容,再在一个明确的 2.0 alpha 版本中清理废弃 API。
442
+
443
+ ## 11. 官方模板与应用开发体验
444
+
445
+ 新应用默认生成:
446
+
447
+ - 完整的 Provider、Shell、主题和安全退出;
448
+ - 首页、标准 DataManagementList、详情/编辑 Drawer、Workflow 和运维管理示例;
449
+ - 静态路由 manifest 与 capability 常量;
450
+ - 本地平台 Mock 和四种角色数据;
451
+ - 单元测试、浏览器 E2E、构建预算和无循环依赖检查;
452
+ - `domain -> service/controller -> page` 的单向依赖示例。
453
+
454
+ 应用开发者的最小工作应是:声明资源、列、搜索字段、路由和业务动作,而不是重新实现布局、身份、缓存、权限和查询生命周期。
455
+
456
+ ## 12. 实施顺序
457
+
458
+ | 阶段 | 修改范围 | 主要结果 | 验证与回滚边界 |
459
+ | --- | --- | --- | --- |
460
+ | A. 协议与测试先行 | contracts、platform server 源码、devkit、platform Mock、Admin 测试 | `SubjectProfile`、平台生成的 identityScope、Provider identityEpoch、作用域键测试 | 只在本地联调;不部署平台,不影响生产 |
461
+ | B. Shell 内核 | provider、routing、shell、tabs、preferences | 主题、菜单校验、访问表达式、标准退出、标签/保活分离 | 旧实现仍可由前一 npm alpha 回退 |
462
+ | C. 个人中心解耦 | personal-center、workflow contribution | 完整个人中心,Workflow 代理按需加载 | 无 Workflow 应用的 bundle/E2E 必须通过 |
463
+ | D. Data Workbench 拆分 | data controller/query/list/form/detail | 不改变 Data API 语义,降低模块耦合 | 新旧查询请求做契约对照;无平台迁移 |
464
+ | E. 官方模板 | create-openxiangda 模板、文档、skills | 新建应用开箱即用 | 用临时新应用执行 install/check/test/build/e2e |
465
+ | F. 平台集成验证 | 本地 platform server + 真实 HTTP | bootstrap 资料/scope 与 Admin 身份生命周期一致 | A 阶段同一协议实现;不允许用只通过 Mock 的另一套结构 |
466
+ | G. 参考应用验收 | reference app | 多角色、权限、列表、流程、移动端全链路 | 本地版本验证后才发布 npm alpha |
467
+ | H. 发布与晋级 | changesets、npm、AppVersion | 不可变制品预发后晋级正式 | 同一 AppVersion,不在环境间重建 |
468
+
469
+ 实现阶段每个主题单独提交,不把 Shell 重构、Data API 语义变化和平台部署混在同一个提交或发布中。
470
+
471
+ 发布顺序固定为:先发布并部署向后兼容的平台能力,在预发验证现有 2.0 与代表性 1.x 应用不受影响;再发布要求 `authz.role-session-context` 的工具链包;最后由新建应用用不可变版本进入预发并晋级正式。不得先发布新版 Admin,再期待运行时碰巧连到已升级平台。
472
+
473
+ 平台登录代理的 callback 消费端必须在实现 B1 前完成全仓生产者审计。若当前合法生产者全部是同源地址,则在消费端统一收紧;若存在确需跨域的历史场景,必须改为注册 redirect URI 或服务端签名 continuation,不能为兼容历史继续接受任意 URL。该安全变更独立提交,并对 1.x 登录、SSO、CLI 授权和平台管理端分别回归。
474
+
475
+ ### 12.1 实施依赖与禁止跨层修补
476
+
477
+ 确认本文后,A-D 不是四组可以随意并行的界面任务,而是以下单向依赖:
478
+
479
+ ```mermaid
480
+ flowchart LR
481
+ A1["A1:SubjectProfile additive contract"] --> A2["A2:identityScope / identityEpoch"]
482
+ B0["B0:通用登录 callback 收敛"] --> B1["B1:路由、主题、标准退出"]
483
+ A2 --> B1
484
+ A2 --> B2["B2:标签与有界 keepAlive"]
485
+ B1 --> C["C:个人中心贡献化"]
486
+ A2 --> D1["D1:Data Workbench controller"]
487
+ D1 --> D2["D2:标准列表 UI 复用 controller"]
488
+ ```
489
+
490
+ 每个阶段的实现合同如下:
491
+
492
+ 0. **B0 通用登录 callback 收敛**
493
+ - 平台登录页只解析一次 callback,所有登录分支复用同一个 `safeCallback`;无效目标退回平台首页。
494
+ - CAS login-url 和 callback 在服务端按当前租户 origin 双重校验;CLI 同源一次性 session 路径保持可用。
495
+ - Admin 同应用校验与平台通用同源校验是两层不同策略,不复制成表面相同、实际语义冲突的函数。
496
+ - 该阶段独立回归 1.x view、平台管理端、流程设计器、CAS/SSO、第三方登录和 CLI 授权,不依赖新版 Admin 包。
497
+ 1. **A1 平台资料协议**
498
+ - `RoleSessionBootstrapResult` additive 增加 `subjectProfile` 和平台生成的 `identityScope`,旧客户端可忽略;不改 Principal,不增加 SQL migration。
499
+ - `active`、`selection_required` 和 `unassigned` 都返回当前已认证主体的资料;姓名为空时用 userId 作为 displayName。租户内用户记录不存在时 fail closed,不能把失效登录态包装成半份资料。
500
+ - 只有 active 返回 identityScope;其余状态返回 null。identityScope 是 UI 隔离标识,不可作为授权输入。
501
+ - 平台 capabilities 新增 `authz.role-session-context`;2.0 编译器自动把它加入基础 AppPackage,旧平台在上传制品前拒绝。
502
+ - 平台测试必须覆盖跨 tenant 主部门不能串联、无角色用户仍有资料、敏感字段不出现在序列化结果。
503
+ - 平台先交付 additive 响应与 capability;工具链随后增加 required capability。两次发布可独立回滚,不允许颠倒顺序。
504
+ 2. **A2 身份生命周期**
505
+ - Provider 消费平台返回的稳定 `identityScope`,并暴露仅在当前页面生命周期内单调增加的 `identityEpoch`;前端不得重新计算 scope。
506
+ - 现有 `appCode + environmentKey` 请求键改名为 `bootstrapRequestScope`,只用于合并 bootstrap 请求和拒绝旧响应;不得作为 UI identityScope 或持久化键。
507
+ - `identityScope` 绑定 tenant、app、environment、user 和 active assignment;同一 assignment 的会话续期不改变 scope,角色、用户、租户或环境变化必须改变 scope。
508
+ - RoleSession id 或 authzVersion 改变时 epoch 增加并清空授权、查询、跨页选择和页面实例;不得在 local/session storage 保存权限结果或业务响应。
509
+ 3. **B1 Shell 协议**
510
+ - `capability` 保留为简写,新增 `anyOf/allOf`;父访问约束按祖先到子路由合并,不能只隐藏菜单而允许直接访问。
511
+ - manifest 校验在开发启动、测试和生产构建执行同一个纯函数;重复 key/path、不可解析入口、参数固定标签和缺 fallback 均 fail closed。
512
+ - 默认 auth adapter 调用平台真实会话撤销接口,成功后进入 `/platform/login`,其 callback 只允许同 origin、同应用 runtime base;应用覆盖也必须经过相同 return URL 校验。失败时保留当前身份并允许重试。
513
+ 4. **B2 标签与保活**
514
+ - 标签存在、session 恢复和 React 实例保活拆成不同字段;默认有标签、可恢复但不保活。
515
+ - 最大 12 个标签、最大 6 个显式 memory keepAlive 页面;第 7 个按 LRU 卸载最久未访问实例。`pinned` 不豁免实例容量,动态详情、审批、创建和编辑默认不保活。
516
+ - identity epoch 改变时先卸载旧 scope 页面,再恢复新 scope 标签;不能等下一次 Data API 请求才发现身份变化。
517
+ 5. **C 个人中心解耦**
518
+ - Core 只渲染资料、身份、环境、范围、刷新、切换和退出。
519
+ - `PersonalCenterSectionContribution[]` 由应用显式传入;Workflow 包提供代理贡献,但 Core 源码和无 Workflow 应用 bundle 都不能依赖 Workflow chunk。
520
+ 6. **D Data Workbench**
521
+ - 第一阶段只移动查询、分页、排序、筛选、并发序列、选择和刷新状态,不改变 Data API 请求与外部 `DataListPage` 行为。
522
+ - controller 以 identity epoch 为失效边界;旧响应即使返回也不能写入新身份状态。偏好键使用完整 identityScope,并带 schema/config version。
523
+ - UI 层继续消费同一字段策略和 capability 结果;禁止在拆分过程中增加全量拉取、本地筛选或第二套查询 DSL。
524
+
525
+ 禁止为了某个页面先行添加临时全局状态、角色名判断、未绑定 environment 的 localStorage key 或 Core→Workflow 反向依赖。若实现中发现必须违反上述边界,应回到本文调整设计,而不是在组件里加例外。
526
+
527
+ ### 12.2 分阶段证明与回滚边界
528
+
529
+ | 阶段 | 必须新增的证明 | 回滚边界 |
530
+ | --- | --- | --- |
531
+ | B0 | 所有共享登录生产者正向回归;跨域/协议相对/凭据/控制字符/多重编码拒绝;CAS 服务端二次校验;CLI session 成功 | 平台前端与服务端各自独立提交;不改数据和认证协议 |
532
+ | A1 | contracts schema/type;平台 unit + 真实 HTTP bootstrap;旧客户端忽略 additive 字段、新 Admin 对缺字段 fail closed;三种状态都有资料且非 active scope 为 null;跨 tenant 主部门 fail closed;敏感字段负向断言;旧平台在制品上传前拒绝新 capability;本地 Mock 与真实平台响应契约对照 | 单独回滚平台协议提交或工具链 capability 要求;无数据库回滚 |
533
+ | A2 | 两租户、两环境、两用户、两角色 scope 不碰撞;重复 payload 不增 epoch;session/authz 变化增 epoch | 只回滚 Provider/contract,不影响业务页面协议 |
534
+ | B1 | manifest 负向测试、anyOf/allOf、直接 URL 403、真实登出撤销、登出失败保留身份、生成端与消费端恶意 return URL 拒绝 | 保留前一 npm alpha,可整体回退 Shell 包;登录代理校验独立提交 |
535
+ | B2 | 第 7 个 keepAlive LRU、身份切换同步卸载、标签恢复不恢复动态详情 | 仅标签/页面实例状态,无平台数据回滚 |
536
+ | C | 无 Workflow 模板的依赖图与生产 chunk 中不存在 Workflow;启用时代理 E2E 通过 | 移除 contribution 即回到核心个人中心 |
537
+ | D | 新旧请求契约对照;服务端搜索/排序网络断言;慢响应与角色切换不污染 | controller/UI 可按同一外部 props 回退 |
538
+
539
+ 以上每一阶段必须独立提交并通过 `pnpm --filter openxiangda-admin check test build`。A1 涉及平台时还必须通过 `npm run verify:openxiangda-v2:release`;A-D 全部完成后才运行临时 registry 新应用与持久 reference app 的完整浏览器验收。
540
+
541
+ ## 13. 验收标准
542
+
543
+ ### 13.1 身份与权限
544
+
545
+ - 两个租户、两个环境、两个用户和两个角色的偏好键互不复用。
546
+ - 角色切换后旧页面实例、选择项、授权结果和未完成查询全部失效。
547
+ - 切换失败仍可继续使用旧的有效角色会话。
548
+ - 同名角色但不同 scope grant 的选择项可以明确区分。
549
+ - 应用管理员绕过和普通角色拒绝都由后端证据验证。
550
+ - 直接输入无权限路由显示 403,不能通过隐藏菜单绕过。
551
+
552
+ ### 13.2 Shell 与交互
553
+
554
+ - 桌面端和 390px 移动端导航都可用。
555
+ - 固定、关闭、关闭其他、关闭全部、刷新、恢复标签行为确定。
556
+ - 保活页面数量有上限,动态详情默认不保活。
557
+ - 单页渲染错误不会导致整站白屏。
558
+ - 个人中心显示真实姓名/头像回退、当前身份、环境和数据范围。
559
+ - 退出登录回到同应用安全入口,跨域 return URL 被拒绝。
560
+ - 缺少 `authz.role-session-context` 的平台在部署预检阶段拒绝新版应用,浏览器不会进入不兼容运行时。
561
+ - 键盘焦点、Drawer/Modal focus trap、loading/error 状态通过浏览器测试。
562
+
563
+ ### 13.3 数据管理
564
+
565
+ - 所有搜索和排序请求都可在网络断言中证明由服务端执行。
566
+ - 慢请求不会覆盖后发请求;角色切换后的旧响应不会渲染。
567
+ - 字段无读权限时不出现在列、搜索、详情和导出中;无写权限时不出现在表单和导入中。
568
+ - create/update/delete/import/export 分别验证允许和拒绝路径。
569
+ - revision 冲突、网络失败、JSON 协议错误和空数据都有可恢复界面。
570
+ - 10/20/50/100 分页工作,不存在 1000 行本地筛选路径。
571
+
572
+ ### 13.4 工程门禁
573
+
574
+ - `pnpm --filter openxiangda-admin check test build`
575
+ - Ant Design 6 精确版本 API 查询和 `antd lint`
576
+ - 官方模板单测与完整 Playwright E2E
577
+ - 新建临时应用的 install/check/test/build/e2e
578
+ - 包依赖图无环、首屏和最大 chunk 预算通过
579
+ - npm pack 安装验证,不依赖 monorepo 隐式文件
580
+ - 参考应用四角色真实权限矩阵通过
581
+ - 平台变更先预发,公共页面和旧 1.x 应用回归无异常
582
+
583
+ 门禁按开发阶段分层,不把候选级开销重新塞回每个小改动:
584
+
585
+ - 单次提交/PR:`pnpm verify:affected`,Admin 阶段另跑 `pnpm --filter openxiangda-admin check test build`;浏览器交互实际变化时运行模板聚焦 Playwright。
586
+ - 本地完整里程碑:`pnpm verify:local`,用真实 tarball 创建并销毁全新应用,执行 generate/check/test/Chromium/build,并验证持久 reference app、Skills 和文档;它是主动全量验收,不是每次保存文件的必跑项。
587
+ - 正式候选:Changesets 经 `pnpm release:version` 物化并形成已推送的版本提交后,`pnpm release:publish` 冻结真实候选 tarball,再由机器生成并执行一次增量矩阵。每个候选始终在 monorepo 外的新应用完成 install/generate/check/test/build;只有 Admin/浏览器契约变化升级 Chromium,核心 SDK 变化增加临时 Verdaccio reference app,未知变化 fail closed 到全量。`pnpm verify:release` 仅用于无发布演练。
588
+ - 周期审计或重大 alpha:`pnpm verify:release:full`。发包不回放 1.x 测试,也不为多个候选包重复运行同一浏览器路径。
589
+
590
+ ## 14. 明确拒绝的方案
591
+
592
+ 1. **直接套 Ant Design Pro/Umi 项目**:会让 OpenXiangda 身份、权限、Data API、发布和路由协议依赖另一个应用框架,且当前 Pro Components 未声明支持 Ant Design 6。
593
+ 2. **同时保留 ProTable 和 DataManagementList 两套标准**:查询、字段权限、偏好和导入导出会长期分叉。
594
+ 3. **平台下发菜单组件或页面代码**:破坏构建可复现、版本控制、CSP 和安全审计。
595
+ 4. **用 localStorage 长期保存页面数据或权限结果**:角色撤销和环境切换后会留下陈旧敏感状态。
596
+ 5. **为了显示姓名调用旧用户管理接口**:个人资料是平台身份协议的一部分,不应要求业务角色获得组织管理权限。
597
+ 6. **切换角色后继续复用旧 React 页面实例**:旧组件可能携带旧数据、表单草稿和选择项。
598
+ 7. **一次完成所有源码重写再测试**:必须按协议、Shell、个人中心、数据页、模板分阶段,每阶段都有可执行门禁。
599
+
600
+ ## 15. 已确认假设与待确认门禁
601
+
602
+ 已确认:
603
+
604
+ - 2.0 可以放弃 1.x 兼容,1.x 继续在原有代码线维护。
605
+ - Admin 是标准 B 端后台,PC 为主,同时保证移动端基本可用。
606
+ - OAuth2、稳定单角色会话、应用管理员后端绕过和三环境模型保持不变。
607
+ - Data API 是标准数据访问入口,复杂敏感业务写操作进入 NestJS App API。
608
+ - 本地未发布包和新建独立应用可以用于先行验证。
609
+
610
+ 等待确认后开始实现的门禁:
611
+
612
+ - 采用本文整体方案;
613
+ - 不引入 Pro Components 运行时,基于 Ant Design 6 完善自有 Admin 协议组件;
614
+ - 标准路由默认有标签但不默认保活,列表/工作台按需开启保活;
615
+ - 个人中心采用平台 `SubjectProfile` 只读资料协议,Workflow 代理改为可选贡献;
616
+ - 首轮实现先完成 A-D 和本地新应用验证,再决定 npm alpha 与平台发布时点。
@@ -21,11 +21,12 @@
21
21
  ## 测试层级
22
22
 
23
23
  1. 提交与 PR 使用 `pnpm verify:affected`,只运行受影响包及其依赖任务。
24
- 2. 发布候选先运行 `pnpm release:plan`,由候选 tarball 差异和包依赖图确定门禁;`pnpm verify:release` 执行该计划。
25
- 3. `pnpm release:publish` 从干净的远端 `master` 生成一次带摘要的候选工件清单;候选安装、reference 验证和 npm publish 复用相同 tarball,禁止重新打包。
26
- 4. 每次候选都在全新独立项目安装真实 tarball 并完成 generate/check/test/build;浏览器相关变化再执行完整 Chromium E2E。
27
- 5. 平台集成测试部署到测试环境;只有平台协议或部署能力变更才需要阻塞核心发布。
28
- 6. prod-1 只晋级已经验证的相同 npm 版本、AppPackage 和 OCI digest,不重新构建。
24
+ 2. 已评审 Changesets 先由 `pnpm release:version` 在干净且已同步远端的 `master` 上物化;人工只审核生成的版本、内部依赖与模板 BOM,不手改版本。版本 diff 必须提交并推送后才成为候选源码事实。
25
+ 3. 发布候选运行 `pnpm release:plan`,由真实候选 tarball 差异和包依赖图确定门禁;待消费 Changesets 未物化时在任何构建前失败。
26
+ 4. `pnpm release:publish` 从干净的远端 `master` 生成一次带摘要的候选工件清单;候选安装、reference 验证和 npm publish 复用相同 tarball,禁止重新打包,并且只执行一次正式验证。`pnpm verify:release` 只用于需要提前获得无发布验收证据的演练,不是正式发布前必须重复执行的步骤。
27
+ 5. 每次候选都在全新独立项目安装真实 tarball 并完成 generate/check/test/build;浏览器相关变化再执行完整 Chromium E2E。
28
+ 6. 平台集成测试部署到测试环境;只有平台协议或部署能力变更才需要阻塞核心发布。
29
+ 7. prod-1 只晋级已经验证的相同 npm 版本、AppPackage 和 OCI digest,不重新构建。
29
30
 
30
31
  任何层级都不得调用 1.x 测试套件。
31
32
 
@@ -34,18 +35,22 @@
34
35
  ```text
35
36
  reviewed Changesets
36
37
  -> authoritative master commit
38
+ -> deterministic Changesets version materialization
39
+ -> reviewed version commit on authoritative master
37
40
  -> frozen dependency install
38
41
  -> immutable tarball/version preflight
39
42
  -> deterministic affected validation plan
40
43
  -> candidate closure + independent tarball verification
41
- -> deterministic version commit
42
- -> CI npm publish
44
+ -> npm publish of the exact validated tarballs
43
45
  -> independent-project acceptance
44
46
  -> immutable promotion
45
47
  ```
46
48
 
47
49
  发包阶段不允许 AI 决定包、版本、测试范围或发布顺序。AI 可以编写代码和
48
- Changeset,但最终范围由 Git diff、workspace 依赖图和机器校验共同确定。
50
+ Changeset,但包版本只能由 Changesets 物化,最终范围由 Git diff、workspace
51
+ 依赖图和机器校验共同确定。版本物化不提交、不发布;正式候选必须是远端
52
+ `master` 可审计提交,Git 中的 package.json、候选 tarball、npm 版本和 Git tag
53
+ 共同指向同一份源码事实。
49
54
 
50
55
  增量门禁不是按文件名随意跳测试:计划器读取 npm 当前版本和上一发布版本,解包并比较真实发行物。候选包和依赖闭包永远 check/test/build,候选 tarball 永远安装进全新应用。Admin 或浏览器契约变化触发 Chromium;核心 SDK/后端/Workflow 变化触发 reference app;Skill Kit 变化触发 Skill 与文档;未知包、没有可比较前版或显式 `--full` 一律运行完整矩阵。正式写 registry 前再次确认候选版本仍未被并发发布。
51
56
 
package/docs/delivery.md CHANGED
@@ -42,15 +42,17 @@ stateDiagram-v2
42
42
 
43
43
  Changesets 管理各个 `openxiangda-*` 包、CLI、MCP 与 skill-kit 的独立版本和内部依赖传播。官方模板固定经过同一候选矩阵验证的精确 BOM。文档参考从命令和 MCP 注册表生成,避免文档与实现漂移。
44
44
 
45
+ 版本物化是发布状态机的独立步骤:`pnpm release:version` 只允许在干净、已同步 `origin/master` 的 `master` 上运行,把尚未消费的评审 Changesets 确定性写入包版本、内部依赖和模板 BOM;它不提交、不发包。生成 diff 经审核、提交并推送后,才允许 `release:plan`、`verify:release` 或 `release:publish` 识别候选。这样 Git 中的版本清单是 npm 工件和 Git tag 的唯一源码事实,不使用临时目录里的虚拟版本,也不允许手工跳过物化。
46
+
45
47
  `release:publish` 在真正写 registry 前执行不可变版本门禁:未发布版本进入候选集;已经发布的版本则分别解包 registry 工件和当前本地包并逐文件比较。相同内容视为未变包,不重复发布;任何内容差异都必须先用 Changesets 产生新版本;没有新版本时拒绝空发布。发布范围、版本和是否允许写入由机器判定,不由 AI 临场决定。
46
48
 
47
49
  发布入口只打包一次,并在 Git 私有目录写入绑定源码 `HEAD`、registry、包版本、字节数、SHA-256 与 npm SHA-512 integrity 的工件清单。全新应用、独立 reference app 与正式 npm publish 必须消费同一批 `.tgz`;任何字节或清单变化都会在写 registry 前失败。部分发布中断后可以从 receipt 继续:已经发布且 integrity 一致的包被跳过,不一致则停止。npm 在逐包写入时可能先推进预发布 tag;receipt 只接受这一个可恢复中间态,并在所有包确认后统一收敛最终 dist-tag 和 Git tag。
48
50
 
49
- `pnpm verify:local` 是日常可重复执行的全量验收入口。正式候选先由 `pnpm release:plan` 比较 npm 与本地 tarball,并比较候选与上一发布版本;漏升版会在昂贵测试前直接失败。`pnpm verify:release` 执行机器生成的增量计划:候选依赖闭包永远完成 check/test/build,每个候选 tarball 都在 monorepo 外安装进全新生成应用并完成 generate/check/test/build;浏览器相关变化提升到真实 Chromium E2E,核心应用 SDK 变化增加持久 reference app,Skill/文档变化增加相应门禁。未知变化 fail-closed 到完整矩阵。`pnpm verify:release:full` 保留周期性全量审计。
51
+ `pnpm verify:local` 是日常可重复执行的全量验收入口。正式候选在版本提交推送后由 `pnpm release:plan` 比较 npm 与本地 tarball,并比较候选与上一发布版本;漏升版或待消费 Changeset 会在昂贵测试前直接失败。`pnpm release:publish` 冻结一次候选工件,执行机器生成的增量计划,并在成功后发布同一批 tarball;候选依赖闭包永远完成 check/test/build,每个候选 tarball 都在 monorepo 外安装进全新生成应用并完成 generate/check/test/build。浏览器相关变化提升到真实 Chromium E2E,核心应用 SDK 变化增加持久 reference app,Skill/文档变化增加相应门禁。`pnpm verify:release` 提供相同规则的无发布演练,不能与正式发布组合成必须重复执行的双门禁。未知变化 fail-closed 到完整矩阵;`pnpm verify:release:full` 保留周期性全量审计。
50
52
 
51
53
  Playwright 浏览器使用官方缓存目录;`playwright install chromium` 已安装对应版本时是无操作。发包门禁不会额外运行 1.x 测试,也不会为每个包重复浏览器验收,而是在最终独立新应用上只运行一次完整用户路径。
52
54
 
53
- 当计划要求 reference app 时,工具仓只把本次候选包发布到一次性、仅绑定 `127.0.0.1` 的 registry;未变化的依赖继续从 npm 代理取得。持久独立 reference app 再执行安装、契约生成、类型检查、单测、真实 NestJS 身份/Data API 进程验收和生产构建。`pnpm reference:install:from-build` 可在未公开发包时通过同一 registry 协议刷新 reference worktree 的本地依赖,避免 `file:`/`link:` 破坏独立性。
55
+ 当计划要求 reference app 时,工具仓只把本次候选包发布到一次性、仅绑定 `127.0.0.1` 的 registry。每个候选包按精确包名注册为本地权威源且禁止回源,防止同版本公网包抢先占位或旧包被静默安装;未变化的包和普通依赖才继续从 npm 代理取得。持久独立 reference app 再执行安装、契约生成、类型检查、单测、真实 NestJS 身份/Data API 进程验收和生产构建。`pnpm reference:install:from-build` 可在未公开发包时通过同一 registry 协议刷新 reference worktree 的本地依赖,避免 `file:`/`link:` 破坏独立性。
54
56
 
55
57
  增量门禁先用 Turbo 完成候选包及其依赖闭包的 check/test/build;全量模式完成整个 workspace。后续生成契约、tarball 黑盒、技能校验和文档构建复用已验证的 `dist`。`distribution:smoke`、`skills:check`、`docs:build` 仍保留可独立执行的自包含入口。
56
58
 
@@ -67,7 +69,7 @@ pnpm distribution:smoke
67
69
  当同一候选版本还修改了 OpenXiangda 2.0 平台接口时,平台服务必须先运行自己的独立门禁:
68
70
 
69
71
  ```bash
70
- npm run verify:openxiangda-v2
72
+ npm run verify:openxiangda-v2:release
71
73
  ```
72
74
 
73
- 该门禁校验平台 SQL migration、自动发现所有 2.0 测试、运行共享出站 HTTP/存储安全测试并完成平台编译。它与 `pnpm verify:release` 分工明确:前者证明平台实现,后者证明独立 SDK、CLI、技能、模板和真实 tarball 新应用。两边都通过后才可进入人工发布确认;任何一个命令都不会自动提交、发包或部署。
75
+ 该门禁固定执行一次平台 SQL migration、全部 2.0 测试、共享出站 HTTP/存储安全测试和平台编译,再连续执行两次真实 OAuth2、Data API、App API、Events 与 Workflow HTTP 纵向链路。根平台发布器只在 Platform Server 进入最终镜像构建计划时自动调用,并保证它发生在第一个 Docker build 之前。它与 `pnpm verify:release` 分工明确:前者证明平台实现,后者证明独立 SDK、CLI、技能、模板和真实 tarball 新应用。两边都通过后才可进入人工发布确认;任何一个命令都不会自动提交、发包或部署。
@@ -57,14 +57,17 @@ pnpm verify:local
57
57
 
58
58
  它会用本地 tarball 创建并销毁一个独立新应用,覆盖脚手架、安装、生成、单测、Chromium 交互、生产构建、Skill 和文档,不会执行 npm 发布、Git 提交或平台部署。
59
59
 
60
- 准备发包时先查看机器生成的影响计划:
60
+ 准备发包时,先从干净且已同步远端的 `master` 物化已经评审的 Changesets,
61
+ 检查生成的包版本、内部依赖与模板 BOM,提交并推送该版本提交,然后查看机器生成的影响计划:
61
62
 
62
63
  ```bash
64
+ pnpm release:version
65
+ # review, commit and push the generated version diff
63
66
  pnpm release:plan
64
- pnpm verify:release
67
+ pnpm release:publish
65
68
  ```
66
69
 
67
- `release:plan` 会在运行昂贵测试前完成 npm 不可变性检查并比较候选包与上一发布版本的真实 tarball。`verify:release` 至少执行候选依赖闭包的 check/test/build 和全新应用 tarball 安装;只有 Admin/浏览器契约、核心应用 SDK、模板、Skills 或文档实际变化时才增加对应 Chromium、reference app、Skill 或 VitePress 门禁。无法分类的新包或变化自动升级为全量验证。周期审计使用 `pnpm verify:release:full`。
70
+ `release:version` 不提交、不发包,只把 Changesets 确定的候选版本写入 Git 工作树;未完成这一步时,后续所有发布命令会在构建前失败。`release:plan` 会完成 npm 不可变性检查并比较候选包与上一发布版本的真实 tarball。`release:publish` 冻结一批带摘要的候选 tarball,只针对这批工件执行一次正式增量门禁,成功后再发布同一批字节;不会在发包前后重复跑同一测试。需要提前做一次不写 registry 的演练时单独运行 `pnpm verify:release`,它不是正式发布的必选重复步骤。无法分类的新包或变化自动升级为全量验证,周期审计使用 `pnpm verify:release:full`。
68
71
 
69
72
  ## OAuth2 应用身份
70
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-skill-kit",
3
- "version": "2.0.0-alpha.18",
3
+ "version": "2.0.0-alpha.19",
4
4
  "description": "Validation and deterministic packaging for OpenXiangda 2.0 AI skills.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,9 +36,13 @@ command itself. A failed deployment must leave the currently active key intact.
36
36
 
37
37
  ## Toolchain release
38
38
 
39
- When changing or publishing OpenXiangda 2.0 itself, run
40
- `pnpm release:plan` before `pnpm verify:release`. The gate is native to the
41
- independent 2.0 repository and must not invoke 1.x compatibility suites. The
39
+ When changing or publishing OpenXiangda 2.0 itself, first run
40
+ `pnpm release:version` from a clean, up-to-date `master`. Review, commit, and
41
+ push the generated package/template version diff; the command itself never
42
+ commits or publishes. Only then run `pnpm release:plan`. Planning, validation,
43
+ and publishing fail before builds while reviewed Changesets remain
44
+ unmaterialized. The gate is native to the independent 2.0 repository and must
45
+ not invoke 1.x compatibility suites. The
42
46
  plan compares current npm tarballs before expensive tests, rejects changed
43
47
  published versions, compares candidates with their nearest earlier release and
44
48
  maps actual artifact differences to deterministic gates. Candidate dependency
@@ -47,7 +51,10 @@ the monorepo into a newly generated application. Admin/browser changes require
47
51
  Chromium, core application SDK changes require the independent reference app,
48
52
  and Skill Kit changes require Skills/docs. Unknown or first-release packages
49
53
  fail closed to the full matrix. Run `pnpm verify:release:full` for the periodic
50
- complete audit. The final publish command repeats version availability before
54
+ complete audit. `pnpm release:publish` freezes one candidate artifact manifest,
55
+ runs the formal gate once, and publishes those exact bytes. `pnpm
56
+ verify:release` is a publish-free rehearsal, not a second mandatory step before
57
+ `release:publish`. The final publish command repeats version availability before
51
58
  writing npm, so concurrent publication cannot invalidate the reviewed plan.
52
59
  `pnpm distribution:smoke` runs only this packed-distribution verification.
53
60
  Keep `strictDepBuilds: true` in the official workspace and template. Permit only
@@ -56,9 +63,13 @@ warning with a broad allow-all setting. The packed gate must fail if a fresh
56
63
  install reports an ignored build script.
57
64
 
58
65
  If the same release train changes the OpenXiangda 2.0 platform server, run its
59
- `npm run verify:openxiangda-v2` gate before the independent toolchain gate. It
60
- must validate SQL migrations, discover every platform v2 suite, run the shared
61
- HTTP/storage security suites, and compile without pulling in the 1.x baseline.
66
+ `npm run verify:openxiangda-v2:release` gate before the independent toolchain
67
+ gate. It must validate SQL migrations, discover every platform v2 suite, run
68
+ the shared HTTP/storage security suites, compile without pulling in the 1.x
69
+ baseline, and complete the real OAuth2/Data API/App API/Events/Workflow HTTP
70
+ chain twice. The platform root release script invokes this gate automatically
71
+ only when Platform Server is in its resolved image build plan, before any
72
+ Docker candidate is created.
62
73
  Passing either local gate is evidence for review, never authorization to commit,
63
74
  publish, or deploy.
64
75