@bolloon/bolloon-agent 0.1.23 → 0.1.25

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.
@@ -623,6 +623,11 @@ async function selectChannel(channelId, targetSessionId = null) {
623
623
  currentChannelId = channelId;
624
624
  reconnectAttempts.set(channelId, 0);
625
625
 
626
+ // v3: 盾牌弹窗打开时, 切 channel 要刷列表 (tab 标题 + 已绑/未绑 分组)
627
+ if (typeof judgmentsModal !== 'undefined' && judgmentsModal && judgmentsModal.classList.contains('active')) {
628
+ if (typeof lastJudgmentsCache !== 'undefined') renderJudgments(lastJudgmentsCache);
629
+ }
630
+
626
631
  // 找到当前频道和 session
627
632
  const channel = channels.find(c => c.id === channelId);
628
633
  if (channel) {
@@ -909,7 +914,17 @@ function addMessage(content, type, save = true, container) {
909
914
  };
910
915
 
911
916
  actionsDiv.appendChild(copyBtn);
912
- actionsDiv.appendChild(regenerateBtn);
917
+
918
+ // "存为判断" 按钮: 把这条消息正文作为 decision 存到判断库
919
+ const saveJudgmentBtn = document.createElement('button');
920
+ saveJudgmentBtn.className = 'action-btn save-as-judgment';
921
+ saveJudgmentBtn.title = '把这条消息存为判断';
922
+ saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800)); // 截断防超长
923
+ 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> 存为判断`;
924
+ actionsDiv.appendChild(saveJudgmentBtn);
925
+ if (type === 'ai') {
926
+ actionsDiv.appendChild(regenerateBtn);
927
+ }
913
928
  div.appendChild(actionsDiv);
914
929
  }
915
930
 
@@ -926,8 +941,8 @@ let agentStatusState = null; // 'planning' | 'executing' | null
926
941
  let agentStatusTextIdx = 0;
927
942
 
928
943
  const AGENT_STATUS_TEXTS = {
929
- planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
930
- executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
944
+ planning: ['正在计划', '正在分析', '正在思考'],
945
+ executing: ['正在执行', '正在调用工具', '正在处理'],
931
946
  };
932
947
 
933
948
  function setAgentStatus(state) {
@@ -1235,10 +1250,9 @@ function handleWorkflowLoopEvent(data, container) {
1235
1250
  const loopCount = workflowDisplayEl.querySelector('.workflow-loop-count');
1236
1251
  const streamsDiv = workflowDisplayEl.querySelector('.workflow-streams');
1237
1252
 
1238
- // 更新循环次数
1253
+ // 更新循环次数 — 用户不需要看到 "循环 N", 只看步骤内容
1239
1254
  if (data.loopCount !== undefined) {
1240
- loopCount.textContent = `循环 #${data.loopCount}`;
1241
- loopCount.style.display = 'inline';
1255
+ // 不显示循环计数, 仅在内部保留
1242
1256
  }
1243
1257
 
1244
1258
  // 显示循环信息
@@ -1248,8 +1262,7 @@ function handleWorkflowLoopEvent(data, container) {
1248
1262
  loopEl.innerHTML = `
1249
1263
  <div class="loop-header">
1250
1264
  <span class="loop-icon">🔁</span>
1251
- <span>循环 ${data.loopCount || '?'}</span>
1252
- <span class="loop-status">${data.status || ''}</span>
1265
+ <span class="loop-status">${data.status || '执行中'}</span>
1253
1266
  </div>
1254
1267
  <div class="loop-content">${data.content}</div>
1255
1268
  `;
@@ -1496,6 +1509,44 @@ input.addEventListener('keydown', (e) => {
1496
1509
  }
1497
1510
  });
1498
1511
 
1512
+ // 拖拽落点: 把判断库里的判断拖到输入框, 直接作为指令发给 AI (走"代我决定"路径).
1513
+ // 用户拖进来后输入框被预填, 点发送就把这条判断作为指令交给当前 agent.
1514
+ const inputArea = document.querySelector('.input-area');
1515
+ if (input && inputArea) {
1516
+ const onDragOver = (e) => {
1517
+ if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('application/x-bolloon-judgment')) {
1518
+ e.preventDefault();
1519
+ e.dataTransfer.dropEffect = 'copy';
1520
+ inputArea.classList.add('drop-target');
1521
+ }
1522
+ };
1523
+ const onDragLeave = (e) => {
1524
+ if (e.target === inputArea || !inputArea.contains(e.relatedTarget)) {
1525
+ inputArea.classList.remove('drop-target');
1526
+ }
1527
+ };
1528
+ const onDrop = (e) => {
1529
+ inputArea.classList.remove('drop-target');
1530
+ const raw = e.dataTransfer.getData('application/x-bolloon-judgment');
1531
+ if (!raw) return;
1532
+ e.preventDefault();
1533
+ try {
1534
+ const { id, decision } = JSON.parse(raw);
1535
+ // 预填输入框: 用户可改, 然后发出去 AI 就知道"按这条判断做"
1536
+ const prefix = input.value.trim() ? input.value.trim() + '\n' : '';
1537
+ input.value = `${prefix}按我的判断 #${id?.substring(0, 8) || ''} 执行: ${decision}`;
1538
+ input.focus();
1539
+ // 视觉提示
1540
+ input.style.transition = 'box-shadow 0.3s';
1541
+ input.style.boxShadow = '0 0 0 2px #2563eb';
1542
+ setTimeout(() => { input.style.boxShadow = ''; }, 800);
1543
+ } catch {}
1544
+ };
1545
+ inputArea.addEventListener('dragover', onDragOver);
1546
+ inputArea.addEventListener('dragleave', onDragLeave);
1547
+ inputArea.addEventListener('drop', onDrop);
1548
+ }
1549
+
1499
1550
  if (themeToggle) {
1500
1551
  themeToggle.addEventListener('click', toggleTheme);
1501
1552
  }
@@ -1932,6 +1983,513 @@ if (taskModalClose) {
1932
1983
  taskModalClose.addEventListener('click', hideTaskModal);
1933
1984
  }
1934
1985
 
1986
+ // ==================== Judgments (v1 极简) ====================
1987
+ const judgmentsModal = document.getElementById('judgments-modal');
1988
+ const judgmentsBtn = document.getElementById('judgments-btn');
1989
+ const judgmentsModalClose = document.getElementById('judgments-modal-close');
1990
+ const judgmentDecision = document.getElementById('judgment-decision');
1991
+ const judgmentReason = document.getElementById('judgment-reason');
1992
+ const judgmentDomain = document.getElementById('judgment-domain');
1993
+ const judgmentStakes = document.getElementById('judgment-stakes');
1994
+ const judgmentSubmitBtn = document.getElementById('judgment-submit-btn');
1995
+ const judgmentError = document.getElementById('judgment-error');
1996
+ const judgmentsList = document.getElementById('judgments-list');
1997
+ const judgmentsBadge = document.getElementById('judgments-badge');
1998
+
1999
+ let judgmentsLoaded = false;
2000
+
2001
+ function showJudgmentsModal() {
2002
+ if (judgmentsModal) judgmentsModal.classList.add('active');
2003
+ if (!judgmentsLoaded) loadJudgments();
2004
+ else renderJudgments(lastJudgmentsCache); // 打开时按当前 channel / tab 重渲
2005
+ }
2006
+
2007
+ function switchJudgmentTab(tab) {
2008
+ currentJudgmentTab = tab;
2009
+ document.querySelectorAll('.judgment-tab').forEach(btn => {
2010
+ const active = btn.dataset.tab === tab;
2011
+ btn.classList.toggle('active', active);
2012
+ btn.style.borderBottomColor = active ? '#2563eb' : 'transparent';
2013
+ btn.style.color = active ? '#2563eb' : '#6b7280';
2014
+ });
2015
+ renderJudgments(lastJudgmentsCache);
2016
+ }
2017
+
2018
+ function hideJudgmentsModal() {
2019
+ if (judgmentsModal) judgmentsModal.classList.remove('active');
2020
+ }
2021
+
2022
+ function escapeHtml(s) {
2023
+ return String(s || '').replace(/[&<>"']/g, c => ({
2024
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
2025
+ }[c]));
2026
+ }
2027
+
2028
+ let currentJudgmentTab = 'channel'; // 'channel' | 'global'
2029
+ let lastJudgmentsCache = []; // 最近一次 loadJudgments 拿到的原始列表, 切 tab / 切 channel 时复用
2030
+
2031
+ /**
2032
+ * v3 重做: 渲染判断力列表 (受 tab + 当前 channel 影响)
2033
+ * tab = 'channel': 拆为"已绑定" + "未绑定"两组, 每条带 + / × 按钮
2034
+ * tab = 'global': 全部 judgment 列表, 无 + / × 按钮
2035
+ * 如果没选 channel, 'channel' tab 自动显示提示 + 全部 judgment
2036
+ */
2037
+ function renderJudgments(items) {
2038
+ if (!judgmentsList) return;
2039
+ const all = items || [];
2040
+ const titleEl = document.getElementById('judgments-list-title');
2041
+ const chNameEl = document.getElementById('judgments-tab-channel-name');
2042
+ const currentCh = currentChannelId
2043
+ ? channels.find(c => c.id === currentChannelId)
2044
+ : null;
2045
+
2046
+ if (chNameEl) {
2047
+ chNameEl.textContent = currentCh ? `(${currentCh.name})` : '(未选)';
2048
+ }
2049
+
2050
+ if (all.length === 0) {
2051
+ judgmentsList.innerHTML = '<div class="task-empty">还没有判断, 在上面记录第一条吧</div>';
2052
+ if (titleEl) titleEl.textContent = '本 channel 的判断力';
2053
+ return;
2054
+ }
2055
+
2056
+ if (currentJudgmentTab === 'global') {
2057
+ // 全局 tab: 全部 judgment, 简单列表
2058
+ if (titleEl) titleEl.textContent = `全局判断力 (${all.length} 条)`;
2059
+ judgmentsList.innerHTML = renderJudgmentItems(all, { showBindToggle: false });
2060
+ return;
2061
+ }
2062
+
2063
+ // channel tab: 必须有 channel
2064
+ if (!currentCh) {
2065
+ if (titleEl) titleEl.textContent = '本 channel 的判断力';
2066
+ judgmentsList.innerHTML = `
2067
+ <div style="padding:24px 12px;text-align:center;color:#6b7280;font-size:13px;">
2068
+ 请先在左侧选中一个 channel,<br>然后这里会显示已绑定和可加入的判断力。
2069
+ </div>
2070
+ `;
2071
+ return;
2072
+ }
2073
+
2074
+ const boundIds = new Set(
2075
+ Array.isArray(currentCh.bound_judgment_ids) ? currentCh.bound_judgment_ids : []
2076
+ );
2077
+ const bound = all.filter(j => boundIds.has(j.id));
2078
+ const unbound = all.filter(j => !boundIds.has(j.id));
2079
+
2080
+ if (titleEl) titleEl.textContent = `${currentCh.name} 的判断力 (已绑 ${bound.length} / 共 ${all.length})`;
2081
+
2082
+ let html = '';
2083
+ if (bound.length > 0) {
2084
+ html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:8px 4px 4px;">已绑定 (${bound.length})</div>`;
2085
+ html += renderJudgmentItems(bound, { showBindToggle: true, isBound: true });
2086
+ }
2087
+ if (unbound.length > 0) {
2088
+ html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:14px 4px 4px;">未绑定 (${unbound.length})</div>`;
2089
+ html += renderJudgmentItems(unbound, { showBindToggle: true, isBound: false });
2090
+ }
2091
+ judgmentsList.innerHTML = html;
2092
+ }
2093
+
2094
+ function renderJudgmentItems(items, opts) {
2095
+ const { showBindToggle, isBound } = opts || {};
2096
+ return items.map(j => {
2097
+ const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
2098
+ const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
2099
+ const stakes = (j.context && j.context.stakes) ? escapeHtml(j.context.stakes) : 'medium';
2100
+ const bindBtn = showBindToggle
2101
+ ? isBound
2102
+ ? `<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>`
2103
+ : `<button class="judgment-toggle-btn" data-id="${escapeHtml(j.id)}" data-action="bind" title="加进当前 channel" style="background:none;border:1px solid #6b7280;color:#6b7280;padding:1px 8px;border-radius:3px;cursor:pointer;font-size:11px;">+ 加入</button>`
2104
+ : '';
2105
+ return `
2106
+ <div class="task-item completed judgment-row"
2107
+ data-judgment-id="${escapeHtml(j.id)}"
2108
+ draggable="true"
2109
+ style="cursor:grab;">
2110
+ <div class="task-item-header">
2111
+ <label class="judgment-checkbox" style="display:flex;align-items:center;cursor:pointer;margin-right:8px;" onclick="event.stopPropagation();">
2112
+ <input type="checkbox" class="judgment-select-cb" data-id="${escapeHtml(j.id)}" style="cursor:pointer;" onclick="event.stopPropagation();">
2113
+ </label>
2114
+ <div class="task-item-title">
2115
+ <span>🛡️</span>
2116
+ <span class="judgment-decision">${escapeHtml(j.decision)}</span>
2117
+ </div>
2118
+ <span class="task-item-status completed">${stakes}</span>
2119
+ </div>
2120
+ ${reason ? `<div class="task-item-desc" style="color:#555;font-size:13px;margin-top:4px;">理由: ${reason}</div>` : ''}
2121
+ <div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
2122
+ <span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
2123
+ <span style="display:flex;gap:4px;">
2124
+ ${bindBtn}
2125
+ <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>
2126
+ <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>
2127
+ </span>
2128
+ </div>
2129
+ </div>
2130
+ `;
2131
+ }).join('');
2132
+ }
2133
+
2134
+ async function loadJudgments() {
2135
+ if (!judgmentsList) return;
2136
+ try {
2137
+ const res = await fetch('/api/judgments');
2138
+ if (!res.ok) throw new Error('HTTP ' + res.status);
2139
+ const data = await res.json();
2140
+ lastJudgmentsCache = data.judgments || [];
2141
+ renderJudgments(lastJudgmentsCache);
2142
+ if (judgmentsBadge) {
2143
+ if (data.count > 0) {
2144
+ judgmentsBadge.textContent = data.count;
2145
+ judgmentsBadge.style.display = '';
2146
+ } else {
2147
+ judgmentsBadge.style.display = 'none';
2148
+ }
2149
+ }
2150
+ judgmentsLoaded = true;
2151
+ } catch (e) {
2152
+ if (judgmentsList) judgmentsList.innerHTML = '<div class="task-empty">加载失败: ' + escapeHtml(e.message) + '</div>';
2153
+ }
2154
+ }
2155
+
2156
+ /** 把 judgment id 加进 / 移出当前 channel.bound_judgment_ids, 然后刷新两边 UI */
2157
+ async function toggleChannelJudgment(judgmentId, action) {
2158
+ if (!currentChannelId) {
2159
+ showJudgmentError('请先选中一个 channel');
2160
+ return;
2161
+ }
2162
+ const ch = channels.find(c => c.id === currentChannelId);
2163
+ if (!ch) return;
2164
+ const set = new Set(Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids : []);
2165
+ if (action === 'bind') set.add(judgmentId);
2166
+ else set.delete(judgmentId);
2167
+ const next = Array.from(set);
2168
+ try {
2169
+ const res = await fetch(`/channels/${currentChannelId}`, {
2170
+ method: 'PATCH',
2171
+ headers: { 'Content-Type': 'application/json' },
2172
+ body: JSON.stringify({ bound_judgment_ids: next })
2173
+ });
2174
+ if (!res.ok) {
2175
+ const err = await res.json().catch(() => ({}));
2176
+ throw new Error(err.error || `HTTP ${res.status}`);
2177
+ }
2178
+ const updated = await res.json();
2179
+ const idx = channels.findIndex(c => c.id === currentChannelId);
2180
+ if (idx >= 0) channels[idx] = updated;
2181
+ // 弹窗开着就刷新, 关着就跳过
2182
+ if (judgmentsModal && judgmentsModal.classList.contains('active')) {
2183
+ renderJudgments(lastJudgmentsCache);
2184
+ }
2185
+ } catch (err) {
2186
+ showJudgmentError('绑定失败: ' + err.message);
2187
+ }
2188
+ }
2189
+
2190
+ // 列表内编辑/删除 + 拖拽 — 事件委托
2191
+ if (judgmentsList) {
2192
+ judgmentsList.addEventListener('click', async (e) => {
2193
+ const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
2194
+ const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
2195
+ const toggleBtn = e.target.closest && e.target.closest('.judgment-toggle-btn');
2196
+ if (editBtn) {
2197
+ const id = editBtn.getAttribute('data-id');
2198
+ await editJudgment(id);
2199
+ } else if (delBtn) {
2200
+ const id = delBtn.getAttribute('data-id');
2201
+ if (!confirm('确定删除这条判断?')) return;
2202
+ try {
2203
+ const res = await fetch('/api/judgments/' + encodeURIComponent(id), { method: 'DELETE' });
2204
+ const out = await res.json();
2205
+ if (!out.ok) throw new Error(out.error || 'delete failed');
2206
+ await loadJudgments();
2207
+ } catch (err) {
2208
+ showJudgmentError('删除失败: ' + err.message);
2209
+ }
2210
+ } else if (toggleBtn) {
2211
+ const id = toggleBtn.getAttribute('data-id');
2212
+ const action = toggleBtn.getAttribute('data-action');
2213
+ await toggleChannelJudgment(id, action);
2214
+ }
2215
+ });
2216
+
2217
+ // tab 切换
2218
+ document.querySelectorAll('.judgment-tab').forEach(btn => {
2219
+ btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
2220
+ });
2221
+
2222
+ // 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
2223
+ judgmentsList.addEventListener('dragstart', (e) => {
2224
+ const row = e.target.closest && e.target.closest('.judgment-row');
2225
+ if (!row) return;
2226
+ const decision = row.querySelector('.judgment-decision')?.textContent || '';
2227
+ const id = row.getAttribute('data-judgment-id') || '';
2228
+ e.dataTransfer.effectAllowed = 'copy';
2229
+ e.dataTransfer.setData('text/plain', decision);
2230
+ e.dataTransfer.setData('application/x-bolloon-judgment', JSON.stringify({ id, decision }));
2231
+ });
2232
+
2233
+ // 多选 checkbox 变化 → 更新工具栏
2234
+ judgmentsList.addEventListener('change', (e) => {
2235
+ if (e.target.classList && e.target.classList.contains('judgment-select-cb')) {
2236
+ updateBulkDeleteToolbar();
2237
+ }
2238
+ });
2239
+ }
2240
+
2241
+ // 批量选择工具栏: 全选 / 计数 / 启用删除按钮
2242
+ const judgmentSelectAll = document.getElementById('judgment-select-all');
2243
+ const judgmentSelectedCount = document.getElementById('judgment-selected-count');
2244
+ const judgmentBulkDeleteBtn = document.getElementById('judgment-bulk-delete-btn');
2245
+
2246
+ function getSelectedJudgmentIds() {
2247
+ if (!judgmentsList) return [];
2248
+ return Array.from(judgmentsList.querySelectorAll('.judgment-select-cb'))
2249
+ .filter(cb => cb.checked)
2250
+ .map(cb => cb.getAttribute('data-id'))
2251
+ .filter(Boolean);
2252
+ }
2253
+
2254
+ function updateBulkDeleteToolbar() {
2255
+ const ids = getSelectedJudgmentIds();
2256
+ if (judgmentSelectedCount) judgmentSelectedCount.textContent = `已选 ${ids.length}`;
2257
+ if (judgmentBulkDeleteBtn) {
2258
+ judgmentBulkDeleteBtn.disabled = ids.length === 0;
2259
+ judgmentBulkDeleteBtn.style.opacity = ids.length === 0 ? '0.5' : '1';
2260
+ judgmentBulkDeleteBtn.style.cursor = ids.length === 0 ? 'not-allowed' : 'pointer';
2261
+ }
2262
+ // 全选 checkbox 的 indeterminate / checked 状态同步
2263
+ if (judgmentSelectAll && judgmentsList) {
2264
+ const all = judgmentsList.querySelectorAll('.judgment-select-cb');
2265
+ const checked = Array.from(all).filter(cb => cb.checked);
2266
+ judgmentSelectAll.checked = all.length > 0 && checked.length === all.length;
2267
+ judgmentSelectAll.indeterminate = checked.length > 0 && checked.length < all.length;
2268
+ }
2269
+ }
2270
+
2271
+ if (judgmentSelectAll) {
2272
+ judgmentSelectAll.addEventListener('change', (e) => {
2273
+ if (!judgmentsList) return;
2274
+ const checked = e.target.checked;
2275
+ judgmentsList.querySelectorAll('.judgment-select-cb').forEach(cb => { cb.checked = checked; });
2276
+ updateBulkDeleteToolbar();
2277
+ });
2278
+ }
2279
+
2280
+ if (judgmentBulkDeleteBtn) {
2281
+ judgmentBulkDeleteBtn.addEventListener('click', async () => {
2282
+ const ids = getSelectedJudgmentIds();
2283
+ if (ids.length === 0) return;
2284
+ if (!confirm(`确定删除选中的 ${ids.length} 条判断? 此操作不可撤销.`)) return;
2285
+ judgmentBulkDeleteBtn.disabled = true;
2286
+ try {
2287
+ const res = await fetch('/api/judgments/batch-delete', {
2288
+ method: 'POST',
2289
+ headers: { 'Content-Type': 'application/json' },
2290
+ body: JSON.stringify({ ids }),
2291
+ });
2292
+ const out = await res.json();
2293
+ if (!out.ok) throw new Error(out.error || 'failed');
2294
+ showJudgmentOk(`✓ 批量删除 ${out.deleted} 条${out.notFound?.length ? ` (${out.notFound.length} 条未找到)` : ''}`);
2295
+ await loadJudgments();
2296
+ } catch (err) {
2297
+ showJudgmentError('批量删除失败: ' + err.message);
2298
+ } finally {
2299
+ if (judgmentBulkDeleteBtn) judgmentBulkDeleteBtn.disabled = false;
2300
+ }
2301
+ });
2302
+ }
2303
+
2304
+ async function editJudgment(id) {
2305
+ // 简单做法: 用 prompt 弹 3 个字段. 想要更好的体验就用 inline editor, 但 v1 不必.
2306
+ const all = await (await fetch('/api/judgments')).json();
2307
+ const j = (all.judgments || []).find(x => x.id === id);
2308
+ if (!j) { showJudgmentError('找不到该判断 (可能已删除)'); return; }
2309
+ const newDecision = prompt('修改判断 (decision):', j.decision);
2310
+ if (newDecision === null) return;
2311
+ const newReason = prompt('修改理由 (reason, 留空不改):', (j.reasons && j.reasons[0]) || '');
2312
+ const newStakes = prompt('修改风险 (low/medium/high/critical):', (j.context && j.context.stakes) || 'medium');
2313
+ const patch = {
2314
+ decision: newDecision.trim() || j.decision,
2315
+ reasons: newReason !== null ? [newReason.trim()].filter(Boolean) : j.reasons,
2316
+ context: newStakes ? { ...(j.context || {}), stakes: newStakes } : j.context,
2317
+ };
2318
+ try {
2319
+ const res = await fetch('/api/judgments/' + encodeURIComponent(id), {
2320
+ method: 'PATCH',
2321
+ headers: { 'Content-Type': 'application/json' },
2322
+ body: JSON.stringify(patch),
2323
+ });
2324
+ const out = await res.json();
2325
+ if (!out.ok) throw new Error(out.error || 'update failed');
2326
+ showJudgmentOk('✓ 已更新');
2327
+ await loadJudgments();
2328
+ } catch (err) {
2329
+ showJudgmentError('更新失败: ' + err.message);
2330
+ }
2331
+ }
2332
+
2333
+ async function submitJudgment() {
2334
+ if (!judgmentSubmitBtn) return;
2335
+ const decision = (judgmentDecision?.value || '').trim();
2336
+ const reason = (judgmentReason?.value || '').trim();
2337
+ if (!decision) {
2338
+ if (judgmentError) { judgmentError.textContent = '判断不能为空'; judgmentError.style.display = ''; }
2339
+ return;
2340
+ }
2341
+ judgmentSubmitBtn.disabled = true;
2342
+ if (judgmentError) judgmentError.style.display = 'none';
2343
+ try {
2344
+ const res = await fetch('/api/judgments', {
2345
+ method: 'POST',
2346
+ headers: { 'Content-Type': 'application/json' },
2347
+ body: JSON.stringify({
2348
+ decision,
2349
+ reason: reason || undefined,
2350
+ context: { domain: judgmentDomain?.value, stakes: judgmentStakes?.value },
2351
+ }),
2352
+ });
2353
+ const out = await res.json();
2354
+ if (!out.ok) throw new Error(out.error || 'unknown');
2355
+ if (judgmentDecision) judgmentDecision.value = '';
2356
+ if (judgmentReason) judgmentReason.value = '';
2357
+ await loadJudgments();
2358
+
2359
+ // AI 自动委派: fire-and-forget. 根据 domain 找匹配的远端 agent, 触发 agent_delegate 协议.
2360
+ // 失败也不影响本次记录.
2361
+ try {
2362
+ const del = await fetch('/api/judgments/auto-delegate', {
2363
+ method: 'POST',
2364
+ headers: { 'Content-Type': 'application/json' },
2365
+ body: JSON.stringify({
2366
+ judgmentId: out.judgment.id,
2367
+ capability: judgmentDomain?.value || 'general',
2368
+ instruction: `执行判断: ${out.judgment.decision}` + (reason ? ` (理由: ${reason})` : ''),
2369
+ }),
2370
+ });
2371
+ const delOut = await del.json();
2372
+ if (delOut.matched && delOut.sent) {
2373
+ showJudgmentOk(`✓ 已记录并自动委派给 ${delOut.targetAgent.name}`);
2374
+ } else if (delOut.matched) {
2375
+ showJudgmentOk(`✓ 已记录 (匹配到 ${delOut.targetAgent.name}, 但 ${delOut.reason || '未发送'})`);
2376
+ } else {
2377
+ showJudgmentOk('✓ 已记录 (本地, 无匹配远端 agent)');
2378
+ }
2379
+ } catch (e) {
2380
+ console.warn('[judgments] auto-delegate fire failed:', e);
2381
+ }
2382
+ } catch (e) {
2383
+ if (judgmentError) { judgmentError.textContent = '记录失败: ' + e.message; judgmentError.style.display = ''; }
2384
+ } finally {
2385
+ judgmentSubmitBtn.disabled = false;
2386
+ }
2387
+ }
2388
+
2389
+ if (judgmentsBtn) judgmentsBtn.addEventListener('click', showJudgmentsModal);
2390
+ if (judgmentsModalClose) judgmentsModalClose.addEventListener('click', hideJudgmentsModal);
2391
+ if (judgmentsModal) {
2392
+ judgmentsModal.addEventListener('click', (e) => {
2393
+ if (e.target === judgmentsModal) hideJudgmentsModal();
2394
+ });
2395
+ }
2396
+
2397
+ // --- 导入文件 (.json / .yaml / .md / .txt / .html) ---
2398
+ const judgmentImportBtn = document.getElementById('judgment-import-btn');
2399
+ const judgmentImportFile = document.getElementById('judgment-import-file');
2400
+
2401
+ function showJudgmentError(msg) {
2402
+ if (!judgmentError) return;
2403
+ judgmentError.textContent = msg;
2404
+ judgmentError.style.display = '';
2405
+ judgmentError.style.color = '#b91c1c';
2406
+ }
2407
+ function showJudgmentOk(msg) {
2408
+ if (!judgmentError) return;
2409
+ judgmentError.textContent = msg;
2410
+ judgmentError.style.display = '';
2411
+ judgmentError.style.color = '#15803d';
2412
+ }
2413
+
2414
+ function fileToBase64(file) {
2415
+ return new Promise((resolve, reject) => {
2416
+ const r = new FileReader();
2417
+ r.onload = () => {
2418
+ // result is "data:<mime>;base64,<payload>" — strip prefix
2419
+ const s = String(r.result || '');
2420
+ const idx = s.indexOf(',');
2421
+ resolve(idx >= 0 ? s.substring(idx + 1) : s);
2422
+ };
2423
+ r.onerror = () => reject(r.error || new Error('read failed'));
2424
+ r.readAsDataURL(file);
2425
+ });
2426
+ }
2427
+
2428
+ async function importJudgmentFile(file) {
2429
+ if (!file) return;
2430
+ if (judgmentImportBtn) judgmentImportBtn.disabled = true;
2431
+ try {
2432
+ const content = await fileToBase64(file);
2433
+ const res = await fetch('/api/judgments/import', {
2434
+ method: 'POST',
2435
+ headers: { 'Content-Type': 'application/json' },
2436
+ body: JSON.stringify({ filename: file.name, content }),
2437
+ });
2438
+ const out = await res.json();
2439
+ if (!out.ok) throw new Error(out.error || 'import failed');
2440
+ showJudgmentOk(`✓ 导入 ${out.imported} 条${out.failed ? `, ${out.failed} 条失败` : ''}`);
2441
+ await loadJudgments();
2442
+ } catch (e) {
2443
+ showJudgmentError('导入失败: ' + e.message);
2444
+ } finally {
2445
+ if (judgmentImportBtn) judgmentImportBtn.disabled = false;
2446
+ if (judgmentImportFile) judgmentImportFile.value = '';
2447
+ }
2448
+ }
2449
+
2450
+ if (judgmentImportBtn) {
2451
+ judgmentImportBtn.addEventListener('click', () => {
2452
+ if (judgmentImportFile) judgmentImportFile.click();
2453
+ });
2454
+ }
2455
+ if (judgmentImportFile) {
2456
+ judgmentImportFile.addEventListener('change', (e) => {
2457
+ const f = e.target.files && e.target.files[0];
2458
+ if (f) importJudgmentFile(f);
2459
+ });
2460
+ }
2461
+
2462
+ // --- 从对话里 "存为判断": 事件委托到消息容器, 匹配 .save-as-judgment ---
2463
+ document.addEventListener('click', async (e) => {
2464
+ const btn = e.target.closest && e.target.closest('.save-as-judgment');
2465
+ if (!btn) return;
2466
+ e.preventDefault();
2467
+ e.stopPropagation();
2468
+ const decision = (btn.getAttribute('data-decision') || '').trim();
2469
+ if (!decision) return;
2470
+ try {
2471
+ const res = await fetch('/api/judgments', {
2472
+ method: 'POST',
2473
+ headers: { 'Content-Type': 'application/json' },
2474
+ body: JSON.stringify({ decision, reason: '从对话保存' }),
2475
+ });
2476
+ const out = await res.json();
2477
+ if (!out.ok) throw new Error(out.error || 'failed');
2478
+ btn.classList.add('saved');
2479
+ btn.title = '已存为判断';
2480
+ // 顶部徽章会通过 setInterval 拉新数据, 不用手动触发
2481
+ } catch (err) {
2482
+ console.error('[judgments] save-from-chat failed:', err);
2483
+ btn.title = '保存失败: ' + err.message;
2484
+ }
2485
+ });
2486
+ if (judgmentSubmitBtn) judgmentSubmitBtn.addEventListener('click', submitJudgment);
2487
+
2488
+ // 启动时拉一次, 让徽章显示总数 (不打开 modal 也能看到)
2489
+ loadJudgments();
2490
+ // 后台定期刷新 (与 modal 打开/关闭无关, 任何时候都保持徽章新鲜)
2491
+ setInterval(loadJudgments, 10000);
2492
+
1935
2493
  if (taskModal) {
1936
2494
  taskModal.addEventListener('click', (e) => {
1937
2495
  if (e.target === taskModal) {
@@ -2402,7 +2960,10 @@ if (agentAddConfirmBtn) {
2402
2960
  const res = await fetch(`/channels/${channelId}`, {
2403
2961
  method: 'PATCH',
2404
2962
  headers: { 'Content-Type': 'application/json' },
2405
- body: JSON.stringify({ walletAddress: walletAddress || null, autoInvokeTools })
2963
+ body: JSON.stringify({
2964
+ walletAddress: walletAddress || null,
2965
+ autoInvokeTools
2966
+ })
2406
2967
  });
2407
2968
  if (!res.ok) throw new Error('update failed');
2408
2969
  const updated = await res.json();
@@ -2417,4 +2978,3 @@ if (agentAddConfirmBtn) {
2417
2978
  }
2418
2979
  });
2419
2980
  }
2420
-