本地 API

nullprint 客户端在本机开一个 HTTP 服务(默认 http://127.0.0.1:7801),脚本 / Playwright / 任何语言都能调。所有请求带 X-API-Key。

快速开始

  1. 安装 nullprint 客户端并登录。客户端开着,本地 API 就在。
  2. 打开客户端的「自动化」页,在「本地 API」卡片里复制地址和 Key(Key 形如 np_ 开头的一串)。
  3. 先用 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}
statusrunning / 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, "…": "…"}
}
字段说明
statusrunning / idle
cdp_endpoint浏览器调试地址(ws://127.0.0.1:<端口>/devtools/browser/<id>),传给 Playwright 的 connect_over_cdp。身份没运行、或刚启动还没就绪时为 null——启动后轮询本接口直到它非空
proxy代理的 协议://主机:端口(不含账号密码)
meta显示名称、分组、标签、备注、启动选项 launch 等

其余字段略(完整指纹档案)。错误:404 身份不存在。

POST /profiles

新建身份,返回 201。

参数类型必填说明
namestring是显示名称(可重复)。内部编号由它派生并保证唯一,以返回的 name 为准
proxystring否代理地址,如 http://user:pass@host:port、socks5://host:port、host:port:user:pass
enginestring否内核,默认 chromium
kernelstring否Chrome 大版本,如 "148";不填用安装包默认
osstring否windows / macos / linux,默认与本机相同
notestring否备注
tagsstring[]否标签
groupstring否分组名
urlsstring[]否启动时打开的网址
launchobject否启动选项(与客户端「启动设置」对应)
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)的不动。

参数类型必填说明
labelstring否显示名称;"" 改回用内部编号显示
groupstring否分组名
tagsstring[]否标签(整体替换)
notestring否备注
proxystring否代理地址;"" 清除代理
urlsstring[]否启动时打开的网址(整体替换)
launchobject否启动选项(整体替换,改其中一项请先读出 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(进程已拉起,窗口随后出现)。

参数类型必填说明
headlessbool否无头运行,默认 false
urlstring否启动后打开的网址
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,在后台按并发上限依次启动。

参数类型必填说明
namesstring[]是要启动的身份
maxint否同时开着的窗口上限,默认 5。窗口关闭后才腾出名额
headlessbool否无头运行,默认 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。会话已开着时直接返回现有会话。

参数类型必填说明
headlessbool否无头运行,默认 false(有窗口)
idle_timeoutint否空闲多少秒后自动关闭,默认 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

参数类型必填说明
urlstring是要打开的网址(最长等 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

参数类型必填说明
selectorstring是CSS / Playwright 选择器
timeoutint否等待元素的毫秒数

返回 {"ok": true}。

POST /sessions/{name}/fill_selector

参数类型必填说明
selectorstring是输入框选择器
valuestring是要填的内容(覆盖原内容)
submitbool否填完按回车,默认 false
timeoutint否等待元素的毫秒数

返回 {"ok": true}。

POST /sessions/{name}/type

按无障碍角色和名称定位元素并填入文字(不用写选择器)。

参数类型必填说明
rolestring是角色,如 textbox / searchbox
namestring是元素的可访问名称(通常是标签文字或 placeholder)
textstring是要填的内容
submitbool否填完按回车,默认 false

返回 {"ok": true}。角色和名称可以从 snapshot 里看。

POST /sessions/{name}/press

参数类型必填说明
keystring是按键名,如 Enter、Tab、Control+A

返回 {"ok": true}。

POST /sessions/{name}/scroll

参数类型必填说明
dynumber是纵向滚动像素(正数向下)
dxnumber否横向滚动像素,默认 0

以真实滚轮事件滚动,返回 {"ok": true}。

POST /sessions/{name}/wait_for

等待条件满足;selector / text / url 三选一(都不传回 502)。

参数类型必填说明
selectorstring三选一等元素出现
textstring三选一等页面出现这段文字
urlstring三选一等地址变成这个(支持通配符,如 **/dashboard)
timeoutint否毫秒,默认 10000;超时回 502

返回 {"ok": true}。

GET /sessions/{name}/screenshot

直接返回图片(不是 JSON)。

查询参数类型必填说明
full_pagebool否整页长图,默认 false
selectorstring否只截这个元素
formatstring否png(默认)/ jpeg
qualityint否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

参数类型必填说明
exprstring是在页面里执行的 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

参数类型必填说明
indexint是标签页序号(见 tabs),之后的指令都作用在它上面
{"index": 0, "url": "https://example.com/"}

序号不存在回 502。

POST /sessions/{name}/new_tab

参数类型必填说明
urlstring否新标签页打开的网址;新标签页成为当前标签页
{"index": 2, "url": "https://example.net/"}

DELETE /sessions/{name}

关闭会话,返回 204。错误:404 身份不存在;409 会话没开。

体检

GET /profiles/{name}/checkup

检查身份的指纹是否自洽(系统、显卡、时区语言与出口等)。默认只看记录,不开浏览器。

查询参数类型必填说明
livebool否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。

Cookie

导出身份的全部 Cookie(含值)。身份在运行时直接从浏览器读;没运行时客户端会在后台无头打开一下、读完即关。

查询参数类型必填说明
formatstring否json(默认)/ netscape
curl -H "X-API-Key: np_你的Key" "http://127.0.0.1:7801/profiles/shop-1/cookies/export?format=json"
{
  "name": "shop-1",
  "format": "json",
  "filename": "shop-1-cookies-20261002-0930.json",
  "count": 42,
  "text": "[{\"name\": \"session-id\", \"value\": \"…\", \"domain\": \".amazon.com\", \"path\": \"/\", …}]"
}

text 就是文件内容,可直接保存为 filename。错误:422 format 不对;404;403 forbidden(无导出权限);409 kernel_missing、read_failed、launch_failed、no_cdp。

批量导出,打成一个 zip。某个身份失败不影响其他身份,失败的在 zip 里放一个 <name>.error.txt。

参数类型必填说明
namesstring[]是要导出的身份
formatstring否json(默认)/ netscape
{
  "filename": "cookies-20261002-0930.zip",
  "zip_base64": "UEsDBBQAAAAIA…",
  "ok": ["shop-1", "shop-2"],
  "failed": [{"name": "shop-3", "reason": "forbidden"}]
}

把 zip_base64 做 base64 解码后写成文件即可。错误:422 format 不对。

导入 Cookie。导入的 Cookie 先排队,在身份下一次启动(或开会话)时注入浏览器。

参数类型必填说明
textstring是Cookie 文本:JSON、Netscape 格式或 name=value; …
formatstring否json / netscape / namevalue;不填自动识别
domainstring否namevalue 格式必填;JSON 里缺域名的行也用它
modestring否merge(默认,并入排队中的)/ replace(替换排队中的)
curl -X POST http://127.0.0.1:7801/profiles/shop-1/cookies/import \
  -H "X-API-Key: np_你的Key" -H "Content-Type: application/json" \
  -d '{"text": "session-id=abc; ubid-main=xyz", "domain": ".amazon.com"}'
{"name": "shop-1", "imported": 2, "pending": 2}

imported 为本次解析出的条数,pending 为排队待注入的总数。错误:422 文本解析不了或 mode 不对;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 和新增的条目(格式同上)。任何一条不合法则整批不保存。

参数类型必填说明
urlstring二选一一个代理地址
urlsstring[]二选一多个代理地址
labelstring否备注
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}

参数类型必填说明
labelstring否新备注
urlstring否新地址(改地址会清空上次检测结果)

返回更新后的条目。错误:404;422 地址不合法。

DELETE /proxies/{pid}

从池子删除,返回 204。错误:404。

POST /proxies/check

检测代理的出口 IP、国家和延迟,结果同时写回 last_check。

参数类型必填说明
idsstring[]否只检测这些;不传检测全部
[
  {"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_idstring是代理池条目的 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

把这些身份的窗口按网格平铺到屏幕上。

参数类型必填说明
namesstring[]是要平铺的身份
colsint否列数;不填自动
waitnumber否最多等多少秒让窗口出来(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

开始窗口同步:在主窗口里的鼠标键盘操作,同步到其他窗口。

参数类型必填说明
leaderstring是主窗口的身份
followersstring[]否跟随的身份
{"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 鉴权、本页。