DivisionBox API
DivisionBox API
概述
DivisionBox 是一个轻量级的子窗口系统,基于 WebContentsView 实现,用于承载插件 UI、系统工具和调试界面。
介绍
Scope
本文档描述 当前已落地 的 DivisionBox 基础能力(open/close/state + 生命周期事件)。
- 不包含:多视图并行、复杂 Dock 布局等高级能力。
- 生命周期事件已通过
DivisionBoxSDK暴露(onLifecycleChange/onStateChange)。
权限说明:DivisionBox 已接入权限中心(Permission Center),插件需具备
window.create权限。
规范:优先使用 SDK;仅在 SDK 不覆盖时使用 transport 通道。
核心概念
生命周期状态
DivisionBox 有六个生命周期状态:
prepare → attach → active → inactive → detach → destroy
| 状态 | 说明 |
|---|---|
prepare | 准备阶段,资源加载中 |
attach | 已附加到窗口 |
active | 活跃状态,用户正在交互 |
inactive | 非活跃状态,可能被缓存 |
detach | 已从窗口分离 |
destroy | 已销毁,资源已释放 |
DivisionBox 配置
interface DivisionBoxConfig {
/** 加载的 URL */
url: string
/** 窗口标题 */
title: string
/** 图标(可选) */
icon?: string
/** 窗口大小 */
size?: 'compact' | 'medium' | 'expanded'
/** 是否保持活跃(后台缓存) */
keepAlive?: boolean
/** 关联的插件 ID */
pluginId?: string
/** Header 配置(可选) */
header?: {
show: boolean
title?: string
icon?: string
}
/** CoreBox 头部 UI 控制(可选) */
ui?: {
showInput?: boolean
inputPlaceholder?: string
showResults?: boolean
initialInput?: string
}
}
快捷键
| 快捷键 | 功能 |
|---|---|
Command/Ctrl+D | 将当前选中项分离到 DivisionBox |
快捷键可在系统设置中自定义。
使用方式
插件 SDK(推荐)
import { useDivisionBox } from '@talex-touch/utils/plugin/sdk'
const divisionBox = useDivisionBox()
// 打开 DivisionBox
const { sessionId } = await divisionBox.open({
url: 'https://example.com/tool',
title: '我的工具',
size: 'medium',
keepAlive: true
})
// 监听生命周期变化
const unsubscribe = divisionBox.onLifecycleChange((event) => {
console.log(event.sessionId, event.oldState, event.newState)
})
// 关闭 DivisionBox
await divisionBox.close(sessionId)
unsubscribe()
从渲染进程打开 DivisionBox
import { useTuffTransport } from '@talex-touch/utils/transport'
import { DivisionBoxEvents } from '@talex-touch/utils/transport/events'
async function openDivisionBox() {
const transport = useTuffTransport()
const response = await transport.send(DivisionBoxEvents.open, {
url: 'plugin://my-plugin/panel.html',
title: '我的面板',
icon: 'ri:dashboard-line',
size: 'medium',
keepAlive: true,
pluginId: 'my-plugin'
})
if (response?.success) {
console.log('Session ID:', response.data.sessionId)
}
}
关闭 DivisionBox
async function closeDivisionBox(sessionId: string) {
const transport = useTuffTransport()
await transport.send(DivisionBoxEvents.close, {
sessionId,
options: {
delay: 0,
animation: false,
force: false
}
})
}
获取会话状态
async function getSessionState(sessionId: string) {
const transport = useTuffTransport()
const response = await transport.send(DivisionBoxEvents.getState, {
sessionId
})
if (response?.success) {
console.log('State:', response.data.state)
}
}
更新会话状态
async function updateSessionState(sessionId: string, key: string, value: any) {
const transport = useTuffTransport()
await transport.send(DivisionBoxEvents.updateState, {
sessionId,
key,
value
})
}
获取所有活跃会话
async function getActiveSessions() {
const transport = useTuffTransport()
const response = await transport.send(DivisionBoxEvents.getActiveSessions, {})
if (response?.success) {
console.log('Active sessions:', response.data)
}
}
插件 SDK
完整 API
open(config)
打开新的 DivisionBox 窗口。
import { useDivisionBox } from '@talex-touch/utils/plugin/sdk'
const divisionBox = useDivisionBox()
const { sessionId } = await divisionBox.open({
url: 'https://example.com',
title: 'Web Tool',
icon: 'ri:tools-line',
size: 'medium',
keepAlive: true,
header: {
show: true,
title: 'Custom Title'
}
})
close(sessionId, options?)
关闭 DivisionBox 窗口。
// 简单关闭
await divisionBox.close(sessionId)
// 带延迟和动画
await divisionBox.close(sessionId, {
delay: 1000,
animation: true
})
// 强制关闭(忽略 keepAlive)
await divisionBox.close(sessionId, { force: true })
onStateChange(handler)
监听状态变化。
onStateChange提供简化的状态变更回调;需要完整生命周期信息时使用onLifecycleChange。
const unsubscribe = divisionBox.onStateChange((data) => {
console.log(`Session ${data.sessionId} changed to ${data.state}`)
})
// 停止监听
unsubscribe()
onLifecycleChange(handler)
监听完整生命周期变化事件。
const unsubscribe = divisionBox.onLifecycleChange((event) => {
console.log(event.sessionId, event.oldState, event.newState)
})
// 停止监听
unsubscribe()
updateState(sessionId, key, value)
更新会话状态数据。
// 保存滚动位置
await divisionBox.updateState(sessionId, 'scrollY', 150)
// 保存草稿
await divisionBox.updateState(sessionId, 'draft', {
text: 'Hello world',
timestamp: Date.now()
})
getState(sessionId, key)
获取会话状态数据。
const scrollY = await divisionBox.getState(sessionId, 'scrollY')
const draft = await divisionBox.getState(sessionId, 'draft')
URL 协议
DivisionBox 支持多种 URL 协议:
| 协议 | 说明 | 示例 |
|---|---|---|
plugin:// | 插件资源 | plugin://my-plugin/index.html |
file:// | 本地文件 | file:///path/to/file.html |
http(s):// | 网络资源 | https://example.com |
tuff:// | 系统内置页面 | tuff://detached?itemId=xxx |
与 Flow Transfer 集成
DivisionBox 可以作为 Flow Transfer 的目标:
{
"flowTargets": [
{
"id": "open-in-panel",
"name": "在面板中打开",
"supportedTypes": ["json", "text"],
"featureId": "open-panel"
}
]
}
当 Flow 触发时,可以在 Feature 中打开 DivisionBox:
function onFeatureTriggered(featureId: string, query: TuffQuery) {
if (isFlowTriggered(query)) {
const flowData = extractFlowData(query)
// 打开 DivisionBox 显示 Flow 数据
divisionBox.open({
url: `/viewer.html?sessionId=${flowData.sessionId}`,
title: '查看数据'
})
}
}
IPC 通道
| 通道 | 方向 | 说明 |
|---|---|---|
division-box:open | 渲染 → 主 | 打开新会话 |
division-box:close | 渲染 → 主 | 关闭会话 |
division-box:get-state | 渲染 → 主 | 获取会话状态 |
division-box:update-state | 渲染 → 主 | 更新会话状态 |
division-box:get-active-sessions | 渲染 → 主 | 获取所有活跃会话 |
division-box:state-changed | 主 → 渲染 | 状态变化通知 |
division-box:session-destroyed | 主 → 渲染 | 会话销毁通知 |
资源限制
- 资源限制与缓存策略以主进程实现为准(
apps/core-app/src/main/modules/division-box/)。
技术原理
- DivisionBox 以
WebContentsView作为承载层,由主进程统一管理生命周期。 - SDK 负责封装窗口创建、状态变更与事件订阅,隔离底层实现。
生命周期详解
prepare 阶段
prepare 是 DivisionBox 会话的初始状态,发生在 open() 调用后、窗口创建前。
触发时机:
- 调用
divisionBox.open(config)且通过下述前置检查后进入;被拒绝的 open 不会到达该状态 - 随后主进程分配 sessionId、创建 DivisionBoxSession 实例
资源管理:
- 先检查全局会话上限,通过后才分配任何资源
- 生成 sessionId 并登记到 session map
- 验证配置并应用默认值;构造窗口前先解析插件归属
- 配置了
keepAlive时只是安装状态变更回调,此时尚未进入 LRU 缓存——缓存条目在会话首次进入inactive时加入,回到active时刷新访问时间,destroy时移除
异常处理:
以下三种都会让 open() 直接失败,不会产生一个随后再发生状态转换的会话。
- 超过全局会话上限
MAX_ACTIVE_SESSIONS→ 抛出DivisionBoxError(LIMIT_EXCEEDED),在生成 sessionId 之前 - 请求 UI view 但缺少
pluginId,或pluginId找不到已加载插件 → 抛出DivisionBoxError(CONFIG_ERROR) - 非 authoritative 调用方请求 UI view,或
pluginId与调用插件不符 → 抛出DivisionBoxError(PERMISSION_DENIED),在 IPC 边界拦截,不会进入 manager
attach 阶段
attach 发生在窗口创建完成后。
触发时机:
- 窗口创建完成后,
createWindow()末尾显式调用setState(attach)进入该状态 - 状态推进完全由显式
setState调用驱动,主进程并未监听did-start-loading等 Electron 加载事件
资源管理:
- 创建 WebContentsView 实例
- 加载指定 URL
- 注册 IPC 通道监听
- 如果是从 CoreBox detach,转移 UI view 所有权
异常处理:
- 窗口创建失败 → 清理窗口并抛出
DivisionBoxError(RESOURCE_ERROR) - URL 加载失败 →
loadURL()抛错并传递给调用方,管理器将会话移出注册表;当前没有did-fail-load/render-process-gone的事件监听
active 阶段
active 表示 DivisionBox 正在被用户交互。
触发时机:
attachUIView()在loadURL()完成后显式调用setState(active)- 从 CoreBox 转移已有视图时,
attachExistingUIView()附加完成后显式进入active;无 UI 视图的会话由管理器在窗口创建后直接置为active - 从
inactive恢复同样通过显式setState完成,没有基于窗口焦点的自动切换
资源管理:
- 正常运行状态,无需特殊资源管理
- 可以安全地执行 IPC 通信
inactive 阶段
inactive 表示 DivisionBox 处于非活跃状态,可能被缓存。
触发时机:
- 当前没有自动触发来源:主进程未监听
blur或窗口遮挡等事件 - 目前只能通过
setState/ SDK 编程式进入;管理器监听到该状态后会把keepAlive会话放入 LRU 缓存
资源管理:
- 如果
keepAlive=false,可能触发延迟销毁 - 如果
keepAlive=true,保持资源但降低优先级 - LRU 缓存可能在此阶段回收低优先级会话
异常处理:
- 长时间 inactive 可能触发内存压力回收
- 超过 LRU 缓存上限时,最久未使用的会话被销毁
detach 阶段
detach 表示会话已从窗口显式分离。
触发时机:
- 当前没有自动触发来源,只能通过
setState/ SDK 编程式进入该状态 divisionBox.close(sessionId)与窗口closed事件都会直接进入destroy,不会经过detach
资源管理:
- 如果
keepAlive=true,会话在首次inactive时就已进入 LRU 缓存,此处从缓存中等待恢复 - 如果
keepAlive=false,直接进入destroy阶段 - 释放 WebContentsView 和关联资源
异常处理:
- 关闭动画可能被中断(
force: true) - 缓存满时可能立即销毁而非缓存
destroy 阶段
destroy 是会话的最终状态,资源已完全释放。
触发时机:
- 调用
divisionBox.close(sessionId)时,destroySession()直接调用destroy() - 窗口被用户关闭时,
closed事件监听器调用destroy() keepAlive会话被 LRU 缓存回收时(缓存溢出触发驱逐)- 内存压力触发的强制销毁(堆内存超限时驱逐部分缓存会话)
资源管理:
- 释放所有关联资源
- 从 session map 中移除
- 从 LRU 缓存中移除
- 注销 IPC 通道监听
- 触发
session-destroyed事件
异常处理:
- 此阶段不应有异常,资源必须完全释放
- 如果释放失败,记录错误日志但不阻止状态转换
状态转换图
最佳实践
- 使用 keepAlive:对于频繁使用的面板,启用
keepAlive提升响应速度 - 合理设置大小:根据内容选择合适的
size - 保存状态:使用
updateState/getState保存用户数据 - 监听状态变化:及时响应
inactive状态,释放不必要的资源 - 处理销毁事件:监听
session-destroyed清理相关资源 - 理解生命周期:根据业务需求选择合适的生命周期钩子
相关文档
- Flow Transfer API - 插件间数据流转
- Plugin Manifest - 插件配置