Skip to content

编写 Skill 教程

一个 skill 就是一组驱动某款 App(或系统功能)的自动化脚本。你写的 skill 合并到 phonebase-skill-hub 之后,任何 phonebase 用户都能 pb skills install <你的skill> 装上,在自己的云手机上跑。

本教程带你 10 分钟写完第一个 skill。


0. 准备

你需要:

  • 本地装好 pb CLI(参考 快速开始
  • 一台能连上的云手机或 Android 真机
  • Node.js 20+
  • 一个 GitHub 账号(用来提 PR)

Fork 仓库到你自己的账号:

bash
gh repo fork phonebase-cloud/phonebase-skill-hub --clone
cd phonebase-skill-hub

1. 目录结构

每个 skill 住在 skills/<id>/ 下,最少只要两个东西:

skills/
└── your-skill/
    ├── SKILL.md           ← 元数据 + 说明(必需)
    └── scripts/           ← 脚本目录(必需)
        ├── open.js
        ├── close.js
        └── state.js

规则:

  • <id> 是 skill 的唯一标识,会直接变成命令前缀(pb your-skill open)。全小写、连字符,不要带空格
  • 每个 .js 文件就是一个子命令,文件名 = 命令名(open.jspb your-skill open
  • _lib.js / _utils.js 等下划线开头的文件不会被注册为命令,可以放共享代码

2. 写 SKILL.md

SKILL.mdYAML frontmatter + markdown 正文。frontmatter 是结构化元数据(文档站会读它生成列表页和详情页),正文是给人看的说明。

最小示例:

markdown
---
name: your-skill
description: 一句话说明这个 skill 做什么
platform: android
package: com.example.yourapp
---

# Your Skill

详细说明 skill 的用途、依赖、注意事项。

可用字段

字段类型必填说明
namestringskill 名称,通常与目录名一致,用作命令前缀
descriptionstring一句话描述,显示在列表页
app_namestring友好的展示名称,如 Google Play Store。文档站卡片和详情页标题用它
display_namestringapp_name 的别名,优先级更高。两者只写一个即可
versionstring语义版本,如 1.0.0
authorstring作者或组织
platformandroid | ios | web目标平台,影响列表页分组。缺省时按 package / bundle_id 自动推断
categorystring自由分类标签,如 app-store / social / e-commerce
packagestringAndroid 包名(仅 android 平台使用)
bundle_idstringiOS bundle id(仅 ios 平台使用)
tagsstring[]任意标签数组
requiresstring[]依赖的其他 skill 或 capability,如 ["googleservices"]
commandsobject[]显式声明命令列表(见下)

关于 app_namedisplay_name

两个字段作用完全一样 —— 提供给文档站展示的友好名称(如 Google Play Store)。 实际项目里用 app_namedisplay_name 作为兼容别名保留。如果两个都写了,display_name 优先。

资源文件

图标放在 resources/ic_launcher.<ext>,文档站构建时会自动提取并展示在 Skill Hub 卡片上:

skills/your-skill/
├── SKILL.md
├── scripts/
└── resources/
    └── ic_launcher.webp    ← 推荐 webp,也支持 png / jpg / svg

commands 字段

虽然 scripts/*.js 会自动注册为命令,但在 frontmatter 里显式声明一遍可以让文档站渲染出命令表格:

yaml
commands:
  - name: open
    description: 启动 App 首页
  - name: search
    description: 搜索关键词
    args:
      - name: keyword
        required: true
        description: 搜索词
  - name: close
    description: 强制停止 App

3. 写脚本

每个 scripts/*.js 是一个 ES module,默认导出一个异步函数,接收 ctx 参数:

js
// 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 里有什么

字段说明
pbphonebase 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)等待

参数处理示例

js
// 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,
  }
}

调用:

bash
pb your-skill search --keyword "coffee"

4. 写作约定

Android 上大部分应用都有 scheme://market:// 这类 deeplink,直接 pb.activity.start 发 intent 比模拟点击稳定 10 倍。

反例:模拟点击搜索框 → 等弹出 → 输入文本 → 点搜索按钮(每一步都可能因为 UI 版本不同而失败)

正例:直接 market://search?q=xxx 让系统路由到 Play Store 结果页

4.2 所有脚本必须幂等可重入

用户可能在任何状态下调用 open,脚本要能处理:

  • App 未安装 → 抛清晰的错误
  • App 已在前台 → 直接返回
  • App 在后台 → 拉回前台

4.3 返回结构化数据,不要只打印

js
// ❌ 不好
console.log('打开成功')

// ✅ 好
return { ok: true, foreground: 'com.example.app' }

CLI 会把返回值格式化给用户;HTTP/TCP 调用也能拿到这个对象。

4.4 错误要有上下文

js
// ❌ 不好
throw new Error('失败')

// ✅ 好
throw new Error(`启动 ${pkg} 失败:前台仍是 ${currentPkg}`)

4.5 不要把密钥硬编码进脚本

如果 skill 需要账号密码、token 之类,走 args 从命令行传,或者用 pb.secret.get(key) 从用户的密钥存储读。


5. 本地测试

把你的 skill 目录软链到本地 pb 的 skills 目录,或直接在仓库根目录运行:

bash
# 在 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

bash
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" --fill

PR 自动触发的 CI 会检查:

  • SKILL.md frontmatter schema 校验
  • 每个 scripts/*.js 默认导出
  • 基本的 lint 规则

合并后文档站会自动重建,你的 skill 会出现在 Skill Hub 列表里。


7. 进阶话题

7.1 共享工具代码

多个脚本有重复逻辑?放到 _lib.js

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 定位:

js
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 里声明:

yaml
requires:
  capability: '>=2.0.0'
  skills:
    - name: login
      version: '>=1.0.0'

8. 检查清单

提 PR 前过一遍:

  • [ ] SKILL.mdname / description / platform
  • [ ] commands 字段列全了所有子命令
  • [ ] 每个脚本都是 ES module 默认导出的异步函数
  • [ ] 脚本返回结构化对象,不是 console.log
  • [ ] 错误信息带上下文
  • [ ] 幂等、可重入
  • [ ] 真实设备跑通(不能只在 mock 上测
  • [ ] 没有硬编码密钥

9. 参考已有 skill

看别人怎么写的最快:


写完了?提 PR 吧 → phonebase-skill-hub