mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-04 22:51:08 +00:00
Merge pull request #2668 from JohnKeating1997/feat/optimize-auth-intro
This commit is contained in:
commit
4bacdea01e
7 changed files with 629 additions and 9 deletions
24
packages/cli/src/constants/alibabaStandardApiKey.ts
Normal file
24
packages/cli/src/constants/alibabaStandardApiKey.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export type AlibabaStandardRegion =
|
||||
| 'cn-beijing'
|
||||
| 'sg-singapore'
|
||||
| 'us-virginia'
|
||||
| 'cn-hongkong';
|
||||
|
||||
export const DASHSCOPE_STANDARD_API_KEY_ENV_KEY = 'DASHSCOPE_API_KEY';
|
||||
|
||||
export const ALIBABA_STANDARD_API_KEY_ENDPOINTS: Record<
|
||||
AlibabaStandardRegion,
|
||||
string
|
||||
> = {
|
||||
'cn-beijing': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
'sg-singapore': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
||||
'us-virginia': 'https://dashscope-us.aliyuncs.com/compatible-mode/v1',
|
||||
'cn-hongkong':
|
||||
'https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
};
|
||||
|
|
@ -190,6 +190,8 @@ describe('AppContainer State Management', () => {
|
|||
isAuthDialogOpen: false,
|
||||
isAuthenticating: false,
|
||||
handleAuthSelect: vi.fn(),
|
||||
handleCodingPlanSubmit: vi.fn(),
|
||||
handleAlibabaStandardSubmit: vi.fn(),
|
||||
openAuthDialog: vi.fn(),
|
||||
cancelAuthentication: vi.fn(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -457,6 +457,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
qwenAuthState,
|
||||
handleAuthSelect,
|
||||
handleCodingPlanSubmit,
|
||||
handleAlibabaStandardSubmit,
|
||||
openAuthDialog,
|
||||
cancelAuthentication,
|
||||
} = useAuthCommand(settings, config, historyManager.addItem, refreshStatic);
|
||||
|
|
@ -1691,6 +1692,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
onAuthError,
|
||||
cancelAuthentication,
|
||||
handleCodingPlanSubmit,
|
||||
handleAlibabaStandardSubmit,
|
||||
handleEditorSelect,
|
||||
exitEditorDialog,
|
||||
closeSettingsDialog,
|
||||
|
|
@ -1748,6 +1750,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
onAuthError,
|
||||
cancelAuthentication,
|
||||
handleCodingPlanSubmit,
|
||||
handleAlibabaStandardSubmit,
|
||||
handleEditorSelect,
|
||||
exitEditorDialog,
|
||||
closeSettingsDialog,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ const createMockUIActions = (overrides: Partial<UIActions> = {}): UIActions => {
|
|||
// AuthDialog only uses handleAuthSelect
|
||||
const baseActions = {
|
||||
handleAuthSelect: vi.fn(),
|
||||
handleCodingPlanSubmit: vi.fn(),
|
||||
handleAlibabaStandardSubmit: vi.fn(),
|
||||
onAuthError: vi.fn(),
|
||||
handleRetryLastPrompt: vi.fn(),
|
||||
} as Partial<UIActions>;
|
||||
|
||||
|
|
@ -555,4 +558,121 @@ describe('AuthDialog', () => {
|
|||
expect(handleAuthSelect).toHaveBeenCalledWith(undefined);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows API Key subtype menu and opens custom info', async () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
originalSettings: {},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
originalSettings: {
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { stdin, lastFrame, unmount } = renderAuthDialog(settings);
|
||||
await wait();
|
||||
|
||||
// Move from Qwen OAuth -> Coding Plan -> API Key, then enter
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
expect(lastFrame()).toContain('Select API Key Type');
|
||||
expect(lastFrame()).toContain('Alibaba Cloud ModelStudio Standard API Key');
|
||||
expect(lastFrame()).toContain('Custom API Key');
|
||||
|
||||
// Move to Custom API Key and enter
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
expect(lastFrame()).toContain('Custom Configuration');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows Alibaba Cloud ModelStudio Standard API Key region endpoint', async () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
originalSettings: {},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
originalSettings: {
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
originalSettings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { stdin, lastFrame, unmount } = renderAuthDialog(settings, {}, {});
|
||||
await wait();
|
||||
|
||||
// Main -> API Key
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
// API Key type -> Alibaba Cloud ModelStudio Standard API Key (default)
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
// Region -> Singapore
|
||||
stdin.write('\u001B[B');
|
||||
stdin.write('\r');
|
||||
await wait();
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'Enter Alibaba Cloud ModelStudio Standard API Key',
|
||||
);
|
||||
expect(lastFrame()).toContain(
|
||||
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { theme } from '../semantic-colors.js';
|
|||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { DescriptiveRadioButtonSelect } from '../components/shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ApiKeyInput } from '../components/ApiKeyInput.js';
|
||||
import { TextInput } from '../components/shared/TextInput.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
|
|
@ -21,6 +22,10 @@ import {
|
|||
CodingPlanRegion,
|
||||
isCodingPlanConfig,
|
||||
} from '../../constants/codingPlan.js';
|
||||
import {
|
||||
ALIBABA_STANDARD_API_KEY_ENDPOINTS,
|
||||
type AlibabaStandardRegion,
|
||||
} from '../../constants/alibabaStandardApiKey.js';
|
||||
|
||||
const MODEL_PROVIDERS_DOCUMENTATION_URL =
|
||||
'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/';
|
||||
|
|
@ -39,15 +44,39 @@ function parseDefaultAuthType(
|
|||
|
||||
// Main menu option type
|
||||
type MainOption = typeof AuthType.QWEN_OAUTH | 'CODING_PLAN' | 'API_KEY';
|
||||
type ApiKeyOption = 'ALIBABA_STANDARD_API_KEY' | 'CUSTOM_API_KEY';
|
||||
|
||||
// View level for navigation
|
||||
type ViewLevel = 'main' | 'region-select' | 'api-key-input' | 'custom-info';
|
||||
type ViewLevel =
|
||||
| 'main'
|
||||
| 'region-select'
|
||||
| 'api-key-input'
|
||||
| 'api-key-type-select'
|
||||
| 'alibaba-standard-region-select'
|
||||
| 'alibaba-standard-api-key-input'
|
||||
| 'alibaba-standard-model-id-input'
|
||||
| 'custom-info';
|
||||
|
||||
const ALIBABA_STANDARD_MODEL_IDS_PLACEHOLDER = 'qwen3.5-plus,glm-5,kimi-k2.5';
|
||||
const ALIBABA_STANDARD_API_DOCUMENTATION_URLS: Record<
|
||||
AlibabaStandardRegion,
|
||||
string
|
||||
> = {
|
||||
'cn-beijing': 'https://bailian.console.aliyun.com/cn-beijing?tab=api#/api',
|
||||
'sg-singapore':
|
||||
'https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=api#/api/?type=model&url=2712195',
|
||||
'us-virginia':
|
||||
'https://modelstudio.console.alibabacloud.com/us-east-1?tab=api#/api/?type=model&url=2712195',
|
||||
'cn-hongkong':
|
||||
'https://modelstudio.console.alibabacloud.com/cn-hongkong?tab=api#/api/?type=model&url=2712195',
|
||||
};
|
||||
|
||||
export function AuthDialog(): React.JSX.Element {
|
||||
const { pendingAuthType, authError } = useUIState();
|
||||
const {
|
||||
handleAuthSelect: onAuthSelect,
|
||||
handleCodingPlanSubmit,
|
||||
handleAlibabaStandardSubmit,
|
||||
onAuthError,
|
||||
} = useUIActions();
|
||||
const config = useConfig();
|
||||
|
|
@ -58,6 +87,18 @@ export function AuthDialog(): React.JSX.Element {
|
|||
const [region, setRegion] = useState<CodingPlanRegion>(
|
||||
CodingPlanRegion.CHINA,
|
||||
);
|
||||
const [alibabaStandardRegionIndex, setAlibabaStandardRegionIndex] =
|
||||
useState<number>(0);
|
||||
const [apiKeyTypeIndex, setApiKeyTypeIndex] = useState<number>(0);
|
||||
const [alibabaStandardRegion, setAlibabaStandardRegion] =
|
||||
useState<AlibabaStandardRegion>('cn-beijing');
|
||||
const [alibabaStandardApiKey, setAlibabaStandardApiKey] = useState('');
|
||||
const [alibabaStandardApiKeyError, setAlibabaStandardApiKeyError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [alibabaStandardModelId, setAlibabaStandardModelId] = useState('');
|
||||
const [alibabaStandardModelIdError, setAlibabaStandardModelIdError] =
|
||||
useState<string | null>(null);
|
||||
|
||||
// Main authentication entries (flat three-option layout)
|
||||
const mainItems = [
|
||||
|
|
@ -124,21 +165,87 @@ export function AuthDialog(): React.JSX.Element {
|
|||
},
|
||||
];
|
||||
|
||||
const alibabaStandardRegionItems = [
|
||||
{
|
||||
key: 'cn-beijing',
|
||||
title: t('China (Beijing)'),
|
||||
label: t('China (Beijing)'),
|
||||
description: (
|
||||
<Text color={theme.text.secondary}>
|
||||
Endpoint: {ALIBABA_STANDARD_API_KEY_ENDPOINTS['cn-beijing']}
|
||||
</Text>
|
||||
),
|
||||
value: 'cn-beijing' as AlibabaStandardRegion,
|
||||
},
|
||||
{
|
||||
key: 'sg-singapore',
|
||||
title: t('Singapore'),
|
||||
label: t('Singapore'),
|
||||
description: (
|
||||
<Text color={theme.text.secondary}>
|
||||
Endpoint: {ALIBABA_STANDARD_API_KEY_ENDPOINTS['sg-singapore']}
|
||||
</Text>
|
||||
),
|
||||
value: 'sg-singapore' as AlibabaStandardRegion,
|
||||
},
|
||||
{
|
||||
key: 'us-virginia',
|
||||
title: t('US (Virginia)'),
|
||||
label: t('US (Virginia)'),
|
||||
description: (
|
||||
<Text color={theme.text.secondary}>
|
||||
Endpoint: {ALIBABA_STANDARD_API_KEY_ENDPOINTS['us-virginia']}
|
||||
</Text>
|
||||
),
|
||||
value: 'us-virginia' as AlibabaStandardRegion,
|
||||
},
|
||||
{
|
||||
key: 'cn-hongkong',
|
||||
title: t('China (Hong Kong)'),
|
||||
label: t('China (Hong Kong)'),
|
||||
description: (
|
||||
<Text color={theme.text.secondary}>
|
||||
Endpoint: {ALIBABA_STANDARD_API_KEY_ENDPOINTS['cn-hongkong']}
|
||||
</Text>
|
||||
),
|
||||
value: 'cn-hongkong' as AlibabaStandardRegion,
|
||||
},
|
||||
];
|
||||
|
||||
const apiKeyTypeItems = [
|
||||
{
|
||||
key: 'ALIBABA_STANDARD_API_KEY',
|
||||
title: t('Alibaba Cloud ModelStudio Standard API Key'),
|
||||
label: t('Alibaba Cloud ModelStudio Standard API Key'),
|
||||
description: t('Quick setup for Model Studio (China/International)'),
|
||||
value: 'ALIBABA_STANDARD_API_KEY' as ApiKeyOption,
|
||||
},
|
||||
{
|
||||
key: 'CUSTOM_API_KEY',
|
||||
title: t('Custom API Key'),
|
||||
label: t('Custom API Key'),
|
||||
description: t(
|
||||
'For other OpenAI / Anthropic / Gemini-compatible providers',
|
||||
),
|
||||
value: 'CUSTOM_API_KEY' as ApiKeyOption,
|
||||
},
|
||||
];
|
||||
|
||||
// Map an AuthType to the corresponding main menu option.
|
||||
// QWEN_OAUTH maps directly; any other auth type maps to CODING_PLAN only
|
||||
// if the current config actually uses a Coding Plan baseUrl+envKey,
|
||||
// otherwise it maps to API_KEY.
|
||||
// QWEN_OAUTH maps directly; USE_OPENAI maps to:
|
||||
// - CODING_PLAN when current config matches coding plan
|
||||
// - API_KEY for other OpenAI / Anthropic / Gemini-compatible configs
|
||||
const contentGenConfig = config.getContentGeneratorConfig();
|
||||
const isCurrentlyCodingPlan =
|
||||
isCodingPlanConfig(
|
||||
contentGenConfig?.baseUrl,
|
||||
contentGenConfig?.apiKeyEnvKey,
|
||||
) !== false;
|
||||
|
||||
const authTypeToMainOption = (authType: AuthType): MainOption => {
|
||||
if (authType === AuthType.QWEN_OAUTH) return AuthType.QWEN_OAUTH;
|
||||
if (authType === AuthType.USE_OPENAI && isCurrentlyCodingPlan)
|
||||
if (authType === AuthType.USE_OPENAI && isCurrentlyCodingPlan) {
|
||||
return 'CODING_PLAN';
|
||||
}
|
||||
return 'API_KEY';
|
||||
};
|
||||
|
||||
|
|
@ -180,8 +287,7 @@ export function AuthDialog(): React.JSX.Element {
|
|||
}
|
||||
|
||||
if (value === 'API_KEY') {
|
||||
// Navigate directly to custom API key info
|
||||
setViewLevel('custom-info');
|
||||
setViewLevel('api-key-type-select');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +295,20 @@ export function AuthDialog(): React.JSX.Element {
|
|||
await onAuthSelect(value);
|
||||
};
|
||||
|
||||
const handleApiKeyTypeSelect = async (value: ApiKeyOption) => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
||||
if (value === 'ALIBABA_STANDARD_API_KEY') {
|
||||
setAlibabaStandardModelIdError(null);
|
||||
setAlibabaStandardApiKeyError(null);
|
||||
setViewLevel('alibaba-standard-region-select');
|
||||
return;
|
||||
}
|
||||
|
||||
setViewLevel('custom-info');
|
||||
};
|
||||
|
||||
const handleRegionSelect = async (selectedRegion: CodingPlanRegion) => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
|
@ -196,6 +316,17 @@ export function AuthDialog(): React.JSX.Element {
|
|||
setViewLevel('api-key-input');
|
||||
};
|
||||
|
||||
const handleAlibabaStandardRegionSelect = async (
|
||||
selectedRegion: AlibabaStandardRegion,
|
||||
) => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
setAlibabaStandardApiKeyError(null);
|
||||
setAlibabaStandardModelIdError(null);
|
||||
setAlibabaStandardRegion(selectedRegion);
|
||||
setViewLevel('alibaba-standard-api-key-input');
|
||||
};
|
||||
|
||||
const handleApiKeyInputSubmit = async (apiKey: string) => {
|
||||
setErrorMessage(null);
|
||||
|
||||
|
|
@ -208,14 +339,59 @@ export function AuthDialog(): React.JSX.Element {
|
|||
await handleCodingPlanSubmit(apiKey, region);
|
||||
};
|
||||
|
||||
const handleAlibabaStandardApiKeySubmit = () => {
|
||||
const trimmedKey = alibabaStandardApiKey.trim();
|
||||
if (!trimmedKey) {
|
||||
setAlibabaStandardApiKeyError(t('API key cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setAlibabaStandardApiKeyError(null);
|
||||
if (!alibabaStandardModelId.trim()) {
|
||||
setAlibabaStandardModelId(ALIBABA_STANDARD_MODEL_IDS_PLACEHOLDER);
|
||||
}
|
||||
setViewLevel('alibaba-standard-model-id-input');
|
||||
};
|
||||
|
||||
const handleAlibabaStandardModelSubmit = () => {
|
||||
const trimmedApiKey = alibabaStandardApiKey.trim();
|
||||
const trimmedModelIds = alibabaStandardModelId.trim();
|
||||
if (!trimmedApiKey) {
|
||||
setAlibabaStandardApiKeyError(t('API key cannot be empty.'));
|
||||
setViewLevel('alibaba-standard-api-key-input');
|
||||
return;
|
||||
}
|
||||
if (!trimmedModelIds) {
|
||||
setAlibabaStandardModelIdError(t('Model IDs cannot be empty.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setAlibabaStandardModelIdError(null);
|
||||
void handleAlibabaStandardSubmit(
|
||||
trimmedApiKey,
|
||||
alibabaStandardRegion,
|
||||
trimmedModelIds,
|
||||
);
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
setErrorMessage(null);
|
||||
onAuthError(null);
|
||||
|
||||
if (viewLevel === 'region-select' || viewLevel === 'custom-info') {
|
||||
if (viewLevel === 'region-select') {
|
||||
setViewLevel('main');
|
||||
} else if (viewLevel === 'api-key-input') {
|
||||
setViewLevel('region-select');
|
||||
} else if (viewLevel === 'api-key-type-select') {
|
||||
setViewLevel('main');
|
||||
} else if (viewLevel === 'custom-info') {
|
||||
setViewLevel('api-key-type-select');
|
||||
} else if (viewLevel === 'alibaba-standard-region-select') {
|
||||
setViewLevel('api-key-type-select');
|
||||
} else if (viewLevel === 'alibaba-standard-api-key-input') {
|
||||
setViewLevel('alibaba-standard-region-select');
|
||||
} else if (viewLevel === 'alibaba-standard-model-id-input') {
|
||||
setViewLevel('alibaba-standard-api-key-input');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -232,6 +408,15 @@ export function AuthDialog(): React.JSX.Element {
|
|||
handleGoBack();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
viewLevel === 'api-key-type-select' ||
|
||||
viewLevel === 'alibaba-standard-region-select' ||
|
||||
viewLevel === 'alibaba-standard-api-key-input' ||
|
||||
viewLevel === 'alibaba-standard-model-id-input'
|
||||
) {
|
||||
handleGoBack();
|
||||
return;
|
||||
}
|
||||
|
||||
// For main view, use existing logic
|
||||
if (errorMessage) {
|
||||
|
|
@ -304,6 +489,135 @@ export function AuthDialog(): React.JSX.Element {
|
|||
</Box>
|
||||
);
|
||||
|
||||
const renderApiKeyTypeSelectView = () => (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={apiKeyTypeItems}
|
||||
initialIndex={apiKeyTypeIndex}
|
||||
onSelect={handleApiKeyTypeSelect}
|
||||
onHighlight={(value) => {
|
||||
const index = apiKeyTypeItems.findIndex(
|
||||
(item) => item.value === value,
|
||||
);
|
||||
setApiKeyTypeIndex(index);
|
||||
}}
|
||||
itemGap={1}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t('Enter to select, ↑↓ to navigate, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderAlibabaStandardRegionSelectView = () => (
|
||||
<>
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={alibabaStandardRegionItems}
|
||||
initialIndex={alibabaStandardRegionIndex}
|
||||
onSelect={handleAlibabaStandardRegionSelect}
|
||||
onHighlight={(value) => {
|
||||
const index = alibabaStandardRegionItems.findIndex(
|
||||
(item) => item.value === value,
|
||||
);
|
||||
setAlibabaStandardRegionIndex(index);
|
||||
}}
|
||||
itemGap={1}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme?.text?.secondary}>
|
||||
{t('Enter to select, ↑↓ to navigate, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderAlibabaStandardApiKeyInputView = () => (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
Endpoint: {ALIBABA_STANDARD_API_KEY_ENDPOINTS[alibabaStandardRegion]}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>{t('Documentation')}:</Text>
|
||||
</Box>
|
||||
<Box marginTop={0}>
|
||||
<Link
|
||||
url={ALIBABA_STANDARD_API_DOCUMENTATION_URLS[alibabaStandardRegion]}
|
||||
fallback={false}
|
||||
>
|
||||
<Text color={theme.text.link}>
|
||||
{ALIBABA_STANDARD_API_DOCUMENTATION_URLS[alibabaStandardRegion]}
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<TextInput
|
||||
value={alibabaStandardApiKey}
|
||||
onChange={(value) => {
|
||||
setAlibabaStandardApiKey(value);
|
||||
if (alibabaStandardApiKeyError) {
|
||||
setAlibabaStandardApiKeyError(null);
|
||||
}
|
||||
}}
|
||||
onSubmit={handleAlibabaStandardApiKeySubmit}
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</Box>
|
||||
{alibabaStandardApiKeyError && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.status.error}>{alibabaStandardApiKeyError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to submit, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderAlibabaStandardModelIdInputView = () => (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t(
|
||||
'You can enter multiple model IDs, separated by commas. Examples: qwen3.5-plus,glm-5,kimi-k2.5',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<TextInput
|
||||
value={alibabaStandardModelId}
|
||||
onChange={(value) => {
|
||||
setAlibabaStandardModelId(value);
|
||||
if (alibabaStandardModelIdError) {
|
||||
setAlibabaStandardModelIdError(null);
|
||||
}
|
||||
}}
|
||||
onSubmit={handleAlibabaStandardModelSubmit}
|
||||
placeholder={ALIBABA_STANDARD_MODEL_IDS_PLACEHOLDER}
|
||||
/>
|
||||
</Box>
|
||||
{alibabaStandardModelIdError && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.status.error}>{alibabaStandardModelIdError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Enter to submit, Esc to go back')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Render custom mode info
|
||||
const renderCustomInfoView = () => (
|
||||
<>
|
||||
|
|
@ -336,8 +650,18 @@ export function AuthDialog(): React.JSX.Element {
|
|||
return t('Select Region for Coding Plan');
|
||||
case 'api-key-input':
|
||||
return t('Enter Coding Plan API Key');
|
||||
case 'api-key-type-select':
|
||||
return t('Select API Key Type');
|
||||
case 'custom-info':
|
||||
return t('Custom Configuration');
|
||||
case 'alibaba-standard-region-select':
|
||||
return t(
|
||||
'Select Region for Alibaba Cloud ModelStudio Standard API Key',
|
||||
);
|
||||
case 'alibaba-standard-api-key-input':
|
||||
return t('Enter Alibaba Cloud ModelStudio Standard API Key');
|
||||
case 'alibaba-standard-model-id-input':
|
||||
return t('Enter Model IDs');
|
||||
default:
|
||||
return t('Select Authentication Method');
|
||||
}
|
||||
|
|
@ -356,6 +680,13 @@ export function AuthDialog(): React.JSX.Element {
|
|||
{viewLevel === 'main' && renderMainView()}
|
||||
{viewLevel === 'region-select' && renderRegionSelectView()}
|
||||
{viewLevel === 'api-key-input' && renderApiKeyInputView()}
|
||||
{viewLevel === 'api-key-type-select' && renderApiKeyTypeSelectView()}
|
||||
{viewLevel === 'alibaba-standard-region-select' &&
|
||||
renderAlibabaStandardRegionSelectView()}
|
||||
{viewLevel === 'alibaba-standard-api-key-input' &&
|
||||
renderAlibabaStandardApiKeyInputView()}
|
||||
{viewLevel === 'alibaba-standard-model-id-input' &&
|
||||
renderAlibabaStandardModelIdInputView()}
|
||||
{viewLevel === 'custom-info' && renderCustomInfoView()}
|
||||
|
||||
{(authError || errorMessage) && (
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ import {
|
|||
CODING_PLAN_ENV_KEY,
|
||||
} from '../../constants/codingPlan.js';
|
||||
import { backupSettingsFile } from '../../utils/settingsUtils.js';
|
||||
import {
|
||||
ALIBABA_STANDARD_API_KEY_ENDPOINTS,
|
||||
DASHSCOPE_STANDARD_API_KEY_ENV_KEY,
|
||||
type AlibabaStandardRegion,
|
||||
} from '../../constants/alibabaStandardApiKey.js';
|
||||
|
||||
export type { QwenAuthState } from '../hooks/useQwenAuth.js';
|
||||
|
||||
|
|
@ -421,6 +426,134 @@ export const useAuthCommand = (
|
|||
[settings, config, handleAuthFailure, addItem, onAuthChange],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle Alibaba Cloud standard API key flow.
|
||||
* Persists key to env.DASHSCOPE_API_KEY and creates a modelProviders.openai entry.
|
||||
*/
|
||||
const handleAlibabaStandardSubmit = useCallback(
|
||||
async (
|
||||
apiKey: string,
|
||||
region: AlibabaStandardRegion,
|
||||
modelIdsInput: string,
|
||||
) => {
|
||||
try {
|
||||
setIsAuthenticating(true);
|
||||
setAuthError(null);
|
||||
|
||||
const trimmedApiKey = apiKey.trim();
|
||||
const modelIds = modelIdsInput
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(
|
||||
(id, index, array) => id.length > 0 && array.indexOf(id) === index,
|
||||
);
|
||||
if (!trimmedApiKey) {
|
||||
throw new Error(t('API key cannot be empty.'));
|
||||
}
|
||||
if (modelIds.length === 0) {
|
||||
throw new Error(t('Model IDs cannot be empty.'));
|
||||
}
|
||||
|
||||
const baseUrl = ALIBABA_STANDARD_API_KEY_ENDPOINTS[region];
|
||||
const persistScope = getPersistScopeForModelSelection(settings);
|
||||
|
||||
const settingsFile = settings.forScope(persistScope);
|
||||
backupSettingsFile(settingsFile.path);
|
||||
|
||||
settings.setValue(
|
||||
persistScope,
|
||||
`env.${DASHSCOPE_STANDARD_API_KEY_ENV_KEY}`,
|
||||
trimmedApiKey,
|
||||
);
|
||||
process.env[DASHSCOPE_STANDARD_API_KEY_ENV_KEY] = trimmedApiKey;
|
||||
|
||||
const newConfigs: ProviderModelConfig[] = modelIds.map((modelId) => ({
|
||||
id: modelId,
|
||||
name: `[ModelStudio Standard] ${modelId}`,
|
||||
baseUrl,
|
||||
envKey: DASHSCOPE_STANDARD_API_KEY_ENV_KEY,
|
||||
}));
|
||||
|
||||
const existingConfigs =
|
||||
(
|
||||
settings.merged.modelProviders as ModelProvidersConfig | undefined
|
||||
)?.[AuthType.USE_OPENAI] || [];
|
||||
|
||||
const nonAlibabaStandardConfigs = existingConfigs.filter(
|
||||
(existing) =>
|
||||
!(
|
||||
existing.envKey === DASHSCOPE_STANDARD_API_KEY_ENV_KEY &&
|
||||
typeof existing.baseUrl === 'string' &&
|
||||
Object.values(ALIBABA_STANDARD_API_KEY_ENDPOINTS).includes(
|
||||
existing.baseUrl,
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
const updatedConfigs = [...newConfigs, ...nonAlibabaStandardConfigs];
|
||||
|
||||
settings.setValue(
|
||||
persistScope,
|
||||
`modelProviders.${AuthType.USE_OPENAI}`,
|
||||
updatedConfigs,
|
||||
);
|
||||
settings.setValue(
|
||||
persistScope,
|
||||
'security.auth.selectedType',
|
||||
AuthType.USE_OPENAI,
|
||||
);
|
||||
settings.setValue(persistScope, 'model.name', modelIds[0]);
|
||||
|
||||
const updatedModelProviders: ModelProvidersConfig = {
|
||||
...(settings.merged.modelProviders as
|
||||
| ModelProvidersConfig
|
||||
| undefined),
|
||||
[AuthType.USE_OPENAI]: updatedConfigs,
|
||||
};
|
||||
config.reloadModelProvidersConfig(updatedModelProviders);
|
||||
await config.refreshAuth(AuthType.USE_OPENAI);
|
||||
|
||||
setAuthError(null);
|
||||
setAuthState(AuthState.Authenticated);
|
||||
setPendingAuthType(undefined);
|
||||
setIsAuthDialogOpen(false);
|
||||
setIsAuthenticating(false);
|
||||
onAuthChange?.();
|
||||
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t(
|
||||
'Alibaba Cloud ModelStudio Standard API Key successfully entered. Settings updated with env.DASHSCOPE_API_KEY and {{modelCount}} model(s).',
|
||||
{ modelCount: String(modelIds.length) },
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t(
|
||||
'You can use /model to see new ModelStudio Standard models and switch between them.',
|
||||
),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
const authEvent = new AuthEvent(
|
||||
AuthType.USE_OPENAI,
|
||||
'manual',
|
||||
'success',
|
||||
);
|
||||
logAuth(config, authEvent);
|
||||
} catch (error) {
|
||||
handleAuthFailure(error);
|
||||
}
|
||||
},
|
||||
[settings, config, handleAuthFailure, addItem, onAuthChange],
|
||||
);
|
||||
|
||||
/**
|
||||
/**
|
||||
* We previously used a useEffect to trigger authentication automatically when
|
||||
|
|
@ -472,6 +605,7 @@ export const useAuthCommand = (
|
|||
qwenAuthState,
|
||||
handleAuthSelect,
|
||||
handleCodingPlanSubmit,
|
||||
handleAlibabaStandardSubmit,
|
||||
openAuthDialog,
|
||||
cancelAuthentication,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '@qwen-code/qwen-code-core';
|
||||
import { type SettingScope } from '../../config/settings.js';
|
||||
import { type CodingPlanRegion } from '../../constants/codingPlan.js';
|
||||
import { type AlibabaStandardRegion } from '../../constants/alibabaStandardApiKey.js';
|
||||
import type { AuthState } from '../types.js';
|
||||
import { type ArenaDialogType } from '../hooks/useArenaCommand.js';
|
||||
// OpenAICredentials type (previously imported from OpenAIKeyPrompt)
|
||||
|
|
@ -45,6 +46,11 @@ export interface UIActions {
|
|||
apiKey: string,
|
||||
region?: CodingPlanRegion,
|
||||
) => Promise<void>;
|
||||
handleAlibabaStandardSubmit: (
|
||||
apiKey: string,
|
||||
region: AlibabaStandardRegion,
|
||||
modelIdsInput: string,
|
||||
) => Promise<void>;
|
||||
setAuthState: (state: AuthState) => void;
|
||||
onAuthError: (error: string | null) => void;
|
||||
cancelAuthentication: () => void;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue