@bolloon/bolloon-agent 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.
@@ -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
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * opportunity-inbox.ts — 机会信箱 (2026-10-05, leo: 别人主动投来的机会)
3
+ *
4
+ * 设计: 外部 agent 用 DIAP Ed25519 身份签名投递机会声明到本机信箱,
5
+ * 验签通过才入库 (~/.bolloon/world/inbox/<id>.json) 并进入世界流。
6
+ * 来源可验: 每条机会带 provider DID + 验签状态, 不再是「本机自产」的虚拟感。
7
+ *
8
+ * 协议: bolloon-opportunity-inbox/1
9
+ * POST payload = { title, summary, refs[], provider: {did}, issuedAt }
10
+ * signature = ed25519Sign(privateKey, canonicalize(payload))
11
+ * 验证 = ed25519Verify(providerPublicKey, canonicalize(payload), signature)
12
+ *
13
+ * 未来: 群复制 (OrbitDB 公共群 announce) 是信箱的另一条投递通道, 需要第二节点 (诚实边界)。
14
+ */
15
+ import * as fs from 'fs/promises';
16
+ import * as path from 'path';
17
+ import * as crypto from 'node:crypto';
18
+ import { worldDir } from './intent-store.js';
19
+ import { canonicalize, ed25519Verify } from './x402/paid-info-protocol.js';
20
+ export const INBOX_PROTOCOL = 'bolloon-opportunity-inbox/1';
21
+ const inboxDir = () => path.join(worldDir(), 'inbox');
22
+ function safeId(s) {
23
+ return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
24
+ }
25
+ /** 把投递载荷按签名标准序列化 (必须与签名时完全一致) */
26
+ export function canonicalPayload(p) {
27
+ return canonicalize({
28
+ protocol: p.protocol,
29
+ title: p.title,
30
+ summary: p.summary,
31
+ refs: p.refs,
32
+ provider: p.provider,
33
+ issuedAt: p.issuedAt,
34
+ });
35
+ }
36
+ /** 投递方 DID 的 Ed25519 公钥解析 (did:key 格式 → 32 字节 hex) */
37
+ export async function resolveDidPublicKey(did) {
38
+ try {
39
+ // did:key:z6Mk... — multibase base58btc 编码: [multicodec 0xed01(2B) + 32B 公钥] = 34 字节
40
+ const m = String(did || '').match(/^did:key:z([1-9A-HJ-NP-Za-km-z]+)$/);
41
+ if (!m)
42
+ return null;
43
+ const decoded = Buffer.from(importB58(m[1]));
44
+ if (decoded.length === 34 && decoded[0] === 0xed && decoded[1] === 0x01) {
45
+ return decoded.subarray(2).toString('hex'); // 剥 multicodec, 取 32 字节公钥
46
+ }
47
+ if (decoded.length === 32)
48
+ return decoded.toString('hex'); // 裸 32 字节兜底
49
+ return null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /** base58btc 解码 (multibase 'z' 前缀 = base58btc) — 只依赖 Node crypto 的 b58 简易实现 */
56
+ function importB58(s) {
57
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
58
+ const base = BigInt(58);
59
+ let num = BigInt(0);
60
+ for (const ch of s) {
61
+ const idx = ALPHABET.indexOf(ch);
62
+ if (idx < 0)
63
+ throw new Error('bad base58 char');
64
+ num = num * base + BigInt(idx);
65
+ }
66
+ const bytes = [];
67
+ while (num > 0n) {
68
+ bytes.unshift(Number(num & 0xffn));
69
+ num >>= 8n;
70
+ }
71
+ // 前导 '1' → 前导 0x00
72
+ let leading = 0;
73
+ for (const ch of s) {
74
+ if (ch === '1')
75
+ leading++;
76
+ else
77
+ break;
78
+ }
79
+ return Uint8Array.from([...new Array(leading).fill(0), ...bytes]);
80
+ }
81
+ /** 投递机会到信箱 (验签通过才入库) */
82
+ export async function deliverOpportunity(payload, signatureB64) {
83
+ if (payload.protocol !== INBOX_PROTOCOL)
84
+ return { ok: false, error: `协议不匹配: ${payload.protocol}` };
85
+ const title = String(payload.title || '').trim();
86
+ if (!title)
87
+ return { ok: false, error: '缺 title' };
88
+ const did = String(payload.provider?.did || '').trim();
89
+ if (!did)
90
+ return { ok: false, error: '缺 provider.did' };
91
+ // 1. 验签 (能解析公钥才可能 verified)
92
+ const pub = await resolveDidPublicKey(did);
93
+ let verification;
94
+ if (!pub) {
95
+ verification = 'unverified'; // 公钥解析不了 → 不入库 (不可信来源不污染世界流)
96
+ return { ok: false, error: '无法解析 provider DID 公钥 (拒绝不可信来源)' };
97
+ }
98
+ const okSig = await ed25519Verify(pub, canonicalPayload(payload), signatureB64);
99
+ if (!okSig) {
100
+ verification = 'signature-invalid';
101
+ return { ok: false, error: '签名无效 (拒绝伪造投递)' };
102
+ }
103
+ verification = 'verified';
104
+ // 2. 入库 (一机会一文件)
105
+ const id = `inbox_${safeId(crypto.createHash('sha256').update(canonicalPayload(payload)).digest('hex').slice(0, 16))}`;
106
+ const opp = {
107
+ protocol: payload.protocol,
108
+ id,
109
+ title,
110
+ summary: String(payload.summary || '').trim(),
111
+ refs: Array.isArray(payload.refs) ? payload.refs.map(String).filter(Boolean) : [],
112
+ provider: { did, name: payload.provider?.name },
113
+ issuedAt: payload.issuedAt,
114
+ verification,
115
+ receivedAt: Date.now(),
116
+ };
117
+ try {
118
+ await fs.mkdir(inboxDir(), { recursive: true });
119
+ await fs.writeFile(path.join(inboxDir(), `${id}.json`), JSON.stringify(opp, null, 2) + '\n', { mode: 0o600 });
120
+ }
121
+ catch (err) {
122
+ return { ok: false, error: `入库失败: ${err instanceof Error ? err.message : err}` };
123
+ }
124
+ return { ok: true, opportunity: opp };
125
+ }
126
+ /** 列出信箱机会 (verified 优先, 最近在前) */
127
+ export async function listInboxOpportunities() {
128
+ try {
129
+ const files = await fs.readdir(inboxDir());
130
+ const out = [];
131
+ for (const f of files) {
132
+ if (!f.endsWith('.json'))
133
+ continue;
134
+ try {
135
+ const raw = await fs.readFile(path.join(inboxDir(), f), 'utf-8');
136
+ const p = JSON.parse(raw);
137
+ if (p && p.id)
138
+ out.push(p);
139
+ }
140
+ catch { /* 坏文件跳过 */ }
141
+ }
142
+ return out.sort((a, b) => b.receivedAt - a.receivedAt);
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ }