feat(proxy): 增加数据库和 AI 代理支持

This commit is contained in:
t8y2
2026-05-11 15:05:25 +08:00
parent 1d5a2b2220
commit 79277c660f
20 changed files with 712 additions and 19 deletions
+89 -3
View File
@@ -22,7 +22,7 @@ type DbOption = { value: string; label: string };
type DbCategory = { key: string; title: string; options: DbOption[] };
type DialogStep = "select" | "config";
type DbPickerView = "icon" | "list";
type ConfigTab = "connection" | "ssh";
type ConfigTab = "connection" | "ssh" | "proxy";
const { t } = useI18n();
const { toast } = useToast();
@@ -67,6 +67,12 @@ const defaultForm = (): Omit<ConnectionConfig, "id"> => ({
ssh_key_passphrase: "",
ssh_expose_lan: false,
ssh_connect_timeout_secs: 5,
proxy_enabled: false,
proxy_type: "socks5",
proxy_host: "",
proxy_port: 1080,
proxy_username: "",
proxy_password: "",
ssl: false,
connection_string: undefined,
jdbc_driver_class: undefined,
@@ -264,6 +270,12 @@ watch(
ssh_key_passphrase: config.ssh_key_passphrase || "",
ssh_expose_lan: config.ssh_expose_lan || false,
ssh_connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
proxy_enabled: config.proxy_enabled || false,
proxy_type: config.proxy_type || "socks5",
proxy_host: config.proxy_host || "",
proxy_port: config.proxy_port || 1080,
proxy_username: config.proxy_username || "",
proxy_password: config.proxy_password || "",
ssl: config.ssl || false,
connection_string: config.connection_string,
jdbc_driver_class: config.jdbc_driver_class,
@@ -408,6 +420,9 @@ const hasDbPickerResults = computed(() => filteredDbCategories.value.some((categ
const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value);
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
const canUseSsh = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "jdbc");
const canUseProxy = computed(
() => form.value.db_type !== "sqlite" && form.value.db_type !== "duckdb" && form.value.db_type !== "jdbc",
);
const testResultMessage = computed(() => {
if (!testResult.value) return "";
return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message;
@@ -456,6 +471,8 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
const config: ConnectionConfig = { ...form.value, id };
const sshTimeout = Number(config.ssh_connect_timeout_secs);
config.ssh_connect_timeout_secs = Number.isFinite(sshTimeout) && sshTimeout > 0 ? sshTimeout : 5;
const proxyPort = Number(config.proxy_port);
config.proxy_port = Number.isFinite(proxyPort) && proxyPort > 0 ? proxyPort : 1080;
if (config.db_type === "mongodb" && !mongoUseUrl.value) {
config.connection_string = undefined;
}
@@ -534,6 +551,12 @@ watch(canUseSsh, (value) => {
}
});
watch(canUseProxy, (value) => {
if (!value && configTab.value === "proxy") {
configTab.value = "connection";
}
});
async function save() {
if (isSaving.value) return;
isSaving.value = true;
@@ -778,10 +801,11 @@ function openExternalUrl(url: string) {
<template v-else>
<div class="space-y-3">
<Tabs v-model="configTab" class="min-h-0">
<div v-if="canUseSsh" class="flex items-center justify-between border-b pb-2">
<div v-if="canUseSsh || canUseProxy" class="flex items-center justify-between border-b pb-2">
<TabsList>
<TabsTrigger value="connection">{{ t("connection.basicTab") }}</TabsTrigger>
<TabsTrigger value="ssh">{{ t("connection.sshTunnel") }}</TabsTrigger>
<TabsTrigger v-if="canUseSsh" value="ssh">{{ t("connection.sshTunnel") }}</TabsTrigger>
<TabsTrigger v-if="canUseProxy" value="proxy">{{ t("connection.proxy") }}</TabsTrigger>
</TabsList>
</div>
@@ -1202,6 +1226,68 @@ function openExternalUrl(url: string) {
</div>
</div>
</TabsContent>
<TabsContent v-if="canUseProxy" value="proxy" class="m-0">
<div class="grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxy") }}</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.proxy_enabled" class="mr-0" />
<span class="text-xs text-muted-foreground">{{ t("connection.proxyEnable") }}</span>
</label>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxyType") }}</Label>
<Select
:model-value="form.proxy_type || 'socks5'"
:disabled="!form.proxy_enabled"
@update:model-value="(value: any) => (form.proxy_type = value)"
>
<SelectTrigger class="col-span-3 h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="socks5">SOCKS5</SelectItem>
<SelectItem value="http">HTTP CONNECT</SelectItem>
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxyHost") }}</Label>
<Input
v-model="form.proxy_host"
class="col-span-2"
placeholder="127.0.0.1"
:disabled="!form.proxy_enabled"
/>
<Input
v-model.number="form.proxy_port"
type="number"
class="col-span-1"
:disabled="!form.proxy_enabled"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxyUsername") }}</Label>
<Input
v-model="form.proxy_username"
class="col-span-3"
:placeholder="t('connection.proxyUsernamePlaceholder')"
:disabled="!form.proxy_enabled"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.proxyPassword") }}</Label>
<Input
v-model="form.proxy_password"
type="password"
class="col-span-3"
:placeholder="t('connection.proxyPasswordPlaceholder')"
:disabled="!form.proxy_enabled"
/>
</div>
</div>
</TabsContent>
</Tabs>
</div>
+25
View File
@@ -173,6 +173,8 @@ const tempApiKey = ref(settings.aiConfig.apiKey);
const tempEndpoint = ref(settings.aiConfig.endpoint);
const tempModel = ref(settings.aiConfig.model);
const tempApiStyle = ref<AiApiStyle>(settings.aiConfig.apiStyle || "completions");
const tempProxyEnabled = ref(!!settings.aiConfig.proxyEnabled);
const tempProxyUrl = ref(settings.aiConfig.proxyUrl || "");
const providerDefaults: Record<AiProvider, { endpoint: string; model: string }> = {
claude: { endpoint: "https://api.anthropic.com/v1/messages", model: "claude-sonnet-4-20250514" },
@@ -213,6 +215,8 @@ function openSettings() {
tempEndpoint.value = settings.aiConfig.endpoint;
tempModel.value = settings.aiConfig.model;
tempApiStyle.value = settings.aiConfig.apiStyle || "completions";
tempProxyEnabled.value = !!settings.aiConfig.proxyEnabled;
tempProxyUrl.value = settings.aiConfig.proxyUrl || "";
showSettings.value = true;
}
@@ -223,6 +227,8 @@ function saveSettings() {
endpoint: tempEndpoint.value,
model: tempModel.value,
apiStyle: tempApiStyle.value,
proxyEnabled: tempProxyEnabled.value,
proxyUrl: tempProxyUrl.value,
});
showSettings.value = false;
}
@@ -242,6 +248,8 @@ async function testAiConnection() {
endpoint: tempEndpoint.value,
model: tempModel.value,
apiStyle: tempApiStyle.value,
proxyEnabled: tempProxyEnabled.value,
proxyUrl: tempProxyUrl.value,
});
testResult.value = "success";
} catch (e: any) {
@@ -799,6 +807,23 @@ function formatInlineText(text: string): string {
>
</div>
</div>
<div class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">{{ t("ai.proxy") }}</Label>
<label class="col-span-2 flex items-center gap-2 text-xs text-muted-foreground">
<input v-model="tempProxyEnabled" type="checkbox" class="h-4 w-4 shrink-0 accent-primary" />
{{ t("ai.proxyEnable") }}
</label>
</div>
<div class="grid grid-cols-3 items-center gap-3">
<Label class="text-right text-xs">{{ t("ai.proxyUrl") }}</Label>
<Input
v-model="tempProxyUrl"
autocomplete="off"
class="col-span-2 h-8 text-xs"
placeholder="socks5://127.0.0.1:7890"
:disabled="!tempProxyEnabled"
/>
</div>
</div>
<DialogFooter class="flex items-center gap-2">
<div class="flex-1 flex items-center gap-2">
+11
View File
@@ -143,6 +143,14 @@ export default {
sshKeyPathBrowse: "Browse",
sshExposeLan: "Expose tunnel to LAN",
sshConnectTimeout: "SSH Timeout (seconds)",
proxy: "Proxy",
proxyEnable: "Connect database through proxy",
proxyType: "Proxy Type",
proxyHost: "Proxy Host",
proxyUsername: "Proxy User",
proxyUsernamePlaceholder: "Optional",
proxyPassword: "Proxy Password",
proxyPasswordPlaceholder: "Optional",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "Driver Class (optional)",
@@ -467,6 +475,9 @@ export default {
truncated: "Context truncated",
contextSummary: "{database} · {tables} tables",
autoSqlBlocked: "The AI-generated SQL looked too risky to auto-execute. Review it manually before running.",
proxy: "Proxy",
proxyEnable: "Send AI requests through proxy",
proxyUrl: "Proxy URL",
settingsHint:
"The config is stored in the local app data directory. Requests are sent by the Tauri backend instead of directly from the frontend.",
actions: {
+11
View File
@@ -143,6 +143,14 @@ export default {
sshKeyPathBrowse: "Examinar",
sshExposeLan: "Exponer túnel a la red local",
sshConnectTimeout: "Tiempo de espera SSH (segundos)",
proxy: "Proxy",
proxyEnable: "Conectar la base de datos mediante proxy",
proxyType: "Tipo de proxy",
proxyHost: "Host del proxy",
proxyUsername: "Usuario del proxy",
proxyUsernamePlaceholder: "Opcional",
proxyPassword: "Contraseña del proxy",
proxyPasswordPlaceholder: "Opcional",
jdbcUrl: "URL JDBC",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/base_de_datos",
jdbcDriverClass: "Clase del driver (opcional)",
@@ -474,6 +482,9 @@ export default {
contextSummary: "{database} · {tables} tablas",
autoSqlBlocked:
"El SQL generado por la IA parecía demasiado riesgoso para ejecutarse automáticamente. Revísalo manualmente antes de ejecutar.",
proxy: "Proxy",
proxyEnable: "Enviar solicitudes de IA mediante proxy",
proxyUrl: "URL del proxy",
settingsHint:
"La configuración se almacena en el directorio de datos local de la aplicación. Las solicitudes son enviadas por el backend de Tauri en lugar de directamente desde el frontend.",
actions: {
+11
View File
@@ -142,6 +142,14 @@ export default {
sshKeyPathBrowse: "浏览",
sshExposeLan: "允许局域网访问隧道",
sshConnectTimeout: "SSH 超时时间(秒)",
proxy: "代理",
proxyEnable: "通过代理连接数据库",
proxyType: "代理类型",
proxyHost: "代理主机",
proxyUsername: "代理用户",
proxyUsernamePlaceholder: "可选",
proxyPassword: "代理密码",
proxyPasswordPlaceholder: "可选",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "驱动类(可选)",
@@ -458,6 +466,9 @@ export default {
truncated: "上下文已截断",
contextSummary: "{database} · {tables} 张表",
autoSqlBlocked: "AI 生成的 SQL 风险较高,已阻止自动执行,请手动检查后再运行。",
proxy: "代理",
proxyEnable: "AI 请求通过代理发送",
proxyUrl: "代理地址",
settingsHint: "配置会保存在本机应用数据目录中。请求由 Tauri 后端发出,避免在前端直接暴露给模型服务。",
actions: {
generate: "生成 SQL",
+2
View File
@@ -171,6 +171,8 @@ export const useConnectionStore = defineStore("connection", () => {
driver_label: config.driver_label || labelMap[profile] || config.db_type,
url_params: config.url_params || "",
ssh_connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
proxy_type: config.proxy_type || "socks5",
proxy_port: config.proxy_port || 1080,
};
}
+2
View File
@@ -11,6 +11,8 @@ export interface AiConfig {
endpoint: string;
model: string;
apiStyle: AiApiStyle;
proxyEnabled?: boolean;
proxyUrl?: string;
}
const defaultConfigs: Record<AiProvider, Omit<AiConfig, "apiKey">> = {
+6
View File
@@ -38,6 +38,12 @@ export interface ConnectionConfig {
ssh_key_passphrase?: string;
ssh_expose_lan?: boolean;
ssh_connect_timeout_secs?: number;
proxy_enabled?: boolean;
proxy_type?: "socks5" | "http";
proxy_host?: string;
proxy_port?: number;
proxy_username?: string;
proxy_password?: string;
ssl?: boolean;
sysdba?: boolean;
connection_string?: string;