archive: 紫鸟/HubStudio 文档快照(3889 篇 Markdown)
来源:紫鸟帮助中心/博客/主站资讯 + HubStudio API 文档/帮助中心 + 三个官方 skill 仓库。 只入库 Markdown 与元数据;原始 HTML 与 tarball 留在本机。
This commit is contained in:
commit
322b7737eb
3976 files changed
+695714
No files matched your search
@@ -0,0 +1,151 @@
|
||||
# HubStudio ADB Connection Skill
|
||||
|
||||
This guide standardizes ADB connection workflows for HubStudio cloud phones.
|
||||
|
||||
## Scope
|
||||
|
||||
- Android 12 / Android 15: direct ADB connection
|
||||
- Android 13 / Android 14 / Android 15A: SSH tunnel + local ADB connection
|
||||
|
||||
## Preconditions
|
||||
|
||||
- HubStudio local API is reachable (`http://127.0.0.1:6873`)
|
||||
- Cloud phone is powered on
|
||||
- ADB access is enabled
|
||||
- Local `adb` command is installed and available in PATH
|
||||
|
||||
### If ADB is not installed
|
||||
|
||||
macOS (Homebrew):
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
adb version
|
||||
```
|
||||
|
||||
Windows (winget):
|
||||
|
||||
```powershell
|
||||
winget install Google.PlatformTools
|
||||
adb version
|
||||
```
|
||||
|
||||
Manual install (all platforms):
|
||||
|
||||
- Download Android SDK Platform Tools from Google
|
||||
- Add `platform-tools` directory to PATH
|
||||
- Re-open terminal and run `adb version`
|
||||
|
||||
## Step 0: Enable ADB and Query Connection Info
|
||||
|
||||
1) Enable ADB:
|
||||
|
||||
```bash
|
||||
node hubstudio.js adbEnable <mobileId>
|
||||
```
|
||||
|
||||
2) Query ADB info:
|
||||
|
||||
```bash
|
||||
node hubstudio.js adbInfo <mobileId>
|
||||
```
|
||||
|
||||
Expected fields from response:
|
||||
- `adbIp`
|
||||
- `adbPort`
|
||||
- `adbPassword` (connection code)
|
||||
- For some versions/platforms, tunnel command fields may also be returned
|
||||
|
||||
## Workflow A: Android 12 / Android 15
|
||||
|
||||
### A1. Connect directly to cloud phone
|
||||
|
||||
```bash
|
||||
adb connect <adbIp>:<adbPort>
|
||||
```
|
||||
|
||||
### A2. Authenticate with connection code
|
||||
|
||||
```bash
|
||||
adb shell <adbPassword>
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
adb -s <adbIp>:<adbPort> shell <adbPassword>
|
||||
```
|
||||
|
||||
### A3. Verify connection
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
## Workflow B: Android 13 / Android 14 / Android 15A
|
||||
|
||||
### B1. Create SSH tunnel (first terminal)
|
||||
|
||||
```bash
|
||||
ssh -oHostKeyAlgorithms=+ssh-rsa <sshUser>@<sshHost> -p <sshPort> -L <localPort>:adb-proxy:<remoteAdbPort> -Nf
|
||||
```
|
||||
|
||||
When prompted, input the SSH password from platform response.
|
||||
|
||||
### B2. Connect local forwarded ADB port (second terminal)
|
||||
|
||||
```bash
|
||||
adb connect localhost:<localPort>
|
||||
```
|
||||
|
||||
### B3. Verify connection
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
List of devices attached
|
||||
localhost:<localPort> device
|
||||
```
|
||||
|
||||
## Complete Operation Checklist
|
||||
|
||||
- [ ] Cloud phone is powered on
|
||||
- [ ] `enableAdb=true` applied successfully
|
||||
- [ ] `list-adb` returns non-null ADB fields
|
||||
- [ ] Chosen proper workflow by Android version
|
||||
- [ ] `adb devices` shows connected device
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
adb shell
|
||||
adb shell pm list packages
|
||||
adb install app.apk
|
||||
adb uninstall <package.name>
|
||||
adb reboot
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `adb connect` timeout or refused
|
||||
|
||||
- Confirm cloud phone status is running
|
||||
- Confirm ADB is enabled
|
||||
- Re-query `list-adb` to ensure `adbIp/adbPort` is valid
|
||||
- Check local firewall/network policy
|
||||
|
||||
### SSH tunnel mode cannot connect
|
||||
|
||||
- Verify tunnel process is alive
|
||||
- Confirm `<localPort>` is not occupied
|
||||
- Rebuild SSH tunnel and retry `adb connect localhost:<localPort>`
|
||||
|
||||
### Connected but shell auth fails
|
||||
|
||||
- Recheck `adbPassword` from latest `list-adb` response
|
||||
- Retry on specific device with `adb -s ... shell <code>`
|
||||
@@ -0,0 +1,606 @@
|
||||
# OpenClaw + HubStudio + Playwright 实操教程(完整可执行)
|
||||
|
||||
---
|
||||
|
||||
## 1. 三个工具怎么分工
|
||||
|
||||
- `OpenClaw`:作为统一入口和运行环境(网关、控制台、任务入口)
|
||||
- `HubStudio`:提供本地 API,负责启动/关闭浏览器环境
|
||||
- `Playwright`:连接 HubStudio 打开的浏览器并执行自动化动作
|
||||
|
||||
一句话理解:**OpenClaw 管流程,HubStudio 管环境,Playwright 管页面操作。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 官方文档入口
|
||||
|
||||
- OpenClaw 入门与安装:<https://docs.openclaw.ai/start/getting-started>
|
||||
- HubStudio 接口文档:<https://api-docs.hubstudio.cn/>
|
||||
- github HubStudio Skill 项目地址:<https://github.com/hubstudio-Max/hubstudio-skill>
|
||||
---
|
||||
|
||||
## 3. 第一次配置(只需做一次)
|
||||
|
||||
在终端中执行:
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill"
|
||||
```
|
||||
|
||||
### 3.1 检查基础工具
|
||||
|
||||
```bash
|
||||
node --version
|
||||
openclaw --version
|
||||
node hubstudio.js help
|
||||
```
|
||||
|
||||
### 3.2 初始化 OpenClaw(关键)
|
||||
|
||||
按 OpenClaw 官方推荐,先做 onboarding:
|
||||
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
如果网关正常,你会看到网关在线状态。
|
||||
|
||||
### 3.3 安装 Playwright
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install playwright
|
||||
```
|
||||
|
||||
校验:
|
||||
|
||||
```bash
|
||||
node -e "const { chromium } = require('playwright'); console.log('playwright ok:', !!chromium)"
|
||||
```
|
||||
|
||||
输出 `playwright ok: true` 即可继续。
|
||||
|
||||
---
|
||||
|
||||
## 4. 结合 OpenClaw 的标准执行流程
|
||||
|
||||
本项目已经提供脚本:
|
||||
|
||||
- `playwright_hubstudio_baidu_demo.js`
|
||||
|
||||
推荐按下面顺序执行,这样就体现了 OpenClaw 的结合方式。
|
||||
|
||||
### Step 1:先确认 OpenClaw 网关与状态
|
||||
|
||||
```bash
|
||||
openclaw gateway status
|
||||
openclaw status
|
||||
```
|
||||
|
||||
### Step 2:可选,打开 OpenClaw 控制台(便于观察)
|
||||
|
||||
```bash
|
||||
openclaw dashboard
|
||||
```
|
||||
|
||||
> 这一步是可选的,主要用于你在浏览器中查看 OpenClaw 控制界面。
|
||||
|
||||
### Step 3:执行自动化任务脚本
|
||||
|
||||
```bash
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
如果你要指定其他环境编号:
|
||||
|
||||
```bash
|
||||
CONTAINER_CODE="1502764609" node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 这个脚本里 OpenClaw 是怎么“参与”的
|
||||
|
||||
很多同学会问:脚本里没写 `openclaw run ...`,怎么算结合?
|
||||
|
||||
这里的结合点是:
|
||||
|
||||
1. 你先用 `openclaw onboard` 和 `openclaw gateway status` 建立统一运行入口
|
||||
2. 你可通过 `openclaw dashboard` 观察控制面
|
||||
3. 业务自动化脚本在同一工程环境执行,OpenClaw 负责“控制层”和“入口层”
|
||||
4. 具体 API 调用与浏览器操作由 `hubstudio.js + Playwright` 完成
|
||||
|
||||
这是一种稳定、常见的“控制层与执行层分离”方式。
|
||||
|
||||
---
|
||||
|
||||
## 6. 成功后你会看到什么
|
||||
|
||||
终端会输出类似:
|
||||
|
||||
- `== [1/6] 启动 HubStudio 环境 ==`
|
||||
- `环境启动成功,CDP 端口:xxxxx`
|
||||
- `== [4/6] 点击第一条搜索结果 ==`
|
||||
- `== [5/6] 提取页面内容并生成摘要 ==`
|
||||
- `摘要文件已生成:.../summary_hubstudio_baidu_playwright.txt`
|
||||
- `== [6/6] 关闭 HubStudio 环境 ==`
|
||||
|
||||
---
|
||||
|
||||
## 7. 结果文件说明
|
||||
|
||||
脚本会生成:
|
||||
|
||||
- `summary_hubstudio_baidu_playwright.txt`
|
||||
|
||||
文件内包含:
|
||||
|
||||
- 首条搜索结果标题/链接
|
||||
- 跳转后页面标题/URL
|
||||
- 页面摘要(中文)
|
||||
- 抓取正文片段(前 1500 字符)
|
||||
|
||||
---
|
||||
|
||||
## 8. 一条命令跑全流程(给同事复制)
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill" && \
|
||||
openclaw gateway status && \
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### Q1:`openclaw gateway status` 显示异常
|
||||
|
||||
先重新执行:
|
||||
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
### Q2:脚本启动环境失败
|
||||
|
||||
检查:
|
||||
|
||||
- HubStudio 客户端是否在线并登录
|
||||
- 环境编号 `1502764609` 是否属于当前账号
|
||||
- 本地 API 是否可访问(默认 `http://127.0.0.1:6873`)
|
||||
|
||||
### Q3:脚本提示缺少 Playwright
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install playwright
|
||||
```
|
||||
|
||||
### Q4:第一条链接点击后没跳转
|
||||
|
||||
这是百度页面结构或弹窗变化导致,通常重跑一次即可。
|
||||
如果仍失败,可把脚本中的等待时间适当调大(如 `3500` 调到 `5000`)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 推荐的日常使用顺序
|
||||
|
||||
每天执行任务时,按这个顺序最稳:
|
||||
|
||||
1. `openclaw gateway status`
|
||||
2. `node playwright_hubstudio_baidu_demo.js`
|
||||
3. 检查 `summary_hubstudio_baidu_playwright.txt`
|
||||
|
||||
这样就实现了“通过 OpenClaw 统一入口 + HubStudio 环境 + Playwright 执行”的完整闭环。
|
||||
# OpenClaw + HubStudio + Playwright 完整示例教程(零基础版)
|
||||
|
||||
> 本教程是“只保留可成功路径”的版本。
|
||||
> 你只需要照着复制命令,就能完成:
|
||||
> 启动 HubStudio 环境 `1502764609` -> 打开百度搜索 `HubStudio` -> 点击第一条 -> 抓取内容并生成摘要文件 -> 关闭环境。
|
||||
|
||||
---
|
||||
|
||||
## 1. 你要准备什么
|
||||
|
||||
### 1.1 已安装软件
|
||||
|
||||
- HubStudio 客户端(并且可正常登录)
|
||||
- Node.js(建议 22+)
|
||||
- OpenClaw CLI
|
||||
|
||||
### 1.2 官方文档入口(建议先收藏)
|
||||
|
||||
- OpenClaw 安装与入门:<https://docs.openclaw.ai/start/getting-started>
|
||||
- HubStudio 接口文档:<https://api-docs.hubstudio.cn/>
|
||||
- github <https://github.com/hubstudio-Max/hubstudio-skill>
|
||||
---
|
||||
|
||||
## 2. 第一次操作前检查(3 分钟)
|
||||
|
||||
在终端里执行下面 4 条命令,确认环境是通的:
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill"
|
||||
node --version
|
||||
openclaw --version
|
||||
node hubstudio.js help
|
||||
```
|
||||
|
||||
看到版本号和帮助信息就可以继续。
|
||||
|
||||
---
|
||||
|
||||
## 3. 安装 Playwright(本教程用它做自动化)
|
||||
|
||||
> 注意:本教程不使用 `agent-browser`,统一改为 `Playwright`。
|
||||
|
||||
在项目目录执行:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install playwright
|
||||
```
|
||||
|
||||
安装完成后,测试一下:
|
||||
|
||||
```bash
|
||||
node -e "const { chromium } = require('playwright'); console.log('playwright ok:', !!chromium)"
|
||||
```
|
||||
|
||||
输出 `playwright ok: true` 即表示安装成功。
|
||||
|
||||
---
|
||||
|
||||
## 4. 一次性创建自动化脚本(推荐)
|
||||
|
||||
本项目里已经放好脚本文件:
|
||||
|
||||
- `playwright_hubstudio_baidu_demo.js`
|
||||
|
||||
这个脚本会自动做 6 件事:
|
||||
|
||||
1. 调用 HubStudio API 启动环境 `1502764609`
|
||||
2. 读取返回里的 `debuggingPort`
|
||||
3. 用 Playwright 通过 CDP 连接到该环境
|
||||
4. 打开百度,搜索 `HubStudio`,点击第一条结果
|
||||
5. 抓取结果页文本,并生成摘要文件
|
||||
6. 调用 HubStudio API 关闭环境
|
||||
|
||||
---
|
||||
|
||||
## 5. 直接运行(复制即用)
|
||||
|
||||
在项目目录执行:
|
||||
|
||||
```bash
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
如果你要换环境编号,可以这样执行:
|
||||
|
||||
```bash
|
||||
CONTAINER_CODE="1502764609" node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 运行成功后你会看到什么
|
||||
|
||||
终端会依次打印:
|
||||
|
||||
- `== [1/6] 启动 HubStudio 环境 ==`
|
||||
- `环境启动成功,CDP 端口:xxxx`
|
||||
- `== [3/6] 打开百度并搜索 HubStudio ==`
|
||||
- `== [4/6] 点击第一条搜索结果 ==`
|
||||
- `== [5/6] 提取页面内容并生成摘要 ==`
|
||||
- `摘要文件已生成:.../summary_hubstudio_baidu_playwright.txt`
|
||||
- `== [6/6] 关闭 HubStudio 环境 ==`
|
||||
|
||||
---
|
||||
|
||||
## 7. 结果文件在哪里
|
||||
|
||||
脚本会在当前目录生成:
|
||||
|
||||
- `summary_hubstudio_baidu_playwright.txt`
|
||||
|
||||
文件内包含:
|
||||
|
||||
- 首条结果标题
|
||||
- 首条结果链接
|
||||
- 跳转后页面标题
|
||||
- 跳转后页面 URL
|
||||
- 自动生成的中文摘要
|
||||
- 抓取到的正文片段(前 1500 字符)
|
||||
|
||||
---
|
||||
|
||||
## 8. 本教程对应的真实示例结果(已验证)
|
||||
|
||||
按本教程实际执行后,典型结果如下:
|
||||
|
||||
- 环境 `1502764609` 启动成功(`code: 0`)
|
||||
- 成功拿到 `debuggingPort` 并完成 CDP 连接
|
||||
- 成功执行百度搜索并点击第一条
|
||||
- 成功提取页面文本并输出摘要文件
|
||||
- 环境关闭成功(`code: 0`)
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题(新手友好)
|
||||
|
||||
### Q1:运行时提示 `Cannot find module 'playwright'`
|
||||
|
||||
说明还没安装依赖,回到第 3 节执行:
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install playwright
|
||||
```
|
||||
|
||||
### Q2:脚本卡在“启动 HubStudio 环境”
|
||||
|
||||
请确认:
|
||||
|
||||
- HubStudio 客户端处于打开状态
|
||||
- 账号已登录并已开通VIP
|
||||
- 环境编号 `1502764609` 在当前账号下可用
|
||||
|
||||
### Q3:百度第一页结构变化,点不到第一条
|
||||
|
||||
这是网页结构偶尔变化导致,脚本里已经用了多个候选选择器。
|
||||
通常重试一次就可以;如仍失败,可把脚本中的等待时间 `3500` 改成 `5000` 再试。
|
||||
|
||||
### Q4:我想给同事复用
|
||||
|
||||
只要把这 3 个文件给同事并按教程执行即可:
|
||||
|
||||
- `hubstudio.js`
|
||||
- `commands.generated.json`
|
||||
- `playwright_hubstudio_baidu_demo.js`
|
||||
|
||||
---
|
||||
|
||||
## 10. 你可以复制给同事的“最短步骤”
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill"
|
||||
npm init -y
|
||||
npm install playwright
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
如果终端出现摘要文件路径,说明流程成功。
|
||||
# OpenClaw + HubStudio + agent-browser 新手实操教程
|
||||
|
||||
> 目标:按这份文档一步一步操作,完成以下任务:
|
||||
> 启动环境 `1502764609` -> 打开百度搜索 `HubStudio` -> 点击第一条结果 -> 提取页面内容并给出总结 -> 关闭环境。
|
||||
|
||||
---
|
||||
|
||||
## 1) 这份文档适合谁
|
||||
|
||||
- 第一次接触命令行、只会复制粘贴也能操作
|
||||
- 已经安装好 `OpenClaw`、`HubStudio`、`agent-browser`
|
||||
- 希望有一条“从开始到结束”的固定流程,不需要自己拼命令
|
||||
|
||||
---
|
||||
|
||||
## 2) 你会得到什么结果
|
||||
|
||||
跑完后你会拿到 4 类结果:
|
||||
|
||||
- 环境是否成功启动(会看到 `code: 0`)
|
||||
- 当前连接的页面标题和网址
|
||||
- 目标页面的正文片段(用于总结)
|
||||
- 自动关闭环境,避免环境一直占用
|
||||
|
||||
---
|
||||
|
||||
## 3) 开始前准备(只做一次)
|
||||
|
||||
### 3.1 打开 HubStudio 客户端
|
||||
|
||||
请先确认:
|
||||
|
||||
- HubStudio 客户端是打开状态
|
||||
- 当前账号已登录并已开通VIP
|
||||
- 环境 `1502764609` 在你的账号下可见
|
||||
|
||||
### 3.2 打开终端并进入项目目录
|
||||
|
||||
复制执行:
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill"
|
||||
```
|
||||
|
||||
### 3.3 快速检查三个工具
|
||||
|
||||
复制执行:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
agent-browser --version
|
||||
node hubstudio.js help
|
||||
```
|
||||
|
||||
看到版本号或帮助信息即可,不用纠结输出细节。
|
||||
|
||||
---
|
||||
|
||||
## 4) 一键执行脚本(最推荐)
|
||||
|
||||
这一节是给“希望最稳最省事”的用法。
|
||||
你只需要创建脚本一次,后面每次运行一条命令就行。
|
||||
|
||||
### 4.1 创建脚本文件
|
||||
|
||||
在项目目录新建文件 `examples_baidu_hubstudio.sh`,内容如下:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_CODE="1502764609"
|
||||
|
||||
echo "========== [1/6] 启动 HubStudio 环境 =========="
|
||||
START_JSON=$(node hubstudio.js browserStart "$CONTAINER_CODE")
|
||||
echo "$START_JSON" | jq '{code,msg,debuggingPort:(.payload.data.debuggingPort // null)}'
|
||||
|
||||
CDP_PORT=$(echo "$START_JSON" | jq -r '.payload.data.debuggingPort // empty')
|
||||
if [ -z "$CDP_PORT" ]; then
|
||||
echo "未拿到 debuggingPort,流程终止"
|
||||
exit 1
|
||||
fi
|
||||
echo "CDP_PORT=$CDP_PORT"
|
||||
|
||||
echo "========== [2/6] OpenClaw 状态确认 =========="
|
||||
openclaw status || true
|
||||
|
||||
echo "========== [3/6] 打开百度搜索 HubStudio =========="
|
||||
agent-browser --cdp "$CDP_PORT" open "https://www.baidu.com/s?wd=HubStudio"
|
||||
agent-browser --cdp "$CDP_PORT" wait 3500
|
||||
|
||||
echo "========== [4/6] 点击第一条搜索结果 =========="
|
||||
CLICK_JSON=$(agent-browser --cdp "$CDP_PORT" --json eval "(() => {
|
||||
const a = document.querySelector('#content_left .result h3 a, #content_left .c-container h3 a, h3 a');
|
||||
if (!a) return { ok:false, reason:'no-result-link' };
|
||||
const href = a.href;
|
||||
const title = a.innerText.trim();
|
||||
a.click();
|
||||
return { ok:true, title, href };
|
||||
})()")
|
||||
echo "$CLICK_JSON" | jq .
|
||||
|
||||
agent-browser --cdp "$CDP_PORT" wait 6000
|
||||
|
||||
echo "========== [5/6] 提取页面信息并生成摘要 =========="
|
||||
TITLE_JSON=$(agent-browser --cdp "$CDP_PORT" --json get title)
|
||||
URL_JSON=$(agent-browser --cdp "$CDP_PORT" --json get url)
|
||||
CONTENT_JSON=$(agent-browser --cdp "$CDP_PORT" --json eval "(() => {
|
||||
const text = (document.body?.innerText || '').replace(/\\s+/g, ' ').trim();
|
||||
return text.slice(0, 3500);
|
||||
})()")
|
||||
|
||||
echo "$TITLE_JSON" | jq .
|
||||
echo "$URL_JSON" | jq .
|
||||
|
||||
TITLE=$(echo "$TITLE_JSON" | jq -r '.data.title // "unknown"')
|
||||
URL=$(echo "$URL_JSON" | jq -r '.data.url // "unknown"')
|
||||
EXCERPT=$(echo "$CONTENT_JSON" | jq -r '.data.result // ""')
|
||||
|
||||
SUMMARY_FILE="summary_hubstudio_baidu.txt"
|
||||
{
|
||||
echo "页面标题: $TITLE"
|
||||
echo "页面地址: $URL"
|
||||
echo ""
|
||||
echo "页面内容摘要:"
|
||||
echo "- 该页面是 Hubstudio 相关的下载/介绍页。"
|
||||
echo "- 主要强调多账号环境、指纹浏览器、云手机等能力。"
|
||||
echo "- 页面通常包含版本信息、下载入口和安装说明。"
|
||||
echo ""
|
||||
echo "抓取片段(前3500字符):"
|
||||
echo "$EXCERPT"
|
||||
} > "$SUMMARY_FILE"
|
||||
|
||||
echo "摘要已写入: $SUMMARY_FILE"
|
||||
|
||||
echo "========== [6/6] 关闭环境 =========="
|
||||
node hubstudio.js browserStop "$CONTAINER_CODE" | jq '{code,msg}'
|
||||
|
||||
echo "全部完成。"
|
||||
```
|
||||
|
||||
### 4.2 给脚本执行权限
|
||||
|
||||
```bash
|
||||
chmod +x examples_baidu_hubstudio.sh
|
||||
```
|
||||
|
||||
### 4.3 运行脚本
|
||||
|
||||
```bash
|
||||
./examples_baidu_hubstudio.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) 每一步成功的判断标准
|
||||
|
||||
- 启动成功:输出里有 `code: 0`,且出现 `CDP_PORT=xxxx`
|
||||
- 点击成功:`CLICK_JSON` 里 `ok: true`
|
||||
- 跳转成功:`get title` 和 `get url` 不是百度搜索页
|
||||
- 提取成功:生成文件 `summary_hubstudio_baidu.txt`
|
||||
- 收尾成功:`browserStop` 输出里 `code: 0`
|
||||
|
||||
---
|
||||
|
||||
## 6) 本文档对应的实测结果(已通过)
|
||||
|
||||
同样流程已经在本机实际跑通,关键结果如下:
|
||||
|
||||
- 环境 `1502764609` 启动成功,返回 `code: 0`
|
||||
- 返回过可用的 `debuggingPort`(示例中曾拿到 `60343`)
|
||||
- 成功打开百度并点击首条结果
|
||||
- 实际进入页面标题:`Hubstudio下载-Hubstudio最新版下载V3.51.0`
|
||||
- 成功提取页面正文并生成摘要
|
||||
- 最后环境成功关闭,返回 `code: 0`
|
||||
|
||||
---
|
||||
|
||||
## 7) 常见问题(小白版)
|
||||
|
||||
### 7.1 运行脚本时提示命令不存在
|
||||
|
||||
说明工具没有安装到终端环境里。分别执行:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
agent-browser --version
|
||||
node --version
|
||||
```
|
||||
|
||||
哪个报错就先安装哪个。
|
||||
|
||||
### 7.2 启动环境拿不到 `CDP_PORT`
|
||||
|
||||
优先检查:
|
||||
|
||||
- HubStudio 客户端是否在线并已登录
|
||||
- 环境编号是否是 `1502764609`
|
||||
- 是否是当前账号下的环境
|
||||
|
||||
### 7.3 点击第一条失败(`no-result-link`)
|
||||
|
||||
这是网页加载慢或页面结构变化导致。
|
||||
直接重新运行脚本,通常可恢复;也可把等待时间从 `3500` 调到 `5000`。
|
||||
|
||||
### 7.4 结果页不是你预期的网站
|
||||
|
||||
百度首条结果会随地区/时间变化,这是正常现象。
|
||||
流程目标是“点击首条并提取内容”,不是固定某个域名。
|
||||
|
||||
---
|
||||
|
||||
## 8) 你可以直接复制的最短命令版
|
||||
|
||||
如果你不想建脚本,直接按顺序执行以下命令也可以:
|
||||
|
||||
```bash
|
||||
cd "/Users/kxc/Documents/workspace/projects/skills/hubstudio-skill"
|
||||
START_JSON=$(node hubstudio.js browserStart 1502764609)
|
||||
CDP_PORT=$(echo "$START_JSON" | jq -r '.payload.data.debuggingPort // empty')
|
||||
agent-browser --cdp "$CDP_PORT" open "https://www.baidu.com/s?wd=HubStudio"
|
||||
agent-browser --cdp "$CDP_PORT" wait 3500
|
||||
agent-browser --cdp "$CDP_PORT" --json eval "(() => { const a = document.querySelector('#content_left .result h3 a, #content_left .c-container h3 a, h3 a'); if(!a) return {ok:false}; a.click(); return {ok:true}; })()"
|
||||
agent-browser --cdp "$CDP_PORT" wait 6000
|
||||
agent-browser --cdp "$CDP_PORT" --json get title
|
||||
agent-browser --cdp "$CDP_PORT" --json get url
|
||||
node hubstudio.js browserStop 1502764609
|
||||
```
|
||||
@@ -0,0 +1,340 @@
|
||||
# HubStudio OpenAPI Skill
|
||||
|
||||
这是一个面向 `HubStudio` 本地 API 的技能项目,目标是让你可以用统一方式完成:
|
||||
|
||||
- API 能力检索(有哪些接口、每个接口做什么)
|
||||
- 请求参数校验(必填项、类型、约束)
|
||||
- 自动化调用(浏览器环境、云手机、账号、分组等)
|
||||
- 常见问题排查(HTTP 成功但业务失败、环境无法启动等)
|
||||
|
||||
项目核心定位:**把 `openapi.yaml` 的接口能力,转成可查询、可执行、可自动化的工程化工具链。**
|
||||
|
||||
---
|
||||
|
||||
## 项目提供的 Skill 能力
|
||||
|
||||
## 1) 全量接口查询能力
|
||||
|
||||
- 基于 `openapi.yaml` 提供 HubStudio 全量接口定义
|
||||
- 当前生成命令总数:`56`(见 `commands.generated.json`)
|
||||
- 覆盖业务域:
|
||||
- 浏览器环境
|
||||
- 云手机
|
||||
- 环境管理
|
||||
- 平台账号管理
|
||||
- 分组管理
|
||||
|
||||
你可以快速回答类似问题:
|
||||
|
||||
- “打开环境接口叫什么?请求体怎么写?”
|
||||
- “批量开启云手机需要哪些字段?”
|
||||
- “某接口返回字段 `debuggingPort` 是什么含义?”
|
||||
|
||||
---
|
||||
|
||||
## 2) 请求/响应字段解释能力
|
||||
|
||||
Skill 可以按接口粒度解释:
|
||||
|
||||
- 请求参数(query/header/body)
|
||||
- 必填字段与可选字段
|
||||
- 字段类型与含义
|
||||
- 响应 `code/msg/data` 结构
|
||||
|
||||
这使得你可以在自动化流程里先做参数检查,再发请求,减少无效调用。
|
||||
|
||||
---
|
||||
|
||||
## 3) 本地 CLI 执行能力(开箱即用)
|
||||
|
||||
项目内置 `hubstudio.js`,可直接调用 HubStudio 本地 API:
|
||||
|
||||
```bash
|
||||
node hubstudio.js help
|
||||
node hubstudio.js list
|
||||
node hubstudio.js browserStart 1502764609
|
||||
node hubstudio.js browserStatus 1502764609
|
||||
node hubstudio.js browserStop 1502764609
|
||||
```
|
||||
|
||||
也支持生成命令直调:
|
||||
|
||||
```bash
|
||||
node hubstudio.js postV1BrowserStart --body '{"containerCode":"1502764609"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) 浏览器自动化接入能力(Playwright)
|
||||
|
||||
项目已提供端到端示例脚本:`playwright_hubstudio_baidu_demo.js`,可完成:
|
||||
|
||||
1. 启动 HubStudio 环境
|
||||
2. 获取 `debuggingPort`
|
||||
3. 使用 Playwright 通过 CDP 连接该浏览器
|
||||
4. 打开百度并搜索 `HubStudio`
|
||||
5. 点击第一条结果并提取正文
|
||||
6. 生成摘要文件并关闭环境
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
输出结果示例文件:
|
||||
|
||||
- `summary_hubstudio_baidu_playwright.txt`
|
||||
|
||||
---
|
||||
|
||||
## 5) ADB 连接辅助能力(云手机)
|
||||
|
||||
项目提供云手机 ADB 连接专项说明:`ADB_CONNECTION_GUIDE.md`,用于:
|
||||
|
||||
- 开启 ADB
|
||||
- 查询连接信息
|
||||
- 根据 Android 版本选择连接模式
|
||||
- 验证 `adb devices`
|
||||
|
||||
---
|
||||
|
||||
## 项目文件说明
|
||||
|
||||
- `SKILL.md`:Skill 主说明(使用指引、调用建议、FAQ)
|
||||
- `openapi.yaml`:HubStudio OpenAPI 源定义
|
||||
- `reference.md`:从 OpenAPI 生成的本地接口参考文档
|
||||
- `commands.generated.json`:从 OpenAPI 生成的 CLI 命令映射
|
||||
- `hubstudio.js`:统一命令行入口(别名 + 生成命令)
|
||||
- `playwright_hubstudio_baidu_demo.js`:Playwright 端到端自动化示例
|
||||
- `ADB_CONNECTION_GUIDE.md`:云手机 ADB 接入指南
|
||||
- `OPENCLAW_AGENT_BROWSER_TUTORIAL.md`:面向新手的实操教程(当前已切换为 Playwright 流程)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始(建议流程)
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 先进入项目根目录(README 所在目录)
|
||||
cd ./
|
||||
node --version
|
||||
openclaw --version
|
||||
```
|
||||
|
||||
### 2. 安装依赖(如需运行 Playwright 示例)
|
||||
|
||||
```bash
|
||||
npm init -y
|
||||
npm install playwright
|
||||
```
|
||||
|
||||
### 3. 验证 HubStudio API 可用
|
||||
|
||||
```bash
|
||||
node hubstudio.js browserStatus 1502764609
|
||||
```
|
||||
|
||||
若返回 `code: 0` 说明 API 可正常调用。
|
||||
|
||||
### 4. 运行示例
|
||||
|
||||
```bash
|
||||
node playwright_hubstudio_baidu_demo.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装 HubStudio Skill 到 OpenClaw(详细步骤)
|
||||
|
||||
这一节专门说明:如何把当前项目作为一个 Skill 安装到 OpenClaw 并让 OpenClaw 识别。
|
||||
|
||||
### 1) 先确认 OpenClaw 已初始化
|
||||
|
||||
```bash
|
||||
openclaw onboard --install-daemon
|
||||
openclaw gateway status
|
||||
```
|
||||
|
||||
### 2) 创建 OpenClaw 的 workspace skills 目录
|
||||
|
||||
```bash
|
||||
mkdir -p "$HOME/.openclaw/workspace/skills"
|
||||
```
|
||||
|
||||
### 3) 安装方式二选一
|
||||
|
||||
#### 方式 A(推荐):软链接,方便你持续更新
|
||||
|
||||
适合开发调试:你修改当前项目文件,OpenClaw 立即读取最新内容。
|
||||
|
||||
```bash
|
||||
ln -sfn "./" "$HOME/.openclaw/workspace/skills/hubstudio"
|
||||
```
|
||||
|
||||
#### 方式 B:复制一份稳定版本
|
||||
|
||||
适合“先固定版本再使用”。
|
||||
|
||||
```bash
|
||||
rm -rf "$HOME/.openclaw/workspace/skills/hubstudio"
|
||||
cp -R "./" "$HOME/.openclaw/workspace/skills/hubstudio"
|
||||
```
|
||||
|
||||
### 4) 让 OpenClaw 刷新技能状态
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### 5) 验证 Skill 是否安装成功
|
||||
|
||||
```bash
|
||||
openclaw skills check
|
||||
openclaw skills list
|
||||
openclaw skills info hubstudio-openapi
|
||||
```
|
||||
|
||||
成功时你应看到:
|
||||
|
||||
- `hubstudio-openapi` 状态为 `ready`
|
||||
- 来源显示为 `openclaw-workspace`
|
||||
- 路径类似:`~/.openclaw/workspace/skills/hubstudio/SKILL.md`
|
||||
|
||||
### 6) 安装后建议做一次能力验证
|
||||
|
||||
```bash
|
||||
cd ./
|
||||
node hubstudio.js list
|
||||
node hubstudio.js browserStatus 1502764609
|
||||
```
|
||||
|
||||
如果你在 OpenClaw 中调用与 HubStudio API 相关任务时,模型能够命中 `hubstudio-openapi` 这个 skill,即说明集成生效。
|
||||
|
||||
---
|
||||
|
||||
## 响应判定规范(非常重要)
|
||||
|
||||
HubStudio 常见返回结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "Success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
判定规则:
|
||||
|
||||
- `HTTP 200` 不代表业务成功
|
||||
- 以 `code` 为准:
|
||||
- `code = 0`:成功
|
||||
- `code != 0`:业务失败(参数、权限、资源状态等)
|
||||
|
||||
---
|
||||
|
||||
## 参数校验模式(团队统一建议)
|
||||
|
||||
`hubstudio.js` 已内置 OpenAPI 驱动的参数强校验,支持两种模式:
|
||||
|
||||
- `strict=true`(默认)
|
||||
- 开启必填、类型、范围、格式、枚举校验
|
||||
- 拒绝未知字段(如拼错字段名)
|
||||
- `strict=false`
|
||||
- 必填、类型、范围、格式、枚举仍然校验
|
||||
- 仅放过未知字段(用于调试或灰度联调)
|
||||
|
||||
### 1) 默认严格模式(推荐生产)
|
||||
|
||||
```bash
|
||||
node hubstudio.js postV1BrowserStart --body '{"containerCode":"1502764609"}'
|
||||
```
|
||||
|
||||
### 2) 临时切换宽松模式(仅用于排障)
|
||||
|
||||
```bash
|
||||
node hubstudio.js postV1BrowserStart --strict false --body '{"containerCode":"1502764609","x":1}'
|
||||
```
|
||||
|
||||
### 3) 通过环境变量统一设置模式
|
||||
|
||||
```bash
|
||||
# 关闭严格模式(当前 shell 会话生效)
|
||||
export HUBSTUDIO_STRICT_VALIDATION=false
|
||||
node hubstudio.js postV1BrowserStart --body '{"containerCode":"1502764609","x":1}'
|
||||
|
||||
# 恢复严格模式
|
||||
export HUBSTUDIO_STRICT_VALIDATION=true
|
||||
```
|
||||
|
||||
### 4) 校验失败返回格式(标准化)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Request validation failed",
|
||||
"command": "postV1BrowserStart",
|
||||
"fieldErrors": [
|
||||
{
|
||||
"field": "body.containerCode",
|
||||
"reason": "required field missing or empty",
|
||||
"expected": "required",
|
||||
"actualType": "missing"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5) 团队使用建议
|
||||
|
||||
- 日常开发与生产执行统一用默认严格模式
|
||||
- 只在定位线上疑难问题时,短时使用 `--strict false`
|
||||
- 排障结束后务必恢复严格模式,避免脏字段长期进入调用链
|
||||
|
||||
---
|
||||
|
||||
## 典型使用场景
|
||||
|
||||
- 批量管理浏览器环境(开关、状态、窗口排列)
|
||||
- 云手机应用安装/启动/停止自动化
|
||||
- ADB 开启与连接状态查询
|
||||
- 将 HubStudio 接口能力接入 OpenClaw/脚本工作流
|
||||
- 用 Playwright 基于 HubStudio 环境做网页自动化
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1) 接口返回 200 但任务没成功
|
||||
|
||||
请检查返回体中的 `code` 和 `msg`,不要只看 HTTP 状态码。
|
||||
|
||||
### 2) 启动环境失败
|
||||
|
||||
优先检查:
|
||||
|
||||
- HubStudio 客户端是否登录
|
||||
- 是否开通VIP
|
||||
- `containerCode` 是否存在且属于当前账号
|
||||
- 本地服务地址是否是 `http://127.0.0.1:6873` 端口可能存在变动需要检查
|
||||
|
||||
### 3) 自动化脚本连接不上浏览器
|
||||
|
||||
请确认:
|
||||
|
||||
- `browserStart` 后返回了 `debuggingPort`
|
||||
- 环境未提前关闭
|
||||
- 本机端口未被拦截
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- OpenClaw Getting Started: <https://docs.openclaw.ai/start/getting-started>
|
||||
- HubStudio API Docs: <https://api-docs.hubstudio.cn/>
|
||||
- 项目内接口参考:`reference.md`
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
name: hubstudio-openapi
|
||||
description: HubStudio OpenAPI skill for full endpoint lookup, request/response field explanation, and parameter constraint checking. Use when querying HubStudio API interfaces, validating request payloads, or building automation against openapi.yaml.
|
||||
---
|
||||
|
||||
# HubStudio Integration Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Installation](#installation)
|
||||
3. [Configure HubStudio](#configure-hubstudio)
|
||||
4. [Using API/CLI](#using-apicli)
|
||||
5. [Connection Details](#connection-details)
|
||||
6. [Automation Examples](#automation-examples)
|
||||
7. [FAQ](#faq)
|
||||
8. [Advanced Usage](#advanced-usage)
|
||||
9. [ADB Connection Skill](#adb-connection-skill)
|
||||
10. [Resources](#resources)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
HubStudio provides browser environment automation, cloud phone operations, environment management, account management, and group management through local APIs.
|
||||
|
||||
This Skill is designed to:
|
||||
- Locate all HubStudio endpoints quickly
|
||||
- Explain request/response fields and constraints
|
||||
- Provide safe call patterns for automation
|
||||
- Standardize API testing and troubleshooting
|
||||
|
||||
Core docs:
|
||||
- Official API docs: [https://api-docs.hubstudio.cn/](https://api-docs.hubstudio.cn/)
|
||||
- Full local generated reference: [reference.md](reference.md)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install HubStudio Desktop
|
||||
|
||||
Install and open HubStudio client on your machine, then log in.
|
||||
|
||||
### 2. Verify Local API Service
|
||||
|
||||
The current OpenAPI file uses:
|
||||
|
||||
```bash
|
||||
http://127.0.0.1:6873
|
||||
```
|
||||
|
||||
Verify service health with:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "http://127.0.0.1:6873/api/v1/browser/all-browser-status" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### 3. Install Optional Tooling
|
||||
|
||||
```bash
|
||||
# API calls and JSON inspection
|
||||
brew install curl jq
|
||||
|
||||
# Python validation scripts
|
||||
python3 -m pip install --user pyyaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configure HubStudio
|
||||
|
||||
### 1. Login and Prepare Data
|
||||
|
||||
1. Login to HubStudio client
|
||||
2. Ensure browser environments/cloud phones exist
|
||||
3. Confirm local API is reachable
|
||||
|
||||
### 2. Configure Runtime Variables (Optional)
|
||||
|
||||
```bash
|
||||
export HUBSTUDIO_BASE_URL="http://127.0.0.1:6873"
|
||||
export HUBSTUDIO_AUTH_TOKEN="<your-token-if-needed>"
|
||||
```
|
||||
|
||||
### 3. Timing Notes
|
||||
|
||||
- Browser environment startup typically takes **3-5 seconds**
|
||||
- Cloud phone startup typically takes **30-90 seconds**
|
||||
- Build retry/timeout logic based on those windows
|
||||
|
||||
---
|
||||
|
||||
## Using API/CLI
|
||||
|
||||
HubStudio OpenAPI in this project contains 56 operations across these domains:
|
||||
- Browser Environment
|
||||
- Cloud Phone
|
||||
- Environment Management
|
||||
- Account Management
|
||||
- Group Management
|
||||
|
||||
### Recommended Call Pattern
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$HUBSTUDIO_BASE_URL/<endpoint>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '<json-body>'
|
||||
```
|
||||
|
||||
### Node CLI (Direct Execution)
|
||||
|
||||
This project includes `hubstudio.js`, so you can execute capabilities directly:
|
||||
|
||||
```bash
|
||||
node hubstudio.js help
|
||||
node hubstudio.js list
|
||||
node hubstudio.js browserCreate
|
||||
node hubstudio.js browserStart 1474900026
|
||||
node hubstudio.js browserStatus 1474900026
|
||||
node hubstudio.js browserForeground 1474900026
|
||||
node hubstudio.js browserArrange
|
||||
node hubstudio.js browserStop 1474900026
|
||||
node hubstudio.js testAll
|
||||
```
|
||||
|
||||
`hubstudio.js` now supports all OpenAPI endpoints via generated commands in `commands.generated.json`.
|
||||
Example generated command:
|
||||
|
||||
```bash
|
||||
node hubstudio.js postV1BrowserStart --body '{"containerCode":"1474900026"}'
|
||||
```
|
||||
|
||||
### Cloud Phone ADB Commands
|
||||
|
||||
```bash
|
||||
# Enable ADB
|
||||
node hubstudio.js postV1CloudMobileBatchUpdateAdb --body '{"mobileIds":["<mobileId>"],"enableAdb":true}'
|
||||
|
||||
# Query ADB connection info
|
||||
node hubstudio.js postV1CloudMobileListAdb --body '{"mobileIds":["<mobileId>"]}'
|
||||
```
|
||||
|
||||
### Common Browser Environment APIs
|
||||
|
||||
```bash
|
||||
# Open environment
|
||||
curl -s -X POST "http://127.0.0.1:6873/api/v1/browser/start" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"containerCode":"1474900026"}'
|
||||
|
||||
# Close environment
|
||||
curl -s -X POST "http://127.0.0.1:6873/api/v1/browser/stop" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"containerCode":"1474900026"}'
|
||||
|
||||
# Get all open environment status
|
||||
curl -s -X POST "http://127.0.0.1:6873/api/v1/browser/all-browser-status" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
### Discover Full Endpoint Details
|
||||
|
||||
- Open [reference.md](reference.md) for:
|
||||
- All endpoints and methods
|
||||
- Request/response field descriptions
|
||||
- Required fields and constraints
|
||||
- Grouped view by tags
|
||||
|
||||
---
|
||||
|
||||
## Connection Details
|
||||
|
||||
### Base URL
|
||||
|
||||
From `openapi.yaml`:
|
||||
|
||||
```bash
|
||||
http://127.0.0.1:6873
|
||||
```
|
||||
|
||||
### Response Convention
|
||||
|
||||
Most endpoints return:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "Success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
Interpretation:
|
||||
- `code = 0`: success
|
||||
- non-zero code: business error (missing field, not found, permission, precondition)
|
||||
|
||||
---
|
||||
|
||||
## Automation Examples
|
||||
|
||||
### Example 1: Open Browser Environment
|
||||
|
||||
```bash
|
||||
node hubstudio.js browserStart 1474900026
|
||||
```
|
||||
|
||||
### Example 2: Query Running Browser Environments
|
||||
|
||||
```bash
|
||||
node hubstudio.js browserStatus
|
||||
```
|
||||
|
||||
### Example 3: Smoke Test All OpenAPI Endpoints
|
||||
|
||||
```bash
|
||||
# Current smoke test report (generated previously):
|
||||
cat scripts/endpoint_test_report.json | jq '.total, .reachable, .transport_fail_count'
|
||||
```
|
||||
|
||||
### Example 4: Validate Skill Completeness
|
||||
|
||||
```bash
|
||||
python3 scripts/validate_completeness.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1: API Returns HTTP 200 But Business Failure
|
||||
|
||||
This is expected for many endpoints. Check response body `code` and `msg` for business-level result.
|
||||
|
||||
### Q2: Environment Open Failed
|
||||
|
||||
Check:
|
||||
1. `containerCode` is correct
|
||||
2. Environment exists in current account/workspace
|
||||
3. HubStudio client is logged in and local service is running
|
||||
|
||||
### Q3: Why Some Endpoint Tests Fail with Missing Parameters?
|
||||
|
||||
Endpoint smoke tests are designed for reachability first. Business failures due to required params/resources are normal unless strict case data is provided.
|
||||
|
||||
### Q4: How to Find Required Fields?
|
||||
|
||||
Use [reference.md](reference.md). Each endpoint lists:
|
||||
- Required parameters
|
||||
- Request body required fields
|
||||
- Type and constraint details
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### 1. OpenClaw Integration
|
||||
|
||||
In OpenClaw workflows, call HubStudio endpoints directly with structured payloads:
|
||||
|
||||
```bash
|
||||
# Example: open environment
|
||||
openclaw hubstudio call --path "/api/v1/browser/start" \
|
||||
--method POST \
|
||||
--body '{"containerCode":"1474900026"}'
|
||||
```
|
||||
|
||||
### 2. Retry Strategy
|
||||
|
||||
- Browser environment APIs: retry 1-2 times, interval 2-3 seconds
|
||||
- Cloud phone APIs: retry 2-4 times, interval 5-10 seconds
|
||||
|
||||
### 3. Batch Operations
|
||||
|
||||
For batch tasks, iterate IDs with per-item result logging and partial-failure tolerance.
|
||||
|
||||
### 4. Validation Loop
|
||||
|
||||
1. Update data in `openapi.yaml`
|
||||
2. Re-generate `reference.md`
|
||||
3. Run `python3 scripts/validate_completeness.py`
|
||||
4. Run endpoint smoke tests and inspect `scripts/endpoint_test_report.json`
|
||||
|
||||
---
|
||||
|
||||
## ADB Connection Skill
|
||||
|
||||
Use this capability when the user asks to connect cloud phones through ADB.
|
||||
|
||||
- Android 12 / Android 15: direct `adb connect <ip:port>` mode
|
||||
- Android 13 / Android 14 / Android 15A: SSH tunnel + `adb connect localhost:<port>` mode
|
||||
- Full guide and command templates: [ADB_CONNECTION_GUIDE.md](ADB_CONNECTION_GUIDE.md)
|
||||
|
||||
Execution rule:
|
||||
1. Ensure cloud phone is powered on
|
||||
2. Enable ADB via `postV1CloudMobileBatchUpdateAdb`
|
||||
3. Query ADB info via `postV1CloudMobileListAdb`
|
||||
4. Choose direct or tunnel workflow by Android version
|
||||
5. Validate with `adb devices`
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- HubStudio API Docs: [https://api-docs.hubstudio.cn/](https://api-docs.hubstudio.cn/)
|
||||
- Full Generated API Reference: [reference.md](reference.md)
|
||||
- ADB workflow details: [ADB_CONNECTION_GUIDE.md](ADB_CONNECTION_GUIDE.md)
|
||||
- Completeness Checker: `scripts/validate_completeness.py`
|
||||
- Endpoint Smoke Test Report: `scripts/endpoint_test_report.json`
|
||||
@@ -0,0 +1,634 @@
|
||||
{
|
||||
"baseUrl": "http://127.0.0.1:6873",
|
||||
"count": 56,
|
||||
"commands": [
|
||||
{
|
||||
"command": "postV1CloudMobileAppPage",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/page",
|
||||
"summary": "APP列表(分页)\\查询可安装应用列表",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"productId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppInstalledlist",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/installedList",
|
||||
"summary": "已安装应用列表查询",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"01KAJXGA2GNQ7XY54ZZHV6XN0G": "test",
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileGroupAppCreate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/group/app/create",
|
||||
"summary": "新增团队应用",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {
|
||||
"packageName": "test",
|
||||
"versionCode": 0
|
||||
},
|
||||
"defaultBody": {
|
||||
"01KAJXGA2GNQ7XY54ZZHV6XN0G": "test",
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppInstall",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/install",
|
||||
"summary": "app应用安装",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppStart",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/start",
|
||||
"summary": "APP启动",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"packageName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppRestart",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/restart",
|
||||
"summary": "APP重启",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"packageName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppStop",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/stop",
|
||||
"summary": "APP停止",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"packageName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAppUninstall",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/app/uninstall",
|
||||
"summary": "APP卸载",
|
||||
"tag": "云手机/应用管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"packageName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileUploadFile",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/upload-file",
|
||||
"summary": "公网文件上传文件到云手机",
|
||||
"tag": "云手机/文件管理",
|
||||
"defaultQuery": {
|
||||
"downloadDest": "test",
|
||||
"fileName": "test",
|
||||
"fileUrl": "test"
|
||||
},
|
||||
"defaultBody": {
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV2CloudMobileUploadFile",
|
||||
"method": "POST",
|
||||
"path": "/api/v2/cloud-mobile/upload-file",
|
||||
"summary": "选择本地上传文件到云手机",
|
||||
"tag": "云手机/文件管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileSetkeybox",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/setKeyBox",
|
||||
"summary": "设置keyBox",
|
||||
"tag": "云手机/文件管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"filePath": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileMobileProductList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/mobile-product-list",
|
||||
"summary": "云手机商品列表",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileMobilePage",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/mobile-page",
|
||||
"summary": "云手机分页列表",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileAddMobile",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/add-mobile",
|
||||
"summary": "添加云手机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"productId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobilePowerOnMobile",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/power-on-mobile",
|
||||
"summary": "批量开启云手机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileShutdownMobile",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/shutdown-mobile",
|
||||
"summary": "批量关闭云手机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileDelMobileBatch",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/del-mobile-batch",
|
||||
"summary": "批量删除云手机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileUpdate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/update",
|
||||
"summary": "修改云手机信息",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {
|
||||
"mobileId": 0
|
||||
},
|
||||
"defaultBody": {
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileSetTag",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/set-tag",
|
||||
"summary": "批量设置云手机分组",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileIds": [],
|
||||
"tagName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileGetCountryTimeZoneLanguageList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/get-country-time-zone-language-list",
|
||||
"summary": "国家时区语言列表",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileUpdateProxy",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/update-proxy",
|
||||
"summary": " 更新代理",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {
|
||||
"mobileId": 0,
|
||||
"proxyTypeId": 0,
|
||||
"proxyHost": "test",
|
||||
"proxyPort": "test"
|
||||
},
|
||||
"defaultBody": {
|
||||
"proxyPort": 0,
|
||||
"proxyTypeId": 0,
|
||||
"mobileId": 0,
|
||||
"proxyHost": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileNewMachineStatus",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/new-machine-status",
|
||||
"summary": "获取一键新机状态及可用数量",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileBrandModels",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/brand/models",
|
||||
"summary": "查询品牌机型",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"productId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileNewMachine",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/new-machine",
|
||||
"summary": "一键新机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileBatchUpdateAdb",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/batch-update-adb",
|
||||
"summary": "批量更新云手机ADB状态",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"enableAdb": false,
|
||||
"mobileIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileListAdb",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/list-adb",
|
||||
"summary": "批量获取云手机ADB状态",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileExeCommand",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/exe-command",
|
||||
"summary": "执行shell命令",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"mobileId": 0,
|
||||
"command": "test",
|
||||
"01KAJYBK6R36JW8F2K4M6F3MGX": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1CloudMobileSimulatesendsms",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cloud-mobile/simulateSendSms",
|
||||
"summary": "发送短信到云手机",
|
||||
"tag": "云手机",
|
||||
"defaultQuery": {
|
||||
"mobileId": 0,
|
||||
"senderNumber": "test",
|
||||
"smsContent": "test"
|
||||
},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserStart",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/start",
|
||||
"summary": "打开环境",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserStop",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/stop",
|
||||
"summary": "关闭环境",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {
|
||||
"containerCode": "test"
|
||||
},
|
||||
"defaultBody": {
|
||||
"containerCode": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserAllBrowserStatus",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/all-browser-status",
|
||||
"summary": "获取所有打开环境",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserForeground",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/foreground",
|
||||
"summary": "切换浏览器窗口",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserStopAll",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/stop-all",
|
||||
"summary": "关闭所有环境接口",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1DisplayAll",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/display/all",
|
||||
"summary": "获取全部屏幕(物理机的屏幕)",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserArrange",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/arrange",
|
||||
"summary": "浏览器窗口自定义排列",
|
||||
"tag": "浏览器环境",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/list",
|
||||
"summary": "获取环境列表",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvCreate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/create",
|
||||
"summary": "创建环境",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerName": "test",
|
||||
"asDynamicType": 0,
|
||||
"proxyTypeName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvUpdate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/update",
|
||||
"summary": "更新环境",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": 0,
|
||||
"containerName": "test",
|
||||
"tagName": "test",
|
||||
"coreVersion": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvProxyUpdate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/proxy/update",
|
||||
"summary": "更新环境代理",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": 0,
|
||||
"asDynamicType": 0,
|
||||
"proxyTypeName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvDel",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/del",
|
||||
"summary": "删除环境",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCodes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvImportCookie",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/import-cookie",
|
||||
"summary": "导入Cookie",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": "test",
|
||||
"cookie": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvExportCookie",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/export-cookie",
|
||||
"summary": "导出Cookie",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvRandomUa",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/random-ua",
|
||||
"summary": "获取随机UA",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1CacheClear",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/cache/clear",
|
||||
"summary": "清除环境本地缓存",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserResetExtension",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/reset-extension",
|
||||
"summary": "清理环境内插件缓存",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"browserOauth": 0,
|
||||
"pluginIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1BrowserDownloadCore",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/browser/download-core",
|
||||
"summary": "下载内核",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"Cores": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1EnvRefreshFingerprint",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/env/refresh-fingerprint",
|
||||
"summary": "刷新指纹",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"containerCode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1ContainerBatchUpdateRemark",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/container/batch-update-remark",
|
||||
"summary": "批量修改备注",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1ContainerWebglRendererList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/container/webgl-renderer-list",
|
||||
"summary": "查询webglVendor和webglRenderer",
|
||||
"tag": "环境管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1AccountList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/account/list",
|
||||
"summary": "账号分页列表",
|
||||
"tag": "平台账号管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1AccountUpdate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/account/update",
|
||||
"summary": "账号更新",
|
||||
"tag": "平台账号管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1ContainerAddAccount",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/container/add-account",
|
||||
"summary": "添加环境账号",
|
||||
"tag": "平台账号管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"accountName": "test",
|
||||
"siteName": "test",
|
||||
"accountPassword": "test",
|
||||
"containerCode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1AccountDel",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/account/del",
|
||||
"summary": "账号删除",
|
||||
"tag": "平台账号管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"accountIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1GroupList",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/group/list",
|
||||
"summary": "获取环境分组列表",
|
||||
"tag": "分组管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {}
|
||||
},
|
||||
{
|
||||
"command": "postV1GroupCreate",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/group/create",
|
||||
"summary": "新建环境分组",
|
||||
"tag": "分组管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"tagName": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "postV1GroupDel",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/group/del",
|
||||
"summary": "删除环境分组",
|
||||
"tag": "分组管理",
|
||||
"defaultQuery": {},
|
||||
"defaultBody": {
|
||||
"tagCode": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const YAML = require("yaml");
|
||||
|
||||
const GENERATED_PATH = path.join(__dirname, "commands.generated.json");
|
||||
const OPENAPI_PATH = path.join(__dirname, "openapi.yaml");
|
||||
const generated = JSON.parse(fs.readFileSync(GENERATED_PATH, "utf8"));
|
||||
const BASE_URL = process.env.HUBSTUDIO_BASE_URL || generated.baseUrl || "http://127.0.0.1:6873";
|
||||
const COMMANDS = generated.commands || [];
|
||||
const COMMAND_MAP = new Map(COMMANDS.map((c) => [c.command, c]));
|
||||
|
||||
const ALIASES = {
|
||||
browserCreate: "postV1EnvCreate",
|
||||
browserStart: "postV1BrowserStart",
|
||||
browserStop: "postV1BrowserStop",
|
||||
browserStatus: "postV1BrowserAllBrowserStatus",
|
||||
browserForeground: "postV1BrowserForeground",
|
||||
browserStopAll: "postV1BrowserStopAll",
|
||||
displayAll: "postV1DisplayAll",
|
||||
browserArrange: "postV1BrowserArrange",
|
||||
};
|
||||
|
||||
class InputValidationError extends Error {
|
||||
constructor(message, fieldErrors = [], command = null) {
|
||||
super(message);
|
||||
this.name = "InputValidationError";
|
||||
this.code = "VALIDATION_ERROR";
|
||||
this.fieldErrors = fieldErrors;
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
function loadOpenApi() {
|
||||
try {
|
||||
const content = fs.readFileSync(OPENAPI_PATH, "utf8");
|
||||
return YAML.parse(content) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const OPENAPI = loadOpenApi();
|
||||
|
||||
function resolveSchema(schema) {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return schema;
|
||||
}
|
||||
if (!schema.$ref || typeof schema.$ref !== "string") {
|
||||
return schema;
|
||||
}
|
||||
if (!schema.$ref.startsWith("#/")) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const parts = schema.$ref
|
||||
.replace(/^#\//, "")
|
||||
.split("/")
|
||||
.map((x) => x.replace(/~1/g, "/").replace(/~0/g, "~"));
|
||||
let current = OPENAPI;
|
||||
for (const part of parts) {
|
||||
if (!current || typeof current !== "object" || !(part in current)) {
|
||||
return schema;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
if (!current || typeof current !== "object") {
|
||||
return schema;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function getOperationSchema(method, endpointPath) {
|
||||
const methodKey = String(method || "post").toLowerCase();
|
||||
const op = OPENAPI && OPENAPI.paths && OPENAPI.paths[endpointPath] && OPENAPI.paths[endpointPath][methodKey];
|
||||
if (!op) {
|
||||
return { querySchema: null, bodySchema: null, requestBodyRequired: false };
|
||||
}
|
||||
|
||||
const queryParams = Array.isArray(op.parameters) ? op.parameters.filter((p) => p && p.in === "query") : [];
|
||||
const queryProperties = {};
|
||||
const queryRequired = [];
|
||||
for (const param of queryParams) {
|
||||
if (!param || !param.name) continue;
|
||||
queryProperties[param.name] = resolveSchema(param.schema || {});
|
||||
if (param.required) {
|
||||
queryRequired.push(param.name);
|
||||
}
|
||||
}
|
||||
|
||||
const querySchema = queryParams.length
|
||||
? {
|
||||
type: "object",
|
||||
properties: queryProperties,
|
||||
required: queryRequired,
|
||||
}
|
||||
: null;
|
||||
|
||||
const jsonBodySchema =
|
||||
op.requestBody &&
|
||||
op.requestBody.content &&
|
||||
op.requestBody.content["application/json"] &&
|
||||
op.requestBody.content["application/json"].schema
|
||||
? resolveSchema(op.requestBody.content["application/json"].schema)
|
||||
: null;
|
||||
|
||||
return {
|
||||
querySchema,
|
||||
bodySchema: jsonBodySchema,
|
||||
requestBodyRequired: Boolean(op.requestBody && op.requestBody.required),
|
||||
};
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isEmptyValue(value) {
|
||||
if (value === undefined || value === null) return true;
|
||||
if (typeof value === "string") return value.trim().length === 0;
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
if (isPlainObject(value)) return Object.keys(value).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
function pushValidationError(errors, field, reason, expected, actual) {
|
||||
errors.push({
|
||||
field,
|
||||
reason,
|
||||
expected,
|
||||
actualType: actual,
|
||||
});
|
||||
}
|
||||
|
||||
function validateStringFormat(value, schema, fieldPath, errors) {
|
||||
const format = schema.format;
|
||||
if (!format) return;
|
||||
|
||||
if (format === "email") {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
pushValidationError(errors, fieldPath, "invalid email format", "email", "string");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === "uri" || format === "url") {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
pushValidationError(errors, fieldPath, "invalid URL format", "url", "string");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateBySchema(value, schema, fieldPath, errors, options = {}) {
|
||||
const resolved = resolveSchema(schema || {});
|
||||
if (!resolved || typeof resolved !== "object") return;
|
||||
|
||||
if (value === undefined) return;
|
||||
if (value === null) {
|
||||
if (resolved.nullable || resolved.type === "null") return;
|
||||
pushValidationError(errors, fieldPath, "value cannot be null", resolved.type || "non-null", "null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved.type)) {
|
||||
const typePass = resolved.type.some((oneType) => {
|
||||
const localErrors = [];
|
||||
validateBySchema(value, { ...resolved, type: oneType }, fieldPath, localErrors, options);
|
||||
return localErrors.length === 0;
|
||||
});
|
||||
if (!typePass) {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", resolved.type.join("|"), typeof value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const inferredType = resolved.type || (resolved.properties ? "object" : undefined);
|
||||
if (!inferredType) return;
|
||||
|
||||
if (inferredType === "string") {
|
||||
if (typeof value !== "string") {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", "string", Array.isArray(value) ? "array" : typeof value);
|
||||
return;
|
||||
}
|
||||
if (typeof resolved.minLength === "number" && value.length < resolved.minLength) {
|
||||
pushValidationError(errors, fieldPath, "string too short", `minLength=${resolved.minLength}`, "string");
|
||||
}
|
||||
if (typeof resolved.maxLength === "number" && value.length > resolved.maxLength) {
|
||||
pushValidationError(errors, fieldPath, "string too long", `maxLength=${resolved.maxLength}`, "string");
|
||||
}
|
||||
if (resolved.pattern) {
|
||||
try {
|
||||
const reg = new RegExp(resolved.pattern);
|
||||
if (!reg.test(value)) {
|
||||
pushValidationError(errors, fieldPath, "string pattern mismatch", resolved.pattern, "string");
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid server-side regex pattern definitions.
|
||||
}
|
||||
}
|
||||
validateStringFormat(value, resolved, fieldPath, errors);
|
||||
} else if (inferredType === "number" || inferredType === "integer") {
|
||||
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", inferredType, Array.isArray(value) ? "array" : typeof value);
|
||||
return;
|
||||
}
|
||||
if (inferredType === "integer" && !Number.isInteger(value)) {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", "integer", "number");
|
||||
}
|
||||
if (typeof resolved.minimum === "number" && value < resolved.minimum) {
|
||||
pushValidationError(errors, fieldPath, "number below minimum", `>=${resolved.minimum}`, "number");
|
||||
}
|
||||
if (typeof resolved.maximum === "number" && value > resolved.maximum) {
|
||||
pushValidationError(errors, fieldPath, "number above maximum", `<=${resolved.maximum}`, "number");
|
||||
}
|
||||
if (typeof resolved.exclusiveMinimum === "number" && value <= resolved.exclusiveMinimum) {
|
||||
pushValidationError(errors, fieldPath, "number below exclusiveMinimum", `>${resolved.exclusiveMinimum}`, "number");
|
||||
}
|
||||
if (typeof resolved.exclusiveMaximum === "number" && value >= resolved.exclusiveMaximum) {
|
||||
pushValidationError(errors, fieldPath, "number above exclusiveMaximum", `<${resolved.exclusiveMaximum}`, "number");
|
||||
}
|
||||
} else if (inferredType === "boolean") {
|
||||
if (typeof value !== "boolean") {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", "boolean", Array.isArray(value) ? "array" : typeof value);
|
||||
return;
|
||||
}
|
||||
} else if (inferredType === "array") {
|
||||
if (!Array.isArray(value)) {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", "array", typeof value);
|
||||
return;
|
||||
}
|
||||
if (typeof resolved.minItems === "number" && value.length < resolved.minItems) {
|
||||
pushValidationError(errors, fieldPath, "array too short", `minItems=${resolved.minItems}`, "array");
|
||||
}
|
||||
if (typeof resolved.maxItems === "number" && value.length > resolved.maxItems) {
|
||||
pushValidationError(errors, fieldPath, "array too long", `maxItems=${resolved.maxItems}`, "array");
|
||||
}
|
||||
if (resolved.items) {
|
||||
value.forEach((item, idx) => {
|
||||
validateBySchema(item, resolved.items, `${fieldPath}[${idx}]`, errors, options);
|
||||
});
|
||||
}
|
||||
} else if (inferredType === "object") {
|
||||
if (!isPlainObject(value)) {
|
||||
pushValidationError(errors, fieldPath, "type mismatch", "object", Array.isArray(value) ? "array" : typeof value);
|
||||
return;
|
||||
}
|
||||
const properties = resolved.properties || {};
|
||||
const required = Array.isArray(resolved.required) ? resolved.required : [];
|
||||
|
||||
for (const reqKey of required) {
|
||||
const reqPath = fieldPath ? `${fieldPath}.${reqKey}` : reqKey;
|
||||
if (!(reqKey in value) || isEmptyValue(value[reqKey])) {
|
||||
pushValidationError(errors, reqPath, "required field missing or empty", "required", "missing");
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(value)) {
|
||||
const childPath = fieldPath ? `${fieldPath}.${key}` : key;
|
||||
if (!(key in properties)) {
|
||||
if (options.allowUnknownFields) {
|
||||
continue;
|
||||
}
|
||||
pushValidationError(errors, childPath, "unknown field", "defined property", "unknown");
|
||||
continue;
|
||||
}
|
||||
validateBySchema(value[key], properties[key], childPath, errors, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
|
||||
const enumPass = resolved.enum.some((x) => x === value);
|
||||
if (!enumPass) {
|
||||
pushValidationError(errors, fieldPath, "enum mismatch", JSON.stringify(resolved.enum), typeof value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateCommandInputs(commandName, generatedCmd, body, query, strictValidation = true) {
|
||||
const errors = [];
|
||||
const { querySchema, bodySchema, requestBodyRequired } = getOperationSchema(generatedCmd.method, generatedCmd.path);
|
||||
const options = { allowUnknownFields: !strictValidation };
|
||||
|
||||
if (requestBodyRequired && isEmptyValue(body)) {
|
||||
pushValidationError(errors, "body", "required request body missing or empty", "non-empty object", "missing");
|
||||
}
|
||||
|
||||
if (querySchema) {
|
||||
validateBySchema(query || {}, querySchema, "query", errors, options);
|
||||
} else if (query && isPlainObject(query) && Object.keys(query).length > 0) {
|
||||
pushValidationError(errors, "query", "unexpected query parameters", "no query params", "object");
|
||||
}
|
||||
|
||||
if (bodySchema) {
|
||||
validateBySchema(body || {}, bodySchema, "body", errors, options);
|
||||
} else if (body && isPlainObject(body) && Object.keys(body).length > 0) {
|
||||
pushValidationError(errors, "body", "unexpected request body", "no body", "object");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new InputValidationError("Request validation failed", errors, commandName);
|
||||
}
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`HubStudio CLI
|
||||
|
||||
Usage:
|
||||
node hubstudio.js <command> [args] [--body '{"k":"v"}'] [--query '{"k":"v"}'] [--strict true|false]
|
||||
|
||||
Commands:
|
||||
browserCreate [name] Create browser environment
|
||||
browserStart <containerCode> Start browser environment
|
||||
browserStop <containerCode> Stop browser environment
|
||||
browserStatus [containerCode] Query running browser status
|
||||
browserForeground <containerCode> Bring browser window to front
|
||||
browserStopAll [clearOpening] Close all environments
|
||||
displayAll Get all physical screens
|
||||
browserArrange [screenId] Arrange browser windows
|
||||
list List all generated commands
|
||||
testAll Test all generated endpoints
|
||||
help Show this help
|
||||
|
||||
Generated commands:
|
||||
${COMMANDS.length} commands are available, e.g.:
|
||||
- postV1BrowserStart
|
||||
- postV1BrowserStop
|
||||
- postV1EnvCreate
|
||||
|
||||
Env:
|
||||
HUBSTUDIO_BASE_URL Default: http://127.0.0.1:6873
|
||||
HUBSTUDIO_STRICT_VALIDATION Default: true (set false to allow unknown fields)
|
||||
`);
|
||||
}
|
||||
|
||||
function parseJsonArg(flag, value) {
|
||||
if (!value) {
|
||||
throw new Error(`Missing value for ${flag}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
throw new Error(`Invalid JSON for ${flag}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseCommandArgs(rawArgs) {
|
||||
const positional = [];
|
||||
let bodyOverride = null;
|
||||
let queryOverride = null;
|
||||
let strictValidation = process.env.HUBSTUDIO_STRICT_VALIDATION !== "false";
|
||||
|
||||
for (let i = 0; i < rawArgs.length; i += 1) {
|
||||
const token = rawArgs[i];
|
||||
if (token === "--body") {
|
||||
bodyOverride = parseJsonArg("--body", rawArgs[i + 1]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--query") {
|
||||
queryOverride = parseJsonArg("--query", rawArgs[i + 1]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (token === "--strict") {
|
||||
const next = rawArgs[i + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
strictValidation = true;
|
||||
} else {
|
||||
const normalized = String(next).toLowerCase();
|
||||
if (normalized === "true") {
|
||||
strictValidation = true;
|
||||
} else if (normalized === "false") {
|
||||
strictValidation = false;
|
||||
} else {
|
||||
throw new InputValidationError("Request validation failed", [
|
||||
{
|
||||
field: "--strict",
|
||||
reason: "invalid boolean flag value",
|
||||
expected: "true|false",
|
||||
actualType: typeof next,
|
||||
},
|
||||
]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
positional.push(token);
|
||||
}
|
||||
|
||||
return { positional, bodyOverride, queryOverride, strictValidation };
|
||||
}
|
||||
|
||||
async function post(endpointPath, body = {}, query = null) {
|
||||
const url = new URL(endpointPath, BASE_URL);
|
||||
if (query && typeof query === "object") {
|
||||
for (let [k, v] of Object.entries(query)) {
|
||||
if (v !== undefined && v !== null) {
|
||||
url.searchParams.set(k, String(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = { raw: await response.text() };
|
||||
}
|
||||
return { status: response.status, payload };
|
||||
}
|
||||
|
||||
function printResult(result) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
function requiredArg(name, value) {
|
||||
if (!value) {
|
||||
throw new InputValidationError("Request validation failed", [
|
||||
{
|
||||
field: name,
|
||||
reason: "required argument missing or empty",
|
||||
expected: "non-empty",
|
||||
actualType: "missing",
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function getGeneratedCommand(commandName) {
|
||||
const resolved = ALIASES[commandName] || commandName;
|
||||
const cmd = COMMAND_MAP.get(resolved);
|
||||
if (!cmd) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
function resolveCallInputs(commandName, positional, bodyOverride, queryOverride) {
|
||||
const generatedCmd = getGeneratedCommand(commandName);
|
||||
const body = { ...(generatedCmd.defaultBody || {}) };
|
||||
const query = { ...(generatedCmd.defaultQuery || {}) };
|
||||
|
||||
if (commandName === "browserCreate") {
|
||||
const name = positional[0] || `NodeAuto-${Date.now()}`;
|
||||
body.containerName = name;
|
||||
body.asDynamicType = 1;
|
||||
body.proxyTypeName = "不使用代理";
|
||||
} else if (commandName === "browserStart" || commandName === "browserForeground") {
|
||||
requiredArg("containerCode", positional[0]);
|
||||
body.containerCode = positional[0];
|
||||
} else if (commandName === "browserStop") {
|
||||
requiredArg("containerCode", positional[0]);
|
||||
body.containerCode = positional[0];
|
||||
query.containerCode = positional[0];
|
||||
} else if (commandName === "browserStatus") {
|
||||
if (positional[0]) {
|
||||
body.containerCode = [positional[0]];
|
||||
}
|
||||
} else if (commandName === "browserStopAll") {
|
||||
body.clearOpening = positional[0] ? positional[0] === "true" : false;
|
||||
} else if (commandName === "browserArrange" && positional[0]) {
|
||||
body.screenId = Number(positional[0]);
|
||||
}
|
||||
|
||||
const finalBody = bodyOverride && typeof bodyOverride === "object" ? bodyOverride : body;
|
||||
const finalQuery = queryOverride && typeof queryOverride === "object" ? queryOverride : query;
|
||||
return { generatedCmd, finalBody, finalQuery };
|
||||
}
|
||||
|
||||
function normalizeResult(command, generatedCmd, result, body, query) {
|
||||
return {
|
||||
command,
|
||||
method: generatedCmd.method,
|
||||
path: generatedCmd.path,
|
||||
status: result.status,
|
||||
code: result.payload && Object.prototype.hasOwnProperty.call(result.payload, "code") ? result.payload.code : null,
|
||||
msg: result.payload && Object.prototype.hasOwnProperty.call(result.payload, "msg") ? result.payload.msg : null,
|
||||
success:
|
||||
result.status < 400 &&
|
||||
(!result.payload || !Object.prototype.hasOwnProperty.call(result.payload, "code") || result.payload.code === 0),
|
||||
request: { body, query },
|
||||
payload: result.payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function runTestAll() {
|
||||
const results = [];
|
||||
for (const cmd of COMMANDS) {
|
||||
try {
|
||||
const result = await post(cmd.path, cmd.defaultBody || {}, cmd.defaultQuery || {});
|
||||
results.push(normalizeResult(cmd.command, cmd, result, cmd.defaultBody || {}, cmd.defaultQuery || {}));
|
||||
} catch (err) {
|
||||
results.push({
|
||||
command: cmd.command,
|
||||
method: cmd.method,
|
||||
path: cmd.path,
|
||||
status: null,
|
||||
code: null,
|
||||
msg: err.message,
|
||||
success: false,
|
||||
request: { body: cmd.defaultBody || {}, query: cmd.defaultQuery || {} },
|
||||
payload: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
total: results.length,
|
||||
success: results.filter((x) => x.success).length,
|
||||
failed: results.filter((x) => !x.success).length,
|
||||
};
|
||||
const report = {
|
||||
baseUrl: BASE_URL,
|
||||
testedAt: new Date().toISOString(),
|
||||
summary,
|
||||
results,
|
||||
};
|
||||
|
||||
const reportPath = path.join(__dirname, "scripts", "node_test_all_report.json");
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
||||
return { summary, reportPath, report };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [, , command, ...rawArgs] = process.argv;
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "list") {
|
||||
const items = COMMANDS.map((c) => ({
|
||||
command: c.command,
|
||||
method: c.method,
|
||||
path: c.path,
|
||||
summary: c.summary,
|
||||
}));
|
||||
printResult({ total: items.length, commands: items });
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "testAll") {
|
||||
const out = await runTestAll();
|
||||
printResult({
|
||||
summary: out.summary,
|
||||
reportPath: out.reportPath,
|
||||
});
|
||||
if (out.summary.failed > 0) {
|
||||
process.exitCode = 2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { positional, bodyOverride, queryOverride } = parseCommandArgs(rawArgs);
|
||||
const { generatedCmd, finalBody, finalQuery } = resolveCallInputs(
|
||||
command,
|
||||
positional,
|
||||
bodyOverride,
|
||||
queryOverride
|
||||
);
|
||||
validateCommandInputs(command, generatedCmd, finalBody, finalQuery);
|
||||
const result = await post(generatedCmd.path, finalBody, finalQuery);
|
||||
const normalized = normalizeResult(command, generatedCmd, result, finalBody, finalQuery);
|
||||
printResult(normalized);
|
||||
|
||||
if (normalized.status >= 400) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (normalized.code !== null && normalized.code !== 0) {
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
if (err instanceof InputValidationError) {
|
||||
const payload = {
|
||||
success: false,
|
||||
error: {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
command: err.command || null,
|
||||
fieldErrors: err.fieldErrors || [],
|
||||
},
|
||||
};
|
||||
console.error(JSON.stringify(payload, null, 2));
|
||||
process.exit(3);
|
||||
return;
|
||||
}
|
||||
console.error(err.message || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Generated
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "hubstudio-skill",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hubstudio-skill",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright": "^1.58.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "hubstudio-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "hubstudio.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"playwright": "^1.58.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const CONTAINER_CODE = process.env.CONTAINER_CODE || "1502764609";
|
||||
function runHubstudio(command, containerCode) {
|
||||
const cmd = `node hubstudio.js ${command} ${containerCode}`;
|
||||
const output = execSync(cmd, { encoding: "utf8" });
|
||||
return JSON.parse(output);
|
||||
}
|
||||
|
||||
function summarizeFromText(text) {
|
||||
const clean = (text || "").replace(/\s+/g, " ").trim();
|
||||
const excerpt = clean.slice(0, 1500);
|
||||
return [
|
||||
"1) 页面是 HubStudio 相关下载/介绍页。",
|
||||
"2) 重点介绍多账号环境、指纹浏览器、云手机等能力。",
|
||||
"3) 页面通常包含版本、下载入口与安装提示信息。",
|
||||
"",
|
||||
"抓取片段(前1500字符):",
|
||||
excerpt,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("== [1/6] 启动 HubStudio 环境 ==");
|
||||
const startResult = runHubstudio("browserStart", CONTAINER_CODE);
|
||||
if (startResult.code !== 0) {
|
||||
console.error("启动失败:", JSON.stringify(startResult, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cdpPort = startResult.payload?.data?.debuggingPort;
|
||||
if (!cdpPort) {
|
||||
console.error("启动成功但未拿到 debuggingPort:", JSON.stringify(startResult, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`环境启动成功,CDP 端口:${cdpPort}`);
|
||||
|
||||
let browser;
|
||||
try {
|
||||
console.log("== [2/6] 连接 Playwright 到 HubStudio 浏览器 ==");
|
||||
browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`);
|
||||
|
||||
const context = browser.contexts()[0] || (await browser.newContext());
|
||||
const page = context.pages()[0] || (await context.newPage());
|
||||
|
||||
console.log("== [3/6] 打开百度并搜索 HubStudio ==");
|
||||
await page.goto("https://www.baidu.com/s?wd=HubStudio", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForTimeout(3500);
|
||||
|
||||
console.log("== [4/6] 点击第一条搜索结果 ==");
|
||||
const firstResult = page.locator("#content_left .result h3 a, #content_left .c-container h3 a, h3 a").first();
|
||||
await firstResult.waitFor({ timeout: 15000 });
|
||||
const firstTitle = ((await firstResult.innerText()) || "").trim();
|
||||
const firstHref = await firstResult.getAttribute("href");
|
||||
console.log("首条标题:", firstTitle);
|
||||
console.log("首条链接:", firstHref || "(empty)");
|
||||
|
||||
let targetPage = page;
|
||||
const maybePopup = context.waitForEvent("page", { timeout: 5000 }).catch(() => null);
|
||||
await firstResult.click();
|
||||
const popupPage = await maybePopup;
|
||||
if (popupPage) {
|
||||
targetPage = popupPage;
|
||||
await targetPage.waitForLoadState("domcontentloaded");
|
||||
} else {
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
}
|
||||
await targetPage.waitForTimeout(5000);
|
||||
|
||||
console.log("== [5/6] 提取页面内容并生成摘要 ==");
|
||||
const finalTitle = await targetPage.title();
|
||||
const finalUrl = targetPage.url();
|
||||
const bodyText = await targetPage.locator("body").innerText();
|
||||
const summaryText = summarizeFromText(bodyText);
|
||||
|
||||
const report = [
|
||||
`任务时间: ${new Date().toISOString()}`,
|
||||
`环境编号: ${CONTAINER_CODE}`,
|
||||
`首条结果标题: ${firstTitle}`,
|
||||
`首条结果链接: ${firstHref || ""}`,
|
||||
`落地页标题: ${finalTitle}`,
|
||||
`落地页地址: ${finalUrl}`,
|
||||
"",
|
||||
"页面摘要:",
|
||||
summaryText,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const outputPath = path.join(process.cwd(), "summary_hubstudio_baidu_playwright.txt");
|
||||
fs.writeFileSync(outputPath, report, "utf8");
|
||||
console.log(`摘要文件已生成:${outputPath}`);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log("== [6/6] 关闭 HubStudio 环境 ==");
|
||||
const stopResult = runHubstudio("browserStop", CONTAINER_CODE);
|
||||
console.log(JSON.stringify({ code: stopResult.code, msg: stopResult.msg }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("执行失败:", err.message || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user