@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 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-BoVSEDwY.mjs"),
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-CABtqkf_.mjs";
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 = 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,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
- log?.info?.(`[DingTalk][dwsDeliveryContext] 解析到 dws ${parsed.kind} 但无 group/user 目标,跳过 target 注入`);
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 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
- }
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-CABtqkf_.mjs";
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-CABtqkf_.mjs";
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
- ].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-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: 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.35",
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.35",
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,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) return false;
377
+ if (!parsed) {
378
+ // 不是 dws send 命令:静默(避免刷屏)
379
+ return false;
380
+ }
199
381
 
200
382
  // 无明确 target 时,仅 source 模式才写
201
383
  if (!parsed.target && effectiveMode === "target") {
202
- log?.info?.(
203
- `[DingTalk][dwsDeliveryContext] 解析到 dws ${parsed.kind} 但无 group/user 目标,跳过 target 注入`,
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 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
- }
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
  );