@raolin2025/claude-code-node 2.8.21 → 2.8.22

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.21",
3
+ "version": "2.8.22",
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",
@@ -249,6 +249,17 @@ test('框架代执行:非读取任务或无路径返回 null', () => {
249
249
  assert.equal(extractFrameworkAction('运行测试', { cwd: 'D:\\x' }), null)
250
250
  })
251
251
 
252
+ test('框架代执行:路径后带中文说明时正确截断(不吞入多余内容)', () => {
253
+ // 复现 error.log 的 bug:路径后跟中文说明,之前会被贪婪匹配进路径
254
+ const a = extractFrameworkAction(
255
+ '阅读D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader\\COMPLETION_REPORT.md,对项目有个全盘了解',
256
+ { cwd: 'D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader' }
257
+ )
258
+ assert.ok(a, '应识别到框架动作')
259
+ assert.equal(a.input.file_path, 'D:\\workspace\\QMT_CrossPlatform\\miniQMT-trader\\COMPLETION_REPORT.md',
260
+ `路径不应吞入中文说明(实际 ${a.input.file_path})`)
261
+ })
262
+
252
263
  // ---- _buildRequest 把 system 统一前置(防 llama.cpp 500)----
253
264
  test('_buildRequest:把中间 system 统一前置到开头', async () => {
254
265
  const { QueryEngine } = await import('../core/query-engine.js')
@@ -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] 框架代执行:${execResult.summary}`)
313
- // 框架已执行工具并把结果注入对话,继续循环让模型基于结果总结
314
- continue
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)}\n\n请基于以上【真实执行结果】,用中文直接回答用户的问题或总结内容。不要再调用工具,直接总结即可。`,
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
 
@@ -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
- if (readIntent && pathMatch) {
265
- let filePath = pathMatch[1].trim()
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]:\\/.test(filePath) && !filePath.startsWith('/') && cwd) {
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 } }