@zhin.js/adapter-lark 4.0.2 → 4.0.3

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +59 -88
  3. package/adapters/lark.ts +26 -0
  4. package/agent/tools/add_members.ts +2 -2
  5. package/agent/tools/create_chat.ts +2 -2
  6. package/agent/tools/dissolve_chat.ts +2 -2
  7. package/agent/tools/get_user.ts +2 -2
  8. package/agent/tools/remove_managers.ts +2 -2
  9. package/agent/tools/set_managers.ts +2 -2
  10. package/agent/tools/update_chat.ts +2 -2
  11. package/agent/tools/upload_file.ts +2 -2
  12. package/lib/endpoint.d.ts +53 -0
  13. package/lib/endpoint.js +333 -0
  14. package/lib/index.d.ts +5 -0
  15. package/lib/index.js +5 -0
  16. package/lib/lark-agent-deps.d.ts +27 -0
  17. package/lib/lark-agent-deps.js +30 -0
  18. package/lib/platform-permit.d.ts +15 -0
  19. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  20. package/lib/protocol.d.ts +111 -0
  21. package/lib/protocol.js +204 -0
  22. package/lib/webhook.d.ts +13 -0
  23. package/lib/webhook.js +67 -0
  24. package/package.json +43 -24
  25. package/plugin.ts +12 -0
  26. package/schema.json +25 -0
  27. package/src/endpoint.ts +354 -705
  28. package/src/index.ts +45 -59
  29. package/src/lark-agent-deps.ts +53 -9
  30. package/src/platform-permit.ts +1 -1
  31. package/src/protocol.ts +332 -0
  32. package/src/webhook.ts +101 -0
  33. package/lib/agent/tools/add_members.js +0 -21
  34. package/lib/agent/tools/add_members.js.map +0 -1
  35. package/lib/agent/tools/create_chat.js +0 -20
  36. package/lib/agent/tools/create_chat.js.map +0 -1
  37. package/lib/agent/tools/dissolve_chat.js +0 -18
  38. package/lib/agent/tools/dissolve_chat.js.map +0 -1
  39. package/lib/agent/tools/get_user.js +0 -17
  40. package/lib/agent/tools/get_user.js.map +0 -1
  41. package/lib/agent/tools/remove_managers.js +0 -21
  42. package/lib/agent/tools/remove_managers.js.map +0 -1
  43. package/lib/agent/tools/set_managers.js +0 -21
  44. package/lib/agent/tools/set_managers.js.map +0 -1
  45. package/lib/agent/tools/update_chat.js +0 -22
  46. package/lib/agent/tools/update_chat.js.map +0 -1
  47. package/lib/agent/tools/upload_file.js +0 -19
  48. package/lib/agent/tools/upload_file.js.map +0 -1
  49. package/lib/src/adapter.js +0 -48
  50. package/lib/src/adapter.js.map +0 -1
  51. package/lib/src/endpoint.js +0 -677
  52. package/lib/src/endpoint.js.map +0 -1
  53. package/lib/src/index.js +0 -43
  54. package/lib/src/index.js.map +0 -1
  55. package/lib/src/lark-agent-deps.js +0 -10
  56. package/lib/src/lark-agent-deps.js.map +0 -1
  57. package/lib/src/platform-permit.js.map +0 -1
  58. package/lib/src/segment-mapper.js +0 -2
  59. package/lib/src/segment-mapper.js.map +0 -1
  60. package/lib/src/types.js +0 -2
  61. package/lib/src/types.js.map +0 -1
  62. package/plugin.yml +0 -3
  63. package/src/adapter.ts +0 -57
  64. package/src/segment-mapper.ts +0 -1
  65. package/src/types.ts +0 -58
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Lark/Feishu protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { createHash, timingSafeEqual } from 'node:crypto';
6
+ export function resolveLarkConfig(config = {}) {
7
+ const entry = config.endpoints?.find((item) => item.context === 'lark');
8
+ const appId = config.appId ?? entry?.appId ?? process.env.LARK_APP_ID;
9
+ const appSecret = config.appSecret ?? entry?.appSecret ?? process.env.LARK_APP_SECRET;
10
+ if (!appId || !appSecret) {
11
+ throw new TypeError('Lark adapter requires appId + appSecret (plugins.<key> or endpoints with context: lark)');
12
+ }
13
+ const isFeishu = config.isFeishu ?? entry?.isFeishu ?? true;
14
+ const name = (typeof config.name === 'string' && config.name)
15
+ || (typeof entry?.name === 'string' && entry.name)
16
+ || process.env.LARK_BOT_NAME
17
+ || 'lark-bot';
18
+ const webhookPath = normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? '/lark/webhook');
19
+ const defaultBase = isFeishu
20
+ ? 'https://open.feishu.cn/open-apis'
21
+ : 'https://open.larksuite.com/open-apis';
22
+ const apiBaseUrl = (config.apiBaseUrl ?? entry?.apiBaseUrl ?? defaultBase).replace(/\/$/, '');
23
+ const encryptKey = config.encryptKey ?? entry?.encryptKey;
24
+ const verificationToken = config.verificationToken ?? entry?.verificationToken;
25
+ return {
26
+ context: 'lark',
27
+ name,
28
+ appId,
29
+ appSecret,
30
+ ...(encryptKey ? { encryptKey } : {}),
31
+ ...(verificationToken ? { verificationToken } : {}),
32
+ webhookPath,
33
+ apiBaseUrl,
34
+ isFeishu,
35
+ };
36
+ }
37
+ export function normalizeWebhookPath(path) {
38
+ const trimmed = path.trim() || '/lark/webhook';
39
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
40
+ }
41
+ export function resolveChatType(chatId) {
42
+ return chatId?.startsWith('oc_') ? 'group' : 'private';
43
+ }
44
+ export function resolveTarget(msg) {
45
+ return msg.chat_id || 'unknown';
46
+ }
47
+ export function resolveSender(msg) {
48
+ return msg.sender?.sender_id?.open_id
49
+ || msg.sender?.sender_id?.user_id
50
+ || 'unknown';
51
+ }
52
+ export function generateMessageId(msg) {
53
+ return msg.message_id || `${msg.create_time ?? Date.now()}`;
54
+ }
55
+ /** Build inbound text for MessageGateway.receive. */
56
+ export function formatInboundContent(msg) {
57
+ if (!msg.content || !msg.message_type)
58
+ return '';
59
+ try {
60
+ const parsed = JSON.parse(msg.content);
61
+ switch (msg.message_type) {
62
+ case 'text':
63
+ return typeof parsed.text === 'string' ? parsed.text : '';
64
+ case 'image':
65
+ return '[image]';
66
+ case 'file': {
67
+ const name = typeof parsed.file_name === 'string' ? parsed.file_name : '';
68
+ return name ? `[file: ${name}]` : '[file]';
69
+ }
70
+ case 'audio':
71
+ return '[audio]';
72
+ case 'video':
73
+ return '[video]';
74
+ case 'sticker':
75
+ return '[sticker]';
76
+ case 'post':
77
+ case 'interactive':
78
+ return `[${msg.message_type}]`;
79
+ case 'rich_text':
80
+ return '[rich_text]';
81
+ default:
82
+ return `[${msg.message_type}]`;
83
+ }
84
+ }
85
+ catch {
86
+ return '[消息解析失败]';
87
+ }
88
+ }
89
+ export function verifySignature(encryptKey, timestamp, nonce, body, signature) {
90
+ try {
91
+ const stringToSign = `${timestamp}${nonce}${encryptKey}${body}`;
92
+ const calculated = createHash('sha256').update(stringToSign).digest('hex');
93
+ const a = Buffer.from(calculated);
94
+ const b = Buffer.from(signature);
95
+ if (a.length !== b.length)
96
+ return false;
97
+ return timingSafeEqual(a, b);
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Wire-encode an already-rendered outbound payload into Lark im/v1 message body.
105
+ * Segment canonicalization is intentionally not done here.
106
+ */
107
+ export function formatOutboundBody(payload) {
108
+ if (typeof payload === 'string') {
109
+ return {
110
+ msg_type: 'text',
111
+ content: JSON.stringify({ text: payload }),
112
+ };
113
+ }
114
+ const items = Array.isArray(payload)
115
+ ? payload
116
+ : payload && typeof payload === 'object' && 'type' in payload
117
+ ? [payload]
118
+ : [];
119
+ if (items.length === 0) {
120
+ const text = payload == null
121
+ ? ''
122
+ : typeof payload === 'object'
123
+ ? JSON.stringify(payload)
124
+ : String(payload);
125
+ return {
126
+ msg_type: 'text',
127
+ content: JSON.stringify({ text }),
128
+ };
129
+ }
130
+ const textParts = [];
131
+ let media = null;
132
+ for (const item of items) {
133
+ if (typeof item === 'string') {
134
+ textParts.push(item);
135
+ continue;
136
+ }
137
+ const data = item.data ?? {};
138
+ switch (item.type) {
139
+ case 'text':
140
+ textParts.push(String(data.content ?? data.text ?? ''));
141
+ break;
142
+ case 'at':
143
+ textParts.push(`<at user_id="${String(data.id ?? '')}">${String(data.name || data.id || '')}</at>`);
144
+ break;
145
+ case 'image':
146
+ if (!media) {
147
+ media = {
148
+ msg_type: 'image',
149
+ content: JSON.stringify({
150
+ image_key: data.file_key ?? data.key ?? data.url,
151
+ }),
152
+ };
153
+ }
154
+ break;
155
+ case 'file':
156
+ if (!media) {
157
+ media = {
158
+ msg_type: 'file',
159
+ content: JSON.stringify({
160
+ file_key: data.file_key ?? data.key,
161
+ }),
162
+ };
163
+ }
164
+ break;
165
+ case 'card':
166
+ if (!media) {
167
+ media = {
168
+ msg_type: 'interactive',
169
+ content: JSON.stringify(data),
170
+ };
171
+ }
172
+ break;
173
+ default:
174
+ textParts.push(`[${item.type}]`);
175
+ }
176
+ }
177
+ if (media)
178
+ return media;
179
+ return {
180
+ msg_type: 'text',
181
+ content: JSON.stringify({ text: textParts.join('') }),
182
+ };
183
+ }
184
+ export function headerValue(headers, name) {
185
+ const value = headers[name] ?? headers[name.toLowerCase()];
186
+ if (Array.isArray(value))
187
+ return value[0] ?? '';
188
+ return value ?? '';
189
+ }
190
+ export async function readTextBody(request, options = {}) {
191
+ const limit = options.limit ?? 1_048_576;
192
+ const chunks = [];
193
+ let size = 0;
194
+ for await (const chunk of request) {
195
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
196
+ size += buffer.length;
197
+ if (size > limit) {
198
+ request.destroy();
199
+ throw new Error(`Request body exceeds ${limit} bytes`);
200
+ }
201
+ chunks.push(buffer);
202
+ }
203
+ return Buffer.concat(chunks).toString('utf8');
204
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lark/Feishu webhook HTTP: verification → signature → challenge/event → admit.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
6
+ import { type LarkMessage, type ResolvedLarkConfig } from './protocol.js';
7
+ export interface LarkWebhookHandler {
8
+ readonly config: ResolvedLarkConfig;
9
+ readonly isOpen: boolean;
10
+ admit(msg: LarkMessage): void;
11
+ }
12
+ export declare function registerLarkWebhookRoutes(http: HttpHost, handler: LarkWebhookHandler): HttpRouteRegistration[];
13
+ export declare function handleLarkWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: LarkWebhookHandler): Promise<void>;
package/lib/webhook.js ADDED
@@ -0,0 +1,67 @@
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { headerValue, readTextBody, verifySignature, } from './protocol.js';
3
+ const logger = getLogger('lark');
4
+ export function registerLarkWebhookRoutes(http, handler) {
5
+ const path = handler.config.webhookPath;
6
+ return [
7
+ http.route('POST', path, async (request, response) => {
8
+ await handleLarkWebhookRequest(request, response, handler);
9
+ }, { summary: 'Lark/Feishu event webhook', tags: ['lark'] }),
10
+ ];
11
+ }
12
+ export async function handleLarkWebhookRequest(request, response, handler) {
13
+ try {
14
+ const rawBody = await readTextBody(request);
15
+ if (handler.config.verificationToken) {
16
+ const token = headerValue(request.headers, 'x-lark-request-token');
17
+ if (token !== handler.config.verificationToken) {
18
+ logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid verification token' }));
19
+ response.writeHead(403, { 'Content-Type': 'text/plain' });
20
+ response.end('Forbidden');
21
+ return;
22
+ }
23
+ }
24
+ if (handler.config.encryptKey) {
25
+ const timestamp = headerValue(request.headers, 'x-lark-request-timestamp');
26
+ const nonce = headerValue(request.headers, 'x-lark-request-nonce');
27
+ const signature = headerValue(request.headers, 'x-lark-signature');
28
+ if (!timestamp
29
+ || !nonce
30
+ || !signature
31
+ || !verifySignature(handler.config.encryptKey, timestamp, nonce, rawBody, signature)) {
32
+ logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid signature' }));
33
+ response.writeHead(403, { 'Content-Type': 'text/plain' });
34
+ response.end('Forbidden');
35
+ return;
36
+ }
37
+ }
38
+ let event;
39
+ try {
40
+ event = JSON.parse(rawBody);
41
+ }
42
+ catch {
43
+ response.writeHead(200, { 'Content-Type': 'application/json' });
44
+ response.end(JSON.stringify({ code: 0, msg: 'success' }));
45
+ return;
46
+ }
47
+ if (event.type === 'url_verification') {
48
+ response.writeHead(200, { 'Content-Type': 'application/json' });
49
+ response.end(JSON.stringify({ challenge: event.challenge }));
50
+ return;
51
+ }
52
+ if (event.type === 'event_callback' && event.event?.message && handler.isOpen) {
53
+ const message = {
54
+ ...event.event.message,
55
+ sender: event.event.message.sender ?? event.event.sender,
56
+ };
57
+ handler.admit(message);
58
+ }
59
+ response.writeHead(200, { 'Content-Type': 'application/json' });
60
+ response.end(JSON.stringify({ code: 0, msg: 'success' }));
61
+ }
62
+ catch (error) {
63
+ logger.error('Webhook error:', error);
64
+ response.writeHead(500, { 'Content-Type': 'application/json' });
65
+ response.end(JSON.stringify({ code: -1, msg: 'Internal Server Error' }));
66
+ }
67
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "4.0.2",
4
- "description": "Zhin.js adapter for Lark/Feishu (飞书)",
3
+ "version": "4.0.3",
4
+ "description": "Zhin.js Lark/Feishu (飞书) adapter for Plugin Runtime (HTTP webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -34,43 +34,48 @@
34
34
  "directory": "plugins/adapters/lark"
35
35
  },
36
36
  "dependencies": {
37
- "axios": "^1.18.0"
38
- },
39
- "devDependencies": {
40
- "@types/koa": "^3.0.3",
41
- "@types/node": "^26.1.0",
42
- "typescript": "^6.0.3",
43
- "zod": "^4.4.3",
44
- "@zhin.js/host-router": "2.0.3",
45
- "zhin.js": "4.1.2",
46
- "@zhin.js/agent": "1.0.3"
37
+ "@zhin.js/adapter": "1.0.1",
38
+ "@zhin.js/core": "1.3.5",
39
+ "@zhin.js/host-http": "1.0.1",
40
+ "@zhin.js/logger": "1.0.75",
41
+ "@zhin.js/plugin-runtime": "1.0.1"
47
42
  },
48
43
  "peerDependencies": {
49
44
  "zod": "^4.0.0",
50
- "@zhin.js/agent": "1.0.3",
51
- "@zhin.js/host-router": "2.0.3",
52
- "zhin.js": "4.1.2"
45
+ "@zhin.js/adapter": "1.0.1",
46
+ "@zhin.js/agent": "1.0.4",
47
+ "@zhin.js/core": "1.3.5",
48
+ "@zhin.js/host-http": "1.0.1",
49
+ "@zhin.js/plugin-runtime": "1.0.1",
50
+ "zhin.js": "4.1.3"
53
51
  },
54
52
  "peerDependenciesMeta": {
55
- "@zhin.js/agent": {
53
+ "zhin.js": {
56
54
  "optional": true
57
55
  },
58
- "@zhin.js/host-router": {
59
- "optional": false
56
+ "@zhin.js/agent": {
57
+ "optional": true
60
58
  },
61
59
  "zod": {
62
60
  "optional": true
63
61
  }
64
62
  },
63
+ "devDependencies": {
64
+ "@types/node": "^26.1.0",
65
+ "typescript": "^6.0.3",
66
+ "vitest": "^4.1.10",
67
+ "zod": "^4.4.3",
68
+ "@zhin.js/agent": "1.0.4",
69
+ "zhin.js": "4.1.3"
70
+ },
65
71
  "files": [
72
+ "adapters",
73
+ "plugin.ts",
74
+ "schema.json",
66
75
  "src",
67
76
  "lib",
68
77
  "agent",
69
- "client",
70
- "dist",
71
- "plugin.yml",
72
78
  "README.md",
73
- "node",
74
79
  "CHANGELOG.md"
75
80
  ],
76
81
  "publishConfig": {
@@ -80,9 +85,23 @@
80
85
  "engines": {
81
86
  "node": "^20.19.0 || >=22.12.0"
82
87
  },
88
+ "zhin": {
89
+ "protocol": 1,
90
+ "type": "plugin",
91
+ "entry": "./plugin.ts",
92
+ "engine": "^1.0.0",
93
+ "runtime": "trusted",
94
+ "features": [
95
+ {
96
+ "package": "@zhin.js/adapter",
97
+ "api": "^1.0.0"
98
+ }
99
+ ],
100
+ "plugins": []
101
+ },
83
102
  "scripts": {
84
- "build": "pnpm build:node",
103
+ "build": "tsc",
85
104
  "clean": "rimraf lib",
86
- "build:node": "tsc"
105
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/lark/tests"
87
106
  }
88
107
  }
package/plugin.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { definePlugin } from '@zhin.js/plugin-runtime';
2
+ import { registerLarkPlatformPermitChecker } from './src/platform-permit.js';
3
+
4
+ export default definePlugin({
5
+ name: 'lark',
6
+ metadata: {
7
+ displayName: 'Lark/Feishu (飞书) Adapter',
8
+ },
9
+ setup() {
10
+ return registerLarkPlatformPermitChecker();
11
+ },
12
+ });
package/schema.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "name": {
7
+ "type": "string",
8
+ "default": "lark-bot"
9
+ },
10
+ "appId": { "type": "string" },
11
+ "appSecret": { "type": "string" },
12
+ "encryptKey": { "type": "string" },
13
+ "verificationToken": { "type": "string" },
14
+ "webhookPath": {
15
+ "type": "string",
16
+ "default": "/lark/webhook"
17
+ },
18
+ "apiBaseUrl": { "type": "string" },
19
+ "isFeishu": {
20
+ "type": "boolean",
21
+ "default": true
22
+ }
23
+ },
24
+ "required": ["appId", "appSecret"]
25
+ }