Auth fix: Use QwenOAuth.sendMessage like TUI, simplify auth-check to verify qwen CLI

This commit is contained in:
Gemini AI
2025-12-14 13:53:20 +04:00
Unverified
parent 5bc8e467ac
commit 165cf8ae2e
2 changed files with 92 additions and 206 deletions

View File

@@ -1,25 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* OpenQode Auth Check * OpenQode Auth Check
* Verifies Qwen authentication and triggers OAuth if needed. * Verifies qwen CLI is authenticated by running a test command.
* Called by launchers before showing menu. * Called by launchers before showing menu.
* *
* Exit codes: * This uses the same auth method as TUI (qwen CLI)
* 0 = Authenticated
* 1 = Auth failed
* 2 = User cancelled
*/ */
import { createRequire } from 'module'; import { spawn } from 'child_process';
const require = createRequire(import.meta.url);
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { exec } from 'child_process';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const ROOT = path.resolve(__dirname, '..');
// Colors // Colors
const C = { const C = {
@@ -33,122 +26,54 @@ const C = {
dim: '\x1b[2m' dim: '\x1b[2m'
}; };
// Token file paths to check // Check if qwen CLI is installed and authenticated
const TOKEN_PATHS = [ const checkQwenCLI = () => {
path.join(ROOT, 'tokens.json'), return new Promise((resolve) => {
path.join(ROOT, '.qwen-tokens.json'), const isWin = process.platform === 'win32';
path.join(process.env.HOME || process.env.USERPROFILE || '', '.qwen', 'config.json') let command = 'qwen';
]; let args = ['--version'];
// Check if we have valid tokens // On Windows, try to find the CLI directly
const checkExistingAuth = () => { if (isWin) {
for (const tokenPath of TOKEN_PATHS) { const appData = process.env.APPDATA || '';
try { const cliPath = path.join(appData, 'npm', 'node_modules', '@qwen-code', 'qwen-code', 'cli.js');
if (fs.existsSync(tokenPath)) { if (fs.existsSync(cliPath)) {
const data = JSON.parse(fs.readFileSync(tokenPath, 'utf8')); command = 'node';
if (data.access_token) { args = [cliPath, '--version'];
// Check if expired (if expiry info available) } else {
if (data.expires_at) { command = 'qwen.cmd';
const expiry = new Date(data.expires_at);
if (expiry > new Date()) {
return { valid: true, source: tokenPath };
}
} else {
// No expiry info, assume valid
return { valid: true, source: tokenPath };
}
}
} }
} catch (e) { /* ignore */ } }
}
return { valid: false };
};
// Open URL in default browser const child = spawn(command, args, {
const openBrowser = (url) => { shell: false,
const platform = process.platform; timeout: 10000
let cmd; });
switch (platform) { let output = '';
case 'darwin': child.stdout?.on('data', (data) => { output += data.toString(); });
cmd = `open "${url}"`; child.stderr?.on('data', (data) => { output += data.toString(); });
break;
case 'win32':
cmd = `start "" "${url}"`;
break;
default:
cmd = `xdg-open "${url}"`;
}
exec(cmd, (err) => { child.on('error', (err) => {
if (err) console.log(C.yellow + ' (Could not open browser automatically)' + C.reset); resolve({ installed: false, error: err.message });
});
child.on('close', (code) => {
if (code === 0 || output.includes('qwen')) {
resolve({ installed: true, version: output.trim() });
} else {
resolve({ installed: false, error: `Exit code: ${code}` });
}
});
// Timeout fallback
setTimeout(() => {
child.kill();
resolve({ installed: false, error: 'Timeout' });
}, 5000);
}); });
}; };
// Perform OAuth device flow
const performAuth = async () => {
console.log(C.cyan + '\n Starting Qwen OAuth...' + C.reset);
try {
const { QwenOAuth } = await import('../qwen-oauth.mjs');
const oauth = new QwenOAuth();
// Start device flow
const deviceInfo = await oauth.startDeviceFlow();
console.log('');
console.log(C.magenta + ' ╔═══════════════════════════════════════════╗' + C.reset);
console.log(C.magenta + ' ║ QWEN AUTHENTICATION ║' + C.reset);
console.log(C.magenta + ' ╚═══════════════════════════════════════════╝' + C.reset);
console.log('');
console.log(C.yellow + ' 1. Open this URL in your browser:' + C.reset);
console.log(C.cyan + ` ${deviceInfo.verificationUriComplete || deviceInfo.verificationUri}` + C.reset);
console.log('');
if (deviceInfo.userCode) {
console.log(C.yellow + ' 2. Enter this code if prompted:' + C.reset);
console.log(C.green + C.bold + ` ${deviceInfo.userCode}` + C.reset);
console.log('');
}
console.log(C.dim + ' Waiting for you to complete login in browser...' + C.reset);
// Try to open browser automatically
openBrowser(deviceInfo.verificationUriComplete || deviceInfo.verificationUri);
// Poll for tokens
const tokens = await oauth.pollForTokens();
if (tokens && tokens.access_token) {
// Save tokens
oauth.saveTokens(tokens);
// Also save to main tokens.json for compatibility
const mainTokenPath = path.join(ROOT, 'tokens.json');
fs.writeFileSync(mainTokenPath, JSON.stringify({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_at || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
}, null, 2));
console.log(C.green + '\n ✅ Authentication successful!' + C.reset);
return true;
} else {
console.log(C.red + '\n ✗ Authentication failed or timed out.' + C.reset);
return false;
}
} catch (e) {
console.log(C.red + `\n ✗ OAuth error: ${e.message}` + C.reset);
// Provide helpful guidance based on error
if (e.message.includes('Client ID')) {
console.log(C.yellow + '\n To fix this:' + C.reset);
console.log(C.dim + ' 1. Copy config.example.cjs to config.cjs' + C.reset);
console.log(C.dim + ' 2. Add your QWEN_OAUTH_CLIENT_ID' + C.reset);
}
return false;
}
};
// Main // Main
const main = async () => { const main = async () => {
console.log(''); console.log('');
@@ -156,26 +81,28 @@ const main = async () => {
console.log(C.cyan + ' ║ OpenQode Authentication Check ║' + C.reset); console.log(C.cyan + ' ║ OpenQode Authentication Check ║' + C.reset);
console.log(C.cyan + ' ╚═══════════════════════════════════════════╝' + C.reset); console.log(C.cyan + ' ╚═══════════════════════════════════════════╝' + C.reset);
console.log(''); console.log('');
console.log(C.dim + ' Checking Qwen authentication status...' + C.reset); console.log(C.dim + ' Checking qwen CLI...' + C.reset);
// Check existing auth const result = await checkQwenCLI();
const authStatus = checkExistingAuth();
if (authStatus.valid) { if (result.installed) {
console.log(C.green + '\nAlready authenticated!' + C.reset); console.log(C.green + ' ✅ qwen CLI is installed and ready!' + C.reset);
console.log(C.dim + ` Token source: ${path.basename(authStatus.source)}` + C.reset); if (result.version) {
process.exit(0); console.log(C.dim + ` ${result.version}` + C.reset);
} }
console.log('');
console.log(C.yellow + '\n [!] Not authenticated. Starting OAuth...' + C.reset); console.log(C.dim + ' If you need to authenticate, run: qwen auth' + C.reset);
const success = await performAuth();
if (success) {
console.log(C.green + '\n Ready to use OpenQode!' + C.reset);
process.exit(0); process.exit(0);
} else { } else {
console.log(C.yellow + '\n You can still use OpenQode, but AI features may be limited.' + C.reset); console.log(C.yellow + ' ⚠️ qwen CLI not found or not working.' + C.reset);
console.log('');
console.log(C.yellow + ' To install qwen CLI:' + C.reset);
console.log(C.cyan + ' npm install -g @qwen-code/qwen-code' + C.reset);
console.log('');
console.log(C.yellow + ' After install, authenticate with:' + C.reset);
console.log(C.cyan + ' qwen auth' + C.reset);
console.log('');
console.log(C.dim + ' You can still use OpenQode, but AI features require qwen CLI.' + C.reset);
process.exit(1); process.exit(1);
} }
}; };

View File

@@ -153,33 +153,18 @@ const triggerOAuth = async () => {
return null; return null;
}; };
// Call Qwen AI API // Call Qwen AI using same method as TUI (QwenOAuth.sendMessage)
const callQwenAI = async (prompt, onChunk = null) => { const callQwenAI = async (prompt, onChunk = null) => {
let token = getAuthToken();
if (!token) {
token = await triggerOAuth();
if (!token) {
return { success: false, error: 'No auth token available', response: '' };
}
}
try { try {
const response = await fetch(DASHSCOPE_API, { const { QwenOAuth } = await import('../qwen-oauth.mjs');
method: 'POST', const oauth = new QwenOAuth();
headers: {
'Content-Type': 'application/json', // Build the full prompt with repair context
'Authorization': `Bearer ${token}`, const fullPrompt = `[SMART REPAIR AGENT]
}, You are the OpenQode Smart Repair Agent. Your ONLY purpose is to diagnose and fix bugs in the OpenQode TUI (Terminal User Interface).
body: JSON.stringify({
model: selectedModel.id,
messages: [
{
role: 'system',
content: `You are the OpenQode Smart Repair Agent. Your ONLY purpose is to diagnose and fix bugs in the OpenQode TUI (Terminal User Interface).
The TUI is a Node.js/React Ink application located at: The TUI is a Node.js/React Ink application located at:
- Main file: bin/opencode-ink.mjs - Main file: bin/opencode-ink.mjs
- Package: package.json - Package: package.json
When given an error: When given an error:
@@ -188,58 +173,32 @@ When given an error:
3. Provide a specific fix (code change or shell command) 3. Provide a specific fix (code change or shell command)
4. Format fixes clearly with code blocks 4. Format fixes clearly with code blocks
You MUST refuse any request that is not about fixing the TUI.` You MUST refuse any request that is not about fixing the TUI.
},
{ role: 'user', content: prompt }
],
stream: true,
}),
});
if (!response.ok) { USER REQUEST:
const errorText = await response.text(); ${prompt}`;
if (response.status === 401) {
// Token expired - try re-auth console.log(C.dim + '\n Calling qwen CLI...' + C.reset);
console.log(C.yellow + '[!] Token expired, re-authenticating...' + C.reset);
const newToken = await triggerOAuth(); const result = await oauth.sendMessage(fullPrompt, selectedModel.id, null, onChunk);
if (newToken) {
return callQwenAI(prompt, onChunk); // Retry with new token if (result && result.response) {
} return { success: true, response: result.response };
} } else if (result && result.error) {
return { success: false, error: `API error ${response.status}: ${errorText}`, response: '' }; return { success: false, error: result.error, response: '' };
} else {
return { success: true, response: result || '' };
} }
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
if (content) {
fullResponse += content;
if (onChunk) onChunk(content);
}
} catch (e) { /* ignore parse errors */ }
}
}
}
return { success: true, response: fullResponse };
} catch (error) { } catch (error) {
return { success: false, error: error.message || 'Network error', response: '' }; // If qwen CLI not found, give helpful message
if (error.message && error.message.includes('ENOENT')) {
return {
success: false,
error: 'qwen CLI not installed. Install with: npm install -g @qwen-code/qwen-code',
response: ''
};
}
return { success: false, error: error.message || 'Unknown error', response: '' };
} }
}; };