@jeik/dingtalk-connector 0.8.33 → 0.8.34
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 +6 -0
- package/dist/{channel-CABtqkf_.mjs → channel-HfPzJGwO.mjs} +1 -1
- package/dist/{dws-delivery-context-ESC7sij7.mjs → dws-delivery-context-DLR1ghNQ.mjs} +105 -18
- package/dist/index.mjs +1 -1
- package/dist/{message-handler-BoVSEDwY.mjs → message-handler-Dsyk414y.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 +169 -28
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.34] - 2026-07-31
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- **dwsDeliveryContext 在 jeikclaw 上无法注入** — 插件原先只 `import("openclaw/plugin-sdk/outbound-runtime")`,而生产安装包名为 `jeikclaw`,导致模块解析失败、目标会话写不进 outbound_message。现按顺序尝试 `openclaw` / `jeikclaw` / 从 gateway 入口 resolve 包路径;并改为始终 `console` 输出注入结果,便于无 debug 时排障。解析命令时折叠 shell `\` 续行。
|
|
13
|
+
|
|
8
14
|
## [0.8.33] - 2026-07-31
|
|
9
15
|
|
|
10
16
|
### 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-Dsyk414y.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-HfPzJGwO.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,84 @@ function resolveModeFromConfigs(cfg, accountConfig) {
|
|
|
92
103
|
return "target";
|
|
93
104
|
}
|
|
94
105
|
/**
|
|
106
|
+
* 加载 OC-4 append API。
|
|
107
|
+
*
|
|
108
|
+
* 注意:钉钉插件装在 ~/.openclaw/npm/projects/... 下,Node 从插件路径
|
|
109
|
+
* 解析不到名为 openclaw/jeikclaw 的 package(包名还可能是 jeikclaw)。
|
|
110
|
+
* 必须用 gateway 进程入口(process.argv[1])定位 dist/plugin-sdk。
|
|
111
|
+
*/
|
|
112
|
+
async function loadOutboundRuntime() {
|
|
113
|
+
const { pathToFileURL } = await import("node:url");
|
|
114
|
+
const path = await import("node:path");
|
|
115
|
+
const fs = await import("node:fs");
|
|
116
|
+
const { createRequire } = await import("node:module");
|
|
117
|
+
const candidates = [
|
|
118
|
+
"openclaw/plugin-sdk/outbound-runtime",
|
|
119
|
+
"jeikclaw/plugin-sdk/outbound-runtime",
|
|
120
|
+
"jeikclaw/dist/plugin-sdk/outbound-runtime.js",
|
|
121
|
+
"openclaw/dist/plugin-sdk/outbound-runtime.js"
|
|
122
|
+
];
|
|
123
|
+
const pushIfExists = (filePath) => {
|
|
124
|
+
try {
|
|
125
|
+
if (filePath && fs.existsSync(filePath)) candidates.push(pathToFileURL(filePath).href);
|
|
126
|
+
} catch {}
|
|
127
|
+
};
|
|
128
|
+
const entry = typeof process.argv[1] === "string" ? process.argv[1] : "";
|
|
129
|
+
if (entry) {
|
|
130
|
+
const entryDir = path.dirname(path.resolve(entry));
|
|
131
|
+
pushIfExists(path.join(entryDir, "plugin-sdk", "outbound-runtime.js"));
|
|
132
|
+
pushIfExists(path.join(entryDir, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
133
|
+
let dir = entryDir;
|
|
134
|
+
for (let i = 0; i < 5; i++) {
|
|
135
|
+
const pkgJson = path.join(dir, "package.json");
|
|
136
|
+
if (fs.existsSync(pkgJson)) {
|
|
137
|
+
pushIfExists(path.join(dir, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
138
|
+
pushIfExists(path.join(dir, "plugin-sdk", "outbound-runtime.js"));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const parent = path.dirname(dir);
|
|
142
|
+
if (parent === dir) break;
|
|
143
|
+
dir = parent;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const req = createRequire(pathToFileURL(path.resolve(entry)).href);
|
|
147
|
+
for (const name of ["jeikclaw", "openclaw"]) try {
|
|
148
|
+
const pkgJson = req.resolve(`${name}/package.json`);
|
|
149
|
+
const root = path.dirname(pkgJson);
|
|
150
|
+
pushIfExists(path.join(root, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
151
|
+
} catch {}
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const execDir = path.dirname(process.execPath);
|
|
156
|
+
pushIfExists(path.join(execDir, "..", "lib", "node_modules", "jeikclaw", "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
157
|
+
pushIfExists(path.join(execDir, "..", "lib", "node_modules", "openclaw", "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
158
|
+
} catch {}
|
|
159
|
+
const errors = [];
|
|
160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
161
|
+
for (const id of candidates) {
|
|
162
|
+
if (!id || seen.has(id)) continue;
|
|
163
|
+
seen.add(id);
|
|
164
|
+
try {
|
|
165
|
+
const oc = await import(id);
|
|
166
|
+
if (typeof oc.appendOutboundMessageDeliveryContext === "function") return {
|
|
167
|
+
append: oc.appendOutboundMessageDeliveryContext.bind(oc),
|
|
168
|
+
resolveRoute: typeof oc.resolveOutboundSessionRoute === "function" ? oc.resolveOutboundSessionRoute.bind(oc) : void 0,
|
|
169
|
+
ensureEntry: typeof oc.ensureOutboundSessionEntry === "function" ? oc.ensureOutboundSessionEntry.bind(oc) : void 0,
|
|
170
|
+
via: id
|
|
171
|
+
};
|
|
172
|
+
errors.push(`${id}: no append export`);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
errors.push(`${id}: ${e?.message || e}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!warnedMissingApi) {
|
|
178
|
+
warnedMissingApi = true;
|
|
179
|
+
alwaysLog("warn", `无法加载 outbound-runtime。argv1=${entry || "-"} tried=${seen.size} err=${errors.slice(0, 4).join(" | ")}`);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
95
184
|
* 在 dws 发消息命令成功结束后,向目标/来源会话写入 outbound_message。
|
|
96
185
|
* 全程 best-effort,失败只打日志。
|
|
97
186
|
*/
|
|
@@ -104,25 +193,16 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
104
193
|
const parsed = parseDwsSendCommand(params.commandText);
|
|
105
194
|
if (!parsed) return false;
|
|
106
195
|
if (!parsed.target && effectiveMode === "target") {
|
|
107
|
-
|
|
196
|
+
alwaysLog("warn", `解析到 dws ${parsed.kind} 但无 --group/--user 目标,跳过 target 注入。cmd=${parsed.command.slice(0, 160)}`);
|
|
108
197
|
return false;
|
|
109
198
|
}
|
|
110
199
|
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
|
-
}
|
|
200
|
+
const oc = await loadOutboundRuntime();
|
|
201
|
+
if (!oc?.append) return false;
|
|
122
202
|
let targetRoute = null;
|
|
123
203
|
const targetTo = parsed.target;
|
|
124
|
-
if (targetTo && typeof resolveRoute === "function") try {
|
|
125
|
-
targetRoute = await resolveRoute({
|
|
204
|
+
if (targetTo && typeof oc.resolveRoute === "function") try {
|
|
205
|
+
targetRoute = await oc.resolveRoute({
|
|
126
206
|
cfg: params.cfg,
|
|
127
207
|
channel: CHANNEL_ID,
|
|
128
208
|
agentId: params.agentId,
|
|
@@ -130,15 +210,20 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
130
210
|
target: targetTo,
|
|
131
211
|
currentSessionKey: params.sourceSessionKey
|
|
132
212
|
});
|
|
133
|
-
if (targetRoute && typeof ensureEntry === "function") await ensureEntry({
|
|
213
|
+
if (targetRoute && typeof oc.ensureEntry === "function") await oc.ensureEntry({
|
|
134
214
|
cfg: params.cfg,
|
|
135
215
|
channel: CHANNEL_ID,
|
|
136
216
|
accountId: params.accountId,
|
|
137
217
|
route: targetRoute
|
|
138
218
|
});
|
|
139
219
|
} catch (err) {
|
|
220
|
+
alwaysLog("warn", `resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
140
221
|
log?.warn?.(`[DingTalk][dwsDeliveryContext] resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
141
222
|
}
|
|
223
|
+
if (effectiveMode === "target" && !targetRoute?.sessionKey) {
|
|
224
|
+
alwaysLog("warn", `target 模式但未解析到 sessionKey,无法写入目标会话。targetTo=${targetTo} via=${oc.via}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
142
227
|
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
228
|
const actionParams = {
|
|
144
229
|
action: "send",
|
|
@@ -158,7 +243,7 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
158
243
|
command: parsed.command
|
|
159
244
|
};
|
|
160
245
|
const idempotencyKey = params.toolCallId?.trim() || `dws:${params.sourceSessionKey}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
161
|
-
await append({
|
|
246
|
+
await oc.append({
|
|
162
247
|
cfg: params.cfg,
|
|
163
248
|
mode: effectiveMode,
|
|
164
249
|
agentId: params.agentId,
|
|
@@ -174,9 +259,11 @@ async function maybeInjectDwsOutboundContext(params) {
|
|
|
174
259
|
action,
|
|
175
260
|
idempotencyKey
|
|
176
261
|
});
|
|
262
|
+
alwaysLog("info", `已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"} via=${oc.via}`);
|
|
177
263
|
log?.info?.(`[DingTalk][dwsDeliveryContext] 已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"}`);
|
|
178
264
|
return true;
|
|
179
265
|
} catch (err) {
|
|
266
|
+
alwaysLog("warn", `注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
180
267
|
log?.warn?.(`[DingTalk][dwsDeliveryContext] 注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
181
268
|
return false;
|
|
182
269
|
}
|
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-HfPzJGwO.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-HfPzJGwO.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-DLR1ghNQ.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,129 @@ function resolveModeFromConfigs(cfg: any, accountConfig?: any): DwsDeliveryConte
|
|
|
176
193
|
return "target";
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
/**
|
|
197
|
+
* 加载 OC-4 append API。
|
|
198
|
+
*
|
|
199
|
+
* 注意:钉钉插件装在 ~/.openclaw/npm/projects/... 下,Node 从插件路径
|
|
200
|
+
* 解析不到名为 openclaw/jeikclaw 的 package(包名还可能是 jeikclaw)。
|
|
201
|
+
* 必须用 gateway 进程入口(process.argv[1])定位 dist/plugin-sdk。
|
|
202
|
+
*/
|
|
203
|
+
async function loadOutboundRuntime(): Promise<{
|
|
204
|
+
append?: (p: any) => Promise<void>;
|
|
205
|
+
resolveRoute?: (p: any) => Promise<any>;
|
|
206
|
+
ensureEntry?: (p: any) => Promise<void>;
|
|
207
|
+
via?: string;
|
|
208
|
+
} | null> {
|
|
209
|
+
const { pathToFileURL } = await import("node:url");
|
|
210
|
+
const path = await import("node:path");
|
|
211
|
+
const fs = await import("node:fs");
|
|
212
|
+
const { createRequire } = await import("node:module");
|
|
213
|
+
|
|
214
|
+
const candidates: string[] = [
|
|
215
|
+
"openclaw/plugin-sdk/outbound-runtime",
|
|
216
|
+
"jeikclaw/plugin-sdk/outbound-runtime",
|
|
217
|
+
"jeikclaw/dist/plugin-sdk/outbound-runtime.js",
|
|
218
|
+
"openclaw/dist/plugin-sdk/outbound-runtime.js",
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const pushIfExists = (filePath: string) => {
|
|
222
|
+
try {
|
|
223
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
224
|
+
candidates.push(pathToFileURL(filePath).href);
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
/* ignore */
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// gateway 入口通常是 .../jeikclaw/dist/index.js 或 openclaw.mjs
|
|
232
|
+
const entry = typeof process.argv[1] === "string" ? process.argv[1] : "";
|
|
233
|
+
if (entry) {
|
|
234
|
+
const entryDir = path.dirname(path.resolve(entry));
|
|
235
|
+
// .../dist/index.js → .../dist/plugin-sdk/outbound-runtime.js
|
|
236
|
+
pushIfExists(path.join(entryDir, "plugin-sdk", "outbound-runtime.js"));
|
|
237
|
+
// .../openclaw.mjs → .../dist/plugin-sdk/...
|
|
238
|
+
pushIfExists(path.join(entryDir, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
239
|
+
// 再向上找 package root
|
|
240
|
+
let dir = entryDir;
|
|
241
|
+
for (let i = 0; i < 5; i++) {
|
|
242
|
+
const pkgJson = path.join(dir, "package.json");
|
|
243
|
+
if (fs.existsSync(pkgJson)) {
|
|
244
|
+
pushIfExists(path.join(dir, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
245
|
+
pushIfExists(path.join(dir, "plugin-sdk", "outbound-runtime.js"));
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const parent = path.dirname(dir);
|
|
249
|
+
if (parent === dir) break;
|
|
250
|
+
dir = parent;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// createRequire 从 gateway 入口解析 package 名
|
|
254
|
+
try {
|
|
255
|
+
const req = createRequire(pathToFileURL(path.resolve(entry)).href);
|
|
256
|
+
for (const name of ["jeikclaw", "openclaw"]) {
|
|
257
|
+
try {
|
|
258
|
+
const pkgJson = req.resolve(`${name}/package.json`);
|
|
259
|
+
const root = path.dirname(pkgJson);
|
|
260
|
+
pushIfExists(path.join(root, "dist", "plugin-sdk", "outbound-runtime.js"));
|
|
261
|
+
} catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
/* ignore */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 也试 process.execPath 同级的全局 node_modules
|
|
271
|
+
try {
|
|
272
|
+
const execDir = path.dirname(process.execPath);
|
|
273
|
+
pushIfExists(
|
|
274
|
+
path.join(execDir, "..", "lib", "node_modules", "jeikclaw", "dist", "plugin-sdk", "outbound-runtime.js"),
|
|
275
|
+
);
|
|
276
|
+
pushIfExists(
|
|
277
|
+
path.join(execDir, "..", "lib", "node_modules", "openclaw", "dist", "plugin-sdk", "outbound-runtime.js"),
|
|
278
|
+
);
|
|
279
|
+
} catch {
|
|
280
|
+
/* ignore */
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const errors: string[] = [];
|
|
284
|
+
const seen = new Set<string>();
|
|
285
|
+
for (const id of candidates) {
|
|
286
|
+
if (!id || seen.has(id)) continue;
|
|
287
|
+
seen.add(id);
|
|
288
|
+
try {
|
|
289
|
+
const oc: any = await import(id);
|
|
290
|
+
if (typeof oc.appendOutboundMessageDeliveryContext === "function") {
|
|
291
|
+
return {
|
|
292
|
+
append: oc.appendOutboundMessageDeliveryContext.bind(oc),
|
|
293
|
+
resolveRoute:
|
|
294
|
+
typeof oc.resolveOutboundSessionRoute === "function"
|
|
295
|
+
? oc.resolveOutboundSessionRoute.bind(oc)
|
|
296
|
+
: undefined,
|
|
297
|
+
ensureEntry:
|
|
298
|
+
typeof oc.ensureOutboundSessionEntry === "function"
|
|
299
|
+
? oc.ensureOutboundSessionEntry.bind(oc)
|
|
300
|
+
: undefined,
|
|
301
|
+
via: id,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
errors.push(`${id}: no append export`);
|
|
305
|
+
} catch (e: any) {
|
|
306
|
+
errors.push(`${id}: ${e?.message || e}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!warnedMissingApi) {
|
|
310
|
+
warnedMissingApi = true;
|
|
311
|
+
alwaysLog(
|
|
312
|
+
"warn",
|
|
313
|
+
`无法加载 outbound-runtime。argv1=${entry || "-"} tried=${seen.size} err=${errors.slice(0, 4).join(" | ")}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
179
319
|
/**
|
|
180
320
|
* 在 dws 发消息命令成功结束后,向目标/来源会话写入 outbound_message。
|
|
181
321
|
* 全程 best-effort,失败只打日志。
|
|
@@ -195,44 +335,31 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
195
335
|
}
|
|
196
336
|
|
|
197
337
|
const parsed = parseDwsSendCommand(params.commandText);
|
|
198
|
-
if (!parsed)
|
|
338
|
+
if (!parsed) {
|
|
339
|
+
// 不是 dws send 命令:静默(避免刷屏)
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
199
342
|
|
|
200
343
|
// 无明确 target 时,仅 source 模式才写
|
|
201
344
|
if (!parsed.target && effectiveMode === "target") {
|
|
202
|
-
|
|
203
|
-
|
|
345
|
+
alwaysLog(
|
|
346
|
+
"warn",
|
|
347
|
+
`解析到 dws ${parsed.kind} 但无 --group/--user 目标,跳过 target 注入。cmd=${parsed.command.slice(0, 160)}`,
|
|
204
348
|
);
|
|
205
349
|
return false;
|
|
206
350
|
}
|
|
207
351
|
|
|
208
352
|
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
|
-
}
|
|
353
|
+
const oc = await loadOutboundRuntime();
|
|
354
|
+
if (!oc?.append) {
|
|
228
355
|
return false;
|
|
229
356
|
}
|
|
230
357
|
|
|
231
358
|
let targetRoute: any = null;
|
|
232
359
|
const targetTo = parsed.target;
|
|
233
|
-
if (targetTo && typeof resolveRoute === "function") {
|
|
360
|
+
if (targetTo && typeof oc.resolveRoute === "function") {
|
|
234
361
|
try {
|
|
235
|
-
targetRoute = await resolveRoute({
|
|
362
|
+
targetRoute = await oc.resolveRoute({
|
|
236
363
|
cfg: params.cfg,
|
|
237
364
|
channel: CHANNEL_ID,
|
|
238
365
|
agentId: params.agentId,
|
|
@@ -240,8 +367,8 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
240
367
|
target: targetTo,
|
|
241
368
|
currentSessionKey: params.sourceSessionKey,
|
|
242
369
|
});
|
|
243
|
-
if (targetRoute && typeof ensureEntry === "function") {
|
|
244
|
-
await ensureEntry({
|
|
370
|
+
if (targetRoute && typeof oc.ensureEntry === "function") {
|
|
371
|
+
await oc.ensureEntry({
|
|
245
372
|
cfg: params.cfg,
|
|
246
373
|
channel: CHANNEL_ID,
|
|
247
374
|
accountId: params.accountId,
|
|
@@ -249,12 +376,21 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
249
376
|
});
|
|
250
377
|
}
|
|
251
378
|
} catch (err: any) {
|
|
379
|
+
alwaysLog("warn", `resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
252
380
|
log?.warn?.(
|
|
253
381
|
`[DingTalk][dwsDeliveryContext] resolveOutboundSessionRoute 失败: ${err?.message || err}`,
|
|
254
382
|
);
|
|
255
383
|
}
|
|
256
384
|
}
|
|
257
385
|
|
|
386
|
+
if (effectiveMode === "target" && !targetRoute?.sessionKey) {
|
|
387
|
+
alwaysLog(
|
|
388
|
+
"warn",
|
|
389
|
+
`target 模式但未解析到 sessionKey,无法写入目标会话。targetTo=${targetTo} via=${oc.via}`,
|
|
390
|
+
);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
258
394
|
// target 模式但路由失败 → 仍可写 source(若 both/source)
|
|
259
395
|
const action =
|
|
260
396
|
parsed.kind === "bot-send"
|
|
@@ -287,7 +423,7 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
287
423
|
params.toolCallId?.trim() ||
|
|
288
424
|
`dws:${params.sourceSessionKey}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
289
425
|
|
|
290
|
-
await append({
|
|
426
|
+
await oc.append({
|
|
291
427
|
cfg: params.cfg,
|
|
292
428
|
mode: effectiveMode,
|
|
293
429
|
agentId: params.agentId,
|
|
@@ -304,11 +440,16 @@ export async function maybeInjectDwsOutboundContext(
|
|
|
304
440
|
idempotencyKey,
|
|
305
441
|
});
|
|
306
442
|
|
|
443
|
+
alwaysLog(
|
|
444
|
+
"info",
|
|
445
|
+
`已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"} via=${oc.via}`,
|
|
446
|
+
);
|
|
307
447
|
log?.info?.(
|
|
308
448
|
`[DingTalk][dwsDeliveryContext] 已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"}`,
|
|
309
449
|
);
|
|
310
450
|
return true;
|
|
311
451
|
} catch (err: any) {
|
|
452
|
+
alwaysLog("warn", `注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
312
453
|
log?.warn?.(
|
|
313
454
|
`[DingTalk][dwsDeliveryContext] 注入失败(已忽略,不影响发消息): ${err?.message || err}`,
|
|
314
455
|
);
|