@zhin.js/adapter-wecom 3.0.0 → 3.0.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 +60 -0
- package/adapters/wecom.js +35 -0
- package/adapters/wecom.ts +14 -1
- package/commands/endpoint/add/[name:string].js +3 -0
- package/commands/endpoint/add/[name:string].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[name:string].js +3 -0
- package/commands/endpoint/remove/[name:string].ts +3 -0
- package/lib/endpoint.d.ts +6 -1
- package/lib/endpoint.js +52 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/media-upload.d.ts +22 -0
- package/lib/media-upload.js +59 -0
- package/lib/protocol.js +8 -3
- package/lib/wecom-endpoint-commands.d.ts +1 -0
- package/lib/wecom-endpoint-commands.js +19 -0
- package/lib/wecom-runtime-state.d.ts +1 -0
- package/lib/wecom-runtime-state.js +6 -0
- package/package.json +21 -15
- package/plugin.js +16 -0
- package/src/endpoint.ts +54 -2
- package/src/index.ts +8 -0
- package/src/media-upload.ts +77 -0
- package/src/protocol.ts +7 -3
- package/src/wecom-endpoint-commands.ts +20 -0
- package/src/wecom-runtime-state.ts +7 -0
- package/plugin.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# @zhin.js/adapter-wecom
|
|
2
2
|
|
|
3
|
+
## 3.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d5cd4aa: Publish Plugin Runtime entry points and convention modules as JavaScript so
|
|
8
|
+
installed npm plugins load on Node without TypeScript stripping. Workspace
|
|
9
|
+
development continues to prefer TypeScript sources for local HMR.
|
|
10
|
+
|
|
11
|
+
Remove the unconsumed legacy game hub APIs from game-kit; game navigation and
|
|
12
|
+
records now use ordinary convention commands owned by the game hub plugin.
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [d5cd4aa]
|
|
15
|
+
- @zhin.js/command@1.0.4
|
|
16
|
+
- zhin.js@5.0.2
|
|
17
|
+
- @zhin.js/adapter@1.1.2
|
|
18
|
+
- @zhin.js/core@1.4.2
|
|
19
|
+
- @zhin.js/agent@1.0.7
|
|
20
|
+
|
|
21
|
+
## 3.0.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- cdf64e7: 多方向审计修复批(8 面 30+ bug):
|
|
26
|
+
|
|
27
|
+
- **安全**:钉钉 webhook 验签绕过修复(缺 timestamp/sign 一律 403 + ±1h 防重放);exec-policy fail-closed(换行/`$(`/反引号拒绝、管道逐段过白名单、env dump 复合拆段);wecom 验签改 timingSafeEqual;config:set 拒绝 `__proto__` 等魔术键、host 键优先防覆写。
|
|
28
|
+
- **P0 功能**:`zhin packages` scoped 包名解析为空导致 rm -rf 风险;命令数字参数解析失败炸断消息链路(dispatch 捕获 continue);TaskQueue 监听器首个事件自摘除导致 assistant 队列挂死(+超时后完成不覆盖终态);DatabaseHost 跨世代共享崩溃(define 幂等 + stop 改进程级);rss 按不存在 id 列删除改业务键。
|
|
29
|
+
- **Runtime/HMR**:native watcher 过滤忽略目录(lib/.zhin 不再触发重载风暴);host 段配置 patch(http.port 等)存在 installResources 时全量重建;capability 目录非 entry 支持文件升级进程重启;documentTransaction 失败回滚。
|
|
30
|
+
- **CLI 写侧**:config/setup 对 toml 静默假成功改统一 config-file(不支持格式报错);doctor/onboard 默认配置改新形态 plugins 映射;schedule add 改 --prompt;migrate engines/中文模板误伤/覆盖前备份。
|
|
31
|
+
- **适配器生命周期**:napcat/milky start 失败竞态与僵尸连接、心跳 close 清理、stop-during-connect settle;slack webhook 二次 writeHead + messageChannelMap LRU。
|
|
32
|
+
- **Host/向导**:logs/stats 与 inbox 查询改 DB 侧 count/orderBy/limit 下推;save-yaml 写入前校验;zhipu/moonshot baseUrl 必填预填;.env 写入转义 + 幂等合并;setup 数据库密码不再明文落 config;60s fetch 超时与 JSON 守卫。
|
|
33
|
+
|
|
34
|
+
- 74b035c: endpoint 管理命令扩展至 18/20 适配器:kook / discord / github(private_key 支持内联文件路径)/ icqq(bindFlow 登记式 add + `icqq login` 引导)/ dingtalk / lark / line / satori / wechat-mp / wecom / weixin-ilink 接入 `endpoint list/add/remove`(字段对齐各自 schema,凭据写 `.env`)。email(smtp/imap 嵌套对象)与 sandbox(无凭据)暂不接。
|
|
35
|
+
- 6cb6152: 统一消息元素通道(UNI-Channel)落地:
|
|
36
|
+
|
|
37
|
+
- **入站契约**:`IncomingMessage.segments`(canonical Segment[],与 content 纯文本视图同源双轨),Message 透传;AI 兜底链路经 `collectSegmentMedia` 把图片/语音/视频/文件 MediaRef 写入会话 extra——多模态输入不再丢失。
|
|
38
|
+
- **出站协商**:`normalizeOutboundPayload` 升级全量 canonical 归一(复用 generic-segment-mapper),html→image 按端点 `segments.outboundMedia` 声明降级(base64 直发 / url-or-text / passthrough 自行物化);`MediaRef.kind` 新增 `'file'` 承载平台不透明引用(file_id/resource_id)。
|
|
39
|
+
- **能力声明**:`defineAdapter.segments` policy(outboundMedia / interactive),三道段门禁复活(探测点改 adapters/\*.ts,豁免名单渐进收敛)。
|
|
40
|
+
- **首批迁移**:icqq 全保真出入站(CQ ↔ canonical,quote→reply 段);milky/telegram/discord 入站媒体段恢复(附件/贴纸/callback action);napcat/onebot11/onebot12 出站 canonical→OneBot 数组段;wechat-mp/wecom `/cgi-bin/media/upload` 与 lark `/im/v1/images` 上传通路(base64/URL 图片不再静默丢图,失败降级文本)。
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [cdf64e7]
|
|
43
|
+
- Updated dependencies [2d0a159]
|
|
44
|
+
- Updated dependencies [5691aba]
|
|
45
|
+
- Updated dependencies [078e3f7]
|
|
46
|
+
- Updated dependencies [50497a5]
|
|
47
|
+
- Updated dependencies [9c997b2]
|
|
48
|
+
- Updated dependencies [09d4f25]
|
|
49
|
+
- Updated dependencies [43485a9]
|
|
50
|
+
- Updated dependencies [f0ec5ab]
|
|
51
|
+
- Updated dependencies [3e925d0]
|
|
52
|
+
- Updated dependencies [fa66c4c]
|
|
53
|
+
- Updated dependencies [fa66c4c]
|
|
54
|
+
- Updated dependencies [6cb6152]
|
|
55
|
+
- @zhin.js/command@1.0.3
|
|
56
|
+
- @zhin.js/agent@1.0.6
|
|
57
|
+
- @zhin.js/plugin-runtime@1.1.1
|
|
58
|
+
- @zhin.js/host-http@1.0.3
|
|
59
|
+
- zhin.js@5.0.1
|
|
60
|
+
- @zhin.js/adapter@1.1.1
|
|
61
|
+
- @zhin.js/core@1.4.1
|
|
62
|
+
|
|
3
63
|
## 3.0.0
|
|
4
64
|
|
|
5
65
|
### Patch Changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* Convention entry: discover `adapters/wecom.ts` → defineAdapter.
|
|
4
|
+
* Implementation lives under `src/` (endpoint / webhook / protocol).
|
|
5
|
+
*/
|
|
6
|
+
import { defineAdapter } from '@zhin.js/adapter';
|
|
7
|
+
import { messageGatewayToken } from '@zhin.js/core/runtime';
|
|
8
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
9
|
+
import { WecomEndpoint } from "../lib/endpoint.js";
|
|
10
|
+
import { resolveWecomConfig, } from "../lib/protocol.js";
|
|
11
|
+
import { wecomRuntimeStateToken } from "../lib/wecom-runtime-state.js";
|
|
12
|
+
export { WecomEndpoint } from "../lib/endpoint.js";
|
|
13
|
+
export default defineAdapter({
|
|
14
|
+
capabilities: ['inbound', 'outbound'],
|
|
15
|
+
// image 段经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传);
|
|
16
|
+
// 无卡片交互面,交互段降级纯文本。
|
|
17
|
+
segments: {
|
|
18
|
+
outboundMedia: ['url', 'upload'],
|
|
19
|
+
interactive: 'text',
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const config = resolveWecomConfig(context.config);
|
|
23
|
+
// 注册到插件运行时状态(wecom endpoint list 的"运行中"数据源)
|
|
24
|
+
context.use(wecomRuntimeStateToken).endpoints.set(config.name, {
|
|
25
|
+
name: config.name,
|
|
26
|
+
mode: 'webhook',
|
|
27
|
+
});
|
|
28
|
+
return new WecomEndpoint({
|
|
29
|
+
id: context.id,
|
|
30
|
+
gateway: context.use(messageGatewayToken),
|
|
31
|
+
http: context.use(httpHostToken),
|
|
32
|
+
config,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
package/adapters/wecom.ts
CHANGED
|
@@ -10,18 +10,31 @@ import {
|
|
|
10
10
|
resolveWecomConfig,
|
|
11
11
|
type WecomAdapterConfig,
|
|
12
12
|
} from '../src/protocol.js';
|
|
13
|
+
import { wecomRuntimeStateToken } from '../src/wecom-runtime-state.js';
|
|
13
14
|
|
|
14
15
|
export { WecomEndpoint } from '../src/endpoint.js';
|
|
15
16
|
export type { WecomEndpointOptions, WecomFetch } from '../src/endpoint.js';
|
|
16
17
|
|
|
17
18
|
export default defineAdapter<WecomAdapterConfig>({
|
|
18
19
|
capabilities: ['inbound', 'outbound'],
|
|
20
|
+
// image 段经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传);
|
|
21
|
+
// 无卡片交互面,交互段降级纯文本。
|
|
22
|
+
segments: {
|
|
23
|
+
outboundMedia: ['url', 'upload'],
|
|
24
|
+
interactive: 'text',
|
|
25
|
+
},
|
|
19
26
|
create(context) {
|
|
27
|
+
const config = resolveWecomConfig(context.config);
|
|
28
|
+
// 注册到插件运行时状态(wecom endpoint list 的"运行中"数据源)
|
|
29
|
+
context.use(wecomRuntimeStateToken).endpoints.set(config.name, {
|
|
30
|
+
name: config.name,
|
|
31
|
+
mode: 'webhook',
|
|
32
|
+
});
|
|
20
33
|
return new WecomEndpoint({
|
|
21
34
|
id: context.id,
|
|
22
35
|
gateway: context.use(messageGatewayToken),
|
|
23
36
|
http: context.use(httpHostToken),
|
|
24
|
-
config
|
|
37
|
+
config,
|
|
25
38
|
});
|
|
26
39
|
},
|
|
27
40
|
});
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ResolvedWecomConfig, type WecomApiResponse, type WecomMessage } fr
|
|
|
9
9
|
export type WecomFetch = (url: string, init?: {
|
|
10
10
|
readonly method?: string;
|
|
11
11
|
readonly headers?: Record<string, string>;
|
|
12
|
-
readonly body?: string;
|
|
12
|
+
readonly body?: string | FormData;
|
|
13
13
|
}) => Promise<{
|
|
14
14
|
readonly ok: boolean;
|
|
15
15
|
readonly status: number;
|
|
@@ -23,6 +23,11 @@ export interface WecomEndpointOptions {
|
|
|
23
23
|
readonly config: ResolvedWecomConfig;
|
|
24
24
|
readonly fetch?: WecomFetch;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 企业微信服务端 API 无 bot 群列表/群成员列表接口(客户群接口属「客户联系」
|
|
28
|
+
* 独立授权域,非 bot 社交面),好友/频道概念亦不存在;
|
|
29
|
+
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
30
|
+
*/
|
|
26
31
|
export declare class WecomEndpoint implements EndpointInstance {
|
|
27
32
|
#private;
|
|
28
33
|
constructor(options: WecomEndpointOptions);
|
package/lib/endpoint.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
2
|
import { registerWecomAgentEndpoint } from './wecom-agent-deps.js';
|
|
3
|
+
import { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
|
|
3
4
|
import { buildSendRequestBody, formatInboundContent, formatOutboundBody, resolveChatType, } from './protocol.js';
|
|
4
5
|
import { registerWecomWebhookRoutes } from './webhook.js';
|
|
5
6
|
const logger = getLogger('wecom');
|
|
7
|
+
/**
|
|
8
|
+
* 企业微信服务端 API 无 bot 群列表/群成员列表接口(客户群接口属「客户联系」
|
|
9
|
+
* 独立授权域,非 bot 社交面),好友/频道概念亦不存在;
|
|
10
|
+
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
11
|
+
*/
|
|
6
12
|
export class WecomEndpoint {
|
|
7
13
|
#options;
|
|
8
14
|
#fetch;
|
|
@@ -59,7 +65,8 @@ export class WecomEndpoint {
|
|
|
59
65
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
60
66
|
}
|
|
61
67
|
async send({ target, payload }) {
|
|
62
|
-
const
|
|
68
|
+
const materialized = await this.#materializeOutboundMedia(payload);
|
|
69
|
+
const content = formatOutboundBody(materialized);
|
|
63
70
|
const body = buildSendRequestBody(target, content,
|
|
64
71
|
// Legacy field: historically written into agentid (preserved for cutover).
|
|
65
72
|
this.#options.config.agentSecret);
|
|
@@ -73,6 +80,50 @@ export class WecomEndpoint {
|
|
|
73
80
|
logger.debug(formatCompact({ op: 'send', endpoint: this.#options.config.name, to: target }));
|
|
74
81
|
return data.msgid || `${Date.now()}`;
|
|
75
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* message/send 的 image 段只接受 media_id:canonical MediaRef(base64/本地路径/URL)
|
|
85
|
+
* 先经 /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
|
|
86
|
+
*/
|
|
87
|
+
async #materializeOutboundMedia(payload) {
|
|
88
|
+
if (!Array.isArray(payload))
|
|
89
|
+
return payload;
|
|
90
|
+
return Promise.all(payload.map(async (item) => {
|
|
91
|
+
if (typeof item === 'string' || !item || typeof item !== 'object')
|
|
92
|
+
return item;
|
|
93
|
+
const seg = item;
|
|
94
|
+
if (seg.type !== 'image')
|
|
95
|
+
return item;
|
|
96
|
+
const data = seg.data ?? {};
|
|
97
|
+
const media = readOutboundImageMedia(data);
|
|
98
|
+
if (!media)
|
|
99
|
+
return item;
|
|
100
|
+
try {
|
|
101
|
+
const mediaId = await this.#uploadMedia('image', media);
|
|
102
|
+
return { type: 'image', data: { media_id: mediaId } };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.warn(formatCompact({
|
|
106
|
+
op: 'wecom_media_upload_failed',
|
|
107
|
+
endpoint: this.#options.config.name,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
}));
|
|
110
|
+
const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
|
|
111
|
+
return { type: 'text', data: { text: alt } };
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
|
|
116
|
+
async #uploadMedia(type, media) {
|
|
117
|
+
await this.#ensureAccessToken();
|
|
118
|
+
const binary = await resolveMediaBinary(media);
|
|
119
|
+
const form = buildMediaUploadForm(binary);
|
|
120
|
+
const url = `${this.#options.config.apiBaseUrl}/cgi-bin/media/upload?access_token=${this.#accessToken.access_token}&type=${type}`;
|
|
121
|
+
const response = await this.#fetch(url, { method: 'POST', body: form });
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
if (data.errcode === 0 && data.media_id)
|
|
124
|
+
return data.media_id;
|
|
125
|
+
throw new Error(`WeCom media upload failed: ${data.errmsg ?? 'unknown'} (${data.errcode ?? '?'})`);
|
|
126
|
+
}
|
|
76
127
|
/** Test / internal: admit a parsed message when open (non-webhook path). */
|
|
77
128
|
admit(msg) {
|
|
78
129
|
if (!this.#open)
|
package/lib/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { buildSendRequestBody, decryptMessage, extractEncryptFromXml, formatInbo
|
|
|
2
2
|
export { getWecomAgentDeps, registerWecomAgentEndpoint, setWecomAgentDeps, type WecomAgentDeps, type WecomAgentEndpoint, } from './wecom-agent-deps.js';
|
|
3
3
|
export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, registerWecomPlatformPermitChecker, wecomGroupPermitResolver, } from './platform-permit.js';
|
|
4
4
|
export { WecomEndpoint, type WecomEndpointOptions, type WecomFetch, } from './endpoint.js';
|
|
5
|
+
export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, type WecomMediaUploadResult, } from './media-upload.js';
|
|
5
6
|
export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, type WecomWebhookHandler, } from './webhook.js';
|
package/lib/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { buildSendRequestBody, decryptMessage, extractEncryptFromXml, formatInbo
|
|
|
2
2
|
export { getWecomAgentDeps, registerWecomAgentEndpoint, setWecomAgentDeps, } from './wecom-agent-deps.js';
|
|
3
3
|
export { checkWecomPlatformPermit, normalizeWecomSenderForPermit, platformPermit, registerWecomPlatformPermitChecker, wecomGroupPermitResolver, } from './platform-permit.js';
|
|
4
4
|
export { WecomEndpoint, } from './endpoint.js';
|
|
5
|
+
export { buildMediaUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
|
|
5
6
|
export { registerWecomWebhookRoutes, handleWecomVerificationRequest, handleWecomWebhookRequest, } from './webhook.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type MediaRef } from '@zhin.js/core';
|
|
2
|
+
export interface MediaBinary {
|
|
3
|
+
readonly data: Buffer;
|
|
4
|
+
readonly mimeType: string;
|
|
5
|
+
readonly fileName: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WecomMediaUploadResult {
|
|
8
|
+
readonly errcode?: number;
|
|
9
|
+
readonly errmsg?: string;
|
|
10
|
+
readonly type?: string;
|
|
11
|
+
readonly media_id?: string;
|
|
12
|
+
readonly created_at?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
|
|
15
|
+
export declare function resolveMediaBinary(media: MediaRef, download?: (url: string) => Promise<Buffer>): Promise<MediaBinary>;
|
|
16
|
+
/** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
|
|
17
|
+
export declare function buildMediaUploadForm(binary: MediaBinary): FormData;
|
|
18
|
+
/**
|
|
19
|
+
* 出站 image 段的媒体引用:已有 media_id 的视为已物化;
|
|
20
|
+
* 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
|
|
21
|
+
*/
|
|
22
|
+
export declare function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 临时素材上传(message/send 的 image 段需要 media_id)。
|
|
3
|
+
* 接口:POST {apiBaseUrl}/cgi-bin/media/upload?access_token=…&type=image
|
|
4
|
+
* 与 message/send 同属应用消息接口域(同一 access_token),不引入额外授权域。
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { isMediaRef, mediaRefFromLegacyData } from '@zhin.js/core';
|
|
9
|
+
const MIME_EXT = {
|
|
10
|
+
'image/jpeg': 'jpg',
|
|
11
|
+
'image/png': 'png',
|
|
12
|
+
'image/gif': 'gif',
|
|
13
|
+
'image/webp': 'webp',
|
|
14
|
+
'image/bmp': 'bmp',
|
|
15
|
+
};
|
|
16
|
+
/** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
|
|
17
|
+
export async function resolveMediaBinary(media, download = defaultDownload) {
|
|
18
|
+
const mimeType = media.mime_type ?? 'image/png';
|
|
19
|
+
const ext = MIME_EXT[mimeType] ?? 'png';
|
|
20
|
+
if (media.kind === 'base64') {
|
|
21
|
+
const value = media.value.startsWith('base64://')
|
|
22
|
+
? media.value.slice('base64://'.length)
|
|
23
|
+
: media.value;
|
|
24
|
+
return { data: Buffer.from(value, 'base64'), mimeType, fileName: `image.${ext}` };
|
|
25
|
+
}
|
|
26
|
+
if (media.kind === 'path') {
|
|
27
|
+
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
28
|
+
return { data: await readFile(path), mimeType, fileName: basename(path) };
|
|
29
|
+
}
|
|
30
|
+
return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
|
|
31
|
+
}
|
|
32
|
+
async function defaultDownload(url) {
|
|
33
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new Error(`download failed: HTTP ${response.status}`);
|
|
36
|
+
return Buffer.from(await response.arrayBuffer());
|
|
37
|
+
}
|
|
38
|
+
/** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
|
|
39
|
+
export function buildMediaUploadForm(binary) {
|
|
40
|
+
// Buffer 的 ArrayBufferLike 不满足 BlobPart(SharedArrayBuffer 分支),拷贝为 Uint8Array<ArrayBuffer>
|
|
41
|
+
const bytes = new Uint8Array(binary.data.byteLength);
|
|
42
|
+
bytes.set(binary.data);
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.append('media', new Blob([bytes], { type: binary.mimeType }), binary.fileName);
|
|
45
|
+
return form;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 出站 image 段的媒体引用:已有 media_id 的视为已物化;
|
|
49
|
+
* 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
|
|
50
|
+
*/
|
|
51
|
+
export function readOutboundImageMedia(data) {
|
|
52
|
+
if (typeof data.media_id === 'string' && data.media_id)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (typeof data.mediaId === 'string' && data.mediaId)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (isMediaRef(data.media))
|
|
57
|
+
return data.media;
|
|
58
|
+
return mediaRefFromLegacyData(data);
|
|
59
|
+
}
|
package/lib/protocol.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* WeCom (企业微信) protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
*/
|
|
5
|
-
import { createHash, createDecipheriv } from 'node:crypto';
|
|
5
|
+
import { createHash, createDecipheriv, timingSafeEqual } from 'node:crypto';
|
|
6
6
|
export function resolveWecomConfig(config = {}) {
|
|
7
7
|
const entry = config.endpoints?.find((item) => item.context === 'wecom');
|
|
8
8
|
const corpId = config.corpId ?? entry?.corpId ?? process.env.WECOM_CORP_ID;
|
|
@@ -54,7 +54,11 @@ export function verifySignature(token, timestamp, nonce, encrypt, signature) {
|
|
|
54
54
|
const hash = createHash('sha1')
|
|
55
55
|
.update([token, timestamp, nonce, encrypt].sort().join(''))
|
|
56
56
|
.digest('hex');
|
|
57
|
-
|
|
57
|
+
const a = Buffer.from(hash);
|
|
58
|
+
const b = Buffer.from(signature);
|
|
59
|
+
if (a.length !== b.length)
|
|
60
|
+
return false;
|
|
61
|
+
return timingSafeEqual(a, b);
|
|
58
62
|
}
|
|
59
63
|
catch {
|
|
60
64
|
return false;
|
|
@@ -188,7 +192,8 @@ export function formatOutboundBody(payload) {
|
|
|
188
192
|
if (!hasMedia) {
|
|
189
193
|
hasMedia = true;
|
|
190
194
|
mediaType = 'image';
|
|
191
|
-
|
|
195
|
+
// media_id 由 endpoint 上传物化写入;file/url 为旧调用方直传 media_id 的兼容入口。
|
|
196
|
+
mediaData = { media_id: data.media_id ?? data.mediaId ?? data.file ?? data.url };
|
|
192
197
|
}
|
|
193
198
|
break;
|
|
194
199
|
case 'markdown':
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const wecomEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wecom endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*/
|
|
5
|
+
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
6
|
+
import { defineCommand } from '@zhin.js/command';
|
|
7
|
+
import { wecomRuntimeStateToken } from './wecom-runtime-state.js';
|
|
8
|
+
export const wecomEndpointCommands = createEndpointCommands({
|
|
9
|
+
adapterKey: 'wecom',
|
|
10
|
+
adapterDisplayName: 'WeCom',
|
|
11
|
+
fields: [
|
|
12
|
+
{ key: 'corpId', required: true, env: true, description: '企业 corpId' },
|
|
13
|
+
{ key: 'agentSecret', required: true, env: true, description: '应用 secret' },
|
|
14
|
+
{ key: 'token', required: true, env: true, description: '回调 token' },
|
|
15
|
+
{ key: 'encodingAESKey', required: true, env: true, description: '回调加解密 key' },
|
|
16
|
+
],
|
|
17
|
+
running: (use) => use(wecomRuntimeStateToken).endpoints.values(),
|
|
18
|
+
describeEntry: (entry) => `corpId: ${String(entry.corpId)}`,
|
|
19
|
+
}, defineCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const wecomRuntimeStateToken: import("@zhin.js/plugin-runtime").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `wecom endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
export const wecomRuntimeStateToken = defineEndpointRuntimeStateToken('wecom');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-wecom",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Zhin.js WeCom (企业微信) adapter for Plugin Runtime (HTTP webhook)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -34,20 +34,21 @@
|
|
|
34
34
|
"directory": "plugins/adapters/wecom"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@zhin.js/adapter": "1.1.
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
37
|
+
"@zhin.js/adapter": "1.1.2",
|
|
38
|
+
"@zhin.js/command": "1.0.4",
|
|
39
|
+
"@zhin.js/core": "1.4.2",
|
|
40
|
+
"@zhin.js/host-http": "1.0.3",
|
|
40
41
|
"@zhin.js/logger": "1.0.75",
|
|
41
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
42
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"zod": "^4.0.0",
|
|
45
|
-
"@zhin.js/adapter": "1.1.
|
|
46
|
-
"@zhin.js/agent": "1.0.
|
|
47
|
-
"@zhin.js/core": "1.4.
|
|
48
|
-
"@zhin.js/host-http": "1.0.
|
|
49
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
50
|
-
"zhin.js": "5.0.
|
|
46
|
+
"@zhin.js/adapter": "1.1.2",
|
|
47
|
+
"@zhin.js/agent": "1.0.7",
|
|
48
|
+
"@zhin.js/core": "1.4.2",
|
|
49
|
+
"@zhin.js/host-http": "1.0.3",
|
|
50
|
+
"@zhin.js/plugin-runtime": "1.1.1",
|
|
51
|
+
"zhin.js": "5.0.2"
|
|
51
52
|
},
|
|
52
53
|
"peerDependenciesMeta": {
|
|
53
54
|
"zhin.js": {
|
|
@@ -65,12 +66,13 @@
|
|
|
65
66
|
"typescript": "^6.0.3",
|
|
66
67
|
"vitest": "^4.1.10",
|
|
67
68
|
"zod": "^4.4.3",
|
|
68
|
-
"@zhin.js/agent": "1.0.
|
|
69
|
-
"zhin.js": "5.0.
|
|
69
|
+
"@zhin.js/agent": "1.0.7",
|
|
70
|
+
"zhin.js": "5.0.2"
|
|
70
71
|
},
|
|
71
72
|
"files": [
|
|
72
73
|
"adapters",
|
|
73
|
-
"
|
|
74
|
+
"commands",
|
|
75
|
+
"plugin.js",
|
|
74
76
|
"schema.json",
|
|
75
77
|
"src",
|
|
76
78
|
"lib",
|
|
@@ -88,13 +90,17 @@
|
|
|
88
90
|
"zhin": {
|
|
89
91
|
"protocol": 1,
|
|
90
92
|
"type": "plugin",
|
|
91
|
-
"entry": "./plugin.
|
|
93
|
+
"entry": "./plugin.js",
|
|
92
94
|
"engine": "^1.0.0",
|
|
93
95
|
"runtime": "trusted",
|
|
94
96
|
"features": [
|
|
95
97
|
{
|
|
96
98
|
"package": "@zhin.js/adapter",
|
|
97
99
|
"api": "^1.0.0"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"package": "@zhin.js/command",
|
|
103
|
+
"api": "^1.0.0"
|
|
98
104
|
}
|
|
99
105
|
],
|
|
100
106
|
"plugins": []
|
package/plugin.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createEndpointRuntimeState } from '@zhin.js/adapter';
|
|
3
|
+
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { registerWecomPlatformPermitChecker } from "./lib/platform-permit.js";
|
|
5
|
+
import { wecomRuntimeStateToken } from "./lib/wecom-runtime-state.js";
|
|
6
|
+
export default definePlugin({
|
|
7
|
+
name: 'wecom',
|
|
8
|
+
metadata: {
|
|
9
|
+
displayName: 'WeCom (企业微信) Adapter',
|
|
10
|
+
},
|
|
11
|
+
setup(context) {
|
|
12
|
+
// 运行中 endpoint 注册表(wecom endpoint list 的"运行中"数据源)
|
|
13
|
+
context.resources.provide(wecomRuntimeStateToken, createEndpointRuntimeState());
|
|
14
|
+
return registerWecomPlatformPermitChecker();
|
|
15
|
+
},
|
|
16
|
+
});
|
package/src/endpoint.ts
CHANGED
|
@@ -7,6 +7,12 @@ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
|
7
7
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
8
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
9
9
|
import { registerWecomAgentEndpoint } from './wecom-agent-deps.js';
|
|
10
|
+
import {
|
|
11
|
+
buildMediaUploadForm,
|
|
12
|
+
readOutboundImageMedia,
|
|
13
|
+
resolveMediaBinary,
|
|
14
|
+
type WecomMediaUploadResult,
|
|
15
|
+
} from './media-upload.js';
|
|
10
16
|
import {
|
|
11
17
|
buildSendRequestBody,
|
|
12
18
|
formatInboundContent,
|
|
@@ -26,7 +32,7 @@ export type WecomFetch = (
|
|
|
26
32
|
init?: {
|
|
27
33
|
readonly method?: string;
|
|
28
34
|
readonly headers?: Record<string, string>;
|
|
29
|
-
readonly body?: string;
|
|
35
|
+
readonly body?: string | FormData;
|
|
30
36
|
},
|
|
31
37
|
) => Promise<{
|
|
32
38
|
readonly ok: boolean;
|
|
@@ -43,6 +49,11 @@ export interface WecomEndpointOptions {
|
|
|
43
49
|
readonly fetch?: WecomFetch;
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 企业微信服务端 API 无 bot 群列表/群成员列表接口(客户群接口属「客户联系」
|
|
54
|
+
* 独立授权域,非 bot 社交面),好友/频道概念亦不存在;
|
|
55
|
+
* 因此本 endpoint 不暴露 EndpointManagement(Console 社交面 RPC 对该平台保持未接线)。
|
|
56
|
+
*/
|
|
46
57
|
export class WecomEndpoint implements EndpointInstance {
|
|
47
58
|
readonly #options: WecomEndpointOptions;
|
|
48
59
|
readonly #fetch: WecomFetch;
|
|
@@ -104,7 +115,8 @@ export class WecomEndpoint implements EndpointInstance {
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
107
|
-
const
|
|
118
|
+
const materialized = await this.#materializeOutboundMedia(payload);
|
|
119
|
+
const content = formatOutboundBody(materialized);
|
|
108
120
|
const body = buildSendRequestBody(
|
|
109
121
|
target,
|
|
110
122
|
content,
|
|
@@ -122,6 +134,46 @@ export class WecomEndpoint implements EndpointInstance {
|
|
|
122
134
|
return (data.msgid as string) || `${Date.now()}`;
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
/**
|
|
138
|
+
* message/send 的 image 段只接受 media_id:canonical MediaRef(base64/本地路径/URL)
|
|
139
|
+
* 先经 /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
|
|
140
|
+
*/
|
|
141
|
+
async #materializeOutboundMedia(payload: unknown): Promise<unknown> {
|
|
142
|
+
if (!Array.isArray(payload)) return payload;
|
|
143
|
+
return Promise.all(payload.map(async (item) => {
|
|
144
|
+
if (typeof item === 'string' || !item || typeof item !== 'object') return item;
|
|
145
|
+
const seg = item as { type?: unknown; data?: Record<string, unknown> };
|
|
146
|
+
if (seg.type !== 'image') return item;
|
|
147
|
+
const data = seg.data ?? {};
|
|
148
|
+
const media = readOutboundImageMedia(data);
|
|
149
|
+
if (!media) return item;
|
|
150
|
+
try {
|
|
151
|
+
const mediaId = await this.#uploadMedia('image', media);
|
|
152
|
+
return { type: 'image', data: { media_id: mediaId } };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.warn(formatCompact({
|
|
155
|
+
op: 'wecom_media_upload_failed',
|
|
156
|
+
endpoint: this.#options.config.name,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
}));
|
|
159
|
+
const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
|
|
160
|
+
return { type: 'text', data: { text: alt } };
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
|
|
166
|
+
async #uploadMedia(type: 'image', media: Parameters<typeof resolveMediaBinary>[0]): Promise<string> {
|
|
167
|
+
await this.#ensureAccessToken();
|
|
168
|
+
const binary = await resolveMediaBinary(media);
|
|
169
|
+
const form = buildMediaUploadForm(binary);
|
|
170
|
+
const url = `${this.#options.config.apiBaseUrl}/cgi-bin/media/upload?access_token=${this.#accessToken.access_token}&type=${type}`;
|
|
171
|
+
const response = await this.#fetch(url, { method: 'POST', body: form });
|
|
172
|
+
const data = await response.json() as WecomMediaUploadResult;
|
|
173
|
+
if (data.errcode === 0 && data.media_id) return data.media_id;
|
|
174
|
+
throw new Error(`WeCom media upload failed: ${data.errmsg ?? 'unknown'} (${data.errcode ?? '?'})`);
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
/** Test / internal: admit a parsed message when open (non-webhook path). */
|
|
126
178
|
admit(msg: WecomMessage): void {
|
|
127
179
|
if (!this.#open) return;
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,14 @@ export {
|
|
|
44
44
|
type WecomFetch,
|
|
45
45
|
} from './endpoint.js';
|
|
46
46
|
|
|
47
|
+
export {
|
|
48
|
+
buildMediaUploadForm,
|
|
49
|
+
readOutboundImageMedia,
|
|
50
|
+
resolveMediaBinary,
|
|
51
|
+
type MediaBinary,
|
|
52
|
+
type WecomMediaUploadResult,
|
|
53
|
+
} from './media-upload.js';
|
|
54
|
+
|
|
47
55
|
export {
|
|
48
56
|
registerWecomWebhookRoutes,
|
|
49
57
|
handleWecomVerificationRequest,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 临时素材上传(message/send 的 image 段需要 media_id)。
|
|
3
|
+
* 接口:POST {apiBaseUrl}/cgi-bin/media/upload?access_token=…&type=image
|
|
4
|
+
* 与 message/send 同属应用消息接口域(同一 access_token),不引入额外授权域。
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { isMediaRef, mediaRefFromLegacyData, type MediaRef } from '@zhin.js/core';
|
|
9
|
+
|
|
10
|
+
export interface MediaBinary {
|
|
11
|
+
readonly data: Buffer;
|
|
12
|
+
readonly mimeType: string;
|
|
13
|
+
readonly fileName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WecomMediaUploadResult {
|
|
17
|
+
readonly errcode?: number;
|
|
18
|
+
readonly errmsg?: string;
|
|
19
|
+
readonly type?: string;
|
|
20
|
+
readonly media_id?: string;
|
|
21
|
+
readonly created_at?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MIME_EXT: Record<string, string> = {
|
|
25
|
+
'image/jpeg': 'jpg',
|
|
26
|
+
'image/png': 'png',
|
|
27
|
+
'image/gif': 'gif',
|
|
28
|
+
'image/webp': 'webp',
|
|
29
|
+
'image/bmp': 'bmp',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
|
|
33
|
+
export async function resolveMediaBinary(
|
|
34
|
+
media: MediaRef,
|
|
35
|
+
download: (url: string) => Promise<Buffer> = defaultDownload,
|
|
36
|
+
): Promise<MediaBinary> {
|
|
37
|
+
const mimeType = media.mime_type ?? 'image/png';
|
|
38
|
+
const ext = MIME_EXT[mimeType] ?? 'png';
|
|
39
|
+
if (media.kind === 'base64') {
|
|
40
|
+
const value = media.value.startsWith('base64://')
|
|
41
|
+
? media.value.slice('base64://'.length)
|
|
42
|
+
: media.value;
|
|
43
|
+
return { data: Buffer.from(value, 'base64'), mimeType, fileName: `image.${ext}` };
|
|
44
|
+
}
|
|
45
|
+
if (media.kind === 'path') {
|
|
46
|
+
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
47
|
+
return { data: await readFile(path), mimeType, fileName: basename(path) };
|
|
48
|
+
}
|
|
49
|
+
return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function defaultDownload(url: string): Promise<Buffer> {
|
|
53
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
54
|
+
if (!response.ok) throw new Error(`download failed: HTTP ${response.status}`);
|
|
55
|
+
return Buffer.from(await response.arrayBuffer());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
|
|
59
|
+
export function buildMediaUploadForm(binary: MediaBinary): FormData {
|
|
60
|
+
// Buffer 的 ArrayBufferLike 不满足 BlobPart(SharedArrayBuffer 分支),拷贝为 Uint8Array<ArrayBuffer>
|
|
61
|
+
const bytes = new Uint8Array(binary.data.byteLength);
|
|
62
|
+
bytes.set(binary.data);
|
|
63
|
+
const form = new FormData();
|
|
64
|
+
form.append('media', new Blob([bytes], { type: binary.mimeType }), binary.fileName);
|
|
65
|
+
return form;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 出站 image 段的媒体引用:已有 media_id 的视为已物化;
|
|
70
|
+
* 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
|
|
71
|
+
*/
|
|
72
|
+
export function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined {
|
|
73
|
+
if (typeof data.media_id === 'string' && data.media_id) return undefined;
|
|
74
|
+
if (typeof data.mediaId === 'string' && data.mediaId) return undefined;
|
|
75
|
+
if (isMediaRef(data.media)) return data.media;
|
|
76
|
+
return mediaRefFromLegacyData(data);
|
|
77
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createHash, createDecipheriv } from 'node:crypto';
|
|
6
|
+
import { createHash, createDecipheriv, timingSafeEqual } from 'node:crypto';
|
|
7
7
|
import type { IncomingMessage } from 'node:http';
|
|
8
8
|
|
|
9
9
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
@@ -149,7 +149,10 @@ export function verifySignature(
|
|
|
149
149
|
const hash = createHash('sha1')
|
|
150
150
|
.update([token, timestamp, nonce, encrypt].sort().join(''))
|
|
151
151
|
.digest('hex');
|
|
152
|
-
|
|
152
|
+
const a = Buffer.from(hash);
|
|
153
|
+
const b = Buffer.from(signature);
|
|
154
|
+
if (a.length !== b.length) return false;
|
|
155
|
+
return timingSafeEqual(a, b);
|
|
153
156
|
} catch {
|
|
154
157
|
return false;
|
|
155
158
|
}
|
|
@@ -289,7 +292,8 @@ export function formatOutboundBody(payload: unknown): WecomSendBody {
|
|
|
289
292
|
if (!hasMedia) {
|
|
290
293
|
hasMedia = true;
|
|
291
294
|
mediaType = 'image';
|
|
292
|
-
|
|
295
|
+
// media_id 由 endpoint 上传物化写入;file/url 为旧调用方直传 media_id 的兼容入口。
|
|
296
|
+
mediaData = { media_id: data.media_id ?? data.mediaId ?? data.file ?? data.url };
|
|
293
297
|
}
|
|
294
298
|
break;
|
|
295
299
|
case 'markdown':
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wecom endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*/
|
|
5
|
+
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
6
|
+
import { defineCommand } from '@zhin.js/command';
|
|
7
|
+
import { wecomRuntimeStateToken } from './wecom-runtime-state.js';
|
|
8
|
+
|
|
9
|
+
export const wecomEndpointCommands = createEndpointCommands({
|
|
10
|
+
adapterKey: 'wecom',
|
|
11
|
+
adapterDisplayName: 'WeCom',
|
|
12
|
+
fields: [
|
|
13
|
+
{ key: 'corpId', required: true, env: true, description: '企业 corpId' },
|
|
14
|
+
{ key: 'agentSecret', required: true, env: true, description: '应用 secret' },
|
|
15
|
+
{ key: 'token', required: true, env: true, description: '回调 token' },
|
|
16
|
+
{ key: 'encodingAESKey', required: true, env: true, description: '回调加解密 key' },
|
|
17
|
+
],
|
|
18
|
+
running: (use) => use(wecomRuntimeStateToken).endpoints.values(),
|
|
19
|
+
describeEntry: (entry) => `corpId: ${String(entry.corpId)}`,
|
|
20
|
+
}, defineCommand);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `wecom endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
|
|
7
|
+
export const wecomRuntimeStateToken = defineEndpointRuntimeStateToken('wecom');
|
package/plugin.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { registerWecomPlatformPermitChecker } from './src/platform-permit.js';
|
|
3
|
-
|
|
4
|
-
export default definePlugin({
|
|
5
|
-
name: 'wecom',
|
|
6
|
-
metadata: {
|
|
7
|
-
displayName: 'WeCom (企业微信) Adapter',
|
|
8
|
-
},
|
|
9
|
-
setup() {
|
|
10
|
-
return registerWecomPlatformPermitChecker();
|
|
11
|
-
},
|
|
12
|
-
});
|