@lark-apaas/miaoda-cli 0.1.2 → 0.1.3-alpha.09899c4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/api/app/api.js +25 -0
- package/dist/api/app/index.js +15 -0
- package/dist/api/app/schemas.js +79 -0
- package/dist/api/app/types.js +58 -0
- package/dist/api/db/api.js +390 -55
- package/dist/api/db/client.js +65 -25
- 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 +60 -0
- package/dist/api/deploy/index.js +16 -0
- package/dist/api/deploy/schemas.js +105 -0
- package/dist/api/deploy/types.js +22 -0
- 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/index.js +7 -1
- package/dist/api/observability/api.js +52 -0
- package/dist/api/observability/index.js +16 -0
- package/dist/api/observability/schemas.js +60 -0
- package/dist/api/observability/types.js +27 -0
- package/dist/api/plugin/api.js +31 -31
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +600 -59
- package/dist/cli/commands/deploy/index.js +155 -0
- package/dist/cli/commands/file/index.js +91 -63
- package/dist/cli/commands/index.js +10 -6
- package/dist/cli/commands/observability/index.js +240 -0
- package/dist/cli/commands/plugin/index.js +27 -27
- package/dist/cli/commands/shared.js +86 -10
- package/dist/cli/handlers/app/get.js +47 -0
- package/dist/cli/handlers/app/index.js +7 -0
- package/dist/cli/handlers/app/update.js +59 -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 +34 -34
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +245 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +387 -0
- package/dist/cli/handlers/db/schema.js +35 -36
- package/dist/cli/handlers/db/sql.js +70 -71
- package/dist/cli/handlers/deploy/deploy.js +84 -0
- package/dist/cli/handlers/deploy/error-log.js +60 -0
- package/dist/cli/handlers/deploy/format.js +39 -0
- package/dist/cli/handlers/deploy/get.js +71 -0
- package/dist/cli/handlers/deploy/helpers.js +41 -0
- package/dist/cli/handlers/deploy/history.js +70 -0
- package/dist/cli/handlers/deploy/index.js +14 -0
- package/dist/cli/handlers/deploy/polling.js +162 -0
- package/dist/cli/handlers/file/cp.js +31 -32
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +6 -7
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +33 -32
- package/dist/cli/handlers/file/sign.js +4 -5
- package/dist/cli/handlers/file/stat.js +11 -11
- package/dist/cli/handlers/observability/analytics.js +212 -0
- package/dist/cli/handlers/observability/helpers.js +66 -0
- package/dist/cli/handlers/observability/index.js +12 -0
- package/dist/cli/handlers/observability/log.js +94 -0
- package/dist/cli/handlers/observability/metric.js +208 -0
- package/dist/cli/handlers/observability/trace.js +102 -0
- 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 +13 -9
- package/dist/utils/args.js +8 -0
- package/dist/utils/colors.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/error.js +2 -2
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +119 -1
- package/dist/utils/index.js +15 -1
- package/dist/utils/output.js +373 -20
- package/dist/utils/poll.js +35 -0
- package/dist/utils/render.js +27 -27
- package/dist/utils/spinner.js +46 -0
- package/dist/utils/time.js +208 -0
- package/package.json +7 -5
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.errorJobSchema = exports.deployGetSchema = exports.deployHistorySchema = void 0;
|
|
4
|
+
exports.nodeStatusText = nodeStatusText;
|
|
5
|
+
exports.nodeStatusFromText = nodeStatusFromText;
|
|
6
|
+
const output_1 = require("../../utils/output");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const NODE_STATUS_TEXT = {
|
|
9
|
+
[types_1.NodeStatus.UNSPECIFIED]: 'unknown',
|
|
10
|
+
[types_1.NodeStatus.TODO]: 'todo',
|
|
11
|
+
[types_1.NodeStatus.RUNNING]: 'running',
|
|
12
|
+
[types_1.NodeStatus.SUCCESS]: 'success',
|
|
13
|
+
[types_1.NodeStatus.FAILED]: 'failed',
|
|
14
|
+
[types_1.NodeStatus.CANCELED]: 'canceled',
|
|
15
|
+
[types_1.NodeStatus.HOLD_ON]: 'hold_on',
|
|
16
|
+
};
|
|
17
|
+
function nodeStatusText(v) {
|
|
18
|
+
if (typeof v === 'string')
|
|
19
|
+
return v;
|
|
20
|
+
if (typeof v !== 'number')
|
|
21
|
+
return undefined;
|
|
22
|
+
return NODE_STATUS_TEXT[v];
|
|
23
|
+
}
|
|
24
|
+
/** NodeStatus 文本(CLI flag 字符串)→ 枚举数值 */
|
|
25
|
+
function nodeStatusFromText(text) {
|
|
26
|
+
const lower = text.toLowerCase();
|
|
27
|
+
for (const [num, label] of Object.entries(NODE_STATUS_TEXT)) {
|
|
28
|
+
if (label === lower)
|
|
29
|
+
return Number(num);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function pipelineDurationMs(row) {
|
|
34
|
+
// SimpleInstance.createdAt / updatedAt:BAM IDL 未注单位,e2e 看是 ms。
|
|
35
|
+
const start = Number(row.createdAt);
|
|
36
|
+
const end = Number(row.updatedAt);
|
|
37
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
38
|
+
return undefined;
|
|
39
|
+
return end - start;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* deploy history 列表的 pretty 渲染契约。
|
|
43
|
+
*
|
|
44
|
+
* BAM 返回 SimpleInstance(pipeline 实例),状态走 NodeStatus。
|
|
45
|
+
* 默认列:deploy-id(=ID)/ status / creator / created-at / duration(updatedAt-createdAt)。
|
|
46
|
+
*/
|
|
47
|
+
exports.deployHistorySchema = {
|
|
48
|
+
columns: [
|
|
49
|
+
{ key: 'ID', label: 'deploy-id' },
|
|
50
|
+
{
|
|
51
|
+
key: 'status_text',
|
|
52
|
+
label: 'status',
|
|
53
|
+
derive: (row) => nodeStatusText(row.status),
|
|
54
|
+
},
|
|
55
|
+
{ key: 'creator', label: 'creator' },
|
|
56
|
+
{ key: 'createdAt', label: 'created-at', format: output_1.fmt.ms() },
|
|
57
|
+
{
|
|
58
|
+
key: 'duration',
|
|
59
|
+
label: 'duration',
|
|
60
|
+
format: output_1.fmt.durationMs(),
|
|
61
|
+
derive: pipelineDurationMs,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
strict: true,
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* deploy get 单条详情的 key-value 渲染契约(基于 SimpleInstance)。
|
|
68
|
+
*
|
|
69
|
+
* 由于 BAM 没有 by-ID 的查询接口,handler 用 listPipelineInstances 在最近窗口
|
|
70
|
+
* 里筛一条 SimpleInstance 透传过来。
|
|
71
|
+
*/
|
|
72
|
+
exports.deployGetSchema = {
|
|
73
|
+
columns: [
|
|
74
|
+
{ key: 'ID', label: 'deploy-id' },
|
|
75
|
+
{
|
|
76
|
+
key: 'status_text',
|
|
77
|
+
label: 'status',
|
|
78
|
+
derive: (row) => nodeStatusText(row.status),
|
|
79
|
+
},
|
|
80
|
+
{ key: 'creator', label: 'creator' },
|
|
81
|
+
{ key: 'updater', label: 'updater' },
|
|
82
|
+
{ key: 'createdAt', label: 'created-at', format: output_1.fmt.ms() },
|
|
83
|
+
{ key: 'updatedAt', label: 'updated-at', format: output_1.fmt.ms() },
|
|
84
|
+
{
|
|
85
|
+
key: 'duration',
|
|
86
|
+
label: 'duration',
|
|
87
|
+
format: output_1.fmt.durationMs(),
|
|
88
|
+
derive: pipelineDurationMs,
|
|
89
|
+
},
|
|
90
|
+
{ key: 'templateID', label: 'template' },
|
|
91
|
+
{ key: 'description' },
|
|
92
|
+
{ key: 'parameters' },
|
|
93
|
+
{ key: 'envVariables', label: 'env-vars' },
|
|
94
|
+
],
|
|
95
|
+
strict: true,
|
|
96
|
+
};
|
|
97
|
+
/** deploy error-log 表格渲染契约 */
|
|
98
|
+
exports.errorJobSchema = {
|
|
99
|
+
columns: [
|
|
100
|
+
{ key: 'jobID', label: 'job-id' },
|
|
101
|
+
{ key: 'componentName', label: 'component' },
|
|
102
|
+
{ key: 'errorMsg', label: 'error' },
|
|
103
|
+
],
|
|
104
|
+
strict: true,
|
|
105
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 与 BAM lark.apaas.devops v1.0.326 / 1.0.327 + lark.apaas.devops_platform v1.0.247 对齐:
|
|
3
|
+
// - CLICreateForceRelease (4070318) 创建发布 lark.apaas.devops
|
|
4
|
+
// - CliQueryPipelineInstance (4069872) 轮询发布状态 lark.apaas.devops_platform
|
|
5
|
+
// - CliListPipelineInstances (4073969) 分页获取发布历史 lark.apaas.devops_platform
|
|
6
|
+
// - CliGetToolInstanceErrorJobs (4073972) 获取发布错误日志 lark.apaas.devops_platform
|
|
7
|
+
//
|
|
8
|
+
// 发布单的唯一标识是 pipelineTaskID,对外即 deploy-id;error-log 等接口直接用它做
|
|
9
|
+
// URL 路径段(instanceID)。曾经的 Release/releaseID 概念已全面废弃。
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.NodeStatus = void 0;
|
|
12
|
+
/** Pipeline 节点状态(devops_platform) */
|
|
13
|
+
var NodeStatus;
|
|
14
|
+
(function (NodeStatus) {
|
|
15
|
+
NodeStatus[NodeStatus["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
16
|
+
NodeStatus[NodeStatus["TODO"] = 1] = "TODO";
|
|
17
|
+
NodeStatus[NodeStatus["RUNNING"] = 2] = "RUNNING";
|
|
18
|
+
NodeStatus[NodeStatus["SUCCESS"] = 3] = "SUCCESS";
|
|
19
|
+
NodeStatus[NodeStatus["FAILED"] = 4] = "FAILED";
|
|
20
|
+
NodeStatus[NodeStatus["CANCELED"] = 5] = "CANCELED";
|
|
21
|
+
NodeStatus[NodeStatus["HOLD_ON"] = 6] = "HOLD_ON";
|
|
22
|
+
})(NodeStatus || (exports.NodeStatus = NodeStatus = {}));
|
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
|
+
}
|