@marshulll/openclaw-wecom 0.1.17 → 0.1.18
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/package.json
CHANGED
package/wecom/src/accounts.ts
CHANGED
|
@@ -88,6 +88,9 @@ export function resolveWecomAccount(params: {
|
|
|
88
88
|
const callbackAesKey = merged.callbackAesKey?.trim()
|
|
89
89
|
|| resolveAccountEnv(params.cfg, accountId, "CALLBACK_AES_KEY")
|
|
90
90
|
|| undefined;
|
|
91
|
+
const pushToken = merged.pushToken?.trim()
|
|
92
|
+
|| resolveAccountEnv(params.cfg, accountId, "PUSH_TOKEN")
|
|
93
|
+
|| undefined;
|
|
91
94
|
const webhookPath = merged.webhookPath?.trim()
|
|
92
95
|
|| resolveAccountEnv(params.cfg, accountId, "WEBHOOK_PATH")
|
|
93
96
|
|| undefined;
|
|
@@ -109,6 +112,7 @@ export function resolveWecomAccount(params: {
|
|
|
109
112
|
agentId,
|
|
110
113
|
callbackToken,
|
|
111
114
|
callbackAesKey,
|
|
115
|
+
pushToken,
|
|
112
116
|
};
|
|
113
117
|
|
|
114
118
|
return {
|
package/wecom/src/channel.ts
CHANGED
|
@@ -183,6 +183,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
183
183
|
return { stop: () => {} };
|
|
184
184
|
}
|
|
185
185
|
const path = (account.config.webhookPath ?? "/wecom").trim();
|
|
186
|
+
const pushPath = path.endsWith("/") ? `${path}push` : `${path}/push`;
|
|
186
187
|
const unregister = registerWecomWebhookTarget({
|
|
187
188
|
account,
|
|
188
189
|
config: ctx.cfg as ClawdbotConfig,
|
|
@@ -191,7 +192,16 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
191
192
|
path,
|
|
192
193
|
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
193
194
|
});
|
|
195
|
+
const unregisterPush = registerWecomWebhookTarget({
|
|
196
|
+
account,
|
|
197
|
+
config: ctx.cfg as ClawdbotConfig,
|
|
198
|
+
runtime: ctx.runtime,
|
|
199
|
+
core: ({} as unknown) as any,
|
|
200
|
+
path: pushPath,
|
|
201
|
+
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
202
|
+
});
|
|
194
203
|
ctx.log?.info(`[${account.accountId}] wecom webhook registered at ${path}`);
|
|
204
|
+
ctx.log?.info(`[${account.accountId}] wecom push endpoint registered at ${pushPath}`);
|
|
195
205
|
ctx.setStatus({
|
|
196
206
|
accountId: account.accountId,
|
|
197
207
|
running: true,
|
|
@@ -202,6 +212,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
202
212
|
return {
|
|
203
213
|
stop: () => {
|
|
204
214
|
unregister();
|
|
215
|
+
unregisterPush();
|
|
205
216
|
ctx.setStatus({
|
|
206
217
|
accountId: account.accountId,
|
|
207
218
|
running: false,
|
|
@@ -41,6 +41,7 @@ const accountSchema = z.object({
|
|
|
41
41
|
agentId: z.union([z.string(), z.number()]).optional(),
|
|
42
42
|
callbackToken: z.string().optional(),
|
|
43
43
|
callbackAesKey: z.string().optional(),
|
|
44
|
+
pushToken: z.string().optional(),
|
|
44
45
|
|
|
45
46
|
media: z.object({
|
|
46
47
|
tempDir: z.string().optional(),
|
|
@@ -85,6 +86,7 @@ export const WecomConfigSchema = ensureJsonSchema(z.object({
|
|
|
85
86
|
agentId: z.union([z.string(), z.number()]).optional(),
|
|
86
87
|
callbackToken: z.string().optional(),
|
|
87
88
|
callbackAesKey: z.string().optional(),
|
|
89
|
+
pushToken: z.string().optional(),
|
|
88
90
|
|
|
89
91
|
media: z.object({
|
|
90
92
|
tempDir: z.string().optional(),
|
package/wecom/src/monitor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
3
3
|
import type { ClawdbotConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
4
4
|
|
|
5
5
|
import type { ResolvedWecomAccount } from "./types.js";
|
|
6
|
-
import { handleWecomAppWebhook } from "./wecom-app.js";
|
|
6
|
+
import { handleWecomAppWebhook, handleWecomPushRequest } from "./wecom-app.js";
|
|
7
7
|
import { handleWecomBotWebhook } from "./wecom-bot.js";
|
|
8
8
|
|
|
9
9
|
export type WecomRuntimeEnv = {
|
|
@@ -55,6 +55,9 @@ export async function handleWecomWebhookRequest(
|
|
|
55
55
|
const path = resolvePath(req);
|
|
56
56
|
const targets = webhookTargets.get(path);
|
|
57
57
|
if (!targets || targets.length === 0) return false;
|
|
58
|
+
if (path.endsWith("/push")) {
|
|
59
|
+
return await handleWecomPushRequest({ req, res, targets });
|
|
60
|
+
}
|
|
58
61
|
const firstTarget = targets[0];
|
|
59
62
|
const ua = req.headers["user-agent"] ?? "";
|
|
60
63
|
const fwd = req.headers["x-forwarded-for"] ?? "";
|
package/wecom/src/types.ts
CHANGED
package/wecom/src/wecom-app.ts
CHANGED
|
@@ -101,6 +101,27 @@ async function readRequestBody(req: IncomingMessage, maxSize = MAX_REQUEST_BODY_
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function resolveHeaderToken(req: IncomingMessage): string {
|
|
105
|
+
const auth = req.headers.authorization ?? "";
|
|
106
|
+
if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
|
|
107
|
+
return auth.slice(7).trim();
|
|
108
|
+
}
|
|
109
|
+
const token = req.headers["x-openclaw-token"];
|
|
110
|
+
if (typeof token === "string") return token.trim();
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pickFirstString(...values: unknown[]): string {
|
|
115
|
+
for (const value of values) {
|
|
116
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
117
|
+
}
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sleep(ms: number): Promise<void> {
|
|
122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
function logVerbose(target: WecomWebhookTarget, message: string): void {
|
|
105
126
|
target.runtime.log?.(`[wecom] ${message}`);
|
|
106
127
|
}
|
|
@@ -809,6 +830,163 @@ async function processAppMessage(params: {
|
|
|
809
830
|
}
|
|
810
831
|
}
|
|
811
832
|
|
|
833
|
+
type PushMessage = {
|
|
834
|
+
text?: string;
|
|
835
|
+
mediaUrl?: string;
|
|
836
|
+
mediaPath?: string;
|
|
837
|
+
mediaBase64?: string;
|
|
838
|
+
mediaType?: string;
|
|
839
|
+
filename?: string;
|
|
840
|
+
title?: string;
|
|
841
|
+
description?: string;
|
|
842
|
+
delayMs?: number;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
type PushPayload = PushMessage & {
|
|
846
|
+
accountId?: string;
|
|
847
|
+
toUser?: string;
|
|
848
|
+
chatId?: string;
|
|
849
|
+
token?: string;
|
|
850
|
+
intervalMs?: number;
|
|
851
|
+
messages?: PushMessage[];
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
function resolvePushToken(target: WecomWebhookTarget): string {
|
|
855
|
+
return target.account.config.pushToken?.trim() || "";
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function selectPushTarget(targets: WecomWebhookTarget[], accountId?: string): WecomWebhookTarget | undefined {
|
|
859
|
+
const appTargets = targets.filter((candidate) => shouldHandleApp(candidate));
|
|
860
|
+
if (!accountId) return appTargets[0];
|
|
861
|
+
return appTargets.find((candidate) => candidate.account.accountId === accountId);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export async function handleWecomPushRequest(params: {
|
|
865
|
+
req: IncomingMessage;
|
|
866
|
+
res: ServerResponse;
|
|
867
|
+
targets: WecomWebhookTarget[];
|
|
868
|
+
}): Promise<boolean> {
|
|
869
|
+
const { req, res, targets } = params;
|
|
870
|
+
if ((req.method ?? "").toUpperCase() !== "POST") {
|
|
871
|
+
res.statusCode = 405;
|
|
872
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
873
|
+
res.end("Method Not Allowed");
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
let payload: PushPayload | null = null;
|
|
878
|
+
try {
|
|
879
|
+
const raw = await readRequestBody(req, MAX_REQUEST_BODY_SIZE);
|
|
880
|
+
payload = raw ? (JSON.parse(raw) as PushPayload) : {};
|
|
881
|
+
} catch (err) {
|
|
882
|
+
res.statusCode = 400;
|
|
883
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
884
|
+
res.end(JSON.stringify({ ok: false, error: `Invalid JSON: ${String(err)}` }));
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
889
|
+
const accountId = pickFirstString(payload?.accountId, url.searchParams.get("accountId"));
|
|
890
|
+
const target = selectPushTarget(targets, accountId);
|
|
891
|
+
if (!target) {
|
|
892
|
+
res.statusCode = 404;
|
|
893
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
894
|
+
res.end(JSON.stringify({ ok: false, error: "No matching WeCom app account" }));
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const expectedToken = resolvePushToken(target);
|
|
899
|
+
const requestToken = pickFirstString(
|
|
900
|
+
payload?.token,
|
|
901
|
+
url.searchParams.get("token"),
|
|
902
|
+
resolveHeaderToken(req),
|
|
903
|
+
);
|
|
904
|
+
if (expectedToken && expectedToken !== requestToken) {
|
|
905
|
+
res.statusCode = 403;
|
|
906
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
907
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid push token" }));
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const toUser = pickFirstString(payload?.toUser, url.searchParams.get("toUser"));
|
|
912
|
+
const chatId = pickFirstString(payload?.chatId, url.searchParams.get("chatId"));
|
|
913
|
+
if (!toUser && !chatId) {
|
|
914
|
+
res.statusCode = 400;
|
|
915
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
916
|
+
res.end(JSON.stringify({ ok: false, error: "Missing toUser or chatId" }));
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!target.account.corpId || !target.account.corpSecret || !target.account.agentId) {
|
|
921
|
+
res.statusCode = 500;
|
|
922
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
923
|
+
res.end(JSON.stringify({ ok: false, error: "WeCom app not configured" }));
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const messages = Array.isArray(payload?.messages) && payload?.messages.length > 0
|
|
928
|
+
? payload.messages
|
|
929
|
+
: [payload ?? {}];
|
|
930
|
+
const intervalMs = typeof payload?.intervalMs === "number" && payload.intervalMs > 0 ? payload.intervalMs : 0;
|
|
931
|
+
let sent = 0;
|
|
932
|
+
|
|
933
|
+
for (const message of messages) {
|
|
934
|
+
if (message.delayMs && message.delayMs > 0) {
|
|
935
|
+
await sleep(message.delayMs);
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const outbound = await loadOutboundMedia({
|
|
939
|
+
payload: message,
|
|
940
|
+
account: target.account,
|
|
941
|
+
maxBytes: resolveMediaMaxBytes(target),
|
|
942
|
+
});
|
|
943
|
+
if (outbound) {
|
|
944
|
+
const mediaId = await uploadWecomMedia({
|
|
945
|
+
account: target.account,
|
|
946
|
+
type: outbound.type,
|
|
947
|
+
buffer: outbound.buffer,
|
|
948
|
+
filename: outbound.filename,
|
|
949
|
+
});
|
|
950
|
+
if (outbound.type === "image") {
|
|
951
|
+
await sendWecomImage({ account: target.account, toUser, chatId: chatId || undefined, mediaId });
|
|
952
|
+
} else if (outbound.type === "voice") {
|
|
953
|
+
await sendWecomVoice({ account: target.account, toUser, chatId: chatId || undefined, mediaId });
|
|
954
|
+
} else if (outbound.type === "video") {
|
|
955
|
+
await sendWecomVideo({
|
|
956
|
+
account: target.account,
|
|
957
|
+
toUser,
|
|
958
|
+
chatId: chatId || undefined,
|
|
959
|
+
mediaId,
|
|
960
|
+
title: message.title,
|
|
961
|
+
description: message.description,
|
|
962
|
+
});
|
|
963
|
+
} else {
|
|
964
|
+
await sendWecomFile({ account: target.account, toUser, chatId: chatId || undefined, mediaId });
|
|
965
|
+
}
|
|
966
|
+
sent += 1;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const text = markdownToWecomText(message.text ?? "");
|
|
970
|
+
if (text) {
|
|
971
|
+
await sendWecomText({ account: target.account, toUser, chatId: chatId || undefined, text });
|
|
972
|
+
sent += 1;
|
|
973
|
+
}
|
|
974
|
+
} catch (err) {
|
|
975
|
+
target.runtime.error?.(`wecom push failed: ${String(err)}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (intervalMs) {
|
|
979
|
+
await sleep(intervalMs);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
984
|
+
res.statusCode = 200;
|
|
985
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
986
|
+
res.end(JSON.stringify({ ok: true, sent }));
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
|
|
812
990
|
export async function handleWecomAppWebhook(params: {
|
|
813
991
|
req: IncomingMessage;
|
|
814
992
|
res: ServerResponse;
|