@zhin.js/adapter-wechat-mp 5.0.2 → 5.0.5
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 +50 -0
- package/adapters/wechat-mp.js +2 -2
- package/adapters/wechat-mp.ts +2 -2
- package/lib/endpoint.js +22 -21
- package/lib/protocol.d.ts +3 -3
- package/lib/protocol.js +5 -5
- package/lib/webhook.js +2 -2
- package/package.json +13 -13
- package/schema.json +41 -5
- package/src/endpoint.ts +23 -22
- package/src/protocol.ts +7 -7
- package/src/webhook.ts +2 -2
- /package/commands/endpoint/add/{[name].js → [id].js} +0 -0
- /package/commands/endpoint/add/{[name].ts → [id].ts} +0 -0
- /package/commands/endpoint/remove/{[name].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name].ts → [id].ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @zhin.js/adapter-wechat-mp
|
|
2
2
|
|
|
3
|
+
## 5.0.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [2d0a622]
|
|
8
|
+
- @zhin.js/command@1.0.10
|
|
9
|
+
- @zhin.js/adapter@1.1.7
|
|
10
|
+
- @zhin.js/core@1.5.5
|
|
11
|
+
- zhin.js@6.0.5
|
|
12
|
+
|
|
13
|
+
## 5.0.4
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [0de46a8]
|
|
18
|
+
- Updated dependencies [c106ecc]
|
|
19
|
+
- Updated dependencies [ca92e03]
|
|
20
|
+
- Updated dependencies [b0f37ae]
|
|
21
|
+
- Updated dependencies [ba08a2f]
|
|
22
|
+
- Updated dependencies [daffd4c]
|
|
23
|
+
- Updated dependencies [36c7400]
|
|
24
|
+
- Updated dependencies [162fa34]
|
|
25
|
+
- Updated dependencies [e40b048]
|
|
26
|
+
- Updated dependencies [f1708c3]
|
|
27
|
+
- Updated dependencies [e53444f]
|
|
28
|
+
- Updated dependencies [92b0dd7]
|
|
29
|
+
- Updated dependencies [a7df753]
|
|
30
|
+
- @zhin.js/host-http@1.0.8
|
|
31
|
+
- @zhin.js/im-contract@1.0.3
|
|
32
|
+
- @zhin.js/adapter@1.1.7
|
|
33
|
+
- @zhin.js/plugin-runtime@1.1.5
|
|
34
|
+
- @zhin.js/command@1.0.9
|
|
35
|
+
- @zhin.js/core@1.5.4
|
|
36
|
+
- zhin.js@6.0.4
|
|
37
|
+
|
|
38
|
+
## 5.0.3
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- f8c7a54: fix: im
|
|
43
|
+
- Updated dependencies [f8c7a54]
|
|
44
|
+
- @zhin.js/logger@1.0.76
|
|
45
|
+
- @zhin.js/host-http@1.0.7
|
|
46
|
+
- @zhin.js/adapter@1.1.6
|
|
47
|
+
- @zhin.js/command@1.0.8
|
|
48
|
+
- @zhin.js/core@1.5.3
|
|
49
|
+
- @zhin.js/im-contract@1.0.2
|
|
50
|
+
- @zhin.js/plugin-runtime@1.1.4
|
|
51
|
+
- zhin.js@6.0.3
|
|
52
|
+
|
|
3
53
|
## 5.0.2
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/adapters/wechat-mp.js
CHANGED
|
@@ -20,8 +20,8 @@ export default defineAdapter({
|
|
|
20
20
|
create(context) {
|
|
21
21
|
const config = resolveWeChatMpConfig(context.config);
|
|
22
22
|
// 注册到插件运行时状态(wechat-mp.endpoint list 的"运行中"数据源)
|
|
23
|
-
context.use(wechatMpRuntimeStateToken).endpoints.set(config.
|
|
24
|
-
|
|
23
|
+
context.use(wechatMpRuntimeStateToken).endpoints.set(config.id, {
|
|
24
|
+
id: config.id,
|
|
25
25
|
mode: 'webhook',
|
|
26
26
|
});
|
|
27
27
|
return new WeChatMpEndpoint({
|
package/adapters/wechat-mp.ts
CHANGED
|
@@ -25,8 +25,8 @@ export default defineAdapter<WeChatMpAdapterConfig>({
|
|
|
25
25
|
create(context) {
|
|
26
26
|
const config = resolveWeChatMpConfig(context.config);
|
|
27
27
|
// 注册到插件运行时状态(wechat-mp.endpoint list 的"运行中"数据源)
|
|
28
|
-
context.use(wechatMpRuntimeStateToken).endpoints.set(config.
|
|
29
|
-
|
|
28
|
+
context.use(wechatMpRuntimeStateToken).endpoints.set(config.id, {
|
|
29
|
+
id: config.id,
|
|
30
30
|
mode: 'webhook',
|
|
31
31
|
});
|
|
32
32
|
return new WeChatMpEndpoint({
|
package/lib/endpoint.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* WeChatMpEndpoint — lifecycle, outbound, admit, access token refresh.
|
|
3
3
|
*/
|
|
4
4
|
import axios from 'axios';
|
|
5
|
-
import { formatCompact,
|
|
5
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
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';
|
|
10
|
-
const logger = getLogger('wechat-mp');
|
|
11
10
|
/** token 失效类错误码:40001/40014 invalid access_token、42001 access_token expired。 */
|
|
12
11
|
const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
|
|
13
12
|
/**
|
|
@@ -29,6 +28,7 @@ function defaultFetch(url, init) {
|
|
|
29
28
|
}).then((response) => ({ data: response.data }));
|
|
30
29
|
}
|
|
31
30
|
export class WeChatMpEndpoint {
|
|
31
|
+
#logger;
|
|
32
32
|
#options;
|
|
33
33
|
#fetch;
|
|
34
34
|
#routeReleases = [];
|
|
@@ -42,6 +42,7 @@ export class WeChatMpEndpoint {
|
|
|
42
42
|
#started = false;
|
|
43
43
|
management = createWeChatMpEndpointManagement(this);
|
|
44
44
|
constructor(options) {
|
|
45
|
+
this.#logger = getAdapterLogger('wechat-mp', options.config.id);
|
|
45
46
|
this.#options = options;
|
|
46
47
|
this.#fetch = options.fetch ?? defaultFetch;
|
|
47
48
|
}
|
|
@@ -81,15 +82,15 @@ export class WeChatMpEndpoint {
|
|
|
81
82
|
await this.#refreshAccessToken();
|
|
82
83
|
this.#routeReleases.push(...registerWeChatMpWebhookRoutes(this.#options.http, this));
|
|
83
84
|
this.#startTokenRefreshTimer();
|
|
84
|
-
logger.debug(formatCompact({
|
|
85
|
-
endpoint: this.#options.config.
|
|
85
|
+
this.#logger.debug(formatCompact({
|
|
86
|
+
endpoint: this.#options.config.id,
|
|
86
87
|
op: 'webhook',
|
|
87
88
|
path: this.#options.config.path,
|
|
88
89
|
}));
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
await this.stop();
|
|
92
|
-
logger.error('Failed to connect WeChat MP bot:', error);
|
|
93
|
+
this.#logger.error('Failed to connect WeChat MP bot:', error);
|
|
93
94
|
throw error;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -108,7 +109,7 @@ export class WeChatMpEndpoint {
|
|
|
108
109
|
for (const release of this.#routeReleases.splice(0))
|
|
109
110
|
release();
|
|
110
111
|
this.#started = false;
|
|
111
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
112
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
112
113
|
}
|
|
113
114
|
async send({ conversation, payload }) {
|
|
114
115
|
if (getPassiveReplyCapture()) {
|
|
@@ -119,10 +120,10 @@ export class WeChatMpEndpoint {
|
|
|
119
120
|
if (this.#options.config.replyMode === 'customer_service') {
|
|
120
121
|
return this.#sendCustomerService(conversation.id, payload);
|
|
121
122
|
}
|
|
122
|
-
logger.warn(formatCompact({
|
|
123
|
+
this.#logger.warn(formatCompact({
|
|
123
124
|
op: 'send',
|
|
124
125
|
skip: 'passive_outside_webhook',
|
|
125
|
-
endpoint: this.#options.config.
|
|
126
|
+
endpoint: this.#options.config.id,
|
|
126
127
|
target: `${conversation.kind}:${conversation.id}`,
|
|
127
128
|
}));
|
|
128
129
|
return `passive_skipped_${Date.now()}`;
|
|
@@ -136,15 +137,15 @@ export class WeChatMpEndpoint {
|
|
|
136
137
|
conversation,
|
|
137
138
|
message: { conversation, id: formatInboundId(msg) },
|
|
138
139
|
content: formatInboundContent(msg),
|
|
139
|
-
sender: msg.FromUserName,
|
|
140
|
+
sender: { id: msg.FromUserName },
|
|
141
|
+
endpointId: this.#options.config.id,
|
|
140
142
|
metadata: Object.freeze({
|
|
141
143
|
msgType: msg.MsgType,
|
|
142
144
|
event: msg.Event,
|
|
143
|
-
endpoint: this.#options.config.name,
|
|
144
145
|
toUserName: msg.ToUserName,
|
|
145
146
|
}),
|
|
146
147
|
}).catch((err) => {
|
|
147
|
-
logger.warn(formatCompact({
|
|
148
|
+
this.#logger.warn(formatCompact({
|
|
148
149
|
op: 'wechat_mp_gateway_receive_failed',
|
|
149
150
|
target: `${conversation.kind}:${conversation.id}`,
|
|
150
151
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -161,7 +162,7 @@ export class WeChatMpEndpoint {
|
|
|
161
162
|
let result = await this.#postCustomerService(messageData);
|
|
162
163
|
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
163
164
|
// 对端提前作废 token(多端共用等):刷新后重试一次。
|
|
164
|
-
logger.warn(formatCompact({
|
|
165
|
+
this.#logger.warn(formatCompact({
|
|
165
166
|
op: 'wechat_mp_token_invalid_retry',
|
|
166
167
|
errcode: result.errcode,
|
|
167
168
|
}));
|
|
@@ -171,7 +172,7 @@ export class WeChatMpEndpoint {
|
|
|
171
172
|
if (result.errcode && result.errcode !== 0) {
|
|
172
173
|
throw new Error(`WeChat API error: ${result.errcode} - ${result.errmsg}`);
|
|
173
174
|
}
|
|
174
|
-
logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
|
|
175
|
+
this.#logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
|
|
175
176
|
return result.msgid?.toString() || `cs_${Date.now()}`;
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
@@ -202,9 +203,9 @@ export class WeChatMpEndpoint {
|
|
|
202
203
|
return item;
|
|
203
204
|
if (typeof data.media_id === 'string' && data.media_id)
|
|
204
205
|
return item;
|
|
205
|
-
logger.warn(formatCompact({
|
|
206
|
+
this.#logger.warn(formatCompact({
|
|
206
207
|
op: 'wechat_mp_outbound_media_dropped',
|
|
207
|
-
endpoint: this.#options.config.
|
|
208
|
+
endpoint: this.#options.config.id,
|
|
208
209
|
type: seg.type,
|
|
209
210
|
reason: 'missing_media_ref',
|
|
210
211
|
}));
|
|
@@ -215,9 +216,9 @@ export class WeChatMpEndpoint {
|
|
|
215
216
|
return { type: seg.type, data: { mediaId: media.value } };
|
|
216
217
|
}
|
|
217
218
|
if (!uploadType) {
|
|
218
|
-
logger.warn(formatCompact({
|
|
219
|
+
this.#logger.warn(formatCompact({
|
|
219
220
|
op: 'wechat_mp_outbound_media_dropped',
|
|
220
|
-
endpoint: this.#options.config.
|
|
221
|
+
endpoint: this.#options.config.id,
|
|
221
222
|
type: seg.type,
|
|
222
223
|
reason: 'unsupported_segment_type',
|
|
223
224
|
}));
|
|
@@ -228,9 +229,9 @@ export class WeChatMpEndpoint {
|
|
|
228
229
|
return { type: seg.type, data: { mediaId } };
|
|
229
230
|
}
|
|
230
231
|
catch (error) {
|
|
231
|
-
logger.warn(formatCompact({
|
|
232
|
+
this.#logger.warn(formatCompact({
|
|
232
233
|
op: 'wechat_mp_media_upload_failed',
|
|
233
|
-
endpoint: this.#options.config.
|
|
234
|
+
endpoint: this.#options.config.id,
|
|
234
235
|
error: error instanceof Error ? error.message : String(error),
|
|
235
236
|
}));
|
|
236
237
|
const alt = typeof data.alt === 'string' && data.alt ? data.alt : `[${seg.type}]`;
|
|
@@ -294,7 +295,7 @@ export class WeChatMpEndpoint {
|
|
|
294
295
|
if (data.access_token) {
|
|
295
296
|
this.#accessToken = data.access_token;
|
|
296
297
|
this.#tokenExpireTime = Date.now() + (data.expires_in - 300) * 1000;
|
|
297
|
-
logger.debug(formatCompact({ op: 'token_refresh' }));
|
|
298
|
+
this.#logger.debug(formatCompact({ op: 'token_refresh' }));
|
|
298
299
|
return;
|
|
299
300
|
}
|
|
300
301
|
throw new Error(data.errmsg
|
|
@@ -305,7 +306,7 @@ export class WeChatMpEndpoint {
|
|
|
305
306
|
this.#tokenRefreshTimer = setInterval(() => {
|
|
306
307
|
if (Date.now() >= this.#tokenExpireTime) {
|
|
307
308
|
void this.#refreshAccessToken().catch((error) => {
|
|
308
|
-
logger.error('Failed to refresh access token in timer:', error);
|
|
309
|
+
this.#logger.error('Failed to refresh access token in timer:', error);
|
|
309
310
|
});
|
|
310
311
|
}
|
|
311
312
|
}, 3_600_000);
|
package/lib/protocol.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { IncomingMessage } from 'node:http';
|
|
6
6
|
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
7
7
|
export interface WeChatMpAdapterConfig {
|
|
8
|
-
readonly
|
|
8
|
+
readonly id?: string;
|
|
9
9
|
readonly appId?: string;
|
|
10
10
|
readonly appSecret?: string;
|
|
11
11
|
readonly token?: string;
|
|
@@ -32,7 +32,7 @@ export interface WeChatMpAdapterConfig {
|
|
|
32
32
|
}
|
|
33
33
|
export interface ResolvedWeChatMpConfig {
|
|
34
34
|
readonly context: 'wechat-mp';
|
|
35
|
-
readonly
|
|
35
|
+
readonly id: string;
|
|
36
36
|
readonly appId: string;
|
|
37
37
|
readonly appSecret: string;
|
|
38
38
|
readonly token: string;
|
|
@@ -106,7 +106,7 @@ export declare function buildTextReply(wechatMsg: Pick<WeChatMessage, 'FromUserN
|
|
|
106
106
|
* 入站归一化:公众号只有粉丝单聊场景,一律 kind='private',id=openid(FromUserName),
|
|
107
107
|
* 无群/频道容器(parent 恒缺省)。
|
|
108
108
|
*/
|
|
109
|
-
export declare function wechatMpInboundConversation(
|
|
109
|
+
export declare function wechatMpInboundConversation(endpointKey: string, msg: WeChatMessage): ConversationRef;
|
|
110
110
|
/**
|
|
111
111
|
* 入站消息 id:普通消息用 MsgId;无 MsgId 的事件消息拼 Event/EventKey,
|
|
112
112
|
* 避免只用秒级 CreateTime 时同秒多事件 id 碰撞。
|
package/lib/protocol.js
CHANGED
|
@@ -12,8 +12,8 @@ export function resolveWeChatMpConfig(config = {}) {
|
|
|
12
12
|
if (!appId || !appSecret || !token) {
|
|
13
13
|
throw new TypeError('WeChat MP adapter requires appId + appSecret + token (plugins.<key> or endpoints with context: wechat-mp)');
|
|
14
14
|
}
|
|
15
|
-
const
|
|
16
|
-
|| (typeof entry?.
|
|
15
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
16
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
17
17
|
|| process.env.WECHAT_BOT_NAME
|
|
18
18
|
|| 'wechat-mp-bot';
|
|
19
19
|
const path = config.path ?? entry?.path ?? '/wechat/webhook';
|
|
@@ -26,7 +26,7 @@ export function resolveWeChatMpConfig(config = {}) {
|
|
|
26
26
|
?? 4500;
|
|
27
27
|
return {
|
|
28
28
|
context: 'wechat-mp',
|
|
29
|
-
|
|
29
|
+
id,
|
|
30
30
|
appId,
|
|
31
31
|
appSecret,
|
|
32
32
|
token,
|
|
@@ -174,9 +174,9 @@ export function buildTextReply(wechatMsg, content) {
|
|
|
174
174
|
* 入站归一化:公众号只有粉丝单聊场景,一律 kind='private',id=openid(FromUserName),
|
|
175
175
|
* 无群/频道容器(parent 恒缺省)。
|
|
176
176
|
*/
|
|
177
|
-
export function wechatMpInboundConversation(
|
|
177
|
+
export function wechatMpInboundConversation(endpointKey, msg) {
|
|
178
178
|
return {
|
|
179
|
-
endpoint: { id:
|
|
179
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
180
180
|
kind: 'private',
|
|
181
181
|
id: msg.FromUserName,
|
|
182
182
|
};
|
package/lib/webhook.js
CHANGED
|
@@ -138,11 +138,11 @@ export async function collectPassiveReply(handler, wechatMsg) {
|
|
|
138
138
|
conversation,
|
|
139
139
|
message: { conversation, id: formatInboundId(wechatMsg) },
|
|
140
140
|
content: formatInboundContent(wechatMsg),
|
|
141
|
-
sender: wechatMsg.FromUserName,
|
|
141
|
+
sender: { id: wechatMsg.FromUserName },
|
|
142
|
+
endpointId: handler.config.id,
|
|
142
143
|
metadata: Object.freeze({
|
|
143
144
|
msgType: wechatMsg.MsgType,
|
|
144
145
|
event: wechatMsg.Event,
|
|
145
|
-
endpoint: handler.config.name,
|
|
146
146
|
toUserName: wechatMsg.ToUserName,
|
|
147
147
|
}),
|
|
148
148
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-wechat-mp",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
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,20 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"axios": "^1.19.0",
|
|
10
10
|
"xml2js": "^0.6.2",
|
|
11
|
-
"@zhin.js/adapter": "1.1.
|
|
12
|
-
"@zhin.js/command": "1.0.
|
|
13
|
-
"@zhin.js/core": "1.5.
|
|
14
|
-
"@zhin.js/host-http": "1.0.
|
|
15
|
-
"@zhin.js/im-contract": "1.0.
|
|
16
|
-
"@zhin.js/logger": "1.0.
|
|
17
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
11
|
+
"@zhin.js/adapter": "1.1.7",
|
|
12
|
+
"@zhin.js/command": "1.0.10",
|
|
13
|
+
"@zhin.js/core": "1.5.5",
|
|
14
|
+
"@zhin.js/host-http": "1.0.8",
|
|
15
|
+
"@zhin.js/im-contract": "1.0.3",
|
|
16
|
+
"@zhin.js/logger": "1.0.76",
|
|
17
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@zhin.js/adapter": "1.1.
|
|
21
|
-
"@zhin.js/core": "1.5.
|
|
22
|
-
"@zhin.js/host-http": "1.0.
|
|
23
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
24
|
-
"zhin.js": "6.0.
|
|
20
|
+
"@zhin.js/adapter": "1.1.7",
|
|
21
|
+
"@zhin.js/core": "1.5.5",
|
|
22
|
+
"@zhin.js/host-http": "1.0.8",
|
|
23
|
+
"@zhin.js/plugin-runtime": "1.1.5",
|
|
24
|
+
"zhin.js": "6.0.5"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"zhin.js": {
|
package/schema.json
CHANGED
|
@@ -32,16 +32,48 @@
|
|
|
32
32
|
"type": "number",
|
|
33
33
|
"default": 4500
|
|
34
34
|
},
|
|
35
|
+
"master": {
|
|
36
|
+
"type": [
|
|
37
|
+
"string",
|
|
38
|
+
"number"
|
|
39
|
+
],
|
|
40
|
+
"description": "框架 master(WeChat OpenID;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
41
|
+
},
|
|
42
|
+
"trusted": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": [
|
|
46
|
+
"string",
|
|
47
|
+
"number"
|
|
48
|
+
],
|
|
49
|
+
"description": "Trusted WeChat OpenID"
|
|
50
|
+
},
|
|
51
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
52
|
+
},
|
|
35
53
|
"endpoints": {
|
|
36
54
|
"type": "array",
|
|
37
|
-
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(
|
|
55
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
38
56
|
"items": {
|
|
39
57
|
"type": "object",
|
|
40
58
|
"additionalProperties": true,
|
|
41
59
|
"properties": {
|
|
42
|
-
"
|
|
43
|
-
"type":
|
|
44
|
-
|
|
60
|
+
"master": {
|
|
61
|
+
"type": [
|
|
62
|
+
"string",
|
|
63
|
+
"number"
|
|
64
|
+
],
|
|
65
|
+
"description": "本 endpoint 的框架 master(WeChat OpenID);覆盖顶层 master"
|
|
66
|
+
},
|
|
67
|
+
"trusted": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": {
|
|
70
|
+
"type": [
|
|
71
|
+
"string",
|
|
72
|
+
"number"
|
|
73
|
+
],
|
|
74
|
+
"description": "Trusted WeChat OpenID"
|
|
75
|
+
},
|
|
76
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
45
77
|
},
|
|
46
78
|
"appId": {
|
|
47
79
|
"type": "string",
|
|
@@ -58,10 +90,14 @@
|
|
|
58
90
|
"encodingAESKey": {
|
|
59
91
|
"type": "string",
|
|
60
92
|
"description": "WeChat MP encoding AES key"
|
|
93
|
+
},
|
|
94
|
+
"id": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "WeChat MP bot name"
|
|
61
97
|
}
|
|
62
98
|
},
|
|
63
99
|
"required": [
|
|
64
|
-
"
|
|
100
|
+
"id",
|
|
65
101
|
"appId",
|
|
66
102
|
"appSecret",
|
|
67
103
|
"token"
|
package/src/endpoint.ts
CHANGED
|
@@ -5,7 +5,7 @@ import axios from 'axios';
|
|
|
5
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
|
-
import { formatCompact,
|
|
8
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
10
|
import {
|
|
11
11
|
extractOutboundText,
|
|
@@ -30,8 +30,6 @@ import {
|
|
|
30
30
|
} from './passive-reply.js';
|
|
31
31
|
import { registerWeChatMpWebhookRoutes } from './webhook.js';
|
|
32
32
|
|
|
33
|
-
const logger = getLogger('wechat-mp');
|
|
34
|
-
|
|
35
33
|
/** token 失效类错误码:40001/40014 invalid access_token、42001 access_token expired。 */
|
|
36
34
|
const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
|
|
37
35
|
|
|
@@ -72,6 +70,8 @@ function defaultFetch(
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
export class WeChatMpEndpoint implements EndpointInstance {
|
|
73
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
74
|
+
|
|
75
75
|
readonly #options: WeChatMpEndpointOptions;
|
|
76
76
|
readonly #fetch: WeChatMpFetch;
|
|
77
77
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
@@ -86,6 +86,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
86
86
|
readonly management: EndpointManagement = createWeChatMpEndpointManagement(this);
|
|
87
87
|
|
|
88
88
|
constructor(options: WeChatMpEndpointOptions) {
|
|
89
|
+
this.#logger = getAdapterLogger('wechat-mp', options.config.id);
|
|
89
90
|
this.#options = options;
|
|
90
91
|
this.#fetch = options.fetch ?? defaultFetch;
|
|
91
92
|
}
|
|
@@ -129,14 +130,14 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
129
130
|
await this.#refreshAccessToken();
|
|
130
131
|
this.#routeReleases.push(...registerWeChatMpWebhookRoutes(this.#options.http, this));
|
|
131
132
|
this.#startTokenRefreshTimer();
|
|
132
|
-
logger.debug(formatCompact({
|
|
133
|
-
endpoint: this.#options.config.
|
|
133
|
+
this.#logger.debug(formatCompact({
|
|
134
|
+
endpoint: this.#options.config.id,
|
|
134
135
|
op: 'webhook',
|
|
135
136
|
path: this.#options.config.path,
|
|
136
137
|
}));
|
|
137
138
|
} catch (error) {
|
|
138
139
|
await this.stop();
|
|
139
|
-
logger.error('Failed to connect WeChat MP bot:', error);
|
|
140
|
+
this.#logger.error('Failed to connect WeChat MP bot:', error);
|
|
140
141
|
throw error;
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -157,7 +158,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
157
158
|
}
|
|
158
159
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
159
160
|
this.#started = false;
|
|
160
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
161
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
@@ -171,10 +172,10 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
171
172
|
return this.#sendCustomerService(conversation.id, payload);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
logger.warn(formatCompact({
|
|
175
|
+
this.#logger.warn(formatCompact({
|
|
175
176
|
op: 'send',
|
|
176
177
|
skip: 'passive_outside_webhook',
|
|
177
|
-
endpoint: this.#options.config.
|
|
178
|
+
endpoint: this.#options.config.id,
|
|
178
179
|
target: `${conversation.kind}:${conversation.id}`,
|
|
179
180
|
}));
|
|
180
181
|
return `passive_skipped_${Date.now()}`;
|
|
@@ -188,15 +189,15 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
188
189
|
conversation,
|
|
189
190
|
message: { conversation, id: formatInboundId(msg) },
|
|
190
191
|
content: formatInboundContent(msg),
|
|
191
|
-
sender: msg.FromUserName,
|
|
192
|
+
sender: { id: msg.FromUserName },
|
|
193
|
+
endpointId: this.#options.config.id,
|
|
192
194
|
metadata: Object.freeze({
|
|
193
195
|
msgType: msg.MsgType,
|
|
194
196
|
event: msg.Event,
|
|
195
|
-
endpoint: this.#options.config.name,
|
|
196
197
|
toUserName: msg.ToUserName,
|
|
197
198
|
}),
|
|
198
199
|
}).catch((err) => {
|
|
199
|
-
logger.warn(formatCompact({
|
|
200
|
+
this.#logger.warn(formatCompact({
|
|
200
201
|
op: 'wechat_mp_gateway_receive_failed',
|
|
201
202
|
target: `${conversation.kind}:${conversation.id}`,
|
|
202
203
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -214,7 +215,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
214
215
|
let result = await this.#postCustomerService(messageData);
|
|
215
216
|
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
216
217
|
// 对端提前作废 token(多端共用等):刷新后重试一次。
|
|
217
|
-
logger.warn(formatCompact({
|
|
218
|
+
this.#logger.warn(formatCompact({
|
|
218
219
|
op: 'wechat_mp_token_invalid_retry',
|
|
219
220
|
errcode: result.errcode,
|
|
220
221
|
}));
|
|
@@ -224,7 +225,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
224
225
|
if (result.errcode && result.errcode !== 0) {
|
|
225
226
|
throw new Error(`WeChat API error: ${result.errcode} - ${result.errmsg}`);
|
|
226
227
|
}
|
|
227
|
-
logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
|
|
228
|
+
this.#logger.debug(formatCompact({ op: 'wechat_mp_send', target, messageId: result.msgid }));
|
|
228
229
|
return result.msgid?.toString() || `cs_${Date.now()}`;
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -250,9 +251,9 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
250
251
|
// 已物化(mediaId/media_id)的段透传;其余无 canonical 媒体引用,丢弃留痕
|
|
251
252
|
if (typeof data.mediaId === 'string' && data.mediaId) return item;
|
|
252
253
|
if (typeof data.media_id === 'string' && data.media_id) return item;
|
|
253
|
-
logger.warn(formatCompact({
|
|
254
|
+
this.#logger.warn(formatCompact({
|
|
254
255
|
op: 'wechat_mp_outbound_media_dropped',
|
|
255
|
-
endpoint: this.#options.config.
|
|
256
|
+
endpoint: this.#options.config.id,
|
|
256
257
|
type: seg.type,
|
|
257
258
|
reason: 'missing_media_ref',
|
|
258
259
|
}));
|
|
@@ -263,9 +264,9 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
263
264
|
return { type: seg.type, data: { mediaId: media.value } };
|
|
264
265
|
}
|
|
265
266
|
if (!uploadType) {
|
|
266
|
-
logger.warn(formatCompact({
|
|
267
|
+
this.#logger.warn(formatCompact({
|
|
267
268
|
op: 'wechat_mp_outbound_media_dropped',
|
|
268
|
-
endpoint: this.#options.config.
|
|
269
|
+
endpoint: this.#options.config.id,
|
|
269
270
|
type: seg.type,
|
|
270
271
|
reason: 'unsupported_segment_type',
|
|
271
272
|
}));
|
|
@@ -275,9 +276,9 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
275
276
|
const mediaId = await this.#uploadMedia(uploadType, media);
|
|
276
277
|
return { type: seg.type, data: { mediaId } };
|
|
277
278
|
} catch (error) {
|
|
278
|
-
logger.warn(formatCompact({
|
|
279
|
+
this.#logger.warn(formatCompact({
|
|
279
280
|
op: 'wechat_mp_media_upload_failed',
|
|
280
|
-
endpoint: this.#options.config.
|
|
281
|
+
endpoint: this.#options.config.id,
|
|
281
282
|
error: error instanceof Error ? error.message : String(error),
|
|
282
283
|
}));
|
|
283
284
|
const alt = typeof data.alt === 'string' && data.alt ? data.alt : `[${seg.type}]`;
|
|
@@ -350,7 +351,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
350
351
|
if (data.access_token) {
|
|
351
352
|
this.#accessToken = data.access_token;
|
|
352
353
|
this.#tokenExpireTime = Date.now() + (data.expires_in - 300) * 1000;
|
|
353
|
-
logger.debug(formatCompact({ op: 'token_refresh' }));
|
|
354
|
+
this.#logger.debug(formatCompact({ op: 'token_refresh' }));
|
|
354
355
|
return;
|
|
355
356
|
}
|
|
356
357
|
throw new Error(
|
|
@@ -364,7 +365,7 @@ export class WeChatMpEndpoint implements EndpointInstance {
|
|
|
364
365
|
this.#tokenRefreshTimer = setInterval(() => {
|
|
365
366
|
if (Date.now() >= this.#tokenExpireTime) {
|
|
366
367
|
void this.#refreshAccessToken().catch((error) => {
|
|
367
|
-
logger.error('Failed to refresh access token in timer:', error);
|
|
368
|
+
this.#logger.error('Failed to refresh access token in timer:', error);
|
|
368
369
|
});
|
|
369
370
|
}
|
|
370
371
|
}, 3_600_000);
|
package/src/protocol.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as xml2js from 'xml2js';
|
|
|
9
9
|
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
10
10
|
|
|
11
11
|
export interface WeChatMpAdapterConfig {
|
|
12
|
-
readonly
|
|
12
|
+
readonly id?: string;
|
|
13
13
|
readonly appId?: string;
|
|
14
14
|
readonly appSecret?: string;
|
|
15
15
|
readonly token?: string;
|
|
@@ -37,7 +37,7 @@ export interface WeChatMpAdapterConfig {
|
|
|
37
37
|
|
|
38
38
|
export interface ResolvedWeChatMpConfig {
|
|
39
39
|
readonly context: 'wechat-mp';
|
|
40
|
-
readonly
|
|
40
|
+
readonly id: string;
|
|
41
41
|
readonly appId: string;
|
|
42
42
|
readonly appSecret: string;
|
|
43
43
|
readonly token: string;
|
|
@@ -99,8 +99,8 @@ export function resolveWeChatMpConfig(config: WeChatMpAdapterConfig = {}): Resol
|
|
|
99
99
|
'WeChat MP adapter requires appId + appSecret + token (plugins.<key> or endpoints with context: wechat-mp)',
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
|
-
const
|
|
103
|
-
|| (typeof entry?.
|
|
102
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
103
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
104
104
|
|| process.env.WECHAT_BOT_NAME
|
|
105
105
|
|| 'wechat-mp-bot';
|
|
106
106
|
const path = config.path ?? entry?.path ?? '/wechat/webhook';
|
|
@@ -113,7 +113,7 @@ export function resolveWeChatMpConfig(config: WeChatMpAdapterConfig = {}): Resol
|
|
|
113
113
|
?? 4500;
|
|
114
114
|
return {
|
|
115
115
|
context: 'wechat-mp',
|
|
116
|
-
|
|
116
|
+
id,
|
|
117
117
|
appId,
|
|
118
118
|
appSecret,
|
|
119
119
|
token,
|
|
@@ -316,11 +316,11 @@ export function buildTextReply(
|
|
|
316
316
|
* 无群/频道容器(parent 恒缺省)。
|
|
317
317
|
*/
|
|
318
318
|
export function wechatMpInboundConversation(
|
|
319
|
-
|
|
319
|
+
endpointKey: string,
|
|
320
320
|
msg: WeChatMessage,
|
|
321
321
|
): ConversationRef {
|
|
322
322
|
return {
|
|
323
|
-
endpoint: { id:
|
|
323
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
324
324
|
kind: 'private',
|
|
325
325
|
id: msg.FromUserName,
|
|
326
326
|
};
|
package/src/webhook.ts
CHANGED
|
@@ -225,11 +225,11 @@ export async function collectPassiveReply(
|
|
|
225
225
|
conversation,
|
|
226
226
|
message: { conversation, id: formatInboundId(wechatMsg) },
|
|
227
227
|
content: formatInboundContent(wechatMsg),
|
|
228
|
-
sender: wechatMsg.FromUserName,
|
|
228
|
+
sender: { id: wechatMsg.FromUserName },
|
|
229
|
+
endpointId: handler.config.id,
|
|
229
230
|
metadata: Object.freeze({
|
|
230
231
|
msgType: wechatMsg.MsgType,
|
|
231
232
|
event: wechatMsg.Event,
|
|
232
|
-
endpoint: handler.config.name,
|
|
233
233
|
toUserName: wechatMsg.ToUserName,
|
|
234
234
|
}),
|
|
235
235
|
}),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|