文档/DivisionBox API

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 有六个生命周期状态:

EXAMPLE.VUE
prepare → attach → active → inactive → detach → destroy
状态说明
prepare准备阶段,资源加载中
attach已附加到窗口
active活跃状态,用户正在交互
inactive非活跃状态,可能被缓存
detach已从窗口分离
destroy已销毁,资源已释放

DivisionBox 配置

EXAMPLE.TYPESCRIPT
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(推荐)

EXAMPLE.TYPESCRIPT
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

EXAMPLE.TYPESCRIPT
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

EXAMPLE.TYPESCRIPT
async function closeDivisionBox(sessionId: string) {
  const transport = useTuffTransport()
  await transport.send(DivisionBoxEvents.close, {
    sessionId,
    options: {
      delay: 0,
      animation: false,
      force: false
    }
  })
}

获取会话状态

EXAMPLE.TYPESCRIPT
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)
  }
}

更新会话状态

EXAMPLE.TYPESCRIPT
async function updateSessionState(sessionId: string, key: string, value: any) {
  const transport = useTuffTransport()
  await transport.send(DivisionBoxEvents.updateState, {
    sessionId,
    key,
    value
  })
}

获取所有活跃会话

EXAMPLE.TYPESCRIPT
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 窗口。

EXAMPLE.TYPESCRIPT
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 窗口。

EXAMPLE.TYPESCRIPT
// 简单关闭
await divisionBox.close(sessionId)

// 带延迟和动画
await divisionBox.close(sessionId, {
  delay: 1000,
  animation: true
})

// 强制关闭(忽略 keepAlive)
await divisionBox.close(sessionId, { force: true })

onStateChange(handler)

监听状态变化。

onStateChange 提供简化的状态变更回调;需要完整生命周期信息时使用 onLifecycleChange。

EXAMPLE.TYPESCRIPT
const unsubscribe = divisionBox.onStateChange((data) => {
  console.log(`Session ${data.sessionId} changed to ${data.state}`)
})

// 停止监听
unsubscribe()

onLifecycleChange(handler)

监听完整生命周期变化事件。

EXAMPLE.TYPESCRIPT
const unsubscribe = divisionBox.onLifecycleChange((event) => {
  console.log(event.sessionId, event.oldState, event.newState)
})

// 停止监听
unsubscribe()

updateState(sessionId, key, value)

更新会话状态数据。

EXAMPLE.TYPESCRIPT
// 保存滚动位置
await divisionBox.updateState(sessionId, 'scrollY', 150)

// 保存草稿
await divisionBox.updateState(sessionId, 'draft', {
  text: 'Hello world',
  timestamp: Date.now()
})

getState(sessionId, key)

获取会话状态数据。

EXAMPLE.TYPESCRIPT
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 的目标:

EXAMPLE.JSON
{
  "flowTargets": [
    {
      "id": "open-in-panel",
      "name": "在面板中打开",
      "supportedTypes": ["json", "text"],
      "featureId": "open-panel"
    }
  ]
}

当 Flow 触发时,可以在 Feature 中打开 DivisionBox:

EXAMPLE.TYPESCRIPT
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 事件

异常处理:

  • 此阶段不应有异常,资源必须完全释放
  • 如果释放失败,记录错误日志但不阻止状态转换

状态转换图

stateDiagram-v2 [*] --> prepare: open() prepare --> attach: 窗口创建完成 attach --> active: 窗口获得焦点 active --> inactive: 窗口失去焦点 inactive --> active: 窗口重新获得焦点 inactive --> detach: close() / 资源回收 active --> detach: close() / 资源回收 attach --> detach: close() / 资源回收 detach --> [*]: keepAlive=false detach --> prepare: keepAlive=true, 恢复 prepare --> destroy: 加载失败 attach --> destroy: 加载失败 / 崩溃 active --> destroy: 崩溃 inactive --> destroy: LRU 回收 / 内存压力 destroy --> [*]

最佳实践

  1. 使用 keepAlive:对于频繁使用的面板,启用 keepAlive 提升响应速度
  2. 合理设置大小:根据内容选择合适的 size
  3. 保存状态:使用 updateState/getState 保存用户数据
  4. 监听状态变化:及时响应 inactive 状态,释放不必要的资源
  5. 处理销毁事件:监听 session-destroyed 清理相关资源
  6. 理解生命周期:根据业务需求选择合适的生命周期钩子

相关文档