@raolin2025/claude-code-node 1.2.0 → 2.1.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/channel/index.js +81 -146
- package/src/channel/notify-daemon.js +454 -350
- package/src/core/cli.js +235 -114
- package/src/core/compact.js +171 -0
- package/src/core/config.js +1 -2
- package/src/core/cost-tracker.js +171 -0
- package/src/core/paths.js +12 -0
- package/src/core/query-engine.js +223 -117
- package/src/core/session.js +27 -10
- package/src/mcp/client.js +112 -4
- package/src/mcp/registry.js +1 -2
- package/src/security/bash-guard.js +174 -141
- package/src/security/enhanced-permission.js +72 -34
- package/src/security/path-guard.js +43 -30
- package/src/security/ssrf-guard.js +153 -50
- package/src/tools/glob.js +1 -1
- package/src/tools/web-fetch.js +3 -1
- package/src/types/index.js +2 -1
- package/src/utils/file-ops.js +2 -3
package/src/core/cli.js
CHANGED
|
@@ -1,15 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI 入口 — 命令行解析和 REPL 循环
|
|
3
3
|
* 对应原版: src/cli/ + src/entrypoints/
|
|
4
|
+
*
|
|
5
|
+
* v1.2: 增加 Unix socket 服务,让 cc-notify 能发现并转发消息
|
|
4
6
|
*/
|
|
5
7
|
import { createInterface } from 'readline'
|
|
8
|
+
import { createServer as createNetServer } from 'net'
|
|
9
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync, readFileSync, chmodSync } from 'fs'
|
|
10
|
+
import { join } from 'path'
|
|
6
11
|
import { QueryEngine, QueryEngineConfig } from './query-engine.js'
|
|
7
12
|
import { createDefaultRegistry } from '../tools/index.js'
|
|
8
13
|
import { SessionManager } from './session.js'
|
|
9
14
|
import { Config } from './config.js'
|
|
10
15
|
import { TokenBudget } from './token-budget.js'
|
|
11
|
-
import { PermissionChecker } from '../permission/permission.js'
|
|
12
16
|
import { ChannelManager } from '../channel/index.js'
|
|
17
|
+
import { CostTracker } from './cost-tracker.js'
|
|
18
|
+
import { autoCompact } from './compact.js'
|
|
19
|
+
import { SOCK_DIR, SOCK_PATH, CC_NODE_PID } from './paths.js'
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// Unix Socket — 让 cc-notify 能发现 cc-node
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 启动 Unix socket 服务器
|
|
29
|
+
* cc-notify 通过此 socket 转发消息给已运行的 cc-node
|
|
30
|
+
*/
|
|
31
|
+
function startSocketServer(engine, session, sessionManager, channelManager, verbose) {
|
|
32
|
+
mkdirSync(SOCK_DIR, { recursive: true })
|
|
33
|
+
|
|
34
|
+
// v1.1 修复: 安全清理残留 socket — 检查 PID 文件确认进程已死
|
|
35
|
+
if (existsSync(SOCK_PATH)) {
|
|
36
|
+
let shouldClean = true
|
|
37
|
+
if (existsSync(CC_NODE_PID)) {
|
|
38
|
+
try {
|
|
39
|
+
const oldPid = parseInt(readFileSync(CC_NODE_PID, 'utf8').trim(), 10)
|
|
40
|
+
// 检查旧进程是否还活着
|
|
41
|
+
process.kill(oldPid, 0) // 如果进程存在且活着,这不会抛出
|
|
42
|
+
shouldClean = false // 旧进程还活着,不要清理
|
|
43
|
+
console.error(`cc-node already running (PID ${oldPid}). Use /exit first or kill ${oldPid}`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
} catch {
|
|
46
|
+
// 旧进程已死,安全清理
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (shouldClean) {
|
|
50
|
+
try { unlinkSync(SOCK_PATH) } catch {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const server = createNetServer((client) => {
|
|
55
|
+
// v1.1: socket 连接来源验证 — 只允许同用户连接
|
|
56
|
+
// Unix socket 本身通过文件系统权限保护
|
|
57
|
+
let buffer = ''
|
|
58
|
+
|
|
59
|
+
client.on('data', async (data) => {
|
|
60
|
+
buffer += data.toString()
|
|
61
|
+
|
|
62
|
+
// 按行解析 JSON 消息
|
|
63
|
+
const lines = buffer.split('\n')
|
|
64
|
+
buffer = lines.pop() // 保留不完整的行
|
|
65
|
+
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (!line.trim()) continue
|
|
68
|
+
try {
|
|
69
|
+
const msg = JSON.parse(line)
|
|
70
|
+
if (msg.type === 'user_input' && msg.text) {
|
|
71
|
+
// 转发到引擎处理
|
|
72
|
+
const result = await engine.processMessage(msg.text)
|
|
73
|
+
const reply = JSON.stringify({ type: 'reply', text: result.response }) + '\n'
|
|
74
|
+
client.write(reply)
|
|
75
|
+
|
|
76
|
+
// 保存到会话
|
|
77
|
+
await sessionManager.appendMessage({ role: 'user', content: msg.text })
|
|
78
|
+
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
79
|
+
// M5: 保存 engine state 到 session
|
|
80
|
+
session.state = session.state || {}
|
|
81
|
+
session.state.turnCount = engine.state.turnCount
|
|
82
|
+
session.state.costHistory = engine.costTracker.history.slice(-50) // 只保留最近50条
|
|
83
|
+
await sessionManager.save(session)
|
|
84
|
+
} else if (msg.type === 'ping') {
|
|
85
|
+
client.write(JSON.stringify({ type: 'pong', pid: process.pid }) + '\n')
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
client.write(JSON.stringify({ type: 'error', text: e.message }) + '\n')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
client.on('error', () => {}) // 忽略连接断开
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
server.listen(SOCK_PATH, () => {
|
|
97
|
+
// v1.1 修复: socket 文件权限 0600(仅所有者可读写),阻止其他用户连接
|
|
98
|
+
try { chmodSync(SOCK_PATH, 0o600) } catch {}
|
|
99
|
+
// 写 PID 文件(权限 0644)
|
|
100
|
+
writeFileSync(CC_NODE_PID, String(process.pid), { mode: 0o644 })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// 退出时清理
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
try { unlinkSync(SOCK_PATH) } catch {}
|
|
106
|
+
try { unlinkSync(CC_NODE_PID) } catch {}
|
|
107
|
+
}
|
|
108
|
+
process.on('SIGTERM', cleanup)
|
|
109
|
+
process.on('SIGINT', cleanup)
|
|
110
|
+
process.on('exit', cleanup)
|
|
111
|
+
|
|
112
|
+
return server
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// Banner & Help
|
|
117
|
+
// ============================================================
|
|
13
118
|
|
|
14
119
|
const BANNER = `
|
|
15
120
|
╔═══════════════════════════════════════════════╗
|
|
@@ -31,13 +136,17 @@ Commands:
|
|
|
31
136
|
/config KEY — Show config value
|
|
32
137
|
/budget — Show token budget
|
|
33
138
|
/channel CMD — Manage notification channels (list|send|test)
|
|
139
|
+
/cost — Show API cost report
|
|
140
|
+
/compact — Manually compact conversation context
|
|
141
|
+
/allow [tool] — Allow a tool for the current session (default: all)
|
|
34
142
|
/exit — Exit (also Ctrl+C)
|
|
35
143
|
/quit — Same as /exit
|
|
36
144
|
`
|
|
37
145
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
146
|
+
// ============================================================
|
|
147
|
+
// 参数解析
|
|
148
|
+
// ============================================================
|
|
149
|
+
|
|
41
150
|
function parseArgs(argv) {
|
|
42
151
|
const args = {
|
|
43
152
|
model: 'deepseek-chat',
|
|
@@ -50,7 +159,7 @@ function parseArgs(argv) {
|
|
|
50
159
|
noStream: false,
|
|
51
160
|
}
|
|
52
161
|
|
|
53
|
-
let i = 2
|
|
162
|
+
let i = 2
|
|
54
163
|
while (i < argv.length) {
|
|
55
164
|
const arg = argv[i]
|
|
56
165
|
switch (arg) {
|
|
@@ -64,14 +173,14 @@ function parseArgs(argv) {
|
|
|
64
173
|
case '--verbose': case '-v': args.verbose = true; break
|
|
65
174
|
case '--no-stream': args.noStream = true; break
|
|
66
175
|
case '--help': case '-h':
|
|
67
|
-
console.log(`Usage: cc-node [options]
|
|
176
|
+
console.log(`Usage: cc-node [options] [prompt]
|
|
68
177
|
|
|
69
178
|
Options:
|
|
70
|
-
-m, --model NAME Model to use
|
|
179
|
+
-m, --model NAME Model to use
|
|
71
180
|
-s, --system-prompt TEXT System prompt
|
|
72
|
-
-p, --permission-mode Permission mode: ask|always-allow|deny
|
|
181
|
+
-p, --permission-mode Permission mode: ask|always-allow|deny
|
|
73
182
|
-t, --max-turns N Max tool loop turns (default: 100)
|
|
74
|
-
--api-base URL API base URL
|
|
183
|
+
--api-base URL API base URL
|
|
75
184
|
--api-key *** API key (or set LLM_API_KEY env)
|
|
76
185
|
-r, --resume ID Resume a session
|
|
77
186
|
-v, --verbose Verbose mode
|
|
@@ -79,22 +188,17 @@ Options:
|
|
|
79
188
|
-h, --help Show this help
|
|
80
189
|
|
|
81
190
|
Environment variables:
|
|
82
|
-
LLM_API_KEY
|
|
83
|
-
|
|
84
|
-
OPENAI_API_KEY OpenAI API key
|
|
85
|
-
QWEN_API_KEY Qwen (DashScope) API key
|
|
86
|
-
GLM_API_KEY Zhipu GLM API key
|
|
87
|
-
KIMI_API_KEY Moonshot Kimi API key
|
|
88
|
-
LLM_API_BASE API base URL (default: https://api.deepseek.com/v1)
|
|
191
|
+
LLM_API_KEY, DEEPSEEK_API_KEY, OPENAI_API_KEY,
|
|
192
|
+
QWEN_API_KEY, GLM_API_KEY, KIMI_API_KEY, LLM_API_BASE
|
|
89
193
|
|
|
90
194
|
Channel environment variables:
|
|
91
|
-
CC_NODE_CHANNEL_DEFAULT
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
195
|
+
CC_NODE_CHANNEL_DEFAULT, CC_NODE_CHANNEL_TELEGRAM_TOKEN,
|
|
196
|
+
CC_NODE_CHANNEL_TELEGRAM_CHAT_ID, CC_NODE_CHANNEL_WECOM_WEBHOOK_URL,
|
|
197
|
+
CC_NODE_CHANNEL_FEISHU_WEBHOOK_URL, CC_NODE_CHANNEL_DISCORD_WEBHOOK_URL,
|
|
198
|
+
CC_NODE_CHANNEL_SLACK_WEBHOOK_URL
|
|
199
|
+
|
|
200
|
+
Unix Socket (for cc-notify):
|
|
201
|
+
${SOCK_PATH} — cc-notify 通过此 socket 转发消息
|
|
98
202
|
`)
|
|
99
203
|
process.exit(0)
|
|
100
204
|
default:
|
|
@@ -109,17 +213,16 @@ Channel environment variables:
|
|
|
109
213
|
return args
|
|
110
214
|
}
|
|
111
215
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
216
|
+
// ============================================================
|
|
217
|
+
// 主入口
|
|
218
|
+
// ============================================================
|
|
219
|
+
|
|
115
220
|
export async function main() {
|
|
116
221
|
const cliArgs = parseArgs(process.argv)
|
|
117
222
|
|
|
118
|
-
// 加载配置
|
|
119
223
|
const config = new Config()
|
|
120
224
|
await config.load(process.cwd())
|
|
121
225
|
|
|
122
|
-
// 合并 CLI 参数 > 项目配置 > 用户配置 > 默认值
|
|
123
226
|
const model = cliArgs.model || config.get('model')
|
|
124
227
|
const systemPrompt = cliArgs.systemPrompt || ''
|
|
125
228
|
const permissionMode = cliArgs.permissionMode || config.get('permissionMode')
|
|
@@ -128,55 +231,53 @@ export async function main() {
|
|
|
128
231
|
const apiKey = cliArgs.apiKey || config.get('apiKey') || ''
|
|
129
232
|
const verbose = cliArgs.verbose || config.get('verbose')
|
|
130
233
|
|
|
131
|
-
// 创建工具注册表
|
|
132
234
|
const registry = createDefaultRegistry()
|
|
235
|
+
const sessionManager = new SessionManager({ sessionsDir: config.get('sessionsDir') })
|
|
133
236
|
|
|
134
|
-
// 创建会话管理器
|
|
135
|
-
const sessionManager = new SessionManager({
|
|
136
|
-
sessionsDir: config.get('sessionsDir'),
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// 恢复或创建会话
|
|
140
237
|
let session
|
|
141
238
|
if (cliArgs.resume) {
|
|
142
239
|
session = await sessionManager.load(cliArgs.resume)
|
|
143
|
-
if (!session) {
|
|
144
|
-
console.error(`Session not found: ${cliArgs.resume}`)
|
|
145
|
-
process.exit(1)
|
|
146
|
-
}
|
|
240
|
+
if (!session) { console.error(`Session not found: ${cliArgs.resume}`); process.exit(1) }
|
|
147
241
|
} else {
|
|
148
242
|
session = await sessionManager.create()
|
|
149
243
|
}
|
|
150
244
|
|
|
151
|
-
//
|
|
245
|
+
// M1 fix: tokenBudget 必须在 engineConfig 之前定义,否则 TDZ ReferenceError
|
|
246
|
+
const tokenBudget = new TokenBudget({ maxTokens: config.get('maxBudgetTokens') || 200_000 })
|
|
247
|
+
const costTracker = new CostTracker({ model })
|
|
248
|
+
|
|
152
249
|
const engineConfig = new QueryEngineConfig({
|
|
153
|
-
model,
|
|
154
|
-
systemPrompt,
|
|
155
|
-
permissionMode,
|
|
156
|
-
maxTurns,
|
|
157
|
-
apiBase,
|
|
158
|
-
apiKey,
|
|
159
|
-
verbose,
|
|
250
|
+
model, systemPrompt, permissionMode, maxTurns, apiBase, apiKey, verbose,
|
|
160
251
|
tools: registry.getAll(),
|
|
252
|
+
noStream: cliArgs.noStream,
|
|
253
|
+
costTracker,
|
|
254
|
+
tokenBudget,
|
|
161
255
|
})
|
|
162
256
|
const engine = new QueryEngine(engineConfig)
|
|
163
257
|
|
|
164
|
-
//
|
|
258
|
+
// M5: 恢复会话历史和状态 — 完整恢复所有角色(含 tool_calls、tool 结果)
|
|
165
259
|
if (session?.messages?.length) {
|
|
166
260
|
for (const msg of session.messages) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
261
|
+
const entry = { role: msg.role, content: msg.content }
|
|
262
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
263
|
+
entry.toolCalls = msg.toolCalls
|
|
264
|
+
}
|
|
265
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
266
|
+
entry.tool_call_id = msg.tool_call_id
|
|
267
|
+
}
|
|
268
|
+
engine.state.messages.push(entry)
|
|
269
|
+
}
|
|
270
|
+
// 恢复 turn count
|
|
271
|
+
if (session.state?.turnCount) engine.state.turnCount = session.state.turnCount
|
|
272
|
+
// 恢复费用记录
|
|
273
|
+
if (session.state?.costHistory) {
|
|
274
|
+
for (const record of session.state.costHistory) {
|
|
275
|
+
engine.costTracker.recordUsage(record)
|
|
171
276
|
}
|
|
172
277
|
}
|
|
173
278
|
}
|
|
174
279
|
|
|
175
|
-
const tokenBudget = new TokenBudget({
|
|
176
|
-
maxTokens: config.get('maxBudgetTokens') || 200_000,
|
|
177
|
-
})
|
|
178
280
|
|
|
179
|
-
// 初始化通讯通道
|
|
180
281
|
const channelManager = new ChannelManager({
|
|
181
282
|
channels: config.get('channels') || {},
|
|
182
283
|
defaultChannel: config.get('defaultChannel') || null,
|
|
@@ -184,27 +285,46 @@ export async function main() {
|
|
|
184
285
|
|
|
185
286
|
// 一次性输入模式
|
|
186
287
|
if (cliArgs.oneShot) {
|
|
288
|
+
// 一次性模式下用户已明确表达了执行意图,自动批准所有工具调用
|
|
289
|
+
if (engine.permissionChecker.mode === 'ask') {
|
|
290
|
+
engine.config.onConfirmTool = async () => true
|
|
291
|
+
}
|
|
187
292
|
const result = await engine.processMessage(cliArgs.oneShot)
|
|
188
293
|
console.log(result.response)
|
|
189
|
-
//
|
|
294
|
+
// 保存会话
|
|
295
|
+
session = await sessionManager.create(`one-shot: ${cliArgs.oneShot.slice(0, 50)}`)
|
|
296
|
+
await sessionManager.appendMessage({ role: 'user', content: cliArgs.oneShot })
|
|
297
|
+
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
190
298
|
if (channelManager.list().length > 0) {
|
|
191
299
|
await channelManager.sendTemplate('task-done', {
|
|
192
300
|
task: cliArgs.oneShot.slice(0, 80),
|
|
193
301
|
result: result.response.slice(0, 200),
|
|
194
|
-
})
|
|
302
|
+
}).catch(() => {})
|
|
195
303
|
}
|
|
196
304
|
process.exit(0)
|
|
197
305
|
}
|
|
198
306
|
|
|
199
|
-
// REPL 模式
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
307
|
+
// REPL 模式 — 启动 Unix socket 让 cc-notify 能发现
|
|
308
|
+
startSocketServer(engine, session, sessionManager, channelManager, verbose)
|
|
309
|
+
|
|
310
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
|
311
|
+
|
|
312
|
+
// 将 readline 注入引擎配置,用于 ask 模式确认和 AskUserQuestion 工具
|
|
313
|
+
if (permissionMode === 'ask') {
|
|
314
|
+
engine.config.onConfirmTool = async (toolName, input) => {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
const snippet = JSON.stringify(input).slice(0, 120) || '(no params)'
|
|
317
|
+
rl.question(`\n⚠️ Allow tool "${toolName}"?\n Input: ${snippet}\n (y/N) `, (answer) => {
|
|
318
|
+
resolve(answer.toLowerCase().startsWith('y'))
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
engine.config.readline = rl
|
|
205
324
|
|
|
206
325
|
console.log(BANNER)
|
|
207
326
|
console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
|
|
327
|
+
console.log(`Socket: ${SOCK_PATH} (cc-notify can connect)`)
|
|
208
328
|
if (channelManager.list().length > 0) {
|
|
209
329
|
const chList = channelManager.list().join(', ')
|
|
210
330
|
const def = channelManager.defaultChannel ? ` (default: ${channelManager.defaultChannel})` : ''
|
|
@@ -213,24 +333,22 @@ export async function main() {
|
|
|
213
333
|
console.log()
|
|
214
334
|
rl.prompt()
|
|
215
335
|
|
|
336
|
+
// REPL 消息处理包装(留作扩展点)
|
|
337
|
+
async function processInput(input) {
|
|
338
|
+
return engine.processMessage(input)
|
|
339
|
+
}
|
|
340
|
+
|
|
216
341
|
rl.on('line', async (line) => {
|
|
217
342
|
const input = line.trim()
|
|
218
343
|
if (!input) { rl.prompt(); return }
|
|
219
344
|
|
|
220
|
-
// 命令处理
|
|
221
345
|
if (input.startsWith('/')) {
|
|
222
346
|
const [cmd, ...rest] = input.slice(1).split(' ')
|
|
223
347
|
switch (cmd) {
|
|
224
|
-
case 'help':
|
|
225
|
-
console.log(HELP_TEXT)
|
|
226
|
-
break
|
|
348
|
+
case 'help': console.log(HELP_TEXT); break
|
|
227
349
|
case 'model':
|
|
228
|
-
if (rest[0]) {
|
|
229
|
-
|
|
230
|
-
console.log(`Model switched to: ${engine.config.model}`)
|
|
231
|
-
} else {
|
|
232
|
-
console.log(`Current model: ${engine.config.model}`)
|
|
233
|
-
}
|
|
350
|
+
if (rest[0]) { engine.config.model = rest.join(' '); console.log(`Model → ${engine.config.model}`) }
|
|
351
|
+
else console.log(`Model: ${engine.config.model}`)
|
|
234
352
|
break
|
|
235
353
|
case 'tools':
|
|
236
354
|
console.log('Available tools:')
|
|
@@ -247,13 +365,8 @@ export async function main() {
|
|
|
247
365
|
break
|
|
248
366
|
case 'sessions': {
|
|
249
367
|
const sessions = await sessionManager.list()
|
|
250
|
-
if (sessions.length === 0)
|
|
251
|
-
|
|
252
|
-
} else {
|
|
253
|
-
for (const s of sessions) {
|
|
254
|
-
console.log(` ${s.id} — ${s.title} (${s.messageCount} msgs, ${s.updated})`)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
368
|
+
if (sessions.length === 0) console.log('No sessions found')
|
|
369
|
+
else for (const s of sessions) console.log(` ${s.id} — ${s.title} (${s.messageCount} msgs, ${s.updated})`)
|
|
257
370
|
break
|
|
258
371
|
}
|
|
259
372
|
case 'clear':
|
|
@@ -269,18 +382,14 @@ export async function main() {
|
|
|
269
382
|
console.log(JSON.stringify(config.toJSON(), null, 2))
|
|
270
383
|
}
|
|
271
384
|
break
|
|
272
|
-
case 'budget':
|
|
273
|
-
console.log(tokenBudget.format())
|
|
274
|
-
break
|
|
385
|
+
case 'budget': console.log(tokenBudget.format()); break
|
|
275
386
|
case 'channel': {
|
|
276
387
|
const subCmd = rest.join(' ')
|
|
277
388
|
if (subCmd === 'list' || subCmd === '') {
|
|
278
389
|
const channels = channelManager.list()
|
|
279
390
|
if (channels.length === 0) {
|
|
280
391
|
console.log('No channels configured')
|
|
281
|
-
console.log('Setup
|
|
282
|
-
console.log(' 1. Environment: CC_NODE_CHANNEL_TELEGRAM_TOKEN=xxx CC_NODE_CHANNEL_TELEGRAM_CHAT_ID=xxx')
|
|
283
|
-
console.log(' 2. Config: .claude-code/config.json -> { "channels": { "telegram": { ... } } }')
|
|
392
|
+
console.log('Setup: CC_NODE_CHANNEL_TELEGRAM_TOKEN=xxx CC_NODE_CHANNEL_TELEGRAM_CHAT_ID=xxx')
|
|
284
393
|
} else {
|
|
285
394
|
console.log('Channels:')
|
|
286
395
|
for (const ch of channels) {
|
|
@@ -291,24 +400,39 @@ export async function main() {
|
|
|
291
400
|
} else if (subCmd.startsWith('send ')) {
|
|
292
401
|
const text = subCmd.slice(5)
|
|
293
402
|
const results = await channelManager.send(text)
|
|
294
|
-
for (const r of results) {
|
|
295
|
-
console.log(r.ok ? `✅ ${r.channel}: sent` : `❌ ${r.channel}: ${r.error}`)
|
|
296
|
-
}
|
|
403
|
+
for (const r of results) console.log(r.ok ? `✅ ${r.channel}: sent` : `❌ ${r.channel}: ${r.error}`)
|
|
297
404
|
} else if (subCmd.startsWith('test')) {
|
|
298
405
|
const results = await channelManager.send('📡 cc-node channel test')
|
|
299
|
-
for (const r of results) {
|
|
300
|
-
|
|
406
|
+
for (const r of results) console.log(r.ok ? `✅ ${r.channel}: test OK` : `❌ ${r.channel}: ${r.error}`)
|
|
407
|
+
} else {
|
|
408
|
+
console.log('Usage: /channel list|send <msg>|test')
|
|
409
|
+
}
|
|
410
|
+
break
|
|
411
|
+
}
|
|
412
|
+
case 'allow': {
|
|
413
|
+
const allowTool = rest.join(' ') || '*'
|
|
414
|
+
engine.permissionChecker.allowForSession(allowTool, '*')
|
|
415
|
+
console.log(`✅ Tool "${allowTool}" allowed for this session`)
|
|
416
|
+
break
|
|
417
|
+
}
|
|
418
|
+
case 'cost':
|
|
419
|
+
console.log(engine.costTracker.formatReport())
|
|
420
|
+
break
|
|
421
|
+
case 'compact': {
|
|
422
|
+
if (engine.tokenBudget) {
|
|
423
|
+
const { compacted, messages } = autoCompact(engine.state.messages, engine.tokenBudget, { keepRecentTurns: 4 })
|
|
424
|
+
if (compacted) {
|
|
425
|
+
engine.state.messages = messages
|
|
426
|
+
console.log('✅ Context compressed')
|
|
427
|
+
} else {
|
|
428
|
+
console.log('ℹ️ No compression needed')
|
|
301
429
|
}
|
|
302
430
|
} else {
|
|
303
|
-
console.log('
|
|
304
|
-
console.log(' /channel list — List configured channels')
|
|
305
|
-
console.log(' /channel send <msg> — Send message to channels')
|
|
306
|
-
console.log(' /channel test — Test channel connectivity')
|
|
431
|
+
console.log('Token budget not configured')
|
|
307
432
|
}
|
|
308
433
|
break
|
|
309
434
|
}
|
|
310
|
-
case 'exit':
|
|
311
|
-
case 'quit':
|
|
435
|
+
case 'exit': case 'quit':
|
|
312
436
|
console.log('Goodbye!')
|
|
313
437
|
process.exit(0)
|
|
314
438
|
default:
|
|
@@ -320,35 +444,32 @@ export async function main() {
|
|
|
320
444
|
|
|
321
445
|
// 发送到引擎
|
|
322
446
|
try {
|
|
323
|
-
const result = await
|
|
324
|
-
|
|
325
|
-
// 输出助手回复
|
|
447
|
+
const result = await processInput(input)
|
|
326
448
|
console.log()
|
|
327
449
|
console.log(result.response)
|
|
328
450
|
console.log()
|
|
329
|
-
|
|
330
|
-
// 保存到会话
|
|
331
451
|
await sessionManager.appendMessage({ role: 'user', content: input })
|
|
332
452
|
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
453
|
+
// 保存引擎状态到会话
|
|
454
|
+
session.state = session.state || {}
|
|
455
|
+
session.state.turnCount = engine.state.turnCount
|
|
456
|
+
session.state.costHistory = engine.costTracker.history.slice(-50)
|
|
457
|
+
await sessionManager.save(session)
|
|
458
|
+
if (verbose) console.log(`[Turns: ${result.turns} | Tools: ${result.toolResults.length}]`)
|
|
459
|
+
// 显示费用(即使非 verbose 也显示)
|
|
460
|
+
if (engine.costTracker && engine.costTracker.totalApiCalls > 0) {
|
|
461
|
+
console.log(engine.costTracker.formatShort())
|
|
336
462
|
}
|
|
337
463
|
} catch (err) {
|
|
338
464
|
console.error(`\nError: ${err.message}\n`)
|
|
339
|
-
// 错误也通知
|
|
340
465
|
if (channelManager.list().length > 0) {
|
|
341
466
|
await channelManager.sendTemplate('error', {
|
|
342
|
-
task: input.slice(0, 80),
|
|
343
|
-
|
|
344
|
-
}).catch(() => {}) // 通知失败不影响主流程
|
|
467
|
+
task: input.slice(0, 80), error: err.message.slice(0, 200),
|
|
468
|
+
}).catch(() => {})
|
|
345
469
|
}
|
|
346
470
|
}
|
|
347
471
|
rl.prompt()
|
|
348
472
|
})
|
|
349
473
|
|
|
350
|
-
rl.on('close', () => {
|
|
351
|
-
console.log('\nGoodbye!')
|
|
352
|
-
process.exit(0)
|
|
353
|
-
})
|
|
474
|
+
rl.on('close', () => { console.log('\nGoodbye!'); process.exit(0) })
|
|
354
475
|
}
|