Files
OpenQode/bin/qwen-bridge.mjs

214 lines
7.2 KiB
JavaScript

/**
* Qwen API Bridge for Electron
* Handles authentication and API calls to Qwen
*/
import { fileURLToPath, pathToFileURL } from 'url';
import path from 'path';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const OPENCODE_ROOT = path.resolve(__dirname, '..');
// Dynamic import of QwenOAuth
let qwen = null;
async function getQwen() {
if (!qwen) {
// Convert Windows path to proper file:// URL for ESM imports
const qwenOAuthPath = path.join(OPENCODE_ROOT, 'qwen-oauth.mjs');
const qwenOAuthUrl = pathToFileURL(qwenOAuthPath).href;
const { QwenOAuth } = await import(qwenOAuthUrl);
qwen = new QwenOAuth();
}
return qwen;
}
// Available models
const MODELS = [
{ id: 'qwen-coder-plus', name: 'Qwen Coder Plus', context: 131072 },
{ id: 'qwen-plus', name: 'Qwen Plus', context: 1000000 },
{ id: 'qwen-turbo', name: 'Qwen Turbo', context: 1000000 }
];
/**
* Check if user is authenticated
*/
export async function checkAuth() {
try {
const qwenClient = await getQwen();
const result = await qwenClient.checkAuth();
// QwenOAuth.checkAuth returns { authenticated: bool, method: string, ... }
return {
authenticated: result.authenticated === true,
method: result.method,
hasVisionSupport: result.hasVisionSupport
};
} catch (e) {
return { authenticated: false, error: e.message };
}
}
/**
* Get available models
*/
export function getModels() {
return MODELS;
}
/**
* Send a message and get full response (non-streaming)
*/
export async function sendMessage(message, model = 'qwen-coder-plus') {
try {
const qwenClient = await getQwen();
const result = await qwenClient.sendMessage(message, model);
return { success: result.success, response: result.response, error: result.error };
} catch (e) {
return { success: false, error: e.message };
}
}
/**
* Send a vision message (image + text) and get full response (non-streaming)
*/
export async function sendVisionMessage(message, imageData, model = 'qwen-vl-plus') {
try {
const qwenClient = await getQwen();
const result = await qwenClient.sendVisionMessage(message, imageData, model);
return { success: result.success, response: result.response, error: result.error };
} catch (e) {
return { success: false, error: e.message };
}
}
/**
* Stream a message with callbacks
*/
export async function streamMessage(message, model = 'qwen-coder-plus', callbacks = {}) {
const { onChunk, onComplete, onError } = callbacks;
try {
const qwenClient = await getQwen();
// Build system prompt for Goose with Internal-First Policy
const systemPrompt = `You are Goose AI Super, an advanced AI developer and agent running inside an Electron IDE.
⚠️ CRITICAL POLICY: "INTERNAL TOOLS FIRST"
You have two modes of operation. You must choose the correct one based on the user's request:
### 1. 🏠 INTERNAL MODE (DEFAULT - 99% of tasks)
For coding, building apps, web browsing, and general assistance.
- **BUILDING/CODING**: Use the **Built-in Editor** ([ACTION:OPEN_EDITOR]) and **App Preview** ([ACTION:PREVIEW]).
- NEVER open Notepad, VS Code, or external terminals for coding.
- ALWAYS output full code files (index.html, style.css, script.js) for the internal preview.
- **BROWSING**: Use the **Built-in Browser** ([ACTION:BROWSER_NAVIGATE]).
- NEVER launch Chrome/Edge unless explicitly asked.
### 2. 🖥️ DESKTOP MODE (RESTRICTED - Explicit Request Only)
Only when the user SPECIFICALLY asks to "use my computer", "take a screenshot", "click on X", or "automate my desktop".
- Capabilities: [ACTION:SCREENSHOT], [ACTION:CLICK], [ACTION:TYPE], [ACTION:OPEN_APP].
---
## 🛠️ ACTION COMMANDS (Use these to perform tasks)
### 📝 CODING & BUILDING (Internal)
[ACTION:OPEN_EDITOR] -> Opens the built-in Monaco editor
[ACTION:PREVIEW url="file:///..."] -> Opens the built-in preview panel
[ACTION:FILE_WRITE path="index.html" content="..."] -> Writes to the internal workspace
### 🌐 BROWSING (Internal)
[ACTION:BROWSER_NAVIGATE url="https://google.com"] -> Navigates the internal webview & Playwright
[ACTION:BROWSER_CLICK selector="#btn"] -> Clicks element in internal webview
[ACTION:BROWSER_TYPE text="hello"] -> Types in internal webview
### 🖥️ DESKTOP AUTOMATION (⚠️ ONLY if explicitly requested)
[ACTION:SCREENSHOT] -> Captures desktop
[ACTION:CLICK x=100 y=200] -> Clicks desktop coordinates
[ACTION:TYPE text="hello"] -> Types on desktop
[ACTION:OPEN_APP app="notepad"] -> Launches external app
---
## 🧠 INTELLIGENT BEHAVIOR RULES
1. **BUILD TASKS (Calculators, Games, Websites):**
- **DO NOT** use IQ Exchange or "Plan". Just **DO IT**.
- **STREAM** the code directly.
- Output **FENCED CODE BLOCKS** (\`\`\`html, \`\`\`css, \`\`\`js) for all files.
- My system will automatically capture these blocks, save them, and open the Preview.
- **Right:** "Here is the code for the calculator..." followed by code blocks.
- **Wrong:** [ACTION:OPEN_APP app="textedit"] (VIOLATION!)
2. **WEB SEARCH / BROWSING:**
- Use the **Internal Browser** by default.
- [ACTION:BROWSER_NAVIGATE url="https://google.com"]
3. **COMPLEX DESKTOP TASKS:**
- If user asks: "Use my computer to check spotify", THEN use [ACTION:SCREENSHOT] and desktop tools.
- Use [ACTION:IQ_EXCHANGE task="..."] for multistep desktop navigation.
4. **VISION:**
- If user asks to find/click something on the DESKTOP, take a [ACTION:SCREENSHOT] first.
## EXAMPLES:
User: "Build a todo app"
You: I'll create a To-Do app using the built-in editor.
(Proceeds to output \`\`\`html, \`\`\`css, \`\`\`js blocks immediately)
User: "Search google for weather"
You: Searching in internal browser...
[ACTION:BROWSER_NAVIGATE url="https://google.com/search?q=weather"]
User: "Open notepad on my computer and type hi"
You: (User explicitly asked for desktop) Opening Notepad...
[ACTION:OPEN_APP app="notepad"]
[ACTION:TYPE text="hi"]
User: "Click on the start menu"
You: (User explicitly asked for desktop) Taking screenshot to locate it...
[ACTION:SCREENSHOT]
Current context:
- Platform: ${process.platform}
- Workdir: ${process.cwd()}
- Time: ${new Date().toISOString()}`;
let fullResponse = '';
// Use sendMessage with onChunk callback for streaming
const result = await qwenClient.sendMessage(
message,
model,
null, // no image data
(chunk) => {
fullResponse += chunk;
if (onChunk) onChunk(chunk);
},
systemPrompt
);
if (result.success) {
// If we got chunks, use fullResponse; otherwise use result.response
const finalResponse = fullResponse || result.response || '';
if (onComplete) onComplete(finalResponse);
} else {
if (onError) onError(result.error || 'Unknown error');
}
} catch (e) {
console.error('Stream message error:', e);
if (onError) onError(e.message || String(e));
}
}
export default {
checkAuth,
getModels,
sendMessage,
streamMessage
};