@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.
- package/dist/api/app/api.js +3 -3
- package/dist/api/app/schemas.js +43 -43
- package/dist/api/db/api.js +398 -55
- package/dist/api/db/client.js +155 -28
- package/dist/api/db/index.js +12 -1
- 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/index.js +12 -1
- package/dist/api/deploy/modern-types.js +23 -0
- package/dist/api/deploy/modern.js +70 -0
- package/dist/api/deploy/plugin-instances-types.js +6 -0
- package/dist/api/deploy/plugin-instances.js +30 -0
- package/dist/api/deploy/schemas.js +32 -32
- package/dist/api/file/api.js +89 -87
- package/dist/api/file/client.js +62 -22
- package/dist/api/file/detect.js +3 -3
- package/dist/api/file/index.js +2 -1
- 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 +94 -13
- package/dist/cli/commands/db/index.js +602 -54
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/deploy/modern.js +48 -0
- package/dist/cli/commands/file/index.js +85 -58
- package/dist/cli/commands/index.js +31 -5
- 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/index.js +6 -1
- package/dist/cli/handlers/app/init.js +86 -0
- package/dist/cli/handlers/app/update.js +2 -2
- package/dist/cli/handlers/db/_destructive.js +67 -0
- package/dist/cli/handlers/db/_env.js +26 -0
- package/dist/cli/handlers/db/_operator.js +35 -0
- package/dist/cli/handlers/db/audit.js +383 -0
- package/dist/cli/handlers/db/changelog.js +160 -0
- package/dist/cli/handlers/db/data.js +32 -31
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +234 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +413 -0
- 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/modern.js +32 -0
- package/dist/cli/handlers/deploy/polling.js +11 -11
- package/dist/cli/handlers/file/cp.js +30 -30
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +5 -5
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +32 -30
- 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/services/app/init/index.js +14 -0
- package/dist/services/app/init/install.js +101 -0
- package/dist/services/app/init/steering.js +74 -0
- package/dist/services/app/init/template.js +85 -0
- package/dist/services/deploy/modern/atoms/build.js +54 -0
- package/dist/services/deploy/modern/atoms/context.js +30 -0
- package/dist/services/deploy/modern/atoms/index.js +17 -0
- package/dist/services/deploy/modern/atoms/local-release.js +27 -0
- package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
- package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
- package/dist/services/deploy/modern/atoms/upload.js +192 -0
- package/dist/services/deploy/modern/check.js +58 -0
- package/dist/services/deploy/modern/constants.js +13 -0
- package/dist/services/deploy/modern/index.js +16 -0
- package/dist/services/deploy/modern/pipelines/index.js +5 -0
- package/dist/services/deploy/modern/pipelines/local.js +75 -0
- package/dist/services/deploy/modern/protocol.js +43 -0
- package/dist/services/deploy/modern/run-types.js +4 -0
- package/dist/services/deploy/modern/run.js +13 -0
- package/dist/services/deploy/modern/template-key-map.js +29 -0
- 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 +27 -21
- package/dist/utils/index.js +3 -1
- package/dist/utils/npm-pack.js +55 -0
- package/dist/utils/output.js +67 -45
- package/dist/utils/poll.js +35 -0
- package/dist/utils/render.js +27 -27
- package/dist/utils/spark-meta.js +48 -0
- package/dist/utils/spinner.js +46 -0
- package/dist/utils/time.js +47 -42
- package/package.json +1 -1
package/dist/api/file/api.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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:
|
|
75
|
+
conds.push({ field: 'name', operator: 'eq', value: opts.name });
|
|
74
76
|
}
|
|
75
77
|
if (opts.type) {
|
|
76
|
-
conds.push({ field:
|
|
78
|
+
conds.push({ field: 'type', operator: 'eq', value: opts.type });
|
|
77
79
|
}
|
|
78
80
|
if (opts.sizeGt !== undefined) {
|
|
79
|
-
conds.push({ field:
|
|
81
|
+
conds.push({ field: 'size', operator: 'gt', value: String(opts.sizeGt) });
|
|
80
82
|
}
|
|
81
83
|
if (opts.sizeLt !== undefined) {
|
|
82
|
-
conds.push({ field:
|
|
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,
|
|
87
|
-
conds.push({ field:
|
|
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,
|
|
91
|
-
conds.push({ field:
|
|
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:
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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) ?
|
|
212
|
-
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);
|
|
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:
|
|
209
|
+
return { status: 'ok', input, file: info };
|
|
222
210
|
}
|
|
223
211
|
catch (err) {
|
|
224
|
-
if (err instanceof error_1.AppError && err.code ===
|
|
212
|
+
if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
|
|
225
213
|
return {
|
|
226
|
-
status:
|
|
214
|
+
status: 'error',
|
|
227
215
|
input,
|
|
228
216
|
error: {
|
|
229
|
-
code:
|
|
217
|
+
code: 'FILE_NOT_FOUND',
|
|
230
218
|
message: `File '${input}' does not exist`,
|
|
231
|
-
hint:
|
|
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:
|
|
242
|
-
groups: [{ conditions: [{ field:
|
|
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(
|
|
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:
|
|
240
|
+
status: 'error',
|
|
253
241
|
input,
|
|
254
242
|
error: {
|
|
255
|
-
code:
|
|
243
|
+
code: 'FILE_NOT_FOUND',
|
|
256
244
|
message: `File '${input}' does not exist`,
|
|
257
|
-
hint:
|
|
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:
|
|
251
|
+
status: 'error',
|
|
264
252
|
input,
|
|
265
253
|
error: {
|
|
266
|
-
code:
|
|
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:
|
|
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:
|
|
272
|
+
notFoundCode: 'FILE_NOT_FOUND',
|
|
285
273
|
notFoundMessage: `File '${opts.filePath}' does not exist`,
|
|
286
|
-
notFoundHint:
|
|
287
|
-
errorContext:
|
|
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:
|
|
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:
|
|
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 ===
|
|
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(
|
|
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:
|
|
346
|
+
method: 'PUT',
|
|
359
347
|
headers: {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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(
|
|
356
|
+
throw new error_1.AppError('FILE_UPLOAD_FAILED', `Upload PUT failed with status ${String(res.status)}`);
|
|
369
357
|
}
|
|
370
|
-
etag = res.headers.get(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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:
|
|
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(
|
|
452
|
-
const fileName = displayPath.split(
|
|
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(
|
|
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(
|
|
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:
|
|
483
|
+
method: 'DELETE',
|
|
496
484
|
url,
|
|
497
|
-
headers: {
|
|
485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
498
486
|
body: JSON.stringify(reqBody),
|
|
499
|
-
}, { errorContext:
|
|
487
|
+
}, { errorContext: 'delete files' });
|
|
500
488
|
const data = extractEnvelope(body);
|
|
501
489
|
// 后端响应里只给出被删除成功的 attachments(无前导 /),没删掉的靠对比输入判断
|
|
502
|
-
const toDisplay = (p) => (p.startsWith(
|
|
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:
|
|
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
|
+
}
|
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,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:
|
|
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
|
+
},
|
|
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 ??
|
|
99
|
-
if (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 ??
|
|
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 !==
|
|
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 ??
|
|
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 ??
|
|
145
|
-
const statusText = err.response?.statusText ??
|
|
146
|
-
throw new error_1.HttpError(status, err.config.url ??
|
|
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(
|
|
199
|
+
traceHttp('GET', url, start, response);
|
|
160
200
|
return (await response.json());
|
|
161
201
|
}
|
|
162
202
|
catch (err) {
|
|
163
|
-
traceHttp(
|
|
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(
|
|
213
|
+
traceHttp('POST', url, start, response);
|
|
174
214
|
return (await response.json());
|
|
175
215
|
}
|
|
176
216
|
catch (err) {
|
|
177
|
-
traceHttp(
|
|
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
|
}
|
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/index.js
CHANGED
|
@@ -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; } });
|