@zhin.js/adapter-wechat-mp 1.1.7 → 1.1.9
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 +43 -0
- package/lib/client.d.ts +4 -0
- package/lib/client.js +43 -8
- package/lib/endpoint.js +3 -0
- package/package.json +15 -16
- package/src/client.ts +47 -8
- package/src/endpoint.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @zhin.js/adapter-wechat-mp
|
|
2
2
|
|
|
3
|
+
## 1.1.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 75e26a6: Raise the supported Node.js range to `>=22.19.0` (was `^20.19.0 || >=22.12.0`). `@zhin.js/ai` depends on undici 8, whose `engines` require Node >=22.19.0; on Node 20 it crashes at import (`webidl.util.markAsUncloneable is not a function`). CI already tests only Node 22 / 24 / 26. `zhin doctor`, `zhin new`, `zhin migrate` and `create-zhin-app` now check for and write the same range.
|
|
8
|
+
- fdc2148: Unblock external plugin authors.
|
|
9
|
+
|
|
10
|
+
- Published manifests no longer expose the source-only `development` export condition (it pointed at `./src/*.ts`, which is not in the tarball, so Vitest 4 and other tools that enable `development` failed to import Zhin packages). Each package now carries `publishConfig.exports` without that condition; `pnpm publish` swaps it in when packing, and the monorepo keeps source-level development.
|
|
11
|
+
- `zhin new` no longer crashes without arguments (uses inquirer `select` instead of the removed `list` prompt; same fix for `zhin onboard` and `zhin pub`). The generated package.json uses `^x.y.z` ranges derived from installed versions instead of `latest`, declares capability packages as optional peers (plus devDependencies for local tests) instead of dependencies, sets `zhin.entry` to `./plugin.js` and builds sibling JavaScript on `prepack`, fixes the adapter template test, and adds `--package-name`. `--skip-install` now also skips the project-root install.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [75e26a6]
|
|
14
|
+
- Updated dependencies [fdc2148]
|
|
15
|
+
- @zhin.js/adapter@1.1.19
|
|
16
|
+
- @zhin.js/core@1.1.44
|
|
17
|
+
- @zhin.js/feature-kit@1.1.5
|
|
18
|
+
- @zhin.js/host-http@1.1.6
|
|
19
|
+
- @zhin.js/im-contract@1.1.4
|
|
20
|
+
- @zhin.js/skill@1.1.6
|
|
21
|
+
- @zhin.js/logger@1.1.2
|
|
22
|
+
|
|
23
|
+
## 1.1.8
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 2dfe5fa: 修复 ScheduleEngine every/at 任务回调异常未被捕获、超长延时被 Node 截断立即触发、at 回调内同 id 重注册被误删;修复微信公众号 access_token 并发刷新互相顶掉、expires_in 缺失时永不过期。
|
|
28
|
+
- Updated dependencies [9891e66]
|
|
29
|
+
- Updated dependencies [bf52b40]
|
|
30
|
+
- Updated dependencies [bf52b40]
|
|
31
|
+
- Updated dependencies [bf52b40]
|
|
32
|
+
- Updated dependencies [386150d]
|
|
33
|
+
- Updated dependencies [9891e66]
|
|
34
|
+
- Updated dependencies [9891e66]
|
|
35
|
+
- Updated dependencies [4778587]
|
|
36
|
+
- Updated dependencies [40537e3]
|
|
37
|
+
- Updated dependencies [681abf3]
|
|
38
|
+
- Updated dependencies [9891e66]
|
|
39
|
+
- Updated dependencies [9891e66]
|
|
40
|
+
- @zhin.js/adapter@1.1.18
|
|
41
|
+
- @zhin.js/core@1.1.43
|
|
42
|
+
- @zhin.js/host-http@1.1.5
|
|
43
|
+
- @zhin.js/feature-kit@1.1.4
|
|
44
|
+
- @zhin.js/skill@1.1.5
|
|
45
|
+
|
|
3
46
|
## 1.1.7
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/lib/client.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export declare class WeChatMpClient {
|
|
|
14
14
|
constructor(config: ResolvedWeChatMpConfig, fetch: WeChatMpFetch);
|
|
15
15
|
get accessToken(): string | null;
|
|
16
16
|
get tokenExpired(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
|
|
19
|
+
* 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
|
|
20
|
+
*/
|
|
17
21
|
refreshAccessToken(): Promise<string>;
|
|
18
22
|
request<T = unknown>(path: string, init?: Parameters<WeChatMpFetch>[1]): Promise<T>;
|
|
19
23
|
sendCustomerService(messageData: unknown): Promise<WeChatAPIResponse>;
|
package/lib/client.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
2
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;
|
|
3
7
|
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
4
8
|
export class WeChatMpClient {
|
|
5
9
|
config;
|
|
6
10
|
fetch;
|
|
7
11
|
#accessToken = null;
|
|
8
12
|
#tokenExpireTime = 0;
|
|
13
|
+
#refreshing = null;
|
|
9
14
|
constructor(config, fetch) {
|
|
10
15
|
this.config = config;
|
|
11
16
|
this.fetch = fetch;
|
|
@@ -16,10 +21,28 @@ export class WeChatMpClient {
|
|
|
16
21
|
get tokenExpired() {
|
|
17
22
|
return !this.#accessToken || Date.now() >= this.#tokenExpireTime;
|
|
18
23
|
}
|
|
19
|
-
|
|
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
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return this.#refreshing;
|
|
35
|
+
}
|
|
36
|
+
async #fetchAccessToken() {
|
|
20
37
|
const { appId, appSecret } = this.config;
|
|
21
|
-
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
|
22
|
-
|
|
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));
|
|
23
46
|
const data = response.data;
|
|
24
47
|
if (!data.access_token) {
|
|
25
48
|
throw new Error(data.errmsg
|
|
@@ -27,20 +50,32 @@ export class WeChatMpClient {
|
|
|
27
50
|
: 'Failed to get access token');
|
|
28
51
|
}
|
|
29
52
|
this.#accessToken = data.access_token;
|
|
30
|
-
|
|
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;
|
|
31
59
|
return data.access_token;
|
|
32
60
|
}
|
|
33
61
|
async request(path, init) {
|
|
62
|
+
return (await this.#requestWithToken(path, init)).data;
|
|
63
|
+
}
|
|
64
|
+
async #requestWithToken(path, init) {
|
|
34
65
|
if (this.tokenExpired)
|
|
35
66
|
await this.refreshAccessToken();
|
|
67
|
+
const token = this.#accessToken;
|
|
36
68
|
const separator = path.includes('?') ? '&' : '?';
|
|
37
|
-
const response = await this.fetch(`https://api.weixin.qq.com${path}${separator}access_token=${
|
|
38
|
-
return response.data;
|
|
69
|
+
const response = await this.fetch(`https://api.weixin.qq.com${path}${separator}access_token=${token}`, init);
|
|
70
|
+
return { data: response.data, token };
|
|
39
71
|
}
|
|
40
72
|
async sendCustomerService(messageData) {
|
|
41
|
-
|
|
73
|
+
const first = await this.#requestWithToken('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
|
|
74
|
+
let result = first.data;
|
|
42
75
|
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
43
|
-
|
|
76
|
+
// 失败请求使用的 token 若已被其他请求换掉,直接用新 token 重试,不再重复刷新。
|
|
77
|
+
if (this.#accessToken === first.token)
|
|
78
|
+
await this.refreshAccessToken();
|
|
44
79
|
result = await this.request('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
|
|
45
80
|
}
|
|
46
81
|
return result;
|
package/lib/endpoint.js
CHANGED
|
@@ -243,6 +243,8 @@ export class WeChatMpEndpoint extends Endpoint {
|
|
|
243
243
|
return this.client.uploadMedia(type, form);
|
|
244
244
|
}
|
|
245
245
|
#startTokenRefreshTimer() {
|
|
246
|
+
if (this.#tokenRefreshTimer)
|
|
247
|
+
clearInterval(this.#tokenRefreshTimer);
|
|
246
248
|
this.#tokenRefreshTimer = setInterval(() => {
|
|
247
249
|
if (this.client.tokenExpired) {
|
|
248
250
|
void this.client.refreshAccessToken().catch((error) => {
|
|
@@ -250,6 +252,7 @@ export class WeChatMpEndpoint extends Endpoint {
|
|
|
250
252
|
});
|
|
251
253
|
}
|
|
252
254
|
}, 3_600_000);
|
|
255
|
+
this.#tokenRefreshTimer.unref?.();
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
function createWeChatMpEndpointManagement(requireClient) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-wechat-mp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
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/core": "1.1.
|
|
13
|
-
"@zhin.js/feature-kit": "1.1.
|
|
14
|
-
"@zhin.js/host-http": "1.1.
|
|
15
|
-
"@zhin.js/im-contract": "1.1.
|
|
16
|
-
"@zhin.js/logger": "1.1.
|
|
17
|
-
"@zhin.js/skill": "1.1.
|
|
11
|
+
"@zhin.js/adapter": "1.1.19",
|
|
12
|
+
"@zhin.js/core": "1.1.44",
|
|
13
|
+
"@zhin.js/feature-kit": "1.1.5",
|
|
14
|
+
"@zhin.js/host-http": "1.1.6",
|
|
15
|
+
"@zhin.js/im-contract": "1.1.4",
|
|
16
|
+
"@zhin.js/logger": "1.1.2",
|
|
17
|
+
"@zhin.js/skill": "1.1.6"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@zhin.js/adapter": "^1.1.
|
|
21
|
-
"@zhin.js/command": "^1.1.
|
|
22
|
-
"@zhin.js/core": "^1.1.
|
|
23
|
-
"@zhin.js/host-http": "^1.1.
|
|
24
|
-
"zhin.js": "^1.1.
|
|
20
|
+
"@zhin.js/adapter": "^1.1.19",
|
|
21
|
+
"@zhin.js/command": "^1.1.7",
|
|
22
|
+
"@zhin.js/core": "^1.1.44",
|
|
23
|
+
"@zhin.js/host-http": "^1.1.6",
|
|
24
|
+
"zhin.js": "^1.1.9"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"@zhin.js/command": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@types/xml2js": "^0.4.14",
|
|
37
37
|
"typescript": "^6.0.3",
|
|
38
38
|
"vitest": "^4.1.10",
|
|
39
|
-
"zhin.js": "1.1.
|
|
39
|
+
"zhin.js": "1.1.9"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"zhin",
|
|
@@ -79,12 +79,11 @@
|
|
|
79
79
|
"exports": {
|
|
80
80
|
".": {
|
|
81
81
|
"types": "./lib/index.d.ts",
|
|
82
|
-
"development": "./src/index.ts",
|
|
83
82
|
"import": "./lib/index.js"
|
|
84
83
|
}
|
|
85
84
|
},
|
|
86
85
|
"engines": {
|
|
87
|
-
"node": "
|
|
86
|
+
"node": ">=22.19.0"
|
|
88
87
|
},
|
|
89
88
|
"zhin": {
|
|
90
89
|
"protocol": 1,
|
package/src/client.ts
CHANGED
|
@@ -7,6 +7,10 @@ import type { WeChatMediaUploadResult } from './media-upload.js';
|
|
|
7
7
|
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
8
8
|
|
|
9
9
|
const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
|
|
10
|
+
/** 换取 token 请求的超时,防止挂起的请求让 single-flight 永久卡住。 */
|
|
11
|
+
const TOKEN_FETCH_TIMEOUT_MS = 15_000;
|
|
12
|
+
/** 官方 access_token 有效期为 7200s;对异常大的 expires_in 设上限。 */
|
|
13
|
+
const MAX_TOKEN_LIFETIME_SEC = 7200;
|
|
10
14
|
|
|
11
15
|
export type WeChatMpFetch = (
|
|
12
16
|
url: string,
|
|
@@ -21,6 +25,7 @@ export type WeChatMpFetch = (
|
|
|
21
25
|
export class WeChatMpClient {
|
|
22
26
|
#accessToken: string | null = null;
|
|
23
27
|
#tokenExpireTime = 0;
|
|
28
|
+
#refreshing: Promise<string> | null = null;
|
|
24
29
|
|
|
25
30
|
constructor(
|
|
26
31
|
readonly config: ResolvedWeChatMpConfig,
|
|
@@ -35,10 +40,29 @@ export class WeChatMpClient {
|
|
|
35
40
|
return !this.#accessToken || Date.now() >= this.#tokenExpireTime;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
|
|
45
|
+
* 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
|
|
46
|
+
*/
|
|
47
|
+
refreshAccessToken(): Promise<string> {
|
|
48
|
+
if (!this.#refreshing) {
|
|
49
|
+
this.#refreshing = this.#fetchAccessToken().finally(() => {
|
|
50
|
+
this.#refreshing = null;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return this.#refreshing;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async #fetchAccessToken(): Promise<string> {
|
|
39
57
|
const { appId, appSecret } = this.config;
|
|
40
|
-
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
|
41
|
-
|
|
58
|
+
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(appSecret)}`;
|
|
59
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
const response = await Promise.race([
|
|
61
|
+
this.fetch(url),
|
|
62
|
+
new Promise<never>((_, reject) => {
|
|
63
|
+
timeout = setTimeout(() => reject(new Error('Failed to get access token: request timed out')), TOKEN_FETCH_TIMEOUT_MS);
|
|
64
|
+
}),
|
|
65
|
+
]).finally(() => clearTimeout(timeout));
|
|
42
66
|
const data = response.data as TokenResponse & WeChatAPIResponse;
|
|
43
67
|
if (!data.access_token) {
|
|
44
68
|
throw new Error(data.errmsg
|
|
@@ -46,7 +70,12 @@ export class WeChatMpClient {
|
|
|
46
70
|
: 'Failed to get access token');
|
|
47
71
|
}
|
|
48
72
|
this.#accessToken = data.access_token;
|
|
49
|
-
|
|
73
|
+
// expires_in 缺失/非法时 NaN 会让 tokenExpired 永远为 false;按官方默认 7200s 兜底。
|
|
74
|
+
const expiresIn = Number(data.expires_in);
|
|
75
|
+
const lifetimeSec = Number.isFinite(expiresIn) && expiresIn > 0
|
|
76
|
+
? Math.min(expiresIn, MAX_TOKEN_LIFETIME_SEC)
|
|
77
|
+
: MAX_TOKEN_LIFETIME_SEC;
|
|
78
|
+
this.#tokenExpireTime = Date.now() + (lifetimeSec - 300) * 1000;
|
|
50
79
|
return data.access_token;
|
|
51
80
|
}
|
|
52
81
|
|
|
@@ -54,22 +83,32 @@ export class WeChatMpClient {
|
|
|
54
83
|
path: string,
|
|
55
84
|
init?: Parameters<WeChatMpFetch>[1],
|
|
56
85
|
): Promise<T> {
|
|
86
|
+
return (await this.#requestWithToken<T>(path, init)).data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async #requestWithToken<T>(
|
|
90
|
+
path: string,
|
|
91
|
+
init?: Parameters<WeChatMpFetch>[1],
|
|
92
|
+
): Promise<{ readonly data: T; readonly token: string | null }> {
|
|
57
93
|
if (this.tokenExpired) await this.refreshAccessToken();
|
|
94
|
+
const token = this.#accessToken;
|
|
58
95
|
const separator = path.includes('?') ? '&' : '?';
|
|
59
96
|
const response = await this.fetch(
|
|
60
|
-
`https://api.weixin.qq.com${path}${separator}access_token=${
|
|
97
|
+
`https://api.weixin.qq.com${path}${separator}access_token=${token}`,
|
|
61
98
|
init,
|
|
62
99
|
);
|
|
63
|
-
return response.data as T;
|
|
100
|
+
return { data: response.data as T, token };
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
async sendCustomerService(messageData: unknown): Promise<WeChatAPIResponse> {
|
|
67
|
-
|
|
104
|
+
const first = await this.#requestWithToken<WeChatAPIResponse>(
|
|
68
105
|
'/cgi-bin/message/custom/send',
|
|
69
106
|
{ method: 'POST', body: messageData },
|
|
70
107
|
);
|
|
108
|
+
let result = first.data;
|
|
71
109
|
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
72
|
-
|
|
110
|
+
// 失败请求使用的 token 若已被其他请求换掉,直接用新 token 重试,不再重复刷新。
|
|
111
|
+
if (this.#accessToken === first.token) await this.refreshAccessToken();
|
|
73
112
|
result = await this.request<WeChatAPIResponse>(
|
|
74
113
|
'/cgi-bin/message/custom/send',
|
|
75
114
|
{ method: 'POST', body: messageData },
|
package/src/endpoint.ts
CHANGED
|
@@ -289,6 +289,7 @@ export class WeChatMpEndpoint extends Endpoint<WeChatMpClient> {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
#startTokenRefreshTimer(): void {
|
|
292
|
+
if (this.#tokenRefreshTimer) clearInterval(this.#tokenRefreshTimer);
|
|
292
293
|
this.#tokenRefreshTimer = setInterval(() => {
|
|
293
294
|
if (this.client.tokenExpired) {
|
|
294
295
|
void this.client.refreshAccessToken().catch((error) => {
|
|
@@ -296,6 +297,7 @@ export class WeChatMpEndpoint extends Endpoint<WeChatMpClient> {
|
|
|
296
297
|
});
|
|
297
298
|
}
|
|
298
299
|
}, 3_600_000);
|
|
300
|
+
(this.#tokenRefreshTimer as { unref?: () => void }).unref?.();
|
|
299
301
|
}
|
|
300
302
|
}
|
|
301
303
|
|