@microi.net/cli 5.2.5 → 5.2.7

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 (51) hide show
  1. package/.codebuddy-plugin/marketplace.json +2 -2
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.workbuddy-plugin/marketplace.json +2 -2
  5. package/.workbuddy-plugin/plugin.json +1 -1
  6. package/assets/build-meta.json +6 -6
  7. package/cordis.patch.yml +1 -1
  8. package/package.json +1 -1
  9. package/scripts/mcp-server.js +88 -88
  10. package/scripts/microi-cli.js +15 -0
  11. package/scripts/microi-skills.meta.json +225 -222
  12. package/skills/.microi-skills-version.json +2 -2
  13. package/skills/.progressive-disclosure-manifest.json +93 -93
  14. package/skills/README.md +2 -1
  15. package/skills/ai-engine/SKILL.md +1 -1
  16. package/skills/app-store/SKILL.md +13 -9
  17. package/skills/microi-client-frontend/SKILL.md +1 -1
  18. package/skills/microi-client-frontend/references/progressive-01-3-/345/212/250/346/200/201/346/214/211/351/222/256/347/263/273/347/273/237.md +1 -1
  19. package/skills/microi-client-frontend/references/progressive-03-vue3-/345/211/215/347/253/257/345/276/256/346/234/215/345/212/241/345/256/277/344/270/273/350/247/204/345/210/231.md +2 -2
  20. package/skills/microi-docs-coverage/references/capability-map.md +3 -1
  21. package/skills/microi-form-layout/SKILL.md +6 -6
  22. package/skills/microi-frontend-sdk/SKILL.md +6 -6
  23. package/skills/microi-microservice/SKILL.md +2 -1
  24. package/skills/microi-sso/SKILL.md +3 -1
  25. package/skills/microi-sso/references/acceptance.md +1 -1
  26. package/skills/microi-sso/references/configuration-and-security.md +1 -1
  27. package/skills/microi-system-delivery/SKILL.md +2 -2
  28. package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +1 -1
  29. package/skills/microi.v8.js +1 -1
  30. package/skills/performance-testing/SKILL.md +12 -0
  31. package/skills/system-observability/SKILL.md +139 -0
  32. package/skills/translate-engine/SKILL.md +3 -0
  33. package/skills/ui-design/SKILL.md +6 -3
  34. package/skills/v8-api-config/SKILL.md +31 -9
  35. package/skills/v8-cache-pattern/SKILL.md +304 -289
  36. package/skills/v8-debugging/SKILL.md +1 -1
  37. package/skills/v8-file-upload/SKILL.md +3 -3
  38. package/skills/v8-file-upload/references/progressive-01-/345/205/254/346/234/211/346/241/266-vs-/347/247/201/346/234/211/346/241/266.md +1 -1
  39. package/skills/v8-menu-buttons/SKILL.md +1 -1
  40. package/skills/v8-menu-buttons/references/progressive-01-2-/346/214/211/351/222/256/345/257/271/350/261/241-schema.md +1 -1
  41. package/skills/v8-mq-mqtt/SKILL.md +130 -109
  42. package/skills/v8-mq-mqtt/references/mqtt-production.md +3 -2
  43. package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +14 -16
  44. package/skills/v8-security/SKILL.md +7 -5
  45. package/skills/v8-table-event/SKILL.md +1 -1
  46. package/skills/v8-table-event/references/progressive-02-/345/211/215/347/253/257/344/272/213/344/273/266/345/220/215-v8-eventname-/345/217/257/350/203/275/347/232/204/345/200/274.md +1 -1
  47. package/skills/v8-utilities/SKILL.md +1 -1
  48. package/skills/v8-utilities/references/platform-http-routes.md +4 -2
  49. package/skills/v8-utilities/references/server-api-index.md +6 -3
  50. package/skills/v8-workflow/SKILL.md +1 -1
  51. package/skills/workspace-conventions/SKILL.md +1 -1
@@ -1,289 +1,304 @@
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
- > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
-
8
- # Microi V8 Redis 缓存模式
9
-
10
- 你正在开发 Microi 吾码平台的 V8 引擎代码,需要使用 Redis 缓存提升性能。V8 只获得当前租户的安全缓存代理,不会获得 Redis `IDatabase`、连接管理或服务器扫描能力。
11
-
12
- ## V8.Cache API
13
-
14
- | 方法 | 说明 | 返回值 |
15
- |------|------|--------|
16
- | `V8.Cache.Set(key, value, expire)` | 设置缓存 | `boolean` |
17
- | `V8.Cache.Get(key)` | 获取缓存 | `string \| null` |
18
- | `V8.Cache.Remove(key)` | 删除缓存 | `boolean` |
19
- | `V8.Cache.KeyExist(key)` | 是否存在(兼容旧版运行时的真实方法名) | `boolean` |
20
- | `V8.Cache.HashSet(key, field, value)` | 写入 Hash 字段 | `boolean` |
21
- | `V8.Cache.HashGet(key, field)` | 读取 Hash 字段 | `string \| null` |
22
- | `V8.Cache.HashGetAll(key)` | 读取全部 Hash 字段 | Hash 条目数组 |
23
- | `V8.Cache.HashDelete(key, field)` | 删除 Hash 字段 | `boolean` |
24
- | `V8.Cache.HashIncrement(key, field, amount)` | 原子增减数值字段 | `number` |
25
-
26
- > 需要把接口引擎复制到不同版本的 Microi 环境时,统一使用 `V8.Cache.KeyExist(key)`。部分新版本可能提供 `Exists` 别名,但旧版运行时没有该方法。
27
-
28
- > 新运行时把逻辑 Key 自动规范为 `Microi:${V8.OsClient}:{逻辑Key}`;已带当前租户完整前缀的历史 Key 不会重复添加。任何其它租户的 `Microi:` 前缀都会被拒绝,而不是改写后继续执行。
29
-
30
- Hash 适合保存同一对象的多个独立字段或原子计数:
31
-
32
- ```javascript
33
- var hashKey = 'ProductStock:' + V8.Param.productId;
34
- V8.Cache.HashSet(hashKey, 'Available', '120');
35
- V8.Cache.HashSet(hashKey, 'Reserved', '8');
36
-
37
- var available = V8.Cache.HashGet(hashKey, 'Available');
38
- var allFields = V8.Cache.HashGetAll(hashKey);
39
- var reserved = V8.Cache.HashIncrement(hashKey, 'Reserved', 1);
40
-
41
- V8.Cache.HashDelete(hashKey, 'Reserved');
42
- ```
43
-
44
- `HashIncrement` `amount` 可以为负数。当前 V8 Hash API 不提供独立 TTL 设置;需要自动过期时,优先把对象序列化为 String 后用 `Set(key, value, expire)`,或由受控 Redis 管理流程设置整 Key 的 TTL。
45
-
46
- ## Redis 管理器与 MCP
47
-
48
- 平台 Redis 管理器固定路由为 `#/mci-redis-manager`:
49
-
50
- - 已登录平台管理员可使用当前租户默认 Redis,并可管理保存于主租户 `mci_redis_connection` 表的额外连接;记录必须按 `TenantOsClient` 隔离,密码只在后端加密保存且永不回传前端。
51
- - 未登录时只允许创建当前页面内存中的临时连接;不得加载当前租户 Redis、已保存连接或缓存中的旧用户信息,刷新页面后必须清空临时凭据。
52
- - Key 列表必须使用 `SCAN` 游标分页,禁止在生产 Redis 上使用阻塞式 `KEYS *`。内容查看支持 String、Hash、List、Set、Sorted Set、Stream;集合内容要分页并限制单次条数。
53
- - 写入 Hash/List/Set/Sorted Set 时先完整解析 JSON,再覆盖旧 Key;删除、覆盖、重命名和 TTL 变更属于破坏性操作,必须先展示目标连接、数据库与 Key 并要求明确确认。
54
- - 临时匿名接口只开放白名单操作,不开放任意 Redis 命令、Lua、`FLUSHALL` 或 `FLUSHDB`;设置短连接超时、访问频率限制、单次 Key 数量和内容大小上限。
55
-
56
- MCP 默认操作当前 MCP `OsClient` 的租户 Redis;额外连接只传管理页保存后的 `connectionId`,禁止在 MCP 参数、日志或回答中传递 Redis 密码。
57
-
58
- | MCP 工具 | 用途 | 确认规则 |
59
- |------|------|------|
60
- | `microi_redis_statistics` | 服务器、内存、客户端、命中率与 Key 类型统计 | 只读 |
61
- | `microi_redis_list_keys` | SCAN 分页检索 Key、类型、TTL、内存估算 | 只读 |
62
- | `microi_redis_get_key` | 分页查看单个 Key 内容 | 只读 |
63
- | `microi_redis_delete_keys` | 单个或批量删除,最多 500 个 | `confirmExecution="DELETE"` |
64
- | `microi_redis_replace_value` | 新建或覆盖 String/Hash/List/Set/Sorted Set | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
65
- | `microi_redis_rename_key` | 不覆盖目标的 Key 重命名 | `confirmExecution` 等于新 Key 或 `EXECUTE` |
66
- | `microi_redis_set_ttl` | `-1` 永久、`0` 删除、正数为秒 | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
67
-
68
- **过期时间格式:** 支持两种写法
69
- - 整数(秒):`V8.Cache.Set(key, value, 3600)` = 1 小时
70
- - 字符串 `d.HH:mm:ss`:
71
- - `'0.00:00:59'` = 59
72
- - `'0.01:00:00'` = 1 小时
73
- - `'0.12:00:00'` = 12 小时
74
- - `'1.00:00:00'` = 1 天
75
- - `'7.00:00:00'` = 7
76
- - 不传则**永久缓存**(直到手动 RemoveRedis 重启)
77
-
78
- ## 🔑 Key 命名规范(必须遵守)
79
-
80
- Redis 中统一保存 4 段式 Key:`Microi:${OsClient}:{Category}:{Key}`。V8 代码推荐只传 `{Category}:{Key}`,服务端自动添加当前 `OsClient`;传完整当前租户 Key 用于兼容旧脚本。
81
-
82
- ```javascript
83
- // 推荐:逻辑 Key,运行时自动绑定当前租户
84
- var k1 = 'User:' + userId;
85
- var k2 = 'SmsCode:' + phone;
86
- var k3 = 'Lock:OrderPay:' + orderId;
87
-
88
- // ✅ 兼容:完整当前租户 Key
89
- var fullKey = 'Microi:' + V8.OsClient + ':User:' + userId;
90
-
91
- // 拒绝:不能访问其它租户
92
- var foreignKey = 'Microi:other-tenant:User:' + userId;
93
- ```
94
-
95
- | | 说明 |
96
- |----|------|
97
- | `Microi:` | 平台前缀,固定 |
98
- | `${V8.OsClient}` | 租户隔离 |
99
- | `{Category}` | 业务分类(User / SmsCode / Lock / Token / ImportStep …) |
100
- | `{Key}` | 具体业务 Key |
101
-
102
- > 系统已用前缀(避免冲突):`Microi:${OsClient}:Token:`、`Microi:${OsClient}:User:`、`Microi:${OsClient}:OsClient`、`Microi:${OsClient}:DiyTable:`、`Microi:${OsClient}:Sys:`
103
-
104
- ## 缓存层级(L1 + L2)
105
-
106
- 平台内部对系统配置等场景实现了 **L1 进程内缓存 + L2 Redis 缓存**:
107
-
108
- - L1:.NET 进程内 `IMemoryCache`(每个容器独立)
109
- - L2:Redis(全集群共享)
110
-
111
- 读取顺序:L1 命中 L2 命中 → 数据库
112
- 写入顺序:DB → L2 → L1
113
-
114
- > ⚠️ 直接修改数据库未走平台保存流程时,可能绕过缓存失效。优先调用受支持的保存/刷新接口并回读验证;不要把重启容器或清空整个 Redis 当作日常缓存刷新方案。
115
-
116
- ### FormEngine 授权缓存(Redis epoch + 用户级快照)
117
-
118
- FormEngine 授权是平台内部安全缓存,不能由业务 V8 直接读写。它既要兼容历史前端 V8 的无 `_SysMenuId` 调用,也要避免每个请求重复查询 `sys_user`、`sys_role`、`sys_rolelimit` 和 `sys_menu`:
119
-
120
- 1. 每个 `OsClient` 在共享 Redis 中维护单调递增的授权版本 `epoch`。
121
- 2. 用户授权快照 Key 至少包含 `OsClient + epoch + UserId`,内容包含当前有效用户状态/级别、有效角色、可访问菜单、菜单绑定表、操作权限和数据范围元数据。
122
- 3. 每个 API 节点可用短 TTL 的进程内 L1 加速;Redis L2 在所有节点间共享。读取顺序为“当前 epoch → L1 用户快照 L2 用户快照 → 主库冷加载”。
123
- 4. 冷加载必须查询主库而不是只读副本,防止复制延迟把刚禁用的用户、撤销的角色或旧菜单范围重新写回缓存。并发冷加载可在单节点合并,但正确性仍以 Redis `epoch` 和主库事实为准。
124
- 5. 用户状态/级别/角色、角色状态、角色菜单/高级表权限、菜单绑定表、菜单权限 JSON、`SqlWhere`、`SqlJoin` / `JoinTables` 等授权事实变更后,必须在写入成功后递增 Redis `epoch`。新旧节点滚动发布期间都通过版本切换自然淘汰旧快照。
125
- 6. L1 丢失、节点重启或发布不影响正确性;禁止把永久 `static` 字典、单机文件或粘性会话当作授权事实源。短 TTL 只是兜底,不能代替变更时递增 `epoch`。
126
-
127
- 无菜单客户端请求只使用该快照推断当前用户对目标表的权限;显式 `_SysMenuId` 仍按对应菜单严格精确校验。两种路径都必须在实际 SQL 中应用菜单 `SqlWhere` / `SqlJoin` 数据范围,不能只缓存一个“允许/拒绝”结果后绕过行级范围。
128
-
129
- ## 基本读写
130
-
131
- ```javascript
132
- // 设置缓存(有效期 1 小时)
133
- V8.Cache.Set('user:' + userId, JSON.stringify(userData), '0.01:00:00');
134
-
135
- // 读取缓存
136
- var cached = V8.Cache.Get('user:' + userId);
137
- if (cached) {
138
- return { Code: 1, Data: JSON.parse(cached) };
139
- }
140
-
141
- // 删除缓存
142
- V8.Cache.Remove('user:' + userId);
143
- ```
144
-
145
- ## Cache-Aside 模式(最常用)
146
-
147
- 先查缓存,缓存不存在时查数据库并回填缓存。
148
-
149
- ```javascript
150
- var cacheKey = 'Microi:' + V8.OsClient + ':product:detail:' + V8.Param.id;
151
-
152
- // 1. 先查缓存
153
- var cached = V8.Cache.Get(cacheKey);
154
- if (cached) {
155
- return { Code: 1, Data: JSON.parse(cached) };
156
- }
157
-
158
- // 2. 缓存未命中,查数据库
159
- var result = V8.FormEngine.GetFormData('Product', {
160
- _Where: [['Id', '=', V8.Param.id]]
161
- });
162
-
163
- if (result.Code !== 1 || !result.Data) {
164
- return { Code: 0, Msg: '数据不存在' };
165
- }
166
-
167
- // 3. 回填缓存(有效期 30 分钟)
168
- V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
169
-
170
- return { Code: 1, Data: result.Data };
171
- ```
172
-
173
- ## 数据更新时清除缓存
174
-
175
- ```javascript
176
- // 在 SubmitAfterServerV8.js(数据写入后)清除缓存
177
- if (V8.FormSubmitAction === 'Update' || V8.FormSubmitAction === 'Delete') {
178
- V8.Cache.Remove('Microi:' + V8.OsClient + ':product:detail:' + V8.Form.Id);
179
- V8.Cache.Remove('Microi:' + V8.OsClient + ':product:list');
180
- }
181
- ```
182
-
183
- ## 列表缓存(含分页)
184
-
185
- ```javascript
186
- var pageIndex = parseInt(V8.Param.pageIndex) || 1;
187
- var pageSize = parseInt(V8.Param.pageSize) || 20;
188
- var cacheKey = 'Microi:' + V8.OsClient + ':product:list:' + pageIndex + ':' + pageSize;
189
-
190
- var cached = V8.Cache.Get(cacheKey);
191
- if (cached) {
192
- return JSON.parse(cached);
193
- }
194
-
195
- var result = V8.FormEngine.GetTableData('Product', {
196
- _Where: [['Status', '=', 1]],
197
- _OrderBy: 'SortOrder',
198
- _PageIndex: pageIndex,
199
- _PageSize: pageSize
200
- });
201
-
202
- var response = { Code: 1, Data: result.Data, DataCount: result.DataCount };
203
-
204
- // 列表缓存时间短一些(5 分钟)
205
- V8.Cache.Set(cacheKey, JSON.stringify(response), '0.00:05:00');
206
-
207
- return response;
208
- ```
209
-
210
- ## 防缓存穿透(查询不存在的数据)
211
-
212
- ```javascript
213
- var cacheKey = 'Microi:' + V8.OsClient + ':user:' + V8.Param.id;
214
- var cached = V8.Cache.Get(cacheKey);
215
-
216
- // 注意:缓存值可能是 "null" 字符串(空对象占位)
217
- if (cached !== null) {
218
- if (cached === 'null') {
219
- return { Code: 0, Msg: '数据不存在' };
220
- }
221
- return { Code: 1, Data: JSON.parse(cached) };
222
- }
223
-
224
- var result = V8.FormEngine.GetFormData('SysUser', {
225
- _Where: [['Id', '=', V8.Param.id]]
226
- });
227
-
228
- if (result.Code === 1 && result.Data) {
229
- V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
230
- return { Code: 1, Data: result.Data };
231
- } else {
232
- // 缓存空值,短过期时间防止穿透
233
- V8.Cache.Set(cacheKey, 'null', '0.00:01:00');
234
- return { Code: 0, Msg: '数据不存在' };
235
- }
236
- ```
237
-
238
- ## 分布式锁:不要用普通 Cache 拼装
239
-
240
- `KeyExist → Set → Remove` 不是分布式锁:检查与写入不原子、没有唯一持有者令牌、锁过期后旧持有者会删除新持有者的锁,也无法处理节点暂停、网络分区和滚动发布。
241
-
242
- V8 业务脚本需要互斥时:
243
-
244
- 1. 接口引擎使用平台 `LockKey/LockTimeout` 配置;
245
- 2. Job/Worker 使用带租约、唯一持有者令牌、续租、超时自动释放和“仅持有者可释放”语义的共享锁;
246
- 3. Key 至少包含 `OsClient + 任务/业务唯一标识`;
247
- 4. 分布式锁只能减少并发,业务副作用仍必须用幂等键、唯一约束/条件更新、状态机或 outbox/inbox 保证只执行一次。
248
-
249
- `V8.Cache` 没有公开安全的 compare-and-set/带令牌释放原语时,禁止自行实现锁。
250
-
251
- ## 原子计数与限流
252
-
253
- `Get → parseInt → Set` 在并发下会丢计数。普通 Hash 计数可使用 `V8.Cache.HashIncrement`;需要“计数 + 首次设置 TTL + 超限拒绝”的安全限流、日上传配额或金额额度时,应使用平台 `RateLimit` / SecurityGuard 或后端 Redis Lua 原子脚本,并在 Redis 不可用时按风险选择失败关闭。不要在 V8 中用多个普通 Cache 调用模拟原子配额。
254
-
255
- ## 缓存 Key 命名规范
256
-
257
- ```
258
- Microi:{OsClient}:{业务}:{类型}:{标识}
259
- Microi:myapp:product:detail:xxx-id 单条产品
260
- Microi:myapp:product:list:1:20 产品列表第1页
261
- Microi:myapp:user:profile:xxx-id 用户资料
262
- Microi:myapp:config:system 系统配置
263
- Microi:myapp:wx:access_token 微信 token
264
- Microi:myapp:lock:order:xxx-id 订单锁
265
- Microi:myapp:api:count:userId:date API 调用计数
266
- ```
267
-
268
- ## 注意事项
269
-
270
- - `V8.Cache.Get()` 返回 `null` 表示 key 不存在,返回空字符串 `''` 是合法值
271
- - `V8.Cache.Set()` 的 value 必须是字符串,对象需要 `JSON.stringify()`
272
- - **过期时间格式为 `d.HH:mm:ss` 字符串**(非秒数),不传则永久缓存
273
- - Key 命名建议:`Microi:{V8.OsClient}:{分类}:{Key}`,避免跨应用冲突
274
- - 写操作后即时清除相关缓存,避免脏数据
275
- - 不要缓存频繁变化的数据(如实时库存),不如每次查库
276
-
277
- ## 后端批量写入与 Redis Pub/Sub 回压
278
-
279
- 平台源码中的缓存写入、删除和按模式删除不仅操作 Redis 数据,还会发布跨节点 L1
280
- 失效通知。批量导入、自动升级和迁移代码必须 `await` 这些异步调用,禁止
281
- fire-and-forget;否则数千个 `SCAN/DEL/PUBLISH` 会同时进入同一个
282
- `ConnectionMultiplexer`,表现为 `outstanding` 持续升高、`SocketClosed`,并可能让
283
- 其它节点继续使用旧缓存。
284
-
285
- - 同一租户的失效广播要有界并发,短暂连接异常可做有限次数重试;
286
- - 持续故障的日志应按时间窗口汇总,但不得静默吞掉一致性告警;
287
- - 每个租户可能使用不同 Redis,订阅初始化状态不得用一个全局 `static bool` 共享;
288
- - 缓存实例必须保存创建时的准确 `OsClient`,按模式 `SCAN/DEL` 时直接使用该租户连接;禁止根据 Redis DB 编号反推连接,因为不同租户可能在不同服务器上使用相同 DB 编号;
289
- - 等待发布只解决回压,业务写入和缓存失效仍需保持 `OsClient` 隔离及可重试幂等。
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
+ > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
+
8
+ # Microi V8 Redis 缓存模式
9
+
10
+ 你正在开发 Microi 吾码平台的 V8 引擎代码,需要使用 Redis 缓存提升性能。V8 只获得当前租户的安全缓存代理,不会获得 Redis `IDatabase`、连接管理或服务器扫描能力。
11
+
12
+ ## V8.Cache API
13
+
14
+ | 方法 | 说明 | 返回值 |
15
+ |------|------|--------|
16
+ | `V8.Cache.Set(key, value, expire)` | 设置缓存 | `boolean` |
17
+ | `V8.Cache.Get(key)` | 获取缓存 | `string \| null` |
18
+ | `V8.Cache.Remove/Delete/Del(key)` | 删除缓存(兼容别名) | `boolean` |
19
+ | `V8.Cache.KeyExist(key)` | 是否存在(兼容旧版运行时的真实方法名) | `boolean` |
20
+ | `V8.Cache.Exists(key)` | 是否存在(新版别名) | `boolean` |
21
+ | `V8.Cache.SetIfNotExists(key, value, seconds)` | Redis `SET NX`,只在不存在时写入 | `boolean` |
22
+ | `V8.Cache.Expire(key, seconds)` | 为整个 Redis Key 设置正数秒 TTL | `boolean` |
23
+ | `V8.Cache.HashSet(key, field, value)` | 写入 Hash 字段 | `boolean` |
24
+ | `V8.Cache.HashGet(key, field)` | 读取 Hash 字段 | `string \| null` |
25
+ | `V8.Cache.HashGetAll(key)` | 读取全部 Hash 字段 | Hash 条目数组 |
26
+ | `V8.Cache.HashGetAllKeys/HashGetAllValues(key)` | 读取全部字段名或反序列化值 | 数组 |
27
+ | `V8.Cache.HashDelete/HashRemove(key, field)` | 删除 Hash 字段(兼容别名) | `boolean` |
28
+ | `V8.Cache.HashExists/HashLength(key, field?)` | 字段存在判断或 Hash 长度 | `boolean / number` |
29
+ | `V8.Cache.HashIncrement(key, field, amount)` | 原子增减数值字段 | `number` |
30
+
31
+ > 需要把接口引擎复制到不同版本的 Microi 环境时,统一使用 `V8.Cache.KeyExist(key)`。部分新版本可能提供 `Exists` 别名,但旧版运行时没有该方法。
32
+
33
+ > 新运行时把逻辑 Key 自动规范为 `Microi:${V8.OsClient}:{逻辑Key}`;已带当前租户完整前缀的历史 Key 不会重复添加。任何其它租户的 `Microi:` 前缀都会被拒绝,而不是改写后继续执行。
34
+
35
+ Hash 适合保存同一对象的多个独立字段或原子计数:
36
+
37
+ ```javascript
38
+ var hashKey = 'ProductStock:' + V8.Param.productId;
39
+ V8.Cache.HashSet(hashKey, 'Available', '120');
40
+ V8.Cache.HashSet(hashKey, 'Reserved', '8');
41
+
42
+ var available = V8.Cache.HashGet(hashKey, 'Available');
43
+ var allFields = V8.Cache.HashGetAll(hashKey);
44
+ var reserved = V8.Cache.HashIncrement(hashKey, 'Reserved', 1);
45
+
46
+ V8.Cache.HashDelete(hashKey, 'Reserved');
47
+ ```
48
+
49
+ `HashIncrement` 的 `amount` 可以为负数。Hash 字段没有独立 TTL;可用
50
+ `V8.Cache.Expire(hashKey, seconds)` 为整个 Hash Key 设置 TTL。Hash 不进入 L1,
51
+ 因此这里没有 String L1 副本晚于 Redis TTL 的问题。
52
+
53
+ ## Redis 管理器与 MCP
54
+
55
+ 平台 Redis 管理器固定路由为 `#/mci-redis-manager`:
56
+
57
+ - Redis 管理器接口要求已登录且当前用户 `Level >= 9999`。它只接受 `tenant`(当前租户默认 Redis)和 `saved`(主租户 `mci_redis_connection` 中保存的额外连接)两种模式;当前源码明确拒绝 `temporary`,也没有匿名临时连接模式。
58
+ - 保存连接记录必须按 `TenantOsClient` 隔离,密码只在后端保护并且不回传前端;调用方只传保存后的 `connectionId`。
59
+ - Key 列表必须使用 `SCAN` 游标分页,禁止在生产 Redis 上使用阻塞式 `KEYS *`。内容查看支持 String、Hash、List、Set、Sorted Set、Stream;集合内容要分页并限制单次条数。
60
+ - 写入 Hash/List/Set/Sorted Set 时先完整解析 JSON,再覆盖旧 Key;删除、覆盖、重命名和 TTL 变更属于破坏性操作,必须先展示目标连接、数据库与 Key 并要求明确确认。
61
+ - 管理接口不开放任意 Redis 命令、Lua、`FLUSHALL` `FLUSHDB`;批量删除最多 500 Key,重命名不覆盖既有目标。
62
+
63
+ 对应后端管理入口包括只读统计 `/api/cache/statistics`、节点 L1 精确失效
64
+ `/api/cache/invalidate`、模式失效 `/api/cache/invalidate-pattern`,以及 Redis 管理器
65
+ 路由前缀 `/api/cache/redis/`。它们都属于受权管理能力,不是匿名业务 API。
66
+
67
+ MCP 默认操作当前 MCP `OsClient` 的租户 Redis;额外连接只传管理页保存后的 `connectionId`,禁止在 MCP 参数、日志或回答中传递 Redis 密码。
68
+
69
+ | MCP 工具 | 用途 | 确认规则 |
70
+ |------|------|------|
71
+ | `microi_redis_statistics` | 服务器、内存、客户端、命中率与 Key 类型统计 | 只读 |
72
+ | `microi_redis_list_keys` | SCAN 分页检索 Key、类型、TTL、内存估算 | 只读 |
73
+ | `microi_redis_get_key` | 分页查看单个 Key 内容 | 只读 |
74
+ | `microi_redis_delete_keys` | 单个或批量删除,最多 500 个 | `confirmExecution="DELETE"` |
75
+ | `microi_redis_replace_value` | 新建或覆盖 String/Hash/List/Set/Sorted Set | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
76
+ | `microi_redis_rename_key` | 不覆盖目标的 Key 重命名 | `confirmExecution` 等于新 Key `EXECUTE` |
77
+ | `microi_redis_set_ttl` | `-1` 永久、`0` 删除、正数为秒 | `confirmExecution` 等于完整 Key 或 `EXECUTE` |
78
+
79
+ **过期时间格式:** 支持两种写法
80
+ - 整数(秒):`V8.Cache.Set(key, value, 3600)` = 1 小时
81
+ - 字符串 `d.HH:mm:ss`:
82
+ - `'0.00:00:59'` = 59 秒
83
+ - `'0.01:00:00'` = 1 小时
84
+ - `'0.12:00:00'` = 12 小时
85
+ - `'1.00:00:00'` = 1 天
86
+ - `'7.00:00:00'` = 7 天
87
+ - 不传则不设置业务 TTL;实际存续还受显式删除、Redis 淘汰策略和持久化配置影响
88
+
89
+ ## 🔑 Key 命名规范(必须遵守)
90
+
91
+ Redis 中统一保存 4 段式 Key:`Microi:${OsClient}:{Category}:{Key}`。V8 代码推荐只传 `{Category}:{Key}`,服务端自动添加当前 `OsClient`;传完整当前租户 Key 用于兼容旧脚本。
92
+
93
+ ```javascript
94
+ // ✅ 推荐:逻辑 Key,运行时自动绑定当前租户
95
+ var k1 = 'User:' + userId;
96
+ var k2 = 'SmsCode:' + phone;
97
+ var k3 = 'Lock:OrderPay:' + orderId;
98
+
99
+ // 兼容:完整当前租户 Key
100
+ var fullKey = 'Microi:' + V8.OsClient + ':User:' + userId;
101
+
102
+ // ❌ 拒绝:不能访问其它租户
103
+ var foreignKey = 'Microi:other-tenant:User:' + userId;
104
+ ```
105
+
106
+ | | 说明 |
107
+ |----|------|
108
+ | `Microi:` | 平台前缀,固定 |
109
+ | `${V8.OsClient}` | 租户隔离 |
110
+ | `{Category}` | 业务分类(User / SmsCode / Lock / Token / ImportStep …) |
111
+ | `{Key}` | 具体业务 Key |
112
+
113
+ > 系统已用前缀(避免冲突):`Microi:${OsClient}:Token:`、`Microi:${OsClient}:User:`、`Microi:${OsClient}:OsClient`、`Microi:${OsClient}:DiyTable:`、`Microi:${OsClient}:Sys:`
114
+
115
+ ## 缓存层级(L1 + L2)
116
+
117
+ 平台内部对系统配置等场景实现了 **L1 进程内缓存 + L2 Redis 缓存**:
118
+
119
+ - L1:进程内静态 `ConcurrentDictionary<string, CacheEntry>`(每个 API 进程独立)
120
+ - L2:当前租户的 Redis(同一租户各 API 节点共享)
121
+
122
+ 缓存组件读取顺序:L1 命中 L2 命中并回填 L1 → 返回未命中。它本身不查询
123
+ 业务数据库;数据库查询与回填属于调用方的 Cache-Aside 流程。
124
+
125
+ 缓存组件写入顺序:L2 Redis 成功 更新本节点 L1 等待 Pub/Sub 失效广播。
126
+ Redis 写入是权威结果;广播短暂失败时其它节点的旧 L1 最迟由本地 TTL 兜底淘汰。
127
+
128
+ > ⚠️ 直接修改数据库未走平台保存流程时,可能绕过缓存失效。优先调用受支持的保存/刷新接口并回读验证;不要把重启容器或清空整个 Redis 当作日常缓存刷新方案。
129
+
130
+ ### FormEngine 授权缓存(Redis epoch + 用户级快照)
131
+
132
+ FormEngine 授权是平台内部安全缓存,不能由业务 V8 直接读写。它既要兼容历史前端 V8 的无 `_SysMenuId` 调用,也要避免每个请求重复查询 `sys_user`、`sys_role`、`sys_rolelimit` 和 `sys_menu`:
133
+
134
+ 1. 每个 `OsClient` 在共享 Redis 中维护单调递增的授权版本 `epoch`。
135
+ 2. 用户授权快照 Key 至少包含 `OsClient + epoch + UserId`,内容包含当前有效用户状态/级别、有效角色、可访问菜单、菜单绑定表、操作权限和数据范围元数据。
136
+ 3. 每个 API 节点可用短 TTL 的进程内 L1 加速;Redis L2 在所有节点间共享。读取顺序为“当前 epoch → L1 用户快照 → L2 用户快照 → 主库冷加载”。
137
+ 4. 冷加载必须查询主库而不是只读副本,防止复制延迟把刚禁用的用户、撤销的角色或旧菜单范围重新写回缓存。并发冷加载可在单节点合并,但正确性仍以 Redis `epoch` 和主库事实为准。
138
+ 5. 用户状态/级别/角色、角色状态、角色菜单/高级表权限、菜单绑定表、菜单权限 JSON、`SqlWhere`、`SqlJoin` / `JoinTables` 等授权事实变更后,必须在写入成功后递增 Redis `epoch`。新旧节点滚动发布期间都通过版本切换自然淘汰旧快照。
139
+ 6. L1 丢失、节点重启或发布不影响正确性;禁止把永久 `static` 字典、单机文件或粘性会话当作授权事实源。短 TTL 只是兜底,不能代替变更时递增 `epoch`。
140
+
141
+ 无菜单客户端请求只使用该快照推断当前用户对目标表的权限;显式 `_SysMenuId` 仍按对应菜单严格精确校验。两种路径都必须在实际 SQL 中应用菜单 `SqlWhere` / `SqlJoin` 数据范围,不能只缓存一个“允许/拒绝”结果后绕过行级范围。
142
+
143
+ ## 基本读写
144
+
145
+ ```javascript
146
+ // 设置缓存(有效期 1 小时)
147
+ V8.Cache.Set('user:' + userId, JSON.stringify(userData), '0.01:00:00');
148
+
149
+ // 读取缓存
150
+ var cached = V8.Cache.Get('user:' + userId);
151
+ if (cached) {
152
+ return { Code: 1, Data: JSON.parse(cached) };
153
+ }
154
+
155
+ // 删除缓存
156
+ V8.Cache.Remove('user:' + userId);
157
+ ```
158
+
159
+ ## Cache-Aside 模式(最常用)
160
+
161
+ 先查缓存,缓存不存在时查数据库并回填缓存。
162
+
163
+ ```javascript
164
+ var cacheKey = 'Product:Detail:' + V8.Param.id;
165
+
166
+ // 1. 先查缓存
167
+ var cached = V8.Cache.Get(cacheKey);
168
+ if (cached) {
169
+ return { Code: 1, Data: JSON.parse(cached) };
170
+ }
171
+
172
+ // 2. 缓存未命中,查数据库
173
+ var result = V8.FormEngine.GetFormData('Product', {
174
+ _Where: [['Id', '=', V8.Param.id]]
175
+ });
176
+
177
+ if (result.Code !== 1 || !result.Data) {
178
+ return { Code: 0, Msg: '数据不存在' };
179
+ }
180
+
181
+ // 3. 回填缓存(有效期 30 分钟)
182
+ V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
183
+
184
+ return { Code: 1, Data: result.Data };
185
+ ```
186
+
187
+ ## 数据更新时清除缓存
188
+
189
+ ```javascript
190
+ // SubmitAfterServerV8.js(数据写入后)清除缓存
191
+ if (V8.FormSubmitAction === 'Upt' || V8.FormSubmitAction === 'Del') {
192
+ V8.Cache.Remove('Product:Detail:' + V8.Form.Id);
193
+ V8.Cache.Remove('Product:List');
194
+ }
195
+ ```
196
+
197
+ ## 列表缓存(含分页)
198
+
199
+ ```javascript
200
+ var pageIndex = parseInt(V8.Param.pageIndex) || 1;
201
+ var pageSize = parseInt(V8.Param.pageSize) || 20;
202
+ var cacheKey = 'Product:List:' + pageIndex + ':' + pageSize;
203
+
204
+ var cached = V8.Cache.Get(cacheKey);
205
+ if (cached) {
206
+ return JSON.parse(cached);
207
+ }
208
+
209
+ var result = V8.FormEngine.GetTableData('Product', {
210
+ _Where: [['Status', '=', 1]],
211
+ _OrderBy: 'SortOrder',
212
+ _PageIndex: pageIndex,
213
+ _PageSize: pageSize
214
+ });
215
+
216
+ var response = { Code: 1, Data: result.Data, DataCount: result.DataCount };
217
+
218
+ // 列表缓存时间短一些(5 分钟)
219
+ V8.Cache.Set(cacheKey, JSON.stringify(response), '0.00:05:00');
220
+
221
+ return response;
222
+ ```
223
+
224
+ ## 防缓存穿透(查询不存在的数据)
225
+
226
+ ```javascript
227
+ var cacheKey = 'User:Detail:' + V8.Param.id;
228
+ var cached = V8.Cache.Get(cacheKey);
229
+
230
+ // 注意:缓存值可能是 "null" 字符串(空对象占位)
231
+ if (cached !== null) {
232
+ if (cached === 'null') {
233
+ return { Code: 0, Msg: '数据不存在' };
234
+ }
235
+ return { Code: 1, Data: JSON.parse(cached) };
236
+ }
237
+
238
+ var result = V8.FormEngine.GetFormData('SysUser', {
239
+ _Where: [['Id', '=', V8.Param.id]]
240
+ });
241
+
242
+ if (result.Code === 1 && result.Data) {
243
+ V8.Cache.Set(cacheKey, JSON.stringify(result.Data), '0.00:30:00');
244
+ return { Code: 1, Data: result.Data };
245
+ } else {
246
+ // 缓存空值,短过期时间防止穿透
247
+ V8.Cache.Set(cacheKey, 'null', '0.00:01:00');
248
+ return { Code: 0, Msg: '数据不存在' };
249
+ }
250
+ ```
251
+
252
+ ## 分布式锁:不要用普通 Cache 拼装
253
+
254
+ `KeyExist → Set → Remove` 不是分布式锁:检查与写入不原子、没有唯一持有者令牌、锁过期后旧持有者会删除新持有者的锁,也无法处理节点暂停、网络分区和滚动发布。
255
+
256
+ V8 业务脚本需要互斥时:
257
+
258
+ 1. 接口引擎使用平台 `LockKey/LockTimeout` 配置;
259
+ 2. Job/Worker 使用带租约、唯一持有者令牌、续租、超时自动释放和“仅持有者可释放”语义的共享锁;
260
+ 3. Key 至少包含 `OsClient + 任务/业务唯一标识`;
261
+ 4. 分布式锁只能减少并发,业务副作用仍必须用幂等键、唯一约束/条件更新、状态机或 outbox/inbox 保证只执行一次。
262
+
263
+ `SetIfNotExists` 只提供原子的“首次写入 + TTL”,没有唯一持有者令牌、续租和
264
+ 仅持有者释放语义。禁止把它或其它普通 Cache 调用拼成分布式锁。
265
+
266
+ ## 原子计数与限流
267
+
268
+ `Get → parseInt → Set` 在并发下会丢计数。普通 Hash 计数可使用 `V8.Cache.HashIncrement`;需要“计数 + 首次设置 TTL + 超限拒绝”的安全限流、日上传配额或金额额度时,应使用平台 `RateLimit` / SecurityGuard 或后端 Redis Lua 原子脚本,并在 Redis 不可用时按风险选择失败关闭。不要在 V8 中用多个普通 Cache 调用模拟原子配额。
269
+
270
+ ## 缓存 Key 命名规范
271
+
272
+ ```
273
+ Microi:{OsClient}:{业务}:{类型}:{标识}
274
+ Microi:myapp:product:detail:xxx-id 单条产品
275
+ Microi:myapp:product:list:1:20 产品列表第1页
276
+ Microi:myapp:user:profile:xxx-id 用户资料
277
+ Microi:myapp:config:system 系统配置
278
+ Microi:myapp:wx:access_token 微信 token
279
+ Microi:myapp:lock:order:xxx-id 订单锁
280
+ Microi:myapp:api:count:userId:date API 调用计数
281
+ ```
282
+
283
+ ## 注意事项
284
+
285
+ - `V8.Cache.Get()` 返回 `null` 表示 key 不存在,返回空字符串 `''` 是合法值
286
+ - `V8.Cache.Set()` 的 value 必须是字符串,对象需要 `JSON.stringify()`
287
+ - 过期时间支持正数秒或 `d.HH:mm:ss` 字符串;不传则不设置业务 TTL
288
+ - Key 命名建议:`Microi:{V8.OsClient}:{分类}:{Key}`,避免跨应用冲突
289
+ - 写操作后即时清除相关缓存,避免脏数据
290
+ - 不要缓存频繁变化的数据(如实时库存),不如每次查库
291
+
292
+ ## 后端批量写入与 Redis Pub/Sub 回压
293
+
294
+ 平台源码中的缓存写入、删除和按模式删除不仅操作 Redis 数据,还会发布跨节点 L1
295
+ 失效通知。批量导入、自动升级和迁移代码必须 `await` 这些异步调用,禁止
296
+ fire-and-forget;否则数千个 `SCAN/DEL/PUBLISH` 会同时进入同一个
297
+ `ConnectionMultiplexer`,表现为 `outstanding` 持续升高、`SocketClosed`,并可能让
298
+ 其它节点继续使用旧缓存。
299
+
300
+ - 同一租户的失效广播要有界并发,短暂连接异常可做有限次数重试;
301
+ - 持续故障的日志应按时间窗口汇总,但不得静默吞掉一致性告警;
302
+ - 每个租户可能使用不同 Redis,订阅初始化状态不得用一个全局 `static bool` 共享;
303
+ - 缓存实例必须保存创建时的准确 `OsClient`,按模式 `SCAN/DEL` 时直接使用该租户连接;禁止根据 Redis DB 编号反推连接,因为不同租户可能在不同服务器上使用相同 DB 编号;
304
+ - 等待发布只解决回压,业务写入和缓存失效仍需保持 `OsClient` 隔离及可重试幂等。
@@ -9,7 +9,7 @@ description: Microi V8 调试与日志指南。用于排查接口引擎、V8 事
9
9
 
10
10
  你正在为 Microi 吾码平台编写 V8 引擎代码,需要在开发/测试/生产环境进行排错。本指南提供调试模式、异常捕获、系统日志、调试输出的标准做法。
11
11
 
12
- MongoDB 运行日志通过 `microi_query_mongodb_logs` 只读查询;必须限制租户、时间窗、页大小和返回字段,不在结果或回答中输出 Token、连接串、Secret 或完整敏感请求体。
12
+ 系统级排查优先读取 `../system-observability/SKILL.md` 并通过 `microi_query_system_observability` 查询统一日志、统计、详情、Trace、热点接口和运行数据;`microi_query_mongodb_logs` 仅保留给只需要旧 Mongo 日志列表的兼容场景。两者都必须限制租户、时间窗、页大小和返回字段,不在结果或回答中输出 Token、连接串、Secret 或完整敏感请求体。
13
13
 
14
14
  ## 三种输出通道
15
15