@raolin2025/claude-code-node 2.6.12 → 2.6.14

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.6.12",
3
+ "version": "2.6.14",
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",
@@ -385,14 +385,15 @@ export class TelegramListener {
385
385
  }
386
386
  }
387
387
 
388
- /** 将消息加入串行处理队列(不阻塞轮询循环) */
388
+ /** 异步处理一条消息(不串行排队,避免权限确认死锁) */
389
389
  _enqueueMessage(msg) {
390
- this._msgQueue = this._msgQueue.then(async () => {
391
- try {
392
- await this._handleMessage(msg)
393
- } catch (e) {
394
- log(`[TG] handle error: ${e.message}`)
395
- }
390
+ // 关键:不能串行排队等待前一条消息处理完。
391
+ // 若第一条消息触发权限确认而挂起(await pendingConfirm),
392
+ // 后续的权限确认回复 a 会排在后面积压,造成死锁(a 永远处理不到)。
393
+ // 因此每条消息独立异步处理:权限确认回复能立即响应,普通消息由
394
+ // processInputLine 内部处理"引擎忙"的情况。
395
+ this._handleMessage(msg).catch((e) => {
396
+ log(`[TG] handle error: ${e.message}`)
396
397
  })
397
398
  }
398
399
 
package/src/core/cli.js CHANGED
@@ -855,9 +855,24 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
855
855
  return
856
856
  }
857
857
 
858
+ // 发送到引擎 — 引擎忙(如正在处理上一条消息)时,提示而不是崩溃/吞掉
859
+ if (engine.state.isRunning) {
860
+ const busyMsg = '⏳ 引擎正在处理其他任务,请稍候或输入 /stop 停止当前任务。'
861
+ console.log(busyMsg)
862
+ if (source === 'telegram' && tgListener?.bot) {
863
+ await sendTelegram(busyMsg, tgChatId || null).catch(() => {})
864
+ }
865
+ return
866
+ }
867
+
858
868
  // 发送到引擎
859
869
  try {
870
+ // 有 Telegram 通道时,流式推送思维链到当前回复目标,避免远程"以为没回应"
871
+ const streamTarget = (source === 'telegram') ? (tgChatId || null) : (tgListener?.bot ? tgChatId || null : null)
872
+ if (streamTarget) tgThinkingStart(streamTarget)
860
873
  const result = await processInput(input)
874
+ // 处理结束,清掉思维链流(最终回复随后发送)
875
+ tgThinkingEnd()
861
876
  console.log()
862
877
  console.log(result.response)
863
878
  console.log()
@@ -876,6 +891,7 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
876
891
  await sendTelegram(result.response, tgChatId || null)
877
892
  }
878
893
  } catch (err) {
894
+ tgThinkingEnd()
879
895
  console.error(`\nError: ${err.message}\n`)
880
896
  if (tgListener?.bot) {
881
897
  await sendTelegram(`❌ Error: ${err.message}`, tgChatId || null)
@@ -916,6 +932,63 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
916
932
  }
917
933
  }
918
934
 
935
+ // ============================================================
936
+ // 思维链流式推送 → Telegram
937
+ // 把引擎 onDelta 产出的 reasoning(思考过程)实时推送到 Telegram,
938
+ // 让远程用户看到 AI 正在工作,而不是"以为没回应"。
939
+ // 采用节流 + 编辑同一消息的方式,避免刷屏和触发速率限制。
940
+ // ============================================================
941
+ const tgThinking = { buffer: '', timer: null, target: null, lastMsgId: null }
942
+
943
+ function tgThinkingStart(target) {
944
+ tgThinking.target = target || null
945
+ tgThinking.buffer = ''
946
+ tgThinking.lastMsgId = null
947
+ // 先发送 typing 动作,让 Telegram 立即显示"正在输入"
948
+ if (tgListener?.bot && tgThinking.target) {
949
+ tgListener.bot.sendChatAction(tgThinking.target, 'typing').catch(() => {})
950
+ }
951
+ }
952
+
953
+ function tgThinkingPush(text) {
954
+ if (!text || !tgListener?.bot || !tgThinking.target) return
955
+ tgThinking.buffer += text
956
+ // 只保留最近 8000 字符,避免无限增长
957
+ if (tgThinking.buffer.length > 8000) tgThinking.buffer = tgThinking.buffer.slice(-8000)
958
+ // 节流:2 秒内最多刷新一次
959
+ if (tgThinking.timer) clearTimeout(tgThinking.timer)
960
+ tgThinking.timer = setTimeout(() => tgThinkingFlush(), 2000)
961
+ }
962
+
963
+ async function tgThinkingFlush() {
964
+ if (tgThinking.timer) { clearTimeout(tgThinking.timer); tgThinking.timer = null }
965
+ if (!tgListener?.bot || !tgThinking.target || !tgThinking.buffer) return
966
+ const target = tgThinking.target
967
+ const body = `🧠 思考中…\n\n${tgThinking.buffer.slice(-3500)}`
968
+ try {
969
+ if (tgThinking.lastMsgId) {
970
+ await tgListener.bot.editMessage(target, tgThinking.lastMsgId, body, { parseMode: 'HTML' })
971
+ } else {
972
+ const res = await tgListener.bot.sendMessage(target, body, { parseMode: 'HTML' })
973
+ tgThinking.lastMsgId = res?.message_id || null
974
+ }
975
+ } catch (e) {
976
+ // 编辑失败(如消息被删)→ 重发一条新的
977
+ tgThinking.lastMsgId = null
978
+ try {
979
+ const res = await tgListener.bot.sendMessage(target, body, { parseMode: 'HTML' })
980
+ tgThinking.lastMsgId = res?.message_id || null
981
+ } catch {}
982
+ }
983
+ }
984
+
985
+ function tgThinkingEnd() {
986
+ if (tgThinking.timer) { clearTimeout(tgThinking.timer); tgThinking.timer = null }
987
+ tgThinking.buffer = ''
988
+ tgThinking.lastMsgId = null
989
+ tgThinking.target = null
990
+ }
991
+
919
992
  // REPL 主循环 — 由 readline 原生处理回显、退格、行回绕与 Enter 提交
920
993
  rl.on('line', (line) => {
921
994
  processInputLine(line)
@@ -1027,6 +1100,23 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1027
1100
  console.log('📡 Built-in channel listeners started (--with-notify)')
1028
1101
  }
1029
1102
 
1103
+ // ============================================================
1104
+ // 引擎流式回调 — 保持 CLI 实时输出,并把思维链推送到 Telegram
1105
+ // onDelta 会在流式生成时被调用({type:'text'|'reasoning', text})。
1106
+ // 设置 onDelta 后引擎不再直接写终端,因此这里需手动维持终端输出。
1107
+ // ============================================================
1108
+ engine.config.onDelta = ({ type, text }) => {
1109
+ if (type === 'text') {
1110
+ // 保持 CLI 实时输出(与未设置 onDelta 时行为一致)
1111
+ process.stdout.write(text)
1112
+ } else if (type === 'reasoning') {
1113
+ // CLI 也显示思维链
1114
+ process.stdout.write(text)
1115
+ // 实时推送到 Telegram(节流)
1116
+ tgThinkingPush(text)
1117
+ }
1118
+ }
1119
+
1030
1120
  console.log(buildBanner({ model, permissionMode, session, maxTokens: tokenBudget.maxTokens }))
1031
1121
  console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
1032
1122
  console.log(`Socket: ${SOCK_PATH} (cc-notify can connect)`)
@@ -413,9 +413,11 @@ export class QueryEngine {
413
413
  }
414
414
  currentText += event.text
415
415
  } else if (event.type === 'reasoning') {
416
- // 推理内容(thinking)同样支持回调
416
+ // 推理内容(thinking)同样支持回调;无 onDelta 时在终端展示思维链
417
417
  if (typeof this.config.onDelta === 'function') {
418
418
  this.config.onDelta({ type: 'reasoning', text: event.text })
419
+ } else if (this.config.verbose) {
420
+ process.stdout.write(event.text)
419
421
  }
420
422
  } else if (event.type === 'tool_use') {
421
423
  // 收集工具调用
@@ -426,6 +428,7 @@ export class QueryEngine {
426
428
  ))
427
429
  } else if (event.type === 'done') {
428
430
  result.content = event.result.content || currentText
431
+ result.reasoningContent = event.result.reasoningContent || result.reasoningContent
429
432
  result.toolCalls = event.result.toolCalls?.map(tc =>
430
433
  new ToolCall(tc.id, tc.name, tc.input)
431
434
  ) || result.toolCalls
@@ -44,6 +44,8 @@ export async function* parseStream(response) {
44
44
  // reasoning_content (DeepSeek thinking mode)
45
45
  if (delta.reasoning_content) {
46
46
  result.reasoningContent += delta.reasoning_content
47
+ // 实时产出 reasoning 事件,供 onDelta 回调(如推送思维链到 Telegram)消费
48
+ yield { type: 'reasoning', text: delta.reasoning_content }
47
49
  }
48
50
 
49
51
  // 文本
@@ -15,26 +15,84 @@
15
15
 
16
16
  import { ToolDef } from '../types/index.js'
17
17
  import { TelegramBotClient } from '../channel/tg-listener.js'
18
+ import { readFileSync, statSync } from 'fs'
19
+ import { homedir } from 'os'
20
+ import { join } from 'path'
18
21
 
19
22
  const API_BASE = (token) => `https://api.telegram.org/bot${token}`
20
23
 
21
- /** 读取配置 */
24
+ /**
25
+ * 读取 Telegram 配置(环境变量优先,其次 config.json 的 channels.telegram)
26
+ *
27
+ * 配置优先级:
28
+ * 1. 环境变量 CC_NODE_CHANNEL_TELEGRAM_*
29
+ * 2. 用户级 ~/.claude-code/config.json → channels.telegram
30
+ * 3. 项目级 .claude-code/config.json → channels.telegram(覆盖用户级)
31
+ */
32
+ let _cachedTgConfig = null
33
+ let _cachedTgConfigMtime = null
34
+
35
+ function loadTgConfigFromFile() {
36
+ const files = [
37
+ join(homedir(), '.claude-code/config.json'), // 用户级
38
+ '.claude-code/config.json', // 项目级(覆盖用户级)
39
+ ]
40
+ let merged = {}
41
+ for (const file of files) {
42
+ try {
43
+ const raw = readFileSync(file, 'utf-8')
44
+ const data = JSON.parse(raw)
45
+ if (data?.channels?.telegram) {
46
+ merged = { ...merged, ...data.channels.telegram }
47
+ }
48
+ } catch { /* 文件不存在或解析失败则跳过 */ }
49
+ }
50
+ return merged
51
+ }
52
+
53
+ /** 读取 Telegram 配置(带缓存,文件 mtime 变化自动失效) */
54
+ function getTgConfig() {
55
+ const userFile = join(homedir(), '.claude-code/config.json')
56
+ const projectFile = '.claude-code/config.json'
57
+ let mtimeKey = ''
58
+ for (const f of [userFile, projectFile]) {
59
+ try { mtimeKey += statSync(f).mtimeMs } catch {}
60
+ }
61
+ if (_cachedTgConfigMtime !== mtimeKey) {
62
+ _cachedTgConfig = loadTgConfigFromFile()
63
+ _cachedTgConfigMtime = mtimeKey
64
+ }
65
+ return _cachedTgConfig
66
+ }
67
+
68
+ /** 读取 token(环境变量优先,其次 config.json) */
22
69
  function getToken() {
23
- return process.env.CC_NODE_CHANNEL_TELEGRAM_TOKEN || ''
70
+ return process.env.CC_NODE_CHANNEL_TELEGRAM_TOKEN || getTgConfig().token || ''
24
71
  }
25
72
 
73
+ /** 读取默认聊天 ID(环境变量优先,其次 config.json) */
26
74
  function getDefaultChatId() {
27
- return process.env.CC_NODE_CHANNEL_TELEGRAM_CHAT_ID || ''
75
+ return process.env.CC_NODE_CHANNEL_TELEGRAM_CHAT_ID || getTgConfig().chatId || ''
76
+ }
77
+
78
+ /** 读取代理(环境变量优先,其次 config.json) */
79
+ function getProxy() {
80
+ return process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || getTgConfig().proxy || ''
81
+ }
82
+
83
+ /** 读取 API Base(环境变量优先,其次 config.json) */
84
+ function getApiBase(token) {
85
+ return process.env.CC_NODE_CHANNEL_TELEGRAM_API_BASE || getTgConfig().apiBase || API_BASE(token)
28
86
  }
29
87
 
30
88
  /** 创建 TelegramBotClient 实例 */
31
89
  function getClient() {
32
90
  const token = getToken()
33
91
  if (!token) {
34
- throw new Error('未配置 Telegram Token(请设置 CC_NODE_CHANNEL_TELEGRAM_TOKEN)')
92
+ throw new Error('未配置 Telegram Token(请设置 CC_NODE_CHANNEL_TELEGRAM_TOKEN 或 config.json 的 channels.telegram.token)')
35
93
  }
36
- const proxy = process.env.CC_NODE_CHANNEL_TELEGRAM_PROXY || ''
37
- const apiBase = process.env.CC_NODE_CHANNEL_TELEGRAM_API_BASE || API_BASE(token)
94
+ const proxy = getProxy()
95
+ const apiBase = getApiBase(token)
38
96
  return new TelegramBotClient(token, { proxy, apiBase })
39
97
  }
40
98
 
@@ -93,7 +151,7 @@ async function sendMedia(args) {
93
151
 
94
152
  const client = getClient()
95
153
  const token = getToken()
96
- const apiBase = client.apiBase || API_BASE(token)
154
+ const apiBase = client.apiBase || getApiBase(token)
97
155
 
98
156
  // 检测文件类型
99
157
  const ext = absPath.split('.').pop().toLowerCase()
@@ -156,7 +214,7 @@ async function channelApi(args) {
156
214
 
157
215
  const client = getClient()
158
216
  const token = getToken()
159
- const apiBase = client.apiBase || API_BASE(token)
217
+ const apiBase = client.apiBase || getApiBase(token)
160
218
 
161
219
  const url = new URL(apiBase + (path.startsWith('/') ? path : '/' + path))
162
220
  for (const [k, v] of Object.entries(query)) url.searchParams.append(k, String(v))