SeimiRender / 文档

seimi-render#

📖 文档语言: 简体中文 | [English(https://github.com/zhegexiaohuozi/seimi-render/blob/master/docs/README.en.md) | 项目地址一键直达 | 欢迎Star

这个工程的更原始的版本是SeimiAgent,SeimiRender是SeimiAgent的现代化升级版。基于Chromium的网页渲染服务。提交 URL,以真实的 Chromium 浏览器行为渲染页面(执行 JS、等待异步内容),然后返回渲染后的完整 HTML、pdf、png、json结构化搜素结果。支持长轮询与 WebSocket 推送获取结果。可在后台静默无打扰运行。支持MCP协议,完美适配各类AI Agent工具,拓展你的Harness数据获取边界。也可以是Vibe Coding中AI进行RL迭代自动优化的基础设施。

特性#

架构#

架构图

构建与运行#

安装依赖#

平台命令
Windowsscripts\setup-windows.bat
Linuxsh scripts\setup-linux.sh
macOSsh scripts\setup-macos.sh

构建打包#

平台命令
Windowsscripts\package-windows.bat
Linuxsh scripts\package-linux.sh
macOSsh scripts\package.sh

免构建直接下载地址#

https://github.com/zhegexiaohuozi/seimi-render/releases

快速开始#

启动服务(默认无头 offscreen):

MacOs#

bash
build/seimi-render.app/Contents/MacOS/seimi-render --http-port 8088 --ws-port 8089

AI工具使用示例#

标准通用MCP工具配置:

text
{
  "mcpServers": {
    "seimi-render": {
      "type": "http",
      "url": "http://127.0.0.1:8090/mcp"
    }
  }
}

注意:如果带密码启动,需要增加授权信息。可以去 管理后台 配置示例 标签页直接拷贝。

AI工具使用效果参考#

seimi-render 通过 MCP 协议接入各类 AI agent 客户端后,agent 可用自然语言直接驱动「渲染网页 → 提取正文 → 结构化分析」全流程,无需用户手写 URL 或解析 HTML。下表给出几种典型用法实测(截图取自真实 agent 会话),基于Seimi-render,你可以自由构建任意数据抓取和分析工作流,如:股票舆情、历史K线分析等等,下面是简单的场景作为功能演示。复杂任务可以自定义Skill把seimi-render作为数据获取的基础能力,完成自己预期的闭环数据工作流搭建。

场景说明截图(点击放大)
自然语言驱动渲染在 ZCode 等 agent 客户端里,用户用一句话描述需求(如「我想了解北京种植牙哪些医院靠谱」),agent 自动规划调用 mcp__seimi-render__browser_search 同步等待获取结构化搜素引擎结果,如果判断有必要获得更多信息,再用 get_web_content 获取具体网页完整内容。ZCode 自然语言驱动渲染长流程
搜索结果结构化输出调用browser_search工具,返回结构化搜索结果,方便Agent处理和识别。MCP render_url 返回的 markdown 正文
多步迭代复杂需求下,agent 会拆解为多步:先渲染目标页 → 读取结构化结果 → 再渲染关联页或调用搜索引擎结果提取(百度/必应/Google SERP)→ 综合输出。图中展示 agent 在单次会话内连续调用多次 MCP 工具 + 中间思考的完整链路。ZCode 多步协同渲染与分析

接入方式见上文 AI工具使用示例 章节;配置 JSON 可在管理后台 配置示例 标签页一键复制。截图工具调用名 mcp__seimi-render__* 对应 [McpServer(https://github.com/zhegexiaohuozi/seimi-render/blob/master/src/McpServer.cpp) 暴露的工具(详见 接口文档 标签页的 MCP 分组)。

手动对接或者自己开发应用对接流程#

一条命令渲染 https://www.sohu.com/ 并拿到 HTML(提交 + 长轮询合一):

bash
curl -X POST http://localhost:8088/render \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.sohu.com/","settle_ms":2500,"long_poll_ms":35000}'

返回(已格式化,HTML 省略):

json
{
  "task_id": "a3f1...",
  "url": "https://www.sohu.com/",
  "state": "succeeded",
  "elapsed_ms": 8550,
  "html": "<!DOCTYPE html><html lang=\"zh-CN\"><head><title>搜狐</title>..."
}

实测数据:搜狐首页(https://www.sohu.com/)渲染耗时 ~8.5s,返回 HTML ~220KB,含 44 个 <script>、406 个链接,标题为「搜狐」——即 Chromium 执行完 JS 后的完整 DOM。

命令行参数#

参数默认值说明
--http-port <n>8088HTTP 服务端口
--ws-port <n>8089WebSocket 服务端口
--mcp-port <n>8090MCP (Model Context Protocol) HTTP 端口,供 Claude Code / Cursor 等 agent 接入
--host <addr>127.0.0.1绑定地址;对外暴露用 0.0.0.0
--concurrency <n>3WebEngine 渲染槽并发数(详见下方 并发与吞吐
--http-threads <n>8HTTP 工作线程数
--settle-ms <n>2000默认 JS settle 延时,loadFinished 后等待 JS 执行的毫秒数(0–30000)
--load-timeout-ms <n>20000单任务总超时(毫秒)
--windowedoffscreen强制原生窗口 QPA 平台;默认 offscreen 无头模式
--no-sandbox关闭禁用 Chromium sandbox;WSL2 / 容器 / root 常需开启
--sandbox关闭强制启用 Chromium sandbox(覆盖 root 自动判定)
--verbose-chromium关闭显示 Chromium / web console 日志(默认过滤已知噪音)
--password <pw>管理密码。(不安全:ps/进程列表可见明文,推荐用下面两种方式)
--password-file <f>从文件第一行读取密码(推荐)
SEIMI_PASSWORD 环境变量从环境变量读取密码(推荐)。优先级:--password > --password-file > SEIMI_PASSWORD。不设 = 无密码、开放访问
--no-admin关闭(admin 开启)禁用内置管理界面;默认 GET / 提供管理控制台
--trusted-proxy <list>逗号分隔的受信反向代理 IP/CIDR(如 10.8.0.0/16,127.0.0.1)。设置后 /api/login 限流基于从 X-Forwarded-For 提取的真实客户端 IP;未设置时使用 TCP 对端地址(忽略 XFF,防伪造)
--proxy <url>所有 Chromium 流量的上游代理。格式 http://[user:pass@]host:portsocks5://[user:pass@]host:port。经 QNetworkProxy::setApplicationProxy 设置,支持运行时 POST /proxy 热切换无需重启。type=direct 清除
--no-stealth关闭(stealth 开启)禁用浏览器指纹统一。默认开启 stealth,将所有渲染实例伪装成同一 Chrome 桌面环境(UA/screen/WebGL/canvas),融入人群以绕过基础反爬(如 Google)
--no-warmup关闭(warmup 开启)关闭启动 Google 会话预热。默认开启:进程启动时先以隐藏 page 加载一次 --warmup-url(默认 https://www.google.com/)拿到 NID/CONSENT 等会话 cookie,启动 dispatch 定时器接业务请求——冷启动零 cookie 直打搜索是 Google 风控的最高危窗口,预热能显著降低首条搜索的 sorry 概率
--warmup-url <url>https://www.google.com/会话预热目标 URL(必须 http/https)。仅在 warmup 开启时使用;一般无需改,特殊环境(如走代理时 google.com 不通)可换成能稳定返回 google 域 cookie 的镜像。warmup 自带失败自适应:连续 3 次预热失败(约 90min,Google 不可达)会自动暂停 30min 周期、改用 5min 低频探活;探活成功立即恢复正常周期。状态变化在服务日志里以 [warmup] SUSPENDED / [warmup] RESUMED 标记
--remote-debugging-port <n>—(关闭)开启 Chromium 远程调试端口,启用 HAR 网络录制能力(详见 网络录制与 HAR 导出)。开启后 stealth 的 AutomationControlled 标记会被自动跳过(调试模式与反爬互斥),并显著打印警告。生产环境请勿对外暴露此端口——它授予完整的浏览器控制权
--data-dir <path><binary_dir>/data持久化目录(cookies.dat 加密 cookie / seimi.key 机器绑定盐 / metrics.json 累计指标)。容器化或只读部署(二进制目录不可写)时需显式指定到可写卷
--help显示帮助信息

并发与吞吐(--concurrency#

--concurrency 是吞吐能力的核心调节阀:每个渲染槽是一个独立的 QWebEnginePage,共享同一个 GUI 线程事件循环,由 Chromium 内部多进程提供真正的 CPU/网络并行。槽数越多,同时能渲染的任务越多,整体吞吐越高——但存在拐点。

实测数据(链接池 80 条媒体文章 — 搜狐/网易/新浪/澎湃 各 20,16 核 / 32 线程机器,每档持续 2 分钟):

--concurrency吞吐 (req/s)相对提升p50 (ms)p99 (ms)成功率
20.31基准61259497100%
40.64+106%60128322100%
81.24+300%59848857100%
121.30+319%869712265100%
161.29+316%1175717356100%
201.29+316%1446018538100%

浏览器图形化界面可远程管理后台#

seimi-render 内置一个浏览器管理控制台(GET /,访问根路径即打开),把「查看运行状态、测试渲染、管理 cookie、配置 Agent 接入、查接口文档」全收进一个 Web 界面——本地或远程都能用,无需 SSH 进服务器敲 curl。默认开启,--no-admin 可关闭。支持设置鉴权。

控制台各标签页介绍参见下表:

标签页用途截图(点击放大)
运行时统计监控大盘:运行时长、总请求/成功/失败/成功率、吞吐(req/s)、延迟分布(min/avg/p50/p90/p99/max)、输出类型需求、按域名的请求量与成功率 Top-N、队列快照。适合容量规划、定位高失败率站点。运行时统计
渲染测试台(中文)核心调试工具:输入 URL,可调 settle_ms / 长轮询、勾选输出格式(HTML/Markdown/PDF/截图)、选截图编码与 markdown 算法、站点特定提取(百度/必应/Google SERP)。下图演示必应搜索结果提取出 10 条去广告结果。渲染测试台 - Bing 搜索结果提取
渲染测试台(English)同一界面切到 English(右上角「中」切回中文),演示 Google 搜索结果结构化提取(extract=google_serp)。英文界面 - Google 搜索结果提取
Cookie 状态查看渲染服务持有的登录态 cookie,按域名展示「携带 cookie 数量」(不含 value,防泄露),与浏览器插件对账。「清空当前会话」/「永久删除」分别清内存会话与加密持久化存储(data/cookies.dat)。Cookie 状态
配置示例一键复制 Agent 接入与调用配置:Claude Code / Cursor 的 mcpServers JSON、curl 调 /render 示例、WebSocket 示例,以及当前访问 token(启用 --password 时显示)。配置示例
接口文档交互式 API 文档:左目录按「HTTP REST / MCP 工具 / WebSocket」分组(17 HTTP 端点 + 5 MCP 工具 + WS 的 render/subscribe/auth),右详情含参数表、curl/JSON 示例、响应示例与错误码,均可一键复制。接口文档

cookie 同步来源见上文 浏览器插件 章节。管理界面是纯静态资源([admin-ui/(https://github.com/zhegexiaohuozi/seimi-render/tree/master/admin-ui/)),随二进制打包分发,不依赖外部服务。远程访问需显式 --host 0.0.0.0 并强烈建议配置 --password(管理界面可改代理、注入 cookie,是高权限入口)。

浏览器插件#

seimi-render 默认Chromum内核渲染,没有你的登录态。要渲染「登录后才看得见」的页面(个人后台、付费内容、内部系统、需登录的搜索结果等),可以先把浏览器里的登录 cookie 同步过来。配套提供 Chrome 插件([chrome-extension/(https://github.com/zhegexiaohuozi/seimi-render/tree/master/chrome-extension/))一键完成。

seimi-render 浏览器插件

插件读取浏览器所有 cookie,按域名聚合,你勾选后一键 POST 到 seimi-render 的 /cookies 接口。之后渲染这些域名的页面时,Chromium 会自动带上登录态。

安装(开发者模式加载)#

  1. 启动 seimi-render 服务(默认 http://localhost:8088)。
  2. Chrome 打开 chrome://extensions → 右上角开「开发者模式」。
  3. 点「加载已解压的扩展程序」→ 选 [chrome-extension/(https://github.com/zhegexiaohuozi/seimi-render/tree/master/chrome-extension/) 目录。
  4. 工具栏出现 seimi-render 图标,点开即用。

支持 Chrome / Edge / Brave 等基于 Chromium 的浏览器(MV3)。Firefox 的 API 名不同,需自行适配。

操作演示#

  1. 点工具栏插件图标 → 弹窗自动读取浏览器所有 cookie,按域名聚合显示。
    • 顶部确认 seimi-render 端点(默认 http://localhost:8088,会记忆);设了 --password 时在「访问 token」填入对应 token。
    • 右上角徽标显示连接状态(绿色「已连接」/「未连接」/「检测中」),确认插件能连上服务。
  2. 筛选要同步的域名:
    • 顶部「全选」一键全选/取消。
    • 搜索框输入关键词过滤(几百个域名也能秒级定位)。
    • 列表按 cookie 数倒序排列——登录态重的站排最前(如截图中 jd.com 23aliyun.com 22)。
    • 默认全选,建议只勾选需要的域名(别把银行/邮箱等敏感会话也灌进去)。
  3. 点蓝色「一键同步 (N)」按钮 → 插件把勾选域名的 cookie 批量 POST 到 /cookies,完成后自动对账,显示「已同步 N cookies(服务端共 M)」,N/M 一致即成功。
  4. 之后 seimi-render 渲染这些域名的页面时自动带上登录态。

「清空服务端」按钮 = 一键调 DELETE /cookies 清空 seimi-render 上已同步的 cookie(换账号/调试时用)。

查看服务端已同步的 cookies#

同步后可在 seimi-render 管理界面(GET /)的「Cookie 状态」页查看当前渲染服务持有的 cookie 列表:

Cookie 状态

隐私:插件只读 cookie 明文、不存储其他信息;GET /cookies 概览接口只返回「域名→数量」不含 cookie value,防会话泄露;cookie 加密落盘,密钥由编译期 pepper + 机器绑定盐(data/seimi.key)PBKDF2 派生,换机器即失效。详见 [chrome-extension/README.md(https://github.com/zhegexiaohuozi/seimi-render/blob/master/chrome-extension/README.md)。

HTTP API#

方法路径说明
GET/管理界面
POST/render提交渲染任务(可带 long_poll_ms 同步等结果)
GET/status/:id查询单个任务状态(非阻塞)
GET/status运行时全景:累计计数、成功率、延迟分布、吞吐、域名分布、队列快照
GET/result/:id?timeout=N长轮询拉取 HTML(默认 25s)
GET/har/:id下载任务的 HAR 网络录制(需 --remote-debugging-port + record_network=true,详见 网络录制与 HAR 导出
POST/cookies同步浏览器登录态 cookies(插件用)
GET/cookies已同步 cookie 概览(域名→数量,不含 value)
DELETE/cookies清空已同步的 cookie
GET/stats队列统计(简单快照,向后兼容)

响应字段#

字段说明
task_id任务 ID(16 位十六进制)
url提交的 URL
statepending / running / succeeded / failed
html渲染后的完整 HTML(仅 succeeded 时有)
error失败原因(仅 failed 时有)。反爬拦截类失败errorblocked: 开头,典型形如 blocked: google /sorry/ page (2 retries exhausted)(命中 Google 验证页 → 自动退避重试 → 重试耗尽判 blocked)
blocked布尔,仅在 state=failed 且失败因反爬拦截时为 true(典型场景:Google /sorry/ 验证页重试耗尽)。配合上面的 error 前缀 blocked:,调用方可显式区分「反爬拦截」与「超时/网络错」两条失败路径
elapsed_ms从开始渲染到当前的耗时(ms)

提交任务(异步)#

提交后立即返回 pending,自行轮询或用 WebSocket 等结果:

bash
curl -X POST http://localhost:8088/render \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.sohu.com/","settle_ms":2500}'
# => {"task_id":"a3f1...","url":"https://www.sohu.com/","state":"pending"}

带长轮询(提交后阻塞,直到完成或超时,一步拿结果):

bash
curl -X POST http://localhost:8088/render \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.sohu.com/","settle_ms":2500,"long_poll_ms":35000}'
# succeeded => {"task_id":"...","state":"succeeded","html":"...","elapsed_ms":8550}
# 超时未完成 => {"task_id":"...","state":"running","elapsed_ms":35000}

请求参数:

查询状态#

bash
curl http://localhost:8088/status/a3f1...
# => {"task_id":"a3f1...","url":"https://www.sohu.com/","state":"running","elapsed_ms":1203}

长轮询拉取 HTML#

适合「先异步提交、稍后再来取结果」的场景:

bash
curl "http://localhost:8088/result/a3f1...?timeout=30000"
# 完成时: {"task_id":"...","state":"succeeded","html":"<!DOCTYPE html>...","elapsed_ms":8550}
# 超时未完成: {"task_id":"...","state":"running","elapsed_ms":30000}
# 失败: {"task_id":"...","state":"failed","error":"...","elapsed_ms":...}
# 反爬拦截失败(Google /sorry/ 重试耗尽):
#   {"task_id":"...","state":"failed","error":"blocked: google /sorry/ page (2 retries exhausted)","blocked":true,"elapsed_ms":...}

反爬失败类型化:当渲染 Google 等站点命中验证页(典型为 /sorry/)时,seimi-render 会自动退避重试(全新 page,2 次),重试仍命中则判为 blocked 失败——响应里 error 前缀 blocked: + blocked:true 双标记,调用方可据此显式区分「反爬拦截」与「超时/网络错」,前者建议换引擎/走代理/IP 池,后者只需重试。统计层 /status 同时累加 blocked_exhausted(见下)。

运行时状态(GET /status#

全局运维视图:自启动以来的累计计数、成功率、渲染延迟分布(min/avg/p50/p90/p99/max)、吞吐、输出类型需求分布、按域名的请求量分布,以及当前队列快照。适合做监控大盘、容量规划、定位哪类站点失败率高。

bash
curl http://localhost:8088/status
# 可带 ?domains=N 控制返回的域名条数(默认 20,上限 200):
curl 'http://localhost:8088/status?domains=50'

返回(已格式化):

json
{
  "started_at_ms": 1781876333526,
  "uptime_ms": 1300620,
  "uptime_human": "00:21:40",
  "queue": {
    "total": 2, "pending": 0, "running": 1, "done": 1
  },
  "totals": {
    "requests": 1280, "succeeded": 1244, "failed": 36, "success_rate": 0.972,
    "blocked_total": 14, "blocked_recovered": 9, "blocked_exhausted": 5
  },
  "latency_ms": {
    "min": 980, "avg": 5230.4, "p50": 4120, "p90": 8910, "p99": 15630, "max": 28010
  },
  "throughput_per_sec": 0.985,
  "outputs": { "html": 320, "markdown": 940, "pdf": 20 },
  "domains": {
    "distinct": 87,
    "top": [
      { "host": "www.google.com", "total": 210, "succeeded": 205, "failed": 5, "blocked": 14, "success_rate": 0.976 },
      { "host": "www.sohu.com",   "total": 600, "succeeded": 598, "failed": 2, "blocked": 0,  "success_rate": 0.997 }
    ]
  }
}

上面 totals / domains.top[] 中带 blocked 的字段是 反爬拦截三态计数(与 succeeded/failed 正交,仅在任务到达终态时累加一次):

字段说明:

字段说明
started_at_ms / uptime_ms / uptime_human启动时刻、运行时长(ms 与人类可读)
queue当前队列实时快照(同 /stats
totals.requests/succeeded/failed自启动以来的累计终态计数
totals.success_ratesucceeded / requests,0–1
totals.blocked_total反爬拦截页检测事件总数(含重试中的每次命中——一次任务命中 sorry 后退避重试,重试再次命中会被记 2 次)
totals.blocked_recovered经历过拦截但最终成功的任务数(重试后跳出 sorry 的任务计入这里,体现 stealth + 预热 + 重试的综合修复力)
totals.blocked_exhausted重试耗尽判 blocked 失败的任务数(对应响应里的 blocked:true)。三者关系:blocked_recovered + blocked_exhausted ≤ 命中过拦截的任务总数;blocked_totalblocked_recovered + blocked_exhausted(含重试中的多次命中)
latency_ms仅成功任务的渲染耗时分布;失败不计入。p50/p90/p99 用对数桶直方图近似(固定内存,不存逐样本)
throughput_per_secrequests / uptime(秒)
outputs.html/markdown/pdf各输出类型被请求的次数(位标记统计)
domains.distinct见过的不同 host 总数
domains.top[]total 倒序的 top-N 域名,含各自的 succeeded/failed/success_rate
domains.top[].blocked该域名累计拦截页检测次数(含重试中的每次命中)。定位高风控站点(如 www.google.comblocked 远高于其他域)时最有用

资源开销:指标只在任务到达终态(成功/失败)时更新一次,非热路径;延迟用固定 32 桶直方图(O(1) 内存、O(1) 更新);域名映射设 1000 host 上限,超限剔除最冷门者,防止恶意/爬虫场景下 host map 无限膨胀。GET /status 查询时一次性加锁拷贝快照,top-N 排序仅对返回的 N 条做,开销可忽略。

解析渲染结果示例(Python)#

python
import requests, json

# 提交 + 长轮询一步到位
r = requests.post("http://localhost:8088/render", json={
    "url": "https://www.sohu.com/",
    "settle_ms": 2500,
    "long_poll_ms": 35000,
}).json()

if r["state"] == "succeeded":
    html = r["html"]
    print("HTML 长度:", len(html))
    import re
    title = re.search(r"<title>(.*?)</title>", html, re.S)
    print("标题:", title.group(1).strip() if title else "(无)")
    print("链接数:", html.lower().count("href"))

输出:

text
HTML 长度: 221576
标题: 搜狐
链接数: 406

交互时序图#

提交渲染(长轮询一步取结果,最常用):客户端带 long_poll_ms 提交,服务端阻塞至渲染完成(或超时)再返回。

sequence diagram
sequenceDiagram
    participant C as 客户端
    participant H as HTTP Server<br/>(httplib 线程)
    participant Q as RenderQueue<br/>(线程安全中枢)
    participant W as RenderWorker<br/>(GUI 线程)

    C->>H: POST /render {url, output, settle_ms,<br/>long_poll_ms:35000, md_algorithm?, extract?}
    H->>H: 鉴权 + JSON 解析 + URL/SSRF 校验
    H->>Q: submit(url, settle, outputs, ...) → task_id
    Note over H,Q: long_poll_ms > 0:长轮询阻塞
    H->>Q: waitForCompletion(task_id, 35000)(阻塞当前 httplib 线程)
    Note over W: GUI 线程异步渲染:<br/>load → settle → 提取/采集 → 写结果
    W->>Q: reportSucceeded(id, result)
    Q-->>H: waitForCompletion 唤醒(任务终态)
    H->>Q: snapshot(id)(拷贝结果字段)
    alt succeeded
        H-->>C: 200 {state:succeeded, html?, markdown?,<br/>serp_json?, has_pdf?, has_image?, elapsed_ms}
    else 超时仍未完成
        H-->>C: 200 {state:running, elapsed_ms}(不含产物)
        Note over C: 用 GET /result/:id 继续轮询
    else 渲染失败
        H-->>C: 200 {state:failed, error:"...", elapsed_ms}
    end

异步提交 + 二次拉取(大文件 / 截图 / PDF)long_poll_ms:0 立即返回 pending,后续按需拉取产物。

sequence diagram
sequenceDiagram
    participant C as 客户端
    participant H as HTTP Server
    participant Q as RenderQueue
    participant W as RenderWorker<br/>(GUI 线程)

    C->>H: POST /render {url, output:"html,pdf,screenshot",<br/>long_poll_ms:0}
    H->>Q: submit(...) → task_id
    H-->>C: 200 {task_id, state:pending, elapsed_ms:0}
    Note over C: 不阻塞,做其它事
    Note over W: GUI 线程异步渲染(后台完成,产物存入队列)
    W->>Q: reportSucceeded(id, result)
    C->>H: GET /status/:id(轻量,无产物)
    H->>Q: snapshot(id)
    H-->>C: {state:running, elapsed_ms} / {state:succeeded}
    C->>H: GET /result/:id?output=html&timeout=25000(长轮询拉 HTML)
    H->>Q: waitForCompletion + snapshot
    H-->>C: {state:succeeded, html:"..."}
    C->>H: GET /pdf/:id(拉 PDF 二进制)
    H-->>C: application/pdf(或 404 未请求 / 409 未完成)
    C->>H: GET /image/:id(拉截图二进制)
    H-->>C: image/png | image/jpeg(或 404 / 409)

搜索引擎结果结构化提取(extract=baidu_serp/bing_serp/google_serp:与普通渲染同路径,settle 后走 SERP 提取分支,响应多一个 serp_json 字段。

sequence diagram
sequenceDiagram
    participant C as 客户端
    participant H as HTTP Server
    participant Q as RenderQueue<br/>(线程安全中枢)
    participant W as RenderWorker<br/>(GUI 线程)

    C->>H: POST /render {url:"baidu.com/s?wd=...",<br/>output:html, extract:"baidu_serp", long_poll_ms:50000}
    H->>Q: submit(...) → task_id
    Note over H,Q: long_poll_ms > 0:H 阻塞等 waitForCompletion
    H->>Q: waitForCompletion(task_id)(阻塞)
    Note over W: GUI 线程异步渲染:<br/>load → settle 到期后走 SERP 分支<br/>(注入引擎 JS 在 live DOM 提取去广告结果)
    W->>Q: reportSucceeded(id, result)(含 serp_json)
    Q-->>H: waitForCompletion 唤醒(任务终态)
    H->>Q: snapshot(id)(拷出结果)
    H-->>C: 200 {state:succeeded, html:"...",<br/>serp_json:{results:[...], meta:{ads_filtered:N}}}
    Note over C: JS 缺失/异常时静默降级:<br/>无 serp_json 字段,仅返回 html

网络录制与 HAR 导出(record_network#

seimi-render 支持在渲染时录制页面加载的全过程网络流量(所有 HTTP 请求与响应,含 headers、状态码、响应体、timing),产出 标准 HAR 1.2 文件——与 Chrome DevTools 的 Network 面板「Save all as HAR」功能等价。录制的 HAR 可直接导入 Chrome DevTools、Charles、haralyzer 等工具进行性能分析、接口逆向、故障排查。

工作原理#

启用后,seimi-render 会通过 CDP(Chrome DevTools Protocol) 连接到渲染页面的 Chromium target,订阅 Network domain 事件(requestWillBeSent / responseReceived / loadingFinished 等),按 requestId 聚合为 HAR entry。CdpClient 内嵌在进程内(基于 Qt WebSocket),不依赖任何外部 CDP 客户端。

启用方式#

两步:

  1. 启动服务时加 --remote-debugging-port
bash
./build/seimi-render.app/Contents/MacOS/seimi-render \
  --http-port 8088 --ws-port 8099 \
  --remote-debugging-port 9222
  1. 渲染时带 record_network: true
bash
TASK_ID=$(curl -s -X POST http://localhost:8088/render \
  -H "Content-Type: application/json" \
  -d '{"url":"https://weibo.com/","output":"html","record_network":true,"long_poll_ms":45000}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['task_id'])")

# 渲染完成后取 HAR(finalize 有 ~0.3s 延时,稍等片刻)
sleep 2
curl http://localhost:8088/har/$TASK_ID -o trace.har

HAR 文件保留 10 分钟后自动淘汰(HarStore 的 TTL + 200 份上限 LRU)。

HTTP 接口#

方法路径说明
GET/har/:id下载指定任务的 HAR。成功返回 application/json(HAR 1.2 正文),附 Content-Disposition: attachment; filename="<task_id>.har"。未开 --remote-debugging-port 时返回 503;任务未录制(没带 record_network)或已过期返回 404

POST /render 新增参数#

参数类型默认说明
record_networkboolfalse设为 true 时,本次渲染会录制所有网络流量。未开 --remote-debugging-port 时静默降级(渲染正常,但不产出 HAR)。录制会增加约 50-200ms 的 CDP attach 开销

MCP 工具#

Agent 可通过两个 MCP 工具完成完整录制闭环:

bash
# 1. render_url 带 record_network
#    agent 调用 render_url 时传 record_network: true
#    返回 task_id 后,HAR 在渲染结束时自动归档

# 2. get_har 取结果
#    用上一步的 task_id 调 get_har,返回 HAR JSON 字符串
工具参数说明
render_urlrecord_network(bool, 默认 false)渲染时录制网络流量,结果用 get_har
get_hartask_id(必填)取指定任务的 HAR,返回 HAR 1.2 JSON。归档有 ~0.3s 延时,渲染刚结束立即取可能提示「not available」,稍等重试即可

HAR 内容#

产出的是标准 HAR 1.2,每个 entry 包含:

字段来源说明
requestCDP Network.requestWillBeSentmethod / url / httpVersion / headers / queryString / postData / bodySize
responseCDP Network.responseReceivedstatus / statusText / httpVersion / headers / mimeType / redirectURL / bodySize
response.contentCDP Network.getResponseBodysize / mimeType / text(响应体,二进制走 base64 encoding)/ compression
timingsCDP ResourceTimingsend / wait / receive(毫秒)
_resourceTypeCDP typedocument / xhr / script / stylesheet / image / font / media / fetch / websocket 等(Chrome 扩展字段)
_initiatorCDP initiator请求发起方(parser / script / other,Chrome 扩展字段)
_transferSizeCDP encodedDataLength线上传输字节数(Chrome 扩展字段)
startedDateTimeCDP wallTimeISO 8601 带毫秒(UTC)
timetimings 之和总耗时(ms)

响应体默认全量捕获;单条 >1MB 时降级为只留 size 不留 text(省内存,仍保留 mimeType)。

⚠️ 重要:与 stealth(反爬)的互斥#

--remote-debugging-port 开启后,stealth 的 --disable-blink-features=AutomationControlled 标记会被自动跳过——因为该标记会抹除 CDP 痕迹,与录制所需的 CDP 连接冲突。

这意味着:

启动时服务会打印显著警告:Stealth AutomationControlled flag will be SKIPPED to avoid CDP conflict

安全提示#

--remote-debugging-port 授予完整的浏览器控制权(可执行任意 JS、读取 cookie、导航到任意 URL)。生产环境部署时:

MCP(供 Claude Code / Cursor 等 agent 接入)#

seimi-render 内置一个 MCP(Model Context Protocol)server(默认端口 8090,基于 hkr04/cpp-mcp,Streamable HTTP 传输,符合 2025-03-26 规范)。这让 Claude Code、Cursor 等 AI agent 工具能直接把 seimi-render 当成渲染工具来调用——agent 自动决定何时渲染网页、拿到渲染后的内容。

暴露的 tools#

共 5 个工具,按「语义层级」从高到低排列——agent 优先用高层工具,底层 render_url 兜底所有它覆盖不到的场景:

tool作用必填参数可选参数
browser_search关键词搜索:自动构造搜索引擎 URL 并渲染,返回结果列表。没给具体 URL、只想「搜一下/查一下」时优先用queryengine(google/bing/baidu/duckduckgo,默认 google)、settle_ms(2500)、timeout_ms(45000)
get_web_content读单篇文章正文:用 readability 提取干净 markdown(去导航/广告/侧栏)。给了具体 URL 要读全文时优先用urlmd_algorithm(readability/conservative,默认 readability)、settle_ms(2500)、timeout_ms(45000)
render_url全功能渲染器:任意 URL、任意输出组合。需要 PDF/截图/原始 HTML/手动指定搜索引擎 URL/自定义 settle 时用urloutput(markdown/html/pdf/screenshot 逗号组合,默认 markdown)、md_algorithm(conservative/readability,默认 conservative)、format(auto/png/jpg,仅影响截图)、settle_ms(2500)、timeout_ms(45000)、record_network(false,开启 HAR 网络录制,详见 网络录制与 HAR 导出)
get_har按 task_id 取网络录制 HAR:配合 render_urlrecord_network=true 使用,返回标准 HAR 1.2 JSONtask_id
get_render_result按 task_id 补取已提交任务的结果:render_url 超时返回 running 后轮询,或重新拉取已完成产物task_idoutput(markdown/html/screenshot/pdf,默认 markdown)、timeout_ms(5000)

工具选择速查(每个工具的 description 里也写了,agent 会自动判断):

输出格式说明render_urloutput 参数):

返回形式
markdown(默认)MCP text content,干净可读文本
htmlMCP text content,渲染后的完整 HTML
pdf先拉 /pdf/<id> 二进制 → base64 编码成 text content(agent 解码后存为 .pdf)
screenshot先拉 /image/<id> 二进制 → MCP 原生 image content(base64 + mimeType,agent 可直接显示图片)

支持逗号组合(如 markdown,screenshot):文本部分在前,截图作 image content 跟在后,PDF 作 base64 text 附在末尾。

browser_search 的引擎差异baidu/bing/google 走 SERP 结构化提取(extract=baidu_serp 等),返回去广告的 JSON 结果数组(每条含 title/url/snippet/source,附「相关搜索」);duckduckgo 返回整页 conservative markdown(可能含广告)。命中反爬验证页时返回 BLOCKED 提示并建议换引擎/重试。

工具内部通过本机 HTTP(127.0.0.1:8088)调渲染 API,复用现成的线程安全渲染链路——MCP server 全程不直接碰 WebEngine

鉴权(启用 --password 时)#

seimi-render 启用密码(--password / --password-file / SEIMI_PASSWORD)时,MCP 端点(:8090)同样受保护:每个请求必须带 Authorization: Bearer <token>,token 与 HTTP/WS 共用同一确定性 tokenHttpServer::computeToken 派生)。MCP server 跑在独立线程,用恒定时间比较校验 token(防时序侧信道)。

各 agent 客户端通过请求头带 token(具体字段名见各客户端文档,多数支持自定义 headers):

json
{
  "mcpServers": {
    "seimi-render": {
      "type": "http",
      "url": "http://localhost:8090/mcp",
      "headers": {
        "Authorization": "Bearer <你的 token>"
      }
    }
  }
}

未启用密码时无需此配置(默认 MCP 仅绑 127.0.0.1,不暴露公网)。

Claude Code 接入#

在 Claude Code 的配置(~/.claude.json 或项目 .mcp.json)中添加:

json
{
  "mcpServers": {
    "seimi-render": {
      "type": "http",
      "url": "http://localhost:8090/mcp"
    }
  }
}

确保 seimi-render 正在运行(./build/seimi-render.app/Contents/MacOS/seimi-render)。接入后,在 Claude Code 里可以直接说"帮我渲染 https://www.sohu.com/ 拿到 markdown",Claude 会自动调用 render_url 工具。

Cursor 接入#

Cursor → Settings → MCP → Add new MCP server:

json
{
  "mcpServers": {
    "seimi-render": {
      "url": "http://localhost:8090/mcp"
    }
  }
}

启动参数#

bash
# 默认 MCP 端口 8090
./seimi-render --mcp-port 8090

# 自定义
./seimi-render --http-port 8088 --mcp-port 9090

手动验证 MCP 端点#

MCP 是有状态会话协议(initialize → initialized 通知 → tools/list)。完整流程示例(Streamable HTTP):

bash
# 1. initialize,拿到 session id
SID=$(curl -s -D - -X POST http://localhost:8090/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
  | grep -i "^mcp-session-id:" | sed 's/.*: //I' | tr -d '\r\n')

# 2. 发 initialized 通知(必须,否则 session 未就绪)
curl -X POST http://localhost:8090/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. 列出工具
curl -X POST http://localhost:8090/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# 4. 调用 render_url
curl -X POST http://localhost:8090/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"render_url","arguments":{"url":"https://www.sohu.com/"}}}'

推荐用 MCP Inspectornpx @modelcontextprotocol/inspector)可视化调试:URL 填 http://localhost:8090/mcp,它自动处理握手/session,可直接点选工具调用。

实现说明#

交互时序图#

MCP 工具不直接碰 WebEngine,而是经 httplib 客户端回调本机渲染 HTTP API(127.0.0.1:8088),再把结果组装成 MCP content(text/image)返回给 agent。

连接握手 + render_url(渲染并取结果)

sequence diagram
sequenceDiagram
    participant A as AI Agent
    participant M as MCP Server<br/>(:8090, 独立线程)
    participant H as Render HTTP API<br/>(:8088)

    Note over A,M: 握手(每个请求都带 Authorization: Bearer)
    A->>M: initialize {protocolVersion, clientInfo}
    M-->>A: 200 + Mcp-Session-Id 头<br/>{capabilities:{tools:{listChanged:false}}}
    A->>M: notifications/initialized
    M-->>A: 202
    A->>M: tools/call render_url<br/>{url, output:"markdown,screenshot"}
    M->>H: POST /render {url, output, long_poll_ms:timeout}
    alt 渲染超时
        H-->>M: {state:running, task_id}
        M-->>A: content:[text "still running, call get_render_result"]
    else 渲染成功
        H-->>M: {state:succeeded, markdown:"...", image:"/image/<id>"}
        opt 含截图
            M->>H: GET /image/<id>
            H-->>M: <PNG/JPEG 字节>
        end
        opt 含 PDF
            M->>H: GET /pdf/<id>
            H-->>M: <PDF 字节>
        end
        M-->>A: content:[<br/>{type:text, text:"[meta]\n"+markdown},<br/>{type:image, data:base64, mimeType:"image/png"}]
    end

browser_search(关键词搜索 + 结构化提取):baidu/bing/google 走 extract 结构化路径,返回去广告的 JSON 结果。

sequence diagram
sequenceDiagram
    participant A as AI Agent
    participant M as MCP Server
    participant H as Render HTTP API

    A->>M: tools/call browser_search {query:"Python", engine:"baidu"}
    M->>M: 构建 baidu.com/s?wd=Python
    M->>H: POST /render {url, output:html, extract:"baidu_serp",<br/>long_poll_ms:45000}
    Note over H: 渲染 + 注入 baidu_serp.js<br/>提取去广告结果 → serp_json
    H-->>M: {state:succeeded, serp_json:{results:[...],<br/>meta:{ads_filtered:5}}}
    alt 反爬拦截页
        M-->>A: content:[text "BLOCKED: 验证页,建议重试/换引擎"]
    else 正常结果
        M-->>A: content:[text "[browser_search engine=baidu count=8]\n```json\n[...]\n```\nResults:\n1. **标题** — 来源\n   https://..."]
    end
    Note over A: agent 可对任一结果调 get_web_content 读全文

get_web_content(读单篇文章正文):单 URL → readability 正文提取,返回干净 markdown。

sequence diagram
sequenceDiagram
    participant A as AI Agent
    participant M as MCP Server
    participant H as Render HTTP API

    A->>M: tools/call get_web_content {url:"https://.../article"}
    M->>H: POST /render {url, output:markdown,<br/>md_algorithm:"readability", long_poll_ms:45000}
    Note over H: 渲染 + Readability 正文定位<br/>(非文章页自动回退 conservative/raw)
    H-->>M: {state:succeeded, markdown:"# 标题\n\n正文...",<br/>md_algorithm_used:"readability"}
    M-->>A: content:[text "[get_web_content url=... md=readability]\n\n# 标题\n\n正文..."]

seimi-render 默认用干净的 WebEngine profile 渲染,没有登录态。要渲染「登录后才看得见」的页面(个人后台、付费内容等),先把浏览器里的登录 cookie 同步过来。配套提供 Chrome 插件chrome-extension/)一键同步。

Chrome 插件一键同步(推荐)#

bash
# 1. 先启动 seimi-render
./build/seimi-render --http-port 8088 --ws-port 8089
  1. Chrome 打开 chrome://extensions → 开「开发者模式」→「加载已解压的扩展程序」→ 选 chrome-extension/ 目录。
  2. 点工具栏 seimi-render 图标,弹窗自动读取浏览器所有 cookie,按域名聚合(默认全选)。
  3. 勾选要同步的域名(支持全选 / 搜索过滤),点「一键同步」。
  4. 同步后自动对账显示「已同步 N cookies(服务端共 M)」。

之后渲染这些域名的页面时自动带上登录态。详细见 [chrome-extension/README.md(https://github.com/zhegexiaohuozi/seimi-render/blob/master/chrome-extension/README.md)。

HTTP 接口#

也可以不用插件,直接调接口(比如从别的工具/脚本同步):

bash
# 批量同步 cookies(字段对应 Chrome cookies.getAll() 的输出)
curl -X POST http://localhost:8088/cookies \
  -H "Content-Type: application/json" \
  -d '{"cookies":[
    {"name":"sid","value":"abc","domain":".example.com","hostOnly":false,
     "secure":true,"httpOnly":true,"path":"/","expirationDate":1893456000},
    {"name":"token","value":"xyz","domain":"example.org","hostOnly":true}
  ]}'
# => {"stored":2,"applied":true}

# 概览(域名→数量,不含 value,防会话泄露)
curl http://localhost:8088/cookies
# => {"total":2,"domains":[{"domain":"example.org","count":1},{"domain":".example.com","count":1}]}

# 清空
curl -X DELETE http://localhost:8088/cookies
# => {"cleared":true}

字段说明:

字段说明
name / value必填
domain域;hostOnly=true 时用作 origin host,不设 setDomain(精确 host 匹配)
hostOnlytrue=仅精确 host(不含子域),false=含子域(domain 自动补前导点)
secure / httpOnly直接映射到 cookie 属性
path默认 /
expirationDateepoch 秒;≤0 视为会话级 cookie(随进程生命周期)

安全:cookie 仅存内存(NoPersistentCookies),不落盘;服务重启即清空,需重新同步。GET /cookies 只返回域名计数不含 value。同步走本机 HTTP(默认 localhost),cookie 不出本机。

WebSocket 推送#

适合「异步提交、服务端主动通知完成」的场景——避免客户端空轮询。WebSocket 支持两种操作,可全程只用一个连接:

收到 finished 后,用 HTTP /result/:id 拉取 HTML。典型全程 WS 流程:

text
客户端 → {"action":"render","url":"https://www.sohu.com/","settle_ms":2500}
服务端 ← {"event":"created","task_id":"..."}      # 受理
服务端 ← {"event":"finished","task_id":"...","state":"succeeded"}  # 完成

消息格式#

连接 ws://localhost:8089/(文本帧,JSON 编码)。通信是请求-响应 + 服务端推送模型:客户端发请求,服务端先回一条确认(created/subscribed),之后在任务完成时主动推送 finished

客户端 → 服务端:两种 action#

render —— 提交渲染请求(推荐,一个连接搞定提交+收结果)

字段类型必填说明
actionstring固定 "render"
urlstringhttp/https 地址
settle_msintloadFinished 后等待 JS 执行的毫秒数(0–30000,默认 2000)
outputstring | array产物类型,逗号分隔串("html,markdown,pdf,screenshot")或数组(与 HTTP /render 同语义;md/image/png/jpg 为别名)。默认 html
md_algorithmstringmarkdown 算法:conservative(默认)/ readability(与 HTTP 同语义)
formatstring截图编码:auto(默认)/ png / jpg(与 HTTP 同语义)

服务端收到后向渲染队列提交任务,自动把本连接订阅到该任务,并立即回 created;渲染完成时推送 finished。真实示例:

json
{"action":"render","url":"https://www.sohu.com/","settle_ms":2500}

subscribe —— 订阅一个已有任务(任务通常由 HTTP /render 或另一条 render 消息创建)

字段类型必填说明
actionstring固定 "subscribe"
task_idstring要订阅的任务 ID(16 位十六进制)
json
{"action":"subscribe","task_id":"f1c40ecda5bd41b6"}

一个连接可以反复发送请求切换/订阅不同任务,新订阅不会清除对旧任务的订阅(同一连接可同时关注多个任务)。

服务端 → 客户端:响应与推送#

服务端会发五类消息,用 event 字段区分:

event触发时机携带字段
createdrender 请求已受理并提交到渲染队列task_idurl
subscribedsubscribe 请求合法,订阅成功task_id
finished订阅的任务到达终态(成功或失败),主动推送task_idstate
authorized启用密码且鉴权通过(连接 URL ?token=auth 动作);未启用密码时 auth 动作也会回此
error请求非法(缺字段 / 非 JSON / URL 非法 / 未知 action)message

真实示例(通过 WS 渲染搜狐首页的完整交互,全程一个连接):

text
客户端 → {"action":"render","url":"https://www.sohu.com/","settle_ms":2500}
服务端 ← {"event":"created","task_id":"64fe7c39de9e404e","url":"https://www.sohu.com/"}    # 已受理
服务端 ← {"event":"finished","task_id":"64fe7c39de9e404e","state":"succeeded"}             # 渲染完成
# 之后用 HTTP GET /result/64fe7c39de9e404e 拉取 HTML

finishedstate 取值:

state含义
succeeded渲染成功,HTML 已就绪,可用 /result/:id 拉取
failed渲染失败(加载超时、网络错误、HTTP 4xx/5xx 等),/result/:id 返回的 JSON 含 error 字段

错误响应#

请求不合法时,服务端返回 error 消息。大多数情况下连接保持,可继续发新请求;两类鉴权错误会随后关闭连接(close code 1008 Policy Violation):

客户端发送(错误)服务端返回是否关连接
hello world(非 JSON){"event":"error","message":"invalid json"}
{"action":"render"}(缺 url{"event":"error","message":"missing 'url'"}
{"action":"render","url":"ftp://x"}(非 http/https){"event":"error","message":"url must be http/https"}
{"action":"render","url":"http://10.0.0.1"}(内网/元数据){"event":"error","message":"url blocked by SSRF guard: ..."}
{"action":"subscribe"}(缺 task_id{"event":"error","message":"missing 'task_id'"}
{"action":"foo"}(未知 action){"event":"error","message":"unknown action; expect 'render' or 'subscribe'"}
任务表过载(洪泛/积压){"event":"error","message":"server overloaded, retry later"}
启用密码但未鉴权就发 render/subscribe{"event":"error","message":"unauthorized; send {\"action\":\"auth\",...} or connect with ?token="}是(1008)
{"action":"auth","token":"错"}(token 错误){"event":"error","message":"invalid token"}是(1008)

收到 finished 后,用 HTTP /result/:id 拉取 HTML。

用例:渲染搜狐首页并接收完成推送(Python,纯标准库,可直接运行)#

全程只用一个 WS 连接完成「提交渲染请求 → 收完成推送」,最后用 HTTP 拉一次 HTML:

python
import socket, base64, os, struct, json, re
import urllib.request

HTTP, WS_PORT = "http://localhost:8088", 8089

def ws_connect(port):
    s = socket.create_connection(("127.0.0.1", port), timeout=10)
    key = base64.b64encode(os.urandom(16)).decode()
    s.sendall(f"GET / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\n"
              f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
              f"Sec-WebSocket-Version: 13\r\n\r\n".encode())
    buf = b""
    while b"\r\n\r\n" not in buf:
        buf += s.recv(4096)
    assert b"101" in buf.split(b"\r\n")[0]
    return s

def ws_send(s, text):
    payload, mask = text.encode(), os.urandom(4)
    h = bytearray([0x81]); l = len(payload)
    if l < 126:       h.append(0x80 | l)
    elif l < 65536:   h.append(0x80 | 126); h += struct.pack(">H", l)
    else:             h.append(0x80 | 127); h += struct.pack(">Q", l)
    h += mask
    s.sendall(bytes(h) + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))

def ws_recv(s):
    s.recv(1); ln = s.recv(1)[0] & 0x7f
    if ln == 126: ln = struct.unpack(">H", s.recv(2))[0]
    elif ln == 127: ln = struct.unpack(">Q", s.recv(8))[0]
    buf = b""
    while len(buf) < ln: buf += s.recv(ln - len(buf))
    return buf.decode(errors="replace")

# 1) WS 发送 render 请求(搜狐首页)
ws = ws_connect(WS_PORT)
ws_send(ws, json.dumps({"action": "render",
                        "url": "https://www.sohu.com/", "settle_ms": 2500}))
created = json.loads(ws_recv(ws))                 # {"event":"created","task_id":"..."}
assert created["event"] == "created", created
task_id = created["task_id"]
print(f"[受理] task_id={task_id}")

# 2) 等待渲染完成推送
ws.settimeout(60)
finished = json.loads(ws_recv(ws))                # {"event":"finished","state":"succeeded"}
print(f"[推送] {finished['state']}")

# 3) 用 task_id 走 HTTP 拉取渲染后的 HTML
result = json.loads(urllib.request.urlopen(
    f"{HTTP}/result/{task_id}", timeout=60).read())
html = result["html"]
title = re.search(r"<title>(.*?)</title>", html, re.S)
print(f"[结果] HTML={len(html)} 字节 标题={title.group(1).strip()}")
ws.close()

实际运行输出(真实数据):

text
[受理] task_id=64fe7c39de9e404e
[推送] succeeded
[结果] HTML=221331 字节 标题=搜狐

用例#

若已安装 pip install websockets requests

python
import asyncio, json, re
import websockets, requests

HTTP = "http://localhost:8088"
WS_URL = "ws://localhost:8089/"

async def main():
    # 一个 WS 连接完成:render 提交 → created 受理 → finished 推送
    async with websockets.connect(WS_URL) as ws:
        await ws.send(json.dumps({"action": "render",
                                  "url": "https://www.sohu.com/", "settle_ms": 2500}))
        created = json.loads(await ws.recv())     # {"event":"created","task_id":"..."}
        task_id = created["task_id"]
        finished = json.loads(await ws.recv())    # {"event":"finished","state":"succeeded"}

    # 用 HTTP 拉取渲染后的 HTML
    html = requests.get(f"{HTTP}/result/{task_id}").json()["html"]
    title = re.search(r"<title>(.*?)</title>", html, re.S)
    print(f"搜狐: {title.group(1).strip()} | state={finished['state']} | HTML {len(html)} 字节")

asyncio.run(main())

用例:多任务并发 + 推送(生产场景)#

一个连接连续发多个 render 请求,谁先渲染完谁先收到 finished(不一定等于提交顺序),无需 HTTP/WS 混用:

python
import asyncio, json, re
import websockets, requests

HTTP = "http://localhost:8088"
WS_URL = "ws://localhost:8089/"
URLS = ["https://www.sohu.com/"] * 3   # 同一站点并发 3 次

async def main():
    async with websockets.connect(WS_URL) as ws:
        # 连续发多个 render
        for u in URLS:
            await ws.send(json.dumps({"action": "render", "url": u, "settle_ms": 2500}))
            await ws.recv()                          # 每个先回 created

        # 按完成顺序收 finished
        for _ in URLS:
            msg = json.loads(await ws.recv())        # {"event":"finished","task_id":...,"state":...}
            r = requests.get(f"{HTTP}/result/{msg['task_id']}").json()
            print(f"{msg['task_id']} -> {msg['state']}  HTML {len(r.get('html',''))} 字节")

asyncio.run(main())

若任务由 HTTP /render 创建(比如别的进程提交),用 {"action":"subscribe","task_id":"..."} 订阅同样能在该连接收到 finishedrendersubscribe 可在同一连接混用。

交互时序图#

WS 提交渲染 + 接收完成推送:WS 只推事件(created/finished),产物(html/markdown)仍需 HTTP /result/:id 拉取。

sequence diagram
sequenceDiagram
    participant C as 客户端
    participant WS as WebSocket Server<br/>(GUI 线程信号槽)
    participant Q as RenderQueue
    participant P as RenderPool<br/>(GUI 线程)

    C->>WS: 连接 ws://host:8089/?token=<token>
    alt token 有效
        WS-->>C: {"event":"authorized"}
    else 未带 token
        Note over C,WS: 连接保持,首条消息可发 auth
        C->>WS: {"action":"auth","token":"<token>"}
        WS-->>C: {"event":"authorized"}(或 error + 关闭 1008)
    end
    C->>WS: {"action":"render","url":"https://...","output":"html"}
    WS->>WS: 参数解析 + SSRF 校验
    WS->>Q: submit(url, settle, outputs, ...) → task_id
    WS->>WS: 自动把本连接订阅到该任务
    WS-->>C: {"event":"created","task_id":"...","url":"..."}
    Note over P: GUI 线程异步渲染(不阻塞 WS)
    P->>Q: reportSucceeded(id, result)
    P-->>WS: taskFinished(id) 信号
    WS-->>C: {"event":"finished","task_id":"...","state":"succeeded"}
    Note over C: finished 只含 task_id + state,无产物正文
    C->>WS: (走 HTTP)GET /result/:id?output=html

跨传输订阅:HTTP 提交 → WS 接收推送:一个进程用 HTTP 提交,另一个进程用 WS 订阅同一 task_id。

sequence diagram
sequenceDiagram
    participant HC as HTTP 客户端
    participant H as HTTP Server
    participant WC as WS 客户端
    participant WS as WebSocket Server
    participant Q as RenderQueue
    participant P as RenderPool<br/>(GUI 线程)

    HC->>H: POST /render {url}(long_poll_ms:0)
    H->>Q: submit → task_id
    H-->>HC: {task_id, state:pending}
    WC->>WS: {"action":"subscribe","task_id":"<上面那个 id>"}
    WS-->>WC: {"event":"subscribed","task_id":"..."}
    Note over P: GUI 线程异步渲染(无论谁提交的,任务都在此消费)
    P->>Q: reportSucceeded(id, result)
    P-->>WS: taskFinished(id) 信号
    WS-->>WC: {"event":"finished","task_id":"...","state":"succeeded"}
    Note over WC: 再用 HTTP /result/:id 取产物

License#

本项目(seimi-render 自有源码,位于 [src/(https://github.com/zhegexiaohuozi/seimi-render/tree/master/src/)、[`scripts/`(https://github.com/zhegexiaohuozi/seimi-render/tree/master/scripts/)、[`chrome-extension/`(https://github.com/zhegexiaohuozi/seimi-render/tree/master/chrome-extension/))采用 Apache License 2.0 开源,版权所有 © 2026 wanghaomiao.cn。协议全文见 [LICENSE(https://github.com/zhegexiaohuozi/seimi-render/blob/master/LICENSE),归属声明见 [NOTICE(https://github.com/zhegexiaohuozi/seimi-render/blob/master/NOTICE)。

第三方依赖#

本项目使用以下第三方组件,其许可协议各自独立、版权声明保留在对应源文件中:

依赖用途协议
Qt 6(Qt WebEngine / Network / WebSockets 等)Chromium 渲染核心、网络栈LGPL v3(开源选项)/ 商业双授权
[cpp-mcp(https://github.com/zhegexiaohuozi/seimi-render/tree/master/third_party/cpp-mcp/)MCP 协议实现MIT
[cpp-httplib(https://github.com/zhegexiaohuozi/seimi-render/blob/master/third_party/httplib.h)HTTP 服务MIT
[html2md(https://github.com/zhegexiaohuozi/seimi-render/blob/master/third_party/html2md.cpp) / [table(https://github.com/zhegexiaohuozi/seimi-render/blob/master/third_party/table.cpp)HTML→Markdown 转换MIT
[qaes(https://github.com/zhegexiaohuozi/seimi-render/tree/master/third_party/qaes/)AES 加密Public Domain
[Mozilla Readability(https://github.com/zhegexiaohuozi/seimi-render/tree/master/third_party/readability/)正文提取Apache License 2.0