@bolloon/bolloon-agent 0.1.32 → 0.1.33
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/pi-sdk.js +10 -1
- package/dist/llm/audio-config-store.js +199 -0
- package/dist/llm/config-store.js +20 -10
- package/dist/llm/pi-ai.js +2 -2
- package/dist/llm/video-config-store.js +31 -1
- package/dist/pi-ecosystem/index.js +1 -1
- package/dist/web/api-config.html +13 -1
- package/dist/web/client.js +375 -8
- package/dist/web/server.js +269 -5
- package/package.json +1 -1
- package/src/agents/pi-sdk.ts +9 -1
- package/src/llm/audio-config-store.ts +6 -1
- package/src/llm/config-store.ts +21 -11
- package/src/llm/pi-ai.ts +2 -2
- package/src/llm/video-config-store.ts +7 -1
- package/src/web/api-config.html +13 -1
- package/src/web/client.js +375 -8
- package/src/web/server.ts +228 -5
package/dist/web/client.js
CHANGED
|
@@ -169,6 +169,19 @@ function startV3GlobalSSE() {
|
|
|
169
169
|
log.scrollTop = log.scrollHeight;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
} else if (msg.type === 'cross-mention-received') {
|
|
173
|
+
// v3 新增: A 节点上, 某个 channel 的 LLM @-mention 了另一个 channel, SSE 推过来
|
|
174
|
+
// 在所有打开的 chat modal 上显示"AI 跨渠道 @-mention" 提示
|
|
175
|
+
const allModals = document.querySelectorAll('.rcm-mention-toast, [id^="rcm-log"]');
|
|
176
|
+
for (const log of allModals) {
|
|
177
|
+
if (!log.id) continue;
|
|
178
|
+
const toast = document.createElement('div');
|
|
179
|
+
toast.style.cssText = 'margin:6px 0;padding:8px 10px;background:#fce7f3;border-left:3px solid #ec4899;border-radius:4px;font-size:12px;color:#831843;';
|
|
180
|
+
const fromTxt = msg.source === 'ai-mention-remote' ? `远端节点 ${(msg.fromPublicKey || '').substring(0, 8)}… 的 ${msg.originChannelName}` : `${msg.originChannelName} (本地)`;
|
|
181
|
+
toast.innerHTML = `📡 <b>${fromTxt}</b> @-mention → 当前 channel: <i>${escapeHtml((msg.text || '').slice(0, 100))}</i>${msg.text && msg.text.length > 100 ? '…' : ''}`;
|
|
182
|
+
log.appendChild(toast);
|
|
183
|
+
log.scrollTop = log.scrollHeight;
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
186
|
} catch (err) {
|
|
174
187
|
console.error('[v3] 全局 SSE 解析失败:', err);
|
|
@@ -1586,6 +1599,329 @@ input.addEventListener('keydown', (e) => {
|
|
|
1586
1599
|
}
|
|
1587
1600
|
});
|
|
1588
1601
|
|
|
1602
|
+
// ============ v3 新增: @-mention 单选自动补全 (主聊天框 #input) ============
|
|
1603
|
+
let mentionChannels = []; // { id, name, source: 'local'|'remote', ownerPublicKey? }
|
|
1604
|
+
let mentionDropdownEl = null;
|
|
1605
|
+
let mentionHighlightIdx = -1;
|
|
1606
|
+
let mentionQuery = null;
|
|
1607
|
+
let mentionAnchor = -1; // @ 字符的绝对位置 (固定, 直到 dropdown 关闭)
|
|
1608
|
+
let mentionBlockEnd = -1; // 插入区块的终点 (单选模式下 = anchor + 1 + query)
|
|
1609
|
+
let mentionDocMousedownBound = false; // 防止重复注册 document mousedown
|
|
1610
|
+
|
|
1611
|
+
function ensureMentionDocMousedown() {
|
|
1612
|
+
if (mentionDocMousedownBound) return;
|
|
1613
|
+
mentionDocMousedownBound = true;
|
|
1614
|
+
document.addEventListener('mousedown', (e) => {
|
|
1615
|
+
if (mentionDropdownEl && !mentionDropdownEl.contains(e.target) && e.target !== input) {
|
|
1616
|
+
closeMentionDropdown();
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async function refreshMentionChannels() {
|
|
1622
|
+
try {
|
|
1623
|
+
const res = await fetch('/channels');
|
|
1624
|
+
const local = res.ok ? await res.json() : [];
|
|
1625
|
+
const r2 = await fetch('/api/remote-channels');
|
|
1626
|
+
const remoteData = r2.ok ? await r2.json() : { peers: [] };
|
|
1627
|
+
const remote = [];
|
|
1628
|
+
for (const p of (remoteData.peers || [])) {
|
|
1629
|
+
for (const c of (p.channels || [])) {
|
|
1630
|
+
remote.push({ id: c.id, name: c.name, source: 'remote', ownerPublicKey: p.peerId });
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
mentionChannels = [
|
|
1634
|
+
...(Array.isArray(local) ? local.map(c => ({ id: c.id, name: c.name, source: 'local' })) : []),
|
|
1635
|
+
...remote
|
|
1636
|
+
];
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
console.warn('[mention] 加载渠道列表失败:', err);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function closeMentionDropdown() {
|
|
1643
|
+
if (mentionDropdownEl) { mentionDropdownEl.remove(); mentionDropdownEl = null; }
|
|
1644
|
+
mentionHighlightIdx = -1;
|
|
1645
|
+
mentionQuery = null;
|
|
1646
|
+
mentionAnchor = -1;
|
|
1647
|
+
mentionBlockEnd = -1;
|
|
1648
|
+
// 不重置 mentionDocMousedownBound — 监听器是空操作 (mentionDropdownEl === null) 留着无妨, 避免重复绑
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function getCurrentMentionQuery() {
|
|
1652
|
+
const pos = input.selectionStart || input.value.length;
|
|
1653
|
+
const before = input.value.slice(0, pos);
|
|
1654
|
+
const m = before.match(/@([一-龥A-Za-z0-9_\-]{0,30})$/);
|
|
1655
|
+
return m ? { query: m[1], anchor: pos - m[0].length } : null;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function renderMentionDropdown(items) {
|
|
1659
|
+
if (!mentionDropdownEl) {
|
|
1660
|
+
mentionDropdownEl = document.createElement('div');
|
|
1661
|
+
mentionDropdownEl.id = 'mention-dropdown';
|
|
1662
|
+
mentionDropdownEl.style.cssText = 'position:fixed;background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.15);max-height:240px;overflow-y:auto;z-index:10000;font-size:13px;min-width:240px;';
|
|
1663
|
+
document.body.appendChild(mentionDropdownEl);
|
|
1664
|
+
ensureMentionDocMousedown();
|
|
1665
|
+
}
|
|
1666
|
+
// v3 简化: 单选 + 立即填入输入框
|
|
1667
|
+
const headerHtml = `<div style="padding:6px 10px;background:#f9fafb;border-bottom:1px solid #e5e7eb;font-size:11px;color:#6b7280;display:flex;justify-content:space-between;align-items:center;">
|
|
1668
|
+
<span>💡 点击或回车选中 → 自动填入输入框</span>
|
|
1669
|
+
<span style="color:#9ca3af;">↑↓ 移动</span>
|
|
1670
|
+
</div>`;
|
|
1671
|
+
|
|
1672
|
+
if (items.length === 0) {
|
|
1673
|
+
mentionDropdownEl.innerHTML = headerHtml + '<div style="padding:10px 12px;color:#6b7280;font-size:12px;">没有匹配的渠道</div>';
|
|
1674
|
+
} else {
|
|
1675
|
+
const rows = items.map((c, i) => {
|
|
1676
|
+
const isLocal = c.source === 'local';
|
|
1677
|
+
const tag = isLocal ? '🏠 本地' : '🌐 远端';
|
|
1678
|
+
const owner = !isLocal && c.ownerPublicKey ? ` <span style="color:#9ca3af;font-size:11px;">(${c.ownerPublicKey.substring(0, 8)}…)</span>` : '';
|
|
1679
|
+
// 浅蓝 = 键盘高亮, 白 = 普通
|
|
1680
|
+
const bg = i === mentionHighlightIdx ? '#eff6ff' : '#fff';
|
|
1681
|
+
const borderLeft = i === mentionHighlightIdx ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1682
|
+
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(c.name)}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1683
|
+
<span style="font-size:10px;color:${isLocal ? '#059669' : '#2563eb'};background:${isLocal ? '#d1fae5' : '#dbeafe'};padding:1px 6px;border-radius:3px;white-space:nowrap;">${tag}</span>
|
|
1684
|
+
<span style="flex:1;">${escapeHtml(c.name)}</span>${owner}
|
|
1685
|
+
</div>`;
|
|
1686
|
+
}).join('');
|
|
1687
|
+
mentionDropdownEl.innerHTML = headerHtml + rows;
|
|
1688
|
+
mentionDropdownEl.querySelectorAll('.mention-item').forEach((el) => {
|
|
1689
|
+
const idx = parseInt(el.getAttribute('data-idx'));
|
|
1690
|
+
el.onclick = () => {
|
|
1691
|
+
// 单击 → 立即填入输入框 + 关闭 dropdown
|
|
1692
|
+
applyMention(items[idx]);
|
|
1693
|
+
};
|
|
1694
|
+
// v3 关键修复: mouseenter 只更新高亮, 不重建 dropdown — 否则用户实际点击的 element 被销毁,
|
|
1695
|
+
// click 事件落到新 element, 但实际触发的是新 element 的 onclick (空), 而不是被销毁前那个
|
|
1696
|
+
el.onmouseenter = () => {
|
|
1697
|
+
if (mentionHighlightIdx === idx) return;
|
|
1698
|
+
mentionHighlightIdx = idx;
|
|
1699
|
+
// 只更新背景色 + 左边框, 不重建 innerHTML
|
|
1700
|
+
const itemEls = mentionDropdownEl.querySelectorAll('.mention-item');
|
|
1701
|
+
itemEls.forEach((ie, ii) => {
|
|
1702
|
+
const isHi = ii === idx;
|
|
1703
|
+
ie.style.background = isHi ? '#eff6ff' : '#fff';
|
|
1704
|
+
ie.style.borderLeft = isHi ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1705
|
+
});
|
|
1706
|
+
};
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
const rect = input.getBoundingClientRect();
|
|
1710
|
+
mentionDropdownEl.style.left = rect.left + 'px';
|
|
1711
|
+
mentionDropdownEl.style.top = 'auto';
|
|
1712
|
+
mentionDropdownEl.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/** v3 单选: 把 @xxx 替换为 @渠道名 + 空格, 关闭 dropdown, 光标放空格后 */
|
|
1716
|
+
function applyMention(channel) {
|
|
1717
|
+
const anchor = mentionAnchor;
|
|
1718
|
+
const blockEnd = mentionBlockEnd >= 0 ? mentionBlockEnd : (anchor + 1 + (mentionQuery || '').length);
|
|
1719
|
+
if (anchor < 0 || anchor > input.value.length || input.value[anchor] !== '@') {
|
|
1720
|
+
closeMentionDropdown();
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const before = input.value.slice(0, anchor); // 含 @
|
|
1724
|
+
const after = input.value.slice(blockEnd); // query 之后 (可能用户已输入正文)
|
|
1725
|
+
const insert = `@${channel.name} `;
|
|
1726
|
+
input.value = before + insert + after;
|
|
1727
|
+
const newPos = before.length + insert.length;
|
|
1728
|
+
input.focus();
|
|
1729
|
+
input.setSelectionRange(newPos, newPos);
|
|
1730
|
+
closeMentionDropdown();
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function updateMentionDropdown() {
|
|
1734
|
+
if (!mentionChannels.length) return;
|
|
1735
|
+
const m = getCurrentMentionQuery();
|
|
1736
|
+
if (!m) { closeMentionDropdown(); return; }
|
|
1737
|
+
// 只在 dropdown 刚打开时设置 anchor (blockEnd 跟着 insert 走)
|
|
1738
|
+
if (mentionAnchor === -1) {
|
|
1739
|
+
mentionAnchor = m.anchor;
|
|
1740
|
+
mentionBlockEnd = m.anchor + 1 + (m.query || '').length;
|
|
1741
|
+
}
|
|
1742
|
+
mentionQuery = m.query;
|
|
1743
|
+
const q = m.query.toLowerCase();
|
|
1744
|
+
const items = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1745
|
+
mentionHighlightIdx = items.length > 0 ? 0 : -1;
|
|
1746
|
+
renderMentionDropdown(items);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
input.addEventListener('input', () => {
|
|
1750
|
+
updateMentionDropdown();
|
|
1751
|
+
});
|
|
1752
|
+
input.addEventListener('keydown', (e) => {
|
|
1753
|
+
if (!mentionDropdownEl) return;
|
|
1754
|
+
const items = mentionDropdownEl.querySelectorAll('.mention-item');
|
|
1755
|
+
if (e.key === 'ArrowDown') {
|
|
1756
|
+
e.preventDefault();
|
|
1757
|
+
if (items.length === 0) return;
|
|
1758
|
+
mentionHighlightIdx = (mentionHighlightIdx + 1) % items.length;
|
|
1759
|
+
const q = (mentionQuery || '').toLowerCase();
|
|
1760
|
+
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1761
|
+
renderMentionDropdown(filtered);
|
|
1762
|
+
} else if (e.key === 'ArrowUp') {
|
|
1763
|
+
e.preventDefault();
|
|
1764
|
+
if (items.length === 0) return;
|
|
1765
|
+
mentionHighlightIdx = (mentionHighlightIdx - 1 + items.length) % items.length;
|
|
1766
|
+
const q = (mentionQuery || '').toLowerCase();
|
|
1767
|
+
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1768
|
+
renderMentionDropdown(filtered);
|
|
1769
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
1770
|
+
// 单选: Enter/Tab 立即填入 + 关闭 dropdown
|
|
1771
|
+
if (items.length > 0) {
|
|
1772
|
+
e.preventDefault();
|
|
1773
|
+
e.stopPropagation();
|
|
1774
|
+
const q = (mentionQuery || '').toLowerCase();
|
|
1775
|
+
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1776
|
+
const cur = filtered[mentionHighlightIdx];
|
|
1777
|
+
if (cur) applyMention(cur);
|
|
1778
|
+
}
|
|
1779
|
+
} else if (e.key === 'Escape') {
|
|
1780
|
+
e.preventDefault();
|
|
1781
|
+
closeMentionDropdown();
|
|
1782
|
+
}
|
|
1783
|
+
}, true); // capture phase, 先于 sendMessage 那个 keydown
|
|
1784
|
+
|
|
1785
|
+
// 初始化
|
|
1786
|
+
refreshMentionChannels();
|
|
1787
|
+
// 定时刷新 (channel 列表可能变化)
|
|
1788
|
+
setInterval(refreshMentionChannels, 5000);
|
|
1789
|
+
// 远端 channel 列表变化时也刷新 (loadRemoteChannels 是 function declaration, 不能重新赋值)
|
|
1790
|
+
// 用 setInterval 兜底: 每 5s 刷一次 (已经有定时器, 这里不重复)
|
|
1791
|
+
// 实际上 refreshMentionChannels() 已经在 setInterval 里跑了
|
|
1792
|
+
|
|
1793
|
+
// v3 新增: 通用版 @-autocomplete (任意 input 元素都能挂, 比如 B 端的 #rcm-input)
|
|
1794
|
+
function setupMentionAutocomplete(inputEl) {
|
|
1795
|
+
if (!inputEl || inputEl.__mentionBound) return;
|
|
1796
|
+
inputEl.__mentionBound = true;
|
|
1797
|
+
let localQuery = null;
|
|
1798
|
+
let localAnchor = -1; // @ 字符的绝对位置 (固定, 直到 dropdown 关闭)
|
|
1799
|
+
let localBlockEnd = -1; // 插入区块的终点
|
|
1800
|
+
let localHighlight = -1;
|
|
1801
|
+
|
|
1802
|
+
function closeLocal() {
|
|
1803
|
+
if (inputEl.__mentionDD) { inputEl.__mentionDD.remove(); inputEl.__mentionDD = null; }
|
|
1804
|
+
localHighlight = -1; localQuery = null; localAnchor = -1; localBlockEnd = -1;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function detectQuery() {
|
|
1808
|
+
const pos = inputEl.selectionStart || inputEl.value.length;
|
|
1809
|
+
const before = inputEl.value.slice(0, pos);
|
|
1810
|
+
const m = before.match(/@([一-龥A-Za-z0-9_\-]{0,30})$/);
|
|
1811
|
+
return m ? { query: m[1], anchor: pos - m[0].length } : null;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// v3 单选: 点击 / Enter 立即填入输入框 + 关闭 dropdown
|
|
1815
|
+
function applyLocal(channel) {
|
|
1816
|
+
const anchor = localAnchor;
|
|
1817
|
+
const blockEnd = localBlockEnd >= 0 ? localBlockEnd : (anchor + 1 + (localQuery || '').length);
|
|
1818
|
+
if (anchor < 0 || anchor > inputEl.value.length || inputEl.value[anchor] !== '@') {
|
|
1819
|
+
closeLocal();
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const before = inputEl.value.slice(0, anchor); // 含 @
|
|
1823
|
+
const after = inputEl.value.slice(blockEnd);
|
|
1824
|
+
const insert = `@${channel.name} `;
|
|
1825
|
+
inputEl.value = before + insert + after;
|
|
1826
|
+
const newPos = before.length + insert.length;
|
|
1827
|
+
inputEl.focus();
|
|
1828
|
+
inputEl.setSelectionRange(newPos, newPos);
|
|
1829
|
+
closeLocal();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function renderLocal(items) {
|
|
1833
|
+
if (!inputEl.__mentionDD) {
|
|
1834
|
+
inputEl.__mentionDD = document.createElement('div');
|
|
1835
|
+
inputEl.__mentionDD.style.cssText = 'position:fixed;background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.15);max-height:240px;overflow-y:auto;z-index:10001;font-size:13px;min-width:240px;';
|
|
1836
|
+
document.body.appendChild(inputEl.__mentionDD);
|
|
1837
|
+
}
|
|
1838
|
+
const headerHtml = `<div style="padding:6px 10px;background:#f9fafb;border-bottom:1px solid #e5e7eb;font-size:11px;color:#6b7280;display:flex;justify-content:space-between;align-items:center;">
|
|
1839
|
+
<span>💡 点击或回车选中 → 自动填入输入框</span>
|
|
1840
|
+
<span style="color:#9ca3af;">↑↓ 移动</span>
|
|
1841
|
+
</div>`;
|
|
1842
|
+
if (items.length === 0) {
|
|
1843
|
+
inputEl.__mentionDD.innerHTML = headerHtml + '<div style="padding:10px 12px;color:#6b7280;font-size:12px;">没有匹配的渠道</div>';
|
|
1844
|
+
} else {
|
|
1845
|
+
inputEl.__mentionDD.innerHTML = headerHtml + items.map((c, i) => {
|
|
1846
|
+
const isLocal = c.source === 'local';
|
|
1847
|
+
const tag = isLocal ? '🏠 本地' : '🌐 远端';
|
|
1848
|
+
const owner = !isLocal && c.ownerPublicKey ? ` <span style="color:#9ca3af;font-size:11px;">(${c.ownerPublicKey.substring(0, 8)}…)</span>` : '';
|
|
1849
|
+
const bg = i === localHighlight ? '#eff6ff' : '#fff';
|
|
1850
|
+
const borderLeft = i === localHighlight ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1851
|
+
return `<div class="mention-item" data-idx="${i}" data-channel-id="${escapeHtml(c.id)}" data-channel-name="${escapeHtml(c.name)}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
|
|
1852
|
+
<span style="font-size:10px;color:${isLocal ? '#059669' : '#2563eb'};background:${isLocal ? '#d1fae5' : '#dbeafe'};padding:1px 6px;border-radius:3px;white-space:nowrap;">${tag}</span>
|
|
1853
|
+
<span style="flex:1;">${escapeHtml(c.name)}</span>${owner}
|
|
1854
|
+
</div>`;
|
|
1855
|
+
}).join('');
|
|
1856
|
+
inputEl.__mentionDD.querySelectorAll('.mention-item').forEach((el) => {
|
|
1857
|
+
const idx = parseInt(el.getAttribute('data-idx'));
|
|
1858
|
+
el.onclick = () => applyLocal(items[idx]);
|
|
1859
|
+
// v3 关键修复: mouseenter 只更新高亮, 不重建 dropdown (同主 input)
|
|
1860
|
+
el.onmouseenter = () => {
|
|
1861
|
+
if (localHighlight === idx) return;
|
|
1862
|
+
localHighlight = idx;
|
|
1863
|
+
const itemEls = inputEl.__mentionDD.querySelectorAll('.mention-item');
|
|
1864
|
+
itemEls.forEach((ie, ii) => {
|
|
1865
|
+
const isHi = ii === idx;
|
|
1866
|
+
ie.style.background = isHi ? '#eff6ff' : '#fff';
|
|
1867
|
+
ie.style.borderLeft = isHi ? '3px solid #93c5fd' : '3px solid transparent';
|
|
1868
|
+
});
|
|
1869
|
+
};
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
const rect = inputEl.getBoundingClientRect();
|
|
1873
|
+
inputEl.__mentionDD.style.left = rect.left + 'px';
|
|
1874
|
+
inputEl.__mentionDD.style.top = 'auto';
|
|
1875
|
+
inputEl.__mentionDD.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function update() {
|
|
1879
|
+
if (!mentionChannels.length) return;
|
|
1880
|
+
const m = detectQuery();
|
|
1881
|
+
if (!m) { closeLocal(); return; }
|
|
1882
|
+
if (localAnchor === -1) {
|
|
1883
|
+
localAnchor = m.anchor;
|
|
1884
|
+
localBlockEnd = m.anchor + 1 + (m.query || '').length;
|
|
1885
|
+
}
|
|
1886
|
+
localQuery = m.query;
|
|
1887
|
+
const q = m.query.toLowerCase();
|
|
1888
|
+
const items = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1889
|
+
localHighlight = items.length > 0 ? 0 : -1;
|
|
1890
|
+
renderLocal(items);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
inputEl.addEventListener('input', update);
|
|
1894
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
1895
|
+
if (!inputEl.__mentionDD) return;
|
|
1896
|
+
const items = inputEl.__mentionDD.querySelectorAll('.mention-item');
|
|
1897
|
+
if (e.key === 'ArrowDown') {
|
|
1898
|
+
e.preventDefault();
|
|
1899
|
+
if (items.length === 0) return;
|
|
1900
|
+
localHighlight = (localHighlight + 1) % items.length;
|
|
1901
|
+
const q = (localQuery || '').toLowerCase();
|
|
1902
|
+
renderLocal(mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8));
|
|
1903
|
+
} else if (e.key === 'ArrowUp') {
|
|
1904
|
+
e.preventDefault();
|
|
1905
|
+
if (items.length === 0) return;
|
|
1906
|
+
localHighlight = (localHighlight - 1 + items.length) % items.length;
|
|
1907
|
+
const q = (localQuery || '').toLowerCase();
|
|
1908
|
+
renderLocal(mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8));
|
|
1909
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
1910
|
+
if (items.length > 0) {
|
|
1911
|
+
e.preventDefault();
|
|
1912
|
+
e.stopPropagation();
|
|
1913
|
+
const q = (localQuery || '').toLowerCase();
|
|
1914
|
+
const filtered = mentionChannels.filter(c => c.name.toLowerCase().includes(q)).slice(0, 8);
|
|
1915
|
+
const cur = filtered[localHighlight];
|
|
1916
|
+
if (cur) applyLocal(cur);
|
|
1917
|
+
}
|
|
1918
|
+
} else if (e.key === 'Escape') {
|
|
1919
|
+
e.preventDefault();
|
|
1920
|
+
closeLocal();
|
|
1921
|
+
}
|
|
1922
|
+
}, true);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1589
1925
|
// 拖拽落点: 把判断库里的判断拖到输入框, 直接作为指令发给 AI (走"代我决定"路径).
|
|
1590
1926
|
// 用户拖进来后输入框被预填, 点发送就把这条判断作为指令交给当前 agent.
|
|
1591
1927
|
const inputArea = document.querySelector('.input-area');
|
|
@@ -2811,14 +3147,36 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2811
3147
|
log.appendChild(jh);
|
|
2812
3148
|
}
|
|
2813
3149
|
|
|
2814
|
-
// 2. 显示历史 messages
|
|
3150
|
+
// 2. 显示历史 messages (带 source 标签: 内部 owner vs 远端访客)
|
|
2815
3151
|
const msgs = data.messages || [];
|
|
2816
3152
|
if (msgs.length === 0) {
|
|
2817
3153
|
appendSystem('还没有历史消息, 在下面发第一条吧', 'info');
|
|
2818
3154
|
} else {
|
|
2819
|
-
appendSystem(`从远端拉到 ${msgs.length}
|
|
3155
|
+
appendSystem(`从远端拉到 ${msgs.length} 条历史消息 (A 内部 owner + B 远端访客 都会显示)`, 'info');
|
|
2820
3156
|
for (const m of msgs) {
|
|
2821
|
-
|
|
3157
|
+
const isUser = m.type === 'user';
|
|
3158
|
+
// v3: 加 source 标签
|
|
3159
|
+
let tag = '';
|
|
3160
|
+
if (isUser) {
|
|
3161
|
+
if (m.source === 'remote') {
|
|
3162
|
+
tag = `<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">🌐 远端访客${m.fromPublicKey ? ' (' + m.fromPublicKey.substring(0, 8) + '…)' : ''} → A 的 channel</div>`;
|
|
3163
|
+
} else {
|
|
3164
|
+
tag = `<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">👤 A (内部 owner) → A 的 channel</div>`;
|
|
3165
|
+
}
|
|
3166
|
+
} else {
|
|
3167
|
+
tag = `<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">🤖 A 的 LLM (在 A 节点上跑)</div>`;
|
|
3168
|
+
}
|
|
3169
|
+
const bubble = document.createElement('div');
|
|
3170
|
+
const isRemoteUser = isUser && m.source === 'remote';
|
|
3171
|
+
bubble.style.cssText = `padding:8px 10px;margin:4px 0;border-radius:6px;font-size:13px;line-height:1.4;max-width:80%;word-wrap:break-word;${
|
|
3172
|
+
isUser
|
|
3173
|
+
? (isRemoteUser
|
|
3174
|
+
? 'background:#dbeafe;color:#1e3a8a;margin-right:auto;text-align:left;border:1px solid #93c5fd;'
|
|
3175
|
+
: 'background:#f3f4f6;color:#374151;margin-left:auto;text-align:left;border:1px solid #d1d5db;')
|
|
3176
|
+
: 'background:#e5e7eb;color:#111;'
|
|
3177
|
+
}`;
|
|
3178
|
+
bubble.innerHTML = tag + `<div>${escapeHtml(m.content || '')}</div>`;
|
|
3179
|
+
log.appendChild(bubble);
|
|
2822
3180
|
}
|
|
2823
3181
|
// 滚到底部
|
|
2824
3182
|
setTimeout(() => { log.scrollTop = log.scrollHeight; }, 50);
|
|
@@ -2855,6 +3213,8 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2855
3213
|
};
|
|
2856
3214
|
sendBtn.onclick = doSend;
|
|
2857
3215
|
inputEl.onkeydown = (e) => { if (e.key === 'Enter') doSend(); };
|
|
3216
|
+
// v3 新增: B 端远端 chat 也支持 @-autocomplete
|
|
3217
|
+
setupMentionAutocomplete(inputEl);
|
|
2858
3218
|
inputEl.focus();
|
|
2859
3219
|
startV3GlobalSSE();
|
|
2860
3220
|
|
|
@@ -3407,19 +3767,23 @@ function openAgentAddModal(existingChannel) {
|
|
|
3407
3767
|
if (existingChannel) {
|
|
3408
3768
|
agentAddTitle.textContent = '配置智能体:' + existingChannel.name;
|
|
3409
3769
|
agentAddName.value = existingChannel.name || '';
|
|
3410
|
-
agentAddName.readOnly =
|
|
3770
|
+
agentAddName.readOnly = false; // v3: 名字改成可编辑 (PATCH 支持更新 name)
|
|
3771
|
+
agentAddName.placeholder = '输入新名称';
|
|
3411
3772
|
agentAddWallet.value = existingChannel.walletAddress || '';
|
|
3412
3773
|
agentAddAutoTools.checked = !!existingChannel.autoInvokeTools;
|
|
3413
3774
|
agentAddConfirmBtn.dataset.mode = 'update';
|
|
3414
3775
|
agentAddConfirmBtn.dataset.channelId = existingChannel.id;
|
|
3776
|
+
agentAddConfirmBtn.dataset.originalName = existingChannel.name || '';
|
|
3415
3777
|
} else {
|
|
3416
3778
|
agentAddTitle.textContent = '添加智能体';
|
|
3417
3779
|
agentAddName.value = '';
|
|
3418
3780
|
agentAddName.readOnly = false;
|
|
3781
|
+
agentAddName.placeholder = '例如: 交易助手';
|
|
3419
3782
|
agentAddWallet.value = '';
|
|
3420
3783
|
agentAddAutoTools.checked = true;
|
|
3421
3784
|
agentAddConfirmBtn.dataset.mode = 'create';
|
|
3422
3785
|
delete agentAddConfirmBtn.dataset.channelId;
|
|
3786
|
+
delete agentAddConfirmBtn.dataset.originalName;
|
|
3423
3787
|
}
|
|
3424
3788
|
agentAddWalletInfo.style.display = 'none';
|
|
3425
3789
|
agentAddWalletInfo.innerHTML = '';
|
|
@@ -3531,13 +3895,16 @@ if (agentAddConfirmBtn) {
|
|
|
3531
3895
|
} else {
|
|
3532
3896
|
// update
|
|
3533
3897
|
const channelId = agentAddConfirmBtn.dataset.channelId;
|
|
3898
|
+
const originalName = agentAddConfirmBtn.dataset.originalName || '';
|
|
3899
|
+
// v3 新增: 名字改了才发 (没改就不发, 保持原状)
|
|
3900
|
+
const body = { walletAddress: walletAddress || null, autoInvokeTools };
|
|
3901
|
+
if (name && name !== originalName) {
|
|
3902
|
+
body.name = name;
|
|
3903
|
+
}
|
|
3534
3904
|
const res = await fetch(`/channels/${channelId}`, {
|
|
3535
3905
|
method: 'PATCH',
|
|
3536
3906
|
headers: { 'Content-Type': 'application/json' },
|
|
3537
|
-
body: JSON.stringify(
|
|
3538
|
-
walletAddress: walletAddress || null,
|
|
3539
|
-
autoInvokeTools
|
|
3540
|
-
})
|
|
3907
|
+
body: JSON.stringify(body)
|
|
3541
3908
|
});
|
|
3542
3909
|
if (!res.ok) throw new Error('update failed');
|
|
3543
3910
|
const updated = await res.json();
|