@microi.net/cli 6.1.6 → 6.1.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 (47) hide show
  1. package/.codebuddy-plugin/marketplace.json +2 -2
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +43 -43
  4. package/.workbuddy-plugin/marketplace.json +2 -2
  5. package/.workbuddy-plugin/plugin.json +1 -1
  6. package/assets/build-meta.json +4 -4
  7. package/cordis.patch.yml +1 -1
  8. package/package.json +82 -82
  9. package/scripts/mcp-server.js +199 -164
  10. package/scripts/microi-skills.meta.json +243 -237
  11. package/skills/.microi-skills-version.json +2 -2
  12. package/skills/.progressive-disclosure-manifest.json +67 -51
  13. package/skills/ai-engine/SKILL.md +18 -5
  14. package/skills/ai-engine/references/ai-employees.md +4 -0
  15. package/skills/app-store/SKILL.md +10 -1
  16. package/skills/business-blueprint/SKILL.md +1 -1
  17. package/skills/job-engine/SKILL.md +15 -0
  18. package/skills/message-notification/SKILL.md +2 -0
  19. package/skills/microi-codex/SKILL.md +6 -1
  20. package/skills/microi-deployment/SKILL.md +64 -18
  21. package/skills/microi-deployment/references/deployment-matrix.md +7 -1
  22. package/skills/microi-docs-coverage/SKILL.md +11 -1
  23. package/skills/microi-frontend-sdk/SKILL.md +12 -0
  24. package/skills/microi-microservice/SKILL.md +12 -1
  25. package/skills/microi-mobile-app-quality/references/mobile-first-delivery.md +2 -1
  26. package/skills/microi-system-delivery/SKILL.md +1 -0
  27. package/skills/microi-system-delivery/references/context-quality.md +56 -54
  28. 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 +9 -5
  29. package/skills/microi-system-delivery/references/progressive-03-/345/256/211/345/205/250/344/270/216/350/267/250/347/253/257/351/252/214/346/224/266.md +65 -65
  30. package/skills/microi.v8.js +445 -0
  31. package/skills/module-engine/SKILL.md +13 -0
  32. package/skills/module-engine/references/module-config.md +17 -0
  33. package/skills/ocr-engine/SKILL.md +1 -0
  34. package/skills/playwright-e2e/SKILL.md +2 -0
  35. package/skills/scripts/sync-public-resource-router.mjs +29 -0
  36. package/skills/system-observability/SKILL.md +29 -6
  37. package/skills/translate-engine/SKILL.md +2 -0
  38. package/skills/v8-api-config/SKILL.md +1 -1
  39. package/skills/v8-file-upload/SKILL.md +17 -32
  40. package/skills/v8-file-upload/references/hotspot-offload-operations.md +103 -0
  41. 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 +2 -2
  42. package/skills/v8-security/SKILL.md +5 -8
  43. package/skills/v8-sql-query/SKILL.md +2 -2
  44. package/skills/workspace-conventions/SKILL.md +154 -136
  45. 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 +8 -5
  46. 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 +2 -2
  47. package/skills/workspace-conventions/references/progressive-04-/345/267/245/344/275/234/345/214/272/344/270/216/345/271/263/345/217/260/344/270/223/351/241/271/344/277/235/351/232/234.md +4 -4
@@ -2,70 +2,70 @@
2
2
 
3
3
  > 原规则按适用范围无损迁移;触发对应工作时必须完整读取相关章节。
4
4
 
5
- <!-- microi-progressive:chunk id=microi-system-delivery-002 sha256=b4cd10b169b54235314d170c8e74f17bf6bcb5f658d0d4d2e10c73dc8aa37ef7 -->
6
- ## 平台安全与存量兼容验收(强制)
7
-
8
- AI 零代码交付不能只验证管理员帐号和页面能打开。任何涉及 FormEngine、菜单、角色、子表、文件、SaaS 或登录协议的交付,都必须按以下服务端边界设计和验收:
9
-
10
- ### FormEngine 混合授权
11
-
12
- - Token 只完成身份认证,不是任意表的访问凭证。浏览器、UniApp、SDK 等外部请求必须继续校验表、菜单、角色、操作权限和数据范围。
13
- - 显式 `_SysMenuId` / `ModuleEngineKey` 进入精确菜单校验;伪造、传错或借用其它表菜单时失败关闭,不能退回兼容推断。
14
- - 为兼容存量前端 V8,无菜单请求由后端从当前用户真正拥有的菜单授权快照中推断目标表权限。没有候选菜单、范围无法安全合并或解析失败时失败关闭;不要要求所有历史项目一次性补 `_SysMenuId`。
15
- - 标准 PC facade 只给当前菜单绑定的当前表注入菜单 Id,跨表调用不借用主表菜单。没有菜单入口的 SDK/定制页才使用最小【高级表权限】。
16
- - 后端接口引擎、后端表单 V8 和平台内部调用由服务端建立可信上下文,不要求 `_SysMenuId`;客户端伪造 `_TrustedServerInvocation` 或 `_InvokeType:'Server'` 不能获得信任。
17
- - `TableChild` 使用父记录范围内委托:验证父菜单、父表字段配置、子菜单、父记录数据范围和外键,并由服务端强制注入外键。隐藏子菜单不要求存量角色逐个补权限。
18
- - 菜单 `SqlWhere` / `SqlJoin` 只约束列表、计数、导出等集合查询;单行详情只校验同表菜单访问权。主表写入由 `Add` / `Edit` / `Del` / `Import` 专项权限控制,不把查询范围追加到写入 SQL,也不因查询 Join 拒绝。行级写业务限制放在后端表单 V8 或专用接口引擎。
19
-
20
- ### 保护表、控制面与缓存
21
-
22
- - 平台表按 `PlatformResourceSecurity` 的管理员专用、只读委托、按角色管理三类校验,不使用过时的统一硬拒绝清单。`sys_apiengine` 的标准 FormEngine 管理按真实菜单/表操作授权,不能因 `Level < 9999` 拒绝已获授权的开发角色;无授权、伪造菜单、匿名和撤权必须拒绝。
23
- - 接口代码写权限只授予可信开发角色;任务、Redis 等其它控制面门禁、接口运行权限和 MCP 专用开发权限仍独立执行,不能只靠前端隐藏菜单,也不能从接口管理菜单权限推导其它控制面权限。
24
- - 权限缓存使用按 `OsClient` 隔离的共享 Redis `epoch`、用户快照、短 TTL L1/L2。用户/角色/菜单/权限变化后提升 `epoch`;验收至少使用两个节点确认无需清 Redis或重启即可生效。
25
- - Upgrade15 只清理普通角色的保护表直接授权,不能删除正常业务菜单权限。
26
-
27
- ### 上传、私有文件与 SaaS
28
-
29
- - 上传业务默认值为 100 MB/文件、200 MB/次、10 文件、2 GB/帐号/日、20 GB/租户/日。有效值只按当前租户 `sys_osclients` → 代码安全默认值解析,租户可以提高或降低业务默认值;不得为上传开关、额度或硬上限新增 API 环境变量或 `appsettings` 节点。最终仍受代码内不可突破的灾难保护、HTTP/Multipart/Form 和反向代理上限约束。共享 Redis 原子预留,Redis 故障失败关闭。
30
- - 无字段上下文的普通交互式上传默认私有桶,一级安全目录为 `file`、`img`、`avatar`、`editor`;可由管理员在普通 `sys_config.HdfsUploadRules` 按真实角色授权业务目录与公有权限。通配符必须有界并先校验实际路径,保留目录与跨租户不能放行。标准表单仍按权威字段配置执行。可信后端 V8 仍受全局文件大小硬上限。
31
- - 普通客户端私有文件签名必须提交 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId` 并验证记录字段真实引用;不能把后端 `V8.Method.GetPrivateFileUrl({FilePathName})` 的可信调用方式照搬到浏览器。
32
- - Upgrade16 六个上传字段全部可空,空值保持老租户兼容;升级后回读字段元数据、租户值并刷新 SaaS 缓存。
33
- - `V8.OsClientModel` / `V8.SysConfig` 只使用脱敏副本,不返回整个对象。新租户不能复制主租户整条 `sys_osclients`;数据库、认证、Redis、存储、MQ/MQTT、搜索凭据必须独立创建或由服务端托管。
34
- - Redis 管理器只允许超级管理员使用当前租户或已保存连接;匿名/temporary 任意 Host 连接必须拒绝,MCP 写操作必须 `confirmExecution`,禁止传递 Redis 密码。
35
-
36
- ### 网络、登录和发布兼容
37
-
38
- - `CorsAllowOrigins` 和全局 CORS 来源都为空时默认允许任意来源;配置后才按精确来源/通配符收紧。CORS 不是鉴权边界。
39
- - 严格 SSRF 默认关闭,未配置时不得拒绝存量非 HTTP(S)、URL 凭据、私网、云元数据和重定向调用;显式启用后才执行严格拦截与精确主机白名单。
40
- - 登录 RSA 仅避免密码在请求体直接显示,HTTPS 才是安全边界。必须保留历史 RSA fallback,专属公私钥成对切换,不能因删除旧常量造成所有客户无法登录。
41
- - 登录传 `_ClientType`,请求携带稳定 `did`,每次响应接收 `authorization`。多标签续签 single-flight;`TokenReplaced` 先检查新 Token,旧响应不能清掉新登录态。
42
- - 安全升级不得删除私有子 Git 的 `Microi.Server/Microi.net/License/keys/`。授权签名资产与登录 RSA 用途不同,禁止以安全清理为由混删。
43
- - 不得修改默认 CORS/SSRF/RSA 行为后只用新装环境验收;必须覆盖旧前端、历史 V8、滚动升级和至少两个节点。
44
-
45
- ### 安全自动化最低断言
46
-
47
- 1. 普通角色保护表读写、伪造菜单、伪造可信标记均失败。
48
- 2. 真实菜单、历史无菜单推断和父记录范围内 TableChild 均成功,跨父记录失败。
49
- 3. 查询范围在列表、计数、导出中生效;详情按同表菜单访问权成功;有写权限的单表/Join 模块写入成功,无写权限失败,后端 V8 行级业务校验可回滚。
50
- 4. 节点 A 改权限后节点 B 无需重启生效。
51
- 5. 上传大小/数量/双日额度、多节点并发和 Redis 故障行为正确。
52
- 6. 私有文件跨菜单/记录/字段失败,授权访问成功,匿名失败。
53
- 7. CORS 空配置兼容、配置后收紧;SSRF 默认兼容、严格模式拦截。
54
- 8. 历史 RSA fallback 和专属匹配密钥均可登录;并发续签不反复退出。
55
- 9. SaaS 脱敏投影不包含基础设施密钥,Redis anonymous/temporary/非管理员管理失败。
56
-
5
+ <!-- microi-progressive:chunk id=microi-system-delivery-002 sha256=1cf73c1365e3f8245b2069f5f2e24bcbfdc7f6e67c0f13524998b16f7bbf029b -->
6
+ ## 平台安全与存量兼容验收(强制)
7
+
8
+ AI 零代码交付不能只验证管理员帐号和页面能打开。任何涉及 FormEngine、菜单、角色、子表、文件、SaaS 或登录协议的交付,都必须按以下服务端边界设计和验收:
9
+
10
+ ### FormEngine 混合授权
11
+
12
+ - Token 只完成身份认证,不是任意表的访问凭证。浏览器、UniApp、SDK 等外部请求必须继续校验表、菜单、角色、操作权限和数据范围。
13
+ - 显式 `_SysMenuId` / `ModuleEngineKey` 进入精确菜单校验;伪造、传错或借用其它表菜单时失败关闭,不能退回兼容推断。
14
+ - 为兼容存量前端 V8,无菜单请求由后端从当前用户真正拥有的菜单授权快照中推断目标表权限。没有候选菜单、范围无法安全合并或解析失败时失败关闭;不要要求所有历史项目一次性补 `_SysMenuId`。
15
+ - 标准 PC facade 只给当前菜单绑定的当前表注入菜单 Id,跨表调用不借用主表菜单。没有菜单入口的 SDK/定制页才使用最小【高级表权限】。
16
+ - 后端接口引擎、后端表单 V8 和平台内部调用由服务端建立可信上下文,不要求 `_SysMenuId`;客户端伪造 `_TrustedServerInvocation` 或 `_InvokeType:'Server'` 不能获得信任。
17
+ - `TableChild` 使用父记录范围内委托:验证父菜单、父表字段配置、子菜单、父记录数据范围和外键,并由服务端强制注入外键。隐藏子菜单不要求存量角色逐个补权限。
18
+ - 菜单 `SqlWhere` / `SqlJoin` 只约束列表、计数、导出等集合查询;单行详情只校验同表菜单访问权。主表写入由 `Add` / `Edit` / `Del` / `Import` 专项权限控制,不把查询范围追加到写入 SQL,也不因查询 Join 拒绝。行级写业务限制放在后端表单 V8 或专用接口引擎。
19
+
20
+ ### 保护表、控制面与缓存
21
+
22
+ - 平台表按 `PlatformResourceSecurity` 的管理员专用、只读委托、按角色管理三类校验,不使用过时的统一硬拒绝清单。`sys_apiengine` 的标准 FormEngine 管理按真实菜单/表操作授权,不能因 `Level < 9999` 拒绝已获授权的开发角色;无授权、伪造菜单、匿名和撤权必须拒绝。
23
+ - 接口代码写权限只授予可信开发角色;任务、Redis 等其它控制面门禁、接口运行权限和 MCP 专用开发权限仍独立执行,不能只靠前端隐藏菜单,也不能从接口管理菜单权限推导其它控制面权限。
24
+ - 权限缓存使用按 `OsClient` 隔离的共享 Redis `epoch`、用户快照、短 TTL L1/L2。用户/角色/菜单/权限变化后提升 `epoch`;验收至少使用两个节点确认无需清 Redis或重启即可生效。
25
+ - Upgrade15 只清理普通角色的保护表直接授权,不能删除正常业务菜单权限。
26
+
27
+ ### 上传、私有文件与 SaaS
28
+
29
+ - 上传业务默认值为 100 MB/文件、200 MB/次、10 文件、2 GB/帐号/日、20 GB/租户/日。有效值只按当前租户 `sys_osclients` → 代码安全默认值解析,租户可以提高或降低业务默认值;不得为上传开关、额度或硬上限新增 API 环境变量或 `appsettings` 节点。最终仍受代码内不可突破的灾难保护、HTTP/Multipart/Form 和反向代理上限约束。共享 Redis 原子预留,Redis 故障失败关闭。
30
+ - 无字段上下文的普通交互式上传默认私有桶,一级安全目录为 `file`、`img`、`avatar`、`editor`;可由管理员在普通 `sys_config.HdfsUploadRules` 按真实角色授权业务目录与公有权限。通配符必须有界并先校验实际路径,保留目录与跨租户不能放行。标准表单仍按权威字段配置执行。可信后端 V8 仍受全局文件大小硬上限。
31
+ - 普通客户端私有文件签名必须提交 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId` 并验证记录字段真实引用;不能把后端 `V8.Method.GetPrivateFileUrl({FilePathName})` 的可信调用方式照搬到浏览器。
32
+ - Upgrade16 六个上传字段全部可空,空值保持老租户兼容;升级后回读字段元数据、租户值并刷新 SaaS 缓存。
33
+ - `V8.OsClientModel` / `V8.SysConfig` 只使用脱敏副本,不返回整个对象。新租户不能复制主租户整条 `sys_osclients`;数据库、认证、Redis、存储、MQ/MQTT、搜索凭据必须独立创建或由服务端托管。
34
+ - Redis 管理器只允许超级管理员使用当前租户或已保存连接;匿名/temporary 任意 Host 连接必须拒绝,MCP 写操作必须 `confirmExecution`,禁止传递 Redis 密码。
35
+
36
+ ### 网络、登录和发布兼容
37
+
38
+ - `CorsAllowOrigins` 和全局 CORS 来源都为空时默认允许任意来源;配置后才按精确来源/通配符收紧。CORS 不是鉴权边界。
39
+ - 严格 SSRF 默认关闭,未配置时不得拒绝存量非 HTTP(S)、URL 凭据、私网、云元数据和重定向调用;显式启用后才执行严格拦截与精确主机白名单。
40
+ - 登录 RSA 仅避免密码在请求体直接显示,HTTPS 才是安全边界。必须保留历史 RSA fallback,专属公私钥成对切换,不能因删除旧常量造成所有客户无法登录。
41
+ - 登录传 `_ClientType`,请求携带稳定 `did`,每次响应接收 `authorization`。多标签续签 single-flight;`TokenReplaced` 先检查新 Token,旧响应不能清掉新登录态。
42
+ - 安全升级不得删除私有子 Git 的 `Microi.Server/Microi.net/License/keys/`。授权签名资产与登录 RSA 用途不同,禁止以安全清理为由混删。
43
+ - 不得修改默认 CORS/SSRF/RSA 行为后只用新装环境验收;必须覆盖旧前端、历史 V8、滚动升级和至少两个节点。
44
+
45
+ ### 安全自动化最低断言
46
+
47
+ 1. 普通角色保护表读写、伪造菜单、伪造可信标记均失败。
48
+ 2. 真实菜单、历史无菜单推断和父记录范围内 TableChild 均成功,跨父记录失败。
49
+ 3. 查询范围在列表、计数、导出中生效;详情按同表菜单访问权成功;有写权限的单表/Join 模块写入成功,无写权限失败,后端 V8 行级业务校验可回滚。
50
+ 4. 节点 A 改权限后节点 B 无需重启生效。
51
+ 5. 上传大小/数量/双日额度、多节点并发和 Redis 故障行为正确。
52
+ 6. 私有文件跨菜单/记录/字段失败,授权访问成功,匿名失败。
53
+ 7. CORS 空配置兼容、配置后收紧;SSRF 默认兼容、严格模式拦截。
54
+ 8. 历史 RSA fallback 和专属匹配密钥均可登录;并发续签不反复退出。
55
+ 9. SaaS 脱敏投影不包含基础设施密钥,Redis anonymous/temporary/非管理员管理失败。
56
+
57
57
  <!-- /microi-progressive:chunk -->
58
- <!-- microi-progressive:chunk id=microi-system-delivery-003 sha256=479ccdb65450c7741b052a30d8e4a11917bc19d80bf9b4ac89ea00092b4aaa73 -->
59
- ## 前端交付检查清单
60
-
61
- - 底部 tabBar 图标不 404。
62
- - 首页、分类、详情、Banner、头像、订单凭证图全部真实加载。
63
- - 主题切换是全局生效,不是只对一个页面生效。
64
- - 列表页和详情页显示同一主体名称、头像、状态,不能列表对、详情错。
65
- - 关联人名不得退化成“卖家BMB9”“匿名卖家”等可推断 Id 的兜底,除非确实无数据。
66
- - 关键业务资产不得默认选中第一条,必须用户手动选择。
67
- - 自己不能抢自己的单,不能约自己的单,必须给明确提示。
68
- - 消息/待办/约单/审核类提醒必须有未读角标,已读后消失。
69
- - 订单状态角标只给需要处理的状态,不给“全部/已完成/已取消”等静态分类堆数字。
70
-
58
+ <!-- microi-progressive:chunk id=microi-system-delivery-003 sha256=8e2891f2a149d6828b653b9f0735ea39a4df5ba60178c79f0e5b0065ce72ee84 -->
59
+ ## 前端交付检查清单
60
+
61
+ - 底部 tabBar 图标不 404。
62
+ - 首页、分类、详情、Banner、头像、订单凭证图全部真实加载。
63
+ - 主题切换是全局生效,不是只对一个页面生效。
64
+ - 列表页和详情页显示同一主体名称、头像、状态,不能列表对、详情错。
65
+ - 关联人名不得退化成“卖家BMB9”“匿名卖家”等可推断 Id 的兜底,除非确实无数据。
66
+ - 关键业务资产不得默认选中第一条,必须用户手动选择。
67
+ - 自己不能抢自己的单,不能约自己的单,必须给明确提示。
68
+ - 消息/待办/约单/审核类提醒必须有未读角标,已读后消失。
69
+ - 订单状态角标只给需要处理的状态,不给“全部/已完成/已取消”等静态分类堆数字。
70
+
71
71
  <!-- /microi-progressive:chunk -->
@@ -569,12 +569,394 @@ function installDatePrototypeCompat() {
569
569
  }
570
570
  }
571
571
 
572
+ // BEGIN GENERATED PUBLIC RESOURCE ROUTER; source SHA256 d90207991f19b46429dc21a654e2915e1b4fd6e96f191cede20485d823f3a975
573
+ const createSdkPublicResourceClient = (() => {
574
+ // 公有资源路由的纯校验器与实际消费者桥接;私有分支继续走既有授权接口。
575
+ const HASH = /^[a-f0-9]{64}$/;
576
+ const IDENTIFIER = /^[a-zA-Z0-9_-]{1,64}$/;
577
+ const BLOCKED = /^(?:\.git|\.env|private|private-source|privatesource|source|_origin)$/i;
578
+
579
+ function key(value) {
580
+ if (typeof value !== 'string' || !value || value.length > 1000 || /[\\%?#:\u0000-\u001f\u007f]/.test(value)
581
+ || value.split('/').some(part => !part || part === '.' || part === '..' || BLOCKED.test(part)) || /\.map$/i.test(value)) throw Error('InvalidPublicObjectKey');
582
+ encodeURIComponent(value); // Reject malformed UTF-16 rather than changing the key.
583
+ return value;
584
+ }
585
+ const encodeKey = value => value.split('/').map(encodeURIComponent).join('/');
586
+ function plainBase(value, originOnly = false) {
587
+ if (typeof value !== 'string' || /[\\%?#@\u0000-\u0020\u007f]/.test(value)) throw Error('InvalidPublicBinding');
588
+ const parsed = new URL(value);
589
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.search || parsed.hash
590
+ || (originOnly && parsed.pathname !== '/')) throw Error('InvalidPublicBinding');
591
+ return value.replace(/\/+$/, '');
592
+ }
593
+ function bindingSnapshot(binding) {
594
+ if (!binding || !IDENTIFIER.test(binding.tenant) || !IDENTIFIER.test(binding.storageOwnerTenant)
595
+ || !HASH.test(binding.targetConfigHash) || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(binding.publicBucket)
596
+ || binding.publicBucket.includes('..') || binding.publicBucket === binding.privateBucket) throw Error('InvalidPublicBinding');
597
+ const value = { tenant: binding.tenant, storageOwnerTenant: binding.storageOwnerTenant.toLowerCase(),
598
+ primaryBase: plainBase(binding.primaryBase), backupOrigin: plainBase(binding.backupOrigin, true),
599
+ publicBucket: binding.publicBucket, targetConfigHash: binding.targetConfigHash,
600
+ sourceMappings: (binding.sourceMappings || []).map(item => ({ origin: plainBase(item.origin, true),
601
+ pathPrefix: item.pathPrefix ? key(item.pathPrefix.replace(/\/+$/, '')) + '/' : '' })) };
602
+ if (!value.sourceMappings.length || value.sourceMappings.length > 20) throw Error('InvalidPublicBinding');
603
+ return { ...value, identity: JSON.stringify(value) };
604
+ }
605
+ function legacyPublicFallback(input, binding) {
606
+ if (!input || input.private === true || typeof input.path !== 'string') return '';
607
+ const path = input.path;
608
+ try {
609
+ if (/^https?:/i.test(path)) {
610
+ const url = new URL(path);
611
+ return !/[\\\u0000-\u0020\u007f]/.test(path) && !url.username && !url.password ? path : '';
612
+ }
613
+ if (path.startsWith('//') || path.startsWith('.')) return '';
614
+ if (/[\\\u0000-\u001f\u007f]/.test(path)) return '';
615
+ const relative = path.startsWith('/') ? path.slice(1) : path;
616
+ const decoratedAt = relative.search(/[?#]/);
617
+ const rawKey = decoratedAt < 0 ? relative : relative.slice(0, decoratedAt);
618
+ const suffix = decoratedAt < 0 ? '' : relative.slice(decoratedAt);
619
+ const objectKey = key(rawKey.split('/').map(part => decodeURIComponent(part)).join('/'));
620
+ return plainBase(binding?.primaryBase) + '/' + encodeKey(objectKey) + suffix;
621
+ } catch { return ''; }
622
+ }
623
+ function identify(input, binding) {
624
+ if (!input || input.private === true) return { input, accepted: false, reason: 'PrivateRoutingNotSupported', primaryUrl: '' };
625
+ const path = input.path;
626
+ try {
627
+ if (typeof path !== 'string' || !path) throw Error('InvalidInput');
628
+ if (/^https?:/i.test(path)) {
629
+ if (/[\\?#@\u0000-\u001f\u007f]/.test(path)) throw Error('SignedOrDecoratedUrlNotEligible');
630
+ const parsed = new URL(path);
631
+ const mapping = binding.sourceMappings.find(value => new URL(value.origin).origin === parsed.origin);
632
+ if (!mapping || parsed.protocol !== 'https:') throw Error('UnmanagedOrigin');
633
+ // Check raw path before URL() can normalize dot segments.
634
+ const begin = path.indexOf('/', path.indexOf('://') + 3);
635
+ if (begin < 0) throw Error('ObjectKeyMissing');
636
+ const objectKey = key(path.slice(begin + 1).split('/').map(part => {
637
+ const decoded = decodeURIComponent(part);
638
+ if (/[\/%\\?#:\u0000-\u001f\u007f]/.test(decoded)) throw Error('InvalidEncodedPath');
639
+ return decoded;
640
+ }).join('/'));
641
+ if (!objectKey.startsWith(mapping.pathPrefix)) throw Error('BucketPrefixMismatch');
642
+ return { accepted: true, objectKey: key(objectKey.slice(mapping.pathPrefix.length)), primaryUrl: path };
643
+ }
644
+ // Protocol-relative, traversal and double-slash paths are never interpreted
645
+ // as the configured public bucket. A single leading slash is legacy input.
646
+ if (path.startsWith('//') || path.startsWith('.')) throw Error('UnmanagedRelativePath');
647
+ const objectKey = key(path.startsWith('/') ? path.slice(1) : path);
648
+ return { accepted: true, objectKey, primaryUrl: binding.primaryBase + '/' + encodeKey(objectKey) };
649
+ } catch (error) { return { accepted: false, reason: error.message, primaryUrl: legacyPublicFallback(input, binding) }; }
650
+ }
651
+ function original(item, reason) {
652
+ return Object.freeze({ useBackup: false, url: item.primaryUrl, primaryUrl: item.primaryUrl,
653
+ objectKey: item.objectKey || null, reason });
654
+ }
655
+ function acceptedRoute(row, item, binding, time) {
656
+ if (!row || row.UseBackup !== true || row.ObjectKey !== item.objectKey || !HASH.test(row.SourceRevision)
657
+ || !HASH.test(row.ResourceId) || row.TargetConfigHash !== binding.targetConfigHash
658
+ || !Number.isSafeInteger(row.ExpiresAtUtc) || row.ExpiresAtUtc * 1000 <= time
659
+ || row.ExpiresAtUtc * 1000 > time + 300000 || typeof row.Url !== 'string' || /[?#@\\]/.test(row.Url)) return null;
660
+ const prefix = binding.backupOrigin + '/' + binding.publicBucket + '/_microi-hotspot/public/v3/'
661
+ + binding.storageOwnerTenant + '/' + row.ResourceId + '/' + row.SourceRevision + '/';
662
+ if (!row.Url.startsWith(prefix)) return null;
663
+ const rest = row.Url.slice(prefix.length), slash = rest.indexOf('/');
664
+ if (!/^[a-f0-9]{32}$/.test(rest.slice(0, slash)) || rest.slice(slash + 1) !== encodeKey(item.objectKey)) return null;
665
+ return Object.freeze({ useBackup: true, url: row.Url, primaryUrl: item.primaryUrl, objectKey: item.objectKey,
666
+ expiresAt: row.ExpiresAtUtc * 1000, reason: 'VerifiedPublicReplica' });
667
+ }
668
+
669
+ function createPublicResourceRouter({ getBinding, resolveBatch, now = Date.now, maxEntries = 512, timeoutMs = 5000 }) {
670
+ if (typeof getBinding !== 'function' || typeof resolveBatch !== 'function' || !Number.isInteger(maxEntries)
671
+ || maxEntries < 1 || maxEntries > 10000 || !Number.isInteger(timeoutMs)
672
+ || timeoutMs < 1 || timeoutMs > 30000) throw Error('InvalidRouterDependencies');
673
+ const cache = new Map(), pending = new Map(), cooldown = new Map();
674
+ const cacheKey = (binding, item) => binding.identity + '\n' + item.objectKey;
675
+ const put = (map, id, value) => { map.delete(id); map.set(id, value); while (map.size > maxEntries) map.delete(map.keys().next().value); };
676
+ function cached(binding, item) {
677
+ const id = cacheKey(binding, item), time = now();
678
+ if ((cooldown.get(id) || 0) > time) return original(item, 'BackupFailureCooldown');
679
+ const hit = cache.get(id);
680
+ if (hit && hit.until > time) return hit.route.useBackup ? Object.freeze({ ...hit.route, primaryUrl: item.primaryUrl }) : original(item, hit.route.reason);
681
+ cache.delete(id); cooldown.delete(id); return null;
682
+ }
683
+ function snapshot() { return bindingSnapshot(getBinding()); }
684
+ function fallback(input) { try { return legacyPublicFallback(input, getBinding()); } catch { return legacyPublicFallback(input, null); } }
685
+
686
+ async function resolveMany(inputs) {
687
+ if (!Array.isArray(inputs) || inputs.length > 100) throw Error('ResourceBatchLimitExceeded');
688
+ let binding;
689
+ try { binding = snapshot(); }
690
+ catch { return inputs.map(item => original({ primaryUrl: fallback(item) }, 'BackupBindingUnavailable')); }
691
+ const work = [], created = [];
692
+ const tasks = inputs.map(input => {
693
+ const item = identify(input, binding);
694
+ if (!item.accepted) return Promise.resolve(original(item, item.reason));
695
+ const known = cached(binding, item); if (known) return Promise.resolve(known);
696
+ const id = cacheKey(binding, item);
697
+ if (pending.has(id)) return pending.get(id).then(route => Object.freeze({ ...route, primaryUrl: item.primaryUrl,
698
+ url: route.useBackup ? route.url : item.primaryUrl }));
699
+ let done; const promise = new Promise(resolve => { done = resolve; });
700
+ pending.set(id, promise); work.push(item); created.push({ item, id, done, promise }); return promise;
701
+ });
702
+ if (work.length) {
703
+ // Do not pass secrets, arbitrary URLs or a caller-selected storage endpoint.
704
+ const controller = new AbortController(); let timer;
705
+ Promise.race([
706
+ Promise.resolve().then(() => resolveBatch({ ObjectKeys: work.map(item => item.objectKey), OsClient: binding.tenant }, { signal: controller.signal })),
707
+ new Promise((_, reject) => { timer = setTimeout(() => reject(Error('ResolverTimeout')), timeoutMs); })
708
+ ])
709
+ .then(response => {
710
+ let current; try { current = snapshot(); } catch { current = null; }
711
+ const data = response?.Code === 1 ? response.Data : null;
712
+ const valid = current?.identity === binding.identity && data?.Protocol === 'GenericPublicResourceRoutes/1'
713
+ && data.TargetConfigHash === binding.targetConfigHash && Array.isArray(data.Routes) && data.Routes.length <= 100;
714
+ const rows = new Map(), duplicates = new Set();
715
+ if (valid) for (const row of data.Routes) {
716
+ if (rows.has(row?.ObjectKey)) duplicates.add(row.ObjectKey);
717
+ rows.set(row?.ObjectKey, row);
718
+ }
719
+ for (const job of created) {
720
+ const time = now();
721
+ const route = valid && !duplicates.has(job.item.objectKey)
722
+ ? acceptedRoute(rows.get(job.item.objectKey), job.item, binding, time) : null;
723
+ const selected = (cooldown.get(job.id) || 0) > time ? original(job.item, 'BackupFailureCooldown')
724
+ : route || original(job.item, current?.identity !== binding.identity ? 'TenantOrBindingChanged' : 'VerifiedRouteUnavailable');
725
+ put(cache, job.id, { route: selected, until: selected.useBackup ? selected.expiresAt : time + 15000 });
726
+ job.done(selected);
727
+ }
728
+ })
729
+ .catch(() => {
730
+ for (const job of created) { const route = original(job.item, 'ResolverUnavailable');
731
+ put(cache, job.id, { route, until: now() + 15000 }); job.done(route); }
732
+ })
733
+ .finally(() => { clearTimeout(timer); controller.abort();
734
+ for (const job of created) if (pending.get(job.id) === job.promise) pending.delete(job.id); });
735
+ }
736
+ return Promise.all(tasks);
737
+ }
738
+ return Object.freeze({
739
+ resolveMany,
740
+ async resolve(input) { return (await resolveMany([input]))[0]; },
741
+ // Sync compatibility is cache-only. Callers must pre-resolve before assigning
742
+ // src/href if their FIRST request must avoid the original CDN/OSS endpoint.
743
+ peek(input) {
744
+ try { const binding = snapshot(), item = identify(input, binding);
745
+ return item.accepted ? cached(binding, item) || original(item, 'NotResolvedYet') : original(item, item.reason); }
746
+ catch { return original({ primaryUrl: fallback(input) }, 'BackupBindingUnavailable'); }
747
+ },
748
+ reportFailure(input) {
749
+ try { const binding = snapshot(), item = identify(input, binding);
750
+ if (!item.accepted) return original(item, item.reason);
751
+ const id = cacheKey(binding, item); cache.delete(id); put(cooldown, id, now() + 30000);
752
+ return original(item, 'BackupFailureCooldown'); }
753
+ catch { return original({ primaryUrl: fallback(input) }, 'BackupBindingUnavailable'); }
754
+ },
755
+ clear() { cache.clear(); cooldown.clear(); }
756
+ });
757
+ }
758
+
759
+ /**
760
+ * DiyCommon 与真实批量解析器的桥接。配置只从当前租户的原生 Binding 回读,
761
+ * 不将 SaaS 私桶名/凭据塞入前端。首次 src 赋值必须等待 resolve。
762
+ */
763
+ function createExactAliasRouter({ getBinding, post, now }) {
764
+ const cache = new Map(), pending = new Map(), cooldown = new Map(); let queue = [], scheduled = false;
765
+ const put = (map, id, value) => { map.delete(id); map.set(id, value); while (map.size > 512) map.delete(map.keys().next().value); };
766
+ function pick(input) {
767
+ try {
768
+ const raw = getBinding(), b = bindingSnapshot(raw);
769
+ if (input?.private !== false || raw.aliasUrlRoutingProtocol !== 'PublicSourceUrlRoutes/1'
770
+ || typeof input.path !== 'string' || !input.path.startsWith('https://') || /[\\?#@\u0000-\u0020\u007f]/.test(input.path)) return null;
771
+ const origin = new URL(input.path).origin;
772
+ if (!raw.aliasCandidates.includes(origin)) return null;
773
+ // Only parse the exact key here. Candidate membership is not authorization:
774
+ // the server must revalidate this complete SourceUrl before returning NAS.
775
+ const item = identify(input, { ...b, sourceMappings: [{ origin, pathPrefix: '' }] });
776
+ return item.accepted ? { ...item, input, origin, binding: b, id: b.identity + '\nURL:' + input.path } : null;
777
+ } catch { return null; }
778
+ }
779
+ async function flush() {
780
+ scheduled = false; const jobs = queue; queue = [];
781
+ while (jobs.length) {
782
+ const origins = new Set(), batch = [];
783
+ const identity = jobs[0].item.binding.identity;
784
+ for (let index = 0; index < jobs.length && batch.length < 100;) {
785
+ const job = jobs[index];
786
+ if (job.item.binding.identity !== identity) { index++; continue; }
787
+ if (!origins.has(job.item.origin) && origins.size === 2) { index++; continue; }
788
+ origins.add(job.item.origin); batch.push(...jobs.splice(index, 1));
789
+ }
790
+ let response, timer;
791
+ try { response = await Promise.race([post({ PublicUrls: batch.map(x => x.item.input.path), OsClient: batch[0].item.binding.tenant }),
792
+ new Promise((_, reject) => { timer = setTimeout(() => reject(Error('AliasResolverTimeout')), 5000); })]); }
793
+ catch { response = null; }
794
+ finally { clearTimeout(timer); }
795
+ for (const job of batch) {
796
+ const item = job.item, current = pick(item.input), time = now(), data = response?.Code === 1 ? response.Data : null;
797
+ const rows = data?.Protocol === 'PublicSourceUrlRoutes/1' && data.TargetConfigHash === item.binding.targetConfigHash
798
+ && Array.isArray(data.Routes) && data.Routes.length <= 100
799
+ ? data.Routes.filter(x => x && typeof x === 'object' && !Array.isArray(x) && x.SourceUrl === item.input.path) : [];
800
+ const row = rows.length === 1 ? rows[0] : null;
801
+ const accepted = current?.id === item.id && row?.SourceOrigin === item.origin && HASH.test(row.SourceRoutingFingerprint)
802
+ && row.ExpiresAtUtc * 1000 <= time + 60000 ? acceptedRoute(row, item, item.binding, time) : null;
803
+ const route = (cooldown.get(item.id) || 0) > time ? original(item, 'BackupFailureCooldown')
804
+ : accepted || original(item, 'VerifiedAliasRouteUnavailable');
805
+ put(cache, item.id, { route, until: route.useBackup ? route.expiresAt : time + 15000 });
806
+ pending.delete(item.id); job.done(route);
807
+ }
808
+ }
809
+ }
810
+ return {
811
+ eligible: input => !!pick(input),
812
+ async resolve(input) {
813
+ const item = pick(input); if (!item) return null;
814
+ if ((cooldown.get(item.id) || 0) > now()) return original(item, 'BackupFailureCooldown');
815
+ const old = cache.get(item.id); if (old && old.until > now()) return old.route;
816
+ if (pending.has(item.id)) return pending.get(item.id);
817
+ let done; const result = new Promise(resolve => { done = resolve; }); pending.set(item.id, result);
818
+ queue.push({ item, done }); if (!scheduled) { scheduled = true; queueMicrotask(flush); } return result;
819
+ },
820
+ reportFailure(input) { const item = pick(input); if (!item) return null;
821
+ cache.delete(item.id); put(cooldown, item.id, now() + 30000); return original(item, 'BackupFailureCooldown'); },
822
+ clear() { cache.clear(); cooldown.clear(); }
823
+ };
824
+ }
825
+
826
+ function createPublicResourceClient({ getContext, post, now = Date.now, fetchHead = globalThis.fetch }) {
827
+ let active = '', binding = null, bootstrap = null, expires = 0, generation = 0;
828
+ let queue = [], scheduled = false;
829
+ const context = () => {
830
+ const value = getContext() || {};
831
+ return { tenant: value.tenant, primaryBase: value.primaryBase?.replace(/\/+$/, ''),
832
+ backupOrigin: value.backupOrigin?.replace(/\/+$/, '') };
833
+ };
834
+ const signature = value => JSON.stringify(value);
835
+ const router = createPublicResourceRouter({ getBinding: () => binding || context(), resolveBatch: data => post(data), now });
836
+ const aliases = createExactAliasRouter({ getBinding: () => binding, post, now });
837
+ const fallback = input => Object.freeze({ useBackup: false,
838
+ url: legacyPublicFallback(input, context()), primaryUrl: legacyPublicFallback(input, context()), reason: 'BindingUnavailable' });
839
+ async function ensureBinding() {
840
+ const current = context(), id = signature(current);
841
+ if (active !== id) { active = id; binding = null; bootstrap = null; expires = 0; generation++; router.clear(); aliases.clear(); }
842
+ if (binding && expires > now()) return true;
843
+ if (!binding && expires > now()) return false;
844
+ if (bootstrap) return bootstrap;
845
+ if (!current.backupOrigin || !IDENTIFIER.test(current.tenant || '')) return false;
846
+ const revision = generation;
847
+ const run = (async () => {
848
+ let timer;
849
+ try {
850
+ const response = await Promise.race([post({ ObjectKeys: [], OsClient: current.tenant }),
851
+ new Promise((_, reject) => { timer = setTimeout(() => reject(Error('BindingTimeout')), 5000); })]);
852
+ const data = response?.Data, source = data?.Binding;
853
+ if (generation !== revision || signature(context()) !== id || response?.Code !== 1 || data?.Protocol !== 'GenericPublicResourceRoutes/1' || !source
854
+ || source.PrimaryBase?.replace(/\/+$/, '') !== current.primaryBase
855
+ || source.BackupOrigin?.replace(/\/+$/, '') !== current.backupOrigin
856
+ || source.TargetConfigHash !== data.TargetConfigHash) return false;
857
+ const candidate = { tenant: current.tenant, storageOwnerTenant: source.StorageOwnerTenant,
858
+ primaryBase: source.PrimaryBase, backupOrigin: source.BackupOrigin, publicBucket: source.PublicBucket,
859
+ privateBucket: '', targetConfigHash: source.TargetConfigHash,
860
+ aliasUrlRoutingProtocol: source.AliasUrlRoutingProtocol,
861
+ aliasCandidates: source.AliasUrlRoutingProtocol === 'PublicSourceUrlRoutes/1'
862
+ ? (source.AliasCandidates || []).map(row => plainBase(row.Origin, true)) : [],
863
+ sourceMappings: (source.SourceMappings || []).map(row => ({ origin: row.Origin, pathPrefix: row.PathPrefix || '' })) };
864
+ if (candidate.aliasCandidates.length > 50 || new Set(candidate.aliasCandidates).size !== candidate.aliasCandidates.length) return false;
865
+ bindingSnapshot(candidate);
866
+ binding = candidate; expires = now() + 60000; return true;
867
+ } catch { return false; }
868
+ finally { clearTimeout(timer); }
869
+ })();
870
+ bootstrap = run;
871
+ try { const ready = await run;
872
+ if (!ready && revision === generation && signature(context()) === id) { binding = null; expires = now() + 15000; }
873
+ return ready;
874
+ } finally { if (bootstrap === run) bootstrap = null; }
875
+ }
876
+ async function flush() {
877
+ scheduled = false; const jobs = queue; queue = [];
878
+ for (let start = 0; start < jobs.length; start += 100) {
879
+ const slice = jobs.slice(start, start + 100);
880
+ try { const routes = await router.resolveMany(slice.map(job => job.input));
881
+ slice.forEach((job, index) => job.done(signature(context()) === job.context ? routes[index] : fallback(job.input))); }
882
+ catch { slice.forEach(job => job.done(fallback(job.input))); }
883
+ }
884
+ }
885
+ async function resolve(input) {
886
+ // 只有明确公有输入才触发任何匿名解析;旧 Limit 缺失由调用者保持原兼容分支。
887
+ if (!input || input.private !== false) return Object.freeze({ useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' });
888
+ const id = signature(context());
889
+ if (!await ensureBinding() || id !== signature(context())) return fallback(input);
890
+ if (aliases.eligible(input)) {
891
+ const revision = generation, route = await aliases.resolve(input);
892
+ return revision === generation && id === signature(context()) ? route || fallback(input) : fallback(input);
893
+ }
894
+ return await new Promise(done => { queue.push({ input, done, context: id });
895
+ if (!scheduled) { scheduled = true; queueMicrotask(flush); } });
896
+ }
897
+ const reportFailure = input => input?.private === false ? aliases.reportFailure(input) || router.reportFailure(input) : fallback({ private: true });
898
+ return Object.freeze({ resolve,
899
+ resolveMany(inputs) { return Promise.all(inputs.map(resolve)); },
900
+ reportFailure,
901
+ async resolveForNavigation(input) {
902
+ const id = signature(context()), route = await resolve(input);
903
+ if (id !== signature(context())) return fallback(input);
904
+ if (!route.useBackup) return route;
905
+ const revision = generation;
906
+ if (!(route.expiresAt > now())) return fallback(input);
907
+ // 新窗口无法监听跨域导航失败。打开前只做一次有界 HEAD,失败回原地址,不循环重试。
908
+ const controller = new AbortController(), timer = setTimeout(() => controller.abort(), 3000);
909
+ try {
910
+ const response = await fetchHead(route.url, { method: 'HEAD', credentials: 'omit', redirect: 'error', signal: controller.signal });
911
+ if (id !== signature(context()) || revision !== generation || !(route.expiresAt > now())) return fallback(input);
912
+ return response.ok ? route : reportFailure(input);
913
+ } catch { return reportFailure(input); }
914
+ finally { clearTimeout(timer); }
915
+ },
916
+ clear() { generation++; binding = null; bootstrap = null; expires = 0; router.clear(); aliases.clear(); }
917
+ });
918
+ }
919
+
920
+ // 历史 Limit 缺失不是公开声明,不能据此新增匿名 NAS 访问。
921
+ function isExplicitPublicResource(metadata, configuredLimit) {
922
+ return metadata && typeof metadata === 'object' && Object.hasOwn(metadata, 'Limit')
923
+ ? metadata.Limit === false : configuredLimit === false;
924
+ }
925
+
926
+ /** 表单实例、租户、记录、字段及文件身份共同限定异步地址赋值。 */
927
+ function createPublicResourceAssignmentGuard({ getContext, contains }) {
928
+ const pending = new Map(); let disposed = false;
929
+ const current = ticket => {
930
+ if (disposed || pending.get(ticket.key) !== ticket) return false;
931
+ const context = getContext();
932
+ return Object.keys(ticket.context).every(name => context[name] === ticket.context[name])
933
+ && contains(ticket.path, ticket.fileId);
934
+ };
935
+ return Object.freeze({
936
+ begin(key, path, fileId = null) {
937
+ const ticket = { key, path, fileId, context: { ...getContext() }, route: null, fallbackUsed: false };
938
+ pending.set(key, ticket); return ticket;
939
+ },
940
+ current,
941
+ get(key) { return pending.get(key); },
942
+ tickets() { return [...pending.values()]; },
943
+ invalidate(key) { pending.delete(key); },
944
+ clear() { pending.clear(); },
945
+ dispose() { disposed = true; pending.clear(); }
946
+ });
947
+ }
948
+
949
+ return createPublicResourceClient;
950
+ })();
951
+ // END GENERATED PUBLIC RESOURCE ROUTER
952
+
572
953
  export function createMicroiV8(options = {}) {
573
954
  // 运行时配置可通过 createMicroiV8(options) 或 client.configure(next) 覆盖。
574
955
  let config = {
575
956
  apiBase: '',
576
957
  webBase: '',
577
958
  fileServer: '',
959
+ fileServerBak: '',
578
960
  osClient: '',
579
961
  token: '',
580
962
  clientType: getUni() ? 'Mobile' : 'PC',
@@ -606,6 +988,9 @@ export function createMicroiV8(options = {}) {
606
988
 
607
989
  // 更新配置后立即刷新并发队列,保证 maxConcurrent 热更新生效。
608
990
  function configure(next = {}) {
991
+ if (['apiBase','osClient','fileServer','fileServerBak'].some(key => Object.prototype.hasOwnProperty.call(next,key) && next[key] !== config[key])) {
992
+ sdkPublicResourceEpoch++; sdkPublicResourceClient?.clear();
993
+ }
609
994
  config = { ...config, ...next };
610
995
  if (Object.prototype.hasOwnProperty.call(next, 'maxConcurrent')) {
611
996
  runQueued = createQueue(config.maxConcurrent);
@@ -961,6 +1346,63 @@ export function createMicroiV8(options = {}) {
961
1346
  }
962
1347
 
963
1348
  // 资源地址统一过滤占位图,并兼容 HDFS 私有文件、FileServer 和绝对地址。
1349
+ // Public media selection is asynchronous. assetUrl remains a synchronous legacy API.
1350
+ let sdkPublicResourceClient = null;
1351
+ let sdkPublicResourceEpoch = 0;
1352
+ function sdkPublicMetadata(value, path, depth = 0) {
1353
+ if (depth > 8 || value == null) return null;
1354
+ if (typeof value === 'string' && /^[\[{]/.test(value.trim())) {
1355
+ try { return sdkPublicMetadata(JSON.parse(value), path, depth + 1); } catch { return null; }
1356
+ }
1357
+ if (Array.isArray(value)) {
1358
+ for (const entry of value) { const selected = sdkPublicMetadata(entry, path, depth + 1); if (selected) return selected; }
1359
+ return null;
1360
+ }
1361
+ return typeof value === 'object' && extractUploadPath(value) === path ? value : null;
1362
+ }
1363
+ function sdkPublicInputAllowed(value, options = {}) {
1364
+ const path = extractUploadPath(value), meta = sdkPublicMetadata(value, path);
1365
+ if (options.private === true || (meta && (
1366
+ (Object.prototype.hasOwnProperty.call(meta, 'Limit') && meta.Limit !== false)
1367
+ || meta.Private === true || meta.IsPrivate === true || meta.Public === false))) return false;
1368
+ return options.private === false || meta?.Limit === false || meta?.Public === true;
1369
+ }
1370
+ function sdkPublicContext() {
1371
+ return { tenant: config.osClient, primaryBase: config.fileServer || '',
1372
+ backupOrigin: config.fileServerBak || client.SysConfig?.FileServerBak || '' };
1373
+ }
1374
+ function sdkPublicIdentity() {
1375
+ return JSON.stringify([sdkPublicResourceEpoch, config.apiBase, sdkPublicContext()]);
1376
+ }
1377
+ function getSdkPublicResourceClient() {
1378
+ if (!sdkPublicResourceClient) sdkPublicResourceClient = createSdkPublicResourceClient({
1379
+ getContext: sdkPublicContext,
1380
+ post: data => apiEngineRun('mci-file-resource-public-url', data, { checkCode: false, silentError: true, timeout: 5000 })
1381
+ });
1382
+ return sdkPublicResourceClient;
1383
+ }
1384
+ async function resolvePublicResource(value, options = {}) {
1385
+ const path = extractUploadPath(value);
1386
+ if (!path || isBlockedAsset(path) || !sdkPublicInputAllowed(value, options))
1387
+ return { useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' };
1388
+ // Local files and API-owned protected/runtime endpoints keep their existing transport.
1389
+ if (/^(?:data:|blob:|file:|\.?\/static\/)/i.test(path) || /^\/?(?:file\/|micro-app\/v3(?:\/|$))/i.test(path))
1390
+ return { useBackup: false, url: assetUrl(value), primaryUrl: assetUrl(value), reason: 'LocalOrApiResource' };
1391
+ const identity = sdkPublicIdentity(), publicClient = getSdkPublicResourceClient();
1392
+ const route = options.navigation === true
1393
+ ? await publicClient.resolveForNavigation({ path, private: false })
1394
+ : await publicClient.resolve({ path, private: false });
1395
+ // The portable SDK also binds the API base, which is not a public storage binding field.
1396
+ return identity === sdkPublicIdentity() ? route
1397
+ : { useBackup: false, url: '', primaryUrl: '', reason: 'ClientContextChanged' };
1398
+ }
1399
+ function reportPublicResourceFailure(value, options = {}) {
1400
+ if (!sdkPublicInputAllowed(value, options)) return { useBackup: false, url: '', primaryUrl: '', reason: 'ExplicitPublicRequired' };
1401
+ const path = extractUploadPath(value);
1402
+ return sdkPublicResourceClient?.reportFailure({ path, private: false })
1403
+ || { useBackup: false, url: assetUrl(value), primaryUrl: assetUrl(value), reason: 'PrimaryFallback' };
1404
+ }
1405
+
964
1406
  function assetUrl(value) {
965
1407
  const picked = extractUploadPath(value);
966
1408
  if (!picked || isBlockedAsset(picked)) return '';
@@ -999,6 +1441,7 @@ export function createMicroiV8(options = {}) {
999
1441
  async function resolveFileUrl(filePathName, options = {}) {
1000
1442
  const path = extractUploadPath(filePathName);
1001
1443
  if (!path || isBlockedAsset(path)) return '';
1444
+ if (sdkPublicInputAllowed(filePathName, options)) return (await resolvePublicResource(filePathName, options)).url;
1002
1445
  if (/^(https?:|blob:|data:|file:)/i.test(path)) return assetUrl(path);
1003
1446
  // 文件实际私有标记和私有字段上下文优先,禁止因字段曾改为公有而错误降级 CDN。
1004
1447
  const privateAccess = options.private === true || hasPrivateUploadFlag(filePathName);
@@ -1534,6 +1977,8 @@ export function createMicroiV8(options = {}) {
1534
1977
  setCurrentUser: setUser,
1535
1978
  getCurrentUser: getUser,
1536
1979
  assetUrl,
1980
+ resolvePublicResource,
1981
+ reportPublicResourceFailure,
1537
1982
  sanitizeAssetUrl: assetUrl,
1538
1983
  resolveAssetUrl: assetUrl,
1539
1984
  resolveAvatarUrl: resolveFileUrl,