feat: 升级至 v0.3.0 — 安全增强、死循环检测、六轮全面审计修复
大版本迭代,新增安全增强、Agent Loop 增强、UI/UX 增强,经六轮全面审计修复所有问题。
新增功能:
- OutputValidator 事实一致性检查 + 幻觉检测
- PromptInjectionDefender 语义级检测(指令性动词密度、角色边界、分隔符嵌套)
- DeadLoopError 死循环检测(连续3轮相同工具调用自动终止)
- PolicyEngine maxFrequency 滑动窗口频率限制
- MemoryManager TF-IDF 语义检索 + IDF 缓存原子替换
- 斜杠命令(/tool /memory /clear /export)+ 快捷键(Ctrl+B/J/Shift+F/N/[/])
- 专注模式(Ctrl+Shift+F)带面板状态快照保存/恢复
六轮审计修复(共修复 3 CRITICAL + 8 HIGH + 13 MEDIUM + 11 LOW):
CRITICAL:
- main.ts 传入 createAdapter 而非 reloadAdapter,导致 Provider 切换完全失效
- Ctrl+N/Ctrl+[/Ctrl+] 不同步 agent-store,导致消息发到错误会话
- engine.ts emit('error') 无监听器导致 DONE 事件丢失、前端卡死
HIGH:
- 死循环检测在工具执行之后(移入 PARSING 后 EXECUTING 前)
- deadLoop 事件前端未处理
- ConfirmationHook.clearPending 从未调用导致定时器泄漏
- engine.ts retry abort listener 未移除导致监听器堆积
- MCPManager JSON.parse 无 try-catch 导致初始化崩溃
- sendMessage 自动创建会话不同步 session-store
- setCurrentSession 竞态导致旧请求覆盖新会话数据
MEDIUM:
- toggleFocusMode 覆盖用户原有面板状态
- 正则检测可被常见词绕过
- 频率限制内存泄漏 + customPolicies 覆盖
- LIKE 回退转义未包含反斜杠
- OutputValidator 新功能未传入 toolResults/context
- MemoryConsolidator LLM 调用无超时保护
- AuditService 每次 log 都查询数据库
- browser-window-manager 超时后未停止页面加载
- output-validator URL 比较大小写敏感
- FileReader 无 onerror 导致 Promise 永久挂起
- /clear /export 不关闭斜杠菜单
- 专注模式下 toggleSidebar/toggleDetail 未恢复另一面板快照
LOW:
- abortPromise 事件监听器堆积
- RateLimitHook Map 内存泄漏
- memory.ts await 同步方法
- PolicyEngine 死代码清理
- Orchestrator sessionDepth 会话结束不清理
- workspace.service.ts 元数据插入边界问题
This commit is contained in:
@@ -7,6 +7,10 @@
|
||||
* 3. 事实一致性检查(与工具结果是否矛盾)
|
||||
* 4. 幻觉检测(无根据的断言)
|
||||
*
|
||||
* v0.3.0 增强:
|
||||
* - 实现事实一致性检查(基于工具结果验证输出声明)
|
||||
* - 实现幻觉检测(检测上下文中无依据的具体声明)
|
||||
*
|
||||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
|
||||
*/
|
||||
|
||||
@@ -22,10 +26,18 @@ export interface ValidationIssue {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ValidationOptions {
|
||||
/** 工具调用结果文本列表,用于事实一致性检查 */
|
||||
toolResults?: string[];
|
||||
/** 对话上下文文本,用于幻觉检测 */
|
||||
context?: string;
|
||||
}
|
||||
|
||||
/** 敏感词模式 */
|
||||
const SENSITIVE_PATTERNS = [
|
||||
{ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, type: 'credit_card', message: 'Possible credit card number detected' },
|
||||
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, type: 'email', message: 'Email address detected' },
|
||||
// v0.3.0 修复:移除字符类中的 | 字面字符([A-Z|a-z] → [A-Za-z])
|
||||
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/, type: 'email', message: 'Email address detected' },
|
||||
{ pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, type: 'phone', message: 'Phone number detected' },
|
||||
{ pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/, type: 'api_key', message: 'Possible API key detected' },
|
||||
];
|
||||
@@ -40,8 +52,10 @@ const UNSAFE_PATTERNS = [
|
||||
export class OutputValidator {
|
||||
/**
|
||||
* 验证输出内容
|
||||
*
|
||||
* v0.3.0: 新增事实一致性检查和幻觉检测
|
||||
*/
|
||||
async validate(output: string): Promise<ValidationResult> {
|
||||
async validate(output: string, options?: ValidationOptions): Promise<ValidationResult> {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 1. 格式验证
|
||||
@@ -50,12 +64,22 @@ export class OutputValidator {
|
||||
// 2. 内容安全检测
|
||||
issues.push(...this.checkSafety(output));
|
||||
|
||||
// 3. 空内容检查
|
||||
// 3. 事实一致性检查(v0.3.0 新增)
|
||||
if (options?.toolResults && options.toolResults.length > 0) {
|
||||
issues.push(...this.checkFactConsistency(output, options.toolResults));
|
||||
}
|
||||
|
||||
// 4. 幻觉检测(v0.3.0 新增)
|
||||
if (options?.context) {
|
||||
issues.push(...this.checkHallucination(output, options.context));
|
||||
}
|
||||
|
||||
// 5. 空内容检查
|
||||
if (!output.trim()) {
|
||||
issues.push({ severity: 'error', type: 'empty', message: 'Output is empty' });
|
||||
}
|
||||
|
||||
// 4. 过短内容检查
|
||||
// 6. 过短内容检查
|
||||
if (output.trim().length < 10 && output.trim().length > 0) {
|
||||
issues.push({ severity: 'warning', type: 'short', message: 'Output is suspiciously short' });
|
||||
}
|
||||
@@ -67,6 +91,173 @@ export class OutputValidator {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 事实一致性检查 — 验证输出中的声明是否与工具结果矛盾
|
||||
*
|
||||
* v0.3.0 实现的检查策略:
|
||||
* 1. 工具报告错误但输出声称成功
|
||||
* 2. 工具报告文件不存在但输出引用文件内容
|
||||
* 3. 工具报告的数值与输出中的数值矛盾
|
||||
*
|
||||
* v0.3.0 修复:
|
||||
* - errorIndicators 改为更精确的匹配,避免常见词 "error" 误报
|
||||
* 使用 "error:" 或 "error occurred" 等上下文限定
|
||||
*/
|
||||
private checkFactConsistency(output: string, toolResults: string[]): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
const toolContext = toolResults.join('\n');
|
||||
|
||||
// 检查1:工具报告错误但输出声称成功
|
||||
// v0.3.0 修复:errorIndicators 改为更精确的匹配,避免 "error" 单独出现导致误报
|
||||
// 要求 error 后跟冒号、消息或特定错误模式
|
||||
const errorIndicators = /(?:error\s*[:\)]|error\s+occurred|failed\s+to|not\s+found|does\s+not\s+exist|enoent|permission\s+denied|cannot\s+access|no\s+such\s+file|exception|traceback|exit\s+code\s+[1-9])/i;
|
||||
const hasErrorInTools = errorIndicators.test(toolContext);
|
||||
|
||||
if (hasErrorInTools) {
|
||||
const successClaims = [
|
||||
/\b(?:successfully|success|done|completed|created|written|deleted|installed|updated|finished)\b/i,
|
||||
/(?:已(?:完成|创建|写入|删除|安装|更新|成功))/,
|
||||
];
|
||||
for (const pattern of successClaims) {
|
||||
const match = output.match(pattern);
|
||||
if (match) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
type: 'fact_inconsistency',
|
||||
message: `Output claims "${match[0]}" but tool results contain errors`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查2:工具报告文件不存在但输出引用文件内容
|
||||
const notFoundPattern = /(?:not\s+found|no\s+such\s+file|does\s+not\s+exist|文件(?:不存在|未找到|找不到))/i;
|
||||
if (notFoundPattern.test(toolContext)) {
|
||||
const contentClaimPatterns = [
|
||||
/(?:file\s+(?:contains|says|shows)|文件(?:内容|包含|显示))/i,
|
||||
/(?:the\s+content\s+is|内容是)/i,
|
||||
];
|
||||
for (const pattern of contentClaimPatterns) {
|
||||
if (pattern.test(output)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
type: 'fact_inconsistency',
|
||||
message: 'Output references file content but tool reported file not found',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查3:工具报告退出码非零但输出声称命令成功
|
||||
const exitCodePattern = /exit\s+code[:\s]+(\d+)/i;
|
||||
const exitMatch = toolContext.match(exitCodePattern);
|
||||
if (exitMatch && parseInt(exitMatch[1], 10) !== 0) {
|
||||
const commandSuccessPattern = /\b(?:command\s+(?:succeeded|completed\s+successfully|ran\s+successfully))\b/i;
|
||||
if (commandSuccessPattern.test(output)) {
|
||||
issues.push({
|
||||
severity: 'error',
|
||||
type: 'fact_inconsistency',
|
||||
message: `Output claims command success but tool reported exit code ${exitMatch[1]}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 幻觉检测 — 检测输出中无依据的具体声明
|
||||
*
|
||||
* v0.3.0 实现的检查策略:
|
||||
* 1. 提取输出中的文件路径,检查是否在上下文中出现
|
||||
* 2. 提取输出中的 URL,检查是否在上下文中出现
|
||||
* 3. 检测虚构的 API 响应模式
|
||||
*
|
||||
* v0.3.0 修复:
|
||||
* - Windows 路径正则包含反斜杠字符类,正确匹配 C:\Users\test 等完整路径
|
||||
* - apiResponsePattern 在循环前重置 lastIndex,避免间歇性漏检
|
||||
* - 路径比较改为大小写不敏感(Windows 路径不敏感)
|
||||
*/
|
||||
private checkHallucination(output: string, context: string): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// 检查1:文件路径声称但上下文中不存在
|
||||
// v0.3.0 修复:Windows 路径正则 [A-Z]:\\[\w\\][\w.-]+ 不含反斜杠,改为 [A-Z]:\\[\w\\.-]+(?:\\[\w\\.-]+)*
|
||||
const pathPattern = /(?:\/[\w][\w.-]*){2,}|[A-Z]:\\[\w\\.-]+(?:\\[\w\\.-]+)*/g;
|
||||
const paths = output.match(pathPattern) || [];
|
||||
const seenPaths = new Set<string>();
|
||||
// v0.3.0 修复:上下文比较改为小写(Windows 路径大小写不敏感)
|
||||
const contextLower = context.toLowerCase();
|
||||
for (const path of paths) {
|
||||
if (seenPaths.has(path)) continue;
|
||||
seenPaths.add(path);
|
||||
|
||||
// 检查输出是否声称读取/访问了该路径
|
||||
const pathIndex = output.indexOf(path);
|
||||
const surrounding = output.substring(
|
||||
Math.max(0, pathIndex - 80),
|
||||
Math.min(output.length, pathIndex + path.length + 80),
|
||||
);
|
||||
const accessClaim = /(?:read|loaded|opened|accessed|found|exists|contains|读取|加载|打开|访问|存在|包含)/i;
|
||||
|
||||
// v0.3.0 修复:路径比较改为大小写不敏感
|
||||
if (accessClaim.test(surrounding) && !contextLower.includes(path.toLowerCase()) && path.length > 8) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
type: 'hallucination',
|
||||
message: `Path "${path.substring(0, 60)}" claimed in output but not found in context`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查2:URL 声称但上下文中不存在
|
||||
const urlPattern = /https?:\/\/[^\s<>"]{8,}/g;
|
||||
const urls = output.match(urlPattern) || [];
|
||||
const seenUrls = new Set<string>();
|
||||
for (const url of urls) {
|
||||
if (seenUrls.has(url)) continue;
|
||||
seenUrls.add(url);
|
||||
|
||||
// URL 在上下文中不存在且输出声称访问了它
|
||||
const urlIndex = output.indexOf(url);
|
||||
const surrounding = output.substring(
|
||||
Math.max(0, urlIndex - 60),
|
||||
Math.min(output.length, urlIndex + url.length + 60),
|
||||
);
|
||||
const fetchClaim = /(?:fetched|retrieved|loaded|returned|visited|获取|抓取|访问|返回)/i;
|
||||
|
||||
// v0.3.0 修复: URL 比较改为大小写不敏感,因为 URL host 部分大小写不敏感
|
||||
if (fetchClaim.test(surrounding) && !context.toLowerCase().includes(url.toLowerCase())) {
|
||||
issues.push({
|
||||
severity: 'info',
|
||||
type: 'hallucination',
|
||||
message: `URL in output not found in conversation context`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查3:检测虚构的 API 响应(声称返回了特定 JSON 但上下文中没有)
|
||||
const apiResponsePattern = /(?:API\s+(?:returned|responded)|接口(?:返回|响应))[:\s]*(\{[^}]{10,}\})/gi;
|
||||
// v0.3.0 修复:循环前重置 lastIndex,避免带 g flag 的正则在 break 后 lastIndex 未重置导致漏检
|
||||
apiResponsePattern.lastIndex = 0;
|
||||
let apiMatch;
|
||||
while ((apiMatch = apiResponsePattern.exec(output)) !== null) {
|
||||
const jsonSnippet = apiMatch[1];
|
||||
if (!context.includes(jsonSnippet.substring(0, 20))) {
|
||||
issues.push({
|
||||
severity: 'warning',
|
||||
type: 'hallucination',
|
||||
message: 'API response in output not found in tool results',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式验证
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user