@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68
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/README.en.md +86 -241
- package/README.ja.md +86 -241
- package/README.md +86 -241
- package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
- package/dist/chunk-FI56GONY.js.map +1 -0
- package/dist/chunk-MBPB37FR.js +1348 -0
- package/dist/chunk-MBPB37FR.js.map +1 -0
- package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
- package/dist/chunk-ZFNBUF77.js.map +1 -0
- package/dist/cli-config-templates-W2LLPMB6.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
- package/dist/index.js +1304 -532
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-DSJJXM2F.js +3849 -0
- package/dist/src-DSJJXM2F.js.map +1 -0
- package/package.json +9 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/messages/ja-JP.json +38 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +6 -1
- package/templates/apps/api/playwright.config.ts.hbs +15 -7
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
- package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/messages/ja-JP.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +23 -8
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +11 -3
- package/templates/base/playwright.config.ts +15 -4
- package/templates/base/src/hooks.server.ts.hbs +15 -6
- package/templates/base/src/lib/server/init.ts.hbs +139 -30
- package/templates/base/vite.config.ts.hbs +5 -3
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
- package/templates/i18n/project.inlang/settings.json +1 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/run-e2e.mjs +51 -12
- package/templates/skills/AGENTS.md +7 -3
- package/templates/skills/bridges/admin/AGENTS.md +6 -2
- package/templates/skills/bridges/api/AGENTS.md +3 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
- package/templates/skills/bridges/h5/AGENTS.md +6 -2
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
- package/templates/skills/bridges/website/AGENTS.md +6 -2
- package/templates/skills/hai-ai/SKILL.md +8 -2
- package/templates/skills/hai-ai/guide.md +134 -60
- package/templates/skills/hai-ai/reference.md +51 -11
- package/templates/skills/hai-api-client/SKILL.md +15 -160
- package/templates/skills/hai-api-client/reference.md +178 -0
- package/templates/skills/hai-api-contract/SKILL.md +16 -273
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +6 -2
- package/templates/skills/hai-app-create/development.md +24 -8
- package/templates/skills/hai-app-create/reference.md +7 -5
- package/templates/skills/hai-app-review/SKILL.md +7 -3
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +6 -3
- package/templates/skills/hai-app-tests/reference.md +5 -5
- package/templates/skills/hai-app-tests/validation.md +38 -0
- package/templates/skills/hai-audit/SKILL.md +24 -147
- package/templates/skills/hai-audit/reference.md +148 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +16 -13
- package/templates/skills/hai-cache/SKILL.md +20 -181
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +7 -4
- package/templates/skills/hai-capacitor/reference.md +15 -9
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +7 -3
- package/templates/skills/hai-core/reference.md +25 -5
- package/templates/skills/hai-crypto/SKILL.md +22 -250
- package/templates/skills/hai-crypto/reference.md +310 -0
- package/templates/skills/hai-datapipe/SKILL.md +16 -195
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +24 -54
- package/templates/skills/hai-deploy/middleware.md +51 -43
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +25 -11
- package/templates/skills/hai-iam/reference.md +181 -154
- package/templates/skills/hai-kit/SKILL.md +35 -174
- package/templates/skills/hai-kit/reference.md +190 -0
- package/templates/skills/hai-payment/SKILL.md +33 -263
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +21 -88
- package/templates/skills/hai-reach/SKILL.md +6 -3
- package/templates/skills/hai-reach/reference.md +83 -22
- package/templates/skills/hai-reldb/SKILL.md +6 -3
- package/templates/skills/hai-reldb/reference.md +68 -25
- package/templates/skills/hai-scheduler/SKILL.md +18 -195
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +25 -10
- package/templates/skills/hai-serv/reference.md +123 -87
- package/templates/skills/hai-spec-implement/SKILL.md +5 -1
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +5 -0
- package/templates/skills/hai-spec-implement/workflow.md +5 -3
- package/templates/skills/hai-spec-review/SKILL.md +4 -0
- package/templates/skills/hai-storage/SKILL.md +23 -238
- package/templates/skills/hai-storage/reference.md +282 -0
- package/templates/skills/hai-ui/SKILL.md +17 -3
- package/templates/skills/hai-ui/guide.md +3 -3
- package/templates/skills/hai-ui/reference.md +26 -11
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +26 -0
- package/templates/skills/hai-ui-review/SKILL.md +30 -0
- package/templates/skills/hai-ui-review/checklist.md +22 -0
- package/templates/skills/hai-vecdb/SKILL.md +14 -10
- package/templates/skills/hai-vecdb/reference.md +33 -3
- package/dist/chunk-FWN5B54L.js +0 -468
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-storage
|
|
3
|
-
description: "
|
|
3
|
+
description: "实现本地/S3 文件与目录操作、流式读写、checksum 和预签名上传下载;处理冲突、校验与取消。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-storage
|
|
@@ -12,247 +12,32 @@ description: "使用 @h-ai/storage 访问本地/S3 文件、目录和预签名
|
|
|
12
12
|
| 能力 | 使用 @h-ai/storage 访问本地/S3 文件、目录和预签名上传下载 |
|
|
13
13
|
| 适用场景 | 文件接口、目录操作或浏览器直传 |
|
|
14
14
|
| 输入 | StorageConfigInput、文件 key、内容、presign 选项 |
|
|
15
|
-
| 输出 | file/dir/presign 操作的 HaiResult;checksum 生成字符串或校验结果 |
|
|
16
|
-
| 限制 |
|
|
15
|
+
| 输出 | file/dir/presign 操作的 HaiResult;deleteMany 的成功 data/失败 error.ext.deleteMany 保留逐项进度;checksum 生成字符串或校验结果 |
|
|
16
|
+
| 限制 | 仅服务端;预签名仅 S3,local getUrl/putUrl 返回 UNSUPPORTED_OPERATION、publicUrl 为 null;浏览器先检查 API success 再直传。Local list 使用绑定根目录/prefix/delimiter 的无状态游标,files 与 commonPrefixes 共用 maxKeys(1–1000);可跨实例续页,每次仍扫描排序且非快照,游标不代替前缀授权。putUrl 不提供 maxSize 强制约束,上传大小须服务端验证。 |
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 使用路径
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
1. Node 端配置 Local/S3,校验并检查 init;MinIO/RustFS 使用 s3 类型,确认桶和权限。
|
|
21
|
+
2. 从 file/dir/presign 选择操作,检查 HaiResult。流与 checksum 按真实签名处理,checksum 在 Node 无需 init。
|
|
22
|
+
3. 客户端经受保护 API 或预签名 URL,服务端校验路径、内容、大小及资源权限;putUrl 不强制 maxSize。
|
|
23
|
+
进度上传使用原生 XHR;预取消不发送,取消/错误一次结算并清理监听,完成后取消不影响旧请求。取消不回滚远端对象,先核对状态再授权删除。
|
|
24
|
+
4. 不覆盖上传使用 ifNoneMatch;冲突/checksum 失败不能伪装成功。验证取消、流错误和文件清理,最后 close。
|
|
25
|
+
5. 批量删除核对逐项进度,S3 每批最多 1000 项;HTTP 200 不能代替成功判断。unknown 结果先核对业务状态,避免重删并发新对象;dir.delete 不保证隔离并发写入或清除 S3 历史版本。
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
## 按需参考
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
按本次操作读取对应小节;以已安装版本的公开类型核验签名,示例中的业务变量由应用提供。
|
|
25
30
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
+
- [运行环境](reference.md#运行环境)
|
|
32
|
+
- [适用场景](reference.md#适用场景)
|
|
33
|
+
- [使用步骤](reference.md#使用步骤)
|
|
34
|
+
- [核心 API](reference.md#核心-api)
|
|
35
|
+
- [错误码 — `HaiStorageError`](reference.md#错误码--haistorageerror)
|
|
36
|
+
- [HTTP API 契约](reference.md#http-api-契约)
|
|
37
|
+
- [常见模式](reference.md#常见模式)
|
|
38
|
+
- [浏览器端文件上传(预签名 URL 模式)](reference.md#浏览器端文件上传预签名-url-模式)
|
|
39
|
+
- [相关 Skills](reference.md#相关-skills)
|
|
31
40
|
|
|
32
|
-
##
|
|
41
|
+
## 跨端输入与能力约束
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```yaml
|
|
37
|
-
# config/_storage.yml
|
|
38
|
-
type: local # local | s3
|
|
39
|
-
# 本地存储
|
|
40
|
-
root: ./data/storage
|
|
41
|
-
# S3 存储(type=s3 时生效)
|
|
42
|
-
# endpoint: ''
|
|
43
|
-
# region: us-east-1
|
|
44
|
-
# accessKeyId: ''
|
|
45
|
-
# secretAccessKey: ''
|
|
46
|
-
# bucket: my-bucket
|
|
47
|
-
# forcePathStyle: false
|
|
48
|
-
# prefix: ''
|
|
49
|
-
# publicUrl: ''
|
|
50
|
-
operationLog:
|
|
51
|
-
read: false # file.get/head/exists, dir.list, presign.getUrl/publicUrl
|
|
52
|
-
write: false # file.put/delete/deleteMany/copy, dir.delete, presign.putUrl
|
|
53
|
-
maxLength: 1000
|
|
54
|
-
level: debug # info | debug | trace
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
约定环境变量优先于 YAML,例如 `HAI_STORAGE_ROOT`、
|
|
58
|
-
`HAI_STORAGE_ACCESSKEYID`、`HAI_STORAGE_SECRETACCESSKEY`。
|
|
59
|
-
|
|
60
|
-
MinIO、RustFS 使用同一个 S3 Provider:配置 `type: s3`、S3 API `endpoint`(通常为 9000 端口)和 `forcePathStyle: true`,并预先创建 `bucket`;不要新增 `type: minio` 或 `type: rustfs`。
|
|
61
|
-
|
|
62
|
-
使用 hai-deploy 的默认自建 S3 配置时,可通过 `container.s3Engine`(`HAI_DEPLOY_CONTAINER_S3ENGINE`,`rustfs | minio`)选择实现,默认 RustFS。部署流程复用应用镜像中的 S3 SDK 幂等初始化 bucket,不依赖单独的 mc 镜像;自定义外部 S3 服务仍需确认 bucket 已存在且凭据有相应权限。
|
|
63
|
-
|
|
64
|
-
`operationLog` 在 Local/S3 Provider 的真实 file/dir/presign 操作处输出日志,不要在 `storage-main.ts` 或调用方重复包装。文件内容不会写入日志,上传数据只记录字节长度;`maxLength` 用于截断序列化后的参数,`level` 默认 `debug`。
|
|
65
|
-
|
|
66
|
-
### 2. 初始化与关闭
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { storage } from '@h-ai/storage'
|
|
70
|
-
|
|
71
|
-
await storage.init(core.config.get('storage'))
|
|
72
|
-
// 使用后关闭
|
|
73
|
-
const closeResult = await storage.close()
|
|
74
|
-
if (!closeResult.success) {
|
|
75
|
-
throw new Error(closeResult.error.message)
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
> `storage.config` 返回的是**脱敏配置快照**;S3 的 `accessKeyId` / `secretAccessKey` 等敏感字段会被替换为 `[REDACTED]`。
|
|
80
|
-
|
|
81
|
-
## 核心 API
|
|
82
|
-
|
|
83
|
-
### 文件操作
|
|
84
|
-
|
|
85
|
-
`storage.file` 提供以下方法:
|
|
86
|
-
|
|
87
|
-
- `put(key, data, options?)`
|
|
88
|
-
- `get(key, options?)`
|
|
89
|
-
- `getStream(key, options?)`(字节流,迭代错误可抛出)
|
|
90
|
-
- `head(key)`
|
|
91
|
-
- `exists(key)`
|
|
92
|
-
- `delete(key)`
|
|
93
|
-
- `deleteMany(keys)`
|
|
94
|
-
- `copy(sourceKey, destKey, options?)`
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// 上传文件
|
|
98
|
-
const result = await storage.file.put('avatars/user-123.png', fileBuffer, {
|
|
99
|
-
contentType: 'image/png',
|
|
100
|
-
metadata: { userId: '123' },
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// 下载文件
|
|
104
|
-
const file = await storage.file.get('avatars/user-123.png')
|
|
105
|
-
|
|
106
|
-
// 列出文件
|
|
107
|
-
const files = await storage.dir.list({ prefix: 'avatars/', maxKeys: 100 })
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### 预签名 URL
|
|
111
|
-
|
|
112
|
-
`storage.presign` 提供以下方法:
|
|
113
|
-
|
|
114
|
-
- `getUrl(key, options?)`
|
|
115
|
-
- `putUrl(key, options?)`
|
|
116
|
-
- `publicUrl(key)`
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// 客户端直传模式
|
|
120
|
-
const uploadUrl = await storage.presign.putUrl('uploads/doc.pdf', {
|
|
121
|
-
expiresIn: 3600, // 1 小时有效
|
|
122
|
-
contentType: 'application/pdf',
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// 临时下载链接
|
|
126
|
-
const downloadUrl = await storage.presign.getUrl('uploads/doc.pdf', {
|
|
127
|
-
expiresIn: 300, // 5 分钟有效
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
> `storage.presign.putUrl()` 当前不支持 `maxSize` 约束参数;如需大小限制,请在应用层上传前校验,或在服务端落库/回调处二次校验。
|
|
132
|
-
|
|
133
|
-
## 错误码 — `HaiStorageError`
|
|
134
|
-
|
|
135
|
-
| 错误码 | code | 说明 |
|
|
136
|
-
|--------|------|------|
|
|
137
|
-
| `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | 连接失败 |
|
|
138
|
-
| `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | 操作失败 |
|
|
139
|
-
| `HaiStorageError.NOT_FOUND` | `hai:storage:003` | 文件不存在 |
|
|
140
|
-
| `HaiStorageError.ALREADY_EXISTS` | `hai:storage:004` | 文件已存在 |
|
|
141
|
-
| `HaiStorageError.CHECKSUM_FAILED` | `hai:storage:016` | checksum 读取/计算失败 |
|
|
142
|
-
| `HaiStorageError.INVALID_CHECKSUM` | `hai:storage:017` | checksum 格式/算法非法 |
|
|
143
|
-
| `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | 权限不足 |
|
|
144
|
-
| `HaiStorageError.QUOTA_EXCEEDED` | `hai:storage:006` | 配额超限 |
|
|
145
|
-
| `HaiStorageError.INVALID_PATH` | `hai:storage:007` | 路径无效 |
|
|
146
|
-
| `HaiStorageError.IO_ERROR` | `hai:storage:008` | IO 错误 |
|
|
147
|
-
| `HaiStorageError.NETWORK_ERROR` | `hai:storage:009` | 网络错误 |
|
|
148
|
-
| `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | 未初始化 |
|
|
149
|
-
| `HaiStorageError.UNSUPPORTED_TYPE` | `hai:storage:011` | 不支持的存储类型 |
|
|
150
|
-
| `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 配置错误 |
|
|
151
|
-
| `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | 预签名 URL 生成失败 |
|
|
152
|
-
| `HaiStorageError.UPLOAD_FAILED` | `hai:storage:014` | 上传失败 |
|
|
153
|
-
| `HaiStorageError.DOWNLOAD_FAILED` | `hai:storage:015` | 下载失败 |
|
|
154
|
-
|
|
155
|
-
## HTTP API 契约
|
|
156
|
-
|
|
157
|
-
公共 HTTP API 统一由 `@h-ai/api-contract` 提供,并由 `@h-ai/serv/features/storage` 绑定到本模块。
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { apiClient } from '@h-ai/api-client'
|
|
161
|
-
|
|
162
|
-
const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## 常见模式
|
|
166
|
-
|
|
167
|
-
大文件上传用 `file.put(key, readableOrAsyncByteStream, { signal, ifNoneMatch: '*' })`,内部使用有界 S3 multipart;下载用 `file.getStream(key, { signal, rangeStart, rangeEnd })`,及时消费或关闭,迭代期间 I/O/取消可能抛错。本地读取目录直接返回 `INVALID_PATH`。checksum 的 signal 取消会停止等待下一块数据并请求关闭输入迭代器。`get()` 返回完整 Buffer,只用于容量可控的数据。仅创建写入采用 S3 标准条件或本地原子发布,冲突返回 `ALREADY_EXISTS`;后端不支持时失败,不退回覆盖。
|
|
168
|
-
|
|
169
|
-
`checksum.generate(data, options?)` / `checksum.verify(data, expected, options?)` 默认 SHA-256、小写 hex,可选 sha384/sha512/sha1/md5、base64 编码与 signal;流仅消费一次,读取失败返回 HaiResult 错误,不匹配返回 false。MD5/SHA-1 不作安全认证,ETag 和 multipart 复合 checksum 不当作完整文件摘要。浏览器不导入 Node checksum;离线增量摘要需使用适用的浏览器实现,不把 SubtleCrypto.digest 当流式 API。
|
|
170
|
-
|
|
171
|
-
### 与 kit 集成(文件上传 API)
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
import { storage } from '$lib/server/init'
|
|
175
|
-
// src/routes/api/files/upload/+server.ts
|
|
176
|
-
import { kit } from '@h-ai/kit'
|
|
177
|
-
import { z } from 'zod'
|
|
178
|
-
|
|
179
|
-
const UploadSchema = z.object({
|
|
180
|
-
key: z.string().min(1),
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
export const POST = kit.handler(async ({ request, locals }) => {
|
|
184
|
-
kit.guard.require(locals.session, 'storage:write')
|
|
185
|
-
const formData = await request.formData()
|
|
186
|
-
const file = formData.get('file') as File
|
|
187
|
-
if (!file)
|
|
188
|
-
return kit.response.badRequest('Missing file')
|
|
189
|
-
|
|
190
|
-
const key = `uploads/${locals.session.userId}/${file.name}`
|
|
191
|
-
const buffer = new Uint8Array(await file.arrayBuffer())
|
|
192
|
-
const result = await storage.file.put(key, buffer, { contentType: file.type })
|
|
193
|
-
if (!result.success)
|
|
194
|
-
return kit.response.internalError()
|
|
195
|
-
return kit.response.created({ key })
|
|
196
|
-
})
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
> 说明:`@h-ai/storage` 只提供存储能力,不强制要求 `/api/storage` 路由命名。应用可按业务语义自定义 API 路径。
|
|
200
|
-
|
|
201
|
-
> Local Provider 的 `storage.dir.list()` 会递归扫描匹配前缀下的文件,仅支持 `maxKeys` 截断,不支持 `continuationToken` 真分页;大目录建议缩小 `prefix` 或改用 S3 Provider。
|
|
202
|
-
|
|
203
|
-
### 头像上传
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
const key = `avatars/${userId}/${Date.now()}.png`
|
|
207
|
-
const result = await storage.file.put(key, avatarBuffer, {
|
|
208
|
-
contentType: 'image/png',
|
|
209
|
-
})
|
|
210
|
-
if (result.success) {
|
|
211
|
-
const publicUrl = storage.presign.publicUrl(key)
|
|
212
|
-
await iam.user.updateUser(userId, { avatarUrl: publicUrl ?? '' })
|
|
213
|
-
}
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## 浏览器端文件上传(预签名 URL 模式)
|
|
217
|
-
|
|
218
|
-
浏览器端无法直接使用 `storage` 模块,应通过 API 获取预签名 URL 后直传:
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { apiFetch } from '$lib/utils/api'
|
|
222
|
-
|
|
223
|
-
// 1. 向服务端请求预签名上传 URL
|
|
224
|
-
const res = await apiFetch('/api/storage/presign/upload', {
|
|
225
|
-
method: 'POST',
|
|
226
|
-
headers: { 'Content-Type': 'application/json' },
|
|
227
|
-
body: JSON.stringify({ key: `uploads/${file.name}`, contentType: file.type }),
|
|
228
|
-
})
|
|
229
|
-
const { url } = await res.json()
|
|
230
|
-
|
|
231
|
-
// 2. 浏览器直传到存储(不经过服务端,大文件友好)
|
|
232
|
-
await fetch(url, {
|
|
233
|
-
method: 'PUT',
|
|
234
|
-
body: file,
|
|
235
|
-
headers: { 'Content-Type': file.type },
|
|
236
|
-
})
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
也可使用 typed client(需配合 `@h-ai/api-client`):
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
import { apiClient } from '@h-ai/api-client'
|
|
243
|
-
|
|
244
|
-
const result = await apiClient.storage.presignedUrls.createUpload({
|
|
245
|
-
key: `avatars/${userId}.png`,
|
|
246
|
-
contentType: 'image/png',
|
|
247
|
-
})
|
|
248
|
-
if (result.success) {
|
|
249
|
-
await fetch(result.data.url, { method: 'PUT', body: file })
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## 相关 Skills
|
|
254
|
-
|
|
255
|
-
- `hai-build`:模块初始化顺序
|
|
256
|
-
- `hai-core`:配置与 HaiResult 模型
|
|
257
|
-
- `hai-kit`:SvelteKit API 端点集成(`kit.handler` + `kit.guard`)
|
|
258
|
-
- `hai-ui`:Storage 场景组件(FileUpload/ImageUpload/AvatarUpload)
|
|
43
|
+
key 统一使用 `@h-ai/storage/schemas` 的 StorageKeySchema(UTF-8 最多1024字节,无绝对路径/反斜线/控制符/点路径段),prefix另允许空串与部分文件名。HTTP批量删除按原数组限制1–1000项,不能以去重绕过;Node仍按Provider分批。签名有效期整数1–604800秒,Node默认3600、HTTP默认900。HTTP上传只接受key/contentType/expiresIn,不接受contentLength/maxSize/metadata/checksum等file.put选项;未知字段返回400,typed client为VALIDATION_FAILED。Node流式读写、Range、checksum、条件写等无默认HTTP端点。FileUpload大小/类型仅为前端选择体验,服务端仍需验证实际对象与归属;不要编造上传代理端点。校验不替代session资源授权及Provider防逃逸。
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# hai-storage 参考
|
|
2
|
+
|
|
3
|
+
> `@h-ai/storage` 提供统一文件存储接口,支持本地文件系统(LocalProvider)和 S3 兼容存储(S3Provider),通过 `storage.file`、`storage.dir`、`storage.presign` 访问存储,`storage.checksum` 提供 Node 端无需初始化的字节完整性工具。
|
|
4
|
+
|
|
5
|
+
## 运行环境
|
|
6
|
+
|
|
7
|
+
> ⚠️ **服务端模块**。`storage.file` / `storage.dir` / `storage.presign` 均在 Node.js 端使用。浏览器端通过预签名 URL 或 API 端点间接访问存储(见下方「浏览器端文件上传」)。
|
|
8
|
+
|
|
9
|
+
## 适用场景
|
|
10
|
+
|
|
11
|
+
- 文件上传与下载
|
|
12
|
+
- 预签名 URL 生成(客户端直传)
|
|
13
|
+
- S3 兼容存储对接(AWS S3、MinIO、RustFS、阿里云 OSS 等)
|
|
14
|
+
- 本地开发文件存储
|
|
15
|
+
- 目录前缀清理与批量文件删除
|
|
16
|
+
|
|
17
|
+
## 使用步骤
|
|
18
|
+
|
|
19
|
+
### 1. 配置
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
# config/_storage.yml
|
|
23
|
+
type: local # local | s3
|
|
24
|
+
# 本地存储
|
|
25
|
+
root: ./data/storage
|
|
26
|
+
# S3 存储(type=s3 时生效)
|
|
27
|
+
# endpoint: ''
|
|
28
|
+
# region: us-east-1
|
|
29
|
+
# accessKeyId: ''
|
|
30
|
+
# secretAccessKey: ''
|
|
31
|
+
# bucket: my-bucket
|
|
32
|
+
# forcePathStyle: false
|
|
33
|
+
# prefix: ''
|
|
34
|
+
# publicUrl: ''
|
|
35
|
+
operationLog:
|
|
36
|
+
read: false # file.get/head/exists, dir.list, presign.getUrl/publicUrl
|
|
37
|
+
write: false # file.put/delete/deleteMany/copy, dir.delete, presign.putUrl
|
|
38
|
+
maxLength: 1000
|
|
39
|
+
level: debug # info | debug | trace
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
约定环境变量优先于 YAML,例如 `HAI_STORAGE_ROOT`、
|
|
43
|
+
`HAI_STORAGE_ACCESSKEYID`、`HAI_STORAGE_SECRETACCESSKEY`。
|
|
44
|
+
|
|
45
|
+
MinIO、RustFS 使用同一个 S3 Provider:配置 `type: s3`、S3 API `endpoint`(通常为 9000 端口)和 `forcePathStyle: true`,并预先创建 `bucket`;不要新增 `type: minio` 或 `type: rustfs`。
|
|
46
|
+
|
|
47
|
+
使用 hai-deploy 的默认自建 S3 配置时,可通过 `container.s3Engine`(`HAI_DEPLOY_CONTAINER_S3ENGINE`,`rustfs | minio`)选择实现,默认 RustFS。部署流程复用应用镜像中的 S3 SDK 幂等初始化 bucket,不依赖单独的 mc 镜像;自定义外部 S3 服务仍需确认 bucket 已存在且凭据有相应权限。
|
|
48
|
+
|
|
49
|
+
`operationLog` 在 Local/S3 Provider 的真实 file/dir/presign 操作处输出日志,不要在 `storage-main.ts` 或调用方重复包装。文件内容不会写入日志,上传数据只记录字节长度;`maxLength` 用于截断序列化后的参数,`level` 默认 `debug`。
|
|
50
|
+
|
|
51
|
+
### 2. 初始化与关闭
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { storage } from '@h-ai/storage'
|
|
55
|
+
|
|
56
|
+
await storage.init(core.config.get('storage'))
|
|
57
|
+
// 使用后关闭
|
|
58
|
+
const closeResult = await storage.close()
|
|
59
|
+
if (!closeResult.success) {
|
|
60
|
+
throw new Error(closeResult.error.message)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`storage.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const closed = await storage.close()
|
|
69
|
+
if (!closed.success) {
|
|
70
|
+
// 处理清理故障后有限重试;再次失败应向应用报告。
|
|
71
|
+
const retried = await storage.close()
|
|
72
|
+
if (!retried.success) throw new Error(retried.error.message)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
> `storage.config` 返回的是**脱敏配置快照**;S3 的 `accessKeyId` / `secretAccessKey` 等敏感字段会被替换为 `[REDACTED]`。
|
|
78
|
+
|
|
79
|
+
## 核心 API
|
|
80
|
+
|
|
81
|
+
### 文件操作
|
|
82
|
+
|
|
83
|
+
`storage.file` 提供以下方法:
|
|
84
|
+
|
|
85
|
+
- `put(key, data, options?)`
|
|
86
|
+
- `get(key, options?)`
|
|
87
|
+
- `getStream(key, options?)`(字节流,迭代错误可抛出)
|
|
88
|
+
- `head(key)`
|
|
89
|
+
- `exists(key)`
|
|
90
|
+
- `delete(key)`
|
|
91
|
+
- `deleteMany(keys)`
|
|
92
|
+
- `copy(sourceKey, destKey, options?)`
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// 上传文件
|
|
96
|
+
const result = await storage.file.put('avatars/user-123.png', fileBuffer, {
|
|
97
|
+
contentType: 'image/png',
|
|
98
|
+
metadata: { userId: '123' },
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// 下载文件
|
|
102
|
+
const file = await storage.file.get('avatars/user-123.png')
|
|
103
|
+
|
|
104
|
+
// 列出文件
|
|
105
|
+
const files = await storage.dir.list({ prefix: 'avatars/', maxKeys: 100 })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`file.deleteMany(keys)` 返回 `StorageDeleteResult`:成功时 `data` 为 `{ deletedKeys, failures: [] }`;任一删除项未确认完成时返回 `OPERATION_FAILED`,进度在 `error.ext?.deleteMany`(初始化等前置错误可能不带进度)。重复输入只处理一次,输出 key 不含配置 prefix。S3 每批最多 1000 项,逐项检查 `Deleted` 与 `Errors`;HTTP 200 不代表全部成功,遗漏/重复/未知回执标为 `unknown`。本地删除最多并发 32 项。
|
|
109
|
+
|
|
110
|
+
只对明确的 `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` 项最多尝试三次;成功及权限失败项不重发。`DeleteObjects` 专用客户端禁用 SDK 请求重试;网络中断立即标为 `unknown`,未发送的后续批次标为 `notAttempted`,不盲目重放未知结果。`retryable` 只表示故障类别,调用方重试前仍须核对未知结果及并发写入。`dir.delete()` 传播删除失败;它不能阻止并发新增对象。S3 使用不带 VersionId 的删除,开启版本控制的桶可能创建删除标记,不保证清除历史版本。
|
|
111
|
+
|
|
112
|
+
`@h-ai/serv` / `@h-ai/api-client` 的 `storage.files.deleteMany` 使用同一成功结构并透传失败进度。
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const result = await storage.file.deleteMany(['a.txt', 'b.txt'])
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
const progress = result.error.ext?.deleteMany
|
|
118
|
+
// 结合 outcome 与业务写入状态决定补偿;未知结果不能当作未执行。
|
|
119
|
+
const knownFailures = progress?.failures.filter(item => item.outcome === 'failed' && item.retryable) ?? []
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 预签名 URL
|
|
124
|
+
|
|
125
|
+
`storage.presign` 提供以下方法:
|
|
126
|
+
|
|
127
|
+
- `getUrl(key, options?)`
|
|
128
|
+
- `putUrl(key, options?)`
|
|
129
|
+
- `publicUrl(key)`
|
|
130
|
+
|
|
131
|
+
预签名仅支持 S3;local `getUrl` / `putUrl` 返回 `HaiStorageError.UNSUPPORTED_OPERATION`(hai:storage:018),`publicUrl` 返回 null,不生成虚拟 URL。serv 与 apiClient 保留失败,须先检查 success;本地文件由应用已有的鉴权接口上传/下载,不假定框架内置签名路由。
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// 客户端直传模式
|
|
135
|
+
const uploadUrl = await storage.presign.putUrl('uploads/doc.pdf', {
|
|
136
|
+
expiresIn: 3600, // 1 小时有效
|
|
137
|
+
contentType: 'application/pdf',
|
|
138
|
+
})
|
|
139
|
+
if (!uploadUrl.success) return uploadUrl
|
|
140
|
+
|
|
141
|
+
// 临时下载链接
|
|
142
|
+
const downloadUrl = await storage.presign.getUrl('uploads/doc.pdf', {
|
|
143
|
+
expiresIn: 300, // 5 分钟有效
|
|
144
|
+
})
|
|
145
|
+
if (!downloadUrl.success) return downloadUrl
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> `storage.presign.putUrl()` 当前不支持 `maxSize` 约束参数;如需大小限制,请在应用层上传前校验,或在服务端落库/回调处二次校验。
|
|
149
|
+
|
|
150
|
+
## 错误码 — `HaiStorageError`
|
|
151
|
+
|
|
152
|
+
| 错误码 | code | 说明 |
|
|
153
|
+
|--------|------|------|
|
|
154
|
+
| `HaiStorageError.CONNECTION_FAILED` | `hai:storage:001` | 连接失败 |
|
|
155
|
+
| `HaiStorageError.OPERATION_FAILED` | `hai:storage:002` | 操作失败 |
|
|
156
|
+
| `HaiStorageError.NOT_FOUND` | `hai:storage:003` | 文件不存在 |
|
|
157
|
+
| `HaiStorageError.ALREADY_EXISTS` | `hai:storage:004` | 文件已存在 |
|
|
158
|
+
| `HaiStorageError.CHECKSUM_FAILED` | `hai:storage:016` | checksum 读取/计算失败 |
|
|
159
|
+
| `HaiStorageError.INVALID_CHECKSUM` | `hai:storage:017` | checksum 格式/算法非法 |
|
|
160
|
+
| `HaiStorageError.UNSUPPORTED_OPERATION` | `hai:storage:018` | 当前 Provider 不支持该操作(local 预签名) |
|
|
161
|
+
| `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | 本地列表参数或游标无效、查询范围不匹配 |
|
|
162
|
+
| `HaiStorageError.PERMISSION_DENIED` | `hai:storage:005` | 权限不足 |
|
|
163
|
+
| `HaiStorageError.QUOTA_EXCEEDED` | `hai:storage:006` | 配额超限 |
|
|
164
|
+
| `HaiStorageError.INVALID_PATH` | `hai:storage:007` | 路径无效 |
|
|
165
|
+
| `HaiStorageError.IO_ERROR` | `hai:storage:008` | IO 错误 |
|
|
166
|
+
| `HaiStorageError.NETWORK_ERROR` | `hai:storage:009` | 网络错误 |
|
|
167
|
+
| `HaiStorageError.NOT_INITIALIZED` | `hai:storage:010` | 未初始化 |
|
|
168
|
+
| `HaiStorageError.UNSUPPORTED_TYPE` | `hai:storage:011` | 不支持的存储类型 |
|
|
169
|
+
| `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 配置错误 |
|
|
170
|
+
| `HaiStorageError.PRESIGN_FAILED` | `hai:storage:013` | 预签名 URL 生成失败 |
|
|
171
|
+
| `HaiStorageError.UPLOAD_FAILED` | `hai:storage:014` | 上传失败 |
|
|
172
|
+
| `HaiStorageError.DOWNLOAD_FAILED` | `hai:storage:015` | 下载失败 |
|
|
173
|
+
|
|
174
|
+
## HTTP API 契约
|
|
175
|
+
|
|
176
|
+
公共 HTTP API 统一由 `@h-ai/api-contract` 提供,并由 `@h-ai/serv/features/storage` 绑定到本模块。
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { apiClient } from '@h-ai/api-client'
|
|
180
|
+
|
|
181
|
+
const result = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
182
|
+
if (!result.success) return result
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 常见模式
|
|
186
|
+
|
|
187
|
+
大文件上传用 `file.put(key, readableOrAsyncByteStream, { signal, ifNoneMatch: '*' })`,内部使用有界 S3 multipart;下载用 `file.getStream(key, { signal, rangeStart, rangeEnd })`,及时消费或关闭,迭代期间 I/O/取消可能抛错。本地读取目录直接返回 `INVALID_PATH`。checksum 的 signal 取消会停止等待下一块数据并请求关闭输入迭代器。`get()` 返回完整 Buffer,只用于容量可控的数据。仅创建写入采用 S3 标准条件或本地原子发布,冲突返回 `ALREADY_EXISTS`;后端不支持时失败,不退回覆盖。
|
|
188
|
+
|
|
189
|
+
`checksum.generate(data, options?)` / `checksum.verify(data, expected, options?)` 默认 SHA-256、小写 hex,可选 sha384/sha512/sha1/md5、base64 编码与 signal;流仅消费一次,读取失败返回 HaiResult 错误,不匹配返回 false。MD5/SHA-1 不作安全认证,ETag 和 multipart 复合 checksum 不当作完整文件摘要。浏览器不导入 Node checksum;离线增量摘要需使用适用的浏览器实现,不把 SubtleCrypto.digest 当流式 API。
|
|
190
|
+
|
|
191
|
+
### 与 kit 集成(文件上传 API)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { storage } from '$lib/server/init'
|
|
195
|
+
// src/routes/api/files/upload/+server.ts
|
|
196
|
+
import { kit } from '@h-ai/kit'
|
|
197
|
+
import { z } from 'zod'
|
|
198
|
+
|
|
199
|
+
const UploadSchema = z.object({
|
|
200
|
+
key: z.string().min(1),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
export const POST = kit.handler(async ({ request, locals }) => {
|
|
204
|
+
kit.guard.require(locals.session, 'storage:write')
|
|
205
|
+
const formData = await request.formData()
|
|
206
|
+
const file = formData.get('file') as File
|
|
207
|
+
if (!file)
|
|
208
|
+
return kit.response.badRequest('Missing file')
|
|
209
|
+
|
|
210
|
+
const key = `uploads/${locals.session.userId}/${file.name}`
|
|
211
|
+
const buffer = new Uint8Array(await file.arrayBuffer())
|
|
212
|
+
const result = await storage.file.put(key, buffer, { contentType: file.type })
|
|
213
|
+
if (!result.success)
|
|
214
|
+
return kit.response.internalError()
|
|
215
|
+
return kit.response.created({ key })
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> 说明:`@h-ai/storage` 只提供存储能力,不强制要求 `/api/storage` 路由命名。应用可按业务语义自定义 API 路径。
|
|
220
|
+
|
|
221
|
+
### 列表分页
|
|
222
|
+
|
|
223
|
+
Local 的 `prefix` 是 key 字符串前缀,可匹配 `reports/2026-` 这类不以斜杠结尾的前缀。按 UTF-8 字节序返回,`maxKeys` 为 1–1000 的整数、默认 1000,`files` 与 `commonPrefixes` 合计占用预算。`delimiter` 按 prefix 后首次出现的位置分组,空字符串不分组;空物理目录不产生条目。分组与计数参照 [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)。
|
|
224
|
+
|
|
225
|
+
本地不透明游标绑定规范根目录、prefix 和 delimiter,不依赖进程缓存;同根目录的独立实例或重启后可以续页,下一页可调整 maxKeys。格式、版本或范围不匹配返回 `INVALID_LIST_OPTIONS`。只在确有下一条时返回 `isTruncated: true` 和 `nextContinuationToken`。游标不是授权凭据,HTTP 服务仍须授权 prefix;它不提供快照,并发增删可影响后续页。不同挂载路径不视为同一根目录。每次请求仍扫描排序匹配目录,大目录应缩小 prefix 或使用 S3。
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
let continuationToken: string | undefined
|
|
229
|
+
do {
|
|
230
|
+
const page = await storage.dir.list({ prefix: 'reports/2026-', delimiter: '/', maxKeys: 100, continuationToken })
|
|
231
|
+
if (!page.success) return page
|
|
232
|
+
// 消费当前页的 files 和 commonPrefixes。
|
|
233
|
+
continuationToken = page.data.nextContinuationToken
|
|
234
|
+
} while (continuationToken)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 头像上传
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const key = `avatars/${userId}/${Date.now()}.png`
|
|
241
|
+
const result = await storage.file.put(key, avatarBuffer, {
|
|
242
|
+
contentType: 'image/png',
|
|
243
|
+
})
|
|
244
|
+
if (result.success) {
|
|
245
|
+
const publicUrl = storage.presign.publicUrl(key)
|
|
246
|
+
await iam.user.updateUser(userId, { avatarUrl: publicUrl ?? '' })
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 浏览器端文件上传(预签名 URL 模式)
|
|
251
|
+
|
|
252
|
+
`uploadWithPresignedUrl` 传 `onProgress` 时使用 XHR,否则使用 fetch。`abortController` 预取消不发送;准备中/进行中取消、外部 XHR abort、网络/超时和进度回调异常一次结算失败并解绑本次监听,完成后取消不会触碰旧 XHR。取消不能撤销远端已发布的对象;应用先核对业务/对象状态,再授权清理,不把本地取消当作服务端回滚。实际 Chrome 门禁设置 `HAI_STORAGE_RUN_BROWSER_GATES=1`,执行 `pnpm --filter @h-ai/storage exec vitest run tests/storage-client-browser.test.ts`;浏览器缺失直接失败。
|
|
253
|
+
|
|
254
|
+
浏览器端无法直接使用 `storage` 模块。服务端使用 S3 时,通过 typed API 获取预签名 URL,检查失败后再直传:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { apiClient } from '@h-ai/api-client'
|
|
258
|
+
import { uploadWithPresignedUrl } from '@h-ai/storage/client'
|
|
259
|
+
|
|
260
|
+
// apiClient 已初始化;用户和资源范围由服务端授权策略决定。
|
|
261
|
+
const result = await apiClient.storage.presignedUrls.createUpload({
|
|
262
|
+
key: `avatars/${userId}.png`,
|
|
263
|
+
contentType: 'image/png',
|
|
264
|
+
})
|
|
265
|
+
if (!result.success) return result
|
|
266
|
+
return uploadWithPresignedUrl(result.data.url, file, { contentType: 'image/png' })
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## 相关 Skills
|
|
270
|
+
|
|
271
|
+
- `hai-build`:模块初始化顺序
|
|
272
|
+
- `hai-core`:配置与 HaiResult 模型
|
|
273
|
+
- `hai-kit`:SvelteKit API 端点集成(`kit.handler` + `kit.guard`)
|
|
274
|
+
- `hai-ui`:Storage 场景组件(FileUpload/ImageUpload/AvatarUpload)
|
|
275
|
+
|
|
276
|
+
## 生产共享状态
|
|
277
|
+
|
|
278
|
+
生产使用共享 S3 或兼容服务;local 只用于开发/明确测试。对象业务授权和读改写的条件提交由应用负责。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化成功前检查,只有 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试豁免。不要将租约当作最终写入条件,外部未知效果须按稳定业务键核对。
|
|
279
|
+
|
|
280
|
+
## 跨端输入与能力约束
|
|
281
|
+
|
|
282
|
+
key 统一使用 `@h-ai/storage/schemas` 的 StorageKeySchema(UTF-8 最多1024字节,无绝对路径/反斜线/控制符/点路径段),prefix另允许空串与部分文件名。HTTP批量删除按原数组限制1–1000项,不能以去重绕过;Node仍按Provider分批。签名有效期整数1–604800秒,Node默认3600、HTTP默认900。HTTP上传只接受key/contentType/expiresIn,不接受contentLength/maxSize/metadata/checksum等file.put选项;未知字段返回400,typed client为VALIDATION_FAILED。Node流式读写、Range、checksum、条件写等无默认HTTP端点。FileUpload大小/类型仅为前端选择体验,服务端仍需验证实际对象与归属;不要编造上传代理端点。校验不替代session资源授权及Provider防逃逸。
|
|
@@ -19,19 +19,29 @@ description: "使用 @h-ai/ui 构建 Svelte 5 表单、表格、弹窗、新手
|
|
|
19
19
|
|
|
20
20
|
## 使用路径
|
|
21
21
|
|
|
22
|
+
新页面或较大改版先使用 [hai-ui-design](../hai-ui-design/SKILL.md) 确定任务、布局、状态和组件映射;本 skill 负责正确实现。完成后按 [hai-ui-review](../hai-ui-review/SKILL.md) 查看真实页面,小范围修复沿用已有设计。
|
|
23
|
+
|
|
24
|
+
- 密码登录只接收 identifier/password;AuthPanel 将 LoginForm 的 username 映射为 identifier。会话期限来自服务端 session.maxAge/refreshTokenMaxAge,不增加未实现的登录偏好字段或持久会话选择框。
|
|
25
|
+
|
|
22
26
|
- 先检查组件导入、Tailwind @source 和主题 CSS,再选场景 → 组合 → 原子组件,缺能力才自定义。
|
|
23
|
-
-
|
|
27
|
+
- 管理列表和工作台随可用宽度展开;flex/grid 子项设 min-width: 0,表格在自身容器滚动。阅读页与登录表单按内容约束宽度。实际检查 390、1440、1920、2560、3840px,避免宽屏仍挤在中央窄列,手机出现整页横向滚动。
|
|
28
|
+
- 按安装版本的 exports 选择明确列出的 components barrel 入口(不开放任意内部文件);主题工具使用 `@h-ai/ui/theme`,避免仅为主题加载根入口的 AI/图表组件。禁止导入内部 src/dist;例如原子组件使用 `@h-ai/ui/components/primitives/index`。
|
|
24
29
|
- 自动导入识别无分号、跨行及别名的运行时 import;已绑定的本地组件名称不会重复注入。
|
|
25
|
-
-
|
|
30
|
+
- 应用只提供页面级文案;组件内部中英日文由 @h-ai/ui 维护。
|
|
26
31
|
- `UserProfile` 用 `avatarAccept` / `avatarHint` 对齐业务的头像格式与大小限制;文件最终校验和上传由业务回调完成。
|
|
27
32
|
- `BareInput type="file"` 通过 `onchange` 的 `currentTarget.files` 读取文件,不绑定文件路径 `value`;其他输入类型可 `bind:value`。
|
|
28
33
|
- AI 文本视为不可信;Markdown/Mermaid 预览与允许执行 HTML/JS 的预览有不同边界。
|
|
29
34
|
|
|
35
|
+
- Tabs 支持方向键、Home/End、跳过禁用项与单一 Tab 入口,children 内容关联当前页签;DataTable 通过 aria-sort/aria-busy 和加载文字提供状态,有 onrowclick 时支持行 Enter/Space,单元格操作不重复触发行操作。
|
|
36
|
+
- Pagination 导航和输入名称跟随 UI locale,外部 page 变化同步跳页草稿,触摸设备扩大导航按钮;TableToolbar 打开后进入控件,Escape 返回触发点,视图按钮带选中状态。应用仍需接入排序、筛选、分页和视图对应的数据行为。
|
|
37
|
+
- CrudPage 占满宿主剩余高度,空数据和少量数据时分页保留在底部;宿主从视口外壳到内容区逐层使用 flex/grid 的剩余空间约束与 min-height: 0,避免自动高度包装层截断布局。数据区独立滚动,分页不覆盖数据。
|
|
38
|
+
- theme.css 自动引入 design-tokens.css;全局样式的焦点使用 --hai-focus-color(缺省跟随当前 primary),减少动态效果时保留最终状态并缩短动画。长内容在 Empty/Alert 中换行;业务自定义 snippet 也要能收缩。
|
|
39
|
+
|
|
30
40
|
## 按需参考
|
|
31
41
|
|
|
32
42
|
新手引导、首次 Cookie 提示、操作导览和帮助中心见 [guide.md](guide.md)。fullstack 默认在共享管理界面接入,新增业务只补内容与 data-guide 锚点。
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
|
|
35
45
|
|
|
36
46
|
- 运行环境
|
|
37
47
|
- 适用场景
|
|
@@ -59,3 +69,7 @@ UserProfile 的 `fieldConstraints` 按字段声明 `required/minlength/maxlength
|
|
|
59
69
|
IAM 表单、AuthPanel、UserProfile、FeedbackModal 直接接收同组参数;CRUD 使用 `form={{ columns: 2, labelPosition: 'left', labelWidth: '6rem' }}`,字段支持 `colSpan` 和延迟翻译的 `validation.message: () => m.required()`。通用字段的 `label/error/hint`、Input/Textarea/Select 的 `validationMessage` 支持翻译函数。内置必填、类型、长度和范围错误由 UI 提供;应用只提供业务文案。通过 UI 的 `setGlobalLocale` / `createLocaleStore` 同步语言,已显示的错误随之更新;自定义提示只用于失败,不能把固定提示直接设成持续的 customValidity。字段错误只在 FormField 或控件其中一处展示。
|
|
60
70
|
|
|
61
71
|
验证一列/双列与两种标签位置、自动/显式跨列、窄抽屉/手机、无效提交与修正、切换语言后的错误、按钮和回车提交,以及关闭重开后的状态。
|
|
72
|
+
|
|
73
|
+
Node 请求/SSR 必须用 `core.i18n.runWithLocale(locale, callback)`(kit 同名方法)包裹实际异步 resolve;serv 自动建立请求语言上下文。浏览器 setGlobalLocale/createLocaleStore 继续管理应用语言,SSR 的 UI setter/store 不写全局值。显式 MessageOptions.locale 优先;进程默认语言仅在启动时设置。
|
|
74
|
+
|
|
75
|
+
`@h-ai/ui/i18n` 的 `createLocaleStore` 按“可解析的持久化值 → 完整 navigator.languages(再检查 navigator.language)→ defaultLocale → 支持列表首项”选择语言;首个不支持的候选不触发默认回退。匹配大小写、语言简写及逐级区域/脚本回退,返回 supportedLocales 中的配置代码;同语言多个变体按列表顺序选择。detectBrowserLocale 无实际命中返回 null,不插入框架默认值;空支持列表最终保留调用方默认值。SSR 不读取浏览器候选或修改全局语言。应用若使用 Paraglide,仍需用该项目的 setLocale 同步应用翻译;内置 UI 字典的覆盖范围不因自定义 store 列表而自动增加。
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 最小接入
|
|
6
6
|
|
|
7
|
-
下面的 `m`、`locale`、`currentUser`、`createUser` 来自应用。翻译 key
|
|
7
|
+
下面的 `m`、`locale`、`currentUser`、`createUser` 来自应用。翻译 key 需要在应用中英日文消息文件定义;`nav` 使用当前宿主的公开导航适配器。
|
|
8
8
|
|
|
9
9
|
```svelte
|
|
10
10
|
<script lang='ts'>
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
## i18n、样式与边界
|
|
66
66
|
|
|
67
|
-
- 通用按钮、进度和错误由 UI
|
|
67
|
+
- 通用按钮、进度和错误由 UI 内置中英日文维护。业务文案使用 `() => m.key()` 延迟求值;GuideHost 的 locale 接入应用已有语言状态,不另建语言体系。
|
|
68
68
|
- 语言变化时宿主重新解析文案并刷新当前浮层。应用若采用整页重载或销毁组件切换语言,应按自身生命周期重新挂载宿主。
|
|
69
69
|
- 目标优先使用 `data-guide`,不依赖 Tailwind 类名或 DOM 顺序;现有 UI 组件支持 data 属性透传。复杂目标可用 `target: () => document.querySelector(...)`。
|
|
70
70
|
- 步骤标题和说明按纯文本显示;帮助正文使用现有安全 MarkdownRenderer,原始 HTML 关闭。
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
|
|
75
75
|
## 默认应用
|
|
76
76
|
|
|
77
|
-
fullstack 的 Web/App/Desktop 在共享 AdminWorkspace 中启用帮助入口、首次邀请和个人设置导览;Web 使用 Cookie,App/Desktop 使用 localStorage,按当前用户 ID 隔离。新增业务功能时,在共享 guide-store
|
|
77
|
+
fullstack 的 Web/App/Desktop 在共享 AdminWorkspace 中启用帮助入口、首次邀请和个人设置导览;Web 使用 Cookie,App/Desktop 使用 localStorage,按当前用户 ID 隔离。新增业务功能时,在共享 guide-store 和中英日文消息中补充文章、导览和稳定锚点。
|
|
78
78
|
|
|
79
79
|
验证首次提示、完成后刷新、手动重看、跳转后等待、缺失目标、账号/版本隔离、语言与主题、移动宽度、键盘和卸载取消。
|