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/src/cli.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { PluginLogger } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import {
|
|
3
|
+
resolveConfigPath,
|
|
4
|
+
readFullConfig,
|
|
5
|
+
writeFullConfig,
|
|
6
|
+
getNonDefaultConfig,
|
|
7
|
+
getDefaultConfig,
|
|
8
|
+
} from "./config-io.js";
|
|
9
|
+
import {
|
|
10
|
+
createReadlinePrompter,
|
|
11
|
+
runSetupWizard,
|
|
12
|
+
WizardCancelledError,
|
|
13
|
+
} from "./wizard.js";
|
|
14
|
+
|
|
15
|
+
// OpenClawPluginCliContext is defined in openclaw's types.d.ts but not re-exported
|
|
16
|
+
// from the public SDK barrel modules at this version. Define the shape locally.
|
|
17
|
+
interface MinimalCommand {
|
|
18
|
+
command(name: string): MinimalCommand;
|
|
19
|
+
description(desc: string): MinimalCommand;
|
|
20
|
+
option(flags: string, description?: string): MinimalCommand;
|
|
21
|
+
action(fn: (...args: any[]) => void | Promise<void>): MinimalCommand;
|
|
22
|
+
}
|
|
23
|
+
interface OpenClawPluginCliContext {
|
|
24
|
+
program: MinimalCommand;
|
|
25
|
+
config: Record<string, unknown>;
|
|
26
|
+
workspaceDir?: string;
|
|
27
|
+
logger: PluginLogger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerLibp2pMeshCli(ctx: OpenClawPluginCliContext): void {
|
|
31
|
+
const { program, config: openclawConfig } = ctx;
|
|
32
|
+
|
|
33
|
+
const meshCmd = program
|
|
34
|
+
.command("libp2p-mesh")
|
|
35
|
+
.description("P2P Mesh 网络插件配置管理");
|
|
36
|
+
|
|
37
|
+
// ---- setup ----
|
|
38
|
+
meshCmd
|
|
39
|
+
.command("setup")
|
|
40
|
+
.description("交互式配置向导")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const configPath = resolveConfigPath();
|
|
43
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
44
|
+
|
|
45
|
+
// Discover available chat channels from config
|
|
46
|
+
const channels = openclawConfig.channels;
|
|
47
|
+
const availableChannels: string[] = [];
|
|
48
|
+
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
|
|
49
|
+
for (const [id, entry] of Object.entries(channels as Record<string, unknown>)) {
|
|
50
|
+
if (
|
|
51
|
+
id !== "libp2p-mesh" &&
|
|
52
|
+
entry &&
|
|
53
|
+
typeof entry === "object" &&
|
|
54
|
+
(entry as Record<string, unknown>).enabled !== false
|
|
55
|
+
) {
|
|
56
|
+
availableChannels.push(id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prompter = createReadlinePrompter();
|
|
62
|
+
try {
|
|
63
|
+
const newConfig = await runSetupWizard(prompter, pluginConfig, availableChannels);
|
|
64
|
+
writeFullConfig(configPath, newConfig);
|
|
65
|
+
console.log(`\n✓ 配置已写入 ${configPath}`);
|
|
66
|
+
console.log(" 运行 openclaw gateway restart 使新配置生效。");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof WizardCancelledError) {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
if (err instanceof Error) {
|
|
72
|
+
console.error(`\n✗ ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
} finally {
|
|
76
|
+
prompter.close();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ---- config subcommand ----
|
|
81
|
+
const configCmd = meshCmd
|
|
82
|
+
.command("config")
|
|
83
|
+
.description("增量配置管理");
|
|
84
|
+
|
|
85
|
+
// ---- config list ----
|
|
86
|
+
configCmd
|
|
87
|
+
.command("list")
|
|
88
|
+
.description("列出当前所有非默认配置")
|
|
89
|
+
.action(() => {
|
|
90
|
+
const configPath = resolveConfigPath();
|
|
91
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
92
|
+
const nonDefault = getNonDefaultConfig(pluginConfig);
|
|
93
|
+
const keys = Object.keys(nonDefault);
|
|
94
|
+
|
|
95
|
+
if (keys.length === 0) {
|
|
96
|
+
console.log("当前无自定义配置,全部使用默认值。");
|
|
97
|
+
console.log("运行 openclaw libp2p-mesh setup 进行配置。");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("─────────────────────────────────");
|
|
102
|
+
console.log(" 当前 libp2p-mesh 配置:\n");
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
const value = nonDefault[key];
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
console.log(` ${key}:`);
|
|
107
|
+
if (value.length === 0) {
|
|
108
|
+
console.log(" (空列表)");
|
|
109
|
+
} else {
|
|
110
|
+
for (const item of value) {
|
|
111
|
+
if (typeof item === "object" && item !== null) {
|
|
112
|
+
const obj = item as Record<string, unknown>;
|
|
113
|
+
const label = obj.id ? `${obj.id} — ` : "";
|
|
114
|
+
console.log(` - ${label}${obj.channel ?? ""} / ${obj.target ?? ""}`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(` - ${item}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${key}: ${value}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log(`\n ...共 ${keys.length} 项非默认配置`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---- config get ----
|
|
128
|
+
configCmd
|
|
129
|
+
.command("get <key>")
|
|
130
|
+
.description("读取单个配置值")
|
|
131
|
+
.action((key: string) => {
|
|
132
|
+
const configPath = resolveConfigPath();
|
|
133
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
134
|
+
const defaults = getDefaultConfig();
|
|
135
|
+
const value = key in pluginConfig ? pluginConfig[key] : defaults[key];
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
console.log(` (未配置)`);
|
|
138
|
+
} else if (Array.isArray(value)) {
|
|
139
|
+
if (value.length === 0) {
|
|
140
|
+
console.log(" (空列表)");
|
|
141
|
+
} else {
|
|
142
|
+
for (const item of value) {
|
|
143
|
+
if (typeof item === "object" && item !== null) {
|
|
144
|
+
console.log(JSON.stringify(item, null, 2));
|
|
145
|
+
} else {
|
|
146
|
+
console.log(` ${item}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.log(` ${value}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---- config set ----
|
|
156
|
+
configCmd
|
|
157
|
+
.command("set <key> [value]")
|
|
158
|
+
.description("设置配置值。数组类型使用 --add / --remove")
|
|
159
|
+
.option("--add <item>", "追加到数组")
|
|
160
|
+
.option("--remove <item>", "从数组中移除")
|
|
161
|
+
.action(async (key: string, value: string | undefined, opts: { add?: string; remove?: string }) => {
|
|
162
|
+
const configPath = resolveConfigPath();
|
|
163
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
164
|
+
const defaults = getDefaultConfig();
|
|
165
|
+
const current = key in pluginConfig ? pluginConfig[key] : defaults[key];
|
|
166
|
+
const oldValue = current;
|
|
167
|
+
|
|
168
|
+
let newValue: unknown;
|
|
169
|
+
|
|
170
|
+
// Array operations
|
|
171
|
+
if (opts.add || opts.remove) {
|
|
172
|
+
const arr: unknown[] = Array.isArray(current) ? [...current] : [];
|
|
173
|
+
if (opts.add) {
|
|
174
|
+
if (arr.includes(opts.add)) {
|
|
175
|
+
console.log(` ⚠ 该值已存在`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
arr.push(opts.add);
|
|
179
|
+
console.log(` ✓ 已追加`);
|
|
180
|
+
}
|
|
181
|
+
if (opts.remove) {
|
|
182
|
+
const idx = arr.indexOf(opts.remove);
|
|
183
|
+
if (idx === -1) {
|
|
184
|
+
console.log(` ⚠ 未找到该值`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
arr.splice(idx, 1);
|
|
188
|
+
console.log(` ✓ 已移除`);
|
|
189
|
+
}
|
|
190
|
+
newValue = arr;
|
|
191
|
+
} else {
|
|
192
|
+
// Scalar
|
|
193
|
+
if (value === undefined) {
|
|
194
|
+
console.error(" 请提供值。用法: openclaw libp2p-mesh config set <key> <value>");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
// Auto-detect boolean and number
|
|
198
|
+
if (value === "true") newValue = true;
|
|
199
|
+
else if (value === "false") newValue = false;
|
|
200
|
+
else if (/^-?\d+(\.\d+)?$/.test(value)) newValue = Number(value);
|
|
201
|
+
else newValue = value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Write
|
|
205
|
+
const updates = { ...pluginConfig, [key]: newValue };
|
|
206
|
+
writeFullConfig(configPath, updates);
|
|
207
|
+
|
|
208
|
+
// Feedback
|
|
209
|
+
if (!opts.add && !opts.remove) {
|
|
210
|
+
console.log(` ✓ ${key}: ${JSON.stringify(oldValue)} → ${JSON.stringify(newValue)}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ---- config unset ----
|
|
215
|
+
configCmd
|
|
216
|
+
.command("unset <key>")
|
|
217
|
+
.description("删除 key,恢复默认值")
|
|
218
|
+
.action((key: string) => {
|
|
219
|
+
const configPath = resolveConfigPath();
|
|
220
|
+
const { pluginConfig } = readFullConfig(configPath);
|
|
221
|
+
const newConfig = { ...pluginConfig };
|
|
222
|
+
delete newConfig[key];
|
|
223
|
+
writeFullConfig(configPath, newConfig);
|
|
224
|
+
console.log(` ✓ ${key} 已恢复为默认值`);
|
|
225
|
+
});
|
|
226
|
+
}
|
package/src/config-io.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
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,})?$/;
|
|
6
|
+
|
|
7
|
+
export function getDefaultConfig(): Record<string, unknown> {
|
|
8
|
+
return {
|
|
9
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
10
|
+
enableWebSocket: false,
|
|
11
|
+
discovery: "mdns",
|
|
12
|
+
meshTopic: "openclaw-mesh",
|
|
13
|
+
enablePubsub: true,
|
|
14
|
+
enableAgentSync: true,
|
|
15
|
+
enableDHT: true,
|
|
16
|
+
enableNATTraversal: true,
|
|
17
|
+
enableIdentify: true,
|
|
18
|
+
enableAutoNAT: true,
|
|
19
|
+
enableUPnP: true,
|
|
20
|
+
enableCircuitRelay: true,
|
|
21
|
+
enableCircuitRelayServer: false,
|
|
22
|
+
enableDCUtR: true,
|
|
23
|
+
discoverRelays: 0,
|
|
24
|
+
deliveryAckTimeoutMs: 15000,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveConfigPath(): string {
|
|
29
|
+
if (process.env.OPENCLAW_CONFIG_PATH) {
|
|
30
|
+
const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
31
|
+
return path.resolve(resolved);
|
|
32
|
+
}
|
|
33
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR
|
|
34
|
+
? process.env.OPENCLAW_STATE_DIR.replace(/^~(?=$|\/|\\)/, os.homedir())
|
|
35
|
+
: path.join(os.homedir(), ".openclaw");
|
|
36
|
+
return path.join(stateDir, "openclaw.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readFullConfig(configPath: string): {
|
|
40
|
+
config: Record<string, unknown>;
|
|
41
|
+
pluginConfig: Record<string, unknown>;
|
|
42
|
+
} {
|
|
43
|
+
let raw: Record<string, unknown>;
|
|
44
|
+
try {
|
|
45
|
+
raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
46
|
+
} catch (err: unknown) {
|
|
47
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
48
|
+
return { config: {}, pluginConfig: {} };
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
`无法解析 ${configPath}: ${(err as Error).message}\n请手动修复 JSON 格式后重试。`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
55
|
+
throw new Error(`${configPath} 内容不是合法的 JSON 对象。`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const plugins = (raw as Record<string, unknown>).plugins;
|
|
59
|
+
const entries =
|
|
60
|
+
plugins && typeof plugins === "object" && !Array.isArray(plugins)
|
|
61
|
+
? (plugins as Record<string, unknown>).entries
|
|
62
|
+
: undefined;
|
|
63
|
+
const entry =
|
|
64
|
+
entries && typeof entries === "object" && !Array.isArray(entries)
|
|
65
|
+
? (entries as Record<string, unknown>)["libp2p-mesh"]
|
|
66
|
+
: undefined;
|
|
67
|
+
const pluginConfig =
|
|
68
|
+
entry && typeof entry === "object" && !Array.isArray(entry)
|
|
69
|
+
? ((entry as Record<string, unknown>).config as Record<string, unknown>) ?? {}
|
|
70
|
+
: {};
|
|
71
|
+
|
|
72
|
+
return { config: raw as Record<string, unknown>, pluginConfig };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeFullConfig(
|
|
76
|
+
configPath: string,
|
|
77
|
+
pluginConfigUpdates: Record<string, unknown>,
|
|
78
|
+
): void {
|
|
79
|
+
// Ensure directory exists
|
|
80
|
+
const dir = path.dirname(configPath);
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
// Read existing or start fresh
|
|
84
|
+
let base: Record<string, unknown> = {};
|
|
85
|
+
try {
|
|
86
|
+
base = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
87
|
+
} catch (err: unknown) {
|
|
88
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`无法读取 ${configPath}: ${(err as Error).message}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create backup
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(configPath)) {
|
|
98
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Build output object with deep merge
|
|
105
|
+
const output = structuredClone(
|
|
106
|
+
typeof base === "object" && !Array.isArray(base) ? base : {},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Ensure plugins.entries["libp2p-mesh"] exists
|
|
110
|
+
if (!output.plugins || typeof output.plugins !== "object" || Array.isArray(output.plugins)) {
|
|
111
|
+
output.plugins = {};
|
|
112
|
+
}
|
|
113
|
+
const plugins = output.plugins as Record<string, unknown>;
|
|
114
|
+
if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) {
|
|
115
|
+
plugins.entries = {};
|
|
116
|
+
}
|
|
117
|
+
const entries = plugins.entries as Record<string, unknown>;
|
|
118
|
+
if (
|
|
119
|
+
!entries["libp2p-mesh"] ||
|
|
120
|
+
typeof entries["libp2p-mesh"] !== "object" ||
|
|
121
|
+
Array.isArray(entries["libp2p-mesh"])
|
|
122
|
+
) {
|
|
123
|
+
entries["libp2p-mesh"] = {};
|
|
124
|
+
}
|
|
125
|
+
const meshEntry = entries["libp2p-mesh"] as Record<string, unknown>;
|
|
126
|
+
meshEntry.enabled = true;
|
|
127
|
+
|
|
128
|
+
// Merge plugin config shallowly (key-level)
|
|
129
|
+
if (!meshEntry.config || typeof meshEntry.config !== "object" || Array.isArray(meshEntry.config)) {
|
|
130
|
+
meshEntry.config = {};
|
|
131
|
+
}
|
|
132
|
+
const existing = meshEntry.config as Record<string, unknown>;
|
|
133
|
+
meshEntry.config = { ...existing, ...pluginConfigUpdates };
|
|
134
|
+
|
|
135
|
+
// Ensure channels["libp2p-mesh"].enabled exists
|
|
136
|
+
if (
|
|
137
|
+
!output.channels ||
|
|
138
|
+
typeof output.channels !== "object" ||
|
|
139
|
+
Array.isArray(output.channels)
|
|
140
|
+
) {
|
|
141
|
+
output.channels = {};
|
|
142
|
+
}
|
|
143
|
+
const channels = output.channels as Record<string, unknown>;
|
|
144
|
+
if (
|
|
145
|
+
!channels["libp2p-mesh"] ||
|
|
146
|
+
typeof channels["libp2p-mesh"] !== "object" ||
|
|
147
|
+
Array.isArray(channels["libp2p-mesh"])
|
|
148
|
+
) {
|
|
149
|
+
channels["libp2p-mesh"] = {};
|
|
150
|
+
}
|
|
151
|
+
const meshChannel = channels["libp2p-mesh"] as Record<string, unknown>;
|
|
152
|
+
meshChannel.enabled = true;
|
|
153
|
+
|
|
154
|
+
// Write atomically (write to temp, then rename)
|
|
155
|
+
const tmpPath = configPath + ".tmp";
|
|
156
|
+
try {
|
|
157
|
+
fs.writeFileSync(tmpPath, JSON.stringify(output, null, 2) + "\n", "utf-8");
|
|
158
|
+
fs.renameSync(tmpPath, configPath);
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
// Rollback from backup
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(configPath + ".bak")) {
|
|
163
|
+
fs.copyFileSync(configPath + ".bak", configPath);
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// rollback failure — leave existing state
|
|
167
|
+
}
|
|
168
|
+
// Clean up temp
|
|
169
|
+
try {
|
|
170
|
+
fs.unlinkSync(tmpPath);
|
|
171
|
+
} catch {
|
|
172
|
+
// ok
|
|
173
|
+
}
|
|
174
|
+
throw new Error(
|
|
175
|
+
`写入 ${configPath} 失败:${(err as Error).message}。配置未更改。`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getNonDefaultConfig(
|
|
181
|
+
pluginConfig: Record<string, unknown>,
|
|
182
|
+
): Record<string, unknown> {
|
|
183
|
+
const defaults = getDefaultConfig();
|
|
184
|
+
const result: Record<string, unknown> = {};
|
|
185
|
+
for (const key of Object.keys(pluginConfig)) {
|
|
186
|
+
const value = pluginConfig[key];
|
|
187
|
+
if (value === undefined) continue;
|
|
188
|
+
if (!(key in defaults)) {
|
|
189
|
+
// Unknown key — include (could be a new key added in later version)
|
|
190
|
+
result[key] = value;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const def = defaults[key];
|
|
194
|
+
if (Array.isArray(value) && Array.isArray(def)) {
|
|
195
|
+
if (value.length === 0 && def.length > 0) continue;
|
|
196
|
+
if (value.length !== def.length || value.some((v, i) => v !== def[i])) {
|
|
197
|
+
result[key] = value;
|
|
198
|
+
}
|
|
199
|
+
} else if (value !== def) {
|
|
200
|
+
result[key] = value;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
package/src/inbound-delivery.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOutboundAdapter,
|
|
3
|
+
OpenClawConfig,
|
|
4
|
+
} from "openclaw/plugin-sdk/core";
|
|
2
5
|
import type {
|
|
3
6
|
InboundDeliveryAdapter,
|
|
4
7
|
InboundDeliveryRequest,
|
|
@@ -11,107 +14,60 @@ export type DeliveryLogger = {
|
|
|
11
14
|
warn?: (message: string) => void;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
export type LoadChannelOutboundAdapter = (
|
|
18
|
+
channel: string,
|
|
19
|
+
) => Promise<ChannelOutboundAdapter | undefined>;
|
|
20
|
+
|
|
21
|
+
function summarizeError(error: unknown): string {
|
|
22
|
+
return error instanceof Error ? error.message : String(error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createOpenClawRuntimeInboundDelivery(options: {
|
|
26
|
+
config: OpenClawConfig;
|
|
27
|
+
loadAdapter: LoadChannelOutboundAdapter;
|
|
17
28
|
logger?: DeliveryLogger;
|
|
18
29
|
}): InboundDeliveryAdapter {
|
|
19
|
-
const
|
|
20
|
-
const timeoutMs = options?.timeoutMs ?? 15000;
|
|
21
|
-
const logger = options?.logger;
|
|
30
|
+
const { config, loadAdapter, logger } = options;
|
|
22
31
|
|
|
23
32
|
return {
|
|
24
|
-
deliver(request: InboundDeliveryRequest): Promise<InboundDeliveryResult> {
|
|
25
|
-
const args = [
|
|
26
|
-
"message",
|
|
27
|
-
"send",
|
|
28
|
-
"--channel",
|
|
29
|
-
request.channel,
|
|
30
|
-
"--target",
|
|
31
|
-
request.target,
|
|
32
|
-
"--message",
|
|
33
|
-
request.text,
|
|
34
|
-
];
|
|
35
|
-
|
|
33
|
+
async deliver(request: InboundDeliveryRequest): Promise<InboundDeliveryResult> {
|
|
36
34
|
logger?.debug?.(
|
|
37
|
-
`[libp2p-mesh] Forwarding inbound delivery via
|
|
35
|
+
`[libp2p-mesh] Forwarding inbound delivery via runtime channel adapter: ${request.channel}/${request.target}`,
|
|
38
36
|
);
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const finish = (result: InboundDeliveryResult): void => {
|
|
48
|
-
if (settled) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
settled = true;
|
|
52
|
-
if (timeout) {
|
|
53
|
-
clearTimeout(timeout);
|
|
54
|
-
}
|
|
55
|
-
resolve(result);
|
|
38
|
+
const adapter = await loadAdapter(request.channel);
|
|
39
|
+
if (!adapter?.sendText) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
channel: request.channel,
|
|
43
|
+
target: request.target,
|
|
44
|
+
error: `channel ${request.channel} does not expose runtime text delivery`,
|
|
56
45
|
};
|
|
46
|
+
}
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
finish({
|
|
64
|
-
ok: false,
|
|
65
|
-
channel: request.channel,
|
|
66
|
-
target: request.target,
|
|
67
|
-
error: `openclaw message send timed out after ${timeoutMs}ms`,
|
|
68
|
-
});
|
|
69
|
-
}, timeoutMs);
|
|
70
|
-
|
|
71
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
72
|
-
stdout.push(chunk);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
76
|
-
stderr.push(chunk);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
child.on("error", (err) => {
|
|
80
|
-
finish({
|
|
81
|
-
ok: false,
|
|
82
|
-
channel: request.channel,
|
|
83
|
-
target: request.target,
|
|
84
|
-
error: String(err),
|
|
85
|
-
});
|
|
48
|
+
try {
|
|
49
|
+
await adapter.sendText({
|
|
50
|
+
cfg: config,
|
|
51
|
+
to: request.target,
|
|
52
|
+
text: request.text,
|
|
86
53
|
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
channel: request.channel,
|
|
58
|
+
target: request.target,
|
|
59
|
+
error: summarizeError(error),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
87
62
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
target: request.target,
|
|
97
|
-
});
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const stderrText = Buffer.concat(stderr).toString().trim();
|
|
102
|
-
const stdoutText = Buffer.concat(stdout).toString().trim();
|
|
103
|
-
|
|
104
|
-
finish({
|
|
105
|
-
ok: false,
|
|
106
|
-
channel: request.channel,
|
|
107
|
-
target: request.target,
|
|
108
|
-
error:
|
|
109
|
-
stderrText ||
|
|
110
|
-
stdoutText ||
|
|
111
|
-
`openclaw message send exited with code ${code}`,
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
|
63
|
+
logger?.info?.(
|
|
64
|
+
`[libp2p-mesh] Delivered inbound message to ${request.channel}/${request.target}`,
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
channel: request.channel,
|
|
69
|
+
target: request.target,
|
|
70
|
+
};
|
|
115
71
|
},
|
|
116
72
|
};
|
|
117
73
|
}
|
package/src/inbound.ts
CHANGED
|
@@ -8,10 +8,11 @@ export type InboundHandlerDeps = {
|
|
|
8
8
|
warn?: (msg: string) => void;
|
|
9
9
|
error?: (msg: string) => void;
|
|
10
10
|
};
|
|
11
|
+
sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
|
|
14
|
-
const { logger } = deps;
|
|
15
|
+
const { logger, sendToChannel } = deps;
|
|
15
16
|
const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
|
|
16
17
|
const signedTag = msg.signature ? " [signed]" : "";
|
|
17
18
|
|
|
@@ -45,9 +46,20 @@ export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): voi
|
|
|
45
46
|
logger?.info?.(
|
|
46
47
|
`[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`,
|
|
47
48
|
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Direct message — log and forward to local channel
|
|
53
|
+
logger?.info?.(
|
|
54
|
+
`[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!sendToChannel || !msg.payload) {
|
|
58
|
+
return;
|
|
52
59
|
}
|
|
60
|
+
|
|
61
|
+
const text = `[来自 ${msg.from}]\n${msg.payload}`;
|
|
62
|
+
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
63
|
+
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
64
|
+
});
|
|
53
65
|
}
|