From b0024474363c54a0ff8a3c7e105831b67dddc12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Wed, 15 Apr 2026 14:18:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20provider=20=E9=9A=94=E7=A6=BB=E4=B8=8E?= =?UTF-8?q?=20Claude=20=E5=AE=98=E6=96=B9=E8=AE=A4=E8=AF=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - provider 配置写入 ~/.claude/cc-haha/settings.json,不再污染原版 settings.json - 子进程 env 剥离 PROVIDER_ENV_KEYS,防止残留 key 传递 - bin/claude-haha 在 CC_HAHA_SKIP_DOTENV=1 时用 --env-file=/dev/null 阻止 Bun 自动加载 .env - managedEnv.ts 增加 getCcHahaSettingsEnv() 读取隔离配置 - 新增 /api/providers/auth-status 端点,启动时检测认证状态 - AppShell 启动时无认证则跳转 provider 设置页 --- bin/claude-haha | 20 +++++++-- desktop/src/api/providers.ts | 9 ++++ desktop/src/components/layout/AppShell.tsx | 14 +++++++ src/server/api/providers.ts | 7 ++++ src/server/index.ts | 29 +++++++++++++ src/server/router.ts | 4 ++ src/server/services/conversationService.ts | 25 ++++++++++- src/server/services/providerService.ts | 49 +++++++++++++++++++++- src/utils/managedEnv.ts | 31 +++++++++++++- 9 files changed, 181 insertions(+), 7 deletions(-) diff --git a/bin/claude-haha b/bin/claude-haha index 920fb89b..e488897f 100755 --- a/bin/claude-haha +++ b/bin/claude-haha @@ -3,14 +3,28 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" #Get your current working directory and export it as an environment variable. -export CALLER_DIR="$(pwd -W 2>/dev/null || pwd)" +export CALLER_DIR="${CALLER_DIR:-$(pwd -W 2>/dev/null || pwd)}" cd "$ROOT_DIR" +# When spawned by the desktop/web server as a child CLI process, +# skip .env loading — the server has already set the correct env +# via cc-haha/settings.json. Loading .env would re-inject stale +# provider keys (e.g., a MiniMax key as ANTHROPIC_API_KEY) that +# override the active provider config. +if [[ "${CC_HAHA_SKIP_DOTENV:-0}" == "1" ]]; then + # Bun auto-loads .env by default; explicitly point to /dev/null to suppress. + ENV_FILE_FLAG="--env-file=/dev/null" +elif [[ -f .env ]]; then + ENV_FILE_FLAG="--env-file=.env" +else + ENV_FILE_FLAG="" +fi + # Force recovery CLI (simple readline REPL, no Ink TUI) if [[ "${CLAUDE_CODE_FORCE_RECOVERY_CLI:-0}" == "1" ]]; then - exec bun --env-file=.env ./src/localRecoveryCli.ts "$@" + exec bun $ENV_FILE_FLAG ./src/localRecoveryCli.ts "$@" fi # Default: full CLI with Ink TUI -exec bun --env-file=.env ./src/entrypoints/cli.tsx "$@" +exec bun $ENV_FILE_FLAG ./src/entrypoints/cli.tsx "$@" diff --git a/desktop/src/api/providers.ts b/desktop/src/api/providers.ts index ae827c61..6531a4a2 100644 --- a/desktop/src/api/providers.ts +++ b/desktop/src/api/providers.ts @@ -14,6 +14,11 @@ type ProvidersResponse = { providers: SavedProvider[]; activeId: string | null } type ProviderResponse = { provider: SavedProvider } type PresetsResponse = { presets: ProviderPreset[] } type TestResultResponse = { result: ProviderTestResult } +type AuthStatusResponse = { + hasAuth: boolean + source: 'cc-haha-provider' | 'original-settings' | 'env' | 'none' + activeProvider?: string +} export const providersApi = { list() { @@ -24,6 +29,10 @@ export const providersApi = { return api.get('/api/providers/presets') }, + authStatus() { + return api.get('/api/providers/auth-status') + }, + create(input: CreateProviderInput) { return api.post('/api/providers', input) }, diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx index 323a7fe0..6d84ecfd 100644 --- a/desktop/src/components/layout/AppShell.tsx +++ b/desktop/src/components/layout/AppShell.tsx @@ -11,6 +11,7 @@ import { TabBar } from './TabBar' import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore' import { useChatStore } from '../../stores/chatStore' import { useTranslation } from '../../i18n' +import { providersApi } from '../../api/providers' export function AppShell() { const fetchSettings = useSettingsStore((s) => s.fetchAll) @@ -25,6 +26,19 @@ export function AppShell() { try { await initializeDesktopServerUrl() await fetchSettings() + + // Check auth status: if no auth at all, redirect to provider setup + try { + const authStatus = await providersApi.authStatus() + if (!authStatus.hasAuth) { + // No auth — open Settings on the providers tab + useUIStore.getState().setPendingSettingsTab('providers') + useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings') + } + } catch { + // auth-status check failed — continue normally + } + // Restore tabs from localStorage await useTabStore.getState().restoreTabs() const activeId = useTabStore.getState().activeTabId diff --git a/src/server/api/providers.ts b/src/server/api/providers.ts index d83324a5..8a2eaef0 100644 --- a/src/server/api/providers.ts +++ b/src/server/api/providers.ts @@ -3,6 +3,7 @@ * * GET /api/providers — list all saved providers + activeId * GET /api/providers/presets — list available presets + * GET /api/providers/auth-status — check whether any usable auth exists * POST /api/providers — add a provider * PUT /api/providers/:id — update a provider * DELETE /api/providers/:id — delete a provider @@ -55,6 +56,12 @@ export async function handleProvidersApi( return Response.json({ presets: PROVIDER_PRESETS }) } + // GET /api/providers/auth-status + if (id === 'auth-status' && req.method === 'GET') { + const status = await providerService.checkAuthStatus() + return Response.json(status) + } + // POST /api/providers/official if (id === 'official' && req.method === 'POST') { await providerService.activateOfficial() diff --git a/src/server/index.ts b/src/server/index.ts index 3d9dd6fd..03d3bd3b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -217,6 +217,35 @@ export function startServer(port = PORT, host = HOST) { return server } +// ─── Graceful shutdown: kill all CLI subprocesses on exit ──────────────────── +import { conversationService } from './services/conversationService.js' + +function cleanupAllSessions() { + const active = conversationService.getActiveSessions() + if (active.length > 0) { + console.log(`[Server] Shutting down — killing ${active.length} CLI subprocess(es)`) + for (const sessionId of active) { + conversationService.stopSession(sessionId) + } + } +} + +process.on('SIGTERM', () => { + console.log('[Server] Received SIGTERM') + cleanupAllSessions() + process.exit(0) +}) + +process.on('SIGINT', () => { + console.log('[Server] Received SIGINT') + cleanupAllSessions() + process.exit(0) +}) + +process.on('exit', () => { + cleanupAllSessions() +}) + // Direct execution if (import.meta.main) { startServer() diff --git a/src/server/router.ts b/src/server/router.ts index b2854e47..427582fb 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -15,6 +15,7 @@ import { handleFilesystemRoute } from './api/filesystem.js' import { handleProvidersApi } from './api/providers.js' import { handleAdaptersApi } from './api/adapters.js' import { handleSkillsApi } from './api/skills.js' +import { handleComputerUseApi } from './api/computer-use.js' export async function handleApiRequest(req: Request, url: URL): Promise { const path = url.pathname @@ -71,6 +72,9 @@ export async function handleApiRequest(req: Request, url: URL): Promise diff --git a/src/server/services/providerService.ts b/src/server/services/providerService.ts index 8556ac07..b4683001 100644 --- a/src/server/services/providerService.ts +++ b/src/server/services/providerService.ts @@ -2,7 +2,8 @@ * Provider Service — preset-based provider configuration * * Storage: ~/.claude/cc-haha/providers.json (lightweight index) - * Active provider env vars written to ~/.claude/settings.json + * Active provider env vars written to ~/.claude/cc-haha/settings.json + * (isolated from the original Claude Code's ~/.claude/settings.json) */ import * as fs from 'fs/promises' @@ -59,7 +60,7 @@ export class ProviderService { } private getSettingsPath(): string { - return path.join(this.getConfigDir(), 'settings.json') + return path.join(this.getCcHahaDir(), 'settings.json') } private async readIndex(): Promise { @@ -250,6 +251,50 @@ export class ProviderService { await this.writeSettings(settings) } + // --- Auth status --- + + /** + * Check whether any usable auth exists: + * 1. A cc-haha provider is active → has auth + * 2. Original ~/.claude/settings.json has ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY → has auth + * 3. process.env already has ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN → has auth + * 4. None of the above → needs setup + */ + async checkAuthStatus(): Promise<{ + hasAuth: boolean + source: 'cc-haha-provider' | 'original-settings' | 'env' | 'none' + activeProvider?: string + }> { + // 1. Check cc-haha active provider + const index = await this.readIndex() + if (index.activeId) { + const provider = index.providers.find(p => p.id === index.activeId) + if (provider?.apiKey) { + return { hasAuth: true, source: 'cc-haha-provider', activeProvider: provider.name } + } + } + + // 2. Check process.env (covers .env file + inherited env) + if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) { + return { hasAuth: true, source: 'env' } + } + + // 3. Check original ~/.claude/settings.json + try { + const originalPath = path.join(this.getConfigDir(), 'settings.json') + const raw = await fs.readFile(originalPath, 'utf-8') + const settings = JSON.parse(raw) as { env?: Record } + const env = settings.env ?? {} + if (env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY) { + return { hasAuth: true, source: 'original-settings' } + } + } catch { + // File doesn't exist or invalid + } + + return { hasAuth: false, source: 'none' } + } + // --- Proxy support --- async getActiveProviderForProxy(): Promise<{ diff --git a/src/utils/managedEnv.ts b/src/utils/managedEnv.ts index 324b2d31..45aeca81 100644 --- a/src/utils/managedEnv.ts +++ b/src/utils/managedEnv.ts @@ -1,7 +1,9 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js' import { clearCACertsCache } from './caCerts.js' import { getGlobalConfig } from './config.js' -import { isEnvTruthy } from './envUtils.js' +import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js' import { isProviderManagedEnvVar, SAFE_ENV_VARS, @@ -90,6 +92,23 @@ function filterSettingsEnv( ) } +/** + * Read env vars from ~/.claude/cc-haha/settings.json (Haha-specific provider + * config). This file is written by ProviderService.syncToSettings() and + * contains ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, model defaults, etc. + * Returns an empty object if the file doesn't exist or is invalid. + */ +function getCcHahaSettingsEnv(): Record { + try { + const ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json') + const raw = readFileSync(ccHahaSettings, 'utf-8') + const parsed = JSON.parse(raw) as { env?: Record } + return parsed.env ?? {} + } catch { + return {} + } +} + /** * Trusted setting sources whose env vars can be applied before the trust dialog. * @@ -148,6 +167,12 @@ export function applySafeConfigEnvironmentVariables(): void { ) } + // cc-haha provider isolation: apply env from ~/.claude/cc-haha/settings.json + // AFTER userSettings so Haha-specific provider config takes priority over + // the original Claude Code's settings. This prevents Haha from polluting + // ~/.claude/settings.json while still allowing it to override provider vars. + Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv())) + // Compute remote-managed-settings eligibility now, with userSettings and // flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK, // ANTHROPIC_BASE_URL — both settable via settings.env. @@ -189,6 +214,10 @@ export function applyConfigEnvironmentVariables(): void { Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env)) + // cc-haha provider isolation: same as in applySafeConfigEnvironmentVariables, + // apply Haha-specific env last so it overrides the original settings. + Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv())) + // Clear caches so agents are rebuilt with the new env vars clearCACertsCache() clearMTLSCache()