@zhin.js/adapter-wecom 1.1.18 → 1.1.19

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/lib/endpoint.js CHANGED
@@ -1,317 +1,32 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- /**
3
- * WecomEndpoint — lifecycle, outbound send, inbound admit, OpenAPI helpers for agent tools.
4
- */
5
- import { createRecallEndpointControl, } from 'zhin.js/adapter';
6
- import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
- import { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
8
- import { buildSendRequestBody, formatInboundContent, formatOutboundBody, resolveChatType, wecomInboundConversation, } from './protocol.js';
9
- import { registerWecomWebhookRoutes } from './webhook.js';
10
- import { receiveWecomSideEvent } from './side-event-dispatch.js';
11
- /** WeCom API surface exposed to plugin events without Endpoint lifecycle methods. */
12
- export class WecomClient {
13
- api;
14
- constructor(api) {
15
- this.api = api;
16
- }
17
- getUserInfo = (userId) => this.api.getUserInfo(userId);
18
- getDepartmentUsers = (deptId) => this.api.getDepartmentUsers(deptId);
19
- getDepartmentList = (deptId) => this.api.getDepartmentList(deptId);
20
- sendTextMessage = (userId, content) => this.api.sendTextMessage(userId, content);
21
- }
22
- /**
23
- * 企业微信服务端 API 无 bot 群列表/群成员列表接口(客户群接口属「客户联系」
24
- * 独立授权域,非 bot 社交面),好友/频道概念亦不存在;
25
- * 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
26
- */
27
- export class WecomEndpoint extends Endpoint {
28
- client = new WecomClient({
29
- getUserInfo: (userId) => this.#getUserInfo(userId),
30
- getDepartmentUsers: (deptId) => this.#getDepartmentUsers(deptId),
31
- getDepartmentList: (deptId) => this.#getDepartmentList(deptId),
32
- sendTextMessage: (userId, content) => this.#sendTextMessage(userId, content),
33
- });
34
- #logger;
35
- #options;
36
- control = createRecallEndpointControl((id) => this.recallMessage(id));
37
- #fetch;
38
- #routeReleases = [];
39
- #accessToken = { access_token: '', expires_in: 0, timestamp: 0 };
40
- #refreshPromise = null;
41
- #open = false;
42
- #started = false;
43
- constructor(options) {
44
- super();
45
- this.#logger = getAdapterLogger('wecom', options.config.id);
46
- this.#options = options;
47
- this.#fetch = options.fetch ?? globalThis.fetch;
48
- }
49
- /** Used by webhook handler. */
50
- get isOpen() {
51
- return this.#open;
52
- }
53
- get config() {
54
- return this.#options.config;
55
- }
56
- async start() {
57
- if (this.#started)
58
- return;
59
- this.#started = true;
60
- try {
61
- await this.#refreshAccessToken();
62
- this.#routeReleases.push(...registerWecomWebhookRoutes(this.#options.http, this));
63
- this.#logger.debug(formatCompact({
64
- endpoint: this.#options.config.id,
65
- op: 'webhook',
66
- path: this.#options.config.webhookPath,
67
- }));
68
- }
69
- catch (error) {
70
- await this.stop();
71
- this.#logger.error('Failed to connect WeCom endpoint:', error);
72
- throw error;
73
- }
74
- }
75
- open() {
76
- this.#open = true;
77
- }
78
- close() {
79
- this.#open = false;
80
- }
81
- async stop() {
82
- this.#open = false;
83
- for (const release of this.#routeReleases.splice(0))
84
- release();
85
- this.#started = false;
86
- this.#logger.debug(formatCompact({ op: 'disconnect' }));
87
- }
88
- async send({ conversation, payload, locale }) {
89
- const materialized = await this.#materializeOutboundMedia(payload);
90
- const content = formatOutboundBody(materialized, locale);
91
- const body = buildSendRequestBody(conversation.id, content,
92
- // Legacy field: historically written into agentid (preserved for cutover).
93
- this.#options.config.agentSecret);
94
- const data = await this.#request('/cgi-bin/message/send', {
95
- method: 'POST',
96
- body,
97
- });
98
- if (data.errcode !== 0) {
99
- throw new Error(`Failed to send message: ${data.errmsg} (${data.errcode})`);
100
- }
101
- this.#logger.debug(formatCompact({ op: 'send', to: `${conversation.kind}:${conversation.id}`,
102
- }));
103
- return data.msgid || `${Date.now()}`;
104
- }
105
- async recallMessage(messageId) {
106
- if (!messageId)
107
- return;
108
- await this.#request('/cgi-bin/message/recall', {
109
- method: 'POST',
110
- body: { msgid: messageId },
111
- });
112
- }
113
- /**
114
- * message/send 的 image 段只接受 media_id:canonical MediaRef 按 kind 投递——
115
- * file(平台不透明引用)直用为 media_id;base64/本地路径/URL 先经
116
- * /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
117
- * 无 canonical MediaRef 的媒体段 warn + 丢弃。
118
- */
119
- async #materializeOutboundMedia(payload) {
120
- if (!Array.isArray(payload))
121
- return payload;
122
- const items = await Promise.all(payload.map(async (item) => {
123
- if (typeof item === 'string' || !item || typeof item !== 'object')
124
- return item;
125
- const seg = item;
126
- if (seg.type !== 'image')
127
- return item;
128
- const data = seg.data ?? {};
129
- const media = readOutboundImageMedia(data);
130
- if (!media) {
131
- this.#logger.warn(formatCompact({
132
- op: 'wecom_outbound_media_dropped',
133
- endpoint: this.#options.config.id,
134
- type: 'image',
135
- reason: 'missing_media_ref',
136
- }));
137
- return null;
138
- }
139
- if (media.kind === 'file') {
140
- return { type: 'image', data: { media_id: media.value } };
141
- }
142
- try {
143
- const mediaId = await this.#uploadMedia('image', media);
144
- return { type: 'image', data: { media_id: mediaId } };
145
- }
146
- catch (error) {
147
- this.#logger.warn(formatCompact({
148
- op: 'wecom_media_upload_failed',
149
- endpoint: this.#options.config.id,
150
- error: error instanceof Error ? error.message : String(error),
151
- }));
152
- const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
153
- return { type: 'text', data: { text: alt } };
154
- }
155
- }));
156
- return items.filter((item) => item !== null);
157
- }
158
- /** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
159
- async #uploadMedia(type, media) {
160
- await this.#ensureAccessToken();
161
- const binary = await resolveMediaBinary(media);
162
- const form = buildMediaUploadForm(binary);
163
- const url = `${this.#options.config.apiBaseUrl}/cgi-bin/media/upload?access_token=${this.#accessToken.access_token}&type=${type}`;
164
- const response = await this.#fetch(url, { method: 'POST', body: form });
165
- const data = await response.json();
166
- if (data.errcode === 0 && data.media_id)
167
- return data.media_id;
168
- throw new Error(`WeCom media upload failed: ${data.errmsg ?? 'unknown'} (${data.errcode ?? '?'})`);
169
- }
170
- /** Test / internal: admit a parsed message when open (non-webhook path). */
171
- admit(msg) {
172
- if (!this.#open)
173
- return;
174
- void this.emitPlatform(msg.Event || msg.MsgType || 'event', msg).catch((error) => {
175
- this.#logger.warn(formatCompact({
176
- op: 'wecom_platform_event_failed',
177
- event: msg.Event || msg.MsgType,
178
- error: error instanceof Error ? error.message : String(error),
179
- }));
180
- });
181
- if (receiveWecomSideEvent((name, payload) => this.emit(name, payload), String(this.#options.id), this.#options.config.id, msg, this.#logger)) {
182
- return;
183
- }
184
- const chatType = resolveChatType(msg.FromUserName);
185
- const conversation = wecomInboundConversation(String(this.#options.id), msg);
186
- void this.emit('message.receive', {
187
- conversation,
188
- message: { conversation, id: msg.MsgId || `${msg.CreateTime}` },
189
- content: formatInboundContent(msg),
190
- sender: { id: msg.FromUserName },
191
- endpointId: this.#options.config.id,
192
- metadata: Object.freeze({
193
- msgType: msg.MsgType,
194
- event: msg.Event,
195
- chatType,
196
- toUserName: msg.ToUserName,
197
- agentId: msg.AgentID,
198
- }),
199
- }).catch((err) => {
200
- this.#logger.warn(formatCompact({
201
- op: 'wecom_gateway_receive_failed',
202
- target: `${conversation.kind}:${conversation.id}`,
203
- error: err instanceof Error ? err.message : String(err),
204
- }));
205
- });
206
- }
207
- async #getUserInfo(userId) {
208
- try {
209
- const data = await this.#request('/cgi-bin/user/get', {
210
- params: { userid: userId },
211
- });
212
- if (data.errcode === 0)
213
- return data;
214
- throw new Error(`Failed to get user info: ${data.errmsg}`);
215
- }
216
- catch (error) {
217
- this.#logger.error('Failed to get user info:', error);
218
- return null;
219
- }
220
- }
221
- async #getDepartmentUsers(deptId) {
222
- try {
223
- const data = await this.#request('/cgi-bin/user/simplelist', {
224
- params: { department_id: deptId },
225
- });
226
- if (data.errcode === 0)
227
- return data.userlist || [];
228
- throw new Error(`Failed to get department users: ${data.errmsg}`);
229
- }
230
- catch (error) {
231
- this.#logger.error('Failed to get department users:', error);
232
- return [];
233
- }
234
- }
235
- async #getDepartmentList(deptId = 1) {
236
- try {
237
- const data = await this.#request('/cgi-bin/department/list', {
238
- params: { id: deptId },
239
- });
240
- if (data.errcode === 0)
241
- return data.department || [];
242
- throw new Error(`Failed to get department list: ${data.errmsg}`);
243
- }
244
- catch (error) {
245
- this.#logger.error('Failed to get department list:', error);
246
- return [];
247
- }
248
- }
249
- async #sendTextMessage(userId, content) {
250
- try {
251
- const endpointKey = String(this.#options.id);
252
- await this.send({
253
- conversation: {
254
- endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
255
- kind: resolveChatType(userId),
256
- id: userId,
257
- },
258
- payload: content,
259
- });
260
- return true;
261
- }
262
- catch (error) {
263
- this.#logger.error('Failed to send text message:', error);
264
- return false;
265
- }
266
- }
267
- async #request(path, options = {}) {
268
- await this.#ensureAccessToken();
269
- const { method = 'GET', params = {}, body } = options;
270
- const urlParams = new URLSearchParams({
271
- ...Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)])),
272
- access_token: this.#accessToken.access_token,
273
- });
274
- const url = `${this.#options.config.apiBaseUrl}${path}?${urlParams.toString()}`;
275
- const response = await this.#fetch(url, {
276
- method,
277
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
278
- body: body && method === 'POST' ? JSON.stringify(body) : undefined,
279
- });
280
- if (!response.ok) {
281
- const text = await response.text().catch(() => '');
282
- throw new Error(`WeCom API error ${response.status}: ${text}`);
283
- }
284
- return await response.json();
285
- }
286
- async #ensureAccessToken() {
287
- const now = Date.now();
288
- if (this.#accessToken.access_token
289
- && now < this.#accessToken.timestamp + (this.#accessToken.expires_in - 300) * 1000) {
290
- return;
291
- }
292
- if (this.#refreshPromise) {
293
- await this.#refreshPromise;
294
- return;
295
- }
296
- this.#refreshPromise = this.#refreshAccessToken()
297
- .then(() => this.#accessToken.access_token)
298
- .finally(() => { this.#refreshPromise = null; });
299
- await this.#refreshPromise;
300
- }
301
- async #refreshAccessToken() {
302
- const { corpId, agentSecret, apiBaseUrl } = this.#options.config;
303
- const url = `${apiBaseUrl}/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${agentSecret}`;
304
- const response = await this.#fetch(url);
305
- const data = await response.json();
306
- if (data.errcode === 0 && data.access_token) {
307
- this.#accessToken = {
308
- access_token: data.access_token,
309
- expires_in: data.expires_in ?? 7200,
310
- timestamp: Date.now(),
311
- };
312
- this.#logger.debug('Access token refreshed successfully');
313
- return;
314
- }
315
- throw new Error(`Failed to get access token: ${data.errmsg} (${data.errcode})`);
1
+ import { createEndpointSpecification, endpointSpec } from 'zhin.js/adapter';
2
+ import { WecomClient } from './client.js';
3
+ /** Actual protocol Client; framework lifecycle and admission stay in their symbol namespace. */
4
+ export class WecomEndpoint extends WecomClient {
5
+ [endpointSpec] = this.#createSpecification();
6
+ constructor(options) { super(options); }
7
+ #createSpecification() {
8
+ const hooks = {
9
+ start: (signal) => this.#startFramework(signal),
10
+ open: () => this.openInput(), close: () => this.closeInput(), stop: () => this.disconnectTransport(),
11
+ send: (request) => this.sendPayload(request),
12
+ name: this.configName(),
13
+ control: this.endpointControl,
14
+ };
15
+ Object.defineProperty(hooks, 'transportState', { get: () => this.transportState });
16
+ return createEndpointSpecification(this, hooks);
17
+ }
18
+ #startFramework(signal) { return this.connectTransport(signal); }
19
+ receiveInboundCallback(receive) {
20
+ return this[endpointSpec].events.run(receive);
21
+ }
22
+ publishEvent(name, payload) {
23
+ return this[endpointSpec].events.emit(name, payload);
24
+ }
25
+ publishAccepted(name, payload) {
26
+ return this[endpointSpec].events.emitAccepted(name, payload).then(() => undefined);
27
+ }
28
+ async publishPlatform(name, event) {
29
+ await super.publishPlatform(name, event);
30
+ return this[endpointSpec].events.platform(name, event);
316
31
  }
317
32
  }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { buildSendRequestBody, decryptMessage, extractEncryptFromXml, formatInboundContent, formatOutboundBody, getAesKey, normalizeEchostrParam, normalizeWebhookPath, parseXmlMessage, queryParam, readTextBody, resolveChatType, resolveWecomConfig, verifySignature, type AccessToken, type ResolvedWecomConfig, type WecomEndpointConfig, type WecomApiResponse, type WecomMessage, type WecomSendBody, type WecomWireSegment, } from './protocol.js';
2
- export { wecomClient, type WecomClientEventMap } from './client.js';
2
+ export { type WecomClientEventMap } from './client.js';
3
3
  export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, wecomGroupPermitResolver, } from './platform-permit.js';
4
- export { WecomClient, WecomEndpoint, type WecomClientApi, type WecomEndpointOptions, type WecomFetch, } from './endpoint.js';
4
+ export { WecomEndpoint, type WecomEndpointOptions, type WecomFetch, } from './endpoint.js';
5
5
  export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, type WecomMediaUploadResult, } from './media-upload.js';
6
6
  export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, type WecomWebhookHandler, } from './webhook.js';
7
+ export { WecomClient, type WecomClientApi } from './client.js';
8
+ export { WecomAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
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
2
  export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, wecomGroupPermitResolver, } from './platform-permit.js';
4
- export { WecomClient, WecomEndpoint, } from './endpoint.js';
3
+ export { WecomEndpoint, } from './endpoint.js';
5
4
  export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
6
5
  export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, } from './webhook.js';
6
+ export { WecomClient } from './client.js';
7
+ export { WecomAdapter } from './adapter.js';
@@ -12,7 +12,11 @@ export interface WecomMediaUploadResult {
12
12
  readonly created_at?: string;
13
13
  }
14
14
  /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
15
- export declare function resolveMediaBinary(media: MediaRef, download?: (url: string) => Promise<Buffer>): Promise<MediaBinary>;
15
+ export interface MediaResolveOptions {
16
+ readonly download?: (url: string, signal?: AbortSignal) => Promise<Buffer>;
17
+ readonly signal?: AbortSignal;
18
+ }
19
+ export declare function resolveMediaBinary(media: MediaRef, { download, signal }?: MediaResolveOptions): Promise<MediaBinary>;
16
20
  /** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
17
21
  export declare function buildMediaUploadForm(binary: MediaBinary): FormData;
18
22
  /**
@@ -13,8 +13,8 @@ const MIME_EXT = {
13
13
  'image/webp': 'webp',
14
14
  'image/bmp': 'bmp',
15
15
  };
16
- /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
17
- export async function resolveMediaBinary(media, download = defaultDownload) {
16
+ export async function resolveMediaBinary(media, { download = defaultDownload, signal } = {}) {
17
+ signal?.throwIfAborted();
18
18
  const mimeType = media.mime_type ?? 'image/png';
19
19
  const ext = MIME_EXT[mimeType] ?? 'png';
20
20
  if (media.kind === 'base64') {
@@ -25,16 +25,16 @@ export async function resolveMediaBinary(media, download = defaultDownload) {
25
25
  }
26
26
  if (media.kind === 'path') {
27
27
  const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
28
- return { data: await readFile(path), mimeType, fileName: basename(path) };
28
+ return { data: await readFile(path, { signal }), mimeType, fileName: basename(path) };
29
29
  }
30
30
  if (media.kind === 'url') {
31
- return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
31
+ return { data: await download(media.value, signal), mimeType, fileName: `image.${ext}` };
32
32
  }
33
33
  // kind=file 为平台不透明引用(media_id),无二进制可解,调用方应直用 value
34
34
  throw new Error(`cannot resolve binary from media kind: ${media.kind}`);
35
35
  }
36
- async function defaultDownload(url) {
37
- const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
36
+ async function defaultDownload(url, signal) {
37
+ const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
38
38
  if (!response.ok)
39
39
  throw new Error(`download failed: HTTP ${response.status}`);
40
40
  return Buffer.from(await response.arrayBuffer());
package/lib/protocol.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { ConversationRef } from '@zhin.js/im-contract';
5
5
  export interface WecomEndpointConfig {
6
6
  readonly id: string;
7
7
  readonly corpId: string;
8
+ readonly agentId: string;
8
9
  readonly agentSecret: string;
9
10
  readonly token: string;
10
11
  readonly encodingAESKey: string;
@@ -15,6 +16,7 @@ export interface ResolvedWecomConfig {
15
16
  readonly context: 'wecom';
16
17
  readonly id: string;
17
18
  readonly corpId: string;
19
+ readonly agentId: string;
18
20
  readonly agentSecret: string;
19
21
  readonly token: string;
20
22
  readonly encodingAESKey: string;
@@ -22,6 +24,8 @@ export interface ResolvedWecomConfig {
22
24
  readonly apiBaseUrl: string;
23
25
  }
24
26
  export interface WecomMessage {
27
+ /** Complete decrypted callback XML; preserved before field projection. */
28
+ readonly rawXml?: string;
25
29
  readonly ToUserName: string;
26
30
  readonly FromUserName: string;
27
31
  readonly CreateTime: number;
package/lib/protocol.js CHANGED
@@ -7,6 +7,7 @@ import { createHash, createDecipheriv, timingSafeEqual } from 'node:crypto';
7
7
  export function resolveWecomConfig(config) {
8
8
  const id = requiredEndpointField(config.id, 'id');
9
9
  const corpId = requiredEndpointField(config.corpId, 'corpId');
10
+ const agentId = requiredEndpointField(config.agentId, 'agentId');
10
11
  const agentSecret = requiredEndpointField(config.agentSecret, 'agentSecret');
11
12
  const token = requiredEndpointField(config.token, 'token');
12
13
  const encodingAESKey = requiredEndpointField(config.encodingAESKey, 'encodingAESKey');
@@ -14,6 +15,7 @@ export function resolveWecomConfig(config) {
14
15
  context: 'wecom',
15
16
  id,
16
17
  corpId,
18
+ agentId,
17
19
  agentSecret,
18
20
  token,
19
21
  encodingAESKey,
@@ -100,6 +102,7 @@ export function parseXmlMessage(xml) {
100
102
  if (!msgType)
101
103
  return null;
102
104
  const msg = {
105
+ rawXml: xml,
103
106
  ToUserName: get('ToUserName') || '',
104
107
  FromUserName: get('FromUserName') || '',
105
108
  CreateTime: Number(get('CreateTime') || Date.now()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-wecom",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "Zhin.js WeCom (企业微信) adapter for Plugin Runtime (HTTP webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,27 +33,27 @@
33
33
  "directory": "plugins/adapters/wecom"
34
34
  },
35
35
  "dependencies": {
36
- "@zhin.js/adapter": "1.1.25",
37
- "@zhin.js/agent-feature": "1.1.10",
38
- "@zhin.js/client": "1.1.21",
36
+ "@zhin.js/adapter": "1.1.26",
37
+ "@zhin.js/agent-feature": "1.1.11",
38
+ "@zhin.js/client": "1.1.22",
39
39
  "@zhin.js/console-contract": "1.1.3",
40
- "@zhin.js/core": "1.1.53",
41
- "@zhin.js/feature-kit": "1.1.9",
42
- "@zhin.js/host-http": "1.1.11",
40
+ "@zhin.js/core": "1.1.54",
41
+ "@zhin.js/feature-kit": "1.1.10",
42
+ "@zhin.js/host-http": "1.1.12",
43
43
  "@zhin.js/im-contract": "1.1.9",
44
44
  "@zhin.js/logger": "1.1.2",
45
- "@zhin.js/page": "1.1.10",
46
- "@zhin.js/skill": "1.1.10",
47
- "@zhin.js/tool": "1.1.12"
45
+ "@zhin.js/page": "1.1.11",
46
+ "@zhin.js/skill": "1.1.11",
47
+ "@zhin.js/tool": "1.1.13"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "zod": "^4.0.0",
51
- "@zhin.js/adapter": "^1.1.25",
52
- "@zhin.js/command": "^1.1.11",
53
- "@zhin.js/core": "^1.1.53",
54
- "@zhin.js/host-http": "^1.1.11",
55
- "@zhin.js/permission": "^1.1.9",
56
- "zhin.js": "^1.1.18"
51
+ "@zhin.js/adapter": "^1.1.26",
52
+ "@zhin.js/command": "^1.1.12",
53
+ "@zhin.js/core": "^1.1.54",
54
+ "@zhin.js/host-http": "^1.1.12",
55
+ "@zhin.js/permission": "^1.1.10",
56
+ "zhin.js": "^1.1.19"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@zhin.js/command": {
@@ -71,7 +71,7 @@
71
71
  "typescript": "^6.0.3",
72
72
  "vitest": "^4.1.10",
73
73
  "zod": "^4.4.3",
74
- "zhin.js": "1.1.18"
74
+ "zhin.js": "1.1.19"
75
75
  },
76
76
  "files": [
77
77
  "adapters",
@@ -103,7 +103,7 @@
103
103
  "features": [
104
104
  {
105
105
  "package": "@zhin.js/adapter",
106
- "api": "^1.0.0"
106
+ "api": "^2.0.0"
107
107
  },
108
108
  {
109
109
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
3
  import { permissionHostToken } from '@zhin.js/permission';
5
4
  import { checkWecomPlatformPermit } from "./lib/platform-permit.js";
6
- import { wecomRuntimeStateToken } from "./lib/wecom-runtime-state.js";
7
5
  import { createWecomConsoleRpc } from "./lib/console.js";
8
6
  export default definePlugin({
9
7
  name: 'wecom',
@@ -11,7 +9,6 @@ export default definePlugin({
11
9
  displayName: 'WeCom (企业微信) Adapter',
12
10
  },
13
11
  setup(context) {
14
- context.resources.provide(wecomRuntimeStateToken, createEndpointRuntimeState());
15
12
  context.resources.provide(pluginConsoleRpcToken, createWecomConsoleRpc(context.resources, () => context.config.get()));
16
13
  if (context.resources.has(permissionHostToken)) {
17
14
  const host = context.resources.use(permissionHostToken);
package/schema.json CHANGED
@@ -103,11 +103,22 @@
103
103
  "id": {
104
104
  "type": "string",
105
105
  "description": "WeCom bot name"
106
+ },
107
+ "agentId": {
108
+ "type": "string",
109
+ "minLength": 1,
110
+ "description": "企业微信应用 AgentId(非应用 Secret)",
111
+ "x-i18n": {
112
+ "en": {
113
+ "description": "WeCom application AgentId (not the application secret)"
114
+ }
115
+ }
106
116
  }
107
117
  },
108
118
  "required": [
109
119
  "id",
110
120
  "corpId",
121
+ "agentId",
111
122
  "agentSecret",
112
123
  "token",
113
124
  "encodingAESKey"
package/src/adapter.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { WecomClient, type WecomClientEventMap } from './client.js';
2
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
3
+ import { WecomEndpoint } from './endpoint.js';
4
+ import { resolveWecomConfig, type WecomEndpointConfig } from './protocol.js';
5
+ import { httpHostToken } from '@zhin.js/host-http';
6
+
7
+ export class WecomAdapter extends Adapter<WecomEndpoint, WecomEndpointConfig, WecomClientEventMap> {
8
+ readonly #context: AdapterContext<WecomEndpointConfig>;
9
+ constructor(context: AdapterContext<WecomEndpointConfig>) { super(); this.#context = context; }
10
+ override get configurationIdentityFields(): readonly string[] { return ['id', 'corpId', 'agentId']; }
11
+ override get configurationUpdateFields(): readonly string[] { return ['agentSecret', 'token', 'encodingAESKey', 'webhookPath', 'apiBaseUrl']; }
12
+ override get configurationIdentityProbeFields(): readonly string[] { return ['agentSecret', 'apiBaseUrl']; }
13
+ protected override async identifyConfiguration(input: WecomEndpointConfig, signal: AbortSignal, _changed: readonly string[]): Promise<string | undefined> {
14
+ const config = resolveWecomConfig(input);
15
+ const fetch = async (url: string, init?: Parameters<typeof globalThis.fetch>[1]) => { signal.throwIfAborted(); return globalThis.fetch(url, { ...init, signal }); };
16
+ const client = new WecomClient({ id: this.descriptor.id, config, fetch });
17
+ const response = await client.getApplicationInfo();
18
+ if (typeof response.agentid !== 'string' && typeof response.agentid !== 'number') throw new Error('WeCom identity verification returned no agentid');
19
+ return JSON.stringify(['wecom', new URL(config.apiBaseUrl).origin, config.corpId, String(response.agentid)]);
20
+ }
21
+ protected override validateConfiguration(config: WecomEndpointConfig): void { resolveWecomConfig(config); }
22
+ protected createEndpoint(input: WecomEndpointConfig, execution: EndpointCreationContext): WecomEndpoint {
23
+ const config = resolveWecomConfig(input);
24
+ return new WecomEndpoint({ id: execution.canonicalId, config, http: this.#context.use(httpHostToken) });
25
+ }
26
+ }
27
+
28
+ declare module '@zhin.js/feature-kit' {
29
+ interface AdapterRegistry { readonly 'wecom': WecomAdapter; }
30
+ }