@bolloon/bolloon-agent 0.1.24 → 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/web/client.js +137 -6
- package/dist/web/index.html +13 -2
- package/dist/web/server.js +81 -6
- package/package.json +1 -1
- package/src/web/client.js +137 -6
- package/src/web/index.html +13 -2
- package/src/web/server.ts +98 -6
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) {
|
|
@@ -1996,6 +2001,18 @@ let judgmentsLoaded = false;
|
|
|
1996
2001
|
function showJudgmentsModal() {
|
|
1997
2002
|
if (judgmentsModal) judgmentsModal.classList.add('active');
|
|
1998
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);
|
|
1999
2016
|
}
|
|
2000
2017
|
|
|
2001
2018
|
function hideJudgmentsModal() {
|
|
@@ -2008,16 +2025,83 @@ function escapeHtml(s) {
|
|
|
2008
2025
|
}[c]));
|
|
2009
2026
|
}
|
|
2010
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
|
+
*/
|
|
2011
2037
|
function renderJudgments(items) {
|
|
2012
2038
|
if (!judgmentsList) return;
|
|
2013
|
-
|
|
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) {
|
|
2014
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 });
|
|
2015
2060
|
return;
|
|
2016
2061
|
}
|
|
2017
|
-
|
|
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 => {
|
|
2018
2097
|
const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
|
|
2019
2098
|
const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
|
|
2020
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
|
+
: '';
|
|
2021
2105
|
return `
|
|
2022
2106
|
<div class="task-item completed judgment-row"
|
|
2023
2107
|
data-judgment-id="${escapeHtml(j.id)}"
|
|
@@ -2037,6 +2121,7 @@ function renderJudgments(items) {
|
|
|
2037
2121
|
<div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
|
|
2038
2122
|
<span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
|
|
2039
2123
|
<span style="display:flex;gap:4px;">
|
|
2124
|
+
${bindBtn}
|
|
2040
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>
|
|
2041
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>
|
|
2042
2127
|
</span>
|
|
@@ -2044,7 +2129,6 @@ function renderJudgments(items) {
|
|
|
2044
2129
|
</div>
|
|
2045
2130
|
`;
|
|
2046
2131
|
}).join('');
|
|
2047
|
-
judgmentsList.innerHTML = html;
|
|
2048
2132
|
}
|
|
2049
2133
|
|
|
2050
2134
|
async function loadJudgments() {
|
|
@@ -2053,7 +2137,8 @@ async function loadJudgments() {
|
|
|
2053
2137
|
const res = await fetch('/api/judgments');
|
|
2054
2138
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2055
2139
|
const data = await res.json();
|
|
2056
|
-
|
|
2140
|
+
lastJudgmentsCache = data.judgments || [];
|
|
2141
|
+
renderJudgments(lastJudgmentsCache);
|
|
2057
2142
|
if (judgmentsBadge) {
|
|
2058
2143
|
if (data.count > 0) {
|
|
2059
2144
|
judgmentsBadge.textContent = data.count;
|
|
@@ -2068,11 +2153,46 @@ async function loadJudgments() {
|
|
|
2068
2153
|
}
|
|
2069
2154
|
}
|
|
2070
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
|
+
|
|
2071
2190
|
// 列表内编辑/删除 + 拖拽 — 事件委托
|
|
2072
2191
|
if (judgmentsList) {
|
|
2073
2192
|
judgmentsList.addEventListener('click', async (e) => {
|
|
2074
2193
|
const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
|
|
2075
2194
|
const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
|
|
2195
|
+
const toggleBtn = e.target.closest && e.target.closest('.judgment-toggle-btn');
|
|
2076
2196
|
if (editBtn) {
|
|
2077
2197
|
const id = editBtn.getAttribute('data-id');
|
|
2078
2198
|
await editJudgment(id);
|
|
@@ -2087,9 +2207,18 @@ if (judgmentsList) {
|
|
|
2087
2207
|
} catch (err) {
|
|
2088
2208
|
showJudgmentError('删除失败: ' + err.message);
|
|
2089
2209
|
}
|
|
2210
|
+
} else if (toggleBtn) {
|
|
2211
|
+
const id = toggleBtn.getAttribute('data-id');
|
|
2212
|
+
const action = toggleBtn.getAttribute('data-action');
|
|
2213
|
+
await toggleChannelJudgment(id, action);
|
|
2090
2214
|
}
|
|
2091
2215
|
});
|
|
2092
2216
|
|
|
2217
|
+
// tab 切换
|
|
2218
|
+
document.querySelectorAll('.judgment-tab').forEach(btn => {
|
|
2219
|
+
btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2093
2222
|
// 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
|
|
2094
2223
|
judgmentsList.addEventListener('dragstart', (e) => {
|
|
2095
2224
|
const row = e.target.closest && e.target.closest('.judgment-row');
|
|
@@ -2831,7 +2960,10 @@ if (agentAddConfirmBtn) {
|
|
|
2831
2960
|
const res = await fetch(`/channels/${channelId}`, {
|
|
2832
2961
|
method: 'PATCH',
|
|
2833
2962
|
headers: { 'Content-Type': 'application/json' },
|
|
2834
|
-
body: JSON.stringify({
|
|
2963
|
+
body: JSON.stringify({
|
|
2964
|
+
walletAddress: walletAddress || null,
|
|
2965
|
+
autoInvokeTools
|
|
2966
|
+
})
|
|
2835
2967
|
});
|
|
2836
2968
|
if (!res.ok) throw new Error('update failed');
|
|
2837
2969
|
const updated = await res.json();
|
|
@@ -2846,4 +2978,3 @@ if (agentAddConfirmBtn) {
|
|
|
2846
2978
|
}
|
|
2847
2979
|
});
|
|
2848
2980
|
}
|
|
2849
|
-
|
package/dist/web/index.html
CHANGED
|
@@ -236,8 +236,19 @@
|
|
|
236
236
|
<input type="file" id="judgment-import-file" accept=".json,.yaml,.yml,.md,.txt,.html,.htm" style="display:none">
|
|
237
237
|
</div>
|
|
238
238
|
<div id="judgment-error" class="form-info" style="display:none;color:#b91c1c;"></div>
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
<!-- v3 重做: tab 切换 — 默认显示当前 channel 的 judgment 上下文, 切换后才是全部 -->
|
|
240
|
+
<div id="judgments-tabs" style="display:flex;border-bottom:1px solid #e5e7eb;margin-top:20px;">
|
|
241
|
+
<button class="judgment-tab active" data-tab="channel"
|
|
242
|
+
style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid #2563eb;color:#2563eb;font-size:13px;font-weight:600;cursor:pointer;">
|
|
243
|
+
本 channel <span id="judgments-tab-channel-name" style="font-weight:normal;color:#6b7280;"></span>
|
|
244
|
+
</button>
|
|
245
|
+
<button class="judgment-tab" data-tab="global"
|
|
246
|
+
style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid transparent;color:#6b7280;font-size:13px;font-weight:600;cursor:pointer;">
|
|
247
|
+
全局
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
<h3 style="margin-top:16px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
251
|
+
<span id="judgments-list-title">本 channel 的判断力</span>
|
|
241
252
|
<span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:12px;font-weight:normal;">
|
|
242
253
|
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:#6b7280;">
|
|
243
254
|
<input type="checkbox" id="judgment-select-all" style="cursor:pointer;"> 全选
|
package/dist/web/server.js
CHANGED
|
@@ -256,6 +256,54 @@ async function executeTask(task, channelId) {
|
|
|
256
256
|
let sseClients = new Set();
|
|
257
257
|
let channelSessions = new Map(); // key: channelId
|
|
258
258
|
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
259
|
+
/**
|
|
260
|
+
* v3 重做: 构造 channel 的两路 judgment prompt 片段
|
|
261
|
+
* 路 1: 用户在盾牌里手动绑定的 judgment (channel.bound_judgment_ids)
|
|
262
|
+
* 路 2: 全局 judgment 列表 (供 LLM 在主调用中按需挑选, 写入回复)
|
|
263
|
+
* 返回 "" 表示完全没数据; 否则返回完整 "[系统上下文] ..." 块 (含尾部换行)
|
|
264
|
+
* 失败非致命 — 任何异常都返回空串, 保证 LLM 调用不被阻塞
|
|
265
|
+
*/
|
|
266
|
+
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
267
|
+
try {
|
|
268
|
+
const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
269
|
+
await initializeValueStore();
|
|
270
|
+
const allJudgments = await loadAllJudgments();
|
|
271
|
+
if (allJudgments.length === 0)
|
|
272
|
+
return '';
|
|
273
|
+
const boundIds = new Set(channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []);
|
|
274
|
+
const bound = allJudgments.filter(j => j.id !== undefined && boundIds.has(j.id));
|
|
275
|
+
const others = allJudgments.filter(j => j.id !== undefined && !boundIds.has(j.id));
|
|
276
|
+
let hint = '';
|
|
277
|
+
// 路 1: 用户手动绑定的 judgment — 硬约束, 必须遵循
|
|
278
|
+
if (bound.length > 0) {
|
|
279
|
+
hint += `[系统上下文] 此 channel 用户绑定了 ${bound.length} 条判断力, 必须严格遵循:\n`;
|
|
280
|
+
for (const j of bound) {
|
|
281
|
+
const decision = (j.decision || '').toString().slice(0, 200);
|
|
282
|
+
const reasonList = Array.isArray(j.reasons) ? j.reasons : [];
|
|
283
|
+
const reasonText = reasonList.length > 0
|
|
284
|
+
? ` (理由: ${reasonList.join('; ').slice(0, 100)})`
|
|
285
|
+
: '';
|
|
286
|
+
hint += `- ${decision}${reasonText}\n`;
|
|
287
|
+
}
|
|
288
|
+
hint += '\n';
|
|
289
|
+
}
|
|
290
|
+
// 路 2: 全局 judgment 候选池 — 软参考, LLM 自己挑
|
|
291
|
+
if (others.length > 0) {
|
|
292
|
+
hint += `[系统上下文] 候选判断力 (用户未明确绑定, 你可以按相关性自主选择参考):\n`;
|
|
293
|
+
for (const j of others) {
|
|
294
|
+
const decision = (j.decision || '').toString().slice(0, 120);
|
|
295
|
+
hint += `- [id=${j.id}] ${decision}\n`;
|
|
296
|
+
}
|
|
297
|
+
hint += `\n[系统上下文] 如果你的回复参考了某条候选判断力, 请在回复中自然提及 "我参考了你的判断: <decision 简述>" 即可, 无需复述 id.\n\n`;
|
|
298
|
+
}
|
|
299
|
+
console.log(`[v3] channel ${channelIdForLog} 注入: 绑定 ${bound.length} 条, 候选 ${others.length} 条`);
|
|
300
|
+
return hint;
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.error(`[v3] 加载判断力失败 (非致命):`, err.message);
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
259
307
|
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
260
308
|
// 获取当前 channel 的 currentSessionId
|
|
261
309
|
const channels = await loadChannels();
|
|
@@ -468,6 +516,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
468
516
|
// (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
|
|
469
517
|
const boundWalletAddress = channel?.walletAddress;
|
|
470
518
|
const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
|
|
519
|
+
// 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
|
|
520
|
+
const channelForJudgment = channel;
|
|
471
521
|
try {
|
|
472
522
|
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
473
523
|
let fullResponse = '';
|
|
@@ -503,6 +553,11 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
503
553
|
else {
|
|
504
554
|
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
505
555
|
}
|
|
556
|
+
// v3: 注入 channel 绑定的判断力 (judgment_ids)
|
|
557
|
+
// 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
|
|
558
|
+
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
559
|
+
if (judgmentHint)
|
|
560
|
+
contextHint += judgmentHint;
|
|
506
561
|
if (contextHint)
|
|
507
562
|
contextHint += '\n';
|
|
508
563
|
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
@@ -661,8 +716,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
661
716
|
});
|
|
662
717
|
app.post('/channels', async (req, res) => {
|
|
663
718
|
try {
|
|
664
|
-
const { name, agentId, walletAddress, autoInvokeTools } = req.body;
|
|
665
|
-
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
|
|
719
|
+
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
720
|
+
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
|
|
666
721
|
if (!name || !agentId) {
|
|
667
722
|
return res.status(400).json({ error: 'name and agentId required' });
|
|
668
723
|
}
|
|
@@ -670,6 +725,10 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
670
725
|
const id = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
671
726
|
// 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
|
|
672
727
|
const validWallet = isValidWalletAddress(walletAddress);
|
|
728
|
+
// 过滤 bound_judgment_ids: 只保留 string
|
|
729
|
+
const safeBoundIds = Array.isArray(bound_judgment_ids)
|
|
730
|
+
? bound_judgment_ids.filter((x) => typeof x === 'string' && x.length > 0)
|
|
731
|
+
: [];
|
|
673
732
|
// 先创建频道(不阻塞等待 DID 生成)
|
|
674
733
|
const channel = {
|
|
675
734
|
id,
|
|
@@ -681,6 +740,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
681
740
|
walletAddress: validWallet || undefined,
|
|
682
741
|
walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
|
|
683
742
|
autoInvokeTools: autoInvokeTools !== false, // 默认 true
|
|
743
|
+
bound_judgment_ids: safeBoundIds,
|
|
684
744
|
sessions: [{
|
|
685
745
|
id: `sess_${Date.now()}`,
|
|
686
746
|
createdAt: new Date().toISOString(),
|
|
@@ -839,7 +899,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
839
899
|
app.patch('/channels/:channelId', async (req, res) => {
|
|
840
900
|
try {
|
|
841
901
|
const { channelId } = req.params;
|
|
842
|
-
const { name, walletAddress, autoInvokeTools } = req.body;
|
|
902
|
+
const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
843
903
|
const channels = await loadChannels();
|
|
844
904
|
const channel = channels.find(c => c.id === channelId);
|
|
845
905
|
if (!channel) {
|
|
@@ -866,6 +926,19 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
866
926
|
if (typeof autoInvokeTools === 'boolean') {
|
|
867
927
|
channel.autoInvokeTools = autoInvokeTools;
|
|
868
928
|
}
|
|
929
|
+
// bound_judgment_ids: 允许数组(替换)/null(清空)/undefined(不改)
|
|
930
|
+
if (bound_judgment_ids !== undefined) {
|
|
931
|
+
if (bound_judgment_ids === null) {
|
|
932
|
+
channel.bound_judgment_ids = [];
|
|
933
|
+
}
|
|
934
|
+
else if (Array.isArray(bound_judgment_ids)) {
|
|
935
|
+
channel.bound_judgment_ids = bound_judgment_ids.filter((x) => typeof x === 'string' && x.length > 0);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
return res.status(400).json({ error: 'bound_judgment_ids must be array or null' });
|
|
939
|
+
}
|
|
940
|
+
console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
|
|
941
|
+
}
|
|
869
942
|
channel.updatedAt = new Date().toISOString();
|
|
870
943
|
await saveChannels(channels);
|
|
871
944
|
res.json(channel);
|
|
@@ -1027,8 +1100,9 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1027
1100
|
broadcast({ type: 'error', content: event.content }, channelId);
|
|
1028
1101
|
}
|
|
1029
1102
|
};
|
|
1030
|
-
// 重新生成时只发送用户消息
|
|
1031
|
-
|
|
1103
|
+
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
1104
|
+
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
1105
|
+
fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
|
|
1032
1106
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
1033
1107
|
// 更新 session
|
|
1034
1108
|
const existingSession = await loadSession(channelId, currentSessionId);
|
|
@@ -2140,7 +2214,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2140
2214
|
try {
|
|
2141
2215
|
const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
|
|
2142
2216
|
healthMonitor = createHealthMonitor();
|
|
2143
|
-
watchdog
|
|
2217
|
+
// 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
|
|
2218
|
+
watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
|
|
2144
2219
|
console.log('[24h] Heartbeat modules loaded');
|
|
2145
2220
|
}
|
|
2146
2221
|
catch (err) {
|
package/package.json
CHANGED
package/src/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) {
|
|
@@ -1996,6 +2001,18 @@ let judgmentsLoaded = false;
|
|
|
1996
2001
|
function showJudgmentsModal() {
|
|
1997
2002
|
if (judgmentsModal) judgmentsModal.classList.add('active');
|
|
1998
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);
|
|
1999
2016
|
}
|
|
2000
2017
|
|
|
2001
2018
|
function hideJudgmentsModal() {
|
|
@@ -2008,16 +2025,83 @@ function escapeHtml(s) {
|
|
|
2008
2025
|
}[c]));
|
|
2009
2026
|
}
|
|
2010
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
|
+
*/
|
|
2011
2037
|
function renderJudgments(items) {
|
|
2012
2038
|
if (!judgmentsList) return;
|
|
2013
|
-
|
|
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) {
|
|
2014
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 });
|
|
2015
2060
|
return;
|
|
2016
2061
|
}
|
|
2017
|
-
|
|
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 => {
|
|
2018
2097
|
const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
|
|
2019
2098
|
const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
|
|
2020
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
|
+
: '';
|
|
2021
2105
|
return `
|
|
2022
2106
|
<div class="task-item completed judgment-row"
|
|
2023
2107
|
data-judgment-id="${escapeHtml(j.id)}"
|
|
@@ -2037,6 +2121,7 @@ function renderJudgments(items) {
|
|
|
2037
2121
|
<div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
|
|
2038
2122
|
<span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
|
|
2039
2123
|
<span style="display:flex;gap:4px;">
|
|
2124
|
+
${bindBtn}
|
|
2040
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>
|
|
2041
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>
|
|
2042
2127
|
</span>
|
|
@@ -2044,7 +2129,6 @@ function renderJudgments(items) {
|
|
|
2044
2129
|
</div>
|
|
2045
2130
|
`;
|
|
2046
2131
|
}).join('');
|
|
2047
|
-
judgmentsList.innerHTML = html;
|
|
2048
2132
|
}
|
|
2049
2133
|
|
|
2050
2134
|
async function loadJudgments() {
|
|
@@ -2053,7 +2137,8 @@ async function loadJudgments() {
|
|
|
2053
2137
|
const res = await fetch('/api/judgments');
|
|
2054
2138
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2055
2139
|
const data = await res.json();
|
|
2056
|
-
|
|
2140
|
+
lastJudgmentsCache = data.judgments || [];
|
|
2141
|
+
renderJudgments(lastJudgmentsCache);
|
|
2057
2142
|
if (judgmentsBadge) {
|
|
2058
2143
|
if (data.count > 0) {
|
|
2059
2144
|
judgmentsBadge.textContent = data.count;
|
|
@@ -2068,11 +2153,46 @@ async function loadJudgments() {
|
|
|
2068
2153
|
}
|
|
2069
2154
|
}
|
|
2070
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
|
+
|
|
2071
2190
|
// 列表内编辑/删除 + 拖拽 — 事件委托
|
|
2072
2191
|
if (judgmentsList) {
|
|
2073
2192
|
judgmentsList.addEventListener('click', async (e) => {
|
|
2074
2193
|
const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
|
|
2075
2194
|
const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
|
|
2195
|
+
const toggleBtn = e.target.closest && e.target.closest('.judgment-toggle-btn');
|
|
2076
2196
|
if (editBtn) {
|
|
2077
2197
|
const id = editBtn.getAttribute('data-id');
|
|
2078
2198
|
await editJudgment(id);
|
|
@@ -2087,9 +2207,18 @@ if (judgmentsList) {
|
|
|
2087
2207
|
} catch (err) {
|
|
2088
2208
|
showJudgmentError('删除失败: ' + err.message);
|
|
2089
2209
|
}
|
|
2210
|
+
} else if (toggleBtn) {
|
|
2211
|
+
const id = toggleBtn.getAttribute('data-id');
|
|
2212
|
+
const action = toggleBtn.getAttribute('data-action');
|
|
2213
|
+
await toggleChannelJudgment(id, action);
|
|
2090
2214
|
}
|
|
2091
2215
|
});
|
|
2092
2216
|
|
|
2217
|
+
// tab 切换
|
|
2218
|
+
document.querySelectorAll('.judgment-tab').forEach(btn => {
|
|
2219
|
+
btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2093
2222
|
// 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
|
|
2094
2223
|
judgmentsList.addEventListener('dragstart', (e) => {
|
|
2095
2224
|
const row = e.target.closest && e.target.closest('.judgment-row');
|
|
@@ -2831,7 +2960,10 @@ if (agentAddConfirmBtn) {
|
|
|
2831
2960
|
const res = await fetch(`/channels/${channelId}`, {
|
|
2832
2961
|
method: 'PATCH',
|
|
2833
2962
|
headers: { 'Content-Type': 'application/json' },
|
|
2834
|
-
body: JSON.stringify({
|
|
2963
|
+
body: JSON.stringify({
|
|
2964
|
+
walletAddress: walletAddress || null,
|
|
2965
|
+
autoInvokeTools
|
|
2966
|
+
})
|
|
2835
2967
|
});
|
|
2836
2968
|
if (!res.ok) throw new Error('update failed');
|
|
2837
2969
|
const updated = await res.json();
|
|
@@ -2846,4 +2978,3 @@ if (agentAddConfirmBtn) {
|
|
|
2846
2978
|
}
|
|
2847
2979
|
});
|
|
2848
2980
|
}
|
|
2849
|
-
|
package/src/web/index.html
CHANGED
|
@@ -236,8 +236,19 @@
|
|
|
236
236
|
<input type="file" id="judgment-import-file" accept=".json,.yaml,.yml,.md,.txt,.html,.htm" style="display:none">
|
|
237
237
|
</div>
|
|
238
238
|
<div id="judgment-error" class="form-info" style="display:none;color:#b91c1c;"></div>
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
<!-- v3 重做: tab 切换 — 默认显示当前 channel 的 judgment 上下文, 切换后才是全部 -->
|
|
240
|
+
<div id="judgments-tabs" style="display:flex;border-bottom:1px solid #e5e7eb;margin-top:20px;">
|
|
241
|
+
<button class="judgment-tab active" data-tab="channel"
|
|
242
|
+
style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid #2563eb;color:#2563eb;font-size:13px;font-weight:600;cursor:pointer;">
|
|
243
|
+
本 channel <span id="judgments-tab-channel-name" style="font-weight:normal;color:#6b7280;"></span>
|
|
244
|
+
</button>
|
|
245
|
+
<button class="judgment-tab" data-tab="global"
|
|
246
|
+
style="flex:1;padding:10px 12px;background:none;border:none;border-bottom:2px solid transparent;color:#6b7280;font-size:13px;font-weight:600;cursor:pointer;">
|
|
247
|
+
全局
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
<h3 style="margin-top:16px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
251
|
+
<span id="judgments-list-title">本 channel 的判断力</span>
|
|
241
252
|
<span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:12px;font-weight:normal;">
|
|
242
253
|
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:#6b7280;">
|
|
243
254
|
<input type="checkbox" id="judgment-select-all" style="cursor:pointer;"> 全选
|
package/src/web/server.ts
CHANGED
|
@@ -72,6 +72,8 @@ interface Channel {
|
|
|
72
72
|
updatedAt: string;
|
|
73
73
|
currentSessionId?: string;
|
|
74
74
|
sessions?: SessionSummary[];
|
|
75
|
+
/** 用户在盾牌里手动绑定的判断力 (LLM 跑 channel 时会注入). 默认 []. */
|
|
76
|
+
bound_judgment_ids?: string[];
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
interface SessionSummary {
|
|
@@ -383,6 +385,67 @@ let sseClients: Set<SSEClient> = new Set();
|
|
|
383
385
|
let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
|
|
384
386
|
let sessionMessages: Map<string, any[]> = new Map(); // key: channelId + sessionId
|
|
385
387
|
|
|
388
|
+
/**
|
|
389
|
+
* v3 重做: 构造 channel 的两路 judgment prompt 片段
|
|
390
|
+
* 路 1: 用户在盾牌里手动绑定的 judgment (channel.bound_judgment_ids)
|
|
391
|
+
* 路 2: 全局 judgment 列表 (供 LLM 在主调用中按需挑选, 写入回复)
|
|
392
|
+
* 返回 "" 表示完全没数据; 否则返回完整 "[系统上下文] ..." 块 (含尾部换行)
|
|
393
|
+
* 失败非致命 — 任何异常都返回空串, 保证 LLM 调用不被阻塞
|
|
394
|
+
*/
|
|
395
|
+
async function buildJudgmentHint(
|
|
396
|
+
channel: Channel | undefined | null,
|
|
397
|
+
channelIdForLog: string
|
|
398
|
+
): Promise<string> {
|
|
399
|
+
try {
|
|
400
|
+
const { loadAllJudgments, initializeValueStore } = await import(
|
|
401
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
402
|
+
);
|
|
403
|
+
await initializeValueStore();
|
|
404
|
+
const allJudgments = await loadAllJudgments();
|
|
405
|
+
if (allJudgments.length === 0) return '';
|
|
406
|
+
|
|
407
|
+
const boundIds = new Set(
|
|
408
|
+
channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []
|
|
409
|
+
);
|
|
410
|
+
const bound = allJudgments.filter(j => j.id !== undefined && boundIds.has(j.id));
|
|
411
|
+
const others = allJudgments.filter(j => j.id !== undefined && !boundIds.has(j.id));
|
|
412
|
+
|
|
413
|
+
let hint = '';
|
|
414
|
+
|
|
415
|
+
// 路 1: 用户手动绑定的 judgment — 硬约束, 必须遵循
|
|
416
|
+
if (bound.length > 0) {
|
|
417
|
+
hint += `[系统上下文] 此 channel 用户绑定了 ${bound.length} 条判断力, 必须严格遵循:\n`;
|
|
418
|
+
for (const j of bound) {
|
|
419
|
+
const decision = (j.decision || '').toString().slice(0, 200);
|
|
420
|
+
const reasonList = Array.isArray(j.reasons) ? j.reasons : [];
|
|
421
|
+
const reasonText = reasonList.length > 0
|
|
422
|
+
? ` (理由: ${reasonList.join('; ').slice(0, 100)})`
|
|
423
|
+
: '';
|
|
424
|
+
hint += `- ${decision}${reasonText}\n`;
|
|
425
|
+
}
|
|
426
|
+
hint += '\n';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 路 2: 全局 judgment 候选池 — 软参考, LLM 自己挑
|
|
430
|
+
if (others.length > 0) {
|
|
431
|
+
hint += `[系统上下文] 候选判断力 (用户未明确绑定, 你可以按相关性自主选择参考):\n`;
|
|
432
|
+
for (const j of others) {
|
|
433
|
+
const decision = (j.decision || '').toString().slice(0, 120);
|
|
434
|
+
hint += `- [id=${j.id}] ${decision}\n`;
|
|
435
|
+
}
|
|
436
|
+
hint += `\n[系统上下文] 如果你的回复参考了某条候选判断力, 请在回复中自然提及 "我参考了你的判断: <decision 简述>" 即可, 无需复述 id.\n\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log(
|
|
440
|
+
`[v3] channel ${channelIdForLog} 注入: 绑定 ${bound.length} 条, 候选 ${others.length} 条`
|
|
441
|
+
);
|
|
442
|
+
return hint;
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error(`[v3] 加载判断力失败 (非致命):`, (err as Error).message);
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
386
449
|
async function getAgentForChannel(
|
|
387
450
|
channelId: string,
|
|
388
451
|
channelDid?: string,
|
|
@@ -640,6 +703,8 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
640
703
|
// (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
|
|
641
704
|
const boundWalletAddress = channel?.walletAddress;
|
|
642
705
|
const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
|
|
706
|
+
// 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
|
|
707
|
+
const channelForJudgment = channel;
|
|
643
708
|
|
|
644
709
|
try {
|
|
645
710
|
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
@@ -675,6 +740,12 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
675
740
|
} else {
|
|
676
741
|
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
677
742
|
}
|
|
743
|
+
|
|
744
|
+
// v3: 注入 channel 绑定的判断力 (judgment_ids)
|
|
745
|
+
// 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
|
|
746
|
+
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
747
|
+
if (judgmentHint) contextHint += judgmentHint;
|
|
748
|
+
|
|
678
749
|
if (contextHint) contextHint += '\n';
|
|
679
750
|
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
680
751
|
|
|
@@ -835,8 +906,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
835
906
|
|
|
836
907
|
app.post('/channels', async (req, res) => {
|
|
837
908
|
try {
|
|
838
|
-
const { name, agentId, walletAddress, autoInvokeTools } = req.body;
|
|
839
|
-
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
|
|
909
|
+
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
910
|
+
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
|
|
840
911
|
if (!name || !agentId) {
|
|
841
912
|
return res.status(400).json({ error: 'name and agentId required' });
|
|
842
913
|
}
|
|
@@ -846,6 +917,11 @@ app.get('/channels', async (_req, res) => {
|
|
|
846
917
|
// 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
|
|
847
918
|
const validWallet = isValidWalletAddress(walletAddress);
|
|
848
919
|
|
|
920
|
+
// 过滤 bound_judgment_ids: 只保留 string
|
|
921
|
+
const safeBoundIds = Array.isArray(bound_judgment_ids)
|
|
922
|
+
? bound_judgment_ids.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0)
|
|
923
|
+
: [];
|
|
924
|
+
|
|
849
925
|
// 先创建频道(不阻塞等待 DID 生成)
|
|
850
926
|
const channel: Channel = {
|
|
851
927
|
id,
|
|
@@ -857,6 +933,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
857
933
|
walletAddress: validWallet || undefined,
|
|
858
934
|
walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
|
|
859
935
|
autoInvokeTools: autoInvokeTools !== false, // 默认 true
|
|
936
|
+
bound_judgment_ids: safeBoundIds,
|
|
860
937
|
sessions: [{
|
|
861
938
|
id: `sess_${Date.now()}`,
|
|
862
939
|
createdAt: new Date().toISOString(),
|
|
@@ -1035,7 +1112,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
1035
1112
|
app.patch('/channels/:channelId', async (req, res) => {
|
|
1036
1113
|
try {
|
|
1037
1114
|
const { channelId } = req.params;
|
|
1038
|
-
const { name, walletAddress, autoInvokeTools } = req.body;
|
|
1115
|
+
const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
1039
1116
|
const channels = await loadChannels();
|
|
1040
1117
|
const channel = channels.find(c => c.id === channelId);
|
|
1041
1118
|
if (!channel) {
|
|
@@ -1061,6 +1138,19 @@ app.get('/channels', async (_req, res) => {
|
|
|
1061
1138
|
if (typeof autoInvokeTools === 'boolean') {
|
|
1062
1139
|
channel.autoInvokeTools = autoInvokeTools;
|
|
1063
1140
|
}
|
|
1141
|
+
// bound_judgment_ids: 允许数组(替换)/null(清空)/undefined(不改)
|
|
1142
|
+
if (bound_judgment_ids !== undefined) {
|
|
1143
|
+
if (bound_judgment_ids === null) {
|
|
1144
|
+
channel.bound_judgment_ids = [];
|
|
1145
|
+
} else if (Array.isArray(bound_judgment_ids)) {
|
|
1146
|
+
channel.bound_judgment_ids = bound_judgment_ids.filter(
|
|
1147
|
+
(x: unknown) => typeof x === 'string' && (x as string).length > 0
|
|
1148
|
+
);
|
|
1149
|
+
} else {
|
|
1150
|
+
return res.status(400).json({ error: 'bound_judgment_ids must be array or null' });
|
|
1151
|
+
}
|
|
1152
|
+
console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
|
|
1153
|
+
}
|
|
1064
1154
|
channel.updatedAt = new Date().toISOString();
|
|
1065
1155
|
await saveChannels(channels);
|
|
1066
1156
|
res.json(channel);
|
|
@@ -1225,8 +1315,9 @@ app.get('/channels', async (_req, res) => {
|
|
|
1225
1315
|
}
|
|
1226
1316
|
};
|
|
1227
1317
|
|
|
1228
|
-
// 重新生成时只发送用户消息
|
|
1229
|
-
|
|
1318
|
+
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
1319
|
+
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
1320
|
+
fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
|
|
1230
1321
|
|
|
1231
1322
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
1232
1323
|
|
|
@@ -2451,7 +2542,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
2451
2542
|
try {
|
|
2452
2543
|
const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
|
|
2453
2544
|
healthMonitor = createHealthMonitor();
|
|
2454
|
-
watchdog
|
|
2545
|
+
// 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
|
|
2546
|
+
watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
|
|
2455
2547
|
|
|
2456
2548
|
console.log('[24h] Heartbeat modules loaded');
|
|
2457
2549
|
} catch (err) {
|