@microi.net/cli 4.6.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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/mcp-codex-stdio-adapter.js +189 -0
  4. package/dist/mcp-server.js +972 -0
  5. package/dist/mcp-trae-windows-launcher.cmd +21 -0
  6. package/dist/microi-cli-mcp.js +7 -0
  7. package/dist/microi-cli.js +1645 -0
  8. package/dist/microi-skills.meta.json +335 -0
  9. package/dist/microi.skills/.microi-skills-version.json +6 -0
  10. package/dist/microi.skills/README.md +276 -0
  11. package/dist/microi.skills/ai-engine/SKILL.md +140 -0
  12. package/dist/microi.skills/ai-engine/agents/openai.yaml +4 -0
  13. package/dist/microi.skills/app-store/SKILL.md +105 -0
  14. package/dist/microi.skills/app-store/agents/openai.yaml +4 -0
  15. package/dist/microi.skills/business-blueprint/SKILL.md +184 -0
  16. package/dist/microi.skills/datasource-engine/SKILL.md +89 -0
  17. package/dist/microi.skills/datasource-engine/agents/openai.yaml +4 -0
  18. package/dist/microi.skills/dos-orm/SKILL.md +76 -0
  19. package/dist/microi.skills/dos-orm/references/api-reference.md +229 -0
  20. package/dist/microi.skills/job-engine/SKILL.md +141 -0
  21. package/dist/microi.skills/job-engine/agents/openai.yaml +4 -0
  22. package/dist/microi.skills/message-notification/SKILL.md +113 -0
  23. package/dist/microi.skills/message-notification/agents/openai.yaml +6 -0
  24. package/dist/microi.skills/message-notification/references/contracts.md +99 -0
  25. package/dist/microi.skills/microi-ai-app-auth.js +651 -0
  26. package/dist/microi.skills/microi-ai-application/SKILL.md +80 -0
  27. package/dist/microi.skills/microi-ai-application/agents/openai.yaml +4 -0
  28. package/dist/microi.skills/microi-ai-application/references/frontend-baseline.md +164 -0
  29. package/dist/microi.skills/microi-client-frontend/SKILL.md +562 -0
  30. package/dist/microi.skills/microi-datasource-mapping/SKILL.md +108 -0
  31. package/dist/microi.skills/microi-db-schema/SKILL.md +170 -0
  32. package/dist/microi.skills/microi-db-schema/agents/openai.yaml +4 -0
  33. package/dist/microi.skills/microi-db-schema/references/core-tables.md +695 -0
  34. package/dist/microi.skills/microi-db-schema/references/form-component-options.md +256 -0
  35. package/dist/microi.skills/microi-db-schema/references/schema-overview.md +203 -0
  36. package/dist/microi.skills/microi-db-schema/references/schema.md +647 -0
  37. package/dist/microi.skills/microi-db-schema/references/table-catalog.md +1607 -0
  38. package/dist/microi.skills/microi-deployment/SKILL.md +117 -0
  39. package/dist/microi.skills/microi-deployment/references/deployment-matrix.md +94 -0
  40. package/dist/microi.skills/microi-docs-coverage/SKILL.md +91 -0
  41. package/dist/microi.skills/microi-docs-coverage/references/capability-map.md +65 -0
  42. package/dist/microi.skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +887 -0
  43. package/dist/microi.skills/microi-form-engine/SKILL.md +159 -0
  44. package/dist/microi.skills/microi-form-engine/references/component-catalog.md +116 -0
  45. package/dist/microi.skills/microi-form-engine/references/data-source-events.md +117 -0
  46. package/dist/microi.skills/microi-form-layout/SKILL.md +373 -0
  47. package/dist/microi.skills/microi-frontend-sdk/SKILL.md +304 -0
  48. package/dist/microi.skills/microi-left-right-layout/SKILL.md +132 -0
  49. package/dist/microi.skills/microi-microservice/SKILL.md +115 -0
  50. package/dist/microi.skills/microi-microservice/references/runtime-delivery.md +145 -0
  51. package/dist/microi.skills/microi-mobile-app-quality/SKILL.md +436 -0
  52. package/dist/microi.skills/microi-solution-quotation/SKILL.md +76 -0
  53. package/dist/microi.skills/microi-solution-quotation/agents/openai.yaml +4 -0
  54. package/dist/microi.skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -0
  55. package/dist/microi.skills/microi-system-delivery/SKILL.md +446 -0
  56. package/dist/microi.skills/microi-ui/SKILL.md +321 -0
  57. package/dist/microi.skills/microi-uniapp-frontend/SKILL.md +483 -0
  58. package/dist/microi.skills/microi.v8.js +1758 -0
  59. package/dist/microi.skills/module-engine/SKILL.md +131 -0
  60. package/dist/microi.skills/module-engine/references/module-config.md +174 -0
  61. package/dist/microi.skills/page-engine/SKILL.md +397 -0
  62. package/dist/microi.skills/performance-testing/SKILL.md +207 -0
  63. package/dist/microi.skills/playwright-e2e/SKILL.md +769 -0
  64. package/dist/microi.skills/print-engine/SKILL.md +237 -0
  65. package/dist/microi.skills/production-readonly-audit/SKILL.md +39 -0
  66. package/dist/microi.skills/report-engine/SKILL.md +69 -0
  67. package/dist/microi.skills/report-engine/agents/openai.yaml +4 -0
  68. package/dist/microi.skills/search-engine/SKILL.md +73 -0
  69. package/dist/microi.skills/search-engine/agents/openai.yaml +4 -0
  70. package/dist/microi.skills/spider-engine/SKILL.md +188 -0
  71. package/dist/microi.skills/translate-engine/SKILL.md +91 -0
  72. package/dist/microi.skills/translate-engine/agents/openai.yaml +4 -0
  73. package/dist/microi.skills/ui-design/SKILL.md +1575 -0
  74. package/dist/microi.skills/ui-design/assets/pattern-showcase/app.js +54 -0
  75. package/dist/microi.skills/ui-design/assets/pattern-showcase/index.html +163 -0
  76. package/dist/microi.skills/ui-design/assets/pattern-showcase/styles.css +311 -0
  77. package/dist/microi.skills/ui-design/assets/templates/MCI-DESIGN.md +98 -0
  78. package/dist/microi.skills/ui-design/references/design-pattern-library.md +171 -0
  79. package/dist/microi.skills/ui-design/references/mci-design-contract.md +84 -0
  80. package/dist/microi.skills/ui-design/references/motion-and-media.md +71 -0
  81. package/dist/microi.skills/ui-design/references/product-flow-recipes.md +94 -0
  82. package/dist/microi.skills/uniapp-mall-assets/SKILL.md +105 -0
  83. package/dist/microi.skills/v8-api-config/SKILL.md +272 -0
  84. package/dist/microi.skills/v8-cache-pattern/SKILL.md +286 -0
  85. package/dist/microi.skills/v8-crud-api/SKILL.md +398 -0
  86. package/dist/microi.skills/v8-debugging/SKILL.md +279 -0
  87. package/dist/microi.skills/v8-explorer-tree/SKILL.md +224 -0
  88. package/dist/microi.skills/v8-export-import/SKILL.md +590 -0
  89. package/dist/microi.skills/v8-file-upload/SKILL.md +497 -0
  90. package/dist/microi.skills/v8-formengine-http/SKILL.md +218 -0
  91. package/dist/microi.skills/v8-frontend-events/SKILL.md +349 -0
  92. package/dist/microi.skills/v8-frontend-events/references/bluetooth-print-api.md +107 -0
  93. package/dist/microi.skills/v8-frontend-events/references/bluetooth-print.md +185 -0
  94. package/dist/microi.skills/v8-http-integration/SKILL.md +379 -0
  95. package/dist/microi.skills/v8-image-processing/SKILL.md +187 -0
  96. package/dist/microi.skills/v8-image-processing/agents/openai.yaml +4 -0
  97. package/dist/microi.skills/v8-image-processing/references/api-reference.md +620 -0
  98. package/dist/microi.skills/v8-menu-buttons/SKILL.md +661 -0
  99. package/dist/microi.skills/v8-mongodb/SKILL.md +149 -0
  100. package/dist/microi.skills/v8-mq-mqtt/SKILL.md +227 -0
  101. package/dist/microi.skills/v8-saas-multi-tenant/SKILL.md +193 -0
  102. package/dist/microi.skills/v8-security/SKILL.md +417 -0
  103. package/dist/microi.skills/v8-sql-query/SKILL.md +290 -0
  104. package/dist/microi.skills/v8-table-event/SKILL.md +385 -0
  105. package/dist/microi.skills/v8-template-engine/SKILL.md +165 -0
  106. package/dist/microi.skills/v8-utilities/SKILL.md +79 -0
  107. package/dist/microi.skills/v8-utilities/references/client-api-index.md +136 -0
  108. package/dist/microi.skills/v8-utilities/references/platform-http-routes.md +80 -0
  109. package/dist/microi.skills/v8-utilities/references/server-api-index.md +129 -0
  110. package/dist/microi.skills/v8-workflow/SKILL.md +322 -0
  111. package/dist/microi.skills/workspace-conventions/SKILL.md +479 -0
  112. package/package.json +40 -0
@@ -0,0 +1,272 @@
1
+ ---
2
+ name: v8-api-config
3
+ description: Microi V8 接口引擎配置指南。用于设置 ApiEngineKey、ApiAddress、StopHttp、AllowAnonymous、ResponseFile、锁、日志、超时和 HTTP 暴露。
4
+ ---
5
+
6
+ # Microi V8 接口引擎配置
7
+
8
+ 你正在配置 Microi 吾码平台的接口引擎(API 引擎)。除了 JS 代码本身,每个接口还有一系列**安全/性能配置项**,写代码时必须了解这些选项以决定是否需要调整。
9
+
10
+ ## 配置项总览
11
+
12
+ | 字段 | 说明 | 默认 |
13
+ |------|------|------|
14
+ | `ApiEngineKey` | 接口唯一标识(URL 路径) | 必填 |
15
+ | `ApiAddress` | 自定义接口地址(覆盖默认 `/apiengine/{Key}`) | 空 |
16
+ | `RequestType` | `Get` / `Post` / `Both` | `Both` |
17
+ | `ParamType` | `form` / `json` / `url` —— 但 V8.Param 都能统一接收 | `Both` |
18
+ | `IsAnonymous` | 允许匿名调用(无 Token) | `false` |
19
+ | `StopHttp` | 禁止外部 HTTP 调用(仅允许 V8.ApiEngine.Run 内部调用) | `false` |
20
+ | `IsResponseFile` | 是否响应文件(开启后 Data 必须是文件结构) | `false` |
21
+ | `LockKey` | 分布式锁 Key(同一时刻全集群只能执行一次) | 空 |
22
+ | `LockTimeout` | 锁超时秒数 | `30` |
23
+ | `LockMsg` | 加锁失败时返回提示 | `操作过于频繁` |
24
+ | `RateLimit` | 频率限制(如 `60/m` 每分钟60次) | 空 |
25
+ | `LogParam` | 是否记录请求参数到 `sys_log` | `false` |
26
+ | `LogResult` | 是否记录返回值到 `sys_log` | `false` |
27
+
28
+ ### 资源预算与嵌套调用(强制理解)
29
+
30
+ - `LimitMemory` 是单个 Jint 引擎的**累计托管分配预算**,不是实时堆占用或服务器预留内存。默认 2048MB、节点硬上限默认 8192MB。
31
+ - `V8.ApiEngine.Run` 多层嵌套是正常能力。新版默认隔离父子引擎的单层分配计数,子层不会再被每个父层重复计费;根调用树另有默认 8192MB 总预算。
32
+ - 接口嵌套深度默认 32、节点硬上限默认 64;它与 `LimitRecursion` 的 JavaScript 函数递归不是同一限制。
33
+ - 嵌套调用不重复占用全局/租户并发名额,同一调用树重入同 Key 也不会自锁;不同子接口 Key 仍受自己的 Key 并发门保护。
34
+ - `V8.Limits` 可读取本片有效预算和当前深度。异常优先检查 `DataAppend.V8Limit.Code`,不要看到“2GB”就判断服务器真实吃满 2GB。
35
+ - 后台任务使用同一执行引擎。总任务可以运行数小时,但单片仍受 `Timeout/MaxStatements/LimitMemory` 约束;超过 10 分钟必须返回 `HasMore + Checkpoint` 分片续跑,不能只把 `Timeout` 调到 1800/3600。
36
+ - 只有业务明确要求整条链在一个共享数据库事务中原子提交/回滚、且分片会改变业务语义时,才允许开启接口引擎的 `V8Unlimited`。它仅解除当前 Jint Engine 的超时、语句、函数递归、累计分配和 Promise 固定等待限制;常驻内存保护、取消令牌、并发、接口嵌套深度、权限沙箱及数据库限制仍保留。下游接口和表后端事件不继承,需分别评估并开启。
37
+
38
+ ### 通用实时事件(SignalR)
39
+
40
+ 订单、协作、设备、审批或多人房间需要实时刷新时,业务写命令仍由接口引擎执行并提交事务;成功结果通过 `DataAppend.RealtimeEvent` 声明提交后事件。新业务统一使用通用 v2 Hub `/api-engine-realtime`,不要再新建业务专用 Hub 或把权威状态放进 C# 进程内字典。
41
+
42
+ ```javascript
43
+ return {
44
+ Code: 1,
45
+ Data: snapshot,
46
+ DataAppend: { RealtimeEvent: {
47
+ EventId: requestId,
48
+ ChannelKey: 'order_updates',
49
+ SubjectId: order.Id,
50
+ Version: order.VersionNo,
51
+ EventType: 'StatusChanged',
52
+ Data: { Status: order.Status }
53
+ } }
54
+ };
55
+ ```
56
+
57
+ - Hub 方法固定为 `SubscribeChannel({ ChannelKey, SubjectId })` 与 `UnsubscribeChannel(...)`,客户端事件固定为 `RealtimeEvent`。订阅成功会返回 `ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt`。
58
+ - 连接只接受当前有效的普通登录 Token。现有 AccessKey 权限模型没有 `realtime:subscribe` scope,平台会直接拒绝;在平台正式增加并校验该 scope 前,不得用 AccessKey 建立实时订阅。
59
+ - 对应订阅授权接口固定为 `realtime_{channel_key}_authorize`。它必须用 `V8.CurrentUser` 校验资源权限,并精确回显 `Authorized/ChannelKey/SubjectId/Version`;不能信任客户端传入的 UserId、OsClient 或 ApiEngineKey。
60
+ - 订阅使用 30 秒时隙租约。客户端必须按服务端返回的 `RenewAfterMilliseconds` 再次调用同一个 `SubscribeChannel` 续租;每次续租都会重新验证登录 Token、经过共享 Redis 限流,并重新执行授权接口引擎。不要把一次订阅误当成连接全生命周期永久授权。
61
+ - 当前共享 Redis 限流按 `OsClient + UserId` 聚合为 10 秒最多 96 次订阅授权,跨标签页、API 节点和滚动发布共同生效;Redis 不可用时实时订阅失败关闭,业务必须继续走 HTTP Snapshot。
62
+ - `EventId` 在业务重试时保持稳定;平台先用 Redis 短 Claim 协调跨节点发布,只有真实广播成功后才写 24 小时完成标记,避免“先去重、后崩溃”永久漏发。客户端仍必须按 `EventId` 去重,因为故障恢复可能产生重复通知。
63
+ - `Version` 按同一 `ChannelKey + SubjectId` 单调递增。低版本事件作为过期事件拒绝广播;同版本但内容指纹不同视为版本冲突并拒绝;重放相同事件不推进 latest。
64
+ - 宿主只读取成功 DosResult 中固定大小写的 `DataAppend.RealtimeEvent`,并只广播 `EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt`。`Data` 最大 32KB,只能放该群组所有订阅者都可见的安全投影;个性化私有数据通过按当前用户裁剪的 Snapshot 获取。
65
+ - 客户端按 `EventId` 去重、按 `Version` 检测乱序和缺口;连接失败、续租失败、重连或发现缺口时立即重新拉 HTTP Snapshot,并保留有界轮询兜底。共享存储/状态机才是事实源。
66
+ - 旧 `/game-realtime` 只作兼容。新业务默认使用通用协议,完整契约见官方 `v8-server.md`。
67
+
68
+ ## 1. 匿名调用(IsAnonymous)
69
+
70
+ 公开接口(登录、注册、忘记密码、验证码、扫码登录、第三方回调)必须开启:
71
+
72
+ ```javascript
73
+ // 例:发送验证码(匿名)
74
+ if (!V8.Param.phone) return { Code: 0, Msg: '手机号不能为空' };
75
+ if (!/^1[3-9]\d{9}$/.test(V8.Param.phone)) return { Code: 0, Msg: '手机号格式错误' };
76
+
77
+ // 防刷:1分钟同一手机号最多1次
78
+ var key = 'Microi:' + V8.OsClient + ':SmsCode:' + V8.Param.phone;
79
+ if (V8.Cache.Exists(key)) return { Code: 0, Msg: '请稍后再试' };
80
+
81
+ var code = Math.floor(100000 + Math.random() * 900000).toString();
82
+ V8.Cache.Set(key, code, 60);
83
+ // ... 调短信网关 ...
84
+ return { Code: 1, Msg: '验证码已发送' };
85
+ ```
86
+
87
+ ### 1.1 会员端 Token 优先级
88
+
89
+ 移动端/会员端自建 Token 与 Microi 后台 JWT 并存时,会员业务接口应明确 Token 优先级。MCP、后台自动化测试、PC 管理端代理调用常会在 `V8.Header.Token` 中带平台 JWT,如果接口要校验会员登录态,推荐优先读取显式会员参数或专用 Header,再回退平台 Header:
90
+
91
+ ```javascript
92
+ function getMemberToken() {
93
+ var p = V8.Param || {};
94
+ var h = V8.Header || {};
95
+ var token = p.Token || p.token || h.MallMemberToken || h.mallmembertoken || h.Token || h.token || h.Authorization || h.authorization || '';
96
+ token = String(token || '').trim();
97
+ if (token.indexOf('Bearer ') === 0) token = token.substring(7).trim();
98
+ return token;
99
+ }
100
+ ```
101
+
102
+ 不要让后台 JWT 覆盖前端显式传入的会员 Token,否则 MCP/Playwright 用会员账号做自动化测试时会误判为未登录。
103
+
104
+ ## 2. 禁止外部调用(StopHttp)
105
+
106
+ 仅供其他接口引擎/V8 事件内部调用,不允许直接 HTTP 请求触发:
107
+
108
+ ```javascript
109
+ // 例:核心扣款接口(StopHttp=true)
110
+ // 只能从 order_pay、refund 等接口通过 V8.ApiEngine.Run 调用
111
+ V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
112
+ .AddInParameter("@p0", V8.Param.amount)
113
+ .AddInParameter("@p1", V8.Param.accountId)
114
+ .ExecuteNonQuery();
115
+ return { Code: 1 };
116
+ ```
117
+
118
+ 外部调用直接 `/apiengine/account_deduct` 会被拒绝。
119
+
120
+ ## 3. 分布式锁(LockKey)
121
+
122
+ 集群部署时可用接口引擎 `LockKey` 减少同一任务的并发执行(如:每月对账、自动补单):
123
+
124
+ ```javascript
125
+ // 配置:LockKey = month_settlement,LockTimeout = 600
126
+ // 平台使用共享锁协调多节点;锁超时、节点暂停和网络分区仍可能触发重试
127
+ var month = DateNow('yyyy-MM');
128
+ V8.Db.FromSql('INSERT INTO MonthSettle SELECT ... WHERE Month = @p0')
129
+ .AddInParameter("@p0", month)
130
+ .ExecuteNonQuery();
131
+ return { Code: 1 };
132
+ ```
133
+
134
+ `LockKey` 可包含 `${V8.OsClient}` 实现按租户独立锁。
135
+
136
+ 分布式锁不是“业务只执行一次”的最终保证。扣款、库存、积分、流水、对账等副作用还必须使用稳定幂等键、数据库唯一约束/条件更新、状态机或 outbox/inbox;锁 Key 至少包含 `OsClient + 业务唯一标识`,超时必须大于正常执行时间。所需唯一索引必须写入 Manifest `tables[].indexes` 并通过 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读,接口引擎本身禁止执行索引 DDL。
137
+
138
+ ## 4. 自定义路径(ApiAddress)
139
+
140
+ 让接口暴露为 `/wechat/notify` 而非 `/apiengine/wechat_notify`,对接第三方时常用:
141
+
142
+ ```
143
+ ApiAddress: /wechat/notify
144
+ ```
145
+
146
+ ## 5. 响应文件(IsResponseFile)
147
+
148
+ 开启后接口可直接输出二进制流:
149
+
150
+ 后端会统一处理响应头和文件头校验:图片/PDF 浏览器直接打开,其它文件下载;V8 代码只返回文件三字段,不要在接口里手写复杂的魔数判断。`ContentType` 必须匹配真实字节,金蝶 PLM `KD_C_PLM` 等业务封装流不能伪装成 `application/pdf`。
151
+
152
+ 响应文件动态路由必须同时接受 `GET` 和 `HEAD`。OnlyOffice 等服务端预览器可能先用 `HEAD` 探测文件类型、长度和可达性;如果浏览器直接下载正常但 `HEAD` 返回 `405`,在线预览仍可能一直停在“加载文档”。
153
+
154
+ ```javascript
155
+ // 必须返回特定结构
156
+ return {
157
+ Code: 1,
158
+ Data: {
159
+ FileName: 'report.xlsx',
160
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
161
+ FileByteBase64: System.Convert.ToBase64String(byteArr)
162
+ }
163
+ };
164
+ ```
165
+
166
+ 详见 `v8-file-upload/SKILL.md`。
167
+
168
+ ## 6. 频率限制(RateLimit)
169
+
170
+ 防爬虫、防刷:
171
+
172
+ | 配置 | 含义 |
173
+ |------|------|
174
+ | `60/m` | 每分钟 60 次 |
175
+ | `1000/h` | 每小时 1000 次 |
176
+ | `100/s` | 每秒 100 次 |
177
+
178
+ 按客户端 IP + 接口 维度限流。
179
+
180
+ ## 7. 日志记录(LogParam / LogResult)
181
+
182
+ 支付、撤销等敏感接口建议开启,自动记到 `sys_log` 用于审计回溯:
183
+
184
+ ```
185
+ LogParam = true # 记录每次入参
186
+ LogResult = true # 记录每次返回
187
+ ```
188
+
189
+ > ❌ 接口返回结果含敏感数据(密码、token、密钥)时不要打开 `LogResult`
190
+
191
+ ## 8. 保存后 HTTP 复测
192
+
193
+ 通过 MCP 维护接口引擎时,先用 `microi_list_engines` 发现现有接口,再用
194
+ `microi_get_engine_code` 读取源码;修改后使用 `microi_save_engine_code`
195
+ 保存并回读。只有确认目标不存在时才调用创建工具,避免重复
196
+ `ApiEngineKey`。`microi_run_engine` 适合做引擎上下文内的最小调试,但不能
197
+ 代替下方真实 HTTP 复测。
198
+
199
+ `microi_run_engine` 只能证明引擎代码在 MCP/内部执行上下文可运行,不能证明移动端或外部 HTTP 能调用。新建或更新接口后必须再走一次真实 HTTP 路径:
200
+
201
+ ```text
202
+ POST /apiengine/{ApiEngineKey}
203
+ Headers: Content-Type=application/json, osclient={OsClient}, apiengine=1
204
+ Body: {"Action":"Bootstrap","OsClient":"{OsClient}"}
205
+
206
+ # 兼容旧入口
207
+ POST /api/ApiEngine/Run
208
+ Headers: Content-Type=application/json, OsClient={OsClient}
209
+ Body: {"ApiEngineKey":"your_key","Action":"Bootstrap"}
210
+ ```
211
+
212
+ 复测重点:
213
+
214
+ - `IsEnable=1`、`StopHttp=0`、公开接口 `AllowAnonymous=1`。
215
+ - JSON Body 会恢复到 `V8.Param`;同名参数已由 Query/Form 绑定时保持既有值,避免改变旧调用优先级。直接动态路由与兼容入口都要覆盖 JSON Body 测试,不能只用 Query 参数证明可用。
216
+ - HTTP 请求中的 `_CurrentUser`、`_InvokeType:'Server'`、`_TrustedServerInvocation` 都不能建立可信服务端身份;当前用户和调用类型必须由认证中间件与接口层重新写入。
217
+ - `ApiAddress` 不能为空字符串;空字符串可能导致 404。
218
+ - 响应不能是空 body、字符串 `null`、非 JSON;业务接口必须返回标准 DosResult。
219
+ - 普通 `POST/PUT/PATCH/DELETE` 必须使用稳定路径 `/apiengine/{ApiEngineKey}`,租户放在唯一的 `osclient` Header,并可在 JSON/Form Body 中冗余传入;禁止无脑给路径追加 `--OsClient--...--`。普通 GET 优先 Header 或 `?OsClient=`。只有第三方回调、浏览器直接下载等确实无法设置 Header/Form/Query 的 GET/HEAD 场景,才使用 `--OsClient--{OsClient}--` 特殊路径。
220
+ - 更新接口代码时保留 HTTP 元数据,避免只覆盖 JS 代码却把匿名、启用、自定义地址等配置冲掉。
221
+
222
+ ## 请求内异步与可靠后台任务
223
+
224
+ 接口默认同步返回。对本次请求必须完成的异步 I/O,调用真实的 `*Async` 方法并 `await`。常用入口包括 `V8.Http.*Async`、`V8.FormEngine.GetTableDataAsync` 和 `V8.ApiEngine.RunAsync`:
225
+
226
+ ```javascript
227
+ var resp = await V8.Http.GetResponseAsync({
228
+ Url: 'https://example.com/health',
229
+ Timeout: 5
230
+ });
231
+ if (resp.StatusCode < 200 || resp.StatusCode >= 300) {
232
+ return { Code: 0, Msg: '上游调用失败' };
233
+ }
234
+
235
+ var users = await V8.FormEngine.GetTableDataAsync('SysUser', {
236
+ _Where: [['Status', '=', 1]],
237
+ _SelectFields: ['Id', 'Name'],
238
+ _PageSize: 20
239
+ });
240
+
241
+ var summary = await V8.ApiEngine.RunAsync('build-user-summary', {
242
+ Users: users.Data
243
+ });
244
+ return { Code: 1, Data: { Upstream: resp.Content, Summary: summary.Data } };
245
+ ```
246
+
247
+ 禁止用 `setTimeout` 或 `System.Threading.Tasks.Task.Run` 实现“接口先返回、后台继续执行”:`V8Engine.Run` 返回后会释放 Jint Engine、租户上下文、事务和并发租约,回调不可靠,也没有持久化、重试、幂等或重启恢复保证。
248
+
249
+ 需要先响应再处理时,使用接口引擎后台任务按钮(`RunBackground + ApiEngineKey`)、Job、MQ 或 outbox;消费者按全局 `EventId` 幂等处理并持久化进度。AI 发现预计超过 2 分钟、500 条、1000 个扇出子操作、100 次外部调用,或安装/初始化/迁移/备份/全量生成等任务时,必须主动切换为后台任务;预计超过 10 分钟时还必须设计 checkpoint 分片。见 `job-engine`、`v8-menu-buttons`、`v8-mq-mqtt` 和 `microi-system-delivery`。
250
+
251
+ ## 接口安全检查清单
252
+
253
+ - [ ] 公开接口是否仅开启 `IsAnonymous`,敏感接口是否关闭?
254
+ - [ ] 内部接口是否开启 `StopHttp`?
255
+ - [ ] 写操作(扣款、对账、补单)是否配置 `LockKey`?
256
+ - [ ] 锁之外是否还有幂等键、唯一约束/条件更新或状态机?
257
+ - [ ] 频率敏感接口是否配置 `RateLimit`?
258
+ - [ ] 审计需求接口是否开启 `LogParam`?
259
+ - [ ] 文件响应接口是否开启 `IsResponseFile`?
260
+ - [ ] 接口代码内是否仍校验 `V8.CurrentUser`(`IsAnonymous=true` 时尤其重要)?
261
+ - [ ] 是否没有使用 `setTimeout` / `Task.Run` 承担请求外后台任务?
262
+ - [ ] 大任务是否按阈值主动使用后台任务,超过 10 分钟是否有 `HasMore + Checkpoint`?
263
+ - [ ] 是否区分累计分配、调用树预算、JS递归与接口嵌套,而不是盲目抬高全部限制?
264
+ - [ ] 若开启 `V8Unlimited`,是否有不可分片的单事务依据,并已评估数据库锁、日志、回滚、并发与节点故障重试?
265
+ - [ ] 保存后是否通过稳定路径 `/apiengine/{key}` + `osclient` Header 做过 HTTP 复测?特殊 GET/HEAD 路径是否仅用于无法设置 Header/Form/Query 的场景?
266
+
267
+ ## 常见错误
268
+
269
+ ❌ 把支付回调接口设为非匿名 → 第三方无 Token → 回调失败
270
+ ❌ 内部接口忘开 `StopHttp` → 被外部直接调用绕过校验
271
+ ❌ 对账接口未配置 `LockKey` → 集群多实例并发执行 → 数据双倍
272
+ ❌ 文件下载接口未开 `IsResponseFile` → 返回 JSON 而非文件流
@@ -0,0 +1,286 @@
1
+ ---
2
+ name: v8-cache-pattern
3
+ description: Microi V8 Redis 缓存与管理模式。用于读写 V8.Cache、租户缓存命名、TTL 策略、防陈旧数据,以及使用 Redis 管理器页面或 MCP 检索、统计、查看和维护 String、Hash、List、Set、Sorted Set、Stream。
4
+ ---
5
+
6
+ # Microi V8 Redis 缓存模式
7
+
8
+ 你正在开发 Microi 吾码平台的 V8 引擎代码,需要使用 Redis 缓存提升性能。V8 只获得当前租户的安全缓存代理,不会获得 Redis `IDatabase`、连接管理或服务器扫描能力。
9
+
10
+ ## V8.Cache API
11
+
12
+ | 方法 | 说明 | 返回值 |
13
+ |------|------|--------|
14
+ | `V8.Cache.Set(key, value, expire)` | 设置缓存 | `boolean` |
15
+ | `V8.Cache.Get(key)` | 获取缓存 | `string \| null` |
16
+ | `V8.Cache.Remove(key)` | 删除缓存 | `boolean` |
17
+ | `V8.Cache.KeyExist(key)` | 是否存在(兼容旧版运行时的真实方法名) | `boolean` |
18
+ | `V8.Cache.HashSet(key, field, value)` | 写入 Hash 字段 | `boolean` |
19
+ | `V8.Cache.HashGet(key, field)` | 读取 Hash 字段 | `string \| null` |
20
+ | `V8.Cache.HashGetAll(key)` | 读取全部 Hash 字段 | Hash 条目数组 |
21
+ | `V8.Cache.HashDelete(key, field)` | 删除 Hash 字段 | `boolean` |
22
+ | `V8.Cache.HashIncrement(key, field, amount)` | 原子增减数值字段 | `number` |
23
+
24
+ > 需要把接口引擎复制到不同版本的 Microi 环境时,统一使用 `V8.Cache.KeyExist(key)`。部分新版本可能提供 `Exists` 别名,但旧版运行时没有该方法。
25
+
26
+ > 新运行时把逻辑 Key 自动规范为 `Microi:${V8.OsClient}:{逻辑Key}`;已带当前租户完整前缀的历史 Key 不会重复添加。任何其它租户的 `Microi:` 前缀都会被拒绝,而不是改写后继续执行。
27
+
28
+ Hash 适合保存同一对象的多个独立字段或原子计数:
29
+
30
+ ```javascript
31
+ var hashKey = 'ProductStock:' + V8.Param.productId;
32
+ V8.Cache.HashSet(hashKey, 'Available', '120');
33
+ V8.Cache.HashSet(hashKey, 'Reserved', '8');
34
+
35
+ var available = V8.Cache.HashGet(hashKey, 'Available');
36
+ var allFields = V8.Cache.HashGetAll(hashKey);
37
+ var reserved = V8.Cache.HashIncrement(hashKey, 'Reserved', 1);
38
+
39
+ V8.Cache.HashDelete(hashKey, 'Reserved');
40
+ ```
41
+
42
+ `HashIncrement` 的 `amount` 可以为负数。当前 V8 Hash API 不提供独立 TTL 设置;需要自动过期时,优先把对象序列化为 String 后用 `Set(key, value, expire)`,或由受控 Redis 管理流程设置整 Key 的 TTL。
43
+
44
+ ## Redis 管理器与 MCP
45
+
46
+ 平台 Redis 管理器固定路由为 `#/mci-redis-manager`:
47
+
48
+ - 已登录平台管理员可使用当前租户默认 Redis,并可管理保存于主租户 `mci_redis_connection` 表的额外连接;记录必须按 `TenantOsClient` 隔离,密码只在后端加密保存且永不回传前端。
49
+ - 未登录时只允许创建当前页面内存中的临时连接;不得加载当前租户 Redis、已保存连接或缓存中的旧用户信息,刷新页面后必须清空临时凭据。
50
+ - Key 列表必须使用 `SCAN` 游标分页,禁止在生产 Redis 上使用阻塞式 `KEYS *`。内容查看支持 String、Hash、List、Set、Sorted Set、Stream;集合内容要分页并限制单次条数。
51
+ - 写入 Hash/List/Set/Sorted Set 时先完整解析 JSON,再覆盖旧 Key;删除、覆盖、重命名和 TTL 变更属于破坏性操作,必须先展示目标连接、数据库与 Key 并要求明确确认。
52
+ - 临时匿名接口只开放白名单操作,不开放任意 Redis 命令、Lua、`FLUSHALL` 或 `FLUSHDB`;设置短连接超时、访问频率限制、单次 Key 数量和内容大小上限。
53
+
54
+ MCP 默认操作当前 MCP `OsClient` 的租户 Redis;额外连接只传管理页保存后的 `connectionId`,禁止在 MCP 参数、日志或回答中传递 Redis 密码。
55
+
56
+ | MCP 工具 | 用途 | 确认规则 |
57
+ |------|------|------|
58
+ | `microi_redis_statistics` | 服务器、内存、客户端、命中率与 Key 类型统计 | 只读 |
59
+ | `microi_redis_list_keys` | SCAN 分页检索 Key、类型、TTL、内存估算 | 只读 |
60
+ | `microi_redis_get_key` | 分页查看单个 Key 内容 | 只读 |
61
+ | `microi_redis_delete_keys` | 单个或批量删除,最多 500 个 | `confirmExecution="DELETE"` |
62
+ | `microi_redis_replace_value` | 新建或覆盖 String/Hash/List/Set/Sorted Set | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
63
+ | `microi_redis_rename_key` | 不覆盖目标的 Key 重命名 | `confirmExecution` 等于新 Key 或 `EXECUTE` |
64
+ | `microi_redis_set_ttl` | `-1` 永久、`0` 删除、正数为秒 | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
65
+
66
+ **过期时间格式:** 支持两种写法
67
+ - 整数(秒):`V8.Cache.Set(key, value, 3600)` = 1 小时
68
+ - 字符串 `d.HH:mm:ss`:
69
+ - `'0.00:00:59'` = 59 秒
70
+ - `'0.01:00:00'` = 1 小时
71
+ - `'0.12:00:00'` = 12 小时
72
+ - `'1.00:00:00'` = 1 天
73
+ - `'7.00:00:00'` = 7 天
74
+ - 不传则**永久缓存**(直到手动 Remove 或 Redis 重启)
75
+
76
+ ## 🔑 Key 命名规范(必须遵守)
77
+
78
+ Redis 中统一保存 4 段式 Key:`Microi:${OsClient}:{Category}:{Key}`。V8 代码推荐只传 `{Category}:{Key}`,服务端自动添加当前 `OsClient`;传完整当前租户 Key 用于兼容旧脚本。
79
+
80
+ ```javascript
81
+ // ✅ 推荐:逻辑 Key,运行时自动绑定当前租户
82
+ var k1 = 'User:' + userId;
83
+ var k2 = 'SmsCode:' + phone;
84
+ var k3 = 'Lock:OrderPay:' + orderId;
85
+
86
+ // ✅ 兼容:完整当前租户 Key
87
+ var fullKey = 'Microi:' + V8.OsClient + ':User:' + userId;
88
+
89
+ // ❌ 拒绝:不能访问其它租户
90
+ var foreignKey = 'Microi:other-tenant:User:' + userId;
91
+ ```
92
+
93
+ | 段 | 说明 |
94
+ |----|------|
95
+ | `Microi:` | 平台前缀,固定 |
96
+ | `${V8.OsClient}` | 租户隔离 |
97
+ | `{Category}` | 业务分类(User / SmsCode / Lock / Token / ImportStep …) |
98
+ | `{Key}` | 具体业务 Key |
99
+
100
+ > 系统已用前缀(避免冲突):`Microi:${OsClient}:Token:`、`Microi:${OsClient}:User:`、`Microi:${OsClient}:OsClient`、`Microi:${OsClient}:DiyTable:`、`Microi:${OsClient}:Sys:`
101
+
102
+ ## 缓存层级(L1 + L2)
103
+
104
+ 平台内部对系统配置等场景实现了 **L1 进程内缓存 + L2 Redis 缓存**:
105
+
106
+ - L1:.NET 进程内 `IMemoryCache`(每个容器独立)
107
+ - L2:Redis(全集群共享)
108
+
109
+ 读取顺序:L1 命中 → L2 命中 → 数据库
110
+ 写入顺序:DB → L2 → L1
111
+
112
+ > ⚠️ 直接修改数据库未走平台保存流程时,可能绕过缓存失效。优先调用受支持的保存/刷新接口并回读验证;不要把重启容器或清空整个 Redis 当作日常缓存刷新方案。
113
+
114
+ ### FormEngine 授权缓存(Redis epoch + 用户级快照)
115
+
116
+ FormEngine 授权是平台内部安全缓存,不能由业务 V8 直接读写。它既要兼容历史前端 V8 的无 `_SysMenuId` 调用,也要避免每个请求重复查询 `sys_user`、`sys_role`、`sys_rolelimit` 和 `sys_menu`:
117
+
118
+ 1. 每个 `OsClient` 在共享 Redis 中维护单调递增的授权版本 `epoch`。
119
+ 2. 用户授权快照 Key 至少包含 `OsClient + epoch + UserId`,内容包含当前有效用户状态/级别、有效角色、可访问菜单、菜单绑定表、操作权限和数据范围元数据。
120
+ 3. 每个 API 节点可用短 TTL 的进程内 L1 加速;Redis L2 在所有节点间共享。读取顺序为“当前 epoch → L1 用户快照 → L2 用户快照 → 主库冷加载”。
121
+ 4. 冷加载必须查询主库而不是只读副本,防止复制延迟把刚禁用的用户、撤销的角色或旧菜单范围重新写回缓存。并发冷加载可在单节点合并,但正确性仍以 Redis `epoch` 和主库事实为准。
122
+ 5. 用户状态/级别/角色、角色状态、角色菜单/高级表权限、菜单绑定表、菜单权限 JSON、`SqlWhere`、`SqlJoin` / `JoinTables` 等授权事实变更后,必须在写入成功后递增 Redis `epoch`。新旧节点滚动发布期间都通过版本切换自然淘汰旧快照。
123
+ 6. L1 丢失、节点重启或发布不影响正确性;禁止把永久 `static` 字典、单机文件或粘性会话当作授权事实源。短 TTL 只是兜底,不能代替变更时递增 `epoch`。
124
+
125
+ 无菜单客户端请求只使用该快照推断当前用户对目标表的权限;显式 `_SysMenuId` 仍按对应菜单严格精确校验。两种路径都必须在实际 SQL 中应用菜单 `SqlWhere` / `SqlJoin` 数据范围,不能只缓存一个“允许/拒绝”结果后绕过行级范围。
126
+
127
+ ## 基本读写
128
+
129
+ ```javascript
130
+ // 设置缓存(有效期 1 小时)
131
+ V8.Cache.Set('user:' + userId, JSON.stringify(userData), '0.01:00:00');
132
+
133
+ // 读取缓存
134
+ var cached = V8.Cache.Get('user:' + userId);
135
+ if (cached) {
136
+ return { Code: 1, Data: JSON.parse(cached) };
137
+ }
138
+
139
+ // 删除缓存
140
+ V8.Cache.Remove('user:' + userId);
141
+ ```
142
+
143
+ ## Cache-Aside 模式(最常用)
144
+
145
+ 先查缓存,缓存不存在时查数据库并回填缓存。
146
+
147
+ ```javascript
148
+ var cacheKey = 'Microi:' + V8.OsClient + ':product:detail:' + V8.Param.id;
149
+
150
+ // 1. 先查缓存
151
+ var cached = V8.Cache.Get(cacheKey);
152
+ if (cached) {
153
+ return { Code: 1, Data: JSON.parse(cached) };
154
+ }
155
+
156
+ // 2. 缓存未命中,查数据库
157
+ var result = V8.FormEngine.GetFormData('Product', {
158
+ _Where: [['Id', '=', V8.Param.id]]
159
+ });
160
+
161
+ if (result.Code !== 1 || !result.Data) {
162
+ return { Code: 0, Msg: '数据不存在' };
163
+ }
164
+
165
+ // 3. 回填缓存(有效期 30 分钟)
166
+ V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
167
+
168
+ return { Code: 1, Data: result.Data };
169
+ ```
170
+
171
+ ## 数据更新时清除缓存
172
+
173
+ ```javascript
174
+ // 在 SubmitAfterServerV8.js(数据写入后)清除缓存
175
+ if (V8.FormSubmitAction === 'Update' || V8.FormSubmitAction === 'Delete') {
176
+ V8.Cache.Remove('Microi:' + V8.OsClient + ':product:detail:' + V8.Form.Id);
177
+ V8.Cache.Remove('Microi:' + V8.OsClient + ':product:list');
178
+ }
179
+ ```
180
+
181
+ ## 列表缓存(含分页)
182
+
183
+ ```javascript
184
+ var pageIndex = parseInt(V8.Param.pageIndex) || 1;
185
+ var pageSize = parseInt(V8.Param.pageSize) || 20;
186
+ var cacheKey = 'Microi:' + V8.OsClient + ':product:list:' + pageIndex + ':' + pageSize;
187
+
188
+ var cached = V8.Cache.Get(cacheKey);
189
+ if (cached) {
190
+ return JSON.parse(cached);
191
+ }
192
+
193
+ var result = V8.FormEngine.GetTableData('Product', {
194
+ _Where: [['Status', '=', 1]],
195
+ _OrderBy: 'SortOrder',
196
+ _PageIndex: pageIndex,
197
+ _PageSize: pageSize
198
+ });
199
+
200
+ var response = { Code: 1, Data: result.Data, DataCount: result.DataCount };
201
+
202
+ // 列表缓存时间短一些(5 分钟)
203
+ V8.Cache.Set(cacheKey, JSON.stringify(response), '0.00:05:00');
204
+
205
+ return response;
206
+ ```
207
+
208
+ ## 防缓存穿透(查询不存在的数据)
209
+
210
+ ```javascript
211
+ var cacheKey = 'Microi:' + V8.OsClient + ':user:' + V8.Param.id;
212
+ var cached = V8.Cache.Get(cacheKey);
213
+
214
+ // 注意:缓存值可能是 "null" 字符串(空对象占位)
215
+ if (cached !== null) {
216
+ if (cached === 'null') {
217
+ return { Code: 0, Msg: '数据不存在' };
218
+ }
219
+ return { Code: 1, Data: JSON.parse(cached) };
220
+ }
221
+
222
+ var result = V8.FormEngine.GetFormData('SysUser', {
223
+ _Where: [['Id', '=', V8.Param.id]]
224
+ });
225
+
226
+ if (result.Code === 1 && result.Data) {
227
+ V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
228
+ return { Code: 1, Data: result.Data };
229
+ } else {
230
+ // 缓存空值,短过期时间防止穿透
231
+ V8.Cache.Set(cacheKey, 'null', '0.00:01:00');
232
+ return { Code: 0, Msg: '数据不存在' };
233
+ }
234
+ ```
235
+
236
+ ## 分布式锁:不要用普通 Cache 拼装
237
+
238
+ `KeyExist → Set → Remove` 不是分布式锁:检查与写入不原子、没有唯一持有者令牌、锁过期后旧持有者会删除新持有者的锁,也无法处理节点暂停、网络分区和滚动发布。
239
+
240
+ V8 业务脚本需要互斥时:
241
+
242
+ 1. 接口引擎使用平台 `LockKey/LockTimeout` 配置;
243
+ 2. Job/Worker 使用带租约、唯一持有者令牌、续租、超时自动释放和“仅持有者可释放”语义的共享锁;
244
+ 3. Key 至少包含 `OsClient + 任务/业务唯一标识`;
245
+ 4. 分布式锁只能减少并发,业务副作用仍必须用幂等键、唯一约束/条件更新、状态机或 outbox/inbox 保证只执行一次。
246
+
247
+ `V8.Cache` 没有公开安全的 compare-and-set/带令牌释放原语时,禁止自行实现锁。
248
+
249
+ ## 原子计数与限流
250
+
251
+ `Get → parseInt → Set` 在并发下会丢计数。普通 Hash 计数可使用 `V8.Cache.HashIncrement`;需要“计数 + 首次设置 TTL + 超限拒绝”的安全限流、日上传配额或金额额度时,应使用平台 `RateLimit` / SecurityGuard 或后端 Redis Lua 原子脚本,并在 Redis 不可用时按风险选择失败关闭。不要在 V8 中用多个普通 Cache 调用模拟原子配额。
252
+
253
+ ## 缓存 Key 命名规范
254
+
255
+ ```
256
+ Microi:{OsClient}:{业务}:{类型}:{标识}
257
+ Microi:myapp:product:detail:xxx-id 单条产品
258
+ Microi:myapp:product:list:1:20 产品列表第1页
259
+ Microi:myapp:user:profile:xxx-id 用户资料
260
+ Microi:myapp:config:system 系统配置
261
+ Microi:myapp:wx:access_token 微信 token
262
+ Microi:myapp:lock:order:xxx-id 订单锁
263
+ Microi:myapp:api:count:userId:date API 调用计数
264
+ ```
265
+
266
+ ## 注意事项
267
+
268
+ - `V8.Cache.Get()` 返回 `null` 表示 key 不存在,返回空字符串 `''` 是合法值
269
+ - `V8.Cache.Set()` 的 value 必须是字符串,对象需要 `JSON.stringify()`
270
+ - **过期时间格式为 `d.HH:mm:ss` 字符串**(非秒数),不传则永久缓存
271
+ - Key 命名建议:`Microi:{V8.OsClient}:{分类}:{Key}`,避免跨应用冲突
272
+ - 写操作后即时清除相关缓存,避免脏数据
273
+ - 不要缓存频繁变化的数据(如实时库存),不如每次查库
274
+
275
+ ## 后端批量写入与 Redis Pub/Sub 回压
276
+
277
+ 平台源码中的缓存写入、删除和按模式删除不仅操作 Redis 数据,还会发布跨节点 L1
278
+ 失效通知。批量导入、自动升级和迁移代码必须 `await` 这些异步调用,禁止
279
+ fire-and-forget;否则数千个 `SCAN/DEL/PUBLISH` 会同时进入同一个
280
+ `ConnectionMultiplexer`,表现为 `outstanding` 持续升高、`SocketClosed`,并可能让
281
+ 其它节点继续使用旧缓存。
282
+
283
+ - 同一租户的失效广播要有界并发,短暂连接异常可做有限次数重试;
284
+ - 持续故障的日志应按时间窗口汇总,但不得静默吞掉一致性告警;
285
+ - 每个租户可能使用不同 Redis,订阅初始化状态不得用一个全局 `static bool` 共享;
286
+ - 等待发布只解决回压,业务写入和缓存失效仍需保持 `OsClient` 隔离及可重试幂等。