@raolin2025/claude-code-node 2.8.21 → 2.8.23
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 +1 -1
- package/src/__tests__/small-model.test.js +23 -3
- package/src/core/query-engine.js +15 -12
- package/src/core/small-model.js +52 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.23",
|
|
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
|
-
|
|
133
|
-
assert.
|
|
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 强化 ----
|
|
@@ -249,6 +258,17 @@ test('框架代执行:非读取任务或无路径返回 null', () => {
|
|
|
249
258
|
assert.equal(extractFrameworkAction('运行测试', { cwd: 'D:\\x' }), null)
|
|
250
259
|
})
|
|
251
260
|
|
|
261
|
+
test('框架代执行:路径后带中文说明时正确截断(不吞入多余内容)', () => {
|
|
262
|
+
// 复现 error.log 的 bug:路径后跟中文说明,之前会被贪婪匹配进路径
|
|
263
|
+
const a = extractFrameworkAction(
|
|
264
|
+
'阅读D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader\\COMPLETION_REPORT.md,对项目有个全盘了解',
|
|
265
|
+
{ cwd: 'D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader' }
|
|
266
|
+
)
|
|
267
|
+
assert.ok(a, '应识别到框架动作')
|
|
268
|
+
assert.equal(a.input.file_path, 'D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader\\COMPLETION_REPORT.md',
|
|
269
|
+
`路径不应吞入中文说明(实际 ${a.input.file_path})`)
|
|
270
|
+
})
|
|
271
|
+
|
|
252
272
|
// ---- _buildRequest 把 system 统一前置(防 llama.cpp 500)----
|
|
253
273
|
test('_buildRequest:把中间 system 统一前置到开头', async () => {
|
|
254
274
|
const { QueryEngine } = await import('../core/query-engine.js')
|
package/src/core/query-engine.js
CHANGED
|
@@ -306,12 +306,15 @@ export class QueryEngine {
|
|
|
306
306
|
this.state.messages.push({ role: 'user', content: RETRY_GUIDANCE })
|
|
307
307
|
continue
|
|
308
308
|
}
|
|
309
|
-
// 重试后仍敷衍 →
|
|
309
|
+
// 重试后仍敷衍 → 框架代执行,且【直接把结果作为最终答案】返回,
|
|
310
|
+
// 不再 continue 让模型总结——模型已证明它只会敷衍,继续循环只会无限重复。
|
|
311
|
+
// 这样既完成任务,又不会陷入"代执行→敷衍→再代执行"的死循环。
|
|
310
312
|
const execResult = await this._frameworkExecute(userMessage.content)
|
|
311
313
|
if (execResult.done) {
|
|
312
|
-
if (this.config.verbose) console.error(`[small-model]
|
|
313
|
-
//
|
|
314
|
-
|
|
314
|
+
if (this.config.verbose) console.error(`[small-model] 框架代执行完成,直接返回结果`)
|
|
315
|
+
// 框架已执行并把结果注入对话;直接把真实结果作为最终回复
|
|
316
|
+
finalResponse = execResult.resultText
|
|
317
|
+
break
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
|
|
@@ -522,31 +525,31 @@ export class QueryEngine {
|
|
|
522
525
|
*/
|
|
523
526
|
async _frameworkExecute(userInput) {
|
|
524
527
|
const action = extractFrameworkAction(userInput, { cwd: this.config.cwd })
|
|
525
|
-
if (!action) return { done: false, summary: '' }
|
|
528
|
+
if (!action) return { done: false, summary: '', resultText: '' }
|
|
526
529
|
|
|
527
530
|
// 找到对应的工具
|
|
528
531
|
const tool = this.config.tools.find(t => t.name === action.tool)
|
|
529
|
-
if (!tool) return { done: false, summary: '' }
|
|
532
|
+
if (!tool) return { done: false, summary: '', resultText: '' }
|
|
530
533
|
|
|
531
534
|
// 执行工具(直接调用 handler)
|
|
532
535
|
try {
|
|
533
536
|
const content = await tool.handler(action.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
|
|
534
537
|
|
|
535
|
-
// 注入框架代执行结果(作为 user 消息,避免 tool 消息无对应 assistant tool_call 报错)
|
|
536
|
-
// 说明:框架代执行不是模型发起的工具调用,不能作为 tool 角色(会缺 assistant tool_call),
|
|
537
|
-
// 因此以 user 消息承载真实结果 + 引导模型基于结果总结。
|
|
538
538
|
const resultText = typeof content === 'string' ? content : JSON.stringify(content)
|
|
539
|
+
|
|
540
|
+
// 注入框架代执行结果(作为 user 消息,避免 tool 消息无对应 assistant tool_call 报错)
|
|
541
|
+
// 说明:框架代执行不是模型发起的工具调用,不能作为 tool 角色(会缺 assistant tool_call)。
|
|
539
542
|
this.state.messages.push({
|
|
540
543
|
role: 'user',
|
|
541
|
-
content: `[框架代执行结果] 框架已替你执行工具 ${tool.name}(参数 ${JSON.stringify(action.input)}),结果如下:\n\n${resultText.slice(0, 8000)}
|
|
544
|
+
content: `[框架代执行结果] 框架已替你执行工具 ${tool.name}(参数 ${JSON.stringify(action.input)}),结果如下:\n\n${resultText.slice(0, 8000)}`,
|
|
542
545
|
})
|
|
543
546
|
|
|
544
547
|
const summary = `框架已代执行 ${tool.name}(${JSON.stringify(action.input)})`
|
|
545
548
|
if (this.config.verbose) console.error(`[small-model] 框架代执行 ${tool.name} 完成`)
|
|
546
|
-
return { done: true, summary }
|
|
549
|
+
return { done: true, summary, resultText }
|
|
547
550
|
} catch (err) {
|
|
548
551
|
if (this.config.verbose) console.error(`[small-model] 框架代执行失败: ${err.message}`)
|
|
549
|
-
return { done: false, summary: '' }
|
|
552
|
+
return { done: false, summary: '', resultText: '' }
|
|
550
553
|
}
|
|
551
554
|
}
|
|
552
555
|
|
package/src/core/small-model.js
CHANGED
|
@@ -256,17 +256,28 @@ export function extractFrameworkAction(userInput, opts = {}) {
|
|
|
256
256
|
const cwd = opts.cwd || ''
|
|
257
257
|
|
|
258
258
|
// 1. 读取文件任务:含路径 + 读/查看/打开
|
|
259
|
-
// 匹配绝对路径(Windows: D:\... 或 /... 或 ./...)或文件名
|
|
260
259
|
const readIntent = /(读|阅读|查看|打开|展示|显示|看看|浏览|看)/i.test(userInput)
|
|
261
|
-
// 提取路径:优先 Windows 绝对路径 D:\...、Unix /...、相对 ./ 或 ../,或 .md/.txt/.py/.json 文件
|
|
262
|
-
const pathMatch = userInput.match(/([A-Za-z]:\\[^\s,。;]+|(?:\/|\/\/)[^\s,。;]+|\.{1,2}\/[^\s,。;]+|[\w@.-]+\.(?:md|txt|py|json|log|yaml|yml|toml|ini|cfg|csv|xml))+/i)
|
|
263
260
|
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
// 提取路径。关键:路径字符集【只允许文件路径合法字符】,遇到逗号/分号/空格/中文
|
|
262
|
+
// 立即停止,避免把指令里的说明文字(如 ",对项目有个全盘了解")拼进路径。
|
|
263
|
+
// 合法路径字符:字母数字、\ / . : - _、空格(在引号内罕见,排除以免误吞)、
|
|
264
|
+
// Windows 盘符。
|
|
265
|
+
// 优先匹配带扩展名的完整路径,扩展名后即停止。
|
|
266
|
+
const pathRegex =
|
|
267
|
+
/([A-Za-z]:[\\/][\w@.\-\\/ ]*\.(?:md|txt|py|json|log|yaml|yml|toml|ini|cfg|csv|xml)|[\\/][\w@.\-\\/ ]*\.(?:md|txt|py|json|log|yaml|yml|toml|ini|cfg|csv|xml)|\.{1,2}[\\/][\w@.\-\\/]*\.(?:md|txt|py|json|log|yaml|yml|toml|ini|cfg|csv|xml)|[\w@.-]+\.(?:md|txt|py|json|log|yaml|yml|toml|ini|cfg|csv|xml))/i
|
|
268
|
+
|
|
269
|
+
// 用 [^,,;;。\s] 确保遇到分隔符就停,然后取最长的、以扩展名结尾的匹配
|
|
270
|
+
const match = userInput.match(pathRegex)
|
|
271
|
+
let filePath = null
|
|
272
|
+
if (match) {
|
|
273
|
+
filePath = match[1].trim()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (readIntent && filePath) {
|
|
266
277
|
// 去掉开头的 ./ 或 ../
|
|
267
278
|
filePath = filePath.replace(/^\.\.?[\\/]/, '')
|
|
268
279
|
// 若是相对路径且给了 cwd,拼成绝对路径
|
|
269
|
-
if (!/^[A-Za-z]
|
|
280
|
+
if (!/^[A-Za-z]:[\\/]/.test(filePath) && !filePath.startsWith('/') && cwd) {
|
|
270
281
|
filePath = cwd.replace(/[\\/]+$/, '') + '\\' + filePath
|
|
271
282
|
}
|
|
272
283
|
return { tool: 'Read', toolName: 'Read', input: { file_path: filePath } }
|
|
@@ -366,32 +377,53 @@ export function buildCombinedGuidance(userInput, opts = {}) {
|
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
/**
|
|
369
|
-
*
|
|
380
|
+
* 工具选择(层 A)— 小模型适配的关键经验
|
|
370
381
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
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
|
+
* 原则:**工具给全,让模型自己挑;而不是替它精简,导致它无工具可用而瞎编。**
|
|
373
392
|
*
|
|
374
393
|
* @param {Array} allTools — 完整 ToolDef 列表
|
|
375
394
|
* @param {string} userInput — 用户指令
|
|
376
395
|
* @param {object} [opts]
|
|
377
|
-
* @param {boolean} [opts.enable=true] —
|
|
378
|
-
* @returns {Array}
|
|
396
|
+
* @param {boolean} [opts.enable=true] — 是否启用适配
|
|
397
|
+
* @returns {Array} 选择后的工具列表(至少 6 个核心;找不到适配则全部)
|
|
379
398
|
*/
|
|
380
399
|
export function selectRelevantTools(allTools, userInput, opts = {}) {
|
|
381
400
|
if (opts.enable === false) return allTools
|
|
382
401
|
if (!allTools || allTools.length === 0) return allTools
|
|
383
402
|
|
|
403
|
+
// 核心工具保底集(每次必给):小模型不能过度精简工具,否则不会调用只会瞎编
|
|
404
|
+
const CORE = ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep']
|
|
384
405
|
const intent = detectIntent(userInput)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// 核心工具优先,保留指定工具;若一个都没命中则退回首屏核心集
|
|
391
|
-
const selected = allTools.filter(t => coreNames.includes(t.name))
|
|
392
|
-
if (selected.length === 0) {
|
|
393
|
-
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
|
|
394
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
|
+
|
|
395
427
|
return selected
|
|
396
428
|
}
|
|
397
429
|
|