@raolin2025/claude-code-node 1.0.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.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * 配置管理
3
+ * 对应原版: src/query/config.ts + src/utils/config.ts
4
+ */
5
+ import { readFile, writeFile, mkdir } from 'fs/promises'
6
+ import { resolve, join } from 'path'
7
+ import { existsSync } from 'fs'
8
+ import { homedir } from 'os'
9
+
10
+ const PROJECT_CONFIG_FILE = '.claude-code/config.json'
11
+ const USER_CONFIG_FILE = join(homedir(), '.claude-code/config.json')
12
+
13
+ /**
14
+ * 默认配置
15
+ */
16
+ const DEFAULTS = {
17
+ model: 'deepseek-chat',
18
+ apiBase: 'https://api.deepseek.com/v1',
19
+ maxTurns: 100,
20
+ maxBudgetTokens: 1_000_000,
21
+ permissionMode: 'ask',
22
+ verbose: false,
23
+ apiKey: '',
24
+ sessionsDir: '.claude-code/sessions',
25
+ tools: {
26
+ bash: { timeout: 120, allowed: true },
27
+ fileRead: { maxLines: 2000, maxSizeKB: 256 },
28
+ webFetch: { timeout: 30, maxChars: 100000 },
29
+ },
30
+ mcp: {
31
+ servers: {},
32
+ },
33
+ }
34
+
35
+ export class Config {
36
+ constructor() {
37
+ this.data = { ...DEFAULTS }
38
+ this._projectPath = null
39
+ this._userPath = USER_CONFIG_FILE
40
+ }
41
+
42
+ /** 从项目目录加载配置 */
43
+ async loadFromProject(projectDir) {
44
+ this._projectPath = join(projectDir, PROJECT_CONFIG_FILE)
45
+ await this._load(this._projectPath)
46
+ }
47
+
48
+ /** 从用户目录加载配置 */
49
+ async loadFromUser() {
50
+ await this._load(this._userPath)
51
+ }
52
+
53
+ /** 完整加载流程:用户级 → 项目级(项目级覆盖用户级) */
54
+ async load(projectDir) {
55
+ await this.loadFromUser()
56
+ if (projectDir) await this.loadFromProject(projectDir)
57
+ }
58
+
59
+ async _load(filePath) {
60
+ try {
61
+ const raw = await readFile(filePath, 'utf-8')
62
+ const data = JSON.parse(raw)
63
+ this.data = this._deepMerge(this.data, data)
64
+ } catch {
65
+ // 文件不存在或不合法 — 使用默认值
66
+ }
67
+ }
68
+
69
+ /** 保存到项目配置 */
70
+ async saveToProject(projectDir) {
71
+ const dir = join(projectDir, '.claude-code')
72
+ await mkdir(dir, { recursive: true })
73
+ const filePath = join(dir, 'config.json')
74
+ await writeFile(filePath, JSON.stringify(this.data, null, 2), 'utf-8')
75
+ }
76
+
77
+ /** 保存到用户配置 */
78
+ async saveToUser() {
79
+ const dir = join(homedir(), '.claude-code')
80
+ await mkdir(dir, { recursive: true })
81
+ await writeFile(this._userPath, JSON.stringify(this.data, null, 2), 'utf-8')
82
+ }
83
+
84
+ /** 获取配置值(支持点号路径,如 "tools.bash.timeout") */
85
+ get(key) {
86
+ if (!key) return this.data
87
+ const parts = key.split('.')
88
+ let current = this.data
89
+ for (const part of parts) {
90
+ if (current == null) return undefined
91
+ current = current[part]
92
+ }
93
+ return current
94
+ }
95
+
96
+ /** 设置配置值 */
97
+ set(key, value) {
98
+ const parts = key.split('.')
99
+ let current = this.data
100
+ for (let i = 0; i < parts.length - 1; i++) {
101
+ if (current[parts[i]] == null) current[parts[i]] = {}
102
+ current = current[parts[i]]
103
+ }
104
+ current[parts[parts.length - 1]] = value
105
+ }
106
+
107
+ /** 深度合并 */
108
+ _deepMerge(target, source) {
109
+ const result = { ...target }
110
+ for (const key of Object.keys(source)) {
111
+ if (
112
+ source[key] &&
113
+ typeof source[key] === 'object' &&
114
+ !Array.isArray(source[key]) &&
115
+ target[key] &&
116
+ typeof target[key] === 'object' &&
117
+ !Array.isArray(target[key])
118
+ ) {
119
+ result[key] = this._deepMerge(target[key], source[key])
120
+ } else {
121
+ result[key] = source[key]
122
+ }
123
+ }
124
+ return result
125
+ }
126
+
127
+ /** 导出为 JSON */
128
+ toJSON() {
129
+ return { ...this.data }
130
+ }
131
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 核心模块统一导出
3
+ */
4
+ export { QueryEngine, QueryEngineConfig } from './query-engine.js'
5
+ export { TokenBudget, estimateTokens } from './token-budget.js'
6
+ export { SessionManager } from './session.js'
7
+ export { Config } from './config.js'
8
+ export { parseStream, parseNonStreamResponse } from './streaming.js'
9
+ export { main } from './cli.js'
@@ -0,0 +1,344 @@
1
+ /**
2
+ * QueryEngine — Claude Code 核心引擎的 Node.js 重构
3
+ *
4
+ * 原版: src/QueryEngine.ts (46K 行)
5
+ * 职责: LLM API 调用 → 工具调用循环 → 流式响应 → 重试逻辑
6
+ *
7
+ * 核心循环:
8
+ * 用户输入 → 构建消息列表 → 调用 LLM → 解析工具调用 →
9
+ * 执行工具 → 把结果喂回 LLM → 循环直到无工具调用 → 输出
10
+ *
11
+ * API 协议: OpenAI 兼容(全行业通用)
12
+ * 适用于: OpenAI / DeepSeek / Qwen / GLM / Kimi / Ollama / vLLM / LM Studio / 任何兼容接口
13
+ */
14
+ import crypto from 'crypto'
15
+ import { Message, UserMessage, AssistantMessage, SystemMessage, ToolCall, ToolResult, SessionState } from '../types/index.js'
16
+ import { EnhancedPermissionChecker } from '../security/enhanced-permission.js'
17
+
18
+ /**
19
+ * 配置选项
20
+ */
21
+ export class QueryEngineConfig {
22
+ constructor(options = {}) {
23
+ this.cwd = options.cwd || process.cwd()
24
+ this.tools = options.tools || []
25
+ this.commands = options.commands || []
26
+ this.systemPrompt = options.systemPrompt || ''
27
+ // 默认模型 — 不绑定任何厂商,用户必须通过 --model 或配置指定
28
+ this.model = options.model || ''
29
+ this.maxTurns = options.maxTurns || 100
30
+ this.maxBudgetTokens = options.maxBudgetTokens || 1_000_000
31
+ this.permissionMode = options.permissionMode || 'ask'
32
+ this.verbose = options.verbose || false
33
+ // API 配置 — 通用 OpenAI 兼容协议
34
+ // 优先级: 构造参数 > LLM_API_KEY > 厂商专用 Key
35
+ this.apiKey = options.apiKey || process.env.LLM_API_KEY || process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY || process.env.QWEN_API_KEY || process.env.GLM_API_KEY || process.env.KIMI_API_KEY || ''
36
+ // API Base — DeepSeek 为默认
37
+ this.apiBase = options.apiBase || process.env.LLM_API_BASE || 'https://api.deepseek.com/v1'
38
+ this.initialMessages = options.initialMessages || []
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 查询引擎 — 核心循环
44
+ */
45
+ export class QueryEngine {
46
+ constructor(config) {
47
+ this.config = config instanceof QueryEngineConfig ? config : new QueryEngineConfig(config)
48
+ this.state = new SessionState()
49
+ this.permissionChecker = new EnhancedPermissionChecker(this.config.permissionMode, {
50
+ cwd: this.config.cwd,
51
+ projectDir: this.config.cwd,
52
+ })
53
+ this.abortController = null
54
+ }
55
+
56
+ /**
57
+ * 主入口 — 处理用户消息
58
+ */
59
+ async processMessage(userInput) {
60
+ if (this.state.isRunning) {
61
+ throw new Error('引擎正在运行中,请等待当前回合完成')
62
+ }
63
+ this.state.isRunning = true
64
+ this.state.turnCount++
65
+ this.abortController = new AbortController()
66
+ const userMsg = new UserMessage(userInput)
67
+ this.state.messages.push(userMsg)
68
+
69
+ try {
70
+ const result = await this._runToolLoop(userMsg)
71
+ return result
72
+ } finally {
73
+ this.state.isRunning = false
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 工具调用循环 — 核心逻辑
79
+ *
80
+ * LLM 回复可能包含工具调用 → 执行工具 → 把工具结果喂回 LLM
81
+ * 最多跑 maxTurns 次
82
+ */
83
+ async _runToolLoop(userMessage) {
84
+ let currentMessages = [...this.state.messages]
85
+ let finalResponse = ''
86
+
87
+ for (let turn = 0; turn < this.config.maxTurns; turn++) {
88
+ const requestMessages = this._buildRequest(currentMessages)
89
+ const response = await this._callLLM(requestMessages, currentMessages)
90
+
91
+ if (this.abortController.signal.aborted) {
92
+ throw new Error('操作已取消')
93
+ }
94
+
95
+ // 没有工具调用 → 最终回复
96
+ if (!response.toolCalls || response.toolCalls.length === 0) {
97
+ finalResponse = response.content
98
+ const assistantMsg = new AssistantMessage(response.content)
99
+ this.state.messages.push(assistantMsg)
100
+ break
101
+ }
102
+
103
+ // 有工具调用 → 记录助手消息
104
+ const assistantMsg = new AssistantMessage(response.content, response.toolCalls)
105
+ this.state.messages.push(assistantMsg)
106
+
107
+ // 执行工具
108
+ const toolResults = await this._executeToolCalls(response.toolCalls)
109
+
110
+ // 工具结果加入消息流(OpenAI 兼容格式)
111
+ for (const result of toolResults) {
112
+ // 先加 assistant 的 tool_calls 消息
113
+ currentMessages.push({
114
+ role: 'assistant',
115
+ content: null,
116
+ tool_calls: [{
117
+ id: result.toolCallId,
118
+ type: 'function',
119
+ function: {
120
+ name: result.toolName || '',
121
+ arguments: '{}',
122
+ },
123
+ }],
124
+ })
125
+ // 再加 tool 结果消息
126
+ currentMessages.push({
127
+ role: 'tool',
128
+ tool_call_id: result.toolCallId,
129
+ content: result.isError
130
+ ? `[ERROR] ${result.content}`
131
+ : result.content,
132
+ })
133
+ this.state.toolResults.set(result.toolCallId, result)
134
+ }
135
+
136
+ if (this.config.verbose) {
137
+ console.error(`[QueryEngine] 工具循环第 ${turn + 1} 轮完成,执行了 ${toolResults.length} 个工具`)
138
+ }
139
+ }
140
+
141
+ if (!finalResponse && this.state.turnCount >= this.config.maxTurns) {
142
+ finalResponse = `[达到最大回合数限制 (${this.config.maxTurns}),停止响应]`
143
+ }
144
+
145
+ return {
146
+ response: finalResponse,
147
+ turns: this.state.turnCount,
148
+ toolResults: Array.from(this.state.toolResults.values()),
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 构建 LLM 请求消息列表
154
+ */
155
+ _buildRequest(messages) {
156
+ const request = []
157
+
158
+ // 系统提示
159
+ if (this.config.systemPrompt) {
160
+ request.push({ role: 'system', content: this.config.systemPrompt })
161
+ }
162
+
163
+ // 历史消息
164
+ for (const msg of messages) {
165
+ if (msg.role === 'system') {
166
+ request.push({ role: 'system', content: msg.content })
167
+ } else if (msg.role === 'user') {
168
+ request.push({ role: 'user', content: this._formatContent(msg.content) })
169
+ } else if (msg.role === 'assistant') {
170
+ const content = []
171
+ if (msg.content) content.push({ type: 'text', text: msg.content })
172
+ if (msg.toolCalls) {
173
+ for (const tc of msg.toolCalls) {
174
+ content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input })
175
+ }
176
+ }
177
+ request.push({ role: 'assistant', content })
178
+ }
179
+ }
180
+
181
+ return request
182
+ }
183
+
184
+ /**
185
+ * 执行工具调用
186
+ */
187
+ async _executeToolCalls(toolCalls) {
188
+ const results = []
189
+ for (const tc of toolCalls) {
190
+ // 安全检查(一票否决)
191
+ const permResult = await this.permissionChecker.check(tc.name, tc.input)
192
+ if (!permResult.allowed) {
193
+ results.push(new ToolResult(tc.id, `工具调用被安全策略拒绝: ${tc.name} — ${permResult.reason || ''}`, true))
194
+ continue
195
+ }
196
+
197
+ // 查找工具
198
+ const tool = this.config.tools.find(t => t.name === tc.name)
199
+ if (!tool) {
200
+ results.push(new ToolResult(tc.id, `未找到工具: ${tc.name}`, true))
201
+ continue
202
+ }
203
+
204
+ try {
205
+ tc.status = 'running'
206
+ const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this })
207
+ tc.status = 'done'
208
+ results.push(new ToolResult(tc.id, typeof content === 'string' ? content : JSON.stringify(content), false))
209
+ // 记录工具名用于消息构建
210
+ results[results.length - 1].toolName = tc.name
211
+ } catch (err) {
212
+ tc.status = 'error'
213
+ results.push(new ToolResult(tc.id, `工具执行错误: ${err.message}`, true))
214
+ results[results.length - 1].toolName = tc.name
215
+ }
216
+ }
217
+ return results
218
+ }
219
+
220
+ /**
221
+ * 调用 LLM API — OpenAI 兼容协议(全行业通用)
222
+ *
223
+ * 支持的提供商(举例):
224
+ * - DeepSeek: https://api.deepseek.com/v1
225
+ * - 通义千问: https://dashscope.aliyuncs.com/compatible-mode/v1
226
+ * - 智谱 GLM: https://open.bigmodel.cn/api/paas/v4
227
+ * - Moonshot Kimi: https://api.moonshot.cn/v1
228
+ * - Ollama 本地: http://localhost:11434/v1
229
+ * - vLLM: http://localhost:8000/v1
230
+ * - LM Studio: http://localhost:1234/v1
231
+ * - OpenAI: https://api.openai.com/v1
232
+ */
233
+ async _callLLM(messages, contextMessages) {
234
+ const apiKey = this.config.apiKey
235
+ const apiBase = this.config.apiBase
236
+
237
+ if (!apiKey) {
238
+ throw new Error(
239
+ `未设置 API Key。请设置以下环境变量之一:\n` +
240
+ ` LLM_API_KEY=xxx (通用,推荐)\n` +
241
+ ` DEEPSEEK_API_KEY=xxx (DeepSeek)\n` +
242
+ ` OPENAI_API_KEY=xxx (OpenAI)\n` +
243
+ ` QWEN_API_KEY=xxx (通义千问)\n` +
244
+ ` GLM_API_KEY=xxx (智谱 GLM)\n` +
245
+ ` KIMI_API_KEY=xxx (Moonshot Kimi)\n` +
246
+ `或通过 --api-key 参数传入`
247
+ )
248
+ }
249
+
250
+ if (!apiBase) {
251
+ throw new Error(
252
+ `未设置 API Base URL。默认使用 https://api.deepseek.com/v1
253
+ ` +
254
+ `可通过 LLM_API_BASE 或 --api-base 参数切换其他提供商,例如:\n` +
255
+ ` --api-base https://dashscope.aliyuncs.com/compatible-mode/v1 (通义千问)\n` +
256
+ ` --api-base https://open.bigmodel.cn/api/paas/v4 (智谱 GLM)\n` +
257
+ ` --api-base https://api.moonshot.cn/v1 (Moonshot Kimi)\n` +
258
+ ` --api-base http://localhost:11434/v1 (Ollama 本地)\n` +
259
+ ` --api-base http://localhost:8000/v1 (vLLM 本地)\n` +
260
+ ` --api-base https://api.openai.com/v1 (OpenAI)`
261
+ )
262
+ }
263
+
264
+ // 构建工具定义 — OpenAI function-calling 格式
265
+ const tools = this.config.tools.map(t => ({
266
+ type: 'function',
267
+ function: {
268
+ name: t.name,
269
+ description: t.description,
270
+ parameters: t.parameters,
271
+ },
272
+ }))
273
+
274
+ const body = {
275
+ model: this.config.model,
276
+ messages,
277
+ max_tokens: 4096,
278
+ ...(tools.length && { tools }),
279
+ }
280
+
281
+ const url = apiBase.replace(/\/+$/, '') + '/chat/completions'
282
+
283
+ const response = await fetch(url, {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Content-Type': 'application/json',
287
+ 'Authorization': `Bearer ${apiKey}`,
288
+ },
289
+ body: JSON.stringify(body),
290
+ signal: this.abortController?.signal,
291
+ })
292
+
293
+ if (!response.ok) {
294
+ const errText = await response.text()
295
+ throw new Error(`API 错误 ${response.status}: ${errText}`)
296
+ }
297
+
298
+ const data = await response.json()
299
+ return this._parseResponse(data)
300
+ }
301
+
302
+ /**
303
+ * 解析 OpenAI 兼容响应
304
+ */
305
+ _parseResponse(data) {
306
+ const result = { content: '', toolCalls: [] }
307
+ const choice = data.choices?.[0]
308
+ if (!choice) return result
309
+
310
+ const message = choice.message
311
+ if (message.content) {
312
+ result.content = message.content
313
+ }
314
+
315
+ for (const tc of (message.tool_calls || [])) {
316
+ let input = {}
317
+ try {
318
+ input = JSON.parse(tc.function.arguments || '{}')
319
+ } catch {
320
+ input = { _raw: tc.function.arguments }
321
+ }
322
+ result.toolCalls.push(new ToolCall(tc.id, tc.function.name, input))
323
+ }
324
+
325
+ return result
326
+ }
327
+
328
+ /** 格式化内容 */
329
+ _formatContent(content) {
330
+ if (typeof content === 'string') return content
331
+ if (typeof content === 'object') return JSON.stringify(content)
332
+ return String(content)
333
+ }
334
+
335
+ /** 取消当前运行 */
336
+ abort() {
337
+ this.abortController?.abort()
338
+ }
339
+
340
+ /** 重置引擎状态 */
341
+ reset() {
342
+ this.state = new SessionState()
343
+ }
344
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 会话管理
3
+ * 对应原版: src/utils/sessionState.ts + src/utils/sessionStorage.ts
4
+ */
5
+ import { readFile, writeFile, mkdir, readdir, rm } from 'fs/promises'
6
+ import { resolve, join } from 'path'
7
+ import { existsSync } from 'fs'
8
+ import { SessionState } from '../types/index.js'
9
+
10
+ const DEFAULT_SESSIONS_DIR = '.claude-code/sessions'
11
+
12
+ export class SessionManager {
13
+ constructor(options = {}) {
14
+ this.sessionsDir = options.sessionsDir || resolve(process.cwd(), DEFAULT_SESSIONS_DIR)
15
+ this.currentSession = null
16
+ }
17
+
18
+ /** 确保会话目录存在 */
19
+ async ensureDir() {
20
+ await mkdir(this.sessionsDir, { recursive: true })
21
+ }
22
+
23
+ /** 创建新会话 */
24
+ async create(title = '') {
25
+ await this.ensureDir()
26
+ const id = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
27
+ const session = {
28
+ id,
29
+ title: title || `Session ${new Date().toISOString().slice(0, 19)}`,
30
+ created: new Date().toISOString(),
31
+ updated: new Date().toISOString(),
32
+ messages: [],
33
+ state: { turnCount: 0, budgetUsed: 0 },
34
+ }
35
+ await this.save(session)
36
+ this.currentSession = session
37
+ return session
38
+ }
39
+
40
+ /** 保存会话 */
41
+ async save(session) {
42
+ await this.ensureDir()
43
+ session.updated = new Date().toISOString()
44
+ const filePath = join(this.sessionsDir, `${session.id}.json`)
45
+ await writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8')
46
+ return session
47
+ }
48
+
49
+ /** 加载会话 */
50
+ async load(sessionId) {
51
+ const filePath = join(this.sessionsDir, `${sessionId}.json`)
52
+ try {
53
+ const data = await readFile(filePath, 'utf-8')
54
+ const session = JSON.parse(data)
55
+ this.currentSession = session
56
+ return session
57
+ } catch (err) {
58
+ if (err.code === 'ENOENT') {
59
+ return null
60
+ }
61
+ throw err
62
+ }
63
+ }
64
+
65
+ /** 列出所有会话 */
66
+ async list() {
67
+ await this.ensureDir()
68
+ const files = await readdir(this.sessionsDir)
69
+ const sessions = []
70
+ for (const file of files) {
71
+ if (!file.endsWith('.json')) continue
72
+ try {
73
+ const data = await readFile(join(this.sessionsDir, file), 'utf-8')
74
+ const session = JSON.parse(data)
75
+ sessions.push({
76
+ id: session.id,
77
+ title: session.title,
78
+ created: session.created,
79
+ updated: session.updated,
80
+ messageCount: session.messages?.length || 0,
81
+ })
82
+ } catch { /* skip corrupted */ }
83
+ }
84
+ // 按更新时间倒序
85
+ sessions.sort((a, b) => new Date(b.updated) - new Date(a.updated))
86
+ return sessions
87
+ }
88
+
89
+ /** 删除会话 */
90
+ async delete(sessionId) {
91
+ const filePath = join(this.sessionsDir, `${sessionId}.json`)
92
+ try {
93
+ await rm(filePath)
94
+ if (this.currentSession?.id === sessionId) {
95
+ this.currentSession = null
96
+ }
97
+ return true
98
+ } catch {
99
+ return false
100
+ }
101
+ }
102
+
103
+ /** 获取或创建当前会话 */
104
+ async getOrCreate(title) {
105
+ if (this.currentSession) return this.currentSession
106
+ return this.create(title)
107
+ }
108
+
109
+ /** 追加消息到当前会话 */
110
+ async appendMessage(message) {
111
+ if (!this.currentSession) await this.getOrCreate()
112
+ this.currentSession.messages.push({
113
+ role: message.role,
114
+ content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
115
+ timestamp: new Date().toISOString(),
116
+ })
117
+ await this.save(this.currentSession)
118
+ return this.currentSession
119
+ }
120
+ }