// login.tsx: 真正的登录逻辑 + UI import { feature } from 'bun:bundle'; import * as React from 'react'; // TODO import { useMemo, useState } from 'react' import { resetCostState } from '../../bootstrap/state.js'; import { clearTrustedDeviceToken, enrollTrustedDevice } from '../../bridge/trustedDevice.js'; import type { LocalJSXCommandContext } from '../../commands.js'; import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'; import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js'; // TODO import { Select } from '../../components/CustomSelect/select.js'; import { Dialog } from '../../components/design-system/Dialog.js'; // TODO import { OpenAILoginFlow } from '../../components/OpenAILoginFlow.js' import { OpenRouterLoginFlow } from '../../components/OpenRouterLoginFlow.js' import { LocalLoginFlow } from '../../components/LocalLoginFlow.js' import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; // TODO Box import { Box, Text } from '../../ink.js'; import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'; import { refreshPolicyLimits } from '../../services/policyLimits/index.js'; import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'; import type { LocalJSXCommandOnDone } from '../../types/command.js'; // TODO import { getConfiguredAuthProvider } from '../../utils/auth.js'; import { stripSignatureBlocks } from '../../utils/messages.js'; import { checkAndDisableAutoModeIfNeeded, checkAndDisableBypassPermissionsIfNeeded, resetAutoModeGateCheck, resetBypassPermissionsCheck } from '../../utils/permissions/bypassPermissionsKillswitch.js'; import { resetUserCache } from '../../utils/user.js'; // TODO type AuthProviderChoice = 'anthropic' | 'openai' | 'openrouter' | 'local' /* 第一层: 入口函数 call() * CLI 执行 /login 真正被调用的函数 * 登录成功后的 核心逻辑 * */ export async function call( onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext ): Promise { // 渲染 React 组件: return ( { // 更新 API Key, 通知系统 身份变了 context.onChangeAPIKey(); // Signature-bearing blocks (thinking, connector_text) are bound to the API key — // strip them so the new key doesn't reject stale signatures. // 清理旧消息签名, 防止 新账号 + 旧签名 = 校验失败 context.setMessages(stripSignatureBlocks); if (success) { // 这些 reset 的本质: 换账号 = 全部重置 // Post-login refresh logic. Keep in sync with onboarding in src/interactiveHelpers.tsx // 成本统计 Reset cost state when switching accounts resetCostState(); // 远程配置 Refresh remotely managed settings after login (non-blocking) void refreshRemoteManagedSettings(); // Refresh policy limits after login (non-blocking) void refreshPolicyLimits(); // 用户缓存 Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials resetUserCache(); // Feature flags(GrowthBook) Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs) refreshGrowthBookAfterAuthChange(); // 设备信任 记住这台设备 // Clear any stale trusted device token from a previous account before // re-enrolling — prevents sending the old token on bridge calls while // the async enrollTrustedDevice() is in-flight. clearTrustedDeviceToken(); // Enroll as a trusted device for Remote Control (10-min fresh-session window) void enrollTrustedDevice(); // 权限系统(killswitch) 防止越权操作, 自动模式滥用 // Reset killswitch gate checks and re-run with new org resetBypassPermissionsCheck(); const appState = context.getAppState(); void checkAndDisableBypassPermissionsIfNeeded(appState.toolPermissionContext, context.setAppState); // 自动模式 gating, Feature flag 控制功能开关 if (feature('TRANSCRIPT_CLASSIFIER')) { resetAutoModeGateCheck(); void checkAndDisableAutoModeIfNeeded(appState.toolPermissionContext, context.setAppState, appState.fastMode); } // Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers) context.setAppState(prev => ({ ...prev, authVersion: prev.authVersion + 1 // 触发全局更新, 用 version 触发所有 hook 重新 fetch })); } // 返回结果 onDone(success ? 'Login successful' : 'Login interrupted'); }} /> ) } /* 第二层: UI 组件 Login, CLI UI(用 React + Ink) * UI 结构 * Dialog 是 CLI 弹窗 * 真正执行 OAuth 登录逻辑, 打开浏览器, 获取 token, 回传 CLI * * */ export function Login(props: { onDone: (success: boolean, mainLoopModel: string) => void startingMessage?: string }): React.ReactNode { const mainLoopModel = useMainLoopModel() const configuredAuthProvider = getConfiguredAuthProvider() const [selectedProvider, setSelectedProvider] = useState(null) const providerOptions = useMemo( () => [ { label: ( Anthropic{' '} Subscription login, Console API billing, or Bedrock/Foundry/Vertex {'\n'} ), value: 'anthropic', }, { label: ( OpenAI / Codex{' '} Codex login, Codex auth import, or OpenAI API key {'\n'} ), value: 'openai', }, { label: ( OpenRouter{' '} OpenRouter API key via Responses API {'\n'} ), value: 'openrouter', }, { label: ( Local{' '} Local model server (Ollama, LM Studio, vLLM, etc.) {'\n'} ), value: 'local', }, ], [], ) const onCancel = () => props.onDone(false, mainLoopModel) const onFlowDone = () => props.onDone(true, mainLoopModel) const body = selectedProvider === null ? ( {props.startingMessage ?? 'Choose which provider you want Better-Clawd to use.'} Current default: {configuredAuthProvider}. Pick a provider first, then choose the login method inside that flow. Select provider: