文档自动化
用脚本实现自动化
同一台电脑上的脚本可以打开一个配置文件并接管它的浏览器。脚本得到的就是你手动打开的那个配置文件——同样的指纹、同样的连接、同样的 Cookie。
应用界面目前只有英文和俄文。本页中的按钮和设置名称与英文界面上显示的一致。
本地 API 是什么
本地 API 是应用内部的一个小型服务。它只在这台电脑上应答,并且只应答带有你的密钥的请求。通过它,脚本可以列出配置文件、启动和停止它们,并获得自动化库连接到已启动浏览器所用的地址。
- 所有套餐都可用,包括免费套餐。
- 仅限这台电脑:它监听
127.0.0.1,无法向网络开放。 - 在你打开之前一直是关闭的。
- 配置文件在应用中创建。 API 只运行你已有的配置文件,不能创建或编辑它们。
打开它
- 打开 Settings › API & Automation,然后打开 Local API。旁边的那行文字会变为 Running on this computer only.
- 保留 Port——如果你没有改过,它是
48361——或者点击 Random free port。 - 现在就用 Copy 复制 API key:密钥本身从不显示,离开这个页面之后就无法再复制,只能更换。它的长度是 64 个字符。

- Address
- 服务应答的地址,带有 Copy 按钮——默认是
http://127.0.0.1:48361。 - Regenerate…
- 生成一个新密钥。旧密钥立即失效,用它发出的浏览器地址也随之失效。
- API documentation
- 打开参考文档。
说明. 密钥保存在系统的凭据存储中,而不是文件里。脚本应当从应用写入的文件——Liminal 数据文件夹中的 Local API/status.json——读取地址,而不是把端口写死;参考文档说明了它在哪里。
第一个请求
每个请求都在请求头中携带密钥。先问一下服务是否已启动:
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status{"status":"running","version":"<version>","signedIn":true}signedIn 告诉你是否有人已登录应用。涉及配置文件的操作需要这一点:没有登录时它们会返回 not_signed_in。
找到一个配置文件并启动它
列出配置文件——结果分页返回,可以按名称、工作区、文件夹和状态筛选:
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles取出你想要的配置文件的 id,然后启动它:
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/start{
"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"
}cdpBrowserUrl- Puppeteer 和 Playwright 连接的地址。
seleniumDebuggerUrl,chromedriverPath- Selenium 需要的东西:调试器地址,以及与浏览器匹配、随应用一同提供的驱动程序。
externalIp,countryCode- 配置文件启动时其连接的情况。
应用启动配置文件的方式与你点击 Open 时完全一样。已经打开的配置文件不会被再次启动——参考文档说明了如何连接到正在运行的配置文件。
连接你的库
每个示例都是完整的:它读取地址文件,按名称找到配置文件,启动它并打开一个页面。
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()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()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())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()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()提示. 请连接到配置文件已有的浏览器。不要让库自己启动一个浏览器——那个浏览器不会带有配置文件的指纹和 Cookie。
停止配置文件
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/stop停止会像 Close 那样关闭配置文件。对于关不掉的配置文件,还有强制停止。
值得了解
| 问题 | 回答 |
|---|---|
| 脚本看到的浏览器和我看到的不一样吗? | 不会。指纹和连接的应用方式与你手动打开配置文件时完全相同,同样的保护也都生效。 |
| 网站能看出浏览器是自动化的吗? | 对于通过 API 启动的配置文件,常见的自动化迹象会被隐藏。这一点无法关闭。 |
| 有无头模式吗? | 没有。API 启动配置文件的方式与应用相同。 |
| 请求数量有限制吗? | 没有与套餐挂钩的配额。 |
| 有命令行工具或录制的场景吗? | 没有。本地 API 就是自动化的方式。 |
| 出错时我会得到什么? | 一个状态码和一个可用于分支判断的简短 code——invalid_key、not_signed_in、profile_already_running 等。参考文档按操作逐一列出了它们。 |
已与已发布的应用核对:2026 年 10 月。 如果应用的行为与本页所写的不一致,那是本页有误——请写信至 [email protected]。每个版本的变化见更新日志。