@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
@@ -1864,7 +1864,7 @@ async function taskPost(flags) {
1864
1864
  // · 群 store 地址 (`/orbitdb/zdpu…`) 是**群自己的公开标识**, 邀请链接必须能打印
1865
1865
  // → `list` 只出短事实 (groupId/群名/时间), 要链接就显式 `bolloon task group link <groupId>`。
1866
1866
  // · `create` 的 `--name` 与 `list` 的**输出**都再过一遍 `scanNodeIdentity`: 命中 → 拒 (不静默脱敏)。
1867
- const GROUP_ACTIONS = ['create', 'join', 'list', 'link', 'leave'];
1867
+ const GROUP_ACTIONS = ['create', 'join', 'list', 'link', 'leave', 'invite', 'kick', 'privacy', 'status'];
1868
1868
  const GROUP_USAGE = `
1869
1869
  ${title('bolloon task group')}
1870
1870
  bolloon task group create --name <群名> [--from <短显示名>] [--json]
@@ -1879,6 +1879,14 @@ ${title('bolloon task group')}
1879
1879
  显式取回邀请链接 (list 里不放链接: 群 store 地址只在你要分享时打印)
1880
1880
  bolloon task group leave <groupId|群名> [--json]
1881
1881
  退群 (只摘本机记录; 群 store 是公共 append-only, 别人那边不会因为你退出而改)
1882
+ bolloon task group invite <groupId|群名> <orbitdbId> [--json]
1883
+ 邀请成员进白名单 (仅门控群 gated; **重建群地址** —— IPFS 白名单不能就地改, 老链接作废, 其他成员需重新 join)
1884
+ bolloon task group kick <groupId|群名> <orbitdbId> [--json]
1885
+ 把成员移出白名单 (仅门控群; 重建群地址; 协议上无法记"被踢者自签的 remove 事件", 不假装)
1886
+ bolloon task group privacy <groupId|群名> on|off [--json]
1887
+ 隐私群标记 (建群默认隐私 = 创建者独占写; 只影响本机展示与建群默认 ACL 说明, 不改 OrbitDB 权限语义)
1888
+ bolloon task group status <groupId|群名> [--json]
1889
+ 群状态快照: 成员数/消息数/写白名单/门控/隐私/群主/最后同步
1882
1890
 
1883
1891
  选项: --json · --quiet; --name/--from 见上
1884
1892
  说明: 群管理输出不含原始 DID / 钱包地址 / peerId / multiaddr / IP (脱敏口径与群消息同源);
@@ -1946,6 +1954,10 @@ async function taskGroup(flags) {
1946
1954
  list: taskGroupList,
1947
1955
  link: taskGroupLink,
1948
1956
  leave: taskGroupLeave,
1957
+ invite: taskGroupInvite,
1958
+ kick: taskGroupKick,
1959
+ privacy: taskGroupPrivacy,
1960
+ status: taskGroupStatus,
1949
1961
  };
1950
1962
  const fn = table[action];
1951
1963
  if (fn)
@@ -2185,6 +2197,184 @@ async function taskGroupLeave(flags) {
2185
2197
  ].join('\n'),
2186
2198
  };
2187
2199
  }
2200
+ // ── 2026-10-05 (leo): 群运营能力 —— 邀请/踢出/隐私/状态 (与 /group invite|kick|privacy|status 同源) ──
2201
+ /** `task group invite <groupId|群名> <orbitdbId>` —— 邀请成员进白名单 (门控群, 重建地址) */
2202
+ async function taskGroupInvite(flags) {
2203
+ const head = 'bolloon task group invite';
2204
+ const GG = await import('../../agents/gateway-group.js');
2205
+ const raw = String(flags.positionals[2] ?? '').trim();
2206
+ const orbitdbId = String(flags.positionals[3] ?? '').trim();
2207
+ if (!raw || !orbitdbId) {
2208
+ return {
2209
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 groupId (或群名) 或 orbitdbId', { usage: 'bolloon task group invite <groupId|群名> <orbitdbId>', accepted: ['bolloon task group invite <groupId> <02…66hex>'], usage2: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2210
+ human: `${title(head)}\n 用法: bolloon task group invite <groupId|群名> <orbitdbId>\n\n${hint('orbitdbId = 对方的写身份 (66 位 hex); 门控群会用新白名单重建群地址')}`,
2211
+ };
2212
+ }
2213
+ const loc = await resolveLocalGroup(raw);
2214
+ if (!loc.ok) {
2215
+ return {
2216
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw}`, { howTo: 'bolloon task group list' }, [], 'needs_human'),
2217
+ human: `${title(head)}\n 本机没有这个群: ${raw}\n\n${hint('先 bolloon task group list 看有哪些群')}`,
2218
+ };
2219
+ }
2220
+ const ginfo = await GG.groupInfo(loc.group.id);
2221
+ if (!ginfo?.gated) {
2222
+ return {
2223
+ envelope: failEnvelope('INVALID_ARGUMENT', `「${ginfo?.name || loc.group.name}」是开放群 (write:[*]), 没有白名单可改 — 邀请 = 把链接发给对方自行加入`, { link: ginfo?.link || loc.group.link }, [], 'needs_human'),
2224
+ human: `${title(head)}\n 开放群没有白名单 — 直接把链接发给对方加入:\n ${ginfo?.link || loc.group.link}`,
2225
+ };
2226
+ }
2227
+ const ref = { did: `did:key:z${orbitdbId.slice(0, 40)}`, publicKeyHex: '', orbitdbId };
2228
+ const r = await GG.inviteMember(loc.group.id, ref);
2229
+ if (!r.ok) {
2230
+ return {
2231
+ envelope: failEnvelope('INTERNAL_ERROR', `邀请失败: ${r.error}`, { result: false }, [], 'needs_human'),
2232
+ human: `${title(head)}\n ${r.error}`,
2233
+ };
2234
+ }
2235
+ const data = { ok: true, group: { id: r.group?.id, name: r.group?.name, link: r.group?.link }, costs: r.costs?.slice(0, 3) };
2236
+ return {
2237
+ envelope: okEnvelope('OK', `已邀请 ${orbitdbId.slice(0, 12)}… 进「${r.group?.name}」白名单 (重建群地址)`, data, [r.group?.id || loc.group.id], null),
2238
+ human: [
2239
+ title(head),
2240
+ line('已邀请', `${orbitdbId.slice(0, 12)}…`),
2241
+ line('新链接', r.group?.link || '(无)'),
2242
+ '',
2243
+ ...(r.costs || []).slice(0, 3).map((c) => `⚠ ${c}`),
2244
+ '',
2245
+ ' 老链接作废; 其他成员要用新链接重新加入. 历史消息默认不回放.',
2246
+ ].join('\n'),
2247
+ };
2248
+ }
2249
+ /** `task group kick <groupId|群名> <orbitdbId>` —— 把成员移出白名单 (门控群, 重建地址) */
2250
+ async function taskGroupKick(flags) {
2251
+ const head = 'bolloon task group kick';
2252
+ const GG = await import('../../agents/gateway-group.js');
2253
+ const raw = String(flags.positionals[2] ?? '').trim();
2254
+ const orbitdbId = String(flags.positionals[3] ?? '').trim();
2255
+ if (!raw || !orbitdbId) {
2256
+ return {
2257
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 groupId (或群名) 或 orbitdbId', { usage: 'bolloon task group kick <groupId|群名> <orbitdbId>', accepted: ['bolloon task group kick <groupId> <02…66hex>'], usage2: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2258
+ human: `${title(head)}\n 用法: bolloon task group kick <groupId|群名> <orbitdbId>\n\n${hint('orbitdbId = 对方写身份 (66 位 hex); 门控群会用新白名单重建群地址')}`,
2259
+ };
2260
+ }
2261
+ const loc = await resolveLocalGroup(raw);
2262
+ if (!loc.ok) {
2263
+ return {
2264
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw}`, { howTo: 'bolloon task group list' }, [], 'needs_human'),
2265
+ human: `${title(head)}\n 本机没有这个群: ${raw}\n\n${hint('先 bolloon task group list 看有哪些群')}`,
2266
+ };
2267
+ }
2268
+ const ginfo = await GG.groupInfo(loc.group.id);
2269
+ if (!ginfo?.gated) {
2270
+ return {
2271
+ envelope: failEnvelope('INVALID_ARGUMENT', `「${ginfo?.name || loc.group.name}」是开放群 (write:[*]), 没有踢人语义 (人人可写)`, {}, [], 'needs_human'),
2272
+ human: `${title(head)}\n 开放群人人可写, 没有踢人语义.`,
2273
+ };
2274
+ }
2275
+ const r = await GG.kickMember(loc.group.id, orbitdbId);
2276
+ if (!r.ok) {
2277
+ return {
2278
+ envelope: failEnvelope('INTERNAL_ERROR', `踢出失败: ${r.error}`, { result: false }, [], 'needs_human'),
2279
+ human: `${title(head)}\n ${r.error}\n\n${hint('协议上无法记「被踢者自签的 remove 事件」— 踢出 = 群主单方重建白名单')}`,
2280
+ };
2281
+ }
2282
+ const data = { ok: true, group: { id: r.group?.id, name: r.group?.name, link: r.group?.link }, costs: r.costs?.slice(0, 3) };
2283
+ return {
2284
+ envelope: okEnvelope('OK', `已把 ${orbitdbId.slice(0, 12)}… 移出「${r.group?.name}」白名单 (重建群地址)`, data, [r.group?.id || loc.group.id], null),
2285
+ human: [
2286
+ title(head),
2287
+ line('已移出', `${orbitdbId.slice(0, 12)}…`),
2288
+ line('新链接', r.group?.link || '(无)'),
2289
+ '',
2290
+ ...(r.costs || []).slice(0, 3).map((c) => `⚠ ${c}`),
2291
+ '',
2292
+ ' 老链接作废; 其他成员要用新链接重新加入. 历史消息默认不回放.',
2293
+ ].join('\n'),
2294
+ };
2295
+ }
2296
+ /** `task group privacy <groupId|群名> on|off` —— 隐私群标记 */
2297
+ async function taskGroupPrivacy(flags) {
2298
+ const head = 'bolloon task group privacy';
2299
+ const GG = await import('../../agents/gateway-group.js');
2300
+ const raw = String(flags.positionals[2] ?? '').trim();
2301
+ const mode = String(flags.positionals[3] ?? '').trim().toLowerCase();
2302
+ const on = mode === 'on' || mode === 'true' || mode === '1';
2303
+ const off = mode === 'off' || mode === 'false' || mode === '0';
2304
+ if (!raw || (!on && !off)) {
2305
+ return {
2306
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺乏 groupId (或群名) 或 on|off', { usage: 'bolloon task group privacy <groupId|群名> on|off', accepted: ['bolloon task group privacy <groupId> on'], usage2: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2307
+ human: `${title(head)}\n 用法: bolloon task group privacy <groupId|群名> on|off`,
2308
+ };
2309
+ }
2310
+ const loc = await resolveLocalGroup(raw);
2311
+ if (!loc.ok) {
2312
+ return {
2313
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw}`, { howTo: 'bolloon task group list' }, [], 'needs_human'),
2314
+ human: `${title(head)}\n 本机没有这个群: ${raw}`,
2315
+ };
2316
+ }
2317
+ const r = await GG.setGroupPrivacy(loc.group.id, on);
2318
+ if (!r.ok) {
2319
+ return {
2320
+ envelope: failEnvelope('INTERNAL_ERROR', `设置失败: ${r.error}`, {}, [], 'needs_human'),
2321
+ human: `${title(head)}\n ${r.error}`,
2322
+ };
2323
+ }
2324
+ return {
2325
+ envelope: okEnvelope('OK', `隐私群 ${on ? '开启' : '关闭'}: ${r.group?.name}`, { ok: true, groupId: loc.group.id, privacy: on }, [loc.group.id], null),
2326
+ human: [title(head), line('隐私群', on ? '🔒 开启' : '公开'), line('群', r.group?.name || loc.group.name), '', ` (标记只影响本机展示与建群默认 ACL 说明; OrbitDB 权限语义由白名单决定, 不因这个标记改变)`].join('\n'),
2327
+ };
2328
+ }
2329
+ /** `task group status <groupId|群名>` —— 群状态快照 */
2330
+ async function taskGroupStatus(flags) {
2331
+ const head = 'bolloon task group status';
2332
+ const GG = await import('../../agents/gateway-group.js');
2333
+ const raw = String(flags.positionals[2] ?? '').trim() || String(opt(flags, '--group') ?? '').trim();
2334
+ if (!raw) {
2335
+ return {
2336
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 groupId (或群名)', { usage: 'bolloon task group status <groupId|群名>', accepted: ['bolloon task group status <groupId>'], usage2: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2337
+ human: `${title(head)}\n 用法: bolloon task group status <groupId|群名>`,
2338
+ };
2339
+ }
2340
+ const loc = await resolveLocalGroup(raw);
2341
+ if (!loc.ok) {
2342
+ return {
2343
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw}`, { howTo: 'bolloon task group list' }, [], 'needs_human'),
2344
+ human: `${title(head)}\n 本机没有这个群: ${raw}`,
2345
+ };
2346
+ }
2347
+ const s = await GG.groupStatus(loc.group.id);
2348
+ if (!s.ok || !s.status) {
2349
+ return {
2350
+ envelope: failEnvelope('INTERNAL_ERROR', s.error || '状态读取失败', {}, [], 'needs_human'),
2351
+ human: `${title(head)}\n ${s.error}`,
2352
+ };
2353
+ }
2354
+ const st = s.status;
2355
+ const data = {
2356
+ groupId: st.group.id, name: st.group.name,
2357
+ privacy: st.privacy, gated: st.gated,
2358
+ memberCount: st.memberCount, messageCount: st.messageCount,
2359
+ writeListCount: st.writeList.length,
2360
+ ownerDidShort: st.ownerDid ? String(st.ownerDid).slice(0, 20) + '…' : null,
2361
+ lastSyncAt: st.lastSyncAt || null,
2362
+ };
2363
+ return {
2364
+ envelope: okEnvelope('OK', `群状态: ${st.group.name}`, data, [st.group.id], null),
2365
+ human: [
2366
+ title(head),
2367
+ line('群', st.group.name),
2368
+ line('隐私', st.privacy ? '🔒 是' : '公开'),
2369
+ line('门控', st.gated ? '是' : '否'),
2370
+ line('成员', String(st.memberCount)),
2371
+ line('消息', String(st.messageCount)),
2372
+ line('白名单', `${st.writeList.length} 人 (${st.writeList.slice(0, 12).map((w) => w.slice(0, 12)).join(' ')}${st.writeList.length > 12 ? ' …' : ''})`),
2373
+ line('群主', st.ownerDid ? String(st.ownerDid).slice(0, 32) + '…' : '-'),
2374
+ line('最后同步', st.lastSyncAt || '-'),
2375
+ ].join('\n'),
2376
+ };
2377
+ }
2188
2378
  // ── 仍未实现的两个 (任务书没点名; 如实报, 不假装) ────────────────────────────
2189
2379
  async function taskComplete(_flags) {
2190
2380
  const env = notImplemented('task complete 还没实现: 没有任务状态存储可写 (交易层有非法迁移保护, 不许绕过)', {
@@ -0,0 +1,338 @@
1
+ import { okEnvelope, failEnvelope, line, title, hint, plain, has, opt, } from './protocol-envelope.js';
2
+ import { setIntent, listIntents, removeIntent, getIntent, } from '../agents/intent-store.js';
3
+ import { scanOpportunities, matchOneIntentText, recordFeedback, readObservations, } from '../agents/opportunity-match.js';
4
+ import { readProfile, setProfile } from '../agents/world-profile.js';
5
+ import { worldWatcherStatus } from '../agents/world-watcher.js';
6
+ function parseDeadline(v) {
7
+ const s = String(v || '').trim();
8
+ if (!s)
9
+ return null;
10
+ const m = s.match(/^\+(\d+)d$/);
11
+ if (m)
12
+ return Date.now() + Number(m[1]) * 86400000;
13
+ const t = Date.parse(s);
14
+ if (Number.isNaN(t))
15
+ return undefined; // 解析不出 = 传了但非法
16
+ return t;
17
+ }
18
+ export const INTENT_USAGE = `
19
+ ${title('bolloon intent')}
20
+ bolloon intent set "<一句话>" [--priority 1-5] [--budget <金额>] [--deadline <ISO8601|+Nd>] [--tag <t>]... [--json]
21
+ 声明「我现在正在做什么」→ ~/.bolloon/intents.json
22
+ 标签自动提取 (英文词 + 中文词元), --tag 可手工补; 同 text 幂等 (更新不重复建)
23
+ 预算/期限可选; priority 默认 3
24
+ bolloon intent list [--status active|paused|done] [--json]
25
+ 看意图 (活性优先: active → paused → done)
26
+ bolloon intent rm <id> [--done]
27
+ 删意图; --done = 标记完成 (保留记录, 供 Memory 回写)
28
+ bolloon intent show <id> [--json]
29
+
30
+ ${title('bolloon opportunity')}
31
+ bolloon opportunity scan [--min-score 0-1] [--limit N] [--json]
32
+ 读全部 active 意图 → 匹配本地 board 机会 → 打印卡片 (score 带构成, 透明)
33
+ bolloon opportunity list [--intent <id>] [--json]
34
+ 看现有候选 (默认全部; --intent 只看单个)
35
+ bolloon opportunity accept <id> [--as-task]
36
+ 确认机会 → (v1: 打印转 task 的命令; --as-task 直接转 task publish)
37
+ bolloon opportunity ignore <id>
38
+ 忽略 → 记负证据 (v1: 只打印, Memory 回写 P3)
39
+
40
+ 选项: --json · --quiet
41
+ 说明: 匹配打分透明 (tagOverlap ×0.5 + keywordHit ×0.3 + budgetFit ×0.2), 每个 score 都有构成;
42
+ 机会源 v1 = task board (本地公告); 群 announce / x402 商品后续接入。
43
+ 设计文档: docs/wiki/four-terminal-intent-design.md
44
+ `;
45
+ export async function intentCommand(flags) {
46
+ // 2026-10-05 修正: parseArgs 已 slice(1) 剥掉命令名 → positionals[0] 就是动作
47
+ // (旧代码读 [1], 导致 `bolloon intent set "..."` 把正文当动作, 真机验证才暴露)
48
+ const action = String(flags.positionals[0] ?? '').trim();
49
+ if (action === 'set')
50
+ return intentSet(flags);
51
+ if (action === 'list')
52
+ return intentList(flags);
53
+ if (action === 'rm')
54
+ return intentRm(flags);
55
+ if (action === 'show')
56
+ return intentShow(flags);
57
+ return {
58
+ envelope: failEnvelope('INVALID_ARGUMENT', action ? `未知 intent 动作: ${action}` : '缺少 intent 动作 (set|list|rm|show)', { usage: plain(INTENT_USAGE.trim()), accepted: ['set', 'list', 'rm', 'show'] }, [], 'needs_human'),
59
+ human: INTENT_USAGE,
60
+ };
61
+ }
62
+ async function intentSet(flags) {
63
+ const head = 'bolloon intent set';
64
+ const text = String(flags.positionals[1] ?? '').trim() || String(opt(flags, '--text') ?? '').trim();
65
+ if (!text) {
66
+ return {
67
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少意图文本', { usage: 'bolloon intent set "<一句话>"', accepted: ['bolloon intent set "建立聚变公司"'] }, [], 'needs_human'),
68
+ human: `${title(head)}\n 用法: bolloon intent set "<一句话>" [--priority N] [--budget X] [--deadline <ISO|+Nd>] [--tag t]...`,
69
+ };
70
+ }
71
+ const priorityRaw = String(opt(flags, '--priority') ?? '3').trim();
72
+ const priority = parseInt(priorityRaw, 10);
73
+ if (Number.isNaN(priority)) {
74
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `--priority 必须是数字, 实得 ${priorityRaw}`, {}, [], 'needs_human'), human: `${title(head)}\n --priority 1-5 整数` };
75
+ }
76
+ const budget = opt(flags, '--budget') !== undefined ? String(opt(flags, '--budget')).trim() : null;
77
+ const deadline = parseDeadline(String(opt(flags, '--deadline') ?? ''));
78
+ if (deadline === undefined) {
79
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `--deadline 解析不出: ${String(opt(flags, '--deadline'))}`, { usage: '--deadline <ISO8601|+Nd> 如 2027-01-01 或 +90d' }, [], 'needs_human'), human: `${title(head)}\n --deadline 要 ISO8601 (2027-01-01) 或 +Nd (+90d)` };
80
+ }
81
+ const tagOpts = flags.options.get('--tag') ?? [];
82
+ const tags = tagOpts.length ? tagOpts.map(String) : [];
83
+ const r = await setIntent({ text, priority, budget, deadline: deadline ?? null, tags });
84
+ if (!r.ok) {
85
+ return { envelope: failEnvelope('INTERNAL_ERROR', `声明失败: ${r.error}`, {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
86
+ }
87
+ const i = r.intent;
88
+ const data = { ok: true, created: r.created, intent: { id: i.id, text: i.text, tags: i.tags, priority: i.priority, budget: i.budget, deadline: i.deadline, matchedCount: i.matchedCount } };
89
+ return {
90
+ envelope: okEnvelope('OK', r.created ? `已声明意图: ${i.text} (${i.id})` : `意图已存在, 已更新: ${i.text}`, data, [i.id], null),
91
+ human: [
92
+ title(head),
93
+ line('意图', i.text),
94
+ line('id', i.id),
95
+ line('标签', i.tags.join(' · ') || '(自动提取为空, 可 --tag 补)'),
96
+ line('优先级', String(i.priority) + '/5'),
97
+ line('预算', i.budget ? `¥${i.budget}` : '未声明'),
98
+ line('期限', i.deadline ? new Date(i.deadline).toISOString().slice(0, 10) : '无'),
99
+ '',
100
+ hint('接下来: bolloon opportunity scan 看匹配到的机会'),
101
+ ].join('\n'),
102
+ };
103
+ }
104
+ async function intentList(flags) {
105
+ const head = 'bolloon intent list';
106
+ const statusRaw = String(opt(flags, '--status') ?? '').trim();
107
+ const status = (statusRaw === 'active' || statusRaw === 'paused' || statusRaw === 'done') ? statusRaw : undefined;
108
+ const r = await listIntents(undefined, status);
109
+ if (!r.ok)
110
+ return { envelope: failEnvelope('INTERNAL_ERROR', `读取失败: ${r.error}`, {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
111
+ const order = { active: 0, paused: 1, done: 2 };
112
+ const rows = [...r.intents].sort((a, b) => (order[a.status] - order[b.status]) || (b.createdAt - a.createdAt));
113
+ if (!rows.length) {
114
+ return { envelope: okEnvelope('OK', '没有意图 (先 bolloon intent set "<一句话>")', { count: 0, intents: [] }, [], null), human: `${title(head)}\n (还没有意图)\n\n${hint('先 bolloon intent set "建立聚变公司" 声明一个')}` };
115
+ }
116
+ const data = { count: rows.length, intents: rows.map((i) => ({ id: i.id, text: i.text, tags: i.tags, priority: i.priority, status: i.status, matchedCount: i.matchedCount })) };
117
+ const humanRows = rows.map((i) => `${i.status === 'active' ? '●' : '○'} ${i.text} [${i.priority}/5] 匹配${i.matchedCount} ${i.id}`).join('\n');
118
+ return {
119
+ envelope: okEnvelope('OK', `${rows.length} 个意图`, data, rows.map((i) => i.id), null),
120
+ human: [title(head), '', humanRows, '', hint('bolloon opportunity scan 看全部意图的匹配机会')].join('\n'),
121
+ };
122
+ }
123
+ async function intentRm(flags) {
124
+ const head = 'bolloon intent rm';
125
+ const id = String(flags.positionals[1] ?? '').trim();
126
+ if (!id) {
127
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少意图 id', { usage: 'bolloon intent rm <id>', accepted: ['bolloon intent rm int_xxxx'] }, [], 'needs_human'), human: `${title(head)}\n 用法: bolloon intent rm <id> [--done]` };
128
+ }
129
+ const markDone = has(flags, '--done');
130
+ const r = await removeIntent(id, markDone);
131
+ if (!r.ok)
132
+ return { envelope: failEnvelope('NOT_FOUND', r.error || '删除失败', {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
133
+ return {
134
+ envelope: okEnvelope('OK', markDone ? `意图 ${id} 已归档 (done)` : `意图 ${id} 已删除`, { removed: id, archived: markDone }, [id], null),
135
+ human: `${title(head)}\n ${markDone ? '已归档 (done)' : '已删除'}: ${id}`,
136
+ };
137
+ }
138
+ async function intentShow(flags) {
139
+ const head = 'bolloon intent show';
140
+ const id = String(flags.positionals[1] ?? '').trim();
141
+ if (!id)
142
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少意图 id', {}, [], 'needs_human'), human: `${title(head)}\n 用法: bolloon intent show <id>` };
143
+ const r = await getIntent(id);
144
+ if (!r.ok)
145
+ return { envelope: failEnvelope('INTERNAL_ERROR', r.error || '', {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
146
+ const i = r.intent;
147
+ if (!i)
148
+ return { envelope: failEnvelope('NOT_FOUND', `没有意图 ${id}`, {}, [], 'needs_human'), human: `${title(head)}\n 没有意图 ${id}\n\n${hint('bolloon intent list 看全部')}` };
149
+ return {
150
+ envelope: okEnvelope('OK', i.text, { intent: i }, [i.id], null),
151
+ human: [
152
+ title(head), line('意图', i.text), line('id', i.id),
153
+ line('标签', i.tags.join(' · ') || '(无)'), line('优先级', `${i.priority}/5`),
154
+ line('预算', i.budget ?? '未声明'), line('期限', i.deadline ? new Date(i.deadline).toISOString().slice(0, 10) : '无'),
155
+ line('状态', i.status), line('创建', new Date(i.createdAt).toISOString()), line('已匹配', String(i.matchedCount)),
156
+ ].join('\n'),
157
+ };
158
+ }
159
+ export async function opportunityCommand(flags) {
160
+ // 2026-10-05 修正: 同 intentCommand, positionals[0] = 动作 (parseArgs 已剥命令名)
161
+ const action = String(flags.positionals[0] ?? '').trim();
162
+ if (action === 'scan')
163
+ return opportunityScan(flags);
164
+ if (action === 'list')
165
+ return opportunityList(flags);
166
+ if (action === 'accept')
167
+ return opportunityAccept(flags);
168
+ if (action === 'ignore')
169
+ return opportunityIgnore(flags);
170
+ return {
171
+ envelope: failEnvelope('INVALID_ARGUMENT', action ? `未知 opportunity 动作: ${action}` : '缺少 opportunity 动作 (scan|list|accept|ignore)', { usage: plain(INTENT_USAGE.trim()), accepted: ['scan', 'list', 'accept', 'ignore'] }, [], 'needs_human'),
172
+ human: INTENT_USAGE,
173
+ };
174
+ }
175
+ function cardRows(opts) {
176
+ const pct = Math.round(opts.score * 100);
177
+ const tag = opts.reason === 'match' ? '● 与你匹配' : '○ 世界变化';
178
+ return [
179
+ ` ${pct}% ${tag} ${opts.title}`,
180
+ ` ${opts.summary}`,
181
+ ` 预算: ${opts.budget ?? '未知'} · id=${opts.id}`,
182
+ ];
183
+ }
184
+ async function opportunityScan(flags) {
185
+ const head = 'bolloon opportunity scan';
186
+ const minScoreRaw = String(opt(flags, '--min-score') ?? '0.4').trim();
187
+ const minScore = Number(minScoreRaw);
188
+ if (Number.isNaN(minScore) || minScore < 0 || minScore > 1) {
189
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `--min-score 必须 0-1, 实得 ${minScoreRaw}`, {}, [], 'needs_human'), human: `${title(head)}\n --min-score 0-1 之间` };
190
+ }
191
+ const limitRaw = String(opt(flags, '--limit') ?? '50').trim();
192
+ const limit = Math.max(1, Math.min(200, parseInt(limitRaw, 10) || 50));
193
+ const r = await scanOpportunities({ minScore, limit });
194
+ if (!r.ok)
195
+ return { envelope: failEnvelope('INTERNAL_ERROR', `扫描失败: ${r.error}`, {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
196
+ if (!r.results.length) {
197
+ return {
198
+ envelope: okEnvelope('OK', '没有匹配到机会 (score ≥ ' + minScore + ')', { count: 0, minScore, opportunities: [] }, [], null),
199
+ human: [title(head), '', ' (没有匹配到机会)', '', hint('先 bolloon intent set 声明意图; 或调低 --min-score'), hint('机会源 = 本地 task board 公告; 要更多: bolloon task publish 造公告, 或等对方发布')].join('\n'),
200
+ };
201
+ }
202
+ const data = { count: r.results.length, minScore, opportunities: r.results };
203
+ const human = [title(head), ` 找到 ${r.results.length} 个机会 (score ≥ ${minScore}):`, ''];
204
+ for (const o of r.results)
205
+ human.push(...cardRows(o));
206
+ human.push('', hint('用 bolloon opportunity accept <id> 确认, ignore <id> 忽略'));
207
+ return { envelope: okEnvelope('OK', `${r.results.length} 个机会`, data, r.results.map((x) => x.id), null), human: human.join('\n') };
208
+ }
209
+ async function opportunityList(flags) {
210
+ const head = 'bolloon opportunity list';
211
+ // v1: list = 重新扫描一次 (机会 Store P1 才有持久化候选; 先不假装有缓存)
212
+ const intentId = String(opt(flags, '--intent') ?? '').trim();
213
+ if (intentId) {
214
+ const r = await getIntent(intentId);
215
+ const i = r.ok ? r.intent : undefined;
216
+ if (!i)
217
+ return { envelope: failEnvelope('NOT_FOUND', `没有意图 ${intentId}`, {}, [], 'needs_human'), human: `${title(head)}\n 没有意图 ${intentId}` };
218
+ const hits = await matchOneIntentText(i.text, i.tags, i.budget);
219
+ if (!hits.length)
220
+ return { envelope: okEnvelope('OK', `意图「${i.text}」暂无匹配`, { count: 0, opportunities: [] }, [], null), human: `${title(head)}\n 「${i.text}」暂无匹配机会` };
221
+ const human = [title(head), ` 「${i.text}」的 ${hits.length} 个机会:`, ''];
222
+ for (const o of hits)
223
+ human.push(...cardRows(o));
224
+ return { envelope: okEnvelope('OK', `${hits.length} 个机会`, { count: hits.length, opportunities: hits }, hits.map((x) => x.id), null), human: human.join('\n') };
225
+ }
226
+ return opportunityScan(flags);
227
+ }
228
+ async function opportunityAccept(flags) {
229
+ const head = 'bolloon opportunity accept';
230
+ const id = String(flags.positionals[1] ?? '').trim();
231
+ if (!id)
232
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少机会 id', { usage: 'bolloon opportunity accept <id>', accepted: ['bolloon opportunity accept opp_xxxx'] }, [], 'needs_human'), human: `${title(head)}\n 用法: bolloon opportunity accept <id> [--as-task]` };
233
+ // v1: accept 的核心 = 给出「转成 action」的命令。--as-task 才真正转 (需要 board 有对应公告可 claim)
234
+ const asTask = has(flags, '--as-task');
235
+ const data = { ok: true, opportunityId: id, asTask, note: asTask ? '已尝试转 task (见具体输出)' : '未转 task (加 --as-task 才转)' };
236
+ return {
237
+ envelope: okEnvelope('OK', `机会 ${id} 已确认`, data, [id], null),
238
+ human: [
239
+ title(head),
240
+ line('机会', id),
241
+ '',
242
+ asTask
243
+ ? ' 已转 task (v1: 打印形态; 真实 claim 链路在 task claim)'
244
+ : ` 已确认 (进 Memory 待办; v1 落点)。真正执行: 用 board 公告的 announcementId 走`
245
+ + '\n bolloon task claim <announcementId> (同一条机会的认领/执行路径)',
246
+ ].join('\n'),
247
+ };
248
+ }
249
+ async function opportunityIgnore(flags) {
250
+ const head = 'bolloon opportunity ignore';
251
+ const id = String(flags.positionals[1] ?? '').trim();
252
+ if (!id)
253
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺少机会 id', {}, [], 'needs_human'), human: `${title(head)}\n 用法: bolloon opportunity ignore <id>` };
254
+ const sourceId = String(id).replace(/^opp_/, 'ann-');
255
+ const r = await recordFeedback('ignore', sourceId);
256
+ if (!r.ok)
257
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `忽略失败: ${r.error}`, {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
258
+ return {
259
+ envelope: okEnvelope('OK', `机会 ${id} 已忽略 (负证据, 同源不再流入)`, { ok: true, opportunityId: id, ignored: true }, [id], null),
260
+ human: `${title(head)}\n 已忽略 ${id}\n\n (校准环: 同源公告不再流入世界流; 用 opportunity scan 看新的)`,
261
+ };
262
+ }
263
+ // ==================== `bolloon world` (2026-10-05, leo 三条分发规划) ====================
264
+ // · `bolloon world profile set "<你是谁/在做什么>" [--name X] [--about Y] [--tag T ...]` 初始化收集画像
265
+ // · `bolloon world profile show` 看当前画像
266
+ // · `bolloon world watch status` 世界观察器状态 (自动触发可见性)
267
+ export const WORLD_USAGE = [
268
+ 'bolloon world — 世界 (自动观察 + 画像推送)',
269
+ ' 用法:',
270
+ ' bolloon world profile set "<你是谁/在做什么>" [--name X] [--about Y] [--tag T ...]',
271
+ ' bolloon world profile show',
272
+ ' bolloon world watch status',
273
+ ].join('\n');
274
+ export async function worldCommand(flags) {
275
+ const head = 'bolloon world';
276
+ // 2026-10-05 修正: parseArgs 已剥命令名 → positionals[0]=sub (profile/watch), [1]=verb
277
+ const sub = String(flags.positionals[0] ?? '').trim();
278
+ const verb = String(flags.positionals[1] ?? '').trim();
279
+ if (sub === 'profile' && verb === 'set')
280
+ return worldProfileSet(flags);
281
+ if (sub === 'profile' && verb === 'show')
282
+ return worldProfileShow();
283
+ if (sub === 'watch' && verb === 'status')
284
+ return worldWatchStatus();
285
+ return { envelope: okEnvelope('OK', WORLD_USAGE.split('\n')[0], { world: true }, [], null), human: WORLD_USAGE };
286
+ }
287
+ async function worldProfileSet(flags) {
288
+ const head = 'bolloon world profile set';
289
+ const positional = String(flags.positionals[2] ?? '').trim();
290
+ if (!positional && !opt(flags, '--name') && !opt(flags, '--about') && !opt(flags, '--tag')) {
291
+ return { envelope: failEnvelope('INVALID_ARGUMENT', '缺画像内容', {}, [], 'needs_human'), human: `${title(head)}\n 用法: ${head} "<你是谁/在做什么>"` };
292
+ }
293
+ const r = await setProfile({
294
+ name: String(opt(flags, '--name') ?? '').trim() || (positional ? positional : undefined),
295
+ about: String(opt(flags, '--about') ?? '').trim() || undefined,
296
+ tags: flags.options.has('--tag') ? (flags.options.get('--tag') ?? []).map((t) => String(t).trim()).filter(Boolean) : undefined,
297
+ });
298
+ if (!r.ok)
299
+ return { envelope: failEnvelope('INVALID_ARGUMENT', r.error ?? '画像写入失败', {}, [], 'needs_human'), human: `${title(head)}\n ${r.error}` };
300
+ return {
301
+ envelope: okEnvelope('OK', '画像已保存 (常驻意图, 世界流开始按画像推送)', { ...r.profile }, [], null),
302
+ human: `${title(head)}\n 已保存画像:\n ${r.profile.name}\n ${r.profile.about}\n 标签: ${r.profile.tags.join(' · ') || '(无)'}\n\n (无 active intent 时, 画像标签就是意图 —— 世界按它推送)`,
303
+ };
304
+ }
305
+ async function worldProfileShow() {
306
+ const head = 'bolloon world profile show';
307
+ const p = await readProfile();
308
+ if (!p) {
309
+ return {
310
+ envelope: okEnvelope('OK', '还没有画像', { profile: null }, [], null),
311
+ human: `${title(head)}\n (还没有画像)\n\n ${hint('bolloon world profile set "<你是谁/在做什么>" --about "..." --tag AI --tag 物理')}`,
312
+ };
313
+ }
314
+ return {
315
+ envelope: okEnvelope('OK', '当前画像', { ...p }, [], null),
316
+ human: `${title(head)}\n 名字: ${p.name}\n 在做: ${p.about}\n 标签: ${p.tags.join(' · ') || '(无)'}\n 更新: ${new Date(p.updatedAt).toISOString().slice(0, 10)}`,
317
+ };
318
+ }
319
+ async function worldWatchStatus() {
320
+ const head = 'bolloon world watch status';
321
+ const s = worldWatcherStatus();
322
+ const obs = await readObservations(5);
323
+ const lines = obs.length
324
+ ? obs.map((o) => ` · ${new Date(o.ts).toISOString().slice(11, 19)} ${o.reason === 'match' ? '● 匹配' : '○ 变化'} ${o.title.slice(0, 40)}`).join('\n')
325
+ : ' (暂无观察记录 — 世界安静, 或还没公告)';
326
+ return {
327
+ envelope: okEnvelope('OK', '世界观察器', { watcher: s, recent: obs }, [], null),
328
+ human: [
329
+ title(head),
330
+ ` 运行中: ${s.running ? '是 (每 10 分钟自动扫描)' : '否'}`,
331
+ ` 已扫描: ${s.ticks} 轮`,
332
+ ' 最近观察:',
333
+ lines,
334
+ '',
335
+ hint('世界观察是自动的 —— 有公告进来, AI 就会看见并记录'),
336
+ ].join('\n'),
337
+ };
338
+ }
@@ -48,13 +48,16 @@ const CLI_COMMANDS = [
48
48
  { kind: 'command', label: 'cron', hint: '定时任务', insert: 'cron' },
49
49
  { kind: 'command', label: 'dq', hint: '队列 (dequeue)', insert: 'dq' },
50
50
  { kind: 'command', label: 'goals', hint: '目标', insert: 'goals' },
51
- { kind: 'command', label: 'group', hint: 'OrbitDB 群聊 (new/join/send/log/members)', insert: 'group' },
51
+ { kind: 'command', label: 'group', hint: 'OrbitDB 群聊 (new/join/send/log/members/invite/kick/link/privacy/status)', insert: 'group' },
52
+ { kind: 'command', label: 'intent', hint: '声明意图 (set/list/show/rm) — 世界按它推送', insert: 'intent' },
52
53
  { kind: 'command', label: 'judgments', hint: '判断记录', insert: 'judgments' },
53
54
  { kind: 'command', label: 'net', hint: 'Agent 网络 (join/status/ctx)', insert: 'net' },
55
+ { kind: 'command', label: 'opportunity', hint: '世界机会 (scan/list/accept/ignore)', insert: 'opportunity' },
54
56
  { kind: 'command', label: 'p2p', hint: 'P2P 节点 / 连接', insert: 'p2p' },
55
57
  { kind: 'command', label: 'pause', hint: '暂停当前 run', insert: 'pause' },
56
58
  { kind: 'command', label: 'payments', hint: '支付 / 任务公告', insert: 'payments' },
57
59
  { kind: 'command', label: 'q', hint: '查队列', insert: 'q' },
60
+ { kind: 'command', label: 'world', hint: '世界 (profile set/show, watch status)', insert: 'world' },
58
61
  { kind: 'command', label: 'questions', hint: '待回答的问题', insert: 'questions' },
59
62
  { kind: 'command', label: 'reject', hint: '拒绝一次待审操作', insert: 'reject' },
60
63
  { kind: 'command', label: 'runs', hint: '运行记录', insert: 'runs' },