@zhin.js/adapter-wechat-mp 1.0.1 → 1.1.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 +551 -0
- package/README.md +41 -88
- package/adapters/wechat-mp/index.js +32 -0
- package/adapters/wechat-mp/index.ts +38 -0
- package/commands/wechat-mp/endpoint/add/[id]/index.js +3 -0
- package/commands/wechat-mp/endpoint/add/[id]/index.ts +3 -0
- package/commands/wechat-mp/endpoint/definition.js +20 -0
- package/commands/wechat-mp/endpoint/definition.ts +20 -0
- package/commands/wechat-mp/endpoint/list/index.js +3 -0
- package/commands/wechat-mp/endpoint/list/index.ts +3 -0
- package/commands/wechat-mp/endpoint/remove/[id]/index.js +3 -0
- package/commands/wechat-mp/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +32 -0
- package/lib/client.js +70 -0
- package/lib/endpoint.d.ts +31 -72
- package/lib/endpoint.js +226 -747
- package/lib/index.d.ts +6 -15
- package/lib/index.js +6 -25
- package/lib/media-upload.d.ts +22 -0
- package/lib/media-upload.js +64 -0
- package/lib/passive-reply.d.ts +0 -1
- package/lib/passive-reply.js +0 -1
- package/lib/protocol.d.ts +126 -0
- package/lib/protocol.js +350 -0
- package/lib/side-event-dispatch.d.ts +4 -0
- package/lib/side-event-dispatch.js +38 -0
- package/lib/webhook.d.ts +20 -0
- package/lib/webhook.js +152 -0
- package/lib/wechat-mp-runtime-state.d.ts +1 -0
- package/lib/wechat-mp-runtime-state.js +6 -0
- package/package.json +57 -12
- package/plugin.js +14 -0
- package/schema.json +144 -0
- package/src/client.ts +121 -0
- package/src/endpoint.ts +276 -902
- package/src/index.ts +56 -35
- package/src/media-upload.ts +82 -0
- package/src/protocol.ts +508 -0
- package/src/side-event-dispatch.ts +45 -0
- package/src/webhook.ts +237 -0
- package/src/wechat-mp-runtime-state.ts +7 -0
- package/lib/adapter.d.ts +0 -14
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -17
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/passive-reply.d.ts.map +0 -1
- package/lib/passive-reply.js.map +0 -1
- package/lib/types.d.ts +0 -58
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/skills/wechat-mp/SKILL.md +0 -33
- package/src/adapter.ts +0 -22
- package/src/types.ts +0 -60
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @zhin.js/adapter-wechat-mp
|
|
2
2
|
|
|
3
|
-
Zhin.js
|
|
3
|
+
Zhin.js 微信公众号适配器(Plugin Runtime),通过 Runtime Host HTTP Webhook 收发消息。
|
|
4
4
|
|
|
5
5
|
## 功能特性
|
|
6
6
|
|
|
7
|
-
- Webhook
|
|
8
|
-
-
|
|
7
|
+
- Webhook 事件接收(`httpHostToken` GET 验签 + POST 消息)
|
|
8
|
+
- 签名验证与可选 AES 加解密
|
|
9
9
|
- Access Token 自动刷新
|
|
10
10
|
- XML 消息解析
|
|
11
|
-
-
|
|
11
|
+
- 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
|
|
12
12
|
|
|
13
13
|
## 安装
|
|
14
14
|
|
|
@@ -16,105 +16,54 @@ Zhin.js 微信公众号适配器,支持微信公众号的消息收发。
|
|
|
16
16
|
pnpm add @zhin.js/adapter-wechat-mp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Plugin Runtime
|
|
20
|
+
|
|
21
|
+
- `@zhin.js/adapter` — 约定式 `adapters/wechat-mp/index.ts`(`defineAdapter`)
|
|
22
|
+
- `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
|
|
23
|
+
- `@zhin.js/host-http` — `httpHostToken` 注册 Webhook 路由(**非** legacy host-router/Koa)
|
|
24
|
+
- `zhin.js` — `plugin.ts`(`definePlugin`)
|
|
25
|
+
- 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
|
|
26
|
+
|
|
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
|
+
|
|
19
30
|
## 前置条件
|
|
20
31
|
|
|
21
32
|
| 要求 | 说明 |
|
|
22
33
|
|------|------|
|
|
23
34
|
| **公众号** | 已注册微信公众号,并在 [微信公众平台](https://mp.weixin.qq.com/) 获取 `AppID`、`AppSecret` |
|
|
24
35
|
| **服务器配置** | 配置 Token(与 `token` 字段一致);服务器 URL 须公网可访问 |
|
|
25
|
-
| **host-
|
|
36
|
+
| **host-http** | **必需** — Runtime Host 提供 HTTP;适配器在 `path` 上注册 GET/POST |
|
|
26
37
|
| **响应时限** | 微信要求 **5 秒内**响应;超时会导致接入失败 |
|
|
27
|
-
| **回复模式** | 默认 `replyMode: passive
|
|
28
|
-
| **消息加密** | 可选;`encrypt: true` + `encodingAESKey`;`encryptMode: compatible
|
|
38
|
+
| **回复模式** | 默认 `replyMode: passive`(订阅号被动回复);服务号可设 `customer_service` |
|
|
39
|
+
| **消息加密** | 可选;`encrypt: true` + `encodingAESKey`;`encryptMode: compatible`(默认)或 `secure` |
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
必填字段(`endpoints[i]`):`id`、`appId`、`appSecret`、`token`。运行时由 AdapterIndex
|
|
42
|
+
把实例级默认值与每个 endpoint 合并;协议实现只接收一份展开后的 endpoint 配置。
|
|
31
43
|
|
|
32
44
|
## 最小配置
|
|
33
45
|
|
|
34
46
|
```yaml
|
|
47
|
+
# zhin.config.yml(Plugin Runtime)
|
|
35
48
|
plugins:
|
|
36
|
-
|
|
37
|
-
- "@zhin.js/host-router"
|
|
38
|
-
|
|
39
|
-
endpoints:
|
|
40
|
-
- context: wechat-mp
|
|
41
|
-
name: my-wechat-bot
|
|
42
|
-
appId: "${WECHAT_APP_ID}"
|
|
43
|
-
appSecret: "${WECHAT_APP_SECRET}"
|
|
44
|
-
token: "${WECHAT_TOKEN}"
|
|
45
|
-
path: /wechat/webhook
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## 依赖
|
|
49
|
-
|
|
50
|
-
- `@zhin.js/host-router` — HTTP 服务(提供 Webhook 路由)
|
|
51
|
-
|
|
52
|
-
## 配置
|
|
53
|
-
|
|
54
|
-
### TypeScript 配置
|
|
55
|
-
|
|
56
|
-
完整选项示例(含可选加密字段):
|
|
57
|
-
|
|
58
|
-
```yaml
|
|
59
|
-
endpoints:
|
|
60
|
-
- context: wechat-mp
|
|
61
|
-
name: my-wechat-bot
|
|
62
|
-
appId: ${WECHAT_APP_ID}
|
|
63
|
-
appSecret: ${WECHAT_APP_SECRET}
|
|
64
|
-
token: ${WECHAT_TOKEN}
|
|
49
|
+
wechat-mp:
|
|
65
50
|
path: /wechat/webhook
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
endpoints:
|
|
52
|
+
- id: my-wechat-bot
|
|
53
|
+
appId: "${WECHAT_APP_ID}"
|
|
54
|
+
appSecret: "${WECHAT_APP_SECRET}"
|
|
55
|
+
token: "${WECHAT_TOKEN}"
|
|
68
56
|
```
|
|
69
57
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export default defineConfig({
|
|
74
|
-
endpoints: [
|
|
75
|
-
{
|
|
76
|
-
context: 'wechat-mp',
|
|
77
|
-
name: 'my-wechat-bot',
|
|
78
|
-
appId: process.env.WECHAT_APP_ID!,
|
|
79
|
-
appSecret: process.env.WECHAT_APP_SECRET!,
|
|
80
|
-
token: process.env.WECHAT_TOKEN!,
|
|
81
|
-
path: '/wechat/webhook',
|
|
82
|
-
}
|
|
83
|
-
],
|
|
84
|
-
plugins: ['@zhin.js/adapter-wechat-mp', '@zhin.js/host-router']
|
|
85
|
-
})
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## 使用示例
|
|
89
|
-
|
|
90
|
-
### 注册命令
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
import { usePlugin, MessageCommand } from 'zhin.js'
|
|
94
|
-
|
|
95
|
-
const { addCommand } = usePlugin()
|
|
96
|
-
|
|
97
|
-
addCommand(
|
|
98
|
-
new MessageCommand('hello')
|
|
99
|
-
.desc('微信问候')
|
|
100
|
-
.action((message) => `你好,${message.$sender.name}!`)
|
|
101
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### 消息中间件
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
import { usePlugin } from 'zhin.js'
|
|
58
|
+
根插件 `zhin.plugins`(或项目图)需引用 `@zhin.js/adapter-wechat-mp`(`instanceKey: wechat-mp`)。
|
|
59
|
+
Runtime Host(`http`)须已 listen,Webhook 才可达。
|
|
108
60
|
|
|
109
|
-
|
|
61
|
+
### 可选字段
|
|
110
62
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await next()
|
|
116
|
-
})
|
|
117
|
-
```
|
|
63
|
+
- `path`:Webhook 路径,默认 `/wechat/webhook`
|
|
64
|
+
- `replyMode`:`passive`(默认)| `customer_service`
|
|
65
|
+
- `passiveReplyTimeoutMs`:被动回复等待上限,默认 `4500`
|
|
66
|
+
- `encrypt` / `encodingAESKey` / `encryptMode`
|
|
118
67
|
|
|
119
68
|
## 微信公众号配置
|
|
120
69
|
|
|
@@ -128,12 +77,16 @@ addMiddleware(async (message, next) => {
|
|
|
128
77
|
|
|
129
78
|
| 现象 | 排查 |
|
|
130
79
|
|------|------|
|
|
131
|
-
| 服务器配置验证失败 | `token` 与公众平台一致;URL 为 `https://<host>/wechat/webhook`;Host
|
|
132
|
-
| 收不到用户消息 | 公众号类型是否支持消息接口;用户是否已关注;`path` 与公众平台 URL
|
|
133
|
-
| 回复超时 / 无回复 | 默认被动回复须在 **~4.5s**
|
|
80
|
+
| 服务器配置验证失败 | `token` 与公众平台一致;URL 为 `https://<host>/wechat/webhook`;Runtime Host 已 listen 且公网可达 |
|
|
81
|
+
| 收不到用户消息 | 公众号类型是否支持消息接口;用户是否已关注;`path` 与公众平台 URL 一致;endpoint 已 `open()` |
|
|
82
|
+
| 回复超时 / 无回复 | 默认被动回复须在 **~4.5s** 内完成;可改 `replyMode: customer_service`(需客服接口权限) |
|
|
134
83
|
| `48001 api unauthorized` | 未认证订阅号无客服 API;保持默认 `replyMode: passive` |
|
|
135
84
|
| 加密模式报错 | `encodingAESKey`、`encrypt` 与公众平台「安全模式」设置一致 |
|
|
136
85
|
|
|
86
|
+
## AI 工具
|
|
87
|
+
|
|
88
|
+
技能说明见 `skills/wechat-mp/SKILL.md`。
|
|
89
|
+
|
|
137
90
|
## 文档链接
|
|
138
91
|
|
|
139
92
|
- [微信公众号适配器文档](https://zhin.js.org/adapters/wechat-mp)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* Convention entry: discover `adapters/wechat-mp/index.ts` → defineAdapter.
|
|
4
|
+
*/
|
|
5
|
+
import { defineAdapter } from 'zhin.js/adapter';
|
|
6
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
7
|
+
import { WeChatMpEndpoint } from "../../lib/endpoint.js";
|
|
8
|
+
import { resolveWeChatMpConfig, } from "../../lib/protocol.js";
|
|
9
|
+
import { wechatMpRuntimeStateToken } from "../../lib/wechat-mp-runtime-state.js";
|
|
10
|
+
export { WeChatMpEndpoint } from "../../lib/endpoint.js";
|
|
11
|
+
export default defineAdapter({
|
|
12
|
+
capabilities: ['inbound', 'outbound'],
|
|
13
|
+
// 客服消息媒体统一经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传,
|
|
14
|
+
// kind=file 的 MediaRef 视为既有 media_id 直传);公众号无卡片交互面,交互段降级纯文本。
|
|
15
|
+
segments: {
|
|
16
|
+
outboundMedia: ['upload'],
|
|
17
|
+
interactive: 'text',
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
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
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convention entry: discover `adapters/wechat-mp/index.ts` → defineAdapter.
|
|
3
|
+
*/
|
|
4
|
+
import { defineAdapter } from 'zhin.js/adapter';
|
|
5
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
6
|
+
import { WeChatMpEndpoint } from '../../src/endpoint.js';
|
|
7
|
+
import {
|
|
8
|
+
resolveWeChatMpConfig,
|
|
9
|
+
type WeChatMpEndpointConfig,
|
|
10
|
+
} from '../../src/protocol.js';
|
|
11
|
+
import { wechatMpRuntimeStateToken } from '../../src/wechat-mp-runtime-state.js';
|
|
12
|
+
|
|
13
|
+
export { WeChatMpEndpoint } from '../../src/endpoint.js';
|
|
14
|
+
export type { WeChatMpEndpointOptions } from '../../src/endpoint.js';
|
|
15
|
+
export type { WeChatMpFetch } from '../../src/client.js';
|
|
16
|
+
|
|
17
|
+
export default defineAdapter<WeChatMpEndpointConfig>({
|
|
18
|
+
capabilities: ['inbound', 'outbound'],
|
|
19
|
+
// 客服消息媒体统一经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传,
|
|
20
|
+
// kind=file 的 MediaRef 视为既有 media_id 直传);公众号无卡片交互面,交互段降级纯文本。
|
|
21
|
+
segments: {
|
|
22
|
+
outboundMedia: ['upload'],
|
|
23
|
+
interactive: 'text',
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
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
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* `wechat-mp endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
4
|
+
* commands/wechat-mp/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
5
|
+
*/
|
|
6
|
+
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
7
|
+
import { defineCommand } from 'zhin.js/command';
|
|
8
|
+
import { wechatMpRuntimeStateToken } from "../../../lib/wechat-mp-runtime-state.js";
|
|
9
|
+
export const wechatMpEndpointCommands = createEndpointCommands({
|
|
10
|
+
adapterKey: 'wechat-mp',
|
|
11
|
+
adapterDisplayName: 'WeChat MP',
|
|
12
|
+
fields: [
|
|
13
|
+
{ key: 'appId', required: true, env: true, description: '公众号 AppID' },
|
|
14
|
+
{ key: 'appSecret', required: true, env: true, description: '公众号 AppSecret' },
|
|
15
|
+
{ key: 'token', required: true, env: true, description: '服务器配置 token' },
|
|
16
|
+
{ key: 'encodingAESKey', env: true, description: '消息加解密 key' },
|
|
17
|
+
],
|
|
18
|
+
running: (use) => use(wechatMpRuntimeStateToken).endpoints.values(),
|
|
19
|
+
describeEntry: (entry) => `appId: ${String(entry.appId)}`,
|
|
20
|
+
}, defineCommand);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wechat-mp endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/wechat-mp/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*/
|
|
5
|
+
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
6
|
+
import { defineCommand } from 'zhin.js/command';
|
|
7
|
+
import { wechatMpRuntimeStateToken } from '../../../src/wechat-mp-runtime-state.js';
|
|
8
|
+
|
|
9
|
+
export const wechatMpEndpointCommands = createEndpointCommands({
|
|
10
|
+
adapterKey: 'wechat-mp',
|
|
11
|
+
adapterDisplayName: 'WeChat MP',
|
|
12
|
+
fields: [
|
|
13
|
+
{ key: 'appId', required: true, env: true, description: '公众号 AppID' },
|
|
14
|
+
{ key: 'appSecret', required: true, env: true, description: '公众号 AppSecret' },
|
|
15
|
+
{ key: 'token', required: true, env: true, description: '服务器配置 token' },
|
|
16
|
+
{ key: 'encodingAESKey', env: true, description: '消息加解密 key' },
|
|
17
|
+
],
|
|
18
|
+
running: (use) => use(wechatMpRuntimeStateToken).endpoints.values(),
|
|
19
|
+
describeEntry: (entry) => `appId: ${String(entry.appId)}`,
|
|
20
|
+
}, defineCommand);
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResolvedWeChatMpConfig, WeChatAPIResponse } from './protocol.js';
|
|
2
|
+
export type WeChatMpFetch = (url: string, init?: {
|
|
3
|
+
readonly method?: string;
|
|
4
|
+
readonly body?: unknown;
|
|
5
|
+
readonly headers?: Record<string, string>;
|
|
6
|
+
}) => Promise<{
|
|
7
|
+
readonly data: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
10
|
+
export declare class WeChatMpClient {
|
|
11
|
+
#private;
|
|
12
|
+
readonly config: ResolvedWeChatMpConfig;
|
|
13
|
+
readonly fetch: WeChatMpFetch;
|
|
14
|
+
constructor(config: ResolvedWeChatMpConfig, fetch: WeChatMpFetch);
|
|
15
|
+
get accessToken(): string | null;
|
|
16
|
+
get tokenExpired(): boolean;
|
|
17
|
+
refreshAccessToken(): Promise<string>;
|
|
18
|
+
request<T = unknown>(path: string, init?: Parameters<WeChatMpFetch>[1]): Promise<T>;
|
|
19
|
+
sendCustomerService(messageData: unknown): Promise<WeChatAPIResponse>;
|
|
20
|
+
uploadMedia(type: 'image' | 'voice' | 'video', body: unknown): Promise<string>;
|
|
21
|
+
getFollowerIds(): Promise<readonly string[]>;
|
|
22
|
+
}
|
|
23
|
+
export type WeChatMpClientEventMap = Record<string, unknown>;
|
|
24
|
+
declare module '@zhin.js/feature-kit' {
|
|
25
|
+
interface AdapterClientRegistry {
|
|
26
|
+
readonly 'wechat-mp': {
|
|
27
|
+
readonly client: WeChatMpClient;
|
|
28
|
+
readonly events: WeChatMpClientEventMap;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export declare const wechatMpClient: import("@zhin.js/adapter").EndpointClientToken<WeChatMpClient, WeChatMpClientEventMap>;
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineEndpointClient } from 'zhin.js/adapter';
|
|
2
|
+
const TOKEN_INVALID_ERRCODES = new Set([40001, 40014, 42001]);
|
|
3
|
+
/** Direct WeChat Official Account API client exposed to plugins. */
|
|
4
|
+
export class WeChatMpClient {
|
|
5
|
+
config;
|
|
6
|
+
fetch;
|
|
7
|
+
#accessToken = null;
|
|
8
|
+
#tokenExpireTime = 0;
|
|
9
|
+
constructor(config, fetch) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.fetch = fetch;
|
|
12
|
+
}
|
|
13
|
+
get accessToken() {
|
|
14
|
+
return this.#accessToken;
|
|
15
|
+
}
|
|
16
|
+
get tokenExpired() {
|
|
17
|
+
return !this.#accessToken || Date.now() >= this.#tokenExpireTime;
|
|
18
|
+
}
|
|
19
|
+
async refreshAccessToken() {
|
|
20
|
+
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
|
+
const response = await this.fetch(url);
|
|
23
|
+
const data = response.data;
|
|
24
|
+
if (!data.access_token) {
|
|
25
|
+
throw new Error(data.errmsg
|
|
26
|
+
? `Failed to get access token: ${data.errcode} ${data.errmsg}`
|
|
27
|
+
: 'Failed to get access token');
|
|
28
|
+
}
|
|
29
|
+
this.#accessToken = data.access_token;
|
|
30
|
+
this.#tokenExpireTime = Date.now() + (data.expires_in - 300) * 1000;
|
|
31
|
+
return data.access_token;
|
|
32
|
+
}
|
|
33
|
+
async request(path, init) {
|
|
34
|
+
if (this.tokenExpired)
|
|
35
|
+
await this.refreshAccessToken();
|
|
36
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
37
|
+
const response = await this.fetch(`https://api.weixin.qq.com${path}${separator}access_token=${this.#accessToken}`, init);
|
|
38
|
+
return response.data;
|
|
39
|
+
}
|
|
40
|
+
async sendCustomerService(messageData) {
|
|
41
|
+
let result = await this.request('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
|
|
42
|
+
if (result.errcode && TOKEN_INVALID_ERRCODES.has(Number(result.errcode))) {
|
|
43
|
+
await this.refreshAccessToken();
|
|
44
|
+
result = await this.request('/cgi-bin/message/custom/send', { method: 'POST', body: messageData });
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
async uploadMedia(type, body) {
|
|
49
|
+
const data = await this.request(`/cgi-bin/media/upload?type=${type}`, { method: 'POST', body });
|
|
50
|
+
if (data.media_id)
|
|
51
|
+
return data.media_id;
|
|
52
|
+
throw new Error(`WeChat media upload failed: ${data.errcode ?? 'unknown'} ${data.errmsg ?? ''}`.trim());
|
|
53
|
+
}
|
|
54
|
+
async getFollowerIds() {
|
|
55
|
+
const followers = [];
|
|
56
|
+
let nextOpenid = '';
|
|
57
|
+
do {
|
|
58
|
+
const data = await this.request(`/cgi-bin/user/get?next_openid=${encodeURIComponent(nextOpenid)}`);
|
|
59
|
+
if (data.errcode && data.errcode !== 0) {
|
|
60
|
+
throw new Error(`WeChat API error: ${data.errcode} - ${data.errmsg}`);
|
|
61
|
+
}
|
|
62
|
+
followers.push(...(data.data?.openid ?? []).filter(Boolean));
|
|
63
|
+
if (Number(data.count ?? 0) < 10_000)
|
|
64
|
+
break;
|
|
65
|
+
nextOpenid = data.next_openid ?? '';
|
|
66
|
+
} while (nextOpenid);
|
|
67
|
+
return Object.freeze(followers);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export const wechatMpClient = defineEndpointClient('wechat-mp');
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,73 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
import type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
private usesPassiveReply;
|
|
33
|
-
private extractSendText;
|
|
34
|
-
private collectPassiveReplyXml;
|
|
35
|
-
private handlePassiveReply;
|
|
36
|
-
private buildTextReply;
|
|
37
|
-
private refreshAccessToken;
|
|
38
|
-
private tokenRefreshTimer?;
|
|
39
|
-
private startTokenRefreshTimer;
|
|
40
|
-
getUserInfo(openid: string): Promise<any>;
|
|
41
|
-
/**
|
|
42
|
-
* 上传多媒体文件到微信服务器
|
|
43
|
-
* @param type 媒体类型:image(图片)、voice(语音)、video(视频)、thumb(缩略图)
|
|
44
|
-
* @param buffer 文件 Buffer
|
|
45
|
-
* @param filename 文件名(可选,用于确定文件类型)
|
|
46
|
-
* @returns 微信服务器返回的 media_id
|
|
47
|
-
*/
|
|
48
|
-
uploadMedia(type: 'image' | 'voice' | 'video' | 'thumb', buffer: Buffer, filename?: string): Promise<string>;
|
|
49
|
-
/**
|
|
50
|
-
* 获取文件扩展名
|
|
51
|
-
*/
|
|
52
|
-
private getFileExtension;
|
|
53
|
-
/**
|
|
54
|
-
* 获取 Content-Type
|
|
55
|
-
*/
|
|
56
|
-
private getContentType;
|
|
57
|
-
private getAESKey;
|
|
58
|
-
/** 微信安全模式加密 echostr 为较长 Base64;明文/兼容模式多为短字符串 */
|
|
59
|
-
private isEncryptedEchostr;
|
|
60
|
-
/**
|
|
61
|
-
* 解密安全模式 URL 验证中的 echostr
|
|
62
|
-
*/
|
|
63
|
-
private decryptEchostr;
|
|
64
|
-
/**
|
|
65
|
-
* 解密微信推送的加密消息
|
|
66
|
-
*/
|
|
67
|
-
private decryptMessage;
|
|
68
|
-
/**
|
|
69
|
-
* 加密被动回复消息
|
|
70
|
-
*/
|
|
71
|
-
private encryptMessage;
|
|
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> {
|
|
14
|
+
#private;
|
|
15
|
+
readonly client: WeChatMpClient;
|
|
16
|
+
readonly management: EndpointManagement;
|
|
17
|
+
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>;
|
|
72
32
|
}
|
|
73
|
-
//# sourceMappingURL=endpoint.d.ts.map
|