@microi.net/cli 4.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/mcp-codex-stdio-adapter.js +189 -0
- package/dist/mcp-server.js +972 -0
- package/dist/mcp-trae-windows-launcher.cmd +21 -0
- package/dist/microi-cli-mcp.js +7 -0
- package/dist/microi-cli.js +1645 -0
- package/dist/microi-skills.meta.json +335 -0
- package/dist/microi.skills/.microi-skills-version.json +6 -0
- package/dist/microi.skills/README.md +276 -0
- package/dist/microi.skills/ai-engine/SKILL.md +140 -0
- package/dist/microi.skills/ai-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/app-store/SKILL.md +105 -0
- package/dist/microi.skills/app-store/agents/openai.yaml +4 -0
- package/dist/microi.skills/business-blueprint/SKILL.md +184 -0
- package/dist/microi.skills/datasource-engine/SKILL.md +89 -0
- package/dist/microi.skills/datasource-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/dos-orm/SKILL.md +76 -0
- package/dist/microi.skills/dos-orm/references/api-reference.md +229 -0
- package/dist/microi.skills/job-engine/SKILL.md +141 -0
- package/dist/microi.skills/job-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/message-notification/SKILL.md +113 -0
- package/dist/microi.skills/message-notification/agents/openai.yaml +6 -0
- package/dist/microi.skills/message-notification/references/contracts.md +99 -0
- package/dist/microi.skills/microi-ai-app-auth.js +651 -0
- package/dist/microi.skills/microi-ai-application/SKILL.md +80 -0
- package/dist/microi.skills/microi-ai-application/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-ai-application/references/frontend-baseline.md +164 -0
- package/dist/microi.skills/microi-client-frontend/SKILL.md +562 -0
- package/dist/microi.skills/microi-datasource-mapping/SKILL.md +108 -0
- package/dist/microi.skills/microi-db-schema/SKILL.md +170 -0
- package/dist/microi.skills/microi-db-schema/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-db-schema/references/core-tables.md +695 -0
- package/dist/microi.skills/microi-db-schema/references/form-component-options.md +256 -0
- package/dist/microi.skills/microi-db-schema/references/schema-overview.md +203 -0
- package/dist/microi.skills/microi-db-schema/references/schema.md +647 -0
- package/dist/microi.skills/microi-db-schema/references/table-catalog.md +1607 -0
- package/dist/microi.skills/microi-deployment/SKILL.md +117 -0
- package/dist/microi.skills/microi-deployment/references/deployment-matrix.md +94 -0
- package/dist/microi.skills/microi-docs-coverage/SKILL.md +91 -0
- package/dist/microi.skills/microi-docs-coverage/references/capability-map.md +65 -0
- package/dist/microi.skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +887 -0
- package/dist/microi.skills/microi-form-engine/SKILL.md +159 -0
- package/dist/microi.skills/microi-form-engine/references/component-catalog.md +116 -0
- package/dist/microi.skills/microi-form-engine/references/data-source-events.md +117 -0
- package/dist/microi.skills/microi-form-layout/SKILL.md +373 -0
- package/dist/microi.skills/microi-frontend-sdk/SKILL.md +304 -0
- package/dist/microi.skills/microi-left-right-layout/SKILL.md +132 -0
- package/dist/microi.skills/microi-microservice/SKILL.md +115 -0
- package/dist/microi.skills/microi-microservice/references/runtime-delivery.md +145 -0
- package/dist/microi.skills/microi-mobile-app-quality/SKILL.md +436 -0
- package/dist/microi.skills/microi-solution-quotation/SKILL.md +76 -0
- package/dist/microi.skills/microi-solution-quotation/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -0
- package/dist/microi.skills/microi-system-delivery/SKILL.md +446 -0
- package/dist/microi.skills/microi-ui/SKILL.md +321 -0
- package/dist/microi.skills/microi-uniapp-frontend/SKILL.md +483 -0
- package/dist/microi.skills/microi.v8.js +1758 -0
- package/dist/microi.skills/module-engine/SKILL.md +131 -0
- package/dist/microi.skills/module-engine/references/module-config.md +174 -0
- package/dist/microi.skills/page-engine/SKILL.md +397 -0
- package/dist/microi.skills/performance-testing/SKILL.md +207 -0
- package/dist/microi.skills/playwright-e2e/SKILL.md +769 -0
- package/dist/microi.skills/print-engine/SKILL.md +237 -0
- package/dist/microi.skills/production-readonly-audit/SKILL.md +39 -0
- package/dist/microi.skills/report-engine/SKILL.md +69 -0
- package/dist/microi.skills/report-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/search-engine/SKILL.md +73 -0
- package/dist/microi.skills/search-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/spider-engine/SKILL.md +188 -0
- package/dist/microi.skills/translate-engine/SKILL.md +91 -0
- package/dist/microi.skills/translate-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/ui-design/SKILL.md +1575 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/app.js +54 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/index.html +163 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/styles.css +311 -0
- package/dist/microi.skills/ui-design/assets/templates/MCI-DESIGN.md +98 -0
- package/dist/microi.skills/ui-design/references/design-pattern-library.md +171 -0
- package/dist/microi.skills/ui-design/references/mci-design-contract.md +84 -0
- package/dist/microi.skills/ui-design/references/motion-and-media.md +71 -0
- package/dist/microi.skills/ui-design/references/product-flow-recipes.md +94 -0
- package/dist/microi.skills/uniapp-mall-assets/SKILL.md +105 -0
- package/dist/microi.skills/v8-api-config/SKILL.md +272 -0
- package/dist/microi.skills/v8-cache-pattern/SKILL.md +286 -0
- package/dist/microi.skills/v8-crud-api/SKILL.md +398 -0
- package/dist/microi.skills/v8-debugging/SKILL.md +279 -0
- package/dist/microi.skills/v8-explorer-tree/SKILL.md +224 -0
- package/dist/microi.skills/v8-export-import/SKILL.md +590 -0
- package/dist/microi.skills/v8-file-upload/SKILL.md +497 -0
- package/dist/microi.skills/v8-formengine-http/SKILL.md +218 -0
- package/dist/microi.skills/v8-frontend-events/SKILL.md +349 -0
- package/dist/microi.skills/v8-frontend-events/references/bluetooth-print-api.md +107 -0
- package/dist/microi.skills/v8-frontend-events/references/bluetooth-print.md +185 -0
- package/dist/microi.skills/v8-http-integration/SKILL.md +379 -0
- package/dist/microi.skills/v8-image-processing/SKILL.md +187 -0
- package/dist/microi.skills/v8-image-processing/agents/openai.yaml +4 -0
- package/dist/microi.skills/v8-image-processing/references/api-reference.md +620 -0
- package/dist/microi.skills/v8-menu-buttons/SKILL.md +661 -0
- package/dist/microi.skills/v8-mongodb/SKILL.md +149 -0
- package/dist/microi.skills/v8-mq-mqtt/SKILL.md +227 -0
- package/dist/microi.skills/v8-saas-multi-tenant/SKILL.md +193 -0
- package/dist/microi.skills/v8-security/SKILL.md +417 -0
- package/dist/microi.skills/v8-sql-query/SKILL.md +290 -0
- package/dist/microi.skills/v8-table-event/SKILL.md +385 -0
- package/dist/microi.skills/v8-template-engine/SKILL.md +165 -0
- package/dist/microi.skills/v8-utilities/SKILL.md +79 -0
- package/dist/microi.skills/v8-utilities/references/client-api-index.md +136 -0
- package/dist/microi.skills/v8-utilities/references/platform-http-routes.md +80 -0
- package/dist/microi.skills/v8-utilities/references/server-api-index.md +129 -0
- package/dist/microi.skills/v8-workflow/SKILL.md +322 -0
- package/dist/microi.skills/workspace-conventions/SKILL.md +479 -0
- package/package.json +40 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: v8-file-upload
|
|
3
|
+
description: Microi V8 与 MCP 文件上传下载指南。用于处理流式 AI 应用发布、V8.FilesByteBase64、V8.Method.Upload、私有文件 URL、文件响应、HDFS、OSS、MinIO 和 S3 存储。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microi V8 文件上传下载
|
|
7
|
+
|
|
8
|
+
你正在为 Microi 吾码平台编写文件上传/下载/返回相关代码。平台分布式存储(HDFS)支持阿里云OSS、MinIO、亚马逊S3,存储方案由 SaaS 引擎按租户配置。
|
|
9
|
+
|
|
10
|
+
## 核心 API
|
|
11
|
+
|
|
12
|
+
| API | 说明 |
|
|
13
|
+
|-----|------|
|
|
14
|
+
| `V8.FilesByteBase64` | 接收上传时携带的文件字典 `{ FileName: base64 }` |
|
|
15
|
+
| `V8.Method.Upload({...})` | 服务端上传文件到 HDFS(推荐) |
|
|
16
|
+
| `V8.Method.GetPrivateFileUrl({FilePathName})` | 生成私有桶临时访问 URL |
|
|
17
|
+
| `V8.Http.GetResponse({Url}).RawBytes` | 下载远程文件为字节数组 |
|
|
18
|
+
| 接口返回 `{ FileName, ContentType, FileByteBase64 }` | 接口直接响应文件 |
|
|
19
|
+
|
|
20
|
+
## 第三方数据库附件迁移
|
|
21
|
+
|
|
22
|
+
当第三方表只保存附件路径时,先用 `microi_inspect_external_database` / `microi_query_external_database` 或 `V8.Dbs.<DbKey>` 查询记录。`microi_import_external_attachment` 允许后端已确认的 `Level >= 9999` 当前用户直接提供 HTTP/HTTPS URL、API 节点可读的本机绝对路径或 UNC 路径。
|
|
23
|
+
|
|
24
|
+
- 导入工具必须显式确认;HTTP、私网、重定向、本机和 UNC 均可访问,但最终能力受 API 服务进程账号、网络、磁盘及对象存储权限约束。
|
|
25
|
+
- 下载与上传使用临时文件和文件流,不经过 Base64;不设固定 20/100 MB 上限,`MaxBytes=0` 或省略表示不设置 MCP 上限,可处理 200/500 MB 或更大文件。
|
|
26
|
+
- 带签名参数或用户凭据的源 URL、鉴权 Header 和本机/UNC 路径不得出现在结果、日志或目标表;脱敏审计只记录来源 SHA-256、类型和字节数,目标字段只保存吾码租户内相对路径。
|
|
27
|
+
- 使用第三方附件 Id/版本作为幂等键,回读目标记录后才标记成功;多节点重投不能重复产生业务附件。
|
|
28
|
+
- 批量迁移应落任务状态表并分页处理,失败可重试;不要让 MCP 一次加载整库路径或大文件集合。
|
|
29
|
+
|
|
30
|
+
可信后端 V8 可用 `V8.Http.GetResponse({ Url: url }).RawBytes` 下载,再用 `System.Convert.ToBase64String` 和 `V8.Method.Upload` 上传。该路径同样必须校验域名、大小、Content-Type、后缀和最终重定向目标。
|
|
31
|
+
|
|
32
|
+
## 接收前端上传的文件
|
|
33
|
+
|
|
34
|
+
前端发起文件上传时,平台自动把文件以 base64 形式注入到 `V8.FilesByteBase64`:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// V8.FilesByteBase64 = { '文件名1.png': 'base64...', '文件名2.pdf': 'base64...' }
|
|
38
|
+
if (!V8.FilesByteBase64) {
|
|
39
|
+
return { Code: 0, Msg: '请上传文件' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var fileNames = Object.keys(V8.FilesByteBase64);
|
|
43
|
+
var firstFile = fileNames[0];
|
|
44
|
+
var firstBase64 = V8.FilesByteBase64[firstFile];
|
|
45
|
+
|
|
46
|
+
// 上传到 HDFS
|
|
47
|
+
var upResult = V8.Method.Upload({
|
|
48
|
+
FilesByteBase64: V8.FilesByteBase64,
|
|
49
|
+
Limit: true, // 业务上传默认私有桶(需临时 URL 访问)
|
|
50
|
+
Preview: false, // true=自动生成预览图
|
|
51
|
+
Path: '/business/orders', // 存储路径前缀
|
|
52
|
+
OsClient: V8.OsClient
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (upResult.Code !== 1) return upResult;
|
|
56
|
+
|
|
57
|
+
// upResult.Data = [{ FileName, Path, FullPath, Size, ... }, ...]
|
|
58
|
+
var filePath = upResult.Data[0].Path; // 相对路径,存数据库
|
|
59
|
+
var fullUrl = upResult.Data[0].FullPath; // 完整 URL(公有桶)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 平台上传分层限制
|
|
63
|
+
|
|
64
|
+
Token 不是无限上传授权。所有 HTTP、表单、V8 和移动端上传入口必须在解码 Base64、解析图片或调用对象存储前执行服务端校验。上传限制分为四层,不能把租户业务值误称为平台硬上限:
|
|
65
|
+
|
|
66
|
+
1. **租户业务配置**:有效正数/布尔值按 `sys_osclients` 当前租户 → 代码默认值解析。租户可以按业务需要提高或降低默认值,不要求安装者维护额外环境变量或修改 `appsettings`。
|
|
67
|
+
2. **平台绝对上限**:最终业务值再与代码内固定灾难保护上限取较小值,租户和安装参数都不能放大。
|
|
68
|
+
3. **HTTP 解析上限**:Kestrel 请求正文与 Multipart 固定为 2048 MB,普通表单单值固定为 128 MB,是所有租户共享的请求解析硬顶。
|
|
69
|
+
4. **字段级限制**:前端 `FileUpload` / `ImgUpload` 的 `MaxSize`、`MaxCount` 等只能与当前租户有效值取更小值,不能提高后端上限,也不能替代服务端校验。
|
|
70
|
+
|
|
71
|
+
业务配置与固定边界:
|
|
72
|
+
|
|
73
|
+
| `sys_osclients` 字段 | 代码默认值 | 平台固定边界 |
|
|
74
|
+
|---|---:|---:|
|
|
75
|
+
| `FileUploadEnabled` | `true` | — |
|
|
76
|
+
| `FileUploadMaxFileMB` | 100 MB | 1024 MB |
|
|
77
|
+
| `FileUploadMaxRequestMB` | 200 MB | 2048 MB |
|
|
78
|
+
| `FileUploadMaxCount` | 10 | 100 |
|
|
79
|
+
| `FileUploadDailyUserQuotaMB` | 2048 MB | 10 TB |
|
|
80
|
+
| `FileUploadDailyTenantQuotaMB` | 20480 MB | 10 TB |
|
|
81
|
+
|
|
82
|
+
- `sys_osclients` 六个字段全部可空;空值、无效值或老数据库缺列时使用代码默认值,不会因升级自动停用上传。`FileUploadMaxRequestMB` 指一次上传所有文件的业务合计大小,不等于 Kestrel HTTP 请求正文上限。
|
|
83
|
+
- 固定灾难保护和 HTTP/Multipart/Form 解析上限不属于安装配置;租户值即使更大也会被这些边界截断。最终单次总量还不能超过帐号或租户的有效日额度,单文件不能超过最终单次总量。
|
|
84
|
+
- `FileUploadEnabled=0` 表示禁用当前租户的交互式上传,不表示绕过限制。平台内部受控任务仍受全局大小硬上限;租户配置刷新应走现有 SaaS 引擎重载和共享 Redis 发布订阅,不能依赖单节点内存。
|
|
85
|
+
- 帐号与租户每日额度在共享 Redis 中用单次原子脚本预留,支持多节点;Redis 不可用时失败关闭,不能降级成无限上传。
|
|
86
|
+
- 额度按 UTC 日期统计。为防并发重试绕过限制,后续对象存储失败也不退还已经预留的额度。
|
|
87
|
+
- 每日额度只阻断短期滥用;对象存储必须另外配置租户/桶总容量、账单告警、生命周期与实际用量对账。Redis 计数不能作为长期容量事实源。
|
|
88
|
+
- 普通交互式用户无论客户端是否传 `Limit:false`,服务端都强制使用私有桶,并只允许平台预定义安全一级目录;公有资产必须经过授权的发布流程或超级管理员。
|
|
89
|
+
- 反向代理、Ingress/IIS 的请求体上限应与进程级 HTTP 解析硬顶协调。字段自身的类型、后缀、大小和数量配置只能进一步收紧当前租户有效值,不能替代平台硬顶。
|
|
90
|
+
|
|
91
|
+
### 复盘:“当前租户已停用文件上传”
|
|
92
|
+
|
|
93
|
+
- 该提示只表示当前运行环境命中的 `sys_osclients.FileUploadEnabled` 被明确设置为 `0/false`。缺列、空值、无效值和新租户都按默认 `true`,不要先把问题归因于 MinIO/HDFS。
|
|
94
|
+
- 新版响应同时返回 `DataAppend.ErrorType=TenantFileUploadDisabled`、`OsClient`、`ConfigField=FileUploadEnabled` 和文档地址;客户端必须保留后端 `Msg/DataAppend`,不能只显示“上传失败”。
|
|
95
|
+
- 处理时先读取当前 API 进程的 `OsClient + OsClientType + OsClientNetwork`,再精确回读同三元组的启用记录并把 `FileUploadEnabled` 改为 `1`。不能仅按租户名批量覆盖其它网络或环境记录。
|
|
96
|
+
- 保存后等待 SaaS 共享配置重载,再分别验证一个小公有图片和一个小私有文件。只有错误转为 endpoint、bucket、签名或 `Invalid URI` 后,才进入对象存储配置排查。
|
|
97
|
+
- 不要通过删除 Redis 日额度 Key、扩大文件大小上限或改成公有桶来解除租户停用;这些动作与开关无关,还会扩大安全风险。
|
|
98
|
+
|
|
99
|
+
### AI / MCP 调整租户上传配额
|
|
100
|
+
|
|
101
|
+
用户明确授权修改某个租户的上传额度时,AI 可以直接使用标准 MCP 完成,不要把应用层提示误判成阿里云 OSS、MinIO 或 S3 的存储配额,也不要先清 Redis:
|
|
102
|
+
|
|
103
|
+
1. `microi_get_table_data(tableName: "sys_osclients")` 按 `OsClient`、`IsEnable=1` 查询,选择 `Id/OsClientType/OsClientNetwork` 和六个 `FileUpload*` 配额字段。
|
|
104
|
+
2. 先以当前服务器的 `OsClientType + OsClientNetwork` 收窄到实际生效记录;只有用户明确要求多个环境保持一致时才扩展范围。逐条调用 `microi_update_form_data`,`row` 必须包含 `Id`,并传 `confirmExecution: "sys_osclients"`。
|
|
105
|
+
3. MB 是存储单位:`20 GB = 20480 MB`。可修改字段为 `FileUploadEnabled`、`FileUploadMaxFileMB`、`FileUploadMaxRequestMB`、`FileUploadMaxCount`、`FileUploadDailyUserQuotaMB`、`FileUploadDailyTenantQuotaMB`。
|
|
106
|
+
4. 保存后逐条远程回读;FormEngine 会排队重载 SaaS 运行配置,再用真实小文件上传做生效冒烟。只看到 MCP 返回“更新成功”不算验收。
|
|
107
|
+
5. 提高每日配额保留当天已用计数,剩余额度为新上限减已用量。计数按 UTC 日期,失败上传不退款;除非用户明确授权事故处置,不得删除 Redis 配额 Key。
|
|
108
|
+
|
|
109
|
+
租户 MCP 只能调整业务层配置;平台固定灾难保护、HTTP/Multipart/Form 解析上限和反向代理上限不能通过 `sys_osclients` 绕过。写入 `sys_osclients` 属于控制面操作,只允许当前租户的 `Level >= 9999` 管理身份,并且必须保留 MCP 审计与写后回读。
|
|
110
|
+
|
|
111
|
+
### UniApp / H5 客户端直传路径规则
|
|
112
|
+
|
|
113
|
+
移动端通过 `/api/HDFS/UniappUpload` 上传时,前端必须走 `microi.v8.js` 的 `V8.uploadFile`,不要在页面里手写 `uni.uploadFile`。客户端上传的 `Path` 与服务端 `V8.Method.Upload` 示例不同,必须是安全相对路径:
|
|
114
|
+
|
|
115
|
+
- 正确:`mall/pay-proof`、`mall/member/avatar`、`order/proof`
|
|
116
|
+
- 错误:`/mall/pay-proof`、`https://...`、`C:\...`、`../x`、`mall//x`、`~x`
|
|
117
|
+
- multipart 请求不能带 `Content-Type: application/json`,否则后端可能读不到 `Path` 表单字段并返回“移动端文件上传路径不合法!”
|
|
118
|
+
- `OsClient` 只能保留一个规范字段,避免同时提交 `OsClient`、`osclient` 或 query/header/formData 多处互相冲突。
|
|
119
|
+
- 生产 H5 不能只依赖 `uni.uploadFile`。页面从 `uni.chooseImage` 得到的 `tempFiles[0].file`、`tempFiles[0]`、`blob:` / `data:` 临时路径都要传给 `V8.uploadFile`,并设置 `preferFetch:true`;SDK 必须能用 `fetch + FormData` 兜底,否则线上可能报 `未找到 MicroiV8 上传适配器。`。
|
|
120
|
+
|
|
121
|
+
## 公有桶 vs 私有桶
|
|
122
|
+
|
|
123
|
+
### 应用商城 ZIP
|
|
124
|
+
|
|
125
|
+
应用商城的 AI 应用/微服务资产禁止逐文件 Base64 持久化到数据库。源码/安装包场景可以生成 ZIP;真实在线编译目录优先使用下节的 MCP 流式发布。数据库只保存路径和校验元数据。
|
|
126
|
+
|
|
127
|
+
在线商城安装的后台任务只传 `StoreId/StoreApiBase/StoreOsClient` 等定位信息,不能复制整行、`Form/Row/Btn` 或 `AppPakcet`。兼容旧 Base64 包的导入器必须按片限制真实上传数量/体积,片间靠已提交的 `AppId + FilePath + Hash` 复用;上传后统计大小优先使用包内 `Size/Sha256`,禁止再次 `FromBase64String` 构造完整字节数组。
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
var zipResult = V8.Method.CreateZip({
|
|
131
|
+
Entries: [
|
|
132
|
+
{ Path: 'index.html', Content: '<html></html>' },
|
|
133
|
+
{ Path: 'assets/app.js', FileByteBase64: jsBase64 }
|
|
134
|
+
],
|
|
135
|
+
MaxFileCount: 20000,
|
|
136
|
+
MaxEntryBytes: 268435456,
|
|
137
|
+
MaxTotalBytes: 2147483648
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
var extractResult = V8.Method.ExtractZip({
|
|
141
|
+
FileByteBase64: zipBase64,
|
|
142
|
+
MaxFileCount: 20000,
|
|
143
|
+
MaxCompressionRatio: 200
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`System.IO` 在 Jint 沙箱中被禁止,不能在 V8 代码里直接构造 `MemoryStream/ZipArchive`;必须使用以上受控方法。
|
|
148
|
+
|
|
149
|
+
### AI 应用编译目录流式发布(首选)
|
|
150
|
+
|
|
151
|
+
发布 Web、UniApp、MicroService 的 `dist` / H5 编译目录时,必须优先使用 `microi_publish_application_directory_stream`,不要把每个文件读成 Base64 后传给 `ai_app_build`、`microi_publish_microservice` 或普通 JSON 接口。旧工具仅为小文件兼容保留。
|
|
152
|
+
|
|
153
|
+
标准流程:
|
|
154
|
+
|
|
155
|
+
1. 先运行不带 `confirmExecution` 的预检。MCP 按流计算 SHA-256,拒绝符号链接、`.git`、`node_modules`、密钥/`.env`、路径穿越、超过 20000 个文件或超过 20 GB 的垃圾目录;默认不发布 `.map`。
|
|
156
|
+
2. 确认后把 `confirmExecution` 精确设为 `appIdOrKey`。每个文件通过 multipart 原始流进入 `/api/V8Engine/UploadApplicationAssetStream`,不构造整文件 `Buffer`、Base64 或 JSON 文件体。
|
|
157
|
+
3. 文件只写不可变版本目录。全部成功后,清单确认接口只接收 `Path/Sha256/Size`,由 HDFS Provider 的 `CopyObject` 在服务端复制到 root 与 `latest`;非入口先复制,入口最后复制。
|
|
158
|
+
4. 历史版本 URL 保留语义版本;分享/在线使用 URL 使用不含版本号的 root 稳定地址。重试必须复用同一版本与摘要,不能覆盖已有但缺少完整性证明的历史对象。
|
|
159
|
+
5. 该控制面只允许当前 Token 租户的 `Level >= 9999` 交互式管理员;访问密钥会话不得发布。单文件、HTTP/Multipart 和每日额度仍然生效,不能把“使用流”理解成无限上传。
|
|
160
|
+
|
|
161
|
+
几十 MB **不是** Jint 或 HDFS 的固定上限。旧链路失败的原因是二进制先膨胀为约 `4/3` 的 Base64,又在 JSON、Jint 字符串、.NET 字符串/字节数组之间产生多份累计分配;文件数量、并发和当前进程内存共同决定触发点。描述问题时必须明确“旧 Base64/Jint 发布链路的累计分配”,不得写成“几十 MB 就达到 Jint 硬上限”。HDFS 上传本身应走二进制流。
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"appIdOrKey": "flower-store",
|
|
166
|
+
"versionNo": "v1.2.0",
|
|
167
|
+
"directory": "D:/build/flower-store/dist",
|
|
168
|
+
"entryPath": "index.html",
|
|
169
|
+
"changeSummary": "修复移动端布局",
|
|
170
|
+
"confirmExecution": "flower-store"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
底层断点式单文件工具是 `microi_upload_application_asset_stream`。除诊断或精确恢复单文件外,不要只调用它而遗漏最终清单确认,否则稳定入口不会切换。
|
|
175
|
+
|
|
176
|
+
| 类型 | `Limit` | 访问 URL | 用途 |
|
|
177
|
+
|------|---------|---------|------|
|
|
178
|
+
| 公有桶 | `false` | 直接拼接 `V8.SysConfig.FileServer + Path` | 产品图、Banner、公开文档 |
|
|
179
|
+
| 私有桶 | `true` | 必须用 `V8.Method.GetPrivateFileUrl` 获取临时 URL | 合同、身份证、敏感数据 |
|
|
180
|
+
|
|
181
|
+
### 默认 MinIO 桶名与安装验收
|
|
182
|
+
|
|
183
|
+
- Microi 一键安装的默认私有桶固定为 `mci-private`,默认公有桶固定为 `mci-public`;禁止使用 `mci-publish` 等近似名称。
|
|
184
|
+
- `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 开关;端点禁止携带用户名密码、桶路径、查询或片段。
|
|
185
|
+
- 安装脚本创建 `mci-public` 后必须设置匿名下载权限,并把 `HDFS=MinIO`、内外网端点、AccessKey/SecretKey、`MinIOPrivateBucketName=mci-private`、`MinIOPublicBucketName=mci-public` 同步写入当前租户的 `sys_osclients`。
|
|
186
|
+
- 安装脚本还必须同步当前有效 `sys_config`:`ApiBase` 使用对外可访问的 API 端口,`FileServer` 使用 `http://<访问IP>:<MinIO API端口>/mci-public`。`ApiBase` 不能误用 Web 前端端口,因为 V8 代码会直接在其后拼接 `/api/...` 或 `/apiengine/...`。
|
|
187
|
+
- 安装验收必须使用真实登录 Token 分别执行一次 `Limit=false` 和 `Limit=true` 上传:公有文件匿名访问应返回 `200`,私有文件匿名访问应返回 `403`,私有文件通过签名 URL 访问应返回 `200`,并核对下载内容与上传内容一致。
|
|
188
|
+
|
|
189
|
+
### 复盘:MinIO 已可上传但系统设置仍指向官方地址
|
|
190
|
+
|
|
191
|
+
- 触发场景:一键安装和桶初始化均成功,用户手工上传也成功,但读取系统设置时发现 `ApiBase`、`FileServer` 仍是空库模板中的官方地址。
|
|
192
|
+
- 根因:安装流程只回写了 `sys_osclients` 的 MinIO 配置,没有同步前端和 V8 公共使用的 `sys_config` 地址字段。
|
|
193
|
+
- 通用规则:数据库还原并创建默认桶后,必须按安装模式选择的访问 IP 和动态端口同时回写有效 `sys_config.ApiBase/FileServer`;其中 `ApiBase` 指向 API 服务,`FileServer` 指向公有桶根地址。
|
|
194
|
+
- 自动化检查:安装完成后通过 `GetSysConfig` 回读两个字段,断言均使用本次访问 IP 和实际端口;再执行公有上传并使用 `FileServer + Path` 匿名下载,内容必须一致。
|
|
195
|
+
|
|
196
|
+
公开页面图片(首页 banner、商品主图、公开活动头像等)应返回公有 URL,例如 `V8.SysConfig.FileServer + Path`。不要把公有图片统一转成 `GetPrivateFileUrl` 的 `static-private` 签名地址;部分 H5/浏览器会因响应头或跨域策略触发 ORB/CORS 拦截,表现为 uni-app `<image>` 内层 `background-image: none`。
|
|
197
|
+
|
|
198
|
+
### `sys_user.Avatar` 固定使用私有桶
|
|
199
|
+
|
|
200
|
+
- `sys_user` 是内部系统用户表,`Avatar` 可能暴露员工身份信息,因此字段配置必须保持 `ImgUpload.Limit=true`,上传端也必须显式传 `Limit:true`;自定义用户管理页不能因为绕过表单引擎而回退到公有上传。
|
|
201
|
+
- 数据库继续只保存租户内相对路径,例如 `/tenant_demo/avatar/20240218/user.png`。历史公有头像迁移时,应把同一文件复制到私有桶的同一路径,确认私有对象可访问后再停用公有访问;不得为了迁移批量改写路径或制造重复日期目录。
|
|
202
|
+
- PC、移动端、聊天、工作流等任何页面渲染 `sys_user.Avatar` 时,禁止 `FileServer + Avatar`、`GetServerPath(Avatar)` 或把相对路径直接交给 `<img>`。前端应调用 `DiyCommon.GetUserAvatarUrl(avatar, userId)`,接口/V8 应调用 `V8.Method.GetPrivateFileUrl({ FilePathName: avatar })`,并为临时 URL 设置短期缓存与失败占位图。
|
|
203
|
+
- `ContactUserAvatar`、`FromUserAvatar`、`SenderAvatar` 等从 `sys_user.Avatar` 派生的快照字段同样按私有路径处理;消息数据只保存原始相对路径,不能把会过期的临时 URL 持久化到数据库。
|
|
204
|
+
|
|
205
|
+
#### 复盘:字段改私有后卡片仍访问公有桶
|
|
206
|
+
|
|
207
|
+
- 触发条件:把 `sys_user.Avatar` 改为 `Limit=true`,但模块卡片仍配置 `TableCardImgField=Avatar`。
|
|
208
|
+
- 根因:通用卡片渲染器只读取了字段值,没有读取图片字段的 `Config.ImgUpload.Limit`,仍统一调用 `GetServerPath/FileServer`。
|
|
209
|
+
- 修复规则:通用图片渲染器必须同时检查字段配置;私有字段先异步换取临时地址,`sys_user.Avatar` 还要有表名+字段名语义兜底,避免元数据缓存短暂陈旧时泄露到公有路径。
|
|
210
|
+
- 验收断言:筛选一条有头像的系统用户,页面中 `/tenant_demo/avatar/` 的直接公有请求数必须为 0,私有代理图片 `naturalWidth > 0`,并同时验证无头像占位图不报错。
|
|
211
|
+
|
|
212
|
+
定制移动端项目的 Hero、Banner、音频、视频、字体等大资源也应优先上传到目标租户公有 HDFS,再通过 FileServer/CDN 引用;小型导航图标和离线关键素材才保留在主包。上传前可适度压缩,但必须在多尺寸截图或试听/试播中确认质量,禁止以明显失真换取包体扫描通过。完整移动端规则见 `microi-uniapp-frontend/SKILL.md`。
|
|
213
|
+
|
|
214
|
+
后台 `ImgUpload` 字段通常保存相对路径或 JSON:接口返回给移动端前先解析出 `Path`,再按公私有场景转换 URL:
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
function publicFileUrl(path) {
|
|
218
|
+
if (!path) return '';
|
|
219
|
+
if (/^https?:/i.test(path)) return path;
|
|
220
|
+
return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '');
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 私有桶临时 URL
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
var url = V8.Method.GetPrivateFileUrl({
|
|
228
|
+
FilePathName: '/private/contract/2024/abc.pdf',
|
|
229
|
+
OsClient: V8.OsClient, // 可选,默认当前
|
|
230
|
+
Expires: 600 // 可选,过期秒数
|
|
231
|
+
});
|
|
232
|
+
// 后端审计代理 URL,过期不可访问;真实对象存储签名 URL 不会返回前端
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- 普通客户端调用 `/api/HDFS/GetPrivateFileUrl` 时,不能只提交 `FilePathName`,必须同时提交 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`。服务端校验菜单、菜单绑定表、记录数据范围、字段归属以及字段值确实引用该路径后,才签发临时票据。
|
|
236
|
+
- `FieldId` 必须属于目标表,且组件为 `FileUpload` 或 `ImgUpload`;`SysMenuId` 必须是当前用户真实拥有、并绑定目标表的菜单。
|
|
237
|
+
- 普通用户禁止通过该入口直接取得私有文件 `Byte` / `Stream`。签发失败时不能回退裸路径、真实对象存储签名地址或公有 URL。
|
|
238
|
+
- 私有文件访问必须经过后端短期票据代理:签发链接时记录当前登录用户,实际 `GET/HEAD` 打开或下载时再记录一次访问行为;支持 `Range` 流式响应,并对同一次分片请求做短时去重,不能把文件完整读入内存。
|
|
239
|
+
- 审计代理由平台后端回源对象存储,必须使用服务端内网端点生成上游地址,不能先生成公网 MinIO 签名地址再让后端绕公网回源。否则同一对象经内网上传成功后,可能在公网端点表现为 404。
|
|
240
|
+
- 备份包、安装包、导出包等重要大文件不能只信任 `PutObject` 成功响应;必须在写入后通过同一私有桶和内网端点执行 `ObjectExist` 回读(可行时再核对大小或 SHA-256),通过后才能把业务记录标记为完成。
|
|
241
|
+
- 平台任务若绕过 `V8.Method.Upload` 直接使用底层 `PutObject`,必须保证写入对象键与后续 `V8.Method.GetPrivateFileUrl` 的租户前缀规则一致。像 `/database-backups/` 这种服务端保留目录只能做严格白名单规范化,禁止对普通租户文件泛化去除 `/{OsClient}/` 隔离前缀。
|
|
242
|
+
- 下载验收至少对新签发地址执行一次 `Range: bytes=0-0`,断言返回 `200/206`、内容长度大于 0 且不是 JSON 错误;完整交付再核对下载字节数或 SHA-256。若产品要求保留当前业务页,应在新浏览上下文打开代理地址,并在浏览器拦截弹窗时给出品牌化反馈。
|
|
243
|
+
- 用户把私有链接转发给别人后,接收者没有有效登录身份时按“匿名访问”记录,禁止根据签发人猜测实际访问人;票据仍按原有效期失效。
|
|
244
|
+
- 代理创建或包装失败时必须失败关闭,不得退回未经审计的真实签名 URL;行为日志中也禁止保存真实签名 URL、Token、Authorization 或存储密钥。
|
|
245
|
+
- `Limit:false` 的公有文件允许通过 CDN/公有桶直接访问,不要求记录用户行为日志,也不要为了审计强制改走私有代理。
|
|
246
|
+
|
|
247
|
+
## 接口直接响应文件(下载/导出)
|
|
248
|
+
|
|
249
|
+
接口引擎需要在配置中开启【**响应文件**】选项,然后返回特殊结构:
|
|
250
|
+
|
|
251
|
+
平台后端会统一处理响应文件:图片和 PDF 自动 `inline` 在浏览器中打开,其它类型默认下载;PDF、PNG、JPEG、GIF、WebP、AVIF、BMP、TIFF、ICO、SVG 等常见可预览类型会自动校验文件头。V8 代码不要手写复杂的文件头判断,只需要保证 `ContentType` 与真实文件内容一致。
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// 模板:导出 Excel
|
|
255
|
+
var excelResult = V8.Office.ExportExcel({...});
|
|
256
|
+
return {
|
|
257
|
+
Code: 1,
|
|
258
|
+
Data: {
|
|
259
|
+
FileName: 'export_' + DateNow('yyyyMMdd_HHmmss') + '.xls',
|
|
260
|
+
ContentType: 'application/vnd.ms-excel',
|
|
261
|
+
FileByteBase64: System.Convert.ToBase64String(excelResult.Data)
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// 模板:返回图片
|
|
266
|
+
var resp = V8.Http.GetResponse({ Url: 'https://example.com/img.png' });
|
|
267
|
+
return {
|
|
268
|
+
Code: 1,
|
|
269
|
+
Data: {
|
|
270
|
+
FileName: 'img.png',
|
|
271
|
+
ContentType: 'image/png',
|
|
272
|
+
FileByteBase64: System.Convert.ToBase64String(resp.RawBytes)
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// 模板:返回 PDF(浏览器直接预览;后端自动校验 %PDF- 文件头)
|
|
277
|
+
var pdfResp = V8.Http.GetResponse({ Url: 'https://example.com/report.pdf' });
|
|
278
|
+
return {
|
|
279
|
+
Code: 1,
|
|
280
|
+
Data: {
|
|
281
|
+
FileName: 'report.pdf',
|
|
282
|
+
ContentType: 'application/pdf',
|
|
283
|
+
FileByteBase64: System.Convert.ToBase64String(pdfResp.RawBytes)
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
常用 ContentType:
|
|
289
|
+
- `application/vnd.ms-excel` / `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
|
290
|
+
- `application/pdf`
|
|
291
|
+
- `image/png` / `image/jpeg`
|
|
292
|
+
- `application/octet-stream`(通用二进制)
|
|
293
|
+
|
|
294
|
+
注意:如果远程系统返回的是错误页、登录页、业务容器格式(例如金蝶 PLM 电子仓 `KD_C_PLM`、或其它文件头不是 `%PDF-` 的伪 PDF),不要在 V8 里伪装成 PDF。后端会返回 JSON 错误,包含 `ExpectedFirstAscii`、`ActualFirstAscii`、`ActualFirstHex`、`Length`,按这些信息排查上游下载接口。
|
|
295
|
+
|
|
296
|
+
## 跨平台文件同步登录会话
|
|
297
|
+
|
|
298
|
+
文件柜、文件同步等需要连接另一套 Microi API 的工具,必须把远程平台视为独立登录会话:
|
|
299
|
+
|
|
300
|
+
- 用户必须先完成远程登录,登录成功后显示远程用户名称、帐号、ApiBase、OsClient 和登录状态,并提供明确的退出登录操作。
|
|
301
|
+
- 历史远程连接通过 `mci_` 前缀表保存,并按 `V8.CurrentUser.Id` 做行级隔离;不得把帐号、密码或 Token 放入 `localStorage`。
|
|
302
|
+
- 密码和 Token 只能由受保护的接口引擎写入、读取和清理。数据库必须保存可校验的加密密文,普通 FormEngine 列表不得返回密文字段。
|
|
303
|
+
- 加密密钥优先使用租户专用 `FileCabinetSecret`,可使用仅后端可见的持久化租户密钥兜底;禁止使用进程级临时密钥,否则服务重启后无法解密历史连接。
|
|
304
|
+
- 历史连接列表只返回脱敏元数据;一键重连时再按记录 Id 和当前用户读取凭据。删除连接必须同时清除保存的密码和 Token。
|
|
305
|
+
- 远程目标登录后必须调用文件柜能力探针(如 `mci_file_sync_capability`)检查同步协议版本。接口不存在、返回 404/非标准结果或协议版本过低时,提示目标平台更新【文件柜】应用,不得继续同步。
|
|
306
|
+
- 验收至少覆盖:登录成功显示身份、退出后 Token 清空、历史连接一键重连、删除连接、密文落库、服务重启后仍可解密、目标平台缺少能力接口时的升级提示。
|
|
307
|
+
|
|
308
|
+
## 下载远程文件并存到 HDFS
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// 1) 下载远程图片
|
|
312
|
+
var resp = V8.Http.GetResponse({ Url: V8.Param.imageUrl });
|
|
313
|
+
if (resp.StatusCode !== 200) return { Code: 0, Msg: '下载失败' };
|
|
314
|
+
|
|
315
|
+
// 2) 转 base64 后上传到 HDFS
|
|
316
|
+
var base64 = System.Convert.ToBase64String(resp.RawBytes);
|
|
317
|
+
var fileName = V8.Method.NewGuid() + '.png';
|
|
318
|
+
|
|
319
|
+
var upResult = V8.Method.Upload({
|
|
320
|
+
FilesByteBase64: { [fileName]: base64 },
|
|
321
|
+
Limit: false,
|
|
322
|
+
Path: '/imported',
|
|
323
|
+
OsClient: V8.OsClient
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return upResult;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> 在 V8/Jint 中避免把 `resp.RawBytes` 直接塞进 `FilesByte`;序列化时可能变成数字/浮点数组,导致 `Unexpected token when reading bytes`。更稳的是 `System.Convert.ToBase64String(resp.RawBytes)` 后使用 `FilesByteBase64`。
|
|
330
|
+
|
|
331
|
+
移动端公开图片优先使用 `.jpg` / `.png` / `.webp`。如果上传 `.svg`,必须确认对象存储返回正确 `Content-Type: image/svg+xml`,否则浏览器可能拦截或不渲染。
|
|
332
|
+
|
|
333
|
+
## 通过 URL 列表批量下载并入库
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
var urls = V8.Param.urls; // ['https://...', 'https://...']
|
|
337
|
+
var savedPaths = [];
|
|
338
|
+
for (var i = 0; i < urls.length; i++) {
|
|
339
|
+
try {
|
|
340
|
+
var resp = V8.Http.GetResponse({ Url: urls[i], Timeout: 30 });
|
|
341
|
+
if (resp.StatusCode !== 200) continue;
|
|
342
|
+
|
|
343
|
+
var base64 = System.Convert.ToBase64String(resp.RawBytes);
|
|
344
|
+
var fileName = V8.Method.NewGuid() + '.bin';
|
|
345
|
+
var up = V8.Method.Upload({
|
|
346
|
+
FilesByteBase64: { [fileName]: base64 },
|
|
347
|
+
Limit: false,
|
|
348
|
+
Path: '/batch-import/' + DateNow('yyyy-MM-dd'),
|
|
349
|
+
OsClient: V8.OsClient
|
|
350
|
+
});
|
|
351
|
+
if (up.Code === 1) savedPaths.push(up.Data[0].Path);
|
|
352
|
+
} catch (ex) {
|
|
353
|
+
console.error('第' + (i + 1) + '个下载失败:' + ex.message);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { Code: 1, Data: savedPaths };
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Office 文件在线编辑版本号规则
|
|
360
|
+
|
|
361
|
+
当文件上传控件开启【Office 在线预览】、【允许在线编辑】和【开启 Office 文件版本号】时,前后端必须遵循统一版本规则:
|
|
362
|
+
|
|
363
|
+
- 新上传的 Office 文件(`pdf/doc/docx/xls/xlsx/ppt/pptx`)要立即写入初始版本 `v1.0.0`,字段 JSON 的 `Path` 指向该原始文件,`Version` 为 `v1.0.0`,`Versions[0]` 保存同一份原始文件路径。
|
|
364
|
+
- 用户进入 OnlyOffice 在线编辑页后,每次手动点击【保存文件】才生成新版本;第一次保存生成 `v1.0.1`,之后依次生成 `v1.0.2`、`v1.0.3`。
|
|
365
|
+
- 未开启版本号时,保存文件直接覆盖当前 `Path` 对应的 HDFS/OSS 源文件。
|
|
366
|
+
- 开启版本号时,保存文件必须生成带版本号后缀的新文件,例如 `contract_v1.0.1.docx`,字段 JSON 的 `Path` 指向最新版本,`Versions` 保留历史版本路径。
|
|
367
|
+
- 在线 Office 路由和文件上传字段都要能读取 `Versions`,用于右上角切换历史版本预览/编辑。
|
|
368
|
+
|
|
369
|
+
### OnlyOffice 服务端取文件与匿名预览规则
|
|
370
|
+
|
|
371
|
+
- OnlyOffice 文档服务器会在服务端再次下载文件。浏览器可以下载但 OnlyOffice 提示“下载失败”时,优先检查生成地址是否为 `localhost/127.0.0.1/内网域名`。
|
|
372
|
+
- OnlyOffice 可能先对文档地址发起 `HEAD` 探测。响应文件接口除了 `GET 200`,还必须让 `HEAD` 返回相同的 `Content-Type/Content-Length/Content-Disposition`,不能返回 `405`。
|
|
373
|
+
- 私有文件在线预览调用 `GetPrivateFileUrl` 或 `/api/HDFS/GetPrivateFileUrl` 时传 `ForOfficePreview:true`。审计代理应优先使用租户系统配置的公网 `ApiBase`,但仍把真实对象存储签名地址保存在共享 Redis ticket 中,禁止直接返回真实签名地址。
|
|
374
|
+
- `/online-office` 可以匿名访问。公有存储模式只允许当前 `OsClient` 目录下的 `filePathName`;接口模式通过 `fileUrl` 接收当前平台正式 `ApiBase`,或由同端口本地后端读取的 loopback `/apiengine/...` 响应文件地址,并要求 URL 显式携带当前 `OsClient`。两种模式都拒绝跨租户、路径穿越和任意第三方 URL;`isPrivate=1` 必须登录。
|
|
375
|
+
- 匿名接口引擎预览不要求 V8 代码先上传 HDFS:接口开启【响应文件】和【允许匿名】后,完整地址 URL 编码传给 `fileUrl`,同时传 `fileName/fileType`。页面通过匿名安全中转让当前后端限域读取源文件,并以确定性路径缓存到当前租户公有对象存储;OnlyOffice 只接收公网 `FileServer` 地址。loopback 仅允许同端口本地后端访问,禁止开放任意 URL 代理。
|
|
376
|
+
- `canEdit` 只是前端请求参数,不是权限。最终编辑条件必须是“有效登录态 && canEdit=true”;匿名始终只读,不能因 URL 参数放开编辑。
|
|
377
|
+
- 匿名预览页隐藏左侧菜单、顶部导航和页签;已登录用户保持原系统布局。
|
|
378
|
+
- 匿名导出响应接口应配置频率限制或保证生成逻辑足够轻量,不能让单个公网 URL 无界消耗 CPU/内存;如业务仍需要落盘缓存,缓存事实必须进入共享 Redis/HDFS,不能用进程内变量。
|
|
379
|
+
|
|
380
|
+
字段 JSON 示例:
|
|
381
|
+
|
|
382
|
+
```json
|
|
383
|
+
{
|
|
384
|
+
"Name": "contract.docx",
|
|
385
|
+
"Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx",
|
|
386
|
+
"Version": "v1.0.1",
|
|
387
|
+
"Versions": [
|
|
388
|
+
{ "Version": "v1.0.0", "Name": "contract.docx", "Path": "/tenant_demo/file/20260622/contract.docx", "IsLatest": false },
|
|
389
|
+
{ "Version": "v1.0.1", "Name": "contract_v1.0.1.docx", "Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx", "IsLatest": true }
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## ImgUpload / FileUpload 字段值兼容规则
|
|
395
|
+
|
|
396
|
+
`ImgUpload` 不能假设只是一种值结构。PC 表单、移动端、旧数据、单图/多图、公开/私有桶会混合出现以下格式:
|
|
397
|
+
|
|
398
|
+
| 场景 | 可能的值 |
|
|
399
|
+
|------|----------|
|
|
400
|
+
| 空值/占位 | `''`、`null`、`undefined`、`'[]'`、`'null'`、`'正在上传中...'` |
|
|
401
|
+
| 旧单图 | `'/upload/xxx/a.png'`、`'https://cdn/a.png'` |
|
|
402
|
+
| 新单图 | `{ Path, Name, Size, Id, State }` 或 JSON 字符串 `'{"Path":"..."}'` |
|
|
403
|
+
| 多图 | `[{ Path, Name, Id, State }]` 或 JSON 字符串 `'[{"Path":"..."}]'` |
|
|
404
|
+
| 其它兼容字段 | `Path`、`FilePathName`、`FullPath`、`Url`、`url`、`src` |
|
|
405
|
+
|
|
406
|
+
任何端(PC、uni-app、H5、小程序)渲染图片前都必须先做“归一化 -> 取 Path -> 转最终 URL”,不要直接 `JSON.parse` 后只处理数组,也不要直接把字段值拼到 `FileServer`。
|
|
407
|
+
|
|
408
|
+
推荐归一化:
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
function normalizeUploadValue(value) {
|
|
412
|
+
if (value == null || value === '' || value === 'undefined' || value === 'null') return [];
|
|
413
|
+
if (value === '正在上传中...' || value === '[]' || value === '[ ]') return [];
|
|
414
|
+
|
|
415
|
+
var raw = value;
|
|
416
|
+
if (typeof raw === 'string') {
|
|
417
|
+
var s = raw.trim();
|
|
418
|
+
if ((s.indexOf('{') === 0 || s.indexOf('[') === 0)) {
|
|
419
|
+
try { raw = JSON.parse(s); } catch (e) { raw = s; }
|
|
420
|
+
} else {
|
|
421
|
+
raw = s;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (Array.isArray(raw)) {
|
|
426
|
+
return raw.map(normalizeUploadItem).filter(function (it) { return !!it.Path; });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
var one = normalizeUploadItem(raw);
|
|
430
|
+
return one.Path ? [one] : [];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function normalizeUploadItem(item) {
|
|
434
|
+
if (!item) return {};
|
|
435
|
+
if (typeof item === 'string') {
|
|
436
|
+
return { Path: item, Name: item.split('/').pop() || item, State: 1 };
|
|
437
|
+
}
|
|
438
|
+
if (typeof item === 'object') {
|
|
439
|
+
var path = item.Path || item.FilePathName || item.FullPath || item.Url || item.url || item.src || '';
|
|
440
|
+
return {
|
|
441
|
+
Id: item.Id || item.id || '',
|
|
442
|
+
Name: item.Name || item.FileName || item.name || (path ? String(path).split('/').pop() : ''),
|
|
443
|
+
Size: item.Size || item.size || '',
|
|
444
|
+
CreateTime: item.CreateTime || item.createTime || '',
|
|
445
|
+
State: item.State == null ? 1 : item.State,
|
|
446
|
+
Path: path
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {};
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
公开图片 URL 解析原则与 `Microi.Client/src/utils/diy.common.js` 的 `GetServerPath` 一致:
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
function publicUploadUrl(path) {
|
|
457
|
+
if (!path) return '';
|
|
458
|
+
var s = String(path).trim();
|
|
459
|
+
if (!s || s === '正在上传中...') return '';
|
|
460
|
+
if (s.indexOf('.') === 0) return s; // ./static/img/loading.gif 等本地静态资源
|
|
461
|
+
if (/^(https?:|data:|blob:)/i.test(s)) return s; // 已经是最终 URL
|
|
462
|
+
if (s.indexOf('{') === 0 || s.indexOf('[') === 0) {
|
|
463
|
+
var list = normalizeUploadValue(s);
|
|
464
|
+
s = list.length ? list[0].Path : '';
|
|
465
|
+
}
|
|
466
|
+
if (!s) return '';
|
|
467
|
+
return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + s.replace(/^\/+/, '');
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
私有桶(`Limit === true`)不要拼 `FileServer`,必须把归一化后的 `Path` 传给 `V8.Method.GetPrivateFileUrl({ FilePathName: path })` 或后端签名接口换临时 URL。
|
|
472
|
+
|
|
473
|
+
## 安全注意
|
|
474
|
+
|
|
475
|
+
- ❌ 不要让前端任意指定 `Path`(路径穿越风险),只允许后端固定路径
|
|
476
|
+
- ❌ 不要不校验文件类型 / 大小:根据 ContentType + 后缀双重校验
|
|
477
|
+
- ❌ 不要把持有 Token 当成私有文件授权;普通用户必须证明菜单、记录和字段引用关系
|
|
478
|
+
- ❌ 不要向普通角色开放文件列表、移动、重命名、删除、覆盖等管理接口;这些接口仅限 `Level >= 9999`
|
|
479
|
+
- ❌ 不要开启 UEditor `catchimage` 远程抓图;默认关闭,确需采集时另建带域名白名单、DNS/IP 校验、禁止跳转、超时和响应上限的受控接口
|
|
480
|
+
- ❌ 敏感文件(合同、身份证)必须用私有桶 `Limit: true`
|
|
481
|
+
- ✅ 公有桶 URL 可缓存到前端,私有桶临时 URL 每次重新生成
|
|
482
|
+
- ✅ 删除数据时同步清理 HDFS 文件(避免存储泄漏)
|
|
483
|
+
- ✅ Excel/PDF 等导出文件通过【响应文件】配置返回,不要拼接到 JSON 数据里
|
|
484
|
+
|
|
485
|
+
### 安全升级兼容
|
|
486
|
+
|
|
487
|
+
- 旧页面如果只传 `FilePathName` 获取私有地址,升级后普通帐号会失败;必须补齐 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`,不能改回匿名或放开管理权限。
|
|
488
|
+
- 旧自定义上传若依赖普通用户设置 `Limit:false` 或任意多级 `Path`,应改成私有上传;公开资源改走受控发布流程。
|
|
489
|
+
- 历史公有文件不会自动变成私有文件。迁移时先复制对象、验证私有读取,再停止旧公有访问;数据库继续保存租户内相对路径,不能持久化临时 URL。
|
|
490
|
+
- 上线验收至少覆盖:普通角色跨菜单/跨记录/跨字段读取被拒绝、单文件/单请求/数量限制、帐号与租户配额、Redis 故障失败关闭、多节点并发、公有匿名 `200`、私有匿名 `403`、授权私有访问 `200`。
|
|
491
|
+
|
|
492
|
+
### 复盘:ZIP 发布端与安装端 SHA256 口径不一致
|
|
493
|
+
|
|
494
|
+
- 触发场景:应用包已成功生成 ZIP 和摘要,但安装端下载 ZIP 后调用不存在的哈希辅助函数,或尝试通过当前 V8 环境不可用的 `.NET SHA256.Create()` 校验,导致安装中断。
|
|
495
|
+
- 根因:发布端实际使用 `V8.EncryptHelper.Sha256Hex(FileByteBase64)`,安装端却按原始字节设计了另一套实现,函数名称、输入数据和运行时能力均未对齐。
|
|
496
|
+
- 通用规则:文件摘要必须在包清单中记录算法和输入口径;当前应用 ZIP 统一使用 `SHA256-Base64Text`,发布端与安装端都对同一份 Base64 文本调用 `V8.EncryptHelper.Sha256Hex`。禁止仅凭函数名推测算法,也不要在未验证 V8 互操作能力时直接实例化 `.NET` 加密对象。
|
|
497
|
+
- 自动化检查:生成同时包含源码 ZIP、编译 ZIP 的应用包,再从官方地址下载并安装;分别篡改 Base64 文本和摘要,正常包应安装成功,两个篡改包都必须在解压前被拒绝。
|