@lark-apaas/miaoda-cli 0.1.1-alpha.ddba836 → 0.1.1-alpha.eccb0b7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/db/api.js +83 -6
- package/dist/api/db/client.js +40 -29
- package/dist/api/db/parsers.js +33 -20
- package/dist/api/db/sql-keywords.js +123 -0
- package/dist/api/file/api.js +24 -12
- package/dist/api/file/client.js +1 -5
- package/dist/api/file/parsers.js +1 -5
- package/dist/api/plugin/api.js +8 -3
- package/dist/cli/commands/plugin/index.js +18 -6
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/handlers/db/data.js +11 -3
- package/dist/cli/handlers/db/schema.js +22 -8
- package/dist/cli/handlers/db/sql.js +304 -16
- package/dist/cli/handlers/file/cp.js +41 -19
- package/dist/cli/handlers/file/ls.js +1 -3
- package/dist/cli/handlers/file/rm.js +4 -3
- package/dist/cli/handlers/plugin/plugin-local.js +23 -9
- package/dist/cli/handlers/plugin/plugin.js +21 -7
- package/dist/cli/help.js +5 -2
- package/dist/utils/colors.js +98 -0
- package/dist/utils/error.js +11 -0
- package/dist/utils/fuzzy-match.js +91 -0
- package/dist/utils/output.js +59 -5
- package/dist/utils/render.js +61 -41
- package/package.json +10 -2
package/dist/api/db/api.js
CHANGED
|
@@ -16,7 +16,13 @@ const client_1 = require("./client");
|
|
|
16
16
|
* 配合调用点的 try/catch + traceHttp,让 --verbose 在错误路径上也能拿到
|
|
17
17
|
* x-tt-logid 与 status,方便定位线上问题。
|
|
18
18
|
*/
|
|
19
|
-
async function mapDbHttpError(err, url, ctx
|
|
19
|
+
async function mapDbHttpError(err, url, ctx,
|
|
20
|
+
/**
|
|
21
|
+
* 可选 hook:解析到响应 body 后,如果 envelope 命中业务错误并抛出 AppError,
|
|
22
|
+
* 调用方可以借此把 body 里的额外字段(如 multi-statement 的 partial results)
|
|
23
|
+
* 挂到 AppError 上。
|
|
24
|
+
*/
|
|
25
|
+
onErrorBody) {
|
|
20
26
|
if (err instanceof error_1.AppError)
|
|
21
27
|
throw err;
|
|
22
28
|
if (err instanceof http_client_1.HttpError) {
|
|
@@ -24,8 +30,17 @@ async function mapDbHttpError(err, url, ctx) {
|
|
|
24
30
|
const statusText = err.response?.statusText ?? "";
|
|
25
31
|
try {
|
|
26
32
|
const body = (await err.response?.json());
|
|
27
|
-
if (body)
|
|
28
|
-
|
|
33
|
+
if (body) {
|
|
34
|
+
try {
|
|
35
|
+
(0, client_1.extractData)(body); // 业务 code 命中 → 抛 AppError;不命中走兜底
|
|
36
|
+
}
|
|
37
|
+
catch (appErr) {
|
|
38
|
+
if (appErr instanceof error_1.AppError && onErrorBody) {
|
|
39
|
+
onErrorBody(body, appErr);
|
|
40
|
+
}
|
|
41
|
+
throw appErr;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
29
44
|
}
|
|
30
45
|
catch (innerErr) {
|
|
31
46
|
if (innerErr instanceof error_1.AppError)
|
|
@@ -36,6 +51,20 @@ async function mapDbHttpError(err, url, ctx) {
|
|
|
36
51
|
}
|
|
37
52
|
throw err;
|
|
38
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* 多语句 SQL 失败时把已成功 statement 的 results 挂到 AppError.partial_results 上,
|
|
56
|
+
* 供上层 handler 构造 PRD 要求的 `completed` 数组。
|
|
57
|
+
*
|
|
58
|
+
* 注意参数顺序:(body, appErr) — 与 mapDbHttpError 的 onErrorBody hook 签名对齐。
|
|
59
|
+
*/
|
|
60
|
+
function attachSqlPartialResults(body, appErr) {
|
|
61
|
+
const data = body.data;
|
|
62
|
+
if (!data)
|
|
63
|
+
return;
|
|
64
|
+
if (!Array.isArray(data.results) || data.results.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
appErr.partial_results = data.results;
|
|
67
|
+
}
|
|
39
68
|
// CLI 不再为 dbBranch 设默认值:
|
|
40
69
|
// 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
|
|
41
70
|
// 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
|
|
@@ -61,12 +90,60 @@ async function execSql(opts) {
|
|
|
61
90
|
}
|
|
62
91
|
catch (err) {
|
|
63
92
|
(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");
|
|
93
|
+
await mapDbHttpError(err, url, "Failed to execute SQL", attachSqlPartialResults);
|
|
65
94
|
throw err; // 不可达
|
|
66
95
|
}
|
|
67
96
|
const body = (await response.json());
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
try {
|
|
98
|
+
const data = (0, client_1.extractData)(body);
|
|
99
|
+
const results = data.results ?? [];
|
|
100
|
+
// 新协议(dataloom 端 InnerAdminExecuteSQL 错误统一走 success envelope):
|
|
101
|
+
// results 末尾追加一条 SqlType="ERROR" 的哨兵,data 字段是 {code,message} JSON。
|
|
102
|
+
// 这样网关在错误路径不会吞业务字段,CLI 拿到完整 partial_results + statement_index。
|
|
103
|
+
const last = results.at(-1);
|
|
104
|
+
if (last?.sqlType === "ERROR") {
|
|
105
|
+
const appErr = parseSqlErrorSentinel(last.data);
|
|
106
|
+
appErr.partial_results = results.slice(0, -1);
|
|
107
|
+
const stmtIdx = data.errorStatementIndex;
|
|
108
|
+
if (typeof stmtIdx === "number")
|
|
109
|
+
appErr.statement_index = stmtIdx;
|
|
110
|
+
throw appErr;
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
catch (appErr) {
|
|
115
|
+
// 旧协议兼容(dataloom 服务端未部署新协议时):HTTP 2xx 但 envelope status_code != 0
|
|
116
|
+
// 走 mapDbHttpError → AppError,partial_results 已被网关吞,仅保留 code+message。
|
|
117
|
+
//
|
|
118
|
+
// 新协议下上面的 try 块里已经手动 slice 设置了 partial_results(剥离了 ERROR 哨兵),
|
|
119
|
+
// 这里必须跳过 attach,否则会被 body.data.results 完整数组(含哨兵)覆盖回去。
|
|
120
|
+
if (appErr instanceof error_1.AppError && appErr.partial_results === undefined) {
|
|
121
|
+
attachSqlPartialResults(body, appErr);
|
|
122
|
+
}
|
|
123
|
+
throw appErr;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 解析 InnerAdminExecuteSQL 错误哨兵(results 末尾那条 SqlType="ERROR" 项)的 data 字段。
|
|
128
|
+
* 服务端约定:data 是 `{"code":"k_dl_xxx","message":"..."}` 形态的 JSON 字符串。
|
|
129
|
+
*
|
|
130
|
+
* 拿到 code+message 后复用 mapDataloomBizError 走与旧协议(ensureInnerSuccess)
|
|
131
|
+
* 完全一致的映射规则——k_dl_1300015 → RESULT_SET_TOO_LARGE、SQLSTATE 透传等
|
|
132
|
+
* hint / 语义 code 重写不能丢。否则新协议下 SERVER_ERROR_HINTS 会因为 code key
|
|
133
|
+
* 不匹配而 miss,CLI 用户看不到 LIMIT 引导这种关键 hint。
|
|
134
|
+
*
|
|
135
|
+
* 解析失败则降级为 INTERNAL_DB_ERROR + 原始字符串,避免哨兵格式异常时整条调用炸掉。
|
|
136
|
+
*/
|
|
137
|
+
function parseSqlErrorSentinel(payload) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(payload);
|
|
140
|
+
const code = typeof parsed.code === "string" && parsed.code !== "" ? parsed.code : "INTERNAL_DB_ERROR";
|
|
141
|
+
const message = typeof parsed.message === "string" ? parsed.message : payload;
|
|
142
|
+
return (0, client_1.mapDataloomBizError)(code, message);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return new error_1.AppError("INTERNAL_DB_ERROR", payload);
|
|
146
|
+
}
|
|
70
147
|
}
|
|
71
148
|
// ── db schema → InnerGetSchema ──
|
|
72
149
|
/**
|
package/dist/api/db/client.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SQLSTATE_MAP = void 0;
|
|
4
4
|
exports.traceHttp = traceHttp;
|
|
5
5
|
exports.ensureInnerSuccess = ensureInnerSuccess;
|
|
6
|
+
exports.mapDataloomBizError = mapDataloomBizError;
|
|
6
7
|
exports.extractData = extractData;
|
|
7
8
|
exports.buildInnerUrl = buildInnerUrl;
|
|
8
9
|
const error_1 = require("../../utils/error");
|
|
@@ -26,11 +27,7 @@ function traceHttp(method, url, start, response, err) {
|
|
|
26
27
|
const status = response?.status ?? 0;
|
|
27
28
|
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
28
29
|
if (err !== undefined) {
|
|
29
|
-
const errMsg = err instanceof Error
|
|
30
|
-
? err.message
|
|
31
|
-
: typeof err === "string"
|
|
32
|
-
? err
|
|
33
|
-
: JSON.stringify(err);
|
|
30
|
+
const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
34
31
|
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
@@ -54,40 +51,48 @@ function ensureInnerSuccess(body) {
|
|
|
54
51
|
const code = body.status_code ?? body.ErrorCode ?? "0";
|
|
55
52
|
if (code === "0" || code === "")
|
|
56
53
|
return;
|
|
57
|
-
const
|
|
54
|
+
const appErr = mapDataloomBizError(code, body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
|
|
58
55
|
// PRD 多语句失败:后端在 envelope 顶层透出 errorStatementIndex(从 0 起计),
|
|
59
|
-
// 单语句 /
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
// 单语句 / 单元执行不会带这个字段。挂到 AppError 上让 emitError 落到错误信封
|
|
57
|
+
// statement_index 字段。
|
|
58
|
+
if (typeof body.errorStatementIndex === "number") {
|
|
59
|
+
appErr.statement_index = body.errorStatementIndex;
|
|
60
|
+
}
|
|
61
|
+
throw appErr;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 把 dataloom 业务错误码 + 原始 message 映射为语义化 AppError。
|
|
65
|
+
*
|
|
66
|
+
* 同时被两条错误路径共用:
|
|
67
|
+
* - ensureInnerSuccess(旧协议:BaseResp.status_code != "0")
|
|
68
|
+
* - api.ts 的 parseSqlErrorSentinel(新协议:success envelope + ERROR 哨兵)
|
|
69
|
+
*
|
|
70
|
+
* 优先级:
|
|
71
|
+
* 1. k_dl_1300002 + SQLSTATE 透传 → SQLSTATE_MAP
|
|
72
|
+
* 2. k_dl_1600000 + "Invalid DB Branch" → MULTI_ENV_NOT_INITIALIZED
|
|
73
|
+
* 3. BIZ_ERR_MAP 命中 → 语义 code(可能带 hint)
|
|
74
|
+
* 4. 兜底 DB_API_<code>
|
|
75
|
+
*
|
|
76
|
+
* stripPgPrefix 在这里做一次,调用方传 raw message 即可。
|
|
77
|
+
*/
|
|
78
|
+
function mapDataloomBizError(code, rawMessage) {
|
|
79
|
+
const message = stripPgPrefix(rawMessage);
|
|
63
80
|
if (code === "k_dl_1300002") {
|
|
64
81
|
const sqlstate = extractSqlstate(message);
|
|
65
82
|
if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
|
|
66
|
-
|
|
83
|
+
return new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
|
|
67
84
|
}
|
|
68
85
|
}
|
|
69
|
-
// k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
|
|
70
|
-
// message 以 "Invalid DB Branch" 开头的两种触发场景:
|
|
71
|
-
// 1) 单环境应用使用 --env(多环境未初始化)
|
|
72
|
-
// 2) 多环境应用传入了不存在的 env 名(如 --env staging)
|
|
73
|
-
// 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
|
|
74
86
|
if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
|
|
75
|
-
|
|
76
|
-
next_actions: [
|
|
77
|
-
"Verify the --env value matches an existing dbBranch.",
|
|
78
|
-
],
|
|
79
|
-
});
|
|
87
|
+
return new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", { next_actions: ["Verify the --env value matches an existing dbBranch."] });
|
|
80
88
|
}
|
|
81
|
-
// 业务 code 优先映射到语义 CLI code;未知 code 透传为 DB_API_<code> 样式
|
|
82
89
|
const mapped = BIZ_ERR_MAP.get(code);
|
|
83
90
|
if (mapped) {
|
|
84
|
-
|
|
91
|
+
return new error_1.AppError(mapped.code, mapped.message ?? message, {
|
|
85
92
|
next_actions: mapped.hint ? [mapped.hint] : undefined,
|
|
86
|
-
statement_index: stmtIdx,
|
|
87
93
|
});
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
|
|
95
|
+
return new error_1.AppError(`DB_API_${code}`, message);
|
|
91
96
|
}
|
|
92
97
|
/**
|
|
93
98
|
* 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
|
|
@@ -111,13 +116,19 @@ function extractSqlstate(msg) {
|
|
|
111
116
|
const BIZ_ERR_MAP = new Map(Object.entries({
|
|
112
117
|
// 来源:真实环境探测(dataloom InnerExecuteSQL / InnerGetSchema)
|
|
113
118
|
// k_dl_000001:DB 连接 / QPS 限流 等基础设施层错误
|
|
114
|
-
|
|
119
|
+
k_dl_000001: {
|
|
120
|
+
code: "INTERNAL_DB_ERROR",
|
|
121
|
+
hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试",
|
|
122
|
+
},
|
|
115
123
|
// k_dl_000002:SQL 解析 / 不支持的 SQL 类型(VACUUM、COPY、SET、SHOW 等)
|
|
116
|
-
|
|
124
|
+
k_dl_000002: { code: "SQL_SYNTAX_ERROR" },
|
|
117
125
|
// k_dl_000003:查询命中系统表(pg_tables / pg_user 等)被拒
|
|
118
|
-
|
|
126
|
+
k_dl_000003: { code: "SQL_OPERATION_FORBIDDEN" },
|
|
119
127
|
// k_dl_1300002:PG 执行错误;实际 SQLSTATE 由 extractSqlstate 单独映射
|
|
120
128
|
// 未匹配到 SQLSTATE 时走下面兜底 DB_API_<code>
|
|
129
|
+
// k_dl_1300015:SELECT 结果超过 1000 行硬拦;多行 hint 由 output.ts 的
|
|
130
|
+
// SERVER_ERROR_HINTS 按语义 code 兜底,这里只做 code 改名
|
|
131
|
+
k_dl_1300015: { code: "RESULT_SET_TOO_LARGE" },
|
|
121
132
|
}));
|
|
122
133
|
/** PG SQLSTATE → CLI code(当前 dataloom 不一定透传,预留未来使用) */
|
|
123
134
|
exports.SQLSTATE_MAP = {
|
package/dist/api/db/parsers.js
CHANGED
|
@@ -117,11 +117,35 @@ function toDetail(t, stats) {
|
|
|
117
117
|
// 优先用 tableStats.indexes(PRIMARY + UNIQUE + INDEX 统一视图);
|
|
118
118
|
// 回退到 TableVO.indexes(仅二级索引,老后端没返回 tableStats 时使用)
|
|
119
119
|
const rawIndexes = stats?.indexes ?? t.indexes ?? [];
|
|
120
|
+
const constraints = [];
|
|
121
|
+
const indexes = [];
|
|
122
|
+
for (const i of rawIndexes) {
|
|
123
|
+
const cols = i.indexColumns ?? [];
|
|
124
|
+
// 过滤掉空列条目(后端偶发返 indexColumns=[] 的占位项),渲染成 "UNIQUE ()" 看起来像 bug
|
|
125
|
+
if (cols.length === 0)
|
|
126
|
+
continue;
|
|
127
|
+
const kind = i.indexType.toLowerCase();
|
|
128
|
+
if (kind === "primary") {
|
|
129
|
+
constraints.push({ type: "PRIMARY KEY", columns: cols });
|
|
130
|
+
}
|
|
131
|
+
else if (kind === "unique") {
|
|
132
|
+
constraints.push({ type: "UNIQUE", columns: cols });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// foreign / normal / 其它统一进 indexes 段;method 从 indexDef 的 USING 段解析
|
|
136
|
+
indexes.push({
|
|
137
|
+
name: i.indexName,
|
|
138
|
+
columns: cols,
|
|
139
|
+
method: parseIndexMethod(i.indexDef) ?? "btree",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
120
143
|
return {
|
|
121
144
|
name: t.tableName,
|
|
122
145
|
description: t.comment && t.comment.length > 0 ? t.comment : null,
|
|
123
146
|
columns: (t.fields ?? []).map(toColumn),
|
|
124
|
-
|
|
147
|
+
constraints,
|
|
148
|
+
indexes,
|
|
125
149
|
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
126
150
|
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
127
151
|
};
|
|
@@ -137,25 +161,14 @@ function toColumn(f) {
|
|
|
137
161
|
comment: f.comment && f.comment.length > 0 ? f.comment : null,
|
|
138
162
|
};
|
|
139
163
|
}
|
|
140
|
-
function toIndex(i) {
|
|
141
|
-
return {
|
|
142
|
-
name: i.indexName,
|
|
143
|
-
type: translateIndexType(i.indexType),
|
|
144
|
-
columns: i.indexColumns ?? [],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
164
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* unique → "UNIQUE"
|
|
152
|
-
* 其它 → "INDEX"(foreign 外键通过 relationships 表达,不进 indexes;真的遇到也归 INDEX)
|
|
165
|
+
* 从 PG 的 CREATE INDEX 语句里提取访问方法(btree / gin / gist / hash 等)。
|
|
166
|
+
* indexDef 示例: "CREATE INDEX idx_users_name ON public.users USING btree (name)"
|
|
167
|
+
* 解析失败返 null,由调用方兜底成 PG 默认的 "btree"。
|
|
153
168
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return "UNIQUE";
|
|
160
|
-
return "INDEX";
|
|
169
|
+
function parseIndexMethod(indexDef) {
|
|
170
|
+
if (!indexDef)
|
|
171
|
+
return null;
|
|
172
|
+
const m = /USING\s+(\w+)/i.exec(indexDef);
|
|
173
|
+
return m ? m[1].toLowerCase() : null;
|
|
161
174
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQL_KEYWORDS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 常见 PG SQL 关键字白名单,用于 db sql 拼写错误的 did-you-mean 提示。
|
|
6
|
+
*
|
|
7
|
+
* # 选词原则
|
|
8
|
+
*
|
|
9
|
+
* PG 的关键字总数 700+(见 [pg_keyword 系统视图]),但 CLI 用户实际打错的几乎
|
|
10
|
+
* 都集中在核心动词 / 子句 / 类型 / 事务关键字。这里**少而精**,约 70 个:
|
|
11
|
+
*
|
|
12
|
+
* - DML 动词:SELECT / INSERT / UPDATE / DELETE / MERGE
|
|
13
|
+
* - 子句:FROM / WHERE / JOIN / GROUP / ORDER / HAVING / LIMIT / ...
|
|
14
|
+
* - 操作符词:AND / OR / NOT / IN / IS / NULL / LIKE / ...
|
|
15
|
+
* - DDL:CREATE / ALTER / DROP / TABLE / INDEX / VIEW / ...
|
|
16
|
+
* - 约束:PRIMARY / UNIQUE / FOREIGN / REFERENCES / CHECK / DEFAULT / ...
|
|
17
|
+
* - 事务:BEGIN / COMMIT / ROLLBACK / SAVEPOINT / TRANSACTION
|
|
18
|
+
* - 控制:IF / EXISTS / CASE / WHEN / THEN / ELSE / END / WITH / ...
|
|
19
|
+
*
|
|
20
|
+
* **不要往里加冷僻关键字**——候选集越大,短词越容易 false-positive 错配。
|
|
21
|
+
*/
|
|
22
|
+
exports.SQL_KEYWORDS = [
|
|
23
|
+
// DML 动词
|
|
24
|
+
"SELECT",
|
|
25
|
+
"INSERT",
|
|
26
|
+
"UPDATE",
|
|
27
|
+
"DELETE",
|
|
28
|
+
"MERGE",
|
|
29
|
+
// FROM / JOIN 系列
|
|
30
|
+
"FROM",
|
|
31
|
+
"WHERE",
|
|
32
|
+
"JOIN",
|
|
33
|
+
"LEFT",
|
|
34
|
+
"RIGHT",
|
|
35
|
+
"INNER",
|
|
36
|
+
"OUTER",
|
|
37
|
+
"FULL",
|
|
38
|
+
"CROSS",
|
|
39
|
+
"USING",
|
|
40
|
+
// 聚合 / 排序 / 分页
|
|
41
|
+
"GROUP",
|
|
42
|
+
"ORDER",
|
|
43
|
+
"BY",
|
|
44
|
+
"HAVING",
|
|
45
|
+
"LIMIT",
|
|
46
|
+
"OFFSET",
|
|
47
|
+
"FETCH",
|
|
48
|
+
// 集合操作
|
|
49
|
+
"UNION",
|
|
50
|
+
"INTERSECT",
|
|
51
|
+
"EXCEPT",
|
|
52
|
+
"DISTINCT",
|
|
53
|
+
"ALL",
|
|
54
|
+
// 别名 / 关联
|
|
55
|
+
"AS",
|
|
56
|
+
"ON",
|
|
57
|
+
// 操作符词
|
|
58
|
+
"AND",
|
|
59
|
+
"OR",
|
|
60
|
+
"NOT",
|
|
61
|
+
"IN",
|
|
62
|
+
"IS",
|
|
63
|
+
"NULL",
|
|
64
|
+
"TRUE",
|
|
65
|
+
"FALSE",
|
|
66
|
+
"BETWEEN",
|
|
67
|
+
"LIKE",
|
|
68
|
+
"ILIKE",
|
|
69
|
+
"SIMILAR",
|
|
70
|
+
// DDL
|
|
71
|
+
"CREATE",
|
|
72
|
+
"ALTER",
|
|
73
|
+
"DROP",
|
|
74
|
+
"TRUNCATE",
|
|
75
|
+
"RENAME",
|
|
76
|
+
"TABLE",
|
|
77
|
+
"INDEX",
|
|
78
|
+
"VIEW",
|
|
79
|
+
"DATABASE",
|
|
80
|
+
"SCHEMA",
|
|
81
|
+
"COLUMN",
|
|
82
|
+
"CONSTRAINT",
|
|
83
|
+
"SEQUENCE",
|
|
84
|
+
// 约束
|
|
85
|
+
"PRIMARY",
|
|
86
|
+
"KEY",
|
|
87
|
+
"FOREIGN",
|
|
88
|
+
"REFERENCES",
|
|
89
|
+
"UNIQUE",
|
|
90
|
+
"CHECK",
|
|
91
|
+
"DEFAULT",
|
|
92
|
+
// 写入
|
|
93
|
+
"VALUES",
|
|
94
|
+
"SET",
|
|
95
|
+
"RETURNING",
|
|
96
|
+
"INTO",
|
|
97
|
+
// 事务
|
|
98
|
+
"BEGIN",
|
|
99
|
+
"COMMIT",
|
|
100
|
+
"ROLLBACK",
|
|
101
|
+
"SAVEPOINT",
|
|
102
|
+
"TRANSACTION",
|
|
103
|
+
// 控制流 / CTE
|
|
104
|
+
"IF",
|
|
105
|
+
"EXISTS",
|
|
106
|
+
"REPLACE",
|
|
107
|
+
"WITH",
|
|
108
|
+
"RECURSIVE",
|
|
109
|
+
"CASE",
|
|
110
|
+
"WHEN",
|
|
111
|
+
"THEN",
|
|
112
|
+
"ELSE",
|
|
113
|
+
"END",
|
|
114
|
+
// 类型转换 / 时间提取
|
|
115
|
+
"CAST",
|
|
116
|
+
"EXTRACT",
|
|
117
|
+
// 排序方向
|
|
118
|
+
"ASC",
|
|
119
|
+
"DESC",
|
|
120
|
+
"NULLS",
|
|
121
|
+
"FIRST",
|
|
122
|
+
"LAST",
|
|
123
|
+
];
|
package/dist/api/file/api.js
CHANGED
|
@@ -209,9 +209,7 @@ async function resolveInputs(opts) {
|
|
|
209
209
|
return [];
|
|
210
210
|
return Promise.all(opts.inputs.map((input) => {
|
|
211
211
|
const kind = opts.forceAs ?? ((0, detect_1.looksLikePath)(input) ? "path" : "name");
|
|
212
|
-
return kind === "path"
|
|
213
|
-
? resolveByPath(opts.appId, input)
|
|
214
|
-
: resolveByName(opts.appId, input);
|
|
212
|
+
return kind === "path" ? resolveByPath(opts.appId, input) : resolveByName(opts.appId, input);
|
|
215
213
|
}));
|
|
216
214
|
}
|
|
217
215
|
/** path 输入:HEAD 判存在性,path 在 bucket 内 unique 无需反查。 */
|
|
@@ -309,7 +307,9 @@ async function preUpload(appId, req) {
|
|
|
309
307
|
*/
|
|
310
308
|
async function uploadCallback(appId, req) {
|
|
311
309
|
const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
|
|
312
|
-
const body = await (0, client_1.doPost)(url, req, {
|
|
310
|
+
const body = await (0, client_1.doPost)(url, req, {
|
|
311
|
+
errorContext: "upload callback",
|
|
312
|
+
});
|
|
313
313
|
const data = extractEnvelope(body);
|
|
314
314
|
const metadata = data.metadata;
|
|
315
315
|
if (!metadata) {
|
|
@@ -378,19 +378,31 @@ async function uploadFile(opts) {
|
|
|
378
378
|
throw new error_1.AppError("FILE_UPLOAD_FAILED", "Upload PUT returned no ETag");
|
|
379
379
|
}
|
|
380
380
|
// callback 返回服务端实际生成的对象元数据(filePath / file_name / download_url)。
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
381
|
+
// path 末段是平台生成的 16 位 GID,CLI 无法靠本地信息推断;callback 失败 /
|
|
382
|
+
// metadata.filePath 缺失时直接抛 FILE_UPLOAD_CALLBACK_INCOMPLETE:对象已落
|
|
383
|
+
// TOS(PUT 已成功 + ETag 拿到),但 CLI 拿不到真实 path。把 fileName 写到
|
|
384
|
+
// hint 里引导用户走 `file ls --name` 查实际 path,避免返一个假 path 让后续
|
|
385
|
+
// stat/download/rm 全部 FILE_NOT_FOUND。
|
|
386
|
+
let metadata;
|
|
385
387
|
try {
|
|
386
388
|
metadata = await uploadCallback(opts.appId, { uploadID: pre.uploadID, eTag: etag });
|
|
387
389
|
}
|
|
388
390
|
catch (err) {
|
|
389
|
-
|
|
391
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
392
|
+
throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", `Upload callback failed: ${reason}; file may already exist in storage`, {
|
|
393
|
+
next_actions: [
|
|
394
|
+
`Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (!metadata.filePath) {
|
|
399
|
+
throw new error_1.AppError("FILE_UPLOAD_CALLBACK_INCOMPLETE", "Upload callback returned no filePath; file may already exist in storage", {
|
|
400
|
+
next_actions: [
|
|
401
|
+
`Run \`miaoda file ls --name ${opts.fileName}\` to locate the uploaded file.`,
|
|
402
|
+
],
|
|
403
|
+
});
|
|
390
404
|
}
|
|
391
|
-
const path = metadata.filePath
|
|
392
|
-
? (metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath)
|
|
393
|
-
: (opts.remotePath ?? "/" + opts.fileName);
|
|
405
|
+
const path = metadata.filePath.startsWith("/") ? metadata.filePath : "/" + metadata.filePath;
|
|
394
406
|
const result = {
|
|
395
407
|
// 优先取服务端 ObjectVO.name(来自 PUT 时带的 Content-Disposition),
|
|
396
408
|
// 与后续 file ls / file stat 返回的展示名保持一致;缺失时降级用本地 fileName。
|
package/dist/api/file/client.js
CHANGED
|
@@ -22,11 +22,7 @@ function traceHttp(method, url, start, response, err) {
|
|
|
22
22
|
const status = response?.status ?? 0;
|
|
23
23
|
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
24
24
|
if (err !== undefined) {
|
|
25
|
-
const errMsg = err instanceof Error
|
|
26
|
-
? err.message
|
|
27
|
-
: typeof err === "string"
|
|
28
|
-
? err
|
|
29
|
-
: JSON.stringify(err);
|
|
25
|
+
const errMsg = err instanceof Error ? err.message : typeof err === "string" ? err : JSON.stringify(err);
|
|
30
26
|
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
31
27
|
return;
|
|
32
28
|
}
|
package/dist/api/file/parsers.js
CHANGED
|
@@ -16,11 +16,7 @@ function int(value) {
|
|
|
16
16
|
function readSize(meta) {
|
|
17
17
|
if (!meta)
|
|
18
18
|
return 0;
|
|
19
|
-
const v = meta.size ??
|
|
20
|
-
meta.fileSize ??
|
|
21
|
-
meta.file_size ??
|
|
22
|
-
meta.contentLength ??
|
|
23
|
-
0;
|
|
19
|
+
const v = meta.size ?? meta.fileSize ?? meta.file_size ?? meta.contentLength ?? 0;
|
|
24
20
|
return int(v);
|
|
25
21
|
}
|
|
26
22
|
/**
|
package/dist/api/plugin/api.js
CHANGED
|
@@ -38,7 +38,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
38
38
|
const versions = await getPluginVersions([pluginKey], isLatest);
|
|
39
39
|
const pluginVersions = versions[pluginKey];
|
|
40
40
|
if (!pluginVersions || pluginVersions.length === 0) {
|
|
41
|
-
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
41
|
+
throw new error_1.AppError("PLUGIN_NOT_FOUND", `Plugin not found: ${pluginKey}`, {
|
|
42
|
+
next_actions: ["检查包名拼写,或确认该插件已在插件市场发布"],
|
|
43
|
+
});
|
|
42
44
|
}
|
|
43
45
|
if (isLatest) {
|
|
44
46
|
return pluginVersions[0];
|
|
@@ -53,7 +55,9 @@ async function getPluginVersion(pluginKey, requestedVersion) {
|
|
|
53
55
|
function parsePluginKey(key) {
|
|
54
56
|
const match = /^(@[^/]+)\/(.+)$/.exec(key);
|
|
55
57
|
if (!match) {
|
|
56
|
-
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
58
|
+
throw new error_1.AppError("INVALID_PLUGIN_KEY", `Invalid plugin key format: ${key}`, {
|
|
59
|
+
next_actions: ["插件 key 必须形如 @scope/name"],
|
|
60
|
+
});
|
|
57
61
|
}
|
|
58
62
|
return { scope: match[1], name: match[2] };
|
|
59
63
|
}
|
|
@@ -91,7 +95,8 @@ async function withRetry(operation, description, maxRetries = MAX_RETRIES) {
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
|
-
throw lastError ??
|
|
98
|
+
throw (lastError ??
|
|
99
|
+
new error_1.AppError("INTERNAL_RETRY_EXHAUSTED", `${description} failed after ${String(maxRetries)} retries`, { retryable: true, next_actions: ["检查网络后重试,--verbose 可查看重试日志"] }));
|
|
95
100
|
}
|
|
96
101
|
/** 插件缓存目录 */
|
|
97
102
|
const PLUGIN_CACHE_DIR = "node_modules/.cache/miaoda-cli/plugins";
|
|
@@ -58,7 +58,9 @@ JSON 输出
|
|
|
58
58
|
Error: Invalid plugin name format: bad-name. Expected: @scope/name or @scope/name@version
|
|
59
59
|
hint: 示例:@demo/example-plugin 或 @demo/example-plugin@1.2.3
|
|
60
60
|
`)
|
|
61
|
-
.action(async (names) => {
|
|
61
|
+
.action(async (names) => {
|
|
62
|
+
await (0, index_1.handlePluginInstall)({ names });
|
|
63
|
+
});
|
|
62
64
|
pluginCmd
|
|
63
65
|
.command("update")
|
|
64
66
|
.description("把已安装插件升级到 latest 版本")
|
|
@@ -91,7 +93,9 @@ JSON 输出
|
|
|
91
93
|
$ miaoda plugin update @demo/not-installed --json
|
|
92
94
|
{"updated":[],"skipped":[],"notInstalled":["@demo/not-installed"],"failed":[]}
|
|
93
95
|
`)
|
|
94
|
-
.action(async (names) => {
|
|
96
|
+
.action(async (names) => {
|
|
97
|
+
await (0, index_1.handlePluginUpdate)({ names });
|
|
98
|
+
});
|
|
95
99
|
pluginCmd
|
|
96
100
|
.command("remove")
|
|
97
101
|
.description("从当前项目移除一个已安装的插件")
|
|
@@ -115,7 +119,9 @@ JSON 输出
|
|
|
115
119
|
Error: Plugin @demo/not-installed is not installed
|
|
116
120
|
hint: 运行 miaoda plugin list-packages 查看已安装插件
|
|
117
121
|
`)
|
|
118
|
-
.action((name) => {
|
|
122
|
+
.action((name) => {
|
|
123
|
+
(0, index_1.handlePluginRemove)({ name });
|
|
124
|
+
});
|
|
119
125
|
pluginCmd
|
|
120
126
|
.command("init")
|
|
121
127
|
.description("按 package.json 的 actionPlugins 批量安装所有插件")
|
|
@@ -146,7 +152,9 @@ JSON 输出
|
|
|
146
152
|
Error: package.json not found in current directory
|
|
147
153
|
hint: 在应用项目根目录运行
|
|
148
154
|
`)
|
|
149
|
-
.action(async () => {
|
|
155
|
+
.action(async () => {
|
|
156
|
+
await (0, index_1.handlePluginInit)();
|
|
157
|
+
});
|
|
150
158
|
pluginCmd
|
|
151
159
|
.command("list")
|
|
152
160
|
.description("列出当前项目的 capability 实例(./server/capabilities/*.json)")
|
|
@@ -178,7 +186,9 @@ JSON 输出
|
|
|
178
186
|
Error: server/capabilities directory not found
|
|
179
187
|
hint: 当前目录必须是含 server/capabilities/ 的应用项目
|
|
180
188
|
`)
|
|
181
|
-
.action(async (opts) => {
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
await (0, index_1.handlePluginList)(opts);
|
|
191
|
+
});
|
|
182
192
|
pluginCmd
|
|
183
193
|
.command("list-packages")
|
|
184
194
|
.description("列出 package.json actionPlugins 里已声明的插件包")
|
|
@@ -201,5 +211,7 @@ JSON 输出
|
|
|
201
211
|
Error: package.json not found in current directory
|
|
202
212
|
hint: 在应用项目根目录运行
|
|
203
213
|
`)
|
|
204
|
-
.action(() => {
|
|
214
|
+
.action(() => {
|
|
215
|
+
(0, index_1.handlePluginListPlugins)();
|
|
216
|
+
});
|
|
205
217
|
}
|
|
@@ -22,9 +22,7 @@ function resolveAppId(opts) {
|
|
|
22
22
|
const id = opts.appId ?? process.env.MIAODA_APP_ID ?? process.env.app_id;
|
|
23
23
|
if (!id) {
|
|
24
24
|
throw new error_1.AppError("APP_ID_MISSING", "未指定应用", {
|
|
25
|
-
next_actions: [
|
|
26
|
-
"设置 export MIAODA_APP_ID=<id>",
|
|
27
|
-
],
|
|
25
|
+
next_actions: ["设置 export MIAODA_APP_ID=<id>"],
|
|
28
26
|
});
|
|
29
27
|
}
|
|
30
28
|
return id;
|
|
@@ -56,7 +56,9 @@ async function handleDbDataImport(file, opts) {
|
|
|
56
56
|
catch (err) {
|
|
57
57
|
const code = err.code;
|
|
58
58
|
if (code === "ENOENT") {
|
|
59
|
-
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
59
|
+
throw new error_1.AppError("IMPORT_FILE_NOT_FOUND", `Local file '${file}' does not exist`, {
|
|
60
|
+
next_actions: ["Check the file path."],
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
throw err;
|
|
62
64
|
}
|
|
@@ -103,7 +105,9 @@ async function handleDbDataExport(table, opts) {
|
|
|
103
105
|
if (!opts.force) {
|
|
104
106
|
try {
|
|
105
107
|
await fs.access(outputPath);
|
|
106
|
-
throw new error_1.AppError("FILE_ALREADY_EXISTS", `Output file '${outputPath}' already exists`, {
|
|
108
|
+
throw new error_1.AppError("FILE_ALREADY_EXISTS", `Output file '${outputPath}' already exists`, {
|
|
109
|
+
next_actions: ["Use -f to specify a different path, or --force to overwrite."],
|
|
110
|
+
});
|
|
107
111
|
}
|
|
108
112
|
catch (err) {
|
|
109
113
|
if (err instanceof error_1.AppError)
|
|
@@ -119,7 +123,11 @@ async function handleDbDataExport(table, opts) {
|
|
|
119
123
|
limit,
|
|
120
124
|
});
|
|
121
125
|
if (result.body.length > MAX_SIZE_BYTES) {
|
|
122
|
-
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
126
|
+
throw new error_1.AppError("EXPORT_SIZE_EXCEEDED", `Export exceeds 1 MB limit (body is ${String(result.body.length)} bytes)`, {
|
|
127
|
+
next_actions: [
|
|
128
|
+
`Filter the table with "miaoda db sql" (e.g. WHERE/LIMIT) and export smaller subsets.`,
|
|
129
|
+
],
|
|
130
|
+
});
|
|
123
131
|
}
|
|
124
132
|
await fs.writeFile(outputPath, result.body);
|
|
125
133
|
// 优先信任后端 X-Miaoda-Record-Count header;header 缺失时再用 body 行数兜底
|