@yejiming/dsh-data-agent 0.0.5 → 0.0.9
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 +127 -128
- package/README.md +123 -127
- package/lib/client.js +42071 -114
- package/lib/client.js.map +1 -1
- package/lib/command.js +875 -0
- package/lib/{defaults-Bac6QvNt.js → connections-DeauhaZi.js} +427 -19
- package/lib/defaults-DP4RyRh1.js +21 -0
- package/lib/index.js +185 -51
- package/lib/routes.js +94 -170
- package/lib/tool.js +751 -50
- 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/analysis-charts.d.ts +40 -0
- package/lib/types/client/analysis-view-model.d.ts +44 -0
- package/lib/types/client/locales.d.ts +48 -0
- package/lib/types/client/persistence.d.ts +6 -1
- package/lib/types/clients.d.ts +10 -9
- package/lib/types/command.d.ts +41 -0
- package/lib/types/connections.d.ts +115 -40
- package/lib/types/index.d.ts +67 -47
- package/lib/types/routes.d.ts +25 -77
- 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 +25 -20
- package/lib/types/tui-connection-form.d.ts +98 -0
- package/package.json +61 -4
- package/preset/data-agent/agent.cordis.yml +34 -18
- package/preset/data-agent/preset.yml +2 -2
- package/lib/query-CmhTFklw.js +0 -86
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import "./defaults-DP4RyRh1.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import z from "schemastery";
|
|
4
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
5
|
//#region src/sql.ts
|
|
3
6
|
/**
|
|
4
|
-
* Lightweight SQL-text scanning helpers shared by the
|
|
7
|
+
* Lightweight SQL-text scanning helpers shared by the sql-cmd tool half and
|
|
5
8
|
* the /query route. This is intentionally NOT a SQL parser: the scanner only
|
|
6
9
|
* understands lexical boundaries (strings, quoted identifiers, comments and
|
|
7
10
|
* parenthesis depth) well enough to make the two agent-loop guarantees from
|
|
@@ -888,24 +891,429 @@ function parseColumns(type, stdout) {
|
|
|
888
891
|
return columns;
|
|
889
892
|
}
|
|
890
893
|
//#endregion
|
|
891
|
-
//#region src/
|
|
894
|
+
//#region src/query.ts
|
|
895
|
+
/** Read one collected stream from offset 0. */
|
|
896
|
+
function readCaptured(reader) {
|
|
897
|
+
if (reader === void 0) return {
|
|
898
|
+
text: "",
|
|
899
|
+
truncated: false
|
|
900
|
+
};
|
|
901
|
+
const read = reader.readFrom(0);
|
|
902
|
+
return {
|
|
903
|
+
text: read.text,
|
|
904
|
+
truncated: read.lossy
|
|
905
|
+
};
|
|
906
|
+
}
|
|
892
907
|
/**
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
908
|
+
* Run one SQL text through the type's CLI client. The SQL is written to the
|
|
909
|
+
* child's stdin (`{ data }` batch disposition) so it never appears in argv;
|
|
910
|
+
* passwords travel in the env entries built by the template.
|
|
911
|
+
*
|
|
912
|
+
* Failure classification:
|
|
913
|
+
* - the caller's external signal (e.g. the tool exec signal) aborts → the
|
|
914
|
+
* abort reason propagates;
|
|
915
|
+
* - the internal timeout fires → an Error naming the deadline is thrown;
|
|
916
|
+
* - the executable cannot be resolved → an Error naming the command is thrown;
|
|
917
|
+
* - the process runs to completion → `{ exitCode, stdout, stderr, truncated }`
|
|
918
|
+
* is returned even for a non-zero exit (the caller decides what that means).
|
|
919
|
+
* @param ctx - context exposing the subprocess service.
|
|
920
|
+
* @param connection - the stored connection (password included).
|
|
921
|
+
* @param sql - the SQL text (or client command) to run.
|
|
922
|
+
* @param options - timeouts, caps, client overrides.
|
|
923
|
+
* @param externalSignal - caller-owned cancellation (the tool exec signal).
|
|
924
|
+
* @param introspect - use the machine-readable introspection flag set.
|
|
925
|
+
* @returns the captured outcome.
|
|
897
926
|
*/
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
927
|
+
async function runClientQuery(ctx, connection, sql, options, externalSignal, introspect = false) {
|
|
928
|
+
const template = options.mode === "structured" ? buildStructuredQueryTemplate(connection.type, connection, options.clients[connection.type]) : options.mode === "introspect" || introspect ? buildIntrospectTemplate(connection.type, connection, options.clients[connection.type]) : buildClientTemplate(connection.type, connection, options.clients[connection.type]);
|
|
929
|
+
const controller = new AbortController();
|
|
930
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`查询超过 ${options.timeoutMs}ms 未完成,已终止客户端进程`)), options.timeoutMs);
|
|
931
|
+
const onExternalAbort = () => {
|
|
932
|
+
controller.abort(externalSignal.reason);
|
|
933
|
+
};
|
|
934
|
+
if (externalSignal.aborted) controller.abort(externalSignal.reason);
|
|
935
|
+
else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
936
|
+
try {
|
|
937
|
+
let executable;
|
|
938
|
+
try {
|
|
939
|
+
executable = await ctx.subprocess.resolveExecutable(template.command, template.env, controller.signal);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
controller.signal.throwIfAborted();
|
|
942
|
+
throw new Error(`无法解析数据库客户端 "${template.command}"(${error instanceof Error ? error.message : String(error)});请确认客户端已安装,或在 data-agent 插件配置的 clients 中覆盖命令名/路径`);
|
|
943
|
+
}
|
|
944
|
+
const handle = ctx.subprocess.spawn({
|
|
945
|
+
argv: [executable, ...template.args],
|
|
946
|
+
cwd: process.cwd(),
|
|
947
|
+
stdio: {
|
|
948
|
+
stdin: { data: `${template.stdinPrefix}${sql}\n` },
|
|
949
|
+
stdout: { maxBytes: options.maxResultChars },
|
|
950
|
+
stderr: { maxBytes: options.maxResultChars }
|
|
951
|
+
},
|
|
952
|
+
graceMs: options.graceMs ?? 5e3,
|
|
953
|
+
signal: controller.signal,
|
|
954
|
+
env: template.env
|
|
955
|
+
});
|
|
956
|
+
let outcome;
|
|
957
|
+
try {
|
|
958
|
+
outcome = await handle.done;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
controller.signal.throwIfAborted();
|
|
961
|
+
throw new Error(`启动数据库客户端失败:${error instanceof Error ? error.message : String(error)}`);
|
|
962
|
+
}
|
|
963
|
+
if (controller.signal.aborted) controller.signal.throwIfAborted();
|
|
964
|
+
const stdout = readCaptured(handle.collected.stdout);
|
|
965
|
+
const stderr = readCaptured(handle.collected.stderr);
|
|
966
|
+
return {
|
|
967
|
+
exitCode: outcome.exitCode,
|
|
968
|
+
stdout: stdout.text,
|
|
969
|
+
stderr: stderr.text,
|
|
970
|
+
truncated: stdout.truncated || stderr.truncated
|
|
971
|
+
};
|
|
972
|
+
} finally {
|
|
973
|
+
clearTimeout(timer);
|
|
974
|
+
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/** Build a password-stripped copy of one connection. */
|
|
978
|
+
function summarize(connection) {
|
|
979
|
+
const summary = {
|
|
980
|
+
type: connection.type,
|
|
981
|
+
database: connection.database
|
|
982
|
+
};
|
|
983
|
+
if (connection.host !== void 0) summary.host = connection.host;
|
|
984
|
+
if (connection.port !== void 0) summary.port = connection.port;
|
|
985
|
+
if (connection.user !== void 0) summary.user = connection.user;
|
|
986
|
+
if (connection.passwordRef !== void 0) summary.passwordRef = connection.passwordRef;
|
|
987
|
+
if (connection.readonly !== void 0) summary.readonly = connection.readonly;
|
|
988
|
+
if (connection.profileId !== void 0) summary.profileId = connection.profileId;
|
|
989
|
+
if (connection.name !== void 0) summary.name = connection.name;
|
|
990
|
+
if (connection.tables !== void 0) summary.tables = [...connection.tables];
|
|
991
|
+
return summary;
|
|
992
|
+
}
|
|
993
|
+
/** Replace every occurrence of a resolved secret before crossing a public seam. */
|
|
994
|
+
function redactSecretText(text, secrets) {
|
|
995
|
+
let redacted = text;
|
|
996
|
+
for (const secret of secrets) if (secret !== void 0 && secret.length > 0) redacted = redacted.split(secret).join("[REDACTED]");
|
|
997
|
+
return redacted;
|
|
998
|
+
}
|
|
999
|
+
/** Redact a client result without mutating the runner-owned object. */
|
|
1000
|
+
function redactQueryResult(result, connection) {
|
|
1001
|
+
const secrets = [connection.password];
|
|
1002
|
+
return {
|
|
1003
|
+
...result,
|
|
1004
|
+
stdout: redactSecretText(result.stdout, secrets),
|
|
1005
|
+
stderr: redactSecretText(result.stderr, secrets)
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
/** Validate/normalize a shared connect input before any I/O. */
|
|
1009
|
+
function normalizeConnectionInput(input, cwd = process.cwd()) {
|
|
1010
|
+
if (!isDatabaseType(input.type)) throw new Error("数据库类型无效");
|
|
1011
|
+
if (typeof input.database !== "string" || input.database.trim().length === 0) throw new Error("database 必须是非空字符串");
|
|
1012
|
+
if (input.password !== void 0 && input.passwordRef !== void 0) throw new Error("password 与 passwordRef 不能同时提供");
|
|
1013
|
+
if (input.passwordRef !== void 0) validatePasswordRef(input.passwordRef);
|
|
1014
|
+
if (input.port !== void 0 && (!Number.isInteger(input.port) || input.port < 1 || input.port > 65535)) throw new Error("port 必须是 1-65535 的整数");
|
|
1015
|
+
if (input.profileId !== void 0 && input.profileId.trim().length === 0) throw new Error("profileId 不能为空");
|
|
1016
|
+
if (input.name !== void 0 && input.name.trim().length === 0) throw new Error("name 不能为空");
|
|
1017
|
+
const connection = {
|
|
1018
|
+
type: input.type,
|
|
1019
|
+
database: input.type === "sqlite" ? resolve(cwd, input.database) : input.database
|
|
1020
|
+
};
|
|
1021
|
+
if (input.type !== "sqlite") {
|
|
1022
|
+
if (input.host !== void 0 && input.host.length > 0) connection.host = input.host;
|
|
1023
|
+
if (input.port !== void 0) connection.port = input.port;
|
|
1024
|
+
if (input.user !== void 0 && input.user.length > 0) connection.user = input.user;
|
|
1025
|
+
if (input.password !== void 0 && input.password.length > 0) connection.password = input.password;
|
|
1026
|
+
if (input.passwordRef !== void 0) connection.passwordRef = input.passwordRef;
|
|
1027
|
+
}
|
|
1028
|
+
if (input.readonly !== void 0) connection.readonly = input.readonly;
|
|
1029
|
+
if (input.profileId !== void 0) connection.profileId = input.profileId;
|
|
1030
|
+
if (input.name !== void 0) connection.name = input.name;
|
|
1031
|
+
return connection;
|
|
1032
|
+
}
|
|
1033
|
+
/** Create the surface-independent service. */
|
|
1034
|
+
function createConnectionService(ctx, options, persistence) {
|
|
1035
|
+
const resolvedOptions = options ?? {
|
|
1036
|
+
connectTimeoutMs: 15e3,
|
|
1037
|
+
queryTimeoutMs: 3e4,
|
|
1038
|
+
maxResultChars: 2e5,
|
|
1039
|
+
maxQueryChars: 65536,
|
|
1040
|
+
introspectMaxTables: 500,
|
|
1041
|
+
readonly: false,
|
|
1042
|
+
clients: {}
|
|
1043
|
+
};
|
|
1044
|
+
const runtime = /* @__PURE__ */ new Map();
|
|
1045
|
+
const formDrafts = /* @__PURE__ */ new Map();
|
|
1046
|
+
const profileConnection = (sessionId) => {
|
|
1047
|
+
if (persistence === void 0) return void 0;
|
|
1048
|
+
const binding = persistence.getBinding(sessionId);
|
|
1049
|
+
if (binding === void 0) return void 0;
|
|
1050
|
+
const profile = persistence.getProfile(binding.profileId);
|
|
1051
|
+
return profile === void 0 ? void 0 : connectionFromProfile(binding.profileId, profile);
|
|
1052
|
+
};
|
|
1053
|
+
/** Required precedence: exact runtime, exact binding, wildcard runtime, wildcard binding. */
|
|
1054
|
+
const rawConnection = (sessionId) => runtime.get(sessionId) ?? profileConnection(sessionId) ?? runtime.get("*") ?? profileConnection("*");
|
|
1055
|
+
const requireContext = () => {
|
|
1056
|
+
if (ctx === void 0) throw new Error("数据库执行服务尚未配置");
|
|
1057
|
+
return ctx;
|
|
1058
|
+
};
|
|
1059
|
+
const resolveCredential = async (connection) => {
|
|
1060
|
+
if (connection.passwordRef === void 0) return {
|
|
1061
|
+
...connection,
|
|
1062
|
+
tables: copyTables(connection.tables)
|
|
1063
|
+
};
|
|
1064
|
+
const ref = validatedCredentialRef(connection.passwordRef);
|
|
1065
|
+
const hit = await requireContext().credentials.resolve(ref);
|
|
1066
|
+
if (hit === void 0 || hit.value.length === 0) throw new Error(`凭据引用 "${connection.passwordRef}" 未配置`);
|
|
1067
|
+
return {
|
|
1068
|
+
...connection,
|
|
1069
|
+
password: hit.value,
|
|
1070
|
+
tables: copyTables(connection.tables)
|
|
1071
|
+
};
|
|
1072
|
+
};
|
|
1073
|
+
const queryOptions = (mode, connect = false) => ({
|
|
1074
|
+
clients: resolvedOptions.clients,
|
|
1075
|
+
timeoutMs: connect ? resolvedOptions.connectTimeoutMs : resolvedOptions.queryTimeoutMs,
|
|
1076
|
+
maxResultChars: resolvedOptions.maxResultChars,
|
|
1077
|
+
...mode !== void 0 ? { mode } : {}
|
|
1078
|
+
});
|
|
1079
|
+
const run = async (connection, sql, signal, introspection = false, connect = false) => {
|
|
1080
|
+
try {
|
|
1081
|
+
return redactQueryResult(await runClientQuery(requireContext(), connection, sql, queryOptions(void 0, connect), signal, introspection), connection);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
const message = redactSecretText(error instanceof Error ? error.message : String(error), [connection.password]);
|
|
1084
|
+
throw new Error(message, error instanceof Error ? { cause: error } : void 0);
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
const verify = async (connection, signal, connect = false) => {
|
|
1088
|
+
const result = await run(connection, tableListingSql(connection.type, connection), signal, true, connect);
|
|
1089
|
+
if (result.exitCode !== 0) {
|
|
1090
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
1091
|
+
throw new Error(`数据库连接验证失败(exit ${result.exitCode}):${detail}`);
|
|
1092
|
+
}
|
|
1093
|
+
return parseTableListing(connection.type, result.stdout).slice(0, resolvedOptions.introspectMaxTables);
|
|
1094
|
+
};
|
|
1095
|
+
const persistAtomically = async (sessionId, profileId, profile) => {
|
|
1096
|
+
if (persistence === void 0) return;
|
|
1097
|
+
const previousProfile = persistence.getProfile(profileId);
|
|
1098
|
+
const previousBinding = persistence.getBinding(sessionId);
|
|
1099
|
+
await persistence.putProfile(profileId, profile);
|
|
1100
|
+
try {
|
|
1101
|
+
await persistence.putBinding(sessionId, {
|
|
1102
|
+
profileId,
|
|
1103
|
+
updatedAt: profile.updatedAt
|
|
1104
|
+
});
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (previousProfile === void 0) await persistence.deleteProfile(profileId);
|
|
1107
|
+
else await persistence.putProfile(profileId, previousProfile);
|
|
1108
|
+
if (previousBinding === void 0) await persistence.deleteBinding(sessionId);
|
|
1109
|
+
else await persistence.putBinding(sessionId, previousBinding);
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
const credentialSummary = async (connection) => {
|
|
1114
|
+
if (connection.type === "sqlite") return void 0;
|
|
1115
|
+
if (connection.password !== void 0) return {
|
|
1116
|
+
configured: true,
|
|
1117
|
+
source: "memory"
|
|
1118
|
+
};
|
|
1119
|
+
if (connection.passwordRef === void 0) return { configured: false };
|
|
1120
|
+
const info = await requireContext().credentials.describe(validatedCredentialRef(connection.passwordRef));
|
|
1121
|
+
return {
|
|
1122
|
+
configured: info.configured,
|
|
1123
|
+
...info.source !== void 0 ? { source: info.source } : {}
|
|
1124
|
+
};
|
|
1125
|
+
};
|
|
1126
|
+
const service = {
|
|
1127
|
+
set(sessionId, connection) {
|
|
1128
|
+
if (connection.password !== void 0 && connection.passwordRef !== void 0) throw new Error("password 与 passwordRef 不能同时提供");
|
|
1129
|
+
if (connection.passwordRef !== void 0) validatePasswordRef(connection.passwordRef);
|
|
1130
|
+
runtime.set(sessionId, {
|
|
1131
|
+
...connection,
|
|
1132
|
+
tables: copyTables(connection.tables)
|
|
1133
|
+
});
|
|
1134
|
+
},
|
|
1135
|
+
get(sessionId) {
|
|
1136
|
+
const connection = rawConnection(sessionId);
|
|
1137
|
+
return connection === void 0 ? void 0 : summarize(connection);
|
|
1138
|
+
},
|
|
1139
|
+
getWithSecret(sessionId) {
|
|
1140
|
+
const connection = rawConnection(sessionId);
|
|
1141
|
+
return connection === void 0 ? void 0 : {
|
|
1142
|
+
...connection,
|
|
1143
|
+
tables: copyTables(connection.tables)
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
1146
|
+
has(sessionId) {
|
|
1147
|
+
return rawConnection(sessionId) !== void 0;
|
|
1148
|
+
},
|
|
1149
|
+
clear(sessionId) {
|
|
1150
|
+
runtime.delete(sessionId);
|
|
1151
|
+
},
|
|
1152
|
+
getFormDraft(sessionId) {
|
|
1153
|
+
const draft = persistence?.getDraft?.(sessionId) ?? formDrafts.get(sessionId);
|
|
1154
|
+
return draft === void 0 ? void 0 : copyFormDraft(draft);
|
|
1155
|
+
},
|
|
1156
|
+
async saveFormDraft(sessionId, draft) {
|
|
1157
|
+
if (sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
1158
|
+
const safe = normalizeFormDraft(draft);
|
|
1159
|
+
if (persistence?.putDraft !== void 0) await persistence.putDraft(sessionId, {
|
|
1160
|
+
...safe,
|
|
1161
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1162
|
+
});
|
|
1163
|
+
else formDrafts.set(sessionId, safe);
|
|
1164
|
+
},
|
|
1165
|
+
async status(sessionId) {
|
|
1166
|
+
const connection = rawConnection(sessionId);
|
|
1167
|
+
if (connection === void 0) return void 0;
|
|
1168
|
+
const summary = summarize(connection);
|
|
1169
|
+
summary.credential = await credentialSummary(connection);
|
|
1170
|
+
return summary;
|
|
1171
|
+
},
|
|
1172
|
+
async connect(sessionId, input, signal) {
|
|
1173
|
+
if (sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
1174
|
+
const normalized = normalizeConnectionInput(input, resolvedOptions.cwd);
|
|
1175
|
+
const execution = await resolveCredential(normalized);
|
|
1176
|
+
const tables = await verify(execution, signal, true);
|
|
1177
|
+
const profileId = normalized.profileId ?? `session:${sessionId}`;
|
|
1178
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1179
|
+
await persistAtomically(sessionId, profileId, profileFromConnection(normalized, updatedAt));
|
|
1180
|
+
const published = {
|
|
1181
|
+
...normalized,
|
|
1182
|
+
profileId,
|
|
1183
|
+
tables
|
|
1184
|
+
};
|
|
1185
|
+
runtime.set(sessionId, published);
|
|
1186
|
+
const summary = summarize(published);
|
|
1187
|
+
summary.credential = await credentialSummary(published);
|
|
1188
|
+
return {
|
|
1189
|
+
tables,
|
|
1190
|
+
summary
|
|
1191
|
+
};
|
|
1192
|
+
},
|
|
1193
|
+
async disconnect(sessionId) {
|
|
1194
|
+
runtime.delete(sessionId);
|
|
1195
|
+
if (persistence !== void 0) await persistence.deleteBinding(sessionId);
|
|
1196
|
+
},
|
|
1197
|
+
async test(sessionId, signal) {
|
|
1198
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
1199
|
+
const tables = await verify(connection, signal);
|
|
1200
|
+
const published = {
|
|
1201
|
+
...rawConnection(sessionId),
|
|
1202
|
+
tables
|
|
1203
|
+
};
|
|
1204
|
+
runtime.set(sessionId, published);
|
|
1205
|
+
const summary = summarize(published);
|
|
1206
|
+
summary.credential = await credentialSummary(published);
|
|
1207
|
+
return {
|
|
1208
|
+
tables,
|
|
1209
|
+
summary
|
|
1210
|
+
};
|
|
1211
|
+
},
|
|
1212
|
+
async resolveForExecution(sessionId) {
|
|
1213
|
+
const connection = rawConnection(sessionId);
|
|
1214
|
+
if (connection === void 0) throw new Error("请先在 Web「数据库」标签页连接数据库,或在 TUI 运行 /database connect(未找到当前会话的连接)");
|
|
1215
|
+
return resolveCredential(connection);
|
|
1216
|
+
},
|
|
1217
|
+
async listSchemas(sessionId, signal) {
|
|
1218
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
1219
|
+
const stdout = await runMetadata(connection, "schemas", signal);
|
|
1220
|
+
return parseListing(connection.type, stdout).slice(0, resolvedOptions.introspectMaxTables);
|
|
1221
|
+
},
|
|
1222
|
+
async listTables(sessionId, schema, signal) {
|
|
1223
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
1224
|
+
if (connection.type !== "sqlite") requireIdentifier(connection.type, schema, "schema");
|
|
1225
|
+
const stdout = await runMetadata(connection, "tables", signal, schema);
|
|
1226
|
+
return parseListing(connection.type, stdout).slice(0, resolvedOptions.introspectMaxTables);
|
|
1227
|
+
},
|
|
1228
|
+
async describe(sessionId, schema, table, signal) {
|
|
1229
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
1230
|
+
if (connection.type !== "sqlite") requireIdentifier(connection.type, schema, "schema");
|
|
1231
|
+
requireIdentifier(connection.type, table, "table");
|
|
1232
|
+
const stdout = await runMetadata(connection, "describe", signal, schema, table);
|
|
1233
|
+
return parseColumns(connection.type, stdout);
|
|
1234
|
+
},
|
|
1235
|
+
async query(sessionId, sql, signal) {
|
|
1236
|
+
if (sql.trim().length === 0) throw new Error("sql 必须是非空字符串");
|
|
1237
|
+
const maxQueryChars = resolvedOptions.maxQueryChars ?? 65536;
|
|
1238
|
+
if (sql.length > maxQueryChars) throw new Error(`sql 超过长度上限(${maxQueryChars} 字符)`);
|
|
1239
|
+
assertSingleStatement(sql, "/query");
|
|
1240
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
1241
|
+
if ((connection.readonly ?? resolvedOptions.readonly) && classifyStatement(sql, connection.type) === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
1242
|
+
return run(connection, sql, signal);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
async function runMetadata(connection, kind, signal, schema, table) {
|
|
1246
|
+
const result = await run(connection, metadataQuery(kind, connection.type, schema, table), signal, true);
|
|
1247
|
+
if (result.exitCode !== 0) {
|
|
1248
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
1249
|
+
throw new Error(`元数据查询失败(exit ${result.exitCode}):${detail}`);
|
|
1250
|
+
}
|
|
1251
|
+
return result.stdout;
|
|
1252
|
+
}
|
|
1253
|
+
return service;
|
|
1254
|
+
}
|
|
1255
|
+
function copyTables(tables) {
|
|
1256
|
+
return tables === void 0 ? void 0 : [...tables];
|
|
1257
|
+
}
|
|
1258
|
+
function normalizeFormDraft(draft) {
|
|
1259
|
+
if (!isDatabaseType(draft.type)) throw new Error("数据库类型无效");
|
|
1260
|
+
if (typeof draft.host !== "string" || typeof draft.port !== "string" || typeof draft.user !== "string" || typeof draft.database !== "string" || typeof draft.readonly !== "boolean") throw new Error("数据库表单草稿无效");
|
|
1261
|
+
return copyFormDraft(draft);
|
|
1262
|
+
}
|
|
1263
|
+
function copyFormDraft(draft) {
|
|
1264
|
+
return {
|
|
1265
|
+
type: draft.type,
|
|
1266
|
+
host: draft.host,
|
|
1267
|
+
port: draft.port,
|
|
1268
|
+
user: draft.user,
|
|
1269
|
+
database: draft.database,
|
|
1270
|
+
readonly: draft.readonly
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function isDatabaseType(value) {
|
|
1274
|
+
return value === "mysql" || value === "postgres" || value === "sqlite" || value === "oracle" || value === "hive" || value === "impala";
|
|
1275
|
+
}
|
|
1276
|
+
function validatePasswordRef(value) {
|
|
1277
|
+
try {
|
|
1278
|
+
credentialRef(value);
|
|
1279
|
+
} catch {
|
|
1280
|
+
throw new Error(`passwordRef "${value}" 无效;必须是 POSIX 环境变量形式的名称`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function validatedCredentialRef(value) {
|
|
1284
|
+
validatePasswordRef(value);
|
|
1285
|
+
return credentialRef(value);
|
|
1286
|
+
}
|
|
1287
|
+
function connectionFromProfile(profileId, profile) {
|
|
1288
|
+
return {
|
|
1289
|
+
type: profile.type,
|
|
1290
|
+
database: profile.database,
|
|
1291
|
+
profileId,
|
|
1292
|
+
...profile.name !== void 0 ? { name: profile.name } : {},
|
|
1293
|
+
...profile.host !== void 0 ? { host: profile.host } : {},
|
|
1294
|
+
...profile.port !== void 0 ? { port: profile.port } : {},
|
|
1295
|
+
...profile.user !== void 0 ? { user: profile.user } : {},
|
|
1296
|
+
...profile.readonly !== void 0 ? { readonly: profile.readonly } : {},
|
|
1297
|
+
...profile.passwordRef !== void 0 ? { passwordRef: profile.passwordRef } : {}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function profileFromConnection(connection, updatedAt) {
|
|
1301
|
+
return {
|
|
1302
|
+
type: connection.type,
|
|
1303
|
+
database: connection.database,
|
|
1304
|
+
updatedAt,
|
|
1305
|
+
...connection.name !== void 0 ? { name: connection.name } : {},
|
|
1306
|
+
...connection.host !== void 0 ? { host: connection.host } : {},
|
|
1307
|
+
...connection.port !== void 0 ? { port: connection.port } : {},
|
|
1308
|
+
...connection.user !== void 0 ? { user: connection.user } : {},
|
|
1309
|
+
...connection.readonly !== void 0 ? { readonly: connection.readonly } : {},
|
|
1310
|
+
...connection.passwordRef !== void 0 ? { passwordRef: connection.passwordRef } : {}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function requireIdentifier(type, value, label) {
|
|
1314
|
+
if (value === void 0 || value.length === 0) throw new Error(`${label} 不能为空`);
|
|
1315
|
+
sanitizeIdentifier(type, value);
|
|
1316
|
+
return value;
|
|
1317
|
+
}
|
|
910
1318
|
//#endregion
|
|
911
|
-
export {
|
|
1319
|
+
export { classifyStatement as a, assertSingleStatement as c, runClientQuery as i, redactQueryResult as n, clientsSchema as o, redactSecretText as r, enforceReadRowLimit as s, createConnectionService as t };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/defaults.ts
|
|
2
|
+
/**
|
|
3
|
+
* Package-wide defaults shared by the server half (`src/index.ts`) and the
|
|
4
|
+
* database tool half (`src/tool.ts`). Loader schemas carry these as their
|
|
5
|
+
* defaults so a deployment may override every one of them in cordis.yml.
|
|
6
|
+
* @module @yejiming/dsh-data-agent/defaults
|
|
7
|
+
*/
|
|
8
|
+
/** Preset directory name installed into `$DSH_HOME/.agent-presets/`. */
|
|
9
|
+
const DEFAULT_PRESET_ID = "data-agent";
|
|
10
|
+
/** End-to-end deadline for one `/connect` connectivity check, milliseconds. */
|
|
11
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
12
|
+
/** End-to-end deadline for one database-tool query, milliseconds. */
|
|
13
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
14
|
+
/** In-memory cap on database-tool captured output (stdout and stderr each). */
|
|
15
|
+
const DEFAULT_MAX_RESULT_CHARS = 2e4;
|
|
16
|
+
/** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
|
|
17
|
+
const DEFAULT_MAX_QUERY_CHARS = 65536;
|
|
18
|
+
/** Grace period for the subprocess terminate escalation. */
|
|
19
|
+
const DEFAULT_GRACE_MS = 5e3;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { DEFAULT_PRESET_ID as a, DEFAULT_MAX_RESULT_CHARS as i, DEFAULT_GRACE_MS as n, DEFAULT_QUERY_TIMEOUT_MS as o, DEFAULT_MAX_QUERY_CHARS as r, DEFAULT_CONNECT_TIMEOUT_MS as t };
|