@yaoyuanchao/dingtalk 1.7.1 → 1.7.2
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 +9 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/src/config-schema.ts +169 -167
- package/src/monitor.ts +21 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.11] - 2026-03-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Per-group sender allowlist** — New `groups.<conversationId>.allowFrom` config field.
|
|
13
|
+
When set, the bot silently ignores @ messages from anyone not in the list.
|
|
14
|
+
Supports `"*"` wildcard to allow all senders (same as omitting the field).
|
|
15
|
+
Useful for restricting a shared group bot to specific authorized users.
|
|
16
|
+
|
|
8
17
|
## [1.5.3] - 2026-02-03
|
|
9
18
|
|
|
10
19
|
### Improved
|
package/README.md
CHANGED
|
@@ -61,6 +61,9 @@ clawdbot gateway
|
|
|
61
61
|
| `messageFormat` | 消息格式: `text`/`markdown`/`auto` | `auto` |
|
|
62
62
|
| `typingIndicator` | 显示"思考中"提示 | `true` |
|
|
63
63
|
| `longTextMode` | 长文本处理: `chunk`/`file` | `chunk` |
|
|
64
|
+
| `groups.<id>.systemPrompt` | 指定群的额外 system prompt | - |
|
|
65
|
+
| `groups.<id>.enabled` | 是否启用该群(false = 完全忽略) | `true` |
|
|
66
|
+
| `groups.<id>.allowFrom` | 该群只响应这些 staffId 的消息(支持 `"*"` 通配符) | `[]`(不限制) |
|
|
64
67
|
|
|
65
68
|
</details>
|
|
66
69
|
|
|
@@ -80,6 +83,32 @@ Access denied. Your staffId: 050914XXXXXXXXX
|
|
|
80
83
|
```
|
|
81
84
|
把这个 ID 加到 `dm.allowFrom` 里,重启 gateway 即可。
|
|
82
85
|
|
|
86
|
+
## 🔒 群聊发言人限制(Per-group allowFrom)
|
|
87
|
+
|
|
88
|
+
可以让机器人在某个群里**只响应指定用户**的 @ 消息,其他人 @ 会被静默忽略。
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"channels": {
|
|
93
|
+
"dingtalk": {
|
|
94
|
+
"groups": {
|
|
95
|
+
"<conversationId>": {
|
|
96
|
+
"allowFrom": ["050914185922786044"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**获取 conversationId:** 机器人收到群消息时日志里会打印,或通过钉钉开放平台 API 查询。
|
|
105
|
+
|
|
106
|
+
**支持通配符:** `"allowFrom": ["*"]` 表示允许所有人(等同于不设置)。
|
|
107
|
+
|
|
108
|
+
**与 `dm.allowFrom` 的区别:**
|
|
109
|
+
- `dm.allowFrom` — 私聊白名单(全局)
|
|
110
|
+
- `groups.<id>.allowFrom` — 指定群的发言人白名单(per-group)
|
|
111
|
+
|
|
83
112
|
## 📝 更新日志
|
|
84
113
|
|
|
85
114
|
**v1.5.0** — 新增 Typing Indicator(思考中提示,自动撤回)
|
package/package.json
CHANGED
package/src/config-schema.ts
CHANGED
|
@@ -1,167 +1,169 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
// 定义枚举类型
|
|
4
|
-
export const dmPolicySchema = z.enum(['disabled', 'pairing', 'allowlist', 'open'], {
|
|
5
|
-
description: 'DM access control policy',
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export const groupPolicySchema = z.enum(['disabled', 'allowlist', 'open'], {
|
|
9
|
-
description: 'Group chat access control policy',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const messageFormatSchema = z.enum(['text', 'markdown', 'richtext', 'auto'], {
|
|
13
|
-
description: 'Message format for bot responses (richtext is an alias for markdown, auto detects markdown features)',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export const longTextModeSchema = z.enum(['chunk', 'file']);
|
|
17
|
-
|
|
18
|
-
// Per-account config schema (everything that can be overridden per account)
|
|
19
|
-
export const dingTalkAccountConfigSchema = z.object({
|
|
20
|
-
enabled: z.boolean().default(true).describe('Enable this account'),
|
|
21
|
-
name: z.string().optional().describe('Display name for this account'),
|
|
22
|
-
|
|
23
|
-
// Credentials
|
|
24
|
-
clientId: z.string().optional()
|
|
25
|
-
.describe('DingTalk application AppKey'),
|
|
26
|
-
clientSecret: z.string().optional()
|
|
27
|
-
.describe('DingTalk application AppSecret'),
|
|
28
|
-
robotCode: z.string().optional()
|
|
29
|
-
.describe('Robot code (optional, defaults to clientId if not provided)'),
|
|
30
|
-
|
|
31
|
-
// DM config
|
|
32
|
-
dm: z.object({
|
|
33
|
-
enabled: z.boolean().default(true)
|
|
34
|
-
.describe('Enable direct messages'),
|
|
35
|
-
policy: dmPolicySchema.default('pairing')
|
|
36
|
-
.describe(
|
|
37
|
-
'Access control policy:\n' +
|
|
38
|
-
' - disabled: No DM allowed\n' +
|
|
39
|
-
' - pairing: Show staffId on first contact, admin adds to allowlist\n' +
|
|
40
|
-
' - allowlist: Only specified users can DM\n' +
|
|
41
|
-
' - open: Anyone can DM (not recommended)'
|
|
42
|
-
),
|
|
43
|
-
allowFrom: z.array(z.string()).default([])
|
|
44
|
-
.describe('Allowed staff IDs (for pairing/allowlist policy)'),
|
|
45
|
-
}).default({}),
|
|
46
|
-
|
|
47
|
-
// Group config
|
|
48
|
-
groupPolicy: groupPolicySchema.default('allowlist')
|
|
49
|
-
.describe(
|
|
50
|
-
'Group chat policy:\n' +
|
|
51
|
-
' - disabled: No groups\n' +
|
|
52
|
-
' - allowlist: Only specified groups\n' +
|
|
53
|
-
' - open: All groups'
|
|
54
|
-
),
|
|
55
|
-
groupAllowlist: z.array(z.string()).default([])
|
|
56
|
-
.describe('Allowed group conversation IDs (only used when groupPolicy is "allowlist")'),
|
|
57
|
-
requireMention: z.boolean().default(true)
|
|
58
|
-
.describe('Require @ mention in group chats'),
|
|
59
|
-
|
|
60
|
-
// Message format
|
|
61
|
-
messageFormat: messageFormatSchema.default('text')
|
|
62
|
-
.describe(
|
|
63
|
-
'Message format:\n' +
|
|
64
|
-
' - text: Plain text (recommended, supports tables)\n' +
|
|
65
|
-
' - markdown: DingTalk markdown (limited support, no tables)\n' +
|
|
66
|
-
' - richtext: Alias for markdown (deprecated, use markdown instead)\n' +
|
|
67
|
-
' - auto: Auto-detect markdown features in response'
|
|
68
|
-
),
|
|
69
|
-
|
|
70
|
-
// Thinking feedback
|
|
71
|
-
showThinking: z.boolean().default(false)
|
|
72
|
-
.describe('Send "正在思考..." feedback before AI responds'),
|
|
73
|
-
|
|
74
|
-
// Advanced
|
|
75
|
-
textChunkLimit: z.number().int().positive().default(2000).optional()
|
|
76
|
-
.describe('Text chunk size limit for long messages'),
|
|
77
|
-
|
|
78
|
-
longTextMode: longTextModeSchema.default('chunk')
|
|
79
|
-
.describe(
|
|
80
|
-
'How to handle long text messages:\n' +
|
|
81
|
-
' - chunk: Split into multiple messages (default, same as official channels)\n' +
|
|
82
|
-
' - file: Convert to .md file and send as attachment'
|
|
83
|
-
),
|
|
84
|
-
longTextThreshold: z.number().int().positive().default(8000).optional()
|
|
85
|
-
.describe('Character threshold for longTextMode=file (default 8000)'),
|
|
86
|
-
|
|
87
|
-
// Per-group overrides
|
|
88
|
-
groups: z.record(z.string(), z.object({
|
|
89
|
-
systemPrompt: z.string().optional()
|
|
90
|
-
.describe('Per-group extra system prompt injected as GroupSystemPrompt'),
|
|
91
|
-
enabled: z.boolean().optional()
|
|
92
|
-
.describe('Disable this group (false = ignore all messages)'),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.describe('
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*
|
|
122
|
-
* @
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// 定义枚举类型
|
|
4
|
+
export const dmPolicySchema = z.enum(['disabled', 'pairing', 'allowlist', 'open'], {
|
|
5
|
+
description: 'DM access control policy',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const groupPolicySchema = z.enum(['disabled', 'allowlist', 'open'], {
|
|
9
|
+
description: 'Group chat access control policy',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const messageFormatSchema = z.enum(['text', 'markdown', 'richtext', 'auto'], {
|
|
13
|
+
description: 'Message format for bot responses (richtext is an alias for markdown, auto detects markdown features)',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const longTextModeSchema = z.enum(['chunk', 'file']);
|
|
17
|
+
|
|
18
|
+
// Per-account config schema (everything that can be overridden per account)
|
|
19
|
+
export const dingTalkAccountConfigSchema = z.object({
|
|
20
|
+
enabled: z.boolean().default(true).describe('Enable this account'),
|
|
21
|
+
name: z.string().optional().describe('Display name for this account'),
|
|
22
|
+
|
|
23
|
+
// Credentials
|
|
24
|
+
clientId: z.string().optional()
|
|
25
|
+
.describe('DingTalk application AppKey'),
|
|
26
|
+
clientSecret: z.string().optional()
|
|
27
|
+
.describe('DingTalk application AppSecret'),
|
|
28
|
+
robotCode: z.string().optional()
|
|
29
|
+
.describe('Robot code (optional, defaults to clientId if not provided)'),
|
|
30
|
+
|
|
31
|
+
// DM config
|
|
32
|
+
dm: z.object({
|
|
33
|
+
enabled: z.boolean().default(true)
|
|
34
|
+
.describe('Enable direct messages'),
|
|
35
|
+
policy: dmPolicySchema.default('pairing')
|
|
36
|
+
.describe(
|
|
37
|
+
'Access control policy:\n' +
|
|
38
|
+
' - disabled: No DM allowed\n' +
|
|
39
|
+
' - pairing: Show staffId on first contact, admin adds to allowlist\n' +
|
|
40
|
+
' - allowlist: Only specified users can DM\n' +
|
|
41
|
+
' - open: Anyone can DM (not recommended)'
|
|
42
|
+
),
|
|
43
|
+
allowFrom: z.array(z.string()).default([])
|
|
44
|
+
.describe('Allowed staff IDs (for pairing/allowlist policy)'),
|
|
45
|
+
}).default({}),
|
|
46
|
+
|
|
47
|
+
// Group config
|
|
48
|
+
groupPolicy: groupPolicySchema.default('allowlist')
|
|
49
|
+
.describe(
|
|
50
|
+
'Group chat policy:\n' +
|
|
51
|
+
' - disabled: No groups\n' +
|
|
52
|
+
' - allowlist: Only specified groups\n' +
|
|
53
|
+
' - open: All groups'
|
|
54
|
+
),
|
|
55
|
+
groupAllowlist: z.array(z.string()).default([])
|
|
56
|
+
.describe('Allowed group conversation IDs (only used when groupPolicy is "allowlist")'),
|
|
57
|
+
requireMention: z.boolean().default(true)
|
|
58
|
+
.describe('Require @ mention in group chats'),
|
|
59
|
+
|
|
60
|
+
// Message format
|
|
61
|
+
messageFormat: messageFormatSchema.default('text')
|
|
62
|
+
.describe(
|
|
63
|
+
'Message format:\n' +
|
|
64
|
+
' - text: Plain text (recommended, supports tables)\n' +
|
|
65
|
+
' - markdown: DingTalk markdown (limited support, no tables)\n' +
|
|
66
|
+
' - richtext: Alias for markdown (deprecated, use markdown instead)\n' +
|
|
67
|
+
' - auto: Auto-detect markdown features in response'
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
// Thinking feedback
|
|
71
|
+
showThinking: z.boolean().default(false)
|
|
72
|
+
.describe('Send "正在思考..." feedback before AI responds'),
|
|
73
|
+
|
|
74
|
+
// Advanced
|
|
75
|
+
textChunkLimit: z.number().int().positive().default(2000).optional()
|
|
76
|
+
.describe('Text chunk size limit for long messages'),
|
|
77
|
+
|
|
78
|
+
longTextMode: longTextModeSchema.default('chunk')
|
|
79
|
+
.describe(
|
|
80
|
+
'How to handle long text messages:\n' +
|
|
81
|
+
' - chunk: Split into multiple messages (default, same as official channels)\n' +
|
|
82
|
+
' - file: Convert to .md file and send as attachment'
|
|
83
|
+
),
|
|
84
|
+
longTextThreshold: z.number().int().positive().default(8000).optional()
|
|
85
|
+
.describe('Character threshold for longTextMode=file (default 8000)'),
|
|
86
|
+
|
|
87
|
+
// Per-group overrides
|
|
88
|
+
groups: z.record(z.string(), z.object({
|
|
89
|
+
systemPrompt: z.string().optional()
|
|
90
|
+
.describe('Per-group extra system prompt injected as GroupSystemPrompt'),
|
|
91
|
+
enabled: z.boolean().optional()
|
|
92
|
+
.describe('Disable this group (false = ignore all messages)'),
|
|
93
|
+
allowFrom: z.array(z.string()).optional()
|
|
94
|
+
.describe('Only respond to messages from these senderStaffIds in this group (supports "*" wildcard). If omitted, all senders are allowed.'),
|
|
95
|
+
})).optional().default({})
|
|
96
|
+
.describe('Per-group overrides keyed by conversationId'),
|
|
97
|
+
|
|
98
|
+
// Message aggregation
|
|
99
|
+
messageAggregation: z.boolean().default(true)
|
|
100
|
+
.describe(
|
|
101
|
+
'Aggregate messages from the same sender within a short time window.\n' +
|
|
102
|
+
'Useful when DingTalk splits link cards into multiple messages.'
|
|
103
|
+
),
|
|
104
|
+
messageAggregationDelayMs: z.number().int().positive().default(2000).optional()
|
|
105
|
+
.describe('Time window in milliseconds to wait for additional messages (default 2000)'),
|
|
106
|
+
}).passthrough();
|
|
107
|
+
|
|
108
|
+
// Top-level DingTalk config: account config + multi-account support
|
|
109
|
+
export const dingTalkConfigSchema = dingTalkAccountConfigSchema.extend({
|
|
110
|
+
accounts: z.record(z.string(), dingTalkAccountConfigSchema.partial().optional()).optional()
|
|
111
|
+
.describe('Named accounts that override top-level settings'),
|
|
112
|
+
defaultAccount: z.string().optional()
|
|
113
|
+
.describe('Default account ID (if accounts are defined)'),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 导出配置类型
|
|
117
|
+
export type DingTalkAccountConfig = z.infer<typeof dingTalkAccountConfigSchema>;
|
|
118
|
+
export type DingTalkConfig = z.infer<typeof dingTalkConfigSchema>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 验证 DingTalk 配置
|
|
122
|
+
* @param config - 原始配置对象
|
|
123
|
+
* @returns 验证后的配置
|
|
124
|
+
* @throws ZodError 如果配置无效
|
|
125
|
+
*/
|
|
126
|
+
export function validateDingTalkConfig(config: unknown): DingTalkConfig {
|
|
127
|
+
try {
|
|
128
|
+
return dingTalkConfigSchema.parse(config);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof z.ZodError) {
|
|
131
|
+
const formatted = error.issues.map(e => {
|
|
132
|
+
const path = e.path.join('.');
|
|
133
|
+
return ` - ${path || 'root'}: ${e.message}`;
|
|
134
|
+
}).join('\n');
|
|
135
|
+
throw new Error(`DingTalk config validation failed:\n${formatted}`);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate per-account config (partial, used for account overrides)
|
|
143
|
+
*/
|
|
144
|
+
export function validateDingTalkAccountConfig(config: unknown): DingTalkAccountConfig {
|
|
145
|
+
return dingTalkAccountConfigSchema.parse(config);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 安全地验证配置,返回错误而不抛出异常
|
|
150
|
+
* @param config - 原始配置对象
|
|
151
|
+
* @returns { success: true, data } 或 { success: false, error }
|
|
152
|
+
*/
|
|
153
|
+
export function safeValidateDingTalkConfig(config: unknown):
|
|
154
|
+
| { success: true; data: DingTalkConfig }
|
|
155
|
+
| { success: false; error: string } {
|
|
156
|
+
try {
|
|
157
|
+
const data = dingTalkConfigSchema.parse(config);
|
|
158
|
+
return { success: true, data };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof z.ZodError) {
|
|
161
|
+
const formatted = error.issues.map(e => {
|
|
162
|
+
const path = e.path.join('.');
|
|
163
|
+
return `${path || 'root'}: ${e.message}`;
|
|
164
|
+
}).join('; ');
|
|
165
|
+
return { success: false, error: formatted };
|
|
166
|
+
}
|
|
167
|
+
return { success: false, error: String(error) };
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1231,6 +1231,17 @@ async function processInboundMessage(
|
|
|
1231
1231
|
// Check @mention requirement
|
|
1232
1232
|
const requireMention = account.config.requireMention !== false;
|
|
1233
1233
|
if (requireMention && !msg.isInAtList) return;
|
|
1234
|
+
|
|
1235
|
+
// Check per-group sender allowlist
|
|
1236
|
+
const groupConfig = (account.config.groups ?? {})[conversationId];
|
|
1237
|
+
const groupAllowFrom = groupConfig?.allowFrom;
|
|
1238
|
+
if (groupAllowFrom && groupAllowFrom.length > 0) {
|
|
1239
|
+
log?.info?.("[dingtalk] Group allowFrom check: senderId=" + senderId + " allowFrom=" + JSON.stringify(groupAllowFrom));
|
|
1240
|
+
if (!isSenderAllowed(senderId, groupAllowFrom)) {
|
|
1241
|
+
log?.info?.("[dingtalk] Group sender not in allowFrom: " + senderId + " for group " + conversationId);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1234
1245
|
}
|
|
1235
1246
|
|
|
1236
1247
|
const sessionKey = "dingtalk:" + account.accountId + ":" + (isDm ? "dm" : "group") + ":" + conversationId;
|
|
@@ -1374,21 +1385,20 @@ async function dispatchMessage(params: {
|
|
|
1374
1385
|
const queueKey = `${account.accountId}:${conversationId}`;
|
|
1375
1386
|
const isQueueBusy = sessionQueues.has(queueKey);
|
|
1376
1387
|
|
|
1377
|
-
// If queue is busy,
|
|
1388
|
+
// If queue is busy, add emotion reaction on user's message to indicate queued
|
|
1378
1389
|
let queueAckCleanup: (() => Promise<void>) | null = null;
|
|
1379
1390
|
if (isQueueBusy) {
|
|
1380
|
-
|
|
1381
|
-
log?.info?.("[dingtalk] Queue busy for " + queueKey + ", notifying user");
|
|
1391
|
+
log?.info?.("[dingtalk] Queue busy for " + queueKey + ", adding queue reaction");
|
|
1382
1392
|
try {
|
|
1383
|
-
if (account.clientId && account.clientSecret) {
|
|
1393
|
+
if (account.clientId && account.clientSecret && params.msg.msgId && conversationId) {
|
|
1384
1394
|
const robotCode = account.robotCode || account.clientId;
|
|
1385
|
-
const result = await
|
|
1395
|
+
const result = await addEmotionReply({
|
|
1386
1396
|
clientId: account.clientId,
|
|
1387
1397
|
clientSecret: account.clientSecret,
|
|
1388
1398
|
robotCode,
|
|
1389
|
-
|
|
1390
|
-
conversationId
|
|
1391
|
-
|
|
1399
|
+
msgId: params.msg.msgId,
|
|
1400
|
+
conversationId,
|
|
1401
|
+
emotionName: '⏳排队中',
|
|
1392
1402
|
});
|
|
1393
1403
|
if (!result.error) {
|
|
1394
1404
|
queueAckCleanup = result.cleanup;
|
|
@@ -1527,16 +1537,16 @@ async function dispatchMessageInternal(params: {
|
|
|
1527
1537
|
runtime?.channel?.reply?.dispatchReplyFromConfig
|
|
1528
1538
|
);
|
|
1529
1539
|
|
|
1530
|
-
// Track if we've already cleaned up the
|
|
1540
|
+
// Track if we've already cleaned up the thinking feedback
|
|
1531
1541
|
let typingCleaned = false;
|
|
1532
1542
|
const cleanupTyping = async () => {
|
|
1533
1543
|
if (typingCleanup && !typingCleaned) {
|
|
1534
1544
|
typingCleaned = true;
|
|
1535
1545
|
try {
|
|
1536
1546
|
await typingCleanup();
|
|
1537
|
-
log?.info?.('[dingtalk]
|
|
1547
|
+
log?.info?.('[dingtalk] Thinking feedback recalled');
|
|
1538
1548
|
} catch (err) {
|
|
1539
|
-
log?.info?.('[dingtalk] Failed to recall
|
|
1549
|
+
log?.info?.('[dingtalk] Failed to recall thinking feedback: ' + err);
|
|
1540
1550
|
}
|
|
1541
1551
|
}
|
|
1542
1552
|
};
|