@lark-apaas/miaoda-cli 0.1.0-alpha.41ce8f5 → 0.1.0-alpha.5f650e8

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.
@@ -5,8 +5,6 @@ exports.getSchema = getSchema;
5
5
  exports.importData = importData;
6
6
  exports.exportData = exportData;
7
7
  const http_1 = require("../../utils/http");
8
- // TODO(REMOVE-BEFORE-RELEASE): debug-only HTTP trace(详见 _debug_trace.ts)
9
- const _debug_trace_1 = require("./_debug_trace");
10
8
  const error_1 = require("../../utils/error");
11
9
  const client_1 = require("./client");
12
10
  // CLI 不再为 dbBranch 设默认值:
@@ -26,16 +24,9 @@ async function execSql(opts) {
26
24
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
27
25
  dbBranch: opts.dbBranch,
28
26
  });
29
- let response;
30
- try {
31
- (0, _debug_trace_1.traceRequest)("POST", url, client);
32
- response = await client.post(url, { sql: opts.sql });
33
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
34
- }
35
- catch (err) {
36
- (0, _debug_trace_1.traceError)("POST", url, err, client);
37
- throw err;
38
- }
27
+ const start = Date.now();
28
+ const response = await client.post(url, { sql: opts.sql });
29
+ (0, client_1.traceHttp)("POST", url, start, response);
39
30
  if (!response.ok) {
40
31
  // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
41
32
  let body = null;
@@ -68,16 +59,9 @@ async function getSchema(opts) {
68
59
  includeStats: opts.includeStats ? "true" : undefined,
69
60
  dbBranch: opts.dbBranch,
70
61
  });
71
- let response;
72
- try {
73
- (0, _debug_trace_1.traceRequest)("GET", url, client);
74
- response = await client.get(url);
75
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers, client);
76
- }
77
- catch (err) {
78
- (0, _debug_trace_1.traceError)("GET", url, err, client);
79
- throw err;
80
- }
62
+ const start = Date.now();
63
+ const response = await client.get(url);
64
+ (0, client_1.traceHttp)("GET", url, start, response);
81
65
  if (!response.ok) {
82
66
  let body = null;
83
67
  try {
@@ -93,37 +77,32 @@ async function getSchema(opts) {
93
77
  const body = (await response.json());
94
78
  return (0, client_1.extractData)(body);
95
79
  }
96
- // ── db data import → InnerImportData ──
80
+ // ── db data import → InnerAdminImportData ──
97
81
  /**
98
82
  * 导入文件。
99
- * 后端:POST /v1/dataloom/app/{appId}/data/import?tableName=...&format=csv|json&dbBranch=main
83
+ * 后端:POST /v1/dataloom/app/{appId}/data/import
100
84
  *
101
- * Body 为原始文件字节(不走 JSON envelope)。
85
+ * 全字段走 JSON body envelope(idl-larkgw 36125a2f):
86
+ * {tableName, format, records, dbBranch?}
87
+ *
88
+ * `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
89
+ * Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
90
+ * 字符串后塞进 envelope 即可。
102
91
  */
103
92
  async function importData(opts) {
104
93
  const client = (0, http_1.getHttpClient)();
105
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
94
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
95
+ const reqBody = {
106
96
  tableName: opts.tableName,
107
97
  format: opts.format,
108
- dbBranch: opts.dbBranch,
109
- });
110
- const contentType = opts.format === "csv" ? "text/csv" : "application/json";
111
- const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
112
- let response;
113
- try {
114
- (0, _debug_trace_1.traceRequest)("POST", url, client);
115
- response = await client.request({
116
- method: "POST",
117
- url,
118
- headers: { "Content-Type": contentType },
119
- body: ab,
120
- });
121
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
122
- }
123
- catch (err) {
124
- (0, _debug_trace_1.traceError)("POST", url, err, client);
125
- throw err;
98
+ records: opts.body.toString("utf8"),
99
+ };
100
+ if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
101
+ reqBody.dbBranch = opts.dbBranch;
126
102
  }
103
+ const start = Date.now();
104
+ const response = await client.post(url, reqBody);
105
+ (0, client_1.traceHttp)("POST", url, start, response);
127
106
  if (!response.ok) {
128
107
  let body = null;
129
108
  try {
@@ -136,40 +115,37 @@ async function importData(opts) {
136
115
  (0, client_1.extractData)(body);
137
116
  throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
138
117
  }
139
- // 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
118
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
140
119
  const body = (await response.json());
141
120
  const data = (0, client_1.extractData)(body);
142
121
  return {
143
122
  tableName: data.tableName ?? opts.tableName,
144
- rows: data.rows ?? 0,
123
+ recordCount: data.recordCount ?? 0,
145
124
  durationMs: data.durationMs ?? 0,
146
125
  };
147
126
  }
148
- // ── db data export → InnerExportData ──
127
+ // ── db data export → InnerAdminExportData ──
149
128
  /**
150
129
  * 导出数据。
151
- * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
130
+ * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
152
131
  *
153
- * 响应 body 为原始 CSV/JSON 字节(不走 envelope)。错误仍通过 HTTP 4xx/5xx + BaseResp 传达。
132
+ * 所有参数(含 limit)均按 IDL `api.query` query string;HTTP 方法是 POST(对齐
133
+ * inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
134
+ * 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
135
+ * 回传,错误仍走 HTTP 4xx/5xx + envelope。
154
136
  */
155
137
  async function exportData(opts) {
156
138
  const client = (0, http_1.getHttpClient)();
157
139
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
158
140
  tableName: opts.tableName,
159
141
  format: opts.format,
142
+ limit: String(opts.limit ?? 5000),
160
143
  dbBranch: opts.dbBranch,
161
144
  });
162
- const reqBody = { limit: opts.limit ?? 5000 };
163
- let response;
164
- try {
165
- (0, _debug_trace_1.traceRequest)("POST", url, client);
166
- response = await client.post(url, reqBody);
167
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
168
- }
169
- catch (err) {
170
- (0, _debug_trace_1.traceError)("POST", url, err, client);
171
- throw err;
172
- }
145
+ // POST + body:所有业务参数都在 query
146
+ const start = Date.now();
147
+ const response = await client.request({ method: "POST", url });
148
+ (0, client_1.traceHttp)("POST", url, start, response);
173
149
  if (!response.ok) {
174
150
  // 错误路径:body 是 JSON envelope
175
151
  let body = null;
@@ -183,7 +159,8 @@ async function exportData(opts) {
183
159
  (0, client_1.extractData)(body);
184
160
  throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
185
161
  }
186
- // 成功路径:响应 body 是原始 CSV/JSON 字节
162
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
163
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
187
164
  const contentType = response.headers.get("Content-Type") ??
188
165
  (opts.format === "csv" ? "text/csv" : "application/json");
189
166
  const ab = await response.arrayBuffer();
@@ -191,10 +168,33 @@ async function exportData(opts) {
191
168
  if (buf.length === 0) {
192
169
  throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
193
170
  }
171
+ // Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
172
+ // InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
173
+ // 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
174
+ if (opts.format === "csv" && /application\/json/i.test(contentType)) {
175
+ try {
176
+ const parsed = JSON.parse(buf.toString("utf8"));
177
+ if (parsed.status_code != null && parsed.status_code !== "0") {
178
+ // 复用 extractData 的错误映射逻辑(throw AppError)
179
+ (0, client_1.extractData)(parsed);
180
+ }
181
+ }
182
+ catch (err) {
183
+ // 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
184
+ // 真的是 CSV 文本,继续按成功流程走
185
+ if (err instanceof error_1.AppError)
186
+ throw err;
187
+ }
188
+ }
189
+ // 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
190
+ const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
191
+ const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
192
+ const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
194
193
  return {
195
194
  tableName: opts.tableName,
196
195
  format: opts.format,
197
196
  contentType,
198
197
  body: buf,
198
+ recordCount,
199
199
  };
200
200
  }
@@ -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
  *
@@ -19,12 +54,16 @@ function ensureInnerSuccess(body) {
19
54
  const code = body.status_code ?? body.ErrorCode ?? "0";
20
55
  if (code === "0" || code === "")
21
56
  return;
22
- const message = body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`;
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,19 @@ 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 });
91
+ }
92
+ /**
93
+ * 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
94
+ * 不去掉就会变成 `Error: ERROR: relation ...` 双重前缀,PRD 不要这种冗余。
95
+ * 大小写敏感,只匹配 PG 标准格式。
96
+ */
97
+ function stripPgPrefix(msg) {
98
+ return msg.replace(/^ERROR:\s*/, "");
51
99
  }
52
100
  /** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
53
101
  function extractSqlstate(msg) {
@@ -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") {
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) {
@@ -214,7 +214,7 @@ async function resolveByName(appId, input) {
214
214
  error: {
215
215
  code: "FILE_NOT_FOUND",
216
216
  message: `File '${input}' does not exist`,
217
- hint: "Run `miaoda file ls` to verify file_name.",
217
+ hint: "Run `miaoda file ls` to see available files.",
218
218
  },
219
219
  };
220
220
  }
@@ -225,7 +225,7 @@ async function resolveByName(appId, input) {
225
225
  error: {
226
226
  code: "AMBIGUOUS_FILE_NAME",
227
227
  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.`,
228
+ hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
229
229
  },
230
230
  };
231
231
  }
@@ -280,6 +280,7 @@ async function uploadFile(opts) {
280
280
  // Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
281
281
  // 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
282
282
  const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
283
+ const uploadStart = Date.now();
283
284
  const res = await fetch(pre.uploadURL, {
284
285
  method: "PUT",
285
286
  headers: {
@@ -288,6 +289,7 @@ async function uploadFile(opts) {
288
289
  },
289
290
  body: ab,
290
291
  });
292
+ (0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
291
293
  if (!res.ok) {
292
294
  throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
293
295
  }
@@ -364,12 +366,15 @@ async function signDownload(opts) {
364
366
  */
365
367
  async function downloadFile(opts) {
366
368
  let res;
369
+ const downloadStart = Date.now();
367
370
  try {
368
371
  res = await fetch(opts.signedURL);
369
372
  }
370
373
  catch (err) {
374
+ (0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
371
375
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
372
376
  }
377
+ (0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
373
378
  if (!res.ok) {
374
379
  throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
375
380
  }
@@ -8,9 +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
- // TODO(REMOVE-BEFORE-RELEASE): debug-only HTTP trace(详见 _debug_trace.ts)
12
- const _debug_trace_1 = require("./_debug_trace");
11
+ const logger_1 = require("../../utils/logger");
13
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
+ }
14
39
  /** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
15
40
  const bucketCache = new Map();
16
41
  /**
@@ -132,57 +157,42 @@ async function mapHttpError(err, opts) {
132
157
  * 通过第三个参数显式传入 getRuntimeHttpClient() 切换。
133
158
  */
134
159
  async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
160
+ const start = Date.now();
135
161
  try {
136
- (0, _debug_trace_1.traceRequest)("GET", url, client);
137
162
  const response = await client.get(url);
138
- (0, _debug_trace_1.traceResponse)("GET", url, response.status, response.headers, client);
163
+ traceHttp("GET", url, start, response);
139
164
  return (await response.json());
140
165
  }
141
166
  catch (err) {
142
- if (err instanceof http_client_1.HttpError && err.response) {
143
- (0, _debug_trace_1.traceResponse)("GET", url, err.response.status, err.response.headers, client);
144
- }
145
- else {
146
- (0, _debug_trace_1.traceError)("GET", url, err, client);
147
- }
167
+ traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
148
168
  await mapHttpError(err, opts);
149
169
  throw err; // 不可达,mapHttpError 必定 throw
150
170
  }
151
171
  }
152
172
  /** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
153
173
  async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
174
+ const start = Date.now();
154
175
  try {
155
- (0, _debug_trace_1.traceRequest)("POST", url, client);
156
176
  const response = await client.post(url, body);
157
- (0, _debug_trace_1.traceResponse)("POST", url, response.status, response.headers, client);
177
+ traceHttp("POST", url, start, response);
158
178
  return (await response.json());
159
179
  }
160
180
  catch (err) {
161
- if (err instanceof http_client_1.HttpError && err.response) {
162
- (0, _debug_trace_1.traceResponse)("POST", url, err.response.status, err.response.headers, client);
163
- }
164
- else {
165
- (0, _debug_trace_1.traceError)("POST", url, err, client);
166
- }
181
+ traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
167
182
  await mapHttpError(err, opts);
168
183
  throw err;
169
184
  }
170
185
  }
171
186
  /** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
172
187
  async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
188
+ const start = Date.now();
173
189
  try {
174
- (0, _debug_trace_1.traceRequest)(cfg.method, cfg.url, client);
175
190
  const response = await client.request(cfg);
176
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, response.status, response.headers, client);
191
+ traceHttp(cfg.method, cfg.url, start, response);
177
192
  return (await response.json());
178
193
  }
179
194
  catch (err) {
180
- if (err instanceof http_client_1.HttpError && err.response) {
181
- (0, _debug_trace_1.traceResponse)(cfg.method, cfg.url, err.response.status, err.response.headers, client);
182
- }
183
- else {
184
- (0, _debug_trace_1.traceError)(cfg.method, cfg.url, err, client);
185
- }
195
+ traceHttp(cfg.method, cfg.url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
186
196
  await mapHttpError(err, opts);
187
197
  throw err;
188
198
  }