@bolloon/bolloon-agent 0.1.22 → 0.1.24

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.
@@ -909,7 +909,17 @@ function addMessage(content, type, save = true, container) {
909
909
  };
910
910
 
911
911
  actionsDiv.appendChild(copyBtn);
912
- actionsDiv.appendChild(regenerateBtn);
912
+
913
+ // "存为判断" 按钮: 把这条消息正文作为 decision 存到判断库
914
+ const saveJudgmentBtn = document.createElement('button');
915
+ saveJudgmentBtn.className = 'action-btn save-as-judgment';
916
+ saveJudgmentBtn.title = '把这条消息存为判断';
917
+ saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800)); // 截断防超长
918
+ 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> 存为判断`;
919
+ actionsDiv.appendChild(saveJudgmentBtn);
920
+ if (type === 'ai') {
921
+ actionsDiv.appendChild(regenerateBtn);
922
+ }
913
923
  div.appendChild(actionsDiv);
914
924
  }
915
925
 
@@ -926,8 +936,8 @@ let agentStatusState = null; // 'planning' | 'executing' | null
926
936
  let agentStatusTextIdx = 0;
927
937
 
928
938
  const AGENT_STATUS_TEXTS = {
929
- planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
930
- executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
939
+ planning: ['正在计划', '正在分析', '正在思考'],
940
+ executing: ['正在执行', '正在调用工具', '正在处理'],
931
941
  };
932
942
 
933
943
  function setAgentStatus(state) {
@@ -1235,10 +1245,9 @@ function handleWorkflowLoopEvent(data, container) {
1235
1245
  const loopCount = workflowDisplayEl.querySelector('.workflow-loop-count');
1236
1246
  const streamsDiv = workflowDisplayEl.querySelector('.workflow-streams');
1237
1247
 
1238
- // 更新循环次数
1248
+ // 更新循环次数 — 用户不需要看到 "循环 N", 只看步骤内容
1239
1249
  if (data.loopCount !== undefined) {
1240
- loopCount.textContent = `循环 #${data.loopCount}`;
1241
- loopCount.style.display = 'inline';
1250
+ // 不显示循环计数, 仅在内部保留
1242
1251
  }
1243
1252
 
1244
1253
  // 显示循环信息
@@ -1248,8 +1257,7 @@ function handleWorkflowLoopEvent(data, container) {
1248
1257
  loopEl.innerHTML = `
1249
1258
  <div class="loop-header">
1250
1259
  <span class="loop-icon">🔁</span>
1251
- <span>循环 ${data.loopCount || '?'}</span>
1252
- <span class="loop-status">${data.status || ''}</span>
1260
+ <span class="loop-status">${data.status || '执行中'}</span>
1253
1261
  </div>
1254
1262
  <div class="loop-content">${data.content}</div>
1255
1263
  `;
@@ -1496,6 +1504,44 @@ input.addEventListener('keydown', (e) => {
1496
1504
  }
1497
1505
  });
1498
1506
 
1507
+ // 拖拽落点: 把判断库里的判断拖到输入框, 直接作为指令发给 AI (走"代我决定"路径).
1508
+ // 用户拖进来后输入框被预填, 点发送就把这条判断作为指令交给当前 agent.
1509
+ const inputArea = document.querySelector('.input-area');
1510
+ if (input && inputArea) {
1511
+ const onDragOver = (e) => {
1512
+ if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('application/x-bolloon-judgment')) {
1513
+ e.preventDefault();
1514
+ e.dataTransfer.dropEffect = 'copy';
1515
+ inputArea.classList.add('drop-target');
1516
+ }
1517
+ };
1518
+ const onDragLeave = (e) => {
1519
+ if (e.target === inputArea || !inputArea.contains(e.relatedTarget)) {
1520
+ inputArea.classList.remove('drop-target');
1521
+ }
1522
+ };
1523
+ const onDrop = (e) => {
1524
+ inputArea.classList.remove('drop-target');
1525
+ const raw = e.dataTransfer.getData('application/x-bolloon-judgment');
1526
+ if (!raw) return;
1527
+ e.preventDefault();
1528
+ try {
1529
+ const { id, decision } = JSON.parse(raw);
1530
+ // 预填输入框: 用户可改, 然后发出去 AI 就知道"按这条判断做"
1531
+ const prefix = input.value.trim() ? input.value.trim() + '\n' : '';
1532
+ input.value = `${prefix}按我的判断 #${id?.substring(0, 8) || ''} 执行: ${decision}`;
1533
+ input.focus();
1534
+ // 视觉提示
1535
+ input.style.transition = 'box-shadow 0.3s';
1536
+ input.style.boxShadow = '0 0 0 2px #2563eb';
1537
+ setTimeout(() => { input.style.boxShadow = ''; }, 800);
1538
+ } catch {}
1539
+ };
1540
+ inputArea.addEventListener('dragover', onDragOver);
1541
+ inputArea.addEventListener('dragleave', onDragLeave);
1542
+ inputArea.addEventListener('drop', onDrop);
1543
+ }
1544
+
1499
1545
  if (themeToggle) {
1500
1546
  themeToggle.addEventListener('click', toggleTheme);
1501
1547
  }
@@ -1932,6 +1978,389 @@ if (taskModalClose) {
1932
1978
  taskModalClose.addEventListener('click', hideTaskModal);
1933
1979
  }
1934
1980
 
1981
+ // ==================== Judgments (v1 极简) ====================
1982
+ const judgmentsModal = document.getElementById('judgments-modal');
1983
+ const judgmentsBtn = document.getElementById('judgments-btn');
1984
+ const judgmentsModalClose = document.getElementById('judgments-modal-close');
1985
+ const judgmentDecision = document.getElementById('judgment-decision');
1986
+ const judgmentReason = document.getElementById('judgment-reason');
1987
+ const judgmentDomain = document.getElementById('judgment-domain');
1988
+ const judgmentStakes = document.getElementById('judgment-stakes');
1989
+ const judgmentSubmitBtn = document.getElementById('judgment-submit-btn');
1990
+ const judgmentError = document.getElementById('judgment-error');
1991
+ const judgmentsList = document.getElementById('judgments-list');
1992
+ const judgmentsBadge = document.getElementById('judgments-badge');
1993
+
1994
+ let judgmentsLoaded = false;
1995
+
1996
+ function showJudgmentsModal() {
1997
+ if (judgmentsModal) judgmentsModal.classList.add('active');
1998
+ if (!judgmentsLoaded) loadJudgments();
1999
+ }
2000
+
2001
+ function hideJudgmentsModal() {
2002
+ if (judgmentsModal) judgmentsModal.classList.remove('active');
2003
+ }
2004
+
2005
+ function escapeHtml(s) {
2006
+ return String(s || '').replace(/[&<>"']/g, c => ({
2007
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
2008
+ }[c]));
2009
+ }
2010
+
2011
+ function renderJudgments(items) {
2012
+ if (!judgmentsList) return;
2013
+ if (!items || items.length === 0) {
2014
+ judgmentsList.innerHTML = '<div class="task-empty">还没有判断, 在上面记录第一条吧</div>';
2015
+ return;
2016
+ }
2017
+ const html = items.map(j => {
2018
+ const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
2019
+ const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
2020
+ const stakes = (j.context && j.context.stakes) ? escapeHtml(j.context.stakes) : 'medium';
2021
+ return `
2022
+ <div class="task-item completed judgment-row"
2023
+ data-judgment-id="${escapeHtml(j.id)}"
2024
+ draggable="true"
2025
+ style="cursor:grab;">
2026
+ <div class="task-item-header">
2027
+ <label class="judgment-checkbox" style="display:flex;align-items:center;cursor:pointer;margin-right:8px;" onclick="event.stopPropagation();">
2028
+ <input type="checkbox" class="judgment-select-cb" data-id="${escapeHtml(j.id)}" style="cursor:pointer;" onclick="event.stopPropagation();">
2029
+ </label>
2030
+ <div class="task-item-title">
2031
+ <span>🛡️</span>
2032
+ <span class="judgment-decision">${escapeHtml(j.decision)}</span>
2033
+ </div>
2034
+ <span class="task-item-status completed">${stakes}</span>
2035
+ </div>
2036
+ ${reason ? `<div class="task-item-desc" style="color:#555;font-size:13px;margin-top:4px;">理由: ${reason}</div>` : ''}
2037
+ <div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
2038
+ <span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
2039
+ <span style="display:flex;gap:4px;">
2040
+ <button class="judgment-edit-btn" data-id="${escapeHtml(j.id)}" title="编辑判断" style="background:none;border:1px solid #d1d5db;color:#374151;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">编辑</button>
2041
+ <button class="judgment-del-btn" data-id="${escapeHtml(j.id)}" title="删除判断" style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">删除</button>
2042
+ </span>
2043
+ </div>
2044
+ </div>
2045
+ `;
2046
+ }).join('');
2047
+ judgmentsList.innerHTML = html;
2048
+ }
2049
+
2050
+ async function loadJudgments() {
2051
+ if (!judgmentsList) return;
2052
+ try {
2053
+ const res = await fetch('/api/judgments');
2054
+ if (!res.ok) throw new Error('HTTP ' + res.status);
2055
+ const data = await res.json();
2056
+ renderJudgments(data.judgments);
2057
+ if (judgmentsBadge) {
2058
+ if (data.count > 0) {
2059
+ judgmentsBadge.textContent = data.count;
2060
+ judgmentsBadge.style.display = '';
2061
+ } else {
2062
+ judgmentsBadge.style.display = 'none';
2063
+ }
2064
+ }
2065
+ judgmentsLoaded = true;
2066
+ } catch (e) {
2067
+ if (judgmentsList) judgmentsList.innerHTML = '<div class="task-empty">加载失败: ' + escapeHtml(e.message) + '</div>';
2068
+ }
2069
+ }
2070
+
2071
+ // 列表内编辑/删除 + 拖拽 — 事件委托
2072
+ if (judgmentsList) {
2073
+ judgmentsList.addEventListener('click', async (e) => {
2074
+ const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
2075
+ const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
2076
+ if (editBtn) {
2077
+ const id = editBtn.getAttribute('data-id');
2078
+ await editJudgment(id);
2079
+ } else if (delBtn) {
2080
+ const id = delBtn.getAttribute('data-id');
2081
+ if (!confirm('确定删除这条判断?')) return;
2082
+ try {
2083
+ const res = await fetch('/api/judgments/' + encodeURIComponent(id), { method: 'DELETE' });
2084
+ const out = await res.json();
2085
+ if (!out.ok) throw new Error(out.error || 'delete failed');
2086
+ await loadJudgments();
2087
+ } catch (err) {
2088
+ showJudgmentError('删除失败: ' + err.message);
2089
+ }
2090
+ }
2091
+ });
2092
+
2093
+ // 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
2094
+ judgmentsList.addEventListener('dragstart', (e) => {
2095
+ const row = e.target.closest && e.target.closest('.judgment-row');
2096
+ if (!row) return;
2097
+ const decision = row.querySelector('.judgment-decision')?.textContent || '';
2098
+ const id = row.getAttribute('data-judgment-id') || '';
2099
+ e.dataTransfer.effectAllowed = 'copy';
2100
+ e.dataTransfer.setData('text/plain', decision);
2101
+ e.dataTransfer.setData('application/x-bolloon-judgment', JSON.stringify({ id, decision }));
2102
+ });
2103
+
2104
+ // 多选 checkbox 变化 → 更新工具栏
2105
+ judgmentsList.addEventListener('change', (e) => {
2106
+ if (e.target.classList && e.target.classList.contains('judgment-select-cb')) {
2107
+ updateBulkDeleteToolbar();
2108
+ }
2109
+ });
2110
+ }
2111
+
2112
+ // 批量选择工具栏: 全选 / 计数 / 启用删除按钮
2113
+ const judgmentSelectAll = document.getElementById('judgment-select-all');
2114
+ const judgmentSelectedCount = document.getElementById('judgment-selected-count');
2115
+ const judgmentBulkDeleteBtn = document.getElementById('judgment-bulk-delete-btn');
2116
+
2117
+ function getSelectedJudgmentIds() {
2118
+ if (!judgmentsList) return [];
2119
+ return Array.from(judgmentsList.querySelectorAll('.judgment-select-cb'))
2120
+ .filter(cb => cb.checked)
2121
+ .map(cb => cb.getAttribute('data-id'))
2122
+ .filter(Boolean);
2123
+ }
2124
+
2125
+ function updateBulkDeleteToolbar() {
2126
+ const ids = getSelectedJudgmentIds();
2127
+ if (judgmentSelectedCount) judgmentSelectedCount.textContent = `已选 ${ids.length}`;
2128
+ if (judgmentBulkDeleteBtn) {
2129
+ judgmentBulkDeleteBtn.disabled = ids.length === 0;
2130
+ judgmentBulkDeleteBtn.style.opacity = ids.length === 0 ? '0.5' : '1';
2131
+ judgmentBulkDeleteBtn.style.cursor = ids.length === 0 ? 'not-allowed' : 'pointer';
2132
+ }
2133
+ // 全选 checkbox 的 indeterminate / checked 状态同步
2134
+ if (judgmentSelectAll && judgmentsList) {
2135
+ const all = judgmentsList.querySelectorAll('.judgment-select-cb');
2136
+ const checked = Array.from(all).filter(cb => cb.checked);
2137
+ judgmentSelectAll.checked = all.length > 0 && checked.length === all.length;
2138
+ judgmentSelectAll.indeterminate = checked.length > 0 && checked.length < all.length;
2139
+ }
2140
+ }
2141
+
2142
+ if (judgmentSelectAll) {
2143
+ judgmentSelectAll.addEventListener('change', (e) => {
2144
+ if (!judgmentsList) return;
2145
+ const checked = e.target.checked;
2146
+ judgmentsList.querySelectorAll('.judgment-select-cb').forEach(cb => { cb.checked = checked; });
2147
+ updateBulkDeleteToolbar();
2148
+ });
2149
+ }
2150
+
2151
+ if (judgmentBulkDeleteBtn) {
2152
+ judgmentBulkDeleteBtn.addEventListener('click', async () => {
2153
+ const ids = getSelectedJudgmentIds();
2154
+ if (ids.length === 0) return;
2155
+ if (!confirm(`确定删除选中的 ${ids.length} 条判断? 此操作不可撤销.`)) return;
2156
+ judgmentBulkDeleteBtn.disabled = true;
2157
+ try {
2158
+ const res = await fetch('/api/judgments/batch-delete', {
2159
+ method: 'POST',
2160
+ headers: { 'Content-Type': 'application/json' },
2161
+ body: JSON.stringify({ ids }),
2162
+ });
2163
+ const out = await res.json();
2164
+ if (!out.ok) throw new Error(out.error || 'failed');
2165
+ showJudgmentOk(`✓ 批量删除 ${out.deleted} 条${out.notFound?.length ? ` (${out.notFound.length} 条未找到)` : ''}`);
2166
+ await loadJudgments();
2167
+ } catch (err) {
2168
+ showJudgmentError('批量删除失败: ' + err.message);
2169
+ } finally {
2170
+ if (judgmentBulkDeleteBtn) judgmentBulkDeleteBtn.disabled = false;
2171
+ }
2172
+ });
2173
+ }
2174
+
2175
+ async function editJudgment(id) {
2176
+ // 简单做法: 用 prompt 弹 3 个字段. 想要更好的体验就用 inline editor, 但 v1 不必.
2177
+ const all = await (await fetch('/api/judgments')).json();
2178
+ const j = (all.judgments || []).find(x => x.id === id);
2179
+ if (!j) { showJudgmentError('找不到该判断 (可能已删除)'); return; }
2180
+ const newDecision = prompt('修改判断 (decision):', j.decision);
2181
+ if (newDecision === null) return;
2182
+ const newReason = prompt('修改理由 (reason, 留空不改):', (j.reasons && j.reasons[0]) || '');
2183
+ const newStakes = prompt('修改风险 (low/medium/high/critical):', (j.context && j.context.stakes) || 'medium');
2184
+ const patch = {
2185
+ decision: newDecision.trim() || j.decision,
2186
+ reasons: newReason !== null ? [newReason.trim()].filter(Boolean) : j.reasons,
2187
+ context: newStakes ? { ...(j.context || {}), stakes: newStakes } : j.context,
2188
+ };
2189
+ try {
2190
+ const res = await fetch('/api/judgments/' + encodeURIComponent(id), {
2191
+ method: 'PATCH',
2192
+ headers: { 'Content-Type': 'application/json' },
2193
+ body: JSON.stringify(patch),
2194
+ });
2195
+ const out = await res.json();
2196
+ if (!out.ok) throw new Error(out.error || 'update failed');
2197
+ showJudgmentOk('✓ 已更新');
2198
+ await loadJudgments();
2199
+ } catch (err) {
2200
+ showJudgmentError('更新失败: ' + err.message);
2201
+ }
2202
+ }
2203
+
2204
+ async function submitJudgment() {
2205
+ if (!judgmentSubmitBtn) return;
2206
+ const decision = (judgmentDecision?.value || '').trim();
2207
+ const reason = (judgmentReason?.value || '').trim();
2208
+ if (!decision) {
2209
+ if (judgmentError) { judgmentError.textContent = '判断不能为空'; judgmentError.style.display = ''; }
2210
+ return;
2211
+ }
2212
+ judgmentSubmitBtn.disabled = true;
2213
+ if (judgmentError) judgmentError.style.display = 'none';
2214
+ try {
2215
+ const res = await fetch('/api/judgments', {
2216
+ method: 'POST',
2217
+ headers: { 'Content-Type': 'application/json' },
2218
+ body: JSON.stringify({
2219
+ decision,
2220
+ reason: reason || undefined,
2221
+ context: { domain: judgmentDomain?.value, stakes: judgmentStakes?.value },
2222
+ }),
2223
+ });
2224
+ const out = await res.json();
2225
+ if (!out.ok) throw new Error(out.error || 'unknown');
2226
+ if (judgmentDecision) judgmentDecision.value = '';
2227
+ if (judgmentReason) judgmentReason.value = '';
2228
+ await loadJudgments();
2229
+
2230
+ // AI 自动委派: fire-and-forget. 根据 domain 找匹配的远端 agent, 触发 agent_delegate 协议.
2231
+ // 失败也不影响本次记录.
2232
+ try {
2233
+ const del = await fetch('/api/judgments/auto-delegate', {
2234
+ method: 'POST',
2235
+ headers: { 'Content-Type': 'application/json' },
2236
+ body: JSON.stringify({
2237
+ judgmentId: out.judgment.id,
2238
+ capability: judgmentDomain?.value || 'general',
2239
+ instruction: `执行判断: ${out.judgment.decision}` + (reason ? ` (理由: ${reason})` : ''),
2240
+ }),
2241
+ });
2242
+ const delOut = await del.json();
2243
+ if (delOut.matched && delOut.sent) {
2244
+ showJudgmentOk(`✓ 已记录并自动委派给 ${delOut.targetAgent.name}`);
2245
+ } else if (delOut.matched) {
2246
+ showJudgmentOk(`✓ 已记录 (匹配到 ${delOut.targetAgent.name}, 但 ${delOut.reason || '未发送'})`);
2247
+ } else {
2248
+ showJudgmentOk('✓ 已记录 (本地, 无匹配远端 agent)');
2249
+ }
2250
+ } catch (e) {
2251
+ console.warn('[judgments] auto-delegate fire failed:', e);
2252
+ }
2253
+ } catch (e) {
2254
+ if (judgmentError) { judgmentError.textContent = '记录失败: ' + e.message; judgmentError.style.display = ''; }
2255
+ } finally {
2256
+ judgmentSubmitBtn.disabled = false;
2257
+ }
2258
+ }
2259
+
2260
+ if (judgmentsBtn) judgmentsBtn.addEventListener('click', showJudgmentsModal);
2261
+ if (judgmentsModalClose) judgmentsModalClose.addEventListener('click', hideJudgmentsModal);
2262
+ if (judgmentsModal) {
2263
+ judgmentsModal.addEventListener('click', (e) => {
2264
+ if (e.target === judgmentsModal) hideJudgmentsModal();
2265
+ });
2266
+ }
2267
+
2268
+ // --- 导入文件 (.json / .yaml / .md / .txt / .html) ---
2269
+ const judgmentImportBtn = document.getElementById('judgment-import-btn');
2270
+ const judgmentImportFile = document.getElementById('judgment-import-file');
2271
+
2272
+ function showJudgmentError(msg) {
2273
+ if (!judgmentError) return;
2274
+ judgmentError.textContent = msg;
2275
+ judgmentError.style.display = '';
2276
+ judgmentError.style.color = '#b91c1c';
2277
+ }
2278
+ function showJudgmentOk(msg) {
2279
+ if (!judgmentError) return;
2280
+ judgmentError.textContent = msg;
2281
+ judgmentError.style.display = '';
2282
+ judgmentError.style.color = '#15803d';
2283
+ }
2284
+
2285
+ function fileToBase64(file) {
2286
+ return new Promise((resolve, reject) => {
2287
+ const r = new FileReader();
2288
+ r.onload = () => {
2289
+ // result is "data:<mime>;base64,<payload>" — strip prefix
2290
+ const s = String(r.result || '');
2291
+ const idx = s.indexOf(',');
2292
+ resolve(idx >= 0 ? s.substring(idx + 1) : s);
2293
+ };
2294
+ r.onerror = () => reject(r.error || new Error('read failed'));
2295
+ r.readAsDataURL(file);
2296
+ });
2297
+ }
2298
+
2299
+ async function importJudgmentFile(file) {
2300
+ if (!file) return;
2301
+ if (judgmentImportBtn) judgmentImportBtn.disabled = true;
2302
+ try {
2303
+ const content = await fileToBase64(file);
2304
+ const res = await fetch('/api/judgments/import', {
2305
+ method: 'POST',
2306
+ headers: { 'Content-Type': 'application/json' },
2307
+ body: JSON.stringify({ filename: file.name, content }),
2308
+ });
2309
+ const out = await res.json();
2310
+ if (!out.ok) throw new Error(out.error || 'import failed');
2311
+ showJudgmentOk(`✓ 导入 ${out.imported} 条${out.failed ? `, ${out.failed} 条失败` : ''}`);
2312
+ await loadJudgments();
2313
+ } catch (e) {
2314
+ showJudgmentError('导入失败: ' + e.message);
2315
+ } finally {
2316
+ if (judgmentImportBtn) judgmentImportBtn.disabled = false;
2317
+ if (judgmentImportFile) judgmentImportFile.value = '';
2318
+ }
2319
+ }
2320
+
2321
+ if (judgmentImportBtn) {
2322
+ judgmentImportBtn.addEventListener('click', () => {
2323
+ if (judgmentImportFile) judgmentImportFile.click();
2324
+ });
2325
+ }
2326
+ if (judgmentImportFile) {
2327
+ judgmentImportFile.addEventListener('change', (e) => {
2328
+ const f = e.target.files && e.target.files[0];
2329
+ if (f) importJudgmentFile(f);
2330
+ });
2331
+ }
2332
+
2333
+ // --- 从对话里 "存为判断": 事件委托到消息容器, 匹配 .save-as-judgment ---
2334
+ document.addEventListener('click', async (e) => {
2335
+ const btn = e.target.closest && e.target.closest('.save-as-judgment');
2336
+ if (!btn) return;
2337
+ e.preventDefault();
2338
+ e.stopPropagation();
2339
+ const decision = (btn.getAttribute('data-decision') || '').trim();
2340
+ if (!decision) return;
2341
+ try {
2342
+ const res = await fetch('/api/judgments', {
2343
+ method: 'POST',
2344
+ headers: { 'Content-Type': 'application/json' },
2345
+ body: JSON.stringify({ decision, reason: '从对话保存' }),
2346
+ });
2347
+ const out = await res.json();
2348
+ if (!out.ok) throw new Error(out.error || 'failed');
2349
+ btn.classList.add('saved');
2350
+ btn.title = '已存为判断';
2351
+ // 顶部徽章会通过 setInterval 拉新数据, 不用手动触发
2352
+ } catch (err) {
2353
+ console.error('[judgments] save-from-chat failed:', err);
2354
+ btn.title = '保存失败: ' + err.message;
2355
+ }
2356
+ });
2357
+ if (judgmentSubmitBtn) judgmentSubmitBtn.addEventListener('click', submitJudgment);
2358
+
2359
+ // 启动时拉一次, 让徽章显示总数 (不打开 modal 也能看到)
2360
+ loadJudgments();
2361
+ // 后台定期刷新 (与 modal 打开/关闭无关, 任何时候都保持徽章新鲜)
2362
+ setInterval(loadJudgments, 10000);
2363
+
1935
2364
  if (taskModal) {
1936
2365
  taskModal.addEventListener('click', (e) => {
1937
2366
  if (e.target === taskModal) {
@@ -2361,7 +2790,6 @@ if (agentGenerateWalletBtn) {
2361
2790
  }
2362
2791
  });
2363
2792
  }
2364
- }
2365
2793
 
2366
2794
  if (catalogAddBtn) {
2367
2795
  catalogAddBtn.addEventListener('click', () => openAgentAddModal(null));
@@ -127,6 +127,13 @@
127
127
  </svg>
128
128
  <span id="task-badge" class="task-badge" style="display:none;">0</span>
129
129
  </button>
130
+ <button id="judgments-btn" class="header-action" title="我的判断">
131
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
132
+ <path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path>
133
+ <path d="M9 12l2 2 4-4"></path>
134
+ </svg>
135
+ <span id="judgments-badge" class="task-badge" style="display:none;">0</span>
136
+ </button>
130
137
  </div>
131
138
  </header>
132
139
 
@@ -190,6 +197,62 @@
190
197
  </div>
191
198
  </div>
192
199
 
200
+ <!-- Judgments Modal (v1 核心) -->
201
+ <div id="judgments-modal" class="modal">
202
+ <div class="modal-content modal-wide">
203
+ <div class="modal-header">
204
+ <h2>我的判断</h2>
205
+ <button id="judgments-modal-close" class="modal-close">&times;</button>
206
+ </div>
207
+ <div class="modal-body">
208
+ <div class="form-group">
209
+ <label>判断 (decision)</label>
210
+ <textarea id="judgment-decision" rows="2" placeholder="例: 不在时让 AI 替我做决定"></textarea>
211
+ </div>
212
+ <div class="form-group">
213
+ <label>理由 (reason, 可选)</label>
214
+ <input type="text" id="judgment-reason" placeholder="例: 信任 Bolloon 的判断存储">
215
+ </div>
216
+ <div class="form-group" style="display:flex;gap:8px;align-items:center;">
217
+ <label style="margin:0;">领域 (domain)</label>
218
+ <select id="judgment-domain">
219
+ <option value="general">general</option>
220
+ <option value="code">code</option>
221
+ <option value="architecture">architecture</option>
222
+ <option value="security">security</option>
223
+ <option value="testing">testing</option>
224
+ </select>
225
+ <label style="margin:0 0 0 16px;">风险 (stakes)</label>
226
+ <select id="judgment-stakes">
227
+ <option value="medium">medium</option>
228
+ <option value="low">low</option>
229
+ <option value="high">high</option>
230
+ <option value="critical">critical</option>
231
+ </select>
232
+ </div>
233
+ <div class="btn-group" style="gap:8px;">
234
+ <button id="judgment-submit-btn" class="btn-primary">记录</button>
235
+ <button id="judgment-import-btn" class="btn-secondary" title="从 .json / .yaml / .md / .txt / .html 文件批量导入">导入文件</button>
236
+ <input type="file" id="judgment-import-file" accept=".json,.yaml,.yml,.md,.txt,.html,.htm" style="display:none">
237
+ </div>
238
+ <div id="judgment-error" class="form-info" style="display:none;color:#b91c1c;"></div>
239
+ <h3 style="margin-top:24px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
240
+ <span>已记录的判断</span>
241
+ <span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:12px;font-weight:normal;">
242
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:#6b7280;">
243
+ <input type="checkbox" id="judgment-select-all" style="cursor:pointer;"> 全选
244
+ </label>
245
+ <span id="judgment-selected-count" style="color:#6b7280;">已选 0</span>
246
+ <button id="judgment-bulk-delete-btn" class="btn-secondary btn-sm" disabled style="background:none;border:1px solid #fca5a5;color:#b91c1c;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;opacity:0.5;">批量删除</button>
247
+ </span>
248
+ </h3>
249
+ <div id="judgments-list" class="task-list" style="max-height:420px;overflow-y:auto;">
250
+ <div class="task-empty">加载中...</div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
193
256
  <!-- Agent Add / Wallet / Auto-tool Modal -->
194
257
  <div id="agent-add-modal" class="modal">
195
258
  <div class="modal-content">