@dingxiang-me/openclaw-wechat 1.7.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/CHANGELOG.md +151 -0
- package/README.en.md +379 -11
- package/README.md +620 -12
- package/docs/channels/wecom.md +181 -3
- package/openclaw.plugin.json +148 -5
- package/package.json +9 -5
- package/src/core/delivery-router.js +2 -0
- package/src/core/stream-manager.js +13 -2
- package/src/core.js +96 -6
- package/src/wecom/account-config-core.js +2 -0
- package/src/wecom/account-config.js +12 -3
- package/src/wecom/agent-context.js +7 -1
- package/src/wecom/agent-dispatch-executor.js +13 -1
- package/src/wecom/agent-dispatch-fallback.js +23 -0
- package/src/wecom/agent-inbound-dispatch.js +1 -1
- package/src/wecom/agent-inbound-processor.js +33 -2
- package/src/wecom/agent-late-reply-runtime.js +31 -1
- package/src/wecom/agent-runtime-context.js +3 -0
- package/src/wecom/agent-webhook-handler.js +5 -0
- package/src/wecom/api-client-core.js +1 -1
- package/src/wecom/bot-context.js +7 -1
- package/src/wecom/bot-dispatch-fallback.js +34 -3
- package/src/wecom/bot-dispatch-handlers.js +47 -4
- package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
- package/src/wecom/bot-inbound-executor-helpers.js +11 -4
- package/src/wecom/bot-inbound-executor.js +34 -0
- package/src/wecom/bot-long-connection-manager.js +971 -0
- package/src/wecom/bot-reply-runtime.js +36 -6
- package/src/wecom/bot-runtime-context.js +3 -0
- package/src/wecom/bot-state-store.js +4 -5
- package/src/wecom/bot-webhook-dispatch.js +5 -0
- package/src/wecom/bot-webhook-handler.js +5 -0
- package/src/wecom/callback-health-diagnostics.js +86 -0
- package/src/wecom/channel-config-schema.js +242 -0
- package/src/wecom/channel-plugin.js +162 -4
- package/src/wecom/channel-status-state.js +150 -0
- package/src/wecom/command-handlers.js +6 -0
- package/src/wecom/command-status-text.js +32 -3
- package/src/wecom/doc-client.js +537 -0
- package/src/wecom/doc-schema.js +380 -0
- package/src/wecom/doc-tool.js +833 -0
- package/src/wecom/outbound-active-stream.js +17 -10
- package/src/wecom/outbound-delivery.js +49 -0
- package/src/wecom/plugin-account-policy-services.js +4 -1
- package/src/wecom/plugin-composition.js +2 -0
- package/src/wecom/plugin-constants.js +1 -1
- package/src/wecom/plugin-delivery-inbound-services.js +4 -0
- package/src/wecom/plugin-processing-deps.js +5 -0
- package/src/wecom/plugin-route-runtime-deps.js +2 -0
- package/src/wecom/plugin-services.js +37 -0
- package/src/wecom/register-runtime.js +20 -1
- package/src/wecom/request-parsers.js +1 -0
- package/src/wecom/route-registration.js +4 -1
- package/src/wecom/session-reset.js +168 -0
- package/src/wecom/text-format.js +22 -5
- package/src/wecom/text-inbound-scheduler.js +1 -1
- package/src/wecom/thinking-parser.js +74 -0
- package/src/wecom/voice-transcription-process.js +80 -8
- package/src/wecom/voice-transcription.js +11 -0
|
@@ -4,6 +4,13 @@ function assertFunction(name, value) {
|
|
|
4
4
|
}
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function isTimeoutLikeReason(reason) {
|
|
8
|
+
return String(reason?.message || reason || "")
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.includes("timed out");
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export function createWecomBotDispatchState() {
|
|
8
15
|
return {
|
|
9
16
|
blockText: "",
|
|
@@ -49,6 +56,7 @@ export function createWecomBotLateReplyRuntime({
|
|
|
49
56
|
safeDeliverReply,
|
|
50
57
|
runLateReplyWatcher,
|
|
51
58
|
activeWatchers,
|
|
59
|
+
clearSessionStoreEntry = null,
|
|
52
60
|
now = () => Date.now(),
|
|
53
61
|
randomToken = () => Math.random().toString(36).slice(2, 8),
|
|
54
62
|
} = {}) {
|
|
@@ -64,6 +72,24 @@ export function createWecomBotLateReplyRuntime({
|
|
|
64
72
|
|
|
65
73
|
let lateReplyWatcherPromise = null;
|
|
66
74
|
|
|
75
|
+
const autoResetTimedOutSession = async (reason) => {
|
|
76
|
+
if (typeof clearSessionStoreEntry !== "function" || !isTimeoutLikeReason(reason)) return false;
|
|
77
|
+
try {
|
|
78
|
+
const result = await clearSessionStoreEntry({
|
|
79
|
+
storePath,
|
|
80
|
+
sessionKey: sessionId,
|
|
81
|
+
logger,
|
|
82
|
+
});
|
|
83
|
+
logger?.info?.(
|
|
84
|
+
`wecom(bot): auto-reset timed out session=${sessionId} cleared=${result?.cleared === true ? "yes" : "no"}`,
|
|
85
|
+
);
|
|
86
|
+
return result?.cleared === true;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger?.warn?.(`wecom(bot): failed to auto-reset timed out session=${sessionId}: ${String(err?.message || err)}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
67
93
|
const readTranscriptFallbackResult = async ({
|
|
68
94
|
runtimeStorePath = storePath,
|
|
69
95
|
runtimeSessionId = sessionId,
|
|
@@ -128,12 +154,16 @@ export function createWecomBotLateReplyRuntime({
|
|
|
128
154
|
if (dispatchState.streamFinished) return;
|
|
129
155
|
const reasonText = String(watchErr?.message || watchErr || "");
|
|
130
156
|
const isTimeout = reasonText.includes("timed out");
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
try {
|
|
158
|
+
await safeDeliverReply(
|
|
159
|
+
isTimeout
|
|
160
|
+
? "抱歉,当前模型请求超时或网络不稳定,请稍后重试。"
|
|
161
|
+
: `抱歉,当前模型请求超时或网络不稳定,请稍后重试。\n故障信息: ${reasonText.slice(0, 160)}`,
|
|
162
|
+
isTimeout ? "late-timeout-fallback" : "late-watcher-error",
|
|
163
|
+
);
|
|
164
|
+
} finally {
|
|
165
|
+
await autoResetTimedOutSession(reasonText);
|
|
166
|
+
}
|
|
137
167
|
},
|
|
138
168
|
}).finally(() => {
|
|
139
169
|
lateReplyWatcherPromise = null;
|
|
@@ -16,6 +16,7 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
16
16
|
msgId = "",
|
|
17
17
|
messageText = "",
|
|
18
18
|
commandBody = "",
|
|
19
|
+
commandAuthorized = false,
|
|
19
20
|
originalContent = "",
|
|
20
21
|
fromAddress = "",
|
|
21
22
|
groupChatPolicy = {},
|
|
@@ -82,6 +83,8 @@ export async function prepareWecomBotRuntimeContext({
|
|
|
82
83
|
messageText,
|
|
83
84
|
originalContent,
|
|
84
85
|
commandBody,
|
|
86
|
+
commandAuthorized,
|
|
87
|
+
commandSource: commandAuthorized ? "text" : "",
|
|
85
88
|
fromAddress,
|
|
86
89
|
sessionId,
|
|
87
90
|
accountId,
|
|
@@ -84,13 +84,13 @@ export function createWecomBotStateStore({
|
|
|
84
84
|
return stream;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function updateStream(streamId, content, { append = false, finished = false, msgItem } = {}) {
|
|
88
|
-
return streamManager.update(streamId, content, { append, finished, msgItem });
|
|
87
|
+
function updateStream(streamId, content, { append = false, finished = false, msgItem, thinkingContent } = {}) {
|
|
88
|
+
return streamManager.update(streamId, content, { append, finished, msgItem, thinkingContent });
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function finishStream(streamId, content, { msgItem } = {}) {
|
|
91
|
+
function finishStream(streamId, content, { msgItem, thinkingContent } = {}) {
|
|
92
92
|
const normalizedStreamId = String(streamId ?? "").trim();
|
|
93
|
-
const stream = streamManager.finish(normalizedStreamId, content, { msgItem });
|
|
93
|
+
const stream = streamManager.finish(normalizedStreamId, content, { msgItem, thinkingContent });
|
|
94
94
|
if (stream) {
|
|
95
95
|
const sessionId = streamToSession.get(normalizedStreamId);
|
|
96
96
|
if (sessionId) unregisterActiveStream(sessionId, normalizedStreamId);
|
|
@@ -184,4 +184,3 @@ export function createWecomBotStateStore({
|
|
|
184
184
|
startCleanup,
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
-
|
|
@@ -25,6 +25,7 @@ function buildEncryptedStreamPayload({
|
|
|
25
25
|
content,
|
|
26
26
|
finish,
|
|
27
27
|
msgItem,
|
|
28
|
+
thinkingContent,
|
|
28
29
|
feedbackId,
|
|
29
30
|
}) {
|
|
30
31
|
const streamPayload = {
|
|
@@ -35,6 +36,9 @@ function buildEncryptedStreamPayload({
|
|
|
35
36
|
if (Array.isArray(msgItem) && msgItem.length > 0) {
|
|
36
37
|
streamPayload.msg_item = msgItem;
|
|
37
38
|
}
|
|
39
|
+
if (String(thinkingContent ?? "").trim()) {
|
|
40
|
+
streamPayload.thinking_content = String(thinkingContent).trim();
|
|
41
|
+
}
|
|
38
42
|
if (feedbackId) {
|
|
39
43
|
streamPayload.feedback = { id: feedbackId };
|
|
40
44
|
}
|
|
@@ -113,6 +117,7 @@ export function createWecomBotParsedDispatcher({
|
|
|
113
117
|
content: stream?.content ?? "会话已过期",
|
|
114
118
|
finish: stream ? stream.finished === true : true,
|
|
115
119
|
msgItem: stream?.msgItem,
|
|
120
|
+
thinkingContent: stream?.thinkingContent,
|
|
116
121
|
feedbackId,
|
|
117
122
|
});
|
|
118
123
|
sendEncryptedJson(res, encryptedResponse);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { createWecomBotParsedDispatcher } from "./bot-webhook-dispatch.js";
|
|
3
|
+
import { markWecomInboundActivity } from "./channel-status-state.js";
|
|
3
4
|
|
|
4
5
|
export function createWecomBotWebhookHandler({
|
|
5
6
|
api,
|
|
@@ -167,6 +168,10 @@ export function createWecomBotWebhookHandler({
|
|
|
167
168
|
const parsed = parseWecomBotInboundMessage(incomingPayload);
|
|
168
169
|
if (parsed && typeof parsed === "object") {
|
|
169
170
|
parsed.accountId = String(matchedBotConfig.accountId ?? "default").trim().toLowerCase() || "default";
|
|
171
|
+
markWecomInboundActivity({
|
|
172
|
+
accountId: parsed.accountId,
|
|
173
|
+
timestamp: incomingPayload?.create_time ?? incomingPayload?.CreateTime,
|
|
174
|
+
});
|
|
170
175
|
}
|
|
171
176
|
api.logger.info?.(
|
|
172
177
|
`wecom(bot): inbound ${describeWecomBotParsedMessage(parsed)} account=${matchedBotConfig.accountId || "default"}`,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function previewBody(body, maxLength = 120) {
|
|
2
|
+
return String(body ?? "").slice(0, Math.max(1, maxLength));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isHtmlBody(body) {
|
|
6
|
+
return /<!doctype html|<html/i.test(String(body ?? ""));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function diagnoseWecomCallbackHealth({
|
|
10
|
+
status,
|
|
11
|
+
body,
|
|
12
|
+
mode = "agent",
|
|
13
|
+
endpoint = "",
|
|
14
|
+
webhookPath = "",
|
|
15
|
+
gatewayPort = null,
|
|
16
|
+
location = "",
|
|
17
|
+
} = {}) {
|
|
18
|
+
const rawBody = String(body ?? "");
|
|
19
|
+
const preview = previewBody(rawBody);
|
|
20
|
+
const normalizedMode = String(mode ?? "agent").trim().toLowerCase() === "bot" ? "bot" : "agent";
|
|
21
|
+
const healthyMarker = normalizedMode === "bot" ? "wecom bot webhook" : "wecom webhook";
|
|
22
|
+
const healthy = status === 200 && rawBody.toLowerCase().includes(healthyMarker);
|
|
23
|
+
if (healthy) {
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
detail: `status=${status} body=${preview}`,
|
|
27
|
+
data: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hints = [];
|
|
32
|
+
let reason = "unexpected-response";
|
|
33
|
+
const effectivePath = String(webhookPath ?? "").trim() || String(endpoint ?? "").trim() || "/";
|
|
34
|
+
const authScopeHint =
|
|
35
|
+
normalizedMode === "bot"
|
|
36
|
+
? "为 /wecom/*(以及 legacy /webhooks/wecom*)单独放行,或使用独立回调域名/端口"
|
|
37
|
+
: "为 /wecom/*(以及 legacy /webhooks/app*)单独放行,或使用独立回调域名/端口";
|
|
38
|
+
|
|
39
|
+
if (status === 404) {
|
|
40
|
+
reason = "route-not-found";
|
|
41
|
+
hints.push(`路径 ${effectivePath} 未命中${normalizedMode === "bot" ? " Bot" : ""}回调路由`);
|
|
42
|
+
} else if (status === 401 || status === 403) {
|
|
43
|
+
reason = "gateway-auth";
|
|
44
|
+
hints.push("回调路径被 Gateway Auth / Zero Trust / 反向代理鉴权拦截");
|
|
45
|
+
hints.push("企业微信回调与健康探测必须直达 webhook 路径,不能要求 Authorization、Cookie 或交互登录");
|
|
46
|
+
hints.push(authScopeHint);
|
|
47
|
+
} else if ([301, 302, 303, 307, 308].includes(Number(status))) {
|
|
48
|
+
reason = "redirect-auth";
|
|
49
|
+
hints.push("回调路径发生了重定向,通常被登录页、SSO 或前端路由接管");
|
|
50
|
+
if (location) hints.push(`重定向目标:${location}`);
|
|
51
|
+
hints.push("请让 webhook 路径直接反代到 OpenClaw 网关,不要跳转到登录页或前端应用");
|
|
52
|
+
} else if (status === 502 || status === 503 || status === 504) {
|
|
53
|
+
reason = "gateway-unreachable";
|
|
54
|
+
if (gatewayPort != null) {
|
|
55
|
+
hints.push(`网关 ${gatewayPort} 端口不可达或反向代理后端异常`);
|
|
56
|
+
} else {
|
|
57
|
+
hints.push("网关端口不可达或反向代理后端异常");
|
|
58
|
+
}
|
|
59
|
+
} else if (status === 200 && isHtmlBody(rawBody)) {
|
|
60
|
+
reason = "html-fallback";
|
|
61
|
+
hints.push("返回了 WebUI HTML,通常表示 webhook 路由未注册或 webhookPath 配置不一致");
|
|
62
|
+
if (webhookPath) {
|
|
63
|
+
const configPathHint =
|
|
64
|
+
normalizedMode === "bot"
|
|
65
|
+
? `请确认 channels.wecom.bot.webhookPath=${webhookPath} 与企业微信后台回调地址完全一致`
|
|
66
|
+
: `请确认 channels.wecom.webhookPath=${webhookPath} 与企业微信后台回调地址完全一致`;
|
|
67
|
+
hints.push(configPathHint);
|
|
68
|
+
}
|
|
69
|
+
hints.push("确认插件已加载:plugins.entries.openclaw-wechat.enabled=true 且 plugins.allow 包含 openclaw-wechat");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
detail: `status=${status} body=${preview}${hints.length > 0 ? ` hint=${hints.join(";")}` : ""}`,
|
|
75
|
+
data: {
|
|
76
|
+
status,
|
|
77
|
+
reason,
|
|
78
|
+
mode: normalizedMode,
|
|
79
|
+
endpoint: endpoint || null,
|
|
80
|
+
webhookPath: webhookPath || null,
|
|
81
|
+
gatewayPort: gatewayPort == null ? null : gatewayPort,
|
|
82
|
+
location: location || null,
|
|
83
|
+
hints,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import pluginManifest from "../../openclaw.plugin.json" with { type: "json" };
|
|
2
|
+
|
|
3
|
+
function asObject(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const manifestConfigSchema = asObject(pluginManifest?.configSchema);
|
|
9
|
+
const manifestUiHints = asObject(pluginManifest?.uiHints) ?? {};
|
|
10
|
+
|
|
11
|
+
const localizedUiHints = {
|
|
12
|
+
name: {
|
|
13
|
+
label: "渠道显示名",
|
|
14
|
+
help: "仅用于展示,不影响消息路由。",
|
|
15
|
+
},
|
|
16
|
+
enabled: {
|
|
17
|
+
label: "启用企业微信渠道",
|
|
18
|
+
help: "开启后才会接收/发送企业微信消息。",
|
|
19
|
+
},
|
|
20
|
+
corpId: {
|
|
21
|
+
label: "企业 ID(CorpId)",
|
|
22
|
+
placeholder: "wwxxxxxxxxxxxxxxxx",
|
|
23
|
+
},
|
|
24
|
+
corpSecret: {
|
|
25
|
+
label: "应用 Secret(CorpSecret)",
|
|
26
|
+
sensitive: true,
|
|
27
|
+
},
|
|
28
|
+
agentId: {
|
|
29
|
+
label: "应用 AgentId",
|
|
30
|
+
placeholder: "1000002",
|
|
31
|
+
},
|
|
32
|
+
callbackToken: {
|
|
33
|
+
label: "回调 Token",
|
|
34
|
+
sensitive: true,
|
|
35
|
+
},
|
|
36
|
+
callbackAesKey: {
|
|
37
|
+
label: "回调 EncodingAESKey",
|
|
38
|
+
sensitive: true,
|
|
39
|
+
},
|
|
40
|
+
webhookPath: {
|
|
41
|
+
label: "自建应用回调路径",
|
|
42
|
+
placeholder: "/wecom/callback",
|
|
43
|
+
},
|
|
44
|
+
outboundProxy: {
|
|
45
|
+
label: "WeCom 出站代理",
|
|
46
|
+
placeholder: "http://127.0.0.1:7890",
|
|
47
|
+
},
|
|
48
|
+
defaultAccount: {
|
|
49
|
+
label: "默认账号",
|
|
50
|
+
help: "文档工具等多账号能力未显式指定账号时优先使用该账号。",
|
|
51
|
+
},
|
|
52
|
+
tools: {
|
|
53
|
+
label: "工具能力",
|
|
54
|
+
help: "控制 OpenClaw 工具级能力是否启用。",
|
|
55
|
+
},
|
|
56
|
+
"tools.doc": {
|
|
57
|
+
label: "启用文档工具",
|
|
58
|
+
},
|
|
59
|
+
"tools.docAutoGrantRequesterCollaborator": {
|
|
60
|
+
label: "创建后自动加当前发送者为协作者",
|
|
61
|
+
help: "仅在 WeCom 会话中生效;创建文档后会把当前发送者自动加入协作者。",
|
|
62
|
+
},
|
|
63
|
+
accounts: {
|
|
64
|
+
label: "多账号配置",
|
|
65
|
+
help: "按账户 ID 管理多套企业微信配置。",
|
|
66
|
+
},
|
|
67
|
+
"accounts.*.enabled": {
|
|
68
|
+
label: "启用该账号",
|
|
69
|
+
},
|
|
70
|
+
"accounts.*.name": {
|
|
71
|
+
label: "账号名称",
|
|
72
|
+
},
|
|
73
|
+
"accounts.*.corpId": {
|
|
74
|
+
label: "账号 CorpId",
|
|
75
|
+
},
|
|
76
|
+
"accounts.*.corpSecret": {
|
|
77
|
+
label: "账号 CorpSecret",
|
|
78
|
+
sensitive: true,
|
|
79
|
+
},
|
|
80
|
+
"accounts.*.agentId": {
|
|
81
|
+
label: "账号 AgentId",
|
|
82
|
+
},
|
|
83
|
+
"accounts.*.callbackToken": {
|
|
84
|
+
label: "账号回调 Token",
|
|
85
|
+
sensitive: true,
|
|
86
|
+
},
|
|
87
|
+
"accounts.*.callbackAesKey": {
|
|
88
|
+
label: "账号回调 EncodingAESKey",
|
|
89
|
+
sensitive: true,
|
|
90
|
+
},
|
|
91
|
+
"accounts.*.webhookPath": {
|
|
92
|
+
label: "账号回调路径",
|
|
93
|
+
},
|
|
94
|
+
"accounts.*.tools": {
|
|
95
|
+
label: "账号工具能力",
|
|
96
|
+
},
|
|
97
|
+
"accounts.*.tools.doc": {
|
|
98
|
+
label: "启用该账号文档工具",
|
|
99
|
+
},
|
|
100
|
+
"accounts.*.tools.docAutoGrantRequesterCollaborator": {
|
|
101
|
+
label: "自动加当前发送者为协作者",
|
|
102
|
+
},
|
|
103
|
+
bot: {
|
|
104
|
+
label: "企业微信 Bot 模式",
|
|
105
|
+
help: "用于企业微信群机器人/Bot 回调与回包。",
|
|
106
|
+
},
|
|
107
|
+
"bot.enabled": {
|
|
108
|
+
label: "启用 Bot 模式",
|
|
109
|
+
},
|
|
110
|
+
"bot.token": {
|
|
111
|
+
label: "Bot Token",
|
|
112
|
+
sensitive: true,
|
|
113
|
+
},
|
|
114
|
+
"bot.encodingAesKey": {
|
|
115
|
+
label: "Bot EncodingAESKey",
|
|
116
|
+
sensitive: true,
|
|
117
|
+
},
|
|
118
|
+
"bot.webhookPath": {
|
|
119
|
+
label: "Bot 回调路径",
|
|
120
|
+
placeholder: "/wecom/bot/callback",
|
|
121
|
+
},
|
|
122
|
+
"bot.longConnection": {
|
|
123
|
+
label: "Bot 长连接",
|
|
124
|
+
help: "企业微信智能机器人长连接(WebSocket)模式,无需公网回调地址。",
|
|
125
|
+
},
|
|
126
|
+
"bot.longConnection.enabled": {
|
|
127
|
+
label: "启用 Bot 长连接",
|
|
128
|
+
},
|
|
129
|
+
"bot.longConnection.botId": {
|
|
130
|
+
label: "BotID",
|
|
131
|
+
},
|
|
132
|
+
"bot.longConnection.secret": {
|
|
133
|
+
label: "长连接 Secret",
|
|
134
|
+
sensitive: true,
|
|
135
|
+
},
|
|
136
|
+
"bot.longConnection.url": {
|
|
137
|
+
label: "长连接地址",
|
|
138
|
+
placeholder: "wss://openws.work.weixin.qq.com",
|
|
139
|
+
},
|
|
140
|
+
"bot.longConnection.pingIntervalMs": {
|
|
141
|
+
label: "心跳间隔(毫秒)",
|
|
142
|
+
},
|
|
143
|
+
"bot.longConnection.reconnectDelayMs": {
|
|
144
|
+
label: "重连基准延迟(毫秒)",
|
|
145
|
+
},
|
|
146
|
+
"bot.longConnection.maxReconnectDelayMs": {
|
|
147
|
+
label: "最大重连延迟(毫秒)",
|
|
148
|
+
},
|
|
149
|
+
"bot.replyTimeoutMs": {
|
|
150
|
+
label: "Bot 回复超时(毫秒)",
|
|
151
|
+
},
|
|
152
|
+
"bot.streamExpireMs": {
|
|
153
|
+
label: "Bot 流会话保留(毫秒)",
|
|
154
|
+
},
|
|
155
|
+
"bot.placeholderText": {
|
|
156
|
+
label: "Bot 首包占位文本",
|
|
157
|
+
},
|
|
158
|
+
"accounts.*.bot.longConnection": {
|
|
159
|
+
label: "账号 Bot 长连接",
|
|
160
|
+
},
|
|
161
|
+
"accounts.*.bot.longConnection.enabled": {
|
|
162
|
+
label: "启用该账号长连接",
|
|
163
|
+
},
|
|
164
|
+
"accounts.*.bot.longConnection.botId": {
|
|
165
|
+
label: "账号 BotID",
|
|
166
|
+
},
|
|
167
|
+
"accounts.*.bot.longConnection.secret": {
|
|
168
|
+
label: "账号长连接 Secret",
|
|
169
|
+
sensitive: true,
|
|
170
|
+
},
|
|
171
|
+
"accounts.*.bot.longConnection.url": {
|
|
172
|
+
label: "账号长连接地址",
|
|
173
|
+
},
|
|
174
|
+
"accounts.*.bot.longConnection.pingIntervalMs": {
|
|
175
|
+
label: "账号心跳间隔(毫秒)",
|
|
176
|
+
},
|
|
177
|
+
"accounts.*.bot.longConnection.reconnectDelayMs": {
|
|
178
|
+
label: "账号重连基准延迟(毫秒)",
|
|
179
|
+
},
|
|
180
|
+
"accounts.*.bot.longConnection.maxReconnectDelayMs": {
|
|
181
|
+
label: "账号最大重连延迟(毫秒)",
|
|
182
|
+
},
|
|
183
|
+
webhookBot: {
|
|
184
|
+
label: "Webhook Bot 出站回包",
|
|
185
|
+
},
|
|
186
|
+
"webhookBot.enabled": {
|
|
187
|
+
label: "启用 Webhook Bot 回包",
|
|
188
|
+
},
|
|
189
|
+
"webhookBot.url": {
|
|
190
|
+
label: "Webhook Bot URL",
|
|
191
|
+
},
|
|
192
|
+
"webhookBot.key": {
|
|
193
|
+
label: "Webhook Bot Key",
|
|
194
|
+
sensitive: true,
|
|
195
|
+
},
|
|
196
|
+
groupChat: {
|
|
197
|
+
label: "群聊触发策略",
|
|
198
|
+
},
|
|
199
|
+
"groupChat.triggerMode": {
|
|
200
|
+
label: "群聊触发模式",
|
|
201
|
+
},
|
|
202
|
+
dynamicAgent: {
|
|
203
|
+
label: "动态 Agent 路由",
|
|
204
|
+
},
|
|
205
|
+
dm: {
|
|
206
|
+
label: "私聊策略",
|
|
207
|
+
},
|
|
208
|
+
commands: {
|
|
209
|
+
label: "指令白名单",
|
|
210
|
+
},
|
|
211
|
+
events: {
|
|
212
|
+
label: "事件消息策略",
|
|
213
|
+
},
|
|
214
|
+
voiceTranscription: {
|
|
215
|
+
label: "语音转写",
|
|
216
|
+
},
|
|
217
|
+
"voiceTranscription.enabled": {
|
|
218
|
+
label: "启用语音转写",
|
|
219
|
+
},
|
|
220
|
+
"voiceTranscription.command": {
|
|
221
|
+
label: "本地转写命令",
|
|
222
|
+
placeholder: "whisper / whisper-cli",
|
|
223
|
+
},
|
|
224
|
+
"voiceTranscription.modelPath": {
|
|
225
|
+
label: "本地模型路径",
|
|
226
|
+
},
|
|
227
|
+
"voiceTranscription.language": {
|
|
228
|
+
label: "转写语言",
|
|
229
|
+
placeholder: "zh",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const wecomChannelConfigSchema = manifestConfigSchema ?? {
|
|
234
|
+
type: "object",
|
|
235
|
+
additionalProperties: true,
|
|
236
|
+
properties: {},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const wecomChannelConfigUiHints = {
|
|
240
|
+
...manifestUiHints,
|
|
241
|
+
...localizedUiHints,
|
|
242
|
+
};
|
|
@@ -1,9 +1,138 @@
|
|
|
1
|
+
import { wecomChannelConfigSchema, wecomChannelConfigUiHints } from "./channel-config-schema.js";
|
|
2
|
+
import {
|
|
3
|
+
getWecomChannelInboundActivity,
|
|
4
|
+
getWecomInboundActivity,
|
|
5
|
+
} from "./channel-status-state.js";
|
|
6
|
+
|
|
1
7
|
function assertFunction(name, fn) {
|
|
2
8
|
if (typeof fn !== "function") {
|
|
3
9
|
throw new Error(`createWecomChannelPlugin: ${name} is required`);
|
|
4
10
|
}
|
|
5
11
|
}
|
|
6
12
|
|
|
13
|
+
function readString(value) {
|
|
14
|
+
const trimmed = String(value ?? "").trim();
|
|
15
|
+
return trimmed || "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readNumber(value) {
|
|
19
|
+
const num = Number(value);
|
|
20
|
+
return Number.isFinite(num) ? num : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTimestampMs(value) {
|
|
24
|
+
if (value == null || value === "") return null;
|
|
25
|
+
const direct = Number(value);
|
|
26
|
+
if (Number.isFinite(direct) && direct > 0) {
|
|
27
|
+
return direct < 1e12 ? Math.floor(direct * 1000) : Math.floor(direct);
|
|
28
|
+
}
|
|
29
|
+
const parsed = Date.parse(String(value));
|
|
30
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveBotCallbackConfig(cfg, accountId = "default") {
|
|
35
|
+
const normalizedAccountId = readString(accountId).toLowerCase() || "default";
|
|
36
|
+
const channelConfig = cfg?.channels?.wecom;
|
|
37
|
+
const accountConfig = channelConfig?.accounts?.[normalizedAccountId];
|
|
38
|
+
const accountBot = accountConfig?.bot;
|
|
39
|
+
const channelBot = channelConfig?.bot;
|
|
40
|
+
|
|
41
|
+
const enabled =
|
|
42
|
+
accountBot?.enabled ??
|
|
43
|
+
channelBot?.enabled ??
|
|
44
|
+
false;
|
|
45
|
+
const token = readString(
|
|
46
|
+
accountBot?.token ??
|
|
47
|
+
accountBot?.callbackToken ??
|
|
48
|
+
channelBot?.token ??
|
|
49
|
+
channelBot?.callbackToken ??
|
|
50
|
+
channelConfig?.token ??
|
|
51
|
+
channelConfig?.callbackToken,
|
|
52
|
+
);
|
|
53
|
+
const aesKey = readString(
|
|
54
|
+
accountBot?.encodingAesKey ??
|
|
55
|
+
accountBot?.callbackAesKey ??
|
|
56
|
+
channelBot?.encodingAesKey ??
|
|
57
|
+
channelBot?.callbackAesKey ??
|
|
58
|
+
channelConfig?.encodingAesKey ??
|
|
59
|
+
channelConfig?.callbackAesKey,
|
|
60
|
+
);
|
|
61
|
+
const webhookPath = readString(
|
|
62
|
+
accountBot?.webhookPath ?? channelBot?.webhookPath,
|
|
63
|
+
);
|
|
64
|
+
const longConnection =
|
|
65
|
+
accountBot?.longConnection && typeof accountBot.longConnection === "object"
|
|
66
|
+
? accountBot.longConnection
|
|
67
|
+
: channelBot?.longConnection && typeof channelBot.longConnection === "object"
|
|
68
|
+
? channelBot.longConnection
|
|
69
|
+
: {};
|
|
70
|
+
const longConnectionEnabled = longConnection?.enabled === true;
|
|
71
|
+
const longConnectionBotId = readString(longConnection?.botId ?? longConnection?.botid);
|
|
72
|
+
const longConnectionSecret = readString(longConnection?.secret);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
enabled: enabled === true,
|
|
76
|
+
token,
|
|
77
|
+
aesKey,
|
|
78
|
+
webhookPath,
|
|
79
|
+
longConnectionEnabled,
|
|
80
|
+
longConnectionBotId,
|
|
81
|
+
longConnectionSecret,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasConfiguredBotCallback(cfg, accountId = "default") {
|
|
86
|
+
const bot = resolveBotCallbackConfig(cfg, accountId);
|
|
87
|
+
return (
|
|
88
|
+
bot.enabled &&
|
|
89
|
+
((Boolean(bot.token) && Boolean(bot.aesKey)) ||
|
|
90
|
+
(bot.longConnectionEnabled && Boolean(bot.longConnectionBotId) && Boolean(bot.longConnectionSecret)))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasConfiguredAgentCredentials(account) {
|
|
95
|
+
return Boolean(
|
|
96
|
+
readString(account?.corpId) &&
|
|
97
|
+
readString(account?.corpSecret) &&
|
|
98
|
+
readNumber(account?.agentId),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildWecomAccountSnapshot(account, cfg, runtime = {}) {
|
|
103
|
+
const accountId = readString(account?.accountId).toLowerCase() || "default";
|
|
104
|
+
const agentConfigured = hasConfiguredAgentCredentials(account);
|
|
105
|
+
const botConfig = resolveBotCallbackConfig(cfg, accountId);
|
|
106
|
+
const botConfigured = hasConfiguredBotCallback(cfg, accountId);
|
|
107
|
+
const configured = agentConfigured || botConfigured;
|
|
108
|
+
const enabled = account?.enabled !== false;
|
|
109
|
+
const inboundActivity = getWecomInboundActivity(accountId);
|
|
110
|
+
const mode = agentConfigured && botConfigured ? "agent+bot" : botConfigured ? "bot" : "agent";
|
|
111
|
+
const running = runtime?.running ?? (enabled && configured);
|
|
112
|
+
const connected =
|
|
113
|
+
runtime?.connected ??
|
|
114
|
+
inboundActivity?.connected ??
|
|
115
|
+
(running && configured);
|
|
116
|
+
const lastInboundAt =
|
|
117
|
+
normalizeTimestampMs(runtime?.lastInboundAt ?? runtime?.lastInbound) ??
|
|
118
|
+
normalizeTimestampMs(inboundActivity?.lastInboundAtMs ?? inboundActivity?.lastInbound) ??
|
|
119
|
+
null;
|
|
120
|
+
const localizedName = accountId === "default" ? "默认账号" : accountId;
|
|
121
|
+
return {
|
|
122
|
+
...runtime,
|
|
123
|
+
accountId,
|
|
124
|
+
name: readString(account?.name) || localizedName,
|
|
125
|
+
displayName: readString(account?.name) || localizedName,
|
|
126
|
+
enabled,
|
|
127
|
+
configured,
|
|
128
|
+
running,
|
|
129
|
+
connected,
|
|
130
|
+
lastInboundAt,
|
|
131
|
+
mode,
|
|
132
|
+
webhookPath: readString(account?.webhookPath) || botConfig.webhookPath || runtime?.webhookPath || undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
7
136
|
export function createWecomChannelPlugin({
|
|
8
137
|
listWecomAccountIds,
|
|
9
138
|
getWecomConfig,
|
|
@@ -29,12 +158,16 @@ export function createWecomChannelPlugin({
|
|
|
29
158
|
id: "wecom",
|
|
30
159
|
meta: {
|
|
31
160
|
id: "wecom",
|
|
32
|
-
label: "WeCom",
|
|
33
|
-
selectionLabel: "WeCom
|
|
161
|
+
label: "企业微信 WeCom",
|
|
162
|
+
selectionLabel: "企业微信 WeCom(自建应用/Bot)",
|
|
34
163
|
docsPath: "/channels/wecom",
|
|
35
|
-
blurb: "
|
|
164
|
+
blurb: "企业微信消息通道(自建应用回调 + Bot 回调 + 发送 API)。",
|
|
36
165
|
aliases: ["wework", "qiwei", "wxwork"],
|
|
37
166
|
},
|
|
167
|
+
configSchema: {
|
|
168
|
+
schema: wecomChannelConfigSchema,
|
|
169
|
+
uiHints: wecomChannelConfigUiHints,
|
|
170
|
+
},
|
|
38
171
|
capabilities: {
|
|
39
172
|
chatTypes: ["direct", "group"],
|
|
40
173
|
media: {
|
|
@@ -44,11 +177,36 @@ export function createWecomChannelPlugin({
|
|
|
44
177
|
markdown: true,
|
|
45
178
|
},
|
|
46
179
|
config: {
|
|
47
|
-
listAccountIds: (cfg) =>
|
|
180
|
+
listAccountIds: (cfg) => {
|
|
181
|
+
const accountIds = listWecomAccountIds({ config: cfg });
|
|
182
|
+
if (accountIds.length > 0) return accountIds;
|
|
183
|
+
return hasConfiguredBotCallback(cfg, "default") ? ["default"] : [];
|
|
184
|
+
},
|
|
48
185
|
resolveAccount: (cfg, accountId) =>
|
|
49
186
|
(getWecomConfig({ config: cfg }, accountId ?? "default") ?? {
|
|
50
187
|
accountId: accountId ?? "default",
|
|
51
188
|
}),
|
|
189
|
+
isConfigured: (account, cfg) =>
|
|
190
|
+
hasConfiguredAgentCredentials(account) || hasConfiguredBotCallback(cfg, account?.accountId ?? "default"),
|
|
191
|
+
describeAccount: (account, cfg) => buildWecomAccountSnapshot(account, cfg),
|
|
192
|
+
},
|
|
193
|
+
status: {
|
|
194
|
+
buildAccountSnapshot: ({ account, cfg, runtime }) =>
|
|
195
|
+
buildWecomAccountSnapshot(account, cfg, runtime),
|
|
196
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
197
|
+
configured: snapshot?.configured ?? false,
|
|
198
|
+
running: snapshot?.running ?? false,
|
|
199
|
+
connected:
|
|
200
|
+
snapshot?.connected ??
|
|
201
|
+
(snapshot?.running && snapshot?.configured) ??
|
|
202
|
+
null,
|
|
203
|
+
lastInbound:
|
|
204
|
+
normalizeTimestampMs(snapshot?.lastInboundAt ?? snapshot?.lastInbound) ??
|
|
205
|
+
normalizeTimestampMs(
|
|
206
|
+
getWecomChannelInboundActivity([snapshot?.accountId]).lastInboundAtMs,
|
|
207
|
+
) ??
|
|
208
|
+
null,
|
|
209
|
+
}),
|
|
52
210
|
},
|
|
53
211
|
outbound: {
|
|
54
212
|
deliveryMode: "direct",
|