@raolin2025/claude-code-node 2.6.17 → 2.7.0
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 +34 -38
- package/src/core/headpiece.js +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
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
|
@@ -18,6 +18,7 @@ import { CostTracker } from './cost-tracker.js'
|
|
|
18
18
|
import { autoCompact } from './compact.js'
|
|
19
19
|
import { isLocalLlmServer } from '../utils/index.js'
|
|
20
20
|
import { SOCK_DIR, SOCK_PATH, CC_NODE_PID } from './paths.js'
|
|
21
|
+
import { renderHeadpiece } from './headpiece.js'
|
|
21
22
|
import { TelegramListener } from '../channel/tg-listener.js'
|
|
22
23
|
import { fetchViaSocks5 } from '../channel/tg-proxy.js'
|
|
23
24
|
|
|
@@ -147,48 +148,16 @@ function buildBanner({ model, permissionMode, session, maxTokens }) {
|
|
|
147
148
|
const top = `╭${leftLabel}${'─'.repeat(inner - leftLabel.length)}╮`
|
|
148
149
|
const bottom = `╰${'─'.repeat(inner)}╯`
|
|
149
150
|
|
|
150
|
-
const col1 = 24 //
|
|
151
|
+
const col1 = 24 // 左侧头标列(头标宽度 20 + 边距)
|
|
151
152
|
const col2 = 50 // 标题列
|
|
152
153
|
const col3 = inner - col1 - col2 - 2 // 信息列
|
|
153
154
|
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const RESET = '\x1b[0m'
|
|
158
|
-
const RED = '\x1b[31m'
|
|
159
|
-
|
|
160
|
-
// 像素风格 CC 机器人(19字符宽,带颜色边框)
|
|
161
|
-
const robotRaw = [
|
|
162
|
-
' ┌───────────────┐ ',
|
|
163
|
-
' │ ██ ██ │ ',
|
|
164
|
-
' │ ██████ │ ',
|
|
165
|
-
' │ ██ ██ │ ',
|
|
166
|
-
' │ │ ',
|
|
167
|
-
' │ ███████ │ ',
|
|
168
|
-
' │ ██ ██ │ ',
|
|
169
|
-
' │ ██████ │ ',
|
|
170
|
-
' │ ██ ██ ██ │ ',
|
|
171
|
-
' └───────────────┘ ',
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
// 颜色化:边框蓝,眼睛/嘴巴青
|
|
175
|
-
const colorize = (line) =>
|
|
176
|
-
line
|
|
177
|
-
.replace(/[┌└─╭╮┐┘│]/g, BLUE + '$&' + RESET)
|
|
178
|
-
.replace(/[█]/g, RED + '$&' + RESET)
|
|
179
|
-
|
|
180
|
-
const robotColored = robotRaw.map(colorize)
|
|
155
|
+
// 头标(独立模块 headpiece.js 提供,替换只需改该文件)
|
|
156
|
+
const headpiece = renderHeadpiece({ colWidth: col1 })
|
|
157
|
+
const robotLines = headpiece.lines
|
|
181
158
|
|
|
182
|
-
//
|
|
159
|
+
// 去掉 ANSI 转义码,返回真实可见长度
|
|
183
160
|
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '')
|
|
184
|
-
const robotPlain = robotColored.map(line => stripAnsi(line))
|
|
185
|
-
|
|
186
|
-
// 在 col1 内居中(按最大可见长度 21 计算,短的自动靠右)
|
|
187
|
-
const visibleWidth = 21
|
|
188
|
-
const totalCol1 = 24
|
|
189
|
-
const leftPad = Math.floor((totalCol1 - visibleWidth) / 2)
|
|
190
|
-
const rightPad = totalCol1 - visibleWidth - leftPad
|
|
191
|
-
const robotLines = robotPlain.map(line => ' '.repeat(leftPad) + line + ' '.repeat(rightPad))
|
|
192
161
|
|
|
193
162
|
// 辅助函数:字符串填充(忽略 ANSI 颜色码计算真实长度,正确处理嵌入在字符串中的 ANSI)
|
|
194
163
|
const realLen = (s) => stripAnsi(s).length
|
|
@@ -601,6 +570,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
|
|
|
601
570
|
let pendingConfirm = null
|
|
602
571
|
// 最近一次 /models 拉取的模型列表(供 /model <编号> 选择)
|
|
603
572
|
let modelList = []
|
|
573
|
+
// /models 从 Telegram 发出后,等待用户回复编号/名字来选择模型
|
|
574
|
+
let pendingModelSelect = false
|
|
604
575
|
|
|
605
576
|
// 处理输入行
|
|
606
577
|
// source: 'cli' 来自终端输入, 'telegram' 来自 Telegram
|
|
@@ -635,7 +606,31 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
|
|
|
635
606
|
return
|
|
636
607
|
}
|
|
637
608
|
|
|
609
|
+
// /models 从 Telegram 发出后,用户回复编号或名字来选择模型
|
|
610
|
+
if (source === 'telegram' && pendingModelSelect && !trimmed.startsWith('/')) {
|
|
611
|
+
const num = parseInt(trimmed, 10)
|
|
612
|
+
let selected = ''
|
|
613
|
+
if (!isNaN(num) && num >= 1 && num <= modelList.length) {
|
|
614
|
+
selected = modelList[num - 1]
|
|
615
|
+
} else if (modelList.includes(trimmed)) {
|
|
616
|
+
selected = trimmed
|
|
617
|
+
}
|
|
618
|
+
// 无论是否是有效选择,都结束"等待选择"状态
|
|
619
|
+
pendingModelSelect = false
|
|
620
|
+
if (selected) {
|
|
621
|
+
engine.config.model = selected
|
|
622
|
+
console.log(`Model → ${selected}`)
|
|
623
|
+
if (tgListener?.bot) {
|
|
624
|
+
await sendTelegram(`✅ 已切换模型 → ${selected}`, tgChatId || null).catch(() => {})
|
|
625
|
+
}
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
// 不是有效选择 → 当作普通消息继续处理
|
|
629
|
+
}
|
|
630
|
+
|
|
638
631
|
if (trimmed.startsWith('/')) {
|
|
632
|
+
// 用户输入了命令,取消可能存在的"等待模型选择"状态
|
|
633
|
+
pendingModelSelect = false
|
|
639
634
|
// 来自 Telegram 的命令:捕获 console 输出并回发到 Telegram
|
|
640
635
|
const isTgCmd = source === 'telegram' && tgListener?.bot
|
|
641
636
|
let tgOut = ''
|
|
@@ -696,7 +691,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
|
|
|
696
691
|
})
|
|
697
692
|
// 来自 Telegram:直接列出,不等待 CLI 交互(用 /model 名字或编号 切换)
|
|
698
693
|
if (source === 'telegram') {
|
|
699
|
-
console.log('\nℹ️
|
|
694
|
+
console.log('\nℹ️ 直接回复编号或模型名即可切换')
|
|
695
|
+
pendingModelSelect = true // 等待用户回复编号/名字来选择模型
|
|
700
696
|
break
|
|
701
697
|
}
|
|
702
698
|
console.log('\nType a number to select, or model name directly:')
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// headpiece.js — CC-Node 启动头标(独立模块)
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// 只负责「头标」的绘制与配色,与 CLI 框架解耦。
|
|
5
|
+
// 以后要替换/升级头标,只需修改本文件并保持接口不变即可。
|
|
6
|
+
//
|
|
7
|
+
// 头标内容: CC-NODE 艺术字
|
|
8
|
+
// CC : 两个 C, 10列宽 x 4行高 (扁平, 蓝渐变)
|
|
9
|
+
// NODE : 四个字母, 5列宽 x 4行高 (绿渐变)
|
|
10
|
+
// 上下严格对齐 (20列 x 8行)
|
|
11
|
+
//
|
|
12
|
+
// 接口:
|
|
13
|
+
// renderHeadpiece({ colWidth = 24 } = {})
|
|
14
|
+
// -> { lines: string[], height: number, width: number }
|
|
15
|
+
// lines 已上色、并已居中填充到 colWidth 宽度的行数组
|
|
16
|
+
// height 头标实际行数(未填充前的原始行数)
|
|
17
|
+
// width 头标原始可见宽度
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
// ---- 头标原始数据(20列 x 8行)----
|
|
21
|
+
// CC 部分(蓝渐变, 4行)
|
|
22
|
+
const CC = [
|
|
23
|
+
'╔════════╗╔════════╗',
|
|
24
|
+
'██║ ██║ ',
|
|
25
|
+
'██║ ██║ ',
|
|
26
|
+
'╚════════╝╚════════╝',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// NODE 部分(绿渐变, 4行)
|
|
30
|
+
const NODE = [
|
|
31
|
+
'╔╗╔═╗╔═══╗╔═══╗╔═══╗',
|
|
32
|
+
'║║║ ║║ ║║ ║║ ║',
|
|
33
|
+
'║╚╝ ║║ ║║ ║╠═══╣',
|
|
34
|
+
'╚═══╝╚═══╝╚═══╝╚═══╝',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
// ---- 配色(渐变)----
|
|
38
|
+
// CC 蓝色渐变(深 → 浅)
|
|
39
|
+
const BLUE = [81, 75, 69, 63]
|
|
40
|
+
// NODE 绿色渐变(深 → 浅)
|
|
41
|
+
const GREEN = [82, 118, 154, 190]
|
|
42
|
+
|
|
43
|
+
// 给每一行按对应色号上色
|
|
44
|
+
function colorize(lines, colors) {
|
|
45
|
+
return lines.map((line, i) => `\x1b[38;5;${colors[i]}m${line}\x1b[0m`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 去掉 ANSI 转义码,返回真实可见长度
|
|
49
|
+
function stripAnsi(s) {
|
|
50
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 渲染头标。
|
|
55
|
+
* @param {Object} [opts]
|
|
56
|
+
* @param {number} [opts.colWidth=24] 目标列宽(用于居中填充,须 >= 头标宽度)
|
|
57
|
+
* @returns {{ lines: string[], height: number, width: number }}
|
|
58
|
+
*/
|
|
59
|
+
export function renderHeadpiece({ colWidth = 24 } = {}) {
|
|
60
|
+
// 1. 原始可见宽度
|
|
61
|
+
const width = stripAnsi(CC[0]).length // 20
|
|
62
|
+
|
|
63
|
+
// 2. 合并 CC + NODE,分别上色
|
|
64
|
+
const rawLines = [...colorize(CC, BLUE), ...colorize(NODE, GREEN)]
|
|
65
|
+
const height = rawLines.length // 8
|
|
66
|
+
|
|
67
|
+
// 3. 在 colWidth 内居中填充(若 colWidth < width 则不做填充,原样返回)
|
|
68
|
+
if (colWidth <= width) {
|
|
69
|
+
return { lines: rawLines, height, width }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const totalPad = colWidth - width
|
|
73
|
+
const leftPad = Math.floor(totalPad / 2)
|
|
74
|
+
const rightPad = totalPad - leftPad
|
|
75
|
+
|
|
76
|
+
const lines = rawLines.map((line) => {
|
|
77
|
+
const leadingAnsi = line.match(/^(\x1b\[[0-9;]*m)+/)?.[0] || ''
|
|
78
|
+
const trailingAnsi = line.match(/(\x1b\[0m)+$/)?.[0] || '\x1b[0m'
|
|
79
|
+
const plain = stripAnsi(line)
|
|
80
|
+
return leadingAnsi + ' '.repeat(leftPad) + plain + ' '.repeat(rightPad) + trailingAnsi
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return { lines, height, width }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 允许命令行直接预览:node src/core/headpiece.js
|
|
87
|
+
const isDirectRun = process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop())
|
|
88
|
+
if (isDirectRun) {
|
|
89
|
+
const { lines } = renderHeadpiece({ colWidth: 24 })
|
|
90
|
+
console.log(`\n${lines.join('\n')}\n`)
|
|
91
|
+
}
|