@yejiming/dsh-data-agent 0.0.12 → 0.0.13
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 +56 -10
- package/README.md +56 -10
- package/conformance/dsh-ecosystem/baseline.json +59 -0
- package/conformance/dsh-ecosystem/dependencies.json +41 -0
- package/conformance/dsh-ecosystem/fixtures/host-degraded.fixture.json +12 -0
- package/conformance/dsh-ecosystem/fixtures/host-eligible.fixture.json +13 -0
- package/conformance/dsh-ecosystem/fixtures/host-rejected.fixture.json +12 -0
- package/conformance/dsh-ecosystem/fixtures/profiles/native-only/package.json +8 -0
- package/conformance/dsh-ecosystem/fixtures/profiles/native-plus-adapter/package.json +9 -0
- package/conformance/dsh-ecosystem/inventory.json +77 -0
- package/conformance/dsh-ecosystem/restrictions.json +20 -0
- package/dsh-plugin.json +67 -0
- package/lib/client.js +1348 -129
- package/lib/client.js.map +1 -1
- package/lib/{command-DuCpwVbl.js → command-utC5MHd9.js} +101 -60
- package/lib/command.js +1 -1
- package/lib/{connections-5sfdEDsG.js → connections-CHY4uB6z.js} +747 -65
- package/lib/defaults-Cngd8Tf8.js +131 -0
- package/lib/ecosystem.js +19 -0
- package/lib/index.js +30 -22
- package/lib/routes.js +5 -6
- package/lib/{tool-DgL0fBfj.js → tool-ZTOS4B33.js} +5 -167
- package/lib/tool.js +1 -1
- package/lib/types/client/DataAgentWorkbench.d.ts +1 -2
- package/lib/types/client/QueryResultTable.d.ts +13 -0
- package/lib/types/client/locales.d.ts +48 -0
- package/lib/types/client/persistence.d.ts +3 -1
- package/lib/types/client/query-export.d.ts +11 -0
- package/lib/types/client-discovery.d.ts +3 -4
- package/lib/types/clients.d.ts +14 -8
- package/lib/types/command.d.ts +2 -2
- package/lib/types/connections.d.ts +33 -4
- package/lib/types/database-types.d.ts +23 -0
- package/lib/types/defaults.d.ts +4 -0
- package/lib/types/ecosystem.d.ts +13 -0
- package/lib/types/index.d.ts +16 -10
- package/lib/types/query.d.ts +13 -11
- package/lib/types/sql.d.ts +9 -1
- package/lib/types/storage.d.ts +11 -1
- package/lib/types/structured-read.d.ts +2 -2
- package/lib/types/structured.d.ts +1 -1
- package/lib/types/tool.d.ts +5 -5
- package/lib/types/tui-connection-form.d.ts +10 -7
- package/package.json +27 -3
- package/preset/data-agent/agent.cordis.yml +4 -1
- package/lib/defaults-DP4RyRh1.js +0 -21
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//#region src/database-types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Browser-safe database type descriptors shared by every DSH surface.
|
|
4
|
+
* Keep this module dependency-free: server-only client/process details belong
|
|
5
|
+
* in the database adapters, not in Web or persistence bundles.
|
|
6
|
+
*/
|
|
7
|
+
const DATABASE_TYPES = [
|
|
8
|
+
"mysql",
|
|
9
|
+
"postgres",
|
|
10
|
+
"sqlite",
|
|
11
|
+
"oracle",
|
|
12
|
+
"hive",
|
|
13
|
+
"impala",
|
|
14
|
+
"clickhouse",
|
|
15
|
+
"doris",
|
|
16
|
+
"sqlserver"
|
|
17
|
+
];
|
|
18
|
+
const DATABASE_TYPE_DESCRIPTORS = {
|
|
19
|
+
mysql: {
|
|
20
|
+
type: "mysql",
|
|
21
|
+
label: "MySQL",
|
|
22
|
+
localeKey: "type.mysql",
|
|
23
|
+
defaultPort: 3306,
|
|
24
|
+
defaultUser: "root",
|
|
25
|
+
fileBased: false
|
|
26
|
+
},
|
|
27
|
+
postgres: {
|
|
28
|
+
type: "postgres",
|
|
29
|
+
label: "PostgreSQL",
|
|
30
|
+
localeKey: "type.postgres",
|
|
31
|
+
defaultPort: 5432,
|
|
32
|
+
defaultUser: "postgres",
|
|
33
|
+
fileBased: false
|
|
34
|
+
},
|
|
35
|
+
sqlite: {
|
|
36
|
+
type: "sqlite",
|
|
37
|
+
label: "SQLite",
|
|
38
|
+
localeKey: "type.sqlite",
|
|
39
|
+
defaultPort: 0,
|
|
40
|
+
defaultUser: "",
|
|
41
|
+
fileBased: true
|
|
42
|
+
},
|
|
43
|
+
oracle: {
|
|
44
|
+
type: "oracle",
|
|
45
|
+
label: "Oracle",
|
|
46
|
+
localeKey: "type.oracle",
|
|
47
|
+
defaultPort: 1521,
|
|
48
|
+
defaultUser: "",
|
|
49
|
+
fileBased: false
|
|
50
|
+
},
|
|
51
|
+
hive: {
|
|
52
|
+
type: "hive",
|
|
53
|
+
label: "Hive",
|
|
54
|
+
localeKey: "type.hive",
|
|
55
|
+
defaultPort: 1e4,
|
|
56
|
+
defaultUser: "",
|
|
57
|
+
fileBased: false
|
|
58
|
+
},
|
|
59
|
+
impala: {
|
|
60
|
+
type: "impala",
|
|
61
|
+
label: "Impala",
|
|
62
|
+
localeKey: "type.impala",
|
|
63
|
+
defaultPort: 21050,
|
|
64
|
+
defaultUser: "",
|
|
65
|
+
fileBased: false
|
|
66
|
+
},
|
|
67
|
+
clickhouse: {
|
|
68
|
+
type: "clickhouse",
|
|
69
|
+
label: "ClickHouse",
|
|
70
|
+
localeKey: "type.clickhouse",
|
|
71
|
+
defaultPort: 8123,
|
|
72
|
+
securePort: 8443,
|
|
73
|
+
defaultUser: "default",
|
|
74
|
+
fileBased: false
|
|
75
|
+
},
|
|
76
|
+
doris: {
|
|
77
|
+
type: "doris",
|
|
78
|
+
label: "Apache Doris",
|
|
79
|
+
localeKey: "type.doris",
|
|
80
|
+
defaultPort: 9030,
|
|
81
|
+
defaultUser: "root",
|
|
82
|
+
fileBased: false
|
|
83
|
+
},
|
|
84
|
+
sqlserver: {
|
|
85
|
+
type: "sqlserver",
|
|
86
|
+
label: "SQL Server",
|
|
87
|
+
localeKey: "type.sqlserver",
|
|
88
|
+
defaultPort: 1433,
|
|
89
|
+
defaultUser: "sa",
|
|
90
|
+
fileBased: false
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function isDatabaseType(value) {
|
|
94
|
+
return typeof value === "string" && DATABASE_TYPES.includes(value);
|
|
95
|
+
}
|
|
96
|
+
function defaultDatabasePort(type, secure = false) {
|
|
97
|
+
const descriptor = DATABASE_TYPE_DESCRIPTORS[type];
|
|
98
|
+
return secure && descriptor.securePort !== void 0 ? descriptor.securePort : descriptor.defaultPort;
|
|
99
|
+
}
|
|
100
|
+
function defaultDatabaseUser(type) {
|
|
101
|
+
return DATABASE_TYPE_DESCRIPTORS[type].defaultUser;
|
|
102
|
+
}
|
|
103
|
+
function databaseTypeLabel(type) {
|
|
104
|
+
return DATABASE_TYPE_DESCRIPTORS[type].label;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/defaults.ts
|
|
108
|
+
/**
|
|
109
|
+
* Package-wide defaults shared by the server half (`src/index.ts`) and the
|
|
110
|
+
* database tool half (`src/tool.ts`). Loader schemas carry these as their
|
|
111
|
+
* defaults so a deployment may override every one of them in cordis.yml.
|
|
112
|
+
* @module @yejiming/dsh-data-agent/defaults
|
|
113
|
+
*/
|
|
114
|
+
/** Preset directory name installed into `$DSH_HOME/.agent-presets/`. */
|
|
115
|
+
const DEFAULT_PRESET_ID = "data-agent";
|
|
116
|
+
/** End-to-end deadline for one `/connect` connectivity check, milliseconds. */
|
|
117
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
118
|
+
/** End-to-end deadline for one database-tool query, milliseconds. */
|
|
119
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
120
|
+
/** In-memory cap on database-tool captured output (stdout and stderr each). */
|
|
121
|
+
const DEFAULT_MAX_RESULT_CHARS = 2e4;
|
|
122
|
+
/** Hard row cap for one structured Web workbench result/export. */
|
|
123
|
+
const WORKBENCH_MAX_EXPORT_ROWS = 5e4;
|
|
124
|
+
/** Bounded capture size for the larger structured Web workbench result. */
|
|
125
|
+
const WORKBENCH_MAX_RESULT_CHARS = 33554432;
|
|
126
|
+
/** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
|
|
127
|
+
const DEFAULT_MAX_QUERY_CHARS = 65536;
|
|
128
|
+
/** Grace period for the subprocess terminate escalation. */
|
|
129
|
+
const DEFAULT_GRACE_MS = 5e3;
|
|
130
|
+
//#endregion
|
|
131
|
+
export { DEFAULT_PRESET_ID as a, WORKBENCH_MAX_RESULT_CHARS as c, defaultDatabasePort as d, defaultDatabaseUser as f, DEFAULT_MAX_RESULT_CHARS as i, DATABASE_TYPES as l, DEFAULT_GRACE_MS as n, DEFAULT_QUERY_TIMEOUT_MS as o, isDatabaseType as p, DEFAULT_MAX_QUERY_CHARS as r, WORKBENCH_MAX_EXPORT_ROWS as s, DEFAULT_CONNECT_TIMEOUT_MS as t, databaseTypeLabel as u };
|
package/lib/ecosystem.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineFacet } from "@dsh-std/sdk";
|
|
2
|
+
//#region src/ecosystem.ts
|
|
3
|
+
/**
|
|
4
|
+
* Community v0.15 host facet.
|
|
5
|
+
*
|
|
6
|
+
* This facet intentionally publishes no commands, tools, UI, routes, storage,
|
|
7
|
+
* credentials, or database effects. The existing Cordis bundle remains the
|
|
8
|
+
* sole functional runtime so hosts may discover the ecosystem declaration
|
|
9
|
+
* without double-registering data-agent behavior.
|
|
10
|
+
*/
|
|
11
|
+
/** Stable degraded snapshot for declaration-only ecosystem discovery. */
|
|
12
|
+
const ECOSYSTEM_SNAPSHOT = Object.freeze({
|
|
13
|
+
state: "degraded",
|
|
14
|
+
message: "Native Cordis runtime owns all data-agent effects; this facet publishes declarations only.",
|
|
15
|
+
extensions: Object.freeze([])
|
|
16
|
+
});
|
|
17
|
+
const ecosystemFacet = defineFacet(() => void 0, () => void 0, () => ECOSYSTEM_SNAPSHOT);
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ECOSYSTEM_SNAPSHOT, ecosystemFacet as default };
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { o as
|
|
2
|
-
import {
|
|
3
|
-
import { r as apply$1 } from "./command-
|
|
4
|
-
import { n as apply$2 } from "./tool-
|
|
1
|
+
import { a as DEFAULT_PRESET_ID, i as DEFAULT_MAX_RESULT_CHARS, l as DATABASE_TYPES, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-Cngd8Tf8.js";
|
|
2
|
+
import { c as clientsSchema, t as createConnectionService } from "./connections-CHY4uB6z.js";
|
|
3
|
+
import { r as apply$1 } from "./command-utC5MHd9.js";
|
|
4
|
+
import { n as apply$2 } from "./tool-ZTOS4B33.js";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -29,19 +29,13 @@ const CONNECTION_STORAGE_DOMAIN = "data_agent_connections";
|
|
|
29
29
|
/** Durable profile schema. There is deliberately no `password` field. */
|
|
30
30
|
const persistedConnectionProfileSchema = z$1.object({
|
|
31
31
|
name: z$1.string().min(1).optional(),
|
|
32
|
-
type: z$1.enum(
|
|
33
|
-
"mysql",
|
|
34
|
-
"postgres",
|
|
35
|
-
"sqlite",
|
|
36
|
-
"oracle",
|
|
37
|
-
"hive",
|
|
38
|
-
"impala"
|
|
39
|
-
]),
|
|
32
|
+
type: z$1.enum(DATABASE_TYPES),
|
|
40
33
|
host: z$1.string().optional(),
|
|
41
34
|
port: z$1.number().int().min(1).max(65535).optional(),
|
|
42
35
|
user: z$1.string().optional(),
|
|
43
36
|
database: z$1.string().min(1),
|
|
44
37
|
readonly: z$1.boolean().optional(),
|
|
38
|
+
secure: z$1.boolean().optional(),
|
|
45
39
|
passwordRef: z$1.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
|
|
46
40
|
credentialMode: z$1.enum([
|
|
47
41
|
"none",
|
|
@@ -57,19 +51,13 @@ const sessionConnectionBindingSchema = z$1.object({
|
|
|
57
51
|
}).strict();
|
|
58
52
|
/** Session form draft schema. Secret-shaped fields are rejected by strict mode. */
|
|
59
53
|
const persistedConnectionFormDraftSchema = z$1.object({
|
|
60
|
-
type: z$1.enum(
|
|
61
|
-
"mysql",
|
|
62
|
-
"postgres",
|
|
63
|
-
"sqlite",
|
|
64
|
-
"oracle",
|
|
65
|
-
"hive",
|
|
66
|
-
"impala"
|
|
67
|
-
]),
|
|
54
|
+
type: z$1.enum(DATABASE_TYPES),
|
|
68
55
|
host: z$1.string(),
|
|
69
56
|
port: z$1.string(),
|
|
70
57
|
user: z$1.string(),
|
|
71
58
|
database: z$1.string(),
|
|
72
59
|
readonly: z$1.boolean(),
|
|
60
|
+
secure: z$1.boolean().optional(),
|
|
73
61
|
updatedAt: z$1.string().min(1)
|
|
74
62
|
}).strict();
|
|
75
63
|
/** Single source of truth for the storage layout and durable validation. */
|
|
@@ -82,6 +70,15 @@ const connectionStorageSpec = defineDomain({
|
|
|
82
70
|
drafts: domainTable(persistedConnectionFormDraftSchema)
|
|
83
71
|
}
|
|
84
72
|
});
|
|
73
|
+
/** Select the newest successful profile with a deterministic id tie-break. */
|
|
74
|
+
function latestConnectionProfile(entries) {
|
|
75
|
+
let latest;
|
|
76
|
+
for (const [profileId, profile] of entries) if (latest === void 0 || profile.updatedAt > latest.profile.updatedAt || profile.updatedAt === latest.profile.updatedAt && profileId > latest.profileId) latest = {
|
|
77
|
+
profileId,
|
|
78
|
+
profile
|
|
79
|
+
};
|
|
80
|
+
return latest;
|
|
81
|
+
}
|
|
85
82
|
/** Project a typed DSH domain handle onto the service's persistence seam. */
|
|
86
83
|
function createDomainConnectionPersistence(domain) {
|
|
87
84
|
const profiles = domain.table("profiles");
|
|
@@ -91,6 +88,9 @@ function createDomainConnectionPersistence(domain) {
|
|
|
91
88
|
getProfile(profileId) {
|
|
92
89
|
return profiles.get(profileId);
|
|
93
90
|
},
|
|
91
|
+
getLatestProfile() {
|
|
92
|
+
return latestConnectionProfile(profiles.entries());
|
|
93
|
+
},
|
|
94
94
|
putProfile(profileId, profile) {
|
|
95
95
|
return profiles.put(profileId, profile);
|
|
96
96
|
},
|
|
@@ -111,6 +111,9 @@ function createDomainConnectionPersistence(domain) {
|
|
|
111
111
|
},
|
|
112
112
|
putDraft(sessionId, draft) {
|
|
113
113
|
return drafts.put(sessionId, draft);
|
|
114
|
+
},
|
|
115
|
+
deleteDraft(sessionId) {
|
|
116
|
+
return drafts.delete(sessionId);
|
|
114
117
|
}
|
|
115
118
|
};
|
|
116
119
|
}
|
|
@@ -163,13 +166,17 @@ const Config = z.object({
|
|
|
163
166
|
z.const("sqlite"),
|
|
164
167
|
z.const("oracle"),
|
|
165
168
|
z.const("hive"),
|
|
166
|
-
z.const("impala")
|
|
169
|
+
z.const("impala"),
|
|
170
|
+
z.const("clickhouse"),
|
|
171
|
+
z.const("doris"),
|
|
172
|
+
z.const("sqlserver")
|
|
167
173
|
]),
|
|
168
174
|
host: z.string(),
|
|
169
175
|
port: z.natural(),
|
|
170
176
|
user: z.string(),
|
|
171
177
|
database: z.string(),
|
|
172
178
|
readonly: z.boolean(),
|
|
179
|
+
secure: z.boolean(),
|
|
173
180
|
passwordRef: z.string().pattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
|
|
174
181
|
password: z.never().hidden()
|
|
175
182
|
})).default({})
|
|
@@ -306,7 +313,8 @@ async function apply(ctx, config) {
|
|
|
306
313
|
...spec.port !== void 0 ? { port: spec.port } : {},
|
|
307
314
|
...spec.user !== void 0 ? { user: spec.user } : {},
|
|
308
315
|
...spec.passwordRef !== void 0 ? { passwordRef: spec.passwordRef } : {},
|
|
309
|
-
...spec.readonly !== void 0 ? { readonly: spec.readonly } : {}
|
|
316
|
+
...spec.readonly !== void 0 ? { readonly: spec.readonly } : {},
|
|
317
|
+
...spec.secure !== void 0 ? { secure: spec.secure } : {}
|
|
310
318
|
};
|
|
311
319
|
store.set(sessionId, connection);
|
|
312
320
|
}
|
package/lib/routes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
1
|
+
import { i as DEFAULT_MAX_RESULT_CHARS, l as DATABASE_TYPES, o as DEFAULT_QUERY_TIMEOUT_MS, p as isDatabaseType, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-Cngd8Tf8.js";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import z from "schemastery";
|
|
4
4
|
//#region src/routes.ts
|
|
@@ -20,12 +20,13 @@ function validateConnectBody(value, cwd = process.cwd()) {
|
|
|
20
20
|
const candidate = value;
|
|
21
21
|
const sessionId = requireString(candidate.sessionId, "sessionId");
|
|
22
22
|
const type = candidate.type;
|
|
23
|
-
if (!isDatabaseType(type)) throw new Error(
|
|
23
|
+
if (!isDatabaseType(type)) throw new Error(`type 必须是受支持的数据库类型:${DATABASE_TYPES.join("、")}`);
|
|
24
24
|
const database = requireString(candidate.database, "database");
|
|
25
25
|
const password = optionalString(candidate.password, "password");
|
|
26
26
|
const passwordRef = optionalString(candidate.passwordRef, "passwordRef");
|
|
27
27
|
if (password !== void 0 && passwordRef !== void 0) throw new Error("password 与 passwordRef 不能同时提供");
|
|
28
28
|
const readonly = optionalBoolean(candidate.readonly, "readonly");
|
|
29
|
+
const secure = optionalBoolean(candidate.secure, "secure");
|
|
29
30
|
const profileId = optionalString(candidate.profileId, "profileId");
|
|
30
31
|
const profileName = optionalString(candidate.name, "name");
|
|
31
32
|
const request = {
|
|
@@ -34,6 +35,7 @@ function validateConnectBody(value, cwd = process.cwd()) {
|
|
|
34
35
|
database: type === "sqlite" ? resolve(cwd, database) : database
|
|
35
36
|
};
|
|
36
37
|
if (readonly !== void 0) request.readonly = readonly;
|
|
38
|
+
if (type === "clickhouse" && secure !== void 0) request.secure = secure;
|
|
37
39
|
if (profileId !== void 0) request.profileId = profileId;
|
|
38
40
|
if (profileName !== void 0) request.name = profileName;
|
|
39
41
|
if (type === "sqlite") return request;
|
|
@@ -133,7 +135,7 @@ function apply(ctx, _config) {
|
|
|
133
135
|
const sql = requireString(body.sql, "sql");
|
|
134
136
|
writeJson(200, {
|
|
135
137
|
ok: true,
|
|
136
|
-
result: await scope.dataAgentConnections.
|
|
138
|
+
result: await scope.dataAgentConnections.executeInteractive(sessionId, sql, signal)
|
|
137
139
|
});
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
@@ -177,8 +179,5 @@ function optionalBoolean(value, label) {
|
|
|
177
179
|
if (typeof value !== "boolean") throw new Error(`${label} 必须是布尔值`);
|
|
178
180
|
return value;
|
|
179
181
|
}
|
|
180
|
-
function isDatabaseType(value) {
|
|
181
|
-
return value === "mysql" || value === "postgres" || value === "sqlite" || value === "oracle" || value === "hive" || value === "impala";
|
|
182
|
-
}
|
|
183
182
|
//#endregion
|
|
184
183
|
export { Config, DATA_AGENT_PATH, apply, inject, name, validateConnectBody };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS } from "./defaults-Cngd8Tf8.js";
|
|
2
|
+
import { a as parseStructuredQueryOutput, c as clientsSchema, l as enforceReadRowLimit, n as redactQueryResult, o as runClientQuery, r as redactSecretText, s as classifyStatement, u as assertSingleStatement } from "./connections-CHY4uB6z.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -607,168 +607,6 @@ const ANALYSIS_REPORT_OUTPUT_SCHEMA = {
|
|
|
607
607
|
additionalProperties: false
|
|
608
608
|
};
|
|
609
609
|
//#endregion
|
|
610
|
-
//#region src/structured.ts
|
|
611
|
-
function normalizeNewlines(text) {
|
|
612
|
-
return text.replace(/\r\n?/g, "\n");
|
|
613
|
-
}
|
|
614
|
-
function splitLine(line, delimiter) {
|
|
615
|
-
return line.split(delimiter);
|
|
616
|
-
}
|
|
617
|
-
/** Make column names valid unique JSON object keys. */
|
|
618
|
-
function uniqueColumns(columns) {
|
|
619
|
-
const used = /* @__PURE__ */ new Set();
|
|
620
|
-
return columns.map((raw, index) => {
|
|
621
|
-
let name = raw.trim();
|
|
622
|
-
if (name.length === 0) name = `column_${index + 1}`;
|
|
623
|
-
if (used.has(name)) {
|
|
624
|
-
let suffix = 2;
|
|
625
|
-
while (used.has(`${name}_${suffix}`)) suffix += 1;
|
|
626
|
-
name = `${name}_${suffix}`;
|
|
627
|
-
}
|
|
628
|
-
used.add(name);
|
|
629
|
-
return name;
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
function rowObject(columns, fields) {
|
|
633
|
-
const row = {};
|
|
634
|
-
for (let index = 0; index < columns.length; index += 1) row[columns[index]] = fields[index] ?? null;
|
|
635
|
-
return row;
|
|
636
|
-
}
|
|
637
|
-
function emptyOutput() {
|
|
638
|
-
return {
|
|
639
|
-
columns: [],
|
|
640
|
-
rows: [],
|
|
641
|
-
rowLimitExceeded: false
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
function skipLeadingBlank(lines) {
|
|
645
|
-
let index = 0;
|
|
646
|
-
while (index < lines.length && lines[index].trim().length === 0) index += 1;
|
|
647
|
-
return index;
|
|
648
|
-
}
|
|
649
|
-
/** PostgreSQL `-A` appends a `(N rows)` / `(N row)` footer after SELECT output. */
|
|
650
|
-
function isPostgresFooter(line) {
|
|
651
|
-
return /^\(\d+ rows?\)$/.test(line.trim());
|
|
652
|
-
}
|
|
653
|
-
function parseDelimited(stdout, delimiter, maxRows, skipFooter = false) {
|
|
654
|
-
const lines = normalizeNewlines(stdout).split("\n");
|
|
655
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
656
|
-
const headerIndex = skipLeadingBlank(lines);
|
|
657
|
-
if (headerIndex >= lines.length) return emptyOutput();
|
|
658
|
-
const columns = uniqueColumns(splitLine(lines[headerIndex], delimiter));
|
|
659
|
-
const rows = [];
|
|
660
|
-
let rowLimitExceeded = false;
|
|
661
|
-
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
662
|
-
const line = lines[index];
|
|
663
|
-
if (skipFooter && isPostgresFooter(line)) continue;
|
|
664
|
-
if (rows.length >= maxRows) {
|
|
665
|
-
rowLimitExceeded = true;
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
|
-
rows.push(rowObject(columns, splitLine(line, delimiter)));
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
columns,
|
|
672
|
-
rows,
|
|
673
|
-
rowLimitExceeded
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
/** Minimal RFC-4180-style parser for sqlite3 `-csv` output. */
|
|
677
|
-
function parseCsv(text) {
|
|
678
|
-
const records = [];
|
|
679
|
-
let record = [];
|
|
680
|
-
let field = "";
|
|
681
|
-
let quoted = false;
|
|
682
|
-
let index = 0;
|
|
683
|
-
const pushField = () => {
|
|
684
|
-
record.push(field);
|
|
685
|
-
field = "";
|
|
686
|
-
};
|
|
687
|
-
const pushRecord = () => {
|
|
688
|
-
pushField();
|
|
689
|
-
records.push(record);
|
|
690
|
-
record = [];
|
|
691
|
-
};
|
|
692
|
-
while (index < text.length) {
|
|
693
|
-
const char = text[index];
|
|
694
|
-
if (quoted) {
|
|
695
|
-
if (char === "\"") {
|
|
696
|
-
if (text[index + 1] === "\"") {
|
|
697
|
-
field += "\"";
|
|
698
|
-
index += 2;
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
quoted = false;
|
|
702
|
-
index += 1;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
field += char;
|
|
706
|
-
index += 1;
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (char === "\"" && field.length === 0) {
|
|
710
|
-
quoted = true;
|
|
711
|
-
index += 1;
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (char === ",") {
|
|
715
|
-
pushField();
|
|
716
|
-
index += 1;
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (char === "\n") {
|
|
720
|
-
pushRecord();
|
|
721
|
-
index += 1;
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
if (char === "\r") {
|
|
725
|
-
if (text[index + 1] === "\n") index += 1;
|
|
726
|
-
pushRecord();
|
|
727
|
-
index += 1;
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
field += char;
|
|
731
|
-
index += 1;
|
|
732
|
-
}
|
|
733
|
-
if (field.length > 0 || record.length > 0) pushRecord();
|
|
734
|
-
return records;
|
|
735
|
-
}
|
|
736
|
-
function parseCsvOutput(stdout, maxRows) {
|
|
737
|
-
const records = parseCsv(normalizeNewlines(stdout)).filter((record) => !(record.length === 1 && record[0] === ""));
|
|
738
|
-
if (records.length === 0) return emptyOutput();
|
|
739
|
-
const columns = uniqueColumns(records[0]);
|
|
740
|
-
const rows = [];
|
|
741
|
-
let rowLimitExceeded = false;
|
|
742
|
-
for (let index = 1; index < records.length; index += 1) {
|
|
743
|
-
if (rows.length >= maxRows) {
|
|
744
|
-
rowLimitExceeded = true;
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
rows.push(rowObject(columns, records[index]));
|
|
748
|
-
}
|
|
749
|
-
return {
|
|
750
|
-
columns,
|
|
751
|
-
rows,
|
|
752
|
-
rowLimitExceeded
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Parse one database type's structured-query stdout. The matching template is
|
|
757
|
-
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
758
|
-
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
759
|
-
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
760
|
-
*/
|
|
761
|
-
function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
762
|
-
switch (type) {
|
|
763
|
-
case "mysql": return parseDelimited(stdout, " ", maxRows);
|
|
764
|
-
case "postgres": return parseDelimited(stdout, "|", maxRows, true);
|
|
765
|
-
case "sqlite": return parseCsvOutput(stdout, maxRows);
|
|
766
|
-
case "oracle": return parseDelimited(stdout, "|", maxRows);
|
|
767
|
-
case "hive":
|
|
768
|
-
case "impala": return parseDelimited(stdout, " ", maxRows);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
//#endregion
|
|
772
610
|
//#region src/structured-read.ts
|
|
773
611
|
/** Look up the session connection, failing with the same message for every tool. */
|
|
774
612
|
async function requireToolConnection(ctx, exec, toolName) {
|
|
@@ -803,7 +641,7 @@ function runnerOptions(resolved, mode) {
|
|
|
803
641
|
/**
|
|
804
642
|
* Execute one read-only SQL through the structured client template and parse
|
|
805
643
|
* it into the canonical { columns, rows } shape, with maxRows enforced at both
|
|
806
|
-
* the SQL level (
|
|
644
|
+
* the SQL level (dialect rewrite) and the parse level.
|
|
807
645
|
*/
|
|
808
646
|
async function runStructuredReadQuery(ctx, connection, sql, resolved, toolName, signal) {
|
|
809
647
|
if (sql.trim().length === 0) throw new Error(toolName + ": sql 不能为空");
|
|
@@ -1099,7 +937,7 @@ function apply(ctx, config) {
|
|
|
1099
937
|
parameters: { sql: {
|
|
1100
938
|
type: "string",
|
|
1101
939
|
required: true,
|
|
1102
|
-
description: "
|
|
940
|
+
description: "一条符合当前数据库方言的只读 SQL,如 \"SELECT * FROM orders;\"、\"SHOW TABLES;\"、\"DESCRIBE users;\""
|
|
1103
941
|
} },
|
|
1104
942
|
output: {
|
|
1105
943
|
schema: {
|
|
@@ -1221,7 +1059,7 @@ function apply(ctx, config) {
|
|
|
1221
1059
|
parameters: { sql: {
|
|
1222
1060
|
type: "string",
|
|
1223
1061
|
required: true,
|
|
1224
|
-
description: "
|
|
1062
|
+
description: "一条符合当前数据库方言的 SQL 文本(或数据库命令),如 \"SHOW TABLES;\"、\"DESCRIBE users;\"、\"SELECT * FROM orders;\""
|
|
1225
1063
|
} },
|
|
1226
1064
|
output: {
|
|
1227
1065
|
schema: {
|
package/lib/tool.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as name, n as apply, r as inject, t as Config } from "./tool-
|
|
1
|
+
import { i as name, n as apply, r as inject, t as Config } from "./tool-ZTOS4B33.js";
|
|
2
2
|
export { Config, apply, inject, name };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
-
|
|
3
|
-
export type DatabaseType = 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'hive' | 'impala';
|
|
2
|
+
export type { DatabaseType } from '../database-types.ts';
|
|
4
3
|
/** The sessions-list slice the workbench needs (structural; avoids a runtime import). */
|
|
5
4
|
export interface SessionListLike {
|
|
6
5
|
byId: Record<string, {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
export interface StructuredWorkbenchResult {
|
|
3
|
+
kind: 'table';
|
|
4
|
+
columns: string[];
|
|
5
|
+
rows: Record<string, string | null>[];
|
|
6
|
+
elapsedMs: number;
|
|
7
|
+
truncated: boolean;
|
|
8
|
+
maxRows: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function QueryResultTable({ result, t, }: {
|
|
11
|
+
result: StructuredWorkbenchResult;
|
|
12
|
+
t: TranslateNS<'data-agent'>;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -13,6 +13,9 @@ export declare const zh: {
|
|
|
13
13
|
'type.oracle': string;
|
|
14
14
|
'type.hive': string;
|
|
15
15
|
'type.impala': string;
|
|
16
|
+
'type.clickhouse': string;
|
|
17
|
+
'type.doris': string;
|
|
18
|
+
'type.sqlserver': string;
|
|
16
19
|
'form.host': string;
|
|
17
20
|
'form.port': string;
|
|
18
21
|
'form.user': string;
|
|
@@ -28,6 +31,8 @@ export declare const zh: {
|
|
|
28
31
|
'form.rememberPassword.hint': string;
|
|
29
32
|
'form.readonly': string;
|
|
30
33
|
'form.readonly.hint': string;
|
|
34
|
+
'form.secure': string;
|
|
35
|
+
'form.secure.hint': string;
|
|
31
36
|
'form.database': string;
|
|
32
37
|
'form.database.oracle': string;
|
|
33
38
|
'form.database.hive': string;
|
|
@@ -57,10 +62,29 @@ export declare const zh: {
|
|
|
57
62
|
'wb.columns': string;
|
|
58
63
|
'wb.sql': string;
|
|
59
64
|
'wb.sql.placeholder': string;
|
|
65
|
+
'wb.sql.placeholder.sqlserver': string;
|
|
60
66
|
'wb.sql.run': string;
|
|
61
67
|
'wb.sql.running': string;
|
|
62
68
|
'wb.sql.shortcut': string;
|
|
63
69
|
'wb.sql.empty': string;
|
|
70
|
+
'wb.sql.result.summary': string;
|
|
71
|
+
'wb.sql.result.capped': string;
|
|
72
|
+
'wb.sql.result.outputTruncated': string;
|
|
73
|
+
'wb.sql.result.table': string;
|
|
74
|
+
'wb.sql.result.noRows': string;
|
|
75
|
+
'wb.sql.command.done': string;
|
|
76
|
+
'wb.sql.export.actions': string;
|
|
77
|
+
'wb.sql.export.excel': string;
|
|
78
|
+
'wb.sql.export.csv': string;
|
|
79
|
+
'wb.sql.export.clipboard': string;
|
|
80
|
+
'wb.sql.exporting': string;
|
|
81
|
+
'wb.sql.copying': string;
|
|
82
|
+
'wb.sql.export.done': string;
|
|
83
|
+
'wb.sql.copy.done': string;
|
|
84
|
+
'wb.sql.export.failed': string;
|
|
85
|
+
'wb.sql.page.summary': string;
|
|
86
|
+
'wb.sql.page.previous': string;
|
|
87
|
+
'wb.sql.page.next': string;
|
|
64
88
|
'wb.loading': string;
|
|
65
89
|
'wb.empty': string;
|
|
66
90
|
'wb.modal.title': string;
|
|
@@ -101,6 +125,9 @@ export declare const en: {
|
|
|
101
125
|
'type.oracle': string;
|
|
102
126
|
'type.hive': string;
|
|
103
127
|
'type.impala': string;
|
|
128
|
+
'type.clickhouse': string;
|
|
129
|
+
'type.doris': string;
|
|
130
|
+
'type.sqlserver': string;
|
|
104
131
|
'form.host': string;
|
|
105
132
|
'form.port': string;
|
|
106
133
|
'form.user': string;
|
|
@@ -116,6 +143,8 @@ export declare const en: {
|
|
|
116
143
|
'form.rememberPassword.hint': string;
|
|
117
144
|
'form.readonly': string;
|
|
118
145
|
'form.readonly.hint': string;
|
|
146
|
+
'form.secure': string;
|
|
147
|
+
'form.secure.hint': string;
|
|
119
148
|
'form.database': string;
|
|
120
149
|
'form.database.oracle': string;
|
|
121
150
|
'form.database.hive': string;
|
|
@@ -145,10 +174,29 @@ export declare const en: {
|
|
|
145
174
|
'wb.columns': string;
|
|
146
175
|
'wb.sql': string;
|
|
147
176
|
'wb.sql.placeholder': string;
|
|
177
|
+
'wb.sql.placeholder.sqlserver': string;
|
|
148
178
|
'wb.sql.run': string;
|
|
149
179
|
'wb.sql.running': string;
|
|
150
180
|
'wb.sql.shortcut': string;
|
|
151
181
|
'wb.sql.empty': string;
|
|
182
|
+
'wb.sql.result.summary': string;
|
|
183
|
+
'wb.sql.result.capped': string;
|
|
184
|
+
'wb.sql.result.outputTruncated': string;
|
|
185
|
+
'wb.sql.result.table': string;
|
|
186
|
+
'wb.sql.result.noRows': string;
|
|
187
|
+
'wb.sql.command.done': string;
|
|
188
|
+
'wb.sql.export.actions': string;
|
|
189
|
+
'wb.sql.export.excel': string;
|
|
190
|
+
'wb.sql.export.csv': string;
|
|
191
|
+
'wb.sql.export.clipboard': string;
|
|
192
|
+
'wb.sql.exporting': string;
|
|
193
|
+
'wb.sql.copying': string;
|
|
194
|
+
'wb.sql.export.done': string;
|
|
195
|
+
'wb.sql.copy.done': string;
|
|
196
|
+
'wb.sql.export.failed': string;
|
|
197
|
+
'wb.sql.page.summary': string;
|
|
198
|
+
'wb.sql.page.previous': string;
|
|
199
|
+
'wb.sql.page.next': string;
|
|
152
200
|
'wb.loading': string;
|
|
153
201
|
'wb.empty': string;
|
|
154
202
|
'wb.modal.title': string;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* key is versioned so a future shape change can migrate or ignore old data.
|
|
10
10
|
* @module @yejiming/dsh-data-agent/persistence
|
|
11
11
|
*/
|
|
12
|
-
import type
|
|
12
|
+
import { type DatabaseType } from '../database-types.ts';
|
|
13
13
|
/** localStorage key holding the most recent connection configuration. */
|
|
14
14
|
export declare const CONNECTION_STORAGE_KEY = "dsh-data-agent.connection.v1";
|
|
15
15
|
/** The persisted connection configuration. */
|
|
@@ -28,6 +28,8 @@ export interface SavedConnection {
|
|
|
28
28
|
/** Opt-in flag; when true, {@link saveConnection} may write `password`. */
|
|
29
29
|
persistPassword?: boolean;
|
|
30
30
|
readonly?: boolean;
|
|
31
|
+
/** ClickHouse only: HTTPS with normal certificate verification. */
|
|
32
|
+
secure?: boolean;
|
|
31
33
|
/** Diagnostic timestamp of the save. */
|
|
32
34
|
savedAt: string;
|
|
33
35
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One structured result shape accepted by every workbench export. */
|
|
2
|
+
export interface QueryExportData {
|
|
3
|
+
columns: readonly string[];
|
|
4
|
+
rows: readonly Readonly<Record<string, string | null>>[];
|
|
5
|
+
}
|
|
6
|
+
/** UTF-8 CSV with BOM so desktop Excel opens Chinese text correctly. */
|
|
7
|
+
export declare function queryResultToCsv(data: QueryExportData): string;
|
|
8
|
+
/** Tabular plain text suitable for spreadsheet clipboard paste. */
|
|
9
|
+
export declare function queryResultToTsv(data: QueryExportData): string;
|
|
10
|
+
/** Build a real XLSX workbook using inline strings, frozen headers, and filters. */
|
|
11
|
+
export declare function queryResultToXlsx(data: QueryExportData): Uint8Array;
|