@bolloon/bolloon-agent 0.6.2 → 0.6.4

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.
Files changed (54) hide show
  1. package/README.md +24 -17
  2. package/dist/agents/gateway-group.js +172 -4
  3. package/dist/agents/group-autopilot.js +29 -1
  4. package/dist/agents/intent-store.js +193 -0
  5. package/dist/agents/opportunity-match.js +260 -0
  6. package/dist/agents/pi-sdk-tools.js +22 -2
  7. package/dist/agents/pi-sdk.js +8 -1
  8. package/dist/agents/world-profile.js +49 -0
  9. package/dist/agents/world-watcher.js +75 -0
  10. package/dist/cli/commands/tasks.js +191 -1
  11. package/dist/cli/intent-command.js +338 -0
  12. package/dist/cli/mention-data.js +4 -1
  13. package/dist/cli/model-selector.js +84 -104
  14. package/dist/cli/protocol-envelope.js +2 -0
  15. package/dist/cli/setup-wizard.js +2 -10
  16. package/dist/cli/tui-select.js +99 -10
  17. package/dist/cli-entry.js +25 -9
  18. package/dist/electron-build/agents/gateway-group.js +177 -3
  19. package/dist/electron-build/agents/gateway-group.js.map +1 -1
  20. package/dist/electron-build/agents/group-autopilot.js +30 -2
  21. package/dist/electron-build/agents/group-autopilot.js.map +1 -1
  22. package/dist/electron-build/agents/pi-sdk-tools.js +21 -1
  23. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  24. package/dist/electron-build/agents/pi-sdk.js +8 -1
  25. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  26. package/dist/electron-build/llm/config-store.js +2 -2
  27. package/dist/electron-build/llm/config-store.js.map +1 -1
  28. package/dist/electron-build/llm/model-selection.js +16 -2
  29. package/dist/electron-build/llm/model-selection.js.map +1 -1
  30. package/dist/electron-build/llm/pi-ai.js +2 -2
  31. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  32. package/dist/index.js +121 -3
  33. package/dist/ios/bolloon-web.json +3 -3
  34. package/dist/ios/client.js +232 -0
  35. package/dist/ios/index.html +10 -0
  36. package/dist/ios/mobile.css +80 -0
  37. package/dist/ios/mobile.html +10 -0
  38. package/dist/ios/mobile.js +54 -0
  39. package/dist/ios/routes-world.js +105 -0
  40. package/dist/ios/server.js +7 -0
  41. package/dist/ios/sw.js +1 -1
  42. package/dist/llm/config-store.js +2 -2
  43. package/dist/llm/model-selection.js +16 -2
  44. package/dist/llm/pi-ai.js +2 -2
  45. package/dist/web/bolloon-web.json +3 -3
  46. package/dist/web/client.js +232 -0
  47. package/dist/web/index.html +57 -0
  48. package/dist/web/mobile.css +80 -0
  49. package/dist/web/mobile.html +10 -0
  50. package/dist/web/mobile.js +54 -0
  51. package/dist/web/routes-world.js +105 -0
  52. package/dist/web/server.js +7 -0
  53. package/dist/web/sw.js +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,105 @@
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
+ function json(res, status, body) {
6
+ res.status(status).json(body);
7
+ }
8
+ function bodyOf(req) {
9
+ return req.body && typeof req.body === 'object' ? req.body : {};
10
+ }
11
+ /** 限制输入形状: 只收明确字段, 不整个吞前端对象 */
12
+ function intentInputFromBody(body) {
13
+ const b = bodyOf(body);
14
+ const out = {};
15
+ if (typeof b.text === 'string' && b.text.trim())
16
+ out.text = b.text.trim();
17
+ if (Array.isArray(b.tags))
18
+ out.tags = b.tags.filter((t) => typeof t === 'string').map(String).slice(0, 24);
19
+ if (b.priority !== undefined)
20
+ out.priority = Number(b.priority);
21
+ if (typeof b.budget === 'string')
22
+ out.budget = b.budget.trim();
23
+ if (b.deadline !== undefined) {
24
+ const d = typeof b.deadline === 'number' ? b.deadline : Date.parse(String(b.deadline));
25
+ if (!Number.isNaN(d))
26
+ out.deadline = d;
27
+ }
28
+ return out;
29
+ }
30
+ export function registerWorldRoutes(app) {
31
+ // ── intents ──────────────────────────────────────────────────────────────
32
+ app.get('/api/intents', async (_req, res) => {
33
+ const r = await listIntents();
34
+ if (!r.ok)
35
+ return json(res, 500, { error: r.error });
36
+ const order = { active: 0, paused: 1, done: 2 };
37
+ const rows = [...r.intents].sort((a, b) => order[a.status] - order[b.status] || b.createdAt - a.createdAt);
38
+ json(res, 200, { ok: true, count: rows.length, intents: rows });
39
+ });
40
+ app.post('/api/intents', async (req, res) => {
41
+ const input = intentInputFromBody(req.body);
42
+ if (!input.text)
43
+ return json(res, 400, { ok: false, error: 'text 必填 (声明你现在在做什么)' });
44
+ const r = await setIntent(input);
45
+ if (!r.ok)
46
+ return json(res, 400, { ok: false, error: r.error });
47
+ json(res, r.created ? 201 : 200, { ok: true, created: r.created, intent: r.intent });
48
+ });
49
+ app.get('/api/intents/:id', async (req, res) => {
50
+ const r = await getIntent(req.params.id);
51
+ if (!r.ok)
52
+ return json(res, 500, { error: r.error });
53
+ if (!r.intent)
54
+ return json(res, 404, { ok: false, error: `没有意图 ${req.params.id}` });
55
+ json(res, 200, { ok: true, intent: r.intent });
56
+ });
57
+ app.delete('/api/intents/:id', async (req, res) => {
58
+ const done = req.query.done === '1' || req.query.done === 'true';
59
+ const r = await removeIntent(req.params.id, done);
60
+ if (!r.ok)
61
+ return json(res, 404, { ok: false, error: r.error });
62
+ json(res, 200, { ok: true, removed: r.removed, archived: done });
63
+ });
64
+ // ── opportunities ────────────────────────────────────────────────────────
65
+ app.get('/api/opportunities', async (req, res) => {
66
+ const minScoreRaw = typeof req.query['min-score'] === 'string' ? req.query['min-score'] : '0.4';
67
+ const minScore = Number(minScoreRaw);
68
+ const limitRaw = typeof req.query.limit === 'string' ? req.query.limit : '50';
69
+ const limit = Math.max(1, Math.min(200, parseInt(limitRaw, 10) || 50));
70
+ const r = await scanOpportunities({ minScore: Number.isFinite(minScore) ? minScore : 0.4, limit });
71
+ if (!r.ok)
72
+ return json(res, 500, { ok: false, error: r.error });
73
+ json(res, 200, { ok: true, count: r.results.length, minScore, opportunities: r.results });
74
+ });
75
+ app.post('/api/opportunities/:id/ignore', async (req, res) => {
76
+ const sourceId = String(req.params.id).replace(/^opp_/, 'ann-');
77
+ const r = await recordFeedback('ignore', sourceId);
78
+ json(res, r.ok ? 200 : 400, { ok: r.ok, opportunityId: req.params.id, ignored: r.ok, error: r.error });
79
+ });
80
+ app.post('/api/opportunities/:id/accept', async (req, res) => {
81
+ const sourceId = String(req.params.id).replace(/^opp_/, 'ann-');
82
+ const r = await recordFeedback('accept', sourceId);
83
+ json(res, r.ok ? 200 : 400, { ok: r.ok, opportunityId: req.params.id, accepted: r.ok, error: r.error });
84
+ });
85
+ // ── 用户画像 (leo 三条分发规划③: 初始化收集 → 常驻意图推送) ─────────────────
86
+ app.get('/api/world/profile', async (_req, res) => {
87
+ const profile = await readProfile();
88
+ json(res, 200, { ok: true, profile });
89
+ });
90
+ app.put('/api/world/profile', async (req, res) => {
91
+ const b = bodyOf(req);
92
+ const r = await setProfile({
93
+ name: typeof b.name === 'string' ? b.name : undefined,
94
+ about: typeof b.about === 'string' ? b.about : undefined,
95
+ tags: Array.isArray(b.tags) ? b.tags.filter((t) => typeof t === 'string') : undefined,
96
+ });
97
+ json(res, r.ok ? 200 : 400, { ok: r.ok, profile: r.profile, error: r.error });
98
+ });
99
+ // ── 世界观察器状态 (自动触发机制可见性) ─────────────────────────────────────
100
+ app.get('/api/world/watch', async (_req, res) => {
101
+ const status = worldWatcherStatus();
102
+ const observations = await readObservations(20);
103
+ json(res, 200, { ok: true, watcher: status, recentObservations: observations });
104
+ });
105
+ }
@@ -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);
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.2';
9
+ const CACHE = 'bolloon-mobile-v0.6.4';
10
10
  const SHELL = [
11
11
  './mobile.html',
12
12
  './mobile.css',
@@ -161,8 +161,8 @@ export const PROVIDER_INFO = {
161
161
  // 2026-07-17: V3 系列 (deepseek-chat / deepseek-reasoner) 官方已下线, 改 V4
162
162
  deepseek: { name: 'DeepSeek', description: '深度求索大模型 (V4)', requiresApiKey: true, models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
163
163
  kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot Kimi K 系列模型', requiresApiKey: true, models: ['kimi-k3', 'kimi-k2-0905', 'moonshot-v1-128k', 'moonshot-v1-32k'] },
164
- glm: { name: 'GLM (智谱)', description: '智谱 GLM 系列模型', requiresApiKey: true, models: ['glm-5.2', 'glm-4.6', 'glm-4.5', 'glm-4-flash', 'glm-4'] },
165
- qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen3-max', 'qwen-max', 'qwen-plus', 'qwen-turbo'] },
164
+ glm: { name: 'GLM (智谱)', description: '智谱 GLM 系列模型', requiresApiKey: true, models: ['glm-5.3-flash', 'glm-5.3', 'glm-5.2', 'glm-5', 'glm-4.6', 'glm-4.5', 'glm-4.7', 'glm-4-flash', 'glm-4'] },
165
+ qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen3.8-max', 'qwen3-max', 'qwen-max', 'qwen-plus', 'qwen-turbo'] },
166
166
  mimo: { name: 'MiMo (小米)', description: '小米 MiMo V2 系列 (openai 兼容)', requiresApiKey: true, models: ['mimo-v2.5-pro', 'mimo-v2-pro', 'mimo-v2-omni', 'mimo-v2-flash', 'mimo-v2.5-pro-ultraspeed'] },
167
167
  grok: { name: 'Grok (xAI)', description: 'xAI Grok 系列模型 (openai 兼容)', requiresApiKey: true, models: ['grok-4.5', 'grok-4', 'grok-4-fast'] },
168
168
  local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
@@ -763,7 +763,21 @@ export function validateSelection(req, ctx) {
763
763
  return { ok: false, failureClass: 'invalid_model', message: `${provider} 没有可用模型 (配置为空且默认模型为空)` };
764
764
  }
765
765
  let baseUrl = base.baseUrl;
766
- if (req.baseUrl !== undefined) {
766
+ const urlAction = req.baseUrlAction ?? (req.baseUrl !== undefined ? 'replace' : 'keep');
767
+ if (urlAction === 'clear') {
768
+ // 清除 URL 覆盖 → 回落**官方默认** (内置表/注册表声明的那个, 不含配置里覆盖过的值)。
769
+ // leo 2026-10-05: 「展示的 URL 默认需要官方的来源」—— 清除 = 回到官方, 不是回到上一个手填值。
770
+ const official = baseDefaultsOf(provider, null);
771
+ baseUrl = normalizeBaseUrl(official?.baseUrl || '');
772
+ if (!baseUrl && regEntry && regEntry.origin === 'catalog') {
773
+ return {
774
+ ok: false,
775
+ failureClass: 'invalid_url',
776
+ message: `${provider} 在目录里没有官方 api 基址 → 无法回落官方默认, 必须自定义 baseUrl (加 --base-url <地址> 或写进配置)`,
777
+ };
778
+ }
779
+ }
780
+ else if (req.baseUrl !== undefined) {
767
781
  const shape = validateBaseUrlShape(req.baseUrl);
768
782
  if (!shape.ok) {
769
783
  return { ok: false, failureClass: 'invalid_url', message: `base URL 非法 — ${shape.reason}` };
@@ -974,7 +988,7 @@ export async function selectModel(req) {
974
988
  const p = await runConnectionProbe({
975
989
  provider: target.provider,
976
990
  model: target.model,
977
- baseUrl: req.baseUrl, // 显式 --base-url (最高优先)
991
+ baseUrl: target.baseUrl, // 校验已按 URL 意图算好 (keep=当前 / replace=新值 / clear=官方默认)
978
992
  configuredBaseUrl: stored?.baseUrl, // 配置里这一家现在写的地址
979
993
  apiKey: target.apiKey,
980
994
  });
package/dist/llm/pi-ai.js CHANGED
@@ -752,7 +752,7 @@ export class PiAIModel {
752
752
  // 2026-07-17: deepseek-chat (V3) 官方已下线, 迁 deepseek-v4-flash
753
753
  deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
754
754
  kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'kimi-k3',
755
- glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-5.2',
755
+ glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-5.3-flash',
756
756
  qwen: this.config.model || process.env.QWEN_MODEL || process.env.DASHSCOPE_MODEL || 'qwen3-max',
757
757
  // 小米 MiMo (openai 兼容) — env override 优先, 默认 mimo-v2.5-pro
758
758
  mimo: this.config.model || process.env.MIMO_MODEL || 'mimo-v2.5-pro',
@@ -1375,7 +1375,7 @@ function detectModel(provider) {
1375
1375
  // 2026-07-17: V3 官方下线, 迁 V4
1376
1376
  deepseek: 'deepseek-v4-flash',
1377
1377
  kimi: 'kimi-k3',
1378
- glm: 'glm-5.2',
1378
+ glm: 'glm-5.3-flash',
1379
1379
  qwen: 'qwen3-max',
1380
1380
  // 小米 MiMo 默认走最新旗舰版 (v2.5-Pro); 2026-06 当前公开版
1381
1381
  mimo: 'mimo-v2.5-pro',
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema": "bolloon-mobile-web/1",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "channel": "stable",
5
5
  "build": "stable",
6
- "commit": "2d3e3fe88725eeca27a1dc8edcb50c46b1173d6e",
7
- "builtAt": "2026-10-04T12:13:59.101Z",
6
+ "commit": "0dfa01104a71ed0351be8402a6b85efaf909df19",
7
+ "builtAt": "2026-10-05T10:49:06.717Z",
8
8
  "layer": "web",
9
9
  "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
10
  }
@@ -4992,6 +4992,238 @@ ${data.error || "channel not found"}`, "error");
4992
4992
  if (e.target === judgmentsModal) hideJudgmentsModal();
4993
4993
  });
4994
4994
  }
4995
+ var worldView = document.getElementById("world-view");
4996
+ var worldBtn = document.getElementById("world-btn");
4997
+ var worldViewClose = document.getElementById("world-view-close");
4998
+ 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
+ var worldIntentsList = document.getElementById("world-intents-list");
5005
+ var worldOpportunities = document.getElementById("world-opportunities");
5006
+ var worldStats = document.getElementById("world-stats");
5007
+ var worldBadge = document.getElementById("world-badge");
5008
+ var worldProfileInput = document.getElementById("world-profile-input");
5009
+ var worldProfileSet = document.getElementById("world-profile-set");
5010
+ var worldLoaded = false;
5011
+ var worldPollTimer = null;
5012
+ var WORLD_POLL_MS = 3e4;
5013
+ function worldStatus(msg) {
5014
+ if (worldError) {
5015
+ worldError.textContent = msg || "";
5016
+ worldError.style.display = msg ? "" : "none";
5017
+ }
5018
+ }
5019
+ async function fetchWorldIntents() {
5020
+ try {
5021
+ const res = await fetch("/api/intents");
5022
+ if (!res.ok) throw new Error("HTTP " + res.status);
5023
+ const data = await res.json();
5024
+ renderWorldIntents(data.intents || []);
5025
+ if (worldStats) {
5026
+ const active = (data.intents || []).filter((i) => i.status === "active").length;
5027
+ worldStats.textContent = `\u610F\u56FE ${active} \u4E2A\u6D3B\u8DC3 \xB7 \u4E16\u754C\u6301\u7EED\u89C2\u5BDF\u4E2D`;
5028
+ }
5029
+ return data.intents || [];
5030
+ } catch (e) {
5031
+ worldStatus("\u62C9\u53D6\u610F\u56FE\u5931\u8D25: " + (e && e.message ? e.message : e));
5032
+ return [];
5033
+ }
5034
+ }
5035
+ function renderWorldIntents(intents) {
5036
+ if (!worldIntentsList) return;
5037
+ 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>';
5039
+ return;
5040
+ }
5041
+ const active = intents.filter((i) => i.status === "active");
5042
+ const rows = active.map((i) => {
5043
+ const stars = "\u2605".repeat(i.priority) + "\u2606".repeat(5 - i.priority);
5044
+ 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;">
5045
+ <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>
5046
+ <button class="btn-secondary btn-sm" data-intent-rm="${i.id}" style="font-size:10px;padding:1px 6px;">\u2715</button>
5047
+ </div>`;
5048
+ }).join("");
5049
+ worldIntentsList.innerHTML = rows;
5050
+ worldIntentsList.querySelectorAll("[data-intent-rm]").forEach((btn) => {
5051
+ btn.addEventListener("click", async () => {
5052
+ try {
5053
+ await fetch("/api/intents/" + btn.dataset.intentRm, { method: "DELETE" });
5054
+ } catch (e) {
5055
+ }
5056
+ fetchWorldIntents();
5057
+ });
5058
+ });
5059
+ }
5060
+ async function refreshWorldOpportunities() {
5061
+ try {
5062
+ const res = await fetch("/api/opportunities?min-score=0.4");
5063
+ if (!res.ok) throw new Error("HTTP " + res.status);
5064
+ const data = await res.json();
5065
+ renderWorldOpportunities(data.opportunities || []);
5066
+ if (worldBadge) {
5067
+ const n = (data.opportunities || []).length;
5068
+ worldBadge.textContent = String(n);
5069
+ worldBadge.style.display = n > 0 ? "" : "none";
5070
+ }
5071
+ return data.opportunities || [];
5072
+ } catch (e) {
5073
+ worldStatus("\u626B\u63CF\u673A\u4F1A\u5931\u8D25: " + (e && e.message ? e.message : e));
5074
+ return [];
5075
+ }
5076
+ }
5077
+ function renderWorldOpportunities(opps) {
5078
+ if (!worldOpportunities) return;
5079
+ if (!opps.length) {
5080
+ worldOpportunities.innerHTML = '<div class="form-hint">\u4E16\u754C\u6B63\u5728\u6301\u7EED\u9001\u6765 \u2014 \u7A0D\u7B49\u4E00\u4E0B\u3002</div>';
5081
+ return;
5082
+ }
5083
+ const cards = opps.map((o) => {
5084
+ 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);">
5086
+ <div style="display:flex;justify-content:space-between;align-items:center;">
5087
+ <b style="font-size:14px;">${String(o.title || "").slice(0, 60).replace(/</g, "&lt;")}</b>
5088
+ <span style="color:#c4d640;font-weight:700;font-size:14px;">${pct}%</span>
5089
+ </div>
5090
+ <div style="font-size:12px;color:#909088;margin-top:4px;">${String(o.summary || "").slice(0, 90).replace(/</g, "&lt;")}</div>
5091
+ <div style="margin-top:8px;display:flex;gap:8px;">
5092
+ <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
+ <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>
5094
+ </div>
5095
+ </div>`;
5096
+ }).join("");
5097
+ worldOpportunities.innerHTML = cards;
5098
+ worldOpportunities.querySelectorAll("[data-opp-action]").forEach((btn) => {
5099
+ btn.addEventListener("click", async () => {
5100
+ const id = btn.dataset.oppId;
5101
+ if (btn.dataset.oppAction === "ignore") {
5102
+ try {
5103
+ await fetch("/api/opportunities/" + id + "/ignore", { method: "POST" });
5104
+ } catch (e) {
5105
+ }
5106
+ btn.closest(".world-opp-card").style.opacity = "0.3";
5107
+ btn.closest(".world-opp-card").style.filter = "grayscale(1)";
5108
+ } else {
5109
+ try {
5110
+ await fetch("/api/opportunities/" + id + "/accept", { method: "POST" });
5111
+ } catch (e) {
5112
+ }
5113
+ const srcId = id.replace(/^opp_/, "ann-");
5114
+ worldStatus("\u5DF2\u8BB0\u4E0B: " + srcId);
5115
+ btn.closest(".world-opp-card").style.borderColor = "#c4d640";
5116
+ }
5117
+ });
5118
+ });
5119
+ }
5120
+ async function loadWorld() {
5121
+ worldLoaded = true;
5122
+ worldStatus("");
5123
+ await fetchWorldIntents();
5124
+ await refreshWorldOpportunities();
5125
+ }
5126
+ function startWorldPoll() {
5127
+ if (worldPollTimer) clearInterval(worldPollTimer);
5128
+ worldPollTimer = setInterval(() => {
5129
+ refreshWorldOpportunities();
5130
+ fetchWorldIntents();
5131
+ }, WORLD_POLL_MS);
5132
+ }
5133
+ function stopWorldPoll() {
5134
+ if (worldPollTimer) {
5135
+ clearInterval(worldPollTimer);
5136
+ worldPollTimer = null;
5137
+ }
5138
+ }
5139
+ function showWorldView() {
5140
+ if (worldView) {
5141
+ worldView.hidden = false;
5142
+ if (!worldLoaded) loadWorld();
5143
+ else {
5144
+ fetchWorldIntents();
5145
+ refreshWorldOpportunities();
5146
+ }
5147
+ startWorldPoll();
5148
+ }
5149
+ }
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);
5156
+ if (worldRefreshBtn) worldRefreshBtn.addEventListener("click", () => {
5157
+ fetchWorldIntents();
5158
+ refreshWorldOpportunities();
5159
+ });
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;
5193
+ 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) });
5197
+ if (!res.ok) {
5198
+ const err = await res.json().catch(() => ({}));
5199
+ worldStatus("\u58F0\u660E\u5931\u8D25: " + (err.error || "HTTP " + res.status));
5200
+ return;
5201
+ }
5202
+ if (worldIntentInput) worldIntentInput.value = "";
5203
+ if (worldIntentBudget) worldIntentBudget.value = "";
5204
+ await fetchWorldIntents();
5205
+ await refreshWorldOpportunities();
5206
+ if (worldPollTimer) {
5207
+ clearInterval(worldPollTimer);
5208
+ startWorldPoll();
5209
+ }
5210
+ } catch (e) {
5211
+ worldStatus("\u58F0\u660E\u5931\u8D25: " + (e && e.message ? e.message : e));
5212
+ } finally {
5213
+ if (worldIntentSet) worldIntentSet.disabled = false;
5214
+ }
5215
+ });
5216
+ if (worldIntentInput) {
5217
+ worldIntentInput.addEventListener("keydown", (e) => {
5218
+ if (e.key === "Enter" && worldIntentSet) worldIntentSet.click();
5219
+ });
5220
+ }
5221
+ (async function autoWorldBadge() {
5222
+ try {
5223
+ await refreshWorldOpportunities();
5224
+ } catch (e) {
5225
+ }
5226
+ })();
4995
5227
  var judgmentImportBtn = document.getElementById("judgment-import-btn");
4996
5228
  var judgmentImportFile = document.getElementById("judgment-import-file");
4997
5229
  var judgmentCleanupBtn = document.getElementById("judgment-cleanup-btn");
@@ -165,6 +165,15 @@
165
165
  </svg>
166
166
  <span id="wallet-badge" class="task-badge" style="display:none;">0</span>
167
167
  </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>
168
177
  <button id="judgments-btn" class="header-action" title="我的判断">
169
178
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
170
179
  <path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path>
@@ -413,6 +422,54 @@
413
422
  </div>
414
423
  </div>
415
424
 
425
+ <!-- World View (2026-10-05, leo Intent Network) — Twitter 式整页: 中栏机会时间线自动流入 -->
426
+ <div id="world-view" hidden style="position:fixed;inset:0;z-index:999;background:var(--bg,#1a1a18);display:flex;flex-direction:column;">
427
+ <!-- 顶栏 -->
428
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:10px 18px;border-bottom:1px solid var(--border,#3a3a36);">
429
+ <div style="font-weight:700;font-size:15px;">🌍 World <span style="color:#606058;font-weight:400;font-size:12px;">世界在给你送东西</span></div>
430
+ <div style="display:flex;gap:8px;align-items:center;">
431
+ <button id="world-refresh-btn" class="btn-secondary btn-sm" style="font-size:12px;">⟳ 刷新</button>
432
+ <button id="world-view-close" class="btn-secondary btn-sm" style="font-size:12px;">← 回到聊天</button>
433
+ </div>
434
+ </div>
435
+ <!-- Twitter 式三栏 -->
436
+ <div style="display:flex;flex:1;overflow:hidden;">
437
+ <!-- 左栏: 我的意图 (nav 风格) -->
438
+ <aside style="width:240px;border-right:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;display:none;" id="world-left-col">
439
+ <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">我的意图 (Intent)</div>
440
+ <div id="world-intents-list" style="margin-bottom:12px;"><div class="form-hint">(还没有意图)</div></div>
441
+ <div style="font-size:12px;color:#909088;font-weight:600;margin-bottom:8px;">世界变化</div>
442
+ <div id="world-stats" style="font-size:12px;color:#606058;"></div>
443
+ </aside>
444
+ <!-- 中栏: 机会时间线 (Twitter feed 自动流入) -->
445
+ <main style="flex:1;overflow-y:auto;padding:16px;max-width:680px;margin:0 auto;width:100%;">
446
+ <div style="font-size:12px;color:#909088;margin-bottom:12px;">✦ 与你相关 (自动匹配, 持续流入)</div>
447
+ <div id="world-opportunities" style="min-height:120px;">
448
+ <div class="form-hint">世界正在观察… (等待意图声明或机会出现)</div>
449
+ </div>
450
+ </main>
451
+ <!-- 右栏: 声明意图 + 说明 -->
452
+ <aside style="width:280px;border-left:1px solid var(--border,#3a3a36);overflow-y:auto;padding:12px;flex-shrink:0;" id="world-right-col">
453
+ <div style="font-size:13px;font-weight:600;margin-bottom:6px;">先告诉我你是谁</div>
454
+ <input type="text" id="world-profile-input" placeholder="例: 我在做可控核聚变 + AI 仿真" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid var(--border,#3a3a36);background:var(--bg-card,#222220);color:var(--text);font-size:13px;">
455
+ <button id="world-profile-set" class="btn-secondary btn-sm" style="width:100%;padding:6px;font-size:12px;margin:6px 0 14px;">存为画像 → 世界按它推送</button>
456
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px;">我现在正在做什么</div>
457
+ <input type="text" id="world-intent-input" placeholder="例: 建立聚变创业项目 · 找科研合作" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid var(--border,#3a3a36);background:var(--bg-card,#222220);color:var(--text);font-size:13px;">
458
+ <div style="display:flex;gap:8px;margin:8px 0;">
459
+ <select id="world-intent-priority" style="font-size:12px;padding:6px;background:var(--bg-card,#222220);color:var(--text);border:1px solid var(--border,#3a3a36);border-radius:6px;">
460
+ <option value="3">★★★</option><option value="5">★★★★★</option><option value="4">★★★★</option><option value="2">★★</option><option value="1">★</option>
461
+ </select>
462
+ <input type="text" id="world-intent-budget" placeholder="预算 ¥" style="font-size:12px;padding:6px;width:80px;background:var(--bg-card,#222220);color:var(--text);border:1px solid var(--border,#3a3a36);border-radius:6px;">
463
+ </div>
464
+ <button id="world-intent-set" class="btn-primary btn-sm" style="width:100%;padding:8px;font-size:13px;">声明 → 世界开始找</button>
465
+ <div id="world-intent-error" style="font-size:12px;color:#f59e0b;margin-top:8px;min-height:16px;"></div>
466
+ <div style="font-size:11px;color:#606058;margin-top:12px;line-height:1.6;">
467
+ 世界会自己校准 —— 你「看看 / 忽略」的每个动作, 都让接下来的推送更准。
468
+ </div>
469
+ </aside>
470
+ </div>
471
+ </div>
472
+
416
473
  <div class="messages" id="messages"></div>
417
474
 
418
475
  <!-- 2026-06-15: 旧 loop-timeline-panel 删除 — 步骤状态条改在每条 AI 消息气泡内挂 step-timeline 组件 -->
@@ -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">