@bolloon/bolloon-agent 0.3.23 → 0.3.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.
@@ -60,6 +60,11 @@
60
60
  const state = stateMap.get(timelineEl);
61
61
  if (!state) return;
62
62
  const { steps, expanded, showAll } = state;
63
+ if (steps.length === 0) {
64
+ timelineEl.style.display = "none";
65
+ return;
66
+ }
67
+ timelineEl.style.display = "";
63
68
  const titleEl = timelineEl.querySelector("[data-current-tool]");
64
69
  if (titleEl) titleEl.textContent = computeTitle(steps);
65
70
  const dotsEl = timelineEl.querySelector("[data-dots]");
@@ -789,7 +794,8 @@
789
794
  hasStreamingText: () => hasStreamingText,
790
795
  injectRecoveredText: () => injectRecoveredText,
791
796
  replaceStreamingText: () => replaceStreamingText,
792
- resetRendererState: () => resetRendererState
797
+ resetRendererState: () => resetRendererState,
798
+ seedDedupState: () => seedDedupState
793
799
  });
794
800
  function hasStreamingText() {
795
801
  return streamingText.length > 0;
@@ -1000,11 +1006,44 @@
1000
1006
  container.appendChild(content);
1001
1007
  return container;
1002
1008
  }
1009
+ function normalizeMarkdownTables(text) {
1010
+ if (!text || !text.includes("|")) return text;
1011
+ const lines = text.split("\n");
1012
+ const out = [];
1013
+ let inTable = false;
1014
+ let inCode = false;
1015
+ for (let i = 0; i < lines.length; i++) {
1016
+ const line = lines[i];
1017
+ const trimmed = line.trim();
1018
+ if (/^\s*```/.test(line)) {
1019
+ inCode = !inCode;
1020
+ out.push(line);
1021
+ inTable = false;
1022
+ continue;
1023
+ }
1024
+ if (inCode) {
1025
+ out.push(line);
1026
+ continue;
1027
+ }
1028
+ if (trimmed.startsWith("|") || /^\|?\s*:?-{2,}\s*(\|\s*:?-{2,}\s*)*\|?\s*$/.test(trimmed)) {
1029
+ out.push(line);
1030
+ inTable = true;
1031
+ } else {
1032
+ if (inTable && out.length > 0 && out[out.length - 1].trim() !== "") {
1033
+ out.push("");
1034
+ }
1035
+ inTable = false;
1036
+ out.push(line);
1037
+ }
1038
+ }
1039
+ return out.join("\n");
1040
+ }
1003
1041
  function buildBubble(text, type) {
1004
1042
  const bubble = document.createElement("div");
1005
1043
  bubble.className = `bubble bubble-${type}`;
1006
1044
  const marked2 = window.marked;
1007
- bubble.innerHTML = marked2 ? marked2.parse(text) : escapeHtml(text);
1045
+ const normalized = marked2 ? normalizeMarkdownTables(text) : text;
1046
+ bubble.innerHTML = marked2 ? marked2.parse(normalized) : escapeHtml(text);
1008
1047
  return bubble;
1009
1048
  }
1010
1049
  function buildMessageActions(div, rawContent, ctx) {
@@ -1183,6 +1222,10 @@
1183
1222
  scrollToBottomTimer = null;
1184
1223
  }
1185
1224
  }
1225
+ function seedDedupState(lastType, lastContent) {
1226
+ if (lastType === "user") lastUserCommand = lastContent || "";
1227
+ else if (lastType === "ai") lastAiContent = lastContent || "";
1228
+ }
1186
1229
  var streamingMessageEl, streamingTextNode, streamingText, lastUserCommand, lastAiContent, stepEventBuffer, scrollToBottomTimer, MessageRenderer;
1187
1230
  var init_message_renderer = __esm({
1188
1231
  "src/web/ui/message-renderer.ts"() {
@@ -1204,7 +1247,8 @@
1204
1247
  flushStepEventBuffer,
1205
1248
  escapeHtml,
1206
1249
  getMessagesContainerForCurrent,
1207
- resetRendererState
1250
+ resetRendererState,
1251
+ seedDedupState
1208
1252
  };
1209
1253
  if (typeof window !== "undefined") {
1210
1254
  window.MR = MessageRenderer;
@@ -1463,6 +1507,7 @@
1463
1507
  var MR_hasStreamingText = () => _getMR().hasStreamingText?.() ?? false;
1464
1508
  var MR_replaceStreamingText = (text) => _getMR().replaceStreamingText?.(text);
1465
1509
  var MR_injectRecoveredText = (text, ctx) => _getMR().injectRecoveredText?.(text, ctx ?? getRendererCtx());
1510
+ var MR_seedDedupState = (lastType, lastContent) => _getMR().seedDedupState?.(lastType, lastContent);
1466
1511
  var knownToolNames = /* @__PURE__ */ new Set();
1467
1512
  function getRendererCtx() {
1468
1513
  return {
@@ -1661,6 +1706,29 @@
1661
1706
 
1662
1707
  `;
1663
1708
  addMessage2(prefix + (msg.text || "(\u7A7A\u56DE\u590D)"), "ai", false, log);
1709
+ if (msg.channelId && msg.fromPublicKey) {
1710
+ try {
1711
+ const key = `bolloon.rcmCache.${msg.fromPublicKey}.${msg.channelId}`;
1712
+ let arr = [];
1713
+ try {
1714
+ const raw = localStorage.getItem(key);
1715
+ if (raw) arr = JSON.parse(raw);
1716
+ } catch {
1717
+ arr = [];
1718
+ }
1719
+ if (!Array.isArray(arr)) arr = [];
1720
+ const entry = { type: "ai", content: msg.text || "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), source: "remote" };
1721
+ const dup = arr.some((m) => m.type === "ai" && m.content === entry.content);
1722
+ if (!dup) {
1723
+ arr.push(entry);
1724
+ try {
1725
+ localStorage.setItem(key, JSON.stringify(arr.slice(-200)));
1726
+ } catch {
1727
+ }
1728
+ }
1729
+ } catch {
1730
+ }
1731
+ }
1664
1732
  }
1665
1733
  log.scrollTop = log.scrollHeight;
1666
1734
  } else {
@@ -1701,6 +1769,28 @@
1701
1769
  thinkingEl.textContent = "\u{1F4AD} \u5BF9\u65B9\u6B63\u5728\u601D\u8003: " + (msg.partial || "").slice(-200);
1702
1770
  log.scrollTop = log.scrollHeight;
1703
1771
  }
1772
+ } else if (phase === "step") {
1773
+ const stepType = msg.stepType;
1774
+ if (stepType === "step_start" || stepType === "step_done" || stepType === "step_error") {
1775
+ handleStepEvent2({
1776
+ type: stepType,
1777
+ tool: msg.tool,
1778
+ content: msg.content,
1779
+ success: msg.success,
1780
+ output: msg.output,
1781
+ error: msg.error,
1782
+ args: msg.args
1783
+ });
1784
+ const thinkingEl = document.getElementById("rcm-thinking-live");
1785
+ if (thinkingEl && stepType === "step_start") {
1786
+ thinkingEl.textContent = `\u{1F527} \u5BF9\u65B9\u6B63\u5728\u8C03\u7528\u5DE5\u5177: ${msg.tool || "..."}`;
1787
+ } else if (thinkingEl && stepType === "step_done") {
1788
+ thinkingEl.textContent = `\u2705 \u5BF9\u65B9\u5DE5\u5177\u8C03\u7528\u5B8C\u6210: ${msg.tool || ""}`;
1789
+ } else if (thinkingEl && stepType === "step_error") {
1790
+ thinkingEl.textContent = `\u274C \u5BF9\u65B9\u5DE5\u5177\u8C03\u7528\u5931\u8D25: ${msg.tool || ""}`;
1791
+ }
1792
+ log.scrollTop = log.scrollHeight;
1793
+ }
1704
1794
  }
1705
1795
  } else if (msg.type === "cross-mention-received") {
1706
1796
  const allModals = document.querySelectorAll('.rcm-mention-toast, [id^="rcm-log"]');
@@ -1715,7 +1805,20 @@
1715
1805
  }
1716
1806
  } else if (msg.type === "remote-channel-update") {
1717
1807
  const peerId = msg.peerId;
1718
- const channels2 = msg.channels || [];
1808
+ let channels2 = msg.channels || [];
1809
+ const removedKey = `bolloon.removedRemoteChannels`;
1810
+ let removedSet = /* @__PURE__ */ new Set();
1811
+ try {
1812
+ removedSet = new Set(JSON.parse(localStorage.getItem(removedKey) || "[]"));
1813
+ } catch {
1814
+ }
1815
+ if (removedSet.size > 0) {
1816
+ const before = channels2.length;
1817
+ channels2 = channels2.filter((c) => !removedSet.has(`${peerId}::${c.id}`));
1818
+ if (channels2.length !== before) {
1819
+ console.log(`[v3] \u8FC7\u6EE4 ${before - channels2.length} \u4E2A\u5DF2\u5220\u9664\u7684\u8FDC\u7AEF channel (${peerId.substring(0, 8)}...)`);
1820
+ }
1821
+ }
1719
1822
  const peerName = msg.peerName || null;
1720
1823
  let group = remoteChannels.find((g) => g.peerId === peerId);
1721
1824
  if (!group) {
@@ -2217,6 +2320,7 @@
2217
2320
  }
2218
2321
  expandedAgents.add(channelId);
2219
2322
  console.log("[selectChannel] \u9891\u9053:", channel.name, "session:", currentSessionId);
2323
+ updateSendToolsToggleVisibility();
2220
2324
  } else {
2221
2325
  console.warn("[selectChannel] channel \u4E0D\u5B58\u5728:", channelId);
2222
2326
  if (channelNameEl) channelNameEl.textContent = safeChannelName("(channel \u5DF2\u5220\u9664)");
@@ -2261,6 +2365,10 @@
2261
2365
  frag.appendChild(tmpContainer.firstChild);
2262
2366
  }
2263
2367
  container.appendChild(frag);
2368
+ if (dedupedMsgs.length > 0) {
2369
+ const lastMsg = dedupedMsgs[dedupedMsgs.length - 1];
2370
+ MR_seedDedupState(lastMsg.type, lastMsg.content);
2371
+ }
2264
2372
  if (dedupedMsgs.length !== msgs.length) {
2265
2373
  console.log(`[loadSession] \u53BB\u91CD ${msgs.length - dedupedMsgs.length} \u6761\u76F8\u90BB\u91CD\u590D\u6D88\u606F (${msgs.length} \u2192 ${dedupedMsgs.length})`);
2266
2374
  }
@@ -2691,6 +2799,8 @@ ${data.error || "channel not found"}`, "error");
2691
2799
  channelDid,
2692
2800
  attachments: attachmentsForSend
2693
2801
  // 后端解析为 LLM contextHint
2802
+ // 2026-08-02: 本地对话不传 autoInvokeTools (工具开关只针对远程 P2P 对话),
2803
+ // 本地走 channel 自身 autoInvokeTools 配置
2694
2804
  })
2695
2805
  });
2696
2806
  if (!res.ok) {
@@ -2703,6 +2813,38 @@ ${data.error || "channel not found"}`, "error");
2703
2813
  setSendMode("idle");
2704
2814
  }
2705
2815
  }
2816
+ var sendToolsEnabled = true;
2817
+ try {
2818
+ const saved = localStorage.getItem("bolloon.sendToolsEnabled");
2819
+ if (saved !== null) sendToolsEnabled = saved === "1";
2820
+ } catch {
2821
+ }
2822
+ var sendToolsToggleBtn = document.getElementById("send-tools-toggle");
2823
+ var sendToolsLabel = document.getElementById("send-tools-label");
2824
+ function updateSendToolsToggleUI() {
2825
+ if (!sendToolsToggleBtn || !sendToolsLabel) return;
2826
+ sendToolsLabel.textContent = sendToolsEnabled ? "\u5DE5\u5177:\u5F00" : "\u5DE5\u5177:\u5173";
2827
+ sendToolsToggleBtn.style.borderColor = sendToolsEnabled ? "var(--accent, #4f46e5)" : "var(--border)";
2828
+ sendToolsToggleBtn.style.color = sendToolsEnabled ? "var(--accent, #4f46e5)" : "var(--text-muted)";
2829
+ try {
2830
+ localStorage.setItem("bolloon.sendToolsEnabled", sendToolsEnabled ? "1" : "0");
2831
+ } catch {
2832
+ }
2833
+ }
2834
+ function updateSendToolsToggleVisibility() {
2835
+ if (!sendToolsToggleBtn) return;
2836
+ const ch = channels.find((c) => c.id === currentChannelId);
2837
+ const isRemote = !!(ch && ch.ownerPublicKey) || !ch;
2838
+ sendToolsToggleBtn.style.display = isRemote ? "flex" : "none";
2839
+ }
2840
+ if (sendToolsToggleBtn) {
2841
+ sendToolsToggleBtn.onclick = () => {
2842
+ sendToolsEnabled = !sendToolsEnabled;
2843
+ updateSendToolsToggleUI();
2844
+ if (typeof showSimpleToast === "function") showSimpleToast(sendToolsEnabled ? "\u{1F527} \u672C\u6B21\u53D1\u9001\u5C06\u542F\u7528\u5DE5\u5177\u8C03\u7528" : "\u{1F527} \u672C\u6B21\u53D1\u9001\u5C06\u7981\u7528\u5DE5\u5177\u8C03\u7528");
2845
+ };
2846
+ updateSendToolsToggleUI();
2847
+ }
2706
2848
  function persistLastMessageToServer(type, content) {
2707
2849
  if (!currentChannelId || !currentSessionId) return;
2708
2850
  fetch(`/sessions/${currentChannelId}/${currentSessionId}`, {
@@ -2911,6 +3053,139 @@ ${data.error || "channel not found"}`, "error");
2911
3053
  }, true);
2912
3054
  refreshMentionChannels();
2913
3055
  setInterval(refreshMentionChannels, 5e3);
3056
+ var SLASH_COMMANDS = [
3057
+ { cmd: "plan", desc: "\u521B\u5EFA\u6267\u884C\u8BA1\u5212 (create_plan)", args: "\u76EE\u6807; \u6B65\u9AA41,\u6B65\u9AA42..." },
3058
+ { cmd: "todo", desc: "\u52FE\u9009\u8BA1\u5212\u6B65\u9AA4\u5B8C\u6210 (update_plan)", args: "\u8BA1\u5212ID; \u6B65\u9AA4ID; done/blocked" },
3059
+ { cmd: "review", desc: "\u5BA1\u67E5\u8BA1\u5212\u5B8C\u6210\u5EA6 (review_plan)", args: "\u8BA1\u5212ID; \u603B\u7ED3" },
3060
+ { cmd: "task", desc: "\u521B\u5EFA\u4EFB\u52A1 (create_task)", args: "\u63CF\u8FF0" },
3061
+ { cmd: "goal", desc: "\u6682\u505C\u76EE\u6807 (park_goal)", args: "\u76EE\u6807ID; \u539F\u56E0" },
3062
+ { cmd: "skill", desc: "\u6C89\u6DC0\u6280\u80FD (create_skill)", args: "\u6280\u80FD\u540D; \u63CF\u8FF0; \u6B65\u9AA4" },
3063
+ { cmd: "add-friend", desc: "\u6DFB\u52A0 P2P \u597D\u53CB", args: "\u516C\u94A5; \u5907\u6CE8" },
3064
+ { cmd: "help", desc: "\u663E\u793A\u53EF\u7528\u547D\u4EE4", args: "" }
3065
+ ];
3066
+ var slashDropdownEl = null;
3067
+ var slashHighlightIdx = 0;
3068
+ var slashAnchor = -1;
3069
+ var slashBlockEnd = -1;
3070
+ function getCurrentSlashQuery() {
3071
+ const pos = input.selectionStart || input.value.length;
3072
+ const before = input.value.slice(0, pos);
3073
+ const m = before.match(/\/([A-Za-z-]{0,20})$/);
3074
+ return m ? { query: m[1], anchor: pos - m[0].length } : null;
3075
+ }
3076
+ function closeSlashDropdown() {
3077
+ if (slashDropdownEl) {
3078
+ slashDropdownEl.remove();
3079
+ slashDropdownEl = null;
3080
+ }
3081
+ slashHighlightIdx = 0;
3082
+ slashAnchor = -1;
3083
+ slashBlockEnd = -1;
3084
+ }
3085
+ function applySlashCommand(cmdObj) {
3086
+ const anchor = slashAnchor;
3087
+ const blockEnd = slashBlockEnd >= 0 ? slashBlockEnd : anchor + 1 + (getCurrentSlashQuery()?.query || "").length;
3088
+ if (anchor < 0 || anchor > input.value.length || input.value[anchor] !== "/") {
3089
+ closeSlashDropdown();
3090
+ return;
3091
+ }
3092
+ const before = input.value.slice(0, anchor);
3093
+ const after = input.value.slice(blockEnd);
3094
+ const insert = `/${cmdObj.cmd} `;
3095
+ input.value = before + insert + after;
3096
+ const newPos = before.length + insert.length;
3097
+ input.focus();
3098
+ input.setSelectionRange(newPos, newPos);
3099
+ closeSlashDropdown();
3100
+ if (cmdObj.args && typeof showSimpleToast === "function") {
3101
+ showSimpleToast(`\u{1F4A1} /${cmdObj.cmd} \u7528\u6CD5: ${cmdObj.args}`);
3102
+ }
3103
+ }
3104
+ function renderSlashDropdown(items) {
3105
+ if (!slashDropdownEl) {
3106
+ slashDropdownEl = document.createElement("div");
3107
+ slashDropdownEl.id = "slash-dropdown";
3108
+ slashDropdownEl.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:280px;";
3109
+ document.body.appendChild(slashDropdownEl);
3110
+ }
3111
+ const rect = input.getBoundingClientRect();
3112
+ slashDropdownEl.style.left = rect.left + "px";
3113
+ slashDropdownEl.style.bottom = window.innerHeight - rect.top + 4 + "px";
3114
+ 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;">
3115
+ <span>\u26A1 \u547D\u4EE4 (\u56DE\u8F66\u9009\u4E2D \u2192 \u63D2\u5165\u8F93\u5165\u6846)</span>
3116
+ <span style="color:#9ca3af;">\u2191\u2193 \u79FB\u52A8 \xB7 Esc \u5173\u95ED</span>
3117
+ </div>`;
3118
+ if (items.length === 0) {
3119
+ slashDropdownEl.innerHTML = headerHtml + '<div style="padding:10px 12px;color:#6b7280;font-size:12px;">\u6CA1\u6709\u5339\u914D\u7684\u547D\u4EE4</div>';
3120
+ } else {
3121
+ const rows = items.map((c, i) => {
3122
+ const bg = i === slashHighlightIdx ? "#eff6ff" : "#fff";
3123
+ const borderLeft = i === slashHighlightIdx ? "3px solid #93c5fd" : "3px solid transparent";
3124
+ return `<div class="slash-item" data-idx="${i}" style="padding:8px 12px;cursor:pointer;background:${bg};border-bottom:1px solid #f3f4f6;display:flex;align-items:center;gap:8px;border-left:${borderLeft};">
3125
+ <span style="font-weight:600;color:#4f46e5;min-width:70px;">/${c.cmd}</span>
3126
+ <span style="flex:1;color:#374151;">${c.desc}</span>
3127
+ </div>`;
3128
+ }).join("");
3129
+ slashDropdownEl.innerHTML = headerHtml + rows;
3130
+ slashDropdownEl.querySelectorAll(".slash-item").forEach((el, i) => {
3131
+ el.onclick = () => applySlashCommand(items[i]);
3132
+ });
3133
+ }
3134
+ }
3135
+ function updateSlashDropdown() {
3136
+ const m = getCurrentSlashQuery();
3137
+ if (!m) {
3138
+ closeSlashDropdown();
3139
+ return;
3140
+ }
3141
+ if (slashAnchor === -1) slashAnchor = m.anchor;
3142
+ slashBlockEnd = m.anchor + 1 + m.query.length;
3143
+ const q = m.query.toLowerCase();
3144
+ const filtered = SLASH_COMMANDS.filter((c) => c.cmd.startsWith(q)).slice(0, 8);
3145
+ if (slashHighlightIdx >= filtered.length) slashHighlightIdx = 0;
3146
+ renderSlashDropdown(filtered);
3147
+ }
3148
+ var _origInputHandler = input.oninput;
3149
+ input.addEventListener("input", () => {
3150
+ const pos = input.selectionStart || input.value.length;
3151
+ const before = input.value.slice(0, pos);
3152
+ if (before.endsWith("/") || slashDropdownEl && getCurrentSlashQuery()) {
3153
+ updateSlashDropdown();
3154
+ } else if (!getCurrentMentionQuery()) {
3155
+ closeSlashDropdown();
3156
+ }
3157
+ });
3158
+ input.addEventListener("keydown", (e) => {
3159
+ if (!slashDropdownEl) return;
3160
+ const items = slashDropdownEl.querySelectorAll(".slash-item");
3161
+ if (e.key === "ArrowDown") {
3162
+ e.preventDefault();
3163
+ e.stopPropagation();
3164
+ if (items.length === 0) return;
3165
+ slashHighlightIdx = (slashHighlightIdx + 1) % items.length;
3166
+ const q = (getCurrentSlashQuery()?.query || "").toLowerCase();
3167
+ updateSlashDropdown();
3168
+ } else if (e.key === "ArrowUp") {
3169
+ e.preventDefault();
3170
+ e.stopPropagation();
3171
+ if (items.length === 0) return;
3172
+ slashHighlightIdx = (slashHighlightIdx - 1 + items.length) % items.length;
3173
+ updateSlashDropdown();
3174
+ } else if (e.key === "Enter" || e.key === "Tab") {
3175
+ if (items.length > 0) {
3176
+ e.preventDefault();
3177
+ e.stopPropagation();
3178
+ const q = (getCurrentSlashQuery()?.query || "").toLowerCase();
3179
+ const filtered = SLASH_COMMANDS.filter((c) => c.cmd.startsWith(q)).slice(0, 8);
3180
+ const cur = filtered[slashHighlightIdx];
3181
+ if (cur) applySlashCommand(cur);
3182
+ }
3183
+ } else if (e.key === "Escape") {
3184
+ e.preventDefault();
3185
+ e.stopPropagation();
3186
+ closeSlashDropdown();
3187
+ }
3188
+ }, true);
2914
3189
  function setupMentionAutocomplete(inputEl) {
2915
3190
  if (!inputEl || inputEl.__mentionBound) return;
2916
3191
  inputEl.__mentionBound = true;
@@ -3395,6 +3670,53 @@ ${data.error || "channel not found"}`, "error");
3395
3670
  if (nameEl) nameEl.textContent = identity.name || "\u533F\u540D";
3396
3671
  if (didEl) didEl.textContent = identity.didShort ? `did:key:${identity.didShort}` : "";
3397
3672
  if (letterEl) letterEl.textContent = letter;
3673
+ if (nameEl) {
3674
+ nameEl.onclick = () => {
3675
+ const current = nameEl.textContent || "";
3676
+ const input2 = document.createElement("input");
3677
+ input2.type = "text";
3678
+ input2.value = current;
3679
+ input2.maxLength = 40;
3680
+ input2.style.cssText = "width:100%;background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:12px;padding:2px 6px;";
3681
+ nameEl.replaceWith(input2);
3682
+ input2.focus();
3683
+ input2.select();
3684
+ const commit = async (save) => {
3685
+ const newName = input2.value.trim();
3686
+ if (save && newName && newName !== current) {
3687
+ try {
3688
+ const r = await fetch("/api/user/identity", {
3689
+ method: "PUT",
3690
+ headers: { "Content-Type": "application/json" },
3691
+ body: JSON.stringify({ name: newName })
3692
+ });
3693
+ if (r.ok) {
3694
+ const updated = await r.json();
3695
+ nameEl.textContent = updated.name || newName;
3696
+ const letterEl2 = document.getElementById("avatar-letter");
3697
+ if (letterEl2) letterEl2.textContent = (updated.name || "?").charAt(0).toUpperCase();
3698
+ if (typeof showSimpleToast === "function") showSimpleToast("\u2713 \u540D\u5B57\u5DF2\u66F4\u65B0");
3699
+ }
3700
+ } catch (e) {
3701
+ }
3702
+ }
3703
+ if (!input2.isConnected) return;
3704
+ nameEl.textContent = input2.value.trim() || current;
3705
+ input2.replaceWith(nameEl);
3706
+ };
3707
+ input2.onblur = () => commit(true);
3708
+ input2.onkeydown = (e) => {
3709
+ if (e.key === "Enter") {
3710
+ e.preventDefault();
3711
+ input2.blur();
3712
+ }
3713
+ if (e.key === "Escape") {
3714
+ input2.blur();
3715
+ commit(false);
3716
+ }
3717
+ };
3718
+ };
3719
+ }
3398
3720
  } catch (e) {
3399
3721
  }
3400
3722
  }
@@ -4368,13 +4690,24 @@ ${data.error || "channel not found"}`, "error");
4368
4690
  style="background:transparent;border:1px solid var(--border);color:var(--text);cursor:pointer;width:22px;height:22px;border-radius:4px;font-size:12px;line-height:1;padding:0;display:flex;align-items:center;justify-content:center;flex:0 0 auto;">\u{1F4E4}</button>
4369
4691
  </div>
4370
4692
  <div class="remote-peer-channels" style="margin-top:4px;margin-left:8px;">
4371
- ${peerChannels.length === 0 ? '<div style="font-size:10px;color:var(--text-muted);padding:2px 4px;">(\u5BF9\u65B9\u8FD8\u6CA1\u5206\u4EAB channel \u7ED9\u4F60)</div>' : peerChannels.map((c) => `
4372
- <div class="remote-channel-row" data-peer-id="${escapeHtml2(peer.publicKey)}" data-channel-id="${escapeHtml2(c.id)}"
4373
- style="display:flex;align-items:center;gap:6px;padding:4px 6px;cursor:pointer;border-radius:4px;font-size:12px;">
4374
- <span>\u{1F916}</span>
4375
- <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml2(safeChannelName(c.name, ""))}">${escapeHtml2(safeChannelName(c.name))}</span>
4376
- </div>
4377
- `).join("")}
4693
+ ${peerChannels.length === 0 ? '<div style="font-size:10px;color:var(--text-muted);padding:2px 4px;">(\u5BF9\u65B9\u8FD8\u6CA1\u5206\u4EAB channel \u7ED9\u4F60)</div>' : (() => {
4694
+ let removedSet = /* @__PURE__ */ new Set();
4695
+ try {
4696
+ removedSet = new Set(JSON.parse(localStorage.getItem("bolloon.removedRemoteChannels") || "[]"));
4697
+ } catch {
4698
+ }
4699
+ const visible = peerChannels.filter((c) => !removedSet.has(`${peer.publicKey}::${c.id}`));
4700
+ if (visible.length === 0) return '<div style="font-size:10px;color:var(--text-muted);padding:2px 4px;">(\u5DF2\u5168\u90E8\u79FB\u9664)</div>';
4701
+ return visible.map((c) => `
4702
+ <div class="remote-channel-row" data-peer-id="${escapeHtml2(peer.publicKey)}" data-channel-id="${escapeHtml2(c.id)}"
4703
+ style="display:flex;align-items:center;gap:6px;padding:4px 6px;cursor:pointer;border-radius:4px;font-size:12px;">
4704
+ <span>\u{1F916}</span>
4705
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml2(safeChannelName(c.name, ""))}">${escapeHtml2(safeChannelName(c.name))}</span>
4706
+ <button class="remote-channel-del" data-peer-id="${escapeHtml2(peer.publicKey)}" data-channel-id="${escapeHtml2(c.id)}" title="\u4ECE\u672C\u5730\u79FB\u9664 (\u4E0D\u518D\u663E\u793A\u8BE5\u8FDC\u7AEF channel)"
4707
+ style="background:transparent;border:1px solid var(--border);color:var(--text-muted);cursor:pointer;width:20px;height:20px;border-radius:4px;font-size:11px;line-height:1;padding:0;display:flex;align-items:center;justify-content:center;flex:0 0 auto;">\u{1F5D1}\uFE0F</button>
4708
+ </div>
4709
+ `).join("");
4710
+ })()}
4378
4711
  </div>
4379
4712
  </li>
4380
4713
  `;
@@ -4396,6 +4729,33 @@ ${data.error || "channel not found"}`, "error");
4396
4729
  openRemoteChannelChat(peerId, channelId, channelName);
4397
4730
  });
4398
4731
  });
4732
+ list.querySelectorAll(".remote-channel-del").forEach((btn) => {
4733
+ btn.addEventListener("click", (e) => {
4734
+ e.stopPropagation();
4735
+ const peerId = btn.dataset.peerId;
4736
+ const channelId = btn.dataset.channelId;
4737
+ try {
4738
+ const key = "bolloon.removedRemoteChannels";
4739
+ let arr = [];
4740
+ try {
4741
+ arr = JSON.parse(localStorage.getItem(key) || "[]");
4742
+ } catch {
4743
+ arr = [];
4744
+ }
4745
+ if (!Array.isArray(arr)) arr = [];
4746
+ const entry = `${peerId}::${channelId}`;
4747
+ if (!arr.includes(entry)) arr.push(entry);
4748
+ localStorage.setItem(key, JSON.stringify(arr));
4749
+ } catch {
4750
+ }
4751
+ const group = remoteChannels.find((g) => g.peerId === peerId);
4752
+ if (group && Array.isArray(group.channels)) {
4753
+ group.channels = group.channels.filter((c) => c.id !== channelId);
4754
+ }
4755
+ renderRemoteChannels();
4756
+ showSimpleToast("\u{1F5D1}\uFE0F \u5DF2\u4ECE\u672C\u5730\u79FB\u9664\u8BE5\u8FDC\u7AEF channel (\u5BF9\u65B9\u91CD\u65B0\u5206\u4EAB\u540E\u4F1A\u518D\u6B21\u51FA\u73B0, \u9664\u975E\u5237\u65B0\u540E\u4ECD\u88AB\u8FC7\u6EE4)");
4757
+ });
4758
+ });
4399
4759
  list.querySelectorAll(".remote-peer-header").forEach((row) => {
4400
4760
  const shareBtn = row.querySelector(".peer-share-btn");
4401
4761
  if (shareBtn) {
@@ -4465,6 +4825,7 @@ ${data.error || "channel not found"}`, "error");
4465
4825
  style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-main);color:var(--text);font-family:inherit;font-size:13px;box-sizing:border-box;resize:vertical;">${escapeHtml2(currentNotes)}</textarea>
4466
4826
  </div>
4467
4827
  <div class="friend-req-actions">
4828
+ <button id="epm-delete" class="friend-req-btn-deny" style="border-color:var(--danger,#e05d5d);color:var(--danger,#e05d5d);margin-right:auto;">\u{1F5D1}\uFE0F \u5220\u9664\u597D\u53CB</button>
4468
4829
  <button id="epm-cancel" class="friend-req-btn-deny">\u53D6\u6D88</button>
4469
4830
  <button id="epm-save" class="friend-req-btn-accept">\u4FDD\u5B58</button>
4470
4831
  </div>
@@ -4474,6 +4835,30 @@ ${data.error || "channel not found"}`, "error");
4474
4835
  document.body.insertAdjacentHTML("beforeend", html);
4475
4836
  const close = () => document.getElementById("edit-peer-modal")?.remove();
4476
4837
  document.getElementById("epm-cancel").onclick = close;
4838
+ document.getElementById("epm-delete").onclick = async () => {
4839
+ if (!confirm(`\u786E\u5B9A\u5220\u9664\u597D\u53CB "${currentName}" \u5417\uFF1F
4840
+ \u5BF9\u65B9\u5206\u4EAB\u7ED9\u4F60\u7684 channel \u5C06\u4E0D\u518D\u663E\u793A\uFF0C\u4F60\u5206\u4EAB\u7ED9\u5BF9\u65B9\u7684 channel \u4E5F\u4F1A\u64A4\u56DE\u3002`)) return;
4841
+ try {
4842
+ const delKey = peerPublicKey || peerName;
4843
+ const r = await fetch(`/api/p2p-peers/${encodeURIComponent(delKey)}`, { method: "DELETE" });
4844
+ if (!r.ok) {
4845
+ const data = await r.json().catch(() => ({}));
4846
+ throw new Error(data.error || `HTTP ${r.status}`);
4847
+ }
4848
+ console.log("[v3] \u5220\u9664\u597D\u53CB\u6210\u529F:", currentName);
4849
+ showSimpleToast(`\u2705 \u5DF2\u5220\u9664 ${currentName}`);
4850
+ close();
4851
+ const r2 = await fetch("/api/p2p-peers");
4852
+ if (r2.ok) {
4853
+ const d2 = await r2.json();
4854
+ knownPeers = Array.isArray(d2.peers) ? d2.peers : [];
4855
+ }
4856
+ renderRemoteChannels();
4857
+ } catch (err) {
4858
+ console.error("[v3] \u5220\u9664\u597D\u53CB\u5931\u8D25:", err);
4859
+ alert("\u5220\u9664\u5931\u8D25: " + (err.message || err));
4860
+ }
4861
+ };
4477
4862
  document.getElementById("epm-save").onclick = async () => {
4478
4863
  const newName = document.getElementById("epm-name").value.trim() || currentName;
4479
4864
  const newNotes = document.getElementById("epm-notes").value;
@@ -4601,6 +4986,10 @@ ${data.error || "channel not found"}`, "error");
4601
4986
  </div>
4602
4987
  <div id="rcm-log" class="messages remote-chat-log"></div>
4603
4988
  <div class="remote-chat-input-row">
4989
+ <button id="rcm-tools-toggle" class="remote-chat-tools-toggle" title="\u672C\u6B21\u53D1\u9001\u662F\u5426\u5141\u8BB8\u5BF9\u65B9\u8C03\u7528\u5DE5\u5177 (\u70B9\u51FB\u5207\u6362)"
4990
+ style="display:flex;align-items:center;gap:4px;background:transparent;border:1px solid var(--border,#444);color:var(--text-muted,#909088);border-radius:6px;padding:5px 9px;font-size:11px;cursor:pointer;white-space:nowrap;flex-shrink:0;">
4991
+ \u{1F527} <span id="rcm-tools-label">\u5DE5\u5177:\u5F00</span>
4992
+ </button>
4604
4993
  <input id="rcm-input" type="text" placeholder="\u8F93\u5165\u6D88\u606F, \u53D1\u9001\u5230\u8FDC\u7AEF channel..." class="remote-chat-input">
4605
4994
  <button id="rcm-send" class="remote-chat-btn-send">\u53D1\u9001</button>
4606
4995
  </div>
@@ -4613,6 +5002,16 @@ ${data.error || "channel not found"}`, "error");
4613
5002
  const sendBtn2 = document.getElementById("rcm-send");
4614
5003
  const thinkingEl = document.getElementById("rcm-thinking");
4615
5004
  let historyRefreshTimer = null;
5005
+ const overlayEl = document.getElementById("remote-chat-modal");
5006
+ overlayEl.addEventListener("mousedown", (e) => {
5007
+ if (e.target === overlayEl) {
5008
+ if (historyRefreshTimer) {
5009
+ clearInterval(historyRefreshTimer);
5010
+ historyRefreshTimer = null;
5011
+ }
5012
+ overlayEl.remove();
5013
+ }
5014
+ });
4616
5015
  document.getElementById("rcm-close").onclick = () => {
4617
5016
  if (historyRefreshTimer) {
4618
5017
  clearInterval(historyRefreshTimer);
@@ -4714,11 +5113,22 @@ ${data.error || "channel not found"}`, "error");
4714
5113
  log.scrollTop = log.scrollHeight;
4715
5114
  }, 50);
4716
5115
  }
5116
+ try {
5117
+ const cacheMsgs = msgs.map((m) => ({
5118
+ type: m.type === "user" ? "user" : "ai",
5119
+ content: m.content || "",
5120
+ timestamp: m.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
5121
+ source: m.source || "remote"
5122
+ }));
5123
+ writeRcmCache(cacheMsgs);
5124
+ } catch {
5125
+ }
4717
5126
  }
4718
5127
  const doSend = async () => {
4719
5128
  const text = inputEl.value.trim();
4720
5129
  if (!text) return;
4721
5130
  append(text, "user");
5131
+ cacheRemoteMessage(peerPublicKey, channelId, { type: "user", content: text, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
4722
5132
  inputEl.value = "";
4723
5133
  sendBtn2.disabled = true;
4724
5134
  sendBtn2.textContent = "...";
@@ -4726,7 +5136,8 @@ ${data.error || "channel not found"}`, "error");
4726
5136
  const res = await fetch("/api/remote-channels/chat-send", {
4727
5137
  method: "POST",
4728
5138
  headers: { "Content-Type": "application/json" },
4729
- body: JSON.stringify({ targetPublicKey: peerPublicKey, channelId, text })
5139
+ // 2026-08-02: 透传工具开关 (P2P 🔧 toggle, 只对本次远端消息生效)
5140
+ body: JSON.stringify({ targetPublicKey: peerPublicKey, channelId, text, autoInvokeTools: rcmToolsEnabled })
4730
5141
  });
4731
5142
  const data = await res.json();
4732
5143
  if (!res.ok) throw new Error(data.error || "send failed");
@@ -4744,7 +5155,94 @@ ${data.error || "channel not found"}`, "error");
4744
5155
  setupMentionAutocomplete(inputEl);
4745
5156
  inputEl.focus();
4746
5157
  startV3GlobalSSE();
4747
- loadHistory(false);
5158
+ let rcmToolsEnabled = true;
5159
+ try {
5160
+ const saved = localStorage.getItem("bolloon.rcmToolsEnabled");
5161
+ if (saved !== null) rcmToolsEnabled = saved === "1";
5162
+ } catch {
5163
+ }
5164
+ const rcmToolsBtn = document.getElementById("rcm-tools-toggle");
5165
+ const rcmToolsLabel = document.getElementById("rcm-tools-label");
5166
+ function updateRcmToolsUI() {
5167
+ if (!rcmToolsLabel) return;
5168
+ rcmToolsLabel.textContent = rcmToolsEnabled ? "\u5DE5\u5177:\u5F00" : "\u5DE5\u5177:\u5173";
5169
+ if (rcmToolsBtn) {
5170
+ rcmToolsBtn.style.borderColor = rcmToolsEnabled ? "#4f46e5" : "var(--border,#444)";
5171
+ rcmToolsBtn.style.color = rcmToolsEnabled ? "#4f46e5" : "var(--text-muted,#909088)";
5172
+ }
5173
+ try {
5174
+ localStorage.setItem("bolloon.rcmToolsEnabled", rcmToolsEnabled ? "1" : "0");
5175
+ } catch {
5176
+ }
5177
+ }
5178
+ if (rcmToolsBtn) {
5179
+ rcmToolsBtn.onclick = () => {
5180
+ rcmToolsEnabled = !rcmToolsEnabled;
5181
+ updateRcmToolsUI();
5182
+ };
5183
+ updateRcmToolsUI();
5184
+ }
5185
+ const rcmCacheKey = `bolloon.rcmCache.${peerPublicKey}.${channelId}`;
5186
+ const MAX_CACHE_MSGS = 200;
5187
+ function readRcmCache() {
5188
+ try {
5189
+ const raw = localStorage.getItem(rcmCacheKey);
5190
+ if (!raw) return [];
5191
+ const arr = JSON.parse(raw);
5192
+ return Array.isArray(arr) ? arr : [];
5193
+ } catch {
5194
+ return [];
5195
+ }
5196
+ }
5197
+ function writeRcmCache(msgs) {
5198
+ try {
5199
+ const trimmed = Array.isArray(msgs) ? msgs.slice(-MAX_CACHE_MSGS) : [];
5200
+ localStorage.setItem(rcmCacheKey, JSON.stringify(trimmed));
5201
+ } catch {
5202
+ }
5203
+ }
5204
+ function cacheRemoteMessage(pk, chId, msg) {
5205
+ const key = `bolloon.rcmCache.${pk}.${chId}`;
5206
+ let arr = [];
5207
+ try {
5208
+ const raw = localStorage.getItem(key);
5209
+ if (raw) arr = JSON.parse(raw);
5210
+ } catch {
5211
+ arr = [];
5212
+ }
5213
+ if (!Array.isArray(arr)) arr = [];
5214
+ const dup = arr.some((m) => m.type === msg.type && m.content === msg.content && m.timestamp === msg.timestamp);
5215
+ if (!dup) {
5216
+ arr.push(msg);
5217
+ try {
5218
+ localStorage.setItem(key, JSON.stringify(arr.slice(-MAX_CACHE_MSGS)));
5219
+ } catch {
5220
+ }
5221
+ }
5222
+ }
5223
+ const cached = readRcmCache();
5224
+ if (cached.length > 0) {
5225
+ log.innerHTML = "";
5226
+ for (const m of cached) {
5227
+ const type = m.type === "user" ? "user" : "ai";
5228
+ let prefix = "";
5229
+ if (m.type === "user") {
5230
+ prefix = m.source === "remote" ? `\u{1F310} \u8FDC\u7AEF\u8BBF\u5BA2
5231
+
5232
+ ` : "";
5233
+ } else {
5234
+ prefix = m.source === "remote" ? `\u{1F916} \u8FDC\u7AEF LLM
5235
+
5236
+ ` : "";
5237
+ }
5238
+ addMessage2(prefix + (m.content || ""), type, false, log, [], m.timestamp);
5239
+ }
5240
+ thinkingEl.style.display = "none";
5241
+ loadHistory(true);
5242
+ log.scrollTop = log.scrollHeight;
5243
+ } else {
5244
+ loadHistory(false);
5245
+ }
4748
5246
  historyRefreshTimer = setInterval(() => loadHistory(true), 15e3);
4749
5247
  }
4750
5248
  var showMyIdBtn = document.getElementById("show-my-p2p-id-btn");
@@ -4844,6 +5342,9 @@ ${data.error || "channel not found"}`, "error");
4844
5342
  <label style="display:block;margin-bottom:6px;font-size:12px;color:var(--text-secondary);">\u7533\u8BF7\u6D88\u606F\uFF08\u53EF\u9009\uFF09</label>
4845
5343
  <input id="afm-msg" type="text" value="\u60F3\u52A0\u4F60\u4E3A P2P \u597D\u53CB, \u5171\u4EAB channel \u534F\u4F5C"
4846
5344
  style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-main);color:var(--text);font-family:inherit;font-size:13px;box-sizing:border-box;margin-bottom:12px;">
5345
+ <label style="display:block;margin-bottom:6px;font-size:12px;color:var(--text-secondary);">\u5907\u6CE8\uFF08\u81EA\u6211\u4ECB\u7ECD/\u6765\u6E90, \u5BF9\u65B9\u63A5\u53D7\u65F6\u4F1A\u770B\u5230, \u4FBF\u4E8E\u5206\u8FA8\uFF09</label>
5346
+ <textarea id="afm-note" rows="2" placeholder="\u5982: \u6211\u662F\u5C0F\u5251\u7684 Bolloon, \u6765\u81EA\u676D\u5DDE, \u60F3\u4E00\u8D77\u505A P2P \u667A\u80FD\u4F53\u534F\u4F5C\u6D4B\u8BD5"
5347
+ style="width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-main);color:var(--text);font-family:inherit;font-size:13px;box-sizing:border-box;resize:vertical;margin-bottom:12px;"></textarea>
4847
5348
  <div id="afm-status" style="display:none;font-size:12px;margin-bottom:8px;"></div>
4848
5349
  <p style="margin:0;color:var(--text-muted);font-size:11px;">\u5BF9\u65B9\u9700\u5DF2\u542F\u52A8 Bolloon \u5E76\u5728\u7EBF. \u63A5\u53D7\u540E\u53CC\u65B9\u4E92\u52A0\u597D\u53CB, \u5BF9\u65B9\u5206\u4EAB\u7684 channel \u4F1A\u81EA\u52A8\u51FA\u73B0.</p>
4849
5350
  </div>
@@ -4868,6 +5369,7 @@ ${data.error || "channel not found"}`, "error");
4868
5369
  const name = document.getElementById("afm-name").value.trim();
4869
5370
  const publicKey = document.getElementById("afm-pk").value.trim();
4870
5371
  const message = document.getElementById("afm-msg").value.trim();
5372
+ const note = document.getElementById("afm-note")?.value.trim() || "";
4871
5373
  if (!publicKey) {
4872
5374
  setStatus("\u8BF7\u7C98\u8D34\u5BF9\u65B9\u7684 publicKey", "#b91c1c");
4873
5375
  return;
@@ -4886,7 +5388,7 @@ ${data.error || "channel not found"}`, "error");
4886
5388
  const res = await fetch("/api/friend-request", {
4887
5389
  method: "POST",
4888
5390
  headers: { "Content-Type": "application/json" },
4889
- body: JSON.stringify({ targetPublicKey: publicKey, name: name || void 0, message: message || void 0 })
5391
+ body: JSON.stringify({ targetPublicKey: publicKey, name: name || void 0, message: message || void 0, note: note || void 0 })
4890
5392
  });
4891
5393
  const data = await res.json();
4892
5394
  if (res.status === 502) {
@@ -4925,6 +5427,7 @@ ${data.error || "channel not found"}`, "error");
4925
5427
  }
4926
5428
  function showFriendRequestModal(req) {
4927
5429
  document.getElementById("friend-request-modal")?.remove();
5430
+ const reqNote = req.note || req.message || "";
4928
5431
  const html = `
4929
5432
  <div id="friend-request-modal" class="friend-req-overlay">
4930
5433
  <div class="friend-req-shell">
@@ -4936,12 +5439,12 @@ ${data.error || "channel not found"}`, "error");
4936
5439
  </div>
4937
5440
  </div>
4938
5441
  <div class="friend-req-body">
4939
- <p style="margin:0 0 8px;">${escapeHtml2(req.message || "\u60F3\u52A0\u4F60\u4E3A P2P \u597D\u53CB")}</p>
5442
+ ${reqNote ? `<div style="margin:0 0 10px;padding:8px 10px;background:var(--bg-active);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text);white-space:pre-wrap;word-break:break-word;">\u{1F4AC} ${escapeHtml2(reqNote)}</div>` : ""}
4940
5443
  <p style="margin:0;color:var(--text-muted);font-size:11px;">\u63A5\u53D7\u540E: \u53CC\u65B9\u4E92\u52A0\u597D\u53CB, \u5BF9\u65B9\u5206\u4EAB\u7684 channel \u4F1A\u81EA\u52A8\u51FA\u73B0\u5728 P2P \u597D\u53CB\u533A.</p>
4941
5444
  </div>
4942
5445
  <div class="friend-req-actions">
4943
5446
  <button id="frm-deny" class="friend-req-btn-deny">\u62D2\u7EDD</button>
4944
- <button id="frm-accept" class="friend-req-btn-accept">\u63A5\u53D7</button>
5447
+ <button id="frm-accept" class="friend-req-btn-accept" style="font-weight:700;">\u2705 \u4E00\u952E\u901A\u8FC7</button>
4945
5448
  </div>
4946
5449
  </div>
4947
5450
  </div>
@@ -4955,7 +5458,7 @@ ${data.error || "channel not found"}`, "error");
4955
5458
  const res = await fetch("/api/friend-accept", {
4956
5459
  method: "POST",
4957
5460
  headers: { "Content-Type": "application/json" },
4958
- body: JSON.stringify({ fromPublicKey: req.fromPublicKey, name: req.fromName })
5461
+ body: JSON.stringify({ fromPublicKey: req.fromPublicKey, name: req.fromName, requestId: req.requestId })
4959
5462
  });
4960
5463
  const data = await res.json();
4961
5464
  if (!res.ok) throw new Error(data.error || "accept failed");