@yanhaidao/wecom 2.2.7 → 2.3.2

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.
Files changed (54) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/CLAUDE.md +1 -1
  3. package/GOVERNANCE.md +26 -0
  4. package/LICENSE +7 -0
  5. package/README.md +275 -91
  6. package/assets/01.bot-add.png +0 -0
  7. package/assets/01.bot-setp2.png +0 -0
  8. package/assets/02.agent.add.png +0 -0
  9. package/assets/02.agent.api-set.png +0 -0
  10. package/assets/register.png +0 -0
  11. package/changelog/v2.2.28.md +70 -0
  12. package/changelog/v2.3.2.md +70 -0
  13. package/compat-single-account.md +118 -0
  14. package/package.json +10 -2
  15. package/src/accounts.ts +17 -55
  16. package/src/agent/api-client.ts +84 -37
  17. package/src/agent/api-client.upload.test.ts +110 -0
  18. package/src/agent/handler.event-filter.test.ts +50 -0
  19. package/src/agent/handler.ts +147 -145
  20. package/src/channel.config.test.ts +147 -0
  21. package/src/channel.lifecycle.test.ts +234 -0
  22. package/src/channel.ts +90 -140
  23. package/src/config/accounts.resolve.test.ts +38 -0
  24. package/src/config/accounts.ts +257 -22
  25. package/src/config/index.ts +6 -0
  26. package/src/config/network.ts +9 -5
  27. package/src/config/routing.test.ts +88 -0
  28. package/src/config/routing.ts +26 -0
  29. package/src/config/schema.ts +35 -4
  30. package/src/config-schema.ts +5 -41
  31. package/src/dynamic-agent.account-scope.test.ts +17 -0
  32. package/src/dynamic-agent.ts +13 -13
  33. package/src/gateway-monitor.ts +200 -0
  34. package/src/http.ts +16 -2
  35. package/src/media.test.ts +28 -1
  36. package/src/media.ts +59 -1
  37. package/src/monitor/state.queue.test.ts +1 -1
  38. package/src/monitor/state.ts +1 -1
  39. package/src/monitor/types.ts +1 -1
  40. package/src/monitor.active.test.ts +13 -7
  41. package/src/monitor.inbound-filter.test.ts +63 -0
  42. package/src/monitor.ts +948 -128
  43. package/src/monitor.webhook.test.ts +288 -3
  44. package/src/outbound.test.ts +130 -0
  45. package/src/outbound.ts +44 -9
  46. package/src/shared/command-auth.ts +4 -2
  47. package/src/shared/xml-parser.test.ts +21 -1
  48. package/src/shared/xml-parser.ts +18 -0
  49. package/src/types/account.ts +43 -14
  50. package/src/types/config.ts +37 -2
  51. package/src/types/index.ts +3 -0
  52. package/src/types.ts +29 -147
  53. package/GEMINI.md +0 -76
  54. package//345/212/250/346/200/201Agent/350/267/257/347/224/261.md +0 -360
@@ -0,0 +1,234 @@
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
+ `&timestamp=${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 active = await sendWecomGetVerify({
182
+ path: "/wecom/bot",
183
+ token,
184
+ encodingAESKey,
185
+ receiveId,
186
+ });
187
+ expect(active.handled).toBe(true);
188
+ expect(active.status).toBe(200);
189
+ expect(active.body).toBe("ping");
190
+
191
+ abortController.abort();
192
+ await startPromise;
193
+
194
+ const inactive = await sendWecomGetVerify({
195
+ path: "/wecom/bot",
196
+ token,
197
+ encodingAESKey,
198
+ receiveId,
199
+ });
200
+ expect(inactive.handled).toBe(false);
201
+ });
202
+
203
+ it("rejects startup when matrix account credentials conflict", async () => {
204
+ const cfg = {
205
+ channels: {
206
+ wecom: {
207
+ enabled: true,
208
+ accounts: {
209
+ "acct-a": {
210
+ enabled: true,
211
+ bot: {
212
+ token: "token-shared",
213
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
214
+ },
215
+ },
216
+ "acct-b": {
217
+ enabled: true,
218
+ bot: {
219
+ token: "token-shared",
220
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ } as OpenClawConfig;
227
+ const abortController = new AbortController();
228
+ const ctx = createCtx({ cfg, accountId: "acct-b", abortController });
229
+
230
+ await expect(wecomPlugin.gateway!.startAccount!(ctx)).rejects.toThrow(
231
+ /Duplicate WeCom bot token/i,
232
+ );
233
+ });
234
+ });
package/src/channel.ts CHANGED
@@ -4,15 +4,19 @@ import type {
4
4
  OpenClawConfig,
5
5
  } from "openclaw/plugin-sdk";
6
6
  import {
7
- buildChannelConfigSchema,
8
- DEFAULT_ACCOUNT_ID,
7
+ deleteAccountFromConfigSection,
9
8
  setAccountEnabledInConfigSection,
10
9
  } from "openclaw/plugin-sdk";
11
10
 
12
- import { resolveWecomAccounts } from "./config/index.js";
13
- import { WecomConfigSchema } from "./config/index.js";
14
- import type { ResolvedAgentAccount, ResolvedBotAccount } from "./types/index.js";
15
- import { registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
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";
18
22
 
@@ -34,36 +38,6 @@ function normalizeWecomMessagingTarget(raw: string): string | undefined {
34
38
  return trimmed.replace(/^(wecom-agent|wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
35
39
  }
36
40
 
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
41
  export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
68
42
  id: "wecom",
69
43
  meta,
@@ -78,11 +52,21 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
78
52
  blockStreaming: true,
79
53
  },
80
54
  reload: { configPrefixes: ["channels.wecom"] },
81
- configSchema: buildChannelConfigSchema(WecomConfigSchema),
55
+ // NOTE: We intentionally avoid Zod -> JSON Schema conversion at plugin-load time.
56
+ // Some OpenClaw runtime environments load plugin modules via jiti in a way that can
57
+ // surface zod `toJSONSchema()` binding issues (e.g. `this` undefined leading to `_zod` errors).
58
+ // A permissive schema keeps config UX working while preventing startup failures.
59
+ configSchema: {
60
+ schema: {
61
+ type: "object",
62
+ additionalProperties: true,
63
+ properties: {},
64
+ },
65
+ },
82
66
  config: {
83
- listAccountIds: () => [DEFAULT_ACCOUNT_ID],
84
- resolveAccount: (cfg) => resolveWecomAccount(cfg as OpenClawConfig),
85
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
67
+ listAccountIds: (cfg) => listWecomAccountIds(cfg as OpenClawConfig),
68
+ resolveAccount: (cfg, accountId) => resolveWecomAccount({ cfg: cfg as OpenClawConfig, accountId }),
69
+ defaultAccountId: (cfg) => resolveDefaultWecomAccountId(cfg as OpenClawConfig),
86
70
  setAccountEnabled: ({ cfg, accountId, enabled }) =>
87
71
  setAccountEnabledInConfigSection({
88
72
  cfg: cfg as OpenClawConfig,
@@ -91,25 +75,47 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
91
75
  enabled,
92
76
  allowTopLevel: true,
93
77
  }),
94
- deleteAccount: ({ cfg }) => {
95
- const next = { ...(cfg as OpenClawConfig) };
96
- if (next.channels?.wecom) {
97
- const channels = { ...(next.channels ?? {}) } as Record<string, unknown>;
98
- delete (channels as Record<string, unknown>).wecom;
99
- return { ...next, channels } as OpenClawConfig;
78
+ deleteAccount: ({ cfg, accountId }) =>
79
+ deleteAccountFromConfigSection({
80
+ cfg: cfg as OpenClawConfig,
81
+ sectionKey: "wecom",
82
+ accountId,
83
+ clearBaseFields: ["bot", "agent"],
84
+ }),
85
+ isConfigured: (account, cfg) => {
86
+ if (!account.configured) {
87
+ return false;
100
88
  }
101
- return next;
89
+ return !resolveWecomAccountConflict({
90
+ cfg: cfg as OpenClawConfig,
91
+ accountId: account.accountId,
92
+ });
93
+ },
94
+ unconfiguredReason: (account, cfg) =>
95
+ resolveWecomAccountConflict({
96
+ cfg: cfg as OpenClawConfig,
97
+ accountId: account.accountId,
98
+ })?.message ?? "not configured",
99
+ describeAccount: (account, cfg): ChannelAccountSnapshot => {
100
+ const matrixMode = account.accountId !== DEFAULT_ACCOUNT_ID;
101
+ const conflict = resolveWecomAccountConflict({
102
+ cfg: cfg as OpenClawConfig,
103
+ accountId: account.accountId,
104
+ });
105
+ return {
106
+ accountId: account.accountId,
107
+ name: account.name,
108
+ enabled: account.enabled,
109
+ configured: account.configured && !conflict,
110
+ webhookPath: account.bot?.config
111
+ ? (matrixMode ? `/wecom/bot/${account.accountId}` : "/wecom/bot")
112
+ : account.agent?.config
113
+ ? (matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent")
114
+ : "/wecom",
115
+ };
102
116
  },
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
117
  resolveAllowFrom: ({ cfg, accountId }) => {
112
- const account = resolveWecomAccount(cfg as OpenClawConfig);
118
+ const account = resolveWecomAccount({ cfg: cfg as OpenClawConfig, accountId });
113
119
  // 与其他渠道保持一致:直接返回 allowFrom,空则允许所有人
114
120
  const allowFrom = account.agent?.config.dm?.allowFrom ?? account.bot?.config.dm?.allowFrom ?? [];
115
121
  return allowFrom.map((entry) => String(entry));
@@ -159,95 +165,39 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
159
165
  lastProbeAt: snapshot.lastProbeAt ?? null,
160
166
  }),
161
167
  probeAccount: async () => ({ ok: true }),
162
- buildAccountSnapshot: ({ account, runtime }) => ({
163
- accountId: account.accountId,
164
- name: account.name,
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({
168
+ buildAccountSnapshot: ({ account, runtime, cfg }) => {
169
+ const conflict = resolveWecomAccountConflict({
170
+ cfg: cfg as OpenClawConfig,
232
171
  accountId: account.accountId,
233
- running: true,
234
- configured: true,
235
- webhookPath: botConfigured ? "/wecom/bot" : "/wecom/agent",
236
- lastStartAt: Date.now(),
237
172
  });
238
173
  return {
239
- stop: () => {
240
- for (const unregister of unregisters) {
241
- unregister();
242
- }
243
- ctx.setStatus({
244
- accountId: account.accountId,
245
- running: false,
246
- lastStopAt: Date.now(),
247
- });
248
- },
174
+ accountId: account.accountId,
175
+ name: account.name,
176
+ enabled: account.enabled,
177
+ configured: account.configured && !conflict,
178
+ webhookPath: account.bot?.config
179
+ ? (account.accountId === DEFAULT_ACCOUNT_ID ? "/wecom/bot" : `/wecom/bot/${account.accountId}`)
180
+ : account.agent?.config
181
+ ? (account.accountId === DEFAULT_ACCOUNT_ID ? "/wecom/agent" : `/wecom/agent/${account.accountId}`)
182
+ : "/wecom",
183
+ running: runtime?.running ?? false,
184
+ lastStartAt: runtime?.lastStartAt ?? null,
185
+ lastStopAt: runtime?.lastStopAt ?? null,
186
+ lastError: runtime?.lastError ?? conflict?.message ?? null,
187
+ lastInboundAt: runtime?.lastInboundAt ?? null,
188
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
189
+ dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
249
190
  };
250
191
  },
192
+ },
193
+ gateway: {
194
+ /**
195
+ * **startAccount (启动账号)**
196
+ *
197
+ * WeCom lifecycle is long-running: keep webhook targets active until
198
+ * gateway stop/reload aborts the account.
199
+ */
200
+ startAccount: monitorWecomProvider,
251
201
  stopAccount: async (ctx) => {
252
202
  ctx.setStatus({
253
203
  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
+ });