mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-07 14:53:59 +02:00
Add files via upload
This commit is contained in:
+162
-8
@@ -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%; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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()">×</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');
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user