• 简体中文
  • 终端集成示例

    本节提供与 AIO Sandbox 的 WebSocket 终端接口集成的全面示例。

    基本终端客户端

    简单的 Node.js 实现

    const WebSocket = require('ws');
    
    class TerminalClient {
        constructor(baseUrl = 'ws://localhost:8080') {
            this.baseUrl = baseUrl;
            this.ws = null;
            this.sessionId = null;
        }
    
        connect(sessionId = null) {
            const wsUrl = `${this.baseUrl}/v1/shell/ws${sessionId ? `?session_id=${sessionId}` : ''}`;
    
            this.ws = new WebSocket(wsUrl);
    
            this.ws.on('open', () => {
                console.log('已连接到终端');
            });
    
            this.ws.on('message', (data) => {
                const message = JSON.parse(data.toString());
                this.handleMessage(message);
            });
    
            this.ws.on('close', () => {
                console.log('终端连接已关闭');
            });
    
            this.ws.on('error', (error) => {
                console.error('终端错误:', error);
            });
        }
    
        handleMessage(message) {
            const { type, data } = message;
    
            switch (type) {
                case 'session_id':
                    this.sessionId = data;
                    console.log('会话 ID:', data);
                    break;
                case 'output':
                    process.stdout.write(data);
                    break;
                case 'ready':
                    console.log('终端就绪:', data);
                    break;
                case 'ping':
                    this.sendPong(data);
                    break;
                case 'error':
                    console.error('服务器错误:', data);
                    break;
            }
        }
    
        sendCommand(command) {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({
                    type: 'input',
                    data: command + '\n'
                }));
            }
        }
    
        sendPong(timestamp) {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({
                    type: 'pong',
                    timestamp: timestamp
                }));
            }
        }
    
        resize(cols, rows) {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({
                    type: 'resize',
                    data: { cols, rows }
                }));
            }
        }
    
        disconnect() {
            if (this.ws) {
                this.ws.close();
            }
        }
    }
    
    // 使用示例
    const terminal = new TerminalClient();
    terminal.connect();
    
    // 连接后发送命令
    setTimeout(() => {
        terminal.sendCommand('ls -la');
        terminal.sendCommand('pwd');
        terminal.sendCommand('echo "你好,来自 AIO Sandbox!"');
    }, 1000);

    Python Asyncio 实现

    import asyncio
    import websockets
    import json
    
    class AsyncTerminalClient:
        def __init__(self, base_url="ws://localhost:8080"):
            self.base_url = base_url
            self.websocket = None
            self.session_id = None
    
        async def connect(self, session_id=None):
            ws_url = f"{self.base_url}/v1/shell/ws"
            if session_id:
                ws_url += f"?session_id={session_id}"
    
            self.websocket = await websockets.connect(ws_url)
            print("已连接到终端")
    
            # 启动消息处理器
            asyncio.create_task(self.handle_messages())
    
        async def handle_messages(self):
            try:
                async for message in self.websocket:
                    data = json.loads(message)
                    await self.process_message(data)
            except websockets.exceptions.ConnectionClosed:
                print("终端连接已关闭")
            except Exception as e:
                print(f"处理消息时出错:{e}")
    
        async def process_message(self, message):
            msg_type = message.get('type')
            data = message.get('data')
    
            if msg_type == 'session_id':
                self.session_id = data
                print(f"会话 ID:{data}")
            elif msg_type == 'output':
                print(data, end='')
            elif msg_type == 'ready':
                print(f"终端就绪:{data}")
            elif msg_type == 'ping':
                await self.send_pong(data)
            elif msg_type == 'error':
                print(f"服务器错误:{data}")
    
        async def send_command(self, command):
            if self.websocket:
                await self.websocket.send(json.dumps({
                    'type': 'input',
                    'data': command + '\n'
                }))
    
        async def send_pong(self, timestamp):
            if self.websocket:
                await self.websocket.send(json.dumps({
                    'type': 'pong',
                    'timestamp': timestamp
                }))
    
        async def resize_terminal(self, cols, rows):
            if self.websocket:
                await self.websocket.send(json.dumps({
                    'type': 'resize',
                    'data': {'cols': cols, 'rows': rows}
                }))
    
        async def disconnect(self):
            if self.websocket:
                await self.websocket.close()
    
    # 使用示例
    async def main():
        client = AsyncTerminalClient()
        await client.connect()
    
        # 等待连接建立
        await asyncio.sleep(1)
    
        # 发送一些命令
        await client.send_command('ls -la')
        await client.send_command('whoami')
        await client.send_command('python3 --version')
    
        # 保持活动以接收输出
        await asyncio.sleep(5)
    
        await client.disconnect()
    
    # 运行示例
    asyncio.run(main())

    高级功能

    会话管理和重连

    class AdvancedTerminalClient extends TerminalClient {
        constructor(baseUrl = 'ws://localhost:8080') {
            super(baseUrl);
            this.reconnectAttempts = 0;
            this.maxReconnectAttempts = 5;
            this.reconnectDelay = 1000;
            this.heartbeatInterval = null;
        }
    
        connect(sessionId = null) {
            super.connect(sessionId);
    
            this.ws.on('close', (code) => {
                this.stopHeartbeat();
                if (code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
                    console.log(`连接关闭(${code})。尝试重连...`);
                    this.reconnect();
                }
            });
    
            this.ws.on('open', () => {
                this.reconnectAttempts = 0;
                this.startHeartbeat();
                console.log('已连接到带会话管理的终端');
            });
        }
    
        reconnect() {
            this.reconnectAttempts++;
            setTimeout(() => {
                console.log(`重连尝试 ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
                this.connect(this.sessionId); // 重连到现有会话
            }, this.reconnectDelay * this.reconnectAttempts);
        }
    
        startHeartbeat() {
            this.heartbeatInterval = setInterval(() => {
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                    this.ws.send(JSON.stringify({
                        type: 'ping',
                        timestamp: Date.now()
                    }));
                }
            }, 30000); // 每 30 秒 ping 一次
        }
    
        stopHeartbeat() {
            if (this.heartbeatInterval) {
                clearInterval(this.heartbeatInterval);
                this.heartbeatInterval = null;
            }
        }
    
        async executeCommand(command, timeout = 10000) {
            return new Promise((resolve, reject) => {
                let output = '';
                let timeoutId;
    
                const originalHandler = this.handleMessage.bind(this);
    
                this.handleMessage = (message) => {
                    originalHandler(message);
    
                    if (message.type === 'output') {
                        output += message.data;
    
                        // 简单的命令完成检测
                        if (output.includes('$') && output.trim().endsWith('$')) {
                            clearTimeout(timeoutId);
                            this.handleMessage = originalHandler;
                            resolve(output);
                        }
                    }
                };
    
                timeoutId = setTimeout(() => {
                    this.handleMessage = originalHandler;
                    reject(new Error('命令超时'));
                }, timeout);
    
                this.sendCommand(command);
            });
        }
    }
    
    // 使用会话持久性
    const terminalWithSession = new AdvancedTerminalClient();
    terminalWithSession.connect();
    
    // 使用 Promise 执行命令的示例
    terminalWithSession.executeCommand('ls -la').then(output => {
        console.log('命令输出:', output);
    }).catch(error => {
        console.error('命令失败:', error);
    });

    多会话管理

    import asyncio
    import websockets
    import json
    from typing import Dict, Optional
    
    class MultiSessionTerminal:
        def __init__(self, base_url="ws://localhost:8080"):
            self.base_url = base_url
            self.sessions: Dict[str, websockets.WebSocketServerProtocol] = {}
            self.session_outputs: Dict[str, str] = {}
    
        async def create_session(self, session_name: str) -> str:
            """创建新的终端会话"""
            ws_url = f"{self.base_url}/v1/shell/ws"
            websocket = await websockets.connect(ws_url)
    
            # 等待会话 ID
            session_id = None
            async for message in websocket:
                data = json.loads(message)
                if data.get('type') == 'session_id':
                    session_id = data.get('data')
                    break
    
            if session_id:
                self.sessions[session_name] = websocket
                self.session_outputs[session_name] = ""
    
                # 为此会话启动消息处理器
                asyncio.create_task(self.handle_session_messages(session_name))
    
                return session_id
            else:
                raise Exception("获取会话 ID 失败")
    
        async def connect_to_session(self, session_name: str, session_id: str):
            """连接到现有会话"""
            ws_url = f"{self.base_url}/v1/shell/ws?session_id={session_id}"
            websocket = await websockets.connect(ws_url)
    
            self.sessions[session_name] = websocket
            if session_name not in self.session_outputs:
                self.session_outputs[session_name] = ""
    
            asyncio.create_task(self.handle_session_messages(session_name))
    
        async def handle_session_messages(self, session_name: str):
            """处理特定会话的消息"""
            websocket = self.sessions[session_name]
    
            try:
                async for message in websocket:
                    data = json.loads(message)
                    await self.process_session_message(session_name, data)
            except websockets.exceptions.ConnectionClosed:
                print(f"会话 {session_name} 连接已关闭")
                if session_name in self.sessions:
                    del self.sessions[session_name]
    
        async def process_session_message(self, session_name: str, message):
            msg_type = message.get('type')
            data = message.get('data')
    
            if msg_type == 'output':
                self.session_outputs[session_name] += data
                print(f"[{session_name}] {data}", end='')
            elif msg_type == 'ready':
                print(f"[{session_name}] 终端就绪:{data}")
            elif msg_type == 'ping':
                await self.send_pong(session_name, data)
    
        async def send_command(self, session_name: str, command: str):
            """向特定会话发送命令"""
            if session_name in self.sessions:
                websocket = self.sessions[session_name]
                await websocket.send(json.dumps({
                    'type': 'input',
                    'data': command + '\n'
                }))
            else:
                raise Exception(f"会话 {session_name} 未找到")
    
        async def send_pong(self, session_name: str, timestamp):
            if session_name in self.sessions:
                websocket = self.sessions[session_name]
                await websocket.send(json.dumps({
                    'type': 'pong',
                    'timestamp': timestamp
                }))
    
        async def get_session_output(self, session_name: str) -> str:
            """获取会话的累积输出"""
            return self.session_outputs.get(session_name, "")
    
        async def close_session(self, session_name: str):
            """关闭特定会话"""
            if session_name in self.sessions:
                await self.sessions[session_name].close()
                del self.sessions[session_name]
                if session_name in self.session_outputs:
                    del self.session_outputs[session_name]
    
        async def close_all_sessions(self):
            """关闭所有会话"""
            for session_name in list(self.sessions.keys()):
                await self.close_session(session_name)
    
    # 使用示例
    async def multi_session_demo():
        terminal = MultiSessionTerminal()
    
        # 创建多个会话
        session1_id = await terminal.create_session("build")
        session2_id = await terminal.create_session("test")
        session3_id = await terminal.create_session("dev")
    
        print(f"创建的会话:build={session1_id}, test={session2_id}, dev={session3_id}")
    
        # 等待会话准备就绪
        await asyncio.sleep(2)
    
        # 并行运行不同的命令
        await asyncio.gather(
            terminal.send_command("build", "npm run build"),
            terminal.send_command("test", "npm test"),
            terminal.send_command("dev", "npm run dev")
        )
    
        # 让命令运行
        await asyncio.sleep(10)
    
        # 获取输出
        build_output = await terminal.get_session_output("build")
        test_output = await terminal.get_session_output("test")
    
        print("构建输出长度:", len(build_output))
        print("测试输出长度:", len(test_output))
    
        # 清理
        await terminal.close_all_sessions()
    
    asyncio.run(multi_session_demo())

    与流行库的集成

    xterm.js 前端集成

    <!DOCTYPE html>
    <html>
    <head>
        <title>AIO Sandbox 终端</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
        <style>
            #terminal { height: 400px; width: 100%; }
            .controls { margin: 10px 0; }
            button { margin: 5px; padding: 10px; }
        </style>
    </head>
    <body>
        <h1>AIO Sandbox 终端界面</h1>
    
        <div class="controls">
            <button onclick="connectTerminal()">连接</button>
            <button onclick="disconnectTerminal()">断开</button>
            <button onclick="clearTerminal()">清除</button>
            <button onclick="resizeTerminal()">适应</button>
        </div>
    
        <div id="terminal"></div>
    
        <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
    
        <script>
            let terminal, websocket, fitAddon;
    
            function initTerminal() {
                terminal = new Terminal({
                    cursorBlink: true,
                    theme: {
                        background: '#1e1e1e',
                        foreground: '#ffffff'
                    }
                });
    
                fitAddon = new FitAddon.FitAddon();
                terminal.loadAddon(fitAddon);
    
                terminal.open(document.getElementById('terminal'));
                fitAddon.fit();
    
                terminal.onData(data => {
                    if (websocket && websocket.readyState === WebSocket.OPEN) {
                        websocket.send(JSON.stringify({
                            type: 'input',
                            data: data
                        }));
                    }
                });
    
                terminal.onResize(({ cols, rows }) => {
                    if (websocket && websocket.readyState === WebSocket.OPEN) {
                        websocket.send(JSON.stringify({
                            type: 'resize',
                            data: { cols, rows }
                        }));
                    }
                });
            }
    
            function connectTerminal() {
                websocket = new WebSocket('ws://localhost:8080/v1/shell/ws');
    
                websocket.onopen = () => {
                    terminal.write('\\r\\n\\x1b[32m已连接到 AIO Sandbox\\x1b[0m\\r\\n');
                };
    
                websocket.onmessage = (event) => {
                    const message = JSON.parse(event.data);
                    handleTerminalMessage(message);
                };
    
                websocket.onclose = () => {
                    terminal.write('\\r\\n\\x1b[31m连接已关闭\\x1b[0m\\r\\n');
                };
            }
    
            function handleTerminalMessage(message) {
                const { type, data } = message;
    
                switch (type) {
                    case 'output':
                        terminal.write(data);
                        break;
                    case 'ready':
                        terminal.write(`\\r\\n\\x1b[32m${data}\\x1b[0m\\r\\n`);
                        break;
                    case 'session_id':
                        console.log('会话 ID:', data);
                        break;
                    case 'ping':
                        websocket.send(JSON.stringify({
                            type: 'pong',
                            timestamp: data
                        }));
                        break;
                }
            }
    
            function disconnectTerminal() {
                if (websocket) {
                    websocket.close();
                }
            }
    
            function clearTerminal() {
                terminal.clear();
            }
    
            function resizeTerminal() {
                fitAddon.fit();
            }
    
            // 页面加载时初始化
            window.onload = () => {
                initTerminal();
            };
    
            // 窗口调整大小时自动调整
            window.onresize = () => {
                if (fitAddon) {
                    fitAddon.fit();
                }
            };
        </script>
    </body>
    </html>

    React 组件

    import React, { useEffect, useRef, useState } from 'react';
    import { Terminal } from 'xterm';
    import { FitAddon } from 'xterm-addon-fit';
    import 'xterm/css/xterm.css';
    
    const AIOTerminal = ({ baseUrl = 'ws://localhost:8080' }) => {
        const terminalRef = useRef(null);
        const [terminal, setTerminal] = useState(null);
        const [websocket, setWebsocket] = useState(null);
        const [connected, setConnected] = useState(false);
        const [sessionId, setSessionId] = useState(null);
        const fitAddonRef = useRef(null);
    
        useEffect(() => {
            // 初始化终端
            const term = new Terminal({
                cursorBlink: true,
                theme: {
                    background: '#1e1e1e',
                    foreground: '#ffffff'
                }
            });
    
            const fitAddon = new FitAddon();
            term.loadAddon(fitAddon);
            fitAddonRef.current = fitAddon;
    
            term.open(terminalRef.current);
            fitAddon.fit();
    
            setTerminal(term);
    
            // 处理用户输入
            term.onData(data => {
                if (websocket && websocket.readyState === WebSocket.OPEN) {
                    websocket.send(JSON.stringify({
                        type: 'input',
                        data: data
                    }));
                }
            });
    
            // 处理终端调整大小
            term.onResize(({ cols, rows }) => {
                if (websocket && websocket.readyState === WebSocket.OPEN) {
                    websocket.send(JSON.stringify({
                        type: 'resize',
                        data: { cols, rows }
                    }));
                }
            });
    
            return () => {
                term.dispose();
            };
        }, []);
    
        const connect = () => {
            const ws = new WebSocket(`${baseUrl}/v1/shell/ws`);
    
            ws.onopen = () => {
                setConnected(true);
                terminal.write('\\r\\n\\x1b[32m已连接到 AIO Sandbox\\x1b[0m\\r\\n');
            };
    
            ws.onmessage = (event) => {
                const message = JSON.parse(event.data);
                handleMessage(message);
            };
    
            ws.onclose = () => {
                setConnected(false);
                terminal.write('\\r\\n\\x1b[31m连接已关闭\\x1b[0m\\r\\n');
            };
    
            ws.onerror = (error) => {
                console.error('WebSocket 错误:', error);
                terminal.write('\\r\\n\\x1b[31m连接错误\\x1b[0m\\r\\n');
            };
    
            setWebsocket(ws);
        };
    
        const handleMessage = (message) => {
            const { type, data } = message;
    
            switch (type) {
                case 'session_id':
                    setSessionId(data);
                    break;
                case 'output':
                    terminal.write(data);
                    break;
                case 'ready':
                    terminal.write(`\\r\\n\\x1b[32m${data}\\x1b[0m\\r\\n`);
                    break;
                case 'ping':
                    if (websocket) {
                        websocket.send(JSON.stringify({
                            type: 'pong',
                            timestamp: data
                        }));
                    }
                    break;
                case 'error':
                    terminal.write(`\\r\\n\\x1b[31m错误:${data}\\x1b[0m\\r\\n`);
                    break;
            }
        };
    
        const disconnect = () => {
            if (websocket) {
                websocket.close();
                setWebsocket(null);
            }
        };
    
        const clear = () => {
            if (terminal) {
                terminal.clear();
            }
        };
    
        const fit = () => {
            if (fitAddonRef.current) {
                fitAddonRef.current.fit();
            }
        };
    
        return (
            <div>
                <div style={{ marginBottom: '10px' }}>
                    <button onClick={connect} disabled={connected}>
                        连接
                    </button>
                    <button onClick={disconnect} disabled={!connected}>
                        断开
                    </button>
                    <button onClick={clear}>清除</button>
                    <button onClick={fit}>适应</button>
                    <span style={{ marginLeft: '20px' }}>
                        状态:{connected ? '已连接' : '已断开'}
                        {sessionId && ` (会话:${sessionId.substr(0, 8)}...)`}
                    </span>
                </div>
                <div
                    ref={terminalRef}
                    style={{
                        height: '400px',
                        border: '1px solid #ccc',
                        borderRadius: '4px'
                    }}
                />
            </div>
        );
    };
    
    export default AIOTerminal;

    下一步