diff --git a/src/components/settings/PresetConnectDialog.tsx b/src/components/settings/PresetConnectDialog.tsx index f2c6b0a2..45a0f383 100644 --- a/src/components/settings/PresetConnectDialog.tsx +++ b/src/components/settings/PresetConnectDialog.tsx @@ -241,6 +241,12 @@ export function PresetConnectDialog({ setSaving(true); try { + // For CLI OAuth preset, inject auth style marker into env_overrides_json + let finalEnvOverrides = isEdit ? envOverridesJson.trim() || "" : ""; + if (preset.key === "cli-oauth") { + finalEnvOverrides = JSON.stringify({ __AUTH_STYLE: "cli_oauth" }); + } + await onSave({ name: name.trim() || preset.name, provider_type: preset.provider_type, @@ -250,7 +256,7 @@ export function PresetConnectDialog({ extra_env: finalExtraEnv, role_models_json: roleModelsJson, headers_json: isEdit ? headersJson.trim() || "{}" : undefined, - env_overrides_json: isEdit ? envOverridesJson.trim() || "" : undefined, + env_overrides_json: finalEnvOverrides || undefined, notes: isEdit ? notes.trim() : "", }); onOpenChange(false); diff --git a/src/components/settings/provider-presets.tsx b/src/components/settings/provider-presets.tsx index 49905fef..b21cd231 100644 --- a/src/components/settings/provider-presets.tsx +++ b/src/components/settings/provider-presets.tsx @@ -65,6 +65,19 @@ export interface QuickPreset { } export const QUICK_PRESETS: QuickPreset[] = [ + // ── Claude CLI OAuth (Subscription) ── + { + key: "cli-oauth", + name: "Claude CLI (Subscription)", + description: "Use Claude CLI built-in OAuth — for Pro/Max/Team subscription accounts. Requires: claude login", + descriptionZh: "Claude CLI 内置登录 — 适用于 Pro/Max/Team 订阅账号。需先运行: claude login", + icon: , + provider_type: "anthropic", + protocol: "anthropic", + base_url: "", + extra_env: "{}", + fields: [], + }, // ── Custom endpoints ── { key: "custom-openai", diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 00000000..55683cf9 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,16 @@ +/** + * Next.js instrumentation — runs once at server startup. + * Sets up a global fetch dispatcher that respects HTTP_PROXY / HTTPS_PROXY + * environment variables, so Telegram API calls (and other outbound fetch) + * work behind a proxy. + */ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici'); + const proxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.https_proxy || process.env.http_proxy; + if (proxy) { + setGlobalDispatcher(new EnvHttpProxyAgent()); + console.log(`[instrumentation] Global fetch proxy enabled: ${proxy}`); + } + } +} diff --git a/src/lib/platform.ts b/src/lib/platform.ts index 0be9860b..44c27f45 100644 --- a/src/lib/platform.ts +++ b/src/lib/platform.ts @@ -71,11 +71,12 @@ export function getClaudeCandidatePaths(): string[] { return candidates; } return [ - '/usr/local/bin/claude', - '/opt/homebrew/bin/claude', path.join(home, '.npm-global', 'bin', 'claude'), + path.join(home, '.bun', 'bin', 'claude'), path.join(home, '.local', 'bin', 'claude'), path.join(home, '.claude', 'bin', 'claude'), + '/opt/homebrew/bin/claude', + '/usr/local/bin/claude', ]; } diff --git a/src/lib/provider-catalog.ts b/src/lib/provider-catalog.ts index 0b9ad2df..0e99b37a 100644 --- a/src/lib/provider-catalog.ts +++ b/src/lib/provider-catalog.ts @@ -30,6 +30,7 @@ export type AuthStyle = | 'api_key' // ANTHROPIC_API_KEY | 'auth_token' // ANTHROPIC_AUTH_TOKEN | 'env_only' // No API key; auth via extra env (bedrock/vertex) + | 'cli_oauth' // No API key; use Claude CLI's built-in OAuth (subscription accounts) | 'custom_header'; // API key in custom header (future) /** @@ -122,6 +123,21 @@ const ANTHROPIC_DEFAULT_MODELS: CatalogModel[] = [ // ── Vendor presets ────────────────────────────────────────────── export const VENDOR_PRESETS: VendorPreset[] = [ + // ── Claude CLI OAuth (Subscription) ── + { + key: 'cli-oauth', + name: 'Claude CLI (Subscription)', + description: 'Use Claude CLI built-in OAuth — for Pro/Max/Team subscription accounts', + descriptionZh: 'Claude CLI 内置登录 — 适用于 Pro/Max/Team 订阅账号', + protocol: 'anthropic', + authStyle: 'cli_oauth', + baseUrl: '', + defaultEnvOverrides: {}, + defaultModels: ANTHROPIC_DEFAULT_MODELS, + fields: [], + iconKey: 'anthropic', + }, + // ── Official Anthropic ── { key: 'anthropic-official', diff --git a/src/lib/provider-resolver.ts b/src/lib/provider-resolver.ts index 0e59fa80..15199593 100644 --- a/src/lib/provider-resolver.ts +++ b/src/lib/provider-resolver.ts @@ -166,7 +166,32 @@ export function toClaudeCodeEnv( 'GEMINI_API_KEY', ]); - if (resolved.provider && resolved.hasCredentials) { + if (resolved.authStyle === 'cli_oauth') { + // CLI OAuth mode — clear all ANTHROPIC_* variables so the Claude CLI + // uses its own built-in OAuth flow (subscription account tokens in ~/.claude/). + // We must NOT inject any API key or auth token here. + for (const key of Object.keys(env)) { + if (key.startsWith('ANTHROPIC_') || MANAGED_ENV_KEYS.has(key)) { + delete env[key]; + } + } + + // Inject role models as env vars (if configured) + if (resolved.roleModels.default) env.ANTHROPIC_MODEL = resolved.roleModels.default; + if (resolved.roleModels.reasoning) env.ANTHROPIC_REASONING_MODEL = resolved.roleModels.reasoning; + if (resolved.roleModels.small) env.ANTHROPIC_SMALL_FAST_MODEL = resolved.roleModels.small; + if (resolved.roleModels.haiku) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolved.roleModels.haiku; + if (resolved.roleModels.sonnet) env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolved.roleModels.sonnet; + if (resolved.roleModels.opus) env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolved.roleModels.opus; + + // Inject env overrides (skip internal markers) + for (const [key, value] of Object.entries(resolved.envOverrides)) { + if (key.startsWith('__')) continue; // skip internal markers like __AUTH_STYLE + if (typeof value === 'string') { + if (value === '') { delete env[key]; } else { env[key] = value; } + } + } + } else if (resolved.provider && resolved.hasCredentials) { // Clear all ANTHROPIC_* variables AND managed env vars to prevent cross-provider leaks for (const key of Object.keys(env)) { if (key.startsWith('ANTHROPIC_') || MANAGED_ENV_KEYS.has(key)) { @@ -543,7 +568,7 @@ function buildResolution( } // Has credentials? - const hasCredentials = !!(provider.api_key) || authStyle === 'env_only'; + const hasCredentials = !!(provider.api_key) || authStyle === 'env_only' || authStyle === 'cli_oauth'; // Settings sources — always include 'user' so SDK can load skills from // ~/.claude/skills/. Env override conflicts are handled by envOverrides. @@ -579,6 +604,12 @@ function inferProtocolFromProvider(provider: ApiProvider): Protocol { } function inferAuthStyleFromProvider(provider: ApiProvider): AuthStyle { + // Check env_overrides for CLI OAuth marker + try { + const overrides = JSON.parse(provider.env_overrides_json || '{}'); + if (overrides.__AUTH_STYLE === 'cli_oauth') return 'cli_oauth'; + } catch { /* ignore */ } + // Check preset match first const preset = findPresetForLegacy(provider.base_url, provider.provider_type); if (preset) return preset.authStyle;