libp2p-mesh 2026.6.2 → 2026.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.
- package/README.md +81 -19
- package/api.ts +2 -0
- package/dist/api.d.ts +1 -1
- package/dist/index.js +31 -1
- package/dist/src/agent-tools.d.ts +6 -2
- package/dist/src/agent-tools.js +29 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.js +196 -0
- package/dist/src/config-io.d.ts +9 -0
- package/dist/src/config-io.js +173 -0
- package/dist/src/inbound-delivery.d.ts +5 -3
- package/dist/src/inbound-delivery.js +35 -77
- package/dist/src/inbound.d.ts +1 -0
- package/dist/src/inbound.js +10 -3
- package/dist/src/instance-router.js +313 -37
- package/dist/src/plugin.js +34 -4
- package/dist/src/types.d.ts +18 -1
- package/dist/src/wizard.d.ts +22 -0
- package/dist/src/wizard.js +276 -0
- package/index.ts +31 -1
- package/openclaw.plugin.json +71 -1
- package/package.json +6 -5
- package/src/agent-tools.ts +36 -1
- package/src/cli.ts +226 -0
- package/src/config-io.ts +204 -0
- package/src/inbound-delivery.ts +47 -91
- package/src/inbound.ts +17 -5
- package/src/instance-router.ts +370 -39
- package/src/plugin.ts +40 -5
- package/src/types.ts +20 -1
- package/src/wizard.ts +332 -0
package/README.md
CHANGED
|
@@ -42,29 +42,49 @@ openclaw plugins registry --refresh
|
|
|
42
42
|
|
|
43
43
|
The published npm package includes compiled JavaScript under `dist/`, so OpenClaw and acpx can load it directly.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
## Configuration
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"discovery": "mdns"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"channels": {
|
|
58
|
-
"libp2p-mesh": {
|
|
59
|
-
"enabled": true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
47
|
+
### Quick Setup (Recommended)
|
|
48
|
+
|
|
49
|
+
After installation, run the interactive setup wizard:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
openclaw libp2p-mesh setup
|
|
63
53
|
```
|
|
64
54
|
|
|
65
|
-
|
|
55
|
+
The wizard will guide you through:
|
|
56
|
+
- Discovery mode (mDNS / Bootstrap / DHT)
|
|
57
|
+
- Bootstrap peer addresses (for cross-network scenarios)
|
|
58
|
+
- Inbound channel targets (where to display received P2P messages)
|
|
59
|
+
- Optional: NAT traversal, circuit relay, fixed ports, and custom instance name
|
|
60
|
+
|
|
61
|
+
The configuration is written to `~/.openclaw/openclaw.json` automatically.
|
|
62
|
+
|
|
63
|
+
### Incremental Config Management
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# View all current non-default settings
|
|
67
|
+
openclaw libp2p-mesh config list
|
|
68
|
+
|
|
69
|
+
# Read a single value
|
|
70
|
+
openclaw libp2p-mesh config get discovery
|
|
71
|
+
|
|
72
|
+
# Set a value
|
|
73
|
+
openclaw libp2p-mesh config set discovery bootstrap
|
|
74
|
+
|
|
75
|
+
# Add to an array
|
|
76
|
+
openclaw libp2p-mesh config set bootstrapList --add /ip4/10.0.0.5/tcp/4001/p2p/12D3KooW...
|
|
77
|
+
|
|
78
|
+
# Remove from an array
|
|
79
|
+
openclaw libp2p-mesh config set bootstrapList --remove /ip4/203.0.113.10/tcp/4001/p2p/12D3KooW...
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
# Reset a key to default
|
|
82
|
+
openclaw libp2p-mesh config unset relayList
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Manual Configuration (Advanced)
|
|
86
|
+
|
|
87
|
+
You can still directly edit `~/.openclaw/openclaw.json`:
|
|
68
88
|
|
|
69
89
|
### Minimal LAN Setup (Default)
|
|
70
90
|
|
|
@@ -158,6 +178,7 @@ If peers are on different networks, use a bootstrap node:
|
|
|
158
178
|
| `announceAddrs` | `string[]` | `[]` | Extra multiaddrs to announce on top of auto-detected ones |
|
|
159
179
|
| `inboundChannel` | `string` | `undefined` | OpenClaw channel used to display inbound P2P user messages, for example `"feishu"` |
|
|
160
180
|
| `inboundTarget` | `string` | `undefined` | OpenClaw channel target for inbound P2P messages, for example `user:ou_xxx` or `chat:oc_xxx` |
|
|
181
|
+
| `inboundTargets` | `{ id?: string, channel: string, target: string }[]` | `undefined` | Receiver-owned list of channel targets for inbound P2P user messages. When present, overrides `inboundChannel`/`inboundTarget`; an empty array disables inbound delivery |
|
|
161
182
|
| `deliveryAckTimeoutMs` | `number` | `15000` | Timeout for waiting on remote channel delivery ACKs |
|
|
162
183
|
|
|
163
184
|
## NAT Traversal
|
|
@@ -316,6 +337,47 @@ p2p_send_instance_message({ "instanceId": "<target-instance-id>", "message": "
|
|
|
316
337
|
|
|
317
338
|
The sender reports success only after the remote OpenClaw instance forwards the message to its configured inbound channel and returns a delivery ACK.
|
|
318
339
|
|
|
340
|
+
### Multi-channel inbound delivery
|
|
341
|
+
|
|
342
|
+
To display the same inbound P2P message in multiple local OpenClaw channel targets, configure `inboundTargets` on the receiving instance:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"plugins": {
|
|
347
|
+
"libp2p-mesh": {
|
|
348
|
+
"enabled": true,
|
|
349
|
+
"config": {
|
|
350
|
+
"discovery": "mdns",
|
|
351
|
+
"inboundTargets": [
|
|
352
|
+
{
|
|
353
|
+
"id": "feishu-user",
|
|
354
|
+
"channel": "feishu",
|
|
355
|
+
"target": "user:ou_xxx"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"id": "telegram-chat",
|
|
359
|
+
"channel": "telegram",
|
|
360
|
+
"target": "chat:123456"
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
"deliveryAckTimeoutMs": 15000
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"channels": {
|
|
368
|
+
"libp2p-mesh": { "enabled": true }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The sender still calls only:
|
|
374
|
+
|
|
375
|
+
```text
|
|
376
|
+
p2p_send_instance_message({ "instanceId": "<target-instance-id>", "message": "今晚出来吃饭" })
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The sender does not choose the receiver channel. The receiving instance owns that routing decision through `inboundTargets`, and the message appears in each configured `channel`/`target`. Each target can include an `id`, which is shown in the sender-side tool result alongside each target's success or failure status.
|
|
380
|
+
|
|
319
381
|
Tools are not configured in `openclaw.json`; they are registered automatically by the plugin through `api.registerTool()`.
|
|
320
382
|
|
|
321
383
|
## Troubleshooting
|
package/api.ts
CHANGED
|
@@ -3,9 +3,11 @@ export { createInstancePeerStore } from "./src/instance-peer-store.js";
|
|
|
3
3
|
export { createInstanceRouter } from "./src/instance-router.js";
|
|
4
4
|
export type {
|
|
5
5
|
DeliveryAckPayload,
|
|
6
|
+
DeliveryTargetResult,
|
|
6
7
|
InboundDeliveryAdapter,
|
|
7
8
|
InboundDeliveryRequest,
|
|
8
9
|
InboundDeliveryResult,
|
|
10
|
+
InboundTargetConfig,
|
|
9
11
|
InstanceAnnouncePayload,
|
|
10
12
|
InstanceIdentity,
|
|
11
13
|
InstancePeerRecord,
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createMeshNetwork } from "./src/mesh.js";
|
|
2
2
|
export { createInstancePeerStore } from "./src/instance-peer-store.js";
|
|
3
3
|
export { createInstanceRouter } from "./src/instance-router.js";
|
|
4
|
-
export type { DeliveryAckPayload, InboundDeliveryAdapter, InboundDeliveryRequest, InboundDeliveryResult, InstanceAnnouncePayload, InstanceIdentity, InstancePeerRecord, InstancePeerStore, InstancePeerTable, InstanceRouter, MeshConfig, MeshNetwork, P2PMessage, P2PMessageType, UserMessagePayload, } from "./src/types.js";
|
|
4
|
+
export type { DeliveryAckPayload, DeliveryTargetResult, InboundDeliveryAdapter, InboundDeliveryRequest, InboundDeliveryResult, InboundTargetConfig, InstanceAnnouncePayload, InstanceIdentity, InstancePeerRecord, InstancePeerStore, InstancePeerTable, InstanceRouter, MeshConfig, MeshNetwork, P2PMessage, P2PMessageType, UserMessagePayload, } from "./src/types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { registerLibp2pMesh } from "./src/plugin.js";
|
|
3
|
+
import { registerLibp2pMeshCli } from "./src/cli.js";
|
|
3
4
|
function createLibp2pMeshConfigSchema() {
|
|
4
5
|
return {
|
|
5
6
|
safeParse(value) {
|
|
@@ -127,6 +128,29 @@ function createLibp2pMeshConfigSchema() {
|
|
|
127
128
|
type: "string",
|
|
128
129
|
description: "OpenClaw channel target for inbound P2P user messages, for example user:ou_xxx or chat:oc_xxx.",
|
|
129
130
|
},
|
|
131
|
+
inboundTargets: {
|
|
132
|
+
type: "array",
|
|
133
|
+
description: "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
properties: {
|
|
138
|
+
id: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Optional local display name used in logs and delivery ACK output.",
|
|
141
|
+
},
|
|
142
|
+
channel: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\".",
|
|
145
|
+
},
|
|
146
|
+
target: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx.",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ["channel", "target"],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
130
154
|
deliveryAckTimeoutMs: {
|
|
131
155
|
type: "number",
|
|
132
156
|
default: 15000,
|
|
@@ -141,5 +165,11 @@ export default definePluginEntry({
|
|
|
141
165
|
name: "libp2p Mesh Network",
|
|
142
166
|
description: "P2P network for cross-instance agent communication via libp2p.",
|
|
143
167
|
configSchema: createLibp2pMeshConfigSchema(),
|
|
144
|
-
register:
|
|
168
|
+
register: (api) => {
|
|
169
|
+
registerLibp2pMesh(api);
|
|
170
|
+
// 5. Register CLI commands (setup wizard + config management)
|
|
171
|
+
api.registerCli(registerLibp2pMeshCli, {
|
|
172
|
+
commands: ["libp2p-mesh"],
|
|
173
|
+
});
|
|
174
|
+
},
|
|
145
175
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstanceRouter, MeshNetwork } from "./types.js";
|
|
1
|
+
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
|
|
2
2
|
export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter): ({
|
|
3
3
|
name: string;
|
|
4
4
|
label: string;
|
|
@@ -407,9 +407,11 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
407
407
|
toPeerId: string;
|
|
408
408
|
ackMessageId?: string;
|
|
409
409
|
inboundChannel?: string;
|
|
410
|
+
inboundTarget?: string;
|
|
411
|
+
deliveryResults?: DeliveryTargetResult[];
|
|
410
412
|
error?: string;
|
|
411
413
|
};
|
|
412
|
-
isError: boolean;
|
|
414
|
+
isError: boolean | undefined;
|
|
413
415
|
} | {
|
|
414
416
|
content: {
|
|
415
417
|
type: "text";
|
|
@@ -422,6 +424,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
422
424
|
toPeerId: string;
|
|
423
425
|
ackMessageId?: string;
|
|
424
426
|
inboundChannel?: string;
|
|
427
|
+
inboundTarget?: string;
|
|
428
|
+
deliveryResults?: DeliveryTargetResult[];
|
|
425
429
|
error?: string;
|
|
426
430
|
};
|
|
427
431
|
isError?: undefined;
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
function targetLabel(result) {
|
|
2
|
+
const id = result.id?.trim();
|
|
3
|
+
const location = `${result.channel} / ${result.target}`;
|
|
4
|
+
return id ? `${id} (${location})` : location;
|
|
5
|
+
}
|
|
6
|
+
function formatDeliveryResults(instanceId, delivered, results) {
|
|
7
|
+
const header = delivered
|
|
8
|
+
? `发往 ${instanceId} 的消息投递结果:`
|
|
9
|
+
: `发往 ${instanceId} 的消息投递失败:`;
|
|
10
|
+
const lines = results.map((result) => {
|
|
11
|
+
if (result.ok) {
|
|
12
|
+
return `- ${targetLabel(result)}:已送达`;
|
|
13
|
+
}
|
|
14
|
+
return `- ${targetLabel(result)}:失败:${result.error ?? "unknown error"}`;
|
|
15
|
+
});
|
|
16
|
+
return [header, ...lines].join("\n");
|
|
17
|
+
}
|
|
1
18
|
export function buildP2PTools(mesh, router) {
|
|
2
19
|
return [
|
|
3
20
|
{
|
|
@@ -320,6 +337,18 @@ export function buildP2PTools(mesh, router) {
|
|
|
320
337
|
};
|
|
321
338
|
}
|
|
322
339
|
const result = await router.sendInstanceMessage(instanceId, message);
|
|
340
|
+
if (result.deliveryResults && result.deliveryResults.length > 0) {
|
|
341
|
+
return {
|
|
342
|
+
content: [
|
|
343
|
+
{
|
|
344
|
+
type: "text",
|
|
345
|
+
text: formatDeliveryResults(instanceId, result.delivered, result.deliveryResults),
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
details: result,
|
|
349
|
+
isError: result.delivered ? undefined : true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
323
352
|
if (!result.delivered) {
|
|
324
353
|
return {
|
|
325
354
|
content: [
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PluginLogger } from "openclaw/plugin-sdk/core";
|
|
2
|
+
interface MinimalCommand {
|
|
3
|
+
command(name: string): MinimalCommand;
|
|
4
|
+
description(desc: string): MinimalCommand;
|
|
5
|
+
option(flags: string, description?: string): MinimalCommand;
|
|
6
|
+
action(fn: (...args: any[]) => void | Promise<void>): MinimalCommand;
|
|
7
|
+
}
|
|
8
|
+
interface OpenClawPluginCliContext {
|
|
9
|
+
program: MinimalCommand;
|
|
10
|
+
config: Record<string, unknown>;
|
|
11
|
+
workspaceDir?: string;
|
|
12
|
+
logger: PluginLogger;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerLibp2pMeshCli(ctx: OpenClawPluginCliContext): void;
|
|
15
|
+
export {};
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { resolveConfigPath, readFullConfig, writeFullConfig, getNonDefaultConfig, getDefaultConfig, } from "./config-io.js";
|
|
2
|
+
import { createReadlinePrompter, runSetupWizard, WizardCancelledError, } from "./wizard.js";
|
|
3
|
+
export function registerLibp2pMeshCli(ctx) {
|
|
4
|
+
const { program, config: openclawConfig } = ctx;
|
|
5
|
+
const meshCmd = program
|
|
6
|
+
.command("libp2p-mesh")
|
|
7
|
+
.description("P2P Mesh 网络插件配置管理");
|
|
8
|
+
// ---- setup ----
|
|
9
|
+
meshCmd
|
|
10
|
+
.command("setup")
|
|
11
|
+
.description("交互式配置向导")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const configPath = resolveConfigPath();
|
|
14
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
15
|
+
// Discover available chat channels from config
|
|
16
|
+
const channels = openclawConfig.channels;
|
|
17
|
+
const availableChannels = [];
|
|
18
|
+
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
|
|
19
|
+
for (const [id, entry] of Object.entries(channels)) {
|
|
20
|
+
if (id !== "libp2p-mesh" &&
|
|
21
|
+
entry &&
|
|
22
|
+
typeof entry === "object" &&
|
|
23
|
+
entry.enabled !== false) {
|
|
24
|
+
availableChannels.push(id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const prompter = createReadlinePrompter();
|
|
29
|
+
try {
|
|
30
|
+
const newConfig = await runSetupWizard(prompter, pluginConfig, availableChannels);
|
|
31
|
+
writeFullConfig(configPath, newConfig);
|
|
32
|
+
console.log(`\n✓ 配置已写入 ${configPath}`);
|
|
33
|
+
console.log(" 运行 openclaw gateway restart 使新配置生效。");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof WizardCancelledError) {
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if (err instanceof Error) {
|
|
40
|
+
console.error(`\n✗ ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
prompter.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// ---- config subcommand ----
|
|
49
|
+
const configCmd = meshCmd
|
|
50
|
+
.command("config")
|
|
51
|
+
.description("增量配置管理");
|
|
52
|
+
// ---- config list ----
|
|
53
|
+
configCmd
|
|
54
|
+
.command("list")
|
|
55
|
+
.description("列出当前所有非默认配置")
|
|
56
|
+
.action(() => {
|
|
57
|
+
const configPath = resolveConfigPath();
|
|
58
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
59
|
+
const nonDefault = getNonDefaultConfig(pluginConfig);
|
|
60
|
+
const keys = Object.keys(nonDefault);
|
|
61
|
+
if (keys.length === 0) {
|
|
62
|
+
console.log("当前无自定义配置,全部使用默认值。");
|
|
63
|
+
console.log("运行 openclaw libp2p-mesh setup 进行配置。");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log("─────────────────────────────────");
|
|
67
|
+
console.log(" 当前 libp2p-mesh 配置:\n");
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = nonDefault[key];
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
console.log(` ${key}:`);
|
|
72
|
+
if (value.length === 0) {
|
|
73
|
+
console.log(" (空列表)");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
for (const item of value) {
|
|
77
|
+
if (typeof item === "object" && item !== null) {
|
|
78
|
+
const obj = item;
|
|
79
|
+
const label = obj.id ? `${obj.id} — ` : "";
|
|
80
|
+
console.log(` - ${label}${obj.channel ?? ""} / ${obj.target ?? ""}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(` - ${item}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(` ${key}: ${value}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log(`\n ...共 ${keys.length} 项非默认配置`);
|
|
93
|
+
});
|
|
94
|
+
// ---- config get ----
|
|
95
|
+
configCmd
|
|
96
|
+
.command("get <key>")
|
|
97
|
+
.description("读取单个配置值")
|
|
98
|
+
.action((key) => {
|
|
99
|
+
const configPath = resolveConfigPath();
|
|
100
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
101
|
+
const defaults = getDefaultConfig();
|
|
102
|
+
const value = key in pluginConfig ? pluginConfig[key] : defaults[key];
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
console.log(` (未配置)`);
|
|
105
|
+
}
|
|
106
|
+
else if (Array.isArray(value)) {
|
|
107
|
+
if (value.length === 0) {
|
|
108
|
+
console.log(" (空列表)");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const item of value) {
|
|
112
|
+
if (typeof item === "object" && item !== null) {
|
|
113
|
+
console.log(JSON.stringify(item, null, 2));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log(` ${item}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(` ${value}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// ---- config set ----
|
|
126
|
+
configCmd
|
|
127
|
+
.command("set <key> [value]")
|
|
128
|
+
.description("设置配置值。数组类型使用 --add / --remove")
|
|
129
|
+
.option("--add <item>", "追加到数组")
|
|
130
|
+
.option("--remove <item>", "从数组中移除")
|
|
131
|
+
.action(async (key, value, opts) => {
|
|
132
|
+
const configPath = resolveConfigPath();
|
|
133
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
134
|
+
const defaults = getDefaultConfig();
|
|
135
|
+
const current = key in pluginConfig ? pluginConfig[key] : defaults[key];
|
|
136
|
+
const oldValue = current;
|
|
137
|
+
let newValue;
|
|
138
|
+
// Array operations
|
|
139
|
+
if (opts.add || opts.remove) {
|
|
140
|
+
const arr = Array.isArray(current) ? [...current] : [];
|
|
141
|
+
if (opts.add) {
|
|
142
|
+
if (arr.includes(opts.add)) {
|
|
143
|
+
console.log(` ⚠ 该值已存在`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
arr.push(opts.add);
|
|
147
|
+
console.log(` ✓ 已追加`);
|
|
148
|
+
}
|
|
149
|
+
if (opts.remove) {
|
|
150
|
+
const idx = arr.indexOf(opts.remove);
|
|
151
|
+
if (idx === -1) {
|
|
152
|
+
console.log(` ⚠ 未找到该值`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
arr.splice(idx, 1);
|
|
156
|
+
console.log(` ✓ 已移除`);
|
|
157
|
+
}
|
|
158
|
+
newValue = arr;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Scalar
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
console.error(" 请提供值。用法: openclaw libp2p-mesh config set <key> <value>");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
// Auto-detect boolean and number
|
|
167
|
+
if (value === "true")
|
|
168
|
+
newValue = true;
|
|
169
|
+
else if (value === "false")
|
|
170
|
+
newValue = false;
|
|
171
|
+
else if (/^-?\d+(\.\d+)?$/.test(value))
|
|
172
|
+
newValue = Number(value);
|
|
173
|
+
else
|
|
174
|
+
newValue = value;
|
|
175
|
+
}
|
|
176
|
+
// Write
|
|
177
|
+
const updates = { ...pluginConfig, [key]: newValue };
|
|
178
|
+
writeFullConfig(configPath, updates);
|
|
179
|
+
// Feedback
|
|
180
|
+
if (!opts.add && !opts.remove) {
|
|
181
|
+
console.log(` ✓ ${key}: ${JSON.stringify(oldValue)} → ${JSON.stringify(newValue)}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// ---- config unset ----
|
|
185
|
+
configCmd
|
|
186
|
+
.command("unset <key>")
|
|
187
|
+
.description("删除 key,恢复默认值")
|
|
188
|
+
.action((key) => {
|
|
189
|
+
const configPath = resolveConfigPath();
|
|
190
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
191
|
+
const newConfig = { ...pluginConfig };
|
|
192
|
+
delete newConfig[key];
|
|
193
|
+
writeFullConfig(configPath, newConfig);
|
|
194
|
+
console.log(` ✓ ${key} 已恢复为默认值`);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const MULTIADDR_PATTERN: RegExp;
|
|
2
|
+
export declare function getDefaultConfig(): Record<string, unknown>;
|
|
3
|
+
export declare function resolveConfigPath(): string;
|
|
4
|
+
export declare function readFullConfig(configPath: string): {
|
|
5
|
+
config: Record<string, unknown>;
|
|
6
|
+
pluginConfig: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export declare function writeFullConfig(configPath: string, pluginConfigUpdates: Record<string, unknown>): void;
|
|
9
|
+
export declare function getNonDefaultConfig(pluginConfig: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
export const MULTIADDR_PATTERN = /^(\/ip[46]\/(?:[\d.]+|[0-9a-fA-F:]+)|\/dns(?:[46])?\/[a-zA-Z0-9][a-zA-Z0-9.-]*)\/(?:tcp|udp|ws|wss)\/\d+(?:\/p2p\/[12][a-zA-Z2-7]{48,})?$/;
|
|
5
|
+
export function getDefaultConfig() {
|
|
6
|
+
return {
|
|
7
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
8
|
+
enableWebSocket: false,
|
|
9
|
+
discovery: "mdns",
|
|
10
|
+
meshTopic: "openclaw-mesh",
|
|
11
|
+
enablePubsub: true,
|
|
12
|
+
enableAgentSync: true,
|
|
13
|
+
enableDHT: true,
|
|
14
|
+
enableNATTraversal: true,
|
|
15
|
+
enableIdentify: true,
|
|
16
|
+
enableAutoNAT: true,
|
|
17
|
+
enableUPnP: true,
|
|
18
|
+
enableCircuitRelay: true,
|
|
19
|
+
enableCircuitRelayServer: false,
|
|
20
|
+
enableDCUtR: true,
|
|
21
|
+
discoverRelays: 0,
|
|
22
|
+
deliveryAckTimeoutMs: 15000,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function resolveConfigPath() {
|
|
26
|
+
if (process.env.OPENCLAW_CONFIG_PATH) {
|
|
27
|
+
const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
28
|
+
return path.resolve(resolved);
|
|
29
|
+
}
|
|
30
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR
|
|
31
|
+
? process.env.OPENCLAW_STATE_DIR.replace(/^~(?=$|\/|\\)/, os.homedir())
|
|
32
|
+
: path.join(os.homedir(), ".openclaw");
|
|
33
|
+
return path.join(stateDir, "openclaw.json");
|
|
34
|
+
}
|
|
35
|
+
export function readFullConfig(configPath) {
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code === "ENOENT") {
|
|
42
|
+
return { config: {}, pluginConfig: {} };
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`无法解析 ${configPath}: ${err.message}\n请手动修复 JSON 格式后重试。`);
|
|
45
|
+
}
|
|
46
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
47
|
+
throw new Error(`${configPath} 内容不是合法的 JSON 对象。`);
|
|
48
|
+
}
|
|
49
|
+
const plugins = raw.plugins;
|
|
50
|
+
const entries = plugins && typeof plugins === "object" && !Array.isArray(plugins)
|
|
51
|
+
? plugins.entries
|
|
52
|
+
: undefined;
|
|
53
|
+
const entry = entries && typeof entries === "object" && !Array.isArray(entries)
|
|
54
|
+
? entries["libp2p-mesh"]
|
|
55
|
+
: undefined;
|
|
56
|
+
const pluginConfig = entry && typeof entry === "object" && !Array.isArray(entry)
|
|
57
|
+
? entry.config ?? {}
|
|
58
|
+
: {};
|
|
59
|
+
return { config: raw, pluginConfig };
|
|
60
|
+
}
|
|
61
|
+
export function writeFullConfig(configPath, pluginConfigUpdates) {
|
|
62
|
+
// Ensure directory exists
|
|
63
|
+
const dir = path.dirname(configPath);
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
// Read existing or start fresh
|
|
66
|
+
let base = {};
|
|
67
|
+
try {
|
|
68
|
+
base = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err.code !== "ENOENT") {
|
|
72
|
+
throw new Error(`无法读取 ${configPath}: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Create backup
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(configPath)) {
|
|
78
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
83
|
+
}
|
|
84
|
+
// Build output object with deep merge
|
|
85
|
+
const output = structuredClone(typeof base === "object" && !Array.isArray(base) ? base : {});
|
|
86
|
+
// Ensure plugins.entries["libp2p-mesh"] exists
|
|
87
|
+
if (!output.plugins || typeof output.plugins !== "object" || Array.isArray(output.plugins)) {
|
|
88
|
+
output.plugins = {};
|
|
89
|
+
}
|
|
90
|
+
const plugins = output.plugins;
|
|
91
|
+
if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) {
|
|
92
|
+
plugins.entries = {};
|
|
93
|
+
}
|
|
94
|
+
const entries = plugins.entries;
|
|
95
|
+
if (!entries["libp2p-mesh"] ||
|
|
96
|
+
typeof entries["libp2p-mesh"] !== "object" ||
|
|
97
|
+
Array.isArray(entries["libp2p-mesh"])) {
|
|
98
|
+
entries["libp2p-mesh"] = {};
|
|
99
|
+
}
|
|
100
|
+
const meshEntry = entries["libp2p-mesh"];
|
|
101
|
+
meshEntry.enabled = true;
|
|
102
|
+
// Merge plugin config shallowly (key-level)
|
|
103
|
+
if (!meshEntry.config || typeof meshEntry.config !== "object" || Array.isArray(meshEntry.config)) {
|
|
104
|
+
meshEntry.config = {};
|
|
105
|
+
}
|
|
106
|
+
const existing = meshEntry.config;
|
|
107
|
+
meshEntry.config = { ...existing, ...pluginConfigUpdates };
|
|
108
|
+
// Ensure channels["libp2p-mesh"].enabled exists
|
|
109
|
+
if (!output.channels ||
|
|
110
|
+
typeof output.channels !== "object" ||
|
|
111
|
+
Array.isArray(output.channels)) {
|
|
112
|
+
output.channels = {};
|
|
113
|
+
}
|
|
114
|
+
const channels = output.channels;
|
|
115
|
+
if (!channels["libp2p-mesh"] ||
|
|
116
|
+
typeof channels["libp2p-mesh"] !== "object" ||
|
|
117
|
+
Array.isArray(channels["libp2p-mesh"])) {
|
|
118
|
+
channels["libp2p-mesh"] = {};
|
|
119
|
+
}
|
|
120
|
+
const meshChannel = channels["libp2p-mesh"];
|
|
121
|
+
meshChannel.enabled = true;
|
|
122
|
+
// Write atomically (write to temp, then rename)
|
|
123
|
+
const tmpPath = configPath + ".tmp";
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(tmpPath, JSON.stringify(output, null, 2) + "\n", "utf-8");
|
|
126
|
+
fs.renameSync(tmpPath, configPath);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Rollback from backup
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(configPath + ".bak")) {
|
|
132
|
+
fs.copyFileSync(configPath + ".bak", configPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// rollback failure — leave existing state
|
|
137
|
+
}
|
|
138
|
+
// Clean up temp
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(tmpPath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// ok
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`写入 ${configPath} 失败:${err.message}。配置未更改。`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function getNonDefaultConfig(pluginConfig) {
|
|
149
|
+
const defaults = getDefaultConfig();
|
|
150
|
+
const result = {};
|
|
151
|
+
for (const key of Object.keys(pluginConfig)) {
|
|
152
|
+
const value = pluginConfig[key];
|
|
153
|
+
if (value === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
if (!(key in defaults)) {
|
|
156
|
+
// Unknown key — include (could be a new key added in later version)
|
|
157
|
+
result[key] = value;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const def = defaults[key];
|
|
161
|
+
if (Array.isArray(value) && Array.isArray(def)) {
|
|
162
|
+
if (value.length === 0 && def.length > 0)
|
|
163
|
+
continue;
|
|
164
|
+
if (value.length !== def.length || value.some((v, i) => v !== def[i])) {
|
|
165
|
+
result[key] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (value !== def) {
|
|
169
|
+
result[key] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter, OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
1
2
|
import type { InboundDeliveryAdapter } from "./types.js";
|
|
2
3
|
export type DeliveryLogger = {
|
|
3
4
|
info?: (message: string) => void;
|
|
4
5
|
debug?: (message: string) => void;
|
|
5
6
|
warn?: (message: string) => void;
|
|
6
7
|
};
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export type LoadChannelOutboundAdapter = (channel: string) => Promise<ChannelOutboundAdapter | undefined>;
|
|
9
|
+
export declare function createOpenClawRuntimeInboundDelivery(options: {
|
|
10
|
+
config: OpenClawConfig;
|
|
11
|
+
loadAdapter: LoadChannelOutboundAdapter;
|
|
10
12
|
logger?: DeliveryLogger;
|
|
11
13
|
}): InboundDeliveryAdapter;
|