@lark-apaas/miaoda-cli 0.1.3-alpha.fbac0be → 0.1.3-alpha.fc1e1a9

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.
@@ -13,6 +13,7 @@ exports.migrate = migrate;
13
13
  exports.getMigrationStatus = getMigrationStatus;
14
14
  exports.recover = recover;
15
15
  exports.getRecoveryPreview = getRecoveryPreview;
16
+ exports.getRecoveryStatus = getRecoveryStatus;
16
17
  exports.getDbQuota = getDbQuota;
17
18
  const http_1 = require("../../utils/http");
18
19
  const error_1 = require("../../utils/error");
@@ -572,6 +573,29 @@ async function getRecoveryPreview(opts) {
572
573
  const body = (await response.json());
573
574
  return (0, client_1.extractData)(body);
574
575
  }
576
+ /**
577
+ * 后端:GET /v1/dataloom/app/{appId}/db/recovery/status
578
+ * CLI apply 触发后定时调本接口直到 status=success/failed。dataloom 内部 Redis
579
+ * 维护 workspace 级 restore 状态,无需传 task id;workspace+dbBranch 维度同时
580
+ * 只允许一个 restore 进行中。
581
+ */
582
+ async function getRecoveryStatus(opts) {
583
+ const client = (0, http_1.getHttpClient)();
584
+ const url = (0, client_1.buildInnerUrl)(opts.appId, '/db/recovery/status', { dbBranch: opts.dbBranch });
585
+ const start = Date.now();
586
+ let response;
587
+ try {
588
+ response = await client.get(url);
589
+ (0, client_1.traceHttp)('GET', url, start, response);
590
+ }
591
+ catch (err) {
592
+ (0, client_1.traceHttp)('GET', url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
593
+ await mapDbHttpError(err, url, 'Failed to get recovery status');
594
+ throw err; // 不可达
595
+ }
596
+ const body = (await response.json());
597
+ return (0, client_1.extractData)(body);
598
+ }
575
599
  // ── db quota → InnerAdminGetDbQuota ──
576
600
  /**
577
601
  * 后端:GET /v1/dataloom/app/{appId}/db/quota?dbBranch=
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.getRecoveryPreview = exports.recover = exports.getMigrationStatus = exports.migrate = exports.migrationInit = exports.listAuditLog = exports.setAuditConfig = exports.getAuditStatus = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
3
+ exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.getRecoveryStatus = exports.getRecoveryPreview = exports.recover = exports.getMigrationStatus = exports.migrate = exports.migrationInit = exports.listAuditLog = exports.setAuditConfig = exports.getAuditStatus = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
4
4
  var api_1 = require("./api");
5
5
  Object.defineProperty(exports, "execSql", { enumerable: true, get: function () { return api_1.execSql; } });
6
6
  Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return api_1.getSchema; } });
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "migrate", { enumerable: true, get: function () {
15
15
  Object.defineProperty(exports, "getMigrationStatus", { enumerable: true, get: function () { return api_1.getMigrationStatus; } });
16
16
  Object.defineProperty(exports, "recover", { enumerable: true, get: function () { return api_1.recover; } });
17
17
  Object.defineProperty(exports, "getRecoveryPreview", { enumerable: true, get: function () { return api_1.getRecoveryPreview; } });
18
+ Object.defineProperty(exports, "getRecoveryStatus", { enumerable: true, get: function () { return api_1.getRecoveryStatus; } });
18
19
  Object.defineProperty(exports, "getDbQuota", { enumerable: true, get: function () { return api_1.getDbQuota; } });
19
20
  var client_1 = require("./client");
20
21
  Object.defineProperty(exports, "buildInnerUrl", { enumerable: true, get: function () { return client_1.buildInnerUrl; } });
@@ -90,7 +90,20 @@ const BIZ_ERR_MAP = new Map(Object.entries({
90
90
  message: 'A file at this path already exists',
91
91
  hint: 'Rename the target or delete the existing file first (`miaoda file rm`).',
92
92
  },
93
+ // k_ec_000040:file-storage 上游引擎层连不上下游服务(依赖抖动 / 远端 RPC 失败)。
94
+ // 文案不向用户暴露内部 code,统一成 INTERNAL_API_ERROR + 重试引导。
95
+ k_ec_000040: {
96
+ code: 'INTERNAL_API_ERROR',
97
+ message: 'Service temporarily unavailable',
98
+ hint: 'Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team.',
99
+ },
93
100
  }));
101
+ // k_ec_* 命名空间是 file-storage 引擎层通用错误(基础设施 / 上游 RPC),不是
102
+ // 用户输入问题。无显式条目时统一兜底成 INTERNAL_API_ERROR + 重试 hint,避免
103
+ // 暴露内部 code 给最终用户。
104
+ function isEngineCommonError(code) {
105
+ return /^k_ec_\d+$/.test(code);
106
+ }
94
107
  function ensureSuccess(body) {
95
108
  // 后端 envelope 字段历史遗留多种命名:
96
109
  // - ErrorCode / error_code: 部分接口
@@ -106,6 +119,15 @@ function ensureSuccess(body) {
106
119
  next_actions: mapped.hint ? [mapped.hint] : undefined,
107
120
  });
108
121
  }
122
+ // k_ec_* 引擎层错误统一兜底:用户视角是"服务暂时不可用",重试即可,
123
+ // 不要把 `File API error [k_ec_000xxx]: <内部翻译>` 这种半成品文案抛给用户。
124
+ if (isEngineCommonError(code)) {
125
+ throw new error_1.AppError('INTERNAL_API_ERROR', 'Service temporarily unavailable', {
126
+ next_actions: [
127
+ `Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team. (upstream code: ${code})`,
128
+ ],
129
+ });
130
+ }
109
131
  throw new error_1.AppError(`FILE_API_${code}`, `File API error [${code}]: ${backendMsg}`);
110
132
  }
111
133
  /** 从 HttpError 的 response 里尝试读 body,用于拿后端返的业务 ErrorCode。 */
@@ -130,6 +152,24 @@ async function mapHttpError(err, opts) {
130
152
  if (body) {
131
153
  const code = body.ErrorCode ?? body.error_code ?? '';
132
154
  if (code && code !== '0') {
155
+ const mapped = BIZ_ERR_MAP.get(code);
156
+ if (mapped) {
157
+ const msg = body.ErrorMessage ??
158
+ body.error_message ??
159
+ body.Message ??
160
+ mapped.message ??
161
+ err.message;
162
+ throw new error_1.AppError(mapped.code, mapped.message ?? msg, {
163
+ next_actions: mapped.hint ? [mapped.hint] : undefined,
164
+ });
165
+ }
166
+ if (isEngineCommonError(code)) {
167
+ throw new error_1.AppError('INTERNAL_API_ERROR', 'Service temporarily unavailable', {
168
+ next_actions: [
169
+ `Please retry the command. If it keeps failing, share the x-tt-logid (via --verbose) with the on-call team. (upstream code: ${code})`,
170
+ ],
171
+ });
172
+ }
133
173
  const msg = body.ErrorMessage ?? body.error_message ?? body.Message ?? err.message;
134
174
  throw new error_1.AppError(`FILE_API_${code}`, `File API error [${code}]: ${msg}`);
135
175
  }
@@ -79,7 +79,7 @@ Examples:
79
79
  .command('list')
80
80
  .summary('列出所有表的概览(表名、行数、大小等)')
81
81
  .description('列出当前应用所有表的概览信息:表名、描述、行数、大小、列数、最近更新时间。\n' +
82
- '查看单表完整结构请用 `schema get`;查看 DDL 变更历史请用 `changelog`(P1)。')
82
+ '查看单表完整结构请用 `schema get`;查看 DDL 变更历史请用 `changelog`。')
83
83
  .usage('[flags]')
84
84
  .action(async function () {
85
85
  await (0, index_1.handleDbSchemaList)({ ...this.optsWithGlobals(), appId: (0, shared_1.resolveAppId)({}) });
@@ -212,9 +212,29 @@ Examples:
212
212
  `);
213
213
  fileCmd
214
214
  .command('quota')
215
- .summary('查看文件存储用量与限额')
215
+ .summary('查看文件存储的用量与配额')
216
+ .description('查看文件存储的用量与配额。')
216
217
  .usage('[flags]')
217
218
  .action(async function () {
218
219
  await (0, index_1.handleFileQuota)(this.optsWithGlobals());
219
- });
220
+ })
221
+ .addHelpText('after', `
222
+ Examples:
223
+ $ miaoda file quota
224
+ Storage: 150 MB / 1 GB (15%)
225
+ Files: 42
226
+
227
+ # --json
228
+ $ miaoda file quota --json
229
+ {
230
+ "data": {
231
+ "storage_used_bytes": 157286400,
232
+ "storage_quota_bytes": 1073741824,
233
+ "usage_percent": 15,
234
+ "files": 42
235
+ },
236
+ "next_cursor": null,
237
+ "has_more": false
238
+ }
239
+ `);
220
240
  }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ // changelog / audit 共用:把后端透传的 operator 字符串还原成 {id, name}。
3
+ //
4
+ // 后端约定:Operator 字段值是 `{"id":"<user_id>","name":"<resolved_name>"}` JSON
5
+ // 字符串(dataloom inner_service.encodeOperator 生成),用同一字段同时承载机器可
6
+ // 识别的 user_id 与人类可读 name。
7
+ //
8
+ // 选择"字符串内嵌 JSON"而不是 IDL struct,是为了避免 dataloom_inner.thrift +
9
+ // kitex_gen 跨仓库改造;CLI 端 JSON.parse 拆开即可分场景渲染:
10
+ // - --json 模式:返 {id, name} 对象(agent / 下游能区分同名用户)
11
+ // - pretty 模式:只取 name(兼容 PRD 原始 string 形态)
12
+ //
13
+ // 历史数据 / 旧版后端没接这层时,operator 仍是纯字符串。该函数兼容:
14
+ // - 解析失败 → 当作 {id: raw, name: raw}
15
+ // - 解析成功但缺字段 → 用现有字段兜底另一个
16
+ // 解析失败默认不报错,保留 raw 作为兜底显示文本。
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.parseOperator = parseOperator;
19
+ function parseOperator(raw) {
20
+ if (raw === undefined || raw === null || raw === '') {
21
+ return { id: '', name: '' };
22
+ }
23
+ if (!raw.startsWith('{')) {
24
+ return { id: raw, name: raw };
25
+ }
26
+ try {
27
+ const obj = JSON.parse(raw);
28
+ const id = typeof obj.id === 'string' ? obj.id : '';
29
+ const name = typeof obj.name === 'string' && obj.name !== '' ? obj.name : id;
30
+ return { id, name };
31
+ }
32
+ catch {
33
+ return { id: raw, name: raw };
34
+ }
35
+ }
@@ -44,6 +44,7 @@ const error_1 = require("../../../utils/error");
44
44
  const output_1 = require("../../../utils/output");
45
45
  const render_1 = require("../../../utils/render");
46
46
  const time_1 = require("../../../utils/time");
47
+ const _operator_1 = require("../../../cli/handlers/db/_operator");
47
48
  const VALID_RETENTION = new Set(['7d', '30d', '180d', '360d', 'forever']);
48
49
  async function handleDbAuditStatus(table, opts) {
49
50
  const appId = (0, shared_1.resolveAppId)(opts);
@@ -198,25 +199,32 @@ async function handleDbAuditList(tables, opts) {
198
199
  // 后端管理(本质也是 ISO 时间),CLI 不再混用 cursor/since。
199
200
  const since = normalizeTime(opts.since, '--since');
200
201
  const until = normalizeTime(opts.until, '--until');
201
- const result = await api.db.listAuditLog({
202
- appId,
203
- tables,
204
- since,
205
- until,
206
- limit,
207
- cursor: opts.cursor,
208
- dbBranch: opts.env,
209
- });
202
+ let result;
203
+ try {
204
+ result = await api.db.listAuditLog({
205
+ appId,
206
+ tables,
207
+ since,
208
+ until,
209
+ limit,
210
+ cursor: opts.cursor,
211
+ dbBranch: opts.env,
212
+ });
213
+ }
214
+ catch (err) {
215
+ throw decorateAuditListError(err, tables);
216
+ }
210
217
  const visible = result.items;
211
218
  const skipped = result.skipped;
212
219
  // PRD JSON:data 是数组,元素是事件对象(snake_case),分页信封 next_cursor / has_more
220
+ // operator 后端用 JSON 字符串内嵌 {id, name},--json 输出还原成对象供下游消费
213
221
  if ((0, output_1.isJsonMode)()) {
214
222
  const items = visible.map((it) => ({
215
223
  event_id: it.eventId,
216
224
  event_time: it.eventTime,
217
225
  target_table: it.targetTable,
218
226
  type: it.type,
219
- operator: it.operator,
227
+ operator: (0, _operator_1.parseOperator)(it.operator),
220
228
  summary: it.summary,
221
229
  // before/after 服务端是 JSON 字符串,反序列化回结构化对象供下游消费
222
230
  ...(it.before !== undefined ? { before: safeParseJson(it.before) } : {}),
@@ -224,7 +232,7 @@ async function handleDbAuditList(tables, opts) {
224
232
  }));
225
233
  (0, output_1.emitPaged)(items, result.nextCursor, result.hasMore);
226
234
  if (skipped.length > 0) {
227
- process.stderr.write(`(${String(skipped.length)} table(s) skipped: ${skipped.join(', ')})\n`);
235
+ process.stderr.write(formatSkippedHint(skipped, tables.length) + '\n');
228
236
  }
229
237
  if (visible.length === 0)
230
238
  process.exitCode = 1;
@@ -233,7 +241,7 @@ async function handleDbAuditList(tables, opts) {
233
241
  if (visible.length === 0) {
234
242
  (0, output_1.emit)('No audit log entries found.');
235
243
  if (skipped.length > 0) {
236
- process.stderr.write(`(${String(skipped.length)} table(s) skipped: ${skipped.join(', ')})\n`);
244
+ process.stderr.write(formatSkippedHint(skipped, tables.length) + '\n');
237
245
  }
238
246
  process.exitCode = 1;
239
247
  return;
@@ -247,12 +255,19 @@ async function handleDbAuditList(tables, opts) {
247
255
  const rows = visible.map((it) => {
248
256
  const eventTime = (0, render_1.formatTime)(it.eventTime, tty);
249
257
  // event_id 完整透传——PRD 截图里的 "..." 只是文档省略写法,不是 CLI 行为
250
- const cells = [eventTime, it.type, it.eventId, it.operator || '—', it.summary];
258
+ // operator pretty 只展示 name;--json 上面已经走 parseOperator 输出 {id, name}
259
+ const cells = [
260
+ eventTime,
261
+ it.type,
262
+ it.eventId,
263
+ (0, _operator_1.parseOperator)(it.operator).name || '—',
264
+ it.summary,
265
+ ];
251
266
  return isMultiTable ? [it.targetTable, ...cells] : cells;
252
267
  });
253
268
  (0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
254
269
  if (skipped.length > 0) {
255
- process.stderr.write(`(${String(skipped.length)} table(s) skipped: ${skipped.join(', ')})\n`);
270
+ process.stderr.write(formatSkippedHint(skipped, tables.length) + '\n');
256
271
  }
257
272
  if (result.hasMore && result.nextCursor) {
258
273
  process.stderr.write(`(more results; use --cursor '${result.nextCursor}')\n`);
@@ -278,6 +293,53 @@ function normalizeTime(input, flagName) {
278
293
  throw err;
279
294
  }
280
295
  }
296
+ // PRD 41-48 行格式:`— Skipped 1 of 3 tables: orders (audit not enabled)`
297
+ // 服务端约定 skipped 列表 = audit 未启用的表(表不存在直接 raise k_dl_000005),
298
+ // 所以原因固定为 "audit not enabled",无需带每项 reason。
299
+ function formatSkippedHint(skipped, totalRequested) {
300
+ const items = skipped.map((t) => `${t} (audit not enabled)`).join(', ');
301
+ return `— Skipped ${String(skipped.length)} of ${String(totalRequested)} tables: ${items}`;
302
+ }
303
+ // audit list 后端错误码加 hint:
304
+ // - k_dl_000005 表不存在 → 文案与 hint 对齐 `db schema get` 的统一格式
305
+ // - k_dl_1300040 单表 audit 未启用 → 指引 enable
306
+ // - k_dl_1300041 多表全部未启用 → 指引 status
307
+ function decorateAuditListError(err, tables) {
308
+ if (!(err instanceof error_1.AppError))
309
+ return err;
310
+ if (err.code === 'DB_API_k_dl_000005') {
311
+ // 不复用 dataloom 透传的 message(旧版/新版文案不一致:`table [x] not exist` /
312
+ // `table x not found`),统一改写成跟 schema get 一样的 `Table '<name>' does not exist`,
313
+ // 单表传 tables[0],多表场景兜底用解析到的 raw message 里第一个表名。
314
+ const t = tables.length > 0 ? tables[0] : (extractMissingTable(err.message) ?? '<table>');
315
+ return new error_1.AppError('TABLE_NOT_FOUND', `Table '${t}' does not exist`, {
316
+ next_actions: [`Did you mean another table? Run "miaoda db schema list" to see all tables.`],
317
+ });
318
+ }
319
+ if (err.code === 'DB_API_k_dl_1300040') {
320
+ const t = tables[0] ?? '<table>';
321
+ return new error_1.AppError(err.code, err.message, {
322
+ next_actions: [`Enable with: miaoda db audit enable ${t}`],
323
+ });
324
+ }
325
+ if (err.code === 'DB_API_k_dl_1300041') {
326
+ return new error_1.AppError(err.code, err.message, {
327
+ next_actions: ['Check audit status with: miaoda db audit status'],
328
+ });
329
+ }
330
+ return err;
331
+ }
332
+ // extractMissingTable 从 dataloom 原始文案里抠表名兜底。dataloom 端有两种格式:
333
+ // - 旧版:`Table [foo] doesn't exist` / `table [foo] not exist`
334
+ // - 新版(本次 commits):`table foo not found`
335
+ // 都失配时返 null,调用方走 `<table>` 占位。
336
+ function extractMissingTable(msg) {
337
+ const bracket = /\[([^\]]+)\]/.exec(msg);
338
+ if (bracket)
339
+ return bracket[1];
340
+ const m = /table\s+([\w.]+)\s+not (?:exist|found)/i.exec(msg);
341
+ return m ? m[1] : null;
342
+ }
281
343
  // 服务端 before/after 是 JSON 字符串透传,CLI JSON 输出再反序列化回对象,
282
344
  // 给下游 jq 等工具消费。失败时透传原始字符串避免数据丢失。
283
345
  function safeParseJson(s) {
@@ -40,11 +40,12 @@ const error_1 = require("../../../utils/error");
40
40
  const output_1 = require("../../../utils/output");
41
41
  const render_1 = require("../../../utils/render");
42
42
  const time_1 = require("../../../utils/time");
43
+ const _operator_1 = require("../../../cli/handlers/db/_operator");
43
44
  function toRow(it) {
44
45
  return {
45
46
  change_id: it.changeId,
46
47
  changed_at: it.changedAt,
47
- operator: it.operator,
48
+ operator: (0, _operator_1.parseOperator)(it.operator),
48
49
  target_table: it.targetTable,
49
50
  change_type: it.changeType,
50
51
  summary: it.summary,
@@ -115,10 +116,11 @@ async function handleDbChangelog(opts) {
115
116
  const tty = (0, render_1.isStdoutTty)();
116
117
  // PRD: change_id / changed_at / operator / target_table / change_type / summary
117
118
  const headers = ['change_id', 'changed_at', 'operator', 'target_table', 'change_type', 'summary'];
119
+ // pretty 渲染只展示 operator.name(兼容 PRD 原 string 形态);--json 走 toRow 输出完整对象
118
120
  const rows = allItems.map((it) => [
119
121
  it.changeId,
120
122
  (0, render_1.formatTime)(it.changedAt, tty),
121
- it.operator || '—',
123
+ (0, _operator_1.parseOperator)(it.operator).name || '—',
122
124
  it.targetTable || '—',
123
125
  it.changeType,
124
126
  it.summary || '—',
@@ -61,15 +61,11 @@ async function handleDbRecoveryDiff(target, opts) {
61
61
  async function handleDbRecoveryApply(target, opts) {
62
62
  const appId = (0, shared_1.resolveAppId)(opts);
63
63
  const ts = normalizeTimestamp(target);
64
- // PRD 要求 apply 输出包含 N tables affected / Ms elapsed,dataloom apply 路径不
65
- // 回带这俩信息(pgsvc.BranchRestore 是异步触发、不等终态)。所以 CLI 这里始终
66
- // 先跑一次 preview——它本来就是 PITR 推荐流程,--yes 时静默跑、TTY 时同时给用户审。
64
+ // PRD 要求 apply 输出包含 N tables affected / Ms elapsed
65
+ // tables_affected preview 拿;elapsed 用 CLI 端墙钟(dataloom apply 是异步
66
+ // 触发,server-side elapsed=0;下面 poll 真终态后用本地计时器算 elapsed)。
67
67
  const preview = await runRecoveryPreview(appId, ts);
68
68
  const tablesAffected = preview.tablesAffected ?? preview.changes?.length ?? 0;
69
- // dataloom 端 apply 是异步触发,elapsed_seconds 返 0。这里改用 preview 给出的
70
- // estimated_seconds 当 "预计耗时",更贴近 PRD 示例的 30s elapsed 含义;
71
- // 都没填时按 PRD 默认值 30 兜底。
72
- const elapsedFromPreview = preview.estimatedSeconds ?? 30;
73
69
  if (!opts.yes && !(0, output_1.isJsonMode)()) {
74
70
  renderDiff(ts, preview);
75
71
  const ok = await confirm(`? Restore database to ${ts}? This will overwrite current data. (y/N) `);
@@ -85,7 +81,12 @@ async function handleDbRecoveryApply(target, opts) {
85
81
  catch (err) {
86
82
  throw decorateRecoveryError(err);
87
83
  }
88
- const elapsed = result.elapsedSeconds && result.elapsedSeconds > 0 ? result.elapsedSeconds : elapsedFromPreview;
84
+ // 关键:墙钟从 apply 触发瞬间开始,poll 真完成时停。dataloom restore 是异步触发,
85
+ // 立即返 status="restore_triggered",CLI 必须 poll GET /db/recovery/status 拿
86
+ // Redis 缓存的 Resetting/Ready/Failed,否则用户会被误导以为已恢复完。
87
+ const startedAt = Date.now();
88
+ await waitRecoveryDone(appId, ts);
89
+ const elapsed = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
89
90
  if ((0, output_1.isJsonMode)()) {
90
91
  // PRD:{"status": "restored", "target": "...", "tables_affected": N, "elapsed_seconds": M}
91
92
  (0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
@@ -101,6 +102,37 @@ async function handleDbRecoveryApply(target, opts) {
101
102
  `(${String(tablesAffected)} tables affected, ${String(elapsed)}s elapsed)`;
102
103
  (0, output_1.emit)(tty ? colors_1.c.success(`✓ ${body}`) : `OK ${body}`);
103
104
  }
105
+ /**
106
+ * 触发 BranchRestore 后 poll 直到任务终态。dataloom 端 Redis 缓存 workspace 级
107
+ * status,归一成 running / success / failed:
108
+ * - running → 继续 poll
109
+ * - success → 退出,CLI 渲染 ✓
110
+ * - failed → 抛 DB_API_k_dl_1300036 + errorMessage
111
+ *
112
+ * 不设固定超时:BranchRestore 实际时长依库大小变化(前端文案示例 1 分钟,大库可能
113
+ * 几分钟),由 pollUntilDone 内置 60 分钟上限兜底,远超正常场景。
114
+ */
115
+ async function waitRecoveryDone(appId, target) {
116
+ try {
117
+ return await (0, poll_1.pollUntilDone)({
118
+ label: 'recovery apply',
119
+ intervalMs: 2000,
120
+ fetch: () => api.db.getRecoveryStatus({ appId }),
121
+ isDone: (cur) => {
122
+ const status = (cur.status || '').toLowerCase();
123
+ if (status === 'success')
124
+ return { done: true, value: cur };
125
+ if (status === 'failed') {
126
+ throw new error_1.AppError('DB_API_k_dl_1300036', cur.errorMessage ?? `recovery to ${target} failed`);
127
+ }
128
+ return { done: false };
129
+ },
130
+ });
131
+ }
132
+ catch (err) {
133
+ throw decorateRecoveryError(err);
134
+ }
135
+ }
104
136
  /**
105
137
  * 触发 PITR 预览任务并轮询到终态:
106
138
  * 1. POST /db/recovery dryRun=true → previewRequestId
@@ -171,20 +203,20 @@ function normalizeTimestamp(input) {
171
203
  }
172
204
  // PRD diff 输出,三套:
173
205
  //
174
- // TTY pretty(缩进 prose、带 Unicode 箭头 / 波浪号、表名按列对齐):
206
+ // TTY pretty(缩进 prose、带 Unicode 箭头、表名按列对齐):
175
207
  // Recovery preview (→ 2026-04-15T10:00:00Z):
176
208
  //
177
209
  // tables affected: 2
178
- // users: +3 rows, -1 row, ~5 rows modified
179
- // orders: table will be restored (was dropped at 10:25:00)
210
+ // users: +3 rows, -1 row
211
+ // orders: table will be restored
180
212
  //
181
213
  // estimated time: ~30s
182
214
  //
183
215
  // non-TTY pretty(管道友好,扁平 TSV、ASCII 箭头、snake_case key、estimated_time 不带 ~/s):
184
216
  // Recovery preview (-> 2026-04-15T10:00:00Z):
185
217
  // tables_affected\t2
186
- // users\t+3 rows, -1 row, ~5 rows modified
187
- // orders\ttable will be restored (was dropped at 10:25:00)
218
+ // users\t+3 rows, -1 row
219
+ // orders\ttable will be restored
188
220
  // estimated_time\t30
189
221
  //
190
222
  // --json:标准 envelope,字段名固定 snake_case。
@@ -200,7 +232,6 @@ function renderDiff(target, preview) {
200
232
  table: c.table,
201
233
  inserted: c.inserted,
202
234
  deleted: c.deleted,
203
- modified: c.modified,
204
235
  action: c.action,
205
236
  droppedAt: c.droppedAt,
206
237
  })),
@@ -250,51 +281,31 @@ function renderDiffPipe(target, changes, tablesAffected, estimated) {
250
281
  return lines.join('\n');
251
282
  }
252
283
  function describeChange(c) {
253
- // 表级操作:dataloom 现在把 schema diff 透出为 action="schema_<diffType>",
254
- // 老协议的 restore_table / drop / create 一并保留兼容。
284
+ // dataloom action PRD 三态 + unavailable 边界:
285
+ // restore_table schema diff 显示该表在目标时间点存在但当前没有
286
+ // drop_table — 该表当前有但目标时间点没有
287
+ // alter_table — 两侧都在但结构有差异(列 / 索引 / 关系等)
288
+ // unavailable — PITR diff 算不出来,droppedAt 字段复用透传 message
289
+ // 没 action 时是数据行数变化,走下面的 +N / -N 渲染。
255
290
  if (c.action === 'restore_table') {
256
- const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : '';
257
- return `table will be restored${ts}`;
291
+ return 'table will be restored';
258
292
  }
259
- if (c.action === 'drop') {
260
- const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : '';
261
- return `table will be dropped${ts}`;
293
+ if (c.action === 'drop_table') {
294
+ return 'table will be dropped';
262
295
  }
263
- if (c.action === 'create') {
264
- const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : '';
265
- return `table will be created${ts}`;
266
- }
267
- if (c.action?.startsWith('schema_') === true) {
268
- // dataloom 透出的 schema diffType 来自 schema_handler/common/constants:
269
- // - create: 当前没这表 / 目标时间点有 → 恢复后表会被建出来(PRD 用 restored 表达)
270
- // - drop: 当前有 / 目标时间点没 → 恢复后会被删掉
271
- // - alter: 两侧都在但结构有差异 → 列 / 索引 / 关系等会被改回
272
- const diffType = c.action.slice('schema_'.length);
273
- const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : '';
274
- switch (diffType) {
275
- case 'create':
276
- return `table will be restored${ts}`;
277
- case 'drop':
278
- return `table will be dropped${ts}`;
279
- case 'alter':
280
- return `schema will be altered${ts}`;
281
- default:
282
- return `schema changed${diffType !== '' ? ` (${diffType})` : ''}${ts}`;
283
- }
296
+ if (c.action === 'alter_table') {
297
+ return 'table will be altered';
284
298
  }
285
299
  if (c.action === 'unavailable') {
286
- // dataloom 端 count 失败的表,复用 droppedAt 透传 message
287
300
  return `diff unavailable${c.droppedAt !== undefined && c.droppedAt !== '' ? `: ${c.droppedAt}` : ''}`;
288
301
  }
289
- // 数据变更:+N rows / -N rows / ~N rows modified
302
+ // 数据变更:+N rows / -N rows
290
303
  const parts = [];
291
304
  if (c.inserted !== undefined && c.inserted !== 0)
292
305
  parts.push(`+${String(c.inserted)} rows`);
293
306
  if (c.deleted !== undefined && c.deleted !== 0) {
294
307
  parts.push(`-${String(c.deleted)} ${c.deleted === 1 ? 'row' : 'rows'}`);
295
308
  }
296
- if (c.modified !== undefined && c.modified !== 0)
297
- parts.push(`~${String(c.modified)} rows modified`);
298
309
  return parts.length === 0 ? 'no changes' : parts.join(', ');
299
310
  }
300
311
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.3-alpha.fbac0be",
3
+ "version": "0.1.3-alpha.fc1e1a9",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {