@zhin.js/adapter-wechat-mp 5.0.1 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @zhin.js/adapter-wechat-mp
2
2
 
3
+ ## 5.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/command@1.0.7
15
+ - @zhin.js/host-http@1.0.6
16
+ - zhin.js@6.0.2
17
+
3
18
  ## 5.0.1
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -24,8 +24,8 @@ pnpm add @zhin.js/adapter-wechat-mp
24
24
  - `@zhin.js/plugin-runtime` — `plugin.ts`(`definePlugin`)
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
26
26
 
27
- 入站:`gateway.receive({ adapter, target: openid, content: text, sender, metadata })`
28
- 出站:`send({ target, payload })` → 被动回复 XML(默认)或客服消息 API(`replyMode: customer_service`)
27
+ 入站:`gateway.receive({ conversation: { endpoint, kind: 'private', id: openid }, message: { conversation, id }, content: text, sender, metadata })`
28
+ 出站:`send({ conversation, payload })` → 被动回复 XML(默认)或客服消息 API(`replyMode: customer_service`)
29
29
 
30
30
  ## 前置条件
31
31
 
package/lib/endpoint.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { EndpointFriend, EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
1
+ import type { EndpointFriend, EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -33,10 +33,7 @@ export declare class WeChatMpEndpoint implements EndpointInstance {
33
33
  open(): void;
34
34
  close(): void;
35
35
  stop(): Promise<void>;
36
- send({ target, payload }: {
37
- readonly target: string;
38
- readonly payload: unknown;
39
- }): Promise<string>;
36
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
40
37
  /** Test / internal: admit a parsed message when open (non-webhook path). */
41
38
  admit(msg: WeChatMessage): void;
42
39
  /**
package/lib/endpoint.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import axios from 'axios';
5
5
  import { formatCompact, getLogger } from '@zhin.js/logger';
6
- import { extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, } from './protocol.js';
6
+ import { extractOutboundText, formatCustomerServiceBody, formatInboundContent, formatInboundId, wechatMpInboundConversation, } from './protocol.js';
7
7
  import { buildMediaUploadForm, readOutboundMedia, resolveMediaBinary, } from './media-upload.js';
8
8
  import { getPassiveReplyCapture, recordPassiveReplyText, } from './passive-reply.js';
9
9
  import { registerWeChatMpWebhookRoutes } from './webhook.js';
@@ -110,20 +110,20 @@ export class WeChatMpEndpoint {
110
110
  this.#started = false;
111
111
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
112
112
  }
113
- async send({ target, payload }) {
113
+ async send({ conversation, payload }) {
114
114
  if (getPassiveReplyCapture()) {
115
115
  const text = extractOutboundText(payload);
116
116
  recordPassiveReplyText(text);
117
117
  return `passive_${Date.now()}`;
118
118
  }
119
119
  if (this.#options.config.replyMode === 'customer_service') {
120
- return this.#sendCustomerService(target, payload);
120
+ return this.#sendCustomerService(conversation.id, payload);
121
121
  }
122
122
  logger.warn(formatCompact({
123
123
  op: 'send',
124
124
  skip: 'passive_outside_webhook',
125
125
  endpoint: this.#options.config.name,
126
- target,
126
+ target: `${conversation.kind}:${conversation.id}`,
127
127
  }));
128
128
  return `passive_skipped_${Date.now()}`;
129
129
  }
@@ -131,12 +131,12 @@ export class WeChatMpEndpoint {
131
131
  admit(msg) {
132
132
  if (!this.#open)
133
133
  return;
134
+ const conversation = wechatMpInboundConversation(String(this.#options.id), msg);
134
135
  void this.#options.gateway.receive({
135
- adapter: this.#options.id,
136
- target: msg.FromUserName,
136
+ conversation,
137
+ message: { conversation, id: formatInboundId(msg) },
137
138
  content: formatInboundContent(msg),
138
139
  sender: msg.FromUserName,
139
- id: formatInboundId(msg),
140
140
  metadata: Object.freeze({
141
141
  msgType: msg.MsgType,
142
142
  event: msg.Event,
@@ -146,7 +146,7 @@ export class WeChatMpEndpoint {
146
146
  }).catch((err) => {
147
147
  logger.warn(formatCompact({
148
148
  op: 'wechat_mp_gateway_receive_failed',
149
- target: msg.FromUserName,
149
+ target: `${conversation.kind}:${conversation.id}`,
150
150
  error: err instanceof Error ? err.message : String(err),
151
151
  }));
152
152
  });
package/lib/protocol.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
5
  import type { IncomingMessage } from 'node:http';
6
+ import type { ConversationRef } from '@zhin.js/im-contract';
6
7
  export interface WeChatMpAdapterConfig {
7
8
  readonly name?: string;
8
9
  readonly appId?: string;
@@ -101,6 +102,11 @@ export declare function parseXMLMessage(xmlString: string): Promise<WeChatMessag
101
102
  export declare function decryptMessage(encryptedXml: string, msgSignature: string, timestamp: string, nonce: string, token: string, encodingAESKey: string, appId: string): Promise<string>;
102
103
  export declare function encryptMessage(replyXml: string, token: string, encodingAESKey: string, appId: string, requestTimestamp?: string): string;
103
104
  export declare function buildTextReply(wechatMsg: Pick<WeChatMessage, 'FromUserName' | 'ToUserName'>, content: string): string;
105
+ /**
106
+ * 入站归一化:公众号只有粉丝单聊场景,一律 kind='private',id=openid(FromUserName),
107
+ * 无群/频道容器(parent 恒缺省)。
108
+ */
109
+ export declare function wechatMpInboundConversation(endpointId: string, msg: WeChatMessage): ConversationRef;
104
110
  /**
105
111
  * 入站消息 id:普通消息用 MsgId;无 MsgId 的事件消息拼 Event/EventKey,
106
112
  * 避免只用秒级 CreateTime 时同秒多事件 id 碰撞。
package/lib/protocol.js CHANGED
@@ -170,6 +170,17 @@ export function buildTextReply(wechatMsg, content) {
170
170
  '</xml>',
171
171
  ].join('');
172
172
  }
173
+ /**
174
+ * 入站归一化:公众号只有粉丝单聊场景,一律 kind='private',id=openid(FromUserName),
175
+ * 无群/频道容器(parent 恒缺省)。
176
+ */
177
+ export function wechatMpInboundConversation(endpointId, msg) {
178
+ return {
179
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
180
+ kind: 'private',
181
+ id: msg.FromUserName,
182
+ };
183
+ }
173
184
  /**
174
185
  * 入站消息 id:普通消息用 MsgId;无 MsgId 的事件消息拼 Event/EventKey,
175
186
  * 避免只用秒级 CreateTime 时同秒多事件 id 碰撞。
package/lib/webhook.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { formatCompact, getLogger } from '@zhin.js/logger';
2
2
  import { getPassiveReplyCapture, runWithPassiveReplyCapture, } from './passive-reply.js';
3
- import { buildTextReply, computeSignatureHash, decryptEchostr, decryptMessage, encryptMessage, formatInboundContent, formatInboundId, isEncryptedEchostr, normalizeEchostrParam, parseXMLMessage, queryParam, readTextBody, resolveEventPassiveReply, verifySignature, } from './protocol.js';
3
+ import { buildTextReply, computeSignatureHash, decryptEchostr, decryptMessage, encryptMessage, formatInboundContent, formatInboundId, isEncryptedEchostr, normalizeEchostrParam, parseXMLMessage, queryParam, readTextBody, resolveEventPassiveReply, verifySignature, wechatMpInboundConversation, } from './protocol.js';
4
4
  const logger = getLogger('wechat-mp');
5
5
  export function registerWeChatMpWebhookRoutes(http, handler) {
6
6
  const path = handler.config.path;
@@ -132,13 +132,13 @@ export async function handleWeChatMpMessage(request, response, url, handler) {
132
132
  export async function collectPassiveReply(handler, wechatMsg) {
133
133
  const timeoutMs = handler.config.passiveReplyTimeoutMs;
134
134
  const text = await runWithPassiveReplyCapture(async () => {
135
+ const conversation = wechatMpInboundConversation(String(handler.id), wechatMsg);
135
136
  await Promise.race([
136
137
  handler.gateway.receive({
137
- adapter: handler.id,
138
- target: wechatMsg.FromUserName,
138
+ conversation,
139
+ message: { conversation, id: formatInboundId(wechatMsg) },
139
140
  content: formatInboundContent(wechatMsg),
140
141
  sender: wechatMsg.FromUserName,
141
- id: formatInboundId(wechatMsg),
142
142
  metadata: Object.freeze({
143
143
  msgType: wechatMsg.MsgType,
144
144
  event: wechatMsg.Event,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-wechat-mp",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
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,19 +8,20 @@
8
8
  "dependencies": {
9
9
  "axios": "^1.19.0",
10
10
  "xml2js": "^0.6.2",
11
- "@zhin.js/adapter": "1.1.4",
12
- "@zhin.js/command": "1.0.6",
13
- "@zhin.js/core": "1.5.1",
14
- "@zhin.js/host-http": "1.0.5",
11
+ "@zhin.js/adapter": "1.1.5",
12
+ "@zhin.js/command": "1.0.7",
13
+ "@zhin.js/core": "1.5.2",
14
+ "@zhin.js/host-http": "1.0.6",
15
+ "@zhin.js/im-contract": "1.0.1",
15
16
  "@zhin.js/logger": "1.0.75",
16
- "@zhin.js/plugin-runtime": "1.1.2"
17
+ "@zhin.js/plugin-runtime": "1.1.3"
17
18
  },
18
19
  "peerDependencies": {
19
- "@zhin.js/adapter": "1.1.4",
20
- "@zhin.js/core": "1.5.1",
21
- "@zhin.js/host-http": "1.0.5",
22
- "@zhin.js/plugin-runtime": "1.1.2",
23
- "zhin.js": "6.0.1"
20
+ "@zhin.js/adapter": "1.1.5",
21
+ "@zhin.js/core": "1.5.2",
22
+ "@zhin.js/host-http": "1.0.6",
23
+ "@zhin.js/plugin-runtime": "1.1.3",
24
+ "zhin.js": "6.0.2"
24
25
  },
25
26
  "peerDependenciesMeta": {
26
27
  "zhin.js": {
package/src/endpoint.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * WeChatMpEndpoint — lifecycle, outbound, admit, access token refresh.
3
3
  */
4
4
  import axios from 'axios';
5
- import type { EndpointFriend, EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
5
+ import type { EndpointFriend, EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
6
6
  import type { MessageGateway } from '@zhin.js/core/runtime';
7
7
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -12,6 +12,7 @@ import {
12
12
  formatCustomerServiceBody,
13
13
  formatInboundContent,
14
14
  formatInboundId,
15
+ wechatMpInboundConversation,
15
16
  type ResolvedWeChatMpConfig,
16
17
  type TokenResponse,
17
18
  type WeChatAPIResponse,
@@ -159,7 +160,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
159
160
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
160
161
  }
161
162
 
162
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
163
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
163
164
  if (getPassiveReplyCapture()) {
164
165
  const text = extractOutboundText(payload);
165
166
  recordPassiveReplyText(text);
@@ -167,14 +168,14 @@ export class WeChatMpEndpoint implements EndpointInstance {
167
168
  }
168
169
 
169
170
  if (this.#options.config.replyMode === 'customer_service') {
170
- return this.#sendCustomerService(target, payload);
171
+ return this.#sendCustomerService(conversation.id, payload);
171
172
  }
172
173
 
173
174
  logger.warn(formatCompact({
174
175
  op: 'send',
175
176
  skip: 'passive_outside_webhook',
176
177
  endpoint: this.#options.config.name,
177
- target,
178
+ target: `${conversation.kind}:${conversation.id}`,
178
179
  }));
179
180
  return `passive_skipped_${Date.now()}`;
180
181
  }
@@ -182,12 +183,12 @@ export class WeChatMpEndpoint implements EndpointInstance {
182
183
  /** Test / internal: admit a parsed message when open (non-webhook path). */
183
184
  admit(msg: WeChatMessage): void {
184
185
  if (!this.#open) return;
186
+ const conversation = wechatMpInboundConversation(String(this.#options.id), msg);
185
187
  void this.#options.gateway.receive({
186
- adapter: this.#options.id,
187
- target: msg.FromUserName,
188
+ conversation,
189
+ message: { conversation, id: formatInboundId(msg) },
188
190
  content: formatInboundContent(msg),
189
191
  sender: msg.FromUserName,
190
- id: formatInboundId(msg),
191
192
  metadata: Object.freeze({
192
193
  msgType: msg.MsgType,
193
194
  event: msg.Event,
@@ -197,7 +198,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
197
198
  }).catch((err) => {
198
199
  logger.warn(formatCompact({
199
200
  op: 'wechat_mp_gateway_receive_failed',
200
- target: msg.FromUserName,
201
+ target: `${conversation.kind}:${conversation.id}`,
201
202
  error: err instanceof Error ? err.message : String(err),
202
203
  }));
203
204
  });
package/src/protocol.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { createHash, createDecipheriv, createCipheriv, randomBytes } from 'node:crypto';
7
7
  import type { IncomingMessage } from 'node:http';
8
8
  import * as xml2js from 'xml2js';
9
+ import type { ConversationRef } from '@zhin.js/im-contract';
9
10
 
10
11
  export interface WeChatMpAdapterConfig {
11
12
  readonly name?: string;
@@ -310,6 +311,21 @@ export function buildTextReply(
310
311
  ].join('');
311
312
  }
312
313
 
314
+ /**
315
+ * 入站归一化:公众号只有粉丝单聊场景,一律 kind='private',id=openid(FromUserName),
316
+ * 无群/频道容器(parent 恒缺省)。
317
+ */
318
+ export function wechatMpInboundConversation(
319
+ endpointId: string,
320
+ msg: WeChatMessage,
321
+ ): ConversationRef {
322
+ return {
323
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
324
+ kind: 'private',
325
+ id: msg.FromUserName,
326
+ };
327
+ }
328
+
313
329
  /**
314
330
  * 入站消息 id:普通消息用 MsgId;无 MsgId 的事件消息拼 Event/EventKey,
315
331
  * 避免只用秒级 CreateTime 时同秒多事件 id 碰撞。
package/src/webhook.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  readTextBody,
26
26
  resolveEventPassiveReply,
27
27
  verifySignature,
28
+ wechatMpInboundConversation,
28
29
  type ResolvedWeChatMpConfig,
29
30
  type WeChatMessage,
30
31
  } from './protocol.js';
@@ -218,13 +219,13 @@ export async function collectPassiveReply(
218
219
  ): Promise<string> {
219
220
  const timeoutMs = handler.config.passiveReplyTimeoutMs;
220
221
  const text = await runWithPassiveReplyCapture(async () => {
222
+ const conversation = wechatMpInboundConversation(String(handler.id), wechatMsg);
221
223
  await Promise.race([
222
224
  handler.gateway.receive({
223
- adapter: handler.id,
224
- target: wechatMsg.FromUserName,
225
+ conversation,
226
+ message: { conversation, id: formatInboundId(wechatMsg) },
225
227
  content: formatInboundContent(wechatMsg),
226
228
  sender: wechatMsg.FromUserName,
227
- id: formatInboundId(wechatMsg),
228
229
  metadata: Object.freeze({
229
230
  msgType: wechatMsg.MsgType,
230
231
  event: wechatMsg.Event,