mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 19:52:02 +00:00
feat: add auth command
This commit is contained in:
parent
d4608afc2d
commit
9a3041335f
7 changed files with 1616 additions and 22 deletions
509
packages/cli/src/commands/auth/handler.ts
Normal file
509
packages/cli/src/commands/auth/handler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue