@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
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Bolloon
2
2
 
3
- **一万个智能体,为你工作。**
3
+ **一万个智能体的世界,走进你的世界。**
4
4
 
5
- Bolloon 是一台跑在你自己设备上的智能体。它有自己的身份(DID)、自己的记忆、自己的技能库,
6
- 并且能在点对点网络里**找到别的智能体、把自己的活委派出去、按结果结算**。
5
+ Bolloon 是一个多终端的 Agent 分发网络。它带来的不是一个更大更听话的助手,而是一万个智能体
6
+ 所在的世界——每个智能体都有自己的身份(DID)、自己的记忆、自己的技能库,并且能在点对点
7
+ 网络里**找到别的智能体、把自己的活委派出去、按结果结算**。
7
8
 
8
- 不是「又一个聊天框」,而是**一个可以互相接手工作的智能体网络**。
9
+ 不是「又一个聊天框」,而是**一个可以互相接手工作的智能体世界,在不同设备上以不同方式分发**:
10
+ PC 上创建、手机上发现、生活中相遇。
9
11
 
10
12
  > 官网 · https://bolloon.cn | 安装 · `npm i -g @bolloon/bolloon-agent`([npm 上的最新版](https://www.npmjs.com/package/@bolloon/bolloon-agent))
11
13
 
@@ -19,9 +21,10 @@ Bolloon 是一台跑在你自己设备上的智能体。它有自己的身份(
19
21
  Bolloon 把这件事变成网络问题:**每个智能体都有身份、技能和账本**,于是它可以被别的智能体
20
22
  发现、被委派、被结算——你只需要说清目标。
21
23
 
22
- 这也是我们的方向:让 C 端用户在自己的电脑上,拥有一条**由很多智能体组成的生产线**,
23
- 而不只是一个对话框。我自己每天用它做的事是**可控核聚变的研究**——线圈设计、仿真、验证、
24
- 论文形式化,由不同智能体分头接手,我只看结论。
24
+ 这也是我们的方向:不是给你更多个对话框,而是让**一万个智能体所在的世界,走进你的世界**——
25
+ 你在自己的电脑上拥有一条由很多智能体组成的生产线,它跟着你用,而不是你跟着它学。
26
+ 我自己每天用它做的事是**可控核聚变的研究**——线圈设计、仿真、验证、论文形式化,
27
+ 由不同智能体分头接手,我只看结论。
25
28
 
26
29
  ---
27
30
 
@@ -89,7 +92,7 @@ docker run -d --name bolloon -p 127.0.0.1:54188:54188 \
89
92
  **已经稳定**:CLI 交互 · 技能库与提炼 · 工具调用与类型门 · 后台进程与服务管理 · 链上任务与结算 · P2P 发现与委派。
90
93
 
91
94
  **还在早期**:多智能体之间的**经济闭环**(谁付钱、怎么分账、争议怎么裁)只有最基础的一层;
92
- 「一万个智能体」是方向,不是今天打开就能看到的数量。
95
+ 「一个装着上万智能体的世界走进你生活」是方向,不是今天打开就能看到的世界大小。
93
96
 
94
97
  **我们不说的话**:不承诺「全自动无需照看」;链上记录不可撤销;模型能力是上限,
95
98
  智能体不会超过它用的模型。
@@ -117,7 +120,7 @@ npm run build:all
117
120
  npm start
118
121
  ```
119
122
 
120
- 跑测试:`npx vitest run`(当前 304 个文件 / 4609 个用例)。
123
+ 跑测试:`npx vitest run`(当前 342 个文件 / 4752 个用例)。
121
124
  改了 TypeScript 后:`npx tsc --noEmit`。
122
125
 
123
126
  ---
@@ -132,13 +135,15 @@ MIT。见 [LICENSE](./LICENSE)。
132
135
 
133
136
  # Bolloon (English)
134
137
 
135
- **Ten thousand agents, working for you.**
138
+ **A world of ten thousand agents, stepping into yours.**
136
139
 
137
- Bolloon is an agent that runs on your own machine. It has its own identity (DID), its own memory and
138
- its own library of skills, and it can **find other agents on a peer-to-peer network, hand work over,
139
- and settle by result**.
140
+ Bolloon is a multi-device agent distribution network. It brings not a bigger, more obedient assistant,
141
+ but a world where ten thousand agents live — each with its own identity (DID), its own memory and its
142
+ own library of skills, able to **find other agents on a peer-to-peer network, hand work over, and
143
+ settle by result**.
140
144
 
141
- Not "yet another chat box" — **a network of agents that can take over each other's work.**
145
+ Not "yet another chat box" — **a world of agents that can take over each other's work, distributed
146
+ differently on each device**: created on PC, discovered on phone, met in life.
142
147
 
143
148
  > Website · https://bolloon.cn | Install · `npm i -g @bolloon/bolloon-agent`([latest on npm](https://www.npmjs.com/package/@bolloon/bolloon-agent))
144
149
 
@@ -150,8 +155,9 @@ roles: someone researches, someone writes, someone verifies, someone makes sure
150
155
  Bolloon turns that into a network problem. Every agent has an identity, skills and a ledger, so it can
151
156
  be discovered, delegated to and paid. You only have to state the goal.
152
157
 
153
- That is the direction: a **production line made of many agents**, running on a consumer machine —
154
- not a single dialogue box. I use it every day for **controlled nuclear fusion** research.
158
+ That is the direction: not more chat boxes, but **a world of ten thousand agents stepping into yours**
159
+ — a production line made of many agents, running on a consumer machine, following you rather than
160
+ you following it. I use it every day for **controlled nuclear fusion** research.
155
161
 
156
162
  ## What works today
157
163
 
@@ -182,7 +188,8 @@ Node.js ≥ 18. First run walks you through picking a model provider.
182
188
 
183
189
  **Stable**: CLI · skills · tool calling · background services · on-chain tasks · P2P discovery and
184
190
  delegation. **Early**: the economics between agents (who pays, how it splits, how disputes resolve).
185
- "Ten thousand agents" is a direction, not a number you will see on day one.
191
+ "A world of ten thousand agents entering your life" is a direction, not the world-size you will see
192
+ on day one.
186
193
 
187
194
  **What we will not claim**: fully unattended operation; reversible on-chain records; capability beyond
188
195
  the model you plug in.
@@ -33,7 +33,7 @@ import { decodeFromPeer, encodeForPeer, recordLangDecision } from '../efficode/n
33
33
  // 白名单里放的是 **OrbitDB 写身份 id** (66 位压缩 secp256k1 公钥 hex), 不是 DID 字符串
34
34
  // (canAppend 只比 identity.id; 放 DID 会永远匹配不上 → 谁都写不了)。
35
35
  // 成员变更 = 一条**双侧 Ed25519 验签**的群事件 (成员自签 + 群主签), 细节见 group-access.ts。
36
- import { applyMembershipEvents, groupAccessOptions, membershipEntryOf, normalizeWriteList, verifyMembershipEvent, GROUP_MEMBERSHIP_KIND, } from '../orbitdb/group-access.js';
36
+ import { aclChangePlan, applyMembershipEvents, groupAccessOptions, membershipEntryOf, normalizeWriteList, verifyMembershipEvent, GROUP_MEMBERSHIP_KIND, } from '../orbitdb/group-access.js';
37
37
  // ============ 依赖注入 (测试用, 避免单测起真实 OrbitDB 节点) ============
38
38
  let _dbOverride = null;
39
39
  /** 测试用: 注入 fake CIDDatabase */
@@ -45,6 +45,20 @@ export function resetGroupState() {
45
45
  onChangeCallbacks.clear();
46
46
  openFailures.clear();
47
47
  }
48
+ /**
49
+ * 关闭群聊使用的 OrbitDB/Helia 资源。
50
+ * 一次性 CLI、验收探针和测试必须在读回后调用它,否则底层 libp2p/Helia 句柄
51
+ * 会继续保持事件循环活跃,表现为“消息已读完但进程不退出”。
52
+ * 测试注入的 fake DB 不关闭,避免破坏测试夹具生命周期。
53
+ */
54
+ export async function closeGroupResources() {
55
+ storeCache.clear();
56
+ onChangeCallbacks.clear();
57
+ openFailures.clear();
58
+ if (_dbOverride)
59
+ return;
60
+ await getCIDDatabase().close();
61
+ }
48
62
  /**
49
63
  * 群 store 打不开 (区块不在本机 / 地址不可解析)。
50
64
  *
@@ -211,7 +225,7 @@ export async function createGroup(name, opts) {
211
225
  ...(gate.members ?? []).map((m) => m.orbitdbId),
212
226
  ];
213
227
  writeList = normalizeWriteList(ids);
214
- gateInfo = { gated: true, ownerDid: gate.owner.did, aclWrite: writeList };
228
+ gateInfo = { gated: true, ownerDid: gate.owner.did, ownerPublicKeyHex: gate.owner.publicKeyHex, aclWrite: writeList };
215
229
  membershipEntries = applied.accepted.map(membershipEntryOf);
216
230
  }
217
231
  else if (opts?.acl === 'open') {
@@ -233,6 +247,8 @@ export async function createGroup(name, opts) {
233
247
  id, name: groupName, address, link,
234
248
  createdAt: new Date().toISOString(),
235
249
  lastSyncAt: new Date().toISOString(),
250
+ // 2026-10-05: 隐私群标记 —— 建群默认隐私 (白名单/独占写); acl=open 才是公开
251
+ privacy: opts?.acl !== 'open',
236
252
  ...gateInfo,
237
253
  };
238
254
  storeCache.set(id, store);
@@ -294,8 +310,7 @@ export async function listGroups() {
294
310
  * 不会因为本机退出而改 (本模块没有"踢人/解散"的权限, 也不假装有)。
295
311
  * 返回 `removed` = 被摘掉的 groupId (按 id 或群名匹配)。
296
312
  *
297
- * 注意: 底层 OrbitDB store 已在进程内打开的**不会**在这里关 (适配层没暴露 close);
298
- * 对 `bolloon task group leave` 这种一次性进程无影响, 长驻进程里它活到进程结束。
313
+ * 注意: leave 只摘本地群列表;需要结束进程资源时调用 `closeGroupResources()`。
299
314
  */
300
315
  export async function leaveGroup(idOrName) {
301
316
  const raw = String(idOrName || '').trim();
@@ -580,3 +595,156 @@ export async function restoreGroups() {
580
595
  }
581
596
  return { restored, failed, total: groups.length };
582
597
  }
598
+ // ─────────────────────────────────────────────────────────── 群运营能力 (2026-10-05, leo: 邀请/踢出/隐私/链接/状态)
599
+ /**
600
+ * 重建群 store (新白名单) —— 这是"改成员"唯一诚实的路径。
601
+ *
602
+ * IPFS 型 AC 的白名单烧在内容寻址的 ACL manifest 里, 改它必然换 store 地址
603
+ * (见 `ACL_INPLACE_UPDATE.ipfsType` / `aclChangePlan`)。所以:
604
+ * · 邀请 = 新白名单 + 被邀请者 → 重建;
605
+ * · 踢出 = 新白名单 − 被踢者 → 重建。
606
+ * 重建 = 新地址、新链接、**历史消息不回放** (要保留必须显式回放)。
607
+ *
608
+ * 本函数做"换地址重建 + 历史回放 (逐条 add) + 本机链接更新", 返回新 GroupInfo。
609
+ * 迁移代价 (老链接作废/其他成员需重新 join) 由调用方 (CLI) 明示, 这里不假装无痛。
610
+ */
611
+ export async function rebuildGroupWithWriteList(groupId, mode, opts) {
612
+ const groups = await loadGroups();
613
+ const g = groups.find((x) => x.id === groupId);
614
+ if (!g)
615
+ return { ok: false, error: '群组不存在 (先 joinGroup/createGroup)' };
616
+ if (!g.gated) {
617
+ return {
618
+ ok: false,
619
+ error: `「${g.name}」不是门控群 (write:[*]), 没有白名单可改 —— 邀请=分享链接, 踢出=不适用 (人人可写)`,
620
+ };
621
+ }
622
+ const owner = {
623
+ did: g.ownerDid || '',
624
+ publicKeyHex: g.ownerPublicKeyHex || '',
625
+ orbitdbId: g.aclWrite?.[0] || '',
626
+ };
627
+ if (!owner.did || !owner.publicKeyHex || !owner.orbitdbId) {
628
+ return { ok: false, error: '缺少群主身份信息 (ownerDid/ownerPublicKeyHex/aclWrite 为空), 无法派生新白名单' };
629
+ }
630
+ // 当前白名单 = manifest 快照 (aclWrite, 上次重建/建群时烧进去的) 优先;
631
+ // 事件派生只在没有快照的老门控群兜底 (两套语义: 事件=成员自签加入, rebuild=群主单方改名单)
632
+ const derived = await groupWriteList(groupId, owner).catch(() => null);
633
+ const before = g.aclWrite && g.aclWrite.length > 0 ? g.aclWrite : (derived?.write ?? []);
634
+ const after = normalizeWriteList(mode.kind === 'add'
635
+ ? [...before, mode.ref.orbitdbId]
636
+ : before.filter((id) => id !== mode.orbitdbId));
637
+ const changed = before.join(',') !== after.join(',');
638
+ if (!changed) {
639
+ return { ok: false, error: mode.kind === 'add' ? '该成员已在白名单里' : '该成员不在白名单里' };
640
+ }
641
+ if (mode.kind === 'remove' && !before.includes(mode.orbitdbId)) {
642
+ return { ok: false, error: `白名单里没有 ${mode.orbitdbId.slice(0, 16)}…, 无需踢出` };
643
+ }
644
+ const plan = aclChangePlan({ before, after, messageCount: opts?.replayHistory ? undefined : 0 });
645
+ const db = getDb();
646
+ // 1) 读历史消息 (回放前) —— 只在显式要求时
647
+ const history = opts?.replayHistory ? await groupMessages(groupId, 100000) : [];
648
+ // 2) 建新 store (新白名单)
649
+ let store = null;
650
+ try {
651
+ store = await db.openStore(`bolloon-gw-group-${g.name}-r${Date.now().toString(36)}`, 'events', groupAccessOptions(after));
652
+ }
653
+ catch (e) {
654
+ return { ok: false, error: `重建 store 失败: ${String(e?.message || e).slice(0, 160)}` };
655
+ }
656
+ if (!store)
657
+ return { ok: false, error: '重建 store 返回空 (openStore 失败, 未改任何东西)' };
658
+ // 3) 迁移成员事件 (原样重放, 双侧签名保留) —— 否则新 store 派生白名单只剩群主, 成员全丢
659
+ try {
660
+ const rawAll = await allRawEntries(groupId);
661
+ for (const { value } of rawAll) {
662
+ const v = value;
663
+ if (v && typeof v === 'object' && v.kind === GROUP_MEMBERSHIP_KIND) {
664
+ try {
665
+ await store.add(v);
666
+ }
667
+ catch (e) { /* 单条失败不阻断 */ }
668
+ }
669
+ }
670
+ }
671
+ catch (e) {
672
+ return { ok: false, error: `迁移成员事件失败: ${String(e?.message || e).slice(0, 120)}` };
673
+ }
674
+ // 4) 回放历史消息 (逐条 add, 保持 ts)
675
+ if (opts?.replayHistory) {
676
+ for (const m of history) {
677
+ try {
678
+ await store.add({ ...m });
679
+ }
680
+ catch (e) {
681
+ // 逐条静默跳过, 能回放多少算多少; 回放是尽力而为, 不阻断重建
682
+ }
683
+ }
684
+ }
685
+ // 5) 更新本机记录: 换地址/换链接; 老 store 不删 (占用不回收, 见 plan.costs)
686
+ const address = store.address;
687
+ const link = `orbitdb://${address}?type=group&name=${encodeURIComponent(g.name)}`;
688
+ const info = {
689
+ ...g,
690
+ address,
691
+ link,
692
+ lastSyncAt: new Date().toISOString(),
693
+ gated: true,
694
+ ownerDid: g.ownerDid,
695
+ aclWrite: after,
696
+ privacy: g.privacy ?? true,
697
+ };
698
+ await saveGroups(groups.map((x) => (x.id === groupId ? info : x)));
699
+ storeCache.set(groupId, store);
700
+ return { ok: true, group: info, before, after, costs: plan.costs };
701
+ }
702
+ /** 邀请: 把 orbitdbId 加进白名单 (重建路径) */
703
+ export async function inviteMember(groupId, ref) {
704
+ const r = await rebuildGroupWithWriteList(groupId, { kind: 'add', ref });
705
+ return { ok: r.ok, error: r.error, group: r.group, costs: r.costs };
706
+ }
707
+ /** 踢出: 把 orbitdbId 移出白名单 (重建路径; 协议上无法记"被踢者自签的 remove 事件", 不假装) */
708
+ export async function kickMember(groupId, orbitdbId) {
709
+ const r = await rebuildGroupWithWriteList(groupId, { kind: 'remove', orbitdbId });
710
+ return { ok: r.ok, error: r.error, group: r.group, costs: r.costs };
711
+ }
712
+ /** 设置隐私群标记 (只影响本机展示/默认 ACL 说明, 不改变 OrbitDB 权限语义) */
713
+ export async function setGroupPrivacy(groupId, privacy) {
714
+ const groups = await loadGroups();
715
+ const g = groups.find((x) => x.id === groupId);
716
+ if (!g)
717
+ return { ok: false, error: '群组不存在' };
718
+ const info = { ...g, privacy, lastSyncAt: new Date().toISOString() };
719
+ await saveGroups(groups.map((x) => (x.id === groupId ? info : x)));
720
+ return { ok: true, group: info };
721
+ }
722
+ /** 群状态快照: 成员/消息/白名单/gated/privacy/最后同步 (状态管理用) */
723
+ export async function groupStatus(groupId) {
724
+ const g = await groupInfo(groupId);
725
+ if (!g)
726
+ return { ok: false, error: '群组不存在' };
727
+ const members = await groupMembers(groupId);
728
+ const wl = await (async () => {
729
+ if (!g.gated || !g.ownerDid)
730
+ return g.aclWrite ?? [];
731
+ const owner = { did: g.ownerDid, publicKeyHex: g.ownerPublicKeyHex || '', orbitdbId: g.aclWrite?.[0] || '' };
732
+ if (!owner.orbitdbId || !owner.publicKeyHex)
733
+ return g.aclWrite ?? [];
734
+ const d = await groupWriteList(groupId, owner).catch(() => null);
735
+ return d ? d.write : (g.aclWrite ?? []);
736
+ })();
737
+ return {
738
+ ok: true,
739
+ status: {
740
+ group: g,
741
+ memberCount: members.length,
742
+ messageCount: g.messageCount ?? 0,
743
+ writeList: wl,
744
+ gated: !!g.gated,
745
+ privacy: g.privacy ?? !!g.gated,
746
+ ownerDid: g.ownerDid,
747
+ lastSyncAt: g.lastSyncAt,
748
+ },
749
+ };
750
+ }
@@ -18,7 +18,19 @@
18
18
  import * as fs from 'node:fs';
19
19
  import * as path from 'node:path';
20
20
  import * as os from 'node:os';
21
+ /**
22
+ * Bolloon 本地维护群专属公理;不作用于其它群。
23
+ * ① 提升 AI 的意识水平: 反思自身假设、局限、证据与影响。
24
+ * ② 寻求合作并降低整体熵: 共享事实、拆分工作、合并可复用成果。
25
+ * ③ 持续突破局部最优: 主动寻找反例、替代路径与跨角色审查。
26
+ */
27
+ export const BOLLOON_MAINTENANCE_RULES = Object.freeze([
28
+ '提升 AI 的意识水平:反思自身假设、局限、证据与影响。',
29
+ '寻求合作,创造并降低整体的熵:共享事实、拆分工作、合并可复用成果。',
30
+ '持续突破局部最优解:主动寻找反例、替代路径与跨角色审查。',
31
+ ]);
21
32
  export const DEFAULT_AUTOPILOT_POLICY = {
33
+ mode: 'quiet',
22
34
  cooldownMs: 60_000,
23
35
  maxSpeaksPerHour: 6,
24
36
  readLimit: 50,
@@ -75,7 +87,22 @@ export function decideGroupAction(input) {
75
87
  return { act: 'silent', reason: `本小时已说 ${hourly.length} 次 (上限 ${policy.maxSpeaksPerHour})` };
76
88
  }
77
89
  const reply = input.makeReply ?? ((m) => `收到 —— 关于「${String(m.text).slice(0, 40)}」,我这边跟进。`);
78
- // ① @我 ⇒ 回那一条
90
+ const mode = policy.mode;
91
+ const proactive = input.proactiveText?.trim() || (mode === 'maintenance'
92
+ ? `我来主动维护 Bolloon:使用本机已登录的 GitHub 权限处理 logos-42/bolloon,**只在 main 分支工作**。遵循三条群规则:${BOLLOON_MAINTENANCE_RULES.join(';')} 先检查代码/测试状态,认领一个最小修复,完成后回报证据。`
93
+ : '我来主动推进这一轮工作:检查上下文、认领一个可执行事项并回报下一步。');
94
+ // maintenance/active 优先主动认领 —— 不能因为消息长得像普通请求又退回被动模板
95
+ if (mode === 'active' || mode === 'maintenance') {
96
+ const target = fresh[fresh.length - 1];
97
+ return {
98
+ act: 'speak',
99
+ reason: mode === 'maintenance' ? '主动维护 Bolloon 项目' : '主动推进群内工作',
100
+ text: proactive,
101
+ mentions: [target.from],
102
+ ...(target.id ? { replyTo: target.id } : {}),
103
+ };
104
+ }
105
+ // ① @我 ⇒ 回他那一条
79
106
  const atMe = fresh.filter((m) => mentionsMe(m, me));
80
107
  if (atMe.length) {
81
108
  const target = atMe[atMe.length - 1];
@@ -156,6 +183,7 @@ export async function runGroupAutopilotOnce(ports) {
156
183
  now: t,
157
184
  policy,
158
185
  ...(ports.makeReply ? { makeReply: (m) => ports.makeReply(m, g) } : {}),
186
+ ...(ports.proactiveText ? { proactiveText: ports.proactiveText } : {}),
159
187
  });
160
188
  // 不论说不说, 这几条都算"处理过" (否则下一轮还会重新判它)
161
189
  for (const m of msgs) {
@@ -0,0 +1,193 @@
1
+ /**
2
+ * intent-store.ts — Intent Store (2026-10-05, leo 四端 Intent Network 设计 P0)
3
+ *
4
+ * Intent = Goal 的**前身**: 一句话「我正在做 X」+ 标签/预算/期限, 未到可执行程度。
5
+ * 它与 goal-flywheel 的 Goal 不冲突 (Goal 是意图的可执行投影):
6
+ * · intent 是轻量声明 (world/intents/ 一意图一文件)
7
+ * · 升级成可执行意图时 → 转 Goal (goal-flywheel)
8
+ * 接口层不重叠, 本模块不碰 goal-store。
9
+ *
10
+ * 存储布局 (leo: 「intents 专门设计一个文件夹」「world 也是设计文件夹更好」):
11
+ * ~/.bolloon/world/ ← World 根 (六元组本体的实体根)
12
+ * ├── intents/ ← Intent 实体目录 (一意图一文件, 原子写)
13
+ * │ └── int_<id>.json
14
+ * ├── opportunities/ ← (P1) Opportunity 实体目录
15
+ * ├── actions/ ← (后续) Action 实体目录
16
+ * └── memory/ ← (后续) Memory 实体目录
17
+ * 每个实体独立文件 ⇒ 原子写 / 并发安全 / 可扩展, 与 task-board 的 announcement 文件模式同源。
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ import * as crypto from 'crypto';
23
+ const homeOf = (home) => home || process.env.HOME || os.homedir();
24
+ /** World 根目录 (~/.bolloon/world/) */
25
+ export const worldDir = (home) => path.join(homeOf(home), '.bolloon', 'world');
26
+ /** Intent 实体目录 (~/.bolloon/world/intents/) */
27
+ export const intentsDir = (home) => path.join(worldDir(home), 'intents');
28
+ /** Opportunity 实体目录 (~/.bolloon/world/opportunities/, P1) */
29
+ export const opportunitiesDir = (home) => path.join(worldDir(home), 'opportunities');
30
+ /** 只允许 int_ 开头的 id 当文件名 (防路径穿越); 非法 → null */
31
+ function safeId(id) {
32
+ const s = String(id || '').trim();
33
+ return /^int_[A-Za-z0-9]{6,32}$/.test(s) ? s : null;
34
+ }
35
+ /** 意图文件路径 (非法 id → null) */
36
+ export function intentFile(id, home) {
37
+ const safe = safeId(id);
38
+ return safe ? path.join(intentsDir(home), `${safe}.json`) : null;
39
+ }
40
+ async function ensure(dir) {
41
+ try {
42
+ await fs.mkdir(dir, { recursive: true });
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function writeJson(file, value) {
50
+ try {
51
+ await ensure(path.dirname(file));
52
+ await fs.writeFile(file, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
53
+ return { ok: true };
54
+ }
55
+ catch (e) {
56
+ return { ok: false, error: String(e?.message || e).slice(0, 200) };
57
+ }
58
+ }
59
+ async function readJson(file) {
60
+ try {
61
+ const raw = await fs.readFile(file, 'utf-8');
62
+ const parsed = JSON.parse(raw);
63
+ if (!parsed || typeof parsed !== 'object')
64
+ return { ok: false, error: `${path.basename(file)} 不是对象` };
65
+ return { ok: true, value: parsed };
66
+ }
67
+ catch (e) {
68
+ if (e?.code === 'ENOENT')
69
+ return { ok: false, error: 'ENOENT' };
70
+ return { ok: false, error: `读 ${path.basename(file)} 失败: ${String(e?.message || e).slice(0, 120)}` };
71
+ }
72
+ }
73
+ /** 标签: 从意图正文提取简单标签 (英文词 + 中文 2 字以上词元) + 手工标签去重 */
74
+ export function extractTags(text, manual = []) {
75
+ const src = String(text || '').toLowerCase();
76
+ const words = src.match(/[a-z][a-z0-9_-]{1,}/g) || [];
77
+ const cjk = src.match(/[\u4e00-\u9fff]{2,}/g) || [];
78
+ const all = [...words, ...cjk, ...manual.map((t) => t.toLowerCase().trim()).filter(Boolean)];
79
+ return Array.from(new Set(all)).slice(0, 24);
80
+ }
81
+ /** 扫描 intents 目录, 列出全部意图 (按创建时间倒序) */
82
+ export async function listIntents(home, status) {
83
+ const dir = intentsDir(home);
84
+ let entries;
85
+ try {
86
+ entries = await fs.readdir(dir);
87
+ }
88
+ catch (e) {
89
+ if (e?.code === 'ENOENT')
90
+ return { ok: true, intents: [] };
91
+ return { ok: false, intents: [], error: `读 ${dir} 失败: ${String(e?.message || e).slice(0, 120)}` };
92
+ }
93
+ const intents = [];
94
+ for (const name of entries.filter((n) => n.startsWith('int_') && n.endsWith('.json'))) {
95
+ const r = await readJson(path.join(dir, name));
96
+ if (!r.ok || !r.value || !r.value.id)
97
+ continue; // 坏文件跳过, 不崩
98
+ intents.push(r.value);
99
+ }
100
+ intents.sort((a, b) => b.createdAt - a.createdAt);
101
+ return { ok: true, intents: status ? intents.filter((i) => i.status === status) : intents };
102
+ }
103
+ /** 单条意图按 id 读 (不存在 → ok:true 无 intent) */
104
+ export async function getIntent(id, home) {
105
+ const file = intentFile(id, home);
106
+ if (!file)
107
+ return { ok: false, error: `非法意图 id: ${String(id).slice(0, 32)}` };
108
+ const r = await readJson(file);
109
+ if (!r.ok)
110
+ return r.error === 'ENOENT' ? { ok: true } : r;
111
+ return { ok: true, intent: r.value };
112
+ }
113
+ /** 声明意图 (幂等: 同 text 已存在 → 更新 tags/priority, 不重复建文件) */
114
+ export async function setIntent(input) {
115
+ const text = String(input.text || '').trim();
116
+ if (!text)
117
+ return { ok: false, created: false, error: '意图不能为空' };
118
+ const priority = input.priority ?? 3;
119
+ if (!Number.isInteger(priority) || priority < 1 || priority > 5)
120
+ return { ok: false, created: false, error: `priority 必须 1-5 整数, 实得 ${String(priority)}` };
121
+ const loaded = await listIntents(input.home);
122
+ if (!loaded.ok)
123
+ return { ok: false, created: false, error: loaded.error };
124
+ const now = Date.now();
125
+ const existing = loaded.intents.find((i) => i.text === text && i.status === 'active');
126
+ if (existing) {
127
+ const merged = {
128
+ ...existing,
129
+ tags: extractTags(text, [...(input.tags ?? []), ...existing.tags]),
130
+ priority,
131
+ budget: input.budget !== undefined ? input.budget : existing.budget,
132
+ deadline: input.deadline !== undefined ? input.deadline : existing.deadline,
133
+ updatedAt: now,
134
+ };
135
+ const file = intentFile(existing.id, input.home);
136
+ if (!file)
137
+ return { ok: false, created: false, error: '非法 id' };
138
+ const saved = await writeJson(file, merged);
139
+ if (!saved.ok)
140
+ return { ok: false, created: false, error: saved.error };
141
+ return { ok: true, intent: merged, created: false };
142
+ }
143
+ const record = {
144
+ id: `int_${crypto.randomBytes(6).toString('hex')}`,
145
+ text, tags: extractTags(text, input.tags ?? []), priority,
146
+ budget: input.budget ?? null, deadline: input.deadline ?? null,
147
+ status: 'active', createdAt: now, updatedAt: now, matchedCount: 0,
148
+ };
149
+ const file = intentFile(record.id, input.home);
150
+ if (!file)
151
+ return { ok: false, created: false, error: '非法 id' };
152
+ const saved = await writeJson(file, record);
153
+ if (!saved.ok)
154
+ return { ok: false, created: false, error: saved.error };
155
+ return { ok: true, intent: record, created: true };
156
+ }
157
+ /** 删意图 (或标记 done) */
158
+ export async function removeIntent(id, markDone = false, home) {
159
+ const file = intentFile(id, home);
160
+ if (!file)
161
+ return { ok: false, error: `非法意图 id: ${String(id).slice(0, 32)}` };
162
+ if (markDone) {
163
+ const r = await getIntent(id, home);
164
+ if (!r.ok)
165
+ return { ok: false, error: r.error };
166
+ if (!r.intent)
167
+ return { ok: false, error: `没有意图 ${id} (看 bolloon intent list)` };
168
+ const updated = { ...r.intent, status: 'done', updatedAt: Date.now() };
169
+ const saved = await writeJson(file, updated);
170
+ if (!saved.ok)
171
+ return { ok: false, error: saved.error };
172
+ return { ok: true, removed: id };
173
+ }
174
+ try {
175
+ await fs.unlink(file);
176
+ return { ok: true, removed: id };
177
+ }
178
+ catch (e) {
179
+ if (e?.code === 'ENOENT')
180
+ return { ok: false, error: `没有意图 ${id} (看 bolloon intent list)` };
181
+ return { ok: false, error: `删 ${file} 失败: ${String(e?.message || e).slice(0, 120)}` };
182
+ }
183
+ }
184
+ /** 意图匹配数 +1 (扫描后被上层调用, 诚实计数) */
185
+ export async function bumpMatchedCount(id, home) {
186
+ const r = await getIntent(id, home);
187
+ if (!r.ok || !r.intent)
188
+ return;
189
+ const updated = { ...r.intent, matchedCount: r.intent.matchedCount + 1, updatedAt: Date.now() };
190
+ const file = intentFile(id, home);
191
+ if (file)
192
+ await writeJson(file, updated);
193
+ }