feat: add auth command

This commit is contained in:
LaZzyMan 2026-03-17 18:11:22 +08:00
parent d4608afc2d
commit 9a3041335f
7 changed files with 1616 additions and 22 deletions

View file

@ -0,0 +1,509 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
AuthType,
getErrorMessage,
type Config,
type ProviderModelConfig as ModelConfig,
} from '@qwen-code/qwen-code-core';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { t } from '../../i18n/index.js';
import {
getCodingPlanConfig,
isCodingPlanConfig,
CodingPlanRegion,
CODING_PLAN_ENV_KEY,
} from '../../constants/codingPlan.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
import { backupSettingsFile } from '../../utils/settingsUtils.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { loadCliConfig } from '../../config/config.js';
import type { CliArgs } from '../../config/config.js';
import { InteractiveSelector } from './interactiveSelector.js';
interface QwenAuthOptions {
region?: string;
key?: string;
}
interface CodingPlanSettings {
region?: CodingPlanRegion;
version?: string;
}
interface MergedSettingsWithCodingPlan {
security?: {
auth?: {
selectedType?: string;
};
};
codingPlan?: CodingPlanSettings;
model?: {
name?: string;
};
modelProviders?: Record<string, ModelConfig[]>;
env?: Record<string, string>;
}
/**
* Handles the authentication process based on the specified command and options
*/
export async function handleQwenAuth(
command: 'qwen-oauth' | 'code-plan',
options: QwenAuthOptions,
) {
try {
const settings = loadSettings();
// Create a minimal argv for config loading
const minimalArgv: CliArgs = {
query: undefined,
model: undefined,
sandbox: undefined,
sandboxImage: undefined,
debug: undefined,
prompt: undefined,
promptInteractive: undefined,
yolo: undefined,
approvalMode: undefined,
telemetry: undefined,
checkpointing: undefined,
telemetryTarget: undefined,
telemetryOtlpEndpoint: undefined,
telemetryOtlpProtocol: undefined,
telemetryLogPrompts: undefined,
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
acp: undefined,
experimentalAcp: undefined,
experimentalLsp: undefined,
experimentalHooks: undefined,
extensions: [],
listExtensions: undefined,
openaiLogging: undefined,
openaiApiKey: undefined,
openaiBaseUrl: undefined,
openaiLoggingDir: undefined,
proxy: undefined,
includeDirectories: undefined,
tavilyApiKey: undefined,
googleApiKey: undefined,
googleSearchEngineId: undefined,
webSearchDefault: undefined,
screenReader: undefined,
inputFormat: undefined,
outputFormat: undefined,
includePartialMessages: undefined,
chatRecording: undefined,
continue: undefined,
resume: undefined,
sessionId: undefined,
maxSessionTurns: undefined,
coreTools: undefined,
excludeTools: undefined,
authType: undefined,
channel: undefined,
};
// Create a minimal config to access settings and storage
const config = await loadCliConfig(
settings.merged,
minimalArgv,
process.cwd(),
[], // No extensions for auth command
);
if (command === 'qwen-oauth') {
await handleQwenOAuth(config, settings);
} else if (command === 'code-plan') {
await handleCodePlanAuth(config, settings, options);
}
// Exit after authentication is complete
writeStdoutLine(t('Authentication completed successfully.'));
process.exit(0);
} catch (error) {
writeStderrLine(getErrorMessage(error));
process.exit(1);
}
}
/**
* Handles Qwen OAuth authentication
*/
async function handleQwenOAuth(
config: Config,
settings: LoadedSettings,
): Promise<void> {
writeStdoutLine(t('Starting Qwen OAuth authentication...'));
try {
await config.refreshAuth(AuthType.QWEN_OAUTH);
// Persist the auth type
const authTypeScope = getPersistScopeForModelSelection(settings);
settings.setValue(
authTypeScope,
'security.auth.selectedType',
AuthType.QWEN_OAUTH,
);
writeStdoutLine(t('Successfully authenticated with Qwen OAuth.'));
process.exit(0);
} catch (error) {
writeStderrLine(
t('Failed to authenticate with Qwen OAuth: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}
/**
* Handles Alibaba Cloud Coding Plan authentication
*/
async function handleCodePlanAuth(
config: Config,
settings: LoadedSettings,
options: QwenAuthOptions,
): Promise<void> {
const { region, key } = options;
let selectedRegion: CodingPlanRegion;
let selectedKey: string;
// If region and key are provided as options, use them
if (region && key) {
selectedRegion =
region.toLowerCase() === 'global'
? CodingPlanRegion.GLOBAL
: CodingPlanRegion.CHINA;
selectedKey = key;
} else {
// Otherwise, prompt interactively
selectedRegion = await promptForRegion();
selectedKey = await promptForKey();
}
writeStdoutLine(t('Processing Alibaba Cloud Coding Plan authentication...'));
try {
// Get configuration based on region
const { template, version } = getCodingPlanConfig(selectedRegion);
// Get persist scope
const authTypeScope = getPersistScopeForModelSelection(settings);
// Backup settings file before modification
const settingsFile = settings.forScope(authTypeScope);
backupSettingsFile(settingsFile.path);
// Store api-key in settings.env (unified env key)
settings.setValue(authTypeScope, `env.${CODING_PLAN_ENV_KEY}`, selectedKey);
// Sync to process.env immediately so refreshAuth can read the apiKey
process.env[CODING_PLAN_ENV_KEY] = selectedKey;
// Generate model configs from template
const newConfigs = template.map((templateConfig) => ({
...templateConfig,
envKey: CODING_PLAN_ENV_KEY,
}));
// Get existing configs
const existingConfigs =
(settings.merged.modelProviders as Record<string, ModelConfig[]>)?.[
AuthType.USE_OPENAI
] || [];
// Filter out all existing Coding Plan configs (mutually exclusive)
const nonCodingPlanConfigs = existingConfigs.filter(
(existing) => !isCodingPlanConfig(existing.baseUrl, existing.envKey),
);
// Add new Coding Plan configs at the beginning
const updatedConfigs = [...newConfigs, ...nonCodingPlanConfigs];
// Persist to modelProviders
settings.setValue(
authTypeScope,
`modelProviders.${AuthType.USE_OPENAI}`,
updatedConfigs,
);
// Also persist authType
settings.setValue(
authTypeScope,
'security.auth.selectedType',
AuthType.USE_OPENAI,
);
// Persist coding plan region
settings.setValue(authTypeScope, 'codingPlan.region', selectedRegion);
// Persist coding plan version (single field for backward compatibility)
settings.setValue(authTypeScope, 'codingPlan.version', version);
// If there are configs, use the first one as the model
if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) {
settings.setValue(
authTypeScope,
'model.name',
(updatedConfigs[0] as ModelConfig).id,
);
}
// Refresh auth with the new configuration
await config.refreshAuth(AuthType.USE_OPENAI);
writeStdoutLine(
t('Successfully authenticated with Alibaba Cloud Coding Plan.'),
);
} catch (error) {
writeStderrLine(
t('Failed to authenticate with Coding Plan: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}
/**
* Prompts the user to select a region using an interactive selector
*/
async function promptForRegion(): Promise<CodingPlanRegion> {
const selector = new InteractiveSelector(
[
{
value: CodingPlanRegion.CHINA,
label: t('中国 (China)'),
description: t('阿里云百炼 (aliyun.com)'),
},
{
value: CodingPlanRegion.GLOBAL,
label: t('Global'),
description: t('Alibaba Cloud (alibabacloud.com)'),
},
],
t('Select region for Coding Plan:'),
);
return await selector.select();
}
/**
* Prompts the user to enter an API key
*/
async function promptForKey(): Promise<string> {
// Create a simple password-style input (without echoing characters)
const stdin = process.stdin;
const stdout = process.stdout;
stdout.write(t('Enter your Coding Plan API key: '));
// Set raw mode to capture keystrokes
const wasRaw = stdin.isRaw;
if (stdin.setRawMode) {
stdin.setRawMode(true);
}
stdin.resume();
return new Promise<string>((resolve, reject) => {
let input = '';
const onData = (chunk: string) => {
for (const char of chunk) {
switch (char) {
case '\r': // Enter
case '\n':
stdin.removeListener('data', onData);
if (stdin.setRawMode) {
stdin.setRawMode(wasRaw);
}
stdout.write('\n'); // New line after input
resolve(input);
return;
case '\x03': // Ctrl+C
stdin.removeListener('data', onData);
if (stdin.setRawMode) {
stdin.setRawMode(wasRaw);
}
stdout.write('^C\n');
reject(new Error('Interrupted'));
return;
case '\x08': // Backspace
case '\x7F': // Delete
if (input.length > 0) {
input = input.slice(0, -1);
// Move cursor back, print space, move back again
stdout.write('\x1B[D \x1B[D');
}
break;
default:
// Add character to input
input += char;
// Print asterisk instead of the actual character for security
stdout.write('*');
break;
}
}
};
stdin.on('data', onData);
});
}
/**
* Runs the interactive authentication flow
*/
export async function runInteractiveAuth() {
const selector = new InteractiveSelector(
[
{
value: 'qwen-oauth' as const,
label: t('Qwen OAuth'),
description: t('Free · Up to 1,000 requests/day · Qwen latest models'),
},
{
value: 'code-plan' as const,
label: t('Alibaba Cloud Coding Plan'),
description: t(
'Paid · Up to 6,000 requests/5 hrs · All Alibaba Cloud Coding Plan Models',
),
},
],
t('Select authentication method:'),
);
const choice = await selector.select();
if (choice === 'code-plan') {
await handleQwenAuth('code-plan', {});
} else {
await handleQwenAuth('qwen-oauth', {});
}
}
/**
* Shows the current authentication status
*/
export async function showAuthStatus(): Promise<void> {
try {
const settings = loadSettings();
const mergedSettings = settings.merged as MergedSettingsWithCodingPlan;
writeStdoutLine(t('\n=== Authentication Status ===\n'));
// Check for selected auth type
const selectedType = mergedSettings.security?.auth?.selectedType;
if (!selectedType) {
writeStdoutLine(t('⚠️ No authentication method configured.\n'));
writeStdoutLine(t('Run one of the following commands to get started:\n'));
writeStdoutLine(
t(
' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)',
),
);
writeStdoutLine(
t(
' qwen auth code-plan - Authenticate with Alibaba Cloud Coding Plan\n',
),
);
writeStdoutLine(t('Or simply run:'));
writeStdoutLine(
t(' qwen auth - Interactive authentication setup\n'),
);
process.exit(0);
}
// Display status based on auth type
if (selectedType === AuthType.QWEN_OAUTH) {
writeStdoutLine(t('✓ Authentication Method: Qwen OAuth'));
writeStdoutLine(t(' Type: Free tier'));
writeStdoutLine(t(' Limit: Up to 1,000 requests/day'));
writeStdoutLine(t(' Models: Qwen latest models\n'));
} else if (selectedType === AuthType.USE_OPENAI) {
// Check for Coding Plan configuration
const codingPlanRegion = mergedSettings.codingPlan?.region;
const codingPlanVersion = mergedSettings.codingPlan?.version;
const modelName = mergedSettings.model?.name;
// Check if API key is set in environment
const hasApiKey =
!!process.env[CODING_PLAN_ENV_KEY] ||
!!mergedSettings.env?.[CODING_PLAN_ENV_KEY];
if (hasApiKey) {
writeStdoutLine(
t('✓ Authentication Method: Alibaba Cloud Coding Plan'),
);
if (codingPlanRegion) {
const regionDisplay =
codingPlanRegion === CodingPlanRegion.CHINA
? t('中国 (China) - 阿里云百炼')
: t('Global - Alibaba Cloud');
writeStdoutLine(t(' Region: {{region}}', { region: regionDisplay }));
}
if (modelName) {
writeStdoutLine(
t(' Current Model: {{model}}', { model: modelName }),
);
}
if (codingPlanVersion) {
writeStdoutLine(
t(' Config Version: {{version}}', {
version: codingPlanVersion.substring(0, 8) + '...',
}),
);
}
writeStdoutLine(t(' Status: API key configured\n'));
} else {
writeStdoutLine(
t(
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)',
),
);
writeStdoutLine(
t(' Issue: API key not found in environment or settings\n'),
);
writeStdoutLine(t(' Run `qwen auth code-plan` to re-configure.\n'));
}
} else {
writeStdoutLine(
t('✓ Authentication Method: {{type}}', { type: selectedType }),
);
writeStdoutLine(t(' Status: Configured\n'));
}
// Show available commands
writeStdoutLine(t('---'));
writeStdoutLine(t('Commands:'));
writeStdoutLine(
t(' qwen auth - Change authentication method'),
);
writeStdoutLine(t(' qwen auth status - Show this status'));
writeStdoutLine(t(' qwen auth qwen-oauth - Switch to Qwen OAuth'));
writeStdoutLine(t(' qwen auth code-plan - Switch to Coding Plan\n'));
process.exit(0);
} catch (error) {
writeStderrLine(
t('Failed to check authentication status: {{error}}', {
error: getErrorMessage(error),
}),
);
process.exit(1);
}
}