# 实战:APK 安装器
从零构建一个插件:用户选择本地 APK 文件,推送到 Android 设备,执行安装,实时显示结果。
# 创建项目
克隆插件模板并修改标识信息:
git clone https://github.com/zsh2401/AutumnBoxPluginTemplate.git apk-installer
cd apk-installer
编辑 package.json:
{
"name": "@autumnbox/apk-installer",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "autumnbox-sdk build",
"deploy": "pnpm run build && cp *.atmb ../packages/app/builtin-plugins/"
},
"devDependencies": {
"@autumnbox/sdk": "workspace:*",
"@types/react": "^19.0.0",
"antd": "^6.3.4",
"react": "^19.0.0"
}
}
删除模板中的示例文件,保留目录结构。
# 定义 App
创建 src/apps/ApkInstallerApp.tsx:
import { defineApp } from '@autumnbox/sdk';
import { ApkInstallerAppView } from '../components/ApkInstallerAppView';
export const ApkInstallerApp = defineApp({
id: 'apk-installer',
name: 'APK 安装器',
icon: '📦',
shallSelectAdbDevice: true, // 打开前要求用户选择设备
tags: ['tools'],
component: ApkInstallerAppView,
});
shallSelectAdbDevice: true 告诉秋之盒在打开标签页之前弹出设备选择器,选定的设备会通过 useRequiredDevice() hook 获取。
构建时 autumnbox-sdk build 会自动扫描 src/apps/ 目录发现 ApkInstallerApp,无需手动注册。
# 编写 ApkInstallerApp
创建 src/ApkInstallerApp.tsx。以下分步讲解,完整代码在本节末尾。
# 文件选择
使用 Ant Design 的 Upload.Dragger 让用户拖拽或点击选择 APK。beforeUpload 返回 false 阻止自动上传,仅缓存文件引用:
const [file, setFile] = React.useState<File | null>(null);
<Dragger
accept=".apk"
maxCount={1}
beforeUpload={(f) => {
setFile(f as unknown as File);
return false; // 阻止自动上传
}}
onRemove={() => setFile(null)}
>
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
<p className="ant-upload-text">点击或拖拽 APK 文件到此处</p>
</Dragger>
# 获取服务
通过 useService hook 从 IoC 容器中获取 DeviceFileSystemService 和 ShellService:
import { useService } from '@autumnbox/sdk/app';
import { DeviceFileSystemService, ShellService } from '@autumnbox/sdk/core';
const fileService = useService(DeviceFileSystemService);
const shellService = useService(ShellService);
# 推送与安装
安装流程分三步:推送文件到设备临时目录 → 执行 pm install → 清理临时文件。
const install = async () => {
if (!targetDevice || !file) return;
setInstalling(true);
setResult(null);
try {
const remotePath = `/data/local/tmp/${file.name}`;
// 1. 推送 APK 到设备
await fileService.push(targetDevice, '/data/local/tmp/', {
data: file,
name: file.name,
size: file.size,
});
// 2. 执行 pm install
const output = await shellService.exec(targetDevice, [
'pm', 'install', '-r', remotePath,
]);
if (output.includes('Success')) {
setResult({ success: true, message: '安装成功' });
} else {
setResult({ success: false, message: output.trim() });
}
// 3. 清理临时文件
await shellService.exec(targetDevice, ['rm', '-f', remotePath]);
} catch (err) {
setResult({
success: false,
message: err instanceof Error ? err.message : String(err),
});
} finally {
setInstalling(false);
}
};
几个要点:
DeviceFileSystemService.push(device, deviceDir, source)的第二个参数是设备上的目录路径,第三个参数是PushSource——可以是路径字符串,也可以是IFileData对象({ data: Blob | ArrayBuffer; name: string; size: number })。浏览器环境下File继承自Blob,可以直接作为data传入。ShellService.exec(device, command)等待命令执行完毕并返回完整 stdout 字符串。pm install -r中-r表示允许覆盖安装。
# 完整代码
src/ApkInstallerApp.tsx:
import React from 'react';
import { Button, Typography, Alert, Upload, Space } from 'antd';
import { InboxOutlined } from '@ant-design/icons';
import { useService } from '@autumnbox/sdk/app';
import { DeviceFileSystemService, ShellService } from '@autumnbox/sdk/core';
import type { AdbDeviceHandle } from '@autumnbox/sdk/interfaces';
const { Title, Text } = Typography;
const { Dragger } = Upload;
interface InstallResult {
success: boolean;
message: string;
}
export const ApkInstallerApp: React.FC<{
targetDevice?: AdbDeviceHandle;
}> = ({ targetDevice }) => {
const fileService = useService(DeviceFileSystemService);
const shellService = useService(ShellService);
const [file, setFile] = React.useState<File | null>(null);
const [installing, setInstalling] = React.useState(false);
const [result, setResult] = React.useState<InstallResult | null>(null);
const install = async () => {
if (!targetDevice || !file) return;
setInstalling(true);
setResult(null);
try {
const remotePath = `/data/local/tmp/${file.name}`;
// 推送 APK 到设备临时目录
await fileService.push(targetDevice, '/data/local/tmp/', {
data: file,
name: file.name,
size: file.size,
});
// 执行安装
const output = await shellService.exec(targetDevice, [
'pm', 'install', '-r', remotePath,
]);
if (output.includes('Success')) {
setResult({ success: true, message: '安装成功' });
} else {
setResult({ success: false, message: output.trim() });
}
// 清理临时文件
await shellService.exec(targetDevice, ['rm', '-f', remotePath]);
} catch (err) {
setResult({
success: false,
message: err instanceof Error ? err.message : String(err),
});
} finally {
setInstalling(false);
}
};
return (
<div style={{ padding: 24, maxWidth: 520 }}>
<Title level={3}>APK 安装器</Title>
<Text type="secondary">
设备:{targetDevice?.sn ?? '未选择'}
</Text>
<div style={{ margin: '16px 0' }}>
<Dragger
accept=".apk"
maxCount={1}
beforeUpload={(f) => {
setFile(f as unknown as File);
return false;
}}
onRemove={() => {
setFile(null);
setResult(null);
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽 APK 文件到此处</p>
</Dragger>
</div>
<Space direction="vertical" style={{ width: '100%' }}>
<Button
type="primary"
block
loading={installing}
disabled={!file || !targetDevice}
onClick={install}
>
{installing ? '安装中...' : '安装到设备'}
</Button>
{result && (
<Alert
type={result.success ? 'success' : 'error'}
message={result.success ? '安装成功' : '安装失败'}
description={result.message}
showIcon
/>
)}
</Space>
</div>
);
};
# 使用 PackageService 简化
上面的流程(推送 → 安装 → 清理)是手动实现的,便于理解原理。秋之盒的 PackageService 已经封装了这一流程:
import { useService } from '@autumnbox/sdk/app';
import { PackageService } from '@autumnbox/sdk/core';
const packageService = useService(PackageService);
// 一行搞定:推送 + pm install -r + 清理临时文件
await packageService.installApk(targetDevice, {
data: file,
name: file.name,
size: file.size,
});
installApk 内部执行的逻辑与手动方式完全一致。如果不需要自定义安装参数,直接用它即可。
# 构建与测试
# 在插件目录下
pnpm run build
构建产出:
dist/index.js # UMD bundle
autumnbox.apk-installer.atmb # 可部署的插件包
将 .atmb 复制到秋之盒的内置插件目录:
cp autumnbox.apk-installer.atmb ../packages/app/builtin-plugins/
启动秋之盒开发服务器(pnpm run dev),在应用列表中找到「APK 安装器」,点击打开后选择设备,拖入 APK 文件即可测试。
如果 package.json 中配置了 deploy 脚本,可以直接 pnpm run deploy 一步到位。