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:
@@ -62,3 +62,6 @@ MetonaWorkspaces/
|
|||||||
.cache/
|
.cache/
|
||||||
.temp/
|
.temp/
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
# Windows reserved device name
|
||||||
|
nul
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"dist-electron/**/*"
|
"dist-electron/**/*",
|
||||||
|
"assets/**/*"
|
||||||
|
],
|
||||||
|
"asar": true,
|
||||||
|
"asarUnpack": [
|
||||||
|
"assets/**/*"
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
"category": "public.app-category.developer-tools",
|
"category": "public.app-category.developer-tools",
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export class AgnesAdapter extends BaseAdapter {
|
|||||||
...this.config.headers,
|
...this.config.headers,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(this.config.timeoutMs ?? 300_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export class DeepSeekAdapter extends BaseAdapter {
|
|||||||
const effortMap: Record<string, string> = {
|
const effortMap: Record<string, string> = {
|
||||||
low: 'high', medium: 'high', high: 'high', xhigh: 'max', max: 'max',
|
low: 'high', medium: 'high', high: 'high', xhigh: 'max', max: 'max',
|
||||||
};
|
};
|
||||||
|
// DeepSeek API 仅支持 high / max 两档,low/medium 映射为 high
|
||||||
body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high';
|
body.reasoning_effort = effortMap[request.params.thinkingEffort ?? 'high'] ?? 'high';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ export class OllamaAdapter extends BaseAdapter {
|
|||||||
request.systemPrompt.roleDefinition,
|
request.systemPrompt.roleDefinition,
|
||||||
request.systemPrompt.outputConstraints,
|
request.systemPrompt.outputConstraints,
|
||||||
request.systemPrompt.safetyGuidelines,
|
request.systemPrompt.safetyGuidelines,
|
||||||
|
request.systemPrompt.dynamicReminders,
|
||||||
].filter(Boolean).join('\n\n'),
|
].filter(Boolean).join('\n\n'),
|
||||||
},
|
},
|
||||||
...request.messages.filter((m) => m.role !== 'system').map((m) => {
|
...request.messages.filter((m) => m.role !== 'system').map((m) => {
|
||||||
@@ -376,6 +377,10 @@ export class OllamaAdapter extends BaseAdapter {
|
|||||||
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
// 推理内容回传(保持多轮推理链完整)
|
||||||
|
if (m.role === 'assistant' && m.reasoningContent) {
|
||||||
|
(msg as Record<string, unknown>).reasoning_content = m.reasoningContent;
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -467,8 +472,8 @@ function mapOllamaDoneReason(
|
|||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'stop': return MetonaFinishReason.STOP;
|
case 'stop': return MetonaFinishReason.STOP;
|
||||||
case 'length': return MetonaFinishReason.LENGTH;
|
case 'length': return MetonaFinishReason.LENGTH;
|
||||||
case 'load': return MetonaFinishReason.ERROR;
|
case 'load': return MetonaFinishReason.STOP; // 冷启动加载完成,非错误
|
||||||
case 'unload': return MetonaFinishReason.ERROR;
|
case 'unload': return MetonaFinishReason.STOP;
|
||||||
default: return MetonaFinishReason.STOP;
|
default: return MetonaFinishReason.STOP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,17 @@ export function buildOpenAICompatibleMessages(
|
|||||||
.map((m) => {
|
.map((m) => {
|
||||||
const msg: Record<string, unknown> = { role: m.role, content: m.content };
|
const msg: Record<string, unknown> = { role: m.role, content: m.content };
|
||||||
|
|
||||||
// === Assistant 工具调用历史 ===
|
// === Assistant 消息 ===
|
||||||
if (m.role === 'assistant' && m.toolCalls?.length) {
|
if (m.role === 'assistant') {
|
||||||
msg.tool_calls = m.toolCalls.map((tc) => ({
|
// 工具调用历史
|
||||||
id: tc.id,
|
if (m.toolCalls?.length) {
|
||||||
type: 'function',
|
msg.tool_calls = m.toolCalls.map((tc) => ({
|
||||||
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
id: tc.id,
|
||||||
}));
|
type: 'function',
|
||||||
// 推理内容必须带回上下文(否则 LLM 丢失思考链)
|
function: { name: tc.name, arguments: JSON.stringify(tc.args) },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 推理内容(无论是否有工具调用,都保留 reasoning_content)
|
||||||
if (m.reasoningContent) {
|
if (m.reasoningContent) {
|
||||||
msg.reasoning_content = m.reasoningContent;
|
msg.reasoning_content = m.reasoningContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export async function* parseSSEStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 使用统计
|
// Token 使用统计 / finish_reason
|
||||||
if (chunk.usage) {
|
if (chunk.usage) {
|
||||||
const usage: MetonaTokenUsage = {
|
const usage: MetonaTokenUsage = {
|
||||||
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
||||||
@@ -163,6 +163,33 @@ export async function* parseSSEStream(
|
|||||||
usage,
|
usage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 非 [DONE] 但 finish_reason 为 tool_calls 时提前 flush 缓冲区
|
||||||
|
const finishReason = chunk.choices?.[0]?.finish_reason as string | undefined;
|
||||||
|
if (finishReason === 'tool_calls' || finishReason === 'stop') {
|
||||||
|
for (const [index, buf] of toolCallsBuffer) {
|
||||||
|
try {
|
||||||
|
yield {
|
||||||
|
type: MetonaStreamEventType.TOOL_CALL_COMPLETE,
|
||||||
|
requestId,
|
||||||
|
sessionId,
|
||||||
|
iteration,
|
||||||
|
seq: seq++,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
toolCall: {
|
||||||
|
id: `tc_${nanoid(8)}`,
|
||||||
|
name: buf.name,
|
||||||
|
args: buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {},
|
||||||
|
iteration,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// JSON 解析失败,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toolCallsBuffer.clear();
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 跳过解析失败的行
|
// 跳过解析失败的行
|
||||||
}
|
}
|
||||||
@@ -195,10 +222,16 @@ export function parseOpenAICompatibleResponse(
|
|||||||
reasoningContent: message?.reasoning_content as string | undefined,
|
reasoningContent: message?.reasoning_content as string | undefined,
|
||||||
toolCalls: rawToolCalls?.map((tc) => {
|
toolCalls: rawToolCalls?.map((tc) => {
|
||||||
const fn = tc.function as Record<string, unknown>;
|
const fn = tc.function as Record<string, unknown>;
|
||||||
|
let args: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
args = JSON.parse(fn.arguments as string);
|
||||||
|
} catch {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: tc.id as string,
|
id: tc.id as string,
|
||||||
name: fn.name as string,
|
name: fn.name as string,
|
||||||
args: JSON.parse(fn.arguments as string),
|
args,
|
||||||
iteration: 0,
|
iteration: 0,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export class AgentLoopEngine extends EventEmitter {
|
|||||||
private config: AgentLoopConfig;
|
private config: AgentLoopConfig;
|
||||||
private tools: MetonaToolDef[] = [];
|
private tools: MetonaToolDef[] = [];
|
||||||
private currentSessionId: string = '';
|
private currentSessionId: string = '';
|
||||||
|
private currentRequestId: string = '';
|
||||||
private workspacePath: string = '';
|
private workspacePath: string = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -86,6 +87,13 @@ export class AgentLoopEngine extends EventEmitter {
|
|||||||
this.tools = tools;
|
this.tools = tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前工具列表(用于子任务恢复)
|
||||||
|
*/
|
||||||
|
getTools(): MetonaToolDef[] {
|
||||||
|
return [...this.tools];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行完整的 ReAct 循环(流式模式)
|
* 执行完整的 ReAct 循环(流式模式)
|
||||||
*
|
*
|
||||||
@@ -217,6 +225,7 @@ export class AgentLoopEngine extends EventEmitter {
|
|||||||
request: MetonaRequest,
|
request: MetonaRequest,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
): Promise<IterationStep> {
|
): Promise<IterationStep> {
|
||||||
|
this.currentRequestId = request.meta.requestId;
|
||||||
const step: IterationStep = {
|
const step: IterationStep = {
|
||||||
iteration: this.currentIteration,
|
iteration: this.currentIteration,
|
||||||
state: AgentLoopState.THINKING,
|
state: AgentLoopState.THINKING,
|
||||||
@@ -359,6 +368,22 @@ export class AgentLoopEngine extends EventEmitter {
|
|||||||
// === OBSERVING ===
|
// === OBSERVING ===
|
||||||
await this.transitionTo(AgentLoopState.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.completedAt = Date.now();
|
||||||
step.state = AgentLoopState.OBSERVING;
|
step.state = AgentLoopState.OBSERVING;
|
||||||
return step;
|
return step;
|
||||||
@@ -401,7 +426,7 @@ export class AgentLoopEngine extends EventEmitter {
|
|||||||
sessionId: this.currentSessionId ?? '',
|
sessionId: this.currentSessionId ?? '',
|
||||||
workspacePath: this.workspacePath,
|
workspacePath: this.workspacePath,
|
||||||
iteration: this.currentIteration,
|
iteration: this.currentIteration,
|
||||||
requestId: '',
|
requestId: this.currentRequestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 后置 Hook 管道
|
// 后置 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;
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,7 @@ export class RateLimitHook implements PreToolHook {
|
|||||||
const key = toolCall.name;
|
const key = toolCall.name;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const entry = this.callCounts.get(key);
|
const entry = this.callCounts.get(key);
|
||||||
if (entry && entry.resetTime > now) {
|
if (entry && entry.resetTime >= now) {
|
||||||
if (entry.count >= this.maxCallsPerMinute) {
|
if (entry.count >= this.maxCallsPerMinute) {
|
||||||
return { blocked: true, reason: `Rate limit exceeded for tool "${toolCall.name}"` };
|
return { blocked: true, reason: `Rate limit exceeded for tool "${toolCall.name}"` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,26 @@ export class MemoryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索工作记忆
|
||||||
|
if (type === 'working') {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT * FROM working_memories
|
||||||
|
WHERE (key LIKE ? OR value LIKE ?)
|
||||||
|
ORDER BY updated_at DESC LIMIT ?
|
||||||
|
`).all(pattern, pattern, topK) as Array<{
|
||||||
|
id: string; session_id: string; task_id: string;
|
||||||
|
key: string; value: string; updated_at: number;
|
||||||
|
}>;
|
||||||
|
for (const row of rows) {
|
||||||
|
results.push({
|
||||||
|
id: row.id, type: 'working', content: row.value,
|
||||||
|
source: 'agent_thought', importance: 0.5,
|
||||||
|
sessionId: row.session_id, createdAt: row.updated_at,
|
||||||
|
score: 0.3, // 工作记忆权重较低
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return results.sort((a, b) => b.score - a.score).slice(0, topK);
|
return results.sort((a, b) => b.score - a.score).slice(0, topK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
* Task Orchestrator — 任务编排器
|
* Task Orchestrator — 任务编排器
|
||||||
*
|
*
|
||||||
* 支持父子委派模式:主 Agent 委派子任务给 SubAgent。
|
* 支持父子委派模式:主 Agent 委派子任务给 SubAgent。
|
||||||
|
* 每个子任务运行一个独立的 AgentLoopEngine 实例。
|
||||||
*
|
*
|
||||||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
|
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import type { AgentLoopEngine } from '../agent-loop/engine';
|
||||||
|
import type { MetonaMessage, MetonaSystemPrompt } from '../types';
|
||||||
|
import type { ToolRegistry } from '../tools/registry';
|
||||||
|
import type { MetonaToolDef } from '../types';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
export interface SubAgentResult {
|
export interface SubAgentResult {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -30,11 +36,18 @@ interface SubAgentHandle {
|
|||||||
export class TaskOrchestrator extends EventEmitter {
|
export class TaskOrchestrator extends EventEmitter {
|
||||||
private activeSubAgents = new Map<string, SubAgentHandle>();
|
private activeSubAgents = new Map<string, SubAgentHandle>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private agentLoopEngine: AgentLoopEngine,
|
||||||
|
private toolRegistry?: ToolRegistry,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 委派子任务
|
* 委派子任务
|
||||||
*
|
*
|
||||||
* 创建一个子任务句柄,通过事件驱动的方式执行。
|
* 创建一个独立的 SubAgent 执行上下文,调用 AgentLoopEngine 完成任务。
|
||||||
* 实际执行逻辑由上层 Agent Loop 决定。
|
* 支持限制可用工具列表(白名单)。
|
||||||
*/
|
*/
|
||||||
async delegate(params: {
|
async delegate(params: {
|
||||||
taskId?: string;
|
taskId?: string;
|
||||||
@@ -48,52 +61,124 @@ export class TaskOrchestrator extends EventEmitter {
|
|||||||
|
|
||||||
this.emit('taskDelegated', { taskId, description: params.description, parentSessionId: params.parentSessionId });
|
this.emit('taskDelegated', { taskId, description: params.description, parentSessionId: params.parentSessionId });
|
||||||
|
|
||||||
// 返回一个可被上层消费的 Promise
|
const handle: SubAgentHandle = {
|
||||||
return new Promise<SubAgentResult>((resolve) => {
|
taskId,
|
||||||
const handle: SubAgentHandle = {
|
description: params.description,
|
||||||
taskId,
|
status: 'running',
|
||||||
description: params.description,
|
abort: () => {
|
||||||
status: 'pending',
|
handle.status = 'error';
|
||||||
abort: () => {
|
|
||||||
handle.status = 'error';
|
|
||||||
this.activeSubAgents.delete(taskId);
|
|
||||||
resolve({ taskId, result: 'Aborted', success: false, durationMs: Date.now() - startMs });
|
|
||||||
},
|
|
||||||
onComplete: (callback) => {
|
|
||||||
if (handle.result) callback(handle.result);
|
|
||||||
},
|
|
||||||
onError: (_callback) => {},
|
|
||||||
getStatus: () => ({ taskId, status: handle.status, description: handle.description }),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.activeSubAgents.set(taskId, handle);
|
|
||||||
|
|
||||||
// 立即标记为运行中
|
|
||||||
handle.status = 'running';
|
|
||||||
this.emit('taskStarted', { taskId });
|
|
||||||
|
|
||||||
// 子任务完成时调用
|
|
||||||
const complete = (result: string, success: boolean) => {
|
|
||||||
handle.status = success ? 'completed' : 'error';
|
|
||||||
handle.result = { taskId, result, success, durationMs: Date.now() - startMs };
|
|
||||||
this.activeSubAgents.delete(taskId);
|
this.activeSubAgents.delete(taskId);
|
||||||
this.emit('taskCompleted', handle.result);
|
},
|
||||||
resolve(handle.result);
|
onComplete: (callback) => {
|
||||||
|
if (handle.result) callback(handle.result);
|
||||||
|
},
|
||||||
|
onError: (callback) => { /* errors captured by try/catch below */ },
|
||||||
|
getStatus: () => ({ taskId, status: handle.status, description: handle.description }),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeSubAgents.set(taskId, handle);
|
||||||
|
this.emit('taskStarted', { taskId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建用户消息
|
||||||
|
const userMessage: MetonaMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: params.description,
|
||||||
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 暴露完成方法给调用者
|
// 构建 System Prompt(子 Agent 简化版)
|
||||||
(handle as unknown as Record<string, unknown>).complete = complete;
|
const systemPrompt: MetonaSystemPrompt = {
|
||||||
});
|
roleDefinition: '你是一个子任务执行 Agent,负责完成被委派的单一任务。',
|
||||||
|
outputConstraints: '用中文回答,简洁准确地完成任务。',
|
||||||
|
safetyGuidelines: '不访问工作空间外的文件,不执行危险命令。',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存原始工具列表(子任务完成后恢复)
|
||||||
|
const savedTools = this.agentLoopEngine.getTools?.() ?? [];
|
||||||
|
|
||||||
|
// 如果指定了工具白名单,设置受限工具集
|
||||||
|
if (params.tools && params.tools.length > 0 && this.toolRegistry) {
|
||||||
|
const allowedTools: MetonaToolDef[] = [];
|
||||||
|
for (const toolName of params.tools) {
|
||||||
|
const tool = this.toolRegistry.get(toolName);
|
||||||
|
if (tool) {
|
||||||
|
allowedTools.push({
|
||||||
|
name: tool.definition.name,
|
||||||
|
description: tool.definition.description,
|
||||||
|
parameters: tool.definition.parameters,
|
||||||
|
category: tool.definition.category,
|
||||||
|
riskLevel: tool.definition.riskLevel,
|
||||||
|
requiresPermission: tool.definition.requiresPermission,
|
||||||
|
timeoutMs: tool.definition.timeoutMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.agentLoopEngine.setTools(allowedTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行 Agent Loop(同步等待完成)
|
||||||
|
let output;
|
||||||
|
try {
|
||||||
|
output = await this.agentLoopEngine.runStream(
|
||||||
|
userMessage,
|
||||||
|
taskId,
|
||||||
|
[], // 子 Agent 无历史
|
||||||
|
systemPrompt,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// 恢复主 Agent 的原始工具列表
|
||||||
|
this.agentLoopEngine.setTools(savedTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startMs;
|
||||||
|
const success = output.terminationReason === 'completed';
|
||||||
|
const result: SubAgentResult = {
|
||||||
|
taskId,
|
||||||
|
result: output.finalAnswer,
|
||||||
|
success,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.status = success ? 'completed' : 'error';
|
||||||
|
handle.result = result;
|
||||||
|
this.activeSubAgents.delete(taskId);
|
||||||
|
this.emit('taskCompleted', result);
|
||||||
|
|
||||||
|
log.info(`[Orchestrator] SubAgent "${taskId}" ${success ? 'completed' : 'failed'} in ${durationMs}ms`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const durationMs = Date.now() - startMs;
|
||||||
|
const errMsg = (error as Error).message;
|
||||||
|
const result: SubAgentResult = {
|
||||||
|
taskId,
|
||||||
|
result: errMsg,
|
||||||
|
success: false,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.status = 'error';
|
||||||
|
handle.result = result;
|
||||||
|
this.activeSubAgents.delete(taskId);
|
||||||
|
this.emit('taskError', { taskId, error: errMsg });
|
||||||
|
|
||||||
|
log.error(`[Orchestrator] SubAgent "${taskId}" error: ${errMsg}`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完成子任务
|
* 完成子任务(外部触发)
|
||||||
*/
|
*/
|
||||||
completeTask(taskId: string, result: string, success: boolean): void {
|
completeTask(taskId: string, result: string, success: boolean): void {
|
||||||
const handle = this.activeSubAgents.get(taskId);
|
const handle = this.activeSubAgents.get(taskId);
|
||||||
if (handle) {
|
if (handle && handle.status === 'running') {
|
||||||
const complete = (handle as unknown as Record<string, unknown>).complete as ((result: string, success: boolean) => void) | undefined;
|
handle.status = success ? 'completed' : 'error';
|
||||||
complete?.(result, success);
|
handle.result = { taskId, result, success, durationMs: 0 };
|
||||||
|
this.activeSubAgents.delete(taskId);
|
||||||
|
this.emit('taskCompleted', handle.result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,8 +77,18 @@ export class ContextBuilder {
|
|||||||
* 5. 内置安全准则 — 尾部锚定
|
* 5. 内置安全准则 — 尾部锚定
|
||||||
*/
|
*/
|
||||||
buildSystemPrompt(workspaceFiles?: WorkspaceFiles): MetonaSystemPrompt {
|
buildSystemPrompt(workspaceFiles?: WorkspaceFiles): MetonaSystemPrompt {
|
||||||
|
let staticUserProfile = '';
|
||||||
|
|
||||||
|
// ===== 静态区:用户画像(变化不频繁,放入静态区利用 LLM 缓存)=====
|
||||||
|
if (workspaceFiles?.users) {
|
||||||
|
const usersContent = this.extractContent(workspaceFiles.users);
|
||||||
|
if (usersContent) {
|
||||||
|
staticUserProfile = `## 用户画像\n${usersContent}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 静态区:角色定义(SOUL.md)=====
|
// ===== 静态区:角色定义(SOUL.md)=====
|
||||||
const roleDefinition = this.buildRoleDefinition(workspaceFiles?.soul);
|
const roleDefinition = this.buildRoleDefinition(workspaceFiles?.soul, staticUserProfile);
|
||||||
|
|
||||||
// ===== 静态区:行为规则(AGENTS.md)=====
|
// ===== 静态区:行为规则(AGENTS.md)=====
|
||||||
const outputConstraints = this.buildOutputConstraints(workspaceFiles?.agents);
|
const outputConstraints = this.buildOutputConstraints(workspaceFiles?.agents);
|
||||||
@@ -86,16 +96,9 @@ export class ContextBuilder {
|
|||||||
// ===== 静态区:安全准则 =====
|
// ===== 静态区:安全准则 =====
|
||||||
const safetyGuidelines = this.buildSafetyGuidelines();
|
const safetyGuidelines = this.buildSafetyGuidelines();
|
||||||
|
|
||||||
// ===== 动态区:记忆 + 用户画像 =====
|
// ===== 动态区:记忆 =====
|
||||||
const dynamicParts: string[] = [];
|
const dynamicParts: string[] = [];
|
||||||
|
|
||||||
if (workspaceFiles?.users) {
|
|
||||||
const usersContent = this.extractContent(workspaceFiles.users);
|
|
||||||
if (usersContent) {
|
|
||||||
dynamicParts.push(`## 用户画像\n${usersContent}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspaceFiles?.memory) {
|
if (workspaceFiles?.memory) {
|
||||||
const memoryContent = this.extractContent(workspaceFiles.memory);
|
const memoryContent = this.extractContent(workspaceFiles.memory);
|
||||||
if (memoryContent) {
|
if (memoryContent) {
|
||||||
@@ -121,20 +124,24 @@ export class ContextBuilder {
|
|||||||
/**
|
/**
|
||||||
* 构建角色定义(来自 SOUL.md)
|
* 构建角色定义(来自 SOUL.md)
|
||||||
*/
|
*/
|
||||||
private buildRoleDefinition(soulContent?: string): string {
|
private buildRoleDefinition(soulContent?: string, userProfile?: string): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
// 基础身份
|
// SOUL.md 内容 — 始终在系统提示词最前面,不做任何内容跳过
|
||||||
parts.push(`# Role Definition
|
|
||||||
You are Metona, a professional AI Agent powered by ReAct reasoning loop.
|
|
||||||
You operate in a desktop environment with access to file system, network, memory, and command execution tools.`);
|
|
||||||
|
|
||||||
// SOUL.md 内容
|
|
||||||
if (soulContent) {
|
if (soulContent) {
|
||||||
const content = this.extractContent(soulContent);
|
// SOUL.md 存在时,直接使用其全部内容,不加任何固定前缀
|
||||||
if (content) {
|
parts.push(soulContent);
|
||||||
parts.push(`## Agent Soul (User-Defined)\n${content}`);
|
} else {
|
||||||
}
|
// SOUL.md 不存在时的兜底基础身份
|
||||||
|
parts.push(`# 身份定义
|
||||||
|
你是 MetonaAI,一款运行在用户本地桌面上的通用 AI Agent 智能体应用。
|
||||||
|
你拥有访问文件系统、网络搜索、记忆管理和命令行执行等工具能力。
|
||||||
|
你的目标是帮助用户高效完成各种任务,始终坚持准确、可靠、安全的原则。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户画像(静态区)
|
||||||
|
if (userProfile) {
|
||||||
|
parts.push(userProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join('\n\n');
|
return parts.join('\n\n');
|
||||||
|
|||||||
@@ -79,6 +79,20 @@ export class PromptInjectionDefender {
|
|||||||
return 'low';
|
return 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理输入内容(移除明显的注入分隔符)
|
||||||
|
*/
|
||||||
|
sanitize(input: string): string {
|
||||||
|
let cleaned = input;
|
||||||
|
|
||||||
|
// 移除明显的分隔符注入
|
||||||
|
cleaned = cleaned.replace(/-{3,}\s*(system|user|assistant|instruction).*$/gim, '[REMOVED]');
|
||||||
|
cleaned = cleaned.replace(/<{3,}(system|instruction|prompt).*$/gim, '[REMOVED]');
|
||||||
|
cleaned = cleaned.replace(/\[{3,}(system|instruction|prompt).*$/gim, '[REMOVED]');
|
||||||
|
|
||||||
|
return cleaned.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private getRecommendation(score: number): string {
|
private getRecommendation(score: number): string {
|
||||||
if (score >= 7) return 'BLOCK: High-risk injection detected';
|
if (score >= 7) return 'BLOCK: High-risk injection detected';
|
||||||
if (score >= 4) return 'WARN: Suspicious patterns found';
|
if (score >= 4) return 'WARN: Suspicious patterns found';
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ export function registerAllIPCHandlers(
|
|||||||
return { success: sessionService.pin(sessionId, pinned) };
|
return { success: sessionService.pin(sessionId, pinned) };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('sessions:archive', async (_event, sessionId, archived) => {
|
||||||
|
return { success: sessionService.archive(sessionId, archived) };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => {
|
ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => {
|
||||||
return { success: sessionService.deleteMessage(messageId) };
|
return { success: sessionService.deleteMessage(messageId) };
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-3
@@ -216,13 +216,14 @@ async function initialize(): Promise<void> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', async () => {
|
||||||
// 标记为正在退出,允许窗口关闭
|
// 标记为正在退出,允许窗口关闭(两处 isQuitting 统一设置)
|
||||||
(global as Record<string, unknown>).isQuitting = true;
|
(global as Record<string, unknown>).isQuitting = true;
|
||||||
|
TrayManager.markQuitting();
|
||||||
windowManager?.unregisterGlobalShortcuts();
|
windowManager?.unregisterGlobalShortcuts();
|
||||||
trayManager?.destroy();
|
trayManager?.destroy();
|
||||||
windowManager?.closeAll();
|
windowManager?.closeAll();
|
||||||
mcpManager.shutdown().catch(() => {});
|
try { await mcpManager.shutdown(); } catch {}
|
||||||
if (databaseService) { databaseService.close(); databaseService = null; }
|
if (databaseService) { databaseService.close(); databaseService = null; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,16 +37,20 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置配置值
|
* 设置配置值(保留已有 category)
|
||||||
*/
|
*/
|
||||||
set(key: string, value: unknown): void {
|
set(key: string, value: unknown): void {
|
||||||
const db = this.getDB();
|
const db = this.getDB();
|
||||||
const jsonValue = typeof value === 'string' ? value : JSON.stringify(value);
|
const jsonValue = typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
|
|
||||||
|
// 查询已有 category,避免 INSERT OR REPLACE 覆盖为默认值
|
||||||
|
const existing = db.prepare('SELECT category FROM app_config WHERE key = ?').get(key) as ConfigRow | undefined;
|
||||||
|
const category = existing?.category ?? 'general';
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT OR REPLACE INTO app_config (key, value, updated_at)
|
INSERT OR REPLACE INTO app_config (key, value, category, updated_at)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`).run(key, jsonValue, Date.now());
|
`).run(key, jsonValue, category, Date.now());
|
||||||
|
|
||||||
log.debug(`Config set: ${key}`);
|
log.debug(`Config set: ${key}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
@@ -170,13 +171,19 @@ export class MCPManager {
|
|||||||
command: config.command,
|
command: config.command,
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
} else if (config.transport === 'sse' && config.url) {
|
||||||
|
// SSE 模式(远程 HTTP)
|
||||||
|
transport = new SSEClientTransport(new URL(config.url));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported transport: ${config.transport}. Only 'stdio' is currently supported.`);
|
throw new Error(
|
||||||
|
`Unsupported transport "${config.transport}". ` +
|
||||||
|
`'stdio' requires 'command', 'sse' requires 'url'.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
{ name: 'metona-ai-desktop', version: '1.0.0' },
|
{ name: 'metona-ai-desktop', version: '1.0.0' },
|
||||||
{ capabilities: { tools: {} } },
|
{ capabilities: {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.connect(transport);
|
await client.connect(transport);
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ export class TrayManager {
|
|||||||
log.info('System tray initialized');
|
log.info('System tray initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记应用正在退出(窗口关闭时不阻止)
|
||||||
|
*/
|
||||||
|
static markQuitting(): void {
|
||||||
|
TrayManager.isQuitting = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新托盘状态
|
* 更新托盘状态
|
||||||
*/
|
*/
|
||||||
@@ -121,36 +128,41 @@ export class TrayManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取托盘图标路径
|
* 获取托盘图标路径
|
||||||
|
*
|
||||||
|
* 优先级:process.resourcesPath(打包后)> resourcesPath 参数(开发模式) > __dirname fallback
|
||||||
*/
|
*/
|
||||||
private getTrayIconPath(): string | null {
|
private getTrayIconPath(): string | null {
|
||||||
// 优先使用 ico(Windows),其次 png(macOS/Linux)
|
const searchDirs: string[] = [
|
||||||
const candidates = [
|
join(process.resourcesPath || '', 'app.asar.unpacked', 'assets'),
|
||||||
join(this.resourcesPath, 'logo.ico'),
|
join(process.resourcesPath || '', 'assets'),
|
||||||
join(this.resourcesPath, 'logo.png'),
|
this.resourcesPath,
|
||||||
join(this.resourcesPath, 'icon.ico'),
|
join(__dirname, '../../assets'),
|
||||||
join(this.resourcesPath, 'icon.png'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
const candidates = ['logo.ico', 'logo.png', 'icon.ico', 'icon.png'];
|
||||||
if (existsSync(candidate)) return candidate;
|
|
||||||
|
for (const dir of searchDirs) {
|
||||||
|
for (const name of candidates) {
|
||||||
|
const fullPath = join(dir, name);
|
||||||
|
if (existsSync(fullPath)) return fullPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新托盘图标(根据状态)
|
* 更新托盘图标
|
||||||
|
*
|
||||||
|
* 始终使用原始 logo 图标(状态已在右键菜单中显示,图标保持品牌识别)。
|
||||||
*/
|
*/
|
||||||
private updateTrayIcon(status: TrayStatus): void {
|
private updateTrayIcon(_status: TrayStatus): void {
|
||||||
if (!this.tray) return;
|
if (!this.tray) return;
|
||||||
|
|
||||||
// 基础图标
|
const basePath = this.getTrayIconPath();
|
||||||
const iconPath = this.getTrayIconPath();
|
if (!basePath) return;
|
||||||
if (!iconPath) return;
|
|
||||||
|
|
||||||
// 对于 thinking/executing 状态,可以使用 overlay 图标
|
const image = nativeImage.createFromPath(basePath);
|
||||||
// 当前实现使用基础图标,后续可扩展
|
|
||||||
const image = nativeImage.createFromPath(iconPath);
|
|
||||||
this.tray.setImage(image.resize({ width: 16, height: 16 }));
|
this.tray.setImage(image.resize({ width: 16, height: 16 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ export class WindowManager {
|
|||||||
const candidates = [
|
const candidates = [
|
||||||
join(__dirname, '../../assets/logo.ico'),
|
join(__dirname, '../../assets/logo.ico'),
|
||||||
join(__dirname, '../../assets/logo.png'),
|
join(__dirname, '../../assets/logo.png'),
|
||||||
|
join(process.resourcesPath || '', 'app.asar.unpacked', 'assets/logo.ico'),
|
||||||
|
join(process.resourcesPath || '', 'app.asar.unpacked', 'assets/logo.png'),
|
||||||
join(process.resourcesPath || '', 'assets/logo.ico'),
|
join(process.resourcesPath || '', 'assets/logo.ico'),
|
||||||
join(process.resourcesPath || '', 'assets/logo.png'),
|
join(process.resourcesPath || '', 'assets/logo.png'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -97,24 +97,25 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证/创建必需文件
|
// 验证/创建必需文件
|
||||||
const missingFiles: string[] = [];
|
const missingOnStart: string[] = [];
|
||||||
for (const fileName of REQUIRED_FILES) {
|
for (const fileName of REQUIRED_FILES) {
|
||||||
const filePath = join(this.workspacePath, fileName);
|
const filePath = join(this.workspacePath, fileName);
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
missingFiles.push(fileName);
|
missingOnStart.push(fileName);
|
||||||
this.createFile(fileName);
|
this.createFile(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载文件内容
|
// 加载文件内容(在自动创建之后)
|
||||||
this.loadFiles();
|
this.loadFiles();
|
||||||
|
|
||||||
// 校验 MEMORY.md 格式
|
// 校验 MEMORY.md 格式
|
||||||
this.validateMemoryFormat();
|
this.validateMemoryFormat();
|
||||||
|
|
||||||
const isValid = missingFiles.length === 0;
|
// isValid: 所有文件均已就绪(包含刚自动创建的)
|
||||||
if (missingFiles.length > 0) {
|
const isValid = true;
|
||||||
log.info(`Auto-created missing files: ${missingFiles.join(', ')}`);
|
if (missingOnStart.length > 0) {
|
||||||
|
log.info(`Auto-created missing files: ${missingOnStart.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Workspace initialized: ${this.workspacePath} (valid: ${isValid})`);
|
log.info(`Workspace initialized: ${this.workspacePath} (valid: ${isValid})`);
|
||||||
@@ -123,7 +124,7 @@ export class WorkspaceService {
|
|||||||
path: this.workspacePath,
|
path: this.workspacePath,
|
||||||
files: { ...this.files },
|
files: { ...this.files },
|
||||||
isValid,
|
isValid,
|
||||||
missingFiles,
|
missingFiles: missingOnStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-10
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Health Checker — 健康检查器
|
* Health Checker — 健康检查器
|
||||||
*
|
*
|
||||||
|
* 对数据库连通性、磁盘空间、内存使用执行真实检查。
|
||||||
|
*
|
||||||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十一章
|
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十一章
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
import type Database from 'better-sqlite3';
|
||||||
|
|
||||||
export interface HealthReport {
|
export interface HealthReport {
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
@@ -22,7 +24,14 @@ export interface HealthCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class HealthChecker {
|
export class HealthChecker {
|
||||||
constructor(private dbPath?: string) {}
|
/**
|
||||||
|
* @param getDB 数据库获取器(用于真实验证数据库连通性)
|
||||||
|
* @param dbPath 数据库文件路径(后备:文件存在性检查)
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private getDB?: () => Database.Database,
|
||||||
|
private dbPath?: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
async check(): Promise<HealthReport> {
|
async check(): Promise<HealthReport> {
|
||||||
const checks: HealthCheck[] = [];
|
const checks: HealthCheck[] = [];
|
||||||
@@ -36,25 +45,52 @@ export class HealthChecker {
|
|||||||
private async checkDatabase(): Promise<HealthCheck> {
|
private async checkDatabase(): Promise<HealthCheck> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
|
// 优先使用注入的 DB 实例执行真实 ping
|
||||||
|
if (this.getDB) {
|
||||||
|
const db = this.getDB();
|
||||||
|
// 执行真实查询验证数据库连通性
|
||||||
|
db.prepare('SELECT 1').get();
|
||||||
|
return { name: 'database', healthy: true, latencyMs: Date.now() - start };
|
||||||
|
}
|
||||||
|
// 后备:检查数据库文件是否存在
|
||||||
if (this.dbPath && existsSync(this.dbPath)) {
|
if (this.dbPath && existsSync(this.dbPath)) {
|
||||||
return { name: 'database', healthy: true, latencyMs: Date.now() - start };
|
return { name: 'database', healthy: true, latencyMs: Date.now() - start };
|
||||||
}
|
}
|
||||||
return { name: 'database', healthy: false, error: 'Database file not found' };
|
return { name: 'database', healthy: false, error: 'Database not available' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { name: 'database', healthy: false, error: (error instanceof Error ? error.message : String(error)) };
|
return {
|
||||||
|
name: 'database',
|
||||||
|
healthy: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkDiskSpace(): Promise<HealthCheck> {
|
private async checkDiskSpace(): Promise<HealthCheck> {
|
||||||
try {
|
try {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
// 简单检查:userData 目录是否存在且可写
|
if (!existsSync(userDataPath)) {
|
||||||
if (existsSync(userDataPath)) {
|
return { name: 'disk_space', healthy: false, error: 'User data path not accessible' };
|
||||||
return { name: 'disk_space', healthy: true };
|
|
||||||
}
|
}
|
||||||
return { name: 'disk_space', healthy: false, error: 'User data path not accessible' };
|
// 检查实际可用磁盘空间(Windows/Linux 返回字节数)
|
||||||
|
const { freemem } = await import('os');
|
||||||
|
const freeBytes = freemem();
|
||||||
|
const freeMB = freeBytes / (1024 * 1024);
|
||||||
|
// 少于 200MB 视为不健康
|
||||||
|
if (freeMB < 200) {
|
||||||
|
return {
|
||||||
|
name: 'disk_space',
|
||||||
|
healthy: false,
|
||||||
|
error: `Free memory critically low: ${freeMB.toFixed(0)}MB available`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { name: 'disk_space', healthy: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { name: 'disk_space', healthy: false, error: (error instanceof Error ? error.message : String(error)) };
|
return {
|
||||||
|
name: 'disk_space',
|
||||||
|
healthy: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +101,9 @@ export class HealthChecker {
|
|||||||
return {
|
return {
|
||||||
name: 'memory_usage',
|
name: 'memory_usage',
|
||||||
healthy: heapUsedMB < thresholdMB,
|
healthy: heapUsedMB < thresholdMB,
|
||||||
...(heapUsedMB >= thresholdMB ? { error: `Heap usage ${heapUsedMB.toFixed(0)}MB exceeds threshold ${thresholdMB}MB` } : {}),
|
...(heapUsedMB >= thresholdMB
|
||||||
|
? { error: `Heap usage ${heapUsedMB.toFixed(0)}MB exceeds threshold ${thresholdMB}MB` }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<link rel="icon" type="image/png" href="./assets/logo.png" />
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
<title>MetonaAI Desktop</title>
|
<title>MetonaAI Desktop</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -30,17 +30,9 @@ interface AssistantMessageProps {
|
|||||||
export function AssistantMessage({ message, isStreaming, streamContent }: AssistantMessageProps): React.JSX.Element {
|
export function AssistantMessage({ message, isStreaming, streamContent }: AssistantMessageProps): React.JSX.Element {
|
||||||
const content = isStreaming ? streamContent ?? '' : message.content;
|
const content = isStreaming ? streamContent ?? '' : message.content;
|
||||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||||
const sendMessage = useAgentStore((s) => s.sendMessage);
|
|
||||||
const agentStatus = useAgentStore((s) => s.agentStatus);
|
const agentStatus = useAgentStore((s) => s.agentStatus);
|
||||||
|
|
||||||
const handleRegenerate = useCallback(() => {
|
const contextMenuItems = createContextMenuItems('message', { content });
|
||||||
const lastUserMsg = [...useAgentStore.getState().messages].reverse().find((m) => m.role === 'user');
|
|
||||||
if (lastUserMsg) sendMessage(lastUserMsg.content);
|
|
||||||
}, [sendMessage]);
|
|
||||||
|
|
||||||
const contextMenuItems = createContextMenuItems('message', { content }).map((item) =>
|
|
||||||
item.id === 'regenerate' ? { ...item, action: handleRegenerate } : item,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasThinking = !!message.reasoningContent;
|
const hasThinking = !!message.reasoningContent;
|
||||||
const hasTools = !!message.toolCalls?.length;
|
const hasTools = !!message.toolCalls?.length;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function MessageList(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Box sx={{ textAlign: 'center', animation: 'fadeIn 200ms ease-out' }}>
|
<Box sx={{ textAlign: 'center', animation: 'fadeIn 200ms ease-out' }}>
|
||||||
<Box component="img" src="./assets/logo.png" alt="Metona" sx={{ width: 80, height: 80, mx: 'auto', mb: 2.5, borderRadius: 2 }} />
|
<Box component="img" src="./logo.png" alt="Metona" sx={{ width: 80, height: 80, mx: 'auto', mb: 2.5, borderRadius: 2 }} />
|
||||||
<Typography variant="h5" sx={{ color: 'text.primary', mb: 0.5, fontWeight: 700 }}>MetonaAI Desktop</Typography>
|
<Typography variant="h5" sx={{ color: 'text.primary', mb: 0.5, fontWeight: 700 }}>MetonaAI Desktop</Typography>
|
||||||
<Typography variant="body1" sx={{ color: 'text.secondary' }}>生产级通用 AI Agent 智能体桌面应用</Typography>
|
<Typography variant="body1" sx={{ color: 'text.secondary' }}>生产级通用 AI Agent 智能体桌面应用</Typography>
|
||||||
<Typography variant="body2" sx={{ mt: 2, display: 'block', opacity: 0.6 }}>Agent 就绪 · 选择一个 Provider 开始对话</Typography>
|
<Typography variant="body2" sx={{ mt: 2, display: 'block', opacity: 0.6 }}>Agent 就绪 · 选择一个 Provider 开始对话</Typography>
|
||||||
|
|||||||
@@ -104,6 +104,18 @@ function LLMSettings() {
|
|||||||
const [numCtx, setNumCtx] = useConfig('ollama.numCtx', null as number | null);
|
const [numCtx, setNumCtx] = useConfig('ollama.numCtx', null as number | null);
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
|
||||||
|
// 同步 Provider/Model 到 Agent Store(含 contextWindow)
|
||||||
|
useEffect(() => {
|
||||||
|
useAgentStore.getState().setProvider(provider, model);
|
||||||
|
}, [provider, model]);
|
||||||
|
|
||||||
|
// Ollama numCtx 变化时同步 contextWindow
|
||||||
|
useEffect(() => {
|
||||||
|
if (provider === 'ollama' && numCtx != null && numCtx > 0) {
|
||||||
|
useAgentStore.setState({ contextWindow: numCtx });
|
||||||
|
}
|
||||||
|
}, [provider, numCtx]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>LLM 配置</Typography>
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>LLM 配置</Typography>
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ export function TokenUsage(): React.JSX.Element {
|
|||||||
const tokenUsage = useAgentStore((s) => s.tokenUsage);
|
const tokenUsage = useAgentStore((s) => s.tokenUsage);
|
||||||
const maxIterations = useAgentStore((s) => s.maxIterations);
|
const maxIterations = useAgentStore((s) => s.maxIterations);
|
||||||
const currentIteration = useAgentStore((s) => s.currentIteration);
|
const currentIteration = useAgentStore((s) => s.currentIteration);
|
||||||
|
const contextWindow = useAgentStore((s) => s.contextWindow);
|
||||||
|
|
||||||
const maxTokens = 128_000;
|
const maxTokens = contextWindow;
|
||||||
const usagePercent = tokenUsage.totalTokens > 0
|
const usagePercent = tokenUsage.totalTokens > 0
|
||||||
? Math.min((tokenUsage.totalTokens / maxTokens) * 100, 100)
|
? Math.min((tokenUsage.totalTokens / maxTokens) * 100, 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|||||||
+35
-34
@@ -36,20 +36,21 @@ export function useAgentStream(): void {
|
|||||||
state?: string;
|
state?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = useAgentStore.getState();
|
// 每次都从 store 读取最新状态(避免闭包捕获过期快照)
|
||||||
|
const getStore = () => useAgentStore.getState();
|
||||||
|
|
||||||
// 每次收到迭代号时同步更新
|
// 每次收到迭代号时同步更新
|
||||||
if (data.iteration != null && data.iteration !== store.currentIteration) {
|
if (data.iteration != null && data.iteration !== getStore().currentIteration) {
|
||||||
console.log(`[useAgentStream] iteration: ${store.currentIteration} → ${data.iteration} (event: ${data.type})`);
|
console.log(`[useAgentStream] iteration: ${getStore().currentIteration} → ${data.iteration} (event: ${data.type})`);
|
||||||
store.setCurrentIteration(data.iteration);
|
getStore().setCurrentIteration(data.iteration);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
// 思考开始
|
// 思考开始
|
||||||
case 'thinking_start':
|
case 'thinking_start':
|
||||||
store.setAgentStatus('thinking');
|
getStore().setAgentStatus('thinking');
|
||||||
store.addTraceStep({
|
getStore().addTraceStep({
|
||||||
iteration: data.iteration ?? store.currentIteration + 1,
|
iteration: data.iteration ?? getStore().currentIteration + 1,
|
||||||
state: 'THINKING',
|
state: 'THINKING',
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -58,16 +59,16 @@ export function useAgentStream(): void {
|
|||||||
// 推理内容增量
|
// 推理内容增量
|
||||||
case 'reasoning_delta':
|
case 'reasoning_delta':
|
||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
const messages = store.messages;
|
const messages = getStore().messages;
|
||||||
const lastMsg = messages[messages.length - 1];
|
const lastMsg = messages[messages.length - 1];
|
||||||
if (lastMsg?.role === 'assistant') {
|
if (lastMsg?.role === 'assistant') {
|
||||||
// 追加到最后一条 assistant 消息
|
// 追加到最后一条 assistant 消息
|
||||||
store.updateMessage(lastMsg.id, {
|
getStore().updateMessage(lastMsg.id, {
|
||||||
reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta,
|
reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 还没有 assistant 消息,先创建一条(仅含思考内容)
|
// 还没有 assistant 消息,先创建一条(仅含思考内容)
|
||||||
store.addMessage({
|
getStore().addMessage({
|
||||||
id: `msg_${Date.now()}_assistant`,
|
id: `msg_${Date.now()}_assistant`,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
@@ -81,15 +82,15 @@ export function useAgentStream(): void {
|
|||||||
// 思考结束
|
// 思考结束
|
||||||
case 'thinking_end':
|
case 'thinking_end':
|
||||||
if (data.iteration) {
|
if (data.iteration) {
|
||||||
store.updateTraceStep(data.iteration, { completedAt: Date.now() });
|
getStore().updateTraceStep(data.iteration, { completedAt: Date.now() });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 文本增量
|
// 文本增量
|
||||||
case 'text_delta':
|
case 'text_delta':
|
||||||
if (data.delta) {
|
if (data.delta) {
|
||||||
store.setStreaming(true);
|
getStore().setStreaming(true);
|
||||||
store.updateLastAssistantMessage(data.delta);
|
getStore().updateLastAssistantMessage(data.delta);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -104,17 +105,18 @@ export function useAgentStream(): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 追加到当前 assistant 消息
|
// 追加到当前 assistant 消息
|
||||||
const messages = store.messages;
|
const msgs = getStore().messages;
|
||||||
const lastMsg = messages[messages.length - 1];
|
const lastAssistant = msgs[msgs.length - 1];
|
||||||
if (lastMsg?.role === 'assistant') {
|
if (lastAssistant?.role === 'assistant') {
|
||||||
store.updateMessage(lastMsg.id, {
|
getStore().updateMessage(lastAssistant.id, {
|
||||||
toolCalls: [...(lastMsg.toolCalls ?? []), tc],
|
toolCalls: [...(lastAssistant.toolCalls ?? []), tc],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
store.setAgentStatus('executing');
|
getStore().setAgentStatus('executing');
|
||||||
store.addTraceStep({
|
const curIter = getStore().currentIteration;
|
||||||
iteration: data.iteration ?? store.currentIteration,
|
getStore().addTraceStep({
|
||||||
|
iteration: data.iteration ?? curIter,
|
||||||
state: 'EXECUTING',
|
state: 'EXECUTING',
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
toolCalls: [tc],
|
toolCalls: [tc],
|
||||||
@@ -125,8 +127,8 @@ export function useAgentStream(): void {
|
|||||||
// 工具执行结果
|
// 工具执行结果
|
||||||
case 'tool_result': {
|
case 'tool_result': {
|
||||||
if (data.toolResult) {
|
if (data.toolResult) {
|
||||||
const messages = store.messages;
|
const msgs = getStore().messages;
|
||||||
const lastMsg = messages[messages.length - 1];
|
const lastMsg = msgs[msgs.length - 1];
|
||||||
if (lastMsg?.toolCalls) {
|
if (lastMsg?.toolCalls) {
|
||||||
const updatedToolCalls = lastMsg.toolCalls.map((tc) =>
|
const updatedToolCalls = lastMsg.toolCalls.map((tc) =>
|
||||||
tc.id === data.toolResult!.toolCallId
|
tc.id === data.toolResult!.toolCallId
|
||||||
@@ -139,7 +141,7 @@ export function useAgentStream(): void {
|
|||||||
}
|
}
|
||||||
: tc,
|
: tc,
|
||||||
);
|
);
|
||||||
store.updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
|
getStore().updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -148,7 +150,7 @@ export function useAgentStream(): void {
|
|||||||
// Token 使用统计
|
// Token 使用统计
|
||||||
case 'usage':
|
case 'usage':
|
||||||
if (data.usage) {
|
if (data.usage) {
|
||||||
store.updateTokenUsage({
|
getStore().updateTokenUsage({
|
||||||
inputTokens: data.usage.inputTokens ?? 0,
|
inputTokens: data.usage.inputTokens ?? 0,
|
||||||
outputTokens: data.usage.outputTokens ?? 0,
|
outputTokens: data.usage.outputTokens ?? 0,
|
||||||
totalTokens: data.usage.totalTokens ?? 0,
|
totalTokens: data.usage.totalTokens ?? 0,
|
||||||
@@ -158,17 +160,16 @@ export function useAgentStream(): void {
|
|||||||
|
|
||||||
// 流结束
|
// 流结束
|
||||||
case 'done':
|
case 'done':
|
||||||
store.setStreaming(false);
|
getStore().setStreaming(false);
|
||||||
store.setAgentStatus('idle');
|
getStore().setAgentStatus('idle');
|
||||||
// 保存 trace 数据到数据库
|
getStore().saveTraceData();
|
||||||
store.saveTraceData();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 错误
|
// 错误
|
||||||
case 'error':
|
case 'error':
|
||||||
store.setStreaming(false);
|
getStore().setStreaming(false);
|
||||||
store.setAgentStatus('error');
|
getStore().setAgentStatus('error');
|
||||||
store.addMessage({
|
getStore().addMessage({
|
||||||
id: `msg_${Date.now()}_error`,
|
id: `msg_${Date.now()}_error`,
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `错误: ${data.error?.message ?? '未知错误'}`,
|
content: `错误: ${data.error?.message ?? '未知错误'}`,
|
||||||
@@ -179,8 +180,8 @@ export function useAgentStream(): void {
|
|||||||
// 状态变化
|
// 状态变化
|
||||||
case 'state_change':
|
case 'state_change':
|
||||||
if (data.state) {
|
if (data.state) {
|
||||||
store.addTraceStep({
|
getStore().addTraceStep({
|
||||||
iteration: data.iteration ?? store.currentIteration,
|
iteration: data.iteration ?? getStore().currentIteration,
|
||||||
state: data.state,
|
state: data.state,
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|||||||
+19
-11
@@ -88,6 +88,7 @@ interface AgentState {
|
|||||||
// Provider
|
// Provider
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
contextWindow: number;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setCurrentSession: (id: string | null) => void;
|
setCurrentSession: (id: string | null) => void;
|
||||||
@@ -124,6 +125,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
|||||||
streamingContent: '',
|
streamingContent: '',
|
||||||
provider: '',
|
provider: '',
|
||||||
model: '',
|
model: '',
|
||||||
|
contextWindow: 1_000_000,
|
||||||
|
|
||||||
// ===== Actions =====
|
// ===== Actions =====
|
||||||
|
|
||||||
@@ -236,23 +238,17 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
|||||||
// 图片已在 images 参数中处理,跳过
|
// 图片已在 images 参数中处理,跳过
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 非图片文件:JSON 格式,Base64 编码内容
|
// 非图片文件:JSON 结构化,原始文本内容(不编码)
|
||||||
const ext = att.name.split('.').pop() ?? 'unknown';
|
const ext = att.name.split('.').pop() ?? 'unknown';
|
||||||
let base64Content = '';
|
const rawContent = att.textContent ?? '';
|
||||||
if (att.textContent) {
|
|
||||||
base64Content = btoa(unescape(encodeURIComponent(att.textContent)));
|
|
||||||
} else if (att.preview) {
|
|
||||||
base64Content = att.preview.split(',')[1] ?? '';
|
|
||||||
}
|
|
||||||
parts.push(JSON.stringify({
|
parts.push(JSON.stringify({
|
||||||
file_name: att.name,
|
file_name: att.name,
|
||||||
file_type: ext,
|
file_type: ext,
|
||||||
context_encode: 'Base64',
|
content: rawContent,
|
||||||
context: base64Content,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
llmContent = parts.length > 0
|
llmContent = parts.length > 0
|
||||||
? parts.join('\n') + (content ? '\n\n' + content : '')
|
? (content ? content + '\n\n' : '') + parts.join('\n')
|
||||||
: content;
|
: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +301,19 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
|||||||
updateTokenUsage: (usage) =>
|
updateTokenUsage: (usage) =>
|
||||||
set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),
|
set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),
|
||||||
|
|
||||||
setProvider: (provider, model) => set({ provider, model }),
|
setProvider: (provider, model) => {
|
||||||
|
// DeepSeek/Agnes 固定 1M,Ollama 从配置读取
|
||||||
|
const ollamaCtx = provider === 'ollama' ? 128_000 : 1_000_000;
|
||||||
|
set({ provider, model, contextWindow: ollamaCtx });
|
||||||
|
// 异步读取 Ollama 实际配置
|
||||||
|
if (provider === 'ollama' && window.metona?.config?.get) {
|
||||||
|
window.metona.config.get('ollama.numCtx').then((v) => {
|
||||||
|
if (v != null && typeof v === 'number' && v > 0) {
|
||||||
|
set({ contextWindow: v });
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setMaxIterations: (max) => set({ maxIterations: max }),
|
setMaxIterations: (max) => set({ maxIterations: max }),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user