fix: enhance 429 error handling and fix failed cases

This commit is contained in:
mingholy.lmh
2025-12-01 16:18:18 +08:00
parent 3056f8a63d
commit 50e3a6ee0a
7 changed files with 68 additions and 43 deletions

View File

@@ -213,7 +213,7 @@ describe('simple-mcp-server', () => {
it('should add two numbers', async () => { it('should add two numbers', async () => {
// Test directory is already set up in before hook // Test directory is already set up in before hook
// Just run the command - MCP server config is in settings.json // Just run the command - MCP server config is in settings.json
const output = await rig.run('add 5 and 10'); const output = await rig.run('add 5 and 10, use tool if you can.');
const foundToolCall = await rig.waitForToolCall('add'); const foundToolCall = await rig.waitForToolCall('add');

View File

@@ -19,6 +19,8 @@ import type {
CLIControlInitializeRequest, CLIControlInitializeRequest,
CLIControlSetModelRequest, CLIControlSetModelRequest,
} from '../../types.js'; } from '../../types.js';
import { CommandService } from '../../../services/CommandService.js';
import { BuiltinCommandLoader } from '../../../services/BuiltinCommandLoader.js';
export class SystemController extends BaseController { export class SystemController extends BaseController {
/** /**
@@ -141,31 +143,10 @@ export class SystemController extends BaseController {
can_set_permission_mode: can_set_permission_mode:
typeof this.context.config.setApprovalMode === 'function', typeof this.context.config.setApprovalMode === 'function',
can_set_model: typeof this.context.config.setModel === 'function', can_set_model: typeof this.context.config.setModel === 'function',
/* TODO: sdkMcpServers support */
can_handle_mcp_message: false,
}; };
// Check if MCP message handling is available
try {
const mcpProvider = this.context.config as unknown as {
getMcpServers?: () => Record<string, unknown> | undefined;
};
if (typeof mcpProvider.getMcpServers === 'function') {
const servers = mcpProvider.getMcpServers();
capabilities['can_handle_mcp_message'] = Boolean(
servers && Object.keys(servers).length > 0,
);
} else {
capabilities['can_handle_mcp_message'] = false;
}
} catch (error) {
if (this.context.debugMode) {
console.error(
'[SystemController] Failed to determine MCP capability:',
error,
);
}
capabilities['can_handle_mcp_message'] = false;
}
return capabilities; return capabilities;
} }
@@ -240,27 +221,45 @@ export class SystemController extends BaseController {
/** /**
* Handle supported_commands request * Handle supported_commands request
* *
* Returns list of supported control commands * Returns list of supported slash commands loaded dynamically
*
* Note: This list should match the ControlRequestType enum in
* packages/sdk/typescript/src/types/controlRequests.ts
*/ */
private async handleSupportedCommands(): Promise<Record<string, unknown>> { private async handleSupportedCommands(): Promise<Record<string, unknown>> {
const commands = [ const slashCommands = await this.loadSlashCommandNames();
'initialize',
'interrupt',
'set_model',
'supported_commands',
'can_use_tool',
'set_permission_mode',
'mcp_message',
'mcp_server_status',
'hook_callback',
];
return { return {
subtype: 'supported_commands', subtype: 'supported_commands',
commands, commands: slashCommands,
}; };
} }
/**
* Load slash command names using CommandService
*
* @returns Promise resolving to array of slash command names
*/
private async loadSlashCommandNames(): Promise<string[]> {
const controller = new AbortController();
try {
const service = await CommandService.create(
[new BuiltinCommandLoader(this.context.config)],
controller.signal,
);
const names = new Set<string>();
const commands = service.getCommands();
for (const command of commands) {
names.add(command.name);
}
return Array.from(names).sort();
} catch (error) {
if (this.context.debugMode) {
console.error(
'[SystemController] Failed to load slash commands:',
error,
);
}
return [];
} finally {
controller.abort();
}
}
} }

View File

@@ -13,7 +13,11 @@ import type {
ServerGeminiStreamEvent, ServerGeminiStreamEvent,
TaskResultDisplay, TaskResultDisplay,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { GeminiEventType, ToolErrorType } from '@qwen-code/qwen-code-core'; import {
GeminiEventType,
ToolErrorType,
parseAndFormatApiError,
} from '@qwen-code/qwen-code-core';
import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai'; import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai';
import type { import type {
CLIAssistantMessage, CLIAssistantMessage,
@@ -600,6 +604,18 @@ export abstract class BaseJsonOutputAdapter {
} }
this.finalizePendingBlocks(state, null); this.finalizePendingBlocks(state, null);
break; break;
case GeminiEventType.Error: {
// Format the error message using parseAndFormatApiError for consistency
// with interactive mode error display
const errorText = parseAndFormatApiError(
event.value.error,
this.config.getContentGeneratorConfig()?.authType,
undefined,
this.config.getModel(),
);
this.appendText(state, errorText, null);
break;
}
default: default:
break; break;
} }

View File

@@ -69,6 +69,7 @@ function createConfig(overrides: ConfigOverrides = {}): Config {
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => 'auto', getApprovalMode: () => 'auto',
getOutputFormat: () => 'stream-json', getOutputFormat: () => 'stream-json',
initialize: vi.fn(),
}; };
return { ...base, ...overrides } as unknown as Config; return { ...base, ...overrides } as unknown as Config;
} }

View File

@@ -17,6 +17,7 @@ import {
OutputFormat, OutputFormat,
InputFormat, InputFormat,
uiTelemetryService, uiTelemetryService,
parseAndFormatApiError,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import type { Content, Part, PartListUnion } from '@google/genai'; import type { Content, Part, PartListUnion } from '@google/genai';
import type { CLIUserMessage, PermissionMode } from './nonInteractive/types.js'; import type { CLIUserMessage, PermissionMode } from './nonInteractive/types.js';
@@ -210,6 +211,15 @@ export async function runNonInteractive(
process.stdout.write(event.value); process.stdout.write(event.value);
} else if (event.type === GeminiEventType.ToolCallRequest) { } else if (event.type === GeminiEventType.ToolCallRequest) {
toolCallRequests.push(event.value); toolCallRequests.push(event.value);
} else if (event.type === GeminiEventType.Error) {
// Format and output the error message for text mode
const errorText = parseAndFormatApiError(
event.value.error,
config.getContentGeneratorConfig()?.authType,
undefined,
config.getModel(),
);
process.stderr.write(`${errorText}\n`);
} }
} }
} }

View File

@@ -24,7 +24,7 @@ execSync('tsc --project tsconfig.build.json', {
try { try {
execSync( execSync(
'npx dts-bundle-generator --project tsconfig.build.json -o dist/index.d.ts src/index.ts --no-check', 'npx dts-bundle-generator --project tsconfig.build.json -o dist/index.d.ts src/index.ts',
{ {
stdio: 'inherit', stdio: 'inherit',
cwd: rootDir, cwd: rootDir,

View File

@@ -38,7 +38,6 @@ export default defineConfig({
}, },
testTimeout: testTimeoutMs, testTimeout: testTimeoutMs,
hookTimeout: 10000, hookTimeout: 10000,
globalSetup: './test/e2e/globalSetup.ts',
}, },
resolve: { resolve: {
alias: { alias: {