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

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.
@@ -24,7 +24,9 @@ async function execSql(opts) {
24
24
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
25
25
  dbBranch: opts.dbBranch,
26
26
  });
27
+ const start = Date.now();
27
28
  const response = await client.post(url, { sql: opts.sql });
29
+ (0, client_1.traceHttp)("POST", url, start, response);
28
30
  if (!response.ok) {
29
31
  // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
30
32
  let body = null;
@@ -57,7 +59,9 @@ async function getSchema(opts) {
57
59
  includeStats: opts.includeStats ? "true" : undefined,
58
60
  dbBranch: opts.dbBranch,
59
61
  });
62
+ const start = Date.now();
60
63
  const response = await client.get(url);
64
+ (0, client_1.traceHttp)("GET", url, start, response);
61
65
  if (!response.ok) {
62
66
  let body = null;
63
67
  try {
@@ -73,28 +77,32 @@ async function getSchema(opts) {
73
77
  const body = (await response.json());
74
78
  return (0, client_1.extractData)(body);
75
79
  }
76
- // ── db data import → InnerImportData ──
80
+ // ── db data import → InnerAdminImportData ──
77
81
  /**
78
82
  * 导入文件。
79
- * 后端:POST /v1/dataloom/app/{appId}/data/import?tableName=...&format=csv|json&dbBranch=main
83
+ * 后端:POST /v1/dataloom/app/{appId}/data/import
80
84
  *
81
- * 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 即可。
82
91
  */
83
92
  async function importData(opts) {
84
93
  const client = (0, http_1.getHttpClient)();
85
- const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
94
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
95
+ const reqBody = {
86
96
  tableName: opts.tableName,
87
97
  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
+ records: opts.body.toString("utf8"),
99
+ };
100
+ if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
101
+ reqBody.dbBranch = opts.dbBranch;
102
+ }
103
+ const start = Date.now();
104
+ const response = await client.post(url, reqBody);
105
+ (0, client_1.traceHttp)("POST", url, start, response);
98
106
  if (!response.ok) {
99
107
  let body = null;
100
108
  try {
@@ -107,31 +115,37 @@ async function importData(opts) {
107
115
  (0, client_1.extractData)(body);
108
116
  throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
109
117
  }
110
- // 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
118
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
111
119
  const body = (await response.json());
112
120
  const data = (0, client_1.extractData)(body);
113
121
  return {
114
122
  tableName: data.tableName ?? opts.tableName,
115
- rows: data.rows ?? 0,
123
+ recordCount: data.recordCount ?? 0,
116
124
  durationMs: data.durationMs ?? 0,
117
125
  };
118
126
  }
119
- // ── db data export → InnerExportData ──
127
+ // ── db data export → InnerAdminExportData ──
120
128
  /**
121
129
  * 导出数据。
122
- * 后端: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
123
131
  *
124
- * 响应 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。
125
136
  */
126
137
  async function exportData(opts) {
127
138
  const client = (0, http_1.getHttpClient)();
128
139
  const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
129
140
  tableName: opts.tableName,
130
141
  format: opts.format,
142
+ limit: String(opts.limit ?? 5000),
131
143
  dbBranch: opts.dbBranch,
132
144
  });
133
- const reqBody = { limit: opts.limit ?? 5000 };
134
- const response = await client.post(url, reqBody);
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);
135
149
  if (!response.ok) {
136
150
  // 错误路径:body 是 JSON envelope
137
151
  let body = null;
@@ -145,7 +159,8 @@ async function exportData(opts) {
145
159
  (0, client_1.extractData)(body);
146
160
  throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
147
161
  }
148
- // 成功路径:响应 body 是原始 CSV/JSON 字节
162
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
163
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
149
164
  const contentType = response.headers.get("Content-Type") ??
150
165
  (opts.format === "csv" ? "text/csv" : "application/json");
151
166
  const ab = await response.arrayBuffer();
@@ -153,10 +168,33 @@ async function exportData(opts) {
153
168
  if (buf.length === 0) {
154
169
  throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
155
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;
156
193
  return {
157
194
  tableName: opts.tableName,
158
195
  format: opts.format,
159
196
  contentType,
160
197
  body: buf,
198
+ recordCount,
161
199
  };
162
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
  *
@@ -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) {
@@ -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,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
  }
@@ -2,73 +2,67 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerDbCommands = registerDbCommands;
4
4
  const index_1 = require("../../../cli/handlers/db/index");
5
- const shared_1 = require("../../../cli/commands/shared");
6
5
  function registerDbCommands(program) {
7
6
  const dbCmd = program
8
7
  .command("db")
9
- .description("数据操作:SQL 执行、表结构查询、数据导入导出");
8
+ .description("数据库操作:执行 SQL、查看表结构、导入导出数据");
10
9
  dbCmd.action(() => {
11
10
  dbCmd.outputHelp();
12
11
  });
13
12
  dbCmd
14
13
  .command("sql")
15
- .description("执行任意 SQL(SELECT / DML / DDL / TCL / EXPLAIN);query 省略时读 stdin")
16
- .argument("[query]", "要执行的 SQL 语句;省略时从 stdin 读取")
17
- .addOption((0, shared_1.appIdOption)())
18
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
14
+ .description("执行 SQL 语句")
15
+ .argument("[query]", "要执行的 SQL 语句")
16
+ .option("--env <env>", "目标环境(main / dev)")
19
17
  .action(async (query, opts) => {
20
18
  await (0, index_1.handleDbSql)(query, opts);
21
19
  });
22
20
  // schema 二级资源分组
23
21
  const schemaCmd = dbCmd
24
22
  .command("schema")
25
- .description("表结构查询");
23
+ .description("查看表结构");
26
24
  schemaCmd.action(() => {
27
25
  schemaCmd.outputHelp();
28
26
  });
29
27
  schemaCmd
30
28
  .command("list")
31
- .description("列出应用所有表(rows / size / columns / updated_at)")
32
- .addOption((0, shared_1.appIdOption)())
33
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
29
+ .description("列出当前应用的所有表(含行数估算、占用大小、列数)")
30
+ .option("--env <env>", "目标环境(main / dev)")
34
31
  .action(async (opts) => {
35
32
  await (0, index_1.handleDbSchemaList)(opts);
36
33
  });
37
34
  schemaCmd
38
35
  .command("get")
39
- .description("查看单表结构(字段 / 索引 / DDL)")
36
+ .description("查看单张表的字段、索引与建表语句")
40
37
  .argument("<table>", "表名")
41
- .addOption((0, shared_1.appIdOption)())
42
- .option("--ddl", "TTY 下强制输出完整 DDL 而非结构化概览")
43
- .option("--env <env>", "目标环境(main / dev);不传由后端按多环境状态兜底")
38
+ .option("--ddl", "输出完整建表语句而非概要")
39
+ .option("--env <env>", "目标环境(main / dev)")
44
40
  .action(async (table, opts) => {
45
41
  await (0, index_1.handleDbSchemaGet)(table, opts);
46
42
  });
47
43
  // data 二级资源分组
48
44
  const dataCmd = dbCmd
49
45
  .command("data")
50
- .description("数据导入 / 导出");
46
+ .description("批量导入与导出数据");
51
47
  dataCmd.action(() => {
52
48
  dataCmd.outputHelp();
53
49
  });
54
50
  dataCmd
55
51
  .command("import")
56
- .description(" CSV / JSON 文件导入数据(P0:≤ 1 MB / 5000 行,单事务原子)")
52
+ .description("把本地 CSV / JSON 文件导入到表(单次最多 5000 / 1 MB,全部成功或全部回滚)")
57
53
  .argument("<file>", "本地文件路径")
58
- .addOption((0, shared_1.appIdOption)())
59
- .option("--table <name>", "目标表名;省略时按文件名推断")
60
- .option("--format <fmt>", "csv | json;省略时按文件扩展名推断")
54
+ .option("--table <name>", "目标表名(缺省按文件名推断)")
55
+ .option("--format <fmt>", "文件格式 csv / json(缺省按扩展名推断)")
61
56
  .action(async (file, opts) => {
62
57
  await (0, index_1.handleDbDataImport)(file, opts);
63
58
  });
64
59
  dataCmd
65
60
  .command("export")
66
- .description("导出整表到 CSV / JSON(P0:≤ 1 MB / 5000 行)")
61
+ .description("把整张表导出为 CSV / JSON 文件(单次最多 5000 / 1 MB)")
67
62
  .argument("<table>", "表名")
68
- .addOption((0, shared_1.appIdOption)())
69
- .option("--format <fmt>", "csv | json;默认 csv")
70
- .option("-f, --file <path>", "输出文件路径;默认 <table>.<format>")
71
- .option("--limit <n>", "最多导出行数(≤ 5000)")
63
+ .option("--format <fmt>", "导出格式 csv / json(默认 csv)")
64
+ .option("-f, --file <path>", "输出文件路径(默认 <table>.<format>)")
65
+ .option("--limit <n>", "最多导出行数(不超过 5000)")
72
66
  .action(async (table, opts) => {
73
67
  await (0, index_1.handleDbDataExport)(table, opts);
74
68
  });
@@ -2,65 +2,73 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerFileCommands = registerFileCommands;
4
4
  const index_1 = require("../../../cli/handlers/file/index");
5
- const shared_1 = require("../../../cli/commands/shared");
5
+ const error_1 = require("../../../utils/error");
6
+ /**
7
+ * commander option 校验器:把 --limit <n> 解析成正整数(≥1)。
8
+ * 默认值(如 "50")会先经过这里被规范化成 number。
9
+ * 非整数 / 负数 / 0 抛 AppError("ARGS_INVALID"),由 main.ts 的全局 catch
10
+ * 走 emitError,同时 process.exitCode 由 commander 自然为 1。
11
+ */
12
+ function parsePositiveInt(raw) {
13
+ const n = Number(raw);
14
+ if (!Number.isInteger(n) || n < 1) {
15
+ throw new error_1.AppError("ARGS_INVALID", `--limit must be a positive integer (got '${raw}')`);
16
+ }
17
+ return n;
18
+ }
6
19
  function registerFileCommands(program) {
7
20
  const fileCmd = program
8
21
  .command("file")
9
- .description("文件操作:上传、下载、删除、查询");
22
+ .description("文件操作:上传、下载、删除、查询");
10
23
  fileCmd.action(() => {
11
24
  fileCmd.outputHelp();
12
25
  });
13
26
  fileCmd
14
27
  .command("ls")
15
28
  .description("列出应用下的文件")
16
- .argument("[query]", "可选筛选值:形似 /path、包含 / 或 16+位 fileKey 时按 path 精确匹配;否则按 file_name 精确匹配")
17
- .addOption((0, shared_1.appIdOption)())
18
- .option("--path <path>", "按 path 精确匹配(跳过自动识别)")
19
- .option("--name <name>", "按 file_name 精确匹配(跳过自动识别)")
20
- .option("--type <mime>", " MIME 类型过滤(精确匹配)")
21
- .option("--size-gt <size>", "大小大于,支持 B/KB/MB/GB")
22
- .option("--size-lt <size>", "大小小于")
23
- .option("--uploaded-since <time>", "上传时间晚于(ISO 8601)")
24
- .option("--limit <n>", "返回条数上限", "50")
25
- .option("--cursor <token>", "分页游标")
26
- .option("--all", "自动翻页聚合全部结果")
29
+ .argument("[query]", "可选筛选值:以 / 开头视为路径(精确匹配),否则按文件名匹配")
30
+ .option("--path <path>", "按路径精确匹配")
31
+ .option("--name <name>", "按文件名精确匹配")
32
+ .option("--type <mime>", "按 MIME 类型筛选(如 image/png)")
33
+ .option("--size-gt <size>", "文件大小下限(支持 B/KB/MB/GB)")
34
+ .option("--size-lt <size>", "文件大小上限(支持 B/KB/MB/GB")
35
+ .option("--uploaded-since <time>", "上传时间下限(ISO 8601)")
36
+ .option("--limit <n>", "单次返回上限(正整数,默认 50)", parsePositiveInt, 50)
37
+ .option("--cursor <token>", "分页游标,从上次响应的 next_cursor 取值")
38
+ .option("--all", "自动翻页返回全部结果")
27
39
  .action(async (query, opts) => {
28
40
  await (0, index_1.handleFileLs)({ ...opts, query });
29
41
  });
30
42
  fileCmd
31
43
  .command("stat")
32
- .description("查看指定文件的元数据")
33
- .argument("<file>", "path 或 file_name:自动识别(见 ls 命令说明)")
34
- .addOption((0, shared_1.appIdOption)())
44
+ .description("查看文件的元数据(不含下载链接,需要链接请用 sign)")
45
+ .argument("<file>", "文件的路径或文件名")
35
46
  .action(async (file, opts) => {
36
47
  await (0, index_1.handleFileStat)(file, opts);
37
48
  });
38
49
  fileCmd
39
50
  .command("cp")
40
- .description("上传或下载文件(方向由 src/dst 自动判断)")
41
- .argument("<src>", "源:本地路径远程 /path / file_name")
51
+ .description("上传或下载文件(按 src/dst 自动判断方向)")
52
+ .argument("<src>", "源:本地文件路径远程文件路径/名")
42
53
  .argument("<dst>", "目标:本地路径 或 远程路径")
43
- .addOption((0, shared_1.appIdOption)())
44
- .option("--rename <name>", "上传后使用的新文件名")
54
+ .option("--rename <name>", "上传后在远端使用的新文件名")
45
55
  .action(async (src, dst, opts) => {
46
56
  await (0, index_1.handleFileCp)(src, dst, opts);
47
57
  });
48
58
  fileCmd
49
59
  .command("rm")
50
- .description("删除一个或多个文件(best-effort)")
51
- .argument("[paths...]", "path 或 file_name:形似 /path、含 / 或 16+位 fileKey 时按 path 精确删除,其余按 file_name 查找后删除")
52
- .addOption((0, shared_1.appIdOption)())
53
- .option("-n, --name <name>", "强制按 file_name 查找后删除(跳过自动识别);可重复指定;多匹配报 AMBIGUOUS_FILE_NAME", (value, prev) => [...(prev ?? []), value])
54
- .option("-y, --yes", "跳过交互确认")
60
+ .description("批量删除文件(单次最多 100 个,部分失败不影响其他)")
61
+ .argument("[paths...]", "要删除的文件,每项可填路径或文件名(自动识别)")
62
+ .option("-n, --name <name>", "按文件名删除(可多次指定)", (value, prev) => [...(prev ?? []), value])
63
+ .option("-y, --yes", "跳过交互确认(脚本 / agent 调用必加)")
55
64
  .action(async (paths, opts) => {
56
65
  await (0, index_1.handleFileRm)(paths, opts);
57
66
  });
58
67
  fileCmd
59
68
  .command("sign")
60
- .description("生成文件临时下载链接")
61
- .argument("<file>", "path 或 file_name:自动识别(见 ls 命令说明)")
62
- .addOption((0, shared_1.appIdOption)())
63
- .option("--expires <duration>", "过期时长(默认 7d,最长 30d)")
69
+ .description("生成可分享的临时下载链接")
70
+ .argument("<file>", "文件的路径或文件名")
71
+ .option("--expires <duration>", "链接有效期,支持 30m / 24h / 7d 单位(默认 7d,最长 30d)")
64
72
  .action(async (file, opts) => {
65
73
  await (0, index_1.handleFileSign)(file, opts);
66
74
  });
@@ -1,18 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appIdOption = appIdOption;
4
3
  exports.softRequiredOption = softRequiredOption;
5
4
  exports.resolveAppId = resolveAppId;
6
5
  exports.withHelp = withHelp;
7
6
  exports.failArgs = failArgs;
8
7
  const commander_1 = require("commander");
9
8
  const error_1 = require("../../utils/error");
10
- /** --app-id option,需要应用上下文的命令自行 .addOption(appIdOption())。
11
- * Commander 的 .env() 只接受单个变量名,第二个兜底 `app_id` 在 resolveAppId 里手动检查。
12
- */
13
- function appIdOption() {
14
- return new commander_1.Option("--app-id <id>", "指定目标应用").env("MIAODA_APP_ID");
15
- }
16
9
  /**
17
10
  * soft-required: Commander 类型上 optional,runtime 校验必填。
18
11
  */
@@ -20,15 +13,16 @@ function softRequiredOption(name, desc) {
20
13
  return new commander_1.Option(name, desc);
21
14
  }
22
15
  /**
23
- * 解析 appId,优先级:CLI flag > MIAODA_APP_ID > app_id > 抛错。
24
- * app_id 是部分外部沙箱环境注入应用 ID 的别名,作为兜底兼容(小写下划线形态)。
16
+ * 解析 appId,从环境变量 MIAODA_APP_ID app_id(小写下划线,部分外部沙箱注入)读取。
17
+ * 缺失时抛 APP_ID_MISSING,由全局 catch 处理。
18
+ *
19
+ * opts 里仍保留 appId(可选),用于测试 / 高级场景显式注入;正常 CLI 不暴露此 flag。
25
20
  */
26
21
  function resolveAppId(opts) {
27
22
  const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
28
23
  if (!id) {
29
24
  throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
30
25
  next_actions: [
31
- "传入 --app-id <id>",
32
26
  "设置 export MIAODA_APP_ID=<id>",
33
27
  ],
34
28
  });
@@ -82,15 +82,15 @@ async function handleDbDataImport(file, opts) {
82
82
  data: {
83
83
  file,
84
84
  table: result.tableName,
85
- rows: result.rows,
85
+ rows: result.recordCount,
86
86
  },
87
87
  });
88
88
  return;
89
89
  }
90
90
  const tty = (0, render_1.isStdoutTty)();
91
91
  (0, output_1.emit)(tty
92
- ? `✓ Imported ${file} → table '${result.tableName}' (${String(result.rows)} rows)`
93
- : `OK Imported ${file} -> table '${result.tableName}' (${String(result.rows)} rows)`);
92
+ ? `✓ Imported ${file} → table '${result.tableName}' (${String(result.recordCount)} rows)`
93
+ : `OK Imported ${file} -> table '${result.tableName}' (${String(result.recordCount)} rows)`);
94
94
  }
95
95
  async function handleDbDataExport(table, opts) {
96
96
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -110,7 +110,8 @@ async function handleDbDataExport(table, opts) {
110
110
  throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, { next_actions: [`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`] });
111
111
  }
112
112
  await fs.writeFile(outputPath, result.body);
113
- const rows = countRows(result.body, format);
113
+ // 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
114
+ const rows = result.recordCount ?? countRows(result.body, format);
114
115
  if ((0, output_1.isJsonMode)()) {
115
116
  (0, output_1.emit)({
116
117
  data: {
@@ -55,14 +55,14 @@ async function handleDbSchemaList(opts) {
55
55
  return;
56
56
  }
57
57
  const tty = (0, render_1.isStdoutTty)();
58
- // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)
58
+ // PRD 对齐:TTY 表头用 `size`(友好格式),non-TTY 用 `size_bytes`(原始整数)。
59
+ // updated_at 暂时不展示——PG pg_catalog 不存真实表时间,详见 renderDetail 注释。
59
60
  const headers = [
60
61
  "name",
61
62
  "description",
62
63
  "estimated_row_count",
63
64
  tty ? "size" : "size_bytes",
64
65
  "columns",
65
- "updated_at",
66
66
  ];
67
67
  const rows = tables.map((t) => [
68
68
  t.name,
@@ -70,7 +70,6 @@ async function handleDbSchemaList(opts) {
70
70
  t.estimated_row_count === null ? "—" : String(t.estimated_row_count),
71
71
  t.size_bytes === null ? "—" : (tty ? (0, render_1.formatSize)(t.size_bytes) : String(t.size_bytes)),
72
72
  String(t.columns),
73
- (0, render_1.formatTime)(t.updated_at, tty),
74
73
  ]);
75
74
  (0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
76
75
  }
@@ -123,7 +122,10 @@ async function handleDbSchemaGet(table, opts) {
123
122
  function renderDetail(d, tty) {
124
123
  const systemFields = d.columns.filter((c) => c.name.startsWith("_"));
125
124
  const userFields = d.columns.filter((c) => !c.name.startsWith("_"));
126
- // PRD 的 header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size / Created / Updated
125
+ // header 布局:Name / Description / Columns(含"+ N system") / Estimated Rows / Size
126
+ // 不展示 Created / Updated:PG pg_catalog 不存表创建时间,dataloom 用 OID
127
+ // 构造的伪时间戳(baseTime=2020-01-01 + OID 秒偏移),仅保排序意义、绝对值
128
+ // 误导性强,先去掉。后续如果接 ddl_change_log 取真实时间再加回。
127
129
  const header = [
128
130
  ["Name", d.name],
129
131
  ["Description", d.description ?? "—"],
@@ -135,8 +137,6 @@ function renderDetail(d, tty) {
135
137
  ],
136
138
  ["Estimated Rows", d.estimated_row_count === null ? "—" : String(d.estimated_row_count)],
137
139
  ["Size", d.size_bytes === null ? "—" : (0, render_1.formatSize)(d.size_bytes)],
138
- ["Created", (0, render_1.formatTime)(d.created_at, tty)],
139
- ["Updated", (0, render_1.formatTime)(d.updated_at, tty)],
140
140
  ];
141
141
  const colHeaders = ["column", "type", "nullable", "default", "comment"];
142
142
  const colRows = userFields.map((c) => [
@@ -37,6 +37,7 @@ exports.handleDbSql = handleDbSql;
37
37
  const api = __importStar(require("../../../api/index"));
38
38
  const error_1 = require("../../../utils/error");
39
39
  const output_1 = require("../../../utils/output");
40
+ const config_1 = require("../../../utils/config");
40
41
  const shared_1 = require("../../../cli/commands/shared");
41
42
  const render_1 = require("../../../utils/render");
42
43
  const index_1 = require("../../../api/db/index");
@@ -47,7 +48,8 @@ const index_1 = require("../../../api/db/index");
47
48
  * - --env 透传给后端 admin-inner(dbBranch 参数),不传时由后端按 workspace
48
49
  * 多环境状态兜底(多环境 → dev / 单环境 → main);后端检测到环境不存在
49
50
  * 会返 k_dl_1600000 + "Invalid DB Branch:...",CLI 侧映射为 MULTI_ENV_NOT_INITIALIZED
50
- * - 多条语句时只展示最后一条的结果(与 PRD 对齐);所有为 DDL 时展示批量摘要
51
+ * - 多语句行为对齐 PRD:每条 statement 一个独立结果元素,pretty 逐条 +
52
+ * 末尾汇总,--json 输出 data 数组。
51
53
  */
52
54
  async function handleDbSql(query, opts) {
53
55
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -65,22 +67,16 @@ async function handleDbSql(query, opts) {
65
67
  (0, output_1.emit)("✓ No results");
66
68
  return;
67
69
  }
68
- // 全部 DDL 的批量场景:展示统计摘要(对齐 PRD `✓ 3 statements executed`)
69
- const allDdl = results.every((r) => r.sqlType === "DDL");
70
- if (allDdl && results.length > 1) {
71
- if ((0, output_1.isJsonMode)()) {
72
- (0, output_1.emit)({ data: { statements: results.length } });
73
- return;
74
- }
75
- const tty = (0, render_1.isStdoutTty)();
76
- (0, output_1.emit)(tty
77
- ? `✓ ${String(results.length)} statements executed`
78
- : `OK ${String(results.length)} statements executed`);
70
+ if (results.length === 1) {
71
+ renderSingle(results[0]);
72
+ return;
73
+ }
74
+ // 多语句:每条 statement 独立结果
75
+ if ((0, output_1.isJsonMode)()) {
76
+ (0, output_1.emit)({ data: results.map((r) => toMultiElement((0, index_1.parseSqlResult)(r))) });
79
77
  return;
80
78
  }
81
- // 其他场景:展示最后一条
82
- const last = results[results.length - 1];
83
- renderSingle(last);
79
+ renderMultiPretty(results);
84
80
  }
85
81
  /** 读取 stdin 并返回完整 SQL 文本(stdin 不是 TTY 即认为被 pipe)。 */
86
82
  async function readSql(inline) {
@@ -130,10 +126,11 @@ function renderSingle(raw) {
130
126
  }
131
127
  function toJson(parsed) {
132
128
  if (parsed.kind === "select") {
133
- return { data: parsed.rows };
129
+ // PRD 单 SELECT:data 直接是行数组(按 --json 字段投影裁剪)
130
+ return { data: projectRows(parsed.rows) };
134
131
  }
135
132
  if (parsed.kind === "dml") {
136
- // PRD:DML data = {command: "UPDATE", rows_affected: 3}
133
+ // PRD DML:data = {command, rows_affected}
137
134
  return {
138
135
  data: {
139
136
  command: parsed.sqlType,
@@ -141,14 +138,90 @@ function toJson(parsed) {
141
138
  },
142
139
  };
143
140
  }
144
- return { data: { command: "DDL" } };
141
+ // PRD 单 DDL:data = {command, target?}。command 直接用后端给的细粒度
142
+ // (CREATE_TABLE / DROP_TABLE / ...),target 待后端给对象名后再加。
143
+ return { data: { command: parsed.sqlType } };
144
+ }
145
+ /**
146
+ * 多语句 --json 元素:与单 DDL/DML 形状一致,但 SELECT 包成
147
+ * {command:"SELECT", rows:[...]}(PRD 约定,避免数组里嵌套数组造成歧义)。
148
+ */
149
+ function toMultiElement(parsed) {
150
+ if (parsed.kind === "select") {
151
+ return { command: "SELECT", rows: projectRows(parsed.rows) };
152
+ }
153
+ if (parsed.kind === "dml") {
154
+ return { command: parsed.sqlType, rows_affected: parsed.affectedRows };
155
+ }
156
+ // DDL:用后端给的细粒度 command
157
+ return { command: parsed.sqlType };
158
+ }
159
+ /**
160
+ * PRD:`--json id,name` 字段投影。--json 不带值(boolean true)等价于不裁剪。
161
+ * 字段不存在时按 undefined 处理(JSON.stringify 会忽略 undefined value 的 key),
162
+ * 这样 Agent 拿到的 row 永远只含请求过的列。
163
+ */
164
+ function projectRows(rows) {
165
+ const fields = parseJsonFields();
166
+ if (!fields)
167
+ return rows;
168
+ return rows.map((r) => {
169
+ const out = {};
170
+ for (const f of fields) {
171
+ out[f] = r[f];
172
+ }
173
+ return out;
174
+ });
175
+ }
176
+ /** 读取 --json [fields]:返回字段列表;boolean true 或 undefined 返回 null(不裁剪)。 */
177
+ function parseJsonFields() {
178
+ const v = (0, config_1.getConfig)().json;
179
+ if (typeof v !== "string" || v === "")
180
+ return null;
181
+ return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
182
+ }
183
+ /** 多语句 pretty:逐条 `Statement N: ...` + 末尾 `✓ N statements executed`。 */
184
+ function renderMultiPretty(results) {
185
+ const tty = (0, render_1.isStdoutTty)();
186
+ const lines = [];
187
+ for (let i = 0; i < results.length; i++) {
188
+ const parsed = (0, index_1.parseSqlResult)(results[i]);
189
+ const idx = i + 1;
190
+ if (parsed.kind === "select") {
191
+ const n = parsed.rows.length;
192
+ lines.push(`Statement ${String(idx)}: SELECT (${String(n)} row${n === 1 ? "" : "s"})`);
193
+ if (n > 0) {
194
+ const cols = collectColumns(parsed.rows);
195
+ const tbl = parsed.rows.map((r) => cols.map((c) => formatCell(r[c], tty)));
196
+ lines.push(tty ? (0, render_1.renderAlignedTable)(cols, tbl) : (0, render_1.renderTsv)(cols, tbl));
197
+ }
198
+ // 块间空行(最后一条不留)
199
+ if (i < results.length - 1)
200
+ lines.push("");
201
+ continue;
202
+ }
203
+ if (parsed.kind === "dml") {
204
+ const verb = dmlVerb(parsed.sqlType);
205
+ const n = parsed.affectedRows;
206
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}${String(n)} row${n === 1 ? "" : "s"} ${verb}`);
207
+ continue;
208
+ }
209
+ // DDL
210
+ lines.push(`Statement ${String(idx)}: ${tty ? "✓ " : ""}DDL executed`);
211
+ }
212
+ // 汇总行:所有 statement 都跑完了
213
+ lines.push(tty
214
+ ? `✓ ${String(results.length)} statements executed`
215
+ : `OK ${String(results.length)} statements executed`);
216
+ (0, output_1.emit)(lines.join("\n"));
145
217
  }
146
218
  function dmlVerb(type) {
147
219
  switch (type) {
148
220
  case "INSERT": return "inserted";
149
221
  case "UPDATE": return "updated";
150
222
  case "DELETE": return "deleted";
151
- case "DML": return "affected"; // MERGE / 未识别子类的兜底
223
+ case "MERGE": return "merged";
224
+ case "DML": return "affected"; // 未识别子类的兜底
152
225
  }
153
226
  }
154
227
  /** 从第一行收集列顺序;缺失列保留空白(兼容稀疏行)。 */
@@ -66,7 +66,8 @@ function resolveQueryRouting(opts) {
66
66
  }
67
67
  async function handleFileLs(opts) {
68
68
  const appId = (0, shared_1.resolveAppId)(opts);
69
- const limit = opts.limit ? Number(opts.limit) : undefined;
69
+ // commander 已经把 --limit 解析为 number;保留 ?? undefined 兼容老调用
70
+ const limit = opts.limit;
70
71
  const sizeGt = opts.sizeGt ? (0, render_1.parseSize)(opts.sizeGt) : undefined;
71
72
  const sizeLt = opts.sizeLt ? (0, render_1.parseSize)(opts.sizeLt) : undefined;
72
73
  const { path, name } = resolveQueryRouting(opts);
@@ -44,7 +44,7 @@ const shared_1 = require("../../../cli/commands/shared");
44
44
  const index_1 = require("../../../api/file/index");
45
45
  const render_1 = require("../../../utils/render");
46
46
  const node_readline_1 = __importDefault(require("node:readline"));
47
- const MAX_BATCH = 1000;
47
+ const MAX_BATCH = 100;
48
48
  /**
49
49
  * 解析位置参数(自动识别 path / file_name)与 `--name`(强制 file_name)两类输入。
50
50
  *
@@ -103,13 +103,20 @@ async function resolveDeleteInputs(appId, paths, names) {
103
103
  }
104
104
  return { resolved, errors };
105
105
  }
106
- async function confirm(count) {
106
+ /**
107
+ * 删除前 TTY 二次确认。
108
+ * PRD 单文件场景下提示带具体路径:`? Delete '/path'? (y/N)`,
109
+ * 多文件场景汇总条数:`? Delete N files? (y/N)`。
110
+ * `firstInput` 是用户传入的第一个值(可能是 path 或 file_name),
111
+ * 单文件时直接展示给用户,方便核对目标。
112
+ */
113
+ async function confirm(count, firstInput) {
107
114
  const rl = node_readline_1.default.createInterface({
108
115
  input: process.stdin,
109
116
  output: process.stderr,
110
117
  });
111
118
  return new Promise((resolve) => {
112
- const prompt = count === 1 ? `? Delete 1 file? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
119
+ const prompt = count === 1 ? `? Delete '${firstInput}'? (y/N) ` : `? Delete ${String(count)} files? (y/N) `;
113
120
  rl.question(prompt, (answer) => {
114
121
  rl.close();
115
122
  resolve(answer.trim().toLowerCase() === "y");
@@ -123,13 +130,16 @@ async function handleFileRm(paths, opts) {
123
130
  throw new error_1.AppError("ARGS_INVALID", "No file specified (give a /path or --name <name>)");
124
131
  }
125
132
  if (totalCount > MAX_BATCH) {
126
- throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 1000 limit`);
133
+ throw new error_1.AppError("FILE_BATCH_TOO_MANY", `Batch size ${String(totalCount)} exceeds the 100 limit`);
127
134
  }
128
135
  const appId = (0, shared_1.resolveAppId)(opts);
129
136
  // destructive guardrail
130
137
  const tty = (0, render_1.isStdoutTty)();
131
138
  if (tty && !opts.yes) {
132
- const ok = await confirm(totalCount);
139
+ // 单文件提示带具体目标方便用户核对;多文件传第一个 input 占位(实际只显示 N files)
140
+ // 上面已校验 totalCount > 0,paths/names 至少有一个非空
141
+ const firstInput = paths.length > 0 ? paths[0] : names[0];
142
+ const ok = await confirm(totalCount, firstInput);
133
143
  if (!ok) {
134
144
  throw new error_1.AppError("DESTRUCTIVE_CANCELLED", "Cancelled by user");
135
145
  }
@@ -173,6 +183,7 @@ async function handleFileRm(paths, opts) {
173
183
  input,
174
184
  code: "INTERNAL_ERROR",
175
185
  message: "Delete request returned success but file still exists (server-side issue)",
186
+ hint: undefined,
176
187
  };
177
188
  }
178
189
  catch (err) {
@@ -181,12 +192,14 @@ async function handleFileRm(paths, opts) {
181
192
  input,
182
193
  code: "FILE_NOT_FOUND",
183
194
  message: `File '${input}' does not exist at delete time`,
195
+ hint: "Run `miaoda file ls` to see available files.",
184
196
  };
185
197
  }
186
198
  return {
187
199
  input,
188
200
  code: "INTERNAL_ERROR",
189
201
  message: err instanceof Error ? err.message : "verification failed",
202
+ hint: undefined,
190
203
  };
191
204
  }
192
205
  }));
@@ -194,7 +207,9 @@ async function handleFileRm(paths, opts) {
194
207
  results.push({
195
208
  status: "error",
196
209
  input: c.input,
197
- error: { code: c.code, message: c.message },
210
+ error: c.hint
211
+ ? { code: c.code, message: c.message, hint: c.hint }
212
+ : { code: c.code, message: c.message },
198
213
  });
199
214
  }
200
215
  }
@@ -233,7 +248,12 @@ async function handleFileRm(paths, opts) {
233
248
  else
234
249
  lines.push(`FAIL\t${r.input}\t${r.error.message}`);
235
250
  }
236
- lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
251
+ if (failCount === 0) {
252
+ lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files`);
253
+ }
254
+ else {
255
+ lines.push(`Deleted ${String(okCount)} of ${String(totalCount)} files (${String(failCount)} failed)`);
256
+ }
237
257
  (0, output_1.emit)(lines.join("\n"));
238
258
  }
239
259
  // 退出码:任一失败 → 1;全成功 → 0
@@ -5,12 +5,14 @@ class AppError extends Error {
5
5
  code;
6
6
  retryable;
7
7
  next_actions;
8
+ statement_index;
8
9
  constructor(code, message, opts) {
9
10
  super(message);
10
11
  this.name = "AppError";
11
12
  this.code = code;
12
13
  this.retryable = opts?.retryable ?? false;
13
14
  this.next_actions = opts?.next_actions ?? [];
15
+ this.statement_index = opts?.statement_index;
14
16
  }
15
17
  toJSON() {
16
18
  return {
@@ -18,6 +20,7 @@ class AppError extends Error {
18
20
  message: this.message,
19
21
  retryable: this.retryable,
20
22
  next_actions: this.next_actions,
23
+ statement_index: this.statement_index,
21
24
  };
22
25
  }
23
26
  }
@@ -12,7 +12,7 @@ let runtimeClient;
12
12
  /**
13
13
  * 获取单例 HttpClient(默认:管理端 innerapi)。
14
14
  *
15
- * 管理端链路仅适用于妙搭开发态——baseURL 从 `MIAODA_DEV_INNER_DOMAIN` 读取,
15
+ * 管理端链路仅适用于妙搭开发态——baseURL 从 `MIAODA_DEV_INNER_DOMAIN_WITH_PREFIX` 读取,
16
16
  * 每次请求自动从 `MIAODA_AUTHN_CODE` 读取用户凭证并注入 `X-Miaoda-Client-Token`。
17
17
  * AK/SK 的 `Authorization` / `x-api-key` 照旧叠加。
18
18
  *
@@ -44,6 +44,9 @@ function emitError(err) {
44
44
  if (info.next_actions && info.next_actions.length > 0) {
45
45
  errObj.hint = info.next_actions.join(" ");
46
46
  }
47
+ if (typeof info.statement_index === "number") {
48
+ errObj.statement_index = info.statement_index;
49
+ }
47
50
  process.stderr.write(JSON.stringify({ error: errObj }) + "\n");
48
51
  }
49
52
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.0-alpha.ca2912e",
3
+ "version": "0.1.0-alpha.ec1a658",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "node": ">=20"
26
26
  },
27
27
  "dependencies": {
28
- "@lark-apaas/http-client": "^0.1.4",
28
+ "@lark-apaas/http-client": "^0.1.5",
29
29
  "commander": "^13.1.0"
30
30
  },
31
31
  "devDependencies": {