@lark-apaas/miaoda-cli 0.1.0-alpha.097a394 → 0.1.0-alpha.0d863af

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,35 +6,63 @@ 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");
10
- /** dataloom 默认 dbBranch —— 单环境应用均传 main */
11
- const DEFAULT_DB_BRANCH = "main";
12
- // ── db sql → InnerExecuteSQL ──
13
11
  /**
14
- * 执行 SQL。
15
- * 后端:POST /v1/app/{appId}/dataloom/sql?dbBranch=main
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
+ }
39
+ // CLI 不再为 dbBranch 设默认值:
40
+ // 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
41
+ // 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
42
+ // 这样可以避免单环境应用被强行打到 main 之外、或多环境应用被默认打到 main 上线库。
43
+ // ── db sql → InnerAdminExecuteSQL ──
44
+ /**
45
+ * 执行 SQL(admin-inner)。
46
+ * 后端:POST /v1/dataloom/app/{appId}/db/sql?dbBranch=main
16
47
  *
17
48
  * 返回所有 results[](多条语句时每条一项);CLI 侧按 PRD 仅取最后一条展示,
18
49
  * 但 API 层保留完整列表以便测试和高级用法。
19
50
  */
20
51
  async function execSql(opts) {
21
52
  const client = (0, http_1.getHttpClient)();
22
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/sql", {
23
- dbBranch: opts.dbBranch ?? DEFAULT_DB_BRANCH,
53
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
54
+ dbBranch: opts.dbBranch,
24
55
  });
25
- const response = await client.post(url, { sql: opts.sql });
26
- if (!response.ok) {
27
- // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
28
- let body = null;
29
- try {
30
- body = (await response.json());
31
- }
32
- catch {
33
- // ignore
34
- }
35
- if (body)
36
- (0, client_1.extractData)(body);
37
- 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; // 不可达
38
66
  }
39
67
  const body = (await response.json());
40
68
  const data = (0, client_1.extractData)(body);
@@ -42,108 +70,108 @@ async function execSql(opts) {
42
70
  }
43
71
  // ── db schema → InnerGetSchema ──
44
72
  /**
45
- * 查询 schema
46
- * 后端:GET /v1/app/{appId}/dataloom/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
73
+ * 查询 schema(admin-inner)。
74
+ * 后端:GET /v1/dataloom/app/{appId}/db/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
47
75
  *
48
76
  * 返回 body.data(含 `schema` 或 `ddl`)供 handler 使用。
49
77
  */
50
78
  async function getSchema(opts) {
51
79
  const client = (0, http_1.getHttpClient)();
52
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/schema", {
80
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/schema", {
53
81
  format: opts.format ?? "schema",
54
82
  tableNames: opts.tableNames,
55
83
  includeStats: opts.includeStats ? "true" : undefined,
56
- dbBranch: opts.dbBranch ?? DEFAULT_DB_BRANCH,
84
+ dbBranch: opts.dbBranch,
57
85
  });
58
- const response = await client.get(url);
59
- if (!response.ok) {
60
- let body = null;
61
- try {
62
- body = (await response.json());
63
- }
64
- catch {
65
- // ignore
66
- }
67
- if (body)
68
- (0, client_1.extractData)(body);
69
- 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; // 不可达
70
96
  }
71
97
  const body = (await response.json());
72
98
  return (0, client_1.extractData)(body);
73
99
  }
74
- // ── db data import → InnerImportData ──
100
+ // ── db data import → InnerAdminImportData ──
75
101
  /**
76
102
  * 导入文件。
77
- * 后端:POST /v1/app/{appId}/dataloom/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?}
78
107
  *
79
- * Body 为原始文件字节(不走 JSON envelope)。
108
+ * `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
109
+ * Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
110
+ * 字符串后塞进 envelope 即可。
80
111
  */
81
112
  async function importData(opts) {
82
113
  const client = (0, http_1.getHttpClient)();
83
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
114
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
115
+ const reqBody = {
84
116
  tableName: opts.tableName,
85
117
  format: opts.format,
86
- dbBranch: opts.dbBranch ?? DEFAULT_DB_BRANCH,
87
- });
88
- const contentType = opts.format === "csv" ? "text/csv" : "application/json";
89
- const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
90
- const response = await client.request({
91
- method: "POST",
92
- url,
93
- headers: { "Content-Type": contentType },
94
- body: ab,
95
- });
96
- if (!response.ok) {
97
- let body = null;
98
- try {
99
- body = (await response.json());
100
- }
101
- catch {
102
- // ignore
103
- }
104
- if (body)
105
- (0, client_1.extractData)(body);
106
- 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; // 不可达
107
133
  }
108
- // 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
134
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
109
135
  const body = (await response.json());
110
136
  const data = (0, client_1.extractData)(body);
111
137
  return {
112
138
  tableName: data.tableName ?? opts.tableName,
113
- rows: data.rows ?? 0,
139
+ recordCount: data.recordCount ?? 0,
114
140
  durationMs: data.durationMs ?? 0,
115
141
  };
116
142
  }
117
- // ── db data export → InnerExportData ──
143
+ // ── db data export → InnerAdminExportData ──
118
144
  /**
119
145
  * 导出数据。
120
- * 后端:POST /v1/app/{appId}/dataloom/data/export?tableName=...&format=csv|json&dbBranch=main
146
+ * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
121
147
  *
122
- * 响应 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。
123
152
  */
124
153
  async function exportData(opts) {
125
154
  const client = (0, http_1.getHttpClient)();
126
155
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
127
156
  tableName: opts.tableName,
128
157
  format: opts.format,
129
- dbBranch: opts.dbBranch ?? DEFAULT_DB_BRANCH,
158
+ limit: String(opts.limit ?? 5000),
159
+ dbBranch: opts.dbBranch,
130
160
  });
131
- const reqBody = { limit: opts.limit ?? 5000 };
132
- const response = await client.post(url, reqBody);
133
- if (!response.ok) {
134
- // 错误路径:body 是 JSON envelope
135
- let body = null;
136
- try {
137
- body = (await response.json());
138
- }
139
- catch {
140
- // ignore
141
- }
142
- if (body)
143
- (0, client_1.extractData)(body);
144
- 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; // 不可达
145
172
  }
146
- // 成功路径:响应 body 是原始 CSV/JSON 字节
173
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
174
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
147
175
  const contentType = response.headers.get("Content-Type") ??
148
176
  (opts.format === "csv" ? "text/csv" : "application/json");
149
177
  const ab = await response.arrayBuffer();
@@ -151,10 +179,33 @@ async function exportData(opts) {
151
179
  if (buf.length === 0) {
152
180
  throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
153
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;
154
204
  return {
155
205
  tableName: opts.tableName,
156
206
  format: opts.format,
157
207
  contentType,
158
208
  body: buf,
209
+ recordCount,
159
210
  };
160
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
  *
@@ -19,21 +54,27 @@ 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 通用参数错误,不能整体映射;
31
- // 只有 message 以 "Invalid DB Branch" 开头的情况对应"用户传了 --env 但 dbBranch 不存在 / 多环境未初始化",
32
- // 按技术方案固定映射为 MULTI_ENV_NOT_INITIALIZED(含固定 message 与 hint)。
70
+ // message 以 "Invalid DB Branch" 开头的两种触发场景:
71
+ // 1) 单环境应用使用 --env(多环境未初始化)
72
+ // 2) 多环境应用传入了不存在的 env 名(如 --env staging)
73
+ // 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
33
74
  if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
34
75
  throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
35
76
  next_actions: [
36
- "Current app uses a single database for dev and production. Run `miaoda db migration init` to set up multi-env.",
77
+ "Verify the --env value matches an existing dbBranch.",
37
78
  ],
38
79
  });
39
80
  }
@@ -42,10 +83,19 @@ function ensureInnerSuccess(body) {
42
83
  if (mapped) {
43
84
  throw new error_1.AppError(mapped.code, mapped.message ?? message, {
44
85
  next_actions: mapped.hint ? [mapped.hint] : undefined,
86
+ statement_index: stmtIdx,
45
87
  });
46
88
  }
47
89
  // 兜底:dataloom 未映射的 code 原样透传
48
- 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*/, "");
49
99
  }
50
100
  /** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
51
101
  function extractSqlstate(msg) {
@@ -89,11 +139,19 @@ function extractData(body) {
89
139
  return body.data;
90
140
  }
91
141
  /**
92
- * 组装 `/v1/app/{appId}/dataloom/...` URL
142
+ * 组装 `/v1/dataloom/app/{appId}/...` URL(admin-inner 链路)。
143
+ *
144
+ * 后端 IDL 已把 dataloom 的 4 个 inner 接口切到管理态(ForceAdminKctx),
145
+ * 路径前缀也统一改为 `/v1/dataloom/app/:appID/...`:
146
+ * - sql: POST /v1/dataloom/app/{appId}/db/sql
147
+ * - schema: GET /v1/dataloom/app/{appId}/db/schema
148
+ * - import: POST /v1/dataloom/app/{appId}/data/import
149
+ * - export: POST /v1/dataloom/app/{appId}/data/export
150
+ * 调用方传入的 path 已包含 `/db` / `/data` 中段,这里只负责拼前缀和 query。
93
151
  */
94
152
  function buildInnerUrl(appId, path, query) {
95
153
  const normalized = path.startsWith("/") ? path : `/${path}`;
96
- let url = `/v1/app/${encodeURIComponent(appId)}/dataloom${normalized}`;
154
+ let url = `/v1/dataloom/app/${encodeURIComponent(appId)}${normalized}`;
97
155
  if (query) {
98
156
  const usp = new URLSearchParams();
99
157
  for (const [k, v] of Object.entries(query)) {
@@ -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) {
@@ -36,7 +36,7 @@ function extractEnvelope(body) {
36
36
  // ── list ──
37
37
  /** 底层单次 list 调用(一页)。支持 filterExpr / sortBy 下推。 */
38
38
  async function listOnce(appId, bucketId, opts) {
39
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/list`;
39
+ const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/list`;
40
40
  const reqBody = {
41
41
  bucketID: bucketId,
42
42
  maxKeys: opts.limit,
@@ -94,7 +94,7 @@ function buildFilterExpr(opts) {
94
94
  /**
95
95
  * 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
96
96
  *
97
- * 后端:POST /api/v1/storage/inner/app/{appId}/bucket/{bucketId}/list
97
+ * 后端:POST /v1/storage/app/{appId}/bucket/{bucketId}/list
98
98
  */
99
99
  async function listFiles(opts) {
100
100
  const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
@@ -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
  }
@@ -234,12 +234,12 @@ async function resolveByName(appId, input) {
234
234
  // ── stat ──
235
235
  /**
236
236
  * 查看文件元数据。
237
- * 后端:GET /api/v1/storage/inner/app/{appId}/object/{bucketId}/{filePath}/head
237
+ * 后端:GET /v1/storage/app/{appId}/object/{bucketId}/{filePath}/head
238
238
  */
239
239
  async function statFile(opts) {
240
240
  const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
241
241
  // 对齐 file-storage-skill Py 版:URL 顺序是 .../object/{bucket}/head/{path}
242
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(opts.appId)}/object/${encodeURIComponent(bucketId)}/head/${encodePath(opts.filePath)}`;
242
+ const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/${encodeURIComponent(bucketId)}/head/${encodePath(opts.filePath)}`;
243
243
  const body = await (0, client_1.doGet)(url, {
244
244
  notFoundCode: "FILE_NOT_FOUND",
245
245
  notFoundMessage: `File '${opts.filePath}' does not exist`,
@@ -251,12 +251,12 @@ async function statFile(opts) {
251
251
  // ── 预签上传 URL ──
252
252
  async function preUpload(appId, req) {
253
253
  const bucketId = await (0, client_1.getDefaultBucketId)(appId);
254
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/preUpload`;
254
+ const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/preUpload`;
255
255
  const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: "pre-upload" });
256
256
  return extractEnvelope(body);
257
257
  }
258
258
  async function uploadCallback(appId, req) {
259
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(appId)}/object/callback`;
259
+ const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
260
260
  const body = await (0, client_1.doPost)(url, req, { errorContext: "upload callback" });
261
261
  (0, client_1.ensureSuccess)(body);
262
262
  }
@@ -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
  }
@@ -332,11 +334,11 @@ async function uploadFile(opts) {
332
334
  // ── 预签下载 URL ──
333
335
  /**
334
336
  * 获取预签下载 URL。
335
- * 后端:POST /api/v1/storage/inner/app/{appId}/object/sign/{bucketId}/{filePath}
337
+ * 后端:POST /v1/storage/app/{appId}/object/sign/{bucketId}/{filePath}
336
338
  */
337
339
  async function signDownload(opts) {
338
340
  const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
339
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(opts.appId)}/object/sign/${encodeURIComponent(bucketId)}/${encodePath(opts.filePath)}`;
341
+ const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/sign/${encodeURIComponent(bucketId)}/${encodePath(opts.filePath)}`;
340
342
  const reqBody = {
341
343
  bucketID: bucketId,
342
344
  filePath: toApiPath(opts.filePath),
@@ -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
  }
@@ -381,11 +386,11 @@ async function downloadFile(opts) {
381
386
  // ── 批量删除(best-effort) ──
382
387
  /**
383
388
  * 批量删除文件。
384
- * 后端:DELETE /api/v1/storage/inner/app/{appId}/bucket/{bucketId}/delete
389
+ * 后端:DELETE /v1/storage/app/{appId}/bucket/{bucketId}/delete
385
390
  */
386
391
  async function deleteFiles(opts) {
387
392
  const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
388
- const url = `/api/v1/storage/inner/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/delete`;
393
+ const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/delete`;
389
394
  const reqBody = {
390
395
  bucketID: bucketId,
391
396
  filePaths: opts.filePaths.map(toApiPath),