@raolin2025/claude-code-node 2.6.13 → 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.13",
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",
package/src/core/cli.js CHANGED
@@ -867,7 +867,12 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
867
867
 
868
868
  // 发送到引擎
869
869
  try {
870
+ // 有 Telegram 通道时,流式推送思维链到当前回复目标,避免远程"以为没回应"
871
+ const streamTarget = (source === 'telegram') ? (tgChatId || null) : (tgListener?.bot ? tgChatId || null : null)
872
+ if (streamTarget) tgThinkingStart(streamTarget)
870
873
  const result = await processInput(input)
874
+ // 处理结束,清掉思维链流(最终回复随后发送)
875
+ tgThinkingEnd()
871
876
  console.log()
872
877
  console.log(result.response)
873
878
  console.log()
@@ -886,6 +891,7 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
886
891
  await sendTelegram(result.response, tgChatId || null)
887
892
  }
888
893
  } catch (err) {
894
+ tgThinkingEnd()
889
895
  console.error(`\nError: ${err.message}\n`)
890
896
  if (tgListener?.bot) {
891
897
  await sendTelegram(`❌ Error: ${err.message}`, tgChatId || null)
@@ -926,6 +932,63 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
926
932
  }
927
933
  }
928
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
+
929
992
  // REPL 主循环 — 由 readline 原生处理回显、退格、行回绕与 Enter 提交
930
993
  rl.on('line', (line) => {
931
994
  processInputLine(line)
@@ -1037,6 +1100,23 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1037
1100
  console.log('📡 Built-in channel listeners started (--with-notify)')
1038
1101
  }
1039
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
+
1040
1120
  console.log(buildBanner({ model, permissionMode, session, maxTokens: tokenBudget.maxTokens }))
1041
1121
  console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
1042
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))