mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Merge branch 'main' into feature/stream-json-migration
This commit is contained in:
@@ -43,6 +43,7 @@ import { mcpCommand } from '../commands/mcp.js';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { buildWebSearchConfig } from './webSearch.js';
|
||||
|
||||
// Simple console logger for now - replace with actual logger if available
|
||||
const logger = {
|
||||
@@ -114,9 +115,13 @@ export interface CliArgs {
|
||||
openaiLogging: boolean | undefined;
|
||||
openaiApiKey: string | undefined;
|
||||
openaiBaseUrl: string | undefined;
|
||||
openaiLoggingDir: string | undefined;
|
||||
proxy: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
tavilyApiKey: string | undefined;
|
||||
googleApiKey: string | undefined;
|
||||
googleSearchEngineId: string | undefined;
|
||||
webSearchDefault: string | undefined;
|
||||
screenReader: boolean | undefined;
|
||||
vlmSwitchMode: string | undefined;
|
||||
useSmartEdit: boolean | undefined;
|
||||
@@ -331,6 +336,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
description:
|
||||
'Enable logging of OpenAI API calls for debugging and analysis',
|
||||
})
|
||||
.option('openai-logging-dir', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Custom directory path for OpenAI API logs. Overrides settings files.',
|
||||
})
|
||||
.option('openai-api-key', {
|
||||
type: 'string',
|
||||
description: 'OpenAI API key to use for authentication',
|
||||
@@ -341,7 +351,20 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
})
|
||||
.option('tavily-api-key', {
|
||||
type: 'string',
|
||||
description: 'Tavily API key for web search functionality',
|
||||
description: 'Tavily API key for web search',
|
||||
})
|
||||
.option('google-api-key', {
|
||||
type: 'string',
|
||||
description: 'Google Custom Search API key',
|
||||
})
|
||||
.option('google-search-engine-id', {
|
||||
type: 'string',
|
||||
description: 'Google Custom Search Engine ID',
|
||||
})
|
||||
.option('web-search-default', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Default web search provider (dashscope, tavily, google)',
|
||||
})
|
||||
.option('screen-reader', {
|
||||
type: 'boolean',
|
||||
@@ -822,12 +845,15 @@ export async function loadCliConfig(
|
||||
(typeof argv.openaiLogging === 'undefined'
|
||||
? settings.model?.enableOpenAILogging
|
||||
: argv.openaiLogging) ?? false,
|
||||
openAILoggingDir:
|
||||
argv.openaiLoggingDir || settings.model?.openAILoggingDir,
|
||||
},
|
||||
cliVersion: await getCliVersion(),
|
||||
tavilyApiKey:
|
||||
argv.tavilyApiKey ||
|
||||
settings.advanced?.tavilyApiKey ||
|
||||
process.env['TAVILY_API_KEY'],
|
||||
webSearch: buildWebSearchConfig(
|
||||
argv,
|
||||
settings,
|
||||
settings.security?.auth?.selectedType,
|
||||
),
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
chatCompression: settings.model?.chatCompression,
|
||||
|
||||
@@ -558,6 +558,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable OpenAI logging.',
|
||||
showInDialog: true,
|
||||
},
|
||||
openAILoggingDir: {
|
||||
type: 'string',
|
||||
label: 'OpenAI Logging Directory',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'Custom directory path for OpenAI API logs. If not specified, defaults to logs/openai in the current working directory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
generationConfig: {
|
||||
type: 'object',
|
||||
label: 'Generation Configuration',
|
||||
@@ -1072,17 +1082,36 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
tavilyApiKey: {
|
||||
type: 'string',
|
||||
label: 'Tavily API Key',
|
||||
label: 'Tavily API Key (Deprecated)',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'The API key for the Tavily API. Required to enable the web_search tool functionality.',
|
||||
'⚠️ DEPRECATED: Please use webSearch.provider configuration instead. Legacy API key for the Tavily API.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
webSearch: {
|
||||
type: 'object',
|
||||
label: 'Web Search',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as
|
||||
| {
|
||||
provider: Array<{
|
||||
type: 'tavily' | 'google' | 'dashscope';
|
||||
apiKey?: string;
|
||||
searchEngineId?: string;
|
||||
}>;
|
||||
default: string;
|
||||
}
|
||||
| undefined,
|
||||
description: 'Configuration for web search providers.',
|
||||
showInDialog: false,
|
||||
},
|
||||
|
||||
experimental: {
|
||||
type: 'object',
|
||||
label: 'Experimental',
|
||||
|
||||
121
packages/cli/src/config/webSearch.ts
Normal file
121
packages/cli/src/config/webSearch.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import type { WebSearchProviderConfig } from '@qwen-code/qwen-code-core';
|
||||
import type { Settings } from './settings.js';
|
||||
|
||||
/**
|
||||
* CLI arguments related to web search configuration
|
||||
*/
|
||||
export interface WebSearchCliArgs {
|
||||
tavilyApiKey?: string;
|
||||
googleApiKey?: string;
|
||||
googleSearchEngineId?: string;
|
||||
webSearchDefault?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web search configuration structure
|
||||
*/
|
||||
export interface WebSearchConfig {
|
||||
provider: WebSearchProviderConfig[];
|
||||
default: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build webSearch configuration from multiple sources with priority:
|
||||
* 1. settings.json (new format) - highest priority
|
||||
* 2. Command line args + environment variables
|
||||
* 3. Legacy tavilyApiKey (backward compatibility)
|
||||
*
|
||||
* @param argv - Command line arguments
|
||||
* @param settings - User settings from settings.json
|
||||
* @param authType - Authentication type (e.g., 'qwen-oauth')
|
||||
* @returns WebSearch configuration or undefined if no providers available
|
||||
*/
|
||||
export function buildWebSearchConfig(
|
||||
argv: WebSearchCliArgs,
|
||||
settings: Settings,
|
||||
authType?: string,
|
||||
): WebSearchConfig | undefined {
|
||||
const isQwenOAuth = authType === AuthType.QWEN_OAUTH;
|
||||
|
||||
// Step 1: Collect providers from settings or command line/env
|
||||
let providers: WebSearchProviderConfig[] = [];
|
||||
let userDefault: string | undefined;
|
||||
|
||||
if (settings.webSearch) {
|
||||
// Use providers from settings.json
|
||||
providers = [...settings.webSearch.provider];
|
||||
userDefault = settings.webSearch.default;
|
||||
} else {
|
||||
// Build providers from command line args and environment variables
|
||||
const tavilyKey =
|
||||
argv.tavilyApiKey ||
|
||||
settings.advanced?.tavilyApiKey ||
|
||||
process.env['TAVILY_API_KEY'];
|
||||
if (tavilyKey) {
|
||||
providers.push({
|
||||
type: 'tavily',
|
||||
apiKey: tavilyKey,
|
||||
} as WebSearchProviderConfig);
|
||||
}
|
||||
|
||||
const googleKey = argv.googleApiKey || process.env['GOOGLE_API_KEY'];
|
||||
const googleEngineId =
|
||||
argv.googleSearchEngineId || process.env['GOOGLE_SEARCH_ENGINE_ID'];
|
||||
if (googleKey && googleEngineId) {
|
||||
providers.push({
|
||||
type: 'google',
|
||||
apiKey: googleKey,
|
||||
searchEngineId: googleEngineId,
|
||||
} as WebSearchProviderConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Ensure dashscope is available for qwen-oauth users
|
||||
if (isQwenOAuth) {
|
||||
const hasDashscope = providers.some((p) => p.type === 'dashscope');
|
||||
if (!hasDashscope) {
|
||||
providers.push({ type: 'dashscope' } as WebSearchProviderConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: If no providers available, return undefined
|
||||
if (providers.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Step 4: Determine default provider
|
||||
// Priority: user explicit config > CLI arg > first available provider (tavily > google > dashscope)
|
||||
const providerPriority: Array<'tavily' | 'google' | 'dashscope'> = [
|
||||
'tavily',
|
||||
'google',
|
||||
'dashscope',
|
||||
];
|
||||
|
||||
// Determine default provider based on availability
|
||||
let defaultProvider = userDefault || argv.webSearchDefault;
|
||||
if (!defaultProvider) {
|
||||
// Find first available provider by priority order
|
||||
for (const providerType of providerPriority) {
|
||||
if (providers.some((p) => p.type === providerType)) {
|
||||
defaultProvider = providerType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback to first available provider if none found in priority list
|
||||
if (!defaultProvider) {
|
||||
defaultProvider = providers[0]?.type || 'dashscope';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
provider: providers,
|
||||
default: defaultProvider,
|
||||
};
|
||||
}
|
||||
@@ -469,9 +469,13 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
openaiLogging: undefined,
|
||||
openaiApiKey: undefined,
|
||||
openaiBaseUrl: undefined,
|
||||
openaiLoggingDir: undefined,
|
||||
proxy: undefined,
|
||||
includeDirectories: undefined,
|
||||
tavilyApiKey: undefined,
|
||||
googleApiKey: undefined,
|
||||
googleSearchEngineId: undefined,
|
||||
webSearchDefault: undefined,
|
||||
screenReader: undefined,
|
||||
vlmSwitchMode: undefined,
|
||||
useSmartEdit: undefined,
|
||||
|
||||
@@ -1227,4 +1227,28 @@ describe('FileCommandLoader', () => {
|
||||
expect(commands).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AbortError handling', () => {
|
||||
it('should silently ignore AbortError when operation is cancelled', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
mock({
|
||||
[userCommandsDir]: {
|
||||
'test1.toml': 'prompt = "Prompt 1"',
|
||||
'test2.toml': 'prompt = "Prompt 2"',
|
||||
},
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(null);
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
// Start loading and immediately abort
|
||||
const loadPromise = loader.loadCommands(signal);
|
||||
controller.abort();
|
||||
|
||||
// Should not throw or print errors
|
||||
const commands = await loadPromise;
|
||||
expect(commands).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +120,11 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
// Add all commands without deduplication
|
||||
allCommands.push(...commands);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// Ignore ENOENT (directory doesn't exist) and AbortError (operation was cancelled)
|
||||
const isEnoent = (error as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
const isAbortError =
|
||||
error instanceof Error && error.name === 'AbortError';
|
||||
if (!isEnoent && !isAbortError) {
|
||||
console.error(
|
||||
`[FileCommandLoader] Error loading commands from ${dirInfo.path}:`,
|
||||
error,
|
||||
|
||||
@@ -916,17 +916,9 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
(result: IdeIntegrationNudgeResult) => {
|
||||
if (result.userSelection === 'yes') {
|
||||
handleSlashCommand('/ide install');
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'hasSeenIdeIntegrationNudge',
|
||||
true,
|
||||
);
|
||||
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
|
||||
} else if (result.userSelection === 'dismiss') {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'hasSeenIdeIntegrationNudge',
|
||||
true,
|
||||
);
|
||||
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
|
||||
}
|
||||
setIdePromptAnswered(true);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user