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 已配置"
|
echo -e "${GREEN}✓${NC} Native Host 已配置"
|
||||||
|
|
||||||
# 第三步:检查 Qwen CLI(可选)
|
# 第三步:检查 Qwen CLI
|
||||||
echo -e "\n${BLUE}[3/5]${NC} 检查 Qwen CLI..."
|
echo -e "\n${BLUE}[3/5]${NC} 检查 Qwen CLI..."
|
||||||
|
|
||||||
QWEN_AVAILABLE=false
|
QWEN_AVAILABLE=false
|
||||||
if command -v qwen &> /dev/null; then
|
if command -v qwen &> /dev/null; then
|
||||||
QWEN_AVAILABLE=true
|
QWEN_AVAILABLE=true
|
||||||
echo -e "${GREEN}✓${NC} Qwen CLI $(qwen --version 2>/dev/null || echo "已安装")"
|
QWEN_VERSION=$(qwen --version 2>/dev/null || echo "已安装")
|
||||||
|
echo -e "${GREEN}✓${NC} Qwen CLI ${QWEN_VERSION}"
|
||||||
# 尝试启动 Qwen server
|
echo -e "${CYAN}→${NC} 使用 ACP 模式与 Chrome 插件通信"
|
||||||
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
|
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}!${NC} Qwen CLI 未安装(插件基础功能仍可使用)"
|
echo -e "${YELLOW}!${NC} Qwen CLI 未安装(插件基础功能仍可使用)"
|
||||||
|
echo -e " 安装方法: npm install -g @anthropic-ai/qwen-code"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 第四步:启动测试页面
|
# 第四步:启动测试页面
|
||||||
@@ -338,7 +324,7 @@ echo -e " • 测试页面: ${BLUE}http://localhost:3000/qwen-test.html${NC}"
|
|||||||
echo -e " • 插件: 已加载到工具栏"
|
echo -e " • 插件: 已加载到工具栏"
|
||||||
|
|
||||||
if [ "$QWEN_AVAILABLE" = true ]; then
|
if [ "$QWEN_AVAILABLE" = true ]; then
|
||||||
echo -e " • Qwen Server: ${BLUE}http://localhost:8080${NC}"
|
echo -e " • Qwen CLI: 可用 (ACP 模式)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -347,10 +333,6 @@ echo -e " • 插件日志: Chrome DevTools Console"
|
|||||||
echo -e " • 后台脚本: chrome://extensions → Service Worker"
|
echo -e " • 后台脚本: chrome://extensions → Service Worker"
|
||||||
echo -e " • Native Host: /tmp/qwen-bridge-host.log"
|
echo -e " • Native Host: /tmp/qwen-bridge-host.log"
|
||||||
|
|
||||||
if [ "$QWEN_AVAILABLE" = true ]; then
|
|
||||||
echo -e " • Qwen 日志: /tmp/qwen-server.log"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
|
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -361,7 +343,6 @@ cleanup() {
|
|||||||
|
|
||||||
# 停止进程
|
# 停止进程
|
||||||
[ ! -z "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
|
[ ! -z "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
|
||||||
[ ! -z "$QWEN_PID" ] && kill $QWEN_PID 2>/dev/null
|
|
||||||
|
|
||||||
echo -e "${GREEN}✓${NC} 已停止所有服务"
|
echo -e "${GREEN}✓${NC} 已停止所有服务"
|
||||||
exit 0
|
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
|
* Native Messaging Host for Qwen CLI Bridge
|
||||||
* This script acts as a bridge between the Chrome extension and Qwen CLI
|
* 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');
|
const { spawn } = require('child_process');
|
||||||
@@ -10,8 +11,31 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
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 buffer = Buffer.from(JSON.stringify(message));
|
||||||
const length = Buffer.allocUnsafe(4);
|
const length = Buffer.allocUnsafe(4);
|
||||||
length.writeUInt32LE(buffer.length, 0);
|
length.writeUInt32LE(buffer.length, 0);
|
||||||
@@ -20,56 +44,462 @@ function sendMessage(message) {
|
|||||||
process.stdout.write(buffer);
|
process.stdout.write(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readMessages() {
|
function readMessagesFromExtension() {
|
||||||
let messageLength = null;
|
let messageLength = null;
|
||||||
let chunks = [];
|
let chunks = [];
|
||||||
|
|
||||||
process.stdin.on('readable', () => {
|
// Keep stdin open and in flowing mode
|
||||||
let chunk;
|
process.stdin.resume();
|
||||||
|
|
||||||
while ((chunk = process.stdin.read()) !== null) {
|
process.stdin.on('data', (chunk) => {
|
||||||
chunks.push(chunk);
|
log(`Received ${chunk.length} bytes from extension`);
|
||||||
|
chunks.push(chunk);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
const buffer = Buffer.concat(chunks);
|
const buffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
// Read message length if we haven't yet
|
// Need at least 4 bytes for length
|
||||||
if (messageLength === null) {
|
if (messageLength === null) {
|
||||||
if (buffer.length >= 4) {
|
if (buffer.length < 4) break;
|
||||||
messageLength = buffer.readUInt32LE(0);
|
messageLength = buffer.readUInt32LE(0);
|
||||||
chunks = [buffer.slice(4)];
|
chunks = [buffer.slice(4)];
|
||||||
}
|
log(`Message length: ${messageLength}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read message if we have the full length
|
// Check if we have the full message
|
||||||
if (messageLength !== null) {
|
const fullBuffer = Buffer.concat(chunks);
|
||||||
const fullBuffer = Buffer.concat(chunks);
|
if (fullBuffer.length < messageLength) break;
|
||||||
|
|
||||||
if (fullBuffer.length >= messageLength) {
|
// Extract and parse message
|
||||||
const messageBuffer = fullBuffer.slice(0, messageLength);
|
const messageBuffer = fullBuffer.slice(0, messageLength);
|
||||||
const message = JSON.parse(messageBuffer.toString());
|
try {
|
||||||
|
const message = JSON.parse(messageBuffer.toString());
|
||||||
|
log(`Received message: ${JSON.stringify(message)}`);
|
||||||
|
|
||||||
// Reset for next message
|
// Reset for next message
|
||||||
chunks = [fullBuffer.slice(messageLength)];
|
chunks = [fullBuffer.slice(messageLength)];
|
||||||
messageLength = null;
|
messageLength = null;
|
||||||
|
|
||||||
// Handle the message
|
// Handle the message
|
||||||
handleMessage(message);
|
handleExtensionMessage(message);
|
||||||
}
|
} catch (err) {
|
||||||
|
logError(`Failed to parse message: ${err.message}`);
|
||||||
|
chunks = [fullBuffer.slice(messageLength)];
|
||||||
|
messageLength = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
log('stdin ended');
|
||||||
|
cleanup();
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.stdin.on('error', (err) => {
|
||||||
|
logError(`stdin error: ${err.message}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Qwen CLI process management
|
// ============================================================================
|
||||||
let qwenProcess = null;
|
// ACP Protocol (Native Host <-> Qwen CLI)
|
||||||
let qwenStatus = 'disconnected';
|
// ============================================================================
|
||||||
let qwenCapabilities = [];
|
|
||||||
|
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
|
// Check if Qwen CLI is installed
|
||||||
function checkQwenInstallation() {
|
async function checkQwenInstallation() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const checkProcess = spawn('qwen', ['--version'], {
|
const checkProcess = spawn('qwen', ['--version'], {
|
||||||
@@ -77,255 +507,56 @@ function checkQwenInstallation() {
|
|||||||
windowsHide: true
|
windowsHide: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
checkProcess.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
checkProcess.on('error', () => {
|
checkProcess.on('error', () => {
|
||||||
resolve(false);
|
resolve({ installed: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
checkProcess.on('close', (code) => {
|
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(() => {
|
setTimeout(() => {
|
||||||
checkProcess.kill();
|
checkProcess.kill();
|
||||||
resolve(false);
|
resolve({ installed: false });
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resolve(false);
|
resolve({ installed: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start Qwen CLI process
|
// ============================================================================
|
||||||
async function startQwenCli(config = {}) {
|
// Message Handlers
|
||||||
if (qwenProcess) {
|
// ============================================================================
|
||||||
return { success: false, error: 'Qwen CLI is already running' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
async function handleExtensionMessage(message) {
|
||||||
// Build command arguments
|
log(`Received from extension: ${JSON.stringify(message)}`);
|
||||||
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) {
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'handshake':
|
case 'handshake':
|
||||||
// Initial handshake with extension
|
const installInfo = await checkQwenInstallation();
|
||||||
const isInstalled = await checkQwenInstallation();
|
|
||||||
response = {
|
response = {
|
||||||
type: 'handshake_response',
|
type: 'handshake_response',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
qwenInstalled: isInstalled,
|
qwenInstalled: installInfo.installed,
|
||||||
qwenStatus: qwenStatus,
|
qwenVersion: installInfo.version,
|
||||||
capabilities: qwenCapabilities
|
qwenStatus: acpConnection.getStatus().status
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'start_qwen':
|
case 'start_qwen':
|
||||||
// Start Qwen CLI
|
const cwd = message.cwd || process.cwd();
|
||||||
const startResult = await startQwenCli(message.config);
|
const startResult = await acpConnection.start(cwd);
|
||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
@@ -334,8 +565,7 @@ async function handleMessage(message) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'stop_qwen':
|
case 'stop_qwen':
|
||||||
// Stop Qwen CLI
|
const stopResult = acpConnection.stop();
|
||||||
const stopResult = stopQwenCli();
|
|
||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
@@ -343,43 +573,43 @@ async function handleMessage(message) {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'qwen_request':
|
case 'qwen_prompt':
|
||||||
// Send request to Qwen CLI
|
const promptResult = await acpConnection.prompt(message.text);
|
||||||
try {
|
response = {
|
||||||
if (qwenStatus !== 'running') {
|
type: 'response',
|
||||||
throw new Error('Qwen CLI is not running');
|
id: message.id,
|
||||||
}
|
...promptResult
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
const qwenResponse = await sendToQwenHttp(
|
case 'qwen_cancel':
|
||||||
message.action,
|
const cancelResult = await acpConnection.cancel();
|
||||||
message.data,
|
response = {
|
||||||
message.config
|
type: 'response',
|
||||||
);
|
id: message.id,
|
||||||
|
...cancelResult
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
response = {
|
case 'permission_response':
|
||||||
type: 'response',
|
acpConnection.respondToPermission(message.requestId, message.optionId);
|
||||||
id: message.id,
|
response = {
|
||||||
data: qwenResponse
|
type: 'response',
|
||||||
};
|
id: message.id,
|
||||||
} catch (error) {
|
success: true
|
||||||
response = {
|
};
|
||||||
type: 'response',
|
|
||||||
id: message.id,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_status':
|
case 'get_status':
|
||||||
// Get current status
|
const status = acpConnection.getStatus();
|
||||||
|
const installStatus = await checkQwenInstallation();
|
||||||
response = {
|
response = {
|
||||||
type: 'response',
|
type: 'response',
|
||||||
id: message.id,
|
id: message.id,
|
||||||
data: {
|
data: {
|
||||||
qwenInstalled: await checkQwenInstallation(),
|
...status,
|
||||||
qwenStatus: qwenStatus,
|
qwenInstalled: installStatus.installed,
|
||||||
qwenPid: qwenProcess ? qwenProcess.pid : null,
|
qwenVersion: installStatus.version
|
||||||
capabilities: qwenCapabilities
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
break;
|
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', () => {
|
process.on('SIGINT', () => {
|
||||||
if (qwenProcess) {
|
cleanup();
|
||||||
qwenProcess.kill();
|
|
||||||
}
|
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
if (qwenProcess) {
|
cleanup();
|
||||||
qwenProcess.kill();
|
|
||||||
}
|
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log function for debugging (writes to a file since stdout is used for messaging)
|
// ============================================================================
|
||||||
function log(message) {
|
// Main
|
||||||
const logFile = path.join(os.tmpdir(), 'qwen-bridge-host.log');
|
// ============================================================================
|
||||||
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${message}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main execution
|
log('Native host started (ACP mode)');
|
||||||
log('Native host started');
|
readMessagesFromExtension();
|
||||||
readMessages();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user