@bolloon/bolloon-agent 0.1.24 → 0.1.26
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 +295 -6
- package/dist/web/index.html +32 -2
- package/dist/web/server.js +221 -6
- package/dist/web/style.css +35 -0
- package/package.json +1 -1
- package/src/web/client.js +295 -6
- package/src/web/index.html +32 -2
- package/src/web/server.ts +240 -6
- package/src/web/style.css +35 -0
package/src/web/client.js
CHANGED
|
@@ -23,6 +23,7 @@ let eventSources = new Map(); // channelId -> EventSource
|
|
|
23
23
|
let currentChannelId = null;
|
|
24
24
|
let currentAgentId = '';
|
|
25
25
|
let channels = [];
|
|
26
|
+
let remoteChannels = []; // v3: 远端 channel UI 元数据 (按 peer 分组)
|
|
26
27
|
let isSidebarCollapsed = false;
|
|
27
28
|
let reconnectAttempts = new Map(); // channelId -> attempts
|
|
28
29
|
let reconnectTimers = new Map(); // channelId -> timer
|
|
@@ -623,6 +624,11 @@ async function selectChannel(channelId, targetSessionId = null) {
|
|
|
623
624
|
currentChannelId = channelId;
|
|
624
625
|
reconnectAttempts.set(channelId, 0);
|
|
625
626
|
|
|
627
|
+
// v3: 盾牌弹窗打开时, 切 channel 要刷列表 (tab 标题 + 已绑/未绑 分组)
|
|
628
|
+
if (typeof judgmentsModal !== 'undefined' && judgmentsModal && judgmentsModal.classList.contains('active')) {
|
|
629
|
+
if (typeof lastJudgmentsCache !== 'undefined') renderJudgments(lastJudgmentsCache);
|
|
630
|
+
}
|
|
631
|
+
|
|
626
632
|
// 找到当前频道和 session
|
|
627
633
|
const channel = channels.find(c => c.id === channelId);
|
|
628
634
|
if (channel) {
|
|
@@ -1996,6 +2002,18 @@ let judgmentsLoaded = false;
|
|
|
1996
2002
|
function showJudgmentsModal() {
|
|
1997
2003
|
if (judgmentsModal) judgmentsModal.classList.add('active');
|
|
1998
2004
|
if (!judgmentsLoaded) loadJudgments();
|
|
2005
|
+
else renderJudgments(lastJudgmentsCache); // 打开时按当前 channel / tab 重渲
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function switchJudgmentTab(tab) {
|
|
2009
|
+
currentJudgmentTab = tab;
|
|
2010
|
+
document.querySelectorAll('.judgment-tab').forEach(btn => {
|
|
2011
|
+
const active = btn.dataset.tab === tab;
|
|
2012
|
+
btn.classList.toggle('active', active);
|
|
2013
|
+
btn.style.borderBottomColor = active ? '#2563eb' : 'transparent';
|
|
2014
|
+
btn.style.color = active ? '#2563eb' : '#6b7280';
|
|
2015
|
+
});
|
|
2016
|
+
renderJudgments(lastJudgmentsCache);
|
|
1999
2017
|
}
|
|
2000
2018
|
|
|
2001
2019
|
function hideJudgmentsModal() {
|
|
@@ -2008,16 +2026,83 @@ function escapeHtml(s) {
|
|
|
2008
2026
|
}[c]));
|
|
2009
2027
|
}
|
|
2010
2028
|
|
|
2029
|
+
let currentJudgmentTab = 'channel'; // 'channel' | 'global'
|
|
2030
|
+
let lastJudgmentsCache = []; // 最近一次 loadJudgments 拿到的原始列表, 切 tab / 切 channel 时复用
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* v3 重做: 渲染判断力列表 (受 tab + 当前 channel 影响)
|
|
2034
|
+
* tab = 'channel': 拆为"已绑定" + "未绑定"两组, 每条带 + / × 按钮
|
|
2035
|
+
* tab = 'global': 全部 judgment 列表, 无 + / × 按钮
|
|
2036
|
+
* 如果没选 channel, 'channel' tab 自动显示提示 + 全部 judgment
|
|
2037
|
+
*/
|
|
2011
2038
|
function renderJudgments(items) {
|
|
2012
2039
|
if (!judgmentsList) return;
|
|
2013
|
-
|
|
2040
|
+
const all = items || [];
|
|
2041
|
+
const titleEl = document.getElementById('judgments-list-title');
|
|
2042
|
+
const chNameEl = document.getElementById('judgments-tab-channel-name');
|
|
2043
|
+
const currentCh = currentChannelId
|
|
2044
|
+
? channels.find(c => c.id === currentChannelId)
|
|
2045
|
+
: null;
|
|
2046
|
+
|
|
2047
|
+
if (chNameEl) {
|
|
2048
|
+
chNameEl.textContent = currentCh ? `(${currentCh.name})` : '(未选)';
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (all.length === 0) {
|
|
2014
2052
|
judgmentsList.innerHTML = '<div class="task-empty">还没有判断, 在上面记录第一条吧</div>';
|
|
2053
|
+
if (titleEl) titleEl.textContent = '本 channel 的判断力';
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
if (currentJudgmentTab === 'global') {
|
|
2058
|
+
// 全局 tab: 全部 judgment, 简单列表
|
|
2059
|
+
if (titleEl) titleEl.textContent = `全局判断力 (${all.length} 条)`;
|
|
2060
|
+
judgmentsList.innerHTML = renderJudgmentItems(all, { showBindToggle: false });
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// channel tab: 必须有 channel
|
|
2065
|
+
if (!currentCh) {
|
|
2066
|
+
if (titleEl) titleEl.textContent = '本 channel 的判断力';
|
|
2067
|
+
judgmentsList.innerHTML = `
|
|
2068
|
+
<div style="padding:24px 12px;text-align:center;color:#6b7280;font-size:13px;">
|
|
2069
|
+
请先在左侧选中一个 channel,<br>然后这里会显示已绑定和可加入的判断力。
|
|
2070
|
+
</div>
|
|
2071
|
+
`;
|
|
2015
2072
|
return;
|
|
2016
2073
|
}
|
|
2017
|
-
|
|
2074
|
+
|
|
2075
|
+
const boundIds = new Set(
|
|
2076
|
+
Array.isArray(currentCh.bound_judgment_ids) ? currentCh.bound_judgment_ids : []
|
|
2077
|
+
);
|
|
2078
|
+
const bound = all.filter(j => boundIds.has(j.id));
|
|
2079
|
+
const unbound = all.filter(j => !boundIds.has(j.id));
|
|
2080
|
+
|
|
2081
|
+
if (titleEl) titleEl.textContent = `${currentCh.name} 的判断力 (已绑 ${bound.length} / 共 ${all.length})`;
|
|
2082
|
+
|
|
2083
|
+
let html = '';
|
|
2084
|
+
if (bound.length > 0) {
|
|
2085
|
+
html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:8px 4px 4px;">已绑定 (${bound.length})</div>`;
|
|
2086
|
+
html += renderJudgmentItems(bound, { showBindToggle: true, isBound: true });
|
|
2087
|
+
}
|
|
2088
|
+
if (unbound.length > 0) {
|
|
2089
|
+
html += `<div style="font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px;padding:14px 4px 4px;">未绑定 (${unbound.length})</div>`;
|
|
2090
|
+
html += renderJudgmentItems(unbound, { showBindToggle: true, isBound: false });
|
|
2091
|
+
}
|
|
2092
|
+
judgmentsList.innerHTML = html;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function renderJudgmentItems(items, opts) {
|
|
2096
|
+
const { showBindToggle, isBound } = opts || {};
|
|
2097
|
+
return items.map(j => {
|
|
2018
2098
|
const reason = (j.reasons && j.reasons[0]) ? escapeHtml(j.reasons[0]) : '';
|
|
2019
2099
|
const domain = (j.context && j.context.domain) ? escapeHtml(j.context.domain) : 'general';
|
|
2020
2100
|
const stakes = (j.context && j.context.stakes) ? escapeHtml(j.context.stakes) : 'medium';
|
|
2101
|
+
const bindBtn = showBindToggle
|
|
2102
|
+
? isBound
|
|
2103
|
+
? `<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>`
|
|
2104
|
+
: `<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>`
|
|
2105
|
+
: '';
|
|
2021
2106
|
return `
|
|
2022
2107
|
<div class="task-item completed judgment-row"
|
|
2023
2108
|
data-judgment-id="${escapeHtml(j.id)}"
|
|
@@ -2037,6 +2122,7 @@ function renderJudgments(items) {
|
|
|
2037
2122
|
<div class="task-item-meta" style="color:#999;font-size:11px;margin-top:4px;display:flex;justify-content:space-between;align-items:center;">
|
|
2038
2123
|
<span>${domain} · ${escapeHtml(j.timestamp)} · ${escapeHtml(j.id)}</span>
|
|
2039
2124
|
<span style="display:flex;gap:4px;">
|
|
2125
|
+
${bindBtn}
|
|
2040
2126
|
<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
2127
|
<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
2128
|
</span>
|
|
@@ -2044,7 +2130,6 @@ function renderJudgments(items) {
|
|
|
2044
2130
|
</div>
|
|
2045
2131
|
`;
|
|
2046
2132
|
}).join('');
|
|
2047
|
-
judgmentsList.innerHTML = html;
|
|
2048
2133
|
}
|
|
2049
2134
|
|
|
2050
2135
|
async function loadJudgments() {
|
|
@@ -2053,7 +2138,8 @@ async function loadJudgments() {
|
|
|
2053
2138
|
const res = await fetch('/api/judgments');
|
|
2054
2139
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
2055
2140
|
const data = await res.json();
|
|
2056
|
-
|
|
2141
|
+
lastJudgmentsCache = data.judgments || [];
|
|
2142
|
+
renderJudgments(lastJudgmentsCache);
|
|
2057
2143
|
if (judgmentsBadge) {
|
|
2058
2144
|
if (data.count > 0) {
|
|
2059
2145
|
judgmentsBadge.textContent = data.count;
|
|
@@ -2068,11 +2154,46 @@ async function loadJudgments() {
|
|
|
2068
2154
|
}
|
|
2069
2155
|
}
|
|
2070
2156
|
|
|
2157
|
+
/** 把 judgment id 加进 / 移出当前 channel.bound_judgment_ids, 然后刷新两边 UI */
|
|
2158
|
+
async function toggleChannelJudgment(judgmentId, action) {
|
|
2159
|
+
if (!currentChannelId) {
|
|
2160
|
+
showJudgmentError('请先选中一个 channel');
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const ch = channels.find(c => c.id === currentChannelId);
|
|
2164
|
+
if (!ch) return;
|
|
2165
|
+
const set = new Set(Array.isArray(ch.bound_judgment_ids) ? ch.bound_judgment_ids : []);
|
|
2166
|
+
if (action === 'bind') set.add(judgmentId);
|
|
2167
|
+
else set.delete(judgmentId);
|
|
2168
|
+
const next = Array.from(set);
|
|
2169
|
+
try {
|
|
2170
|
+
const res = await fetch(`/channels/${currentChannelId}`, {
|
|
2171
|
+
method: 'PATCH',
|
|
2172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2173
|
+
body: JSON.stringify({ bound_judgment_ids: next })
|
|
2174
|
+
});
|
|
2175
|
+
if (!res.ok) {
|
|
2176
|
+
const err = await res.json().catch(() => ({}));
|
|
2177
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
2178
|
+
}
|
|
2179
|
+
const updated = await res.json();
|
|
2180
|
+
const idx = channels.findIndex(c => c.id === currentChannelId);
|
|
2181
|
+
if (idx >= 0) channels[idx] = updated;
|
|
2182
|
+
// 弹窗开着就刷新, 关着就跳过
|
|
2183
|
+
if (judgmentsModal && judgmentsModal.classList.contains('active')) {
|
|
2184
|
+
renderJudgments(lastJudgmentsCache);
|
|
2185
|
+
}
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
showJudgmentError('绑定失败: ' + err.message);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2071
2191
|
// 列表内编辑/删除 + 拖拽 — 事件委托
|
|
2072
2192
|
if (judgmentsList) {
|
|
2073
2193
|
judgmentsList.addEventListener('click', async (e) => {
|
|
2074
2194
|
const editBtn = e.target.closest && e.target.closest('.judgment-edit-btn');
|
|
2075
2195
|
const delBtn = e.target.closest && e.target.closest('.judgment-del-btn');
|
|
2196
|
+
const toggleBtn = e.target.closest && e.target.closest('.judgment-toggle-btn');
|
|
2076
2197
|
if (editBtn) {
|
|
2077
2198
|
const id = editBtn.getAttribute('data-id');
|
|
2078
2199
|
await editJudgment(id);
|
|
@@ -2087,9 +2208,18 @@ if (judgmentsList) {
|
|
|
2087
2208
|
} catch (err) {
|
|
2088
2209
|
showJudgmentError('删除失败: ' + err.message);
|
|
2089
2210
|
}
|
|
2211
|
+
} else if (toggleBtn) {
|
|
2212
|
+
const id = toggleBtn.getAttribute('data-id');
|
|
2213
|
+
const action = toggleBtn.getAttribute('data-action');
|
|
2214
|
+
await toggleChannelJudgment(id, action);
|
|
2090
2215
|
}
|
|
2091
2216
|
});
|
|
2092
2217
|
|
|
2218
|
+
// tab 切换
|
|
2219
|
+
document.querySelectorAll('.judgment-tab').forEach(btn => {
|
|
2220
|
+
btn.addEventListener('click', () => switchJudgmentTab(btn.dataset.tab));
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2093
2223
|
// 拖拽: 每条 judgment 是 drag source, dataTransfer 装 decision text
|
|
2094
2224
|
judgmentsList.addEventListener('dragstart', (e) => {
|
|
2095
2225
|
const row = e.target.closest && e.target.closest('.judgment-row');
|
|
@@ -2361,6 +2491,163 @@ loadJudgments();
|
|
|
2361
2491
|
// 后台定期刷新 (与 modal 打开/关闭无关, 任何时候都保持徽章新鲜)
|
|
2362
2492
|
setInterval(loadJudgments, 10000);
|
|
2363
2493
|
|
|
2494
|
+
// ============================================================================
|
|
2495
|
+
// v3: 远端智能体 (从 P2P 连接的 peer 拉取的 channel UI 元数据)
|
|
2496
|
+
// ============================================================================
|
|
2497
|
+
|
|
2498
|
+
async function loadRemoteChannels() {
|
|
2499
|
+
try {
|
|
2500
|
+
const res = await fetch('/api/remote-channels');
|
|
2501
|
+
if (!res.ok) return;
|
|
2502
|
+
const data = await res.json();
|
|
2503
|
+
remoteChannels = Array.isArray(data.peers) ? data.peers : [];
|
|
2504
|
+
renderRemoteChannels();
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
console.error('[v3] loadRemoteChannels 失败:', err);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function renderRemoteChannels() {
|
|
2511
|
+
const list = document.getElementById('remote-channel-list');
|
|
2512
|
+
if (!list) return;
|
|
2513
|
+
if (remoteChannels.length === 0) {
|
|
2514
|
+
list.innerHTML = '<li style="color:var(--text-muted);font-size:11px;padding:8px 4px;text-align:center;">(暂无, 点 ↻ 刷新)</li>';
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
const html = remoteChannels.map(p => {
|
|
2518
|
+
const peerShort = p.peerId.substring(0, 12) + '…';
|
|
2519
|
+
return `
|
|
2520
|
+
<li class="remote-peer-group" style="margin-bottom:8px;">
|
|
2521
|
+
<div style="font-size:10px;color:var(--text-muted);padding:2px 4px;display:flex;align-items:center;gap:4px;">
|
|
2522
|
+
<span>🌐</span><span title="${escapeHtml(p.peerId)}">${escapeHtml(peerShort)}</span>
|
|
2523
|
+
<span>·</span>
|
|
2524
|
+
<span>${p.channels.length} 个</span>
|
|
2525
|
+
</div>
|
|
2526
|
+
${p.channels.map(c => `
|
|
2527
|
+
<div class="remote-channel-row" data-peer-id="${escapeHtml(p.peerId)}" data-channel-id="${escapeHtml(c.id)}"
|
|
2528
|
+
style="display:flex;align-items:center;gap:6px;padding:4px 6px;cursor:pointer;border-radius:4px;font-size:12px;color:var(--text);">
|
|
2529
|
+
<span>🤖</span>
|
|
2530
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(c.name || '')}">${escapeHtml(c.name || '(未命名)')}</span>
|
|
2531
|
+
${c.hasWallet ? '<span title="绑定钱包">⛓</span>' : ''}
|
|
2532
|
+
<span title="绑定判断力数" style="font-size:10px;color:var(--text-muted);">🧠${c.boundJudgmentCount || 0}</span>
|
|
2533
|
+
</div>
|
|
2534
|
+
`).join('')}
|
|
2535
|
+
</li>
|
|
2536
|
+
`;
|
|
2537
|
+
}).join('');
|
|
2538
|
+
list.innerHTML = html;
|
|
2539
|
+
|
|
2540
|
+
// 绑定点击 — 暂时只是 console.log + 提示, Phase 2 接 chat
|
|
2541
|
+
list.querySelectorAll('.remote-channel-row').forEach(row => {
|
|
2542
|
+
row.addEventListener('click', () => {
|
|
2543
|
+
const peerId = row.dataset.peerId;
|
|
2544
|
+
const channelId = row.dataset.channelId;
|
|
2545
|
+
console.log('[v3] 点击远端 channel:', peerId.substring(0,12), channelId);
|
|
2546
|
+
alert(`远端智能体点击 — Phase 2 实现 chat 调用\n\nPeer: ${peerId}\nChannel: ${channelId}`);
|
|
2547
|
+
});
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
const refreshRemoteBtn = document.getElementById('refresh-remote-agents-btn');
|
|
2552
|
+
if (refreshRemoteBtn) {
|
|
2553
|
+
refreshRemoteBtn.addEventListener('click', async (e) => {
|
|
2554
|
+
e.stopPropagation(); // 防止冒泡触发折叠
|
|
2555
|
+
refreshRemoteBtn.disabled = true;
|
|
2556
|
+
refreshRemoteBtn.textContent = '⏳';
|
|
2557
|
+
try {
|
|
2558
|
+
const res = await fetch('/api/remote-channels/refresh', { method: 'POST' });
|
|
2559
|
+
const data = await res.json();
|
|
2560
|
+
console.log('[v3] 刷新远端智能体:', data);
|
|
2561
|
+
setTimeout(loadRemoteChannels, 1500);
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
console.error('[v3] 刷新失败:', err);
|
|
2564
|
+
} finally {
|
|
2565
|
+
setTimeout(() => {
|
|
2566
|
+
refreshRemoteBtn.disabled = false;
|
|
2567
|
+
refreshRemoteBtn.textContent = '↻ 刷新';
|
|
2568
|
+
}, 2000);
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// 启动时拉一次 + 定期轮询 (SSE 接收 P2P reply 后也会更新)
|
|
2574
|
+
loadRemoteChannels();
|
|
2575
|
+
setInterval(loadRemoteChannels, 8000);
|
|
2576
|
+
|
|
2577
|
+
// ============ v3: 折叠 + 拖拽分隔线 ============
|
|
2578
|
+
|
|
2579
|
+
// 给本地/远端 section 加 flex 修饰类 (CSS variable 驱动比例)
|
|
2580
|
+
const localSection = document.querySelector('.sidebar-section'); // 第一个 section = 本地 channel
|
|
2581
|
+
const remoteSection = document.getElementById('remote-agents-section');
|
|
2582
|
+
if (localSection) localSection.classList.add('local-flex');
|
|
2583
|
+
if (remoteSection) remoteSection.classList.add('remote-flex');
|
|
2584
|
+
|
|
2585
|
+
// 折叠: 点 header 切换 collapsed 类
|
|
2586
|
+
const remoteHeader = document.getElementById('remote-agents-header');
|
|
2587
|
+
if (remoteHeader && remoteSection) {
|
|
2588
|
+
remoteHeader.addEventListener('click', (e) => {
|
|
2589
|
+
// 阻止刷新按钮的事件冒泡在 refreshRemoteBtn 里已处理
|
|
2590
|
+
remoteSection.classList.toggle('collapsed');
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// 拖拽分隔线: 鼠标按下开始拖, mousemove 改 --local-flex / --remote-flex, mouseup 结束
|
|
2595
|
+
const splitHandle = document.getElementById('sidebar-split-handle');
|
|
2596
|
+
if (splitHandle && localSection && remoteSection) {
|
|
2597
|
+
// 初始化等分
|
|
2598
|
+
const updateFlexVars = (localRatio, remoteRatio) => {
|
|
2599
|
+
localSection.style.setProperty('--local-flex', String(localRatio));
|
|
2600
|
+
remoteSection.style.setProperty('--remote-flex', String(remoteRatio));
|
|
2601
|
+
};
|
|
2602
|
+
updateFlexVars(1, 1);
|
|
2603
|
+
|
|
2604
|
+
let isDragging = false;
|
|
2605
|
+
let dragStartY = 0;
|
|
2606
|
+
let startLocalFlex = 1;
|
|
2607
|
+
let startRemoteFlex = 1;
|
|
2608
|
+
let sidebarHeight = 0;
|
|
2609
|
+
|
|
2610
|
+
splitHandle.addEventListener('mousedown', (e) => {
|
|
2611
|
+
isDragging = true;
|
|
2612
|
+
splitHandle.classList.add('dragging');
|
|
2613
|
+
dragStartY = e.clientY;
|
|
2614
|
+
// 读当前 CSS variable 拿真实 flex 值
|
|
2615
|
+
const lf = parseFloat(getComputedStyle(localSection).getPropertyValue('--local-flex')) || 1;
|
|
2616
|
+
const rf = parseFloat(getComputedStyle(remoteSection).getPropertyValue('--remote-flex')) || 1;
|
|
2617
|
+
startLocalFlex = lf;
|
|
2618
|
+
startRemoteFlex = rf;
|
|
2619
|
+
// 父容器可用高度 = sidebar-section 总和 (本地+远端+handle)
|
|
2620
|
+
const sidebar = document.querySelector('.sidebar');
|
|
2621
|
+
if (sidebar) sidebarHeight = sidebar.clientHeight;
|
|
2622
|
+
e.preventDefault();
|
|
2623
|
+
document.body.style.cursor = 'ns-resize';
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
document.addEventListener('mousemove', (e) => {
|
|
2627
|
+
if (!isDragging) return;
|
|
2628
|
+
const deltaY = e.clientY - dragStartY;
|
|
2629
|
+
if (sidebarHeight <= 0) return;
|
|
2630
|
+
// deltaY 正 = 鼠标下移 = 拉大本地 / 缩小远端
|
|
2631
|
+
// 转换: 1 像素 ≈ sidebarHeight 中 0.005 的比例
|
|
2632
|
+
const deltaRatio = deltaY / sidebarHeight * 4; // 4 倍灵敏
|
|
2633
|
+
let newLocal = Math.max(0.1, startLocalFlex + deltaRatio);
|
|
2634
|
+
let newRemote = Math.max(0.1, startRemoteFlex - deltaRatio);
|
|
2635
|
+
updateFlexVars(newLocal, newRemote);
|
|
2636
|
+
});
|
|
2637
|
+
|
|
2638
|
+
document.addEventListener('mouseup', () => {
|
|
2639
|
+
if (!isDragging) return;
|
|
2640
|
+
isDragging = false;
|
|
2641
|
+
splitHandle.classList.remove('dragging');
|
|
2642
|
+
document.body.style.cursor = '';
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
// 双击分隔线 = 重置为等分
|
|
2646
|
+
splitHandle.addEventListener('dblclick', () => {
|
|
2647
|
+
updateFlexVars(1, 1);
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2364
2651
|
if (taskModal) {
|
|
2365
2652
|
taskModal.addEventListener('click', (e) => {
|
|
2366
2653
|
if (e.target === taskModal) {
|
|
@@ -2831,7 +3118,10 @@ if (agentAddConfirmBtn) {
|
|
|
2831
3118
|
const res = await fetch(`/channels/${channelId}`, {
|
|
2832
3119
|
method: 'PATCH',
|
|
2833
3120
|
headers: { 'Content-Type': 'application/json' },
|
|
2834
|
-
body: JSON.stringify({
|
|
3121
|
+
body: JSON.stringify({
|
|
3122
|
+
walletAddress: walletAddress || null,
|
|
3123
|
+
autoInvokeTools
|
|
3124
|
+
})
|
|
2835
3125
|
});
|
|
2836
3126
|
if (!res.ok) throw new Error('update failed');
|
|
2837
3127
|
const updated = await res.json();
|
|
@@ -2846,4 +3136,3 @@ if (agentAddConfirmBtn) {
|
|
|
2846
3136
|
}
|
|
2847
3137
|
});
|
|
2848
3138
|
}
|
|
2849
|
-
|
package/src/web/index.html
CHANGED
|
@@ -64,6 +64,25 @@
|
|
|
64
64
|
<ul class="channel-list" id="channel-list"></ul>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
|
+
<!-- v3: 本地/远端分隔线 — 可拖拽改变两边高度 -->
|
|
68
|
+
<div id="sidebar-split-handle" title="拖动调整上方/下方高度">
|
|
69
|
+
<div class="split-handle-grip"></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- v3: 远端智能体 — 从 P2P 连接的 peer 拉取的 channel UI 元数据 -->
|
|
73
|
+
<div class="sidebar-section" id="remote-agents-section">
|
|
74
|
+
<div class="section-header" id="remote-agents-header" style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;cursor:pointer;user-select:none;">
|
|
75
|
+
<span style="display:flex;align-items:center;gap:6px;">
|
|
76
|
+
<span id="remote-agents-toggle" style="font-size:10px;color:var(--text-muted);transition:transform 0.2s;">▼</span>
|
|
77
|
+
<span class="section-title" style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);">远端智能体</span>
|
|
78
|
+
</span>
|
|
79
|
+
<button id="refresh-remote-agents-btn" title="从已连接 P2P peer 拉取" style="background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:2px 6px;border-radius:4px;font-size:11px;line-height:1;">↻ 刷新</button>
|
|
80
|
+
</div>
|
|
81
|
+
<ul class="channel-list" id="remote-channel-list" style="list-style:none;padding:0;margin:0;">
|
|
82
|
+
<li style="color:var(--text-muted);font-size:11px;padding:8px 4px;text-align:center;">(暂无, 点 ↻ 刷新)</li>
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
67
86
|
<div class="sidebar-footer">
|
|
68
87
|
<div class="agent-status">
|
|
69
88
|
<span class="status-dot"></span>
|
|
@@ -236,8 +255,19 @@
|
|
|
236
255
|
<input type="file" id="judgment-import-file" accept=".json,.yaml,.yml,.md,.txt,.html,.htm" style="display:none">
|
|
237
256
|
</div>
|
|
238
257
|
<div id="judgment-error" class="form-info" style="display:none;color:#b91c1c;"></div>
|
|
239
|
-
|
|
240
|
-
|
|
258
|
+
<!-- v3 重做: tab 切换 — 默认显示当前 channel 的 judgment 上下文, 切换后才是全部 -->
|
|
259
|
+
<div id="judgments-tabs" style="display:flex;border-bottom:1px solid #e5e7eb;margin-top:20px;">
|
|
260
|
+
<button class="judgment-tab active" data-tab="channel"
|
|
261
|
+
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;">
|
|
262
|
+
本 channel <span id="judgments-tab-channel-name" style="font-weight:normal;color:#6b7280;"></span>
|
|
263
|
+
</button>
|
|
264
|
+
<button class="judgment-tab" data-tab="global"
|
|
265
|
+
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;">
|
|
266
|
+
全局
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
<h3 style="margin-top:16px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
270
|
+
<span id="judgments-list-title">本 channel 的判断力</span>
|
|
241
271
|
<span style="display:flex;align-items:center;gap:6px;margin-left:auto;font-size:12px;font-weight:normal;">
|
|
242
272
|
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;color:#6b7280;">
|
|
243
273
|
<input type="checkbox" id="judgment-select-all" style="cursor:pointer;"> 全选
|