chenbhao commited on
Commit
ceeca02
·
1 Parent(s): 6a0f3a8

feat: live refresh typing status

Browse files
src/hooks/useTelegramBridge.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from 'react'
2
  import type { AppStateStore } from '../state/AppState.js'
3
  import { telegramService } from '../services/telegram/TelegramService.js'
4
  import {
@@ -44,6 +44,8 @@ type ActiveTelegramTurn = {
44
  responseParts: string[]
45
  }
46
 
 
 
47
  function stripXmlTags(text: string): string {
48
  return text.replace(/<[^>]+>/g, '').trim()
49
  }
@@ -54,6 +56,55 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
54
  const lastProcessedMessageCountRef = useRef(messages.length)
55
  const previousLoadingRef = useRef(isLoading)
56
  const autoStartAttemptedRef = useRef(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  useEffect(() => {
59
  if (autoStartAttemptedRef.current) return
@@ -65,6 +116,16 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
65
  })
66
  }, [])
67
 
 
 
 
 
 
 
 
 
 
 
68
  useEffect(() => {
69
  return telegramService.subscribeToInbound(event => {
70
  void (async () => {
@@ -78,6 +139,8 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
78
  pendingInboundRef.current.push(event)
79
  logTelegramDebug(`[telegram] added to pendingInboundRef: chatId=${event.chatId}`)
80
 
 
 
81
  enqueue({
82
  value: event.text,
83
  mode: 'prompt',
@@ -99,7 +162,7 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
99
  }
100
  })()
101
  })
102
- }, [store])
103
 
104
  useEffect(() => {
105
  return telegramService.subscribeToCallbacks(event => {
@@ -142,6 +205,7 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
142
  chatId: inbound.chatId,
143
  responseParts: [],
144
  }
 
145
  logTelegramDebug(`[telegram] active turn started for chatId: ${inbound.chatId}`)
146
  }
147
  continue
@@ -171,7 +235,7 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
171
  }
172
 
173
  lastProcessedMessageCountRef.current = messages.length
174
- }, [messages])
175
 
176
  useEffect(() => {
177
  const wasLoading = previousLoadingRef.current
@@ -183,28 +247,26 @@ export function useTelegramBridge({ messages, isLoading, store }: Props): void {
183
  return
184
  }
185
 
186
- const { chatId, responseParts } = activeTurnRef.current
187
- const messageToSend = responseParts.join('\n\n').trim() ||
188
- '这一轮没有可回传的文本结果,请查看本地终端会话。'
189
-
190
- logTelegramDebug(`[telegram] sending message to chatId: ${chatId}, parts count: ${responseParts.length}, total length: ${messageToSend.length}`)
191
- logTelegramDebug(`[telegram] message content: ${JSON.stringify(messageToSend)}`)
192
- logTelegramDebug(`[telegram] response parts: ${JSON.stringify(responseParts)}`)
193
-
194
  activeTurnRef.current = null
195
 
196
- void telegramService
197
- .sendMessage(
198
- chatId,
199
- messageToSend,
200
- )
201
- .catch(error => {
 
 
202
  logTelegramDebug(
203
  `[telegram] failed to send outbound reply: ${
204
  error instanceof Error ? error.message : String(error)
205
  }`,
206
  'error',
207
  )
208
- })
209
- }, [isLoading])
 
 
 
210
  }
 
1
+ import { useCallback, useEffect, useRef } from 'react'
2
  import type { AppStateStore } from '../state/AppState.js'
3
  import { telegramService } from '../services/telegram/TelegramService.js'
4
  import {
 
44
  responseParts: string[]
45
  }
46
 
47
+ const TELEGRAM_TYPING_REFRESH_MS = 4000
48
+
49
  function stripXmlTags(text: string): string {
50
  return text.replace(/<[^>]+>/g, '').trim()
51
  }
 
56
  const lastProcessedMessageCountRef = useRef(messages.length)
57
  const previousLoadingRef = useRef(isLoading)
58
  const autoStartAttemptedRef = useRef(false)
59
+ const typingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
60
+ const typingChatIdRef = useRef<string | null>(null)
61
+
62
+ const stopTypingKeepAlive = useCallback(() => {
63
+ if (typingIntervalRef.current) {
64
+ clearInterval(typingIntervalRef.current)
65
+ typingIntervalRef.current = null
66
+ }
67
+ typingChatIdRef.current = null
68
+ }, [])
69
+
70
+ const sendTyping = useCallback(
71
+ (chatId: string) => {
72
+ void telegramService.sendChatAction(chatId, 'typing').catch(error => {
73
+ logTelegramDebug(
74
+ `[telegram] failed to send typing action: ${
75
+ error instanceof Error ? error.message : String(error)
76
+ }`,
77
+ 'error',
78
+ )
79
+ if (
80
+ error instanceof Error &&
81
+ error.message === 'Telegram service is not running'
82
+ ) {
83
+ stopTypingKeepAlive()
84
+ }
85
+ })
86
+ },
87
+ [stopTypingKeepAlive],
88
+ )
89
+
90
+ const startTypingKeepAlive = useCallback(
91
+ (chatId: string) => {
92
+ if (
93
+ typingChatIdRef.current === chatId &&
94
+ typingIntervalRef.current !== null
95
+ ) {
96
+ return
97
+ }
98
+
99
+ stopTypingKeepAlive()
100
+ typingChatIdRef.current = chatId
101
+ sendTyping(chatId)
102
+ typingIntervalRef.current = setInterval(() => {
103
+ sendTyping(chatId)
104
+ }, TELEGRAM_TYPING_REFRESH_MS)
105
+ },
106
+ [sendTyping, stopTypingKeepAlive],
107
+ )
108
 
109
  useEffect(() => {
110
  if (autoStartAttemptedRef.current) return
 
116
  })
117
  }, [])
118
 
119
+ useEffect(() => stopTypingKeepAlive, [stopTypingKeepAlive])
120
+
121
+ useEffect(() => {
122
+ return telegramService.subscribe(() => {
123
+ if (telegramService.getStateSnapshot().status !== 'running') {
124
+ stopTypingKeepAlive()
125
+ }
126
+ })
127
+ }, [stopTypingKeepAlive])
128
+
129
  useEffect(() => {
130
  return telegramService.subscribeToInbound(event => {
131
  void (async () => {
 
139
  pendingInboundRef.current.push(event)
140
  logTelegramDebug(`[telegram] added to pendingInboundRef: chatId=${event.chatId}`)
141
 
142
+ sendTyping(event.chatId)
143
+
144
  enqueue({
145
  value: event.text,
146
  mode: 'prompt',
 
162
  }
163
  })()
164
  })
165
+ }, [sendTyping, store])
166
 
167
  useEffect(() => {
168
  return telegramService.subscribeToCallbacks(event => {
 
205
  chatId: inbound.chatId,
206
  responseParts: [],
207
  }
208
+ startTypingKeepAlive(inbound.chatId)
209
  logTelegramDebug(`[telegram] active turn started for chatId: ${inbound.chatId}`)
210
  }
211
  continue
 
235
  }
236
 
237
  lastProcessedMessageCountRef.current = messages.length
238
+ }, [messages, startTypingKeepAlive])
239
 
240
  useEffect(() => {
241
  const wasLoading = previousLoadingRef.current
 
247
  return
248
  }
249
 
250
+ const completedTurn = activeTurnRef.current
 
 
 
 
 
 
 
251
  activeTurnRef.current = null
252
 
253
+ void (async () => {
254
+ try {
255
+ await telegramService.sendMessage(
256
+ completedTurn.chatId,
257
+ completedTurn.responseParts.join('\n\n').trim() ||
258
+ '这一轮没有可回传的文本结果,请查看本地终端会话。',
259
+ )
260
+ } catch (error) {
261
  logTelegramDebug(
262
  `[telegram] failed to send outbound reply: ${
263
  error instanceof Error ? error.message : String(error)
264
  }`,
265
  'error',
266
  )
267
+ } finally {
268
+ stopTypingKeepAlive()
269
+ }
270
+ })()
271
+ }, [isLoading, stopTypingKeepAlive])
272
  }
src/services/telegram/TelegramService.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
  TelegramAnswerCallbackQueryResponse,
4
  TelegramBotCommand,
5
  TelegramCallbackEvent,
 
6
  TelegramEditMessageResponse,
7
  TelegramGetMeResponse,
8
  TelegramGetUpdatesResponse,
@@ -10,6 +11,7 @@ import type {
10
  TelegramInlineKeyboardMarkup,
11
  TelegramRuntimeConfig,
12
  TelegramSendMessageResponse,
 
13
  TelegramSetMyCommandsResponse,
14
  TelegramServiceState,
15
  TelegramUpdate,
@@ -305,6 +307,24 @@ class TelegramService {
305
  )
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
  private setState(nextState: TelegramServiceState): void {
310
  this.state = nextState
 
3
  TelegramAnswerCallbackQueryResponse,
4
  TelegramBotCommand,
5
  TelegramCallbackEvent,
6
+ TelegramChatAction,
7
  TelegramEditMessageResponse,
8
  TelegramGetMeResponse,
9
  TelegramGetUpdatesResponse,
 
11
  TelegramInlineKeyboardMarkup,
12
  TelegramRuntimeConfig,
13
  TelegramSendMessageResponse,
14
+ TelegramSendChatActionResponse,
15
  TelegramSetMyCommandsResponse,
16
  TelegramServiceState,
17
  TelegramUpdate,
 
307
  )
308
  }
309
 
310
+ async sendChatAction(
311
+ chatId: string,
312
+ action: TelegramChatAction = 'typing',
313
+ ): Promise<void> {
314
+ if (!this.config) {
315
+ throw new Error('Telegram service is not running')
316
+ }
317
+
318
+ await this.callTelegram<TelegramSendChatActionResponse>(
319
+ this.config,
320
+ 'sendChatAction',
321
+ {
322
+ chat_id: Number(chatId),
323
+ action,
324
+ },
325
+ )
326
+ }
327
+
328
 
329
  private setState(nextState: TelegramServiceState): void {
330
  this.state = nextState
src/services/telegram/telegramTypes.ts CHANGED
@@ -124,3 +124,22 @@ export type TelegramCallbackEvent = {
124
  }
125
 
126
  export type TelegramConfigDraft = Partial<TelegramConfig>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
  export type TelegramConfigDraft = Partial<TelegramConfig>
127
+
128
+ export type TelegramChatAction =
129
+ | 'typing'
130
+ | 'upload_photo'
131
+ | 'record_video'
132
+ | 'upload_video'
133
+ | 'record_voice'
134
+ | 'upload_voice'
135
+ | 'upload_document'
136
+ | 'choose_sticker'
137
+ | 'find_location'
138
+ | 'record_video_note'
139
+ | 'upload_video_note'
140
+
141
+ export type TelegramSendChatActionResponse = {
142
+ ok: boolean
143
+ description?: string
144
+ result?: boolean
145
+ }