opencode / tui-server.js
tanbushi's picture
update
f11170b
import express from 'express';
import cors from 'cors';
import { config } from 'dotenv';
import { spawn } from 'child_process';
import { WebSocketServer } from 'ws';
import http from 'http';
// 加载环境变量
config();
const app = express();
const PORT = process.env.PORT || 7860;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// OpenCode CLI 进程管理
let opencodeProcess = null;
let opencodeStdin = null;
let opencodeStdout = null;
let opencodeStderr = null;
// 启动 OpenCode CLI
function startOpenCode() {
console.log('🚀 Starting OpenCode CLI TUI...');
const args = ['--no-color']; // 禁用颜色以更好地在 Web 中显示
opencodeProcess = spawn('opencode', args, {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
OPENCODE_DISABLE_AUTOUPDATE: 'true',
OPENCODE_CLIENT: 'hf-tui-web',
TERM: 'xterm-256color',
COLORTERM: 'truecolor'
}
});
// 设置输入输出流
opencodeStdin = opencodeProcess.stdin;
opencodeStdout = opencodeProcess.stdout;
opencodeStderr = opencodeProcess.stderr;
// 处理输出
opencodeStdout.on('data', (data) => {
broadcastToTerminals(data.toString());
});
opencodeStderr.on('data', (data) => {
broadcastToTerminals(data.toString());
});
// 处理进程结束
opencodeProcess.on('close', (code) => {
console.log(`OpenCode process exited with code ${code}`);
opencodeProcess = null;
});
opencodeProcess.on('error', (error) => {
console.error('OpenCode process error:', error);
opencodeProcess = null;
});
// 发送初始提示
setTimeout(() => {
if (opencodeStdout) {
opencodeStdin.write('\n'); // 发送回车以激活界面
}
}, 2000);
return opencodeProcess;
}
// WebSocket 连接管理
const terminals = new Set();
function broadcastToTerminals(data) {
terminals.forEach((ws) => {
if (ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({ type: 'data', data }));
}
});
}
// 创建 HTTP 服务器以支持 WebSocket
const server = http.createServer(app);
// WebSocket 服务器
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
console.log('🔗 New terminal connection');
terminals.add(ws);
// 发送欢迎消息
ws.send(JSON.stringify({
type: 'welcome',
message: 'Connected to OpenCode CLI TUI'
}));
// 如果 OpenCode 没有运行,启动它
if (!opencodeProcess) {
startOpenCode();
}
// 处理来自终端的输入
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'input' && opencodeStdin) {
// 将用户输入发送到 OpenCode
opencodeStdin.write(data.input);
}
} catch (error) {
console.error('WebSocket message error:', error);
}
});
// 处理连接关闭
ws.on('close', () => {
console.log('🔌 Terminal connection closed');
terminals.delete(ws);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
terminals.delete(ws);
});
});
// 健康检查端点
app.get('/health', (req, res) => {
const isRunning = opencodeProcess !== null;
res.status(isRunning ? 200 : 503).json({
status: isRunning ? 'ok' : 'error',
timestamp: new Date().toISOString(),
service: 'OpenCode CLI TUI Web',
opencode_running: isRunning,
active_terminals: terminals.size
});
});
// 重启 OpenCode CLI 端点
app.post('/api/restart', (req, res) => {
if (opencodeProcess) {
opencodeProcess.kill('SIGTERM');
}
setTimeout(() => {
startOpenCode();
res.json({ success: true, message: 'OpenCode CLI restarted' });
}, 1000);
});
// 向 OpenCode 发送命令端点
app.post('/api/command', (req, res) => {
const { command } = req.body;
if (!opencodeStdin) {
return res.status(503).json({ error: 'OpenCode CLI not running' });
}
if (command) {
opencodeStdin.write(command + '\n');
res.json({ success: true, command });
} else {
res.status(400).json({ error: 'Command is required' });
}
});
// API 信息端点
app.get('/api', (req, res) => {
res.json({
message: 'OpenCode CLI TUI Web Server',
version: '1.0.0',
endpoints: {
health: '/health',
restart: '/api/restart',
command: '/api/command',
websocket: 'WebSocket (ws://host:port)'
},
features: {
tui_embedding: true,
realtime_output: true,
multi_user: true,
opencode_integration: true
}
});
});
// 主页面
app.get('/', (req, res) => {
res.sendFile('index.html', { root: 'public' });
});
// 启动服务器
server.listen(PORT, '0.0.0.0', () => {
console.log(`🌐 OpenCode CLI TUI Web Server running on port ${PORT}`);
console.log(`📱 Access at: http://0.0.0.0:${PORT}`);
console.log(`🔌 WebSocket endpoint: ws://0.0.0.0:${PORT}`);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log('🛑 Received SIGTERM, shutting down gracefully...');
// 关闭所有 WebSocket 连接
terminals.forEach((ws) => {
ws.close();
});
// 终止 OpenCode 进程
if (opencodeProcess) {
opencodeProcess.kill('SIGTERM');
}
server.close(() => {
console.log('🔚 Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('🛑 Received SIGINT, shutting down gracefully...');
// 关闭所有 WebSocket 连接
terminals.forEach((ws) => {
ws.close();
});
// 终止 OpenCode 进程
if (opencodeProcess) {
opencodeProcess.kill('SIGINT');
}
server.close(() => {
console.log('🔚 Server closed');
process.exit(0);
});
});
export default app;