Electron localAIHandler 深度解析:把页面 Prompt API 代理到本地 LLM 的 Utility 进程机制
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
Electron 提供了localAIHandler模块,运行在 Utility 进程中,其核心职责是把渲染进程发出的浏览器内建 AI(Prompt API)请求,代理到你自行实现的本地大语言模型后端。读完本文,你将理解session.registerLocalAIHandler(handler)与localAIHandler.setPromptAPIHandler(handler)的完整调用链、LanguageModelUtility的接口契约(create、availability、prompt、append、measureContextUsage、clone、destroy),以及请求排队、按webContentsId + securityOrigin去重、返回null拒绝建会话等行为细节,从而能落地一个可运行的本地 AI 桥接层。
模块定位与运行进程
根据 localAIHandler 文档,该模块属于Utility 进程(参见 进程模型术语表),并且明确说明:
This module is intended to be used by a script registered to a session via
ses.registerLocalAIHandler(handler)
也就是说,localAIHandler不是浏览器主进程(Browser)API,也不是渲染进程(Renderer)API——它的宿主是一个通过Session.registerLocalAIHandler()注册到某个Session上的UtilityProcess实例所加载的脚本。这个设计把本地推理(往往计算密集、甚至需要加载本地模型权重)隔离在独立进程中,避免阻塞浏览器主进程,也避免让模型权重进入渲染进程的可信域。
注册入口在 session 文档 中定义:
ses.registerLocalAIHandler(handler) _Experimental_ * handler UtilityProcess | nullRegisters a local AI handler
UtilityProcess. To clear the handler, callregisterLocalAIHandler(null), which will disconnect any existing Prompt API sessions and destroy anyLanguageModelUtilityinstances.
在主进程侧的实现位于 Session.prototype.registerLocalAIHandler:
Session.prototype.registerLocalAIHandler = function (handler: UtilityProcess | null) { // ... return this._registerLocalAIHandler(handler !== null ? (handler as any)._unwrapHandle() : null); };从源码结构看,主进程把UtilityProcess的内部 handle 透传给原生_registerLocalAIHandler绑定,由 C++ 侧建立 Session 与 Utility 进程之间的 Mojo 通道,这正是后续setPromptAPIHandler所处理的"Prompt API 绑定请求"的来路。
核心方法:localAIHandler.setPromptAPIHandler(promptAPIHandler)Experimental
localAIHandler模块当前暴露的方法只有一个:setPromptAPIHandler(文档原文)。其签名为:
localAIHandler.setPromptAPIHandler(promptAPIHandler) _Experimental_ * promptAPIHandler Function<typeof LanguageModelUtility | null> * details Object * webContentsId Integer - The unique id of the WebContents calling the Prompt API. * securityOrigin string - Origin of the page calling the Prompt API. * frameToken string - The frame token of the frame calling the Prompt API. * renderProcessId Integer - The process id of the renderer process hosting the frame.参数与语义要点:
webContentsId:调用 Prompt API 的WebContents唯一 id(对应 WebContents.id)。securityOrigin:调用页面源(origin),是安全边界的关键字段。frameToken:调用所在 frame 的 token(对应 webFrame.frameToken),可用于区分同一 origin 下的不同 frame。renderProcessId:承载该 frame 的渲染进程 pid(对应 webFrame.processId)。
方法行为(文档原文语义):
- 注册回调:设置一个处理"渲染进程新 Prompt API 绑定请求"的回调。该回调按
webContentsId与securityOrigin的配对触发——即同一 WebContents 的同一源只触发一次。 - 返回
null即拒绝:如果回调返回null,则拒绝在该渲染进程中创建新的 Prompt API 会话。这是把"是否允许某页面调用本地 AI"作为策略点交给应用层的机制。 - 失效已有会话:若要作废当前已存在的 Prompt API 会话,应在主进程侧调用
ses.registerLocalAIHandler(null),它会断开所有 Prompt API 会话并销毁所有LanguageModelUtility实例。
源码层面的印证
JS 层的模块导出非常薄,见 local-ai-handler.ts:
import { EventEmitter } from 'events'; const binding = process._linkedBinding('electron_utility_local_ai_handler'); Object.setPrototypeOf(binding, EventEmitter.prototype); module.exports = binding;真正的实现是原生绑定,注册入口为 electron_api_local_ai_handler.cc:
void SetPromptAPIHandler(v8::Isolate* isolate, v8::Local<v8::Value> val) { PromptAPIHandler handler; if (!gin::ConvertFromV8(isolate, val, &handler)) { isolate->ThrowException(v8::Exception::TypeError( gin::StringToV8(isolate, "Must pass a function"))); return; } GetPromptAPIHandler() = handler; auto& cb = GetHandlerChangedCallbackStorage(); if (cb) { cb.Run(); } }可以确认几个实现事实:
setPromptAPIHandler要求参数是一个function,否则抛TypeError: Must pass a function。- 回调被存为进程内单例(
GetPromptAPIHandler()基于base::NoDestructor<std::optional<PromptAPIHandler>>),即每个 Utility 进程只保存一个 handler。 - 设置成功后会触发
GetHandlerChangedCallbackStorage()中注册的base::RepeatingClosure——从源码结构看,该回调由 AI 管理器在等待 handler 就绪时注入,用来在 handler 可用时冲刷排队中的请求(见下节"请求排队")。
类型定义在 electron_api_local_ai_handler.h:
using PromptAPIHandler = base::RepeatingCallback<v8::Local<v8::Value>(gin_helper::Dictionary)>;即:PromptAPIHandler接收一个gin_helper::Dictionary(对应文档中的details对象),返回一个v8::Local<v8::Value>(文档中约定为typeof LanguageModelUtility或null)。
请求排队机制(务必尽早调用setPromptAPIHandler)
文档原文 NOTE:
If a renderer calls the Prompt API before
setPromptAPIHandler()has been called, the request is queued. Once the handler is set, all queued requests are flushed. If too many requests are queued, the oldest pending request is dropped and pending promises in the renderer will be rejected. To avoid this, be sure to callsetPromptAPIHandler()as early as possible.
要点:
- 早到的请求会被排队,不会直接丢失。
- handler 一旦设置,所有排队请求立即冲刷。
- 队列溢出保护:排队过多时,最老的待处理请求被丢弃,渲染进程侧对应的 pending Promise 被 reject。
- 最佳实践:在 Utility 进程脚本一加载完成就调用
setPromptAPIHandler,例如:
// 运行于 UtilityProcess 中(被 registerLocalAIHandler 注册的脚本) const { localAIHandler } = require('electron'); const { LanguageModelUtility } = require('./my-local-llm-bridge'); // 你实现的桥接 // 尽早注册,避免渲染进程的 Prompt API 请求堆积到队列上限 localAIHandler.setPromptAPIHandler(async (details) => { // details: { webContentsId, securityOrigin, frameToken, renderProcessId } if (!isOriginAllowed(details.securityOrigin)) { return null; // 拒绝该 origin 创建 Prompt API 会话 } return LanguageModelUtility.create({ contextWindow: 8192, // 其他 LanguageModelCreateOptions 字段 }); });注意:上例中
LanguageModelUtility.create(...)的具体参数形态以 LanguageModelCreateOptions 结构 为准;返回null表示拒绝创建会话,这是文档中明确定义的策略点。
返回对象:LanguageModelUtility
setPromptAPIHandler的回调返回一个LanguageModelUtility实例(或null)。这个类是本地模型实现的"契约载体",其文档见 LanguageModelUtility。JS 侧默认实现位于 language-model-utility.ts,方法体大多为空实现或返回固定值——它的意义在于定义接口形状,真实推理逻辑由你在 Utility 脚本中自行补全(通过覆写或继承)。
构造函数
new LanguageModelUtility(initialState) * initialState Object * contextUsage number * contextWindow number[!NOTE] Do not use this constructor directly outside of the class itself, as it will not be properly connected to the
localAIHandler
即:不要绕过LanguageModelUtility.create()直接new。这一点与 源码实现 一致——create会走一次带contextUsage: 0, contextWindow: 0初始状态的构造:
static async create(): Promise<LanguageModelUtility> { return new LanguageModelUtility({ contextUsage: 0, contextWindow: 0 }); }静态方法
LanguageModelUtility.create(options)Experimental
- 入参:
optionsLanguageModelCreateOptions - 返回:
Promise<LanguageModelUtility> - 用途:用给定
options创建一个LanguageModelUtility。这是你在setPromptAPIHandler回调里应当调用的入口。
LanguageModelUtility.availability([options])Experimental
- 入参:
optionsLanguageModelCreateCoreOptions(可选) - 返回:
Promise<string> - 用途:判定语言模型可用性,返回以下四种字符串之一:
| 返回值 | 含义 |
|---|---|
available | 模型已可用 |
downloadable | 可下载(模型尚未就绪) |
downloading | 正在下载 |
unavailable | 不可用 |
从 C++ 侧看,这四种状态由 Mojo 的ModelAvailabilityCheckResult枚举映射而来,见 utility_ai_manager.cc 中的Converter<blink::mojom::ModelAvailabilityCheckResult>,其中available → kAvailable、unavailable → kUnavailableUnknown、downloading → kDownloading、downloadable → kDownloadable。
实例属性
languageModelUtility.contextUsageExperimental:当前上下文窗口已占用的 token 数(number)。languageModelUtility.contextWindowExperimental:上下文窗口总大小(token 数)。
实例方法
languageModelUtility.prompt(input, options)ExperimentalinputLanguageModelMessage[]optionsLanguageModelPromptOptions- 返回:
Promise<string> | Promise<ReadableStream<string>> - 用途:向模型发出一次提问;可以一次性返回字符串,也可以返回流式结果。
languageModelUtility.append(input, options)ExperimentalinputLanguageModelMessage[]optionsLanguageModelAppendOptions- 返回:
Promise<undefined> - 用途:追加一条消息但不触发推理,用于维持上下文。
languageModelUtility.measureContextUsage(input, options)Experimental- 返回:
Promise<number> - 用途:测量给定 input 会消耗多少 token,便于在做上下文裁剪前预先估算。
- 返回:
languageModelUtility.clone(options)ExperimentaloptionsLanguageModelCloneOptions- 返回:
Promise<LanguageModelUtility> - 用途:克隆一份保留当前上下文与初始 prompt 的副本,便于做分支推理。
languageModelUtility.destroy()Experimental- 用途:销毁模型并中止所有在途执行。
从 utility_ai_manager.cc 中引入的 Blink Mojo 头(ai_common.mojom.h、ai_language_model.mojom.h、ai_proofreader.mojom.h、ai_rewriter.mojom.h、ai_summarizer.mojom.h、ai_writer.mojom.h)可以推断,Utility 进程内维护了一个UtilityAIManager,通过 Mojo 与浏览器侧的 Blink AI 抽象对接;AILanguageModelPromptType的枚举包含text/image/audio/unknown,即prompt(input, ...)的输入类型可能覆盖多模态场景。具体类型映射见 PromptType Converter。
一次完整调用链(从渲染进程到 Utility 进程)
综合 session.md、localAIHandler.md、language-model-utility.md 与源码,可以还原出一条清晰的调用链:
- 主进程:
session.defaultSession.registerLocalAIHandler(utilProc),其中utilProc是一个加载了你 Utility 脚本的UtilityProcess。实现见 session.ts,透传内部 handle 到原生绑定。 - Utility 脚本:脚本加载后尽早调用
localAIHandler.setPromptAPIHandler(handler)。该调用落到 SetPromptAPIHandler,把 handler 存入进程级单例,并触发HandlerChangedCallback冲刷队列。 - 渲染进程:页面在某个
WebContents+securityOrigin组合下首次调用浏览器内建 Prompt API(如LanguageModel相关接口)。浏览器侧按webContentsId + securityOrigin去重,向 Utility 进程投递一个绑定请求。 - Utility 进程:
setPromptAPIHandler注册的handler(details)被调用,details包含webContentsId、securityOrigin、frameToken、renderProcessId。回调返回:LanguageModelUtility实例:允许创建 Prompt API 会话;null:拒绝创建会话。
- 会话存续期:页面持续通过
LanguageModelUtility实例上的prompt/append/measureContextUsage/clone与本地模型交互。 - 清理:主进程调用
ses.registerLocalAIHandler(null)会断开所有 Prompt API 会话,并销毁所有LanguageModelUtility实例。
安全与策略建议
- 以
securityOrigin作为策略门:在 handler 内维护一个允许的 origin 白名单,命中即返回LanguageModelUtility.create(...),未命中返回null。这是文档给出的"返回null即拒绝"策略点的直接落地方式。 - 以
webContentsId+frameToken做更细粒度控制:同一 origin 下不同 frame 可能承载不同可信度(例如嵌入的第三方 iframe),可以利用frameToken拒绝非顶层 frame 的推理请求。 - 尽早注册:Utility 脚本第一行就调用
setPromptAPIHandler,避免渲染进程 Prompt API 请求堆积触发"最老请求被丢弃、pending Promise 被 reject"。 - 模型清理:页面卸载或业务主动断开时,主进程调用
ses.registerLocalAIHandler(null)是最彻底、语义最明确的清理方式,避免残留会话与模型资源。
参考实现骨架
下面给出一个可以直接在 Utility 进程脚本中使用的最小骨架(接口形状以 language-model-utility.md 为准,真实推理逻辑需你按本地模型 SDK 自行实现):
// utility-local-ai.js —— 由 UtilityProcess 加载 const { localAIHandler } = require('electron'); // 以 origin 作为策略门 const ALLOWED_ORIGINS = new Set([ 'https://app.example.com', 'http://localhost:5173', ]); class MyLocalLanguageModel extends (require('electron').LanguageModelUtility) { async prompt(input, options) { // 将 input(LanguageModelMessage[])转换为你本地模型可接受的格式 // 支持返回 Promise<string> 或 Promise<ReadableStream<string>> return 'echo: ' + JSON.stringify(input); } async measureContextUsage(input, options) { // 用本地分词器估算 token 数 return input.reduce((n, m) => n + Math.max(0, (m.content || '').length / 4 | 0), 0); } } // 尽早注册,避免排队溢出 localAIHandler.setPromptAPIHandler(async (details) => { const { webContentsId, securityOrigin, frameToken, renderProcessId } = details; if (!ALLOWED_ORIGINS.has(securityOrigin)) { return null; // 拒绝 } return require('electron').LanguageModelUtility.create({ contextWindow: 8192, expectedInputs: ['text'], }); });提示:
LanguageModelUtility的静态方法create参数类型是 LanguageModelCreateOptions;availability参数类型是 LanguageModelCreateCoreOptions。请以此结构文档为准调整字段的实际字段名与取值范围,上述骨架仅为接口形状示意。
小结与要点回顾
localAIHandler是运行在Utility 进程的模块,唯一入口是setPromptAPIHandler(promptAPIHandler)Experimental(文档)。promptAPIHandler回调按webContentsId+securityOrigin组合触发一次;details携带webContentsId、securityOrigin、frameToken、renderProcessId四个字段,构成安全策略的输入。- 回调返回
LanguageModelUtility即允许创建 Prompt API 会话;返回null即拒绝。 - 未注册 handler 前,渲染进程请求会被排队;handler 设置后统一冲刷;排队过多时最老请求被丢弃并 reject 对应 Promise。
- 主进程通过
ses.registerLocalAIHandler(handler | null)注册或清空 handler;传null会断开所有 Prompt API 会话并销毁所有LanguageModelUtility实例。 - 相关文档与源码入口:
- docs/api/local-ai-handler.md
- docs/api/language-model-utility.md
- docs/api/session.md(registerLocalAIHandler 章节)
- lib/utility/api/local-ai-handler.ts
- lib/utility/api/language-model-utility.ts
- shell/utility/api/electron_api_local_ai_handler.cc
- shell/utility/api/electron_api_local_ai_handler.h
- shell/utility/ai/utility_ai_manager.cc
- lib/browser/api/session.ts
该 API 目前标记为_Experimental_,接口在 Electron 后续版本中可能调整,落地前请对照当前版本 localAIHandler 文档 与 LanguageModelUtility 文档 中的签名核对一次。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考