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,53 @@
|
|
|
1
|
+
export interface FeishuChannelConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
appSecret: string;
|
|
4
|
+
webhookPort: number;
|
|
5
|
+
webhookPath: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FeishuWebhookEvent {
|
|
8
|
+
schema: string;
|
|
9
|
+
header: {
|
|
10
|
+
eventId: string;
|
|
11
|
+
eventType: string;
|
|
12
|
+
createTime: string;
|
|
13
|
+
token: string;
|
|
14
|
+
appId: string;
|
|
15
|
+
tenantKey: string;
|
|
16
|
+
};
|
|
17
|
+
event: {
|
|
18
|
+
sender: {
|
|
19
|
+
senderId: {
|
|
20
|
+
openId: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
message: {
|
|
24
|
+
messageId: string;
|
|
25
|
+
rootId?: string;
|
|
26
|
+
parentId?: string;
|
|
27
|
+
msgType: "text" | "post" | "image" | "interactive";
|
|
28
|
+
createTime: string;
|
|
29
|
+
chatId: string;
|
|
30
|
+
chatType: "p2p" | "group";
|
|
31
|
+
content: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface FeishuSendMessageRequest {
|
|
36
|
+
receiveId: string;
|
|
37
|
+
receiveIdType: "openId" | "chatId";
|
|
38
|
+
msgType: "text";
|
|
39
|
+
content: string;
|
|
40
|
+
}
|
|
41
|
+
export interface FeishuSendMessageResponse {
|
|
42
|
+
code: number;
|
|
43
|
+
msg: string;
|
|
44
|
+
data: {
|
|
45
|
+
messageId: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface FeishuTokenResponse {
|
|
49
|
+
code: number;
|
|
50
|
+
msg: string;
|
|
51
|
+
tenant_access_token: string;
|
|
52
|
+
expire: number;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
describe("FeishuChannelConfig", () => {
|
|
3
|
+
it("should have correct default values", () => {
|
|
4
|
+
const config = {
|
|
5
|
+
appId: "test-app-id",
|
|
6
|
+
appSecret: "test-secret",
|
|
7
|
+
webhookPort: 9222,
|
|
8
|
+
webhookPath: "/webhook/feishu",
|
|
9
|
+
};
|
|
10
|
+
expect(config.webhookPort).toBe(9222);
|
|
11
|
+
expect(config.webhookPath).toBe("/webhook/feishu");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe("FeishuWebhookEvent", () => {
|
|
15
|
+
const sampleEvent = {
|
|
16
|
+
schema: "2.0",
|
|
17
|
+
header: {
|
|
18
|
+
eventId: "event-123",
|
|
19
|
+
eventType: "im.message.receive_v1",
|
|
20
|
+
createTime: "1234567890000",
|
|
21
|
+
token: "verification-token",
|
|
22
|
+
appId: "app-123",
|
|
23
|
+
tenantKey: "tenant-123",
|
|
24
|
+
},
|
|
25
|
+
event: {
|
|
26
|
+
sender: { senderId: { openId: "ou-abc123" } },
|
|
27
|
+
message: {
|
|
28
|
+
messageId: "msg-123",
|
|
29
|
+
msgType: "text",
|
|
30
|
+
createTime: "1234567890000",
|
|
31
|
+
chatId: "chat-123",
|
|
32
|
+
chatType: "p2p",
|
|
33
|
+
content: '{"text":"hello"}',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
it("should parse header fields correctly", () => {
|
|
38
|
+
expect(sampleEvent.header.eventId).toBe("event-123");
|
|
39
|
+
expect(sampleEvent.header.eventType).toBe("im.message.receive_v1");
|
|
40
|
+
expect(sampleEvent.header.appId).toBe("app-123");
|
|
41
|
+
});
|
|
42
|
+
it("should extract openId from sender", () => {
|
|
43
|
+
expect(sampleEvent.event.sender.senderId.openId).toBe("ou-abc123");
|
|
44
|
+
});
|
|
45
|
+
it("should parse message fields correctly", () => {
|
|
46
|
+
expect(sampleEvent.event.message.messageId).toBe("msg-123");
|
|
47
|
+
expect(sampleEvent.event.message.msgType).toBe("text");
|
|
48
|
+
expect(sampleEvent.event.message.chatType).toBe("p2p");
|
|
49
|
+
expect(sampleEvent.event.message.content).toBe('{"text":"hello"}');
|
|
50
|
+
});
|
|
51
|
+
it("should support optional message fields", () => {
|
|
52
|
+
const withOptional = {
|
|
53
|
+
...sampleEvent,
|
|
54
|
+
event: {
|
|
55
|
+
...sampleEvent.event,
|
|
56
|
+
message: {
|
|
57
|
+
...sampleEvent.event.message,
|
|
58
|
+
rootId: "msg-root",
|
|
59
|
+
parentId: "msg-parent",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
expect(withOptional.event.message.rootId).toBe("msg-root");
|
|
64
|
+
expect(withOptional.event.message.parentId).toBe("msg-parent");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("FeishuSendMessageRequest", () => {
|
|
68
|
+
it("should require receiveId, receiveIdType, msgType, content", () => {
|
|
69
|
+
const req = {
|
|
70
|
+
receiveId: "ou-abc123",
|
|
71
|
+
receiveIdType: "openId",
|
|
72
|
+
msgType: "text",
|
|
73
|
+
content: '{"text":"hello"}',
|
|
74
|
+
};
|
|
75
|
+
expect(req.receiveId).toBe("ou-abc123");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("FeishuSendMessageResponse", () => {
|
|
79
|
+
it("should parse success response", () => {
|
|
80
|
+
const resp = {
|
|
81
|
+
code: 0,
|
|
82
|
+
msg: "success",
|
|
83
|
+
data: { messageId: "msg-sent-123" },
|
|
84
|
+
};
|
|
85
|
+
expect(resp.code).toBe(0);
|
|
86
|
+
expect(resp.data.messageId).toBe("msg-sent-123");
|
|
87
|
+
});
|
|
88
|
+
it("should parse error response", () => {
|
|
89
|
+
const resp = {
|
|
90
|
+
code: 99991663,
|
|
91
|
+
msg: "token invalid",
|
|
92
|
+
data: { messageId: "" },
|
|
93
|
+
};
|
|
94
|
+
expect(resp.code).toBe(99991663);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("FeishuTokenResponse", () => {
|
|
98
|
+
it("should parse token response", () => {
|
|
99
|
+
const resp = {
|
|
100
|
+
code: 0,
|
|
101
|
+
msg: "ok",
|
|
102
|
+
tenant_access_token: "t-abc123",
|
|
103
|
+
expire: 7200,
|
|
104
|
+
};
|
|
105
|
+
expect(resp.tenant_access_token).toBe("t-abc123");
|
|
106
|
+
expect(resp.expire).toBe(7200);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { handleP2PInbound } from "./inbound.js";
|
|
3
|
+
const makeMsg = (overrides = {}) => ({
|
|
4
|
+
id: "msg-1",
|
|
5
|
+
type: "direct",
|
|
6
|
+
from: "peer-abc",
|
|
7
|
+
payload: "hello from p2p",
|
|
8
|
+
timestamp: Date.now(),
|
|
9
|
+
...overrides,
|
|
10
|
+
});
|
|
11
|
+
describe("handleP2PInbound with Feishu forwarding", () => {
|
|
12
|
+
let logger;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
logger = {
|
|
15
|
+
info: vi.fn(),
|
|
16
|
+
debug: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
it("should call feishuClient.sendMessage when feishuClient is provided", async () => {
|
|
22
|
+
const sendMessage = vi.fn().mockResolvedValue({ success: true });
|
|
23
|
+
const deps = {
|
|
24
|
+
logger,
|
|
25
|
+
feishuClient: { sendMessage },
|
|
26
|
+
};
|
|
27
|
+
handleP2PInbound(makeMsg(), deps);
|
|
28
|
+
await Promise.resolve();
|
|
29
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(sendMessage).toHaveBeenCalledWith(expect.any(String), "hello from p2p");
|
|
31
|
+
});
|
|
32
|
+
it("should not call feishuClient when feishuClient is not provided", () => {
|
|
33
|
+
const sendMessage = vi.fn();
|
|
34
|
+
const deps = { logger };
|
|
35
|
+
handleP2PInbound(makeMsg(), deps);
|
|
36
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
it("should log warning when feishu forwarding fails", async () => {
|
|
39
|
+
const sendMessage = vi.fn().mockRejectedValue(new Error("network error"));
|
|
40
|
+
const deps = {
|
|
41
|
+
logger,
|
|
42
|
+
feishuClient: { sendMessage },
|
|
43
|
+
};
|
|
44
|
+
handleP2PInbound(makeMsg(), deps);
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
47
|
+
const warnCalls = logger.warn.mock.calls;
|
|
48
|
+
expect(warnCalls.some((c) => c.some((s) => s.includes("Failed to forward")))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("should forward broadcast messages to Feishu", async () => {
|
|
51
|
+
const sendMessage = vi.fn().mockResolvedValue({ success: true });
|
|
52
|
+
const deps = {
|
|
53
|
+
logger,
|
|
54
|
+
feishuClient: { sendMessage },
|
|
55
|
+
};
|
|
56
|
+
handleP2PInbound(makeMsg({ type: "broadcast", topic: "general", payload: "broadcast msg" }), deps);
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
expect(sendMessage).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(sendMessage).toHaveBeenCalledWith(expect.any(String), "broadcast msg");
|
|
60
|
+
});
|
|
61
|
+
it("should not break existing logging when feishuClient fails", async () => {
|
|
62
|
+
const sendMessage = vi.fn().mockRejectedValue(new Error("fail"));
|
|
63
|
+
const deps = {
|
|
64
|
+
logger,
|
|
65
|
+
feishuClient: { sendMessage },
|
|
66
|
+
};
|
|
67
|
+
expect(() => handleP2PInbound(makeMsg(), deps)).not.toThrow();
|
|
68
|
+
expect(logger.info).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
package/dist/src/inbound.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { P2PMessage } from "./types.js";
|
|
2
|
+
import type { FeishuApiClient } from "./feishu-client.js";
|
|
2
3
|
export type InboundHandlerDeps = {
|
|
3
4
|
logger?: {
|
|
4
5
|
info?: (msg: string) => void;
|
|
@@ -7,5 +8,6 @@ export type InboundHandlerDeps = {
|
|
|
7
8
|
error?: (msg: string) => void;
|
|
8
9
|
};
|
|
9
10
|
sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
|
|
11
|
+
feishuClient?: FeishuApiClient;
|
|
10
12
|
};
|
|
11
13
|
export declare function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void;
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
export function handleP2PInbound(msg, deps) {
|
|
2
|
-
const { logger, sendToChannel } = deps;
|
|
2
|
+
const { logger, sendToChannel, feishuClient } = deps;
|
|
3
3
|
if (msg.type === "broadcast") {
|
|
4
4
|
logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
|
|
5
|
-
return;
|
|
6
5
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
else {
|
|
7
|
+
logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}: ${msg.payload}`);
|
|
8
|
+
}
|
|
9
|
+
if (msg.type !== "broadcast" && sendToChannel && msg.payload) {
|
|
10
|
+
const text = `[来自 ${msg.from}]\n${msg.payload}`;
|
|
11
|
+
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
12
|
+
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
if (feishuClient && msg.payload) {
|
|
16
|
+
feishuClient.sendMessage(msg.from, msg.payload).catch(() => {
|
|
17
|
+
logger?.warn?.("[libp2p-mesh] Failed to forward P2P message to Feishu");
|
|
18
|
+
});
|
|
11
19
|
}
|
|
12
|
-
const text = `[来自 ${msg.from}]\n${msg.payload}`;
|
|
13
|
-
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
14
|
-
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
15
|
-
});
|
|
16
20
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLibp2pMeshConfigSchema } from "../index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLibp2pMeshConfigSchema } from "../index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { registerLibp2pMesh } from "./plugin.js";
|
|
3
|
+
describe("registerLibp2pMesh Feishu registration", () => {
|
|
4
|
+
let mockApi;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
mockApi = {
|
|
7
|
+
registerService: vi.fn(),
|
|
8
|
+
registerChannel: vi.fn(),
|
|
9
|
+
registerTool: vi.fn(),
|
|
10
|
+
registerHook: vi.fn(),
|
|
11
|
+
logger: {
|
|
12
|
+
info: vi.fn(),
|
|
13
|
+
warn: vi.fn(),
|
|
14
|
+
error: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
pluginConfig: undefined,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
it("should always register P2P mesh service and channel", () => {
|
|
21
|
+
registerLibp2pMesh(mockApi);
|
|
22
|
+
expect(mockApi.registerService).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
it("should register Feishu channel when feishu config is provided", () => {
|
|
26
|
+
registerLibp2pMesh(mockApi, {
|
|
27
|
+
appId: "cli-123",
|
|
28
|
+
appSecret: "secret-456",
|
|
29
|
+
webhookPort: 9222,
|
|
30
|
+
webhookPath: "/webhook/feishu",
|
|
31
|
+
});
|
|
32
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(2);
|
|
33
|
+
});
|
|
34
|
+
it("should not register Feishu channel when no feishu config", () => {
|
|
35
|
+
registerLibp2pMesh(mockApi);
|
|
36
|
+
expect(mockApi.registerChannel).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
it("should register 3 P2P agent tools", () => {
|
|
39
|
+
registerLibp2pMesh(mockApi);
|
|
40
|
+
expect(mockApi.registerTool).toHaveBeenCalledTimes(3);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
-
|
|
2
|
+
import type { FeishuChannelConfig } from "./feishu-types.js";
|
|
3
|
+
export declare function registerLibp2pMesh(api: OpenClawPluginApi, feishuConfig?: FeishuChannelConfig): void;
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { createLibp2pMeshChannel } from "./channel.js";
|
|
2
2
|
import { handleP2PInbound } from "./inbound.js";
|
|
3
3
|
import { createMeshNetwork } from "./mesh.js";
|
|
4
|
-
import { buildP2PTools } from "./agent-tools.js";
|
|
4
|
+
import { buildP2PTools, buildFeishuTools } from "./agent-tools.js";
|
|
5
5
|
import { sendViaMesh } from "./send.js";
|
|
6
|
-
|
|
6
|
+
import { FeishuApiClient } from "./feishu-client.js";
|
|
7
|
+
import { createFeishuChannel } from "./feishu-channel.js";
|
|
8
|
+
export function registerLibp2pMesh(api, feishuConfig) {
|
|
7
9
|
const mesh = createMeshNetwork({
|
|
8
10
|
config: api.pluginConfig,
|
|
9
11
|
logger: api.logger,
|
|
10
12
|
});
|
|
11
13
|
const channel = createLibp2pMeshChannel(mesh);
|
|
14
|
+
const feishuClient = feishuConfig?.appId
|
|
15
|
+
? new FeishuApiClient(feishuConfig, { logger: api.logger })
|
|
16
|
+
: undefined;
|
|
12
17
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
13
18
|
api.registerService({
|
|
14
19
|
id: "libp2p-mesh",
|
|
15
20
|
start: async () => {
|
|
16
|
-
api.logger.info?.("[libp2p-mesh] >>> Service start called");
|
|
17
21
|
await mesh.start();
|
|
18
|
-
api.logger.info?.(`[libp2p-mesh] >>> mesh.start() done, Peer ID: ${mesh.getLocalPeerId()}`);
|
|
19
22
|
mesh.onMessage((msg) => {
|
|
20
|
-
api.logger.info?.("[libp2p-mesh] >>> onMessage handler registered");
|
|
21
23
|
const sendToChannel = async (_channelId, target, text) => {
|
|
22
24
|
try {
|
|
23
25
|
await sendViaMesh(mesh, target, text);
|
|
@@ -26,7 +28,7 @@ export function registerLibp2pMesh(api) {
|
|
|
26
28
|
api.logger.error?.(`[libp2p-mesh] sendToChannel error: ${err}`);
|
|
27
29
|
}
|
|
28
30
|
};
|
|
29
|
-
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
31
|
+
handleP2PInbound(msg, { logger: api.logger, sendToChannel, feishuClient });
|
|
30
32
|
});
|
|
31
33
|
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
|
|
32
34
|
},
|
|
@@ -44,41 +46,20 @@ export function registerLibp2pMesh(api) {
|
|
|
44
46
|
for (const tool of tools) {
|
|
45
47
|
api.registerTool(tool);
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
description: "P2P mesh network operations (list peers, send messages, broadcast).",
|
|
53
|
-
agentPromptGuidance: [
|
|
54
|
-
"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.",
|
|
55
|
-
"When the user asks to send a direct message to another peer/agent, use p2p_send_message.",
|
|
56
|
-
"When the user asks to broadcast a message to all peers on a topic, use p2p_broadcast.",
|
|
57
|
-
],
|
|
58
|
-
handler: (_ctx) => {
|
|
59
|
-
return { text: "P2P mesh tools are available. Use p2p_list_peers, p2p_send_message, or p2p_broadcast." };
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
// 5. Register CLI commands for operator access
|
|
63
|
-
api.registerCli((ctx) => {
|
|
64
|
-
ctx.program
|
|
65
|
-
.command("status")
|
|
66
|
-
.description("Show local peer ID and connected peers")
|
|
67
|
-
.action(async () => {
|
|
68
|
-
const peers = mesh.getConnectedPeers();
|
|
69
|
-
ctx.logger.info?.(`Local Peer ID: ${mesh.getLocalPeerId()}\nConnected peers (${peers.length}): ${peers.join(", ") || "none"}`);
|
|
70
|
-
});
|
|
71
|
-
ctx.program
|
|
72
|
-
.command("peers")
|
|
73
|
-
.description("Alias for mesh status — list connected peers")
|
|
74
|
-
.action(async () => {
|
|
75
|
-
const peers = mesh.getConnectedPeers();
|
|
76
|
-
ctx.logger.info?.(`Connected peers (${peers.length}): ${peers.join(", ") || "none"}`);
|
|
77
|
-
});
|
|
78
|
-
}, { parentPath: ["p2p"] });
|
|
79
|
-
// 6. Register Hook (log received messages for observability)
|
|
49
|
+
const feishuTools = buildFeishuTools(feishuClient ?? null);
|
|
50
|
+
for (const tool of feishuTools) {
|
|
51
|
+
api.registerTool(tool);
|
|
52
|
+
}
|
|
53
|
+
// 4. Register Hook (log received messages for observability)
|
|
80
54
|
api.registerHook("message:received", async (event) => {
|
|
81
55
|
const ctx = event.context;
|
|
82
56
|
api.logger.debug?.(`[libp2p-mesh] message received on channel ${ctx?.channelId ?? "unknown"}`);
|
|
83
57
|
}, { name: "libp2p-mesh-message-received" });
|
|
58
|
+
// 5. Conditionally register Feishu channel
|
|
59
|
+
if (feishuClient && feishuConfig) {
|
|
60
|
+
const feishuChannel = createFeishuChannel(feishuConfig, { logger: api.logger }, feishuClient);
|
|
61
|
+
api.registerChannel({
|
|
62
|
+
plugin: feishuChannel,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
84
65
|
}
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
|
2
2
|
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { registerLibp2pMesh } from "./src/plugin.js";
|
|
4
4
|
|
|
5
|
-
function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
|
|
5
|
+
export function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
|
|
6
6
|
return {
|
|
7
7
|
safeParse(value: unknown) {
|
|
8
8
|
if (value === undefined) {
|
|
@@ -51,6 +51,30 @@ function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
|
|
|
51
51
|
type: "boolean",
|
|
52
52
|
default: true,
|
|
53
53
|
},
|
|
54
|
+
feishu: {
|
|
55
|
+
type: "object",
|
|
56
|
+
description: "Feishu integration configuration",
|
|
57
|
+
properties: {
|
|
58
|
+
appId: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Feishu app App ID",
|
|
61
|
+
},
|
|
62
|
+
appSecret: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Feishu app App Secret",
|
|
65
|
+
},
|
|
66
|
+
webhookPort: {
|
|
67
|
+
type: "number",
|
|
68
|
+
default: 9222,
|
|
69
|
+
description: "Port for Feishu webhook callback (default: 9222)",
|
|
70
|
+
},
|
|
71
|
+
webhookPath: {
|
|
72
|
+
type: "string",
|
|
73
|
+
default: "/webhook/feishu",
|
|
74
|
+
description: "Path for Feishu webhook callback",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
54
78
|
},
|
|
55
79
|
},
|
|
56
80
|
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2p-mesh",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.13",
|
|
4
4
|
"description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"openclaw": "^2026.5.7",
|
|
37
37
|
"tsx": "^4.0.0",
|
|
38
|
-
"typescript": "^5.9.3"
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^4.1.8"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"openclaw": ">=2026.3.24"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { buildFeishuTools } from "./agent-tools.js";
|
|
3
|
+
|
|
4
|
+
describe("buildFeishuTools", () => {
|
|
5
|
+
it("should return empty array when feishuClient is null", () => {
|
|
6
|
+
const tools = buildFeishuTools(null);
|
|
7
|
+
expect(tools).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should return array with one tool when feishuClient is provided", () => {
|
|
11
|
+
const mockClient = {
|
|
12
|
+
sendMessage: vi.fn(),
|
|
13
|
+
} as any;
|
|
14
|
+
const tools = buildFeishuTools(mockClient);
|
|
15
|
+
expect(tools).toHaveLength(1);
|
|
16
|
+
expect(tools[0].name).toBe("feishu_send_message");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("feishu_send_message tool", () => {
|
|
20
|
+
let tools: ReturnType<typeof buildFeishuTools>;
|
|
21
|
+
let mockClient: any;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockClient = {
|
|
25
|
+
sendMessage: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
tools = buildFeishuTools(mockClient);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should require openId and message parameters", () => {
|
|
31
|
+
const tool = tools[0];
|
|
32
|
+
expect(tool.parameters.required).toEqual(["openId", "message"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return success result on successful send", async () => {
|
|
36
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
37
|
+
success: true,
|
|
38
|
+
messageId: "msg-789",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result = await tools[0].execute("call-1", {
|
|
42
|
+
openId: "ou-test",
|
|
43
|
+
message: "hello",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.isError).toBeUndefined();
|
|
47
|
+
expect(result.details.sent).toBe(true);
|
|
48
|
+
expect(result.details.openId).toBe("ou-test");
|
|
49
|
+
expect(mockClient.sendMessage).toHaveBeenCalledWith("ou-test", "hello");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return error result on failed send", async () => {
|
|
53
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
54
|
+
success: false,
|
|
55
|
+
error: "rate limited",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await tools[0].execute("call-1", {
|
|
59
|
+
openId: "ou-test",
|
|
60
|
+
message: "hello",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.isError).toBe(true);
|
|
64
|
+
expect(result.details.sent).toBe(false);
|
|
65
|
+
expect(result.details.error).toBe("rate limited");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/agent-tools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MeshNetwork } from "./types.js";
|
|
2
|
+
import type { FeishuApiClient } from "./feishu-client.js";
|
|
2
3
|
|
|
3
4
|
export function buildP2PTools(mesh: MeshNetwork) {
|
|
4
5
|
return [
|
|
@@ -114,3 +115,47 @@ export function buildP2PTools(mesh: MeshNetwork) {
|
|
|
114
115
|
},
|
|
115
116
|
];
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
export function buildFeishuTools(feishuClient: FeishuApiClient | null) {
|
|
120
|
+
if (!feishuClient) return [];
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
name: "feishu_send_message",
|
|
124
|
+
label: "Feishu Send Message",
|
|
125
|
+
description: "Send a text message to a Feishu user by their openId. Use this to notify users through Feishu.",
|
|
126
|
+
parameters: {
|
|
127
|
+
type: "object" as const,
|
|
128
|
+
properties: {
|
|
129
|
+
openId: {
|
|
130
|
+
type: "string" as const,
|
|
131
|
+
description: "Feishu openId of the recipient",
|
|
132
|
+
},
|
|
133
|
+
message: {
|
|
134
|
+
type: "string" as const,
|
|
135
|
+
description: "Text message content to send",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ["openId", "message"],
|
|
139
|
+
},
|
|
140
|
+
async execute(_toolCallId: string, params: { openId: string; message: string }) {
|
|
141
|
+
const result = await feishuClient.sendMessage(params.openId, params.message);
|
|
142
|
+
if (result.success) {
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text" as const, text: `Message sent to Feishu user ${params.openId}` }],
|
|
145
|
+
details: { sent: true, openId: params.openId, messageId: result.messageId },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text" as const,
|
|
152
|
+
text: `Failed to send Feishu message to ${params.openId}: ${result.error}`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
details: { sent: false, openId: params.openId, error: result.error },
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|