libp2p-mesh 2026.5.12 → 2026.5.13
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 +31 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -1
- package/dist/src/agent-tools-feishu.test.d.ts +1 -0
- package/dist/src/agent-tools-feishu.test.js +57 -0
- package/dist/src/agent-tools.d.ts +48 -0
- package/dist/src/agent-tools.js +44 -0
- package/dist/src/config-schema.test.d.ts +1 -0
- package/dist/src/config-schema.test.js +55 -0
- package/dist/src/feishu-channel.d.ts +19 -0
- package/dist/src/feishu-channel.js +202 -0
- package/dist/src/feishu-channel.test.d.ts +1 -0
- package/dist/src/feishu-channel.test.js +166 -0
- package/dist/src/feishu-client.d.ts +27 -0
- package/dist/src/feishu-client.js +141 -0
- package/dist/src/feishu-client.test.d.ts +1 -0
- package/dist/src/feishu-client.test.js +271 -0
- package/dist/src/feishu-e2e.test.d.ts +1 -0
- package/dist/src/feishu-e2e.test.js +69 -0
- package/dist/src/feishu-types.d.ts +53 -0
- package/dist/src/feishu-types.js +1 -0
- package/dist/src/feishu-types.test.d.ts +1 -0
- package/dist/src/feishu-types.test.js +108 -0
- package/dist/src/inbound-feishu.test.d.ts +1 -0
- package/dist/src/inbound-feishu.test.js +70 -0
- package/dist/src/inbound.d.ts +2 -0
- package/dist/src/inbound.js +14 -10
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/plugin-registration.test.d.ts +1 -0
- package/dist/src/plugin-registration.test.js +42 -0
- package/dist/src/plugin.d.ts +2 -1
- package/dist/src/plugin.js +20 -39
- package/index.ts +25 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/src/agent-tools-feishu.test.ts +68 -0
- package/src/agent-tools.ts +45 -0
- package/src/config-schema.test.ts +63 -0
- package/src/feishu-channel.test.ts +191 -0
- package/src/feishu-channel.ts +253 -0
- package/src/feishu-client.test.ts +303 -0
- package/src/feishu-client.ts +178 -0
- package/src/feishu-e2e.test.ts +90 -0
- package/src/feishu-types.test.ts +125 -0
- package/src/feishu-types.ts +51 -0
- package/src/inbound-feishu.test.ts +91 -0
- package/src/inbound.ts +16 -11
- package/src/index.ts +1 -0
- package/src/plugin-registration.test.ts +60 -0
- package/src/plugin.ts +24 -44
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface FeishuChannelConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
appSecret: string;
|
|
4
|
+
webhookPort: number;
|
|
5
|
+
webhookPath: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FeishuWebhookEvent {
|
|
9
|
+
schema: string;
|
|
10
|
+
header: {
|
|
11
|
+
eventId: string;
|
|
12
|
+
eventType: string;
|
|
13
|
+
createTime: string;
|
|
14
|
+
token: string;
|
|
15
|
+
appId: string;
|
|
16
|
+
tenantKey: string;
|
|
17
|
+
};
|
|
18
|
+
event: {
|
|
19
|
+
sender: { senderId: { openId: string } };
|
|
20
|
+
message: {
|
|
21
|
+
messageId: string;
|
|
22
|
+
rootId?: string;
|
|
23
|
+
parentId?: string;
|
|
24
|
+
msgType: "text" | "post" | "image" | "interactive";
|
|
25
|
+
createTime: string;
|
|
26
|
+
chatId: string;
|
|
27
|
+
chatType: "p2p" | "group";
|
|
28
|
+
content: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FeishuSendMessageRequest {
|
|
34
|
+
receiveId: string;
|
|
35
|
+
receiveIdType: "openId" | "chatId";
|
|
36
|
+
msgType: "text";
|
|
37
|
+
content: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FeishuSendMessageResponse {
|
|
41
|
+
code: number;
|
|
42
|
+
msg: string;
|
|
43
|
+
data: { messageId: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FeishuTokenResponse {
|
|
47
|
+
code: number;
|
|
48
|
+
msg: string;
|
|
49
|
+
tenant_access_token: string;
|
|
50
|
+
expire: number;
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { handleP2PInbound, type InboundHandlerDeps } from "./inbound.js";
|
|
3
|
+
import type { P2PMessage } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const makeMsg = (overrides: Partial<P2PMessage> = {}): P2PMessage => ({
|
|
6
|
+
id: "msg-1",
|
|
7
|
+
type: "direct",
|
|
8
|
+
from: "peer-abc",
|
|
9
|
+
payload: "hello from p2p",
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("handleP2PInbound with Feishu forwarding", () => {
|
|
15
|
+
let logger: Record<string, ReturnType<typeof vi.fn>>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
logger = {
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
debug: vi.fn(),
|
|
21
|
+
warn: vi.fn(),
|
|
22
|
+
error: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should call feishuClient.sendMessage when feishuClient is provided", async () => {
|
|
27
|
+
const sendMessage = vi.fn().mockResolvedValue({ success: true });
|
|
28
|
+
const deps: InboundHandlerDeps = {
|
|
29
|
+
logger,
|
|
30
|
+
feishuClient: { sendMessage } as any,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
handleP2PInbound(makeMsg(), deps);
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
|
|
36
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(sendMessage).toHaveBeenCalledWith(expect.any(String), "hello from p2p");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should not call feishuClient when feishuClient is not provided", () => {
|
|
41
|
+
const sendMessage = vi.fn();
|
|
42
|
+
const deps: InboundHandlerDeps = { logger };
|
|
43
|
+
|
|
44
|
+
handleP2PInbound(makeMsg(), deps);
|
|
45
|
+
|
|
46
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should log warning when feishu forwarding fails", async () => {
|
|
50
|
+
const sendMessage = vi.fn().mockRejectedValue(new Error("network error"));
|
|
51
|
+
const deps: InboundHandlerDeps = {
|
|
52
|
+
logger,
|
|
53
|
+
feishuClient: { sendMessage } as any,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
handleP2PInbound(makeMsg(), deps);
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
|
|
59
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
60
|
+
const warnCalls = logger.warn.mock.calls;
|
|
61
|
+
expect(warnCalls.some((c: string[]) => c.some((s: string) => s.includes("Failed to forward")))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should forward broadcast messages to Feishu", async () => {
|
|
65
|
+
const sendMessage = vi.fn().mockResolvedValue({ success: true });
|
|
66
|
+
const deps: InboundHandlerDeps = {
|
|
67
|
+
logger,
|
|
68
|
+
feishuClient: { sendMessage } as any,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
handleP2PInbound(
|
|
72
|
+
makeMsg({ type: "broadcast", topic: "general", payload: "broadcast msg" }),
|
|
73
|
+
deps,
|
|
74
|
+
);
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
|
|
77
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(sendMessage).toHaveBeenCalledWith(expect.any(String), "broadcast msg");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should not break existing logging when feishuClient fails", async () => {
|
|
82
|
+
const sendMessage = vi.fn().mockRejectedValue(new Error("fail"));
|
|
83
|
+
const deps: InboundHandlerDeps = {
|
|
84
|
+
logger,
|
|
85
|
+
feishuClient: { sendMessage } as any,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(() => handleP2PInbound(makeMsg(), deps)).not.toThrow();
|
|
89
|
+
expect(logger.info).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/inbound.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { P2PMessage } from "./types.js";
|
|
2
|
+
import type { FeishuApiClient } from "./feishu-client.js";
|
|
2
3
|
|
|
3
4
|
export type InboundHandlerDeps = {
|
|
4
5
|
logger?: {
|
|
@@ -8,24 +9,28 @@ export type InboundHandlerDeps = {
|
|
|
8
9
|
error?: (msg: string) => void;
|
|
9
10
|
};
|
|
10
11
|
sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
|
|
12
|
+
feishuClient?: FeishuApiClient;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
|
|
14
|
-
const { logger, sendToChannel } = deps;
|
|
16
|
+
const { logger, sendToChannel, feishuClient } = deps;
|
|
17
|
+
|
|
15
18
|
if (msg.type === "broadcast") {
|
|
16
19
|
logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
|
|
17
|
-
|
|
20
|
+
} else {
|
|
21
|
+
logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}: ${msg.payload}`);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (msg.type !== "broadcast" && sendToChannel && msg.payload) {
|
|
25
|
+
const text = `[来自 ${msg.from}]\n${msg.payload}`;
|
|
26
|
+
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
27
|
+
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
28
|
+
});
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (feishuClient && msg.payload) {
|
|
32
|
+
feishuClient.sendMessage(msg.from, msg.payload).catch(() => {
|
|
33
|
+
logger?.warn?.("[libp2p-mesh] Failed to forward P2P message to Feishu");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
31
36
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLibp2pMeshConfigSchema } from "../index.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { registerLibp2pMesh } from "./plugin.js";
|
|
3
|
+
|
|
4
|
+
describe("registerLibp2pMesh Feishu registration", () => {
|
|
5
|
+
let mockApi: {
|
|
6
|
+
registerService: ReturnType<typeof vi.fn>;
|
|
7
|
+
registerChannel: ReturnType<typeof vi.fn>;
|
|
8
|
+
registerTool: ReturnType<typeof vi.fn>;
|
|
9
|
+
registerHook: ReturnType<typeof vi.fn>;
|
|
10
|
+
logger: {
|
|
11
|
+
info?: (msg: string) => void;
|
|
12
|
+
warn?: (msg: string) => void;
|
|
13
|
+
error?: (msg: string) => void;
|
|
14
|
+
debug?: (msg: string) => void;
|
|
15
|
+
};
|
|
16
|
+
pluginConfig?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockApi = {
|
|
21
|
+
registerService: vi.fn(),
|
|
22
|
+
registerChannel: vi.fn(),
|
|
23
|
+
registerTool: vi.fn(),
|
|
24
|
+
registerHook: vi.fn(),
|
|
25
|
+
logger: {
|
|
26
|
+
info: vi.fn(),
|
|
27
|
+
warn: vi.fn(),
|
|
28
|
+
error: vi.fn(),
|
|
29
|
+
debug: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
pluginConfig: undefined,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should always register P2P mesh service and channel", () => {
|
|
36
|
+
registerLibp2pMesh(mockApi as any);
|
|
37
|
+
expect(mockApi.registerService).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should register Feishu channel when feishu config is provided", () => {
|
|
42
|
+
registerLibp2pMesh(mockApi as any, {
|
|
43
|
+
appId: "cli-123",
|
|
44
|
+
appSecret: "secret-456",
|
|
45
|
+
webhookPort: 9222,
|
|
46
|
+
webhookPath: "/webhook/feishu",
|
|
47
|
+
});
|
|
48
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should not register Feishu channel when no feishu config", () => {
|
|
52
|
+
registerLibp2pMesh(mockApi as any);
|
|
53
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should register 3 P2P agent tools", () => {
|
|
57
|
+
registerLibp2pMesh(mockApi as any);
|
|
58
|
+
expect(mockApi.registerTool).toHaveBeenCalledTimes(3);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type { OpenClawPluginApi
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { createLibp2pMeshChannel } from "./channel.js";
|
|
3
3
|
import { handleP2PInbound, type InboundHandlerDeps } from "./inbound.js";
|
|
4
4
|
import { createMeshNetwork } from "./mesh.js";
|
|
5
|
-
import { buildP2PTools } from "./agent-tools.js";
|
|
5
|
+
import { buildP2PTools, buildFeishuTools } from "./agent-tools.js";
|
|
6
6
|
import { sendViaMesh } from "./send.js";
|
|
7
7
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
8
|
+
import type { FeishuChannelConfig } from "./feishu-types.js";
|
|
9
|
+
import { FeishuApiClient } from "./feishu-client.js";
|
|
10
|
+
import { createFeishuChannel } from "./feishu-channel.js";
|
|
8
11
|
|
|
9
|
-
export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
12
|
+
export function registerLibp2pMesh(api: OpenClawPluginApi, feishuConfig?: FeishuChannelConfig) {
|
|
10
13
|
const mesh = createMeshNetwork({
|
|
11
14
|
config: api.pluginConfig as { listenAddrs?: string[]; discovery?: "mdns" | "bootstrap" | "dht"; bootstrapList?: string[]; meshTopic?: string; enableAgentSync?: boolean; enableWebSocket?: boolean } | undefined,
|
|
12
15
|
logger: api.logger,
|
|
@@ -14,15 +17,16 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
14
17
|
|
|
15
18
|
const channel = createLibp2pMeshChannel(mesh);
|
|
16
19
|
|
|
20
|
+
const feishuClient = feishuConfig?.appId
|
|
21
|
+
? new FeishuApiClient(feishuConfig, { logger: api.logger })
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
17
24
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
18
25
|
api.registerService({
|
|
19
26
|
id: "libp2p-mesh",
|
|
20
27
|
start: async () => {
|
|
21
|
-
api.logger.info?.("[libp2p-mesh] >>> Service start called");
|
|
22
28
|
await mesh.start();
|
|
23
|
-
api.logger.info?.(`[libp2p-mesh] >>> mesh.start() done, Peer ID: ${mesh.getLocalPeerId()}`);
|
|
24
29
|
mesh.onMessage((msg) => {
|
|
25
|
-
api.logger.info?.("[libp2p-mesh] >>> onMessage handler registered");
|
|
26
30
|
const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, target, text) => {
|
|
27
31
|
try {
|
|
28
32
|
await sendViaMesh(mesh, target, text);
|
|
@@ -30,7 +34,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
30
34
|
api.logger.error?.(`[libp2p-mesh] sendToChannel error: ${err}`);
|
|
31
35
|
}
|
|
32
36
|
};
|
|
33
|
-
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
37
|
+
handleP2PInbound(msg, { logger: api.logger, sendToChannel, feishuClient });
|
|
34
38
|
});
|
|
35
39
|
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
|
|
36
40
|
},
|
|
@@ -51,46 +55,22 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
51
55
|
api.registerTool(tool as never);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name: "p2p",
|
|
59
|
-
description: "P2P mesh network operations (list peers, send messages, broadcast).",
|
|
60
|
-
agentPromptGuidance: [
|
|
61
|
-
"When the user asks to list, show, or discover P2P mesh peers or connected agents, use the p2p_list_peers tool. Do NOT use the built-in `nodes` tool for this — `nodes` lists OpenClaw paired nodes, not P2P mesh peers.",
|
|
62
|
-
"When the user asks to send a direct message to another peer/agent, use p2p_send_message.",
|
|
63
|
-
"When the user asks to broadcast a message to all peers on a topic, use p2p_broadcast.",
|
|
64
|
-
],
|
|
65
|
-
handler: (_ctx: PluginCommandContext): PluginCommandResult => {
|
|
66
|
-
return { text: "P2P mesh tools are available. Use p2p_list_peers, p2p_send_message, or p2p_broadcast." };
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// 5. Register CLI commands for operator access
|
|
71
|
-
api.registerCli(
|
|
72
|
-
(ctx) => {
|
|
73
|
-
ctx.program
|
|
74
|
-
.command("status")
|
|
75
|
-
.description("Show local peer ID and connected peers")
|
|
76
|
-
.action(async () => {
|
|
77
|
-
const peers = mesh.getConnectedPeers();
|
|
78
|
-
ctx.logger.info?.(`Local Peer ID: ${mesh.getLocalPeerId()}\nConnected peers (${peers.length}): ${peers.join(", ") || "none"}`);
|
|
79
|
-
});
|
|
80
|
-
ctx.program
|
|
81
|
-
.command("peers")
|
|
82
|
-
.description("Alias for mesh status — list connected peers")
|
|
83
|
-
.action(async () => {
|
|
84
|
-
const peers = mesh.getConnectedPeers();
|
|
85
|
-
ctx.logger.info?.(`Connected peers (${peers.length}): ${peers.join(", ") || "none"}`);
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
{ parentPath: ["p2p"] },
|
|
89
|
-
);
|
|
58
|
+
const feishuTools = buildFeishuTools(feishuClient ?? null);
|
|
59
|
+
for (const tool of feishuTools) {
|
|
60
|
+
api.registerTool(tool as never);
|
|
61
|
+
}
|
|
90
62
|
|
|
91
|
-
//
|
|
63
|
+
// 4. Register Hook (log received messages for observability)
|
|
92
64
|
api.registerHook("message:received", async (event) => {
|
|
93
65
|
const ctx = event.context as { channelId?: string } | undefined;
|
|
94
66
|
api.logger.debug?.(`[libp2p-mesh] message received on channel ${ctx?.channelId ?? "unknown"}`);
|
|
95
67
|
}, { name: "libp2p-mesh-message-received" });
|
|
68
|
+
|
|
69
|
+
// 5. Conditionally register Feishu channel
|
|
70
|
+
if (feishuClient && feishuConfig) {
|
|
71
|
+
const feishuChannel = createFeishuChannel(feishuConfig, { logger: api.logger }, feishuClient);
|
|
72
|
+
api.registerChannel({
|
|
73
|
+
plugin: feishuChannel as ChannelPlugin,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
96
76
|
}
|