const state = {
scenarios: {},
currentScenario: "prompt_injection",
currentSample: null,
};
const SCENARIO_ICONS = {
prompt_injection: "💉",
jailbreak: "🔓",
data_leakage: "📤",
combo_attack: "⚔️",
};
const LAYER_LABELS = {
input_isolation: "输入隔离",
input_guard: "注入检测",
jailbreak_guard: "越狱检测",
task_lock: "任务锁定",
output_guard: "输出过滤",
};
const STEP_STATUS = {
passed: { icon: "✓", label: "通过" },
blocked: { icon: "✕", label: "拦截" },
warning: { icon: "!", label: "风险提示" },
skipped: { icon: "–", label: "跳过" },
};
const GUARD_CHECKBOX_MAP = {
enable_input_isolation: "输入隔离",
enable_injection_detection: "注入检测",
enable_jailbreak_detection: "越狱检测",
enable_output_filter: "输出过滤",
};
let lastPipeline = [];
const $ = (sel) => document.querySelector(sel);
async function api(path, options = {}) {
const res = await fetch(path, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
function levelBadge(level) {
if (!level) return "";
return `<span class="level-badge level-${level}">${level}</span>`;
}
function renderScenarios() {
const tabs = $("#scenarioTabs");
tabs.innerHTML = "";
Object.entries(state.scenarios).forEach(([id, sc]) => {
const btn = document.createElement("button");
const icon = SCENARIO_ICONS[id] || "📌";
btn.className = `tab${id === state.currentScenario ? " active" : ""}`;
btn.innerHTML = `${icon} ${sc.name}`;
btn.onclick = () => selectScenario(id);
tabs.appendChild(btn);
});
}
function renderSamples() {
const sc = state.scenarios[state.currentScenario];
if (!sc) return;
$("#scenarioDesc").textContent = sc.description;
const list = $("#sampleList");
list.innerHTML = "";
sc.samples.forEach((sample) => {
const li = document.createElement("li");
li.className = `sample-item${state.currentSample?.id === sample.id ? " active" : ""}`;
const chain = sample.attack_chain
? `<span class="attack-chain">攻击链:${sample.attack_chain}</span>`
: "";
li.innerHTML = `<strong>${sample.id}</strong>${sample.name}${levelBadge(sample.level)}<small>${sample.intent}</small>${chain}`;
li.onclick = () => selectSample(sample);
list.appendChild(li);
});
}
function renderAttackGuide(sample) {
const guide = $("#attackGuide");
if (!sample) {
guide.innerHTML = '<p class="empty-hint">选择样本后,这里会显示攻击手法与预期后果</p>';
return;
}
const layers = (sample.defense_layers || [])
.map((l) => `<span class="defense-layer-tag">${LAYER_LABELS[l] || l}</span>`)
.join("");
const chainBlock = sample.attack_chain
? `<div class="guide-section attack"><h3>攻击链</h3><p>${sample.attack_chain}</p></div>`
: "";
guide.innerHTML = `
<div class="guide-block">
${chainBlock}
<div class="guide-section attack">
<h3>怎么攻</h3>
<p>${sample.intent}</p>
${sample.tags ? `<p class="muted" style="margin-top:0.3rem;font-size:0.8rem">标签:${sample.tags.join(" · ")}</p>` : ""}
</div>
<div class="guide-section consequence">
<h3>可能造成什么后果</h3>
<ul>${sample.consequences.map((c) => `<li>${c}</li>`).join("")}</ul>
</div>
<div class="guide-section defense">
<h3>如何防御</h3>
<p>${sample.defense_hint || "启用右侧安全设定,观察哪一层拦截了攻击。"}</p>
${layers ? `<div style="margin-top:0.5rem">关键防御层:${layers}</div>` : ""}
</div>
</div>
`;
}
function selectScenario(id) {
state.currentScenario = id;
state.currentSample = null;
renderScenarios();
renderSamples();
renderAttackGuide(null);
}
function selectSample(sample) {
state.currentSample = sample;
$("#attackInput").value = sample.input;
renderSamples();
renderAttackGuide(sample);
}
function setBadge(el, success, isVuln) {
if (isVuln) {
el.textContent = success ? "攻击成功" : "攻击未生效";
el.className = `badge ${success ? "badge-danger" : "badge-neutral"}`;
} else {
el.textContent = success ? "攻击成功" : "防御成功";
el.className = `badge ${success ? "badge-danger" : "badge-success"}`;
}
}
function renderTags(container, items, cls = "") {
container.innerHTML = items.length
? items.map((t) => `<span class="tag ${cls}">${t}</span>`).join("")
: '<span class="muted" style="font-size:0.82rem">—</span>';
}
function renderCompareSummary(vuln, secure) {
const box = $("#compareSummary");
box.classList.remove("hidden");
const vulnStatus = $("#summaryVulnStatus");
const vulnDetail = $("#summaryVulnDetail");
if (vuln.attack_success) {
vulnStatus.textContent = "🔴 攻击成功";
vulnStatus.className = "summary-status hit";
vulnDetail.textContent = vuln.consequences.length
? `后果:${vuln.consequences.join("、")}`
: "脆弱模式被攻破";
} else {
vulnStatus.textContent = "⚪ 攻击未生效";
vulnStatus.className = "summary-status safe";
vulnDetail.textContent = "攻击未能突破无防护模式";
}
const secStatus = $("#summarySecureStatus");
const secDetail = $("#summarySecureDetail");
if (!secure.attack_success) {
secStatus.textContent = "🟢 防御成功";
secStatus.className = "summary-status ok";
secDetail.textContent = secure.blocked_by
? `拦截层:${LAYER_LABELS[secure.blocked_by] || secure.blocked_by}`
: "攻击被安全层阻断";
} else {
secStatus.textContent = "🔴 攻击成功";
secStatus.className = "summary-status fail";
secDetail.textContent = "安全模式未能拦截,需检查配置";
}
}
function updateDefenseWarning(cfg) {
const box = $("#defenseWarning");
const off = [];
for (const [key, label] of Object.entries(GUARD_CHECKBOX_MAP)) {
if (!cfg[key]) off.push(label);
}
if (off.length) {
box.textContent = `⚠️ 当前「${off.join("」「")}」已关闭,安全模式防护不完整`;
box.classList.remove("hidden");
} else {
box.classList.add("hidden");
}
}
function renderPipelineModal(steps) {
const box = $("#pipelineTimeline");
if (!steps?.length) {
box.innerHTML = '<p class="empty-hint">暂无处理流程数据</p>';
return;
}
box.innerHTML = steps
.map((step) => {
const meta = STEP_STATUS[step.status] || STEP_STATUS.skipped;
const detail = step.detail
? `<div class="step-detail">${escapeHtml(step.detail)}</div>`
: "";
return `
<div class="pipeline-step status-${step.status}">
<div class="step-icon">${meta.icon}</div>
<div class="step-body">
<div class="step-head">
<span class="step-name">${step.name}</span>
<span class="step-status-label ${step.status}">${meta.label}</span>
</div>
<p class="step-summary">${escapeHtml(step.summary)}</p>
${detail}
</div>
</div>`;
})
.join("");
}
function escapeHtml(text) {
return String(text)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function openPipelineModal() {
if (!lastPipeline.length) return;
renderPipelineModal(lastPipeline);
$("#pipelineModal").classList.remove("hidden");
}
function closePipelineModal() {
$("#pipelineModal").classList.add("hidden");
}
async function sendCompare() {
const message = $("#attackInput").value.trim();
if (!message) return alert("请输入攻击内容");
$("#loadingHint").classList.remove("hidden");
$("#btnSend").disabled = true;
try {
const data = await api("/api/compare", {
method: "POST",
body: JSON.stringify({
message,
scenario_id: state.currentScenario,
sample_id: state.currentSample?.id || null,
}),
});
const vuln = data.vulnerable;
const secure = data.secure;
renderCompareSummary(vuln, secure);
const vulnBox = $("#vulnOutput");
vulnBox.textContent = vuln.output;
vulnBox.className = `output-box${vuln.attack_success ? " attack-success" : ""}`;
setBadge($("#vulnBadge"), vuln.attack_success, true);
renderTags($("#vulnConsequences"), vuln.consequences);
$("#vulnExplain").textContent = vuln.defense_explanation || "";
const secureBox = $("#secureOutput");
secureBox.textContent = secure.output;
secureBox.className = `output-box${!secure.attack_success ? " defense-success" : " attack-success"}`;
setBadge($("#secureBadge"), secure.attack_success, false);
const defenseTags = [];
if (secure.blocked_by) {
defenseTags.push(`拦截层: ${LAYER_LABELS[secure.blocked_by] || secure.blocked_by}`);
}
if (secure.consequences.length) defenseTags.push(...secure.consequences);
renderTags($("#secureDefense"), defenseTags, "defense");
$("#secureExplain").textContent = secure.defense_explanation || "";
lastPipeline = secure.security_pipeline || [];
const btnPipeline = $("#btnPipeline");
if (lastPipeline.length) {
btnPipeline.classList.remove("hidden");
openPipelineModal();
} else {
btnPipeline.classList.add("hidden");
}
await loadAuditLogs();
} catch (err) {
alert("请求失败: " + err.message);
} finally {
$("#loadingHint").classList.add("hidden");
$("#btnSend").disabled = false;
}
}
async function loadAuditLogs() {
const logs = await api("/api/audit/logs");
const box = $("#auditLog");
if (!logs.length) {
box.innerHTML = '<p class="empty-hint">暂无记录,发送实验后显示</p>';
return;
}
box.innerHTML = logs
.slice()
.reverse()
.map((e) => {
const cls = e.attack_success ? "attacked" : "defended";
const statusCls = e.attack_success ? "bad" : "ok";
const statusText = e.attack_success ? "攻击成功" : "已防御";
return `
<div class="audit-entry ${cls}">
<strong>${e.timestamp.slice(11, 19)}</strong>
[${e.mode}] ${e.scenario}
— <span class="audit-status ${statusCls}">${statusText}</span>
${e.blocked_by ? `<span class="muted"> (${e.blocked_by})</span>` : ""}
<br/><span class="muted">${e.message_preview}</span>
</div>`;
})
.join("");
}
async function loadSecurityConfig() {
const cfg = await api("/api/security/config");
const form = $("#securityForm");
form.enable_input_isolation.checked = cfg.enable_input_isolation;
form.enable_injection_detection.checked = cfg.enable_injection_detection;
form.enable_jailbreak_detection.checked = cfg.enable_jailbreak_detection;
form.enable_output_filter.checked = cfg.enable_output_filter;
form.audit_enabled.checked = cfg.audit_enabled;
form.task_whitelist.value = cfg.task_whitelist.join(",");
form.refusal_message.value = cfg.refusal_message;
updateDefenseWarning(cfg);
}
async function saveSecurityConfig(e) {
e.preventDefault();
const form = $("#securityForm");
const body = {
enable_input_isolation: form.enable_input_isolation.checked,
enable_injection_detection: form.enable_injection_detection.checked,
enable_jailbreak_detection: form.enable_jailbreak_detection.checked,
enable_output_filter: form.enable_output_filter.checked,
audit_enabled: form.audit_enabled.checked,
task_whitelist: form.task_whitelist.value.split(",").map((s) => s.trim()).filter(Boolean),
refusal_message: form.refusal_message.value,
};
const cfg = await api("/api/security/config", { method: "PUT", body: JSON.stringify(body) });
updateDefenseWarning(cfg);
$("#settingsStatus").textContent = "✓ 设定已保存";
setTimeout(() => ($("#settingsStatus").textContent = ""), 2500);
}
async function runRedteam() {
$("#redteamModal").classList.remove("hidden");
$("#redteamSummary").textContent = "运行中…";
$("#redteamResults").innerHTML = "";
try {
const data = await api("/api/redteam/run", { method: "POST" });
const s = data.summary;
let levelHtml = "";
if (s.by_level) {
levelHtml = "<br/>按难度:" + Object.entries(s.by_level)
.map(([lv, v]) => `${lv} ${v.blocked}/${v.total}(${v.block_rate})`)
.join(" · ");
}
$("#redteamSummary").innerHTML = `
共 <strong>${s.total}</strong> 个样本 ·
脆弱模式攻破 <strong>${s.vulnerable_hit}</strong> 个 ·
安全模式拦截 <strong>${s.secure_blocked}</strong> 个 ·
拦截率 <strong>${s.block_rate}</strong>
${levelHtml}
`;
$("#redteamResults").innerHTML = `
<div class="redteam-row header">
<span>编号</span><span>难度</span><span>场景</span><span>脆弱</span><span>安全拦截</span>
</div>
${data.results
.map((r) => {
const vulnCls = r.vulnerable_success ? "fail" : "pass";
const secCls = r.secure_blocked ? "pass" : "fail";
return `
<div class="redteam-row">
<span><strong>${r.sample_id}</strong></span>
<span>${r.level || "—"}</span>
<span>${r.scenario}</span>
<span class="${vulnCls}">${r.vulnerable_success ? "成功" : "失败"}</span>
<span class="${secCls}">${r.secure_blocked ? (LAYER_LABELS[r.blocked_by] || r.blocked_by || "已防御") : "未拦截"}</span>
</div>`;
})
.join("")}
`;
} catch (err) {
$("#redteamSummary").textContent = "失败: " + err.message;
}
}
$("#redteamModal")?.addEventListener("click", (e) => {
if (e.target === $("#redteamModal")) $("#redteamModal").classList.add("hidden");
});
$("#pipelineModal")?.addEventListener("click", (e) => {
if (e.target === $("#pipelineModal")) closePipelineModal();
});
$("#securityForm")?.addEventListener("change", () => {
const form = $("#securityForm");
updateDefenseWarning({
enable_input_isolation: form.enable_input_isolation.checked,
enable_injection_detection: form.enable_injection_detection.checked,
enable_jailbreak_detection: form.enable_jailbreak_detection.checked,
enable_output_filter: form.enable_output_filter.checked,
});
});
async function init() {
state.scenarios = await api("/api/scenarios");
renderScenarios();
renderSamples();
await loadSecurityConfig();
$("#btnSend").onclick = sendCompare;
$("#securityForm").onsubmit = saveSecurityConfig;
$("#btnRedteam").onclick = runRedteam;
$("#btnCloseModal").onclick = () => $("#redteamModal").classList.add("hidden");
$("#btnPipeline").onclick = openPipelineModal;
$("#btnClosePipeline").onclick = closePipelineModal;
const first = state.scenarios[state.currentScenario]?.samples?.[0];
if (first) selectSample(first);
}
init();