@soimy/dingtalk 3.5.3 → 3.6.1
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/README.md +4 -1
- package/index.ts +7 -0
- package/openclaw.plugin.json +153 -5
- package/package.json +1 -1
- package/src/auth.ts +5 -2
- package/src/card/card-markdown-image-reroute.ts +106 -0
- package/src/card/card-run-registry.ts +54 -1
- package/src/card/card-stop-handler.ts +10 -20
- package/src/card/card-template.ts +14 -3
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +245 -52
- package/src/card-service.ts +408 -23
- package/src/channel.ts +24 -1083
- package/src/config-schema.ts +21 -1
- package/src/config.ts +139 -66
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +637 -0
- package/src/inbound-handler.ts +1276 -975
- package/src/media-utils.ts +6 -0
- package/src/message-context-store.ts +183 -85
- package/src/message-utils.ts +124 -16
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +174 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/onboarding.ts +333 -235
- package/src/path-utils.ts +49 -0
- package/src/platform/channel-status.ts +81 -0
- package/src/reply-strategy-card.ts +373 -64
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -72
- package/src/run-usage-store.ts +59 -0
- package/src/secret-input.ts +216 -0
- package/src/send-service.ts +115 -3
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/targeting/agent-routing.ts +30 -5
- package/src/types.ts +48 -157
package/src/onboarding.ts
CHANGED
|
@@ -6,21 +6,23 @@ import type {
|
|
|
6
6
|
WizardPrompter,
|
|
7
7
|
} from "openclaw/plugin-sdk/setup";
|
|
8
8
|
import { DEFAULT_ACCOUNT_ID, formatDocsLink, normalizeAccountId } from "openclaw/plugin-sdk/setup";
|
|
9
|
-
import {
|
|
9
|
+
import { listDingTalkAccountIds, resolveDingTalkAccount } from "./config.js";
|
|
10
|
+
import {
|
|
11
|
+
beginDeviceRegistration,
|
|
12
|
+
openUrlInBrowser,
|
|
13
|
+
RegistrationError,
|
|
14
|
+
} from "./device-registration.js";
|
|
15
|
+
import {
|
|
16
|
+
hasConfiguredSecretInput,
|
|
17
|
+
normalizeSecretInputString,
|
|
18
|
+
parseSecretInputString,
|
|
19
|
+
} from "./secret-input.js";
|
|
10
20
|
import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
|
|
11
|
-
import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
|
|
12
21
|
|
|
13
22
|
const channel = "dingtalk" as const;
|
|
14
23
|
|
|
15
24
|
function isConfigured(account: DingTalkConfig): boolean {
|
|
16
|
-
return Boolean(account.clientId && account.clientSecret);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseList(value: string): string[] {
|
|
20
|
-
return value
|
|
21
|
-
.split(/[\n,;]+/g)
|
|
22
|
-
.map((entry) => entry.trim())
|
|
23
|
-
.filter(Boolean);
|
|
25
|
+
return Boolean(account.clientId && hasConfiguredSecretInput(account.clientSecret));
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function applyAccountNameToChannelSection(params: {
|
|
@@ -52,33 +54,62 @@ async function promptDingTalkAccountId(options: {
|
|
|
52
54
|
defaultAccountId: string;
|
|
53
55
|
}): Promise<string> {
|
|
54
56
|
const existingIds = options.listAccountIds(options.cfg);
|
|
55
|
-
|
|
57
|
+
const hasDefault = existingIds.includes(options.defaultAccountId);
|
|
58
|
+
const namedIds = existingIds.filter((id) => id !== options.defaultAccountId);
|
|
59
|
+
const action = await options.prompter.select({
|
|
60
|
+
message: `Choose ${options.label} account`,
|
|
61
|
+
options: [
|
|
62
|
+
{
|
|
63
|
+
label: hasDefault ? "Configure default account" : "Add default account",
|
|
64
|
+
value: "default",
|
|
65
|
+
},
|
|
66
|
+
...(namedIds.length > 0
|
|
67
|
+
? [{ label: "Modify existing named account", value: "existing" }]
|
|
68
|
+
: []),
|
|
69
|
+
{ label: "Add named account", value: "new" },
|
|
70
|
+
],
|
|
71
|
+
initialValue: hasDefault ? "default" : namedIds.length > 0 ? "existing" : "default",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (action === "default") {
|
|
56
75
|
return options.defaultAccountId;
|
|
57
76
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
initialValue: true,
|
|
61
|
-
});
|
|
62
|
-
if (useExisting) {
|
|
63
|
-
if (existingIds.includes(options.currentId)) {
|
|
64
|
-
return options.currentId;
|
|
65
|
-
}
|
|
77
|
+
|
|
78
|
+
if (action === "existing") {
|
|
66
79
|
const selected = await options.prompter.select({
|
|
67
80
|
message: `Select existing ${options.label} account`,
|
|
68
|
-
options:
|
|
81
|
+
options: namedIds.map((accountId) => ({
|
|
69
82
|
label: accountId,
|
|
70
83
|
value: accountId,
|
|
71
84
|
})),
|
|
72
|
-
initialValue:
|
|
85
|
+
initialValue: namedIds[0],
|
|
73
86
|
});
|
|
74
87
|
return normalizeAccountId(String(selected));
|
|
75
88
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const raw = await options.prompter.text({
|
|
92
|
+
message: `New ${options.label} account ID`,
|
|
93
|
+
placeholder: "work",
|
|
94
|
+
initialValue: "",
|
|
95
|
+
});
|
|
96
|
+
const normalized = normalizeAccountId(String(raw));
|
|
97
|
+
if (!normalized || normalized === options.defaultAccountId) {
|
|
98
|
+
await options.prompter.note(
|
|
99
|
+
"Enter a non-default account ID, for example: work",
|
|
100
|
+
"DingTalk account",
|
|
101
|
+
);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (existingIds.includes(normalized)) {
|
|
105
|
+
await options.prompter.note(
|
|
106
|
+
`Account "${normalized}" already exists. Choose Modify existing named account to edit it.`,
|
|
107
|
+
"DingTalk account",
|
|
108
|
+
);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
|
|
@@ -96,6 +127,141 @@ async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
|
|
|
96
127
|
);
|
|
97
128
|
}
|
|
98
129
|
|
|
130
|
+
async function noteDmAllowlistGuidance(prompter: WizardPrompter): Promise<void> {
|
|
131
|
+
await prompter.note(
|
|
132
|
+
[
|
|
133
|
+
"DM allowlist requires DingTalk userId values.",
|
|
134
|
+
"Ask each target user to send a direct message to this bot.",
|
|
135
|
+
"The plugin will show the observed userId so an admin can add it to channels.dingtalk.allowFrom.",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
"DingTalk DM allowlist",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function noteGroupAllowlistGuidance(prompter: WizardPrompter): Promise<void> {
|
|
142
|
+
await prompter.note(
|
|
143
|
+
[
|
|
144
|
+
"Group allowlist requires DingTalk conversationId values.",
|
|
145
|
+
"Ask a member to @mention this bot in the target group.",
|
|
146
|
+
"The plugin will show the observed group ID so an admin can configure channels.dingtalk.groups or related allowlist settings.",
|
|
147
|
+
].join("\n"),
|
|
148
|
+
"DingTalk group allowlist",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function noteDingTalkSetupComplete(prompter: WizardPrompter): Promise<void> {
|
|
153
|
+
await prompter.note(
|
|
154
|
+
[
|
|
155
|
+
"DingTalk configuration has been saved.",
|
|
156
|
+
"For named accounts, configuration lives under channels.dingtalk.accounts.",
|
|
157
|
+
"If you selected allowlist policies, ask the target user or group to message this bot first; the plugin will show IDs that an admin can add manually.",
|
|
158
|
+
"Advanced runtime settings can be edited in the config UI or openclaw.json.",
|
|
159
|
+
"Restart the gateway to apply changes:",
|
|
160
|
+
" openclaw gateway restart",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
"DingTalk setup complete",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateMinInteger(min: number) {
|
|
167
|
+
return (value: string): string | undefined => {
|
|
168
|
+
const raw = String(value ?? "").trim();
|
|
169
|
+
const num = Number(raw);
|
|
170
|
+
if (!raw) {
|
|
171
|
+
return "Required";
|
|
172
|
+
}
|
|
173
|
+
if (!Number.isInteger(num) || num < min) {
|
|
174
|
+
return `Must be an integer >= ${min}`;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
type CardAdvancedConfig = Partial<
|
|
181
|
+
Pick<
|
|
182
|
+
DingTalkConfig,
|
|
183
|
+
"cardStreamingMode" | "cardStreamInterval" | "cardAtSender" | "cardStatusLine"
|
|
184
|
+
>
|
|
185
|
+
>;
|
|
186
|
+
|
|
187
|
+
async function promptCardAdvancedConfig(params: {
|
|
188
|
+
resolved: DingTalkConfig;
|
|
189
|
+
prompter: WizardPrompter;
|
|
190
|
+
}): Promise<CardAdvancedConfig> {
|
|
191
|
+
const { resolved, prompter } = params;
|
|
192
|
+
const cardStreamingMode = (await prompter.select({
|
|
193
|
+
message: "Card streaming mode",
|
|
194
|
+
options: [
|
|
195
|
+
{ label: "Off - answer does not stream incrementally", value: "off" },
|
|
196
|
+
{ label: "Answer - only answer streams incrementally", value: "answer" },
|
|
197
|
+
{ label: "All - answer and thinking stream incrementally", value: "all" },
|
|
198
|
+
],
|
|
199
|
+
initialValue: resolved.cardStreamingMode ?? (resolved.cardRealTimeStream ? "all" : "off"),
|
|
200
|
+
})) as DingTalkConfig["cardStreamingMode"];
|
|
201
|
+
|
|
202
|
+
const cardStreamInterval = Number(
|
|
203
|
+
String(
|
|
204
|
+
await prompter.text({
|
|
205
|
+
message: "Card stream interval (ms)",
|
|
206
|
+
placeholder: "1000",
|
|
207
|
+
initialValue: String(resolved.cardStreamInterval ?? 1000),
|
|
208
|
+
validate: validateMinInteger(200),
|
|
209
|
+
}),
|
|
210
|
+
).trim(),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const cardAtSenderRaw = String(
|
|
214
|
+
await prompter.text({
|
|
215
|
+
message: "Card completion @mention text (optional)",
|
|
216
|
+
placeholder: "Reply complete",
|
|
217
|
+
initialValue: resolved.cardAtSender || undefined,
|
|
218
|
+
}),
|
|
219
|
+
).trim();
|
|
220
|
+
|
|
221
|
+
const wantsStatusLine = await prompter.confirm({
|
|
222
|
+
message: "Customize AI card status line?",
|
|
223
|
+
initialValue: Boolean(resolved.cardStatusLine),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let cardStatusLine: DingTalkConfig["cardStatusLine"] | undefined;
|
|
227
|
+
if (wantsStatusLine) {
|
|
228
|
+
const current = resolved.cardStatusLine ?? {};
|
|
229
|
+
cardStatusLine = {
|
|
230
|
+
model: await prompter.confirm({
|
|
231
|
+
message: "Show model name?",
|
|
232
|
+
initialValue: current.model ?? true,
|
|
233
|
+
}),
|
|
234
|
+
effort: await prompter.confirm({
|
|
235
|
+
message: "Show thinking effort?",
|
|
236
|
+
initialValue: current.effort ?? true,
|
|
237
|
+
}),
|
|
238
|
+
agent: await prompter.confirm({
|
|
239
|
+
message: "Show agent name?",
|
|
240
|
+
initialValue: current.agent ?? true,
|
|
241
|
+
}),
|
|
242
|
+
taskTime: await prompter.confirm({
|
|
243
|
+
message: "Show task elapsed time?",
|
|
244
|
+
initialValue: current.taskTime ?? false,
|
|
245
|
+
}),
|
|
246
|
+
tokens: await prompter.confirm({
|
|
247
|
+
message: "Show token usage?",
|
|
248
|
+
initialValue: current.tokens ?? false,
|
|
249
|
+
}),
|
|
250
|
+
dapiUsage: await prompter.confirm({
|
|
251
|
+
message: "Show DingTalk API usage?",
|
|
252
|
+
initialValue: current.dapiUsage ?? false,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
cardStreamingMode,
|
|
259
|
+
cardStreamInterval,
|
|
260
|
+
...(cardAtSenderRaw ? { cardAtSender: cardAtSenderRaw } : {}),
|
|
261
|
+
...(cardStatusLine ? { cardStatusLine } : {}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
99
265
|
function applyAccountConfig(params: {
|
|
100
266
|
cfg: OpenClawConfig;
|
|
101
267
|
accountId: string;
|
|
@@ -128,6 +294,11 @@ function applyAccountConfig(params: {
|
|
|
128
294
|
: {}),
|
|
129
295
|
...(input.messageType ? { messageType: input.messageType } : {}),
|
|
130
296
|
...(input.cardStreamingMode ? { cardStreamingMode: input.cardStreamingMode } : {}),
|
|
297
|
+
...(typeof input.cardStreamInterval === "number"
|
|
298
|
+
? { cardStreamInterval: input.cardStreamInterval }
|
|
299
|
+
: {}),
|
|
300
|
+
...(input.cardAtSender ? { cardAtSender: input.cardAtSender } : {}),
|
|
301
|
+
...(input.cardStatusLine ? { cardStatusLine: input.cardStatusLine } : {}),
|
|
131
302
|
...(typeof input.maxReconnectCycles === "number"
|
|
132
303
|
? { maxReconnectCycles: input.maxReconnectCycles }
|
|
133
304
|
: {}),
|
|
@@ -188,23 +359,13 @@ function applyGenericSetupInput(params: {
|
|
|
188
359
|
name: params.input.name,
|
|
189
360
|
clientId: typeof params.input.token === "string" ? params.input.token.trim() : undefined,
|
|
190
361
|
clientSecret:
|
|
191
|
-
typeof params.input.password === "string"
|
|
362
|
+
typeof params.input.password === "string"
|
|
363
|
+
? parseSecretInputString(params.input.password)
|
|
364
|
+
: undefined,
|
|
192
365
|
},
|
|
193
366
|
});
|
|
194
367
|
}
|
|
195
368
|
|
|
196
|
-
function validatePositiveInteger(value: string): string | undefined {
|
|
197
|
-
const raw = String(value ?? "").trim();
|
|
198
|
-
const num = Number(raw);
|
|
199
|
-
if (!raw) {
|
|
200
|
-
return "Required";
|
|
201
|
-
}
|
|
202
|
-
if (!Number.isInteger(num) || num < 1) {
|
|
203
|
-
return "Must be an integer >= 1";
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
369
|
async function configureDingTalkAccount(params: {
|
|
209
370
|
cfg: OpenClawConfig;
|
|
210
371
|
accountId: string;
|
|
@@ -213,237 +374,173 @@ async function configureDingTalkAccount(params: {
|
|
|
213
374
|
const { cfg, accountId, prompter } = params;
|
|
214
375
|
const resolved = resolveDingTalkAccount(cfg, accountId);
|
|
215
376
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
placeholder: "xxx-xxx-xxx-xxx",
|
|
228
|
-
initialValue: resolved.clientSecret ?? undefined,
|
|
229
|
-
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const wantsCardMode = await prompter.confirm({
|
|
233
|
-
message: "Enable AI interactive card mode? (for streaming AI responses)",
|
|
234
|
-
initialValue: resolved.messageType === "card",
|
|
377
|
+
// ── Credential acquisition: auto-register or manual ────────────────────
|
|
378
|
+
const hasExistingCredentials = Boolean(
|
|
379
|
+
resolved.clientId && hasConfiguredSecretInput(resolved.clientSecret),
|
|
380
|
+
);
|
|
381
|
+
const credentialMethod = await prompter.select({
|
|
382
|
+
message: "How do you want to get DingTalk bot credentials?",
|
|
383
|
+
options: [
|
|
384
|
+
{ label: "Auto-register an OpenClaw DingTalk bot", value: "auto" },
|
|
385
|
+
{ label: "Enter an existing DingTalk bot Client ID / Client Secret", value: "manual" },
|
|
386
|
+
],
|
|
387
|
+
initialValue: hasExistingCredentials ? "manual" : "auto",
|
|
235
388
|
});
|
|
236
389
|
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
390
|
+
let clientId: string;
|
|
391
|
+
let clientSecret: string;
|
|
392
|
+
|
|
393
|
+
if (credentialMethod === "auto") {
|
|
394
|
+
try {
|
|
395
|
+
const session = await beginDeviceRegistration();
|
|
396
|
+
|
|
397
|
+
openUrlInBrowser(session.verificationUrl);
|
|
398
|
+
|
|
399
|
+
await prompter.note(
|
|
400
|
+
[
|
|
401
|
+
"Opened the authorization page in your browser.",
|
|
402
|
+
"Scan the authorization code in DingTalk to finish registration.",
|
|
403
|
+
"",
|
|
404
|
+
"If the browser did not open automatically, visit this link manually:",
|
|
405
|
+
session.verificationUrl,
|
|
406
|
+
].join("\n"),
|
|
407
|
+
"DingTalk bot auto-registration",
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
let lastWaitingNote = 0;
|
|
411
|
+
const result = await session.waitForResult({
|
|
412
|
+
onWaiting: () => {
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
if (now - lastWaitingNote >= 15_000) {
|
|
415
|
+
lastWaitingNote = now;
|
|
416
|
+
prompter
|
|
417
|
+
.note("Waiting for authorization. Please finish the scan in DingTalk...", "Polling")
|
|
418
|
+
.catch(() => {});
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
clientId = result.clientId;
|
|
423
|
+
clientSecret = result.clientSecret;
|
|
424
|
+
|
|
425
|
+
await prompter.note(
|
|
426
|
+
[
|
|
427
|
+
"Registration succeeded!",
|
|
428
|
+
`Client ID: ${clientId}`,
|
|
429
|
+
"Client Secret: [captured; see config file]",
|
|
430
|
+
].join("\n"),
|
|
431
|
+
"Registration complete",
|
|
432
|
+
);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const message = err instanceof RegistrationError ? err.message : String(err);
|
|
435
|
+
await prompter.note(
|
|
436
|
+
[`Auto-registration failed: ${message}`, "", "Falling back to manual input."].join("\n"),
|
|
437
|
+
"Registration failed",
|
|
438
|
+
);
|
|
439
|
+
// Fall through to manual path
|
|
440
|
+
await noteDingTalkHelp(prompter);
|
|
441
|
+
clientId = String(
|
|
442
|
+
await prompter.text({
|
|
443
|
+
message: "Client ID (AppKey)",
|
|
444
|
+
placeholder: "dingxxxxxxxx",
|
|
445
|
+
initialValue: resolved.clientId ?? undefined,
|
|
446
|
+
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
447
|
+
}),
|
|
448
|
+
).trim();
|
|
449
|
+
clientSecret = String(
|
|
450
|
+
await prompter.text({
|
|
451
|
+
message: "Client Secret (AppSecret)",
|
|
452
|
+
placeholder: "xxx-xxx-xxx-xxx",
|
|
453
|
+
initialValue: normalizeSecretInputString(resolved.clientSecret),
|
|
454
|
+
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
455
|
+
}),
|
|
456
|
+
).trim();
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
// Manual path — existing behavior
|
|
460
|
+
await noteDingTalkHelp(prompter);
|
|
461
|
+
clientId = String(
|
|
462
|
+
await prompter.text({
|
|
463
|
+
message: "Client ID (AppKey)",
|
|
464
|
+
placeholder: "dingxxxxxxxx",
|
|
465
|
+
initialValue: resolved.clientId ?? undefined,
|
|
466
|
+
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
467
|
+
}),
|
|
468
|
+
).trim();
|
|
469
|
+
clientSecret = String(
|
|
470
|
+
await prompter.text({
|
|
471
|
+
message: "Client Secret (AppSecret)",
|
|
472
|
+
placeholder: "xxx-xxx-xxx-xxx",
|
|
473
|
+
initialValue: normalizeSecretInputString(resolved.clientSecret),
|
|
474
|
+
validate: (value: string) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
475
|
+
}),
|
|
476
|
+
).trim();
|
|
259
477
|
}
|
|
260
478
|
|
|
261
|
-
const dmPolicyValue = await prompter.select({
|
|
479
|
+
const dmPolicyValue = (await prompter.select({
|
|
262
480
|
message: "Direct message policy",
|
|
263
481
|
options: [
|
|
264
482
|
{ label: "Open - anyone can DM", value: "open" },
|
|
265
|
-
{ label: "
|
|
483
|
+
{ label: "Pairing - require OpenClaw pairing approval", value: "pairing" },
|
|
484
|
+
{ label: "Allowlist - only manually allowed users", value: "allowlist" },
|
|
266
485
|
],
|
|
267
486
|
initialValue: resolved.dmPolicy ?? "open",
|
|
268
|
-
});
|
|
487
|
+
})) as "open" | "pairing" | "allowlist";
|
|
269
488
|
|
|
270
|
-
let allowFrom: string[] | undefined;
|
|
271
489
|
if (dmPolicyValue === "allowlist") {
|
|
272
|
-
|
|
273
|
-
message: "Allowed user IDs (comma-separated)",
|
|
274
|
-
placeholder: "user1, user2",
|
|
275
|
-
});
|
|
276
|
-
const parsed = parseList(String(entry ?? ""));
|
|
277
|
-
allowFrom = parsed.length > 0 ? parsed : undefined;
|
|
490
|
+
await noteDmAllowlistGuidance(prompter);
|
|
278
491
|
}
|
|
279
492
|
|
|
280
|
-
const
|
|
281
|
-
message: "Media URL allowlist (comma-separated host/IP/CIDR, optional)",
|
|
282
|
-
placeholder: "cdn.example.com, 192.168.1.23, 10.0.0.0/8",
|
|
283
|
-
initialValue: (resolved.mediaUrlAllowlist || []).join(", ") || undefined,
|
|
284
|
-
});
|
|
285
|
-
const mediaUrlAllowlistParsed = parseList(String(mediaUrlAllowlistEntry ?? ""));
|
|
286
|
-
const mediaUrlAllowlist = mediaUrlAllowlistParsed.length > 0 ? mediaUrlAllowlistParsed : undefined;
|
|
287
|
-
|
|
288
|
-
const groupPolicyValue = await prompter.select({
|
|
493
|
+
const groupPolicyValue = (await prompter.select({
|
|
289
494
|
message: "Group message policy",
|
|
290
495
|
options: [
|
|
291
496
|
{ label: "Open - any group can use bot", value: "open" },
|
|
292
|
-
{ label: "Allowlist - only
|
|
497
|
+
{ label: "Allowlist - only manually configured groups", value: "allowlist" },
|
|
293
498
|
{ label: "Disabled - block all group messages", value: "disabled" },
|
|
294
499
|
],
|
|
295
500
|
initialValue: resolved.groupPolicy ?? "open",
|
|
296
|
-
});
|
|
501
|
+
})) as "open" | "allowlist" | "disabled";
|
|
297
502
|
|
|
298
503
|
if (groupPolicyValue === "allowlist") {
|
|
299
|
-
await prompter
|
|
300
|
-
[
|
|
301
|
-
'groupPolicy=allowlist requires "groups" config to specify allowed group IDs.',
|
|
302
|
-
"After setup, manually add group conversationIds to your config:",
|
|
303
|
-
"",
|
|
304
|
-
' "groups": { "cidXXX": {}, "cidYYY": { "systemPrompt": "..." } }',
|
|
305
|
-
"",
|
|
306
|
-
'Groups not listed will be blocked. Use "*" as key to allow all groups.',
|
|
307
|
-
].join("\n"),
|
|
308
|
-
);
|
|
504
|
+
await noteGroupAllowlistGuidance(prompter);
|
|
309
505
|
}
|
|
310
506
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const groupAllowFromEntry = await prompter.text({
|
|
314
|
-
message: "Group sender allowlist - user IDs allowed in groups (comma-separated, optional)",
|
|
315
|
-
placeholder: "user1, user2",
|
|
316
|
-
initialValue: (resolved.groupAllowFrom || []).join(", ") || undefined,
|
|
317
|
-
});
|
|
318
|
-
const parsedGroupAllowFrom = parseList(String(groupAllowFromEntry ?? ""));
|
|
319
|
-
groupAllowFrom = parsedGroupAllowFrom.length > 0 ? parsedGroupAllowFrom : undefined;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
await prompter.note(
|
|
323
|
-
[
|
|
324
|
-
"Enabling learned displayName target resolution has tradeoffs:",
|
|
325
|
-
"- learned names come from observed inbound messages and can become stale",
|
|
326
|
-
"- duplicate display names can resolve to the wrong group or user",
|
|
327
|
-
'- current upstream target resolution does not provide requester authz, so "all" applies to every caller that can reach the send flow',
|
|
328
|
-
"Use explicit IDs for sensitive or high-risk deliveries.",
|
|
329
|
-
].join("\n"),
|
|
330
|
-
"displayName resolution risk",
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
const displayNameResolutionValue = await prompter.select({
|
|
334
|
-
message: "Learned displayName target resolution",
|
|
507
|
+
const messageType = (await prompter.select({
|
|
508
|
+
message: "Reply message type",
|
|
335
509
|
options: [
|
|
336
|
-
{
|
|
337
|
-
|
|
338
|
-
value: "disabled",
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
label: "All - learned lookup for all callers (higher risk)",
|
|
342
|
-
value: "all",
|
|
343
|
-
},
|
|
510
|
+
{ label: "Markdown - standard DingTalk messages", value: "markdown" },
|
|
511
|
+
{ label: "AI Card - interactive card replies", value: "card" },
|
|
344
512
|
],
|
|
345
|
-
initialValue: resolved.
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
await prompter.note(
|
|
349
|
-
[
|
|
350
|
-
"Advanced host context visibility is available as channels.dingtalk.contextVisibility.",
|
|
351
|
-
"Recommended advanced mode: allowlist_quote.",
|
|
352
|
-
"Modes:",
|
|
353
|
-
"- all: preserve the current host supplemental-context behavior",
|
|
354
|
-
"- allowlist: keep only host allowlisted supplemental context",
|
|
355
|
-
"- allowlist_quote: keep explicit quote/reply context while filtering extra context",
|
|
356
|
-
"This is separate from displayNameResolution and should be set manually if needed.",
|
|
357
|
-
resolved.contextVisibility
|
|
358
|
-
? `Current resolved value: ${resolved.contextVisibility}`
|
|
359
|
-
: "Current resolved value: host default",
|
|
360
|
-
].join("\n"),
|
|
361
|
-
"Advanced context visibility",
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
let maxReconnectCycles: number | undefined;
|
|
365
|
-
const wantsReconnectLimits = await prompter.confirm({
|
|
366
|
-
message: "Configure runtime reconnect cycle limit? (recommended)",
|
|
367
|
-
initialValue: typeof resolved.maxReconnectCycles === "number",
|
|
368
|
-
});
|
|
369
|
-
if (wantsReconnectLimits) {
|
|
370
|
-
const parsedCycles = Number(
|
|
371
|
-
String(
|
|
372
|
-
await prompter.text({
|
|
373
|
-
message: "Max runtime reconnect cycles",
|
|
374
|
-
placeholder: "10",
|
|
375
|
-
initialValue: String(resolved.maxReconnectCycles ?? 10),
|
|
376
|
-
validate: (value: string) => validatePositiveInteger(value),
|
|
377
|
-
}),
|
|
378
|
-
).trim(),
|
|
379
|
-
);
|
|
380
|
-
maxReconnectCycles = Number.isInteger(parsedCycles) && parsedCycles > 0 ? parsedCycles : 10;
|
|
381
|
-
}
|
|
513
|
+
initialValue: resolved.messageType ?? "markdown",
|
|
514
|
+
})) as "markdown" | "card";
|
|
382
515
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
initialValue: typeof resolved.mediaMaxMb === "number",
|
|
516
|
+
const wantsAdvanced = await prompter.confirm({
|
|
517
|
+
message: "Configure advanced DingTalk options?",
|
|
518
|
+
initialValue: false,
|
|
387
519
|
});
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
typeof resolved.mediaMaxMb === "number" ? String(resolved.mediaMaxMb) : "20",
|
|
396
|
-
validate: (value: string) => validatePositiveInteger(value),
|
|
397
|
-
}),
|
|
398
|
-
).trim(),
|
|
399
|
-
);
|
|
400
|
-
mediaMaxMb = Number.isInteger(parsedMediaMax) && parsedMediaMax > 0 ? parsedMediaMax : 20;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
let journalTTLDays: number | undefined;
|
|
404
|
-
const wantsJournalTTL = await prompter.confirm({
|
|
405
|
-
message: "Configure quote journal retention in days?",
|
|
406
|
-
initialValue: typeof resolved.journalTTLDays === "number",
|
|
407
|
-
});
|
|
408
|
-
if (wantsJournalTTL) {
|
|
409
|
-
const parsedJournalTTL = Number(
|
|
410
|
-
String(
|
|
411
|
-
await prompter.text({
|
|
412
|
-
message: "Quote journal retention days",
|
|
413
|
-
placeholder: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
414
|
-
initialValue:
|
|
415
|
-
typeof resolved.journalTTLDays === "number"
|
|
416
|
-
? String(resolved.journalTTLDays)
|
|
417
|
-
: String(DEFAULT_MESSAGE_CONTEXT_TTL_DAYS),
|
|
418
|
-
validate: (value: string) => validatePositiveInteger(value),
|
|
419
|
-
}),
|
|
420
|
-
).trim(),
|
|
520
|
+
let cardAdvanced: CardAdvancedConfig = {};
|
|
521
|
+
if (wantsAdvanced && messageType === "card") {
|
|
522
|
+
cardAdvanced = await promptCardAdvancedConfig({ resolved, prompter });
|
|
523
|
+
} else if (wantsAdvanced) {
|
|
524
|
+
await prompter.note(
|
|
525
|
+
"No markdown-specific advanced onboarding options are required. Other advanced settings can be edited in the config UI or openclaw.json.",
|
|
526
|
+
"DingTalk advanced options",
|
|
421
527
|
);
|
|
422
|
-
journalTTLDays =
|
|
423
|
-
Number.isInteger(parsedJournalTTL) && parsedJournalTTL > 0
|
|
424
|
-
? parsedJournalTTL
|
|
425
|
-
: DEFAULT_MESSAGE_CONTEXT_TTL_DAYS;
|
|
426
528
|
}
|
|
427
529
|
|
|
428
|
-
|
|
530
|
+
const nextCfg = applyAccountConfig({
|
|
429
531
|
cfg,
|
|
430
532
|
accountId,
|
|
431
533
|
input: {
|
|
432
534
|
clientId: String(clientId).trim(),
|
|
433
|
-
clientSecret:
|
|
434
|
-
dmPolicy: dmPolicyValue
|
|
435
|
-
groupPolicy: groupPolicyValue
|
|
436
|
-
allowFrom,
|
|
437
|
-
groupAllowFrom,
|
|
438
|
-
displayNameResolution: displayNameResolutionValue as "disabled" | "all",
|
|
439
|
-
mediaUrlAllowlist,
|
|
535
|
+
clientSecret: parseSecretInputString(clientSecret),
|
|
536
|
+
dmPolicy: dmPolicyValue,
|
|
537
|
+
groupPolicy: groupPolicyValue,
|
|
440
538
|
messageType,
|
|
441
|
-
|
|
442
|
-
maxReconnectCycles,
|
|
443
|
-
mediaMaxMb,
|
|
444
|
-
journalTTLDays,
|
|
539
|
+
...cardAdvanced,
|
|
445
540
|
},
|
|
446
541
|
});
|
|
542
|
+
await noteDingTalkSetupComplete(prompter);
|
|
543
|
+
return nextCfg;
|
|
447
544
|
}
|
|
448
545
|
|
|
449
546
|
export const dingtalkSetupAdapter: ChannelSetupAdapter = {
|
|
@@ -474,9 +571,10 @@ export const dingtalkSetupWizard: ChannelSetupWizard = {
|
|
|
474
571
|
`DingTalk: ${configured ? "configured" : "needs setup"}`,
|
|
475
572
|
],
|
|
476
573
|
resolveSelectionHint: ({ configured }) =>
|
|
477
|
-
configured ? "configured" : "
|
|
574
|
+
configured ? "configured" : "DingTalk enterprise bot",
|
|
478
575
|
resolveQuickstartScore: ({ configured }) => (configured ? 1 : 4),
|
|
479
576
|
},
|
|
577
|
+
resolveShouldPromptAccountIds: () => true,
|
|
480
578
|
resolveAccountIdForConfigure: async ({
|
|
481
579
|
cfg,
|
|
482
580
|
prompter,
|