@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.
- package/dist/api/db/api.js +61 -61
- package/dist/api/db/client.js +52 -4
- package/dist/api/db/parsers.js +10 -6
- package/dist/api/file/api.js +7 -2
- package/dist/api/file/client.js +36 -26
- package/dist/cli/commands/db/index.js +135 -32
- package/dist/cli/commands/file/index.js +132 -35
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +4 -10
- package/dist/cli/handlers/db/data.js +5 -4
- package/dist/cli/handlers/db/schema.js +6 -6
- package/dist/cli/handlers/db/sql.js +94 -19
- package/dist/cli/handlers/file/ls.js +2 -1
- package/dist/cli/handlers/file/rm.js +27 -7
- package/dist/cli/help.js +84 -0
- package/dist/main.js +8 -0
- package/dist/utils/error.js +3 -0
- package/dist/utils/http.js +1 -1
- package/dist/utils/output.js +8 -5
- package/dist/utils/render.js +32 -2
- package/package.json +2 -2
- package/dist/api/db/_debug_trace.js +0 -83
- package/dist/api/file/_debug_trace.js +0 -70
package/dist/api/db/api.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 →
|
|
80
|
+
// ── db data import → InnerAdminImportData ──
|
|
97
81
|
/**
|
|
98
82
|
* 导入文件。
|
|
99
|
-
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
83
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
100
84
|
*
|
|
101
|
-
*
|
|
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
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
// 后端
|
|
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
|
-
|
|
123
|
+
recordCount: data.recordCount ?? 0,
|
|
145
124
|
durationMs: data.durationMs ?? 0,
|
|
146
125
|
};
|
|
147
126
|
}
|
|
148
|
-
// ── db data export →
|
|
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
|
-
*
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
}
|
package/dist/api/db/client.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/api/db/parsers.js
CHANGED
|
@@ -27,16 +27,23 @@ function parseSqlResult(r) {
|
|
|
27
27
|
recordCount: r.recordCount ?? rows.length,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
if (r.sqlType === "INSERT" ||
|
|
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
|
-
|
|
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) {
|
package/dist/api/file/api.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/dist/api/file/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
(
|
|
163
|
+
traceHttp("GET", url, start, response);
|
|
139
164
|
return (await response.json());
|
|
140
165
|
}
|
|
141
166
|
catch (err) {
|
|
142
|
-
|
|
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
|
-
(
|
|
177
|
+
traceHttp("POST", url, start, response);
|
|
158
178
|
return (await response.json());
|
|
159
179
|
}
|
|
160
180
|
catch (err) {
|
|
161
|
-
|
|
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
|
-
(
|
|
191
|
+
traceHttp(cfg.method, cfg.url, start, response);
|
|
177
192
|
return (await response.json());
|
|
178
193
|
}
|
|
179
194
|
catch (err) {
|
|
180
|
-
|
|
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
|
}
|