@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.
@@ -12,8 +12,12 @@
12
12
  * 适用于: OpenAI / DeepSeek / Qwen / GLM / Kimi / Ollama / vLLM / LM Studio / 任何兼容接口
13
13
  */
14
14
  import crypto from 'crypto'
15
- import { Message, UserMessage, AssistantMessage, SystemMessage, ToolCall, ToolResult, SessionState } from '../types/index.js'
15
+ import { UserMessage, AssistantMessage, ToolCall, ToolResult, SessionState } from '../types/index.js'
16
+ import { parseStream, parseNonStreamResponse } from './streaming.js'
17
+ import { autoCompact } from './compact.js'
18
+ import { CostTracker } from './cost-tracker.js'
16
19
  import { EnhancedPermissionChecker } from '../security/enhanced-permission.js'
20
+ import { checkHostSafety } from '../security/ssrf-guard.js'
17
21
 
18
22
  /**
19
23
  * 配置选项
@@ -35,7 +39,12 @@ export class QueryEngineConfig {
35
39
  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
40
  // API Base — DeepSeek 为默认
37
41
  this.apiBase = options.apiBase || process.env.LLM_API_BASE || 'https://api.deepseek.com/v1'
42
+ this.noStream = options.noStream || false
43
+ this.costTracker = options.costTracker || null
44
+ this.tokenBudget = options.tokenBudget || null
38
45
  this.initialMessages = options.initialMessages || []
46
+ this.onConfirmTool = options.onConfirmTool || null // ask 模式确认回调
47
+ this.readline = options.readline || null // 用于 AskUserQuestion 工具
39
48
  }
40
49
  }
41
50
 
@@ -51,6 +60,8 @@ export class QueryEngine {
51
60
  projectDir: this.config.cwd,
52
61
  })
53
62
  this.abortController = null
63
+ this.costTracker = this.config.costTracker || new CostTracker({ model: this.config.model })
64
+ this.tokenBudget = this.config.tokenBudget || null
54
65
  }
55
66
 
56
67
  /**
@@ -66,6 +77,15 @@ export class QueryEngine {
66
77
  const userMsg = new UserMessage(userInput)
67
78
  this.state.messages.push(userMsg)
68
79
 
80
+ // M3: 自动上下文压缩
81
+ if (this.tokenBudget) {
82
+ const { compacted, messages } = autoCompact(this.state.messages, this.tokenBudget)
83
+ if (compacted) {
84
+ this.state.messages = messages
85
+ if (this.config.verbose) console.error('[compact] Context compressed to fit token budget')
86
+ }
87
+ }
88
+
69
89
  try {
70
90
  const result = await this._runToolLoop(userMsg)
71
91
  return result
@@ -81,12 +101,11 @@ export class QueryEngine {
81
101
  * 最多跑 maxTurns 次
82
102
  */
83
103
  async _runToolLoop(userMessage) {
84
- let currentMessages = [...this.state.messages]
85
104
  let finalResponse = ''
86
105
 
87
106
  for (let turn = 0; turn < this.config.maxTurns; turn++) {
88
- const requestMessages = this._buildRequest(currentMessages)
89
- const response = await this._callLLM(requestMessages, currentMessages)
107
+ const requestMessages = this._buildRequest(this.state.messages)
108
+ const response = await this._callLLM(requestMessages, this.state.messages)
90
109
 
91
110
  if (this.abortController.signal.aborted) {
92
111
  throw new Error('操作已取消')
@@ -95,40 +114,22 @@ export class QueryEngine {
95
114
  // 没有工具调用 → 最终回复
96
115
  if (!response.toolCalls || response.toolCalls.length === 0) {
97
116
  finalResponse = response.content
98
- const assistantMsg = new AssistantMessage(response.content)
99
- this.state.messages.push(assistantMsg)
117
+ this.state.messages.push(new AssistantMessage(response.content))
100
118
  break
101
119
  }
102
120
 
103
- // 有工具调用 → 记录助手消息
104
- const assistantMsg = new AssistantMessage(response.content, response.toolCalls)
105
- this.state.messages.push(assistantMsg)
121
+ // 有工具调用 → 记录 assistant 消息(含 tool_calls)
122
+ this.state.messages.push(new AssistantMessage(response.content, response.toolCalls))
106
123
 
107
124
  // 执行工具
108
125
  const toolResults = await this._executeToolCalls(response.toolCalls)
109
126
 
110
- // 工具结果加入消息流(OpenAI 兼容格式)
127
+ // 工具结果加入 state.messages(OpenAI 兼容格式)
111
128
  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({
129
+ this.state.messages.push({
127
130
  role: 'tool',
128
131
  tool_call_id: result.toolCallId,
129
- content: result.isError
130
- ? `[ERROR] ${result.content}`
131
- : result.content,
132
+ content: result.isError ? `[ERROR] ${result.content}` : result.content,
132
133
  })
133
134
  this.state.toolResults.set(result.toolCallId, result)
134
135
  }
@@ -150,7 +151,7 @@ export class QueryEngine {
150
151
  }
151
152
 
152
153
  /**
153
- * 构建 LLM 请求消息列表
154
+ * 构建 LLM 请求消息列表 — 统一 OpenAI 兼容格式
154
155
  */
155
156
  _buildRequest(messages) {
156
157
  const request = []
@@ -160,76 +161,101 @@ export class QueryEngine {
160
161
  request.push({ role: 'system', content: this.config.systemPrompt })
161
162
  }
162
163
 
163
- // 历史消息
164
+ // 历史消息 — 转换为 OpenAI 兼容格式
164
165
  for (const msg of messages) {
165
166
  if (msg.role === 'system') {
166
167
  request.push({ role: 'system', content: msg.content })
167
168
  } else if (msg.role === 'user') {
168
169
  request.push({ role: 'user', content: this._formatContent(msg.content) })
169
170
  } 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
- }
171
+ // tool_calls 的 assistant 消息:OpenAI 格式
172
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
173
+ request.push({
174
+ role: 'assistant',
175
+ content: msg.content || null,
176
+ tool_calls: msg.toolCalls.map(tc => ({
177
+ id: tc.id,
178
+ type: 'function',
179
+ function: { name: tc.name, arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input) },
180
+ })),
181
+ })
182
+ } else {
183
+ // 纯文本 assistant 消息
184
+ request.push({ role: 'assistant', content: this._formatContent(msg.content) })
176
185
  }
177
- request.push({ role: 'assistant', content })
186
+ } else if (msg.role === 'tool') {
187
+ // tool 结果消息 — 直接透传
188
+ request.push({
189
+ role: 'tool',
190
+ tool_call_id: msg.tool_call_id,
191
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
192
+ })
178
193
  }
179
194
  }
180
195
 
181
196
  return request
182
197
  }
183
198
 
199
+
184
200
  /**
185
- * 执行工具调用
201
+ * 执行工具调用 — 两阶段策略
202
+ * 阶段1(串行):安全检查 + ask 模式确认(需要用户交互,必须串行)
203
+ * 阶段2(并行):批准后的工具并行执行,互不依赖的工具同时跑
186
204
  */
187
205
  async _executeToolCalls(toolCalls) {
188
- const results = []
206
+ // 阶段1:串行安全检查
207
+ const approved = []
189
208
  for (const tc of toolCalls) {
190
- // 安全检查(一票否决)
191
209
  const permResult = await this.permissionChecker.check(tc.name, tc.input)
192
210
  if (!permResult.allowed) {
193
- results.push(new ToolResult(tc.id, `工具调用被安全策略拒绝: ${tc.name} — ${permResult.reason || ''}`, true))
194
- continue
211
+ if (permResult.requiresConfirmation && this.config.onConfirmTool) {
212
+ const confirmed = await this.config.onConfirmTool(tc.name, tc.input)
213
+ if (!confirmed) {
214
+ approved.push({ tc, error: '用户未确认' })
215
+ continue
216
+ }
217
+ } else {
218
+ approved.push({ tc, error: `安全策略拒绝: ${permResult.reason || ""}` })
219
+ continue
220
+ }
195
221
  }
196
222
 
197
- // 查找工具
198
223
  const tool = this.config.tools.find(t => t.name === tc.name)
199
224
  if (!tool) {
200
- results.push(new ToolResult(tc.id, `未找到工具: ${tc.name}`, true))
225
+ approved.push({ tc, error: `未找到工具: ${tc.name}` })
201
226
  continue
202
227
  }
203
228
 
229
+ approved.push({ tc, tool })
230
+ }
231
+
232
+ // 阶段2:并行执行已批准的工具
233
+ const execPromises = approved.map(async (item) => {
234
+ if (item.error) {
235
+ const r = new ToolResult(item.tc.id, item.error, true)
236
+ r.toolName = item.tc.name
237
+ return r
238
+ }
239
+ const { tc, tool } = item
240
+ tc.status = 'running'
204
241
  try {
205
- tc.status = 'running'
206
- const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this })
242
+ const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
207
243
  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
244
+ const r = new ToolResult(tc.id, typeof content === 'string' ? content : JSON.stringify(content), false)
245
+ r.toolName = tc.name
246
+ return r
211
247
  } catch (err) {
212
248
  tc.status = 'error'
213
- results.push(new ToolResult(tc.id, `工具执行错误: ${err.message}`, true))
214
- results[results.length - 1].toolName = tc.name
249
+ const r = new ToolResult(tc.id, `工具执行错误: ${err.message}`, true)
250
+ r.toolName = tc.name
251
+ return r
215
252
  }
216
- }
253
+ })
254
+
255
+ const results = await Promise.all(execPromises)
217
256
  return results
218
257
  }
219
258
 
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
259
  async _callLLM(messages, contextMessages) {
234
260
  const apiKey = this.config.apiKey
235
261
  const apiBase = this.config.apiBase
@@ -237,96 +263,176 @@ export class QueryEngine {
237
263
  if (!apiKey) {
238
264
  throw new Error(
239
265
  `未设置 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` +
266
+ ` LLM_API_KEY=xxx (通用,推荐)\n` +
267
+ ` DEEPSEEK_API_KEY=xxx (DeepSeek)\n` +
268
+ ` OPENAI_API_KEY=xxx (OpenAI)\n` +
269
+ ` QWEN_API_KEY=xxx (通义千问)\n` +
270
+ ` GLM_API_KEY=xxx (智谱 GLM)\n` +
271
+ ` KIMI_API_KEY=xxx (Moonshot Kimi)\n` +
246
272
  `或通过 --api-key 参数传入`
247
273
  )
248
274
  }
249
275
 
250
276
  if (!apiBase) {
251
277
  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)`
278
+ `未设置 API Base URL。默认使用 https://api.deepseek.com/v1 ` +
279
+ `可通过 LLM_API_BASE 或 --api-base 参数切换其他提供商`
261
280
  )
262
281
  }
263
282
 
264
- // 构建工具定义 — OpenAI function-calling 格式
283
+ // 构建工具定义
265
284
  const tools = this.config.tools.map(t => ({
266
285
  type: 'function',
267
- function: {
268
- name: t.name,
269
- description: t.description,
270
- parameters: t.parameters,
271
- },
286
+ function: { name: t.name, description: t.description, parameters: t.parameters },
272
287
  }))
273
288
 
289
+ const useStream = !this.config.noStream
274
290
  const body = {
275
291
  model: this.config.model,
276
292
  messages,
277
293
  max_tokens: 4096,
278
294
  ...(tools.length && { tools }),
295
+ ...(useStream && { stream: true }),
279
296
  }
280
297
 
281
298
  const url = apiBase.replace(/\/+$/, '') + '/chat/completions'
282
299
 
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
- })
300
+ // H3 修复:SSRF 防护 — 在 fetch 之前检查 API 主机名
301
+ try {
302
+ const parsedUrl = new URL(url)
303
+ const hostResult = await checkHostSafety(parsedUrl.hostname)
304
+ if (!hostResult.allowed) {
305
+ throw new Error(`SSRF blocked: ${hostResult.reason}`)
306
+ }
307
+ } catch (err) {
308
+ if (err.message.startsWith('SSRF blocked:')) {
309
+ throw err
310
+ }
311
+ // URL 解析错误忽略,让 fetch 自己处理
312
+ }
292
313
 
293
- if (!response.ok) {
294
- const errText = await response.text()
295
- throw new Error(`API 错误 ${response.status}: ${errText}`)
314
+ // 带重试的 fetch
315
+ const maxRetries = 3
316
+ // Jitter 退避 指数退避 + 随机 ±50%,防止惊群效应
317
+ const retryDelay = (baseMs, attempt) => {
318
+ const ms = baseMs * Math.pow(2, attempt - 1)
319
+ const jitter = ms * (0.5 + Math.random() * 0.5) // 50%-100% of base
320
+ return Math.round(jitter)
296
321
  }
322
+ let lastError = null
323
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
324
+ try {
325
+ if (this.abortController?.signal?.aborted) {
326
+ throw new Error('请求已取消')
327
+ }
328
+
329
+ const response = await fetch(url, {
330
+ method: 'POST',
331
+ headers: {
332
+ 'Content-Type': 'application/json',
333
+ 'Authorization': `Bearer ${apiKey}`,
334
+ },
335
+ body: JSON.stringify(body),
336
+ signal: this.abortController?.signal,
337
+ })
338
+
339
+ if (!response.ok) {
340
+ const errText = await response.text()
341
+ // 429/503 可重试
342
+ if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
343
+ const waitMs = retryDelay(response.status === 429 ? 2000 : 1000, attempt)
344
+ if (this.config.verbose) {
345
+ console.error(`[retry] API ${response.status}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
346
+ }
347
+ await new Promise(r => setTimeout(r, waitMs))
348
+ continue
349
+ }
350
+ throw new Error(`API 错误 ${response.status}: ${errText}`)
351
+ }
297
352
 
298
- const data = await response.json()
299
- return this._parseResponse(data)
353
+ // 流式或非流式处理
354
+ if (useStream && response.body) {
355
+ const result = await this._handleStreamResponse(response)
356
+ if (result.usage && this.costTracker) {
357
+ this.costTracker.recordUsage(result.usage)
358
+ }
359
+ if (this.tokenBudget && result.usage) {
360
+ this.tokenBudget.recordUsage(result.usage)
361
+ }
362
+ return result
363
+ } else {
364
+ const data = await response.json()
365
+ const result = parseNonStreamResponse(data)
366
+ if (result.usage && this.costTracker) {
367
+ this.costTracker.recordUsage(result.usage)
368
+ }
369
+ if (this.tokenBudget && result.usage) {
370
+ this.tokenBudget.recordUsage(result.usage)
371
+ }
372
+ return result
373
+ }
374
+ } catch (err) {
375
+ lastError = err
376
+ // 网络错误重试
377
+ if (err.name !== 'AbortError' && attempt < maxRetries && !err.message.startsWith('API 错误')) {
378
+ const waitMs = retryDelay(1000, attempt)
379
+ if (this.config.verbose) {
380
+ console.error(`[retry] Network error: ${err.message}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
381
+ }
382
+ await new Promise(r => setTimeout(r, waitMs))
383
+ continue
384
+ }
385
+ throw err
386
+ }
387
+ }
388
+ throw lastError
300
389
  }
301
390
 
302
391
  /**
303
- * 解析 OpenAI 兼容响应
392
+ * 处理流式响应 逐 token 输出
304
393
  */
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
- }
394
+ async _handleStreamResponse(response) {
395
+ const result = { content: '', toolCalls: [], usage: {} }
396
+ let currentText = ''
314
397
 
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 }
398
+ try {
399
+ for await (const event of parseStream(response)) {
400
+ if (event.type === 'text') {
401
+ // 实时输出到终端
402
+ process.stdout.write(event.text)
403
+ currentText += event.text
404
+ } else if (event.type === 'tool_use') {
405
+ // 收集工具调用
406
+ result.toolCalls.push(new ToolCall(
407
+ event.toolCall.id,
408
+ event.toolCall.name,
409
+ event.toolCall.input
410
+ ))
411
+ } else if (event.type === 'done') {
412
+ result.content = event.result.content || currentText
413
+ result.toolCalls = event.result.toolCalls?.map(tc =>
414
+ new ToolCall(tc.id, tc.name, tc.input)
415
+ ) || result.toolCalls
416
+ result.usage = event.result.usage || {}
417
+ }
418
+ }
419
+ } catch (err) {
420
+ // 流中断 — 返回已收到的内容
421
+ result.content = currentText || ''
422
+ if (this.config.verbose) {
423
+ console.error(`[stream] interrupted: ${err.message}`)
321
424
  }
322
- result.toolCalls.push(new ToolCall(tc.id, tc.function.name, input))
323
425
  }
324
426
 
427
+ // 流式输出后换行
428
+ if (currentText) process.stdout.write('\n')
429
+
325
430
  return result
326
431
  }
327
432
 
328
433
  /** 格式化内容 */
329
434
  _formatContent(content) {
435
+ if (content == null) return ''
330
436
  if (typeof content === 'string') return content
331
437
  if (typeof content === 'object') return JSON.stringify(content)
332
438
  return String(content)
@@ -2,10 +2,9 @@
2
2
  * 会话管理
3
3
  * 对应原版: src/utils/sessionState.ts + src/utils/sessionStorage.ts
4
4
  */
5
- import { readFile, writeFile, mkdir, readdir, rm } from 'fs/promises'
5
+ import { readFile, writeFile, mkdir, readdir, rm, chmod } from 'fs/promises'
6
6
  import { resolve, join } from 'path'
7
- import { existsSync } from 'fs'
8
- import { SessionState } from '../types/index.js'
7
+ import { randomBytes } from 'crypto'
9
8
 
10
9
  const DEFAULT_SESSIONS_DIR = '.claude-code/sessions'
11
10
 
@@ -15,15 +14,16 @@ export class SessionManager {
15
14
  this.currentSession = null
16
15
  }
17
16
 
18
- /** 确保会话目录存在 */
17
+ /** 确保会话目录存在(v1.1: 目录权限 0700) */
19
18
  async ensureDir() {
20
- await mkdir(this.sessionsDir, { recursive: true })
19
+ await mkdir(this.sessionsDir, { recursive: true, mode: 0o700 })
21
20
  }
22
21
 
23
22
  /** 创建新会话 */
24
23
  async create(title = '') {
25
24
  await this.ensureDir()
26
- const id = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
25
+ // M4 fix: 使用 crypto.randomBytes 生成不可预测的会话 ID
26
+ const id = `session-${Date.now()}-${randomBytes(8).toString('hex')}`
27
27
  const session = {
28
28
  id,
29
29
  title: title || `Session ${new Date().toISOString().slice(0, 19)}`,
@@ -37,12 +37,12 @@ export class SessionManager {
37
37
  return session
38
38
  }
39
39
 
40
- /** 保存会话 */
40
+ /** 保存会话(v1.1: 文件权限 0600,防止其他用户读取敏感对话内容) */
41
41
  async save(session) {
42
42
  await this.ensureDir()
43
43
  session.updated = new Date().toISOString()
44
44
  const filePath = join(this.sessionsDir, `${session.id}.json`)
45
- await writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8')
45
+ await writeFile(filePath, JSON.stringify(session, null, 2), { encoding: 'utf-8', mode: 0o600 })
46
46
  return session
47
47
  }
48
48
 
@@ -109,11 +109,28 @@ export class SessionManager {
109
109
  /** 追加消息到当前会话 */
110
110
  async appendMessage(message) {
111
111
  if (!this.currentSession) await this.getOrCreate()
112
- this.currentSession.messages.push({
112
+
113
+ const entry = {
113
114
  role: message.role,
114
115
  content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
115
116
  timestamp: new Date().toISOString(),
116
- })
117
+ }
118
+
119
+ // 保存 tool_calls 信息(assistant 消息可能包含工具调用)
120
+ if (message.toolCalls && message.toolCalls.length > 0) {
121
+ entry.toolCalls = message.toolCalls.map(tc => ({
122
+ id: tc.id,
123
+ name: tc.name,
124
+ input: tc.input,
125
+ }))
126
+ }
127
+
128
+ // 保存 tool_call_id(tool 结果消息)
129
+ if (message.tool_call_id) {
130
+ entry.tool_call_id = message.tool_call_id
131
+ }
132
+
133
+ this.currentSession.messages.push(entry)
117
134
  await this.save(this.currentSession)
118
135
  return this.currentSession
119
136
  }