moltbot-wecom 1.0.2 → 2.0.0
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/clawdbot.plugin.json +42 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/src/accounts.d.ts +15 -0
- package/dist/src/accounts.d.ts.map +1 -0
- package/dist/src/accounts.js +90 -0
- package/dist/src/channel.d.ts +8 -0
- package/dist/src/channel.d.ts.map +1 -0
- package/dist/src/channel.js +365 -0
- package/dist/src/config-schema.d.ts +211 -0
- package/dist/src/config-schema.d.ts.map +1 -0
- package/dist/src/config-schema.js +21 -0
- package/dist/src/dedup.d.ts +8 -0
- package/dist/src/dedup.d.ts.map +1 -0
- package/dist/src/dedup.js +24 -0
- package/dist/src/group-filter.d.ts +15 -0
- package/dist/src/group-filter.d.ts.map +1 -0
- package/dist/src/group-filter.js +43 -0
- package/dist/src/probe.d.ts +10 -0
- package/dist/src/probe.d.ts.map +1 -0
- package/dist/src/probe.js +70 -0
- package/dist/src/receive.d.ts +26 -0
- package/dist/src/receive.d.ts.map +1 -0
- package/dist/src/receive.js +282 -0
- package/dist/src/runtime.d.ts +7 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +13 -0
- package/dist/src/send.d.ts +26 -0
- package/dist/src/send.d.ts.map +1 -0
- package/dist/src/send.js +75 -0
- package/dist/src/status-issues.d.ts +7 -0
- package/dist/src/status-issues.d.ts.map +1 -0
- package/dist/src/status-issues.js +47 -0
- package/dist/src/types.d.ts +79 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +4 -0
- package/package.json +28 -11
- package/moltbot.plugin.json +0 -27
- package/plugin.js +0 -243
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "wecom",
|
|
3
|
+
"name": "WeCom",
|
|
4
|
+
"description": "WeCom (企业微信) channel plugin — WebSocket proxy Smart Bot",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"channels": ["wecom"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"proxyUrl": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "WebSocket proxy URL (wss://...)"
|
|
13
|
+
},
|
|
14
|
+
"proxyToken": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Token for authenticating with the WebSocket proxy"
|
|
17
|
+
},
|
|
18
|
+
"dmPolicy": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"enum": ["pairing", "allowlist", "open", "disabled"],
|
|
21
|
+
"default": "pairing",
|
|
22
|
+
"description": "Direct message access policy"
|
|
23
|
+
},
|
|
24
|
+
"thinkingThresholdMs": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"default": 0,
|
|
27
|
+
"description": "Delay in ms before showing 'Thinking...' placeholder (0 to disable)"
|
|
28
|
+
},
|
|
29
|
+
"pingIntervalMs": {
|
|
30
|
+
"type": "number",
|
|
31
|
+
"default": 30000,
|
|
32
|
+
"description": "Heartbeat ping interval in milliseconds"
|
|
33
|
+
},
|
|
34
|
+
"reconnectDelayMs": {
|
|
35
|
+
"type": "number",
|
|
36
|
+
"default": 5000,
|
|
37
|
+
"description": "Reconnect delay after disconnect in milliseconds"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"additionalProperties": true
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom (Enterprise WeChat) channel plugin for Clawdbot/Moltbot.
|
|
3
|
+
*
|
|
4
|
+
* Connects WeCom Smart Bot via WebSocket proxy (no public server required).
|
|
5
|
+
*/
|
|
6
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
7
|
+
declare const plugin: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
configSchema: {
|
|
12
|
+
type: "object";
|
|
13
|
+
properties: {
|
|
14
|
+
proxyUrl: {
|
|
15
|
+
type: "string";
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
proxyToken: {
|
|
19
|
+
type: "string";
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
required: readonly [];
|
|
24
|
+
additionalProperties: true;
|
|
25
|
+
};
|
|
26
|
+
register(api: ClawdbotPluginApi): void;
|
|
27
|
+
};
|
|
28
|
+
export default plugin;
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAK7D,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;kBAaI,iBAAiB;CAahC,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom (Enterprise WeChat) channel plugin for Clawdbot/Moltbot.
|
|
3
|
+
*
|
|
4
|
+
* Connects WeCom Smart Bot via WebSocket proxy (no public server required).
|
|
5
|
+
*/
|
|
6
|
+
import { wecomDock, wecomPlugin } from "./src/channel.js";
|
|
7
|
+
import { setWeComRuntime } from "./src/runtime.js";
|
|
8
|
+
const plugin = {
|
|
9
|
+
id: "wecom",
|
|
10
|
+
name: "WeCom",
|
|
11
|
+
description: "WeCom (Enterprise WeChat) channel plugin — WebSocket proxy bot",
|
|
12
|
+
configSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
proxyUrl: { type: "string", description: "WebSocket proxy URL" },
|
|
16
|
+
proxyToken: { type: "string", description: "Proxy authentication token" },
|
|
17
|
+
},
|
|
18
|
+
required: [],
|
|
19
|
+
additionalProperties: true,
|
|
20
|
+
},
|
|
21
|
+
register(api) {
|
|
22
|
+
setWeComRuntime(api.runtime);
|
|
23
|
+
// If plugin-level config has proxyUrl, start the client eagerly
|
|
24
|
+
const pluginCfg = api.pluginConfig;
|
|
25
|
+
if (pluginCfg?.proxyUrl) {
|
|
26
|
+
api.runtime.log?.info?.(`[wecom] Plugin config detected (proxyUrl=${pluginCfg.proxyUrl.slice(0, 30)}...)`);
|
|
27
|
+
}
|
|
28
|
+
api.registerChannel({ plugin: wecomPlugin, dock: wecomDock });
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export default plugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom account resolution — multi-account support.
|
|
3
|
+
*/
|
|
4
|
+
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
5
|
+
import type { ResolvedWeComAccount } from "./types.js";
|
|
6
|
+
/** List all configured WeCom account IDs (falls back to ["default"]). */
|
|
7
|
+
export declare function listWeComAccountIds(cfg: ClawdbotConfig): string[];
|
|
8
|
+
/** Resolve the default account ID. */
|
|
9
|
+
export declare function resolveDefaultWeComAccountId(cfg: ClawdbotConfig): string;
|
|
10
|
+
/** Fully resolve a WeCom account. */
|
|
11
|
+
export declare function resolveWeComAccount(params: {
|
|
12
|
+
cfg: ClawdbotConfig;
|
|
13
|
+
accountId?: string | null;
|
|
14
|
+
}): ResolvedWeComAccount;
|
|
15
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAGV,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAQpB,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,EAAE,CAIjE;AAED,sCAAsC;AACtC,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAMxE;AA8DD,qCAAqC;AACrC,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,GAAG,EAAE,cAAc,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,GAAG,oBAAoB,CAevB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom account resolution — multi-account support.
|
|
3
|
+
*/
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
|
5
|
+
function listConfiguredAccountIds(cfg) {
|
|
6
|
+
const accounts = cfg.channels?.wecom?.accounts;
|
|
7
|
+
if (!accounts || typeof accounts !== "object")
|
|
8
|
+
return [];
|
|
9
|
+
return Object.keys(accounts).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
/** List all configured WeCom account IDs (falls back to ["default"]). */
|
|
12
|
+
export function listWeComAccountIds(cfg) {
|
|
13
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
14
|
+
if (ids.length === 0)
|
|
15
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
16
|
+
return ids.sort((a, b) => a.localeCompare(b));
|
|
17
|
+
}
|
|
18
|
+
/** Resolve the default account ID. */
|
|
19
|
+
export function resolveDefaultWeComAccountId(cfg) {
|
|
20
|
+
const wecomConfig = cfg.channels?.wecom;
|
|
21
|
+
if (wecomConfig?.defaultAccount?.trim())
|
|
22
|
+
return wecomConfig.defaultAccount.trim();
|
|
23
|
+
const ids = listWeComAccountIds(cfg);
|
|
24
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID))
|
|
25
|
+
return DEFAULT_ACCOUNT_ID;
|
|
26
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
27
|
+
}
|
|
28
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
29
|
+
const accounts = cfg.channels?.wecom?.accounts;
|
|
30
|
+
if (!accounts || typeof accounts !== "object")
|
|
31
|
+
return undefined;
|
|
32
|
+
return accounts[accountId];
|
|
33
|
+
}
|
|
34
|
+
function mergeWeComAccountConfig(cfg, accountId) {
|
|
35
|
+
const raw = (cfg.channels?.wecom ?? {});
|
|
36
|
+
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
|
37
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
38
|
+
return { ...base, ...account };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve proxyUrl + proxyToken for an account.
|
|
42
|
+
* Checks: account config → base config → plugin config → env.
|
|
43
|
+
*/
|
|
44
|
+
function resolveCredentials(cfg, merged) {
|
|
45
|
+
// From channel config
|
|
46
|
+
if (merged.proxyUrl?.trim()) {
|
|
47
|
+
return {
|
|
48
|
+
proxyUrl: merged.proxyUrl.trim(),
|
|
49
|
+
proxyToken: merged.proxyToken?.trim() ?? "",
|
|
50
|
+
source: "config",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// From plugin config (plugins.entries.wecom.config)
|
|
54
|
+
const pluginCfg = cfg.plugins;
|
|
55
|
+
const wecomPluginCfg = pluginCfg?.entries?.wecom?.config;
|
|
56
|
+
if (wecomPluginCfg?.proxyUrl?.trim()) {
|
|
57
|
+
return {
|
|
58
|
+
proxyUrl: wecomPluginCfg.proxyUrl.trim(),
|
|
59
|
+
proxyToken: wecomPluginCfg.proxyToken?.trim() ?? "",
|
|
60
|
+
source: "plugin",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// From environment variables
|
|
64
|
+
const envProxyUrl = process.env.WECOM_PROXY_URL ?? process.env.PROXY_URL;
|
|
65
|
+
const envProxyToken = process.env.WECOM_PROXY_TOKEN ?? process.env.PROXY_TOKEN;
|
|
66
|
+
if (envProxyUrl?.trim()) {
|
|
67
|
+
return {
|
|
68
|
+
proxyUrl: envProxyUrl.trim(),
|
|
69
|
+
proxyToken: envProxyToken?.trim() ?? "",
|
|
70
|
+
source: "env",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { proxyUrl: "", proxyToken: "", source: "none" };
|
|
74
|
+
}
|
|
75
|
+
/** Fully resolve a WeCom account. */
|
|
76
|
+
export function resolveWeComAccount(params) {
|
|
77
|
+
const { cfg, accountId: rawAccountId } = params;
|
|
78
|
+
const accountId = normalizeAccountId(rawAccountId);
|
|
79
|
+
const merged = mergeWeComAccountConfig(cfg, accountId);
|
|
80
|
+
const credentials = resolveCredentials(cfg, merged);
|
|
81
|
+
return {
|
|
82
|
+
accountId,
|
|
83
|
+
name: merged.name,
|
|
84
|
+
enabled: merged.enabled !== false,
|
|
85
|
+
proxyUrl: credentials.proxyUrl,
|
|
86
|
+
proxyToken: credentials.proxyToken,
|
|
87
|
+
tokenSource: credentials.source,
|
|
88
|
+
config: merged,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom ChannelPlugin + ChannelDock — core channel integration.
|
|
3
|
+
*/
|
|
4
|
+
import type { ChannelDock, ChannelPlugin } from "clawdbot/plugin-sdk";
|
|
5
|
+
import type { ResolvedWeComAccount } from "./types.js";
|
|
6
|
+
export declare const wecomDock: ChannelDock;
|
|
7
|
+
export declare const wecomPlugin: ChannelPlugin<ResolvedWeComAccount>;
|
|
8
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAEV,WAAW,EACX,aAAa,EAEd,MAAM,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAuBvD,eAAO,MAAM,SAAS,EAAE,WA0BvB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,oBAAoB,CA6U3D,CAAC"}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom ChannelPlugin + ChannelDock — core channel integration.
|
|
3
|
+
*/
|
|
4
|
+
import { applyAccountNameToChannelSection, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, setAccountEnabledInConfigSection, } from "clawdbot/plugin-sdk";
|
|
5
|
+
import { listWeComAccountIds, resolveDefaultWeComAccountId, resolveWeComAccount, } from "./accounts.js";
|
|
6
|
+
import { probeWeCom } from "./probe.js";
|
|
7
|
+
import { collectWeComStatusIssues } from "./status-issues.js";
|
|
8
|
+
import { startWeComProvider } from "./receive.js";
|
|
9
|
+
const meta = {
|
|
10
|
+
id: "wecom",
|
|
11
|
+
label: "WeCom",
|
|
12
|
+
selectionLabel: "WeCom (企业微信)",
|
|
13
|
+
docsPath: "/channels/wecom",
|
|
14
|
+
docsLabel: "wecom",
|
|
15
|
+
blurb: "WeCom (Enterprise WeChat) Smart Bot with WebSocket proxy.",
|
|
16
|
+
aliases: ["wechat-work", "qywx", "企微"],
|
|
17
|
+
order: 86,
|
|
18
|
+
quickstartAllowFrom: true,
|
|
19
|
+
};
|
|
20
|
+
function normalizeWeComMessagingTarget(raw) {
|
|
21
|
+
const trimmed = raw?.trim();
|
|
22
|
+
if (!trimmed)
|
|
23
|
+
return undefined;
|
|
24
|
+
return trimmed.replace(/^(wecom|wechat-work|qywx):/i, "");
|
|
25
|
+
}
|
|
26
|
+
export const wecomDock = {
|
|
27
|
+
id: "wecom",
|
|
28
|
+
capabilities: {
|
|
29
|
+
chatTypes: ["direct"],
|
|
30
|
+
media: false,
|
|
31
|
+
blockStreaming: false,
|
|
32
|
+
},
|
|
33
|
+
outbound: { textChunkLimit: 2000 },
|
|
34
|
+
config: {
|
|
35
|
+
resolveAllowFrom: ({ cfg, accountId }) => (resolveWeComAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)),
|
|
36
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom
|
|
37
|
+
.map((entry) => String(entry).trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((entry) => entry.replace(/^(wecom|wechat-work|qywx):/i, ""))
|
|
40
|
+
.map((entry) => entry.toLowerCase()),
|
|
41
|
+
},
|
|
42
|
+
groups: {
|
|
43
|
+
resolveRequireMention: () => true,
|
|
44
|
+
},
|
|
45
|
+
threading: {
|
|
46
|
+
resolveReplyToMode: () => "off",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export const wecomPlugin = {
|
|
50
|
+
id: "wecom",
|
|
51
|
+
meta,
|
|
52
|
+
capabilities: {
|
|
53
|
+
chatTypes: ["direct"],
|
|
54
|
+
media: false,
|
|
55
|
+
reactions: false,
|
|
56
|
+
threads: false,
|
|
57
|
+
polls: false,
|
|
58
|
+
nativeCommands: false,
|
|
59
|
+
blockStreaming: false,
|
|
60
|
+
},
|
|
61
|
+
reload: { configPrefixes: ["channels.wecom"] },
|
|
62
|
+
// Use JSON Schema directly instead of Zod to avoid toJSONSchema dependency
|
|
63
|
+
configSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
name: { type: "string" },
|
|
67
|
+
enabled: { type: "boolean" },
|
|
68
|
+
proxyUrl: { type: "string" },
|
|
69
|
+
proxyToken: { type: "string" },
|
|
70
|
+
dmPolicy: { type: "string", enum: ["pairing", "allowlist", "open", "disabled"] },
|
|
71
|
+
allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
72
|
+
thinkingThresholdMs: { type: "number" },
|
|
73
|
+
botNames: { type: "array", items: { type: "string" } },
|
|
74
|
+
pingIntervalMs: { type: "number" },
|
|
75
|
+
reconnectDelayMs: { type: "number" },
|
|
76
|
+
accounts: { type: "object", additionalProperties: true },
|
|
77
|
+
defaultAccount: { type: "string" },
|
|
78
|
+
},
|
|
79
|
+
additionalProperties: true,
|
|
80
|
+
},
|
|
81
|
+
config: {
|
|
82
|
+
listAccountIds: (cfg) => listWeComAccountIds(cfg),
|
|
83
|
+
resolveAccount: (cfg, accountId) => resolveWeComAccount({ cfg: cfg, accountId }),
|
|
84
|
+
defaultAccountId: (cfg) => resolveDefaultWeComAccountId(cfg),
|
|
85
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
|
|
86
|
+
cfg: cfg,
|
|
87
|
+
sectionKey: "wecom",
|
|
88
|
+
accountId,
|
|
89
|
+
enabled,
|
|
90
|
+
allowTopLevel: true,
|
|
91
|
+
}),
|
|
92
|
+
deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
|
|
93
|
+
cfg: cfg,
|
|
94
|
+
sectionKey: "wecom",
|
|
95
|
+
accountId,
|
|
96
|
+
clearBaseFields: ["proxyUrl", "proxyToken", "name"],
|
|
97
|
+
}),
|
|
98
|
+
isConfigured: (account) => Boolean(account.proxyUrl?.trim()),
|
|
99
|
+
describeAccount: (account) => ({
|
|
100
|
+
accountId: account.accountId,
|
|
101
|
+
name: account.name,
|
|
102
|
+
enabled: account.enabled,
|
|
103
|
+
configured: Boolean(account.proxyUrl?.trim()),
|
|
104
|
+
tokenSource: account.tokenSource,
|
|
105
|
+
}),
|
|
106
|
+
resolveAllowFrom: ({ cfg, accountId }) => (resolveWeComAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)),
|
|
107
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom
|
|
108
|
+
.map((entry) => String(entry).trim())
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((entry) => entry.replace(/^(wecom|wechat-work|qywx):/i, ""))
|
|
111
|
+
.map((entry) => entry.toLowerCase()),
|
|
112
|
+
},
|
|
113
|
+
security: {
|
|
114
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
115
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
116
|
+
const wecomCfg = cfg.channels?.wecom;
|
|
117
|
+
const accountsMap = wecomCfg?.accounts;
|
|
118
|
+
const useAccountPath = Boolean(accountsMap?.[resolvedAccountId]);
|
|
119
|
+
const basePath = useAccountPath
|
|
120
|
+
? `channels.wecom.accounts.${resolvedAccountId}.`
|
|
121
|
+
: "channels.wecom.";
|
|
122
|
+
return {
|
|
123
|
+
policy: account.config.dmPolicy ?? "pairing",
|
|
124
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
125
|
+
policyPath: `${basePath}dmPolicy`,
|
|
126
|
+
allowFromPath: basePath,
|
|
127
|
+
approveHint: formatPairingApproveHint("wecom"),
|
|
128
|
+
normalizeEntry: (raw) => raw.replace(/^(wecom|wechat-work|qywx):/i, ""),
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
groups: {
|
|
133
|
+
resolveRequireMention: () => true,
|
|
134
|
+
},
|
|
135
|
+
threading: {
|
|
136
|
+
resolveReplyToMode: () => "off",
|
|
137
|
+
},
|
|
138
|
+
messaging: {
|
|
139
|
+
normalizeTarget: normalizeWeComMessagingTarget,
|
|
140
|
+
targetResolver: {
|
|
141
|
+
looksLikeId: (raw) => {
|
|
142
|
+
const trimmed = raw.trim();
|
|
143
|
+
if (!trimmed)
|
|
144
|
+
return false;
|
|
145
|
+
// WeCom user IDs are typically alphanumeric strings
|
|
146
|
+
return /^[a-zA-Z0-9_-]{6,64}$/.test(trimmed);
|
|
147
|
+
},
|
|
148
|
+
hint: "<userId>",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
directory: {
|
|
152
|
+
self: async () => null,
|
|
153
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
154
|
+
const account = resolveWeComAccount({ cfg: cfg, accountId });
|
|
155
|
+
const q = query?.trim().toLowerCase() || "";
|
|
156
|
+
const peers = Array.from(new Set((account.config.allowFrom ?? [])
|
|
157
|
+
.map((entry) => String(entry).trim())
|
|
158
|
+
.filter((entry) => Boolean(entry) && entry !== "*")
|
|
159
|
+
.map((entry) => entry.replace(/^(wecom|wechat-work|qywx):/i, ""))))
|
|
160
|
+
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
|
161
|
+
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
162
|
+
.map((id) => ({ kind: "user", id }));
|
|
163
|
+
return peers;
|
|
164
|
+
},
|
|
165
|
+
listGroups: async () => [],
|
|
166
|
+
},
|
|
167
|
+
setup: {
|
|
168
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
169
|
+
applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
|
|
170
|
+
cfg: cfg,
|
|
171
|
+
channelKey: "wecom",
|
|
172
|
+
accountId,
|
|
173
|
+
name: name,
|
|
174
|
+
}),
|
|
175
|
+
validateInput: ({ accountId, input }) => {
|
|
176
|
+
if (!input.proxyUrl) {
|
|
177
|
+
return "WeCom requires proxyUrl (WebSocket proxy URL).";
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
},
|
|
181
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
182
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
183
|
+
cfg: cfg,
|
|
184
|
+
channelKey: "wecom",
|
|
185
|
+
accountId,
|
|
186
|
+
name: input.name,
|
|
187
|
+
});
|
|
188
|
+
const next = accountId !== DEFAULT_ACCOUNT_ID
|
|
189
|
+
? migrateBaseNameToDefaultAccount({
|
|
190
|
+
cfg: namedConfig,
|
|
191
|
+
channelKey: "wecom",
|
|
192
|
+
})
|
|
193
|
+
: namedConfig;
|
|
194
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
195
|
+
return {
|
|
196
|
+
...next,
|
|
197
|
+
channels: {
|
|
198
|
+
...next.channels,
|
|
199
|
+
wecom: {
|
|
200
|
+
...next.channels?.wecom,
|
|
201
|
+
enabled: true,
|
|
202
|
+
...(input.proxyUrl ? { proxyUrl: input.proxyUrl } : {}),
|
|
203
|
+
...(input.proxyToken ? { proxyToken: input.proxyToken } : {}),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const wecomCfg = (next.channels?.wecom ?? {});
|
|
209
|
+
const accountsCfg = (wecomCfg.accounts ?? {});
|
|
210
|
+
const existingAccount = (accountsCfg[accountId] ?? {});
|
|
211
|
+
return {
|
|
212
|
+
...next,
|
|
213
|
+
channels: {
|
|
214
|
+
...(next.channels ?? {}),
|
|
215
|
+
wecom: {
|
|
216
|
+
...wecomCfg,
|
|
217
|
+
enabled: true,
|
|
218
|
+
accounts: {
|
|
219
|
+
...accountsCfg,
|
|
220
|
+
[accountId]: {
|
|
221
|
+
...existingAccount,
|
|
222
|
+
enabled: true,
|
|
223
|
+
...(input.proxyUrl ? { proxyUrl: input.proxyUrl } : {}),
|
|
224
|
+
...(input.proxyToken ? { proxyToken: input.proxyToken } : {}),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
pairing: {
|
|
233
|
+
idLabel: "wecomUserId",
|
|
234
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(wecom|wechat-work|qywx):/i, ""),
|
|
235
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
236
|
+
// WeCom Smart Bot uses passive reply; cannot actively send messages
|
|
237
|
+
// Approval notification will happen on next user message
|
|
238
|
+
return;
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
outbound: {
|
|
242
|
+
deliveryMode: "direct",
|
|
243
|
+
chunker: (text, limit) => {
|
|
244
|
+
if (!text)
|
|
245
|
+
return [];
|
|
246
|
+
if (limit <= 0 || text.length <= limit)
|
|
247
|
+
return [text];
|
|
248
|
+
const chunks = [];
|
|
249
|
+
let remaining = text;
|
|
250
|
+
while (remaining.length > limit) {
|
|
251
|
+
const window = remaining.slice(0, limit);
|
|
252
|
+
const lastNewline = window.lastIndexOf("\n");
|
|
253
|
+
const lastSpace = window.lastIndexOf(" ");
|
|
254
|
+
let breakIdx = lastNewline > 0 ? lastNewline : lastSpace;
|
|
255
|
+
if (breakIdx <= 0)
|
|
256
|
+
breakIdx = limit;
|
|
257
|
+
const rawChunk = remaining.slice(0, breakIdx);
|
|
258
|
+
const chunk = rawChunk.trimEnd();
|
|
259
|
+
if (chunk.length > 0)
|
|
260
|
+
chunks.push(chunk);
|
|
261
|
+
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
|
262
|
+
const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
|
|
263
|
+
remaining = remaining.slice(nextStart).trimStart();
|
|
264
|
+
}
|
|
265
|
+
if (remaining.length)
|
|
266
|
+
chunks.push(remaining);
|
|
267
|
+
return chunks;
|
|
268
|
+
},
|
|
269
|
+
chunkerMode: "text",
|
|
270
|
+
textChunkLimit: 2000,
|
|
271
|
+
sendText: async ({ to, text, accountId, cfg }) => {
|
|
272
|
+
const account = resolveWeComAccount({
|
|
273
|
+
accountId: accountId ?? undefined,
|
|
274
|
+
cfg: cfg,
|
|
275
|
+
});
|
|
276
|
+
if (!account.proxyUrl) {
|
|
277
|
+
return { channel: "wecom", ok: false, messageId: "", error: new Error("WeCom proxy not configured") };
|
|
278
|
+
}
|
|
279
|
+
// WeCom Smart Bot uses passive reply mode
|
|
280
|
+
// Active messaging would require direct WeCom API access
|
|
281
|
+
return {
|
|
282
|
+
channel: "wecom",
|
|
283
|
+
ok: false,
|
|
284
|
+
messageId: "",
|
|
285
|
+
error: new Error("Active messaging not supported. Use reply flow."),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
|
|
289
|
+
// WeCom Smart Bot doesn't support media in passive reply mode
|
|
290
|
+
return {
|
|
291
|
+
channel: "wecom",
|
|
292
|
+
ok: false,
|
|
293
|
+
messageId: "",
|
|
294
|
+
error: new Error("Media not supported in WeCom Smart Bot mode"),
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
status: {
|
|
299
|
+
defaultRuntime: {
|
|
300
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
301
|
+
running: false,
|
|
302
|
+
lastStartAt: null,
|
|
303
|
+
lastStopAt: null,
|
|
304
|
+
lastError: null,
|
|
305
|
+
},
|
|
306
|
+
collectStatusIssues: collectWeComStatusIssues,
|
|
307
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
308
|
+
configured: snapshot.configured ?? false,
|
|
309
|
+
tokenSource: snapshot.tokenSource ?? "none",
|
|
310
|
+
running: snapshot.running ?? false,
|
|
311
|
+
mode: "websocket-proxy",
|
|
312
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
313
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
314
|
+
lastError: snapshot.lastError ?? null,
|
|
315
|
+
probe: snapshot.probe,
|
|
316
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
317
|
+
}),
|
|
318
|
+
probeAccount: async ({ account, timeoutMs }) => probeWeCom(account.proxyUrl, account.proxyToken, timeoutMs),
|
|
319
|
+
buildAccountSnapshot: ({ account, runtime }) => {
|
|
320
|
+
const configured = Boolean(account.proxyUrl?.trim());
|
|
321
|
+
return {
|
|
322
|
+
accountId: account.accountId,
|
|
323
|
+
name: account.name,
|
|
324
|
+
enabled: account.enabled,
|
|
325
|
+
configured,
|
|
326
|
+
tokenSource: account.tokenSource,
|
|
327
|
+
running: runtime?.running ?? false,
|
|
328
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
329
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
330
|
+
lastError: runtime?.lastError ?? null,
|
|
331
|
+
mode: "websocket-proxy",
|
|
332
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
333
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
334
|
+
dmPolicy: account.config.dmPolicy ?? "pairing",
|
|
335
|
+
proxyUrl: account.proxyUrl ? `${account.proxyUrl.slice(0, 30)}...` : undefined,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
gateway: {
|
|
340
|
+
startAccount: async (ctx) => {
|
|
341
|
+
const account = ctx.account;
|
|
342
|
+
try {
|
|
343
|
+
const probe = await probeWeCom(account.proxyUrl, account.proxyToken, 3000);
|
|
344
|
+
ctx.setStatus({ accountId: account.accountId, probe: probe.ok ? "ok" : "failed" });
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Ignore probe errors during startup
|
|
348
|
+
}
|
|
349
|
+
ctx.log?.info(`[${account.accountId}] Starting WeCom provider (proxy=${account.proxyUrl})`);
|
|
350
|
+
const provider = startWeComProvider({
|
|
351
|
+
account,
|
|
352
|
+
config: ctx.cfg,
|
|
353
|
+
log: {
|
|
354
|
+
info: (msg) => ctx.log?.info(msg),
|
|
355
|
+
error: (msg) => ctx.log?.error(msg),
|
|
356
|
+
warn: (msg) => ctx.log?.warn?.(msg),
|
|
357
|
+
debug: (msg) => ctx.log?.debug?.(msg),
|
|
358
|
+
},
|
|
359
|
+
abortSignal: ctx.abortSignal,
|
|
360
|
+
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
361
|
+
});
|
|
362
|
+
return provider;
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
};
|