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
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ P2P mesh network plugin for OpenClaw. Enables direct peer-to-peer communication
|
|
|
9
9
|
- **Broadcast** — Publish messages to a shared topic, flood-fill forwarded across the mesh
|
|
10
10
|
- **Bootstrap Mode** — Optional static bootstrap peer list for non-LAN scenarios
|
|
11
11
|
- **WebSocket Transport** — Optional WebSocket support for NAT/firewall-friendly connections
|
|
12
|
+
- **Feishu Integration (Beta)** — Receive and send messages via Feishu (Lark) IM, with automatic P2P forwarding
|
|
12
13
|
|
|
13
14
|
## Requirements
|
|
14
15
|
|
|
@@ -21,7 +22,7 @@ P2P mesh network plugin for OpenClaw. Enables direct peer-to-peer communication
|
|
|
21
22
|
### Method 1: Via OpenClaw CLI (Recommended)
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
openclaw install
|
|
25
|
+
openclaw plugins install libp2p-mesh-v1
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
### Method 2: Manual (npm)
|
|
@@ -30,7 +31,7 @@ openclaw install openclaw-libp2p-mesh
|
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
33
|
cd ~/.openclaw/npm
|
|
33
|
-
npm install
|
|
34
|
+
npm install libp2p-mesh-v1
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
然后刷新插件注册表:
|
|
@@ -146,6 +147,34 @@ If peers are on different networks, use a bootstrap node:
|
|
|
146
147
|
| `meshTopic` | `string` | `"openclaw-mesh"` | Default broadcast topic |
|
|
147
148
|
| `enableAgentSync` | `boolean` | `true` | Enable agent state synchronization over the mesh |
|
|
148
149
|
|
|
150
|
+
## Feishu 集成(Beta)
|
|
151
|
+
|
|
152
|
+
OpenClaw 可以通过飞书与用户交互。配置后,你的 OpenClaw 实例会:
|
|
153
|
+
- 接收你在飞书中的消息
|
|
154
|
+
- 通过 P2P mesh 与其他 OpenClaw 实例通信
|
|
155
|
+
- 将 P2P 消息自动转发到你的飞书
|
|
156
|
+
|
|
157
|
+
### 配置步骤
|
|
158
|
+
|
|
159
|
+
1. 在[飞书开放平台](https://open.feishu.cn/)创建企业自建应用
|
|
160
|
+
2. 开启"机器人"能力,获取 App ID 和 App Secret
|
|
161
|
+
3. 配置事件订阅:请求地址填 `http://<your-host>:9222/webhook/feishu`
|
|
162
|
+
4. 订阅事件类型:`im.message.receive_v1`
|
|
163
|
+
5. 在 OpenClaw 配置中添加:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
feishu:
|
|
167
|
+
appId: "cli-xxxxxxxxxx"
|
|
168
|
+
appSecret: "xxxxxxxxxxxxxxxx"
|
|
169
|
+
webhookPort: 9222
|
|
170
|
+
webhookPath: "/webhook/feishu"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 部署要求
|
|
174
|
+
|
|
175
|
+
- webhook 端口必须对飞书服务器可达(公网 IP 或端口映射)
|
|
176
|
+
- 本地开发可以使用 ngrok: `ngrok http 9222`
|
|
177
|
+
|
|
149
178
|
## Usage: Two Computers on the Same LAN
|
|
150
179
|
|
|
151
180
|
### Step 1 — Start both gateways
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { registerLibp2pMesh } from "./src/plugin.js";
|
|
3
|
-
function createLibp2pMeshConfigSchema() {
|
|
3
|
+
export function createLibp2pMeshConfigSchema() {
|
|
4
4
|
return {
|
|
5
5
|
safeParse(value) {
|
|
6
6
|
if (value === undefined) {
|
|
@@ -49,6 +49,30 @@ function createLibp2pMeshConfigSchema() {
|
|
|
49
49
|
type: "boolean",
|
|
50
50
|
default: true,
|
|
51
51
|
},
|
|
52
|
+
feishu: {
|
|
53
|
+
type: "object",
|
|
54
|
+
description: "Feishu integration configuration",
|
|
55
|
+
properties: {
|
|
56
|
+
appId: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Feishu app App ID",
|
|
59
|
+
},
|
|
60
|
+
appSecret: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Feishu app App Secret",
|
|
63
|
+
},
|
|
64
|
+
webhookPort: {
|
|
65
|
+
type: "number",
|
|
66
|
+
default: 9222,
|
|
67
|
+
description: "Port for Feishu webhook callback (default: 9222)",
|
|
68
|
+
},
|
|
69
|
+
webhookPath: {
|
|
70
|
+
type: "string",
|
|
71
|
+
default: "/webhook/feishu",
|
|
72
|
+
description: "Path for Feishu webhook callback",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
52
76
|
},
|
|
53
77
|
},
|
|
54
78
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { buildFeishuTools } from "./agent-tools.js";
|
|
3
|
+
describe("buildFeishuTools", () => {
|
|
4
|
+
it("should return empty array when feishuClient is null", () => {
|
|
5
|
+
const tools = buildFeishuTools(null);
|
|
6
|
+
expect(tools).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it("should return array with one tool when feishuClient is provided", () => {
|
|
9
|
+
const mockClient = {
|
|
10
|
+
sendMessage: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
const tools = buildFeishuTools(mockClient);
|
|
13
|
+
expect(tools).toHaveLength(1);
|
|
14
|
+
expect(tools[0].name).toBe("feishu_send_message");
|
|
15
|
+
});
|
|
16
|
+
describe("feishu_send_message tool", () => {
|
|
17
|
+
let tools;
|
|
18
|
+
let mockClient;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockClient = {
|
|
21
|
+
sendMessage: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
tools = buildFeishuTools(mockClient);
|
|
24
|
+
});
|
|
25
|
+
it("should require openId and message parameters", () => {
|
|
26
|
+
const tool = tools[0];
|
|
27
|
+
expect(tool.parameters.required).toEqual(["openId", "message"]);
|
|
28
|
+
});
|
|
29
|
+
it("should return success result on successful send", async () => {
|
|
30
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
31
|
+
success: true,
|
|
32
|
+
messageId: "msg-789",
|
|
33
|
+
});
|
|
34
|
+
const result = await tools[0].execute("call-1", {
|
|
35
|
+
openId: "ou-test",
|
|
36
|
+
message: "hello",
|
|
37
|
+
});
|
|
38
|
+
expect(result.isError).toBeUndefined();
|
|
39
|
+
expect(result.details.sent).toBe(true);
|
|
40
|
+
expect(result.details.openId).toBe("ou-test");
|
|
41
|
+
expect(mockClient.sendMessage).toHaveBeenCalledWith("ou-test", "hello");
|
|
42
|
+
});
|
|
43
|
+
it("should return error result on failed send", async () => {
|
|
44
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
45
|
+
success: false,
|
|
46
|
+
error: "rate limited",
|
|
47
|
+
});
|
|
48
|
+
const result = await tools[0].execute("call-1", {
|
|
49
|
+
openId: "ou-test",
|
|
50
|
+
message: "hello",
|
|
51
|
+
});
|
|
52
|
+
expect(result.isError).toBe(true);
|
|
53
|
+
expect(result.details.sent).toBe(false);
|
|
54
|
+
expect(result.details.error).toBe("rate limited");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MeshNetwork } from "./types.js";
|
|
2
|
+
import type { FeishuApiClient } from "./feishu-client.js";
|
|
2
3
|
export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
3
4
|
name: string;
|
|
4
5
|
label: string;
|
|
@@ -128,3 +129,50 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
128
129
|
isError: boolean;
|
|
129
130
|
}>;
|
|
130
131
|
})[];
|
|
132
|
+
export declare function buildFeishuTools(feishuClient: FeishuApiClient | null): {
|
|
133
|
+
name: string;
|
|
134
|
+
label: string;
|
|
135
|
+
description: string;
|
|
136
|
+
parameters: {
|
|
137
|
+
type: "object";
|
|
138
|
+
properties: {
|
|
139
|
+
openId: {
|
|
140
|
+
type: "string";
|
|
141
|
+
description: string;
|
|
142
|
+
};
|
|
143
|
+
message: {
|
|
144
|
+
type: "string";
|
|
145
|
+
description: string;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
required: string[];
|
|
149
|
+
};
|
|
150
|
+
execute(_toolCallId: string, params: {
|
|
151
|
+
openId: string;
|
|
152
|
+
message: string;
|
|
153
|
+
}): Promise<{
|
|
154
|
+
content: {
|
|
155
|
+
type: "text";
|
|
156
|
+
text: string;
|
|
157
|
+
}[];
|
|
158
|
+
details: {
|
|
159
|
+
sent: boolean;
|
|
160
|
+
openId: string;
|
|
161
|
+
messageId: string | undefined;
|
|
162
|
+
error?: undefined;
|
|
163
|
+
};
|
|
164
|
+
isError?: undefined;
|
|
165
|
+
} | {
|
|
166
|
+
content: {
|
|
167
|
+
type: "text";
|
|
168
|
+
text: string;
|
|
169
|
+
}[];
|
|
170
|
+
details: {
|
|
171
|
+
sent: boolean;
|
|
172
|
+
openId: string;
|
|
173
|
+
error: string | undefined;
|
|
174
|
+
messageId?: undefined;
|
|
175
|
+
};
|
|
176
|
+
isError: boolean;
|
|
177
|
+
}>;
|
|
178
|
+
}[];
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -114,3 +114,47 @@ export function buildP2PTools(mesh) {
|
|
|
114
114
|
},
|
|
115
115
|
];
|
|
116
116
|
}
|
|
117
|
+
export function buildFeishuTools(feishuClient) {
|
|
118
|
+
if (!feishuClient)
|
|
119
|
+
return [];
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
name: "feishu_send_message",
|
|
123
|
+
label: "Feishu Send Message",
|
|
124
|
+
description: "Send a text message to a Feishu user by their openId. Use this to notify users through Feishu.",
|
|
125
|
+
parameters: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
openId: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Feishu openId of the recipient",
|
|
131
|
+
},
|
|
132
|
+
message: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Text message content to send",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ["openId", "message"],
|
|
138
|
+
},
|
|
139
|
+
async execute(_toolCallId, params) {
|
|
140
|
+
const result = await feishuClient.sendMessage(params.openId, params.message);
|
|
141
|
+
if (result.success) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: `Message sent to Feishu user ${params.openId}` }],
|
|
144
|
+
details: { sent: true, openId: params.openId, messageId: result.messageId },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: `Failed to send Feishu message to ${params.openId}: ${result.error}`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
details: { sent: false, openId: params.openId, error: result.error },
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createLibp2pMeshConfigSchema } from "./index.js";
|
|
3
|
+
describe("createLibp2pMeshConfigSchema - Feishu config", () => {
|
|
4
|
+
const schema = createLibp2pMeshConfigSchema();
|
|
5
|
+
it("should include feishu in jsonSchema properties", () => {
|
|
6
|
+
const props = schema.jsonSchema.properties;
|
|
7
|
+
expect(props).toHaveProperty("feishu");
|
|
8
|
+
});
|
|
9
|
+
it("feishu should be an object type with correct properties", () => {
|
|
10
|
+
const props = schema.jsonSchema.properties;
|
|
11
|
+
const feishu = props.feishu;
|
|
12
|
+
expect(feishu.type).toBe("object");
|
|
13
|
+
expect(feishu.properties).toHaveProperty("appId");
|
|
14
|
+
expect(feishu.properties).toHaveProperty("appSecret");
|
|
15
|
+
expect(feishu.properties).toHaveProperty("webhookPort");
|
|
16
|
+
expect(feishu.properties).toHaveProperty("webhookPath");
|
|
17
|
+
});
|
|
18
|
+
it("feishu.webhookPort should default to 9222", () => {
|
|
19
|
+
const props = schema.jsonSchema.properties;
|
|
20
|
+
expect(props.feishu.properties.webhookPort.default).toBe(9222);
|
|
21
|
+
});
|
|
22
|
+
it("feishu.webhookPath should default to /webhook/feishu", () => {
|
|
23
|
+
const props = schema.jsonSchema.properties;
|
|
24
|
+
expect(props.feishu.properties.webhookPath.default).toBe("/webhook/feishu");
|
|
25
|
+
});
|
|
26
|
+
it("feishu.appId and appSecret should be string type without default", () => {
|
|
27
|
+
const props = schema.jsonSchema.properties;
|
|
28
|
+
expect(props.feishu.properties.appId.type).toBe("string");
|
|
29
|
+
expect(props.feishu.properties.appSecret.type).toBe("string");
|
|
30
|
+
expect(props.feishu.properties.appId.default).toBeUndefined();
|
|
31
|
+
expect(props.feishu.properties.appSecret.default).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
it("should parse config with feishu fields", () => {
|
|
34
|
+
const result = schema.safeParse({
|
|
35
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
36
|
+
feishu: {
|
|
37
|
+
appId: "cli-123",
|
|
38
|
+
appSecret: "secret-456",
|
|
39
|
+
webhookPort: 9999,
|
|
40
|
+
webhookPath: "/custom/path",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(result.success).toBe(true);
|
|
44
|
+
if (result.success) {
|
|
45
|
+
expect(result.data.feishu.appId).toBe("cli-123");
|
|
46
|
+
expect(result.data.feishu.webhookPort).toBe(9999);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it("should parse config without feishu (feishu is optional)", () => {
|
|
50
|
+
const result = schema.safeParse({
|
|
51
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
52
|
+
});
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { FeishuApiClient } from "./feishu-client.js";
|
|
3
|
+
import type { FeishuChannelConfig } from "./feishu-types.js";
|
|
4
|
+
export interface FeishuChannelDeps {
|
|
5
|
+
logger?: {
|
|
6
|
+
info?: (msg: string) => void;
|
|
7
|
+
warn?: (msg: string) => void;
|
|
8
|
+
error?: (msg: string) => void;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Plugin type with a base wrapper for test compatibility */
|
|
12
|
+
export interface FeishuChannelPlugin extends ChannelPlugin {
|
|
13
|
+
base: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createFeishuChannel(config: FeishuChannelConfig, deps?: FeishuChannelDeps, feishuClient?: FeishuApiClient): FeishuChannelPlugin;
|
|
16
|
+
export declare function handleFeishuWebhook(config: FeishuChannelConfig, feishuClient: FeishuApiClient, deps: FeishuChannelDeps): {
|
|
17
|
+
startServer: () => Promise<() => Promise<void>>;
|
|
18
|
+
handler: (req: Request) => Promise<Response>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { createHmac } from "node:crypto";
|
|
3
|
+
import { FeishuApiClient } from "./feishu-client.js";
|
|
4
|
+
export function createFeishuChannel(config, deps, feishuClient) {
|
|
5
|
+
const client = feishuClient ?? new FeishuApiClient(config, deps);
|
|
6
|
+
const raw = createChatChannelPlugin({
|
|
7
|
+
base: {
|
|
8
|
+
id: "feishu",
|
|
9
|
+
meta: {
|
|
10
|
+
id: "feishu",
|
|
11
|
+
label: "Feishu",
|
|
12
|
+
selectionLabel: "Feishu",
|
|
13
|
+
docsPath: "/channels/feishu",
|
|
14
|
+
docsLabel: "feishu",
|
|
15
|
+
blurb: "Feishu integration for user interaction.",
|
|
16
|
+
systemImage: "message",
|
|
17
|
+
},
|
|
18
|
+
capabilities: {
|
|
19
|
+
chatTypes: ["direct"],
|
|
20
|
+
media: false,
|
|
21
|
+
blockStreaming: true,
|
|
22
|
+
},
|
|
23
|
+
configSchema: {
|
|
24
|
+
schema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
appId: { type: "string" },
|
|
29
|
+
appSecret: { type: "string" },
|
|
30
|
+
webhookPort: { type: "number" },
|
|
31
|
+
webhookPath: { type: "string" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
config: {
|
|
36
|
+
listAccountIds: () => ["default"],
|
|
37
|
+
resolveAccount: () => ({
|
|
38
|
+
accountId: "default",
|
|
39
|
+
configured: client.isConfigured(),
|
|
40
|
+
enabled: true,
|
|
41
|
+
}),
|
|
42
|
+
isConfigured: () => client.isConfigured(),
|
|
43
|
+
isEnabled: () => true,
|
|
44
|
+
describeAccount: () => ({
|
|
45
|
+
accountId: "default",
|
|
46
|
+
name: "default",
|
|
47
|
+
configured: client.isConfigured(),
|
|
48
|
+
enabled: true,
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
messaging: {
|
|
52
|
+
normalizeTarget: (raw) => raw.trim(),
|
|
53
|
+
targetResolver: {
|
|
54
|
+
looksLikeId: () => true,
|
|
55
|
+
hint: "openId",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
outbound: {
|
|
60
|
+
deliveryMode: "gateway",
|
|
61
|
+
sendText: async ({ to, text }) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await client.sendMessage(to, text);
|
|
64
|
+
if (result.success && result.messageId) {
|
|
65
|
+
return { channel: "feishu", messageId: result.messageId };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
channel: "feishu",
|
|
69
|
+
messageId: `feishu-${Date.now()}`,
|
|
70
|
+
meta: { error: result.error ?? "send failed" },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
channel: "feishu",
|
|
76
|
+
messageId: `feishu-${Date.now()}`,
|
|
77
|
+
meta: { error: String(err) },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
// Attach base wrapper so tests can access channel.base.* (mirrors the nested input structure)
|
|
84
|
+
const plugin = raw;
|
|
85
|
+
plugin.base = {
|
|
86
|
+
id: plugin.id,
|
|
87
|
+
meta: {
|
|
88
|
+
...plugin.meta,
|
|
89
|
+
systemImage: plugin.meta.systemImage ?? "message",
|
|
90
|
+
},
|
|
91
|
+
capabilities: plugin.capabilities,
|
|
92
|
+
outbound: plugin.outbound,
|
|
93
|
+
config: plugin.config,
|
|
94
|
+
messaging: plugin.messaging,
|
|
95
|
+
};
|
|
96
|
+
return plugin;
|
|
97
|
+
}
|
|
98
|
+
export function handleFeishuWebhook(config, feishuClient, deps) {
|
|
99
|
+
const { webhookPort, webhookPath, appSecret } = config;
|
|
100
|
+
function verifySignature(body, signature) {
|
|
101
|
+
if (!signature)
|
|
102
|
+
return false;
|
|
103
|
+
const expected = createHmac("sha256", appSecret)
|
|
104
|
+
.update(body)
|
|
105
|
+
.digest("base64");
|
|
106
|
+
return signature === expected;
|
|
107
|
+
}
|
|
108
|
+
async function handler(req) {
|
|
109
|
+
const url = new URL(req.url);
|
|
110
|
+
// Challenge verification (GET)
|
|
111
|
+
if (req.method === "GET") {
|
|
112
|
+
const challenge = url.searchParams.get("challenge");
|
|
113
|
+
if (challenge) {
|
|
114
|
+
return new Response(JSON.stringify({ challenge }), {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return new Response("Not Found", { status: 404 });
|
|
120
|
+
}
|
|
121
|
+
// Event handling (POST)
|
|
122
|
+
if (req.method === "POST") {
|
|
123
|
+
const body = await req.text();
|
|
124
|
+
const signature = req.headers.get("x-lark-signature");
|
|
125
|
+
if (!verifySignature(body, signature)) {
|
|
126
|
+
return new Response("Invalid signature", { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const event = JSON.parse(body);
|
|
130
|
+
// Only process im.message.receive_v1 events
|
|
131
|
+
const eventType = event.header?.eventType;
|
|
132
|
+
if (eventType !== "im.message.receive_v1") {
|
|
133
|
+
return new Response(null, { status: 200 });
|
|
134
|
+
}
|
|
135
|
+
const message = event.event?.message;
|
|
136
|
+
const openId = event.event?.sender?.senderId?.openId;
|
|
137
|
+
if (message && openId && message.msgType === "text") {
|
|
138
|
+
try {
|
|
139
|
+
const content = JSON.parse(message.content);
|
|
140
|
+
const text = content.text?.trim();
|
|
141
|
+
if (text) {
|
|
142
|
+
await feishuClient.sendMessage(openId, text);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
deps.logger?.warn?.(`Failed to parse message content: ${message.content}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return new Response(null, { status: 200 });
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
156
|
+
}
|
|
157
|
+
async function startServer() {
|
|
158
|
+
const httpModule = await import("node:http");
|
|
159
|
+
const server = httpModule.createServer(async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const host = req.headers.host ?? "localhost";
|
|
162
|
+
const reqUrl = req.url ?? "/";
|
|
163
|
+
const url = new URL(reqUrl, `http://${host}`);
|
|
164
|
+
// Convert Node.js IncomingMessage to a Web API-like Request for handler
|
|
165
|
+
const request = new Request(url.toString(), {
|
|
166
|
+
method: req.method,
|
|
167
|
+
headers: Object.fromEntries(Object.entries(req.headers)),
|
|
168
|
+
body: req.method !== "GET" && req.method !== "HEAD"
|
|
169
|
+
? await new Promise((resolve) => {
|
|
170
|
+
const chunks = [];
|
|
171
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
172
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
173
|
+
})
|
|
174
|
+
: undefined,
|
|
175
|
+
});
|
|
176
|
+
const response = await handler(request);
|
|
177
|
+
res.statusCode = response.status;
|
|
178
|
+
// Forward response headers to Node.js response
|
|
179
|
+
const headersObj = Object.fromEntries(response.headers);
|
|
180
|
+
for (const [key, value] of Object.entries(headersObj)) {
|
|
181
|
+
res.setHeader(key, value);
|
|
182
|
+
}
|
|
183
|
+
const responseBody = await response.text();
|
|
184
|
+
res.end(responseBody);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
deps.logger?.error?.(`Webhook handler error: ${err}`);
|
|
188
|
+
res.statusCode = 500;
|
|
189
|
+
res.end("Internal Server Error");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
await new Promise((resolve) => {
|
|
193
|
+
server.listen(webhookPort, () => {
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return () => new Promise((resolve) => {
|
|
198
|
+
server.close(() => resolve());
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return { startServer, handler };
|
|
202
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|