@lark-apaas/miaoda-cli 0.1.3 → 0.1.4-alpha.abaa17f

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 (103) hide show
  1. package/dist/api/app/api.js +3 -3
  2. package/dist/api/app/schemas.js +43 -43
  3. package/dist/api/db/api.js +398 -55
  4. package/dist/api/db/client.js +155 -28
  5. package/dist/api/db/index.js +12 -1
  6. package/dist/api/db/parsers.js +20 -20
  7. package/dist/api/db/sql-keywords.js +87 -87
  8. package/dist/api/deploy/api.js +5 -5
  9. package/dist/api/deploy/index.js +12 -1
  10. package/dist/api/deploy/modern-types.js +23 -0
  11. package/dist/api/deploy/modern.js +70 -0
  12. package/dist/api/deploy/plugin-instances-types.js +6 -0
  13. package/dist/api/deploy/plugin-instances.js +30 -0
  14. package/dist/api/deploy/schemas.js +32 -32
  15. package/dist/api/file/api.js +89 -87
  16. package/dist/api/file/client.js +62 -22
  17. package/dist/api/file/detect.js +3 -3
  18. package/dist/api/file/index.js +2 -1
  19. package/dist/api/file/parsers.js +18 -7
  20. package/dist/api/observability/api.js +6 -6
  21. package/dist/api/observability/schemas.js +14 -14
  22. package/dist/api/plugin/api.js +31 -31
  23. package/dist/cli/commands/app/index.js +94 -13
  24. package/dist/cli/commands/db/index.js +602 -54
  25. package/dist/cli/commands/deploy/index.js +28 -28
  26. package/dist/cli/commands/deploy/modern.js +48 -0
  27. package/dist/cli/commands/file/index.js +85 -58
  28. package/dist/cli/commands/index.js +31 -5
  29. package/dist/cli/commands/observability/index.js +69 -69
  30. package/dist/cli/commands/plugin/index.js +27 -27
  31. package/dist/cli/commands/shared.js +10 -10
  32. package/dist/cli/handlers/app/index.js +6 -1
  33. package/dist/cli/handlers/app/init.js +86 -0
  34. package/dist/cli/handlers/app/update.js +2 -2
  35. package/dist/cli/handlers/db/_destructive.js +67 -0
  36. package/dist/cli/handlers/db/_env.js +26 -0
  37. package/dist/cli/handlers/db/_operator.js +35 -0
  38. package/dist/cli/handlers/db/audit.js +383 -0
  39. package/dist/cli/handlers/db/changelog.js +160 -0
  40. package/dist/cli/handlers/db/data.js +32 -31
  41. package/dist/cli/handlers/db/index.js +17 -1
  42. package/dist/cli/handlers/db/migration.js +234 -0
  43. package/dist/cli/handlers/db/quota.js +68 -0
  44. package/dist/cli/handlers/db/recovery.js +413 -0
  45. package/dist/cli/handlers/db/schema.js +33 -33
  46. package/dist/cli/handlers/db/sql.js +69 -69
  47. package/dist/cli/handlers/deploy/deploy.js +4 -4
  48. package/dist/cli/handlers/deploy/error-log.js +1 -1
  49. package/dist/cli/handlers/deploy/get.js +3 -3
  50. package/dist/cli/handlers/deploy/modern.js +32 -0
  51. package/dist/cli/handlers/deploy/polling.js +11 -11
  52. package/dist/cli/handlers/file/cp.js +30 -30
  53. package/dist/cli/handlers/file/index.js +3 -1
  54. package/dist/cli/handlers/file/ls.js +5 -5
  55. package/dist/cli/handlers/file/quota.js +66 -0
  56. package/dist/cli/handlers/file/rm.js +32 -30
  57. package/dist/cli/handlers/file/sign.js +3 -3
  58. package/dist/cli/handlers/file/stat.js +10 -9
  59. package/dist/cli/handlers/observability/analytics.js +47 -47
  60. package/dist/cli/handlers/observability/helpers.js +2 -2
  61. package/dist/cli/handlers/observability/log.js +9 -9
  62. package/dist/cli/handlers/observability/metric.js +26 -26
  63. package/dist/cli/handlers/observability/trace.js +5 -5
  64. package/dist/cli/handlers/plugin/plugin-local.js +53 -53
  65. package/dist/cli/handlers/plugin/plugin.js +15 -15
  66. package/dist/cli/help.js +16 -16
  67. package/dist/main.js +12 -12
  68. package/dist/services/app/init/index.js +14 -0
  69. package/dist/services/app/init/install.js +101 -0
  70. package/dist/services/app/init/steering.js +74 -0
  71. package/dist/services/app/init/template.js +85 -0
  72. package/dist/services/deploy/modern/atoms/build.js +54 -0
  73. package/dist/services/deploy/modern/atoms/context.js +30 -0
  74. package/dist/services/deploy/modern/atoms/index.js +17 -0
  75. package/dist/services/deploy/modern/atoms/local-release.js +27 -0
  76. package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
  77. package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
  78. package/dist/services/deploy/modern/atoms/upload.js +192 -0
  79. package/dist/services/deploy/modern/check.js +58 -0
  80. package/dist/services/deploy/modern/constants.js +13 -0
  81. package/dist/services/deploy/modern/index.js +16 -0
  82. package/dist/services/deploy/modern/pipelines/index.js +5 -0
  83. package/dist/services/deploy/modern/pipelines/local.js +75 -0
  84. package/dist/services/deploy/modern/protocol.js +43 -0
  85. package/dist/services/deploy/modern/run-types.js +4 -0
  86. package/dist/services/deploy/modern/run.js +13 -0
  87. package/dist/services/deploy/modern/template-key-map.js +29 -0
  88. package/dist/utils/args.js +1 -1
  89. package/dist/utils/colors.js +2 -2
  90. package/dist/utils/config.js +2 -2
  91. package/dist/utils/devops-error.js +9 -9
  92. package/dist/utils/error.js +2 -2
  93. package/dist/utils/git.js +4 -4
  94. package/dist/utils/http.js +27 -21
  95. package/dist/utils/index.js +3 -1
  96. package/dist/utils/npm-pack.js +55 -0
  97. package/dist/utils/output.js +67 -45
  98. package/dist/utils/poll.js +35 -0
  99. package/dist/utils/render.js +27 -27
  100. package/dist/utils/spark-meta.js +48 -0
  101. package/dist/utils/spinner.js +46 -0
  102. package/dist/utils/time.js +47 -42
  103. package/package.json +1 -1
@@ -8,8 +8,10 @@ exports.uploadFile = uploadFile;
8
8
  exports.signDownload = signDownload;
9
9
  exports.downloadFile = downloadFile;
10
10
  exports.deleteFiles = deleteFiles;
11
+ exports.getStorageQuota = getStorageQuota;
11
12
  const error_1 = require("../../utils/error");
12
13
  const logger_1 = require("../../utils/logger");
14
+ const time_1 = require("../../utils/time");
13
15
  const client_1 = require("./client");
14
16
  const detect_1 = require("./detect");
15
17
  const parsers_1 = require("./parsers");
@@ -19,18 +21,18 @@ const parsers_1 = require("./parsers");
19
21
  * 所有进入 API 层的 path 都要 strip 前导 `/`。
20
22
  */
21
23
  function toApiPath(filePath) {
22
- return filePath.replace(/^\/+/, "");
24
+ return filePath.replace(/^\/+/, '');
23
25
  }
24
26
  function encodePath(filePath) {
25
27
  return toApiPath(filePath)
26
- .split("/")
28
+ .split('/')
27
29
  .map((seg) => encodeURIComponent(seg))
28
- .join("/");
30
+ .join('/');
29
31
  }
30
32
  function extractEnvelope(body) {
31
33
  (0, client_1.ensureSuccess)(body);
32
34
  if (!body.data) {
33
- throw new error_1.AppError("INTERNAL_FILE_API_ERROR", "API returned empty data");
35
+ throw new error_1.AppError('INTERNAL_FILE_API_ERROR', 'API returned empty data');
34
36
  }
35
37
  return body.data;
36
38
  }
@@ -49,7 +51,7 @@ async function listOnce(appId, bucketId, opts) {
49
51
  if (opts.sortBy && opts.sortBy.length > 0)
50
52
  reqBody.sortBy = opts.sortBy;
51
53
  const body = await (0, client_1.doPost)(url, reqBody, {
52
- errorContext: "list files",
54
+ errorContext: 'list files',
53
55
  });
54
56
  return extractEnvelope(body);
55
57
  }
@@ -70,29 +72,29 @@ function buildFilterExpr(opts) {
70
72
  return opts.filterExpr;
71
73
  const conds = [];
72
74
  if (opts.name) {
73
- conds.push({ field: "name", operator: "eq", value: opts.name });
75
+ conds.push({ field: 'name', operator: 'eq', value: opts.name });
74
76
  }
75
77
  if (opts.type) {
76
- conds.push({ field: "type", operator: "eq", value: opts.type });
78
+ conds.push({ field: 'type', operator: 'eq', value: opts.type });
77
79
  }
78
80
  if (opts.sizeGt !== undefined) {
79
- conds.push({ field: "size", operator: "gt", value: String(opts.sizeGt) });
81
+ conds.push({ field: 'size', operator: 'gt', value: String(opts.sizeGt) });
80
82
  }
81
83
  if (opts.sizeLt !== undefined) {
82
- conds.push({ field: "size", operator: "lt", value: String(opts.sizeLt) });
84
+ conds.push({ field: 'size', operator: 'lt', value: String(opts.sizeLt) });
83
85
  }
84
86
  if (opts.uploadedSince) {
85
87
  // 后端期望毫秒 timestamp(strconv.ParseInt → time.UnixMilli)
86
- const ms = parseTimeFilterMs(opts.uploadedSince, "--uploaded-since");
87
- conds.push({ field: "createdAt", operator: "gte", value: String(ms) });
88
+ const ms = parseTimeFilterMs(opts.uploadedSince, '--uploaded-since');
89
+ conds.push({ field: 'createdAt', operator: 'gte', value: String(ms) });
88
90
  }
89
91
  if (opts.uploadedUntil) {
90
- const ms = parseTimeFilterMs(opts.uploadedUntil, "--uploaded-until");
91
- conds.push({ field: "createdAt", operator: "lte", value: String(ms) });
92
+ const ms = parseTimeFilterMs(opts.uploadedUntil, '--uploaded-until');
93
+ conds.push({ field: 'createdAt', operator: 'lte', value: String(ms) });
92
94
  }
93
95
  if (conds.length === 0)
94
96
  return undefined;
95
- return { logic: "and", groups: [{ conditions: conds }] };
97
+ return { logic: 'and', groups: [{ conditions: conds }] };
96
98
  }
97
99
  /**
98
100
  * 解析时间过滤参数(--uploaded-since / --uploaded-until)的三种输入格式
@@ -107,31 +109,17 @@ function buildFilterExpr(opts) {
107
109
  * flagName 用于错误信息,调用方传 "--uploaded-since" 或 "--uploaded-until"。
108
110
  */
109
111
  function parseTimeFilterMs(input, flagName) {
110
- // 相对时间:<positive int><unit>,单位 s/m/h/d/w
111
- const RELATIVE = /^(\d+)([smhdw])$/;
112
- const rel = RELATIVE.exec(input);
113
- if (rel) {
114
- const n = parseInt(rel[1], 10);
115
- if (n <= 0) {
116
- throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 必须是正整数 + 单位(s / m / h / d / w)`);
117
- }
118
- const UNIT_MS = {
119
- s: 1_000,
120
- m: 60_000,
121
- h: 3_600_000,
122
- d: 86_400_000,
123
- w: 604_800_000,
124
- };
125
- return Date.now() - n * UNIT_MS[rel[2]];
112
+ try {
113
+ return (0, time_1.parseTimeToMs)(input);
126
114
  }
127
- // 绝对时间:date / ISO 8601。Date.parse 对 "YYYY-MM-DD" 按 UTC 00:00:00 解析,
128
- // 对带 Z ISO 8601 也直接出 UTC ms。
129
- const ms = Date.parse(input);
130
- if (Number.isNaN(ms)) {
131
- throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 格式无法识别。支持:` +
132
- `相对时间(如 1h / 2d / 1w)、日期(YYYY-MM-DD)、ISO 8601(如 2026-04-01T10:00:00Z)`);
115
+ catch (err) {
116
+ if (err instanceof error_1.AppError) {
117
+ throw new error_1.AppError(err.code, `${flagName}: ${err.message}`, {
118
+ next_actions: err.next_actions,
119
+ });
120
+ }
121
+ throw err;
133
122
  }
134
- return ms;
135
123
  }
136
124
  /**
137
125
  * 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
@@ -147,7 +135,7 @@ async function listFiles(opts) {
147
135
  let hasMore = false;
148
136
  // 分页 cursor 用 raw attachment.id(后端 continuationToken 语义);
149
137
  // CLI 对外不暴露 attachment.id,所以这里单独追踪最后一页的 raw id
150
- let lastRawId = "";
138
+ let lastRawId = '';
151
139
  for (;;) {
152
140
  const fetchLimit = opts.all ? 200 : pageSize;
153
141
  const data = await listOnce(opts.appId, bucketId, {
@@ -159,7 +147,7 @@ async function listFiles(opts) {
159
147
  const rawItems = data.attachments ?? [];
160
148
  hasMore = Boolean(data.hasMore);
161
149
  if (rawItems.length > 0) {
162
- lastRawId = rawItems[rawItems.length - 1].id ?? "";
150
+ lastRawId = rawItems[rawItems.length - 1].id ?? '';
163
151
  }
164
152
  for (const raw of rawItems) {
165
153
  const info = (0, parsers_1.parseAttachment)(raw);
@@ -208,8 +196,8 @@ async function resolveInputs(opts) {
208
196
  if (opts.inputs.length === 0)
209
197
  return [];
210
198
  return Promise.all(opts.inputs.map((input) => {
211
- const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? "path" : "name");
212
- return kind === "path" ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
199
+ const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? 'path' : 'name');
200
+ return kind === 'path' ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
213
201
  }));
214
202
  }
215
203
  /** path 输入:HEAD 判存在性,path 在 bucket 内 unique 无需反查。 */
@@ -218,17 +206,17 @@ async function resolveByPath(appId, input) {
218
206
  const filePath = (0, detect_1.toAbsolutePath)(input);
219
207
  try {
220
208
  const info = await statFile({ appId, filePath });
221
- return { status: "ok", input, file: info };
209
+ return { status: 'ok', input, file: info };
222
210
  }
223
211
  catch (err) {
224
- if (err instanceof error_1.AppError && err.code === "FILE_NOT_FOUND") {
212
+ if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
225
213
  return {
226
- status: "error",
214
+ status: 'error',
227
215
  input,
228
216
  error: {
229
- code: "FILE_NOT_FOUND",
217
+ code: 'FILE_NOT_FOUND',
230
218
  message: `File '${input}' does not exist`,
231
- hint: "Run `miaoda file ls` to see available files.",
219
+ hint: 'Run `miaoda file ls` to see available files.',
232
220
  },
233
221
  };
234
222
  }
@@ -238,38 +226,38 @@ async function resolveByPath(appId, input) {
238
226
  /** file_name 输入:list + FilterExpression `name eq X`,多匹配 AMBIGUOUS。 */
239
227
  async function resolveByName(appId, input) {
240
228
  const filterExpr = {
241
- logic: "and",
242
- groups: [{ conditions: [{ field: "name", operator: "eq", value: input }] }],
229
+ logic: 'and',
230
+ groups: [{ conditions: [{ field: 'name', operator: 'eq', value: input }] }],
243
231
  };
244
232
  const result = await listFiles({ appId, filterExpr, limit: 200 });
245
233
  // 后端返 name 精确匹配的所有 attachments。basename fallback 防御(后端某些场景 name 可能为空)
246
234
  const matches = result.items.filter((i) => {
247
- const basename = i.path.split("/").pop() ?? i.path;
235
+ const basename = i.path.split('/').pop() ?? i.path;
248
236
  return i.file_name === input || basename === input;
249
237
  });
250
238
  if (matches.length === 0) {
251
239
  return {
252
- status: "error",
240
+ status: 'error',
253
241
  input,
254
242
  error: {
255
- code: "FILE_NOT_FOUND",
243
+ code: 'FILE_NOT_FOUND',
256
244
  message: `File '${input}' does not exist`,
257
- hint: "Run `miaoda file ls` to see available files.",
245
+ hint: 'Run `miaoda file ls` to see available files.',
258
246
  },
259
247
  };
260
248
  }
261
249
  if (matches.length > 1) {
262
250
  return {
263
- status: "error",
251
+ status: 'error',
264
252
  input,
265
253
  error: {
266
- code: "AMBIGUOUS_FILE_NAME",
254
+ code: 'AMBIGUOUS_FILE_NAME',
267
255
  message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
268
256
  hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
269
257
  },
270
258
  };
271
259
  }
272
- return { status: "ok", input, file: matches[0] };
260
+ return { status: 'ok', input, file: matches[0] };
273
261
  }
274
262
  // ── stat ──
275
263
  /**
@@ -281,10 +269,10 @@ async function statFile(opts) {
281
269
  // 对齐 file-storage-skill Py 版:URL 顺序是 .../object/{bucket}/head/{path}
282
270
  const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/${encodeURIComponent(bucketId)}/head/${encodePath(opts.filePath)}`;
283
271
  const body = await (0, client_1.doGet)(url, {
284
- notFoundCode: "FILE_NOT_FOUND",
272
+ notFoundCode: 'FILE_NOT_FOUND',
285
273
  notFoundMessage: `File '${opts.filePath}' does not exist`,
286
- notFoundHint: "Run `miaoda file ls` to see available files.",
287
- errorContext: "stat file",
274
+ notFoundHint: 'Run `miaoda file ls` to see available files.',
275
+ errorContext: 'stat file',
288
276
  });
289
277
  return (0, parsers_1.parseHead)(extractEnvelope(body), opts.filePath);
290
278
  }
@@ -292,7 +280,7 @@ async function statFile(opts) {
292
280
  async function preUpload(appId, req) {
293
281
  const bucketId = await (0, client_1.getDefaultBucketId)(appId);
294
282
  const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/preUpload`;
295
- const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: "pre-upload" });
283
+ const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: 'pre-upload' });
296
284
  return extractEnvelope(body);
297
285
  }
298
286
  /**
@@ -308,14 +296,14 @@ async function preUpload(appId, req) {
308
296
  async function uploadCallback(appId, req) {
309
297
  const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
310
298
  const body = await (0, client_1.doPost)(url, req, {
311
- errorContext: "upload callback",
299
+ errorContext: 'upload callback',
312
300
  });
313
301
  const data = extractEnvelope(body);
314
302
  const metadata = data.metadata;
315
303
  if (!metadata) {
316
304
  return {};
317
305
  }
318
- if (typeof metadata === "object") {
306
+ if (typeof metadata === 'object') {
319
307
  return metadata;
320
308
  }
321
309
  // string 形态兜底
@@ -342,10 +330,10 @@ async function uploadFile(opts) {
342
330
  filePath: opts.remotePath ? toApiPath(opts.remotePath) : undefined,
343
331
  });
344
332
  if (!pre.uploadURL || !pre.uploadID) {
345
- throw new error_1.AppError("FILE_UPLOAD_FAILED", "preUpload did not return uploadURL/uploadID");
333
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', 'preUpload did not return uploadURL/uploadID');
346
334
  }
347
335
  const body = await opts.readFile();
348
- let etag = "";
336
+ let etag = '';
349
337
  try {
350
338
  // Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
351
339
  // 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
@@ -355,27 +343,27 @@ async function uploadFile(opts) {
355
343
  // header 作为对象 metadata 存住,服务端 callback 阶段 HeadObject 读回并解析
356
344
  // filename 写入 DB。我们要不传 header,服务端走兜底会把 storage key 当文件名。
357
345
  const res = await fetch(pre.uploadURL, {
358
- method: "PUT",
346
+ method: 'PUT',
359
347
  headers: {
360
- "Content-Type": opts.contentType,
361
- "Content-Length": String(opts.fileSize),
362
- "Content-Disposition": `attachment; filename="${sanitizeFileName(opts.fileName)}"`,
348
+ 'Content-Type': opts.contentType,
349
+ 'Content-Length': String(opts.fileSize),
350
+ 'Content-Disposition': `attachment; filename="${sanitizeFileName(opts.fileName)}"`,
363
351
  },
364
352
  body: ab,
365
353
  });
366
354
  (0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
367
355
  if (!res.ok) {
368
- throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
356
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed with status ${String(res.status)}`);
369
357
  }
370
- etag = res.headers.get("ETag") ?? "";
358
+ etag = res.headers.get('ETag') ?? '';
371
359
  }
372
360
  catch (err) {
373
361
  if (err instanceof error_1.AppError)
374
362
  throw err;
375
- throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed: ${err instanceof Error ? err.message : String(err)}`);
363
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed: ${err instanceof Error ? err.message : String(err)}`);
376
364
  }
377
365
  if (!etag) {
378
- throw new error_1.AppError("FILE_UPLOAD_FAILED", "Upload PUT returned no ETag");
366
+ throw new error_1.AppError('FILE_UPLOAD_FAILED', 'Upload PUT returned no ETag');
379
367
  }
380
368
  // callback 返回服务端实际生成的对象元数据(filePath / file_name / download_url)。
381
369
  // path 末段是平台生成的 16 位 GID,CLI 无法靠本地信息推断;callback 失败 /
@@ -389,20 +377,20 @@ async function uploadFile(opts) {
389
377
  }
390
378
  catch (err) {
391
379
  const reason = err instanceof Error ? err.message : String(err);
392
- throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", `Upload callback failed: ${reason}; file may already exist in storage`, {
380
+ throw new error_1.AppError('FILE_UPLOAD_CALLBACK_INCOMPLETE', `Upload callback failed: ${reason}; file may already exist in storage`, {
393
381
  next_actions: [
394
382
  `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
395
383
  ],
396
384
  });
397
385
  }
398
386
  if (!metadata.filePath) {
399
- throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", "Upload callback returned no filePath; file may already exist in storage", {
387
+ throw new error_1.AppError('FILE_UPLOAD_CALLBACK_INCOMPLETE', 'Upload callback returned no filePath; file may already exist in storage', {
400
388
  next_actions: [
401
389
  `Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
402
390
  ],
403
391
  });
404
392
  }
405
- const path = metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath;
393
+ const path = metadata.filePath.startsWith('/') ? metadata.filePath : '/' + metadata.filePath;
406
394
  const result = {
407
395
  // 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
408
396
  // 与后续 file ls / file stat 返回的展示名保持一致;缺失时降级用本地 fileName。
@@ -425,7 +413,7 @@ async function uploadFile(opts) {
425
413
  */
426
414
  function sanitizeFileName(fileName) {
427
415
  const illegalChars = /[:"\\/*?<>|,;]/g;
428
- return encodeURIComponent(fileName.replace(illegalChars, "")) || "download_file";
416
+ return encodeURIComponent(fileName.replace(illegalChars, '')) || 'download_file';
429
417
  }
430
418
  // ── 预签下载 URL ──
431
419
  /**
@@ -441,15 +429,15 @@ async function signDownload(opts) {
441
429
  expiresIn: opts.expiresIn,
442
430
  };
443
431
  const body = await (0, client_1.doPost)(url, reqBody, {
444
- errorContext: "sign download URL",
432
+ errorContext: 'sign download URL',
445
433
  });
446
434
  // 后端对不存在文件走业务 ErrorCode(status_code=k_img_ec_000034),
447
435
  // 由 ensureSuccess → BIZ_ERR_MAP 统一抛出 FILE_NOT_FOUND,此处无需兜底
448
436
  const data = extractEnvelope(body);
449
- const signed = data.signedURL ?? "";
437
+ const signed = data.signedURL ?? '';
450
438
  const expiresAt = new Date(Date.now() + opts.expiresIn * 1000).toISOString();
451
- const displayPath = opts.filePath.startsWith("/") ? opts.filePath : `/${opts.filePath}`;
452
- const fileName = displayPath.split("/").pop() ?? displayPath;
439
+ const displayPath = opts.filePath.startsWith('/') ? opts.filePath : `/${opts.filePath}`;
440
+ const fileName = displayPath.split('/').pop() ?? displayPath;
453
441
  return {
454
442
  file_name: fileName,
455
443
  path: displayPath,
@@ -468,11 +456,11 @@ async function downloadFile(opts) {
468
456
  }
469
457
  catch (err) {
470
458
  (0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
471
- throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
459
+ throw new error_1.AppError('FILE_DOWNLOAD_FAILED', `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
472
460
  }
473
461
  (0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
474
462
  if (!res.ok) {
475
- throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
463
+ throw new error_1.AppError('FILE_DOWNLOAD_FAILED', `Download failed with status ${String(res.status)}`);
476
464
  }
477
465
  const array = new Uint8Array(await res.arrayBuffer());
478
466
  const buf = Buffer.from(array);
@@ -492,17 +480,17 @@ async function deleteFiles(opts) {
492
480
  filePaths: opts.filePaths.map(toApiPath),
493
481
  };
494
482
  const body = await (0, client_1.doRequest)({
495
- method: "DELETE",
483
+ method: 'DELETE',
496
484
  url,
497
- headers: { "Content-Type": "application/json" },
485
+ headers: { 'Content-Type': 'application/json' },
498
486
  body: JSON.stringify(reqBody),
499
- }, { errorContext: "delete files" });
487
+ }, { errorContext: 'delete files' });
500
488
  const data = extractEnvelope(body);
501
489
  // 后端响应里只给出被删除成功的 attachments(无前导 /),没删掉的靠对比输入判断
502
- const toDisplay = (p) => (p.startsWith("/") ? p : `/${p}`);
490
+ const toDisplay = (p) => (p.startsWith('/') ? p : `/${p}`);
503
491
  const deletedSet = new Set();
504
492
  for (const att of data.attachments ?? []) {
505
- const p = att.filePath ?? att.name ?? "";
493
+ const p = att.filePath ?? att.name ?? '';
506
494
  if (p)
507
495
  deletedSet.add(toApiPath(p));
508
496
  }
@@ -514,8 +502,22 @@ async function deleteFiles(opts) {
514
502
  deleted.push(toDisplay(normalized));
515
503
  }
516
504
  else {
517
- failed.push({ path: toDisplay(normalized), error: "file not found or not deleted" });
505
+ failed.push({ path: toDisplay(normalized), error: 'file not found or not deleted' });
518
506
  }
519
507
  }
520
508
  return { deleted, failed };
521
509
  }
510
+ // ── storage quota ──
511
+ /**
512
+ * 后端:GET /v1/storage/app/{appId}/bucket/{bucketId}/quota
513
+ * 单 bucket 用量;StorageQuotaBytes 暂未对接,CLI 拿到 0 时按 "—" 渲染。
514
+ * bucketId 缺省时走默认 bucket(与 ls / stat / cp 等一致)。
515
+ */
516
+ async function getStorageQuota(opts) {
517
+ const bucketId = opts.bucketId ?? (await (0, client_1.getDefaultBucketId)(opts.appId));
518
+ const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/quota`;
519
+ const body = await (0, client_1.doGet)(url, {
520
+ errorContext: `fetch storage quota for app '${opts.appId}' bucket '${bucketId}'`,
521
+ });
522
+ return extractEnvelope(body);
523
+ }
@@ -20,9 +20,9 @@ function traceHttp(method, url, start, response, err) {
20
20
  try {
21
21
  const cost = Date.now() - start;
22
22
  const status = response?.status ?? 0;
23
- const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
23
+ const logid = response?.headers?.get?.('x-tt-logid') ?? '-';
24
24
  if (err !== undefined) {
25
- const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
25
+ const errMsg = err instanceof Error ? err.message : typeof err === 'string' ? err : JSON.stringify(err);
26
26
  (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
27
27
  return;
28
28
  }
@@ -51,7 +51,7 @@ async function getDefaultBucketId(appId) {
51
51
  ensureSuccess(body);
52
52
  const bucketId = body.data?.app_runtime_extra?.bucket?.default_bucket_id;
53
53
  if (!bucketId) {
54
- throw new error_1.AppError("BUCKET_NOT_FOUND", `No default bucket for app '${appId}'`);
54
+ throw new error_1.AppError('BUCKET_NOT_FOUND', `No default bucket for app '${appId}'`);
55
55
  }
56
56
  bucketCache.set(appId, bucketId);
57
57
  return bucketId;
@@ -81,31 +81,53 @@ function resetBucketCache() {
81
81
  */
82
82
  const BIZ_ERR_MAP = new Map(Object.entries({
83
83
  k_img_ec_000034: {
84
- code: "FILE_NOT_FOUND",
85
- message: "File does not exist",
86
- hint: "Run `miaoda file ls` to see available files.",
84
+ code: 'FILE_NOT_FOUND',
85
+ message: 'File does not exist',
86
+ hint: 'Run `miaoda file ls` to see available files.',
87
87
  },
88
88
  k_img_ec_000035: {
89
- code: "FILE_ALREADY_EXISTS",
90
- message: "A file at this path already exists",
91
- hint: "Rename the target or delete the existing file first (`miaoda file rm`).",
89
+ code: 'FILE_ALREADY_EXISTS',
90
+ message: 'A file at this path already exists',
91
+ hint: 'Rename the target or delete the existing file first (`miaoda file rm`).',
92
+ },
93
+ // k_ec_000040:file-storage 上游引擎层连不上下游服务(依赖抖动 / 远端 RPC 失败)。
94
+ // 文案不向用户暴露内部 code,统一成 INTERNAL_API_ERROR + 重试引导。
95
+ k_ec_000040: {
96
+ code: 'INTERNAL_API_ERROR',
97
+ message: 'Service temporarily unavailable',
98
+ hint: 'Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team.',
92
99
  },
93
100
  }));
101
+ // k_ec_* 命名空间是 file-storage 引擎层通用错误(基础设施 / 上游 RPC),不是
102
+ // 用户输入问题。无显式条目时统一兜底成 INTERNAL_API_ERROR + 重试 hint,避免
103
+ // 暴露内部 code 给最终用户。
104
+ function isEngineCommonError(code) {
105
+ return /^k_ec_\d+$/.test(code);
106
+ }
94
107
  function ensureSuccess(body) {
95
108
  // 后端 envelope 字段历史遗留多种命名:
96
109
  // - ErrorCode / error_code: 部分接口
97
110
  // - status_code: delete / sign / head / preUpload 等新接口
98
- const code = body.ErrorCode ?? body.error_code ?? body.status_code ?? "0";
99
- if (code === "0" || code === "")
111
+ const code = body.ErrorCode ?? body.error_code ?? body.status_code ?? '0';
112
+ if (code === '0' || code === '')
100
113
  return;
101
114
  // 错误 message 字段同样散:ErrorMessage / error_message / error_msg / Message
102
- const backendMsg = body.ErrorMessage ?? body.error_message ?? body.error_msg ?? body.Message ?? "unknown error";
115
+ const backendMsg = body.ErrorMessage ?? body.error_message ?? body.error_msg ?? body.Message ?? 'unknown error';
103
116
  const mapped = BIZ_ERR_MAP.get(code);
104
117
  if (mapped) {
105
118
  throw new error_1.AppError(mapped.code, mapped.message ?? backendMsg, {
106
119
  next_actions: mapped.hint ? [mapped.hint] : undefined,
107
120
  });
108
121
  }
122
+ // k_ec_* 引擎层错误统一兜底:用户视角是"服务暂时不可用",重试即可,
123
+ // 不要把 `File API error [k_ec_000xxx]: <内部翻译>` 这种半成品文案抛给用户。
124
+ if (isEngineCommonError(code)) {
125
+ throw new error_1.AppError('INTERNAL_API_ERROR', 'Service temporarily unavailable', {
126
+ next_actions: [
127
+ `Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team. (upstream code: ${code})`,
128
+ ],
129
+ });
130
+ }
109
131
  throw new error_1.AppError(`FILE_API_${code}`, `File API error [${code}]: ${backendMsg}`);
110
132
  }
111
133
  /** 从 HttpError 的 response 里尝试读 body,用于拿后端返的业务 ErrorCode。 */
@@ -128,22 +150,40 @@ async function mapHttpError(err, opts) {
128
150
  const body = await extractBody(err.response);
129
151
  // 1. 先看后端业务 ErrorCode(优先级最高)
130
152
  if (body) {
131
- const code = body.ErrorCode ?? body.error_code ?? "";
132
- if (code && code !== "0") {
153
+ const code = body.ErrorCode ?? body.error_code ?? '';
154
+ if (code && code !== '0') {
155
+ const mapped = BIZ_ERR_MAP.get(code);
156
+ if (mapped) {
157
+ const msg = body.ErrorMessage ??
158
+ body.error_message ??
159
+ body.Message ??
160
+ mapped.message ??
161
+ err.message;
162
+ throw new error_1.AppError(mapped.code, mapped.message ?? msg, {
163
+ next_actions: mapped.hint ? [mapped.hint] : undefined,
164
+ });
165
+ }
166
+ if (isEngineCommonError(code)) {
167
+ throw new error_1.AppError('INTERNAL_API_ERROR', 'Service temporarily unavailable', {
168
+ next_actions: [
169
+ `Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team. (upstream code: ${code})`,
170
+ ],
171
+ });
172
+ }
133
173
  const msg = body.ErrorMessage ?? body.error_message ?? body.Message ?? err.message;
134
174
  throw new error_1.AppError(`FILE_API_${code}`, `File API error [${code}]: ${msg}`);
135
175
  }
136
176
  }
137
177
  // 2. 404 常见情况 —— 路径不存在,映射到业务语义
138
178
  if (status === 404 && opts.notFoundCode) {
139
- throw new error_1.AppError(opts.notFoundCode, opts.notFoundMessage ?? "resource not found", {
179
+ throw new error_1.AppError(opts.notFoundCode, opts.notFoundMessage ?? 'resource not found', {
140
180
  next_actions: opts.notFoundHint ? [opts.notFoundHint] : undefined,
141
181
  });
142
182
  }
143
183
  // 3. 兜底:保留原始 status 的 HttpError(给上层看到真实状态码)
144
- const ctx = opts.errorContext ?? "HTTP request failed";
145
- const statusText = err.response?.statusText ?? "";
146
- throw new error_1.HttpError(status, err.config.url ?? "", `${ctx}: ${String(status)} ${statusText}`.trim());
184
+ const ctx = opts.errorContext ?? 'HTTP request failed';
185
+ const statusText = err.response?.statusText ?? '';
186
+ throw new error_1.HttpError(status, err.config.url ?? '', `${ctx}: ${String(status)} ${statusText}`.trim());
147
187
  }
148
188
  throw err;
149
189
  }
@@ -156,11 +196,11 @@ async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
156
196
  const start = Date.now();
157
197
  try {
158
198
  const response = await client.get(url);
159
- traceHttp("GET", url, start, response);
199
+ traceHttp('GET', url, start, response);
160
200
  return (await response.json());
161
201
  }
162
202
  catch (err) {
163
- traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
203
+ traceHttp('GET', url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
164
204
  await mapHttpError(err, opts);
165
205
  throw err; // 不可达,mapHttpError 必定 throw
166
206
  }
@@ -170,11 +210,11 @@ async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()
170
210
  const start = Date.now();
171
211
  try {
172
212
  const response = await client.post(url, body);
173
- traceHttp("POST", url, start, response);
213
+ traceHttp('POST', url, start, response);
174
214
  return (await response.json());
175
215
  }
176
216
  catch (err) {
177
- traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
217
+ traceHttp('POST', url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
178
218
  await mapHttpError(err, opts);
179
219
  throw err;
180
220
  }
@@ -41,9 +41,9 @@ const BACKEND_KEY_PATTERN = /^\d{16,}(?:\.[a-zA-Z0-9]{1,10}){0,2}$/;
41
41
  function looksLikePath(input) {
42
42
  if (!input)
43
43
  return false;
44
- if (input.startsWith("/"))
44
+ if (input.startsWith('/'))
45
45
  return true;
46
- if (input.includes("/"))
46
+ if (input.includes('/'))
47
47
  return true;
48
48
  return BACKEND_KEY_PATTERN.test(input);
49
49
  }
@@ -52,5 +52,5 @@ function looksLikePath(input) {
52
52
  * 便于从 auto-detected 的 "abc123...xyz.txt" 回到统一的 `/abc123...xyz.txt`。
53
53
  */
54
54
  function toAbsolutePath(input) {
55
- return input.startsWith("/") ? input : `/${input}`;
55
+ return input.startsWith('/') ? input : `/${input}`;
56
56
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
3
+ exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.getStorageQuota = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
4
4
  var api_1 = require("./api");
5
5
  Object.defineProperty(exports, "listFiles", { enumerable: true, get: function () { return api_1.listFiles; } });
6
6
  Object.defineProperty(exports, "statFile", { enumerable: true, get: function () { return api_1.statFile; } });
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function
10
10
  Object.defineProperty(exports, "deleteFiles", { enumerable: true, get: function () { return api_1.deleteFiles; } });
11
11
  Object.defineProperty(exports, "resolveInputs", { enumerable: true, get: function () { return api_1.resolveInputs; } });
12
12
  Object.defineProperty(exports, "parseTimeFilterMs", { enumerable: true, get: function () { return api_1.parseTimeFilterMs; } });
13
+ Object.defineProperty(exports, "getStorageQuota", { enumerable: true, get: function () { return api_1.getStorageQuota; } });
13
14
  var client_1 = require("./client");
14
15
  Object.defineProperty(exports, "getDefaultBucketId", { enumerable: true, get: function () { return client_1.getDefaultBucketId; } });
15
16
  Object.defineProperty(exports, "resetBucketCache", { enumerable: true, get: function () { return client_1.resetBucketCache; } });