news 2026/10/3 8:27:46

ScriptCat Agent 子系统架构深度解析:服务组合、工具注册表与 LLM Tool Loop 全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ScriptCat Agent 子系统架构深度解析:服务组合、工具注册表与 LLM Tool Loop 全链路
  • 前端
  • 开发者工具
  • 插件系统

【免费下载链接】scriptcat

ScriptCat, a browser extension that can execute userscript; 脚本猫,一个可以执行用户脚本的浏览器扩展

项目地址:https://gitcode.com/gh_mirrors/sc/scriptcat
点击查看免费下载

ScriptCat(脚本猫)在五个既有运行时上下文(service worker、content、inject、offscreen、sandbox)之上,构建了一套完整的 AI Agent 子系统(src/app/service/agent/)。本文以仓库文档 docs/references/architecture-agent.md 为骨架,结合源码逐层拆解其服务组合方式、全局/会话级工具注册表、LLM 流式调用与重试压缩机制、后台会话与子代理生命周期、存储选型与页面自动化权限边界。读完你将掌握:Agent 功能在 ScriptCat 中如何组装、为什么需要双层工具注册表、tool loop 如何驱动一轮完整对话,以及新增工具、MCP 工具和子代理类型的标准扩展路径。

1. 定位:构建在五上下文之上的 Agent 层

从 docs/architecture.md 可知,ScriptCat 是一个 Manifest V3 浏览器扩展,运行在 service worker、content、inject、offscreen、sandbox 五个相互隔离的沙箱 realm 中,彼此通过 packages/message 的消息通道通信。Agent 子系统不是第六个上下文,而是叠加在这五个上下文之上的一层 AI 代理能力:

  • 代码统一位于src/app/service/agent/;
  • 按"与上下文无关的core/"与"service_worker 组装层"两部分组织;
  • 用户脚本通过 content 侧的CAT.agent.*API 使用对话能力;
  • 技能(Skill)脚本执行时复用 offscreen/sandbox 的脚本执行通道,不另起炉灶。

核心组装点位于 ServiceWorkerManager:

const agent = new AgentService(this.api.group("agent"), this.offscreenSend, resource); agent.init(); // 注入 AgentService 到 GMApi,使 Agent API 走权限验证通道 const gmApi = runtime.getGMApi(); if (gmApi) { gmApi.setAgentService(agent); }

AgentService通过this.api.group("agent")挂载消息 action,与 architecture.md 中描述的其他服务的 RPC 模式一致——Agent 层的差异在于内部组合方式,而非接入Group/Server的方式。

2. Service-worker 组合:AgentService 是装配器而非巨类

AgentService在构造器中只注入它真正需要的依赖(Group、MessageSend、ResourceService),随后组合出一组职责单一的窄服务。每个子服务只拿自己需要的依赖,而不是统一注入一套Group/IMessageQueue/DAO 三元组。完整清单如下:

服务文件职责
ChatServicechat_service.ts聊天请求生命周期:构建 system prompt、为每个请求装配SessionToolRegistry、把 tool loop 委托给编排器
AgentTaskServicetask_service.tsAgentTask的 CRUD 与调度(类 cron 触发器),通过同一 tool-loop 编排器运行任务
SkillServiceskill_service.ts从.md或.zip源安装/更新/列出技能(parseSkillMd/parseSkillZip),由SkillRepo持久化
AgentModelServicemodel_service.ts模型配置 CRUD,以及默认/摘要模型选择,由AgentModelRepo持久化
MCPServicemcp.ts按配置管理MCPClient连接,并把工具注册/注销到共享ToolRegistry
BackgroundSessionManagerbackground_session_manager.ts跟踪后台运行中的对话(流式状态、listener、待处理的ask_user提问),供 UI 重新附加到进行中的会话
SubAgentServicesub_agent_service.ts通过共享 tool loop 运行子代理对话,带按类型区分的工具排除列表
CompactServicecompact_service.ts用专门的 compact prompt 对长对话历史做摘要压缩
AgentDomServicedom.ts(+dom_cdp.ts辅助)页面自动化,见下文"默认模式 vs 可信模式"的拆分
AgentOPFSServiceopfs_service.ts同时服务 content 脚本(不支持 Blob)与 offscreen(支持 Blob)的CAT.agent.opfs请求,按调用方是否携带sender分派

当前实现清单可通过git grep -n "export class" -- src/app/service/agent/service_worker/验证。以AgentService构造器中的组装为例,ToolLoopOrchestrator不持有工具注册表,而是由调用方在每次callLLMWithToolLoop时传入(通常是SessionToolRegistry),从而保证并发会话的工具注册互相隔离:

this.toolLoopOrchestrator = new ToolLoopOrchestrator( { // callLLM 通过 lambda 注入,确保测试 spy 可以拦截 service.callLLM callLLM: (model, params, sendEvent, signal) => this.callLLM(model, params, sendEvent, signal), autoCompact: (convId, generation, model, msgs, sendEvent, signal) => this.compactService.autoCompact(convId, generation, model, msgs, sendEvent, signal), }, agentChatRepo );

3. 工具注册表:全局ToolRegistry与会话级SessionToolRegistry

3.1 全局注册表与 ToolSource 分类

ToolRegistry是进程级全局注册表,持有的工具在进程生命周期内持续存在,并按ToolSource分类:

  • builtin:启动期永久注册的内置工具,例如web_fetch、web_search、opfs_*,以及标签页工具list_tabs、open_tab、get_tab_content、close_tab、activate_tab;
  • mcp:来自MCPService管理的 MCP server 的工具;
  • skill:技能元工具load_skill、execute_skill_script、read_reference;
  • session:按对话注册的工具:任务工具、ask_user、agent(子代理)、execute_script;
  • script:用户脚本通过conv.chat传入的自定义工具,不存入 Map,而是通过回调(ScriptToolCallback)分派执行。

从源码可见注册表的完整操作面:register(source, definition, executor)、unregister(name)、unregisterBySource(source)(MCP server 断开时批量清理)、listBySource(source)、getDefinitions(extraTools),以及executeTools()对内置工具与脚本工具的分流处理。其中脚本工具若无回调可用,会返回带可用工具列表的错误提示,并引导 LLM 自我纠正(例如提示"若这是 skill script,请改用 execute_skill_script 工具")。

注意:工具名与其源文件不一定同名。定义在sub_agent.ts的子代理工具注册名是agent;tab_tools.ts注册的get_tab_content/list_tabs/open_tab/close_tab/activate_tab没有任何共享前缀。阅读代码时要读name:字段,而不是文件名。

3.2 为什么需要 SessionToolRegistry

SessionToolRegistry持有对全局ToolRegistry的只读引用,外加自己的会话级Map。它存在的根本原因:若把同名内置工具(任务工具、ask_user、agent)直接注册到全局注册表,并发会话会用彼此的闭包互相覆盖——会话级工具必须绑定到自己的conversationId/sendEvent。

其行为契约:

  • register()只写 session 自己的Map,不污染 parent;
  • getDefinitions()合并 session + parent 工具(session 同名遮蔽 parent),extraTools最后并入且不覆盖前两者;
  • execute()构建合并 Map 后复用parent.executeTools(),使附件持久化等共享逻辑不被重复实现。

会话结束时,该实例超出作用域被 GC 回收即完成清理,无需显式 unregister 循环。

4. LLM 调用链路:流式、重试、自动压缩与 Tool Loop

4.1 ToolLoopOrchestrator:统一的一轮对话驱动

ToolLoopOrchestrator驱动一次会话轮次:调用模型 → 执行模型请求的工具调用 → 把结果回喂 → 重复直到模型不再调用工具(或用户通过 Loop Guard/取消中止)。它依赖注入的callLLM与autoCompact函数(而非直接 import 具体客户端),因此测试可以用 spy 替换。UI 对话与脚本驱动的对话共用同一条边界,不存在两套 tool loop 实现。

关键行为(均有源码佐证):

  • 上下文预算:按模型完整上下文窗口计算使用率(getContextInputTokens/getContextWindow),达到 80% 时触发autoCompact,而不是用固定估算提前触发;
  • 工具轮持久化:assistant 消息与全部 tool 消息通过commitToolRound一次性原子提交,避免持久化历史暴露半轮状态;若提交失败,checkToolRoundDurability区分"已落盘/未落盘/不确定"三态——只有正向证实未落盘才允许回收本轮租约态附件(not_durable),确认读失败(indeterminate)时宁可保留租约、以persist_indeterminate终止,也不误删仍被引用的文件;
  • 取消终态化:emitCancelled统一收敛"取消"路径,持久化一条终态记录并发送唯一终态事件(携带累计 usage/耗时),落库失败不阻塞事件发送;
  • Loop Guard:tool_call_guard的循环检测连续命中GUARD_ESCALATION_STRIKES = 2次时,暂停并向用户询问是否继续(仅 UI 对话传入askUserForGuard;定时任务与子代理保持仅告警不暂停)。

4.2 重试与错误分类

retry_utils.ts定义了可重试错误的判定与退避策略:

export function isRetryableError(e: Error): boolean { const msg = e.message; return /429|5\d\d|network|fetch|ECONNRESET/i.test(msg) && !/40[0134]/.test(msg); }
  • 匹配429、5xx或网络类信号(network/fetch/ECONNRESET)判为可重试;
  • 同时排除400、401、403、404——注意是这四个具体状态码,而非所有 4xx;
  • withRetry默认最多重试 3 次(maxRetries = 3),指数退避(1000 * 2^attempt + 随机抖动),调用方的AbortSignal触发时立即退出。

classifyErrorCode则把错误归一为结构化错误码:内置受信任码context_too_large/persist_indeterminate/tool_timeout优先,其余按文案匹配context_too_large、rate_limit、auth、tool_timeout,兜底api_error——这样 UI 与自动压缩可以用同一套"上下文超限"处理逻辑响应。

4.3 Provider 归一化

Provider 特定的请求/响应整形位于core/providers/(anthropic.ts、openai.ts、registry.ts),使编排器保持 provider 无关。以上下文输入 token 计算为例,Anthropic 把缓存命中/写入 token 与断点后的input_tokens分开返回,而 OpenAI 的prompt_tokens已含缓存部分,因此getContextInputTokens对 anthropic 做了单独累加。

5. 三种运行生命周期:后台会话、子代理、定时任务

5.1 后台会话(Background session)

BackgroundSessionManager维护一个RunningConversation(流式缓冲、已发生的工具调用、待处理的ask_user状态、abort controller),其存在与否不依赖 UI 是否在监听,因此 popup/options 页面可以对同一进行中的会话 attach、detach 再 reattach。handleAttach会先发送sync快照(当前流式内容、pending ask_user、任务列表),再把 UI 连接注册为 listener。状态机细节:stop()只置为cancelling并 abort,不广播终态事件;真正携带累计 usage/耗时的终态事件由 orchestrator 的emitCancelled在 promise 落定后广播。清理带 30 秒延迟窗口(cleanupIfDone),给迟到的重连者留出机会。

5.2 子代理(Sub-agent)

SubAgentService通过与顶层聊天相同的callLLMWithToolLoop契约运行嵌套对话,但通过resolveSubAgentType/getExcludeToolsForType(core/sub_agent_types.ts)解析按类型区分的工具排除列表。内置三种类型:

类型说明工具边界超时
researcher研究型:搜索/抓取/读页面(只读,不操作 DOM)白名单:web_fetch、web_search、get_tab_content、open_tab、list_tabs、close_tab、opfs_*600s
page_operator页面操作:标签导航、页面自动化白名单:get_tab_content、list_tabs、open_tab、close_tab、activate_tab、execute_script、web_fetch、opfs_*600s
general通用:全部工具(默认类型)黑名单:排除ask_user、agent(不能再派生子代理、不能问用户)600s

任务工具(create_task/update_task/list_tasks)对所有子代理类型始终可用,用于与主代理共享任务进度。白名单模式下排除allowedTools之外的所有工具;未知类型名直接抛错而非静默降级(防止攻击者传任意类型名获得更宽权限)。子代理事件通过subAgent字段回标到父会话,父会话的流式状态更新会忽略子代理事件。chat_service.ts中创建子代理时还会组合AbortSignal.any([父信号, AbortSignal.timeout(typeConfig.timeoutMs)])并为其创建完全独立的SessionToolRegistry。

5.3 定时任务(Scheduled task)

AgentTaskService持久化AgentTask定义(AgentTaskRepo)与运行记录(AgentTaskRunRepo),下次触发时间由core/task_scheduler.ts与 pkg/utils/cron 计算。service worker 的chrome.alarmshandler(agentTaskScheduler,在src/app/service/service_worker/index.ts中注册)调用agent.onSchedulerTick(),把到期任务通过与交互式聊天相同的 tool loop驱动执行。AgentTaskRepo使用带 generation/revision 的乐观并发控制(RevisionConflictError),写入经navigator.locks(回退到stackAsyncTask)串行化。

6. 存储选型:按数据形状选择后端

Agent 子系统并不采用单一持久化模式,而是按数据特征匹配 docs/references/architecture-data.md 的选型:

  • Repo<T>(chrome.storage.local):AgentModelRepo(小型配置对象)、AgentTaskRepo(任务定义,见 src/app/repo/agent_task.ts);
  • OPFSRepo(Origin Private File System):AgentChatRepo(会话历史,可能增长很大且持有附件)、AgentTaskRunRepo(任务运行历史)、SkillRepo(技能.md/脚本包);
  • MCPServerRepo(Repo<T>):MCP server 配置。

7. 页面/offscreen/sandbox 委派与权限边界

7.1 用户脚本侧的 CAT.agent API

content 侧 src/app/service/content/gm_api/cat_agent.ts 向用户脚本暴露CAT.agent.*API。ConversationInstance包装一个会话,并分派由调用脚本注册的工具调用 handler。它走的是与传统 GM API 相同的@GMContext.API/@PermissionVerify.API/@grant注册路径,带点号 grant 名与基于connect()的聊天流式传输——具体差异见 docs/references/architecture-gm-api.md。

7.2 DOM 自动化:默认模式 vs 可信模式并非统一开关

DOM 自动化统一由 service worker 中的单个AgentDomService(dom.ts)负责,处理全部动作(navigate、readPage、screenshot、click、fill、scroll、waitFor、executeScript、标签页监控),并在需要chrome.debugger时委托给从dom_cdp.ts导入的 CDP 辅助函数(cdpClick、cdpFill、cdpScreenshot、cdpStartMonitor/cdpStopMonitor/cdpPeekMonitor)。dom_cdp.ts是dom.ts调用的辅助模块,不是拥有独立请求路径的服务。

关键点:默认 vs 可信的拆分在不同动作上并不一致,需要逐个核对:

  • 导航与标签页簿记(navigate、update、create、query):永远走chrome.tabs,绝不走 CDP;
  • click/fill:真正按调用方的trusted选项分支——默认模式用chrome.scripting.executeScript驱动;"trusted" 模式委托 CDP 产生真实合成输入(isTrusted: true),CDP 调用失败时回退到非 trusted 路径;
  • screenshot:与trusted标志无关,有独立逻辑——selector限定区域的截图永远走 CDP;后台(非激活)标签优先 CDP、失败回退chrome.tabs.captureVisibleTab;无 selector 的激活标签直接用chrome.tabs.captureVisibleTab;
  • 标签页监控(startMonitor/stopMonitor/peekMonitor):无条件走 CDP,根本不存在非 CDP 路径。

CDP 会向标签附加 debugger,随之带来chrome.debugger的额外权限与用户可见横幅影响;其适用范围取决于具体动作,而不是一个二元的"默认/可信"总开关。navigate还通过dom_policy.ts的assertDomUrlAllowed校验目标 URL 是否命中黑名单。

7.3 OPFS 与技能脚本

  • OPFS 访问按调用方分派:AgentOPFSService.handleOPFSApi检查请求是否携带sender(content 脚本,不支持 Blob)还是经postMessage到来(offscreen,支持 Blob),据此调整行为,而不是假设单一执行上下文;
  • 技能脚本通过core/skill_script_executor.ts执行,与普通后台/定时脚本相同的方式委托给 Sandbox(见 docs/references/architecture-execution.md)——Agent 子系统不引入并行的脚本执行路径。

8. 测试约定

service_worker/下的测试文件名并不都与源文件一一对应,部分按行为分组:

  • background.test.ts覆盖background_session_manager.ts;
  • retry.test.ts覆盖retry_utils.ts;
  • autocompact.test.ts覆盖压缩触发路径。

因此某个<source>.test.ts缺失并不代表覆盖缺口。当前清单可用git ls-tree --name-only HEAD src/app/service/agent/service_worker/ | grep test验证;core/遵循同目录*.test.ts约定。整体 Vitest 约定见 docs/references/develop-testing.md。

9. 扩展 Agent 子系统:三条标准路径

  • 新增工具:放在core/tools/下,以合适的ToolSource注册(builtin在启动期,session在相关服务的会话装配中),并实现ToolExecutor。不要把会话级工具注册到全局ToolRegistry——使用SessionToolRegistry,否则并发会话会互相覆盖闭包;
  • 新增 MCP 后端工具:走MCPService而非手动注册。它已处理连接、命名(mcp_<server>_<tool>格式,源码中为mcp_${safeName}_${encodedId}_${toolName},用码点编码 server id 避免a-b/a_b碰撞)与断开清理(unregisterBySource("mcp")批量注销);
  • 新增子代理类型:扩展core/sub_agent_types.ts的SUB_AGENT_TYPES注册表,配置白名单/黑名单与systemPromptAddition角色说明,而不是在SubAgentService里写特判。

结语

ScriptCat 的 Agent 子系统是一套精心设计的组合式架构:AgentService只做装配,十个窄服务各司其职;全局ToolRegistry承载进程级工具,SessionToolRegistry保证并发会话隔离;ToolLoopOrchestrator以注入依赖的方式驱动流式调用、指数退避重试、80% 上下文触发压缩与工具轮原子提交;后台会话、子代理与定时任务共用同一条 tool loop。理解这些边界(尤其是默认/可信模式的非统一拆分与持久化"三态"判定),是安全、正确地扩展 ScriptCat Agent 能力的起点。

  • 前端
  • 开发者工具
  • 插件系统

【免费下载链接】scriptcat

ScriptCat, a browser extension that can execute userscript; 脚本猫,一个可以执行用户脚本的浏览器扩展

项目地址:https://gitcode.com/gh_mirrors/sc/scriptcat
点击查看免费下载

相关推荐

上一篇:开源语音识别工具 AsrTools:一键实现高效音频转字幕的智能解决方案
下一篇:终极杀戮尖塔模组管理器:ModTheSpire完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:26:14

linux-command 仓库详解 Linux tee 命令:标准输出与文件双写实战指南

文档教程 【免费下载链接】linux-command Linux命令大全搜索工具&#xff0c;内容包含Linux命令手册、详解、学习、搜集。https://git.io/linux 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/linux/linux-command 点击查看 免费下载 tee 是 GNU coreutils 中一个…

作者头像 李华
网站建设 2026/10/3 8:21:37

Playnite:三步跑通多平台游戏库管理

Playnite&#xff1a;三步跑通多平台游戏库管理 【免费下载链接】Playnite Video game library manager with support for wide range of 3rd party libraries and game emulation support, providing one unified interface for your games. 项目地址: https://gitcode.com/…

作者头像 李华