@raolin2025/claude-code-node 2.8.23 → 2.8.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.8.23",
3
+ "version": "2.8.25",
4
4
  "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
5
5
  "type": "module",
6
6
  "main": "src/core/index.js",
package/src/core/cli.js CHANGED
@@ -254,6 +254,9 @@ Commands:
254
254
  /help — Show this help
255
255
  /model NAME — Switch model
256
256
  /models — List available models from current API
257
+ /api-base URL — Switch API base URL at runtime (local session only)
258
+ /api-key KEY — Switch API key at runtime (local session only)
259
+ /api — Show current API base/key/model
257
260
  /tools — List available tools
258
261
  /session — Show session info
259
262
  /sessions — List all sessions
@@ -284,6 +287,11 @@ const DETAILED_HELP = {
284
287
 
285
288
  models: "/models\n Fetch and display all available models from the current API provider.\n Shows a numbered list, then prompts you to select by number or name.\n Requires a configured API key (the one you used to start cc-node).\n Uses the endpoint: <apiBase>/models",
286
289
 
290
+ apiBase:"/api-base <url>\n Switch the LLM API base URL at runtime (session-only, not persisted).\n Takes effect immediately for the next message.\n After switching, cc-node re-probes the context window.\n Then use /model <name> or /models to pick the new provider's model.\n For local llama.cpp/Ollama (no auth) you can skip /api-key.\n\n Example: /api-base http://127.0.0.1:11434/v1",
291
+ apiKey: "/api-key <key>\n Switch the LLM API key at runtime (session-only, not persisted).\n Takes effect immediately for the next message.\n For local llama.cpp/Ollama (no auth) you can leave it unset.\n\n Example: /api-key sk-xxxxxxxx",
292
+
293
+ api: "/api\n Show the current API configuration: base URL, key (masked), and model.",
294
+
287
295
  tools: "/tools\n List all available tools that cc-node can use.\n Shows tool names with their short descriptions.\n\n Tools include: Bash, Read, Edit, Write, Glob, Grep,\n WebFetch, WebSearch, AskUserQuestion, GitTool",
288
296
 
289
297
  session: "/session\n Show current session information:\n - Session ID\n - Session title\n - Number of messages\n - Number of tool call turns",
@@ -374,9 +382,9 @@ Options:
374
382
  -v, --verbose Verbose mode
375
383
  --no-stream Disable streaming
376
384
  --max-messages N Fold history when message count exceeds N (default: 0 = off)
377
- --small-model Enable small-model adaptation (tool-call enforcement, filler retry, intent guidance)
385
+ --small-model Reserved flag (no-op now tool engine uses plain mode like v2.8.11)
378
386
  --max-output-tokens N Override max single-response output tokens (default: computed from window size)
379
- --small-model-max-turns N Small-model tool-loop cap (default: 8, prevents infinite loops)
387
+ --small-model-max-turns N Reserved (no-op tool loop uses maxTurns)
380
388
  --with-notify Start built-in channel listener (Telegram)
381
389
  (replaces cc-notify daemon — no external script needed)
382
390
  -h, --help Show this help
@@ -425,7 +433,7 @@ export async function main() {
425
433
  const config = new Config()
426
434
  await config.load(process.cwd())
427
435
 
428
- const apiBase = cliArgs.apiBase || config.get('apiBase') || process.env.LLM_API_BASE || ''
436
+ let apiBase = cliArgs.apiBase || config.get('apiBase') || process.env.LLM_API_BASE || ''
429
437
  // apiBase 指向自建本地服务(Ollama / llama.cpp / vLLM 等)时允许缺省 apiKey
430
438
  const localApiBase = isLocalLlmServer(apiBase)
431
439
  let model = cliArgs.model || config.get('model') || ''
@@ -488,7 +496,7 @@ export async function main() {
488
496
  const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
489
497
  const permissionMode = cliArgs.permissionMode || config.get('permissionMode')
490
498
  const maxTurns = cliArgs.maxTurns || config.get('maxTurns')
491
- const apiKey = cliArgs.apiKey || config.get('apiKey') || ''
499
+ let apiKey = cliArgs.apiKey || config.get('apiKey') || ''
492
500
  const verbose = cliArgs.verbose || config.get('verbose')
493
501
 
494
502
  const registry = createDefaultRegistry()
@@ -749,6 +757,47 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
749
757
  }
750
758
  else console.log(`Model: ${engine.config.model}`)
751
759
  break
760
+ case 'api-base':
761
+ // /api-base <url> — 运行时切换 LLM 来源地址(仅本次会话生效)
762
+ if (rest[0]) {
763
+ const newBase = rest.join(' ').trim().replace(/\/+$/, '')
764
+ apiBase = newBase
765
+ engine.config.apiBase = newBase
766
+ console.log(`API Base → ${newBase}`)
767
+ // 切换来源后重新探测上下文窗口(旧来源窗口可能不同)
768
+ const manual = config.get('maxBudgetTokens') || 0
769
+ try {
770
+ const { window: win, source } = await detectContextWindow({ model, apiBase: newBase, apiKey, manualWindow: manual })
771
+ tokenBudget.setWindow(win, source)
772
+ windowSource = source
773
+ console.log(` ↳ Context window → ${formatTokens(win)} (${windowSourceLabel(source)})`)
774
+ } catch (e) {
775
+ console.log(` ⚠️ 窗口探测失败: ${e.message}`)
776
+ }
777
+ // 提示切模型:不同来源的模型名不同
778
+ const localNow = isLocalLlmServer(newBase)
779
+ if (!localNow && !apiKey) {
780
+ console.log(' ⚠️ 新来源需 API Key → 请用 /api-key <key> 设置')
781
+ }
782
+ console.log(' ℹ️ 不同模型的名称不同,请用 /model <名称> 选择新来源的模型,或用 /models 查看')
783
+ }
784
+ else console.log(`API Base: ${engine.config.apiBase}`)
785
+ break
786
+ case 'api-key':
787
+ // /api-key <key> — 运行时切换 LLM API Key(仅本次会话生效)
788
+ if (rest[0]) {
789
+ const newKey = rest.join(' ').trim()
790
+ apiKey = newKey
791
+ engine.config.apiKey = newKey
792
+ console.log(`API Key → ${newKey.slice(0, 4)}...${newKey.slice(-4)}`)
793
+ }
794
+ else console.log(`API Key: ${engine.config.apiKey ? `已设置 (${engine.config.apiKey.slice(0, 4)}...${engine.config.apiKey.slice(-4)})` : '未设置'}`)
795
+ break
796
+ case 'api':
797
+ console.log(`API Base: ${engine.config.apiBase}`)
798
+ console.log(`API Key: ${engine.config.apiKey ? `已设置 (${engine.config.apiKey.slice(0, 4)}...${engine.config.apiKey.slice(-4)})` : '未设置'}`)
799
+ console.log(`Model: ${engine.config.model}`)
800
+ break
752
801
  case 'window': {
753
802
  const arg = rest[0] ? rest.join(' ').trim() : ''
754
803
  if (!arg) {
@@ -15,15 +15,6 @@ import crypto from 'crypto'
15
15
  import { UserMessage, AssistantMessage, ToolCall, ToolResult, SessionState } from '../types/index.js'
16
16
  import { parseStream, parseNonStreamResponse } from './streaming.js'
17
17
  import { compactMessages, trimToWindow, foldHistoryByCount, trimToolResults } from './compact.js'
18
- import {
19
- isSmallModelEnabled,
20
- buildSmallModelSystemPrompt,
21
- isFillerResponse,
22
- buildCombinedGuidance,
23
- selectRelevantTools,
24
- RETRY_GUIDANCE,
25
- extractFrameworkAction,
26
- } from './small-model.js'
27
18
  import { CostTracker } from './cost-tracker.js'
28
19
  import { EnhancedPermissionChecker } from '../security/enhanced-permission.js'
29
20
  import { isLocalLlmServer, buildAuthHeaders } from '../utils/index.js'
@@ -97,8 +88,6 @@ export class QueryEngine {
97
88
  this.tokenBudget = this.config.tokenBudget || null
98
89
  // 最近一次 processMessage 是否已通过流式回调/直写把正文输出(供调用方避免重复打印 response)
99
90
  this.lastStreamed = false
100
- // 当前正在处理的用户输入(小模型模式用于按意图精简工具集)
101
- this.currentUserInput = ''
102
91
  }
103
92
 
104
93
  /**
@@ -114,7 +103,6 @@ export class QueryEngine {
114
103
  this.state.turnCount++
115
104
  this.lastStreamed = false // 本轮是否已流式输出正文
116
105
  this.abortController = new AbortController()
117
- this.currentUserInput = userInput // 记录当前用户指令(小模型模式按意图精简工具)
118
106
  const userMsg = new UserMessage(userInput, images)
119
107
  this.state.messages.push(userMsg)
120
108
 
@@ -226,65 +214,17 @@ export class QueryEngine {
226
214
  */
227
215
  async _runToolLoop(userMessage) {
228
216
  let finalResponse = ''
229
- const smallModel = isSmallModelEnabled(this.config)
230
- // 小模型模式:首轮注入意图引导(层 B),帮助模型锁定该用哪些工具
231
- let intentGuided = false
232
- // 敷衍重试次数(层 A):模型没调工具只回空话时,追加强引导重试
233
- let fillerRetries = 0
234
- const MAX_FILLER_RETRIES = 1
235
- // 工具循环轮数上限:
236
- // - 小模型模式:用更小的上限(默认 8),避免模型陷入"写一个又写一个"的无限循环
237
- // - 普通模式:用 config.maxTurns
238
- const toolLoopLimit = smallModel
239
- ? (this.config.smallModelMaxTurns || 8)
240
- : this.config.maxTurns
241
-
242
- for (let turn = 0; turn < toolLoopLimit; turn++) {
217
+
218
+ // 朴素工具循环(v2.8.11 风格):
219
+ // 实测结论——小模型在"朴素模式"(16 个工具全上、无引导注入、无代执行、
220
+ // 无工具精简)下才能正常工作。v2.8.13 后我加的意图引导注入、工具精简、
221
+ // 多步计划、框架代执行等"辅助机制"反而让模型乱套(不调用工具、瞎编内容)。
222
+ // 因此这里【不做任何干预】,回归最原始行为:构建请求 调 LLM →
223
+ // 有工具调用就执行 → 循环。
224
+ for (let turn = 0; turn < this.config.maxTurns; turn++) {
243
225
  // 发送前硬校验:工具结果可能已使上下文超窗,确保 ≤ 窗口(摘要优先 + 滑动窗口裁剪兜底)
244
226
  this._ensureFitWindow()
245
227
 
246
- // 小模型模式:首轮若识别到意图/多步计划,把引导【追加到首条 system 消息】。
247
- // 注意:不能 push 一条新的 system 消息到中间——llama.cpp/Jinja 严格要求
248
- // system 消息必须在对话开头,中间插 system 会报 "System message must be at the beginning" (500)。
249
- if (smallModel && !intentGuided && this.state.messages.length > 0) {
250
- const guidance = buildCombinedGuidance(userMessage.content, {
251
- enable: true,
252
- cwd: this.config.cwd,
253
- })
254
- if (guidance) {
255
- // 找到第一条 system 消息,把引导拼接进去(保持 system 全在开头)
256
- const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
257
- if (sysIdx !== -1) {
258
- const sysMsg = this.state.messages[sysIdx]
259
- this.state.messages[sysIdx] = {
260
- ...sysMsg,
261
- content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + guidance,
262
- }
263
- } else {
264
- // 没有 system 消息 → 作为 user 引导插入(跟随在最新 user 之后作为新 user)
265
- this.state.messages.push({ role: 'user', content: guidance })
266
- }
267
- if (this.config.verbose) console.error('[small-model] 已注入引导(并入首条 system):' + guidance.slice(0, 60) + '...')
268
- }
269
- intentGuided = true
270
- }
271
-
272
- // 小模型模式:接近工具循环上限时,注入"总结收尾"引导,防止模型无限循环
273
- if (smallModel && turn === toolLoopLimit - 1) {
274
- const stopGuidance = `[系统提示] 你已经完成了大部分工作。现在请【停止调用新工具】,把已经完成的内容整理成一段总结回复给用户。如果确实还有关键步骤未完成,只再调用一次工具完成它,然后立即总结。不要再继续无休止地调用工具。`
275
- const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
276
- if (sysIdx !== -1) {
277
- const sysMsg = this.state.messages[sysIdx]
278
- this.state.messages[sysIdx] = {
279
- ...sysMsg,
280
- content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + stopGuidance,
281
- }
282
- } else {
283
- this.state.messages.push({ role: 'user', content: stopGuidance })
284
- }
285
- if (this.config.verbose) console.error(`[small-model] 已到工具循环第 ${turn + 1} 轮(上限 ${toolLoopLimit}),注入收尾引导`)
286
- }
287
-
288
228
  const requestMessages = this._buildRequest(this.state.messages)
289
229
  const response = await this._callLLM(requestMessages, this.state.messages)
290
230
 
@@ -292,32 +232,8 @@ export class QueryEngine {
292
232
  throw new Error('操作已取消')
293
233
  }
294
234
 
295
- // 没有工具调用 → 潜在最终回复
235
+ // 没有工具调用 → 最终回复
296
236
  if (!response.toolCalls || response.toolCalls.length === 0) {
297
- // 小模型模式:检测敷衍输出(空话/太短/命中占位句)
298
- if (smallModel && isFillerResponse(response)) {
299
- // 第一次敷衍 → 追加强引导重试
300
- if (fillerRetries < MAX_FILLER_RETRIES) {
301
- fillerRetries++
302
- if (this.config.verbose) {
303
- console.error(`[small-model] 检测到敷衍输出(第 ${fillerRetries} 次),追加强引导重试`)
304
- }
305
- this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
306
- this.state.messages.push({ role: 'user', content: RETRY_GUIDANCE })
307
- continue
308
- }
309
- // 重试后仍敷衍 → 框架代执行,且【直接把结果作为最终答案】返回,
310
- // 不再 continue 让模型总结——模型已证明它只会敷衍,继续循环只会无限重复。
311
- // 这样既完成任务,又不会陷入"代执行→敷衍→再代执行"的死循环。
312
- const execResult = await this._frameworkExecute(userMessage.content)
313
- if (execResult.done) {
314
- if (this.config.verbose) console.error(`[small-model] 框架代执行完成,直接返回结果`)
315
- // 框架已执行并把结果注入对话;直接把真实结果作为最终回复
316
- finalResponse = execResult.resultText
317
- break
318
- }
319
- }
320
-
321
237
  finalResponse = response.content
322
238
  this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
323
239
  break
@@ -344,9 +260,8 @@ export class QueryEngine {
344
260
  }
345
261
  }
346
262
 
347
- // 工具循环达到上限但仍未收尾(模型一直调用工具)→ 强制收尾
348
- if (!finalResponse) {
349
- finalResponse = `[已达到工具循环上限 (${toolLoopLimit}),已停止进一步调用工具。请查看上方工具执行结果,确认任务完成情况。]`
263
+ if (!finalResponse && this.state.turnCount >= this.config.maxTurns) {
264
+ finalResponse = `[达到最大回合数限制 (${this.config.maxTurns}),停止响应]`
350
265
  }
351
266
 
352
267
  return {
@@ -388,12 +303,10 @@ export class QueryEngine {
388
303
  _buildRequest(messages) {
389
304
  const request = []
390
305
 
391
- // 系统提示(小模型模式:追加强制工具调用引导 + 当前工作目录)
306
+ // 系统提示(朴素模式:直接用基础 system prompt,不做 cwd/工具铁律等注入——
307
+ // 实测 v2.8.11 朴素模式能正常工作,注入反而干扰模型)
392
308
  if (this.config.systemPrompt) {
393
- const sysContent = isSmallModelEnabled(this.config)
394
- ? buildSmallModelSystemPrompt(this.config.systemPrompt, { cwd: this.config.cwd })
395
- : this.config.systemPrompt
396
- request.push({ role: 'system', content: sysContent })
309
+ request.push({ role: 'system', content: this.config.systemPrompt })
397
310
  }
398
311
 
399
312
  // 历史消息 — 转换为 OpenAI 兼容格式
@@ -512,47 +425,6 @@ export class QueryEngine {
512
425
  return results
513
426
  }
514
427
 
515
- /**
516
- * 框架代执行 — 模型敷衍调不起工具时,框架直接替它执行最合理的工具
517
- *
518
- * 背景:27B Q3 量化模型工具调用极弱,即使强引导也常只回"研究一下"而不调工具。
519
- * 此时框架【绕过模型】,根据指令推断该执行什么工具并直接运行,把真实结果
520
- * 注入对话,再让模型基于结果总结——"一句话调用小模型工作"才能成立。
521
- *
522
- * @param {string} userInput — 用户指令
523
- * @returns {Promise<{ done: boolean, summary: string }>}
524
- * done=true 表示框架已执行工具(结果已注入 state.messages)
525
- */
526
- async _frameworkExecute(userInput) {
527
- const action = extractFrameworkAction(userInput, { cwd: this.config.cwd })
528
- if (!action) return { done: false, summary: '', resultText: '' }
529
-
530
- // 找到对应的工具
531
- const tool = this.config.tools.find(t => t.name === action.tool)
532
- if (!tool) return { done: false, summary: '', resultText: '' }
533
-
534
- // 执行工具(直接调用 handler)
535
- try {
536
- const content = await tool.handler(action.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
537
-
538
- const resultText = typeof content === 'string' ? content : JSON.stringify(content)
539
-
540
- // 注入框架代执行结果(作为 user 消息,避免 tool 消息无对应 assistant tool_call 报错)
541
- // 说明:框架代执行不是模型发起的工具调用,不能作为 tool 角色(会缺 assistant tool_call)。
542
- this.state.messages.push({
543
- role: 'user',
544
- content: `[框架代执行结果] 框架已替你执行工具 ${tool.name}(参数 ${JSON.stringify(action.input)}),结果如下:\n\n${resultText.slice(0, 8000)}`,
545
- })
546
-
547
- const summary = `框架已代执行 ${tool.name}(${JSON.stringify(action.input)})`
548
- if (this.config.verbose) console.error(`[small-model] 框架代执行 ${tool.name} 完成`)
549
- return { done: true, summary, resultText }
550
- } catch (err) {
551
- if (this.config.verbose) console.error(`[small-model] 框架代执行失败: ${err.message}`)
552
- return { done: false, summary: '', resultText: '' }
553
- }
554
- }
555
-
556
428
  async _callLLM(messages, contextMessages) {
557
429
  const apiKey = this.config.apiKey
558
430
  const apiBase = this.config.apiBase
@@ -580,16 +452,9 @@ export class QueryEngine {
580
452
  }
581
453
 
582
454
  // 构建工具定义
583
- // 小模型模式:按用户指令意图精简工具集,减少模型的选择负担(层 A)
584
- let effectiveTools = this.config.tools
585
- if (isSmallModelEnabled(this.config)) {
586
- const reduced = selectRelevantTools(this.config.tools, this.currentUserInput, { enable: true })
587
- effectiveTools = reduced
588
- if (this.config.verbose && reduced.length !== this.config.tools.length) {
589
- console.error(`[small-model] 按意图精简工具:${this.config.tools.length} → ${reduced.length}(${reduced.map(t => t.name).join(', ')})`)
590
- }
591
- }
592
- const tools = effectiveTools.map(t => ({
455
+ // 朴素模式(v2.8.11 风格):16 个工具全上,不做精简。
456
+ // 实测结论——小模型在工具全给时才能正常调用;按意图精简反而让它不会调用、瞎编。
457
+ const tools = this.config.tools.map(t => ({
593
458
  type: 'function',
594
459
  function: { name: t.name, description: t.description, parameters: t.parameters },
595
460
  }))