refactor: 移除多余依赖,统一 MUI 为唯一 UI 库

- 移除 radix-ui、clsx、tailwind-merge、class-variance-authority、react-router-dom

- 前后端全面适配 MUI 组件,清理残留 Tailwind 工具类引用

- 优化 Agent Loop 引擎、IPC 处理器、Provider Adapter 等后端模块

- 新增 Agent 网络工具通用设计文档 v2
This commit is contained in:
thzxx
2026-07-05 19:15:48 +08:00
parent ba85328c4f
commit f4532a2bb2
50 changed files with 1474 additions and 2042 deletions
@@ -125,7 +125,6 @@ export class AgnesAdapter extends BaseAdapter {
contentParts.push({ type: 'text', text: origMsg.content });
}
for (const img of origMsg.images) {
const isDataUrl = img.url.startsWith('data:');
contentParts.push({
type: 'image_url',
image_url: { url: img.url },
+11 -1
View File
@@ -30,7 +30,7 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
async healthCheck(): Promise<boolean> {
try {
await this.listModels?.();
await this.listModels();
return true;
} catch {
return false;
@@ -58,6 +58,16 @@ export abstract class BaseAdapter implements IMetonaProviderAdapter {
};
}
if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('ECONNRESET')) {
return {
code: MetonaErrorCode.NETWORK_ERROR,
message: error.message,
provider: this.provider,
retryable: true,
retryAfterMs: 3000,
};
}
if (msg.includes('401') || msg.includes('unauthorized')) {
return {
code: MetonaErrorCode.AUTH_INVALID,
+12 -2
View File
@@ -66,6 +66,7 @@ export class OllamaAdapter extends BaseAdapter {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...nativeRequest, stream: true }),
signal: AbortSignal.timeout(300_000),
});
if (!response.ok || !response.body) {
@@ -124,6 +125,8 @@ export class OllamaAdapter extends BaseAdapter {
// 工具调用(Ollama 在最后一个 chunk 中整块返回)
if (chunk.message?.tool_calls) {
for (const tc of chunk.message.tool_calls) {
const args = tc.function?.arguments;
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args ?? {});
yield {
type: MetonaStreamEventType.TOOL_CALL_COMPLETE,
requestId: request.meta.requestId,
@@ -134,7 +137,7 @@ export class OllamaAdapter extends BaseAdapter {
toolCall: {
id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
name: tc.function?.name ?? '',
args: tc.function?.arguments ?? {},
args: parsedArgs,
iteration: request.meta.iteration,
timestamp: Date.now(),
},
@@ -441,10 +444,17 @@ export class OllamaAdapter extends BaseAdapter {
reasoningContent: message?.thinking as string | undefined,
toolCalls: toolCalls?.map((tc, i) => {
const fn = tc.function as Record<string, unknown>;
const rawArgs = fn?.arguments;
let args: Record<string, unknown> = {};
try {
args = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : (rawArgs as Record<string, unknown>) ?? {};
} catch {
args = {};
}
return {
id: `tc_${Date.now()}_${i}`,
name: (fn?.name as string) ?? '',
args: (fn?.arguments as Record<string, unknown>) ?? {},
args,
iteration: 0,
timestamp: Date.now(),
};
@@ -166,7 +166,7 @@ export async function* parseSSEStream(
// 非 [DONE] 但 finish_reason 为 tool_calls 时提前 flush 缓冲区
const finishReason = chunk.choices?.[0]?.finish_reason as string | undefined;
if (finishReason === 'tool_calls' || finishReason === 'stop') {
if (finishReason === 'tool_calls') {
for (const [index, buf] of toolCallsBuffer) {
try {
yield {
+61 -18
View File
@@ -44,6 +44,9 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
contextWindow: 128_000,
retryCount: 3,
temperature: 0.0,
maxTokens: 63488,
thinkingEnabled: true,
thinkingEffort: 'high',
};
/**
@@ -157,11 +160,11 @@ export class AgentLoopEngine extends EventEmitter {
messages,
tools: this.tools.length > 0 ? this.tools : undefined,
params: {
maxTokens: 63488,
maxTokens: this.config.maxTokens,
temperature: this.config.temperature,
stream: true,
thinkingEnabled: true,
thinkingEffort: 'high',
thinkingEnabled: this.config.thinkingEnabled,
thinkingEffort: this.config.thinkingEffort,
contextLength: this.config.contextLength,
},
};
@@ -254,9 +257,12 @@ export class AgentLoopEngine extends EventEmitter {
let tokenUsage: TokenUsage | undefined;
// 流式接收响应
for await (const event of this.adapter.chatStream(request)) {
for await (const event of this.chatStreamWithRetry(request)) {
if (this.aborted) break;
// 过滤掉每轮的 DONE 事件 — 只在全部迭代完成后发送一个最终 DONE
if (event.type === MetonaStreamEventType.DONE) continue;
// 转发流式事件到渲染进程
this.emit('streamEvent', event);
@@ -299,9 +305,6 @@ export class AgentLoopEngine extends EventEmitter {
}
break;
case MetonaStreamEventType.DONE:
break;
case MetonaStreamEventType.ERROR:
if (event.error) {
throw new Error(event.error.message);
@@ -313,18 +316,21 @@ export class AgentLoopEngine extends EventEmitter {
// 处理缓冲区中的工具调用(TOOL_CALL_DELTA 拼接)
if (toolCallsBuffer.size > 0 && (!step.toolCalls || step.toolCalls.length === 0)) {
step.toolCalls = [];
for (const [index, buf] of toolCallsBuffer) {
for (const [, buf] of toolCallsBuffer) {
let args: Record<string, unknown>;
try {
step.toolCalls.push({
id: `tc_${nanoid(8)}`,
name: buf.name,
args: buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {},
iteration: this.currentIteration,
timestamp: Date.now(),
});
args = buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {};
} catch {
// JSON 解析失败,跳过
// JSON 解析失败,使用空参数(LLM 仍请求了该工具调用)
args = {};
}
step.toolCalls.push({
id: `tc_${nanoid(8)}`,
name: buf.name,
args,
iteration: this.currentIteration,
timestamp: Date.now(),
});
}
}
@@ -361,7 +367,7 @@ export class AgentLoopEngine extends EventEmitter {
// 转发工具结果到渲染进程
this.emit('streamEvent', {
type: 'tool_result',
type: MetonaStreamEventType.TOOL_RESULT,
requestId: request.meta.requestId,
sessionId,
iteration: this.currentIteration,
@@ -444,10 +450,37 @@ export class AgentLoopEngine extends EventEmitter {
return toolResult;
}
/**
* 带重试的流式调用
*
* 如果 adapter 抛出错误,在 retryCount 次数内重试,每次等待 1 秒。
*/
private async *chatStreamWithRetry(request: MetonaRequest): AsyncIterable<MetonaStreamEvent> {
let lastError: unknown;
for (let attempt = 0; attempt <= this.config.retryCount; attempt++) {
try {
yield* this.adapter.chatStream(request);
return;
} catch (error) {
lastError = error;
if (attempt < this.config.retryCount) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
throw lastError;
}
private async transitionTo(state: AgentLoopState): Promise<void> {
const previous = this.currentState;
this.currentState = state;
this.emit('stateChange', { previous, current: state });
this.emit('stateChange', {
previous,
current: state,
state,
sessionId: this.currentSessionId,
iteration: this.currentIteration,
});
}
private accumulateTokens(usage: TokenUsage): void {
@@ -461,6 +494,16 @@ export class AgentLoopEngine extends EventEmitter {
answer?: string,
error?: Error,
): AgentLoopOutput {
// 发送唯一的最终 DONE 事件 — UI 只在此处结束流式状态
this.emit('streamEvent', {
type: MetonaStreamEventType.DONE,
requestId: this.currentRequestId,
sessionId: this.currentSessionId,
iteration: this.currentIteration,
seq: 0,
timestamp: Date.now(),
});
this.currentState = AgentLoopState.TERMINATED;
return {
finalAnswer: answer ?? (error ? error.message : 'No answer produced'),
+10 -2
View File
@@ -4,6 +4,8 @@
* 用于 Agent Loop 引擎内部的状态管理和迭代记录。
*/
import type { MetonaToolCall, MetonaToolResult } from '../types';
// ===== Agent Loop 状态机 =====
export enum AgentLoopState {
@@ -41,8 +43,8 @@ export interface IterationStep {
startedAt: number;
completedAt?: number;
thought?: Thought;
toolCalls?: import('@shared/index').MetonaToolCall[];
toolResults?: import('@shared/index').MetonaToolResult[];
toolCalls?: MetonaToolCall[];
toolResults?: MetonaToolResult[];
tokenUsage?: TokenUsage;
}
@@ -63,6 +65,12 @@ export interface AgentLoopConfig {
contextWindow: number;
retryCount: number;
temperature: number;
/** 最大生成 token 数(默认 63488 */
maxTokens: number;
/** 是否启用思考模式(默认 true) */
thinkingEnabled: boolean;
/** 思考强度(默认 'high' */
thinkingEffort: 'low' | 'medium' | 'high' | 'max';
/** Ollama 上下文窗口大小(num_ctx),其他 Provider 忽略 */
contextLength?: number;
}
+13 -7
View File
@@ -41,13 +41,19 @@ export class MemoryTriggerHook implements PostToolHook {
async afterExecute(toolCall: MetonaToolCall, result: MetonaToolResult, sessionId: string): Promise<void> {
if (this.memorableTools.includes(toolCall.name) && result.success) {
const content = typeof result.result === 'string' ? result.result : JSON.stringify(result.result);
this.memoryManager.store({
type: 'episodic',
content: `Tool ${toolCall.name} returned: ${content.slice(0, 500)}`,
source: 'tool_result',
sessionId,
importance: 0.6,
});
try {
this.memoryManager.store({
type: 'episodic',
content: `Tool ${toolCall.name} returned: ${content.slice(0, 500)}`,
source: 'tool_result',
sessionId,
importance: 0.6,
});
} catch (error) {
// 记忆存储失败不应影响工具执行结果
// eslint-disable-next-line no-console
console.error('[MemoryTriggerHook] Failed to store episodic memory:', error);
}
}
}
}
+2 -2
View File
@@ -219,8 +219,8 @@ Use tools when needed to gather information or perform actions. Think step by st
let skipMeta = true;
for (const line of lines) {
// 跳过元数据注释行(以 # 开头的非标题
if (skipMeta && line.startsWith('#') && !line.startsWith('## ')) {
// 跳过元数据注释行(以 # 开头且不跟另一个 # 的行,即只跳过一级标题)
if (skipMeta && line.match(/^#[^#]/)) {
continue;
}
skipMeta = false;
+2 -2
View File
@@ -22,12 +22,12 @@ export interface PermissionPolicy {
}
export const DEFAULT_POLICIES: PermissionPolicy[] = [
{ toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc\//, /\/proc\//] },
{ toolName: 'read_file', requiredLevel: PermissionLevel.READ, deniedPatterns: [/\/etc\//, /\/proc\//, /C:\\Windows\\/i, /C:\\System32\\/i] },
{ toolName: 'web_search', requiredLevel: PermissionLevel.READ, maxFrequency: 10 },
{ toolName: 'list_directory', requiredLevel: PermissionLevel.READ },
{ toolName: 'search_files', requiredLevel: PermissionLevel.READ },
{ toolName: 'memory_search', requiredLevel: PermissionLevel.READ },
{ toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc\//, /\/proc\//, /\/System\//], requireConfirmation: true, maxFrequency: 5 },
{ toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc\//, /\/proc\//, /\/System\//, /C:\\Windows\\/i, /C:\\System32\\/i], requireConfirmation: true, maxFrequency: 5 },
{ toolName: 'memory_store', requiredLevel: PermissionLevel.WRITE },
{ toolName: 'run_command', requiredLevel: PermissionLevel.EXTERNAL_ACTION, requireConfirmation: true, maxFrequency: 3 },
{ toolName: 'web_extract', requiredLevel: PermissionLevel.READ },
+15 -3
View File
@@ -13,6 +13,7 @@ import type { IMetonaTool, ToolRegistryEntry, ToolExecutionContext } from '../ty
export class ToolRegistry {
private tools = new Map<string, ToolRegistryEntry>();
private disabledTools = new Set<string>();
/** 注册内置工具 */
registerBuiltin(tool: IMetonaTool): void {
@@ -45,16 +46,27 @@ export class ToolRegistry {
/** 获取工具 */
get(name: string): IMetonaTool | undefined {
const entry = this.tools.get(name);
return entry?.enabled ? entry.tool : undefined;
if (!entry?.enabled) return undefined;
if (this.disabledTools.has(name)) return undefined;
return entry.tool;
}
/** 列出所有已启用工具的定义 */
listTools(): MetonaToolDef[] {
return Array.from(this.tools.values())
.filter((e) => e.enabled)
.filter((e) => e.enabled && !this.disabledTools.has(e.tool.definition.name))
.map((e) => e.tool.definition);
}
/** 设置工具启用/禁用状态(供 IPC tools:toggle 调用) */
setToolEnabled(name: string, enabled: boolean): void {
if (enabled) {
this.disabledTools.delete(name);
} else {
this.disabledTools.add(name);
}
}
/** 执行工具 */
async execute(
toolCall: MetonaToolCall,
@@ -99,6 +111,6 @@ export class ToolRegistry {
/** 获取工具数量 */
get size(): number {
return Array.from(this.tools.values()).filter((e) => e.enabled).length;
return Array.from(this.tools.values()).filter((e) => e.enabled && !this.disabledTools.has(e.tool.definition.name)).length;
}
}
@@ -83,6 +83,7 @@ export enum MetonaStreamEventType {
REASONING_DELTA = 'reasoning_delta',
TOOL_CALL_DELTA = 'tool_call_delta',
TOOL_CALL_COMPLETE = 'tool_call_complete',
TOOL_RESULT = 'tool_result',
THINKING_START = 'thinking_start',
THINKING_END = 'thinking_end',
ERROR = 'error',
@@ -116,6 +117,9 @@ export interface MetonaStreamEvent {
/** TOOL_CALL_COMPLETE(拼接完成后的完整调用) */
toolCall?: MetonaToolCall;
/** TOOL_RESULT */
toolResult?: MetonaToolResult;
/** USAGE */
usage?: MetonaTokenUsage;