feat: 优化运行时判定

This commit is contained in:
richarjiang
2026-09-24 14:27:07 +08:00
parent dfd3998399
commit f0ac603f27
25 changed files with 2368 additions and 58 deletions

View File

@@ -147,6 +147,33 @@
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.switch input { accent-color: var(--blue); width: 16px; height: 16px; }
.stage-controls {
display: inline-flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.segmented {
display: inline-flex;
background: white;
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.segmented button {
border: 0;
background: transparent;
padding: 5px 12px;
border-radius: 999px;
font-size: 13px;
color: var(--secondary);
transition: background 0.18s, color 0.18s;
}
.segmented button.active {
background: var(--blue);
color: white;
}
.empty {
max-width: 36rem;
color: var(--secondary);
@@ -168,6 +195,15 @@
to { opacity: 1; transform: none; }
}
.when { margin: 0; color: var(--tertiary); font-size: 12px; }
.tag {
display: inline-block;
margin-right: 6px;
padding: 0 6px;
border-radius: 999px;
background: var(--fill);
color: var(--secondary);
font-size: 12px;
}
.prompt {
margin: 10px 0 16px;
font-size: 17px;
@@ -214,7 +250,7 @@
}
@media (prefers-reduced-motion: reduce) {
.card.arrive { animation: none; }
.split span, .track i { transition: none; }
.split span, .track i, .segmented button { transition: none; }
}
</style>
</head>
@@ -244,7 +280,28 @@ const METERS = [
["needs_web", "先查文档"],
["needs_deep_reasoning", "先想清楚"],
];
const state = { records: [], follow: true, selected: null, fresh: new Set() };
const EVENTS = {
UserPromptSubmit: "用户提问",
PreToolUse: "工具前",
PostToolUse: "工具后",
Stop: "结束检查",
judge: "主动判定",
};
const ACTIONS = {
inject: "注入",
deny: "拦截",
ask: "询问",
block: "拦截",
silent: "沉默",
};
const SORT_KEY = "laya-router:turn-sort";
const state = {
records: [],
follow: true,
selected: null,
fresh: new Set(),
sort: localStorage.getItem(SORT_KEY) === "asc" ? "asc" : "desc",
};
function keyOf(record) {
if (record.session_id) return record.session_id;
@@ -295,7 +352,7 @@ function render() {
aside.innerHTML = groups.map(([key, items]) => {
const last = items[items.length - 1];
const snippet = (last.prompt || "没有记下原文").replace(/\s+/g, " ");
const levelName = LEVELS.find(([id]) => id === last.level)?.[1] || last.level;
const levelName = LEVELS.find(([id]) => id === last.level)?.[1] || ACTIONS[last.action] || EVENTS[last.event] || last.level || "";
return `<button class="session ${key === state.selected ? "active" : ""}" data-key="${encodeURIComponent(key)}" type="button">
<div class="rowline"><span>${escapeHtml(titleOf(last))}</span><span class="level quiet">${escapeHtml(levelName)}</span></div>
<p class="snippet">${escapeHtml(snippet)}</p>
@@ -312,24 +369,32 @@ function render() {
const [key, items] = group;
const head = items[items.length - 1];
const followChecked = state.follow ? "checked" : "";
const ordered = state.sort === "desc"
? items.map((r, i) => [i, r]).reverse()
: items.map((r, i) => [i, r]);
stage.innerHTML = `<div class="stage-head">
<div>
<h2>${escapeHtml(titleOf(head))}</h2>
<p class="path">${escapeHtml(head.cwd || "没有记录工作目录")}</p>
</div>
<label class="switch"><input id="follow" type="checkbox" ${followChecked} /> 跟随最新</label>
<div class="stage-controls">
<div class="segmented" role="group" aria-label="排序方式">
<button type="button" data-sort="desc" class="${state.sort === "desc" ? "active" : ""}" aria-pressed="${state.sort === "desc"}">最新在前</button>
<button type="button" data-sort="asc" class="${state.sort === "asc" ? "active" : ""}" aria-pressed="${state.sort === "asc"}">最早在前</button>
</div>
<label class="switch"><input id="follow" type="checkbox" ${followChecked} /> 跟随最新</label>
</div>
</div>
${items.map((record, index) => {
${ordered.map(([index, record]) => {
const id = recordId(record);
const arrive = state.fresh.has(id) ? "arrive" : "";
state.fresh.delete(id);
const probs = record.level_probabilities || {};
const levelName = LEVELS.find(([name]) => name === record.level)?.[1] || record.level;
return `<article class="turn">
<div class="card ${arrive}">
<p class="when">第 ${index + 1} 次 · ${when(record.ts)} · ${record.elapsed_ms ?? "?"} 毫秒</p>
<p class="prompt">${escapeHtml(record.prompt || "原文没有写入日志")}</p>
<div class="split" role="img" aria-label="复杂度分布">
const eventLabel = EVENTS[record.event] || (record.event ? record.event : "用户提问");
const actionLabel = ACTIONS[record.action] || "";
const hasLevel = Boolean(record.level);
const distribution = hasLevel ? `<div class="split" role="img" aria-label="复杂度分布">
${LEVELS.map(([name]) => `<span class="${name}" style="width:${Math.max(0, Math.min(100, Number(probs[name] || 0) * 100))}%"></span>`).join("")}
</div>
<div class="legend">
@@ -339,7 +404,14 @@ function render() {
const value = Number(record[field] || 0);
return `<div class="meter"><div class="rowline"><span>${label}</span><span class="quiet">${pct(value)}</span></div><div class="track ${value >= 0.7 ? "hot" : ""}"><i style="width:${Math.max(0, Math.min(100, value * 100))}%"></i></div></div>`;
}).join("")}
<p class="note">${escapeHtml(levelName)}。${escapeHtml(advice(record))}</p>
<p class="note">${escapeHtml(levelName)}。${escapeHtml(advice(record))}</p>` : "";
const hint = !hasLevel && record.hint ? `<p class="note">${escapeHtml(record.hint)}</p>` : "";
return `<article class="turn">
<div class="card ${arrive}">
<p class="when"><span class="tag">${escapeHtml(eventLabel)}</span>${actionLabel ? `<span class="tag">${escapeHtml(actionLabel)}</span>` : ""}第 ${index + 1} 次 · ${when(record.ts)} · ${record.elapsed_ms ?? "?"} 毫秒</p>
<p class="prompt">${escapeHtml(record.prompt || "原文没有写入日志")}</p>
${distribution}
${hint}
</div>
</article>`;
}).join("")}`;
@@ -350,10 +422,22 @@ function render() {
render();
});
}
if (state.follow) stage.scrollTop = stage.scrollHeight;
const segmented = stage.querySelector(".segmented");
if (segmented) {
segmented.addEventListener("click", (event) => {
const button = event.target.closest("button[data-sort]");
if (!button || button.classList.contains("active")) return;
state.sort = button.dataset.sort === "asc" ? "asc" : "desc";
localStorage.setItem(SORT_KEY, state.sort);
render();
});
}
if (state.follow) {
stage.scrollTop = state.sort === "desc" ? 0 : stage.scrollHeight;
}
}
function recordId(record) {
return `${record.ts}|${record.prompt_sha256}|${record.session_id || ""}`;
return `${record.seq || ""}|${record.ts}|${record.prompt_sha256}|${record.session_id || ""}|${record.event || ""}|${record.tool_name || ""}`;
}
function remember(record) {
const id = recordId(record);