内嵌应用集成操作手册(jsBridge)
面向把企业 Web 页面装进 QwenWork 桌面客户端的开发团队:一张图看懂整体架构与两种承载形态,其余篇幅全部给 JSAPI。
1 · 整体架构与接入

1.1 两种承载形态
客户端提供两种模式把企业页面装进来,JSAPI 完全一致(同名同参同事件,bridgeVersion: 1.0.0),只是「挂在哪、承载什么、怎么承载」不同:
| 模式 | 挂在哪 | 承载什么 | 怎么承载 |
|---|---|---|---|
| 内嵌应用(WebView) | 侧边栏入口(后台自定义链接选「在 QwenWork 内打开」) | 企业自己的业务系统页(知识库、OA、工单等) | preload 注入 window.qwBridge;常驻,切走只隐藏 |
| iframe(企业市场页) | 技能广场、专家套件(及连接器)的「企业专属」市场页 | 企业自己的私有市场/广场页(技能、套件、连接器目录) | postMessage + 自写 callBridge;激活 tab 才挂载,切走即卸载 |

内嵌应用(WebView)· 侧边栏入口

iframe · 技能广场 / 专家套件「企业专属」市场页
1.2 后台配置
- 路径:个性化配置 → 导航配置 → 自定义链接,打开方式选「在 QwenWork 内打开」即有桥;
- 高级选项(可选):路由标识
route(deeplink 用)、离线包 zip、环境变量envVars; - 保存节点后必须在导航配置页发布才下发到客户端。
2 · JSAPI
按域分类,每个方法给调用示例与返回说明。WebView 用 window.qwBridge,iframe 用自写的 callBridge,方法名与参数完全相同。
2.1 两种调用方式
方法名与参数两形态完全相同。WebView 由宿主直接注入 window.qwBridge;iframe 宿主不注入任何脚本,页面用 postMessage 自己封一个 callBridge(下方给完整可抄实现)。
WebView · 宿主注入 window.qwBridge
const qw = window.qwBridge
const info = await qw.invoke("app.getInfo")
const r = await qw.invokeSafe("skill.list") // { ok:true, data } | { ok:false, error }
qw.on("skills:changed", reload)iframe · postMessage 完整封装(可直接抄)
// 信封:请求 bridge-request / 响应 bridge-response / 事件 bridge-event
// 响应形如 { type:"bridge-response", id, success, data?, error? }
const pending = new Map() // id → { resolve, reject }
const listeners = new Map() // event → Set of handler
window.addEventListener("message", (ev) => {
const msg = ev.data
if (!msg || typeof msg !== "object") return
if (msg.type === "bridge-response") {
const p = pending.get(msg.id)
if (!p) return
pending.delete(msg.id)
msg.success ? p.resolve(msg.data) : p.reject(new Error(String(msg.error)))
} else if (msg.type === "bridge-event") {
;(listeners.get(msg.event) || [ ]).forEach((fn) => fn(msg.data)) // 宿主双推新旧信封,只监听一种
}
})
function callBridge(method, params = {}) {
const id = crypto.randomUUID()
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error("timeout: " + method)) }, 10000)
pending.set(id, {
resolve: (v) => { clearTimeout(timer); resolve(v) },
reject: (e) => { clearTimeout(timer); reject(e) },
})
window.parent.postMessage({ type: "bridge-request", id, method, params }, "*")
})
}
function onBridgeEvent(event, fn) {
if (!listeners.has(event)) listeners.set(event, new Set())
listeners.get(event).add(fn)
return () => listeners.get(event).delete(fn)
}
// 使用:等 bridge:ready 后再发起调用
onBridgeEvent("bridge:ready", (d) => console.log("bridge ready", d.version))
const info = await callBridge("app.getInfo")
onBridgeEvent("skills:changed", () => reload())- 首屏即可调用,无需握手;推荐
invokeSafe拿结构化错误信封。 - 宿主事件两形态同名:
skills:changed/connectors:changed/expertKits:changed/connector.statusChanged/connector.toolsChanged/page.themeChanged/page.languageChanged,异步任务另有<方法>:progress/:complete(带requestId)。 - WebView 宿主不设超时(页面自加);iframe 默认 10s。
2.2 app.\ / auth.\(应用与登录态)
app.\ 查内嵌应用自身信息与宿主桥能力;auth.\ 取登录态与企业后端调用凭据。以下逐个方法说明用途与返回。
app.getInfo · 获取当前内嵌应用的基本信息
返回后台配置的应用 ID、展示名、路由标识与本次发布版本号,页面据此确认「自己是哪个应用、哪个版本」。
await bridge.invoke("app.getInfo")
// → { embeddedAppId:"enterprise-kb", name:"企业知识库", route:"enterprise-kb", version:"2026.09.05-143022" }app.ping · 桥连通性探活
轻量活体检查、立即返回,供页面初始化时判断桥是否可用。
await bridge.invoke("app.ping") // → { pong:true }app.getEnv · 读取后台下发的环境变量
读取后台自定义链接里为本应用配置的环境变量(如租户 ID),见 1.2 高级选项;未配置时返回空对象。
await bridge.invoke("app.getEnv") // → { envVars:{ TENANT_ID:"abc123" } }(未配置为 {})app.getCapabilities · 查询桥支持的能力清单
返回桥版本、可调用方法列表、可订阅事件列表与错误协议,供页面做能力检测与降级。
await bridge.invoke("app.getCapabilities")
// → { bridgeVersion:"1.0.0", methods:[…], events:[…], errorProtocol }auth.getLoginState · 获取登录态与当前用户信息
判断用户是否已登录并取用户基本信息;未登录时 user 为 null,页面可自行引导登录。
const { isAuthenticated, user } = await bridge.invoke("auth.getLoginState")
// user = { id, name, username, email, avatarUrl, tier, isBiz, orgId, orgName, teamId };未登录 user:nullauth.getToken · 获取调企业后端的令牌
返回 Bearer token 与过期时间,页面调自己企业后端时放入 Authorization 头;按 expiresIn 到期重取,不要长期缓存。
const { token, expiresIn } = await bridge.invoke("auth.getToken")
// → { token:"eyJ…", tokenType:"Bearer", expiresAt, expiresIn:287 }auth.logout · 退出当前账号登录
通知宿主发起登出流程;接口先返回、宿主随后登出。
await bridge.invoke("auth.logout") // → { ok:true },响应先于登出返回user.id 是跨系统唯一标识;登录态与 token 都不要长期缓存,token 禁止写日志/URL/localStorage。
2.3 host.\ / agentRuntime.\(环境与设备)
await bridge.invoke("host.getEnvironment")
// → { inQwenWork:true, product, productDisplayName, version, edition:{tier,deployment}, isPremium, platform }
const info = await bridge.invoke("host.getClientInfo")
// info.app = { version, name, locale, packaged, edition:{tier,deployment}, isPremium }
// info.runtime = { electron, chrome, node }
// info.os = { platform, arch, label, version, kernelRelease }
// info.device = { machineId, serialNumber, macAddress, model, deviceName, loginUsername, cpuModel, cpuCores, totalMemoryMb }
const { homedir } = await bridge.invoke("host.getHomedir") // → { homedir:"/Users/example" }
await bridge.invoke("host.fileExists", { path: `${homedir}/x` }) // → { exists:true }
await bridge.invoke("host.directoryExists", { path: `${homedir}/.d` }) // → { exists:true }
const reg = await bridge.invoke("agentRuntime.getRegistration", { key: "agentCode" })
// → { status:"available", value } | { status:"not-registered" } | { status:"unsupported-key" }getEnvironment 是轻量判定(调不通即普通浏览器);getClientInfo 设备字段采集失败回落空字符串,有 60s 缓存;路径检测只收绝对路径,相对/~ 报 INVALID_PARAMS。
2.4 page.*(页面与跳转)
await bridge.invoke("page.getTheme") // → { theme:"light"|"dark", themeVariant:"dark-glass" }
await bridge.invoke("page.getLanguage") // → { language:"zh-CN" }
await bridge.invoke("page.openUrl", { url: "https://example.com" }) // 系统浏览器打开,仅 http(s)
// 带技能/套件开新会话并预填 mention;skillName 与 pluginName 二选一(本地目录名)
await bridge.invoke("page.toIndex", { skillName: "weekly-report", prompt: "帮我写本周周报" })
// 切到客户端原生技能页并定位;target: explore(市场) / mine(已装) / detail(需 skillId)
await bridge.invoke("page.navigateSkillCenter", { target: "detail", skillId: "code-review" })
// 关闭承载当前页面的视图;不传 name 即关自己
await bridge.invoke("page.close") // → { ok:true }toIndex 的 skillName/pluginName 二选一(本地目录名),未安装报 SKILL_NOT_FOUND/EXPERT_KIT_NOT_FOUND;close 不传 name 即关自己。
2.5 skill.*(技能)
const { installedSkills } = await bridge.invoke("skill.list")
// item = { skillId, name, displayName, description, iconUrl?, installSource?, isUserEnable, category?, extension:{sourceType, version?, installedVersion?} }
await bridge.invoke("skill.installFromUrl", { url: "https://…/x.zip", installSource: "ant-market", iconUrl: "https://…/x.png" })
// → { success, skill_id, name, version, installSource?, iconUrl? }
const { filePath } = await bridge.invoke("skill.upload") // 拉文件选择器;取消为 ""
const { requestId } = await bridge.invoke("skill.upload.start", { filePath, force: false })
// 异步:skill.upload:progress / :complete(带 requestId;同名回 requireOverwrite,带 force:true 重试)
await bridge.invoke("skill.aoneKitInstall", { name: "x", displayName: "X" }) // 异步 :progress/:complete
await bridge.invoke("skill.update", { name: "x", displayName: "X" }) // 异步 :progress/:complete
await bridge.invoke("skill.setEnabled", { skillId: "code-review", enabled: false })
await bridge.invoke("skill.remove", { skillId: "code-review" })
await bridge.invoke("skill.getPackage", { skillId: "code-review" }) // → { zipBase64, fileName, fileSize }
await bridge.invoke("skill.create", { prompt: "做个周报转 PPT 的技能" }) // 跳新对话预填创建引导语
const detail = await bridge.invoke("skill.detail", { skillId: "code-review" })
// → { source, name, displayName, description, markdown, version?, installSource?, iconUrl? }
const { examples } = await bridge.invoke("skill.examples", { folderName: "weekly-report" })
await bridge.invoke("skill.accessPolicy") // → { publicMarketVisible, personalUploadEnabled, teamSubmissionEnabled, enterpriseEligible }
await bridge.invoke("skill.checkUpdates") // → { updates:{ [folderName]:{ hasUpdate, remoteVersion? } } }
await bridge.invoke("skill.checkConflict", { folderName: "weekly-report" }) // → { type, existingVersion? }
const page1 = await bridge.invoke("skill.market.list", { keyword: "周报", page: 1, pageSize: 20 })
await bridge.invoke("skill.market.install", { folderName: page1.skills[0].name })skillId/name 都是本地目录名;upload.start/aoneKitInstall/update/installFromZip 是异步任务(:progress/:complete);accessPolicy 只读不拦,按钮显隐是页面自己的事。
2.6 connector.*(连接器 / MCP)
const { connectors } = await bridge.invoke("connector.list")
// item = { serverName, source, transport, displayName(多语言对象), description, authState?, capabilities?, 编辑回显字段 }
await bridge.invoke("connector.saveConfig", { servers: [{
serverName: "adl-kb", url: "https://mcp.example.com", type: "http",
displayName: { zh: "知识库", en: "KB" }, installSource: "ant-market",
}] }) // upsert;addCustom 只新增(同名保护)、update 改单条
await bridge.invoke("connector.remove", { serverName: "adl-kb" }) // 或 { connectorId }
await bridge.invoke("connector.setEnabled", { serverName: "adl-kb", enabled: true })
const { servers } = await bridge.invoke("connector.status") // connected/failed/needs-auth + tools/error
bridge.on("connector.statusChanged", ({ servers }) => /* 按 serverName 局部更新连接态 */)
await bridge.invoke("connector.accessPolicy") // → { mode, restricted, blockedSources, blockedIds }
await bridge.invoke("connector.market.list") // → { connectors, categories }
await bridge.invoke("connector.market.install", { serverId: "…" })
await bridge.invoke("connector.auth", { serverName: "adl-kb", action: "start" }) // clear = 重置认证
await bridge.invoke("connector.tools", { serverName: "adl-kb" }) // → { tools:[{name,description,enabled}] }
await bridge.invoke("connector.setToolEnabled", { serverName: "adl-kb", toolName: "search", enabled: false })
await bridge.invoke("connector.open", { serverName: "adl-kb", triggerAuth: true }) // 宿主原生详情弹窗
await bridge.invoke("connector.diagnose", { serverName: "adl-kb" }) // → { logPath, status? }displayName/description 列表返回是多语言对象,页面按 page.getLanguage 索引并自行回落;启停统一用 setEnabled;内置连接器(source:"builtin")只读,配置与授权走 connector.open 宿主弹窗。
2.7 expertKit.\ / storage.\ / enterprise.*(套件 / 存储 / 企业票据)
const { expertKits } = await bridge.invoke("expertKit.list")
await bridge.invoke("expertKit.install", item) // → { success, folderName, pluginName, version }
await bridge.invoke("expertKit.remove", { folderName: "sales-kit" }) // pluginId 优先
await bridge.invoke("storage.setItem", { key: "lastSpace", value: { url: "https://space.example.com" } })
const { value } = await bridge.invoke("storage.getItem", { key: "lastSpace" }) // 不存在 → { value:null }
await bridge.invoke("storage.removeItem", { key: "lastSpace" })
await bridge.invoke("storage.clear") // 清空当前应用 + 当前账号桶
await bridge.invoke("enterprise.getTmpAuthCode")
// → { code, data, expiresTime, signature, timestamp } // 一次性 SSO 免登票据,每次现取2.8 兼容扁平方法(仅 WebView)
await bridge.invoke("addToChat", {
message: "请对比这两份材料的差异",
resources: [
{ name: "白皮书.pdf", type: "file", url: "https://kb.example.com/f/1.pdf" }, // 宿主下载成附件
{ name: "资料库", type: "folder", url: "https://kb.example.com/space/1", // 远程引用 chip
mcp: { server: "adl-kb", tool: "search_folder", args: { spaceId: "space_1" } } },
],
}) // → { ok:true, count }
await bridge.invoke("openExternal", { url: "https://example.com" }) // → { ok:true }
await bridge.invoke("ping", { a: 1 }) // → { pong:true, echo:{a:1}, at:<ms> }
await bridge.invoke("getEnv") // → { embeddedAppId, platform, versions:{electron,chrome,node} }这几个是 WebView 侧的扁平兼容名;iframe 形态没有它们,外链请走 page.openUrl。addToChat 的 file 由宿主下载成附件,folder 配 mcp 时做成远程引用 chip 交给指定 MCP 工具。