@junjiezhang/openclaw-wecom-plugin 1.0.0 → 1.0.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/src/channel.ts DELETED
@@ -1,278 +0,0 @@
1
- import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import {
3
- buildBaseChannelStatusSummary,
4
- createDefaultChannelRuntimeState,
5
- DEFAULT_ACCOUNT_ID,
6
- PAIRING_APPROVED_MESSAGE,
7
- } from "openclaw/plugin-sdk";
8
- import {
9
- resolveWeComAccount,
10
- listWeComAccountIds,
11
- resolveDefaultWeComAccountId,
12
- } from "./accounts.js";
13
- import { wecomOutbound } from "./outbound.js";
14
- import { resolveWeComGroupToolPolicy } from "./policy.js";
15
- import { probeWeCom } from "./probe.js";
16
- import { sendMessageWeCom } from "./send.js";
17
- import { normalizeWeComTarget, looksLikeWeComId } from "./targets.js";
18
- import type { ResolvedWeComAccount, WeComConfig } from "./types.js";
19
-
20
- const meta: ChannelMeta = {
21
- id: "wecom",
22
- label: "WeCom",
23
- selectionLabel: "WeCom (企业微信)",
24
- docsPath: "/channels/wecom",
25
- docsLabel: "wecom",
26
- blurb: "企业微信 enterprise messaging.",
27
- aliases: ["wechat-work"],
28
- order: 75,
29
- };
30
-
31
- export const wecomPlugin: ChannelPlugin<ResolvedWeComAccount> = {
32
- id: "wecom",
33
- meta: {
34
- ...meta,
35
- },
36
- pairing: {
37
- idLabel: "wecomUserId",
38
- normalizeAllowEntry: (entry) => entry.replace(/^(wecom|user):/i, ""),
39
- notifyApproval: async ({ cfg, id }) => {
40
- await sendMessageWeCom({
41
- cfg,
42
- to: id,
43
- text: PAIRING_APPROVED_MESSAGE,
44
- });
45
- },
46
- },
47
- capabilities: {
48
- chatTypes: ["direct", "channel"],
49
- polls: false,
50
- threads: false,
51
- media: true,
52
- reactions: false,
53
- edit: false,
54
- reply: false,
55
- },
56
- agentPrompt: {
57
- messageToolHints: () => [
58
- "- WeCom targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:userid`.",
59
- ],
60
- },
61
- groups: {
62
- resolveToolPolicy: resolveWeComGroupToolPolicy,
63
- },
64
- reload: { configPrefixes: ["channels.wecom"] },
65
- configSchema: {
66
- schema: {
67
- type: "object",
68
- additionalProperties: false,
69
- properties: {
70
- enabled: { type: "boolean" },
71
- corpId: { type: "string" },
72
- agentId: { type: "string" },
73
- secret: { type: "string" },
74
- token: { type: "string" },
75
- encodingAESKey: { type: "string" },
76
- webhookPath: { type: "string" },
77
- webhookHost: { type: "string" },
78
- webhookPort: { type: "integer", minimum: 1 },
79
- dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
80
- allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
81
- groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
82
- groupAllowFrom: {
83
- type: "array",
84
- items: { oneOf: [{ type: "string" }, { type: "number" }] },
85
- },
86
- requireMention: { type: "boolean" },
87
- historyLimit: { type: "integer", minimum: 0 },
88
- dmHistoryLimit: { type: "integer", minimum: 0 },
89
- textChunkLimit: { type: "integer", minimum: 1 },
90
- mediaMaxMb: { type: "number", minimum: 0 },
91
- },
92
- },
93
- },
94
- config: {
95
- listAccountIds: (cfg) => listWeComAccountIds(cfg),
96
- resolveAccount: (cfg, accountId) =>
97
- resolveWeComAccount({ cfg, accountId: accountId ?? undefined }),
98
- defaultAccountId: (cfg) => resolveDefaultWeComAccountId(cfg),
99
- setAccountEnabled: ({ cfg, accountId, enabled }) => {
100
- return {
101
- ...cfg,
102
- channels: {
103
- ...cfg.channels,
104
- wecom: {
105
- ...cfg.channels?.wecom,
106
- enabled,
107
- },
108
- },
109
- };
110
- },
111
- deleteAccount: ({ cfg }) => {
112
- const next = { ...cfg } as ClawdbotConfig;
113
- const nextChannels = { ...cfg.channels };
114
- delete (nextChannels as Record<string, unknown>).wecom;
115
- if (Object.keys(nextChannels).length > 0) {
116
- next.channels = nextChannels;
117
- } else {
118
- delete next.channels;
119
- }
120
- return next;
121
- },
122
- isConfigured: (account) => account.configured,
123
- describeAccount: (account) => ({
124
- accountId: account.accountId,
125
- enabled: account.enabled,
126
- configured: account.configured,
127
- name: account.name,
128
- corpId: account.corpId,
129
- agentId: account.agentId,
130
- }),
131
- resolveAllowFrom: ({ cfg }) => {
132
- const account = resolveWeComAccount({ cfg });
133
- return (account.config?.allowFrom ?? []).map((entry) => String(entry));
134
- },
135
- formatAllowFrom: ({ allowFrom }) =>
136
- allowFrom
137
- .map((entry) => String(entry).trim())
138
- .filter(Boolean)
139
- .map((entry) => entry.toLowerCase()),
140
- },
141
- security: {
142
- collectWarnings: () => [],
143
- },
144
- setup: {
145
- resolveAccountId: () => DEFAULT_ACCOUNT_ID,
146
- applyAccountConfig: ({ cfg }) => {
147
- return {
148
- ...cfg,
149
- channels: {
150
- ...cfg.channels,
151
- wecom: {
152
- ...cfg.channels?.wecom,
153
- enabled: true,
154
- },
155
- },
156
- };
157
- },
158
- },
159
- messaging: {
160
- normalizeTarget: (raw) => normalizeWeComTarget(raw) ?? undefined,
161
- targetResolver: {
162
- looksLikeId: looksLikeWeComId,
163
- hint: "<userId|user:userId>",
164
- },
165
- },
166
- directory: {
167
- self: async () => null,
168
- listPeers: async ({ cfg, query, limit, accountId }) => {
169
- const { getDepartmentUsersWeCom } = await import("./directory.js");
170
- try {
171
- // Get users from root department (1)
172
- const users = await getDepartmentUsersWeCom({
173
- cfg,
174
- departmentId: "1",
175
- fetchChild: true,
176
- accountId: accountId ?? undefined,
177
- });
178
-
179
- let filtered = users;
180
- if (query) {
181
- const lowerQuery = query.toLowerCase();
182
- filtered = users.filter(
183
- (u) =>
184
- u.name.toLowerCase().includes(lowerQuery) ||
185
- u.userid.toLowerCase().includes(lowerQuery),
186
- );
187
- }
188
-
189
- if (limit && limit > 0) {
190
- filtered = filtered.slice(0, limit);
191
- }
192
-
193
- return filtered.map((u) => ({
194
- kind: "user" as const,
195
- id: u.userid,
196
- name: u.name,
197
- }));
198
- } catch {
199
- return [];
200
- }
201
- },
202
- listGroups: async () => [],
203
- listPeersLive: async ({ cfg, query, limit, accountId }) => {
204
- const { getDepartmentUsersWeCom } = await import("./directory.js");
205
- try {
206
- const users = await getDepartmentUsersWeCom({
207
- cfg,
208
- departmentId: "1",
209
- fetchChild: true,
210
- accountId: accountId ?? undefined,
211
- });
212
-
213
- let filtered = users;
214
- if (query) {
215
- const lowerQuery = query.toLowerCase();
216
- filtered = users.filter(
217
- (u) =>
218
- u.name.toLowerCase().includes(lowerQuery) ||
219
- u.userid.toLowerCase().includes(lowerQuery),
220
- );
221
- }
222
-
223
- if (limit && limit > 0) {
224
- filtered = filtered.slice(0, limit);
225
- }
226
-
227
- return filtered.map((u) => ({
228
- kind: "user" as const,
229
- id: u.userid,
230
- name: u.name,
231
- }));
232
- } catch {
233
- return [];
234
- }
235
- },
236
- listGroupsLive: async () => [],
237
- },
238
- outbound: wecomOutbound,
239
- status: {
240
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
241
- buildChannelSummary: ({ snapshot }) => ({
242
- ...buildBaseChannelStatusSummary(snapshot),
243
- port: snapshot.port ?? null,
244
- probe: snapshot.probe,
245
- lastProbeAt: snapshot.lastProbeAt ?? null,
246
- }),
247
- probeAccount: async ({ account }) => await probeWeCom(account),
248
- buildAccountSnapshot: ({ account, runtime, probe }) => ({
249
- accountId: account.accountId,
250
- enabled: account.enabled,
251
- configured: account.configured,
252
- name: account.name,
253
- corpId: account.corpId,
254
- agentId: account.agentId,
255
- running: runtime?.running ?? false,
256
- lastStartAt: runtime?.lastStartAt ?? null,
257
- lastStopAt: runtime?.lastStopAt ?? null,
258
- lastError: runtime?.lastError ?? null,
259
- port: runtime?.port ?? null,
260
- probe,
261
- }),
262
- },
263
- gateway: {
264
- startAccount: async (ctx) => {
265
- const { monitorWeComProvider } = await import("./monitor.js");
266
- const account = resolveWeComAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
267
- const port = account.config?.webhookPort ?? null;
268
- ctx.setStatus({ accountId: ctx.accountId, port });
269
- ctx.log?.info(`starting wecom[${ctx.accountId}]`);
270
- return monitorWeComProvider({
271
- config: ctx.cfg,
272
- runtime: ctx.runtime,
273
- abortSignal: ctx.abortSignal,
274
- accountId: ctx.accountId,
275
- });
276
- },
277
- },
278
- };
package/src/client.ts DELETED
@@ -1,55 +0,0 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
3
- import { resolveWeComCredentials } from "./accounts.js";
4
-
5
- const accessTokenCache = new Map<string, { token: string; expiresAt: number }>();
6
-
7
- const WECOM_API_POLICY = { allowedHostnames: ["qyapi.weixin.qq.com"] };
8
-
9
- export async function getWeComAccessToken({
10
- cfg,
11
- accountId,
12
- }: {
13
- cfg: ClawdbotConfig;
14
- accountId?: string;
15
- }): Promise<string> {
16
- const { corpId, secret } = resolveWeComCredentials({ cfg, accountId });
17
-
18
- const cached = accessTokenCache.get(corpId);
19
- if (cached && cached.expiresAt > Date.now()) {
20
- return cached.token;
21
- }
22
-
23
- const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${secret}`;
24
- const { response, release } = await fetchWithSsrFGuard({
25
- url,
26
- policy: WECOM_API_POLICY,
27
- auditContext: "wecom-get-token",
28
- });
29
- let data: { errcode: number; errmsg: string; access_token: string };
30
- try {
31
- data = await response.json();
32
- } finally {
33
- await release();
34
- }
35
-
36
- if (data.errcode !== 0) {
37
- throw new Error(`Failed to get WeCom access token: ${data.errmsg}`);
38
- }
39
-
40
- // Cache token (expires in 7200 seconds, cache for 7000 to be safe)
41
- accessTokenCache.set(corpId, {
42
- token: data.access_token,
43
- expiresAt: Date.now() + 7000 * 1000,
44
- });
45
-
46
- return data.access_token;
47
- }
48
-
49
- export function clearWeComAccessTokenCache(corpId?: string): void {
50
- if (corpId) {
51
- accessTokenCache.delete(corpId);
52
- } else {
53
- accessTokenCache.clear();
54
- }
55
- }
@@ -1,102 +0,0 @@
1
- import { z } from "zod";
2
-
3
- const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
4
- const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
5
-
6
- const ToolPolicySchema = z
7
- .object({
8
- allow: z.array(z.string()).optional(),
9
- deny: z.array(z.string()).optional(),
10
- })
11
- .strict()
12
- .optional();
13
-
14
- const DmConfigSchema = z
15
- .object({
16
- enabled: z.boolean().optional(),
17
- systemPrompt: z.string().optional(),
18
- })
19
- .strict()
20
- .optional();
21
-
22
- export const WeComGroupSchema = z
23
- .object({
24
- requireMention: z.boolean().optional(),
25
- tools: ToolPolicySchema,
26
- skills: z.array(z.string()).optional(),
27
- enabled: z.boolean().optional(),
28
- allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
29
- systemPrompt: z.string().optional(),
30
- })
31
- .strict();
32
-
33
- const WeComSharedConfigShape = {
34
- webhookHost: z.string().optional(),
35
- webhookPort: z.number().int().positive().optional(),
36
- dmPolicy: DmPolicySchema.optional(),
37
- allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
38
- groupPolicy: GroupPolicySchema.optional(),
39
- groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
40
- requireMention: z.boolean().optional(),
41
- groups: z.record(z.string(), WeComGroupSchema.optional()).optional(),
42
- historyLimit: z.number().int().min(0).optional(),
43
- dmHistoryLimit: z.number().int().min(0).optional(),
44
- dms: z.record(z.string(), DmConfigSchema).optional(),
45
- textChunkLimit: z.number().int().positive().optional(),
46
- chunkMode: z.enum(["length", "newline"]).optional(),
47
- mediaMaxMb: z.number().positive().optional(),
48
- };
49
-
50
- export const WeComConfigSchema = z
51
- .object({
52
- enabled: z.boolean().optional(),
53
- corpId: z.string().optional(),
54
- agentId: z.string().optional(),
55
- secret: z.string().optional(),
56
- token: z.string().optional(),
57
- encodingAESKey: z.string().optional(),
58
- webhookPath: z.string().optional().default("/wecom/events"),
59
- ...WeComSharedConfigShape,
60
- dmPolicy: DmPolicySchema.optional().default("pairing"),
61
- groupPolicy: GroupPolicySchema.optional().default("allowlist"),
62
- requireMention: z.boolean().optional().default(false),
63
- })
64
- .strict()
65
- .superRefine((value, ctx) => {
66
- // Validate that token and encodingAESKey are present if enabled
67
- if (value.enabled && (!value.token?.trim() || !value.encodingAESKey?.trim())) {
68
- ctx.addIssue({
69
- code: z.ZodIssueCode.custom,
70
- path: ["token"],
71
- message: "channels.wecom requires token and encodingAESKey when enabled",
72
- });
73
- }
74
-
75
- if (value.dmPolicy === "open") {
76
- const allowFrom = value.allowFrom ?? [];
77
- const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");
78
- if (!hasWildcard) {
79
- ctx.addIssue({
80
- code: z.ZodIssueCode.custom,
81
- path: ["allowFrom"],
82
- message:
83
- 'channels.wecom.dmPolicy="open" requires channels.wecom.allowFrom to include "*"',
84
- });
85
- }
86
- }
87
- });
88
-
89
- export type WeComConfig = z.infer<typeof WeComConfigSchema>;
90
- export type WeComGroupConfig = z.infer<typeof WeComGroupSchema>;
91
-
92
- export interface ResolvedWeComAccount {
93
- accountId: string;
94
- enabled: boolean;
95
- configured: boolean;
96
- name: string;
97
- corpId?: string;
98
- agentId?: string;
99
- config?: WeComConfig;
100
- token?: string;
101
- encodingAESKey?: string;
102
- }
package/src/dedup.ts DELETED
@@ -1,60 +0,0 @@
1
- import { homedir } from "node:os";
2
- import path from "node:path";
3
- import {
4
- createDedupeCache,
5
- createPersistentDedupe,
6
- resolvePreferredOpenClawTmpDir,
7
- } from "openclaw/plugin-sdk";
8
-
9
- // Persistent TTL: 24 hours — survives restarts
10
- const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
11
- const MEMORY_MAX_SIZE = 1_000;
12
- const FILE_MAX_ENTRIES = 10_000;
13
-
14
- const memoryDedupe = createDedupeCache({ ttlMs: DEDUP_TTL_MS, maxSize: MEMORY_MAX_SIZE });
15
-
16
- function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
17
- const stateOverride = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
18
- if (stateOverride) {
19
- return stateOverride;
20
- }
21
- if (env.VITEST || env.NODE_ENV === "test") {
22
- return path.join(
23
- resolvePreferredOpenClawTmpDir(),
24
- ["openclaw-vitest", String(process.pid)].join("-"),
25
- );
26
- }
27
- return path.join(homedir(), ".openclaw");
28
- }
29
-
30
- function resolveNamespaceFilePath(namespace: string): string {
31
- const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
32
- return path.join(resolveStateDirFromEnv(), "wecom", "dedup", `${safe}.json`);
33
- }
34
-
35
- const persistentDedupe = createPersistentDedupe({
36
- ttlMs: DEDUP_TTL_MS,
37
- memoryMaxSize: MEMORY_MAX_SIZE,
38
- fileMaxEntries: FILE_MAX_ENTRIES,
39
- resolveFilePath: resolveNamespaceFilePath,
40
- });
41
-
42
- /**
43
- * Synchronous dedup — memory only.
44
- */
45
- export function tryRecordMessage(messageId: string): boolean {
46
- return !memoryDedupe.check(messageId);
47
- }
48
-
49
- export async function tryRecordMessagePersistent(
50
- messageId: string,
51
- namespace = "global",
52
- log?: (...args: unknown[]) => void,
53
- ): Promise<boolean> {
54
- return persistentDedupe.checkAndRecord(messageId, {
55
- namespace,
56
- onDiskError: (error) => {
57
- log?.(`wecom-dedup: disk error, falling back to memory: ${String(error)}`);
58
- },
59
- });
60
- }
package/src/directory.ts DELETED
@@ -1,150 +0,0 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
3
- import { getWeComAccessToken } from "./client.js";
4
-
5
- const WECOM_API_POLICY = { allowedHostnames: ["qyapi.weixin.qq.com"] };
6
-
7
- /**
8
- * Get user info from WeCom
9
- */
10
- export async function getUserInfoWeCom({
11
- cfg,
12
- userId,
13
- accountId,
14
- }: {
15
- cfg: ClawdbotConfig;
16
- userId: string;
17
- accountId?: string;
18
- }): Promise<{
19
- userid: string;
20
- name: string;
21
- department: number[];
22
- position?: string;
23
- mobile?: string;
24
- email?: string;
25
- avatar?: string;
26
- }> {
27
- const accessToken = await getWeComAccessToken({ cfg, accountId });
28
- const url = `https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}&userid=${userId}`;
29
-
30
- const { response, release } = await fetchWithSsrFGuard({
31
- url,
32
- policy: WECOM_API_POLICY,
33
- auditContext: "wecom-get-user-info",
34
- });
35
- let data: {
36
- errcode: number;
37
- errmsg: string;
38
- userid: string;
39
- name: string;
40
- department: number[];
41
- position?: string;
42
- mobile?: string;
43
- email?: string;
44
- avatar?: string;
45
- };
46
- try {
47
- data = await response.json();
48
- } finally {
49
- await release();
50
- }
51
-
52
- if (data.errcode !== 0) {
53
- throw new Error(`Failed to get user info: ${data.errmsg}`);
54
- }
55
-
56
- return data;
57
- }
58
-
59
- /**
60
- * Get department list from WeCom
61
- */
62
- export async function getDepartmentListWeCom({
63
- cfg,
64
- departmentId,
65
- accountId,
66
- }: {
67
- cfg: ClawdbotConfig;
68
- departmentId?: string;
69
- accountId?: string;
70
- }): Promise<
71
- Array<{
72
- id: number;
73
- name: string;
74
- parentid: number;
75
- order: number;
76
- }>
77
- > {
78
- const accessToken = await getWeComAccessToken({ cfg, accountId });
79
- const url = departmentId
80
- ? `https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=${accessToken}&id=${departmentId}`
81
- : `https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=${accessToken}`;
82
-
83
- const { response, release } = await fetchWithSsrFGuard({
84
- url,
85
- policy: WECOM_API_POLICY,
86
- auditContext: "wecom-get-department-list",
87
- });
88
- let data: { errcode: number; errmsg: string; department?: unknown[] };
89
- try {
90
- data = await response.json();
91
- } finally {
92
- await release();
93
- }
94
-
95
- if (data.errcode !== 0) {
96
- throw new Error(`Failed to get department list: ${data.errmsg}`);
97
- }
98
-
99
- return (data.department ?? []) as Array<{
100
- id: number;
101
- name: string;
102
- parentid: number;
103
- order: number;
104
- }>;
105
- }
106
-
107
- /**
108
- * Get department users from WeCom
109
- */
110
- export async function getDepartmentUsersWeCom({
111
- cfg,
112
- departmentId,
113
- fetchChild,
114
- accountId,
115
- }: {
116
- cfg: ClawdbotConfig;
117
- departmentId: string;
118
- fetchChild?: boolean;
119
- accountId?: string;
120
- }): Promise<
121
- Array<{
122
- userid: string;
123
- name: string;
124
- department: number[];
125
- position?: string;
126
- mobile?: string;
127
- email?: string;
128
- }>
129
- > {
130
- const accessToken = await getWeComAccessToken({ cfg, accountId });
131
- const url = `https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=${accessToken}&department_id=${departmentId}&fetch_child=${fetchChild ? 1 : 0}`;
132
-
133
- const { response, release } = await fetchWithSsrFGuard({
134
- url,
135
- policy: WECOM_API_POLICY,
136
- auditContext: "wecom-get-department-users",
137
- });
138
- let data: { errcode: number; errmsg: string; userlist?: unknown[] };
139
- try {
140
- data = await response.json();
141
- } finally {
142
- await release();
143
- }
144
-
145
- if (data.errcode !== 0) {
146
- throw new Error(`Failed to get department users: ${data.errmsg}`);
147
- }
148
-
149
- return (data.userlist ?? []) as Array<{ userid: string; name: string; department: number[] }>;
150
- }
package/src/index.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { wecomPlugin } from "./channel.js";
3
- import { setWeComRuntime } from "./runtime.js";
4
-
5
- export { wecomPlugin } from "./channel.js";
6
-
7
- const plugin = {
8
- id: "wecom",
9
- name: "WeCom",
10
- description: "WeCom (企业微信) channel plugin for OpenClaw",
11
- version: "1.0.0",
12
-
13
- register(api: OpenClawPluginApi) {
14
- setWeComRuntime(api.runtime);
15
- api.registerChannel({ plugin: wecomPlugin });
16
- api.logger.info("wecom: plugin registered successfully");
17
- },
18
- };
19
-
20
- export default plugin;