@lark-apaas/miaoda-cli 0.1.0-alpha.ca2912e → 0.1.0-alpha.d98ea14

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.
@@ -6,7 +6,36 @@ exports.importData = importData;
6
6
  exports.exportData = exportData;
7
7
  const http_1 = require("../../utils/http");
8
8
  const error_1 = require("../../utils/error");
9
+ const http_client_1 = require("@lark-apaas/http-client");
9
10
  const client_1 = require("./client");
11
+ /**
12
+ * 把 SDK 抛出的 HttpError 统一映射成 CLI 层错误:
13
+ * 1. 先尝试从 response body 解 envelope,命中 dataloom 业务 code → AppError
14
+ * 2. 兜底返 HttpError,保留真实 status 码与上下文
15
+ *
16
+ * 配合调用点的 try/catch + traceHttp,让 --verbose 在错误路径上也能拿到
17
+ * x-tt-logid 与 status,方便定位线上问题。
18
+ */
19
+ async function mapDbHttpError(err, url, ctx) {
20
+ if (err instanceof error_1.AppError)
21
+ throw err;
22
+ if (err instanceof http_client_1.HttpError) {
23
+ const status = err.response?.status ?? 0;
24
+ const statusText = err.response?.statusText ?? "";
25
+ try {
26
+ const body = (await err.response?.json());
27
+ if (body)
28
+ (0, client_1.extractData)(body); // 业务 code 命中 → 抛 AppError;不命中走兜底
29
+ }
30
+ catch (innerErr) {
31
+ if (innerErr instanceof error_1.AppError)
32
+ throw innerErr;
33
+ // body 解析失败 → 当成无 envelope 的纯 HTTP 错误处理
34
+ }
35
+ throw new error_1.HttpError(status, url, `${ctx}: ${String(status)} ${statusText}`.trim());
36
+ }
37
+ throw err;
38
+ }
10
39
  // CLI 不再为 dbBranch 设默认值:
11
40
  // 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
12
41
  // 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
@@ -24,19 +53,16 @@ async function execSql(opts) {
24
53
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
25
54
  dbBranch: opts.dbBranch,
26
55
  });
27
- const response = await client.post(url, { sql: opts.sql });
28
- if (!response.ok) {
29
- // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
30
- let body = null;
31
- try {
32
- body = (await response.json());
33
- }
34
- catch {
35
- // ignore
36
- }
37
- if (body)
38
- (0, client_1.extractData)(body);
39
- throw new error_1.HttpError(response.status, url, `Failed to execute SQL: ${String(response.status)} ${response.statusText}`);
56
+ const start = Date.now();
57
+ let response;
58
+ try {
59
+ response = await client.post(url, { sql: opts.sql });
60
+ (0, client_1.traceHttp)("POST", url, start, response);
61
+ }
62
+ catch (err) {
63
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
64
+ await mapDbHttpError(err, url, "Failed to execute SQL");
65
+ throw err; // 不可达
40
66
  }
41
67
  const body = (await response.json());
42
68
  const data = (0, client_1.extractData)(body);
@@ -57,95 +83,95 @@ async function getSchema(opts) {
57
83
  includeStats: opts.includeStats ? "true" : undefined,
58
84
  dbBranch: opts.dbBranch,
59
85
  });
60
- const response = await client.get(url);
61
- if (!response.ok) {
62
- let body = null;
63
- try {
64
- body = (await response.json());
65
- }
66
- catch {
67
- // ignore
68
- }
69
- if (body)
70
- (0, client_1.extractData)(body);
71
- throw new error_1.HttpError(response.status, url, `Failed to get schema: ${String(response.status)} ${response.statusText}`);
86
+ const start = Date.now();
87
+ let response;
88
+ try {
89
+ response = await client.get(url);
90
+ (0, client_1.traceHttp)("GET", url, start, response);
91
+ }
92
+ catch (err) {
93
+ (0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
94
+ await mapDbHttpError(err, url, "Failed to get schema");
95
+ throw err; // 不可达
72
96
  }
73
97
  const body = (await response.json());
74
98
  return (0, client_1.extractData)(body);
75
99
  }
76
- // ── db data import → InnerImportData ──
100
+ // ── db data import → InnerAdminImportData ──
77
101
  /**
78
102
  * 导入文件。
79
- * 后端:POST /v1/dataloom/app/{appId}/data/import?tableName=...&format=csv|json&dbBranch=main
103
+ * 后端:POST /v1/dataloom/app/{appId}/data/import
104
+ *
105
+ * 全字段走 JSON body envelope(idl-larkgw 36125a2f):
106
+ * {tableName, format, records, dbBranch?}
80
107
  *
81
- * Body 为原始文件字节(不走 JSON envelope)。
108
+ * `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
109
+ * Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
110
+ * 字符串后塞进 envelope 即可。
82
111
  */
83
112
  async function importData(opts) {
84
113
  const client = (0, http_1.getHttpClient)();
85
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
114
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
115
+ const reqBody = {
86
116
  tableName: opts.tableName,
87
117
  format: opts.format,
88
- dbBranch: opts.dbBranch,
89
- });
90
- const contentType = opts.format === "csv" ? "text/csv" : "application/json";
91
- const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
92
- const response = await client.request({
93
- method: "POST",
94
- url,
95
- headers: { "Content-Type": contentType },
96
- body: ab,
97
- });
98
- if (!response.ok) {
99
- let body = null;
100
- try {
101
- body = (await response.json());
102
- }
103
- catch {
104
- // ignore
105
- }
106
- if (body)
107
- (0, client_1.extractData)(body);
108
- throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
118
+ records: opts.body.toString("utf8"),
119
+ };
120
+ if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
121
+ reqBody.dbBranch = opts.dbBranch;
122
+ }
123
+ const start = Date.now();
124
+ let response;
125
+ try {
126
+ response = await client.post(url, reqBody);
127
+ (0, client_1.traceHttp)("POST", url, start, response);
128
+ }
129
+ catch (err) {
130
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
131
+ await mapDbHttpError(err, url, "Failed to import data");
132
+ throw err; // 不可达
109
133
  }
110
- // 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
134
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
111
135
  const body = (await response.json());
112
136
  const data = (0, client_1.extractData)(body);
113
137
  return {
114
138
  tableName: data.tableName ?? opts.tableName,
115
- rows: data.rows ?? 0,
139
+ recordCount: data.recordCount ?? 0,
116
140
  durationMs: data.durationMs ?? 0,
117
141
  };
118
142
  }
119
- // ── db data export → InnerExportData ──
143
+ // ── db data export → InnerAdminExportData ──
120
144
  /**
121
145
  * 导出数据。
122
- * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
146
+ * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
123
147
  *
124
- * 响应 body 为原始 CSV/JSON 字节(不走 envelope)。错误仍通过 HTTP 4xx/5xx + BaseResp 传达。
148
+ * 所有参数(含 limit)均按 IDL `api.query` query string;HTTP 方法是 POST(对齐
149
+ * inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
150
+ * 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
151
+ * 回传,错误仍走 HTTP 4xx/5xx + envelope。
125
152
  */
126
153
  async function exportData(opts) {
127
154
  const client = (0, http_1.getHttpClient)();
128
155
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
129
156
  tableName: opts.tableName,
130
157
  format: opts.format,
158
+ limit: String(opts.limit ?? 5000),
131
159
  dbBranch: opts.dbBranch,
132
160
  });
133
- const reqBody = { limit: opts.limit ?? 5000 };
134
- const response = await client.post(url, reqBody);
135
- if (!response.ok) {
136
- // 错误路径:body 是 JSON envelope
137
- let body = null;
138
- try {
139
- body = (await response.json());
140
- }
141
- catch {
142
- // ignore
143
- }
144
- if (body)
145
- (0, client_1.extractData)(body);
146
- throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
161
+ // POST + body:所有业务参数都在 query
162
+ const start = Date.now();
163
+ let response;
164
+ try {
165
+ response = await client.request({ method: "POST", url });
166
+ (0, client_1.traceHttp)("POST", url, start, response);
167
+ }
168
+ catch (err) {
169
+ (0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
170
+ await mapDbHttpError(err, url, "Failed to export data");
171
+ throw err; // 不可达
147
172
  }
148
- // 成功路径:响应 body 是原始 CSV/JSON 字节
173
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
174
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
149
175
  const contentType = response.headers.get("Content-Type") ??
150
176
  (opts.format === "csv" ? "text/csv" : "application/json");
151
177
  const ab = await response.arrayBuffer();
@@ -153,10 +179,33 @@ async function exportData(opts) {
153
179
  if (buf.length === 0) {
154
180
  throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
155
181
  }
182
+ // Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
183
+ // InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
184
+ // 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
185
+ if (opts.format === "csv" && /application\/json/i.test(contentType)) {
186
+ try {
187
+ const parsed = JSON.parse(buf.toString("utf8"));
188
+ if (parsed.status_code != null && parsed.status_code !== "0") {
189
+ // 复用 extractData 的错误映射逻辑(throw AppError)
190
+ (0, client_1.extractData)(parsed);
191
+ }
192
+ }
193
+ catch (err) {
194
+ // 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
195
+ // 真的是 CSV 文本,继续按成功流程走
196
+ if (err instanceof error_1.AppError)
197
+ throw err;
198
+ }
199
+ }
200
+ // 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
201
+ const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
202
+ const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
203
+ const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
156
204
  return {
157
205
  tableName: opts.tableName,
158
206
  format: opts.format,
159
207
  contentType,
160
208
  body: buf,
209
+ recordCount,
161
210
  };
162
211
  }
@@ -1,10 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SQLSTATE_MAP = void 0;
4
+ exports.traceHttp = traceHttp;
4
5
  exports.ensureInnerSuccess = ensureInnerSuccess;
5
6
  exports.extractData = extractData;
6
7
  exports.buildInnerUrl = buildInnerUrl;
7
8
  const error_1 = require("../../utils/error");
9
+ const logger_1 = require("../../utils/logger");
10
+ /**
11
+ * 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
12
+ *
13
+ * 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
14
+ * 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
15
+ *
16
+ * 使用约定:
17
+ * const start = Date.now();
18
+ * const response = await client.post(url, body);
19
+ * traceHttp("POST", url, start, response);
20
+ * // 或错误路径:traceHttp("POST", url, start, err.response, err)
21
+ */
22
+ function traceHttp(method, url, start, response, err) {
23
+ // debug() 内部已判断 verbose 开关,这里不重复判断;保持调用点轻量
24
+ try {
25
+ const cost = Date.now() - start;
26
+ const status = response?.status ?? 0;
27
+ const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
28
+ if (err !== undefined) {
29
+ const errMsg = err instanceof Error
30
+ ? err.message
31
+ : typeof err === "string"
32
+ ? err
33
+ : JSON.stringify(err);
34
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
35
+ return;
36
+ }
37
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
38
+ }
39
+ catch {
40
+ // debug 失败不应影响业务,吞掉
41
+ }
42
+ }
8
43
  /**
9
44
  * 校验 dataloom InnerAPI 响应的 envelope。
10
45
  *
@@ -20,11 +55,15 @@ function ensureInnerSuccess(body) {
20
55
  if (code === "0" || code === "")
21
56
  return;
22
57
  const message = stripPgPrefix(body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
58
+ // PRD 多语句失败:后端在 envelope 顶层透出 errorStatementIndex(从 0 起计),
59
+ // 单语句 / 单元执行不会带这个字段。下面的 AppError 都把它带上,让最终
60
+ // CLI JSON envelope 写到 error.statement_index。
61
+ const stmtIdx = typeof body.errorStatementIndex === "number" ? body.errorStatementIndex : undefined;
23
62
  // k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
24
63
  if (code === "k_dl_1300002") {
25
64
  const sqlstate = extractSqlstate(message);
26
65
  if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
27
- throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
66
+ throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message, { statement_index: stmtIdx });
28
67
  }
29
68
  }
30
69
  // k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
@@ -35,7 +74,7 @@ function ensureInnerSuccess(body) {
35
74
  if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
36
75
  throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
37
76
  next_actions: [
38
- "Verify the --env value matches an existing dbBranch, or run `miaoda db migration init` to set up multi-env for this app.",
77
+ "Verify the --env value matches an existing dbBranch.",
39
78
  ],
40
79
  });
41
80
  }
@@ -44,10 +83,11 @@ function ensureInnerSuccess(body) {
44
83
  if (mapped) {
45
84
  throw new error_1.AppError(mapped.code, mapped.message ?? message, {
46
85
  next_actions: mapped.hint ? [mapped.hint] : undefined,
86
+ statement_index: stmtIdx,
47
87
  });
48
88
  }
49
89
  // 兜底:dataloom 未映射的 code 原样透传
50
- throw new error_1.AppError(`DB_API_${code}`, message);
90
+ throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
51
91
  }
52
92
  /**
53
93
  * 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
@@ -27,16 +27,23 @@ function parseSqlResult(r) {
27
27
  recordCount: r.recordCount ?? rows.length,
28
28
  };
29
29
  }
30
- if (r.sqlType === "INSERT" || r.sqlType === "UPDATE" || r.sqlType === "DELETE" || r.sqlType === "DML") {
30
+ if (r.sqlType === "INSERT" ||
31
+ r.sqlType === "UPDATE" ||
32
+ r.sqlType === "DELETE" ||
33
+ r.sqlType === "MERGE" ||
34
+ r.sqlType === "DML") {
31
35
  const affected = r.affectedRows ?? extractRowCount(r.data);
32
36
  return {
33
37
  kind: "dml",
38
+ // 上面已 narrow,这里 cast 是为了 SqlType 联合里的 (string & {}) 让 TS 无法
39
+ // 自动收窄到字面量集合,不影响运行时安全
34
40
  sqlType: r.sqlType,
35
41
  affectedRows: affected,
36
42
  };
37
43
  }
38
- // DDL or unknown
39
- return { kind: "ddl" };
44
+ // DDL or unknown — sqlType 透传后端给的细粒度(CREATE_TABLE / DROP_TABLE / ...
45
+ // / 笼统 "DDL"),CLI JSON 输出直接当 command 用
46
+ return { kind: "ddl", sqlType: r.sqlType };
40
47
  }
41
48
  /** DML 的 data 通常是 `[{"rowCount": N}]`;兜底从这里读影响行数。 */
42
49
  function extractRowCount(data) {
@@ -82,7 +89,6 @@ function toSummary(t, stats) {
82
89
  columns: (t.fields ?? []).length,
83
90
  estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
84
91
  size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
85
- updated_at: t.updatedAt,
86
92
  };
87
93
  }
88
94
  /**
@@ -118,8 +124,6 @@ function toDetail(t, stats) {
118
124
  indexes: rawIndexes.map(toIndex),
119
125
  estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
120
126
  size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
121
- created_at: t.createdAt,
122
- updated_at: t.updatedAt,
123
127
  };
124
128
  }
125
129
  function toColumn(f) {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTimeFilterMs = parseTimeFilterMs;
3
4
  exports.listFiles = listFiles;
4
5
  exports.resolveInputs = resolveInputs;
5
6
  exports.statFile = statFile;
@@ -82,15 +83,56 @@ function buildFilterExpr(opts) {
82
83
  }
83
84
  if (opts.uploadedSince) {
84
85
  // 后端期望毫秒 timestamp(strconv.ParseInt → time.UnixMilli)
85
- const ms = Date.parse(opts.uploadedSince);
86
- if (!Number.isNaN(ms)) {
87
- conds.push({ field: "createdAt", operator: "gte", value: String(ms) });
88
- }
86
+ const ms = parseTimeFilterMs(opts.uploadedSince, "--uploaded-since");
87
+ conds.push({ field: "createdAt", operator: "gte", value: String(ms) });
88
+ }
89
+ if (opts.uploadedUntil) {
90
+ const ms = parseTimeFilterMs(opts.uploadedUntil, "--uploaded-until");
91
+ conds.push({ field: "createdAt", operator: "lte", value: String(ms) });
89
92
  }
90
93
  if (conds.length === 0)
91
94
  return undefined;
92
95
  return { logic: "and", groups: [{ conditions: conds }] };
93
96
  }
97
+ /**
98
+ * 解析时间过滤参数(--uploaded-since / --uploaded-until)的三种输入格式
99
+ * (→ ms timestamp):
100
+ * 1. 相对时间:"30s" / "10m" / "1h" / "2d" / "1w" → 当前时间往前推 N 单位
101
+ * 2. 日期: "2026-04-01" → 按当日 00:00:00 UTC
102
+ * 3. ISO 8601:"2026-04-01T10:00:00Z" → 严格解析(推荐用 Z 结尾标识 UTC)
103
+ *
104
+ * 三种格式都不命中时抛 AppError("ARGS_INVALID"),避免之前 Date.parse=NaN
105
+ * 时静默跳过过滤、用户却以为筛选生效的悄然失败问题。
106
+ *
107
+ * flagName 用于错误信息,调用方传 "--uploaded-since" 或 "--uploaded-until"。
108
+ */
109
+ function parseTimeFilterMs(input, flagName) {
110
+ // 相对时间:<positive int><unit>,单位 s/m/h/d/w
111
+ const RELATIVE = /^(\d+)([smhdw])$/;
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]];
126
+ }
127
+ // 绝对时间:date / ISO 8601。Date.parse 对 "YYYY-MM-DD" 按 UTC 00:00:00 解析,
128
+ // 对带 Z 的 ISO 8601 也直接出 UTC ms。
129
+ const ms = Date.parse(input);
130
+ if (Number.isNaN(ms)) {
131
+ throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 格式无法识别。支持:` +
132
+ `相对时间(如 1h / 2d / 1w)、日期(YYYY-MM-DD)、ISO 8601(如 2026-04-01T10:00:00Z)`);
133
+ }
134
+ return ms;
135
+ }
94
136
  /**
95
137
  * 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
96
138
  *
@@ -214,7 +256,7 @@ async function resolveByName(appId, input) {
214
256
  error: {
215
257
  code: "FILE_NOT_FOUND",
216
258
  message: `File '${input}' does not exist`,
217
- hint: "Run `miaoda file ls` to verify file_name.",
259
+ hint: "Run `miaoda file ls` to see available files.",
218
260
  },
219
261
  };
220
262
  }
@@ -225,7 +267,7 @@ async function resolveByName(appId, input) {
225
267
  error: {
226
268
  code: "AMBIGUOUS_FILE_NAME",
227
269
  message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
228
- hint: `Use absolute /path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
270
+ hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
229
271
  },
230
272
  };
231
273
  }
@@ -280,6 +322,7 @@ async function uploadFile(opts) {
280
322
  // Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
281
323
  // 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
282
324
  const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
325
+ const uploadStart = Date.now();
283
326
  const res = await fetch(pre.uploadURL, {
284
327
  method: "PUT",
285
328
  headers: {
@@ -288,6 +331,7 @@ async function uploadFile(opts) {
288
331
  },
289
332
  body: ab,
290
333
  });
334
+ (0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
291
335
  if (!res.ok) {
292
336
  throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
293
337
  }
@@ -364,12 +408,15 @@ async function signDownload(opts) {
364
408
  */
365
409
  async function downloadFile(opts) {
366
410
  let res;
411
+ const downloadStart = Date.now();
367
412
  try {
368
413
  res = await fetch(opts.signedURL);
369
414
  }
370
415
  catch (err) {
416
+ (0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
371
417
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
372
418
  }
419
+ (0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
373
420
  if (!res.ok) {
374
421
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
375
422
  }
@@ -8,7 +8,34 @@ exports.doPost = doPost;
8
8
  exports.doRequest = doRequest;
9
9
  const http_1 = require("../../utils/http");
10
10
  const error_1 = require("../../utils/error");
11
+ const logger_1 = require("../../utils/logger");
11
12
  const http_client_1 = require("@lark-apaas/http-client");
13
+ /**
14
+ * 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
15
+ *
16
+ * 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
17
+ * 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
18
+ */
19
+ function traceHttp(method, url, start, response, err) {
20
+ try {
21
+ const cost = Date.now() - start;
22
+ const status = response?.status ?? 0;
23
+ const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
24
+ if (err !== undefined) {
25
+ const errMsg = err instanceof Error
26
+ ? err.message
27
+ : typeof err === "string"
28
+ ? err
29
+ : JSON.stringify(err);
30
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
31
+ return;
32
+ }
33
+ (0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
34
+ }
35
+ catch {
36
+ // debug 失败不应影响业务,吞掉
37
+ }
38
+ }
12
39
  /** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
13
40
  const bucketCache = new Map();
14
41
  /**
@@ -130,33 +157,42 @@ async function mapHttpError(err, opts) {
130
157
  * 通过第三个参数显式传入 getRuntimeHttpClient() 切换。
131
158
  */
132
159
  async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
160
+ const start = Date.now();
133
161
  try {
134
162
  const response = await client.get(url);
163
+ traceHttp("GET", url, start, response);
135
164
  return (await response.json());
136
165
  }
137
166
  catch (err) {
167
+ traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
138
168
  await mapHttpError(err, opts);
139
169
  throw err; // 不可达,mapHttpError 必定 throw
140
170
  }
141
171
  }
142
172
  /** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
143
173
  async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
174
+ const start = Date.now();
144
175
  try {
145
176
  const response = await client.post(url, body);
177
+ traceHttp("POST", url, start, response);
146
178
  return (await response.json());
147
179
  }
148
180
  catch (err) {
181
+ traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
149
182
  await mapHttpError(err, opts);
150
183
  throw err;
151
184
  }
152
185
  }
153
186
  /** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
154
187
  async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
188
+ const start = Date.now();
155
189
  try {
156
190
  const response = await client.request(cfg);
191
+ traceHttp(cfg.method, cfg.url, start, response);
157
192
  return (await response.json());
158
193
  }
159
194
  catch (err) {
195
+ traceHttp(cfg.method, cfg.url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
160
196
  await mapHttpError(err, opts);
161
197
  throw err;
162
198
  }
@@ -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.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.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; } });
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "signDownload", { enumerable: true, get: function
9
9
  Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function () { return api_1.downloadFile; } });
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
+ Object.defineProperty(exports, "parseTimeFilterMs", { enumerable: true, get: function () { return api_1.parseTimeFilterMs; } });
12
13
  var client_1 = require("./client");
13
14
  Object.defineProperty(exports, "getDefaultBucketId", { enumerable: true, get: function () { return client_1.getDefaultBucketId; } });
14
15
  Object.defineProperty(exports, "resetBucketCache", { enumerable: true, get: function () { return client_1.resetBucketCache; } });