@microi.net/cli 4.9.6 → 4.9.8

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 (93) 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 -5
  7. package/package.json +1 -1
  8. package/scripts/mcp-server.js +83 -83
  9. package/scripts/microi-cli.js +55 -85
  10. package/scripts/microi-codex-broker.js +418 -0
  11. package/scripts/microi-codex-router.js +129 -65
  12. package/scripts/microi-skills.meta.json +310 -151
  13. package/skills/.microi-skills-version.json +2 -2
  14. package/skills/.progressive-disclosure-manifest.json +3566 -0
  15. package/skills/ai-platform-governance/SKILL.md +21 -166
  16. package/skills/ai-platform-governance/references/progressive-01-/345/212/237/350/203/275/345/274/200/345/205/263.md +190 -0
  17. package/skills/microi-client-frontend/SKILL.md +17 -434
  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 +144 -0
  19. package/skills/microi-client-frontend/references/progressive-02-8-/350/277/220/350/241/214/346/227/266/351/253/230/351/242/221/345/235/221/345/244/215/347/233/230.md +178 -0
  20. 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 +144 -0
  21. package/skills/microi-db-schema/SKILL.md +3 -3
  22. package/skills/microi-db-schema/references/schema-overview.md +1 -1
  23. package/skills/microi-db-schema/references/schema.md +1 -1
  24. package/skills/microi-db-schema/references/table-catalog.md +1 -1
  25. package/skills/microi-form-engine/SKILL.md +1 -1
  26. package/skills/microi-form-layout/SKILL.md +19 -225
  27. package/skills/microi-form-layout/references/progressive-01-3-/344/270/211/347/247/215/345/210/206/347/273/204/347/232/204/345/255/230/345/202/250/344/270/216/351/205/215/347/275/256.md +235 -0
  28. package/skills/microi-frontend-sdk/SKILL.md +17 -151
  29. package/skills/microi-frontend-sdk/references/progressive-01-token-/345/275/223/345/211/215/347/231/273/345/275/225/347/224/250/346/210/267/344/270/216/345/275/223/345/211/215/347/273/210/347/253/257/347/231/273/345/275/225/345/215/217/350/256/256.md +171 -0
  30. package/skills/microi-mobile-app-quality/SKILL.md +22 -288
  31. package/skills/microi-mobile-app-quality/references/progressive-01-4-/351/207/215/350/246/201/346/214/211/351/222/256/345/277/205/351/241/273/345/270/246/345/233/276/346/240/207.md +209 -0
  32. package/skills/microi-mobile-app-quality/references/progressive-02-9-/344/270/273/351/242/230/345/210/207/346/215/242/345/277/205/351/241/273/347/234/237/345/256/236/344/270/224/345/205/250/345/261/200/347/224/237/346/225/210.md +117 -0
  33. package/skills/microi-system-delivery/SKILL.md +16 -380
  34. package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +186 -0
  35. package/skills/microi-system-delivery/references/progressive-02-/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/345/277/205/351/241/273/350/246/206/347/233/226/347/232/204/345/235/221.md +210 -0
  36. package/skills/microi-ui/SKILL.md +19 -169
  37. package/skills/microi-ui/references/progressive-01-/347/247/273/345/212/250/347/253/257/345/234/272/346/231/257/350/223/235/345/233/276.md +183 -0
  38. package/skills/microi-uniapp-frontend/SKILL.md +26 -335
  39. package/skills/microi-uniapp-frontend/references/progressive-01-/347/247/273/345/212/250/347/253/257/345/210/206/347/261/273-/345/217/214/346/240/217/345/210/227/350/241/250/347/213/254/347/253/213/346/273/232/345/212/250.md +225 -0
  40. package/skills/microi-uniapp-frontend/references/progressive-02-/345/205/263/351/224/256/344/270/232/345/212/241/350/265/204/344/272/247/344/270/215/345/276/227/351/273/230/350/256/244/351/200/211/344/270/255.md +154 -0
  41. package/skills/page-engine/SKILL.md +23 -271
  42. package/skills/page-engine/references/progressive-01-/346/211/200/346/234/211/347/273/204/344/273/266/347/261/273/345/236/213.md +234 -0
  43. package/skills/page-engine/references/progressive-02-/347/211/210/346/234/254/345/216/206/345/217/262-/345/271/266/345/217/221/344/277/235/345/255/230/344/270/216/345/233/236/346/273/232.md +60 -0
  44. package/skills/playwright-e2e/SKILL.md +24 -590
  45. package/skills/playwright-e2e/references/progressive-01-/345/205/250/350/207/252/345/212/250/347/231/273/345/275/225-/345/205/215/351/252/214/350/257/201/347/240/201-/344/275/206/344/270/215/345/205/215/345/257/206/347/240/201-/345/277/205/350/257/273.md +173 -0
  46. package/skills/playwright-e2e/references/progressive-02-/346/226/207/345/255/227/345/257/271/346/257/224/345/272/246/344/270/216/345/217/257/350/257/273/346/200/247/350/207/252/345/212/250/345/214/226/346/243/200/346/237/245-/345/277/205/345/201/232.md +183 -0
  47. package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md +221 -0
  48. package/skills/playwright-e2e/references/progressive-04-ci-/345/273/272/350/256/256.md +69 -0
  49. package/skills/scripts/optimize-progressive-disclosure.mjs +204 -0
  50. package/skills/scripts/refresh-progressive-disclosure.mjs +64 -0
  51. package/skills/scripts/validate-progressive-disclosure.mjs +52 -0
  52. package/skills/ui-design/SKILL.md +26 -1461
  53. package/skills/ui-design/references/progressive-01-/351/242/234/350/211/262/344/275/223/347/263/273-css-variables-/346/224/257/346/214/201/344/270/273/351/242/230/345/210/207/346/215/242.md +218 -0
  54. package/skills/ui-design/references/progressive-02-/345/255/227/344/275/223.md +155 -0
  55. package/skills/ui-design/references/progressive-03-/345/212/250/346/225/210/350/247/204/350/214/203-/344/270/260/345/257/214/344/275/206/344/270/215/345/215/241.md +235 -0
  56. package/skills/ui-design/references/progressive-04-/347/273/204/344/273/266/351/243/216/346/240/274/351/200/237/346/237/245.md +152 -0
  57. package/skills/ui-design/references/progressive-05-/347/247/273/345/212/250/347/253/257/344/270/223/347/224/250/350/247/204/350/214/203.md +238 -0
  58. package/skills/ui-design/references/progressive-06-/344/270/273/351/242/230/345/210/207/346/215/242/345/256/236/347/216/260.md +194 -0
  59. package/skills/ui-design/references/progressive-07-/351/200/237/346/237/245-/344/273/216/345/244/264/346/220/255/345/273/272/344/270/200/344/270/252/347/247/273/345/212/250/347/253/257/351/241/265/351/235/242.md +207 -0
  60. package/skills/ui-design/references/progressive-08-/350/241/250/345/215/225/345/210/206/347/273/204/350/247/204/350/214/203-tabs-vs-collapsegroup-/345/274/272/345/210/266.md +142 -0
  61. package/skills/v8-crud-api/SKILL.md +20 -245
  62. package/skills/v8-crud-api/references/progressive-01-/346/237/245/350/257/242/345/210/227/350/241/250-/345/210/206/351/241/265.md +226 -0
  63. package/skills/v8-crud-api/references/progressive-02-where-/346/235/241/344/273/266/350/257/255/346/263/225/351/200/237/346/237/245.md +49 -0
  64. package/skills/v8-export-import/SKILL.md +15 -425
  65. package/skills/v8-export-import/references/progressive-01-excellayout-/351/253/230/347/272/247/350/207/252/347/224/261/345/270/203/345/261/200.md +211 -0
  66. package/skills/v8-export-import/references/progressive-02-powerpoint-/345/257/274/345/207/272.md +202 -0
  67. package/skills/v8-export-import/references/progressive-03-/345/256/211/345/205/250-/346/200/247/350/203/275/346/263/250/346/204/217.md +42 -0
  68. package/skills/v8-file-upload/SKILL.md +16 -354
  69. 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 +227 -0
  70. package/skills/v8-file-upload/references/progressive-02-office-/346/226/207/344/273/266/345/234/250/347/272/277/347/274/226/350/276/221/347/211/210/346/234/254/345/217/267/350/247/204/345/210/231.md +149 -0
  71. package/skills/v8-frontend-events/SKILL.md +19 -205
  72. package/skills/v8-frontend-events/references/progressive-01-/345/210/227/350/241/250/344/272/213/344/273/266.md +219 -0
  73. package/skills/v8-http-integration/SKILL.md +14 -236
  74. package/skills/v8-http-integration/references/progressive-01-get-/350/257/267/346/261/202.md +220 -0
  75. package/skills/v8-http-integration/references/progressive-02-/351/224/231/350/257/257/345/244/204/347/220/206/346/250/241/345/274/217.md +44 -0
  76. package/skills/v8-menu-buttons/SKILL.md +15 -511
  77. package/skills/v8-menu-buttons/references/progressive-01-2-/346/214/211/351/222/256/345/257/271/350/261/241-schema.md +221 -0
  78. package/skills/v8-menu-buttons/references/progressive-02-8-/346/250/241/345/274/217-f-/345/220/216/345/217/260/344/273/273/345/212/241/346/214/211/351/222/256-/351/225/277/344/273/273/345/212/241.md +224 -0
  79. package/skills/v8-menu-buttons/references/progressive-03-10-/345/217/215/346/250/241/345/274/217-/351/201/277/345/205/215.md +104 -0
  80. package/skills/v8-mq-mqtt/SKILL.md +11 -175
  81. package/skills/v8-mq-mqtt/references/progressive-01-v8-mqtt-iot-/347/211/251/350/201/224/347/275/221.md +181 -0
  82. package/skills/v8-security/SKILL.md +16 -329
  83. package/skills/v8-security/references/progressive-01-2-/346/235/203/351/231/220/346/240/241/351/252/214.md +199 -0
  84. package/skills/v8-security/references/progressive-02-7-/346/227/245/345/277/227/350/256/260/345/275/225.md +158 -0
  85. package/skills/v8-table-event/SKILL.md +16 -236
  86. package/skills/v8-table-event/references/progressive-01-informv8-js-/350/241/250/345/215/225/346/211/223/345/274/200/344/272/213/344/273/266.md +216 -0
  87. 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 +46 -0
  88. package/skills/v8-workflow/SKILL.md +19 -160
  89. package/skills/v8-workflow/references/progressive-01-/350/212/202/347/202/271/345/274/200/345/247/213-v8-/344/272/213/344/273/266.md +180 -0
  90. package/skills/workspace-conventions/SKILL.md +29 -361
  91. package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +208 -0
  92. package/skills/workspace-conventions/references/progressive-02-microi-net-api-/346/234/254/345/234/260/345/220/257/345/212/250/347/272/246/345/256/232.md +196 -0
  93. package/skills/workspace-conventions/references/progressive-03-cli-/344/270/216-ide-/346/217/222/344/273/266/351/224/231/347/211/210/345/205/261/345/255/230/347/272/246/345/256/232.md +27 -0
@@ -0,0 +1,227 @@
1
+ # v8-file-upload 详细参考 1
2
+
3
+ > 按需读取;本文件由 SKILL.md 的原章节无损拆分。
4
+
5
+ <!-- microi-progressive:chunk id=v8-file-upload-005 sha256=431aed9f824e7a604e8e1e02e03404cc4940814f352e6afd3f7639a106d1f332 -->
6
+ ## 公有桶 vs 私有桶
7
+
8
+ ### 应用商城 ZIP
9
+
10
+ 应用商城的 AI 应用/微服务资产禁止逐文件 Base64 持久化到数据库。源码/安装包场景可以生成 ZIP;真实在线编译目录优先使用下节的 MCP 流式发布。数据库只保存路径和校验元数据。
11
+
12
+ 在线商城安装的后台任务只传 `StoreId/StoreApiBase/StoreOsClient` 等定位信息,不能复制整行、`Form/Row/Btn` 或 `AppPakcet`。兼容旧 Base64 包的导入器必须按片限制真实上传数量/体积,片间靠已提交的 `AppId + FilePath + Hash` 复用;上传后统计大小优先使用包内 `Size/Sha256`,禁止再次 `FromBase64String` 构造完整字节数组。
13
+
14
+ ```javascript
15
+ var zipResult = V8.Method.CreateZip({
16
+ Entries: [
17
+ { Path: 'index.html', Content: '<html></html>' },
18
+ { Path: 'assets/app.js', FileByteBase64: jsBase64 }
19
+ ],
20
+ MaxFileCount: 20000,
21
+ MaxEntryBytes: 268435456,
22
+ MaxTotalBytes: 2147483648
23
+ });
24
+
25
+ var extractResult = V8.Method.ExtractZip({
26
+ FileByteBase64: zipBase64,
27
+ MaxFileCount: 20000,
28
+ MaxCompressionRatio: 200
29
+ });
30
+ ```
31
+
32
+ `System.IO` 在 Jint 沙箱中被禁止,不能在 V8 代码里直接构造 `MemoryStream/ZipArchive`;必须使用以上受控方法。
33
+
34
+ ### AI 应用编译目录流式发布(首选)
35
+
36
+ 发布 Web、UniApp、MicroService 的 `dist` / H5 编译目录时,必须优先使用 `microi_publish_application_directory_stream`,不要把每个文件读成 Base64 后传给 `ai_app_build`、`microi_publish_microservice` 或普通 JSON 接口。旧工具仅为小文件兼容保留。
37
+
38
+ 标准流程:
39
+
40
+ 1. 先运行不带 `confirmExecution` 的预检。MCP 按流计算 SHA-256,拒绝符号链接、`.git`、`node_modules`、密钥/`.env`、路径穿越、超过 20000 个文件或超过 20 GB 的垃圾目录;默认不发布 `.map`。
41
+ 2. 确认后把 `confirmExecution` 精确设为 `appIdOrKey`。每个文件通过 multipart 原始流进入 `/api/V8Engine/UploadApplicationAssetStream`,不构造整文件 `Buffer`、Base64 或 JSON 文件体。
42
+ 3. 文件只写不可变版本目录。全部成功后,清单确认接口只接收 `Path/Sha256/Size`,由 HDFS Provider 的 `CopyObject` 在服务端复制到 root 与 `latest`;非入口先复制,入口最后复制。
43
+ 4. 历史版本 URL 保留语义版本;分享/在线使用 URL 使用不含版本号的 root 稳定地址。重试必须复用同一版本与摘要,不能覆盖已有但缺少完整性证明的历史对象。
44
+ 5. 该控制面只允许当前 Token 租户的 `Level >= 9999` 交互式管理员;访问密钥会话不得发布。单文件、HTTP/Multipart 和每日额度仍然生效,不能把“使用流”理解成无限上传。
45
+
46
+ 几十 MB **不是** Jint 或 HDFS 的固定上限。旧链路失败的原因是二进制先膨胀为约 `4/3` 的 Base64,又在 JSON、Jint 字符串、.NET 字符串/字节数组之间产生多份累计分配;文件数量、并发和当前进程内存共同决定触发点。描述问题时必须明确“旧 Base64/Jint 发布链路的累计分配”,不得写成“几十 MB 就达到 Jint 硬上限”。HDFS 上传本身应走二进制流。
47
+
48
+ ```json
49
+ {
50
+ "appIdOrKey": "flower-store",
51
+ "versionNo": "v1.2.0",
52
+ "directory": "D:/build/flower-store/dist",
53
+ "entryPath": "index.html",
54
+ "changeSummary": "修复移动端布局",
55
+ "confirmExecution": "flower-store"
56
+ }
57
+ ```
58
+
59
+ 底层断点式单文件工具是 `microi_upload_application_asset_stream`。除诊断或精确恢复单文件外,不要只调用它而遗漏最终清单确认,否则稳定入口不会切换。
60
+
61
+ | 类型 | `Limit` | 访问 URL | 用途 |
62
+ |------|---------|---------|------|
63
+ | 公有桶 | `false` | 直接拼接 `V8.SysConfig.FileServer + Path` | 产品图、Banner、公开文档 |
64
+ | 私有桶 | `true` | 必须用 `V8.Method.GetPrivateFileUrl` 获取临时 URL | 合同、身份证、敏感数据 |
65
+
66
+ ### 默认 MinIO 桶名与安装验收
67
+
68
+ - Microi 一键安装的默认私有桶固定为 `mci-private`,默认公有桶固定为 `mci-public`;禁止使用 `mci-publish` 等近似名称。
69
+ - `MinIOEndPointInternet` / `MinIOPrivateEndPoint` 同时兼容 `host:port` 与 `http(s)://host:port`。Provider 必须先归一化为 Host、Port、UseSsl,再调用 MinIO SDK 的 host/port 重载;不得把包含协议的整串 URL 直接作为 hostname,否则会出现 `Invalid URI: The hostname could not be parsed.`。显式 URL 的协议优先于历史 SSL 开关;端点禁止携带用户名密码、桶路径、查询或片段。
70
+ - 安装脚本创建 `mci-public` 后必须设置匿名下载权限,并把 `HDFS=MinIO`、内外网端点、AccessKey/SecretKey、`MinIOPrivateBucketName=mci-private`、`MinIOPublicBucketName=mci-public` 同步写入当前租户的 `sys_osclients`。
71
+ - 安装脚本还必须同步当前有效 `sys_config`:`ApiBase` 使用对外可访问的 API 端口,`FileServer` 使用 `http://<访问IP>:<MinIO API端口>/mci-public`。`ApiBase` 不能误用 Web 前端端口,因为 V8 代码会直接在其后拼接 `/api/...` 或 `/apiengine/...`。
72
+ - 安装验收必须使用真实登录 Token 分别执行一次 `Limit=false` 和 `Limit=true` 上传:公有文件匿名访问应返回 `200`,私有文件匿名访问应返回 `403`,私有文件通过签名 URL 访问应返回 `200`,并核对下载内容与上传内容一致。
73
+
74
+ ### 复盘:签名 HEAD 被代理转换为 GET 导致上传后回读误报
75
+
76
+ - 触发场景:`PutObject` 已返回成功、对象可通过 GET 下载,但公有桶和私有桶的上传后 `StatObject` 均对桶根路径返回 `AccessDenied`;常见于启用严格回读校验后,MinIO Endpoint 前的 Nginx 同时启用了缓存与默认的 `proxy_cache_convert_head on`。
77
+ - 根因判断:S3 SigV4 的签名包含 HTTP 方法;代理把客户端签名的 HEAD 转为上游 GET 后会造成签名不一致。另一个可能原因是对象级凭据缺少桶级 `ListBucket` / `GetBucketLocation` 权限,因此不能只凭 `AccessDenied /bucket/` 推断对象未落盘,也不能把空 Region 当作唯一原因。
78
+ - 通用规则:优先在 MinIO 代理位置设置 `proxy_cache_convert_head off`;若仍使用缓存,缓存键需区分 `$request_method`。平台不得跳过上传后回读:当 HEAD/Stat 失败时,使用同一凭据生成签名 GET,并以 `Range: bytes=0-0` 回读;禁用重定向,非空对象必须同时验证期望总长度与首字节,空对象验证长度为零,`404` 判不存在,`403`、网络错误和证据不足继续失败关闭。禁止记录或返回带签名查询参数的 URL。
79
+ - 配置边界:只有实时回读证明 Endpoint、桶名、Region 或凭据确实错误时才修改 SaaS 配置;对象 GET 正常而仅 HEAD 失败时应修复代理或兼容回读路径,不能猜测内网地址、降低校验强度或轮换正常凭据。
80
+ - 自动化检查:覆盖签名 GET 的单字节 Range、期望总长度、首字节实际读取、空对象、长度不符、重定向、`403` 和 `404`;真实环境同时验证公有桶与私有桶的 `Put -> Range GET -> 内容一致`,并对比相同签名在 GET 与 HEAD 方法下的响应。
81
+
82
+ ### 复盘:旧空库缺少可选字段导致 MinIO 初始化后中断
83
+
84
+ - 触发场景:MinIO 容器、私有桶和公有桶均已成功创建,但安装器更新 `sys_osclients` 时因旧库缺少 `NetworkIsInternet` 返回 `Unknown column`,整套安装停在 API 部署之前。
85
+ - 根因:安装器在 API/Upgrade 尚未启动时依赖了并非 MinIO 必需、且存量数据库不保证存在的旧可选字段;同时只按 `OsClient` 更新且没有写后回读。
86
+ - 通用规则:MinIO 安装前先校验真正必需的物理字段,并按 `OsClient + OsClientType + OsClientNetwork + IsEnable + IsDeleted` 唯一定位运行租户。内外网端点由 API 允许的启动项 `OsClientNetwork` 选择,安装器不得再写 `NetworkIsInternet`;配置更新后逐字段回读一致才继续。
87
+ - 恢复规则:桶初始化成功而配置写入失败不需要删除桶或数据卷。中断的新安装应先备份现有 Compose 并记录绑定数据目录,只对对应编排执行不带 `-v` 的 `docker compose down`,保留数据恢复点后再使用最新版脚本;禁止直接删除数据库或对象存储目录。
88
+ - 自动化检查:用一个不含 `NetworkIsInternet`、但包含 MinIO 必需字段的临时 MySQL 表执行 schema、唯一租户、UPDATE 和回读闭环;再插入重复三参数租户,断言安装器失败关闭且不批量覆盖。
89
+
90
+ ### 复盘:MinIO 已可上传但系统设置仍指向官方地址
91
+
92
+ - 触发场景:一键安装和桶初始化均成功,用户手工上传也成功,但读取系统设置时发现 `ApiBase`、`FileServer` 仍是空库模板中的官方地址。
93
+ - 根因:安装流程只回写了 `sys_osclients` 的 MinIO 配置,没有同步前端和 V8 公共使用的 `sys_config` 地址字段。
94
+ - 通用规则:数据库还原并创建默认桶后,必须按安装模式选择的访问 IP 和动态端口同时回写有效 `sys_config.ApiBase/FileServer`;其中 `ApiBase` 指向 API 服务,`FileServer` 指向公有桶根地址。
95
+ - 自动化检查:安装完成后通过 `GetSysConfig` 回读两个字段,断言均使用本次访问 IP 和实际端口;再执行公有上传并使用 `FileServer + Path` 匿名下载,内容必须一致。
96
+
97
+ 公开页面图片(首页 banner、商品主图、公开活动头像等)应返回公有 URL,例如 `V8.SysConfig.FileServer + Path`。不要把公有图片统一转成 `GetPrivateFileUrl` 的 `static-private` 签名地址;部分 H5/浏览器会因响应头或跨域策略触发 ORB/CORS 拦截,表现为 uni-app `<image>` 内层 `background-image: none`。
98
+
99
+ ### `sys_user.Avatar` 固定使用私有桶
100
+
101
+ - `sys_user` 是内部系统用户表,`Avatar` 可能暴露员工身份信息,因此字段配置必须保持 `ImgUpload.Limit=true`,上传端也必须显式传 `Limit:true`;自定义用户管理页不能因为绕过表单引擎而回退到公有上传。
102
+ - 数据库继续只保存租户内相对路径,例如 `/tenant_demo/avatar/20240218/user.png`。历史公有头像迁移时,应把同一文件复制到私有桶的同一路径,确认私有对象可访问后再停用公有访问;不得为了迁移批量改写路径或制造重复日期目录。
103
+ - PC、移动端、聊天、工作流等任何页面渲染 `sys_user.Avatar` 时,禁止 `FileServer + Avatar`、`GetServerPath(Avatar)` 或把相对路径直接交给 `<img>`。前端应调用 `DiyCommon.GetUserAvatarUrl(avatar, userId)`,接口/V8 应调用 `V8.Method.GetPrivateFileUrl({ FilePathName: avatar })`,并为临时 URL 设置短期缓存与失败占位图。
104
+ - `ContactUserAvatar`、`FromUserAvatar`、`SenderAvatar` 等从 `sys_user.Avatar` 派生的快照字段同样按私有路径处理;消息数据只保存原始相对路径,不能把会过期的临时 URL 持久化到数据库。
105
+
106
+ #### 复盘:字段改私有后卡片仍访问公有桶
107
+
108
+ - 触发条件:把 `sys_user.Avatar` 改为 `Limit=true`,但模块卡片仍配置 `TableCardImgField=Avatar`。
109
+ - 根因:通用卡片渲染器只读取了字段值,没有读取图片字段的 `Config.ImgUpload.Limit`,仍统一调用 `GetServerPath/FileServer`。
110
+ - 修复规则:通用图片渲染器必须同时检查字段配置;私有字段先异步换取临时地址,`sys_user.Avatar` 还要有表名+字段名语义兜底,避免元数据缓存短暂陈旧时泄露到公有路径。
111
+ - 验收断言:筛选一条有头像的系统用户,页面中 `/tenant_demo/avatar/` 的直接公有请求数必须为 0,私有代理图片 `naturalWidth > 0`,并同时验证无头像占位图不报错。
112
+
113
+ 定制移动端项目的 Hero、Banner、音频、视频、字体等大资源也应优先上传到目标租户公有 HDFS,再通过 FileServer/CDN 引用;小型导航图标和离线关键素材才保留在主包。上传前可适度压缩,但必须在多尺寸截图或试听/试播中确认质量,禁止以明显失真换取包体扫描通过。完整移动端规则见 `microi-uniapp-frontend/SKILL.md`。
114
+
115
+ 后台 `ImgUpload` 字段通常保存相对路径或 JSON:接口返回给移动端前先解析出 `Path`,再按公私有场景转换 URL:
116
+
117
+ ```javascript
118
+ function publicFileUrl(path) {
119
+ if (!path) return '';
120
+ if (/^https?:/i.test(path)) return path;
121
+ return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '');
122
+ }
123
+ ```
124
+
125
+ ### 私有桶临时 URL
126
+
127
+ ```javascript
128
+ var url = V8.Method.GetPrivateFileUrl({
129
+ FilePathName: '/private/contract/2024/abc.pdf',
130
+ OsClient: V8.OsClient, // 可选,默认当前
131
+ Expires: 600 // 可选,过期秒数
132
+ });
133
+ // 后端审计代理 URL,过期不可访问;真实对象存储签名 URL 不会返回前端
134
+ ```
135
+
136
+ - 普通客户端调用 `/api/HDFS/GetPrivateFileUrl` 时,不能只提交 `FilePathName`,必须同时提交 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`。服务端校验菜单、菜单绑定表、记录数据范围、字段归属以及字段值确实引用该路径后,才签发临时票据。
137
+ - `FieldId` 必须属于目标表,且组件为 `FileUpload` 或 `ImgUpload`;`SysMenuId` 必须是当前用户真实拥有、并绑定目标表的菜单。
138
+ - 普通用户禁止通过该入口直接取得私有文件 `Byte` / `Stream`。签发失败时不能回退裸路径、真实对象存储签名地址或公有 URL。
139
+ - 私有文件访问必须经过后端短期票据代理:签发链接时记录当前登录用户,实际 `GET/HEAD` 打开或下载时再记录一次访问行为;支持 `Range` 流式响应,并对同一次分片请求做短时去重,不能把文件完整读入内存。
140
+ - 审计代理由平台后端回源对象存储,必须使用服务端内网端点生成上游地址,不能先生成公网 MinIO 签名地址再让后端绕公网回源。否则同一对象经内网上传成功后,可能在公网端点表现为 404。
141
+ - 备份包、安装包、导出包等重要大文件不能只信任 `PutObject` 成功响应;必须在写入后通过同一私有桶和内网端点执行 `ObjectExist` 回读(可行时再核对大小或 SHA-256),通过后才能把业务记录标记为完成。
142
+ - 平台任务若绕过 `V8.Method.Upload` 直接使用底层 `PutObject`,必须保证写入对象键与后续 `V8.Method.GetPrivateFileUrl` 的租户前缀规则一致。像 `/database-backups/` 这种服务端保留目录只能做严格白名单规范化,禁止对普通租户文件泛化去除 `/{OsClient}/` 隔离前缀。
143
+ - 下载验收至少对新签发地址执行一次 `Range: bytes=0-0`,断言返回 `200/206`、内容长度大于 0 且不是 JSON 错误;完整交付再核对下载字节数或 SHA-256。若产品要求保留当前业务页,应在新浏览上下文打开代理地址,并在浏览器拦截弹窗时给出品牌化反馈。
144
+ - 用户把私有链接转发给别人后,接收者没有有效登录身份时按“匿名访问”记录,禁止根据签发人猜测实际访问人;票据仍按原有效期失效。
145
+ - 代理创建或包装失败时必须失败关闭,不得退回未经审计的真实签名 URL;行为日志中也禁止保存真实签名 URL、Token、Authorization 或存储密钥。
146
+ - `Limit:false` 的公有文件允许通过 CDN/公有桶直接访问,不要求记录用户行为日志,也不要为了审计强制改走私有代理。
147
+
148
+ <!-- /microi-progressive:chunk -->
149
+ <!-- microi-progressive:chunk id=v8-file-upload-006 sha256=8d4fedab2418c7df7973b78d4b9d6c5fa77907929d401800689cd1b5b468f32f -->
150
+ ## 接口直接响应文件(下载/导出)
151
+
152
+ 接口引擎需要在配置中开启【**响应文件**】选项,然后返回特殊结构:
153
+
154
+ 平台后端会统一处理响应文件:图片和 PDF 自动 `inline` 在浏览器中打开,其它类型默认下载;PDF、PNG、JPEG、GIF、WebP、AVIF、BMP、TIFF、ICO、SVG 等常见可预览类型会自动校验文件头。V8 代码不要手写复杂的文件头判断,只需要保证 `ContentType` 与真实文件内容一致。
155
+
156
+ ```javascript
157
+ // 模板:导出 Excel
158
+ var excelResult = V8.Office.ExportExcel({...});
159
+ return {
160
+ Code: 1,
161
+ Data: {
162
+ FileName: 'export_' + DateNow('yyyyMMdd_HHmmss') + '.xls',
163
+ ContentType: 'application/vnd.ms-excel',
164
+ FileByteBase64: System.Convert.ToBase64String(excelResult.Data)
165
+ }
166
+ };
167
+
168
+ // 模板:返回图片
169
+ var resp = V8.Http.GetResponse({ Url: 'https://example.com/img.png' });
170
+ return {
171
+ Code: 1,
172
+ Data: {
173
+ FileName: 'img.png',
174
+ ContentType: 'image/png',
175
+ FileByteBase64: System.Convert.ToBase64String(resp.RawBytes)
176
+ }
177
+ };
178
+
179
+ // 模板:返回 PDF(浏览器直接预览;后端自动校验 %PDF- 文件头)
180
+ var pdfResp = V8.Http.GetResponse({ Url: 'https://example.com/report.pdf' });
181
+ return {
182
+ Code: 1,
183
+ Data: {
184
+ FileName: 'report.pdf',
185
+ ContentType: 'application/pdf',
186
+ FileByteBase64: System.Convert.ToBase64String(pdfResp.RawBytes)
187
+ }
188
+ };
189
+ ```
190
+
191
+ 常用 ContentType:
192
+ - `application/vnd.ms-excel` / `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
193
+ - `application/pdf`
194
+ - `image/png` / `image/jpeg`
195
+ - `application/octet-stream`(通用二进制)
196
+
197
+ 注意:如果远程系统返回的是错误页、登录页、业务容器格式(例如金蝶 PLM 电子仓 `KD_C_PLM`、或其它文件头不是 `%PDF-` 的伪 PDF),不要在 V8 里伪装成 PDF。后端会返回 JSON 错误,包含 `ExpectedFirstAscii`、`ActualFirstAscii`、`ActualFirstHex`、`Length`,按这些信息排查上游下载接口。
198
+
199
+ <!-- /microi-progressive:chunk -->
200
+ <!-- microi-progressive:chunk id=v8-file-upload-007 sha256=59ebcaa605d26def44cd027c0975a01fffee8670a890b4ed18b4138527be8a1b -->
201
+ ## 通过 URL 列表批量下载并入库
202
+
203
+ ```javascript
204
+ var urls = V8.Param.urls; // ['https://...', 'https://...']
205
+ var savedPaths = [];
206
+ for (var i = 0; i < urls.length; i++) {
207
+ try {
208
+ var resp = V8.Http.GetResponse({ Url: urls[i], Timeout: 30 });
209
+ if (resp.StatusCode !== 200) continue;
210
+
211
+ var base64 = System.Convert.ToBase64String(resp.RawBytes);
212
+ var fileName = V8.Method.NewGuid() + '.bin';
213
+ var up = V8.Method.Upload({
214
+ FilesByteBase64: { [fileName]: base64 },
215
+ Limit: false,
216
+ Path: '/batch-import/' + DateNow('yyyy-MM-dd'),
217
+ OsClient: V8.OsClient
218
+ });
219
+ if (up.Code === 1) savedPaths.push(up.Data[0].Path);
220
+ } catch (ex) {
221
+ console.error('第' + (i + 1) + '个下载失败:' + ex.message);
222
+ }
223
+ }
224
+ return { Code: 1, Data: savedPaths };
225
+ ```
226
+
227
+ <!-- /microi-progressive:chunk -->
@@ -0,0 +1,149 @@
1
+ # v8-file-upload 详细参考 2
2
+
3
+ > 按需读取;本文件由 SKILL.md 的原章节无损拆分。
4
+
5
+ <!-- microi-progressive:chunk id=v8-file-upload-008 sha256=73506d076e7917db846f615cd9c0d899bb8b58a3917f375bce9eff6ea903e8a0 -->
6
+ ## Office 文件在线编辑版本号规则
7
+
8
+ 当文件上传控件开启【Office 在线预览】、【允许在线编辑】和【开启 Office 文件版本号】时,前后端必须遵循统一版本规则:
9
+
10
+ - 新上传的 Office 文件(`pdf/doc/docx/xls/xlsx/ppt/pptx`)要立即写入初始版本 `v1.0.0`,字段 JSON 的 `Path` 指向该原始文件,`Version` 为 `v1.0.0`,`Versions[0]` 保存同一份原始文件路径。
11
+ - 用户进入 OnlyOffice 在线编辑页后,每次手动点击【保存文件】才生成新版本;第一次保存生成 `v1.0.1`,之后依次生成 `v1.0.2`、`v1.0.3`。
12
+ - 未开启版本号时,保存文件直接覆盖当前 `Path` 对应的 HDFS/OSS 源文件。
13
+ - 开启版本号时,保存文件必须生成带版本号后缀的新文件,例如 `contract_v1.0.1.docx`,字段 JSON 的 `Path` 指向最新版本,`Versions` 保留历史版本路径。
14
+ - 在线 Office 路由和文件上传字段都要能读取 `Versions`,用于右上角切换历史版本预览/编辑。
15
+
16
+ ### OnlyOffice 服务端取文件与匿名预览规则
17
+
18
+ - OnlyOffice 文档服务器会在服务端再次下载文件。浏览器可以下载但 OnlyOffice 提示“下载失败”时,优先检查生成地址是否为 `localhost/127.0.0.1/内网域名`。
19
+ - OnlyOffice 可能先对文档地址发起 `HEAD` 探测。响应文件接口除了 `GET 200`,还必须让 `HEAD` 返回相同的 `Content-Type/Content-Length/Content-Disposition`,不能返回 `405`。
20
+ - 私有文件在线预览调用 `GetPrivateFileUrl` 或 `/api/HDFS/GetPrivateFileUrl` 时传 `ForOfficePreview:true`。审计代理应优先使用租户系统配置的公网 `ApiBase`,但仍把真实对象存储签名地址保存在共享 Redis ticket 中,禁止直接返回真实签名地址。
21
+ - `/online-office` 可以匿名访问。公有存储模式只允许当前 `OsClient` 目录下的 `filePathName`;接口模式通过 `fileUrl` 接收当前平台正式 `ApiBase`,或由同端口本地后端读取的 loopback `/apiengine/...` 响应文件地址,并要求 URL 显式携带当前 `OsClient`。两种模式都拒绝跨租户、路径穿越和任意第三方 URL;`isPrivate=1` 必须登录。
22
+ - 匿名接口引擎预览不要求 V8 代码先上传 HDFS:接口开启【响应文件】和【允许匿名】后,完整地址 URL 编码传给 `fileUrl`,同时传 `fileName/fileType`。页面通过匿名安全中转让当前后端限域读取源文件,并以确定性路径缓存到当前租户公有对象存储;OnlyOffice 只接收公网 `FileServer` 地址。loopback 仅允许同端口本地后端访问,禁止开放任意 URL 代理。
23
+ - `canEdit` 只是前端请求参数,不是权限。最终编辑条件必须是“有效登录态 && canEdit=true”;匿名始终只读,不能因 URL 参数放开编辑。
24
+ - 匿名预览页隐藏左侧菜单、顶部导航和页签;已登录用户保持原系统布局。
25
+ - 匿名导出响应接口应配置频率限制或保证生成逻辑足够轻量,不能让单个公网 URL 无界消耗 CPU/内存;如业务仍需要落盘缓存,缓存事实必须进入共享 Redis/HDFS,不能用进程内变量。
26
+
27
+ 字段 JSON 示例:
28
+
29
+ ```json
30
+ {
31
+ "Name": "contract.docx",
32
+ "Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx",
33
+ "Version": "v1.0.1",
34
+ "Versions": [
35
+ { "Version": "v1.0.0", "Name": "contract.docx", "Path": "/tenant_demo/file/20260622/contract.docx", "IsLatest": false },
36
+ { "Version": "v1.0.1", "Name": "contract_v1.0.1.docx", "Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx", "IsLatest": true }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ <!-- /microi-progressive:chunk -->
42
+ <!-- microi-progressive:chunk id=v8-file-upload-009 sha256=50fba71370f45de3dd677f5731f3956c328a8d3b1ca2125b853128067f9af40a -->
43
+ ## ImgUpload / FileUpload 字段值兼容规则
44
+
45
+ `ImgUpload` 不能假设只是一种值结构。PC 表单、移动端、旧数据、单图/多图、公开/私有桶会混合出现以下格式:
46
+
47
+ | 场景 | 可能的值 |
48
+ |------|----------|
49
+ | 空值/占位 | `''`、`null`、`undefined`、`'[]'`、`'null'`、`'正在上传中...'` |
50
+ | 旧单图 | `'/upload/xxx/a.png'`、`'https://cdn/a.png'` |
51
+ | 新单图 | `{ Path, Name, Size, Id, State }` 或 JSON 字符串 `'{"Path":"..."}'` |
52
+ | 多图 | `[{ Path, Name, Id, State }]` 或 JSON 字符串 `'[{"Path":"..."}]'` |
53
+ | 其它兼容字段 | `Path`、`FilePathName`、`FullPath`、`Url`、`url`、`src` |
54
+
55
+ 任何端(PC、uni-app、H5、小程序)渲染图片前都必须先做“归一化 -> 取 Path -> 转最终 URL”,不要直接 `JSON.parse` 后只处理数组,也不要直接把字段值拼到 `FileServer`。
56
+
57
+ 推荐归一化:
58
+
59
+ ```javascript
60
+ function normalizeUploadValue(value) {
61
+ if (value == null || value === '' || value === 'undefined' || value === 'null') return [];
62
+ if (value === '正在上传中...' || value === '[]' || value === '[ ]') return [];
63
+
64
+ var raw = value;
65
+ if (typeof raw === 'string') {
66
+ var s = raw.trim();
67
+ if ((s.indexOf('{') === 0 || s.indexOf('[') === 0)) {
68
+ try { raw = JSON.parse(s); } catch (e) { raw = s; }
69
+ } else {
70
+ raw = s;
71
+ }
72
+ }
73
+
74
+ if (Array.isArray(raw)) {
75
+ return raw.map(normalizeUploadItem).filter(function (it) { return !!it.Path; });
76
+ }
77
+
78
+ var one = normalizeUploadItem(raw);
79
+ return one.Path ? [one] : [];
80
+ }
81
+
82
+ function normalizeUploadItem(item) {
83
+ if (!item) return {};
84
+ if (typeof item === 'string') {
85
+ return { Path: item, Name: item.split('/').pop() || item, State: 1 };
86
+ }
87
+ if (typeof item === 'object') {
88
+ var path = item.Path || item.FilePathName || item.FullPath || item.Url || item.url || item.src || '';
89
+ return {
90
+ Id: item.Id || item.id || '',
91
+ Name: item.Name || item.FileName || item.name || (path ? String(path).split('/').pop() : ''),
92
+ Size: item.Size || item.size || '',
93
+ CreateTime: item.CreateTime || item.createTime || '',
94
+ State: item.State == null ? 1 : item.State,
95
+ Path: path
96
+ };
97
+ }
98
+ return {};
99
+ }
100
+ ```
101
+
102
+ 公开图片 URL 解析原则与 `Microi.Client/src/utils/diy.common.js` 的 `GetServerPath` 一致:
103
+
104
+ ```javascript
105
+ function publicUploadUrl(path) {
106
+ if (!path) return '';
107
+ var s = String(path).trim();
108
+ if (!s || s === '正在上传中...') return '';
109
+ if (s.indexOf('.') === 0) return s; // ./static/img/loading.gif 等本地静态资源
110
+ if (/^(https?:|data:|blob:)/i.test(s)) return s; // 已经是最终 URL
111
+ if (s.indexOf('{') === 0 || s.indexOf('[') === 0) {
112
+ var list = normalizeUploadValue(s);
113
+ s = list.length ? list[0].Path : '';
114
+ }
115
+ if (!s) return '';
116
+ return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + s.replace(/^\/+/, '');
117
+ }
118
+ ```
119
+
120
+ 私有桶(`Limit === true`)不要拼 `FileServer`,必须把归一化后的 `Path` 传给 `V8.Method.GetPrivateFileUrl({ FilePathName: path })` 或后端签名接口换临时 URL。
121
+
122
+ <!-- /microi-progressive:chunk -->
123
+ <!-- microi-progressive:chunk id=v8-file-upload-010 sha256=f34d476435c3a8321df017b481771d757f580c8d75f7648fdd54ee1c624d116c -->
124
+ ## 安全注意
125
+
126
+ - ❌ 不要让前端任意指定 `Path`(路径穿越风险),只允许后端固定路径
127
+ - ❌ 不要不校验文件类型 / 大小:根据 ContentType + 后缀双重校验
128
+ - ❌ 不要把持有 Token 当成私有文件授权;普通用户必须证明菜单、记录和字段引用关系
129
+ - ❌ 不要向普通角色开放文件列表、移动、重命名、删除、覆盖等管理接口;这些接口仅限 `Level >= 9999`
130
+ - ❌ 不要开启 UEditor `catchimage` 远程抓图;默认关闭,确需采集时另建带域名白名单、DNS/IP 校验、禁止跳转、超时和响应上限的受控接口
131
+ - ❌ 敏感文件(合同、身份证)必须用私有桶 `Limit: true`
132
+ - ✅ 公有桶 URL 可缓存到前端,私有桶临时 URL 每次重新生成
133
+ - ✅ 删除数据时同步清理 HDFS 文件(避免存储泄漏)
134
+ - ✅ Excel/PDF 等导出文件通过【响应文件】配置返回,不要拼接到 JSON 数据里
135
+
136
+ ### 安全升级兼容
137
+
138
+ - 旧页面如果只传 `FilePathName` 获取私有地址,升级后普通帐号会失败;必须补齐 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`,不能改回匿名或放开管理权限。
139
+ - 旧自定义上传若依赖普通用户设置 `Limit:false` 或任意多级 `Path`,应改成私有上传;公开资源改走受控发布流程。
140
+ - 历史公有文件不会自动变成私有文件。迁移时先复制对象、验证私有读取,再停止旧公有访问;数据库继续保存租户内相对路径,不能持久化临时 URL。
141
+ - 上线验收至少覆盖:普通角色跨菜单/跨记录/跨字段读取被拒绝、单文件/单请求/数量限制、帐号与租户配额、Redis 故障失败关闭、多节点并发、公有匿名 `200`、私有匿名 `403`、授权私有访问 `200`。
142
+
143
+ ### 复盘:ZIP 发布端与安装端 SHA256 口径不一致
144
+
145
+ - 触发场景:应用包已成功生成 ZIP 和摘要,但安装端下载 ZIP 后调用不存在的哈希辅助函数,或尝试通过当前 V8 环境不可用的 `.NET SHA256.Create()` 校验,导致安装中断。
146
+ - 根因:发布端实际使用 `V8.EncryptHelper.Sha256Hex(FileByteBase64)`,安装端却按原始字节设计了另一套实现,函数名称、输入数据和运行时能力均未对齐。
147
+ - 通用规则:文件摘要必须在包清单中记录算法和输入口径;当前应用 ZIP 统一使用 `SHA256-Base64Text`,发布端与安装端都对同一份 Base64 文本调用 `V8.EncryptHelper.Sha256Hex`。禁止仅凭函数名推测算法,也不要在未验证 V8 互操作能力时直接实例化 `.NET` 加密对象。
148
+ - 自动化检查:生成同时包含源码 ZIP、编译 ZIP 的应用包,再从官方地址下载并安装;分别篡改 Base64 文本和摘要,正常包应安装成功,两个篡改包都必须在解压前被拒绝。
149
+ <!-- /microi-progressive:chunk -->
@@ -13,6 +13,8 @@ description: Microi 前端 V8 事件与客户端能力指南。用于编写浏
13
13
  > **菜单按钮事件**(MoreBtns/FormBtns 等)见 `v8-menu-buttons/SKILL.md`。
14
14
  > 本文重点是 **字段事件、按钮事件、列表事件、模板引擎、其它前端钩子**。
15
15
 
16
+ <!-- microi-progressive:begin -->
17
+ <!-- microi-progressive:chunk id=v8-frontend-events-000 sha256=df2662a9a006d35d989d7ff7316890ffff2c08e422d0e88033f44cdd40210c88 -->
16
18
  ## 能力路由
17
19
 
18
20
  - 查询前端 V8 全部上下文、导航、表单、列表、网络、引擎与工具入口时,读取 `../v8-utilities/references/client-api-index.md`。
@@ -21,6 +23,8 @@ description: Microi 前端 V8 事件与客户端能力指南。用于编写浏
21
23
  - 扫码使用 `V8.Method.ScanCode`,结果从 Promise/回调取得;`V8.ScanCodeRes` 只作兼容结果槽,详见客户端 API 索引。
22
24
  - 登录后的敏感操作使用 `V8.Identity.Verify` 完成 Passkey/严格人脸交互;前端只取得一次性 Ticket,后端接口引擎必须重算 `ActionHash` 并原子消费,不能把前端成功当作授权。
23
25
 
26
+ <!-- /microi-progressive:chunk -->
27
+ <!-- microi-progressive:chunk id=v8-frontend-events-001 sha256=a9f8e1aed64db4b57ff762eb557c0d764b5f703a722dc080d273c4111afe234f -->
24
28
  ## 字段事件(在【字段属性】中配置)
25
29
 
26
30
  ### FieldValueChange — 值变更事件(最常用)
@@ -105,6 +109,8 @@ V8.OpenAnyTable({
105
109
 
106
110
  `ReadOnlyButton` 的产品文案是【禁用插槽按钮】:只控制按钮是否可点击,不等同于字段只读,应保留用于权限和状态控制。
107
111
 
112
+ <!-- /microi-progressive:chunk -->
113
+ <!-- microi-progressive:chunk id=v8-frontend-events-002 sha256=986a67e78b0274941c6f2ab6589b06b8545f8f3261d4972dc56d4061b019d799 -->
108
114
  ## 按钮事件
109
115
 
110
116
  ### V8BtnRun — 按钮点击执行(菜单按钮、表单按钮)
@@ -143,219 +149,20 @@ return V8.Form.Status === '待审核' && V8.CurrentUser.RoleName.indexOf('审批
143
149
  代码在列表可用、进入嵌套表单后会报 `await is only valid in async functions`。回归测试
144
150
  至少覆盖“主表详情 → 定制子表 → 子记录详情”链路中的含 `await` 显隐代码。
145
151
 
146
- ## 列表事件
147
-
148
- ### TableRowClick — 行点击
149
-
150
- ```javascript
151
- // V8.Form === 被点击的行
152
- console.log('点击行:', V8.Form.Id);
153
- // 自定义跳转
154
- V8.OpenAnyForm({ TableName: 'OrderDetail', Id: V8.Form.Id, FormMode: 'View' });
155
- ```
156
-
157
- ### OpenTableBefore — 打开列表前(拦截/初始化筛选)
158
-
159
- ```javascript
160
- // 固定弹出表格的可选数据范围;搜索、高级筛选、分页不会移除此条件
161
- V8.OpenTableSetWhere(V8.Field.CustomerId, [
162
- ['OwnerId', '=', V8.CurrentUser.Id]
163
- ]);
164
- ```
165
-
166
- ### OpenTableSubmit — 列表查询提交前(追加条件)
167
-
168
- ```javascript
169
- // V8.Param 是即将发起查询的参数
170
- V8.Param._Where = V8.Param._Where || [];
171
- V8.Param._Where.push(['DeptId', '=', V8.CurrentUser.DeptId]);
172
- ```
173
-
174
- ### PageTab — 页签切换
175
-
176
- ```javascript
177
- // PageTab:"待办"
178
- V8.SearchSet({ Status: '待办' });
179
- V8.RefreshTable({ _PageIndex: 1 });
180
- ```
181
-
152
+ <!-- /microi-progressive:chunk -->
153
+ <!-- microi-progressive:chunk id=v8-frontend-events-003 sha256=68aab95ab02bfca93b69eaea1c188650cb7ef13f8566bee81a63c660a4437512 -->
182
154
  ## 模板引擎事件
183
155
 
184
156
  `TableTemplateEngine` / `FormTemplateEngine` — 见 `v8-template-engine/SKILL.md`。
185
157
 
158
+ <!-- /microi-progressive:chunk -->
159
+ <!-- microi-progressive:chunk id=v8-frontend-events-004 sha256=2db34699ad5433a8c8b194c9ce9909ed4d420d257dd8c6532238215402499c82 -->
186
160
  ## 工作流事件(前端)
187
161
 
188
162
  `WFNodeEnd` — 流程节点结束后前端通知。详见 `v8-workflow/SKILL.md`。
189
163
 
190
- ## 常用前端 API
191
-
192
- | API | 说明 |
193
- |-----|------|
194
- | `V8.Tips(msg, ok?)` | 浮层提示。`ok=true` 绿色 |
195
- | `V8.ConfirmTips(msg, cb)` | 回调式确认弹窗;内容按 HTML 渲染,只能传可信/已转义文本 |
196
- | `V8.FormSet(field, value)` | 普通表单会触发目标字段 V8;列表上下文只更新当前行/模板 |
197
- | `V8.FieldSet(field, prop, value)` | 设置字段属性;跨上下文只依赖顶层 Visible/Required/Readonly/Data |
198
- | `V8.FormSubmit({CloseForm:true})` | 提交当前表单 |
199
- | `V8.RefreshTable({_PageIndex:1})` | 刷新表格(-1 保持当前页) |
200
- | `V8.SearchSet({field: value})` | 设置筛选条件 |
201
- | `V8.OpenAnyForm({...})` | 打开任意表单(弹窗/抽屉) |
202
- | `V8.OpenAnyTable({...})` | 打开任意列表 |
203
- | `V8.OpenDialog({...})` | 打开自定义弹窗 |
204
- | `V8.OpenAppDialog({...})` | 按 AppKey 打开已发布在线微服务页面,支持 Dialog/Drawer 与结果回调 |
205
- | `V8.ApiEngine.Run({ApiEngineKey, ...})` | 调接口引擎(前端,参数对象格式) |
206
- | `V8.FormEngine.GetTableData(name, params, cb)` | 前端查列表(参数对象、回调或 await) |
207
- | `V8.Post(url, data, cb, errCb, headers, contentType)` | 通用 POST |
208
- | `V8.Method.ScanCode({...})` | 调用当前终端支持的扫码能力 |
209
- | `V8.Print.isConnected()` | 检查当前 BLE 写特征或 Android SPP Socket 是否仍可用 |
210
- | `V8.Print.OpenBluetoothPage()` | 在用户手势中打开蓝牙连接页,返回 Promise |
211
- | `V8.Print.reconnect()` | 使用已记住的设备授权或设备 ID 尝试重连 |
212
- | `V8.Print.getConnectionState()` | 获取连接、设备、传输、型号、指令、记忆和错误状态快照 |
213
- | `V8.Print.subscribeConnection(listener)` | 订阅应用级共享连接状态,返回取消订阅函数 |
214
- | `V8.Print.getPrinterProfile()` | 查看自动识别或手工选择后的型号配置 |
215
- | `V8.Print.setPrinterProfile(mode)` | 广播名无法识别时选 `zicox-cc4` 等型号;旧业务通常不调用 |
216
- | `V8.Print.prepareSend(bytes)` | 按型号适配后串行分包发送 TSPL、CPCL 或 ESC/POS,必须 `await` |
217
-
218
- `V8.OpenAnyForm` 只发起打开动作,不返回“用户关闭后的 Promise”。需要替换
219
- 子表单保存时,通过 `EventReplace.Submit(v8, param, callback)` 注册提交替换;
220
- 其中小写 `v8` 是子表单上下文,外层 `V8` 仍是父上下文。自定义提交结束后
221
- 必须调用 `callback(DosResult)`,否则子表单会一直等待。
222
-
223
- ### 蓝牙打印最小安全流程
224
-
225
- ```javascript
226
- if (!V8.Print) {
227
- V8.Tips('当前客户端未加载蓝牙打印能力', false);
228
- return;
229
- }
230
-
231
- if (!V8.Print.isConnected()) {
232
- var connected = await V8.Print.OpenBluetoothPage();
233
- if (!connected || !V8.Print.isConnected()) return;
234
- }
235
-
236
- var command = V8.Print.createNew(); // 同一 TSC 调用:GP-M322 原 TSPL,CC4 自动转 CPCL
237
- command.setSize(60, 40);
238
- command.setGap(2);
239
- command.setCls();
240
- command.setText(20, 20, 'TSS24.BF2', 1, 1, '测试标签');
241
- command.setPagePrint();
242
-
243
- try {
244
- await V8.Print.prepareSend(command.getData());
245
- V8.Tips('打印数据已发送', true);
246
- } catch (error) {
247
- V8.Tips('发送失败:' + (error.message || error), false);
248
- }
249
- ```
250
-
251
- PC/平板顶部导航与移动端【我的】页共用同一个应用级 `V8.Print` 实例,用户可先在全局入口连接,再进入任意模块打印。佳博 GP-M322 路径必须保持原 TSPL 字节不变;ZICOX CC4 只转换有明确 CPCL 等价语义的高层 TSC 调用,不支持的命令必须在首包写入前失败,禁止盲目透传乱码。Android 5+App 的 CC4 可在 BLE 失败时使用已配对 SPP,Web 端不能使用经典蓝牙。
252
-
253
- `prepareSend` 内部会把不同 V8 上下文排入同一发送队列;成功只证明字节已经写入 BLE 特征或 SPP 输出流,不代表打印机已走纸、无缺纸或无硬件故障。批量打印仍应逐条 `await`,不得用固定 `setTimeout` 猜测完成时间,也不要用 `Promise.all` 表达同一设备的并行打印。完整挂载范围、双型号连接、协议映射、批量恢复、安全与硬件验收见 `references/bluetooth-print.md`;源码级 TSC/ESC/CPCL 方法表见 `references/bluetooth-print-api.md`。
254
-
255
- ### 常用上下文差异
256
-
257
- | 变量 | 可用范围 |
258
- |------|----------|
259
- | `V8.OldForm` | 普通表单已加载旧数据后可用;服务端提交前/后事件也可用 |
260
- | `V8.OldValue` | 仅表格行内字段值变更可靠提供 |
261
- | `V8.Event` | 插槽按钮等显式传原生事件的场景;键盘事件使用 `V8.KeyCode` |
262
- | `V8.Row/Rows/RowIndex` | 表格行事件 |
263
- | `V8.TableRowSelected/SelectedData` | 列表批量按钮,互为兼容别名 |
264
- | `V8.SearchParam` | 列表 `{Keyword, Where}` 搜索快照 |
265
- | `V8.SysMenuModel` | 列表/菜单按钮 |
266
- | `V8.DataAppend` | 打开表单、列表、弹窗时传入的附加数据 |
267
-
268
- ### 在线微服务弹窗 V8.OpenAppDialog
269
-
270
- 复杂定制页面使用 `V8.OpenAppDialog`,不要在 V8 事件中内嵌大量 HTML/CSS:
271
-
272
- ```js
273
- V8.OpenAppDialog({
274
- AppKey: 'customer_profile_editor', // 必传:sys_microiservice.MsKey
275
- RoutePath: '/edit', // 可选,默认 /
276
- Version: '', // 可选,空值自动使用当前 BuildVersion
277
- Title: '编辑客户资料',
278
- TitleIcon: 'fas fa-user-edit',
279
- Width: 'min(920px, calc(100vw - 32px))',
280
- OpenType: 'Dialog', // Dialog / Drawer
281
- Data: { id: V8.Form.Id }, // 子应用 dialogData,只放普通数据
282
- OnSuccess: function (data) {
283
- V8.RefreshTable({ _PageIndex: -1 });
284
- },
285
- OnCancel: function (data) {},
286
- OnError: function (error) {
287
- V8.Tips(error.message || '加载失败', false);
288
- }
289
- });
290
- ```
291
-
292
- 子应用用 `window.microApp.getData()` 获取自动下发的 `apiBase`、`osClient`、`token`、`appKey`、`version`、`microRoute`、`dialog` 和 `dialogData`;用 `window.microApp.dispatch({type:'app-dialog:success', data:{...}})` 返回成功结果。完整参数表和结果协议见 `v8-menu-buttons/SKILL.md`。
293
-
294
- ### ConfirmTips 的 HTML 安全边界
295
-
296
- `V8.ConfirmTips` 当前是 callback API,且内容使用 HTML 模式渲染。只传固定文案或经过 HTML 转义的简单展示;严禁直接拼接用户输入、接口消息、数据库富文本和不可信 URL。三个以上字段、上传、表格、Tab、步骤条、代码编辑器或需要复用的页面必须使用 `V8.OpenAppDialog`。
297
-
298
- ## 前端 FormEngine 菜单上下文与兼容授权
299
-
300
- 前端 V8 不需要为每个历史项目手工补 `_SysMenuId`。新版 PC 表单引擎通过作用域 FormEngine facade 透明处理菜单上下文:
301
-
302
- - V8 调用的目标表就是当前菜单绑定表时,facade 自动注入真实 `_SysMenuId`;如果业务代码已经显式传入 `_SysMenuId` 或兼容的 `ModuleEngineKey`,平台保留显式值,由后端做严格精确校验。
303
- - V8 查询其它表时,不得把当前主表菜单 Id 带给目标表。facade 保持无菜单,由后端根据当前登录用户有效角色可访问的 `sys_menu` 缓存推断该目标表及当前操作权限。
304
- - 历史 V8 的对象参数、`表名 + 参数`、Promise、回调、批量参数等调用形式继续兼容。业务代码不得自行伪造角色、菜单或 `_TrustedServerInvocation`。
305
- - 平台敏感表仍对普通客户端硬拒绝;Import/Export 仍必须携带目标模块的真实菜单上下文及专项权限,不能依赖无菜单推断。
306
- - 菜单配置的 `SqlWhere`、`SqlJoin` / `JoinTables` 由后端追加到真实查询。前端追加 `_Where` 只能进一步缩小结果,不能扩大或覆盖服务端数据范围。
307
- - 标准 `TableChild` 自动携带内部 `_TableChildAuth` 关系提示。服务端仍会重载父/子表、菜单和字段配置,校验父记录范围并强制子表外键;业务 V8 禁止构造、缓存、跨父记录复用该对象。
308
-
309
- ```javascript
310
- // 假设当前菜单绑定 Customer:平台 facade 自动注入真实菜单,无需历史 V8 手工改造
311
- var current = await V8.FormEngine.GetFormData('Customer', { Id: V8.Form.Id });
312
-
313
- // 跨表:不要传当前表的菜单 Id;后端按用户对 Product 的菜单授权推断
314
- var products = await V8.FormEngine.GetTableData('Product', {
315
- _Where: [['Status', '=', 1]],
316
- _PageSize: 20
317
- });
318
- ```
319
-
320
- 后端接口引擎和后端表单 V8 不是浏览器调用链:平台只在服务端内部构造参数时写入 `_TrustedServerInvocation`,所以它们调用 `V8.FormEngine` 不要求 `_SysMenuId`。该标记不能从浏览器 JSON、URL 或表单参数获得,前端 V8 也不得尝试设置。
321
-
322
- ### 前端 FormEngine 方法矩阵
323
-
324
- 前端 facade 当前公开 `GetFormData`、`GetFormDataAnonymous`、`GetTableData`、`GetTableTree`、`AddFormData`、`AddFormDataBatch`、`UptFormData`、`UptFormDataBatch`、`UptFormDataByWhere`、`DelFormData`、`DelFormDataBatch`、`DelFormDataByWhere`。全部返回 Promise,并兼容可选 callback。
325
-
326
- 前端没有 `GetTableDataCount`、`GetTableDataTree`(前端名称是 `GetTableTree`)、`AddTableData`、`UptTableData`、`DelTableData`、`AddField`。Import/Export 是独立端点与专项菜单权限,也不是 facade 方法。
327
-
328
- ## 异步写法(async/await vs 回调)
329
-
330
- ```javascript
331
- // ✅ 推荐 async/await
332
- var r = await V8.FormEngine.GetTableData('Product', { _PageSize: 10 });
333
- if (r.Code === 1) { /* ... */ }
334
-
335
- // ✅ 也可回调式
336
- V8.FormEngine.GetTableData('Product', { _PageSize: 10 }, function(r) {
337
- if (r.Code === 1) { /* ... */ }
338
- });
339
- ```
340
-
341
- ## 死循环陷阱
342
-
343
- ❌ **禁止** 在 `SubmitFormV8.js` 里调用 `V8.FormSubmit()` —— 会无限递归
344
- ⚠️ **避免** 在 `FieldValueChange` 里 `V8.FormSet(同字段)` —— 前端会阻止同步直接重入,但异步回写或多字段互相赋值仍可能形成循环;需要静默赋值时使用 `V8.Form.字段名 = value`
345
- ❌ **禁止** 在 `InFormV8.js` 里写大量同步 `V8.FormEngine.Get*` —— 阻塞渲染
346
-
347
- ### 下拉框对象赋值
348
-
349
- ```javascript
350
- // 会更新下拉选项并触发 SelectUser 的值变更 V8。
351
- // 对象至少包含 SelectSaveField、SelectLabel 对应的属性;字段事件需要的其它属性也要传入。
352
- V8.FormSet('SelectUser', { Id: 'u1', Name: '张三', DeptId: 'd1' });
353
-
354
- // 响应式静默赋值:界面会更新,但不触发目标字段 V8,
355
- // 也不会执行 FormSet 的修改字段记录、模板通知等处理。
356
- V8.Form.SelectUser = { Id: 'u1', Name: '张三', DeptId: 'd1' };
357
- ```
358
-
164
+ <!-- /microi-progressive:chunk -->
165
+ <!-- microi-progressive:chunk id=v8-frontend-events-005 sha256=e7f73123dfc00720f60c49705e91854b6f18cb0506a115a6078481dc37aed41c -->
359
166
  ## 设计模式保护(CRITICAL)
360
167
 
361
168
  ```javascript
@@ -363,3 +170,10 @@ V8.Form.SelectUser = { Id: 'u1', Name: '张三', DeptId: 'd1' };
363
170
  if (V8.LoadMode === 'Design') return;
364
171
  ```
365
172
  否则在【表单设计器】中编辑字段时,事件会被误触发,可能弹提示、报错或触发副作用。
173
+ <!-- /microi-progressive:chunk -->
174
+ ## 详细参考路由(渐进披露)
175
+
176
+ 仅在当前任务涉及对应主题时读取;下列文件合计保留了原 SKILL.md 的全部详细知识。
177
+
178
+ - [references/progressive-01-列表事件.md](references/progressive-01-列表事件.md):列表事件;常用前端 API;前端 FormEngine 菜单上下文与兼容授权;异步写法(async/await vs 回调);死循环陷阱
179
+ <!-- microi-progressive:end -->