@zhin.js/adapter-wechat-mp 1.1.17 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +13 -2
  3. package/adapters/wechat-mp/index.js +3 -17
  4. package/adapters/wechat-mp/index.ts +2 -17
  5. package/commands/wechat-mp/endpoint/definition.js +0 -2
  6. package/commands/wechat-mp/endpoint/definition.ts +0 -2
  7. package/lib/adapter.d.ts +18 -0
  8. package/lib/adapter.js +16 -0
  9. package/lib/api-client.d.ts +34 -0
  10. package/lib/api-client.js +113 -0
  11. package/lib/client.d.ts +39 -33
  12. package/lib/client.js +292 -97
  13. package/lib/console-spec.d.ts +12 -0
  14. package/lib/console-spec.js +13 -0
  15. package/lib/console.d.ts +8 -0
  16. package/lib/console.js +13 -0
  17. package/lib/endpoint.d.ts +11 -29
  18. package/lib/endpoint.js +30 -264
  19. package/lib/index.d.ts +2 -1
  20. package/lib/index.js +2 -1
  21. package/lib/media-upload.d.ts +5 -1
  22. package/lib/media-upload.js +6 -6
  23. package/lib/side-event-dispatch.d.ts +1 -1
  24. package/lib/side-event-dispatch.js +2 -2
  25. package/lib/webhook.d.ts +2 -2
  26. package/lib/webhook.js +4 -4
  27. package/package.json +22 -14
  28. package/pages/index/index.js +15 -0
  29. package/pages/index/index.ts +16 -0
  30. package/plugin.js +3 -4
  31. package/src/adapter.ts +22 -0
  32. package/src/api-client.ts +164 -0
  33. package/src/client.ts +326 -134
  34. package/src/console-spec.ts +18 -0
  35. package/src/console.ts +18 -0
  36. package/src/endpoint.ts +33 -313
  37. package/src/index.ts +1 -1
  38. package/src/media-upload.ts +10 -5
  39. package/src/side-event-dispatch.ts +2 -1
  40. package/src/webhook.ts +5 -4
  41. package/lib/wechat-mp-runtime-state.d.ts +0 -1
  42. package/lib/wechat-mp-runtime-state.js +0 -6
  43. package/src/wechat-mp-runtime-state.ts +0 -7
package/lib/endpoint.js CHANGED
@@ -1,267 +1,33 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- /**
3
- * WeChatMpEndpoint — lifecycle, outbound, admit, access token refresh.
4
- */
5
- import axios from 'axios';
6
- import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
7
- import { extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, wechatMpInboundConversation, } from './protocol.js';
8
- import { buildMediaUploadForm, readOutboundMedia, resolveMediaBinary, } from './media-upload.js';
9
- import { getPassiveReplyCapture, recordPassiveReplyText, } from './passive-reply.js';
10
- import { registerWeChatMpWebhookRoutes } from './webhook.js';
11
- import { receiveWeChatMpSideEvent } from './side-event-dispatch.js';
1
+ import { createEndpointSpecification, endpointSpec } from 'zhin.js/adapter';
12
2
  import { WeChatMpClient } from './client.js';
13
- /**
14
- * canonical 媒体段类型 → 微信 /cgi-bin/media/upload 的 type。
15
- * 客服消息无 file 投递面,file 段不可投递。
16
- */
17
- const WECHAT_UPLOAD_TYPE = {
18
- image: 'image',
19
- audio: 'voice',
20
- voice: 'voice',
21
- video: 'video',
22
- };
23
- function defaultFetch(url, init) {
24
- return axios({
25
- url,
26
- method: (init?.method ?? 'GET'),
27
- data: init?.body,
28
- headers: init?.headers,
29
- }).then((response) => ({ data: response.data }));
30
- }
31
- export class WeChatMpEndpoint extends Endpoint {
32
- client;
33
- #logger;
34
- #options;
35
- #fetch;
36
- #routeReleases = [];
37
- #tokenRefreshTimer;
38
- /** MsgId → 首次回复 XML(微信 5s 重推去重,有界 LRU)。 */
39
- #replyCache = new Map();
40
- static #REPLY_CACHE_LIMIT = 1000;
41
- #open = false;
42
- #started = false;
43
- management = createWeChatMpEndpointManagement(() => this.client);
44
- constructor(options) {
45
- super();
46
- this.#logger = getAdapterLogger('wechat-mp', options.config.id);
47
- this.#options = options;
48
- this.#fetch = options.fetch ?? defaultFetch;
49
- this.client = new WeChatMpClient(options.config, this.#fetch);
50
- }
51
- /** Used by webhook handler. */
52
- get isOpen() {
53
- return this.#open;
54
- }
55
- get config() {
56
- return this.#options.config;
57
- }
58
- get id() {
59
- return this.#options.id;
60
- }
61
- /** 微信 5s 重推去重:见过该 MsgId 时返回首次回复 XML(含空串=success)。 */
62
- getCachedReply(msgId) {
63
- return this.#replyCache.get(msgId);
64
- }
65
- cacheReply(msgId, replyXML) {
66
- if (this.#replyCache.has(msgId))
67
- this.#replyCache.delete(msgId);
68
- this.#replyCache.set(msgId, replyXML);
69
- while (this.#replyCache.size > WeChatMpEndpoint.#REPLY_CACHE_LIMIT) {
70
- const oldest = this.#replyCache.keys().next().value;
71
- if (oldest === undefined)
72
- break;
73
- this.#replyCache.delete(oldest);
74
- }
75
- }
76
- async start() {
77
- if (this.#started)
78
- return;
79
- this.#started = true;
80
- try {
81
- await this.client.refreshAccessToken();
82
- this.#routeReleases.push(...registerWeChatMpWebhookRoutes(this.#options.http, this));
83
- this.#startTokenRefreshTimer();
84
- this.#logger.debug(formatCompact({
85
- endpoint: this.#options.config.id,
86
- op: 'webhook',
87
- path: this.#options.config.path,
88
- }));
89
- }
90
- catch (error) {
91
- await this.stop();
92
- this.#logger.error('Failed to connect WeChat MP bot:', error);
93
- throw error;
94
- }
95
- }
96
- open() {
97
- this.#open = true;
98
- }
99
- close() {
100
- this.#open = false;
3
+ /** Actual protocol Client; framework lifecycle and admission stay in their symbol namespace. */
4
+ export class WeChatMpEndpoint extends WeChatMpClient {
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
+ management: this.endpointManagement,
14
+ };
15
+ Object.defineProperty(hooks, 'transportState', { get: () => this.transportState });
16
+ return createEndpointSpecification(this, hooks);
17
+ }
18
+ resolvePresentationLocale(senderId) { return this[endpointSpec].resolveLocale(senderId); }
19
+ #startFramework(signal) { return this.connectTransport(signal); }
20
+ receiveInboundCallback(receive) {
21
+ return this[endpointSpec].events.run(receive);
22
+ }
23
+ publishEvent(name, payload) {
24
+ return this[endpointSpec].events.emit(name, payload);
25
+ }
26
+ publishAccepted(name, payload) {
27
+ return this[endpointSpec].events.emitAccepted(name, payload).then(() => undefined);
28
+ }
29
+ async publishPlatform(name, event) {
30
+ await super.publishPlatform(name, event);
31
+ return this[endpointSpec].events.platform(name, event);
101
32
  }
102
- async stop() {
103
- this.#open = false;
104
- if (this.#tokenRefreshTimer) {
105
- clearInterval(this.#tokenRefreshTimer);
106
- this.#tokenRefreshTimer = undefined;
107
- }
108
- for (const release of this.#routeReleases.splice(0))
109
- release();
110
- this.#started = false;
111
- this.#logger.debug(formatCompact({ op: 'disconnect' }));
112
- }
113
- async send({ conversation, payload }) {
114
- if (getPassiveReplyCapture()) {
115
- const text = extractOutboundText(payload);
116
- recordPassiveReplyText(text);
117
- return `passive_${Date.now()}`;
118
- }
119
- if (this.#options.config.replyMode === 'customer_service') {
120
- return this.#sendCustomerService(conversation.id, payload);
121
- }
122
- this.#logger.warn(formatCompact({
123
- op: 'send',
124
- skip: 'passive_outside_webhook',
125
- endpoint: this.#options.config.id,
126
- target: `${conversation.kind}:${conversation.id}`,
127
- }));
128
- return `passive_skipped_${Date.now()}`;
129
- }
130
- /** Test / internal: admit a parsed message when open (non-webhook path). */
131
- admit(msg) {
132
- if (!this.#open)
133
- return;
134
- void this.emitPlatform(msg.Event ? `${msg.MsgType}.${msg.Event}` : msg.MsgType, msg).catch((error) => {
135
- this.#logger.warn(formatCompact({
136
- op: 'wechat_mp_platform_event_failed',
137
- error: error instanceof Error ? error.message : String(error),
138
- }));
139
- });
140
- if (receiveWeChatMpSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, msg, this.#logger)) {
141
- return undefined;
142
- }
143
- const conversation = wechatMpInboundConversation(String(this.#options.id), msg);
144
- return this.emit('message.receive', {
145
- conversation,
146
- message: { conversation, id: formatInboundId(msg) },
147
- content: formatInboundContent(msg),
148
- sender: { id: msg.FromUserName },
149
- endpointId: this.#options.config.id,
150
- metadata: Object.freeze({
151
- msgType: msg.MsgType,
152
- event: msg.Event,
153
- toUserName: msg.ToUserName,
154
- }),
155
- }).catch((err) => {
156
- this.#logger.warn(formatCompact({
157
- op: 'wechat_mp_gateway_receive_failed',
158
- target: `${conversation.kind}:${conversation.id}`,
159
- error: err instanceof Error ? err.message : String(err),
160
- }));
161
- });
162
- }
163
- async #sendCustomerService(target, payload) {
164
- // 发送前检查过期(不只判 null):过期 token 直接刷新,不白跑一次 40001。
165
- const materialized = await this.#materializeOutboundMedia(payload);
166
- const messageData = formatCustomerServiceBody(target, materialized);
167
- const result = await this.client.sendCustomerService(messageData);
168
- if (result.errcode && result.errcode !== 0) {
169
- throw new Error(`WeChat API error: ${result.errcode} - ${result.errmsg}`);
170
- }
171
- this.#logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
172
- return result.msgid?.toString() || `cs_${Date.now()}`;
173
- }
174
- /**
175
- * 客服消息媒体段只接受 media_id:canonical MediaRef 是唯一来源。
176
- * - kind=file(平台不透明引用,即既有 media_id)→ 直接透传;
177
- * - kind=base64 / path / url → 经 /cgi-bin/media/upload 物化;
178
- * - 无 MediaRef / 类型不可投递(file 段)→ warn + 丢弃;
179
- * - 上传失败降级为文本(alt 优先),不阻断发送。
180
- */
181
- async #materializeOutboundMedia(payload) {
182
- if (!Array.isArray(payload))
183
- return payload;
184
- const materialized = await Promise.all(payload.map(async (item) => {
185
- if (typeof item === 'string' || !item || typeof item !== 'object')
186
- return item;
187
- const seg = item;
188
- if (typeof seg.type !== 'string')
189
- return item;
190
- const data = seg.data ?? {};
191
- const uploadType = WECHAT_UPLOAD_TYPE[seg.type];
192
- const isMediaSegment = uploadType != null || seg.type === 'file';
193
- if (!isMediaSegment)
194
- return item;
195
- const media = readOutboundMedia(data);
196
- if (!media) {
197
- // 已物化(mediaId/media_id)的段透传;其余无 canonical 媒体引用,丢弃留痕
198
- if (typeof data.mediaId === 'string' && data.mediaId)
199
- return item;
200
- if (typeof data.media_id === 'string' && data.media_id)
201
- return item;
202
- this.#logger.warn(formatCompact({
203
- op: 'wechat_mp_outbound_media_dropped',
204
- endpoint: this.#options.config.id,
205
- type: seg.type,
206
- reason: 'missing_media_ref',
207
- }));
208
- return null;
209
- }
210
- if (media.kind === 'file') {
211
- // 平台不透明引用:value 即 media_id,直接透传不上传
212
- return { type: seg.type, data: { mediaId: media.value } };
213
- }
214
- if (!uploadType) {
215
- this.#logger.warn(formatCompact({
216
- op: 'wechat_mp_outbound_media_dropped',
217
- endpoint: this.#options.config.id,
218
- type: seg.type,
219
- reason: 'unsupported_segment_type',
220
- }));
221
- return null;
222
- }
223
- try {
224
- const mediaId = await this.#uploadMedia(uploadType, media);
225
- return { type: seg.type, data: { mediaId } };
226
- }
227
- catch (error) {
228
- this.#logger.warn(formatCompact({
229
- op: 'wechat_mp_media_upload_failed',
230
- endpoint: this.#options.config.id,
231
- error: error instanceof Error ? error.message : String(error),
232
- }));
233
- const alt = typeof data.alt === 'string' && data.alt ? data.alt : `[${seg.type}]`;
234
- return { type: 'text', data: { text: alt } };
235
- }
236
- }));
237
- return materialized.filter((item) => item != null);
238
- }
239
- /** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
240
- async #uploadMedia(type, media) {
241
- const binary = await resolveMediaBinary(media);
242
- const form = buildMediaUploadForm(binary);
243
- return this.client.uploadMedia(type, form);
244
- }
245
- #startTokenRefreshTimer() {
246
- if (this.#tokenRefreshTimer)
247
- clearInterval(this.#tokenRefreshTimer);
248
- this.#tokenRefreshTimer = setInterval(() => {
249
- if (this.client.tokenExpired) {
250
- void this.client.refreshAccessToken().catch((error) => {
251
- this.#logger.error('Failed to refresh access token in timer:', error);
252
- });
253
- }
254
- }, 3_600_000);
255
- this.#tokenRefreshTimer.unref?.();
256
- }
257
- }
258
- function createWeChatMpEndpointManagement(requireClient) {
259
- return Object.freeze({
260
- // 公众号无群/频道概念;关注者即"好友"(nickname 为 openid 占位,见 getFollowers)。
261
- listFriends: async () => (await requireClient().getFollowerIds()).map((openid) => ({
262
- user_id: openid,
263
- nickname: openid,
264
- remark: '',
265
- })),
266
- });
267
33
  }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { buildTextReply, computeSignatureHash, decryptEchostr, decryptMessage, encryptMessage, extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, isEncryptedEchostr, normalizeEchostrParam, parseXMLMessage, queryParam, readTextBody, resolveEventPassiveReply, resolveWeChatMpConfig, verifySignature, type ResolvedWeChatMpConfig, type TokenResponse, type WeChatAPIResponse, type WeChatMessage, type WeChatMpEndpointConfig, type WeChatWireSegment, } from './protocol.js';
2
2
  export { getPassiveReplyCapture, recordPassiveReplyText, runWithPassiveReplyCapture, type PassiveReplyCapture, } from './passive-reply.js';
3
- export { WeChatMpClient, wechatMpClient, type WeChatMpClientEventMap, type WeChatMpFetch, } from './client.js';
3
+ export { WeChatMpClient, type WeChatMpClientEventMap, type WeChatMpFetch, } from './client.js';
4
4
  export { WeChatMpEndpoint, type WeChatMpEndpointOptions, } from './endpoint.js';
5
5
  export { buildMediaUploadForm, readOutboundMedia, resolveMediaBinary, type MediaBinary, type WeChatMediaUploadResult, } from './media-upload.js';
6
6
  export { registerWeChatMpWebhookRoutes, handleWeChatMpVerification, handleWeChatMpMessage, collectPassiveReply, type WeChatMpWebhookHandler, } from './webhook.js';
7
+ export { WeChatMpAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { buildTextReply, computeSignatureHash, decryptEchostr, decryptMessage, encryptMessage, extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, isEncryptedEchostr, normalizeEchostrParam, parseXMLMessage, queryParam, readTextBody, resolveEventPassiveReply, resolveWeChatMpConfig, verifySignature, } from './protocol.js';
2
2
  export { getPassiveReplyCapture, recordPassiveReplyText, runWithPassiveReplyCapture, } from './passive-reply.js';
3
- export { WeChatMpClient, wechatMpClient, } from './client.js';
3
+ export { WeChatMpClient, } from './client.js';
4
4
  export { WeChatMpEndpoint, } from './endpoint.js';
5
5
  export { buildMediaUploadForm, readOutboundMedia, resolveMediaBinary, } from './media-upload.js';
6
6
  export { registerWeChatMpWebhookRoutes, handleWeChatMpVerification, handleWeChatMpMessage, collectPassiveReply, } from './webhook.js';
7
+ export { WeChatMpAdapter } from './adapter.js';
@@ -12,7 +12,11 @@ export interface WeChatMediaUploadResult {
12
12
  readonly errmsg?: 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
  /**
@@ -14,8 +14,8 @@ const MIME_EXT = {
14
14
  'image/webp': 'webp',
15
15
  'image/bmp': 'bmp',
16
16
  };
17
- /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
18
- export async function resolveMediaBinary(media, download = defaultDownload) {
17
+ export async function resolveMediaBinary(media, { download = defaultDownload, signal } = {}) {
18
+ signal?.throwIfAborted();
19
19
  const mimeType = media.mime_type ?? 'image/png';
20
20
  const ext = MIME_EXT[mimeType] ?? 'png';
21
21
  if (media.kind === 'base64') {
@@ -26,16 +26,16 @@ export async function resolveMediaBinary(media, download = defaultDownload) {
26
26
  }
27
27
  if (media.kind === 'path') {
28
28
  const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
29
- return { data: await readFile(path), mimeType, fileName: basename(path) };
29
+ return { data: await readFile(path, { signal }), mimeType, fileName: basename(path) };
30
30
  }
31
31
  if (media.kind === 'file') {
32
32
  // 平台不透明引用(media_id):由调用方直接透传,不走上传。
33
33
  throw new Error('MediaRef kind=file is a platform opaque reference; binary resolution not applicable');
34
34
  }
35
- return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
35
+ return { data: await download(media.value, signal), mimeType, fileName: `image.${ext}` };
36
36
  }
37
- async function defaultDownload(url) {
38
- const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
37
+ async function defaultDownload(url, signal) {
38
+ const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
39
39
  if (!response.ok)
40
40
  throw new Error(`download failed: HTTP ${response.status}`);
41
41
  return Buffer.from(await response.arrayBuffer());
@@ -1,4 +1,4 @@
1
1
  import type { EndpointEventEmitter } from 'zhin.js/adapter';
2
2
  import { type getAdapterLogger } from '@zhin.js/logger';
3
3
  import { type WeChatMessage } from './protocol.js';
4
- export declare function receiveWeChatMpSideEvent(emit: EndpointEventEmitter, configId: string, msg: WeChatMessage, logger: ReturnType<typeof getAdapterLogger>): boolean;
4
+ export declare function receiveWeChatMpSideEvent(emit: EndpointEventEmitter, configId: string, msg: WeChatMessage, logger: ReturnType<typeof getAdapterLogger>, nativeEvent?: unknown): boolean;
@@ -11,12 +11,12 @@ function mapWeChatMpEventParts(eventName) {
11
11
  return { scene_type: 'wechat-mp', sub_type: eventName || 'unknown' };
12
12
  }
13
13
  }
14
- export function receiveWeChatMpSideEvent(emit, configId, msg, logger) {
14
+ export function receiveWeChatMpSideEvent(emit, configId, msg, logger, nativeEvent = msg) {
15
15
  if (msg.MsgType !== 'event')
16
16
  return false;
17
17
  const eventName = msg.Event ?? 'unknown';
18
18
  const parts = mapWeChatMpEventParts(eventName);
19
- void emit('notice.receive', buildNotice(msg, {
19
+ void emit('notice.receive', buildNotice(typeof nativeEvent === 'string' ? { ...msg, rawXml: nativeEvent } : msg, {
20
20
  id: `wechat-mp:${formatInboundId(msg)}`,
21
21
  clientAdapter: 'wechat-mp',
22
22
  endpointId: configId,
package/lib/webhook.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface WeChatMpWebhookHandler {
10
10
  readonly config: ResolvedWeChatMpConfig;
11
11
  readonly isOpen: boolean;
12
12
  readonly id: CapabilityId;
13
- admit(msg: WeChatMessage): void | Promise<unknown>;
13
+ admit(msg: WeChatMessage, nativeEvent?: unknown): void | Promise<unknown>;
14
14
  resolveLocale?(senderId: string): Promise<Locale | undefined>;
15
15
  /** MsgId 去重缓存(可选;实现见 WeChatMpEndpoint)。 */
16
16
  getCachedReply?(msgId: string): string | undefined;
@@ -19,4 +19,4 @@ export interface WeChatMpWebhookHandler {
19
19
  export declare function registerWeChatMpWebhookRoutes(http: HttpHost, handler: WeChatMpWebhookHandler): HttpRouteRegistration[];
20
20
  export declare function handleWeChatMpVerification(_request: IncomingMessage, response: ServerResponse, url: URL, config: ResolvedWeChatMpConfig): void;
21
21
  export declare function handleWeChatMpMessage(request: IncomingMessage, response: ServerResponse, url: URL, handler: WeChatMpWebhookHandler): Promise<void>;
22
- export declare function collectPassiveReply(handler: WeChatMpWebhookHandler, wechatMsg: WeChatMessage): Promise<string>;
22
+ export declare function collectPassiveReply(handler: WeChatMpWebhookHandler, wechatMsg: WeChatMessage, nativeEvent?: unknown): Promise<string>;
package/lib/webhook.js CHANGED
@@ -107,10 +107,10 @@ export async function handleWeChatMpMessage(request, response, url, handler) {
107
107
  let replyXML = resolveEventPassiveReply(wechatMessage, locale);
108
108
  if (!replyXML && handler.isOpen) {
109
109
  if (config.replyMode === 'passive') {
110
- replyXML = await collectPassiveReply(handler, wechatMessage);
110
+ replyXML = await collectPassiveReply(handler, wechatMessage, xmlString);
111
111
  }
112
112
  else {
113
- handler.admit(wechatMessage);
113
+ handler.admit(wechatMessage, xmlString);
114
114
  }
115
115
  }
116
116
  const encryptReply = !!(replyXML
@@ -132,12 +132,12 @@ export async function handleWeChatMpMessage(request, response, url, handler) {
132
132
  response.end('success');
133
133
  }
134
134
  }
135
- export async function collectPassiveReply(handler, wechatMsg) {
135
+ export async function collectPassiveReply(handler, wechatMsg, nativeEvent) {
136
136
  const timeoutMs = handler.config.passiveReplyTimeoutMs;
137
137
  const text = await runWithPassiveReplyCapture(async () => {
138
138
  const conversation = wechatMpInboundConversation(String(handler.id), wechatMsg);
139
139
  await Promise.race([
140
- Promise.resolve(handler.admit(wechatMsg)),
140
+ Promise.resolve(nativeEvent === undefined ? handler.admit(wechatMsg) : handler.admit(wechatMsg, nativeEvent)),
141
141
  new Promise((resolve) => setTimeout(resolve, timeoutMs)),
142
142
  ]);
143
143
  return getPassiveReplyCapture()?.text ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-wechat-mp",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "type": "module",
5
5
  "description": "Zhin.js WeChat Official Account adapter for Plugin Runtime (HTTP webhook)",
6
6
  "main": "./lib/index.js",
@@ -8,20 +8,23 @@
8
8
  "dependencies": {
9
9
  "axios": "^1.20.0",
10
10
  "xml2js": "^0.6.2",
11
- "@zhin.js/adapter": "1.1.24",
12
- "@zhin.js/core": "1.1.52",
13
- "@zhin.js/feature-kit": "1.1.9",
14
- "@zhin.js/host-http": "1.1.11",
15
- "@zhin.js/im-contract": "1.1.8",
11
+ "@zhin.js/adapter": "1.1.26",
12
+ "@zhin.js/client": "1.1.22",
13
+ "@zhin.js/console-contract": "1.1.3",
14
+ "@zhin.js/core": "1.1.54",
15
+ "@zhin.js/feature-kit": "1.1.10",
16
+ "@zhin.js/host-http": "1.1.12",
17
+ "@zhin.js/im-contract": "1.1.9",
16
18
  "@zhin.js/logger": "1.1.2",
17
- "@zhin.js/skill": "1.1.10"
19
+ "@zhin.js/page": "1.1.11",
20
+ "@zhin.js/skill": "1.1.11"
18
21
  },
19
22
  "peerDependencies": {
20
- "@zhin.js/adapter": "^1.1.24",
21
- "@zhin.js/command": "^1.1.11",
22
- "@zhin.js/core": "^1.1.52",
23
- "@zhin.js/host-http": "^1.1.11",
24
- "zhin.js": "^1.1.17"
23
+ "@zhin.js/adapter": "^1.1.26",
24
+ "@zhin.js/command": "^1.1.12",
25
+ "@zhin.js/core": "^1.1.54",
26
+ "@zhin.js/host-http": "^1.1.12",
27
+ "zhin.js": "^1.1.19"
25
28
  },
26
29
  "peerDependenciesMeta": {
27
30
  "@zhin.js/command": {
@@ -36,7 +39,7 @@
36
39
  "@types/xml2js": "^0.4.14",
37
40
  "typescript": "^6.0.3",
38
41
  "vitest": "^4.1.10",
39
- "zhin.js": "1.1.17"
42
+ "zhin.js": "1.1.19"
40
43
  },
41
44
  "keywords": [
42
45
  "zhin",
@@ -64,6 +67,7 @@
64
67
  "files": [
65
68
  "adapters",
66
69
  "commands",
70
+ "pages",
67
71
  "plugin.js",
68
72
  "schema.json",
69
73
  "src",
@@ -94,7 +98,7 @@
94
98
  "features": [
95
99
  {
96
100
  "package": "@zhin.js/adapter",
97
- "api": "^1.0.0"
101
+ "api": "^2.0.0"
98
102
  },
99
103
  {
100
104
  "package": "@zhin.js/command",
@@ -103,6 +107,10 @@
103
107
  {
104
108
  "package": "@zhin.js/skill",
105
109
  "api": "^1.0.0"
110
+ },
111
+ {
112
+ "package": "@zhin.js/page",
113
+ "api": "^1.0.0"
106
114
  }
107
115
  ],
108
116
  "plugins": []
@@ -0,0 +1,15 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePage } from '@zhin.js/console-contract';
3
+ import { createAdapterConsolePage } from '@zhin.js/client';
4
+ export const meta = definePage({
5
+ title: '微信公众号',
6
+ titleI18n: { en: 'WeChat MP' },
7
+ icon: 'MessageSquare',
8
+ order: 75,
9
+ requiredPermissions: ['wechat-mp:read'],
10
+ });
11
+ /**
12
+ * Convention page: `pages/index/index.ts` → `/wechat-mp`(插件路径)。
13
+ * 连接状态、连接测试、endpoint 增删;登录待办来自 LoginAssist(ICQQ 等)。
14
+ */
15
+ export default createAdapterConsolePage('wechat-mp');
@@ -0,0 +1,16 @@
1
+ import { definePage } from '@zhin.js/console-contract';
2
+ import { createAdapterConsolePage } from '@zhin.js/client';
3
+
4
+ export const meta = definePage({
5
+ title: '微信公众号',
6
+ titleI18n: { en: 'WeChat MP' },
7
+ icon: 'MessageSquare',
8
+ order: 75,
9
+ requiredPermissions: ['wechat-mp:read'],
10
+ });
11
+
12
+ /**
13
+ * Convention page: `pages/index/index.ts` → `/wechat-mp`(插件路径)。
14
+ * 连接状态、连接测试、endpoint 增删;登录待办来自 LoginAssist(ICQQ 等)。
15
+ */
16
+ export default createAdapterConsolePage('wechat-mp');
package/plugin.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
- import { definePlugin } from 'zhin.js';
4
- import { wechatMpRuntimeStateToken } from "./lib/wechat-mp-runtime-state.js";
2
+ import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
3
+ import { createWechatMpConsoleRpc } from "./lib/console.js";
5
4
  export default definePlugin({
6
5
  name: 'wechat-mp',
7
6
  metadata: {
@@ -9,6 +8,6 @@ export default definePlugin({
9
8
  },
10
9
  setup(context) {
11
10
  // 运行中 endpoint 注册表(wechat-mp endpoint list 的"运行中"数据源)
12
- context.resources.provide(wechatMpRuntimeStateToken, createEndpointRuntimeState());
11
+ context.resources.provide(pluginConsoleRpcToken, createWechatMpConsoleRpc(context.resources, () => context.config.get()));
13
12
  },
14
13
  });
package/src/adapter.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
2
+ import { WeChatMpEndpoint } from './endpoint.js';
3
+ import { resolveWeChatMpConfig, type WeChatMpEndpointConfig } from './protocol.js';
4
+ import type { WeChatMpClientEventMap } from './client.js';
5
+ import { httpHostToken } from '@zhin.js/host-http';
6
+
7
+ export class WeChatMpAdapter extends Adapter<WeChatMpEndpoint, WeChatMpEndpointConfig, WeChatMpClientEventMap> {
8
+ readonly #context: AdapterContext<WeChatMpEndpointConfig>;
9
+ constructor(context: AdapterContext<WeChatMpEndpointConfig>) { super(); this.#context = context; }
10
+ override get configurationIdentityFields(): readonly string[] { return ['id', 'appId']; }
11
+ override get configurationUpdateFields(): readonly string[] { return ['appSecret', 'token', 'encodingAESKey', 'path', 'encrypt', 'encryptMode', 'replyMode', 'passiveReplyTimeoutMs']; }
12
+ override get configurationIdentityProbeFields(): readonly string[] { return ['appSecret']; }
13
+ protected override validateConfiguration(config: WeChatMpEndpointConfig): void { resolveWeChatMpConfig(config); }
14
+ protected createEndpoint(input: WeChatMpEndpointConfig, execution: EndpointCreationContext): WeChatMpEndpoint {
15
+ const config = resolveWeChatMpConfig(input);
16
+ return new WeChatMpEndpoint({ id: execution.canonicalId, config, http: this.#context.use(httpHostToken) });
17
+ }
18
+ }
19
+
20
+ declare module '@zhin.js/feature-kit' {
21
+ interface AdapterRegistry { readonly 'wechat-mp': WeChatMpAdapter; }
22
+ }