@raolin2025/claude-code-node 2.5.2 → 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.
@@ -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
  /**