@lark-apaas/miaoda-cli 0.1.3-alpha.4bf312e → 0.1.3-alpha.67da0bb

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