@h-ai/storage 1.0.0-beta.2 → 1.0.0-beta.21

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.md CHANGED
@@ -4,34 +4,11 @@
4
4
 
5
5
  [框架首页](../../README.md) · 相关模块:[@h-ai/core](../core/README.md)
6
6
 
7
- ## 简介
8
-
9
7
  对象存储模块,提供统一 `storage` 访问入口,支持 S3 兼容存储与本地文件系统。
10
8
 
11
- 初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`storage.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
12
-
13
- ```ts
14
- const closed = await storage.close()
15
- if (!closed.success) {
16
- // 处理清理故障后有限重试;再次失败应向应用报告。
17
- const retried = await storage.close()
18
- if (!retried.success)
19
- throw new Error(retried.error.message)
20
- }
21
- ```
22
-
23
- S3 普通请求客户端与禁用请求重试的批量删除客户端分别释放;一个失败不阻止另一个清理,已释放的客户端不会重复释放。
24
-
25
- ## 核心功能清单
26
-
27
- - S3 协议(AWS S3 / MinIO / RustFS / OSS 等)
28
- - 本地文件系统
29
-
30
- ## 使用入门
9
+ ## 快速开始
31
10
 
32
- ### 快速开始
33
-
34
- #### Node.js 服务端
11
+ ### Node.js 服务端
35
12
 
36
13
  ```ts
37
14
  import { storage } from '@h-ai/storage'
@@ -83,6 +60,72 @@ await uploadWithPresignedUrl(signed.data.url, file)
83
60
 
84
61
  > 注意:`@h-ai/storage` 不内置 HTTP 路由;请在应用层自行实现签名 URL API。
85
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)
73
+ throw new Error(retried.error.message)
74
+ }
75
+ ```
76
+
77
+ S3 普通请求客户端与禁用请求重试的批量删除客户端分别释放;一个失败不阻止另一个清理,已释放的客户端不会重复释放。
78
+
79
+ ### 功能输入输出
80
+
81
+ 以下片段依赖本节完整初始化流程。示例中的业务值由宿主校验和授权后传入;异步领域结果先判断 success,再读取 data。表中 void 表示操作已完成且没有返回数据,null 表示不存在;同步工厂/工具直接返回表中类型。
82
+
83
+ #### storage.file
84
+
85
+ 对象生命周期输入原始 key、字节/流和选项;put/copy/head 返回元数据,get 返回 Buffer,getStream 返回异步字节流,exists 返回 boolean,delete 返回 void;deleteMany 返回逐项确认进度。
86
+
87
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
88
+ | ------------------------- | --------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
89
+ | `storage.file.put` | key, data, options | `FileMetadata` | `await storage.file.put('report.txt', 'text', { contentType: 'text/plain' })` |
90
+ | `storage.file.get` | key, options | `Buffer<ArrayBufferLike>` | `await storage.file.get('report.txt')` |
91
+ | `storage.file.getStream` | key, options | `AsyncIterable<Uint8Array<ArrayBufferLike>>` | `await storage.file.getStream('report.txt')` |
92
+ | `storage.file.head` | key | `FileMetadata` | `await storage.file.head('report.txt')` |
93
+ | `storage.file.exists` | key | `boolean` | `await storage.file.exists('report.txt')` |
94
+ | `storage.file.copy` | sourceKey, destKey, options | `FileMetadata` | `await storage.file.copy('report.txt', 'archive/report.txt')` |
95
+ | `storage.file.delete` | key | `void` | `await storage.file.delete('report.txt')` |
96
+ | `storage.file.deleteMany` | keys | `StorageDeleteResult` | `await storage.file.deleteMany(['report.txt', 'archive/report.txt'])` |
97
+
98
+ #### storage.dir
99
+
100
+ 按前缀浏览/清理;list 接收 prefix、分页选项返回文件列表/续页信息,delete 接收 prefix 返回 void,不能阻止并发新增。
101
+
102
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
103
+ | -------------------- | -------- | ------------ | ------------------------------------------------------------- |
104
+ | `storage.dir.list` | options | `ListResult` | `await storage.dir.list({ prefix: 'reports/', maxKeys: 20 })` |
105
+ | `storage.dir.delete` | prefix | `void` | `await storage.dir.delete('reports/')` |
106
+
107
+ #### storage.presign
108
+
109
+ S3 客户端直传/下载。key 和期限/响应选项生成 URL HaiResult;publicUrl 同步返回公开 URL 或 null,不代表获得访问授权。
110
+
111
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
112
+ | --------------------------- | ------------ | ---------------- | ------------------------------------------------------------------------------------------- |
113
+ | `storage.presign.getUrl` | key, options | `string` | `await storage.presign.getUrl('report.txt', { expiresIn: 300 })` |
114
+ | `storage.presign.putUrl` | key, options | `string` | `await storage.presign.putUrl('report.txt', { expiresIn: 300, contentType: 'text/plain' })` |
115
+ | `storage.presign.publicUrl` | key | `string \| null` | `storage.presign.publicUrl('report.txt')` |
116
+
117
+ #### storage.checksum
118
+
119
+ 无需初始化的字节完整性校验;数据及算法/编码得到摘要,expected 得到匹配 boolean;流输入消费一次。
120
+
121
+ | 入口 | 输入参数 | 成功结果 | 调用示例 |
122
+ | --------------------------- | ----------------------- | --------- | ------------------------------------------------------ |
123
+ | `storage.checksum.generate` | data, options | `string` | `await storage.checksum.generate('text')` |
124
+ | `storage.checksum.verify` | data, expected, options | `boolean` | `await storage.checksum.verify(bytes, expectedDigest)` |
125
+
126
+ - S3 协议(AWS S3 / MinIO / RustFS / OSS 等)
127
+ - 本地文件系统
128
+
86
129
  ### HTTP API 契约
87
130
 
88
131
  公共 HTTP API 统一由 `@h-ai/api-contract` 提供,并由 `@h-ai/serv` 的 `createStorageProcedures({ storage, authorize })` 绑定到本模块能力。
@@ -112,6 +155,10 @@ if (!result.success)
112
155
 
113
156
  上传选项 `{ ifNoneMatch: '*' }` 通过标准 S3 条件写或本地原子发布拒绝覆盖,冲突返回 `ALREADY_EXISTS`;S3 服务不支持条件写时返回错误,不降级为覆盖。失败/取消会清理临时文件或未完成分片;取消不能撤销已经发布的对象。
114
157
 
158
+ Local 在内容文件旁的保留目录 `.hai-storage-meta/` 中准备与文件身份绑定的代际元数据,再以内容文件的原子发布作为提交点。元数据准备失败不会新增或覆盖内容;默认元数据也有空记录,不继承旧对象的类型或业务字段。发布后清理明确旧代际失败只记录诊断,不将已提交写入报告为失败;并发或崩溃可能留下尚未确认可回收的内部记录,不做推测性删除。`.meta.json`、`.upload.tmp` 后缀和 `.hai-storage-meta` 在每个路径段均保留,不能直接读写或删除。代际记录是唯一元数据格式,缺少有效记录时明确返回错误;重建对象使用可信内容及业务元数据,通过 `file.put()` 写入。备份同时保存内容与内部记录,禁止绕过此格式直接混写。
159
+
160
+ S3 `file.copy()` 对源键按路径段编码,中文、空格及 URL 分隔字符仍作为原始对象键处理;调用方传原始 key,不要提前编码。
161
+
115
162
  `file.deleteMany(keys)` 返回 `StorageDeleteResult`:成功时 `data` 为 `{ deletedKeys, failures: [] }`;任一删除项未确认完成时返回 `OPERATION_FAILED`,进度在 `error.ext?.deleteMany`(初始化等前置错误可能不带进度)。重复输入只处理一次,输出 key 不含配置 prefix。S3 每批最多 1000 项,逐项检查 `Deleted` 与 `Errors`;HTTP 200 不代表全部成功,遗漏/重复/未知回执标为 `unknown`。本地删除最多并发 32 项。
116
163
 
117
164
  只对明确的 `InternalError / ServiceUnavailable / SlowDown / RequestTimeout` 项最多尝试三次;成功及权限失败项不重发。`DeleteObjects` 专用客户端禁用 SDK 请求重试;网络中断立即标为 `unknown`,未发送的后续批次标为 `notAttempted`,不盲目重放未知结果。`retryable` 只表示故障类别,调用方重试前仍须核对未知结果及并发写入。`dir.delete()` 传播删除失败;它不能阻止并发新增对象。S3 使用不带 VersionId 的删除,开启版本控制的桶可能创建删除标记,不保证清除历史版本。
@@ -146,7 +193,65 @@ if (!result.success) {
146
193
  - `getMimeType(extension)`
147
194
  - `formatFileSize(bytes)`
148
195
 
149
- ### 配置
196
+ ### 生产共享状态
197
+
198
+ 生产使用共享 S3 或兼容服务;local 只用于开发/明确测试。对象业务授权和读改写的条件提交由应用负责。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md).
199
+
200
+ ### 输入约束与 HTTP 能力
201
+
202
+ 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](README.md#存储输入约束与跨端能力).
203
+
204
+ ### 浏览器上传取消
205
+
206
+ `uploadWithPresignedUrl(url, data, { onProgress, abortController })` 使用原生 XHR 报告进度;不传 `onProgress` 时使用 fetch。预先取消不发送请求,准备阶段和进行中取消、外部 XHR abort、网络/超时及进度回调异常都只结算一次失败,并移除本次监听。完成后再取消不会操作旧 XHR。取消只终止本地请求,不能回滚远端已写入的对象;应用须先核对对象状态,再授权删除。
207
+
208
+ ### 存储输入约束与跨端能力
209
+
210
+ `@h-ai/storage/schemas` 是仅依赖 Zod 的浏览器安全入口;`api-contract` 与 `serv` 复用同一规则。Schema 只校验输入,服务端仍先认证、再按可信 session 授权每个资源,Local Provider 仍执行文件系统防逃逸。调用方不能把前缀校验或分页游标当成授权。
211
+
212
+ #### 输入边界
213
+
214
+ - `StorageKeySchema`:非空、不是纯空白,UTF-8 最多 1024 **字节**;不允许开头 `/`、反斜线、控制字符,以及独立的 `.` / `..` 路径段。不截断、不归一化 key。
215
+ - `StoragePrefixSchema`:同样的路径和字节限制,另允许空字符串;允许部分文件名,`report` 不等于目录 `report/`。
216
+ - `StorageDeleteKeysSchema`:每个 HTTP 请求 1–1000 个键,按原始数组数量计数,重复项不能绕过上限。Node `file.deleteMany` 允许更大数组,S3 每批最多 1000、Local 并发最多 32;失败时检查 `error.ext.deleteMany` 的已完成/失败/未知/未尝试进度,不能盲目重放整个数组。
217
+ - 列表 `maxKeys`:整数 1–1000,`files` 与 `commonPrefixes` 共用预算。续页使用响应原样返回的 token,并保持相同 Provider/根目录/prefix/delimiter;不保证并发写入期间的快照。
218
+ - 签名有效期:整数 1–604800 秒,Node 默认 3600 秒,默认 HTTP procedures 使用 900 秒。HTTP 上传签名只接受 `key`、`contentType`、`expiresIn`;下载签名只接受 `key`、`expiresIn`。未知字段直接拒绝,包括未实现的 `contentLength` / `maxSize`,不静默丢弃或假装限制已生效。
219
+ - 输入校验在 HTTP 层返回 400;`apiClient` 返回 `HaiApiClientError.VALIDATION_FAILED`,`error.cause.status` 保留 400。合法输入的业务失败仍按 `HaiResult.success` 判断。
220
+
221
+ ```ts
222
+ import { StorageDeleteFilesInputSchema } from '@h-ai/api-contract/storage'
223
+ import { StorageKeySchema } from '@h-ai/storage/schemas'
224
+
225
+ const key = StorageKeySchema.safeParse('users/u1/report.pdf')
226
+ if (key.success) {
227
+ const input = StorageDeleteFilesInputSchema.parse({
228
+ keys: [key.data]
229
+ })
230
+ // 把 input 交给已初始化的 client.storage.files.deleteMany;仍须检查返回值 success。
231
+ }
232
+ ```
233
+
234
+ #### 能力映射
235
+
236
+ | Node storage 能力 | api-contract / serv 默认端点 | apiClient / 浏览器消费 | 边界 |
237
+ | -------------------------------------------------- | --------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
238
+ | `presign.getUrl` | `POST /storage/presigned-urls/download` | `storage.presignedUrls.createDownload` | 仅 S3;Local 返回 `hai:storage:018`,不返回虚拟 URL |
239
+ | `presign.putUrl` | `POST /storage/presigned-urls/upload` | `storage.presignedUrls.createUpload` 后直接 PUT 到 S3 | 仅内容类型与有效期;不是文件代理上传,也不提供上传大小/内容校验保证 |
240
+ | `dir.list` | `GET /storage/files` | `storage.files.list` | 共享前缀约束与分页预算;逐次授权 prefix |
241
+ | `file.head` | `POST /storage/files/metadata` | `storage.files.getMetadata` | 返回元数据,不能把 ETag 当内容 checksum |
242
+ | `file.delete` | `DELETE /storage/files` | `storage.files.delete` | 逐资源授权 |
243
+ | `file.deleteMany` | `POST /storage/files/delete-many` | `storage.files.deleteMany` | HTTP 总量上限 1000,返回可确认的删除进度 |
244
+ | `file.put/get/getStream/copy/exists`、`dir.delete` | 无默认端点 | 应用自行提供有界且鉴权的服务端流程 | 流、Range、AbortSignal、metadata、cacheControl、条件写等 Node 选项不能直接作为预签名 API 入参 |
245
+ | `checksum.generate/verify` | 无默认端点 | 应用选择明确的浏览器实现或服务端校验流程 | Node 无状态工具;不是上传接口自动校验 |
246
+ | `presign.publicUrl` | 无默认端点 | 应用按公开资源策略使用 | 仅配置公开 S3 地址时有 URL;不是鉴权链接,Local 为 null |
247
+
248
+ `FileUpload` / `ImageUpload` / `AvatarUpload` 的 `uploadHandler` 由应用实现。组件的 `maxSize`、`accept` 只负责前端选择体验,不能约束拿到签名后绕过组件的上传;服务端应在对象落库/回调或独立代理上传处验证实际大小、类型、内容和归属,再发布业务记录。不要把 Node `UploadOptions` 当成签名选项,也不要给组件编造不存在的框架上传端点。
249
+
250
+ ## 配置
251
+
252
+ 生命周期:init 输入后端/业务配置,成功后才能使用;close 在释放借用依赖前等待完成。返回值以快速开始示例和公开签名为准。
253
+
254
+ Local 后端拒绝 key 路径中的文件符号链接和目录 junction,保留后缀 `.meta.json` / `.upload.tmp` 的保护不区分大小写。覆盖 put 会替换旧元数据,copy 默认继承源元数据;浏览器根入口仅提供 Schema、类型和客户端工具,不包含服务端 storage 单例。
150
255
 
151
256
  - **S3**:`bucket / region / accessKeyId / secretAccessKey` 必填,可选 `createBucket / endpoint / publicEndpoint / forcePathStyle / prefix / publicUrl`
152
257
  - **Local**:`root` 必填,可选 `directoryMode / fileMode`
@@ -157,6 +262,10 @@ MinIO、RustFS 共用 S3 Provider,使用 `type: 's3'`,无需单独的存储
157
262
 
158
263
  YAML 显式引用 `createBucket: ${HAI_STORAGE_CREATEBUCKET:false}` 后可由环境变量控制;直接传入 `storage.init()` 时使用布尔值。
159
264
 
265
+ 在 `config/_storage.yml` 中设置 `prefix: my-app/uploads/`,或在注册了 `StorageConfigSchema` 的 Hai 应用中设置 `HAI_STORAGE_PREFIX=my-app/uploads/`。上传 `report.pdf` 时,桶内对象键为 `my-app/uploads/report.pdf`。初始化探测和默认列表均查询 `my-app/uploads/`,配置值可省略末尾的斜杠。空前缀使用桶根,修改前缀不会搬迁已有对象。
266
+
267
+ 要限制应用凭据的访问范围,将对象权限限定到 `arn:aws:s3:::my-bucket/my-app/uploads/*`,并对 `s3:ListBucket` 设置 `s3:prefix` 条件 `my-app/uploads/*`。使用预先创建的桶并保持 `createBucket: false`,避免建桶模式触发桶级 `HeadBucket` 检查。
268
+
160
269
  ```ts
161
270
  const initResult = await storage.init({
162
271
  type: 's3',
@@ -173,7 +282,7 @@ if (!initResult.success) {
173
282
  }
174
283
  ```
175
284
 
176
- #### 内部连接与浏览器直连
285
+ ### 内部连接与浏览器直连
177
286
 
178
287
  `endpoint` 用于服务端读写、建桶和连接检查;`publicEndpoint` 用于 `presign.getUrl/putUrl` 的外部地址,无默认值,省略时继续使用 endpoint。可填写 `https://app.example.com/delivery/files`;目录段支持字母、数字、下划线、点和连字符,拒绝 `.`/`..`、重复斜杠及编码的分隔符,不能包含桶名、凭据、查询或片段。`publicUrl` 只拼接公开资源地址,不会为私有对象授权。
179
288
 
@@ -213,7 +322,7 @@ await storage.init({
213
322
 
214
323
  `storage.presign.putUrl()` 当前仅支持 `contentType` 与 `expiresIn` 等签名参数,不提供 `maxSize` 约束。
215
324
 
216
- 本地 `storage.dir.list()` 将 `prefix` 作为 key 字符串前缀(如 `reports/2026-`),按 UTF-8 字节序分页。`maxKeys` 为 1–1000 的整数,默认 1000,`files` 与 `commonPrefixes` 共用该预算。`delimiter` 将前缀之后首次出现的分隔符及其后内容折叠为一个 `commonPrefixes` 条目;空字符串不折叠,空物理目录不生成条目。这些分组与计数规则参照 [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)。
325
+ 本地 `storage.dir.list()` 将 `prefix` 作为 key 字符串前缀(如 `reports/2026-`),按 UTF-8 字节序分页。`maxKeys` 为 1–1000 的整数,默认 1000,`files` 与 `commonPrefixes` 共用该预算。`delimiter` 将前缀之后首次出现的分隔符及其后内容折叠为一个 `commonPrefixes` 条目;空字符串不折叠,空物理目录不生成条目。这些分组与计数规则参照 [S3 ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).
217
326
 
218
327
  仅当还有条目时返回 `isTruncated: true` 与 `nextContinuationToken`。本地游标无需进程内缓存,绑定规范根目录、`prefix` 和 `delimiter`;相同根目录的独立实例或重启后可续页,也可调整下一页的 `maxKeys`。跨范围、格式非法或版本不支持的游标返回 `INVALID_LIST_OPTIONS`。游标不是授权凭据或目录快照:调用方仍须授权前缀,并发增删可能改变后续页;不支持将不同挂载路径的根目录视为同一范围。本地每次请求仍扫描、排序匹配目录,大目录应缩小前缀或使用 S3。
219
328
 
@@ -228,7 +337,13 @@ do {
228
337
  } while (continuationToken)
229
338
  ```
230
339
 
231
- ### 错误处理
340
+ 配置由宿主加载和校验,再传入本模块;私密值放服务端私有环境,不传到浏览器。按实际使用场景逐项选择后端、容量、期限和日志策略,修改后重新初始化并核验成功结果。
341
+
342
+ [逐项配置说明、适用场景和示例](../../docs/usage/configuration/data.md).
343
+
344
+ ## 错误处理
345
+
346
+ 领域操作先判断 `result.success`,失败按本模块错误定义与安全提示处理,服务端保留错误码、关联编号和脱敏原因。同步工具、工厂、流迭代及生命周期按各自签名处理,不把 Promise 完成视为业务成功。具体失败与恢复边界见对应功能。
232
347
 
233
348
  ```ts
234
349
  import { HaiStorageError, storage } from '@h-ai/storage'
@@ -253,58 +368,6 @@ if (!result.success && result.error.code === HaiStorageError.NOT_INITIALIZED.cod
253
368
  | `HaiStorageError.INVALID_LIST_OPTIONS` | `hai:storage:019` | 本地列表参数或游标无效、游标查询范围不匹配 |
254
369
  | `HaiStorageError.CONFIG_ERROR` | `hai:storage:012` | 配置错误 |
255
370
 
256
- ## 生产共享状态
257
-
258
- 生产使用共享 S3 或兼容服务;local 只用于开发/明确测试。对象业务授权和读改写的条件提交由应用负责。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化发布成功前校验;仅 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试可豁免。 [状态、并发和恢复边界](../../.agents/instructions/distributed-state.md)。
259
-
260
- ## 输入约束与 HTTP 能力
261
-
262
- 存储 key 按 UTF-8 最多 1024 字节,并拒绝绝对路径、反斜线、控制字符及点路径段;公开 HTTP 批量删除每次 1–1000 项。`@h-ai/storage/schemas` 提供浏览器安全的共同规则。预签名有效期为整数 1–604800 秒(Node 默认 3600,HTTP 默认 900);HTTP 上传仅接受 key/contentType/expiresIn,不支持 contentLength/maxSize/metadata/checksum。无效输入返回 HTTP 400,typed client 映射 VALIDATION_FAILED。FileUpload 的大小/类型限制仅为前端体验,实际对象须由应用服务端复核。Node 流式读写/checksum/条件写等没有默认 HTTP 端点。完整映射与授权边界见 [存储 API 能力](README.md#存储输入约束与跨端能力)。
263
-
264
- ## 浏览器上传取消
265
-
266
- `uploadWithPresignedUrl(url, data, { onProgress, abortController })` 使用原生 XHR 报告进度;不传 `onProgress` 时使用 fetch。预先取消不发送请求,准备阶段和进行中取消、外部 XHR abort、网络/超时及进度回调异常都只结算一次失败,并移除本次监听。完成后再取消不会操作旧 XHR。取消只终止本地请求,不能回滚远端已写入的对象;应用须先核对对象状态,再授权删除。
267
-
268
- ## 存储输入约束与跨端能力
269
-
270
- `@h-ai/storage/schemas` 是仅依赖 Zod 的浏览器安全入口;`api-contract` 与 `serv` 复用同一规则。Schema 只校验输入,服务端仍先认证、再按可信 session 授权每个资源,Local Provider 仍执行文件系统防逃逸。调用方不能把前缀校验或分页游标当成授权。
271
-
272
- ### 输入边界
273
-
274
- - `StorageKeySchema`:非空、不是纯空白,UTF-8 最多 1024 **字节**;不允许开头 `/`、反斜线、控制字符,以及独立的 `.` / `..` 路径段。不截断、不归一化 key。
275
- - `StoragePrefixSchema`:同样的路径和字节限制,另允许空字符串;允许部分文件名,`report` 不等于目录 `report/`。
276
- - `StorageDeleteKeysSchema`:每个 HTTP 请求 1–1000 个键,按原始数组数量计数,重复项不能绕过上限。Node `file.deleteMany` 允许更大数组,S3 每批最多 1000、Local 并发最多 32;失败时检查 `error.ext.deleteMany` 的已完成/失败/未知/未尝试进度,不能盲目重放整个数组。
277
- - 列表 `maxKeys`:整数 1–1000,`files` 与 `commonPrefixes` 共用预算。续页使用响应原样返回的 token,并保持相同 Provider/根目录/prefix/delimiter;不保证并发写入期间的快照。
278
- - 签名有效期:整数 1–604800 秒,Node 默认 3600 秒,默认 HTTP procedures 使用 900 秒。HTTP 上传签名只接受 `key`、`contentType`、`expiresIn`;下载签名只接受 `key`、`expiresIn`。未知字段直接拒绝,包括未实现的 `contentLength` / `maxSize`,不静默丢弃或假装限制已生效。
279
- - 输入校验在 HTTP 层返回 400;`apiClient` 返回 `HaiApiClientError.VALIDATION_FAILED`,`error.cause.status` 保留 400。合法输入的业务失败仍按 `HaiResult.success` 判断。
280
-
281
- ```ts
282
- import { StorageDeleteFilesInputSchema } from '@h-ai/api-contract/storage'
283
- import { StorageKeySchema } from '@h-ai/storage/schemas'
284
-
285
- const key = StorageKeySchema.safeParse('users/u1/report.pdf')
286
- if (key.success) {
287
- const input = StorageDeleteFilesInputSchema.parse({ keys: [key.data] })
288
- // 把 input 交给已初始化的 client.storage.files.deleteMany;仍须检查返回值 success。
289
- }
290
- ```
291
-
292
- ### 能力映射
293
-
294
- | Node storage 能力 | api-contract / serv 默认端点 | apiClient / 浏览器消费 | 边界 |
295
- | -------------------------------------------------- | --------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
296
- | `presign.getUrl` | `POST /storage/presigned-urls/download` | `storage.presignedUrls.createDownload` | 仅 S3;Local 返回 `hai:storage:018`,不返回虚拟 URL |
297
- | `presign.putUrl` | `POST /storage/presigned-urls/upload` | `storage.presignedUrls.createUpload` 后直接 PUT 到 S3 | 仅内容类型与有效期;不是文件代理上传,也不提供上传大小/内容校验保证 |
298
- | `dir.list` | `GET /storage/files` | `storage.files.list` | 共享前缀约束与分页预算;逐次授权 prefix |
299
- | `file.head` | `POST /storage/files/metadata` | `storage.files.getMetadata` | 返回元数据,不能把 ETag 当内容 checksum |
300
- | `file.delete` | `DELETE /storage/files` | `storage.files.delete` | 逐资源授权 |
301
- | `file.deleteMany` | `POST /storage/files/delete-many` | `storage.files.deleteMany` | HTTP 总量上限 1000,返回可确认的删除进度 |
302
- | `file.put/get/getStream/copy/exists`、`dir.delete` | 无默认端点 | 应用自行提供有界且鉴权的服务端流程 | 流、Range、AbortSignal、metadata、cacheControl、条件写等 Node 选项不能直接作为预签名 API 入参 |
303
- | `checksum.generate/verify` | 无默认端点 | 应用选择明确的浏览器实现或服务端校验流程 | Node 无状态工具;不是上传接口自动校验 |
304
- | `presign.publicUrl` | 无默认端点 | 应用按公开资源策略使用 | 仅配置公开 S3 地址时有 URL;不是鉴权链接,Local 为 null |
305
-
306
- `FileUpload` / `ImageUpload` / `AvatarUpload` 的 `uploadHandler` 由应用实现。组件的 `maxSize`、`accept` 只负责前端选择体验,不能约束拿到签名后绕过组件的上传;服务端应在对象落库/回调或独立代理上传处验证实际大小、类型、内容和归属,再发布业务记录。不要把 Node `UploadOptions` 当成签名选项,也不要给组件编造不存在的框架上传端点。
307
-
308
371
  ## 开发测试
309
372
 
310
373
  ```bash
package/dist/browser.d.ts CHANGED
@@ -892,4 +892,5 @@ interface StorageFunctions {
892
892
  readonly presign: PresignOperations;
893
893
  }
894
894
 
895
- export { type ChecksumAlgorithm, type ChecksumOperations, type ChecksumOptions, type CopyOptions, type DirOperations, type DownloadOptions, type FileMetadata, type FileOperations, HaiStorageError, type ListOptions, type ListResult, type LocalConfig, LocalConfigSchema, type PresignOperations, type PresignOptions, PresignOptionsSchema, type PresignUploadOptions, PresignUploadOptionsSchema, type S3Config, S3ConfigSchema, type StorageConfig, type StorageConfigInput, StorageConfigSchema, type StorageData, type StorageDeleteFailure, type StorageDeleteResult, type StorageDeleteSummary, type StorageFunctions, type StorageOperationLogConfig, StorageOperationLogConfigSchema, type StorageType, StorageTypeSchema, type UploadOptions };
895
+ export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema };
896
+ export type { ChecksumAlgorithm, ChecksumOperations, ChecksumOptions, CopyOptions, DirOperations, DownloadOptions, FileMetadata, FileOperations, ListOptions, ListResult, LocalConfig, PresignOperations, PresignOptions, PresignUploadOptions, S3Config, StorageConfig, StorageConfigInput, StorageData, StorageDeleteFailure, StorageDeleteResult, StorageDeleteSummary, StorageFunctions, StorageOperationLogConfig, StorageType, UploadOptions };
package/dist/browser.js CHANGED
@@ -1,5 +1,5 @@
1
- export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-ANAESRWS.js';
2
- export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-TLZY4IP6.js';
3
- export { STORAGE_DELETE_MAX_KEYS, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema, StorageKeySchema, StoragePrefixSchema, StoragePresignExpiresInSchema, getStorageKeyIssue } from './chunk-P65WYXJG.js';
1
+ export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema } from './chunk-FK7KRCML.js';
2
+ export { downloadAndSave, downloadWithPresignedUrl, formatFileSize, getFileExtension, getMimeType, uploadWithPresignedUrl } from './chunk-JAJYHGLI.js';
3
+ export { STORAGE_DELETE_MAX_KEYS, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema, StorageKeySchema, StoragePrefixSchema, StoragePresignExpiresInSchema, getStorageKeyIssue } from './chunk-6YG3N3CW.js';
4
4
  //# sourceMappingURL=browser.js.map
5
5
  //# sourceMappingURL=browser.js.map
@@ -19,8 +19,12 @@ function getStorageKeyIssue(key, allowEmpty = false) {
19
19
  function keySchema(allowEmpty) {
20
20
  return z.string().superRefine((key, context) => {
21
21
  const issue = getStorageKeyIssue(key, allowEmpty);
22
- if (issue)
23
- context.addIssue({ code: "custom", params: { storageKeyIssue: issue } });
22
+ if (issue) {
23
+ context.addIssue({
24
+ code: "custom",
25
+ params: { storageKeyIssue: issue }
26
+ });
27
+ }
24
28
  });
25
29
  }
26
30
  var StorageKeySchema = keySchema(false);
@@ -29,5 +33,5 @@ var StorageDeleteKeysSchema = z.array(StorageKeySchema).min(1).max(STORAGE_DELET
29
33
  var StoragePresignExpiresInSchema = z.number().int().min(1).max(604800);
30
34
 
31
35
  export { STORAGE_DELETE_MAX_KEYS, STORAGE_KEY_MAX_BYTES, StorageDeleteKeysSchema, StorageKeySchema, StoragePrefixSchema, StoragePresignExpiresInSchema, getStorageKeyIssue };
32
- //# sourceMappingURL=chunk-P65WYXJG.js.map
33
- //# sourceMappingURL=chunk-P65WYXJG.js.map
36
+ //# sourceMappingURL=chunk-6YG3N3CW.js.map
37
+ //# sourceMappingURL=chunk-6YG3N3CW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage-schemas.ts"],"names":[],"mappings":";;;AAgBO,IAAM,qBAAA,GAAwB;AAa9B,IAAM,uBAAA,GAA0B;AA6BhC,SAAS,kBAAA,CAAmB,GAAA,EAAa,UAAA,GAAa,KAAA,EAAoC;AAC/F,EAAA,IAAI,cAAc,GAAA,KAAQ,EAAA;AACxB,IAAA,OAAO,MAAA;AACT,EAAA,IAAI,GAAA,CAAI,MAAK,KAAM,EAAA;AACjB,IAAA,OAAO,OAAA;AACT,EAAA,IAAI,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,GAAG,EAAE,UAAA,GAAa,qBAAA;AAC7C,IAAA,OAAO,SAAA;AACT,EAAA,IAAI,GAAA,CAAI,WAAW,GAAG,CAAA,IAAK,IAAI,QAAA,CAAS,IAAI,CAAA,IAAK,CAAC,GAAG,GAAG,EAAE,IAAA,CAAK,CAAA,IAAA,KAAQ,IAAA,CAAK,UAAA,CAAW,CAAC,CAAA,GAAI,MAAM,IAAA,CAAK,UAAA,CAAW,CAAC,CAAA,KAAM,GAAG,CAAA;AAC1H,IAAA,OAAO,YAAA;AACT,EAAA,IAAI,GAAA,CAAI,MAAM,GAAG,CAAA,CAAE,KAAK,CAAA,OAAA,KAAW,OAAA,KAAY,GAAA,IAAO,OAAA,KAAY,IAAI,CAAA;AACpE,IAAA,OAAO,UAAA;AACT,EAAA,OAAO,MAAA;AACT;AAGA,SAAS,UAAU,UAAA,EAAqB;AACtC,EAAA,OAAO,EAAE,MAAA,EAAO,CAAE,WAAA,CAAY,CAAC,KAAK,OAAA,KAAY;AAC9C,IAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,GAAA,EAAK,UAAU,CAAA;AAChD,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,OAAA,CAAQ,QAAA,CAAS;AAAA,QACf,IAAA,EAAM,QAAA;AAAA,QACN,MAAA,EAAQ,EAAE,eAAA,EAAiB,KAAA;AAAM,OAClC,CAAA;AAAA,IACH;AAAA,EACF,CAAC,CAAA;AACH;AAYO,IAAM,gBAAA,GAAmB,UAAU,KAAK;AAYxC,IAAM,mBAAA,GAAsB,UAAU,IAAI;AAY1C,IAAM,uBAAA,GAA0B,EAAE,KAAA,CAAM,gBAAgB,EAAE,GAAA,CAAI,CAAC,CAAA,CAAE,GAAA,CAAI,uBAAuB;AAY5F,IAAM,6BAAA,GAAgC,CAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,GAAA,CAAI,CAAC,CAAA,CAAE,GAAA,CAAI,MAAM","file":"chunk-6YG3N3CW.js","sourcesContent":["/**\n * @h-ai/storage — 跨端输入约束;仅依赖 Zod,不加载 Provider、配置或 Node 模块。\n * @module storage-schemas\n */\nimport { z } from 'zod'\n\n/**\n * 对象键的 UTF-8 字节上限;不是 JavaScript 字符串长度。\n * @example\n * ```ts\n * import { STORAGE_KEY_MAX_BYTES } from '@h-ai/storage/schemas'\n *\n * const fits = new TextEncoder().encode('uploads/报告.pdf').length <= STORAGE_KEY_MAX_BYTES\n * void fits\n * ```\n */\nexport const STORAGE_KEY_MAX_BYTES = 1024\n\n/**\n * 单次公开 HTTP 删除请求的原始键数量上限;Node deleteMany 仍按 Provider 上限分批。\n * @example\n * ```ts\n * import { STORAGE_DELETE_MAX_KEYS } from '@h-ai/storage/schemas'\n *\n * const keys = ['a.txt', 'b.txt']\n * const page = keys.slice(0, STORAGE_DELETE_MAX_KEYS)\n * void page\n * ```\n */\nexport const STORAGE_DELETE_MAX_KEYS = 1000\n\n/**\n * 键校验失败类别;调用层据此生成本地化消息,不解析错误文本。\n * @example\n * ```ts\n * import type { StorageKeyIssue } from '@h-ai/storage'\n *\n * const value: StorageKeyIssue = 'tooLong'\n * void value\n * ```\n */\nexport type StorageKeyIssue = 'empty' | 'tooLong' | 'characters' | 'segments'\n\n/**\n * 检查跨端键边界,不归一化输入,不替代文件系统防逃逸或资源授权。\n *\n * @param key - 调用方视角的对象键,不含 Provider 配置前缀。\n * @param allowEmpty - 是否允许空字符串(用于列表前缀);纯空白仍无效。\n * @returns 首个失败类别;合法时为 undefined。\n * @example\n * ```ts\n * import { getStorageKeyIssue } from '@h-ai/storage/schemas'\n *\n * const unsafe = getStorageKeyIssue('../private.txt') // segments\n * const emptyPrefix = getStorageKeyIssue('', true) // undefined\n * void [unsafe, emptyPrefix]\n * ```\n */\nexport function getStorageKeyIssue(key: string, allowEmpty = false): StorageKeyIssue | undefined {\n if (allowEmpty && key === '')\n return undefined\n if (key.trim() === '')\n return 'empty'\n if (new TextEncoder().encode(key).byteLength > STORAGE_KEY_MAX_BYTES)\n return 'tooLong'\n if (key.startsWith('/') || key.includes('\\\\') || [...key].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))\n return 'characters'\n if (key.split('/').some(segment => segment === '.' || segment === '..'))\n return 'segments'\n return undefined\n}\n\n/** 通过稳定 params 保留失败类别;默认 Zod 消息交由请求层按 locale 映射。 */\nfunction keySchema(allowEmpty: boolean) {\n return z.string().superRefine((key, context) => {\n const issue = getStorageKeyIssue(key, allowEmpty)\n if (issue) {\n context.addIssue({\n code: 'custom',\n params: { storageKeyIssue: issue },\n })\n }\n })\n}\n\n/**\n * 文件键 Schema:非空、UTF-8 最多 1024 字节,无绝对路径、反斜线、控制符或点路径段。\n * @example\n * ```ts\n * import { StorageKeySchema } from '@h-ai/storage'\n *\n * const value = StorageKeySchema.parse('users/u1/report.pdf')\n * void value\n * ```\n */\nexport const StorageKeySchema = keySchema(false)\n\n/**\n * 列表前缀 Schema;允许空字符串与部分文件名,不把前缀当物理目录。\n * @example\n * ```ts\n * import { StoragePrefixSchema } from '@h-ai/storage'\n *\n * const value = StoragePrefixSchema.parse('users/u1/rep')\n * void value\n * ```\n */\nexport const StoragePrefixSchema = keySchema(true)\n\n/**\n * HTTP 批量删除键 Schema;按输入数量计数,重复键不能绕过上限。\n * @example\n * ```ts\n * import { StorageDeleteKeysSchema } from '@h-ai/storage'\n *\n * const value = StorageDeleteKeysSchema.parse(['users/u1/a.txt', 'users/u1/b.txt'])\n * void value\n * ```\n */\nexport const StorageDeleteKeysSchema = z.array(StorageKeySchema).min(1).max(STORAGE_DELETE_MAX_KEYS)\n\n/**\n * 预签名有效期,单位秒;整数 1–604800,不在此处填充 Node/HTTP 各自默认值。\n * @example\n * ```ts\n * import { StoragePresignExpiresInSchema } from '@h-ai/storage'\n *\n * const value = StoragePresignExpiresInSchema.parse(900)\n * void value\n * ```\n */\nexport const StoragePresignExpiresInSchema = z.number().int().min(1).max(604800)\n"]}
@@ -1,5 +1,5 @@
1
- import { storageM } from './chunk-TLZY4IP6.js';
2
- import { StoragePresignExpiresInSchema } from './chunk-P65WYXJG.js';
1
+ import { storageM } from './chunk-JAJYHGLI.js';
2
+ import { StoragePresignExpiresInSchema } from './chunk-6YG3N3CW.js';
3
3
  import { z } from 'zod';
4
4
  import { core } from '@h-ai/core';
5
5
 
@@ -41,7 +41,7 @@ var S3ConfigSchema = z.object({
41
41
  }, storageM("storage_config_publicEndpointInvalid")).optional(),
42
42
  /** 强制使用路径风格 URL(某些 S3 兼容服务需要) */
43
43
  forcePathStyle: z.boolean().default(false),
44
- /** 路径前缀(所有操作都会加上此前缀) */
44
+ /** S3 桶内对象键前缀,例如 my-app/uploads/,留空使用桶根 */
45
45
  prefix: z.string().default(""),
46
46
  /** 公开访问基础 URL(用于生成公开 URL) */
47
47
  publicUrl: z.string().url().optional(),
@@ -99,5 +99,5 @@ var StorageErrorInfo = {
99
99
  var HaiStorageError = core.error.buildHaiErrorsDef("storage", StorageErrorInfo);
100
100
 
101
101
  export { HaiStorageError, LocalConfigSchema, PresignOptionsSchema, PresignUploadOptionsSchema, S3ConfigSchema, StorageConfigSchema, StorageOperationLogConfigSchema, StorageTypeSchema };
102
- //# sourceMappingURL=chunk-ANAESRWS.js.map
103
- //# sourceMappingURL=chunk-ANAESRWS.js.map
102
+ //# sourceMappingURL=chunk-FK7KRCML.js.map
103
+ //# sourceMappingURL=chunk-FK7KRCML.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage-config.ts","../src/storage-types.ts"],"names":[],"mappings":"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","file":"chunk-FK7KRCML.js","sourcesContent":["/**\n * @h-ai/storage — 存储配置 Schema\n * @module storage-config\n */\n\nimport { z } from 'zod'\nimport { storageM } from './storage-i18n.js'\nimport { StoragePresignExpiresInSchema } from './storage-schemas.js'\n\n// ─── 配置 Schema ───\n\n/**\n * 存储类型枚举\n *\n * 支持的存储类型:\n * - `s3` - S3 协议对象存储(兼容 AWS S3、MinIO、RustFS、阿里云 OSS 等)\n * - `local` - 本地文件存储(仅后端)\n * @example\n * ```ts\n * import { StorageTypeSchema } from '@h-ai/storage'\n *\n * const value = StorageTypeSchema.parse('local')\n * void value\n * ```\n */\nexport const StorageTypeSchema = z.enum(['s3', 'local'])\n\n/**\n * 存储类型\n * @example\n * ```ts\n * import type { StorageType } from '@h-ai/storage'\n *\n * const value: StorageType = 's3'\n * void value\n * ```\n */\nexport type StorageType = z.infer<typeof StorageTypeSchema>\n\n/**\n * StorageOperationLogConfigSchema 的数据约束与操作契约。\n * @example\n * ```ts\n * import { StorageOperationLogConfigSchema } from '@h-ai/storage'\n *\n * const value = StorageOperationLogConfigSchema.parse({ read: false, write: true, maxLength: 500, level: 'debug' })\n * void value\n * ```\n */\nexport const StorageOperationLogConfigSchema = z.object({\n read: z.boolean().default(false),\n write: z.boolean().default(false),\n maxLength: z.number().int().min(0).default(1000),\n level: z.enum(['info', 'debug', 'trace']).default('debug'),\n})\n\n/**\n * StorageOperationLogConfig 的数据约束与操作契约。\n * @example\n * ```ts\n * import { StorageOperationLogConfigSchema, type StorageOperationLogConfig } from '@h-ai/storage'\n *\n * const value: StorageOperationLogConfig = StorageOperationLogConfigSchema.parse({ read: false, write: true, maxLength: 500, level: 'debug' })\n * void value\n * ```\n */\nexport type StorageOperationLogConfig = z.infer<typeof StorageOperationLogConfigSchema>\n\nconst OperationLogConfigFieldSchema = {\n operationLog: StorageOperationLogConfigSchema.optional(),\n}\n\n/**\n * S3 存储配置 Schema\n *\n * 支持标准 AWS S3 以及兼容 S3 协议的存储服务(MinIO、RustFS、阿里云 OSS 等)。\n * 必填字段:bucket、region、accessKeyId、secretAccessKey。\n * @example\n * ```ts\n * import { S3ConfigSchema } from '@h-ai/storage'\n *\n * const value = S3ConfigSchema.parse({ type: 's3', bucket: 'uploads', region: 'us-east-1', accessKeyId: process.env.HAI_STORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.HAI_STORAGE_SECRET_ACCESS_KEY })\n * void value\n * ```\n */\nexport const S3ConfigSchema = z.object({\n /** 存储类型 */\n type: z.literal('s3'),\n\n /** 存储桶名称 */\n bucket: z.string().min(1, storageM('storage_config_bucketRequired')),\n\n /** 初始化时检查并创建缺失的桶;默认关闭,启用后凭据须有建桶权限。 */\n createBucket: z.boolean().default(false),\n\n /** 区域 */\n region: z.string().min(1, storageM('storage_config_regionRequired')),\n\n /** Access Key ID */\n accessKeyId: z.string().min(1, storageM('storage_config_accessKeyIdRequired')),\n\n /** Secret Access Key */\n secretAccessKey: z.string().min(1, storageM('storage_config_secretAccessKeyRequired')),\n\n /** 自定义端点(用于 MinIO、RustFS、阿里云 OSS 等) */\n endpoint: z.string().url().optional(),\n\n /** 浏览器预签名端点,可含代理目录(如 /delivery/files),不含桶名、凭据或查询;代理须去除目录并保留 Host。 */\n publicEndpoint: z.string().refine((value) => {\n // 必须先检查原始文本;URL 校验会移除制表符和换行,不能在标准化后再检查。\n // eslint-disable-next-line no-control-regex -- 在入口显式拒绝控制字符,避免校验与代理解释不一致。\n return !/[\\u0000-\\u0020\\u007F\\\\%]/.test(value) && !/(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(value)\n }, storageM('storage_config_publicEndpointInvalid')).url().refine((value) => {\n try {\n const url = new URL(value)\n return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password\n && /^\\/(?:[\\w.-]+(?:\\/[\\w.-]+)*\\/?)?$/.test(url.pathname) && !url.search && !url.hash\n }\n catch {\n return false\n }\n }, storageM('storage_config_publicEndpointInvalid')).optional(),\n\n /** 强制使用路径风格 URL(某些 S3 兼容服务需要) */\n forcePathStyle: z.boolean().default(false),\n\n /** S3 桶内对象键前缀,例如 my-app/uploads/,留空使用桶根 */\n prefix: z.string().default(''),\n\n /** 公开访问基础 URL(用于生成公开 URL) */\n publicUrl: z.string().url().optional(),\n\n ...OperationLogConfigFieldSchema,\n})\n\n/**\n * S3 配置类型(parse 后,带默认值的字段已填充,optional 字段仍可缺省)\n * @example\n * ```ts\n * import { S3ConfigSchema, type S3Config } from '@h-ai/storage'\n *\n * const value: S3Config = S3ConfigSchema.parse({ type: 's3', bucket: 'uploads', region: 'us-east-1', accessKeyId: process.env.HAI_STORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.HAI_STORAGE_SECRET_ACCESS_KEY })\n * void value\n * ```\n */\nexport type S3Config = z.infer<typeof S3ConfigSchema>\n\n/**\n * 本地存储配置 Schema\n *\n * 基于本地文件系统,仅后端可用。必填字段:root。\n * @example\n * ```ts\n * import { LocalConfigSchema } from '@h-ai/storage'\n *\n * const value = LocalConfigSchema.parse({ type: 'local', root: './data/uploads' })\n * void value\n * ```\n */\nexport const LocalConfigSchema = z.object({\n /** 存储类型 */\n type: z.literal('local'),\n\n /** 根目录路径 */\n root: z.string().min(1, storageM('storage_config_rootRequired')),\n\n /** 目录创建权限 */\n directoryMode: z.number().default(0o755),\n\n /** 文件创建权限 */\n fileMode: z.number().default(0o644),\n\n ...OperationLogConfigFieldSchema,\n})\n\n/**\n * 本地配置类型(parse 后,带默认值的字段已填充,optional 字段仍可缺省)\n * @example\n * ```ts\n * import { LocalConfigSchema, type LocalConfig } from '@h-ai/storage'\n *\n * const value: LocalConfig = LocalConfigSchema.parse({ type: 'local', root: './data/uploads' })\n * void value\n * ```\n */\nexport type LocalConfig = z.infer<typeof LocalConfigSchema>\n\n/**\n * 统一存储配置 Schema\n *\n * 支持 S3 和本地文件两种类型的配置验证。\n * @example\n * ```ts\n * import { StorageConfigSchema } from '@h-ai/storage'\n *\n * const value = StorageConfigSchema.parse({ type: 'local', root: './data/uploads' })\n * void value\n * ```\n */\nexport const StorageConfigSchema = z.discriminatedUnion('type', [\n S3ConfigSchema,\n LocalConfigSchema,\n])\n\n/**\n * 存储配置类型(parse 后的完整配置)\n * @example\n * ```ts\n * import { StorageConfigSchema, type StorageConfig } from '@h-ai/storage'\n *\n * const value: StorageConfig = StorageConfigSchema.parse({ type: 'local', root: './data/uploads' })\n * void value\n * ```\n */\nexport type StorageConfig = z.infer<typeof StorageConfigSchema>\n\n/**\n * 存储配置输入类型(parse 前,允许省略带默认值的字段)\n * @example\n * ```ts\n * import type { StorageConfigInput } from '@h-ai/storage'\n *\n * const value: StorageConfigInput = { type: 'local', root: './data/uploads' }\n * void value\n * ```\n */\nexport type StorageConfigInput = z.input<typeof StorageConfigSchema>\n\n// ─── 签名 URL 配置 ───\n\n/**\n * 签名 URL 配置 Schema\n *\n * 用于控制 `storage.presign.getUrl()` 生成的下载签名 URL 参数。\n * @example\n * ```ts\n * import { PresignOptionsSchema } from '@h-ai/storage'\n *\n * const value = PresignOptionsSchema.parse({ expiresIn: 60 })\n * void value\n * ```\n */\nexport const PresignOptionsSchema = z.object({\n /** 过期时间(秒),范围 1~604800(7 天),默认 3600(1 小时) */\n expiresIn: StoragePresignExpiresInSchema.default(3600), // 最长 7 天\n\n /** 响应内容类型(用于下载时设置 Content-Type) */\n responseContentType: z.string().optional(),\n\n /** 响应内容处置(用于设置下载文件名) */\n responseContentDisposition: z.string().optional(),\n})\n\n/**\n * 签名 URL 输入选项,允许省略带默认值的字段。\n * @example\n * ```ts\n * import type { PresignOptions } from '@h-ai/storage'\n *\n * const value: PresignOptions = { expiresIn: 60, responseContentType: 'text/plain' }\n * void value\n * ```\n */\nexport type PresignOptions = z.input<typeof PresignOptionsSchema>\n\n/**\n * 上传签名 URL 配置 Schema\n *\n * 继承 PresignOptionsSchema 并扩展上传专属参数,用于 `storage.presign.putUrl()`。\n * @example\n * ```ts\n * import { PresignUploadOptionsSchema } from '@h-ai/storage'\n *\n * const value = PresignUploadOptionsSchema.parse({ contentType: 'text/plain' })\n * void value\n * ```\n */\nexport const PresignUploadOptionsSchema = PresignOptionsSchema.extend({\n /** 上传内容类型(必须与实际上传匹配) */\n contentType: z.string().default('application/octet-stream'),\n})\n\n/**\n * 上传签名 URL 输入选项,省略 contentType 时使用 application/octet-stream。\n * @example\n * ```ts\n * import type { PresignUploadOptions } from '@h-ai/storage'\n *\n * const value: PresignUploadOptions = { expiresIn: 60, contentType: 'text/plain' }\n * void value\n * ```\n */\nexport type PresignUploadOptions = z.input<typeof PresignUploadOptionsSchema>\n","/**\n * @h-ai/storage — 公共类型定义\n *\n * 包含:\n * - 错误定义(HaiStorageError)\n * - 文件元数据(FileMetadata)\n * - 列表结果(ListResult)\n * - 文件操作接口(FileOperations)\n * - 目录操作接口(DirOperations)\n * - 签名 URL 操作接口(PresignOperations)\n * - 函数接口(StorageFunctions)\n * @module storage-types\n */\n\nimport type { ErrorInfo, HaiError, HaiResult } from '@h-ai/core'\nimport type { Buffer } from 'node:buffer'\nimport type { PresignOptions, PresignUploadOptions, StorageConfig, StorageConfigInput } from './storage-config.js'\nimport { core } from '@h-ai/core'\n\n// ─── 错误定义(照 @h-ai/core 范式) ───\n\nconst StorageErrorInfo = {\n CONNECTION_FAILED: '001:500',\n OPERATION_FAILED: '002:500',\n NOT_FOUND: '003:404',\n ALREADY_EXISTS: '004:409',\n PERMISSION_DENIED: '005:403',\n QUOTA_EXCEEDED: '006:413',\n INVALID_PATH: '007:400',\n IO_ERROR: '008:500',\n NETWORK_ERROR: '009:502',\n NOT_INITIALIZED: '010:500',\n UNSUPPORTED_TYPE: '011:400',\n CONFIG_ERROR: '012:500',\n PRESIGN_FAILED: '013:500',\n UPLOAD_FAILED: '014:500',\n DOWNLOAD_FAILED: '015:500',\n CHECKSUM_FAILED: '016:500',\n INVALID_CHECKSUM: '017:400',\n UNSUPPORTED_OPERATION: '018:400',\n INVALID_LIST_OPTIONS: '019:400',\n} as const satisfies ErrorInfo\n\n/**\n * HaiStorageError 的数据约束与操作契约。\n * @example\n * ```ts\n * import { HaiStorageError } from '@h-ai/storage'\n * import { err } from '@h-ai/core'\n *\n * const result = err(HaiStorageError.NOT_FOUND, 'File missing')\n * if (!result.success) void result.error.code\n * ```\n */\nexport const HaiStorageError = core.error.buildHaiErrorsDef('storage', StorageErrorInfo)\n\n/**\n * 批量删除中未确认完成的一项;key 始终使用调用方视角,不含 Provider prefix。\n * @example\n * ```ts\n * import type { StorageDeleteFailure } from '@h-ai/storage'\n *\n * const value: StorageDeleteFailure = { key: 'private.txt', code: 'AccessDenied', retryable: false, outcome: 'failed' }\n * void value\n * ```\n */\nexport interface StorageDeleteFailure {\n /** 调用方提交的文件键。 */\n key: string\n /** 渠道/框架错误标识,不包含渠道原始 Message。 */\n code: string\n /** 是否属于可重试的瞬时故障;未知结果重试前仍需考虑并发上传/版本语义。 */\n retryable: boolean\n /** failed 为已知失败,unknown 为已发送但结果未知,notAttempted 为尚未发送。 */\n outcome: 'failed' | 'unknown' | 'notAttempted'\n}\n\n/**\n * 批量删除进度;任一失败不能宣称全部删除完成。\n * @example\n * ```ts\n * import type { StorageDeleteSummary } from '@h-ai/storage'\n *\n * const value: StorageDeleteSummary = { deletedKeys: ['a.txt'], failures: [] }\n * void value\n * ```\n */\nexport interface StorageDeleteSummary {\n /** 已确认删除或本来就不存在的唯一文件键。 */\n deletedKeys: string[]\n /** 失败、未知及尚未尝试的唯一文件键。 */\n failures: StorageDeleteFailure[]\n}\n\n/**\n * 批量删除结果;失败仍遵守 HaiResult,部分进度置于 error.ext.deleteMany。\n * @example\n * ```ts\n * import { storage, type StorageDeleteResult } from '@h-ai/storage'\n *\n * async function remove() {\n * const result: StorageDeleteResult = await storage.file.deleteMany(['a.txt', 'b.txt'])\n * return result.success ? result.data : result.error.ext?.deleteMany\n * }\n * ```\n */\nexport type StorageDeleteResult\n = | { success: true, data: StorageDeleteSummary }\n | { success: false, error: HaiError & { ext?: Record<string, unknown> & { deleteMany?: StorageDeleteSummary } } }\n\n// ─── 文件元数据 ───\n\n/**\n * 文件元数据接口\n *\n * 表示存储中单个文件的元信息,由 put/head/copy/list 等操作返回。\n * @example\n * ```ts\n * import type { FileMetadata } from '@h-ai/storage'\n *\n * const value: FileMetadata = { key: 'a.txt', size: 5, contentType: 'text/plain', lastModified: new Date() }\n * void value\n * ```\n */\nexport interface FileMetadata {\n /** 文件键/路径(不含配置 prefix,对用户透明) */\n key: string\n /** 文件大小(字节) */\n size: number\n /** 内容类型(MIME),未指定时根据扩展名自动推断 */\n contentType: string\n /** 最后修改时间 */\n lastModified: Date\n /** ETag(缓存验证标记;本地为 size-mtime,S3 由服务端生成,不等同于内容 checksum) */\n etag?: string\n /** 自定义元数据键值对 */\n metadata?: Record<string, string>\n}\n\n/**\n * 列表选项\n *\n * 用于控制 `storage.dir.list()` 的过滤、分页和目录模拟行为。\n * @example\n * ```ts\n * import type { ListOptions } from '@h-ai/storage'\n *\n * const value: ListOptions = { prefix: 'reports/', delimiter: '/', maxKeys: 100 }\n * void value\n * ```\n */\nexport interface ListOptions {\n /** 前缀过滤(只返回以此前缀开头的文件) */\n prefix?: string\n /**\n * 分页标记(从上次 ListResult.nextContinuationToken 获取)\n *\n * Local 游标绑定根目录、prefix 和 delimiter,可跨实例使用;不得跨查询或 provider 混用。\n */\n continuationToken?: string\n /** 每页逻辑条目上限(1–1000,默认 1000);files 和 commonPrefixes 合计计数。 */\n maxKeys?: number\n /** 分隔符,通常为 '/';从 prefix 之后首次出现的位置折叠 commonPrefixes,空字符串表示不折叠。 */\n delimiter?: string\n}\n\n/**\n * 列表结果\n *\n * 包含当前页的文件列表、目录前缀、分页信息。\n * @example\n * ```ts\n * import type { ListResult } from '@h-ai/storage'\n *\n * const value: ListResult = { files: [], commonPrefixes: ['reports/'], isTruncated: false }\n * void value\n * ```\n */\nexport interface ListResult {\n /** 当前页文件元数据列表 */\n files: FileMetadata[]\n /** 公共前缀:由实际匹配文件按 delimiter 折叠,不包含 Local 空物理目录。 */\n commonPrefixes: string[]\n /** 下一页标记;无后续页时为 undefined */\n nextContinuationToken?: string\n /** 是否被截断(true 表示还有更多数据,可用 nextContinuationToken 继续拉取) */\n isTruncated: boolean\n}\n\n// ─── 上传下载选项 ───\n\n/**\n * 上传选项\n *\n * 用于控制 `storage.file.put()` 的元数据与缓存行为。\n * @example\n * ```ts\n * import type { UploadOptions } from '@h-ai/storage'\n *\n * const value: UploadOptions = { ifNoneMatch: '*', contentType: 'text/plain', signal: new AbortController().signal }\n * void value\n * ```\n */\nexport interface UploadOptions {\n /** 仅当 key 不存在时原子写入;S3 使用标准 If-None-Match,后端不支持时返回错误。 */\n ifNoneMatch?: '*'\n /** 取消上传;失败或取消时清理未完成的临时文件/分片。 */\n signal?: AbortSignal\n /** 内容类型(不指定时根据文件扩展名自动推断) */\n contentType?: string\n /** 自定义元数据键值对(存储后可通过 head 获取) */\n metadata?: Record<string, string>\n /** Cache-Control 响应头(例如 'max-age=3600') */\n cacheControl?: string\n /** Content-Disposition 响应头(例如 'attachment; filename=\"a.txt\"') */\n contentDisposition?: string\n}\n\n/**\n * 下载选项\n *\n * 用于控制 `storage.file.get()` 的范围读取。\n * 两个字段均为字节偏移(含),与 HTTP Range 语义一致。\n * @example\n * ```ts\n * import type { DownloadOptions } from '@h-ai/storage'\n *\n * const value: DownloadOptions = { rangeStart: 0, rangeEnd: 99 }\n * void value\n * ```\n */\nexport interface DownloadOptions {\n /** 取消读取。 */\n signal?: AbortSignal\n /** 范围起始位置(字节,含);不指定时从文件开头读取 */\n rangeStart?: number\n /** 范围结束位置(字节,含);不指定时读取到文件末尾 */\n rangeEnd?: number\n}\n\n/**\n * 复制选项\n *\n * 用于控制 `storage.file.copy()` 的目标文件元数据。\n * 提供任意字段时,S3 Provider 会使用 MetadataDirective='REPLACE' 覆盖源文件元数据。\n * @example\n * ```ts\n * import type { CopyOptions } from '@h-ai/storage'\n *\n * const value: CopyOptions = { contentType: 'text/plain', metadata: { category: 'reports' } }\n * void value\n * ```\n */\nexport interface CopyOptions {\n /** 目标文件的内容类型(不指定时继承源文件) */\n contentType?: string\n /** 目标文件的自定义元数据(不指定时继承源文件) */\n metadata?: Record<string, string>\n}\n\n// ─── 操作接口 ───\n\n/**\n * 文件内容;异步字节流支持 Node Readable 等,不将整个文件载入内存。\n * @example\n * ```ts\n * import type { StorageData } from '@h-ai/storage'\n *\n * const value: StorageData = new TextEncoder().encode('hello')\n * void value\n * ```\n */\nexport type StorageData = Buffer | Uint8Array | string | AsyncIterable<Uint8Array>\n\n/**\n * 常见 checksum 算法;MD5/SHA-1 仅用于兼容性完整性校验,不用于安全认证。\n * @example\n * ```ts\n * import type { ChecksumAlgorithm } from '@h-ai/storage'\n *\n * const value: ChecksumAlgorithm = 'sha256'\n * void value\n * ```\n */\nexport type ChecksumAlgorithm = 'sha256' | 'sha384' | 'sha512' | 'sha1' | 'md5'\n\n/**\n * checksum 计算选项;默认 SHA-256、小写 hex。\n * @example\n * ```ts\n * import type { ChecksumOptions } from '@h-ai/storage'\n *\n * const value: ChecksumOptions = { algorithm: 'sha256', encoding: 'hex' }\n * void value\n * ```\n */\nexport interface ChecksumOptions {\n algorithm?: ChecksumAlgorithm\n encoding?: 'hex' | 'base64'\n signal?: AbortSignal\n}\n\n/**\n * Node 端无状态 checksum 工具,不依赖 storage.init;流输入仅消费一次。\n * @example\n * ```ts\n * import type { ChecksumOperations } from '@h-ai/storage'\n *\n * async function inspect(operations: ChecksumOperations) {\n * const result = await operations.generate('hello')\n * if (!result.success) return result.error.message\n * return result.data\n * }\n * ```\n */\nexport interface ChecksumOperations {\n generate: (data: StorageData, options?: ChecksumOptions) => Promise<HaiResult<string>>\n verify: (data: StorageData, expected: string, options?: ChecksumOptions) => Promise<HaiResult<boolean>>\n}\n\n/**\n * 文件操作接口\n *\n * 提供文件的读写操作。\n * @example\n * ```ts\n * import type { FileOperations } from '@h-ai/storage'\n *\n * async function inspect(operations: FileOperations) {\n * const result = await operations.getStream('reports/a.txt')\n * if (!result.success) return result.error.message\n * for await (const chunk of result.data) void chunk.byteLength\n * }\n * ```\n */\nexport interface FileOperations {\n /**\n * 上传文件\n *\n * @param key - 文件键/路径\n * @param data - 文件内容(Buffer、Uint8Array、字符串或异步字节流)\n * @param options - 上传选项\n * @returns 上传结果包含文件元数据\n * @example\n * ```ts\n * const result = await storage.file.put('uploads/a.txt', 'hello')\n * if (result.success) {\n * result.data.key\n * }\n * ```\n */\n put: (key: string, data: StorageData, options?: UploadOptions) => Promise<HaiResult<FileMetadata>>\n\n /**\n * 下载文件\n *\n * @param key - 文件键/路径\n * @param options - 下载选项\n * @returns 文件内容\n * @example\n * ```ts\n * const result = await storage.file.get('uploads/a.txt')\n * if (result.success) {\n * const buffer = result.data\n * void buffer\n * }\n * ```\n */\n get: (key: string, options?: DownloadOptions) => Promise<HaiResult<Buffer>>\n\n /**\n * 读取字节流,支持 Range 与取消;消费方负责及时迭代或关闭流。\n * @throws 迭代期间的 I/O/取消错误;初始请求失败通过 HaiResult 返回。\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const result = await storage.file.getStream('reports/a.txt')\n * if (!result.success) throw new Error(result.error.message)\n * try {\n * for await (const chunk of result.data) void chunk.byteLength\n * } catch (error) {\n * // 初始请求成功后,迭代仍可能因 I/O/取消失败。\n * throw error\n * }\n * ```\n */\n getStream: (key: string, options?: DownloadOptions) => Promise<HaiResult<AsyncIterable<Uint8Array>>>\n\n /**\n * 获取文件元数据\n *\n * @param key - 文件键/路径\n * @returns 文件元数据\n * @example\n * ```ts\n * const result = await storage.file.head('uploads/a.txt')\n * if (result.success) {\n * result.data.size\n * }\n * ```\n */\n head: (key: string) => Promise<HaiResult<FileMetadata>>\n\n /**\n * 检查文件是否存在\n *\n * @param key - 文件键/路径\n * @returns 是否存在\n * @example\n * ```ts\n * const result = await storage.file.exists('uploads/a.txt')\n * if (result.success) {\n * result.data\n * }\n * ```\n */\n exists: (key: string) => Promise<HaiResult<boolean>>\n\n /**\n * 删除文件\n *\n * @param key - 文件键/路径\n * @returns 删除结果\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const result = await storage.file.delete('uploads/a.txt')\n * if (!result.success) throw new Error(result.error.message)\n * ```\n */\n delete: (key: string) => Promise<HaiResult<void>>\n\n /**\n * 批量删除文件\n *\n * @param keys - 文件键/路径列表\n * @returns 全部成功时返回进度;任一失败时 success=false,进度在 error.ext.deleteMany\n * @example\n * ```ts\n * const result = await storage.file.deleteMany(['a.txt', 'b.txt'])\n * const progress = result.success ? result.data : result.error.ext?.deleteMany\n * void progress\n * ```\n */\n deleteMany: (keys: string[]) => Promise<StorageDeleteResult>\n\n /**\n * 复制文件\n *\n * @param sourceKey - 源文件键\n * @param destKey - 目标文件键\n * @param options - 复制选项\n * @returns 目标文件元数据\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const result = await storage.file.copy('a.txt', 'b.txt')\n * if (!result.success) throw new Error(result.error.message)\n * void result.data.key\n * ```\n */\n copy: (sourceKey: string, destKey: string, options?: CopyOptions) => Promise<HaiResult<FileMetadata>>\n}\n\n/**\n * 目录操作接口\n *\n * 提供目录(前缀)的列表操作。\n * @example\n * ```ts\n * import type { DirOperations } from '@h-ai/storage'\n *\n * async function inspect(operations: DirOperations) {\n * const page = await operations.list({ prefix: 'reports/', maxKeys: 100 })\n * if (!page.success) return page.error.message\n * return page.data.files\n * }\n * ```\n */\nexport interface DirOperations {\n /**\n * 列出目录内容\n *\n * @param options - 列表选项\n * @returns 文件和目录列表\n * @example\n * ```ts\n * const result = await storage.dir.list({ prefix: 'uploads/' })\n * if (result.success) {\n * result.data.files\n * }\n * ```\n */\n list: (options?: ListOptions) => Promise<HaiResult<ListResult>>\n\n /**\n * 删除目录(删除指定前缀下的所有文件)\n *\n * @param prefix - 目录前缀\n * @returns 删除结果\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const result = await storage.dir.delete('uploads/tmp/')\n * if (!result.success) throw new Error(result.error.message)\n * ```\n */\n delete: (prefix: string) => Promise<HaiResult<void>>\n}\n\n/**\n * 签名 URL 操作接口\n *\n * S3 provider 提供临时授权 URL,支持前端直接上传下载。\n * local 的 getUrl/putUrl 返回 UNSUPPORTED_OPERATION,publicUrl 返回 null。\n * @example\n * ```ts\n * import type { PresignOperations } from '@h-ai/storage'\n *\n * async function inspect(operations: PresignOperations) {\n * const signed = await operations.putUrl('reports/a.txt', { contentType: 'text/plain', expiresIn: 300 })\n * if (!signed.success) return signed.error.message\n * return signed.data\n * }\n * ```\n */\nexport interface PresignOperations {\n /**\n * 生成 GET 下载签名 URL;使用 publicEndpoint,缺省回退 endpoint/AWS 端点。\n * publicEndpoint 可含代理目录,返回值已带此前缀;代理须去除它并保留 S3 URI/Host/query,调用方不得自行改写。\n *\n * @param key - 文件键/路径\n * @param options - 签名选项\n * @returns 成功时为可访问的 S3 签名 URL;local 返回 UNSUPPORTED_OPERATION。\n * @example\n * ```ts\n * const result = await storage.presign.getUrl('a.txt', { expiresIn: 60 })\n * if (!result.success) throw new Error(result.error.message)\n * const downloadUrl = result.data\n * void downloadUrl\n * ```\n */\n getUrl: (key: string, options?: PresignOptions) => Promise<HaiResult<string>>\n\n /**\n * 生成 PUT 上传签名 URL;publicEndpoint 目录与 GET 相同处理,服务端 CRUD 仍使用内部 endpoint。\n *\n * @param key - 文件键/路径\n * @param options - 上传签名选项\n * @returns 成功时为可访问的 S3 签名 URL;local 返回 UNSUPPORTED_OPERATION。\n * @example\n * ```ts\n * const result = await storage.presign.putUrl('a.txt', { contentType: 'text/plain' })\n * if (!result.success) throw new Error(result.error.message)\n * const uploadUrl = result.data\n * void uploadUrl\n * ```\n */\n putUrl: (key: string, options?: PresignUploadOptions) => Promise<HaiResult<string>>\n\n /**\n * 获取公开访问 URL(如果配置了 publicUrl)\n *\n * @param key - 文件键/路径\n * @returns 公开 URL;S3 未配置 publicUrl 或 local provider 均返回 null。\n * @example\n * ```ts\n * const url = storage.presign.publicUrl('a.txt')\n * void url\n * ```\n */\n publicUrl: (key: string) => string | null\n}\n\n// ─── 函数接口 ───\n\n/**\n * 存储函数接口\n *\n * 统一的存储访问入口:\n * - `storage.init()` — 初始化存储连接\n * - `storage.close()` — 关闭连接并释放资源\n * - `storage.file` — 文件操作(上传/下载/删除/复制等)\n * - `storage.dir` — 目录操作(列表/删除)\n * - `storage.presign` — 签名 URL 操作\n * - `storage.config` — 当前脱敏配置快照(未初始化时为 null)\n * - `storage.isInitialized` — 初始化状态\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const initialized = await storage.init({ type: 'local', root: './data/uploads' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * try {\n * const uploaded = await storage.file.put('reports/a.txt', 'hello', { ifNoneMatch: '*' })\n * if (!uploaded.success) throw new Error(uploaded.error.message)\n * const downloaded = await storage.file.get('reports/a.txt')\n * if (!downloaded.success) throw new Error(downloaded.error.message)\n * void downloaded.data\n * } finally {\n * const closed = await storage.close()\n * if (!closed.success) throw new Error(closed.error.message)\n * }\n * ```\n */\nexport interface StorageFunctions {\n /** 无状态 checksum 生成与校验,Node 专用,无需初始化。 */\n readonly checksum: ChecksumOperations\n /**\n * 初始化存储连接\n *\n * 已有连接时会先 close 再重新初始化。\n *\n * @param config - 存储配置(支持省略带默认值的字段)\n * @returns 成功 ok(undefined);失败返回 err(含 HaiStorageError 定义的错误信息)\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const initialized = await storage.init({ type: 'local', root: './data/uploads' })\n * if (!initialized.success) throw new Error(initialized.error.message)\n * const closed = await storage.close()\n * if (!closed.success) throw new Error(closed.error.message)\n * ```\n */\n init: (config: StorageConfigInput) => Promise<HaiResult<void>>\n\n /**\n * 关闭存储连接并释放资源\n *\n * 等待初始化并合并并发关闭;失败时操作不可用,资源保留供 close/init 重试。\n * @returns 所有资源释放成功才返回成功;未初始化时同样成功。\n * @example\n * ```ts\n * import { storage } from '@h-ai/storage'\n *\n * const closed = await storage.close()\n * if (!closed.success) throw new Error(closed.error.message)\n * ```\n */\n close: () => Promise<HaiResult<void>>\n\n /** 当前脱敏后的存储配置快照;未初始化或已关闭时为 null */\n readonly config: StorageConfig | null\n\n /** 是否已完成初始化 */\n readonly isInitialized: boolean\n\n /** 文件操作接口(put / get / head / exists / delete / deleteMany / copy) */\n readonly file: FileOperations\n\n /** 目录操作接口(list / delete) */\n readonly dir: DirOperations\n\n /** 签名 URL 操作接口(getUrl / putUrl / publicUrl) */\n readonly presign: PresignOperations\n}\n"]}