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:
@@ -30,17 +30,9 @@ interface AssistantMessageProps {
|
||||
export function AssistantMessage({ message, isStreaming, streamContent }: AssistantMessageProps): React.JSX.Element {
|
||||
const content = isStreaming ? streamContent ?? '' : message.content;
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const sendMessage = useAgentStore((s) => s.sendMessage);
|
||||
const agentStatus = useAgentStore((s) => s.agentStatus);
|
||||
|
||||
const handleRegenerate = useCallback(() => {
|
||||
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 contextMenuItems = createContextMenuItems('message', { content });
|
||||
|
||||
const hasThinking = !!message.reasoningContent;
|
||||
const hasTools = !!message.toolCalls?.length;
|
||||
|
||||
@@ -20,7 +20,7 @@ export function MessageList(): React.JSX.Element {
|
||||
return (
|
||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<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="body1" sx={{ color: 'text.secondary' }}>生产级通用 AI Agent 智能体桌面应用</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 [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 (
|
||||
<Stack spacing={2}>
|
||||
<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 maxIterations = useAgentStore((s) => s.maxIterations);
|
||||
const currentIteration = useAgentStore((s) => s.currentIteration);
|
||||
const contextWindow = useAgentStore((s) => s.contextWindow);
|
||||
|
||||
const maxTokens = 128_000;
|
||||
const maxTokens = contextWindow;
|
||||
const usagePercent = tokenUsage.totalTokens > 0
|
||||
? Math.min((tokenUsage.totalTokens / maxTokens) * 100, 100)
|
||||
: 0;
|
||||
|
||||
+35
-34
@@ -36,20 +36,21 @@ export function useAgentStream(): void {
|
||||
state?: string;
|
||||
};
|
||||
|
||||
const store = useAgentStore.getState();
|
||||
// 每次都从 store 读取最新状态(避免闭包捕获过期快照)
|
||||
const getStore = () => useAgentStore.getState();
|
||||
|
||||
// 每次收到迭代号时同步更新
|
||||
if (data.iteration != null && data.iteration !== store.currentIteration) {
|
||||
console.log(`[useAgentStream] iteration: ${store.currentIteration} → ${data.iteration} (event: ${data.type})`);
|
||||
store.setCurrentIteration(data.iteration);
|
||||
if (data.iteration != null && data.iteration !== getStore().currentIteration) {
|
||||
console.log(`[useAgentStream] iteration: ${getStore().currentIteration} → ${data.iteration} (event: ${data.type})`);
|
||||
getStore().setCurrentIteration(data.iteration);
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
// 思考开始
|
||||
case 'thinking_start':
|
||||
store.setAgentStatus('thinking');
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.currentIteration + 1,
|
||||
getStore().setAgentStatus('thinking');
|
||||
getStore().addTraceStep({
|
||||
iteration: data.iteration ?? getStore().currentIteration + 1,
|
||||
state: 'THINKING',
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
@@ -58,16 +59,16 @@ export function useAgentStream(): void {
|
||||
// 推理内容增量
|
||||
case 'reasoning_delta':
|
||||
if (data.delta) {
|
||||
const messages = store.messages;
|
||||
const messages = getStore().messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
// 追加到最后一条 assistant 消息
|
||||
store.updateMessage(lastMsg.id, {
|
||||
getStore().updateMessage(lastMsg.id, {
|
||||
reasoningContent: (lastMsg.reasoningContent ?? '') + data.delta,
|
||||
});
|
||||
} else {
|
||||
// 还没有 assistant 消息,先创建一条(仅含思考内容)
|
||||
store.addMessage({
|
||||
getStore().addMessage({
|
||||
id: `msg_${Date.now()}_assistant`,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
@@ -81,15 +82,15 @@ export function useAgentStream(): void {
|
||||
// 思考结束
|
||||
case 'thinking_end':
|
||||
if (data.iteration) {
|
||||
store.updateTraceStep(data.iteration, { completedAt: Date.now() });
|
||||
getStore().updateTraceStep(data.iteration, { completedAt: Date.now() });
|
||||
}
|
||||
break;
|
||||
|
||||
// 文本增量
|
||||
case 'text_delta':
|
||||
if (data.delta) {
|
||||
store.setStreaming(true);
|
||||
store.updateLastAssistantMessage(data.delta);
|
||||
getStore().setStreaming(true);
|
||||
getStore().updateLastAssistantMessage(data.delta);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -104,17 +105,18 @@ export function useAgentStream(): void {
|
||||
};
|
||||
|
||||
// 追加到当前 assistant 消息
|
||||
const messages = store.messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
store.updateMessage(lastMsg.id, {
|
||||
toolCalls: [...(lastMsg.toolCalls ?? []), tc],
|
||||
const msgs = getStore().messages;
|
||||
const lastAssistant = msgs[msgs.length - 1];
|
||||
if (lastAssistant?.role === 'assistant') {
|
||||
getStore().updateMessage(lastAssistant.id, {
|
||||
toolCalls: [...(lastAssistant.toolCalls ?? []), tc],
|
||||
});
|
||||
}
|
||||
|
||||
store.setAgentStatus('executing');
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.currentIteration,
|
||||
getStore().setAgentStatus('executing');
|
||||
const curIter = getStore().currentIteration;
|
||||
getStore().addTraceStep({
|
||||
iteration: data.iteration ?? curIter,
|
||||
state: 'EXECUTING',
|
||||
startedAt: Date.now(),
|
||||
toolCalls: [tc],
|
||||
@@ -125,8 +127,8 @@ export function useAgentStream(): void {
|
||||
// 工具执行结果
|
||||
case 'tool_result': {
|
||||
if (data.toolResult) {
|
||||
const messages = store.messages;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
const msgs = getStore().messages;
|
||||
const lastMsg = msgs[msgs.length - 1];
|
||||
if (lastMsg?.toolCalls) {
|
||||
const updatedToolCalls = lastMsg.toolCalls.map((tc) =>
|
||||
tc.id === data.toolResult!.toolCallId
|
||||
@@ -139,7 +141,7 @@ export function useAgentStream(): void {
|
||||
}
|
||||
: tc,
|
||||
);
|
||||
store.updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
|
||||
getStore().updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -148,7 +150,7 @@ export function useAgentStream(): void {
|
||||
// Token 使用统计
|
||||
case 'usage':
|
||||
if (data.usage) {
|
||||
store.updateTokenUsage({
|
||||
getStore().updateTokenUsage({
|
||||
inputTokens: data.usage.inputTokens ?? 0,
|
||||
outputTokens: data.usage.outputTokens ?? 0,
|
||||
totalTokens: data.usage.totalTokens ?? 0,
|
||||
@@ -158,17 +160,16 @@ export function useAgentStream(): void {
|
||||
|
||||
// 流结束
|
||||
case 'done':
|
||||
store.setStreaming(false);
|
||||
store.setAgentStatus('idle');
|
||||
// 保存 trace 数据到数据库
|
||||
store.saveTraceData();
|
||||
getStore().setStreaming(false);
|
||||
getStore().setAgentStatus('idle');
|
||||
getStore().saveTraceData();
|
||||
break;
|
||||
|
||||
// 错误
|
||||
case 'error':
|
||||
store.setStreaming(false);
|
||||
store.setAgentStatus('error');
|
||||
store.addMessage({
|
||||
getStore().setStreaming(false);
|
||||
getStore().setAgentStatus('error');
|
||||
getStore().addMessage({
|
||||
id: `msg_${Date.now()}_error`,
|
||||
role: 'system',
|
||||
content: `错误: ${data.error?.message ?? '未知错误'}`,
|
||||
@@ -179,8 +180,8 @@ export function useAgentStream(): void {
|
||||
// 状态变化
|
||||
case 'state_change':
|
||||
if (data.state) {
|
||||
store.addTraceStep({
|
||||
iteration: data.iteration ?? store.currentIteration,
|
||||
getStore().addTraceStep({
|
||||
iteration: data.iteration ?? getStore().currentIteration,
|
||||
state: data.state,
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
|
||||
+19
-11
@@ -88,6 +88,7 @@ interface AgentState {
|
||||
// Provider
|
||||
provider: string;
|
||||
model: string;
|
||||
contextWindow: number;
|
||||
|
||||
// Actions
|
||||
setCurrentSession: (id: string | null) => void;
|
||||
@@ -124,6 +125,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
streamingContent: '',
|
||||
provider: '',
|
||||
model: '',
|
||||
contextWindow: 1_000_000,
|
||||
|
||||
// ===== Actions =====
|
||||
|
||||
@@ -236,23 +238,17 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
// 图片已在 images 参数中处理,跳过
|
||||
continue;
|
||||
}
|
||||
// 非图片文件:JSON 格式,Base64 编码内容
|
||||
// 非图片文件:JSON 结构化,原始文本内容(不编码)
|
||||
const ext = att.name.split('.').pop() ?? 'unknown';
|
||||
let base64Content = '';
|
||||
if (att.textContent) {
|
||||
base64Content = btoa(unescape(encodeURIComponent(att.textContent)));
|
||||
} else if (att.preview) {
|
||||
base64Content = att.preview.split(',')[1] ?? '';
|
||||
}
|
||||
const rawContent = att.textContent ?? '';
|
||||
parts.push(JSON.stringify({
|
||||
file_name: att.name,
|
||||
file_type: ext,
|
||||
context_encode: 'Base64',
|
||||
context: base64Content,
|
||||
content: rawContent,
|
||||
}));
|
||||
}
|
||||
llmContent = parts.length > 0
|
||||
? parts.join('\n') + (content ? '\n\n' + content : '')
|
||||
? (content ? content + '\n\n' : '') + parts.join('\n')
|
||||
: content;
|
||||
}
|
||||
|
||||
@@ -305,7 +301,19 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
updateTokenUsage: (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 }),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user