@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.
- package/README.md +268 -0
- package/clawdbot.plugin.json +11 -0
- package/index.ts +17 -0
- package/package.json +17 -0
- package/src/access-token.ts +56 -0
- package/src/channel.ts +114 -0
- package/src/client.ts +124 -0
- package/src/config-schema.ts +19 -0
- package/src/crypto.ts +113 -0
- package/src/gateway.ts +413 -0
- package/src/message-parser.ts +195 -0
- package/src/multipart.ts +70 -0
- package/src/official-api.ts +150 -0
- package/src/runtime.ts +12 -0
package/src/multipart.ts
ADDED
|
@@ -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
|
+
}
|