feat: 添加项目规则、环境配置示例及开发文档

This commit is contained in:
锦麟 王
2026-02-04 18:49:38 +08:00
commit df76882178
88 changed files with 13150 additions and 0 deletions

View File

@@ -0,0 +1,377 @@
/* MineNASAI Web Terminal Styles */
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24283b;
--bg-tertiary: #1f2335;
--text-primary: #c0caf5;
--text-secondary: #565f89;
--accent-primary: #7aa2f7;
--accent-success: #9ece6a;
--accent-warning: #e0af68;
--accent-error: #f7768e;
--border-color: #3b4261;
--header-height: 48px;
--footer-height: 24px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header */
.header {
height: var(--header-height);
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.logo {
font-size: 18px;
font-weight: 600;
color: var(--accent-primary);
}
.version {
font-size: 12px;
color: var(--text-secondary);
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
}
.header-center {
display: flex;
align-items: center;
}
.connection-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--accent-error);
}
.status-dot.connected {
background-color: var(--accent-success);
}
.status-dot.connecting {
background-color: var(--accent-warning);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
/* Buttons */
.btn {
padding: 6px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn:hover {
background-color: var(--bg-secondary);
border-color: var(--accent-primary);
}
.btn-primary {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
color: var(--bg-primary);
}
.btn-primary:hover {
background-color: #89b4fa;
}
.btn-icon {
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-small {
padding: 4px 8px;
font-size: 12px;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.terminal-container {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--bg-tertiary);
}
.terminal-header {
height: 36px;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
}
.terminal-tabs {
display: flex;
align-items: center;
gap: 4px;
}
.terminal-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background-color: var(--bg-tertiary);
border-radius: 4px 4px 0 0;
font-size: 12px;
cursor: pointer;
}
.terminal-tab.active {
background-color: var(--bg-primary);
}
.tab-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 14px;
padding: 0;
line-height: 1;
}
.tab-close:hover {
color: var(--accent-error);
}
.btn-new-tab {
background: none;
border: 1px dashed var(--border-color);
color: var(--text-secondary);
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.btn-new-tab:hover {
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.terminal-actions {
display: flex;
gap: 4px;
}
.terminal {
flex: 1;
padding: 8px;
}
/* Footer */
.footer {
height: var(--footer-height);
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
font-size: 11px;
color: var(--text-secondary);
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
width: 400px;
max-width: 90%;
}
.modal-small {
width: 320px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-size: 16px;
font-weight: 500;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
font-size: 20px;
cursor: pointer;
}
.modal-close:hover {
color: var(--accent-error);
}
.modal-body {
padding: 16px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px;
border-top: 1px solid var(--border-color);
}
/* Form */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 13px;
color: var(--text-secondary);
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 8px 12px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 13px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--accent-primary);
}
.form-group input[type="checkbox"] {
margin-right: 8px;
}
.hint {
font-size: 12px;
color: var(--text-secondary);
margin-top: 8px;
}
/* Fullscreen */
.fullscreen .header,
.fullscreen .footer {
display: none;
}
.fullscreen .terminal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
}
/* Responsive */
@media (max-width: 768px) {
.header-center {
display: none;
}
.terminal-actions {
display: none;
}
}

View File

@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MineNASAI - Web Terminal</title>
<!-- xterm.js -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="app-container">
<!-- 顶部导航栏 -->
<header class="header">
<div class="header-left">
<h1 class="logo">MineNASAI</h1>
<span class="version">v0.1.0</span>
</div>
<div class="header-center">
<span class="connection-status" id="connectionStatus">
<span class="status-dot disconnected"></span>
<span class="status-text">未连接</span>
</span>
</div>
<div class="header-right">
<button class="btn btn-icon" id="settingsBtn" title="设置">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<button class="btn btn-primary" id="connectBtn">连接</button>
</div>
</header>
<!-- 主内容区 -->
<main class="main-content">
<!-- 终端容器 -->
<div class="terminal-container">
<div class="terminal-header">
<div class="terminal-tabs">
<div class="terminal-tab active" data-tab="main">
<span>主终端</span>
<button class="tab-close" title="关闭">&times;</button>
</div>
<button class="btn-new-tab" id="newTabBtn" title="新建终端">+</button>
</div>
<div class="terminal-actions">
<button class="btn btn-small" id="clearBtn" title="清屏">清屏</button>
<button class="btn btn-small" id="fullscreenBtn" title="全屏">全屏</button>
</div>
</div>
<div id="terminal" class="terminal"></div>
</div>
</main>
<!-- 底部状态栏 -->
<footer class="footer">
<div class="footer-left">
<span id="sessionInfo">会话: -</span>
</div>
<div class="footer-right">
<span id="terminalSize">-</span>
</div>
</footer>
</div>
<!-- 设置模态框 -->
<div class="modal" id="settingsModal">
<div class="modal-content">
<div class="modal-header">
<h2>设置</h2>
<button class="modal-close" id="closeSettingsBtn">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>字体大小</label>
<input type="number" id="fontSizeInput" value="14" min="10" max="24">
</div>
<div class="form-group">
<label>主题</label>
<select id="themeSelect">
<option value="dark">深色</option>
<option value="light">浅色</option>
<option value="monokai">Monokai</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="cursorBlinkCheck" checked>
光标闪烁
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn" id="resetSettingsBtn">重置</button>
<button class="btn btn-primary" id="saveSettingsBtn">保存</button>
</div>
</div>
</div>
<!-- 登录模态框 -->
<div class="modal" id="loginModal">
<div class="modal-content modal-small">
<div class="modal-header">
<h2>连接到终端</h2>
</div>
<div class="modal-body">
<div class="form-group">
<label>Token</label>
<input type="password" id="tokenInput" placeholder="输入访问令牌">
</div>
<p class="hint">提示:令牌可从通讯工具消息中获取</p>
</div>
<div class="modal-footer">
<button class="btn" id="cancelLoginBtn">取消</button>
<button class="btn btn-primary" id="doConnectBtn">连接</button>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,393 @@
/**
* MineNASAI Web Terminal
*/
class WebTerminal {
constructor() {
this.terminal = null;
this.fitAddon = null;
this.socket = null;
this.sessionId = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.settings = this.loadSettings();
this.init();
}
loadSettings() {
const defaults = {
fontSize: 14,
theme: 'dark',
cursorBlink: true
};
try {
const saved = localStorage.getItem('terminal_settings');
return saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
} catch {
return defaults;
}
}
saveSettings() {
localStorage.setItem('terminal_settings', JSON.stringify(this.settings));
}
getTheme(name) {
const themes = {
dark: {
background: '#1a1b26',
foreground: '#c0caf5',
cursor: '#c0caf5',
cursorAccent: '#1a1b26',
selection: 'rgba(122, 162, 247, 0.3)',
black: '#15161e',
red: '#f7768e',
green: '#9ece6a',
yellow: '#e0af68',
blue: '#7aa2f7',
magenta: '#bb9af7',
cyan: '#7dcfff',
white: '#a9b1d6',
brightBlack: '#414868',
brightRed: '#f7768e',
brightGreen: '#9ece6a',
brightYellow: '#e0af68',
brightBlue: '#7aa2f7',
brightMagenta: '#bb9af7',
brightCyan: '#7dcfff',
brightWhite: '#c0caf5'
},
light: {
background: '#f5f5f5',
foreground: '#1a1b26',
cursor: '#1a1b26',
cursorAccent: '#f5f5f5',
selection: 'rgba(122, 162, 247, 0.3)'
},
monokai: {
background: '#272822',
foreground: '#f8f8f2',
cursor: '#f8f8f2',
cursorAccent: '#272822',
selection: 'rgba(73, 72, 62, 0.5)',
black: '#272822',
red: '#f92672',
green: '#a6e22e',
yellow: '#f4bf75',
blue: '#66d9ef',
magenta: '#ae81ff',
cyan: '#a1efe4',
white: '#f8f8f2'
}
};
return themes[name] || themes.dark;
}
init() {
// 初始化终端
this.terminal = new Terminal({
fontSize: this.settings.fontSize,
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace',
cursorBlink: this.settings.cursorBlink,
cursorStyle: 'block',
theme: this.getTheme(this.settings.theme),
allowTransparency: true,
scrollback: 10000
});
// 添加插件
this.fitAddon = new FitAddon.FitAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(new WebLinksAddon.WebLinksAddon());
// 挂载终端
const container = document.getElementById('terminal');
this.terminal.open(container);
this.fitAddon.fit();
// 显示欢迎信息
this.showWelcome();
// 绑定事件
this.bindEvents();
// 窗口大小调整
window.addEventListener('resize', () => {
this.fitAddon.fit();
this.sendResize();
});
// 更新设置UI
this.updateSettingsUI();
}
showWelcome() {
const welcome = [
'\x1b[1;34m',
' __ __ _ _ _ _ ____ _ ___ ',
' | \\/ (_)_ __ ___| \\ | | / \\ / ___| / \\ |_ _|',
' | |\\/| | | \'_ \\ / _ \\ \\| | / _ \\ \\___ \\ / _ \\ | | ',
' | | | | | | | | __/ |\\ |/ ___ \\ ___) / ___ \\ | | ',
' |_| |_|_|_| |_|\\___|_| \\_/_/ \\_\\____/_/ \\_\\___|',
'\x1b[0m',
'',
'\x1b[33mWeb Terminal v0.1.0\x1b[0m',
'',
'点击右上角 \x1b[1;32m连接\x1b[0m 按钮开始使用',
''
];
welcome.forEach(line => this.terminal.writeln(line));
}
bindEvents() {
// 连接按钮
document.getElementById('connectBtn').addEventListener('click', () => {
if (this.isConnected) {
this.disconnect();
} else {
this.showLoginModal();
}
});
// 登录模态框
document.getElementById('doConnectBtn').addEventListener('click', () => {
this.connect();
});
document.getElementById('cancelLoginBtn').addEventListener('click', () => {
this.hideLoginModal();
});
document.getElementById('tokenInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.connect();
});
// 设置按钮
document.getElementById('settingsBtn').addEventListener('click', () => {
document.getElementById('settingsModal').classList.add('active');
});
document.getElementById('closeSettingsBtn').addEventListener('click', () => {
document.getElementById('settingsModal').classList.remove('active');
});
document.getElementById('saveSettingsBtn').addEventListener('click', () => {
this.applySettings();
document.getElementById('settingsModal').classList.remove('active');
});
document.getElementById('resetSettingsBtn').addEventListener('click', () => {
this.settings = { fontSize: 14, theme: 'dark', cursorBlink: true };
this.updateSettingsUI();
this.applySettings();
});
// 清屏
document.getElementById('clearBtn').addEventListener('click', () => {
this.terminal.clear();
});
// 全屏
document.getElementById('fullscreenBtn').addEventListener('click', () => {
document.body.classList.toggle('fullscreen');
setTimeout(() => this.fitAddon.fit(), 100);
});
// 终端输入
this.terminal.onData(data => {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'input',
data: data
}));
}
});
// ESC 退出全屏
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.body.classList.contains('fullscreen')) {
document.body.classList.remove('fullscreen');
setTimeout(() => this.fitAddon.fit(), 100);
}
});
}
updateSettingsUI() {
document.getElementById('fontSizeInput').value = this.settings.fontSize;
document.getElementById('themeSelect').value = this.settings.theme;
document.getElementById('cursorBlinkCheck').checked = this.settings.cursorBlink;
}
applySettings() {
this.settings.fontSize = parseInt(document.getElementById('fontSizeInput').value);
this.settings.theme = document.getElementById('themeSelect').value;
this.settings.cursorBlink = document.getElementById('cursorBlinkCheck').checked;
this.terminal.options.fontSize = this.settings.fontSize;
this.terminal.options.cursorBlink = this.settings.cursorBlink;
this.terminal.options.theme = this.getTheme(this.settings.theme);
this.saveSettings();
this.fitAddon.fit();
}
showLoginModal() {
document.getElementById('loginModal').classList.add('active');
document.getElementById('tokenInput').focus();
}
hideLoginModal() {
document.getElementById('loginModal').classList.remove('active');
document.getElementById('tokenInput').value = '';
}
connect() {
const token = document.getElementById('tokenInput').value.trim();
this.hideLoginModal();
this.updateStatus('connecting', '连接中...');
this.terminal.writeln('\x1b[33m正在连接...\x1b[0m');
// 获取 WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/terminal`;
try {
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
// 发送认证
this.socket.send(JSON.stringify({
type: 'auth',
token: token || 'anonymous'
}));
};
this.socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
this.handleMessage(msg);
};
this.socket.onclose = (event) => {
this.isConnected = false;
this.updateStatus('disconnected', '已断开');
this.updateConnectButton(false);
if (event.code !== 1000) {
this.terminal.writeln(`\x1b[31m连接已断开 (${event.code})\x1b[0m`);
this.tryReconnect();
}
};
this.socket.onerror = (error) => {
this.terminal.writeln('\x1b[31m连接错误\x1b[0m');
console.error('WebSocket error:', error);
};
} catch (e) {
this.terminal.writeln(`\x1b[31m连接失败: ${e.message}\x1b[0m`);
this.updateStatus('disconnected', '连接失败');
}
}
handleMessage(msg) {
switch (msg.type) {
case 'auth_ok':
this.isConnected = true;
this.sessionId = msg.session_id;
this.reconnectAttempts = 0;
this.updateStatus('connected', '已连接');
this.updateConnectButton(true);
this.terminal.writeln('\x1b[32m已连接到终端\x1b[0m\r\n');
document.getElementById('sessionInfo').textContent = `会话: ${this.sessionId.slice(0, 8)}`;
// 发送终端尺寸
this.sendResize();
break;
case 'auth_error':
this.terminal.writeln(`\x1b[31m认证失败: ${msg.message}\x1b[0m`);
this.updateStatus('disconnected', '认证失败');
this.socket.close();
break;
case 'output':
this.terminal.write(msg.data);
break;
case 'resize_ok':
const cols = msg.cols;
const rows = msg.rows;
document.getElementById('terminalSize').textContent = `${cols}x${rows}`;
break;
case 'error':
this.terminal.writeln(`\x1b[31m错误: ${msg.message}\x1b[0m`);
break;
case 'ping':
this.socket.send(JSON.stringify({ type: 'pong' }));
break;
}
}
sendResize() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
const dims = this.fitAddon.proposeDimensions();
if (dims) {
this.socket.send(JSON.stringify({
type: 'resize',
cols: dims.cols,
rows: dims.rows
}));
}
}
}
disconnect() {
if (this.socket) {
this.socket.close(1000, 'User disconnect');
this.socket = null;
}
this.isConnected = false;
this.terminal.writeln('\r\n\x1b[33m已断开连接\x1b[0m');
this.updateStatus('disconnected', '已断开');
this.updateConnectButton(false);
}
tryReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.terminal.writeln(`\x1b[33m${delay/1000}秒后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})\x1b[0m`);
setTimeout(() => {
if (!this.isConnected) {
this.connect();
}
}, delay);
}
}
updateStatus(status, text) {
const statusEl = document.getElementById('connectionStatus');
const dot = statusEl.querySelector('.status-dot');
const textEl = statusEl.querySelector('.status-text');
dot.className = 'status-dot ' + status;
textEl.textContent = text;
}
updateConnectButton(connected) {
const btn = document.getElementById('connectBtn');
btn.textContent = connected ? '断开' : '连接';
btn.classList.toggle('btn-primary', !connected);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
window.webTerminal = new WebTerminal();
});