@jeik/dingtalk-connector 0.8.33 → 0.8.35
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 +12 -0
- package/dist/{channel-CABtqkf_.mjs → channel-B56QFS9m.mjs} +1 -1
- package/dist/{dws-delivery-context-ESC7sij7.mjs → dws-delivery-context-ZJalcxYA.mjs} +124 -18
- package/dist/index.mjs +1 -1
- package/dist/{message-handler-BoVSEDwY.mjs → message-handler-0LEoMD85.mjs} +9 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/reply-dispatcher.ts +35 -24
- package/src/services/dws-delivery-context.ts +208 -28
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.35] - 2026-07-31
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- **outbound-runtime 降级顺序明确为 openclaw → jeikclaw** — 包名 import、createRequire、全局 node_modules 均优先 `openclaw`,失败再试 `jeikclaw`;gateway 入口旁 filesystem 仅作最后兜底。
|
|
13
|
+
|
|
14
|
+
## [0.8.34] - 2026-07-31
|
|
15
|
+
|
|
16
|
+
### Fixes
|
|
17
|
+
|
|
18
|
+
- **dwsDeliveryContext 在 jeikclaw 上无法注入** — 插件原先只 `import("openclaw/plugin-sdk/outbound-runtime")`,而生产安装包名为 `jeikclaw`,导致模块解析失败、目标会话写不进 outbound_message。现按顺序尝试 `openclaw` / `jeikclaw` / 从 gateway 入口 resolve 包路径;并改为始终 `console` 输出注入结果,便于无 debug 时排障。解析命令时折叠 shell `\` 续行。
|
|
19
|
+
|
|
8
20
|
## [0.8.33] - 2026-07-31
|
|
9
21
|
|
|
10
22
|
### Features
|
|
@@ -970,7 +970,7 @@ async function monitorDingtalkProvider(opts = {}) {
|
|
|
970
970
|
const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
|
|
971
971
|
const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
|
|
972
972
|
import("./accounts-BQptOmgB.mjs"),
|
|
973
|
-
import("./message-handler-
|
|
973
|
+
import("./message-handler-0LEoMD85.mjs"),
|
|
974
974
|
import("./connection-Bh3U9I4x.mjs")
|
|
975
975
|
]);
|
|
976
976
|
const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as CHANNEL_ID } from "./channel-
|
|
1
|
+
import { t as CHANNEL_ID } from "./channel-B56QFS9m.mjs";
|
|
2
2
|
//#region src/services/dws-delivery-context.ts
|
|
3
3
|
/**
|
|
4
4
|
* DWS 发消息成功后注入 outbound_message 上下文(对齐 OpenClaw message 工具 OC-4)。
|
|
@@ -27,8 +27,12 @@ function splitCsv(raw) {
|
|
|
27
27
|
* 判断是否为 dws 发消息类命令,并解析关键参数。
|
|
28
28
|
* 解析失败返回 null(调用方 no-op,不影响现有体验)。
|
|
29
29
|
*/
|
|
30
|
+
/** 折叠 shell 续行 `\` + 换行,便于从 tool title/output 里解析 */
|
|
31
|
+
function normalizeShellCommandText(raw) {
|
|
32
|
+
return String(raw || "").replace(/\\\r?\n/g, " ").replace(/\r?\n/g, " ").replace(/[ \t]+/g, " ").trim();
|
|
33
|
+
}
|
|
30
34
|
function parseDwsSendCommand(commandText) {
|
|
31
|
-
const cmd =
|
|
35
|
+
const cmd = normalizeShellCommandText(commandText);
|
|
32
36
|
if (!cmd) return null;
|
|
33
37
|
const m = cmd.match(SEND_COMMAND_RE);
|
|
34
38
|
if (!m) return null;
|
|
@@ -85,6 +89,13 @@ function resolveDwsDeliveryContextMode(config) {
|
|
|
85
89
|
return "off";
|
|
86
90
|
}
|
|
87
91
|
let warnedMissingApi = false;
|
|
92
|
+
/** 始终打到 console,不依赖 debug 开关(否则生产无法排障) */
|
|
93
|
+
function alwaysLog(level, msg) {
|
|
94
|
+
const line = `[DingTalk][dwsDeliveryContext] ${msg}`;
|
|
95
|
+
if (level === "error") console.error(line);
|
|
96
|
+
else if (level === "warn") console.warn(line);
|
|
97
|
+
else console.log(line);
|
|
98
|
+
}
|
|
88
99
|
function resolveModeFromConfigs(cfg, accountConfig) {
|
|
89
100
|
if (accountConfig?.dwsDeliveryContext !== void 0) return resolveDwsDeliveryContextMode(accountConfig);
|
|
90
101
|
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
@@ -92,6 +103,103 @@ function resolveModeFromConfigs(cfg, accountConfig) {
|
|
|
92
103
|
return "target";
|
|
93
104
|
}
|
|
94
105
|
/**
|
|
106
|
+
* 尝试 import 一个候选并校验 append 导出。
|
|
107
|
+
* 成功返回 API;失败返回 null(由调用方继续降级)。
|
|
108
|
+
*/
|
|
109
|
+
async function tryImportOutboundRuntime(id, errors) {
|
|
110
|
+
try {
|
|
111
|
+
const oc = await import(id);
|
|
112
|
+
if (typeof oc.appendOutboundMessageDeliveryContext !== "function") {
|
|
113
|
+
errors.push(`${id}: no appendOutboundMessageDeliveryContext`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
append: oc.appendOutboundMessageDeliveryContext.bind(oc),
|
|
118
|
+
resolveRoute: typeof oc.resolveOutboundSessionRoute === "function" ? oc.resolveOutboundSessionRoute.bind(oc) : void 0,
|
|
119
|
+
ensureEntry: typeof oc.ensureOutboundSessionEntry === "function" ? oc.ensureOutboundSessionEntry.bind(oc) : void 0,
|
|
120
|
+
via: id
|
|
121
|
+
};
|
|
122
|
+
} catch (e) {
|
|
123
|
+
errors.push(`${id}: ${e?.message || e}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 加载 OC-4 append API。
|
|
129
|
+
*
|
|
130
|
+
* 降级策略(严格优先 openclaw,导不进去再 jeikclaw):
|
|
131
|
+
* 1) 包名 import:openclaw/* → jeikclaw/*
|
|
132
|
+
* 2) createRequire 从 gateway 入口 resolve 包:openclaw → jeikclaw
|
|
133
|
+
* 3) 文件系统:gateway argv[1] 旁 dist/plugin-sdk(不区分包名,作最后兜底)
|
|
134
|
+
* 4) 全局 node_modules:openclaw → jeikclaw
|
|
135
|
+
*
|
|
136
|
+
* 插件装在 ~/.openclaw/npm/projects/... 时,裸 import 包名常失败,
|
|
137
|
+
* 因此 2/4 会用 gateway 入口 / 全局路径再试一遍。
|
|
138
|
+
*/
|
|
139
|
+
async function loadOutboundRuntime() {
|
|
140
|
+
const { pathToFileURL } = await import("node:url");
|
|
141
|
+
const path = await import("node:path");
|
|
142
|
+
const fs = await import("node:fs");
|
|
143
|
+
const { createRequire } = await import("node:module");
|
|
144
|
+
const errors = [];
|
|
145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
146
|
+
const tryOne = async (id) => {
|
|
147
|
+
if (!id || seen.has(id)) return null;
|
|
148
|
+
seen.add(id);
|
|
149
|
+
return tryImportOutboundRuntime(id, errors);
|
|
150
|
+
};
|
|
151
|
+
for (const pkg of ["openclaw", "jeikclaw"]) for (const sub of [`${pkg}/plugin-sdk/outbound-runtime`, `${pkg}/dist/plugin-sdk/outbound-runtime.js`]) {
|
|
152
|
+
const hit = await tryOne(sub);
|
|
153
|
+
if (hit) return hit;
|
|
154
|
+
}
|
|
155
|
+
const entry = typeof process.argv[1] === "string" ? process.argv[1] : "";
|
|
156
|
+
if (entry) try {
|
|
157
|
+
const req = createRequire(pathToFileURL(path.resolve(entry)).href);
|
|
158
|
+
for (const pkg of ["openclaw", "jeikclaw"]) try {
|
|
159
|
+
const pkgJson = req.resolve(`${pkg}/package.json`);
|
|
160
|
+
const root = path.dirname(pkgJson);
|
|
161
|
+
const file = path.join(root, "dist", "plugin-sdk", "outbound-runtime.js");
|
|
162
|
+
if (fs.existsSync(file)) {
|
|
163
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
164
|
+
if (hit) return hit;
|
|
165
|
+
}
|
|
166
|
+
} catch {}
|
|
167
|
+
} catch {}
|
|
168
|
+
if (entry) {
|
|
169
|
+
const entryDir = path.dirname(path.resolve(entry));
|
|
170
|
+
const fileCandidates = [path.join(entryDir, "plugin-sdk", "outbound-runtime.js"), path.join(entryDir, "dist", "plugin-sdk", "outbound-runtime.js")];
|
|
171
|
+
let dir = entryDir;
|
|
172
|
+
for (let i = 0; i < 5; i++) {
|
|
173
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
174
|
+
fileCandidates.push(path.join(dir, "dist", "plugin-sdk", "outbound-runtime.js"), path.join(dir, "plugin-sdk", "outbound-runtime.js"));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
const parent = path.dirname(dir);
|
|
178
|
+
if (parent === dir) break;
|
|
179
|
+
dir = parent;
|
|
180
|
+
}
|
|
181
|
+
for (const file of fileCandidates) if (fs.existsSync(file)) {
|
|
182
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
183
|
+
if (hit) return hit;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const execDir = path.dirname(process.execPath);
|
|
188
|
+
for (const pkg of ["openclaw", "jeikclaw"]) {
|
|
189
|
+
const file = path.join(execDir, "..", "lib", "node_modules", pkg, "dist", "plugin-sdk", "outbound-runtime.js");
|
|
190
|
+
if (fs.existsSync(file)) {
|
|
191
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
192
|
+
if (hit) return hit;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
if (!warnedMissingApi) {
|
|
197
|
+
warnedMissingApi = true;
|
|
198
|
+
alwaysLog("warn", `无法加载 outbound-runtime(openclaw→jeikclaw 均失败)。argv1=${entry || "-"} tried=${seen.size} err=${errors.slice(0, 4).join(" | ")}`);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
95
203
|
* 在 dws 发消息命令成功结束后,向目标/来源会话写入 outbound_message。
|
|
96
204
|
* 全程 best-effort,失败只打日志。
|
|
97
205
|
*/
|
|
@@ -104,25 +212,16 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
104
212
|
const parsed = parseDwsSendCommand(params.commandText);
|
|
105
213
|
if (!parsed) return false;
|
|
106
214
|
if (!parsed.target && effectiveMode === "target") {
|
|
107
|
-
|
|
215
|
+
alwaysLog("warn", `解析到 dws ${parsed.kind} 但无 --group/--user 目标,跳过 target 注入。cmd=${parsed.command.slice(0, 160)}`);
|
|
108
216
|
return false;
|
|
109
217
|
}
|
|
110
218
|
try {
|
|
111
|
-
const oc = await
|
|
112
|
-
|
|
113
|
-
const resolveRoute = oc.resolveOutboundSessionRoute;
|
|
114
|
-
const ensureEntry = oc.ensureOutboundSessionEntry;
|
|
115
|
-
if (typeof append !== "function") {
|
|
116
|
-
if (!warnedMissingApi) {
|
|
117
|
-
warnedMissingApi = true;
|
|
118
|
-
log?.warn?.("[DingTalk][dwsDeliveryContext] 当前 OpenClaw 未导出 appendOutboundMessageDeliveryContext;请升级/重建 openclaw(plugin-sdk/outbound-runtime)。本功能已跳过,不影响发消息。");
|
|
119
|
-
}
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
219
|
+
const oc = await loadOutboundRuntime();
|
|
220
|
+
if (!oc?.append) return false;
|
|
122
221
|
let targetRoute = null;
|
|
123
222
|
const targetTo = parsed.target;
|
|
124
|
-
if (targetTo && typeof resolveRoute === "function") try {
|
|
125
|
-
targetRoute = await resolveRoute({
|
|
223
|
+
if (targetTo && typeof oc.resolveRoute === "function") try {
|
|
224
|
+
targetRoute = await oc.resolveRoute({
|
|
126
225
|
cfg: params.cfg,
|
|
127
226
|
channel: CHANNEL_ID,
|
|
128
227
|
agentId: params.agentId,
|
|
@@ -130,15 +229,20 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
130
229
|
target: targetTo,
|
|
131
230
|
currentSessionKey: params.sourceSessionKey
|
|
132
231
|
});
|
|
133
|
-
if (targetRoute && typeof ensureEntry === "function") await ensureEntry({
|
|
232
|
+
if (targetRoute && typeof oc.ensureEntry === "function") await oc.ensureEntry({
|
|
134
233
|
cfg: params.cfg,
|
|
135
234
|
channel: CHANNEL_ID,
|
|
136
235
|
accountId: params.accountId,
|
|
137
236
|
route: targetRoute
|
|
138
237
|
});
|
|
139
238
|
} catch (err) {
|
|
239
|
+
alwaysLog("warn", `resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
140
240
|
log?.warn?.(`[DingTalk][dwsDeliveryContext] resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
141
241
|
}
|
|
242
|
+
if (effectiveMode === "target" && !targetRoute?.sessionKey) {
|
|
243
|
+
alwaysLog("warn", `target 模式但未解析到 sessionKey,无法写入目标会话。targetTo=${targetTo} via=${oc.via}`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
142
246
|
const action = parsed.kind === "bot-send" ? "dws.chat.message.send-by-bot" : parsed.kind === "webhook-send" ? "dws.chat.message.send-by-webhook" : parsed.kind === "reply" ? "dws.chat.message.reply" : "dws.chat.message.send";
|
|
143
247
|
const actionParams = {
|
|
144
248
|
action: "send",
|
|
@@ -158,7 +262,7 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
158
262
|
command: parsed.command
|
|
159
263
|
};
|
|
160
264
|
const idempotencyKey = params.toolCallId?.trim() || `dws:${params.sourceSessionKey}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
161
|
-
await append({
|
|
265
|
+
await oc.append({
|
|
162
266
|
cfg: params.cfg,
|
|
163
267
|
mode: effectiveMode,
|
|
164
268
|
agentId: params.agentId,
|
|
@@ -174,9 +278,11 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
174
278
|
action,
|
|
175
279
|
idempotencyKey
|
|
176
280
|
});
|
|
281
|
+
alwaysLog("info", `已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"} via=${oc.via}`);
|
|
177
282
|
log?.info?.(`[DingTalk][dwsDeliveryContext] 已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"}`);
|
|
178
283
|
return true;
|
|
179
284
|
} catch (err) {
|
|
285
|
+
alwaysLog("warn", `注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
180
286
|
log?.warn?.(`[DingTalk][dwsDeliveryContext] 注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
181
287
|
return false;
|
|
182
288
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as dingtalkPlugin, r as initDingtalkPluginConfigSchema } from "./channel-
|
|
1
|
+
import { n as dingtalkPlugin, r as initDingtalkPluginConfigSchema } from "./channel-B56QFS9m.mjs";
|
|
2
2
|
import { n as setDingtalkRuntime } from "./runtime-DDMzayvA.mjs";
|
|
3
3
|
import { t as registerGatewayMethods } from "./gateway-methods-Ce1FXmCx.mjs";
|
|
4
4
|
//#region index.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
|
|
2
|
-
import { t as CHANNEL_ID } from "./channel-
|
|
2
|
+
import { t as CHANNEL_ID } from "./channel-B56QFS9m.mjs";
|
|
3
3
|
import { n as createLoggerFromConfig, r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
|
|
4
4
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
5
5
|
import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
|
|
@@ -1029,13 +1029,14 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
1029
1029
|
log.info(`[DingTalk][onCommandOutput] 检测到 dws 产品: ${product},phase=${payload.phase}, exitCode=${payload.exitCode}`);
|
|
1030
1030
|
} else log.info(`[DingTalk][onCommandOutput] dws 命令执行失败,跳过: ${product},exitCode=${payload.exitCode}`);
|
|
1031
1031
|
}
|
|
1032
|
-
if (payload.phase === "end" && sourceSessionKey) {
|
|
1032
|
+
if ((payload.phase || (payload.exitCode !== void 0 ? "end" : void 0)) === "end" && sourceSessionKey) {
|
|
1033
1033
|
const fullCmd = [
|
|
1034
1034
|
payload.title,
|
|
1035
1035
|
payload.name,
|
|
1036
|
-
payload.output
|
|
1037
|
-
|
|
1038
|
-
|
|
1036
|
+
payload.output,
|
|
1037
|
+
commandText
|
|
1038
|
+
].filter(Boolean).join("\n") || "";
|
|
1039
|
+
if (/\bdws\b/i.test(fullCmd)) import("./dws-delivery-context-ZJalcxYA.mjs").then(({ maybeInjectDwsOutboundContext }) => maybeInjectDwsOutboundContext({
|
|
1039
1040
|
cfg,
|
|
1040
1041
|
accountConfig: account.config,
|
|
1041
1042
|
agentId,
|
|
@@ -1044,11 +1045,12 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
1044
1045
|
invokerId: senderId,
|
|
1045
1046
|
invokerName: senderName,
|
|
1046
1047
|
commandText: fullCmd,
|
|
1047
|
-
exitCode: payload.exitCode,
|
|
1048
|
-
phase:
|
|
1048
|
+
exitCode: payload.exitCode ?? 0,
|
|
1049
|
+
phase: "end",
|
|
1049
1050
|
toolCallId: payload.toolCallId,
|
|
1050
1051
|
log
|
|
1051
1052
|
})).catch((err) => {
|
|
1053
|
+
console.warn(`[DingTalk][dwsDeliveryContext] onCommandOutput 钩子异常: ${err?.message || err}`);
|
|
1052
1054
|
log.warn?.(`[DingTalk][dwsDeliveryContext] onCommandOutput 钩子异常: ${err?.message || err}`);
|
|
1053
1055
|
});
|
|
1054
1056
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/reply-dispatcher.ts
CHANGED
|
@@ -1704,31 +1704,42 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
1706
|
// dws chat message send* 成功后注入 outbound_message(best-effort,不阻塞)
|
|
1707
|
-
|
|
1707
|
+
// buildCommandOutputFromToolResultEvent 会把 tool result 标成 phase=end
|
|
1708
|
+
const phase = payload.phase || (payload.exitCode !== undefined ? 'end' : undefined);
|
|
1709
|
+
if (phase === 'end' && sourceSessionKey) {
|
|
1710
|
+
// title 常为命令;output 为 stdout。两者都扫,兼容多行 `\` 续行
|
|
1708
1711
|
const fullCmd =
|
|
1709
|
-
[payload.title, payload.name, payload.output
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
)
|
|
1731
|
-
|
|
1712
|
+
[payload.title, payload.name, payload.output, commandText]
|
|
1713
|
+
.filter(Boolean)
|
|
1714
|
+
.join('\n') || '';
|
|
1715
|
+
// 快速过滤:不含 dws 则跳过,避免无意义动态 import
|
|
1716
|
+
if (/\bdws\b/i.test(fullCmd)) {
|
|
1717
|
+
void import('./services/dws-delivery-context.ts')
|
|
1718
|
+
.then(({ maybeInjectDwsOutboundContext }) =>
|
|
1719
|
+
maybeInjectDwsOutboundContext({
|
|
1720
|
+
cfg,
|
|
1721
|
+
accountConfig: account.config,
|
|
1722
|
+
agentId,
|
|
1723
|
+
accountId,
|
|
1724
|
+
sourceSessionKey,
|
|
1725
|
+
invokerId: senderId,
|
|
1726
|
+
invokerName: senderName,
|
|
1727
|
+
commandText: fullCmd,
|
|
1728
|
+
exitCode: payload.exitCode ?? 0,
|
|
1729
|
+
phase: 'end',
|
|
1730
|
+
toolCallId: payload.toolCallId,
|
|
1731
|
+
log,
|
|
1732
|
+
}),
|
|
1733
|
+
)
|
|
1734
|
+
.catch((err: any) => {
|
|
1735
|
+
console.warn(
|
|
1736
|
+
`[DingTalk][dwsDeliveryContext] onCommandOutput 钩子异常: ${err?.message || err}`,
|
|
1737
|
+
);
|
|
1738
|
+
log.warn?.(
|
|
1739
|
+
`[DingTalk][dwsDeliveryContext] onCommandOutput 钩子异常: ${err?.message || err}`,
|
|
1740
|
+
);
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1732
1743
|
}
|
|
1733
1744
|
// 工具进度改由 onToolStart + 正文同一 cardContentVar 展示,不再写独立 cardToolVar 字段
|
|
1734
1745
|
},
|
|
@@ -56,8 +56,17 @@ function splitCsv(raw?: string): string[] | undefined {
|
|
|
56
56
|
* 判断是否为 dws 发消息类命令,并解析关键参数。
|
|
57
57
|
* 解析失败返回 null(调用方 no-op,不影响现有体验)。
|
|
58
58
|
*/
|
|
59
|
+
/** 折叠 shell 续行 `\` + 换行,便于从 tool title/output 里解析 */
|
|
60
|
+
function normalizeShellCommandText(raw: string): string {
|
|
61
|
+
return String(raw || "")
|
|
62
|
+
.replace(/\\\r?\n/g, " ")
|
|
63
|
+
.replace(/\r?\n/g, " ")
|
|
64
|
+
.replace(/[ \t]+/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
export function parseDwsSendCommand(commandText: string): ParsedDwsSendCommand | null {
|
|
60
|
-
const cmd =
|
|
69
|
+
const cmd = normalizeShellCommandText(commandText);
|
|
61
70
|
if (!cmd) return null;
|
|
62
71
|
const m = cmd.match(SEND_COMMAND_RE);
|
|
63
72
|
if (!m) return null;
|
|
@@ -164,6 +173,14 @@ export type MaybeInjectDwsOutboundContextParams = {
|
|
|
164
173
|
|
|
165
174
|
let warnedMissingApi = false;
|
|
166
175
|
|
|
176
|
+
/** 始终打到 console,不依赖 debug 开关(否则生产无法排障) */
|
|
177
|
+
function alwaysLog(level: "info" | "warn" | "error", msg: string): void {
|
|
178
|
+
const line = `[DingTalk][dwsDeliveryContext] ${msg}`;
|
|
179
|
+
if (level === "error") console.error(line);
|
|
180
|
+
else if (level === "warn") console.warn(line);
|
|
181
|
+
else console.log(line);
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
function resolveModeFromConfigs(cfg: any, accountConfig?: any): DwsDeliveryContextMode {
|
|
168
185
|
// 账号级 > 渠道顶层 > 默认 target
|
|
169
186
|
if (accountConfig?.dwsDeliveryContext !== undefined) {
|
|
@@ -176,6 +193,168 @@ function resolveModeFromConfigs(cfg: any, accountConfig?: any): DwsDeliveryConte
|
|
|
176
193
|
return "target";
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
type OutboundRuntimeApi = {
|
|
197
|
+
append: (p: any) => Promise<void>;
|
|
198
|
+
resolveRoute?: (p: any) => Promise<any>;
|
|
199
|
+
ensureEntry?: (p: any) => Promise<void>;
|
|
200
|
+
via: string;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 尝试 import 一个候选并校验 append 导出。
|
|
205
|
+
* 成功返回 API;失败返回 null(由调用方继续降级)。
|
|
206
|
+
*/
|
|
207
|
+
async function tryImportOutboundRuntime(
|
|
208
|
+
id: string,
|
|
209
|
+
errors: string[],
|
|
210
|
+
): Promise<OutboundRuntimeApi | null> {
|
|
211
|
+
try {
|
|
212
|
+
const oc: any = await import(id);
|
|
213
|
+
if (typeof oc.appendOutboundMessageDeliveryContext !== "function") {
|
|
214
|
+
errors.push(`${id}: no appendOutboundMessageDeliveryContext`);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
append: oc.appendOutboundMessageDeliveryContext.bind(oc),
|
|
219
|
+
resolveRoute:
|
|
220
|
+
typeof oc.resolveOutboundSessionRoute === "function"
|
|
221
|
+
? oc.resolveOutboundSessionRoute.bind(oc)
|
|
222
|
+
: undefined,
|
|
223
|
+
ensureEntry:
|
|
224
|
+
typeof oc.ensureOutboundSessionEntry === "function"
|
|
225
|
+
? oc.ensureOutboundSessionEntry.bind(oc)
|
|
226
|
+
: undefined,
|
|
227
|
+
via: id,
|
|
228
|
+
};
|
|
229
|
+
} catch (e: any) {
|
|
230
|
+
errors.push(`${id}: ${e?.message || e}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 加载 OC-4 append API。
|
|
237
|
+
*
|
|
238
|
+
* 降级策略(严格优先 openclaw,导不进去再 jeikclaw):
|
|
239
|
+
* 1) 包名 import:openclaw/* → jeikclaw/*
|
|
240
|
+
* 2) createRequire 从 gateway 入口 resolve 包:openclaw → jeikclaw
|
|
241
|
+
* 3) 文件系统:gateway argv[1] 旁 dist/plugin-sdk(不区分包名,作最后兜底)
|
|
242
|
+
* 4) 全局 node_modules:openclaw → jeikclaw
|
|
243
|
+
*
|
|
244
|
+
* 插件装在 ~/.openclaw/npm/projects/... 时,裸 import 包名常失败,
|
|
245
|
+
* 因此 2/4 会用 gateway 入口 / 全局路径再试一遍。
|
|
246
|
+
*/
|
|
247
|
+
async function loadOutboundRuntime(): Promise<OutboundRuntimeApi | null> {
|
|
248
|
+
const { pathToFileURL } = await import("node:url");
|
|
249
|
+
const path = await import("node:path");
|
|
250
|
+
const fs = await import("node:fs");
|
|
251
|
+
const { createRequire } = await import("node:module");
|
|
252
|
+
|
|
253
|
+
const errors: string[] = [];
|
|
254
|
+
const seen = new Set<string>();
|
|
255
|
+
|
|
256
|
+
const tryOne = async (id: string): Promise<OutboundRuntimeApi | null> => {
|
|
257
|
+
if (!id || seen.has(id)) return null;
|
|
258
|
+
seen.add(id);
|
|
259
|
+
return tryImportOutboundRuntime(id, errors);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ── 1) 包名 import:openclaw 优先,失败再 jeikclaw ──
|
|
263
|
+
for (const pkg of ["openclaw", "jeikclaw"] as const) {
|
|
264
|
+
for (const sub of [
|
|
265
|
+
`${pkg}/plugin-sdk/outbound-runtime`,
|
|
266
|
+
`${pkg}/dist/plugin-sdk/outbound-runtime.js`,
|
|
267
|
+
]) {
|
|
268
|
+
const hit = await tryOne(sub);
|
|
269
|
+
if (hit) return hit;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entry = typeof process.argv[1] === "string" ? process.argv[1] : "";
|
|
274
|
+
|
|
275
|
+
// ── 2) 从 gateway 入口 createRequire:openclaw 优先,再 jeikclaw ──
|
|
276
|
+
if (entry) {
|
|
277
|
+
try {
|
|
278
|
+
const req = createRequire(pathToFileURL(path.resolve(entry)).href);
|
|
279
|
+
for (const pkg of ["openclaw", "jeikclaw"] as const) {
|
|
280
|
+
try {
|
|
281
|
+
const pkgJson = req.resolve(`${pkg}/package.json`);
|
|
282
|
+
const root = path.dirname(pkgJson);
|
|
283
|
+
const file = path.join(root, "dist", "plugin-sdk", "outbound-runtime.js");
|
|
284
|
+
if (fs.existsSync(file)) {
|
|
285
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
286
|
+
if (hit) return hit;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
/* 该包名 resolve 失败,试下一个 */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
/* ignore */
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── 3) 文件系统兜底:gateway 入口旁(运行中的就是这份 dist)──
|
|
298
|
+
if (entry) {
|
|
299
|
+
const entryDir = path.dirname(path.resolve(entry));
|
|
300
|
+
const fileCandidates = [
|
|
301
|
+
path.join(entryDir, "plugin-sdk", "outbound-runtime.js"),
|
|
302
|
+
path.join(entryDir, "dist", "plugin-sdk", "outbound-runtime.js"),
|
|
303
|
+
];
|
|
304
|
+
let dir = entryDir;
|
|
305
|
+
for (let i = 0; i < 5; i++) {
|
|
306
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
307
|
+
fileCandidates.push(
|
|
308
|
+
path.join(dir, "dist", "plugin-sdk", "outbound-runtime.js"),
|
|
309
|
+
path.join(dir, "plugin-sdk", "outbound-runtime.js"),
|
|
310
|
+
);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
const parent = path.dirname(dir);
|
|
314
|
+
if (parent === dir) break;
|
|
315
|
+
dir = parent;
|
|
316
|
+
}
|
|
317
|
+
for (const file of fileCandidates) {
|
|
318
|
+
if (fs.existsSync(file)) {
|
|
319
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
320
|
+
if (hit) return hit;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── 4) 全局 node_modules:openclaw 优先,再 jeikclaw ──
|
|
326
|
+
try {
|
|
327
|
+
const execDir = path.dirname(process.execPath);
|
|
328
|
+
for (const pkg of ["openclaw", "jeikclaw"] as const) {
|
|
329
|
+
const file = path.join(
|
|
330
|
+
execDir,
|
|
331
|
+
"..",
|
|
332
|
+
"lib",
|
|
333
|
+
"node_modules",
|
|
334
|
+
pkg,
|
|
335
|
+
"dist",
|
|
336
|
+
"plugin-sdk",
|
|
337
|
+
"outbound-runtime.js",
|
|
338
|
+
);
|
|
339
|
+
if (fs.existsSync(file)) {
|
|
340
|
+
const hit = await tryOne(pathToFileURL(file).href);
|
|
341
|
+
if (hit) return hit;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
/* ignore */
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!warnedMissingApi) {
|
|
349
|
+
warnedMissingApi = true;
|
|
350
|
+
alwaysLog(
|
|
351
|
+
"warn",
|
|
352
|
+
`无法加载 outbound-runtime(openclaw→jeikclaw 均失败)。argv1=${entry || "-"} tried=${seen.size} err=${errors.slice(0, 4).join(" | ")}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
179
358
|
/**
|
|
180
359
|
* 在 dws 发消息命令成功结束后,向目标/来源会话写入 outbound_message。
|
|
181
360
|
* 全程 best-effort,失败只打日志。
|
|
@@ -195,44 +374,31 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
195
374
|
}
|
|
196
375
|
|
|
197
376
|
const parsed = parseDwsSendCommand(params.commandText);
|
|
198
|
-
if (!parsed)
|
|
377
|
+
if (!parsed) {
|
|
378
|
+
// 不是 dws send 命令:静默(避免刷屏)
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
199
381
|
|
|
200
382
|
// 无明确 target 时,仅 source 模式才写
|
|
201
383
|
if (!parsed.target && effectiveMode === "target") {
|
|
202
|
-
|
|
203
|
-
|
|
384
|
+
alwaysLog(
|
|
385
|
+
"warn",
|
|
386
|
+
`解析到 dws ${parsed.kind} 但无 --group/--user 目标,跳过 target 注入。cmd=${parsed.command.slice(0, 160)}`,
|
|
204
387
|
);
|
|
205
388
|
return false;
|
|
206
389
|
}
|
|
207
390
|
|
|
208
391
|
try {
|
|
209
|
-
const oc = await
|
|
210
|
-
|
|
211
|
-
| ((p: any) => Promise<void>)
|
|
212
|
-
| undefined;
|
|
213
|
-
const resolveRoute = (oc as any).resolveOutboundSessionRoute as
|
|
214
|
-
| ((p: any) => Promise<any>)
|
|
215
|
-
| undefined;
|
|
216
|
-
const ensureEntry = (oc as any).ensureOutboundSessionEntry as
|
|
217
|
-
| ((p: any) => Promise<void>)
|
|
218
|
-
| undefined;
|
|
219
|
-
|
|
220
|
-
if (typeof append !== "function") {
|
|
221
|
-
if (!warnedMissingApi) {
|
|
222
|
-
warnedMissingApi = true;
|
|
223
|
-
log?.warn?.(
|
|
224
|
-
"[DingTalk][dwsDeliveryContext] 当前 OpenClaw 未导出 appendOutboundMessageDeliveryContext;" +
|
|
225
|
-
"请升级/重建 openclaw(plugin-sdk/outbound-runtime)。本功能已跳过,不影响发消息。",
|
|
226
|
-
);
|
|
227
|
-
}
|
|
392
|
+
const oc = await loadOutboundRuntime();
|
|
393
|
+
if (!oc?.append) {
|
|
228
394
|
return false;
|
|
229
395
|
}
|
|
230
396
|
|
|
231
397
|
let targetRoute: any = null;
|
|
232
398
|
const targetTo = parsed.target;
|
|
233
|
-
if (targetTo && typeof resolveRoute === "function") {
|
|
399
|
+
if (targetTo && typeof oc.resolveRoute === "function") {
|
|
234
400
|
try {
|
|
235
|
-
targetRoute = await resolveRoute({
|
|
401
|
+
targetRoute = await oc.resolveRoute({
|
|
236
402
|
cfg: params.cfg,
|
|
237
403
|
channel: CHANNEL_ID,
|
|
238
404
|
agentId: params.agentId,
|
|
@@ -240,8 +406,8 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
240
406
|
target: targetTo,
|
|
241
407
|
currentSessionKey: params.sourceSessionKey,
|
|
242
408
|
});
|
|
243
|
-
if (targetRoute && typeof ensureEntry === "function") {
|
|
244
|
-
await ensureEntry({
|
|
409
|
+
if (targetRoute && typeof oc.ensureEntry === "function") {
|
|
410
|
+
await oc.ensureEntry({
|
|
245
411
|
cfg: params.cfg,
|
|
246
412
|
channel: CHANNEL_ID,
|
|
247
413
|
accountId: params.accountId,
|
|
@@ -249,12 +415,21 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
249
415
|
});
|
|
250
416
|
}
|
|
251
417
|
} catch (err: any) {
|
|
418
|
+
alwaysLog("warn", `resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
252
419
|
log?.warn?.(
|
|
253
420
|
`[DingTalk][dwsDeliveryContext] resolveOutboundSessionRoute 失败: ${err?.message || err}`,
|
|
254
421
|
);
|
|
255
422
|
}
|
|
256
423
|
}
|
|
257
424
|
|
|
425
|
+
if (effectiveMode === "target" && !targetRoute?.sessionKey) {
|
|
426
|
+
alwaysLog(
|
|
427
|
+
"warn",
|
|
428
|
+
`target 模式但未解析到 sessionKey,无法写入目标会话。targetTo=${targetTo} via=${oc.via}`,
|
|
429
|
+
);
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
258
433
|
// target 模式但路由失败 → 仍可写 source(若 both/source)
|
|
259
434
|
const action =
|
|
260
435
|
parsed.kind === "bot-send"
|
|
@@ -287,7 +462,7 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
287
462
|
params.toolCallId?.trim() ||
|
|
288
463
|
`dws:${params.sourceSessionKey}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
289
464
|
|
|
290
|
-
await append({
|
|
465
|
+
await oc.append({
|
|
291
466
|
cfg: params.cfg,
|
|
292
467
|
mode: effectiveMode,
|
|
293
468
|
agentId: params.agentId,
|
|
@@ -304,11 +479,16 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
304
479
|
idempotencyKey,
|
|
305
480
|
});
|
|
306
481
|
|
|
482
|
+
alwaysLog(
|
|
483
|
+
"info",
|
|
484
|
+
`已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"} via=${oc.via}`,
|
|
485
|
+
);
|
|
307
486
|
log?.info?.(
|
|
308
487
|
`[DingTalk][dwsDeliveryContext] 已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"}`,
|
|
309
488
|
);
|
|
310
489
|
return true;
|
|
311
490
|
} catch (err: any) {
|
|
491
|
+
alwaysLog("warn", `注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
312
492
|
log?.warn?.(
|
|
313
493
|
`[DingTalk][dwsDeliveryContext] 注入失败(已忽略,不影响发消息): ${err?.message || err}`,
|
|
314
494
|
);
|