seimi-render#
这个工程的更原始的版本是SeimiAgent,SeimiRender是SeimiAgent的现代化升级版。基于Chromium的网页渲染服务。提交 URL,以真实的 Chromium 浏览器行为渲染页面(执行 JS、等待异步内容),然后返回渲染后的完整 HTML、pdf、png、json结构化搜素结果。支持长轮询与 WebSocket 推送获取结果。可在后台静默无打扰运行。支持MCP协议,完美适配各类AI Agent工具,拓展你的Harness数据获取边界。也可以是Vibe Coding中AI进行RL迭代自动优化的基础设施。
特性#
- 真实浏览器渲染:基于 QtWebEngine (Chromium),支持 JS 渲染(Ajax)、SPA、动态内容
- HTTP API:远程管理界面,支持提交任务、查询状态、长轮询拉取结果(html / markdown / pdf / image)
- WebSocket 推送:渲染完成实时通知,方便自由组建各种工作流。
- MCP 协议支持:内置 MCP server(端口 8090),Zcode / Codex /Claude Code / Cursor 等 agent 可直接接入调用渲染。任意网络数据读取或者不限次数是使用搜索引擎能力。
- 自带Chrome插件支持Cookie状态和自用Chrome进行1:1同步,确保Agent或者其他自动化场景使用时登录态快速的继承并保持一致。
- 登录态持久化:cookie 加密落盘(
data/cookies.dat),服务重启自动恢复,无需重新同步 - 运行时代理热切换:
--proxy/POST /proxy随时改上游代理,无需重启,可自行拓展引入代理池,彻底避开IP流控。 - 内置 Web 管理后台:监控大盘、渲染测试台、cookie 管理、接口文档、Agent 配置一键复制
- 搜索引擎结果结构化提取:百度/必应/Google 搜索结果去广告结构化,直接返回 JSON
- 运行时监控指标:成功率、延迟分位数(p50/p90/p99)、吞吐、域名分布,适合运维大盘
- 请求异常明细日志:所有失败 / 反爬拦截(含重试后成功)/ 入口拒绝(SSRF、过载)逐条落盘为 JSONL(
data/anomalies/,按天轮转),携带失败阶段、分段耗时、队列负载、代理状态等根因上下文,配合metrics.json聚合可做站点成功率归因与优化决策(详见 请求异常日志) - 网络录制与 HAR 导出:开启
--remote-debugging-port后,渲染时带record_network=true即可录制页面加载全过程(请求/响应/headers/响应体/timing,类似 Chrome F12 Network),产出标准 HAR 1.2 文件,可直接导入 Chrome DevTools / Charles / haralyzer 分析。可以极大的提高自动化信息挖掘效率。 - 页面内 JS 执行(exec_js):渲染任务带
exec_js参数,页面加载 + settle 后在页面主世界执行调用方 JS(同源上下文:cookie 生效、fetch 自动带凭据),执行结果以js_result字段回传(JSON 值;脚本无返回值为 void 则该字段不出现)。裸表达式自动作为结果(控制台补全语义),多语句用return;async/Promise 自动等待;异常/超时则任务失败并精确报错。典型用途:在已登录页面内调站点私有 API、提取运行时状态。 - JSON/REST 直取(
POST /fetch/ MCPfetch_url):带登录态 cookie + 全局代理直接请求 API 端点,返回原始响应体,完全绕过 Chromium 页面管线(整页渲染 ~6s → 直取百毫秒级)。agent 对已登录站点的 REST API 探索(如 Jira/rest/api/2/search)无需再从<pre>里反解 JSON。 - 登录态可见性(final_url / auth_wall):终态透出重定向后的最终 URL;命中登录墙特征(
/login、/sso等路径)时标记auth_wall=true——session 过期渲染出的「登录页」与正常内容可区分,agent 据此提示重新同步 cookie 而非把登录页 markdown 当业务数据。 - 网络失败自诊断:网络类失败的
error附带 proxy 快照、目标解析 IP 与针对性 hint(fake-ip 段劫持、代理够不着内网、代理切换过渡期),agent 读错误即可自修复,不必碰壁求助人工。 - 代理分流(
--proxy-bypass):内网+外网混合目标场景,指定域名/CIDR 直连、其余走代理(QNetworkProxyFactoryper-host 分流)。 - 多种渲染输出:html、markdown、pdf、png/jpg、搜索结果json结构化。
- 原生桌面控制台(
--gui):QML + Material 风格的跨平台本地操作台(概览/设置/更新/日志/登录态五页 + 系统托盘 + 深浅双主题 + 中英双语),带应用内自更新(检查 → sha256 校验 → 自动应用,安装版/绿色版双路径)。默认不启用,无头服务行为零变化;与浏览器管理后台(admin-ui)分工互补——GUI 管本机,admin-ui 管远端。 - 无头运行:默认 offscreen 平台,无需显示器,不会干扰本地浏览器操作,本地操作也不会干扰浏览结果获取。
- 基础防浏览器指纹识别:基础反爬识别能力。
架构#

构建与运行#
安装依赖#
| 平台 | 命令 |
|---|---|
| Windows | scripts\setup-windows.bat |
| Linux | sh scripts\setup-linux.sh |
| macOS | sh scripts\setup-macos.sh |
构建打包#
| 平台 | 命令 | 产物 |
|---|---|---|
| Windows | scripts\package-windows.bat | 绿色版 zip(windeployqt 含 QML 模块);本机装有 NSIS 时额外产出 per-user 安装器 -setup.exe |
| Linux | sh scripts\package-linux.sh | 自包含 tar.gz(qmlimportscanner 收集 QML 模块 + 打包后 GUI 加载冒烟) |
| macOS | sh scripts\package.sh | .app bundle zip(macdeployqt -qmldir 部署 QML 模块) |
打包脚本会自动把 QML 模块(QtQuick/Controls 等)打进分发版并做校验——
--gui的 QML 源码虽编在二进制 qrc 里,但模块实现(样式/控件插件)运行时按 import path 解析,漏拷的表现是--gui白屏(构建期无报错),Windows/macOS 由部署工具扫描、Linux 由脚本静态清单 + 冒烟双保险兜住。
免构建直接下载地址#
https://github.com/zhegexiaohuozi/seimi-render/releases
快速开始#
启动服务(默认无头 offscreen):
MacOs#
build/seimi-render.app/Contents/MacOS/seimi-render --http-port 8088 --ws-port 8089
AI工具使用示例#
标准通用MCP工具配置:
{
"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作为数据获取的基础能力,完成自己预期的闭环数据工作流搭建。
接入方式见上文 AI工具使用示例 章节;配置 JSON 可在管理后台 配置示例 标签页一键复制。截图工具调用名
mcp__seimi-render__*对应McpServer暴露的工具(详见 接口文档 标签页的 MCP 分组)。
手动对接或者自己开发应用对接流程#
一条命令渲染 https://www.sohu.com/ 并拿到 HTML(提交 + 长轮询合一):
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 省略):
{
"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> | 8088 | HTTP 服务端口 |
--ws-port <n> | 8089 | WebSocket 服务端口 |
--mcp-port <n> | 8090 | MCP (Model Context Protocol) HTTP 端口,供 Claude Code / Cursor 等 agent 接入 |
--host <addr> | 127.0.0.1 | 绑定地址;对外暴露用 0.0.0.0 |
--concurrency <n> | 3 | WebEngine 渲染槽并发数(详见下方 并发与吞吐) |
--http-threads <n> | 8 | HTTP 工作线程数 |
--settle-ms <n> | 2000 | 默认 JS settle 延时,loadFinished 后等待 JS 执行的毫秒数(0–30000) |
--load-timeout-ms <n> | 20000 | 单任务总超时(毫秒) |
--windowed | offscreen | 强制原生窗口 QPA 平台;默认 offscreen 无头模式 |
--gui | 关闭 | 启动原生桌面控制台(QML 窗口 + 系统托盘,详见 原生桌面控制台)。不与 offscreen 冲突:显式 --gui 时不注入 offscreen 平台。Windows 桌面双击(零参数、无终端父进程)默认进 GUI |
--headless | — | 强制无头服务模式(--no-gui 同义)。用于从桌面快捷方式显式退出「双击默认 GUI」;终端 / 重定向 / 服务包装器 / 容器启动本来就恒为无头 |
--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 / 提供管理控制台 |
--no-ssrf | 关闭(SSRF 防护开启) | 关闭 SSRF 防护:允许渲染内网/回环/链路本地/云元数据地址。仅供本机自用 Agent 场景(你完全信任调用方可触达整个内网);入口校验与 Chromium 请求层拦截同步关闭。默认开启防护。切勿与对外 --host 组合使用(启动时会打 DANGER 警告) |
--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:port 或 socks5://[user:pass@]host:port(scheme 拼写错误会自动纠正并打 WARN,如 socket5:// → socks5://)。经 QNetworkProxy 设置,支持运行时 POST /proxy 热切换无需重启。type=direct 清除 |
--proxy-bypass <list> | —(不分流) | 逗号分隔的直连目标列表:域名后缀(gwm.cn 覆盖 jira.gwm.cn)/ 精确主机名 / IP 字面量 / IPv4 CIDR(如 gwm.cn,10.0.0.0/8)。命中列表的目标不走 --proxy 直接连接,其余流量走代理——内网+外网混合目标(企业 Agent 常态)一步到位。回环地址永远直连。限制:仅命令行参数;此模式下渲染侧代理在启动时固定(经 Chromium --proxy-server/--proxy-bypass-list flag 实现 per-host 分流——Qt 的代理机制对渲染栈无按 host 分流能力,实测如此),POST /proxy 热切换仅对 /fetch 生效;需要代理热切换就别配 bypass(改 direct + 系统级路由)。另:Chromium flag 代理不支持内联认证凭据。提示:TUN 模式代理(Clash 等)已开时优先不配 --proxy(TUN 已接管路由;再配显式代理会双重分流、丢失代理的私网直连规则) |
--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 累计指标 / anomalies/anomaly-YYYYMMDD.jsonl 请求异常明细日志 / config.json 持久化配置)。容器化或只读部署(二进制目录不可写)时需显式指定到可写卷 |
--apply-update <dir> <appDir> <exe> | — | 内部子命令(更新管理器自动调用,勿手工使用):更新交接子进程模式,做绿色版目录交换与回滚 |
--help | — | 显示帮助信息 |
配置文件(data/config.json)#
除命令行外,服务参数可持久化在 data/config.json(GUI 设置页保存即写此文件;手工编辑亦可)。优先级:内置默认 < config.json < 命令行显式 flag——被命令行覆盖的字段在 GUI 设置页显示「CLI 覆盖」徽标且不可改。文件损坏时自动回退默认值并打告警,不会拒绝启动。
可配置字段即命令行参数全集(host / http_port / ws_port / mcp_port / concurrency / http_threads / settle_ms / load_timeout_ms / password / admin_ui / ssrf_guard / trusted_proxies / proxy / proxy_bypass / stealth / warmup / warmup_url / remote_debug_port),另有 gui.* 段存 GUI 偏好(主题/语言/侧栏折叠/更新通道/manifest 地址)。注意:password 为明文存储(与本文件其它字段一致,靠文件系统权限保护;对安全敏感的部署请改用 --password-file / SEIMI_PASSWORD,二者优先级高于 config.json)。
并发与吞吐(--concurrency)#
--concurrency 是吞吐能力的核心调节阀:每个渲染槽是一个独立的 QWebEnginePage,共享同一个 GUI 线程事件循环,由 Chromium 内部多进程提供真正的 CPU/网络并行。槽数越多,同时能渲染的任务越多,整体吞吐越高——但存在拐点。
实测数据(链接池 80 条媒体文章 — 搜狐/网易/新浪/澎湃 各 20,16 核 / 32 线程机器,每档持续 2 分钟):
--concurrency | 吞吐 (req/s) | 相对提升 | p50 (ms) | p99 (ms) | 成功率 |
|---|---|---|---|---|---|
| 2 | 0.31 | 基准 | 6125 | 9497 | 100% |
| 4 | 0.64 | +106% | 6012 | 8322 | 100% |
| 8 | 1.24 | +300% | 5984 | 8857 | 100% |
| 12 | 1.30 | +319% | 8697 | 12265 | 100% |
| 16 | 1.29 | +316% | 11757 | 17356 | 100% |
| 20 | 1.29 | +316% | 14460 | 18538 | 100% |
浏览器图形化界面可远程管理后台#
seimi-render 内置一个浏览器管理控制台(GET /,访问根路径即打开),把「查看运行状态、测试渲染、管理 cookie、配置 Agent 接入、查接口文档」全收进一个 Web 界面——本地或远程都能用,无需 SSH 进服务器敲 curl。默认开启,--no-admin 可关闭。支持设置鉴权。
控制台各标签页介绍参见下表:
cookie 同步来源见上文 浏览器插件 章节。管理界面是纯静态资源(
admin-ui/),随二进制打包分发,不依赖外部服务。远程访问需显式--host 0.0.0.0并强烈建议配置--password(管理界面可改代理、注入 cookie,是高权限入口)。
原生桌面控制台(--gui)#
除浏览器管理后台外,seimi-render 还内置一个原生桌面控制台:QML + Qt Quick Controls 2(Material 风格)实现的本地操作台,--gui 启动。两者分工——admin-ui 管远端服务器部署,原生 GUI 管本机:
# Windows / Linux 绿色版
./seimi-render --gui
# macOS
./seimi-render.app/Contents/MacOS/seimi-render --gui
| 页面 | 能力 |
|---|---|
| 概览 | KPI(成功率/吞吐/P90 延迟/队列)、渲染负载进度条、代理与 MCP 会话等摘要卡、域名分布 Top-N;侧栏底部实时吞吐曲线(60s 窗口) |
| 设置 | 网络端口与绑定地址、并发/线程/超时、密码、SSRF 与 stealth 开关、数据目录、代理(保存即热生效);被命令行覆盖的字段显示「CLI 覆盖」不可改,需重启的字段带「重启后生效」徽标;保存写入 data/config.json |
| 更新 | 当前版本/构建信息、检查更新 → 下载(进度+速率)→ sha256 校验 → 自动应用;更新说明中英双语展示 |
| 日志 | 运行日志实时流(5000 条环形缓冲)、级别过滤、关键字过滤、自动滚动 |
| 登录态 | 域名→cookie 数量表、清空会话/永久删除(双重确认)、浏览器插件配对指引(地址+token 一键复制) |
- 系统托盘:关窗即最小化到托盘(首次气泡提示),托盘菜单提供显示主窗口 / 打开 Web 控制台 / 检查更新 / 退出。
- 主题与语言:浅色/深色双主题(跟随系统 + 手动切换)、中英双语即时切换,偏好持久化。
- 双击即 GUI(Windows):桌面双击 /
Win+R直接运行 exe(零参数、无终端父进程、无 std 重定向)时默认进入 GUI——桌面用户的自然预期。终端里./seimi-render.exe、重定向/服务包装器(> log)、容器与服务器部署恒为无头服务(启动来源可区分,行为零变化)。要「带参数的快捷方式仍走无头」或显式退出该默认,给快捷方式加--headless(或--no-gui)。 - 不变式:
--gui不影响无头服务——服务/容器/终端启动行为与既往完全一致(offscreen、无窗口、无托盘,仅 Windows 桌面双击默认进 GUI 这一例外)。GUI 模式下服务与渲染能力照常工作(同一进程共享 GUI 线程事件循环)。
应用内自更新#
更新管理器读取一个静态 manifest JSON(默认指向发布站,可在更新页改),schema:
{
"version": "1.4.0",
"channel": "stable",
"notes_zh": "更新说明(中文)",
"notes_en": "Release notes (English)",
"assets": {
"windows-x64": {
"installer": {"url": "https://.../seimi-render-1.4.0-win-x64-setup.exe", "sha256": "...", "size": 123456789},
"portable": {"url": "https://.../seimi-render-1.4.0-win-x64.zip", "sha256": "...", "size": 123456789}
},
"macos-universal": {...}, "linux-x64": {...}
}
}
应用策略自动区分两种安装形态:
- 安装版(NSIS setup 安装,安装目录带
.seimi-install-manifest标记):下载 installer 资产 → sha256 校验 → 重新运行安装器静默重装(NSIS/S,per-user 免 UAC)→ 重启。 - 绿色版(zip/tar 解压直接运行):下载压缩包 → 校验 → 解包暂存 → 拉起
--apply-update交接子进程 → 主进程退出 → 子进程做目录交换(旧目录改.old,新目录上位,失败自动回滚)→ 启动新版本 → 下次启动清理.old残留。
下载校验失败、交换失败均有明确报错并可重试(失败 ≠ 跳过版本)。托管 manifest 的推荐位置是官网仓库(seimi-render-website)随版本发布一并更新。
浏览器插件#
seimi-render 默认Chromum内核渲染,没有你的登录态。要渲染「登录后才看得见」的页面(个人后台、付费内容、内部系统、需登录的搜索结果等),可以先把浏览器里的登录 cookie 同步过来。配套提供 Chrome 插件(chrome-extension/)一键完成。

插件读取浏览器所有 cookie,按域名聚合,你勾选后一键 POST 到 seimi-render 的 /cookies 接口。之后渲染这些域名的页面时,Chromium 会自动带上登录态。
安装(开发者模式加载)#
- 启动 seimi-render 服务(默认
http://localhost:8088)。 - Chrome 打开
chrome://extensions→ 右上角开「开发者模式」。 - 点「加载已解压的扩展程序」→ 选
chrome-extension/目录。 - 工具栏出现 seimi-render 图标,点开即用。
支持 Chrome / Edge / Brave 等基于 Chromium 的浏览器(MV3)。Firefox 的 API 名不同,需自行适配。
操作演示#
- 点工具栏插件图标 → 弹窗自动读取浏览器所有 cookie,按域名聚合显示。
- 顶部确认 seimi-render 端点(默认
http://localhost:8088,会记忆);设了--password时在「访问 token」填入对应 token。 - 右上角徽标显示连接状态(绿色「已连接」/「未连接」/「检测中」),确认插件能连上服务。
- 顶部确认 seimi-render 端点(默认
- 筛选要同步的域名:
- 顶部「全选」一键全选/取消。
- 搜索框输入关键词过滤(几百个域名也能秒级定位)。
- 列表按 cookie 数倒序排列——登录态重的站排最前(如截图中
jd.com 23、aliyun.com 22)。 - 默认全选,建议只勾选需要的域名(别把银行/邮箱等敏感会话也灌进去)。
- 点蓝色「一键同步 (N)」按钮 → 插件把勾选域名的 cookie 批量 POST 到
/cookies,完成后自动对账,显示「已同步 N cookies(服务端共 M)」,N/M 一致即成功。 - 之后 seimi-render 渲染这些域名的页面时自动带上登录态。
「清空服务端」按钮 = 一键调
DELETE /cookies清空 seimi-render 上已同步的 cookie(换账号/调试时用)。
查看服务端已同步的 cookies#
同步后可在 seimi-render 管理界面(GET /)的「Cookie 状态」页查看当前渲染服务持有的 cookie 列表:

- 表格按域名展示「携带 COOKIE 数量」,与插件对账。
- 右上角「清空当前会话」/「永久删除」可分别清除内存中的会话 cookie 与持久化存储。
- 页面提示 cookie 已加密持久化到
data/cookies.dat,重启自动恢复登录态——所以同步一次后,服务重启无需重新同步。
隐私:插件只读 cookie 明文、不存储其他信息;
GET /cookies概览接口只返回「域名→数量」不含 cookie value,防会话泄露;cookie 加密落盘,密钥由编译期 pepper + 机器绑定盐(data/seimi.key)PBKDF2 派生,换机器即失效。详见chrome-extension/README.md。
HTTP API#
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | / | 管理界面 |
| POST | /render | 提交渲染任务(可带 long_poll_ms 同步等结果) |
| POST | /fetch | 轻量直取:带登录态 cookie + 全局代理直接请求 JSON/REST 端点,绕过 Chromium 管线(详见 轻量直取) |
| 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 |
state | pending / running / succeeded / failed |
html | 渲染后的完整 HTML(仅 succeeded 时有) |
markdown | 提取并转换后的 Markdown(请求 output=markdown 且成功产出非空内容时返回) |
md_algorithm_used | 实际内容提取算法,保留原有字段:conservative / readability / raw;有非空 Markdown 时返回 |
markdown_meta | Markdown 诊断对象:extractor、converter、warnings(数组)、conversion_ms;发生回退时附 fallback_reason。仅返回 Markdown 的成功结果包含此字段,即使 Markdown 为空也保留诊断 |
js_result | exec_js 的执行结果(任意 JSON 值:对象/数组/字符串/数字/null)。仅请求了 exec_js 且脚本有返回值时出现;脚本无返回值(void)时该字段不出现;脚本异常/超时则任务 failed,error 以 exec_js 开头 |
error | 失败原因(仅 failed 时有)。反爬拦截类失败的 error 以 blocked: 开头,典型形如 blocked: google /sorry/ page (2 retries exhausted)(命中 Google 验证页 → 自动退避重试 → 重试耗尽判 blocked) |
blocked | 布尔,仅在 state=failed 且失败因反爬拦截时为 true(典型场景:Google /sorry/ 验证页重试耗尽)。配合上面的 error 前缀 blocked:,调用方可显式区分「反爬拦截」与「超时/网络错」两条失败路径 |
final_url | 渲染结束时页面的实际 URL(重定向/SSO 跳转/登录墙跳转可见;成功与失败路径都可能有值,失败时多为错误页或超时瞬间的 URL) |
auth_wall | 布尔,仅 succeeded 时可能出现,为 true 表示渲染出的是登录页(final_url 路径命中登录特征,如 /login、/sso、/signin)。session 过期的典型信号:调用方应提示重新同步 cookie(浏览器插件或 POST /cookies)并重试,而不是把登录页内容当业务数据。保守启发式(仅 URL 特征,不做内容判断),漏报时可用 final_url 自行判断;命中时附 auth_wall_reason(命中的路径段) |
elapsed_ms | 从开始渲染到当前的耗时(ms) |
提交任务(异步)#
提交后立即返回 pending,自行轮询或用 WebSocket 等结果:
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"}
带长轮询(提交后阻塞,直到完成或超时,一步拿结果):
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}
请求参数:
url(必填):http/https 地址output(可选,默认html):html/markdown/pdf/screenshot,支持逗号组合或数组md_algorithm(可选,默认conservative):内容选择策略。conservative清理整页 DOM;readability定位文章正文,识别失败时回退conservativemd_converter(可选,默认turndown):格式转换器。turndown使用 DOM + Turndown/GFM;legacy_cpp使用旧 C++ 转换器做兼容性对比。与md_algorithm独立,仅影响 Markdown;非法值或非字符串返回 400settle_ms(可选,默认 2000):loadFinished后等待 JS 执行的毫秒数(0–30000);搜狐这类内容多的页面建议 2500+long_poll_ms(可选,默认 0):长轮询等待结果的毫秒数(>0 时阻塞至完成或超时,上限 60000)exec_js(可选):页面加载 + settle 后在页面主世界执行的 JS 代码(≤1MB)。代码在 async 函数体内运行:裸表达式片段自动作为结果(如1+1→js_result:2),多语句/含await用return显式回传(结果需可 JSON 序列化,上限 16MB);无返回值 = void(响应无js_result字段)。脚本抛错/超时/语法错误 → 任务failed,error形如exec_js error: <原因>(含精确的语法错误信息)。可用来在已登录页面同源调用站点 API(fetch自动带 cookie)exec_js_timeout_ms(可选,默认 10000,钳制 1000–30000):exec_js执行超时(页面内 Promise.race 兜底,超时的脚本仍在后台运行但任务按失败终结)
exec_js 示例——在已注入登录态的页面里调站点私有接口:
curl -X POST http://localhost:8088/render -H "Content-Type: application/json" -d '{
"url": "https://example.com/console",
"output": "",
"exec_js": "var r = await fetch(\"/api/data\", {credentials:\"include\"}); return await r.json();",
"exec_js_timeout_ms": 15000,
"settle_ms": 3000,
"long_poll_ms": 60000}'
# => {"state":"succeeded","js_result":{...接口返回的 JSON...},"elapsed_ms":...}
Markdown 提取与转换#
默认流程是 内容选择 → DOM 规范化 → Turndown + GFM → Markdown。md_algorithm 继续决定选择整页内容还是文章正文,md_converter 单独决定转换器;两者可任意组合。提取和默认转换都在 Qt WebEngine 的 ApplicationWorld 隔离 JS 世界中完成,使用渲染后的 DOM,不需要 Node.js 运行时。完整 HTML 输出仍保持原页面内容。
legacy_cpp 复用同一份选择、规范化后的 DOM,便于比较转换差异。默认转换器故障时可回退到 legacy_cpp,响应的 markdown_meta.converter 标明实际转换器,warnings 与 fallback_reason 说明原因;Readability 无法识别正文时同样记录回退到 conservative 的原因。共享提取流程不可用时任务失败,不返回看似正常的空结果。
curl -X POST http://localhost:8088/render \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/article","output":"markdown","md_algorithm":"readability","md_converter":"turndown","long_poll_ms":45000}'
成功响应示例(字段节选):
{
"state": "succeeded",
"markdown": "# Article\n\nText...",
"md_algorithm_used": "readability",
"markdown_meta": {
"extractor": "readability",
"converter": "turndown",
"warnings": [],
"conversion_ms": 12
}
}
异步提交后用 /result/:id?output=markdown 获取同样的诊断。WebSocket 的 finished 推送也携带 markdown_meta;MCP 的 render_url / get_web_content 将其作为正文前的诊断信息返回,get_render_result 保留 HTTP 结果 JSON。
查询状态#
curl http://localhost:8088/status/a3f1...
# => {"task_id":"a3f1...","url":"https://www.sohu.com/","state":"running","elapsed_ms":1203}
长轮询拉取 HTML#
适合「先异步提交、稍后再来取结果」的场景:
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(见下)。
轻量直取(POST /fetch)#
「带浏览器登录态的取数服务」:对 JSON/REST 端点直接发请求,不建 Chromium page、不跑 JS、不渲染。自动携带 CookieStore 里目标域的登录 cookie,跟随全局代理(http/socks5)与 --proxy-bypass 分流——与页面渲染网络路径一致,行为可互相印证。适合 agent 对已登录站点的 API 多轮探索(发现字段元数据 → 试 JQL → 翻页),比 render_url + exec_js 页内 fetch 快一个数量级。
curl -X POST http://localhost:8088/fetch \
-H "Content-Type: application/json" \
-d '{"url":"https://jira.example.com/rest/api/2/search","method":"POST","body":"{\"jql\":\"project=CUXV5VR\",\"maxResults\":50}"}'
# => {"url":"...","final_url":"...","status_code":200,"elapsed_ms":180,
# "cookies_applied":6,"headers":{"content-type":"application/json;charset=utf-8",...},
# "body_truncated":false,"body":"{\"expand\":\"schema\",\"issues\":[...]}"}%`
| 参数 | 默认 | 说明 |
|---|---|---|
url | 必填 | http/https 目标(过 SSRF 校验,与 /render 同规则) |
method | GET | GET/POST/PUT/PATCH/DELETE/HEAD |
headers | — | 请求头对象(如 {"Accept":"application/json"})。显式给 Cookie 会覆盖自动注入的登录 cookie;Content-Length/Host/Connection 由网络栈自管,忽略 |
body | — | 请求体字符串(POST/PUT/PATCH;默认按 application/json 发送,可经 headers 覆盖) |
timeout_ms | 15000 | 钳制 1000–60000 |
response_format | text | 响应体输出模式:text(JSON 字符串内嵌,适合 JSON/HTML API;二进制会经 UTF-8 有损转换,勿用于附件,>10MB 截断标 body_truncated)/ base64(二进制安全,字段 body_base64+body_bytes,超过 50MB 返回 413 明确拒绝——base64 再撑大 1/3 对传输不友好,超大文件用 file)/ file(落盘到 data-dir/fetch/,返回 saved_path+bytes,适合大附件) |
filename | URL 末段 | 仅 response_format=file 时的落盘文件名;净化为 basename(仅字母数字 ._-,防路径穿越),同名冲突自动加序号 |
响应:status_code / final_url(重定向后)/ headers(对象,set-cookie 不回显)/ body(text 模式)或 body_base64+body_bytes(base64 模式)或 saved_path+bytes(file 模式)/ cookies_applied(自动携带的 cookie 数,session 是否健康的直观信号——为 0 通常意味着还没同步该域登录态)/ cookies_updated(Set-Cookie 回写条数——服务端轮换的新会话标识自动回流 CookieStore,用得越多会话越新鲜,登录态快照变「活会话」;渲染管线经 applyTo 同步受益;>0 表示本次登录态被续租)/ auth_wall+auth_wall_reason(登录重定向伪装 200 的服务端标记)/ warning(请求带二进制扩展名却收到 text/html 时的交叉校验提示)。HTTP 401/403 按状态码如实透出(不是错误)——这是 session 过期最明确的信号(MCP fetch_url 会对 401/403 附 AUTH REJECTED 提示,指引用户重新同步 cookie),别把 401 当网络故障排查。默认 UA 与 stealth 统一指纹一致。失败(超时 504 / 网络错 502 / SSRF 400 / base64 超限 413)返回 {"error":...,...}。MCP 通道对应工具为 fetch_url(支持 response_format 透传)。
判断拿到的是不是有效数据:REST API session 过期常返回
200 + {}或登录页 HTML——结合cookies_applied与render_url的auth_wall字段判断,必要时先用插件重同步 cookie。
运行时状态(GET /status)#
全局运维视图:自启动以来的累计计数、成功率、渲染延迟分布(min/avg/p50/p90/p99/max)、吞吐、输出类型需求分布、按域名的请求量分布,以及当前队列快照。适合做监控大盘、容量规划、定位哪类站点失败率高。
curl http://localhost:8088/status
# 可带 ?domains=N 控制返回的域名条数(默认 20,上限 200):
curl 'http://localhost:8088/status?domains=50'
返回(已格式化):
{
"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_rate | succeeded / requests,0–1 |
totals.blocked_total | 反爬拦截页检测事件总数(含重试中的每次命中——一次任务命中 sorry 后退避重试,重试再次命中会被记 2 次) |
totals.blocked_recovered | 经历过拦截但最终成功的任务数(重试后跳出 sorry 的任务计入这里,体现 stealth + 预热 + 重试的综合修复力) |
totals.blocked_exhausted | 重试耗尽判 blocked 失败的任务数(对应响应里的 blocked:true)。三者关系:blocked_recovered + blocked_exhausted ≤ 命中过拦截的任务总数;blocked_total ≥ blocked_recovered + blocked_exhausted(含重试中的多次命中) |
latency_ms | 仅成功任务的渲染耗时分布;失败不计入。p50/p90/p99 用对数桶直方图近似(固定内存,不存逐样本) |
throughput_per_sec | requests / uptime(秒) |
outputs.html/markdown/pdf | 各输出类型被请求的次数(位标记统计) |
domains.distinct | 见过的不同 host 总数 |
domains.top[] | 按 total 倒序的 top-N 域名,含各自的 succeeded/failed/success_rate |
domains.top[].blocked | 该域名累计拦截页检测次数(含重试中的每次命中)。定位高风控站点(如 www.google.com 的 blocked 远高于其他域)时最有用 |
资源开销:指标只在任务到达终态(成功/失败)时更新一次,非热路径;延迟用固定 32 桶直方图(O(1) 内存、O(1) 更新);域名映射设 1000 host 上限,超限剔除最冷门者,防止恶意/爬虫场景下 host map 无限膨胀。
GET /status查询时一次性加锁拷贝快照,top-N 排序仅对返回的 N 条做,开销可忽略。
解析渲染结果示例(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"))
输出:
HTML 长度: 221576
标题: 搜狐
链接数: 406
交互时序图#
提交渲染(长轮询一步取结果,最常用):客户端带 long_poll_ms 提交,服务端阻塞至渲染完成(或超时)再返回。
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,后续按需拉取产物。
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 字段。
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
请求异常日志(anomalies)#
每次「请求级异常」都会逐条追加到 <data-dir>/anomalies/anomaly-YYYYMMDD.jsonl(按本地日期轮转,自动清理 30 天前的旧文件,文件权限 0600——URL query 可能含凭据)。每行一个紧凑 JSON 对象,供离线统一分析异常根因、站点成功率归因与优化决策:
kind | 含义 |
|---|---|
failed | 渲染终态失败:超时 / 网络不可达(Chromium 错误页)/ HTTP 4xx-5xx 小 body / 反爬拦截重试耗尽 / 截图失败等全部失败路径 |
blocked_recovered | 渲染期间命中反爬拦截页但退避重试后成功——风控压力信号(再严一步就该失败了),是「成功率下降前兆」 |
rejected | 入口直接拒绝、任务未建立:SSRF 拦截(stage=ssrf_blocked)/ 队列过载背压 503(stage=overloaded) |
每条记录的关键字段:
- 定位:
ts/ts_ms(终态时刻)、task_id、url、host - 根因:
stage(稳定分类标签,见下)、error(失败原因原文)、blocked、block_attempts(拦截页命中次数) - 归因上下文:
queue_wait_ms(提交→开始渲染)/render_ms/total_ms分段耗时;queue_pending/queue_running(终态时队列负载,判断超时是否伴随堆积);proxy(当时生效的代理摘要,不含凭据——区分「代理故障」vs「站点拦截」);settle_ms/outputs(请求参数);version/commit(构建版本,跨版本对比) - 超时失败的
error自带阶段标注(如render timed out after 30000 ms (phase: load)),加载期超时与采集期超时根因不同
stage 取值:antibot_blocked(反爬拦截)、net_unreachable(DNS/连接/SSL 失败)、load_error(HTTP 4xx/5xx 或空 body)、timeout(超时)、screenshot_failed(截图管线失败)、other(未归类,看 error 原文)。
与 metrics.json(全量成功/失败聚合,含域名分布)配合:按 host join 异常明细与聚合比率,即可回答「哪些站点成功率低、低在哪个阶段、是代理问题还是风控问题」,为下一阶段优化(调超时 / 加代理池 / 增并发 / 改进 stealth)提供数据依据。分析示例:
# 各站点失败原因分布(近 7 天)
ls data/anomalies/anomaly-*.jsonl | tail -7 | xargs cat \
| python3 -c "
import sys, json, collections
c = collections.Counter()
for line in sys.stdin:
r = json.loads(line)
if r['kind'] == 'failed': c[(r['host'], r['stage'])] += 1
for (h, s), n in c.most_common(20): print(f'{n:6d} {h:40s} {s}')"
网络录制与 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 客户端。
启用方式#
两步:
- 启动服务时加
--remote-debugging-port:
./build/seimi-render.app/Contents/MacOS/seimi-render \
--http-port 8088 --ws-port 8099 \
--remote-debugging-port 9222
- 渲染时带
record_network: true:
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_network | bool | false | 设为 true 时,本次渲染会录制所有网络流量。未开 --remote-debugging-port 时静默降级(渲染正常,但不产出 HAR)。录制会增加约 50-200ms 的 CDP attach 开销 |
MCP 工具#
Agent 可通过两个 MCP 工具完成完整录制闭环:
# 1. render_url 带 record_network
# agent 调用 render_url 时传 record_network: true
# 返回 task_id 后,HAR 在渲染结束时自动归档
# 2. get_har 取结果
# 用上一步的 task_id 调 get_har,返回 HAR JSON 字符串
| 工具 | 参数 | 说明 |
|---|---|---|
render_url | record_network(bool, 默认 false) | 渲染时录制网络流量,结果用 get_har 取 |
render_url | exec_js(string) / exec_js_timeout_ms(number, 默认 10000) | 页面加载后执行 JS 并把结果作为 [exec_js result] 小节附在文本输出(void 标注 [exec_js result: void]);语义与 HTTP /render 一致 |
get_har | task_id(必填) | 取指定任务的 HAR,返回 HAR 1.2 JSON。归档有 ~0.3s 延时,渲染刚结束立即取可能提示「not available」,稍等重试即可 |
HAR 内容#
产出的是标准 HAR 1.2,每个 entry 包含:
| 字段 | 来源 | 说明 |
|---|---|---|
request | CDP Network.requestWillBeSent | method / url / httpVersion / headers / queryString / postData / bodySize |
response | CDP Network.responseReceived | status / statusText / httpVersion / headers / mimeType / redirectURL / bodySize |
response.content | CDP Network.getResponseBody | size / mimeType / text(响应体,二进制走 base64 encoding)/ compression |
timings | CDP ResourceTiming | send / wait / receive(毫秒) |
_resourceType | CDP type | document / xhr / script / stylesheet / image / font / media / fetch / websocket 等(Chrome 扩展字段) |
_initiator | CDP initiator | 请求发起方(parser / script / other,Chrome 扩展字段) |
_transferSize | CDP encodedDataLength | 线上传输字节数(Chrome 扩展字段) |
startedDateTime | CDP wallTime | ISO 8601 带毫秒(UTC) |
time | timings 之和 | 总耗时(ms) |
响应体默认全量捕获;单条 >1MB 时降级为只留 size 不留 text(省内存,仍保留 mimeType)。
⚠️ 重要:与 stealth(反爬)的互斥#
--remote-debugging-port 开启后,stealth 的 --disable-blink-features=AutomationControlled 标记会被自动跳过——因为该标记会抹除 CDP 痕迹,与录制所需的 CDP 连接冲突。
这意味着:
- 录制模式 = 调试/分析模式:适合开发调试、性能分析、接口逆向,不适合反爬场景
- 其余 stealth 层(UA 统一、stealth.js 指纹补全、高熵 Client Hints)仍生效,减少指纹漂移
- 如需同时反爬渲染 + HAR 录制,建议跑两个实例:一个不带
--remote-debugging-port(满血 stealth),一个带(录制分析)
启动时服务会打印显著警告:
Stealth AutomationControlled flag will be SKIPPED to avoid CDP conflict。
安全提示#
--remote-debugging-port 授予完整的浏览器控制权(可执行任意 JS、读取 cookie、导航到任意 URL)。生产环境部署时:
- 务必绑定
127.0.0.1(Chromium 默认行为,不要--host 0.0.0.0时开启) - 或放在防火墙/VPN 之后,禁止公网直接访问
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#
共 6 个工具,按「语义层级」从高到低排列——agent 优先用高层工具,底层 render_url 兜底所有它覆盖不到的场景:
| tool | 作用 | 必填参数 | 可选参数 |
|---|---|---|---|
browser_search | 关键词搜索:自动构造搜索引擎 URL 并渲染,返回结果列表。没给具体 URL、只想「搜一下/查一下」时优先用 | query | engine(google/bing/baidu/duckduckgo,默认 google)、settle_ms(2500)、timeout_ms(45000) |
get_web_content | 读单篇文章正文:用 readability 提取干净 markdown(去导航/广告/侧栏)。给了具体 URL 要读全文时优先用 | url | md_algorithm(readability/conservative,默认 readability)、md_converter(turndown/legacy_cpp,默认 turndown)、settle_ms(2500)、timeout_ms(45000) |
fetch_url | 轻量直取:带登录态 cookie + 全局代理直接请求 JSON/REST 端点,返回原始响应体(不走浏览器渲染,百毫秒级)。目标是 API/JSON 端点时优先用 | url | method(GET/POST/PUT/PATCH/DELETE/HEAD,默认 GET)、body(请求体,默认按 JSON 发)、headers(请求头对象,显式 Cookie 覆盖登录态)、timeout_ms(15000)、response_format(text 默认/base64 二进制安全——附件下载场景,>50MB 拒绝) |
render_url | 全功能渲染器:任意 URL、任意输出组合。需要 PDF/截图/原始 HTML/手动指定搜索引擎 URL/自定义 settle 时用 | url | output(markdown/html/pdf/screenshot 逗号组合,默认 markdown)、md_algorithm(conservative/readability,默认 conservative)、md_converter(turndown/legacy_cpp,默认 turndown)、format(auto/png/jpg,仅影响截图)、settle_ms(2500)、timeout_ms(45000)、record_network(false,开启 HAR 网络录制,详见 网络录制与 HAR 导出)、exec_js、exec_js_timeout_ms |
get_har | 按 task_id 取网络录制 HAR:配合 render_url 的 record_network=true 使用,返回标准 HAR 1.2 JSON | task_id | — |
get_render_result | 按 task_id 补取已提交任务的结果:render_url 超时返回 running 后轮询,或重新拉取已完成产物 | task_id | output(markdown/html/screenshot/pdf,默认 markdown)、timeout_ms(5000) |
工具选择速查(每个工具的 description 里也写了,agent 会自动判断):
- 用户说「搜一下 / 查一下 / Google it / 帮我了解…」但没给 URL →
browser_search - 用户给了文章/页面 URL 要读正文 →
get_web_content - 目标是 API/JSON 端点(含已登录站点的私有 REST API)→
fetch_url(比渲染快一个数量级,无需 HTML 反解) - 需要 PDF / 截图 / 原始 HTML / conservative markdown / 手动拼搜索引擎 URL / 调 settle / 页面内执行 JS →
render_url - 上一步返回
state=running(慢站点/反爬没渲完)→get_render_result轮询补取
输出格式说明(render_url 的 output 参数):
| 值 | 返回形式 |
|---|---|
markdown(默认) | MCP text content,干净可读文本 |
html | MCP 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/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 共用同一确定性 token(HttpServer::computeToken 派生)。MCP server 跑在独立线程,用恒定时间比较校验 token(防时序侧信道)。
各 agent 客户端通过请求头带 token(具体字段名见各客户端文档,多数支持自定义 headers):
{
"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)中添加:
{
"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:
{
"mcpServers": {
"seimi-render": {
"url": "http://localhost:8090/mcp"
}
}
}
启动参数#
# 默认 MCP 端口 8090
./seimi-render --mcp-port 8090
# 自定义
./seimi-render --http-port 8088 --mcp-port 9090
手动验证 MCP 端点#
MCP 是有状态会话协议(initialize → initialized 通知 → tools/list)。完整流程示例(Streamable HTTP):
# 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 Inspector(
npx @modelcontextprotocol/inspector)可视化调试:URL 填http://localhost:8090/mcp,它自动处理握手/session,可直接点选工具调用。
实现说明#
- MCP 库(cpp-mcp)自带一份 cpp-httplib,与项目用的版本不同。为避免 ODR 冲突(同进程两份不同版本 header-only 库会运行时崩溃),构建时统一用项目那份 httplib——详见
CMakeLists.txt里seimi-mcptarget 的隔离 include path 设计。 - MCP server 跑在独立线程(非阻塞模式,由 cpp-mcp 自管 server + maintenance 线程),HTTP 渲染服务(8088)和 WebSocket(8089)独立工作,三者互不影响。MCP 端口起不来不影响主渲染功能(仅打 warning)。
- session 容量与回收:
max_sessions=64(留多 agent 并发余量),session_timeout=3600(maintenance 线程每 10s 回收空闲超 1 小时的 session)。cpp-mcp 已 patch 为「过期 session 自动重建」——client 带任何 session id 来调工具都会成功并就地沿用其 id,故 session 配置只影响内存堆积、不影响可用性。必须用非阻塞start(false)启动才会起 maintenance 线程,否则 session 只增不减、连满后新连接永久 503。 - 鉴权 patch:启用密码时,cpp-mcp 上游的
set_auth_handler是空 stub,已在 fork 的mcp_server.cpp补上enforce_auth_强制校验,handler 签名(token, path) -> bool。 - MCP 端口绑定与主服务一致:用户未显式
--host时强制回环(127.0.0.1),避免误开公网;需远程接入才显式--host 0.0.0.0。
交互时序图#
MCP 工具不直接碰 WebEngine,而是经 httplib 客户端回调本机渲染 HTTP API(127.0.0.1:8088),再把结果组装成 MCP content(text/image)返回给 agent。
连接握手 + render_url(渲染并取结果):
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 结果。
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。
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)<br/>DOM 规范化 + Turndown/GFM
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正文..."]
Cookie 同步(渲染登录态页面)#
seimi-render 默认用干净的 WebEngine profile 渲染,没有登录态。要渲染「登录后才看得见」的页面(个人后台、付费内容等),先把浏览器里的登录 cookie 同步过来。配套提供 Chrome 插件(chrome-extension/)一键同步。
Chrome 插件一键同步(推荐)#
# 1. 先启动 seimi-render
./build/seimi-render --http-port 8088 --ws-port 8089
- Chrome 打开
chrome://extensions→ 开「开发者模式」→「加载已解压的扩展程序」→ 选chrome-extension/目录。 - 点工具栏 seimi-render 图标,弹窗自动读取浏览器所有 cookie,按域名聚合(默认全选)。
- 勾选要同步的域名(支持全选 / 搜索过滤),点「一键同步」。
- 同步后自动对账显示「已同步 N cookies(服务端共 M)」。
之后渲染这些域名的页面时自动带上登录态。详细见 chrome-extension/README.md。
HTTP 接口#
也可以不用插件,直接调接口(比如从别的工具/脚本同步):
# 批量同步 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 匹配) |
hostOnly | true=仅精确 host(不含子域),false=含子域(domain 自动补前导点) |
secure / httpOnly | 直接映射到 cookie 属性 |
path | 默认 / |
expirationDate | epoch 秒;≤0 视为会话级 cookie(随进程生命周期) |
安全:cookie 仅存内存(
NoPersistentCookies),不落盘;服务重启即清空,需重新同步。GET /cookies只返回域名计数不含 value。同步走本机 HTTP(默认 localhost),cookie 不出本机。
WebSocket 推送#
适合「异步提交、服务端主动通知完成」的场景——避免客户端空轮询。WebSocket 支持两种操作,可全程只用一个连接:
render:直接发渲染请求(提交 URL),服务端受理后回created,渲染完成推送finishedsubscribe:订阅一个已有任务(通常由别的render消息或 HTTP/render创建),完成时推送finished
收到 finished 后,用 HTTP /result/:id 拉取 HTML。典型全程 WS 流程:
客户端 → {"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 —— 提交渲染请求(推荐,一个连接搞定提交+收结果)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | 是 | 固定 "render" |
url | string | 是 | http/https 地址 |
settle_ms | int | 否 | loadFinished 后等待 JS 执行的毫秒数(0–30000,默认 2000) |
output | string | array | 否 | 产物类型,逗号分隔串("html,markdown,pdf,screenshot")或数组(与 HTTP /render 同语义;md/image/png/jpg 为别名)。默认 html |
md_algorithm | string | 否 | markdown 算法:conservative(默认)/ readability(与 HTTP 同语义) |
md_converter | string | 否 | Markdown 转换器:turndown(默认)/ legacy_cpp(与 HTTP 同语义);非法值返回 error |
format | string | 否 | 截图编码:auto(默认)/ png / jpg(与 HTTP 同语义) |
服务端收到后向渲染队列提交任务,自动把本连接订阅到该任务,并立即回 created;渲染完成时推送 finished。真实示例:
{"action":"render","url":"https://www.sohu.com/","settle_ms":2500}
② subscribe —— 订阅一个已有任务(任务通常由 HTTP /render 或另一条 render 消息创建)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
action | string | 是 | 固定 "subscribe" |
task_id | string | 是 | 要订阅的任务 ID(16 位十六进制) |
{"action":"subscribe","task_id":"f1c40ecda5bd41b6"}
一个连接可以反复发送请求切换/订阅不同任务,新订阅不会清除对旧任务的订阅(同一连接可同时关注多个任务)。
服务端 → 客户端:响应与推送#
服务端会发五类消息,用 event 字段区分:
event | 触发时机 | 携带字段 |
|---|---|---|
created | render 请求已受理并提交到渲染队列 | task_id、url |
subscribed | subscribe 请求合法,订阅成功 | task_id |
finished | 订阅的任务到达终态(成功或失败),主动推送 | task_id、state;Markdown 成功结果附 md_algorithm_used、markdown_meta |
authorized | 启用密码且鉴权通过(连接 URL ?token= 或 auth 动作);未启用密码时 auth 动作也会回此 | — |
error | 请求非法(缺字段 / 非 JSON / URL 非法 / 未知 action) | message |
真实示例(通过 WS 渲染搜狐首页的完整交互,全程一个连接):
客户端 → {"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
finished 的 state 取值:
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:
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()
实际运行输出(真实数据):
[受理] task_id=64fe7c39de9e404e
[推送] succeeded
[结果] HTML=221331 字节 标题=搜狐
用例#
若已安装 pip install websockets requests:
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 混用:
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":"..."}订阅同样能在该连接收到finished。render和subscribe可在同一连接混用。
交互时序图#
WS 提交渲染 + 接收完成推送:WS 只推事件(created/finished),产物(html/markdown)仍需 HTTP /result/:id 拉取。
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。
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/、scripts/、chrome-extension/)采用 Apache License 2.0 开源,版权所有 © 2026 wanghaomiao.cn。协议全文见 LICENSE,归属声明见 NOTICE。
第三方依赖#
本项目使用以下第三方组件,其许可协议各自独立、版权声明保留在对应源文件中:
| 依赖 | 用途 | 协议 |
|---|---|---|
| Qt 6(Qt WebEngine / Network / WebSockets 等) | Chromium 渲染核心、网络栈 | LGPL v3(开源选项)/ 商业双授权 |
| cpp-mcp | MCP 协议实现 | MIT |
| cpp-httplib | HTTP 服务 | MIT |
| Turndown / turndown-plugin-gfm | 默认 DOM→Markdown 转换与 GFM 规则 | MIT |
| html2md / table | legacy_cpp 兼容转换与故障回退 | MIT |
| qaes | AES 加密 | Public Domain |
| Mozilla Readability | 正文提取 | Apache License 2.0 |







