跳转到正文
返回门户

LFRuntime ​

全局变量:LFRuntime · 启用方式:自动注入,无需 @require · 别名:UserScriptContext

始终可用的应用运行时上下文:元信息、热重载生命周期、资源读取、样式注入、MAIN world 执行、页面请求拦截,以及 Agent 侧边栏控制。

属性 ​

ts
LFRuntime.bundleId: string

当前应用(bundle)的唯一 ID

ts
LFRuntime.meta: BundleMeta

入口元信息 { id, name, description, entryFile, matchPatterns }

ts
LFRuntime.url: string

脚本开始执行时的 location.href

ts
LFRuntime.version: string

宿主扩展 PluginX 的版本号(manifest.json,如 "3.0.6"),不是应用自身的版本;无法解析时为空串

ts
LFRuntime.liveDebug: boolean

是否处于调试注入(run / liveDebug 时为 true,已保存应用为 false)

ts
LFRuntime.executionEnvironment: 'page' | 'background' | 'editor-sandbox'

当前执行环境:page、background 或 editor-sandbox;由编译产物确定。

ts
LFRuntime.isBackground: boolean

是否运行在后台沙箱,等价于 executionEnvironment === "background"。

ts
LFRuntime.signal: AbortSignal | null

终端 run 进程的中止信号(Ctrl+C / kill / agent 停止时 abort);liveDebug 与已启用/已安装应用中为 null,热重载不会触发它

ts
LFRuntime.network: { onResponse, interceptRequest, interceptResponse }

页面 fetch/XHR 拦截入口(observe / mock / replace),需要头部声明 @intercept

ts
LFRuntime.cli: { register, unregister }

注册可被 plugincmd 发现的终端命令

ts
LFRuntime.sidebar: { getState, getAgentState, isSupported, open, close, input, setModel }

查询与控制PluginX Agent 侧边栏:开关、向输入框送文字、切换对话模型

ts
LFRuntime.input: { click, hover, focus, fill, clear, press, type, text, key, check, uncheck, select, scrollIntoView, readText, move, down, up, drag, scroll }

可信输入(CDP,事件 isTrusted === true),只作用于脚本所在的标签页;text 是 type 的别名。首次使用会附加调试器,浏览器会显示调试横幅;纯页面交互用 LFDOM 更轻

方法 ​

ts
LFRuntime.sleep(ms): Promise<void>

延时,等价于可中止的 setTimeout

ts
LFRuntime.throwIfAborted(): void

若终端 run 进程已被中止则抛出中止原因,用于长循环里及时退出;signal 为 null 时不做任何事

ts
LFRuntime.registerCleanup(callback): typeof callback

注册清理回调,liveDebug 热重载重新注入前自动执行;返回传入的 callback 本身(不是注销函数,登记后无法取消)。只在 liveDebug 与终端 run 中登记,已启用/已安装应用中什么也不做

ts
LFRuntime.trackNode(node): Node

登记 DOM 节点,重新注入时自动移除

ts
LFRuntime.listen(target, type, handler, options?): () => void

addEventListener 的热重载安全版本:重新注入时自动移除监听;返回手动取消函数

ts
LFRuntime.interval(callback, ms, ...args): number

setInterval 的热重载安全版本:重新注入时自动 clearInterval

ts
LFRuntime.timeout(callback, ms, ...args): number

setTimeout 的热重载安全版本:重新注入时自动 clearTimeout

ts
LFRuntime.mountHot(key, setup): T | undefined

热重载友好的挂载:同 key 重挂载前自动执行上一次的清理

ts
LFRuntime.clearHot(key): void

手动清理指定 key 的热挂载

ts
LFRuntime.getResource(name): string

读取 @resource 声明的资源:文本资源返回原文,二进制资源返回 data: URL,未声明的别名返回空串(可用 !== "" 判断是否存在)

ts
LFRuntime.appendStyle(cssText, styleKey?): HTMLStyleElement

注入 CSS;同一 styleKey(缺省为 "inline")重复调用会替换内容而非叠加,需要多段样式时请传不同的 styleKey。CSS 类型的 @resource 会在启动时以别名为 key 自动注入

ts
LFRuntime.runInMainWorld(sourceOrFunction, options?): Promise<T>

把函数或源码放到页面 MAIN world 执行并取回结果

ts
LFRuntime.execTerminalCommand(command, options?): Promise<Result>

在编辑器终端执行命令(仅终端临时脚本:run / run -c / run --tab / run --target background)

ts
LFRuntime.readFile(path, options?): Promise<{ ok, content, ... }>

读取工作区文件,文本在返回值的 .content 字段(仅终端临时脚本,同上)

ts
LFRuntime.writeFile(path, content, options?): Promise<Result>

写入工作区文件,支持字符串与二进制(仅终端临时脚本,同上)

ts
LFRuntime.sidebar.getState(): Promise<{ open, supported }>

读取侧边栏的打开状态与当前浏览器是否支持侧边栏

ts
LFRuntime.sidebar.getAgentState(): Promise<{ runStatus, isRunning, ready, submitAllowed, ... }>

读取 Agent 执行状态、运行准备状态与提交授权探测结果;面板未打开时也可调用。

ts
LFRuntime.sidebar.isSupported(): Promise<boolean>

仅返回当前浏览器是否支持侧边栏

ts
LFRuntime.sidebar.open(): Promise<{ open: true, windowId, alreadyOpenInAnotherWindow? }>

打开 Agent 侧边栏(必须在用户点击回调中作为第一个异步调用)

ts
LFRuntime.sidebar.close(): Promise<{ open: false }>

关闭 Agent 侧边栏

ts
LFRuntime.sidebar.input(text, options?): Promise<{ accepted: true, insertedLength, submitted, mode, ... }>

向已打开的 Agent 输入框写入文字并聚焦;默认覆盖草稿、不提交,追加与提交行为由 options 指定。

ts
LFRuntime.sidebar.setModel(modelId): Promise<{ modelId, addedToQuickList }>

切换已打开侧边栏的 Agent 模型;快捷列表中没有时自动添加,不存在时抛异常

ts
LFRuntime.print(...args): void

终端 run 脚本中输出到终端 stdout(可重定向);liveDebug 与已启用/已安装应用中写入页面 console,liveDebug 下可用终端 diag 查看;参数全部为 Error 实例时走 stderr。务必写全 LFRuntime.print(...)——裸写 print() 调用的是 window.print(),会弹出浏览器打印对话框

可用性 ​

同一个 API 在不同执行方式下行为不同。「已启用/已安装」指保存编译后注入网页的应用;「后台应用」指 // @match background 应用(含 both 应用的后台分支)的常驻实例。

能力已启用/已安装(page)liveDebugrun --tab不带 --tab 的 runrun --target background后台应用
print 输出位置页面 console页面 console(diag 可查)终端终端终端后台沙箱 console
signal / throwIfAbortednull / 无操作null / 无操作生效生效生效null / 无操作
readFile / writeFile / execTerminalCommand 直接调用不可用¹不可用¹可用可用可用不可用¹
network需声明 @intercept可用可用不生效不生效不生效
runInMainWorld可用可用可用²在编辑器沙箱内直接执行(没有目标网页)reject MAIN_WORLD_NO_TABreject MAIN_WORLD_NO_TAB
sidebar可用可用可用除 open() 外可用除 open() 外可用除 open() 外可用
input本地开发版可用;市场上架版需 LFRuntime.input 能力授权可用可用reject RUNTIME_INPUT_NO_TABreject RUNTIME_INPUT_NO_TABreject RUNTIME_INPUT_NO_TAB
cli可用可用可用可用可用可用

¹ 应用源码里直接调用会在编译期报错;绕过编译检查时运行时 reject WORKSPACE_LEASE_REQUIRED。应用代码请用 LFRuntime.cli.register({ workspace: ['read' / 'write' / 'exec'] }) 注册命令,在 handler 内通过 ctx.workspace.readFile / writeFile / exec 访问。

² run --tab 默认运行在 MAIN world,回调直接在当前 world 执行,options.timeoutMs 不生效;--world user-script 时经后台桥接到 MAIN world。

本地能力边界 ​

  • 本机命令执行(localexec)、本机文件访问(local fs)与 CLI 注册表请求(plugincmd 的列表与调用)只接受 PluginX 编辑器页面发起。插件应用——无论注入网页、运行在后台,还是处于 USER_SCRIPT world——直接发送这些请求都会以 EACCES_PERMISSION 被拒绝。插件代码访问工作区请用 LFRuntime.cli.register({ workspace: [...] }) 注册命令,在 handler 内通过 ctx.workspace.* 访问。
  • 权限弹窗的决定只接受扩展自身的权限弹窗页发出,请求 id 不可预测,应用无法替用户作答。
  • LFHttp.client.fs(客户端文件系统通道)按浏览器认证的应用身份归属:白/黑名单以认证身份判定,而不是调用方自报的 bundleId,应用无法冒用其它应用的 bundleId 访问它的文件。

参数详解 ​

mountHot(key, setup) ​

key 为挂载槽的稳定标识(内部自动加 bundleId 前缀,不同应用不会冲突)。setup 必须是同步函数,收到一个 hot 上下文对象:

字段类型说明
hot.keystring归一化后的 key
hot.liveDebugboolean同 LFRuntime.liveDebug
hot.previousValueT / undefined上一代 setup 返回的值(首次运行为 undefined),用于复用已挂载的节点
hot.onDispose(cleanup)(fn) => fn注册下次卸载时执行的清理函数
hot.setValue(value)(v) => v手动设置槽值(当你想同时返回清理函数时使用)

setup 的返回值按类型解释:返回函数 → 视为清理函数(槽值保持上一代);返回 { value?, cleanup? } 对象 → 拆解为槽值与清理;返回其他非 undefined 值 → 作为新槽值;返回 undefined → 保持上一代值。

每个挂载槽只保留一个清理函数:多次调用 onDispose 以最后一次为准,setup 返回函数(或带 cleanup 的对象)时也会覆盖 onDispose 登记的函数。需要多个清理动作时请合并到一个函数里。

热重载幂等语义(保存源码触发的软热更新时发生了什么) ​

软热更新(保存源码自动触发,或重复执行 liveDebug on)会在当前 document 内重新执行整个 bundle。执行新代码之前,框架会依次调用上一轮通过 registerCleanup / trackNode / listen / interval / timeout / mountHot 登记的清理;但裸调用不受管理——直接 addEventListener / setInterval 挂上的副作用、写到 window 上的全局属性都会跨软热更新存活并与新代码叠加(重复监听、旧闭包继续运行)。终端命令 liveDebug reload 则不同:它会整页刷新页面后重新注入,上一代所有残留(注册过与否)一并销毁。

写法热重载行为建议
LFRuntime.listen / interval / timeout / trackNode / mountHotreload 时自动清理顶层副作用一律用这些助手
裸 addEventListener / setInterval / setTimeout旧监听器/定时器继续存活,与新代码叠加仅在 mountHot 的 cleanup 内自行管理时使用
window.__myFlag 之类的守卫标志跨 reload 保留 → 新代码误判「已初始化」而跳过挂载反模式,不要用;幂等交给 mountHot 的 key 语义
长循环 / 轮询任务旧任务不会被自动打断用 let alive = true; LFRuntime.registerCleanup(() => { alive = false; }); 并在循环条件里检查 alive;throwIfAborted() / signal 只对终端 run 进程生效,热重载不会触发

不要用守卫标志实现幂等

if (window.__inited) return; 在热重载下必然失效:旧标志跨 reload 保留,新代码会直接跳过挂载,表现为「改了代码但页面没变化」。正确做法是把挂载放进 mountHot(key, setup)——同 key 重挂载前框架自动执行上一次的清理,天然幂等。

runInMainWorld(sourceOrFunction, options?) ​

参数类型默认值说明
sourceOrFunctionstring / Function—自包含的函数或括号包裹的表达式;不能闭包引用外部局部变量,参数通过 options.args 或 options.bindings 传入
options.argsunknown[]—传给 MAIN world 函数的参数,须可结构化克隆;已挂载到当前 document 的 Element 会自动桥接
options.bindingsRecord<string, unknown>—显式注入回调词法作用域的局部值(键须是合法标识符,值须可结构化克隆)
options.dependenciesRecord<string, Function / string>—在回调之前声明的自包含 helper 函数/表达式;helper 自身同样不能捕获外部闭包
options.preludesstring[]—在回调私有作用域内、dependencies 之前原样执行的源码(如以文本形式取得的 UMD/IIFE 库构建),用于把库带进 MAIN world
options.timeoutMsnumber10000超时毫秒数;缺省或传 0 为 10000,超出范围会被钳制到 [1000, 60000]

回调内只有 LFRuntime.print 与 LFRuntime.sleep 可用,访问其它 LF* 成员会抛 MAIN_WORLD_RUNTIME_UNAVAILABLE;请让回调只返回普通数据,其余 LF* 调用放在回调外。返回值跨 world 上限 16 MiB,超出 reject MAIN_WORLD_RESULT_TOO_LARGE。各执行方式下是否可用见「可用性」。

execTerminalCommand(command, options?) ​

本节与下一节的三个工作区方法只能在终端临时脚本(run / run -c / run --tab / run --target background)中直接调用。应用源码(liveDebug 与已启用/已安装/后台应用)中直接调用会在编译期报错,运行时 reject WORKSPACE_LEASE_REQUIRED;应用代码请用 LFRuntime.cli.register({ workspace: ['read' / 'write' / 'exec'] }) 注册命令,在 handler 内通过 ctx.workspace.readFile / writeFile / exec 访问(语义与这里相同)。

在编辑器内置的 /workspace 终端(非宿主系统 shell)中执行命令。options 字段:timeoutMs(默认 30000,范围 [1000, 60000])、echoCommand(是否回显命令)、recordHistory(是否记入历史)、writePrompt(结束后是否输出新提示行)、focusTerminal(是否聚焦终端面板)。返回值结构:

字段类型说明
okboolean便捷标志,默认等价于 exitCode === 0
exitCodenumber命令退出码(命令不存在为 127)
stdout / stderrstring捕获的标准输出 / 标准错误(无输出时为空串)
messagestring桥接层附加的说明信息(通常为空)
clearRequestedboolean命令是否请求清屏(如 clear)

readFile(path, options?) / writeFile(path, content, options?) ​

两者都是异步方法,必须 await;返回的是信封对象而非文件内容本身——文件文本在 .content 字段(常见错误:JSON.parse(await LFRuntime.readFile(p)) 是错的,应先解构 const { content } = await LFRuntime.readFile(p))。

参数/字段类型默认值说明
pathstring—工作区相对路径(如 src/foo.ts)或 /workspace/... 前缀;仅 /workspace 内可写
options.timeoutMsnumber30000范围 [1000, 60000]
options.encoding(读)'utf8' / 'base64' / 'binary' / null'utf8'utf8 返回文本;binary(或 null,以及别名 'buffer' / 'uint8array')返回 Uint8Array;base64 返回 base64 字符串。无法识别的值(如 'utf-8')按 utf8 处理。读二进制文件(PNG/字体等)务必用 binary
options.encoding(写)'utf8' / 'base64''utf8'仅当 content 是字符串时生效;二进制 buffer 始终按原始字节写入
options.mkdirP(写)booleantrue自动创建缺失的父目录(类似 mkdir -p)
content(写)string / Uint8Array / ArrayBuffer / TypedArray / DataView—二进制 buffer 直接传即可(如 await response.arrayBuffer());不要把二进制响应经 response.text() 转字符串再写——有损且不可恢复。null/undefined 写入空文件;其它非字符串、非二进制的值会被 String() 转换(对象会写成 [object Object]),写 JSON 请先 JSON.stringify

readFile 在 utf8 模式返回 { ok, path, content, message },binary/base64 模式额外包含 encoding 与 bytesRead;writeFile 返回 { ok, path, bytesWritten, message }。文件不存在、路径是目录或父路径不是目录时 readFile 不抛错,而是 resolve { ok: false },content 为空串(binary 模式为空 Uint8Array)——请用 ok === false 判断;传输、超时等其它错误仍会 reject。

network.onResponse / interceptRequest / interceptResponse(urlPattern, callback, options?) ​

参数类型说明
urlPatternstring / RegExp字符串按子串匹配请求 URL;正则按 test 匹配
callbackFunctiononResponse 收到 { request, response, status, source }(request/response 含 url/method/headers/body);interceptRequest 收到 request,返回 { body, init? } 即短路模拟响应(真实请求不发出),返回 undefined 放行;interceptResponse 收到 { request, response },返回 { body, init? } 替换页面看到的响应
options.onceboolean只生效一次后自动取消订阅:onResponse 首次命中即消耗;interceptRequest / interceptResponse 仅在回调实际返回了数据时消耗(返回 undefined 放行不算)
options.timeout / timeoutHandlernumber / () => void超时未命中则自动取消订阅并调用 timeoutHandler

返回值中的 body 可以是字符串、ArrayBuffer、TypedArray 或 DataView(二进制体原样送达页面,不要先转成 base64 字符串);普通对象/数组会被 JSON.stringify 后作为响应体;Blob / FormData / URLSearchParams 不支持——这次结果会被拒绝并在页面 console 报错,请求按原样放行。

三个方法都返回取消订阅函数,liveDebug 热重载时订阅会自动释放;后台应用、run --target background 与不带 --tab 的 run 没有页面,订阅永远不会命中。init 默认 { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } }。注意:只能「模拟或放行」,不能修改请求后继续发送;已安装应用必须在头部声明 // @intercept <url-pattern>。拦截范围仅限页面自身 JS 发起的 fetch/XHR:应用代码运行在隔离世界,自己调用的 fetch 与 LFHttp 请求不会经过拦截,也不会触发 onResponse。

cli.register(def) ​

字段类型必填说明
def.namestring是命令名(plugincmd call <name> 使用),非空、无空白、≤64 字符;不合规时 register() 同步抛错
def.briefstring否一行简介(≤200 字,超出部分静默截断),显示在 plugincmd list。IDE 会在每条用户消息里自动把可调用命令的 name: brief 注入 Agent 上下文(网页对话模式:当前 tab + 后台应用的注册;IDE 编辑器模式:后台应用的注册),所以请像写 skill 描述一样写清「做什么、什么时候用」
def.usagestring / string[]否详细用法说明,显示在 plugincmd help <name>(上限 5 万字,超出部分静默截断;数组按换行连接)。按需加载,可以写完整的操作手册:参数约定、前置条件、执行步骤、注意事项——相当于 skill 的正文
def.workspace('read' / 'write' / 'exec' / 'agent')[]否声明命令需要的工作区 scope:read/write/exec 分别解锁 ctx.workspace.readFile/writeFile/exec;agent 解锁 LFAgent 内置库(见「LFAgent」)。未知的 scope 在注册时即抛错。租约仅在本次 plugincmd call 期间有效,调用结束即吊销
def.handler(argv, ctx) => string / { exitCode?, output?, errorOutput? } / void是见下表

handler 的参数与返回值:

项说明
argv命令名之后的参数数组。-- 可省略:从第一个非 plugincmd call 选项的参数起即为 argv;参数本身以 --reg / --tab / --bundle / --source / --timeout 开头时,需要先写 --
ctx.signal调用方中断(Ctrl+C / agent 中止 / IDE 关闭 / 超时)时触发,长任务应检查 ctx.signal.aborted
ctx.callId本次调用的唯一 ID
ctx.timeoutMs本次调用生效的超时(毫秒),0 表示不限时(调用方未传 --timeout,默认即不限时);必须不限时才能安全执行的任务可据此在入口主动拒绝限时调用
ctx.workspace工作区门面 { scopes, readFile, writeFile, exec },仅当命令声明了 def.workspace 且调用方带终端会话时存在;handler 结束后再调用会 reject WORKSPACE_LEASE_EXPIRED,调用未声明的 scope 会 reject WORKSPACE_SCOPE_DENIED
返回值字符串视为 stdout;返回 undefined 视为空输出;对象形式可自定义 exitCode。output / errorOutput 单次上限 100 万字符,超出截断

退出码:

退出码含义
0正常返回(字符串、undefined 或未指定 exitCode 的对象)
1handler 抛错,错误消息写入 stderr
126市场上架版应用声明了 def.workspace 但不具备 LFRuntime.workspace 授权,调用以 WORKSPACE_UNAUTHORIZED 被拒,handler 不会执行
127命令在提供方上下文中已不存在(例如已被注销)
130调用方中断

register() 返回注销函数;cli.unregister(name) 按名字注销当前上下文注册的命令。注册的生命周期与提供方上下文一致:run 脚本退出即注销,需要持续服务时脚本必须保持存活(如后台任务);编译应用的 page 实例随页面/标签页关闭注销;background 实例常驻(不依赖任何标签页,浏览器重启后随实例恢复自动重新注册),应用被禁用或卸载时注销。both 应用的两个分支各自注册一份同名命令,plugincmd 默认优先调用 background 分支。

侧边栏对所有安装用户开放,这些方法不做角色区分(真正的鉴权由服务端接口把关)。整组方法需要能与后台通信的执行环境:已启用/已安装应用(含后台应用)、liveDebug、run --tab(编辑器会自动注入 ISOLATED 中继桥)、不带 --tab 的 run 与 run --target background 均可;没有可用通信通道时,方法返回的 Promise 会 reject(不会同步抛错)。后台应用、run --target background 与不带 --tab 的 run 没有调用方窗口,其中 open() 会被拒绝,其余方法正常。

方法前置条件返回值与行为
getState()—返回 { open, supported }:open 以侧边栏是否有活动连接为准(面板是单实例,不按调用方窗口判断),supported 表示当前浏览器是否具备 Side Panel API
getAgentState()—(面板未打开不抛错)返回 Agent 执行态 { open, supported, runStatus, isRunning, stopRequested, ready, submitAllowed, sessionId, modelId }。runStatus:closed 面板未打开 / idle 可接受运行 / running 正在跑 / aborting 已请求停止但尚未收尾;ready 预判提交能否通过前置校验(已打开项目且已配置模型);submitAllowed 是不弹窗的授权探测
isSupported()—返回 boolean,等价于 getState().supported,即浏览器能力探测
open()必须能确定调用方窗口(即脚本运行在网页里);必须在直接用户手势中作为第一个异步调用打开调用方所在窗口的 Agent 侧边栏并等待面板建连,返回 { open: true, windowId };若面板已开在另一个窗口,则不重复打开,返回 { open: true, windowId: <该窗口>, alreadyOpenInAnotherWindow: true }
close()—关闭侧边栏,返回 { open: false }
input(text, options?)侧边栏必须已打开;text 去除首尾空白后不可为空。submit: true 额外要求已获执行授权、侧边栏已打开项目且已配置模型把文字写进当前 Agent 输入框并聚焦。默认覆盖原有草稿(mode: "replace"),追加请传 { mode: "append" }。默认不提交,由用户按发送;传 { submit: true } 则直接提交、让 agent 开跑。返回 { accepted: true, insertedLength, submitted, draftAppended, sessionId, mode } —— 传了 submit 也要检查 submitted
setModel(modelId)侧边栏必须已打开;modelId 非空按模型 ID、canonical slug 或别名切换当前 Agent 模型;不在快捷列表时先自动添加;不存在或不可用于 Agent 聊天时抛异常;返回 { modelId, addedToQuickList }

open() 的调用时机

open() 底层是 chrome.sidePanel.open(),必须在瞬态用户激活失效前同步发出:在按钮 click 回调里把它作为第一个异步调用,前面不能有任何 await(把 isSupported() 之类的探测放到页面初始化阶段先做完)。它还需要从消息中解析出调用方窗口,因此只能在网页中调用——编辑器沙箱(不带 --tab 的 run)拿不到窗口 id,会直接拒绝。面板已开在另一个窗口时不会重复打开(两个面板等于两个 agent 运行时,为只留一个就得杀掉那边正在跑的对话),请读 alreadyOpenInAnotherWindow 并用返回的 windowId 提示用户切换过去。

input 的写入模式 ​

input(text) 默认覆盖输入框里已有的草稿(等价于 { mode: "replace" }):应用递进来的通常是一条完整的话,拼在用户手打的半句后面很少是想要的结果——尤其配合 submit: true 时,那半句会被一起发出去。覆盖会连同用户已暂存的代码选区、文件引用一起清掉,因为选区 token 就嵌在草稿文本里,只清文字会留下孤儿引用。确实要做增量拼接(比如分几次把一段长内容送进去)时显式传 { mode: "append" },它会追加到草稿末尾并在需要时补一个换行。返回值里的 mode 会回显本次实际生效的模式。

模式行为
"replace"(默认)先清空草稿(含代码选区/附件)再写入本次文字
"append"追加到现有草稿末尾,必要时补换行;保留已有选区/附件

input 的 submit 选项与授权 ​

input(text) 只是把文字放进输入框,用户还要自己按发送——人始终在回路里,所以不需要任何授权。input(text, { submit: true }) 则直接开跑一轮 agent 对话,那一轮里 agent 可以读写工作区、跑终端命令、调模型花额度:这是把「应用建议一件事」升级成「应用替用户下令做一件事」,因此和本机命令执行同一量级地受一道闸约束。

选项类型默认说明
mode"replace" / "append""replace"文字怎么落进草稿:replace 先清空(含代码选区/附件)再写入;append 追加到末尾
submitbooleanfalsetrue 时提交并让 agent 立即执行;缺省只填输入框
onBusy"reject" / "append_draft""reject"仅 submit 时有意义。agent 正在跑时:reject 抛错且不写入草稿(避免悄悄堆积);append_draft 静默降级为只追加草稿。没有排队机制

授权分两层,顺序固定:① Agent 设置 → Agent → 「允许应用向侧边栏 agent 发起执行命令」,默认开启,开着就直接放行;② 用户把它关掉后,改为按应用弹窗询问一次,同意即长期记住(与本机命令执行同一范式),拒绝则本次失败且不留授权记录、下次仍会问。也就是说关掉开关不是一刀切禁用,而是「改为逐应用询问」。

先探测,再决定要不要提交

getAgentState() 返回的 submitAllowed 是只读探测,不会弹窗。想避免打断用户的应用可以据此降级:submitAllowed 为 false 时就只填草稿并提示用户自己按发送。另外 agent 没有「运行结束」事件,提交后需要轮询 getAgentState().isRunning。终端 run 脚本的 bundleId 带 ::terminal-run::<pid> 后缀,授权会归到宿主应用名下,不会每跑一次都重新问。

侧边栏示例

js
// @require dom
// @require model

// 浏览器能力探测放在页面初始化阶段,不要等到 click 内再 await
const sidebarSupported = await LFRuntime.sidebar.isSupported();

function addButton(label, onClick) {
  const button = LFRuntime.trackNode(document.createElement("button"));
  button.textContent = label;
  document.body.appendChild(button);
  LFRuntime.listen(button, "click", onClick);
}

addButton("让 Agent 分析本页", async () => {
  if (!sidebarSupported) return;
  // open 必须是 click 回调中的第一个异步调用
  const result = await LFRuntime.sidebar.open();
  if (result.alreadyOpenInAnotherWindow) {
    // 已开在别的窗口:不会重复打开,提示用户切换过去
    LFDOM.toast("侧边栏已在其它窗口打开,窗口 ID " + result.windowId);
    return;
  }

  // input / setModel 要求侧边栏已打开,所以放在 open() 之后
  await LFRuntime.sidebar.input("请分析当前页面的商品信息");
  // 默认覆盖草稿、只填入文字,由用户自己按发送

  // 分几次拼一段长内容时才用追加
  await LFRuntime.sidebar.input("补充:只看近 30 天的订单", { mode: "append" });
});

addButton("让 Agent 直接导出 CSV", async () => {
  // 不调用 open(),所以可以先 await 其它方法
  const { open } = await LFRuntime.sidebar.getState();
  if (!open) {
    LFDOM.toast("请先打开 Agent 侧边栏");
    return;
  }

  // 从当前目录选择对话模型
  const { models } = await LFModel.listModels();
  const chatModel = models.find((model) => model.kind === "chat");
  if (!chatModel) {
    LFDOM.toast("当前列表无可用对话模型");
    return;
  }
  // 若模型不在快捷列表会自动添加;模型不存在时直接抛出异常
  await LFRuntime.sidebar.setModel(chatModel.id);

  // 想让 agent 直接开跑:先看执行状态,避免撞上正在运行的一轮
  const agentState = await LFRuntime.sidebar.getAgentState();
  if (agentState.runStatus === "idle" && agentState.ready) {
    // submitAllowed=false 说明这一次会弹授权窗,可据此降级为只填草稿
    const sent = await LFRuntime.sidebar.input("统计本页所有商品并导出 CSV", {
      submit: agentState.submitAllowed,
    });
    if (sent.submitted) {
      // 没有"运行结束"事件,需要轮询
      while ((await LFRuntime.sidebar.getAgentState()).isRunning) {
        await LFRuntime.sleep(1000);
      }
      LFDOM.toast("Agent 已完成");
    }
  }
});

示例

js
// LFRuntime 无需 @require,自动注入到每个应用
LFRuntime.print(LFRuntime.bundleId, LFRuntime.meta.name);

// 热重载友好的挂载:重新注入时自动清理旧节点
LFRuntime.mountHot("main-panel", (hot) => {
  const el = document.createElement("div");
  // 头部声明了 `// @resource panel ./panel.html` 时读到其内容;未声明的别名返回空串
  const html = LFRuntime.getResource("panel");
  if (html !== "") {
    el.innerHTML = html;
  } else {
    el.textContent = "panel";
  }
  document.body.appendChild(el);
  hot.onDispose(() => el.remove());
});

// 注入样式:同一 styleKey 重复调用会替换而非叠加
LFRuntime.appendStyle(".lf-btn { color: red; }", "theme");