diff --git a/src/app/repo/agent_task.test.ts b/src/app/repo/agent_task.test.ts index 9a456b377..068cac8ae 100644 --- a/src/app/repo/agent_task.test.ts +++ b/src/app/repo/agent_task.test.ts @@ -141,13 +141,21 @@ describe("AgentTaskRunRepo", () => { it("appendRun 超过 MAX_RUNS_PER_TASK 时裁剪最老记录", async () => { const taskId = "task-ring"; - for (let i = 0; i < 105; i++) { + // 预填 500 条数据(最新在前),避免逐条 append 超时 + const prefilled: AgentTaskRun[] = []; + for (let i = 499; i >= 0; i--) { + prefilled.push(makeRun({ id: `rr-${i}`, taskId, starttime: i })); + } + await (repo as any).writeJsonFile(`${taskId}.json`, prefilled); + + // 再 append 5 条(id rr-500 ~ rr-504),触发裁剪 + for (let i = 500; i < 505; i++) { await repo.appendRun(makeRun({ id: `rr-${i}`, taskId, starttime: i })); } - const runs = await repo.listRuns(taskId, 200); - expect(runs.length).toBe(100); + const runs = await repo.listRuns(taskId, 600); + expect(runs.length).toBe(500); // 最新的在前,最老 5 条被裁剪掉(rr-0 ~ rr-4) - expect(runs[0].id).toBe("rr-104"); - expect(runs[99].id).toBe("rr-5"); + expect(runs[0].id).toBe("rr-504"); + expect(runs[499].id).toBe("rr-5"); }); }); diff --git a/src/app/repo/agent_task.ts b/src/app/repo/agent_task.ts index a66acb863..6720b7cb6 100644 --- a/src/app/repo/agent_task.ts +++ b/src/app/repo/agent_task.ts @@ -28,7 +28,7 @@ export class AgentTaskRepo extends Repo { } } -const MAX_RUNS_PER_TASK = 100; +const MAX_RUNS_PER_TASK = 500; export class AgentTaskRunRepo extends OPFSRepo { constructor() { diff --git a/src/app/repo/sub_agent_context.test.ts b/src/app/repo/sub_agent_context.test.ts deleted file mode 100644 index b14ec0a8f..000000000 --- a/src/app/repo/sub_agent_context.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, it, beforeEach } from "vitest"; -import { SubAgentContextRepo, type SubAgentContextEntry } from "./sub_agent_context"; -import { createMockOPFS } from "./test-helpers"; - -function makeEntry(overrides: Partial = {}): SubAgentContextEntry { - return { - agentId: "agent-1", - typeName: "researcher", - description: "test agent", - messages: [ - { role: "system", content: "You are a researcher." }, - { role: "user", content: "Hello" }, - ], - status: "completed", - result: "Done", - ...overrides, - }; -} - -describe("SubAgentContextRepo", () => { - let repo: SubAgentContextRepo; - - beforeEach(() => { - createMockOPFS(); - repo = new SubAgentContextRepo(); - }); - - it("空对话返回空数组", async () => { - const contexts = await repo.getContexts("conv-1"); - expect(contexts).toEqual([]); - }); - - it("保存并读取单个上下文", async () => { - const entry = makeEntry(); - await repo.saveContext("conv-1", entry); - - const result = await repo.getContext("conv-1", "agent-1"); - expect(result).toBeDefined(); - expect(result!.agentId).toBe("agent-1"); - expect(result!.typeName).toBe("researcher"); - expect(result!.messages).toHaveLength(2); - }); - - it("更新已有上下文", async () => { - await repo.saveContext("conv-1", makeEntry()); - await repo.saveContext("conv-1", makeEntry({ result: "Updated" })); - - const contexts = await repo.getContexts("conv-1"); - expect(contexts).toHaveLength(1); - expect(contexts[0].result).toBe("Updated"); - }); - - it("同一对话保存多个子代理", async () => { - await repo.saveContext("conv-1", makeEntry({ agentId: "agent-1" })); - await repo.saveContext("conv-1", makeEntry({ agentId: "agent-2", typeName: "page_operator" })); - - const contexts = await repo.getContexts("conv-1"); - expect(contexts).toHaveLength(2); - expect(contexts[0].agentId).toBe("agent-1"); - expect(contexts[1].agentId).toBe("agent-2"); - }); - - it("不同对话互相隔离", async () => { - await repo.saveContext("conv-1", makeEntry({ agentId: "a1" })); - await repo.saveContext("conv-2", makeEntry({ agentId: "a2" })); - - expect(await repo.getContext("conv-1", "a1")).toBeDefined(); - expect(await repo.getContext("conv-1", "a2")).toBeUndefined(); - expect(await repo.getContext("conv-2", "a2")).toBeDefined(); - }); - - it("LRU 淘汰:超过 10 个时移除最早的", async () => { - for (let i = 0; i < 11; i++) { - await repo.saveContext("conv-1", makeEntry({ agentId: `agent-${i}` })); - } - - const contexts = await repo.getContexts("conv-1"); - expect(contexts).toHaveLength(10); - // 第 0 个被淘汰 - expect(contexts[0].agentId).toBe("agent-1"); - expect(contexts[9].agentId).toBe("agent-10"); - }); - - it("getContext 返回 undefined 当 agentId 不存在", async () => { - await repo.saveContext("conv-1", makeEntry()); - const result = await repo.getContext("conv-1", "nonexistent"); - expect(result).toBeUndefined(); - }); - - it("removeContexts 清除整个对话的上下文", async () => { - await repo.saveContext("conv-1", makeEntry({ agentId: "a1" })); - await repo.saveContext("conv-1", makeEntry({ agentId: "a2" })); - - await repo.removeContexts("conv-1"); - const contexts = await repo.getContexts("conv-1"); - expect(contexts).toEqual([]); - }); - - it("removeContexts 不影响其他对话", async () => { - await repo.saveContext("conv-1", makeEntry({ agentId: "a1" })); - await repo.saveContext("conv-2", makeEntry({ agentId: "a2" })); - - await repo.removeContexts("conv-1"); - expect(await repo.getContext("conv-2", "a2")).toBeDefined(); - }); -}); diff --git a/src/app/repo/sub_agent_context.ts b/src/app/repo/sub_agent_context.ts deleted file mode 100644 index 328a8d135..000000000 --- a/src/app/repo/sub_agent_context.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { OPFSRepo } from "@App/app/repo/opfs_repo"; -import type { ChatRequest } from "@App/app/service/agent/core/types"; - -const MAX_CONTEXTS_PER_CONVERSATION = 10; - -/** 子代理上下文条目(持久化格式) */ -export interface SubAgentContextEntry { - agentId: string; - typeName: string; - description: string; - messages: ChatRequest["messages"]; - status: "completed" | "error"; - result?: string; -} - -export class SubAgentContextRepo extends OPFSRepo { - constructor() { - super("subagent_contexts"); - } - - private filename(parentConversationId: string): string { - return `${parentConversationId}.json`; - } - - async getContexts(parentConversationId: string): Promise { - return this.readJsonFile(this.filename(parentConversationId), []); - } - - async getContext(parentConversationId: string, agentId: string): Promise { - const entries = await this.getContexts(parentConversationId); - return entries.find((e) => e.agentId === agentId); - } - - async saveContext(parentConversationId: string, entry: SubAgentContextEntry): Promise { - const entries = await this.getContexts(parentConversationId); - const idx = entries.findIndex((e) => e.agentId === entry.agentId); - if (idx >= 0) { - entries[idx] = entry; - } else { - // LRU:超过上限时淘汰最早的条目 - if (entries.length >= MAX_CONTEXTS_PER_CONVERSATION) { - entries.shift(); - } - entries.push(entry); - } - await this.writeJsonFile(this.filename(parentConversationId), entries); - } - - async removeContexts(parentConversationId: string): Promise { - await this.deleteFile(this.filename(parentConversationId)); - } -} - -// 模块单例 -export const subAgentContextRepo = new SubAgentContextRepo(); diff --git a/src/app/service/agent/core/system_prompt.ts b/src/app/service/agent/core/system_prompt.ts index c0bff0f30..580fb6508 100644 --- a/src/app/service/agent/core/system_prompt.ts +++ b/src/app/service/agent/core/system_prompt.ts @@ -136,7 +136,6 @@ The sub-agent starts fresh — it has zero context from this conversation. Brief - **Launch multiple agents concurrently** whenever possible — call \`agent\` multiple times **in the same response**. - Sub-agent results are not visible to the user. Summarize the results for the user after sub-agents complete. - Sub-agents share the parent's task list — they can call \`update_task\` to report progress. -- To continue a previously completed sub-agent, use the \`to\` parameter with the agentId. ### When NOT to Use diff --git a/src/app/service/agent/core/tools/sub_agent.test.ts b/src/app/service/agent/core/tools/sub_agent.test.ts index 1bda2b2c5..81edc42cb 100644 --- a/src/app/service/agent/core/tools/sub_agent.test.ts +++ b/src/app/service/agent/core/tools/sub_agent.test.ts @@ -15,7 +15,6 @@ describe("sub_agent", () => { prompt: "Search for X", description: "Searching X", type: undefined, - to: undefined, }); expect(result).toContain("[agentId: test-id]"); expect(result).toContain("Sub-agent result"); @@ -30,20 +29,18 @@ describe("sub_agent", () => { prompt: "Do something", description: "Sub-agent task", type: undefined, - to: undefined, }); }); - it("should pass type and to parameters", async () => { + it("should pass type parameter", async () => { const mockRunSubAgent = vi.fn().mockResolvedValue({ agentId: "id3", result: "ok" }); const { executor } = createSubAgentTool({ runSubAgent: mockRunSubAgent }); - await executor.execute({ prompt: "Research X", type: "researcher", to: "prev-agent-id" }); + await executor.execute({ prompt: "Research X", type: "researcher" }); expect(mockRunSubAgent).toHaveBeenCalledWith({ prompt: "Research X", description: "Sub-agent task", type: "researcher", - to: "prev-agent-id", }); }); diff --git a/src/app/service/agent/core/tools/sub_agent.ts b/src/app/service/agent/core/tools/sub_agent.ts index 011d6e74b..8a93d8c98 100644 --- a/src/app/service/agent/core/tools/sub_agent.ts +++ b/src/app/service/agent/core/tools/sub_agent.ts @@ -8,7 +8,6 @@ export type SubAgentRunOptions = { prompt: string; description: string; type?: string; - to?: string; // 延续已有子代理 tabId?: number; // 父代理传递的标签页上下文 }; @@ -26,18 +25,17 @@ const SUB_AGENT_TYPE_NAMES = Object.keys(SUB_AGENT_TYPES); export const SUB_AGENT_DEFINITION: ToolDefinition = { name: "agent", description: - "Launch a sub-agent to handle a subtask autonomously. Sub-agents run in their own conversation context. Use the `type` parameter to select a specialized sub-agent, or use `to` to continue a previous sub-agent with follow-up instructions.", + "Launch a sub-agent to handle a subtask autonomously. Sub-agents run in their own conversation context. Use the `type` parameter to select a specialized sub-agent.", parameters: { type: "object", properties: { prompt: { type: "string", - description: "The task description or follow-up message for the sub-agent. Be specific about what you need.", + description: "The task description for the sub-agent. Be specific about what you need.", }, description: { type: "string", - description: - "A short (3-5 word) description of what the sub-agent will do, shown in the UI. Optional when resuming a previous sub-agent via `to`.", + description: "A short (3-5 word) description of what the sub-agent will do, shown in the UI.", }, type: { type: "string", @@ -45,11 +43,6 @@ export const SUB_AGENT_DEFINITION: ToolDefinition = { description: "Sub-agent type. 'researcher' (web search/fetch, page reading — read-only, no DOM interaction), 'page_operator' (browser tab interaction, DOM manipulation, page automation), 'general' (all tools, default). Choose the most specific type for better results.", }, - to: { - type: "string", - description: - "agentId of a previously completed sub-agent. Sends a follow-up message while preserving the sub-agent's full conversation context.", - }, tab_id: { type: "number", description: @@ -71,10 +64,9 @@ export function createSubAgentTool(params: { const prompt = requireString(args, "prompt"); const description = (args.description as string) || "Sub-agent task"; const type = args.type as string | undefined; - const to = args.to as string | undefined; const tabId = args.tab_id as number | undefined; - const result = await params.runSubAgent({ prompt, description, type, to, tabId }); + const result = await params.runSubAgent({ prompt, description, type, tabId }); // 返回结构化结果,附带子代理执行详情用于持久化 const content = `[agentId: ${result.agentId}]\n\n${result.result}`; diff --git a/src/app/service/agent/service_worker/chat_service.ts b/src/app/service/agent/service_worker/chat_service.ts index e5c060591..8ce7a833b 100644 --- a/src/app/service/agent/service_worker/chat_service.ts +++ b/src/app/service/agent/service_worker/chat_service.ts @@ -522,7 +522,7 @@ export class ChatService { // Sub-agent const subAgentTool = createSubAgentTool({ runSubAgent: (options: SubAgentRunOptions) => { - const agentId = options.to || uuidv4(); + const agentId = uuidv4(); const typeConfig = resolveSubAgentType(options.type); // 组合父信号和类型配置的超时信号 const subSignal = AbortSignal.any([abortController.signal, AbortSignal.timeout(typeConfig.timeoutMs)]); diff --git a/src/app/service/agent/service_worker/sub_agent_service.test.ts b/src/app/service/agent/service_worker/sub_agent_service.test.ts index 673fe924a..b99a83469 100644 --- a/src/app/service/agent/service_worker/sub_agent_service.test.ts +++ b/src/app/service/agent/service_worker/sub_agent_service.test.ts @@ -4,20 +4,6 @@ import type { SubAgentOrchestrator } from "./sub_agent_service"; import type { AgentModelConfig, ChatStreamEvent } from "@App/app/service/agent/core/types"; import type { ToolExecutorLike } from "@App/app/service/agent/core/tool_registry"; -// mock subAgentContextRepo 单例 -const { mockContextRepo } = vi.hoisted(() => ({ - mockContextRepo: { - getContext: vi.fn(), - saveContext: vi.fn(), - removeContexts: vi.fn(), - }, -})); - -vi.mock("@App/app/repo/sub_agent_context", () => ({ - subAgentContextRepo: mockContextRepo, - SubAgentContextRepo: class {}, -})); - // 简单 mock toolRegistry,返回空工具列表 function makeMockToolRegistry(): ToolExecutorLike { return { @@ -46,7 +32,7 @@ function makeMockOrchestrator(): SubAgentOrchestrator { }; } -describe("SubAgentService OPFS 持久化", () => { +describe("SubAgentService", () => { let orchestrator: SubAgentOrchestrator; let service: SubAgentService; let toolRegistry: ToolExecutorLike; @@ -55,10 +41,6 @@ describe("SubAgentService OPFS 持久化", () => { beforeEach(() => { vi.clearAllMocks(); - mockContextRepo.saveContext.mockResolvedValue(undefined); - mockContextRepo.getContext.mockResolvedValue(undefined); - mockContextRepo.removeContexts.mockResolvedValue(undefined); - orchestrator = makeMockOrchestrator(); service = new SubAgentService(orchestrator); toolRegistry = makeMockToolRegistry(); @@ -66,7 +48,7 @@ describe("SubAgentService OPFS 持久化", () => { signal = new AbortController().signal; }); - it("a) 新建子代理后 saveContext 被调用", async () => { + it("新建子代理并返回结果", async () => { const result = await service.runSubAgent({ options: { prompt: "做个任务", description: "测试任务" }, agentId: "test-agent-1", @@ -77,121 +59,12 @@ describe("SubAgentService OPFS 持久化", () => { signal, }); - expect(result.agentId).toBeTruthy(); + expect(result.agentId).toBe("test-agent-1"); expect(result.result).toBe("result"); - expect(mockContextRepo.saveContext).toHaveBeenCalledOnce(); - const [calledConvId, calledEntry] = mockContextRepo.saveContext.mock.calls[0]; - expect(calledConvId).toBe("conv-1"); - expect(calledEntry.agentId).toBe(result.agentId); - expect(calledEntry.description).toBe("测试任务"); - expect(calledEntry.status).toBe("completed"); + expect(orchestrator.callLLMWithToolLoop).toHaveBeenCalledOnce(); }); - it("b) resume 时内存命中,不调用 OPFS getContext", async () => { - // 先新建,使其进入内存缓存 - const created = await service.runSubAgent({ - options: { prompt: "初始任务", description: "初始" }, - agentId: "test-agent-2", - model: MODEL, - parentConversationId: "conv-2", - toolRegistry, - sendEvent, - signal, - }); - - vi.clearAllMocks(); - mockContextRepo.saveContext.mockResolvedValue(undefined); - - // resume,内存中已有 - await service.runSubAgent({ - options: { prompt: "继续任务", description: "继续", to: created.agentId }, - agentId: created.agentId, - model: MODEL, - parentConversationId: "conv-2", - toolRegistry, - sendEvent, - signal, - }); - - // 内存命中,不应调用 getContext - expect(mockContextRepo.getContext).not.toHaveBeenCalled(); - // 但应持久化更新 - expect(mockContextRepo.saveContext).toHaveBeenCalledOnce(); - }); - - it("c) resume 时内存未命中,从 OPFS 恢复,getContext 被调用且结果恢复到内存", async () => { - const agentId = "agent-from-opfs"; - const persistedEntry = { - agentId, - typeName: "general", - description: "OPFS 中的代理", - messages: [ - { role: "system" as const, content: "system prompt" }, - { role: "user" as const, content: "原始任务" }, - ], - status: "completed" as const, - result: "原始结果", - }; - mockContextRepo.getContext.mockResolvedValue(persistedEntry); - - // 直接 resume(内存中没有) - const result = await service.runSubAgent({ - options: { prompt: "继续", description: "继续", to: agentId }, - agentId, - model: MODEL, - parentConversationId: "conv-3", - toolRegistry, - sendEvent, - signal, - }); - - expect(mockContextRepo.getContext).toHaveBeenCalledWith("conv-3", agentId); - expect(result.agentId).toBe(agentId); - expect(result.result).toBe("result"); - - // 恢复后应更新持久化 - expect(mockContextRepo.saveContext).toHaveBeenCalledOnce(); - - // 再次 resume 同一 agent,内存已有,不再调 getContext - vi.clearAllMocks(); - mockContextRepo.saveContext.mockResolvedValue(undefined); - await service.runSubAgent({ - options: { prompt: "再继续", description: "再继续", to: agentId }, - agentId, - model: MODEL, - parentConversationId: "conv-3", - toolRegistry, - sendEvent, - signal, - }); - expect(mockContextRepo.getContext).not.toHaveBeenCalled(); - }); - - it("d) resume 时内存和 OPFS 都未命中,返回 error 消息", async () => { - mockContextRepo.getContext.mockResolvedValue(undefined); - - const result = await service.runSubAgent({ - options: { prompt: "继续", description: "继续", to: "nonexistent-agent" }, - agentId: "nonexistent-agent", - model: MODEL, - parentConversationId: "conv-4", - toolRegistry, - sendEvent, - signal, - }); - - expect(result.agentId).toBe("nonexistent-agent"); - expect(result.result).toContain("Error"); - expect(result.result).toContain("nonexistent-agent"); - // 未执行 LLM,也未持久化 - expect(orchestrator.callLLMWithToolLoop).not.toHaveBeenCalled(); - expect(mockContextRepo.saveContext).not.toHaveBeenCalled(); - }); - - it("e) cleanup 调用 removeContexts", () => { - service.cleanup("conv-5"); - - // removeContexts 异步调用,不 await,但应已触发 - expect(mockContextRepo.removeContexts).toHaveBeenCalledWith("conv-5"); + it("cleanup 不抛异常", () => { + expect(() => service.cleanup("conv-1")).not.toThrow(); }); }); diff --git a/src/app/service/agent/service_worker/sub_agent_service.ts b/src/app/service/agent/service_worker/sub_agent_service.ts index 006a7fd5d..fc3e18463 100644 --- a/src/app/service/agent/service_worker/sub_agent_service.ts +++ b/src/app/service/agent/service_worker/sub_agent_service.ts @@ -8,7 +8,6 @@ import type { ToolExecutorLike } from "@App/app/service/agent/core/tool_registry import type { SubAgentRunOptions, SubAgentRunResult } from "@App/app/service/agent/core/tools/sub_agent"; import { resolveSubAgentType, getExcludeToolsForType } from "@App/app/service/agent/core/sub_agent_types"; import { buildSubAgentSystemPrompt } from "@App/app/service/agent/core/system_prompt"; -import { subAgentContextRepo, type SubAgentContextEntry } from "@App/app/repo/sub_agent_context"; /** 供 SubAgentService 调用的 orchestrator 能力 */ export interface SubAgentOrchestrator { @@ -26,12 +25,9 @@ export interface SubAgentOrchestrator { } export class SubAgentService { - // 子代理上下文缓存,按父对话 ID 分组,对话结束时清理 - private subAgentContexts = new Map>(); - constructor(private orchestrator: SubAgentOrchestrator) {} - // 子代理公共编排层:处理 type 解析、resume 路由 + // 子代理公共编排层:处理 type 解析 // toolRegistry 由调用方传入(隔离的 childRegistry), // 包含子代理需要的工具(task / execute_script),不含父会话的 skill 等动态工具 async runSubAgent(params: { @@ -44,73 +40,13 @@ export class SubAgentService { sendEvent: (event: ChatStreamEvent) => void; signal: AbortSignal; }): Promise { - const { options, agentId: callerAgentId, model, parentConversationId, toolRegistry, sendEvent, signal } = params; + const { options, agentId: callerAgentId, model, toolRegistry, sendEvent, signal } = params; const typeConfig = resolveSubAgentType(options.type); // 从传入的 toolRegistry 获取可用工具名,计算排除列表(包含父会话的 session 工具) const allToolNames = toolRegistry.getDefinitions().map((d) => d.name); const excludeTools = getExcludeToolsForType(typeConfig, allToolNames); - // resume 模式:延续已有子代理 - if (options.to) { - // 先查内存缓存��未命中时从 OPFS 恢复 - const contextMap = this.subAgentContexts.get(parentConversationId); - let ctx = contextMap?.get(options.to); - if (!ctx) { - const persisted = await subAgentContextRepo.getContext(parentConversationId, options.to); - if (persisted) { - ctx = persisted; - // 恢复到内存缓存 - if (!this.subAgentContexts.has(parentConversationId)) { - this.subAgentContexts.set(parentConversationId, new Map()); - } - this.subAgentContexts.get(parentConversationId)!.set(options.to, ctx); - } - } - if (!ctx) { - return { - agentId: options.to, - result: `Error: Sub-agent "${options.to}" not found. It may have been cleaned up when the conversation ended.`, - }; - } - - // 追加新的 user message 到已有上下文 - ctx.messages.push({ role: "user", content: options.prompt }); - ctx.status = "completed"; // 重置,将由 core 更新 - - const { - result, - details, - usage: subUsage, - } = await this.runSubAgentCore({ - toolRegistry, - messages: ctx.messages, - model, - excludeTools, - maxIterations: typeConfig.maxIterations, - sendEvent, - signal, - }); - - // 更新缓存 + 持久化 - ctx.result = result; - ctx.status = "completed"; - await subAgentContextRepo.saveContext(parentConversationId, ctx); - - return { - agentId: options.to, - result, - details: { - agentId: options.to, - description: ctx.description, - subAgentType: ctx.typeName, - messages: details, - usage: subUsage, - }, - }; - } - - // 新建模式:使用调用方传入的 agentId,确保与事件路由一致 const agentId = callerAgentId; // 构建子代理专用 system prompt @@ -144,28 +80,6 @@ export class SubAgentService { signal, }); - // 保存子代理上下文(用于延续) - if (!this.subAgentContexts.has(parentConversationId)) { - this.subAgentContexts.set(parentConversationId, new Map()); - } - const contextMap = this.subAgentContexts.get(parentConversationId)!; - // 限制每个对话最多缓存 10 个子代理上下文,LRU 淘汰 - if (contextMap.size >= 10) { - const oldestKey = contextMap.keys().next().value; - if (oldestKey) contextMap.delete(oldestKey); - } - const entry: SubAgentContextEntry = { - agentId, - typeName: typeConfig.name, - description: options.description, - messages, - status: "completed", - result, - }; - contextMap.set(agentId, entry); - // 持久化到 OPFS - await subAgentContextRepo.saveContext(parentConversationId, entry); - return { agentId, result, @@ -292,9 +206,8 @@ export class SubAgentService { }; } - /** 清理某对话的所有子代理上下文(内存 + OPFS) */ - cleanup(parentConversationId: string): void { - this.subAgentContexts.delete(parentConversationId); - subAgentContextRepo.removeContexts(parentConversationId).catch(() => {}); + /** 清理某对话的资源(预留扩展点) */ + cleanup(_parentConversationId: string): void { + // 当前无需清理,保留接口供后续扩展 } } diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 3cd779884..bbeee6445 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -781,6 +781,7 @@ "agent_tasks_never_run": "Never run", "agent_settings": "Settings", "agent_settings_title": "Agent Settings", + "agent_doc_link": "Documentation", "agent_model_settings": "Model Settings", "agent_summary_model": "Summary Model", "agent_summary_model_desc": "Model for summarization, falls back to default if not set", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index bfe4f0c82..b57146481 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -781,6 +781,7 @@ "agent_tasks_never_run": "未运行", "agent_settings": "设置", "agent_settings_title": "Agent 设置", + "agent_doc_link": "查看文档", "agent_model_settings": "模型设置", "agent_summary_model": "摘要模型", "agent_summary_model_desc": "用于网页摘要等场景,未设置时使用默认模型", diff --git a/src/pages/options/routes/AgentDocLink.tsx b/src/pages/options/routes/AgentDocLink.tsx new file mode 100644 index 000000000..4f2af313a --- /dev/null +++ b/src/pages/options/routes/AgentDocLink.tsx @@ -0,0 +1,29 @@ +import { Button, Tooltip } from "@arco-design/web-react"; +import { IconQuestionCircle } from "@arco-design/web-react/icon"; +import { useTranslation } from "react-i18next"; +import { DocumentationSite } from "@App/app/const"; +import { localePath } from "@App/locales/locales"; + +// Agent 各页面对应的文档路径 +const DOC_PATHS: Record = { + provider: "agent-model", + skills: "agent-skill-install", + mcp: "agent-mcp", + tasks: "agent-task", + opfs: "agent-opfs", + settings: "agent", +}; + +function AgentDocLink({ page }: { page: keyof typeof DOC_PATHS }) { + const { t } = useTranslation(); + const docPath = DOC_PATHS[page]; + return ( + document.body}> + + + + + + } > {servers.length === 0 ? ( diff --git a/src/pages/options/routes/AgentOPFS.tsx b/src/pages/options/routes/AgentOPFS.tsx index dbef3c1e9..edaeaf716 100644 --- a/src/pages/options/routes/AgentOPFS.tsx +++ b/src/pages/options/routes/AgentOPFS.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Breadcrumb, Button, Card, Empty, Message, Modal, Space, Table } from "@arco-design/web-react"; import { IconDelete, IconDownload, IconEye, IconFolder, IconFile, IconImage } from "@arco-design/web-react/icon"; +import AgentDocLink from "./AgentDocLink"; import { isImageFileName } from "@App/app/service/agent/core/content_utils"; interface FileEntry { @@ -244,7 +245,7 @@ function AgentOPFS() { return ( - + }> navigateTo(-1)} style={{ cursor: "pointer" }}> {t("agent_opfs_root")} diff --git a/src/pages/options/routes/AgentProvider.tsx b/src/pages/options/routes/AgentProvider.tsx index e4b9a7720..ac9eacc46 100644 --- a/src/pages/options/routes/AgentProvider.tsx +++ b/src/pages/options/routes/AgentProvider.tsx @@ -15,6 +15,7 @@ import { Typography, } from "@arco-design/web-react"; import { IconCheck, IconCopy, IconDelete, IconEdit, IconEye, IconImage, IconPlus } from "@arco-design/web-react/icon"; +import AgentDocLink from "./AgentDocLink"; import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { AgentModelConfig } from "@App/app/service/agent/core/types"; @@ -447,9 +448,12 @@ function AgentProvider() { title={t("agent_provider_title")} bordered={false} extra={ - + + + + } > {models.length === 0 ? ( diff --git a/src/pages/options/routes/AgentSettings.tsx b/src/pages/options/routes/AgentSettings.tsx index 85866ee15..1e7a5adc9 100644 --- a/src/pages/options/routes/AgentSettings.tsx +++ b/src/pages/options/routes/AgentSettings.tsx @@ -1,4 +1,5 @@ import { Card, Message, Select, Input, Space, Typography, Alert } from "@arco-design/web-react"; +import AgentDocLink from "./AgentDocLink"; import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useState } from "react"; import type { AgentModelConfig } from "@App/app/service/agent/core/types"; @@ -82,7 +83,12 @@ function AgentSettings() { return ( - {t("agent_settings_title")} +
+ + {t("agent_settings_title")} + + +
diff --git a/src/pages/options/routes/AgentSkills.tsx b/src/pages/options/routes/AgentSkills.tsx index 21aff7fa1..890d0a89c 100644 --- a/src/pages/options/routes/AgentSkills.tsx +++ b/src/pages/options/routes/AgentSkills.tsx @@ -22,6 +22,7 @@ import { IconRefresh, IconSettings, } from "@arco-design/web-react/icon"; +import AgentDocLink from "./AgentDocLink"; import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useRef, useState } from "react"; import type { @@ -653,6 +654,7 @@ function AgentSkills() { + } > diff --git a/src/pages/options/routes/AgentTasks.tsx b/src/pages/options/routes/AgentTasks.tsx index bb9d52524..ced1b772e 100644 --- a/src/pages/options/routes/AgentTasks.tsx +++ b/src/pages/options/routes/AgentTasks.tsx @@ -18,6 +18,7 @@ import { Typography, } from "@arco-design/web-react"; import { IconDelete, IconEdit, IconHistory, IconPlayArrow, IconPlus } from "@arco-design/web-react/icon"; +import AgentDocLink from "./AgentDocLink"; import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useState } from "react"; import type { @@ -388,9 +389,12 @@ function AgentTasks() { title={t("agent_tasks_title")} bordered={false} extra={ - + + + + } > {tasks.length === 0 ? (