@max1874/openclaw-wecom 0.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.
package/src/send.ts ADDED
@@ -0,0 +1,250 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type {
3
+ WecomConfig,
4
+ WecomSendResult,
5
+ WecomSendRequest,
6
+ WecomSendResponse,
7
+ SendTextPayload,
8
+ SendImagePayload,
9
+ SendFilePayload,
10
+ SendLinkPayload,
11
+ } from "./types.js";
12
+ import { normalizeWecomTarget } from "./targets.js";
13
+
14
+ const STRIDE_API_URL = "https://stride-bg.dpclouds.com/stream-api/message/send";
15
+
16
+ /**
17
+ * Send a message via Stride API.
18
+ */
19
+ async function sendStrideMessage(request: WecomSendRequest): Promise<WecomSendResponse> {
20
+ const response = await fetch(STRIDE_API_URL, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ },
25
+ body: JSON.stringify(request),
26
+ });
27
+
28
+ if (!response.ok) {
29
+ throw new Error(`Stride API error: ${response.status} ${response.statusText}`);
30
+ }
31
+
32
+ return (await response.json()) as WecomSendResponse;
33
+ }
34
+
35
+ export type SendWecomMessageParams = {
36
+ cfg: ClawdbotConfig;
37
+ to: string;
38
+ text: string;
39
+ };
40
+
41
+ /**
42
+ * Send a text message.
43
+ */
44
+ export async function sendMessageWecom(params: SendWecomMessageParams): Promise<WecomSendResult> {
45
+ const { cfg, to, text } = params;
46
+ const wecomCfg = cfg.channels?.wecom as WecomConfig | undefined;
47
+
48
+ if (!wecomCfg?.token) {
49
+ throw new Error("WeChat channel not configured (token required)");
50
+ }
51
+
52
+ const chatId = normalizeWecomTarget(to) ?? wecomCfg.chatId;
53
+ if (!chatId) {
54
+ throw new Error(`Invalid WeChat target: ${to}`);
55
+ }
56
+
57
+ const payload: SendTextPayload = { text };
58
+
59
+ const request: WecomSendRequest = {
60
+ token: wecomCfg.token,
61
+ chatId,
62
+ messageType: 0,
63
+ payload,
64
+ };
65
+
66
+ try {
67
+ const response = await sendStrideMessage(request);
68
+ if (response.code !== 0) {
69
+ return {
70
+ chatId,
71
+ success: false,
72
+ error: response.message ?? `Error code: ${response.code}`,
73
+ };
74
+ }
75
+ return { chatId, success: true };
76
+ } catch (err) {
77
+ return {
78
+ chatId,
79
+ success: false,
80
+ error: String(err),
81
+ };
82
+ }
83
+ }
84
+
85
+ export type SendWecomImageParams = {
86
+ cfg: ClawdbotConfig;
87
+ to: string;
88
+ imageUrl: string;
89
+ };
90
+
91
+ /**
92
+ * Send an image message.
93
+ */
94
+ export async function sendImageWecom(params: SendWecomImageParams): Promise<WecomSendResult> {
95
+ const { cfg, to, imageUrl } = params;
96
+ const wecomCfg = cfg.channels?.wecom as WecomConfig | undefined;
97
+
98
+ if (!wecomCfg?.token) {
99
+ throw new Error("WeChat channel not configured (token required)");
100
+ }
101
+
102
+ const chatId = normalizeWecomTarget(to) ?? wecomCfg.chatId;
103
+ if (!chatId) {
104
+ throw new Error(`Invalid WeChat target: ${to}`);
105
+ }
106
+
107
+ const payload: SendImagePayload = {
108
+ imageUrl,
109
+ url: imageUrl,
110
+ };
111
+
112
+ const request: WecomSendRequest = {
113
+ token: wecomCfg.token,
114
+ chatId,
115
+ messageType: 1,
116
+ payload,
117
+ };
118
+
119
+ try {
120
+ const response = await sendStrideMessage(request);
121
+ if (response.code !== 0) {
122
+ return {
123
+ chatId,
124
+ success: false,
125
+ error: response.message ?? `Error code: ${response.code}`,
126
+ };
127
+ }
128
+ return { chatId, success: true };
129
+ } catch (err) {
130
+ return {
131
+ chatId,
132
+ success: false,
133
+ error: String(err),
134
+ };
135
+ }
136
+ }
137
+
138
+ export type SendWecomFileParams = {
139
+ cfg: ClawdbotConfig;
140
+ to: string;
141
+ fileUrl: string;
142
+ fileName: string;
143
+ fileSize?: number;
144
+ };
145
+
146
+ /**
147
+ * Send a file message.
148
+ */
149
+ export async function sendFileWecom(params: SendWecomFileParams): Promise<WecomSendResult> {
150
+ const { cfg, to, fileUrl, fileName, fileSize } = params;
151
+ const wecomCfg = cfg.channels?.wecom as WecomConfig | undefined;
152
+
153
+ if (!wecomCfg?.token) {
154
+ throw new Error("WeChat channel not configured (token required)");
155
+ }
156
+
157
+ const chatId = normalizeWecomTarget(to) ?? wecomCfg.chatId;
158
+ if (!chatId) {
159
+ throw new Error(`Invalid WeChat target: ${to}`);
160
+ }
161
+
162
+ const payload: SendFilePayload = {
163
+ name: fileName,
164
+ url: fileUrl,
165
+ size: fileSize,
166
+ };
167
+
168
+ const request: WecomSendRequest = {
169
+ token: wecomCfg.token,
170
+ chatId,
171
+ messageType: 3,
172
+ payload,
173
+ };
174
+
175
+ try {
176
+ const response = await sendStrideMessage(request);
177
+ if (response.code !== 0) {
178
+ return {
179
+ chatId,
180
+ success: false,
181
+ error: response.message ?? `Error code: ${response.code}`,
182
+ };
183
+ }
184
+ return { chatId, success: true };
185
+ } catch (err) {
186
+ return {
187
+ chatId,
188
+ success: false,
189
+ error: String(err),
190
+ };
191
+ }
192
+ }
193
+
194
+ export type SendWecomLinkParams = {
195
+ cfg: ClawdbotConfig;
196
+ to: string;
197
+ title: string;
198
+ url: string;
199
+ summary?: string;
200
+ imageUrl?: string;
201
+ };
202
+
203
+ /**
204
+ * Send a link message.
205
+ */
206
+ export async function sendLinkWecom(params: SendWecomLinkParams): Promise<WecomSendResult> {
207
+ const { cfg, to, title, url, summary, imageUrl } = params;
208
+ const wecomCfg = cfg.channels?.wecom as WecomConfig | undefined;
209
+
210
+ if (!wecomCfg?.token) {
211
+ throw new Error("WeChat channel not configured (token required)");
212
+ }
213
+
214
+ const chatId = normalizeWecomTarget(to) ?? wecomCfg.chatId;
215
+ if (!chatId) {
216
+ throw new Error(`Invalid WeChat target: ${to}`);
217
+ }
218
+
219
+ const payload: SendLinkPayload = {
220
+ title,
221
+ sourceUrl: url,
222
+ summary,
223
+ imageUrl,
224
+ };
225
+
226
+ const request: WecomSendRequest = {
227
+ token: wecomCfg.token,
228
+ chatId,
229
+ messageType: 2,
230
+ payload,
231
+ };
232
+
233
+ try {
234
+ const response = await sendStrideMessage(request);
235
+ if (response.code !== 0) {
236
+ return {
237
+ chatId,
238
+ success: false,
239
+ error: response.message ?? `Error code: ${response.code}`,
240
+ };
241
+ }
242
+ return { chatId, success: true };
243
+ } catch (err) {
244
+ return {
245
+ chatId,
246
+ success: false,
247
+ error: String(err),
248
+ };
249
+ }
250
+ }
package/src/targets.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Normalize a target string to a raw ID.
3
+ * Supported formats:
4
+ * - "chat:<chatId>" -> chatId
5
+ * - "user:<contactId>" -> contactId
6
+ * - "room:<roomId>" -> roomId
7
+ * - raw ID (no prefix)
8
+ */
9
+ export function normalizeWecomTarget(raw: string): string | null {
10
+ const trimmed = raw.trim();
11
+ if (!trimmed) return null;
12
+
13
+ const lowered = trimmed.toLowerCase();
14
+ if (lowered.startsWith("chat:")) {
15
+ return trimmed.slice("chat:".length).trim() || null;
16
+ }
17
+ if (lowered.startsWith("user:")) {
18
+ return trimmed.slice("user:".length).trim() || null;
19
+ }
20
+ if (lowered.startsWith("room:")) {
21
+ return trimmed.slice("room:".length).trim() || null;
22
+ }
23
+
24
+ return trimmed;
25
+ }
26
+
27
+ /**
28
+ * Format a target ID with type prefix for display.
29
+ */
30
+ export function formatWecomTarget(id: string, type?: "chat" | "user" | "room"): string {
31
+ const trimmed = id.trim();
32
+ if (type === "chat") return `chat:${trimmed}`;
33
+ if (type === "user") return `user:${trimmed}`;
34
+ if (type === "room") return `room:${trimmed}`;
35
+ return trimmed;
36
+ }
37
+
38
+ /**
39
+ * Check if a string looks like a WeChat/Stride ID.
40
+ */
41
+ export function looksLikeWecomId(raw: string): boolean {
42
+ const trimmed = raw.trim();
43
+ if (!trimmed) return false;
44
+ if (/^(chat|user|room):/i.test(trimmed)) return true;
45
+ // Stride chatId format: 24-char hex
46
+ if (/^[a-f0-9]{24}$/.test(trimmed)) return true;
47
+ // WeChat wxid format: numeric string
48
+ if (/^\d{10,20}$/.test(trimmed)) return true;
49
+ // Room ID format: R:xxxxx
50
+ if (/^R:\d+$/.test(trimmed)) return true;
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Detect the type of an ID based on its format.
56
+ */
57
+ export function detectIdType(id: string): "chat" | "room" | "user" | null {
58
+ const trimmed = id.trim();
59
+ // Stride chatId: 24-char hex
60
+ if (/^[a-f0-9]{24}$/.test(trimmed)) return "chat";
61
+ // Room ID: R:xxxxx
62
+ if (/^R:\d+$/.test(trimmed)) return "room";
63
+ // WeChat wxid: numeric
64
+ if (/^\d{10,20}$/.test(trimmed)) return "user";
65
+ return null;
66
+ }
package/src/types.ts ADDED
@@ -0,0 +1,257 @@
1
+ import type { WecomConfigSchema, WecomGroupSchema, z } from "./config-schema.js";
2
+
3
+ export type WecomConfig = z.infer<typeof WecomConfigSchema>;
4
+ export type WecomGroupConfig = z.infer<typeof WecomGroupSchema>;
5
+
6
+ export type ResolvedWecomAccount = {
7
+ accountId: string;
8
+ enabled: boolean;
9
+ configured: boolean;
10
+ token?: string;
11
+ chatId?: string;
12
+ };
13
+
14
+ // --- Stride Webhook Event Types ---
15
+
16
+ /**
17
+ * Common sender/room information present in all webhook events
18
+ */
19
+ export type WecomEventMeta = {
20
+ contactName: string;
21
+ contactId: string;
22
+ avatar?: string;
23
+ roomTopic?: string;
24
+ roomId?: string;
25
+ chatId: string;
26
+ mentionSelf: boolean;
27
+ isSelf: boolean;
28
+ };
29
+
30
+ /**
31
+ * Inbound message types (from webhook)
32
+ */
33
+ export type InboundMessageType = 1 | 2 | 4 | 6 | 7 | 12 | 10000 | 10001;
34
+
35
+ // Type 1: File message
36
+ export type FilePayload = {
37
+ fileUrl: string;
38
+ name: string;
39
+ size?: number;
40
+ md5?: string;
41
+ };
42
+
43
+ // Type 2: Voice message (with transcription)
44
+ export type VoicePayload = {
45
+ voiceUrl: string;
46
+ duration?: number;
47
+ text?: string; // Transcribed text from Stride
48
+ md5?: string;
49
+ wxVoiceUrl?: string;
50
+ };
51
+
52
+ // Type 4: Chat history (merged forward)
53
+ export type ChatHistoryItem = {
54
+ type: number;
55
+ avatar?: string;
56
+ senderName?: string;
57
+ corpName?: string;
58
+ time?: number;
59
+ message: {
60
+ content?: string;
61
+ imageUrl?: string;
62
+ name?: string;
63
+ fileUrl?: string;
64
+ link?: {
65
+ title?: string;
66
+ des?: string;
67
+ thumbnailUrl?: string;
68
+ link?: string;
69
+ };
70
+ };
71
+ };
72
+
73
+ export type ChatHistoryPayload = {
74
+ content: string;
75
+ chatHistoryList: ChatHistoryItem[];
76
+ };
77
+
78
+ // Type 6: Image message
79
+ export type ImagePayload = {
80
+ imageUrl: string;
81
+ size?: number;
82
+ width?: number;
83
+ height?: number;
84
+ };
85
+
86
+ // Type 7: Text message
87
+ export type TextPayload = {
88
+ text: string;
89
+ mention?: string[];
90
+ pureText?: string;
91
+ };
92
+
93
+ // Type 12: Link message
94
+ export type LinkPayload = {
95
+ title: string;
96
+ description?: string;
97
+ url: string;
98
+ thumbnailUrl?: string;
99
+ };
100
+
101
+ // Type 10000: System message (join notification)
102
+ export type SystemJoinPayload = {
103
+ type: number;
104
+ subPayload: {
105
+ memberNames?: string[];
106
+ inviterName?: string;
107
+ };
108
+ };
109
+
110
+ // Type 10001: System message (member list)
111
+ export type SystemMemberListPayload = {
112
+ subPayload: {
113
+ inviteeList?: Array<{
114
+ displayName: string;
115
+ wxid: string;
116
+ isSelf: boolean;
117
+ }>;
118
+ inviter?: {
119
+ displayName: string;
120
+ wxid: string;
121
+ isSelf: boolean;
122
+ };
123
+ };
124
+ };
125
+
126
+ /**
127
+ * Union type for all inbound message payloads
128
+ */
129
+ export type InboundPayload =
130
+ | FilePayload
131
+ | VoicePayload
132
+ | ChatHistoryPayload
133
+ | ImagePayload
134
+ | TextPayload
135
+ | LinkPayload
136
+ | SystemJoinPayload
137
+ | SystemMemberListPayload;
138
+
139
+ /**
140
+ * Inbound webhook event structure
141
+ */
142
+ export type WecomWebhookEvent = WecomEventMeta & {
143
+ type: InboundMessageType;
144
+ payload: InboundPayload;
145
+ };
146
+
147
+ // --- Outbound Message Types (for sending) ---
148
+
149
+ export type OutboundMessageType = 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10;
150
+
151
+ // messageType 0: Text
152
+ export type SendTextPayload = {
153
+ text: string;
154
+ };
155
+
156
+ // messageType 1: Image
157
+ export type SendImagePayload = {
158
+ imageUrl: string;
159
+ url: string;
160
+ };
161
+
162
+ // messageType 2: Link
163
+ export type SendLinkPayload = {
164
+ title: string;
165
+ summary?: string;
166
+ sourceUrl: string;
167
+ imageUrl?: string;
168
+ };
169
+
170
+ // messageType 3: File
171
+ export type SendFilePayload = {
172
+ name: string;
173
+ url: string;
174
+ size?: number;
175
+ };
176
+
177
+ // messageType 5: Video
178
+ export type SendVideoPayload = {
179
+ url: string; // mp4 only
180
+ };
181
+
182
+ // messageType 8: Voice
183
+ export type SendVoicePayload = {
184
+ voiceUrl: string; // silk only
185
+ duration?: number;
186
+ };
187
+
188
+ // messageType 9: Emoji
189
+ export type SendEmojiPayload = {
190
+ imageUrl: string;
191
+ };
192
+
193
+ /**
194
+ * Union type for all outbound message payloads
195
+ */
196
+ export type OutboundPayload =
197
+ | SendTextPayload
198
+ | SendImagePayload
199
+ | SendLinkPayload
200
+ | SendFilePayload
201
+ | SendVideoPayload
202
+ | SendVoicePayload
203
+ | SendEmojiPayload;
204
+
205
+ /**
206
+ * Outbound message request structure
207
+ */
208
+ export type WecomSendRequest = {
209
+ token: string;
210
+ chatId: string;
211
+ externalRequestId?: string;
212
+ messageType: OutboundMessageType;
213
+ payload: OutboundPayload;
214
+ };
215
+
216
+ /**
217
+ * Outbound API response
218
+ */
219
+ export type WecomSendResponse = {
220
+ code: number;
221
+ message?: string;
222
+ data?: unknown;
223
+ };
224
+
225
+ // --- Internal Types ---
226
+
227
+ export type WecomMessageContext = {
228
+ chatId: string;
229
+ contactId: string;
230
+ contactName: string;
231
+ roomId?: string;
232
+ roomTopic?: string;
233
+ chatType: "dm" | "group";
234
+ mentionedBot: boolean;
235
+ content: string;
236
+ contentType: string;
237
+ mediaUrl?: string;
238
+ mediaType?: string;
239
+ };
240
+
241
+ export type WecomSendResult = {
242
+ chatId: string;
243
+ success: boolean;
244
+ error?: string;
245
+ };
246
+
247
+ export type WecomProbeResult = {
248
+ ok: boolean;
249
+ error?: string;
250
+ chatId?: string;
251
+ };
252
+
253
+ export type WecomMediaInfo = {
254
+ url: string;
255
+ contentType?: string;
256
+ placeholder: string;
257
+ };