其他
在构建或扩展基于 WebGPU 的设备端 Gemma 4…
当要在 Chrome 扩展中通过 WebGPU 本地运行 Gemma 4 并让 AI 代理调用页面 DOM/浏览器工具时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 39/100 ×0.15
适用场景
在构建或扩展基于 WebGPU 的设备端特定
解决思路
- 按四层架构组织代码:`offscreen/` 负责通过 `@huggingface/transformers` 加载 ONNX 模型、运行 agent loop 并流式输出 token;`background/` 作为 Service Worker 路由消息并处理 `take_screenshot` 与 `run_javascript`;`content/` 注入 shadow DOM 聊天 UI 并执行 DOM 工具;`agent/` 作为零依赖模块定义 `ModelBackend` 与 `ToolExecutor` 接口,可抽成独立库。
- 安装依赖:先准备 Node.js 18+ 与 pnpm,然后运行 `pnpm install`。
- 开发构建:运行 `pnpm build` 生成带日志和 source maps 的开发版本。
- 生产构建:运行 `pnpm build:prod` 生成仅报错、压缩的生产版本。
- 加载扩展:打开 `chrome://extensions`,启用 Developer mode,点击 Load unpacked,选择 `.output/chrome-mv3-dev/`。
- 模型下载:首次打开聊天时自动下载 `onnx-community/gemma-4-E2B-it-ONNX`(约 500 MB,默认)或 `onnx-community/gemma-4-E4B-it-ONNX`(约 1.5 GB);首次运行后模型缓存在浏览器 cache storage。
- 定义 `ModelBackend` 接口:`generate(messages: ChatMessage[], tools: ToolDefinition[], options: GenerateOptions): AsyncGenerator<StreamChunk>`。
- 定义 `ToolDefinition`:包含 `name`、`description`、`parameters`(JSONSchema)。
- 定义 `GenerateOptions`:包含 `maxNewTokens?`、`thinking?`。
- 定义 `ToolExecutor`:`execute(toolName: string, args: Record<string, unknown>): Promise<unknown>`。
- 实现 agent loop:`runAgentLoop(userMessage, history, model, tools, toolDefs, maxIterations)`,初始化 messages 为 history 加 user message,最多迭代 `maxIterations` 次;遍历 `model.generate` 输出;遇到 `token` 事件则 yield token;遇到 `tool_call` 则 yield `tool_start`,调用 `tools.execute`,yield `tool_result`,并把 `{ role: "tool", name, content: String(result) }` 推入 messages;遇到 `done` 则返回。
- 内置工具 `read_page_content`:在 content script 中读取页面文本/HTML 或指定 CSS selector。
- 内置工具 `take_screenshot`:在 Service Worker 中截图当前可见标签页为 PNG。
- 内置工具 `click_element`:在 content script 中按 CSS selector 点击元素。
- 内置工具 `type_text`:在 content script 中按 CSS selector 向输入框输入文本。
- 内置工具 `scroll_page`:在 content script 中按像素滚动页面。
- 内置工具 `run_javascript`:在 Service Worker 中于页面上下文执行 JS。
- 新增工具时同时修改两个位置:定义放在 offscreen agent,执行器放在 content script 或 Service Worker。
- Step 1 定义工具 schema:在 `offscreen/tools/definitions.ts` 导出 `MY_TOOL_DEFINITION: ToolDefinition`,填写 `name`、`description`、`parameters`(如 `{ type: "object", properties: {}, required: [] }`)。
- Step 2 注册工具:在 `offscreen/tools/index.ts` 导入定义,并把定义加入 `ALL_TOOLS: ToolDefinition[]` 数组。
- Step 3 在 content script 实现执行:在 `content/tools/executor.ts` 的 `executeContentTool(name, args)` 中用 `switch` 增加 case;例如 `get_page_title` 返回 `document.title`;`read_page_content` 若有 `selector` 则返回 `document.querySelector(selector)?.textContent ?? "Not found"`,否则返回 `document.body.innerText`;`click_element` 查询 selector,找不到抛错,找到则 `el.click()` 返回 `"clicked"`;`type_text` 查询 input,找不到抛错,找到则 `focus()`、设置 `value`,并 dispatch `input` 和 `change` 事件(`bubbles: true`),返回 `"typed"`;默认 case 抛 `Unknown content tool`。
- Step 4 处理 Service Worker 侧工具:在 `background/tools.ts` 的 `executeSwTool(name, args, tabId)` 中用 `switch`;`take_screenshot` 调用 `chrome.tabs.captureVisibleTab({ format: "png" })` 并返回 dataUrl;`run_javascript` 调用 `chrome.scripting.executeScript({ target: { tabId }, func: new Function(args.code as string) as () => unknown })` 并返回 `results[0]?.result ?? null`;默认返回 `null` 以转发给 content script。
- 消息路由:Service Worker 作为消息总线,所有通信使用 `chrome.runtime.sendMessage`。
- 定义消息类型 `ExtMessage`:联合类型包含 `{ type: "TOOL_CALL"; name; args; tabId }`、`{ type: "TOOL_RESULT"; name; result }`、`{ type: "TOKEN"; token }`、`{ type: "AGENT_DONE" }`、`{ type: "AGENT_ERROR"; error }`。
- Offscreen → Service Worker:用 `chrome.runtime.sendMessage<ExtMessage>({ type: "TOOL_CALL", name, args, tabId: currentTabId })`。
- Service Worker → Content script:用 `chrome.tabs.sendMessage<ExtMessage>(tabId, { type: "TOOL_CALL", name, args, tabId })`。
- 配置模型加载:在 `offscreen/model.ts` 从 `@huggingface/transformers` 导入 `pipeline` 和 `TextGenerationPipeline`。
- 定义模型 ID:`MODEL_IDS` 中 `E2B: "onnx-community/gemma-4-E2B-it-ONNX"`、`E4B: "onnx-community/gemma-4-E4B-it-ONNX"`;`ModelSize = keyof typeof MODEL_IDS`。
- 加载模型:实现 `loadModel(size, onProgress)`,调用 `pipeline("text-generation", MODEL_IDS[size], { dtype: "q4f16", device: "webgpu", progress_callback: (p) => onProgress(p.progress) })`,返回 `TextGenerationPipeline`。
- 设置持久化:使用 `chrome.storage.sync` 存储设置。
- 定义 `GemmaGemSettings`:包含 `modelSize: "E2B" | "E4B"`、`thinking: boolean`、`maxIterations: number`、`disabledHosts: string[]`。
- 定义 `DEFAULT_SETTINGS`:`{ modelSize: "E2B", thinking: false, maxIterations: 10, disabledHosts: [] }`。
- 实现 `getSettings()`:从 `chrome.storage.sync.get("settings")` 读取,并与 `DEFAULT_SETTINGS` 合并返回。
- 实现 `saveSettings(patch)`:先 `getSettings()`,再 `chrome.storage.sync.set({ settings: { ...current, ...patch } })`。
- 实现按站点禁用:`disableOnCurrentSite()` 用 `new URL(location.href).hostname` 取 host;若 `disabledHosts` 不含该 host,则 `saveSettings({ disabledHosts: [...settings.disabledHosts, host] })`。
- Shadow DOM 聊天 UI:在 `content/ui.ts` 实现 `injectChatOverlay(): ShadowRoot`。
- 注入步骤:创建 `div#gemma-gem-host`;调用 `host.attachShadow({ mode: "closed" })` 隔离样式;创建 `style` 并设置 `textContent = CHAT_STYLES`;创建 `div#gemma-gem-container`;把 style 和 container 加入 shadow;把 host 加入 `document.body`;返回 shadow。
- 调试日志:所有日志使用 `[Gemma Gem]` 前缀;开发构建记录 info/debug/warn,生产构建只记录 errors。
- 查看 Service Worker 日志:`chrome://extensions` → Gemma Gem → "Inspect views: service worker"。
- 查看 offscreen 日志:`chrome://extensions` → Gemma Gem → "Inspect views: offscreen.html"。
- 查看 content script 日志:任意页面 DevTools → Console,过滤 `[Gemma Gem]`。
- 查看所有扩展上下文:`chrome://inspect#other`。
- Offscreen 日志重点检查:模型下载进度、完整 prompt 构造、每轮 token 数、解析工具调用前的原始模型输出、工具执行结果。
- WebGPU 可用性检查:若 `!navigator.gpu` 则抛错 `"WebGPU not supported. Use Chrome 113+ with hardware acceleration enabled."`;再 `const adapter = await navigator.gpu.requestAdapter()`,若无 adapter 则抛错 `"No WebGPU adapter found."`。
- 处理 offscreen document 生命周期:Chrome 可能挂起 offscreen document,发送消息前先 ping;实现 `ensureOffscreen()`:用 `chrome.offscreen.hasDocument()` 检查,若不存在则 `chrome.offscreen.createDocument({ url: "offscreen.html", reasons: [chrome.offscreen.Reason.WORKERS], justification: "Run Gemma 4 inference via WebGPU" })`。
- 管理上下文窗口:Gemma 4 支持 128K tokens 但长上下文会变慢;用 `clear_context` 按页清历史,或限制存储轮数;设置 `MAX_HISTORY_TURNS = 20`;实现 `trimHistory(messages)`:若 `messages.length <= MAX_HISTORY_TURNS * 2` 则原样返回,否则返回 `messages.slice(-MAX_HISTORY_TURNS * 2)`。
- 解析工具调用:Gemma 4 以结构化格式发出工具调用;添加自定义解析时要防流式 partial JSON;实现 `safeParseToolCall(raw)`:`try { return JSON.parse(raw); } catch { return null; }`(仍在流式输出)。
- DOM 工具 CSS selector 安全:实现 `safeQuerySelector(selector)`:`try { return document.querySelector(selector); } catch { return null; }`,防止模型给出非法 selector。
适用边界 · 注意事项
- 浏览器不支持 WebGPU 或未开启硬件加速(需要 Chrome 113+)时,不适合使用此方案。
- 页面属于敏感站点、不允许注入脚本/执行 JS/截图,或用户不希望扩展操作 DOM 时,不应启用或应加入 `disabledHosts`。
- 无法使用 Node.js 18+/pnpm,或不愿首次下载约 500 MB~1.5 GB 本地模型时,不适合构建/运行。