@adongguo/dingtalk 0.1.3
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/LICENSE +21 -0
- package/README.md +247 -0
- package/clawdbot.plugin.json +9 -0
- package/index.ts +86 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +60 -0
- package/src/accounts.ts +49 -0
- package/src/ai-card.ts +341 -0
- package/src/bot.ts +403 -0
- package/src/channel.ts +220 -0
- package/src/client.ts +49 -0
- package/src/config-schema.ts +119 -0
- package/src/directory.ts +90 -0
- package/src/gateway-stream.ts +159 -0
- package/src/media.ts +608 -0
- package/src/monitor.ts +127 -0
- package/src/onboarding.ts +355 -0
- package/src/outbound.ts +46 -0
- package/src/policy.ts +92 -0
- package/src/probe.ts +41 -0
- package/src/reactions.ts +64 -0
- package/src/reply-dispatcher.ts +167 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +314 -0
- package/src/session.ts +144 -0
- package/src/streaming-handler.ts +298 -0
- package/src/targets.ts +56 -0
- package/src/types.ts +198 -0
- package/src/typing.ts +36 -0
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { DWClient, TOPIC_ROBOT } from "dingtalk-stream";
|
|
2
|
+
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { DingTalkConfig, DingTalkIncomingMessage } from "./types.js";
|
|
4
|
+
import { createDingTalkClient } from "./client.js";
|
|
5
|
+
import { resolveDingTalkCredentials } from "./accounts.js";
|
|
6
|
+
import { handleDingTalkMessage } from "./bot.js";
|
|
7
|
+
|
|
8
|
+
export type MonitorDingTalkOpts = {
|
|
9
|
+
config?: ClawdbotConfig;
|
|
10
|
+
runtime?: RuntimeEnv;
|
|
11
|
+
abortSignal?: AbortSignal;
|
|
12
|
+
accountId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let currentClient: DWClient | null = null;
|
|
16
|
+
|
|
17
|
+
export async function monitorDingTalkProvider(opts: MonitorDingTalkOpts = {}): Promise<void> {
|
|
18
|
+
const cfg = opts.config;
|
|
19
|
+
if (!cfg) {
|
|
20
|
+
throw new Error("Config is required for DingTalk monitor");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const dingtalkCfg = cfg.channels?.dingtalk as DingTalkConfig | undefined;
|
|
24
|
+
const creds = resolveDingTalkCredentials(dingtalkCfg);
|
|
25
|
+
if (!creds) {
|
|
26
|
+
throw new Error("DingTalk credentials not configured (appKey, appSecret required)");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const log = opts.runtime?.log ?? console.log;
|
|
30
|
+
|
|
31
|
+
const connectionMode = dingtalkCfg?.connectionMode ?? "stream";
|
|
32
|
+
|
|
33
|
+
if (connectionMode === "stream") {
|
|
34
|
+
return monitorStream({ cfg, dingtalkCfg: dingtalkCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log("dingtalk: webhook mode not implemented in monitor, use HTTP server directly");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function monitorStream(params: {
|
|
41
|
+
cfg: ClawdbotConfig;
|
|
42
|
+
dingtalkCfg: DingTalkConfig;
|
|
43
|
+
runtime?: RuntimeEnv;
|
|
44
|
+
abortSignal?: AbortSignal;
|
|
45
|
+
}): Promise<void> {
|
|
46
|
+
const { cfg, dingtalkCfg, runtime, abortSignal } = params;
|
|
47
|
+
const log = runtime?.log ?? console.log;
|
|
48
|
+
const error = runtime?.error ?? console.error;
|
|
49
|
+
|
|
50
|
+
log("dingtalk: starting Stream connection...");
|
|
51
|
+
|
|
52
|
+
const client = createDingTalkClient(dingtalkCfg);
|
|
53
|
+
currentClient = client;
|
|
54
|
+
|
|
55
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const cleanup = () => {
|
|
59
|
+
if (currentClient === client) {
|
|
60
|
+
try {
|
|
61
|
+
client.disconnect();
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore disconnect errors
|
|
64
|
+
}
|
|
65
|
+
currentClient = null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleAbort = () => {
|
|
70
|
+
log("dingtalk: abort signal received, stopping Stream client");
|
|
71
|
+
cleanup();
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (abortSignal?.aborted) {
|
|
76
|
+
cleanup();
|
|
77
|
+
resolve();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Register callback listener for robot messages
|
|
85
|
+
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
|
86
|
+
try {
|
|
87
|
+
const messageData = JSON.parse(res.data) as DingTalkIncomingMessage;
|
|
88
|
+
log(`dingtalk: received message from ${messageData.senderNick}: ${messageData.text?.content || messageData.msgtype}`);
|
|
89
|
+
|
|
90
|
+
await handleDingTalkMessage({
|
|
91
|
+
cfg,
|
|
92
|
+
message: messageData,
|
|
93
|
+
runtime,
|
|
94
|
+
chatHistories,
|
|
95
|
+
client,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Acknowledge the message
|
|
99
|
+
client.socketCallBackResponse(res.headers.messageId, { success: true });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
error(`dingtalk: error handling message: ${String(err)}`);
|
|
102
|
+
// Still acknowledge to prevent redelivery
|
|
103
|
+
client.socketCallBackResponse(res.headers.messageId, { success: false, error: String(err) });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Connect to DingTalk Stream
|
|
108
|
+
client.connect();
|
|
109
|
+
log("dingtalk: Stream client connected");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
cleanup();
|
|
112
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
113
|
+
reject(err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function stopDingTalkMonitor(): void {
|
|
119
|
+
if (currentClient) {
|
|
120
|
+
try {
|
|
121
|
+
currentClient.disconnect();
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore errors
|
|
124
|
+
}
|
|
125
|
+
currentClient = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
ClawdbotConfig,
|
|
5
|
+
DmPolicy,
|
|
6
|
+
WizardPrompter,
|
|
7
|
+
} from "openclaw/plugin-sdk";
|
|
8
|
+
import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk";
|
|
9
|
+
|
|
10
|
+
import { resolveDingTalkCredentials } from "./accounts.js";
|
|
11
|
+
import { probeDingTalk } from "./probe.js";
|
|
12
|
+
import type { DingTalkConfig } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const channel = "dingtalk" as const;
|
|
15
|
+
|
|
16
|
+
function setDingTalkDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
17
|
+
const allowFrom =
|
|
18
|
+
dmPolicy === "open"
|
|
19
|
+
? addWildcardAllowFrom(cfg.channels?.dingtalk?.allowFrom)?.map((entry) => String(entry))
|
|
20
|
+
: undefined;
|
|
21
|
+
return {
|
|
22
|
+
...cfg,
|
|
23
|
+
channels: {
|
|
24
|
+
...cfg.channels,
|
|
25
|
+
dingtalk: {
|
|
26
|
+
...cfg.channels?.dingtalk,
|
|
27
|
+
dmPolicy,
|
|
28
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setDingTalkAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
35
|
+
return {
|
|
36
|
+
...cfg,
|
|
37
|
+
channels: {
|
|
38
|
+
...cfg.channels,
|
|
39
|
+
dingtalk: {
|
|
40
|
+
...cfg.channels?.dingtalk,
|
|
41
|
+
allowFrom,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseAllowFromInput(raw: string): string[] {
|
|
48
|
+
return raw
|
|
49
|
+
.split(/[\n,;]+/g)
|
|
50
|
+
.map((entry) => entry.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function promptDingTalkAllowFrom(params: {
|
|
55
|
+
cfg: ClawdbotConfig;
|
|
56
|
+
prompter: WizardPrompter;
|
|
57
|
+
}): Promise<ClawdbotConfig> {
|
|
58
|
+
const existing = params.cfg.channels?.dingtalk?.allowFrom ?? [];
|
|
59
|
+
await params.prompter.note(
|
|
60
|
+
[
|
|
61
|
+
"Allowlist DingTalk DMs by staffId.",
|
|
62
|
+
"You can find user staffId in DingTalk admin console or via API.",
|
|
63
|
+
"Examples:",
|
|
64
|
+
"- 123456789",
|
|
65
|
+
"- manager001",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
"DingTalk allowlist",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
const entry = await params.prompter.text({
|
|
72
|
+
message: "DingTalk allowFrom (user staffIds)",
|
|
73
|
+
placeholder: "123456789, manager001",
|
|
74
|
+
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
75
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
76
|
+
});
|
|
77
|
+
const parts = parseAllowFromInput(String(entry));
|
|
78
|
+
if (parts.length === 0) {
|
|
79
|
+
await params.prompter.note("Enter at least one user.", "DingTalk allowlist");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const unique = [
|
|
84
|
+
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
|
|
85
|
+
];
|
|
86
|
+
return setDingTalkAllowFrom(params.cfg, unique);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function noteDingTalkCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
91
|
+
await prompter.note(
|
|
92
|
+
[
|
|
93
|
+
"1) Go to DingTalk Developer Console (open-dev.dingtalk.com)",
|
|
94
|
+
"2) Create an enterprise internal application",
|
|
95
|
+
"3) Get AppKey (ClientID) and AppSecret (ClientSecret) from Credentials page",
|
|
96
|
+
"4) Enable Robot capability and select Stream mode",
|
|
97
|
+
"5) Publish the app or add to test group",
|
|
98
|
+
"Tip: you can also set DINGTALK_APP_KEY / DINGTALK_APP_SECRET env vars.",
|
|
99
|
+
`Docs: ${formatDocsLink("/channels/dingtalk", "dingtalk")}`,
|
|
100
|
+
].join("\n"),
|
|
101
|
+
"DingTalk credentials",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setDingTalkGroupPolicy(
|
|
106
|
+
cfg: ClawdbotConfig,
|
|
107
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
108
|
+
): ClawdbotConfig {
|
|
109
|
+
return {
|
|
110
|
+
...cfg,
|
|
111
|
+
channels: {
|
|
112
|
+
...cfg.channels,
|
|
113
|
+
dingtalk: {
|
|
114
|
+
...cfg.channels?.dingtalk,
|
|
115
|
+
enabled: true,
|
|
116
|
+
groupPolicy,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setDingTalkGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
123
|
+
return {
|
|
124
|
+
...cfg,
|
|
125
|
+
channels: {
|
|
126
|
+
...cfg.channels,
|
|
127
|
+
dingtalk: {
|
|
128
|
+
...cfg.channels?.dingtalk,
|
|
129
|
+
groupAllowFrom,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
136
|
+
label: "DingTalk",
|
|
137
|
+
channel,
|
|
138
|
+
policyKey: "channels.dingtalk.dmPolicy",
|
|
139
|
+
allowFromKey: "channels.dingtalk.allowFrom",
|
|
140
|
+
getCurrent: (cfg) => (cfg.channels?.dingtalk as DingTalkConfig | undefined)?.dmPolicy ?? "pairing",
|
|
141
|
+
setPolicy: (cfg, policy) => setDingTalkDmPolicy(cfg, policy),
|
|
142
|
+
promptAllowFrom: promptDingTalkAllowFrom,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
146
|
+
channel,
|
|
147
|
+
getStatus: async ({ cfg }) => {
|
|
148
|
+
const dingtalkCfg = cfg.channels?.dingtalk as DingTalkConfig | undefined;
|
|
149
|
+
const configured = Boolean(resolveDingTalkCredentials(dingtalkCfg));
|
|
150
|
+
|
|
151
|
+
// Try to probe if configured
|
|
152
|
+
let probeResult = null;
|
|
153
|
+
if (configured && dingtalkCfg) {
|
|
154
|
+
try {
|
|
155
|
+
probeResult = await probeDingTalk(dingtalkCfg);
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore probe errors
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const statusLines: string[] = [];
|
|
162
|
+
if (!configured) {
|
|
163
|
+
statusLines.push("DingTalk: needs app credentials");
|
|
164
|
+
} else if (probeResult?.ok) {
|
|
165
|
+
statusLines.push(`DingTalk: connected (${probeResult.appKey ?? "bot"})`);
|
|
166
|
+
} else {
|
|
167
|
+
statusLines.push("DingTalk: configured (connection not verified)");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
channel,
|
|
172
|
+
configured,
|
|
173
|
+
statusLines,
|
|
174
|
+
selectionHint: configured ? "configured" : "needs app creds",
|
|
175
|
+
quickstartScore: configured ? 2 : 0,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
configure: async ({ cfg, prompter }) => {
|
|
180
|
+
const dingtalkCfg = cfg.channels?.dingtalk as DingTalkConfig | undefined;
|
|
181
|
+
const resolved = resolveDingTalkCredentials(dingtalkCfg);
|
|
182
|
+
const hasConfigCreds = Boolean(dingtalkCfg?.appKey?.trim() && dingtalkCfg?.appSecret?.trim());
|
|
183
|
+
const canUseEnv = Boolean(
|
|
184
|
+
!hasConfigCreds &&
|
|
185
|
+
process.env.DINGTALK_APP_KEY?.trim() &&
|
|
186
|
+
process.env.DINGTALK_APP_SECRET?.trim(),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let next = cfg;
|
|
190
|
+
let appKey: string | null = null;
|
|
191
|
+
let appSecret: string | null = null;
|
|
192
|
+
|
|
193
|
+
if (!resolved) {
|
|
194
|
+
await noteDingTalkCredentialHelp(prompter);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (canUseEnv) {
|
|
198
|
+
const keepEnv = await prompter.confirm({
|
|
199
|
+
message: "DINGTALK_APP_KEY + DINGTALK_APP_SECRET detected. Use env vars?",
|
|
200
|
+
initialValue: true,
|
|
201
|
+
});
|
|
202
|
+
if (keepEnv) {
|
|
203
|
+
next = {
|
|
204
|
+
...next,
|
|
205
|
+
channels: {
|
|
206
|
+
...next.channels,
|
|
207
|
+
dingtalk: { ...next.channels?.dingtalk, enabled: true },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
} else {
|
|
211
|
+
appKey = String(
|
|
212
|
+
await prompter.text({
|
|
213
|
+
message: "Enter DingTalk AppKey (ClientID)",
|
|
214
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
215
|
+
}),
|
|
216
|
+
).trim();
|
|
217
|
+
appSecret = String(
|
|
218
|
+
await prompter.text({
|
|
219
|
+
message: "Enter DingTalk AppSecret (ClientSecret)",
|
|
220
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
221
|
+
}),
|
|
222
|
+
).trim();
|
|
223
|
+
}
|
|
224
|
+
} else if (hasConfigCreds) {
|
|
225
|
+
const keep = await prompter.confirm({
|
|
226
|
+
message: "DingTalk credentials already configured. Keep them?",
|
|
227
|
+
initialValue: true,
|
|
228
|
+
});
|
|
229
|
+
if (!keep) {
|
|
230
|
+
appKey = String(
|
|
231
|
+
await prompter.text({
|
|
232
|
+
message: "Enter DingTalk AppKey (ClientID)",
|
|
233
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
234
|
+
}),
|
|
235
|
+
).trim();
|
|
236
|
+
appSecret = String(
|
|
237
|
+
await prompter.text({
|
|
238
|
+
message: "Enter DingTalk AppSecret (ClientSecret)",
|
|
239
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
240
|
+
}),
|
|
241
|
+
).trim();
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
appKey = String(
|
|
245
|
+
await prompter.text({
|
|
246
|
+
message: "Enter DingTalk AppKey (ClientID)",
|
|
247
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
248
|
+
}),
|
|
249
|
+
).trim();
|
|
250
|
+
appSecret = String(
|
|
251
|
+
await prompter.text({
|
|
252
|
+
message: "Enter DingTalk AppSecret (ClientSecret)",
|
|
253
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
254
|
+
}),
|
|
255
|
+
).trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (appKey && appSecret) {
|
|
259
|
+
next = {
|
|
260
|
+
...next,
|
|
261
|
+
channels: {
|
|
262
|
+
...next.channels,
|
|
263
|
+
dingtalk: {
|
|
264
|
+
...next.channels?.dingtalk,
|
|
265
|
+
enabled: true,
|
|
266
|
+
appKey,
|
|
267
|
+
appSecret,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Test connection
|
|
273
|
+
const testCfg = next.channels?.dingtalk as DingTalkConfig;
|
|
274
|
+
try {
|
|
275
|
+
const probe = await probeDingTalk(testCfg);
|
|
276
|
+
if (probe.ok) {
|
|
277
|
+
await prompter.note(
|
|
278
|
+
`Connected (${probe.appKey ?? "bot"})`,
|
|
279
|
+
"DingTalk connection test",
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
await prompter.note(
|
|
283
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
284
|
+
"DingTalk connection test",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
await prompter.note(`Connection test failed: ${String(err)}`, "DingTalk connection test");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Robot code (optional)
|
|
293
|
+
const currentRobotCode = (next.channels?.dingtalk as DingTalkConfig | undefined)?.robotCode;
|
|
294
|
+
const robotCode = await prompter.text({
|
|
295
|
+
message: "Robot code (optional, for media download)",
|
|
296
|
+
placeholder: "dingxxxxxxxxx",
|
|
297
|
+
initialValue: currentRobotCode,
|
|
298
|
+
});
|
|
299
|
+
if (robotCode) {
|
|
300
|
+
next = {
|
|
301
|
+
...next,
|
|
302
|
+
channels: {
|
|
303
|
+
...next.channels,
|
|
304
|
+
dingtalk: {
|
|
305
|
+
...next.channels?.dingtalk,
|
|
306
|
+
robotCode: String(robotCode).trim(),
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Group policy
|
|
313
|
+
const groupPolicy = await prompter.select({
|
|
314
|
+
message: "Group chat policy",
|
|
315
|
+
options: [
|
|
316
|
+
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
317
|
+
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
318
|
+
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
319
|
+
],
|
|
320
|
+
initialValue:
|
|
321
|
+
(next.channels?.dingtalk as DingTalkConfig | undefined)?.groupPolicy ?? "allowlist",
|
|
322
|
+
});
|
|
323
|
+
if (groupPolicy) {
|
|
324
|
+
next = setDingTalkGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Group allowlist if needed
|
|
328
|
+
if (groupPolicy === "allowlist") {
|
|
329
|
+
const existing = (next.channels?.dingtalk as DingTalkConfig | undefined)?.groupAllowFrom ?? [];
|
|
330
|
+
const entry = await prompter.text({
|
|
331
|
+
message: "Group chat allowlist (conversationIds)",
|
|
332
|
+
placeholder: "cidXXXXX, cidYYYYY",
|
|
333
|
+
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
334
|
+
});
|
|
335
|
+
if (entry) {
|
|
336
|
+
const parts = parseAllowFromInput(String(entry));
|
|
337
|
+
if (parts.length > 0) {
|
|
338
|
+
next = setDingTalkGroupAllowFrom(next, parts);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
dmPolicy,
|
|
347
|
+
|
|
348
|
+
disable: (cfg) => ({
|
|
349
|
+
...cfg,
|
|
350
|
+
channels: {
|
|
351
|
+
...cfg.channels,
|
|
352
|
+
dingtalk: { ...cfg.channels?.dingtalk, enabled: false },
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
};
|
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getDingTalkRuntime } from "./runtime.js";
|
|
3
|
+
import { sendMessageDingTalk } from "./send.js";
|
|
4
|
+
import { sendMediaDingTalk } from "./media.js";
|
|
5
|
+
|
|
6
|
+
// Note: DingTalk outbound adapter has limited functionality
|
|
7
|
+
// because it requires sessionWebhook which is only available during message handling.
|
|
8
|
+
// This adapter is primarily for interface compatibility.
|
|
9
|
+
|
|
10
|
+
export const dingtalkOutbound: ChannelOutboundAdapter = {
|
|
11
|
+
deliveryMode: "direct",
|
|
12
|
+
chunker: (text, limit) => getDingTalkRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
13
|
+
chunkerMode: "markdown",
|
|
14
|
+
textChunkLimit: 4000,
|
|
15
|
+
sendText: async ({ cfg, to, text }) => {
|
|
16
|
+
// Note: DingTalk requires sessionWebhook, which must be provided in the 'to' field
|
|
17
|
+
// Format: sessionWebhook URL directly
|
|
18
|
+
const result = await sendMessageDingTalk({ cfg, sessionWebhook: to, text });
|
|
19
|
+
return { channel: "dingtalk", conversationId: result.conversationId, messageId: result.processQueryKey || "" };
|
|
20
|
+
},
|
|
21
|
+
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
|
22
|
+
// Send text first if provided
|
|
23
|
+
if (text?.trim()) {
|
|
24
|
+
await sendMessageDingTalk({ cfg, sessionWebhook: to, text });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Upload and send media if URL provided
|
|
28
|
+
if (mediaUrl) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await sendMediaDingTalk({ cfg, sessionWebhook: to, mediaUrl });
|
|
31
|
+
return { channel: "dingtalk", conversationId: result.conversationId, messageId: result.processQueryKey || "" };
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Log the error for debugging
|
|
34
|
+
console.error(`[dingtalk] sendMediaDingTalk failed:`, err);
|
|
35
|
+
// Fallback to URL link if upload fails
|
|
36
|
+
const fallbackText = `📎 ${mediaUrl}`;
|
|
37
|
+
const result = await sendMessageDingTalk({ cfg, sessionWebhook: to, text: fallbackText });
|
|
38
|
+
return { channel: "dingtalk", conversationId: result.conversationId, messageId: result.processQueryKey || "" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// No media URL, just return text result
|
|
43
|
+
const result = await sendMessageDingTalk({ cfg, sessionWebhook: to, text: text ?? "" });
|
|
44
|
+
return { channel: "dingtalk", conversationId: result.conversationId, messageId: result.processQueryKey || "" };
|
|
45
|
+
},
|
|
46
|
+
};
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { DingTalkConfig, DingTalkGroupConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type DingTalkAllowlistMatch = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
matchKey?: string;
|
|
7
|
+
matchSource?: "wildcard" | "id" | "name";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveDingTalkAllowlistMatch(params: {
|
|
11
|
+
allowFrom: Array<string | number>;
|
|
12
|
+
senderId: string;
|
|
13
|
+
senderName?: string | null;
|
|
14
|
+
}): DingTalkAllowlistMatch {
|
|
15
|
+
const allowFrom = params.allowFrom
|
|
16
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
|
|
19
|
+
if (allowFrom.length === 0) return { allowed: false };
|
|
20
|
+
if (allowFrom.includes("*")) {
|
|
21
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const senderId = params.senderId.toLowerCase();
|
|
25
|
+
if (allowFrom.includes(senderId)) {
|
|
26
|
+
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const senderName = params.senderName?.toLowerCase();
|
|
30
|
+
if (senderName && allowFrom.includes(senderName)) {
|
|
31
|
+
return { allowed: true, matchKey: senderName, matchSource: "name" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { allowed: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveDingTalkGroupConfig(params: {
|
|
38
|
+
cfg?: DingTalkConfig;
|
|
39
|
+
groupId?: string | null;
|
|
40
|
+
}): DingTalkGroupConfig | undefined {
|
|
41
|
+
const groups = params.cfg?.groups ?? {};
|
|
42
|
+
const groupId = params.groupId?.trim();
|
|
43
|
+
if (!groupId) return undefined;
|
|
44
|
+
|
|
45
|
+
const direct = groups[groupId] as DingTalkGroupConfig | undefined;
|
|
46
|
+
if (direct) return direct;
|
|
47
|
+
|
|
48
|
+
const lowered = groupId.toLowerCase();
|
|
49
|
+
const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
|
|
50
|
+
return matchKey ? (groups[matchKey] as DingTalkGroupConfig | undefined) : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveDingTalkGroupToolPolicy(
|
|
54
|
+
params: ChannelGroupContext,
|
|
55
|
+
): GroupToolPolicyConfig | undefined {
|
|
56
|
+
const cfg = params.cfg.channels?.dingtalk as DingTalkConfig | undefined;
|
|
57
|
+
if (!cfg) return undefined;
|
|
58
|
+
|
|
59
|
+
const groupConfig = resolveDingTalkGroupConfig({
|
|
60
|
+
cfg,
|
|
61
|
+
groupId: params.groupId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return groupConfig?.tools;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isDingTalkGroupAllowed(params: {
|
|
68
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
69
|
+
allowFrom: Array<string | number>;
|
|
70
|
+
senderId: string;
|
|
71
|
+
senderName?: string | null;
|
|
72
|
+
}): boolean {
|
|
73
|
+
const { groupPolicy } = params;
|
|
74
|
+
if (groupPolicy === "disabled") return false;
|
|
75
|
+
if (groupPolicy === "open") return true;
|
|
76
|
+
return resolveDingTalkAllowlistMatch(params).allowed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveDingTalkReplyPolicy(params: {
|
|
80
|
+
isDirectMessage: boolean;
|
|
81
|
+
globalConfig?: DingTalkConfig;
|
|
82
|
+
groupConfig?: DingTalkGroupConfig;
|
|
83
|
+
}): { requireMention: boolean } {
|
|
84
|
+
if (params.isDirectMessage) {
|
|
85
|
+
return { requireMention: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const requireMention =
|
|
89
|
+
params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
|
|
90
|
+
|
|
91
|
+
return { requireMention };
|
|
92
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DingTalkConfig, DingTalkProbeResult } from "./types.js";
|
|
2
|
+
import { createDingTalkClient } from "./client.js";
|
|
3
|
+
import { resolveDingTalkCredentials } from "./accounts.js";
|
|
4
|
+
|
|
5
|
+
export async function probeDingTalk(cfg?: DingTalkConfig): Promise<DingTalkProbeResult> {
|
|
6
|
+
const creds = resolveDingTalkCredentials(cfg);
|
|
7
|
+
if (!creds) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
error: "missing credentials (appKey, appSecret)",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const client = createDingTalkClient(cfg!);
|
|
16
|
+
|
|
17
|
+
// Try to get access token as a connectivity test
|
|
18
|
+
const accessToken = await client.getAccessToken();
|
|
19
|
+
|
|
20
|
+
if (!accessToken) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
appKey: creds.appKey,
|
|
24
|
+
error: "Failed to get access token",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
appKey: creds.appKey,
|
|
31
|
+
robotCode: creds.robotCode,
|
|
32
|
+
connected: true,
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
appKey: creds.appKey,
|
|
38
|
+
error: err instanceof Error ? err.message : String(err),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|