| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 |
- 'use strict';
- /**
- * 智能问数智能体 · 护栏
- * ----------------------------------------------------------------------------
- * 预算常量(轮次/工具调用/修复次数/墙钟,零配置起步)、图表规格钳制
- * (复用语义层白名单,防模型捏造指标 key)、循环侦测(重复调用先提醒后强制收敛,
- * 对齐 ai-fde runtime「回注重复提醒而非硬杀」的 Codex/Claude Code 同款思路)。
- */
- function clampChart(raw, svc) {
- if (!raw || typeof raw !== 'object') return null;
- const validKeys = new Set((svc.REPORT_COLUMNS || []).map((c) => c.key));
- const type = ['bar', 'line'].includes(raw.type) ? raw.type : null;
- if (!type) return null;
- const x = raw.x === 'date' ? 'date' : 'shopName';
- const y = (Array.isArray(raw.y) ? raw.y : []).map(String).filter((k) => validKeys.has(k)).slice(0, 2);
- return {
- type,
- x,
- y: y.length ? y : ['grossAmt'],
- title: String(raw.title || '').slice(0, 60),
- };
- }
- const BUDGET = {
- maxTurns: 10, // LLM 调用次数上限(含 final)
- maxToolCalls: 16, // 工具执行次数上限
- maxRepairs: 2, // 解析失败/非法动作的连续回注修复次数
- llmTimeoutMs: 45000, // 单次对话模型超时(与 nl-query 同量级)
- runTimeoutMs: 120000, // 单次运行墙钟
- };
- /** 循环侦测:规范化(tool + 排序后的参数)做指纹;同调用第 2 次提醒、第 3 次强制收敛 */
- class LoopDetector {
- constructor() { this.lastKey = null; this.repeatCount = 0; }
- static fingerprint(tool, args) {
- const norm = {};
- for (const k of Object.keys(args || {}).sort()) norm[k] = args[k];
- return tool + '|' + JSON.stringify(norm);
- }
- record(tool, args) {
- const key = LoopDetector.fingerprint(tool, args);
- if (key === this.lastKey) {
- this.repeatCount += 1;
- return this.repeatCount >= 2 ? 'loop' : 'repeat';
- }
- this.lastKey = key;
- this.repeatCount = 0;
- return 'ok';
- }
- }
- module.exports = { BUDGET, clampChart, LoopDetector };
|