@yaoyuanchao/dingtalk 1.2.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.
- package/CHANGELOG.md +149 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/clawdbot.plugin.json +9 -0
- package/index.ts +16 -0
- package/package.json +55 -0
- package/src/accounts.ts +73 -0
- package/src/api.ts +382 -0
- package/src/channel.ts +271 -0
- package/src/config-schema.ts +115 -0
- package/src/monitor.ts +622 -0
- package/src/onboarding.ts +152 -0
- package/src/probe.ts +36 -0
- package/src/runtime.ts +10 -0
- package/src/types.ts +56 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { probeDingTalk } from './probe.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interactive onboarding wizard for DingTalk plugin configuration
|
|
5
|
+
*/
|
|
6
|
+
export async function onboardDingTalk(ctx: { runtime: any; accountId: string }) {
|
|
7
|
+
const { runtime } = ctx;
|
|
8
|
+
const ui = runtime.ui;
|
|
9
|
+
|
|
10
|
+
ui.info('欢迎使用钉钉插件配置向导!\n');
|
|
11
|
+
|
|
12
|
+
// Step 1: Credential input
|
|
13
|
+
ui.info('Step 1/4: 获取钉钉应用凭证');
|
|
14
|
+
ui.info('请在钉钉开发者平台创建企业内部应用:');
|
|
15
|
+
ui.info('https://open-dev.dingtalk.com/\n');
|
|
16
|
+
|
|
17
|
+
const clientId = await ui.prompt({
|
|
18
|
+
type: 'text',
|
|
19
|
+
message: '请输入 Client ID (AppKey):',
|
|
20
|
+
validate: (val: string) => val.trim().length > 0 || '不能为空',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const clientSecret = await ui.prompt({
|
|
24
|
+
type: 'password',
|
|
25
|
+
message: '请输入 Client Secret (AppSecret):',
|
|
26
|
+
validate: (val: string) => val.trim().length > 0 || '不能为空',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Step 2: Connection test
|
|
30
|
+
ui.info('\nStep 2/4: 测试连接...');
|
|
31
|
+
try {
|
|
32
|
+
const result = await probeDingTalk(clientId, clientSecret);
|
|
33
|
+
if (result.ok) {
|
|
34
|
+
ui.success(`✓ 连接成功!延迟: ${result.latency}ms`);
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(result.error || '连接失败');
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
ui.error(`✗ 连接失败: ${error}`);
|
|
40
|
+
const retry = await ui.confirm('是否重新输入凭证?');
|
|
41
|
+
if (retry) {
|
|
42
|
+
return onboardDingTalk(ctx);
|
|
43
|
+
}
|
|
44
|
+
throw new Error('配置取消');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 3: DM policy configuration
|
|
48
|
+
ui.info('\nStep 3/4: 配置私聊策略');
|
|
49
|
+
const dmPolicy = await ui.select({
|
|
50
|
+
message: '选择私聊策略:',
|
|
51
|
+
choices: [
|
|
52
|
+
{
|
|
53
|
+
value: 'pairing',
|
|
54
|
+
label: 'Pairing (推荐)',
|
|
55
|
+
description: '首次联系时显示 staffId,需管理员添加白名单',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'allowlist',
|
|
59
|
+
label: 'Allowlist',
|
|
60
|
+
description: '只允许指定用户私聊',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: 'open',
|
|
64
|
+
label: 'Open',
|
|
65
|
+
description: '任何人都可以私聊(不推荐)',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: 'disabled',
|
|
69
|
+
label: 'Disabled',
|
|
70
|
+
description: '禁用私聊',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
default: 'pairing',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let dmAllowlist: string[] = [];
|
|
77
|
+
if (dmPolicy === 'allowlist') {
|
|
78
|
+
const input = await ui.prompt({
|
|
79
|
+
type: 'text',
|
|
80
|
+
message: '输入允许的 staffId(逗号分隔):',
|
|
81
|
+
default: '',
|
|
82
|
+
});
|
|
83
|
+
dmAllowlist = input.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 4: Group policy configuration
|
|
87
|
+
ui.info('\nStep 4/4: 配置群聊策略');
|
|
88
|
+
const groupPolicy = await ui.select({
|
|
89
|
+
message: '选择群聊策略:',
|
|
90
|
+
choices: [
|
|
91
|
+
{
|
|
92
|
+
value: 'allowlist',
|
|
93
|
+
label: 'Allowlist (推荐)',
|
|
94
|
+
description: '只允许指定群聊',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: 'open',
|
|
98
|
+
label: 'Open',
|
|
99
|
+
description: '允许所有群聊',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
value: 'disabled',
|
|
103
|
+
label: 'Disabled',
|
|
104
|
+
description: '禁用群聊',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
default: 'allowlist',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let groupAllowlist: string[] = [];
|
|
111
|
+
if (groupPolicy === 'allowlist') {
|
|
112
|
+
ui.info('\n获取群聊 conversationId 的方法:');
|
|
113
|
+
ui.info(' 1. 将机器人添加到群聊');
|
|
114
|
+
ui.info(' 2. 在群聊中 @机器人 发送消息');
|
|
115
|
+
ui.info(' 3. 查看日志找到 conversationId\n');
|
|
116
|
+
const input = await ui.prompt({
|
|
117
|
+
type: 'text',
|
|
118
|
+
message: 'conversationId (可稍后添加):',
|
|
119
|
+
default: '',
|
|
120
|
+
});
|
|
121
|
+
groupAllowlist = input.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requireMention = await ui.confirm({
|
|
125
|
+
message: '在群聊中是否要求 @机器人?',
|
|
126
|
+
default: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Build configuration object
|
|
130
|
+
const config = {
|
|
131
|
+
enabled: true,
|
|
132
|
+
clientId,
|
|
133
|
+
clientSecret,
|
|
134
|
+
dm: {
|
|
135
|
+
enabled: dmPolicy !== 'disabled',
|
|
136
|
+
policy: dmPolicy,
|
|
137
|
+
allowFrom: dmAllowlist,
|
|
138
|
+
},
|
|
139
|
+
groupPolicy,
|
|
140
|
+
groupAllowlist,
|
|
141
|
+
requireMention,
|
|
142
|
+
messageFormat: 'text' as const,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Save configuration
|
|
146
|
+
await runtime.config.set('channels.dingtalk', config);
|
|
147
|
+
|
|
148
|
+
ui.success('\n✓ 配置完成!');
|
|
149
|
+
ui.info('下一步: 运行 clawdbot gateway 启动网关\n');
|
|
150
|
+
|
|
151
|
+
return config;
|
|
152
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getDingTalkAccessToken } from './api.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Probe DingTalk connection health
|
|
5
|
+
* @param clientId - DingTalk application AppKey
|
|
6
|
+
* @param clientSecret - DingTalk application AppSecret
|
|
7
|
+
* @returns Health check result with latency measurement
|
|
8
|
+
*/
|
|
9
|
+
export async function probeDingTalk(
|
|
10
|
+
clientId: string,
|
|
11
|
+
clientSecret: string,
|
|
12
|
+
): Promise<{ ok: boolean; latency?: number; error?: string }> {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const token = await getDingTalkAccessToken(clientId, clientSecret);
|
|
17
|
+
if (!token) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
error: 'Failed to get access token',
|
|
21
|
+
latency: Date.now() - startTime,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
latency: Date.now() - startTime,
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: error instanceof Error ? error.message : String(error),
|
|
33
|
+
latency: Date.now() - startTime,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/runtime.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Inbound robot message from DingTalk Stream SDK */
|
|
2
|
+
export interface DingTalkRobotMessage {
|
|
3
|
+
conversationId: string;
|
|
4
|
+
chatbotCorpId: string;
|
|
5
|
+
chatbotUserId: string;
|
|
6
|
+
msgId: string;
|
|
7
|
+
senderNick: string;
|
|
8
|
+
isAdmin: boolean;
|
|
9
|
+
senderStaffId: string;
|
|
10
|
+
sessionWebhookExpiredTime: number;
|
|
11
|
+
createAt: number;
|
|
12
|
+
senderCorpId: string;
|
|
13
|
+
conversationType: string; // "1" = DM, "2" = group
|
|
14
|
+
senderId: string;
|
|
15
|
+
sessionWebhook: string;
|
|
16
|
+
robotCode: string;
|
|
17
|
+
msgtype: string;
|
|
18
|
+
text?: { content: string };
|
|
19
|
+
richText?: unknown;
|
|
20
|
+
picture?: { downloadCode: string };
|
|
21
|
+
atUsers?: Array<{ dingtalkId: string; staffId?: string }>;
|
|
22
|
+
isInAtList?: boolean;
|
|
23
|
+
conversationTitle?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolved account for DingTalk */
|
|
27
|
+
export interface ResolvedDingTalkAccount {
|
|
28
|
+
accountId: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
configured: boolean;
|
|
32
|
+
clientId?: string;
|
|
33
|
+
clientSecret?: string;
|
|
34
|
+
robotCode?: string;
|
|
35
|
+
credentialSource: "config" | "env" | "none";
|
|
36
|
+
config: Record<string, any>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** DingTalk channel config shape */
|
|
40
|
+
export interface DingTalkChannelConfig {
|
|
41
|
+
enabled?: boolean;
|
|
42
|
+
clientId?: string;
|
|
43
|
+
clientSecret?: string;
|
|
44
|
+
robotCode?: string;
|
|
45
|
+
dm?: {
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
policy?: string;
|
|
48
|
+
allowFrom?: string[];
|
|
49
|
+
};
|
|
50
|
+
groupPolicy?: string;
|
|
51
|
+
groupAllowlist?: string[];
|
|
52
|
+
requireMention?: boolean;
|
|
53
|
+
textChunkLimit?: number;
|
|
54
|
+
messageFormat?: 'text' | 'markdown';
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|