Add files via upload

This commit is contained in:
公明
2026-06-07 19:12:43 +08:00
committed by GitHub
parent c1bd94684c
commit 5b5a532d4f
6 changed files with 748 additions and 118 deletions
+162 -8
View File
@@ -1218,32 +1218,172 @@
Task Detail Modal
============================================================================ */
.c2-task-detail { line-height: 2; }
.c2-task-detail > div { margin-bottom: 6px; font-size: 13px; }
.c2-modal.c2-modal--wide {
max-width: 720px;
}
.c2-task-modal-header {
align-items: flex-start;
}
.c2-task-modal-heading {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.c2-task-modal-heading h3 {
margin: 0;
}
.c2-task-detail {
display: flex;
flex-direction: column;
gap: 20px;
}
.c2-task-detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.c2-task-kv {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 14px;
background: var(--c2-surface-alt);
border: 1px solid var(--c2-border);
border-radius: var(--c2-radius-sm);
}
.c2-task-kv__label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--c2-text-muted);
}
.c2-task-kv__value {
font-size: 13px;
font-weight: 500;
color: var(--c2-text);
word-break: break-all;
line-height: 1.45;
}
.c2-task-kv__value--mono {
font-family: var(--c2-mono);
font-size: 12px;
color: var(--c2-text-dim);
}
.c2-task-kv__value--accent {
font-family: var(--c2-mono);
font-weight: 600;
color: var(--c2-accent);
}
.c2-task-timeline {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 14px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.06), rgba(59, 130, 246, 0.02));
border: 1px solid rgba(59, 130, 246, 0.14);
border-radius: var(--c2-radius-sm);
}
.c2-task-time-card {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.c2-task-time-card:not(:last-child) {
padding-right: 10px;
border-right: 1px solid rgba(59, 130, 246, 0.12);
}
.c2-task-code-section,
.c2-task-error-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.c2-task-code-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.c2-task-code-title {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--c2-text-dim);
}
.c2-task-error {
color: var(--c2-red);
padding: 14px;
padding: 14px 16px;
background: var(--c2-red-dim);
border: 1px solid rgba(239, 68, 68, 0.15);
border-radius: var(--c2-radius-sm);
margin-top: 12px;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.c2-task-result pre {
.c2-task-result-pre,
.c2-task-command-pre {
background: #0f172a;
color: #e2e8f0;
padding: 16px;
padding: 14px 16px;
border-radius: var(--c2-radius-sm);
overflow-x: auto;
font-family: var(--c2-mono);
font-size: 12px;
margin-top: 8px;
max-height: 400px;
margin: 0;
max-height: 360px;
overflow-y: auto;
border: 1px solid #1e293b;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.c2-task-command-pre {
max-height: 140px;
}
.c2-task-command-cell {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--c2-mono);
font-size: 12px;
color: var(--c2-text-muted, #64748b);
}
.c2-task-item-compact .c2-task-command {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--c2-mono);
font-size: 11px;
color: var(--c2-text-muted, #64748b);
}
/* ============================================================================
@@ -1277,6 +1417,11 @@
Modal
============================================================================ */
/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */
#c2-toast-container {
z-index: 10100 !important;
}
.c2-modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -1388,4 +1533,13 @@
.c2-stats { flex-direction: column; gap: 12px; }
.c2-payload-grid { grid-template-columns: 1fr; }
.c2-listener-grid { grid-template-columns: 1fr; padding: 16px; }
.c2-task-detail-grid { grid-template-columns: 1fr; }
.c2-task-timeline { grid-template-columns: 1fr; }
.c2-task-time-card:not(:last-child) {
padding-right: 0;
padding-bottom: 10px;
border-right: none;
border-bottom: 1px solid rgba(59, 130, 246, 0.12);
}
.c2-modal.c2-modal--wide { max-width: 100%; }
}
+5
View File
@@ -21184,6 +21184,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
gap: 12px;
}
/* 全局 Toast 须高于模态遮罩 (10050) */
#toast-notification-container {
z-index: 10100 !important;
}
.chat-files-toast {
position: fixed;
z-index: 1100;
+7 -1
View File
@@ -2671,7 +2671,7 @@
"confirmDeleteSession": "Remove this session and related tasks/files from the server? (Does not send exit to the implant; use Kill Session to exit the agent.)",
"toastExitSent": "Exit command sent",
"toastSessionDeleted": "Session record deleted",
"terminalWelcome": "CyberStrikeAI C2 Terminal — AI-Native Command & Control",
"terminalWelcome": "CyberStrikeAI C2 Terminal — Enter to run; ↑↓ history; Ctrl+L clear; Ctrl+C cancel input",
"termStatusReady": "Ready",
"termStatusExec": "Executing…",
"termStatusErr": "Error",
@@ -2680,6 +2680,9 @@
"termWaitTimeout": "[Timed out waiting for result]",
"termCleared": "Terminal cleared",
"termNoSelection": "No text selected",
"termWaitFinish": "Please wait for the current command to finish",
"termCtrlC": "Remote interrupt is not supported in this version",
"termQueued": "[Command queued — will run after the current task completes]",
"clearTerminal": "Clear"
},
"tasks": {
@@ -2706,6 +2709,7 @@
"colTask": "Task",
"colSession": "Session",
"colType": "Type",
"colCommand": "Command",
"colStatus": "Status",
"colDuration": "Duration",
"colCreated": "Created",
@@ -2716,6 +2720,8 @@
"labelId": "ID",
"labelSession": "Session",
"labelType": "Type",
"labelCommand": "Command",
"labelPayload": "Payload",
"labelStatus": "Status",
"labelCreated": "Created",
"labelSent": "Sent",
+7 -1
View File
@@ -2660,7 +2660,7 @@
"confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)",
"toastExitSent": "退出指令已发送",
"toastSessionDeleted": "会话记录已删除",
"terminalWelcome": "CyberStrikeAI C2 终端 — AI-Native 命令与控制",
"terminalWelcome": "CyberStrikeAI C2 终端 — 回车执行;↑↓ 历史;Ctrl+L 清屏;Ctrl+C 取消输入",
"termStatusReady": "就绪",
"termStatusExec": "执行中…",
"termStatusErr": "错误",
@@ -2669,6 +2669,9 @@
"termWaitTimeout": "[等待结果超时]",
"termCleared": "终端已清屏",
"termNoSelection": "未选中文本",
"termWaitFinish": "请等待当前命令执行完成",
"termCtrlC": "当前版本暂不支持中断远程命令",
"termQueued": "[命令已加入队列,将在当前任务完成后执行]",
"clearTerminal": "清屏"
},
"tasks": {
@@ -2695,6 +2698,7 @@
"colTask": "任务",
"colSession": "会话",
"colType": "类型",
"colCommand": "命令",
"colStatus": "状态",
"colDuration": "耗时",
"colCreated": "创建时间",
@@ -2705,6 +2709,8 @@
"labelId": "ID",
"labelSession": "会话",
"labelType": "类型",
"labelCommand": "命令",
"labelPayload": "参数",
"labelStatus": "状态",
"labelCreated": "创建时间",
"labelSent": "发送时间",
+566 -107
View File
@@ -27,7 +27,11 @@
terminalFitAddon: null,
terminalResizeObserver: null,
terminalContainer: null,
terminalSessionId: 'main',
terminalSessionId: null,
terminalHistory: {},
terminalLogs: {},
terminalBusy: false,
terminalQueue: [],
// 文件管理
currentPath: '/',
fileList: [],
@@ -90,6 +94,56 @@
return status;
}
function formatTaskCommand(task) {
if (!task) return '';
const type = String(task.taskType || '').toLowerCase();
const p = task.payload;
if (!p || typeof p !== 'object' || Object.keys(p).length === 0) {
if (type === 'pwd' || type === 'ps' || type === 'screenshot') return type;
return '';
}
switch (type) {
case 'shell':
case 'exec':
return p.command != null ? String(p.command) : '';
case 'ls':
case 'cd':
return p.path != null ? String(p.path) : '';
case 'download':
return p.remote_path != null ? String(p.remote_path) : '';
case 'upload':
if (p.remote_path) return String(p.remote_path);
if (p.file_id) return 'file:' + String(p.file_id);
return '';
case 'kill_proc':
return p.pid != null ? 'pid:' + String(p.pid) : '';
case 'sleep':
let sleepStr = p.seconds != null ? 'sleep ' + p.seconds + 's' : '';
if (p.jitter != null) sleepStr += (sleepStr ? ', ' : '') + 'jitter ' + p.jitter + '%';
return sleepStr;
case 'port_fwd':
return [p.action, p.remote_host, p.remote_port, p.local_port].filter(v => v != null && v !== '').join(':');
case 'socks_start':
case 'socks_stop':
return p.port != null ? 'port:' + String(p.port) : type;
case 'load_assembly':
if (p.args) return String(p.args);
if (p.file_id) return 'file:' + String(p.file_id);
return '';
case 'persist':
return p.method != null ? String(p.method) : '';
default:
try { return JSON.stringify(p); } catch (e) { return ''; }
}
}
function truncateCommand(cmd, maxLen) {
if (!cmd) return '';
const s = String(cmd);
if (!maxLen || s.length <= maxLen) return s;
return s.substring(0, maxLen - 1) + '\u2026';
}
// ============================================================================
// 工具函数
// ============================================================================
@@ -116,10 +170,11 @@
const container = document.getElementById('c2-toast-container') || (() => {
const div = document.createElement('div');
div.id = 'c2-toast-container';
div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:8px;';
div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10100;display:flex;flex-direction:column;gap:8px;';
document.body.appendChild(div);
return div;
})();
container.style.zIndex = '10100';
const toast = document.createElement('div');
const colors = { error: '#e53e3e', success: '#38a169', info: '#3182ce', warn: '#d69e2e' };
toast.style.cssText = `background:${colors[type] || colors.info};color:#fff;padding:10px 18px;border-radius:6px;font-size:0.875rem;box-shadow:0 4px 12px rgba(0,0,0,0.2);opacity:0;transition:opacity .3s;max-width:400px;word-break:break-word;`;
@@ -725,7 +780,6 @@
C2.selectedSessionId = id;
C2.renderSessions();
C2.renderSessionDetail(id);
C2.initTerminal();
};
C2.renderSessionDetail = function(id) {
@@ -829,7 +883,10 @@
if (panel) panel.style.display = 'block';
if (tab === 'terminal') {
setTimeout(() => C2.fitTerminal(), 50);
setTimeout(function () {
C2.fitTerminal();
if (C2.terminalInstance) C2.terminalInstance.focus();
}, 50);
}
};
@@ -875,97 +932,57 @@
// xterm 终端
// ============================================================================
C2.initTerminal = function() {
const container = document.getElementById('c2-terminal-container');
if (!container || typeof Terminal === 'undefined') return;
if (C2.terminalInstance) {
C2.terminalInstance.dispose();
C2.serializeTerminalBuffer = function(term) {
if (!term || !term.buffer || !term.buffer.active) return '';
const buf = term.buffer.active;
const lines = [];
for (let i = 0; i < buf.length; i++) {
const line = buf.getLine(i);
if (line) lines.push(line.translateToString(true));
}
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
lineHeight: 1.3,
scrollback: 5000,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#58a6ff',
selection: 'rgba(88, 166, 255, 0.3)'
}
});
if (typeof FitAddon !== 'undefined') {
const FitCtor = FitAddon.FitAddon || FitAddon;
C2.terminalFitAddon = new FitCtor();
term.loadAddon(C2.terminalFitAddon);
}
term.open(container);
try {
if (C2.terminalFitAddon) C2.terminalFitAddon.fit();
} catch (e) {}
let lineBuffer = '';
const prompt = '$ ';
term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m');
term.writeln('');
term.write(prompt);
term.onData(e => {
const code = e.charCodeAt(0);
if (code === 13) { // Enter
term.writeln('');
const cmd = lineBuffer.trim();
lineBuffer = '';
if (cmd) {
C2.executeInTerminal(cmd, term);
} else {
term.write(prompt);
}
} else if (code === 127) { // Backspace
if (lineBuffer.length > 0) {
lineBuffer = lineBuffer.slice(0, -1);
term.write('\b \b');
}
} else if (code >= 32) { // Printable
lineBuffer += e;
term.write(e);
}
});
C2.terminalInstance = term;
// Resize observer
if (C2.terminalResizeObserver) {
C2.terminalResizeObserver.disconnect();
}
C2.terminalResizeObserver = new ResizeObserver(() => {
C2.fitTerminal();
});
C2.terminalResizeObserver.observe(container);
return lines.join('\n');
};
C2.fitTerminal = function() {
if (C2.terminalFitAddon && C2.terminalInstance) {
try {
C2.terminalFitAddon.fit();
} catch (e) {}
C2.pushTerminalHistory = function(cmd) {
const sid = C2.selectedSessionId;
if (!sid || !cmd) return;
if (!C2.terminalHistory[sid]) C2.terminalHistory[sid] = [];
const hist = C2.terminalHistory[sid];
if (hist.length === 0 || hist[hist.length - 1] !== cmd) {
hist.push(cmd);
if (hist.length > 200) hist.shift();
}
};
C2.executeInTerminal = function(cmd, term) {
C2.finishTerminalCommand = function(term, status) {
C2.terminalBusy = false;
const statusEl = document.getElementById('c2-terminal-status');
if (status === 'err' && statusEl) {
statusEl.textContent = c2t('c2.sessions.termStatusErr');
} else if (status === 'timeout' && statusEl) {
statusEl.textContent = c2t('c2.sessions.termStatusTimeout');
} else if (statusEl && C2.terminalQueue.length === 0) {
statusEl.textContent = c2t('c2.sessions.termStatusReady');
}
if (C2.terminalQueue.length > 0) {
const next = C2.terminalQueue.shift();
C2.runTerminalCommand(next, term);
return;
}
term.write('$ ');
if (statusEl && status !== 'err' && status !== 'timeout') {
statusEl.textContent = c2t('c2.sessions.termStatusReady');
}
};
C2.runTerminalCommand = function(cmd, term) {
if (!C2.selectedSessionId) {
term.writeln('\x1b[31m' + c2t('c2.sessions.termNoSession') + '\x1b[0m');
term.write('$ ');
return;
}
C2.terminalBusy = true;
C2.pushTerminalHistory(cmd);
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusExec');
@@ -976,14 +993,29 @@
}).then(data => {
if (data.error) {
term.writeln(`\x1b[31mError: ${data.error}\x1b[0m`);
term.write('$ ');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusErr');
C2.finishTerminalCommand(term, 'err');
} else {
C2.waitForTaskResult(data.task?.id || data.task_id, term);
}
}).catch(function () {
term.writeln('\x1b[31mError: request failed\x1b[0m');
C2.finishTerminalCommand(term, 'err');
});
};
C2.executeInTerminal = function(cmd, term) {
if (!cmd) {
term.write('$ ');
return;
}
if (C2.terminalBusy) {
C2.terminalQueue.push(cmd);
term.writeln('\x1b[33m' + c2t('c2.sessions.termQueued') + '\x1b[0m');
return;
}
C2.runTerminalCommand(cmd, term);
};
C2.waitForTaskResult = function(taskId, term) {
let attempts = 0;
const maxAttempts = 60;
@@ -992,9 +1024,7 @@
const check = () => {
if (++attempts > maxAttempts) {
term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitTimeout') + '\x1b[0m');
term.write('$ ');
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusTimeout');
C2.finishTerminalCommand(term, 'timeout');
return;
}
apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => {
@@ -1007,24 +1037,376 @@
if (task.error) {
term.writeln(`\x1b[31m${task.error}\x1b[0m`);
}
term.write('$ ');
const statusEl = document.getElementById('c2-terminal-status');
if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusReady');
C2.finishTerminalCommand(term, task.status === 'failed' ? 'err' : 'ready');
} else {
delay = Math.min(delay * 1.5, maxDelay);
setTimeout(check, delay);
}
}).catch(function () {
C2.finishTerminalCommand(term, 'err');
});
};
check();
};
C2.initTerminal = function() {
const container = document.getElementById('c2-terminal-container');
if (!container || typeof Terminal === 'undefined') return;
if (C2.terminalInstance && C2.terminalSessionId) {
C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance);
}
if (C2.terminalInstance) {
C2.terminalInstance.dispose();
}
const sessionId = C2.selectedSessionId || '_none';
C2.terminalSessionId = sessionId;
C2.terminalQueue = [];
C2.terminalBusy = false;
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'block',
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
lineHeight: 1.3,
scrollback: 5000,
theme: {
background: '#0d1117',
foreground: '#e6edf3',
cursor: '#58a6ff',
cursorAccent: '#0d1117',
selection: 'rgba(88, 166, 255, 0.3)'
}
});
if (typeof FitAddon !== 'undefined') {
const FitCtor = FitAddon.FitAddon || FitAddon;
C2.terminalFitAddon = new FitCtor();
term.loadAddon(C2.terminalFitAddon);
}
term.open(container);
try {
if (C2.terminalFitAddon) C2.terminalFitAddon.fit();
} catch (e) {}
let lineBuffer = '';
let cursorIndex = 0;
let historyIndex = -1;
let lastPasteAt = 0;
let lastPasteText = '';
const prompt = '$ ';
function redrawInputLine() {
term.write('\x1b[2K\r' + prompt + lineBuffer);
const tail = lineBuffer.length - cursorIndex;
if (tail > 0) term.write('\x1b[' + tail + 'D');
}
function resetInputLine() {
lineBuffer = '';
cursorIndex = 0;
historyIndex = -1;
term.write('\x1b[2K\r' + prompt);
}
function insertPlainText(text) {
const safe = String(text).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
if (!safe) return;
lineBuffer = lineBuffer.slice(0, cursorIndex) + safe + lineBuffer.slice(cursorIndex);
cursorIndex += safe.length;
redrawInputLine();
}
function deleteWordBeforeCursor() {
if (cursorIndex === 0) return;
let start = cursorIndex;
while (start > 0 && /\s/.test(lineBuffer[start - 1])) start--;
while (start > 0 && !/\s/.test(lineBuffer[start - 1])) start--;
lineBuffer = lineBuffer.slice(0, start) + lineBuffer.slice(cursorIndex);
cursorIndex = start;
redrawInputLine();
}
function moveWordLeft() {
if (cursorIndex === 0) return;
let pos = cursorIndex;
while (pos > 0 && /\s/.test(lineBuffer[pos - 1])) pos--;
while (pos > 0 && !/\s/.test(lineBuffer[pos - 1])) pos--;
const delta = cursorIndex - pos;
if (delta > 0) {
term.write('\x1b[' + delta + 'D');
cursorIndex = pos;
}
}
function moveWordRight() {
if (cursorIndex >= lineBuffer.length) return;
let pos = cursorIndex;
while (pos < lineBuffer.length && /\s/.test(lineBuffer[pos])) pos++;
while (pos < lineBuffer.length && !/\s/.test(lineBuffer[pos])) pos++;
const delta = pos - cursorIndex;
if (delta > 0) {
term.write('\x1b[' + delta + 'C');
cursorIndex = pos;
}
}
function showHistoryEntry(entry) {
lineBuffer = entry || '';
cursorIndex = lineBuffer.length;
term.write('\x1b[2K\r' + prompt + lineBuffer);
}
function submitCurrentLine() {
if (C2.terminalBusy) {
term.writeln('');
term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitFinish') + '\x1b[0m');
term.write(prompt + lineBuffer);
const tail = lineBuffer.length - cursorIndex;
if (tail > 0) term.write('\x1b[' + tail + 'D');
return;
}
term.writeln('');
const cmd = lineBuffer.trim();
lineBuffer = '';
cursorIndex = 0;
historyIndex = -1;
if (cmd) {
C2.executeInTerminal(cmd, term);
} else {
term.write(prompt);
}
}
function handlePasteText(text) {
const now = Date.now();
if (text === lastPasteText && now - lastPasteAt < 80) return;
lastPasteAt = now;
lastPasteText = text;
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
if (normalized.indexOf('\n') === -1) {
insertPlainText(normalized);
return;
}
const endsWithNewline = normalized.endsWith('\n');
const parts = normalized.split('\n');
const tail = parts.pop() || '';
parts.forEach(function (part) {
insertPlainText(part);
submitCurrentLine();
});
if (tail) insertPlainText(tail);
else if (endsWithNewline && parts.length === 0) submitCurrentLine();
}
const savedLog = C2.terminalLogs[sessionId];
if (savedLog) {
term.write(String(savedLog).replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'));
if (!savedLog.endsWith('\n')) term.write('\r\n');
} else {
term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m');
term.writeln('');
}
term.write(prompt);
term.onData(function (e) {
if (e === '\x0c') {
term.clear();
resetInputLine();
C2.terminalLogs[sessionId] = '';
return;
}
if (e === '\x03') {
if (C2.terminalBusy) {
term.writeln('');
term.writeln('\x1b[33m^C (' + c2t('c2.sessions.termCtrlC') + ')\x1b[0m');
}
resetInputLine();
return;
}
if (e === '\x16') {
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then(handlePasteText).catch(function () {});
}
return;
}
if (e.length > 1 && e.indexOf('\x1b') !== 0) {
handlePasteText(e);
return;
}
if (e === '\x1b[D' || e === '\x1bOD') {
if (cursorIndex > 0) {
cursorIndex--;
term.write('\x1b[D');
}
return;
}
if (e === '\x1b[C' || e === '\x1bOC') {
if (cursorIndex < lineBuffer.length) {
cursorIndex++;
term.write('\x1b[C');
}
return;
}
if (e === '\x1b[1;3D' || e === '\x1bb') {
moveWordLeft();
return;
}
if (e === '\x1b[1;3C' || e === '\x1bf') {
moveWordRight();
return;
}
if (e === '\x1b[A' || e === '\x1bOA') {
const hist = C2.terminalHistory[sessionId] || [];
if (hist.length === 0) return;
historyIndex = historyIndex < 0 ? hist.length - 1 : Math.max(0, historyIndex - 1);
showHistoryEntry(hist[historyIndex]);
return;
}
if (e === '\x1b[B' || e === '\x1bOB') {
const hist = C2.terminalHistory[sessionId] || [];
if (hist.length === 0) return;
historyIndex = historyIndex < 0 ? -1 : Math.min(hist.length - 1, historyIndex + 1);
if (historyIndex < 0) showHistoryEntry('');
else showHistoryEntry(hist[historyIndex]);
return;
}
if (e === '\x1b[H' || e === '\x1bOH' || e === '\x01') {
if (cursorIndex > 0) {
term.write('\x1b[' + cursorIndex + 'D');
cursorIndex = 0;
}
return;
}
if (e === '\x1b[F' || e === '\x1bOF' || e === '\x05') {
const move = lineBuffer.length - cursorIndex;
if (move > 0) {
term.write('\x1b[' + move + 'C');
cursorIndex = lineBuffer.length;
}
return;
}
if (e === '\x1b[3~') {
if (cursorIndex < lineBuffer.length) {
lineBuffer = lineBuffer.slice(0, cursorIndex) + lineBuffer.slice(cursorIndex + 1);
redrawInputLine();
}
return;
}
if (e === '\x15') {
resetInputLine();
return;
}
if (e === '\x0b') {
lineBuffer = lineBuffer.slice(0, cursorIndex);
redrawInputLine();
return;
}
if (e === '\x17') {
deleteWordBeforeCursor();
return;
}
if (e === '\x1b\x7f') {
deleteWordBeforeCursor();
return;
}
const code = e.charCodeAt(0);
if (code === 13 || code === 10) {
submitCurrentLine();
} else if (code === 127 || code === 8) {
if (cursorIndex > 0) {
lineBuffer = lineBuffer.slice(0, cursorIndex - 1) + lineBuffer.slice(cursorIndex);
cursorIndex--;
redrawInputLine();
}
} else if (e.length === 1 && code >= 32) {
historyIndex = -1;
lineBuffer = lineBuffer.slice(0, cursorIndex) + e + lineBuffer.slice(cursorIndex);
cursorIndex++;
if (cursorIndex === lineBuffer.length) {
term.write(e);
} else {
redrawInputLine();
}
}
});
const onTerminalPaste = function (ev) {
const text = ev.clipboardData && ev.clipboardData.getData('text');
if (!text) return;
ev.preventDefault();
handlePasteText(text);
};
if (term.element) {
term.element.addEventListener('paste', onTerminalPaste);
}
term.attachCustomKeyEventHandler(function (ev) {
if (ev.type !== 'keydown') return true;
if ((ev.ctrlKey || ev.metaKey) && !ev.shiftKey && (ev.key === 'c' || ev.key === 'C')) {
if (term.getSelection()) return true;
}
const isPaste = (ev.ctrlKey || ev.metaKey) && !ev.shiftKey && !ev.altKey
&& (ev.key === 'v' || ev.key === 'V');
if (isPaste && navigator.clipboard && navigator.clipboard.readText) {
ev.preventDefault();
navigator.clipboard.readText().then(handlePasteText).catch(function () {});
return false;
}
if (ev.shiftKey && ev.key === 'Insert' && navigator.clipboard && navigator.clipboard.readText) {
ev.preventDefault();
navigator.clipboard.readText().then(handlePasteText).catch(function () {});
return false;
}
return true;
});
container.addEventListener('click', function () {
term.focus();
});
container.setAttribute('tabindex', '0');
C2.terminalInstance = term;
if (C2.terminalResizeObserver) {
C2.terminalResizeObserver.disconnect();
}
C2.terminalResizeObserver = new ResizeObserver(function () {
C2.fitTerminal();
});
C2.terminalResizeObserver.observe(container);
setTimeout(function () {
try {
if (C2.terminalFitAddon) C2.terminalFitAddon.fit();
term.focus();
} catch (e) {}
}, 100);
};
C2.fitTerminal = function() {
if (C2.terminalFitAddon && C2.terminalInstance) {
try {
C2.terminalFitAddon.fit();
} catch (e) {}
}
};
C2.clearTerminal = function() {
if (C2.terminalInstance) {
C2.terminalInstance.clear();
C2.terminalInstance.writeln('\x1b[36m' + c2t('c2.sessions.termCleared') + '\x1b[0m');
C2.terminalInstance.write('$ ');
if (C2.terminalSessionId) {
C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance);
}
}
C2.terminalQueue = [];
};
C2.copyTerminal = function() {
@@ -1314,10 +1696,13 @@
container.innerHTML = tasks.map(t => {
const rawId = t.id || '';
const cmd = formatTaskCommand(t);
const cmdShort = truncateCommand(cmd, 40);
return `
<div class="c2-task-item-compact">
<span class="c2-task-status-dot ${escapeHtml(t.status || '')}"></span>
<span class="c2-task-type">${escapeHtml(t.taskType || '')}</span>
${cmdShort ? `<span class="c2-task-command" title="${escapeHtml(cmd)}">${escapeHtml(cmdShort)}</span>` : ''}
<span class="c2-task-meta">${escapeHtml(taskStatusLabel(t.status))} | ${formatDuration(t.durationMs)}</span>
<button type="button" class="btn-secondary btn-small" data-c2-task-action="view" data-task-id="${escapeHtml(rawId)}">${escapeHtml(c2t('c2.tasks.view'))}</button>
</div>
@@ -1353,6 +1738,7 @@
<th>${escapeHtml(c2t('c2.tasks.colTask'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colSession'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colType'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colCommand'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colStatus'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colDuration'))}</th>
<th>${escapeHtml(c2t('c2.tasks.colCreated'))}</th>
@@ -1364,6 +1750,8 @@
const rawId = t.id || '';
const shortTaskId = rawId.length > 14 ? escapeHtml(rawId.substring(0, 12)) + '\u2026' : escapeHtml(rawId);
const sid = t.sessionId ? escapeHtml(String(t.sessionId).substring(0, 8)) + '\u2026' : '-';
const cmd = formatTaskCommand(t);
const cmdShort = truncateCommand(cmd, 48);
return `
<tr>
<td class="c2-task-table-col-check">
@@ -1374,6 +1762,7 @@
<td>${shortTaskId}</td>
<td>${sid}</td>
<td>${escapeHtml(t.taskType || '')}</td>
<td class="c2-task-command-cell" title="${escapeHtml(cmd)}">${cmdShort ? escapeHtml(cmdShort) : '<span class="c2-muted">-</span>'}</td>
<td><span class="c2-status-badge ${escapeHtml(t.status || '')}">${escapeHtml(taskStatusLabel(t.status))}</span></td>
<td>${formatDuration(t.durationMs)}</td>
<td>${formatTime(t.createdAt)}</td>
@@ -1403,26 +1792,87 @@
const renderTaskModal = function(t) {
if (!t || !modal) return;
const cmd = formatTaskCommand(t);
const hasPayload = t.payload && typeof t.payload === 'object' && Object.keys(t.payload).length > 0;
const modalBox = modal.querySelector('.c2-modal');
if (modalBox) modalBox.classList.add('c2-modal--wide');
content.innerHTML = `
<div class="c2-modal-header">
<h3>${escapeHtml(c2t('c2.tasks.modalTitle'))}</h3>
<div class="c2-modal-header c2-task-modal-header">
<div class="c2-task-modal-heading">
<h3>${escapeHtml(c2t('c2.tasks.modalTitle'))}</h3>
<span class="c2-status-badge ${escapeHtml(t.status || '')}">${escapeHtml(taskStatusLabel(t.status))}</span>
</div>
<button class="c2-modal-close" onclick="C2.closeModal()">&times;</button>
</div>
<div class="c2-modal-body">
<div class="c2-task-detail">
<div><strong>${escapeHtml(c2t('c2.tasks.labelId'))}:</strong> ${escapeHtml(t.id || '')}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelSession'))}:</strong> ${escapeHtml(t.sessionId || '')}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelType'))}:</strong> ${escapeHtml(t.taskType || '')}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelStatus'))}:</strong> <span class="c2-status-badge ${escapeHtml(t.status || '')}">${escapeHtml(taskStatusLabel(t.status))}</span></div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelCreated'))}:</strong> ${formatTime(t.createdAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelSent'))}:</strong> ${formatTime(t.sentAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelCompleted'))}:</strong> ${formatTime(t.completedAt)}</div>
<div><strong>${escapeHtml(c2t('c2.tasks.labelDuration'))}:</strong> ${formatDuration(t.durationMs)}</div>
${t.error ? `<div class="c2-task-error"><strong>${escapeHtml(c2t('c2.tasks.labelError'))}:</strong> ${escapeHtml(t.error)}</div>` : ''}
<div class="c2-task-detail-grid">
<div class="c2-task-kv">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelId'))}</span>
<span class="c2-task-kv__value c2-task-kv__value--mono">${escapeHtml(t.id || '-')}</span>
</div>
<div class="c2-task-kv">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelSession'))}</span>
<span class="c2-task-kv__value c2-task-kv__value--mono">${escapeHtml(t.sessionId || '-')}</span>
</div>
<div class="c2-task-kv">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelType'))}</span>
<span class="c2-task-kv__value">${escapeHtml(t.taskType || '-')}</span>
</div>
<div class="c2-task-kv">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelDuration'))}</span>
<span class="c2-task-kv__value c2-task-kv__value--accent">${formatDuration(t.durationMs)}</span>
</div>
</div>
<div class="c2-task-timeline">
<div class="c2-task-time-card">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelCreated'))}</span>
<span class="c2-task-kv__value">${formatTime(t.createdAt) || '-'}</span>
</div>
<div class="c2-task-time-card">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelSent'))}</span>
<span class="c2-task-kv__value">${formatTime(t.sentAt) || '-'}</span>
</div>
<div class="c2-task-time-card">
<span class="c2-task-kv__label">${escapeHtml(c2t('c2.tasks.labelCompleted'))}</span>
<span class="c2-task-kv__value">${formatTime(t.completedAt) || '-'}</span>
</div>
</div>
${cmd ? `
<div class="c2-task-code-section">
<div class="c2-task-code-header">
<span class="c2-task-code-title">${escapeHtml(c2t('c2.tasks.labelCommand'))}</span>
<button type="button" class="btn-ghost btn-sm" onclick="C2.copyTaskBlock('c2-task-cmd-pre')">${escapeHtml(c2t('common.copy'))}</button>
</div>
<pre class="c2-task-command-pre" id="c2-task-cmd-pre">${escapeHtml(cmd)}</pre>
</div>
` : ''}
${hasPayload && !cmd ? `
<div class="c2-task-code-section">
<div class="c2-task-code-header">
<span class="c2-task-code-title">${escapeHtml(c2t('c2.tasks.labelPayload'))}</span>
<button type="button" class="btn-ghost btn-sm" onclick="C2.copyTaskBlock('c2-task-payload-pre')">${escapeHtml(c2t('common.copy'))}</button>
</div>
<pre class="c2-task-command-pre" id="c2-task-payload-pre">${escapeHtml(JSON.stringify(t.payload, null, 2))}</pre>
</div>
` : ''}
${t.error ? `
<div class="c2-task-error-section">
<div class="c2-task-code-header">
<span class="c2-task-code-title">${escapeHtml(c2t('c2.tasks.labelError'))}</span>
</div>
<div class="c2-task-error">${escapeHtml(t.error)}</div>
</div>
` : ''}
${t.resultText ? `
<div class="c2-task-result">
<strong>${escapeHtml(c2t('c2.tasks.labelResult'))}:</strong>
<pre>${escapeHtml(t.resultText)}</pre>
<div class="c2-task-code-section">
<div class="c2-task-code-header">
<span class="c2-task-code-title">${escapeHtml(c2t('c2.tasks.labelResult'))}</span>
<button type="button" class="btn-ghost btn-sm" onclick="C2.copyTaskBlock('c2-task-result-pre')">${escapeHtml(c2t('common.copy'))}</button>
</div>
<pre class="c2-task-result-pre" id="c2-task-result-pre">${escapeHtml(t.resultText)}</pre>
</div>
` : ''}
</div>
@@ -2049,9 +2499,18 @@
// 模态框
// ============================================================================
C2.copyTaskBlock = function(elementId) {
const el = document.getElementById(elementId);
if (el && el.textContent) copyToClipboard(el.textContent);
};
C2.closeModal = function() {
const modal = document.getElementById('c2-modal');
if (modal) modal.style.display = 'none';
if (modal) {
modal.style.display = 'none';
const modalBox = modal.querySelector('.c2-modal');
if (modalBox) modalBox.classList.remove('c2-modal--wide');
}
};
// ============================================================================
+1 -1
View File
@@ -2055,7 +2055,7 @@ function showToastNotification(message, type = 'info') {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
z-index: 10100;
display: flex;
flex-direction: column;
gap: 12px;