@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.
- package/.github/workflows/release.yml +56 -0
- package/CLAUDE.md +1 -1
- package/GOVERNANCE.md +26 -0
- package/LICENSE +7 -0
- package/README.md +275 -91
- 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 +70 -0
- package/compat-single-account.md +118 -0
- package/package.json +10 -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 +234 -0
- package/src/channel.ts +90 -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 +200 -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 +13 -7
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.ts +948 -128
- package/src/monitor.webhook.test.ts +288 -3
- 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/index.ts +3 -0
- package/src/types.ts +29 -147
- package/GEMINI.md +0 -76
- 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
|
+
`×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 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
|
-
|
|
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";
|
|
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
|
-
|
|
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: () =>
|
|
84
|
-
resolveAccount: (cfg) => resolveWecomAccount(cfg as OpenClawConfig),
|
|
85
|
-
defaultAccountId: () =>
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
});
|