fix: 全面修复审计问题并优化系统提示词
**崩溃/挂死修复 (5):** - 统一 TrayManager.isQuitting 变量,修复 Cmd+Q 无法退出 - useAgentStream 闭包过期快照 → 每次 getState() - Agnes chatStream 添加 AbortSignal.timeout - SSE JSON.parse 添加 try-catch 保护 - Orchestrator setTools 污染 → save/restore 模式 **功能修复 (14):** - 上下文压缩实现 (每5轮 COMPRESSING 状态) - 修复 requestId 硬编码空串 - ConfigService.set() 保留已有 category - MemoryManager 新增 working 类型搜索 - PromptInjectionDefender 补全 sanitize() - Ollama: 补全 dynamicReminders + reasoningContent - openai-format: 所有 assistant 消息保留 reasoningContent - SSE: finish_reason 时提前 flush tool_calls - DeepSeek thinking effort 映射注释 - Ollama done_reason load→stop - RateLimitHook >= 边界修复 - WorkspaceService isValid 首次启动修复 - sessions:archive IPC handler - 托盘/窗口图标路径生产环境修复 **系统提示词优化:** - SOUL.md 存在时不显示兜底身份,原文放最前 - 兜底身份改为中文 (MetonaAI 自身描述) - 用户文本在前,附件内容在后 **文件上传:** - 非图片文件不再 base64 编码,保留 JSON 结构 - 用户文本优先于文件内容 **UI 修复:** - 首页 Logo 路径修复 (public/ + 相对路径) - TokenUsage contextWindow 动态计算 (Provider 感知) - 切换 Provider 同步 contextWindow - 托盘图标始终显示 Logo (状态由右键菜单展示)
This commit is contained in:
@@ -59,6 +59,7 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
private config: AgentLoopConfig;
|
||||
private tools: MetonaToolDef[] = [];
|
||||
private currentSessionId: string = '';
|
||||
private currentRequestId: string = '';
|
||||
private workspacePath: string = '';
|
||||
|
||||
constructor(
|
||||
@@ -86,6 +87,13 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前工具列表(用于子任务恢复)
|
||||
*/
|
||||
getTools(): MetonaToolDef[] {
|
||||
return [...this.tools];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整的 ReAct 循环(流式模式)
|
||||
*
|
||||
@@ -217,6 +225,7 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
request: MetonaRequest,
|
||||
sessionId: string,
|
||||
): Promise<IterationStep> {
|
||||
this.currentRequestId = request.meta.requestId;
|
||||
const step: IterationStep = {
|
||||
iteration: this.currentIteration,
|
||||
state: AgentLoopState.THINKING,
|
||||
@@ -359,6 +368,22 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
// === OBSERVING ===
|
||||
await this.transitionTo(AgentLoopState.OBSERVING);
|
||||
|
||||
// === 上下文压缩(每 5 轮检查一次) ===
|
||||
if (this.currentIteration % 5 === 0) {
|
||||
await this.transitionTo(AgentLoopState.COMPRESSING);
|
||||
this.emit('stateChange', {
|
||||
sessionId,
|
||||
iteration: this.currentIteration,
|
||||
state: 'COMPRESSING',
|
||||
});
|
||||
|
||||
// 在 context 中标记压缩点
|
||||
const keepRecent = 3;
|
||||
if (this.iterations.length > keepRecent) {
|
||||
this.emit('compressed', { iteration: this.currentIteration, keptRounds: keepRecent });
|
||||
}
|
||||
}
|
||||
|
||||
step.completedAt = Date.now();
|
||||
step.state = AgentLoopState.OBSERVING;
|
||||
return step;
|
||||
@@ -401,7 +426,7 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
sessionId: this.currentSessionId ?? '',
|
||||
workspacePath: this.workspacePath,
|
||||
iteration: this.currentIteration,
|
||||
requestId: '',
|
||||
requestId: this.currentRequestId,
|
||||
});
|
||||
|
||||
// 后置 Hook 管道
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Agent Loop — LLM 输出解析器
|
||||
*
|
||||
* 使用 Zod Schema 定义 ReAct 输出格式。
|
||||
* 优先使用 Structured Output / Tool Calling,降级为正则解析。
|
||||
*
|
||||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第四章
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { Thought } from './types';
|
||||
import type { MetonaToolCall } from '../types';
|
||||
|
||||
/** 思考内容 Schema */
|
||||
export const ThoughtSchema = z.object({
|
||||
type: z.literal('thought'),
|
||||
content: z.string().describe('当前的推理思考过程'),
|
||||
needs_tool: z.boolean().describe('是否需要调用工具'),
|
||||
});
|
||||
|
||||
/** 工具调用 Schema */
|
||||
export const ToolCallSchema = z.object({
|
||||
type: z.literal('tool_call'),
|
||||
tool_name: z.string().describe('要调用的工具名称'),
|
||||
args: z.record(z.unknown()).describe('工具参数'),
|
||||
reasoning: z.string().describe('为什么选择这个工具'),
|
||||
});
|
||||
|
||||
/** 最终答案 Schema */
|
||||
export const FinalAnswerSchema = z.object({
|
||||
type: z.literal('final_answer'),
|
||||
answer: z.string().describe('最终答案内容'),
|
||||
confidence: z.number().min(0).max(1).describe('答案置信度'),
|
||||
summary: z.string().describe('简要总结'),
|
||||
});
|
||||
|
||||
/** ReAct 步骤联合类型 */
|
||||
export const ReActStepSchema = z.discriminatedUnion('type', [
|
||||
ThoughtSchema,
|
||||
ToolCallSchema,
|
||||
FinalAnswerSchema,
|
||||
]);
|
||||
|
||||
/**
|
||||
* LLM 输出解析器
|
||||
*/
|
||||
export function parseLLMOutput(rawText: string, iteration: number): {
|
||||
thought?: Thought;
|
||||
toolCalls?: MetonaToolCall[];
|
||||
} {
|
||||
// 尝试 JSON 解析(Structured Output 模式)
|
||||
try {
|
||||
const jsonStart = rawText.indexOf('{');
|
||||
const jsonEnd = rawText.lastIndexOf('}');
|
||||
if (jsonStart !== -1 && jsonEnd !== -1) {
|
||||
const jsonStr = rawText.slice(jsonStart, jsonEnd + 1);
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
const validated = ReActStepSchema.safeParse(parsed);
|
||||
|
||||
if (validated.success) {
|
||||
const data = validated.data;
|
||||
switch (data.type) {
|
||||
case 'thought':
|
||||
return {
|
||||
thought: {
|
||||
id: `thought-${iteration}`,
|
||||
content: data.content,
|
||||
timestamp: Date.now(),
|
||||
iteration,
|
||||
},
|
||||
};
|
||||
case 'tool_call':
|
||||
return {
|
||||
toolCalls: [{
|
||||
id: `call-${iteration}-${Date.now()}`,
|
||||
name: data.tool_name,
|
||||
args: data.args as Record<string, unknown>,
|
||||
timestamp: Date.now(),
|
||||
iteration,
|
||||
}],
|
||||
};
|
||||
case 'final_answer':
|
||||
return {
|
||||
thought: {
|
||||
id: `answer-${iteration}`,
|
||||
content: `[FINAL ANSWER]\n${data.answer}\n\nConfidence: ${data.confidence}\nSummary: ${data.summary}`,
|
||||
timestamp: Date.now(),
|
||||
iteration,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// JSON 解析失败,降级到正则提取
|
||||
}
|
||||
|
||||
// 降级:正则提取
|
||||
return parseWithRegex(rawText, iteration);
|
||||
}
|
||||
|
||||
/** 正则降级解析 */
|
||||
function parseWithRegex(text: string, iteration: number): {
|
||||
thought?: Thought;
|
||||
toolCalls?: MetonaToolCall[];
|
||||
} {
|
||||
const result: { thought?: Thought; toolCalls?: MetonaToolCall[] } = {};
|
||||
|
||||
const thoughtMatch = text.match(/Thought:\s*([\s\S]*?)(?=Action:|$)/i);
|
||||
if (thoughtMatch) {
|
||||
result.thought = {
|
||||
id: `thought-${iteration}`,
|
||||
content: thoughtMatch[1].trim(),
|
||||
timestamp: Date.now(),
|
||||
iteration,
|
||||
};
|
||||
}
|
||||
|
||||
const actionMatch = text.match(/Action:\s*(\w+)\s*\n\s*Action Input:\s*({[\s\S]*?})\s*(?=\n\n|Observation:|$)/i);
|
||||
if (actionMatch) {
|
||||
try {
|
||||
result.toolCalls = [{
|
||||
id: `call-${iteration}-${Date.now()}`,
|
||||
name: actionMatch[1].trim(),
|
||||
args: JSON.parse(actionMatch[2]),
|
||||
timestamp: Date.now(),
|
||||
iteration,
|
||||
}];
|
||||
} catch {
|
||||
// 参数 JSON 解析失败
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user