@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.
- package/README.md +17 -0
- package/package.json +2 -2
- package/src/channel/notify-daemon.js +105 -18
- package/src/channel/qqbot-listener.js +3 -7
- package/src/channel/tg-listener.js +1 -1
- package/src/channel/tg-proxy.js +105 -32
- package/src/core/cli.js +113 -2
- package/src/core/index.js +2 -0
- package/src/core/query-engine.js +35 -7
- package/src/stdio/server.js +426 -0
- package/src/tools/index.js +3 -3
- package/src/tools/telegram-tools.js +319 -0
- package/src/types/index.js +6 -1
- package/src/utils/llm-server.js +1 -1
- package/src/tools/qqbot-channel-api.js +0 -147
- package/src/tools/qqbot-media.js +0 -177
- package/src/tools/qqbot-remind.js +0 -42
- package/src/tools/qqbot-tools-wrapper.js +0 -185
package/src/core/query-engine.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/tools/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
...
|
|
31
|
+
...telegramTools
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
/**
|