@raolin2025/claude-code-node 2.7.3 → 2.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.7.3",
3
+ "version": "2.7.5",
4
4
  "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
5
5
  "type": "module",
6
6
  "main": "src/core/index.js",
@@ -283,6 +283,50 @@ async function createTelegramListener(config) {
283
283
 
284
284
 
285
285
 
286
+ // ============================================================
287
+ // Telegram typing 心跳 — 让"正在输入"持续显示直到处理完成
288
+ // ============================================================
289
+
290
+ /**
291
+ * 发送一次 Telegram typing 动作。
292
+ * 兼容代理 / 直连两种方式。
293
+ */
294
+ async function tgSendTyping(config, chatId) {
295
+ if (!config.channels?.telegram?.token || !chatId) return
296
+ try {
297
+ const proxyAddr = config.channels.telegram.proxy || process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
298
+ const apiBase = config.channels.telegram.apiBase || `https://api.telegram.org`
299
+ const url = `${apiBase}/bot${config.channels.telegram.token}/sendChatAction`
300
+ const body = JSON.stringify({ chat_id: chatId, action: 'typing' })
301
+ if (proxyAddr) {
302
+ const { fetchViaSocks5 } = await import('./tg-proxy.js')
303
+ await fetchViaSocks5(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }, proxyAddr)
304
+ } else {
305
+ await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body })
306
+ }
307
+ } catch {}
308
+ }
309
+
310
+ /**
311
+ * 启动 typing 心跳:Telegram 的 typing 提示约 5 秒后自动消失,
312
+ * 若处理耗时较长(cc-node 多轮思考 / 工具执行),需周期重发才能持续显示。
313
+ * 返回停止函数,处理结束后调用即可让"正在输入"消失。
314
+ */
315
+ function tgStartTypingHeartbeat(config, chatId) {
316
+ if (!config.channels?.telegram?.token || !chatId) return () => {}
317
+ let stopped = false
318
+ const timer = setInterval(async () => {
319
+ if (stopped) return
320
+ await tgSendTyping(config, chatId)
321
+ }, 4000)
322
+ // 立即发送一次,让提示马上出现
323
+ tgSendTyping(config, chatId)
324
+ return () => {
325
+ stopped = true
326
+ clearInterval(timer)
327
+ }
328
+ }
329
+
286
330
  // ============================================================
287
331
  // 统一消息处理器
288
332
  // ============================================================
@@ -434,27 +478,10 @@ function createMessageHandler(config) {
434
478
  // ============================================================
435
479
  log(`[route] processing: "${text.slice(0, 50)}${text.length > 50 ? '...' : ''}"`)
436
480
 
437
- // 发送"处理中"提示
481
+ // 发送"处理中"提示 — 启动 typing 心跳,让"正在输入"持续显示直到处理完成
482
+ let stopTyping = () => {}
438
483
  if (isTelegram && config.channels.telegram?.token) {
439
- try {
440
- const proxyAddr = config.channels.telegram.proxy || process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
441
- const apiBase = config.channels.telegram.apiBase || `https://api.telegram.org`
442
- const url = `${apiBase}/bot${config.channels.telegram.token}/sendChatAction`
443
- if (proxyAddr) {
444
- const { fetchViaSocks5 } = await import('./tg-proxy.js')
445
- await fetchViaSocks5(url, {
446
- method: 'POST',
447
- headers: { 'Content-Type': 'application/json' },
448
- body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
449
- }, proxyAddr)
450
- } else {
451
- await fetch(url, {
452
- method: 'POST',
453
- headers: { 'Content-Type': 'application/json' },
454
- body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
455
- })
456
- }
457
- } catch {}
484
+ stopTyping = tgStartTypingHeartbeat(config, chatId)
458
485
  }
459
486
  if (isQQBot) {
460
487
  await sendToChannel(config.channels, 'qqbot', '🤖 收到,正在处理...')
@@ -462,6 +489,8 @@ function createMessageHandler(config) {
462
489
 
463
490
  try {
464
491
  const result = await routeMessage(text, config)
492
+ // 处理完成,停掉 typing 心跳 → "正在输入" 提示消失
493
+ stopTyping()
465
494
  const reply = result || '(no response)'
466
495
 
467
496
  if (isTelegram && config.channels.telegram?.token) {
@@ -494,6 +523,8 @@ function createMessageHandler(config) {
494
523
  log(`[route] done (${reply.length} chars)`)
495
524
 
496
525
  } catch (e) {
526
+ // 出错也停掉 typing 心跳,避免提示一直挂起
527
+ stopTyping()
497
528
  log(`[route] error: ${e.message}`)
498
529
  const errMsg = `❌ Error processing: ${e.message}`
499
530
  if (isTelegram && config.channels.telegram?.token) {
package/src/core/cli.js CHANGED
@@ -1008,16 +1008,42 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1008
1008
  // 让远程用户看到 AI 正在工作,而不是"以为没回应"。
1009
1009
  // 采用节流 + 编辑同一消息的方式,避免刷屏和触发速率限制。
1010
1010
  // ============================================================
1011
- const tgThinking = { buffer: '', timer: null, target: null, lastMsgId: null, flushing: false }
1011
+ const tgThinking = { buffer: '', timer: null, target: null, lastMsgId: null, flushing: false, typingTimer: null, lastResetAt: 0 }
1012
+ // 周期性重置"🧠 思考中…"消息,避免它无限膨胀成一个无法判断是否仍在工作的静态块。
1013
+ // 每隔 RESET_MS 就删掉旧消息、发一条新的,让用户每隔几秒看到一次明确的活动信号。
1014
+ const TG_THINKING_RESET_MS = 5000
1015
+
1016
+ // Telegram 的 typing 提示约 5 秒后自动消失。若 cc-node 处理耗时较长
1017
+ //(如执行工具、读取文件、多轮思考),单次 sendChatAction 撑不住整个周期,
1018
+ // 用户会看到"正在输入"消失,误以为已完成/卡死。
1019
+ // 因此用一个心跳定时器在整轮处理期间每隔几秒重发一次 typing 动作,
1020
+ // 让"正在输入"持续显示,直到本轮处理完全结束(tgThinkingEnd 清掉定时器)。
1021
+ const TG_TYPING_HEARTBEAT_MS = 4000
1022
+
1023
+ function tgStartTypingHeartbeat() {
1024
+ // 停止旧的,避免重复
1025
+ if (tgThinking.typingTimer) { clearInterval(tgThinking.typingTimer); tgThinking.typingTimer = null }
1026
+ if (!tgListener?.bot || !tgThinking.target) return
1027
+ tgThinking.typingTimer = setInterval(() => {
1028
+ if (!tgThinking.target) { tgStopTypingHeartbeat(); return }
1029
+ tgListener.bot.sendChatAction(tgThinking.target, 'typing').catch(() => {})
1030
+ }, TG_TYPING_HEARTBEAT_MS)
1031
+ }
1032
+
1033
+ function tgStopTypingHeartbeat() {
1034
+ if (tgThinking.typingTimer) { clearInterval(tgThinking.typingTimer); tgThinking.typingTimer = null }
1035
+ }
1012
1036
 
1013
1037
  function tgThinkingStart(target) {
1014
1038
  tgThinking.target = target || null
1015
1039
  tgThinking.buffer = ''
1016
1040
  tgThinking.lastMsgId = null
1017
1041
  tgThinking.flushing = false
1018
- // 先发送 typing 动作,让 Telegram 立即显示"正在输入"
1042
+ tgThinking.lastResetAt = Date.now()
1043
+ // 先发送 typing 动作,让 Telegram 立即显示"正在输入",并启动心跳保持持续显示
1019
1044
  if (tgListener?.bot && tgThinking.target) {
1020
1045
  tgListener.bot.sendChatAction(tgThinking.target, 'typing').catch(() => {})
1046
+ tgStartTypingHeartbeat()
1021
1047
  }
1022
1048
  }
1023
1049
 
@@ -1040,6 +1066,14 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1040
1066
  const target = tgThinking.target
1041
1067
  const body = `🧠 思考中…\n\n${tgThinking.buffer.slice(-3500)}`
1042
1068
  try {
1069
+ // 周期性重置:若已有一条消息且距上次重置超过阈值,删除旧消息并发新消息,
1070
+ // 让用户每隔几秒看到"🧠 思考中…"重新出现,明确 AI 仍在工作(而非已卡死/完成)。
1071
+ const needReset = tgThinking.lastMsgId && (Date.now() - tgThinking.lastResetAt) >= TG_THINKING_RESET_MS
1072
+ if (needReset) {
1073
+ tgListener.bot.deleteMessage(target, tgThinking.lastMsgId).catch(() => {})
1074
+ tgThinking.lastMsgId = null
1075
+ tgThinking.lastResetAt = Date.now()
1076
+ }
1043
1077
  if (tgThinking.lastMsgId) {
1044
1078
  await tgListener.bot.editMessage(target, tgThinking.lastMsgId, body, { parseMode: 'HTML' })
1045
1079
  } else {
@@ -1060,6 +1094,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1060
1094
 
1061
1095
  function tgThinkingEnd() {
1062
1096
  if (tgThinking.timer) { clearTimeout(tgThinking.timer); tgThinking.timer = null }
1097
+ // 处理结束,停掉 typing 心跳 → "正在输入" 提示消失
1098
+ tgStopTypingHeartbeat()
1063
1099
  tgThinking.buffer = ''
1064
1100
  tgThinking.lastMsgId = null
1065
1101
  tgThinking.target = null