From 42c0dc7f8997ff8d7509b96c64dd84f7388a853b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 31 Aug 2026 23:47:14 +0800 Subject: [PATCH] feat(ai): support custom headers for api services --- .../editor/EditorSettingsDialog.vue | 90 ++++++++++++++- apps/desktop/src/i18n/locales/en.ts | 9 ++ apps/desktop/src/i18n/locales/es.ts | 9 ++ apps/desktop/src/i18n/locales/it.ts | 9 ++ apps/desktop/src/i18n/locales/ja.ts | 9 ++ apps/desktop/src/i18n/locales/ko.ts | 9 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 9 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 9 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 9 ++ apps/desktop/src/stores/settingsStore.ts | 12 ++ apps/desktop/src/types/ai.ts | 2 + crates/dbx-core/src/ai.rs | 107 +++++++++++++++++- crates/dbx-core/src/ai_claude_code_cli.rs | 1 + crates/dbx-core/src/ai_codebuddy_cli.rs | 1 + crates/dbx-core/src/ai_codex_cli.rs | 1 + crates/dbx-core/src/ai_cursor_cli.rs | 1 + crates/dbx-core/src/ai_effort.rs | 1 + crates/dbx-core/src/ai_grok_cli.rs | 1 + crates/dbx-core/src/ai_opencode_cli.rs | 1 + crates/dbx-core/src/ai_pi_agent_cli.rs | 1 + crates/dbx-core/src/ai_qoder_cli.rs | 1 + crates/dbx-core/src/cloud_sync.rs | 1 + crates/dbx-core/src/storage.rs | 1 + crates/dbx-web/src/routes/ai.rs | 2 + docs/content/docs/ai-assistant.cn.mdx | 6 +- docs/content/docs/ai-assistant.mdx | 6 +- src-tauri/src/commands/ai.rs | 1 + 27 files changed, 299 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index e3770e1f42..6066d41564 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -62,6 +62,7 @@ import { DUCKDB_WORKER_MAX_PROCESSES_MIN, normalizeDuckDbWorkerMaxProcesses, normalizeAiEnv, + normalizeAiHeaders, type AiProvider, type AiApiStyle, type AiAuthMethod, @@ -325,6 +326,12 @@ interface AiEnvRow { value: string; } +interface AiHeaderRow { + id: string; + name: string; + value: string; +} + function tableColumnTemplateRowsFromSettings(lines: readonly string[]): TableColumnTemplateGridRow[] { return parseTableColumnTemplateFields([...lines]).map((field) => ({ id: uuid(), @@ -3341,6 +3348,7 @@ const aiEditEndpoint = ref(""); const aiEditModel = ref(""); const aiEditLegacyModels = ref([]); const aiEditApiStyle = ref("completions"); +const aiEditCustomHeaderRows = ref([]); const aiEditProxyEnabled = ref(false); const aiEditProxyUrl = ref(""); const aiEditEnableThinking = ref(true); @@ -3505,6 +3513,7 @@ const aiCliMcpActionLabel = computed(() => { return t("settings.mcpUpToDate"); }); const aiCliEnvError = computed(() => cliEnvValidationError()); +const aiHeadersValidationError = computed(() => customHeadersValidationError()); const aiCliPathError = computed(() => { const path = aiEditCliPath.value.trim(); const firstToken = path.split(/\s+/)[0] || ""; @@ -3520,6 +3529,49 @@ function aiEnvRowsFromConfig(env: unknown): AiEnvRow[] { })); } +function aiHeaderRowsFromConfig(headers: unknown): AiHeaderRow[] { + return Object.entries(normalizeAiHeaders(headers)).map(([name, value]) => ({ + id: uuid(), + name, + value, + })); +} + +const AI_HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; +const RESERVED_AI_HEADERS = new Set(["host", "content-length", "content-type", "connection", "transfer-encoding", "proxy-authorization"]); + +function customHeadersFromRows(): Record { + const result: Record = {}; + for (const row of aiEditCustomHeaderRows.value) { + const name = row.name.trim(); + if (name) result[name] = row.value; + } + return result; +} + +function customHeadersValidationError(): string { + const names = new Set(); + for (const row of aiEditCustomHeaderRows.value) { + const name = row.name.trim(); + if (!name && !row.value) continue; + if (!AI_HEADER_NAME_RE.test(name)) return t("ai.customHeadersInvalidName", { name: row.name || "—" }); + const normalizedName = name.toLowerCase(); + if (RESERVED_AI_HEADERS.has(normalizedName)) return t("ai.customHeadersReserved", { name }); + if (names.has(normalizedName)) return t("ai.customHeadersDuplicate", { name }); + if (/\r|\n/.test(row.value)) return t("ai.customHeadersInvalidValue", { name }); + names.add(normalizedName); + } + return ""; +} + +function addAiCustomHeaderRow() { + aiEditCustomHeaderRows.value.push({ id: uuid(), name: "", value: "" }); +} + +function removeAiCustomHeaderRow(id: string) { + aiEditCustomHeaderRows.value = aiEditCustomHeaderRows.value.filter((row) => row.id !== id); +} + function cliEnvFromRows(rows = aiEditCliEnvRows.value): Record { const result: Record = {}; for (const row of rows) { @@ -3578,6 +3630,7 @@ function currentAiEditConfig() { supportedEffortLevels: model.supportedEffortLevels ? [...model.supportedEffortLevels] : undefined, })), apiStyle: aiEditApiStyle.value, + customHeaders: customHeadersFromRows(), proxyEnabled: aiEditProxyEnabled.value, proxyUrl: aiEditProxyUrl.value, enableThinking: aiEditEnableThinking.value, @@ -3624,6 +3677,7 @@ function aiSelectProvider(provider: AiProvider) { aiEditModel.value = ""; aiEditLegacyModels.value = []; aiEditApiStyle.value = preset.apiStyle; + aiEditCustomHeaderRows.value = []; aiEditEnableThinking.value = true; aiEditReasoningLevel.value = "default"; if (CLI_AI_PROVIDERS.has(provider)) void ensureCliMcpStatus(); @@ -3659,6 +3713,7 @@ function aiEnterEditMode(configId?: string) { supportedEffortLevels: model.supportedEffortLevels ? [...model.supportedEffortLevels] : undefined, })); aiEditApiStyle.value = config.apiStyle; + aiEditCustomHeaderRows.value = aiHeaderRowsFromConfig(config.customHeaders); aiEditProxyEnabled.value = config.proxyEnabled ?? false; aiEditProxyUrl.value = config.proxyUrl ?? ""; aiEditEnableThinking.value = config.enableThinking ?? true; @@ -3690,6 +3745,7 @@ function aiEnterEditMode(configId?: string) { aiEditModel.value = ""; aiEditLegacyModels.value = []; aiEditApiStyle.value = AI_PROVIDER_PRESETS["claude"].apiStyle; + aiEditCustomHeaderRows.value = []; aiEditProxyEnabled.value = false; aiEditProxyUrl.value = ""; aiEditEnableThinking.value = true; @@ -3730,6 +3786,7 @@ async function applyPendingAiConfigDeepLinkDraft() { aiEditModel.value = draft.model; aiEditLegacyModels.value = []; aiEditApiStyle.value = draft.apiStyle; + aiEditCustomHeaderRows.value = []; if (!draft.promptForClipboardApiKey) return; @@ -3764,6 +3821,10 @@ async function aiSaveConfig() { toast(t("ai.configNameExists", { name: aiEditConfigName.value }), 3000); return; } + if (aiHeadersValidationError.value) { + toast(aiHeadersValidationError.value, 3000); + return; + } const editConfig = currentAiEditConfig(); const config: AiConfigItem = { @@ -3817,6 +3878,11 @@ async function aiSetDefaultConfig(id: string) { async function aiTestConn() { if ((aiRequiresApiKey.value && !aiEditApiKey.value.trim()) || (!aiIsCliProvider.value && !aiEditEndpoint.value.trim())) return; + if (aiHeadersValidationError.value) { + aiTestResult.value = "error"; + aiTestError.value = aiHeadersValidationError.value; + return; + } if (aiCliValidationError.value) { aiTestResult.value = "error"; aiTestError.value = aiCliValidationError.value; @@ -7206,6 +7272,28 @@ onUnmounted(() => { + +
+ +
+
+
+ + + +
+
+ +

{{ aiHeadersValidationError }}

+

{{ t("ai.customHeadersHint") }}

+
+
+
@@ -8191,7 +8279,7 @@ onUnmounted(() => {