openxiangda-skill-kit 2.1.3 → 2.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-skill-kit",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "OpenXiangda 2.0 中文 AI 技能的校验、分发与安装。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "openxiangda-devkit-core": "2.11.0"
20
+ "openxiangda-devkit-core": "2.13.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "tsx": "4.23.12",
@@ -63,6 +63,7 @@ pnpm dlx openxiangda@__OPENXIANGDA_VERSION__ skill install --force
63
63
  | 模糊想法、模块发现、PRD、权限与架构设计 | [产品设计](references/product-design.md)、[交互模式](references/interaction-patterns.md) |
64
64
  | 界面设计、改版、原型和视觉修正 | 先读[设计工作流](references/design-workflow.md),使用 `openxiangda design open` 和 `design cli` 调用原版;[离线方法](references/opendesign-methods.md)与[设计 Craft](references/design-craft.md)仅作补充 |
65
65
  | 理解需求与选择能力 | [开发流程](references/development.md)、[架构](references/concepts.md) |
66
+ | 写 openxiangda.config.ts 声明、避免首轮校验返工 | [声明速查](references/declarations-cheatsheet.md);先扫规则表再动手 |
66
67
  | 模型、CRUD、字段与移动表单 | [业务模块](references/application-foundation.md)、[字段](references/field-components.md) |
67
68
  | 图片压缩、缩略图、附件和缓存 | [图片与附件读取](references/field-components.md#图片缩略图和附件读取):卡片优先缩略图,原图按需,使用平台权限与缓存规则 |
68
69
  | 页面、标准组件与扩展 | [前端](references/frontend.md) |
@@ -74,6 +74,41 @@ ISO 时间字符串;不接受空值、嵌套路径、引用或表达式。offs
74
74
  最多正负 366 天的整数,所有时间条件共享一个接受时刻。需要平台 Data API 1.1.0。
75
75
 
76
76
 
77
+
78
+ ### 事务内写入快照字段 {#snapshot-values}
79
+
80
+ option / user / department / resource-ref 字段在 Data API 与事务写入里保存 `{ label, value }` 显示快照,`value` 是比较键。写裸字符串会被字段校验拒绝(`OPENXIANGDA_NATIVE_DATA_OBJECT_REQUIRED`)。使用助手函数避免手写形状:
81
+
82
+ ```ts
83
+ import { optionSnapshot, userSnapshot, resourceSnapshot } from 'openxiangda/nest';
84
+
85
+ data: {
86
+ status: optionSnapshot('处理中', 'processing'),
87
+ assignedTechnician: userSnapshot(input.technicianId),
88
+ requestId: resourceSnapshot('repair-requests', input.requestId, title),
89
+ }
90
+ ```
91
+
92
+ 读取判断状态用 `record.data.status?.value === 'pending'`。
93
+
94
+ ### 幂等冲突复核 {#idempotency-recovery}
95
+
96
+ 同一 `idempotencyKey` 要求内容指纹一致。update 事务携带 `expectedRevision`,重试时 revision 已前进会触发 409 `OPENXIANGDA_NATIVE_DATA_IDEMPOTENCY_CONFLICT`。捕获后回读当前状态确认效果已生效,按幂等结果返回;不要换新键重试:
97
+
98
+ ```ts
99
+ import { isIdempotencyConflict } from 'openxiangda/nest';
100
+
101
+ try {
102
+ result = await this.data.transaction(idempotentTransaction(key, operations, guards));
103
+ } catch (error) {
104
+ if (isIdempotencyConflict(error) && alreadyApplied(await this.data.get(...))) {
105
+ return { idempotencyKey: key, replayed: true, ...currentState };
106
+ }
107
+ throw error;
108
+ }
109
+ ```
110
+
111
+ 事务守卫的 `errorCode` 必须匹配 `^OPENXIANGDA_[A-Z0-9_]{1,96}$`,例如 `OPENXIANGDA_REPAIR_REQUEST_NOT_PENDING`。
77
112
  ## 业务动作与普通查询 {#business-action}
78
113
 
79
114
  `OpenXiangdaDataApiService` 按当前用户的普通资源、行和字段权限执行。具名业务动作使用 `OpenXiangdaBusinessDataApiService`:入口先检查该动作 capability,平台在精确应用和环境内以受信任后端执行,并保留发起人与动作审计。业务动作不能接受任意模型/字段/用户 ID 后不做业务校验;应用负责该动作的输入约束和业务不变量。
@@ -0,0 +1,191 @@
1
+ # 声明速查:一次写对 openxiangda.config.ts {#cheatsheet}
2
+
3
+ 按"错误码 → 规则 → 正确片段"组织。这些规则全部来自真实返工:先扫一遍本页,再写声明,能省掉绝大多数首轮校验迭代。普通 CRUD 的完整可过检骨架见文末。
4
+
5
+ ## 模块与 CRUD 视图
6
+
7
+ | 规则 | 正确写法 |
8
+ | --- | --- |
9
+ | `crud[].model` 必须引用本模块已声明的模型 | `crud: [{ model: 'repair-requests', ... }]` |
10
+ | `user: true` 一行声明即可生成用户端标准面(“我的记录”列表 + 提交表单,双端),并自动成为登录落地页 | `crud: [{ model: 'supply-requests', user: true, ... }]`;多资源时 `user: { home: true }` 指定落地资源,“仅本人”是展示过滤,行级隔离仍用 dataPolicies |
11
+ | 视图 `list`/`form`/`detail` 的 `model` 可省略(继承视图模型);显式声明时必须与 `crud[].model` 一致 | `list: { fields: [...] }` 即可,不必写 `model` |
12
+ | 每个模型最多 20 个命名视图;命名视图需要稳定 `code` + `name` | `crud: [{ model: 'x', code: 'x-active', name: '进行中', ... }]` |
13
+ | 新建视图的 `form.fields` 必须覆盖全部无默认必填字段,或显式 `generated.create: false` | 检查器会列出缺失字段 |
14
+
15
+ ## 字段声明
16
+
17
+ | 规则 | 正确片段 |
18
+ | --- | --- |
19
+ | `option.*` 字段的值是 `{ label, value }` 快照(比较键是 `value`) | `options: [{ label: '教学设备', value: 'teaching' }]` |
20
+ | `number.integer` 可省略精度;只允许 `precision`(位数),不允许 `scale` | `{ code: 'qty', type: 'number.integer' }` |
21
+ | `number.decimal` 的 `precision` 必填、`scale` 0 到 precision | `{ type: 'number.decimal', precision: 12, scale: 2 }` |
22
+ | `audit.read` 可写 `true`(绑定本资源读能力)或能力数组 | `audit: { read: true }` |
23
+ | `resource-ref.*` 必须带 `source` 来源协议 | `{ type: 'resource-ref.single', source: { kind: 'resource', resourceCode: 'repair-requests', labelField: 'title', searchFields: ['title'], pageSize: 20, loadMode: 'search' } }` |
24
+ | `labelField` 必须指向目标资源的 `text.short` / `text.long` 字段 | 不要用流水号/选项字段当 label |
25
+ | 每个字段都必须带中文/业务 `label`(含子表外键与排序字段) | `{ code: 'requestId', type: 'uuid', label: '所属申请', required: true }` |
26
+ | 子表 `subtable` 的外键是子资源的 **uuid** 字段,排序字段是**可写 number.integer** | 子资源:`{ code: 'requestId', type: 'uuid', required: true }` + `{ code: 'sortOrder', type: 'number.integer', required: true }`;父表:`subtable: { resourceCode: 'repair-items', foreignKey: 'requestId', orderField: 'sortOrder', maxRows: 20 }` |
27
+ | 图片/附件的 `file` 限定数量与大小 | `file: { maxCount: 3, maxSizeMb: 10, accept: ['image/png', 'image/jpeg'] }` |
28
+
29
+ ## 权限声明
30
+
31
+ | 规则 | 正确片段 |
32
+ | --- | --- |
33
+ | 平台保留能力(如 `app:<app>:directory:read`)**不能**在 `capabilities` 里重复声明,直接在角色中引用即可 | `const directoryRead = \`app:\${APP_CODE}:directory:read\`` → `roles: [{ code: 'admin', capabilities: [directoryRead] }]` |
34
+ | 资源 CRUD 能力码用 `resourceCapabilityCodes(appCode, resourceCode)` 生成 | `const crud = resourceCapabilityCodes(APP_CODE, 'repair-requests')` → `capabilities: [crud.read, crud.create]` |
35
+ | `authenticatedUserRoleCode` 是平台登录用户的基线角色 | `authz: { authenticatedUserRoleCode: 'app-user', ... }` |
36
+
37
+ ## 后端操作(Nest)
38
+
39
+ | 规则 | 正确片段 |
40
+ | --- | --- |
41
+ | 写操作的 `ai.sideEffects` 至少一条具体副作用 | `ai: { name: '受理派单', ..., risk: 'write', sideEffects: ['更新报修单状态为处理中', '写入一条派工记录'] }` |
42
+ | GET 操作的 `ai.risk` 只能是 `read`;写操作不能是 `read` | `risk: 'read'` ↔ `method: 'GET'` |
43
+ | controller 路由必须绑定 `@OpenXiangdaOperation(appOperations.<code>)`,普通 CRUD 不写 controller | check 门禁会拒绝未绑定路由 |
44
+ | 事务守卫 `errorCode` 必须匹配 `^OPENXIANGDA_[A-Z0-9_]{1,96}$` | `errorCode: 'OPENXIANGDA_REPAIR_REQUEST_NOT_PENDING'` |
45
+
46
+ ## 事务写入快照字段
47
+
48
+ option / user / department / resource-ref 字段在事务和普通写入里都必须写快照对象,不能写裸字符串:
49
+
50
+ ```ts
51
+ import { optionSnapshot, userSnapshot, resourceSnapshot } from 'openxiangda/nest';
52
+
53
+ await this.data.transaction(idempotentTransaction(input.idempotencyKey, [
54
+ {
55
+ operation: 'update',
56
+ resourceCode: 'repair-requests',
57
+ id: input.requestId,
58
+ expectedRevision: revision,
59
+ data: {
60
+ status: optionSnapshot('处理中', 'processing'), // 不是 'processing'
61
+ assignedTechnician: userSnapshot(input.technicianId), // 不是裸 userId
62
+ },
63
+ },
64
+ {
65
+ operation: 'create',
66
+ resourceCode: 'repair-assignments',
67
+ data: {
68
+ requestId: resourceSnapshot('repair-requests', input.requestId, requestTitle),
69
+ technician: userSnapshot(input.technicianId),
70
+ },
71
+ },
72
+ ]));
73
+ ```
74
+
75
+ 读取时状态判断用 `record.data.status?.value === 'pending'`(存储值是快照对象)。
76
+
77
+ ## 幂等冲突复核模式
78
+
79
+ update 事务必须携带 `expectedRevision`;重试时 revision 已前进会让同一幂等键的内容指纹漂移,平台返回 409 `OPENXIANGDA_NATIVE_DATA_IDEMPOTENCY_CONFLICT`(而非 `replayed: true`)。标准处理:
80
+
81
+ ```ts
82
+ import { isIdempotencyConflict } from 'openxiangda/nest';
83
+
84
+ try {
85
+ result = await this.data.transaction(idempotentTransaction(key, operations, guards));
86
+ } catch (error) {
87
+ if (isIdempotencyConflict(error)) {
88
+ const current = await this.data.get('repair-requests', input.requestId);
89
+ if (/* 状态已离开 pending,说明本键的效果已生效 */) {
90
+ return { idempotencyKey: key, replayed: true, ... 当前状态 };
91
+ }
92
+ }
93
+ throw error;
94
+ }
95
+ ```
96
+
97
+ 不要用新生成的幂等键重试冲突——那会绕过幂等保护重复执行业务动作。
98
+
99
+ ## 两模型起步骨架(可直接改造)
100
+
101
+ ```ts
102
+ import {
103
+ adminNavigationGroup, adminResourcePage, defineAdminNavigation,
104
+ defineApplicationModule, defineOpenXiangdaApp, resourceCapabilityCodes,
105
+ } from 'openxiangda/config';
106
+
107
+ const APP_CODE = 'my-app';
108
+ const requestCrud = resourceCapabilityCodes(APP_CODE, 'requests');
109
+
110
+ const requests = {
111
+ code: 'requests', name: '申请单',
112
+ audit: { read: true },
113
+ fields: [
114
+ { code: 'title', type: 'text.short', label: '标题', required: true },
115
+ { code: 'category', type: 'option.single', label: '类别', required: true,
116
+ options: [{ label: '普通', value: 'normal' }, { label: '紧急', value: 'urgent' }] },
117
+ { code: 'status', type: 'option.single', label: '状态', required: true,
118
+ options: [{ label: '待受理', value: 'pending' }, { label: '已完成', value: 'done' }] },
119
+ { code: 'photo', type: 'image', label: '照片', file: { maxCount: 3, maxSizeMb: 10 } },
120
+ ],
121
+ };
122
+ const items = {
123
+ code: 'request-items', name: '明细',
124
+ fields: [
125
+ // 所有字段(含子表外键/排序)都必须声明中文 label。
126
+ { code: 'requestId', type: 'uuid', label: '所属申请', required: true },
127
+ { code: 'sortOrder', type: 'number.integer', label: '排序', required: true },
128
+ { code: 'name', type: 'text.short', label: '名称', required: true },
129
+ { code: 'qty', type: 'number.integer', label: '数量' },
130
+ { code: 'price', type: 'number.decimal', label: '单价', precision: 12, scale: 2 },
131
+ ],
132
+ };
133
+
134
+ export default defineOpenXiangdaApp({
135
+ app: { code: APP_CODE, name: '我的应用' },
136
+ frontend: {
137
+ root: 'apps/web',
138
+ devicePolicy: { kind: 'viewport-family', mobileMaxWidthPx: 900, desktopMinWidthPx: 901 },
139
+ routes: [
140
+ { code: 'application-home', path: '/home', label: '应用首页', surface: 'user' },
141
+ { code: 'application-home-mobile', path: '/m/home', label: '移动首页', surface: 'user' },
142
+ ],
143
+ authentication: {
144
+ accountMode: 'existing-platform-users-only',
145
+ registration: { mode: 'reject' },
146
+ methods: [{ code: 'password', type: 'password', label: '账号密码登录', presentation: 'primary', required: true }],
147
+ surfaces: {
148
+ desktop: { routeCode: 'application-login', path: '/login', defaultRouteCode: 'application-home' },
149
+ mobile: { routeCode: 'application-login-mobile', path: '/m/login', defaultRouteCode: 'application-home-mobile' },
150
+ },
151
+ },
152
+ admin: {
153
+ navigation: defineAdminNavigation([
154
+ adminNavigationGroup('main', '业务管理', [adminResourcePage('requests', { label: '申请单' })], { icon: 'database', order: 100 }),
155
+ ]),
156
+ },
157
+ },
158
+ modules: [defineApplicationModule({
159
+ code: 'main',
160
+ models: [requests, items],
161
+ crud: [
162
+ {
163
+ model: 'requests',
164
+ list: {
165
+ fields: ['title', 'category', 'status'],
166
+ filterFields: ['category', 'status'],
167
+ searchableFields: ['title'],
168
+ defaultPageSize: 20,
169
+ defaultSort: { field: 'title', order: 'asc' },
170
+ },
171
+ mobile: { enabled: true },
172
+ },
173
+ ],
174
+ })],
175
+ authz: {
176
+ authenticatedUserRoleCode: 'app-user',
177
+ capabilities: [],
178
+ roles: [
179
+ { code: 'app-user', name: '应用用户', capabilities: [requestCrud.read, requestCrud.create] },
180
+ { code: 'admin', name: '管理员', capabilities: [requestCrud.read, requestCrud.create, requestCrud.update, requestCrud.delete] },
181
+ ],
182
+ scopeDimensions: [], scopeSources: [], dataPolicies: [], authorizationTransitions: [],
183
+ },
184
+ });
185
+ ```
186
+
187
+ `request-items` 不出现在 `crud` 里:子表行随 `requests` 表单的 `subtable` 字段写入(在 `requests.fields` 里补 `{ code: 'items', type: 'subtable', subtable: { resourceCode: 'request-items', foreignKey: 'requestId', orderField: 'sortOrder', maxRows: 20 } }`)。
188
+
189
+ ## 图片上传的像素上限
190
+
191
+ image/signature/富文本图片字段在上传计划(initiate)里返回 `maxPixels`;超过上限的图片会被标准组件自动压缩后重新发起上传。用 API 直传时自行按 `maxPixels` 预检。当前平台上限覆盖主流手机主摄(48/50/64MP);超出会得到带实际尺寸的 `OPENXIANGDA_NATIVE_DATA_IMAGE_PIXEL_LIMIT_EXCEEDED` 错误。
@@ -63,7 +63,22 @@ capability、应用角色和数据策略。前端只根据当前登录用户完
63
63
  可替换 Data API adapter。不要调用自定义 Nest CRUD、Function 或 Workflow 来绕过
64
64
  Data API。只有真正需要事务或外部系统的动作才使用同源 `/api`。
65
65
 
66
- ### 自定义页面消费平台数据 {#data-access}
66
+ ### 生成式用户标准面 {#user-surface}
67
+
68
+ 对普通用户(登录基线角色)开放提交的 CRUD 资源,在 CRUD 视图上声明 `user: true` 即可获得
69
+ 开箱即用的用户端标准页,无需编写任何 React 代码:
70
+
71
+ - “我的记录”列表(`/my/<resource>`,移动端 `/m/my/<resource>`):默认仅显示当前登录用户
72
+ 创建的记录(`created_by` 展示过滤),支持分页、行详情抽屉与“提交”入口。
73
+ - “提交”表单(`/my/<resource>/submit`):复用标准字段渲染与文件上传,成功后回到列表。
74
+ - 首页接线:首个启用资源自动成为登录落地页与根路径;多资源时用 `user: { home: true }` 显式指定;
75
+ 声明的 `/home` 占位路由保留可用,也可以继续用自定义页面覆盖用户端体验。
76
+ - 能力接线自动完成:列表要求资源 read,提交要求 create;无能力的用户看到标准 403 面。
77
+
78
+ 注意:“仅本人”是页面展示过滤,不是授权边界。需要服务端强制行级隔离时按[数据与权限](data-authz.md)
79
+ 声明 dataPolicies。
80
+
81
+ ## 自定义页面消费平台数据 {#data-access}
67
82
 
68
83
  自定义报表、工具页和用户端页面从 `openxiangda/core` 导入 `createNativeResourceClient`,
69
84
  用生成契约里的 surface 直接获得该资源的权威读写客户端;行、字段与操作授权由平台在