mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 01:37:50 +00:00
chore(chrome-qwen-bridge): connect
This commit is contained in:
@@ -73,32 +73,18 @@ EOF
|
||||
|
||||
echo -e "${GREEN}✓${NC} Native Host 已配置"
|
||||
|
||||
# 第三步:检查 Qwen CLI(可选)
|
||||
# 第三步:检查 Qwen CLI
|
||||
echo -e "\n${BLUE}[3/5]${NC} 检查 Qwen CLI..."
|
||||
|
||||
QWEN_AVAILABLE=false
|
||||
if command -v qwen &> /dev/null; then
|
||||
QWEN_AVAILABLE=true
|
||||
echo -e "${GREEN}✓${NC} Qwen CLI $(qwen --version 2>/dev/null || echo "已安装")"
|
||||
|
||||
# 尝试启动 Qwen server
|
||||
if ! lsof -i:8080 &> /dev/null; then
|
||||
echo -e "${CYAN}→${NC} 启动 Qwen server (端口 8080)..."
|
||||
qwen server --port 8080 > /tmp/qwen-server.log 2>&1 &
|
||||
QWEN_PID=$!
|
||||
sleep 2
|
||||
|
||||
if kill -0 $QWEN_PID 2>/dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Qwen server 已启动 (PID: $QWEN_PID)"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Qwen server 启动失败,继续运行..."
|
||||
QWEN_AVAILABLE=false
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} 端口 8080 已被占用"
|
||||
fi
|
||||
QWEN_VERSION=$(qwen --version 2>/dev/null || echo "已安装")
|
||||
echo -e "${GREEN}✓${NC} Qwen CLI ${QWEN_VERSION}"
|
||||
echo -e "${CYAN}→${NC} 使用 ACP 模式与 Chrome 插件通信"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Qwen CLI 未安装(插件基础功能仍可使用)"
|
||||
echo -e " 安装方法: npm install -g @anthropic-ai/qwen-code"
|
||||
fi
|
||||
|
||||
# 第四步:启动测试页面
|
||||
@@ -338,7 +324,7 @@ echo -e " • 测试页面: ${BLUE}http://localhost:3000/qwen-test.html${NC}"
|
||||
echo -e " • 插件: 已加载到工具栏"
|
||||
|
||||
if [ "$QWEN_AVAILABLE" = true ]; then
|
||||
echo -e " • Qwen Server: ${BLUE}http://localhost:8080${NC}"
|
||||
echo -e " • Qwen CLI: 可用 (ACP 模式)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -347,10 +333,6 @@ echo -e " • 插件日志: Chrome DevTools Console"
|
||||
echo -e " • 后台脚本: chrome://extensions → Service Worker"
|
||||
echo -e " • Native Host: /tmp/qwen-bridge-host.log"
|
||||
|
||||
if [ "$QWEN_AVAILABLE" = true ]; then
|
||||
echo -e " • Qwen 日志: /tmp/qwen-server.log"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
|
||||
echo ""
|
||||
@@ -361,7 +343,6 @@ cleanup() {
|
||||
|
||||
# 停止进程
|
||||
[ ! -z "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
|
||||
[ ! -z "$QWEN_PID" ] && kill $QWEN_PID 2>/dev/null
|
||||
|
||||
echo -e "${GREEN}✓${NC} 已停止所有服务"
|
||||
exit 0
|
||||
|
||||
11
packages/chrome-qwen-bridge/native-host/host-wrapper.sh
Executable file
11
packages/chrome-qwen-bridge/native-host/host-wrapper.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 添加必要的 PATH
|
||||
export PATH="/usr/local/bin:/Users/yiliang/.npm-global/bin:$PATH"
|
||||
|
||||
LOG="/var/folders/sy/9mwf8c3n2b57__q35fyxwdhh0000gp/T/qwen-wrapper.log"
|
||||
echo "$(date): Wrapper started" >> "$LOG"
|
||||
echo "$(date): PATH=$PATH" >> "$LOG"
|
||||
|
||||
# 使用完整路径运行 node
|
||||
exec /usr/local/bin/node "$(dirname "$0")/host.js" 2>> "$LOG"
|
||||
@@ -3,6 +3,7 @@
|
||||
/**
|
||||
* Native Messaging Host for Qwen CLI Bridge
|
||||
* This script acts as a bridge between the Chrome extension and Qwen CLI
|
||||
* Uses ACP (Agent Communication Protocol) for communication with Qwen CLI
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
@@ -10,8 +11,31 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Native Messaging protocol helpers
|
||||
function sendMessage(message) {
|
||||
// ============================================================================
|
||||
// Logging
|
||||
// ============================================================================
|
||||
|
||||
const LOG_FILE = path.join(os.tmpdir(), 'qwen-bridge-host.log');
|
||||
|
||||
function log(message, level = 'INFO') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
||||
fs.appendFileSync(LOG_FILE, logLine);
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(message, 'ERROR');
|
||||
}
|
||||
|
||||
function logDebug(message) {
|
||||
log(message, 'DEBUG');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Native Messaging Protocol (Chrome Extension <-> Native Host)
|
||||
// ============================================================================
|
||||
|
||||
function sendMessageToExtension(message) {
|
||||
const buffer = Buffer.from(JSON.stringify(message));
|
||||
const length = Buffer.allocUnsafe(4);
|
||||
length.writeUInt32LE(buffer.length, 0);
|
||||
@@ -20,56 +44,462 @@ function sendMessage(message) {
|
||||
process.stdout.write(buffer);
|
||||
}
|
||||
|
||||
function readMessages() {
|
||||
function readMessagesFromExtension() {
|
||||
let messageLength = null;
|
||||
let chunks = [];
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
// Keep stdin open and in flowing mode
|
||||
process.stdin.resume();
|
||||
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
process.stdin.on('data', (chunk) => {
|
||||
log(`Received ${chunk.length} bytes from extension`);
|
||||
chunks.push(chunk);
|
||||
|
||||
while (true) {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
|
||||
// Read message length if we haven't yet
|
||||
// Need at least 4 bytes for length
|
||||
if (messageLength === null) {
|
||||
if (buffer.length >= 4) {
|
||||
messageLength = buffer.readUInt32LE(0);
|
||||
chunks = [buffer.slice(4)];
|
||||
}
|
||||
if (buffer.length < 4) break;
|
||||
messageLength = buffer.readUInt32LE(0);
|
||||
chunks = [buffer.slice(4)];
|
||||
log(`Message length: ${messageLength}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read message if we have the full length
|
||||
if (messageLength !== null) {
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
// Check if we have the full message
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
if (fullBuffer.length < messageLength) break;
|
||||
|
||||
if (fullBuffer.length >= messageLength) {
|
||||
const messageBuffer = fullBuffer.slice(0, messageLength);
|
||||
const message = JSON.parse(messageBuffer.toString());
|
||||
// Extract and parse message
|
||||
const messageBuffer = fullBuffer.slice(0, messageLength);
|
||||
try {
|
||||
const message = JSON.parse(messageBuffer.toString());
|
||||
log(`Received message: ${JSON.stringify(message)}`);
|
||||
|
||||
// Reset for next message
|
||||
chunks = [fullBuffer.slice(messageLength)];
|
||||
messageLength = null;
|
||||
// Reset for next message
|
||||
chunks = [fullBuffer.slice(messageLength)];
|
||||
messageLength = null;
|
||||
|
||||
// Handle the message
|
||||
handleMessage(message);
|
||||
}
|
||||
// Handle the message
|
||||
handleExtensionMessage(message);
|
||||
} catch (err) {
|
||||
logError(`Failed to parse message: ${err.message}`);
|
||||
chunks = [fullBuffer.slice(messageLength)];
|
||||
messageLength = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
log('stdin ended');
|
||||
cleanup();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.stdin.on('error', (err) => {
|
||||
logError(`stdin error: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Qwen CLI process management
|
||||
let qwenProcess = null;
|
||||
let qwenStatus = 'disconnected';
|
||||
let qwenCapabilities = [];
|
||||
// ============================================================================
|
||||
// ACP Protocol (Native Host <-> Qwen CLI)
|
||||
// ============================================================================
|
||||
|
||||
const ACP_PROTOCOL_VERSION = 1;
|
||||
|
||||
class AcpConnection {
|
||||
constructor() {
|
||||
this.process = null;
|
||||
this.status = 'disconnected';
|
||||
this.sessionId = null;
|
||||
this.pendingRequests = new Map();
|
||||
this.nextRequestId = 1;
|
||||
this.inputBuffer = '';
|
||||
}
|
||||
|
||||
async start(cwd = process.cwd()) {
|
||||
if (this.process) {
|
||||
return { success: false, error: 'Qwen CLI is already running' };
|
||||
}
|
||||
|
||||
try {
|
||||
log(`Starting Qwen CLI with ACP mode in ${cwd}`);
|
||||
|
||||
this.process = spawn('qwen', ['--experimental-acp'], {
|
||||
cwd,
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
if (!this.process || !this.process.pid) {
|
||||
this.process = null;
|
||||
this.status = 'stopped';
|
||||
return { success: false, error: 'Failed to start Qwen CLI process' };
|
||||
}
|
||||
|
||||
this.status = 'starting';
|
||||
|
||||
// Handle stdout (ACP messages from Qwen CLI)
|
||||
this.process.stdout.on('data', (data) => {
|
||||
this.handleAcpData(data.toString());
|
||||
});
|
||||
|
||||
// Handle stderr (logs from Qwen CLI)
|
||||
this.process.stderr.on('data', (data) => {
|
||||
const message = data.toString().trim();
|
||||
if (message) {
|
||||
log(`Qwen stderr: ${message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process exit
|
||||
this.process.on('close', (code) => {
|
||||
log(`Qwen CLI exited with code ${code}`);
|
||||
this.process = null;
|
||||
this.status = 'stopped';
|
||||
this.sessionId = null;
|
||||
|
||||
// Reject all pending requests
|
||||
for (const [id, { reject }] of this.pendingRequests) {
|
||||
reject(new Error('Qwen CLI process exited'));
|
||||
}
|
||||
this.pendingRequests.clear();
|
||||
|
||||
sendMessageToExtension({
|
||||
type: 'event',
|
||||
data: { type: 'qwen_stopped', code }
|
||||
});
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
logError(`Qwen CLI process error: ${err.message}`);
|
||||
this.status = 'error';
|
||||
});
|
||||
|
||||
// Initialize ACP connection
|
||||
const initResult = await this.initialize();
|
||||
if (!initResult.success) {
|
||||
this.stop();
|
||||
return initResult;
|
||||
}
|
||||
|
||||
// Create a new session
|
||||
const sessionResult = await this.createSession(cwd);
|
||||
if (!sessionResult.success) {
|
||||
this.stop();
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
this.status = 'running';
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status: 'running',
|
||||
pid: this.process.pid,
|
||||
sessionId: this.sessionId,
|
||||
agentInfo: initResult.data.agentInfo
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logError(`Failed to start Qwen CLI: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
handleAcpData(data) {
|
||||
this.inputBuffer += data;
|
||||
const lines = this.inputBuffer.split('\n');
|
||||
this.inputBuffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
try {
|
||||
const message = JSON.parse(trimmed);
|
||||
this.handleAcpMessage(message);
|
||||
} catch (err) {
|
||||
logError(`Failed to parse ACP message: ${trimmed}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAcpMessage(message) {
|
||||
logDebug(`ACP received: ${JSON.stringify(message)}`);
|
||||
|
||||
// Handle response to our request
|
||||
if ('id' in message && !('method' in message)) {
|
||||
const pending = this.pendingRequests.get(message.id);
|
||||
if (pending) {
|
||||
this.pendingRequests.delete(message.id);
|
||||
if ('result' in message) {
|
||||
pending.resolve(message.result);
|
||||
} else if ('error' in message) {
|
||||
pending.reject(new Error(message.error.message || 'ACP error'));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle notification from Qwen CLI
|
||||
if ('method' in message && !('id' in message)) {
|
||||
this.handleAcpNotification(message.method, message.params);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle request from Qwen CLI (e.g., permission requests)
|
||||
if ('method' in message && 'id' in message) {
|
||||
this.handleAcpRequest(message.id, message.method, message.params);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleAcpNotification(method, params) {
|
||||
switch (method) {
|
||||
case 'session/update':
|
||||
// Forward session updates to the extension
|
||||
sendMessageToExtension({
|
||||
type: 'event',
|
||||
data: {
|
||||
type: 'session_update',
|
||||
sessionId: params.sessionId,
|
||||
update: params.update
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'authenticate/update':
|
||||
sendMessageToExtension({
|
||||
type: 'event',
|
||||
data: {
|
||||
type: 'auth_update',
|
||||
authUri: params._meta?.authUri
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
log(`Unknown ACP notification: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleAcpRequest(id, method, params) {
|
||||
switch (method) {
|
||||
case 'session/request_permission':
|
||||
// Forward permission request to extension
|
||||
sendMessageToExtension({
|
||||
type: 'permission_request',
|
||||
requestId: id,
|
||||
sessionId: params.sessionId,
|
||||
toolCall: params.toolCall,
|
||||
options: params.options
|
||||
});
|
||||
break;
|
||||
|
||||
case 'fs/read_text_file':
|
||||
// Handle file read request
|
||||
this.handleFileReadRequest(id, params);
|
||||
break;
|
||||
|
||||
case 'fs/write_text_file':
|
||||
// Handle file write request
|
||||
this.handleFileWriteRequest(id, params);
|
||||
break;
|
||||
|
||||
default:
|
||||
log(`Unknown ACP request: ${method}`);
|
||||
this.sendAcpResponse(id, { error: { code: -32601, message: 'Method not found' } });
|
||||
}
|
||||
}
|
||||
|
||||
handleFileReadRequest(id, params) {
|
||||
try {
|
||||
const content = fs.readFileSync(params.path, 'utf-8');
|
||||
this.sendAcpResponse(id, { result: { content } });
|
||||
} catch (err) {
|
||||
this.sendAcpResponse(id, {
|
||||
error: { code: -32000, message: `Failed to read file: ${err.message}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleFileWriteRequest(id, params) {
|
||||
try {
|
||||
fs.writeFileSync(params.path, params.content, 'utf-8');
|
||||
this.sendAcpResponse(id, { result: null });
|
||||
} catch (err) {
|
||||
this.sendAcpResponse(id, {
|
||||
error: { code: -32000, message: `Failed to write file: ${err.message}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendAcpMessage(message) {
|
||||
if (!this.process || !this.process.stdin.writable) {
|
||||
throw new Error('Qwen CLI is not running');
|
||||
}
|
||||
|
||||
const json = JSON.stringify(message) + '\n';
|
||||
logDebug(`ACP send: ${json.trim()}`);
|
||||
this.process.stdin.write(json);
|
||||
}
|
||||
|
||||
sendAcpRequest(method, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = this.nextRequestId++;
|
||||
this.pendingRequests.set(id, { resolve, reject });
|
||||
|
||||
try {
|
||||
this.sendAcpMessage({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params
|
||||
});
|
||||
} catch (err) {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (this.pendingRequests.has(id)) {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`Request ${method} timed out`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
sendAcpResponse(id, response) {
|
||||
this.sendAcpMessage({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
...response
|
||||
});
|
||||
}
|
||||
|
||||
sendAcpNotification(method, params) {
|
||||
this.sendAcpMessage({
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
const result = await this.sendAcpRequest('initialize', {
|
||||
protocolVersion: ACP_PROTOCOL_VERSION,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log(`Qwen CLI initialized: ${JSON.stringify(result)}`);
|
||||
return { success: true, data: result };
|
||||
} catch (err) {
|
||||
logError(`Failed to initialize Qwen CLI: ${err.message}`);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async createSession(cwd) {
|
||||
try {
|
||||
const result = await this.sendAcpRequest('session/new', {
|
||||
cwd,
|
||||
mcpServers: []
|
||||
});
|
||||
|
||||
this.sessionId = result.sessionId;
|
||||
log(`Session created: ${this.sessionId}`);
|
||||
return { success: true, data: result };
|
||||
} catch (err) {
|
||||
logError(`Failed to create session: ${err.message}`);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(text) {
|
||||
if (!this.sessionId) {
|
||||
return { success: false, error: 'No active session' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.sendAcpRequest('session/prompt', {
|
||||
sessionId: this.sessionId,
|
||||
prompt: [{ type: 'text', text }]
|
||||
});
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (err) {
|
||||
logError(`Prompt failed: ${err.message}`);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (!this.sessionId) {
|
||||
return { success: false, error: 'No active session' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.sendAcpNotification('session/cancel', {
|
||||
sessionId: this.sessionId
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
respondToPermission(requestId, optionId) {
|
||||
this.sendAcpResponse(requestId, {
|
||||
result: {
|
||||
outcome: optionId ? { outcome: 'selected', optionId } : { outcome: 'cancelled' }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.process) {
|
||||
return { success: false, error: 'Qwen CLI is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
this.process.kill('SIGTERM');
|
||||
this.process = null;
|
||||
this.status = 'stopped';
|
||||
this.sessionId = null;
|
||||
|
||||
return { success: true, data: 'Qwen CLI stopped' };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
status: this.status,
|
||||
sessionId: this.sessionId,
|
||||
pid: this.process?.pid || null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global State
|
||||
// ============================================================================
|
||||
|
||||
const acpConnection = new AcpConnection();
|
||||
|
||||
// Check if Qwen CLI is installed
|
||||
function checkQwenInstallation() {
|
||||
async function checkQwenInstallation() {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const checkProcess = spawn('qwen', ['--version'], {
|
||||
@@ -77,255 +507,56 @@ function checkQwenInstallation() {
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
let output = '';
|
||||
checkProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
checkProcess.on('error', () => {
|
||||
resolve(false);
|
||||
resolve({ installed: false });
|
||||
});
|
||||
|
||||
checkProcess.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
if (code === 0) {
|
||||
resolve({ installed: true, version: output.trim() });
|
||||
} else {
|
||||
resolve({ installed: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
checkProcess.kill();
|
||||
resolve(false);
|
||||
resolve({ installed: false });
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
resolve(false);
|
||||
resolve({ installed: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start Qwen CLI process
|
||||
async function startQwenCli(config = {}) {
|
||||
if (qwenProcess) {
|
||||
return { success: false, error: 'Qwen CLI is already running' };
|
||||
}
|
||||
// ============================================================================
|
||||
// Message Handlers
|
||||
// ============================================================================
|
||||
|
||||
try {
|
||||
// Build command arguments
|
||||
const args = [];
|
||||
|
||||
// Add MCP servers if specified
|
||||
if (config.mcpServers && config.mcpServers.length > 0) {
|
||||
for (const server of config.mcpServers) {
|
||||
args.push('mcp', 'add', '--transport', 'http', server, `http://localhost:${config.httpPort || 8080}/mcp/${server}`);
|
||||
args.push('&&');
|
||||
}
|
||||
}
|
||||
|
||||
// Start the CLI server
|
||||
args.push('qwen', 'server');
|
||||
|
||||
if (config.httpPort) {
|
||||
args.push('--port', String(config.httpPort));
|
||||
}
|
||||
|
||||
// Spawn the process
|
||||
qwenProcess = spawn(args.join(' '), {
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
detached: false,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Check if process started successfully
|
||||
if (!qwenProcess || !qwenProcess.pid) {
|
||||
qwenProcess = null;
|
||||
qwenStatus = 'stopped';
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to start Qwen CLI process'
|
||||
};
|
||||
}
|
||||
|
||||
qwenStatus = 'running';
|
||||
|
||||
// Handle process output
|
||||
qwenProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
sendMessage({
|
||||
type: 'event',
|
||||
data: {
|
||||
type: 'qwen_output',
|
||||
content: output
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
qwenProcess.stderr.on('data', (data) => {
|
||||
const error = data.toString();
|
||||
sendMessage({
|
||||
type: 'event',
|
||||
data: {
|
||||
type: 'qwen_error',
|
||||
content: error
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
qwenProcess.on('close', (code) => {
|
||||
qwenProcess = null;
|
||||
qwenStatus = 'stopped';
|
||||
sendMessage({
|
||||
type: 'event',
|
||||
data: {
|
||||
type: 'qwen_stopped',
|
||||
code: code
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wait a bit for the process to start
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Get capabilities
|
||||
qwenCapabilities = await getQwenCapabilities();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
status: 'running',
|
||||
pid: qwenProcess && qwenProcess.pid ? qwenProcess.pid : null,
|
||||
capabilities: qwenCapabilities
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stop Qwen CLI process
|
||||
function stopQwenCli() {
|
||||
if (!qwenProcess) {
|
||||
return { success: false, error: 'Qwen CLI is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
qwenProcess.kill('SIGTERM');
|
||||
qwenProcess = null;
|
||||
qwenStatus = 'stopped';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: 'Qwen CLI stopped'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get Qwen CLI capabilities (MCP servers, tools, etc.)
|
||||
async function getQwenCapabilities() {
|
||||
return new Promise((resolve) => {
|
||||
const checkProcess = spawn('qwen', ['mcp', 'list', '--json'], {
|
||||
shell: true,
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
let output = '';
|
||||
|
||||
checkProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
checkProcess.on('close', () => {
|
||||
try {
|
||||
const capabilities = JSON.parse(output);
|
||||
resolve(capabilities);
|
||||
} catch {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
|
||||
checkProcess.on('error', () => {
|
||||
resolve([]);
|
||||
});
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
checkProcess.kill();
|
||||
resolve([]);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Send request to Qwen CLI via HTTP
|
||||
async function sendToQwenHttp(action, data, config = {}) {
|
||||
const http = require('http');
|
||||
|
||||
const port = config.httpPort || 8080;
|
||||
const hostname = 'localhost';
|
||||
|
||||
const postData = JSON.stringify({
|
||||
action,
|
||||
data
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname,
|
||||
port,
|
||||
path: '/api/process',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(options, (res) => {
|
||||
let responseData = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
responseData += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(responseData);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(new Error('Invalid response from Qwen CLI'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle messages from Chrome extension
|
||||
async function handleMessage(message) {
|
||||
async function handleExtensionMessage(message) {
|
||||
log(`Received from extension: ${JSON.stringify(message)}`);
|
||||
let response;
|
||||
|
||||
switch (message.type) {
|
||||
case 'handshake':
|
||||
// Initial handshake with extension
|
||||
const isInstalled = await checkQwenInstallation();
|
||||
const installInfo = await checkQwenInstallation();
|
||||
response = {
|
||||
type: 'handshake_response',
|
||||
version: '1.0.0',
|
||||
qwenInstalled: isInstalled,
|
||||
qwenStatus: qwenStatus,
|
||||
capabilities: qwenCapabilities
|
||||
qwenInstalled: installInfo.installed,
|
||||
qwenVersion: installInfo.version,
|
||||
qwenStatus: acpConnection.getStatus().status
|
||||
};
|
||||
break;
|
||||
|
||||
case 'start_qwen':
|
||||
// Start Qwen CLI
|
||||
const startResult = await startQwenCli(message.config);
|
||||
const cwd = message.cwd || process.cwd();
|
||||
const startResult = await acpConnection.start(cwd);
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
@@ -334,8 +565,7 @@ async function handleMessage(message) {
|
||||
break;
|
||||
|
||||
case 'stop_qwen':
|
||||
// Stop Qwen CLI
|
||||
const stopResult = stopQwenCli();
|
||||
const stopResult = acpConnection.stop();
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
@@ -343,43 +573,43 @@ async function handleMessage(message) {
|
||||
};
|
||||
break;
|
||||
|
||||
case 'qwen_request':
|
||||
// Send request to Qwen CLI
|
||||
try {
|
||||
if (qwenStatus !== 'running') {
|
||||
throw new Error('Qwen CLI is not running');
|
||||
}
|
||||
case 'qwen_prompt':
|
||||
const promptResult = await acpConnection.prompt(message.text);
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
...promptResult
|
||||
};
|
||||
break;
|
||||
|
||||
const qwenResponse = await sendToQwenHttp(
|
||||
message.action,
|
||||
message.data,
|
||||
message.config
|
||||
);
|
||||
case 'qwen_cancel':
|
||||
const cancelResult = await acpConnection.cancel();
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
...cancelResult
|
||||
};
|
||||
break;
|
||||
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
data: qwenResponse
|
||||
};
|
||||
} catch (error) {
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
case 'permission_response':
|
||||
acpConnection.respondToPermission(message.requestId, message.optionId);
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
success: true
|
||||
};
|
||||
break;
|
||||
|
||||
case 'get_status':
|
||||
// Get current status
|
||||
const status = acpConnection.getStatus();
|
||||
const installStatus = await checkQwenInstallation();
|
||||
response = {
|
||||
type: 'response',
|
||||
id: message.id,
|
||||
data: {
|
||||
qwenInstalled: await checkQwenInstallation(),
|
||||
qwenStatus: qwenStatus,
|
||||
qwenPid: qwenProcess ? qwenProcess.pid : null,
|
||||
capabilities: qwenCapabilities
|
||||
...status,
|
||||
qwenInstalled: installStatus.installed,
|
||||
qwenVersion: installStatus.version
|
||||
}
|
||||
};
|
||||
break;
|
||||
@@ -392,30 +622,31 @@ async function handleMessage(message) {
|
||||
};
|
||||
}
|
||||
|
||||
sendMessage(response);
|
||||
sendMessageToExtension(response);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
function cleanup() {
|
||||
log('Cleaning up...');
|
||||
acpConnection.stop();
|
||||
}
|
||||
|
||||
// Clean up on exit
|
||||
process.on('SIGINT', () => {
|
||||
if (qwenProcess) {
|
||||
qwenProcess.kill();
|
||||
}
|
||||
cleanup();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
if (qwenProcess) {
|
||||
qwenProcess.kill();
|
||||
}
|
||||
cleanup();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Log function for debugging (writes to a file since stdout is used for messaging)
|
||||
function log(message) {
|
||||
const logFile = path.join(os.tmpdir(), 'qwen-bridge-host.log');
|
||||
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${message}\n`);
|
||||
}
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
// Main execution
|
||||
log('Native host started');
|
||||
readMessages();
|
||||
log('Native host started (ACP mode)');
|
||||
readMessagesFromExtension();
|
||||
|
||||
Reference in New Issue
Block a user