@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.
- package/dist/api/app/api.js +3 -3
- package/dist/api/app/schemas.js +43 -43
- package/dist/api/db/api.js +108 -112
- package/dist/api/db/client.js +41 -41
- package/dist/api/db/parsers.js +20 -20
- package/dist/api/db/sql-keywords.js +87 -87
- package/dist/api/deploy/api.js +5 -5
- package/dist/api/deploy/schemas.js +32 -32
- package/dist/api/file/api.js +74 -87
- package/dist/api/file/client.js +22 -22
- package/dist/api/file/detect.js +3 -3
- package/dist/api/file/parsers.js +18 -7
- package/dist/api/observability/api.js +6 -6
- package/dist/api/observability/schemas.js +14 -14
- package/dist/api/plugin/api.js +31 -31
- package/dist/cli/commands/app/index.js +12 -12
- package/dist/cli/commands/db/index.js +268 -163
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/file/index.js +81 -61
- package/dist/cli/commands/observability/index.js +69 -69
- package/dist/cli/commands/plugin/index.js +27 -27
- package/dist/cli/commands/shared.js +10 -10
- package/dist/cli/handlers/app/update.js +2 -2
- package/dist/cli/handlers/db/audit.js +52 -43
- package/dist/cli/handlers/db/changelog.js +25 -12
- package/dist/cli/handlers/db/data.js +31 -30
- package/dist/cli/handlers/db/migration.js +27 -27
- package/dist/cli/handlers/db/quota.js +3 -3
- package/dist/cli/handlers/db/recovery.js +111 -82
- package/dist/cli/handlers/db/schema.js +33 -33
- package/dist/cli/handlers/db/sql.js +69 -69
- package/dist/cli/handlers/deploy/deploy.js +4 -4
- package/dist/cli/handlers/deploy/error-log.js +1 -1
- package/dist/cli/handlers/deploy/get.js +3 -3
- package/dist/cli/handlers/deploy/polling.js +11 -11
- package/dist/cli/handlers/file/cp.js +30 -30
- package/dist/cli/handlers/file/ls.js +5 -5
- package/dist/cli/handlers/file/quota.js +2 -2
- package/dist/cli/handlers/file/rm.js +24 -24
- package/dist/cli/handlers/file/sign.js +3 -3
- package/dist/cli/handlers/file/stat.js +10 -9
- package/dist/cli/handlers/observability/analytics.js +47 -47
- package/dist/cli/handlers/observability/helpers.js +2 -2
- package/dist/cli/handlers/observability/log.js +9 -9
- package/dist/cli/handlers/observability/metric.js +26 -26
- package/dist/cli/handlers/observability/trace.js +5 -5
- package/dist/cli/handlers/plugin/plugin-local.js +53 -53
- package/dist/cli/handlers/plugin/plugin.js +15 -15
- package/dist/cli/help.js +16 -16
- package/dist/main.js +12 -12
- package/dist/utils/args.js +1 -1
- package/dist/utils/colors.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/devops-error.js +9 -9
- package/dist/utils/error.js +2 -2
- package/dist/utils/git.js +4 -4
- package/dist/utils/http.js +19 -19
- package/dist/utils/output.js +47 -47
- package/dist/utils/poll.js +2 -2
- package/dist/utils/render.js +27 -27
- package/dist/utils/time.js +47 -42
- package/package.json +1 -1
package/dist/api/file/api.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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:
|
|
75
|
+
conds.push({ field: 'name', operator: 'eq', value: opts.name });
|
|
75
76
|
}
|
|
76
77
|
if (opts.type) {
|
|
77
|
-
conds.push({ field:
|
|
78
|
+
conds.push({ field: 'type', operator: 'eq', value: opts.type });
|
|
78
79
|
}
|
|
79
80
|
if (opts.sizeGt !== undefined) {
|
|
80
|
-
conds.push({ field:
|
|
81
|
+
conds.push({ field: 'size', operator: 'gt', value: String(opts.sizeGt) });
|
|
81
82
|
}
|
|
82
83
|
if (opts.sizeLt !== undefined) {
|
|
83
|
-
conds.push({ field:
|
|
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,
|
|
88
|
-
conds.push({ field:
|
|
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,
|
|
92
|
-
conds.push({ field:
|
|
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:
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) ?
|
|
213
|
-
return kind ===
|
|
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:
|
|
209
|
+
return { status: 'ok', input, file: info };
|
|
223
210
|
}
|
|
224
211
|
catch (err) {
|
|
225
|
-
if (err instanceof error_1.AppError && err.code ===
|
|
212
|
+
if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
|
|
226
213
|
return {
|
|
227
|
-
status:
|
|
214
|
+
status: 'error',
|
|
228
215
|
input,
|
|
229
216
|
error: {
|
|
230
|
-
code:
|
|
217
|
+
code: 'FILE_NOT_FOUND',
|
|
231
218
|
message: `File '${input}' does not exist`,
|
|
232
|
-
hint:
|
|
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:
|
|
243
|
-
groups: [{ conditions: [{ field:
|
|
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(
|
|
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:
|
|
240
|
+
status: 'error',
|
|
254
241
|
input,
|
|
255
242
|
error: {
|
|
256
|
-
code:
|
|
243
|
+
code: 'FILE_NOT_FOUND',
|
|
257
244
|
message: `File '${input}' does not exist`,
|
|
258
|
-
hint:
|
|
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:
|
|
251
|
+
status: 'error',
|
|
265
252
|
input,
|
|
266
253
|
error: {
|
|
267
|
-
code:
|
|
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:
|
|
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:
|
|
272
|
+
notFoundCode: 'FILE_NOT_FOUND',
|
|
286
273
|
notFoundMessage: `File '${opts.filePath}' does not exist`,
|
|
287
|
-
notFoundHint:
|
|
288
|
-
errorContext:
|
|
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:
|
|
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:
|
|
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 ===
|
|
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(
|
|
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:
|
|
346
|
+
method: 'PUT',
|
|
360
347
|
headers: {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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(
|
|
356
|
+
throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed with status ${String(res.status)}`);
|
|
370
357
|
}
|
|
371
|
-
etag = res.headers.get(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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:
|
|
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(
|
|
453
|
-
const fileName = displayPath.split(
|
|
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(
|
|
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(
|
|
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:
|
|
483
|
+
method: 'DELETE',
|
|
497
484
|
url,
|
|
498
|
-
headers: {
|
|
485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
499
486
|
body: JSON.stringify(reqBody),
|
|
500
|
-
}, { errorContext:
|
|
487
|
+
}, { errorContext: 'delete files' });
|
|
501
488
|
const data = extractEnvelope(body);
|
|
502
489
|
// 后端响应里只给出被删除成功的 attachments(无前导 /),没删掉的靠对比输入判断
|
|
503
|
-
const toDisplay = (p) => (p.startsWith(
|
|
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:
|
|
505
|
+
failed.push({ path: toDisplay(normalized), error: 'file not found or not deleted' });
|
|
519
506
|
}
|
|
520
507
|
}
|
|
521
508
|
return { deleted, failed };
|
package/dist/api/file/client.js
CHANGED
|
@@ -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?.(
|
|
23
|
+
const logid = response?.headers?.get?.('x-tt-logid') ?? '-';
|
|
24
24
|
if (err !== undefined) {
|
|
25
|
-
const errMsg = err instanceof Error ? err.message : typeof 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(
|
|
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:
|
|
85
|
-
message:
|
|
86
|
-
hint:
|
|
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:
|
|
90
|
-
message:
|
|
91
|
-
hint:
|
|
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 ??
|
|
99
|
-
if (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 ??
|
|
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 !==
|
|
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 ??
|
|
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 ??
|
|
145
|
-
const statusText = err.response?.statusText ??
|
|
146
|
-
throw new error_1.HttpError(status, err.config.url ??
|
|
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(
|
|
159
|
+
traceHttp('GET', url, start, response);
|
|
160
160
|
return (await response.json());
|
|
161
161
|
}
|
|
162
162
|
catch (err) {
|
|
163
|
-
traceHttp(
|
|
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(
|
|
173
|
+
traceHttp('POST', url, start, response);
|
|
174
174
|
return (await response.json());
|
|
175
175
|
}
|
|
176
176
|
catch (err) {
|
|
177
|
-
traceHttp(
|
|
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
|
}
|
package/dist/api/file/detect.js
CHANGED
|
@@ -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(
|
|
55
|
+
return input.startsWith('/') ? input : `/${input}`;
|
|
56
56
|
}
|
package/dist/api/file/parsers.js
CHANGED
|
@@ -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 ===
|
|
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 ===
|
|
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(
|
|
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
|
|
45
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
49
|
+
errPrefix: 'Failed to query analytics',
|
|
50
50
|
defaultErrCode: DEFAULT_ERR_CODE,
|
|
51
51
|
});
|
|
52
52
|
}
|