@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.
@@ -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:19:40.124Z",
6
+ "commit": "bc1ef6920ae4c4ef7c4d6b6433e81dcf3e17a55d",
7
+ "builtAt": "2026-10-06T07:02:09.463Z",
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");
@@ -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">
@@ -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 {
@@ -0,0 +1,159 @@
1
+ import { setIntent, listIntents, getIntent, removeIntent } from '../agents/intent-store.js';
2
+ import { scanOpportunities, recordFeedback, readObservations } from '../agents/opportunity-match.js';
3
+ import { readProfile, setProfile } from '../agents/world-profile.js';
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';
7
+ function json(res, status, body) {
8
+ res.status(status).json(body);
9
+ }
10
+ function bodyOf(req) {
11
+ return req.body && typeof req.body === 'object' ? req.body : {};
12
+ }
13
+ /** 限制输入形状: 只收明确字段, 不整个吞前端对象 */
14
+ function intentInputFromBody(body) {
15
+ const b = bodyOf(body);
16
+ const out = {};
17
+ if (typeof b.text === 'string' && b.text.trim())
18
+ out.text = b.text.trim();
19
+ if (Array.isArray(b.tags))
20
+ out.tags = b.tags.filter((t) => typeof t === 'string').map(String).slice(0, 24);
21
+ if (b.priority !== undefined)
22
+ out.priority = Number(b.priority);
23
+ if (typeof b.budget === 'string')
24
+ out.budget = b.budget.trim();
25
+ if (b.deadline !== undefined) {
26
+ const d = typeof b.deadline === 'number' ? b.deadline : Date.parse(String(b.deadline));
27
+ if (!Number.isNaN(d))
28
+ out.deadline = d;
29
+ }
30
+ return out;
31
+ }
32
+ export function registerWorldRoutes(app) {
33
+ // ── intents ──────────────────────────────────────────────────────────────
34
+ app.get('/api/intents', async (_req, res) => {
35
+ const r = await listIntents();
36
+ if (!r.ok)
37
+ return json(res, 500, { error: r.error });
38
+ const order = { active: 0, paused: 1, done: 2 };
39
+ const rows = [...r.intents].sort((a, b) => order[a.status] - order[b.status] || b.createdAt - a.createdAt);
40
+ json(res, 200, { ok: true, count: rows.length, intents: rows });
41
+ });
42
+ app.post('/api/intents', async (req, res) => {
43
+ const input = intentInputFromBody(req.body);
44
+ if (!input.text)
45
+ return json(res, 400, { ok: false, error: 'text 必填 (声明你现在在做什么)' });
46
+ const r = await setIntent(input);
47
+ if (!r.ok)
48
+ return json(res, 400, { ok: false, error: r.error });
49
+ json(res, r.created ? 201 : 200, { ok: true, created: r.created, intent: r.intent });
50
+ });
51
+ app.get('/api/intents/:id', async (req, res) => {
52
+ const r = await getIntent(req.params.id);
53
+ if (!r.ok)
54
+ return json(res, 500, { error: r.error });
55
+ if (!r.intent)
56
+ return json(res, 404, { ok: false, error: `没有意图 ${req.params.id}` });
57
+ json(res, 200, { ok: true, intent: r.intent });
58
+ });
59
+ app.delete('/api/intents/:id', async (req, res) => {
60
+ const done = req.query.done === '1' || req.query.done === 'true';
61
+ const r = await removeIntent(req.params.id, done);
62
+ if (!r.ok)
63
+ return json(res, 404, { ok: false, error: r.error });
64
+ json(res, 200, { ok: true, removed: r.removed, archived: done });
65
+ });
66
+ // ── opportunities ────────────────────────────────────────────────────────
67
+ app.get('/api/opportunities', async (req, res) => {
68
+ const minScoreRaw = typeof req.query['min-score'] === 'string' ? req.query['min-score'] : '0.4';
69
+ const minScore = Number(minScoreRaw);
70
+ const limitRaw = typeof req.query.limit === 'string' ? req.query.limit : '50';
71
+ const limit = Math.max(1, Math.min(200, parseInt(limitRaw, 10) || 50));
72
+ const r = await scanOpportunities({ minScore: Number.isFinite(minScore) ? minScore : 0.4, limit });
73
+ if (!r.ok)
74
+ return json(res, 500, { ok: false, error: r.error });
75
+ json(res, 200, { ok: true, count: r.results.length, minScore, opportunities: r.results });
76
+ });
77
+ app.post('/api/opportunities/:id/ignore', async (req, res) => {
78
+ const sourceId = String(req.params.id).replace(/^opp_/, 'ann-');
79
+ const r = await recordFeedback('ignore', sourceId);
80
+ json(res, r.ok ? 200 : 400, { ok: r.ok, opportunityId: req.params.id, ignored: r.ok, error: r.error });
81
+ });
82
+ app.post('/api/opportunities/:id/accept', async (req, res) => {
83
+ const sourceId = String(req.params.id).replace(/^opp_/, 'ann-');
84
+ const r = await recordFeedback('accept', sourceId);
85
+ json(res, r.ok ? 200 : 400, { ok: r.ok, opportunityId: req.params.id, accepted: r.ok, error: r.error });
86
+ });
87
+ // ── 用户画像 (leo 三条分发规划③: 初始化收集 → 常驻意图推送) ─────────────────
88
+ app.get('/api/world/profile', async (_req, res) => {
89
+ const profile = await readProfile();
90
+ json(res, 200, { ok: true, profile });
91
+ });
92
+ app.put('/api/world/profile', async (req, res) => {
93
+ const b = bodyOf(req);
94
+ const r = await setProfile({
95
+ name: typeof b.name === 'string' ? b.name : undefined,
96
+ about: typeof b.about === 'string' ? b.about : undefined,
97
+ tags: Array.isArray(b.tags) ? b.tags.filter((t) => typeof t === 'string') : undefined,
98
+ });
99
+ json(res, r.ok ? 200 : 400, { ok: r.ok, profile: r.profile, error: r.error });
100
+ });
101
+ // ── 世界观察器状态 (自动触发机制可见性) ─────────────────────────────────────
102
+ app.get('/api/world/watch', async (_req, res) => {
103
+ const status = worldWatcherStatus();
104
+ const observations = await readObservations(20);
105
+ json(res, 200, { ok: true, watcher: status, recentObservations: observations });
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
+ });
159
+ }
@@ -11,6 +11,8 @@ import { validateMessageInput, validateChannelInput, healthCheck, } from './inpu
11
11
  import { segmentChatReply } from '../agents/chat-segmenter.js';
12
12
  import { registerJudgmentsRoutes } from './routes-judgments.js';
13
13
  import { registerLlmConfigRoutes } from './routes-llm-config.js';
14
+ import { startWorldWatcher } from '../agents/world-watcher.js';
15
+ import { registerWorldRoutes } from './routes-world.js';
14
16
  import { registerExternalEngineRoutes } from './routes-external-engines.js';
15
17
  import { registerTaskRoutes } from './routes-tasks.js';
16
18
  // 2026-09-19: 联系方式 / 人工批准 / 手机—桌面配对 路由
@@ -6956,6 +6958,11 @@ fetchState();
6956
6958
  registerOnchainTradeRoutes(app);
6957
6959
  // 2026-07-06: LLM/Video/Audio 配置路由抽到 ./routes-llm-config.ts
6958
6960
  registerLlmConfigRoutes(app);
6961
+ // 2026-10-05 (leo 四端 Intent Network P1): World API — 意图声明 + 机会匹配 (Web/Mobile 共用)
6962
+ registerWorldRoutes(app);
6963
+ // 2026-10-05 (leo 三条分发规划①): 自动触发机制 —— 世界观察器常驻,
6964
+ // 定时扫描 + 观察日志自动积累, 不依赖用户打开界面 (AI 主动看世界)。
6965
+ startWorldWatcher();
6959
6966
  // 2026-07-22: 外部编码智能体 (codex/claude-code/opencode/openclaw/hermes/实验 API)
6960
6967
  // 发现 + 配置为供应商 + 委派
6961
6968
  registerExternalEngineRoutes(app);