@lark-apaas/miaoda-cli 0.1.3 → 0.1.4
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/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 +12 -12
- package/dist/cli/commands/db/index.js +602 -54
- package/dist/cli/commands/deploy/index.js +28 -28
- package/dist/cli/commands/file/index.js +85 -58
- 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/_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/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/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/index.js +3 -1
- package/dist/utils/output.js +67 -45
- 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 +47 -42
- package/package.json +1 -1
|
@@ -55,7 +55,7 @@ function resolveQueryRouting(opts) {
|
|
|
55
55
|
if (!opts.query)
|
|
56
56
|
return explicit;
|
|
57
57
|
if (explicit.path || explicit.name) {
|
|
58
|
-
throw new error_1.AppError(
|
|
58
|
+
throw new error_1.AppError('ARGS_INVALID', 'Do not mix positional <query> with --path / --name; pick one.');
|
|
59
59
|
}
|
|
60
60
|
if ((0, index_1.looksLikePath)(opts.query)) {
|
|
61
61
|
// 补齐前导 `/`,对齐 ls sidecar 比较逻辑(info.path 永远带前导 `/`)
|
|
@@ -88,13 +88,13 @@ async function handleFileLs(opts) {
|
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
if (result.items.length === 0) {
|
|
91
|
-
(0, output_1.emit)(
|
|
91
|
+
(0, output_1.emit)('No files found.');
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
const tty = (0, render_1.isStdoutTty)();
|
|
95
|
-
const headers = [
|
|
95
|
+
const headers = ['file_name', 'path', 'size', 'type', 'uploaded_at'];
|
|
96
96
|
const rows = result.items.map((info) => [
|
|
97
|
-
info.file_name ||
|
|
97
|
+
info.file_name || '—',
|
|
98
98
|
info.path,
|
|
99
99
|
tty ? (0, render_1.formatSize)(info.size_bytes) : String(info.size_bytes),
|
|
100
100
|
info.type,
|
|
@@ -103,6 +103,6 @@ async function handleFileLs(opts) {
|
|
|
103
103
|
const table = tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows);
|
|
104
104
|
const hint = result.has_more && result.next_cursor
|
|
105
105
|
? `\n— ${String(result.items.length)} results. Next: --cursor ${result.next_cursor}`
|
|
106
|
-
:
|
|
106
|
+
: '';
|
|
107
107
|
(0, output_1.emit)(table + hint);
|
|
108
108
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleFileQuota = handleFileQuota;
|
|
37
|
+
const api = __importStar(require("../../../api/index"));
|
|
38
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
39
|
+
const output_1 = require("../../../utils/output");
|
|
40
|
+
const render_1 = require("../../../utils/render");
|
|
41
|
+
async function handleFileQuota(opts) {
|
|
42
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
43
|
+
const data = await api.file.getStorageQuota({ appId });
|
|
44
|
+
if ((0, output_1.isJsonMode)()) {
|
|
45
|
+
// 配额未对接(storageQuotaBytes=0)时,quota / usage_percent 字段都不输出
|
|
46
|
+
const out = {
|
|
47
|
+
storageUsedBytes: data.storageUsedBytes,
|
|
48
|
+
files: data.files,
|
|
49
|
+
};
|
|
50
|
+
if (data.storageQuotaBytes > 0) {
|
|
51
|
+
out.storageQuotaBytes = data.storageQuotaBytes;
|
|
52
|
+
out.usagePercent = data.usagePercent;
|
|
53
|
+
}
|
|
54
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)(out));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// PRD:单行 "Storage: 150 MB / 1 GB (15%)";配额未对接时只显示 used
|
|
58
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
59
|
+
const storageLine = data.storageQuotaBytes > 0
|
|
60
|
+
? `${(0, render_1.formatSize)(data.storageUsedBytes)} / ${(0, render_1.formatSize)(data.storageQuotaBytes)} (${data.usagePercent.toFixed(1)}%)`
|
|
61
|
+
: (0, render_1.formatSize)(data.storageUsedBytes);
|
|
62
|
+
(0, output_1.emit)((0, render_1.renderKeyValue)([
|
|
63
|
+
['Storage', storageLine],
|
|
64
|
+
['Files', String(data.files)],
|
|
65
|
+
], tty));
|
|
66
|
+
}
|
|
@@ -73,7 +73,7 @@ async function resolveDeleteInputs(appId, paths, names) {
|
|
|
73
73
|
// 1a. path 分支:直接进删除队列,file_name 取 basename 先占位
|
|
74
74
|
for (const p of positionalAsPath) {
|
|
75
75
|
const abs = (0, index_1.toAbsolutePath)(p);
|
|
76
|
-
const basename = abs.split(
|
|
76
|
+
const basename = abs.split('/').pop() ?? abs;
|
|
77
77
|
resolved.push({ input: p, path: abs, file_name: basename });
|
|
78
78
|
}
|
|
79
79
|
// 1b. name 分支(来自位置参数的非 path):和 --name 合并走 resolveInputs
|
|
@@ -82,10 +82,10 @@ async function resolveDeleteInputs(appId, paths, names) {
|
|
|
82
82
|
const results = await api.file.resolveInputs({
|
|
83
83
|
appId,
|
|
84
84
|
inputs: allNames,
|
|
85
|
-
forceAs:
|
|
85
|
+
forceAs: 'name',
|
|
86
86
|
});
|
|
87
87
|
for (const r of results) {
|
|
88
|
-
if (r.status ===
|
|
88
|
+
if (r.status === 'ok') {
|
|
89
89
|
resolved.push({
|
|
90
90
|
input: r.input,
|
|
91
91
|
path: r.file.path,
|
|
@@ -94,7 +94,7 @@ async function resolveDeleteInputs(appId, paths, names) {
|
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
96
|
errors.push({
|
|
97
|
-
status:
|
|
97
|
+
status: 'error',
|
|
98
98
|
input: r.input,
|
|
99
99
|
error: r.error,
|
|
100
100
|
});
|
|
@@ -104,11 +104,11 @@ async function resolveDeleteInputs(appId, paths, names) {
|
|
|
104
104
|
return { resolved, errors };
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* 删除前 TTY
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* `firstInput`
|
|
111
|
-
*
|
|
107
|
+
* 删除前 TTY 二次确认。文案强调"permanently"(删除不可撤销,无回收站):
|
|
108
|
+
* - 单文件:`? Are you sure you want to permanently delete '<input>'? (y/N)`
|
|
109
|
+
* - 多文件:`? Are you sure you want to permanently delete N files? (y/N)`
|
|
110
|
+
* `firstInput` 是用户传入的第一个值(path 或 file_name),单文件时直接展示给
|
|
111
|
+
* 用户核对目标。
|
|
112
112
|
*/
|
|
113
113
|
async function confirm(count, firstInput) {
|
|
114
114
|
const rl = node_readline_1.default.createInterface({
|
|
@@ -116,10 +116,12 @@ async function confirm(count, firstInput) {
|
|
|
116
116
|
output: process.stderr,
|
|
117
117
|
});
|
|
118
118
|
return new Promise((resolve) => {
|
|
119
|
-
const prompt = count === 1
|
|
119
|
+
const prompt = count === 1
|
|
120
|
+
? `? Are you sure you want to permanently delete '${firstInput}'? (y/N) `
|
|
121
|
+
: `? Are you sure you want to permanently delete ${String(count)} files? (y/N) `;
|
|
120
122
|
rl.question(prompt, (answer) => {
|
|
121
123
|
rl.close();
|
|
122
|
-
resolve(answer.trim().toLowerCase() ===
|
|
124
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
123
125
|
});
|
|
124
126
|
});
|
|
125
127
|
}
|
|
@@ -127,10 +129,10 @@ async function handleFileRm(paths, opts) {
|
|
|
127
129
|
const names = opts.name ?? [];
|
|
128
130
|
const totalCount = paths.length + names.length;
|
|
129
131
|
if (totalCount === 0) {
|
|
130
|
-
throw new error_1.AppError(
|
|
132
|
+
throw new error_1.AppError('ARGS_INVALID', 'No file specified (give a /path or --name <name>)');
|
|
131
133
|
}
|
|
132
134
|
if (totalCount > MAX_BATCH) {
|
|
133
|
-
throw new error_1.AppError(
|
|
135
|
+
throw new error_1.AppError('FILE_BATCH_TOO_MANY', `Batch size ${String(totalCount)} exceeds the 100 limit`);
|
|
134
136
|
}
|
|
135
137
|
const appId = opts.appId;
|
|
136
138
|
// destructive guardrail
|
|
@@ -141,11 +143,11 @@ async function handleFileRm(paths, opts) {
|
|
|
141
143
|
const firstInput = paths.length > 0 ? paths[0] : names[0];
|
|
142
144
|
const ok = await confirm(totalCount, firstInput);
|
|
143
145
|
if (!ok) {
|
|
144
|
-
throw new error_1.AppError(
|
|
146
|
+
throw new error_1.AppError('DESTRUCTIVE_CANCELLED', 'Cancelled by user');
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
else if (!tty && !opts.yes) {
|
|
148
|
-
throw new error_1.AppError(
|
|
150
|
+
throw new error_1.AppError('DESTRUCTIVE_REQUIRES_CONFIRM', 'This operation is destructive. Rerun with --yes to confirm.');
|
|
149
151
|
}
|
|
150
152
|
const { resolved, errors } = await resolveDeleteInputs(appId, paths, names);
|
|
151
153
|
// path → entry 映射(用于 delete 响应归档回原始 input 和 file_name)
|
|
@@ -167,9 +169,9 @@ async function handleFileRm(paths, opts) {
|
|
|
167
169
|
for (const p of deleted) {
|
|
168
170
|
const entry = pathToEntry.get(p);
|
|
169
171
|
results.push({
|
|
170
|
-
status:
|
|
172
|
+
status: 'ok',
|
|
171
173
|
input: entry?.input ?? p,
|
|
172
|
-
file_name: entry?.file_name ?? p.split(
|
|
174
|
+
file_name: entry?.file_name ?? p.split('/').pop() ?? p,
|
|
173
175
|
path: p,
|
|
174
176
|
});
|
|
175
177
|
}
|
|
@@ -181,31 +183,31 @@ async function handleFileRm(paths, opts) {
|
|
|
181
183
|
await api.file.statFile({ appId, filePath: f.path });
|
|
182
184
|
return {
|
|
183
185
|
input,
|
|
184
|
-
code:
|
|
185
|
-
message:
|
|
186
|
+
code: 'INTERNAL_ERROR',
|
|
187
|
+
message: 'Delete request returned success but file still exists (server-side issue)',
|
|
186
188
|
hint: undefined,
|
|
187
189
|
};
|
|
188
190
|
}
|
|
189
191
|
catch (err) {
|
|
190
|
-
if (err instanceof error_1.AppError && err.code ===
|
|
192
|
+
if (err instanceof error_1.AppError && err.code === 'FILE_NOT_FOUND') {
|
|
191
193
|
return {
|
|
192
194
|
input,
|
|
193
|
-
code:
|
|
195
|
+
code: 'FILE_NOT_FOUND',
|
|
194
196
|
message: `File '${input}' does not exist at delete time`,
|
|
195
|
-
hint:
|
|
197
|
+
hint: 'Run `miaoda file ls` to see available files.',
|
|
196
198
|
};
|
|
197
199
|
}
|
|
198
200
|
return {
|
|
199
201
|
input,
|
|
200
|
-
code:
|
|
201
|
-
message: err instanceof Error ? err.message :
|
|
202
|
+
code: 'INTERNAL_ERROR',
|
|
203
|
+
message: err instanceof Error ? err.message : 'verification failed',
|
|
202
204
|
hint: undefined,
|
|
203
205
|
};
|
|
204
206
|
}
|
|
205
207
|
}));
|
|
206
208
|
for (const c of classified) {
|
|
207
209
|
results.push({
|
|
208
|
-
status:
|
|
210
|
+
status: 'error',
|
|
209
211
|
input: c.input,
|
|
210
212
|
error: c.hint
|
|
211
213
|
? { code: c.code, message: c.message, hint: c.hint }
|
|
@@ -219,7 +221,7 @@ async function handleFileRm(paths, opts) {
|
|
|
219
221
|
names.forEach((n, i) => orderIndex.set(n, paths.length + i));
|
|
220
222
|
results = results.sort((a, b) => (orderIndex.get(a.input) ?? Number.MAX_SAFE_INTEGER) -
|
|
221
223
|
(orderIndex.get(b.input) ?? Number.MAX_SAFE_INTEGER));
|
|
222
|
-
const okCount = results.filter((r) => r.status ===
|
|
224
|
+
const okCount = results.filter((r) => r.status === 'ok').length;
|
|
223
225
|
const failCount = results.length - okCount;
|
|
224
226
|
if ((0, output_1.isJsonMode)()) {
|
|
225
227
|
(0, output_1.emit)({ data: results });
|
|
@@ -227,7 +229,7 @@ async function handleFileRm(paths, opts) {
|
|
|
227
229
|
else if (tty) {
|
|
228
230
|
const lines = [];
|
|
229
231
|
for (const r of results) {
|
|
230
|
-
if (r.status ===
|
|
232
|
+
if (r.status === 'ok')
|
|
231
233
|
lines.push(colors_1.c.success(`✓ Deleted ${r.input}`));
|
|
232
234
|
else
|
|
233
235
|
lines.push(colors_1.c.fail(`✗ ${r.input}: ${r.error.message}`));
|
|
@@ -238,12 +240,12 @@ async function handleFileRm(paths, opts) {
|
|
|
238
240
|
else {
|
|
239
241
|
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
|
|
240
242
|
}
|
|
241
|
-
(0, output_1.emit)(lines.join(
|
|
243
|
+
(0, output_1.emit)(lines.join('\n'));
|
|
242
244
|
}
|
|
243
245
|
else {
|
|
244
246
|
const lines = [];
|
|
245
247
|
for (const r of results) {
|
|
246
|
-
if (r.status ===
|
|
248
|
+
if (r.status === 'ok')
|
|
247
249
|
lines.push(`OK\t${r.input}`);
|
|
248
250
|
else
|
|
249
251
|
lines.push(`FAIL\t${r.input}\t${r.error.message}`);
|
|
@@ -254,7 +256,7 @@ async function handleFileRm(paths, opts) {
|
|
|
254
256
|
else {
|
|
255
257
|
lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
|
|
256
258
|
}
|
|
257
|
-
(0, output_1.emit)(lines.join(
|
|
259
|
+
(0, output_1.emit)(lines.join('\n'));
|
|
258
260
|
}
|
|
259
261
|
// 退出码:任一失败 → 1;全成功 → 0
|
|
260
262
|
if (failCount > 0) {
|
|
@@ -53,9 +53,9 @@ async function resolveFilePath(appId, input) {
|
|
|
53
53
|
const [resolved] = await api.file.resolveInputs({
|
|
54
54
|
appId,
|
|
55
55
|
inputs: [input],
|
|
56
|
-
forceAs:
|
|
56
|
+
forceAs: 'name',
|
|
57
57
|
});
|
|
58
|
-
if (resolved.status ===
|
|
58
|
+
if (resolved.status === 'error') {
|
|
59
59
|
throw new error_1.AppError(resolved.error.code, resolved.error.message, {
|
|
60
60
|
next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
|
|
61
61
|
});
|
|
@@ -68,7 +68,7 @@ async function handleFileSign(file, opts) {
|
|
|
68
68
|
if (opts.expires) {
|
|
69
69
|
expiresSec = (0, render_1.parseDuration)(opts.expires);
|
|
70
70
|
if (expiresSec > MAX_EXPIRES_SECONDS) {
|
|
71
|
-
throw new error_1.AppError(
|
|
71
|
+
throw new error_1.AppError('FILE_SIGN_DURATION_EXCEEDED', `Expires duration '${opts.expires}' exceeds the maximum of 30d`, {
|
|
72
72
|
next_actions: [
|
|
73
73
|
`Maximum allowed value is 30d. Use \`--expires 30d\` for the longest link.`,
|
|
74
74
|
],
|
|
@@ -52,9 +52,9 @@ async function resolveFile(appId, input) {
|
|
|
52
52
|
const [resolved] = await api.file.resolveInputs({
|
|
53
53
|
appId,
|
|
54
54
|
inputs: [input],
|
|
55
|
-
forceAs:
|
|
55
|
+
forceAs: 'name',
|
|
56
56
|
});
|
|
57
|
-
if (resolved.status ===
|
|
57
|
+
if (resolved.status === 'error') {
|
|
58
58
|
throw new error_1.AppError(resolved.error.code, resolved.error.message, {
|
|
59
59
|
next_actions: resolved.error.hint ? [resolved.error.hint] : undefined,
|
|
60
60
|
});
|
|
@@ -74,23 +74,24 @@ async function handleFileStat(file, opts) {
|
|
|
74
74
|
}
|
|
75
75
|
const tty = (0, render_1.isStdoutTty)();
|
|
76
76
|
const pairs = [
|
|
77
|
-
[
|
|
78
|
-
[
|
|
77
|
+
['file_name', info.file_name || '—'],
|
|
78
|
+
['path', info.path],
|
|
79
79
|
[
|
|
80
|
-
|
|
80
|
+
'size',
|
|
81
81
|
tty
|
|
82
82
|
? `${(0, render_1.formatSize)(info.size_bytes)} (${String(info.size_bytes)} bytes)`
|
|
83
83
|
: String(info.size_bytes),
|
|
84
84
|
],
|
|
85
|
-
[
|
|
86
|
-
[
|
|
85
|
+
['type', info.type || '—'],
|
|
86
|
+
['uploaded_at', (0, render_1.formatTime)(info.uploaded_at, tty)],
|
|
87
87
|
];
|
|
88
88
|
if (info.uploaded_by) {
|
|
89
|
+
// pretty 模式只展示 name;id 仅在 --json 下保留(避免 TTY 输出冒出一长串 userID)。
|
|
89
90
|
// uploaded_by 紧跟 uploaded_at 前插入(index = "uploaded_at" 之前)
|
|
90
|
-
pairs.splice(pairs.length - 1, 0, [
|
|
91
|
+
pairs.splice(pairs.length - 1, 0, ['uploaded_by', info.uploaded_by.name]);
|
|
91
92
|
}
|
|
92
93
|
if (info.download_url) {
|
|
93
|
-
pairs.push([
|
|
94
|
+
pairs.push(['download_url', info.download_url]);
|
|
94
95
|
}
|
|
95
96
|
(0, output_1.emit)((0, render_1.renderKeyValue)(pairs, tty));
|
|
96
97
|
}
|
|
@@ -39,17 +39,17 @@ const output_1 = require("../../../utils/output");
|
|
|
39
39
|
const args_1 = require("../../../utils/args");
|
|
40
40
|
const helpers_1 = require("./helpers");
|
|
41
41
|
const GRANULARITY_TO_UNIT = {
|
|
42
|
-
day:
|
|
43
|
-
daily:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
hour:
|
|
47
|
-
week:
|
|
48
|
-
weekly:
|
|
49
|
-
|
|
50
|
-
month:
|
|
51
|
-
monthly:
|
|
52
|
-
|
|
42
|
+
day: 'DAY',
|
|
43
|
+
daily: 'DAY',
|
|
44
|
+
'1d': 'DAY',
|
|
45
|
+
'1h': 'HOUR',
|
|
46
|
+
hour: 'HOUR',
|
|
47
|
+
week: 'WEEK',
|
|
48
|
+
weekly: 'WEEK',
|
|
49
|
+
'1w': 'WEEK',
|
|
50
|
+
month: 'MONTH',
|
|
51
|
+
monthly: 'MONTH',
|
|
52
|
+
'1M': 'MONTH',
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
55
|
* <analytics-name> + --series → metricType → 表头列名(=对应 --series 取值)。
|
|
@@ -60,26 +60,26 @@ const GRANULARITY_TO_UNIT = {
|
|
|
60
60
|
*/
|
|
61
61
|
const ANALYTICS_LABELS = {
|
|
62
62
|
users: {
|
|
63
|
-
ACTIVE_USER:
|
|
64
|
-
NEW_USER:
|
|
65
|
-
TOTAL_USER:
|
|
63
|
+
ACTIVE_USER: 'active-users',
|
|
64
|
+
NEW_USER: 'new-users',
|
|
65
|
+
TOTAL_USER: 'total-users',
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
/** miaoda observability analytics <analytics-name> */
|
|
69
69
|
async function handleObservabilityAnalytics(opts) {
|
|
70
70
|
if (!opts.analyticsName)
|
|
71
|
-
(0, args_1.failArgs)(
|
|
71
|
+
(0, args_1.failArgs)('<analytics-name> 必填');
|
|
72
72
|
const appID = opts.appId;
|
|
73
73
|
const { metricTypes, labelByMetric, extraFilters } = resolveAnalyticsSelection(opts.analyticsName, opts.series);
|
|
74
74
|
const timeAggregationUnit = opts.granularity
|
|
75
75
|
? (GRANULARITY_TO_UNIT[opts.granularity] ?? opts.granularity.toUpperCase())
|
|
76
|
-
:
|
|
76
|
+
: 'DAY';
|
|
77
77
|
const nowMs = Date.now();
|
|
78
78
|
const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
|
|
79
79
|
const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
|
|
80
80
|
const bucket = timeAggregationUnit;
|
|
81
81
|
const fieldFilters = (0, helpers_1.buildFieldFilters)([
|
|
82
|
-
{ key:
|
|
82
|
+
{ key: 'path', value: opts.page ? (0, helpers_1.eqFilter)(opts.page) : undefined },
|
|
83
83
|
...extraFilters,
|
|
84
84
|
]);
|
|
85
85
|
const req = {
|
|
@@ -131,7 +131,7 @@ function renameAndSort(points, labelByMetric) {
|
|
|
131
131
|
function buildAnalyticsPivotSchema(seriesLabels) {
|
|
132
132
|
return {
|
|
133
133
|
columns: [
|
|
134
|
-
{ key:
|
|
134
|
+
{ key: 'timestampNs', label: 'time', format: output_1.fmt.ns() },
|
|
135
135
|
...seriesLabels.map((label) => ({ key: label, label })),
|
|
136
136
|
],
|
|
137
137
|
strict: true,
|
|
@@ -150,27 +150,27 @@ function buildAnalyticsPivotSchema(seriesLabels) {
|
|
|
150
150
|
function resolveAnalyticsSelection(cliName, series) {
|
|
151
151
|
const extras = [];
|
|
152
152
|
const normalizedSeries = normalizeAnalyticsSeries(cliName, series);
|
|
153
|
-
if (cliName ===
|
|
154
|
-
if (normalizedSeries ===
|
|
155
|
-
return single(
|
|
156
|
-
if (normalizedSeries ===
|
|
157
|
-
return single(
|
|
158
|
-
if (normalizedSeries ===
|
|
159
|
-
return single(
|
|
153
|
+
if (cliName === 'users') {
|
|
154
|
+
if (normalizedSeries === 'active-users')
|
|
155
|
+
return single('ACTIVE_USER', 'active-users', extras);
|
|
156
|
+
if (normalizedSeries === 'new-users')
|
|
157
|
+
return single('NEW_USER', 'new-users', extras);
|
|
158
|
+
if (normalizedSeries === 'total-users')
|
|
159
|
+
return single('TOTAL_USER', 'total-users', extras);
|
|
160
160
|
// 缺省:三条线
|
|
161
161
|
return all(ANALYTICS_LABELS.users, extras);
|
|
162
162
|
}
|
|
163
|
-
if (cliName ===
|
|
164
|
-
if (normalizedSeries ===
|
|
165
|
-
extras.push({ key:
|
|
166
|
-
return single(
|
|
163
|
+
if (cliName === 'page-view') {
|
|
164
|
+
if (normalizedSeries === 'desktop-view') {
|
|
165
|
+
extras.push({ key: 'device_type', value: (0, helpers_1.eqFilter)('desktop') });
|
|
166
|
+
return single('PAGE_VIEW', 'desktop-view', extras);
|
|
167
167
|
}
|
|
168
|
-
if (normalizedSeries ===
|
|
169
|
-
extras.push({ key:
|
|
170
|
-
return single(
|
|
168
|
+
if (normalizedSeries === 'mobile-view') {
|
|
169
|
+
extras.push({ key: 'device_type', value: (0, helpers_1.eqFilter)('mobile') });
|
|
170
|
+
return single('PAGE_VIEW', 'mobile-view', extras);
|
|
171
171
|
}
|
|
172
172
|
// 缺省 / all-view → 不附加 device 过滤
|
|
173
|
-
return single(
|
|
173
|
+
return single('PAGE_VIEW', 'all-view', extras);
|
|
174
174
|
}
|
|
175
175
|
// 兜底:CLI 名直接当 metricType + label
|
|
176
176
|
return single(cliName, cliName, extras);
|
|
@@ -178,21 +178,21 @@ function resolveAnalyticsSelection(cliName, series) {
|
|
|
178
178
|
function normalizeAnalyticsSeries(cliName, series) {
|
|
179
179
|
if (series === undefined)
|
|
180
180
|
return undefined;
|
|
181
|
-
if (cliName ===
|
|
182
|
-
if (series ===
|
|
183
|
-
return
|
|
184
|
-
if (series ===
|
|
185
|
-
return
|
|
186
|
-
if (series ===
|
|
187
|
-
return
|
|
181
|
+
if (cliName === 'users') {
|
|
182
|
+
if (series === 'active')
|
|
183
|
+
return 'active-users';
|
|
184
|
+
if (series === 'new')
|
|
185
|
+
return 'new-users';
|
|
186
|
+
if (series === 'total')
|
|
187
|
+
return 'total-users';
|
|
188
188
|
}
|
|
189
|
-
if (cliName ===
|
|
190
|
-
if (series ===
|
|
191
|
-
return
|
|
192
|
-
if (series ===
|
|
193
|
-
return
|
|
194
|
-
if (series ===
|
|
195
|
-
return
|
|
189
|
+
if (cliName === 'page-view') {
|
|
190
|
+
if (series === 'all')
|
|
191
|
+
return 'all-view';
|
|
192
|
+
if (series === 'desktop')
|
|
193
|
+
return 'desktop-view';
|
|
194
|
+
if (series === 'mobile')
|
|
195
|
+
return 'mobile-view';
|
|
196
196
|
}
|
|
197
197
|
return series;
|
|
198
198
|
}
|
|
@@ -23,8 +23,8 @@ Object.defineProperty(exports, "ceilMsToBucket", { enumerable: true, get: functi
|
|
|
23
23
|
// BAM FieldFilter 是「类型分桶 + 算子」结构(str/i64/double),不是扁平字段;
|
|
24
24
|
// 字符串字段无 server-side contains,模糊匹配走 fuzzyFilter。
|
|
25
25
|
/** 字符串字段等值过滤 → { str: { eq } } */
|
|
26
|
-
function eqFilter(value, type =
|
|
27
|
-
const v = type !==
|
|
26
|
+
function eqFilter(value, type = 'str') {
|
|
27
|
+
const v = type !== 'str' ? Number(value) : value;
|
|
28
28
|
return { [type]: { eq: v } };
|
|
29
29
|
}
|
|
30
30
|
/** i64 数值范围过滤 → { i64: { gte, lte } };输入接受 string(CLI flag)或 number */
|
|
@@ -41,7 +41,7 @@ const helpers_1 = require("./helpers");
|
|
|
41
41
|
/** miaoda observability log */
|
|
42
42
|
async function handleObservabilityLog(opts) {
|
|
43
43
|
const appID = opts.appId;
|
|
44
|
-
const appEnv =
|
|
44
|
+
const appEnv = 'runtime';
|
|
45
45
|
const limit = (0, helpers_1.validateLimit)(opts.limit ?? 50);
|
|
46
46
|
// 过滤 key 对齐 BAM 查询字段名:
|
|
47
47
|
// - 顶层字段:severity_text / trace_id(输出仍映射成 severityText / traceID)
|
|
@@ -49,16 +49,16 @@ async function handleObservabilityLog(opts) {
|
|
|
49
49
|
// 依据 BAM IDL 的 LogItem.attributes 描述:"包括 tenant_id, module, user_id, page, api 等")
|
|
50
50
|
// - duration_ms 暂未在 BAM 描述里明确列出,留 TODO 等 e2e 验证
|
|
51
51
|
const fieldFilters = (0, helpers_1.buildFieldFilters)([
|
|
52
|
-
{ key:
|
|
53
|
-
{ key:
|
|
54
|
-
{ key:
|
|
55
|
-
{ key:
|
|
56
|
-
{ key:
|
|
57
|
-
{ key:
|
|
58
|
-
{ key:
|
|
52
|
+
{ key: 'severity_text', value: opts.level ? (0, helpers_1.eqFilter)(opts.level) : undefined },
|
|
53
|
+
{ key: 'attributes.ob_data_id', value: opts.logId ? (0, helpers_1.eqFilter)(opts.logId) : undefined },
|
|
54
|
+
{ key: 'trace_id', value: opts.traceId ? (0, helpers_1.eqFilter)(opts.traceId) : undefined },
|
|
55
|
+
{ key: 'module', value: opts.module ? (0, helpers_1.eqFilter)(opts.module) : undefined },
|
|
56
|
+
{ key: 'user_id', value: opts.userId ? (0, helpers_1.eqFilter)(opts.userId, 'i64') : undefined },
|
|
57
|
+
{ key: 'page', value: opts.page ? (0, helpers_1.eqFilter)(opts.page) : undefined },
|
|
58
|
+
{ key: 'api', value: opts.api ? (0, helpers_1.eqFilter)(opts.api) : undefined },
|
|
59
59
|
{
|
|
60
60
|
// TODO: 确认 BAM 是否真用 duration_ms 作为 attribute key
|
|
61
|
-
key:
|
|
61
|
+
key: 'duration_ms',
|
|
62
62
|
value: opts.minDuration !== undefined || opts.maxDuration !== undefined
|
|
63
63
|
? (0, helpers_1.rangeFilter)({ gte: opts.minDuration, lte: opts.maxDuration })
|
|
64
64
|
: undefined,
|