@mocrane/wecom 2026.2.27 → 2026.3.4
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/LICENSE +4 -18
- package/README.md +572 -0
- package/assets/01.bot-add.png +0 -0
- package/assets/01.bot-setp2.png +0 -0
- package/assets/02.agent.add.png +0 -0
- package/assets/02.agent.api-set.png +0 -0
- package/assets/register.png +0 -0
- package/changelog/v2.2.28.md +70 -0
- package/changelog/v2.3.2.md +28 -0
- package/changelog/v2.3.4.md +20 -0
- package/index.ts +11 -3
- package/package.json +3 -2
- package/src/accounts.ts +17 -55
- package/src/agent/api-client.ts +84 -37
- package/src/agent/api-client.upload.test.ts +110 -0
- package/src/agent/handler.event-filter.test.ts +50 -0
- package/src/agent/handler.ts +147 -145
- package/src/channel.config.test.ts +147 -0
- package/src/channel.lifecycle.test.ts +252 -0
- package/src/channel.ts +95 -140
- package/src/config/accounts.resolve.test.ts +38 -0
- package/src/config/accounts.ts +257 -22
- package/src/config/index.ts +6 -0
- package/src/config/network.ts +9 -5
- package/src/config/routing.test.ts +88 -0
- package/src/config/routing.ts +26 -0
- package/src/config/schema.ts +35 -4
- package/src/config-schema.ts +5 -41
- package/src/dynamic-agent.account-scope.test.ts +17 -0
- package/src/dynamic-agent.ts +13 -13
- package/src/gateway-monitor.ts +238 -0
- package/src/http.ts +16 -2
- package/src/media.test.ts +28 -1
- package/src/media.ts +59 -1
- package/src/monitor/state.queue.test.ts +1 -1
- package/src/monitor/state.ts +1 -1
- package/src/monitor/types.ts +1 -1
- package/src/monitor.active.test.ts +15 -9
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +970 -128
- package/src/monitor.webhook.test.ts +381 -3
- package/src/onboarding.ts +229 -53
- package/src/outbound.test.ts +130 -0
- package/src/outbound.ts +44 -9
- package/src/shared/command-auth.ts +4 -2
- package/src/shared/xml-parser.test.ts +21 -1
- package/src/shared/xml-parser.ts +18 -0
- package/src/types/account.ts +43 -14
- package/src/types/config.ts +37 -2
- package/src/types/constants.ts +7 -3
- package/src/types/index.ts +3 -0
- package/src/types.ts +29 -147
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { Socket } from "node:net";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type ChannelAccountSnapshot,
|
|
6
|
+
type ChannelGatewayContext,
|
|
7
|
+
type OpenClawConfig,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
|
|
12
|
+
import { computeWecomMsgSignature, encryptWecomPlaintext } from "./crypto.js";
|
|
13
|
+
import { wecomPlugin } from "./channel.js";
|
|
14
|
+
import { handleWecomWebhookRequest } from "./monitor.js";
|
|
15
|
+
import type { ResolvedWecomAccount } from "./types/index.js";
|
|
16
|
+
|
|
17
|
+
function createMockRequest(params: {
|
|
18
|
+
method: "GET" | "POST";
|
|
19
|
+
url: string;
|
|
20
|
+
body?: unknown;
|
|
21
|
+
}): IncomingMessage {
|
|
22
|
+
const socket = new Socket();
|
|
23
|
+
const req = new IncomingMessage(socket);
|
|
24
|
+
req.method = params.method;
|
|
25
|
+
req.url = params.url;
|
|
26
|
+
if (params.method === "POST") {
|
|
27
|
+
req.push(JSON.stringify(params.body ?? {}));
|
|
28
|
+
}
|
|
29
|
+
req.push(null);
|
|
30
|
+
return req;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createMockResponse(): ServerResponse & {
|
|
34
|
+
_getData: () => string;
|
|
35
|
+
_getStatusCode: () => number;
|
|
36
|
+
} {
|
|
37
|
+
type MockResponse = ServerResponse & {
|
|
38
|
+
_getData: () => string;
|
|
39
|
+
_getStatusCode: () => number;
|
|
40
|
+
};
|
|
41
|
+
const req = new IncomingMessage(new Socket());
|
|
42
|
+
const res = new ServerResponse(req) as MockResponse;
|
|
43
|
+
let data = "";
|
|
44
|
+
res.write = (chunk: string | Uint8Array) => {
|
|
45
|
+
data += String(chunk);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
res.end = ((chunk?: string | Uint8Array) => {
|
|
49
|
+
if (chunk) data += String(chunk);
|
|
50
|
+
return res;
|
|
51
|
+
}) as MockResponse["end"];
|
|
52
|
+
res._getData = () => data;
|
|
53
|
+
res._getStatusCode = () => res.statusCode;
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createCtx(params: {
|
|
58
|
+
cfg: OpenClawConfig;
|
|
59
|
+
accountId?: string;
|
|
60
|
+
abortController: AbortController;
|
|
61
|
+
}): ChannelGatewayContext<ResolvedWecomAccount> & {
|
|
62
|
+
statusUpdates: Array<Partial<ChannelAccountSnapshot>>;
|
|
63
|
+
} {
|
|
64
|
+
const accountId = params.accountId ?? "default";
|
|
65
|
+
const account = wecomPlugin.config.resolveAccount(
|
|
66
|
+
params.cfg,
|
|
67
|
+
accountId,
|
|
68
|
+
) as ResolvedWecomAccount;
|
|
69
|
+
const snapshot: ChannelAccountSnapshot = {
|
|
70
|
+
accountId,
|
|
71
|
+
configured: true,
|
|
72
|
+
enabled: true,
|
|
73
|
+
running: false,
|
|
74
|
+
};
|
|
75
|
+
const statusUpdates: Array<Partial<ChannelAccountSnapshot>> = [];
|
|
76
|
+
return {
|
|
77
|
+
cfg: params.cfg,
|
|
78
|
+
accountId,
|
|
79
|
+
account,
|
|
80
|
+
runtime: createRuntimeEnv(),
|
|
81
|
+
abortSignal: params.abortController.signal,
|
|
82
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
83
|
+
getStatus: () => snapshot,
|
|
84
|
+
setStatus: (next) => {
|
|
85
|
+
statusUpdates.push(next);
|
|
86
|
+
Object.assign(snapshot, next);
|
|
87
|
+
},
|
|
88
|
+
statusUpdates,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createLegacyBotConfig(params: {
|
|
93
|
+
token: string;
|
|
94
|
+
encodingAESKey: string;
|
|
95
|
+
receiveId?: string;
|
|
96
|
+
}): OpenClawConfig {
|
|
97
|
+
return {
|
|
98
|
+
channels: {
|
|
99
|
+
wecom: {
|
|
100
|
+
enabled: true,
|
|
101
|
+
bot: {
|
|
102
|
+
token: params.token,
|
|
103
|
+
encodingAESKey: params.encodingAESKey,
|
|
104
|
+
receiveId: params.receiveId ?? "",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
} as OpenClawConfig;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function sendWecomGetVerify(params: {
|
|
112
|
+
path: string;
|
|
113
|
+
token: string;
|
|
114
|
+
encodingAESKey: string;
|
|
115
|
+
receiveId: string;
|
|
116
|
+
}): Promise<{ handled: boolean; status: number; body: string }> {
|
|
117
|
+
const timestamp = "1700000000";
|
|
118
|
+
const nonce = "nonce";
|
|
119
|
+
const echostr = encryptWecomPlaintext({
|
|
120
|
+
encodingAESKey: params.encodingAESKey,
|
|
121
|
+
receiveId: params.receiveId,
|
|
122
|
+
plaintext: "ping",
|
|
123
|
+
});
|
|
124
|
+
const msgSignature = computeWecomMsgSignature({
|
|
125
|
+
token: params.token,
|
|
126
|
+
timestamp,
|
|
127
|
+
nonce,
|
|
128
|
+
encrypt: echostr,
|
|
129
|
+
});
|
|
130
|
+
const req = createMockRequest({
|
|
131
|
+
method: "GET",
|
|
132
|
+
url:
|
|
133
|
+
`${params.path}?msg_signature=${encodeURIComponent(msgSignature)}` +
|
|
134
|
+
`×tamp=${encodeURIComponent(timestamp)}` +
|
|
135
|
+
`&nonce=${encodeURIComponent(nonce)}` +
|
|
136
|
+
`&echostr=${encodeURIComponent(echostr)}`,
|
|
137
|
+
});
|
|
138
|
+
const res = createMockResponse();
|
|
139
|
+
const handled = await handleWecomWebhookRequest(req, res);
|
|
140
|
+
return {
|
|
141
|
+
handled,
|
|
142
|
+
status: res._getStatusCode(),
|
|
143
|
+
body: res._getData(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("wecomPlugin gateway lifecycle", () => {
|
|
148
|
+
it("keeps startAccount pending until abort signal", async () => {
|
|
149
|
+
const token = "token";
|
|
150
|
+
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
151
|
+
const cfg = createLegacyBotConfig({ token, encodingAESKey });
|
|
152
|
+
const abortController = new AbortController();
|
|
153
|
+
const ctx = createCtx({ cfg, abortController });
|
|
154
|
+
|
|
155
|
+
const startPromise = wecomPlugin.gateway!.startAccount!(ctx);
|
|
156
|
+
let resolved = false;
|
|
157
|
+
void startPromise.then(() => {
|
|
158
|
+
resolved = true;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await Promise.resolve();
|
|
162
|
+
await Promise.resolve();
|
|
163
|
+
expect(resolved).toBe(false);
|
|
164
|
+
|
|
165
|
+
abortController.abort();
|
|
166
|
+
await startPromise;
|
|
167
|
+
expect(resolved).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("unregisters webhook targets after abort", async () => {
|
|
171
|
+
const token = "token";
|
|
172
|
+
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
173
|
+
const receiveId = "";
|
|
174
|
+
const cfg = createLegacyBotConfig({ token, encodingAESKey, receiveId });
|
|
175
|
+
const abortController = new AbortController();
|
|
176
|
+
const ctx = createCtx({ cfg, abortController });
|
|
177
|
+
|
|
178
|
+
const startPromise = wecomPlugin.gateway!.startAccount!(ctx);
|
|
179
|
+
await Promise.resolve();
|
|
180
|
+
|
|
181
|
+
const activeLegacyRoute = await sendWecomGetVerify({
|
|
182
|
+
path: "/wecom/bot",
|
|
183
|
+
token,
|
|
184
|
+
encodingAESKey,
|
|
185
|
+
receiveId,
|
|
186
|
+
});
|
|
187
|
+
expect(activeLegacyRoute.handled).toBe(true);
|
|
188
|
+
expect(activeLegacyRoute.status).toBe(200);
|
|
189
|
+
expect(activeLegacyRoute.body).toBe("ping");
|
|
190
|
+
|
|
191
|
+
const activePluginRoute = await sendWecomGetVerify({
|
|
192
|
+
path: "/plugins/wecom/bot",
|
|
193
|
+
token,
|
|
194
|
+
encodingAESKey,
|
|
195
|
+
receiveId,
|
|
196
|
+
});
|
|
197
|
+
expect(activePluginRoute.handled).toBe(true);
|
|
198
|
+
expect(activePluginRoute.status).toBe(200);
|
|
199
|
+
expect(activePluginRoute.body).toBe("ping");
|
|
200
|
+
|
|
201
|
+
abortController.abort();
|
|
202
|
+
await startPromise;
|
|
203
|
+
|
|
204
|
+
const inactiveLegacyRoute = await sendWecomGetVerify({
|
|
205
|
+
path: "/wecom/bot",
|
|
206
|
+
token,
|
|
207
|
+
encodingAESKey,
|
|
208
|
+
receiveId,
|
|
209
|
+
});
|
|
210
|
+
expect(inactiveLegacyRoute.handled).toBe(false);
|
|
211
|
+
|
|
212
|
+
const inactivePluginRoute = await sendWecomGetVerify({
|
|
213
|
+
path: "/plugins/wecom/bot",
|
|
214
|
+
token,
|
|
215
|
+
encodingAESKey,
|
|
216
|
+
receiveId,
|
|
217
|
+
});
|
|
218
|
+
expect(inactivePluginRoute.handled).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("rejects startup when matrix account credentials conflict", async () => {
|
|
222
|
+
const cfg = {
|
|
223
|
+
channels: {
|
|
224
|
+
wecom: {
|
|
225
|
+
enabled: true,
|
|
226
|
+
accounts: {
|
|
227
|
+
"acct-a": {
|
|
228
|
+
enabled: true,
|
|
229
|
+
bot: {
|
|
230
|
+
token: "token-shared",
|
|
231
|
+
encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
"acct-b": {
|
|
235
|
+
enabled: true,
|
|
236
|
+
bot: {
|
|
237
|
+
token: "token-shared",
|
|
238
|
+
encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
} as OpenClawConfig;
|
|
245
|
+
const abortController = new AbortController();
|
|
246
|
+
const ctx = createCtx({ cfg, accountId: "acct-b", abortController });
|
|
247
|
+
|
|
248
|
+
await expect(wecomPlugin.gateway!.startAccount!(ctx)).rejects.toThrow(
|
|
249
|
+
/Duplicate WeCom bot token/i,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
package/src/channel.ts
CHANGED
|
@@ -4,17 +4,22 @@ import type {
|
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
} from "openclaw/plugin-sdk";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
DEFAULT_ACCOUNT_ID,
|
|
7
|
+
deleteAccountFromConfigSection,
|
|
9
8
|
setAccountEnabledInConfigSection,
|
|
10
9
|
} from "openclaw/plugin-sdk";
|
|
11
10
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_ACCOUNT_ID,
|
|
13
|
+
listWecomAccountIds,
|
|
14
|
+
resolveDefaultWecomAccountId,
|
|
15
|
+
resolveWecomAccount,
|
|
16
|
+
resolveWecomAccountConflict,
|
|
17
|
+
} from "./config/index.js";
|
|
18
|
+
import type { ResolvedWecomAccount } from "./types/index.js";
|
|
19
|
+
import { monitorWecomProvider } from "./gateway-monitor.js";
|
|
16
20
|
import { wecomOnboardingAdapter } from "./onboarding.js";
|
|
17
21
|
import { wecomOutbound } from "./outbound.js";
|
|
22
|
+
import { WEBHOOK_PATHS } from "./types/constants.js";
|
|
18
23
|
|
|
19
24
|
const meta = {
|
|
20
25
|
id: "wecom",
|
|
@@ -34,36 +39,6 @@ function normalizeWecomMessagingTarget(raw: string): string | undefined {
|
|
|
34
39
|
return trimmed.replace(/^(wecom-agent|wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
type ResolvedWecomAccount = {
|
|
38
|
-
accountId: string;
|
|
39
|
-
name?: string;
|
|
40
|
-
enabled: boolean;
|
|
41
|
-
configured: boolean;
|
|
42
|
-
bot?: ResolvedBotAccount;
|
|
43
|
-
agent?: ResolvedAgentAccount;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* **resolveWecomAccount (解析账号配置)**
|
|
48
|
-
*
|
|
49
|
-
* 从全局配置中解析出 WeCom 渠道的配置状态。
|
|
50
|
-
* 兼容 Bot 和 Agent 两种模式的配置检查。
|
|
51
|
-
*/
|
|
52
|
-
function resolveWecomAccount(cfg: OpenClawConfig): ResolvedWecomAccount {
|
|
53
|
-
const enabled = (cfg.channels?.wecom as { enabled?: boolean } | undefined)?.enabled !== false;
|
|
54
|
-
const accounts = resolveWecomAccounts(cfg);
|
|
55
|
-
const bot = accounts.bot;
|
|
56
|
-
const agent = accounts.agent;
|
|
57
|
-
const configured = Boolean(bot?.configured || agent?.configured);
|
|
58
|
-
return {
|
|
59
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
60
|
-
enabled,
|
|
61
|
-
configured,
|
|
62
|
-
bot,
|
|
63
|
-
agent,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
42
|
export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
68
43
|
id: "wecom",
|
|
69
44
|
meta,
|
|
@@ -78,11 +53,21 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
78
53
|
blockStreaming: true,
|
|
79
54
|
},
|
|
80
55
|
reload: { configPrefixes: ["channels.wecom"] },
|
|
81
|
-
|
|
56
|
+
// NOTE: We intentionally avoid Zod -> JSON Schema conversion at plugin-load time.
|
|
57
|
+
// Some OpenClaw runtime environments load plugin modules via jiti in a way that can
|
|
58
|
+
// surface zod `toJSONSchema()` binding issues (e.g. `this` undefined leading to `_zod` errors).
|
|
59
|
+
// A permissive schema keeps config UX working while preventing startup failures.
|
|
60
|
+
configSchema: {
|
|
61
|
+
schema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
additionalProperties: true,
|
|
64
|
+
properties: {},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
82
67
|
config: {
|
|
83
|
-
listAccountIds: () =>
|
|
84
|
-
resolveAccount: (cfg) => resolveWecomAccount(cfg as OpenClawConfig),
|
|
85
|
-
defaultAccountId: () =>
|
|
68
|
+
listAccountIds: (cfg) => listWecomAccountIds(cfg as OpenClawConfig),
|
|
69
|
+
resolveAccount: (cfg, accountId) => resolveWecomAccount({ cfg: cfg as OpenClawConfig, accountId }),
|
|
70
|
+
defaultAccountId: (cfg) => resolveDefaultWecomAccountId(cfg as OpenClawConfig),
|
|
86
71
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
87
72
|
setAccountEnabledInConfigSection({
|
|
88
73
|
cfg: cfg as OpenClawConfig,
|
|
@@ -91,25 +76,47 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
91
76
|
enabled,
|
|
92
77
|
allowTopLevel: true,
|
|
93
78
|
}),
|
|
94
|
-
deleteAccount: ({ cfg }) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
deleteAccount: ({ cfg, accountId }) =>
|
|
80
|
+
deleteAccountFromConfigSection({
|
|
81
|
+
cfg: cfg as OpenClawConfig,
|
|
82
|
+
sectionKey: "wecom",
|
|
83
|
+
accountId,
|
|
84
|
+
clearBaseFields: ["bot", "agent"],
|
|
85
|
+
}),
|
|
86
|
+
isConfigured: (account, cfg) => {
|
|
87
|
+
if (!account.configured) {
|
|
88
|
+
return false;
|
|
100
89
|
}
|
|
101
|
-
return
|
|
90
|
+
return !resolveWecomAccountConflict({
|
|
91
|
+
cfg: cfg as OpenClawConfig,
|
|
92
|
+
accountId: account.accountId,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
unconfiguredReason: (account, cfg) =>
|
|
96
|
+
resolveWecomAccountConflict({
|
|
97
|
+
cfg: cfg as OpenClawConfig,
|
|
98
|
+
accountId: account.accountId,
|
|
99
|
+
})?.message ?? "not configured",
|
|
100
|
+
describeAccount: (account, cfg): ChannelAccountSnapshot => {
|
|
101
|
+
const matrixMode = account.accountId !== DEFAULT_ACCOUNT_ID;
|
|
102
|
+
const conflict = resolveWecomAccountConflict({
|
|
103
|
+
cfg: cfg as OpenClawConfig,
|
|
104
|
+
accountId: account.accountId,
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
accountId: account.accountId,
|
|
108
|
+
name: account.name,
|
|
109
|
+
enabled: account.enabled,
|
|
110
|
+
configured: account.configured && !conflict,
|
|
111
|
+
webhookPath: account.bot?.config
|
|
112
|
+
? (matrixMode ? `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.BOT_PLUGIN)
|
|
113
|
+
: account.agent?.config
|
|
114
|
+
? (matrixMode ? `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.AGENT_PLUGIN)
|
|
115
|
+
: WEBHOOK_PATHS.BOT_PLUGIN,
|
|
116
|
+
};
|
|
102
117
|
},
|
|
103
|
-
isConfigured: (account) => account.configured,
|
|
104
|
-
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
105
|
-
accountId: account.accountId,
|
|
106
|
-
name: account.name,
|
|
107
|
-
enabled: account.enabled,
|
|
108
|
-
configured: account.configured,
|
|
109
|
-
webhookPath: account.bot?.config ? "/wecom/bot" : account.agent?.config ? "/wecom/agent" : "/wecom",
|
|
110
|
-
}),
|
|
111
118
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
|
112
|
-
const account = resolveWecomAccount(cfg as OpenClawConfig);
|
|
119
|
+
const account = resolveWecomAccount({ cfg: cfg as OpenClawConfig, accountId });
|
|
113
120
|
// 与其他渠道保持一致:直接返回 allowFrom,空则允许所有人
|
|
114
121
|
const allowFrom = account.agent?.config.dm?.allowFrom ?? account.bot?.config.dm?.allowFrom ?? [];
|
|
115
122
|
return allowFrom.map((entry) => String(entry));
|
|
@@ -159,95 +166,43 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
|
|
|
159
166
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
160
167
|
}),
|
|
161
168
|
probeAccount: async () => ({ ok: true }),
|
|
162
|
-
buildAccountSnapshot: ({ account, runtime }) =>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
enabled: account.enabled,
|
|
166
|
-
configured: account.configured,
|
|
167
|
-
webhookPath: account.bot?.config ? "/wecom/bot" : account.agent?.config ? "/wecom/agent" : "/wecom",
|
|
168
|
-
running: runtime?.running ?? false,
|
|
169
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
170
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
171
|
-
lastError: runtime?.lastError ?? null,
|
|
172
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
173
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
174
|
-
dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
|
|
175
|
-
}),
|
|
176
|
-
},
|
|
177
|
-
gateway: {
|
|
178
|
-
/**
|
|
179
|
-
* **startAccount (启动账号)**
|
|
180
|
-
*
|
|
181
|
-
* 插件生命周期:启动
|
|
182
|
-
* 职责:
|
|
183
|
-
* 1. 检查配置是否有效。
|
|
184
|
-
* 2. 注册 Bot Webhook (`/wecom`, `/wecom/bot`)。
|
|
185
|
-
* 3. 注册 Agent Webhook (`/wecom/agent`)。
|
|
186
|
-
* 4. 更新运行时状态 (Running)。
|
|
187
|
-
* 5. 返回停止回调 (Cleanup)。
|
|
188
|
-
*/
|
|
189
|
-
startAccount: async (ctx) => {
|
|
190
|
-
const account = ctx.account;
|
|
191
|
-
const bot = account.bot;
|
|
192
|
-
const agent = account.agent;
|
|
193
|
-
const botConfigured = Boolean(bot?.configured);
|
|
194
|
-
const agentConfigured = Boolean(agent?.configured);
|
|
195
|
-
|
|
196
|
-
if (!botConfigured && !agentConfigured) {
|
|
197
|
-
ctx.log?.warn(`[${account.accountId}] wecom not configured; skipping webhook registration`);
|
|
198
|
-
ctx.setStatus({ accountId: account.accountId, running: false, configured: false });
|
|
199
|
-
return { stop: () => { } };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const unregisters: Array<() => void> = [];
|
|
203
|
-
if (bot && botConfigured) {
|
|
204
|
-
for (const path of ["/wecom", "/wecom/bot"]) {
|
|
205
|
-
unregisters.push(
|
|
206
|
-
registerWecomWebhookTarget({
|
|
207
|
-
account: bot,
|
|
208
|
-
config: ctx.cfg as OpenClawConfig,
|
|
209
|
-
runtime: ctx.runtime,
|
|
210
|
-
// The HTTP handler resolves the active PluginRuntime via getWecomRuntime().
|
|
211
|
-
// The stored target only needs to be decrypt/verify-capable.
|
|
212
|
-
core: ({} as unknown) as any,
|
|
213
|
-
path,
|
|
214
|
-
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
215
|
-
}),
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
ctx.log?.info(`[${account.accountId}] wecom bot webhook registered at /wecom and /wecom/bot`);
|
|
219
|
-
}
|
|
220
|
-
if (agent && agentConfigured) {
|
|
221
|
-
unregisters.push(
|
|
222
|
-
registerAgentWebhookTarget({
|
|
223
|
-
agent,
|
|
224
|
-
config: ctx.cfg as OpenClawConfig,
|
|
225
|
-
runtime: ctx.runtime,
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at /wecom/agent`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
ctx.setStatus({
|
|
169
|
+
buildAccountSnapshot: ({ account, runtime, cfg }) => {
|
|
170
|
+
const conflict = resolveWecomAccountConflict({
|
|
171
|
+
cfg: cfg as OpenClawConfig,
|
|
232
172
|
accountId: account.accountId,
|
|
233
|
-
running: true,
|
|
234
|
-
configured: true,
|
|
235
|
-
webhookPath: botConfigured ? "/wecom/bot" : "/wecom/agent",
|
|
236
|
-
lastStartAt: Date.now(),
|
|
237
173
|
});
|
|
238
174
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
175
|
+
accountId: account.accountId,
|
|
176
|
+
name: account.name,
|
|
177
|
+
enabled: account.enabled,
|
|
178
|
+
configured: account.configured && !conflict,
|
|
179
|
+
webhookPath: account.bot?.config
|
|
180
|
+
? (account.accountId === DEFAULT_ACCOUNT_ID
|
|
181
|
+
? WEBHOOK_PATHS.BOT_PLUGIN
|
|
182
|
+
: `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}`)
|
|
183
|
+
: account.agent?.config
|
|
184
|
+
? (account.accountId === DEFAULT_ACCOUNT_ID
|
|
185
|
+
? WEBHOOK_PATHS.AGENT_PLUGIN
|
|
186
|
+
: `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}`)
|
|
187
|
+
: WEBHOOK_PATHS.BOT_PLUGIN,
|
|
188
|
+
running: runtime?.running ?? false,
|
|
189
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
190
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
191
|
+
lastError: runtime?.lastError ?? conflict?.message ?? null,
|
|
192
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
193
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
194
|
+
dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
|
|
249
195
|
};
|
|
250
196
|
},
|
|
197
|
+
},
|
|
198
|
+
gateway: {
|
|
199
|
+
/**
|
|
200
|
+
* **startAccount (启动账号)**
|
|
201
|
+
*
|
|
202
|
+
* WeCom lifecycle is long-running: keep webhook targets active until
|
|
203
|
+
* gateway stop/reload aborts the account.
|
|
204
|
+
*/
|
|
205
|
+
startAccount: monitorWecomProvider,
|
|
251
206
|
stopAccount: async (ctx) => {
|
|
252
207
|
ctx.setStatus({
|
|
253
208
|
accountId: ctx.account.accountId,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { resolveWecomAccount } from "./accounts.js";
|
|
5
|
+
|
|
6
|
+
describe("resolveWecomAccount", () => {
|
|
7
|
+
const cfg: OpenClawConfig = {
|
|
8
|
+
channels: {
|
|
9
|
+
wecom: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
defaultAccount: "acct-a",
|
|
12
|
+
accounts: {
|
|
13
|
+
"acct-a": {
|
|
14
|
+
enabled: true,
|
|
15
|
+
bot: {
|
|
16
|
+
token: "token-a",
|
|
17
|
+
encodingAESKey: "aes-a",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as OpenClawConfig;
|
|
24
|
+
|
|
25
|
+
it("does not fall back when explicit accountId does not exist", () => {
|
|
26
|
+
const account = resolveWecomAccount({ cfg, accountId: "missing" });
|
|
27
|
+
expect(account.accountId).toBe("missing");
|
|
28
|
+
expect(account.enabled).toBe(false);
|
|
29
|
+
expect(account.configured).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("uses configured default account when accountId is omitted", () => {
|
|
33
|
+
const account = resolveWecomAccount({ cfg });
|
|
34
|
+
expect(account.accountId).toBe("acct-a");
|
|
35
|
+
expect(account.enabled).toBe(true);
|
|
36
|
+
expect(account.configured).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|