@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.
Files changed (39) hide show
  1. package/README.en.md +142 -119
  2. package/README.md +138 -118
  3. package/cordis.patch.yml +8 -9
  4. package/lib/client.js +42423 -524
  5. package/lib/client.js.map +1 -1
  6. package/lib/command-LFgLb6el.js +875 -0
  7. package/lib/command.js +2 -0
  8. package/lib/connections-WmjuUrDj.js +1608 -0
  9. package/lib/defaults-DP4RyRh1.js +21 -0
  10. package/lib/index.js +265 -68
  11. package/lib/routes.js +94 -170
  12. package/lib/tool-Dka6RyEp.js +1128 -0
  13. package/lib/tool.js +1 -426
  14. package/lib/types/analysis.d.ts +1071 -0
  15. package/lib/types/client/AnalysisChart.d.ts +26 -0
  16. package/lib/types/client/AnalysisDashboard.d.ts +30 -0
  17. package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
  18. package/lib/types/client/analysis-charts.d.ts +40 -0
  19. package/lib/types/client/analysis-view-model.d.ts +44 -0
  20. package/lib/types/client/index.d.ts +3 -4
  21. package/lib/types/client/locales.d.ts +66 -0
  22. package/lib/types/client/persistence.d.ts +6 -1
  23. package/lib/types/client-discovery.d.ts +45 -0
  24. package/lib/types/clients.d.ts +17 -10
  25. package/lib/types/command.d.ts +41 -0
  26. package/lib/types/connections.d.ts +115 -40
  27. package/lib/types/defaults.d.ts +2 -0
  28. package/lib/types/index.d.ts +109 -63
  29. package/lib/types/routes.d.ts +25 -91
  30. package/lib/types/sql.d.ts +1 -1
  31. package/lib/types/storage.d.ts +70 -0
  32. package/lib/types/structured-read.d.ts +50 -0
  33. package/lib/types/tool.d.ts +29 -20
  34. package/lib/types/tui-connection-form.d.ts +98 -0
  35. package/package.json +65 -4
  36. package/preset/data-agent/agent.cordis.yml +22 -25
  37. package/preset/data-agent/preset.yml +1 -1
  38. package/lib/defaults-Bac6QvNt.js +0 -911
  39. package/lib/query-CmhTFklw.js +0 -86
package/lib/routes.js CHANGED
@@ -1,20 +1,11 @@
1
- import { _ as sanitizeIdentifier, g as parseTableListing, h as parseListing, i as DEFAULT_MAX_RESULT_CHARS, m as parseColumns, o as DEFAULT_QUERY_TIMEOUT_MS, p as metadataQuery, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS, u as classifyStatement, v as tableListingSql, y as assertSingleStatement } from "./defaults-Bac6QvNt.js";
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 !== "mysql" && type !== "postgres" && type !== "sqlite" && type !== "oracle" && type !== "hive" && type !== "impala") throw new Error("type 必须是 \"mysql\"、\"postgres\"、\"sqlite\"、\"oracle\"、\"hive\" 或 \"impala\"");
40
- const database = candidate.database;
41
- if (typeof database !== "string" || database.length === 0) throw new Error("database 必须是非空字符串" + (type === "sqlite" ? "(SQLite 为数据库文件路径)" : ""));
42
- if (type === "sqlite") return {
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
- const host = candidate.host;
48
- if (host !== void 0 && typeof host !== "string") throw new Error("host 必须是字符串");
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
- const user = candidate.user;
52
- if (user !== void 0 && typeof user !== "string") throw new Error("user 必须是字符串");
53
- const password = candidate.password;
54
- if (password !== void 0 && typeof password !== "string") throw new Error("password 必须是字符串");
55
- const readonly = candidate.readonly;
56
- if (readonly !== void 0 && typeof readonly !== "boolean") throw new Error("readonly 必须是布尔值");
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
- /** Validate one schema/table identifier: reuse the clients' sanitizer (validation + quoting). */
78
- function requireIdentifier(type, value, label) {
79
- if (value === null || value.length === 0) throw new Error(`${label} 不能为空`);
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
- if (req.method === "POST" && segments.length === 1 && segments[0] === "connect") {
147
- const request = validateConnectBody(await readJson(req));
148
- const connection = {
149
- type: request.type,
150
- database: request.database,
151
- ...request.host !== void 0 ? { host: request.host } : {},
152
- ...request.port !== void 0 ? { port: request.port } : {},
153
- ...request.user !== void 0 ? { user: request.user } : {},
154
- ...request.password !== void 0 ? { password: request.password } : {},
155
- ...request.readonly !== void 0 ? { readonly: request.readonly } : {}
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: `数据库连接验证失败(exit ${listing.exitCode}):${detail}`
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.length === 1 && segments[0] === "disconnect") {
176
- const sessionId = (await readJson(req)).sessionId;
177
- if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
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.length === 1 && segments[0] === "status") {
183
- const sessionId = url.searchParams.get("sessionId") ?? "";
184
- const summary = store.get(sessionId);
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.length === 1 && segments[0] === "schemas") {
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: parseListing(connection.type, stdout).slice(0, introspectMaxTables)
103
+ schemas: await scope.dataAgentConnections.listSchemas(sessionId, signal)
199
104
  });
200
105
  return;
201
106
  }
202
- if (req.method === "GET" && segments.length === 1 && segments[0] === "tables") {
203
- const sessionId = url.searchParams.get("sessionId") ?? "";
204
- if (sessionId.length === 0) throw new Error("sessionId 不能为空");
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: parseListing(connection.type, stdout).slice(0, introspectMaxTables)
112
+ tables: await scope.dataAgentConnections.listTables(sessionId, schema, signal)
211
113
  });
212
114
  return;
213
115
  }
214
- if (req.method === "GET" && segments.length === 1 && segments[0] === "describe") {
215
- const sessionId = url.searchParams.get("sessionId") ?? "";
216
- if (sessionId.length === 0) throw new Error("sessionId 不能为空");
217
- const connection = requireConnection(sessionId);
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: parseColumns(connection.type, stdout)
122
+ columns: await scope.dataAgentConnections.describe(sessionId, schema, table, signal)
224
123
  });
225
124
  return;
226
125
  }
227
- if (req.method === "POST" && segments.length === 1 && segments[0] === "query") {
126
+ if (req.method === "POST" && routeIs(segments, "query")) {
228
127
  const body = await readJson(req);
229
- const sessionId = body.sessionId;
230
- if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
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 runClientQuery(scope, connection, sql, queryOptions, new AbortController().signal)
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 };