chore(chrome-qwen-bridge): connect

This commit is contained in:
yiliang114
2025-12-20 15:54:20 +08:00
parent a60c5c6697
commit a1f893f0c6
3 changed files with 546 additions and 323 deletions

View File

@@ -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

View 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"

View File

@@ -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) => {
log(`Received ${chunk.length} bytes from extension`);
chunks.push(chunk); 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);
try {
const message = JSON.parse(messageBuffer.toString()); 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
}); });
checkProcess.on('error', () => {
resolve(false);
});
checkProcess.on('close', (code) => {
resolve(code === 0);
});
// Timeout after 5 seconds
setTimeout(() => {
checkProcess.kill();
resolve(false);
}, 5000);
} catch (error) {
resolve(false);
}
});
}
// Start Qwen CLI process
async function startQwenCli(config = {}) {
if (qwenProcess) {
return { success: false, error: 'Qwen CLI is already running' };
}
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 = ''; let output = '';
checkProcess.stdout.on('data', (data) => { checkProcess.stdout.on('data', (data) => {
output += data.toString(); output += data.toString();
}); });
checkProcess.on('close', () => { checkProcess.on('error', () => {
try { resolve({ installed: false });
const capabilities = JSON.parse(output); });
resolve(capabilities);
} catch { checkProcess.on('close', (code) => {
resolve([]); if (code === 0) {
resolve({ installed: true, version: output.trim() });
} else {
resolve({ installed: false });
} }
}); });
checkProcess.on('error', () => {
resolve([]);
});
// Timeout after 5 seconds
setTimeout(() => { setTimeout(() => {
checkProcess.kill(); checkProcess.kill();
resolve([]); resolve({ installed: false });
}, 5000); }, 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) { } catch (error) {
reject(new Error('Invalid response from Qwen CLI')); resolve({ installed: false });
} }
}); });
});
req.on('error', (error) => {
reject(error);
});
req.write(postData);
req.end();
});
} }
// Handle messages from Chrome extension // ============================================================================
async function handleMessage(message) { // Message Handlers
// ============================================================================
async function handleExtensionMessage(message) {
log(`Received from extension: ${JSON.stringify(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 {
if (qwenStatus !== 'running') {
throw new Error('Qwen CLI is not running');
}
const qwenResponse = await sendToQwenHttp(
message.action,
message.data,
message.config
);
response = { response = {
type: 'response', type: 'response',
id: message.id, id: message.id,
data: qwenResponse ...promptResult
}; };
} catch (error) { break;
case 'qwen_cancel':
const cancelResult = await acpConnection.cancel();
response = { response = {
type: 'response', type: 'response',
id: message.id, id: message.id,
error: error.message ...cancelResult
};
break;
case 'permission_response':
acpConnection.respondToPermission(message.requestId, message.optionId);
response = {
type: 'response',
id: message.id,
success: true
}; };
}
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();