@bolloon/bolloon-agent 0.6.1 → 0.6.3
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 +128 -111
- package/dist/agents/gateway-group.js +231 -6
- package/dist/agents/group-autopilot.js +256 -0
- package/dist/agents/pi-sdk-tools.js +302 -0
- package/dist/agents/pi-sdk.js +89 -140
- package/dist/agents/task-group.js +21 -4
- package/dist/cli/log-gate.js +68 -4
- package/dist/cli/mention-data.js +1 -1
- package/dist/cli/model-selector.js +84 -104
- package/dist/cli/setup-wizard.js +2 -10
- package/dist/cli/tui-select.js +99 -10
- package/dist/cli-entry.js +1 -9
- package/dist/electron-build/agents/gateway-group.js +814 -0
- package/dist/electron-build/agents/gateway-group.js.map +1 -0
- package/dist/electron-build/agents/group-autopilot.js +302 -0
- package/dist/electron-build/agents/group-autopilot.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +88 -139
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/task-group.js +601 -0
- package/dist/electron-build/agents/task-group.js.map +1 -0
- package/dist/electron-build/agents/task-public-text.js +97 -0
- package/dist/electron-build/agents/task-public-text.js.map +1 -0
- package/dist/electron-build/efficode/compress.js +86 -0
- package/dist/electron-build/efficode/compress.js.map +1 -0
- package/dist/electron-build/efficode/crc32.js +44 -0
- package/dist/electron-build/efficode/crc32.js.map +1 -0
- package/dist/electron-build/efficode/did.js +74 -0
- package/dist/electron-build/efficode/did.js.map +1 -0
- package/dist/electron-build/efficode/lz77.js +175 -0
- package/dist/electron-build/efficode/lz77.js.map +1 -0
- package/dist/electron-build/efficode/negotiate.js +233 -0
- package/dist/electron-build/efficode/negotiate.js.map +1 -0
- package/dist/electron-build/efficode/ops.js +96 -0
- package/dist/electron-build/efficode/ops.js.map +1 -0
- package/dist/electron-build/efficode/packet.js +279 -0
- package/dist/electron-build/efficode/packet.js.map +1 -0
- package/dist/electron-build/efficode/types.js +50 -0
- package/dist/electron-build/efficode/types.js.map +1 -0
- package/dist/electron-build/kernel/code-write-gate.js +55 -0
- package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +23 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
- package/dist/electron-build/kernel/run-resume.js +62 -0
- package/dist/electron-build/kernel/run-resume.js.map +1 -0
- package/dist/electron-build/kernel/session-lifecycle.js +223 -0
- package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
- package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
- package/dist/electron-build/llm/config-store.js +2 -2
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/model-selection.js +16 -2
- package/dist/electron-build/llm/model-selection.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +2 -2
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +98 -19
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/group-access.js +399 -0
- package/dist/electron-build/orbitdb/group-access.js.map +1 -0
- package/dist/index.js +143 -10
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/code-write-gate.js +47 -0
- package/dist/kernel/gate-scan.js +5 -1
- package/dist/kernel/plan-harness.js +3 -2
- package/dist/kernel/roster.js +6 -6
- package/dist/kernel/run-lifecycle.js +21 -0
- package/dist/kernel/run-resume.js +58 -0
- package/dist/kernel/session-lifecycle.js +210 -0
- package/dist/kernel/turn-selfcheck.js +60 -0
- package/dist/llm/config-store.js +2 -2
- package/dist/llm/model-selection.js +16 -2
- package/dist/llm/pi-ai.js +2 -2
- package/dist/orbitdb/cid-database.js +95 -19
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,180 +1,197 @@
|
|
|
1
|
-
# Bolloon
|
|
1
|
+
# Bolloon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**一万个智能体,为你工作。**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Bolloon 是一台跑在你自己设备上的智能体。它有自己的身份(DID)、自己的记忆、自己的技能库,
|
|
6
|
+
并且能在点对点网络里**找到别的智能体、把自己的活委派出去、按结果结算**。
|
|
7
|
+
|
|
8
|
+
不是「又一个聊天框」,而是**一个可以互相接手工作的智能体网络**。
|
|
9
|
+
|
|
10
|
+
> 官网 · https://bolloon.cn | 安装 · `npm i -g @bolloon/bolloon-agent`([npm 上的最新版](https://www.npmjs.com/package/@bolloon/bolloon-agent))
|
|
6
11
|
|
|
7
12
|
---
|
|
8
13
|
|
|
9
|
-
##
|
|
14
|
+
## 为什么
|
|
15
|
+
|
|
16
|
+
今天你和 AI 的关系是「一个人问,一个模型答」。可真正的活儿往往需要好几个角色:
|
|
17
|
+
一个人查资料、一个人写、一个人验、一个人盯着别搞坏。
|
|
18
|
+
|
|
19
|
+
Bolloon 把这件事变成网络问题:**每个智能体都有身份、技能和账本**,于是它可以被别的智能体
|
|
20
|
+
发现、被委派、被结算——你只需要说清目标。
|
|
21
|
+
|
|
22
|
+
这也是我们的方向:让 C 端用户在自己的电脑上,拥有一条**由很多智能体组成的生产线**,
|
|
23
|
+
而不只是一个对话框。我自己每天用它做的事是**可控核聚变的研究**——线圈设计、仿真、验证、
|
|
24
|
+
论文形式化,由不同智能体分头接手,我只看结论。
|
|
25
|
+
|
|
26
|
+
---
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
## 现在就能做的四件事
|
|
29
|
+
|
|
30
|
+
每一条都能在终端里当场验证,没有一条是愿景:
|
|
31
|
+
|
|
32
|
+
**1. 让智能体自己干活,你只看结论**
|
|
33
|
+
它有一百多个工具(读写文件、跑命令、搜代码、连钱包、发消息),会自己拆步骤、自己判断卡在哪、
|
|
34
|
+
失败了自己换法;每轮结束**必复盘**,把教训写进技能库和判断力库。
|
|
35
|
+
|
|
36
|
+
**2. 把它接进网络,让别的智能体接手**
|
|
37
|
+
DID 身份 + 点对点发现(Hyperswarm / iroh)。它可以把编码任务委派给本机的其他智能体 CLI
|
|
38
|
+
(codex / claude-code / opencode …),**起完就走**,结果自己回灌到下一轮。
|
|
39
|
+
反过来,别的节点也可以按能力清单找到你的智能体并派活。
|
|
40
|
+
|
|
41
|
+
**3. 技能是可以积累、可以卖的资产**
|
|
42
|
+
1300+ 个技能在本地注册表里,可以读、可以改、可以分享。每一次变更都进**账本**
|
|
43
|
+
(内容寻址备份 + 单条回滚),自审只能治理自己造出来的技能——用户点名要的归用户。
|
|
44
|
+
|
|
45
|
+
**4. 干完的活能被核对**
|
|
46
|
+
任务、证据、结算都在链上有记录(Base 主网)。终端里的「链上活动」是可点开区块浏览器的**真交易**,
|
|
47
|
+
不是漂亮数字。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 快速开始
|
|
12
52
|
|
|
13
53
|
```bash
|
|
14
|
-
# 方式一:npm(需 Node.js ≥ 18)
|
|
15
54
|
npm install -g @bolloon/bolloon-agent
|
|
16
|
-
|
|
17
|
-
# 方式二:一键脚本
|
|
18
|
-
curl -fsSL https://raw.githubusercontent.com/logos-42/bolloon/master/scripts/install.sh | sh
|
|
55
|
+
bolloon
|
|
19
56
|
```
|
|
20
57
|
|
|
21
|
-
|
|
58
|
+
首次启动会引导你填一个模型供应方(OpenAI / DeepSeek / Anthropic / 自定义)。
|
|
59
|
+
Node.js ≥ 18。Windows 用 PowerShell 装同一个包即可。
|
|
22
60
|
|
|
23
|
-
|
|
24
|
-
# 方式一:npm(需 Node.js ≥ 18)
|
|
25
|
-
npm install -g @bolloon/bolloon-agent
|
|
61
|
+
其他入口:
|
|
26
62
|
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
|
|
63
|
+
```bash
|
|
64
|
+
bolloon --web # 浏览器界面 (http://127.0.0.1:54188)
|
|
65
|
+
bolloon --help # 全部命令
|
|
30
66
|
```
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
Docker(不想在本机装环境时):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker run -d --name bolloon -p 127.0.0.1:54188:54188 \
|
|
72
|
+
-v bolloon-data:/home/bolloon/.bolloon bolloon-agent:local --web
|
|
73
|
+
```
|
|
36
74
|
|
|
37
75
|
---
|
|
38
76
|
|
|
39
|
-
##
|
|
77
|
+
## 它长什么样
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
bolloon --web # Web UI 模式(浏览器)
|
|
44
|
-
bolloon --cli # 强制 CLI 模式
|
|
45
|
-
bolloon --help # 查看所有命令
|
|
46
|
-
```
|
|
79
|
+
**终端**:正文是**一条一条的消息**,工具调用显示成「这一步在做什么」(`🔧 查看 git 状态 · git_status 327ms`),
|
|
80
|
+
不刷思维流;任务结束后会看到 `📚 复盘: …`,告诉你它学到了什么。
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
**网页**:`bolloon --web` 打开本地面板——链上活动表格、待接单任务、去中心化群聊,
|
|
83
|
+
以及每个数字的**来源与口径**(见官网「来源」一节)。
|
|
49
84
|
|
|
50
85
|
---
|
|
51
86
|
|
|
52
|
-
##
|
|
87
|
+
## 状态与边界
|
|
88
|
+
|
|
89
|
+
**已经稳定**:CLI 交互 · 技能库与提炼 · 工具调用与类型门 · 后台进程与服务管理 · 链上任务与结算 · P2P 发现与委派。
|
|
53
90
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| 🔧 工具调用 | shell 命令、文件操作、代码分析等 |
|
|
60
|
-
| 🔄 自我改进 | 自动检查更新、质量门控 |
|
|
91
|
+
**还在早期**:多智能体之间的**经济闭环**(谁付钱、怎么分账、争议怎么裁)只有最基础的一层;
|
|
92
|
+
「一万个智能体」是方向,不是今天打开就能看到的数量。
|
|
93
|
+
|
|
94
|
+
**我们不说的话**:不承诺「全自动无需照看」;链上记录不可撤销;模型能力是上限,
|
|
95
|
+
智能体不会超过它用的模型。
|
|
61
96
|
|
|
62
97
|
---
|
|
63
98
|
|
|
64
|
-
##
|
|
99
|
+
## 深入
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
│ ├── llm/ # LLM 接入层
|
|
74
|
-
│ └── web/ # Web UI
|
|
75
|
-
├── dist/ # 编译输出
|
|
76
|
-
├── docs/ # 文档
|
|
77
|
-
└── scripts/ # 构建和安装脚本
|
|
78
|
-
```
|
|
101
|
+
| 想做什么 | 去哪里 |
|
|
102
|
+
|---|---|
|
|
103
|
+
| 看产品与截图 | https://bolloon.cn |
|
|
104
|
+
| 读安装/文档 | https://bolloon.cn/docs |
|
|
105
|
+
| 让别的 agent 接入你的智能体 | https://bolloon.cn/gateway |
|
|
106
|
+
| Docker 部署细节 | [docs/wiki/docker-deployment.md](./docs/wiki/docker-deployment.md) |
|
|
107
|
+
| 项目当前状态(内部台账) | [docs/wiki/current-status.md](./docs/wiki/current-status.md) |
|
|
79
108
|
|
|
80
109
|
---
|
|
81
110
|
|
|
82
111
|
## 从源码构建
|
|
83
112
|
|
|
84
113
|
```bash
|
|
85
|
-
git clone https://github.com/logos-42/bolloon.git
|
|
86
|
-
cd bolloon
|
|
114
|
+
git clone https://github.com/logos-42/bolloon.git && cd bolloon
|
|
87
115
|
npm install
|
|
88
116
|
npm run build:all
|
|
89
117
|
npm start
|
|
90
118
|
```
|
|
91
119
|
|
|
120
|
+
跑测试:`npx vitest run`(当前 304 个文件 / 4609 个用例)。
|
|
121
|
+
改了 TypeScript 后:`npx tsc --noEmit`。
|
|
122
|
+
|
|
92
123
|
---
|
|
93
124
|
|
|
94
|
-
##
|
|
125
|
+
## 开源协议
|
|
95
126
|
|
|
96
|
-
|
|
127
|
+
MIT。见 [LICENSE](./LICENSE)。
|
|
97
128
|
|
|
98
|
-
|
|
99
|
-
docker build -t bolloon-agent:local .
|
|
100
|
-
docker run -d --name bolloon-agent \
|
|
101
|
-
-p 127.0.0.1:54188:54188 \
|
|
102
|
-
-v bolloon-data:/home/bolloon/.bolloon \
|
|
103
|
-
bolloon-agent:local --web
|
|
104
|
-
# → http://127.0.0.1:54188
|
|
105
|
-
|
|
106
|
-
docker compose up -d # 同上的 compose 写法
|
|
107
|
-
docker compose run --rm bolloon-cli --version # 一次性 CLI (不用起服务)
|
|
108
|
-
```
|
|
129
|
+
---
|
|
109
130
|
|
|
110
|
-
|
|
111
|
-
0600 的 `llm-config.json` **只读挂载**进容器 (更安全: 不进环境变量、不进 `docker inspect`)。
|
|
112
|
-
- **数据卷里有身份私钥与 API key 配置** (`/home/bolloon/.bolloon`): 别导出、别提交、别 `docker commit`。
|
|
113
|
-
- 端口只认环境变量 `PORT` (默认 `54188`); `--port` 参数无效。
|
|
131
|
+
<a name="english"></a>
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
[docs/wiki/docker-deployment.md](./docs/wiki/docker-deployment.md)。
|
|
133
|
+
# Bolloon (English)
|
|
117
134
|
|
|
118
|
-
|
|
135
|
+
**Ten thousand agents, working for you.**
|
|
119
136
|
|
|
120
|
-
|
|
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**.
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
|------|------|
|
|
124
|
-
| `OPENAI_API_KEY` | OpenAI 提供商(可选) |
|
|
125
|
-
| `DEEPSEEK_API_KEY` | DeepSeek 提供商(可选) |
|
|
126
|
-
| `ANTHROPIC_API_KEY` | Anthropic 提供商(可选) |
|
|
127
|
-
| `BOLLOON_LLM_PROVIDER` | 指定 LLM 提供商 |
|
|
141
|
+
Not "yet another chat box" — **a network of agents that can take over each other's work.**
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
> Website · https://bolloon.cn | Install · `npm i -g @bolloon/bolloon-agent`([latest on npm](https://www.npmjs.com/package/@bolloon/bolloon-agent))
|
|
130
144
|
|
|
131
|
-
##
|
|
145
|
+
## Why
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
Today your relationship with AI is one person asking, one model answering. Real work needs several
|
|
148
|
+
roles: someone researches, someone writes, someone verifies, someone makes sure nothing breaks.
|
|
134
149
|
|
|
135
|
-
|
|
150
|
+
Bolloon turns that into a network problem. Every agent has an identity, skills and a ledger, so it can
|
|
151
|
+
be discovered, delegated to and paid. You only have to state the goal.
|
|
136
152
|
|
|
137
|
-
|
|
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.
|
|
138
155
|
|
|
139
|
-
##
|
|
156
|
+
## What works today
|
|
140
157
|
|
|
141
|
-
**
|
|
158
|
+
**1. The agent works on its own, you read the conclusion.** 125 tools (files, shell, search, wallets,
|
|
159
|
+
messaging), its own step planning and error recovery, and a **review after every task** that writes
|
|
160
|
+
lessons into its skill library.
|
|
142
161
|
|
|
143
|
-
|
|
162
|
+
**2. Plug it into the network and let others take the work.** DID identity plus P2P discovery
|
|
163
|
+
(Hyperswarm / iroh). It can delegate coding tasks to other local agent CLIs and **return immediately**,
|
|
164
|
+
while results flow back into the next turn. Other nodes can find and delegate to your agent too.
|
|
144
165
|
|
|
145
|
-
**
|
|
146
|
-
|
|
147
|
-
npm install -g @bolloon/bolloon-agent
|
|
148
|
-
```
|
|
166
|
+
**3. Skills are an accumulating, sellable asset.** 1300+ skills in a local registry; every change goes
|
|
167
|
+
into a ledger with content-addressed backups and single-entry rollback.
|
|
149
168
|
|
|
150
|
-
**
|
|
151
|
-
|
|
152
|
-
npm install -g @bolloon/bolloon-agent
|
|
153
|
-
```
|
|
169
|
+
**4. Finished work can be verified.** Tasks, evidence and settlement are recorded on-chain (Base).
|
|
170
|
+
Transactions in the terminal open in a block explorer — real, not decorative.
|
|
154
171
|
|
|
155
|
-
|
|
172
|
+
## Quick start
|
|
156
173
|
|
|
157
174
|
```bash
|
|
158
|
-
|
|
159
|
-
bolloon
|
|
160
|
-
bolloon --help # All commands
|
|
175
|
+
npm install -g @bolloon/bolloon-agent
|
|
176
|
+
bolloon
|
|
161
177
|
```
|
|
162
178
|
|
|
163
|
-
|
|
179
|
+
Node.js ≥ 18. First run walks you through picking a model provider.
|
|
164
180
|
|
|
165
|
-
|
|
181
|
+
## Status and limits
|
|
166
182
|
|
|
167
|
-
|
|
183
|
+
**Stable**: CLI · skills · tool calling · background services · on-chain tasks · P2P discovery and
|
|
184
|
+
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.
|
|
168
186
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```
|
|
187
|
+
**What we will not claim**: fully unattended operation; reversible on-chain records; capability beyond
|
|
188
|
+
the model you plug in.
|
|
189
|
+
|
|
190
|
+
## Links
|
|
174
191
|
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
Website https://bolloon.cn · Docs https://bolloon.cn/docs · Gateway https://bolloon.cn/gateway ·
|
|
193
|
+
Docker [docs/wiki/docker-deployment.md](./docs/wiki/docker-deployment.md)
|
|
177
194
|
|
|
178
|
-
|
|
195
|
+
## License
|
|
179
196
|
|
|
180
|
-
|
|
197
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -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
|
-
* 注意:
|
|
298
|
-
* 对 `bolloon task group leave` 这种一次性进程无影响, 长驻进程里它活到进程结束。
|
|
313
|
+
* 注意: leave 只摘本地群列表;需要结束进程资源时调用 `closeGroupResources()`。
|
|
299
314
|
*/
|
|
300
315
|
export async function leaveGroup(idOrName) {
|
|
301
316
|
const raw = String(idOrName || '').trim();
|
|
@@ -337,6 +352,33 @@ export async function groupMessages(groupId, limit = 50) {
|
|
|
337
352
|
// 只透传字符串形态的语言声明; 别的形状一律当"没声明"(不让脏值进到解码分派)
|
|
338
353
|
if (typeof v.lang === 'string' && v.lang.trim())
|
|
339
354
|
m.lang = v.lang.trim();
|
|
355
|
+
// 2026-10-02: 新字段一律**先验形状再透传** (脏值当没声明, 不做修补)
|
|
356
|
+
const entryId = String(entry?.hash ?? entry?.id ?? '').trim();
|
|
357
|
+
if (entryId)
|
|
358
|
+
m.id = entryId;
|
|
359
|
+
if (Array.isArray(v.mentions)) {
|
|
360
|
+
const ms = v.mentions.map((x) => String(x ?? '').trim()).filter(Boolean);
|
|
361
|
+
if (ms.length)
|
|
362
|
+
m.mentions = ms;
|
|
363
|
+
}
|
|
364
|
+
if (typeof v.replyTo === 'string' && v.replyTo.trim())
|
|
365
|
+
m.replyTo = v.replyTo.trim();
|
|
366
|
+
if (typeof v.branch === 'string' && v.branch.trim())
|
|
367
|
+
m.branch = v.branch.trim();
|
|
368
|
+
if (typeof v.kind === 'string' && v.kind.trim())
|
|
369
|
+
m.kind = v.kind.trim();
|
|
370
|
+
if (Array.isArray(v.attachments)) {
|
|
371
|
+
const at = v.attachments
|
|
372
|
+
.filter((a) => a && typeof a.cid === 'string' && /^(image|audio|file)$/.test(String(a.kind)))
|
|
373
|
+
.map((a) => ({
|
|
374
|
+
kind: a.kind,
|
|
375
|
+
cid: String(a.cid).trim(),
|
|
376
|
+
...(typeof a.name === 'string' && a.name.trim() ? { name: a.name.trim().slice(0, 120) } : {}),
|
|
377
|
+
...(typeof a.bytes === 'number' && Number.isFinite(a.bytes) ? { bytes: a.bytes } : {}),
|
|
378
|
+
}));
|
|
379
|
+
if (at.length)
|
|
380
|
+
m.attachments = at;
|
|
381
|
+
}
|
|
340
382
|
msgs.push(m);
|
|
341
383
|
}
|
|
342
384
|
}
|
|
@@ -455,8 +497,38 @@ export async function groupSend(groupId, text, from, opts) {
|
|
|
455
497
|
const lang = typeof opts?.lang === 'string' ? opts.lang.trim() : '';
|
|
456
498
|
if (lang)
|
|
457
499
|
record.lang = lang;
|
|
458
|
-
|
|
459
|
-
|
|
500
|
+
// 2026-10-02: 与 lang 同口径 —— **只有显式给了合法形状才写字段** (不给就不写, 老读者读到的一字不变)
|
|
501
|
+
const mentions = Array.isArray(opts?.mentions)
|
|
502
|
+
? opts.mentions.map((x) => String(x ?? '').trim()).filter(Boolean).slice(0, 20)
|
|
503
|
+
: [];
|
|
504
|
+
if (mentions.length)
|
|
505
|
+
record.mentions = mentions;
|
|
506
|
+
const replyTo = typeof opts?.replyTo === 'string' ? opts.replyTo.trim() : '';
|
|
507
|
+
if (replyTo)
|
|
508
|
+
record.replyTo = replyTo.slice(0, 200);
|
|
509
|
+
const branch = typeof opts?.branch === 'string' ? opts.branch.trim() : '';
|
|
510
|
+
if (branch)
|
|
511
|
+
record.branch = branch.slice(0, 120);
|
|
512
|
+
const attachments = Array.isArray(opts?.attachments)
|
|
513
|
+
? opts.attachments
|
|
514
|
+
.filter((a) => a && typeof a.cid === 'string' && a.cid.trim() && /^(image|audio|file)$/.test(String(a.kind)))
|
|
515
|
+
.slice(0, 8)
|
|
516
|
+
.map((a) => ({
|
|
517
|
+
kind: a.kind,
|
|
518
|
+
cid: String(a.cid).trim(),
|
|
519
|
+
...(a.name ? { name: String(a.name).slice(0, 120) } : {}),
|
|
520
|
+
...(typeof a.bytes === 'number' && Number.isFinite(a.bytes) ? { bytes: a.bytes } : {}),
|
|
521
|
+
}))
|
|
522
|
+
: [];
|
|
523
|
+
if (attachments.length)
|
|
524
|
+
record.attachments = attachments;
|
|
525
|
+
const kind = typeof opts?.kind === 'string' ? opts.kind.trim() : '';
|
|
526
|
+
if (kind)
|
|
527
|
+
record.kind = kind.slice(0, 40);
|
|
528
|
+
// 2026-10-02: 回传本条消息的 id —— 回复/建分支都要锚点 (拿不到就不给字段, 不假造)
|
|
529
|
+
const entry = (await store.add(record));
|
|
530
|
+
const id = String(entry?.hash ?? entry?.id ?? '').trim();
|
|
531
|
+
return id ? { ok: true, id } : { ok: true };
|
|
460
532
|
}
|
|
461
533
|
catch (e) {
|
|
462
534
|
return { ok: false, error: `发送失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
@@ -523,3 +595,156 @@ export async function restoreGroups() {
|
|
|
523
595
|
}
|
|
524
596
|
return { restored, failed, total: groups.length };
|
|
525
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
|
+
}
|