编写 Skill 教程
一个 skill 就是一组驱动某款 App(或系统功能)的自动化脚本。你写的 skill 合并到 phonebase-skill-hub 之后,任何 phonebase 用户都能 pb skills install <你的skill> 装上,在自己的云手机上跑。
本教程带你 10 分钟写完第一个 skill。
0. 准备
你需要:
- 本地装好
pbCLI(参考 快速开始) - 一台能连上的云手机或 Android 真机
- Node.js 20+
- 一个 GitHub 账号(用来提 PR)
Fork 仓库到你自己的账号:
gh repo fork phonebase-cloud/phonebase-skill-hub --clone
cd phonebase-skill-hub1. 目录结构
每个 skill 住在 skills/<id>/ 下,最少只要两个东西:
skills/
└── your-skill/
├── SKILL.md ← 元数据 + 说明(必需)
└── scripts/ ← 脚本目录(必需)
├── open.js
├── close.js
└── state.js规则:
<id>是 skill 的唯一标识,会直接变成命令前缀(pb your-skill open)。全小写、连字符,不要带空格- 每个
.js文件就是一个子命令,文件名 = 命令名(open.js→pb your-skill open) _lib.js/_utils.js等下划线开头的文件不会被注册为命令,可以放共享代码
2. 写 SKILL.md
SKILL.md 是 YAML frontmatter + markdown 正文。frontmatter 是结构化元数据(文档站会读它生成列表页和详情页),正文是给人看的说明。
最小示例:
---
name: your-skill
description: 一句话说明这个 skill 做什么
platform: android
package: com.example.yourapp
---
# Your Skill
详细说明 skill 的用途、依赖、注意事项。可用字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | skill 名称,通常与目录名一致,用作命令前缀 |
description | string | ✅ | 一句话描述,显示在列表页 |
app_name | string | 友好的展示名称,如 Google Play Store。文档站卡片和详情页标题用它 | |
display_name | string | app_name 的别名,优先级更高。两者只写一个即可 | |
version | string | 语义版本,如 1.0.0 | |
author | string | 作者或组织 | |
platform | android | ios | web | 目标平台,影响列表页分组。缺省时按 package / bundle_id 自动推断 | |
category | string | 自由分类标签,如 app-store / social / e-commerce | |
package | string | Android 包名(仅 android 平台使用) | |
bundle_id | string | iOS bundle id(仅 ios 平台使用) | |
tags | string[] | 任意标签数组 | |
requires | string[] | 依赖的其他 skill 或 capability,如 ["googleservices"] | |
commands | object[] | 显式声明命令列表(见下) |
关于 app_name 和 display_name
两个字段作用完全一样 —— 提供给文档站展示的友好名称(如 Google Play Store)。 实际项目里用 app_name,display_name 作为兼容别名保留。如果两个都写了,display_name 优先。
资源文件
图标放在 resources/ic_launcher.<ext>,文档站构建时会自动提取并展示在 Skill Hub 卡片上:
skills/your-skill/
├── SKILL.md
├── scripts/
└── resources/
└── ic_launcher.webp ← 推荐 webp,也支持 png / jpg / svgcommands 字段
虽然 scripts/*.js 会自动注册为命令,但在 frontmatter 里显式声明一遍可以让文档站渲染出命令表格:
commands:
- name: open
description: 启动 App 首页
- name: search
description: 搜索关键词
args:
- name: keyword
required: true
description: 搜索词
- name: close
description: 强制停止 App3. 写脚本
每个 scripts/*.js 是一个 ES module,默认导出一个异步函数,接收 ctx 参数:
// skills/your-skill/scripts/open.js
export default async function open(ctx) {
const { pb, device, args, log } = ctx
log.info(`在设备 ${device.id} 上启动 ${args.package || 'com.example.yourapp'}`)
await pb.activity.start({
package: 'com.example.yourapp',
action: 'android.intent.action.MAIN',
category: 'android.intent.category.LAUNCHER',
})
// 等应用起来
await pb.sleep(1500)
// 返回当前页面状态
return {
ok: true,
foreground: await pb.window.topPackage(),
}
}ctx 里有什么
| 字段 | 说明 |
|---|---|
pb | phonebase capability 对象,所有设备操作都从这里走 |
device | 目标设备信息 { id, model, platform, ... } |
args | 命令行参数(经过解析的对象) |
log | 结构化日志 log.info / log.warn / log.error |
skill | 当前 skill 的元数据(frontmatter 解析结果) |
pb.* 常用 capability
| Capability | 说明 |
|---|---|
pb.activity.start({ package, action, data }) | 启动 Activity / 打开 deeplink |
pb.activity.forceStop(package) | 强制停止 App |
pb.input.click(x, y) | 点击坐标 |
pb.input.text(str) | 输入文本 |
pb.input.keyEvent(code) | 发送按键事件 |
pb.window.dump() | 获取当前窗口 UI 树(JSON) |
pb.window.topPackage() | 当前前台 App 包名 |
pb.screen.capture() | 截图,返回 Buffer |
pb.shell.exec(cmd) | 执行 shell 命令 |
pb.file.push(local, remote) | 推文件到设备 |
pb.file.pull(remote, local) | 从设备拉文件 |
pb.sleep(ms) | 等待 |
参数处理示例
// skills/your-skill/scripts/search.js
export default async function search(ctx) {
const { pb, args, log } = ctx
if (!args.keyword) {
throw new Error('缺少 --keyword 参数')
}
log.info(`搜索: ${args.keyword}`)
// 优先走 deeplink,比模拟 UI 点击稳定得多
await pb.activity.start({
action: 'android.intent.action.VIEW',
data: `myapp://search?q=${encodeURIComponent(args.keyword)}`,
})
await pb.sleep(1500)
const ui = await pb.window.dump()
return {
ok: true,
resultCount: ui.nodes.filter((n) => n.class === 'ResultItem').length,
}
}调用:
pb your-skill search --keyword "coffee"4. 写作约定
4.1 优先 deeplink,不要盲点 UI
Android 上大部分应用都有 scheme:// 或 market:// 这类 deeplink,直接 pb.activity.start 发 intent 比模拟点击稳定 10 倍。
反例:模拟点击搜索框 → 等弹出 → 输入文本 → 点搜索按钮(每一步都可能因为 UI 版本不同而失败)
正例:直接 market://search?q=xxx 让系统路由到 Play Store 结果页
4.2 所有脚本必须幂等或可重入
用户可能在任何状态下调用 open,脚本要能处理:
- App 未安装 → 抛清晰的错误
- App 已在前台 → 直接返回
- App 在后台 → 拉回前台
4.3 返回结构化数据,不要只打印
// ❌ 不好
console.log('打开成功')
// ✅ 好
return { ok: true, foreground: 'com.example.app' }CLI 会把返回值格式化给用户;HTTP/TCP 调用也能拿到这个对象。
4.4 错误要有上下文
// ❌ 不好
throw new Error('失败')
// ✅ 好
throw new Error(`启动 ${pkg} 失败:前台仍是 ${currentPkg}`)4.5 不要把密钥硬编码进脚本
如果 skill 需要账号密码、token 之类,走 args 从命令行传,或者用 pb.secret.get(key) 从用户的密钥存储读。
5. 本地测试
把你的 skill 目录软链到本地 pb 的 skills 目录,或直接在仓库根目录运行:
# 在 phonebase-skill-hub 仓库根目录
pb skills link skills/your-skill
pb device list
pb your-skill open --device <your-device-id>
pb your-skill state --device <your-device-id>
pb your-skill close --device <your-device-id>确认:
- [ ] 每个命令都能正常返回
- [ ] 错误情况有清晰的提示
- [ ] 不同设备、不同应用版本都能跑
6. 提交 PR
git checkout -b add-your-skill
git add skills/your-skill
git commit -m "feat: 新增 your-skill"
git push origin add-your-skill
gh pr create --title "新增 your-skill" --fillPR 自动触发的 CI 会检查:
SKILL.mdfrontmatter schema 校验- 每个
scripts/*.js默认导出 - 基本的 lint 规则
合并后文档站会自动重建,你的 skill 会出现在 Skill Hub 列表里。
7. 进阶话题
7.1 共享工具代码
多个脚本有重复逻辑?放到 _lib.js:
// skills/your-skill/scripts/_lib.js
export async function ensureAppForeground(pb, pkg) {
const current = await pb.window.topPackage()
if (current === pkg) return
await pb.activity.start({ package: pkg })
await pb.sleep(1500)
}
// skills/your-skill/scripts/search.js
import { ensureAppForeground } from './_lib.js'
export default async function search(ctx) {
await ensureAppForeground(ctx.pb, 'com.example.app')
// ...
}下划线开头的文件不会被注册为命令。
7.2 引用资源文件
如果你的 skill 需要图片、配置等静态资源,放在 skills/your-skill/assets/ 下,脚本里用 import.meta.url 定位:
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const assetPath = fileURLToPath(new URL('../assets/config.json', import.meta.url))7.3 跨 skill 调用
在脚本里通过 ctx.skill.invoke('<other-skill>', '<command>', args) 调用别的 skill 的命令,协议层会帮你分发。
7.4 声明依赖
如果你的 skill 依赖特定 capability 版本,在 frontmatter 里声明:
requires:
capability: '>=2.0.0'
skills:
- name: login
version: '>=1.0.0'8. 检查清单
提 PR 前过一遍:
- [ ]
SKILL.md有name/description/platform - [ ]
commands字段列全了所有子命令 - [ ] 每个脚本都是 ES module 默认导出的异步函数
- [ ] 脚本返回结构化对象,不是
console.log - [ ] 错误信息带上下文
- [ ] 幂等、可重入
- [ ] 真实设备跑通(不能只在 mock 上测)
- [ ] 没有硬编码密钥
9. 参考已有 skill
看别人怎么写的最快:
- googleplay —— 经典 deeplink 驱动
- tiktok —— UI 操作 + 元素定位
写完了?提 PR 吧 → phonebase-skill-hub