@microi.net/cli 5.2.1 → 5.2.2

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,6 +1,6 @@
1
1
  ---
2
2
  name: v8-api-config
3
- description: Microi V8 接口引擎配置指南。用于设置 ApiEngineKey、ApiAddress、StopHttp、AllowAnonymous、ResponseFile、锁、日志、超时和 HTTP 暴露。
3
+ description: Microi V8 接口引擎配置指南。用于设置 ApiEngineKey、ApiAddress、StopHttp、AllowAnonymous、ResponseFile、锁、日志、超时和 HTTP 暴露。
4
4
  ---
5
5
 
6
6
  > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
@@ -25,47 +25,47 @@ description: Microi V8 接口引擎配置指南。用于设置 ApiEngineKey、Ap
25
25
  | `LockMsg` | 加锁失败时返回提示 | `操作过于频繁` |
26
26
  | `RateLimit` | 频率限制(如 `60/m` 每分钟60次) | 空 |
27
27
  | `LogParam` | 是否记录请求参数到 `sys_log` | `false` |
28
- | `LogResult` | 是否记录返回值到 `sys_log` | `false` |
29
-
30
- ### 资源预算与嵌套调用(强制理解)
31
-
32
- - `LimitMemory` 是单个 Jint 引擎的**累计托管分配预算**,不是实时堆占用或服务器预留内存。默认 2048MB、节点硬上限默认 8192MB。
33
- - `V8.ApiEngine.Run` 多层嵌套是正常能力。新版默认隔离父子引擎的单层分配计数,子层不会再被每个父层重复计费;根调用树另有默认 8192MB 总预算。
34
- - 接口嵌套深度默认 32、节点硬上限默认 64;它与 `LimitRecursion` 的 JavaScript 函数递归不是同一限制。
35
- - 嵌套调用不重复占用全局/租户并发名额,同一调用树重入同 Key 也不会自锁;不同子接口 Key 仍受自己的 Key 并发门保护。
36
- - `V8.Limits` 可读取本片有效预算和当前深度。异常优先检查 `DataAppend.V8Limit.Code`,不要看到“2GB”就判断服务器真实吃满 2GB。
37
- - 后台任务使用同一执行引擎。总任务可以运行数小时,但单片仍受 `Timeout/MaxStatements/LimitMemory` 约束;超过 10 分钟必须返回 `HasMore + Checkpoint` 分片续跑,不能只把 `Timeout` 调到 1800/3600。
38
- - 接口引擎使用正向 `V8Limit`:默认 `0/false`,不设置当前 Jint Engine 的单次超时、语句、函数递归、累计分配和 Promise 固定等待预算;只有 `1/true` 才应用 `Timeout/MaxStatements/LimitMemory/LimitRecursion`。常驻内存保护、取消令牌、并发、接口嵌套深度、权限沙箱及数据库限制在两种状态下都保留。老 `V8Unlimited` 只作协议兼容;MCP/Manifest 新配置统一写 `v8Limit`。
39
-
40
- ### 通用实时事件(SignalR)
41
-
42
- 订单、协作、设备、审批或多人房间需要实时刷新时,业务写命令仍由接口引擎执行并提交事务;成功结果通过 `DataAppend.RealtimeEvent` 声明提交后事件。新业务统一使用通用 v2 Hub `/api-engine-realtime`,不要再新建业务专用 Hub 或把权威状态放进 C# 进程内字典。
43
-
44
- ```javascript
45
- return {
46
- Code: 1,
47
- Data: snapshot,
48
- DataAppend: { RealtimeEvent: {
49
- EventId: requestId,
50
- ChannelKey: 'order_updates',
51
- SubjectId: order.Id,
52
- Version: order.VersionNo,
53
- EventType: 'StatusChanged',
54
- Data: { Status: order.Status }
55
- } }
56
- };
57
- ```
58
-
59
- - Hub 方法固定为 `SubscribeChannel({ ChannelKey, SubjectId })` 与 `UnsubscribeChannel(...)`,客户端事件固定为 `RealtimeEvent`。订阅成功会返回 `ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt`。
60
- - 连接只接受当前有效的普通登录 Token。现有 AccessKey 权限模型没有 `realtime:subscribe` scope,平台会直接拒绝;在平台正式增加并校验该 scope 前,不得用 AccessKey 建立实时订阅。
61
- - 对应订阅授权接口固定为 `realtime_{channel_key}_authorize`。它必须用 `V8.CurrentUser` 校验资源权限,并精确回显 `Authorized/ChannelKey/SubjectId/Version`;不能信任客户端传入的 UserId、OsClient 或 ApiEngineKey。
62
- - 订阅使用 30 秒时隙租约。客户端必须按服务端返回的 `RenewAfterMilliseconds` 再次调用同一个 `SubscribeChannel` 续租;每次续租都会重新验证登录 Token、经过共享 Redis 限流,并重新执行授权接口引擎。不要把一次订阅误当成连接全生命周期永久授权。
63
- - 当前共享 Redis 限流按 `OsClient + UserId` 聚合为 10 秒最多 96 次订阅授权,跨标签页、API 节点和滚动发布共同生效;Redis 不可用时实时订阅失败关闭,业务必须继续走 HTTP Snapshot。
64
- - `EventId` 在业务重试时保持稳定;平台先用 Redis 短 Claim 协调跨节点发布,只有真实广播成功后才写 24 小时完成标记,避免“先去重、后崩溃”永久漏发。客户端仍必须按 `EventId` 去重,因为故障恢复可能产生重复通知。
65
- - `Version` 按同一 `ChannelKey + SubjectId` 单调递增。低版本事件作为过期事件拒绝广播;同版本但内容指纹不同视为版本冲突并拒绝;重放相同事件不推进 latest。
66
- - 宿主只读取成功 DosResult 中固定大小写的 `DataAppend.RealtimeEvent`,并只广播 `EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt`。`Data` 最大 32KB,只能放该群组所有订阅者都可见的安全投影;个性化私有数据通过按当前用户裁剪的 Snapshot 获取。
67
- - 客户端按 `EventId` 去重、按 `Version` 检测乱序和缺口;连接失败、续租失败、重连或发现缺口时立即重新拉 HTTP Snapshot,并保留有界轮询兜底。共享存储/状态机才是事实源。
68
- - 旧 `/game-realtime` 只作兼容。新业务默认使用通用协议,完整契约见官方 `v8-server.md`。
28
+ | `LogResult` | 是否记录返回值到 `sys_log` | `false` |
29
+
30
+ ### 资源预算与嵌套调用(强制理解)
31
+
32
+ - `LimitMemory` 是单个 Jint 引擎的**累计托管分配预算**,不是实时堆占用或服务器预留内存。默认 2048MB、节点硬上限默认 8192MB。
33
+ - `V8.ApiEngine.Run` 多层嵌套是正常能力。新版默认隔离父子引擎的单层分配计数,子层不会再被每个父层重复计费;根调用树另有默认 8192MB 总预算。
34
+ - 接口嵌套深度默认 32、节点硬上限默认 64;它与 `LimitRecursion` 的 JavaScript 函数递归不是同一限制。
35
+ - 嵌套调用不重复占用全局/租户并发名额,同一调用树重入同 Key 也不会自锁;不同子接口 Key 仍受自己的 Key 并发门保护。
36
+ - `V8.Limits` 可读取本片有效预算和当前深度。异常优先检查 `DataAppend.V8Limit.Code`,不要看到“2GB”就判断服务器真实吃满 2GB。
37
+ - 后台任务使用同一执行引擎。总任务可以运行数小时,但单片仍受 `Timeout/MaxStatements/LimitMemory` 约束;超过 10 分钟必须返回 `HasMore + Checkpoint` 分片续跑,不能只把 `Timeout` 调到 1800/3600。
38
+ - 接口引擎使用正向 `V8Limit`:默认 `0/false`,不设置当前 Jint Engine 的单次超时、语句、函数递归、累计分配和 Promise 固定等待预算;只有 `1/true` 才应用 `Timeout/MaxStatements/LimitMemory/LimitRecursion`。常驻内存保护、取消令牌、并发、接口嵌套深度、权限沙箱及数据库限制在两种状态下都保留。老 `V8Unlimited` 只作协议兼容;MCP/Manifest 新配置统一写 `v8Limit`。
39
+
40
+ ### 通用实时事件(SignalR)
41
+
42
+ 订单、协作、设备、审批或多人房间需要实时刷新时,业务写命令仍由接口引擎执行并提交事务;成功结果通过 `DataAppend.RealtimeEvent` 声明提交后事件。新业务统一使用通用 v2 Hub `/api-engine-realtime`,不要再新建业务专用 Hub 或把权威状态放进 C# 进程内字典。
43
+
44
+ ```javascript
45
+ return {
46
+ Code: 1,
47
+ Data: snapshot,
48
+ DataAppend: { RealtimeEvent: {
49
+ EventId: requestId,
50
+ ChannelKey: 'order_updates',
51
+ SubjectId: order.Id,
52
+ Version: order.VersionNo,
53
+ EventType: 'StatusChanged',
54
+ Data: { Status: order.Status }
55
+ } }
56
+ };
57
+ ```
58
+
59
+ - Hub 方法固定为 `SubscribeChannel({ ChannelKey, SubjectId })` 与 `UnsubscribeChannel(...)`,客户端事件固定为 `RealtimeEvent`。订阅成功会返回 `ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt`。
60
+ - 连接只接受当前有效的普通登录 Token。现有 AccessKey 权限模型没有 `realtime:subscribe` scope,平台会直接拒绝;在平台正式增加并校验该 scope 前,不得用 AccessKey 建立实时订阅。
61
+ - 对应订阅授权接口固定为 `realtime_{channel_key}_authorize`。它必须用 `V8.CurrentUser` 校验资源权限,并精确回显 `Authorized/ChannelKey/SubjectId/Version`;不能信任客户端传入的 UserId、OsClient 或 ApiEngineKey。
62
+ - 订阅使用 30 秒时隙租约。客户端必须按服务端返回的 `RenewAfterMilliseconds` 再次调用同一个 `SubscribeChannel` 续租;每次续租都会重新验证登录 Token、经过共享 Redis 限流,并重新执行授权接口引擎。不要把一次订阅误当成连接全生命周期永久授权。
63
+ - 当前共享 Redis 限流按 `OsClient + UserId` 聚合为 10 秒最多 96 次订阅授权,跨标签页、API 节点和滚动发布共同生效;Redis 不可用时实时订阅失败关闭,业务必须继续走 HTTP Snapshot。
64
+ - `EventId` 在业务重试时保持稳定;平台先用 Redis 短 Claim 协调跨节点发布,只有真实广播成功后才写 24 小时完成标记,避免“先去重、后崩溃”永久漏发。客户端仍必须按 `EventId` 去重,因为故障恢复可能产生重复通知。
65
+ - `Version` 按同一 `ChannelKey + SubjectId` 单调递增。低版本事件作为过期事件拒绝广播;同版本但内容指纹不同视为版本冲突并拒绝;重放相同事件不推进 latest。
66
+ - 宿主只读取成功 DosResult 中固定大小写的 `DataAppend.RealtimeEvent`,并只广播 `EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt`。`Data` 最大 32KB,只能放该群组所有订阅者都可见的安全投影;个性化私有数据通过按当前用户裁剪的 Snapshot 获取。
67
+ - 客户端按 `EventId` 去重、按 `Version` 检测乱序和缺口;连接失败、续租失败、重连或发现缺口时立即重新拉 HTTP Snapshot,并保留有界轮询兜底。共享存储/状态机才是事实源。
68
+ - 旧 `/game-realtime` 只作兼容。新业务默认使用通用协议,完整契约见官方 `v8-server.md`。
69
69
 
70
70
  ## 1. 匿名调用(IsAnonymous)
71
71
 
@@ -83,59 +83,59 @@ if (V8.Cache.Exists(key)) return { Code: 0, Msg: '请稍后再试' };
83
83
  var code = Math.floor(100000 + Math.random() * 900000).toString();
84
84
  V8.Cache.Set(key, code, 60);
85
85
  // ... 调短信网关 ...
86
- return { Code: 1, Msg: '验证码已发送' };
87
- ```
88
-
89
- ### 1.1 会员端 Token 优先级
90
-
91
- 移动端/会员端自建 Token 与 Microi 后台 JWT 并存时,会员业务接口应明确 Token 优先级。MCP、后台自动化测试、PC 管理端代理调用常会在 `V8.Header.Token` 中带平台 JWT,如果接口要校验会员登录态,推荐优先读取显式会员参数或专用 Header,再回退平台 Header:
92
-
93
- ```javascript
94
- function getMemberToken() {
95
- var p = V8.Param || {};
96
- var h = V8.Header || {};
97
- var token = p.Token || p.token || h.MallMemberToken || h.mallmembertoken || h.Token || h.token || h.Authorization || h.authorization || '';
98
- token = String(token || '').trim();
99
- if (token.indexOf('Bearer ') === 0) token = token.substring(7).trim();
100
- return token;
101
- }
102
- ```
103
-
104
- 不要让后台 JWT 覆盖前端显式传入的会员 Token,否则 MCP/Playwright 用会员账号做自动化测试时会误判为未登录。
105
-
106
- ## 2. 禁止外部调用(StopHttp)
86
+ return { Code: 1, Msg: '验证码已发送' };
87
+ ```
88
+
89
+ ### 1.1 会员端 Token 优先级
90
+
91
+ 移动端/会员端自建 Token 与 Microi 后台 JWT 并存时,会员业务接口应明确 Token 优先级。MCP、后台自动化测试、PC 管理端代理调用常会在 `V8.Header.Token` 中带平台 JWT,如果接口要校验会员登录态,推荐优先读取显式会员参数或专用 Header,再回退平台 Header:
92
+
93
+ ```javascript
94
+ function getMemberToken() {
95
+ var p = V8.Param || {};
96
+ var h = V8.Header || {};
97
+ var token = p.Token || p.token || h.MallMemberToken || h.mallmembertoken || h.Token || h.token || h.Authorization || h.authorization || '';
98
+ token = String(token || '').trim();
99
+ if (token.indexOf('Bearer ') === 0) token = token.substring(7).trim();
100
+ return token;
101
+ }
102
+ ```
103
+
104
+ 不要让后台 JWT 覆盖前端显式传入的会员 Token,否则 MCP/Playwright 用会员账号做自动化测试时会误判为未登录。
105
+
106
+ ## 2. 禁止外部调用(StopHttp)
107
107
 
108
108
  仅供其他接口引擎/V8 事件内部调用,不允许直接 HTTP 请求触发:
109
109
 
110
110
  ```javascript
111
- // 例:核心扣款接口(StopHttp=true)
112
- // 只能从 order_pay、refund 等接口通过 V8.ApiEngine.Run 调用
113
- V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
114
- .AddInParameter("@p0", V8.Param.amount)
115
- .AddInParameter("@p1", V8.Param.accountId)
116
- .ExecuteNonQuery();
117
- return { Code: 1 };
111
+ // 例:核心扣款接口(StopHttp=true)
112
+ // 只能从 order_pay、refund 等接口通过 V8.ApiEngine.Run 调用
113
+ V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
114
+ .AddInParameter("@p0", V8.Param.amount)
115
+ .AddInParameter("@p1", V8.Param.accountId)
116
+ .ExecuteNonQuery();
117
+ return { Code: 1 };
118
118
  ```
119
119
 
120
120
  外部调用直接 `/apiengine/account_deduct` 会被拒绝。
121
121
 
122
122
  ## 3. 分布式锁(LockKey)
123
123
 
124
- 集群部署时可用接口引擎 `LockKey` 减少同一任务的并发执行(如:每月对账、自动补单):
124
+ 集群部署时可用接口引擎 `LockKey` 减少同一任务的并发执行(如:每月对账、自动补单):
125
125
 
126
126
  ```javascript
127
- // 配置:LockKey = month_settlement,LockTimeout = 600
128
- // 平台使用共享锁协调多节点;锁超时、节点暂停和网络分区仍可能触发重试
129
- var month = DateNow('yyyy-MM');
130
- V8.Db.FromSql('INSERT INTO MonthSettle SELECT ... WHERE Month = @p0')
131
- .AddInParameter("@p0", month)
132
- .ExecuteNonQuery();
133
- return { Code: 1 };
127
+ // 配置:LockKey = month_settlement,LockTimeout = 600
128
+ // 平台使用共享锁协调多节点;锁超时、节点暂停和网络分区仍可能触发重试
129
+ var month = DateNow('yyyy-MM');
130
+ V8.Db.FromSql('INSERT INTO MonthSettle SELECT ... WHERE Month = @p0')
131
+ .AddInParameter("@p0", month)
132
+ .ExecuteNonQuery();
133
+ return { Code: 1 };
134
134
  ```
135
135
 
136
- `LockKey` 可包含 `${V8.OsClient}` 实现按租户独立锁。
137
-
138
- 分布式锁不是“业务只执行一次”的最终保证。扣款、库存、积分、流水、对账等副作用还必须使用稳定幂等键、数据库唯一约束/条件更新、状态机或 outbox/inbox;锁 Key 至少包含 `OsClient + 业务唯一标识`,超时必须大于正常执行时间。所需唯一索引必须写入 Manifest `tables[].indexes` 并通过 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读,接口引擎本身禁止执行索引 DDL。
136
+ `LockKey` 可包含 `${V8.OsClient}` 实现按租户独立锁。
137
+
138
+ 分布式锁不是“业务只执行一次”的最终保证。扣款、库存、积分、流水、对账等副作用还必须使用稳定幂等键、数据库唯一约束/条件更新、状态机或 outbox/inbox;锁 Key 至少包含 `OsClient + 业务唯一标识`,超时必须大于正常执行时间。所需唯一索引必须写入 Manifest `tables[].indexes` 并通过 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读,接口引擎本身禁止执行索引 DDL。
139
139
 
140
140
  ## 4. 自定义路径(ApiAddress)
141
141
 
@@ -147,11 +147,11 @@ ApiAddress: /wechat/notify
147
147
 
148
148
  ## 5. 响应文件(IsResponseFile)
149
149
 
150
- 开启后接口可直接输出二进制流:
151
-
152
- 后端会统一处理响应头和文件头校验:图片/PDF 浏览器直接打开,其它文件下载;V8 代码只返回文件三字段,不要在接口里手写复杂的魔数判断。`ContentType` 必须匹配真实字节,金蝶 PLM `KD_C_PLM` 等业务封装流不能伪装成 `application/pdf`。
153
-
154
- 响应文件动态路由必须同时接受 `GET` 和 `HEAD`。OnlyOffice 等服务端预览器可能先用 `HEAD` 探测文件类型、长度和可达性;如果浏览器直接下载正常但 `HEAD` 返回 `405`,在线预览仍可能一直停在“加载文档”。
150
+ 开启后接口可直接输出二进制流:
151
+
152
+ 后端会统一处理响应头和文件头校验:图片/PDF 浏览器直接打开,其它文件下载;V8 代码只返回文件三字段,不要在接口里手写复杂的魔数判断。`ContentType` 必须匹配真实字节,金蝶 PLM `KD_C_PLM` 等业务封装流不能伪装成 `application/pdf`。
153
+
154
+ 响应文件动态路由必须同时接受 `GET` 和 `HEAD`。OnlyOffice 等服务端预览器可能先用 `HEAD` 探测文件类型、长度和可达性;如果浏览器直接下载正常但 `HEAD` 返回 `405`,在线预览仍可能一直停在“加载文档”。
155
155
 
156
156
  ```javascript
157
157
  // 必须返回特定结构
@@ -190,82 +190,95 @@ LogResult = true # 记录每次返回
190
190
 
191
191
  > ❌ 接口返回结果含敏感数据(密码、token、密钥)时不要打开 `LogResult`
192
192
 
193
- ## 8. 保存后 HTTP 复测
194
-
195
- 通过 MCP 维护接口引擎时,先用 `microi_list_engines` 发现现有接口,再用
196
- `microi_get_engine_code` 读取源码;修改后使用 `microi_save_engine_code`
197
- 保存并回读。只有确认目标不存在时才调用创建工具,避免重复
198
- `ApiEngineKey`。`microi_run_engine` 适合做引擎上下文内的最小调试,但不能
199
- 代替下方真实 HTTP 复测。
200
-
201
- `microi_run_engine` 只能证明引擎代码在 MCP/内部执行上下文可运行,不能证明移动端或外部 HTTP 能调用。新建或更新接口后必须再走一次真实 HTTP 路径:
202
-
203
- ```text
204
- POST /apiengine/{ApiEngineKey}
205
- Headers: Content-Type=application/json, osclient={OsClient}, apiengine=1
206
- Body: {"Action":"Bootstrap","OsClient":"{OsClient}"}
207
-
208
- # 兼容旧入口
209
- POST /api/ApiEngine/Run
210
- Headers: Content-Type=application/json, OsClient={OsClient}
211
- Body: {"ApiEngineKey":"your_key","Action":"Bootstrap"}
212
- ```
213
-
214
- 复测重点:
215
-
216
- - `IsEnable=1`、`StopHttp=0`、公开接口 `AllowAnonymous=1`。
217
- - JSON Body 会恢复到 `V8.Param`;同名参数已由 Query/Form 绑定时保持既有值,避免改变旧调用优先级。直接动态路由与兼容入口都要覆盖 JSON Body 测试,不能只用 Query 参数证明可用。
218
- - HTTP 请求中的 `_CurrentUser`、`_InvokeType:'Server'`、`_TrustedServerInvocation` 都不能建立可信服务端身份;当前用户和调用类型必须由认证中间件与接口层重新写入。
219
- - `ApiAddress` 不能为空字符串;空字符串可能导致 404。
220
- - 响应不能是空 body、字符串 `null`、非 JSON;业务接口必须返回标准 DosResult。
221
- - 普通 `POST/PUT/PATCH/DELETE` 必须使用稳定路径 `/apiengine/{ApiEngineKey}`,租户放在唯一的 `osclient` Header,并可在 JSON/Form Body 中冗余传入;禁止无脑给路径追加 `--OsClient--...--`。普通 GET 优先 Header 或 `?OsClient=`。只有微信/支付等第三方回调(包括 POST)、浏览器直接下载等调用方确实无法设置 Header 或 Query 的场景,才使用 `--OsClient--{OsClient}--` 特殊路径;Query 参数名固定为 `OsClient`,禁止 `o` 等缩写。
222
- - 需要 C# 验签/AES 解密或隐藏 SaaS 密钥的回调,使用“最小协议网关 + `Managed` 核心接口 + `CreateIfMissing` 租户 Hook”。网关不得承载日志、写表、通知等业务逻辑;传给 V8 的事件必须脱敏,并包含稳定 `EventId` 供 Hook 幂等。
223
- - 更新接口代码时保留 HTTP 元数据,避免只覆盖 JS 代码却把匿名、启用、自定义地址等配置冲掉。
224
-
225
- ## 请求内异步与可靠后台任务
226
-
227
- 接口默认同步返回。对本次请求必须完成的异步 I/O,调用真实的 `*Async` 方法并 `await`。常用入口包括 `V8.Http.*Async`、`V8.FormEngine.GetTableDataAsync` `V8.ApiEngine.RunAsync`:
228
-
229
- ```javascript
230
- var resp = await V8.Http.GetResponseAsync({
231
- Url: 'https://example.com/health',
232
- Timeout: 5
233
- });
234
- if (resp.StatusCode < 200 || resp.StatusCode >= 300) {
235
- return { Code: 0, Msg: '上游调用失败' };
236
- }
237
-
238
- var users = await V8.FormEngine.GetTableDataAsync('SysUser', {
239
- _Where: [['Status', '=', 1]],
240
- _SelectFields: ['Id', 'Name'],
241
- _PageSize: 20
242
- });
243
-
244
- var summary = await V8.ApiEngine.RunAsync('build-user-summary', {
245
- Users: users.Data
246
- });
247
- return { Code: 1, Data: { Upstream: resp.Content, Summary: summary.Data } };
248
- ```
249
-
250
- 禁止用 `setTimeout` 或 `System.Threading.Tasks.Task.Run` 实现“接口先返回、后台继续执行”:`V8Engine.Run` 返回后会释放 Jint Engine、租户上下文、事务和并发租约,回调不可靠,也没有持久化、重试、幂等或重启恢复保证。
251
-
252
- 需要先响应再处理时,使用接口引擎后台任务按钮(`RunBackground + ApiEngineKey`)、Job、MQ 或 outbox;消费者按全局 `EventId` 幂等处理并持久化进度。AI 发现预计超过 2 分钟、500 条、1000 个扇出子操作、100 次外部调用,或安装/初始化/迁移/备份/全量生成等任务时,必须主动切换为后台任务;预计超过 10 分钟时还必须设计 checkpoint 分片。见 `job-engine`、`v8-menu-buttons`、`v8-mq-mqtt` 和 `microi-system-delivery`。
193
+ ## 8. 保存后 HTTP 复测
194
+
195
+ 通过 MCP 维护接口引擎时,先用 `microi_list_engines` 发现现有接口,再用
196
+ `microi_get_engine_code` 读取源码;修改后使用 `microi_save_engine_code`
197
+ 保存并回读。只有确认目标不存在时才调用创建工具,避免重复
198
+ `ApiEngineKey`。`microi_run_engine` 适合做引擎上下文内的最小调试,但不能
199
+ 代替下方真实 HTTP 复测。
200
+
201
+ `microi_run_engine` 只能证明引擎代码在 MCP/内部执行上下文可运行,不能证明移动端或外部 HTTP 能调用。新建或更新接口后必须再走一次真实 HTTP 路径:
202
+
203
+ ```text
204
+ POST /apiengine/{ApiEngineKey}
205
+ Headers: Content-Type=application/json, osclient={OsClient}, apiengine=1
206
+ Body: {"Action":"Bootstrap","OsClient":"{OsClient}"}
207
+
208
+ # 兼容旧入口
209
+ POST /api/ApiEngine/Run
210
+ Headers: Content-Type=application/json, OsClient={OsClient}
211
+ Body: {"ApiEngineKey":"your_key","Action":"Bootstrap"}
212
+ ```
213
+
214
+ 复测重点:
215
+
216
+ - `IsEnable=1`、`StopHttp=0`、公开接口 `AllowAnonymous=1`。
217
+ - JSON Body 会恢复到 `V8.Param`;同名参数已由 Query/Form 绑定时保持既有值,避免改变旧调用优先级。直接动态路由与兼容入口都要覆盖 JSON Body 测试,不能只用 Query 参数证明可用。
218
+ - HTTP 请求中的 `_CurrentUser`、`_InvokeType:'Server'`、`_TrustedServerInvocation` 都不能建立可信服务端身份;当前用户和调用类型必须由认证中间件与接口层重新写入。
219
+ - `ApiAddress` 不能为空字符串;空字符串可能导致 404。
220
+ - 响应不能是空 body、字符串 `null`、非 JSON;业务接口必须返回标准 DosResult。
221
+ - 普通 `POST/PUT/PATCH/DELETE` 必须使用稳定路径 `/apiengine/{ApiEngineKey}`,租户放在唯一的 `osclient` Header,并可在 JSON/Form Body 中冗余传入;禁止无脑给路径追加 `--OsClient--...--`。普通 GET 优先 Header 或 `?OsClient=`。只有微信/支付等第三方回调(包括 POST)、浏览器直接下载等调用方确实无法设置 Header 或 Query 的场景,才使用 `--OsClient--{OsClient}--` 特殊路径;Query 参数名固定为 `OsClient`,禁止 `o` 等缩写。
222
+ - 需要 C# 验签/AES 解密或隐藏 SaaS 密钥的回调,使用“最小协议网关 + `Managed` 核心接口 + `CreateIfMissing` 租户 Hook”。网关不得承载日志、写表、通知等业务逻辑;传给 V8 的事件必须脱敏,并包含稳定 `EventId` 供 Hook 幂等。
223
+ - 更新接口代码时保留 HTTP 元数据,避免只覆盖 JS 代码却把匿名、启用、自定义地址等配置冲掉。
224
+
225
+ ### 路由冷缓存与客户端直达头(强制)
226
+
227
+ - `/apiengine/{ApiEngineKey}` 客户端请求必须携带 `apiengine: 1`;标准前端 SDK 应从稳定路径自动识别并补齐,不能要求每个业务页面手写 Header。自定义 `ApiAddress` 无法从路径识别时,调用方显式设置 `apiEngine: true`。
228
+ - 动态路由缓存未命中后允许从 `sys_apiengine` 权威回源并重建 `ApiEngineKey`、`ApiAddress` 两个别名。回源对象如果来自 `dynamic`,先转换为 `JObject`/`object`,并把字段显式赋给 `string`、`bool` 等强类型局部变量,再调用普通方法或扩展方法;禁止让 `dynamic` 调用链延续到 `DosIsNullOrWhiteSpace`、LINQ 或 JToken 扩展。
229
+ - 自动化必须覆盖“缓存预热命中”和“缓存为空首次请求”两条路径;首次请求不得 404,且回源后两个缓存别名均可再次命中。滚动发布、节点重启或缓存清理后要重复执行无 Header 与带 `apiengine: 1` 的真实 HTTP smoke test。
230
+
231
+ ### 复盘:接口引擎冷缓存回源异常被吞成空 404
232
+
233
+ - 触发场景:接口配置、启用和匿名设置都正确,接口昨天可用;节点重启或缓存缺失后,小程序首次请求 `/apiengine/{key}` 返回空 body 404。
234
+ - 根因:动态路由从数据库回源成功后,局部变量仍沿着 `dynamic` 调用链传播;运行时对实际 `string` 绑定扩展方法失败,外层异常处理返回原路由值,最终由 ASP.NET Core 表现为无正文 404。
235
+ - 通用规则:数据库/缓存的动态对象在进入路由、鉴权、缓存键和 LINQ 逻辑前必须强类型落地;稳定接口引擎路径由 SDK 自动携带 `apiengine: 1` 作为直达兜底。
236
+ - 自动化检查:单元测试直接传入 `JObject` 验证回源别名强类型归一化;前端传输测试断言 `/apiengine/*` 自动携带 `apiengine: 1`,普通 `/api/*` 不误带;本地启动后清空专用测试别名并验证首次 HTTP 请求成功及缓存重建。
237
+
238
+ ## 请求内异步与可靠后台任务
239
+
240
+ 接口默认同步返回。对本次请求必须完成的异步 I/O,调用真实的 `*Async` 方法并 `await`。常用入口包括 `V8.Http.*Async`、`V8.FormEngine.GetTableDataAsync` 和 `V8.ApiEngine.RunAsync`:
241
+
242
+ ```javascript
243
+ var resp = await V8.Http.GetResponseAsync({
244
+ Url: 'https://example.com/health',
245
+ Timeout: 5
246
+ });
247
+ if (resp.StatusCode < 200 || resp.StatusCode >= 300) {
248
+ return { Code: 0, Msg: '上游调用失败' };
249
+ }
250
+
251
+ var users = await V8.FormEngine.GetTableDataAsync('SysUser', {
252
+ _Where: [['Status', '=', 1]],
253
+ _SelectFields: ['Id', 'Name'],
254
+ _PageSize: 20
255
+ });
256
+
257
+ var summary = await V8.ApiEngine.RunAsync('build-user-summary', {
258
+ Users: users.Data
259
+ });
260
+ return { Code: 1, Data: { Upstream: resp.Content, Summary: summary.Data } };
261
+ ```
262
+
263
+ 禁止用 `setTimeout` 或 `System.Threading.Tasks.Task.Run` 实现“接口先返回、后台继续执行”:`V8Engine.Run` 返回后会释放 Jint Engine、租户上下文、事务和并发租约,回调不可靠,也没有持久化、重试、幂等或重启恢复保证。
264
+
265
+ 需要先响应再处理时,使用接口引擎后台任务按钮(`RunBackground + ApiEngineKey`)、Job、MQ 或 outbox;消费者按全局 `EventId` 幂等处理并持久化进度。AI 发现预计超过 2 分钟、500 条、1000 个扇出子操作、100 次外部调用,或安装/初始化/迁移/备份/全量生成等任务时,必须主动切换为后台任务;预计超过 10 分钟时还必须设计 checkpoint 分片。见 `job-engine`、`v8-menu-buttons`、`v8-mq-mqtt` 和 `microi-system-delivery`。
253
266
 
254
267
  ## 接口安全检查清单
255
268
 
256
269
  - [ ] 公开接口是否仅开启 `IsAnonymous`,敏感接口是否关闭?
257
270
  - [ ] 内部接口是否开启 `StopHttp`?
258
- - [ ] 写操作(扣款、对账、补单)是否配置 `LockKey`?
259
- - [ ] 锁之外是否还有幂等键、唯一约束/条件更新或状态机?
271
+ - [ ] 写操作(扣款、对账、补单)是否配置 `LockKey`?
272
+ - [ ] 锁之外是否还有幂等键、唯一约束/条件更新或状态机?
260
273
  - [ ] 频率敏感接口是否配置 `RateLimit`?
261
274
  - [ ] 审计需求接口是否开启 `LogParam`?
262
275
  - [ ] 文件响应接口是否开启 `IsResponseFile`?
263
- - [ ] 接口代码内是否仍校验 `V8.CurrentUser`(`IsAnonymous=true` 时尤其重要)?
264
- - [ ] 是否没有使用 `setTimeout` / `Task.Run` 承担请求外后台任务?
265
- - [ ] 大任务是否按阈值主动使用后台任务,超过 10 分钟是否有 `HasMore + Checkpoint`?
266
- - [ ] 是否区分累计分配、调用树预算、JS递归与接口嵌套,而不是盲目抬高全部限制?
267
- - [ ] 是否确认 `V8Limit=false` 表示接口不限 Jint 单次预算、`true` 才启用限制,并避免继续写入旧 `V8Unlimited` 字段?
268
- - [ ] 保存后是否通过稳定路径 `/apiengine/{key}` + `osclient` Header 做过 HTTP 复测?特殊 GET/HEAD 路径是否仅用于无法设置 Header/Form/Query 的场景?
276
+ - [ ] 接口代码内是否仍校验 `V8.CurrentUser`(`IsAnonymous=true` 时尤其重要)?
277
+ - [ ] 是否没有使用 `setTimeout` / `Task.Run` 承担请求外后台任务?
278
+ - [ ] 大任务是否按阈值主动使用后台任务,超过 10 分钟是否有 `HasMore + Checkpoint`?
279
+ - [ ] 是否区分累计分配、调用树预算、JS递归与接口嵌套,而不是盲目抬高全部限制?
280
+ - [ ] 是否确认 `V8Limit=false` 表示接口不限 Jint 单次预算、`true` 才启用限制,并避免继续写入旧 `V8Unlimited` 字段?
281
+ - [ ] 保存后是否通过稳定路径 `/apiengine/{key}` + `osclient` Header 做过 HTTP 复测?特殊 GET/HEAD 路径是否仅用于无法设置 Header/Form/Query 的场景?
269
282
 
270
283
  ## 常见错误
271
284