@bolloon/bolloon-agent 0.6.3 → 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.
@@ -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/ios/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.3';
9
+ const CACHE = 'bolloon-mobile-v0.6.5';
10
10
  const SHELL = [
11
11
  './mobile.html',
12
12
  './mobile.css',
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema": "bolloon-mobile-web/1",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "channel": "stable",
5
5
  "build": "stable",
6
- "commit": "eb3ebe6fed23cb70e901ac8da92e6420bc0673ad",
7
- "builtAt": "2026-10-05T06:24:49.679Z",
6
+ "commit": "0d876c1b48ea86d5a390bd21b62468c8d2487fe0",
7
+ "builtAt": "2026-10-06T07:04:38.761Z",
8
8
  "layer": "web",
9
9
  "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
10
  }
@@ -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");
@@ -4992,6 +5000,228 @@ ${data.error || "channel not found"}`, "error");
4992
5000
  if (e.target === judgmentsModal) hideJudgmentsModal();
4993
5001
  });
4994
5002
  }
5003
+ var worldView = document.getElementById("world-view");
5004
+ var worldBtn = document.getElementById("world-btn");
5005
+ var worldRefreshBtn = document.getElementById("world-refresh-btn");
5006
+ var worldIntentsList = document.getElementById("world-intents-list");
5007
+ var worldOpportunities = document.getElementById("world-opportunities");
5008
+ var worldStats = document.getElementById("world-stats");
5009
+ var worldBadge = document.getElementById("world-badge");
5010
+ var worldLoaded = false;
5011
+ var worldPollTimer = null;
5012
+ var WORLD_POLL_MS = 3e4;
5013
+ function worldStatus(msg) {
5014
+ if (msg) console.info("[world]", msg);
5015
+ }
5016
+ async function fetchWorldIntents() {
5017
+ try {
5018
+ const res = await fetch("/api/intents");
5019
+ if (!res.ok) throw new Error("HTTP " + res.status);
5020
+ const data = await res.json();
5021
+ renderWorldIntents(data.intents || []);
5022
+ if (worldStats) {
5023
+ const active = (data.intents || []).filter((i) => i.status === "active").length;
5024
+ worldStats.textContent = `\u610F\u56FE ${active} \u4E2A\u6D3B\u8DC3 \xB7 \u4E16\u754C\u6301\u7EED\u89C2\u5BDF\u4E2D`;
5025
+ }
5026
+ return data.intents || [];
5027
+ } catch (e) {
5028
+ worldStatus("\u62C9\u53D6\u610F\u56FE\u5931\u8D25: " + (e && e.message ? e.message : e));
5029
+ return [];
5030
+ }
5031
+ }
5032
+ function renderWorldIntents(intents) {
5033
+ if (!worldIntentsList) return;
5034
+ if (!intents.length) {
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>';
5036
+ return;
5037
+ }
5038
+ const active = intents.filter((i) => i.status === "active");
5039
+ const rows = active.map((i) => {
5040
+ const stars = "\u2605".repeat(i.priority) + "\u2606".repeat(5 - i.priority);
5041
+ return `<div class="world-intent-row" style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border:1px solid var(--border,#3a3a36);border-radius:8px;margin-bottom:6px;font-size:11px;">
5042
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><b>${i.text.replace(/</g, "&lt;")}</b> <span style="color:#909088;">${stars}</span></span>
5043
+ <button class="btn-secondary btn-sm" data-intent-rm="${i.id}" style="font-size:10px;padding:1px 6px;">\u2715</button>
5044
+ </div>`;
5045
+ }).join("");
5046
+ worldIntentsList.innerHTML = rows;
5047
+ worldIntentsList.querySelectorAll("[data-intent-rm]").forEach((btn) => {
5048
+ btn.addEventListener("click", async () => {
5049
+ try {
5050
+ await fetch("/api/intents/" + btn.dataset.intentRm, { method: "DELETE" });
5051
+ } catch (e) {
5052
+ }
5053
+ fetchWorldIntents();
5054
+ });
5055
+ });
5056
+ }
5057
+ async function refreshWorldOpportunities() {
5058
+ try {
5059
+ const res = await fetch("/api/opportunities?min-score=0.4");
5060
+ if (!res.ok) throw new Error("HTTP " + res.status);
5061
+ const data = await res.json();
5062
+ renderWorldOpportunities(data.opportunities || []);
5063
+ if (worldBadge) {
5064
+ const n = (data.opportunities || []).length;
5065
+ worldBadge.textContent = String(n);
5066
+ worldBadge.style.display = n > 0 ? "" : "none";
5067
+ }
5068
+ return data.opportunities || [];
5069
+ } catch (e) {
5070
+ worldStatus("\u626B\u63CF\u673A\u4F1A\u5931\u8D25: " + (e && e.message ? e.message : e));
5071
+ return [];
5072
+ }
5073
+ }
5074
+ function renderWorldOpportunities(opps) {
5075
+ if (!worldOpportunities) return;
5076
+ if (!opps.length) {
5077
+ worldOpportunities.innerHTML = '<div class="form-hint">\u4E16\u754C\u6B63\u5728\u6301\u7EED\u9001\u6765 \u2014 \u7A0D\u7B49\u4E00\u4E0B\u3002</div>';
5078
+ return;
5079
+ }
5080
+ const cards = opps.map((o) => {
5081
+ const pct = Math.round(o.score * 100);
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;">
5098
+ <div style="display:flex;justify-content:space-between;align-items:center;">
5099
+ <b style="font-size:14px;">${String(o.title || "").slice(0, 60).replace(/</g, "&lt;")}</b>
5100
+ <span style="color:#c4d640;font-weight:700;font-size:14px;">${pct}%</span>
5101
+ </div>
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}
5106
+ <div style="margin-top:8px;display:flex;gap:8px;">
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>
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>
5109
+ </div>
5110
+ </div>`;
5111
+ }).join("");
5112
+ worldOpportunities.innerHTML = cards;
5113
+ worldOpportunities.querySelectorAll("[data-opp-action]").forEach((btn) => {
5114
+ btn.addEventListener("click", async (ev) => {
5115
+ ev.stopPropagation();
5116
+ const id = btn.dataset.oppId;
5117
+ if (btn.dataset.oppAction === "ignore") {
5118
+ try {
5119
+ await fetch("/api/opportunities/" + id + "/ignore", { method: "POST" });
5120
+ } catch (e) {
5121
+ }
5122
+ const card = btn.closest(".world-opp-card");
5123
+ if (card) {
5124
+ card.style.opacity = "0.3";
5125
+ card.style.filter = "grayscale(1)";
5126
+ }
5127
+ } else {
5128
+ try {
5129
+ await fetch("/api/opportunities/" + id + "/accept", { method: "POST" });
5130
+ } catch (e) {
5131
+ }
5132
+ toggleOppDetail(btn.closest(".world-opp-card"));
5133
+ }
5134
+ });
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";
5147
+ }
5148
+ async function loadWorld() {
5149
+ worldLoaded = true;
5150
+ worldStatus("");
5151
+ await fetchWorldIntents();
5152
+ await refreshWorldOpportunities();
5153
+ }
5154
+ function startWorldPoll() {
5155
+ if (worldPollTimer) clearInterval(worldPollTimer);
5156
+ worldPollTimer = setInterval(() => {
5157
+ refreshWorldOpportunities();
5158
+ fetchWorldIntents();
5159
+ }, WORLD_POLL_MS);
5160
+ }
5161
+ function stopWorldPoll() {
5162
+ if (worldPollTimer) {
5163
+ clearInterval(worldPollTimer);
5164
+ worldPollTimer = null;
5165
+ }
5166
+ }
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) {
5180
+ if (!worldLoaded) loadWorld();
5181
+ else {
5182
+ fetchWorldIntents();
5183
+ refreshWorldOpportunities();
5184
+ }
5185
+ startWorldPoll();
5186
+ } else {
5187
+ stopWorldPoll();
5188
+ }
5189
+ }
5190
+ if (tabWorld) tabWorld.addEventListener("click", () => switchPanel("world"));
5191
+ if (tabChat) tabChat.addEventListener("click", () => switchPanel("chat"));
5192
+ if (worldBtn) worldBtn.addEventListener("click", () => switchPanel("world"));
5193
+ if (worldRefreshBtn) worldRefreshBtn.addEventListener("click", () => {
5194
+ fetchWorldIntents();
5195
+ refreshWorldOpportunities();
5196
+ });
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 = "";
5203
+ try {
5204
+ const res = await fetch("/api/intents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, priority: 3 }) });
5205
+ if (!res.ok) {
5206
+ console.info("[world] \u58F0\u660E\u5931\u8D25 HTTP", res.status);
5207
+ return;
5208
+ }
5209
+ await fetchWorldIntents();
5210
+ await refreshWorldOpportunities();
5211
+ } catch (e) {
5212
+ console.info("[world] \u58F0\u660E\u5931\u8D25", e);
5213
+ }
5214
+ }
5215
+ if (worldInputSend) worldInputSend.addEventListener("click", tellWorld);
5216
+ if (worldInput) worldInput.addEventListener("keydown", (e) => {
5217
+ if (e.key === "Enter") tellWorld();
5218
+ });
5219
+ (async function autoWorldBadge() {
5220
+ try {
5221
+ await refreshWorldOpportunities();
5222
+ } catch (e) {
5223
+ }
5224
+ })();
4995
5225
  var judgmentImportBtn = document.getElementById("judgment-import-btn");
4996
5226
  var judgmentImportFile = document.getElementById("judgment-import-file");
4997
5227
  var judgmentCleanupBtn = document.getElementById("judgment-cleanup-btn");
@@ -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">
@@ -413,7 +418,40 @@
413
418
  </div>
414
419
  </div>
415
420
 
416
- <div class="messages" id="messages"></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>
428
+ </div>
429
+ <!-- Twitter 式两栏 (2026-10-05: 右侧输入栏删除 — 匹配是内置自动算法, 不需要用户填写) -->
430
+ <div style="display:flex;flex:1;overflow:hidden;">
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">
433
+ <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">我的意图 (Intent)</div>
434
+ <div id="world-intents-list" style="margin-bottom:12px;"><div class="form-hint">(还没有意图)</div></div>
435
+ <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">世界变化</div>
436
+ <div id="world-stats" style="font-size:12px;color:#606058;"></div>
437
+ </aside>
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>
441
+ <div id="world-opportunities" style="min-height:120px;">
442
+ <div class="form-hint">世界正在观察…</div>
443
+ </div>
444
+ </main>
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>
451
+ </div>
452
+ </div>
453
+
454
+ <div class="messages" id="messages" hidden></div>
417
455
 
418
456
  <!-- 2026-06-15: 旧 loop-timeline-panel 删除 — 步骤状态条改在每条 AI 消息气泡内挂 step-timeline 组件 -->
419
457
  <!-- 2026-06-16: 循环 status bar — 渲染 tool='loop' | 'compactor' | 'recovery' 的 status 事件
@@ -427,7 +465,7 @@
427
465
  <button id="loop-status-check" class="loop-status-action" hidden type="button">✓ 检查</button>
428
466
  </div>
429
467
 
430
- <div class="input-area">
468
+ <div class="input-area" hidden>
431
469
  <div class="input-wrapper">
432
470
  <button id="send-tools-toggle" class="send-tools-toggle" title="本次发送是否允许工具调用 (点击切换)"
433
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;">
@@ -359,6 +359,86 @@ body {
359
359
  }
360
360
  .agent-trace-line:first-child { color: var(--text); }
361
361
 
362
+ /* ============ 世界机会流 (2026-10-05, leo Intent Network) ============ */
363
+ /* 主动流入: 打开首页即拉, 持续刷新; 不是按钮式被动响应 */
364
+ .world-feed {
365
+ padding: 10px 14px 4px;
366
+ border-bottom: 1px solid var(--border);
367
+ background: var(--bg);
368
+ overflow-y: auto;
369
+ max-height: 42vh;
370
+ }
371
+ .world-feed-header {
372
+ display: flex;
373
+ justify-content: space-between;
374
+ align-items: baseline;
375
+ font-size: 13px;
376
+ font-weight: 600;
377
+ color: var(--text);
378
+ margin-bottom: 8px;
379
+ }
380
+ .world-feed-sub {
381
+ font-size: 11px;
382
+ color: var(--text-muted);
383
+ font-weight: 400;
384
+ }
385
+ .world-feed-cards { display: flex; flex-direction: column; gap: 8px; }
386
+ .world-feed-loading { font-size: 12px; color: var(--text-muted); padding: 8px 0; }
387
+ .world-feed-card {
388
+ background: var(--bg-card);
389
+ border: 1px solid var(--border);
390
+ border-left: 3px solid transparent;
391
+ border-radius: 10px;
392
+ padding: 10px 12px;
393
+ }
394
+ .world-feed-card-top {
395
+ display: flex;
396
+ justify-content: space-between;
397
+ align-items: center;
398
+ gap: 8px;
399
+ }
400
+ .world-feed-title {
401
+ font-size: 13px;
402
+ font-weight: 600;
403
+ color: var(--text);
404
+ overflow: hidden;
405
+ text-overflow: ellipsis;
406
+ white-space: nowrap;
407
+ }
408
+ .world-feed-pct {
409
+ color: var(--accent);
410
+ font-weight: 700;
411
+ font-size: 13px;
412
+ white-space: nowrap;
413
+ }
414
+ .world-feed-pct small { color: var(--text-muted); font-weight: 400; font-size: 10px; }
415
+ .world-feed-summary {
416
+ font-size: 12px;
417
+ color: var(--text-secondary);
418
+ margin-top: 4px;
419
+ line-height: 1.5;
420
+ }
421
+ .world-feed-actions {
422
+ display: flex;
423
+ gap: 8px;
424
+ margin-top: 8px;
425
+ }
426
+ .world-feed-btn {
427
+ flex: 1;
428
+ padding: 6px 0;
429
+ border: 1px solid var(--accent);
430
+ background: var(--accent);
431
+ color: #1a1a18;
432
+ border-radius: 8px;
433
+ font-size: 12px;
434
+ font-weight: 600;
435
+ }
436
+ .world-feed-btn.ghost {
437
+ background: transparent;
438
+ color: var(--text-secondary);
439
+ border-color: var(--border);
440
+ }
441
+
362
442
  /* ============ 双频滑动卡片 (主页面) ============ */
363
443
 
364
444
  .card-carousel {
@@ -44,6 +44,16 @@
44
44
 
45
45
  <!-- 主页面: 双频滑动卡片 -->
46
46
  <main class="page-container" id="page-main" data-tab="main" hidden>
47
+ <!-- 2026-10-05 (leo Intent Network): 世界机会流 — 自动流入, 不是按钮式被动响应 -->
48
+ <div class="world-feed" id="world-feed" hidden>
49
+ <div class="world-feed-header">
50
+ <span>✦ 世界送来</span>
51
+ <span class="world-feed-sub">自动匹配 · 持续校准</span>
52
+ </div>
53
+ <div class="world-feed-cards" id="world-feed-cards">
54
+ <div class="world-feed-loading">世界正在观察…</div>
55
+ </div>
56
+ </div>
47
57
  <!-- 水平滑动卡片轨道 -->
48
58
  <div class="card-carousel" id="card-carousel">
49
59
  <div class="card-track" id="card-track">
@@ -506,6 +506,59 @@
506
506
  let currentCardIndex = 0;
507
507
  let allAgentCards = [];
508
508
 
509
+ // === 世界机会流 (2026-10-05, leo Intent Network) ===
510
+ // 设计: 自动流入 (打开首页即拉), 不是按钮式被动响应。ignore/accept 走 feedback 校准环。
511
+ let worldFeedShown = false;
512
+ let worldFeedTimer = null;
513
+ async function loadWorldFeed() {
514
+ const feed = $('#world-feed');
515
+ const cards = $('#world-feed-cards');
516
+ if (!feed || !cards) return;
517
+ try {
518
+ const res = await fetch('/api/opportunities?min-score=0.2&limit=10');
519
+ if (!res.ok) return;
520
+ const data = await res.json();
521
+ const opps = (data.opportunities || []).slice(0, 6);
522
+ if (!opps.length) { feed.hidden = true; return; }
523
+ feed.hidden = false;
524
+ worldFeedShown = true;
525
+ cards.innerHTML = opps.map((o) => {
526
+ const pct = Math.round(o.score * 100);
527
+ const tag = o.reason === 'match' ? '与你匹配' : '世界变化';
528
+ return `<div class="world-feed-card">
529
+ <div class="world-feed-card-top">
530
+ <span class="world-feed-title">${escapeHtml(String(o.title || '').slice(0, 34))}</span>
531
+ <span class="world-feed-pct">${pct}%<small> ${tag}</small></span>
532
+ </div>
533
+ <div class="world-feed-summary">${escapeHtml(String(o.summary || '').slice(0, 60))}</div>
534
+ <div class="world-feed-actions">
535
+ <button class="world-feed-btn" data-wf-action="accept" data-wf-id="${o.id}">看看</button>
536
+ <button class="world-feed-btn ghost" data-wf-action="ignore" data-wf-id="${o.id}">忽略</button>
537
+ </div>
538
+ </div>`;
539
+ }).join('');
540
+ cards.querySelectorAll('[data-wf-action]').forEach((btn) => {
541
+ btn.addEventListener('click', async () => {
542
+ const id = btn.dataset.wfId;
543
+ const action = btn.dataset.wfAction;
544
+ try { await fetch('/api/opportunities/' + id + '/' + action, { method: 'POST' }); } catch (e) {}
545
+ const card = btn.closest('.world-feed-card');
546
+ if (card) {
547
+ card.style.opacity = '0.3';
548
+ card.style.filter = action === 'ignore' ? 'grayscale(1)' : 'none';
549
+ card.style.borderLeftColor = action === 'accept' ? '#c4d640' : 'transparent';
550
+ }
551
+ });
552
+ });
553
+ } catch (e) {
554
+ feed.hidden = true; // 拉不到就不显示世界区 (不阻塞首页)
555
+ }
556
+ // 持续流入: 世界区可见时每 45s 自动刷新 (AI 持续观察, 不是人刷新)
557
+ if (feed && !feed.hidden && worldFeedShown && !worldFeedTimer) {
558
+ worldFeedTimer = setInterval(() => { loadWorldFeed(); }, 45000);
559
+ }
560
+ }
561
+
509
562
  // === 卡片封面 (docs/fig 导出 → src/web/covers, 每个 agent 唯一不重复) ===
510
563
  let _coverList = null;
511
564
  async function loadCovers() {
@@ -533,6 +586,7 @@
533
586
  async function loadAgentCovers() {
534
587
  const track = $('#card-track');
535
588
  if (!track) return;
589
+ loadWorldFeed(); // 2026-10-05: 世界机会流自动流入 (不阻塞智能体卡片, fire-and-forget)
536
590
  await loadCovers();
537
591
  track.innerHTML = '<div class="card-loading">加载智能体卡片...</div>';
538
592
  try {