@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.
- package/dist/agents/agent-manifest-protocol.js +81 -0
- package/dist/agents/iroh-secret.js +32 -0
- package/dist/index.js +5 -8
- package/dist/network/iroh-transport.js +14 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +40 -0
- package/dist/utils/auto-update.js +11 -2
- package/dist/web/agent-delegate-server.js +123 -0
- package/dist/web/client.js +570 -10
- package/dist/web/index.html +74 -0
- package/dist/web/iroh-delegate-transport.js +125 -0
- package/dist/web/server.js +385 -7
- package/dist/web/style.css +7 -0
- package/package.json +1 -1
- package/src/agents/agent-manifest-protocol.ts +117 -0
- package/src/agents/iroh-secret.ts +32 -0
- package/src/index.ts +6 -8
- package/src/network/iroh-transport.ts +14 -0
- package/src/utils/auto-update.ts +12 -2
- package/src/web/agent-delegate-server.ts +148 -0
- package/src/web/client.js +570 -10
- package/src/web/index.html +74 -0
- package/src/web/iroh-delegate-transport.ts +139 -0
- package/src/web/server.ts +410 -7
- package/src/web/style.css +7 -0
package/dist/web/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
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({
|
|
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
|
-
|