@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.
- package/dist/agents/opportunity-inbox.js +147 -0
- package/dist/agents/opportunity-match.js +48 -7
- package/dist/agents/opportunity-web-search.js +183 -0
- package/dist/agents/world-watcher.js +30 -1
- package/dist/cli/intent-command.js +1 -1
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +80 -82
- package/dist/ios/routes-world.js +54 -0
- package/dist/ios/style.css +39 -0
- package/dist/ios/sw.js +1 -1
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +80 -82
- package/dist/web/index.html +27 -46
- package/dist/web/routes-world.js +54 -0
- package/dist/web/style.css +39 -0
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
package/dist/web/client.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 \
|
|
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
|
-
|
|
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, "<")}" 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, "<")}" 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, "<")}</div>
|
|
5094
|
+
${o.search?.url ? `<div style="margin-top:6px;">\u{1F517} <a href="${String(o.search.url).replace(/</g, "<")}" 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, "<")}</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, "<")}</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")
|
|
5107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
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
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
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
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/dist/web/index.html
CHANGED
|
@@ -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) —
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
<div style="
|
|
431
|
-
|
|
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:
|
|
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:
|
|
446
|
-
<div style="font-size:12px;color:#909088;margin-bottom:12px;">✦
|
|
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"
|
|
442
|
+
<div class="form-hint">世界正在观察…</div>
|
|
449
443
|
</div>
|
|
450
444
|
</main>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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;">
|
package/dist/web/routes-world.js
CHANGED
|
@@ -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
|
}
|
package/dist/web/style.css
CHANGED
|
@@ -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.
|
|
9
|
+
const CACHE = 'bolloon-mobile-v0.6.5';
|
|
10
10
|
const SHELL = [
|
|
11
11
|
'./mobile.html',
|
|
12
12
|
'./mobile.css',
|