本地 API
nullprint 客户端在本机开一个 HTTP 服务(默认 http://127.0.0.1:7801),脚本 / Playwright / 任何语言都能调。所有请求带 X-API-Key。
快速开始
- 安装 nullprint 客户端并登录。客户端开着,本地 API 就在。
- 打开客户端的「自动化」页,在「本地 API」卡片里复制地址和 Key(Key 形如
np_开头的一串)。 - 先用 curl 试一下,能列出你的身份就通了:
curl -H "X-API-Key: np_你的Key" http://127.0.0.1:7801/profiles
接口路径里的 {name} 是身份的内部编号,即 GET /profiles 返回的 name 字段(界面上显示的名称在 meta.label,可能重复)。
Python:启动一个身份,用 Playwright 接管
启动(POST /profiles/{name}/launch)后轮询 GET /profiles/{name},等 status 变成 running 且拿到 cdp_endpoint,交给 Playwright。需要 pip install requests playwright。
import time, requests
from playwright.sync_api import sync_playwright
BASE, KEY = "http://127.0.0.1:7801", "np_你的Key"
H = {"X-API-Key": KEY}
requests.post(f"{BASE}/profiles/shop-1/launch", json={"headless": False}, headers=H).raise_for_status()
for _ in range(60): # 等窗口起来、拿到 CDP 地址
p = requests.get(f"{BASE}/profiles/shop-1", headers=H).json()
if p["status"] == "running" and p.get("cdp_endpoint"):
break
time.sleep(1)
else:
raise RuntimeError("60 秒内没拿到 cdp_endpoint")
with sync_playwright() as pw:
browser = pw.chromium.connect_over_cdp(p["cdp_endpoint"])
page = browser.contexts[0].pages[0]
page.goto("https://example.com")
print(page.title())
requests.post(f"{BASE}/profiles/shop-1/stop", headers=H)
Node:同样的流程
Node 18+,npm i playwright,保存为 quickstart.mjs 运行。
import { chromium } from "playwright";
const BASE = "http://127.0.0.1:7801";
const H = { "X-API-Key": "np_你的Key", "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const r = await fetch(`${BASE}/profiles/shop-1/launch`, {
method: "POST", headers: H, body: JSON.stringify({ headless: false }),
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
let p;
for (let i = 0; i < 60; i++) { // 等窗口起来、拿到 CDP 地址
p = await (await fetch(`${BASE}/profiles/shop-1`, { headers: H })).json();
if (p.status === "running" && p.cdp_endpoint) break;
await sleep(1000);
}
if (!p.cdp_endpoint) throw new Error("60 秒内没拿到 cdp_endpoint");
const browser = await chromium.connectOverCDP(p.cdp_endpoint);
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");
console.log(await page.title());
await browser.close(); // 只断开连接,不关浏览器
await fetch(`${BASE}/profiles/shop-1/stop`, { method: "POST", headers: H });
不想自己装 Playwright 就用会话式接口
开一个会话,之后直接用 HTTP 指令操作浏览器(详见会话式操作)。开会话在浏览器就绪后才返回,返回里也带 cdp_endpoint。
curl -X POST http://127.0.0.1:7801/sessions/shop-1 -H "X-API-Key: np_你的Key" -H "Content-Type: application/json" -d '{"headless": false}'
curl -X POST http://127.0.0.1:7801/sessions/shop-1/goto -H "X-API-Key: np_你的Key" -H "Content-Type: application/json" -d '{"url": "https://example.com"}'
curl -H "X-API-Key: np_你的Key" -o shot.png http://127.0.0.1:7801/sessions/shop-1/screenshot
curl -X DELETE http://127.0.0.1:7801/sessions/shop-1 -H "X-API-Key: np_你的Key"
约定
地址与端口
Base URL 为 http://127.0.0.1:7801,只监听本机。7801 被别的程序占用时,客户端会顺延到 7802–7810 中第一个空闲端口。实际端口和当前 Key 写在本机文件 daemon.json 里:
- Windows:
%USERPROFILE%\.nullprint\daemon.json - macOS:
~/.nullprint/daemon.json
文件内容形如 {"port": 7801, "pid": 12345, "started": "…", "api_key": "np_…"}。长期运行的脚本建议每次启动时从这里读端口和 Key:
import json, pathlib
d = json.loads((pathlib.Path.home() / ".nullprint" / "daemon.json").read_text(encoding="utf-8-sig"))
BASE, KEY = f"http://127.0.0.1:{d['port']}", d["api_key"]
API Key
- 每个请求带请求头
X-API-Key: <Key>;不方便加请求头时也可以用查询参数?api_key=<Key>。 - 唯一例外:
GET /version不需要 Key,可用来探测服务是否在。 - Key 只保存在本机,不会上传。在自动化页点「重置 Key」(或调
POST /api-key/reset)后,老 Key 立即失效。 - 缺 Key 回
401 {"detail": {"reason": "api_key_required"}};Key 不对回401 {"detail": {"reason": "invalid_api_key"}}。
请求与响应
请求体和响应都是 JSON(Content-Type: application/json),截图接口除外(直接返回图片)。成功一般是 200;新建类接口回 201,启动类接口回 202(已受理、在后台进行),删除类接口回 204(无响应体)。
出错时响应体是 {"detail": …}。程序可判断的错误,detail 是对象,带机器可读的 reason(有时带更多字段):
{"detail": {"reason": "kernel_missing", "kernel": "chromium-148"}}
其余错误的 detail 是一段说明文字,例如 {"detail": "already running: shop-1"}。FastAPI 自带的字段校验错误(缺字段、类型不对)的 detail 是一个数组。
| 状态码 | 含义 |
|---|---|
| 401 | 没带 Key 或 Key 不对(api_key_required / invalid_api_key) |
| 403 | 没有权限或套餐限制:团队成员缺对应权限(forbidden,带 perm)、未登录或订阅过期(reason 为授权状态,如 unlicensed / plan_expired)、身份数超出套餐(quota_exceeded / over_limit) |
| 404 | 身份 / 代理不存在 |
| 409 | 状态冲突:身份正在运行、还没运行、内核没装、地理库下载中、会话没开等 |
| 422 | 参数错误:取值不合法,或 FastAPI 字段校验没过 |
| 429 | 同时开着的会话数已达上限 |
| 501 | 当前系统不支持(窗口平铺 / 同步只做了 Windows) |
| 502 | 会话里的浏览器操作失败(如找不到元素、超时),detail 为错误说明 |
| 503 | 需要联网但连不上(团队账户的写操作,needs_network) |
并发上限
POST /launch-many的max(默认 5)是同时开着的窗口数上限:超出的身份排队,前面有窗口关掉后才会启动下一个。想一次全开,把max设成身份个数。- 会话(
/sessions)同时最多 8 个,再开回 429。
身份
GET /profiles
列出全部身份,含运行状态。
curl -H "X-API-Key: np_你的Key" http://127.0.0.1:7801/profiles
[
{
"name": "shop-1",
"os": "windows",
"engine": "chromium",
"kernel": "148",
"created": "2026-10-01T10:20:30+08:00",
"proxy": "http://1.2.3.4:8000",
"status": "running",
"last_error": null,
"meta": {"label": "店铺 1", "group": "美区", "tags": ["amazon"], "note": "…", "last_launched": "…"}
}
]
| 字段 | 说明 |
|---|---|
name | 内部编号,其他接口路径里的 {name} |
status | running / idle(启动的窗口或会话任一开着即为 running) |
engine / kernel | 内核与 Chrome 大版本 |
proxy | 代理的 协议://主机:端口(不含账号密码);无代理为 null |
last_error | 最近一次启动失败的记录(at / code / error),没有为 null |
meta | 显示名称 label、分组、标签、备注、last_launched 等 |
其余字段略。团队成员看不到自己没有启动权限的身份。
GET /profiles/{name}
单个身份的详情、运行状态、调试地址与指纹档案。
{
"name": "shop-1",
"status": "running",
"cdp_endpoint": "ws://127.0.0.1:53123/devtools/browser/0f6c…",
"engine": "chromium",
"os": "windows",
"created": "2026-10-01T10:20:30+08:00",
"proxy": "http://1.2.3.4:8000",
"meta": {"label": "店铺 1", "launch": {"noise": "kernel"}, "…": "…"},
"geo": {"timezone": "America/New_York", "locale": "en-US", "latitude": 40.7, "longitude": -74.0, "ip": "1.2.3.4"},
"fingerprint": {"user_agent": null, "seed": 123456789, "…": "…"}
}
| 字段 | 说明 |
|---|---|
status | running / idle |
cdp_endpoint | 浏览器调试地址(ws://127.0.0.1:<端口>/devtools/browser/<id>),传给 Playwright 的 connect_over_cdp。身份没运行、或刚启动还没就绪时为 null——启动后轮询本接口直到它非空 |
proxy | 代理的 协议://主机:端口(不含账号密码) |
meta | 显示名称、分组、标签、备注、启动选项 launch 等 |
其余字段略(完整指纹档案)。错误:404 身份不存在。
POST /profiles
新建身份,返回 201。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 显示名称(可重复)。内部编号由它派生并保证唯一,以返回的 name 为准 |
proxy | string | 否 | 代理地址,如 http://user:pass@host:port、socks5://host:port、host:port:user:pass |
engine | string | 否 | 内核,默认 chromium |
kernel | string | 否 | Chrome 大版本,如 "148";不填用安装包默认 |
os | string | 否 | windows / macos / linux,默认与本机相同 |
note | string | 否 | 备注 |
tags | string[] | 否 | 标签 |
group | string | 否 | 分组名 |
urls | string[] | 否 | 启动时打开的网址 |
launch | object | 否 | 启动选项(与客户端「启动设置」对应) |
curl -X POST http://127.0.0.1:7801/profiles \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"name": "店铺 1", "proxy": "http://user:[email protected]:8000", "group": "美区"}'
{"name": "店铺 1", "label": "店铺 1", "os": "windows", "engine": "chromium", "created": "2026-10-02T09:00:00+08:00"}
错误:403 quota_exceeded(带 limit / used)、未登录或订阅过期(reason 为授权状态)、forbidden(无新建权限);422 名称为空、代理地址不合法、firefox_disabled;503 needs_network。
PATCH /profiles/{name}
改显示名称、分组、标签、备注、代理等。只传要改的字段;不传(或 null)的不动。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
label | string | 否 | 显示名称;"" 改回用内部编号显示 |
group | string | 否 | 分组名 |
tags | string[] | 否 | 标签(整体替换) |
note | string | 否 | 备注 |
proxy | string | 否 | 代理地址;"" 清除代理 |
urls | string[] | 否 | 启动时打开的网址(整体替换) |
launch | object | 否 | 启动选项(整体替换,改其中一项请先读出 meta.launch 再合并) |
curl -X PATCH http://127.0.0.1:7801/profiles/shop-1 \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"note": "绑定 [email protected]", "tags": ["amazon", "us"]}'
{"name": "shop-1", "proxy": "http://1.2.3.4:8000", "meta": {"note": "绑定 [email protected]", "tags": ["amazon", "us"], "…": "…"}}
错误:404;403 forbidden(无编辑权限);422 取值不合法;503 needs_network。
DELETE /profiles/{name}
移入回收站(可恢复),返回 204。错误:404;409 身份正在运行,先停止;403 forbidden;503 needs_network。
GET /trash
回收站列表,最近删除的在前。
[{"name": "shop-9", "engine": "chromium", "os": "windows", "created": "…", "deleted_at": "…"}]
POST /trash/{name}/restore
从回收站恢复。恢复会重新占用套餐名额。
{"name": "shop-9", "engine": "chromium", "created": "…"}
错误:404;409 已有同名身份;403 quota_exceeded / forbidden;503 needs_network。
启动与接管
有两种方式打开一个身份:
- 启动(
/launch、/launch-many):和客户端里点「打开」一样,开一个独立的浏览器窗口。适合人看着用、批量开窗、平铺。 - 会话(
/sessions/{name},见会话式操作):由客户端托管的浏览器,可以直接用 HTTP 指令操作,不用自己装 Playwright。
同一个身份同一时间只用其中一种方式打开。POST /profiles/{name}/stop 两种都能关。
POST /profiles/{name}/launch
启动一个身份,返回 202(进程已拉起,窗口随后出现)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
headless | bool | 否 | 无头运行,默认 false |
url | string | 否 | 启动后打开的网址 |
curl -X POST http://127.0.0.1:7801/profiles/shop-1/launch \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
{"name": "shop-1", "pid": 23456}
错误:404;403 订阅过期(reason 为授权状态)、over_limit(超出套餐身份数,带 locked)、forbidden;409 kernel_missing(带 kernel)、geoip_downloading(首次使用正在下载地理库,稍后重试)、firefox_disabled、身份已在运行、出口 IP 与身份记录不符或代理不通(开了对应启动选项时);503 geoip_update_failed(地理库下载起不来)。
启动是异步的:子进程随后若启动失败,会记在 GET /profiles 的 last_error 里。
POST /launch-many
批量启动,立即返回 202,在后台按并发上限依次启动。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
names | string[] | 是 | 要启动的身份 |
max | int | 否 | 同时开着的窗口上限,默认 5。窗口关闭后才腾出名额 |
headless | bool | 否 | 无头运行,默认 false |
curl -X POST http://127.0.0.1:7801/launch-many \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"names": ["shop-1", "shop-2", "shop-3"], "max": 3}'
{"accepted": ["shop-1", "shop-2", "shop-3"], "max": 3}
错误(在受理前整批检查,任何一个不过就整批不启动):404;409 kernel_missing、geoip_downloading、其中有身份已在运行;403 订阅过期 / over_limit。
POST /profiles/{name}/stop
停止身份:关掉它的启动窗口和/或会话。会先让浏览器正常退出(Cookie 落盘),最多等几秒再强制结束。对没在运行的身份调用也返回 200。
{"name": "shop-1", "stopped": ["launch"], "status": "idle"}
stopped 里是实际关掉的:launch(启动的窗口)、session(会话)。错误:404。
GET /windows
正在运行的启动窗口的进程号(不含会话)。
[{"name": "shop-1", "engine": "chromium", "wrapper_pid": 23456, "browser_pid": 23460, "pids": [23460, 23471]}]
browser_pid 是浏览器主进程(窗口所属进程),刚启动时可能还是 null。
cdp_endpoint:用 Playwright / Puppeteer 接管
Chrome 内核身份运行时都开着一个本机调试端口,调试地址在这些地方返回:
- 启动的窗口:
GET /profiles/{name}的cdp_endpoint。启动是异步的,刚启动时为null,轮询到status == "running"且cdp_endpoint非空即可,见快速开始。 - 会话:
POST /sessions/{name}和GET /sessions的返回里直接带cdp_endpoint。
调试端口默认每次启动随机分配。想固定端口(比如别的工具要连固定地址),可以设置启动选项 launch.debugPort(1024–65535,每个身份不同;launch 是整体替换,先读出 meta.launch 再合并):
launch = requests.get(f"{BASE}/profiles/shop-1", headers=H).json()["meta"].get("launch") or {}
requests.patch(f"{BASE}/profiles/shop-1", json={"launch": {**launch, "debugPort": 9301}}, headers=H)
接管后关闭 Playwright 连接只是断开,不会关浏览器;关浏览器用 POST /profiles/{name}/stop。
会话式操作
不想自己装 Playwright,也可以直接用 HTTP 指令操作会话里的浏览器。先开会话,再发指令,最后关掉。指令都作用在会话的当前标签页上。单条指令最长 120 秒。
指令通用错误:409 会话没开(session not open,身份不存在也是这个)或会话已失效需重开(session dead);502 浏览器操作失败(找不到元素、超时等,detail 为错误说明,会话保持可用)。
POST /sessions/{name}
打开会话,浏览器就绪后返回 201。会话已开着时直接返回现有会话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
headless | bool | 否 | 无头运行,默认 false(有窗口) |
idle_timeout | int | 否 | 空闲多少秒后自动关闭,默认 600。只算经本 API 发的指令;只用 Playwright 直连操作的长任务请设大一些 |
{"name": "shop-1", "opened_at": 1759370000.0, "idle_seconds": 0, "url": "about:blank", "cdp_endpoint": "ws://127.0.0.1:53123/devtools/browser/…"}
错误:404;409 kernel_missing、firefox_disabled、浏览器没能起来(session dead, reopen: session failed to start: …,例如该身份已经用 /launch 打开着);403 订阅过期 / over_limit;429 会话数已达上限(8)。
POST /sessions/{name}/goto
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 要打开的网址(最长等 60 秒) |
curl -X POST http://127.0.0.1:7801/sessions/shop-1/goto \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# → {"url": "https://example.com/"}
POST /sessions/{name}/click_selector
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
selector | string | 是 | CSS / Playwright 选择器 |
timeout | int | 否 | 等待元素的毫秒数 |
返回 {"ok": true}。
POST /sessions/{name}/fill_selector
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
selector | string | 是 | 输入框选择器 |
value | string | 是 | 要填的内容(覆盖原内容) |
submit | bool | 否 | 填完按回车,默认 false |
timeout | int | 否 | 等待元素的毫秒数 |
返回 {"ok": true}。
POST /sessions/{name}/type
按无障碍角色和名称定位元素并填入文字(不用写选择器)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
role | string | 是 | 角色,如 textbox / searchbox |
name | string | 是 | 元素的可访问名称(通常是标签文字或 placeholder) |
text | string | 是 | 要填的内容 |
submit | bool | 否 | 填完按回车,默认 false |
返回 {"ok": true}。角色和名称可以从 snapshot 里看。
POST /sessions/{name}/press
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 是 | 按键名,如 Enter、Tab、Control+A |
返回 {"ok": true}。
POST /sessions/{name}/scroll
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
dy | number | 是 | 纵向滚动像素(正数向下) |
dx | number | 否 | 横向滚动像素,默认 0 |
以真实滚轮事件滚动,返回 {"ok": true}。
POST /sessions/{name}/wait_for
等待条件满足;selector / text / url 三选一(都不传回 502)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
selector | string | 三选一 | 等元素出现 |
text | string | 三选一 | 等页面出现这段文字 |
url | string | 三选一 | 等地址变成这个(支持通配符,如 **/dashboard) |
timeout | int | 否 | 毫秒,默认 10000;超时回 502 |
返回 {"ok": true}。
GET /sessions/{name}/screenshot
直接返回图片(不是 JSON)。
| 查询参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
full_page | bool | 否 | 整页长图,默认 false |
selector | string | 否 | 只截这个元素 |
format | string | 否 | png(默认)/ jpeg |
quality | int | 否 | JPEG 质量,默认 60 |
curl -H "X-API-Key: np_你的Key" -o shot.png "http://127.0.0.1:7801/sessions/shop-1/screenshot?full_page=true"
GET /sessions/{name}/snapshot
当前页面的无障碍树(文本形式),适合给 AI 读页面结构、找 type 要用的角色和名称。
{"snapshot": "- heading \"Example Domain\" [level=1]\n- link \"More information...\""}
POST /sessions/{name}/eval
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
expr | string | 是 | 在页面里执行的 JavaScript 表达式(不超过 2 MB,超出回 413) |
{"ok": true, "result": "Example Domain"}
// 页面脚本出错时不算接口错误:
{"ok": false, "error": "ReferenceError: foo is not defined", "error_type": "Error"}
GET /sessions/{name}/tabs
{"tabs": [{"index": 0, "url": "https://example.com/"}, {"index": 1, "url": "https://example.org/"}], "active": 1}
POST /sessions/{name}/switch_tab
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
index | int | 是 | 标签页序号(见 tabs),之后的指令都作用在它上面 |
{"index": 0, "url": "https://example.com/"}
序号不存在回 502。
POST /sessions/{name}/new_tab
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 否 | 新标签页打开的网址;新标签页成为当前标签页 |
{"index": 2, "url": "https://example.net/"}
DELETE /sessions/{name}
关闭会话,返回 204。错误:404 身份不存在;409 会话没开。
体检
GET /profiles/{name}/checkup
检查身份的指纹是否自洽(系统、显卡、时区语言与出口等)。默认只看记录,不开浏览器。
| 查询参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
live | bool | 否 | true 时额外无头启动一次,实测 TLS 指纹和 User-Agent 并与基准比对(较慢) |
{
"name": "shop-1",
"engine": "chromium",
"live": false,
"summary": "pass",
"checks": [
{"id": "os_host", "title": "OS 与宿主一致", "verdict": "pass", "detail": "windows(Phase 0 铁律)"}
]
}
summary 与每项 verdict 取值:pass / warn / fail(单项还可能是 skip)。错误:404。
代理
代理池:先把代理加进池子,再分配给身份。也可以直接在身份上写 proxy(见 PATCH /profiles/{name})。
GET /proxies
列出代理池,密码打码。
[{"id": "9f2c1a7b3d4e5f60", "label": "美区住宅", "url": "http://user:***@1.2.3.4:8000", "type": "http",
"added": "2026-10-02", "last_check": {"ok": true, "exit_ip": "1.2.3.4", "country": "US", "latency_ms": 420, "at": "…"},
"assigned": 2}]
assigned 为已分配的身份数;last_check 从未检测过为 null。
POST /proxies
添加一个或多个代理,返回 201 和新增的条目(格式同上)。任何一条不合法则整批不保存。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 二选一 | 一个代理地址 |
urls | string[] | 二选一 | 多个代理地址 |
label | string | 否 | 备注 |
curl -X POST http://127.0.0.1:7801/proxies \
-H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
-d '{"urls": ["http://user:[email protected]:8000", "5.6.7.8:1080:user:pass"], "label": "美区住宅"}'
错误:422 没给地址或地址不合法。
PATCH /proxies/{pid}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
label | string | 否 | 新备注 |
url | string | 否 | 新地址(改地址会清空上次检测结果) |
返回更新后的条目。错误:404;422 地址不合法。
DELETE /proxies/{pid}
从池子删除,返回 204。错误:404。
POST /proxies/check
检测代理的出口 IP、国家和延迟,结果同时写回 last_check。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
ids | string[] | 否 | 只检测这些;不传检测全部 |
[
{"id": "9f2c1a7b3d4e5f60", "ok": true, "exit_ip": "1.2.3.4", "country": "US", "latency_ms": 420, "geo": {"…": "…"}, "at": "…"},
{"id": "0a1b2c3d4e5f6a7b", "ok": false, "error": "ConnectTimeout: …", "at": "…"}
]
POST /profiles/{name}/assign-proxy
把池子里的代理分配给身份(替换身份原有代理)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
proxy_id | string | 是 | 代理池条目的 id |
返回该身份保存的完整记录(含 name、proxy_id,以及对象形式的 proxy:{"server": "http://1.2.3.4:8000", "username": "…", "password": "…"};其余字段略)。注意:响应里带代理账号密码,不要原样写进日志。错误:404 身份或代理不存在;403 forbidden;503 needs_network。
窗口 仅 Windows
/windows/tile 和 /sync/start 只在 Windows 上可用,其他系统回 501 {"detail": {"reason": "unsupported_platform"}};/sync/stop 各系统都回 200。只作用于「启动」打开的窗口,不含会话。
POST /windows/tile
把这些身份的窗口按网格平铺到屏幕上。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
names | string[] | 是 | 要平铺的身份 |
cols | int | 否 | 列数;不填自动 |
wait | number | 否 | 最多等多少秒让窗口出来(0–60,默认 0)。刚调完 launch-many 就平铺时设上 |
{"placed": ["shop-1", "shop-2"], "not_ready": ["shop-3"], "cols": 2}
not_ready 是没找到窗口的身份。错误:501;404;409 not_running(wait 为 0 且身份没在运行)。
POST /sync/start
开始窗口同步:在主窗口里的鼠标键盘操作,同步到其他窗口。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
leader | string | 是 | 主窗口的身份 |
followers | string[] | 否 | 跟随的身份 |
{"active": true, "leader": "shop-1", "followers": [{"…": "…"}], "paused_reason": null, "events_total": 0}
错误:501;404;409 sync_active(已有同步在进行)、not_running、no_cdp、主窗口还没出来。
POST /sync/stop
停止同步,返回 {"active": false}。没有同步在进行时、以及非 Windows 系统上,也返回同样结果(不回 501)。
其他
GET /version 免 Key
客户端版本、已装内核、本机系统。不需要 Key,适合探测服务是否在、在哪个端口。
{"app": "…", "daemon": "…", "kernels": {"…": "…"}, "host": {"os": "windows", "…": "…"}, "brand": {"id": "nullprint", "…": "…"}}
其余字段略。
GET /license
当前授权状态与套餐用量。
{"state": "valid", "plan_name": "…", "profile_limit": 50, "used": 12, "expires_at": "…", "days_left": 20, "role": "…"}
state 为 valid 时正常;其他取值如 unlicensed(未登录)、plan_expired。其余字段略。
POST /api-key/reset
生成新 Key,老 Key 立即失效(用老 Key 的脚本会收到 401 invalid_api_key)。新 Key 同时写入 daemon.json。
{"api_key": "np_…"}
错误:500 api_key_write_failed(写文件失败,老 Key 保持有效)。
完整示例
批量启动 → 平铺 → 各自打开网页 → 导出 Cookie → 停止。启动后轮询 GET /profiles/{name} 拿 cdp_endpoint,再用 Playwright 连上去。平铺只在 Windows 上可用,其他系统回 501,示例里跳过。
Python(requests + playwright)
import base64, time, requests
from playwright.sync_api import sync_playwright
BASE, KEY = "http://127.0.0.1:7801", "np_你的Key"
H = {"X-API-Key": KEY}
NAMES = ["shop-1", "shop-2", "shop-3"]
def api(method, path, **kw):
r = requests.request(method, f"{BASE}{path}", headers=H, timeout=120, **kw)
r.raise_for_status()
return r.json() if r.content else None
def wait_cdp(name, seconds=60):
for _ in range(seconds): # 等窗口起来、拿到 CDP 地址
p = api("GET", f"/profiles/{name}")
if p["status"] == "running" and p.get("cdp_endpoint"):
return p["cdp_endpoint"]
time.sleep(1)
raise RuntimeError(f"{name} {seconds} 秒内没拿到 cdp_endpoint")
# 1. 批量启动(立即返回;max 设成个数 = 全部同时开)
api("POST", "/launch-many", json={"names": NAMES, "max": len(NAMES)})
# 2. 平铺(仅 Windows;最多等 30 秒让窗口出来)
r = requests.post(f"{BASE}/windows/tile", json={"names": NAMES, "wait": 30}, headers=H, timeout=120)
print("平铺:", r.json() if r.ok else f"跳过({r.status_code})")
# 3. 各自打开网页
with sync_playwright() as pw:
for n in NAMES:
browser = pw.chromium.connect_over_cdp(wait_cdp(n))
page = browser.contexts[0].pages[0]
page.goto("https://example.com")
print(n, page.title())
# 4. 导出 Cookie(打成一个 zip)
z = api("POST", "/cookies/export-batch", json={"names": NAMES, "format": "json"})
with open(z["filename"], "wb") as f:
f.write(base64.b64decode(z["zip_base64"]))
print("导出成功:", z["ok"], "失败:", z["failed"])
# 5. 停止
for n in NAMES:
print(api("POST", f"/profiles/{n}/stop"))
Node(fetch + playwright)
Node 18+,npm i playwright,保存为 batch.mjs 运行。
import { chromium } from "playwright";
import { writeFileSync } from "node:fs";
const BASE = "http://127.0.0.1:7801", KEY = "np_你的Key";
const NAMES = ["shop-1", "shop-2", "shop-3"];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function api(method, path, body) {
const r = await fetch(BASE + path, {
method,
headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!r.ok) throw Object.assign(new Error(`${method} ${path} → ${r.status} ${await r.text()}`), { status: r.status });
return r.status === 204 ? null : r.json();
}
async function waitCdp(name, seconds = 60) {
for (let i = 0; i < seconds; i++) { // 等窗口起来、拿到 CDP 地址
const p = await api("GET", `/profiles/${name}`);
if (p.status === "running" && p.cdp_endpoint) return p.cdp_endpoint;
await sleep(1000);
}
throw new Error(`${name} ${seconds} 秒内没拿到 cdp_endpoint`);
}
// 1. 批量启动
await api("POST", "/launch-many", { names: NAMES, max: NAMES.length });
// 2. 平铺(仅 Windows;其他系统 501,跳过)
try {
console.log("平铺:", await api("POST", "/windows/tile", { names: NAMES, wait: 30 }));
} catch (e) {
if (e.status !== 501) throw e;
console.log("平铺:跳过(非 Windows)");
}
// 3. 各自打开网页
for (const n of NAMES) {
const browser = await chromium.connectOverCDP(await waitCdp(n));
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");
console.log(n, await page.title());
await browser.close(); // 只断开连接,不关浏览器
}
// 4. 导出 Cookie
const z = await api("POST", "/cookies/export-batch", { names: NAMES, format: "json" });
writeFileSync(z.filename, Buffer.from(z.zip_base64, "base64"));
console.log("导出成功:", z.ok, "失败:", z.failed);
// 5. 停止
for (const n of NAMES) console.log(await api("POST", `/profiles/${n}/stop`));
稳定性与变更记录
本页列出的接口、参数和字段承诺向后兼容:以后只会新增,不会改名或删除;确需不兼容的改动会提前在这里公告。客户端还有一些本页没有列出的内部接口,随时可能变化,请不要依赖。
变更记录
- 2026-10 首版:固定端口 7801(被占顺延到 7810)、API Key 鉴权、本页。