@microi.net/cli 5.5.0 → 5.5.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.
@@ -1,32 +1,32 @@
1
- ---
2
- name: v8-api-config
1
+ ---
2
+ name: v8-api-config
3
3
  description: Microi V8 接口引擎配置指南。用于设置 ApiEngineKey、ApiAddress、StopHttp、AllowAnonymous、ResponseFile、ResponseType=HTTP、锁、日志、超时和 HTTP 暴露。
4
- ---
5
-
6
- > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
-
8
- # Microi V8 接口引擎配置
9
-
10
- 你正在配置 Microi 吾码平台的接口引擎(API 引擎)。除了 JS 代码本身,每个接口还有一系列**安全/性能配置项**,写代码时必须了解这些选项以决定是否需要调整。
11
-
12
- ## 配置项总览
13
-
14
- | 字段 | 说明 | 默认 |
15
- |------|------|------|
4
+ ---
5
+
6
+ > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
+
8
+ # Microi V8 接口引擎配置
9
+
10
+ 你正在配置 Microi 吾码平台的接口引擎(API 引擎)。除了 JS 代码本身,每个接口还有一系列**安全/性能配置项**,写代码时必须了解这些选项以决定是否需要调整。
11
+
12
+ ## 配置项总览
13
+
14
+ | 字段 | 说明 | 默认 |
15
+ |------|------|------|
16
16
  | `ApiEngineKey` | 接口唯一标识(URL 路径) | 必填 |
17
17
  | `ApiAddress` | 自定义接口地址(覆盖默认 `/apiengine/{Key}`) | 空 |
18
18
  | `ApiRoutes`(多路由) | 同一接口的兼容地址,多个绝对路径用英文分号分隔 | 空 |
19
- | `RequestType` | `Get` / `Post` / `Both` | `Both` |
20
- | `ParamType` | `form` / `json` / `url` —— 但 V8.Param 都能统一接收 | `Both` |
21
- | `IsAnonymous` | 允许匿名调用(无 Token) | `false` |
22
- | `StopHttp` | 禁止外部 HTTP 调用(仅允许 V8.ApiEngine.Run 内部调用) | `false` |
19
+ | `RequestType` | `Get` / `Post` / `Both` | `Both` |
20
+ | `ParamType` | `form` / `json` / `url` —— 但 V8.Param 都能统一接收 | `Both` |
21
+ | `IsAnonymous` | 允许匿名调用(无 Token) | `false` |
22
+ | `StopHttp` | 禁止外部 HTTP 调用(仅允许 V8.ApiEngine.Run 内部调用) | `false` |
23
23
  | `IsResponseFile` | 是否响应文件(开启后 Data 必须是文件结构) | `false` |
24
24
  | `ResponseType` | `JSON/String/File/HTML/Stream/HTTP`;`HTTP` 返回受控状态码、响应头和正文 | 自动识别 |
25
- | `LockKey` | 分布式锁 Key(同一时刻全集群只能执行一次) | 空 |
26
- | `LockTimeout` | 锁超时秒数 | `30` |
27
- | `LockMsg` | 加锁失败时返回提示 | `操作过于频繁` |
28
- | `RateLimit` | 频率限制(如 `60/m` 每分钟60次) | 空 |
29
- | `LogParam` | 是否记录请求参数到 `sys_log` | `false` |
25
+ | `LockKey` | 用作分布式锁值的请求参数字段名;为空时按接口 Key 串行 | 空 |
26
+ | `Timeout` | 接口执行预算;开启锁时也作为单次 Redis 租期(秒) | 租户运行配置 |
27
+ | `LockMsg` | 加锁失败时返回提示 | `操作过于频繁` |
28
+ | `RateLimit` | 频率限制(如 `60/m` 每分钟60次) | 空 |
29
+ | `LogParam` | 是否记录请求参数到 `sys_log` | `false` |
30
30
  | `LogResult` | 是否记录返回值到 `sys_log` | `false` |
31
31
 
32
32
  ### 多路由(ApiRoutes)
@@ -43,15 +43,15 @@ ApiRoutes: /api/SysMenu/GetSysMenuModel;/api/SysMenu/GetSysMenuStep
43
43
  - 主路由与多路由不得重复,也不得与其它启用接口的主路由/多路由冲突;保存、启动闭包和缓存初始化都必须失败关闭并指出冲突 Key,禁止后写覆盖先写。
44
44
  - 新客户端仍使用 `/apiengine/{ApiEngineKey}` 或主 `ApiAddress`。多路由用于 Controller 迁移、旧移动端和第三方已登记回调的兼容,不得拿它复制多份相同接口代码。
45
45
  - MCP 创建接口时传 `apiRoutes: ['/api/Old/A', '/api/Old/B']`;更新时省略表示保留,传空字符串/空数组表示清空。官方应用包必须同时携带 `ApiRoutes`、醒目 Managed 提示与 `ResourcePolicies.ApiEngines`。
46
-
47
- ### 资源预算与嵌套调用(强制理解)
48
-
49
- - `LimitMemory` 是单个 Jint 引擎的**累计托管分配预算**,不是实时堆占用或服务器预留内存。默认 2048MB、节点硬上限默认 8192MB。
50
- - `V8.ApiEngine.Run` 多层嵌套是正常能力。新版默认隔离父子引擎的单层分配计数,子层不会再被每个父层重复计费;根调用树另有默认 8192MB 总预算。
51
- - 接口嵌套深度默认 32、节点硬上限默认 64;它与 `LimitRecursion` 的 JavaScript 函数递归不是同一限制。
52
- - 嵌套调用不重复占用全局/租户并发名额,同一调用树重入同 Key 也不会自锁;不同子接口 Key 仍受自己的 Key 并发门保护。
53
- - `V8.Limits` 可读取本片有效预算和当前深度。异常优先检查 `DataAppend.V8Limit.Code`,不要看到“2GB”就判断服务器真实吃满 2GB。
54
- - 后台任务使用同一执行引擎。总任务可以运行数小时,但单片仍受 `Timeout/MaxStatements/LimitMemory` 约束;超过 10 分钟必须返回 `HasMore + Checkpoint` 分片续跑,不能只把 `Timeout` 调到 1800/3600。
46
+
47
+ ### 资源预算与嵌套调用(强制理解)
48
+
49
+ - `LimitMemory` 是单个 Jint 引擎的**累计托管分配预算**,不是实时堆占用或服务器预留内存。默认 2048MB、节点硬上限默认 8192MB。
50
+ - `V8.ApiEngine.Run` 多层嵌套是正常能力。新版默认隔离父子引擎的单层分配计数,子层不会再被每个父层重复计费;根调用树另有默认 8192MB 总预算。
51
+ - 接口嵌套深度默认 32、节点硬上限默认 64;它与 `LimitRecursion` 的 JavaScript 函数递归不是同一限制。
52
+ - 嵌套调用不重复占用全局/租户并发名额,同一调用树重入同 Key 也不会自锁;不同子接口 Key 仍受自己的 Key 并发门保护。
53
+ - `V8.Limits` 可读取本片有效预算和当前深度。异常优先检查 `DataAppend.V8Limit.Code`,不要看到“2GB”就判断服务器真实吃满 2GB。
54
+ - 后台任务使用同一执行引擎。总任务可以运行数小时,但单片仍受 `Timeout/MaxStatements/LimitMemory` 约束;超过 10 分钟必须返回 `HasMore + Checkpoint` 分片续跑,不能只把 `Timeout` 调到 1800/3600。
55
55
  - 接口引擎使用正向 `V8Limit`:默认 `0/false`,不设置当前 Jint Engine 的单次超时、语句、函数递归、累计分配和 Promise 固定等待预算;只有 `1/true` 才应用 `Timeout/MaxStatements/LimitMemory/LimitRecursion`。常驻内存保护、取消令牌、并发、接口嵌套深度、权限沙箱及数据库限制在两种状态下都保留。老 `V8Unlimited` 只作协议兼容;MCP/Manifest 新配置统一写 `v8Limit`。
56
56
 
57
57
  ### 流式响应(ResponseType=Stream)
@@ -95,110 +95,120 @@ return {
95
95
  - `204/304` 不得带正文;状态码限制为 100–599,正文和响应头有大小/数量限制。HTTP 契约校验失败时宿主返回标准错误,不写出半截协议响应。
96
96
 
97
97
  ### 通用实时事件(SignalR)
98
-
99
- 订单、协作、设备、审批或多人房间需要实时刷新时,业务写命令仍由接口引擎执行并提交事务;成功结果通过 `DataAppend.RealtimeEvent` 声明提交后事件。新业务统一使用通用 v2 Hub `/api-engine-realtime`,不要再新建业务专用 Hub 或把权威状态放进 C# 进程内字典。
100
-
101
- ```javascript
102
- return {
103
- Code: 1,
104
- Data: snapshot,
105
- DataAppend: { RealtimeEvent: {
106
- EventId: requestId,
107
- ChannelKey: 'order_updates',
108
- SubjectId: order.Id,
109
- Version: order.VersionNo,
110
- EventType: 'StatusChanged',
111
- Data: { Status: order.Status }
112
- } }
113
- };
114
- ```
115
-
116
- - Hub 方法固定为 `SubscribeChannel({ ChannelKey, SubjectId })` 与 `UnsubscribeChannel(...)`,客户端事件固定为 `RealtimeEvent`。订阅成功会返回 `ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt`。
117
- - 连接只接受当前有效的普通登录 Token。现有 AccessKey 权限模型没有 `realtime:subscribe` scope,平台会直接拒绝;在平台正式增加并校验该 scope 前,不得用 AccessKey 建立实时订阅。
118
- - 对应订阅授权接口固定为 `realtime_{channel_key}_authorize`。它必须用 `V8.CurrentUser` 校验资源权限,并精确回显 `Authorized/ChannelKey/SubjectId/Version`;不能信任客户端传入的 UserId、OsClient 或 ApiEngineKey。
119
- - 订阅使用 30 秒时隙租约。客户端必须按服务端返回的 `RenewAfterMilliseconds` 再次调用同一个 `SubscribeChannel` 续租;每次续租都会重新验证登录 Token、经过共享 Redis 限流,并重新执行授权接口引擎。不要把一次订阅误当成连接全生命周期永久授权。
120
- - 当前共享 Redis 限流按 `OsClient + UserId` 聚合为 10 秒最多 96 次订阅授权,跨标签页、API 节点和滚动发布共同生效;Redis 不可用时实时订阅失败关闭,业务必须继续走 HTTP Snapshot。
121
- - `EventId` 在业务重试时保持稳定;平台先用 Redis 短 Claim 协调跨节点发布,只有真实广播成功后才写 24 小时完成标记,避免“先去重、后崩溃”永久漏发。客户端仍必须按 `EventId` 去重,因为故障恢复可能产生重复通知。
122
- - `Version` 按同一 `ChannelKey + SubjectId` 单调递增。低版本事件作为过期事件拒绝广播;同版本但内容指纹不同视为版本冲突并拒绝;重放相同事件不推进 latest。
123
- - 宿主只读取成功 DosResult 中固定大小写的 `DataAppend.RealtimeEvent`,并只广播 `EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt`。`Data` 最大 32KB,只能放该群组所有订阅者都可见的安全投影;个性化私有数据通过按当前用户裁剪的 Snapshot 获取。
124
- - 客户端按 `EventId` 去重、按 `Version` 检测乱序和缺口;连接失败、续租失败、重连或发现缺口时立即重新拉 HTTP Snapshot,并保留有界轮询兜底。共享存储/状态机才是事实源。
125
- - 旧 `/game-realtime` 只作兼容。新业务默认使用通用协议,完整契约见官方 `v8-server.md`。
126
-
127
- ## 1. 匿名调用(IsAnonymous)
128
-
129
- 公开接口(登录、注册、忘记密码、验证码、扫码登录、第三方回调)必须开启:
130
-
131
- ```javascript
132
- // 例:发送验证码(匿名)
133
- if (!V8.Param.phone) return { Code: 0, Msg: '手机号不能为空' };
134
- if (!/^1[3-9]\d{9}$/.test(V8.Param.phone)) return { Code: 0, Msg: '手机号格式错误' };
135
-
136
- // 防刷:1分钟同一手机号最多1次
137
- var key = 'Microi:' + V8.OsClient + ':SmsCode:' + V8.Param.phone;
138
- if (V8.Cache.Exists(key)) return { Code: 0, Msg: '请稍后再试' };
139
-
140
- var code = Math.floor(100000 + Math.random() * 900000).toString();
141
- V8.Cache.Set(key, code, 60);
142
- // ... 调短信网关 ...
143
- return { Code: 1, Msg: '验证码已发送' };
144
- ```
145
-
146
- ### 1.1 会员端 Token 优先级
147
-
148
- 移动端/会员端自建 Token 与 Microi 后台 JWT 并存时,会员业务接口应明确 Token 优先级。MCP、后台自动化测试、PC 管理端代理调用常会在 `V8.Header.Token` 中带平台 JWT,如果接口要校验会员登录态,推荐优先读取显式会员参数或专用 Header,再回退平台 Header:
149
-
150
- ```javascript
151
- function getMemberToken() {
152
- var p = V8.Param || {};
153
- var h = V8.Header || {};
154
- var token = p.Token || p.token || h.MallMemberToken || h.mallmembertoken || h.Token || h.token || h.Authorization || h.authorization || '';
155
- token = String(token || '').trim();
156
- if (token.indexOf('Bearer ') === 0) token = token.substring(7).trim();
157
- return token;
158
- }
159
- ```
160
-
161
- 不要让后台 JWT 覆盖前端显式传入的会员 Token,否则 MCP/Playwright 用会员账号做自动化测试时会误判为未登录。
162
-
163
- ## 2. 禁止外部调用(StopHttp)
164
-
165
- 仅供其他接口引擎/V8 事件内部调用,不允许直接 HTTP 请求触发:
166
-
167
- ```javascript
168
- // 例:核心扣款接口(StopHttp=true)
169
- // 只能从 order_pay、refund 等接口通过 V8.ApiEngine.Run 调用
170
- V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
171
- .AddInParameter("@p0", V8.Param.amount)
172
- .AddInParameter("@p1", V8.Param.accountId)
173
- .ExecuteNonQuery();
174
- return { Code: 1 };
175
- ```
176
-
177
- 外部调用直接 `/apiengine/account_deduct` 会被拒绝。
178
-
179
- ## 3. 分布式锁(LockKey)
180
-
181
- 集群部署时可用接口引擎 `LockKey` 减少同一任务的并发执行(如:每月对账、自动补单):
182
-
183
- ```javascript
184
- // 配置:LockKey = month_settlement,LockTimeout = 600
185
- // 平台使用共享锁协调多节点;锁超时、节点暂停和网络分区仍可能触发重试
186
- var month = DateNow('yyyy-MM');
187
- V8.Db.FromSql('INSERT INTO MonthSettle SELECT ... WHERE Month = @p0')
188
- .AddInParameter("@p0", month)
189
- .ExecuteNonQuery();
190
- return { Code: 1 };
191
- ```
192
-
193
- `LockKey` 可包含 `${V8.OsClient}` 实现按租户独立锁。
194
-
195
- 分布式锁不是“业务只执行一次”的最终保证。扣款、库存、积分、流水、对账等副作用还必须使用稳定幂等键、数据库唯一约束/条件更新、状态机或 outbox/inbox;锁 Key 至少包含 `OsClient + 业务唯一标识`,超时必须大于正常执行时间。所需唯一索引必须写入 Manifest `tables[].indexes` 并通过 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读,接口引擎本身禁止执行索引 DDL。
196
-
98
+
99
+ 订单、协作、设备、审批或多人房间需要实时刷新时,业务写命令仍由接口引擎执行并提交事务;成功结果通过 `DataAppend.RealtimeEvent` 声明提交后事件。新业务统一使用通用 v2 Hub `/api-engine-realtime`,不要再新建业务专用 Hub 或把权威状态放进 C# 进程内字典。
100
+
101
+ ```javascript
102
+ return {
103
+ Code: 1,
104
+ Data: snapshot,
105
+ DataAppend: { RealtimeEvent: {
106
+ EventId: requestId,
107
+ ChannelKey: 'order_updates',
108
+ SubjectId: order.Id,
109
+ Version: order.VersionNo,
110
+ EventType: 'StatusChanged',
111
+ Data: { Status: order.Status }
112
+ } }
113
+ };
114
+ ```
115
+
116
+ - Hub 方法固定为 `SubscribeChannel({ ChannelKey, SubjectId })` 与 `UnsubscribeChannel(...)`,客户端事件固定为 `RealtimeEvent`。订阅成功会返回 `ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt`。
117
+ - 连接只接受当前有效的普通登录 Token。现有 AccessKey 权限模型没有 `realtime:subscribe` scope,平台会直接拒绝;在平台正式增加并校验该 scope 前,不得用 AccessKey 建立实时订阅。
118
+ - 对应订阅授权接口固定为 `realtime_{channel_key}_authorize`。它必须用 `V8.CurrentUser` 校验资源权限,并精确回显 `Authorized/ChannelKey/SubjectId/Version`;不能信任客户端传入的 UserId、OsClient 或 ApiEngineKey。
119
+ - 订阅使用 30 秒时隙租约。客户端必须按服务端返回的 `RenewAfterMilliseconds` 再次调用同一个 `SubscribeChannel` 续租;每次续租都会重新验证登录 Token、经过共享 Redis 限流,并重新执行授权接口引擎。不要把一次订阅误当成连接全生命周期永久授权。
120
+ - 当前共享 Redis 限流按 `OsClient + UserId` 聚合为 10 秒最多 96 次订阅授权,跨标签页、API 节点和滚动发布共同生效;Redis 不可用时实时订阅失败关闭,业务必须继续走 HTTP Snapshot。
121
+ - `EventId` 在业务重试时保持稳定;平台先用 Redis 短 Claim 协调跨节点发布,只有真实广播成功后才写 24 小时完成标记,避免“先去重、后崩溃”永久漏发。客户端仍必须按 `EventId` 去重,因为故障恢复可能产生重复通知。
122
+ - `Version` 按同一 `ChannelKey + SubjectId` 单调递增。低版本事件作为过期事件拒绝广播;同版本但内容指纹不同视为版本冲突并拒绝;重放相同事件不推进 latest。
123
+ - 宿主只读取成功 DosResult 中固定大小写的 `DataAppend.RealtimeEvent`,并只广播 `EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt`。`Data` 最大 32KB,只能放该群组所有订阅者都可见的安全投影;个性化私有数据通过按当前用户裁剪的 Snapshot 获取。
124
+ - 客户端按 `EventId` 去重、按 `Version` 检测乱序和缺口;连接失败、续租失败、重连或发现缺口时立即重新拉 HTTP Snapshot,并保留有界轮询兜底。共享存储/状态机才是事实源。
125
+ - 旧 `/game-realtime` 只作兼容。新业务默认使用通用协议,完整契约见官方 `v8-server.md`。
126
+
127
+ ## 1. 匿名调用(IsAnonymous)
128
+
129
+ 公开接口(登录、注册、忘记密码、验证码、扫码登录、第三方回调)必须开启:
130
+
131
+ ```javascript
132
+ // 例:发送验证码(匿名)
133
+ if (!V8.Param.phone) return { Code: 0, Msg: '手机号不能为空' };
134
+ if (!/^1[3-9]\d{9}$/.test(V8.Param.phone)) return { Code: 0, Msg: '手机号格式错误' };
135
+
136
+ // 防刷:1分钟同一手机号最多1次
137
+ var key = 'Microi:' + V8.OsClient + ':SmsCode:' + V8.Param.phone;
138
+ if (V8.Cache.Exists(key)) return { Code: 0, Msg: '请稍后再试' };
139
+
140
+ var code = Math.floor(100000 + Math.random() * 900000).toString();
141
+ V8.Cache.Set(key, code, 60);
142
+ // ... 调短信网关 ...
143
+ return { Code: 1, Msg: '验证码已发送' };
144
+ ```
145
+
146
+ ### 1.1 会员端 Token 优先级
147
+
148
+ 移动端/会员端自建 Token 与 Microi 后台 JWT 并存时,会员业务接口应明确 Token 优先级。MCP、后台自动化测试、PC 管理端代理调用常会在 `V8.Header.Token` 中带平台 JWT,如果接口要校验会员登录态,推荐优先读取显式会员参数或专用 Header,再回退平台 Header:
149
+
150
+ ```javascript
151
+ function getMemberToken() {
152
+ var p = V8.Param || {};
153
+ var h = V8.Header || {};
154
+ var token = p.Token || p.token || h.MallMemberToken || h.mallmembertoken || h.Token || h.token || h.Authorization || h.authorization || '';
155
+ token = String(token || '').trim();
156
+ if (token.indexOf('Bearer ') === 0) token = token.substring(7).trim();
157
+ return token;
158
+ }
159
+ ```
160
+
161
+ 不要让后台 JWT 覆盖前端显式传入的会员 Token,否则 MCP/Playwright 用会员账号做自动化测试时会误判为未登录。
162
+
163
+ ## 2. 禁止外部调用(StopHttp)
164
+
165
+ 仅供其他接口引擎/V8 事件内部调用,不允许直接 HTTP 请求触发:
166
+
167
+ ```javascript
168
+ // 例:核心扣款接口(StopHttp=true)
169
+ // 只能从 order_pay、refund 等接口通过 V8.ApiEngine.Run 调用
170
+ V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
171
+ .AddInParameter("@p0", V8.Param.amount)
172
+ .AddInParameter("@p1", V8.Param.accountId)
173
+ .ExecuteNonQuery();
174
+ return { Code: 1 };
175
+ ```
176
+
177
+ 外部调用直接 `/apiengine/account_deduct` 会被拒绝。
178
+
179
+ ## 3. 分布式锁(LockKey)
180
+
181
+ 集群部署时可用接口引擎 `LockKey` 减少同一任务的并发执行(如:每月对账、自动补单):
182
+
183
+ ```javascript
184
+ // 配置:LockKey = Month,Timeout = 600
185
+ // 调用方传入 Month;平台使用共享锁协调多节点
186
+ var month = String(V8.Param.Month || '');
187
+ if (!/^\d{4}-\d{2}$/.test(month)) return { Code: 0, Msg: 'Month 格式不正确' };
188
+ V8.Db.FromSql('INSERT INTO MonthSettle SELECT ... WHERE Month = @p0')
189
+ .AddInParameter("@p0", month)
190
+ .ExecuteNonQuery();
191
+ return { Code: 1 };
192
+ ```
193
+
194
+ `LockKey` 填写请求参数字段名;上例调用方应传入 `Month`,平台使用其值区分不同月份。未填写时按 `ApiEngineKey` 串行。平台自动把锁放入当前 `OsClient` 命名空间,不需要也不应让客户端自行拼接其它租户前缀。缺少已配置的参数字段时会退回使用字段名本身,虽然仍能互斥,但会让所有请求共享一把锁,因此保存与 HTTP 复测必须覆盖实际参数。
195
+
196
+ ### 普通调用与可信后台任务的租约差异
197
+
198
+ - 普通 HTTP 或普通 V8 调用保持固定租期:`Timeout` 是锁成功获取后的 Redis TTL,不会自动延长。它必须大于正常执行时间,但不能靠设置超大数值代替可靠后台任务。
199
+ - 通过平台 `RunBackground`/后台任务服务进入的可信持久执行,会在回调期间按持有者令牌自动续租。当前默认最长续租边界为 12 小时;若接口显式配置的单次租期本身更长,平台不会把它缩短到 12 小时。
200
+ - 可信身份由服务端建立,并同时校验任务 Id、后台任务信封和正数 fencing token。客户端或普通 V8 自行传入 `_BackgroundTaskId`、`_BackgroundTask`、`_BackgroundTaskFencingToken`、`_TrustedServerInvocation` 或 `_CurrentUser`,不能开启自动续租。
201
+ - 续租和释放都以唯一持有者令牌做 Redis 原子比较;锁每次成功获取还会产生单调递增 fencing token。持有者不匹配、锁已过期、Redis 所有权/续租确认失败或达到最长租约时,执行必须失败关闭,旧持有者不得继续提交副作用。
202
+
203
+ 分布式锁不是“业务只执行一次”的最终保证。扣款、库存、积分、流水、对账等副作用还必须使用稳定幂等键、数据库唯一约束/条件更新、状态机或 outbox/inbox;锁 Key 至少包含 `OsClient + 业务唯一标识`,超时必须大于正常执行时间。所需唯一索引必须写入 Manifest `tables[].indexes` 并通过 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读,接口引擎本身禁止执行索引 DDL。
204
+
205
+ 预计超过 10 分钟的任务即使具备自动续租,也必须使用 `HasMore + Checkpoint` 分片并持久化真实进度。每个业务条件写入使用 `_BackgroundTaskFencingToken` 拒绝租约过期的旧执行者;错误信息包含“分布式锁租约已丢失”时不得捕获后返回成功,任务应保留最后进度与原始原因,交由后台任务的幂等恢复或人工诊断处理。
206
+
197
207
  ## 4. 自定义路径(ApiAddress)
198
-
199
- 让接口暴露为 `/wechat/notify` 而非 `/apiengine/wechat_notify`,对接第三方时常用:
200
-
201
- ```
208
+
209
+ 让接口暴露为 `/wechat/notify` 而非 `/apiengine/wechat_notify`,对接第三方时常用:
210
+
211
+ ```
202
212
  ApiAddress: /wechat/notify
203
213
  ```
204
214
 
@@ -210,67 +220,67 @@ ApiAddress: /saml/{OsClient}/sp/{ConnectionKey}/metadata
210
220
  ```
211
221
 
212
222
  模板只匹配完整路径段,不支持贪婪通配符。命中的路由值会写入 `V8.Param._RouteValues`,并以权威路径值覆盖同名 Query/Form/JSON 参数,防止调用者伪造另一租户或连接。包含 `{OsClient}` 时租户由该路径段解析;同一路径匹配多个模板视为配置冲突并拒绝执行。
213
-
214
- ## 5. 响应文件(IsResponseFile)
215
-
216
- 开启后接口可直接输出二进制流:
217
-
218
- 后端会统一处理响应头和文件头校验:图片/PDF 浏览器直接打开,其它文件下载;V8 代码只返回文件三字段,不要在接口里手写复杂的魔数判断。`ContentType` 必须匹配真实字节,金蝶 PLM `KD_C_PLM` 等业务封装流不能伪装成 `application/pdf`。
219
-
220
- 响应文件动态路由必须同时接受 `GET` 和 `HEAD`。OnlyOffice 等服务端预览器可能先用 `HEAD` 探测文件类型、长度和可达性;如果浏览器直接下载正常但 `HEAD` 返回 `405`,在线预览仍可能一直停在“加载文档”。
221
-
222
- ```javascript
223
- // 必须返回特定结构
224
- return {
225
- Code: 1,
226
- Data: {
227
- FileName: 'report.xlsx',
228
- ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
229
- FileByteBase64: System.Convert.ToBase64String(byteArr)
230
- }
231
- };
232
- ```
233
-
234
- 详见 `v8-file-upload/SKILL.md`。
235
-
236
- ## 6. 频率限制(RateLimit)
237
-
238
- 防爬虫、防刷:
239
-
240
- | 配置 | 含义 |
241
- |------|------|
242
- | `60/m` | 每分钟 60 次 |
243
- | `1000/h` | 每小时 1000 次 |
244
- | `100/s` | 每秒 100 次 |
245
-
246
- 按客户端 IP + 接口 维度限流。
247
-
248
- ## 7. 日志记录(LogParam / LogResult)
249
-
250
- 支付、撤销等敏感接口建议开启,自动记到 `sys_log` 用于审计回溯:
251
-
252
- ```
253
- LogParam = true # 记录每次入参
254
- LogResult = true # 记录每次返回
255
- ```
256
-
257
- > ❌ 接口返回结果含敏感数据(密码、token、密钥)时不要打开 `LogResult`
258
-
259
- ## 8. 保存后 HTTP 复测
260
-
261
- 通过 MCP 维护接口引擎时,先用 `microi_list_engines` 发现现有接口,再用
262
- `microi_get_engine_code` 读取源码;修改后使用 `microi_save_engine_code`
263
- 保存并回读。只有确认目标不存在时才调用创建工具,避免重复
264
- `ApiEngineKey`。`microi_run_engine` 适合做引擎上下文内的最小调试,但不能
265
- 代替下方真实 HTTP 复测。
266
-
267
- `microi_run_engine` 只能证明引擎代码在 MCP/内部执行上下文可运行,不能证明移动端或外部 HTTP 能调用。新建或更新接口后必须再走一次真实 HTTP 路径:
268
-
269
- ```text
270
- POST /apiengine/{ApiEngineKey}
271
- Headers: Content-Type=application/json, osclient={OsClient}, apiengine=1
272
- Body: {"Action":"Bootstrap","OsClient":"{OsClient}"}
273
-
223
+
224
+ ## 5. 响应文件(IsResponseFile)
225
+
226
+ 开启后接口可直接输出二进制流:
227
+
228
+ 后端会统一处理响应头和文件头校验:图片/PDF 浏览器直接打开,其它文件下载;V8 代码只返回文件三字段,不要在接口里手写复杂的魔数判断。`ContentType` 必须匹配真实字节,金蝶 PLM `KD_C_PLM` 等业务封装流不能伪装成 `application/pdf`。
229
+
230
+ 响应文件动态路由必须同时接受 `GET` 和 `HEAD`。OnlyOffice 等服务端预览器可能先用 `HEAD` 探测文件类型、长度和可达性;如果浏览器直接下载正常但 `HEAD` 返回 `405`,在线预览仍可能一直停在“加载文档”。
231
+
232
+ ```javascript
233
+ // 必须返回特定结构
234
+ return {
235
+ Code: 1,
236
+ Data: {
237
+ FileName: 'report.xlsx',
238
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
239
+ FileByteBase64: System.Convert.ToBase64String(byteArr)
240
+ }
241
+ };
242
+ ```
243
+
244
+ 详见 `v8-file-upload/SKILL.md`。
245
+
246
+ ## 6. 频率限制(RateLimit)
247
+
248
+ 防爬虫、防刷:
249
+
250
+ | 配置 | 含义 |
251
+ |------|------|
252
+ | `60/m` | 每分钟 60 次 |
253
+ | `1000/h` | 每小时 1000 次 |
254
+ | `100/s` | 每秒 100 次 |
255
+
256
+ 按客户端 IP + 接口 维度限流。
257
+
258
+ ## 7. 日志记录(LogParam / LogResult)
259
+
260
+ 支付、撤销等敏感接口建议开启,自动记到 `sys_log` 用于审计回溯:
261
+
262
+ ```
263
+ LogParam = true # 记录每次入参
264
+ LogResult = true # 记录每次返回
265
+ ```
266
+
267
+ > ❌ 接口返回结果含敏感数据(密码、token、密钥)时不要打开 `LogResult`
268
+
269
+ ## 8. 保存后 HTTP 复测
270
+
271
+ 通过 MCP 维护接口引擎时,先用 `microi_list_engines` 发现现有接口,再用
272
+ `microi_get_engine_code` 读取源码;修改后使用 `microi_save_engine_code`
273
+ 保存并回读。只有确认目标不存在时才调用创建工具,避免重复
274
+ `ApiEngineKey`。`microi_run_engine` 适合做引擎上下文内的最小调试,但不能
275
+ 代替下方真实 HTTP 复测。
276
+
277
+ `microi_run_engine` 只能证明引擎代码在 MCP/内部执行上下文可运行,不能证明移动端或外部 HTTP 能调用。新建或更新接口后必须再走一次真实 HTTP 路径:
278
+
279
+ ```text
280
+ POST /apiengine/{ApiEngineKey}
281
+ Headers: Content-Type=application/json, osclient={OsClient}, apiengine=1
282
+ Body: {"Action":"Bootstrap","OsClient":"{OsClient}"}
283
+
274
284
  # 仅用于不能立即升级的旧客户端;新增或可修改代码禁止使用
275
285
  POST /api/ApiEngine/Run
276
286
  Headers: Content-Type=application/json, OsClient={OsClient}
@@ -281,24 +291,24 @@ Body: {"ApiEngineKey":"your_key","Action":"Bootstrap"}
281
291
  `ApiAddress`。禁止新增 `/api/ApiEngine/Run` 依赖,否则反向代理、限流、审计和
282
292
  系统日志/监控只能看到同一个通用入口,难以按真实接口引擎准确归因。SDK 只可在
283
293
  显式命名的 `RunLegacy` 兼容方法中保留旧地址,普通 `Run` 必须生成动态地址。
284
-
285
- 复测重点:
286
-
287
- - `IsEnable=1`、`StopHttp=0`、公开接口 `AllowAnonymous=1`。
288
- - JSON Body 会恢复到 `V8.Param`;同名参数已由 Query/Form 绑定时保持既有值,避免改变旧调用优先级。直接动态路由与兼容入口都要覆盖 JSON Body 测试,不能只用 Query 参数证明可用。
289
- - HTTP 请求中的 `_CurrentUser`、`_InvokeType:'Server'`、`_TrustedServerInvocation` 都不能建立可信服务端身份;当前用户和调用类型必须由认证中间件与接口层重新写入。
290
- - `ApiAddress` 不能为空字符串;空字符串可能导致 404。
291
- - 响应不能是空 body、字符串 `null`、非 JSON;业务接口必须返回标准 DosResult。
292
- - 普通 `POST/PUT/PATCH/DELETE` 必须使用稳定路径 `/apiengine/{ApiEngineKey}`,租户放在唯一的 `osclient` Header,并可在 JSON/Form Body 中冗余传入;禁止无脑给路径追加 `--OsClient--...--`。普通 GET 优先 Header 或 `?OsClient=`。只有微信/支付等第三方回调(包括 POST)、浏览器直接下载等调用方确实无法设置 Header 或 Query 的场景,才使用 `--OsClient--{OsClient}--` 特殊路径;Query 参数名固定为 `OsClient`,禁止 `o` 等缩写。
294
+
295
+ 复测重点:
296
+
297
+ - `IsEnable=1`、`StopHttp=0`、公开接口 `AllowAnonymous=1`。
298
+ - JSON Body 会恢复到 `V8.Param`;同名参数已由 Query/Form 绑定时保持既有值,避免改变旧调用优先级。直接动态路由与兼容入口都要覆盖 JSON Body 测试,不能只用 Query 参数证明可用。
299
+ - HTTP 请求中的 `_CurrentUser`、`_InvokeType:'Server'`、`_TrustedServerInvocation` 都不能建立可信服务端身份;当前用户和调用类型必须由认证中间件与接口层重新写入。
300
+ - `ApiAddress` 不能为空字符串;空字符串可能导致 404。
301
+ - 响应不能是空 body、字符串 `null`、非 JSON;业务接口必须返回标准 DosResult。
302
+ - 普通 `POST/PUT/PATCH/DELETE` 必须使用稳定路径 `/apiengine/{ApiEngineKey}`,租户放在唯一的 `osclient` Header,并可在 JSON/Form Body 中冗余传入;禁止无脑给路径追加 `--OsClient--...--`。普通 GET 优先 Header 或 `?OsClient=`。只有微信/支付等第三方回调(包括 POST)、浏览器直接下载等调用方确实无法设置 Header 或 Query 的场景,才使用 `--OsClient--{OsClient}--` 特殊路径;Query 参数名固定为 `OsClient`,禁止 `o` 等缩写。
293
303
  - 需要 C# 验签/AES 解密、协议编解码或隐藏 SaaS 密钥的回调,使用“`Managed` HTTP 接口引擎 + 精确 Key 可调用的最小 `V8.Method` 可信原子 + `CreateIfMissing` 租户 Hook”。公开地址仍归接口引擎,禁止为此恢复 Controller;传给 Hook 的事件必须脱敏,并包含稳定 `EventId` 供幂等。
294
- - 更新接口代码时保留 HTTP 元数据,避免只覆盖 JS 代码却把匿名、启用、自定义地址等配置冲掉。
295
-
296
- ### 路由冷缓存与客户端直达头(强制)
297
-
298
- - `/apiengine/{ApiEngineKey}` 客户端请求必须携带 `apiengine: 1`;标准前端 SDK 应从稳定路径自动识别并补齐,不能要求每个业务页面手写 Header。自定义 `ApiAddress` 无法从路径识别时,调用方显式设置 `apiEngine: true`。
299
- - 动态路由缓存未命中后允许从 `sys_apiengine` 权威回源并重建 `ApiEngineKey`、`ApiAddress` 两个别名。回源对象如果来自 `dynamic`,先转换为 `JObject`/`object`,并把字段显式赋给 `string`、`bool` 等强类型局部变量,再调用普通方法或扩展方法;禁止让 `dynamic` 调用链延续到 `DosIsNullOrWhiteSpace`、LINQ 或 JToken 扩展。
300
- - 自动化必须覆盖“缓存预热命中”和“缓存为空首次请求”两条路径;首次请求不得 404,且回源后两个缓存别名均可再次命中。滚动发布、节点重启或缓存清理后要重复执行无 Header 与带 `apiengine: 1` 的真实 HTTP smoke test。
301
-
304
+ - 更新接口代码时保留 HTTP 元数据,避免只覆盖 JS 代码却把匿名、启用、自定义地址等配置冲掉。
305
+
306
+ ### 路由冷缓存与客户端直达头(强制)
307
+
308
+ - `/apiengine/{ApiEngineKey}` 客户端请求必须携带 `apiengine: 1`;标准前端 SDK 应从稳定路径自动识别并补齐,不能要求每个业务页面手写 Header。自定义 `ApiAddress` 无法从路径识别时,调用方显式设置 `apiEngine: true`。
309
+ - 动态路由缓存未命中后允许从 `sys_apiengine` 权威回源并重建 `ApiEngineKey`、`ApiAddress` 两个别名。回源对象如果来自 `dynamic`,先转换为 `JObject`/`object`,并把字段显式赋给 `string`、`bool` 等强类型局部变量,再调用普通方法或扩展方法;禁止让 `dynamic` 调用链延续到 `DosIsNullOrWhiteSpace`、LINQ 或 JToken 扩展。
310
+ - 自动化必须覆盖“缓存预热命中”和“缓存为空首次请求”两条路径;首次请求不得 404,且回源后两个缓存别名均可再次命中。滚动发布、节点重启或缓存清理后要重复执行无 Header 与带 `apiengine: 1` 的真实 HTTP smoke test。
311
+
302
312
  ### 复盘:接口引擎冷缓存回源异常被吞成空 404
303
313
 
304
314
  - 触发场景:接口配置、启用和匿名设置都正确,接口昨天可用;节点重启或缓存缺失后,小程序首次请求 `/apiengine/{key}` 返回空 body 404。
@@ -314,53 +324,55 @@ Body: {"ApiEngineKey":"your_key","Action":"Bootstrap"}
314
324
  - 自动化检查:至少覆盖“显式地址尾段与 Key 不同”和“ApiAddress 为空的传统 Key 路由”,并分别验证 `?OsClient=`、`--OsClient--...--`、冷缓存首次请求、停用地址阻断及匿名关闭返回鉴权错误而非不存在。
315
325
 
316
326
  ## 请求内异步与可靠后台任务
317
-
318
- 接口默认同步返回。对本次请求必须完成的异步 I/O,调用真实的 `*Async` 方法并 `await`。常用入口包括 `V8.Http.*Async`、`V8.FormEngine.GetTableDataAsync` 和 `V8.ApiEngine.RunAsync`:
319
-
320
- ```javascript
321
- var resp = await V8.Http.GetResponseAsync({
322
- Url: 'https://example.com/health',
323
- Timeout: 5
324
- });
325
- if (resp.StatusCode < 200 || resp.StatusCode >= 300) {
326
- return { Code: 0, Msg: '上游调用失败' };
327
- }
328
-
329
- var users = await V8.FormEngine.GetTableDataAsync('SysUser', {
330
- _Where: [['Status', '=', 1]],
331
- _SelectFields: ['Id', 'Name'],
332
- _PageSize: 20
333
- });
334
-
335
- var summary = await V8.ApiEngine.RunAsync('build-user-summary', {
336
- Users: users.Data
337
- });
338
- return { Code: 1, Data: { Upstream: resp.Content, Summary: summary.Data } };
339
- ```
340
-
341
- 禁止用 `setTimeout` 或 `System.Threading.Tasks.Task.Run` 实现“接口先返回、后台继续执行”:`V8Engine.Run` 返回后会释放 Jint Engine、租户上下文、事务和并发租约,回调不可靠,也没有持久化、重试、幂等或重启恢复保证。
342
-
343
- 需要先响应再处理时,使用接口引擎后台任务按钮(`RunBackground + ApiEngineKey`)、Job、MQ 或 outbox;消费者按全局 `EventId` 幂等处理并持久化进度。AI 发现预计超过 2 分钟、500 条、1000 个扇出子操作、100 次外部调用,或安装/初始化/迁移/备份/全量生成等任务时,必须主动切换为后台任务;预计超过 10 分钟时还必须设计 checkpoint 分片。见 `job-engine`、`v8-menu-buttons`、`v8-mq-mqtt` 和 `microi-system-delivery`。
344
-
345
- ## 接口安全检查清单
346
-
347
- - [ ] 公开接口是否仅开启 `IsAnonymous`,敏感接口是否关闭?
348
- - [ ] 内部接口是否开启 `StopHttp`?
349
- - [ ] 写操作(扣款、对账、补单)是否配置 `LockKey`?
350
- - [ ] 锁之外是否还有幂等键、唯一约束/条件更新或状态机?
351
- - [ ] 频率敏感接口是否配置 `RateLimit`?
352
- - [ ] 审计需求接口是否开启 `LogParam`?
353
- - [ ] 文件响应接口是否开启 `IsResponseFile`?
354
- - [ ] 接口代码内是否仍校验 `V8.CurrentUser`(`IsAnonymous=true` 时尤其重要)?
355
- - [ ] 是否没有使用 `setTimeout` / `Task.Run` 承担请求外后台任务?
356
- - [ ] 大任务是否按阈值主动使用后台任务,超过 10 分钟是否有 `HasMore + Checkpoint`?
357
- - [ ] 是否区分累计分配、调用树预算、JS递归与接口嵌套,而不是盲目抬高全部限制?
358
- - [ ] 是否确认 `V8Limit=false` 表示接口不限 Jint 单次预算、`true` 才启用限制,并避免继续写入旧 `V8Unlimited` 字段?
359
- - [ ] 保存后是否通过稳定路径 `/apiengine/{key}` + `osclient` Header 做过 HTTP 复测?特殊 GET/HEAD 路径是否仅用于无法设置 Header/Form/Query 的场景?
360
-
361
- ## 常见错误
362
-
363
- ❌ 把支付回调接口设为非匿名 → 第三方无 Token → 回调失败
364
- ❌ 内部接口忘开 `StopHttp` → 被外部直接调用绕过校验
365
- ❌ 对账接口未配置 `LockKey` → 集群多实例并发执行 → 数据双倍
366
- ❌ 文件下载接口未开 `IsResponseFile` → 返回 JSON 而非文件流
327
+
328
+ 接口默认同步返回。对本次请求必须完成的异步 I/O,调用真实的 `*Async` 方法并 `await`。常用入口包括 `V8.Http.*Async`、`V8.FormEngine.GetTableDataAsync` 和 `V8.ApiEngine.RunAsync`:
329
+
330
+ ```javascript
331
+ var resp = await V8.Http.GetResponseAsync({
332
+ Url: 'https://example.com/health',
333
+ Timeout: 5
334
+ });
335
+ if (resp.StatusCode < 200 || resp.StatusCode >= 300) {
336
+ return { Code: 0, Msg: '上游调用失败' };
337
+ }
338
+
339
+ var users = await V8.FormEngine.GetTableDataAsync('SysUser', {
340
+ _Where: [['Status', '=', 1]],
341
+ _SelectFields: ['Id', 'Name'],
342
+ _PageSize: 20
343
+ });
344
+
345
+ var summary = await V8.ApiEngine.RunAsync('build-user-summary', {
346
+ Users: users.Data
347
+ });
348
+ return { Code: 1, Data: { Upstream: resp.Content, Summary: summary.Data } };
349
+ ```
350
+
351
+ 禁止用 `setTimeout` 或 `System.Threading.Tasks.Task.Run` 实现“接口先返回、后台继续执行”:`V8Engine.Run` 返回后会释放 Jint Engine、租户上下文、事务和并发租约,回调不可靠,也没有持久化、重试、幂等或重启恢复保证。
352
+
353
+ 需要先响应再处理时,使用接口引擎后台任务按钮(`RunBackground + ApiEngineKey`)、Job、MQ 或 outbox;消费者按全局 `EventId` 幂等处理并持久化进度。AI 发现预计超过 2 分钟、500 条、1000 个扇出子操作、100 次外部调用,或安装/初始化/迁移/备份/全量生成等任务时,必须主动切换为后台任务;预计超过 10 分钟时还必须设计 checkpoint 分片。见 `job-engine`、`v8-menu-buttons`、`v8-mq-mqtt` 和 `microi-system-delivery`。
354
+
355
+ ## 接口安全检查清单
356
+
357
+ - [ ] 公开接口是否仅开启 `IsAnonymous`,敏感接口是否关闭?
358
+ - [ ] 内部接口是否开启 `StopHttp`?
359
+ - [ ] 写操作(扣款、对账、补单)是否配置 `LockKey`?
360
+ - [ ] `LockKey` 是否指向真实存在的请求参数,`Timeout` 是否是合理的单次租期?
361
+ - [ ] 锁之外是否还有幂等键、唯一约束/条件更新或状态机?
362
+ - [ ] 长任务是否只由平台可信后台上下文自动续租,并在租约丢失时失败关闭?
363
+ - [ ] 频率敏感接口是否配置 `RateLimit`?
364
+ - [ ] 审计需求接口是否开启 `LogParam`?
365
+ - [ ] 文件响应接口是否开启 `IsResponseFile`?
366
+ - [ ] 接口代码内是否仍校验 `V8.CurrentUser`(`IsAnonymous=true` 时尤其重要)?
367
+ - [ ] 是否没有使用 `setTimeout` / `Task.Run` 承担请求外后台任务?
368
+ - [ ] 大任务是否按阈值主动使用后台任务,超过 10 分钟是否有 `HasMore + Checkpoint`?
369
+ - [ ] 是否区分累计分配、调用树预算、JS递归与接口嵌套,而不是盲目抬高全部限制?
370
+ - [ ] 是否确认 `V8Limit=false` 表示接口不限 Jint 单次预算、`true` 才启用限制,并避免继续写入旧 `V8Unlimited` 字段?
371
+ - [ ] 保存后是否通过稳定路径 `/apiengine/{key}` + `osclient` Header 做过 HTTP 复测?特殊 GET/HEAD 路径是否仅用于无法设置 Header/Form/Query 的场景?
372
+
373
+ ## 常见错误
374
+
375
+ ❌ 把支付回调接口设为非匿名 → 第三方无 Token → 回调失败
376
+ ❌ 内部接口忘开 `StopHttp` → 被外部直接调用绕过校验
377
+ ❌ 对账接口未配置 `LockKey` → 集群多实例并发执行 → 数据双倍
378
+ ❌ 文件下载接口未开 `IsResponseFile` → 返回 JSON 而非文件流
@@ -255,7 +255,7 @@ if (result.Code === 1 && result.Data) {
255
255
 
256
256
  V8 业务脚本需要互斥时:
257
257
 
258
- 1. 接口引擎使用平台 `LockKey/LockTimeout` 配置;
258
+ 1. 接口引擎使用平台 `LockKey/Timeout` 配置;`Timeout` 是单次锁租期,可信持久后台任务可由平台续租,普通 HTTP/V8 调用保持固定租期;
259
259
  2. Job/Worker 使用带租约、唯一持有者令牌、续租、超时自动释放和“仅持有者可释放”语义的共享锁;
260
260
  3. Key 至少包含 `OsClient + 任务/业务唯一标识`;
261
261
  4. 分布式锁只能减少并发,业务副作用仍必须用幂等键、唯一约束/条件更新、状态机或 outbox/inbox 保证只执行一次。
@@ -263,6 +263,8 @@ V8 业务脚本需要互斥时:
263
263
  `SetIfNotExists` 只提供原子的“首次写入 + TTL”,没有唯一持有者令牌、续租和
264
264
  仅持有者释放语义。禁止把它或其它普通 Cache 调用拼成分布式锁。
265
265
 
266
+ 自动续租只允许平台建立的可信后台执行上下文使用;客户端伪造后台任务字段不能开启。持有者令牌不匹配、锁已过期、Redis 续租失败或达到最长租约时必须失败关闭,旧执行者还要由 fencing token 条件写入阻断。完整规则见 `../v8-api-config/SKILL.md`。
267
+
266
268
  ## 原子计数与限流
267
269
 
268
270
  `Get → parseInt → Set` 在并发下会丢计数。普通 Hash 计数可使用 `V8.Cache.HashIncrement`;需要“计数 + 首次设置 TTL + 超限拒绝”的安全限流、日上传配额或金额额度时,应使用平台 `RateLimit` / SecurityGuard 或后端 Redis Lua 原子脚本,并在 Redis 不可用时按风险选择失败关闭。不要在 V8 中用多个普通 Cache 调用模拟原子配额。