ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

浏览器扩展端侧AI推理:五层架构与Manifest V3工程实践全解

浏览器扩展端侧AI推理:五层架构与Manifest V3工程实践全解 说实话我第一次在浏览器扩展里跑通端侧 AI 推理时脑子里最先冒出来的不是“性能还行”而是“这堆线程到底归谁管”。如果你也研究过“浏览器扩展 端侧 AI 推理 系统架构”这几个词你会发现大多数资料都在讲模型怎么选、推理框架怎么调很少有人认真把浏览器扩展这套运行环境本身的约束讲透。这篇内容就是想把我在实际项目中踩过的那些架构坑、工程规范坑以及最终沉淀下来的一套可落地实现完整分享出来。先说清楚本文要解决什么问题我们做的不是“网页版 AI 应用”而是真正部署在 Chrome/Edge 扩展里、不依赖云端的端侧 AI 推理系统。它要满足三个硬性条件——模型跑在用户本地、推理过程中页面不卡顿、扩展本身符合 Manifest V3 的规范。适合正在做浏览器 AI 扩展、或者打算把已有 Web 端 AI Demo 搬进扩展体系里的人参考。1. 为什么端侧推理要单独设计一套扩展架构这个话题得从“普通 Web 页面做 AI 推理”和“浏览器扩展做 AI 推理”的差异说起。很多人在页面里跑过 Transformers.js 或者 ONNX Runtime Web模型加载、推理、输出结果链路非常顺。但在扩展环境里同样的代码会突然冒出各种奇怪问题后台 Service Worker 被浏览器休眠了、内容脚本无法访问隔离世界的模型对象、扩展的 CSP 把远程脚本直接拦掉、用户点开弹窗时模型还在下载……1.1 扩展本质上是一个“多容器”运行环境很多人把扩展当成一个网页来写这是第一层认知错误。现代浏览器扩展至少包含四个独立执行容器容器运行环境典型用途Service WorkerMV3 后台线程消息中转、扩展生命周期管理Content Script页面隔离世界读取页面 DOM、注入 UIPopup / Options扩展页面用户交互界面Offscreen 页面隐藏扩展页承载无法在后台执行的场景端侧 AI 推理介入之后这个模型还要再加两层用于跑模型的独立 Worker 线程以及负责模型元数据和权重文件的缓存层。容器一多“架构”就不是设计感的问题而是活命问题。你如果不在最开始设计清楚消息流和职责边界后面每一次加功能都会引发连锁踩雷。1.2 端侧推理给扩展带来的四个新问题长时间计算的生存问题推理可能持续几秒甚至几十秒而 MV3 的 Service Worker 可能在空闲 30 秒后被回收。模型加载完正准备推理线程没了这是最痛苦的局面。主线程卡顿问题扩展的 UI 和页面消息处理都在浏览器主线程上排队推理如果直接塞进 Popup 或后台用户会明显感觉到扩展“死了”。模型文件获取问题扩展的 CSP 默认限制远程脚本执行但模型权重文件通常几十 MB 以上。到底打包进扩展还是运行时拉取需要单独决策。多入口状态一致问题内容脚本、Popup、后台都可能发起推理请求它们之间怎么传输文本、怎么拿结果、怎么处理并发必须有一套统一消息协议。这套问题组合起来已经不是“调一个模型 API”能解决的必须从系统架构层面来定规范。这也是为什么同样是用 Transformers.js浏览器页面里跑和扩展里跑完全是两个工程量级。2. 五层架构把推理能力从扩展壳子里解耦出来我最终沉淀下来的方案是下面这个五层架构。它本质上是把“模型推理”当成一个独立的后台服务来对待扩展的 UI、消息、存储都只是它的上下游。2.1 架构分层清单采集层Content Script。负责从当前页面提取文本、图片或选中内容也负责在页面上渲染推理结果。通信层Service Worker。不承担任何推理计算只做消息路由、扩展状态维护、唤醒 Offscreen 页面。执行层Offscreen 页面 独立 Worker。真正加载模型、执行推理的容器。Worker 负责计算Offscreen 页面负责桥接消息。模型层模型权重文件、分词器、配置文件。可以打包进扩展也可以由 Worker 运行时拉取后缓存。展示层Popup、SidePanel、Content Script 注入的 UI。负责把结果呈现给用户。这套分层的核心原则只有一条计算必须远离 UI也必须远离易被回收的后台线程。最稳妥的做法是把所有重计算放到由 Offscreen 页面创建的 Worker 里。Offscreen 页面作为扩展页面生命周期比 Service Worker 稳定Worker 又能提供独立线程两者结合刚好补齐短板。2.2 消息流的走向必须固定扩展里最忌讳的就是每个组件各自发消息、各自等结果。我统一把消息流固定成单行道用户选择文本 → Content Script 发送“AI_REQUEST” → Service Worker 确认 Offscreen 已就绪 → 转发给 Offscreen 页面 → Offscreen 将任务交给 Worker → Worker 加载模型并推理 → 结果原路返回。这个链路初看多跳了一次但好处非常明显。Service Worker 可以统一做权限校验、请求去重、失败重试Offscreen 可以统一维护 Worker 的连接状态Content Script 不需要知道模型层任何细节。后续如果要把推理能力扩展到翻译、分类、做词云都只是加任务类型架构不用动。2.3 一个容易被忽略的小事Offscreen 页面的复用Chrome 的 Offscreen 页面数量有限同一时刻最多只能有几类场景存在。所以我只在后台需要执行推理任务时才创建 Offscreen 页面任务全部结束后再考虑关闭。这个开关逻辑放在 Service Worker 里需要用chrome.offscreen.hasDocument()判断当前是否存在避免重复创建。3. Manifest V3 的硬约束直接影响模型投放和运行方式讨论端侧推理架构之前必须先认清一个现实MV3 已经不是“推荐规范”了它就是你绕不开的执行环境。Chrome 早就不再加载 MV2 扩展很多 MV3 的限制也不是权限声明那么简单它们会直接改变推理系统的运行逻辑。3.1 Service Worker 的“短命”特性决定了职责上限MV3 把之前的常驻后台页面换成了 Service Worker并明确要求“在可能的情况下保持非活动状态”。浏览器大概率会在扩展空闲一段时间之后把它回收掉。如果你想当然地让 Service Worker 持有模型实例、持有着模型加载进度、持有推理中间态那么一旦被回收全部状态清零。我见过最多的一种翻车现场用户第一次点开 Popup 发起摘要请求Service Worker 被唤醒后开始加载模型刚加载到一半浏览器因为内存压力收掉了 Service Worker。下次点击又从零开始永远看不到结果。所以在我的架构规范里Service Worker 被默认视为“无状态转发层”。它可以有路由信息可以维护一个简单的任务队列但绝不能持有庞大的模型对象。复杂的推理状态要么放在 Offscreen 页面里要么通过chrome.storage.session短暂落盘。3.2 CSP 限制了远程代码但不限制远程权重MV3 默认的扩展 CSP 是script-src self; object-src self。这句话翻译成人话就是你可以通过 fetch 拉取数据文件但不能执行 CDN 上的 JS 脚本。很多 AI 推理库为了方便浏览器端使用直接提供 CDN 版脚本页面里一个script标签就能加载。但你在扩展的 Offscreen 页面或者 Worker 里这样干会被 CSP 直接拦截。正确姿势是推理库的 JS 文件必须打包进扩展本地通过importScripts(chrome.runtime.getURL(vendor/transformers.min.js))或者 ES Module 方式加载。但模型权重属于“数据”不受执行限制。Transformers.js 这类库运行时照样能从 HuggingFace 等模型仓库 fetch ONNX/GGUF 权重文件。所以 MV3 下比较合理的实践是推理框架代码本地打包模型权重按需拉取并缓存。当然如果你做的是隐私敏感型扩展完全可以把小模型也打进安装包里只是安装体积会直接膨胀几十 MB。3.3 浏览器版本、API 能力与模型 dType 要一起对齐端侧推理对浏览器版本非常敏感。WebGPU 在 Chrome 113 之后稳定可用WebNN 的扩展支持更晚。你如果目标用户用的是旧版 Chrome就必须在 Worker 里做能力探测拿不到 WebGPU 就回退到 WASM 推理。我在 manifest.json 里通常会写minimum_chrome_version: 116同时在实际代码里通过navigator.gpu判断是否启用 WebGPU 后端。这个版本号不能乱定它直接影响你能否用offscreenAPI、能否用chrome.sidePanel。这些能力在 MV3 体系里不是一个文件能搞定的必须在设计架构前就把最低版本当成一个架构参数来对待。4. 推理引擎选型我实测过的三条路线对比如果只看宣传文档每个推理框架看起来都很美好。但在扩展这个受限环境里选型标准会被重新排序加载方式、线程模型、内存管理、量化支持这些比浮点精度更重要。4.1 三条主流路线的实际表现方案适用场景优点扩展环境痛点点Transformers.jsNLP摘要、翻译、分类等API 友好生态成熟模型覆盖广模型较多需依赖浏览器缓存多线程支持不见得透明ONNX Runtime WebCV、NLP、通用模型WASM/WebGPU 双后端算子覆盖广中间表示转换成本高算子兼容检查需要编译期提前做MediaPipe Tasks视觉、人像、手部检测扩展包小移动端成熟集成简单面向视觉为主文本类模型支持有限我做文本摘要类扩展时首选 Transformers.js。不是因为它最接近 PyTorch 的写法而是因为它在 Worker 里执行时模型加载和推理的状态管理最透明不需要我手写太多底层张量代码。4.2 量化优先级必须前置端侧推理跑不跑得动关键在于模型量化。实测下来同样做英文新闻摘要任务FP16 的 distilbart 模型在普通笔记本上加载后浏览器可能直接占掉 700 MB 以上内存换成 8-bit 量化版内存能压到 300 MB 以内推理耗时反而更稳定。我整理了一个简单的选型经验值300 MB 以上的模型除非用户机器极好否则慎做默认方案应提供量化选项。100 MB 到 300 MB必须量化同时建议懒加载用户第一次触发任务时才下载。100 MB 以下可以打包进扩展体验最稳定。量化也分q8、int8、q4几种。文本类任务我建议至少保持 8-bit 量化4-bit 虽然更省但对摘要、翻译这种长文本任务来说质量损耗经常肉眼可见。4.3 别迷信 GPU 万能论WebGPU 很香但它不是所有模型所有时延场景的银弹。我在扩展里见过一个很夸张的情况某个视觉模型在 WebGPU 后端下推理只要 80 ms但模型加载和管线创建花掉了 2.3 秒反而 WASM 后端推理要 280 ms整体首次体验却更稳。原因在于 WebGPU 需要把权重 copy 到 GPU 显存这个时间在大模型场景下不可忽略。而且扩展环境里 Prewarm 不可能做得太激进你要考虑 Service Worker 被回收、Offscreen 页面被关闭这些现实。所以我对引擎选型的最终建议是不要绑定单一后端架构上留出按设备能力切换的开关。5. 代码级落地一个完整的端侧摘要扩展实现理论说再多都不如一套能跑的代码直观。我用一个最小可用的“端侧文本摘要扩展”作为示例走一遍完整链路。代码里的扩展名不关键你关注的是架构骨架。5.1 项目文件结构my-summary-extension/ ├── manifest.json ├── background.js ├── offscreen.html ├── offscreen.js ├── worker.js ├── content.js ├── popup.html └── popup.js └── vendor/ └── transformers.min.js注意vendor/transformers.min.js是从 Transformers.js 官方仓库下载的本地构建版本。这一步不能省也不能用 CDN 地址代替。5.2 manifest.json{ manifest_version: 3, name: Local Page Summarizer, version: 0.1.0, description: 端侧 AI 推理的浏览器扩展示例, minimum_chrome_version: 116, permissions: [offscreen, storage, scripting], host_permissions: [ https://huggingface.co/*, https://cdn-lfs.huggingface.co/* ], background: { service_worker: background.js }, action: { default_popup: popup.html }, content_scripts: [ { matches: [http://*/*, https://*/*], js: [content.js], run_at: document_idle } ] }几个容易踩的小细节offscreen权限必须显式声明否则chrome.offscreen是 undefinedhost_permissions只给了模型仓库域名而不是all_urls这符合最小权限原则content script 的 match 也没用all_urls避免扩展权限过大被浏览器商店审核盯上。5.3 background.js无状态消息路由let offscreenCreation null; async function ensureOffscreen() { if (await chrome.offscreen.hasDocument()) return; if (!offscreenCreation) { offscreenCreation chrome.offscreen.createDocument({ url: offscreen.html, reasons: [WORKERS], justification: 承载端侧 AI 推理所需的 Worker 线程 }); } try { await offscreenCreation; } finally { offscreenCreation null; } } chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type ! AI_REQUEST) return; ensureOffscreen() .then(() { return chrome.runtime.sendMessage({ target: ai, task: message.task, payload: message.payload }); }) .then((response) sendResponse(response)) .catch((error) sendResponse({ ok: false, error: String(error) })); return true; });这里的核心是return true它告诉浏览器这是异步响应让 sendResponse 可以在模型推理结束后才调用。很多人在这里只写了一部分逻辑忘了 return true结果消息一发出就被当作无响应报错还贼难查。5.4 offscreen.js连接 Worker 与消息系统const worker new Worker(chrome.runtime.getURL(worker.js)); let sequence 0; const pending new Map(); worker.addEventListener(message, (event) { const { requestId, ok, result, error } event.data; const callback pending.get(requestId); if (!callback) return; pending.delete(requestId); if (ok) callback.resolve(result); else callback.reject(new Error(error)); }); function postToWorker(task, payload) { const requestId sequence; worker.postMessage({ requestId, task, payload }); return new Promise((resolve, reject) { pending.set(requestId, { resolve, reject }); }); } chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.target ! ai) return; postToWorker(message.task, message.payload) .then((result) sendResponse({ ok: true, result })) .catch((error) sendResponse({ ok: false, error: String(error) })); return true; });offscreen.js 维护了一个pendingMap用 requestId 把消息和 Promise 对应起来。这样设计是为了后续方便添加超时机制——比如 Worker 卡住时可以定期扫描 pending 里超过 15 秒的请求并直接 reject。5.5 worker.js真正执行端侧推理的位置importScripts(vendor/transformers.min.js); const { pipeline, env } self.transformers; env.allowLocalModels false; env.useBrowserCache true; let summarizer null; self.addEventListener(message, async (event) { const { requestId, task, payload } event.data; try { if (task load) { summarizer await pipeline(summarization, Xenova/distilbart-cnn-6-6, { quantized: true }); postMessage({ requestId, ok: true, result: ready }); return; } if (task summarize summarizer) { const output await summarizer(payload.text, { max_length: payload.maxLength || 128, min_length: payload.minLength || 24 }); postMessage({ requestId, ok: true, result: output[0].summary_text }); return; } postMessage({ requestId, ok: false, error: unsupported task }); } catch (error) { postMessage({ requestId, ok: false, error: String(error) }); } });这个 Worker 只在两种情况下使用加载模型、执行摘要。模型实例summarizer是 Worker 线程内的全局变量只要 Worker 不被销毁二次推理就无需重新加载模型。合理的工程规范是在 Offscreen 页面创建后的第一次消息中强制先发一个load任务完成模型预热之后再回应真正的任务。5.6 content.js采集页面文本let lastSelectedText ; document.addEventListener(mouseup, () { const selection window.getSelection(); if (!selection || selection.isCollapsed) return; lastSelectedText selection.toString().trim(); if (lastSelectedText.length 20) { chrome.storage.session.set({ lastSelectedText }); } }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type GET_SELECTED_TEXT) { sendResponse({ text: lastSelectedText }); } });这里用chrome.storage.session保存最近选中的文本比让 Popup 自己向 Content Script 发消息查询更可靠。Popup 打开时直接读取 session storage不会因为 Content Script 注入时机问题而拿不到数据。5.7 popup.js一行请求触发推理async function runSummary() { const { lastSelectedText } await chrome.storage.session.get(lastSelectedText); if (!lastSelectedText) { document.querySelector(#result).textContent 请先在页面上选中文本; return; } const response await chrome.runtime.sendMessage({ type: AI_REQUEST, task: summarize, payload: { text: lastSelectedText } }); document.querySelector(#result).textContent response.ok ? response.result : 推理失败: response.error; }整个请求链路到这里就闭环了。要注意的是chrome.runtime.sendMessage在 MV3 中是 Promise 化的但如果你在 background 的监听函数里不写return true这里拿到的 response 就会是 undefined。这个坑值得单独记一下。6. 性能调优的落地点从首次加载到后续复用的几个关键指标架构跑通只是第一步真正让用户觉得“还行”的是性能体验。在端侧推理扩展里我永远盯着三个数字首次加载时间、二次推理时间、内存占用。6.1 用低优先级模型做首次预热用户第一次点击摘要按钮时我不想让他干等 8 秒。所以我在 Offscreen 页面创建后先不急着让用户等而是由 background 在后台发一个load任务让 Worker 立刻开始拉模型、建 pipeline。这个预热动作是独立的就算用户没在 Popup 界面模型也已经加载好了。用户真正发请求时很大概率直接走二次推理路径响应时间可能只有几百毫秒。这种预热方案有个前提你必须控制好预热的粒度。模型下载动辄几十 MB如果用户根本不会用到摘要功能白白浪费流量。所以我的实现里加了一个开关只有当用户主动在 Popup 里选择过模型或者打开过扩展页面时才允许做预热。6.2 模型缓存不能只靠浏览器默认行为Transformers.js 默认会把模型文件缓存到 Cache Storage 或者 IndexedDB但扩展环境里的缓存行为有时候并不稳定。Service Worker 被回收、浏览器清理站点数据、扩展版本更新都可能让缓存失效。我的工程规范是在第一次成功加载模型后把模型的版本号、量化精度、文件字节数记录到chrome.storage.local。后续每次调度前先做一次快速校验发现模型文件的字节数和版本不匹配就直接清掉缓存重新拉取。这样可以避免“缓存坏了但没坏透”的混沌局面——那种情况的表现是加载不报错、推理输出乱码排查起来极其费时间。6.3 同一时间只跑一个推理任务虽然 Worker 是独立线程但端侧推理的瓶颈通常在内存带宽和算子调度而不是线程数。我有段时间放开并发允许用户连续触发多条摘要任务结果模型在一个任务里刚计算到一半另一个任务的张量又挤进来显存直接被打满浏览器干脆整个标签页崩溃。最后我把任务并发收紧为单队列。新请求进来时如果 Worker 正在推理直接进入等待队列当前任务结束后再轮到下一条。实测对这种文本摘要场景来说单队列的吞吐反而更高也更稳。不同任务之间的等待时间通过 UI 上显示一个“推理中”状态来消解比让用户看到一条错误要体面得多。6.4 内存释放不要走极端端侧推理里“释放”和“卸载”是两个概念。模型加载完毕后即使不再使用保持summarizer引用也不会主动释放太多内存因为推理引擎内部有缓存池。但每次推理后临时生成的张量数据最好在结果 postMessage 出去之后立刻被垃圾回收。一个简单的优化在 Worker 里推理结束后把不再需要的上下文变量赋值为null避免闭包持有大量中间张量。如果你发现扩展在多次推理后内存持续上涨优先检查是不是旧请求的 Promise 回调链还没有释放而不是怀疑模型本身。7. 调试这类扩展时最容易“鬼打墙”的几个环节扩展 AI 功能的调试体验比普通网页差一大截因为你有四个以上独立的 console分布在不同的执行上下文里。肉眼排查基本失效必须用系统方法。7.1 先确认消息是否被正确响应一个很经典的鬼打墙Popup 里发起消息等了几秒没有返回也没报错。这种问题 80% 出在 background 监听函数没有正确使用return true。剩余 20% 出在消息字段被多层 listener 抢答了。我的调试做法是先给每条消息打上唯一requestId在 background、offscreen、worker 三处分别打印日志能一眼看出请求卡在哪一层。注意日志不要只用console.log要加上前缀标记比如[bg]、[offscreen]、[worker]因为多个 console 页面里日志不会自动区分来源。7.2 用 chrome://serviceworker-internals 检查后台状态如果你怀疑是 Service Worker 被回收打开chrome://serviceworker-internals/比想象中好用。那里能看到扩展身份 ID、状态、最后活跃时间。我一般在这种环境下做测试的方法是发起一次推理请求等结果返回。在 Popup 里什么都不做等 30 秒以上。回到 Popup 再发起一次请求观察第二次请求是否触发重新加载模型。如果第二次请求明显变慢说明 Service Worker 被回收后整个 Offscreen 页面和 Worker 也被一起干掉了。解决方案是确保 background 的ensureOffscreen()逻辑足够幂等无论 Offscreen 是否存在都能在任意时刻重建。7.3 模型加载慢不等于模型没在执行端侧推理最耗时的环节很多时候不是推理本身而是模型权重下载。你用 Transformers.js 时如果看到 worker 日志里长时间没有消息先检查 Network 面板里是否有来自cdn-lfs.huggingface.co的请求在传输。模型文件大到一定程度浏览器需要做分片下载这期间看起来像卡死其实只是进度条没有暴露出来。为了体验我通常在 Popup 里展示一个简单的状态加载模型 x%。Transformers.js 的进度事件可以通过回调拿到虽然示例代码里没展示但这在生产环境里几乎是必需品。7.4 扩展更新后的“残留 Worker”问题开发调试时改完代码重新加载扩展经常出现 Offscreen 页面还是旧版本、Worker 还是旧脚本的情况。这时候最彻底的办法是在扩展管理页点击“重新加载”再清理一次chrome.storage.session。工程规范上我建议每次扩展版本升级时用chrome.runtime.onInstalled事件把旧模型缓存、旧消息队列全部清一遍避免新旧代码状态不兼容。8. 工程实现规范哪些规矩必须从第一天就立下写到这里架构和实现细节都讲完了最后想整理几条我踩过很多次坑后立下来、并且认为有普适价值的工程规范。如果你打算把这套东西做成长期维护的项目这几条值得直接抄进团队规范里。8.1 明确定义消息协议禁止乱发自定义字段所有扩展内部消息统一使用typetaskpayload结构。type表示这是哪一类消息比如AI_REQUESTtask表示具体任务比如summarize、translatepayload是业务数据。禁止任何组件直接给对方发“裸字段”比如text: xxx这种没有上下文的写法。原因很简单扩展组件会越来越多消息协议只要有一个人图省事后面的排查成本就会指数增长。统一协议形状之后你才能在上面做全局拦截、打日志、做权限校验。8.2 模型仓库地址和模型版本要固化不要用类似Xenova/distilbart-cnn-6-6这样的字符串散落在代码里。应该在扩展根目录放一个constants.js集中管理export const MODELS { SUMMARIZATION: { id: Xenova/distilbart-cnn-6-6, revision: main, cacheVersion: 1.0.0, quantized: true } };模型仓库里的文件一旦被重置或者上游更新了权重文件而你的代码还假设它是旧的二进制格式就会出现推理结果莫名其妙变差的情况。固定的 revision 加 cacheVersion 字段是端侧模型服务化最基本但最有效的保障。8.3 扩展 UI 永远不能直接感知模型层细节Popup 和 Content Script 只允许知道“我发起了请求、我在等待结果、结果成功或失败”。至于模型是 8-bit 还是 16-bit、后端是 WebGPU 还是 WASM、请求被转发了几轮这些细节不许出现在 UI 代码里。UI 和模型层之间靠背景消息层做隔离即可。我甚至建议你做一个统一的“推理状态机”idle、loading、ready、running、error。整个扩展只是这个状态机的外壳任何 UI 组件发生变化只更新状态不直接操作 Worker。这一层抽象在项目早期看起来有点“过度设计”但一旦你要再加入一个新模型、新任务你就知道它替你省了多少事。8.4 谨慎对待扩展商店的审核边界端侧推理是本地能力不涉及远程代码执行这是合规上的优势。但要注意两点一是host_permissions不要随手写成all_urls因为你可能并不需要访问每个域名二是模型下载属于网络行为要在隐私说明里写明“模型权重由浏览器向模型仓库发起请求并缓存于本地不上传用户内容”。写在最后的实际操作体会我真正把浏览器扩展 端侧 AI 推理跑进生产环境之后最大的感受是模型选型和精度调优其实只占了工作量的一部分更大的精力被消耗在理解浏览器扩展自身的生命周期、消息限制和状态管理上。不要让“跑通 Demo”的兴奋掩盖了架构规范的缺失。建议你从最小闭环开始——只支持一种任务、一个模型、一条消息流——把它跑稳然后再逐步加并发、加模型、加 UI。先把 Service Worker 的短命问题、Offscreen 的重建问题、Worker 的队列问题这三件基础事解决掉端侧 AI 扩展的骨架就稳了后面的一切都只是往里填肉。
返回列表