@raolin2025/claude-code-node 2.6.13 → 2.6.15

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.15",
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,70 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
926
932
  }
927
933
  }
928
934
 
935
+ // ============================================================
936
+ // 流式推送 → Telegram
937
+ // 把引擎 onDelta 产出的生成内容(text / reasoning)实时推送到 Telegram,
938
+ // 让远程用户看到 AI 正在工作,而不是"以为没回应"。
939
+ // 采用节流 + 编辑同一消息的方式,避免刷屏和触发速率限制。
940
+ // ============================================================
941
+ const tgThinking = { buffer: '', timer: null, target: null, lastMsgId: null, flushing: false }
942
+
943
+ function tgThinkingStart(target) {
944
+ tgThinking.target = target || null
945
+ tgThinking.buffer = ''
946
+ tgThinking.lastMsgId = null
947
+ tgThinking.flushing = false
948
+ // 先发送 typing 动作,让 Telegram 立即显示"正在输入"
949
+ if (tgListener?.bot && tgThinking.target) {
950
+ tgListener.bot.sendChatAction(tgThinking.target, 'typing').catch(() => {})
951
+ }
952
+ }
953
+
954
+ function tgThinkingPush(text) {
955
+ if (!text || !tgListener?.bot || !tgThinking.target) return
956
+ tgThinking.buffer += text
957
+ // 只保留最近 8000 字符,避免无限增长
958
+ if (tgThinking.buffer.length > 8000) tgThinking.buffer = tgThinking.buffer.slice(-8000)
959
+ if (tgThinking.timer) clearTimeout(tgThinking.timer)
960
+ // 首帧快速发送(500ms 聚合首段),之后节流编辑(1.5s)
961
+ const delay = tgThinking.lastMsgId ? 1500 : 500
962
+ tgThinking.timer = setTimeout(() => tgThinkingFlush(), delay)
963
+ }
964
+
965
+ async function tgThinkingFlush() {
966
+ if (tgThinking.flushing) return // 防止并发
967
+ if (tgThinking.timer) { clearTimeout(tgThinking.timer); tgThinking.timer = null }
968
+ if (!tgListener?.bot || !tgThinking.target || !tgThinking.buffer) return
969
+ tgThinking.flushing = true
970
+ const target = tgThinking.target
971
+ const body = `🧠 思考中…\n\n${tgThinking.buffer.slice(-3500)}`
972
+ try {
973
+ if (tgThinking.lastMsgId) {
974
+ await tgListener.bot.editMessage(target, tgThinking.lastMsgId, body, { parseMode: 'HTML' })
975
+ } else {
976
+ const res = await tgListener.bot.sendMessage(target, body, { parseMode: 'HTML' })
977
+ tgThinking.lastMsgId = res?.message_id || null
978
+ }
979
+ } catch (e) {
980
+ // 编辑失败(如消息被删)→ 重发一条新的
981
+ tgThinking.lastMsgId = null
982
+ try {
983
+ const res = await tgListener.bot.sendMessage(target, body, { parseMode: 'HTML' })
984
+ tgThinking.lastMsgId = res?.message_id || null
985
+ } catch {}
986
+ } finally {
987
+ tgThinking.flushing = false
988
+ }
989
+ }
990
+
991
+ function tgThinkingEnd() {
992
+ if (tgThinking.timer) { clearTimeout(tgThinking.timer); tgThinking.timer = null }
993
+ tgThinking.buffer = ''
994
+ tgThinking.lastMsgId = null
995
+ tgThinking.target = null
996
+ tgThinking.flushing = false
997
+ }
998
+
929
999
  // REPL 主循环 — 由 readline 原生处理回显、退格、行回绕与 Enter 提交
930
1000
  rl.on('line', (line) => {
931
1001
  processInputLine(line)
@@ -1037,6 +1107,20 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
1037
1107
  console.log('📡 Built-in channel listeners started (--with-notify)')
1038
1108
  }
1039
1109
 
1110
+ // ============================================================
1111
+ // 引擎流式回调 — 保持 CLI 实时输出,并把生成内容推送到 Telegram
1112
+ // onDelta 会在流式生成时被调用({type:'text'|'reasoning', text})。
1113
+ // 注意:DeepSeek 模型 thinking 被禁用时只有 text 事件、没有 reasoning,
1114
+ // 因此 text 也必须推送到 Telegram,否则远程看不到任何进度。
1115
+ // 设置 onDelta 后引擎不再直接写终端,因此这里需手动维持终端输出。
1116
+ // ============================================================
1117
+ engine.config.onDelta = ({ type, text }) => {
1118
+ // 保持 CLI 实时输出
1119
+ process.stdout.write(text)
1120
+ // 无论 text 还是 reasoning,都实时推送到 Telegram(节流)
1121
+ tgThinkingPush(text)
1122
+ }
1123
+
1040
1124
  console.log(buildBanner({ model, permissionMode, session, maxTokens: tokenBudget.maxTokens }))
1041
1125
  console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
1042
1126
  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))