@jeik/dingtalk-connector 0.8.32 → 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 +20 -0
- package/dist/card-content-cache-BsFPIwOr.mjs +2 -0
- package/dist/{runtime-CjE4Z9G5.mjs → channel-HfPzJGwO.mjs} +18 -29
- package/dist/chunk-upload-CNlZg1SF.mjs +2 -0
- package/dist/{common-BfIYs1yC.mjs → common-1-GDZ2h3.mjs} +2 -2
- package/dist/{common-C0Oaf0z-.mjs → common-DCVdWMiZ.mjs} +1 -1
- package/dist/{connection-obtrPkj4.mjs → connection-Bh3U9I4x.mjs} +2 -2
- package/dist/dws-delivery-context-DLR1ghNQ.mjs +272 -0
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-CV8y0xGw.mjs +2 -0
- package/dist/{gateway-methods-BdcpfxEZ.mjs → gateway-methods-Ce1FXmCx.mjs} +4 -4
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +3 -2
- package/dist/{message-handler-BeTQQCUe.mjs → message-handler-Dsyk414y.mjs} +43 -14
- package/dist/{messaging-CK0Wz3Ze.mjs → messaging-CwQLykl6.mjs} +1 -1
- package/dist/runtime-DDMzayvA.mjs +26 -0
- package/dist/utils-legacy-Bx5izlVF.mjs +3 -0
- package/openclaw.plugin.json +28 -1
- package/package.json +1 -1
- package/src/config/schema.ts +11 -0
- package/src/core/message-handler.ts +2 -0
- package/src/reply-dispatcher.ts +46 -0
- package/src/services/dws-delivery-context.ts +458 -0
- package/dist/card-content-cache-C7-XHhHw.mjs +0 -2
- package/dist/chunk-upload-BKM1y4Ba.mjs +0 -2
- package/dist/gateway-methods-DQ6TwOAr.mjs +0 -2
- package/dist/utils-legacy-BOqY3zE7.mjs +0 -3
- /package/dist/{background-work-DkaSDqWk.mjs → background-work-874-aK4-.mjs} +0 -0
- /package/dist/{card-content-cache-DT_OYThh.mjs → card-content-cache-CGggfgCg.mjs} +0 -0
- /package/dist/{chunk-upload-CIU5G_ay.mjs → chunk-upload-z185bTqH.mjs} +0 -0
- /package/dist/{game-xiyou-uwsv23AC.mjs → game-xiyou-CKoWJobs.mjs} +0 -0
- /package/dist/{utils-legacy-DQ3HeGHh.mjs → utils-legacy-CGEa8tg2.mjs} +0 -0
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dingtalk-connector",
|
|
3
3
|
"name": "DingTalk Channel",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.34",
|
|
5
5
|
"description": "OpenClaw DingTalk channel plugin (community) | 钉钉 OpenClaw 社区增强版",
|
|
6
6
|
"author": "jeik",
|
|
7
7
|
"main": "index.ts",
|
|
@@ -269,6 +269,17 @@
|
|
|
269
269
|
"markdown"
|
|
270
270
|
]
|
|
271
271
|
},
|
|
272
|
+
"dwsDeliveryContext": {
|
|
273
|
+
"type": "string",
|
|
274
|
+
"enum": [
|
|
275
|
+
"off",
|
|
276
|
+
"target",
|
|
277
|
+
"source",
|
|
278
|
+
"both"
|
|
279
|
+
],
|
|
280
|
+
"default": "target",
|
|
281
|
+
"description": "dws CLI 发消息成功后写入 outbound_message 上下文(与 message 工具一致)。target=目标会话(默认);source=发起会话;both=两边;off=关闭。仅叠加,不影响普通回复/message 工具。"
|
|
282
|
+
},
|
|
272
283
|
"cardTemplateId": {
|
|
273
284
|
"type": "string"
|
|
274
285
|
},
|
|
@@ -522,6 +533,17 @@
|
|
|
522
533
|
"markdown"
|
|
523
534
|
]
|
|
524
535
|
},
|
|
536
|
+
"dwsDeliveryContext": {
|
|
537
|
+
"type": "string",
|
|
538
|
+
"enum": [
|
|
539
|
+
"off",
|
|
540
|
+
"target",
|
|
541
|
+
"source",
|
|
542
|
+
"both"
|
|
543
|
+
],
|
|
544
|
+
"default": "target",
|
|
545
|
+
"description": "dws CLI 发消息成功后写入 outbound_message 上下文。target=目标会话(默认);source=发起会话;both=两边;off=关闭。"
|
|
546
|
+
},
|
|
525
547
|
"cardTemplateId": {
|
|
526
548
|
"type": "string"
|
|
527
549
|
},
|
|
@@ -588,6 +610,11 @@
|
|
|
588
610
|
"help": "群回复形态:aicard(默认,流式 AI 卡)/ text / markdown。text、markdown 时群聊不用 AI 卡,便于多机器人互相 @。",
|
|
589
611
|
"advanced": true
|
|
590
612
|
},
|
|
613
|
+
"channels.dingtalk-connector.dwsDeliveryContext": {
|
|
614
|
+
"label": "DWS Delivery Context",
|
|
615
|
+
"help": "dws 发消息成功后注入 outbound_message(与 message 工具一致)。默认 target;设 off 关闭。仅叠加,不影响现有回复。",
|
|
616
|
+
"advanced": true
|
|
617
|
+
},
|
|
591
618
|
"channels.dingtalk-connector.cardTemplateId": {
|
|
592
619
|
"label": "AI Card Template ID",
|
|
593
620
|
"help": "会话流式 AI Card 模板 ID;不填默认 0d2c84b3-12c1-473b-b14a-f329a7a102cd.schema。",
|
package/package.json
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -100,6 +100,17 @@ const DingtalkSharedConfigShape = {
|
|
|
100
100
|
*/
|
|
101
101
|
messageAnswerCard: z.boolean().optional(),
|
|
102
102
|
groupReplyMode: GroupReplyModeSchema,
|
|
103
|
+
/**
|
|
104
|
+
* dws CLI 发消息成功后,是否写入与 message 工具一致的 outbound_message 上下文。
|
|
105
|
+
* - target(默认):写入目标会话(群/好友),对方会话后续能看见「谁让 bot 发了什么」
|
|
106
|
+
* - source:写入发起会话
|
|
107
|
+
* - both:两边都写
|
|
108
|
+
* - off:关闭(完全不改行为)
|
|
109
|
+
* 仅叠加:不影响普通回复 / message 工具;解析失败或 dws 非发消息命令时 no-op。
|
|
110
|
+
*/
|
|
111
|
+
dwsDeliveryContext: z
|
|
112
|
+
.enum(["off", "target", "source", "both"])
|
|
113
|
+
.optional(),
|
|
103
114
|
/** AI Card 模板 ID,不填则使用官方默认模板 */
|
|
104
115
|
cardTemplateId: z.string().optional(),
|
|
105
116
|
/** AI Card 内容变量名,对应卡片模板中的变量字段,不填默认 content(与默认增强模板一致) */
|
|
@@ -1794,6 +1794,8 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1794
1794
|
sessionWebhook: data.sessionWebhook,
|
|
1795
1795
|
asyncMode,
|
|
1796
1796
|
preCreatedCard: earlyCard,
|
|
1797
|
+
sessionKey, // dws 发消息成功后写 outbound_message 用
|
|
1798
|
+
senderName,
|
|
1797
1799
|
});
|
|
1798
1800
|
finalizeDispatchError = finalizeErr;
|
|
1799
1801
|
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -274,6 +274,10 @@ export type CreateDingtalkReplyDispatcherParams = {
|
|
|
274
274
|
asyncMode?: boolean;
|
|
275
275
|
/** 队列繁忙时预先创建的 AI Card,startStreaming 时直接复用而非新建 */
|
|
276
276
|
preCreatedCard?: AICardInstance;
|
|
277
|
+
/** 当前会话 sessionKey(用于 dws 发消息成功后写 outbound_message) */
|
|
278
|
+
sessionKey?: string;
|
|
279
|
+
/** 调用人昵称(dws outbound 上下文 invoker) */
|
|
280
|
+
senderName?: string;
|
|
277
281
|
};
|
|
278
282
|
|
|
279
283
|
export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatcherParams) {
|
|
@@ -288,6 +292,8 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
288
292
|
sessionWebhook,
|
|
289
293
|
asyncMode = false,
|
|
290
294
|
preCreatedCard,
|
|
295
|
+
sessionKey: sourceSessionKey,
|
|
296
|
+
senderName,
|
|
291
297
|
} = params;
|
|
292
298
|
|
|
293
299
|
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
@@ -1670,6 +1676,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
1670
1676
|
}
|
|
1671
1677
|
},
|
|
1672
1678
|
// ===== 养成系统:监听 dws 命令执行 =====
|
|
1679
|
+
// ===== dws 发消息成功 → outbound_message 上下文注入(OC-4 对齐)=====
|
|
1673
1680
|
onCommandOutput: (payload: {
|
|
1674
1681
|
itemId?: string;
|
|
1675
1682
|
phase?: string;
|
|
@@ -1695,6 +1702,45 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
1695
1702
|
log.info(`[DingTalk][onCommandOutput] dws 命令执行失败,跳过: ${product},exitCode=${payload.exitCode}`);
|
|
1696
1703
|
}
|
|
1697
1704
|
}
|
|
1705
|
+
|
|
1706
|
+
// dws chat message send* 成功后注入 outbound_message(best-effort,不阻塞)
|
|
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。两者都扫,兼容多行 `\` 续行
|
|
1711
|
+
const fullCmd =
|
|
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
|
+
}
|
|
1743
|
+
}
|
|
1698
1744
|
// 工具进度改由 onToolStart + 正文同一 cardContentVar 展示,不再写独立 cardToolVar 字段
|
|
1699
1745
|
},
|
|
1700
1746
|
},
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWS 发消息成功后注入 outbound_message 上下文(对齐 OpenClaw message 工具 OC-4)。
|
|
3
|
+
*
|
|
4
|
+
* - 不修改 dws 本体:仅解析 OpenClaw exec/shell 回调里看到的命令行
|
|
5
|
+
* - 默认开启(target):只在 dws 发消息成功时追加上下文,不影响普通回复 / message 工具
|
|
6
|
+
* - 可配置 off | target | source | both
|
|
7
|
+
*/
|
|
8
|
+
import { CHANNEL_ID } from "../channel.ts";
|
|
9
|
+
|
|
10
|
+
export type DwsDeliveryContextMode = "off" | "target" | "source" | "both";
|
|
11
|
+
|
|
12
|
+
export type ParsedDwsSendCommand = {
|
|
13
|
+
/** dws 子命令身份 */
|
|
14
|
+
kind: "user-send" | "bot-send" | "webhook-send" | "reply";
|
|
15
|
+
/** 规范化后的 message 工具风格 target:group:… / user:… */
|
|
16
|
+
target?: string;
|
|
17
|
+
/** 原始 openConversationId / userId / openDingTalkId */
|
|
18
|
+
rawTarget?: string;
|
|
19
|
+
targetChatType?: "group" | "direct";
|
|
20
|
+
message?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
atAll?: boolean;
|
|
23
|
+
atUserIds?: string[];
|
|
24
|
+
atOpenDingTalkIds?: string[];
|
|
25
|
+
atMobiles?: string[];
|
|
26
|
+
robotCode?: string;
|
|
27
|
+
/** 原始命令文本(截断后写入 args) */
|
|
28
|
+
command: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SEND_COMMAND_RE =
|
|
32
|
+
/\bdws\b(?:\s+[^\s]+)*\s+chat\s+message\s+(send-by-bot|send-by-webhook|send|reply)\b/i;
|
|
33
|
+
|
|
34
|
+
/** 从命令行提取 --flag value / --flag=value / 布尔 --flag */
|
|
35
|
+
function extractFlag(cmd: string, name: string): string | undefined {
|
|
36
|
+
const eq = new RegExp(`--${name}(?:=|\\s+)(?:"([^"]*)"|'([^']*)'|(\\S+))`, "i");
|
|
37
|
+
const m = cmd.match(eq);
|
|
38
|
+
if (!m) return undefined;
|
|
39
|
+
return (m[1] ?? m[2] ?? m[3] ?? "").trim() || undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasBoolFlag(cmd: string, name: string): boolean {
|
|
43
|
+
return new RegExp(`(?:^|\\s)--${name}(?:\\s|=|$)`, "i").test(cmd);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function splitCsv(raw?: string): string[] | undefined {
|
|
47
|
+
if (!raw) return undefined;
|
|
48
|
+
const parts = raw
|
|
49
|
+
.split(",")
|
|
50
|
+
.map((s) => s.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
return parts.length ? parts : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 判断是否为 dws 发消息类命令,并解析关键参数。
|
|
57
|
+
* 解析失败返回 null(调用方 no-op,不影响现有体验)。
|
|
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
|
+
|
|
68
|
+
export function parseDwsSendCommand(commandText: string): ParsedDwsSendCommand | null {
|
|
69
|
+
const cmd = normalizeShellCommandText(commandText);
|
|
70
|
+
if (!cmd) return null;
|
|
71
|
+
const m = cmd.match(SEND_COMMAND_RE);
|
|
72
|
+
if (!m) return null;
|
|
73
|
+
|
|
74
|
+
const sub = m[1].toLowerCase();
|
|
75
|
+
let kind: ParsedDwsSendCommand["kind"];
|
|
76
|
+
if (sub === "send-by-bot") kind = "bot-send";
|
|
77
|
+
else if (sub === "send-by-webhook") kind = "webhook-send";
|
|
78
|
+
else if (sub === "reply") kind = "reply";
|
|
79
|
+
else kind = "user-send";
|
|
80
|
+
|
|
81
|
+
const group =
|
|
82
|
+
extractFlag(cmd, "group") ||
|
|
83
|
+
extractFlag(cmd, "conversation-id") ||
|
|
84
|
+
extractFlag(cmd, "id") ||
|
|
85
|
+
extractFlag(cmd, "chat");
|
|
86
|
+
const user =
|
|
87
|
+
extractFlag(cmd, "user") ||
|
|
88
|
+
extractFlag(cmd, "open-dingtalk-id") ||
|
|
89
|
+
// 批量单聊取第一个,便于 session 映射
|
|
90
|
+
splitCsv(extractFlag(cmd, "users"))?.[0] ||
|
|
91
|
+
splitCsv(extractFlag(cmd, "open-dingtalk-ids"))?.[0];
|
|
92
|
+
|
|
93
|
+
const text =
|
|
94
|
+
extractFlag(cmd, "text") ||
|
|
95
|
+
extractFlag(cmd, "content") ||
|
|
96
|
+
extractFlag(cmd, "body") ||
|
|
97
|
+
extractFlag(cmd, "message") ||
|
|
98
|
+
extractFlag(cmd, "markdown");
|
|
99
|
+
|
|
100
|
+
let target: string | undefined;
|
|
101
|
+
let rawTarget: string | undefined;
|
|
102
|
+
let targetChatType: "group" | "direct" | undefined;
|
|
103
|
+
|
|
104
|
+
if (group) {
|
|
105
|
+
rawTarget = group.replace(/^(group:|channel:)/i, "");
|
|
106
|
+
target = `group:${rawTarget}`;
|
|
107
|
+
targetChatType = "group";
|
|
108
|
+
} else if (user) {
|
|
109
|
+
rawTarget = user.replace(/^(user:|dm:)/i, "");
|
|
110
|
+
target = `user:${rawTarget}`;
|
|
111
|
+
targetChatType = "direct";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// webhook 无明确会话目标时无法写 target session
|
|
115
|
+
if (kind === "webhook-send" && !target) {
|
|
116
|
+
return {
|
|
117
|
+
kind,
|
|
118
|
+
message: text,
|
|
119
|
+
title: extractFlag(cmd, "title"),
|
|
120
|
+
atAll: hasBoolFlag(cmd, "at-all"),
|
|
121
|
+
atUserIds: splitCsv(extractFlag(cmd, "at-users")),
|
|
122
|
+
atMobiles: splitCsv(extractFlag(cmd, "at-mobiles")),
|
|
123
|
+
command: cmd.slice(0, 2000),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
kind,
|
|
129
|
+
target,
|
|
130
|
+
rawTarget,
|
|
131
|
+
targetChatType,
|
|
132
|
+
message: text,
|
|
133
|
+
title: extractFlag(cmd, "title"),
|
|
134
|
+
atAll: hasBoolFlag(cmd, "at-all"),
|
|
135
|
+
atUserIds:
|
|
136
|
+
splitCsv(extractFlag(cmd, "at-user-ids")) ||
|
|
137
|
+
splitCsv(extractFlag(cmd, "at-users")),
|
|
138
|
+
atOpenDingTalkIds: splitCsv(extractFlag(cmd, "at-open-dingtalk-ids")),
|
|
139
|
+
atMobiles: splitCsv(extractFlag(cmd, "at-mobiles")),
|
|
140
|
+
robotCode: extractFlag(cmd, "robot-code"),
|
|
141
|
+
command: cmd.slice(0, 2000),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resolveDwsDeliveryContextMode(
|
|
146
|
+
config: any,
|
|
147
|
+
): DwsDeliveryContextMode {
|
|
148
|
+
const raw = config?.dwsDeliveryContext;
|
|
149
|
+
if (raw === "off" || raw === false) return "off";
|
|
150
|
+
if (raw === "source" || raw === "both" || raw === "target") return raw;
|
|
151
|
+
// 默认 target:仅叠加,不改 message 工具 / 普通回复
|
|
152
|
+
if (raw === true || raw === undefined || raw === null) return "target";
|
|
153
|
+
return "off";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type MaybeInjectDwsOutboundContextParams = {
|
|
157
|
+
/** 完整 OpenClaw 配置(resolveOutboundSessionRoute 需要) */
|
|
158
|
+
cfg: any;
|
|
159
|
+
/** 账号合并后的钉钉配置(读取 dwsDeliveryContext) */
|
|
160
|
+
accountConfig?: any;
|
|
161
|
+
agentId: string;
|
|
162
|
+
accountId?: string;
|
|
163
|
+
/** 当前发起会话 sessionKey */
|
|
164
|
+
sourceSessionKey: string;
|
|
165
|
+
invokerId?: string;
|
|
166
|
+
invokerName?: string;
|
|
167
|
+
commandText: string;
|
|
168
|
+
exitCode?: number | null;
|
|
169
|
+
phase?: string;
|
|
170
|
+
toolCallId?: string;
|
|
171
|
+
log?: { info?: (...a: any[]) => void; warn?: (...a: any[]) => void };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let warnedMissingApi = false;
|
|
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
|
+
|
|
184
|
+
function resolveModeFromConfigs(cfg: any, accountConfig?: any): DwsDeliveryContextMode {
|
|
185
|
+
// 账号级 > 渠道顶层 > 默认 target
|
|
186
|
+
if (accountConfig?.dwsDeliveryContext !== undefined) {
|
|
187
|
+
return resolveDwsDeliveryContextMode(accountConfig);
|
|
188
|
+
}
|
|
189
|
+
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
190
|
+
if (channelCfg?.dwsDeliveryContext !== undefined) {
|
|
191
|
+
return resolveDwsDeliveryContextMode(channelCfg);
|
|
192
|
+
}
|
|
193
|
+
return "target";
|
|
194
|
+
}
|
|
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
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 在 dws 发消息命令成功结束后,向目标/来源会话写入 outbound_message。
|
|
321
|
+
* 全程 best-effort,失败只打日志。
|
|
322
|
+
*/
|
|
323
|
+
export async function maybeInjectDwsOutboundContext(
|
|
324
|
+
params: MaybeInjectDwsOutboundContextParams,
|
|
325
|
+
): Promise<boolean> {
|
|
326
|
+
const log = params.log;
|
|
327
|
+
const effectiveMode = resolveModeFromConfigs(params.cfg, params.accountConfig);
|
|
328
|
+
|
|
329
|
+
if (effectiveMode === "off") return false;
|
|
330
|
+
|
|
331
|
+
// 仅在命令结束且成功时注入
|
|
332
|
+
if (params.phase && params.phase !== "end") return false;
|
|
333
|
+
if (params.exitCode !== undefined && params.exitCode !== null && params.exitCode !== 0) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const parsed = parseDwsSendCommand(params.commandText);
|
|
338
|
+
if (!parsed) {
|
|
339
|
+
// 不是 dws send 命令:静默(避免刷屏)
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 无明确 target 时,仅 source 模式才写
|
|
344
|
+
if (!parsed.target && effectiveMode === "target") {
|
|
345
|
+
alwaysLog(
|
|
346
|
+
"warn",
|
|
347
|
+
`解析到 dws ${parsed.kind} 但无 --group/--user 目标,跳过 target 注入。cmd=${parsed.command.slice(0, 160)}`,
|
|
348
|
+
);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const oc = await loadOutboundRuntime();
|
|
354
|
+
if (!oc?.append) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let targetRoute: any = null;
|
|
359
|
+
const targetTo = parsed.target;
|
|
360
|
+
if (targetTo && typeof oc.resolveRoute === "function") {
|
|
361
|
+
try {
|
|
362
|
+
targetRoute = await oc.resolveRoute({
|
|
363
|
+
cfg: params.cfg,
|
|
364
|
+
channel: CHANNEL_ID,
|
|
365
|
+
agentId: params.agentId,
|
|
366
|
+
accountId: params.accountId,
|
|
367
|
+
target: targetTo,
|
|
368
|
+
currentSessionKey: params.sourceSessionKey,
|
|
369
|
+
});
|
|
370
|
+
if (targetRoute && typeof oc.ensureEntry === "function") {
|
|
371
|
+
await oc.ensureEntry({
|
|
372
|
+
cfg: params.cfg,
|
|
373
|
+
channel: CHANNEL_ID,
|
|
374
|
+
accountId: params.accountId,
|
|
375
|
+
route: targetRoute,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
} catch (err: any) {
|
|
379
|
+
alwaysLog("warn", `resolveOutboundSessionRoute 失败: ${err?.message || err}`);
|
|
380
|
+
log?.warn?.(
|
|
381
|
+
`[DingTalk][dwsDeliveryContext] resolveOutboundSessionRoute 失败: ${err?.message || err}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
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
|
+
|
|
394
|
+
// target 模式但路由失败 → 仍可写 source(若 both/source)
|
|
395
|
+
const action =
|
|
396
|
+
parsed.kind === "bot-send"
|
|
397
|
+
? "dws.chat.message.send-by-bot"
|
|
398
|
+
: parsed.kind === "webhook-send"
|
|
399
|
+
? "dws.chat.message.send-by-webhook"
|
|
400
|
+
: parsed.kind === "reply"
|
|
401
|
+
? "dws.chat.message.reply"
|
|
402
|
+
: "dws.chat.message.send";
|
|
403
|
+
|
|
404
|
+
const actionParams: Record<string, unknown> = {
|
|
405
|
+
action: "send",
|
|
406
|
+
via: "dws",
|
|
407
|
+
dwsKind: parsed.kind,
|
|
408
|
+
message: parsed.message ?? "",
|
|
409
|
+
title: parsed.title,
|
|
410
|
+
target: targetTo,
|
|
411
|
+
to: parsed.rawTarget,
|
|
412
|
+
channel: CHANNEL_ID,
|
|
413
|
+
accountId: params.accountId,
|
|
414
|
+
atAll: parsed.atAll || undefined,
|
|
415
|
+
atUserIds: parsed.atUserIds,
|
|
416
|
+
atOpenDingTalkIds: parsed.atOpenDingTalkIds,
|
|
417
|
+
atMobiles: parsed.atMobiles,
|
|
418
|
+
robotCode: parsed.robotCode,
|
|
419
|
+
command: parsed.command,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const idempotencyKey =
|
|
423
|
+
params.toolCallId?.trim() ||
|
|
424
|
+
`dws:${params.sourceSessionKey}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
425
|
+
|
|
426
|
+
await oc.append({
|
|
427
|
+
cfg: params.cfg,
|
|
428
|
+
mode: effectiveMode,
|
|
429
|
+
agentId: params.agentId,
|
|
430
|
+
sourceSessionKey: params.sourceSessionKey,
|
|
431
|
+
sourceAccountId: params.accountId,
|
|
432
|
+
invokerId: params.invokerId,
|
|
433
|
+
invokerName: params.invokerName,
|
|
434
|
+
targetTo: targetTo || params.sourceSessionKey,
|
|
435
|
+
targetChannel: CHANNEL_ID,
|
|
436
|
+
targetAccountId: params.accountId,
|
|
437
|
+
targetRoute,
|
|
438
|
+
actionParams,
|
|
439
|
+
action,
|
|
440
|
+
idempotencyKey,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
alwaysLog(
|
|
444
|
+
"info",
|
|
445
|
+
`已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"} via=${oc.via}`,
|
|
446
|
+
);
|
|
447
|
+
log?.info?.(
|
|
448
|
+
`[DingTalk][dwsDeliveryContext] 已注入 outbound_message mode=${effectiveMode} kind=${parsed.kind} target=${targetTo || "-"} route=${targetRoute?.sessionKey || "-"}`,
|
|
449
|
+
);
|
|
450
|
+
return true;
|
|
451
|
+
} catch (err: any) {
|
|
452
|
+
alwaysLog("warn", `注入失败(已忽略,不影响发消息): ${err?.message || err}`);
|
|
453
|
+
log?.warn?.(
|
|
454
|
+
`[DingTalk][dwsDeliveryContext] 注入失败(已忽略,不影响发消息): ${err?.message || err}`,
|
|
455
|
+
);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|