@raolin2025/claude-code-node 2.8.16 → 2.8.18

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.16",
3
+ "version": "2.8.18",
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
+ })
@@ -226,3 +226,29 @@ test('max_tokens:绝不超过窗口一半(防超窗)', async () => {
226
226
  const qe2 = new QueryEngine({ tokenBudget: new TokenBudget({ maxTokens: 10000 }), outputRatio: 1.0 })
227
227
  assert.ok(qe2._computeMaxOutputTokens() <= 5000, `max_tokens=${qe2._computeMaxOutputTokens()} 不应超过窗口一半 5000`)
228
228
  })
229
+
230
+ // ---- _buildRequest 把 system 统一前置(防 llama.cpp 500)----
231
+ test('_buildRequest:把中间 system 统一前置到开头', async () => {
232
+ const { QueryEngine } = await import('../core/query-engine.js')
233
+ const qe = new QueryEngine({ systemPrompt: 'SYS-ROOT', tools: [] })
234
+ // 模拟 state.messages 里 system 摘要被插到中间(折叠/恢复导致)
235
+ const messages = [
236
+ { role: 'user', content: 'u1' },
237
+ { role: 'assistant', content: 'a1' },
238
+ { role: 'system', content: '[Context Summary] 中间摘要' },
239
+ { role: 'user', content: 'u2' },
240
+ ]
241
+ const req = qe._buildRequest(messages)
242
+ // 验证:所有 system 在开头,中间无 system
243
+ let seenBody = false, badSystem = false
244
+ for (const m of req) {
245
+ if (m.role !== 'system') seenBody = true
246
+ else if (seenBody) badSystem = true
247
+ }
248
+ assert.equal(badSystem, false, '中间不应出现 system(否则 llama.cpp 报 500)')
249
+ // 第一条是 system
250
+ assert.equal(req[0].role, 'system')
251
+ // 原始顺序(非 system 部分)应保持
252
+ const nonSys = req.filter(m => m.role !== 'system').map(m => m.role)
253
+ assert.deepEqual(nonSys, ['user', 'assistant', 'user'])
254
+ })
@@ -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 提示(首条 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' && systemMsgs.length === 0) {
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 提示(首条 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' && systemMsgs.length === 0) {
412
+ if (m.role === 'system') {
408
413
  systemMsgs.push(m)
409
414
  } else {
410
415
  body.push(m)
@@ -358,10 +358,25 @@ export class QueryEngine {
358
358
  }
359
359
 
360
360
  // 历史消息 — 转换为 OpenAI 兼容格式
361
+ // 关键:把所有 system 消息【收集到一起放到最前面】,而不是原样透传。
362
+ // 原因:llama.cpp/Jinja 严格要求 system 消息必须在对话开头,中间出现 system
363
+ // 会报 "System message must be at the beginning" (500)。
364
+ // 折叠/压缩/引导注入可能在 state.messages 中间留下 system 摘要消息,
365
+ // 这里统一前置,彻底规避该错误。
366
+ const systemMsgs = []
367
+ const bodyMsgs = []
361
368
  for (const msg of messages) {
362
369
  if (msg.role === 'system') {
363
- request.push({ role: 'system', content: msg.content })
364
- } else if (msg.role === 'user') {
370
+ systemMsgs.push(msg)
371
+ } else {
372
+ bodyMsgs.push(msg)
373
+ }
374
+ }
375
+ for (const msg of systemMsgs) {
376
+ request.push({ role: 'system', content: msg.content })
377
+ }
378
+ for (const msg of bodyMsgs) {
379
+ if (msg.role === 'user') {
365
380
  request.push({ role: 'user', content: this._buildUserContent(msg) })
366
381
  } else if (msg.role === 'assistant') {
367
382
  // 构建 assistant 消息基础