关于 Reach
本站的代码全部开源,由 4 个仓库组成。下面是各自的说明,README 直接取自 GitHub。
Reach 本体:Next.js 应用,负责镜像 X / YouTube 帖子、发布文章、分享链接与访客行为分析。
Agent Reach 的分支,proxy/ 目录里是 Reach 依赖的 HTTP / MCP 代理:在上游之上补了 X / YouTube 的定制解析和公开访问接口。
视频转发 / 转存反向代理的参考实现(Go),给 Reach 提供带口令的上游取流通道。
Chrome / Edge / Firefox 扩展:在 X / YouTube 页面一键生成 Reach 分享链接并复制,用管理员账号登录。
fujioky/reach
在 GitHub 打开 ↗Reach 本体:Next.js 应用,负责镜像 X / YouTube 帖子、发布文章、分享链接与访客行为分析。
Reach
所不及者,可达于人。 把一条 X / YouTube 的帖子——正文、图片、视频、评论——镜像成一条可控的私密链接;也可以发布自己撰写的文章,并看见访客究竟是怎样阅读它们的。
简体中文 · English
关联仓库
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)。支持快捷共享。
预览
/post | |
功能
镜像 —— 贴上帖子链接,得到一份自托管副本。
- 通过上游 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 批次) |
| 结构化事件 | view、dwell(各内容块可见时长)、scroll(最大深度)、click(页面坐标 + 文档/视口尺寸)、media_click、media_play、outlink_click、video(播放 / 暂停 / 拖动 / 每 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_idcookie 区分。 - 录像不打码输入框(
maskAllInputs: false),访客在评论框里输入的内容会进入录像。 - 没有关闭录制的开关,也没有自动清理:录像随内容项级联删除,长期运行要留意
analytics_chunks的体积。app/privacy-policy页面的文案要与你实际的采集范围保持一致。
浏览器扩展
fujioky/reach-browser-extension 把「打开后台 → 粘贴链接 → 等抓取 → 复制分享链接」压成一步:在 X 帖子或 YouTube 视频页直接生成分享链接,自动复制到剪贴板。支持 Chrome / Edge / Firefox。
安装与连接
- 部署本仓库并执行数据库迁移(
npm run db:migrate,扩展需要其中的api_tokens表)。 - 从扩展的 Releases 下载 zip。Chrome / Edge:解压到固定文件夹,在
chrome://extensions打开开发者模式,「加载已解压的扩展程序」选这个文件夹。Firefox:zip 未签名,可在about:debugging临时载入,长期使用需到 AMO 以「不公开」方式签名。 - 打开扩展设置,填入 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_URL | 是 | Postgres 连接串(drizzle-kit 也用它) |
AUTH_SECRET | 是 | Auth.js 密钥(openssl rand -base64 32) |
BLOB_READ_WRITE_TOKEN | 是 | Vercel 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
- 从本仓库创建 Vercel 项目(框架预设 Next.js,Node 24)。
- 挂载一个 Postgres 数据库(Vercel 市场里的 Neon 开箱即用)和一个 Blob 存储;Vercel 会自动注入
POSTGRES_URL与BLOB_READ_WRITE_TOKEN。 - 添加
AUTH_SECRET、AGENT_REACH_BASE_URL、AGENT_REACH_PWD,按需添加NEXT_PUBLIC_SITE_URL与 Turnstile 密钥。 - 对生产数据库执行一次迁移:
POSTGRES_URL=... npm run db:migrate。 - 部署。
vercel.json已配置每日健康采样的 cron。 - 访问
/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-dlp、twitter-cli、xhs、经 mcporter 的 Exa 等),让 Agent 直接调用,有意不做任何包装 API。所以直接部署上游是没法给 Reach 用的,这一层由本代理补上。
代理补了什么
1. 针对转存场景定制的 X / Twitter 与 YouTube 解析。 上游把原始 CLI 输出直接交给 Agent;代理把它整理成产品能存、能渲染的统一结构:
id、url、title、content、author {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_lang、transcript_vtt——规范化后的带时间轴 WebVTT(去头部、cue id、行内标签、滚动重复行),可直接喂给<track>。优先级:人工中文 › 自动中文(仅中文音频)› 人工英文 › 自动英文 › 原生语言轨;YouTube 的自动翻译轨有意排除,因为质量远不如拿原文在下游翻译。 - 结构化错误:上游任何失败都变成
{stage, kind, message},kind∈rate_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,外加reach、agent_reach、agent_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.sh 把 HOME 和 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
需要登录态的平台,凭据要放进隔离家目录而不是你自己的:
- X:
twitter-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.com、AGENT_REACH_PWD=<口令>,或在 后台 → 系统设置 → Agent Reach 接口 填同样两项。
其它命令:run(前台)、stop、restart、status、logs [n]、sync-commands、cmd <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 别名:twitter→x,xiaohongshu/rednote→xhs,yt→youtube。
配置
除口令外都可选,完整清单见 .env.example。要点:
| 键 | 默认 | 含义 |
|---|---|---|
AGENT_REACH_PROXY_PASSWORD | — | 必填。未设置时 /http/ 一律回 503。 |
AGENT_REACH_PROXY_PUBLIC_URL | http://127.0.0.1:18280 | 对外地址,用于 MCP OAuth 资源元数据。 |
AGENT_REACH_MCP_TOKEN | 同口令 | /mcp 的静态 Bearer;AGENT_REACH_MCP_AUTH_REQUIRED=0 关闭鉴权(仅本地)。 |
AGENT_REACH_MCP_PUBLIC_DISCOVERY | 1 | 允许未鉴权的 initialize / tools/list。 |
AGENT_REACH_TURNSTILE_SITE_KEY / _SECRET_KEY | — | OAuth 授权页加 Turnstile 验证。 |
AGENT_REACH_PROXY_SEARCH_LIMIT / _COMMENT_LIMIT | 15 / 50 | 搜索结果与评论上限。 |
AGENT_REACH_MCP_IMAGE_MAX_BYTES / _DOWNLOAD_MAX_BYTES | 4 MB / 32 MB | 内联图片压缩目标与原图下载上限。 |
AGENT_REACH_RUNTIME_DIR、_VENV、_BIN_DIR、_HOME、_NODE_BIN | runtime/ 之下 | 迁移隔离运行时的位置。 |
代理是单文件 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 用
- 按下文部署,设置
DL_PASSWORD。 - Reach 后台 → 系统设置 → 视频反代 → 外部代理,填
https://dl.example.com/<口令>。Reach 会按<代理地址>/<原始视频 URL>拼接请求,并用<代理地址>/healthz做健康探测(本代理的/<口令>/healthz与/healthz都免认证)。 - 勾选「启用故障转移」,播放请求就会经 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-Cookie与Vary;否则no-store。判定结果写进X-DL-Cache-Policy响应头。 - 连接池复用。 全局共享一个
http.Transport(64 空闲连接 / 每 host 8 条)。服务端WriteTimeout为 0,否则大文件下载会被自己掐断。
访问口令
DL_PASSWORD 环境变量。留空则完全开放(启动时打一行 WARNING)。两种给法:
- URL 前缀 —
/<口令>/https://host/...。适合配给 Reach、下载工具或别的程序。通过前缀认证时,改写用的公开前缀会带上/<口令>,页面里改写出来的链接继续免登录。 - Cookie — 浏览器直接访问
/https://host/...会弹口令页(POST /__dl_login),输对后种一年期 cookiedl_auth。
cookie 里存的是口令的 HMAC-SHA256 派生值而非明文,换口令即让所有旧 cookie 失效;比对用 subtle.ConstantTimeCompare;口令错误 sleep 500ms 拖慢爆破;非浏览器请求(Accept 不含 text/html)直接 401;登录跳转只接受站内路径。
接口
| 路径 | 说明 |
|---|---|
/health、/healthz | JSON 健康检查(status / uptime / time),免认证 |
/__dl_login | 口令表单提交端点(POST) |
| 其它一切 | 当作目标 URL 反代 |
环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
LISTEN_ADDR | 127.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.json 的 version(即扩展版本号),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/ 弹窗与设置页共用的组件