@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.
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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)`)
|
package/src/core/query-engine.js
CHANGED
|
@@ -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
|
package/src/core/streaming.js
CHANGED
|
@@ -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 =
|
|
37
|
-
const apiBase =
|
|
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 ||
|
|
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 ||
|
|
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))
|