@zhin.js/adapter-lark 1.0.97 → 1.1.2
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 +729 -3
- package/PERMITS.md +22 -0
- package/README.md +69 -83
- package/adapters/lark/index.js +36 -0
- package/adapters/lark/index.ts +41 -0
- package/agents/lark/agent.json +20 -0
- package/agents/lark/boundaries.md +3 -0
- package/agents/lark/conventions.md +3 -0
- package/agents/lark/skills/lark/SKILL.md +22 -0
- package/agents/lark/skills/lark/tools/add_members/index.js +19 -0
- package/agents/lark/skills/lark/tools/add_members/index.ts +19 -0
- package/agents/lark/skills/lark/tools/create_chat/index.js +18 -0
- package/agents/lark/skills/lark/tools/create_chat/index.ts +18 -0
- package/agents/lark/skills/lark/tools/dissolve_chat/index.js +16 -0
- package/agents/lark/skills/lark/tools/dissolve_chat/index.ts +16 -0
- package/agents/lark/skills/lark/tools/get_user/index.js +15 -0
- package/agents/lark/skills/lark/tools/get_user/index.ts +16 -0
- package/agents/lark/skills/lark/tools/remove_managers/index.js +19 -0
- package/agents/lark/skills/lark/tools/remove_managers/index.ts +19 -0
- package/agents/lark/skills/lark/tools/set_managers/index.js +19 -0
- package/agents/lark/skills/lark/tools/set_managers/index.ts +19 -0
- package/agents/lark/skills/lark/tools/update_chat/index.js +20 -0
- package/agents/lark/skills/lark/tools/update_chat/index.ts +20 -0
- package/agents/lark/skills/lark/tools/upload_file/index.js +17 -0
- package/agents/lark/skills/lark/tools/upload_file/index.ts +23 -0
- package/agents/lark/system.md +3 -0
- package/commands/lark/endpoint/add/[id]/index.js +3 -0
- package/commands/lark/endpoint/add/[id]/index.ts +3 -0
- package/commands/lark/endpoint/definition.js +20 -0
- package/commands/lark/endpoint/definition.ts +20 -0
- package/commands/lark/endpoint/list/index.js +3 -0
- package/commands/lark/endpoint/list/index.ts +3 -0
- package/commands/lark/endpoint/remove/[id]/index.js +3 -0
- package/commands/lark/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +12 -0
- package/lib/client.js +2 -0
- package/lib/endpoint.d.ts +100 -0
- package/lib/endpoint.js +497 -0
- package/lib/index.d.ts +6 -15
- package/lib/index.js +6 -209
- package/lib/lark-runtime-state.d.ts +1 -0
- package/lib/lark-runtime-state.js +6 -0
- package/lib/media-upload.d.ts +15 -0
- package/lib/media-upload.js +58 -0
- package/lib/platform-permit.d.ts +14 -0
- package/lib/platform-permit.js +35 -0
- package/lib/protocol.d.ts +119 -0
- package/lib/protocol.js +272 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +73 -0
- package/package.json +76 -21
- package/plugin.js +19 -0
- package/schema.json +110 -0
- package/src/client.ts +16 -0
- package/src/endpoint.ts +590 -0
- package/src/index.ts +51 -227
- package/src/lark-runtime-state.ts +7 -0
- package/src/media-upload.ts +69 -0
- package/src/platform-permit.ts +50 -0
- package/src/protocol.ts +396 -0
- package/src/webhook.ts +110 -0
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -45
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -82
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -638
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.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/skills/lark/SKILL.md +0 -89
- package/src/adapter.ts +0 -55
- package/src/bot.ts +0 -712
- package/src/types.ts +0 -58
package/lib/index.js
CHANGED
|
@@ -1,209 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export { LarkBot } from "./bot.js";
|
|
8
|
-
export { LarkAdapter } from "./adapter.js";
|
|
9
|
-
const plugin = usePlugin();
|
|
10
|
-
const { provide, useContext } = plugin;
|
|
11
|
-
useContext("router", (router) => {
|
|
12
|
-
provide({
|
|
13
|
-
name: "lark",
|
|
14
|
-
description: "Lark/Feishu Bot Adapter",
|
|
15
|
-
mounted: async (p) => {
|
|
16
|
-
const adapter = new LarkAdapter(p, router);
|
|
17
|
-
await adapter.start();
|
|
18
|
-
return adapter;
|
|
19
|
-
},
|
|
20
|
-
dispose: async (adapter) => {
|
|
21
|
-
await adapter.stop();
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
useContext('tool', 'lark', (toolService, lark) => {
|
|
26
|
-
const groupTools = createGroupManagementTools(lark, 'lark');
|
|
27
|
-
const disposers = groupTools.map(t => toolService.addTool(t, plugin.name));
|
|
28
|
-
disposers.push(toolService.addTool({
|
|
29
|
-
name: 'lark_get_user',
|
|
30
|
-
description: '获取飞书用户信息',
|
|
31
|
-
parameters: {
|
|
32
|
-
type: 'object',
|
|
33
|
-
properties: {
|
|
34
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
35
|
-
user_id: { type: 'string', description: '用户 ID (open_id)' },
|
|
36
|
-
},
|
|
37
|
-
required: ['bot', 'user_id'],
|
|
38
|
-
},
|
|
39
|
-
platforms: ['lark'],
|
|
40
|
-
tags: ['lark'],
|
|
41
|
-
execute: async (args) => {
|
|
42
|
-
const bot = lark.bots.get(args.bot);
|
|
43
|
-
if (!bot)
|
|
44
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
45
|
-
return await bot.getUserInfo(args.user_id);
|
|
46
|
-
},
|
|
47
|
-
}, plugin.name));
|
|
48
|
-
disposers.push(toolService.addTool({
|
|
49
|
-
name: 'lark_create_chat',
|
|
50
|
-
description: '创建飞书群聊',
|
|
51
|
-
parameters: {
|
|
52
|
-
type: 'object',
|
|
53
|
-
properties: {
|
|
54
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
55
|
-
name: { type: 'string', description: '群名' },
|
|
56
|
-
members: { type: 'string', description: '成员 open_id 列表,逗号分隔' },
|
|
57
|
-
owner: { type: 'string', description: '群主 open_id(可选)' },
|
|
58
|
-
},
|
|
59
|
-
required: ['bot', 'name', 'members'],
|
|
60
|
-
},
|
|
61
|
-
platforms: ['lark'],
|
|
62
|
-
tags: ['lark'],
|
|
63
|
-
execute: async (args) => {
|
|
64
|
-
const bot = lark.bots.get(args.bot);
|
|
65
|
-
if (!bot)
|
|
66
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
67
|
-
const chatId = await bot.createChat(args.name, args.members.split(','), args.owner);
|
|
68
|
-
return { success: !!chatId, chat_id: chatId, message: chatId ? `群聊创建成功: ${chatId}` : '创建失败' };
|
|
69
|
-
},
|
|
70
|
-
}, plugin.name));
|
|
71
|
-
disposers.push(toolService.addTool({
|
|
72
|
-
name: 'lark_update_chat',
|
|
73
|
-
description: '更新飞书群聊信息(群名、描述)',
|
|
74
|
-
parameters: {
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {
|
|
77
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
78
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
79
|
-
name: { type: 'string', description: '新群名(可选)' },
|
|
80
|
-
description: { type: 'string', description: '新描述(可选)' },
|
|
81
|
-
},
|
|
82
|
-
required: ['bot', 'chat_id'],
|
|
83
|
-
},
|
|
84
|
-
platforms: ['lark'],
|
|
85
|
-
tags: ['lark'],
|
|
86
|
-
execute: async (args) => {
|
|
87
|
-
const bot = lark.bots.get(args.bot);
|
|
88
|
-
if (!bot)
|
|
89
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
90
|
-
const success = await bot.updateChatInfo(args.chat_id, { name: args.name, description: args.description });
|
|
91
|
-
return { success, message: success ? '群信息更新成功' : '更新失败' };
|
|
92
|
-
},
|
|
93
|
-
}, plugin.name));
|
|
94
|
-
disposers.push(toolService.addTool({
|
|
95
|
-
name: 'lark_add_members',
|
|
96
|
-
description: '添加飞书群成员',
|
|
97
|
-
parameters: {
|
|
98
|
-
type: 'object',
|
|
99
|
-
properties: {
|
|
100
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
101
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
102
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
103
|
-
},
|
|
104
|
-
required: ['bot', 'chat_id', 'user_ids'],
|
|
105
|
-
},
|
|
106
|
-
platforms: ['lark'],
|
|
107
|
-
tags: ['lark'],
|
|
108
|
-
execute: async (args) => {
|
|
109
|
-
const bot = lark.bots.get(args.bot);
|
|
110
|
-
if (!bot)
|
|
111
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
112
|
-
const success = await bot.addChatMembers(args.chat_id, args.user_ids.split(','));
|
|
113
|
-
return { success, message: success ? '成员添加成功' : '添加失败' };
|
|
114
|
-
},
|
|
115
|
-
}, plugin.name));
|
|
116
|
-
disposers.push(toolService.addTool({
|
|
117
|
-
name: 'lark_set_managers',
|
|
118
|
-
description: '设置飞书群管理员',
|
|
119
|
-
parameters: {
|
|
120
|
-
type: 'object',
|
|
121
|
-
properties: {
|
|
122
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
123
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
124
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
125
|
-
},
|
|
126
|
-
required: ['bot', 'chat_id', 'user_ids'],
|
|
127
|
-
},
|
|
128
|
-
platforms: ['lark'],
|
|
129
|
-
tags: ['lark'],
|
|
130
|
-
execute: async (args) => {
|
|
131
|
-
const bot = lark.bots.get(args.bot);
|
|
132
|
-
if (!bot)
|
|
133
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
134
|
-
const success = await bot.setChatManagers(args.chat_id, args.user_ids.split(','));
|
|
135
|
-
return { success, message: success ? '管理员设置成功' : '设置失败' };
|
|
136
|
-
},
|
|
137
|
-
}, plugin.name));
|
|
138
|
-
disposers.push(toolService.addTool({
|
|
139
|
-
name: 'lark_remove_managers',
|
|
140
|
-
description: '移除飞书群管理员',
|
|
141
|
-
parameters: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {
|
|
144
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
145
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
146
|
-
user_ids: { type: 'string', description: '用户 open_id 列表,逗号分隔' },
|
|
147
|
-
},
|
|
148
|
-
required: ['bot', 'chat_id', 'user_ids'],
|
|
149
|
-
},
|
|
150
|
-
platforms: ['lark'],
|
|
151
|
-
tags: ['lark'],
|
|
152
|
-
execute: async (args) => {
|
|
153
|
-
const bot = lark.bots.get(args.bot);
|
|
154
|
-
if (!bot)
|
|
155
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
156
|
-
const success = await bot.removeChatManagers(args.chat_id, args.user_ids.split(','));
|
|
157
|
-
return { success, message: success ? '管理员移除成功' : '移除失败' };
|
|
158
|
-
},
|
|
159
|
-
}, plugin.name));
|
|
160
|
-
disposers.push(toolService.addTool({
|
|
161
|
-
name: 'lark_dissolve_chat',
|
|
162
|
-
description: '解散飞书群聊(需要群主权限)',
|
|
163
|
-
parameters: {
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: {
|
|
166
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
167
|
-
chat_id: { type: 'string', description: '群聊 ID' },
|
|
168
|
-
},
|
|
169
|
-
required: ['bot', 'chat_id'],
|
|
170
|
-
},
|
|
171
|
-
platforms: ['lark'],
|
|
172
|
-
tags: ['lark'],
|
|
173
|
-
execute: async (args) => {
|
|
174
|
-
const bot = lark.bots.get(args.bot);
|
|
175
|
-
if (!bot)
|
|
176
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
177
|
-
const success = await bot.dissolveChat(args.chat_id);
|
|
178
|
-
return { success, message: success ? '群聊已解散' : '解散失败' };
|
|
179
|
-
},
|
|
180
|
-
}, plugin.name));
|
|
181
|
-
disposers.push(toolService.addTool({
|
|
182
|
-
name: 'lark_upload_file',
|
|
183
|
-
description: '上传文件到飞书(opus/mp4/pdf/doc/xls/ppt/stream)',
|
|
184
|
-
parameters: {
|
|
185
|
-
type: 'object',
|
|
186
|
-
properties: {
|
|
187
|
-
bot: { type: 'string', description: 'Bot 名称' },
|
|
188
|
-
file_path: { type: 'string', description: '本地文件路径' },
|
|
189
|
-
file_type: { type: 'string', description: '文件类型', enum: ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'] },
|
|
190
|
-
},
|
|
191
|
-
required: ['bot', 'file_path', 'file_type'],
|
|
192
|
-
},
|
|
193
|
-
platforms: ['lark'],
|
|
194
|
-
tags: ['lark'],
|
|
195
|
-
execute: async (args) => {
|
|
196
|
-
const validTypes = ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'];
|
|
197
|
-
if (!validTypes.includes(args.file_type)) {
|
|
198
|
-
throw new Error(`不支持的文件类型: ${args.file_type},支持: ${validTypes.join(', ')}`);
|
|
199
|
-
}
|
|
200
|
-
const bot = lark.bots.get(args.bot);
|
|
201
|
-
if (!bot)
|
|
202
|
-
throw new Error(`Bot ${args.bot} 不存在`);
|
|
203
|
-
const result = await bot.uploadFile(args.file_path, args.file_type);
|
|
204
|
-
return { success: true, file_key: result, message: `文件已上传,file_key: ${result}` };
|
|
205
|
-
},
|
|
206
|
-
}, plugin.name));
|
|
207
|
-
return () => disposers.forEach(d => d());
|
|
208
|
-
});
|
|
209
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, } from './protocol.js';
|
|
2
|
+
export { larkClient } from './client.js';
|
|
3
|
+
export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
|
+
export { LarkClient, LarkEndpoint, } from './endpoint.js';
|
|
5
|
+
export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
|
|
6
|
+
export { registerLarkWebhookRoutes, handleLarkWebhookRequest, } from './webhook.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const larkRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `lark endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
|
+
export const larkRuntimeStateToken = defineEndpointRuntimeStateToken('lark');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MediaRef } from '@zhin.js/im-contract';
|
|
2
|
+
export interface MediaBinary {
|
|
3
|
+
readonly data: Buffer;
|
|
4
|
+
readonly mimeType: string;
|
|
5
|
+
readonly fileName: string;
|
|
6
|
+
}
|
|
7
|
+
/** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
|
|
8
|
+
export declare function resolveMediaBinary(media: MediaRef, download?: (url: string) => Promise<Buffer>): Promise<MediaBinary>;
|
|
9
|
+
/** /im/v1/images 的 multipart body(image_type=message 用于消息内图片)。 */
|
|
10
|
+
export declare function buildImageUploadForm(binary: MediaBinary): FormData;
|
|
11
|
+
/**
|
|
12
|
+
* 出站 image 段待上传的媒体引用(canonical `data.media` 唯一来源):
|
|
13
|
+
* kind=file 为平台不透明引用(image_key/file_key),已物化无需再上传,返回 undefined。
|
|
14
|
+
*/
|
|
15
|
+
export declare function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark 图片上传(im/v1 消息 image 段需要 image_key)。
|
|
3
|
+
* 接口:POST {apiBaseUrl}/im/v1/images(multipart:image_type=message + image)
|
|
4
|
+
* 与 im/v1/messages 同属 IM 接口域(同一 tenant_access_token),不引入额外授权域。
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { isMediaRef } from '@zhin.js/im-contract';
|
|
9
|
+
const MIME_EXT = {
|
|
10
|
+
'image/jpeg': 'jpg',
|
|
11
|
+
'image/png': 'png',
|
|
12
|
+
'image/gif': 'gif',
|
|
13
|
+
'image/webp': 'webp',
|
|
14
|
+
'image/bmp': 'bmp',
|
|
15
|
+
};
|
|
16
|
+
/** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
|
|
17
|
+
export async function resolveMediaBinary(media, download = defaultDownload) {
|
|
18
|
+
const mimeType = media.mime_type ?? 'image/png';
|
|
19
|
+
const ext = MIME_EXT[mimeType] ?? 'png';
|
|
20
|
+
if (media.kind === 'base64') {
|
|
21
|
+
const value = media.value.startsWith('base64://')
|
|
22
|
+
? media.value.slice('base64://'.length)
|
|
23
|
+
: media.value;
|
|
24
|
+
return { data: Buffer.from(value, 'base64'), mimeType, fileName: `image.${ext}` };
|
|
25
|
+
}
|
|
26
|
+
if (media.kind === 'path') {
|
|
27
|
+
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
28
|
+
return { data: await readFile(path), mimeType, fileName: basename(path) };
|
|
29
|
+
}
|
|
30
|
+
return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
|
|
31
|
+
}
|
|
32
|
+
async function defaultDownload(url) {
|
|
33
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new Error(`download failed: HTTP ${response.status}`);
|
|
36
|
+
return Buffer.from(await response.arrayBuffer());
|
|
37
|
+
}
|
|
38
|
+
/** /im/v1/images 的 multipart body(image_type=message 用于消息内图片)。 */
|
|
39
|
+
export function buildImageUploadForm(binary) {
|
|
40
|
+
// Buffer 的 ArrayBufferLike 不满足 BlobPart(SharedArrayBuffer 分支),拷贝为 Uint8Array<ArrayBuffer>
|
|
41
|
+
const bytes = new Uint8Array(binary.data.byteLength);
|
|
42
|
+
bytes.set(binary.data);
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.append('image_type', 'message');
|
|
45
|
+
form.append('image', new Blob([bytes], { type: binary.mimeType }), binary.fileName);
|
|
46
|
+
return form;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 出站 image 段待上传的媒体引用(canonical `data.media` 唯一来源):
|
|
50
|
+
* kind=file 为平台不透明引用(image_key/file_key),已物化无需再上传,返回 undefined。
|
|
51
|
+
*/
|
|
52
|
+
export function readOutboundImageMedia(data) {
|
|
53
|
+
if (!isMediaRef(data.media))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (data.media.kind === 'file')
|
|
56
|
+
return undefined;
|
|
57
|
+
return data.media;
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书 Lark platform permit
|
|
3
|
+
*/
|
|
4
|
+
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
|
+
export declare function platformPermit(perm: string): string;
|
|
6
|
+
export declare function larkGroupPermitResolver(logicalPerm: string): string;
|
|
7
|
+
export declare function normalizeLarkSenderForPermit(input: {
|
|
8
|
+
isOwner?: boolean;
|
|
9
|
+
isAdmin?: boolean;
|
|
10
|
+
}): {
|
|
11
|
+
role?: string;
|
|
12
|
+
permissions?: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function checkLarkPlatformPermit(perm: string, subject: PermissionSubject): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const ADAPTER = 'lark';
|
|
2
|
+
export function platformPermit(perm) {
|
|
3
|
+
return `platform(${ADAPTER},${perm})`;
|
|
4
|
+
}
|
|
5
|
+
const FACTORY_PERM_MAP = {
|
|
6
|
+
scene_admin: 'chat_admin',
|
|
7
|
+
scene_owner: 'chat_owner',
|
|
8
|
+
};
|
|
9
|
+
export function larkGroupPermitResolver(logicalPerm) {
|
|
10
|
+
return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
|
|
11
|
+
}
|
|
12
|
+
export function normalizeLarkSenderForPermit(input) {
|
|
13
|
+
if (input.isOwner) {
|
|
14
|
+
return { role: 'owner', permissions: ['chat_owner', 'chat_admin', 'manage_managers'] };
|
|
15
|
+
}
|
|
16
|
+
if (input.isAdmin) {
|
|
17
|
+
return { role: 'admin', permissions: ['chat_admin'] };
|
|
18
|
+
}
|
|
19
|
+
return { role: 'member', permissions: [] };
|
|
20
|
+
}
|
|
21
|
+
export function checkLarkPlatformPermit(perm, subject) {
|
|
22
|
+
const role = subject.sender?.role?.[0];
|
|
23
|
+
const permissions = subject.sender?.permissions ?? [];
|
|
24
|
+
const has = (t) => permissions.includes(t);
|
|
25
|
+
switch (perm) {
|
|
26
|
+
case 'chat_owner':
|
|
27
|
+
return role === 'owner' || has('chat_owner');
|
|
28
|
+
case 'chat_admin':
|
|
29
|
+
return role === 'owner' || role === 'admin' || has('chat_admin') || has('chat_owner');
|
|
30
|
+
case 'manage_managers':
|
|
31
|
+
return role === 'owner' || has('chat_owner');
|
|
32
|
+
default:
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
import { type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
8
|
+
export interface LarkEndpointConfig {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly appId: string;
|
|
11
|
+
readonly appSecret: string;
|
|
12
|
+
readonly encryptKey?: string;
|
|
13
|
+
readonly verificationToken?: string;
|
|
14
|
+
readonly webhookPath?: string;
|
|
15
|
+
readonly apiBaseUrl?: string;
|
|
16
|
+
readonly isFeishu?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedLarkConfig {
|
|
19
|
+
readonly context: 'lark';
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly appId: string;
|
|
22
|
+
readonly appSecret: string;
|
|
23
|
+
readonly encryptKey?: string;
|
|
24
|
+
readonly verificationToken?: string;
|
|
25
|
+
readonly webhookPath: string;
|
|
26
|
+
readonly apiBaseUrl: string;
|
|
27
|
+
readonly isFeishu: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface LarkMessage {
|
|
30
|
+
readonly message_id?: string;
|
|
31
|
+
readonly root_id?: string;
|
|
32
|
+
readonly parent_id?: string;
|
|
33
|
+
readonly create_time?: string;
|
|
34
|
+
readonly update_time?: string;
|
|
35
|
+
readonly chat_id?: string;
|
|
36
|
+
/** 事件携带的会话类型('p2p' | 'group');p2p 的 chat_id 也是 oc_ 开头。 */
|
|
37
|
+
readonly chat_type?: string;
|
|
38
|
+
readonly sender?: {
|
|
39
|
+
readonly sender_id?: {
|
|
40
|
+
readonly user_id?: string;
|
|
41
|
+
readonly open_id?: string;
|
|
42
|
+
readonly union_id?: string;
|
|
43
|
+
};
|
|
44
|
+
readonly sender_type?: string;
|
|
45
|
+
readonly tenant_key?: string;
|
|
46
|
+
};
|
|
47
|
+
readonly message_type?: string;
|
|
48
|
+
readonly content?: string;
|
|
49
|
+
readonly mentions?: ReadonlyArray<{
|
|
50
|
+
readonly key?: string;
|
|
51
|
+
readonly id?: {
|
|
52
|
+
readonly user_id?: string;
|
|
53
|
+
readonly open_id?: string;
|
|
54
|
+
readonly union_id?: string;
|
|
55
|
+
};
|
|
56
|
+
readonly name?: string;
|
|
57
|
+
readonly tenant_key?: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
export interface LarkEventBody {
|
|
61
|
+
readonly uuid?: string;
|
|
62
|
+
readonly token?: string;
|
|
63
|
+
readonly ts?: string;
|
|
64
|
+
readonly type?: string;
|
|
65
|
+
readonly challenge?: string;
|
|
66
|
+
readonly event?: {
|
|
67
|
+
readonly sender?: LarkMessage['sender'];
|
|
68
|
+
readonly message?: LarkMessage;
|
|
69
|
+
readonly [key: string]: unknown;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface AccessToken {
|
|
73
|
+
token: string;
|
|
74
|
+
expires_in: number;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
}
|
|
77
|
+
export interface LarkApiResponse {
|
|
78
|
+
readonly code: number;
|
|
79
|
+
readonly msg?: string;
|
|
80
|
+
readonly tenant_access_token?: string;
|
|
81
|
+
readonly expire?: number;
|
|
82
|
+
readonly data?: Record<string, unknown>;
|
|
83
|
+
readonly [key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
export interface LarkWireSegment {
|
|
86
|
+
readonly type: string;
|
|
87
|
+
readonly data?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface LarkSendBody {
|
|
90
|
+
readonly msg_type: string;
|
|
91
|
+
readonly content: string;
|
|
92
|
+
}
|
|
93
|
+
export declare function resolveLarkConfig(config: LarkEndpointConfig): ResolvedLarkConfig;
|
|
94
|
+
export declare function normalizeWebhookPath(path: string): string;
|
|
95
|
+
export declare function resolveChatType(chatId?: string, chatType?: string): 'group' | 'private';
|
|
96
|
+
/**
|
|
97
|
+
* 入站归一化 → ConversationRef:`chat_type` 为 'group' 的会话映射为 group,
|
|
98
|
+
* p2p 单聊映射为 private;id 一律为平台 chat_id。Lark 无 guild/频道容器,
|
|
99
|
+
* 不产生 `parent`。
|
|
100
|
+
*/
|
|
101
|
+
export declare function larkInboundConversation(endpointKey: string, msg: LarkMessage): ConversationRef;
|
|
102
|
+
export declare function resolveSender(msg: LarkMessage): string;
|
|
103
|
+
export declare function generateMessageId(msg: LarkMessage): string;
|
|
104
|
+
/** Build inbound text for OutboundMessageService.receive. */
|
|
105
|
+
export declare function formatInboundContent(msg: LarkMessage): string;
|
|
106
|
+
/** 验签时间戳时效窗口(防重放)。 */
|
|
107
|
+
export declare const LARK_SIGNATURE_MAX_AGE_MS: number;
|
|
108
|
+
export declare function verifySignature(encryptKey: string, timestamp: string, nonce: string, body: string, signature: string, nowMs?: number): boolean;
|
|
109
|
+
/** timing-safe 比较 webhook verification token(长度不同快速失败)。 */
|
|
110
|
+
export declare function verifyToken(token: string, expected: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Wire-encode an already-rendered outbound payload into Lark im/v1 message body.
|
|
113
|
+
* Segment canonicalization is intentionally not done here.
|
|
114
|
+
*/
|
|
115
|
+
export declare function formatOutboundBody(payload: unknown): LarkSendBody;
|
|
116
|
+
export declare function headerValue(headers: IncomingMessage['headers'], name: string): string;
|
|
117
|
+
export declare function readTextBody(request: IncomingMessage, options?: {
|
|
118
|
+
readonly limit?: number;
|
|
119
|
+
}): Promise<string>;
|