@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 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-BoVSEDwY.mjs"),
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-CABtqkf_.mjs";
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 = String(commandText || "").trim();
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
- log?.info?.(`[DingTalk][dwsDeliveryContext] 解析到 dws ${parsed.kind} 但无 group/user 目标,跳过 target 注入`);
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 import("openclaw/plugin-sdk/outbound-runtime");
112
- const append = oc.appendOutboundMessageDeliveryContext;
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-CABtqkf_.mjs";
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-CABtqkf_.mjs";
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
- ].filter(Boolean).join("\n") || commandText;
1038
- import("./dws-delivery-context-ESC7sij7.mjs").then(({ maybeInjectDwsOutboundContext }) => maybeInjectDwsOutboundContext({
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: payload.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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dingtalk-connector",
3
3
  "name": "DingTalk Channel",
4
- "version": "0.8.33",
4
+ "version": "0.8.34",
5
5
  "description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版",
6
6
  "author": "jeik",
7
7
  "main": "index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.33",
3
+ "version": "0.8.34",
4
4
  "description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版(基于官方 0.8.24,长连接与消息体验增强)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1704,31 +1704,42 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
1704
1704
  }
1705
1705
 
1706
1706
  // dws chat message send* 成功后注入 outbound_message(best-effort,不阻塞)
1707
- if (payload.phase === 'end' && sourceSessionKey) {
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].filter(Boolean).join('\n') || commandText;
1710
- void import('./services/dws-delivery-context.ts')
1711
- .then(({ maybeInjectDwsOutboundContext }) =>
1712
- maybeInjectDwsOutboundContext({
1713
- cfg,
1714
- accountConfig: account.config,
1715
- agentId,
1716
- accountId,
1717
- sourceSessionKey,
1718
- invokerId: senderId,
1719
- invokerName: senderName,
1720
- commandText: fullCmd,
1721
- exitCode: payload.exitCode,
1722
- phase: payload.phase,
1723
- toolCallId: payload.toolCallId,
1724
- log,
1725
- }),
1726
- )
1727
- .catch((err: any) => {
1728
- log.warn?.(
1729
- `[DingTalk][dwsDeliveryContext] onCommandOutput 钩子异常: ${err?.message || err}`,
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 = String(commandText || "").trim();
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) return false;
338
+ if (!parsed) {
339
+ // 不是 dws send 命令:静默(避免刷屏)
340
+ return false;
341
+ }
199
342
 
200
343
  // 无明确 target 时,仅 source 模式才写
201
344
  if (!parsed.target && effectiveMode === "target") {
202
- log?.info?.(
203
- `[DingTalk][dwsDeliveryContext] 解析到 dws ${parsed.kind} 但无 group/user 目标,跳过 target 注入`,
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 import("openclaw/plugin-sdk/outbound-runtime");
210
- const append = (oc as any).appendOutboundMessageDeliveryContext as
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
  );