ylib-wecom-openclaw-plugin 2026.4.29
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/README.md +596 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +99 -0
- package/dist/src/accounts.d.ts +57 -0
- package/dist/src/accounts.js +247 -0
- package/dist/src/agent/api-client.d.ts +95 -0
- package/dist/src/agent/api-client.js +425 -0
- package/dist/src/agent/handler.d.ts +64 -0
- package/dist/src/agent/handler.js +731 -0
- package/dist/src/agent/index.d.ts +5 -0
- package/dist/src/agent/index.js +21 -0
- package/dist/src/agent/webhook.d.ts +25 -0
- package/dist/src/agent/webhook.js +294 -0
- package/dist/src/agent/xml.d.ts +21 -0
- package/dist/src/agent/xml.js +43 -0
- package/dist/src/channel.d.ts +5 -0
- package/dist/src/channel.js +815 -0
- package/dist/src/chat-queue.d.ts +31 -0
- package/dist/src/chat-queue.js +53 -0
- package/dist/src/config-schema.d.ts +587 -0
- package/dist/src/config-schema.js +146 -0
- package/dist/src/const.d.ts +128 -0
- package/dist/src/const.js +168 -0
- package/dist/src/dm-policy.d.ts +29 -0
- package/dist/src/dm-policy.js +146 -0
- package/dist/src/dynamic-agent.d.ts +37 -0
- package/dist/src/dynamic-agent.js +67 -0
- package/dist/src/dynamic-routing.d.ts +65 -0
- package/dist/src/dynamic-routing.js +62 -0
- package/dist/src/endpoint-dispatch.d.ts +54 -0
- package/dist/src/endpoint-dispatch.js +967 -0
- package/dist/src/endpoint-event-adapter.d.ts +15 -0
- package/dist/src/endpoint-event-adapter.js +427 -0
- package/dist/src/group-policy.d.ts +30 -0
- package/dist/src/group-policy.js +126 -0
- package/dist/src/http.d.ts +27 -0
- package/dist/src/http.js +168 -0
- package/dist/src/im-runtime-telemetry.d.ts +25 -0
- package/dist/src/im-runtime-telemetry.js +68 -0
- package/dist/src/interface.d.ts +192 -0
- package/dist/src/interface.js +5 -0
- package/dist/src/markdown-chunk.d.ts +1 -0
- package/dist/src/markdown-chunk.js +396 -0
- package/dist/src/mcp/index.d.ts +6 -0
- package/dist/src/mcp/index.js +28 -0
- package/dist/src/mcp/interceptors/biz-error.d.ts +11 -0
- package/dist/src/mcp/interceptors/biz-error.js +73 -0
- package/dist/src/mcp/interceptors/doc-auth-error.d.ts +10 -0
- package/dist/src/mcp/interceptors/doc-auth-error.js +235 -0
- package/dist/src/mcp/interceptors/index.d.ts +35 -0
- package/dist/src/mcp/interceptors/index.js +143 -0
- package/dist/src/mcp/interceptors/msg-media.d.ts +11 -0
- package/dist/src/mcp/interceptors/msg-media.js +201 -0
- package/dist/src/mcp/interceptors/smartpage-create.d.ts +30 -0
- package/dist/src/mcp/interceptors/smartpage-create.js +252 -0
- package/dist/src/mcp/interceptors/smartpage-export.d.ts +17 -0
- package/dist/src/mcp/interceptors/smartpage-export.js +135 -0
- package/dist/src/mcp/interceptors/smartsheet-upload.d.ts +22 -0
- package/dist/src/mcp/interceptors/smartsheet-upload.js +388 -0
- package/dist/src/mcp/interceptors/types.d.ts +64 -0
- package/dist/src/mcp/interceptors/types.js +8 -0
- package/dist/src/mcp/schema.d.ts +11 -0
- package/dist/src/mcp/schema.js +115 -0
- package/dist/src/mcp/tool.d.ts +63 -0
- package/dist/src/mcp/tool.js +318 -0
- package/dist/src/mcp/transport.d.ts +94 -0
- package/dist/src/mcp/transport.js +702 -0
- package/dist/src/media-handler.d.ts +55 -0
- package/dist/src/media-handler.js +306 -0
- package/dist/src/media-uploader.d.ts +142 -0
- package/dist/src/media-uploader.js +446 -0
- package/dist/src/message-parser.d.ts +104 -0
- package/dist/src/message-parser.js +232 -0
- package/dist/src/message-sender.d.ts +54 -0
- package/dist/src/message-sender.js +210 -0
- package/dist/src/monitor.d.ts +69 -0
- package/dist/src/monitor.js +1846 -0
- package/dist/src/onboarding.d.ts +8 -0
- package/dist/src/onboarding.js +248 -0
- package/dist/src/openclaw-compat.d.ts +148 -0
- package/dist/src/openclaw-compat.js +839 -0
- package/dist/src/proactive-markdown-send.d.ts +14 -0
- package/dist/src/proactive-markdown-send.js +205 -0
- package/dist/src/reqid-store.d.ts +23 -0
- package/dist/src/reqid-store.js +136 -0
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +7 -0
- package/dist/src/shared/command-auth.d.ts +23 -0
- package/dist/src/shared/command-auth.js +112 -0
- package/dist/src/shared/xml-parser.d.ts +46 -0
- package/dist/src/shared/xml-parser.js +228 -0
- package/dist/src/state-dir-resolve.d.ts +2 -0
- package/dist/src/state-dir-resolve.js +33 -0
- package/dist/src/state-manager.d.ts +115 -0
- package/dist/src/state-manager.js +413 -0
- package/dist/src/target.d.ts +35 -0
- package/dist/src/target.js +71 -0
- package/dist/src/template-card-manager.d.ts +55 -0
- package/dist/src/template-card-manager.js +316 -0
- package/dist/src/template-card-parser.d.ts +37 -0
- package/dist/src/template-card-parser.js +672 -0
- package/dist/src/timeout.d.ts +20 -0
- package/dist/src/timeout.js +57 -0
- package/dist/src/types/account.d.ts +29 -0
- package/dist/src/types/account.js +5 -0
- package/dist/src/types/config.d.ts +98 -0
- package/dist/src/types/config.js +8 -0
- package/dist/src/types/constants.d.ts +42 -0
- package/dist/src/types/constants.js +45 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.js +17 -0
- package/dist/src/types/message.d.ts +238 -0
- package/dist/src/types/message.js +6 -0
- package/dist/src/utils.d.ts +148 -0
- package/dist/src/utils.js +92 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +28 -0
- package/dist/src/webhook/command-auth.d.ts +47 -0
- package/dist/src/webhook/command-auth.js +137 -0
- package/dist/src/webhook/gateway.d.ts +36 -0
- package/dist/src/webhook/gateway.js +297 -0
- package/dist/src/webhook/handler.d.ts +19 -0
- package/dist/src/webhook/handler.js +481 -0
- package/dist/src/webhook/helpers.d.ts +157 -0
- package/dist/src/webhook/helpers.js +936 -0
- package/dist/src/webhook/http.d.ts +27 -0
- package/dist/src/webhook/http.js +168 -0
- package/dist/src/webhook/index.d.ts +11 -0
- package/dist/src/webhook/index.js +43 -0
- package/dist/src/webhook/media.d.ts +30 -0
- package/dist/src/webhook/media.js +152 -0
- package/dist/src/webhook/monitor.d.ts +59 -0
- package/dist/src/webhook/monitor.js +1672 -0
- package/dist/src/webhook/state.d.ts +220 -0
- package/dist/src/webhook/state.js +568 -0
- package/dist/src/webhook/target.d.ts +41 -0
- package/dist/src/webhook/target.js +165 -0
- package/dist/src/webhook/types.d.ts +348 -0
- package/dist/src/webhook/types.js +36 -0
- package/dist/src/webhook/video-frame.d.ts +13 -0
- package/dist/src/webhook/video-frame.js +108 -0
- package/openclaw.plugin.json +19 -0
- package/package.json +96 -0
- package/schema.json +534 -0
- package/scripts/generate-schema.mjs +33 -0
- package/skills/wecom-contact/SKILL.md +162 -0
- package/skills/wecom-doc/SKILL.md +162 -0
- package/skills/wecom-doc/references/create-doc.md +56 -0
- package/skills/wecom-doc/references/edit-doc-content.md +68 -0
- package/skills/wecom-doc/references/get-doc-content.md +88 -0
- package/skills/wecom-doc/references/smartpage-create.md +125 -0
- package/skills/wecom-doc/references/smartpage-export.md +160 -0
- package/skills/wecom-meeting/SKILL.md +441 -0
- package/skills/wecom-meeting/references/example-full.md +30 -0
- package/skills/wecom-meeting/references/example-reminder.md +46 -0
- package/skills/wecom-meeting/references/example-security.md +22 -0
- package/skills/wecom-meeting/references/response-get-meeting-info.md +148 -0
- package/skills/wecom-msg/SKILL.md +157 -0
- package/skills/wecom-msg/references/api-get-messages.md +93 -0
- package/skills/wecom-msg/references/api-get-msg-chat-list.md +58 -0
- package/skills/wecom-msg/references/api-get-msg-media.md +44 -0
- package/skills/wecom-msg/references/api-send-message.md +39 -0
- package/skills/wecom-preflight/SKILL.md +141 -0
- package/skills/wecom-schedule/SKILL.md +161 -0
- package/skills/wecom-schedule/references/api-check-availability.md +56 -0
- package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
- package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
- package/skills/wecom-schedule/references/api-update-schedule.md +32 -0
- package/skills/wecom-schedule/references/ref-reminders.md +24 -0
- package/skills/wecom-send-media/SKILL.md +68 -0
- package/skills/wecom-send-template-card/SKILL.md +157 -0
- package/skills/wecom-send-template-card/references/api-template-card-types.md +358 -0
- package/skills/wecom-smartsheet/SKILL.md +164 -0
- package/skills/wecom-smartsheet/references/smartsheet-cell-value-formats.md +163 -0
- package/skills/wecom-smartsheet/references/smartsheet-field-types.md +44 -0
- package/skills/wecom-smartsheet/references/smartsheet-get-records.md +96 -0
- package/skills/wecom-smartsheet/references/webhook-examples.md +185 -0
- package/skills/wecom-smartsheet/references/webhook-fallback.md +184 -0
- package/skills/wecom-todo/SKILL.md +392 -0
- package/skills/wecom-todo/examples/workflows.md +163 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook HTTP 请求处理
|
|
4
|
+
*
|
|
5
|
+
* 从 @mocrane/wecom monitor.ts handleWecomWebhookRequest 部分迁移 + 重构。
|
|
6
|
+
* 负责:
|
|
7
|
+
* 1. GET/POST 请求分流
|
|
8
|
+
* 2. 签名验证(调用 crypto 模块)
|
|
9
|
+
* 3. 消息解密
|
|
10
|
+
* 4. 按消息类型分发到 monitor 层
|
|
11
|
+
*/
|
|
12
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
13
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
14
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
15
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
16
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
17
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
18
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
22
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
23
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
24
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
25
|
+
function step(op) {
|
|
26
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
27
|
+
while (_) try {
|
|
28
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
29
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
30
|
+
switch (op[0]) {
|
|
31
|
+
case 0: case 1: t = op; break;
|
|
32
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
33
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
34
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
35
|
+
default:
|
|
36
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
37
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
38
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
39
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
40
|
+
if (t[2]) _.ops.pop();
|
|
41
|
+
_.trys.pop(); continue;
|
|
42
|
+
}
|
|
43
|
+
op = body.call(thisArg, _);
|
|
44
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
45
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
exports.__esModule = true;
|
|
52
|
+
exports.handleWecomWebhookRequest = void 0;
|
|
53
|
+
var node_crypto_1 = __importDefault(require("node:crypto"));
|
|
54
|
+
var target_js_1 = require("./target.js");
|
|
55
|
+
var utils_js_1 = require("../utils.js");
|
|
56
|
+
var monitor_js_1 = require("./monitor.js");
|
|
57
|
+
var target_js_2 = require("./target.js");
|
|
58
|
+
var helpers_js_1 = require("./helpers.js");
|
|
59
|
+
var aibot_node_sdk_1 = require("@wecom/aibot-node-sdk");
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// 辅助函数
|
|
62
|
+
// ============================================================================
|
|
63
|
+
/** 解析 URL 查询参数 */
|
|
64
|
+
function parseQuery(url) {
|
|
65
|
+
var idx = url.indexOf("?");
|
|
66
|
+
if (idx < 0)
|
|
67
|
+
return {};
|
|
68
|
+
var params = new URLSearchParams(url.slice(idx + 1));
|
|
69
|
+
var result = {};
|
|
70
|
+
for (var _i = 0, params_1 = params; _i < params_1.length; _i++) {
|
|
71
|
+
var _a = params_1[_i], key = _a[0], value = _a[1];
|
|
72
|
+
result[key] = value;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 从查询参数中提取签名字段
|
|
78
|
+
*
|
|
79
|
+
* 企微不同场景下签名参数名不一致,按优先级依次尝试:
|
|
80
|
+
* msg_signature → msgsignature → signature
|
|
81
|
+
*/
|
|
82
|
+
function resolveSignatureParam(query) {
|
|
83
|
+
var _a, _b, _c;
|
|
84
|
+
return (_c = (_b = (_a = query.msg_signature) !== null && _a !== void 0 ? _a : query.msgsignature) !== null && _b !== void 0 ? _b : query.signature) !== null && _c !== void 0 ? _c : "";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 判断入站消息是否应该被处理(对齐原版 shouldProcessBotInboundMessage)
|
|
88
|
+
*
|
|
89
|
+
* 仅允许"真实用户消息"进入 Bot 会话:
|
|
90
|
+
* - 发送者缺失 → 丢弃(避免 unknown 会话串会话)
|
|
91
|
+
* - 发送者是 sys → 丢弃(避免系统回调触发 AI 自动回复)
|
|
92
|
+
* - 群消息缺失 chatid → 丢弃(避免 group:unknown 串群)
|
|
93
|
+
*/
|
|
94
|
+
function shouldProcessBotInboundMessage(msg) {
|
|
95
|
+
var _a, _b, _c;
|
|
96
|
+
var senderUserId = (_a = helpers_js_1.resolveWecomSenderUserId(msg)) === null || _a === void 0 ? void 0 : _a.trim();
|
|
97
|
+
if (!senderUserId) {
|
|
98
|
+
return { shouldProcess: false, reason: "missing_sender" };
|
|
99
|
+
}
|
|
100
|
+
if (senderUserId.toLowerCase() === "sys") {
|
|
101
|
+
return { shouldProcess: false, reason: "system_sender" };
|
|
102
|
+
}
|
|
103
|
+
// 企微 Bot 回调中 chattype 是扁平字段(非嵌套在 chat_info 内)
|
|
104
|
+
var chatType = String((_b = msg.chattype) !== null && _b !== void 0 ? _b : "").trim().toLowerCase();
|
|
105
|
+
if (chatType === "group") {
|
|
106
|
+
var chatId = (_c = msg.chatid) === null || _c === void 0 ? void 0 : _c.trim();
|
|
107
|
+
if (!chatId) {
|
|
108
|
+
return { shouldProcess: false, reason: "missing_chatid", senderUserId: senderUserId };
|
|
109
|
+
}
|
|
110
|
+
return { shouldProcess: true, reason: "user_message", senderUserId: senderUserId, chatId: chatId };
|
|
111
|
+
}
|
|
112
|
+
return { shouldProcess: true, reason: "user_message", senderUserId: senderUserId, chatId: senderUserId };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 从 Target 配置中提取预期的 Bot Identity 集合
|
|
116
|
+
*
|
|
117
|
+
* 用于 aibotid 校验:即使签名匹配,也要确认消息来自预期的 Bot。
|
|
118
|
+
*
|
|
119
|
+
* 配置来源(对齐用户 YAML 配置):
|
|
120
|
+
* - 单账号模式:channels.wecom.botId
|
|
121
|
+
* - 多账号模式:channels.wecom.accounts.xxx.botId
|
|
122
|
+
*
|
|
123
|
+
* 解析后的 botId 已在 account.botId 中,直接读取即可。
|
|
124
|
+
* 同时兼容 config 中可能存在的 aibotid(原版字段名)。
|
|
125
|
+
*/
|
|
126
|
+
function resolveBotIdentitySet(target) {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
var ids = new Set();
|
|
129
|
+
// account.botId — 从 YAML 配置中解析出的 botId(单账号/多账号均可)
|
|
130
|
+
var botId = (_a = target.account.botId) === null || _a === void 0 ? void 0 : _a.trim();
|
|
131
|
+
if (botId)
|
|
132
|
+
ids.add(botId);
|
|
133
|
+
// config.botId — 与 account.botId 相同来源(兜底)
|
|
134
|
+
var configBotId = (_b = target.account.config.botId) === null || _b === void 0 ? void 0 : _b.trim();
|
|
135
|
+
if (configBotId)
|
|
136
|
+
ids.add(configBotId);
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
/** POST body 最大允许字节数 (1 MB) */
|
|
140
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
141
|
+
/**
|
|
142
|
+
* 读取 HTTP 请求 body(带大小限制保护)
|
|
143
|
+
*
|
|
144
|
+
* 超过 maxBytes 时会主动销毁请求并拒绝,防止大包攻击。
|
|
145
|
+
*/
|
|
146
|
+
function readBody(req, maxBytes) {
|
|
147
|
+
if (maxBytes === void 0) { maxBytes = MAX_BODY_BYTES; }
|
|
148
|
+
return new Promise(function (resolve) {
|
|
149
|
+
var chunks = [];
|
|
150
|
+
var total = 0;
|
|
151
|
+
req.on("data", function (chunk) {
|
|
152
|
+
total += chunk.length;
|
|
153
|
+
if (total > maxBytes) {
|
|
154
|
+
resolve({ ok: false, error: "payload too large" });
|
|
155
|
+
req.destroy();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
chunks.push(chunk);
|
|
159
|
+
});
|
|
160
|
+
req.on("end", function () {
|
|
161
|
+
var raw = Buffer.concat(chunks).toString("utf8");
|
|
162
|
+
if (!raw.trim()) {
|
|
163
|
+
resolve({ ok: false, error: "empty payload" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
resolve({ ok: true, value: raw });
|
|
167
|
+
});
|
|
168
|
+
req.on("error", function (err) {
|
|
169
|
+
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** 构造加密 JSON 响应(返回对象,不做 stringify) */
|
|
174
|
+
function encryptResponse(target, responseData, timestamp, nonce) {
|
|
175
|
+
var plaintext = JSON.stringify(responseData);
|
|
176
|
+
var wc = new aibot_node_sdk_1.WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
|
|
177
|
+
var _a = wc.encrypt(plaintext, timestamp, nonce), encrypt = _a.encrypt, signature = _a.signature;
|
|
178
|
+
return { encrypt: encrypt, msgsignature: signature, timestamp: timestamp, nonce: nonce };
|
|
179
|
+
}
|
|
180
|
+
/** 发送 JSON 响应 (Content-Type: application/json) */
|
|
181
|
+
function sendJson(res, statusCode, data) {
|
|
182
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
183
|
+
res.end(JSON.stringify(data));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 发送加密回复响应 (Content-Type: text/plain)
|
|
187
|
+
*
|
|
188
|
+
* 企微官方参考实现要求加密 JSON 以 text/plain 返回。
|
|
189
|
+
*/
|
|
190
|
+
function sendEncryptedReply(res, data) {
|
|
191
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
192
|
+
res.end(JSON.stringify(data));
|
|
193
|
+
}
|
|
194
|
+
/** 发送纯文本响应 */
|
|
195
|
+
function sendText(res, statusCode, text) {
|
|
196
|
+
res.writeHead(statusCode, { "Content-Type": "text/plain charset=utf-8" });
|
|
197
|
+
res.end(text);
|
|
198
|
+
}
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// 路径解析
|
|
201
|
+
// ============================================================================
|
|
202
|
+
/**
|
|
203
|
+
* 标准化 Webhook 路径(不含 query string)
|
|
204
|
+
*/
|
|
205
|
+
function normalizeRequestPath(url) {
|
|
206
|
+
var idx = url.indexOf("?");
|
|
207
|
+
var pathname = idx >= 0 ? url.slice(0, idx) : url;
|
|
208
|
+
var trimmed = pathname.trim();
|
|
209
|
+
if (!trimmed || trimmed === "/")
|
|
210
|
+
return "/";
|
|
211
|
+
var withSlash = trimmed.startsWith("/") ? trimmed : "/" + trimmed;
|
|
212
|
+
if (withSlash.length > 1 && withSlash.endsWith("/"))
|
|
213
|
+
return withSlash.slice(0, -1);
|
|
214
|
+
return withSlash;
|
|
215
|
+
}
|
|
216
|
+
/** 按 accountId 去重 Target 列表(同一 account 注册多条路径时只保留第一个) */
|
|
217
|
+
function deduplicateByAccountId(targets) {
|
|
218
|
+
var seen = new Set();
|
|
219
|
+
var result = [];
|
|
220
|
+
for (var _i = 0, targets_1 = targets; _i < targets_1.length; _i++) {
|
|
221
|
+
var target = targets_1[_i];
|
|
222
|
+
if (!seen.has(target.account.accountId)) {
|
|
223
|
+
seen.add(target.account.accountId);
|
|
224
|
+
result.push(target);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 从已注册的 Target 中匹配签名
|
|
231
|
+
*
|
|
232
|
+
* 匹配策略:
|
|
233
|
+
* 1. 如果路径中有 accountId,优先精确匹配
|
|
234
|
+
* 2. 用 filter 收集所有签名匹配的 Target
|
|
235
|
+
* 3. 检查冲突:0 个 = not_found,1 个 = matched,>1 个 = conflict
|
|
236
|
+
*
|
|
237
|
+
* 与原版保持一致:检查 target.account.token 存在性,防止空 token 的误匹配。
|
|
238
|
+
*/
|
|
239
|
+
function findMatchingTarget(requestPath, signature, timestamp, nonce, encrypt, pathAccountId) {
|
|
240
|
+
var _a;
|
|
241
|
+
// 收集所有候选 Target(路径匹配 + 全局兜底)
|
|
242
|
+
var targetsMap = target_js_1.getWebhookTargetsMap();
|
|
243
|
+
var normalizedPath = normalizeRequestPath(requestPath);
|
|
244
|
+
var pathTargets = targetsMap.get(normalizedPath);
|
|
245
|
+
// 如果路径中有 accountId,优先精确匹配
|
|
246
|
+
if (pathAccountId && pathTargets) {
|
|
247
|
+
var byAccountId = pathTargets.find(function (t) { return t.account.accountId === pathAccountId; });
|
|
248
|
+
if ((_a = byAccountId === null || byAccountId === void 0 ? void 0 : byAccountId.account) === null || _a === void 0 ? void 0 : _a.token) {
|
|
249
|
+
var wc = new aibot_node_sdk_1.WecomCrypto(byAccountId.account.token, byAccountId.account.encodingAESKey, byAccountId.account.receiveId);
|
|
250
|
+
var ok = wc.verifySignature(signature, timestamp, nonce, encrypt);
|
|
251
|
+
if (ok)
|
|
252
|
+
return { status: "matched", target: byAccountId };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 收集候选列表(路径匹配优先,否则全局遍历)
|
|
256
|
+
var candidates = (pathTargets && pathTargets.length > 0)
|
|
257
|
+
? pathTargets
|
|
258
|
+
: target_js_1.getRegisteredTargets();
|
|
259
|
+
// filter 语义:收集所有签名匹配的 Target
|
|
260
|
+
var signatureMatches = candidates.filter(function (target) {
|
|
261
|
+
var _a;
|
|
262
|
+
if (!((_a = target === null || target === void 0 ? void 0 : target.account) === null || _a === void 0 ? void 0 : _a.token))
|
|
263
|
+
return false;
|
|
264
|
+
var wc = new aibot_node_sdk_1.WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
|
|
265
|
+
return wc.verifySignature(signature, timestamp, nonce, encrypt);
|
|
266
|
+
});
|
|
267
|
+
// 按 accountId 去重(同一 account 注册多条路径时,不应被误判为冲突)
|
|
268
|
+
var uniqueMatches = deduplicateByAccountId(signatureMatches);
|
|
269
|
+
if (uniqueMatches.length === 1) {
|
|
270
|
+
return { status: "matched", target: uniqueMatches[0] };
|
|
271
|
+
}
|
|
272
|
+
var candidateAccountIds = (uniqueMatches.length > 0 ? uniqueMatches : candidates)
|
|
273
|
+
.map(function (t) { return t.account.accountId; });
|
|
274
|
+
if (uniqueMatches.length === 0) {
|
|
275
|
+
return { status: "not_found", candidateAccountIds: candidateAccountIds };
|
|
276
|
+
}
|
|
277
|
+
// uniqueMatches.length > 1 → 多账号冲突
|
|
278
|
+
return { status: "conflict", candidateAccountIds: candidateAccountIds };
|
|
279
|
+
}
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// 主入口
|
|
282
|
+
// ============================================================================
|
|
283
|
+
/**
|
|
284
|
+
* Webhook HTTP 请求总入口
|
|
285
|
+
*
|
|
286
|
+
* 处理企微 Bot Webhook 的 GET(URL 验证)和 POST(消息回调)请求。
|
|
287
|
+
* 返回 true 表示已处理,false 表示不匹配(交给其他 handler)。
|
|
288
|
+
*/
|
|
289
|
+
function handleWecomWebhookRequest(req, res) {
|
|
290
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
291
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
292
|
+
var reqId, url, method, remote, ua, cl, query, hasTimestamp, hasNonce, hasEchostr, signature, hasSig, pathAccountId, timestamp, nonce, echostr, msgSignature, matchResult, target, wc, plaintext, timestamp, nonce, msgSignature, bodyResult, bodyStr, body, encrypt, matchResult, reason, detail, target, message, wc, plaintext, expectedBotIds, inboundAibotId, proxyUrl, responseData, encrypted, encrypted, err_1, errorResponse, encrypted;
|
|
293
|
+
return __generator(this, function (_1) {
|
|
294
|
+
switch (_1.label) {
|
|
295
|
+
case 0:
|
|
296
|
+
reqId = node_crypto_1["default"].randomUUID().slice(0, 8);
|
|
297
|
+
url = (_a = req.url) !== null && _a !== void 0 ? _a : "/";
|
|
298
|
+
method = ((_b = req.method) !== null && _b !== void 0 ? _b : "GET").toUpperCase();
|
|
299
|
+
remote = (_d = (_c = req.socket) === null || _c === void 0 ? void 0 : _c.remoteAddress) !== null && _d !== void 0 ? _d : "unknown";
|
|
300
|
+
ua = String((_e = req.headers["user-agent"]) !== null && _e !== void 0 ? _e : "");
|
|
301
|
+
cl = String((_f = req.headers["content-length"]) !== null && _f !== void 0 ? _f : "");
|
|
302
|
+
query = parseQuery(url);
|
|
303
|
+
hasTimestamp = Boolean(query.timestamp);
|
|
304
|
+
hasNonce = Boolean(query.nonce);
|
|
305
|
+
hasEchostr = Boolean(query.echostr);
|
|
306
|
+
signature = resolveSignatureParam(query);
|
|
307
|
+
hasSig = Boolean(signature);
|
|
308
|
+
console.log("[wecom] inbound(http): reqId=" + reqId + " path=" + url.split("?")[0] + " method=" + method + " remote=" + remote + " ua=" + (ua ? "\"" + ua + "\"" : "N/A") + " contentLength=" + (cl || "N/A") + " query={timestamp:" + hasTimestamp + ",nonce:" + hasNonce + ",echostr:" + hasEchostr + ",signature:" + hasSig + "}");
|
|
309
|
+
if (!target_js_2.hasActiveTargets()) {
|
|
310
|
+
console.log("[wecom] inbound(http): reqId=" + reqId + " skipped \u2014 no active targets");
|
|
311
|
+
return [2 /*return*/, false]; // 无注册 Target,不处理
|
|
312
|
+
}
|
|
313
|
+
pathAccountId = target_js_1.parseWebhookPath(url);
|
|
314
|
+
// ── GET 请求:URL 验证 ──────────────────────────────────────────
|
|
315
|
+
if (method === "GET") {
|
|
316
|
+
timestamp = query.timestamp, nonce = query.nonce, echostr = query.echostr;
|
|
317
|
+
msgSignature = resolveSignatureParam(query);
|
|
318
|
+
if (!msgSignature || !timestamp || !nonce || !echostr) {
|
|
319
|
+
sendText(res, 400, "missing required query parameters");
|
|
320
|
+
return [2 /*return*/, true];
|
|
321
|
+
}
|
|
322
|
+
matchResult = findMatchingTarget(url, msgSignature, timestamp, nonce, echostr, pathAccountId);
|
|
323
|
+
if (matchResult.status !== "matched") {
|
|
324
|
+
console.log("[wecom] inbound(http): reqId=" + reqId + " GET route_failure reason=" + matchResult.status + " candidates=[" + matchResult.candidateAccountIds.join(",") + "]");
|
|
325
|
+
sendText(res, 403, "signature verification failed");
|
|
326
|
+
return [2 /*return*/, true];
|
|
327
|
+
}
|
|
328
|
+
target = matchResult.target;
|
|
329
|
+
(_h = (_g = target.runtime).log) === null || _h === void 0 ? void 0 : _h.call(_g, "[webhook] GET URL \u9A8C\u8BC1\u6210\u529F (account=" + target.account.accountId + ")");
|
|
330
|
+
try {
|
|
331
|
+
wc = new aibot_node_sdk_1.WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
|
|
332
|
+
plaintext = wc.decrypt(echostr);
|
|
333
|
+
sendText(res, 200, plaintext);
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
(_k = (_j = target.runtime).log) === null || _k === void 0 ? void 0 : _k.call(_j, "[webhook] echostr \u89E3\u5BC6\u5931\u8D25: " + (err instanceof Error ? err.message : String(err)));
|
|
337
|
+
sendText(res, 403, "decryption failed");
|
|
338
|
+
}
|
|
339
|
+
return [2 /*return*/, true];
|
|
340
|
+
}
|
|
341
|
+
if (!(method === "POST")) return [3 /*break*/, 6];
|
|
342
|
+
timestamp = query.timestamp, nonce = query.nonce;
|
|
343
|
+
msgSignature = resolveSignatureParam(query);
|
|
344
|
+
if (!msgSignature || !timestamp || !nonce) {
|
|
345
|
+
sendJson(res, 400, { error: "missing required query parameters" });
|
|
346
|
+
return [2 /*return*/, true];
|
|
347
|
+
}
|
|
348
|
+
return [4 /*yield*/, readBody(req)];
|
|
349
|
+
case 1:
|
|
350
|
+
bodyResult = _1.sent();
|
|
351
|
+
if (!bodyResult.ok) {
|
|
352
|
+
sendJson(res, 400, { error: bodyResult.error });
|
|
353
|
+
return [2 /*return*/, true];
|
|
354
|
+
}
|
|
355
|
+
bodyStr = bodyResult.value;
|
|
356
|
+
body = void 0;
|
|
357
|
+
try {
|
|
358
|
+
body = JSON.parse(bodyStr);
|
|
359
|
+
}
|
|
360
|
+
catch (_2) {
|
|
361
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
362
|
+
return [2 /*return*/, true];
|
|
363
|
+
}
|
|
364
|
+
encrypt = String((_m = (_l = body.encrypt) !== null && _l !== void 0 ? _l : body.Encrypt) !== null && _m !== void 0 ? _m : "");
|
|
365
|
+
// POST body 诊断日志(不输出 encrypt 内容)
|
|
366
|
+
console.log("[wecom] inbound(bot): reqId=" + reqId + " rawJsonBytes=" + Buffer.byteLength(bodyStr, "utf8") + " hasEncrypt=" + Boolean(encrypt) + " encryptLen=" + encrypt.length);
|
|
367
|
+
if (!encrypt) {
|
|
368
|
+
sendJson(res, 400, { error: "missing encrypt field" });
|
|
369
|
+
return [2 /*return*/, true];
|
|
370
|
+
}
|
|
371
|
+
matchResult = findMatchingTarget(url, msgSignature, timestamp, nonce, encrypt, pathAccountId);
|
|
372
|
+
if (matchResult.status !== "matched") {
|
|
373
|
+
reason = matchResult.status === "conflict"
|
|
374
|
+
? "wecom_account_conflict"
|
|
375
|
+
: "wecom_account_not_found";
|
|
376
|
+
detail = matchResult.status === "conflict"
|
|
377
|
+
? "Bot callback account conflict: multiple accounts matched signature."
|
|
378
|
+
: "Bot callback account not found: signature verification failed.";
|
|
379
|
+
console.log("[wecom] inbound(bot): reqId=" + reqId + " route_failure reason=" + reason + " path=" + url.split("?")[0] + " candidates=[" + matchResult.candidateAccountIds.join(",") + "]");
|
|
380
|
+
sendText(res, 403, detail);
|
|
381
|
+
return [2 /*return*/, true];
|
|
382
|
+
}
|
|
383
|
+
target = matchResult.target;
|
|
384
|
+
(_p = (_o = target.runtime).log) === null || _p === void 0 ? void 0 : _p.call(_o, "[webhook] POST \u7B7E\u540D\u9A8C\u8BC1\u6210\u529F (account=" + target.account.accountId + ")");
|
|
385
|
+
// 更新状态:最后接收消息时间
|
|
386
|
+
(_q = target.statusSink) === null || _q === void 0 ? void 0 : _q.call(target, { lastInboundAt: Date.now() });
|
|
387
|
+
message = void 0;
|
|
388
|
+
try {
|
|
389
|
+
wc = new aibot_node_sdk_1.WecomCrypto(target.account.token, target.account.encodingAESKey, target.account.receiveId);
|
|
390
|
+
plaintext = wc.decrypt(encrypt);
|
|
391
|
+
message = JSON.parse(plaintext);
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
(_s = (_r = target.runtime).log) === null || _s === void 0 ? void 0 : _s.call(_r, "[webhook] \u6D88\u606F\u89E3\u5BC6\u5931\u8D25: " + (err instanceof Error ? err.message : String(err)));
|
|
395
|
+
// 解密失败返回 400 + 可读错误信息(与原版一致,方便排查 EncodingAESKey 配置错误)
|
|
396
|
+
sendText(res, 400, "decrypt failed - 解密失败,请检查 EncodingAESKey");
|
|
397
|
+
return [2 /*return*/, true];
|
|
398
|
+
}
|
|
399
|
+
expectedBotIds = resolveBotIdentitySet(target);
|
|
400
|
+
if (expectedBotIds.size > 0) {
|
|
401
|
+
inboundAibotId = String((_t = message.aibotid) !== null && _t !== void 0 ? _t : "").trim();
|
|
402
|
+
if (!inboundAibotId || !expectedBotIds.has(inboundAibotId)) {
|
|
403
|
+
(_v = (_u = target.runtime).error) === null || _v === void 0 ? void 0 : _v.call(_u, "[webhook] aibotid_mismatch: accountId=" + target.account.accountId + " expected=" + Array.from(expectedBotIds).join(",") + " actual=" + (inboundAibotId || "N/A"));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
(_x = (_w = target.runtime).log) === null || _x === void 0 ? void 0 : _x.call(_w, "[webhook] \u6536\u5230\u6D88\u606F (type=" + message.msgtype + ", msgid=" + ((_y = message.msgid) !== null && _y !== void 0 ? _y : "N/A") + ", account=" + target.account.accountId + ")");
|
|
407
|
+
proxyUrl = utils_js_1.resolveWecomEgressProxyUrl(target.config);
|
|
408
|
+
_1.label = 2;
|
|
409
|
+
case 2:
|
|
410
|
+
_1.trys.push([2, 4, , 5]);
|
|
411
|
+
return [4 /*yield*/, dispatchMessage(target, message, timestamp, nonce, proxyUrl)];
|
|
412
|
+
case 3:
|
|
413
|
+
responseData = _1.sent();
|
|
414
|
+
if (responseData) {
|
|
415
|
+
encrypted = encryptResponse(target, responseData, timestamp, nonce);
|
|
416
|
+
sendEncryptedReply(res, encrypted);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
encrypted = encryptResponse(target, {}, timestamp, nonce);
|
|
420
|
+
sendEncryptedReply(res, encrypted);
|
|
421
|
+
}
|
|
422
|
+
return [3 /*break*/, 5];
|
|
423
|
+
case 4:
|
|
424
|
+
err_1 = _1.sent();
|
|
425
|
+
(_0 = (_z = target.runtime).error) === null || _0 === void 0 ? void 0 : _0.call(_z, "[webhook] \u6D88\u606F\u5904\u7406\u5F02\u5E38: " + (err_1 instanceof Error ? err_1.message : String(err_1)));
|
|
426
|
+
errorResponse = {
|
|
427
|
+
msgtype: "text",
|
|
428
|
+
text: { content: "服务内部错误:Bot 处理异常,请稍后重试。" }
|
|
429
|
+
};
|
|
430
|
+
encrypted = encryptResponse(target, errorResponse, timestamp, nonce);
|
|
431
|
+
sendEncryptedReply(res, encrypted);
|
|
432
|
+
return [3 /*break*/, 5];
|
|
433
|
+
case 5: return [2 /*return*/, true];
|
|
434
|
+
case 6: return [2 /*return*/, false];
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
exports.handleWecomWebhookRequest = handleWecomWebhookRequest;
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// 消息分发
|
|
442
|
+
// ============================================================================
|
|
443
|
+
/**
|
|
444
|
+
* 根据消息类型分发到对应的处理函数
|
|
445
|
+
*/
|
|
446
|
+
function dispatchMessage(target, message, timestamp, nonce, proxyUrl) {
|
|
447
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
448
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
449
|
+
var msgtype, eventType, filterResult;
|
|
450
|
+
return __generator(this, function (_l) {
|
|
451
|
+
msgtype = message.msgtype;
|
|
452
|
+
// stream_refresh 轮询
|
|
453
|
+
if (msgtype === "stream") {
|
|
454
|
+
return [2 /*return*/, monitor_js_1.handleStreamRefresh(target, message)];
|
|
455
|
+
}
|
|
456
|
+
// 事件处理
|
|
457
|
+
if (msgtype === "event") {
|
|
458
|
+
eventType = String((_b = (_a = message.event) === null || _a === void 0 ? void 0 : _a.eventtype) !== null && _b !== void 0 ? _b : "").toLowerCase();
|
|
459
|
+
if (eventType === "enter_chat") {
|
|
460
|
+
return [2 /*return*/, monitor_js_1.handleEnterChat(target, message)];
|
|
461
|
+
}
|
|
462
|
+
if (eventType === "template_card_event") {
|
|
463
|
+
return [2 /*return*/, monitor_js_1.handleTemplateCardEvent(target, message, timestamp, nonce, proxyUrl)];
|
|
464
|
+
}
|
|
465
|
+
(_d = (_c = target.runtime).log) === null || _d === void 0 ? void 0 : _d.call(_c, "[webhook] \u672A\u5904\u7406\u7684\u4E8B\u4EF6\u7C7B\u578B: " + eventType);
|
|
466
|
+
return [2 /*return*/, null];
|
|
467
|
+
}
|
|
468
|
+
// 普通消息(text / image / file / voice / video / mixed)
|
|
469
|
+
if (["text", "image", "file", "voice", "video", "mixed"].includes(msgtype)) {
|
|
470
|
+
filterResult = shouldProcessBotInboundMessage(message);
|
|
471
|
+
if (!filterResult.shouldProcess) {
|
|
472
|
+
(_f = (_e = target.runtime).log) === null || _f === void 0 ? void 0 : _f.call(_e, "[webhook] \u6D88\u606F\u8FC7\u6EE4: msgtype=" + msgtype + " reason=" + filterResult.reason + " from=" + ((_g = helpers_js_1.resolveWecomSenderUserId(message)) !== null && _g !== void 0 ? _g : "N/A") + " chatType=" + String((_h = message.chattype) !== null && _h !== void 0 ? _h : "N/A"));
|
|
473
|
+
return [2 /*return*/, null];
|
|
474
|
+
}
|
|
475
|
+
return [2 /*return*/, monitor_js_1.handleInboundMessage(target, message, timestamp, nonce, proxyUrl, filterResult)];
|
|
476
|
+
}
|
|
477
|
+
(_k = (_j = target.runtime).log) === null || _k === void 0 ? void 0 : _k.call(_j, "[webhook] \u672A\u77E5\u6D88\u606F\u7C7B\u578B: " + msgtype);
|
|
478
|
+
return [2 /*return*/, null];
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook 辅助函数
|
|
3
|
+
*
|
|
4
|
+
* 从 @mocrane/wecom monitor.ts 迁移的辅助工具函数集合。
|
|
5
|
+
* 包含:文本截断、兜底提示构建、本机路径提取、MIME 推断等。
|
|
6
|
+
*/
|
|
7
|
+
/// <reference types="node" />
|
|
8
|
+
import type { OpenClawConfig } from "ylib-openclaw/plugin-sdk";
|
|
9
|
+
import type { StreamState, WecomWebhookTarget, WebhookInboundMessage, WebhookInboundQuote } from "./types.js";
|
|
10
|
+
/** DM 文本最大字节数上限 */
|
|
11
|
+
export declare const STREAM_MAX_DM_BYTES = 200000;
|
|
12
|
+
/** MIME 扩展名映射表 */
|
|
13
|
+
export declare const MIME_BY_EXT: Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* UTF-8 字节截断(保留尾部,截断头部)
|
|
16
|
+
*
|
|
17
|
+
* 对齐原版 truncateUtf8Bytes:保留最后 maxBytes 字节。
|
|
18
|
+
*/
|
|
19
|
+
export declare function truncateUtf8Bytes(text: string, maxBytes: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* 追加 DM 兜底内容(对齐原版 appendDmContent)
|
|
22
|
+
*
|
|
23
|
+
* 每次 deliver 时都追加到 dmContent(不受 STREAM_MAX_BYTES 限制,有 DM 上限保护)
|
|
24
|
+
*/
|
|
25
|
+
export declare function appendDmContent(state: StreamState, text: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* 构建兜底提示文本(对齐原版 buildFallbackPrompt)
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildFallbackPrompt(params: {
|
|
30
|
+
kind: "media" | "timeout" | "error";
|
|
31
|
+
agentConfigured: boolean;
|
|
32
|
+
userId?: string;
|
|
33
|
+
filename?: string;
|
|
34
|
+
chatType?: "group" | "direct";
|
|
35
|
+
}): string;
|
|
36
|
+
/**
|
|
37
|
+
* 从文本中提取本机文件路径(对齐原版 extractLocalFilePathsFromText)
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractLocalFilePathsFromText(text: string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* 从文本中提取本机图片路径(对齐原版 extractLocalImagePathsFromText)
|
|
42
|
+
*
|
|
43
|
+
* 仅提取 text 中存在且也出现在 mustAlsoAppearIn 中的路径(安全:防止泄漏)
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractLocalImagePathsFromText(params: {
|
|
46
|
+
text: string;
|
|
47
|
+
mustAlsoAppearIn: string;
|
|
48
|
+
}): string[];
|
|
49
|
+
/**
|
|
50
|
+
* 判断文本是否包含"发送本机文件"的意图(对齐原版 looksLikeSendLocalFileIntent)
|
|
51
|
+
*/
|
|
52
|
+
export declare function looksLikeSendLocalFileIntent(rawBody: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* 计算 taskKey(对齐原版 computeTaskKey)
|
|
55
|
+
*/
|
|
56
|
+
export declare function computeTaskKey(target: WecomWebhookTarget, msg: WebhookInboundMessage): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* 检查 Agent 凭证是否已配置(对齐原版 resolveAgentAccountOrUndefined 的简化版)
|
|
59
|
+
*
|
|
60
|
+
* 在 webhook 模式下,Agent 凭证直接来自 target.account,不需要复杂的解析
|
|
61
|
+
*/
|
|
62
|
+
export declare function isAgentConfigured(target: WecomWebhookTarget): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* 从路径猜测 content-type
|
|
65
|
+
*/
|
|
66
|
+
export declare function guessContentTypeFromPath(filePath: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* 从 StreamState 构建最终流式回复(对齐原版 buildStreamReplyFromState)
|
|
69
|
+
*
|
|
70
|
+
* 包含 images/msg_item,对 content 做 truncateUtf8Bytes。
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildStreamReplyFromState(state: StreamState, maxBytes: number): Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* 计算 MD5
|
|
75
|
+
*/
|
|
76
|
+
export declare function computeMd5(data: Buffer | string): string;
|
|
77
|
+
/**
|
|
78
|
+
* 解析媒体最大字节数(对齐原版 resolveWecomMediaMaxBytes)
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveWecomMediaMaxBytes(cfg: OpenClawConfig): number;
|
|
81
|
+
/** 入站消息解析结果(对齐原版 InboundResult) */
|
|
82
|
+
export declare type InboundResult = {
|
|
83
|
+
body: string;
|
|
84
|
+
media?: {
|
|
85
|
+
buffer: Buffer;
|
|
86
|
+
contentType: string;
|
|
87
|
+
filename: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* 处理接收消息(对齐原版 processInboundMessage)
|
|
92
|
+
*
|
|
93
|
+
* 解析企业微信传入的消息体:
|
|
94
|
+
* 1. 识别媒体消息(Image/File/Video/Mixed)
|
|
95
|
+
* 2. 如果存在媒体文件,调用 media.ts 进行解密和下载
|
|
96
|
+
* 3. 通过 inferInboundMediaMeta 精确推断 MIME 和文件名
|
|
97
|
+
* 4. 构造统一的 InboundResult 供后续 Agent 处理
|
|
98
|
+
*
|
|
99
|
+
* @param target Webhook 目标配置
|
|
100
|
+
* @param msg 企业微信原始消息对象
|
|
101
|
+
*/
|
|
102
|
+
export declare function processInboundMessage(target: WecomWebhookTarget, msg: WebhookInboundMessage): Promise<InboundResult>;
|
|
103
|
+
/**
|
|
104
|
+
* 构建 Agent 调度所需的 config(对齐原版 cfgForDispatch 逻辑)
|
|
105
|
+
*
|
|
106
|
+
* 关键修改:
|
|
107
|
+
* - tools.deny += "message"(防止 Agent 绕过 Bot 交付)
|
|
108
|
+
* - blockStreamingChunk / blockStreamingCoalesce 使用更小的阈值
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildCfgForDispatch(config: OpenClawConfig): OpenClawConfig;
|
|
111
|
+
/**
|
|
112
|
+
* 解析企微 Bot 回调中的发送者 userid(对齐原版 resolveWecomSenderUserId)
|
|
113
|
+
*
|
|
114
|
+
* 优先级:from.userid → fromuserid → from_userid → fromUserId
|
|
115
|
+
*/
|
|
116
|
+
export declare function resolveWecomSenderUserId(msg: WebhookInboundMessage): string | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* 构造入站消息文本内容(对齐原版 buildInboundBody)
|
|
119
|
+
*
|
|
120
|
+
* 根据消息类型提取文本表示:
|
|
121
|
+
* - text → text.content
|
|
122
|
+
* - voice → voice.content 或 "[voice]"
|
|
123
|
+
* - image → "[image] {url}"
|
|
124
|
+
* - file → "[file] {url}"
|
|
125
|
+
* - video → "[video] {url}"
|
|
126
|
+
* - mixed → 逐项提取拼接
|
|
127
|
+
* - event → "[event] {eventtype}"
|
|
128
|
+
* - stream → "[stream_refresh] {id}"
|
|
129
|
+
*
|
|
130
|
+
* 如果消息包含 quote(引用),追加引用内容。
|
|
131
|
+
*/
|
|
132
|
+
export declare function buildInboundBody(msg: WebhookInboundMessage): string;
|
|
133
|
+
/**
|
|
134
|
+
* 格式化引用消息文本(对齐原版 formatQuote)
|
|
135
|
+
*/
|
|
136
|
+
export declare function formatQuote(quote: WebhookInboundQuote): string;
|
|
137
|
+
/** 检查消息是否有媒体内容 */
|
|
138
|
+
export declare function hasMedia(message: WebhookInboundMessage): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* 构造占位符响应(对齐原版 buildStreamPlaceholderReply)
|
|
141
|
+
*
|
|
142
|
+
* 用于 active_new / queued_new 场景:finish=false,显示占位符文本。
|
|
143
|
+
* 原版规范:第一次回复内容为 "1" 作为最小占位符。
|
|
144
|
+
*/
|
|
145
|
+
export declare function buildStreamPlaceholderReply(streamId: string, placeholderContent?: string): Record<string, unknown>;
|
|
146
|
+
/**
|
|
147
|
+
* 构造文本占位符响应(对齐原版 buildStreamTextPlaceholderReply)
|
|
148
|
+
*
|
|
149
|
+
* 用于 merged 场景:finish=false,显示自定义提示(如"已合并排队处理中...")。
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildStreamTextPlaceholderReply(streamId: string, content: string): Record<string, unknown>;
|
|
152
|
+
/**
|
|
153
|
+
* 构造流式响应(从 StreamState 构建)
|
|
154
|
+
*
|
|
155
|
+
* 用于 stream_refresh 和 msgid 去重场景:返回当前累积内容 + finish 标记。
|
|
156
|
+
*/
|
|
157
|
+
export declare function buildStreamResponse(stream: StreamState): Record<string, unknown>;
|