@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.
- 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 +437 -9
- package/dist/web/index.html +63 -0
- package/dist/web/iroh-delegate-transport.js +125 -0
- package/dist/web/server.js +304 -1
- 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 +437 -9
- package/src/web/index.html +63 -0
- package/src/web/iroh-delegate-transport.ts +139 -0
- package/src/web/server.ts +312 -1
- package/src/web/style.css +7 -0
package/dist/web/client.js
CHANGED
|
@@ -909,7 +909,17 @@ function addMessage(content, type, save = true, container) {
|
|
|
909
909
|
};
|
|
910
910
|
|
|
911
911
|
actionsDiv.appendChild(copyBtn);
|
|
912
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
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));
|
package/dist/web/index.html
CHANGED
|
@@ -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">×</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">
|