@lark-apaas/miaoda-cli 0.1.2-alpha.c7d0c89 → 0.1.2-alpha.ccfdc05
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/deploy/schemas.js +1 -1
- package/dist/api/observability/schemas.js +22 -1
- package/dist/cli/commands/deploy/index.js +17 -11
- package/dist/cli/commands/index.js +10 -12
- package/dist/cli/commands/observability/index.js +20 -10
- package/dist/cli/commands/shared.js +44 -0
- package/dist/cli/handlers/observability/analytics.js +27 -5
- package/dist/cli/handlers/observability/metric.js +1 -1
- package/dist/cli/version.js +15 -0
- package/dist/main.js +27 -7
- package/dist/utils/http.js +88 -2
- package/dist/utils/time.js +82 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,22 +14,22 @@ pnpm add -g @lark-apaas/miaoda-cli
|
|
|
14
14
|
# 设置默认应用(需要应用上下文的命令可用 --app-id 覆盖)
|
|
15
15
|
export MIAODA_APP_ID=app_demo_xxx
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
miaoda
|
|
19
|
-
miaoda
|
|
17
|
+
# 文件操作
|
|
18
|
+
miaoda file ls
|
|
19
|
+
miaoda file upload ./local/path
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
JSON 结构化输出(Agent 推荐):
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# 输出全部字段
|
|
26
|
-
miaoda
|
|
26
|
+
miaoda file ls --json
|
|
27
27
|
|
|
28
28
|
# 可选字段级选择
|
|
29
|
-
miaoda
|
|
29
|
+
miaoda file ls --json id,name
|
|
30
30
|
|
|
31
31
|
# 或通过 --output 指定格式
|
|
32
|
-
miaoda
|
|
32
|
+
miaoda file ls --output json
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## 命令树
|
|
@@ -38,7 +38,8 @@ miaoda plugin list --output json
|
|
|
38
38
|
|
|
39
39
|
| 域 | 用途 |
|
|
40
40
|
|---|---|
|
|
41
|
-
| `miaoda
|
|
41
|
+
| `miaoda file ...` | 文件操作:上传、下载、元数据、签名下载、批量删除 |
|
|
42
|
+
| `miaoda db ...` | 数据操作:SQL 执行、表结构查询、数据导入导出 |
|
|
42
43
|
|
|
43
44
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
44
45
|
|
|
@@ -97,7 +97,7 @@ exports.errorJobSchema = {
|
|
|
97
97
|
columns: [
|
|
98
98
|
{ key: "jobID", label: "job-id" },
|
|
99
99
|
{ key: "componentName", label: "component" },
|
|
100
|
-
{ key: "errorMsg", label: "error"
|
|
100
|
+
{ key: "errorMsg", label: "error" },
|
|
101
101
|
],
|
|
102
102
|
strict: true,
|
|
103
103
|
};
|
|
@@ -6,7 +6,28 @@ const output_1 = require("../../utils/output");
|
|
|
6
6
|
exports.logItemSchema = {
|
|
7
7
|
columns: [
|
|
8
8
|
{ key: "timestampNs", label: "time", format: output_1.fmt.ns("yyyy-MM-dd HH:mm:ss.SSS") },
|
|
9
|
-
{
|
|
9
|
+
{
|
|
10
|
+
key: "module",
|
|
11
|
+
derive: (row) => {
|
|
12
|
+
return row.attributes?.module;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: "user-id",
|
|
17
|
+
derive: (row) => {
|
|
18
|
+
return row.attributes?.user_id;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{ key: "severityText", label: "severity-text" },
|
|
22
|
+
{
|
|
23
|
+
key: "duration",
|
|
24
|
+
format: output_1.fmt.durationMs(),
|
|
25
|
+
derive: (row) => {
|
|
26
|
+
return row.attributes?.duration_ms;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ key: "traceID", label: "trace-id" },
|
|
30
|
+
{ key: "id", label: "log-id" },
|
|
10
31
|
{ key: "body" },
|
|
11
32
|
],
|
|
12
33
|
strict: true,
|
|
@@ -6,9 +6,13 @@ const shared_1 = require("../../../cli/commands/shared");
|
|
|
6
6
|
const index_1 = require("../../../cli/handlers/deploy/index");
|
|
7
7
|
const COMMON_TIME_HELP = `
|
|
8
8
|
时间格式:
|
|
9
|
-
- 相对时间:
|
|
10
|
-
- 日期:2026-04-01
|
|
11
|
-
-
|
|
9
|
+
- 相对时间:30m(30 分钟前)、1h、2d、1w
|
|
10
|
+
- 日期:2026-04-01(本地时区当日 00:00:00)
|
|
11
|
+
- 本地日期+时间:2026-04-01T10:00:00(按本地时区,T 分隔)
|
|
12
|
+
- 带时区 ISO:2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)
|
|
13
|
+
备注:
|
|
14
|
+
1) 不带时区的形式与 pretty 输出闭环(同机器复制粘贴稳定);跨机器请带显式时区。
|
|
15
|
+
2) 必须用 T 分隔日期与时间,禁止空格——shell 会把不带引号的 'YYYY-MM-DD HH:mm:ss' 拆成两个参数。
|
|
12
16
|
`;
|
|
13
17
|
function registerDeployCommands(program) {
|
|
14
18
|
// PRD:`miaoda deploy` 自身即为触发发布;get / history / error-log 是子命令
|
|
@@ -20,6 +24,12 @@ function registerDeployCommands(program) {
|
|
|
20
24
|
.option("--wait", "阻塞直到流水线终态", false)
|
|
21
25
|
.option("--timeout <sec>", "--wait 最长等待秒数(默认 300)", parseTimeout, 300)
|
|
22
26
|
.addHelpText("after", `
|
|
27
|
+
部署前置检查(Agent 必须执行)
|
|
28
|
+
miaoda deploy 跑的是当前分支的远端 HEAD——本地未 commit / 未 push 的代码不会进发布产物。
|
|
29
|
+
调用前先 \`git status\` + \`git rev-list --count @{u}..HEAD\` 确认;有未提交 / 未推送时:
|
|
30
|
+
- 是本次任务的产物 → 先 commit + push 再 deploy
|
|
31
|
+
- 不能确定意图 → 用 AskUser 跟用户确认(不要默默继续)
|
|
32
|
+
|
|
23
33
|
JSON 输出
|
|
24
34
|
不带 --wait:{"data": {"pipelineTaskID": "..."}}(pipelineTaskID 即 deploy-id)
|
|
25
35
|
带 --wait:data 额外包含 detail(pipeline 终态)
|
|
@@ -70,14 +80,9 @@ function registerDeployHistory(parent) {
|
|
|
70
80
|
.command("history")
|
|
71
81
|
.description("查询发布历史(按时间倒序,分页)")
|
|
72
82
|
.addOption((0, shared_1.appIdOption)().hideHelp())
|
|
73
|
-
.addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline
|
|
74
|
-
"todo",
|
|
75
|
-
"running",
|
|
76
|
-
"success",
|
|
77
|
-
"failed",
|
|
78
|
-
"canceled",
|
|
79
|
-
"hold_on",
|
|
80
|
-
]))
|
|
83
|
+
.addOption(new commander_1.Option("--status <status>", "状态过滤(pipeline 节点状态,大小写不敏感)")
|
|
84
|
+
.choices(["todo", "running", "success", "failed", "canceled", "hold_on"])
|
|
85
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["todo", "running", "success", "failed", "canceled", "hold_on"])))
|
|
81
86
|
.option("--since <time>", "起始时间")
|
|
82
87
|
.option("--until <time>", "截止时间")
|
|
83
88
|
.option("--limit <n>", "返回条数上限(1~100)", parseLimit, 50)
|
|
@@ -91,6 +96,7 @@ JSON 输出
|
|
|
91
96
|
$ miaoda deploy history --since 7d
|
|
92
97
|
`);
|
|
93
98
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
99
|
+
(0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
|
|
94
100
|
(0, shared_1.rejectCliOverride)(cmd, "appId");
|
|
95
101
|
await (0, index_1.handleDeployHistory)({
|
|
96
102
|
appId: rawOpts.appId,
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerCommands = registerCommands;
|
|
4
|
-
const index_1 = require("../../cli/commands/
|
|
5
|
-
const index_2 = require("../../cli/commands/
|
|
6
|
-
const index_3 = require("../../cli/commands/
|
|
7
|
-
const index_4 = require("../../cli/commands/
|
|
8
|
-
const index_5 = require("../../cli/commands/
|
|
9
|
-
const index_6 = require("../../cli/commands/deploy/index");
|
|
4
|
+
const index_1 = require("../../cli/commands/file/index");
|
|
5
|
+
const index_2 = require("../../cli/commands/db/index");
|
|
6
|
+
const index_3 = require("../../cli/commands/observability/index");
|
|
7
|
+
const index_4 = require("../../cli/commands/app/index");
|
|
8
|
+
const index_5 = require("../../cli/commands/deploy/index");
|
|
10
9
|
function registerCommands(program) {
|
|
11
|
-
(0, index_1.
|
|
12
|
-
(0, index_2.
|
|
13
|
-
(0, index_3.
|
|
14
|
-
(0, index_4.
|
|
15
|
-
(0, index_5.
|
|
16
|
-
(0, index_6.registerDeployCommands)(program);
|
|
10
|
+
(0, index_1.registerFileCommands)(program);
|
|
11
|
+
(0, index_2.registerDbCommands)(program);
|
|
12
|
+
(0, index_3.registerObservabilityCommands)(program);
|
|
13
|
+
(0, index_4.registerAppCommands)(program);
|
|
14
|
+
(0, index_5.registerDeployCommands)(program);
|
|
17
15
|
}
|
|
@@ -6,9 +6,13 @@ const shared_1 = require("../../../cli/commands/shared");
|
|
|
6
6
|
const index_1 = require("../../../cli/handlers/observability/index");
|
|
7
7
|
const COMMON_TIME_HELP = `
|
|
8
8
|
时间格式:
|
|
9
|
-
- 相对时间:
|
|
10
|
-
- 日期:2026-04-01
|
|
11
|
-
-
|
|
9
|
+
- 相对时间:30m(30 分钟前)、1h、2d、1w
|
|
10
|
+
- 日期:2026-04-01(本地时区当日 00:00:00)
|
|
11
|
+
- 本地日期+时间:2026-04-01T10:00:00(按本地时区,T 分隔)
|
|
12
|
+
- 带时区 ISO:2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)
|
|
13
|
+
备注:
|
|
14
|
+
1) 不带时区的形式与 pretty 输出闭环(同机器复制粘贴稳定);跨机器请带显式时区。
|
|
15
|
+
2) 必须用 T 分隔日期与时间,禁止空格——shell 会把不带引号的 'YYYY-MM-DD HH:mm:ss' 拆成两个参数。
|
|
12
16
|
`;
|
|
13
17
|
function registerObservabilityCommands(program) {
|
|
14
18
|
const obCmd = program
|
|
@@ -23,8 +27,7 @@ function registerObservabilityCommands(program) {
|
|
|
23
27
|
应用上下文:--app-id <id> 或环境变量 MIAODA_APP_ID。
|
|
24
28
|
|
|
25
29
|
应用环境
|
|
26
|
-
|
|
27
|
-
metric/analytics 不需要 --env。
|
|
30
|
+
目前只支持线上环境。
|
|
28
31
|
`);
|
|
29
32
|
registerLog(obCmd);
|
|
30
33
|
registerTrace(obCmd);
|
|
@@ -37,7 +40,9 @@ function registerLog(parent) {
|
|
|
37
40
|
.command("log")
|
|
38
41
|
.description("查询线上运行日志(按时间倒序,分页)")
|
|
39
42
|
.addOption((0, shared_1.appIdOption)().hideHelp())
|
|
40
|
-
.addOption(new commander_1.Option("--level <level>", "
|
|
43
|
+
.addOption(new commander_1.Option("--level <level>", "日志级别(大小写不敏感)")
|
|
44
|
+
.choices(["DEBUG", "INFO", "WARN", "ERROR"])
|
|
45
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["DEBUG", "INFO", "WARN", "ERROR"])))
|
|
41
46
|
.option("--since <time>", "开始时间")
|
|
42
47
|
.option("--until <time>", "截止时间")
|
|
43
48
|
.option("--trace-id <id>", "按 trace ID 过滤")
|
|
@@ -60,6 +65,7 @@ JSON 输出
|
|
|
60
65
|
$ miaoda observability log --api /api/orders --min-duration 200 --json
|
|
61
66
|
`);
|
|
62
67
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
68
|
+
(0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
|
|
63
69
|
(0, shared_1.rejectCliOverride)(cmd, "appId");
|
|
64
70
|
await (0, index_1.handleObservabilityLog)({
|
|
65
71
|
appId: rawOpts.appId,
|
|
@@ -105,6 +111,7 @@ JSON 输出
|
|
|
105
111
|
$ miaoda observability trace list --root-span api-gateway --limit 20 --json
|
|
106
112
|
`);
|
|
107
113
|
listCmd.action((0, shared_1.withHelp)(listCmd, async (rawOpts) => {
|
|
114
|
+
(0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
|
|
108
115
|
(0, shared_1.rejectCliOverride)(listCmd, "appId");
|
|
109
116
|
await (0, index_1.handleObservabilityTraceList)({
|
|
110
117
|
appId: rawOpts.appId,
|
|
@@ -156,9 +163,10 @@ function registerMetric(parent) {
|
|
|
156
163
|
.option("--series <name>", "过滤图表中的某条线:latency 取 p50/p99;requests 取 total/error;缺省返回所有相关线")
|
|
157
164
|
.option("--since <time>", "开始时间")
|
|
158
165
|
.option("--until <time>", "截止时间")
|
|
159
|
-
.addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1
|
|
166
|
+
.addOption(new commander_1.Option("--down-sample <duration>", "降采样粒度(1m=1分钟 / 1h=1小时 / 1d=1天,大小写不敏感)")
|
|
160
167
|
.choices(["1m", "1h", "1d"])
|
|
161
|
-
.
|
|
168
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["1m", "1h", "1d"]))
|
|
169
|
+
.default("1m"))
|
|
162
170
|
.addHelpText("after", `${COMMON_TIME_HELP}
|
|
163
171
|
JSON 输出
|
|
164
172
|
{"data": [{"metricName": "...", "dimensions": {...}, "dataPoints": [...]}], "next_cursor": null, "has_more": false}
|
|
@@ -170,6 +178,7 @@ JSON 输出
|
|
|
170
178
|
$ miaoda observability metric cpu --since 1d --down-sample 1d --json
|
|
171
179
|
`);
|
|
172
180
|
cmd.action((0, shared_1.withHelp)(cmd, async (metricName, rawOpts) => {
|
|
181
|
+
(0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
|
|
173
182
|
(0, shared_1.rejectCliOverride)(cmd, "appId");
|
|
174
183
|
await (0, index_1.handleObservabilityMetric)({
|
|
175
184
|
metricName,
|
|
@@ -191,7 +200,7 @@ function registerAnalytics(parent) {
|
|
|
191
200
|
.argument("<analytics-name>", "指标名:users | page-view")
|
|
192
201
|
.addOption((0, shared_1.appIdOption)().hideHelp())
|
|
193
202
|
.option("--page <path>", "按页面路径过滤(仅对 page-view 生效)")
|
|
194
|
-
.option("--series <name>", "过滤图表中的某条线:users 取 active-users/new-users/total-users
|
|
203
|
+
.option("--series <name>", "过滤图表中的某条线:users 取 active/new/total(兼容 active-users/new-users/total-users);page-view 取 all/desktop/mobile(兼容 all-view/desktop-view/mobile-view)")
|
|
195
204
|
.option("--since <time>", "开始时间")
|
|
196
205
|
.option("--until <time>", "截止时间")
|
|
197
206
|
.option("--granularity <duration>", "时间粒度:day | week | month", "day")
|
|
@@ -201,10 +210,11 @@ JSON 输出
|
|
|
201
210
|
|
|
202
211
|
示例
|
|
203
212
|
$ miaoda observability analytics users --json # 同时返回 active/new/total 三条线
|
|
204
|
-
$ miaoda observability analytics users --series new
|
|
213
|
+
$ miaoda observability analytics users --series new --since 7d --json
|
|
205
214
|
$ miaoda observability analytics page-view --granularity week --json
|
|
206
215
|
`);
|
|
207
216
|
cmd.action((0, shared_1.withHelp)(cmd, async (analyticsName, rawOpts) => {
|
|
217
|
+
(0, shared_1.validateTimeOptions)(rawOpts, "since", "until");
|
|
208
218
|
(0, shared_1.rejectCliOverride)(cmd, "appId");
|
|
209
219
|
await (0, index_1.handleObservabilityAnalytics)({
|
|
210
220
|
analyticsName,
|
|
@@ -6,9 +6,12 @@ exports.softRequiredOption = softRequiredOption;
|
|
|
6
6
|
exports.resolveAppId = resolveAppId;
|
|
7
7
|
exports.withHelp = withHelp;
|
|
8
8
|
exports.failArgs = failArgs;
|
|
9
|
+
exports.caseInsensitiveChoice = caseInsensitiveChoice;
|
|
10
|
+
exports.validateTimeOptions = validateTimeOptions;
|
|
9
11
|
exports.rejectCliOverride = rejectCliOverride;
|
|
10
12
|
const commander_1 = require("commander");
|
|
11
13
|
const error_1 = require("../../utils/error");
|
|
14
|
+
const time_1 = require("../../utils/time");
|
|
12
15
|
/** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption()) */
|
|
13
16
|
function appIdOption() {
|
|
14
17
|
return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
|
|
@@ -62,6 +65,47 @@ function withHelp(cmd, handler) {
|
|
|
62
65
|
function failArgs(message) {
|
|
63
66
|
throw new error_1.AppError("ARGS_INVALID", message);
|
|
64
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* 大小写不敏感的 choice argParser:把用户输入按 lowerCase 匹配回 canonical 数组里
|
|
70
|
+
* 的同名值(保留 canonical 大小写),未命中抛 Commander 的 InvalidArgumentError
|
|
71
|
+
* (exit code 1,自带友好错误)。
|
|
72
|
+
*
|
|
73
|
+
* 必须先 .choices(canonical)、再 .argParser()——Commander 内部 .choices() 会
|
|
74
|
+
* 重置 parseArg,反过来调会让 argParser 失效。.choices() 仅留给 help 渲染
|
|
75
|
+
* "choices: A, B, C",实际白名单校验由 argParser 接管。
|
|
76
|
+
*
|
|
77
|
+
* new Option("--level <level>", "日志级别(不区分大小写)")
|
|
78
|
+
* .choices(["DEBUG", "INFO", "WARN", "ERROR"])
|
|
79
|
+
* .argParser(caseInsensitiveChoice(["DEBUG", "INFO", "WARN", "ERROR"]));
|
|
80
|
+
*/
|
|
81
|
+
function caseInsensitiveChoice(canonical) {
|
|
82
|
+
const map = new Map(canonical.map((v) => [v.toLowerCase(), v]));
|
|
83
|
+
return (raw) => {
|
|
84
|
+
const hit = map.get(raw.toLowerCase());
|
|
85
|
+
if (hit === undefined) {
|
|
86
|
+
throw new commander_1.InvalidArgumentError(`Allowed choices are ${canonical.join(", ")} (case-insensitive).`);
|
|
87
|
+
}
|
|
88
|
+
return hit;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* --since / --until 等时间参数的 action 阶段校验。
|
|
93
|
+
*
|
|
94
|
+
* Commander 的 argParser 会在 action 前直接按 parser error 退出(exit 1),
|
|
95
|
+
* 绕过 withHelp 的 ARGS_INVALID → exit 2 路径;所以时间参数在 action 内先
|
|
96
|
+
* 校验一遍,再把原始字符串交给 handler 解析成接口需要的单位。
|
|
97
|
+
*/
|
|
98
|
+
function validateTimeOptions(opts, ...names) {
|
|
99
|
+
for (const name of names) {
|
|
100
|
+
const value = opts[name];
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (typeof value !== "string") {
|
|
104
|
+
failArgs(`--${name.replace(/([A-Z])/g, "-$1").toLowerCase()} 必须是时间字符串`);
|
|
105
|
+
}
|
|
106
|
+
(0, time_1.parseTimeToMs)(value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
65
109
|
/**
|
|
66
110
|
* 拒绝 CLI 显式覆盖:用于 hideHelp() + .env() 的"沙箱注入参数"(如 --app-id / --branch)。
|
|
67
111
|
*
|
|
@@ -149,22 +149,23 @@ function buildAnalyticsPivotSchema(seriesLabels) {
|
|
|
149
149
|
*/
|
|
150
150
|
function resolveAnalyticsSelection(cliName, series) {
|
|
151
151
|
const extras = [];
|
|
152
|
+
const normalizedSeries = normalizeAnalyticsSeries(cliName, series);
|
|
152
153
|
if (cliName === "users") {
|
|
153
|
-
if (
|
|
154
|
+
if (normalizedSeries === "active-users")
|
|
154
155
|
return single("ACTIVE_USER", "active-users", extras);
|
|
155
|
-
if (
|
|
156
|
+
if (normalizedSeries === "new-users")
|
|
156
157
|
return single("NEW_USER", "new-users", extras);
|
|
157
|
-
if (
|
|
158
|
+
if (normalizedSeries === "total-users")
|
|
158
159
|
return single("TOTAL_USER", "total-users", extras);
|
|
159
160
|
// 缺省:三条线
|
|
160
161
|
return all(ANALYTICS_LABELS.users, extras);
|
|
161
162
|
}
|
|
162
163
|
if (cliName === "page-view") {
|
|
163
|
-
if (
|
|
164
|
+
if (normalizedSeries === "desktop-view") {
|
|
164
165
|
extras.push({ key: "device_type", value: (0, helpers_1.eqFilter)("desktop") });
|
|
165
166
|
return single("PAGE_VIEW", "desktop-view", extras);
|
|
166
167
|
}
|
|
167
|
-
if (
|
|
168
|
+
if (normalizedSeries === "mobile-view") {
|
|
168
169
|
extras.push({ key: "device_type", value: (0, helpers_1.eqFilter)("mobile") });
|
|
169
170
|
return single("PAGE_VIEW", "mobile-view", extras);
|
|
170
171
|
}
|
|
@@ -174,6 +175,27 @@ function resolveAnalyticsSelection(cliName, series) {
|
|
|
174
175
|
// 兜底:CLI 名直接当 metricType + label
|
|
175
176
|
return single(cliName, cliName, extras);
|
|
176
177
|
}
|
|
178
|
+
function normalizeAnalyticsSeries(cliName, series) {
|
|
179
|
+
if (series === undefined)
|
|
180
|
+
return undefined;
|
|
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
|
+
}
|
|
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
|
+
}
|
|
197
|
+
return series;
|
|
198
|
+
}
|
|
177
199
|
function single(metricType, label, extras) {
|
|
178
200
|
return {
|
|
179
201
|
metricTypes: [metricType],
|
|
@@ -71,7 +71,7 @@ async function handleObservabilityMetric(opts) {
|
|
|
71
71
|
]);
|
|
72
72
|
// since/until 直接以秒透传到 BAM;当前未做桶对齐(如需开启把 until
|
|
73
73
|
// 用 ceilMsToBucket(untilMs, downSample as GranularityBucket) 包一层即可)。
|
|
74
|
-
const downSample = opts.downSample ?? "
|
|
74
|
+
const downSample = opts.downSample ?? "1m";
|
|
75
75
|
const nowMs = Date.now();
|
|
76
76
|
const sinceMs = (0, helpers_1.parseToMs)(opts.since) ?? nowMs - 30 * 86_400_000;
|
|
77
77
|
const untilMs = (0, helpers_1.parseToMs)(opts.until) ?? nowMs;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRootVersionArg = resolveRootVersionArg;
|
|
4
|
+
function resolveRootVersionArg(args) {
|
|
5
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--version"))
|
|
6
|
+
return "show";
|
|
7
|
+
if (args.some(isLooseVersionArg))
|
|
8
|
+
return "invalid-variant";
|
|
9
|
+
if (args.includes("-v") || args.includes("--version"))
|
|
10
|
+
return "invalid-placement";
|
|
11
|
+
return "none";
|
|
12
|
+
}
|
|
13
|
+
function isLooseVersionArg(arg) {
|
|
14
|
+
return ((arg.startsWith("-v") && arg !== "-v") || (arg.startsWith("--version") && arg !== "--version"));
|
|
15
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -5,10 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const commander_1 = require("commander");
|
|
7
7
|
const index_1 = require("./cli/commands/index");
|
|
8
|
+
const shared_1 = require("./cli/commands/shared");
|
|
8
9
|
const help_1 = require("./cli/help");
|
|
9
10
|
const config_1 = require("./utils/config");
|
|
10
11
|
const log_id_1 = require("./utils/log_id");
|
|
11
12
|
const output_1 = require("./utils/output");
|
|
13
|
+
const error_1 = require("./utils/error");
|
|
14
|
+
const version_1 = require("./cli/version");
|
|
12
15
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
13
16
|
// MiaodaHelp 对齐 CLI 规范(描述置顶 / Flags / Global Flags / 段顺序):
|
|
14
17
|
// 在 Command.prototype 上 patch createHelp,让所有命令实例(含动态注册的
|
|
@@ -20,11 +23,14 @@ const program = new commander_1.Command();
|
|
|
20
23
|
const { version } = package_json_1.default;
|
|
21
24
|
program
|
|
22
25
|
.name("miaoda")
|
|
23
|
-
.description("妙搭平台 CLI
|
|
26
|
+
.description("妙搭平台 CLI,提供数据服务、文件存储等命令行操作。")
|
|
24
27
|
.usage("<command> [flags]")
|
|
25
|
-
.
|
|
28
|
+
.option("-v, --version", "显示版本号")
|
|
26
29
|
.option("--json [fields]", "JSON 输出,可选字段级选择")
|
|
27
|
-
.
|
|
30
|
+
.addOption(new commander_1.Option("--output <format>", "输出格式(pretty | json,大小写不敏感)")
|
|
31
|
+
.choices(["pretty", "json"])
|
|
32
|
+
.argParser((0, shared_1.caseInsensitiveChoice)(["pretty", "json"]))
|
|
33
|
+
.default("pretty"))
|
|
28
34
|
.option("--verbose", "debug 日志到 stderr")
|
|
29
35
|
.helpOption("-h, --help", "显示帮助信息")
|
|
30
36
|
.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -33,7 +39,21 @@ program
|
|
|
33
39
|
(0, config_1.initConfigFromOpts)(opts);
|
|
34
40
|
});
|
|
35
41
|
(0, index_1.registerCommands)(program);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
process.
|
|
39
|
-
}
|
|
42
|
+
const versionArg = (0, version_1.resolveRootVersionArg)(process.argv.slice(2));
|
|
43
|
+
if (versionArg === "show") {
|
|
44
|
+
process.stdout.write(`${version}\n`);
|
|
45
|
+
}
|
|
46
|
+
else if (versionArg === "invalid-variant") {
|
|
47
|
+
(0, output_1.emitError)(new error_1.AppError("ARGS_INVALID", "版本参数仅支持精确的 -v 或 --version"));
|
|
48
|
+
process.exitCode = 2;
|
|
49
|
+
}
|
|
50
|
+
else if (versionArg === "invalid-placement") {
|
|
51
|
+
(0, output_1.emitError)(new error_1.AppError("ARGS_INVALID", "-v / --version 仅可在根命令单独使用,不能与其他命令或参数混用"));
|
|
52
|
+
process.exitCode = 2;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
56
|
+
(0, output_1.emitError)(err);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
});
|
|
59
|
+
}
|
package/dist/utils/http.js
CHANGED
|
@@ -10,6 +10,8 @@ exports.postInnerApi = postInnerApi;
|
|
|
10
10
|
exports.getInnerApi = getInnerApi;
|
|
11
11
|
const http_client_1 = require("@lark-apaas/http-client");
|
|
12
12
|
const error_1 = require("./error");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const logger_1 = require("./logger");
|
|
13
15
|
let adminClient;
|
|
14
16
|
let runtimeClient;
|
|
15
17
|
/**
|
|
@@ -70,11 +72,31 @@ function applyCanaryHeader(reqConfig) {
|
|
|
70
72
|
}
|
|
71
73
|
/** 走管理端 innerapi 的 POST + 信封解析 */
|
|
72
74
|
async function postInnerApi(url, body, opts) {
|
|
73
|
-
|
|
75
|
+
const startMs = logRequestStart("POST", url, body);
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await getHttpClient().post(url, body);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logResponseFailure("POST", url, err, startMs);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
logResponseEnd("POST", url, response, startMs);
|
|
85
|
+
return handleInnerEnvelope(response, url, opts);
|
|
74
86
|
}
|
|
75
87
|
/** 走管理端 innerapi 的 GET + 信封解析 */
|
|
76
88
|
async function getInnerApi(url, opts) {
|
|
77
|
-
|
|
89
|
+
const startMs = logRequestStart("GET", url);
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await getHttpClient().get(url);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logResponseFailure("GET", url, err, startMs);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
logResponseEnd("GET", url, response, startMs);
|
|
99
|
+
return handleInnerEnvelope(response, url, opts);
|
|
78
100
|
}
|
|
79
101
|
async function handleInnerEnvelope(response, url, opts) {
|
|
80
102
|
if (!response.ok) {
|
|
@@ -85,6 +107,10 @@ async function handleInnerEnvelope(response, url, opts) {
|
|
|
85
107
|
const env = result;
|
|
86
108
|
if (env.status_code !== undefined && env.status_code !== "0") {
|
|
87
109
|
const msg = env.message ?? env.error_msg ?? "unknown error";
|
|
110
|
+
// verbose: 把完整业务错误信封打到 stderr,便于追溯
|
|
111
|
+
if ((0, config_1.getConfig)().verbose) {
|
|
112
|
+
(0, logger_1.debug)(` envelope: ${truncateForLog(safeStringify(env), 1000)}`);
|
|
113
|
+
}
|
|
88
114
|
const mapped = opts.mapErr?.(env.status_code, msg);
|
|
89
115
|
if (mapped)
|
|
90
116
|
throw mapped;
|
|
@@ -97,3 +123,63 @@ async function handleInnerEnvelope(response, url, opts) {
|
|
|
97
123
|
}
|
|
98
124
|
return result;
|
|
99
125
|
}
|
|
126
|
+
// ── verbose 调试日志 ────────────
|
|
127
|
+
//
|
|
128
|
+
// 仅作用于 inner-api 链路(postInnerApi / getInnerApi),即 app / deploy /
|
|
129
|
+
// observability 三个域。--verbose 关闭时所有 helper 都直接退出,零开销。
|
|
130
|
+
function logRequestStart(method, url, body) {
|
|
131
|
+
const startMs = Date.now();
|
|
132
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
133
|
+
return startMs;
|
|
134
|
+
(0, logger_1.debug)(`→ ${method} ${url}`);
|
|
135
|
+
if (body !== undefined) {
|
|
136
|
+
(0, logger_1.debug)(` body: ${truncateForLog(safeStringify(body), 1000)}`);
|
|
137
|
+
}
|
|
138
|
+
return startMs;
|
|
139
|
+
}
|
|
140
|
+
function logResponseEnd(method, url, response, startMs) {
|
|
141
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
142
|
+
return;
|
|
143
|
+
const elapsedMs = Date.now() - startMs;
|
|
144
|
+
const logid = pickLogid(response.headers);
|
|
145
|
+
const tail = logid ? ` logid=${logid}` : "";
|
|
146
|
+
(0, logger_1.debug)(`← ${method} ${url} ${String(response.status)} ${String(elapsedMs)}ms${tail}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* http-client 在 4xx/5xx / 网络错误时直接 reject(不返回 Response);
|
|
150
|
+
* 这条路径下 logResponseEnd 不会跑——这里专门补一行,尽可能从 HttpError
|
|
151
|
+
* 上抠出 status / logid,让 verbose 仍能看到失败请求的关键信息。
|
|
152
|
+
*/
|
|
153
|
+
function logResponseFailure(method, url, err, startMs) {
|
|
154
|
+
if (!(0, config_1.getConfig)().verbose)
|
|
155
|
+
return;
|
|
156
|
+
const elapsedMs = Date.now() - startMs;
|
|
157
|
+
// @lark-apaas/http-client 的 HttpError.response: Response | undefined
|
|
158
|
+
const e = err;
|
|
159
|
+
const status = e.response?.status;
|
|
160
|
+
const logid = e.response?.headers ? pickLogid(e.response.headers) : null;
|
|
161
|
+
const statusPart = status !== undefined ? String(status) : "ERR";
|
|
162
|
+
const logidPart = logid ? ` logid=${logid}` : "";
|
|
163
|
+
const msgPart = e.message ? ` (${e.message})` : "";
|
|
164
|
+
(0, logger_1.debug)(`✗ ${method} ${url} ${statusPart} ${String(elapsedMs)}ms${logidPart}${msgPart}`);
|
|
165
|
+
}
|
|
166
|
+
/** BAM gateway 在不同 PSM/版本上 logid 落在不同 header;按命中顺序取第一个非空。 */
|
|
167
|
+
function pickLogid(headers) {
|
|
168
|
+
return (headers.get("x-tt-logid") ??
|
|
169
|
+
headers.get("x-logid") ??
|
|
170
|
+
headers.get("logid") ??
|
|
171
|
+
headers.get("x-tt-trace-tag"));
|
|
172
|
+
}
|
|
173
|
+
function safeStringify(v) {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.stringify(v);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return String(v);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function truncateForLog(s, n) {
|
|
182
|
+
if (s.length <= n)
|
|
183
|
+
return s;
|
|
184
|
+
return `${s.slice(0, n)}...(truncated, ${String(s.length)} chars)`;
|
|
185
|
+
}
|
package/dist/utils/time.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TIMESTAMP_HELP = void 0;
|
|
3
4
|
exports.parseTimeToMs = parseTimeToMs;
|
|
4
5
|
exports.msToNs = msToNs;
|
|
5
6
|
exports.msToSec = msToSec;
|
|
@@ -9,16 +10,28 @@ exports.parseToSec = parseToSec;
|
|
|
9
10
|
exports.floorMsToBucket = floorMsToBucket;
|
|
10
11
|
exports.ceilMsToBucket = ceilMsToBucket;
|
|
11
12
|
const error_1 = require("./error");
|
|
12
|
-
|
|
13
|
+
exports.TIMESTAMP_HELP = "支持格式:" +
|
|
14
|
+
"相对时间 30m/1h/2d/1w;" +
|
|
15
|
+
"日期 2026-04-01(本地时区当日 00:00:00);" +
|
|
16
|
+
"本地日期+时间 2026-04-01T10:00:00(按本地时区,T 分隔);" +
|
|
17
|
+
"带时区 ISO 2026-04-01T10:00:00Z(UTC)或 2026-04-01T10:00:00+08:00(指定偏移)。" +
|
|
18
|
+
"禁止用空格分隔:'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数。";
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
20
|
+
* 解析时间字符串到毫秒时间戳;不匹配任一支持格式抛 ARGS_INVALID。
|
|
21
|
+
*
|
|
22
|
+
* 时区约定(重要):
|
|
23
|
+
* - 不带显式时区的形式(YYYY-MM-DD、YYYY-MM-DDTHH:mm:ss)一律按
|
|
24
|
+
* **本地时区**解释,与 pretty 输出(output.ts:renderDate 用 getFullYear 等本地方法)形成
|
|
25
|
+
* 输入/输出闭环:用户复制输出文本作为 --since 不会差时区。
|
|
26
|
+
* - 带显式时区(结尾 Z 或 ±HH:MM / ±HHMM)按显式时区解析,跨机器一致。
|
|
27
|
+
* - 因此跨机器同步使用时建议带显式时区;同机器复制粘贴 pretty 输出更省事。
|
|
28
|
+
*
|
|
29
|
+
* 拒绝 Date.parse 松散接受的形式:YYYY/MM/DD、自然语言('April 1 2026')、单独年份等。
|
|
18
30
|
*
|
|
19
31
|
* 失败抛 AppError("ARGS_INVALID", ...);CLI 层用 withHelp 自动转 exit 2 + help。
|
|
20
32
|
*/
|
|
21
33
|
function parseTimeToMs(input, now = new Date()) {
|
|
34
|
+
// 1. 相对时间:30m / 1h / 2d / 1w
|
|
22
35
|
const relative = /^(\d+)([mhdw])$/.exec(input);
|
|
23
36
|
if (relative) {
|
|
24
37
|
const n = Number(relative[1]);
|
|
@@ -32,19 +45,77 @@ function parseTimeToMs(input, now = new Date()) {
|
|
|
32
45
|
: /* w */ 604_800_000;
|
|
33
46
|
return now.getTime() - n * factor;
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
// 2. 纯日期:YYYY-MM-DD → 本地当日 00:00:00
|
|
49
|
+
const date = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
|
|
50
|
+
if (date) {
|
|
51
|
+
return localDateMs(input, date[1], date[2], date[3], "00", "00", "00", "0");
|
|
52
|
+
}
|
|
53
|
+
// 3. 本地日期+时间:YYYY-MM-DDTHH:mm:ss[.SSS](不带时区,T 分隔)。
|
|
54
|
+
// 不接受空格分隔——'YYYY-MM-DD HH:mm:ss' 不带引号会被 shell 拆成两个参数,
|
|
55
|
+
// 宁可不支持也不要让 agent 踩"看起来传了 since 实际只传了一半"的坑。
|
|
56
|
+
const localDt = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(input);
|
|
57
|
+
if (localDt) {
|
|
58
|
+
// capture 7 是可选 .SSS 组,运行时可能 undefined;TS lib 的 RegExp match 索引
|
|
59
|
+
// 类型把它标成 string,所以这里显式 cast 让 ?? 在 lint 视角下也"必要"。
|
|
60
|
+
const msPart = localDt[7] ?? "0";
|
|
61
|
+
return localDateMs(input, localDt[1], localDt[2], localDt[3], localDt[4], localDt[5], localDt[6], msPart);
|
|
62
|
+
}
|
|
63
|
+
// 4. 带显式时区的 ISO 8601:YYYY-MM-DDTHH:mm:ss[.SSS](Z|±HH:MM|±HHMM)
|
|
64
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-](\d{2}):?(\d{2}))$/.exec(input);
|
|
65
|
+
if (iso) {
|
|
66
|
+
validateDateTimeParts(input, iso[1], iso[2], iso[3], iso[4], iso[5], iso[6]);
|
|
67
|
+
if (iso[7] !== "Z")
|
|
68
|
+
validateOffset(input, iso[8], iso[9]);
|
|
69
|
+
const ms = Date.parse(input);
|
|
37
70
|
if (Number.isNaN(ms))
|
|
38
71
|
failInvalidTimestamp(input);
|
|
39
72
|
return ms;
|
|
40
73
|
}
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
failInvalidTimestamp(input);
|
|
75
|
+
}
|
|
76
|
+
function localDateMs(input, y, mo, d, h, mi, s, msPart) {
|
|
77
|
+
validateDateTimeParts(input, y, mo, d, h, mi, s);
|
|
78
|
+
// 毫秒位补齐到 3 位再截断("5" → 500,"12" → 120,"123" → 123)
|
|
79
|
+
const ms = Number(msPart.padEnd(3, "0").slice(0, 3));
|
|
80
|
+
const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s), ms);
|
|
81
|
+
if (date.getFullYear() !== Number(y) ||
|
|
82
|
+
date.getMonth() !== Number(mo) - 1 ||
|
|
83
|
+
date.getDate() !== Number(d) ||
|
|
84
|
+
date.getHours() !== Number(h) ||
|
|
85
|
+
date.getMinutes() !== Number(mi) ||
|
|
86
|
+
date.getSeconds() !== Number(s) ||
|
|
87
|
+
date.getMilliseconds() !== ms) {
|
|
88
|
+
failInvalidTimestamp(input);
|
|
89
|
+
}
|
|
90
|
+
return date.getTime();
|
|
91
|
+
}
|
|
92
|
+
function validateDateTimeParts(input, y, mo, d, h, mi, s) {
|
|
93
|
+
const year = Number(y);
|
|
94
|
+
const month = Number(mo);
|
|
95
|
+
const day = Number(d);
|
|
96
|
+
const hour = Number(h);
|
|
97
|
+
const minute = Number(mi);
|
|
98
|
+
const second = Number(s);
|
|
99
|
+
if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) {
|
|
100
|
+
failInvalidTimestamp(input);
|
|
101
|
+
}
|
|
102
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
103
|
+
if (utc.getUTCFullYear() !== year ||
|
|
104
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
105
|
+
utc.getUTCDate() !== day) {
|
|
106
|
+
failInvalidTimestamp(input);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateOffset(input, h, mi) {
|
|
110
|
+
if (h === undefined || mi === undefined)
|
|
111
|
+
failInvalidTimestamp(input);
|
|
112
|
+
const hour = Number(h);
|
|
113
|
+
const minute = Number(mi);
|
|
114
|
+
if (hour > 23 || minute > 59)
|
|
43
115
|
failInvalidTimestamp(input);
|
|
44
|
-
return ms;
|
|
45
116
|
}
|
|
46
117
|
function failInvalidTimestamp(input) {
|
|
47
|
-
throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${TIMESTAMP_HELP}`);
|
|
118
|
+
throw new error_1.AppError("ARGS_INVALID", `无法解析时间 '${input}'。${exports.TIMESTAMP_HELP}`);
|
|
48
119
|
}
|
|
49
120
|
/** 毫秒 → 纳秒(字符串,避免 JS Number 精度丢失) */
|
|
50
121
|
function msToNs(ms) {
|