libp2p-mesh 2026.5.14 → 2026.5.16
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/dist/index.d.ts +2 -8
- package/index.ts +1 -1
- package/package.json +1 -1
- package/dist/src/agent-tools-feishu.test.d.ts +0 -1
- package/dist/src/agent-tools-feishu.test.js +0 -57
- package/dist/src/config-schema.test.d.ts +0 -1
- package/dist/src/config-schema.test.js +0 -55
- package/dist/src/feishu-channel.d.ts +0 -19
- package/dist/src/feishu-channel.js +0 -202
- package/dist/src/feishu-channel.test.d.ts +0 -1
- package/dist/src/feishu-channel.test.js +0 -166
- package/dist/src/feishu-client.d.ts +0 -27
- package/dist/src/feishu-client.js +0 -141
- package/dist/src/feishu-client.test.d.ts +0 -1
- package/dist/src/feishu-client.test.js +0 -271
- package/dist/src/feishu-e2e.test.d.ts +0 -1
- package/dist/src/feishu-e2e.test.js +0 -69
- package/dist/src/feishu-types.d.ts +0 -53
- package/dist/src/feishu-types.js +0 -1
- package/dist/src/feishu-types.test.d.ts +0 -1
- package/dist/src/feishu-types.test.js +0 -108
- package/dist/src/inbound-feishu.test.d.ts +0 -1
- package/dist/src/inbound-feishu.test.js +0 -70
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/plugin-registration.test.d.ts +0 -1
- package/dist/src/plugin-registration.test.js +0 -42
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
declare const _default:
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
description: string;
|
|
6
|
-
configSchema: OpenClawPluginConfigSchema;
|
|
7
|
-
register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
|
|
8
|
-
} & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "reload" | "kind" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
declare const _default: ReturnType<typeof definePluginEntry>;
|
|
9
3
|
export default _default;
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,57 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,202 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { createFeishuChannel, handleFeishuWebhook } from "./feishu-channel.js";
|
|
3
|
-
const MOCK_CONFIG = {
|
|
4
|
-
appId: "test-app-id",
|
|
5
|
-
appSecret: "test-app-secret",
|
|
6
|
-
webhookPort: 9222,
|
|
7
|
-
webhookPath: "/webhook/feishu",
|
|
8
|
-
};
|
|
9
|
-
const MOCK_CLIENT = {
|
|
10
|
-
isConfigured: () => true,
|
|
11
|
-
getTenantAccessToken: vi.fn().mockResolvedValue("t-valid"),
|
|
12
|
-
sendMessage: vi.fn().mockResolvedValue({ success: true, messageId: "msg-123" }),
|
|
13
|
-
};
|
|
14
|
-
describe("createFeishuChannel", () => {
|
|
15
|
-
it("should return a ChannelPlugin with correct base properties", () => {
|
|
16
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, MOCK_CLIENT);
|
|
17
|
-
expect(channel.base.id).toBe("feishu");
|
|
18
|
-
expect(channel.base.meta.label).toBe("Feishu");
|
|
19
|
-
expect(channel.base.capabilities.chatTypes).toEqual(["direct"]);
|
|
20
|
-
});
|
|
21
|
-
it("should set targetResolver hint to openId", () => {
|
|
22
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, MOCK_CLIENT);
|
|
23
|
-
const messaging = channel.base.messaging;
|
|
24
|
-
const resolver = messaging.targetResolver;
|
|
25
|
-
expect(resolver.hint).toBe("openId");
|
|
26
|
-
});
|
|
27
|
-
it("should set deliveryMode to gateway", () => {
|
|
28
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, MOCK_CLIENT);
|
|
29
|
-
const outbound = channel.base.outbound;
|
|
30
|
-
expect(outbound?.deliveryMode).toBe("gateway");
|
|
31
|
-
});
|
|
32
|
-
it("should delegate sendText to feishuClient", async () => {
|
|
33
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, MOCK_CLIENT);
|
|
34
|
-
const outbound = channel.base.outbound;
|
|
35
|
-
const result = await outbound.sendText({ to: "ou-user", text: "hello" });
|
|
36
|
-
expect(result.channel).toBe("feishu");
|
|
37
|
-
expect(result.messageId).toBe("msg-123");
|
|
38
|
-
expect(MOCK_CLIENT.sendMessage).toHaveBeenCalledWith("ou-user", "hello");
|
|
39
|
-
});
|
|
40
|
-
it("should return error meta when sendMessage fails", async () => {
|
|
41
|
-
const failClient = {
|
|
42
|
-
...MOCK_CLIENT,
|
|
43
|
-
sendMessage: vi.fn().mockResolvedValue({ success: false, error: "API error" }),
|
|
44
|
-
};
|
|
45
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, failClient);
|
|
46
|
-
const outbound = channel.base.outbound;
|
|
47
|
-
const result = await outbound.sendText({ to: "ou-user", text: "hello" });
|
|
48
|
-
expect("meta" in result && result.meta?.error).toBe("API error");
|
|
49
|
-
});
|
|
50
|
-
it("should reflect configured status from feishuClient", () => {
|
|
51
|
-
const channel = createFeishuChannel(MOCK_CONFIG, {}, MOCK_CLIENT);
|
|
52
|
-
const config = channel.base.config;
|
|
53
|
-
expect(config.isConfigured()).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe("handleFeishuWebhook", () => {
|
|
57
|
-
it("should respond to challenge verification", async () => {
|
|
58
|
-
const { handler } = handleFeishuWebhook(MOCK_CONFIG, MOCK_CLIENT, {});
|
|
59
|
-
const req = new Request("http://localhost:9222/webhook/feishu?challenge=abc123");
|
|
60
|
-
const resp = await handler(req);
|
|
61
|
-
const body = (await resp.json());
|
|
62
|
-
expect(body.challenge).toBe("abc123");
|
|
63
|
-
});
|
|
64
|
-
it("should reject request with invalid signature", async () => {
|
|
65
|
-
const { handler } = handleFeishuWebhook(MOCK_CONFIG, MOCK_CLIENT, {});
|
|
66
|
-
const req = new Request("http://localhost:9222/webhook/feishu", {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "x-lark-signature": "invalid-sig" },
|
|
69
|
-
body: JSON.stringify({ type: "url_verification" }),
|
|
70
|
-
});
|
|
71
|
-
const resp = await handler(req);
|
|
72
|
-
expect(resp.status).toBe(400);
|
|
73
|
-
});
|
|
74
|
-
it("should extract openId and message from im.message.receive_v1 event", async () => {
|
|
75
|
-
const { handler } = handleFeishuWebhook(MOCK_CONFIG, MOCK_CLIENT, {});
|
|
76
|
-
const eventBody = {
|
|
77
|
-
schema: "2.0",
|
|
78
|
-
header: {
|
|
79
|
-
eventId: "evt-1",
|
|
80
|
-
eventType: "im.message.receive_v1",
|
|
81
|
-
createTime: "1234567890000",
|
|
82
|
-
token: "v-token",
|
|
83
|
-
appId: "app-123",
|
|
84
|
-
tenantKey: "tk-123",
|
|
85
|
-
},
|
|
86
|
-
event: {
|
|
87
|
-
sender: { senderId: { openId: "ou-sender-1" } },
|
|
88
|
-
message: {
|
|
89
|
-
messageId: "msg-1",
|
|
90
|
-
msgType: "text",
|
|
91
|
-
createTime: "1234567890000",
|
|
92
|
-
chatId: "chat-1",
|
|
93
|
-
chatType: "p2p",
|
|
94
|
-
content: '{"text":"hello from feishu"}',
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
const body = JSON.stringify(eventBody);
|
|
99
|
-
const sig = 'Tk4MriFS/RRZ9zfM0EHVMKk45vyHMPqTNwc6stjTOxo=';
|
|
100
|
-
const req = new Request("http://localhost:9222/webhook/feishu", {
|
|
101
|
-
method: "POST",
|
|
102
|
-
headers: { "content-type": "application/json", "x-lark-signature": sig },
|
|
103
|
-
body,
|
|
104
|
-
});
|
|
105
|
-
const resp = await handler(req);
|
|
106
|
-
expect(resp.status).toBe(200);
|
|
107
|
-
});
|
|
108
|
-
it("should ignore non-im.message.receive_v1 events", async () => {
|
|
109
|
-
const { handler } = handleFeishuWebhook(MOCK_CONFIG, MOCK_CLIENT, {});
|
|
110
|
-
const eventBody = {
|
|
111
|
-
schema: "2.0",
|
|
112
|
-
header: {
|
|
113
|
-
eventId: "evt-2",
|
|
114
|
-
eventType: "im.chat.member.bot.added_v1",
|
|
115
|
-
createTime: "1234567890000",
|
|
116
|
-
token: "v-token",
|
|
117
|
-
appId: "app-123",
|
|
118
|
-
tenantKey: "tk-123",
|
|
119
|
-
},
|
|
120
|
-
event: {},
|
|
121
|
-
};
|
|
122
|
-
const body = JSON.stringify(eventBody);
|
|
123
|
-
const sig = 'fS/7P4Dm1/UjN5vIolDq4Vgc8welngorAiRmo6tk4H8=';
|
|
124
|
-
const req = new Request("http://localhost:9222/webhook/feishu", {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers: { "content-type": "application/json", "x-lark-signature": sig },
|
|
127
|
-
body,
|
|
128
|
-
});
|
|
129
|
-
const resp = await handler(req);
|
|
130
|
-
expect(resp.status).toBe(200);
|
|
131
|
-
});
|
|
132
|
-
it("should extract plain text from JSON content field", async () => {
|
|
133
|
-
const { handler } = handleFeishuWebhook(MOCK_CONFIG, MOCK_CLIENT, {});
|
|
134
|
-
const eventBody = {
|
|
135
|
-
schema: "2.0",
|
|
136
|
-
header: {
|
|
137
|
-
eventId: "evt-3",
|
|
138
|
-
eventType: "im.message.receive_v1",
|
|
139
|
-
createTime: "1234567890000",
|
|
140
|
-
token: "v-token",
|
|
141
|
-
appId: "app-123",
|
|
142
|
-
tenantKey: "tk-123",
|
|
143
|
-
},
|
|
144
|
-
event: {
|
|
145
|
-
sender: { senderId: { openId: "ou-sender" } },
|
|
146
|
-
message: {
|
|
147
|
-
messageId: "msg-3",
|
|
148
|
-
msgType: "text",
|
|
149
|
-
createTime: "1234567890000",
|
|
150
|
-
chatId: "chat-1",
|
|
151
|
-
chatType: "p2p",
|
|
152
|
-
content: '{"text":"邀请用户B吃饭"}',
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
const body = JSON.stringify(eventBody);
|
|
157
|
-
const sig = 'cV4wR/Bd4TkU7oaZJis9LNpBTFWEjRRkGdpkNeljEi8=';
|
|
158
|
-
const req = new Request("http://localhost:9222/webhook/feishu", {
|
|
159
|
-
method: "POST",
|
|
160
|
-
headers: { "content-type": "application/json", "x-lark-signature": sig },
|
|
161
|
-
body,
|
|
162
|
-
});
|
|
163
|
-
const resp = await handler(req);
|
|
164
|
-
expect(resp.status).toBe(200);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { FeishuChannelConfig } from "./feishu-types.js";
|
|
2
|
-
export interface FeishuApiClientDeps {
|
|
3
|
-
logger?: {
|
|
4
|
-
info?: (msg: string) => void;
|
|
5
|
-
warn?: (msg: string) => void;
|
|
6
|
-
error?: (msg: string) => void;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export interface SendResult {
|
|
10
|
-
success: boolean;
|
|
11
|
-
messageId?: string;
|
|
12
|
-
error?: string;
|
|
13
|
-
}
|
|
14
|
-
export declare class FeishuApiClient {
|
|
15
|
-
private config;
|
|
16
|
-
private logger?;
|
|
17
|
-
private cachedToken;
|
|
18
|
-
private cachedExpiresAt;
|
|
19
|
-
private tokenPromise;
|
|
20
|
-
constructor(config: FeishuChannelConfig, deps?: FeishuApiClientDeps);
|
|
21
|
-
isConfigured(): boolean;
|
|
22
|
-
getTenantAccessToken(): Promise<string>;
|
|
23
|
-
sendMessage(openId: string, text: string): Promise<SendResult>;
|
|
24
|
-
private fetchTenantAccessToken;
|
|
25
|
-
private doSendMessage;
|
|
26
|
-
private callSendApi;
|
|
27
|
-
}
|