- 前端
- 开发者工具
- 插件系统
【免费下载链接】scriptcat
ScriptCat, a browser extension that can execute userscript; 脚本猫,一个可以执行用户脚本的浏览器扩展
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 三元组。完整清单如下:
| 服务 | 文件 | 职责 |
|---|---|---|
ChatService | chat_service.ts | 聊天请求生命周期:构建 system prompt、为每个请求装配SessionToolRegistry、把 tool loop 委托给编排器 |
AgentTaskService | task_service.ts | AgentTask的 CRUD 与调度(类 cron 触发器),通过同一 tool-loop 编排器运行任务 |
SkillService | skill_service.ts | 从.md或.zip源安装/更新/列出技能(parseSkillMd/parseSkillZip),由SkillRepo持久化 |
AgentModelService | model_service.ts | 模型配置 CRUD,以及默认/摘要模型选择,由AgentModelRepo持久化 |
MCPService | mcp.ts | 按配置管理MCPClient连接,并把工具注册/注销到共享ToolRegistry |
BackgroundSessionManager | background_session_manager.ts | 跟踪后台运行中的对话(流式状态、listener、待处理的ask_user提问),供 UI 重新附加到进行中的会话 |
SubAgentService | sub_agent_service.ts | 通过共享 tool loop 运行子代理对话,带按类型区分的工具排除列表 |
CompactService | compact_service.ts | 用专门的 compact prompt 对长对话历史做摘要压缩 |
AgentDomService | dom.ts(+dom_cdp.ts辅助) | 页面自动化,见下文"默认模式 vs 可信模式"的拆分 |
AgentOPFSService | opfs_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; 脚本猫,一个可以执行用户脚本的浏览器扩展
相关推荐
Agent Tool Registry (ATR) 深度解析:用 Python 构建类型安全、去中心化的 Agent 工具注册表
Agent Tool Registry ATR 深度解析:用 Python 构建类型安全、去中心化的 Agent 工具注册表 ATR(Agent Tool Re
人工智能AI AgentAI 安全治理策略引擎认证鉴权Agent 沙箱可观测性OpenHuman 工具层深度解析:Tool 特质、默认注册表与内置工具体系
OpenHuman 工具层深度解析:Tool 特质、默认注册表与内置工具体系 本篇文章基于开源项目 OpenHuman(面向 Mac、Windows 与 Lin
人工智能AI 应用本地部署AI Agent交互助手深度研究Kilo 核心工具架构解析:Tool 表示、Location 注册与结算机制深度指南
Kilo 核心工具架构解析:Tool 表示、Location 注册与结算机制深度指南 导读 本文以 packages/core/src/tool/AGENTS.
人工智能大模型AI Agent代码智能体工具调用交互助手CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考