@microi.net/cli 5.3.8 → 5.4.0

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.
@@ -151,42 +151,59 @@ function deserializeUser(value) {
151
151
  return parseMaybeJson(value, null);
152
152
  }
153
153
 
154
- // 吾码文件字段可能来自上传控件、HDFS 接口、字符串或 JSON 字符串,这里统一抽取可用路径。
155
- function extractUploadPath(value) {
156
- if (!value) return '';
157
- if (typeof value === 'object') {
158
- const raw = Array.isArray(value) ? (value[0] || {}) : value;
159
- if (typeof raw === 'string') return extractUploadPath(raw);
160
- return raw.Url || raw.FileUrl || raw.FileURL || raw.PreviewUrl || raw.PreviewURL ||
161
- raw.FullUrl || raw.Path || raw.FilePathName || raw.FilePath || raw.FullPath || '';
162
- }
163
-
164
- const text = String(value || '').trim();
165
- if (!text) return '';
166
- if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) {
167
- return extractUploadPath(parseMaybeJson(text, text));
168
- }
169
- return text;
170
- }
171
-
172
- function normalizeUploadValue(value) {
173
- if (!value) return [];
174
- if (Array.isArray(value)) return value.map(extractUploadPath).filter(Boolean);
175
- if (typeof value === 'object') {
176
- const path = extractUploadPath(value);
177
- return path ? [path] : [];
178
- }
154
+ const uploadPathKeys = [
155
+ 'Url', 'FileUrl', 'FileURL', 'PreviewUrl', 'PreviewURL', 'FullUrl',
156
+ 'Path', 'FilePathName', 'FilePath', 'FullPath', 'Src', 'Href',
157
+ 'url', 'fileUrl', 'previewUrl', 'fullUrl',
158
+ 'path', 'filePathName', 'filePath', 'fullPath', 'src', 'href'
159
+ ];
160
+
161
+ // 吾码文件字段可能是旧版绝对/相对字符串、新版单图对象、多图数组,
162
+ // 也可能是上述结构序列化后的 JSON 字符串;这里统一抽取第一个有效路径。
163
+ function extractUploadPath(value, depth = 0) {
164
+ if (depth > 6 || value === null || value === undefined) return '';
165
+ if (Array.isArray(value)) {
166
+ for (const item of value) {
167
+ const path = extractUploadPath(item, depth + 1);
168
+ if (path) return path;
169
+ }
170
+ return '';
171
+ }
172
+ if (typeof value === 'object') {
173
+ for (const key of uploadPathKeys) {
174
+ const path = extractUploadPath(value[key], depth + 1);
175
+ if (path) return path;
176
+ }
177
+ return '';
178
+ }
179
+
180
+ const text = String(value || '').trim();
181
+ if (!text) return '';
182
+ if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) {
183
+ return extractUploadPath(parseMaybeJson(text, text), depth + 1);
184
+ }
185
+ return text;
186
+ }
179
187
 
180
- const text = String(value || '').trim();
181
- if (!text) return [];
182
- if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
183
- const parsed = parseMaybeJson(text, null);
184
- if (Array.isArray(parsed)) return parsed.map(extractUploadPath).filter(Boolean);
185
- const path = extractUploadPath(parsed);
186
- return path ? [path] : [];
187
- }
188
- return [text];
189
- }
188
+ function normalizeUploadEntries(value) {
189
+ if (!value) return [];
190
+ let parsed = value;
191
+ if (typeof parsed === 'string') {
192
+ const text = parsed.trim();
193
+ if (!text) return [];
194
+ if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
195
+ parsed = parseMaybeJson(text, text);
196
+ }
197
+ }
198
+ const items = Array.isArray(parsed) ? parsed : [parsed];
199
+ return items.map((item) => ({ item, path: extractUploadPath(item) })).filter((entry) => entry.path);
200
+ }
201
+
202
+ // 旧调用方依赖路径数组,继续保持返回类型;需要 Name/Size/Id 等元数据时
203
+ // 使用 normalizeUploadEntries,禁止把上传对象 String(...) 后再解析。
204
+ function normalizeUploadValue(value) {
205
+ return normalizeUploadEntries(value).map((entry) => entry.path);
206
+ }
190
207
 
191
208
  function normalizeUploadData(body) {
192
209
  const raw = Array.isArray(body && body.Data) ? (body.Data[0] || {}) : ((body && body.Data) || {});
@@ -940,7 +957,9 @@ export function createMicroiV8(options = {}) {
940
957
  if (!picked || isBlockedAsset(picked)) return '';
941
958
  if (/^(https?:|data:|blob:|file:)/i.test(picked)) return picked;
942
959
  if (isLocalPackagedAsset(picked)) return picked;
943
- if (/^\/?file\//i.test(picked)) return joinUrl(config.apiBase, picked);
960
+ if (/^\/?file\//i.test(picked) || /^\/?micro-app\/v3(?:\/|$)/i.test(picked)) {
961
+ return joinUrl(config.apiBase, picked);
962
+ }
944
963
  if (/^\//.test(picked) || /^[a-z0-9_-]+\//i.test(picked)) return joinUrl(config.fileServer || config.apiBase, picked);
945
964
  return picked;
946
965
  }
@@ -1509,9 +1528,10 @@ export function createMicroiV8(options = {}) {
1509
1528
  resolveAvatarUrl: resolveFileUrl,
1510
1529
  resolveFileUrl,
1511
1530
  isBlockedAsset,
1512
- extractUploadPath,
1513
- normalizeUploadValue,
1514
- uploadFile,
1531
+ extractUploadPath,
1532
+ normalizeUploadValue,
1533
+ normalizeUploadEntries,
1534
+ uploadFile,
1515
1535
  getSafeArea,
1516
1536
  formatDate,
1517
1537
  toNumber,
@@ -11,13 +11,25 @@ description: Microi UniApp、H5、商城与 PC 页面中的上传图片、附件
11
11
 
12
12
  ## 先识别资源类型
13
13
 
14
- Microi 上传字段通常保存对象存储 Key 或相对路径,例如 `/demo/product/20260101/p1.jpg`。页面不能把相对路径直接交给 `<image>` / `<img>`,也不能一律拼接 API Base。
14
+ Microi 上传字段存在四种必须长期兼容的存储形态,不能假设字段一定是字符串:
15
+
16
+ | 存储形态 | 示例 | 兼容要求 |
17
+ |---|---|---|
18
+ | 旧版绝对地址字符串 | `https://cdn.example.com/a.png` | 校验协议和允许域名后原样使用,禁止再次拼接 `FileServer` |
19
+ | 旧版相对路径字符串 | `/tenant/a.png` | 仅此类纯相对对象 Key 拼接当前租户 `FileServer` |
20
+ | 新版单图对象 | `{ FilePathName:'/tenant/a.png', Name:'a.png' }` | 提取路径字段;保留对象的名称、大小、Id 等元数据,不得把整个对象转成字符串 |
21
+ | 新版多图数组 | `[{ FilePathName:'/tenant/a.png' }, ...]` | 逐项解析并保持原顺序;单图展示取第一个有效路径,多图展示返回全部有效项 |
22
+
23
+ 数据库或接口还可能返回上述对象/数组的 JSON 字符串,解析器也必须兼容。标准路径键按以下顺序识别:`Url`、`FileUrl`、`FileURL`、`PreviewUrl`、`PreviewURL`、`FullUrl`、`Path`、`FilePathName`、`FilePath`、`FullPath`、`Src`、`Href`,并兼容相应 camelCase 键。
24
+
25
+ 页面不能把相对路径直接交给 `<image>` / `<img>`,也不能一律拼接 API Base。项目已使用 `microi.v8.js` 时,优先直接复用 `V8.extractUploadPath`、返回路径数组的 `V8.normalizeUploadValue`、保留 `{ item, path }` 元数据的 `V8.normalizeUploadEntries`、`V8.assetUrl` / `V8.resolveAssetUrl` 和 `V8.resolveFileUrl`,不得在业务页面另造一套不完整的解析规则。
15
26
 
16
27
  | 类型 | 处理方式 |
17
28
  |---|---|
18
29
  | 已允许的 `https://` 绝对地址 | 校验协议和允许域名后使用 |
19
30
  | 公有对象存储相对路径 | 拼接当前租户 `FileServer` |
20
31
  | `/file/...` 平台本地文件路由 | 拼接当前 API Base |
32
+ | `/micro-app/v3/...` 应用稳定解析路由 | 拼接当前 API Base;这是动态指针路由,不是 HDFS 对象 Key,禁止拼接 `FileServer` |
21
33
  | 私有对象 | 后端鉴权后签发短期 URL,前端只使用临时 URL |
22
34
  | `blob:` | 仅用于本页创建且能及时 `revokeObjectURL` 的预览 |
23
35
  | `data:` | 仅允许经过大小和 MIME 校验的图片预览;禁止用于富文本任意 HTML |
@@ -26,15 +38,54 @@ Microi 上传字段通常保存对象存储 Key 或相对路径,例如 `/demo/
26
38
 
27
39
  ## 统一资源解析函数
28
40
 
29
- 项目应在共享请求/资源模块中实现一个 `resolveAssetUrl`,所有页面复用同一逻辑:
41
+ 未使用 `microi.v8.js` 的项目,应在共享请求/资源模块中实现等价的“提取上传路径 + 解析资源 URL”两层函数,所有页面复用同一逻辑:
30
42
 
31
43
  ```js
44
+ const uploadPathKeys = [
45
+ 'Url', 'FileUrl', 'FileURL', 'PreviewUrl', 'PreviewURL', 'FullUrl',
46
+ 'Path', 'FilePathName', 'FilePath', 'FullPath', 'Src', 'Href',
47
+ 'url', 'fileUrl', 'previewUrl', 'fullUrl', 'path', 'filePathName', 'filePath', 'fullPath', 'src', 'href'
48
+ ];
49
+
50
+ export function extractUploadPath(raw, depth = 0) {
51
+ if (depth > 4 || raw === null || raw === undefined) return '';
52
+ if (Array.isArray(raw)) {
53
+ for (const item of raw) {
54
+ const path = extractUploadPath(item, depth + 1);
55
+ if (path) return path;
56
+ }
57
+ return '';
58
+ }
59
+ if (typeof raw === 'object') {
60
+ for (const key of uploadPathKeys) {
61
+ const path = extractUploadPath(raw[key], depth + 1);
62
+ if (path) return path;
63
+ }
64
+ return '';
65
+ }
66
+ const value = String(raw).trim();
67
+ if (!value) return '';
68
+ if (/^[{[]/.test(value)) {
69
+ try { return extractUploadPath(JSON.parse(value), depth + 1); } catch (_) {}
70
+ }
71
+ return value;
72
+ }
73
+
74
+ export function normalizeUploadValue(raw) {
75
+ let value = raw;
76
+ if (typeof value === 'string' && /^[{[]/.test(value.trim())) {
77
+ try { value = JSON.parse(value); } catch (_) {}
78
+ }
79
+ const items = Array.isArray(value) ? value : [value];
80
+ return items.map(item => ({ item, path: extractUploadPath(item) })).filter(entry => entry.path);
81
+ }
82
+
32
83
  export function resolveAssetUrl(raw, {
33
84
  apiBase,
34
85
  fileServer,
35
86
  allowedHosts = []
36
87
  }) {
37
- const value = String(raw || '').trim();
88
+ const value = extractUploadPath(raw);
38
89
  if (!value) return '';
39
90
 
40
91
  if (/^https:\/\//i.test(value)) {
@@ -44,6 +95,9 @@ export function resolveAssetUrl(raw, {
44
95
  if (/^blob:/i.test(value)) return value;
45
96
  if (/^data:image\/(png|jpeg|gif|webp);base64,/i.test(value)) return value;
46
97
  if (value.startsWith('/file/')) return `${apiBase.replace(/\/$/, '')}${value}`;
98
+ if (/^\/?micro-app\/v3(?:\/|$)/i.test(value)) {
99
+ return `${apiBase.replace(/\/$/, '')}/${value.replace(/^\/+/, '')}`;
100
+ }
47
101
 
48
102
  const relative = value.replace(/^\/+/, '');
49
103
  return `${fileServer.replace(/\/$/, '')}/${relative}`;
@@ -103,7 +157,9 @@ function imageUrl(path) {
103
157
 
104
158
  ## 验收清单
105
159
 
106
- - [ ] 公有对象、本地文件、私有对象、绝对 URL 的路径分支均有测试
160
+ - [ ] 公有对象、本地文件、`/micro-app/v3` 动态路由、私有对象、绝对 URL 的路径分支均有测试
161
+ - [ ] 绝对地址字符串、相对路径字符串、单图对象、多图数组及其 JSON 字符串均有测试
162
+ - [ ] 单图从数组取第一个有效路径,多图保持原顺序;解析 URL 时不丢失或改写上传对象元数据
107
163
  - [ ] 切换两个 `OsClient` 后使用各自 `FileServer`,无跨租户路径或缓存复用
108
164
  - [ ] 私有 URL 越权、过期、篡改签名和复制到其它账号均失败
109
165
  - [ ] 网络面板中没有硬编码客户域名、真实租户或永久签名 URL