@raolin2025/claude-code-node 2.8.22 → 2.8.24

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.22",
3
+ "version": "2.8.24",
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",
@@ -127,10 +127,19 @@ test('工具精简:未启用返回全部', () => {
127
127
  assert.equal(selectRelevantTools(fakeTools, 'xx', { enable: false }), fakeTools)
128
128
  })
129
129
 
130
- test('工具精简:无明确意图时返回核心文件工具', () => {
130
+ test('工具精简:无明确意图时返回全部工具(让模型自己挑)', () => {
131
131
  const reduced = selectRelevantTools(fakeTools, '你好', { enable: true })
132
- assert.ok(reduced.length <= 6, '应只保留核心工具')
133
- assert.ok(reduced.some(t => t.name === 'Bash'))
132
+ // 实测经验:小模型不能过度精简工具,无明确适配时干脆全部给
133
+ assert.equal(reduced.length, fakeTools.length, '无明确意图应返回全部工具')
134
+ })
135
+
136
+ test('工具精简:有明确意图时至少保留 6 个核心工具', () => {
137
+ const reduced = selectRelevantTools(fakeTools, '阅读D:\\x\\COMPLETION_REPORT.md', { enable: true })
138
+ // 至少 6 个核心工具,绝不精简到 2 个
139
+ assert.ok(reduced.length >= 6, `应至少保留 6 个工具(实际 ${reduced.length})`)
140
+ assert.ok(reduced.some(t => t.name === 'Read'))
141
+ assert.ok(reduced.some(t => t.name === 'Bash'), '应保留 Bash')
142
+ assert.ok(reduced.some(t => t.name === 'Write'), '应保留 Write')
134
143
  })
135
144
 
136
145
  // ---- system prompt 强化 ----
package/src/core/cli.js CHANGED
@@ -374,9 +374,9 @@ Options:
374
374
  -v, --verbose Verbose mode
375
375
  --no-stream Disable streaming
376
376
  --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)
377
+ --small-model Reserved flag (no-op now tool engine uses plain mode like v2.8.11)
378
378
  --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)
379
+ --small-model-max-turns N Reserved (no-op tool loop uses maxTurns)
380
380
  --with-notify Start built-in channel listener (Telegram)
381
381
  (replaces cc-notify daemon — no external script needed)
382
382
  -h, --help Show this help
@@ -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
  }))
@@ -377,32 +377,53 @@ export function buildCombinedGuidance(userInput, opts = {}) {
377
377
  }
378
378
 
379
379
  /**
380
- * 精简工具列表(层 A)— 小模型一次看太多工具会混乱
380
+ * 工具选择(层 A)— 小模型适配的关键经验
381
381
  *
382
- * 从完整工具列表里,按用户指令筛选出最相关的核心工具子集。
383
- * 这样模型的工具选择负担小,更不容易乱调/漏调。
382
+ * 重要结论(来自实测):**小模型工具调用不能过度精简**。
383
+ * v2.8.12 之前(16 个工具全上)模型能正常调用工具;v2.8.13 之后我做了"按意图
384
+ * 精简成 2-6 个",结果模型反而不调用工具、只会瞎编。因此策略改为:
385
+ *
386
+ * 1. **保底至少 6 个核心工具**:Bash, Read, Edit, Write, Glob, Grep(每次必给);
387
+ * 2. **按任务适配追加**:若意图明确需要额外工具(联网→WebSearch/WebFetch 等),
388
+ * 在核心 6 个之上追加,绝不减少核心集;
389
+ * 3. **找不到合适的适配,就全部给**:宁可全给 16 个,也不要精简到很少。
390
+ *
391
+ * 原则:**工具给全,让模型自己挑;而不是替它精简,导致它无工具可用而瞎编。**
384
392
  *
385
393
  * @param {Array} allTools — 完整 ToolDef 列表
386
394
  * @param {string} userInput — 用户指令
387
395
  * @param {object} [opts]
388
- * @param {boolean} [opts.enable=true] — 是否启用精简
389
- * @returns {Array} 精简后的工具列表
396
+ * @param {boolean} [opts.enable=true] — 是否启用适配
397
+ * @returns {Array} 选择后的工具列表(至少 6 个核心;找不到适配则全部)
390
398
  */
391
399
  export function selectRelevantTools(allTools, userInput, opts = {}) {
392
400
  if (opts.enable === false) return allTools
393
401
  if (!allTools || allTools.length === 0) return allTools
394
402
 
403
+ // 核心工具保底集(每次必给):小模型不能过度精简工具,否则不会调用只会瞎编
404
+ const CORE = ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep']
395
405
  const intent = detectIntent(userInput)
396
- // 未识别到明确意图 → 返回核心工具(不塞满 16 个)
397
- const coreNames = intent
398
- ? intent.toolHint.split(',').map(s => s.trim())
399
- : ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep']
400
-
401
- // 核心工具优先,保留指定工具;若一个都没命中则退回首屏核心集
402
- const selected = allTools.filter(t => coreNames.includes(t.name))
403
- if (selected.length === 0) {
404
- return allTools.filter(t => ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep'].includes(t.name))
406
+
407
+ // 找不到合适的适配(无明确意图)→ 干脆全部调用,让模型自己挑。
408
+ // 这是实测经验:v2.8.12 之前全 16 个工具模型能正常调用;精简后反而变傻。
409
+ if (!intent) {
410
+ return allTools
405
411
  }
412
+
413
+ // 有明确意图 → 核心 6 个(保底)+ 意图命中的额外工具(追加,不减少核心)
414
+ const toolNames = new Set(CORE)
415
+ if (intent.toolHint) {
416
+ for (const name of intent.toolHint.split(',').map(s => s.trim())) {
417
+ toolNames.add(name)
418
+ }
419
+ }
420
+ const selected = allTools.filter(t => toolNames.has(t.name))
421
+
422
+ // 兜底:筛选不足 6 个(核心工具不全)→ 全给
423
+ if (selected.length < 6) {
424
+ return allTools
425
+ }
426
+
406
427
  return selected
407
428
  }
408
429