mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 06:27:53 +02:00
Attack chain: failure text goes to textContent, so opt out of i18next interpolation escaping that rendered upstream errors as //"; give regenerate its own message key; keep the summary card expanded across a 409 refresh; probe for a cached graph before parsing last_react_input so polling no longer re-reads a multi-megabyte column; hide the card in the node-details exclusive state and bound it on narrow screens; drop the dead hardcoded Chinese title from the API payload. Blackboard: stop mirroring note/context-* keys into the fact body links section, which get_project_fact returns verbatim without scope filtering; single-line category and confidence in index rows so a newline cannot inject a fake row; make the path overview fail closed on an empty index set. Provenance: reuse fitUserIntentLedgerText when sanitizing a ledger entry so a single entry cannot exceed its cap by the three ellipsis runes. The .cjs harness now escapes interpolated values like real i18next, so the existing XSS test also pins the escapeValue opt-out.
128 lines
5.4 KiB
JavaScript
128 lines
5.4 KiB
JavaScript
// Compressed context remains separate from attack nodes and original user authorization.
|
|
(function () {
|
|
'use strict';
|
|
|
|
let labels = [];
|
|
|
|
function translate(key, values) {
|
|
if (typeof window.t === 'function') return window.t('attackChainModal.' + key, values);
|
|
const fallback = {
|
|
contextTitle: '压缩摘要与证据线索 ({{count}})',
|
|
contextNotice: '当前会话轨迹中的系统生成摘要,可能晚于图的生成时间。仅供回溯与检索,不代表用户的新授权或已核实工具证据。可按摘要内的快照路径读取原始证据。',
|
|
contextSummary: '压缩摘要 {{number}}',
|
|
contextCopy: '复制完整摘要',
|
|
contextCopied: '已复制',
|
|
contextCopyFailed: '复制失败,可选中文本手动复制。'
|
|
};
|
|
let text = fallback[key] || key;
|
|
Object.entries(values || {}).forEach(([name, value]) => {
|
|
text = text.replace('{{' + name + '}}', String(value));
|
|
});
|
|
return text;
|
|
}
|
|
|
|
function label(element, key, values) {
|
|
const refresh = () => { element.textContent = translate(key, values); };
|
|
labels.push(refresh);
|
|
refresh();
|
|
}
|
|
|
|
function clear() {
|
|
labels = [];
|
|
const card = document.getElementById('attack-chain-context');
|
|
const contents = document.getElementById('attack-chain-context-content');
|
|
if (contents) contents.replaceChildren();
|
|
if (card) {
|
|
card.hidden = true;
|
|
card.open = false;
|
|
}
|
|
}
|
|
|
|
async function copyText(text) {
|
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return;
|
|
} catch (_) {
|
|
// Plain HTTP deployments may deny clipboard access; retain manual-copy fallback.
|
|
}
|
|
}
|
|
const previousFocus = document.activeElement;
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.readOnly = true;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.opacity = '0';
|
|
document.body.appendChild(textarea);
|
|
try {
|
|
textarea.focus();
|
|
textarea.select();
|
|
if (!document.execCommand('copy')) throw new Error('copy failed');
|
|
} finally {
|
|
textarea.remove();
|
|
if (previousFocus && typeof previousFocus.focus === 'function') {
|
|
previousFocus.focus({ preventScroll: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
function render(chainData) {
|
|
const card = document.getElementById('attack-chain-context');
|
|
// A 409 refresh re-renders the same conversation; keep the card's open state.
|
|
const wasOpen = !!card && card.open;
|
|
clear();
|
|
const contents = document.getElementById('attack-chain-context-content');
|
|
if (!card || !contents) return;
|
|
|
|
// Only explicit server provenance qualifies; summary-looking user text stays user text.
|
|
const summaries = Array.isArray(chainData && chainData.context_summaries)
|
|
? chainData.context_summaries.filter(item => item &&
|
|
item.provenance === 'system_generated_summary' &&
|
|
typeof item.content === 'string' && item.content.trim())
|
|
: [];
|
|
if (!summaries.length) return;
|
|
|
|
label(document.getElementById('attack-chain-context-title'), 'contextTitle', { count: summaries.length });
|
|
label(document.getElementById('attack-chain-context-notice'), 'contextNotice');
|
|
summaries.forEach((item, index) => {
|
|
const entry = document.createElement('details');
|
|
entry.className = 'attack-chain-context-entry';
|
|
const heading = document.createElement('summary');
|
|
label(heading, 'contextSummary', { number: index + 1 });
|
|
const body = document.createElement('pre');
|
|
body.className = 'attack-chain-context-body';
|
|
// Do not render Markdown/HTML or turn evidence paths into clickable URLs.
|
|
body.textContent = item.content;
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'btn-secondary attack-chain-context-copy';
|
|
label(button, 'contextCopy');
|
|
const status = document.createElement('span');
|
|
status.className = 'attack-chain-context-copy-status';
|
|
status.setAttribute('role', 'status');
|
|
let statusKey = '';
|
|
labels.push(() => { status.textContent = statusKey ? translate(statusKey) : ''; });
|
|
button.addEventListener('click', async () => {
|
|
button.disabled = true;
|
|
status.textContent = '';
|
|
try {
|
|
await copyText(item.content);
|
|
statusKey = 'contextCopied';
|
|
} catch (_) {
|
|
statusKey = 'contextCopyFailed';
|
|
} finally {
|
|
status.textContent = translate(statusKey);
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
entry.append(heading, body, button, status);
|
|
contents.appendChild(entry);
|
|
});
|
|
card.hidden = false;
|
|
card.open = wasOpen;
|
|
}
|
|
|
|
document.addEventListener('languagechange', () => labels.forEach(refresh => refresh()));
|
|
window.CyberStrikeAttackChainContext = { render, clear };
|
|
})();
|