@yejiming/dsh-data-agent 0.0.6 → 0.0.10
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/README.en.md +142 -119
- package/README.md +138 -118
- package/cordis.patch.yml +8 -9
- package/lib/client.js +42423 -524
- package/lib/client.js.map +1 -1
- package/lib/command-LFgLb6el.js +875 -0
- package/lib/command.js +2 -0
- package/lib/connections-WmjuUrDj.js +1608 -0
- package/lib/defaults-DP4RyRh1.js +21 -0
- package/lib/index.js +265 -68
- package/lib/routes.js +94 -170
- package/lib/tool-Dka6RyEp.js +1128 -0
- package/lib/tool.js +1 -426
- package/lib/types/analysis.d.ts +1071 -0
- package/lib/types/client/AnalysisChart.d.ts +26 -0
- package/lib/types/client/AnalysisDashboard.d.ts +30 -0
- package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
- package/lib/types/client/analysis-charts.d.ts +40 -0
- package/lib/types/client/analysis-view-model.d.ts +44 -0
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/locales.d.ts +66 -0
- package/lib/types/client/persistence.d.ts +6 -1
- package/lib/types/client-discovery.d.ts +45 -0
- package/lib/types/clients.d.ts +17 -10
- package/lib/types/command.d.ts +41 -0
- package/lib/types/connections.d.ts +115 -40
- package/lib/types/defaults.d.ts +2 -0
- package/lib/types/index.d.ts +109 -63
- package/lib/types/routes.d.ts +25 -91
- package/lib/types/sql.d.ts +1 -1
- package/lib/types/storage.d.ts +70 -0
- package/lib/types/structured-read.d.ts +50 -0
- package/lib/types/tool.d.ts +29 -20
- package/lib/types/tui-connection-form.d.ts +98 -0
- package/package.json +65 -4
- package/preset/data-agent/agent.cordis.yml +22 -25
- package/preset/data-agent/preset.yml +1 -1
- package/lib/defaults-Bac6QvNt.js +0 -911
- package/lib/query-CmhTFklw.js +0 -86
package/lib/routes.js
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as runClientQuery } from "./query-CmhTFklw.js";
|
|
1
|
+
import { i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-DP4RyRh1.js";
|
|
3
2
|
import { resolve } from "node:path";
|
|
4
3
|
import z from "schemastery";
|
|
5
4
|
//#region src/routes.ts
|
|
6
|
-
/** Cordis plugin name (diagnostics only). */
|
|
7
5
|
const name = "data-agent-routes";
|
|
8
|
-
/**
|
|
9
|
-
* No top-level `inject` export: the row must ACTIVATE even in headless
|
|
10
|
-
* profiles where `webServer` never exists (a permanently pending entry
|
|
11
|
-
* breaks one-shot runs). The routes register through a nested inject fiber
|
|
12
|
-
* the moment the webserver and the connection store are both available.
|
|
13
|
-
*/
|
|
6
|
+
/** Headless profiles activate this row without waiting forever for webServer. */
|
|
14
7
|
const inject = [];
|
|
15
|
-
/** Route prefix owned by this plugin (the browser half calls under it). */
|
|
16
8
|
const DATA_AGENT_PATH = "/plugins/data-agent";
|
|
17
|
-
/** Loader schema with deployment defaults (no library defaults). */
|
|
18
9
|
const Config = z.object({
|
|
19
10
|
connectTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_CONNECT_TIMEOUT_MS),
|
|
20
11
|
introspectMaxTables: z.number().step(1).min(1).default(500),
|
|
@@ -23,114 +14,43 @@ const Config = z.object({
|
|
|
23
14
|
maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
|
|
24
15
|
readonly: z.boolean().default(false)
|
|
25
16
|
});
|
|
26
|
-
/**
|
|
27
|
-
* Validate an untrusted /connect body; sqlite paths resolve to absolute
|
|
28
|
-
* (the client resolves the path relative to its own cwd, so the server pins
|
|
29
|
-
* it at connect time). Oracle/Hive/Impala follow the mysql/postgres shape:
|
|
30
|
-
* host/port/user/database (Oracle database = service name/SID, Hive/Impala
|
|
31
|
-
* database = default schema).
|
|
32
|
-
*/
|
|
17
|
+
/** Validate the Web wire shape while retaining temporary-password compatibility. */
|
|
33
18
|
function validateConnectBody(value, cwd = process.cwd()) {
|
|
34
19
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("请求体必须是 JSON 对象");
|
|
35
20
|
const candidate = value;
|
|
36
|
-
const sessionId = candidate.sessionId;
|
|
37
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
21
|
+
const sessionId = requireString(candidate.sessionId, "sessionId");
|
|
38
22
|
const type = candidate.type;
|
|
39
|
-
if (type
|
|
40
|
-
const database = candidate.database;
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
if (!isDatabaseType(type)) throw new Error("type 必须是 \"mysql\"、\"postgres\"、\"sqlite\"、\"oracle\"、\"hive\" 或 \"impala\"");
|
|
24
|
+
const database = requireString(candidate.database, "database");
|
|
25
|
+
const password = optionalString(candidate.password, "password");
|
|
26
|
+
const passwordRef = optionalString(candidate.passwordRef, "passwordRef");
|
|
27
|
+
if (password !== void 0 && passwordRef !== void 0) throw new Error("password 与 passwordRef 不能同时提供");
|
|
28
|
+
const readonly = optionalBoolean(candidate.readonly, "readonly");
|
|
29
|
+
const profileId = optionalString(candidate.profileId, "profileId");
|
|
30
|
+
const profileName = optionalString(candidate.name, "name");
|
|
31
|
+
const request = {
|
|
43
32
|
sessionId,
|
|
44
33
|
type,
|
|
45
|
-
database: resolve(cwd, database)
|
|
34
|
+
database: type === "sqlite" ? resolve(cwd, database) : database
|
|
46
35
|
};
|
|
47
|
-
|
|
48
|
-
if (
|
|
36
|
+
if (readonly !== void 0) request.readonly = readonly;
|
|
37
|
+
if (profileId !== void 0) request.profileId = profileId;
|
|
38
|
+
if (profileName !== void 0) request.name = profileName;
|
|
39
|
+
if (type === "sqlite") return request;
|
|
40
|
+
const host = optionalString(candidate.host, "host");
|
|
41
|
+
const user = optionalString(candidate.user, "user");
|
|
49
42
|
const port = candidate.port;
|
|
50
43
|
if (port !== void 0 && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("port 必须是 1-65535 的整数");
|
|
51
|
-
|
|
52
|
-
if (user !== void 0
|
|
53
|
-
|
|
54
|
-
if (password !== void 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const connection = {
|
|
58
|
-
type,
|
|
59
|
-
database
|
|
60
|
-
};
|
|
61
|
-
if (typeof host === "string" && host.length > 0) connection.host = host;
|
|
62
|
-
if (port !== void 0) connection.port = port;
|
|
63
|
-
if (typeof user === "string" && user.length > 0) connection.user = user;
|
|
64
|
-
if (typeof password === "string" && password.length > 0) connection.password = password;
|
|
65
|
-
if (readonly !== void 0) connection.readonly = readonly;
|
|
66
|
-
return {
|
|
67
|
-
sessionId,
|
|
68
|
-
type,
|
|
69
|
-
database,
|
|
70
|
-
...connection.host !== void 0 ? { host: connection.host } : {},
|
|
71
|
-
...connection.port !== void 0 ? { port: connection.port } : {},
|
|
72
|
-
...connection.user !== void 0 ? { user: connection.user } : {},
|
|
73
|
-
...connection.password !== void 0 ? { password: connection.password } : {},
|
|
74
|
-
...connection.readonly !== void 0 ? { readonly: connection.readonly } : {}
|
|
75
|
-
};
|
|
44
|
+
if (host !== void 0) request.host = host;
|
|
45
|
+
if (user !== void 0) request.user = user;
|
|
46
|
+
if (typeof port === "number") request.port = port;
|
|
47
|
+
if (password !== void 0) request.password = password;
|
|
48
|
+
if (passwordRef !== void 0) request.passwordRef = passwordRef;
|
|
49
|
+
return request;
|
|
76
50
|
}
|
|
77
|
-
/**
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
sanitizeIdentifier(type, value);
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Mount the data-agent routes against the host webserver, when one exists.
|
|
85
|
-
* The registration rides a nested inject fiber so this row activates in every
|
|
86
|
-
* profile; headless profiles simply never get routes.
|
|
87
|
-
* @param ctx - host cordis context.
|
|
88
|
-
* @param config - validated loader configuration.
|
|
89
|
-
*/
|
|
90
|
-
function apply(ctx, config) {
|
|
91
|
-
ctx.inject([
|
|
92
|
-
"webServer",
|
|
93
|
-
"subprocess",
|
|
94
|
-
"dataAgentConnections"
|
|
95
|
-
], (scope) => {
|
|
96
|
-
const store = scope.dataAgentConnections;
|
|
97
|
-
const connectOptions = {
|
|
98
|
-
clients: {},
|
|
99
|
-
timeoutMs: config.connectTimeoutMs,
|
|
100
|
-
maxResultChars: config.maxResultChars
|
|
101
|
-
};
|
|
102
|
-
const queryOptions = {
|
|
103
|
-
clients: {},
|
|
104
|
-
timeoutMs: config.queryTimeoutMs,
|
|
105
|
-
maxResultChars: config.maxResultChars
|
|
106
|
-
};
|
|
107
|
-
const introspectMaxTables = config.introspectMaxTables;
|
|
108
|
-
/** Collect the request body into a parsed JSON value. */
|
|
109
|
-
const readJson = async (req) => {
|
|
110
|
-
const chunks = [];
|
|
111
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
112
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
113
|
-
if (raw.length === 0) return {};
|
|
114
|
-
return JSON.parse(raw);
|
|
115
|
-
};
|
|
116
|
-
/** The stored connection for one session, failing loud when absent. */
|
|
117
|
-
const requireConnection = (sessionId) => {
|
|
118
|
-
const connection = store.getWithSecret(sessionId);
|
|
119
|
-
if (connection === void 0) throw new Error("请先连接数据库(未找到当前会话的连接),再执行该操作");
|
|
120
|
-
return connection;
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* Run one metadata query in machine-readable mode and return its stdout;
|
|
124
|
-
* a non-zero exit throws with the client's stderr as the message.
|
|
125
|
-
*/
|
|
126
|
-
const runMetadata = async (connection, kind, schema, table) => {
|
|
127
|
-
const result = await runClientQuery(scope, connection, metadataQuery(kind, connection.type, schema, table), queryOptions, new AbortController().signal, true);
|
|
128
|
-
if (result.exitCode !== 0) {
|
|
129
|
-
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
130
|
-
throw new Error(`元数据查询失败(exit ${result.exitCode}):${detail}`);
|
|
131
|
-
}
|
|
132
|
-
return result.stdout;
|
|
133
|
-
};
|
|
51
|
+
/** Register Web routes only when both the webserver and shared service exist. */
|
|
52
|
+
function apply(ctx, _config) {
|
|
53
|
+
ctx.inject(["webServer", "dataAgentConnections"], (scope) => {
|
|
134
54
|
scope.effect(() => {
|
|
135
55
|
const dispose = scope.webServer.register({
|
|
136
56
|
kind: "prefix",
|
|
@@ -143,100 +63,73 @@ function apply(ctx, config) {
|
|
|
143
63
|
try {
|
|
144
64
|
const url = new URL(req.url ?? "/", "http://dsh.internal");
|
|
145
65
|
const segments = url.pathname.slice(19).split("/").filter(Boolean);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
const listing = await runClientQuery(scope, connection, tableListingSql(connection.type, connection), connectOptions, new AbortController().signal, true);
|
|
158
|
-
if (listing.exitCode !== 0) {
|
|
159
|
-
const detail = listing.stderr.trim() !== "" ? listing.stderr.trim() : listing.stdout.trim();
|
|
66
|
+
const signal = requestSignal(req);
|
|
67
|
+
if (req.method === "POST" && routeIs(segments, "connect")) {
|
|
68
|
+
try {
|
|
69
|
+
const { sessionId, ...input } = validateConnectBody(await readJson(req));
|
|
70
|
+
const result = await scope.dataAgentConnections.connect(sessionId, input, signal);
|
|
71
|
+
writeJson(200, {
|
|
72
|
+
ok: true,
|
|
73
|
+
tables: result.tables,
|
|
74
|
+
summary: result.summary
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
160
77
|
writeJson(200, {
|
|
161
78
|
ok: false,
|
|
162
|
-
error:
|
|
79
|
+
error: error instanceof Error ? error.message : String(error)
|
|
163
80
|
});
|
|
164
|
-
return;
|
|
165
81
|
}
|
|
166
|
-
const tables = parseTableListing(connection.type, listing.stdout).slice(0, introspectMaxTables);
|
|
167
|
-
connection.tables = tables;
|
|
168
|
-
store.set(request.sessionId, connection);
|
|
169
|
-
writeJson(200, {
|
|
170
|
-
ok: true,
|
|
171
|
-
tables
|
|
172
|
-
});
|
|
173
82
|
return;
|
|
174
83
|
}
|
|
175
|
-
if (req.method === "POST" && segments
|
|
176
|
-
const sessionId = (await readJson(req)).sessionId;
|
|
177
|
-
|
|
178
|
-
store.clear(sessionId);
|
|
84
|
+
if (req.method === "POST" && routeIs(segments, "disconnect")) {
|
|
85
|
+
const sessionId = requireString((await readJson(req)).sessionId, "sessionId");
|
|
86
|
+
await scope.dataAgentConnections.disconnect(sessionId);
|
|
179
87
|
writeJson(200, { ok: true });
|
|
180
88
|
return;
|
|
181
89
|
}
|
|
182
|
-
if (req.method === "GET" && segments
|
|
183
|
-
const sessionId = url.searchParams.get("sessionId")
|
|
184
|
-
const summary =
|
|
90
|
+
if (req.method === "GET" && routeIs(segments, "status")) {
|
|
91
|
+
const sessionId = requireString(url.searchParams.get("sessionId"), "sessionId");
|
|
92
|
+
const summary = await scope.dataAgentConnections.status(sessionId);
|
|
185
93
|
writeJson(200, summary === void 0 ? { connected: false } : {
|
|
186
94
|
connected: true,
|
|
187
95
|
summary
|
|
188
96
|
});
|
|
189
97
|
return;
|
|
190
98
|
}
|
|
191
|
-
if (req.method === "GET" && segments
|
|
192
|
-
const sessionId = url.searchParams.get("sessionId")
|
|
193
|
-
if (sessionId.length === 0) throw new Error("sessionId 不能为空");
|
|
194
|
-
const connection = requireConnection(sessionId);
|
|
195
|
-
const stdout = await runMetadata(connection, "schemas");
|
|
99
|
+
if (req.method === "GET" && routeIs(segments, "schemas")) {
|
|
100
|
+
const sessionId = requireString(url.searchParams.get("sessionId"), "sessionId");
|
|
196
101
|
writeJson(200, {
|
|
197
102
|
ok: true,
|
|
198
|
-
schemas:
|
|
103
|
+
schemas: await scope.dataAgentConnections.listSchemas(sessionId, signal)
|
|
199
104
|
});
|
|
200
105
|
return;
|
|
201
106
|
}
|
|
202
|
-
if (req.method === "GET" && segments
|
|
203
|
-
const sessionId = url.searchParams.get("sessionId")
|
|
204
|
-
|
|
205
|
-
const connection = requireConnection(sessionId);
|
|
206
|
-
const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(connection.type, url.searchParams.get("schema"), "schema");
|
|
207
|
-
const stdout = await runMetadata(connection, "tables", schema);
|
|
107
|
+
if (req.method === "GET" && routeIs(segments, "tables")) {
|
|
108
|
+
const sessionId = requireString(url.searchParams.get("sessionId"), "sessionId");
|
|
109
|
+
const schema = url.searchParams.get("schema") ?? void 0;
|
|
208
110
|
writeJson(200, {
|
|
209
111
|
ok: true,
|
|
210
|
-
tables:
|
|
112
|
+
tables: await scope.dataAgentConnections.listTables(sessionId, schema, signal)
|
|
211
113
|
});
|
|
212
114
|
return;
|
|
213
115
|
}
|
|
214
|
-
if (req.method === "GET" && segments
|
|
215
|
-
const sessionId = url.searchParams.get("sessionId")
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const schema = connection.type === "sqlite" ? void 0 : requireIdentifier(connection.type, url.searchParams.get("schema"), "schema");
|
|
219
|
-
const table = requireIdentifier(connection.type, url.searchParams.get("table"), "table");
|
|
220
|
-
const stdout = await runMetadata(connection, "describe", schema, table);
|
|
116
|
+
if (req.method === "GET" && routeIs(segments, "describe")) {
|
|
117
|
+
const sessionId = requireString(url.searchParams.get("sessionId"), "sessionId");
|
|
118
|
+
const schema = url.searchParams.get("schema") ?? void 0;
|
|
119
|
+
const table = requireString(url.searchParams.get("table"), "table");
|
|
221
120
|
writeJson(200, {
|
|
222
121
|
ok: true,
|
|
223
|
-
columns:
|
|
122
|
+
columns: await scope.dataAgentConnections.describe(sessionId, schema, table, signal)
|
|
224
123
|
});
|
|
225
124
|
return;
|
|
226
125
|
}
|
|
227
|
-
if (req.method === "POST" && segments
|
|
126
|
+
if (req.method === "POST" && routeIs(segments, "query")) {
|
|
228
127
|
const body = await readJson(req);
|
|
229
|
-
const sessionId = body.sessionId;
|
|
230
|
-
|
|
231
|
-
const sql = body.sql;
|
|
232
|
-
if (typeof sql !== "string" || sql.trim().length === 0) throw new Error("sql 必须是非空字符串");
|
|
233
|
-
if (sql.length > config.maxQueryChars) throw new Error(`sql 超过长度上限(${config.maxQueryChars} 字符)`);
|
|
234
|
-
assertSingleStatement(sql, "/query");
|
|
235
|
-
const connection = requireConnection(sessionId);
|
|
236
|
-
if ((connection.readonly ?? config.readonly) && classifyStatement(sql, connection.type) === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
128
|
+
const sessionId = requireString(body.sessionId, "sessionId");
|
|
129
|
+
const sql = requireString(body.sql, "sql");
|
|
237
130
|
writeJson(200, {
|
|
238
131
|
ok: true,
|
|
239
|
-
result: await
|
|
132
|
+
result: await scope.dataAgentConnections.query(sessionId, sql, signal)
|
|
240
133
|
});
|
|
241
134
|
return;
|
|
242
135
|
}
|
|
@@ -252,5 +145,36 @@ function apply(ctx, config) {
|
|
|
252
145
|
}, "data-agent-routes: routes");
|
|
253
146
|
});
|
|
254
147
|
}
|
|
148
|
+
function routeIs(segments, expected) {
|
|
149
|
+
return segments.length === 1 && segments[0] === expected;
|
|
150
|
+
}
|
|
151
|
+
async function readJson(req) {
|
|
152
|
+
const chunks = [];
|
|
153
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
154
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
155
|
+
return raw.length === 0 ? {} : JSON.parse(raw);
|
|
156
|
+
}
|
|
157
|
+
function requestSignal(req) {
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
req.once("aborted", () => controller.abort(/* @__PURE__ */ new Error("HTTP request aborted")));
|
|
160
|
+
return controller.signal;
|
|
161
|
+
}
|
|
162
|
+
function requireString(value, label) {
|
|
163
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} 必须是非空字符串`);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function optionalString(value, label) {
|
|
167
|
+
if (value === void 0 || value === "") return void 0;
|
|
168
|
+
if (typeof value !== "string") throw new Error(`${label} 必须是字符串`);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function optionalBoolean(value, label) {
|
|
172
|
+
if (value === void 0) return void 0;
|
|
173
|
+
if (typeof value !== "boolean") throw new Error(`${label} 必须是布尔值`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function isDatabaseType(value) {
|
|
177
|
+
return value === "mysql" || value === "postgres" || value === "sqlite" || value === "oracle" || value === "hive" || value === "impala";
|
|
178
|
+
}
|
|
255
179
|
//#endregion
|
|
256
180
|
export { Config, DATA_AGENT_PATH, apply, inject, name, validateConnectBody };
|