@raolin2025/claude-code-node 2.6.1 → 2.6.2

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.
@@ -45,6 +45,7 @@ export class QueryEngineConfig {
45
45
  this.initialMessages = options.initialMessages || []
46
46
  this.onConfirmTool = options.onConfirmTool || null // ask 模式确认回调
47
47
  this.readline = options.readline || null // 用于 AskUserQuestion 工具
48
+ this.onDelta = options.onDelta || null // 流式增量回调 {type:'text'|'reasoning', text}(供 VS Code 扩展等 UI 消费)
48
49
  }
49
50
  }
50
51
 
@@ -66,15 +67,17 @@ export class QueryEngine {
66
67
 
67
68
  /**
68
69
  * 主入口 — 处理用户消息
70
+ * @param {string} userInput 文本
71
+ * @param {string[]} [images] 图片 URL(data URL / http),视觉模型用
69
72
  */
70
- async processMessage(userInput) {
73
+ async processMessage(userInput, images = []) {
71
74
  if (this.state.isRunning) {
72
75
  throw new Error('引擎正在运行中,请等待当前回合完成')
73
76
  }
74
77
  this.state.isRunning = true
75
78
  this.state.turnCount++
76
79
  this.abortController = new AbortController()
77
- const userMsg = new UserMessage(userInput)
80
+ const userMsg = new UserMessage(userInput, images)
78
81
  this.state.messages.push(userMsg)
79
82
 
80
83
  // M3: 自动上下文压缩
@@ -166,7 +169,7 @@ export class QueryEngine {
166
169
  if (msg.role === 'system') {
167
170
  request.push({ role: 'system', content: msg.content })
168
171
  } else if (msg.role === 'user') {
169
- request.push({ role: 'user', content: this._formatContent(msg.content) })
172
+ request.push({ role: 'user', content: this._buildUserContent(msg) })
170
173
  } else if (msg.role === 'assistant') {
171
174
  // 构建 assistant 消息基础
172
175
  const asstMsg = {
@@ -402,9 +405,18 @@ export class QueryEngine {
402
405
  try {
403
406
  for await (const event of parseStream(response)) {
404
407
  if (event.type === 'text') {
405
- // 实时输出到终端
406
- process.stdout.write(event.text)
408
+ // 实时输出(有 onDelta 回调时交给调用方,如 VS Code 扩展;否则写终端)
409
+ if (typeof this.config.onDelta === 'function') {
410
+ this.config.onDelta({ type: 'text', text: event.text })
411
+ } else {
412
+ process.stdout.write(event.text)
413
+ }
407
414
  currentText += event.text
415
+ } else if (event.type === 'reasoning') {
416
+ // 推理内容(thinking)同样支持回调
417
+ if (typeof this.config.onDelta === 'function') {
418
+ this.config.onDelta({ type: 'reasoning', text: event.text })
419
+ }
408
420
  } else if (event.type === 'tool_use') {
409
421
  // 收集工具调用
410
422
  result.toolCalls.push(new ToolCall(
@@ -428,8 +440,8 @@ export class QueryEngine {
428
440
  }
429
441
  }
430
442
 
431
- // 流式输出后换行
432
- if (currentText) process.stdout.write('\n')
443
+ // 流式输出后换行(仅终端直写模式;onDelta 回调模式由调用方处理,避免污染协议流)
444
+ if (currentText && typeof this.config.onDelta !== 'function') process.stdout.write('\n')
433
445
 
434
446
  return result
435
447
  }
@@ -442,6 +454,22 @@ export class QueryEngine {
442
454
  return String(content)
443
455
  }
444
456
 
457
+ /**
458
+ * 构建用户消息 content(支持多模态:文本 + 图片)
459
+ * 有 images 时输出 OpenAI 兼容 content 数组;否则纯文本(向后兼容)
460
+ */
461
+ _buildUserContent(msg) {
462
+ const text = this._formatContent(msg.content)
463
+ const images = msg.images && msg.images.length ? msg.images : []
464
+ if (images.length === 0) return text
465
+ const parts = []
466
+ if (text) parts.push({ type: 'text', text })
467
+ for (const url of images) {
468
+ parts.push({ type: 'image_url', image_url: { url } })
469
+ }
470
+ return parts
471
+ }
472
+
445
473
  /** 取消当前运行 */
446
474
  abort() {
447
475
  this.abortController?.abort()
@@ -0,0 +1,426 @@
1
+ /**
2
+ * server.js — cc-node --stdio 服务器(JSON-RPC 2.0 over NDJSON)
3
+ *
4
+ * 协议:docs/stdio-protocol.md v1.0(详见 cc-node-bridge 项目)
5
+ *
6
+ * 职责:
7
+ * - 通过 stdin 接收 JSON-RPC 请求,stdout 返回响应与事件(每行一个 JSON)
8
+ * - 引擎子进程形态:每会话一个进程(由桥接层 spawn 本模块)
9
+ * - remote 工具模式:LLM 产生工具调用 → 发 event/toolCall → 挂起等待
10
+ * toolCall/result → 结果喂回 LLM
11
+ *
12
+ * 用法:
13
+ * cc-node --stdio [--api-base ...] [--model ...] [--api-key ...]
14
+ * node src/stdio/server.js 等同
15
+ */
16
+
17
+ import { createInterface } from 'readline'
18
+ import { randomUUID } from 'crypto'
19
+ import { homedir } from 'os'
20
+ import { join, resolve } from 'path'
21
+ import {
22
+ QueryEngine,
23
+ QueryEngineConfig,
24
+ SessionManager,
25
+ } from '../core/index.js'
26
+ import { CostTracker } from '../core/cost-tracker.js'
27
+ import { builtinTools } from '../tools/index.js'
28
+ import { isLocalLlmServer } from '../utils/index.js'
29
+
30
+ const DEFAULT_SYSTEM_PROMPT =
31
+ 'You are cc-node, an AI coding assistant running as a stdio server. ' +
32
+ 'Tools are executed by the client (VS Code / Web / Telegram). ' +
33
+ 'When you call tools, the client performs them and returns results.'
34
+
35
+ export class StdioServer {
36
+ constructor(options = {}) {
37
+ this.cliArgs = options.cliArgs || {}
38
+ this.config = {
39
+ apiBase: this.cliArgs.apiBase || process.env.LLM_API_BASE || '',
40
+ apiKey: this.cliArgs.apiKey || process.env.LLM_API_KEY || '',
41
+ model: this.cliArgs.model || '',
42
+ systemPrompt: this.cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT,
43
+ maxTurns: this.cliArgs.maxTurns || 50,
44
+ }
45
+ this.initialized = false
46
+ this.running = false
47
+ this.aborted = false
48
+ this.engine = null
49
+ this.toolDefs = null // 客户端 tools/define 下发(null = 用内置定义)
50
+ this.pendingToolCalls = new Map() // toolCallId → resolve({content, isError})
51
+ this.sessionManager = null
52
+ this.currentSession = null
53
+ this.shuttingDown = false
54
+ }
55
+
56
+ // ─────────────────────────── 协议基础 ───────────────────────────
57
+
58
+ start() {
59
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity })
60
+ this._chain = Promise.resolve()
61
+ rl.on('line', (line) => {
62
+ if (!line.trim()) return
63
+ let msg
64
+ try {
65
+ msg = JSON.parse(line)
66
+ } catch {
67
+ this._sendError(null, -32700, '解析错误: 无效 JSON')
68
+ return
69
+ }
70
+ // 串行处理:保证响应/事件顺序(协议要求按序)
71
+ this._chain = this._chain.then(() => this._dispatch(msg)).catch((e) => {
72
+ console.error(`[cc-node-stdio] 处理异常: ${e.message}`)
73
+ })
74
+ })
75
+ rl.on('close', () => {
76
+ this._shutdownNow()
77
+ })
78
+ // stdout 只用于协议,日志走 stderr
79
+ console.error('[cc-node-stdio] 服务器已启动,等待请求…')
80
+ }
81
+
82
+ async _dispatch(msg) {
83
+ const { id, method, params } = msg
84
+
85
+ // 通知(无 id)
86
+ if (id === undefined || id === null) {
87
+ if (method === 'abort') this._abort()
88
+ else if (method === 'shutdown') this._shutdownNow()
89
+ // 其他通知忽略
90
+ return
91
+ }
92
+
93
+ try {
94
+ switch (method) {
95
+ case 'initialize':
96
+ this._sendResponse(id, await this._initialize(params))
97
+ break
98
+ case 'chat':
99
+ this._sendResponse(id, await this._chat(params))
100
+ break
101
+ case 'toolCall/result':
102
+ this._sendResponse(id, await this._toolCallResult(params))
103
+ break
104
+ case 'tools/define':
105
+ this._sendResponse(id, await this._toolsDefine(params))
106
+ break
107
+ case 'session/new':
108
+ this._sendResponse(id, await this._sessionNew(params))
109
+ break
110
+ case 'session/load':
111
+ this._sendResponse(id, await this._sessionLoad(params))
112
+ break
113
+ case 'session/list':
114
+ this._sendResponse(id, await this._sessionList())
115
+ break
116
+ case 'session/delete':
117
+ this._sendResponse(id, await this._sessionDelete(params))
118
+ break
119
+ case 'config/get':
120
+ this._sendResponse(id, this._configGet())
121
+ break
122
+ case 'config/set':
123
+ this._sendResponse(id, await this._configSet(params))
124
+ break
125
+ case 'shutdown':
126
+ this._sendResponse(id, { ok: true })
127
+ this._shutdownNow()
128
+ break
129
+ default:
130
+ this._sendError(id, -32601, `方法不存在: ${method}`)
131
+ }
132
+ } catch (err) {
133
+ this._sendError(id, err.code || -32603, err.message)
134
+ }
135
+ }
136
+
137
+ _sendResponse(id, result) {
138
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n')
139
+ }
140
+
141
+ _sendError(id, code, message) {
142
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n')
143
+ }
144
+
145
+ _sendEvent(method, params) {
146
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
147
+ }
148
+
149
+ // ─────────────────────────── 方法实现 ───────────────────────────
150
+
151
+ async _initialize(params = {}) {
152
+ this.initialized = true
153
+ const clientInfo = params.clientInfo || {}
154
+ console.error(`[cc-node-stdio] 客户端接入: ${clientInfo.name || 'unknown'} ${clientInfo.version || ''}`)
155
+ return {
156
+ serverInfo: { name: 'cc-node', version: '2.6.2' },
157
+ capabilities: {
158
+ toolExecution: 'remote',
159
+ streaming: true,
160
+ images: true,
161
+ modes: ['plan', 'code'],
162
+ sessions: true,
163
+ abort: true,
164
+ },
165
+ }
166
+ }
167
+
168
+ async _chat(params = {}) {
169
+ if (!this.initialized) throw this._err(3, '未初始化,先发送 initialize')
170
+ if (this.running) throw this._err(1, '引擎正在运行中')
171
+ const text = params.text
172
+ if (typeof text !== 'string' || !text.trim()) throw this._err(-32602, '缺少 text 参数')
173
+
174
+ // 模式切换(若提供)
175
+ if (params.mode && params.mode !== this._mode) {
176
+ this._mode = params.mode
177
+ this._rebuildEngine()
178
+ this._sendEvent('event/mode', { mode: this._mode })
179
+ }
180
+
181
+ this.running = true
182
+ this.aborted = false
183
+ this._sendEvent('event/status', { state: 'running' })
184
+
185
+ // 异步执行(不阻塞响应)
186
+ this._runChat(text, params.images || [], params.sessionId).catch((err) => {
187
+ this._sendEvent('event/error', { message: err.message })
188
+ this.running = false
189
+ this._sendEvent('event/status', { state: 'idle' })
190
+ })
191
+
192
+ return { accepted: true }
193
+ }
194
+
195
+ async _runChat(text, images, sessionId) {
196
+ try {
197
+ const engine = await this._getEngine()
198
+ const result = await engine.processMessage(text, images)
199
+ if (this.aborted) {
200
+ this._sendEvent('event/done', { content: '', aborted: true, turns: engine.state.turnCount })
201
+ } else {
202
+ // 持久化到会话
203
+ try {
204
+ const session = await this._getSession(sessionId)
205
+ await this.sessionManager.appendMessage({ role: 'assistant', content: result.response })
206
+ } catch (e) { /* 会话保存失败不影响响应 */ }
207
+ this._sendEvent('event/done', {
208
+ content: result.response,
209
+ usage: engine.costTracker ? this._lastUsage(engine) : {},
210
+ turns: result.turns,
211
+ aborted: false,
212
+ })
213
+ }
214
+ } catch (err) {
215
+ if (err.message === '操作已取消' || err.name === 'AbortError') {
216
+ this._sendEvent('event/done', { content: '', aborted: true, turns: 0 })
217
+ } else {
218
+ this._sendEvent('event/error', { message: err.message })
219
+ }
220
+ } finally {
221
+ this.running = false
222
+ this._sendEvent('event/status', { state: 'idle' })
223
+ }
224
+ }
225
+
226
+ _lastUsage(engine) {
227
+ // QueryEngine 内部 costTracker 记录在 usageHistory 或直接读取最近记录
228
+ try {
229
+ const history = engine.costTracker.usageHistory || []
230
+ return history.length ? history[history.length - 1] : {}
231
+ } catch {
232
+ return {}
233
+ }
234
+ }
235
+
236
+ _abort() {
237
+ if (this.engine && this.engine.abortController) {
238
+ this.aborted = true
239
+ this.engine.abortController.abort()
240
+ console.error('[cc-node-stdio] 已请求中断')
241
+ }
242
+ }
243
+
244
+ async _toolCallResult(params = {}) {
245
+ const { toolCallId, result, isError } = params
246
+ const resolve = this.pendingToolCalls.get(toolCallId)
247
+ if (!resolve) throw this._err(-32602, `未知的 toolCallId: ${toolCallId}`)
248
+ this.pendingToolCalls.delete(toolCallId)
249
+ resolve({ content: String(result ?? ''), isError: !!isError })
250
+ return { accepted: true }
251
+ }
252
+
253
+ async _toolsDefine(params = {}) {
254
+ const tools = params.tools
255
+ if (!Array.isArray(tools)) throw this._err(-32602, 'tools 必须为数组')
256
+ this.toolDefs = tools
257
+ this._rebuildEngine()
258
+ return { ok: true, count: tools.length }
259
+ }
260
+
261
+ async _sessionNew(params = {}) {
262
+ const sm = await this._getSessionManager()
263
+ const session = await sm.create(params.title || '')
264
+ this.currentSession = session
265
+ this._rebuildEngine()
266
+ return { sessionId: session.id, title: session.title }
267
+ }
268
+
269
+ async _sessionLoad(params = {}) {
270
+ const sm = await this._getSessionManager()
271
+ const session = await sm.load(params.sessionId)
272
+ if (!session) throw this._err(-32602, `会话不存在: ${params.sessionId}`)
273
+ this.currentSession = session
274
+ this._rebuildEngine()
275
+ return { sessionId: session.id, title: session.title, messageCount: session.messages?.length || 0 }
276
+ }
277
+
278
+ async _sessionList() {
279
+ const sm = await this._getSessionManager()
280
+ const sessions = await sm.list()
281
+ return { sessions }
282
+ }
283
+
284
+ async _sessionDelete(params = {}) {
285
+ const sm = await this._getSessionManager()
286
+ return { ok: await sm.delete(params.sessionId) }
287
+ }
288
+
289
+ _configGet() {
290
+ return {
291
+ config: {
292
+ apiBase: this.config.apiBase,
293
+ apiKey: this.config.apiKey ? '***' : '',
294
+ model: this.config.model,
295
+ mode: this._mode,
296
+ },
297
+ }
298
+ }
299
+
300
+ async _configSet(params = {}) {
301
+ const c = params.config || {}
302
+ if (c.apiBase !== undefined) this.config.apiBase = String(c.apiBase)
303
+ if (c.apiKey !== undefined) this.config.apiKey = String(c.apiKey)
304
+ if (c.model !== undefined) this.config.model = String(c.model)
305
+ if (c.systemPrompt !== undefined) this.config.systemPrompt = String(c.systemPrompt)
306
+ this._rebuildEngine()
307
+ return { ok: true }
308
+ }
309
+
310
+ _shutdownNow() {
311
+ if (this.shuttingDown) return
312
+ this.shuttingDown = true
313
+ console.error('[cc-node-stdio] 关闭中…')
314
+ process.exitCode = 0
315
+ // 等待 stdout 缓冲刷完再退出(立即 exit 会丢弃未写入的响应)
316
+ setTimeout(() => process.exit(0), 50)
317
+ }
318
+
319
+ // ─────────────────────────── 引擎 ───────────────────────────
320
+
321
+ get _mode() {
322
+ return this._currentMode || 'code'
323
+ }
324
+
325
+ set _mode(v) {
326
+ this._currentMode = v
327
+ }
328
+
329
+ async _getSessionManager() {
330
+ if (!this.sessionManager) {
331
+ const sessionsDir = join(homedir(), '.cc-node', 'sessions')
332
+ this.sessionManager = new SessionManager({ sessionsDir })
333
+ await this.sessionManager.ensureDir()
334
+ }
335
+ return this.sessionManager
336
+ }
337
+
338
+ async _getSession(sessionId) {
339
+ const sm = await this._getSessionManager()
340
+ if (sessionId) {
341
+ const s = await sm.load(sessionId)
342
+ if (s) {
343
+ this.currentSession = s
344
+ return s
345
+ }
346
+ }
347
+ if (!this.currentSession) {
348
+ this.currentSession = await sm.create()
349
+ }
350
+ return this.currentSession
351
+ }
352
+
353
+ /** 构建工具列表(remote 模式:handler 转发给客户端执行) */
354
+ _buildTools() {
355
+ // 客户端下发优先;否则用内置工具定义
356
+ const defs = this.toolDefs || builtinTools.map((t) => ({
357
+ name: t.name,
358
+ description: t.description,
359
+ parameters: t.parameters,
360
+ }))
361
+ return defs.map((t) => ({
362
+ name: t.name,
363
+ description: t.description || '',
364
+ parameters: t.parameters || { type: 'object', properties: {} },
365
+ handler: async (input) => {
366
+ const toolCallId = `call_${randomUUID()}`
367
+ this._sendEvent('event/toolCall', {
368
+ toolCall: { id: toolCallId, name: t.name, input: input || {} },
369
+ })
370
+ return new Promise((resolve) => {
371
+ this.pendingToolCalls.set(toolCallId, resolve)
372
+ })
373
+ },
374
+ }))
375
+ }
376
+
377
+ async _getEngine() {
378
+ if (this.engine) return this.engine
379
+ if (!this.config.apiBase) {
380
+ throw this._err(4, '未设置 apiBase(config/set 或 --api-base)')
381
+ }
382
+ if (!this.config.model) {
383
+ throw this._err(4, '未设置 model(config/set 或 --model)')
384
+ }
385
+ return this._rebuildEngine()
386
+ }
387
+
388
+ _rebuildEngine() {
389
+ const apiBase = this.config.apiBase
390
+ const localServer = isLocalLlmServer(apiBase)
391
+ const apiKey = this.config.apiKey
392
+ if (!localServer && !apiKey) {
393
+ console.error('[cc-node-stdio] 警告: 非本地服务未设置 apiKey')
394
+ }
395
+ this.engine = new QueryEngine(
396
+ new QueryEngineConfig({
397
+ cwd: resolve(this.cliArgs.cwd || process.cwd()),
398
+ model: this.config.model,
399
+ apiBase,
400
+ apiKey,
401
+ systemPrompt: this.config.systemPrompt,
402
+ maxTurns: this.config.maxTurns,
403
+ permissionMode: 'always-allow', // 安全底线仍检查;执行确认由客户端负责
404
+ tools: this._buildTools(),
405
+ verbose: false,
406
+ costTracker: new CostTracker({ model: this.config.model }),
407
+ onDelta: (evt) => {
408
+ this._sendEvent('event/delta', { kind: evt.type, text: evt.text })
409
+ },
410
+ })
411
+ )
412
+ return this.engine
413
+ }
414
+
415
+ _err(code, message) {
416
+ const e = new Error(message)
417
+ e.code = code
418
+ return e
419
+ }
420
+ }
421
+
422
+ /** 直接运行:node src/stdio/server.js */
423
+ if (process.argv[1] && process.argv[1].endsWith('server.js')) {
424
+ const server = new StdioServer()
425
+ server.start()
426
+ }
@@ -11,8 +11,8 @@ import { webFetchTool } from './web-fetch.js'
11
11
  import { webSearchTool } from './web-search.js'
12
12
  import { askUserTool } from './ask-user.js'
13
13
  import { gitTool } from './git-tool.js'
14
- // QQ Bot 工具(外部包装)
15
- import { qqbotTools } from '../tools/qqbot-tools-wrapper.js'
14
+ // Telegram 工具(替代原 QQ Bot 工具,QQ 通道已放弃)
15
+ import { telegramTools } from './telegram-tools.js'
16
16
 
17
17
  /**
18
18
  * 所有内置工具列表
@@ -28,7 +28,7 @@ export const builtinTools = [
28
28
  webSearchTool,
29
29
  askUserTool,
30
30
  gitTool,
31
- ...qqbotTools
31
+ ...telegramTools
32
32
  ]
33
33
 
34
34
  /**