@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 +1 -1
- package/src/core/cli.js +84 -0
- package/src/core/query-engine.js +4 -1
- package/src/core/streaming.js +2 -0
- package/src/tools/telegram-tools.js +66 -8
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.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)`)
|
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))
|