2026-05-13 03:09:19 +00:00
|
|
|
|
// --- static/terminal.js ---
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 從剛剛建立的 utils.js 引入工具
|
|
|
|
|
|
import { getGlobalConnectionInfo } from './utils.js';
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 宣告終端機專用的變數
|
|
|
|
|
|
let term, fitAddon, ws;
|
|
|
|
|
|
export let isConnected = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 終端機文字上色 (內部使用,不需要 export)
|
|
|
|
|
|
function colorizeTerminalStream(text) {
|
|
|
|
|
|
let res = text;
|
2026-05-14 07:42:18 +00:00
|
|
|
|
|
|
|
|
|
|
// 1. IP 地址:改為「柔和冰藍色」 (移除 1; 粗體)
|
|
|
|
|
|
res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[38;5;111m$1\x1b[0m');
|
|
|
|
|
|
|
|
|
|
|
|
// 2. MAC 地址:改為「柔和薰衣草紫」 (移除 1; 粗體)
|
|
|
|
|
|
res = res.replace(/\b([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\b/g, '\x1b[38;5;140m$1\x1b[0m');
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 正常狀態:維持「柔和薄荷綠」
|
|
|
|
|
|
res = res.replace(/\b([a-z]-online|online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[38;5;114m$1\x1b[0m');
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 異常狀態:維持「柔和珊瑚紅」
|
|
|
|
|
|
res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[38;5;204m$1\x1b[0m');
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 介面名稱 (Cable/RPD等):改為「柔和奶油黃」 (原本是 \x1b[33m)
|
|
|
|
|
|
res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[38;5;179m$1\x1b[0m');
|
|
|
|
|
|
|
2026-05-13 03:09:19 +00:00
|
|
|
|
return res;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 匯出 (export) 所有需要被外部呼叫的函數
|
|
|
|
|
|
export function initTerminal() {
|
2026-06-10 06:37:16 +00:00
|
|
|
|
const container = document.getElementById('terminal-container');
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
// 🛡️ DOM 防火牆:攔截殘缺的假鍵盤事件
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
container.addEventListener('keydown', (e) => {
|
|
|
|
|
|
// 如果這個事件物件沒有 getModifierState 函數 (代表它是外掛產生的假事件)
|
|
|
|
|
|
if (typeof e.getModifierState !== 'function') {
|
|
|
|
|
|
// 🛑 停止事件傳遞!不讓它流進 Xterm.js 裡
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, true); // 🌟 關鍵:傳入 true 啟用「捕獲階段 (Capture Phase)」,確保我們比 Xterm.js 更早拿到事件
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
|
2026-05-13 03:09:19 +00:00
|
|
|
|
term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 });
|
|
|
|
|
|
fitAddon = new FitAddon.FitAddon();
|
|
|
|
|
|
term.loadAddon(fitAddon);
|
|
|
|
|
|
term.open(document.getElementById('terminal-container'));
|
|
|
|
|
|
fitAddon.fit();
|
|
|
|
|
|
term.writeln('Welcome to Harmonic cOS Web Terminal.');
|
|
|
|
|
|
term.writeln('Please confirm the target device above and click "連線至 CMTS"... \r\n');
|
|
|
|
|
|
|
|
|
|
|
|
term.onData((data) => {
|
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function connectWebSocket(resetCallback) {
|
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
|
|
|
|
const connInfo = getGlobalConnectionInfo();
|
|
|
|
|
|
if (!connInfo) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (resetCallback) resetCallback(); // 呼叫 app.js 傳進來的重置函數
|
|
|
|
|
|
|
2026-06-11 02:24:09 +00:00
|
|
|
|
// 🌟 [修復] 切換設備時,徹底清空終端機舊有畫面與緩衝區
|
|
|
|
|
|
if (term) {
|
|
|
|
|
|
term.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:09:19 +00:00
|
|
|
|
term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`);
|
|
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
|
const wsUrl = `${protocol}//${window.location.host}/ws/terminal?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`;
|
|
|
|
|
|
|
|
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
|
isConnected = true;
|
|
|
|
|
|
const btnLoadTask = document.getElementById('btn-load-task');
|
|
|
|
|
|
if (btnLoadTask) {
|
|
|
|
|
|
btnLoadTask.disabled = false;
|
|
|
|
|
|
btnLoadTask.style.backgroundColor = '#c0392b';
|
|
|
|
|
|
btnLoadTask.style.cursor = 'pointer';
|
|
|
|
|
|
}
|
2026-06-10 07:13:21 +00:00
|
|
|
|
// 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
|
|
|
|
|
|
document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
|
|
|
|
|
|
document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
|
2026-05-13 03:09:19 +00:00
|
|
|
|
document.getElementById('btnConnect').style.display = 'none';
|
|
|
|
|
|
document.getElementById('btnDisconnect').style.display = 'inline-block';
|
|
|
|
|
|
document.getElementById('cmtsHost').disabled = true;
|
|
|
|
|
|
document.getElementById('cmtsUser').disabled = true;
|
|
|
|
|
|
document.getElementById('cmtsPass').disabled = true;
|
|
|
|
|
|
term.focus();
|
2026-06-05 05:45:34 +00:00
|
|
|
|
|
2026-06-10 06:37:16 +00:00
|
|
|
|
// 🌟 原有:連線成功後,自動觸發一次查詢任務的 change 事件,讓背景預載 Datalist
|
2026-06-05 05:45:34 +00:00
|
|
|
|
const queryTaskSelect = document.getElementById('queryTask');
|
|
|
|
|
|
if (queryTaskSelect) {
|
|
|
|
|
|
queryTaskSelect.dispatchEvent(new Event('change'));
|
|
|
|
|
|
}
|
2026-06-10 06:37:16 +00:00
|
|
|
|
|
|
|
|
|
|
// 🌟 新增:連線成功後,強制觸發配置任務切換,藉此啟動「正確 IP」的 SSE 監聽
|
|
|
|
|
|
const configTaskSelect = document.getElementById('configTask');
|
|
|
|
|
|
if (configTaskSelect) {
|
|
|
|
|
|
configTaskSelect.dispatchEvent(new Event('change'));
|
|
|
|
|
|
}
|
2026-05-13 03:09:19 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ws.onmessage = (event) => {
|
2026-06-10 07:13:21 +00:00
|
|
|
|
// 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
|
|
|
|
|
|
const statusEl = document.getElementById('wsStatus');
|
|
|
|
|
|
if (statusEl.textContent.includes('驗證中')) {
|
|
|
|
|
|
statusEl.textContent = `狀態:已連線`;
|
|
|
|
|
|
statusEl.style.color = '#27ae60'; // 綠色
|
|
|
|
|
|
}
|
2026-05-13 03:09:19 +00:00
|
|
|
|
term.write(colorizeTerminalStream(event.data));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ws.onclose = () => {
|
|
|
|
|
|
isConnected = false;
|
|
|
|
|
|
const btnLoadTask = document.getElementById('btn-load-task');
|
|
|
|
|
|
if (btnLoadTask) {
|
|
|
|
|
|
btnLoadTask.disabled = true;
|
|
|
|
|
|
btnLoadTask.style.backgroundColor = '#95a5a6';
|
|
|
|
|
|
btnLoadTask.style.cursor = 'not-allowed';
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('wsStatus').textContent = '狀態:已斷線';
|
|
|
|
|
|
document.getElementById('wsStatus').style.color = '#c0392b';
|
|
|
|
|
|
document.getElementById('btnConnect').style.display = 'inline-block';
|
|
|
|
|
|
document.getElementById('btnDisconnect').style.display = 'none';
|
|
|
|
|
|
document.getElementById('cmtsHost').disabled = false;
|
|
|
|
|
|
document.getElementById('cmtsUser').disabled = false;
|
|
|
|
|
|
document.getElementById('cmtsPass').disabled = false;
|
|
|
|
|
|
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
|
|
|
|
|
|
|
2026-06-10 07:13:21 +00:00
|
|
|
|
// 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
|
|
|
|
|
|
if (event.code === 4001) {
|
|
|
|
|
|
alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 06:37:16 +00:00
|
|
|
|
// 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」SSE 監聽
|
|
|
|
|
|
const configTaskSelect = document.getElementById('configTask');
|
|
|
|
|
|
if (configTaskSelect) {
|
|
|
|
|
|
configTaskSelect.dispatchEvent(new Event('change'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:09:19 +00:00
|
|
|
|
if (resetCallback) resetCallback();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function disconnectWebSocket() { if (ws) ws.close(); }
|
|
|
|
|
|
|
|
|
|
|
|
export function injectCommand() {
|
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return alert("請先點擊「連線至 CMTS」!");
|
|
|
|
|
|
const cmd = document.getElementById('fixedCmd').value;
|
|
|
|
|
|
ws.send(cmd + '\r');
|
|
|
|
|
|
term.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function getTerminalText() {
|
|
|
|
|
|
if (!term) return '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (term.hasSelection()) return term.getSelection();
|
|
|
|
|
|
const buffer = term.buffer.active;
|
|
|
|
|
|
let text = '';
|
|
|
|
|
|
for (let i = 0; i < buffer.length; i++) {
|
|
|
|
|
|
const line = buffer.getLine(i);
|
|
|
|
|
|
if (line) text += line.translateToString(true) + '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("讀取終端機文字失敗:", err);
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function downloadTerminal() {
|
|
|
|
|
|
const text = getTerminalText();
|
|
|
|
|
|
if (!text.trim()) return alert("沒有內容可下載!");
|
|
|
|
|
|
try {
|
|
|
|
|
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
|
|
|
|
a.download = `cmts_terminal_log_${timestamp}.txt`;
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("下載失敗:", err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|