功能,安装图形库太麻烦,直接启动服务器开新线程,浏览器跑命令
不用安装VNC各种
当然,一点bug没时间修复,靠各位了


说明 console.html 是用户端,可以打包成APP等
index是随便弄得首页,防止报错
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termux Web Console</title>
<style>
body {
margin: 0;
padding: 0;
background: #000;
font-family: 'Courier New', monospace;
height: 100vh;
display: flex;
flex-direction: column;
color: #0f0;
}
.header {
background: #111;
padding: 10px;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h2 {
margin: 0;
color: #0f0;
font-size: 16px;
}
.status {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
}
.status.connected .status-dot {
background: #0f0;
}
.status.disconnected .status-dot {
background: #f00;
}
#terminal {
flex: 1;
background: #000;
color: #fff;
padding: 10px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
white-space: pre-wrap;
}
.terminal-line {
margin: 2px 0;
word-wrap: break-word;
}
.command-line {
color: #0f0;
}
.output-line {
color: #fff;
}
.error-line {
color: #f00;
}
.status-line {
color: #ff0;
font-style: italic;
}
.input-prompt {
color: #0ff;
}
.user-input {
color: #0f0;
}
.input-container {
background: #111;
padding: 10px;
border-top: 1px solid #333;
display: flex;
align-items: center;
}
.prompt {
color: #0f0;
margin-right: 8px;
}
#command-input {
flex: 1;
background: #000;
border: 1px solid #333;
color: #0f0;
padding: 5px;
font-family: 'Courier New', monospace;
font-size: 14px;
outline: none;
}
#command-input:focus {
border-color: #0f0;
}
</style>
</head>
<body>
<div class="header">
<h2>Termux Web Console</h2>
<div id="status" class="status disconnected">
<span class="status-dot"></span>
<span>未连接</span>
</div>
</div>
<div id="terminal"></div>
<div class="input-container">
<span class="prompt">$</span>
<input type="text" id="command-input" placeholder="输入命令..." autocomplete="off">
</div>
<script>
const terminal = document.getElementById('terminal');
const input = document.getElementById('command-input');
const statusEl = document.getElementById('status');
let eventSource = null;
let commandHistory = [];
let historyIndex = -1;
let currentCommand = '';
let awaitingInput = false;
let lastOutputWasPrompt = false;
// 简单的HTML转义
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 检查是否是输入提示
function isInputPrompt(text) {
// 检查常见的输入提示模式
const promptPatterns = [
/请输入.*:?\s*$/,
/Enter.*:?\s*$/,
/.*:?\s*$/,
/Password:?\s*$/i,
/username:?\s*$/i,
/路径:?\s*$/,
/目录:?\s*$/,
/文件:?\s*$/,
/choice:?\s*$/i,
/选择.*:?\s*$/
];
// 检查是否匹配任何提示模式
for (const pattern of promptPatterns) {
if (pattern.test(text.trim())) {
return true;
}
}
// 检查是否以冒号结尾但没有命令提示符
if (text.trim().endsWith(':') && !text.includes('$') && !text.includes('#')) {
return true;
}
return false;
}
function addLine(text, className = '') {
const line = document.createElement('div');
line.className = `terminal-line ${className}`;
// 清理文本
let cleanText = escapeHtml(text);
// 如果看起来像HTML标签,移除它们
if (cleanText.includes('<span') || cleanText.includes('</span>')) {
cleanText = cleanText.replace(/<[^>]*>/g, '');
}
// 移除ANSI转义序列
cleanText = cleanText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
line.innerHTML = cleanText;
terminal.appendChild(line);
terminal.scrollTop = terminal.scrollHeight;
}
function addCommand(command) {
currentCommand = command;
addLine(`$ ${command}`, 'command-line');
}
function addUserInput(inputText) {
addLine(inputText, 'user-input');
}
function updateStatus(connected) {
if (connected) {
statusEl.className = 'status connected';
statusEl.querySelector('span:last-child').textContent = '已连接';
} else {
statusEl.className = 'status disconnected';
statusEl.querySelector('span:last-child').textContent = '未连接';
}
}
function connect() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/stream');
eventSource.onopen = function() {
updateStatus(true);
addLine('*** 已连接到服务器 ***', 'status-line');
};
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
switch(data.type) {
case 'stdout':
case 'stderr':
let outputText = data.text;
// 跳过重复的命令显示
if (currentCommand && outputText.includes(currentCommand)) {
const cmdIndex = outputText.indexOf(currentCommand);
if (cmdIndex !== -1) {
outputText = outputText.substring(cmdIndex + currentCommand.length).trim();
}
if (outputText.trim()) {
// 检查是否是输入提示
if (isInputPrompt(outputText)) {
addLine(outputText, 'input-prompt');
awaitingInput = true;
lastOutputWasPrompt = true;
} else {
addLine(outputText, data.type === 'stderr' ? 'error-line' : 'output-line');
lastOutputWasPrompt = false;
}
}
}
// 检查是否是输入提示
else if (isInputPrompt(outputText)) {
addLine(outputText, 'input-prompt');
awaitingInput = true;
lastOutputWasPrompt = true;
}
// 普通输出
else if (outputText.trim()) {
addLine(outputText, data.type === 'stderr' ? 'error-line' : 'output-line');
lastOutputWasPrompt = false;
}
break;
case 'status':
addLine(data.text, 'status-line');
break;
case 'error':
addLine(data.text, 'error-line');
break;
case 'heartbeat':
// 忽略心跳
break;
}
} catch (e) {
console.error('解析消息错误:', e);
}
};
eventSource.onerror = function() {
updateStatus(false);
addLine('*** 连接断开 ***', 'error-line');
setTimeout(connect, 3000);
};
}
async function executeCommand(command) {
if (!command.trim()) return;
addCommand(command);
commandHistory.push(command);
historyIndex = commandHistory.length;
try {
const response = await fetch('/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ command: command })
});
if (!response.ok) {
const error = await response.json();
addLine(`错误: ${error.message}`, 'error-line');
}
} catch (error) {
addLine(`网络错误: ${error.message}`, 'error-line');
}
}
async function sendInput(inputText) {
try {
const response = await fetch('/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ command: inputText })
});
if (!response.ok) {
const error = await response.json();
addLine(`错误: ${error.message}`, 'error-line');
}
} catch (error) {
addLine(`网络错误: ${error.message}`, 'error-line');
}
}
// 事件监听器
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
const command = input.value;
input.value = '';
if (awaitingInput) {
// 如果正在等待输入,显示用户输入并发送
addUserInput(command);
sendInput(command);
awaitingInput = false;
} else {
// 否则作为命令执行
executeCommand(command);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
// 点击终端聚焦输入框
terminal.addEventListener('click', function() {
input.focus();
});
// 初始化
window.addEventListener('load', function() {
connect();
input.focus();
addLine('Welcome to Termux Web Console', 'status-line');
addLine('Type commands and press Enter to execute', 'status-line');
addLine('');
});
</script>
</body>
</html>

import subprocess
import threading
import queue
import json
from flask import Flask, request, jsonify, Response, send_from_directory
import os
import time
import sys
app = Flask(__name__, static_folder='.', static_url_path='')
# 添加CORS支持
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
# --- 全局变量 ---
shell_process = None
output_queue = queue.Queue()
shell_ready = threading.Event()
def enqueue_output(stream, q, stream_name):
"""从子进程流中读取数据并放入队列"""
try:
while True:
line = stream.readline()
if not line:
break
if line:
text = line.decode('utf-8', errors='replace').rstrip()
q.put({'type': stream_name, 'text': text})
except Exception as e:
q.put({'type': 'error', 'text': f"流读取错误({stream_name}): {str(e)}"})
finally:
stream.close()
q.put({'type': 'status', 'text': f"{stream_name}流已关闭"})
def start_shell_session():
"""启动一个持久的shell会话和读取线程"""
global shell_process
print("正在启动Termux Shell会话...")
try:
shell_process = subprocess.Popen(
['/data/data/com.termux/files/usr/bin/bash', '-i'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=0,
env=os.environ.copy()
)
t_stdout = threading.Thread(target=enqueue_output, args=(shell_process.stdout, output_queue, 'stdout'))
t_stderr = threading.Thread(target=enqueue_output, args=(shell_process.stderr, output_queue, 'stderr'))
t_stdout.daemon = True
t_stderr.daemon = True
t_stdout.start()
t_stderr.start()
time.sleep(1)
if shell_process.poll() is None:
shell_ready.set()
print("Shell会话已启动成功。")
output_queue.put({'type': 'status', 'text': '*** Shell已就绪 ***'})
else:
print(f"Shell启动失败,退出码: {shell_process.poll()}")
output_queue.put({'type': 'error', 'text': f'*** Shell启动失败,退出码: {shell_process.poll()} ***'})
except Exception as e:
print(f"启动Shell时出错: {str(e)}")
output_queue.put({'type': 'error', 'text': f'*** 启动Shell失败: {str(e)} ***'})
@app.route('/')
def index():
"""返回简单的index.html"""
return app.send_static_file('index.html')
@app.route('/console')
def console():
"""返回功能完整的控制台页面"""
return app.send_static_file('console.html')
@app.route('/execute', methods=['POST', 'OPTIONS'])
def execute():
"""接收命令并发送到shell的stdin"""
if request.method == 'OPTIONS':
return '', 200
try:
data = request.get_json()
if not data or 'command' not in data:
return jsonify({'status': 'error', 'message': '未提供命令'}), 400
command = data['command']
if not shell_ready.is_set():
return jsonify({'status': 'error', 'message': 'Shell未就绪'}), 503
if shell_process and shell_process.poll() is None:
shell_process.stdin.write((command + '\n').encode('utf-8'))
shell_process.stdin.flush()
return jsonify({'status': 'success'})
else:
return jsonify({'status': 'error', 'message': 'Shell进程已退出'}), 500
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/stream')
def stream():
"""SSE流,将shell输出发送给前端"""
def generate():
# 发送初始连接消息
yield f"data: {json.dumps({'type': 'status', 'text': '*** 已连接到服务器 ***'})}\n\n"
if not shell_ready.is_set():
yield f"data: {json.dumps({'type': 'status', 'text': '*** 等待Shell启动... ***'})}\n\n"
shell_ready.wait(timeout=5)
while True:
try:
try:
output = output_queue.get(timeout=1)
yield f"data: {json.dumps(output)}\n\n"
except queue.Empty:
# 发送心跳保持连接
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
continue
except GeneratorExit:
print("SSE客户端断开连接")
break
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'text': f'流错误: {str(e)}'})}\n\n"
# 移除 'Connection' 和 'X-Accel-Buffering' 头
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache'})
if __name__ == '__main__':
start_shell_session()
# 获取本机IP地址
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
except:
local_ip = "localhost"
print("\n" + "="*50)
print("服务器已启动!")
print(f"本地访问: http://localhost:8080")
print(f"局域网访问: http://{local_ip}:8080")
print(f"控制台页面: http://{local_ip}:8080/console")
print("="*50 + "\n")
# 强制使用Flask开发服务器
print("使用Flask开发服务器...")
app.run(host='0.0.0.0', port=8080, threaded=True)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termux Web Console</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
padding: 50px;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
background: rgba(0, 0, 0, 0.3);
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
}
h1 {
margin-bottom: 30px;
font-size: 2.5em;
}
.button {
display: inline-block;
background: #4CAF50;
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 5px;
font-size: 1.2em;
margin: 10px;
transition: background 0.3s;
}
.button:hover {
background: #45a049;
}
.info {
margin-top: 30px;
font-size: 0.9em;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Termux Web Console</h1>
<p>服务器正在运行中...</p>
<a href="/console" class="button">打开控制台</a>
<div class="info">
<p>提示:将此URL分享给同一网络下的其他设备</p>
<p>其他设备访问: http://[你的手机IP]:8080/console</p>
</div>
</div>
</body>
</html>
1806

被折叠的 条评论
为什么被折叠?



