openxiangda-skill-kit 2.1.2 → 2.1.4

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.2",
3
+ "version": "2.1.4",
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.10.0"
20
+ "openxiangda-devkit-core": "2.12.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) |
@@ -1,6 +1,6 @@
1
1
  # NestJS 后端
2
2
 
3
- 默认模板只包含 Web 和共享契约。只有需要执行服务端业务动作时才增加 NestJS。记录列表、详情、新增、编辑和删除直接由浏览器调用平台 Data API,不在 controller 中重写一遍。
3
+ 默认模板只包含 Web 和共享契约。只有需要执行服务端业务动作时才增加 NestJS。记录列表、详情、新增、编辑和删除直接由浏览器调用平台 Data API,不在 controller 中重写一遍。启用前先按[判定是否真的需要 Nest 后端](development.md#backend-decision)逐行核对:幂等、时间窗、状态前置、角色核对、聚合、导入导出都有声明式答案;只有真实外部副作用或无法声明的跨资源不变量才是启用理由。`check` 会拒绝未以 `@OpenXiangdaOperation(appOperations.<code>)` 绑定已声明 operation 的应用路由。
4
4
 
5
5
  平台网关验证当前用户完整的应用角色并集,并把经平台重验的角色、capability 与可选 Perspective 交给 Nest SDK。业务 controller 使用生成的 operation 合同和 capability 装饰器;平台仍是身份与授权的唯一所有者。请求作用域 `OpenXiangdaDataApiService` 自动继承 Perspective 读取投影;绕过 Data API 的自定义读取才使用 `@CurrentPerspective()` 显式投影。应用代码不替换身份、不保存平台凭据,也不建立第二套用户或权限状态。
6
6
 
@@ -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,188 @@
1
+ # 声明速查:一次写对 openxiangda.config.ts {#cheatsheet}
2
+
3
+ 按"错误码 → 规则 → 正确片段"组织。这些规则全部来自真实返工:先扫一遍本页,再写声明,能省掉绝大多数首轮校验迭代。普通 CRUD 的完整可过检骨架见文末。
4
+
5
+ ## 模块与 CRUD 视图
6
+
7
+ | 规则 | 正确写法 |
8
+ | --- | --- |
9
+ | `crud[].model` 必须引用本模块已声明的模型 | `crud: [{ model: 'repair-requests', ... }]` |
10
+ | 视图 `list`/`form`/`detail` 的 `model` 可省略(继承视图模型);显式声明时必须与 `crud[].model` 一致 | `list: { fields: [...] }` 即可,不必写 `model` |
11
+ | 每个模型最多 20 个命名视图;命名视图需要稳定 `code` + `name` | `crud: [{ model: 'x', code: 'x-active', name: '进行中', ... }]` |
12
+ | 新建视图的 `form.fields` 必须覆盖全部无默认必填字段,或显式 `generated.create: false` | 检查器会列出缺失字段 |
13
+
14
+ ## 字段声明
15
+
16
+ | 规则 | 正确片段 |
17
+ | --- | --- |
18
+ | `option.*` 字段的值是 `{ label, value }` 快照(比较键是 `value`) | `options: [{ label: '教学设备', value: 'teaching' }]` |
19
+ | `number.integer` 可省略精度;只允许 `precision`(位数),不允许 `scale` | `{ code: 'qty', type: 'number.integer' }` |
20
+ | `number.decimal` 的 `precision` 必填、`scale` 0 到 precision | `{ type: 'number.decimal', precision: 12, scale: 2 }` |
21
+ | `audit.read` 可写 `true`(绑定本资源读能力)或能力数组 | `audit: { read: true }` |
22
+ | `resource-ref.*` 必须带 `source` 来源协议 | `{ type: 'resource-ref.single', source: { kind: 'resource', resourceCode: 'repair-requests', labelField: 'title', searchFields: ['title'], pageSize: 20, loadMode: 'search' } }` |
23
+ | `labelField` 必须指向目标资源的 `text.short` / `text.long` 字段 | 不要用流水号/选项字段当 label |
24
+ | 子表 `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 }` |
25
+ | 图片/附件的 `file` 限定数量与大小 | `file: { maxCount: 3, maxSizeMb: 10, accept: ['image/png', 'image/jpeg'] }` |
26
+
27
+ ## 权限声明
28
+
29
+ | 规则 | 正确片段 |
30
+ | --- | --- |
31
+ | 平台保留能力(如 `app:<app>:directory:read`)**不能**在 `capabilities` 里重复声明,直接在角色中引用即可 | `const directoryRead = \`app:\${APP_CODE}:directory:read\`` → `roles: [{ code: 'admin', capabilities: [directoryRead] }]` |
32
+ | 资源 CRUD 能力码用 `resourceCapabilityCodes(appCode, resourceCode)` 生成 | `const crud = resourceCapabilityCodes(APP_CODE, 'repair-requests')` → `capabilities: [crud.read, crud.create]` |
33
+ | `authenticatedUserRoleCode` 是平台登录用户的基线角色 | `authz: { authenticatedUserRoleCode: 'app-user', ... }` |
34
+
35
+ ## 后端操作(Nest)
36
+
37
+ | 规则 | 正确片段 |
38
+ | --- | --- |
39
+ | 写操作的 `ai.sideEffects` 至少一条具体副作用 | `ai: { name: '受理派单', ..., risk: 'write', sideEffects: ['更新报修单状态为处理中', '写入一条派工记录'] }` |
40
+ | GET 操作的 `ai.risk` 只能是 `read`;写操作不能是 `read` | `risk: 'read'` ↔ `method: 'GET'` |
41
+ | controller 路由必须绑定 `@OpenXiangdaOperation(appOperations.<code>)`,普通 CRUD 不写 controller | check 门禁会拒绝未绑定路由 |
42
+ | 事务守卫 `errorCode` 必须匹配 `^OPENXIANGDA_[A-Z0-9_]{1,96}$` | `errorCode: 'OPENXIANGDA_REPAIR_REQUEST_NOT_PENDING'` |
43
+
44
+ ## 事务写入快照字段
45
+
46
+ option / user / department / resource-ref 字段在事务和普通写入里都必须写快照对象,不能写裸字符串:
47
+
48
+ ```ts
49
+ import { optionSnapshot, userSnapshot, resourceSnapshot } from 'openxiangda/nest';
50
+
51
+ await this.data.transaction(idempotentTransaction(input.idempotencyKey, [
52
+ {
53
+ operation: 'update',
54
+ resourceCode: 'repair-requests',
55
+ id: input.requestId,
56
+ expectedRevision: revision,
57
+ data: {
58
+ status: optionSnapshot('处理中', 'processing'), // 不是 'processing'
59
+ assignedTechnician: userSnapshot(input.technicianId), // 不是裸 userId
60
+ },
61
+ },
62
+ {
63
+ operation: 'create',
64
+ resourceCode: 'repair-assignments',
65
+ data: {
66
+ requestId: resourceSnapshot('repair-requests', input.requestId, requestTitle),
67
+ technician: userSnapshot(input.technicianId),
68
+ },
69
+ },
70
+ ]));
71
+ ```
72
+
73
+ 读取时状态判断用 `record.data.status?.value === 'pending'`(存储值是快照对象)。
74
+
75
+ ## 幂等冲突复核模式
76
+
77
+ update 事务必须携带 `expectedRevision`;重试时 revision 已前进会让同一幂等键的内容指纹漂移,平台返回 409 `OPENXIANGDA_NATIVE_DATA_IDEMPOTENCY_CONFLICT`(而非 `replayed: true`)。标准处理:
78
+
79
+ ```ts
80
+ import { isIdempotencyConflict } from 'openxiangda/nest';
81
+
82
+ try {
83
+ result = await this.data.transaction(idempotentTransaction(key, operations, guards));
84
+ } catch (error) {
85
+ if (isIdempotencyConflict(error)) {
86
+ const current = await this.data.get('repair-requests', input.requestId);
87
+ if (/* 状态已离开 pending,说明本键的效果已生效 */) {
88
+ return { idempotencyKey: key, replayed: true, ... 当前状态 };
89
+ }
90
+ }
91
+ throw error;
92
+ }
93
+ ```
94
+
95
+ 不要用新生成的幂等键重试冲突——那会绕过幂等保护重复执行业务动作。
96
+
97
+ ## 两模型起步骨架(可直接改造)
98
+
99
+ ```ts
100
+ import {
101
+ adminNavigationGroup, adminResourcePage, defineAdminNavigation,
102
+ defineApplicationModule, defineOpenXiangdaApp, resourceCapabilityCodes,
103
+ } from 'openxiangda/config';
104
+
105
+ const APP_CODE = 'my-app';
106
+ const requestCrud = resourceCapabilityCodes(APP_CODE, 'requests');
107
+
108
+ const requests = {
109
+ code: 'requests', name: '申请单',
110
+ audit: { read: true },
111
+ fields: [
112
+ { code: 'title', type: 'text.short', label: '标题', required: true },
113
+ { code: 'category', type: 'option.single', label: '类别', required: true,
114
+ options: [{ label: '普通', value: 'normal' }, { label: '紧急', value: 'urgent' }] },
115
+ { code: 'status', type: 'option.single', label: '状态', required: true,
116
+ options: [{ label: '待受理', value: 'pending' }, { label: '已完成', value: 'done' }] },
117
+ { code: 'photo', type: 'image', label: '照片', file: { maxCount: 3, maxSizeMb: 10 } },
118
+ ],
119
+ };
120
+ const items = {
121
+ code: 'request-items', name: '明细',
122
+ fields: [
123
+ { code: 'requestId', type: 'uuid', required: true },
124
+ { code: 'sortOrder', type: 'number.integer', required: true },
125
+ { code: 'name', type: 'text.short', required: true },
126
+ { code: 'qty', type: 'number.integer' },
127
+ { code: 'price', type: 'number.decimal', precision: 12, scale: 2 },
128
+ ],
129
+ };
130
+
131
+ export default defineOpenXiangdaApp({
132
+ app: { code: APP_CODE, name: '我的应用' },
133
+ frontend: {
134
+ root: 'apps/web',
135
+ devicePolicy: { kind: 'viewport-family', mobileMaxWidthPx: 900, desktopMinWidthPx: 901 },
136
+ routes: [
137
+ { code: 'application-home', path: '/home', label: '应用首页', surface: 'user' },
138
+ { code: 'application-home-mobile', path: '/m/home', label: '移动首页', surface: 'user' },
139
+ ],
140
+ authentication: {
141
+ accountMode: 'existing-platform-users-only',
142
+ registration: { mode: 'reject' },
143
+ methods: [{ code: 'password', type: 'password', label: '账号密码登录', presentation: 'primary', required: true }],
144
+ surfaces: {
145
+ desktop: { routeCode: 'application-login', path: '/login', defaultRouteCode: 'application-home' },
146
+ mobile: { routeCode: 'application-login-mobile', path: '/m/login', defaultRouteCode: 'application-home-mobile' },
147
+ },
148
+ },
149
+ admin: {
150
+ navigation: defineAdminNavigation([
151
+ adminNavigationGroup('main', '业务管理', [adminResourcePage('requests', { label: '申请单' })], { icon: 'database', order: 100 }),
152
+ ]),
153
+ },
154
+ },
155
+ modules: [defineApplicationModule({
156
+ code: 'main',
157
+ models: [requests, items],
158
+ crud: [
159
+ {
160
+ model: 'requests',
161
+ list: {
162
+ fields: ['title', 'category', 'status'],
163
+ filterFields: ['category', 'status'],
164
+ searchableFields: ['title'],
165
+ defaultPageSize: 20,
166
+ defaultSort: { field: 'title', order: 'asc' },
167
+ },
168
+ mobile: { enabled: true },
169
+ },
170
+ ],
171
+ })],
172
+ authz: {
173
+ authenticatedUserRoleCode: 'app-user',
174
+ capabilities: [],
175
+ roles: [
176
+ { code: 'app-user', name: '应用用户', capabilities: [requestCrud.read, requestCrud.create] },
177
+ { code: 'admin', name: '管理员', capabilities: [requestCrud.read, requestCrud.create, requestCrud.update, requestCrud.delete] },
178
+ ],
179
+ scopeDimensions: [], scopeSources: [], dataPolicies: [], authorizationTransitions: [],
180
+ },
181
+ });
182
+ ```
183
+
184
+ `request-items` 不出现在 `crud` 里:子表行随 `requests` 表单的 `subtable` 字段写入(在 `requests.fields` 里补 `{ code: 'items', type: 'subtable', subtable: { resourceCode: 'request-items', foreignKey: 'requestId', orderField: 'sortOrder', maxRows: 20 } }`)。
185
+
186
+ ## 图片上传的像素上限
187
+
188
+ image/signature/富文本图片字段在上传计划(initiate)里返回 `maxPixels`;超过上限的图片会被标准组件自动压缩后重新发起上传。用 API 直传时自行按 `maxPixels` 预检。当前平台上限覆盖主流手机主摄(48/50/64MP);超出会得到带实际尺寸的 `OPENXIANGDA_NATIVE_DATA_IMAGE_PIXEL_LIMIT_EXCEEDED` 错误。
@@ -4,13 +4,10 @@
4
4
 
5
5
  ## 测试部署
6
6
 
7
- 托管源码应用通过 `pnpm openxiangda source push -m "本轮变更说明"` 完成提交与推送。
8
- 发布系统在平台绑定仓库中核验精确源码提交,并沿用 AppVersion 的源码和制品摘要关联。
9
- 本地构建仍是当前交付方式;源码提交存在不等于平台已独立验证制品由该提交构建。
7
+ 源码托管仍可通过 `pnpm openxiangda source push` 提交和推送,但不是测试发布的前置步骤。
8
+ 发布使用当前工作区内容,源码提交、分支名称和是否存在未提交修改不会阻塞测试环境;包元数据会保留实际来源信息。
10
9
 
11
- 发布前,先将本轮源码、生成契约及必要记录合入并推送仓库的远端默认主分支,然后从干净且同步的主分支工作区发布。工具从 origin 的远端 HEAD 识别主分支,不把任务分支的 upstream 当作主线。未提交、未推送、未合并或落后主线的问题会在构建和上传前返回;工具不会自动合并分支或覆盖其他会话的改动。
12
-
13
- 开发开始时先同步主线并读取项目现状,开发完成包括提交、推送与主线整合。每个工作区保持一个写者;需要并行时使用独立目录并明确各任务范围。日常 dev/check 仍可验证未提交源码。没有 Git 远端的项目应先建立并绑定仓库再发布。
10
+ 开发者可以从当前任务分支或本地工作区直接发布。团队是否要求合入默认分支属于协作规范,不由 Devkit 在每次发布时重复执行。
14
11
 
15
12
  准备部署时直接执行 deploy,它已经包含兼容性预检、生成、检查、测试和构建。只想检查代码时使用 [check](testing.md),无需在 deploy 前重复运行全套检查。
16
13
 
@@ -37,7 +34,7 @@ pnpm openxiangda status <deployment-id> --watch
37
34
 
38
35
  网络响应不确定时先查询原运行,不凭本地输出创建重复部署。平台部署成功后,仍需执行真实角色的业务验收。
39
36
 
40
- 相同源码候选重试时,工具会重新核对本地验证证据、封存清单和制品字节,复用仍有效的构建结果及后端镜像;已上传内容按摘要查询并复用。只有环境条件改变时不需要重建源码制品。输出损坏、输入变化或缓存缺失时自动回到正式检查和构建。缓存位于 `.openxiangda/build/`,不是新的部署状态源;提交响应不确定时使用原候选和幂等键,已有失败运行按其 recovery 恢复。
37
+ 重复发布直接使用当前源码重新构建。pnpm、Docker 和平台可以自行提供构建缓存,Devkit 不维护另一套工作区摘要或候选状态。提交响应不确定时使用原 DeploymentRun 的 status/logs recovery 继续处理。
41
38
 
42
39
  ## 测试环境验收
43
40
 
@@ -27,6 +27,27 @@
27
27
  - 标准审批、待办与通知:按需声明平台能力,见[工作流](workflow-events.md)。
28
28
  - 真实事务或外部集成:使用[按需后端](backend.md),不为每张表重写 CRUD 控制器。
29
29
 
30
+ ### 判定是否真的需要 Nest 后端 {#backend-decision}
31
+
32
+ 想写后端接口时,先按顺序核对平台已有的声明式答案;命中前几行的需求不得启用 Nest。
33
+ 普通数据增删改查永远由浏览器直接调用平台 Data API 或标准 CRUD 页面完成,
34
+ 应用 controller 不做记录列表、详情、新增、编辑、删除的转发。
35
+
36
+ | 你以为需要写后端 | 平台已有的声明式答案 | 参考 |
37
+ | --- | --- | --- |
38
+ | 列表、筛选、排序、分页接口 | `createNativeResourceClient` 的 `list`,服务端条件树与分页 | [前端数据访问](frontend.md#data-access) |
39
+ | 新增 / 编辑 / 删除接口 | 标准 CRUD 页面,或同一客户端的 `create` / `update` / `remove`(`expectedRevision` 乐观锁) | [前端数据访问](frontend.md#data-access) |
40
+ | 提交防重、幂等重试 | `transactNativeData` / 事务请求自带 `idempotencyKey` 幂等回执 | [前端数据访问](frontend.md#data-access)、[按需后端](backend.md#business-action) |
41
+ | 时间窗、状态前置、指定人角色校验 | 平台事务守卫:`operation-time`、`record-assert`、`record-exists`、`role-member` | [按需后端](backend.md#business-action) |
42
+ | 统计报表数据 | Data API 服务端聚合 `batchAggregateNativeResources`(单个指标也用它),前端不拉全量求和 | [前端](frontend.md#component-selection) |
43
+ | 导入 / 导出 | 标准 CRUD 的 `import` / `export` 动作声明 | [业务模块](application-foundation.md) |
44
+ | 跨模型原子写、外部 API、硬件或第三方推送 | Nest 具名 operation + 平台事务,必要时事务内 `emitEvent` | [按需后端](backend.md) |
45
+
46
+ 启用 Nest 的唯一充分条件是:真实外部副作用,或现有守卫无法声明的跨资源业务不变量,
47
+ 且该动作已作为 operation 声明能力(`kind: 'backend'`)并由角色显式引用。
48
+ "需要一点校验""需要默认值""需要联动查询"不是启用理由;校验优先字段规则与事务守卫,
49
+ 默认值优先服务端字段责任,联动查询优先 Data API 条件树。
50
+
30
51
  ## 实施与交接 {#iteration}
31
52
 
32
53
  开发使用 `pnpm openxiangda dev`,过程中运行必要的聚焦测试。交接前按[检查与验收](testing.md)验证;授权发布后按[交付](delivery.md)部署。失败保留错误码、位置和原始候选,依据平台恢复指令继续。
@@ -63,6 +63,43 @@ capability、应用角色和数据策略。前端只根据当前登录用户完
63
63
  可替换 Data API adapter。不要调用自定义 Nest CRUD、Function 或 Workflow 来绕过
64
64
  Data API。只有真正需要事务或外部系统的动作才使用同源 `/api`。
65
65
 
66
+ ### 自定义页面消费平台数据 {#data-access}
67
+
68
+ 自定义报表、工具页和用户端页面从 `openxiangda/core` 导入 `createNativeResourceClient`,
69
+ 用生成契约里的 surface 直接获得该资源的权威读写客户端;行、字段与操作授权由平台在
70
+ 每次请求时执行,页面不需要也不得复制权限逻辑:
71
+
72
+ ```tsx
73
+ import { createNativeResourceClient } from 'openxiangda/core';
74
+ import { resourceSurfaces } from '@app/contracts';
75
+
76
+ const records = createNativeResourceClient('records', resourceSurfaces.records);
77
+
78
+ // 服务端过滤、排序、分页;字段必须已声明,未声明字段直接报错
79
+ const page = await records.list({
80
+ page: 1,
81
+ pageSize: 20,
82
+ where: { field: 'enabled', operator: 'eq', value: true },
83
+ sort: { field: 'createdAt', order: 'desc' },
84
+ });
85
+
86
+ const record = await records.get(id);
87
+ await records.create(data);
88
+ await records.update(id, expectedRevision, data); // revision 冲突时明确报错
89
+ await records.remove(id, expectedRevision);
90
+ await records.upload('attachment', file, recordId);
91
+ ```
92
+
93
+ 跨模型原子写使用 `transactNativeData(operations, idempotencyKey)`:一次平台事务提交
94
+ 多个资源操作,携带幂等键,不确定的响应复用同一载荷重试,不产生第二份业务效果。
95
+ 多指标统计使用 `batchAggregateNativeResources` 在服务端聚合。导出使用
96
+ `records.exportCsv(query, select)`,与列表共用同一查询条件。
97
+
98
+ 要求服务端校验、状态前置或角色核对时,优先事务守卫(见
99
+ [判定是否真的需要 Nest 后端](development.md#backend-decision));只有在真实外部
100
+ 副作用下才声明 Nest operation。自写 controller 转发单一资源的增删改查无法通过 `check`:
101
+ 每个应用路由必须以 `@OpenXiangdaOperation(appOperations.<code>)` 绑定已声明的 operation。
102
+
66
103
  默认仪器模块有 30 个字段,其中 `id/revision` 是 Data API 系统字段,28 个业务
67
104
  字段由资源声明。新增、编辑和详情共用同一份字段元数据。五个边界字段使用五个独立
68
105
  capability,不使用角色名或影子字段判断。
@@ -18,7 +18,7 @@ CI、离线开发或尚未发布的候选包使用 `pnpm openxiangda check --loc
18
18
 
19
19
  检查会写本地生成结果并按需初始化后端,不是只读操作。前置阶段失败后,下游阶段标为 skipped,不继续构建或上传。保留错误码、pointer、details 和下一步,修正原因后重试。
20
20
 
21
- 源码、锁文件、依赖安装状态、工具链、构建环境和输出摘要均未变化时,检查自动复用已通过的 check/test/build,并在阶段结果标记 `reused: true`。目标平台的权限、密钥和模型条件每次重新预检。检查期间输入发生变化会停止并要求重新检查。缓存只保存摘要;缺少锁文件、外部本地依赖、符号链接、文件过多或无法核对时执行完整检查。手工修改 node_modules 不属于受支持的依赖管理方式,应修改依赖声明并重新安装。
21
+ 每次检查都直接执行当前工作区声明的 check/test/build 脚本;构建工具和包管理器可以自行使用缓存。目标平台的权限、密钥和模型条件仍在正式检查和部署前预检。Devkit 不维护额外的工作区摘要凭据,也不会因为分支、提交或文档变化引入候选复用分支。
22
22
 
23
23
  成功检查返回 `sealedArtifact.state: check-did-not-seal`、`sealed: false`、`usableForDeploy: false`。旧 AppPackage 不是当前检查结果。要发布测试环境可直接运行 deploy,它已包含完整检查;不要连续重复执行 check、test 和 build。
24
24