@zhin.js/adapter-wechat-mp 1.0.1 → 1.1.0

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/README.md +40 -88
  3. package/adapters/wechat-mp.js +32 -0
  4. package/adapters/wechat-mp.ts +38 -0
  5. package/commands/endpoint/add/[id].js +3 -0
  6. package/commands/endpoint/add/[id].ts +3 -0
  7. package/commands/endpoint/list.js +3 -0
  8. package/commands/endpoint/list.ts +3 -0
  9. package/commands/endpoint/remove/[id].js +3 -0
  10. package/commands/endpoint/remove/[id].ts +3 -0
  11. package/lib/client.d.ts +32 -0
  12. package/lib/client.js +70 -0
  13. package/lib/endpoint.d.ts +31 -72
  14. package/lib/endpoint.js +226 -747
  15. package/lib/index.d.ts +6 -15
  16. package/lib/index.js +6 -25
  17. package/lib/media-upload.d.ts +22 -0
  18. package/lib/media-upload.js +64 -0
  19. package/lib/passive-reply.d.ts +0 -1
  20. package/lib/passive-reply.js +0 -1
  21. package/lib/protocol.d.ts +129 -0
  22. package/lib/protocol.js +349 -0
  23. package/lib/side-event-dispatch.d.ts +4 -0
  24. package/lib/side-event-dispatch.js +38 -0
  25. package/lib/webhook.d.ts +20 -0
  26. package/lib/webhook.js +152 -0
  27. package/lib/wechat-mp-endpoint-commands.d.ts +1 -0
  28. package/lib/wechat-mp-endpoint-commands.js +19 -0
  29. package/lib/wechat-mp-runtime-state.d.ts +1 -0
  30. package/lib/wechat-mp-runtime-state.js +6 -0
  31. package/package.json +53 -13
  32. package/plugin.js +14 -0
  33. package/schema.json +116 -0
  34. package/src/client.ts +121 -0
  35. package/src/endpoint.ts +276 -902
  36. package/src/index.ts +56 -35
  37. package/src/media-upload.ts +82 -0
  38. package/src/protocol.ts +507 -0
  39. package/src/side-event-dispatch.ts +45 -0
  40. package/src/webhook.ts +237 -0
  41. package/src/wechat-mp-endpoint-commands.ts +20 -0
  42. package/src/wechat-mp-runtime-state.ts +7 -0
  43. package/lib/adapter.d.ts +0 -14
  44. package/lib/adapter.d.ts.map +0 -1
  45. package/lib/adapter.js +0 -17
  46. package/lib/adapter.js.map +0 -1
  47. package/lib/endpoint.d.ts.map +0 -1
  48. package/lib/endpoint.js.map +0 -1
  49. package/lib/index.d.ts.map +0 -1
  50. package/lib/index.js.map +0 -1
  51. package/lib/passive-reply.d.ts.map +0 -1
  52. package/lib/passive-reply.js.map +0 -1
  53. package/lib/types.d.ts +0 -58
  54. package/lib/types.d.ts.map +0 -1
  55. package/lib/types.js +0 -2
  56. package/lib/types.js.map +0 -1
  57. package/src/adapter.ts +0 -22
  58. package/src/types.ts +0 -60
  59. /package/{skills/wechat-mp/SKILL.md → agent/skills/wechat-mp.md} +0 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Convention entry: discover `adapters/wechat-mp.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 WeChatMpAdapterConfig,
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<WeChatMpAdapterConfig>({
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,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { wechatMpEndpointCommands } from "../../../lib/wechat-mp-endpoint-commands.js";
3
+ export default wechatMpEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { wechatMpEndpointCommands } from '../../../src/wechat-mp-endpoint-commands.js';
2
+
3
+ export default wechatMpEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { wechatMpEndpointCommands } from "../../lib/wechat-mp-endpoint-commands.js";
3
+ export default wechatMpEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { wechatMpEndpointCommands } from '../../src/wechat-mp-endpoint-commands.js';
2
+
3
+ export default wechatMpEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { wechatMpEndpointCommands } from "../../../lib/wechat-mp-endpoint-commands.js";
3
+ export default wechatMpEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { wechatMpEndpointCommands } from '../../../src/wechat-mp-endpoint-commands.js';
2
+
3
+ export default wechatMpEndpointCommands.remove;
@@ -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 { EventEmitter } from "events";
2
- import { Endpoint, Message, MessageSegment, SendOptions } from 'zhin.js';
3
- import { type Router } from "@zhin.js/host-router/router";
4
- import type { WeChatMPConfig, WeChatMessage } from "./types.js";
5
- import type { WeChatMPAdapter } from "./adapter.js";
6
- export declare class WeChatMPEndpoint extends EventEmitter implements Endpoint<WeChatMPConfig, WeChatMessage> {
7
- adapter: WeChatMPAdapter;
8
- $config: WeChatMPConfig;
9
- $connected: boolean;
10
- router: Router;
11
- private accessToken;
12
- private tokenExpireTime;
13
- get logger(): import("zhin.js").Logger;
14
- get $id(): string;
15
- constructor(adapter: WeChatMPAdapter, router: Router, config: WeChatMPConfig);
16
- private setupRoutes;
17
- $connect(): Promise<void>;
18
- $disconnect(): Promise<void>;
19
- private handleVerification;
20
- private handleMessage;
21
- private computeSignatureHash;
22
- private verifySignature;
23
- private parseXMLMessage;
24
- $formatMessage(wechatMsg: WeChatMessage): Message<WeChatMessage>;
25
- static parseMessageContent(wechatMsg: WeChatMessage): MessageSegment[];
26
- $sendMessage(options: SendOptions): Promise<string>;
27
- $recallMessage(id: string): Promise<void>;
28
- private sendCustomerServiceMessage;
29
- private formatSendContent;
30
- private getReplyMode;
31
- private getEncryptMode;
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