@zhin.js/adapter-wecom 0.0.1 → 1.1.0

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 (66) hide show
  1. package/CHANGELOG.md +624 -0
  2. package/README.md +80 -216
  3. package/adapters/wecom.js +34 -0
  4. package/adapters/wecom.ts +39 -0
  5. package/{skills/wecom → agent}/PERMITS.md +1 -1
  6. package/agent/tools/get_dept_users.ts +16 -0
  7. package/agent/tools/get_user.ts +15 -0
  8. package/agent/tools/list_departments.ts +16 -0
  9. package/agent/tools/send_text.ts +17 -0
  10. package/commands/endpoint/add/[id].js +3 -0
  11. package/commands/endpoint/add/[id].ts +3 -0
  12. package/commands/endpoint/list.js +3 -0
  13. package/commands/endpoint/list.ts +3 -0
  14. package/commands/endpoint/remove/[id].js +3 -0
  15. package/commands/endpoint/remove/[id].ts +3 -0
  16. package/lib/client.d.ts +12 -0
  17. package/lib/client.js +2 -0
  18. package/lib/endpoint.d.ts +55 -37
  19. package/lib/endpoint.js +247 -516
  20. package/lib/index.d.ts +6 -15
  21. package/lib/index.js +6 -115
  22. package/lib/media-upload.d.ts +23 -0
  23. package/lib/media-upload.js +58 -0
  24. package/lib/platform-permit.d.ts +2 -4
  25. package/lib/platform-permit.js +5 -11
  26. package/lib/protocol.d.ts +102 -0
  27. package/lib/protocol.js +268 -0
  28. package/lib/side-event-dispatch.d.ts +4 -0
  29. package/lib/side-event-dispatch.js +63 -0
  30. package/lib/webhook.d.ts +14 -0
  31. package/lib/webhook.js +86 -0
  32. package/lib/wecom-endpoint-commands.d.ts +1 -0
  33. package/lib/wecom-endpoint-commands.js +19 -0
  34. package/lib/wecom-runtime-state.d.ts +1 -0
  35. package/lib/wecom-runtime-state.js +6 -0
  36. package/package.json +64 -16
  37. package/plugin.js +19 -0
  38. package/schema.json +96 -0
  39. package/src/client.ts +16 -0
  40. package/src/endpoint.ts +311 -559
  41. package/src/index.ts +52 -131
  42. package/src/media-upload.ts +79 -0
  43. package/src/platform-permit.ts +6 -11
  44. package/src/protocol.ts +379 -0
  45. package/src/side-event-dispatch.ts +70 -0
  46. package/src/webhook.ts +130 -0
  47. package/src/wecom-endpoint-commands.ts +20 -0
  48. package/src/wecom-runtime-state.ts +7 -0
  49. package/lib/adapter.d.ts +0 -15
  50. package/lib/adapter.d.ts.map +0 -1
  51. package/lib/adapter.js +0 -20
  52. package/lib/adapter.js.map +0 -1
  53. package/lib/endpoint.d.ts.map +0 -1
  54. package/lib/endpoint.js.map +0 -1
  55. package/lib/index.d.ts.map +0 -1
  56. package/lib/index.js.map +0 -1
  57. package/lib/platform-permit.d.ts.map +0 -1
  58. package/lib/platform-permit.js.map +0 -1
  59. package/lib/types.d.ts +0 -48
  60. package/lib/types.d.ts.map +0 -1
  61. package/lib/types.js +0 -5
  62. package/lib/types.js.map +0 -1
  63. package/plugin.yml +0 -3
  64. package/src/adapter.ts +0 -26
  65. package/src/types.ts +0 -51
  66. /package/{skills/wecom/SKILL.md → agent/skills/wecom.md} +0 -0
package/lib/index.d.ts CHANGED
@@ -1,15 +1,6 @@
1
- import { WecomAdapter } from './adapter.js';
2
- declare module 'zhin.js' {
3
- namespace Plugin {
4
- interface Contexts {
5
- router: import('@zhin.js/host-router').Router;
6
- }
7
- }
8
- interface Adapters {
9
- wecom: WecomAdapter;
10
- }
11
- }
12
- export * from './types.js';
13
- export { WecomEndpoint } from './endpoint.js';
14
- export { WecomAdapter } from './adapter.js';
15
- //# sourceMappingURL=index.d.ts.map
1
+ export { buildSendRequestBody, decryptMessage, extractEncryptFromXml, formatInboundContent, formatOutboundBody, getAesKey, normalizeEchostrParam, normalizeWebhookPath, parseXmlMessage, queryParam, readTextBody, resolveChatType, resolveWecomConfig, verifySignature, type AccessToken, type ResolvedWecomConfig, type WecomAdapterConfig, type WecomApiResponse, type WecomMessage, type WecomSendBody, type WecomWireSegment, } from './protocol.js';
2
+ export { wecomClient, type WecomClientEventMap } from './client.js';
3
+ export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, wecomGroupPermitResolver, } from './platform-permit.js';
4
+ export { WecomClient, WecomEndpoint, type WecomClientApi, type WecomEndpointOptions, type WecomFetch, } from './endpoint.js';
5
+ export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, type WecomMediaUploadResult, } from './media-upload.js';
6
+ export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, type WecomWebhookHandler, } from './webhook.js';
package/lib/index.js CHANGED
@@ -1,115 +1,6 @@
1
- /**
2
- * 企业微信适配器入口:类型扩展、导出、注册
3
- */
4
- import { usePlugin } from 'zhin.js';
5
- import { WecomAdapter } from './adapter.js';
6
- import { registerWecomPlatformPermitChecker, } from './platform-permit.js';
7
- export * from './types.js';
8
- export { WecomEndpoint } from './endpoint.js';
9
- export { WecomAdapter } from './adapter.js';
10
- const plugin = usePlugin();
11
- const { provide, useContext } = plugin;
12
- useContext('router', (router) => {
13
- provide({
14
- name: 'wecom',
15
- description: 'WeCom (企业微信) Endpoint Adapter',
16
- mounted: async (p) => {
17
- const adapter = new WecomAdapter(p, router);
18
- await adapter.start();
19
- return adapter;
20
- },
21
- dispose: async (adapter) => {
22
- await adapter.stop();
23
- },
24
- });
25
- });
26
- useContext('tool', 'wecom', (toolService, wecom) => {
27
- const disposers = [];
28
- disposers.push(registerWecomPlatformPermitChecker());
29
- disposers.push(toolService.addTool({
30
- name: 'wecom_get_user',
31
- description: '获取企业微信用户信息',
32
- parameters: {
33
- type: 'object',
34
- properties: {
35
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
36
- user_id: { type: 'string', description: '用户 ID' },
37
- },
38
- required: ['endpoint_id', 'user_id'],
39
- },
40
- platforms: ['wecom'],
41
- tags: ['wecom'],
42
- execute: async (args) => {
43
- const endpoint = wecom.endpoints.get(args.endpoint_id);
44
- if (!endpoint)
45
- throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
46
- return await endpoint.getUserInfo(args.user_id);
47
- },
48
- }, plugin.name));
49
- disposers.push(toolService.addTool({
50
- name: 'wecom_get_dept_users',
51
- description: '获取企业微信部门用户列表',
52
- parameters: {
53
- type: 'object',
54
- properties: {
55
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
56
- dept_id: { type: 'string', description: '部门 ID' },
57
- },
58
- required: ['endpoint_id', 'dept_id'],
59
- },
60
- platforms: ['wecom'],
61
- tags: ['wecom'],
62
- execute: async (args) => {
63
- const endpoint = wecom.endpoints.get(args.endpoint_id);
64
- if (!endpoint)
65
- throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
66
- const users = await endpoint.getDepartmentUsers(Number(args.dept_id));
67
- return { users, count: users.length };
68
- },
69
- }, plugin.name));
70
- disposers.push(toolService.addTool({
71
- name: 'wecom_list_departments',
72
- description: '获取企业微信部门列表',
73
- parameters: {
74
- type: 'object',
75
- properties: {
76
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
77
- dept_id: { type: 'string', description: '父部门 ID,默认 1(跟部门)' },
78
- },
79
- required: ['endpoint_id'],
80
- },
81
- platforms: ['wecom'],
82
- tags: ['wecom'],
83
- execute: async (args) => {
84
- const endpoint = wecom.endpoints.get(args.endpoint_id);
85
- if (!endpoint)
86
- throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
87
- const departments = await endpoint.getDepartmentList(Number(args.dept_id) || 1);
88
- return { departments, count: departments.length };
89
- },
90
- }, plugin.name));
91
- disposers.push(toolService.addTool({
92
- name: 'wecom_send_text',
93
- description: '向指定企业微信用户发送文本消息',
94
- parameters: {
95
- type: 'object',
96
- properties: {
97
- endpoint_id: { type: 'string', description: 'Endpoint 名称', contextKey: 'endpointId' },
98
- user_id: { type: 'string', description: '用户 ID' },
99
- content: { type: 'string', description: '消息内容' },
100
- },
101
- required: ['endpoint_id', 'user_id', 'content'],
102
- },
103
- platforms: ['wecom'],
104
- tags: ['wecom'],
105
- execute: async (args) => {
106
- const endpoint = wecom.endpoints.get(args.endpoint_id);
107
- if (!endpoint)
108
- throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
109
- const success = await endpoint.sendTextMessage(args.user_id, args.content);
110
- return { success, message: success ? '消息已发送' : '发送失败' };
111
- },
112
- }, plugin.name));
113
- return () => disposers.forEach(d => d());
114
- });
115
- //# sourceMappingURL=index.js.map
1
+ export { buildSendRequestBody, decryptMessage, extractEncryptFromXml, formatInboundContent, formatOutboundBody, getAesKey, normalizeEchostrParam, normalizeWebhookPath, parseXmlMessage, queryParam, readTextBody, resolveChatType, resolveWecomConfig, verifySignature, } from './protocol.js';
2
+ export { wecomClient } from './client.js';
3
+ export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, wecomGroupPermitResolver, } from './platform-permit.js';
4
+ export { WecomClient, WecomEndpoint, } from './endpoint.js';
5
+ export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
6
+ export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, } from './webhook.js';
@@ -0,0 +1,23 @@
1
+ import { type MediaRef } from '@zhin.js/core';
2
+ export interface MediaBinary {
3
+ readonly data: Buffer;
4
+ readonly mimeType: string;
5
+ readonly fileName: string;
6
+ }
7
+ export interface WecomMediaUploadResult {
8
+ readonly errcode?: number;
9
+ readonly errmsg?: string;
10
+ readonly type?: string;
11
+ readonly media_id?: string;
12
+ readonly created_at?: string;
13
+ }
14
+ /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
15
+ export declare function resolveMediaBinary(media: MediaRef, download?: (url: string) => Promise<Buffer>): Promise<MediaBinary>;
16
+ /** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
17
+ export declare function buildMediaUploadForm(binary: MediaBinary): FormData;
18
+ /**
19
+ * 出站 image 段的媒体引用:只读 canonical `data.media`(MediaRef)。
20
+ * 中央 normalizeOutboundPayload 已保证到达 endpoint 的载荷为 canonical;
21
+ * 无 MediaRef 时返回 undefined,由调用方 warn + 丢弃。
22
+ */
23
+ export declare function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * WeCom 临时素材上传(message/send 的 image 段需要 media_id)。
3
+ * 接口:POST {apiBaseUrl}/cgi-bin/media/upload?access_token=…&type=image
4
+ * 与 message/send 同属应用消息接口域(同一 access_token),不引入额外授权域。
5
+ */
6
+ import { readFile } from 'node:fs/promises';
7
+ import { basename } from 'node:path';
8
+ import { isMediaRef } from '@zhin.js/core';
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
+ if (media.kind === 'url') {
31
+ return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
32
+ }
33
+ // kind=file 为平台不透明引用(media_id),无二进制可解,调用方应直用 value
34
+ throw new Error(`cannot resolve binary from media kind: ${media.kind}`);
35
+ }
36
+ async function defaultDownload(url) {
37
+ const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
38
+ if (!response.ok)
39
+ throw new Error(`download failed: HTTP ${response.status}`);
40
+ return Buffer.from(await response.arrayBuffer());
41
+ }
42
+ /** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
43
+ export function buildMediaUploadForm(binary) {
44
+ // Buffer 的 ArrayBufferLike 不满足 BlobPart(SharedArrayBuffer 分支),拷贝为 Uint8Array<ArrayBuffer>
45
+ const bytes = new Uint8Array(binary.data.byteLength);
46
+ bytes.set(binary.data);
47
+ const form = new FormData();
48
+ form.append('media', new Blob([bytes], { type: binary.mimeType }), binary.fileName);
49
+ return form;
50
+ }
51
+ /**
52
+ * 出站 image 段的媒体引用:只读 canonical `data.media`(MediaRef)。
53
+ * 中央 normalizeOutboundPayload 已保证到达 endpoint 的载荷为 canonical;
54
+ * 无 MediaRef 时返回 undefined,由调用方 warn + 丢弃。
55
+ */
56
+ export function readOutboundImageMedia(data) {
57
+ return isMediaRef(data.media) ? data.media : undefined;
58
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 企业微信 WeCom platform permit
3
3
  */
4
- import type { Message } from 'zhin.js';
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
5
  export declare function platformPermit(perm: string): string;
6
6
  export declare function wecomGroupPermitResolver(logicalPerm: string): string;
7
7
  export declare function normalizeWecomSenderForPermit(input: {
@@ -11,6 +11,4 @@ export declare function normalizeWecomSenderForPermit(input: {
11
11
  role?: string;
12
12
  permissions?: string[];
13
13
  };
14
- export declare function checkWecomPlatformPermit(perm: string, message: Message<any>): boolean;
15
- export declare function registerWecomPlatformPermitChecker(): () => void;
16
- //# sourceMappingURL=platform-permit.d.ts.map
14
+ export declare function checkWecomPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -1,11 +1,10 @@
1
- import { registerPlatformPermitChecker } from 'zhin.js';
2
1
  const ADAPTER = 'wecom';
3
2
  export function platformPermit(perm) {
4
3
  return `platform(${ADAPTER},${perm})`;
5
4
  }
6
5
  const FACTORY_PERM_MAP = {
7
- group_admin: 'chat_admin',
8
- group_owner: 'chat_owner',
6
+ scene_admin: 'chat_admin',
7
+ scene_owner: 'chat_owner',
9
8
  };
10
9
  export function wecomGroupPermitResolver(logicalPerm) {
11
10
  return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
@@ -19,10 +18,9 @@ export function normalizeWecomSenderForPermit(input) {
19
18
  }
20
19
  return { role: 'member', permissions: [] };
21
20
  }
22
- export function checkWecomPlatformPermit(perm, message) {
23
- const sender = message.$sender;
24
- const permissions = sender.permissions ?? [];
25
- const role = sender.role;
21
+ export function checkWecomPlatformPermit(perm, subject) {
22
+ const role = subject.sender?.role?.[0];
23
+ const permissions = subject.sender?.permissions ?? [];
26
24
  const has = (t) => permissions.includes(t);
27
25
  switch (perm) {
28
26
  case 'chat_owner':
@@ -33,7 +31,3 @@ export function checkWecomPlatformPermit(perm, message) {
33
31
  return false;
34
32
  }
35
33
  }
36
- export function registerWecomPlatformPermitChecker() {
37
- return registerPlatformPermitChecker(ADAPTER, checkWecomPlatformPermit);
38
- }
39
- //# sourceMappingURL=platform-permit.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * WeCom (企业微信) 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
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
8
+ export interface WecomAdapterConfig {
9
+ readonly id?: string;
10
+ readonly corpId?: string;
11
+ readonly agentSecret?: string;
12
+ readonly token?: string;
13
+ readonly encodingAESKey?: string;
14
+ readonly webhookPath?: string;
15
+ readonly apiBaseUrl?: string;
16
+ /** Transitional: legacy root `endpoints[]` with `context: wecom`. */
17
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedWecomConfig> & {
18
+ readonly context?: string;
19
+ }>;
20
+ }
21
+ export interface ResolvedWecomConfig {
22
+ readonly context: 'wecom';
23
+ readonly id: string;
24
+ readonly corpId: string;
25
+ readonly agentSecret: string;
26
+ readonly token: string;
27
+ readonly encodingAESKey: string;
28
+ readonly webhookPath: string;
29
+ readonly apiBaseUrl: string;
30
+ }
31
+ export interface WecomMessage {
32
+ readonly ToUserName: string;
33
+ readonly FromUserName: string;
34
+ readonly CreateTime: number;
35
+ readonly MsgType: 'text' | 'image' | 'voice' | 'video' | 'shortvideo' | 'location' | 'link' | 'event' | string;
36
+ readonly Content?: string;
37
+ readonly MsgId?: string;
38
+ readonly PicUrl?: string;
39
+ readonly MediaId?: string;
40
+ readonly ThumbMediaId?: string;
41
+ readonly Format?: string;
42
+ readonly Recognition?: string;
43
+ readonly Location_X?: string;
44
+ readonly Location_Y?: string;
45
+ readonly Scale?: string;
46
+ readonly Label?: string;
47
+ readonly Title?: string;
48
+ readonly Description?: string;
49
+ readonly Url?: string;
50
+ readonly Event?: string;
51
+ readonly EventKey?: string;
52
+ readonly AgentID?: string;
53
+ }
54
+ export interface AccessToken {
55
+ access_token: string;
56
+ expires_in: number;
57
+ timestamp: number;
58
+ }
59
+ export interface WecomApiResponse {
60
+ readonly errcode: number;
61
+ readonly errmsg?: string;
62
+ readonly access_token?: string;
63
+ readonly expires_in?: number;
64
+ readonly msgid?: string;
65
+ readonly userlist?: unknown[];
66
+ readonly department?: unknown[];
67
+ readonly [key: string]: unknown;
68
+ }
69
+ export interface WecomWireSegment {
70
+ readonly type: string;
71
+ readonly data?: Record<string, unknown>;
72
+ }
73
+ export interface WecomSendBody {
74
+ readonly msgtype: string;
75
+ readonly data: Record<string, unknown>;
76
+ }
77
+ export declare function resolveWecomConfig(config?: WecomAdapterConfig): ResolvedWecomConfig;
78
+ export declare function normalizeWebhookPath(path: string): string;
79
+ export declare function queryParam(value: string | null | undefined): string;
80
+ /** URL 查询里的 Base64 可能把 `+` 解码成空格 */
81
+ export declare function normalizeEchostrParam(echostr: string): string;
82
+ export declare function getAesKey(encodingAESKey: string): Buffer;
83
+ export declare function verifySignature(token: string, timestamp: string, nonce: string, encrypt: string, signature: string): boolean;
84
+ export declare function decryptMessage(encrypted: string, encodingAESKey: string, corpId: string): string | null;
85
+ export declare function extractEncryptFromXml(xml: string): string | null;
86
+ export declare function parseXmlMessage(xml: string): WecomMessage | null;
87
+ /** Build inbound text for OutboundMessageService.receive. */
88
+ export declare function formatInboundContent(msg: WecomMessage): string;
89
+ export declare function resolveChatType(fromUserName: string): 'group' | 'private';
90
+ /**
91
+ * 入站归一化 → ConversationRef:WeCom 无 guild/channel 容器概念,
92
+ * `@chatroom` 群会话 → kind 'group',其余(应用消息/单聊)→ kind 'private'。
93
+ */
94
+ export declare function wecomInboundConversation(endpointKey: string, msg: WecomMessage): ConversationRef;
95
+ /**
96
+ * Wire-encode an already-rendered outbound payload into WeCom message/send body parts.
97
+ */
98
+ export declare function formatOutboundBody(payload: unknown): WecomSendBody;
99
+ export declare function buildSendRequestBody(targetId: string, content: WecomSendBody, agentId: string | number): Record<string, unknown>;
100
+ export declare function readTextBody(request: IncomingMessage, options?: {
101
+ readonly limit?: number;
102
+ }): Promise<string>;
@@ -0,0 +1,268 @@
1
+ /**
2
+ * WeCom (企业微信) protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { createHash, createDecipheriv, timingSafeEqual } from 'node:crypto';
6
+ export function resolveWecomConfig(config = {}) {
7
+ const entry = config.endpoints?.find((item) => item.context === 'wecom');
8
+ const corpId = config.corpId ?? entry?.corpId ?? process.env.WECOM_CORP_ID;
9
+ const agentSecret = config.agentSecret ?? entry?.agentSecret ?? process.env.WECOM_AGENT_SECRET;
10
+ const token = config.token ?? entry?.token ?? process.env.WECOM_TOKEN;
11
+ const encodingAESKey = config.encodingAESKey
12
+ ?? entry?.encodingAESKey
13
+ ?? process.env.WECOM_AES_KEY;
14
+ if (!corpId || !agentSecret || !token || !encodingAESKey) {
15
+ throw new TypeError('WeCom adapter requires corpId + agentSecret + token + encodingAESKey (plugins.<key> or endpoints with context: wecom)');
16
+ }
17
+ const id = (typeof config.id === 'string' && config.id)
18
+ || (typeof entry?.id === 'string' && entry.id)
19
+ || process.env.WECOM_BOT_NAME
20
+ || 'wecom-bot';
21
+ return {
22
+ context: 'wecom',
23
+ id,
24
+ corpId,
25
+ agentSecret,
26
+ token,
27
+ encodingAESKey,
28
+ webhookPath: normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? '/wecom/callback'),
29
+ apiBaseUrl: config.apiBaseUrl
30
+ ?? entry?.apiBaseUrl
31
+ ?? 'https://qyapi.weixin.qq.com',
32
+ };
33
+ }
34
+ export function normalizeWebhookPath(path) {
35
+ const trimmed = path.trim() || '/wecom/callback';
36
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
37
+ }
38
+ export function queryParam(value) {
39
+ return value ?? '';
40
+ }
41
+ /** URL 查询里的 Base64 可能把 `+` 解码成空格 */
42
+ export function normalizeEchostrParam(echostr) {
43
+ return echostr.replace(/ /g, '+');
44
+ }
45
+ export function getAesKey(encodingAESKey) {
46
+ const aesKey = Buffer.from(`${encodingAESKey}=`, 'base64');
47
+ if (aesKey.length !== 32) {
48
+ throw new Error(`encodingAESKey must produce a 32-byte key, got ${aesKey.length} bytes`);
49
+ }
50
+ return aesKey;
51
+ }
52
+ export function verifySignature(token, timestamp, nonce, encrypt, signature) {
53
+ try {
54
+ const hash = createHash('sha1')
55
+ .update([token, timestamp, nonce, encrypt].sort().join(''))
56
+ .digest('hex');
57
+ const a = Buffer.from(hash);
58
+ const b = Buffer.from(signature);
59
+ if (a.length !== b.length)
60
+ return false;
61
+ return timingSafeEqual(a, b);
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ export function decryptMessage(encrypted, encodingAESKey, corpId) {
68
+ try {
69
+ const aesKey = getAesKey(encodingAESKey);
70
+ const buf = Buffer.from(encrypted, 'base64');
71
+ const iv = aesKey.subarray(0, 16);
72
+ const decipher = createDecipheriv('aes-256-cbc', aesKey, iv);
73
+ decipher.setAutoPadding(false);
74
+ const decrypted = Buffer.concat([decipher.update(buf), decipher.final()]);
75
+ const pad = decrypted[decrypted.length - 1];
76
+ const content = decrypted.subarray(0, decrypted.length - pad);
77
+ const msgLen = content.readUInt32BE(16);
78
+ const msg = content.subarray(20, 20 + msgLen).toString('utf8');
79
+ const extractedCorpId = content.subarray(20 + msgLen).toString('utf8');
80
+ if (extractedCorpId !== corpId)
81
+ return null;
82
+ return msg;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ export function extractEncryptFromXml(xml) {
89
+ const match = xml.match(/<Encrypt><!\[CDATA\[([^[\]]+)\]\]><\/Encrypt>/);
90
+ return match?.[1] ?? null;
91
+ }
92
+ export function parseXmlMessage(xml) {
93
+ try {
94
+ const get = (tag) => {
95
+ const m = xml.match(new RegExp(`<${tag}><!\\[CDATA\\[([^\\[\\]]*)\\]\\]><\\/${tag}>`))
96
+ || xml.match(new RegExp(`<${tag}>([^<]*)<\\/${tag}>`));
97
+ return m ? m[1] : undefined;
98
+ };
99
+ const msgType = get('MsgType');
100
+ if (!msgType)
101
+ return null;
102
+ const msg = {
103
+ ToUserName: get('ToUserName') || '',
104
+ FromUserName: get('FromUserName') || '',
105
+ CreateTime: Number(get('CreateTime') || Date.now()),
106
+ MsgType: msgType,
107
+ MsgId: get('MsgId'),
108
+ AgentID: get('AgentID'),
109
+ Content: get('Content'),
110
+ PicUrl: get('PicUrl'),
111
+ MediaId: get('MediaId'),
112
+ ThumbMediaId: get('ThumbMediaId'),
113
+ Format: get('Format'),
114
+ Recognition: get('Recognition'),
115
+ Location_X: get('Location_X'),
116
+ Location_Y: get('Location_Y'),
117
+ Scale: get('Scale'),
118
+ Label: get('Label'),
119
+ Title: get('Title'),
120
+ Description: get('Description'),
121
+ Url: get('Url'),
122
+ Event: get('Event'),
123
+ EventKey: get('EventKey'),
124
+ };
125
+ return msg;
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ /** Build inbound text for OutboundMessageService.receive. */
132
+ export function formatInboundContent(msg) {
133
+ switch (msg.MsgType) {
134
+ case 'text':
135
+ return msg.Content || '(空消息)';
136
+ case 'image':
137
+ return msg.PicUrl ? `[image: ${msg.PicUrl}]` : '[image]';
138
+ case 'voice':
139
+ return msg.Recognition
140
+ ? msg.Recognition
141
+ : `[voice${msg.Format ? `: ${msg.Format}` : ''}]`;
142
+ case 'video':
143
+ case 'shortvideo':
144
+ return '[video]';
145
+ case 'location':
146
+ return `[位置] ${msg.Label || ''} (${msg.Location_X}, ${msg.Location_Y})`.trim();
147
+ case 'link':
148
+ return `[link: ${msg.Title ?? ''}${msg.Url ? ` ${msg.Url}` : ''}]`;
149
+ case 'event':
150
+ return `[事件] ${msg.Event || ''} ${msg.EventKey || ''}`.trim();
151
+ default:
152
+ return `[不支持的消息类型: ${msg.MsgType}]`;
153
+ }
154
+ }
155
+ export function resolveChatType(fromUserName) {
156
+ return fromUserName.endsWith('@chatroom') ? 'group' : 'private';
157
+ }
158
+ /**
159
+ * 入站归一化 → ConversationRef:WeCom 无 guild/channel 容器概念,
160
+ * `@chatroom` 群会话 → kind 'group',其余(应用消息/单聊)→ kind 'private'。
161
+ */
162
+ export function wecomInboundConversation(endpointKey, msg) {
163
+ return {
164
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
165
+ kind: resolveChatType(msg.FromUserName),
166
+ id: msg.FromUserName,
167
+ };
168
+ }
169
+ /**
170
+ * Wire-encode an already-rendered outbound payload into WeCom message/send body parts.
171
+ */
172
+ export function formatOutboundBody(payload) {
173
+ if (typeof payload === 'string') {
174
+ return { msgtype: 'text', data: { content: payload } };
175
+ }
176
+ if (!Array.isArray(payload)) {
177
+ return { msgtype: 'text', data: { content: String(payload ?? '') } };
178
+ }
179
+ const textParts = [];
180
+ let hasMedia = false;
181
+ let mediaType = '';
182
+ let mediaData = null;
183
+ for (const item of payload) {
184
+ if (typeof item === 'string') {
185
+ textParts.push(item);
186
+ continue;
187
+ }
188
+ if (!item || typeof item !== 'object')
189
+ continue;
190
+ const seg = item;
191
+ const data = seg.data ?? {};
192
+ switch (seg.type) {
193
+ case 'text':
194
+ textParts.push(String(data.content ?? data.text ?? ''));
195
+ break;
196
+ case 'at': {
197
+ const userId = data.id ?? data.userId;
198
+ if (userId)
199
+ textParts.push(`<@${userId}>`);
200
+ break;
201
+ }
202
+ case 'image':
203
+ if (!hasMedia) {
204
+ hasMedia = true;
205
+ mediaType = 'image';
206
+ // media_id 由 endpoint 的 #materializeOutboundMedia 物化写入(内部 wire)。
207
+ mediaData = { media_id: data.media_id };
208
+ }
209
+ break;
210
+ case 'markdown':
211
+ if (!hasMedia) {
212
+ hasMedia = true;
213
+ mediaType = 'markdown';
214
+ mediaData = { content: data.content || data.text };
215
+ }
216
+ break;
217
+ case 'link':
218
+ if (!hasMedia) {
219
+ hasMedia = true;
220
+ mediaType = 'news';
221
+ mediaData = {
222
+ articles: [{
223
+ title: data.title || '链接',
224
+ description: data.text || data.content || '',
225
+ url: data.url,
226
+ picurl: data.picUrl,
227
+ }],
228
+ };
229
+ }
230
+ break;
231
+ default:
232
+ break;
233
+ }
234
+ }
235
+ if (hasMedia && mediaData) {
236
+ return { msgtype: mediaType, data: mediaData };
237
+ }
238
+ return { msgtype: 'text', data: { content: textParts.join('') } };
239
+ }
240
+ export function buildSendRequestBody(targetId, content, agentId) {
241
+ const body = {
242
+ msgtype: content.msgtype,
243
+ agentid: agentId,
244
+ [content.msgtype]: content.data,
245
+ };
246
+ if (targetId.endsWith('@chatroom')) {
247
+ body.chatid = targetId;
248
+ }
249
+ else {
250
+ body.touser = targetId;
251
+ }
252
+ return body;
253
+ }
254
+ export async function readTextBody(request, options = {}) {
255
+ const limit = options.limit ?? 1_048_576;
256
+ const chunks = [];
257
+ let size = 0;
258
+ for await (const chunk of request) {
259
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
260
+ size += buffer.length;
261
+ if (size > limit) {
262
+ request.destroy();
263
+ throw new Error(`Request body exceeds ${limit} bytes`);
264
+ }
265
+ chunks.push(buffer);
266
+ }
267
+ return Buffer.concat(chunks).toString('utf8');
268
+ }
@@ -0,0 +1,4 @@
1
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
2
+ import { type getAdapterLogger } from '@zhin.js/logger';
3
+ import { type WecomMessage } from './protocol.js';
4
+ export declare function receiveWecomSideEvent(emit: EndpointEventEmitter, endpointKey: string, configId: string, msg: WecomMessage, logger: ReturnType<typeof getAdapterLogger>): boolean;