@largezhou/ddingtalk 1.3.0

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.
@@ -0,0 +1,130 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import type { ChannelOnboardingAdapter } from "openclaw/plugin-sdk";
3
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
4
+ import type { DingTalkConfig } from "./types.js";
5
+ import {
6
+ listDingTalkAccountIds,
7
+ resolveDingTalkAccount,
8
+ } from "./accounts.js";
9
+ import { PLUGIN_ID } from "./constants.js";
10
+
11
+ const channel = PLUGIN_ID;
12
+
13
+ /**
14
+ * Display DingTalk credentials configuration help
15
+ */
16
+ async function noteDingTalkCredentialsHelp(prompter: {
17
+ note: (message: string, title?: string) => Promise<void>;
18
+ }): Promise<void> {
19
+ await prompter.note(
20
+ [
21
+ "1) Log in to DingTalk Open Platform: https://open.dingtalk.com",
22
+ "2) Create an internal enterprise app -> Robot",
23
+ "3) Get AppKey (Client ID) and AppSecret (Client Secret)",
24
+ "4) Enable Stream mode in app configuration",
25
+ "Docs: https://open.dingtalk.com/document/",
26
+ ].join("\n"),
27
+ "DingTalk bot setup"
28
+ );
29
+ }
30
+
31
+ /**
32
+ * DingTalk Onboarding Adapter(单账户模式)
33
+ */
34
+ export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
35
+ channel,
36
+ getStatus: async ({ cfg }) => {
37
+ const configured = listDingTalkAccountIds(cfg).some((accountId) => {
38
+ const account = resolveDingTalkAccount({ cfg, accountId });
39
+ return Boolean(account.clientId?.trim() && account.clientSecret?.trim());
40
+ });
41
+ return {
42
+ channel,
43
+ configured,
44
+ statusLines: [`DingTalk: ${configured ? "configured" : "needs credentials"}`],
45
+ selectionHint: configured ? "configured" : "needs AppKey/AppSecret",
46
+ quickstartScore: configured ? 1 : 5,
47
+ };
48
+ },
49
+ configure: async ({
50
+ cfg,
51
+ prompter,
52
+ }) => {
53
+ let next = cfg;
54
+ const resolvedAccount = resolveDingTalkAccount({ cfg: next });
55
+ const accountConfigured = Boolean(
56
+ resolvedAccount.clientId?.trim() && resolvedAccount.clientSecret?.trim()
57
+ );
58
+ const dingtalkConfig = (next.channels?.[PLUGIN_ID] ?? {}) as DingTalkConfig;
59
+ const hasConfigCredentials = Boolean(dingtalkConfig.clientId);
60
+
61
+ let clientId: string | null = null;
62
+ let clientSecret: string | null = null;
63
+
64
+ if (!accountConfigured) {
65
+ await noteDingTalkCredentialsHelp(prompter);
66
+ }
67
+
68
+ if (hasConfigCredentials) {
69
+ const keep = await prompter.confirm({
70
+ message: "DingTalk credentials already configured. Keep them?",
71
+ initialValue: true,
72
+ });
73
+ if (!keep) {
74
+ clientId = String(
75
+ await prompter.text({
76
+ message: "Enter DingTalk AppKey (Client ID)",
77
+ validate: (value) => (value?.trim() ? undefined : "Required"),
78
+ })
79
+ ).trim();
80
+ clientSecret = String(
81
+ await prompter.text({
82
+ message: "Enter DingTalk AppSecret (Client Secret)",
83
+ validate: (value) => (value?.trim() ? undefined : "Required"),
84
+ })
85
+ ).trim();
86
+ }
87
+ } else {
88
+ clientId = String(
89
+ await prompter.text({
90
+ message: "Enter DingTalk AppKey (Client ID)",
91
+ validate: (value) => (value?.trim() ? undefined : "Required"),
92
+ })
93
+ ).trim();
94
+ clientSecret = String(
95
+ await prompter.text({
96
+ message: "Enter DingTalk AppSecret (Client Secret)",
97
+ validate: (value) => (value?.trim() ? undefined : "Required"),
98
+ })
99
+ ).trim();
100
+ }
101
+
102
+ if (clientId && clientSecret) {
103
+ const updatedDingtalkConfig = (next.channels?.[PLUGIN_ID] ?? {}) as DingTalkConfig;
104
+ next = {
105
+ ...next,
106
+ channels: {
107
+ ...next.channels,
108
+ [PLUGIN_ID]: {
109
+ ...updatedDingtalkConfig,
110
+ enabled: true,
111
+ clientId,
112
+ clientSecret,
113
+ },
114
+ },
115
+ };
116
+ }
117
+
118
+ return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
119
+ },
120
+ disable: (cfg) => {
121
+ const dingtalkConfig = (cfg.channels?.[PLUGIN_ID] ?? {}) as DingTalkConfig;
122
+ return {
123
+ ...cfg,
124
+ channels: {
125
+ ...cfg.channels,
126
+ [PLUGIN_ID]: { ...dingtalkConfig, enabled: false },
127
+ },
128
+ };
129
+ },
130
+ };
package/src/runtime.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setDingTalkRuntime(r: PluginRuntime): void {
6
+ runtime = r;
7
+ }
8
+
9
+ export function getDingTalkRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error("DingTalk runtime not initialized - plugin not registered");
12
+ }
13
+ return runtime;
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,250 @@
1
+ import { z } from "zod";
2
+
3
+ // ======================= DingTalk Config Schema =======================
4
+
5
+ /** 群聊策略 */
6
+ export type DingTalkGroupPolicy = "open" | "allowlist" | "disabled";
7
+
8
+ /** 单个群组的独立配置 Schema */
9
+ export const DingTalkGroupConfigSchema = z.object({
10
+ /** 工具策略 */
11
+ tools: z.object({
12
+ allow: z.array(z.string()).optional(),
13
+ deny: z.array(z.string()).optional(),
14
+ }).optional(),
15
+ /** 是否启用该群 */
16
+ enabled: z.boolean().optional(),
17
+ /** 群内发送者白名单 */
18
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
19
+ /** 群级系统提示词 */
20
+ systemPrompt: z.string().optional(),
21
+ }).strict();
22
+
23
+ export type DingTalkGroupConfig = z.infer<typeof DingTalkGroupConfigSchema>;
24
+
25
+ /**
26
+ * 钉钉渠道配置 Schema(单账户)
27
+ */
28
+ export const DingTalkConfigSchema = z.object({
29
+ /** 是否启用钉钉渠道 */
30
+ enabled: z.boolean().optional(),
31
+ /** 账户名称 */
32
+ name: z.string().optional(),
33
+ /** 钉钉应用 AppKey */
34
+ clientId: z.string().optional(),
35
+ /** 钉钉应用 AppSecret */
36
+ clientSecret: z.string().optional(),
37
+ /** 允许的发送者白名单(单聊),默认 ["*"] 允许所有人 */
38
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
39
+ /** 群聊策略:open=允许所有群, allowlist=白名单, disabled=禁止群聊 */
40
+ groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
41
+ /** 群聊白名单(openConversationId 列表),groupPolicy=allowlist 时生效 */
42
+ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
43
+ /** 按群 ID 的独立配置 */
44
+ groups: z.record(z.string(), DingTalkGroupConfigSchema).optional(),
45
+ });
46
+
47
+ export type DingTalkConfig = z.infer<typeof DingTalkConfigSchema>;
48
+
49
+ // ======================= Resolved Account Type =======================
50
+
51
+ /**
52
+ * 解析后的钉钉账户配置
53
+ */
54
+ export interface ResolvedDingTalkAccount {
55
+ /** 账户 ID(固定为 default) */
56
+ accountId: string;
57
+ /** 账户名称 */
58
+ name?: string;
59
+ /** 是否启用 */
60
+ enabled: boolean;
61
+ /** 钉钉应用 AppKey */
62
+ clientId: string;
63
+ /** 钉钉应用 AppSecret */
64
+ clientSecret: string;
65
+ /** Token 来源 */
66
+ tokenSource: "config" | "none";
67
+ /** 允许的发送者白名单(单聊),默认 ["*"] 允许所有人 */
68
+ allowFrom: Array<string | number>;
69
+ /** 群聊策略 */
70
+ groupPolicy: DingTalkGroupPolicy;
71
+ /** 群聊白名单 */
72
+ groupAllowFrom: Array<string | number>;
73
+ /** 按群 ID 的独立配置 */
74
+ groups: Record<string, DingTalkGroupConfig>;
75
+ }
76
+
77
+ // ======================= Message Types =======================
78
+
79
+ /**
80
+ * 会话类型
81
+ */
82
+ export type ConversationType = "1" | "2"; // 1: 单聊, 2: 群聊
83
+
84
+ /**
85
+ * 消息类型
86
+ */
87
+ export type MessageType = "text" | "picture" | "richText" | "markdown" | "file" | "audio" | "video";
88
+
89
+ // ======================= 消息内容类型 =======================
90
+
91
+ /** 图片消息内容 */
92
+ export interface PictureContent {
93
+ downloadCode?: string;
94
+ pictureDownloadCode?: string;
95
+ height?: number;
96
+ width?: number;
97
+ extension?: string;
98
+ }
99
+
100
+ /** 音频消息内容 */
101
+ export interface AudioContent {
102
+ downloadCode?: string;
103
+ /** 语音时长(秒) */
104
+ duration?: number;
105
+ /** 文件扩展名,如 amr */
106
+ extension?: string;
107
+ mediaId?: string;
108
+ /** 语音转文字结果 */
109
+ recognition?: string;
110
+ }
111
+
112
+ /** 视频消息内容 */
113
+ export interface VideoContent {
114
+ downloadCode?: string;
115
+ /** 视频时长(秒) */
116
+ duration?: number;
117
+ /** 文件扩展名,如 mp4 */
118
+ extension?: string;
119
+ mediaId?: string;
120
+ videoType?: string;
121
+ width?: number;
122
+ height?: number;
123
+ }
124
+
125
+ /** 文件消息内容 */
126
+ export interface FileContent {
127
+ downloadCode?: string;
128
+ /** 文件名 */
129
+ fileName?: string;
130
+ /** 文件大小(字节) */
131
+ fileSize?: number;
132
+ /** 文件扩展名 */
133
+ extension?: string;
134
+ spaceId?: string;
135
+ mediaId?: string;
136
+ }
137
+
138
+ // ======================= 富文本消息类型 =======================
139
+
140
+ /** 富文本元素类型 */
141
+ export type RichTextElementType = "text" | "picture";
142
+
143
+ /** 富文本元素 - 文本 */
144
+ export interface RichTextTextElement {
145
+ /** 文本元素可能没有 type 字段,或 type 为 "text" */
146
+ type?: "text";
147
+ /** 文本内容 */
148
+ text: string;
149
+ }
150
+
151
+ /** 富文本元素 - 图片 */
152
+ export interface RichTextPictureElement {
153
+ type: "picture";
154
+ /** 下载码 */
155
+ downloadCode?: string;
156
+ /** 备选下载码字段 */
157
+ pictureDownloadCode?: string;
158
+ /** 图片宽度 */
159
+ width?: number;
160
+ /** 图片高度 */
161
+ height?: number;
162
+ /** 文件扩展名 */
163
+ extension?: string;
164
+ }
165
+
166
+ /** 富文本元素联合类型 */
167
+ export type RichTextElement = RichTextTextElement | RichTextPictureElement;
168
+
169
+ /** 富文本消息内容 */
170
+ export interface RichTextContent {
171
+ richText: RichTextElement[];
172
+ }
173
+
174
+ /** 消息内容联合类型 */
175
+ export type MessageContent = PictureContent | AudioContent | VideoContent | FileContent | RichTextContent;
176
+
177
+ /**
178
+ * 钉钉机器人消息数据(来自 Stream 回调)
179
+ */
180
+ export interface DingTalkMessageData {
181
+ conversationId: string;
182
+ conversationType: ConversationType;
183
+ chatbotCorpId: string;
184
+ chatbotUserId: string;
185
+ msgId: string;
186
+ msgtype: MessageType;
187
+ createAt: string;
188
+ senderNick: string;
189
+ senderStaffId: string;
190
+ senderCorpId: string;
191
+ robotCode: string;
192
+ isInAtList: boolean;
193
+ sessionWebhook?: string;
194
+ sessionWebhookExpiredTime?: string;
195
+ text?: {
196
+ content: string;
197
+ };
198
+ /** 媒体消息内容(图片、语音、视频、文件) */
199
+ content?: MessageContent;
200
+ atUsers?: Array<{
201
+ dingtalkId: string;
202
+ staffId?: string;
203
+ }>;
204
+ // ---- 群聊特有字段 ----
205
+ /** 群名称(群聊时存在) */
206
+ conversationTitle?: string;
207
+ /** 群会话 ID(群聊时存在,用于主动发消息) */
208
+ openConversationId?: string;
209
+ /** 发送者是否群管理员 */
210
+ isAdmin?: boolean;
211
+ }
212
+
213
+ /**
214
+ * Webhook 响应
215
+ */
216
+ export interface WebhookResponse {
217
+ errcode: number;
218
+ errmsg?: string;
219
+ }
220
+
221
+ // ======================= 回复消息体类型 =======================
222
+
223
+ /** @ 配置 */
224
+ export interface AtConfig {
225
+ atUserIds?: string[];
226
+ atMobiles?: string[];
227
+ isAtAll?: boolean;
228
+ }
229
+
230
+ /** 回复消息体 - 文本 */
231
+ export interface TextReplyBody {
232
+ msgtype: "text";
233
+ text: {
234
+ content: string;
235
+ };
236
+ at?: AtConfig;
237
+ }
238
+
239
+ /** 回复消息体 - Markdown */
240
+ export interface MarkdownReplyBody {
241
+ msgtype: "markdown";
242
+ markdown: {
243
+ title?: string;
244
+ text: string;
245
+ };
246
+ at?: AtConfig;
247
+ }
248
+
249
+ /** 回复消息体联合类型 */
250
+ export type ReplyBody = TextReplyBody | MarkdownReplyBody;