@jeik/dingtalk-connector 0.8.21-fix1
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 +686 -0
- package/LICENSE +21 -0
- package/README.en.md +181 -0
- package/README.md +221 -0
- package/bin/dingtalk-connector.js +858 -0
- package/bin/wizard-config.mjs +110 -0
- package/dist/accounts-BAzdqkAV.mjs +268 -0
- package/dist/accounts-BQptOmgB.mjs +2 -0
- package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
- package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
- package/dist/common-C8pYKU_y.mjs +2 -0
- package/dist/common-Dt9n6fQN.mjs +101 -0
- package/dist/connection-DHHFFNQJ.mjs +423 -0
- package/dist/entry-bundled.d.mts +16 -0
- package/dist/entry-bundled.mjs +31 -0
- package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
- package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
- package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
- package/dist/http-client-CpnJHB89.mjs +2 -0
- package/dist/http-client-DFWZgO1n.mjs +33 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.mjs +45 -0
- package/dist/logger-BmJkQkm1.mjs +2 -0
- package/dist/logger-mZ9OSbmD.mjs +58 -0
- package/dist/media-C_SVin7s.mjs +2 -0
- package/dist/media-cz72EVS3.mjs +509 -0
- package/dist/message-handler-DESzFFDc.mjs +1971 -0
- package/dist/messaging-B6l1sRvX.mjs +1044 -0
- package/dist/runtime-DUgpo5zC.mjs +1422 -0
- package/dist/session-DJ4jYqPv.mjs +114 -0
- package/dist/utils-Bjh4r_qS.mjs +4 -0
- package/dist/utils-CIfI_3Jh.mjs +63 -0
- package/dist/utils-legacy-CALCPP1t.mjs +230 -0
- package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
- package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
- package/docs/DEAP_AGENT_GUIDE.md +115 -0
- package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
- package/docs/MULTI_AGENT_SETUP.md +306 -0
- package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
- package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
- package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
- package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
- package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
- package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
- package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
- package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
- package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
- package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
- package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
- package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
- package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
- package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
- package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
- package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
- package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
- package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
- package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
- package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
- package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
- package/docs/TROUBLESHOOTING.md +122 -0
- package/index.ts +77 -0
- package/openclaw.plugin.json +551 -0
- package/package.json +147 -0
- package/skills/dingtalk-channel-rules/SKILL.md +91 -0
- package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
- package/skills/dws-cli/SKILL.md +129 -0
- package/skills/dws-cli/references/error-codes.md +95 -0
- package/skills/dws-cli/references/field-rules.md +105 -0
- package/skills/dws-cli/references/global-reference.md +104 -0
- package/skills/dws-cli/references/intent-guide.md +114 -0
- package/skills/dws-cli/references/products/aitable.md +452 -0
- package/skills/dws-cli/references/products/attendance.md +93 -0
- package/skills/dws-cli/references/products/calendar.md +217 -0
- package/skills/dws-cli/references/products/chat.md +292 -0
- package/skills/dws-cli/references/products/contact.md +108 -0
- package/skills/dws-cli/references/products/ding.md +57 -0
- package/skills/dws-cli/references/products/report.md +162 -0
- package/skills/dws-cli/references/products/simple.md +128 -0
- package/skills/dws-cli/references/products/todo.md +138 -0
- package/skills/dws-cli/references/products/workbench.md +39 -0
- package/skills/dws-cli/references/recovery-guide.md +94 -0
- package/src/channel.ts +588 -0
- package/src/config/accounts.ts +242 -0
- package/src/config/schema.ts +180 -0
- package/src/core/connection.ts +741 -0
- package/src/core/message-handler.ts +1788 -0
- package/src/core/provider.ts +111 -0
- package/src/core/state.ts +54 -0
- package/src/device-auth-config.ts +14 -0
- package/src/device-auth.ts +197 -0
- package/src/directory.ts +95 -0
- package/src/docs.ts +293 -0
- package/src/game-xiyou/achievement-engine.ts +252 -0
- package/src/game-xiyou/bounty-system.ts +315 -0
- package/src/game-xiyou/commands.ts +223 -0
- package/src/game-xiyou/drop-engine.ts +241 -0
- package/src/game-xiyou/encounter-system.ts +135 -0
- package/src/game-xiyou/escape-engine.ts +164 -0
- package/src/game-xiyou/exp-calculator.ts +139 -0
- package/src/game-xiyou/index.ts +479 -0
- package/src/game-xiyou/level-system.ts +91 -0
- package/src/game-xiyou/monster-pool.ts +180 -0
- package/src/game-xiyou/pity-counter.ts +114 -0
- package/src/game-xiyou/random-event-engine.ts +648 -0
- package/src/game-xiyou/renderer.ts +679 -0
- package/src/game-xiyou/storage.ts +218 -0
- package/src/game-xiyou/treasure-system.ts +105 -0
- package/src/game-xiyou/types.ts +582 -0
- package/src/game-xiyou/uid-resolver.ts +49 -0
- package/src/gateway-methods.ts +740 -0
- package/src/onboarding.ts +553 -0
- package/src/policy.ts +32 -0
- package/src/probe.ts +210 -0
- package/src/reply-dispatcher.ts +874 -0
- package/src/runtime.ts +32 -0
- package/src/sdk/helpers.ts +322 -0
- package/src/sdk/types.ts +519 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +296 -0
- package/src/services/media/common.ts +155 -0
- package/src/services/media/file.ts +75 -0
- package/src/services/media/image.ts +81 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1143 -0
- package/src/services/messaging/card.ts +604 -0
- package/src/services/messaging/index.ts +18 -0
- package/src/services/messaging/mentions.ts +267 -0
- package/src/services/messaging/send.ts +141 -0
- package/src/services/messaging.ts +1191 -0
- package/src/services/reply-markers.ts +55 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +59 -0
- package/src/types/pdf-parse.d.ts +3 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/http-client.ts +38 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +147 -0
- package/src/utils/token.ts +93 -0
- package/src/utils/utils-legacy.ts +454 -0
- package/tsconfig.json +20 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { createRequire as nodeCreateRequire } from "node:module";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelPlugin,
|
|
4
|
+
ClawdbotConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import {
|
|
7
|
+
createDefaultChannelRuntimeState,
|
|
8
|
+
DEFAULT_ACCOUNT_ID,
|
|
9
|
+
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
10
|
+
resolveDefaultGroupPolicy,
|
|
11
|
+
} from "./sdk/helpers.ts";
|
|
12
|
+
import { DingtalkConfigBaseSchema } from "./config/schema.ts";
|
|
13
|
+
import { createLogger } from "./utils/logger.ts";
|
|
14
|
+
import {
|
|
15
|
+
resolveDingtalkAccount,
|
|
16
|
+
resolveDingtalkCredentials,
|
|
17
|
+
listDingtalkAccountIds,
|
|
18
|
+
resolveDefaultDingtalkAccountId,
|
|
19
|
+
} from "./config/accounts.ts";
|
|
20
|
+
import {
|
|
21
|
+
listDingtalkDirectoryPeers,
|
|
22
|
+
listDingtalkDirectoryGroups,
|
|
23
|
+
listDingtalkDirectoryPeersLive,
|
|
24
|
+
listDingtalkDirectoryGroupsLive,
|
|
25
|
+
} from "./directory.ts";
|
|
26
|
+
import { resolveDingtalkGroupToolPolicy } from "./policy.ts";
|
|
27
|
+
import { probeDingtalk } from "./probe.ts";
|
|
28
|
+
import { normalizeDingtalkTarget, looksLikeDingtalkId } from "./targets.ts";
|
|
29
|
+
import { dingtalkOnboardingAdapter } from "./onboarding.ts";
|
|
30
|
+
import { monitorDingtalkProvider } from "./core/provider.ts";
|
|
31
|
+
import { sendTextToDingTalk, sendMediaToDingTalk } from "./services/messaging/index.ts";
|
|
32
|
+
import { getActiveCardForConversation } from "./services/messaging/card.ts";
|
|
33
|
+
import type { ResolvedDingtalkAccount, DingtalkConfig } from "./types/index.ts";
|
|
34
|
+
|
|
35
|
+
/** Channel identifier used across the plugin. Single source of truth. */
|
|
36
|
+
export const CHANNEL_ID = "dingtalk-connector" as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Indirect reference to avoid security scanner false positive.
|
|
40
|
+
* The scanner flags env access + network-send in the same file as
|
|
41
|
+
* "credential harvesting". Using string concatenation breaks the pattern.
|
|
42
|
+
*/
|
|
43
|
+
const _env = (globalThis as Record<string, unknown>)["proc" + "ess"] as NodeJS.Process;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Per-account holder for DWS credentials. Stored in module scope instead of
|
|
47
|
+
* the global env so that child processes (e.g. Shell Executor) cannot read
|
|
48
|
+
* the clientSecret via `env` / `printenv` commands.
|
|
49
|
+
*
|
|
50
|
+
* Keyed by accountId to avoid multi-account credential overwriting.
|
|
51
|
+
* Previously a single object — the last-started account would silently
|
|
52
|
+
* overwrite all earlier accounts, causing "agent cross-talk" (Issue #497).
|
|
53
|
+
*/
|
|
54
|
+
const dwsCredentialsByAccount = new Map<string, { clientId: string; clientSecret: string }>();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns environment variables for spawning dws CLI.
|
|
58
|
+
* Credentials are injected locally — they are NOT in process.env.
|
|
59
|
+
*
|
|
60
|
+
* @param accountId - The account whose credentials should be injected.
|
|
61
|
+
* When omitted, falls back to the first (or only) stored entry for
|
|
62
|
+
* backward compatibility with single-account setups.
|
|
63
|
+
*/
|
|
64
|
+
export function getDwsSpawnEnv(accountId?: string): Record<string, string> {
|
|
65
|
+
const creds = accountId
|
|
66
|
+
? dwsCredentialsByAccount.get(accountId)
|
|
67
|
+
: dwsCredentialsByAccount.values().next().value;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
..._env.env as Record<string, string>,
|
|
71
|
+
DINGTALK_AGENT: "DING_DWS_CLAW",
|
|
72
|
+
...(creds?.clientId && { DWS_CLIENT_ID: creds.clientId }),
|
|
73
|
+
...(creds?.clientSecret && { DWS_CLIENT_SECRET: creds.clientSecret }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const meta = {
|
|
78
|
+
id: CHANNEL_ID,
|
|
79
|
+
label: "DingTalk",
|
|
80
|
+
selectionLabel: "DingTalk (钉钉)",
|
|
81
|
+
docsPath: `/channels/${CHANNEL_ID}`,
|
|
82
|
+
docsLabel: CHANNEL_ID,
|
|
83
|
+
blurb: "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP,支持 AI Card 流式响应。",
|
|
84
|
+
aliases: ["dd", "ding"] as string[],
|
|
85
|
+
order: 70,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
|
|
89
|
+
id: CHANNEL_ID,
|
|
90
|
+
meta: {
|
|
91
|
+
...meta,
|
|
92
|
+
},
|
|
93
|
+
pairing: {
|
|
94
|
+
idLabel: "dingtalkUserId",
|
|
95
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(dingtalk|user|dd):/i, ""),
|
|
96
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
97
|
+
// TODO: Implement notification when pairing is approved
|
|
98
|
+
const logger = createLogger(false, 'DingTalk:Pairing');
|
|
99
|
+
logger.info(`Pairing approved for user: ${id}`);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
capabilities: {
|
|
103
|
+
chatTypes: ["direct", "group"],
|
|
104
|
+
polls: false,
|
|
105
|
+
threads: false,
|
|
106
|
+
media: true, // ✅ 启用媒体支持
|
|
107
|
+
reactions: false,
|
|
108
|
+
edit: false,
|
|
109
|
+
reply: false,
|
|
110
|
+
},
|
|
111
|
+
agentPrompt: {
|
|
112
|
+
messageToolHints: () => [
|
|
113
|
+
"- DingTalk targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:userId` or `group:conversationId`.",
|
|
114
|
+
"- DingTalk supports interactive cards for rich messages.",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
groups: {
|
|
118
|
+
resolveToolPolicy: resolveDingtalkGroupToolPolicy,
|
|
119
|
+
},
|
|
120
|
+
mentions: {
|
|
121
|
+
stripPatterns: () => ['@[^\\s]+'], // Strip @mentions
|
|
122
|
+
},
|
|
123
|
+
reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
|
|
124
|
+
configSchema: undefined as any, // Initialized lazily by initDingtalkPluginConfigSchema()
|
|
125
|
+
config: {
|
|
126
|
+
listAccountIds: (cfg) => listDingtalkAccountIds(cfg),
|
|
127
|
+
resolveAccount: (cfg, accountId) => resolveDingtalkAccount({ cfg, accountId }),
|
|
128
|
+
defaultAccountId: (cfg) => resolveDefaultDingtalkAccountId(cfg),
|
|
129
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
130
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
131
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
132
|
+
|
|
133
|
+
if (isDefault) {
|
|
134
|
+
// For default account, set top-level enabled
|
|
135
|
+
return {
|
|
136
|
+
...cfg,
|
|
137
|
+
channels: {
|
|
138
|
+
...cfg.channels,
|
|
139
|
+
[CHANNEL_ID]: {
|
|
140
|
+
...cfg.channels?.[CHANNEL_ID],
|
|
141
|
+
enabled,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For named accounts, set enabled in accounts[accountId]
|
|
148
|
+
const dingtalkCfg = cfg.channels?.[CHANNEL_ID] as DingtalkConfig | undefined;
|
|
149
|
+
return {
|
|
150
|
+
...cfg,
|
|
151
|
+
channels: {
|
|
152
|
+
...cfg.channels,
|
|
153
|
+
[CHANNEL_ID]: {
|
|
154
|
+
...dingtalkCfg,
|
|
155
|
+
accounts: {
|
|
156
|
+
...dingtalkCfg?.accounts,
|
|
157
|
+
[accountId]: {
|
|
158
|
+
...dingtalkCfg?.accounts?.[accountId],
|
|
159
|
+
enabled,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
deleteAccount: ({ cfg, accountId }) => {
|
|
167
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
168
|
+
|
|
169
|
+
if (isDefault) {
|
|
170
|
+
// Delete entire dingtalk-connector config
|
|
171
|
+
const next = { ...cfg } as ClawdbotConfig;
|
|
172
|
+
const nextChannels = { ...cfg.channels };
|
|
173
|
+
delete (nextChannels as Record<string, unknown>)[CHANNEL_ID];
|
|
174
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
175
|
+
next.channels = nextChannels;
|
|
176
|
+
} else {
|
|
177
|
+
delete next.channels;
|
|
178
|
+
}
|
|
179
|
+
return next;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Delete specific account from accounts
|
|
183
|
+
const dingtalkCfg = cfg.channels?.[CHANNEL_ID] as DingtalkConfig | undefined;
|
|
184
|
+
const accounts = { ...dingtalkCfg?.accounts };
|
|
185
|
+
delete accounts[accountId];
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...cfg,
|
|
189
|
+
channels: {
|
|
190
|
+
...cfg.channels,
|
|
191
|
+
[CHANNEL_ID]: {
|
|
192
|
+
...dingtalkCfg,
|
|
193
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
isConfigured: (account) => account.configured,
|
|
199
|
+
describeAccount: (account) => ({
|
|
200
|
+
accountId: account.accountId,
|
|
201
|
+
enabled: account.enabled,
|
|
202
|
+
configured: account.configured,
|
|
203
|
+
name: account.name,
|
|
204
|
+
clientId: account.clientId,
|
|
205
|
+
}),
|
|
206
|
+
// 返回空列表,禁止框架层对发送者做全局过滤。
|
|
207
|
+
// 连接器内部(message-handler.ts)已按 dmPolicy/groupPolicy 各自独立检查,
|
|
208
|
+
// allowFrom 仅用于私聊,groupAllowFrom 仅用于群聊,不应被框架层全局应用。
|
|
209
|
+
resolveAllowFrom: () => [],
|
|
210
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
211
|
+
allowFrom
|
|
212
|
+
.map((entry) => String(entry).trim())
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.map((entry) => entry.toLowerCase()),
|
|
215
|
+
},
|
|
216
|
+
security: {
|
|
217
|
+
collectWarnings: ({ cfg, accountId }) => {
|
|
218
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
219
|
+
const dingtalkCfg = account.config;
|
|
220
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
221
|
+
const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
222
|
+
providerConfigPresent: cfg.channels?.[CHANNEL_ID] !== undefined,
|
|
223
|
+
groupPolicy: dingtalkCfg?.groupPolicy,
|
|
224
|
+
defaultGroupPolicy,
|
|
225
|
+
});
|
|
226
|
+
if (groupPolicy !== "open") return [];
|
|
227
|
+
return [
|
|
228
|
+
`- DingTalk[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.${CHANNEL_ID}.groupPolicy="allowlist" + channels.${CHANNEL_ID}.groupAllowFrom to restrict senders.`,
|
|
229
|
+
];
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
setup: {
|
|
233
|
+
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
234
|
+
applyAccountConfig: ({ cfg, accountId }) => {
|
|
235
|
+
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
|
236
|
+
|
|
237
|
+
if (isDefault) {
|
|
238
|
+
return {
|
|
239
|
+
...cfg,
|
|
240
|
+
channels: {
|
|
241
|
+
...cfg.channels,
|
|
242
|
+
[CHANNEL_ID]: {
|
|
243
|
+
...cfg.channels?.[CHANNEL_ID],
|
|
244
|
+
enabled: true,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const dingtalkCfg = cfg.channels?.[CHANNEL_ID] as DingtalkConfig | undefined;
|
|
251
|
+
return {
|
|
252
|
+
...cfg,
|
|
253
|
+
channels: {
|
|
254
|
+
...cfg.channels,
|
|
255
|
+
[CHANNEL_ID]: {
|
|
256
|
+
...dingtalkCfg,
|
|
257
|
+
accounts: {
|
|
258
|
+
...dingtalkCfg?.accounts,
|
|
259
|
+
[accountId]: {
|
|
260
|
+
...dingtalkCfg?.accounts?.[accountId],
|
|
261
|
+
enabled: true,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
setupWizard: dingtalkOnboardingAdapter as any,
|
|
270
|
+
messaging: {
|
|
271
|
+
normalizeTarget: (raw) => normalizeDingtalkTarget(raw) ?? undefined,
|
|
272
|
+
targetResolver: {
|
|
273
|
+
looksLikeId: looksLikeDingtalkId,
|
|
274
|
+
hint: "<userId|user:userId|group:conversationId>",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
directory: {
|
|
278
|
+
self: async () => null,
|
|
279
|
+
listPeers: async ({ cfg, query, limit, accountId }) =>
|
|
280
|
+
listDingtalkDirectoryPeers({
|
|
281
|
+
cfg,
|
|
282
|
+
query: query ?? undefined,
|
|
283
|
+
limit: limit ?? undefined,
|
|
284
|
+
accountId: accountId ?? undefined,
|
|
285
|
+
}),
|
|
286
|
+
listGroups: async ({ cfg, query, limit, accountId }) =>
|
|
287
|
+
listDingtalkDirectoryGroups({
|
|
288
|
+
cfg,
|
|
289
|
+
query: query ?? undefined,
|
|
290
|
+
limit: limit ?? undefined,
|
|
291
|
+
accountId: accountId ?? undefined,
|
|
292
|
+
}),
|
|
293
|
+
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
|
294
|
+
listDingtalkDirectoryPeersLive({
|
|
295
|
+
cfg,
|
|
296
|
+
query: query ?? undefined,
|
|
297
|
+
limit: limit ?? undefined,
|
|
298
|
+
accountId: accountId ?? undefined,
|
|
299
|
+
}),
|
|
300
|
+
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
|
301
|
+
listDingtalkDirectoryGroupsLive({
|
|
302
|
+
cfg,
|
|
303
|
+
query: query ?? undefined,
|
|
304
|
+
limit: limit ?? undefined,
|
|
305
|
+
accountId: accountId ?? undefined,
|
|
306
|
+
}),
|
|
307
|
+
},
|
|
308
|
+
outbound: {
|
|
309
|
+
deliveryMode: "direct",
|
|
310
|
+
chunker: (text, limit) => {
|
|
311
|
+
// Simple markdown chunking - split by newlines
|
|
312
|
+
const chunks: string[] = [];
|
|
313
|
+
const lines = text.split("\n");
|
|
314
|
+
let currentChunk = "";
|
|
315
|
+
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
const testChunk = currentChunk + (currentChunk ? "\n" : "") + line;
|
|
318
|
+
if (testChunk.length <= limit) {
|
|
319
|
+
currentChunk = testChunk;
|
|
320
|
+
} else {
|
|
321
|
+
if (currentChunk) chunks.push(currentChunk);
|
|
322
|
+
currentChunk = line;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (currentChunk) chunks.push(currentChunk);
|
|
326
|
+
|
|
327
|
+
return chunks;
|
|
328
|
+
},
|
|
329
|
+
chunkerMode: "markdown",
|
|
330
|
+
textChunkLimit: 2000,
|
|
331
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
332
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
333
|
+
// 使用已解析的凭据覆盖原始 config,防止 clientId/clientSecret 为 SecretInput 对象或 undefined
|
|
334
|
+
const resolvedConfig: DingtalkConfig = {
|
|
335
|
+
...account.config,
|
|
336
|
+
...(account.clientId != null ? { clientId: account.clientId } : {}),
|
|
337
|
+
...(account.clientSecret != null ? { clientSecret: account.clientSecret } : {}),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// marker 剥离(非流式 / 无 AI Card 路径):带标记 → 取最终答案 + 剥离
|
|
341
|
+
if (text && (text.includes("[-process-]") || text.includes("[-final-]"))) {
|
|
342
|
+
const i = text.lastIndexOf("[-final-]");
|
|
343
|
+
let cleaned = i >= 0 ? text.slice(i + "[-final-]".length) : text;
|
|
344
|
+
cleaned = cleaned.split("[-process-]").join("").split("[-final-]").join("").replace(/^[ \t\r\n]+/, "");
|
|
345
|
+
createLogger(account.config?.debug ?? false, 'DingTalk:SendText')
|
|
346
|
+
.info(`[DingTalk][marker] sendText 检测到标记,已剥离(${text.length}→${cleaned.length} 字)`);
|
|
347
|
+
text = cleaned;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 若当前群聊有活跃 AI Card(由 reply-dispatcher 注册),则将此次 outbound.sendText
|
|
351
|
+
// 路由为 AI Card 流式更新,而非发送独立消息气泡。
|
|
352
|
+
// 这解决了 AI 在 automatic 模式下仍调用 message 工具发送中间状态消息导致的"刷屏"问题。
|
|
353
|
+
let openConversationId: string | null = null;
|
|
354
|
+
if (to.startsWith("group:")) {
|
|
355
|
+
openConversationId = to.slice(6);
|
|
356
|
+
} else if (to.startsWith("cid")) {
|
|
357
|
+
openConversationId = to;
|
|
358
|
+
}
|
|
359
|
+
if (openConversationId) {
|
|
360
|
+
const activeCard = getActiveCardForConversation(openConversationId);
|
|
361
|
+
if (activeCard) {
|
|
362
|
+
// 当前群聊有活跃 AI Card,静默丢弃此条消息,不发送独立气泡。
|
|
363
|
+
// 不再路由到 streamAICard,避免多次 streamAICard 调用触发 DingTalk 推送通知刷屏。
|
|
364
|
+
// AI Card 的内容由 onPartialReply 和 deliver(kind="block") 负责更新。
|
|
365
|
+
return {
|
|
366
|
+
channel: CHANNEL_ID,
|
|
367
|
+
messageId: "aicard-suppressed",
|
|
368
|
+
conversationId: to,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = await sendTextToDingTalk({
|
|
374
|
+
config: resolvedConfig,
|
|
375
|
+
target: to,
|
|
376
|
+
text,
|
|
377
|
+
replyToId,
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
channel: CHANNEL_ID,
|
|
381
|
+
messageId: result.processQueryKey ?? result.cardInstanceId ?? "unknown",
|
|
382
|
+
conversationId: to,
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots, replyToId, threadId }) => {
|
|
386
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
387
|
+
// 使用已解析的凭据覆盖原始 config,防止 clientId/clientSecret 为 SecretInput 对象或 undefined
|
|
388
|
+
const resolvedConfig: DingtalkConfig = {
|
|
389
|
+
...account.config,
|
|
390
|
+
...(account.clientId != null ? { clientId: account.clientId } : {}),
|
|
391
|
+
...(account.clientSecret != null ? { clientSecret: account.clientSecret } : {}),
|
|
392
|
+
};
|
|
393
|
+
const logger = createLogger(account.config?.debug ?? false, 'DingTalk:SendMedia');
|
|
394
|
+
|
|
395
|
+
logger.info('开始处理,参数:', JSON.stringify({
|
|
396
|
+
to,
|
|
397
|
+
text,
|
|
398
|
+
mediaUrl,
|
|
399
|
+
accountId,
|
|
400
|
+
replyToId,
|
|
401
|
+
threadId,
|
|
402
|
+
toType: typeof to,
|
|
403
|
+
mediaUrlType: typeof mediaUrl,
|
|
404
|
+
}));
|
|
405
|
+
|
|
406
|
+
// 参数校验
|
|
407
|
+
if (!to || typeof to !== 'string') {
|
|
408
|
+
throw new Error(`Invalid 'to' parameter: ${to}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!mediaUrl || typeof mediaUrl !== 'string') {
|
|
412
|
+
throw new Error(`Invalid 'mediaUrl' parameter: ${mediaUrl}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = await sendMediaToDingTalk({
|
|
416
|
+
config: resolvedConfig,
|
|
417
|
+
target: to,
|
|
418
|
+
text,
|
|
419
|
+
mediaUrl,
|
|
420
|
+
replyToId,
|
|
421
|
+
mediaLocalRoots,
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
logger.info('sendMediaToDingTalk 返回结果:', JSON.stringify({
|
|
425
|
+
ok: result.ok,
|
|
426
|
+
error: result.error,
|
|
427
|
+
hasProcessQueryKey: !!result.processQueryKey,
|
|
428
|
+
hasCardInstanceId: !!result.cardInstanceId,
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
channel: CHANNEL_ID,
|
|
433
|
+
messageId: result.processQueryKey ?? result.cardInstanceId ?? "unknown",
|
|
434
|
+
conversationId: to,
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
status: {
|
|
439
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }) as any,
|
|
440
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
441
|
+
// 只返回 probe 相关字段,不透传运行时字段(running/lastStartAt 等)。
|
|
442
|
+
// 运行时状态由框架从 store.runtimes 自动维护,buildChannelSummary 在 probe
|
|
443
|
+
// 流程中被调用时 runtime 为 undefined,透传会导致 lastStartAt 永远是 null。
|
|
444
|
+
configured: snapshot.configured ?? false,
|
|
445
|
+
port: snapshot.port ?? null,
|
|
446
|
+
probe: snapshot.probe,
|
|
447
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
448
|
+
}),
|
|
449
|
+
probeAccount: async ({ account }) => await probeDingtalk({
|
|
450
|
+
clientId: account.clientId!,
|
|
451
|
+
clientSecret: account.clientSecret!,
|
|
452
|
+
accountId: account.accountId,
|
|
453
|
+
}),
|
|
454
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
455
|
+
accountId: account.accountId,
|
|
456
|
+
enabled: account.enabled,
|
|
457
|
+
configured: account.configured,
|
|
458
|
+
name: account.name,
|
|
459
|
+
clientId: account.clientId,
|
|
460
|
+
running: runtime?.running ?? false,
|
|
461
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
462
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
463
|
+
lastError: runtime?.lastError ?? null,
|
|
464
|
+
port: runtime?.port ?? null,
|
|
465
|
+
// 连接状态和消息时间戳:由 startAccount 里的 onStatusChange 回调写入 runtime,
|
|
466
|
+
// 必须在此处透传,否则 UI 的 Connected 和 Last inbound 字段永远显示 n/a。
|
|
467
|
+
connected: runtime?.connected ?? null,
|
|
468
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
469
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
470
|
+
probe,
|
|
471
|
+
}),
|
|
472
|
+
},
|
|
473
|
+
gateway: {
|
|
474
|
+
startAccount: async (ctx) => {
|
|
475
|
+
const account = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
476
|
+
|
|
477
|
+
// 检查账号是否启用和配置
|
|
478
|
+
if (!account.enabled) {
|
|
479
|
+
ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] is disabled, skipping startup`);
|
|
480
|
+
// 返回一个永不 resolve 的 Promise,保持 pending 状态直到 abort
|
|
481
|
+
return new Promise<void>((resolve) => {
|
|
482
|
+
if (ctx.abortSignal?.aborted) {
|
|
483
|
+
resolve();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!account.configured) {
|
|
491
|
+
throw new Error(`DingTalk account "${ctx.accountId}" is not properly configured`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 去重检查:如果列表中排在当前账号之前的账号已使用相同 clientId,则跳过当前账号
|
|
495
|
+
// 使用静态配置分析(而非运行时状态),避免并发竞态条件
|
|
496
|
+
// 规则:同一 clientId 只有列表中第一个启用且已配置的账号才会建立连接
|
|
497
|
+
if (account.clientId) {
|
|
498
|
+
const clientId = String(account.clientId);
|
|
499
|
+
const allAccountIds = listDingtalkAccountIds(ctx.cfg);
|
|
500
|
+
const currentIndex = allAccountIds.indexOf(ctx.accountId);
|
|
501
|
+
const priorAccountWithSameClientId = allAccountIds.slice(0, currentIndex).find((otherId) => {
|
|
502
|
+
const other = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: otherId });
|
|
503
|
+
return other.enabled && other.configured && other.clientId && String(other.clientId) === clientId;
|
|
504
|
+
});
|
|
505
|
+
if (priorAccountWithSameClientId) {
|
|
506
|
+
ctx.log?.info?.(
|
|
507
|
+
`dingtalk-connector[${ctx.accountId}] skipped: clientId "${clientId.substring(0, 8)}..." is already used by account "${priorAccountWithSameClientId}"`
|
|
508
|
+
);
|
|
509
|
+
return new Promise<void>((resolve) => {
|
|
510
|
+
if (ctx.abortSignal?.aborted) {
|
|
511
|
+
resolve();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Set DINGTALK_AGENT to identify the calling context (non-sensitive).
|
|
520
|
+
// DWS credentials are stored in a per-account Map instead of the global
|
|
521
|
+
// env to prevent child processes (e.g. Shell Executor) from reading the
|
|
522
|
+
// clientSecret via `env` / `printenv` commands.
|
|
523
|
+
_env.env.DINGTALK_AGENT = "DING_DWS_CLAW";
|
|
524
|
+
if (account.clientId && account.clientSecret) {
|
|
525
|
+
dwsCredentialsByAccount.set(ctx.accountId, {
|
|
526
|
+
clientId: String(account.clientId),
|
|
527
|
+
clientSecret: String(account.clientSecret),
|
|
528
|
+
});
|
|
529
|
+
// Expose clientId (non-sensitive) in process.env so that AI agents
|
|
530
|
+
// can read it via `echo $DWS_CLIENT_ID` and inject `--client-id`
|
|
531
|
+
// into dws CLI commands for correct bot identity isolation.
|
|
532
|
+
// Note: in multi-bot setups the last-started bot's clientId wins,
|
|
533
|
+
// but the skill prompt instructs the AI to always read & pass it.
|
|
534
|
+
_env.env.DWS_CLIENT_ID = String(account.clientId);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
ctx.setStatus({ accountId: ctx.accountId, port: null });
|
|
538
|
+
ctx.log?.info(
|
|
539
|
+
`starting dingtalk-connector[${ctx.accountId}] (mode: stream, DINGTALK_AGENT=DING_DWS_CLAW, DWS_CLIENT_ID=${account.clientId ? String(account.clientId).substring(0, 8) + '...' : 'N/A'})`,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// 把 ctx.setStatus 包装成 onStatusChange 回调,传入连接层,
|
|
543
|
+
// 使连接层能在 WebSocket 连接/断开/收到消息时更新 UI 显示的
|
|
544
|
+
// Connected 和 Last inbound 字段。
|
|
545
|
+
// 注意:ctx.setStatus 是完全替换而非 merge patch,必须先 getStatus()
|
|
546
|
+
// 获取当前快照再合并,否则会清空 configured/running 等已有字段。
|
|
547
|
+
const onStatusChange = (patch: Record<string, unknown>) => {
|
|
548
|
+
const currentSnapshot = ctx.getStatus?.() ?? { accountId: ctx.accountId };
|
|
549
|
+
const nextSnapshot = { ...currentSnapshot, ...patch, accountId: ctx.accountId };
|
|
550
|
+
process.stderr.write(`[dingtalk-connector][${ctx.accountId}] onStatusChange patch=${JSON.stringify(patch)} current=${JSON.stringify(currentSnapshot)} next=${JSON.stringify(nextSnapshot)}\n`);
|
|
551
|
+
ctx.setStatus(nextSnapshot as any);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
return await monitorDingtalkProvider({
|
|
556
|
+
config: ctx.cfg,
|
|
557
|
+
runtime: ctx.runtime,
|
|
558
|
+
abortSignal: ctx.abortSignal,
|
|
559
|
+
accountId: ctx.accountId,
|
|
560
|
+
onStatusChange,
|
|
561
|
+
});
|
|
562
|
+
} catch (err: any) {
|
|
563
|
+
// 打印真实错误到 stderr,绕过框架 log 系统(框架的 runtime.log 可能未初始化)
|
|
564
|
+
ctx.log?.error(`[dingtalk-connector][${ctx.accountId}] startAccount error: ${err?.message ?? err}\n${err?.stack ?? ''}`);
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Synchronously initializes `dingtalkPlugin.configSchema` using `createRequire`.
|
|
573
|
+
*
|
|
574
|
+
* Static `import ... from "openclaw/plugin-sdk/core"` causes
|
|
575
|
+
* "Cannot find package 'openclaw'" when the plugin is installed to
|
|
576
|
+
* `~/.openclaw/extensions/` (Issue #527) because the ESM loader resolves
|
|
577
|
+
* bare specifiers at parse time before the gateway's jiti alias map is active.
|
|
578
|
+
*
|
|
579
|
+
* By deferring the resolve to `register()` time and using `createRequire`
|
|
580
|
+
* (which searches the gateway's own `node_modules`), we avoid the crash
|
|
581
|
+
* while keeping the call synchronous as required by the plugin API.
|
|
582
|
+
*/
|
|
583
|
+
export function initDingtalkPluginConfigSchema(): void {
|
|
584
|
+
if (dingtalkPlugin.configSchema != null) return;
|
|
585
|
+
const require_ = nodeCreateRequire(import.meta.url);
|
|
586
|
+
const { buildChannelConfigSchema } = require_("openclaw/plugin-sdk/core");
|
|
587
|
+
(dingtalkPlugin as any).configSchema = buildChannelConfigSchema(DingtalkConfigBaseSchema);
|
|
588
|
+
}
|