@zhin.js/adapter-lark 4.0.1 → 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.
- package/CHANGELOG.md +48 -0
- package/README.md +59 -84
- package/adapters/lark.ts +26 -0
- package/agent/tools/add_members.ts +22 -0
- package/agent/tools/create_chat.ts +20 -0
- package/agent/tools/dissolve_chat.ts +18 -0
- package/agent/tools/get_user.ts +18 -0
- package/agent/tools/remove_managers.ts +22 -0
- package/agent/tools/set_managers.ts +22 -0
- package/agent/tools/update_chat.ts +23 -0
- package/agent/tools/upload_file.ts +27 -0
- package/lib/endpoint.d.ts +41 -72
- package/lib/endpoint.js +219 -563
- package/lib/index.d.ts +5 -15
- package/lib/index.js +5 -216
- package/lib/lark-agent-deps.d.ts +27 -0
- package/lib/lark-agent-deps.js +30 -0
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +111 -0
- package/lib/protocol.js +204 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +67 -0
- package/package.json +50 -21
- package/plugin.ts +12 -0
- package/schema.json +25 -0
- package/src/endpoint.ts +354 -705
- package/src/index.ts +46 -236
- package/src/lark-agent-deps.ts +62 -0
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +332 -0
- package/src/webhook.ts +101 -0
- package/lib/adapter.d.ts +0 -20
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -48
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -63
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -57
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -58
- /package/{skills/lark → agent}/PERMITS.md +0 -0
- /package/{skills/lark/SKILL.md → agent/skills/lark.md} +0 -0
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
interface Adapters {
|
|
9
|
-
lark: LarkAdapter;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export * from "./types.js";
|
|
13
|
-
export { LarkEndpoint } from "./endpoint.js";
|
|
14
|
-
export { LarkAdapter } from "./adapter.js";
|
|
15
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, resolveTarget, verifySignature, type AccessToken, type LarkAdapterConfig, type LarkApiResponse, type LarkEventBody, type LarkMessage, type LarkSendBody, type LarkWireSegment, type ResolvedLarkConfig, } from './protocol.js';
|
|
2
|
+
export { getLarkAgentDeps, registerLarkAgentEndpoint, setLarkAgentDeps, type LarkAgentDeps, type LarkAgentEndpoint, } from './lark-agent-deps.js';
|
|
3
|
+
export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, registerLarkPlatformPermitChecker, } from './platform-permit.js';
|
|
4
|
+
export { LarkEndpoint, type LarkEndpointOptions, type LarkFetch, } from './endpoint.js';
|
|
5
|
+
export { registerLarkWebhookRoutes, handleLarkWebhookRequest, type LarkWebhookHandler, } from './webhook.js';
|
package/lib/index.js
CHANGED
|
@@ -1,216 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { larkGroupPermitResolver, platformPermit, registerLarkPlatformPermitChecker, } from "./platform-permit.js";
|
|
7
|
-
export * from "./types.js";
|
|
8
|
-
export { LarkEndpoint } from "./endpoint.js";
|
|
9
|
-
export { LarkAdapter } from "./adapter.js";
|
|
10
|
-
const plugin = usePlugin();
|
|
11
|
-
const { provide, useContext } = plugin;
|
|
12
|
-
useContext("router", (router) => {
|
|
13
|
-
provide({
|
|
14
|
-
name: "lark",
|
|
15
|
-
description: "Lark/Feishu Endpoint Adapter",
|
|
16
|
-
mounted: async (p) => {
|
|
17
|
-
const adapter = new LarkAdapter(p, router);
|
|
18
|
-
await adapter.start();
|
|
19
|
-
return adapter;
|
|
20
|
-
},
|
|
21
|
-
dispose: async (adapter) => {
|
|
22
|
-
await adapter.stop();
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
useContext('tool', 'lark', (toolService, lark) => {
|
|
27
|
-
const disposers = [];
|
|
28
|
-
disposers.push(registerLarkPlatformPermitChecker());
|
|
29
|
-
const sceneTools = createSceneManagementTools(lark, 'lark', { permitResolver: larkGroupPermitResolver, registerChecker: false });
|
|
30
|
-
disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
|
|
31
|
-
disposers.push(toolService.addTool({
|
|
32
|
-
name: 'lark_get_user',
|
|
33
|
-
description: '获取飞书用户信息',
|
|
34
|
-
parameters: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
38
|
-
user_id: { type: 'string', description: '用户 ID (open_id)' },
|
|
39
|
-
},
|
|
40
|
-
required: ['endpoint_id', 'user_id'],
|
|
41
|
-
},
|
|
42
|
-
platforms: ['lark'],
|
|
43
|
-
tags: ['lark'],
|
|
44
|
-
execute: async (args) => {
|
|
45
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
46
|
-
if (!endpoint)
|
|
47
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
48
|
-
return await endpoint.getUserInfo(args.user_id);
|
|
49
|
-
},
|
|
50
|
-
}, plugin.name));
|
|
51
|
-
disposers.push(toolService.addTool({
|
|
52
|
-
name: 'lark_create_chat',
|
|
53
|
-
description: '创建飞书群聊',
|
|
54
|
-
parameters: {
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: {
|
|
57
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
58
|
-
name: { type: 'string', description: '群名' },
|
|
59
|
-
members: { type: 'string', description: '成员 open_id 列表,逗号分隔' },
|
|
60
|
-
owner: { type: 'string', description: '群主 open_id(可选)' },
|
|
61
|
-
},
|
|
62
|
-
required: ['endpoint_id', 'name', 'members'],
|
|
63
|
-
},
|
|
64
|
-
platforms: ['lark'],
|
|
65
|
-
tags: ['lark'],
|
|
66
|
-
execute: async (args) => {
|
|
67
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
68
|
-
if (!endpoint)
|
|
69
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
70
|
-
const chatId = await endpoint.createChat(args.name, args.members.split(','), args.owner);
|
|
71
|
-
return { success: !!chatId, chat_id: chatId, message: chatId ? `群聊创建成功: ${chatId}` : '创建失败' };
|
|
72
|
-
},
|
|
73
|
-
}, plugin.name));
|
|
74
|
-
disposers.push(toolService.addTool({
|
|
75
|
-
name: 'lark_update_chat',
|
|
76
|
-
description: '更新飞书群聊信息(群名、描述)',
|
|
77
|
-
parameters: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
81
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
82
|
-
name: { type: 'string', description: '新群名(可选)' },
|
|
83
|
-
description: { type: 'string', description: '新描述(可选)' },
|
|
84
|
-
},
|
|
85
|
-
required: ['endpoint_id', 'chat_id'],
|
|
86
|
-
},
|
|
87
|
-
platforms: ['lark'],
|
|
88
|
-
tags: ['lark'],
|
|
89
|
-
permissions: [platformPermit('chat_admin')],
|
|
90
|
-
execute: async (args) => {
|
|
91
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
92
|
-
if (!endpoint)
|
|
93
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
94
|
-
const success = await endpoint.updateChatInfo(args.chat_id, { name: args.name, description: args.description });
|
|
95
|
-
return { success, message: success ? '群信息更新成功' : '更新失败' };
|
|
96
|
-
},
|
|
97
|
-
}, plugin.name));
|
|
98
|
-
disposers.push(toolService.addTool({
|
|
99
|
-
name: 'lark_add_members',
|
|
100
|
-
description: '添加飞书群成员',
|
|
101
|
-
parameters: {
|
|
102
|
-
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
105
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
106
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
107
|
-
},
|
|
108
|
-
required: ['endpoint_id', 'chat_id', 'user_ids'],
|
|
109
|
-
},
|
|
110
|
-
platforms: ['lark'],
|
|
111
|
-
tags: ['lark'],
|
|
112
|
-
permissions: [platformPermit('chat_admin')],
|
|
113
|
-
execute: async (args) => {
|
|
114
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
115
|
-
if (!endpoint)
|
|
116
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
117
|
-
const success = await endpoint.addChatMembers(args.chat_id, args.user_ids.split(','));
|
|
118
|
-
return { success, message: success ? '成员添加成功' : '添加失败' };
|
|
119
|
-
},
|
|
120
|
-
}, plugin.name));
|
|
121
|
-
disposers.push(toolService.addTool({
|
|
122
|
-
name: 'lark_set_managers',
|
|
123
|
-
description: '设置飞书群管理员',
|
|
124
|
-
parameters: {
|
|
125
|
-
type: 'object',
|
|
126
|
-
properties: {
|
|
127
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
128
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
129
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
130
|
-
},
|
|
131
|
-
required: ['endpoint_id', 'chat_id', 'user_ids'],
|
|
132
|
-
},
|
|
133
|
-
platforms: ['lark'],
|
|
134
|
-
tags: ['lark'],
|
|
135
|
-
permissions: [platformPermit('manage_managers')],
|
|
136
|
-
execute: async (args) => {
|
|
137
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
138
|
-
if (!endpoint)
|
|
139
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
140
|
-
const success = await endpoint.setChatManagers(args.chat_id, args.user_ids.split(','));
|
|
141
|
-
return { success, message: success ? '管理员设置成功' : '设置失败' };
|
|
142
|
-
},
|
|
143
|
-
}, plugin.name));
|
|
144
|
-
disposers.push(toolService.addTool({
|
|
145
|
-
name: 'lark_remove_managers',
|
|
146
|
-
description: '移除飞书群管理员',
|
|
147
|
-
parameters: {
|
|
148
|
-
type: 'object',
|
|
149
|
-
properties: {
|
|
150
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
151
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
152
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
153
|
-
},
|
|
154
|
-
required: ['endpoint_id', 'chat_id', 'user_ids'],
|
|
155
|
-
},
|
|
156
|
-
platforms: ['lark'],
|
|
157
|
-
tags: ['lark'],
|
|
158
|
-
permissions: [platformPermit('manage_managers')],
|
|
159
|
-
execute: async (args) => {
|
|
160
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
161
|
-
if (!endpoint)
|
|
162
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
163
|
-
const success = await endpoint.removeChatManagers(args.chat_id, args.user_ids.split(','));
|
|
164
|
-
return { success, message: success ? '管理员移除成功' : '移除失败' };
|
|
165
|
-
},
|
|
166
|
-
}, plugin.name));
|
|
167
|
-
disposers.push(toolService.addTool({
|
|
168
|
-
name: 'lark_dissolve_chat',
|
|
169
|
-
description: '解散飞书群聊(需要群主权限)',
|
|
170
|
-
parameters: {
|
|
171
|
-
type: 'object',
|
|
172
|
-
properties: {
|
|
173
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
174
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
175
|
-
},
|
|
176
|
-
required: ['endpoint_id', 'chat_id'],
|
|
177
|
-
},
|
|
178
|
-
platforms: ['lark'],
|
|
179
|
-
tags: ['lark'],
|
|
180
|
-
execute: async (args) => {
|
|
181
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
182
|
-
if (!endpoint)
|
|
183
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
184
|
-
const success = await endpoint.dissolveChat(args.chat_id);
|
|
185
|
-
return { success, message: success ? '群聊已解散' : '解散失败' };
|
|
186
|
-
},
|
|
187
|
-
}, plugin.name));
|
|
188
|
-
disposers.push(toolService.addTool({
|
|
189
|
-
name: 'lark_upload_file',
|
|
190
|
-
description: '上传文件到飞书(opus/mp4/pdf/doc/xls/ppt/stream)',
|
|
191
|
-
parameters: {
|
|
192
|
-
type: 'object',
|
|
193
|
-
properties: {
|
|
194
|
-
endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
|
|
195
|
-
file_path: { type: 'string', description: '本地文件路径' },
|
|
196
|
-
file_type: { type: 'string', description: '文件类型', enum: ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'] },
|
|
197
|
-
},
|
|
198
|
-
required: ['endpoint_id', 'file_path', 'file_type'],
|
|
199
|
-
},
|
|
200
|
-
platforms: ['lark'],
|
|
201
|
-
tags: ['lark'],
|
|
202
|
-
execute: async (args) => {
|
|
203
|
-
const validTypes = ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'];
|
|
204
|
-
if (!validTypes.includes(args.file_type)) {
|
|
205
|
-
throw new Error(`不支持的文件类型: ${args.file_type},支持: ${validTypes.join(', ')}`);
|
|
206
|
-
}
|
|
207
|
-
const endpoint = lark.endpoints.get(args.endpoint_id);
|
|
208
|
-
if (!endpoint)
|
|
209
|
-
throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
210
|
-
const result = await endpoint.uploadFile(args.file_path, args.file_type);
|
|
211
|
-
return { success: true, file_key: result, message: `文件已上传,file_key: ${result}` };
|
|
212
|
-
},
|
|
213
|
-
}, plugin.name));
|
|
214
|
-
return () => disposers.forEach(d => d());
|
|
215
|
-
});
|
|
216
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, resolveTarget, verifySignature, } from './protocol.js';
|
|
2
|
+
export { getLarkAgentDeps, registerLarkAgentEndpoint, setLarkAgentDeps, } from './lark-agent-deps.js';
|
|
3
|
+
export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, registerLarkPlatformPermitChecker, } from './platform-permit.js';
|
|
4
|
+
export { LarkEndpoint, } from './endpoint.js';
|
|
5
|
+
export { registerLarkWebhookRoutes, handleLarkWebhookRequest, } from './webhook.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for lark (user / chat / managers / upload).
|
|
3
|
+
* Endpoints register themselves on start; tools look up by endpoint id.
|
|
4
|
+
*/
|
|
5
|
+
export interface LarkAgentEndpoint {
|
|
6
|
+
getUserInfo(userId: string, userIdType?: 'open_id' | 'user_id' | 'union_id'): Promise<unknown>;
|
|
7
|
+
getChatInfo(chatId: string): Promise<unknown>;
|
|
8
|
+
uploadFile(filePath: string, fileType?: 'image' | 'file' | 'video' | 'audio'): Promise<string | null>;
|
|
9
|
+
createChat(name: string, userIds: string[], ownerId?: string): Promise<string | null>;
|
|
10
|
+
updateChatInfo(chatId: string, options: {
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}): Promise<boolean>;
|
|
14
|
+
addChatMembers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
15
|
+
removeChatMembers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
16
|
+
getChatMembers(chatId: string): Promise<unknown[]>;
|
|
17
|
+
dissolveChat(chatId: string): Promise<boolean>;
|
|
18
|
+
setChatManagers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
19
|
+
removeChatManagers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
export interface LarkAgentDeps {
|
|
22
|
+
getEndpoint: (endpointId: string) => LarkAgentEndpoint;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerLarkAgentEndpoint(endpointId: string, endpoint: LarkAgentEndpoint): () => void;
|
|
25
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
26
|
+
export declare function setLarkAgentDeps(deps: LarkAgentDeps | null): void;
|
|
27
|
+
export declare function getLarkAgentDeps(): LarkAgentDeps;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for lark (user / chat / managers / upload).
|
|
3
|
+
* Endpoints register themselves on start; tools look up by endpoint id.
|
|
4
|
+
*/
|
|
5
|
+
const endpoints = new Map();
|
|
6
|
+
let override = null;
|
|
7
|
+
export function registerLarkAgentEndpoint(endpointId, endpoint) {
|
|
8
|
+
endpoints.set(endpointId, endpoint);
|
|
9
|
+
return () => {
|
|
10
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
11
|
+
endpoints.delete(endpointId);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
16
|
+
export function setLarkAgentDeps(deps) {
|
|
17
|
+
override = deps;
|
|
18
|
+
}
|
|
19
|
+
export function getLarkAgentDeps() {
|
|
20
|
+
if (override)
|
|
21
|
+
return override;
|
|
22
|
+
return {
|
|
23
|
+
getEndpoint(endpointId) {
|
|
24
|
+
const endpoint = endpoints.get(endpointId);
|
|
25
|
+
if (!endpoint)
|
|
26
|
+
throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
27
|
+
return endpoint;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
package/lib/platform-permit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 飞书 Lark platform permit
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type Message } from '@zhin.js/core';
|
|
5
5
|
export declare function platformPermit(perm: string): string;
|
|
6
6
|
export declare function larkGroupPermitResolver(logicalPerm: string): string;
|
|
7
7
|
export declare function normalizeLarkSenderForPermit(input: {
|
|
@@ -13,4 +13,3 @@ export declare function normalizeLarkSenderForPermit(input: {
|
|
|
13
13
|
};
|
|
14
14
|
export declare function checkLarkPlatformPermit(perm: string, message: Message<any>): boolean;
|
|
15
15
|
export declare function registerLarkPlatformPermitChecker(): () => void;
|
|
16
|
-
//# sourceMappingURL=platform-permit.d.ts.map
|
package/lib/platform-permit.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 飞书 Lark platform permit
|
|
3
|
+
*/
|
|
4
|
+
import { registerPlatformPermitChecker } from '@zhin.js/core';
|
|
2
5
|
const ADAPTER = 'lark';
|
|
3
6
|
export function platformPermit(perm) {
|
|
4
7
|
return `platform(${ADAPTER},${perm})`;
|
|
@@ -38,4 +41,3 @@ export function checkLarkPlatformPermit(perm, message) {
|
|
|
38
41
|
export function registerLarkPlatformPermitChecker() {
|
|
39
42
|
return registerPlatformPermitChecker(ADAPTER, checkLarkPlatformPermit);
|
|
40
43
|
}
|
|
41
|
-
//# sourceMappingURL=platform-permit.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
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 type { IncomingMessage } from 'node:http';
|
|
6
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
7
|
+
export interface LarkAdapterConfig {
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
readonly appId?: string;
|
|
10
|
+
readonly appSecret?: string;
|
|
11
|
+
readonly encryptKey?: string;
|
|
12
|
+
readonly verificationToken?: string;
|
|
13
|
+
readonly webhookPath?: string;
|
|
14
|
+
readonly apiBaseUrl?: string;
|
|
15
|
+
readonly isFeishu?: boolean;
|
|
16
|
+
/** Transitional: legacy root `endpoints[]` with `context: lark`. */
|
|
17
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedLarkConfig> & {
|
|
18
|
+
readonly context?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedLarkConfig {
|
|
22
|
+
readonly context: 'lark';
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly appId: string;
|
|
25
|
+
readonly appSecret: string;
|
|
26
|
+
readonly encryptKey?: string;
|
|
27
|
+
readonly verificationToken?: string;
|
|
28
|
+
readonly webhookPath: string;
|
|
29
|
+
readonly apiBaseUrl: string;
|
|
30
|
+
readonly isFeishu: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface LarkMessage {
|
|
33
|
+
readonly message_id?: string;
|
|
34
|
+
readonly root_id?: string;
|
|
35
|
+
readonly parent_id?: string;
|
|
36
|
+
readonly create_time?: string;
|
|
37
|
+
readonly update_time?: string;
|
|
38
|
+
readonly chat_id?: string;
|
|
39
|
+
readonly sender?: {
|
|
40
|
+
readonly sender_id?: {
|
|
41
|
+
readonly user_id?: string;
|
|
42
|
+
readonly open_id?: string;
|
|
43
|
+
readonly union_id?: string;
|
|
44
|
+
};
|
|
45
|
+
readonly sender_type?: string;
|
|
46
|
+
readonly tenant_key?: string;
|
|
47
|
+
};
|
|
48
|
+
readonly message_type?: string;
|
|
49
|
+
readonly content?: string;
|
|
50
|
+
readonly mentions?: ReadonlyArray<{
|
|
51
|
+
readonly key?: string;
|
|
52
|
+
readonly id?: {
|
|
53
|
+
readonly user_id?: string;
|
|
54
|
+
readonly open_id?: string;
|
|
55
|
+
readonly union_id?: string;
|
|
56
|
+
};
|
|
57
|
+
readonly name?: string;
|
|
58
|
+
readonly tenant_key?: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export interface LarkEventBody {
|
|
62
|
+
readonly uuid?: string;
|
|
63
|
+
readonly token?: string;
|
|
64
|
+
readonly ts?: string;
|
|
65
|
+
readonly type?: string;
|
|
66
|
+
readonly challenge?: string;
|
|
67
|
+
readonly event?: {
|
|
68
|
+
readonly sender?: LarkMessage['sender'];
|
|
69
|
+
readonly message?: LarkMessage;
|
|
70
|
+
readonly [key: string]: unknown;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface AccessToken {
|
|
74
|
+
token: string;
|
|
75
|
+
expires_in: number;
|
|
76
|
+
timestamp: number;
|
|
77
|
+
}
|
|
78
|
+
export interface LarkApiResponse {
|
|
79
|
+
readonly code: number;
|
|
80
|
+
readonly msg?: string;
|
|
81
|
+
readonly tenant_access_token?: string;
|
|
82
|
+
readonly expire?: number;
|
|
83
|
+
readonly data?: Record<string, unknown>;
|
|
84
|
+
readonly [key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
export interface LarkWireSegment {
|
|
87
|
+
readonly type: string;
|
|
88
|
+
readonly data?: Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
export interface LarkSendBody {
|
|
91
|
+
readonly msg_type: string;
|
|
92
|
+
readonly content: string;
|
|
93
|
+
}
|
|
94
|
+
export declare function resolveLarkConfig(config?: LarkAdapterConfig): ResolvedLarkConfig;
|
|
95
|
+
export declare function normalizeWebhookPath(path: string): string;
|
|
96
|
+
export declare function resolveChatType(chatId?: string): 'group' | 'private';
|
|
97
|
+
export declare function resolveTarget(msg: LarkMessage): string;
|
|
98
|
+
export declare function resolveSender(msg: LarkMessage): string;
|
|
99
|
+
export declare function generateMessageId(msg: LarkMessage): string;
|
|
100
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
101
|
+
export declare function formatInboundContent(msg: LarkMessage): string;
|
|
102
|
+
export declare function verifySignature(encryptKey: string, timestamp: string, nonce: string, body: string, signature: string): boolean;
|
|
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 declare function formatOutboundBody(payload: unknown): LarkSendBody;
|
|
108
|
+
export declare function headerValue(headers: IncomingMessage['headers'], name: string): string;
|
|
109
|
+
export declare function readTextBody(request: IncomingMessage, options?: {
|
|
110
|
+
readonly limit?: number;
|
|
111
|
+
}): Promise<string>;
|
package/lib/protocol.js
ADDED
|
@@ -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
|
+
}
|
package/lib/webhook.d.ts
ADDED
|
@@ -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>;
|