关于 Reach

本站的代码全部开源,由 4 个仓库组成。下面是各自的说明,README 直接取自 GitHub。

Reach 本体:Next.js 应用,负责镜像 X / YouTube 帖子、发布文章、分享链接与访客行为分析。

Reach

Reach

所不及者,可达于人。 把一条 X / YouTube 的帖子——正文、图片、视频、评论——镜像成一条可控的私密链接;也可以发布自己撰写的文章,并看见访客究竟是怎样阅读它们的。

简体中文 · English

演示站:https://reach.fujioky.com


关联仓库

Reach 由几个独立仓库协作,各仓库自身的说明更完整:

  • reach-upstream — 对 Agent Reach 的改造,包装出 HTTP / MCP 接口,可直接给 Claude、ChatGPT 等 AI 使用(这一侧还特别针对小红书(xhs)优化:把帖子图片内嵌返回,让 AI 能完整读完整篇帖子);同时为 Reach 规范化信息结构。可以通用,不止服务 Reach——Reach 自身只镜像 X / YouTube。Reach 必须先部署它——直接装原版 agent-reach 用不了,部署步骤见其 README
  • reach-dlproxy — 一个支持访问控制与递归解析的简单反向代理服务器,用作 Reach 的视频转发 / 转存通道。可以通用
  • reach-browser-extension — 浏览器扩展,在你正浏览的 X 帖子或 YouTube 视频页一键生成 Reach 分享链接(Chrome / Edge / Firefox)。支持快捷共享

预览

镜像页
镜像页:X 帖子正文与逐段翻译、视频、互动数据、精选评论
首页
首页(浅色主题)
文章页
文章页:目录、阅读进度、封面与正文
归档
文章归档 /post
创建镜像
创建镜像:粘贴链接 → 抓取预览 → 勾选保留的评论
镜像管理
镜像管理:每条内容下的分享链接与访问统计
分享链接
新增分享链接:有效期、总次数、独立访客数、阅后即焚
会话回放
会话回放:按访客视口还原,附点击热力图
文章管理
文章管理
素材管理
素材管理:引用状态、公开分享、清理未引用
人机验证
访客页的 Cloudflare Turnstile 门
浏览器扩展
浏览器扩展:弹窗里一键分享(访问控制、访问密码、历史记录),设置页用管理员账号登录

功能

镜像 —— 贴上帖子链接,得到一份自托管副本。

  • 通过上游 Agent Reach 接口抓取 X(Twitter)帖子与 YouTube 视频,统一归一化为同一套内容模型:标题、正文、作者、媒体、互动数据、评论。
  • 视频重新托管:可经本站转发(/api/proxy-video)、经外部反向代理转发(参考实现 fujioky/reach-dlproxy),或上传到任意 S3 兼容存储桶(Cloudflare R2、AWS S3、MinIO)并从自定义域名分发。上游取流按有界分块进行,各通道之间自动故障转移。
  • 分享链接(/s/<token>)支持限期、限次、阅后即焚。每条镜像保留版本历史,刷新前可预览差异,随时回滚。
  • 字幕:播放器内直接显示最佳字幕轨,非中文字幕逐条经 DeepL 翻译;正文与评论同样按需翻译,结果缓存在数据库。

文章 —— 用 Markdown 写自己的内容。

  • 左右分栏编辑器带实时预览,拖拽 / 粘贴即上传,全站共用素材库。
  • 图片存 Vercel Blob,视频经分块 multipart 直传 S3 存储桶、每块独立重试——都是浏览器直传,不经过 Serverless 函数。
  • 远程转存:粘贴图片 / 视频链接(或页面地址),服务端把媒体转存到自己的存储;手写规则解析不出媒体地址时,可选用 LLM 解析器兜底。
  • 公开固定链接(/p/<slug>)、归档页(/post)、访客评论与后台审核、两种封面版式、响应式 WebP 变体、逐篇密码保护。

数据分析 —— 自建,不引入第三方脚本。

  • 每个访客页面用 rrweb 录制 DOM 变化,同时采集结构化事件:浏览、分块停留时长、滚动深度、点击、媒体播放、外链点击、视频播放 / 暂停 / 拖动 / 进度。
  • 后台看板:总览趋势、单条内容详情、按访客视口尺寸还原的会话回放、叠加在真实页面快照上的点击热力图。
  • 上报接口无需登录,但层层设防:请求体上限、来源校验、schema 校验、内容存在性校验、按访客限流。地理位置按 IP 解析并按地址缓存。

浏览器扩展 —— fujioky/reach-browser-extension(Chrome / Edge / Firefox)。

  • 在 X 帖子或 YouTube 视频页点图标、按快捷键,或在帖子链接上右键,即可生成分享链接并自动复制,可带有效期、限次、阅后即焚和访问密码。
  • 已经镜像过的帖子直接在原镜像上新建分享链接,不重复抓取;新帖子走与后台向导相同的创建流程,保留全部评论。
  • 扩展用管理员账号登录,换得一个独立令牌(只存哈希),后台「系统 → 浏览器扩展」可逐个撤销。详见下文浏览器扩展

运维

  • 公开状态页(/status)带延迟迷你图,数据来自每日 cron 采样;后台健康面板探测视频代理、S3 存储桶、Agent Reach 与 DeepL。
  • 访客页 Cloudflare Turnstile 人机验证门、可选的全站内容密码、PWA manifest 与 Service Worker、亮 / 暗色主题。

访问记录与会话回放

访客打开镜像页(/s/<token>)或文章页(/p/<slug>)时,页面会挂载一个录制器(app/_components/analytics/Recorder.tsx),一次打开就是一条会话。它记录三类东西:

类别内容落库
DOM 录像rrweb 的完整快照 + 增量变更;鼠标移动 60ms、滚动 150ms、媒体 800ms 采样analytics_chunks(按 seq 排序的 jsonb 批次)
结构化事件viewdwell(各内容块可见时长)、scroll(最大深度)、click(页面坐标 + 文档/视口尺寸)、media_clickmedia_playoutlink_clickvideo(播放 / 暂停 / 拖动 / 每 10% 进度 / 全屏 / 结束 / 倍速)visit_events
会话元数据IP、User-Agent、屏幕与视口尺寸、DPR、语言、来源页、按 IP 解析并缓存的国家 / 地区 / 城市、累计时长与事件计数analytics_sessions

录制器每 5 秒把缓冲批量发到 /api/analytics/ingest,缓冲超过 400 条 rrweb 事件时提前发送;页面关闭时用 sendBeacon 做最后一次提交,失败的批次留在发件箱下次重试,会话元数据在被确认前随每批重发,上报接口的写入是幂等的。视频事件在 document 捕获阶段统一采集,任何 <video>(含 Plyr)都能覆盖,不用给播放器埋点。

后台能看到的:

  • /admin/analytics/sessions —— 会话列表(时间、内容、地区、设备、时长、事件数)。
  • /admin/analytics/session/<id> —— 会话回放:rrweb-player 按访客当时的视口尺寸还原,旁边是行为时间轴;回放里的视频与录像进度同步。
  • /admin/analytics/content/<id>/heatmap —— 点击热力图:取一条真实会话的录像渲染出页面快照,把该内容全部会话的点击叠上去,按桌面 / 移动视口分桶。
  • /admin/analytics/content/<id> —— 单条内容的统计:趋势、分块停留、滚动深度漏斗、视频进度漏斗与暂停位置、点击目标、媒体与外链图表。

几点要知道的:

  • 文章媒体地址带 6 小时签名令牌,录像里记下的是访客当时的 URL;回放接口下发前会重签令牌(lib/analytics/resign-media.ts),所以旧会话的图片与视频照常显示,存储的数据不动。
  • 上报接口无需登录,但有请求体上限、来源校验、schema 校验、内容存在性校验、按访客限流。访客靠 HttpOnly 的 visitor_id cookie 区分。
  • 录像不打码输入框(maskAllInputs: false),访客在评论框里输入的内容会进入录像。
  • 没有关闭录制的开关,也没有自动清理:录像随内容项级联删除,长期运行要留意 analytics_chunks 的体积。app/privacy-policy 页面的文案要与你实际的采集范围保持一致。

浏览器扩展

fujioky/reach-browser-extension 把「打开后台 → 粘贴链接 → 等抓取 → 复制分享链接」压成一步:在 X 帖子或 YouTube 视频页直接生成分享链接,自动复制到剪贴板。支持 Chrome / Edge / Firefox。

浏览器扩展:左为弹窗,右为设置页

安装与连接

  1. 部署本仓库并执行数据库迁移(npm run db:migrate,扩展需要其中的 api_tokens 表)。
  2. 从扩展的 Releases 下载 zip。Chrome / Edge:解压到固定文件夹,在 chrome://extensions 打开开发者模式,「加载已解压的扩展程序」选这个文件夹。Firefox:zip 未签名,可在 about:debugging 临时载入,长期使用需到 AMO 以「不公开」方式签名。
  3. 打开扩展设置,填入 Reach 地址,用管理员账号登录。Reach 验证密码后签发一个只给这个浏览器用的令牌,扩展不保存密码。后台「系统 → 浏览器扩展」页面提供可复制的 Reach 地址,并列出所有登录过的扩展和最近使用时间,可以逐个撤销。

使用

  • 在帖子页点扩展图标或按 Alt+Shift+S 后回车;或者在时间线里的帖子链接上右键「用 Reach 分享此链接」,不用打开帖子。分享在扩展后台完成,弹窗可以随时关,完成后自动复制链接。
  • 每次分享可设链接的有效期(不限 / 1 / 7 / 30 天)、最多打开次数、阅后即焚,以及镜像的访问密码(系统密码 / 单独密码)。独立访客数、精确到期时间仍在后台对链接编辑。
  • 弹窗先显示当前帖子的分享(twitter.com / x.com、youtu.be / watch 等不同写法按同一条帖子识别),其余分享收在「历史记录」里。

接口

接口作用
POST /api/extension/session用管理员账号密码换令牌
GET /api/extension/session检查令牌是否仍然有效
DELETE /api/extension/session退出登录,作废当前令牌
POST /api/extension/share帖子链接进、分享链接出,NDJSON 流式返回进度

几点要知道的:

  • 同一条帖子(按平台 + 帖子 ID 识别)已经有镜像时,只在这份镜像上新建分享链接,不重新抓取;要更新内容,到后台对镜像「重新抓取」。新帖子与后台「创建镜像」走同一套创建逻辑(lib/mirror/create.ts),保留全部评论,视频转存放到响应返回之后进行。
  • 分享时设置的访问密码写在镜像上,与后台镜像详情页的密码设置是同一个:这份镜像已有的分享链接也会随之需要这个密码。不设置则不改动镜像原有的密码;选「系统密码」而系统设置里还没有密码时,接口会直接拒绝,避免给出一个其实没加密的链接。
  • api_tokens 只存令牌的 SHA-256。这些接口对任意来源开放 CORS——它们只认令牌(登录接口认密码本身),从不读 Cookie,所以不会被别的网站借用管理员会话。登录接口和后台登录页一样,没有做限流。
  • 分享接口之所以流式返回,是因为 Chrome 会终止 30 秒内收不到 fetch 响应的扩展 service worker,而抓取一条新帖子经常超过 30 秒。流在没有 done 行时就结束,按失败处理。

技术栈

选型
框架Next.js 16(App Router,Turbopack)、React 19、TypeScript
样式Tailwind CSS 4,自定义设计令牌(/design-system
数据库PostgreSQL + Drizzle ORM(生产环境用 Neon / Vercel Postgres)
认证Auth.js v5 Credentials 提供者、bcrypt、JWT 会话
存储Vercel Blob(图片)、任意 S3 兼容存储桶(视频)
媒体Plyr 播放器、sharp 生成图片变体、rrweb / rrweb-player 回放
图表Recharts
测试Vitest

本地运行

前置条件:Node.js 24、一个 PostgreSQL 数据库、一个 Vercel Blob 存储、一个 Agent Reach 接口(见下文)。

git clone https://github.com/fujioky/reach.git
cd reach
npm install
cp .env.local.example .env.local   # 填入 POSTGRES_URL、AUTH_SECRET、BLOB_READ_WRITE_TOKEN、AGENT_REACH_*
npm run db:migrate
npm run dev

打开 http://localhost:3000/admin/login。用户表为空时,登录页会变成一次性的初始化表单,创建唯一的管理员账号。其余配置——视频代理、S3 存储桶、DeepL 密钥、AI 解析器、内容密码——都在运行时于 后台 → 系统设置 中完成。

运行测试:npm test

环境变量

变量必需用途
POSTGRES_URLPostgres 连接串(drizzle-kit 也用它)
AUTH_SECRETAuth.js 密钥(openssl rand -base64 32
BLOB_READ_WRITE_TOKENVercel Blob 令牌,存图片与头像
AGENT_REACH_BASE_URL是*上游 Agent Reach 接口地址
AGENT_REACH_PWD是*上游 Agent Reach 口令
NEXT_PUBLIC_SITE_URL站点规范源:分享链接、OpenGraph、分析上报来源校验;缺省取 Vercel 生产域名
CRON_SECRET保护 /api/cron/*;Vercel 定时任务会自动带上
AI_PARSER_API_KEY可选 LLM 媒体解析器的密钥
TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY开启访客页的 Cloudflare Turnstile 门;两者留空即关闭

* 也可以在后台设置中填写;环境变量优先级更高。

Agent Reach

Reach 自身不抓取平台内容,而是调用一个封装了 Agent Reach 工具链的 HTTP 服务。直接部署上游是不够的:上游是给 AI Agent 用的本地能力层,没有任何包装 API。请部署 fujioky/reach-upstream 里的代理(proxy/,见其 README)。它加入了 Reach 依赖的 X/Twitter 与 YouTube 定制解析——统一内容结构、全部渐进式 YouTube 视频源、带时间轴的 VTT 字幕、回复串、结构化错误类型——并把工具链通过下面这个接口和一个带 OAuth 的 MCP 服务(可接入 ChatGPT / Claude 连接器)公开出去:

GET {base}/healthz                                   → { "ok": true, ... }
GET {base}/http/?platform=x|youtube&query=<url>&pwd=<pwd>
                                                     → { "ok": true, "item": { ... }, "errors": [] }

item 包含帖子正文、作者、媒体(YouTube 附带全部 yt-dlp 视频源)、互动数据、评论,以及可选的 transcript / transcript_lang / transcript_vtt 字段。lib/fetcher/platforms/ 中的适配器负责归一化,错误类型映射见 lib/fetcher/errors.ts。限流(429)与网络错误会按指数退避重试。

部署到 Vercel

  1. 从本仓库创建 Vercel 项目(框架预设 Next.js,Node 24)。
  2. 挂载一个 Postgres 数据库(Vercel 市场里的 Neon 开箱即用)和一个 Blob 存储;Vercel 会自动注入 POSTGRES_URLBLOB_READ_WRITE_TOKEN
  3. 添加 AUTH_SECRETAGENT_REACH_BASE_URLAGENT_REACH_PWD,按需添加 NEXT_PUBLIC_SITE_URL 与 Turnstile 密钥。
  4. 对生产数据库执行一次迁移:POSTGRES_URL=... npm run db:migrate
  5. 部署。vercel.json 已配置每日健康采样的 cron。
  6. 访问 /admin/login 创建管理员账号,然后填写视频代理 / 存储设置。

vercel CLI 上传的是工作目录而非 git 提交;.vercelignore 负责把本地缓存和媒体文件排除在外。

目录结构

app/
  admin/(shell)/      后台:镜像、分享、文章、素材、分析、设置
  admin/login/        登录与首次初始化
  api/                路由处理器:抓取、视频代理、文章媒体、分析上报、健康检查、cron……
  api/extension/      浏览器扩展接口:登录换令牌、流式快捷分享
  s/[token]/          镜像访客页
  p/[slug]/, post/    文章页与归档页
  status/             公开状态页
lib/
  fetcher/            Agent Reach 客户端与平台适配器(X、YouTube),帖子链接解析
  mirror/             镜像创建(后台向导与扩展共用)、快捷分享、刷新与版本
  video/              分块上游取流、代理故障转移、播放地址解析
  storage/, blob/     S3 multipart 与预签名、Vercel Blob 辅助
  article/            Markdown、素材库、远程转存(含 SSRF 防护)、签名令牌
  analytics/          聚合查询、地理位置、回放媒体重签
  access/, content/   分享访问控制、密码门
  auth/               管理员凭据校验、扩展令牌
  health/, settings/  健康探测、app_settings 类型化访问层
drizzle/migrations/   SQL 迁移(drizzle-kit)

AGENTS.md 汇总了改代码时需要知道的非显性行为与陷阱;docs/article-publishing.md 详细记录了文章发布链路。

fujioky/reach-upstream

在 GitHub 打开 ↗

Agent Reach 的分支,proxy/ 目录里是 Reach 依赖的 HTTP / MCP 代理:在上游之上补了 X / YouTube 的定制解析和公开访问接口。

Agent Reach HTTP/MCP 代理

English · 简体中文

Reach 对接的就是这个目录。上游 Agent Reach 是给 AI Agent 用的「能力层」:负责安装、体检各平台 CLI(yt-dlptwitter-clixhs、经 mcporter 的 Exa 等),让 Agent 直接调用,有意不做任何包装 API。所以直接部署上游是没法给 Reach 用的,这一层由本代理补上。

代理补了什么

1. 针对转存场景定制的 X / Twitter 与 YouTube 解析。 上游把原始 CLI 输出直接交给 Agent;代理把它整理成产品能存、能渲染的统一结构:

  • idurltitlecontentauthor {id, name, handle, url, avatar_url}created_at(ISO 8601)、stats {likes, comments, reposts, views, collects}images[]comments[]
  • X:推文 + 回复串(至多 COMMENT_LIMIT 条),CLI 只给 handle 时也能补全作者,媒体地址保留 video.twimg.com 视频,视频若带字幕轨则一并提取。搜索返回同样精简的推文。
  • YouTube:全部渐进式 video_sources[](清晰度、url、ext、format_id、大小、有无音视频,DASH 仅作兜底),供调用方挑一路流转存;--write-comments 抓评论;从频道页抓头像;duration;缩略图。
  • 字幕是数据,不是一坨文本transcript(纯文本)、transcript_langtranscript_vtt——规范化后的带时间轴 WebVTT(去头部、cue id、行内标签、滚动重复行),可直接喂给 <track>。优先级:人工中文 › 自动中文(仅中文音频)› 人工英文 › 自动英文 › 原生语言轨;YouTube 的自动翻译轨有意排除,因为质量远不如拿原文在下游翻译。
  • 结构化错误:上游任何失败都变成 {stage, kind, message}kindrate_limited | not_found | auth_required | unsupported | not_configured | timeout | internal,并映射到对应 HTTP 状态。原始 stderr 不出机器,客户端可按 kind 分支(重试、显示「已删除」、提示登录)。

2. 公开访问。 上游设计上只在本机使用。代理用两种方式把工具链开放出去:

  • GET /http/?platform=x|youtube|xhs|web&query=<链接或文本>&pwd=<口令> + GET /healthz——Reach 所用的朴素 JSON 契约,口令保护。
  • MCP 服务 /mcp(Streamable HTTP,无状态 JSON):OpenAI 兼容形态的 search / fetch,外加 reachagent_reachagent_reach_health。托管连接器需要的都配齐了:Bearer / API-key 鉴权、OAuth 2.1 授权码 + PKCE 与动态客户端注册(内置信任 ChatGPT 与 Claude 回调)、.well-known 元数据、授权页可选 Cloudflare Turnstile、面向 ChatGPT 净化过的工具描述、DNS 重绑定防护。把 https://<你的域名>/mcp 加为连接器,整套工具链就能脱离 Reach 在 ChatGPT / Claude 里直接用。

3. 顺带包含。 小红书(xhs:短链展开、笔记 + 评论、图片 base64 内联并按变体 / 字节 / 感知哈希去重、超限压缩)、网页(链接走 Jina Reader,搜索走 Exa / mcporter)、每次图片下载都过 SSRF 防护(仅公网 IP,跳转逐跳复验),以及隔离运行时:scripts/reach.shHOME 和 XDG 目录钉在 runtime/home,所有 CLI 装进 runtime/venv,命令壳在 runtime/bin,不碰真实家目录、不注册全局命令。

运行

前置:Python 3.10+、uv;仅 platform=web 的文本搜索需要 Node.js + mcporter 与 Exa MCP 配置(config/mcporter.json)。

git clone https://github.com/fujioky/reach-upstream.git
cd reach-upstream/proxy
cp .env.example .env            # 填 AGENT_REACH_PROXY_PASSWORD
./scripts/reach.sh setup        # runtime/venv:agent-reach(来自 ../)、yt-dlp、twitter-cli、xiaohongshu-cli、mcp……
./scripts/reach.sh cmd doctor   # 在隔离的 runtime/home 里跑上游体检
./scripts/reach.sh start
./scripts/reach.sh health

需要登录态的平台,凭据要放进隔离家目录而不是你自己的:

  • Xtwitter-cli 读浏览器 Cookie,按上游 docs/cookie-export.md 操作,并通过 ./scripts/reach.sh cmd …runtime/bin/… 运行 CLI,凭据才会落在 runtime/home
  • 小红书runtime/bin/xhs login
  • YouTube 与网页不需要任何配置。

对外暴露方式随意(Cloudflare Tunnel、nginx、Tailscale)。然后把 Reach 指过来:AGENT_REACH_BASE_URL=https://your-proxy.example.comAGENT_REACH_PWD=<口令>,或在 后台 → 系统设置 → Agent Reach 接口 填同样两项。

其它命令:run(前台)、stoprestartstatuslogs [n]sync-commandscmd <agent-reach 参数>

接口

GET /healthz                          → { "ok": true, "service": "agent-reach-proxy", "platforms": [...], ... }
GET /http/?platform=<p>&query=<q>&pwd=<口令>
    链接查询 → { "ok": true, "type": "item",   "item": {...},   "errors": [] }
    文本查询 → { "ok": true, "type": "search", "results": [...], "errors": [] }
    失败     → { "ok": false, "errors": [{ "stage": "youtube_info", "kind": "not_found", "message": "..." }] }(按 kind 给 4xx/5xx)
POST /mcp                              Streamable HTTP MCP(Bearer 或 OAuth)
GET  /.well-known/oauth-*              OAuth 元数据;/oauth/register、/oauth/authorize、/oauth/token

platform 别名:twitterxxiaohongshu/rednotexhsytyoutube

配置

除口令外都可选,完整清单见 .env.example。要点:

默认含义
AGENT_REACH_PROXY_PASSWORD必填。未设置时 /http/ 一律回 503。
AGENT_REACH_PROXY_PUBLIC_URLhttp://127.0.0.1:18280对外地址,用于 MCP OAuth 资源元数据。
AGENT_REACH_MCP_TOKEN同口令/mcp 的静态 Bearer;AGENT_REACH_MCP_AUTH_REQUIRED=0 关闭鉴权(仅本地)。
AGENT_REACH_MCP_PUBLIC_DISCOVERY1允许未鉴权的 initialize / tools/list
AGENT_REACH_TURNSTILE_SITE_KEY / _SECRET_KEYOAuth 授权页加 Turnstile 验证。
AGENT_REACH_PROXY_SEARCH_LIMIT / _COMMENT_LIMIT15 / 50搜索结果与评论上限。
AGENT_REACH_MCP_IMAGE_MAX_BYTES / _DOWNLOAD_MAX_BYTES4 MB / 32 MB内联图片压缩目标与原图下载上限。
AGENT_REACH_RUNTIME_DIR_VENV_BIN_DIR_HOME_NODE_BINruntime/ 之下迁移隔离运行时的位置。

代理是单文件 agent_reach_proxy.py(Starlette + uvicorn + 官方 mcp SDK)。上游 CLI 以子进程方式、带 command_env() 的隔离环境调用,升级上游只需再跑一次 ./scripts/reach.sh setup

fujioky/reach-dlproxy

在 GitHub 打开 ↗

视频转发 / 转存反向代理的参考实现(Go),给 Reach 提供带口令的上游取流通道。

reach-dlproxy

简体中文 · English

单文件 Go 写的 URL 前缀式反向代理:把目标地址直接拼在路径里访问,由服务器代为取回,客户端只跟服务器说话。它是 Reach 「视频反代」通道的参考实现:Reach 把 X / YouTube 的视频流经它转发或落盘到对象存储,也可以单独当作大文件下载代理使用。

https://dl.example.com/<口令>/https://video.twimg.com/ext_tw_video/.../video.mp4
                       └口令┘ └────────────── 目标 URL 原样拼接 ──────────────┘

给 Reach 用

  1. 按下文部署,设置 DL_PASSWORD
  2. Reach 后台 → 系统设置 → 视频反代 → 外部代理,填 https://dl.example.com/<口令>。Reach 会按 <代理地址>/<原始视频 URL> 拼接请求,并用 <代理地址>/healthz 做健康探测(本代理的 /<口令>/healthz/healthz 都免认证)。
  3. 勾选「启用故障转移」,播放请求就会经 Reach 自己的 /api/proxy-video 转发,代理地址不会出现在访客页面里。

上游要求 Range 请求,本代理原样透传请求头与 206 响应,适合流式播放和分块转存。

工作方式

请求路径去掉开头的 / 之后,剩下的整段(含 query)被当作目标 URL 解析,交给 httputil.ReverseProxy 转发。围绕这个核心做了几件事:

  • 不走 http.ServeMux ServeMux 会清洗路径,把 /https://host/... 里的 // 折叠成 / 再 301,目标 URL 当场被改坏。所以直接用 http.HandlerFunc 按原始路径分发;登录后的跳转手写 Location 头而不是用 http.Redirect。解析目标时兼容被前置 nginx 合并过斜杠的 https:/host/... 形式;没有 scheme 的目标默认补 http://
  • HTML 链接改写。 响应是 HTML 时读进内存,把绝对链接(https://<目标域>/…)和根相对链接(href="/…"src="/…"action=content=srcset=,单双引号都覆盖)批量替换成带代理前缀的形式。纯字符串替换,不解析 DOM;超过 50 MB 的 HTML 直接流式透传,防止小内存机器被撑爆。
  • 重定向改写。 3xx 的 Location 相对当前目标 URL 解析成绝对地址,再套上代理前缀。
  • 缓存策略重写。 上游的 Cache-Control / Expires / Pragma / Surrogate-Control 一律删掉换成自己的:按 Content-Type 或 URL 后缀判定静态资源(pdf / 图片 / css / js / 字体 / octet-stream),是则 public, max-age=2592000, immutable 并删掉 Set-CookieVary;否则 no-store。判定结果写进 X-DL-Cache-Policy 响应头。
  • 连接池复用。 全局共享一个 http.Transport(64 空闲连接 / 每 host 8 条)。服务端 WriteTimeout 为 0,否则大文件下载会被自己掐断。

访问口令

DL_PASSWORD 环境变量。留空则完全开放(启动时打一行 WARNING)。两种给法:

  1. URL 前缀/<口令>/https://host/...。适合配给 Reach、下载工具或别的程序。通过前缀认证时,改写用的公开前缀会带上 /<口令>,页面里改写出来的链接继续免登录。
  2. Cookie — 浏览器直接访问 /https://host/... 会弹口令页(POST /__dl_login),输对后种一年期 cookie dl_auth

cookie 里存的是口令的 HMAC-SHA256 派生值而非明文,换口令即让所有旧 cookie 失效;比对用 subtle.ConstantTimeCompare;口令错误 sleep 500ms 拖慢爆破;非浏览器请求(Accept 不含 text/html)直接 401;登录跳转只接受站内路径。

接口

路径说明
/health/healthzJSON 健康检查(status / uptime / time),免认证
/__dl_login口令表单提交端点(POST)
其它一切当作目标 URL 反代

环境变量

变量默认值说明
LISTEN_ADDR127.0.0.1:18080监听地址
DL_PASSWORD访问口令,留空 = 无认证

本地运行

go build -o dl-proxy .
DL_PASSWORD=test LISTEN_ADDR=127.0.0.1:18080 ./dl-proxy

curl -s localhost:18080/healthz
curl -sI -H 'Range: bytes=0-99' localhost:18080/test/https://example.com/   # URL 前缀口令 + Range

部署

无第三方依赖,Go 1.22+,go build 几秒完成,1 核 1 GB 的机器足够。

systemd/etc/systemd/system/dl-proxy.service

[Unit]
Description=dl-proxy
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/dl-proxy
Environment=LISTEN_ADDR=127.0.0.1:18080
ExecStart=/opt/dl-proxy/dl-proxy
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

口令放在 drop-in /etc/systemd/system/dl-proxy.service.d/auth.conf[Service]Environment=DL_PASSWORD=…),不进主 unit、不进仓库。

前置 nginx / OpenResty(TLS 终止):

location / {
    proxy_pass http://127.0.0.1:18080;
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_buffering off;
    proxy_request_buffering off;
}

三条硬要求:

  • 不要开 proxy_cache 缓存层会剥掉客户端的 Range 头,googlevideo 对无 Range 请求直接 403;还会把 301/302 和 /healthz 缓存几天,让口令形同虚设。
  • X-Forwarded-Proto / X-Forwarded-Host 必须传对,dl-proxy 靠它们拼对外前缀,否则改写出来的链接会指向 127.0.0.1
  • 若 nginx 开了 merge_slashes(默认开),目标 URL 会变成 https:/host/...;dl-proxy 能兼容,但更干净的做法是在 server 块里 merge_slashes off;

已知局限

  • HTML 改写是字符串替换,JS 运行时拼出的 URL、CSS url()、内联 JSON 里的链接不会被改写,重前端的 SPA 基本代理不动。
  • 没有域名白名单,拿到口令就能访问任意地址——口令即全部安全边界,不要留空暴露在公网。
  • 无日志轮转、无速率限制、无并发上限;小内存机器上大文件并发下载要自己节制。
  • 日志里 proxy error for http://favicon.ico 一类是浏览器对未改写成功的相对路径发起的请求,属噪声。

fujioky/reach-browser-extension

在 GitHub 打开 ↗

Chrome / Edge / Firefox 扩展:在 X / YouTube 页面一键生成 Reach 分享链接并复制,用管理员账号登录。

reach-browser-extension

简体中文 · English

Reach 的浏览器扩展(Chrome / Edge / Firefox):在 X 帖子或 YouTube 视频页一键生成 Reach 分享链接,链接自动复制到剪贴板。不用再打开后台、粘贴链接、等抓取、再复制。

弹窗
设置
弹窗:当前帖子、访问控制与密码、这条帖子的分享,其余收在历史记录里设置:管理员账号登录、默认分享设置

功能

  • 三种入口。 在帖子页点扩展图标或按 Alt+Shift+S,弹窗已经填好当前页面,回车即可;在时间线里的帖子链接上右键「用 Reach 分享此链接」,不用打开帖子;也可以在弹窗里粘贴任意 X / YouTube 链接。
  • 访问控制。 有效期(不限 / 1 天 / 7 天 / 30 天)、最多打开次数、阅后即焚。设置页里的默认值给右键分享用,弹窗里可以逐次调整。独立访客数、精确到期时间等更细的限制,在 Reach 后台对这条链接编辑。
  • 访问密码。 可选「系统密码」(Reach 系统设置里的统一密码)或「单独密码」。和 Reach 后台一致,密码设在镜像上而不是单条链接上:同一条帖子已有的分享链接也会一起需要这个密码。选「不设置」不会改动镜像已有的密码;沿用的镜像本来就有密码时,结果里会提示。设了单独密码时,结果里可以一键复制密码。
  • 不重复抓取。 同一条帖子已经在 Reach 里镜像过,就直接在那份镜像上新建一条分享链接:秒回,也不会再存一份图片。要更新内容,到后台对那份镜像「重新抓取」。
  • 历史记录。 分享记录保存在扩展本地(重启浏览器也在)。弹窗里先显示输入框中这条帖子最近的分享——默认就是当前页面,x.com / twitter.com、youtu.be / watch 等不同写法都能认出来;其余分享收在「历史记录」里,点开查看,可以清空。
  • 后台完成。 抓取一条新帖子可能要几十秒。弹窗随时可以关,分享在扩展后台继续跑,工具栏图标显示进行中的数量;完成后复制链接,弹窗没开着就发一条系统通知(点通知打开后台的镜像详情页)。

前提

需要部署了扩展接口的 Reach(/api/extension/*,见下文;fujioky/reach 2026-09-15 起的 main,访问密码需要其中「分享时设置镜像访问密码」这次提交),并且跑过对应的数据库迁移(api_tokens 表)。

安装

扩展没有上架商店,到 Releases 下载对应浏览器的 zip。

  • Chrome / Edge: 下载 reach-browser-extension-<版本>-chrome.zip 并解压到一个固定的文件夹。打开 chrome://extensions(Edge 为 edge://extensions),打开「开发者模式」,点「加载已解压的扩展程序」,选择这个文件夹。升级时把新版本解压覆盖到同一个文件夹,再在扩展页点刷新——换了文件夹 Chrome 会当成另一个扩展,登录状态和历史记录都不会带过去。
  • Firefox: 正式版 Firefox 只安装签过名的扩展。把 reach-browser-extension-<版本>-firefox.zip 提交到 addons.mozilla.org,选「不公开(自行分发)」签名后安装。只是试用的话,在 about:debugging →「此 Firefox」→「临时载入附加组件」直接选这个 zip,重启浏览器后失效。

从源码构建

要求 Node.js 22+。

git clone https://github.com/fujioky/reach-browser-extension.git
cd reach-browser-extension
npm install
npm run build            # Chrome / Edge → .output/chrome-mv3
npm run build:firefox    # Firefox       → .output/firefox-mv3

加载方式同上:Chrome / Edge 选 .output/chrome-mv3 文件夹,Firefox 临时载入选 .output/firefox-mv3/manifest.json

登录

打开扩展设置(弹窗右上角齿轮),填入 Reach 地址和管理员账号密码。

密码只用这一次:Reach 验证通过后签发一个这个浏览器专用的令牌,扩展只保存令牌(storage.local,不随浏览器账号同步)。Reach 后台「系统 → 浏览器扩展」列出所有登录过的扩展及最近使用时间,可以逐个撤销;扩展里「退出登录」也会同时作废服务端的令牌。令牌被撤销后,下一次分享会提示重新登录。

工作方式

  • 分享在后台跑,弹窗只负责显示。 弹窗一失去焦点就会被浏览器关掉,没法承载要等几十秒的请求。弹窗和右键菜单只把链接交给后台(Chrome 的 service worker / Firefox 的 event page),任务状态写进分享历史(storage.local),弹窗订阅它来渲染。后台重启时仍标记为「进行中」的任务一定已经中断,会被标成失败并提示重试。
  • 接口流式返回。 POST /api/extension/share 以 NDJSON 返回:抓取、转存图片、生成链接各阶段一行,最后一行 done。Chrome 会终止一个 30 秒内收不到 fetch 响应的扩展 service worker,而新帖子的抓取经常超过 30 秒;流式响应让响应头立即返回。没有 done 就结束的流按失败处理(函数超时就是这个样子)。
  • 保活。 有任务进行时,后台每 20 秒写一次 storage.session:Chrome 在任何扩展 API 调用时重置空闲计时,Firefox 在收到扩展事件(这次写入触发的 storage.onChanged)时重置。实测去掉这一步,70 秒的分享会在中途被 Chrome 终止。
  • 剪贴板。 Chrome 的 service worker 没有 navigator.clipboard,文本交给一个 offscreen document 用 execCommand('copy') 写入(offscreen document 无法获得焦点,异步剪贴板 API 在那里也不可用)。Firefox 的 event page 有 DOM,凭 clipboardWrite 权限直接写。
  • 跨域。 扩展不申请 Reach 站点的主机权限,接口对所有来源开放 CORS。这是安全的,因为这些接口从不读取 Cookie:请求要么带令牌,要么(登录时)带密码本身。

权限

权限用途
activeTab点图标或按快捷键时读取当前标签页的地址,用来预填弹窗
contextMenus帖子链接和帖子页上的右键菜单
storage登录信息、默认设置、分享历史
clipboardWrite自动复制分享链接
notifications弹窗关闭时通知分享结果
offscreen(仅 Chrome)在 service worker 之外写剪贴板

扩展不收集任何数据:只把你要分享的链接(以及登录时的账号密码)发给你自己填写的 Reach 地址,不访问其他服务。

接口

扩展只调用 Reach 的以下接口,实现见 Reach 仓库 app/api/extension/

POST   /api/extension/session   { username, password, name }  → { token, username }
GET    /api/extension/session   Authorization: Bearer <token>  → { username }
DELETE /api/extension/session   Authorization: Bearer <token>  → 204(撤销该令牌)
POST   /api/extension/share     Authorization: Bearer <token>
       { url, accessControl: { expiresAt, maxViews, burnAfterRead },
         password?: { mode: "inherit" } | { mode: "custom", value } }
       → NDJSON:
         {"type":"stage","phase":"fetching"}
         {"type":"stage","phase":"images","done":0,"total":2}
         {"type":"stage","phase":"saving"}
         {"type":"done","ok":true,"shareUrl":"…","adminUrl":"…","title":"…","reused":false,"fetchedAt":"…","passwordMode":"custom","warnings":[]}
       或 {"type":"done","ok":false,"error":"内容不存在或已删除"}

password 写到这条帖子的镜像上(影响它的所有链接),不传则保持镜像原有设置;passwordMode 是分享后镜像的密码状态。未设置系统密码时 inherit 会被拒绝。令牌失效时各接口返回 401,扩展据此退出登录。

开发

基于 WXT + React + Tailwind CSS 4。

npm run dev             # 带热更新启动一个装好扩展的 Chrome
npm run dev:firefox
npm test                # Vitest(WXT 的 fake-browser)
npm run typecheck
npm run zip             # 打包 Chrome 版 zip
npm run zip:firefox     # Firefox 版 zip + 源码 zip(AMO 审核需要)

发版:改 package.jsonversion(即扩展版本号),npm run zip && npm run zip:firefox,把 .output/ 下的 chrome / firefox 两个 zip 上传到对应 tag 的 Release。

entrypoints/
  background.ts       分享任务、右键菜单、通知、保活
  popup/              弹窗
  options/            设置页(登录、默认分享设置)
  offscreen/          Chrome 专用:写剪贴板
utils/
  api.ts              Reach 接口客户端(含 NDJSON 读取)
  jobs.ts             分享历史、帖子链接识别、可分享页面的匹配规则
  access.ts           访问控制预设与访问密码
  settings.ts         登录信息与默认设置
  clipboard.ts        后台写剪贴板(Chrome / Firefox 两条路径)
components/           弹窗与设置页共用的组件