archive: 紫鸟/HubStudio 文档快照(3889 篇 Markdown)

来源:紫鸟帮助中心/博客/主站资讯 + HubStudio API 文档/帮助中心 + 三个官方 skill 仓库。
只入库 Markdown 与元数据;原始 HTML 与 tarball 留在本机。
This commit is contained in:
admin9webs committed 2026-10-05 12:11:35 +00:00
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
```
+340
View File
@@ -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`
+312
View File
@@ -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"
}
}
]
}
+604
View File
@@ -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
+60
View File
@@ -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"
}
}
}
}
+16
View File
@@ -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