@yejiming/dsh-data-agent 0.0.13 → 0.1.1
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 +46 -8
- package/README.md +46 -8
- package/conformance/dsh-ecosystem/inventory.json +26 -3
- package/conformance/dsh-ecosystem/restrictions.json +2 -2
- package/cordis.patch.yml +6 -3
- package/dsh-plugin.json +9 -4
- package/lib/catalog-DEJqOXRo.js +1944 -0
- package/lib/catalog-identity-CVftmvQL.js +96 -0
- package/lib/client.js +2214 -106
- package/lib/client.js.map +1 -1
- package/lib/command-CzzSPmag.js +1719 -0
- package/lib/command.js +2 -2
- package/lib/{connections-CHY4uB6z.js → connections-CFXOZTHZ.js} +223 -9
- package/lib/index.js +366 -16
- package/lib/routes.js +257 -4
- package/lib/{tool-ZTOS4B33.js → tool-DNkywSph.js} +364 -3
- package/lib/tool.js +1 -1
- package/lib/types/catalog-adapters.d.ts +52 -0
- package/lib/types/catalog-ai.d.ts +49 -0
- package/lib/types/catalog-command.d.ts +28 -0
- package/lib/types/catalog-identity.d.ts +23 -0
- package/lib/types/catalog-storage.d.ts +265 -0
- package/lib/types/catalog-tools.d.ts +5 -0
- package/lib/types/catalog-tui.d.ts +18 -0
- package/lib/types/catalog-types.d.ts +1376 -0
- package/lib/types/catalog.d.ts +59 -0
- package/lib/types/client/CatalogPanel.d.ts +15 -0
- package/lib/types/client/catalog-client.d.ts +57 -0
- package/lib/types/client/locales.d.ts +242 -0
- package/lib/types/command.d.ts +14 -3
- package/lib/types/connections.d.ts +9 -0
- package/lib/types/defaults.d.ts +22 -0
- package/lib/types/index.d.ts +42 -5
- package/lib/types/tui-connection-form.d.ts +11 -5
- package/package.json +4 -2
- package/preset/data-agent/agent.cordis.yml +9 -1
- package/lib/command-utC5MHd9.js +0 -916
- package/lib/defaults-Cngd8Tf8.js +0 -131
package/lib/command-utC5MHd9.js
DELETED
|
@@ -1,916 +0,0 @@
|
|
|
1
|
-
import { d as defaultDatabasePort$1, l as DATABASE_TYPES, p as isDatabaseType, u as databaseTypeLabel$1 } from "./defaults-Cngd8Tf8.js";
|
|
2
|
-
import { i as validatePasswordRef, r as redactSecretText } from "./connections-CHY4uB6z.js";
|
|
3
|
-
import { RUN_CODE_NAME } from "@deepseek-ai/dsh-tools";
|
|
4
|
-
//#region src/tui-connection-form.ts
|
|
5
|
-
/**
|
|
6
|
-
* Short-lived ANSI connection form used by `/database connect` in dsh-tui.
|
|
7
|
-
*
|
|
8
|
-
* dsh-tui 0.6.x exposes commands but no public custom-form/sensitive-input
|
|
9
|
-
* slot. This adapter therefore owns a small terminal form and only activates
|
|
10
|
-
* for an interactive `dsh-tui` profile. It snapshots the host's `readable`
|
|
11
|
-
* listeners, consumes input for the lifetime of the form, then restores the
|
|
12
|
-
* listeners exactly. It never imports dsh-tui, React, or Ink.
|
|
13
|
-
* @module @yejiming/dsh-data-agent/tui-connection-form
|
|
14
|
-
*/
|
|
15
|
-
const TUI_DATABASE_TYPES = DATABASE_TYPES;
|
|
16
|
-
/** Initial form intentionally leaves host/port empty so placeholders are real defaults. */
|
|
17
|
-
function createTuiConnectionFormState(initialDraft) {
|
|
18
|
-
return {
|
|
19
|
-
type: initialDraft?.type ?? "mysql",
|
|
20
|
-
host: initialDraft?.host ?? "",
|
|
21
|
-
port: initialDraft?.port ?? "",
|
|
22
|
-
user: initialDraft?.user ?? "",
|
|
23
|
-
database: initialDraft?.database ?? "",
|
|
24
|
-
password: "",
|
|
25
|
-
passwordRef: initialDraft?.passwordRef ?? "",
|
|
26
|
-
secure: initialDraft?.secure ?? false,
|
|
27
|
-
readonly: initialDraft?.readonly ?? false,
|
|
28
|
-
focus: "type",
|
|
29
|
-
cursor: 0
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/** Project form state onto the only values allowed to cross the durable seam. */
|
|
33
|
-
function connectionFormDraft(state) {
|
|
34
|
-
return {
|
|
35
|
-
type: state.type,
|
|
36
|
-
host: state.host,
|
|
37
|
-
port: state.port,
|
|
38
|
-
user: state.user,
|
|
39
|
-
database: state.database,
|
|
40
|
-
readonly: state.readonly,
|
|
41
|
-
...state.type === "clickhouse" ? { secure: state.secure } : {}
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** Relevant focus order for the selected database kind. */
|
|
45
|
-
function tuiConnectionFields(type) {
|
|
46
|
-
if (type === "sqlite") return [
|
|
47
|
-
"type",
|
|
48
|
-
"database",
|
|
49
|
-
"readonly",
|
|
50
|
-
"confirm",
|
|
51
|
-
"cancel"
|
|
52
|
-
];
|
|
53
|
-
const fields = [
|
|
54
|
-
"type",
|
|
55
|
-
"host",
|
|
56
|
-
"port",
|
|
57
|
-
"user",
|
|
58
|
-
"database",
|
|
59
|
-
"password",
|
|
60
|
-
"passwordRef"
|
|
61
|
-
];
|
|
62
|
-
if (type === "clickhouse") fields.push("secure");
|
|
63
|
-
return [
|
|
64
|
-
...fields,
|
|
65
|
-
"readonly",
|
|
66
|
-
"confirm",
|
|
67
|
-
"cancel"
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
/** Default network port shown as a placeholder and applied only at submit time. */
|
|
71
|
-
function defaultDatabasePort(type, secure = false) {
|
|
72
|
-
return defaultDatabasePort$1(type, secure);
|
|
73
|
-
}
|
|
74
|
-
/** Detect the supported host without coupling to dsh-tui modules. */
|
|
75
|
-
function isDshTuiTerminal(argv = process.argv, input = process.stdin, output = process.stdout) {
|
|
76
|
-
if (input.isTTY !== true || output.isTTY !== true) return false;
|
|
77
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
78
|
-
const value = argv[index];
|
|
79
|
-
if (value === "--profile" && argv[index + 1] === "dsh-tui") return true;
|
|
80
|
-
if (value === "--profile=dsh-tui") return true;
|
|
81
|
-
}
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
/** Pure keyboard reducer, kept separate from terminal ownership for regression tests. */
|
|
85
|
-
function updateTuiConnectionForm(current, key) {
|
|
86
|
-
let state = {
|
|
87
|
-
...current,
|
|
88
|
-
error: void 0
|
|
89
|
-
};
|
|
90
|
-
if (state.selector !== void 0) return updateOpenSelector(state, key);
|
|
91
|
-
if (key.name === "escape") return {
|
|
92
|
-
kind: "cancelled",
|
|
93
|
-
state: clearPassword(state)
|
|
94
|
-
};
|
|
95
|
-
if (key.name === "tab" || key.name === "backtab") {
|
|
96
|
-
state = moveFocus(state, key.name === "tab" ? 1 : -1);
|
|
97
|
-
return {
|
|
98
|
-
kind: "editing",
|
|
99
|
-
state
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
if (state.focus === "confirm") {
|
|
103
|
-
if (key.name !== "enter") return {
|
|
104
|
-
kind: "editing",
|
|
105
|
-
state
|
|
106
|
-
};
|
|
107
|
-
const validated = validateTuiConnectionForm(state);
|
|
108
|
-
return validated.error !== void 0 ? {
|
|
109
|
-
kind: "editing",
|
|
110
|
-
state: {
|
|
111
|
-
...state,
|
|
112
|
-
error: validated.error
|
|
113
|
-
}
|
|
114
|
-
} : {
|
|
115
|
-
kind: "submitted",
|
|
116
|
-
state: clearPassword(state),
|
|
117
|
-
input: validated.input
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (state.focus === "cancel") return key.name === "enter" ? {
|
|
121
|
-
kind: "cancelled",
|
|
122
|
-
state: clearPassword(state)
|
|
123
|
-
} : {
|
|
124
|
-
kind: "editing",
|
|
125
|
-
state
|
|
126
|
-
};
|
|
127
|
-
if (state.focus === "type") {
|
|
128
|
-
if (key.name === "enter" || key.name === "space") state = {
|
|
129
|
-
...state,
|
|
130
|
-
selector: {
|
|
131
|
-
field: "type",
|
|
132
|
-
index: TUI_DATABASE_TYPES.indexOf(state.type)
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
return {
|
|
136
|
-
kind: "editing",
|
|
137
|
-
state
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
if (state.focus === "readonly" || state.focus === "secure") {
|
|
141
|
-
if (key.name === "enter" || key.name === "space") state = {
|
|
142
|
-
...state,
|
|
143
|
-
selector: {
|
|
144
|
-
field: state.focus,
|
|
145
|
-
index: state[state.focus] ? 1 : 0
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
return {
|
|
149
|
-
kind: "editing",
|
|
150
|
-
state
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (key.name === "enter" || key.name === "up" || key.name === "down") return {
|
|
154
|
-
kind: "editing",
|
|
155
|
-
state
|
|
156
|
-
};
|
|
157
|
-
return {
|
|
158
|
-
kind: "editing",
|
|
159
|
-
state: editTextField(state, key)
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
/** Rendered value is masked before it reaches the ANSI string. */
|
|
163
|
-
function renderTuiConnectionForm(state, columns = 80) {
|
|
164
|
-
const width = Math.max(20, Math.min(72, columns - 8));
|
|
165
|
-
const lines = [
|
|
166
|
-
"\x1B[2J\x1B[H\x1B[?25l",
|
|
167
|
-
`${bold("Data Agent · 数据库连接")}`,
|
|
168
|
-
dim("Tab/Shift+Tab 切换 · Enter 展开/确认选项 · ↑/↓ 选择 · Esc 返回"),
|
|
169
|
-
""
|
|
170
|
-
];
|
|
171
|
-
for (const field of tuiConnectionFields(state.type)) lines.push(...renderField(state, field, width));
|
|
172
|
-
if (state.error !== void 0) lines.push("", red(`! ${state.error}`));
|
|
173
|
-
lines.push("", dim(state.type === "sqlite" ? "SQLite 连接不收集数据库凭据。" : "临时密码不持久化;凭据引用可随非敏感连接 profile 恢复。"));
|
|
174
|
-
return lines.join("\n");
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Own the terminal only for the form lifetime. `undefined` means user cancel.
|
|
178
|
-
* The returned password has never crossed stdout, argv, env, or a DSH event.
|
|
179
|
-
*/
|
|
180
|
-
function runTuiConnectionForm(options = {}) {
|
|
181
|
-
const input = options.input ?? process.stdin;
|
|
182
|
-
const output = options.output ?? process.stdout;
|
|
183
|
-
if (input.isTTY !== true || output.isTTY !== true) return Promise.reject(/* @__PURE__ */ new Error("数据库连接表单需要交互式 TTY"));
|
|
184
|
-
const originalListeners = input.listeners("readable");
|
|
185
|
-
const wasRaw = input.isRaw === true;
|
|
186
|
-
let state = createTuiConnectionFormState(options.initialDraft);
|
|
187
|
-
let settled = false;
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
const redraw = () => output.write(renderTuiConnectionForm(state, output.columns ?? 80));
|
|
190
|
-
const cleanup = () => {
|
|
191
|
-
input.removeListener("readable", onReadable);
|
|
192
|
-
output.removeListener?.("resize", redraw);
|
|
193
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
194
|
-
if (!wasRaw) input.setRawMode?.(false);
|
|
195
|
-
output.write("\x1B[0m\x1B[?25h\x1B[2J\x1B[H");
|
|
196
|
-
for (const listener of originalListeners) input.on("readable", listener);
|
|
197
|
-
requestHostFullRedraw(input, output);
|
|
198
|
-
};
|
|
199
|
-
const finish = async (value, error) => {
|
|
200
|
-
if (settled) return;
|
|
201
|
-
settled = true;
|
|
202
|
-
const draft = connectionFormDraft(state);
|
|
203
|
-
state = clearPassword(state);
|
|
204
|
-
cleanup();
|
|
205
|
-
try {
|
|
206
|
-
await options.persistDraft?.(draft);
|
|
207
|
-
if (error !== void 0) reject(error);
|
|
208
|
-
else resolve(value);
|
|
209
|
-
} catch (persistError) {
|
|
210
|
-
reject(persistError);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const onAbort = () => {
|
|
214
|
-
finish(void 0, options.signal?.reason instanceof Error ? options.signal.reason : /* @__PURE__ */ new Error("数据库连接已取消"));
|
|
215
|
-
};
|
|
216
|
-
const onReadable = () => {
|
|
217
|
-
if (settled) return;
|
|
218
|
-
try {
|
|
219
|
-
let chunk;
|
|
220
|
-
while ((chunk = input.read()) !== null) {
|
|
221
|
-
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
222
|
-
for (const key of decodeTuiFormInput(text)) {
|
|
223
|
-
const transition = updateTuiConnectionForm(state, key);
|
|
224
|
-
state = transition.state;
|
|
225
|
-
if (transition.kind === "submitted") {
|
|
226
|
-
finish(transition.input);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (transition.kind === "cancelled") {
|
|
230
|
-
finish(void 0);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
redraw();
|
|
236
|
-
} catch (error) {
|
|
237
|
-
finish(void 0, error);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
try {
|
|
241
|
-
for (const listener of originalListeners) input.removeListener("readable", listener);
|
|
242
|
-
input.setRawMode?.(true);
|
|
243
|
-
input.ref?.();
|
|
244
|
-
input.on("readable", onReadable);
|
|
245
|
-
output.on?.("resize", redraw);
|
|
246
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
247
|
-
if (options.signal?.aborted === true) onAbort();
|
|
248
|
-
else redraw();
|
|
249
|
-
} catch (error) {
|
|
250
|
-
settled = true;
|
|
251
|
-
state = clearPassword(state);
|
|
252
|
-
cleanup();
|
|
253
|
-
reject(error);
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Ask dsh-tui to invalidate Ink's cached frame after our direct ANSI drawing.
|
|
259
|
-
*
|
|
260
|
-
* A same-size `resize` event does not invalidate Ink's physical-frame cache,
|
|
261
|
-
* so unchanged rows such as the prompt remain blank after the form clears the
|
|
262
|
-
* screen. Ctrl+L is dsh-tui's documented redraw shortcut and reaches the host
|
|
263
|
-
* only after its original readable listener has been restored.
|
|
264
|
-
*/
|
|
265
|
-
function requestHostFullRedraw(input, output) {
|
|
266
|
-
try {
|
|
267
|
-
if (input.push !== void 0) {
|
|
268
|
-
input.push("\f");
|
|
269
|
-
input.emit?.("readable");
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
} catch {}
|
|
273
|
-
output.emit?.("resize");
|
|
274
|
-
}
|
|
275
|
-
/** Decode the keyboard subset owned by the form; unknown terminal reports are ignored. */
|
|
276
|
-
function decodeTuiFormInput(value) {
|
|
277
|
-
const keys = [];
|
|
278
|
-
let index = 0;
|
|
279
|
-
while (index < value.length) {
|
|
280
|
-
const rest = value.slice(index);
|
|
281
|
-
const known = KNOWN_SEQUENCES.find(([sequence]) => rest.startsWith(sequence));
|
|
282
|
-
if (known !== void 0) {
|
|
283
|
-
keys.push({ name: known[1] });
|
|
284
|
-
index += known[0].length;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const character = value[index];
|
|
288
|
-
if (character === "" || character === "\x1B") {
|
|
289
|
-
if (character === "\x1B" && value[index + 1] === "[") {
|
|
290
|
-
index += 2;
|
|
291
|
-
while (index < value.length && !/[\x40-\x7E]/.test(value[index])) index += 1;
|
|
292
|
-
index += 1;
|
|
293
|
-
} else {
|
|
294
|
-
keys.push({ name: "escape" });
|
|
295
|
-
index += 1;
|
|
296
|
-
}
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (character === " ") keys.push({ name: "tab" });
|
|
300
|
-
else if (character === "\r" || character === "\n") keys.push({ name: "enter" });
|
|
301
|
-
else if (character === "" || character === "\b") keys.push({ name: "backspace" });
|
|
302
|
-
else if (character === " ") keys.push({ name: "space" });
|
|
303
|
-
else if (character >= " ") keys.push({
|
|
304
|
-
name: "text",
|
|
305
|
-
text: character
|
|
306
|
-
});
|
|
307
|
-
index += 1;
|
|
308
|
-
}
|
|
309
|
-
return keys;
|
|
310
|
-
}
|
|
311
|
-
const KNOWN_SEQUENCES = [
|
|
312
|
-
["\x1B[Z", "backtab"],
|
|
313
|
-
["\x1B[A", "up"],
|
|
314
|
-
["\x1B[B", "down"],
|
|
315
|
-
["\x1B[C", "right"],
|
|
316
|
-
["\x1B[D", "left"],
|
|
317
|
-
["\x1B[H", "home"],
|
|
318
|
-
["\x1B[F", "end"],
|
|
319
|
-
["\x1B[1~", "home"],
|
|
320
|
-
["\x1B[4~", "end"],
|
|
321
|
-
["\x1B[3~", "delete"]
|
|
322
|
-
];
|
|
323
|
-
function validateTuiConnectionForm(state) {
|
|
324
|
-
const database = state.database.trim();
|
|
325
|
-
if (database === "") return { error: state.type === "sqlite" ? "SQLite 数据库文件路径不能为空" : "数据库名不能为空" };
|
|
326
|
-
if (state.type === "sqlite") return { input: {
|
|
327
|
-
type: "sqlite",
|
|
328
|
-
database,
|
|
329
|
-
readonly: state.readonly
|
|
330
|
-
} };
|
|
331
|
-
const portText = state.port.trim();
|
|
332
|
-
const port = portText === "" ? defaultDatabasePort(state.type, state.secure) : Number(portText);
|
|
333
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return { error: "端口必须是 1–65535 的整数,或留空使用默认值" };
|
|
334
|
-
const input = {
|
|
335
|
-
type: state.type,
|
|
336
|
-
host: state.host.trim() || "127.0.0.1",
|
|
337
|
-
port,
|
|
338
|
-
database,
|
|
339
|
-
readonly: state.readonly
|
|
340
|
-
};
|
|
341
|
-
if (state.type === "clickhouse") input.secure = state.secure;
|
|
342
|
-
const user = state.user.trim();
|
|
343
|
-
if (user !== "") input.user = user;
|
|
344
|
-
const passwordRef = state.passwordRef.trim();
|
|
345
|
-
if (state.password !== "" && passwordRef !== "") return { error: "临时密码与凭据引用不能同时填写" };
|
|
346
|
-
if (passwordRef !== "") {
|
|
347
|
-
try {
|
|
348
|
-
validatePasswordRef(passwordRef);
|
|
349
|
-
} catch (error) {
|
|
350
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
351
|
-
}
|
|
352
|
-
input.passwordRef = passwordRef;
|
|
353
|
-
} else if (state.password !== "") input.password = state.password;
|
|
354
|
-
return { input };
|
|
355
|
-
}
|
|
356
|
-
function editTextField(state, key) {
|
|
357
|
-
if (!isTextField(state.focus)) return state;
|
|
358
|
-
const value = state[state.focus];
|
|
359
|
-
if (key.name === "text") return replaceField(state, value.slice(0, state.cursor) + key.text + value.slice(state.cursor), state.cursor + key.text.length);
|
|
360
|
-
if (key.name === "space") return replaceField(state, value.slice(0, state.cursor) + " " + value.slice(state.cursor), state.cursor + 1);
|
|
361
|
-
if (key.name === "backspace" && state.cursor > 0) return replaceField(state, value.slice(0, state.cursor - 1) + value.slice(state.cursor), state.cursor - 1);
|
|
362
|
-
if (key.name === "delete" && state.cursor < value.length) return replaceField(state, value.slice(0, state.cursor) + value.slice(state.cursor + 1), state.cursor);
|
|
363
|
-
if (key.name === "left") return {
|
|
364
|
-
...state,
|
|
365
|
-
cursor: Math.max(0, state.cursor - 1)
|
|
366
|
-
};
|
|
367
|
-
if (key.name === "right") return {
|
|
368
|
-
...state,
|
|
369
|
-
cursor: Math.min(value.length, state.cursor + 1)
|
|
370
|
-
};
|
|
371
|
-
if (key.name === "home") return {
|
|
372
|
-
...state,
|
|
373
|
-
cursor: 0
|
|
374
|
-
};
|
|
375
|
-
if (key.name === "end") return {
|
|
376
|
-
...state,
|
|
377
|
-
cursor: value.length
|
|
378
|
-
};
|
|
379
|
-
return state;
|
|
380
|
-
}
|
|
381
|
-
function replaceField(state, value, cursor) {
|
|
382
|
-
if (!isTextField(state.focus)) return state;
|
|
383
|
-
return {
|
|
384
|
-
...state,
|
|
385
|
-
[state.focus]: value,
|
|
386
|
-
cursor
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
function isTextField(field) {
|
|
390
|
-
return field === "host" || field === "port" || field === "user" || field === "database" || field === "password" || field === "passwordRef";
|
|
391
|
-
}
|
|
392
|
-
function moveFocus(state, delta) {
|
|
393
|
-
const fields = tuiConnectionFields(state.type);
|
|
394
|
-
const focus = fields[(Math.max(0, fields.indexOf(state.focus)) + delta + fields.length) % fields.length];
|
|
395
|
-
return {
|
|
396
|
-
...state,
|
|
397
|
-
focus,
|
|
398
|
-
cursor: isTextField(focus) ? state[focus].length : 0
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
function clearPassword(state) {
|
|
402
|
-
return {
|
|
403
|
-
...state,
|
|
404
|
-
password: "",
|
|
405
|
-
cursor: state.focus === "password" ? 0 : state.cursor
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
function updateOpenSelector(state, key) {
|
|
409
|
-
const selector = state.selector;
|
|
410
|
-
const optionCount = selector.field === "type" ? TUI_DATABASE_TYPES.length : 2;
|
|
411
|
-
if (key.name === "escape") return {
|
|
412
|
-
kind: "editing",
|
|
413
|
-
state: closeSelector(state)
|
|
414
|
-
};
|
|
415
|
-
if (key.name === "enter") {
|
|
416
|
-
let selected;
|
|
417
|
-
if (selector.field === "type") {
|
|
418
|
-
const type = TUI_DATABASE_TYPES[selector.index];
|
|
419
|
-
const previousDefault = state.type === "sqlite" ? "" : String(defaultDatabasePort(state.type, state.secure));
|
|
420
|
-
const secure = type === "clickhouse" && state.secure;
|
|
421
|
-
const port = state.port === "" || state.port === previousDefault ? type === "sqlite" ? "" : String(defaultDatabasePort(type, secure)) : state.port;
|
|
422
|
-
selected = {
|
|
423
|
-
...state,
|
|
424
|
-
type,
|
|
425
|
-
secure,
|
|
426
|
-
port
|
|
427
|
-
};
|
|
428
|
-
} else if (selector.field === "secure") {
|
|
429
|
-
const secure = selector.index === 1;
|
|
430
|
-
const previousDefault = String(defaultDatabasePort("clickhouse", state.secure));
|
|
431
|
-
const port = state.port === "" || state.port === previousDefault ? String(defaultDatabasePort("clickhouse", secure)) : state.port;
|
|
432
|
-
selected = {
|
|
433
|
-
...state,
|
|
434
|
-
secure,
|
|
435
|
-
port
|
|
436
|
-
};
|
|
437
|
-
} else selected = {
|
|
438
|
-
...state,
|
|
439
|
-
readonly: selector.index === 1
|
|
440
|
-
};
|
|
441
|
-
return {
|
|
442
|
-
kind: "editing",
|
|
443
|
-
state: closeSelector(selected)
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
let delta = 0;
|
|
447
|
-
if (key.name === "up" || key.name === "left") delta = -1;
|
|
448
|
-
if (key.name === "down" || key.name === "right") delta = 1;
|
|
449
|
-
if (delta === 0 && key.name !== "home" && key.name !== "end") return {
|
|
450
|
-
kind: "editing",
|
|
451
|
-
state
|
|
452
|
-
};
|
|
453
|
-
const index = key.name === "home" ? 0 : key.name === "end" ? optionCount - 1 : (selector.index + delta + optionCount) % optionCount;
|
|
454
|
-
return {
|
|
455
|
-
kind: "editing",
|
|
456
|
-
state: {
|
|
457
|
-
...state,
|
|
458
|
-
selector: {
|
|
459
|
-
...selector,
|
|
460
|
-
index
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
function closeSelector(state) {
|
|
466
|
-
const { selector: _selector, ...rest } = state;
|
|
467
|
-
return rest;
|
|
468
|
-
}
|
|
469
|
-
function renderField(state, field, width) {
|
|
470
|
-
const focused = state.focus === field;
|
|
471
|
-
const pointer = focused ? cyan("›") : " ";
|
|
472
|
-
if (field === "confirm" || field === "cancel") {
|
|
473
|
-
const label = field === "confirm" ? "确定并连接" : "取消";
|
|
474
|
-
return [`${pointer} ${focused ? cyan(bold(`[ ${label} ]`)) : `[ ${label} ]`}`];
|
|
475
|
-
}
|
|
476
|
-
const label = fieldLabel(field, state.type);
|
|
477
|
-
let value;
|
|
478
|
-
let placeholder = false;
|
|
479
|
-
if (field === "type") value = databaseTypeLabel(state.type);
|
|
480
|
-
else if (field === "readonly") value = state.readonly ? "是" : "否";
|
|
481
|
-
else if (field === "secure") value = state.secure ? "是" : "否";
|
|
482
|
-
else {
|
|
483
|
-
const raw = state[field];
|
|
484
|
-
if (field === "password") value = "*".repeat([...raw].length);
|
|
485
|
-
else value = raw;
|
|
486
|
-
if (value === "") {
|
|
487
|
-
placeholder = true;
|
|
488
|
-
value = field === "host" ? "127.0.0.1(默认)" : field === "port" ? `${defaultDatabasePort(state.type, state.secure)}(默认)` : field === "password" ? "可留空" : field === "passwordRef" ? "例如 DB_PASSWORD,可留空" : field === "user" ? "可留空" : "请输入";
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const maxValue = Math.max(8, width - 20);
|
|
492
|
-
const shown = truncate(value.replace(/[\r\n\u001B]/g, " "), maxValue);
|
|
493
|
-
const content = placeholder ? dim(shown) : shown;
|
|
494
|
-
const expandable = field === "type" || field === "readonly" || field === "secure";
|
|
495
|
-
const line = `${pointer} ${label.padEnd(8, " ")} [ ${focused ? cyan(content) : content}${expandable ? " ▾" : ""} ]`;
|
|
496
|
-
if (state.selector?.field !== field) return [line];
|
|
497
|
-
return [line, ...renderSelectorOptions(state)];
|
|
498
|
-
}
|
|
499
|
-
function renderSelectorOptions(state) {
|
|
500
|
-
const selector = state.selector;
|
|
501
|
-
const labels = selector.field === "type" ? TUI_DATABASE_TYPES.map(databaseTypeLabel) : ["否", "是"];
|
|
502
|
-
const selectedIndex = selector.field === "type" ? TUI_DATABASE_TYPES.indexOf(state.type) : state[selector.field] ? 1 : 0;
|
|
503
|
-
return labels.map((label, index) => {
|
|
504
|
-
return ` ${index === selector.index ? cyan("›") : " "} ${index === selectedIndex ? cyan("●") : dim("○")} ${index === selector.index ? cyan(bold(label)) : label}`;
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
function databaseTypeLabel(type) {
|
|
508
|
-
return databaseTypeLabel$1(type);
|
|
509
|
-
}
|
|
510
|
-
function fieldLabel(field, type) {
|
|
511
|
-
switch (field) {
|
|
512
|
-
case "type": return "数据库类型";
|
|
513
|
-
case "host": return "数据库主机";
|
|
514
|
-
case "port": return "数据库端口";
|
|
515
|
-
case "user": return "数据库用户";
|
|
516
|
-
case "database": return type === "sqlite" ? "文件路径" : "数据库名";
|
|
517
|
-
case "password": return "密码";
|
|
518
|
-
case "passwordRef": return "凭据引用";
|
|
519
|
-
case "secure": return "HTTPS";
|
|
520
|
-
case "readonly": return "只读模式";
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function truncate(value, width) {
|
|
524
|
-
const characters = [...value];
|
|
525
|
-
return characters.length <= width ? value : `…${characters.slice(-(width - 1)).join("")}`;
|
|
526
|
-
}
|
|
527
|
-
function bold(value) {
|
|
528
|
-
return `\u001B[1m${value}\u001B[22m`;
|
|
529
|
-
}
|
|
530
|
-
function dim(value) {
|
|
531
|
-
return `\u001B[2m${value}\u001B[22m`;
|
|
532
|
-
}
|
|
533
|
-
function cyan(value) {
|
|
534
|
-
return `\u001B[36m${value}\u001B[39m`;
|
|
535
|
-
}
|
|
536
|
-
function red(value) {
|
|
537
|
-
return `\u001B[31m${value}\u001B[39m`;
|
|
538
|
-
}
|
|
539
|
-
//#endregion
|
|
540
|
-
//#region src/command.ts
|
|
541
|
-
const name = "data-agent-database-command";
|
|
542
|
-
const inject = [
|
|
543
|
-
"commands",
|
|
544
|
-
"dataAgentConnections",
|
|
545
|
-
"tools"
|
|
546
|
-
];
|
|
547
|
-
const DATABASE_COMMAND_USAGE = [
|
|
548
|
-
"用法:",
|
|
549
|
-
" /database status",
|
|
550
|
-
` /database connect --type <${DATABASE_TYPES.join("|")}> --database <name|path> [--host <host>] [--port <port>] [--user <user>] [--password-ref <REF>] [--readonly] [--secure]`,
|
|
551
|
-
" /database test",
|
|
552
|
-
" /database disconnect",
|
|
553
|
-
"安全提示:TUI 无参数 connect 可输入掩码临时密码;命令参数不接受 --password,请使用 --password-ref。"
|
|
554
|
-
].join("\n");
|
|
555
|
-
const DATA_AGENT_TOOL_NAMES = [
|
|
556
|
-
"str_replace_editor",
|
|
557
|
-
"sql-query",
|
|
558
|
-
"sql-write",
|
|
559
|
-
"sql-cmd"
|
|
560
|
-
];
|
|
561
|
-
const DATA_AGENT_OWN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
562
|
-
...DATA_AGENT_TOOL_NAMES,
|
|
563
|
-
"render-analysis",
|
|
564
|
-
RUN_CODE_NAME
|
|
565
|
-
]);
|
|
566
|
-
const defaultInteraction = {
|
|
567
|
-
isTuiFormAvailable: () => isDshTuiTerminal(),
|
|
568
|
-
collectTuiConnection: (signal, options) => runTuiConnectionForm({
|
|
569
|
-
signal,
|
|
570
|
-
...options.initialDraft !== void 0 ? { initialDraft: options.initialDraft } : {},
|
|
571
|
-
persistDraft: options.persistDraft
|
|
572
|
-
})
|
|
573
|
-
};
|
|
574
|
-
/** Register the command in the calling preset/agent scope. */
|
|
575
|
-
function apply(ctx) {
|
|
576
|
-
const inheritedToolNames = ctx.tools.schemas().map((schema) => schema.name).filter((toolName) => !DATA_AGENT_OWN_TOOL_NAMES.has(toolName));
|
|
577
|
-
ctx.tools.restrict({ deny: inheritedToolNames });
|
|
578
|
-
ctx.commands.register({
|
|
579
|
-
name: "database",
|
|
580
|
-
description: "查看、连接、测试或断开 data-agent 数据库连接",
|
|
581
|
-
input: { hint: "status | connect | test | disconnect" },
|
|
582
|
-
recordInput: false,
|
|
583
|
-
handler: async (invocation) => executeDatabaseCommand(ctx, invocation)
|
|
584
|
-
});
|
|
585
|
-
const refreshTimer = setTimeout(() => ctx.emit("commands/change"), 0);
|
|
586
|
-
ctx.effect(() => () => clearTimeout(refreshTimer), "data-agent: refresh scoped command adapters");
|
|
587
|
-
}
|
|
588
|
-
/** Public for focused command tests and alternate command adapters. */
|
|
589
|
-
async function executeDatabaseCommand(ctx, invocation, interaction = defaultInteraction) {
|
|
590
|
-
let transientPassword;
|
|
591
|
-
try {
|
|
592
|
-
const action = parseDatabaseAction(invocation.rawInput);
|
|
593
|
-
const sessionId = String(invocation.agent.id);
|
|
594
|
-
switch (action.kind) {
|
|
595
|
-
case "status": {
|
|
596
|
-
const summary = await ctx.dataAgentConnections.status(sessionId);
|
|
597
|
-
const tools = ctx.tools.schemas(invocation.agent).map((schema) => schema.name).sort();
|
|
598
|
-
return {
|
|
599
|
-
kind: "success",
|
|
600
|
-
text: `${formatConnectionStatus(summary)}\n模型工具:${tools.join(", ") || "(无)"}\n\n${DATABASE_COMMAND_USAGE}`
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
case "connect": {
|
|
604
|
-
const input = action.input ?? await askForConnection(ctx, invocation, interaction);
|
|
605
|
-
if (input === void 0) return {
|
|
606
|
-
kind: "error",
|
|
607
|
-
text: `当前界面没有可用的问答 provider。\n\n${DATABASE_COMMAND_USAGE}`
|
|
608
|
-
};
|
|
609
|
-
transientPassword = input.password;
|
|
610
|
-
return {
|
|
611
|
-
kind: "success",
|
|
612
|
-
text: `数据库连接成功。\n${formatConnectionStatus((await ctx.dataAgentConnections.connect(sessionId, input, invocation.signal)).summary)}`
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
case "test": {
|
|
616
|
-
const result = await ctx.dataAgentConnections.test(sessionId, invocation.signal);
|
|
617
|
-
return {
|
|
618
|
-
kind: "success",
|
|
619
|
-
text: `数据库连接测试成功,发现 ${result.tables.length} 张表。\n${formatConnectionStatus(result.summary)}`
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
case "disconnect":
|
|
623
|
-
await ctx.dataAgentConnections.disconnect(sessionId);
|
|
624
|
-
return {
|
|
625
|
-
kind: "success",
|
|
626
|
-
text: "当前会话已断开数据库连接;可复用的非敏感 connection profile 已保留。"
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
} catch (error) {
|
|
630
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
631
|
-
return {
|
|
632
|
-
kind: "error",
|
|
633
|
-
text: redactSecretText(message, [transientPassword])
|
|
634
|
-
};
|
|
635
|
-
} finally {
|
|
636
|
-
transientPassword = void 0;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
/** Parse one command's raw input without ever accepting a plaintext password. */
|
|
640
|
-
function parseDatabaseAction(rawInput) {
|
|
641
|
-
const tokens = splitCommandLine(rawInput.trim());
|
|
642
|
-
if (tokens.length === 0 || tokens[0] === "status") {
|
|
643
|
-
if (tokens.length > 1) throw new Error(`status 不接受额外参数。\n\n${DATABASE_COMMAND_USAGE}`);
|
|
644
|
-
return { kind: "status" };
|
|
645
|
-
}
|
|
646
|
-
const subcommand = tokens[0];
|
|
647
|
-
if (subcommand === "test" || subcommand === "disconnect") {
|
|
648
|
-
if (tokens.length > 1) throw new Error(`${subcommand} 不接受额外参数。\n\n${DATABASE_COMMAND_USAGE}`);
|
|
649
|
-
return { kind: subcommand };
|
|
650
|
-
}
|
|
651
|
-
if (subcommand !== "connect") throw new Error(`未知 database 子命令:${subcommand}\n\n${DATABASE_COMMAND_USAGE}`);
|
|
652
|
-
if (tokens.length === 1) return { kind: "connect" };
|
|
653
|
-
return {
|
|
654
|
-
kind: "connect",
|
|
655
|
-
input: parseConnectArguments(tokens.slice(1))
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
/** Non-interactive `connect` argument grammar. */
|
|
659
|
-
function parseConnectArguments(tokens) {
|
|
660
|
-
const values = /* @__PURE__ */ new Map();
|
|
661
|
-
let readonly;
|
|
662
|
-
let secure;
|
|
663
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
664
|
-
const token = tokens[index];
|
|
665
|
-
if (token === "--password" || token.startsWith("--password=") || token.startsWith("password=")) throw new Error("安全限制:/database 不接受明文密码参数;请改用 --password-ref <REF>。");
|
|
666
|
-
if (token === "--readonly") {
|
|
667
|
-
readonly = true;
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
if (token === "--readwrite") {
|
|
671
|
-
readonly = false;
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
if (token === "--secure") {
|
|
675
|
-
secure = true;
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
if (token === "--insecure") {
|
|
679
|
-
secure = false;
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
const assignment = token.startsWith("--") ? token.slice(2).split("=", 2) : token.split("=", 2);
|
|
683
|
-
let key;
|
|
684
|
-
let value;
|
|
685
|
-
if (assignment.length === 2) {
|
|
686
|
-
key = normalizeArgumentName(assignment[0]);
|
|
687
|
-
value = assignment[1];
|
|
688
|
-
} else {
|
|
689
|
-
if (!token.startsWith("--")) throw new Error(`无法解析参数:${token}\n\n${DATABASE_COMMAND_USAGE}`);
|
|
690
|
-
key = normalizeArgumentName(token.slice(2));
|
|
691
|
-
const next = tokens[index + 1];
|
|
692
|
-
if (next === void 0 || next.startsWith("--")) throw new Error(`参数 --${key} 缺少值`);
|
|
693
|
-
value = next;
|
|
694
|
-
index += 1;
|
|
695
|
-
}
|
|
696
|
-
if (key === "password") throw new Error("安全限制:/database 不接受明文密码参数;请改用 --password-ref <REF>。");
|
|
697
|
-
if (!CONNECT_ARGUMENTS.has(key)) throw new Error(`未知连接参数:--${key}\n\n${DATABASE_COMMAND_USAGE}`);
|
|
698
|
-
values.set(key, value);
|
|
699
|
-
}
|
|
700
|
-
const type = values.get("type");
|
|
701
|
-
if (!isDatabaseType(type)) throw new Error("connect 必须提供有效的 --type");
|
|
702
|
-
const database = values.get("database");
|
|
703
|
-
if (database === void 0 || database.length === 0) throw new Error("connect 必须提供 --database");
|
|
704
|
-
const input = {
|
|
705
|
-
type,
|
|
706
|
-
database
|
|
707
|
-
};
|
|
708
|
-
copyNonEmpty(values, "host", (value) => {
|
|
709
|
-
input.host = value;
|
|
710
|
-
});
|
|
711
|
-
copyNonEmpty(values, "user", (value) => {
|
|
712
|
-
input.user = value;
|
|
713
|
-
});
|
|
714
|
-
copyNonEmpty(values, "passwordRef", (value) => {
|
|
715
|
-
input.passwordRef = value;
|
|
716
|
-
});
|
|
717
|
-
copyNonEmpty(values, "profileId", (value) => {
|
|
718
|
-
input.profileId = value;
|
|
719
|
-
});
|
|
720
|
-
copyNonEmpty(values, "name", (value) => {
|
|
721
|
-
input.name = value;
|
|
722
|
-
});
|
|
723
|
-
const port = values.get("port");
|
|
724
|
-
if (port !== void 0) {
|
|
725
|
-
const number = Number(port);
|
|
726
|
-
if (!Number.isInteger(number) || number < 1 || number > 65535) throw new Error("--port 必须是 1-65535 的整数");
|
|
727
|
-
input.port = number;
|
|
728
|
-
}
|
|
729
|
-
if (readonly !== void 0) input.readonly = readonly;
|
|
730
|
-
if (type === "clickhouse" && secure !== void 0) input.secure = secure;
|
|
731
|
-
return input;
|
|
732
|
-
}
|
|
733
|
-
/** Render a public summary; no password-bearing field exists in the type. */
|
|
734
|
-
function formatConnectionStatus(summary) {
|
|
735
|
-
if (summary === void 0) return "数据库状态:未连接。";
|
|
736
|
-
const endpoint = summary.type === "sqlite" ? summary.database : `${summary.host ?? "localhost"}${summary.port !== void 0 ? `:${summary.port}` : ""}`;
|
|
737
|
-
const lines = [
|
|
738
|
-
summary.reconnectRequired === true ? "数据库状态:需要重新认证" : "数据库状态:已连接",
|
|
739
|
-
`类型:${summary.type}`,
|
|
740
|
-
`地址:${endpoint}`,
|
|
741
|
-
`数据库:${summary.database}`,
|
|
742
|
-
`只读:${summary.readonly === true ? "是" : "否"}`
|
|
743
|
-
];
|
|
744
|
-
if (summary.type === "clickhouse") lines.push(`HTTPS:${summary.secure === true ? "是" : "否"}`);
|
|
745
|
-
if (summary.user !== void 0) lines.push(`用户:${summary.user}`);
|
|
746
|
-
if (summary.profileId !== void 0) lines.push(`Profile:${summary.name ?? summary.profileId}`);
|
|
747
|
-
if (summary.passwordRef !== void 0) lines.push(`凭据引用:${summary.passwordRef}`);
|
|
748
|
-
if (summary.credential !== void 0) lines.push(`凭据:${summary.credential.configured ? `已配置${summary.credential.source !== void 0 ? `(${summary.credential.source})` : ""}` : "未配置"}`);
|
|
749
|
-
if (summary.tables !== void 0) lines.push(`表:${summary.tables.length} 张`);
|
|
750
|
-
return lines.join("\n");
|
|
751
|
-
}
|
|
752
|
-
async function askForConnection(ctx, invocation, interaction) {
|
|
753
|
-
if (interaction.isTuiFormAvailable()) {
|
|
754
|
-
const sessionId = String(invocation.agent.id);
|
|
755
|
-
const initialDraft = ctx.dataAgentConnections.getFormDraft?.(sessionId);
|
|
756
|
-
const input = await interaction.collectTuiConnection(invocation.signal, {
|
|
757
|
-
...initialDraft !== void 0 ? { initialDraft } : {},
|
|
758
|
-
persistDraft: async (draft) => {
|
|
759
|
-
await ctx.dataAgentConnections.saveFormDraft?.(sessionId, draft);
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
if (input === void 0) throw new Error("已取消数据库连接。");
|
|
763
|
-
return input;
|
|
764
|
-
}
|
|
765
|
-
const questions = ctx.get("userQuestions");
|
|
766
|
-
if (questions === void 0) return void 0;
|
|
767
|
-
try {
|
|
768
|
-
const typeValue = answerValue(await questions.ask({
|
|
769
|
-
agent: invocation.agent,
|
|
770
|
-
signal: invocation.signal,
|
|
771
|
-
questions: [{
|
|
772
|
-
id: "type",
|
|
773
|
-
header: "数据库类型",
|
|
774
|
-
question: "选择要连接的数据库类型",
|
|
775
|
-
options: DATABASE_TYPES.map((label) => ({ label }))
|
|
776
|
-
}]
|
|
777
|
-
}), "type");
|
|
778
|
-
if (!isDatabaseType(typeValue)) throw new Error("未选择有效的数据库类型");
|
|
779
|
-
const detailQuestions = typeValue === "sqlite" ? [{
|
|
780
|
-
id: "database",
|
|
781
|
-
header: "文件路径",
|
|
782
|
-
question: "SQLite 数据库文件路径"
|
|
783
|
-
}, {
|
|
784
|
-
id: "readonly",
|
|
785
|
-
header: "只读",
|
|
786
|
-
question: "是否启用只读模式?",
|
|
787
|
-
options: [{ label: "是" }, { label: "否" }]
|
|
788
|
-
}] : [
|
|
789
|
-
{
|
|
790
|
-
id: "host",
|
|
791
|
-
header: "主机",
|
|
792
|
-
question: "数据库主机(留空使用 127.0.0.1)"
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
id: "port",
|
|
796
|
-
header: "端口",
|
|
797
|
-
question: typeValue === "clickhouse" ? `数据库端口(留空使用HTTP ${defaultDatabasePort$1("clickhouse")};HTTPS ${defaultDatabasePort$1("clickhouse", true)})` : `数据库端口(留空使用 ${defaultDatabasePort$1(typeValue)})`
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
id: "user",
|
|
801
|
-
header: "用户",
|
|
802
|
-
question: "数据库用户名"
|
|
803
|
-
},
|
|
804
|
-
{
|
|
805
|
-
id: "database",
|
|
806
|
-
header: "数据库",
|
|
807
|
-
question: "数据库名 / Oracle 服务名"
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
id: "passwordRef",
|
|
811
|
-
header: "凭据引用",
|
|
812
|
-
question: "DSH credential reference(可留空)"
|
|
813
|
-
},
|
|
814
|
-
...typeValue === "clickhouse" ? [{
|
|
815
|
-
id: "secure",
|
|
816
|
-
header: "HTTPS",
|
|
817
|
-
question: "是否使用HTTPS并验证服务器证书?",
|
|
818
|
-
options: [{ label: "是" }, { label: "否" }]
|
|
819
|
-
}] : [],
|
|
820
|
-
{
|
|
821
|
-
id: "readonly",
|
|
822
|
-
header: "只读",
|
|
823
|
-
question: "是否启用只读模式?",
|
|
824
|
-
options: [{ label: "是" }, { label: "否" }]
|
|
825
|
-
}
|
|
826
|
-
];
|
|
827
|
-
const details = await questions.ask({
|
|
828
|
-
agent: invocation.agent,
|
|
829
|
-
signal: invocation.signal,
|
|
830
|
-
questions: detailQuestions
|
|
831
|
-
});
|
|
832
|
-
const database = answerValue(details, "database")?.trim();
|
|
833
|
-
if (database === void 0 || database.length === 0) throw new Error("database 不能为空");
|
|
834
|
-
const input = {
|
|
835
|
-
type: typeValue,
|
|
836
|
-
database,
|
|
837
|
-
readonly: answerValue(details, "readonly") === "是"
|
|
838
|
-
};
|
|
839
|
-
if (typeValue !== "sqlite") {
|
|
840
|
-
input.host = answerValue(details, "host")?.trim() || "127.0.0.1";
|
|
841
|
-
if (typeValue === "clickhouse") input.secure = answerValue(details, "secure") === "是";
|
|
842
|
-
const portText = answerValue(details, "port")?.trim();
|
|
843
|
-
input.port = portText === void 0 || portText === "" ? defaultDatabasePort$1(typeValue, input.secure === true) : Number(portText);
|
|
844
|
-
const user = answerValue(details, "user")?.trim();
|
|
845
|
-
if (user !== void 0 && user !== "") input.user = user;
|
|
846
|
-
const passwordRef = answerValue(details, "passwordRef")?.trim();
|
|
847
|
-
if (passwordRef !== void 0 && passwordRef !== "") input.passwordRef = passwordRef;
|
|
848
|
-
}
|
|
849
|
-
return input;
|
|
850
|
-
} catch (error) {
|
|
851
|
-
if (error.code === "NO_PROVIDER") return void 0;
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
const CONNECT_ARGUMENTS = /* @__PURE__ */ new Set([
|
|
856
|
-
"type",
|
|
857
|
-
"host",
|
|
858
|
-
"port",
|
|
859
|
-
"user",
|
|
860
|
-
"database",
|
|
861
|
-
"passwordRef",
|
|
862
|
-
"profileId",
|
|
863
|
-
"name"
|
|
864
|
-
]);
|
|
865
|
-
function normalizeArgumentName(value) {
|
|
866
|
-
return value.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
867
|
-
}
|
|
868
|
-
function copyNonEmpty(values, key, apply) {
|
|
869
|
-
const value = values.get(key);
|
|
870
|
-
if (value !== void 0 && value.length > 0) apply(value);
|
|
871
|
-
}
|
|
872
|
-
function answerValue(answer, id) {
|
|
873
|
-
const item = answer.answers.find((candidate) => candidate.id === id);
|
|
874
|
-
return item?.custom ?? item?.selected[0];
|
|
875
|
-
}
|
|
876
|
-
/** Minimal shell-like splitter for quoted command arguments; no expansion. */
|
|
877
|
-
function splitCommandLine(value) {
|
|
878
|
-
const tokens = [];
|
|
879
|
-
let token = "";
|
|
880
|
-
let quote;
|
|
881
|
-
let escaping = false;
|
|
882
|
-
for (const character of value) {
|
|
883
|
-
if (escaping) {
|
|
884
|
-
token += character;
|
|
885
|
-
escaping = false;
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
if (character === "\\" && quote !== "'") {
|
|
889
|
-
escaping = true;
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
if (quote !== void 0) {
|
|
893
|
-
if (character === quote) quote = void 0;
|
|
894
|
-
else token += character;
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
if (character === "\"" || character === "'") {
|
|
898
|
-
quote = character;
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
if (/\s/.test(character)) {
|
|
902
|
-
if (token.length > 0) {
|
|
903
|
-
tokens.push(token);
|
|
904
|
-
token = "";
|
|
905
|
-
}
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
token += character;
|
|
909
|
-
}
|
|
910
|
-
if (escaping) token += "\\";
|
|
911
|
-
if (quote !== void 0) throw new Error("命令参数包含未闭合的引号");
|
|
912
|
-
if (token.length > 0) tokens.push(token);
|
|
913
|
-
return tokens;
|
|
914
|
-
}
|
|
915
|
-
//#endregion
|
|
916
|
-
export { formatConnectionStatus as a, parseConnectArguments as c, executeDatabaseCommand as i, parseDatabaseAction as l, DATA_AGENT_TOOL_NAMES as n, inject as o, apply as r, name as s, DATABASE_COMMAND_USAGE as t };
|