GET /v1/status
已包含在发布版本中Liminal 是否在运行,是否有人登录
最轻量的调用,也是等待应用启动时适合轮询的那一个。没有账号也会响应:signedIn 为 false 是一种状态,而不是错误。
参数
无。
响应
{
"status": "running",
"version": "<version>",
"signedIn": true
}拒绝
- 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
输入一个词:按钮名称、设置或错误信息。
文档自动化
上次对照契约核对:2026 年 9 月
已发布,并且就在你今天可以下载的版本里——macOS 和 Windows 都是如此。开关在应用的设置中,在你打开它之前 API 一直处于关闭状态。这个版本会响应哪些调用,标注在下面的每个操作和每个示例上,而不是写成这里的一句话——那样的话,下一次发布就会让它过时。
同一台电脑上的脚本可以把配置文件的浏览器变成它能操控的对象。这个接口由一个服务、一把密钥和一个地址文件组成——它们最先讲,因为其他一切都建立在它们之上——再加上其上的几条路由:找到配置文件,打开它,把 Puppeteer、Playwright 或 Selenium 连接到它返回的地址,然后再把它关闭。
应用界面目前只有英文和俄文。本页中的按钮和设置名称与英文界面上显示的一致。
默认关闭,并且只能从这台电脑访问。
Settings → API & Automation,开启 Local API。在此之前没有任何东西在监听:端口没有打开,脚本得到的是连接被拒绝,而不是一个空响应。127.0.0.1 上接受连接,别处都不行。你网络中的其他电脑无法访问它,也没有任何设置能把它对外开放——脚本是在 Liminal 旁边运行,而不是通过网络去访问它。48361,这是 Liminal 自己的端口,这样就不会与电脑上已经在监听的其他浏览器冲突。可以输入另一个端口,或者点击 Random free port,让应用自己挑选。更改立即生效,无需重启 Liminal。port_in_use。在你选择另一个端口之前不会有任何监听,因此读取该文件的脚本能分清“已关闭”和“无法启动”。一把密钥,只显示一次,保存在操作系统存放机密的地方。
64 个小写十六进制字符组成的密钥,并且只显示一次。离开这个界面后它就被隐藏:应用无法再次显示它,只能生成一把新的。Authorization: Bearer,后面跟着密钥本身。没有查询参数,也没有 cookie;不带这个请求头的请求会像密钥错误的请求一样被拒绝。应用会把自己的地址写进一个文件,这样就不必把同样的内容输入两遍。
Local API/status.json 的绝对路径,它位于 Liminal 的用户数据目录中。请读取这个文件,而不要把端口写死:它会跟随你设置的端口,也会跟随 Random free port 挑选的端口。enabled, status, address, port——不含任何机密。{"enabled":true,"status":"running","address":"http://127.0.0.1:48361","port":48361}off,地址为空,没有端口。同样的结构,不同的状态——脚本只读一个文件就能了解全部情况。{"enabled":false,"status":"off","address":"","port":0}应当最先发出的调用:Liminal 是否在运行,是否有人登录。
/v1/status 的 GET 请求,并在请求头中带上密钥。curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status200,附带正在运行的版本号,以及应用中是否有人登录。signedIn 为 false 并不是错误——API 照常响应;需要账号的是那些读取目录或打开配置文件的操作。{"status":"running","version":"<version>","signedIn":true}401,以及代码 invalid_key——它是固定不变的,应当据此分支处理。旁边的消息是应用写给查看日志的人看的措辞;请把它当作可能变化的文本,而不是契约的一部分。{"error":{"code":"invalid_key","message":"Неверный ключ"}}127.0.0.1 和文件中的端口。以其他主机名到达的请求不会得到响应,即使它到达的是同一个端口——服务只对自己的地址作出响应。off。直截了当地说清楚,免得这里的任何内容被当成它并未作出的承诺。
每条路由的用途、它接收什么、返回什么,以及它可能给出的每一种拒绝。操作旁边的标记说明你今天可以下载的版本是否会响应它。
Liminal 是否在运行,是否有人登录
最轻量的调用,也是等待应用启动时适合轮询的那一个。没有账号也会响应:signedIn 为 false 是一种状态,而不是错误。
无。
{
"status": "running",
"version": "<version>",
"signedIn": true
}目录中的配置文件,可筛选、可分页
读取的是应用显示的同一个目录。筛选条件可以组合,除 name 之外都是精确匹配;name 匹配名称中包含你输入内容的任何配置文件,不区分大小写。
{
"page": 1,
"pageSize": 100,
"total": 1,
"items": [
"<profile>"
]
}单个配置文件,连同它的代理和指纹
与列表返回的是同一个对象,只针对一个标识符。代理地址会显示,密码则绝不会显示。
{
"id": "<profile-id>",
"name": "Shop EU",
"workspaceId": "<workspace-id>",
"folderId": "<folder-id>",
"status": "running",
"health": "ok",
"proxy": {
"id": "<proxy-id>",
"name": "Residential DE",
"type": "socks5",
"host": "proxy.example.net",
"port": 1080,
"working": true,
"lastCheckedExternalIp": "203.0.113.10",
"lastCheckedCountryCode": "DE"
},
"fingerprint": {
"platform": "MacIntel",
"language": "de-DE",
"timezone": "Europe/Berlin"
},
"lastOpenedAt": "2026-09-30T08:15:00.000Z"
}打开配置文件并获得驱动程序要连接的地址
以应用的方式启动配置文件——带着它的指纹、代理和 cookie——并在浏览器启动后返回 CDP 地址。这个调用会等待;它不会返回一个需要轮询的任务。
{
"id": "<profile-id>",
"status": "running",
"cdpDiscoveryUrl": "http://127.0.0.1:48361/devtools/<token>/json/version",
"cdpBrowserUrl": "ws://127.0.0.1:9222/devtools/browser/<token>",
"seleniumDebuggerUrl": "127.0.0.1:9222",
"chromedriverPath": "/Applications/Liminal.app/.../chromedriver",
"debugPort": 9222,
"debugAddress": "127.0.0.1:9222",
"externalIp": "203.0.113.10",
"countryCode": "DE"
}detail 字段会指出是哪个阶段。为已经打开的配置文件获取驱动程序地址
用于你手动打开、现在想用脚本操控的配置文件。它不会重启任何东西:会话、标签页和 cookie 都保持原样。
{
"id": "<profile-id>",
"status": "running",
"cdpDiscoveryUrl": "http://127.0.0.1:48361/devtools/<token>/json/version",
"cdpBrowserUrl": "ws://127.0.0.1:9222/devtools/browser/<token>",
"seleniumDebuggerUrl": "127.0.0.1:9222",
"chromedriverPath": "/Applications/Liminal.app/.../chromedriver",
"debugPort": 9222,
"debugAddress": "127.0.0.1:9222",
"externalIp": "203.0.113.10",
"countryCode": "DE"
}detail 字段会指出是哪个阶段。像关闭窗口那样关闭配置文件
让会话把状态写完。在同一个配置文件上两次运行脚本之间,应当调用的就是这个。
{
"id": "<profile-id>",
"status": "closed"
}关闭一个关不掉的配置文件
返回同样的响应,但不等待会话收尾。在停止调用没有返回时才使用它,不要当作常规做法。
{
"id": "<profile-id>",
"status": "closed"
}此刻打开了什么,哪些可以被操控
每一个已打开的配置文件,无论是窗口还是调用打开的——startedBy 会说明是哪一种——在有驱动程序地址时一并给出。脚本重启后可以从这里重新连接。
无。
{
"items": [
{
"id": "<profile-id>",
"workspaceId": "<workspace-id>",
"status": "running",
"automationAvailable": true,
"startedBy": "api",
"cdpBrowserUrl": "ws://127.0.0.1:9222/devtools/browser/<token>",
"seleniumDebuggerUrl": "127.0.0.1:9222",
"chromedriverPath": "/Applications/Liminal.app/.../chromedriver",
"debugPort": 9222,
"debugAddress": "127.0.0.1:9222"
}
]
}每个代码块都是完整的:它读取地址文件、调用 API 并连接驱动程序。复制一个,填入你自己的配置文件名称,就能运行。标记说明你今天可以下载的版本是否会响应该代码块所需的一切。
通过 CDP 连接到启动调用返回的地址——Puppeteer 脚本就是这样连接一个已经在运行的浏览器的。
调用 /v1/profiles · /v1/profiles/{id}/start
import { readFileSync } from 'node:fs'
import puppeteer from 'puppeteer-core'
// Local API/status.json, inside Liminal's user-data directory. The settings
// screen shows the absolute path; that path and the key are your script’s to
// keep — the app puts neither into the environment for you.
const { address } = JSON.parse(readFileSync(process.env.LIMINAL_ADDRESS_FILE, 'utf8'))
async function call(path, init) {
const response = await fetch(address + path, {
...init,
headers: { Authorization: `Bearer ${process.env.LIMINAL_API_KEY}` },
})
const body = await response.json()
if (!response.ok) throw new Error(body.error.code)
return body
}
const { items } = await call('/v1/profiles?name=' + encodeURIComponent('Shop EU'))
const started = await call(`/v1/profiles/${items[0].id}/start`, { method: 'POST' })
const browser = await puppeteer.connect({ browserWSEndpoint: started.cdpBrowserUrl })
const page = await browser.newPage()
await page.goto('https://example.com')
console.log(await page.title())
await browser.disconnect()
通过 Playwright 的 CDP 入口建立同样的连接,进入配置文件已有的上下文,而不是新建一个。
调用 /v1/profiles · /v1/profiles/{id}/start
import { readFileSync } from 'node:fs'
import { chromium } from 'playwright'
// Local API/status.json, inside Liminal's user-data directory. The settings
// screen shows the absolute path; that path and the key are your script’s to
// keep — the app puts neither into the environment for you.
const { address } = JSON.parse(readFileSync(process.env.LIMINAL_ADDRESS_FILE, 'utf8'))
async function call(path, init) {
const response = await fetch(address + path, {
...init,
headers: { Authorization: `Bearer ${process.env.LIMINAL_API_KEY}` },
})
const body = await response.json()
if (!response.ok) throw new Error(body.error.code)
return body
}
const { items } = await call('/v1/profiles?name=' + encodeURIComponent('Shop EU'))
const started = await call(`/v1/profiles/${items[0].id}/start`, { method: 'POST' })
const browser = await chromium.connectOverCDP(started.cdpBrowserUrl)
const page = await browser.contexts()[0].newPage()
await page.goto('https://example.com')
console.log(await page.title())
await browser.close()
同一件事的 Python 版本,读取地址文件时不依赖标准库之外的任何东西。
调用 /v1/profiles · /v1/profiles/{id}/start
import json
import os
import urllib.parse
import urllib.request
from playwright.sync_api import sync_playwright
# Local API/status.json, inside Liminal's user-data directory. The settings
# screen shows the absolute path; that path and the key are your script’s to
# keep — the app puts neither into the environment for you.
with open(os.environ["LIMINAL_ADDRESS_FILE"], encoding="utf-8") as handle:
address = json.load(handle)["address"]
def call(path, method="GET"):
request = urllib.request.Request(address + path, method=method)
request.add_header("Authorization", "Bearer " + os.environ["LIMINAL_API_KEY"])
with urllib.request.urlopen(request) as response:
return json.load(response)
profile = call("/v1/profiles?name=" + urllib.parse.quote("Shop EU"))["items"][0]
started = call(f"/v1/profiles/{profile['id']}/start", method="POST")
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(started["cdpBrowserUrl"])
page = browser.contexts[0].new_page()
page.goto("https://example.com")
print(page.title())
Selenium 通过调试器地址附加到一个已经在运行的浏览器,并使用响应中指向的 chromedriver——不下载任何东西,也不靠猜测。
调用 /v1/profiles · /v1/profiles/{id}/start
import json
import os
import urllib.parse
import urllib.request
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
# Local API/status.json, inside Liminal's user-data directory. The settings
# screen shows the absolute path; that path and the key are your script’s to
# keep — the app puts neither into the environment for you.
with open(os.environ["LIMINAL_ADDRESS_FILE"], encoding="utf-8") as handle:
address = json.load(handle)["address"]
def call(path, method="GET"):
request = urllib.request.Request(address + path, method=method)
request.add_header("Authorization", "Bearer " + os.environ["LIMINAL_API_KEY"])
with urllib.request.urlopen(request) as response:
return json.load(response)
profile = call("/v1/profiles?name=" + urllib.parse.quote("Shop EU"))["items"][0]
started = call(f"/v1/profiles/{profile['id']}/start", method="POST")
options = webdriver.ChromeOptions()
options.debugger_address = started["seleniumDebuggerUrl"]
driver = webdriver.Chrome(service=Service(started["chromedriverPath"]), options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
Node 版本同理:调试器地址和匹配的 chromedriver 的路径都来自启动调用的响应。
调用 /v1/profiles · /v1/profiles/{id}/start
import { readFileSync } from 'node:fs'
import { Builder } from 'selenium-webdriver'
import chrome from 'selenium-webdriver/chrome.js'
// Local API/status.json, inside Liminal's user-data directory. The settings
// screen shows the absolute path; that path and the key are your script’s to
// keep — the app puts neither into the environment for you.
const { address } = JSON.parse(readFileSync(process.env.LIMINAL_ADDRESS_FILE, 'utf8'))
async function call(path, init) {
const response = await fetch(address + path, {
...init,
headers: { Authorization: `Bearer ${process.env.LIMINAL_API_KEY}` },
})
const body = await response.json()
if (!response.ok) throw new Error(body.error.code)
return body
}
const { items } = await call('/v1/profiles?name=' + encodeURIComponent('Shop EU'))
const started = await call(`/v1/profiles/${items[0].id}/start`, { method: 'POST' })
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(new chrome.Options().debuggerAddress(started.seleniumDebuggerUrl))
.setChromeService(new chrome.ServiceBuilder(started.chromedriverPath))
.build()
await driver.get('https://example.com')
console.log(await driver.getTitle())
await driver.quit()
同一份契约,以文件形式提供,地址固定不变:OpenAPI,JSON 格式,由本页渲染所用的数据生成。可以把它导入 Postman 或 Insomnia,或者用它生成客户端。其中每个操作的可用性都与上面的标记一致。
openapi.json首次带来它的发布版本,以及此后扩展它的每一个发布版本,都记录在更新日志中。关于契约的问题,或者这里的某个值与你的版本的实际行为不符:[email protected]。