@zhin.js/adapter-wechat-mp 1.1.18 → 1.1.20
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/README.md +8 -7
- package/adapters/wechat-mp/index.js +3 -17
- package/adapters/wechat-mp/index.ts +2 -17
- package/commands/wechat-mp/endpoint/definition.js +0 -2
- package/commands/wechat-mp/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +18 -0
- package/lib/adapter.js +16 -0
- package/lib/api-client.d.ts +34 -0
- package/lib/api-client.js +113 -0
- package/lib/client.d.ts +39 -33
- package/lib/client.js +292 -97
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +11 -29
- package/lib/endpoint.js +30 -264
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/media-upload.d.ts +5 -1
- package/lib/media-upload.js +6 -6
- package/lib/side-event-dispatch.d.ts +1 -1
- package/lib/side-event-dispatch.js +2 -2
- package/lib/webhook.d.ts +2 -2
- package/lib/webhook.js +4 -4
- package/package.json +15 -15
- package/plugin.js +0 -3
- package/src/adapter.ts +22 -0
- package/src/api-client.ts +164 -0
- package/src/client.ts +326 -134
- package/src/console.ts +2 -5
- package/src/endpoint.ts +33 -313
- package/src/index.ts +1 -1
- package/src/media-upload.ts +10 -5
- package/src/side-event-dispatch.ts +2 -1
- package/src/webhook.ts +5 -4
- package/lib/wechat-mp-runtime-state.d.ts +0 -1
- package/lib/wechat-mp-runtime-state.js +0 -6
- package/src/wechat-mp-runtime-state.ts +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @zhin.js/adapter-wechat-mp
|
|
2
2
|
|
|
3
|
+
## 1.1.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [5e878ba]
|
|
8
|
+
- @zhin.js/page@1.1.12
|
|
9
|
+
|
|
10
|
+
## 1.1.19
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 2c3610a: Move account lifecycle ownership into the generic Adapter base and let native Endpoints retain their SDK Client inheritance. Infer platform-specific operation Sessions from a single adapter registry while preserving command parameter, option, configuration and factory inference.
|
|
15
|
+
|
|
16
|
+
Bind message replies, management ports, login challenges and Agent cancellation to the captured account incarnation. Persist runtime account intent with encrypted, revision-checked Host storage; keep account management independent of Root process restarts. Require explicit operator confirmation before importing existing accounts, and give replacement accounts a new identity without inheriting previous Sessions.
|
|
17
|
+
|
|
18
|
+
Expose account save, start, stop, update and replacement through the existing adapter Console, with exact account ownership, truthful pending/unknown outcomes and no startup configuration or environment-file rewrites.
|
|
19
|
+
|
|
20
|
+
Complete the target migration across every adapter, the public authoring API, generators, examples and Console. This intentionally removes the old create factory, inheritable Endpoint/ClientEndpoint classes, Client descriptors and file-rewrite account management; plugin authors must use createAdapter, native Client inheritance and the single AdapterRegistry. Satori and OneBot12 require explicit platform/userId selection, and WeCom requires a separate agentId.
|
|
21
|
+
|
|
22
|
+
Verify opaque credential changes before the same configuration snapshot is committed. Drain admitted native callbacks across close/rollback, cancel target work on stop, and keep raw transport parsing, platform ACK timing and SDK native methods under their existing owners. Account metadata conceals credentials carried in URLs.
|
|
23
|
+
|
|
24
|
+
- a9f4e48: PATCH COMPATIBILITY CUTOVER: this batch intentionally changes plugin APIs. The maintainer explicitly chose patch releases with declared version boundaries. Upgrade the complete tested dependency set; patch numbering does not promise compatibility with the removed interfaces.
|
|
25
|
+
|
|
26
|
+
The first planned floors are zhin.js 1.1.19, Core 1.1.54, Adapter 1.1.26, FeatureKit 1.1.10, Plugin Runtime 1.1.18, Runtime/Isolate 1.1.16 and Agent 1.1.45. Keep old-interface plugins with the earlier locked stack until they are migrated. Adapter's independent Feature API changes from 1.0.0 to 2.0.0; official consumers declare ^2.0.0. Existing Feature API admission rejects explicitly incompatible ranges; omitted API metadata keeps its existing behavior and does not certify compatibility. Runtime engine and patch-only release gates remain unchanged.
|
|
27
|
+
|
|
28
|
+
Use the native-Client Adapter target contract, canonical FeatureKit AdapterRegistry augmentation and named Endpoint control/management requests. Route message controls from canonical message/conversation refs; retain exact source authority and propagate caller/account/explicit-Root-stop cancellation. Replace removed Agent activity feedback exports with source-bound feedback. HMR pause/resume belongs to concrete plugin instances; plugins choose accepted-work policy without changing Controller commit/rollback authority. Cooperative worker/process control cancellation does not kill an isolate or reopen retired business admission.
|
|
29
|
+
|
|
30
|
+
Provide authenticated historical-data preview, exact selected binding, transactional application, authoritative inspection and explicit resumption, including active games. Preserve before-images and journals; never replay an unconfirmed effect, infer an account from an alias, or reuse historical permission grants. Owner V2 approvals remain inactive legacy data, while new writes use V3. Rollback requires matching software, lockfile and verified data; installing old code alone cannot read a new V3 approval file. Conflicting after-images are never overwritten.
|
|
31
|
+
|
|
32
|
+
See docs/plans/adapter-operation-version-boundaries.md and the Chinese/English upgrade guide for the complete version inventory, custom-plugin changes, acceptance and recovery sequence. These code and metadata changes do not authorize merging, publication, deployment, login, production migration or removal of installed workspace patches.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [2c3610a]
|
|
35
|
+
- Updated dependencies [a9f4e48]
|
|
36
|
+
- Updated dependencies [f8505b0]
|
|
37
|
+
- Updated dependencies [2c3610a]
|
|
38
|
+
- @zhin.js/adapter@1.1.26
|
|
39
|
+
- @zhin.js/feature-kit@1.1.10
|
|
40
|
+
- @zhin.js/core@1.1.54
|
|
41
|
+
- @zhin.js/host-http@1.1.12
|
|
42
|
+
- @zhin.js/client@1.1.22
|
|
43
|
+
- @zhin.js/page@1.1.11
|
|
44
|
+
- @zhin.js/skill@1.1.11
|
|
45
|
+
|
|
3
46
|
## 1.1.18
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,13 +18,16 @@ pnpm add @zhin.js/adapter-wechat-mp
|
|
|
18
18
|
|
|
19
19
|
## Plugin Runtime
|
|
20
20
|
|
|
21
|
+
`WeChatMpEndpoint` 的 Client 实际拥有 axios API、Token 与签名/加密 Webhook;原始 XML 保留。框架生命周期只经 `endpointSpec` 接入。所属 `Adapter` 是账号期望状态与运行实例的唯一管理者,目录按实际内部身份路由。
|
|
22
|
+
|
|
21
23
|
- `@zhin.js/adapter` — 约定式 `adapters/wechat-mp/index.ts`(`defineAdapter`)
|
|
22
|
-
- `@zhin.js/core` — `
|
|
24
|
+
- `@zhin.js/core` — `endpointSpec.events` 入站、`outboundMessageToken` 出站
|
|
23
25
|
- `@zhin.js/host-http` — `httpHostToken` 注册 Webhook 路由(**非** legacy host-router/Koa)
|
|
24
26
|
- `zhin.js` — `plugin.ts`(`definePlugin`)
|
|
25
27
|
- 配置经插件 `schema.json` 落到顶层 `<instanceKey>`
|
|
26
28
|
|
|
27
|
-
入站:`
|
|
29
|
+
入站:`endpoint[endpointSpec].events.emit('message.receive', { conversation: { endpoint, kind: 'private', id: openid }, message: { conversation, id }, content: text, sender, metadata })`
|
|
30
|
+
|
|
28
31
|
出站:`send({ conversation, payload })` → 被动回复 XML(默认)或客服消息 API(`replyMode: customer_service`)
|
|
29
32
|
|
|
30
33
|
## 前置条件
|
|
@@ -77,13 +80,11 @@ Runtime Host(`http`)须已 listen,Webhook 才可达。
|
|
|
77
80
|
|
|
78
81
|
## Console 页面
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
在所属插件的 Console 页面管理账号:查看配置与运行状态,添加、编辑、启动、停止、替换或删除账号。首次修改启动账号前,管理员需确认迁移清单;保留原身份与历史。此后保存到 Host 运行时存储,不改启动配置或 `.env`,无需重启进程。
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
- 增删 `plugins.wechat-mp.endpoints`(凭据写入 `.env`,配置里只保留 `${ENV}` 引用);
|
|
84
|
-
- 如果适配器要求登录,页面会显示 LoginAssist 的扫码 / 滑块 / 设备确认待办;
|
|
85
|
+
新增仅保存,需手动启动;运行账号的普通配置更新只重连该账号。身份变化走明确替换,新账号不会继承旧 Session 或登录状态。账号凭据变化先按平台实际只读接口核验;平台无法核验时需明确替换。空白或遮蔽的密码不会覆盖既有密码。
|
|
85
86
|
|
|
86
|
-
读取需要 `wechat-mp:read
|
|
87
|
+
读取需要 `wechat-mp:read`,写入需要 `wechat-mp:write`;首次迁移还需管理员身份。无持久化存储时管理操作不可用,提交结果未知时先刷新核实,不重复提交。
|
|
87
88
|
|
|
88
89
|
## 故障排查
|
|
89
90
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { WeChatMpAdapter } from "../../lib/adapter.js";
|
|
2
3
|
/**
|
|
3
4
|
* Convention entry: discover `adapters/wechat-mp/index.ts` → defineAdapter.
|
|
4
5
|
*/
|
|
5
6
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
-
import {
|
|
7
|
-
import { WeChatMpEndpoint } from "../../lib/endpoint.js";
|
|
8
|
-
import { resolveWeChatMpConfig, } from "../../lib/protocol.js";
|
|
9
|
-
import { wechatMpRuntimeStateToken } from "../../lib/wechat-mp-runtime-state.js";
|
|
7
|
+
import {} from "../../lib/protocol.js";
|
|
10
8
|
export { WeChatMpEndpoint } from "../../lib/endpoint.js";
|
|
11
9
|
export default defineAdapter({
|
|
12
10
|
capabilities: ['inbound', 'outbound'],
|
|
@@ -16,17 +14,5 @@ export default defineAdapter({
|
|
|
16
14
|
outboundMedia: ['upload'],
|
|
17
15
|
interactive: 'text',
|
|
18
16
|
},
|
|
19
|
-
|
|
20
|
-
const config = resolveWeChatMpConfig(context.config);
|
|
21
|
-
// 注册到插件运行时状态(wechat-mp endpoint list 的"运行中"数据源)
|
|
22
|
-
context.use(wechatMpRuntimeStateToken).endpoints.set(config.id, {
|
|
23
|
-
id: config.id,
|
|
24
|
-
mode: 'webhook',
|
|
25
|
-
});
|
|
26
|
-
return new WeChatMpEndpoint({
|
|
27
|
-
id: context.id,
|
|
28
|
-
http: context.use(httpHostToken),
|
|
29
|
-
config,
|
|
30
|
-
});
|
|
31
|
-
},
|
|
17
|
+
createAdapter: context => new WeChatMpAdapter(context),
|
|
32
18
|
});
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
+
import { WeChatMpAdapter } from '../../src/adapter.js';
|
|
1
2
|
/**
|
|
2
3
|
* Convention entry: discover `adapters/wechat-mp/index.ts` → defineAdapter.
|
|
3
4
|
*/
|
|
4
5
|
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
-
import { httpHostToken } from '@zhin.js/host-http';
|
|
6
|
-
import { WeChatMpEndpoint } from '../../src/endpoint.js';
|
|
7
6
|
import {
|
|
8
|
-
resolveWeChatMpConfig,
|
|
9
7
|
type WeChatMpEndpointConfig,
|
|
10
8
|
} from '../../src/protocol.js';
|
|
11
|
-
import { wechatMpRuntimeStateToken } from '../../src/wechat-mp-runtime-state.js';
|
|
12
9
|
|
|
13
10
|
export { WeChatMpEndpoint } from '../../src/endpoint.js';
|
|
14
11
|
export type { WeChatMpEndpointOptions } from '../../src/endpoint.js';
|
|
@@ -22,17 +19,5 @@ export default defineAdapter<WeChatMpEndpointConfig>({
|
|
|
22
19
|
outboundMedia: ['upload'],
|
|
23
20
|
interactive: 'text',
|
|
24
21
|
},
|
|
25
|
-
|
|
26
|
-
const config = resolveWeChatMpConfig(context.config);
|
|
27
|
-
// 注册到插件运行时状态(wechat-mp endpoint list 的"运行中"数据源)
|
|
28
|
-
context.use(wechatMpRuntimeStateToken).endpoints.set(config.id, {
|
|
29
|
-
id: config.id,
|
|
30
|
-
mode: 'webhook',
|
|
31
|
-
});
|
|
32
|
-
return new WeChatMpEndpoint({
|
|
33
|
-
id: context.id,
|
|
34
|
-
http: context.use(httpHostToken),
|
|
35
|
-
config,
|
|
36
|
-
});
|
|
37
|
-
},
|
|
22
|
+
createAdapter: context => new WeChatMpAdapter(context),
|
|
38
23
|
});
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
7
7
|
import { defineCommand } from 'zhin.js/command';
|
|
8
|
-
import { wechatMpRuntimeStateToken } from "../../../lib/wechat-mp-runtime-state.js";
|
|
9
8
|
export const wechatMpEndpointCommands = createEndpointCommands({
|
|
10
9
|
adapterKey: 'wechat-mp',
|
|
11
10
|
adapterDisplayName: 'WeChat MP',
|
|
@@ -15,6 +14,5 @@ export const wechatMpEndpointCommands = createEndpointCommands({
|
|
|
15
14
|
{ key: 'token', required: true, env: true, description: '服务器配置 token', descriptionI18n: { en: "Server configuration token" } },
|
|
16
15
|
{ key: 'encodingAESKey', env: true, description: '消息加解密 key', descriptionI18n: { en: "Message encryption key" } },
|
|
17
16
|
],
|
|
18
|
-
running: (use) => use(wechatMpRuntimeStateToken).endpoints.values(),
|
|
19
17
|
describeEntry: (entry) => `appId: ${String(entry.appId)}`,
|
|
20
18
|
}, defineCommand);
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
6
6
|
import { defineCommand } from 'zhin.js/command';
|
|
7
|
-
import { wechatMpRuntimeStateToken } from '../../../src/wechat-mp-runtime-state.js';
|
|
8
7
|
|
|
9
8
|
export const wechatMpEndpointCommands = createEndpointCommands({
|
|
10
9
|
adapterKey: 'wechat-mp',
|
|
@@ -15,6 +14,5 @@ export const wechatMpEndpointCommands = createEndpointCommands({
|
|
|
15
14
|
{ key: 'token', required: true, env: true, description: '服务器配置 token', descriptionI18n: { en: "Server configuration token" } },
|
|
16
15
|
{ key: 'encodingAESKey', env: true, description: '消息加解密 key', descriptionI18n: { en: "Message encryption key" } },
|
|
17
16
|
],
|
|
18
|
-
running: (use) => use(wechatMpRuntimeStateToken).endpoints.values(),
|
|
19
17
|
describeEntry: (entry) => `appId: ${String(entry.appId)}`,
|
|
20
18
|
}, defineCommand);
|
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { WeChatMpEndpoint } from './endpoint.js';
|
|
3
|
+
import { type WeChatMpEndpointConfig } from './protocol.js';
|
|
4
|
+
import type { WeChatMpClientEventMap } from './client.js';
|
|
5
|
+
export declare class WeChatMpAdapter extends Adapter<WeChatMpEndpoint, WeChatMpEndpointConfig, WeChatMpClientEventMap> {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(context: AdapterContext<WeChatMpEndpointConfig>);
|
|
8
|
+
get configurationIdentityFields(): readonly string[];
|
|
9
|
+
get configurationUpdateFields(): readonly string[];
|
|
10
|
+
get configurationIdentityProbeFields(): readonly string[];
|
|
11
|
+
protected validateConfiguration(config: WeChatMpEndpointConfig): void;
|
|
12
|
+
protected createEndpoint(input: WeChatMpEndpointConfig, execution: EndpointCreationContext): WeChatMpEndpoint;
|
|
13
|
+
}
|
|
14
|
+
declare module '@zhin.js/feature-kit' {
|
|
15
|
+
interface AdapterRegistry {
|
|
16
|
+
readonly 'wechat-mp': WeChatMpAdapter;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Adapter } from 'zhin.js/adapter';
|
|
2
|
+
import { WeChatMpEndpoint } from './endpoint.js';
|
|
3
|
+
import { resolveWeChatMpConfig } from './protocol.js';
|
|
4
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
5
|
+
export class WeChatMpAdapter extends Adapter {
|
|
6
|
+
#context;
|
|
7
|
+
constructor(context) { super(); this.#context = context; }
|
|
8
|
+
get configurationIdentityFields() { return ['id', 'appId']; }
|
|
9
|
+
get configurationUpdateFields() { return ['appSecret', 'token', 'encodingAESKey', 'path', 'encrypt', 'encryptMode', 'replyMode', 'passiveReplyTimeoutMs']; }
|
|
10
|
+
get configurationIdentityProbeFields() { return ['appSecret']; }
|
|
11
|
+
validateConfiguration(config) { resolveWeChatMpConfig(config); }
|
|
12
|
+
createEndpoint(input, execution) {
|
|
13
|
+
const config = resolveWeChatMpConfig(input);
|
|
14
|
+
return new WeChatMpEndpoint({ id: execution.canonicalId, config, http: this.#context.use(httpHostToken) });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type EndpointManagementRequest, type EndpointOperationOptions } from 'zhin.js/adapter';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import type { ResolvedWeChatMpConfig, WeChatAPIResponse } from './protocol.js';
|
|
4
|
+
export type WeChatMpFetch = (url: string, init?: {
|
|
5
|
+
readonly method?: string;
|
|
6
|
+
readonly body?: unknown;
|
|
7
|
+
readonly headers?: Record<string, string>;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
readonly data: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
13
|
+
export declare class WeChatMpApiClient extends EventEmitter {
|
|
14
|
+
#private;
|
|
15
|
+
readonly apiConfig: ResolvedWeChatMpConfig;
|
|
16
|
+
constructor(apiConfig: ResolvedWeChatMpConfig, fetch: WeChatMpFetch);
|
|
17
|
+
readonly fetch: WeChatMpFetch;
|
|
18
|
+
protected transportSignal?: AbortSignal;
|
|
19
|
+
get accessToken(): string | null;
|
|
20
|
+
get tokenExpired(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
|
|
23
|
+
* 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
|
|
24
|
+
*/
|
|
25
|
+
refreshAccessToken(): Promise<string>;
|
|
26
|
+
request<T = unknown>(path: string, init?: Parameters<WeChatMpFetch>[1]): Promise<T>;
|
|
27
|
+
sendCustomerService(messageData: unknown, request?: EndpointOperationOptions): Promise<WeChatAPIResponse>;
|
|
28
|
+
uploadMedia({ type, body, signal }: WeChatMpUploadRequest): Promise<string>;
|
|
29
|
+
getFollowerIds(request?: EndpointManagementRequest): Promise<readonly string[]>;
|
|
30
|
+
}
|
|
31
|
+
export interface WeChatMpUploadRequest extends EndpointOperationOptions {
|
|
32
|
+
readonly type: 'image' | 'voice' | 'video';
|
|
33
|
+
readonly body: unknown;
|
|
34
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createEndpointOperation } from 'zhin.js/adapter';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
|
|
4
|
+
/** 换取 token 请求的超时,防止挂起的请求让 single-flight 永久卡住。 */
|
|
5
|
+
const TOKEN_FETCH_TIMEOUT_MS = 15_000;
|
|
6
|
+
/** 官方 access_token 有效期为 7200s;对异常大的 expires_in 设上限。 */
|
|
7
|
+
const MAX_TOKEN_LIFETIME_SEC = 7200;
|
|
8
|
+
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
9
|
+
export class WeChatMpApiClient extends EventEmitter {
|
|
10
|
+
apiConfig;
|
|
11
|
+
#accessToken = null;
|
|
12
|
+
#tokenExpireTime = 0;
|
|
13
|
+
#refreshing = null;
|
|
14
|
+
constructor(apiConfig, fetch) {
|
|
15
|
+
super();
|
|
16
|
+
this.apiConfig = apiConfig;
|
|
17
|
+
this.fetch = (url, init) => {
|
|
18
|
+
this.transportSignal?.throwIfAborted();
|
|
19
|
+
return fetch(url, { ...init, ...(this.transportSignal ? { signal: init?.signal ? AbortSignal.any([this.transportSignal, init.signal]) : this.transportSignal } : {}) });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
fetch;
|
|
23
|
+
transportSignal;
|
|
24
|
+
get accessToken() {
|
|
25
|
+
return this.#accessToken;
|
|
26
|
+
}
|
|
27
|
+
get tokenExpired() {
|
|
28
|
+
return !this.#accessToken || Date.now() >= this.#tokenExpireTime;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
|
|
32
|
+
* 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
|
|
33
|
+
*/
|
|
34
|
+
refreshAccessToken() {
|
|
35
|
+
if (!this.#refreshing) {
|
|
36
|
+
this.#refreshing = this.#fetchAccessToken().finally(() => {
|
|
37
|
+
this.#refreshing = null;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return this.#refreshing;
|
|
41
|
+
}
|
|
42
|
+
async #fetchAccessToken() {
|
|
43
|
+
const { appId, appSecret } = this.apiConfig;
|
|
44
|
+
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(appSecret)}`;
|
|
45
|
+
let timeout;
|
|
46
|
+
const response = await Promise.race([
|
|
47
|
+
this.fetch(url),
|
|
48
|
+
new Promise((_, reject) => {
|
|
49
|
+
timeout = setTimeout(() => reject(new Error('Failed to get access token: request timed out')), TOKEN_FETCH_TIMEOUT_MS);
|
|
50
|
+
}),
|
|
51
|
+
]).finally(() => clearTimeout(timeout));
|
|
52
|
+
const data = response.data;
|
|
53
|
+
if (!data.access_token) {
|
|
54
|
+
throw new Error(data.errmsg
|
|
55
|
+
? `Failed to get access token: ${data.errcode} ${data.errmsg}`
|
|
56
|
+
: 'Failed to get access token');
|
|
57
|
+
}
|
|
58
|
+
this.#accessToken = data.access_token;
|
|
59
|
+
// expires_in 缺失/非法时 NaN 会让 tokenExpired 永远为 false;按官方默认 7200s 兜底。
|
|
60
|
+
const expiresIn = Number(data.expires_in);
|
|
61
|
+
const lifetimeSec = Number.isFinite(expiresIn) && expiresIn > 0
|
|
62
|
+
? Math.min(expiresIn, MAX_TOKEN_LIFETIME_SEC)
|
|
63
|
+
: MAX_TOKEN_LIFETIME_SEC;
|
|
64
|
+
this.#tokenExpireTime = Date.now() + (lifetimeSec - 300) * 1000;
|
|
65
|
+
return data.access_token;
|
|
66
|
+
}
|
|
67
|
+
async request(path, init) {
|
|
68
|
+
return (await this.#requestWithToken(path, init)).data;
|
|
69
|
+
}
|
|
70
|
+
async #requestWithToken(path, init) {
|
|
71
|
+
if (this.tokenExpired)
|
|
72
|
+
await createEndpointOperation({ signal: init?.signal }).prepare(() => this.refreshAccessToken());
|
|
73
|
+
init?.signal?.throwIfAborted();
|
|
74
|
+
const token = this.#accessToken;
|
|
75
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
76
|
+
const response = await this.fetch(`https://api.weixin.qq.com${path}${separator}access_token=${token}`, init);
|
|
77
|
+
return { data: response.data, token };
|
|
78
|
+
}
|
|
79
|
+
async sendCustomerService(messageData, request = {}) {
|
|
80
|
+
const first = await this.#requestWithToken('/cgi-bin/message/custom/send', { method: 'POST', body: messageData, signal: request.signal });
|
|
81
|
+
let result = first.data;
|
|
82
|
+
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
83
|
+
request.signal?.throwIfAborted();
|
|
84
|
+
// 失败请求使用的 token 若已被其他请求换掉,直接用新 token 重试,不再重复刷新。
|
|
85
|
+
if (this.#accessToken === first.token)
|
|
86
|
+
await createEndpointOperation(request).prepare(() => this.refreshAccessToken());
|
|
87
|
+
request.signal?.throwIfAborted();
|
|
88
|
+
result = await this.request('/cgi-bin/message/custom/send', { method: 'POST', body: messageData, signal: request.signal });
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async uploadMedia({ type, body, signal }) {
|
|
93
|
+
const data = await this.request(`/cgi-bin/media/upload?type=${type}`, { method: 'POST', body, signal });
|
|
94
|
+
if (data.media_id)
|
|
95
|
+
return data.media_id;
|
|
96
|
+
throw new Error(`WeChat media upload failed: ${data.errcode ?? 'unknown'} ${data.errmsg ?? ''}`.trim());
|
|
97
|
+
}
|
|
98
|
+
async getFollowerIds(request = {}) {
|
|
99
|
+
const followers = [];
|
|
100
|
+
let nextOpenid = '';
|
|
101
|
+
do {
|
|
102
|
+
const data = await this.request(`/cgi-bin/user/get?next_openid=${encodeURIComponent(nextOpenid)}`, { signal: request.signal });
|
|
103
|
+
if (data.errcode && data.errcode !== 0) {
|
|
104
|
+
throw new Error(`WeChat API error: ${data.errcode} - ${data.errmsg}`);
|
|
105
|
+
}
|
|
106
|
+
followers.push(...(data.data?.openid ?? []).filter(Boolean));
|
|
107
|
+
if (Number(data.count ?? 0) < 10_000)
|
|
108
|
+
break;
|
|
109
|
+
nextOpenid = data.next_openid ?? '';
|
|
110
|
+
} while (nextOpenid);
|
|
111
|
+
return Object.freeze(followers);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/lib/client.d.ts
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
readonly
|
|
8
|
-
|
|
9
|
-
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
10
|
-
export declare class WeChatMpClient {
|
|
11
|
-
#private;
|
|
1
|
+
import { WeChatMpApiClient, type WeChatMpFetch } from './api-client.js';
|
|
2
|
+
import { type EndpointManagement, type 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
|
+
export interface WeChatMpEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly http: HttpHost;
|
|
12
9
|
readonly config: ResolvedWeChatMpConfig;
|
|
13
|
-
readonly fetch
|
|
14
|
-
constructor(config: ResolvedWeChatMpConfig, fetch: WeChatMpFetch);
|
|
15
|
-
get accessToken(): string | null;
|
|
16
|
-
get tokenExpired(): boolean;
|
|
17
|
-
/**
|
|
18
|
-
* 刷新 access_token。并发调用共享同一次请求:微信每次换取新 token 都会使旧 token
|
|
19
|
-
* 进入失效倒计时,且有每日调用次数上限,重复刷新会互相顶掉并消耗配额。
|
|
20
|
-
*/
|
|
21
|
-
refreshAccessToken(): Promise<string>;
|
|
22
|
-
request<T = unknown>(path: string, init?: Parameters<WeChatMpFetch>[1]): Promise<T>;
|
|
23
|
-
sendCustomerService(messageData: unknown): Promise<WeChatAPIResponse>;
|
|
24
|
-
uploadMedia(type: 'image' | 'voice' | 'video', body: unknown): Promise<string>;
|
|
25
|
-
getFollowerIds(): Promise<readonly string[]>;
|
|
10
|
+
readonly fetch?: WeChatMpFetch;
|
|
26
11
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
export declare class WeChatMpClient extends WeChatMpApiClient {
|
|
13
|
+
#private;
|
|
14
|
+
protected readonly endpointManagement: EndpointManagement;
|
|
15
|
+
protected configName(): string;
|
|
16
|
+
protected resolvePresentationLocale(_senderId: string): Promise<import('zhin.js').Locale | undefined>;
|
|
17
|
+
get transportState(): import('zhin.js/adapter').EndpointTransportState | undefined;
|
|
18
|
+
protected executionSignal?: AbortSignal;
|
|
19
|
+
protected setExecutionSignal(signal: AbortSignal): void;
|
|
20
|
+
/** Native protocol work runs here; Endpoint attaches the framework receipt without replacing SDK APIs. */
|
|
21
|
+
protected receiveInboundCallback<T>(receive: () => T | Promise<T>): Promise<T>;
|
|
22
|
+
protected publishEvent(name: string, payload: unknown): Promise<unknown>;
|
|
23
|
+
protected publishAccepted(name: string, payload: unknown): Promise<void>;
|
|
24
|
+
protected publishPlatform(name: string, event: unknown): Promise<unknown>;
|
|
25
|
+
constructor(options: WeChatMpEndpointOptions);
|
|
26
|
+
/** Used by webhook handler. */
|
|
27
|
+
get isOpen(): boolean;
|
|
28
|
+
get config(): ResolvedWeChatMpConfig;
|
|
29
|
+
get id(): CapabilityId;
|
|
30
|
+
/** 微信 5s 重推去重:见过该 MsgId 时返回首次回复 XML(含空串=success)。 */
|
|
31
|
+
getCachedReply(msgId: string): string | undefined;
|
|
32
|
+
cacheReply(msgId: string, replyXML: string): void;
|
|
33
|
+
protected connectTransport(signal?: AbortSignal): Promise<void>;
|
|
34
|
+
protected openInput(): void;
|
|
35
|
+
protected closeInput(): void;
|
|
36
|
+
protected disconnectTransport(): Promise<void>;
|
|
37
|
+
protected sendPayload({ conversation, payload, signal }: EndpointSendRequest): Promise<string>;
|
|
38
|
+
/** Test / internal: admit a parsed message when open (non-webhook path). */
|
|
39
|
+
admit(msg: WeChatMessage, nativeEvent?: unknown): void | Promise<unknown>;
|
|
35
40
|
}
|
|
36
|
-
export
|
|
41
|
+
export type WeChatMpClientEventMap = Record<string, unknown>;
|
|
42
|
+
export type { WeChatMpFetch } from './api-client.js';
|