mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
Merge pull request #438 from q1352013520/feature/codex-subscription
feat: /login支持codex订阅登录
This commit is contained in:
@@ -9,9 +9,14 @@ import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcu
|
||||
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
||||
import { getSSLErrorHint } from '@ant/model-provider';
|
||||
import { sendNotification } from '../services/notifier.js';
|
||||
import {
|
||||
completeChatGPTDeviceLogin,
|
||||
requestChatGPTDeviceCode,
|
||||
type ChatGPTDeviceCode,
|
||||
} from '../services/api/openai/chatgptAuth.js';
|
||||
import { OAuthService } from '../services/oauth/index.js';
|
||||
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
|
||||
|
||||
import { openBrowser } from '../utils/browser.js';
|
||||
import { logError } from '../utils/log.js';
|
||||
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { Select } from './CustomSelect/select.js';
|
||||
@@ -46,6 +51,11 @@ type OAuthStatus =
|
||||
opusModel: string;
|
||||
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||
} // OpenAI Chat Completions API platform
|
||||
| {
|
||||
state: 'chatgpt_subscription';
|
||||
phase: 'requesting' | 'waiting';
|
||||
deviceCode?: ChatGPTDeviceCode;
|
||||
} // ChatGPT account subscription via Codex OAuth device flow
|
||||
| {
|
||||
state: 'gemini_api';
|
||||
baseUrl: string;
|
||||
@@ -445,6 +455,15 @@ function OAuthStatusMessage({
|
||||
),
|
||||
value: 'openai_chat_api',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
ChatGPT account with subscription · <Text dimColor>Plus, Pro, Business, Edu, or Enterprise</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'chatgpt_subscription',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
@@ -515,6 +534,12 @@ function OAuthStatusMessage({
|
||||
opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '',
|
||||
activeField: 'base_url',
|
||||
});
|
||||
} else if (value === 'chatgpt_subscription') {
|
||||
logEvent('tengu_chatgpt_subscription_selected', {});
|
||||
setOAuthStatus({
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'requesting',
|
||||
});
|
||||
} else if (value === 'gemini_api') {
|
||||
logEvent('tengu_gemini_api_selected', {});
|
||||
setOAuthStatus({
|
||||
@@ -807,7 +832,9 @@ function OAuthStatusMessage({
|
||||
|
||||
const doOpenAISave = useCallback(() => {
|
||||
const finalVals = { ...openaiDisplayValues, [activeField]: openaiInputValue };
|
||||
const env: Record<string, string> = {};
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENAI_AUTH_MODE: undefined,
|
||||
};
|
||||
|
||||
// Validate base_url if provided
|
||||
if (finalVals.base_url) {
|
||||
@@ -836,10 +863,11 @@ function OAuthStatusMessage({
|
||||
if (finalVals.haiku_model) env.OPENAI_DEFAULT_HAIKU_MODEL = finalVals.haiku_model;
|
||||
if (finalVals.sonnet_model) env.OPENAI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||
if (finalVals.opus_model) env.OPENAI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
modelType: 'openai' as any,
|
||||
env,
|
||||
} as any);
|
||||
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||
modelType: 'openai',
|
||||
env: env as unknown as Record<string, string>,
|
||||
};
|
||||
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
|
||||
if (error) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
@@ -855,7 +883,13 @@ function OAuthStatusMessage({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
||||
for (const [k, v] of Object.entries(env)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k];
|
||||
} else {
|
||||
process.env[k] = v;
|
||||
}
|
||||
}
|
||||
setOAuthStatus({ state: 'success' });
|
||||
void onDone();
|
||||
}
|
||||
@@ -953,6 +987,93 @@ function OAuthStatusMessage({
|
||||
);
|
||||
}
|
||||
|
||||
case 'chatgpt_subscription': {
|
||||
const status = oauthStatus as {
|
||||
state: 'chatgpt_subscription';
|
||||
phase: 'requesting' | 'waiting';
|
||||
deviceCode?: ChatGPTDeviceCode;
|
||||
};
|
||||
const startedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (startedRef.current) return;
|
||||
startedRef.current = true;
|
||||
let cancelled = false;
|
||||
const controller = new AbortController();
|
||||
async function runLogin() {
|
||||
try {
|
||||
const deviceCode = await requestChatGPTDeviceCode();
|
||||
if (cancelled) return;
|
||||
setOAuthStatus({
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'waiting',
|
||||
deviceCode,
|
||||
});
|
||||
void openBrowser(deviceCode.verificationUrl);
|
||||
await completeChatGPTDeviceLogin(deviceCode, controller.signal);
|
||||
if (cancelled) return;
|
||||
const env: Record<string, string> = {
|
||||
OPENAI_AUTH_MODE: 'chatgpt',
|
||||
};
|
||||
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||
modelType: 'openai',
|
||||
env,
|
||||
};
|
||||
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
|
||||
if (error) {
|
||||
throw new Error('Failed to save settings. Please try again.');
|
||||
}
|
||||
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
||||
setOAuthStatus({ state: 'success' });
|
||||
void onDone();
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: (err as Error).message,
|
||||
toRetry: {
|
||||
state: 'chatgpt_subscription',
|
||||
phase: 'requesting',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
void runLogin();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [setOAuthStatus, onDone]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold>ChatGPT Account Setup</Text>
|
||||
{status.phase === 'requesting' && (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Requesting sign-in code…</Text>
|
||||
</Box>
|
||||
)}
|
||||
{status.phase === 'waiting' && status.deviceCode && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>Open this link and sign in with your ChatGPT account:</Text>
|
||||
<Link url={status.deviceCode.verificationUrl}>
|
||||
<Text dimColor>{status.deviceCode.verificationUrl}</Text>
|
||||
</Link>
|
||||
<Text>
|
||||
Enter code: <Text bold>{status.deviceCode.userCode}</Text>
|
||||
</Text>
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Waiting for ChatGPT authorization…</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Text dimColor>Esc to go back. Device codes expire after 15 minutes.</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
case 'gemini_api': {
|
||||
type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||
const GEMINI_FIELDS: GeminiField[] = ['base_url', 'api_key', 'haiku_model', 'sonnet_model', 'opus_model'];
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getDefaultEffortForModel,
|
||||
modelSupportsEffort,
|
||||
modelSupportsMaxEffort,
|
||||
modelSupportsXhighEffort,
|
||||
resolvePickerEffortPersistence,
|
||||
toPersistableEffort,
|
||||
} from '../utils/effort.js';
|
||||
@@ -146,11 +147,19 @@ export function ModelPicker({
|
||||
focusedValue !== NO_PREFERENCE &&
|
||||
marked1MValues.has(focusedValue.replace(/\[1m\]/i, ''));
|
||||
const focusedSupportsEffort = focusedModel ? modelSupportsEffort(focusedModel) : false;
|
||||
const focusedSupportsXhigh = focusedModel ? modelSupportsXhighEffort(focusedModel) : false;
|
||||
const focusedSupportsMax = focusedModel ? modelSupportsMaxEffort(focusedModel) : false;
|
||||
const focusedDefaultEffort = getDefaultEffortLevelForOption(focusedValue);
|
||||
// Clamp display when 'max' is selected but the focused model doesn't support it.
|
||||
// Clamp display when selected effort isn't supported by the focused model.
|
||||
// resolveAppliedEffort() does the same downgrade at API-send time.
|
||||
const displayEffort = effort === 'max' && !focusedSupportsMax ? 'high' : effort;
|
||||
const displayEffort =
|
||||
effort === 'max' && !focusedSupportsMax
|
||||
? focusedSupportsXhigh
|
||||
? 'xhigh'
|
||||
: 'high'
|
||||
: effort === 'xhigh' && !focusedSupportsXhigh
|
||||
? 'high'
|
||||
: effort;
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(value: string) => {
|
||||
@@ -166,10 +175,12 @@ export function ModelPicker({
|
||||
const handleCycleEffort = useCallback(
|
||||
(direction: 'left' | 'right') => {
|
||||
if (!focusedSupportsEffort) return;
|
||||
setEffort(prev => cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsMax));
|
||||
setEffort(prev =>
|
||||
cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsXhigh, focusedSupportsMax),
|
||||
);
|
||||
setHasToggledEffort(true);
|
||||
},
|
||||
[focusedSupportsEffort, focusedSupportsMax, focusedDefaultEffort],
|
||||
[focusedSupportsEffort, focusedSupportsXhigh, focusedSupportsMax, focusedDefaultEffort],
|
||||
);
|
||||
|
||||
useKeybindings(
|
||||
@@ -333,8 +344,19 @@ function EffortLevelIndicator({ effort }: { effort?: EffortLevel }): React.React
|
||||
return <Text color={effort ? 'claude' : 'subtle'}>{effortLevelToSymbol(effort ?? 'low')}</Text>;
|
||||
}
|
||||
|
||||
function cycleEffortLevel(current: EffortLevel, direction: 'left' | 'right', includeMax: boolean): EffortLevel {
|
||||
const levels: EffortLevel[] = includeMax ? ['low', 'medium', 'high', 'max'] : ['low', 'medium', 'high'];
|
||||
function cycleEffortLevel(
|
||||
current: EffortLevel,
|
||||
direction: 'left' | 'right',
|
||||
includeXhigh: boolean,
|
||||
includeMax: boolean,
|
||||
): EffortLevel {
|
||||
const levels: EffortLevel[] = [
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
...(includeXhigh ? (['xhigh'] as const) : []),
|
||||
...(includeMax ? (['max'] as const) : []),
|
||||
];
|
||||
// If the current level isn't in the cycle (e.g. 'max' after switching to a
|
||||
// non-Opus model), clamp to 'high'.
|
||||
const idx = levels.indexOf(current);
|
||||
|
||||
Reference in New Issue
Block a user