@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();
@@ -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/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.4';
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.4",
3
+ "version": "0.6.5",
4
4
  "channel": "stable",
5
5
  "build": "stable",
6
- "commit": "0dfa01104a71ed0351be8402a6b85efaf909df19",
7
- "builtAt": "2026-10-05T10:49:06.717Z",
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
  }