openxiangda 2.30.0 → 2.31.1

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.
Files changed (45) hide show
  1. package/dist/browser/ManagedCommand.d.ts +36 -0
  2. package/dist/browser/ManagedCommand.d.ts.map +1 -0
  3. package/dist/browser/ManagedCommand.js +41 -0
  4. package/dist/browser/ManagedCommand.js.map +1 -0
  5. package/dist/browser/components/resource/useResourceFormDrafts.d.ts.map +1 -1
  6. package/dist/browser/managed-command.d.ts +72 -0
  7. package/dist/browser/managed-command.d.ts.map +1 -0
  8. package/dist/browser/managed-command.js +282 -0
  9. package/dist/browser/managed-command.js.map +1 -0
  10. package/dist/browser/platform-client.d.ts +3 -0
  11. package/dist/browser/platform-client.d.ts.map +1 -1
  12. package/dist/browser/platform-client.js +32 -0
  13. package/dist/browser/platform-client.js.map +1 -1
  14. package/dist/config.d.ts +1 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/core.d.ts +3 -0
  17. package/dist/core.d.ts.map +1 -1
  18. package/dist/core.js +2 -0
  19. package/dist/core.js.map +1 -1
  20. package/dist/mobile.d.ts +3 -0
  21. package/dist/mobile.d.ts.map +1 -1
  22. package/dist/mobile.js +3 -0
  23. package/dist/mobile.js.map +1 -1
  24. package/dist/react.d.ts +4 -0
  25. package/dist/react.d.ts.map +1 -1
  26. package/dist/react.js +3 -0
  27. package/dist/react.js.map +1 -1
  28. package/documentation/backend.md +44 -0
  29. package/documentation/declarations-cheatsheet.md +2 -0
  30. package/documentation/frontend.md +6 -0
  31. package/documentation/getting-started.md +7 -7
  32. package/documentation/managed-concurrency-frontend.md +196 -0
  33. package/documentation/managed-concurrency.md +100 -0
  34. package/documentation/manifest.json +17 -5
  35. package/package.json +26 -22
  36. package/releases/2.31.0.json +39 -0
  37. package/releases/2.31.1.json +38 -0
  38. package/skills/manifest.json +1 -1
  39. package/skills/openxiangda-v2/SKILL.md +7 -4
  40. package/skills/openxiangda-v2/references/backend.md +44 -0
  41. package/skills/openxiangda-v2/references/declarations-cheatsheet.md +2 -0
  42. package/skills/openxiangda-v2/references/frontend.md +6 -0
  43. package/skills/openxiangda-v2/references/getting-started.md +7 -7
  44. package/skills/openxiangda-v2/references/managed-concurrency-frontend.md +196 -0
  45. package/skills/openxiangda-v2/references/managed-concurrency.md +100 -0
@@ -55,6 +55,8 @@
55
55
  | --- | --- |
56
56
  | 写操作的 `ai.sideEffects` 至少一条具体副作用 | `ai: { name: '受理派单', ..., risk: 'write', sideEffects: ['更新报修单状态为处理中', '写入一条派工记录'] }` |
57
57
  | GET 操作的 `ai.risk` 只能是 `read`;写操作不能是 `read` | `risk: 'read'` ↔ `method: 'GET'` |
58
+ | 面向普通用户的 Agent 只发现显式声明的业务任务;底层 CRUD 不会自动成为任务 | `ai.agent: { visibility: 'task', examples: ['帮我发起报修'], aliases: ['提交报修'] }` |
59
+ | 任务输入需要地点、人员等业务选择时,绑定同应用的只读辅助操作,用户不填内部编码 | `agent: { visibility: 'task', supportOperations: ['repair.locations'], inputLookups: { location: 'repair.locations' } }`;被引用的 GET 操作须有 `agent: { visibility: 'support' }`,`location` 须是请求 Schema 字段 |
58
60
  | controller 路由必须绑定 `@OpenXiangdaOperation(appOperations.<code>)`,普通 CRUD 不写 controller | check 门禁会拒绝未绑定路由 |
59
61
  | 事务守卫 `errorCode` 必须匹配 `^OPENXIANGDA_[A-Z0-9_]{1,96}$` | `errorCode: 'OPENXIANGDA_REPAIR_REQUEST_NOT_PENDING'` |
60
62
 
@@ -47,6 +47,12 @@ Agent 可以在标准后台内部优化布局、视觉和交互,但后台仍
47
47
 
48
48
  在页面设计或实现说明中写清选用组件、理由与必要边界。已有需求内的技术选型由开发者完成,无需让用户逐个指定依赖。ECharts 的按需导入方式见[官方说明](https://echarts.apache.org/handbook/en/basics/import/);后台无需为图表增加手机适配,用户移动图表只按已确认需求处理。
49
49
 
50
+ ## 热点读取与限量提交 {#managed-concurrency}
51
+
52
+ 报名、预约、抢票和限量申领按[并发能力的前端接入](managed-concurrency-frontend.md)选择页面形式:详情使用命名缓存读取,关键动作使用受管命令,按钮与状态面板共享同一操作快照。保持应用现有页面归属与组件体系;身份、排队、事务和配额由平台负责。
53
+
54
+ 先填写再提交、放行后短确认和预占后确认具有不同的输入与有效期语义;不要把通用表单 `onSubmit` 或整页 JSX 包装器当作自动防超卖方案。当前可用 API、代码示例、恢复文案及待补齐的契约均在该专题说明。
55
+
50
56
  ## 数据和权限
51
57
 
52
58
  `modules/` 中的业务模型由编译器派生 DataResource 契约,`openxiangda.config.ts` 声明页面
@@ -68,10 +68,10 @@ MCP 服务随项目根包一起安装,AI 客户端的 stdio 连接仍需配置
68
68
  以下命令的版本占位符由随包资料替换为该根包的精确版本。网站源码阅读者应先确认要使用的发行版本。
69
69
 
70
70
  ```bash
71
- pnpm dlx openxiangda@2.30.0 skill install --force
72
- pnpm dlx openxiangda@2.30.0 auth status --base-url <平台地址> --json
73
- pnpm dlx openxiangda@2.30.0 login --cwd my-app --base-url https://platform.example.com
74
- pnpm dlx openxiangda@2.30.0 create my-app --base-url https://platform.example.com
71
+ pnpm dlx openxiangda@2.31.1 skill install --force
72
+ pnpm dlx openxiangda@2.31.1 auth status --base-url <平台地址> --json
73
+ pnpm dlx openxiangda@2.31.1 login --cwd my-app --base-url https://platform.example.com
74
+ pnpm dlx openxiangda@2.31.1 create my-app --base-url https://platform.example.com
75
75
  cd my-app
76
76
  pnpm openxiangda context --json
77
77
  pnpm openxiangda dev
@@ -174,9 +174,9 @@ MCP 的 `docs_read` 可以读取本说明,当前没有独立的源码操作 MC
174
174
  无需本地工作区,使用本 Skill 随包精确版本或已安装的对应 CLI:
175
175
 
176
176
  ```bash
177
- pnpm dlx openxiangda@2.30.0 auth status --base-url <平台> --json
178
- pnpm dlx openxiangda@2.30.0 source resolve <仓库URL> --base-url <平台> --json
179
- pnpm dlx openxiangda@2.30.0 source clone <仓库URL> <新目录> --base-url <平台> --json
177
+ pnpm dlx openxiangda@2.31.1 auth status --base-url <平台> --json
178
+ pnpm dlx openxiangda@2.31.1 source resolve <仓库URL> --base-url <平台> --json
179
+ pnpm dlx openxiangda@2.31.1 source clone <仓库URL> <新目录> --base-url <平台> --json
180
180
  ```
181
181
 
182
182
  登录缺失或站点不匹配时,先按该平台执行 login。resolve 根据平台已经登记的绑定返回
@@ -0,0 +1,196 @@
1
+ # 并发能力的前端接入
2
+
3
+ 适用于活动报名、限量申领、抢票和预约。本文使用 `openxiangda@2.31.0` 引入的公开 API;目标平台必须支持 `data.managed-concurrency@1.0.0`。先按[缓存、排队与整数配额](managed-concurrency.md)声明读取、命令、配额和权限,再选择页面接入形式。
4
+
5
+ 推荐体验是正常浏览内容,在用户明确提交时按需等待,并持续核对同一次申请的结果。低负载时可以很快完成,不需要人为添加等待页。
6
+
7
+ ## 选择接入形式 {#patterns}
8
+
9
+ | 形式 | 适用场景 | 用户流程 | 当前接入方式 |
10
+ | --- | --- | --- | --- |
11
+ | 热点内容直接浏览 | 详情、场次、活动说明 | 浏览 → 按需刷新 | `client.read`,应用处理读取状态 |
12
+ | 按钮发起、原地等待 | 一键报名、限量申领 | 点击 → 排队 → 处理 → 结果 | `useManagedCommand` + `ManagedCommandStatus` |
13
+ | 先填写、再排队提交 | 有选项的预约和申请 | 填写校验 → 确认内容 → 排队提交 | 有界参数组合现有 hook;复杂资料需要额外冻结契约 |
14
+ | 放行后短确认 | 输入已确定、加载成本较高的确认步骤 | 进入 → 等待 → 短确认 → 提交 | `autoAccept: false` + `ManagedCommandGate` + `submit()` |
15
+ | 预占后确认 | 暂留名额、时段预约 | 预占 → 核实有效期 → 确认或释放 | 应用声明预占/确认/释放命令,配合 `client.allocation` |
16
+ | 返回后恢复原申请 | 刷新、断网、查看本人结果 | 恢复原请求 → 核对 → 结果 | hook 挂载恢复,或 `client.result` 查询已知操作 |
17
+
18
+ 常见报名页面可以组合前三种:详情走缓存,用户确认活动和选项后提交,按钮附近展开状态;成功后进入本人记录。PC 状态区、抽屉和移动端底部区域可以使用不同外观,共用同一个操作状态。
19
+
20
+ ## 页面与平台的分工 {#ownership}
21
+
22
+ 页面负责内容、选项、字段校验、业务文案和结果路由。平台负责当前用户身份、准入、幂等、事务、配额和权威结果。身份使用声明中的可信 `actor`,不使用用户填写的工号决定记录归属。
23
+
24
+ 平台身份就绪后创建 `createManagedConcurrencyClient()`,并保持实例稳定。它绑定当前应用、环境和主体;身份/环境变化时重新挂载对应页面子树,不能复用旧客户端或旧视图。
25
+
26
+ 一次操作只使用一个 `useManagedCommand`。按钮、状态条和弹层消费同一个快照,不要各自调用 hook 创建观察循环。当前 SDK 通过 `openxiangda/react` 和 `openxiangda/mobile` 导出相同的受管并发能力;普通字段仍复用[Field Kit](field-components.md),界面遵循[前端组件与页面归属](frontend.md)。
27
+
28
+ 不要把受管模型继续绑定到普通 CRUD 提交按钮,也不要传入任意 `async onSubmit` 并假设它会自动获得排队和原子保证。最终动作必须是已发布的受管命令。
29
+
30
+ ## 热点详情读取 {#cached-read}
31
+
32
+ 以下示例对应源码 `examples/managed-concurrency/declaration.ts` 中的 `offer` 命名读取。`offerId` 必须是该应用真实资源的 UUID:
33
+
34
+ ```ts
35
+ import { createManagedConcurrencyClient } from 'openxiangda/react';
36
+
37
+ type Client = ReturnType<typeof createManagedConcurrencyClient>;
38
+ type Offer = { id: string; title: string; closesAt: string };
39
+
40
+ export async function readOffer(
41
+ client: Client,
42
+ offerId: string,
43
+ signal?: AbortSignal,
44
+ ) {
45
+ return client.read<Offer>('offer', { id: offerId }, signal);
46
+ }
47
+ ```
48
+
49
+ 将读取接入应用已有的异步查询状态:初次加载显示局部骨架,空数组显示无可展示内容,失败提供局部重试。组件卸载或资源变化时中止旧请求,迟到响应不能覆盖新资源。同页多个观察者可复用应用已有查询层,查询键包含 `client.scope`、读取 code 和规范化参数,身份变化时清除旧主体视图;当前没有内置的 `useManagedRead` hook。
50
+
51
+ `freshness: 'stale'` 表示返回的是允许使用的旧内容,并不证明后台刷新已经成功。保留内容并提示“当前展示最近一次可用信息”;按新鲜期和有界退避刷新,超过 `staleUntil` 后不能继续无限展示。手动刷新仍调用同一命名读取,不绕回普通 CRUD。
52
+
53
+ 详情与余量使用不同读取声明。页面倒计时本地显示,不能每秒重查活动;资格、开放时间和扣减仍由服务端决定。缓存余量不保证获票;允许补量或释放的业务,也不能因旧余量为零永久阻止新的有效申请。等待期间不要重复读取详情或表单。
54
+
55
+ ## 一键申请与原结果恢复 {#one-click}
56
+
57
+ 示例对应官方声明中的 `claim` 命令。父级在身份就绪后挂载,用户、环境或 `offerId` 变化时重新挂载本组件:
58
+
59
+ ```tsx
60
+ import { useMemo } from 'react';
61
+ import {
62
+ createManagedConcurrencyClient,
63
+ useManagedCommand,
64
+ ManagedCommandStatus,
65
+ } from 'openxiangda/react';
66
+
67
+ export function ClaimAction({ offerId }: { offerId: string }) {
68
+ const client = useMemo(() => createManagedConcurrencyClient(), []);
69
+ const command = useManagedCommand({
70
+ client,
71
+ command: 'claim',
72
+ input: { id: offerId },
73
+ storageKey: `claim:${offerId}`,
74
+ });
75
+ // start / resume 的失败进入快照;保留原请求,按错误原因恢复。
76
+ const observe = (action: () => Promise<void>) => {
77
+ void action().catch(() => {});
78
+ };
79
+ return (
80
+ <section>
81
+ <button disabled={command.state !== 'idle'}
82
+ onClick={() => observe(() => command.start())}>
83
+ 申请
84
+ </button>
85
+ <ManagedCommandStatus snapshot={command} />
86
+ {command.state === 'error' && (
87
+ <div>
88
+ <p>暂时无法确认结果,请核对原申请。</p>
89
+ <button onClick={() => observe(() => command.resume())}>
90
+ 核对原申请
91
+ </button>
92
+ </div>
93
+ )}
94
+ {command.state === 'succeeded' && <p>可查看本次申请记录。</p>}
95
+ </section>
96
+ );
97
+ }
98
+ ```
99
+
100
+ 挂载只恢复已有意图;用户点击 `start()` 才创建新意图。默认放行后自动受理,适合点击已经明确表达提交意愿的动作。不要在 `useEffect` 中调用 `start()` 自动报名,不要手写循环换请求键重试。
101
+
102
+ 等待只轮询票据,受理后查询原命令结果;SDK 遵守建议间隔并退避。按钮禁用只是体验,服务端仍执行幂等和业务去重。成功时可从 `receipt.result.items` 定位已创建的业务记录;不要在成功回调里再次写订单、扣名额或发通知。导航和视图刷新应允许重复执行。
103
+
104
+ ## 填写后再提交 {#form-submit}
105
+
106
+ 先校验字段,向用户展示确认摘要,再将输入固定并启动命令。受管 hook 使用固定后的输入,不要直接绑定每次键入都变化的表单对象。提交后刷新页面,要从本次意图的稳定资料恢复相同输入,再挂载 hook;不能用空表单覆盖原请求。
107
+
108
+ 排队后需要修改资料时,先请求取消并核实结果,再允许编辑和创建新意图。取消与受理、执行、成功可能竞争;服务端返回已经成功时,应展示原结果,不能继续当作草稿编辑或假定取消成功。组件卸载或关闭面板仅停止观察,不取消命令。
109
+
110
+ 当前命令参数仅支持 UUID、枚举字符串和有限整数。场次、规格等可以按声明接入;配额 `units` 由命令声明固定,传入整数参数不会自动改变扣减数量。姓名、任意备注、附件及整份表单 JSON 不属于当前参数范围。
111
+
112
+ 复杂资料需要先定义并验证“草稿 → 不可变资料版本 → 命令引用版本 ID”的契约,核实作者、目标资源、版本和字段权限,并阻止冻结资料被普通 CRUD 改写。只传 `draftId` 而允许原稿继续变化不满足要求。该通用冻结机制和表单桥接尚未提供,不能把任意表单保存接口包进 hook 就当作已支持;草稿保存本身也需要独立预算。
113
+
114
+ ## 放行后的短确认 {#confirmation-gate}
115
+
116
+ 此形式要求排队前已确定命令参数,用户明确请求进入确认步骤。沿用前例的身份/client 生命周期,创建命令时设置 `autoAccept: false`;入口按钮调用 `start()`,获准后的确认按钮调用 `submit()`:
117
+
118
+ ```tsx
119
+ // command 已由 useManagedCommand 创建,并设置 autoAccept: false。
120
+ // Confirmation 是应用组件;它只核对已经固定的输入。
121
+ <ManagedCommandGate snapshot={command}>
122
+ {() => (
123
+ <Confirmation
124
+ onConfirm={() => void command.submit().catch(handleActionError)}
125
+ />
126
+ )}
127
+ </ManagedCommandGate>
128
+ ```
129
+
130
+ `ManagedCommandGate` 从与 hook 相同的公开入口导入。上例是前例中的组合片段,`Confirmation` 和 `handleActionError` 由应用定义;错误处理必须保留原请求,不能直接清除并重提。耗时请求和懒加载应在子树内发生,父组件提前发请求会失去保护。
131
+
132
+ `admitted` 是短期受理许可,不是名额预占,也不是长时间填写表单的许可证。当前 Gate 只按快照状态决定是否挂载,没有内建到期倒计时;应用按 `waiting.expiresAt` 提示并核实,最终提交时服务端校验许可,SDK 沿原请求恢复。
133
+
134
+ “先进入页面,再任意决定最终输入”需要与命令输入解耦的页面准入契约,当前 Gate 不提供这个语义。它在应用启动、身份就绪后工作,不能替代静态资源、登录、启动元数据和整个站点入口的保护。
135
+
136
+ ## 预占与最终确认 {#reservation}
137
+
138
+ 应用先声明 `quota.action: 'allocate'`、`mode: 'reserved'` 和 `reservationSeconds`,另声明确认/释放命令。三个动作分别使用稳定恢复键,并按[配额生命周期](managed-concurrency.md#配额和发布边界)绑定原分配 ID。
139
+
140
+ 预占命令 `succeeded` 只证明预占事务曾成功。先读取 `receipt.result.allocation.id`,再调用 `client.allocation(allocationId)` 核实当前状态,界面显示“已暂留名额”。确认或释放后也要核对当前分配,不能用旧成功回执证明仍持有名额。
141
+
142
+ 进入页面、回到前台和临近到期时有界核实。`expiresAt` 用于展示估算倒计时;当前回执没有 `serverNow`,浏览器时间不能作为最终过期依据。归零显示“正在核实有效期”,确认和到期竞争以服务端结果为准。关闭窗口不自动释放。
143
+
144
+ 同池/主体永久去重;释放后不会自动允许同一主体重申同一池。需要下一轮申请时使用新的权威资源/池,不删除分配记录或换请求键绕过限制。
145
+
146
+ ## 状态文案与用户动作 {#states}
147
+
148
+ | 状态 | 界面含义 | 动作规则 |
149
+ | --- | --- | --- |
150
+ | `idle` | 可以申请 | 明确点击后 `start()` |
151
+ | `waiting` | 正在等待进入,尚未获得名额 | 等待;可申请退出排队 |
152
+ | `admitted` | 已轮到您,可以确认 | 手动确认形式调用 `submit()` |
153
+ | `accepted` / `executing` / `retry_wait` | 已收到申请,正在处理 | 查询原结果;按服务端规则申请取消 |
154
+ | `recovering` | 正在核对原申请 | 保留原键,继续退避观察 |
155
+ | `error` | 暂时无法确认或无法继续 | 按原因恢复登录、处理存储或 `resume()`;不统一显示报名失败 |
156
+ | `succeeded` | 权威事务已提交 | 按业务显示报名成功或暂留名额;预占另核实分配 |
157
+ | `rejected` | 明确未通过 | 区分售罄、资格、截止等业务原因 |
158
+ | `expired` / `cancelled` | 等待或命令到期、已取消 | 区分票据与命令终态,明确结束后才 `clear()` |
159
+
160
+ 排队位置只在 `waiting.position` 存在时显示,不换算成保证等待时长或保证名额;缺少位置时显示等待文案,不画虚假百分比。临时队列故障后位置可能变化。
161
+
162
+ `ManagedCommandStatus` 支持 `errorMessages` 映射回执错误。产品层同时检查 `snapshot.errorCode` 与 `receipt.errorCode`,补齐身份变化、存储不可用、恢复输入冲突等原因。用户文案不暴露数据库、锁或票据内部信息。
163
+
164
+ `cancel()` 的 Promise 失败也要明确展示“取消结果尚未确认”,保留原意图并恢复查询;不要吞掉取消错误后宣布取消成功。关闭状态面板、离开页面与取消申请使用不同按钮和文案。状态变化使用无障碍提示,抽屉/底部区域遵循现有组件的焦点管理。
165
+
166
+ ## 刷新、断网与我的申请 {#recovery}
167
+
168
+ 默认恢复信息保存在 `sessionStorage`,按应用、环境、主体、命令和 `storageKey` 隔离。同标签页刷新可以恢复;`storageKey` 必须稳定,不能在每次渲染时随机生成。存储不可用时明确停止或处理,不能悄悄改用易丢失的内存继续提交。
169
+
170
+ 已知 `operationId` 或原 `requestKey` 时,可以用 `client.result` 查询原结果。已成功业务记录可通过本人权限下的普通数据查询展示。当前没有“列出我的所有进行中命令”的接口;关闭标签后找回、跨设备继续和完整处理中列表需要额外的受管操作索引,不能把浏览器存储当作全平台事实。
171
+
172
+ 不要把超时解释为提交失败,也不要在结果未知时 `clear()`。重试、恢复与返回入口都应该指向原意图。配额永久去重与界面请求键是不同边界:清除一个已终结的界面状态不代表可以再次获得同一池名额。
173
+
174
+ ## 当前组件与后续封装边界 {#available-components}
175
+
176
+ 当前可用:`createManagedConcurrencyClient`、`useManagedCommand`、`ManagedCommandStatus`、`ManagedCommandGate`,以及客户端的读取、结果、取消和分配状态方法。
177
+
178
+ `useManagedRead`、`ManagedCommandButton`、`ManagedCommandPanel`、`useManagedFormSubmission`、`useManagedAllocation`、`AllocationStatus` 是建议的后续封装名称,当前不是公开导出。不要照这些名称编写 import。应用现在可以使用已有 hook 与平台普通 UI 组件组合界面,后续标准封装仍应复用同一状态机和传输。
179
+
180
+ 复杂资料冻结、跨设备命令索引、独立页面准入及精确倒计时需要相应的平台契约,不能仅通过前端外观补齐。SDK 版本、平台部署和实际业务容量需要分别确认。
181
+
182
+ ## 接入验收 {#acceptance}
183
+
184
+ | 操作 | 应观察到的结果 |
185
+ | --- | --- |
186
+ | 打开详情或重渲染 | 不自动报名;详情与余量按声明读取,不因排队反复查表 |
187
+ | 连点、刷新、提交响应丢失 | 沿用原意图,恢复原结果,不生成重复业务记录 |
188
+ | 排队时编辑资料 | 固定原输入,取消尚未核实前不切换到另一次提交 |
189
+ | 关闭状态区后返回 | 继续查看原操作,不宣称已取消 |
190
+ | 取消与受理/成功同时发生 | 显示服务端原结果;错误时保留待核实状态 |
191
+ | 身份切换、存储被禁用 | 不串用旧主体资料,不静默丢弃恢复能力 |
192
+ | 网络或依赖异常 | 退避观察,保留原键,不绕回数据库或快速换键重提 |
193
+ | 许可到期、预占到期 | 分别核实准入与分配,互不冒充 |
194
+ | 键盘、屏幕阅读器、窄屏 | 状态可读,焦点可控,关闭与取消语义清楚 |
195
+
196
+ 这些检查与目标环境的吞吐、P95/P99、授权查询成本和故障恢复验证一起完成;页面演示和 SDK 单元测试不能代替真实业务验收。
@@ -0,0 +1,100 @@
1
+ # 缓存、排队与整数配额
2
+
3
+ `data.concurrency` 是可选的平台声明,用于限量申领、抢票和预约。平台负责缓存回源、等待资格、持久受理、受控 Native 事务及整数配额;应用通过 `openxiangda/react` 或 `openxiangda/mobile` 调用。平台必须声明能力 `data.managed-concurrency@1.0.0`。旧平台会在编译/发布能力检查阶段拒绝此声明。
4
+
5
+ 页面开发参见[并发能力的前端接入](managed-concurrency-frontend.md):按热点详情、一键申请、填完再提交、短确认入口、预占确认和原结果恢复选择交互方式,并核对当前 API 与尚未提供的封装边界。
6
+
7
+ ## 声明与权限
8
+
9
+ 完整且由双编译器测试验证的示例位于源码 `examples/managed-concurrency/declaration.ts`。调用 `defineOpenXiangdaApp(concurrencyExample())` 即可编译。把示例中的 `data.concurrency` 合入应用配置,并按实际业务配置模型和参与权限。
10
+
11
+ | 声明 | 作用 |
12
+ | --- | --- |
13
+ | `reads` | 命名读取、有限参数、字段、条件、权限范围、新鲜期、旧值期限、失效字段和回源预算 |
14
+ | `admission` | 本应用/环境所有命令共享的每秒速率、突发额度、在途上限 |
15
+ | `commands` | 当前用户的能力、按资源分队列、最长等待、处理截止、Native 守卫和原子写入 |
16
+ | `quotas` | 容量来源、业务记录上的分配 ID、可选的预占到期时间 |
17
+
18
+ 参数限于 UUID、有限字符串枚举和有限整数范围;绑定只接受 `input`、可信 `actor`、字面量和平台生成的 `allocation`。每个命令最多 8 个操作、8 个记录守卫。首版不执行应用任意 JS/SQL,也不在锁内调用外部系统。通知和外部副作用接已有事务后事件。
19
+
20
+ 配额来源模型的容量字段必须是整数;分配记录模型的引用字段必须是必填 UUID,模型设置 `mutationOwner: 'queued-command'`。角色仍须具备来源读取、分配模型创建和命令能力。普通 CRUD、导入、应用凭据均不能绕过命令所有权。用户身份由 `actor` 绑定,不能使用用户填写的人员编号决定名额归属。
21
+
22
+ `database-now` 条件使用数据库时间,并按“字段 操作符 当前时间”解释。例如截止前允许报名:`{ kind: 'database-now', field: 'closesAt', operator: 'gt' }`。资格和业务截止在执行时重验,因此等待或已受理不等于一定成功。
23
+
24
+ ## 展示读取
25
+
26
+ ```ts
27
+ const client = createManagedConcurrencyClient();
28
+ const snapshot = await client.read('offer', { id: selectedOfferId });
29
+ // snapshot.items、generatedAt、freshUntil、staleUntil、version、freshness
30
+ ```
31
+
32
+ 在平台身份初始化完成后创建并保持 `client` 实例稳定。它绑定应用、环境和当前用户;切换身份后旧客户端拒绝继续操作。组织内共享内容也要求登录与当前权限。`scope: 'application'` 不允许行策略或 actor 条件;个人/有行策略的数据使用 `scope: 'subject'`。
33
+
34
+ 每次访问仍通过平台现有身份、版本与字段权限链路。缓存减少内容查询;不能据此宣称整个请求零 SQL。活动内容和余量使用不同读取声明与依赖,避免每次报名使详情失效。`dependencies` 必须覆盖返回和过滤字段。Native 提交在同一事务写失效事实,恢复任务负责可靠推进;变更传播有延迟,要求即时资格或即时关闭的判断必须放在命令守卫中。
35
+
36
+ 冷缓存使用进程请求合并与跨副本刷新租约,短暂争用返回可重试响应。负缓存最长 5 秒,TTL 带随机抖动。可在应用版本激活后,用正常授权的 `read` 调用预热有限的热点 ID。首版提供认证后的 Redis/进程缓存,不提供公共 CDN 发布或自动扫描全量数据预热。
37
+
38
+ ## 提交与原结果恢复
39
+
40
+ ```tsx
41
+ import { useMemo } from 'react';
42
+ import { createManagedConcurrencyClient, useManagedCommand,
43
+ ManagedCommandStatus } from 'openxiangda/react';
44
+
45
+ export function Claim({ offerId }: { offerId: string }) {
46
+ // 父级在平台身份就绪后挂载;身份变化时重新挂载此子树。
47
+ const client = useMemo(() => createManagedConcurrencyClient(), []);
48
+ const command = useManagedCommand({ client, command: 'claim',
49
+ input: { id: offerId }, storageKey: offerId });
50
+ return <>
51
+ <button disabled={command.state !== 'idle'}
52
+ onClick={() => void command.start().catch(() => {})}>申请</button>
53
+ <ManagedCommandStatus snapshot={command} />
54
+ {command.state === 'error' && <button
55
+ onClick={() => void command.resume().catch(() => {})}>恢复原操作</button>}
56
+ </>;
57
+ }
58
+ ```
59
+
60
+ 挂载只恢复已有请求,用户调用 `start()` 才创建新意图。默认以 `sessionStorage` 保存原请求键及恢复资料,刷新后继续查原结果;浏览器禁止存储时明确失败,不自动改用易丢失的内存状态。可传入同接口的可靠存储。不要另写循环换请求键重试。
61
+
62
+ 等待阶段只轮询签名票据,不重复查业务数据。SDK 遵守建议间隔并加抖动,故障时逐步退避至约 30 秒。组件卸载或 `stop()` 只停止观察,不取消已受理命令。取消使用 `cancel()`,会核对原命令并处理与受理竞争的情况。只有已知终态才能 `clear()` 开始新意图。
63
+
64
+ 需要先排队再加载重型页面时,设置 `autoAccept: false`,用 `ManagedCommandGate` 的 `children={() => <HeavyContent />}` 延迟构建子树;用户确认后调用 `submit()`。命令输入在排队时固定。它不替代整站认证、应用启动元数据或未使用此能力的接口预算,也不能保护提前在父组件发起的请求。
65
+
66
+ | 状态 | 含义 |
67
+ | --- | --- |
68
+ | `waiting` / `admitted` | 等待/获准受理,尚未占名额 |
69
+ | `accepted` / `executing` / `retry_wait` | 已持久受理,平台继续处理 |
70
+ | `succeeded` | 权威事务已提交,原结果可恢复 |
71
+ | `rejected` / `expired` / `cancelled` | 明确终态 |
72
+ | `recovering` / `error` | 正在核对/暂时无法确认,保留原请求键 |
73
+
74
+ ## 配额和发布边界
75
+
76
+ 立即分配用 `quota.action: 'allocate', mode: 'committed'`;需要预占时声明 `reservationSeconds` 并用 `mode: 'reserved'`。另声明 `confirm`、`release` 命令,绑定原 `allocation`,该命令的 `operations` 为空。只有原主体可确认/释放;到期与确认竞争使用数据库时间及固定锁顺序。
77
+
78
+ 原命令回执不可变。预占之后到期或释放,应调用 `client.allocation(allocationId)` 获取当前分配状态,不能用旧成功回执证明仍持有名额。首版同一池/主体保留永久去重事实,释放后不会自动重新报名;应用若需要新一轮,应使用新的权威资源/配额池,不能删除历史分配。
79
+
80
+ 容量减少不能低于已预占加已确认数量。存在配额历史时,禁止删除来源、改写分配引用或发布移除配额映射的版本。应用激活先排空所有非终态命令;不会让旧任务在新 Head 上执行。连接开发消费已激活测试版本的并发声明,未激活的本地模型覆盖层不能作为持久命令契约。
81
+
82
+ ## 资源上限与故障处理
83
+
84
+ | 边界 | 首版硬上限 |
85
+ | --- | --- |
86
+ | 每应用/环境等待 | 128 个资源队列、合计 10000 人,同时受声明的更小上限约束 |
87
+ | 缓存 | 单值 256 KiB;应用/环境累计 32 MiB、2048 个有效键;进程 L1 最多 256 个条目 |
88
+ | 回源 | 每进程 2 个、每应用跨副本 2 个;应用合计 20 次/秒并受声明预算限制 |
89
+ | HTTP | 并发能力普通入口每进程 8 个,查原结果独立 4 个 |
90
+ | worker | 每进程最多 2 个,同一资源通过数据库事务锁协调 |
91
+
92
+ Redis 不可用会停止新准入和缓存回源,绝不把所有请求直接放到数据库。已持久受理命令依赖 PostgreSQL 和现有 RabbitMQ 恢复;原结果在 Redis 故障时每进程最多 4 次/秒、4 个并发可回查。Redis 丢失会重建在途事实并换票据世代,临时等待位置可能变化。
93
+
94
+ RabbitMQ 的消息只是唤醒提示;队列丢失或发布失败由数据库恢复,重复消息读取同一命令。锁等待或慢执行触发有界重试与应用短暂暂停放行。系统并不承诺跨队列严格公平,也不承诺一旦排队就必定获票。
95
+
96
+ 管理员可使用同源 POST `/openxiangda-api/v2/applications/:appCode/native/concurrency/status`,请求带 `environmentKey`,查看非终态数量/最早时间、最多 100 个配额池及当前进程缓存指标。`control` 接收 `{ environmentKey, paused: true }` 停止新受理;只允许应用超级管理员。停用或回滚前停止受理、排空命令、保留配额与原结果;不得通过删锁、删队列数据补偿业务。
97
+
98
+ 暂停/恢复按同一个环境锁串行传播;Redis 不可用时控制接口明确报错。控制值是绝对值,可以使用相同 `paused` 重试并核对状态。数据库提交确认丢失时不猜测控制结果;受理始终复查数据库的暂停状态。
99
+
100
+ 验证应包括内容 SQL 与授权 SQL 占比、每秒完成数、P95/P99、最老等待、连接数、锁等待和恢复时间。仓库故障测试证明协议边界,不代表任何客户环境的生产吞吐。部署者仍需使用目标硬件、真实权限和活动规模做容量验收。