@bolloon/bolloon-agent 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +668 -204
- package/dist/web/index.html +24 -4
- package/dist/web/server.js +531 -10
- package/lefthook.yml +29 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +668 -204
- package/src/web/index.html +24 -4
- package/src/web/server.ts +596 -10
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
package/dist/web/client.js
CHANGED
|
@@ -4,8 +4,6 @@ if (typeof marked === 'undefined') {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
const messagesEl = document.getElementById('messages');
|
|
7
|
-
const agentStatusEl = document.getElementById('agent-status');
|
|
8
|
-
const agentStatusTextEl = document.getElementById('agent-status-text');
|
|
9
7
|
const input = document.getElementById('input');
|
|
10
8
|
const sendBtn = document.getElementById('send');
|
|
11
9
|
const sidebar = document.getElementById('sidebar');
|
|
@@ -913,7 +911,7 @@ async function selectChannel(channelId, targetSessionId = null) {
|
|
|
913
911
|
const tmpContainer = document.createElement('div');
|
|
914
912
|
tmpContainer.style.display = 'none';
|
|
915
913
|
for (const msg of msgs) {
|
|
916
|
-
addMessage(msg.content, msg.type, false, tmpContainer);
|
|
914
|
+
addMessage(msg.content, msg.type, false, tmpContainer, msg.metadata?.usedJudgmentIds || []);
|
|
917
915
|
}
|
|
918
916
|
while (tmpContainer.firstChild) {
|
|
919
917
|
frag.appendChild(tmpContainer.firstChild);
|
|
@@ -938,7 +936,7 @@ async function loadSession(channelId, sessionId = null) {
|
|
|
938
936
|
container.innerHTML = '';
|
|
939
937
|
if (session.messages && session.messages.length > 0) {
|
|
940
938
|
session.messages.forEach(msg => {
|
|
941
|
-
addMessage(msg.content, msg.type, false, container);
|
|
939
|
+
addMessage(msg.content, msg.type, false, container, msg.metadata?.usedJudgmentIds || []);
|
|
942
940
|
});
|
|
943
941
|
} else {
|
|
944
942
|
addMessage('你好!我是 Bolloon Agent。有什么我可以帮你的吗?', 'ai', false, container);
|
|
@@ -950,7 +948,7 @@ async function loadSession(channelId, sessionId = null) {
|
|
|
950
948
|
}
|
|
951
949
|
}
|
|
952
950
|
|
|
953
|
-
function addMessage(content, type, save = true, container) {
|
|
951
|
+
function addMessage(content, type, save = true, container, usedJudgmentIds = []) {
|
|
954
952
|
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
955
953
|
|
|
956
954
|
// 浏览器侧内存保护: 单个 channel 的消息容器超过 MAX_MESSAGES_PER_CHANNEL
|
|
@@ -981,12 +979,6 @@ function addMessage(content, type, save = true, container) {
|
|
|
981
979
|
const div = document.createElement('div');
|
|
982
980
|
div.className = `message message-${type}`;
|
|
983
981
|
|
|
984
|
-
// 清理工具结果容器(当新的 AI 消息到达时)
|
|
985
|
-
if (type === 'ai' && toolResultContainer) {
|
|
986
|
-
toolResultContainer.remove();
|
|
987
|
-
toolResultContainer = null;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
982
|
// 清理内容:移除 tool call 标记和其他不应该显示的内容
|
|
991
983
|
let cleanContent = content
|
|
992
984
|
.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, '')
|
|
@@ -1170,12 +1162,13 @@ function addMessage(content, type, save = true, container) {
|
|
|
1170
1162
|
|
|
1171
1163
|
actionsDiv.appendChild(copyBtn);
|
|
1172
1164
|
|
|
1173
|
-
// "存为判断" 按钮:
|
|
1165
|
+
// "存为判断" 按钮: AI 蒸馏 (30-80 字) + 自动演化对齐
|
|
1174
1166
|
const saveJudgmentBtn = document.createElement('button');
|
|
1175
1167
|
saveJudgmentBtn.className = 'action-btn save-as-judgment';
|
|
1176
|
-
saveJudgmentBtn.title = '
|
|
1177
|
-
saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800));
|
|
1178
|
-
|
|
1168
|
+
saveJudgmentBtn.title = 'AI 蒸馏为 30-80 字判断力 + 自动演化对齐';
|
|
1169
|
+
saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800));
|
|
1170
|
+
if (currentChannelId) saveJudgmentBtn.setAttribute('data-channel-id', currentChannelId);
|
|
1171
|
+
saveJudgmentBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path><path d="M9 12l2 2 4-4"></path></svg> 蒸馏为判断`;
|
|
1179
1172
|
actionsDiv.appendChild(saveJudgmentBtn);
|
|
1180
1173
|
if (type === 'ai') {
|
|
1181
1174
|
actionsDiv.appendChild(regenerateBtn);
|
|
@@ -1183,199 +1176,206 @@ function addMessage(content, type, save = true, container) {
|
|
|
1183
1176
|
div.appendChild(actionsDiv);
|
|
1184
1177
|
}
|
|
1185
1178
|
|
|
1179
|
+
// P0.5: 反向引用链接 (AI 消息) — 极简, 点击跳判断力 modal
|
|
1180
|
+
if (type === 'ai' && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
|
|
1181
|
+
const link = document.createElement('a');
|
|
1182
|
+
link.className = 'used-judgments-link';
|
|
1183
|
+
link.style.cssText = 'display:inline-block;margin-top:4px;font-size:11px;color:#0369a1;text-decoration:underline;cursor:pointer;';
|
|
1184
|
+
link.textContent = `📎 参考 ${usedJudgmentIds.length} 条原则`;
|
|
1185
|
+
link.onclick = (e) => {
|
|
1186
|
+
e.preventDefault();
|
|
1187
|
+
openJudgmentsModalWithFilter(usedJudgmentIds);
|
|
1188
|
+
};
|
|
1189
|
+
div.appendChild(link);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1186
1192
|
div.appendChild(time);
|
|
1187
1193
|
msgContainer.appendChild(div);
|
|
1188
1194
|
|
|
1189
1195
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1190
1196
|
}
|
|
1191
1197
|
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
let
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1198
|
+
// ============================================================
|
|
1199
|
+
// Loop timeline panel (Claude Code 风格)
|
|
1200
|
+
// ============================================================
|
|
1201
|
+
|
|
1202
|
+
let timelinePanelEl = null;
|
|
1203
|
+
let timelineRowsEl = null;
|
|
1204
|
+
let currentTokenRow = null;
|
|
1205
|
+
let currentTokenText = '';
|
|
1206
|
+
let lastUsedJudgmentIds = []; // 用于 finalizeTimelineAsMessage 给 addMessage 第 5 参
|
|
1207
|
+
|
|
1208
|
+
const PHASE_TEXT = {
|
|
1209
|
+
gate_compute: '正在检索相关判断力...',
|
|
1210
|
+
gate_done: '已注入 {usedCount} 条原则',
|
|
1211
|
+
d_detect: 'D 触发: 监测对话...',
|
|
1212
|
+
d_distill: 'D 触发: 蒸馏判断力...',
|
|
1213
|
+
d_done: 'D 触发: 已入库',
|
|
1214
|
+
d_skip: 'D 触发: 跳过',
|
|
1215
|
+
d_error: 'D 触发: 错误',
|
|
1201
1216
|
};
|
|
1202
1217
|
|
|
1203
|
-
function
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1218
|
+
function initTimelinePanel() {
|
|
1219
|
+
if (timelinePanelEl) return;
|
|
1220
|
+
timelinePanelEl = document.getElementById('loop-timeline-panel');
|
|
1221
|
+
timelineRowsEl = document.getElementById('loop-timeline-rows');
|
|
1222
|
+
const abortBtn = document.getElementById('loop-abort-btn');
|
|
1223
|
+
if (abortBtn) abortBtn.addEventListener('click', abortCurrentRun);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function resetTimeline() {
|
|
1227
|
+
if (timelineRowsEl) timelineRowsEl.innerHTML = '';
|
|
1228
|
+
currentTokenRow = null;
|
|
1229
|
+
currentTokenText = '';
|
|
1230
|
+
lastUsedJudgmentIds = [];
|
|
1231
|
+
const title = document.getElementById('loop-timeline-title');
|
|
1232
|
+
if (title) title.textContent = '▸ 运行中';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function showTimelinePanel() {
|
|
1236
|
+
initTimelinePanel();
|
|
1237
|
+
resetTimeline();
|
|
1238
|
+
if (timelinePanelEl) timelinePanelEl.hidden = false;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function hideTimelinePanel() {
|
|
1242
|
+
if (timelinePanelEl) timelinePanelEl.hidden = true;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function scrollTimelineToBottom() {
|
|
1246
|
+
if (timelinePanelEl) timelinePanelEl.scrollTop = timelinePanelEl.scrollHeight;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function appendPhaseRow(text, status) {
|
|
1250
|
+
if (!timelineRowsEl) return;
|
|
1251
|
+
const row = document.createElement('div');
|
|
1252
|
+
row.className = 'loop-row loop-row-phase';
|
|
1253
|
+
row.style.cssText = 'padding:2px 0;color:#374151;';
|
|
1254
|
+
const icon = status === 'done' ? '✓' : status === 'error' ? '✗' : '●';
|
|
1255
|
+
row.innerHTML = `<span style="color:#6b7280;">${icon}</span> <span>${escapeHtml(text)}</span>`;
|
|
1256
|
+
timelineRowsEl.appendChild(row);
|
|
1257
|
+
scrollTimelineToBottom();
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function appendOrUpdateTokenRow(delta) {
|
|
1261
|
+
if (!timelineRowsEl) return;
|
|
1262
|
+
if (!currentTokenRow) {
|
|
1263
|
+
currentTokenRow = document.createElement('div');
|
|
1264
|
+
currentTokenRow.className = 'loop-row loop-row-token';
|
|
1265
|
+
currentTokenRow.style.cssText = 'padding:2px 0 2px 16px;color:#1f2937;white-space:pre-wrap;word-break:break-word;';
|
|
1266
|
+
currentTokenText = '';
|
|
1267
|
+
timelineRowsEl.appendChild(currentTokenRow);
|
|
1268
|
+
}
|
|
1269
|
+
currentTokenText += delta;
|
|
1270
|
+
currentTokenRow.textContent = currentTokenText;
|
|
1271
|
+
scrollTimelineToBottom();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function appendToolRow(toolName, status) {
|
|
1275
|
+
if (!timelineRowsEl) return;
|
|
1276
|
+
const row = document.createElement('div');
|
|
1277
|
+
row.className = 'loop-row loop-row-tool';
|
|
1278
|
+
row.dataset.status = status;
|
|
1279
|
+
row.style.cssText = 'padding:2px 0;color:#1e40af;cursor:pointer;user-select:none;';
|
|
1280
|
+
const icon = status === 'done' ? '✓' : status === 'error' ? '✗' : '●';
|
|
1281
|
+
row.innerHTML = `<span style="color:#6b7280;">${icon}</span> ${escapeHtml(toolName)} <span class="toggle" style="color:#6b7280;">▸</span>`;
|
|
1282
|
+
row.addEventListener('click', () => {
|
|
1283
|
+
const detail = row.nextElementSibling;
|
|
1284
|
+
if (detail && detail.classList.contains('loop-row-tool-detail')) {
|
|
1285
|
+
const isHidden = detail.style.display === 'none';
|
|
1286
|
+
detail.style.display = isHidden ? 'block' : 'none';
|
|
1287
|
+
const tg = row.querySelector('.toggle');
|
|
1288
|
+
if (tg) tg.textContent = isHidden ? '▾' : '▸';
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
timelineRowsEl.appendChild(row);
|
|
1292
|
+
return row;
|
|
1232
1293
|
}
|
|
1233
1294
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
bubble.className = 'bubble bubble-ai streaming-content';
|
|
1244
|
-
streamingMessageEl.appendChild(bubble);
|
|
1245
|
-
msgContainer.appendChild(streamingMessageEl);
|
|
1246
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1295
|
+
function appendToolDetail(row, content) {
|
|
1296
|
+
if (!row || !timelineRowsEl) return;
|
|
1297
|
+
const detail = document.createElement('div');
|
|
1298
|
+
detail.className = 'loop-row loop-row-tool-detail';
|
|
1299
|
+
detail.style.cssText = 'padding:4px 0 6px 24px;color:#4b5563;font-size:11px;white-space:pre-wrap;word-break:break-word;background:#f3f4f6;border-radius:3px;margin-bottom:4px;';
|
|
1300
|
+
detail.textContent = content;
|
|
1301
|
+
detail.style.display = 'none';
|
|
1302
|
+
timelineRowsEl.insertBefore(detail, row.nextSibling);
|
|
1303
|
+
scrollTimelineToBottom();
|
|
1247
1304
|
}
|
|
1248
1305
|
|
|
1249
|
-
function
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1306
|
+
function findLastPendingToolRow() {
|
|
1307
|
+
if (!timelineRowsEl) return null;
|
|
1308
|
+
const rows = timelineRowsEl.querySelectorAll('.loop-row-tool');
|
|
1309
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
1310
|
+
if (rows[i].dataset.status === 'pending') return rows[i];
|
|
1253
1311
|
}
|
|
1312
|
+
return null;
|
|
1254
1313
|
}
|
|
1255
1314
|
|
|
1256
|
-
function
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1315
|
+
function handlePhaseEventTimeline(data) {
|
|
1316
|
+
initTimelinePanel();
|
|
1317
|
+
const tmpl = PHASE_TEXT[data.phase];
|
|
1318
|
+
if (!tmpl) return;
|
|
1319
|
+
if (timelinePanelEl.hidden) showTimelinePanel();
|
|
1320
|
+
let text = tmpl;
|
|
1321
|
+
if (data.usedCount !== undefined) text = text.replace('{usedCount}', String(data.usedCount));
|
|
1322
|
+
if (data.detail && (data.phase === 'd_done' || data.phase === 'd_skip' || data.phase === 'd_error')) {
|
|
1323
|
+
text = `${text} — ${data.detail}`;
|
|
1264
1324
|
}
|
|
1325
|
+
const status = data.phase.endsWith('_done') || data.phase === 'd_skip' ? 'done'
|
|
1326
|
+
: data.phase === 'd_error' ? 'error' : 'pending';
|
|
1327
|
+
appendPhaseRow(text, status);
|
|
1265
1328
|
}
|
|
1266
1329
|
|
|
1267
|
-
function
|
|
1268
|
-
|
|
1269
|
-
// 始终确保有工作流显示区域
|
|
1270
|
-
if (!workflowDisplayEl) {
|
|
1271
|
-
workflowDisplayEl = createWorkflowDisplay();
|
|
1272
|
-
msgContainer.appendChild(workflowDisplayEl);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
if (data.streamType === 'thinking') {
|
|
1276
|
-
showStreaming(msgContainer);
|
|
1277
|
-
updateStreamingContent(data.content || '思考中...', msgContainer);
|
|
1278
|
-
} else if (data.streamType === 'token') {
|
|
1279
|
-
showStreaming(msgContainer);
|
|
1280
|
-
const current = streamingMessageEl?.querySelector('.streaming-content')?.textContent || '';
|
|
1281
|
-
updateStreamingContent(current + data.content, msgContainer);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
function handleStatusEvent(data, container) {
|
|
1286
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1287
|
-
// 检查是否是工具调用结果
|
|
1330
|
+
function handleStatusEventTimeline(data) {
|
|
1331
|
+
initTimelinePanel();
|
|
1288
1332
|
const content = data.content || '';
|
|
1289
1333
|
const isJsonResult = content.startsWith('{') && content.includes('"success"');
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1334
|
+
if (data.tool) {
|
|
1335
|
+
// 工具事件: 折叠行 + 可选挂 output
|
|
1336
|
+
const pending = findLastPendingToolRow();
|
|
1337
|
+
if (pending && pending.textContent && pending.textContent.includes(data.tool) && content.length > 100) {
|
|
1338
|
+
// 把 output 挂到上一个同工具的 pending 行
|
|
1339
|
+
pending.dataset.status = 'done';
|
|
1340
|
+
const iconEl = pending.firstElementChild;
|
|
1341
|
+
if (iconEl) iconEl.textContent = '✓';
|
|
1342
|
+
const tg = pending.querySelector('.toggle');
|
|
1343
|
+
if (tg) tg.textContent = '▸';
|
|
1344
|
+
appendToolDetail(pending, content);
|
|
1345
|
+
} else {
|
|
1346
|
+
const status = isJsonResult ? 'done' : 'pending';
|
|
1347
|
+
const row = appendToolRow(data.tool, status);
|
|
1348
|
+
if (isJsonResult) appendToolDetail(row, content);
|
|
1349
|
+
}
|
|
1350
|
+
scrollTimelineToBottom();
|
|
1294
1351
|
} else {
|
|
1295
|
-
//
|
|
1296
|
-
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1352
|
+
// 普通 status: 当作 phase done
|
|
1353
|
+
if (timelinePanelEl.hidden) showTimelinePanel();
|
|
1354
|
+
const label = (data.tool ? `🔧 ${data.tool}: ` : '') + content;
|
|
1355
|
+
appendPhaseRow(label, 'done');
|
|
1299
1356
|
}
|
|
1300
1357
|
}
|
|
1301
1358
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
1307
|
-
// 清理之前的流式显示
|
|
1308
|
-
hideStreaming();
|
|
1309
|
-
|
|
1310
|
-
// 获取或创建工具结果容器
|
|
1311
|
-
if (!toolResultContainer) {
|
|
1312
|
-
toolResultContainer = document.createElement('div');
|
|
1313
|
-
toolResultContainer.className = 'tool-results-container';
|
|
1314
|
-
msgContainer.appendChild(toolResultContainer);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// 尝试解析并格式化 JSON
|
|
1318
|
-
let formattedResult = resultJson;
|
|
1319
|
-
try {
|
|
1320
|
-
const parsed = JSON.parse(resultJson);
|
|
1321
|
-
formattedResult = formatToolResult(parsed);
|
|
1322
|
-
} catch {}
|
|
1323
|
-
|
|
1324
|
-
// 创建折叠项
|
|
1325
|
-
const resultEl = document.createElement('div');
|
|
1326
|
-
resultEl.className = 'tool-result-item collapsed';
|
|
1327
|
-
|
|
1328
|
-
const toolDisplayName = toolName || '工具结果';
|
|
1329
|
-
const headerEl = document.createElement('div');
|
|
1330
|
-
headerEl.className = 'tool-result-header';
|
|
1331
|
-
headerEl.innerHTML = `
|
|
1332
|
-
<span class="tool-result-icon">🔧</span>
|
|
1333
|
-
<span class="tool-result-name">${toolDisplayName}</span>
|
|
1334
|
-
<span class="tool-result-toggle">▸</span>
|
|
1335
|
-
`;
|
|
1336
|
-
// 绑定事件处理器(避免内联 onclick)
|
|
1337
|
-
headerEl.addEventListener('click', () => {
|
|
1338
|
-
resultEl.classList.toggle('collapsed');
|
|
1339
|
-
resultEl.classList.toggle('expanded');
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
|
-
const contentEl = document.createElement('div');
|
|
1343
|
-
contentEl.className = 'tool-result-content';
|
|
1344
|
-
contentEl.innerHTML = `<pre>${formattedResult}</pre>`;
|
|
1345
|
-
|
|
1346
|
-
resultEl.appendChild(headerEl);
|
|
1347
|
-
resultEl.appendChild(contentEl);
|
|
1348
|
-
toolResultContainer.appendChild(resultEl);
|
|
1349
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1359
|
+
function handleStreamTokenEvent(data) {
|
|
1360
|
+
initTimelinePanel();
|
|
1361
|
+
if (timelinePanelEl.hidden) showTimelinePanel();
|
|
1362
|
+
appendOrUpdateTokenRow(data.content || '');
|
|
1350
1363
|
}
|
|
1351
1364
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
if (typeof obj === 'object') {
|
|
1361
|
-
if (Array.isArray(obj)) {
|
|
1362
|
-
if (obj.length === 0) return '[]';
|
|
1363
|
-
return obj.map(item => spaces + '- ' + formatToolResult(item, indent + 1)).join('\n');
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const keys = Object.keys(obj);
|
|
1367
|
-
if (keys.length === 0) return '{}';
|
|
1365
|
+
function handleQueueUpdateTimeline(data) {
|
|
1366
|
+
const title = document.getElementById('loop-timeline-title');
|
|
1367
|
+
if (!title) return;
|
|
1368
|
+
title.textContent = data.queueLength > 0
|
|
1369
|
+
? `▸ 运行中 · 队列 +${data.queueLength}`
|
|
1370
|
+
: '▸ 运行中';
|
|
1371
|
+
}
|
|
1368
1372
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
}
|
|
1374
|
-
return `${spaces}${key}: ${value}`;
|
|
1375
|
-
}).join('\n');
|
|
1373
|
+
function finalizeTimelineAsMessage() {
|
|
1374
|
+
const container = messagesContainers.get(currentChannelId) || messagesEl;
|
|
1375
|
+
if (currentTokenText.trim().length > 0) {
|
|
1376
|
+
addMessage(currentTokenText, 'ai', true, container, lastUsedJudgmentIds);
|
|
1376
1377
|
}
|
|
1377
|
-
|
|
1378
|
-
return String(obj);
|
|
1378
|
+
// tool 折叠行保留在 timeline 内, 用户能回看
|
|
1379
1379
|
}
|
|
1380
1380
|
|
|
1381
1381
|
// 工作流状态显示
|
|
@@ -1567,6 +1567,12 @@ function showUserCommand(command, container, opts) {
|
|
|
1567
1567
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
1568
1568
|
}
|
|
1569
1569
|
|
|
1570
|
+
// ============================================================
|
|
1571
|
+
// 状态条 + phase + queue + abort (UI 体验补丁) — 旧版, 已被 timeline panel 取代
|
|
1572
|
+
// ============================================================
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
|
|
1570
1576
|
function connect(channelId) {
|
|
1571
1577
|
const targetChannelId = channelId || currentChannelId;
|
|
1572
1578
|
if (!targetChannelId) return;
|
|
@@ -1674,28 +1680,25 @@ function connect(channelId) {
|
|
|
1674
1680
|
}
|
|
1675
1681
|
// 本地 user 已经由 sendMessage 渲染 + 去重, 这里不再显示
|
|
1676
1682
|
} else if (data.type === 'ai') {
|
|
1677
|
-
addMessage(data.content, 'ai', true, container);
|
|
1678
|
-
hideTyping();
|
|
1683
|
+
addMessage(data.content, 'ai', true, container, lastUsedJudgmentIds);
|
|
1679
1684
|
} else if (data.type === 'stream') {
|
|
1680
|
-
|
|
1681
|
-
|
|
1685
|
+
if (data.streamType === 'thinking' || data.streamType === 'token') {
|
|
1686
|
+
handleStreamTokenEvent(data);
|
|
1687
|
+
}
|
|
1682
1688
|
} else if (data.type === 'regenerating') {
|
|
1689
|
+
// 删旧的最后一条 AI 消息, 准备重新生成
|
|
1683
1690
|
const messages = container.querySelectorAll('.message-ai');
|
|
1684
1691
|
if (messages.length > 0) {
|
|
1685
1692
|
const lastAiMsg = messages[messages.length - 1];
|
|
1686
1693
|
lastAiMsg.remove();
|
|
1687
1694
|
}
|
|
1688
|
-
|
|
1695
|
+
showTimelinePanel();
|
|
1689
1696
|
} else if (data.type === 'status') {
|
|
1690
|
-
|
|
1691
|
-
setAgentStatus('executing');
|
|
1697
|
+
handleStatusEventTimeline(data);
|
|
1692
1698
|
} else if (data.type === 'done') {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (lastAi) {
|
|
1697
|
-
persistLastMessageToServer('ai', lastAi.textContent || '');
|
|
1698
|
-
}
|
|
1699
|
+
// AI 回复生成完, 从 timeline 拿出 token 文本作为正式消息
|
|
1700
|
+
finalizeTimelineAsMessage();
|
|
1701
|
+
hideTimelinePanel();
|
|
1699
1702
|
} else if (data.type === 'renamed') {
|
|
1700
1703
|
const channel = channels.find(c => c.id === data.channelId);
|
|
1701
1704
|
if (channel) {
|
|
@@ -1706,7 +1709,6 @@ function connect(channelId) {
|
|
|
1706
1709
|
}
|
|
1707
1710
|
}
|
|
1708
1711
|
} else if (data.type === 'error') {
|
|
1709
|
-
hideTyping();
|
|
1710
1712
|
addMessage('错误: ' + data.content, 'ai', true, container);
|
|
1711
1713
|
} else if (data.type === 'task_status') {
|
|
1712
1714
|
handleTaskStatusEvent(data, container);
|
|
@@ -1714,6 +1716,13 @@ function connect(channelId) {
|
|
|
1714
1716
|
handleWorkflowStepEvent(data, container);
|
|
1715
1717
|
} else if (data.type === 'workflow_loop') {
|
|
1716
1718
|
handleWorkflowLoopEvent(data, container);
|
|
1719
|
+
} else if (data.type === 'phase') {
|
|
1720
|
+
handlePhaseEventTimeline(data);
|
|
1721
|
+
} else if (data.type === 'queue_update') {
|
|
1722
|
+
handleQueueUpdateTimeline(data);
|
|
1723
|
+
} else if (data.type === 'used_judgments' && Array.isArray(data.usedIds)) {
|
|
1724
|
+
// 注入门回传: 保存 usedIds, finalizeTimelineAsMessage 时给 addMessage
|
|
1725
|
+
lastUsedJudgmentIds = data.usedIds;
|
|
1717
1726
|
}
|
|
1718
1727
|
} catch (parseErr) {
|
|
1719
1728
|
console.error('[SSE] 解析错误', parseErr);
|
|
@@ -1734,7 +1743,7 @@ async function sendMessage() {
|
|
|
1734
1743
|
if (container) container.scrollTop = container.scrollHeight;
|
|
1735
1744
|
|
|
1736
1745
|
input.value = '';
|
|
1737
|
-
|
|
1746
|
+
showTimelinePanel();
|
|
1738
1747
|
|
|
1739
1748
|
// 立即把用户消息落盘, 避免切走再切回时丢失
|
|
1740
1749
|
persistLastMessageToServer('user', text);
|
|
@@ -1756,11 +1765,11 @@ async function sendMessage() {
|
|
|
1756
1765
|
});
|
|
1757
1766
|
|
|
1758
1767
|
if (!res.ok) {
|
|
1759
|
-
|
|
1768
|
+
hideTimelinePanel();
|
|
1760
1769
|
addMessage('发送失败', 'ai');
|
|
1761
1770
|
}
|
|
1762
1771
|
} catch (err) {
|
|
1763
|
-
|
|
1772
|
+
hideTimelinePanel();
|
|
1764
1773
|
addMessage('连接错误', 'ai');
|
|
1765
1774
|
console.error('Send error', err);
|
|
1766
1775
|
}
|
|
@@ -2636,11 +2645,45 @@ function switchJudgmentTab(tab) {
|
|
|
2636
2645
|
renderJudgments(lastJudgmentsCache);
|
|
2637
2646
|
}
|
|
2638
2647
|
|
|
2648
|
+
function switchStatusFilter(status) {
|
|
2649
|
+
currentStatusFilter = status;
|
|
2650
|
+
document.querySelectorAll('.judgment-status-tab').forEach(btn => {
|
|
2651
|
+
const active = btn.dataset.status === status;
|
|
2652
|
+
btn.classList.toggle('active', active);
|
|
2653
|
+
btn.style.background = active ? '#2563eb' : '#e5e7eb';
|
|
2654
|
+
btn.style.color = active ? '#fff' : '#374151';
|
|
2655
|
+
});
|
|
2656
|
+
loadJudgments();
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* P0.5: 打开判断力 modal 并 filter 到指定 ids
|
|
2661
|
+
* - 调 openJudgmentsModal() + 等 loadJudgments() 完成
|
|
2662
|
+
* - 然后用 ids filter lastJudgmentsCache
|
|
2663
|
+
*/
|
|
2664
|
+
function openJudgmentsModalWithFilter(ids) {
|
|
2665
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
2666
|
+
if (typeof openJudgmentsModal === 'function') {
|
|
2667
|
+
openJudgmentsModal();
|
|
2668
|
+
} else if (judgmentsModal) {
|
|
2669
|
+
judgmentsModal.classList.add('active');
|
|
2670
|
+
}
|
|
2671
|
+
// 等 loadJudgments 完成 (它会 await fetch 然后 renderJudgments)
|
|
2672
|
+
setTimeout(() => {
|
|
2673
|
+
if (typeof lastJudgmentsCache === 'undefined') return;
|
|
2674
|
+
lastJudgmentsCache = (lastJudgmentsCache || []).filter((j) => ids.includes(j.id));
|
|
2675
|
+
if (typeof renderJudgments === 'function') {
|
|
2676
|
+
renderJudgments(lastJudgmentsCache);
|
|
2677
|
+
}
|
|
2678
|
+
}, 150);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2639
2681
|
function hideJudgmentsModal() {
|
|
2640
2682
|
if (judgmentsModal) judgmentsModal.classList.remove('active');
|
|
2641
2683
|
}
|
|
2642
2684
|
|
|
2643
2685
|
let currentJudgmentTab = 'channel'; // 'channel' | 'global'
|
|
2686
|
+
let currentStatusFilter = 'all'; // 'all' | 'active' | 'superseded' | 'violations'
|
|
2644
2687
|
let lastJudgmentsCache = []; // 最近一次 loadJudgments 拿到的原始列表, 切 tab / 切 channel 时复用
|
|
2645
2688
|
|
|
2646
2689
|
/**
|
|
@@ -2712,6 +2755,19 @@ function renderJudgmentItems(items, opts) {
|
|
|
2712
2755
|
const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
|
|
2713
2756
|
const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
|
|
2714
2757
|
const stakes = (j.context && j.context.stakes) ? escapeHtml(j.context.stakes) : 'medium';
|
|
2758
|
+
const isSuperseded = j.status === 'superseded';
|
|
2759
|
+
const isRejected = j.status === 'rejected';
|
|
2760
|
+
const dimmedStyle = isSuperseded || isRejected
|
|
2761
|
+
? 'opacity:0.55;background:#f3f4f6;'
|
|
2762
|
+
: '';
|
|
2763
|
+
const statusTag = isSuperseded
|
|
2764
|
+
? `<span style="display:inline-block;background:#fef3c7;color:#92400e;font-size:10px;padding:1px 6px;border-radius:3px;margin-left:6px;" title="已被新判断力演化替代">已过时</span>`
|
|
2765
|
+
: isRejected
|
|
2766
|
+
? `<span style="display:inline-block;background:#fee2e2;color:#991b1b;font-size:10px;padding:1px 6px;border-radius:3px;margin-left:6px;">已拒绝</span>`
|
|
2767
|
+
: '';
|
|
2768
|
+
const evolveNote = isSuperseded && j.supersededBy
|
|
2769
|
+
? `<div style="font-size:10px;color:#6b7280;margin-top:2px;">被新条替代 · ${escapeHtml(j.evolutionReason || 'merged')} · ${escapeHtml(j.evolvedAt || '').substring(0,10)}</div>`
|
|
2770
|
+
: '';
|
|
2715
2771
|
const bindBtn = showBindToggle
|
|
2716
2772
|
? isBound
|
|
2717
2773
|
? `<button class="judgment-toggle-btn" data-id="${escapeHtml(j.id)}" data-action="unbind" title="从当前 channel 移除" style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">× 移除</button>`
|
|
@@ -2721,17 +2777,18 @@ function renderJudgmentItems(items, opts) {
|
|
|
2721
2777
|
<div class="task-item completed judgment-row"
|
|
2722
2778
|
data-judgment-id="${escapeHtml(j.id)}"
|
|
2723
2779
|
draggable="true"
|
|
2724
|
-
style="cursor:grab
|
|
2780
|
+
style="cursor:grab;${dimmedStyle}">
|
|
2725
2781
|
<div class="task-item-header">
|
|
2726
2782
|
<label class="judgment-checkbox" style="display:flex;align-items:center;cursor:pointer;margin-right:8px;" onclick="event.stopPropagation();">
|
|
2727
2783
|
<input type="checkbox" class="judgment-select-cb" data-id="${escapeHtml(j.id)}" style="cursor:pointer;" onclick="event.stopPropagation();">
|
|
2728
2784
|
</label>
|
|
2729
2785
|
<div class="task-item-title">
|
|
2730
|
-
<span class="judgment-decision">${escapeHtml(j.decision)}</span
|
|
2786
|
+
<span class="judgment-decision">${escapeHtml(j.decision)}</span>${statusTag}
|
|
2731
2787
|
</div>
|
|
2732
2788
|
<span class="task-item-status completed">${stakes}</span>
|
|
2733
2789
|
</div>
|
|
2734
2790
|
${reason ? `<div class="task-item-desc" style="color:#555;font-size:13px;margin-top:4px;">理由: ${reason}</div>` : ''}
|
|
2791
|
+
${evolveNote}
|
|
2735
2792
|
<div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
|
|
2736
2793
|
<span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
|
|
2737
2794
|
<span style="display:flex;gap:4px;">
|
|
@@ -2748,14 +2805,53 @@ function renderJudgmentItems(items, opts) {
|
|
|
2748
2805
|
async function loadJudgments() {
|
|
2749
2806
|
if (!judgmentsList) return;
|
|
2750
2807
|
try {
|
|
2751
|
-
|
|
2808
|
+
// P3: 违规记录走单独 API
|
|
2809
|
+
if (currentStatusFilter === 'violations') {
|
|
2810
|
+
const res = await fetch('/api/judgments/violations?limit=50');
|
|
2811
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2812
|
+
const data = await res.json();
|
|
2813
|
+
renderViolations(data.items || []);
|
|
2814
|
+
judgmentsLoaded = true;
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// 类 B: 自适应扫描建议
|
|
2819
|
+
if (currentStatusFilter === 'adaptive') {
|
|
2820
|
+
const res = await fetch('/api/judgments/adaptive-suggestions');
|
|
2821
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2822
|
+
const data = await res.json();
|
|
2823
|
+
renderAdaptiveSuggestions(data);
|
|
2824
|
+
judgmentsLoaded = true;
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// 阶段 2: causal-judge 因果分析
|
|
2829
|
+
if (currentStatusFilter === 'causal') {
|
|
2830
|
+
const res = await fetch('/api/judgments/causal/correlation?topN=10');
|
|
2831
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2832
|
+
const data = await res.json();
|
|
2833
|
+
renderCausalAnalysis(data.items || []);
|
|
2834
|
+
judgmentsLoaded = true;
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const res = await fetch('/api/judgments?status=' + encodeURIComponent(currentStatusFilter));
|
|
2752
2839
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2753
2840
|
const data = await res.json();
|
|
2754
2841
|
lastJudgmentsCache = data.judgments || [];
|
|
2755
2842
|
renderJudgments(lastJudgmentsCache);
|
|
2756
2843
|
if (judgmentsBadge) {
|
|
2757
|
-
|
|
2758
|
-
|
|
2844
|
+
// 徽章永远显示 active 数量; 不跟 filter 变
|
|
2845
|
+
// 当 filter=active 时, 主 fetch 已经返回的就是 active 列表, 直接用 data.count
|
|
2846
|
+
// 当 filter=all/superseded 时, 从主列表本地数 active 条 (status ?? 'active' 兼容老数据)
|
|
2847
|
+
let activeCount;
|
|
2848
|
+
if (currentStatusFilter === 'active') {
|
|
2849
|
+
activeCount = data.count;
|
|
2850
|
+
} else {
|
|
2851
|
+
activeCount = lastJudgmentsCache.filter((j) => (j.status ?? 'active') === 'active').length;
|
|
2852
|
+
}
|
|
2853
|
+
if (activeCount > 0) {
|
|
2854
|
+
judgmentsBadge.textContent = activeCount;
|
|
2759
2855
|
judgmentsBadge.style.display = '';
|
|
2760
2856
|
} else {
|
|
2761
2857
|
judgmentsBadge.style.display = 'none';
|
|
@@ -2767,6 +2863,245 @@ async function loadJudgments() {
|
|
|
2767
2863
|
}
|
|
2768
2864
|
}
|
|
2769
2865
|
|
|
2866
|
+
/**
|
|
2867
|
+
* P3 渲染违规记录 (与 renderJudgments 同位置, 但内容不同)
|
|
2868
|
+
*/
|
|
2869
|
+
function renderViolations(items) {
|
|
2870
|
+
if (!judgmentsList) return;
|
|
2871
|
+
if (!items || items.length === 0) {
|
|
2872
|
+
judgmentsList.innerHTML = '<div class="task-empty">暂无违规记录 (AI 回复未违反注入原则).</div>';
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
judgmentsList.innerHTML = items.map((v) => {
|
|
2876
|
+
const ts = escapeHtml((v.ts || '').substring(0, 19).replace('T', ' '));
|
|
2877
|
+
const userPrev = escapeHtml(v.userInputPreview || '');
|
|
2878
|
+
const aiPrev = escapeHtml(v.aiReplyPreview || '');
|
|
2879
|
+
const principles = (v.result?.violatedPrinciples || []).map((p) =>
|
|
2880
|
+
`<div style="margin-top:3px;padding:4px 8px;background:#fef2f2;border-radius:3px;">
|
|
2881
|
+
<span style="color:#dc2626;">⚠</span> <strong>${escapeHtml(p.principle || '')}</strong>
|
|
2882
|
+
<span style="color:#991b1b;">— ${escapeHtml(p.reason || '')}</span>
|
|
2883
|
+
</div>`
|
|
2884
|
+
).join('');
|
|
2885
|
+
return `
|
|
2886
|
+
<div class="task-item" style="border-left:3px solid #dc2626;padding:8px 12px;background:#fffbfb;">
|
|
2887
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px;">${ts} · confidence=${escapeHtml(String(v.result?.confidence ?? 0))}</div>
|
|
2888
|
+
<div style="font-size:12px;color:#1f2937;"><strong>用户:</strong> ${userPrev}</div>
|
|
2889
|
+
<div style="font-size:12px;color:#1f2937;margin-top:2px;"><strong>AI:</strong> ${aiPrev}</div>
|
|
2890
|
+
<div style="margin-top:6px;">${principles}</div>
|
|
2891
|
+
</div>
|
|
2892
|
+
`;
|
|
2893
|
+
}).join('');
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* 类 B 自适应建议渲染
|
|
2898
|
+
* - rising (绿色 boost 标记): 7 天使用率高于 30 天均值的 1.5 倍
|
|
2899
|
+
* - stale (黄色 deprecate 标记): 90 天未用 + 总使用 < 3
|
|
2900
|
+
* - unused (灰色 review 标记): 30 天未用 + 总使用 < 5
|
|
2901
|
+
* 每条带 "✓ 接受" / "✗ 拒绝" 按钮, 接受会真改库, 拒绝只留痕
|
|
2902
|
+
*/
|
|
2903
|
+
function renderAdaptiveSuggestions(data) {
|
|
2904
|
+
if (!judgmentsList) return;
|
|
2905
|
+
const { judgmentsTotal, usageEntriesScanned, suggestions, scannedAt } = data;
|
|
2906
|
+
const ts = escapeHtml((scannedAt || '').substring(0, 19).replace('T', ' '));
|
|
2907
|
+
|
|
2908
|
+
if (!suggestions || suggestions.length === 0) {
|
|
2909
|
+
judgmentsList.innerHTML = `
|
|
2910
|
+
<div class="task-empty">📊 自适应扫描: 无建议
|
|
2911
|
+
<div style="margin-top:8px;font-size:11px;color:#6b7280;">扫了 ${judgmentsTotal} 条原则, ${usageEntriesScanned} 条使用记录, 都挺健康.</div>
|
|
2912
|
+
<div style="margin-top:4px;font-size:11px;color:#6b7280;">扫描于 ${ts}</div>
|
|
2913
|
+
</div>`;
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
const KIND_STYLE = {
|
|
2918
|
+
rising: { color: '#059669', bg: '#ecfdf5', label: '↑ rising', action: 'boost' },
|
|
2919
|
+
stale: { color: '#92400e', bg: '#fef3c7', label: '⏰ stale', action: 'deprecate' },
|
|
2920
|
+
unused: { color: '#6b7280', bg: '#f3f4f6', label: '👀 unused', action: 'review' },
|
|
2921
|
+
};
|
|
2922
|
+
|
|
2923
|
+
const header = `
|
|
2924
|
+
<div style="padding:8px 12px;background:#f9fafb;border-radius:4px;margin-bottom:8px;font-size:11px;color:#374151;">
|
|
2925
|
+
📊 扫描于 ${ts} · ${judgmentsTotal} 条原则 · ${usageEntriesScanned} 条使用记录 · <strong>${suggestions.length}</strong> 条建议
|
|
2926
|
+
<button class="rescan-btn" style="margin-left:8px;background:none;border:1px solid #6b7280;color:#374151;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">🔄 重新扫描</button>
|
|
2927
|
+
</div>
|
|
2928
|
+
`;
|
|
2929
|
+
|
|
2930
|
+
const rows = suggestions.map((s) => {
|
|
2931
|
+
const style = KIND_STYLE[s.kind] || KIND_STYLE.unused;
|
|
2932
|
+
const m = s.metrics || {};
|
|
2933
|
+
return `
|
|
2934
|
+
<div class="task-item" data-suggestion-key="${escapeHtml(s.key)}"
|
|
2935
|
+
style="border-left:3px solid ${style.color};padding:8px 12px;background:${style.bg};margin-bottom:6px;">
|
|
2936
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
|
2937
|
+
<span style="color:${style.color};font-weight:600;font-size:12px;">${style.label}</span>
|
|
2938
|
+
<span style="font-size:11px;color:#6b7280;">${s.action === 'boost' ? '建议加权' : s.action === 'deprecate' ? '建议废弃' : '建议审视'}</span>
|
|
2939
|
+
</div>
|
|
2940
|
+
<div style="font-size:12px;color:#1f2937;margin-bottom:4px;"><strong>${escapeHtml(s.decision)}</strong></div>
|
|
2941
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;">${escapeHtml(s.reason)}</div>
|
|
2942
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:6px;">
|
|
2943
|
+
7天 ${m.usage7d || 0} · 30天 ${m.usage30d || 0} · 共 ${m.totalUsage || 0} · 上次用 ${m.daysSinceLastUse || 0} 天前
|
|
2944
|
+
</div>
|
|
2945
|
+
<div style="display:flex;gap:6px;">
|
|
2946
|
+
<button class="adaptive-accept" data-key="${escapeHtml(s.key)}" data-id="${escapeHtml(s.judgmentId)}" data-action-kind="${escapeHtml(s.action)}"
|
|
2947
|
+
style="background:#059669;color:#fff;border:none;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">✓ 接受</button>
|
|
2948
|
+
<button class="adaptive-reject" data-key="${escapeHtml(s.key)}" data-id="${escapeHtml(s.judgmentId)}" data-action-kind="${escapeHtml(s.action)}"
|
|
2949
|
+
style="background:none;border:1px solid #d1d5db;color:#6b7280;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">✗ 拒绝</button>
|
|
2950
|
+
</div>
|
|
2951
|
+
</div>
|
|
2952
|
+
`;
|
|
2953
|
+
}).join('');
|
|
2954
|
+
|
|
2955
|
+
judgmentsList.innerHTML = header + rows;
|
|
2956
|
+
|
|
2957
|
+
// 绑定按钮
|
|
2958
|
+
const rescanBtn = judgmentsList.querySelector('.rescan-btn');
|
|
2959
|
+
if (rescanBtn) {
|
|
2960
|
+
rescanBtn.onclick = async () => {
|
|
2961
|
+
rescanBtn.disabled = true;
|
|
2962
|
+
rescanBtn.textContent = '🔄 扫描中...';
|
|
2963
|
+
try {
|
|
2964
|
+
const r = await fetch('/api/judgments/adaptive-suggestions?force=1');
|
|
2965
|
+
if (r.ok) renderAdaptiveSuggestions(await r.json());
|
|
2966
|
+
} catch (err) {
|
|
2967
|
+
console.error('[adaptive] rescan failed:', err);
|
|
2968
|
+
} finally {
|
|
2969
|
+
rescanBtn.disabled = false;
|
|
2970
|
+
rescanBtn.textContent = '🔄 重新扫描';
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
judgmentsList.querySelectorAll('.adaptive-accept').forEach((btn) => {
|
|
2975
|
+
btn.onclick = () => applyAdaptiveSuggestion(btn.dataset.key, btn.dataset.id, btn.dataset.actionKind, 'accept');
|
|
2976
|
+
});
|
|
2977
|
+
judgmentsList.querySelectorAll('.adaptive-reject').forEach((btn) => {
|
|
2978
|
+
btn.onclick = () => applyAdaptiveSuggestion(btn.dataset.key, btn.dataset.id, btn.dataset.actionKind, 'reject');
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
async function applyAdaptiveSuggestion(key, judgmentId, actionKind, decision) {
|
|
2983
|
+
const row = judgmentsList?.querySelector(`[data-suggestion-key="${key}"]`);
|
|
2984
|
+
if (row) row.style.opacity = '0.5';
|
|
2985
|
+
try {
|
|
2986
|
+
const res = await fetch('/api/judgments/adaptive-apply', {
|
|
2987
|
+
method: 'POST',
|
|
2988
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2989
|
+
body: JSON.stringify({
|
|
2990
|
+
action: decision,
|
|
2991
|
+
suggestion: {
|
|
2992
|
+
key,
|
|
2993
|
+
judgmentId,
|
|
2994
|
+
kind: actionKind,
|
|
2995
|
+
action: actionKind,
|
|
2996
|
+
decision: '',
|
|
2997
|
+
reason: '',
|
|
2998
|
+
metrics: {},
|
|
2999
|
+
scannedAt: new Date().toISOString(),
|
|
3000
|
+
},
|
|
3001
|
+
}),
|
|
3002
|
+
});
|
|
3003
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
3004
|
+
// 视觉反馈: 隐藏该行
|
|
3005
|
+
if (row) row.style.display = 'none';
|
|
3006
|
+
} catch (err) {
|
|
3007
|
+
if (row) row.style.opacity = '';
|
|
3008
|
+
console.error('[adaptive] apply failed:', err);
|
|
3009
|
+
alert('操作失败: ' + (err && err.message || 'unknown'));
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// ============================================================
|
|
3014
|
+
// 阶段 2: causal-judge 渲染
|
|
3015
|
+
// ============================================================
|
|
3016
|
+
|
|
3017
|
+
/**
|
|
3018
|
+
* 渲染关联分析 (top 5 互信息对)
|
|
3019
|
+
* - 显示每对: judgmentA ↔ judgmentB + 互信息 + co-occurrence + 因果方向
|
|
3020
|
+
* - 每条 judgment 旁加"🔬 跑 do-calculus"按钮, 异步显示 causalEffect
|
|
3021
|
+
*/
|
|
3022
|
+
function renderCausalAnalysis(items) {
|
|
3023
|
+
if (!judgmentsList) return;
|
|
3024
|
+
if (!items || items.length === 0) {
|
|
3025
|
+
judgmentsList.innerHTML = `
|
|
3026
|
+
<div class="task-empty">🔍 因果分析: 无高关联对
|
|
3027
|
+
<div style="margin-top:8px;font-size:11px;color:#6b7280;">usage 数据不足 (至少 3 条同现), 或 LLM 不可用. 多用 bolloon 一段时间后重试.</div>
|
|
3028
|
+
</div>`;
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const rows = items.map((p, idx) => `
|
|
3033
|
+
<div class="task-item" data-causal-idx="${idx}" data-judgment-a="${escapeHtml(p.judgmentA)}" data-judgment-b="${escapeHtml(p.judgmentB)}"
|
|
3034
|
+
style="border-left:3px solid #7c3aed;padding:8px 12px;background:#faf5ff;margin-bottom:6px;">
|
|
3035
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
|
3036
|
+
<span style="color:#7c3aed;font-weight:600;font-size:12px;">${escapeHtml(p.causalDirection)}</span>
|
|
3037
|
+
<span style="font-size:11px;color:#6b7280;">MI=${p.mutualInfo} · co=${p.coOccurrence}</span>
|
|
3038
|
+
</div>
|
|
3039
|
+
<div style="font-size:11px;color:#374151;margin-bottom:4px;">${escapeHtml(p.explanation || '(无 LLM 解释)')}</div>
|
|
3040
|
+
<div style="font-size:10px;color:#9ca3af;">A: ${escapeHtml(p.judgmentA)} ↔ B: ${escapeHtml(p.judgmentB)}</div>
|
|
3041
|
+
<div style="margin-top:6px;display:flex;gap:6px;">
|
|
3042
|
+
<button class="causal-intervention-a" data-jid="${escapeHtml(p.judgmentA)}"
|
|
3043
|
+
style="background:#7c3aed;color:#fff;border:none;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">🔬 do(A)</button>
|
|
3044
|
+
<button class="causal-intervention-b" data-jid="${escapeHtml(p.judgmentB)}"
|
|
3045
|
+
style="background:#7c3aed;color:#fff;border:none;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">🔬 do(B)</button>
|
|
3046
|
+
</div>
|
|
3047
|
+
<div class="causal-result" data-jid="" style="display:none;margin-top:6px;padding:6px;background:#f3e8ff;border-radius:3px;font-size:11px;"></div>
|
|
3048
|
+
</div>
|
|
3049
|
+
`).join('');
|
|
3050
|
+
|
|
3051
|
+
judgmentsList.innerHTML = `
|
|
3052
|
+
<div style="padding:8px 12px;background:#f9fafb;border-radius:4px;margin-bottom:8px;font-size:11px;color:#374151;">
|
|
3053
|
+
🔍 关联分析 (top ${items.length} 互信息对) · <span style="color:#7c3aed;">LLM 推断方向</span>
|
|
3054
|
+
<button class="causal-refresh" style="margin-left:8px;background:none;border:1px solid #7c3aed;color:#7c3aed;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">🔄 重新跑</button>
|
|
3055
|
+
</div>
|
|
3056
|
+
${rows}
|
|
3057
|
+
`;
|
|
3058
|
+
|
|
3059
|
+
// 按钮: 重新跑
|
|
3060
|
+
const refresh = judgmentsList.querySelector('.causal-refresh');
|
|
3061
|
+
if (refresh) {
|
|
3062
|
+
refresh.onclick = async () => {
|
|
3063
|
+
refresh.disabled = true;
|
|
3064
|
+
refresh.textContent = '🔄 跑中...';
|
|
3065
|
+
try {
|
|
3066
|
+
const r = await fetch('/api/judgments/causal/correlation?topN=10');
|
|
3067
|
+
if (r.ok) renderCausalAnalysis((await r.json()).items || []);
|
|
3068
|
+
} finally {
|
|
3069
|
+
refresh.disabled = false;
|
|
3070
|
+
refresh.textContent = '🔄 重新跑';
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// 按钮: 跑 do-calculus
|
|
3076
|
+
judgmentsList.querySelectorAll('.causal-intervention-a, .causal-intervention-b').forEach((btn) => {
|
|
3077
|
+
btn.onclick = async () => {
|
|
3078
|
+
const jid = btn.getAttribute('data-jid');
|
|
3079
|
+
const resultDiv = btn.closest('.task-item')?.querySelector('.causal-result');
|
|
3080
|
+
if (!resultDiv) return;
|
|
3081
|
+
resultDiv.style.display = 'block';
|
|
3082
|
+
resultDiv.textContent = '🔬 跑 do-calculus (LLM 模拟反事实)...';
|
|
3083
|
+
btn.disabled = true;
|
|
3084
|
+
try {
|
|
3085
|
+
const r = await fetch(`/api/judgments/causal/intervention?judgmentId=${encodeURIComponent(jid)}`);
|
|
3086
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
3087
|
+
const data = await r.json();
|
|
3088
|
+
const effect = data.causalEffect;
|
|
3089
|
+
const sign = effect > 0 ? '+' : '';
|
|
3090
|
+
const color = Math.abs(effect) > 0.5 ? '#dc2626' : Math.abs(effect) > 0.2 ? '#d97706' : '#059669';
|
|
3091
|
+
resultDiv.innerHTML = `
|
|
3092
|
+
<div style="color:${color};font-weight:600;">do-calculus: causalEffect = ${sign}${effect} (${data.marginalContribution})</div>
|
|
3093
|
+
<div style="color:#374151;margin-top:4px;">${escapeHtml(data.reasoning)}</div>
|
|
3094
|
+
<div style="color:#9ca3af;margin-top:4px;">confidence=${data.confidence}</div>
|
|
3095
|
+
`;
|
|
3096
|
+
} catch (err) {
|
|
3097
|
+
resultDiv.innerHTML = `<div style="color:#dc2626;">失败: ${escapeHtml(err.message)}</div>`;
|
|
3098
|
+
} finally {
|
|
3099
|
+
btn.disabled = false;
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
|
|
2770
3105
|
/** 把 judgment id 加进 / 移出当前 channel.bound_judgment_ids, 然后刷新两边 UI */
|
|
2771
3106
|
async function toggleChannelJudgment(judgmentId, action) {
|
|
2772
3107
|
if (!currentChannelId) {
|
|
@@ -2833,6 +3168,11 @@ if (judgmentsList) {
|
|
|
2833
3168
|
btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
|
|
2834
3169
|
});
|
|
2835
3170
|
|
|
3171
|
+
// status 过滤
|
|
3172
|
+
document.querySelectorAll('.judgment-status-tab').forEach(btn => {
|
|
3173
|
+
btn.addEventListener('click', () => switchStatusFilter(btn.dataset.status));
|
|
3174
|
+
});
|
|
3175
|
+
|
|
2836
3176
|
// 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
|
|
2837
3177
|
judgmentsList.addEventListener('dragstart', (e) => {
|
|
2838
3178
|
const row = e.target.closest && e.target.closest('.judgment-row');
|
|
@@ -3073,13 +3413,84 @@ if (judgmentImportFile) {
|
|
|
3073
3413
|
});
|
|
3074
3414
|
}
|
|
3075
3415
|
|
|
3076
|
-
// --- 从对话里 "
|
|
3416
|
+
// --- 从对话里 "蒸馏为判断": 事件委托到消息容器, 匹配 .save-as-judgment ---
|
|
3417
|
+
// 两条路径:
|
|
3418
|
+
// 1. 有 data-channel-id → 调 /api/judgments/distill-from-conversation (B 触发, AI 蒸馏 + 演化对齐)
|
|
3419
|
+
// 2. 没有 channelId (老按钮 / 历史数据) → fallback 到老 /api/judgments (直存)
|
|
3077
3420
|
document.addEventListener('click', async (e) => {
|
|
3078
3421
|
const btn = e.target.closest && e.target.closest('.save-as-judgment');
|
|
3079
3422
|
if (!btn) return;
|
|
3080
3423
|
e.preventDefault();
|
|
3081
3424
|
e.stopPropagation();
|
|
3425
|
+
|
|
3426
|
+
const channelId = btn.getAttribute('data-channel-id');
|
|
3082
3427
|
const decision = (btn.getAttribute('data-decision') || '').trim();
|
|
3428
|
+
|
|
3429
|
+
if (channelId) {
|
|
3430
|
+
btn.classList.add('loading');
|
|
3431
|
+
btn.disabled = true;
|
|
3432
|
+
try {
|
|
3433
|
+
const res = await fetch('/api/judgments/distill-from-conversation', {
|
|
3434
|
+
method: 'POST',
|
|
3435
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3436
|
+
body: JSON.stringify({ channelId }),
|
|
3437
|
+
});
|
|
3438
|
+
const out = await res.json();
|
|
3439
|
+
if (!res.ok) throw new Error(out.error || 'HTTP ' + res.status);
|
|
3440
|
+
|
|
3441
|
+
if (!out.triggered) {
|
|
3442
|
+
btn.classList.remove('loading');
|
|
3443
|
+
btn.disabled = false;
|
|
3444
|
+
btn.title = '蒸馏失败: ' + (out.reason || '无内容');
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
const j = out.judgment;
|
|
3449
|
+
const ev = out.evolved || { merged: 0, superseded: 0 };
|
|
3450
|
+
btn.classList.remove('loading');
|
|
3451
|
+
btn.classList.add('saved');
|
|
3452
|
+
btn.title = '已蒸馏为判断';
|
|
3453
|
+
|
|
3454
|
+
// inline 确认弹框 (在按钮下方出现, 5 秒后自动消失)
|
|
3455
|
+
showDistillConfirm(btn, {
|
|
3456
|
+
value: j.decision,
|
|
3457
|
+
evidence: (j.reasons && j.reasons[0]) || '',
|
|
3458
|
+
merged: ev.merged,
|
|
3459
|
+
superseded: ev.superseded,
|
|
3460
|
+
onEdit: async (newText) => {
|
|
3461
|
+
try {
|
|
3462
|
+
await fetch('/api/judgments/' + encodeURIComponent(j.id), {
|
|
3463
|
+
method: 'PATCH',
|
|
3464
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3465
|
+
body: JSON.stringify({ decision: newText }),
|
|
3466
|
+
});
|
|
3467
|
+
} catch (err) {
|
|
3468
|
+
console.error('[judgments] edit failed:', err);
|
|
3469
|
+
}
|
|
3470
|
+
},
|
|
3471
|
+
onReject: async () => {
|
|
3472
|
+
try {
|
|
3473
|
+
await fetch('/api/judgments/' + encodeURIComponent(j.id), {
|
|
3474
|
+
method: 'DELETE',
|
|
3475
|
+
});
|
|
3476
|
+
} catch (err) {
|
|
3477
|
+
console.error('[judgments] reject failed:', err);
|
|
3478
|
+
}
|
|
3479
|
+
},
|
|
3480
|
+
});
|
|
3481
|
+
|
|
3482
|
+
// 刷新判断力库缓存
|
|
3483
|
+
setTimeout(() => loadJudgments(), 100);
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
console.error('[judgments] distill-from-chat failed:', err);
|
|
3486
|
+
btn.classList.remove('loading');
|
|
3487
|
+
btn.disabled = false;
|
|
3488
|
+
btn.title = '蒸馏失败: ' + err.message;
|
|
3489
|
+
}
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// 老路径 fallback (没有 channelId, 直接存原文)
|
|
3083
3494
|
if (!decision) return;
|
|
3084
3495
|
try {
|
|
3085
3496
|
const res = await fetch('/api/judgments', {
|
|
@@ -3091,12 +3502,65 @@ document.addEventListener('click', async (e) => {
|
|
|
3091
3502
|
if (!out.ok) throw new Error(out.error || 'failed');
|
|
3092
3503
|
btn.classList.add('saved');
|
|
3093
3504
|
btn.title = '已存为判断';
|
|
3094
|
-
// 顶部徽章会通过 setInterval 拉新数据, 不用手动触发
|
|
3095
3505
|
} catch (err) {
|
|
3096
3506
|
console.error('[judgments] save-from-chat failed:', err);
|
|
3097
3507
|
btn.title = '保存失败: ' + err.message;
|
|
3098
3508
|
}
|
|
3099
3509
|
});
|
|
3510
|
+
|
|
3511
|
+
/**
|
|
3512
|
+
* inline 蒸馏确认弹框 — 在按钮下方出现, 显示凝练结果 + 演化结果
|
|
3513
|
+
* 5 秒后自动消失, 用户可点 "编辑" / "拒绝"
|
|
3514
|
+
*/
|
|
3515
|
+
function showDistillConfirm(btn, opts) {
|
|
3516
|
+
const { value, evidence, merged, superseded, onEdit, onReject } = opts;
|
|
3517
|
+
const old = document.getElementById('distill-confirm-popup');
|
|
3518
|
+
if (old) old.remove();
|
|
3519
|
+
|
|
3520
|
+
const popup = document.createElement('div');
|
|
3521
|
+
popup.id = 'distill-confirm-popup';
|
|
3522
|
+
popup.style.cssText = `
|
|
3523
|
+
position:absolute; z-index:1000;
|
|
3524
|
+
background:#fff; border:1px solid #d1d5db; border-radius:6px;
|
|
3525
|
+
box-shadow:0 4px 12px rgba(0,0,0,0.1);
|
|
3526
|
+
padding:10px 12px; min-width:280px; max-width:380px;
|
|
3527
|
+
font-size:13px; color:#1f2937;
|
|
3528
|
+
`;
|
|
3529
|
+
let evolveNote = '';
|
|
3530
|
+
if (merged > 0 || superseded > 0) {
|
|
3531
|
+
evolveNote = `<div style="font-size:11px;color:#059669;margin-top:6px;">✓ 演化对齐: ${merged} 条已合并${superseded > 0 ? `, ${superseded} 条已淘汰` : ''}</div>`;
|
|
3532
|
+
}
|
|
3533
|
+
popup.innerHTML = `
|
|
3534
|
+
<div style="font-weight:600;margin-bottom:4px;">已蒸馏为判断力</div>
|
|
3535
|
+
<div style="background:#f9fafb;padding:6px 8px;border-radius:4px;line-height:1.4;">${escapeHtml(value)}</div>
|
|
3536
|
+
${evidence ? `<div style="font-size:11px;color:#6b7280;margin-top:4px;">证据: ${escapeHtml(evidence)}</div>` : ''}
|
|
3537
|
+
${evolveNote}
|
|
3538
|
+
<div style="display:flex;gap:6px;margin-top:8px;">
|
|
3539
|
+
<button class="dc-edit" style="background:none;border:1px solid #d1d5db;color:#374151;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">编辑</button>
|
|
3540
|
+
<button class="dc-reject" style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:2px 10px;border-radius:3px;cursor:pointer;font-size:11px;">拒绝</button>
|
|
3541
|
+
<button class="dc-close" style="margin-left:auto;background:none;border:none;color:#6b7280;cursor:pointer;font-size:14px;">×</button>
|
|
3542
|
+
</div>
|
|
3543
|
+
`;
|
|
3544
|
+
|
|
3545
|
+
// 定位
|
|
3546
|
+
const rect = btn.getBoundingClientRect();
|
|
3547
|
+
popup.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
3548
|
+
popup.style.left = (window.scrollX + rect.left) + 'px';
|
|
3549
|
+
document.body.appendChild(popup);
|
|
3550
|
+
|
|
3551
|
+
// 绑定按钮
|
|
3552
|
+
popup.querySelector('.dc-edit').onclick = () => {
|
|
3553
|
+
const newText = prompt('编辑判断力:', value);
|
|
3554
|
+
if (newText && newText.trim() && onEdit) onEdit(newText.trim());
|
|
3555
|
+
popup.remove();
|
|
3556
|
+
};
|
|
3557
|
+
popup.querySelector('.dc-reject').onclick = () => {
|
|
3558
|
+
if (onReject) onReject();
|
|
3559
|
+
popup.remove();
|
|
3560
|
+
};
|
|
3561
|
+
popup.querySelector('.dc-close').onclick = () => popup.remove();
|
|
3562
|
+
setTimeout(() => popup.remove(), 5000);
|
|
3563
|
+
}
|
|
3100
3564
|
if (judgmentSubmitBtn) judgmentSubmitBtn.addEventListener('click', submitJudgment);
|
|
3101
3565
|
|
|
3102
3566
|
// 启动时拉一次, 让徽章显示总数 (不打开 modal 也能看到)
|