@x.ken/wecom 1.0.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.
@@ -0,0 +1,70 @@
1
+ export interface MultipartFile {
2
+ filename: string;
3
+ data: Buffer;
4
+ mimetype: string;
5
+ }
6
+
7
+ export interface MultipartParseResult {
8
+ fields: Record<string, string>;
9
+ files: MultipartFile[];
10
+ }
11
+
12
+ export function parseMultipart(
13
+ buffer: Buffer,
14
+ boundary: string
15
+ ): MultipartParseResult {
16
+ const fields: Record<string, string> = {};
17
+ const files: MultipartFile[] = [];
18
+
19
+ const boundaryBuffer = Buffer.from(`--${boundary}`);
20
+ const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
21
+
22
+ let offset = 0;
23
+
24
+ while (offset < buffer.length) {
25
+ const boundaryIndex = buffer.indexOf(boundaryBuffer, offset);
26
+ if (boundaryIndex === -1) break;
27
+
28
+ const nextBoundaryIndex = buffer.indexOf(boundaryBuffer, boundaryIndex + boundaryBuffer.length);
29
+
30
+ if (nextBoundaryIndex === -1) {
31
+ break;
32
+ }
33
+
34
+ const partStart = boundaryIndex + boundaryBuffer.length;
35
+ const partEnd = nextBoundaryIndex;
36
+
37
+ const part = buffer.slice(partStart, partEnd);
38
+
39
+ const headerEndIndex = part.indexOf(Buffer.from("\r\n\r\n"));
40
+ if (headerEndIndex === -1) continue;
41
+
42
+ const headers = part.slice(0, headerEndIndex).toString("utf8");
43
+ const body = part.slice(headerEndIndex + 4);
44
+
45
+ const contentDispositionMatch = headers.match(/Content-Disposition:\s*form-data;\s*name="([^"]+)"/i);
46
+ if (!contentDispositionMatch) continue;
47
+
48
+ const name = contentDispositionMatch[1];
49
+
50
+ const filenameMatch = headers.match(/filename="([^"]+)"/i);
51
+ const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i);
52
+
53
+ if (filenameMatch) {
54
+ const filename = filenameMatch[1];
55
+ const mimetype = contentTypeMatch ? contentTypeMatch[1].trim() : "application/octet-stream";
56
+
57
+ files.push({
58
+ filename,
59
+ data: body,
60
+ mimetype,
61
+ });
62
+ } else {
63
+ fields[name] = body.toString("utf8");
64
+ }
65
+
66
+ offset = nextBoundaryIndex;
67
+ }
68
+
69
+ return { fields, files };
70
+ }
@@ -0,0 +1,150 @@
1
+ import { accessTokenManager } from "./access-token.js";
2
+
3
+ export type WeComMessageType = "text" | "image" | "voice" | "video" | "file" | "textcard" | "news" | "mpnews" | "markdown";
4
+
5
+ export interface WeComTextMessagePayload {
6
+ msgtype: "text";
7
+ agentid: number;
8
+ touser?: string;
9
+ toparty?: string;
10
+ totag?: string;
11
+ safe?: 0 | 1;
12
+ enable_id_trans?: 0 | 1;
13
+ enable_duplicate_check?: 0 | 1;
14
+ duplicate_check_interval?: number;
15
+ text: {
16
+ content: string;
17
+ };
18
+ }
19
+
20
+ export interface WeComMarkdownMessagePayload {
21
+ msgtype: "markdown";
22
+ agentid: number;
23
+ touser?: string;
24
+ toparty?: string;
25
+ totag?: string;
26
+ markdown: {
27
+ content: string;
28
+ };
29
+ }
30
+
31
+ export interface WeComImageMessagePayload {
32
+ msgtype: "image";
33
+ agentid: number;
34
+ touser?: string;
35
+ toparty?: string;
36
+ totag?: string;
37
+ image: {
38
+ media_id: string;
39
+ };
40
+ }
41
+
42
+ export interface WeComTextCardMessagePayload {
43
+ msgtype: "textcard";
44
+ agentid: number;
45
+ touser?: string;
46
+ toparty?: string;
47
+ totag?: string;
48
+ textcard: {
49
+ title: string;
50
+ description: string;
51
+ url: string;
52
+ btntxt?: string;
53
+ };
54
+ }
55
+
56
+ export type WeComSendMessagePayload =
57
+ | WeComTextMessagePayload
58
+ | WeComMarkdownMessagePayload
59
+ | WeComImageMessagePayload
60
+ | WeComTextCardMessagePayload;
61
+
62
+ export class WeComOfficialAPI {
63
+ async sendMessage(
64
+ corpid: string,
65
+ corpsecret: string,
66
+ payload: WeComSendMessagePayload
67
+ ): Promise<{ errcode: number; errmsg: string; invaliduser?: string; invalidparty?: string; invalidtag?: string }> {
68
+ const accessToken = await accessTokenManager.getAccessToken(
69
+ corpid,
70
+ corpsecret
71
+ );
72
+
73
+ const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(
74
+ accessToken
75
+ )}`;
76
+
77
+ const response = await fetch(url, {
78
+ method: "POST",
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ },
82
+ body: JSON.stringify(payload),
83
+ });
84
+
85
+ if (!response.ok) {
86
+ throw new Error(
87
+ `WeChat API request failed: ${response.status} ${response.statusText}`
88
+ );
89
+ }
90
+
91
+ const result = await response.json();
92
+
93
+ if (result.errcode !== 0) {
94
+ console.error("WeChat send message error:", result);
95
+
96
+ if (result.errcode === 40014 || result.errcode === 42001) {
97
+ accessTokenManager.clearCache(corpid, corpsecret);
98
+ }
99
+
100
+ throw new Error(
101
+ `WeChat API error: ${result.errcode} - ${result.errmsg}`
102
+ );
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ async uploadMedia(
109
+ corpid: string,
110
+ corpsecret: string,
111
+ type: "image" | "voice" | "video" | "file",
112
+ file: Buffer,
113
+ filename: string
114
+ ): Promise<{ type: string; media_id: string; created_at: number }> {
115
+ const accessToken = await accessTokenManager.getAccessToken(
116
+ corpid,
117
+ corpsecret
118
+ );
119
+
120
+ const url = `https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=${encodeURIComponent(
121
+ accessToken
122
+ )}&type=${type}`;
123
+
124
+ const formData = new FormData();
125
+ formData.append("media", file as any, filename);
126
+
127
+ const response = await fetch(url, {
128
+ method: "POST",
129
+ body: formData,
130
+ });
131
+
132
+ if (!response.ok) {
133
+ throw new Error(
134
+ `Upload media failed: ${response.status} ${response.statusText}`
135
+ );
136
+ }
137
+
138
+ const result = await response.json();
139
+
140
+ if (result.errcode && result.errcode !== 0) {
141
+ throw new Error(
142
+ `WeChat upload error: ${result.errcode} - ${result.errmsg}`
143
+ );
144
+ }
145
+
146
+ return result;
147
+ }
148
+ }
149
+
150
+ export const wecomOfficialAPI = new WeComOfficialAPI();
package/src/runtime.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { PluginRuntime } from "clawdbot/plugin-sdk";
2
+
3
+ let _runtime: PluginRuntime | undefined;
4
+
5
+ export function setWeComRuntime(runtime: PluginRuntime) {
6
+ _runtime = runtime;
7
+ }
8
+
9
+ export function getWeComRuntime() {
10
+ if (!_runtime) throw new Error("WeCom runtime not initialized");
11
+ return _runtime;
12
+ }