@raolin2025/claude-code-node 2.8.17 → 2.8.19
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.
|
|
3
|
+
"version": "2.8.19",
|
|
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",
|
|
@@ -284,3 +284,39 @@ test('工具结果截断:非 tool 消息与空内容不受影响', () => {
|
|
|
284
284
|
assert.equal(r[0].content.length, 10000, 'user 消息不应被截断')
|
|
285
285
|
assert.equal(r[1].content, '')
|
|
286
286
|
})
|
|
287
|
+
|
|
288
|
+
test('折叠/裁剪:多次折叠后所有 system 仍在开头(防 llama.cpp 500)', () => {
|
|
289
|
+
// 模拟:一次折叠后 body 里有摘要 system 残留的场景
|
|
290
|
+
const msgs = [
|
|
291
|
+
{ role: 'system', content: 'ROOT SYS' },
|
|
292
|
+
{ role: 'system', content: '[Context Summary — 旧的折叠摘要]' }, // 已存在第二条 system
|
|
293
|
+
{ role: 'user', content: '任务1' },
|
|
294
|
+
{ role: 'assistant', content: '结果1' },
|
|
295
|
+
{ role: 'user', content: '任务2' },
|
|
296
|
+
{ role: 'assistant', content: '结果2' },
|
|
297
|
+
]
|
|
298
|
+
// foldHistoryByCount 折叠后,所有 system 都应在最前,body 里无 system
|
|
299
|
+
const r = foldHistoryByCount(msgs, { maxMessages: 5, keepRecentTurns: 1 })
|
|
300
|
+
const roles = r.messages.map(m => m.role)
|
|
301
|
+
// 找到第一个非 system 的索引,之后不应再有 system
|
|
302
|
+
const firstBody = roles.findIndex(x => x !== 'system')
|
|
303
|
+
const afterBody = roles.slice(firstBody + 1)
|
|
304
|
+
assert.ok(!afterBody.includes('system'), `折叠后 body 中不应有 system(实际 roles=${roles.join(',')})`)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('trimToWindow:所有 system 均在开头,body 中无 system', () => {
|
|
308
|
+
const msgs = [
|
|
309
|
+
{ role: 'system', content: 'ROOT SYS' },
|
|
310
|
+
{ role: 'system', content: '[Context Summary — 旧摘要]' },
|
|
311
|
+
{ role: 'user', content: 'a'.repeat(50) },
|
|
312
|
+
{ role: 'assistant', content: 'b'.repeat(50) },
|
|
313
|
+
{ role: 'user', content: 'c'.repeat(50) },
|
|
314
|
+
{ role: 'assistant', content: 'd'.repeat(50) },
|
|
315
|
+
]
|
|
316
|
+
const tb = makeBudget(200, 0)
|
|
317
|
+
const r = trimToWindow(msgs, { tokenBudget: tb, maxTokens: tb.maxTokens })
|
|
318
|
+
const roles = r.messages.map(m => m.role)
|
|
319
|
+
const firstBody = roles.findIndex(x => x !== 'system')
|
|
320
|
+
const afterBody = roles.slice(firstBody + 1)
|
|
321
|
+
assert.ok(!afterBody.includes('system'), `trim 后 body 中不应有 system(实际 roles=${roles.join(',')})`)
|
|
322
|
+
})
|
package/src/core/cli.js
CHANGED
|
@@ -333,6 +333,7 @@ function parseArgs(argv) {
|
|
|
333
333
|
maxMessages: 0,
|
|
334
334
|
smallModel: false,
|
|
335
335
|
maxOutputTokens: 0,
|
|
336
|
+
smallModelMaxTurns: 0,
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
let i = 2
|
|
@@ -351,6 +352,7 @@ function parseArgs(argv) {
|
|
|
351
352
|
case '--max-messages': args.maxMessages = parseInt(argv[++i], 10); break
|
|
352
353
|
case '--small-model': args.smallModel = true; break
|
|
353
354
|
case '--max-output-tokens': args.maxOutputTokens = parseInt(argv[++i], 10); break
|
|
355
|
+
case '--small-model-max-turns': args.smallModelMaxTurns = parseInt(argv[++i], 10); break
|
|
354
356
|
case '--stdio': args.stdio = true; break
|
|
355
357
|
case '--with-notify': args.withNotify = true; break
|
|
356
358
|
case '--version':
|
|
@@ -374,6 +376,7 @@ Options:
|
|
|
374
376
|
--max-messages N Fold history when message count exceeds N (default: 0 = off)
|
|
375
377
|
--small-model Enable small-model adaptation (tool-call enforcement, filler retry, intent guidance)
|
|
376
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)
|
|
377
380
|
--with-notify Start built-in channel listener (Telegram)
|
|
378
381
|
(replaces cc-notify daemon — no external script needed)
|
|
379
382
|
-h, --help Show this help
|
|
@@ -540,6 +543,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
|
|
|
540
543
|
smallModel: cliArgs.smallModel || config.get('smallModel') || false,
|
|
541
544
|
// 单次输出上限覆盖(默认根据窗口动态计算)
|
|
542
545
|
maxOutputTokens: cliArgs.maxOutputTokens || config.get('maxOutputTokens') || 0,
|
|
546
|
+
// 小模型工具循环轮数上限(默认 8,防无限循环)
|
|
547
|
+
smallModelMaxTurns: cliArgs.smallModelMaxTurns || config.get('smallModelMaxTurns') || 0,
|
|
543
548
|
})
|
|
544
549
|
const engine = new QueryEngine(engineConfig)
|
|
545
550
|
|
package/src/core/compact.js
CHANGED
|
@@ -197,11 +197,14 @@ export function foldHistoryByCount(messages, options = {}) {
|
|
|
197
197
|
return { folded: false, messages, removed: 0, summary: null }
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// 分离 system
|
|
200
|
+
// 分离 system 提示与普通消息
|
|
201
|
+
// 注意:把所有 system 都收集到 systemMsgs(不只是第一条)——多次折叠后 body 里
|
|
202
|
+
// 可能残留旧的摘要 system,若不全部隔离,折叠结果会把 system 挤到对话中间,
|
|
203
|
+
// 触发 llama.cpp "System message must be at the beginning" (500)。
|
|
201
204
|
const systemMsgs = []
|
|
202
205
|
const body = []
|
|
203
206
|
for (const m of messages) {
|
|
204
|
-
if (m.role === 'system'
|
|
207
|
+
if (m.role === 'system') {
|
|
205
208
|
systemMsgs.push(m)
|
|
206
209
|
} else {
|
|
207
210
|
body.push(m)
|
|
@@ -400,11 +403,13 @@ export function trimToWindow(messages, options = {}) {
|
|
|
400
403
|
return { trimmed: false, messages, removed: 0, summary: null }
|
|
401
404
|
}
|
|
402
405
|
|
|
403
|
-
// 分离 system
|
|
406
|
+
// 分离 system 提示与普通消息
|
|
407
|
+
// 注意:收集【所有】 system(不只第一条),避免多次裁剪后旧的摘要 system
|
|
408
|
+
// 残留在 body 里、被挤到对话中间,触发 llama.cpp "System message must be at the beginning" (500)。
|
|
404
409
|
const systemMsgs = []
|
|
405
410
|
const body = []
|
|
406
411
|
for (const m of messages) {
|
|
407
|
-
if (m.role === 'system'
|
|
412
|
+
if (m.role === 'system') {
|
|
408
413
|
systemMsgs.push(m)
|
|
409
414
|
} else {
|
|
410
415
|
body.push(m)
|
package/src/core/query-engine.js
CHANGED
|
@@ -53,6 +53,9 @@ export class QueryEngineConfig {
|
|
|
53
53
|
// - 工具数量精简 + 意图引导
|
|
54
54
|
// 默认关闭,通过 config.smallModel=true 或 --small-model 开启
|
|
55
55
|
this.smallModel = options.smallModel || false
|
|
56
|
+
// 小模型模式下的工具循环轮数上限(默认 8):
|
|
57
|
+
// 小模型常陷入"写一个又写一个"的无限工具循环,需更小的上限 + 收尾引导
|
|
58
|
+
this.smallModelMaxTurns = options.smallModelMaxTurns || 8
|
|
56
59
|
// 单次输出上限动态计算:
|
|
57
60
|
// outputRatio — 输出占窗口比例(默认 1/16),窗口越大输出越大
|
|
58
61
|
// maxOutputTokens — 输出下限(默认 4096),也可作为硬性覆盖
|
|
@@ -228,8 +231,14 @@ export class QueryEngine {
|
|
|
228
231
|
// 敷衍重试次数(层 A):模型没调工具只回空话时,追加强引导重试
|
|
229
232
|
let fillerRetries = 0
|
|
230
233
|
const MAX_FILLER_RETRIES = 1
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
// 工具循环轮数上限:
|
|
235
|
+
// - 小模型模式:用更小的上限(默认 8),避免模型陷入"写一个又写一个"的无限循环
|
|
236
|
+
// - 普通模式:用 config.maxTurns
|
|
237
|
+
const toolLoopLimit = smallModel
|
|
238
|
+
? (this.config.smallModelMaxTurns || 8)
|
|
239
|
+
: this.config.maxTurns
|
|
240
|
+
|
|
241
|
+
for (let turn = 0; turn < toolLoopLimit; turn++) {
|
|
233
242
|
// 发送前硬校验:工具结果可能已使上下文超窗,确保 ≤ 窗口(摘要优先 + 滑动窗口裁剪兜底)
|
|
234
243
|
this._ensureFitWindow()
|
|
235
244
|
|
|
@@ -259,6 +268,22 @@ export class QueryEngine {
|
|
|
259
268
|
intentGuided = true
|
|
260
269
|
}
|
|
261
270
|
|
|
271
|
+
// 小模型模式:接近工具循环上限时,注入"总结收尾"引导,防止模型无限循环
|
|
272
|
+
if (smallModel && turn === toolLoopLimit - 1) {
|
|
273
|
+
const stopGuidance = `[系统提示] 你已经完成了大部分工作。现在请【停止调用新工具】,把已经完成的内容整理成一段总结回复给用户。如果确实还有关键步骤未完成,只再调用一次工具完成它,然后立即总结。不要再继续无休止地调用工具。`
|
|
274
|
+
const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
|
|
275
|
+
if (sysIdx !== -1) {
|
|
276
|
+
const sysMsg = this.state.messages[sysIdx]
|
|
277
|
+
this.state.messages[sysIdx] = {
|
|
278
|
+
...sysMsg,
|
|
279
|
+
content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + stopGuidance,
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
this.state.messages.push({ role: 'user', content: stopGuidance })
|
|
283
|
+
}
|
|
284
|
+
if (this.config.verbose) console.error(`[small-model] 已到工具循环第 ${turn + 1} 轮(上限 ${toolLoopLimit}),注入收尾引导`)
|
|
285
|
+
}
|
|
286
|
+
|
|
262
287
|
const requestMessages = this._buildRequest(this.state.messages)
|
|
263
288
|
const response = await this._callLLM(requestMessages, this.state.messages)
|
|
264
289
|
|
|
@@ -306,8 +331,9 @@ export class QueryEngine {
|
|
|
306
331
|
}
|
|
307
332
|
}
|
|
308
333
|
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
// 工具循环达到上限但仍未收尾(模型一直调用工具)→ 强制收尾
|
|
335
|
+
if (!finalResponse) {
|
|
336
|
+
finalResponse = `[已达到工具循环上限 (${toolLoopLimit}),已停止进一步调用工具。请查看上方工具执行结果,确认任务完成情况。]`
|
|
311
337
|
}
|
|
312
338
|
|
|
313
339
|
return {
|