Compare commits

..

1 Commits

Author SHA1 Message Date
Mingholy
af03eaa57f fix: output token limit for qwen 2025-09-19 23:37:15 +08:00
13 changed files with 56 additions and 143 deletions

13
.vscode/launch.json vendored
View File

@@ -101,13 +101,6 @@
"env": {
"GEMINI_SANDBOX": "false"
}
},
{
"name": "Attach by Process ID",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
],
"inputs": [
@@ -122,12 +115,6 @@
"type": "promptString",
"description": "Enter your prompt for non-interactive mode",
"default": "Explain this code"
},
{
"id": "debugPort",
"type": "promptString",
"description": "Enter the debug port number (default: 9229)",
"default": "9229"
}
]
}

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.0-preview",
"version": "0.0.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.1.0-preview",
"version": "0.0.12",
"workspaces": [
"packages/*"
],
@@ -13454,7 +13454,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.1.0-preview",
"version": "0.0.12",
"dependencies": {
"@google/genai": "1.9.0",
"@iarna/toml": "^2.2.5",
@@ -13662,7 +13662,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.1.0-preview",
"version": "0.0.12",
"dependencies": {
"@google/genai": "1.13.0",
"@lvce-editor/ripgrep": "^1.6.0",
@@ -13788,7 +13788,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.0-preview",
"version": "0.0.12",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -13800,7 +13800,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.1.0-preview",
"version": "0.0.12",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.0-preview",
"version": "0.0.12",
"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.1.0-preview"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.12"
},
"scripts": {
"start": "node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.0-preview",
"version": "0.0.12",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.0-preview"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.12"
},
"dependencies": {
"@google/genai": "1.9.0",

View File

@@ -126,18 +126,6 @@ describe('validateNonInterActiveAuth', () => {
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('uses configured QWEN_OAUTH if provided', async () => {
const nonInteractiveConfig: NonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
await validateNonInteractiveAuth(
AuthType.QWEN_OAUTH,
undefined,
nonInteractiveConfig,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';

View File

@@ -97,18 +97,6 @@ class GeminiAgent {
name: 'Vertex AI',
description: null,
},
{
id: AuthType.USE_OPENAI,
name: 'Use OpenAI API key',
description:
'Requires setting the `OPENAI_API_KEY` environment variable',
},
{
id: AuthType.QWEN_OAUTH,
name: 'Qwen OAuth',
description:
'OAuth authentication for Qwen models with 2000 daily requests',
},
];
return {

View File

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

View File

@@ -712,6 +712,8 @@ async function authWithQwenDeviceFlow(
`Polling... (attempt ${attempt + 1}/${maxAttempts})`,
);
process.stdout.write('.');
// Wait with cancellation check every 100ms
await new Promise<void>((resolve) => {
const checkInterval = 100; // Check every 100ms

View File

@@ -901,37 +901,5 @@ describe('SharedTokenManager', () => {
);
}
});
it('should properly clean up timeout when file operation completes before timeout', async () => {
const tokenManager = SharedTokenManager.getInstance();
tokenManager.clearCache();
const mockClient = {
getCredentials: vi.fn().mockReturnValue(null),
setCredentials: vi.fn(),
getAccessToken: vi.fn(),
requestDeviceAuthorization: vi.fn(),
pollDeviceToken: vi.fn(),
refreshAccessToken: vi.fn(),
};
// Mock clearTimeout to verify it's called
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
// Mock file stat to resolve quickly (before timeout)
mockFs.stat.mockResolvedValue({ mtimeMs: 12345 } as Stats);
// Call checkAndReloadIfNeeded which uses withTimeout internally
const checkMethod = getPrivateProperty(
tokenManager,
'checkAndReloadIfNeeded',
) as (client?: IQwenOAuth2Client) => Promise<void>;
await checkMethod.call(tokenManager, mockClient);
// Verify that clearTimeout was called to clean up the timer
expect(clearTimeoutSpy).toHaveBeenCalled();
clearTimeoutSpy.mockRestore();
});
});
});

View File

@@ -290,36 +290,6 @@ export class SharedTokenManager {
}
}
/**
* Utility method to add timeout to any promise operation
* Properly cleans up the timeout when the promise completes
*/
private withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operationType = 'Operation',
): Promise<T> {
let timeoutId: NodeJS.Timeout;
return Promise.race([
promise.finally(() => {
// Clear timeout when main promise completes (success or failure)
if (timeoutId) {
clearTimeout(timeoutId);
}
}),
new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() =>
reject(
new Error(`${operationType} timed out after ${timeoutMs}ms`),
),
timeoutMs,
);
}),
]);
}
/**
* Perform the actual file check and reload operation
* This is separated to enable proper promise-based synchronization
@@ -333,12 +303,25 @@ export class SharedTokenManager {
try {
const filePath = this.getCredentialFilePath();
// Add timeout to file stat operation
const withTimeout = async <T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> =>
Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(
new Error(`File operation timed out after ${timeoutMs}ms`),
),
timeoutMs,
),
),
]);
const stats = await this.withTimeout(
fs.stat(filePath),
3000,
'File operation',
);
const stats = await withTimeout(fs.stat(filePath), 3000);
const fileModTime = stats.mtimeMs;
// Reload credentials if file has been modified since last cache
@@ -468,7 +451,7 @@ export class SharedTokenManager {
// Check if we have a refresh token before attempting refresh
const currentCredentials = qwenClient.getCredentials();
if (!currentCredentials.refresh_token) {
// console.debug('create a NO_REFRESH_TOKEN error');
console.debug('create a NO_REFRESH_TOKEN error');
throw new TokenManagerError(
TokenError.NO_REFRESH_TOKEN,
'No refresh token available for token refresh',
@@ -606,12 +589,26 @@ export class SharedTokenManager {
const dirPath = path.dirname(filePath);
const tempPath = `${filePath}.tmp.${randomUUID()}`;
// Add timeout wrapper for file operations
const withTimeout = async <T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> =>
Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)),
timeoutMs,
),
),
]);
// Create directory with restricted permissions
try {
await this.withTimeout(
await withTimeout(
fs.mkdir(dirPath, { recursive: true, mode: 0o700 }),
5000,
'File operation',
);
} catch (error) {
throw new TokenManagerError(
@@ -625,30 +622,21 @@ export class SharedTokenManager {
try {
// Write to temporary file first with restricted permissions
await this.withTimeout(
await withTimeout(
fs.writeFile(tempPath, credString, { mode: 0o600 }),
5000,
'File operation',
);
// Atomic move to final location
await this.withTimeout(
fs.rename(tempPath, filePath),
5000,
'File operation',
);
await withTimeout(fs.rename(tempPath, filePath), 5000);
// Update cached file modification time atomically after successful write
const stats = await this.withTimeout(
fs.stat(filePath),
5000,
'File operation',
);
const stats = await withTimeout(fs.stat(filePath), 5000);
this.memoryCache.fileModTime = stats.mtimeMs;
} catch (error) {
// Clean up temp file if it exists
try {
await this.withTimeout(fs.unlink(tempPath), 1000, 'File operation');
await withTimeout(fs.unlink(tempPath), 1000);
} catch (_cleanupError) {
// Ignore cleanup errors - temp file might not exist
}

View File

@@ -8,6 +8,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { EOL } from 'node:os';
import { spawn } from 'node:child_process';
import { rgPath } from '@lvce-editor/ripgrep';
import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
@@ -17,14 +18,6 @@ import type { Config } from '../config/config.js';
const DEFAULT_TOTAL_MAX_MATCHES = 20000;
/**
* Lazy loads the ripgrep binary path to avoid loading the library until needed
*/
async function getRipgrepPath(): Promise<string> {
const { rgPath } = await import('@lvce-editor/ripgrep');
return rgPath;
}
/**
* Parameters for the GrepTool
*/
@@ -299,9 +292,8 @@ class GrepToolInvocation extends BaseToolInvocation<
rgArgs.push(absolutePath);
try {
const ripgrepPath = await getRipgrepPath();
const output = await new Promise<string>((resolve, reject) => {
const child = spawn(ripgrepPath, rgArgs, {
const child = spawn(rgPath, rgArgs, {
windowsHide: true,
});

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.0-preview",
"version": "0.0.12",
"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.1.0-preview",
"version": "0.0.12",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {