跳到正文

文档自动化

本地 API

上次对照契约核对: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,地址文件会报告 port_in_use。在你选择另一个端口之前不会有任何监听,因此读取该文件的脚本能分清“已关闭”和“无法启动”。

密钥

一把密钥,只显示一次,保存在操作系统存放机密的地方。

出现时就复制
启用 API 时会生成一把由 64 个小写十六进制字符组成的密钥,并且只显示一次。离开这个界面后它就被隐藏:应用无法再次显示它,只能生成一把新的。
它存放在哪里
在 macOS 上是 Keychain,在 Windows 上是 Credential Manager——绝不会出现在设置文件、同步快照或日志里,也不会出现在下面的地址文件里。请像保管脚本使用的其他凭据一样保管你的那份副本。
如何吊销
Regenerate key 会生成一把新密钥,并立即停止接受旧密钥,无需重启应用,端口也不变。仍然拿着旧密钥的脚本从下一个请求起就会被拒绝。
如何发送
每个请求都用一个请求头携带密钥: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}
API 关闭时
文件仍在原处,并如实说明:off,地址为空,没有端口。同样的结构,不同的状态——脚本只读一个文件就能了解全部情况。
{"enabled":false,"status":"off","address":"","port":0}
它是整体替换的,不会只写一半
每一次变化——开启或关闭 API、换了端口、端口无法绑定——都会以原子方式重写该文件。在变化过程中读取它的脚本看到的要么是旧内容,要么是新内容,绝不会是被截断的一行。

状态请求

应当最先发出的调用:Liminal 是否在运行,是否有人登录。

调用
向文件中的地址发送针对 /v1/status 的 GET 请求,并在请求头中带上密钥。
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status
响应
HTTP 200,附带正在运行的版本号,以及应用中是否有人登录。signedIn 为 false 并不是错误——API 照常响应;需要账号的是那些读取目录或打开配置文件的操作。
{"status":"running","version":"<version>","signedIn":true}
密钥缺失或已被吊销
HTTP 401,以及代码 invalid_key——它是固定不变的,应当据此分支处理。旁边的消息是应用写给查看日志的人看的措辞;请把它当作可能变化的文本,而不是契约的一部分。
{"error":{"code":"invalid_key","message":"Неверный ключ"}}
完全按照文件里的写法访问
使用 127.0.0.1 和文件中的端口。以其他主机名到达的请求不会得到响应,即使它到达的是同一个端口——服务只对自己的地址作出响应。
旧版 Liminal 会怎样
什么也不做:端口没有打开,所以连接它的脚本会被拒绝。脚本正是靠这次拒绝来区分没有本地 API 的版本和把它关闭了的版本——在后一种情况下,地址文件存在,并且写着 off。

这个版本能做什么、不能做什么

直截了当地说清楚,免得这里的任何内容被当成它并未作出的承诺。

操作会先出现在本页,然后才进入你的版本
这个接口随着每次发布而扩展,已经写好的调用会先记录在这里,然后才有响应它的版本——会加上标注,绝不隐藏。所以请看标注,而不是看承诺:下面的每个操作和每个示例上都有标注,它告诉你打算用脚本做的事在你手上的应用里能否运行。
适用于哪些套餐
所有套餐,包括免费套餐。本地 API 不是付费功能:对很多人来说,在自己电脑上运行脚本本来就是使用防关联浏览器的方式,把它放到某个套餐后面等于对这种用法收费。套餐
脚本可以多频繁地请求
没有与套餐挂钩的配额。服务会保护自己不被请求淹没,而为它编写的脚本不会察觉到这一点。
自动化下的配置文件还是同一个配置文件
它保留自己的指纹、代理和 cookie,并且像你手动打开它时一样通过自己的代理访问网络。API 是进入它的一条途径,而不是浏览器的第二种模式。数据与安全

逐个说明每个操作

每条路由的用途、它接收什么、返回什么,以及它可能给出的每一种拒绝。操作旁边的标记说明你今天可以下载的版本是否会响应它。

GET /v1/status

已包含在发布版本中

Liminal 是否在运行,是否有人登录

最轻量的调用,也是等待应用启动时适合轮询的那一个。没有账号也会响应:signedIn 为 false 是一种状态,而不是错误。

参数

无。

响应

{
  "status": "running",
  "version": "<version>",
  "signedIn": true
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。

GET /v1/profiles

已包含在发布版本中

目录中的配置文件,可筛选、可分页

读取的是应用显示的同一个目录。筛选条件可以组合,除 name 之外都是精确匹配;name 匹配名称中包含你输入内容的任何配置文件,不区分大小写。

参数

pagequery · integer · 可选 · 1 至 1000000 · 默认为 1
读取第几页,从第一页算起。
pageSizequery · integer · 可选 · 1 至 100 · 默认为 100
每页包含多少个配置文件。上限就是默认值。
workspaceIdquery · string · 可选
只返回该工作区的配置文件。
folderIdquery · string · 可选
只返回该文件夹的配置文件。
statusquery · string · 可选
只返回处于该状态的配置文件;running 是已打开的配置文件的状态。
namequery · string · 可选
只返回名称中包含此内容的配置文件,不区分大小写。

响应

{
  "page": 1,
  "pageSize": 100,
  "total": 1,
  "items": [
    "<profile>"
  ]
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 400 invalid_pagination — page 或 pageSize 超出了允许的范围。

GET /v1/profiles/{id}

已包含在发布版本中

单个配置文件,连同它的代理和指纹

与列表返回的是同一个对象,只针对一个标识符。代理地址会显示,密码则绝不会显示。

参数

idpath · string · 必填
配置文件的标识符,与目录操作返回的一致。

响应

{
  "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"
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 404 profile_not_found — 目录中没有该标识符对应的配置文件。

POST /v1/profiles/{id}/start

已包含在发布版本中

打开配置文件并获得驱动程序要连接的地址

以应用的方式启动配置文件——带着它的指纹、代理和 cookie——并在浏览器启动后返回 CDP 地址。这个调用会等待;它不会返回一个需要轮询的任务。

参数

idpath · string · 必填
配置文件的标识符,与目录操作返回的一致。
debugPortbody · integer · 可选 · 1024 至 65534 · 默认为 0
CDP 端点绑定的端口。填 0 则由应用挑选一个空闲端口;其他任何值都必须是你的用户可以绑定的端口。
timeoutSecondsbody · integer · 可选 · 1 至 180 · 默认为 60
调用等待配置文件启动多久之后才放弃。
maskAutomationbody · boolean · 可选 · 默认为 true
只接受 true。自动化伪装无法关闭:脚本操控下的配置文件,就是你手动打开时网站看到的那个配置文件。

响应

{
  "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"
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 503 runtime_unavailable — 应用此刻无法启动或停止配置文件。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 404 profile_not_found — 目录中没有该标识符对应的配置文件。
  • 409 profile_already_running — 配置文件已经打开、它的启动仍在进行,或者之前给出的驱动程序地址仍然有效。
  • 400 invalid_start_options — 请求体不是 JSON 对象。
  • 400 invalid_port — debugPort 既不是 0,也不是允许范围内的端口。
  • 400 invalid_start_timeout — timeoutSeconds 超出了允许的范围。
  • 400 masking_unavailable — maskAutomation 被设为 false,而应用不提供这种模式。
  • 409 port_in_use — 你请求的调试端口已被占用。
  • 409 listen_failed — 调试端口因其他原因无法绑定。
  • 409 start_failed — 配置文件没有启动起来。应用能够说明时,安全的 detail 字段会指出是哪个阶段。
  • 409 proxy_unavailable — 配置文件要求使用它的代理,而代理没有响应。
  • 502 start_failed — 在要求浏览器启动之前,配置文件的准备工作就失败了。
  • 503 cdp_unavailable — 配置文件已启动,但它的 CDP 端点没有就绪。
  • 504 start_timeout — 配置文件未能在 timeoutSeconds 之内启动。

POST /v1/profiles/{id}/attach-automation

已包含在发布版本中

为已经打开的配置文件获取驱动程序地址

用于你手动打开、现在想用脚本操控的配置文件。它不会重启任何东西:会话、标签页和 cookie 都保持原样。

参数

idpath · string · 必填
配置文件的标识符,与目录操作返回的一致。
debugPortbody · integer · 可选 · 1024 至 65534 · 默认为 0
CDP 端点绑定的端口。填 0 则由应用挑选一个空闲端口;其他任何值都必须是你的用户可以绑定的端口。
timeoutSecondsbody · integer · 可选 · 1 至 180 · 默认为 60
调用等待配置文件启动多久之后才放弃。
maskAutomationbody · boolean · 可选 · 默认为 true
只接受 true。自动化伪装无法关闭:脚本操控下的配置文件,就是你手动打开时网站看到的那个配置文件。

响应

{
  "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"
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 503 runtime_unavailable — 应用此刻无法启动或停止配置文件。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 404 profile_not_found — 目录中没有该标识符对应的配置文件。
  • 409 profile_already_running — 配置文件已经打开、它的启动仍在进行,或者之前给出的驱动程序地址仍然有效。
  • 400 invalid_start_options — 请求体不是 JSON 对象。
  • 400 invalid_port — debugPort 既不是 0,也不是允许范围内的端口。
  • 400 invalid_start_timeout — timeoutSeconds 超出了允许的范围。
  • 400 masking_unavailable — maskAutomation 被设为 false,而应用不提供这种模式。
  • 409 port_in_use — 你请求的调试端口已被占用。
  • 409 listen_failed — 调试端口因其他原因无法绑定。
  • 409 start_failed — 配置文件没有启动起来。应用能够说明时,安全的 detail 字段会指出是哪个阶段。
  • 409 proxy_unavailable — 配置文件要求使用它的代理,而代理没有响应。
  • 502 start_failed — 在要求浏览器启动之前,配置文件的准备工作就失败了。
  • 503 cdp_unavailable — 配置文件已启动,但它的 CDP 端点没有就绪。
  • 504 start_timeout — 配置文件未能在 timeoutSeconds 之内启动。
  • 409 profile_not_running — 配置文件没有打开,所以没有可连接的对象。

POST /v1/profiles/{id}/stop

已包含在发布版本中

像关闭窗口那样关闭配置文件

让会话把状态写完。在同一个配置文件上两次运行脚本之间,应当调用的就是这个。

参数

idpath · string · 必填
配置文件的标识符,与目录操作返回的一致。

响应

{
  "id": "<profile-id>",
  "status": "closed"
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 503 runtime_unavailable — 应用此刻无法启动或停止配置文件。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 404 profile_not_found — 目录中没有该标识符对应的配置文件。
  • 409 profile_not_running — 配置文件没有打开,所以没有可关闭的对象。
  • 409 stop_failed — 应用未能关闭配置文件。

POST /v1/profiles/{id}/force-stop

已包含在发布版本中

关闭一个关不掉的配置文件

返回同样的响应,但不等待会话收尾。在停止调用没有返回时才使用它,不要当作常规做法。

参数

idpath · string · 必填
配置文件的标识符,与目录操作返回的一致。

响应

{
  "id": "<profile-id>",
  "status": "closed"
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 503 runtime_unavailable — 应用此刻无法启动或停止配置文件。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。
  • 404 profile_not_found — 目录中没有该标识符对应的配置文件。
  • 409 profile_not_running — 配置文件没有打开,所以没有可关闭的对象。
  • 409 stop_failed — 应用未能关闭配置文件。

GET /v1/running-profiles

已包含在发布版本中

此刻打开了什么,哪些可以被操控

每一个已打开的配置文件,无论是窗口还是调用打开的——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"
    }
  ]
}

拒绝

  • 401 invalid_key — 密钥缺失、格式不正确,或者此后已重新生成。
  • 403 not_signed_in — 应用中没有人登录;目录操作需要账号。
  • 403 team_closed — 当前团队已关闭:请续订其套餐或切换到其他团队。
  • 503 catalog_loading — 配置文件目录仍在加载。请再试一次。
  • 503 catalog_unavailable — 应用无法打开配置文件目录。

用你自己的脚本操控配置文件

每个代码块都是完整的:它读取地址文件、调用 API 并连接驱动程序。复制一个,填入你自己的配置文件名称,就能运行。标记说明你今天可以下载的版本是否会响应该代码块所需的一切。

Puppeteer (JavaScript)

已包含在发布版本中

通过 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 (JavaScript)

已包含在发布版本中

通过 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()

Playwright (Python)

已包含在发布版本中

同一件事的 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 (Python)

已包含在发布版本中

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()

Selenium (JavaScript)

已包含在发布版本中

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]。