基础知识

# 基础知识

本文档涵盖秋之盒插件开发的核心概念。理解这些基础知识,你就能掌握整个插件系统的运作原理。

# 响应式状态(State)

秋之盒采用自研的轻量级响应式状态原语,贯穿整个框架——从底层服务到 UI 层绑定。所有状态变化都通过 State 系统传播,无论是设备列表更新、语言切换还是自定义业务状态。

# IReadonlyState<V>

IReadonlyState<V> 是只读的响应式状态接口。消费者只能读取当前值和订阅变化通知:

interface IReadonlyState<V> {
  readonly value: V;
  subscribe(listener: (value: V) => void): () => void;
}
成员 说明
value 当前值,只读。同步访问,不涉及异步。
subscribe(listener) 注册变化监听器。每次值变化时调用 listener(newValue)。返回取消订阅函数。

用法示例:

import type { IReadonlyState } from '@autumnbox/sdk/common';

function watchDeviceCount(devices: IReadonlyState<readonly string[]>): void {
  // 同步读取当前值
  console.log('当前设备数:', devices.value.length);

  // 订阅变化
  const unsubscribe = devices.subscribe((newDevices) => {
    console.log('设备列表已更新:', newDevices.length);
  });

  // 不再需要时取消订阅
  // unsubscribe();
}

subscribe 返回的取消订阅函数是防止内存泄漏的关键。在 React 组件中,useServiceState 会自动管理订阅的生命周期。

# IState<V>

IState<V> 继承自 IReadonlyState<V>,增加了写入能力:

interface IState<V> extends IReadonlyState<V> {
  value: V; // 可读可写
}

直接赋值 state.value = newValue 即可触发所有订阅者的回调。这是 Service 内部管理状态的方式。

import type { IState } from '@autumnbox/sdk/common';

function example(counter: IState<number>): void {
  console.log(counter.value); // 读取: 0

  counter.value = 42; // 写入并通知所有订阅者
  console.log(counter.value); // 42
}

# createState

function createState<V>(initial: V): IState<V>;

创建一个可变的响应式状态。通常用于 Service 的 内部实现——不对外暴露 setter 的场景很少,大多数情况应使用 createReadonlyState

import { createState } from '@autumnbox/sdk/common';

const counter = createState(0);

counter.subscribe((value) => {
  console.log('counter 变为:', value);
});

counter.value = 1; // 输出: "counter 变为: 1"
counter.value = 2; // 输出: "counter 变为: 2"

# createReadonlyState

function createReadonlyState<V>(initial: V): [IReadonlyState<V>, (value: V) => void];

创建一个只读视图和私有 setter 的组合,类似于 React 的 useState 解构模式:

import { createReadonlyState } from '@autumnbox/sdk/common';

const [count, setCount] = createReadonlyState(0);

// count 是 IReadonlyState<number>,外部只能读和订阅
console.log(count.value); // 0

// setCount 是私有 setter,只有创建者持有
setCount(42);
console.log(count.value); // 42

核心设计意图:只读视图对外公开,setter 留在 Service 内部。这确保了状态变更的单一来源,消费者无法绕过 Service 的业务逻辑直接修改状态。

# 在 Service 中使用 State

一个完整的 Service 示例,展示 createReadonlyState 的典型用法:

// src/services/CounterService.ts
import { createReadonlyState } from '@autumnbox/sdk/common';

import type { ServiceContainer } from '@autumnbox/sdk/common';
import type { IReadonlyState } from '@autumnbox/sdk/common';

export class CounterService {
  /** 当前计数(只读,UI 可订阅) */
  readonly count: IReadonlyState<number>;

  /** 是否已达上限(只读,UI 可订阅) */
  readonly isMaxed: IReadonlyState<boolean>;

  // 私有 setter——只有 Service 自己能修改
  private readonly setCount: (v: number) => void;
  private readonly setIsMaxed: (v: boolean) => void;

  private static readonly MAX = 100;

  constructor(_container: ServiceContainer) {
    const [count, setCount] = createReadonlyState(0);
    const [isMaxed, setIsMaxed] = createReadonlyState(false);

    this.count = count;
    this.isMaxed = isMaxed;
    this.setCount = setCount;
    this.setIsMaxed = setIsMaxed;
  }

  increment(): void {
    const next = this.count.value + 1;
    this.setCount(next);
    this.setIsMaxed(next >= CounterService.MAX);
  }

  reset(): void {
    this.setCount(0);
    this.setIsMaxed(false);
  }
}

这个 Service 的 countisMaxed 属性是 IReadonlyState,任何消费者——无论是其他 Service 还是 React 组件——都只能读取和订阅,不能直接修改。状态的变更必须通过 increment()reset() 方法。

# 在 React 中使用 State(useServiceState)

useServiceState 是连接 State 系统和 React 渲染的桥梁。它订阅 State 变化,在值更新时触发组件重渲染。

useServiceState 提供三种重载形式,覆盖不同使用场景:

# 重载 1:直接传入 IReadonlyState

function useServiceState<V>(state: IReadonlyState<V>): [V];

传入只读状态,返回 [当前值] 单元素元组。没有 setter——因为状态是只读的。

import { useService, useServiceState } from '@autumnbox/sdk/hooks';

import { CounterService } from '../services/CounterService';

const CounterDisplay: React.FC = () => {
  const counterService = useService(CounterService);

  // counterService.count 是 IReadonlyState<number>
  const [count] = useServiceState(counterService.count);
  const [isMaxed] = useServiceState(counterService.isMaxed);

  return (
    <div>
      <span>计数: {count}</span>
      {isMaxed && <span style={{ color: 'red' }}>已达上限!</span>}
      <button onClick={() => counterService.increment()}>+1</button>
      <button onClick={() => counterService.reset()}>重置</button>
    </div>
  );
};

# 重载 2:直接传入 IState

function useServiceState<V>(state: IState<V>): [V, (v: V) => void];

传入可写状态,返回 [当前值, setter] 二元组。类似 React useState 的返回格式。

import { useServiceState } from '@autumnbox/sdk/hooks';

import type { IState } from '@autumnbox/sdk/common';

interface Props {
  // 某个可写的 IState,例如 TabManager.state
  title: IState<string>;
}

const TitleEditor: React.FC<Props> = ({ title }) => {
  const [value, setValue] = useServiceState(title);

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
};

框架中大多数公开的 State 属性都是 IReadonlyState。只有少数内部状态(如 TabManager.state)是 IState。插件开发时,你拿到的几乎总是只读状态。

# 重载 3:通过 Service + 属性名

function useServiceState<T, K extends StateKeysOf<T>>(
  service: T | ServiceClass<T>,
  stateName: K,
): UseServiceStateReturn<T[K]>;

传入 Service 实例(或 Service 类)和属性名字符串,自动解析对应的 State 属性。返回类型取决于该属性是 IState 还是 IReadonlyState

import { useServiceState } from '@autumnbox/sdk/hooks';
import { DevicesService } from '@autumnbox/sdk/services';

const DeviceList: React.FC = () => {
  // 方式 A:传入 Service 类 + 属性名(自动从容器解析 Service)
  const [devices] = useServiceState(DevicesService, 'devices');

  // 方式 B:传入 Service 实例 + 属性名
  // const devicesService = useService(DevicesService);
  // const [devices] = useServiceState(devicesService, 'devices');

  return (
    <ul>
      {devices.map((device) => (
        <li key={device.sn}>{device.sn}</li>
      ))}
    </ul>
  );
};

StateKeysOf<T> 类型工具会自动提取 Service 中所有 IReadonlyState / IState 类型的属性名,确保只能传入有效的状态属性名——传错名字会在编译期报错。

# 选择哪种重载?

场景 推荐写法
已经持有 State 对象引用 useServiceState(state)
需要从 Service 读取某个状态属性 useServiceState(service, 'propName')
不想先 useService 再访问属性 useServiceState(ServiceClass, 'propName')

# ServiceContainer IoC 容器

ServiceContainer 是秋之盒的依赖注入(IoC)容器,管理所有 Service 的注册、解析和生命周期。

# 核心概念

  • 懒实例化:Service 在首次被 getService() 请求时才创建实例,而非注册时
  • 默认单例:同一个 Service 类在整个应用生命周期中只有一个实例
  • 构造函数注入:Service 的构造函数接收 ServiceContainer,从中获取依赖
import type { ServiceContainer } from '@autumnbox/sdk/common';

export class MyService {
  private readonly shellService: ShellService;

  constructor(container: ServiceContainer) {
    // 从容器中获取其他 Service
    this.shellService = container.getService(ShellService);
  }
}

# 注册

// 类注册(推荐)——懒实例化,默认单例
container.registerService(MyService);

// 带选项注册
container.registerService(MyService, {
  name: 'customName',   // 覆盖自动派生的名称
  singleton: true,       // 默认 true
});

// 实例注册——用于外部创建的对象(驱动、文件系统等)
container.registerInstance('adbDriver', driverInstance);

名称自动派生规则

类名 派生名称
DevicesService 'devices'(去掉 Service 后缀,首字母小写)
ShellService 'shell'
MyHelper 'myHelper'(无 Service 后缀时直接首字母小写)

# 获取

// 按类获取——类型安全,推荐
const devices: DevicesService = container.getService(DevicesService);

// 按名称获取——返回 unknown,需要断言
const driver = container.getService('adbDriver') as IAdbDriver;

// 获取所有同名注册(非单例场景)
const allDrivers = container.getServices('driver');

# 在插件中的用法

插件通常不需要直接操作容器。构建系统会自动注册你的 Service,UI 组件通过 useService() hook 获取:

import { useService } from '@autumnbox/sdk/hooks';
import { ShellService } from '@autumnbox/sdk/services';

const MyApp: React.FC = () => {
  const shell = useService(ShellService);
  // ...
};

如果需要直接访问容器(高级用法),可通过 PluginContext

export function main(context: PluginContext): void {
  const container = context.serviceContainer;
  const shell = container.getService(ShellService);
}

更多 Service 系统的细节,请参考 Service 详解

# PluginContext

PluginContext 是传递给插件入口函数 main() 的上下文对象,也可以在 React 组件中通过 useCurrentPluginContext() 获取。它提供了插件运行所需的全部能力。

# 完整接口

interface PluginContext {
  /** 插件唯一标识(来自 package.json "name") */
  pluginPackageName: string;

  /** 插件专属数据目录路径(如 {autumnHome}/plugins/data/{pluginPackageName}/) */
  pluginHome: string;

  /** 文件系统实例,用于读写插件数据 */
  fs: IFileSystem;

  /** 从插件的 resources/ 目录获取打包资源。不存在时返回 null */
  getResource(name: string): Promise<Blob | null>;

  /** 注册一个或多个 App。返回取消注册函数 */
  registerApp(...apps: AutumnApp[]): () => void;

  /** 注册一个或多个 Card。返回取消注册函数 */
  registerCard(...cards: AutumnCard[]): () => void;

  /** 获取响应式翻译(语言切换时自动更新) */
  t(key: string): IReadonlyState<string>;

  /** 从容器中获取 Service(getService 的快捷方式) */
  getService<T>(token: ServiceClass<T>): T;

  /** IoC 容器(高级用法的逃生门) */
  serviceContainer: ServiceContainer;
}

# 各成员详解

# pluginPackageName

插件的唯一标识,直接取自 package.jsonname 字段。它在框架内部用于关联 App、Card、翻译文本和数据目录。

export function main(context: PluginContext): void {
  console.log(`插件 ${context.pluginPackageName} 已加载`);
  // 输出: "插件 @myplugins/hello-world 已加载"
}

# pluginHome 与 fs

每个插件拥有独立的数据目录。pluginHome 是路径字符串,fs 是操作该路径的文件系统接口:

export function main(context: PluginContext): void {
  const configPath = context.fs.resolve(context.pluginHome, 'config.json');

  // 写入配置
  const data = JSON.stringify({ theme: 'dark' });
  await context.fs.writeFile(configPath, new Blob([data]));

  // 读取配置
  if (await context.fs.exists(configPath)) {
    const stream = await context.fs.readFile(configPath);
    // 处理 ReadableStream...
  }
}

# getResource

.atmb 包内的 resources/ 目录懒加载静态资源(图片、JSON 等):

const iconBlob = await context.getResource('icons/my-icon.png');
if (iconBlob) {
  const url = URL.createObjectURL(iconBlob);
  // 使用 url 作为 <img> 的 src
}

在 React 中可使用 usePluginResource hook:

import { usePluginResource } from '@autumnbox/sdk/hooks';

const MyComponent: React.FC = () => {
  const { data, loading, error } = usePluginResource('icons/logo.png');

  if (loading) return <span>加载中...</span>;
  if (!data) return <span>资源不存在</span>;

  const url = URL.createObjectURL(data);
  return <img src={url} alt="logo" />;
};

# registerApp / registerCard

注册 App 和 Card 到宿主应用。返回取消注册函数,用于清理:

export function main(context: PluginContext): (() => void) | void {
  const unregisterApps = context.registerApp(myApp1, myApp2);
  const unregisterCards = context.registerCard(myCard);

  return () => {
    unregisterApps();
    unregisterCards();
  };
}

如果使用约定式目录结构(src/apps/src/cards/),构建系统会自动生成注册代码,你不需要在 main() 中手动注册。

# t(key)

获取响应式翻译文本。返回 IReadonlyState<string>,在用户切换语言时自动更新:

const title = context.t('app.title');
console.log(title.value); // "秋之盒"(中文环境)

// 翻译值的回退链: 当前语言 → en-US → key 本身

在 React 中推荐使用 useT hook:

import { useT } from '@autumnbox/sdk/hooks';

const Header: React.FC = () => {
  const title = useT('app.title');
  return <h1>{title}</h1>; // 语言切换时自动重渲染
};

# useCurrentPluginContext

在 React 组件中获取当前插件的 PluginContext

import { useCurrentPluginContext } from '@autumnbox/sdk/hooks';

const MyApp: React.FC = () => {
  const ctx = useCurrentPluginContext();
  console.log(ctx.pluginPackageName); // 当前插件 ID
  // ...
};

此 hook 依赖 React Context,只能在插件注册的 App/Card 组件树内部使用。

# Handle-first API 设计

秋之盒的所有设备操作 API 都采用 Handle-first 设计:设备句柄(Handle)作为第一参数传入,而非绑定在某个"当前设备"全局变量上。

# AdbDeviceHandle

type AdbDeviceHandle = Readonly<{
  sn: string; // 设备序列号
}>;

AdbDeviceHandle 是一个不可变的轻量对象,仅包含设备序列号。它由 DevicesService.devices 状态提供,是所有设备操作的入口凭证。

# 为什么采用 Handle-first

传统设计中,工具通常维护一个"当前选中设备"的全局状态:

// 反面示例(不是秋之盒的做法)
deviceManager.setCurrentDevice(device);
shell.exec('ls /sdcard'); // 隐式使用"当前设备"

这种设计在多设备场景下会导致竞态条件和混乱。秋之盒选择了显式传递:

// 秋之盒的做法
const shell = useService(ShellService);
const output = await shell.exec(device, 'ls /sdcard');

优势:

  • 天然支持多设备:不同的 App 标签页可以同时操作不同设备,互不干扰
  • 无歧义:每次调用都明确指定目标设备,不存在"当前设备到底是哪个"的困惑
  • 类型安全:TypeScript 编译器确保你不会忘记传入设备参数

# API 使用示例

# ShellService

import { useService, useRequiredDevice } from '@autumnbox/sdk/hooks';
import { ShellService } from '@autumnbox/sdk/services';

const MyApp: React.FC = () => {
  const device = useRequiredDevice();
  const shell = useService(ShellService);

  const handleCheck = async () => {
    // exec: 执行命令并返回 stdout 字符串
    const output = await shell.exec(device, 'getprop ro.build.version.sdk');
    console.log('SDK 版本:', output.trim());

    // spawn: 更底层的 API,返回 IAdbShellProcess,支持流式 I/O
    const proc = await shell.spawn(device, ['logcat', '-d']);
    // proc.stdout 是 AsyncIterable<Uint8Array>
  };

  return <button onClick={handleCheck}>检查设备</button>;
};

# DeviceFileSystemService

import { useService } from '@autumnbox/sdk/hooks';
import { DeviceFileSystemService } from '@autumnbox/sdk/services';

// 列出目录
const entries = await fileService.listDir(device, '/sdcard/');

// 推送文件到设备
await fileService.push(device, '/sdcard/Download/', fileData);

// 从设备拉取文件
await fileService.pull(device, '/sdcard/photo.jpg', {
  type: 'reader',
  chunkCallback: async (chunk) => { /* 处理数据块 */ },
});

# PackageService

import { PackageService } from '@autumnbox/sdk/services';

// 列出已安装的包名
const packages = await packageService.listPackages(device);

// 安装 APK
await packageService.installApk(device, apkFileData);

# RebootService

import { RebootService } from '@autumnbox/sdk/services';

// 正常重启
await rebootService.reboot(device);

// 重启到 recovery
await rebootService.reboot(device, 'recovery');

// 重启到 bootloader
await rebootService.reboot(device, 'bootloader');

# useRequiredDevice

对于需要设备的 App,在定义时设置 shallSelectAdbDevice: true,宿主会在打开标签页前提示用户选择设备。在组件内通过 useRequiredDevice() 获取保证非空的设备句柄:

import { useRequiredDevice } from '@autumnbox/sdk/hooks';

import type { AutumnApp } from '@autumnbox/sdk';

const MyDeviceApp: React.FC = () => {
  const device = useRequiredDevice(); // 保证非 null
  return <div>设备: {device.sn}</div>;
};

export const MyApp: AutumnApp = {
  id: 'my-device-app',
  name: 'app.my-device-app',
  icon: '...',
  shallSelectAdbDevice: true, // 打开前提示选择设备
  component: MyDeviceApp,
};

# 共享模块

插件以 UMD bundle 的形式运行在宿主环境中。以下模块由宿主应用提供,插件 不需要也不应该 打包它们:

模块 说明
react React 核心
react-dom React DOM 渲染器
react/jsx-runtime JSX 转换运行时
antd Ant Design 组件库
@ant-design/icons Ant Design 图标库
@autumnbox/common State、ServiceContainer
@autumnbox/core 内置 Service(DevicesService 等)
@autumnbox/interfaces 接口类型(IAdbDriver、IFileSystem 等)
@autumnbox/app 应用层(Hooks、PluginContext、App/Card 类型等)
@xterm/xterm 终端模拟器
@xterm/addon-fit xterm 自适应尺寸插件

在插件代码中正常 import 即可,构建时 SDK 会自动将它们标记为 external。运行时宿主通过 ModuleRegistry 提供这些模块的实例。

// 正常写法——构建时自动 external 化
import { useState } from 'react';
import { Button } from 'antd';
import { useService } from '@autumnbox/sdk/hooks';

不要尝试将上述模块安装为 dependencies 并打包进插件。这会导致运行时出现两份不同的 React 实例(hooks 错误)或两份不同的 ServiceContainer(服务找不到)。它们只应出现在 devDependencies 中用于类型检查。

# SDK 导入路径

@autumnbox/sdk 提供了多个子路径入口,按职责组织导出内容。理解这些路径能帮助你准确导入所需的类型和函数。

# @autumnbox/sdk

SDK 主入口,导出插件开发最常用的类型

import type {
  AutumnApp,       // App 定义类型
  AutumnCard,      // Card 定义类型
  PluginContext,   // 插件上下文
  PluginMain,      // 插件入口函数类型
  PluginMetadata,  // 插件元数据
  AppProps,        // App 组件接收的 Props
  IRouteInfo,      // 路由信息
} from '@autumnbox/sdk';

# @autumnbox/sdk/hooks

所有 React Hooks,在组件中使用:

import {
  useService,              // 从容器获取 Service
  useServiceState,         // 订阅 State 变化
  useT,                    // 获取翻译文本
  useCurrentPluginContext,  // 获取插件上下文
  useRequiredDevice,       // 获取当前设备(保证非空)
  usePluginHome,           // 获取插件数据目录
  usePluginFs,             // 获取插件文件系统
  usePluginResource,       // 加载插件资源
  useShell,                // 增强的 Shell 操作
  useNavigation,           // 应用内导航
  useNavigationEvent,      // 监听导航事件
  useTabCloser,            // 关闭当前标签页
  useTabName,              // 读写标签页标题
  useRequestDevice,        // 请求用户选择设备
  useConnectWiFi,          // WiFi ADB 连接
  useLoadPlugin,           // 加载插件
} from '@autumnbox/sdk/hooks';

# @autumnbox/sdk/services

所有内置 Service 类,用于 useService()container.getService()

import {
  DevicesService,       // 设备列表管理
  ShellService,         // Shell 命令执行
  DeviceFileSystemService,          // 文件传输
  PackageService,       // APK 安装/包管理
  RebootService,        // 设备重启
  LanguageService,      // 国际化
  NotificationService,  // 通知
  LocalFileSystemService,    // 文件系统
  DriverService,        // 驱动管理
  NativeService,        // Native library 加载
} from '@autumnbox/sdk/services';

# @autumnbox/sdk/common

基础工具类型和函数——State 原语和 IoC 容器:

import { createState, createReadonlyState, ServiceContainer } from '@autumnbox/sdk/common';

import type {
  IReadonlyState,
  IState,
  ServiceClass,
  ServiceFeats,
} from '@autumnbox/sdk/common';

# @autumnbox/sdk/interfaces

底层接口类型——设备句柄、驱动接口、文件系统等:

import type {
  AdbDeviceHandle,      // 设备句柄
  IAdbDriver,           // ADB 驱动接口
  IAdbShellProcess,     // Shell 进程
  IAdbFileEntry,        // 文件列表项
  IFileSystem,          // 文件系统
  IFileStat,            // 文件状态
  AdbRebootTarget,      // 重启目标
  DeviceState,          // 设备状态
  PushSource,           // 推送来源
  PullDest,             // 拉取目标
} from '@autumnbox/sdk/interfaces';

# 导入路径选择指南

我需要... 从哪里导入
定义 App/Card 的类型 @autumnbox/sdk
React Hook @autumnbox/sdk/hooks
内置 Service 类 @autumnbox/sdk/services
State 创建函数或 ServiceContainer @autumnbox/sdk/common
设备、驱动、文件系统等接口类型 @autumnbox/sdk/interfaces

经验法则:如果你在写 React 组件,大部分导入来自 hooksservices。如果你在写 Service,大部分导入来自 commoninterfaces。主入口 @autumnbox/sdk 只用于类型导入(import type)。

# 总结

概念 核心要点
State IReadonlyState 只读 + 订阅;IState 可读写;createReadonlyState 分离视图与 setter
ServiceContainer 懒实例化单例容器;registerService(Clazz) 注册,getService(Clazz) 获取
PluginContext 插件入口上下文;提供 pluginPackageNamefsregisterAppt() 等便捷方法
Handle-first 所有设备操作以 AdbDeviceHandle 为首参数,天然支持多设备
共享模块 React、antd、@autumnbox/* 由宿主提供,插件不打包
SDK 路径 5 个子入口按职责划分:主类型 / hooks / services / common / interfaces

掌握这些基础后,你可以继续阅读以下文档:

最后更新: 4/8/2026, 2:35:44 AM