@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/client.js CHANGED
@@ -1,105 +1,300 @@
1
- import { defineEndpointClient } from 'zhin.js/adapter';
2
- const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
3
- /** 换取 token 请求的超时,防止挂起的请求让 single-flight 永久卡住。 */
4
- const TOKEN_FETCH_TIMEOUT_MS = 15_000;
5
- /** 官方 access_token 有效期为 7200s;对异常大的 expires_in 设上限。 */
6
- const MAX_TOKEN_LIFETIME_SEC = 7200;
7
- /** Direct WeChat Official Account API client exposed to plugins. */
8
- export class WeChatMpClient {
9
- config;
10
- fetch;
11
- #accessToken = null;
12
- #tokenExpireTime = 0;
13
- #refreshing = null;
14
- constructor(config, fetch) {
15
- this.config = config;
16
- this.fetch = fetch;
17
- }
18
- get accessToken() {
19
- return this.#accessToken;
20
- }
21
- get tokenExpired() {
22
- return !this.#accessToken || Date.now() >= this.#tokenExpireTime;
1
+ import { WeChatMpApiClient } from './api-client.js';
2
+ /**
3
+ * WeChatMpClient — lifecycle, outbound, admit, access token refresh.
4
+ */
5
+ import axios from 'axios';
6
+ import { createEndpointOperation, } from 'zhin.js/adapter';
7
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
+ import { extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, wechatMpInboundConversation, } from './protocol.js';
9
+ import { buildMediaUploadForm, readOutboundMedia, resolveMediaBinary, } from './media-upload.js';
10
+ import { getPassiveReplyCapture, recordPassiveReplyText, } from './passive-reply.js';
11
+ import { registerWeChatMpWebhookRoutes } from './webhook.js';
12
+ import { receiveWeChatMpSideEvent } from './side-event-dispatch.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
+ signal: init?.signal,
30
+ }).then((response) => ({ data: response.data }));
31
+ }
32
+ export class WeChatMpClient extends WeChatMpApiClient {
33
+ #logger;
34
+ #options;
35
+ #routeReleases = [];
36
+ #tokenRefreshTimer;
37
+ /** MsgId → 首次回复 XML(微信 5s 重推去重,有界 LRU)。 */
38
+ #replyCache = new Map();
39
+ static #REPLY_CACHE_LIMIT = 1000;
40
+ #open = false;
41
+ #started = false;
42
+ endpointManagement = createWeChatMpEndpointManagement(() => this);
43
+ configName() { return this.#options.config.id; }
44
+ resolvePresentationLocale(_senderId) { return Promise.resolve(undefined); }
45
+ get transportState() { return undefined; }
46
+ executionSignal;
47
+ setExecutionSignal(signal) { this.executionSignal = signal; this.transportSignal = signal; }
48
+ /** Native protocol work runs here; Endpoint attaches the framework receipt without replacing SDK APIs. */
49
+ receiveInboundCallback(receive) {
50
+ try {
51
+ return Promise.resolve(receive());
52
+ }
53
+ catch (error) {
54
+ return Promise.reject(error);
55
+ }
23
56
  }
24
- /**
25
- * 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
26
- * 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
27
- */
28
- refreshAccessToken() {
29
- if (!this.#refreshing) {
30
- this.#refreshing = this.#fetchAccessToken().finally(() => {
31
- this.#refreshing = null;
32
- });
57
+ publishEvent(name, payload) { this.emit(name, payload); return Promise.resolve(); }
58
+ publishAccepted(name, payload) { return this.publishEvent(name, payload).then(() => undefined); }
59
+ publishPlatform(name, event) { this.emit(name, event); return Promise.resolve(); }
60
+ constructor(options) {
61
+ super(options.config, options.fetch ?? defaultFetch);
62
+ this.#logger = getAdapterLogger('wechat-mp', options.config.id);
63
+ this.#options = options;
64
+ }
65
+ /** Used by webhook handler. */
66
+ get isOpen() {
67
+ return this.#open;
68
+ }
69
+ get config() {
70
+ return this.#options.config;
71
+ }
72
+ get id() {
73
+ return this.#options.id;
74
+ }
75
+ /** 微信 5s 重推去重:见过该 MsgId 时返回首次回复 XML(含空串=success)。 */
76
+ getCachedReply(msgId) {
77
+ return this.#replyCache.get(msgId);
78
+ }
79
+ cacheReply(msgId, replyXML) {
80
+ if (this.#replyCache.has(msgId))
81
+ this.#replyCache.delete(msgId);
82
+ this.#replyCache.set(msgId, replyXML);
83
+ while (this.#replyCache.size > WeChatMpClient.#REPLY_CACHE_LIMIT) {
84
+ const oldest = this.#replyCache.keys().next().value;
85
+ if (oldest === undefined)
86
+ break;
87
+ this.#replyCache.delete(oldest);
33
88
  }
34
- return this.#refreshing;
35
- }
36
- async #fetchAccessToken() {
37
- const { appId, appSecret } = this.config;
38
- const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(appSecret)}`;
39
- let timeout;
40
- const response = await Promise.race([
41
- this.fetch(url),
42
- new Promise((_, reject) => {
43
- timeout = setTimeout(() => reject(new Error('Failed to get access token: request timed out')), TOKEN_FETCH_TIMEOUT_MS);
44
- }),
45
- ]).finally(() => clearTimeout(timeout));
46
- const data = response.data;
47
- if (!data.access_token) {
48
- throw new Error(data.errmsg
49
- ? `Failed to get access token: ${data.errcode} ${data.errmsg}`
50
- : 'Failed to get access token');
89
+ }
90
+ async connectTransport(signal) {
91
+ if (signal) {
92
+ this.setExecutionSignal(signal);
93
+ signal.throwIfAborted();
51
94
  }
52
- this.#accessToken = data.access_token;
53
- // expires_in 缺失/非法时 NaN 会让 tokenExpired 永远为 false;按官方默认 7200s 兜底。
54
- const expiresIn = Number(data.expires_in);
55
- const lifetimeSec = Number.isFinite(expiresIn) && expiresIn > 0
56
- ? Math.min(expiresIn, MAX_TOKEN_LIFETIME_SEC)
57
- : MAX_TOKEN_LIFETIME_SEC;
58
- this.#tokenExpireTime = Date.now() + (lifetimeSec - 300) * 1000;
59
- return data.access_token;
60
- }
61
- async request(path, init) {
62
- return (await this.#requestWithToken(path, init)).data;
63
- }
64
- async #requestWithToken(path, init) {
65
- if (this.tokenExpired)
95
+ if (this.#started)
96
+ return;
97
+ this.#started = true;
98
+ try {
66
99
  await this.refreshAccessToken();
67
- const token = this.#accessToken;
68
- const separator = path.includes('?') ? '&' : '?';
69
- const response = await this.fetch(`https://api.weixin.qq.com${path}${separator}access_token=${token}`, init);
70
- return { data: response.data, token };
71
- }
72
- async sendCustomerService(messageData) {
73
- const first = await this.#requestWithToken('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
74
- let result = first.data;
75
- if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
76
- // 失败请求使用的 token 若已被其他请求换掉,直接用新 token 重试,不再重复刷新。
77
- if (this.#accessToken === first.token)
78
- await this.refreshAccessToken();
79
- result = await this.request('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
100
+ this.executionSignal?.throwIfAborted();
101
+ const isOpen = () => this.#open;
102
+ this.#routeReleases.push(...registerWeChatMpWebhookRoutes(this.#options.http, {
103
+ config: this.config, get isOpen() { return isOpen(); }, id: this.#options.id,
104
+ admit: (message, event) => this.admit(message, event),
105
+ resolveLocale: senderId => this.resolvePresentationLocale(senderId),
106
+ getCachedReply: id => this.getCachedReply(id), cacheReply: (id, reply) => this.cacheReply(id, reply),
107
+ }));
108
+ this.#startTokenRefreshTimer();
109
+ this.#logger.debug(formatCompact({
110
+ endpoint: this.#options.config.id,
111
+ op: 'webhook',
112
+ path: this.#options.config.path,
113
+ }));
114
+ }
115
+ catch (error) {
116
+ await this.disconnectTransport();
117
+ this.#logger.error('Failed to connect WeChat MP bot:', error);
118
+ throw error;
119
+ }
120
+ }
121
+ openInput() {
122
+ this.#open = true;
123
+ }
124
+ closeInput() {
125
+ this.#open = false;
126
+ }
127
+ async disconnectTransport() {
128
+ this.#open = false;
129
+ if (this.#tokenRefreshTimer) {
130
+ clearInterval(this.#tokenRefreshTimer);
131
+ this.#tokenRefreshTimer = undefined;
132
+ }
133
+ for (const release of this.#routeReleases.splice(0))
134
+ release();
135
+ this.#started = false;
136
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
137
+ }
138
+ async sendPayload({ conversation, payload, signal }) {
139
+ if (getPassiveReplyCapture()) {
140
+ const text = extractOutboundText(payload);
141
+ recordPassiveReplyText(text);
142
+ return `passive_${Date.now()}`;
80
143
  }
81
- return result;
82
- }
83
- async uploadMedia(type, body) {
84
- const data = await this.request(`/cgi-bin/media/upload?type=${type}`, { method: 'POST', body });
85
- if (data.media_id)
86
- return data.media_id;
87
- throw new Error(`WeChat media upload failed: ${data.errcode ?? 'unknown'} ${data.errmsg ?? ''}`.trim());
88
- }
89
- async getFollowerIds() {
90
- const followers = [];
91
- let nextOpenid = '';
92
- do {
93
- const data = await this.request(`/cgi-bin/user/get?next_openid=${encodeURIComponent(nextOpenid)}`);
94
- if (data.errcode && data.errcode !== 0) {
95
- throw new Error(`WeChat API error: ${data.errcode} - ${data.errmsg}`);
144
+ if (this.#options.config.replyMode === 'customer_service') {
145
+ return this.#sendCustomerService({ target: conversation.id, payload, signal });
146
+ }
147
+ this.#logger.warn(formatCompact({
148
+ op: 'send',
149
+ skip: 'passive_outside_webhook',
150
+ endpoint: this.#options.config.id,
151
+ target: `${conversation.kind}:${conversation.id}`,
152
+ }));
153
+ return `passive_skipped_${Date.now()}`;
154
+ }
155
+ /** Test / internal: admit a parsed message when open (non-webhook path). */
156
+ admit(msg, nativeEvent = msg) {
157
+ if (!this.#open)
158
+ return;
159
+ return this.receiveInboundCallback(() => this.#admitMessage(msg, nativeEvent));
160
+ }
161
+ #admitMessage(msg, nativeEvent = msg) {
162
+ if (!this.#open)
163
+ return;
164
+ void this.publishPlatform(msg.Event ? `${msg.MsgType}.${msg.Event}` : msg.MsgType, nativeEvent).catch((error) => {
165
+ this.#logger.warn(formatCompact({
166
+ op: 'wechat_mp_platform_event_failed',
167
+ error: error instanceof Error ? error.message : String(error),
168
+ }));
169
+ });
170
+ if (receiveWeChatMpSideEvent((name, payload) => this.publishEvent(name, payload), this.#options.config.id, msg, this.#logger, nativeEvent)) {
171
+ return undefined;
172
+ }
173
+ const conversation = wechatMpInboundConversation(String(this.#options.id), msg);
174
+ return this.publishEvent('message.receive', {
175
+ event: nativeEvent,
176
+ conversation,
177
+ message: { conversation, id: formatInboundId(msg) },
178
+ content: formatInboundContent(msg),
179
+ sender: { id: msg.FromUserName },
180
+ endpointId: this.#options.config.id,
181
+ metadata: Object.freeze({
182
+ msgType: msg.MsgType,
183
+ event: msg.Event,
184
+ toUserName: msg.ToUserName,
185
+ }),
186
+ }).catch((err) => {
187
+ this.#logger.warn(formatCompact({
188
+ op: 'wechat_mp_gateway_receive_failed',
189
+ target: `${conversation.kind}:${conversation.id}`,
190
+ error: err instanceof Error ? err.message : String(err),
191
+ }));
192
+ });
193
+ }
194
+ async #sendCustomerService({ target, payload, signal }) {
195
+ // 发送前检查过期(不只判 null):过期 token 直接刷新,不白跑一次 40001。
196
+ const materialized = await createEndpointOperation({ signal }).prepare(() => this.#materializeOutboundMedia(payload, { signal }));
197
+ const messageData = formatCustomerServiceBody(target, materialized);
198
+ const result = await this.sendCustomerService(messageData, { signal });
199
+ if (result.errcode && result.errcode !== 0) {
200
+ throw new Error(`WeChat API error: ${result.errcode} - ${result.errmsg}`);
201
+ }
202
+ this.#logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
203
+ return result.msgid?.toString() || `cs_${Date.now()}`;
204
+ }
205
+ /**
206
+ * 客服消息媒体段只接受 media_id:canonical MediaRef 是唯一来源。
207
+ * - kind=file(平台不透明引用,即既有 media_id)→ 直接透传;
208
+ * - kind=base64 / path / url → 经 /cgi-bin/media/upload 物化;
209
+ * - 无 MediaRef / 类型不可投递(file 段)→ warn + 丢弃;
210
+ * - 上传失败降级为文本(alt 优先),不阻断发送。
211
+ */
212
+ async #materializeOutboundMedia(payload, request) {
213
+ if (!Array.isArray(payload))
214
+ return payload;
215
+ const materialized = await Promise.all(payload.map(async (item) => {
216
+ if (typeof item === 'string' || !item || typeof item !== 'object')
217
+ return item;
218
+ const seg = item;
219
+ if (typeof seg.type !== 'string')
220
+ return item;
221
+ const data = seg.data ?? {};
222
+ const uploadType = WECHAT_UPLOAD_TYPE[seg.type];
223
+ const isMediaSegment = uploadType != null || seg.type === 'file';
224
+ if (!isMediaSegment)
225
+ return item;
226
+ const media = readOutboundMedia(data);
227
+ if (!media) {
228
+ // 已物化(mediaId/media_id)的段透传;其余无 canonical 媒体引用,丢弃留痕
229
+ if (typeof data.mediaId === 'string' && data.mediaId)
230
+ return item;
231
+ if (typeof data.media_id === 'string' && data.media_id)
232
+ return item;
233
+ this.#logger.warn(formatCompact({
234
+ op: 'wechat_mp_outbound_media_dropped',
235
+ endpoint: this.#options.config.id,
236
+ type: seg.type,
237
+ reason: 'missing_media_ref',
238
+ }));
239
+ return null;
96
240
  }
97
- followers.push(...(data.data?.openid ?? []).filter(Boolean));
98
- if (Number(data.count ?? 0) < 10_000)
99
- break;
100
- nextOpenid = data.next_openid ?? '';
101
- } while (nextOpenid);
102
- return Object.freeze(followers);
241
+ if (media.kind === 'file') {
242
+ // 平台不透明引用:value 即 media_id,直接透传不上传
243
+ return { type: seg.type, data: { mediaId: media.value } };
244
+ }
245
+ if (!uploadType) {
246
+ this.#logger.warn(formatCompact({
247
+ op: 'wechat_mp_outbound_media_dropped',
248
+ endpoint: this.#options.config.id,
249
+ type: seg.type,
250
+ reason: 'unsupported_segment_type',
251
+ }));
252
+ return null;
253
+ }
254
+ try {
255
+ const mediaId = await this.#uploadMedia({ type: uploadType, media, signal: request.signal });
256
+ return { type: seg.type, data: { mediaId } };
257
+ }
258
+ catch (error) {
259
+ if (request.signal?.aborted)
260
+ throw error;
261
+ this.#logger.warn(formatCompact({
262
+ op: 'wechat_mp_media_upload_failed',
263
+ endpoint: this.#options.config.id,
264
+ error: error instanceof Error ? error.message : String(error),
265
+ }));
266
+ const alt = typeof data.alt === 'string' && data.alt ? data.alt : `[${seg.type}]`;
267
+ return { type: 'text', data: { text: alt } };
268
+ }
269
+ }));
270
+ return materialized.filter((item) => item != null);
103
271
  }
272
+ /** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
273
+ async #uploadMedia({ type, media, signal }) {
274
+ const binary = await createEndpointOperation({ signal }).prepare(() => resolveMediaBinary(media, { signal }));
275
+ const form = buildMediaUploadForm(binary);
276
+ return this.uploadMedia({ type, body: form, signal });
277
+ }
278
+ #startTokenRefreshTimer() {
279
+ if (this.#tokenRefreshTimer)
280
+ clearInterval(this.#tokenRefreshTimer);
281
+ this.#tokenRefreshTimer = setInterval(() => {
282
+ if (this.tokenExpired) {
283
+ void this.refreshAccessToken().catch((error) => {
284
+ this.#logger.error('Failed to refresh access token in timer:', error);
285
+ });
286
+ }
287
+ }, 3_600_000);
288
+ this.#tokenRefreshTimer.unref?.();
289
+ }
290
+ }
291
+ function createWeChatMpEndpointManagement(requireClient) {
292
+ return Object.freeze({
293
+ // 公众号无群/频道概念;关注者即"好友"(nickname 为 openid 占位,见 getFollowers)。
294
+ listFriends: async (request = {}) => (await requireClient().getFollowerIds(request)).map((openid) => ({
295
+ user_id: openid,
296
+ nickname: openid,
297
+ remark: '',
298
+ })),
299
+ });
104
300
  }
105
- export const wechatMpClient = defineEndpointClient('wechat-mp');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 微信公众号 Console 页面元数据(浏览器与服务端共享;不要在这里 import 服务端代码)。
3
+ */
4
+ import type { AdapterConsoleField } from 'zhin.js/adapter';
5
+ export declare const wechatMpConsoleSpec: {
6
+ readonly adapterKey: "wechat-mp";
7
+ readonly adapterDisplayName: "微信公众号";
8
+ readonly fields: readonly AdapterConsoleField[];
9
+ readonly docsUrl: "https://zhin.js.org/adapters/wechat-mp";
10
+ readonly readPermission: "wechat-mp:read";
11
+ readonly writePermission: "wechat-mp:write";
12
+ };
@@ -0,0 +1,13 @@
1
+ export const wechatMpConsoleSpec = {
2
+ adapterKey: 'wechat-mp',
3
+ adapterDisplayName: '微信公众号',
4
+ fields: [
5
+ { key: 'appId', required: true, env: true, description: '公众号 AppID', descriptionI18n: { en: 'WeChat Official Account app ID' } },
6
+ { key: 'appSecret', required: true, env: true, description: '公众号 AppSecret', descriptionI18n: { en: 'WeChat Official Account app secret' } },
7
+ { key: 'token', required: true, env: true, description: '服务器配置 token', descriptionI18n: { en: 'Server configuration token' } },
8
+ { key: 'encodingAESKey', env: true, description: '消息加解密 key', descriptionI18n: { en: 'Message encryption key' } },
9
+ ],
10
+ docsUrl: 'https://zhin.js.org/adapters/wechat-mp',
11
+ readPermission: 'wechat-mp:read',
12
+ writePermission: 'wechat-mp:write',
13
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 微信公众号 Console RPC:运行状态 + endpoint 增删(原 `wechat-mp endpoint` 聊天命令)。
3
+ *
4
+ * 页面绑定 `pages/index`,读取 / 写入分别要求 `<adapter>:read` / `<adapter>:write`。
5
+ */
6
+ import { type AdapterConsoleResources } from 'zhin.js/adapter';
7
+ import type { PluginConsoleRpc } from 'zhin.js';
8
+ export declare function createWechatMpConsoleRpc(_resources: AdapterConsoleResources, getConfig: () => unknown): PluginConsoleRpc;
package/lib/console.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 微信公众号 Console RPC:运行状态 + endpoint 增删(原 `wechat-mp endpoint` 聊天命令)。
3
+ *
4
+ * 页面绑定 `pages/index`,读取 / 写入分别要求 `<adapter>:read` / `<adapter>:write`。
5
+ */
6
+ import { createAdapterConsoleRpc } from 'zhin.js/adapter';
7
+ import { wechatMpConsoleSpec } from './console-spec.js';
8
+ export function createWechatMpConsoleRpc(_resources, getConfig) {
9
+ return createAdapterConsoleRpc({
10
+ ...wechatMpConsoleSpec,
11
+ getConfig,
12
+ });
13
+ }
package/lib/endpoint.d.ts CHANGED
@@ -1,32 +1,14 @@
1
- import { Endpoint } from 'zhin.js/adapter';
2
- import type { EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
3
- import type { HttpHost } from '@zhin.js/host-http';
4
- import type { CapabilityId } from 'zhin.js';
5
- import { type ResolvedWeChatMpConfig, type WeChatMessage } from './protocol.js';
6
- import { WeChatMpClient, type WeChatMpFetch } from './client.js';
7
- export interface WeChatMpEndpointOptions {
8
- readonly id: CapabilityId;
9
- readonly http: HttpHost;
10
- readonly config: ResolvedWeChatMpConfig;
11
- readonly fetch?: WeChatMpFetch;
12
- }
13
- export declare class WeChatMpEndpoint extends Endpoint<WeChatMpClient> {
1
+ import { endpointSpec, type Endpoint } from 'zhin.js/adapter';
2
+ import { WeChatMpClient, type WeChatMpEndpointOptions } from './client.js';
3
+ export type { WeChatMpEndpointOptions } from './client.js';
4
+ /** Actual protocol Client; framework lifecycle and admission stay in their symbol namespace. */
5
+ export declare class WeChatMpEndpoint extends WeChatMpClient implements Endpoint {
14
6
  #private;
15
- readonly client: WeChatMpClient;
16
- readonly management: EndpointManagement;
7
+ readonly [endpointSpec]: import("@zhin.js/adapter").EndpointSpecification;
17
8
  constructor(options: WeChatMpEndpointOptions);
18
- /** Used by webhook handler. */
19
- get isOpen(): boolean;
20
- get config(): ResolvedWeChatMpConfig;
21
- get id(): CapabilityId;
22
- /** 微信 5s 重推去重:见过该 MsgId 时返回首次回复 XML(含空串=success)。 */
23
- getCachedReply(msgId: string): string | undefined;
24
- cacheReply(msgId: string, replyXML: string): void;
25
- start(): Promise<void>;
26
- open(): void;
27
- close(): void;
28
- stop(): Promise<void>;
29
- send({ conversation, payload }: EndpointSendRequest): Promise<string>;
30
- /** Test / internal: admit a parsed message when open (non-webhook path). */
31
- admit(msg: WeChatMessage): void | Promise<unknown>;
9
+ protected resolvePresentationLocale(senderId: string): Promise<import("zhin.js").Locale | undefined>;
10
+ protected receiveInboundCallback<T>(receive: () => T | Promise<T>): Promise<T>;
11
+ protected publishEvent(name: string, payload: unknown): Promise<unknown>;
12
+ protected publishAccepted(name: string, payload: unknown): Promise<void>;
13
+ protected publishPlatform(name: string, event: unknown): Promise<unknown>;
32
14
  }