Appearance
JSAPI
在你的页面(浏览器端)里引入:
html
<script src="https://<你的机灵兔部署域名>/api/open/jsapi/jlt.js"></script>方法速查
| 方法 | 需要能力 | 说明 |
|---|---|---|
ready(cb) / getContext() | — | 容器就绪/上下文(用户/组织/平台) |
requestAuthCode() | — | 重新取一次性免登码 |
selectUsers(opts?) | —(用户亲手挑即授权) | 系统选人组件 |
openChat({user_id}) 或 openChat({group_id}) | — | 跳到与该成员的单聊,或打开某个群聊(群聊变体,v1.6.0) |
alert / confirm / toast | — | 系统样式对话框/轻提示 |
setTitle / close | — | 窗口标题/关闭 |
minimize / toggleMaximize / isMaximized / toggleFullscreen / isFullscreen / startDragging | — | 窗口无边框控制(v1.7.0,仅桌面端):应用自绘顶部栏时接管窗口 |
reload / reloadForce | — | 就地刷新 / 强制刷新(重签免登码 + 绕缓存,v1.7.0) |
setToolbarVisible(visible) | — | 显示/隐藏壳自带顶部工具栏(v1.7.0,仅桌面端) |
openGrants() | — | 唤起「始终允许」授权管理面板(v1.7.0,仅桌面端) |
chooseFile(opts?) | —(用户亲手选即同意) | 原生打开文件对话框(v1.6.0,仅桌面端) |
saveFileAs(opts) | —(用户亲手选保存位置即同意) | 原生另存为对话框(v1.6.0,仅桌面端) |
share(opts) | im | 向同组织成员分享文本/链接(v1.6.0,壳弹选人组件,以用户身份发出) |
localFetch({url…}) | local | 从用户电脑抓网页(桌面端 · 首访确认,可「始终允许」,按 应用+组织+类别 记忆 30 天) |
browserExtract({url…}) | local | 无头浏览器提取(桌面端 · 首访确认,可「始终允许」,与抓取类别分开记忆) |
agent({messages…}) | ai + local | 完整 AI 能力:交给客户端内置 Agent,可用技能/系统工具/本地数据库(桌面端 · 每轮确认全文展示任务,v1.6.1 起无「始终允许」;本机命令按危险度分级确认,且不开放带登录态的浏览器工具) |
agentStop() | 同上 | 停止当前运行中的 agent 轮次 |
startChat({message}) | ai | 把任务改到主窗口新建对话(v1.8.0,仅桌面端):jlt.agent 不开放带登录态的浏览器工具,需要真实登录浏览器的任务改走这里——无返回值,过程/结果不回传 |
revealFile({file_id}) | — | 在文件管理器里高亮显示 Agent 的本机产物(files[].id,桌面端) |
browserOpen({url…}) | local | 开隔离浏览器会话:多步爬取/自动化(桌面端 + 目标/新 origin 逐次确认,无「始终允许」) |
browserAct({session_id,action,…}) | local | 会话内受控动作:click/type/fill/press/wait/back |
browserRead / browserSnapshot / browserScreenshot / browserClose | local | 读正文 / 可访问性快照(ref)/ 截图 dataURL / 关会话 |
会挂载全局 window.jlt。也可以使用平台提供的 TypeScript 封装 @ai-im/org-app-sdk(纯类型 + Promise 化,协议实现仍是上面这份 JS,见 SDK 参考)。
平台差异
同一份 SDK 自动适配桌面与移动容器,所有方法两端行为一致;差异只有:
| 差异点 | 桌面端 | 移动端 |
|---|---|---|
getContext().platform | "desktop"(独立应用窗口) | "mobile"(内嵌 WebView 页) |
openChat | 聚焦主窗口并打开单聊/群聊,应用窗口保留 | 直接推入聊天页,返回键回到应用 |
| 对话框/选人 | 壳的原生 DOM 弹层 | 系统原生弹窗/选人页 |
| 窗口标题 | 设置独立窗口标题 | 设置页面导航栏标题 |
chooseFile / saveFileAs | ✅(v1.6.0,不需要 scope) | ❌ 返回“仅桌面端支持本机文件访问” |
minimize / toggleMaximize / toggleFullscreen / startDragging 等窗口控制 | ✅(v1.7.0,不需要 scope) | ❌ 返回“unknown method”(无独立窗口可控制) |
share | ✅(需 im 能力) | ❌ 返回“仅桌面端支持分享” |
localFetch / browserExtract | ✅(需 local 能力;首访用户确认,可「始终允许」,30 天有效) | ❌ 返回“仅桌面端支持本地能力” |
agent / agentStop | ✅(需 ai + local 能力 + 每轮用户确认) | ❌ 返回“仅桌面端支持完整 AI 能力” |
startChat | ✅(需 ai 能力 + 每次确认,无返回值) | ❌ 返回“unknown method”(无主窗口/多对话概念) |
在普通浏览器里打开时所有调用 15 秒超时 reject——务必先判断 window.jlt 做降级(完整对照见 Demo)。例外: agent 不受 15 秒限制(一轮任务可能运行很久),由最终响应或错误收尾。
API
jlt.ready(callback)
容器就绪后触发一次,参数是当前上下文。重复调用会立即用已缓存的上下文触发。
ts
jlt.ready((ctx) => {
console.log(ctx.user.nickname, ctx.org_id);
});jlt.getContext(): Promise<Context>
跟 ready 等价的 Promise 形式。
ts
interface Context {
app_id: string;
org_id: string;
org_name: string; // 当前组织上下文的名称——同一个应用可能被多个组织安装,
// 展示用;分区/鉴权仍以 org_id 为准,组织可以改名
user: { user_id: string; nickname: string; avatar_url: string };
locale: string; // "zh-CN" | "en" | "zh-TW" | "ja"
platform: string; // "desktop" | "mobile"
}jlt.requestAuthCode(): Promise<{ auth_code: string; expires_in: number }>
重新签发一枚一次性免登授权码(5 分钟有效),用于会话续期。
jlt.setTitle(title: string): Promise<void>
设置应用窗口标题。
jlt.close(): Promise<void>
关闭应用窗口。
窗口无边框控制(v1.7.0,仅桌面端)
应用窗口没有 OS 原生标题栏(无边框),默认由壳自带的顶部工具栏提供最小化/ 最大化/关闭;应用要完全自绘顶部样式时,先 jlt.setToolbarVisible(false) 隐藏自带工具栏,让页面铺满整个窗口,再用下面这组方法自行实现。全部不需要 任何 scope——它们只操作应用自己的窗口。
除 isMaximized/isFullscreen(只读查询)外,其余方法(minimize/ toggleMaximize/toggleFullscreen/startDragging/reload/reloadForce/ setToolbarVisible)限频 10 秒 30 次——脚本循环调用会让窗口不停闪烁/ 拖动,超限直接 reject。
jlt.minimize(): Promise<void>
最小化应用窗口。
jlt.toggleMaximize(): Promise<{ maximized: boolean }>
切换最大化/还原,返回切换后的状态(供自绘图标同步)。
jlt.isMaximized(): Promise<{ maximized: boolean }>
查询当前是否最大化。平台不推送状态变化事件(比如系统级 Aero 贴靠触发的最 大化),需要校准时主动查询即可,常见时机是窗口重新获得焦点。
jlt.toggleFullscreen(): Promise<{ fullscreen: boolean }> / jlt.isFullscreen(): Promise<{ fullscreen: boolean }>
切换/查询全屏状态。
jlt.startDragging(): Promise<void>
发起窗口拖动——在自绘顶部栏的 mousedown 事件里调用(双击切换最大化需要 应用自己判断 event.detail === 2 并调用 toggleMaximize(),跟拖动共用同一个 mousedown 处理函数,不要拆成两个事件监听,否则原生拖动会吞掉后续的 dblclick):
ts
topBar.addEventListener("mousedown", (e) => {
if (e.button !== 0 || (e.target as HTMLElement).closest("button")) return;
if (e.detail === 2) { jlt.toggleMaximize(); return; }
jlt.startDragging();
});jlt.reload(): Promise<void>
就地刷新页面,尽量复用引擎缓存;跨源刷新被引擎拒绝的极端情况下自动降级为 reloadForce()。
jlt.reloadForce(): Promise<void>
强制刷新:重新签发一次性免登码并绕过缓存整页重载(这是 v1.7.0 之前壳自带 工具栏「刷新」按钮的行为)。
jlt.setToolbarVisible(visible: boolean): Promise<void>
显示/隐藏壳自带的顶部工具栏。隐藏后页面铺满整个窗口,此时应用需要自行绘制 顶部栏并用本节这组方法接管窗口控制,否则用户将失去关闭/最小化/最大化等 入口(jlt.openGrants() 除外——那个面板始终由壳渲染,不受此开关影响)。
jlt.openGrants(): Promise<void>
唤起壳的「始终允许」授权管理面板(localFetch/browserExtract 的按 origin 授权列表),隐藏自带工具栏后仍可调用。
jlt.selectUsers(opts?): Promise<{ users: User[] }>
系统选人组件——弹出客户端原生的成员选择窗(带搜索),用户挑选后返回:
ts
const { users } = await jlt.selectUsers({ multiple: true, max: 20 });
// users: [{ user_id, nickname, avatar_url, title }]| 参数 | 类型 | 说明 |
|---|---|---|
multiple | boolean | 多选(默认单选:点一位即返回) |
selected | string[] | 打开时预选中的 user_id |
title | string | 选人窗口标题 |
max | number | 多选最多可选人数(1–100,默认 100) |
用户取消时返回 { users: [] }。不需要任何能力(scope):成员由用户亲手 挑选,你的页面只会拿到被选中的那几位——用户挑选即用户授权。
jlt.openChat({ user_id }): Promise<void>
把用户带到 IM 里打开与该成员的单聊(桌面端聚焦主窗口;移动端直接推入聊天页); 应用窗口保持不动。适合「找 TA 催一下」这类跳转。
jlt.openChat({ group_id }): Promise<void>
群聊变体(v1.6.0)——同上,但打开的是某个群聊。group_id 与 user_id 二选一(同时传时以 user_id 为准)。真实权限由 IM 打开会话时自然校验(非成员 打不开),与单聊变体同一套信任模型。仅桌面端。
jlt.chooseFile(opts?): Promise<{ files }>
原生打开文件对话框(v1.6.0,仅桌面端)——不需要任何 scope,用户在系统 对话框里亲手选择即同意(同浏览器 <input type="file"> 同一套许可模型)。
ts
const { files } = await jlt.chooseFile({ multiple: false, accept: ["png", "jpg"] });
if (files.length && !files[0].error) {
console.log(files[0].name, files[0].mime, files[0].data_base64.length);
}opts.multiple:多选(默认单选)。opts.accept:扩展名白名单(不带点),仅作对话框过滤器展示,用户仍可切到 “所有文件”。- 单文件上限 20MB;多选累计上限 60MB——按选中顺序读取,一旦剩余预算不够就 给该文件(及之后的文件)返回
error,不会把超限的内容读进内存再返回。 - 单文件上限 20MB,超出的条目带
error而不是data_base64。用户取消 →files: []。
jlt.saveFileAs(opts): Promise<{ ok }>
原生另存为对话框(v1.6.0,仅桌面端)——同样不需要 scope。
ts
const { ok } = await jlt.saveFileAs({ filename: "report.pdf", data: base64OrDataUrl });
// ok === false 表示用户取消了保存对话框,不是错误data:纯 base64 字符串,或带data:...;base64,前缀的 dataURL(SDK 自动剥除前缀)。- 上限约 50MB。
jlt.share(opts): Promise<{ ok, user_id? }>
向同组织成员分享一段文本/链接(v1.6.0)——壳弹出系统选人组件让用户亲手选一位同组织 成员,消息以用户自己的身份发出(带【应用名】前缀)。需要应用被授予 im 能力( 与 /open/im/send 同一道闸门,即使发起方是用户本人也不例外)。
ts
const { ok, user_id } = await jlt.share({ title: "报销已批准", url: "https://app.example.com/x" });
// ok === false 表示用户取消了选人,不是错误jlt.alert({ message, title?, button? }): Promise<void>
系统样式警告框,用户点掉按钮后 resolve。
jlt.confirm({ message, title?, okText?, cancelText? }): Promise<{ ok }>
系统样式确认框:
ts
const { ok } = await jlt.confirm({ message: "确认提交这份报销单?" });
if (!ok) return;jlt.toast({ message, type?, duration? }): Promise<void>
轻提示,自动消失且不遮挡页面交互。type: "info" | "success" | "error", duration 毫秒(500–6000,默认 2200)。
jlt.localFetch({ url, max_chars? }): Promise<{ url, text, truncated, length }>
从用户电脑抓取网页正文(走用户本机网络——能访问内网、带本地代理)。 需要应用被授予本地能力(local);目标地址弹窗请用户确认(弹窗把解析后的 协议://主机:端口 单独展示,拒绝即失败),确认框可选**「始终允许」**——按 「应用 + 组织 + 能力类别」分别记忆、30 天有效,应用窗口工具栏「授权管理」可 随时撤销(授权不跨组织共用,过期自动重新询问)。 不跟随重定向——目标返回 30x 直接报错,请自己拿规范地址来;URL 含 user@host 形式会被拒绝。仅桌面端。
jlt.browserExtract({ url, max_chars? }): Promise<{ url, text, truncated, length }>
无头浏览器打开页面并提取正文——JS 渲染的页面(SPA、动态表格)用这个。 隔离实例:临时浏览器配置、不带用户任何登录态、用完即杀,跟你日常浏览器 完全隔离;导航后校验最终地址与确认的地址同源,被重定向到其它站点会中止。 同样需要 local 能力;确认策略同 localFetch(首访确认,可「始终允许」—— 但与 localFetch 的授权分开记忆)。仅桌面端。
尊重确认弹窗
本地能力的授权模型是「组织管理员开 scope + 用户确认(可选始终允许,按类别 记忆且 30 天过期)」双层设计——不要高频调用把用户淹没在弹窗里;批量抓取应 合并为少量请求,或在你的后端做缓存。「始终允许」完全由用户主动选择,应用 无法绕过确认。
jlt.agent({ messages, model?, on? }): Promise<{ content, files, stopped }>
完整 AI 能力:把对话交给客户端内置 Agent 执行——Agent 拥有与客户端 AI 对话一致的能力:该用户已启用的技能、系统工具(文件/Shell/代码/ 浏览器)、本地数据库与服务端工具。模型自行决定调用什么工具,页面只负责 展示过程与结果。
ts
const result = await jlt.agent({
messages: [{ role: "user", content: "总结 ~/Downloads 里最近一周的文件" }],
model: "deepseek-chat", // 可省略,用客户端默认
on: {
// 全部可选;工具卡片建议按 calls[].id 建卡、tool_done 按 call_id 回填
delta: ({ delta }) => {}, // 正文增量
tool_start: ({ calls }) => {}, // 工具开始(含本机工具)
tool_done: ({ results }) => {}, // 结束(runtime: local/cloud)
local_tool_start: ({ tool }) => {}, // 即将在用户电脑上执行
local_tool_result: ({ tool, ok }) => {},
files: ({ files }) => {}, // 生成的文件(累计)
subtask_start: (e) => {}, subtask_done: (e) => {},
truncated: () => {}, // 本轮工具迭代达上限
},
});
// result: { content, files: [{ name, url?, size?, id? }], stopped }files 分两类:云端工具产物带 url(https 下载直链);本机工具产物只带 不透明 id——本地路径不进页面,调 jlt.revealFile 让 桌面壳在系统文件管理器里高亮显示该文件(文件不出本机)。
jlt.revealFile({ file_id }): Promise<{ ok }>
在系统文件管理器里高亮显示 Agent 生成的本机文件(macOS 访达 -R / Windows 资源管理器 /select)。file_id 来自 jlt.agent 结果或 files 事件里 条目的 id。
- 仅桌面端;路径只存在壳的 id 表里,页面拿不到任何本地路径或文件内容;
- 只能翻出本窗口 Agent 真实产出过的文件,限频 10 次/分钟;
- 应用窗口重开后 id 失效(文件还在原处,让 Agent 重新生成即可重新拿到 id)。
要求与边界:
- 需要应用同时被授予
ai与local能力;仅桌面端; - 每轮开始前桌面壳弹原生确认框(应用名 + 最后一条 user 消息全文), 拒绝即失败;没有「始终允许」——记住一次等于放行后续任意任务。应用发起的 本机命令按危险度分级确认:普通命令首次弹窗可选「本轮不再询问」(按命令 签名在这一次任务内复用,任务结束即失效、不持久化);以下两类永不豁免、 每次都要单独确认:① 破坏性命令(
rm -rf/sudo/crontab/git push --force等); ② 外发 / 读凭证 / 执行远端代码(curl/wget/scp/ssh等外发命令,.ssh//.aws//.env/.pem/shell 历史/浏览器 Cookie 库等凭证路径,以及… | sh/eval/base64 -d)。三方应用不沿用用户为自己 Agent 设的任何自动放行策略 (含全局静默); - 本机工具走白名单:只开放"每次调用都经用户确认"或"根本不碰用户数据"的 能力(当前:
shell_exec、原生另存为、文件管理器定位、系统通知)。代码执行类 能力一律在沙箱里跑:macOS 上跑在本机内核级沙箱(seatbelt,断网 + 只能读写 专用工作区,子进程也逃不出去),其它平台自动跑在云端 Docker 沙箱(无网络、无 Linux capabilities、512MB/1CPU)。两条路应用侧都无需改代码,区别只是沙箱在哪。 需要真实本机环境(读用户文件/已登录浏览器/本机技能)的任务,改用jlt.startChat()交回用户主窗口执行; - 不开放走用户登录态浏览器的
browser_*工具(要浏览器用隔离会话 JSAPI); messages只接受user/assistant角色(system会被剥离——能力与安全 约束由受信壳注入),最多 40 条、每条 32000 字;同一窗口同时只允许一轮任务;- 计费与用户在客户端里的 AI 对话一致(个人钱包或组织钱包),应用侧不接触额度 凭证。
jlt.agentStop(): Promise<{ stopped: boolean }>
停止当前运行中的 agent 轮次;已生成的内容会随该轮结果正常返回 (stopped: true)。没有运行中的轮次时静默成功。
jlt.startChat({ message }): Promise<{}>(v1.8.0,仅桌面端)
jlt.agent 为安全故意剔除了 browser_* 工具(不给应用借道驱动用户真实登录的 日常浏览器)。需要真实登录态浏览器能力的任务(比如登录某网站抓数据)无法通过 jlt.agent 完成,只能改用 jlt.startChat:把任务改成在用户自己主窗口里 新开一个普通对话发出去——跟用户亲手在输入框敲字发送完全等价,走客户端正常 Agent 通道(包含真实登录态的 browser_*)。
js
await jlt.startChat({ message: "请运行技能「XXX」完成采集并同步" });
// 无返回内容——新对话在主窗口打开,本窗口不感知后续过程/结果。
// 需要查看结果,请让应用自己的后端提供一个持久化的进度查询接口。- 只需
ai能力(不需要local)——执行完全发生在主窗口自己的常规 Agent 通道里,不受组织应用能力限制约束; - 每次调用弹窗展示完整消息文本(跟
jlt.agent的任务全文确认同一安全 等级),无「始终允许」; - 无返回值、无事件回调,过程/结果不会回传到调用方页面。
不想手写事件驱动的 UI?SDK 2.0 的
mountChat(el, { channel: "auto" })已内置 agent 通道:工具调用/本机执行/子任务/生成文件的实时卡片、流式正文、 降级到后端中继,全部开箱即用。
隔离浏览器会话(JSAPI v1.4.1,桌面端)
localFetch/browserExtract 是一次性动词;browserOpen…browserClose 把同一 Playwright 引擎开放成多步会话——开页 → 快照拿 ref → 点击/输入/翻页 → 读取/ 截图 → 关闭。跑在隔离实例上(临时 profile、强制无头、无任何登录态、用完即 杀),与客户端 AI 对话里那个带登录态的浏览器是两套信任等级。
js
const s = await jlt.browserOpen({ url: "https://example.com/list" });
// → { session_id, url, origin, title, text, truncated, length }
const snap = await jlt.browserSnapshot({ session_id: s.session_id });
// snap.text 形如:- button "下一页" [ref=e12] ← ref 就是动作的 target
await jlt.browserAct({
session_id: s.session_id, action: "click",
element: "下一页", target: "e12",
});
const page2 = await jlt.browserRead({ session_id: s.session_id, max_chars: 50000 });
const png = await jlt.browserScreenshot({ session_id: s.session_id });
await jlt.browserClose({ session_id: s.session_id });browserAct 动作一览
| action | 参数 | 说明 |
|---|---|---|
click | element, target | 点击元素 |
type | element, target, text, submit? | 向输入框输入(submit: true 输入后回车) |
fill | fields: [{name?, type?, target, value}] | 批量填表(textbox/checkbox/radio/combobox/slider) |
press | key | 按键(如 Enter、ArrowDown) |
wait | text?, textGone?, time? | 等待文本出现/消失或等指定秒数 |
back | — | 后退一页 |
target 统一用快照 ref(如 e12)或唯一 CSS 选择器。不提供任意 JS 执行 (evaluate 不对三方开放)。
跨源跳转:need_consent 流程
会话内点到外链或被重定向到未确认 origin 时:
- 读类操作(read/snapshot/screenshot)resolve
{ need_consent, url }——桌面壳 弹窗请用户确认;同意后该 origin 入会话白名单,重发同一调用即返回内容; 拒绝则应用可back或关会话。 act分两条路径(2026-08-27 起):- 能预解析的跳转(点击链接/提交表单——壳先在页面里解析目标元素的
a[href]/form[action]):动作不执行,resolve 带pending: true的{ need_consent, url };用户同意后壳自动重发该动作,应用这次拿到的就是 真实结果。 - JS 动态导航(无法预解析):守卫仍在动作之后(不带
pending,动作已 执行)——同意后壳只把 origin 入白名单、不重跑动作,响应带consented: true。
- 能预解析的跳转(点击链接/提交表单——壳先在页面里解析目标元素的
- 未确认 origin 的页面内容一个字节都不会返回。
限额与生命周期
每窗口 2 会话 / 全局 4 / 单会话 200 动作 / 空闲 10 分钟 / 最长 30 分钟自动回收; 关闭应用窗口同样回收;截图 ≤3MB(base64 PNG)。需要 local 能力;目标地址与 每个新 origin 逐次由用户确认——这条通道没有「始终允许」:多步自动化不能被 一次授权整段放行(抓网页/无头提取的「始终允许」不适用于会话)。
降级处理
不在机灵兔客户端里打开时(例如开发者直接用普通浏览器预览页面),所有调用会 在 15 秒后超时 reject。建议先判断:
ts
if (window.jlt) {
// 在客户端里
} else {
// 普通浏览器,降级展示
}使用 SDK 时对应 isInJltClient()。