|
1 | 1 | import type { IMessageSender } from '../bridge/message-sender.interface.js'; |
2 | | -import type { CardState, CardStatus } from '../types.js'; |
| 2 | +import type { CardState } from '../types.js'; |
3 | 3 | import type { Logger } from '../utils/logger.js'; |
4 | 4 | import type { WechatClient } from './wechat-client.js'; |
5 | 5 |
|
6 | 6 | const MAX_TEXT_LENGTH = 4000; |
7 | | -const PROGRESS_THROTTLE_MS = 5_000; |
| 7 | +/** |
| 8 | + * Progress heartbeat throttle. WeChat can't edit messages, so every progress |
| 9 | + * tick is a fresh text in the chat — keep it sparse. 30 s gives long-running |
| 10 | + * tasks visible signal ("it's still going") without flooding the thread. |
| 11 | + */ |
| 12 | +const PROGRESS_THROTTLE_MS = 30_000; |
8 | 13 |
|
9 | 14 | /** |
10 | 15 | * WeChat implementation of IMessageSender. |
@@ -66,19 +71,22 @@ export class WechatSender implements IMessageSender { |
66 | 71 | return true; |
67 | 72 | } |
68 | 73 |
|
69 | | - // Intermediate: send tool progress as actual messages (throttled) |
| 74 | + // Intermediate: single-line heartbeat ("running... N tools"), throttled. |
| 75 | + // Previous behavior dumped tool names + details for every new tool every |
| 76 | + // 5 s — way too noisy. Now we emit at most one terse status line per |
| 77 | + // PROGRESS_THROTTLE_MS, only when the tool count has grown since the |
| 78 | + // last heartbeat, so long runs still show signs of life without flooding |
| 79 | + // the chat. |
70 | 80 | const now = Date.now(); |
71 | 81 | const lastProgress = this.lastProgressSent.get(messageId) || 0; |
72 | 82 | const reported = this.reportedToolCount.get(messageId) || 0; |
73 | 83 | const hasNewTools = state.toolCalls.length > reported; |
74 | 84 |
|
75 | 85 | if (hasNewTools && now - lastProgress > PROGRESS_THROTTLE_MS) { |
76 | 86 | this.lastProgressSent.set(messageId, now); |
77 | | - // Only report new tool calls since last update |
78 | | - const newTools = state.toolCalls.slice(reported); |
79 | 87 | this.reportedToolCount.set(messageId, state.toolCalls.length); |
80 | 88 |
|
81 | | - const text = this.renderProgressMessage(newTools, state.status); |
| 89 | + const text = this.renderHeartbeatMessage(state); |
82 | 90 | await this.client.sendTextMessage(chatId, text).catch((err) => { |
83 | 91 | this.logger.debug({ err, chatId }, 'Failed to send WeChat progress (may lack context_token)'); |
84 | 92 | }); |
@@ -144,19 +152,17 @@ export class WechatSender implements IMessageSender { |
144 | 152 |
|
145 | 153 | // --- Rendering --- |
146 | 154 |
|
147 | | - /** Progress message for new tool calls since last update. */ |
148 | | - private renderProgressMessage(newTools: CardState['toolCalls'], status: CardState['status']): string { |
149 | | - const parts: string[] = []; |
150 | | - const label = status === 'thinking' ? '🤔 思考中...' : '🔧 运行中...'; |
151 | | - parts.push(label); |
152 | | - |
153 | | - for (const t of newTools.slice(-5)) { |
154 | | - const icon = t.status === 'done' ? '✓' : '⏳'; |
155 | | - const detail = t.detail.length > 80 ? t.detail.slice(0, 80) + '...' : t.detail; |
156 | | - parts.push(`${icon} ${t.name} ${detail}`); |
157 | | - } |
158 | | - |
159 | | - return parts.join('\n'); |
| 155 | + /** |
| 156 | + * Heartbeat message — single line, no per-tool details. Sent at most once |
| 157 | + * per PROGRESS_THROTTLE_MS so the WeChat thread stays clean. Just enough |
| 158 | + * signal to tell the user the bot hasn't died. |
| 159 | + */ |
| 160 | + private renderHeartbeatMessage(state: CardState): string { |
| 161 | + const label = state.status === 'thinking' ? '🤔 思考中' : '🔧 运行中'; |
| 162 | + const total = state.toolCalls.length; |
| 163 | + if (total === 0) return label; |
| 164 | + const last = state.toolCalls[state.toolCalls.length - 1]; |
| 165 | + return `${label}:${last.name} · ${total} tool${total > 1 ? 's' : ''}`; |
160 | 166 | } |
161 | 167 |
|
162 | 168 | /** Final message: just the response text (or error). */ |
|
0 commit comments