@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.67

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 (125) hide show
  1. package/README.en.md +66 -257
  2. package/README.ja.md +66 -257
  3. package/README.md +66 -257
  4. package/dist/{chunk-QYX6V7H2.js → chunk-7DSURTJ4.js} +22 -79
  5. package/dist/chunk-7DSURTJ4.js.map +1 -0
  6. package/dist/{chunk-FWN5B54L.js → chunk-AOQ4N657.js} +182 -10
  7. package/dist/chunk-AOQ4N657.js.map +1 -0
  8. package/dist/cli-config-templates-6AHNUGBN.js +5 -0
  9. package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
  10. package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-FC3QRKDO.js} +3 -3
  11. package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
  12. package/dist/index.js +435 -247
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-RDQ2EIBT.js → src-2V6VP4I5.js} +553 -119
  15. package/dist/src-2V6VP4I5.js.map +1 -0
  16. package/package.json +8 -8
  17. package/templates/README.md +48 -145
  18. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +1 -1
  19. package/templates/apps/api/.dockerignore.hbs +14 -7
  20. package/templates/apps/api/.gitignore.hbs +7 -0
  21. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
  22. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +28 -7
  23. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
  24. package/templates/apps/api/hai.build.json.hbs +1 -1
  25. package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
  26. package/templates/apps/api/package.json.hbs +5 -0
  27. package/templates/apps/fullstack/.dockerignore.hbs +14 -9
  28. package/templates/apps/fullstack/.gitignore.hbs +7 -0
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -5
  30. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  33. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +22 -4
  34. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
  35. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +23 -5
  36. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  37. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +11 -1
  38. package/templates/apps/fullstack/hai.build.json.hbs +2 -2
  39. package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
  40. package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
  41. package/templates/apps/fullstack/package.json.hbs +2 -0
  42. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
  43. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +27 -6
  44. package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
  45. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +32 -11
  46. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
  47. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
  48. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
  49. package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
  50. package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
  51. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
  52. package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
  53. package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
  54. package/templates/base/.dockerignore.hbs +14 -8
  55. package/templates/base/.gitignore.hbs +7 -0
  56. package/templates/base/e2e/app.spec.ts.hbs +10 -0
  57. package/templates/base/hai.build.json.hbs +1 -1
  58. package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
  59. package/templates/base/package.json.hbs +8 -3
  60. package/templates/base/vite.config.ts.hbs +3 -1
  61. package/templates/shared/.gitattributes +6 -0
  62. package/templates/skills/AGENTS.md +3 -3
  63. package/templates/skills/bridges/admin/AGENTS.md +1 -1
  64. package/templates/skills/bridges/api/AGENTS.md +1 -1
  65. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
  66. package/templates/skills/bridges/h5/AGENTS.md +1 -1
  67. package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
  68. package/templates/skills/bridges/website/AGENTS.md +1 -1
  69. package/templates/skills/hai-api-client/SKILL.md +14 -161
  70. package/templates/skills/hai-api-client/reference.md +166 -0
  71. package/templates/skills/hai-api-contract/SKILL.md +15 -273
  72. package/templates/skills/hai-api-contract/reference.md +278 -0
  73. package/templates/skills/hai-app-create/SKILL.md +2 -2
  74. package/templates/skills/hai-app-create/development.md +4 -4
  75. package/templates/skills/hai-app-create/reference.md +5 -4
  76. package/templates/skills/hai-app-review/SKILL.md +1 -1
  77. package/templates/skills/hai-app-tests/SKILL.md +4 -3
  78. package/templates/skills/hai-app-tests/reference.md +2 -2
  79. package/templates/skills/hai-app-tests/validation.md +31 -0
  80. package/templates/skills/hai-audit/SKILL.md +17 -142
  81. package/templates/skills/hai-audit/reference.md +145 -0
  82. package/templates/skills/hai-build/SKILL.md +14 -8
  83. package/templates/skills/hai-build/reference.md +10 -9
  84. package/templates/skills/hai-cache/SKILL.md +15 -182
  85. package/templates/skills/hai-cache/reference.md +187 -0
  86. package/templates/skills/hai-capacitor/SKILL.md +4 -3
  87. package/templates/skills/hai-capacitor/reference.md +6 -4
  88. package/templates/skills/hai-ci/SKILL.md +2 -0
  89. package/templates/skills/hai-core/SKILL.md +3 -3
  90. package/templates/skills/hai-crypto/SKILL.md +16 -252
  91. package/templates/skills/hai-crypto/reference.md +256 -0
  92. package/templates/skills/hai-datapipe/SKILL.md +15 -196
  93. package/templates/skills/hai-datapipe/reference.md +202 -0
  94. package/templates/skills/hai-deploy/SKILL.md +20 -55
  95. package/templates/skills/hai-deploy/middleware.md +5 -11
  96. package/templates/skills/hai-framework-sync/SKILL.md +2 -2
  97. package/templates/skills/hai-framework-sync/sync.md +1 -1
  98. package/templates/skills/hai-iam/SKILL.md +1 -3
  99. package/templates/skills/hai-kit/SKILL.md +12 -167
  100. package/templates/skills/hai-kit/reference.md +172 -0
  101. package/templates/skills/hai-payment/SKILL.md +19 -251
  102. package/templates/skills/hai-payment/reference.md +256 -0
  103. package/templates/skills/hai-pr-review/SKILL.md +4 -4
  104. package/templates/skills/hai-reach/SKILL.md +2 -2
  105. package/templates/skills/hai-reldb/SKILL.md +1 -1
  106. package/templates/skills/hai-scheduler/SKILL.md +15 -195
  107. package/templates/skills/hai-scheduler/reference.md +202 -0
  108. package/templates/skills/hai-serv/SKILL.md +2 -2
  109. package/templates/skills/hai-spec-implement/SKILL.md +1 -1
  110. package/templates/skills/hai-spec-implement/workflow.md +3 -3
  111. package/templates/skills/hai-storage/SKILL.md +17 -238
  112. package/templates/skills/hai-storage/reference.md +243 -0
  113. package/templates/skills/hai-ui/SKILL.md +1 -1
  114. package/templates/skills/hai-ui/reference.md +9 -5
  115. package/templates/skills/hai-vecdb/SKILL.md +2 -2
  116. package/dist/chunk-FWN5B54L.js.map +0 -1
  117. package/dist/chunk-QYX6V7H2.js.map +0 -1
  118. package/dist/cli-config-templates-3XSFIXBP.js +0 -5
  119. package/dist/src-RDQ2EIBT.js.map +0 -1
  120. package/templates/apps/api/Dockerfile.hbs +0 -35
  121. package/templates/apps/fullstack/Dockerfile.hbs +0 -57
  122. /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
  123. /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
  124. /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
  125. /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
@@ -0,0 +1,202 @@
1
+ # hai-scheduler 参考
2
+
3
+ 持久化动态任务每个 `tickInterval`(默认 1000ms)在运行节点刷新;API 操作及获锁后的每次尝试重读定义,读取失败或任务已禁用/删除/改期时中断执行。`tasks` 仅表示最近成功刷新快照,已开始的外部副作用不会回滚。跨节点任务使用 `register()` 与共享数据库、Redis;`init({ tasks })` 是节点本地配置,memory cache 不支持跨进程防重。
4
+
5
+ `refreshTasks()` 主动刷新而不执行,同一快照的并发读取合并;保存、更新和删除落库成功后才发布内存投影。清空可选策略使用 `retry: null` / `handler: null`。Hook 业务异常为 `failed` 并重试,缺处理器、取消或 `SchedulerTaskSkippedError` 为 `interrupted`;一次性任务只在成功后删除。`event.context.occurrenceId` 在一次发生的重试期间不变,宿主用其绑定持久幂等回执;跨周期长任务使用宿主持久租约。配置被替换时旧发生停止重试,成功清理前再次核对定义。
6
+
7
+ 关闭必须 `await scheduler.close()` 后再关闭数据库和缓存:关闭会拒绝新任务、取消 HTTP/Worker/重试并等待执行链退出。生命周期回调通过事件 `signal` 协作取消,必须自行停止外部副作用,禁止在 Hook 阻塞主线程。旧任务取消后不重试、不删除新实例同 ID 的任务。
8
+
9
+ JS 任务每次调用独立 Worker,默认 30000 毫秒期限包含线程启动;超时后等待线程终止才返回失败或重试,同步死循环不会占用主事件循环。上下文和结果须支持结构化克隆。Worker/vm 不构成安全沙箱,仍只接受受信任的服务端代码。
10
+
11
+ 初始化为全有或全无:必须检查 `init()` 的 HaiResult。空 ID、重复 ID(包括与数据库记录重复)、无效 cron、持久化读取或解析失败均使初始化失败并清理状态;修正后重试,不会静默跳过任务。
12
+
13
+ API 任务 `timeout` 默认 30000 毫秒,覆盖请求及完整响应体读取;`maxResponseBytes` 指定实际接收字节上限(正安全整数,默认 1048576 即 1 MiB),例如 `maxResponseBytes: 10 * 1024 * 1024` 允许最大 10 MiB;非法值在请求前失败。超时、超限和非成功 HTTP 状态均失败,重试由任务 retry 策略控制。
14
+
15
+
16
+
17
+ > `@h-ai/scheduler` 使用统一任务模型管理 API / JS / Hook 三类执行路径,支持持久化、手工触发来源记录、分布式锁与全局生命周期回调。
18
+
19
+ ## 运行环境
20
+
21
+ > ⚠️ **服务端模块(Node.js only)。** 浏览器端通过 API 端点触发 `scheduler.trigger()` 或查询任务日志。
22
+
23
+ ## 依赖
24
+
25
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
26
+ | --- | --- | --- | --- |
27
+ | `@h-ai/reldb` | 任务定义与执行日志持久化 | `enableDb: true` 时必需 | 需在 `scheduler.init()` 前初始化 |
28
+ | `@h-ai/cache` | 分布式锁 | 可选 | 初始化后自动启用 |
29
+
30
+ ## 使用步骤
31
+
32
+ ### 1. 初始化
33
+
34
+ ```ts
35
+ import { cache } from '@h-ai/cache'
36
+ import { core } from '@h-ai/core'
37
+ import { reldb } from '@h-ai/reldb'
38
+ import { scheduler } from '@h-ai/scheduler'
39
+
40
+ await reldb.init({ type: 'sqlite', database: './scheduler.db' })
41
+ await cache.init({ type: 'memory' })
42
+
43
+ await scheduler.init({
44
+ enableDb: true,
45
+ maxLogs: 1000,
46
+ retentionDays: 30,
47
+ hooks: {
48
+ onTaskStart(event) {
49
+ core.logger.info('Task started', { taskId: event.task.id, trigger: event.trigger })
50
+ },
51
+ onTaskInterrupted(event) {
52
+ core.logger.warn('Task interrupted', { taskId: event.task.id, reason: event.reason })
53
+ },
54
+ onTaskFinish(event) {
55
+ core.logger.info('Task finished', { status: event.log.status })
56
+ },
57
+ },
58
+ })
59
+ ```
60
+
61
+ ### 2. 注册任务
62
+
63
+ ```ts
64
+ await scheduler.register({
65
+ id: 'health-check',
66
+ name: '健康检查',
67
+ description: '每 5 分钟巡检一次接口健康状态',
68
+ cron: '*/5 * * * *',
69
+ params: { channel: 'ops' },
70
+ retry: { maxAttempts: 3, backoffMs: [1000, 5000] },
71
+ handler: {
72
+ kind: 'api',
73
+ url: 'https://api.example.com/health',
74
+ method: 'GET',
75
+ },
76
+ })
77
+
78
+ await scheduler.register({
79
+ id: 'cleanup',
80
+ name: '清理过期数据',
81
+ description: '夜间单次清理任务',
82
+ cron: '0 2 * * *',
83
+ deleteAfterRun: true,
84
+ params: { source: 'nightly' },
85
+ handler: {
86
+ kind: 'js',
87
+ code: '(context) => ({ taskId: context.taskId, params: context.params })',
88
+ },
89
+ })
90
+ ```
91
+
92
+ > ⚠️ **安全警示**:`kind: 'js'` 仅允许受信任的服务端代码。当前实现在线程内使用 Node.js `vm`,超时可终止 Worker,但不构成安全沙箱;禁止把用户、租户或运营后台自由输入的 JS 字符串直接注册为任务。需要可配置执行逻辑时,优先改用 `kind: 'api'` 或 `hooks.onTaskExecute`。
93
+
94
+ ### 3. 使用全局 execute hook 处理无 handler 任务
95
+
96
+ ```ts
97
+ await scheduler.setHooks({
98
+ async onTaskExecute(event) {
99
+ return { via: 'hook', source: event.context.trigger.source }
100
+ },
101
+ })
102
+
103
+ await scheduler.register({
104
+ id: 'hook-task',
105
+ name: 'Hook 任务',
106
+ cron: '* * * * *',
107
+ })
108
+ ```
109
+
110
+ ### 4. 手工触发与日志查询
111
+
112
+ ```ts
113
+ const result = await scheduler.trigger('cleanup', { source: 'admin-console' })
114
+
115
+ const logs = await scheduler.getLogs({
116
+ taskId: 'cleanup',
117
+ triggerType: 'manual',
118
+ triggerSource: 'admin-console',
119
+ startedAfter: Date.now() - 24 * 60 * 60 * 1000,
120
+ startedBefore: Date.now(),
121
+ pagination: { page: 1, pageSize: 20 },
122
+ })
123
+ ```
124
+
125
+ ## 核心 API
126
+
127
+ | 方法 / 属性 | 签名 | 说明 |
128
+ | --- | --- | --- |
129
+ | `init` | `(config?) => Promise<HaiResult<void>>` | 初始化调度器 |
130
+ | `register` | `(task) => Promise<HaiResult<void>>` | 注册统一任务模型 |
131
+ | `updateTask` | `(taskId, updates) => Promise<HaiResult<void>>` | 更新 cron / params / handler |
132
+ | `refreshTasks` | `() => Promise<HaiResult<void>>` | 主动刷新共享定义,不执行任务 |
133
+ | `register` 扩展字段 | `description / deleteAfterRun / retry` | 支持任务描述、一次性任务、失败重试策略 |
134
+ | `trigger` | `(taskId, { source? }) => Promise<HaiResult<TaskExecutionLog>>` | 手工触发并记录来源 |
135
+ | `getLogs` | `(options?) => Promise<HaiResult<PaginatedResult<TaskExecutionLog>>>` | 支持按 trigger + startedAfter/startedBefore 过滤日志 |
136
+ | `setHooks` | `(hooks) => HaiResult<void>` | 设置全局生命周期回调 |
137
+ | `clearHooks` | `() => HaiResult<void>` | 清空全局生命周期回调 |
138
+ | `start / stop` | `() => HaiResult<void>` | 启动 / 停止调度 |
139
+ | `close` | `() => Promise<void>` | 关闭调度器 |
140
+
141
+ ## 常见模式
142
+
143
+ ### 持久化 JS 任务
144
+
145
+ ```ts
146
+ await scheduler.init({ enableDb: true })
147
+
148
+ await scheduler.register({
149
+ id: 'persisted-js',
150
+ name: '持久化 JS 任务',
151
+ cron: '*/10 * * * *',
152
+ handler: {
153
+ kind: 'js',
154
+ code: '(context) => ({ taskId: context.taskId })',
155
+ },
156
+ })
157
+ ```
158
+
159
+ ### 手工触发来源审计
160
+
161
+ ```ts
162
+ await scheduler.trigger('cleanup', { source: 'admin-console' })
163
+ await scheduler.trigger('cleanup', { source: 'cli' })
164
+
165
+ const adminLogs = await scheduler.getLogs({ triggerSource: 'admin-console' })
166
+ ```
167
+
168
+ ### 分布式锁
169
+
170
+ ```ts
171
+ await cache.init({ type: 'redis', host: 'localhost', port: 6379 })
172
+ await reldb.init({ type: 'sqlite', database: './scheduler.db' })
173
+ await scheduler.init({
174
+ enableDb: true,
175
+ maxLogs: 500,
176
+ retentionDays: 14,
177
+ lockExpireMs: 300000,
178
+ nodeId: 'node-1',
179
+ })
180
+ ```
181
+
182
+ ## 错误码 — `HaiSchedulerError`
183
+
184
+ | 错误码 | code | 说明 |
185
+ |--------|------|------|
186
+ | `HaiSchedulerError.NOT_INITIALIZED` | `hai:scheduler:010` | 未初始化 |
187
+ | `HaiSchedulerError.INIT_FAILED` | `hai:scheduler:011` | 初始化失败 |
188
+ | `HaiSchedulerError.CONFIG_ERROR` | `hai:scheduler:012` | 配置错误 |
189
+ | `HaiSchedulerError.TASK_NOT_FOUND` | `hai:scheduler:020` | 任务不存在 |
190
+ | `HaiSchedulerError.TASK_ALREADY_EXISTS` | `hai:scheduler:021` | 任务已存在 |
191
+ | `HaiSchedulerError.INVALID_CRON` | `hai:scheduler:022` | Cron 表达式无效 |
192
+ | `HaiSchedulerError.EXECUTION_FAILED` | `hai:scheduler:023` | 执行失败 |
193
+ | `HaiSchedulerError.JS_EXECUTION_FAILED` | `hai:scheduler:024` | JS 执行失败 |
194
+ | `HaiSchedulerError.API_EXECUTION_FAILED` | `hai:scheduler:025` | API 执行失败 |
195
+ | `HaiSchedulerError.DB_SAVE_FAILED` | `hai:scheduler:026` | DB 保存失败 |
196
+ | `HaiSchedulerError.ALREADY_RUNNING` | `hai:scheduler:027` | 已在运行 |
197
+ | `HaiSchedulerError.NOT_RUNNING` | `hai:scheduler:028` | 未在运行 |
198
+ | `HaiSchedulerError.LOCK_ACQUIRE_FAILED` | `hai:scheduler:029` | 锁获取失败 |
199
+ | `HaiSchedulerError.JS_COMPILE_FAILED` | `hai:scheduler:030` | JS 编译失败 |
200
+ | `HaiSchedulerError.HOOK_EXECUTION_FAILED` | `hai:scheduler:031` | Hook 执行失败 |
201
+
202
+ 调度循环按分钟时间点匹配 cron,并用同一分钟生成分布式锁键;启动或 tick 延迟至非零秒仍检查当前分钟,同一轮运行不会每个 tick 重复调度。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hai-serv
3
- description: "使用 @h-ai/serv 装配 contract/procedures、认证守卫、OpenAPI、HTTP 与语音 WebSocket。"
3
+ description: "为独立 API 服务装配 contract/procedures、认证守卫、Node/Fetch、OpenAPI 与语音 WebSocket。"
4
4
  ---
5
5
 
6
6
  # hai-serv
@@ -25,7 +25,7 @@ description: "使用 @h-ai/serv 装配 contract/procedures、认证守卫、Open
25
25
 
26
26
  ## 按需参考
27
27
 
28
- 详细配置、API 签名、错误码和范本在 [reference.md](reference.md)。按下列标题定位所需小节,不默认通读;用例中的业务变量需结合当前应用补齐。
28
+ 按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
29
29
 
30
30
  - 运行环境
31
31
  - 适用场景
@@ -25,7 +25,7 @@ description: "从需求或设计文档规划、执行、返修 hai 应用,或
25
25
  2. 按下方原则决定一个任务还是多个任务,使用 [任务文档范本](templates.md) 写入清单、文档基线和任务记录。一个任务也保留清单及记录,已有清单优先更新,不重复规划已交付内容。
26
26
  3. 执行、继续或返修时读取 [执行与 Git 流程](workflow.md),核验依赖、工作区、主分支和实际状态;每个任务使用记录中的独立分支。
27
27
  4. 按 [hai-app-create](../hai-app-create/SKILL.md) 与当前版本公开契约实现,按 [hai-app-tests](../hai-app-tests/SKILL.md) 做 Red → Green → Refactor,按 [hai-app-review](../hai-app-review/SKILL.md) 审查改动。只读取当前任务需要的模块资源。
28
- 5. 验证、提交、合并后更新清单与任务记录;全部目标范围验收通过才推进文档基线,规则见 [基线收尾](changes.md#验收与基线推进)。报告完成任务、旧/新文档 SHA、实现/合并提交、门禁和剩余阻塞;验证通过但尚未合并不能标为已完成。
28
+ 5. 按 [分轮验证与集中修复](../hai-app-tests/validation.md) 先收集适用门禁、集中修复再复测;验证、提交、合并后更新清单与任务记录。全部目标范围验收通过才推进文档基线,规则见 [基线收尾](changes.md#验收与基线推进)。报告完成任务、旧/新文档 SHA、实现/合并提交、门禁和剩余阻塞;验证通过但尚未合并不能标为已完成。
29
29
 
30
30
  ## 拆分原则
31
31
 
@@ -36,14 +36,14 @@
36
36
 
37
37
  ## 验证、提交与合并
38
38
 
39
- 1. 按项目已有脚本和影响范围依次运行 typecheck → lint → build(构建/模板/跨包变更)→ test;UI、路由、跨层流程再跑适用 E2E,涉及界面时保留可核验截图。工作区包优先用 filter,不能拿缓存/旧截图代替当前行为。未运行、失败、mock 与真实环境分别记录。
39
+ 1. 按项目已有脚本和影响范围执行 [分轮验证与集中修复](../hai-app-tests/validation.md):先收集本轮全部可执行的 typecheck/lint/build/test 及适用 E2E,普通失败不边测边改;汇总后集中修复、再验证。涉及界面保留可核验截图,工作区包优先用 filter;不能拿旧产物/旧截图代替当前行为,未运行、失败、mock 与真实环境分别记录。
40
40
  2. 把主分支的新提交合入任务分支,解决冲突后重跑受影响门禁;若主分支再次推进,重新同步及验证。冲突按当前需求和双方真实改动解决,不能整体选择 ours/theirs 覆盖其他任务。
41
41
  3. 通过后将本轮状态更新为“已验证”,只提交本任务的代码、配置、测试、说明和任务信息;检查 staged diff,不能混入用户或其他任务改动。可保留有意义的小步提交,不要求每个任务只有一个 commit。
42
42
  4. 回到主分支(或它对应的干净 worktree),将任务分支合并;项目没有相反规则时使用 `git merge --no-ff <任务分支>` 保留任务边界。明确授权执行本工作流时可完成本地合并;用户明确只要分支/待审结果时保持“已验证”。遵守项目受保护分支/PR 规则,不绕过;远端推送、PR、发布依用户实际授权处理。
43
- 5. 核对实际集成提交及主分支已包含本轮改动;项目采用 squash/rebase 时记录对应提交,不能假定 SHA 保持不变。运行集成后受影响验证。所有任务完成后再执行项目要求的全量门禁和整体需求验收;最终任务保持“已验证”,直到这些检查通过。失败先修复,不把 Git 合并成功当成业务验证通过。
43
+ 5. 核对实际集成提交及主分支已包含本轮改动;项目采用 squash/rebase 时记录对应提交,不能假定 SHA 保持不变。运行集成后受影响验证。所有任务完成后再按分轮流程收集项目要求的全量门禁和整体需求验收,汇总后集中修复、再验证;最终任务保持“已验证”,直到这些检查通过。不把 Git 合并成功当成业务验证通过。
44
44
  6. 在主分支将清单和任务记录更新为“已完成”,写入本轮任务分支 tip SHA、实际 merge SHA、集成验证结果和下一步,做独立的文档记账提交。merge SHA 在合并后才能知道,不能预填、也不能 amend 已合并提交去补记录。保留任务分支,以便返修。整个计划/变更轮次的已验收文档与实现基线只按 [基线收尾](changes.md#验收与基线推进) 推进,单个任务完成不代表整体完成。
45
45
 
46
- 有中断时保存实际阶段及下一步。已经合并却验证失败时,记录实际 merge SHA 和失败,清单保持“进行中”或“阻塞”,回原任务分支修复;后续任务暂不越过失败依赖。收尾报告区分“代码已合并”与“任务已完成”。
46
+ 有中断时保存实际阶段及下一步。已经合并却验证失败时,记录实际 merge SHA 和失败,先收集本轮其余可执行检查,清单保持“进行中”或“阻塞”,汇总后回原任务分支集中修复;后续任务暂不越过失败依赖。收尾报告区分“代码已合并”与“任务已完成”。
47
47
 
48
48
  ## 原任务返修
49
49
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hai-storage
3
- description: "使用 @h-ai/storage 访问本地/S3 文件、目录和预签名上传下载。"
3
+ description: "实现本地/S3 文件与目录操作、流式读写、checksum 和预签名上传下载;处理冲突、校验与取消。"
4
4
  ---
5
5
 
6
6
  # hai-storage
@@ -15,244 +15,23 @@ description: "使用 @h-ai/storage 访问本地/S3 文件、目录和预签名
15
15
  | 输出 | file/dir/presign 操作的 HaiResult;checksum 生成字符串或校验结果 |
16
16
  | 限制 | 仅服务端;浏览器经 API/预签名 URL。Local list 非真分页;putUrl 不提供 maxSize 强制约束,上传大小须服务端验证。 |
17
17
 
18
- > `@h-ai/storage` 提供统一文件存储接口,支持本地文件系统(LocalProvider)和 S3 兼容存储(S3Provider),通过 `storage.file`、`storage.dir`、`storage.presign` 访问存储,`storage.checksum` 提供 Node 端无需初始化的字节完整性工具。
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
+ 4. 不覆盖上传使用 ifNoneMatch;冲突/checksum 失败不能伪装成功。验证取消、流错误和文件清理,最后 close。
21
24
 
22
- > ⚠️ **服务端模块**。`storage.file` / `storage.dir` / `storage.presign` 均在 Node.js 端使用。浏览器端通过预签名 URL 或 API 端点间接访问存储(见下方「浏览器端文件上传」)。
25
+ ## 按需参考
23
26
 
24
- ## 适用场景
27
+ 按本次操作读取对应小节;以已安装版本的公开类型核验签名,示例中的业务变量由应用提供。
25
28
 
26
- - 文件上传与下载
27
- - 预签名 URL 生成(客户端直传)
28
- - S3 兼容存储对接(AWS S3、MinIO、RustFS、阿里云 OSS 等)
29
- - 本地开发文件存储
30
- - 目录前缀清理与批量文件删除
31
-
32
- ## 使用步骤
33
-
34
- ### 1. 配置
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)
29
+ - [运行环境](reference.md#运行环境)
30
+ - [适用场景](reference.md#适用场景)
31
+ - [使用步骤](reference.md#使用步骤)
32
+ - [核心 API](reference.md#核心-api)
33
+ - [错误码 — `HaiStorageError`](reference.md#错误码--haistorageerror)
34
+ - [HTTP API 契约](reference.md#http-api-契约)
35
+ - [常见模式](reference.md#常见模式)
36
+ - [浏览器端文件上传(预签名 URL 模式)](reference.md#浏览器端文件上传预签名-url-模式)
37
+ - [相关 Skills](reference.md#相关-skills)