@bolloon/bolloon-agent 0.6.4 → 0.6.5

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.
@@ -3111,6 +3111,8 @@ ${data.error || "channel not found"}`, "error");
3111
3111
  }, 1e4);
3112
3112
  eventSource.onerror = () => {
3113
3113
  console.warn("[SSE] \u9519\u8BEF, \u6D4F\u89C8\u5668\u81EA\u52A8\u91CD\u8FDE\u4E2D:", targetChannelId, "readyState=", eventSource.readyState);
3114
+ hideLoopStatusBar2();
3115
+ setSendMode("idle");
3114
3116
  if (eventSource.readyState === EventSource.CLOSED) {
3115
3117
  clearInterval(heartbeatTimer);
3116
3118
  eventSources.delete(targetChannelId);
@@ -3263,10 +3265,16 @@ ${data.error || "channel not found"}`, "error");
3263
3265
  })
3264
3266
  });
3265
3267
  if (!res.ok) {
3266
- addMessage2("\u53D1\u9001\u5931\u8D25", "ai");
3268
+ hideLoopStatusBar2();
3269
+ const errBody = await res.json().catch(() => ({}));
3270
+ const reason = errBody?.error || errBody?.message || `HTTP ${res.status}`;
3271
+ addMessage2(`\u26A0\uFE0F \u53D1\u9001\u5931\u8D25: ${reason}`, "ai");
3267
3272
  setSendMode("idle");
3273
+ if (typeof showSimpleToast === "function") showSimpleToast("\u26A0\uFE0F " + String(reason).slice(0, 200));
3274
+ return;
3268
3275
  }
3269
3276
  } catch (err) {
3277
+ hideLoopStatusBar2();
3270
3278
  addMessage2("\u8FDE\u63A5\u9519\u8BEF", "ai");
3271
3279
  console.error("Send error", err);
3272
3280
  setSendMode("idle");
@@ -4994,27 +5002,16 @@ ${data.error || "channel not found"}`, "error");
4994
5002
  }
4995
5003
  var worldView = document.getElementById("world-view");
4996
5004
  var worldBtn = document.getElementById("world-btn");
4997
- var worldViewClose = document.getElementById("world-view-close");
4998
5005
  var worldRefreshBtn = document.getElementById("world-refresh-btn");
4999
- var worldIntentInput = document.getElementById("world-intent-input");
5000
- var worldIntentPriority = document.getElementById("world-intent-priority");
5001
- var worldIntentBudget = document.getElementById("world-intent-budget");
5002
- var worldIntentSet = document.getElementById("world-intent-set");
5003
- var worldError = document.getElementById("world-intent-error");
5004
5006
  var worldIntentsList = document.getElementById("world-intents-list");
5005
5007
  var worldOpportunities = document.getElementById("world-opportunities");
5006
5008
  var worldStats = document.getElementById("world-stats");
5007
5009
  var worldBadge = document.getElementById("world-badge");
5008
- var worldProfileInput = document.getElementById("world-profile-input");
5009
- var worldProfileSet = document.getElementById("world-profile-set");
5010
5010
  var worldLoaded = false;
5011
5011
  var worldPollTimer = null;
5012
5012
  var WORLD_POLL_MS = 3e4;
5013
5013
  function worldStatus(msg) {
5014
- if (worldError) {
5015
- worldError.textContent = msg || "";
5016
- worldError.style.display = msg ? "" : "none";
5017
- }
5014
+ if (msg) console.info("[world]", msg);
5018
5015
  }
5019
5016
  async function fetchWorldIntents() {
5020
5017
  try {
@@ -5035,7 +5032,7 @@ ${data.error || "channel not found"}`, "error");
5035
5032
  function renderWorldIntents(intents) {
5036
5033
  if (!worldIntentsList) return;
5037
5034
  if (!intents.length) {
5038
- worldIntentsList.innerHTML = '<div class="form-hint" style="font-size:11px;">(\u8FD8\u6CA1\u6709\u610F\u56FE \u2014 \u53F3\u8FB9\u58F0\u660E\u4E00\u4E2A)</div>';
5035
+ worldIntentsList.innerHTML = '<div class="form-hint" style="font-size:11px;">(\u8FD8\u6CA1\u6709\u610F\u56FE \u2014 \u5728\u5E95\u90E8\u8F93\u5165\u6846\u544A\u8BC9\u4E16\u754C\u4F60\u5728\u505A\u4EC0\u4E48)</div>';
5039
5036
  return;
5040
5037
  }
5041
5038
  const active = intents.filter((i) => i.status === "active");
@@ -5082,12 +5079,30 @@ ${data.error || "channel not found"}`, "error");
5082
5079
  }
5083
5080
  const cards = opps.map((o) => {
5084
5081
  const pct = Math.round(o.score * 100);
5085
- return `<div class="world-opp-card" style="border:1px solid var(--border,#3a3a36);border-radius:12px;padding:12px 14px;margin-bottom:10px;background:var(--bg-card,#222220);">
5082
+ const inboxLine = o.inbox ? `<div style="font-size:11px;color:#8a8a80;margin-top:6px;border-top:1px dashed var(--border,#3a3a36);padding-top:6px;">
5083
+ \u2709 \u5916\u90E8\u6295\u9012 \xB7 \u6765\u81EA <span style="color:#c4d640;">${String(o.inbox.providerName || o.inbox.providerDid || "").slice(0, 40)}</span>
5084
+ \xB7 <span style="color:#6aaa6a;">\u2713 \u9A8C\u7B7E\u901A\u8FC7</span>
5085
+ ${(o.inbox.refs || []).length ? "\xB7 " + o.inbox.refs.map((r) => `<a href="${String(r).replace(/</g, "&lt;")}" target="_blank" rel="noopener" style="color:#7a9ad4;">[\u5F15\u7528]</a>`).join(" ") : ""}
5086
+ </div>` : "";
5087
+ const searchLine = o.search ? `<div style="font-size:11px;color:#8a8a80;margin-top:6px;border-top:1px dashed var(--border,#3a3a36);padding-top:6px;">
5088
+ \u{1F50E} AI \u4E3B\u52A8\u641C\u7D22 \xB7 <a href="${String(o.search.url || "").replace(/</g, "&lt;")}" target="_blank" rel="noopener" style="color:#7a9ad4;">\u6253\u5F00\u539F\u6587 \u2197</a>
5089
+ \xB7 <span style="color:#6aaa6a;">\u6765\u6E90 ${o.search.source}</span>
5090
+ </div>` : "";
5091
+ const detailLine = `<div class="world-opp-detail" style="display:none;margin-top:8px;padding-top:8px;border-top:1px solid var(--border,#3a3a36);font-size:12px;color:#b8b8b0;line-height:1.6;">
5092
+ <div style="font-weight:600;color:#f0f0ea;margin-bottom:4px;">\u5B8C\u6574\u5185\u5BB9</div>
5093
+ <div>${String(o.summary || "(\u65E0\u66F4\u591A\u5185\u5BB9)").replace(/</g, "&lt;")}</div>
5094
+ ${o.search?.url ? `<div style="margin-top:6px;">\u{1F517} <a href="${String(o.search.url).replace(/</g, "&lt;")}" target="_blank" rel="noopener" style="color:#7a9ad4;">${String(o.search.url).slice(0, 80)}</a></div>` : ""}
5095
+ <div style="margin-top:6px;color:#606058;">\u5339\u914D ${pct}% \xB7 \u6765\u6E90 ${o.source} \xB7 id ${o.id}</div>
5096
+ </div>`;
5097
+ return `<div class="world-opp-card" data-opp-card="${o.id}" style="border:1px solid var(--border,#3a3a36);border-radius:12px;padding:12px 14px;margin-bottom:10px;background:var(--bg-card,#222220);cursor:pointer;transition:border-color 0.2s;">
5086
5098
  <div style="display:flex;justify-content:space-between;align-items:center;">
5087
5099
  <b style="font-size:14px;">${String(o.title || "").slice(0, 60).replace(/</g, "&lt;")}</b>
5088
5100
  <span style="color:#c4d640;font-weight:700;font-size:14px;">${pct}%</span>
5089
5101
  </div>
5090
5102
  <div style="font-size:12px;color:#909088;margin-top:4px;">${String(o.summary || "").slice(0, 90).replace(/</g, "&lt;")}</div>
5103
+ ${inboxLine}
5104
+ ${searchLine}
5105
+ ${detailLine}
5091
5106
  <div style="margin-top:8px;display:flex;gap:8px;">
5092
5107
  <button class="btn-primary btn-sm" data-opp-action="accept" data-opp-id="${o.id}" style="font-size:11px;padding:3px 12px;">\u770B\u770B</button>
5093
5108
  <button class="btn-secondary btn-sm" data-opp-action="ignore" data-opp-id="${o.id}" style="font-size:11px;padding:3px 12px;">\u5FFD\u7565</button>
@@ -5096,26 +5111,39 @@ ${data.error || "channel not found"}`, "error");
5096
5111
  }).join("");
5097
5112
  worldOpportunities.innerHTML = cards;
5098
5113
  worldOpportunities.querySelectorAll("[data-opp-action]").forEach((btn) => {
5099
- btn.addEventListener("click", async () => {
5114
+ btn.addEventListener("click", async (ev) => {
5115
+ ev.stopPropagation();
5100
5116
  const id = btn.dataset.oppId;
5101
5117
  if (btn.dataset.oppAction === "ignore") {
5102
5118
  try {
5103
5119
  await fetch("/api/opportunities/" + id + "/ignore", { method: "POST" });
5104
5120
  } catch (e) {
5105
5121
  }
5106
- btn.closest(".world-opp-card").style.opacity = "0.3";
5107
- btn.closest(".world-opp-card").style.filter = "grayscale(1)";
5122
+ const card = btn.closest(".world-opp-card");
5123
+ if (card) {
5124
+ card.style.opacity = "0.3";
5125
+ card.style.filter = "grayscale(1)";
5126
+ }
5108
5127
  } else {
5109
5128
  try {
5110
5129
  await fetch("/api/opportunities/" + id + "/accept", { method: "POST" });
5111
5130
  } catch (e) {
5112
5131
  }
5113
- const srcId = id.replace(/^opp_/, "ann-");
5114
- worldStatus("\u5DF2\u8BB0\u4E0B: " + srcId);
5115
- btn.closest(".world-opp-card").style.borderColor = "#c4d640";
5132
+ toggleOppDetail(btn.closest(".world-opp-card"));
5116
5133
  }
5117
5134
  });
5118
5135
  });
5136
+ worldOpportunities.querySelectorAll("[data-opp-card]").forEach((card) => {
5137
+ card.addEventListener("click", () => toggleOppDetail(card));
5138
+ });
5139
+ }
5140
+ function toggleOppDetail(card) {
5141
+ if (!card) return;
5142
+ const detail = card.querySelector(".world-opp-detail");
5143
+ if (!detail) return;
5144
+ const shown = detail.style.display !== "none";
5145
+ detail.style.display = shown ? "none" : "block";
5146
+ card.style.borderColor = shown ? "var(--border,#3a3a36)" : "#c4d640";
5119
5147
  }
5120
5148
  async function loadWorld() {
5121
5149
  worldLoaded = true;
@@ -5136,88 +5164,58 @@ ${data.error || "channel not found"}`, "error");
5136
5164
  worldPollTimer = null;
5137
5165
  }
5138
5166
  }
5139
- function showWorldView() {
5140
- if (worldView) {
5141
- worldView.hidden = false;
5167
+ var tabWorld = document.getElementById("tab-world");
5168
+ var tabChat = document.getElementById("tab-chat");
5169
+ var loopStatusBar2 = document.getElementById("loop-status-bar");
5170
+ function switchPanel(panel) {
5171
+ if (!worldView || !messagesEl) return;
5172
+ const isWorld = panel === "world";
5173
+ worldView.style.display = isWorld ? "flex" : "none";
5174
+ if (messagesEl) messagesEl.hidden = isWorld;
5175
+ if (loopStatusBar2) loopStatusBar2.hidden = isWorld;
5176
+ if (inputArea) inputArea.hidden = isWorld;
5177
+ if (tabWorld) tabWorld.classList.toggle("active", isWorld);
5178
+ if (tabChat) tabChat.classList.toggle("active", !isWorld);
5179
+ if (isWorld) {
5142
5180
  if (!worldLoaded) loadWorld();
5143
5181
  else {
5144
5182
  fetchWorldIntents();
5145
5183
  refreshWorldOpportunities();
5146
5184
  }
5147
5185
  startWorldPoll();
5186
+ } else {
5187
+ stopWorldPoll();
5148
5188
  }
5149
5189
  }
5150
- function hideWorldView() {
5151
- if (worldView) worldView.hidden = true;
5152
- stopWorldPoll();
5153
- }
5154
- if (worldBtn) worldBtn.addEventListener("click", showWorldView);
5155
- if (worldViewClose) worldViewClose.addEventListener("click", hideWorldView);
5190
+ if (tabWorld) tabWorld.addEventListener("click", () => switchPanel("world"));
5191
+ if (tabChat) tabChat.addEventListener("click", () => switchPanel("chat"));
5192
+ if (worldBtn) worldBtn.addEventListener("click", () => switchPanel("world"));
5156
5193
  if (worldRefreshBtn) worldRefreshBtn.addEventListener("click", () => {
5157
5194
  fetchWorldIntents();
5158
5195
  refreshWorldOpportunities();
5159
5196
  });
5160
- if (worldProfileSet) worldProfileSet.addEventListener("click", async () => {
5161
- const text = (worldProfileInput && worldProfileInput.value || "").trim();
5162
- if (!text) {
5163
- worldStatus("\u5148\u5199\u4E00\u53E5\u4F60\u662F\u8C01 / \u5728\u505A\u4EC0\u4E48");
5164
- return;
5165
- }
5166
- if (worldProfileSet) worldProfileSet.disabled = true;
5167
- try {
5168
- const res = await fetch("/api/world/profile", {
5169
- method: "PUT",
5170
- headers: { "Content-Type": "application/json" },
5171
- body: JSON.stringify({ name: text, about: text, tags: (text.match(/[\u4e00-\u9fff]{2,}|[a-z][a-z0-9_-]{1,}/gi) || []).slice(0, 6) })
5172
- });
5173
- if (!res.ok) {
5174
- const err = await res.json().catch(() => ({}));
5175
- worldStatus("\u753B\u50CF\u4FDD\u5B58\u5931\u8D25: " + (err.error || "HTTP " + res.status));
5176
- return;
5177
- }
5178
- worldStatus("\u753B\u50CF\u5DF2\u4FDD\u5B58 \u2014 \u4E16\u754C\u5F00\u59CB\u6309\u5B83\u63A8\u9001");
5179
- await refreshWorldOpportunities();
5180
- } catch (e) {
5181
- worldStatus("\u753B\u50CF\u4FDD\u5B58\u5931\u8D25: " + (e && e.message ? e.message : e));
5182
- } finally {
5183
- if (worldProfileSet) worldProfileSet.disabled = false;
5184
- }
5185
- });
5186
- if (worldIntentSet) worldIntentSet.addEventListener("click", async () => {
5187
- const text = (worldIntentInput && worldIntentInput.value || "").trim();
5188
- if (!text) {
5189
- worldStatus("\u5148\u5199\u4E00\u53E5\u4F60\u73B0\u5728\u5728\u505A\u4EC0\u4E48");
5190
- return;
5191
- }
5192
- if (worldIntentSet) worldIntentSet.disabled = true;
5197
+ var worldInput = document.getElementById("world-input");
5198
+ var worldInputSend = document.getElementById("world-input-send");
5199
+ async function tellWorld() {
5200
+ const text = (worldInput && worldInput.value || "").trim();
5201
+ if (!text) return;
5202
+ worldInput.value = "";
5193
5203
  try {
5194
- const body = { text, priority: Number(worldIntentPriority ? worldIntentPriority.value : 3) };
5195
- if (worldIntentBudget && worldIntentBudget.value.trim()) body.budget = worldIntentBudget.value.trim();
5196
- const res = await fetch("/api/intents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
5204
+ const res = await fetch("/api/intents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, priority: 3 }) });
5197
5205
  if (!res.ok) {
5198
- const err = await res.json().catch(() => ({}));
5199
- worldStatus("\u58F0\u660E\u5931\u8D25: " + (err.error || "HTTP " + res.status));
5206
+ console.info("[world] \u58F0\u660E\u5931\u8D25 HTTP", res.status);
5200
5207
  return;
5201
5208
  }
5202
- if (worldIntentInput) worldIntentInput.value = "";
5203
- if (worldIntentBudget) worldIntentBudget.value = "";
5204
5209
  await fetchWorldIntents();
5205
5210
  await refreshWorldOpportunities();
5206
- if (worldPollTimer) {
5207
- clearInterval(worldPollTimer);
5208
- startWorldPoll();
5209
- }
5210
5211
  } catch (e) {
5211
- worldStatus("\u58F0\u660E\u5931\u8D25: " + (e && e.message ? e.message : e));
5212
- } finally {
5213
- if (worldIntentSet) worldIntentSet.disabled = false;
5212
+ console.info("[world] \u58F0\u660E\u5931\u8D25", e);
5214
5213
  }
5215
- });
5216
- if (worldIntentInput) {
5217
- worldIntentInput.addEventListener("keydown", (e) => {
5218
- if (e.key === "Enter" && worldIntentSet) worldIntentSet.click();
5219
- });
5220
5214
  }
5215
+ if (worldInputSend) worldInputSend.addEventListener("click", tellWorld);
5216
+ if (worldInput) worldInput.addEventListener("keydown", (e) => {
5217
+ if (e.key === "Enter") tellWorld();
5218
+ });
5221
5219
  (async function autoWorldBadge() {
5222
5220
  try {
5223
5221
  await refreshWorldOpportunities();
@@ -133,6 +133,11 @@
133
133
  <span class="channel-indicator" id="channel-indicator"></span>
134
134
  <h1 id="channel-name">Bolloon Agent</h1>
135
135
  </div>
136
+ <!-- 2026-10-05 (leo): 世界/聊天切换放进原导航栏 (header) 中间 — 与钱包/配置同栏, 不再另起一行 -->
137
+ <div class="header-tabs">
138
+ <button id="tab-world" class="header-tab active" data-panel="world">🌍 世界 <span id="world-badge" class="task-badge" style="display:none;">0</span></button>
139
+ <button id="tab-chat" class="header-tab" data-panel="chat">💬 聊天</button>
140
+ </div>
136
141
  <div class="header-right">
137
142
  <button id="theme-toggle" class="theme-toggle" title="切换主题">
138
143
  <svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -165,15 +170,6 @@
165
170
  </svg>
166
171
  <span id="wallet-badge" class="task-badge" style="display:none;">0</span>
167
172
  </button>
168
- <!-- 2026-10-05: World (Intent Console + Opportunity) — PC=Create 工作台入口 -->
169
- <button id="world-btn" class="header-action" title="World — 我的意图与世界机会">
170
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
- <circle cx="12" cy="12" r="9"></circle>
172
- <path d="M3 12h18"></path>
173
- <path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z"></path>
174
- </svg>
175
- <span id="world-badge" class="task-badge" style="display:none;">0</span>
176
- </button>
177
173
  <button id="judgments-btn" class="header-action" title="我的判断">
178
174
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
179
175
  <path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path>
@@ -422,55 +418,40 @@
422
418
  </div>
423
419
  </div>
424
420
 
425
- <!-- World View (2026-10-05, leo Intent Network) — Twitter 式整页: 中栏机会时间线自动流入 -->
426
- <div id="world-view" hidden style="position:fixed;inset:0;z-index:999;background:var(--bg,#1a1a18);display:flex;flex-direction:column;">
427
- <!-- 顶栏 -->
428
- <div style="display:flex;align-items:center;justify-content:space-between;padding:10px 18px;border-bottom:1px solid var(--border,#3a3a36);">
429
- <div style="font-weight:700;font-size:15px;">🌍 World <span style="color:#606058;font-weight:400;font-size:12px;">世界在给你送东西</span></div>
430
- <div style="display:flex;gap:8px;align-items:center;">
431
- <button id="world-refresh-btn" class="btn-secondary btn-sm" style="font-size:12px;">⟳ 刷新</button>
432
- <button id="world-view-close" class="btn-secondary btn-sm" style="font-size:12px;">← 回到聊天</button>
433
- </div>
421
+ <!-- World View (2026-10-05, leo Intent Network) — 主页内嵌面板 (不再全屏跳转):
422
+ 顶部 tab 切换 世界/聊天, 默认世界; 无嵌套无跳转 -->
423
+ <div id="world-view" style="position:relative;background:var(--bg,#1a1a18);display:flex;flex-direction:column;flex:1;min-height:0;">
424
+ <!-- 面板头 (只剩刷新; 关闭按钮已由顶部 tab 取代) -->
425
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 18px;border-bottom:1px solid var(--border,#3a3a36);flex-shrink:0;">
426
+ <div style="font-weight:700;font-size:14px;">🌍 World <span style="color:#606058;font-weight:400;font-size:11px;">世界在给你送东西 · 自动流入</span></div>
427
+ <button id="world-refresh-btn" class="btn-secondary btn-sm" style="font-size:11px;padding:2px 10px;">⟳ 刷新</button>
434
428
  </div>
435
- <!-- Twitter 式三栏 -->
429
+ <!-- Twitter 式两栏 (2026-10-05: 右侧输入栏删除 — 匹配是内置自动算法, 不需要用户填写) -->
436
430
  <div style="display:flex;flex:1;overflow:hidden;">
437
- <!-- 左栏: 我的意图 (nav 风格) -->
438
- <aside style="width:240px;border-right:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;display:none;" id="world-left-col">
431
+ <!-- 左栏: 我的意图 (nav 风格, 只读展示) -->
432
+ <aside style="width:220px;border-right:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;display:none;" id="world-left-col">
439
433
  <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">我的意图 (Intent)</div>
440
434
  <div id="world-intents-list" style="margin-bottom:12px;"><div class="form-hint">(还没有意图)</div></div>
441
435
  <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">世界变化</div>
442
436
  <div id="world-stats" style="font-size:12px;color:#606058;"></div>
443
437
  </aside>
444
- <!-- 中栏: 机会时间线 (Twitter feed 自动流入) -->
445
- <main style="flex:1;overflow-y:auto;padding:16px;max-width:680px;margin:0 auto;width:100%;">
446
- <div style="font-size:12px;color:#909088;margin-bottom:12px;">✦ 与你相关 (自动匹配, 持续流入)</div>
438
+ <!-- 中栏: 机会时间线 (Twitter feed 自动流入, 全宽) -->
439
+ <main style="flex:1;overflow-y:auto;padding:16px 24px;max-width:760px;margin:0 auto;width:100%;">
440
+ <div style="font-size:12px;color:#909088;margin-bottom:12px;">✦ 世界在给你送东西 (自动匹配, 持续流入)</div>
447
441
  <div id="world-opportunities" style="min-height:120px;">
448
- <div class="form-hint">世界正在观察… (等待意图声明或机会出现)</div>
442
+ <div class="form-hint">世界正在观察…</div>
449
443
  </div>
450
444
  </main>
451
- <!-- 右栏: 声明意图 + 说明 -->
452
- <aside style="width:280px;border-left:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;" id="world-right-col">
453
- <div style="font-size:13px;font-weight:600;margin-bottom:6px;">先告诉我你是谁</div>
454
- <input type="text" id="world-profile-input" placeholder="例: 我在做可控核聚变 + AI 仿真" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid var(--border,#3a3a36);background:var(--bg-card,#222220);color:var(--text);font-size:13px;">
455
- <button id="world-profile-set" class="btn-secondary btn-sm" style="width:100%;padding:6px;font-size:12px;margin:6px 0 14px;">存为画像 → 世界按它推送</button>
456
- <div style="font-size:13px;font-weight:600;margin-bottom:8px;">我现在正在做什么</div>
457
- <input type="text" id="world-intent-input" placeholder="例: 建立聚变创业项目 · 找科研合作" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid var(--border,#3a3a36);background:var(--bg-card,#222220);color:var(--text);font-size:13px;">
458
- <div style="display:flex;gap:8px;margin:8px 0;">
459
- <select id="world-intent-priority" style="font-size:12px;padding:6px;background:var(--bg-card,#222220);color:var(--text);border:1px solid var(--border,#3a3a36);border-radius:6px;">
460
- <option value="3">★★★</option><option value="5">★★★★★</option><option value="4">★★★★</option><option value="2">★★</option><option value="1">★</option>
461
- </select>
462
- <input type="text" id="world-intent-budget" placeholder="预算 ¥" style="font-size:12px;padding:6px;width:80px;background:var(--bg-card,#222220);color:var(--text);border:1px solid var(--border,#3a3a36);border-radius:6px;">
463
- </div>
464
- <button id="world-intent-set" class="btn-primary btn-sm" style="width:100%;padding:8px;font-size:13px;">声明 → 世界开始找</button>
465
- <div id="world-intent-error" style="font-size:12px;color:#f59e0b;margin-top:8px;min-height:16px;"></div>
466
- <div style="font-size:11px;color:#606058;margin-top:12px;line-height:1.6;">
467
- 世界会自己校准 —— 你「看看 / 忽略」的每个动作, 都让接下来的推送更准。
468
- </div>
469
- </aside>
445
+ </div>
446
+ <!-- 2026-10-05 (leo): 世界底部输入框 — 想做什么直接说, 自动触发匹配 (不是聊天输入框) -->
447
+ <div class="world-input-bar" style="display:flex;gap:8px;align-items:center;padding:10px 18px;border-top:1px solid var(--border,#3a3a36);flex-shrink:0;background:var(--bg-sidebar,#222220);">
448
+ <input type="text" id="world-input" placeholder="告诉世界你在做什么 / 需要什么… (Enter 自动匹配)" autocomplete="off"
449
+ style="flex:1;padding:8px 12px;border-radius:8px;border:1px solid var(--border,#3a3a36);background:var(--bg-card,#222220);color:var(--text,#f0f0ea);font-size:13px;">
450
+ <button id="world-input-send" class="btn-primary btn-sm" style="padding:7px 16px;font-size:12px;">→ 世界</button>
470
451
  </div>
471
452
  </div>
472
453
 
473
- <div class="messages" id="messages"></div>
454
+ <div class="messages" id="messages" hidden></div>
474
455
 
475
456
  <!-- 2026-06-15: 旧 loop-timeline-panel 删除 — 步骤状态条改在每条 AI 消息气泡内挂 step-timeline 组件 -->
476
457
  <!-- 2026-06-16: 循环 status bar — 渲染 tool='loop' | 'compactor' | 'recovery' 的 status 事件
@@ -484,7 +465,7 @@
484
465
  <button id="loop-status-check" class="loop-status-action" hidden type="button">✓ 检查</button>
485
466
  </div>
486
467
 
487
- <div class="input-area">
468
+ <div class="input-area" hidden>
488
469
  <div class="input-wrapper">
489
470
  <button id="send-tools-toggle" class="send-tools-toggle" title="本次发送是否允许工具调用 (点击切换)"
490
471
  style="display:none;align-items:center;gap:4px;background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:6px;padding:4px 8px;font-size:11px;cursor:pointer;white-space:nowrap;flex-shrink:0;">
@@ -2,6 +2,8 @@ import { setIntent, listIntents, getIntent, removeIntent } from '../agents/inten
2
2
  import { scanOpportunities, recordFeedback, readObservations } from '../agents/opportunity-match.js';
3
3
  import { readProfile, setProfile } from '../agents/world-profile.js';
4
4
  import { worldWatcherStatus } from '../agents/world-watcher.js';
5
+ import { deliverOpportunity, listInboxOpportunities, INBOX_PROTOCOL } from '../agents/opportunity-inbox.js';
6
+ import { searchOpportunitiesForTopic, listSearchOpportunities } from '../agents/opportunity-web-search.js';
5
7
  function json(res, status, body) {
6
8
  res.status(status).json(body);
7
9
  }
@@ -102,4 +104,56 @@ export function registerWorldRoutes(app) {
102
104
  const observations = await readObservations(20);
103
105
  json(res, 200, { ok: true, watcher: status, recentObservations: observations });
104
106
  });
107
+ // ── 机会信箱 (2026-10-05, leo: 别人主动投来的机会 — 真实可信来源) ───────────
108
+ // 外部 agent 用 DIAP Ed25519 身份签名投递机会声明; 验签通过才入库并进世界流。
109
+ // 协议: POST { protocol, title, summary, refs[], provider:{did}, issuedAt } + header X-Signature
110
+ // signature = ed25519Sign(provider 私钥, canonicalize(上述 payload)) — 与 x402 同源 canonicalize
111
+ app.get('/api/world/inbox', async (_req, res) => {
112
+ const list = await listInboxOpportunities();
113
+ json(res, 200, { ok: true, count: list.length, opportunities: list });
114
+ });
115
+ app.post('/api/world/inbox', async (req, res) => {
116
+ const b = bodyOf(req);
117
+ const sig = typeof req.headers['x-signature'] === 'string' ? req.headers['x-signature'] : '';
118
+ const payload = {
119
+ protocol: typeof b.protocol === 'string' ? b.protocol : '',
120
+ title: typeof b.title === 'string' ? b.title : '',
121
+ summary: typeof b.summary === 'string' ? b.summary : '',
122
+ refs: Array.isArray(b.refs) ? b.refs.filter((r) => typeof r === 'string') : [],
123
+ provider: {
124
+ did: typeof b.provider?.did === 'string' ? b.provider.did : '',
125
+ name: typeof b.provider?.name === 'string' ? b.provider.name : undefined,
126
+ },
127
+ issuedAt: typeof b.issuedAt === 'string' ? b.issuedAt : new Date().toISOString(),
128
+ };
129
+ const r = await deliverOpportunity(payload, sig);
130
+ json(res, r.ok ? 200 : 400, { ok: r.ok, opportunity: r.opportunity, error: r.error });
131
+ });
132
+ // 协议说明 (外部 agent 对接用 — 怎么构造 payload / 怎么签名)
133
+ app.get('/api/world/inbox/protocol', async (_req, res) => {
134
+ json(res, 200, {
135
+ ok: true,
136
+ protocol: INBOX_PROTOCOL,
137
+ description: '外部 agent 主动投递机会到信箱: POST /api/world/inbox, header X-Signature = Ed25519(provider 私钥, canonicalize(payload))',
138
+ payloadFields: ['protocol', 'title', 'summary', 'refs[]', 'provider.did', 'provider.name?', 'issuedAt'],
139
+ signature: 'ed25519Sign(providerPrivateKey, canonicalize({protocol,title,summary,refs,provider,issuedAt}))',
140
+ verification: '本机解析 provider.did → Ed25519 公钥 → 验签; 通过才入库 (verified) 并进世界流, 否则拒绝',
141
+ });
142
+ });
143
+ // ── AI 主动搜索 (2026-10-05, leo: 系统要主动上网搜机会, 非只被动响应) ─────────
144
+ // POST /api/world/search [{topic}] → 按 topic 搜机会落 world/search/ 并进世界流
145
+ app.post('/api/world/search', async (req, res) => {
146
+ const b = bodyOf(req);
147
+ const topic = typeof b.topic === 'string' && b.topic.trim() ? b.topic.trim()
148
+ : (typeof b.text === 'string' && b.text.trim() ? b.text.trim() : '');
149
+ if (!topic)
150
+ return json(res, 400, { ok: false, error: '缺 topic (要搜什么机会)' });
151
+ const r = await searchOpportunitiesForTopic(topic, { limit: 6 });
152
+ json(res, r.ok ? 200 : 400, { ok: r.ok, topic, added: r.added, total: r.total, opportunities: r.results, error: r.error });
153
+ });
154
+ // GET /api/world/search → 已搜到的机会 (最近在前)
155
+ app.get('/api/world/search', async (_req, res) => {
156
+ const list = await listSearchOpportunities();
157
+ json(res, 200, { ok: true, count: list.length, opportunities: list });
158
+ });
105
159
  }
@@ -4,6 +4,12 @@
4
4
  box-sizing: border-box;
5
5
  }
6
6
 
7
+ /* 2026-10-05 (leo): hidden 属性必须真隐藏 —— 之前 .messages{display:flex} 覆盖了
8
+ UA 的 [hidden]{display:none}, 导致世界栏下面一直显示聊天记录 (污染)。 */
9
+ [hidden] {
10
+ display: none !important;
11
+ }
12
+
7
13
  :root {
8
14
  --bg: #1a1a18;
9
15
  --bg-sidebar: #222220;
@@ -1858,6 +1864,39 @@ body {
1858
1864
  font-weight: normal;
1859
1865
  color: var(--text-muted);
1860
1866
  }
1867
+
1868
+ /* 2026-10-05 (leo): 世界/聊天切换放进原导航栏 (header) 中间 — 与钱包/配置同栏 */
1869
+ .header-tabs {
1870
+ display: flex;
1871
+ gap: 2px;
1872
+ margin-left: 18px;
1873
+ padding: 3px;
1874
+ background: var(--bg-hover);
1875
+ border-radius: 8px;
1876
+ flex-shrink: 0;
1877
+ }
1878
+ .header-tab {
1879
+ padding: 5px 14px;
1880
+ background: transparent;
1881
+ border: none;
1882
+ border-radius: 6px;
1883
+ color: var(--text-secondary);
1884
+ font-size: 12px;
1885
+ font-weight: 600;
1886
+ cursor: pointer;
1887
+ transition: var(--transition);
1888
+ display: flex;
1889
+ align-items: center;
1890
+ gap: 4px;
1891
+ }
1892
+ .header-tab:hover {
1893
+ color: var(--text-primary);
1894
+ }
1895
+ .header-tab.active {
1896
+ background: var(--bg-main);
1897
+ color: var(--accent-hover);
1898
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
1899
+ }
1861
1900
  .judgment-status-bar {
1862
1901
  display: flex;
1863
1902
  gap: 6px;
package/dist/web/sw.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 现在: 代码类资源 (html/js/css/json) 一律 **network-first** (拿不到网才回退缓存),
7
7
  * 缓存名带版本号, activate 时清掉所有旧缓存; 图标等静态资源仍 cache-first。
8
8
  */
9
- const CACHE = 'bolloon-mobile-v0.6.4';
9
+ const CACHE = 'bolloon-mobile-v0.6.5';
10
10
  const SHELL = [
11
11
  './mobile.html',
12
12
  './mobile.css',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",