Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions[bot]
d0fac54711 chore(release): v0.7.2 2026-01-20 09:10:43 +00:00
Mingholy
6eb16c0bcf Merge pull request #1548 from QwenLM/mingholy/fix/qwen-oauth-model-info
Fix: Update Qwen OAuth model information
2026-01-20 16:16:30 +08:00
tanzhenxin
7fa1dcb0e6 Merge pull request #1550 from QwenLM/refactor/acp-error-codes
fix(acp): propagate ENOENT errors correctly and centralize error codes
2026-01-20 16:03:16 +08:00
tanzhenxin
3c68a9a5f6 test(acp): update filesystem tests for error code-based ENOENT handling 2026-01-20 15:40:09 +08:00
tanzhenxin
bdfeec24fb refactor(acp): centralize error codes and add RESOURCE_NOT_FOUND handling for file operations 2026-01-20 15:19:18 +08:00
mingholy.lmh
03f12bfa3f fix: update qwen-oauth models info 2026-01-20 15:11:11 +08:00
17 changed files with 211 additions and 72 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.7.1",
"version": "0.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.7.1",
"version": "0.7.2",
"workspaces": [
"packages/*"
],
@@ -17310,7 +17310,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.7.1",
"version": "0.7.2",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17947,7 +17947,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.7.1",
"version": "0.7.2",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21408,7 +21408,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.7.1",
"version": "0.7.2",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21420,7 +21420,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.7.1",
"version": "0.7.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.7.1",
"version": "0.7.2",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.2"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.7.1",
"version": "0.7.2",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.2"
},
"dependencies": {
"@google/genai": "1.30.0",

View File

@@ -8,6 +8,7 @@
import { z } from 'zod';
import * as schema from './schema.js';
import { ACP_ERROR_CODES } from './errorCodes.js';
export * from './schema.js';
import type { WritableStream, ReadableStream } from 'node:stream/web';
@@ -349,27 +350,51 @@ export class RequestError extends Error {
}
static parseError(details?: string): RequestError {
return new RequestError(-32700, 'Parse error', details);
return new RequestError(
ACP_ERROR_CODES.PARSE_ERROR,
'Parse error',
details,
);
}
static invalidRequest(details?: string): RequestError {
return new RequestError(-32600, 'Invalid request', details);
return new RequestError(
ACP_ERROR_CODES.INVALID_REQUEST,
'Invalid request',
details,
);
}
static methodNotFound(details?: string): RequestError {
return new RequestError(-32601, 'Method not found', details);
return new RequestError(
ACP_ERROR_CODES.METHOD_NOT_FOUND,
'Method not found',
details,
);
}
static invalidParams(details?: string): RequestError {
return new RequestError(-32602, 'Invalid params', details);
return new RequestError(
ACP_ERROR_CODES.INVALID_PARAMS,
'Invalid params',
details,
);
}
static internalError(details?: string): RequestError {
return new RequestError(-32603, 'Internal error', details);
return new RequestError(
ACP_ERROR_CODES.INTERNAL_ERROR,
'Internal error',
details,
);
}
static authRequired(details?: string): RequestError {
return new RequestError(-32000, 'Authentication required', details);
return new RequestError(
ACP_ERROR_CODES.AUTH_REQUIRED,
'Authentication required',
details,
);
}
toResult<T>(): Result<T> {

View File

@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export const ACP_ERROR_CODES = {
// Parse error: invalid JSON received by server.
PARSE_ERROR: -32700,
// Invalid request: JSON is not a valid Request object.
INVALID_REQUEST: -32600,
// Method not found: method does not exist or is unavailable.
METHOD_NOT_FOUND: -32601,
// Invalid params: invalid method parameter(s).
INVALID_PARAMS: -32602,
// Internal error: implementation-defined server error.
INTERNAL_ERROR: -32603,
// Authentication required: must authenticate before operation.
AUTH_REQUIRED: -32000,
// Resource not found: e.g. missing file.
RESOURCE_NOT_FOUND: -32002,
} as const;
export type AcpErrorCode =
(typeof ACP_ERROR_CODES)[keyof typeof ACP_ERROR_CODES];

View File

@@ -7,6 +7,7 @@
import { describe, expect, it, vi } from 'vitest';
import type { FileSystemService } from '@qwen-code/qwen-code-core';
import { AcpFileSystemService } from './filesystem.js';
import { ACP_ERROR_CODES } from '../errorCodes.js';
const createFallback = (): FileSystemService => ({
readTextFile: vi.fn(),
@@ -16,11 +17,13 @@ const createFallback = (): FileSystemService => ({
describe('AcpFileSystemService', () => {
describe('readTextFile ENOENT handling', () => {
it('parses path from ACP ENOENT message (quoted)', async () => {
it('converts RESOURCE_NOT_FOUND error to ENOENT', async () => {
const resourceNotFoundError = {
code: ACP_ERROR_CODES.RESOURCE_NOT_FOUND,
message: 'File not found',
};
const client = {
readTextFile: vi
.fn()
.mockResolvedValue({ content: 'ERROR: ENOENT: "/remote/file.txt"' }),
readTextFile: vi.fn().mockRejectedValue(resourceNotFoundError),
} as unknown as import('../acp.js').Client;
const svc = new AcpFileSystemService(
@@ -30,15 +33,20 @@ describe('AcpFileSystemService', () => {
createFallback(),
);
await expect(svc.readTextFile('/local/file.txt')).rejects.toMatchObject({
await expect(svc.readTextFile('/some/file.txt')).rejects.toMatchObject({
code: 'ENOENT',
path: '/remote/file.txt',
errno: -2,
path: '/some/file.txt',
});
});
it('falls back to requested path when none provided', async () => {
it('re-throws other errors unchanged', async () => {
const otherError = {
code: ACP_ERROR_CODES.INTERNAL_ERROR,
message: 'Internal error',
};
const client = {
readTextFile: vi.fn().mockResolvedValue({ content: 'ERROR: ENOENT:' }),
readTextFile: vi.fn().mockRejectedValue(otherError),
} as unknown as import('../acp.js').Client;
const svc = new AcpFileSystemService(
@@ -48,12 +56,34 @@ describe('AcpFileSystemService', () => {
createFallback(),
);
await expect(
svc.readTextFile('/fallback/path.txt'),
).rejects.toMatchObject({
code: 'ENOENT',
path: '/fallback/path.txt',
await expect(svc.readTextFile('/some/file.txt')).rejects.toMatchObject({
code: ACP_ERROR_CODES.INTERNAL_ERROR,
message: 'Internal error',
});
});
it('uses fallback when readTextFile capability is disabled', async () => {
const client = {
readTextFile: vi.fn(),
} as unknown as import('../acp.js').Client;
const fallback = createFallback();
(fallback.readTextFile as ReturnType<typeof vi.fn>).mockResolvedValue(
'fallback content',
);
const svc = new AcpFileSystemService(
client,
'session-3',
{ readTextFile: false, writeTextFile: true },
fallback,
);
const result = await svc.readTextFile('/some/file.txt');
expect(result).toBe('fallback content');
expect(fallback.readTextFile).toHaveBeenCalledWith('/some/file.txt');
expect(client.readTextFile).not.toHaveBeenCalled();
});
});
});

View File

@@ -6,6 +6,7 @@
import type { FileSystemService } from '@qwen-code/qwen-code-core';
import type * as acp from '../acp.js';
import { ACP_ERROR_CODES } from '../errorCodes.js';
/**
* ACP client-based implementation of FileSystemService
@@ -23,25 +24,31 @@ export class AcpFileSystemService implements FileSystemService {
return this.fallback.readTextFile(filePath);
}
const response = await this.client.readTextFile({
path: filePath,
sessionId: this.sessionId,
line: null,
limit: null,
});
let response: { content: string };
try {
response = await this.client.readTextFile({
path: filePath,
sessionId: this.sessionId,
line: null,
limit: null,
});
} catch (error) {
const errorCode =
typeof error === 'object' && error !== null && 'code' in error
? (error as { code?: unknown }).code
: undefined;
if (response.content.startsWith('ERROR: ENOENT:')) {
// Treat ACP error strings as structured ENOENT errors without
// assuming a specific platform format.
const match = /^ERROR:\s*ENOENT:\s*(?<path>.*)$/i.exec(response.content);
const err = new Error(response.content) as NodeJS.ErrnoException;
err.code = 'ENOENT';
err.errno = -2;
const rawPath = match?.groups?.['path']?.trim();
err['path'] = rawPath
? rawPath.replace(/^['"]|['"]$/g, '') || filePath
: filePath;
throw err;
if (errorCode === ACP_ERROR_CODES.RESOURCE_NOT_FOUND) {
const err = new Error(
`File not found: ${filePath}`,
) as NodeJS.ErrnoException;
err.code = 'ENOENT';
err.errno = -2;
err.path = filePath;
throw err;
}
throw error;
}
return response.content;

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.7.1",
"version": "0.7.2",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -102,16 +102,14 @@ export const QWEN_OAUTH_ALLOWED_MODELS = [
export const QWEN_OAUTH_MODELS: ModelConfig[] = [
{
id: 'coder-model',
name: 'Qwen Coder',
description:
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
name: 'coder-model',
description: 'The latest Qwen Coder model from Alibaba Cloud ModelStudio',
capabilities: { vision: false },
},
{
id: 'vision-model',
name: 'Qwen Vision',
description:
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
name: 'vision-model',
description: 'The latest Qwen Vision model from Alibaba Cloud ModelStudio',
capabilities: { vision: true },
},
];

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.7.1",
"version": "0.7.2",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.7.1",
"version": "0.7.2",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {

View File

@@ -23,3 +23,23 @@ export const CLIENT_METHODS = {
session_request_permission: 'session/request_permission',
session_update: 'session/update',
} as const;
export const ACP_ERROR_CODES = {
// Parse error: invalid JSON received by server.
PARSE_ERROR: -32700,
// Invalid request: JSON is not a valid Request object.
INVALID_REQUEST: -32600,
// Method not found: method does not exist or is unavailable.
METHOD_NOT_FOUND: -32601,
// Invalid params: invalid method parameter(s).
INVALID_PARAMS: -32602,
// Internal error: implementation-defined server error.
INTERNAL_ERROR: -32603,
// Authentication required: must authenticate before operation.
AUTH_REQUIRED: -32000,
// Resource not found: e.g. missing file.
RESOURCE_NOT_FOUND: -32002,
} as const;
export type AcpErrorCode =
(typeof ACP_ERROR_CODES)[keyof typeof ACP_ERROR_CODES];

View File

@@ -28,6 +28,7 @@ import * as os from 'node:os';
import type { z } from 'zod';
import type { DiffManager } from './diff-manager.js';
import { OpenFilesManager } from './open-files-manager.js';
import { ACP_ERROR_CODES } from './constants/acpSchema.js';
class CORSError extends Error {
constructor(message: string) {
@@ -264,7 +265,7 @@ export class IDEServer {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
code: ACP_ERROR_CODES.AUTH_REQUIRED,
message:
'Bad Request: No valid session ID provided for non-initialize request.',
},
@@ -283,7 +284,7 @@ export class IDEServer {
res.status(500).json({
jsonrpc: '2.0' as const,
error: {
code: -32603,
code: ACP_ERROR_CODES.INTERNAL_ERROR,
message: 'Internal server error',
},
id: null,

View File

@@ -5,6 +5,7 @@
*/
import { JSONRPC_VERSION } from '../types/acpTypes.js';
import { ACP_ERROR_CODES } from '../constants/acpSchema.js';
import type {
AcpMessage,
AcpPermissionRequest,
@@ -232,12 +233,34 @@ export class AcpConnection {
})
.catch((error) => {
if ('id' in message && typeof message.id === 'number') {
const errorMessage =
error instanceof Error
? error.message
: typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as { message: unknown }).message === 'string'
? (error as { message: string }).message
: String(error);
let errorCode: number = ACP_ERROR_CODES.INTERNAL_ERROR;
const errorCodeValue =
typeof error === 'object' && error !== null && 'code' in error
? (error as { code?: unknown }).code
: undefined;
if (typeof errorCodeValue === 'number') {
errorCode = errorCodeValue;
} else if (errorCodeValue === 'ENOENT') {
errorCode = ACP_ERROR_CODES.RESOURCE_NOT_FOUND;
}
this.messageHandler.sendResponseMessage(this.child, {
jsonrpc: JSONRPC_VERSION,
id: message.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : String(error),
code: errorCode,
message: errorMessage,
},
});
}

View File

@@ -66,6 +66,11 @@ export class AcpFileHandler {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`[ACP] Failed to read file ${params.path}:`, errorMsg);
const nodeError = error as NodeJS.ErrnoException;
if (nodeError?.code === 'ENOENT') {
throw error;
}
throw new Error(`Failed to read file '${params.path}': ${errorMsg}`);
}
}

View File

@@ -4,9 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { ACP_ERROR_CODES } from '../constants/acpSchema.js';
const AUTH_ERROR_PATTERNS = [
'Authentication required', // Standard authentication request message
'(code: -32000)', // RPC error code -32000 indicates authentication failure
`(code: ${ACP_ERROR_CODES.AUTH_REQUIRED})`, // RPC error code indicates auth failure
'Unauthorized', // HTTP unauthorized error
'Invalid token', // Invalid token
'Session expired', // Session expired

View File

@@ -8,6 +8,9 @@ import * as vscode from 'vscode';
import { BaseMessageHandler } from './BaseMessageHandler.js';
import type { ChatMessage } from '../../services/qwenAgentManager.js';
import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js';
import { ACP_ERROR_CODES } from '../../constants/acpSchema.js';
const AUTH_REQUIRED_CODE_PATTERN = `(code: ${ACP_ERROR_CODES.AUTH_REQUIRED})`;
/**
* Session message handler
@@ -355,7 +358,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
createErr instanceof Error ? createErr.message : String(createErr);
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)')
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN)
) {
await this.promptLogin(
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
@@ -421,7 +424,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
errorMsg.includes('Session not found') ||
errorMsg.includes('No active ACP session') ||
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token')
) {
@@ -512,7 +515,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -622,7 +625,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -682,7 +685,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors in session creation
if (
createErrorMsg.includes('Authentication required') ||
createErrorMsg.includes('(code: -32000)') ||
createErrorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
createErrorMsg.includes('Unauthorized') ||
createErrorMsg.includes('Invalid token') ||
createErrorMsg.includes('No active ACP session')
@@ -722,7 +725,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -777,7 +780,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -827,7 +830,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -855,7 +858,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -961,7 +964,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')
@@ -989,7 +992,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
// Check for authentication/session expiration errors
if (
errorMsg.includes('Authentication required') ||
errorMsg.includes('(code: -32000)') ||
errorMsg.includes(AUTH_REQUIRED_CODE_PATTERN) ||
errorMsg.includes('Unauthorized') ||
errorMsg.includes('Invalid token') ||
errorMsg.includes('No active ACP session')