Skip to main content
M

dsh-sakurafrp

mentholtea/dsh-sakurafrp

DeepSeek Harness 内建的内网穿透面板:托管 SakuraFrp frpc,隧道列表 / 端口自动同步 / 流量统计 / 公网密码门。

Install

dsh plugin --profile web add github:mentholtea/dsh-sakurafrp

README

dsh-sakurafrp

CI npm license

DeepSeek Harness 内建的内网穿透面板——替代 SakuraFrp 启动器。 同一台机器上装了本插件后,不需要再开 SakuraLauncher。

[!WARNING] 本插件是「与 AI 对话协作」的产物。 代码与文档由作者与 AI(DeepSeek Harness 会话) 多轮对话生成并反复迭代而来,没有经过人工逐行审计,实测环境也仅限于作者本机的 Windows + WSL2。请自行审阅源码后再使用;涉及安全敏感场景时请格外谨慎。 代码按 MIT「按原样(AS IS)」提供,不附带任何担保,风险自负。

与「AI 生成即不可靠」无关的部分:测试是可复核的。npm test 是 23 个套件、643 项 全离线的断言,其中 admin-auth-test.mjs 专门覆盖网关鉴权(8 种路径花招、WS 同权、 改密后旧 cookie 失效)。数字与结论都能自己跑一遍验证,不必只信这段说明。


第一步:装上并连上(四步,约 5 分钟)

只要你是第一次用,照这四步走就行。这四步在界面上是逐步解锁的:上一步没到位, 下一步不会出现,所以不会一上来就被一堆选项淹没。

0. 装插件

dsh plugin --profile desktop add dsh-sakurafrp -w
# web / CLI 场景把 --profile 换成你的 profile 名,例如 --profile web

备选:从 Release tarball 装(不走 npm,取 GitHub 预构建包,同样是这个版本):

dsh plugin --profile desktop add https://github.com/MentholTea/dsh-sakurafrp/releases/latest/download/dsh-sakurafrp.tgz -w

任一条装不上就换另一条。

重启 DSH。若设置页提示未装 frpc,点一次「一键安装」——插件会按平台/架构自动下载。

1. 拿访问密钥

到 SakuraFrp 面板 → 用户信息 → 查看访问密钥,复制。 在设置页 ① 访问密钥 粘进去,点输入框右侧的「保存」。

2. 选隧道

密钥保存后出现 ② 隧道。下拉选一条已有的;一条都没有就点「新建」, 选个节点(后面标着空闲/一般/拥挤)、填个端口,远端端口交给官方分配。

3. 填本机端口

出现 ③ 本机端口。新装是空的,必须自己填——填完立刻检测能不能用。 选了已有隧道的话这里会自动填好。

4. 设公网密码

端口可用后出现 ④ 访问密码。强烈建议 20 位以上随机串,未设密码时外网一律拒绝。

最后点「保存」→ 打开顶部开关 → 手机访问隧道公网地址 → 输密码 → 完整 DSH 界面。

两个容易踩的点

  • 插件装上默认是关闭的,端口也是空的。 这东西的作用就是把 DSH 暴露到公网, 装完就该先关着;而不预填端口是因为那等于替你决定了「用这个数字、它现在空闲、 且要写进隧道里」三件都可能不成立的事。
  • 一定要绑定子域名走 HTTPS。 用默认域名 xxx.nyat.app 直接访问会撞证书警告, 部分浏览器点「继续」后还会白屏。绑定方法见文末证书警告与白屏问题—— 这也是密码不被中间人截获的前提。

它长什么样

  • 设置页入口:设置 → 内网穿透 (SakuraFrp)
  • 侧边栏入口:左侧面板列表里的「内网穿透」,带三色状态点(🟢已连接 / 🟡未连接 / 🔴故障) 和一键启停开关(🟢开 / 🔴关);折叠成图标栏时状态点回到图标右上角

功能清单

  • frpc 全托管:DSH 启动即拉起、退出即停止、崩溃指数退避重连(1s→2s→…→60s 封顶)
  • 密钥零泄漏:token 经 NATFRP_TOKEN 环境变量传递,不出现在 argv / 进程列表
  • 隧道面板:API v4 拉取隧道列表(带节点名),下拉直接切换;选完本机端口自动跟随隧道
  • 本地新建隧道:手上一条隧道都没有时,选个节点就能建——固定 TCP 类型、 表单里的端口同时作为本机端口、远程端口交给官方分配,不用先有域名和解析; 节点列表带实时负载(空闲/一般/拥挤/满载,颜色区分),满载的置灰不给选
  • 端口双向同步(autoAlign / adoptTunnelPort):改端口前先试绑,绑得上才改; 改完把面板里那条隧道的 local_port 一起改过去。反过来,只是选了条隧道时, 本机端口跟着隧道走——根治「隧道连上了但访问 502」
  • 互踢检测:30s 窗口内断线 ≥6 次判定「疑似互踢」(SakuraLauncher 或其它机器 连了同一条隧道),状态页醒目告警
  • 侧边栏状态点 + 开关:点管连通(绿/黄/红),开关管启停(红/绿),互不干扰; 开关点击即时反馈(乐观更新),以服务端返回为准
  • 流量面板:本日已用 / 剩余流量 + 剩余百分比(绿→橙→红三档); 百分比基于 data_plans 的真实总量,不是用「今日+剩余」硬凑的分母
  • 公网密码门:HMAC 签名 cookie、常量时间比较、按 IP 失败锁定(5 次锁 60s), 登录页零外部依赖,登录成功后回到访客原本想去的地址(仅接受站内路径,防开放重定向)
  • Host 围栏反代:改写 Host/Origin/Referer/Sec-Fetch-Site 为回环权威, 透传 WebSocket upgrade,注入非安全上下文 polyfill(crypto.randomUUID / AbortSignal.any)
  • DSH 二次认证:实时取 ctx.connection.authenticatedUrl() 的 launch token, 首次 HTML 导航自动补 ?token=
  • 桌面/浏览器通吃:外链在 Electron 里走宿主内置浏览器标签,浏览器里走新标签页

frpc 日志默认展开,并且每 2 秒刷新一次,不用收起再展开才看到新内容。


架构

Web 设备 → xxx.nyat.app:端口 → SakuraFrp 节点 → frpc(本插件托管)
                                              → 127.0.0.1:<gatePort> 网关
                                                ├─ /natfrp/*  管理路由(需密码门 cookie)
                                                ├─ 密码门(HMAC cookie / ?gate=)
                                                └─ Host 围栏反代 + WS 透传
                                                  → 127.0.0.1:<dshPort> DSH WebServer

安装(详细)

方式 A:从 npm 装(推荐)

dsh plugin --profile desktop add dsh-sakurafrp -w
# web / CLI 场景把 --profile 换成你的 profile 名,例如 --profile web
# 然后重启 DSH

装完请确认 dsh.profile.bundles 里有 dsh-sakurafrp——没有这一条,DSH 不会加载插件。 正常情况会自动写入;若你看到安装过程报错退出(例如别的插件的构建脚本未获批准), 这一步会被跳过,需要手工补上。

方式 A2:从 Release tarball 装(不走 npm,也不需要构建,与方式 A 同版本)

dsh plugin --profile desktop add https://github.com/MentholTea/dsh-sakurafrp/releases/latest/download/dsh-sakurafrp.tgz -w

方式 B:从源码装(不依赖 npm)

git clone https://github.com/MentholTea/dsh-sakurafrp.git
# 在 profile 的 package.json 里:
#   "dependencies": { "dsh-sakurafrp": "link:<克隆目录绝对路径>" }
# 并把 "dsh-sakurafrp" 加进 dsh.profile.bundles
dsh plugin --profile desktop install

装完还要做两件事(frpc 二进制不会被 npm 包携带):

  1. 打开设置页,若提示未装 frpc,点「一键安装」即可——插件会自动按平台/架构从官方 接口挑包,下到 ~/.dsh/dsh-sakurafrp/frpc-standalone(.exe) (也可以自己下好放到同一路径,或把 frpcPath 指到任意位置)
  2. 重启 DSH → 设置页填访问密钥 → 下拉选隧道 → 设公网密码

frpc 默认取 ~/.dsh/dsh-sakurafrp/frpc-standalone(.exe);旧版本放在 ~/.dsh/bin/ 的会被继续沿用,不必重下。frpc -V 输出应含 sakura。


首次使用(完整说明)

上面「第一步」是精简版。这里补上每一步的细节和判断依据。

  1. 重启 DSH 后,设置页出现「内网穿透 (SakuraFrp)」区块;若提示未装 frpc,先点「一键安装」
  2. ① 访问密钥:这是唯一的必填项。点「获取密钥」→ 用户信息 → 查看访问密钥,粘贴进来, 输入框右侧会出现「保存」,点它即可。想换掉已保存的密钥,点同一个字段后面的「清除」。 ⚠️ 没填密钥之前,下面三步根本不会出现 —— 密钥都没有,隧道列表也拉不到, 摆出来只是给人添堵。
  3. ② 隧道(密钥保存后出现):下拉选一条,或者点右边的「新建」现建一条。 新建表单里一行放完:名字、端口、节点(下拉框,后面标着空闲/一般/拥挤)、保存。 名字可以留空(就叫 DSH),建完自动选中。 (自动 HTTPS 固定用「自动」,不在界面上让选 —— 新建的都是 TCP 隧道,这一项对它不起作用。)
  4. ③ 本机端口(选了隧道后出现):新装是空的,必须自己填(插件不会替你猜一个)。 填完立刻检测这个端口能不能用;保存时会把 SakuraFrp 面板里这条隧道的端口一起改掉。 选了已有隧道的话,这里会自动填成隧道里写的端口,不用手动对。
  5. ④ 访问密码(端口确认可用后才出现):强烈建议 20 位以上随机串。 未设密码时外网一律拒绝,不会直通。
  6. 点「保存」→ 打开顶部开关
  7. 手机访问隧道公网地址 → 输密码 → 完整 DSH 界面(WebSocket 流式同步)

四步是逐步解锁的,不是一开始全摊在面前:密钥 → 隧道 → 端口 → 密码, 上一步没到位,下一步就不出现。旁边那句提示会告诉你当前只差哪一步。

插件装上默认是关闭的,端口也是空的,需要自己填端口、开开关。原因很直接: 这东西的作用是把 DSH 暴露到公网,装完就该先关着;而预填一个端口等于替你决定了 「用这个数字、它现在空闲、且要写进隧道里」三件都可能不成立的事。

新建的隧道会把「本地端口」写成表单里那个端口,远程端口由官方分配; 之后改了本机端口,会把面板里那条隧道的端口一起改过去,不用回面板手动同步。

frpc 日志默认展开,并且每 2 秒刷新一次,不用收起再展开才看到新内容。


配置(cordis.patch.yml → config)

键默认说明
enabledfalse插件启用(设置页可改,持久化到 state.json)。新装默认关闭,避免装完就把 DSH 挂上公网
frpcPath~/.dsh/dsh-sakurafrp/frpc-standalone(.exe)frpc 路径(旧 ~/.dsh/bin/ 下的也会被沿用)
tunnelId—隧道 ID(设置页下拉选择后持久化)
tokenFile—访问密钥文件(优先级最高;设置页写密钥即写此文件)
tokenRefNATFRP_TOKENtokenFile 为空时走 DSH credentials
gatePort—网关端口 = 隧道面板的「本地端口」。留空 = 未设置,网关不监听、frpc 不启动
gateHost127.0.0.1网关监听地址。frpc 同机时无需对外监听;改成 0.0.0.0 会给局域网开一条绕过密码门的入口
gatePassword—公网密码;也可用设置页写(落 ~/.dsh/dsh-sakurafrp/gate-password.txt,不写入 state.json)
autoAligntrue启动前自动把隧道「本地端口」改成 gatePort
maxLogLines500日志环形缓冲行数

GET  /natfrp/api/state     全量快照(frpc + 网关 + 隧道)
GET  /natfrp/api/tunnels   隧道列表(含节点名)
GET  /natfrp/api/nodes     免费节点(含负载 / 是否可新建;离线、付费已剔除)
GET  /natfrp/api/port/check?port=N  端口可用性(available / reason)
GET  /natfrp/api/user      账号信息
GET  /natfrp/api/logs      frpc 日志
POST /natfrp/api/start     启用并启动
POST /natfrp/api/stop      停止
POST /natfrp/api/align     把隧道「本地端口」改成插件正在用的端口
POST /natfrp/api/tunnel/create  新建一条 TCP 隧道 {name,node,port},本地端口指向插件
POST /natfrp/api/config    更新配置 {enabled,tunnelId,gatePort,gatePassword,token,clearGatePassword}
                            gatePort 传 null / '' = 清空端口(网关随之停止)
GET  /natfrp/api/frpc/check        本地 frpc 是否存在 / 版本是否合规(不下载)
GET  /natfrp/api/frpc/update-check 手动对比本地与官方最新版号(无自动检查)
                            结论带 updateAvailable / current / latest;
                            60s 内重复请求带 throttled + retryAfter,不静默复用缓存
POST /natfrp/api/frpc/install      一键下载安装 / 替换为新版(运行中会先停、完成自动恢复)
POST /natfrp/api/frpc/dismiss      忽略当前 frpc 提示(按问题类型 + 路径记忆)
POST /natfrp/api/frpc/undismiss    恢复被忽略的提示
POST /natfrp/api/frpc/ack-result   确认安装结果提示(落盘,跨会话不再弹)

版本更新是纯手动的:插件不会在启动、轮询或任何后台时机去查官方清单, 只有用户在面板顶部点「检查更新」才发一次请求;60 秒内重复点击会被节流并提示 「别按太频繁,N 秒后再查」。确认有新版本后,再点「更新到 X」才真正下载替换。

鉴权边界:/natfrp/* 只在持有密码门 cookie 时才放行,否则返回登录页 / 401。 不能按来源地址判断「本机还是公网」——公网流量是 frpc 从 127.0.0.1 转发进来的, 两者来源完全相同。本机设置页打的是 DSH webServer 上的同名路由,不经过网关, 所以本机操作不需要密码。


证书警告与白屏问题(重要)

通过隧道默认域名(xxx.nyat.app)HTTPS 访问时,浏览器会提示证书不受信任; 部分浏览器在告警页选择继续后仍可能白屏。解决方法是绑定自己的子域名, 让 frpc 从服务端自动加载与域名匹配的证书:

  1. 到 子域绑定页面 申请/绑定子域名, 并将记录关联到你的隧道
  2. 详细教程见官方文档:子域绑定指南
  3. 绑定完成后,用 你的子域名:远程端口 访问,证书正常加载、无白屏

插件设置页在隧道跑起来后,会把这句话连同两个入口一起显示出来: 「不绑定证书 ↗ 会出现证书警告、白屏等;绑定子域名后即可消除。 绑定教程 ↗」 「绑定证书」指向子域绑定页,「绑定教程」指向官方文档。 (「不」不带链接——连成一片蓝色会读成「不去绑定证书」,意思正好反了。)

手机端登录后白屏、只有桌宠、侧边栏一滑就收回(2026-11-05 定位,真机修复)

白屏的真因不是遮罩,是主内容宽度被算成 0。 真机(Xiaomi 2509FPN0BC / Android 17 / Edge Beta 157)实测:

UA           : Mozilla/5.0 (X11; Linux x86_64) ... Edg/157    ← 开了「桌面版网站」
innerWidth   : 980      visualViewport.scale : 0.408
computed     : grid-template-columns = "980px 0px 0px"
centerCol    : x=980 w=0        an3WzG_root : x=980 w=0
DOM          : bodyLen=10978、mask=false、__DSH_BOOT_READY__=object

DOM 完全正常、插件全部加载、桌宠照常绘制(它是 position:fixed + z-index:214748,不受网格影响)—— 只有主内容那两列宽度是 0,所以看起来就是白屏。

源头是 dsh-web-mobile 的移动端样式:

@media (max-width:1023px) and (pointer:coarse){
  html [data-mobile-nav="frame"]{ grid-template-columns: minmax(0,1fr) 0 0 !important }
}

它假设「主内容在第 1 列」,但 DSH 新版把三列定成 sidebarCol grid-area:1/1 | centerCol grid-area:1/2 | rightbarCol grid-area:1/3。 于是那个 1fr 全被分给侧栏列,第 2 列(主内容)被压成 0px。 触发键是 pointer: coarse,手机必现,桌面鼠标(fine)不受影响 —— 这也解释了为什么只有手机白屏。

实测确认:摘除这一条声明后立刻恢复

BEFORE: gridCols="980px 0px 0px"   centerW=0
AFTER : gridCols="56px 924px 0px"  centerW=924

网关现在注入一段幂等脚本(POLYFILL_HTML 里 data-natfrp-gridfix),只摘除命中该选择器的那条声明, 插件其余样式原样保留;并监听样式重插以保持生效。不改第三方插件文件,重启 DSH 后自动起作用。

现象真正的成因处置
白屏,但桌宠正常显示移动端样式把 grid-template-columns 覆成 1fr 0 0,主内容列宽 0(见上)网关注入 data-natfrp-gridfix 精确摘除该声明;或关掉 Edge「桌面版网站」
勾选框点不动须知页单选框仅 13px,低于触摸命中下限;且需先滚到文末网关注入 @media(max-width:768px) 把勾选框放大到 24px
向右滑出侧边栏,立刻自动收回窄视口 DSH 走 drawer:全屏 drawerMask(pointer-events:auto)带 onClick → toggleSidebar(),与 dsh-web-mobile 的边缘手势(START_ZONE_RATIO=0.45)抢同一次触摸,滑动收尾的合成 click 落到遮罩上就关闭用左上角按钮开关;逐帧拖拽必与遮罩抢事件,属第三方插件与宿主 drawer 的手势冲突
「重定向次数过多,清 Cookie」DSH 会话 cookie 名绑定 host:port,端口一变就对不上;旧逻辑补太多或补太少已修,见下一节

真机自检(不需要改任何代码):

adb forward tcp:9222 localabstract:chrome_devtools_remote
# 然后在页面控制台看:列宽是否为 0
# document.querySelector('.mP3x-q_frame').style.gridTemplateColumns
# getComputedStyle(document.querySelector('.mP3x-q_centerCol')).width

最快的自救:Edge 菜单里关掉「桌面版网站」(UA 回到移动版、视口回到设备宽度),或直接重启 DSH 让网关注入的修复脚本生效。

「重定向次数过多 / 清 Cookie」的完整成因(已修)

DSH 的会话 cookie 名是 dsh-auth-<base64url(sha256(host:port))>,名字绑定端口; token 则是每个进程随机生成。网关在两侧都可能失手:

  • 补太多:浏览器没存住 dsh-auth-*(无痕、清理、跨站导航)时,旧实现每轮都补 ?token=,DSH 每轮回 303 ./ → 浏览器报「重定向次数过多」。删 Cookie 也没用, 因为下一轮仍是同样路径。
  • 补太少:DSH 重启换了端口,旧 cookie 名字对不上,而旧实现「看到 dsh-auth-* 就不补 token」,于是永久 401 —— 只能手动删 Cookie。

现在两者都有确定行为(test/gate-redirect-test.mjs 锁死):

  1. 补发过一次就种 natfrp_tok(600s),此后不再补,从源头断掉 303 循环;
  2. 同一路径连续补发超过 2 次仍未拿到页面 → 判定浏览器存不住 cookie, 停下来给出明确说明页,而不是继续重定向;
  3. 上游 401 且没有补发标记 → 清掉失效的 dsh-auth-* 并重新走一次 token(natfrp_heal 只允许自愈一次,防止变成另一种循环)。

验证

全部离线:不连 SakuraFrp、不需要访问密钥、不写任何用户目录(fresh-install-test 也是拿一个临时 DSH_HOME 跑冷启动)。

npm test                 # 24 个套件、655 项,串行跑并汇总
npm test -- path         # 只跑名字含关键字的套件

npm test 等价于 node test-all.mjs。单跑某一个:node test/order-test.mjs。

测试文件随包发布(files 里包含 test-all.mjs 与 test/*.mjs), 所以 npm install 之后也能直接跑 npm test,不必先 clone 仓库。

仓库用 .gitattributes 把换行符钉成 LF。部分断言用正则检查源码结构 (如 /note: '...',\n\s+extra,/),CRLF 检出会让 \n 匹配不到 \r\n, 导致这些断言在本机全线飘红而代码本身无误。

套件项覆盖
verify.mjs31模块级 + 网关全链路(API 客户端 / frpc 解析 / 密码门 / 围栏 / WS 透传)
order-test.mjs61四步解锁顺序、表单排布、按钮文案与出现条件
paths-test.mjs51frpc / DSH home 路径解析(含 $DSH_HOME 空白、PROFILE_DIR 反推)
tunnel-create-test.mjs50新建隧道(端口来源、extra.auto_https、官方告警回传)
progressive-test.mjs59渐进解锁派生状态与提示语
feedback-test.mjs37操作反馈、错误码与文案
installer-test.mjs52frpc 资产挑选 / 下载 / 版本校验 / 版本号提取与比较
dismiss-test.mjs29官方提醒的关闭状态机(按路径记忆)
traffic-test.mjs27流量口径与剩余百分比
apply-test.mjs26apply() 装配、路由注册、冷启动默认值
external-test.mjs24外链策略(内置标签 / 退化 / 非绝对地址拒绝)
token-test.mjs24密钥写入、清除、掩码
diag-test.mjs22诊断信息与状态徽标
admin-auth-test.mjs22网关管理接口鉴权(8 种路径花招 / WS 同权 / 改密后旧 cookie 失效)
fixes-test.mjs31已修 bug 回归(跨盘路径 / 互踢误判 / 密码迁移 / 清空不复活 / API 自愈 / 登录回跳)
defaults-test.mjs19配置默认值
update-check-test.mjs15手动检查更新(结论 / 60s 节流 / 连点拦截 / hooks 规则)
scroll-test.mjs14主面板滚动容器链
state-test.mjs12state.json 持久化与坏文件兜底
switch-test.mjs11侧边栏开关状态机(乐观更新 / 连点保护)
density-test.mjs11面板信息密度与读屏高度
fresh-install-test.mjs10全新安装的冷启动形态
credential-token-test.mjs5tokenRef → DSH credentials 读写

界面走查用 preview-render.mjs:它用极简 React 桩把真实的 Panel 渲染成静态 HTML, 可导出 ready / fresh / create / noport 四种形态,再用无头浏览器截图核对。

$env:PREVIEW_VARIANT='fresh'; node preview-render.mjs .preview/fresh.html

追加真实 API 冒烟(只读端点,会消耗一次 API 调用):

$env:NATFRP_TOKEN='你的密钥'; node test/verify.mjs

已知边界

  • DSH 关闭 = 隧道断开(frpc 生命周期与宿主绑定);需要常驻请另配系统服务
  • 同一隧道绝不能被两个客户端同时连接(互踢),本插件有检测但无法替你关掉另一端
  • 带宽/流量跑满、密钥失效这类问题不会被判成互踢:login to server failed 单独归类, 只写日志并在提示里指向"密钥或网络",不参与互踢计数
  • 失败锁定在 frpc 下是全局的:所有公网访客同源(都是 127.0.0.1),没法按真实 IP 分开计数,所以一个人连错 5 次会把登录入口锁 60 秒;已登录的会话不受影响
  • ?gate=<密码> 直达会把密码写进浏览器历史 / 地址栏,公共设备上请用登录框输入
  • 密码门 cookie 没有 Secure 标志(只有 HttpOnly; SameSite=Lax)。这是有意为之: 网关上游是 http://127.0.0.1,若加 Secure,未绑定证书、通过 http:// 访问的用户 会因为浏览器拒发 cookie 而完全登不进去。代价是走 HTTP 时 cookie 明文传输—— 请务必绑定子域名走 HTTPS(见上文「证书警告与白屏问题」),否则密码与会话可被中间人截获
  • 界面文案目前只有中文(硬编码在 client.js 里)。locale/*.json 只提供 meta.title / meta.description,供插件管理器与设置页显示插件名与描述; 界面内的按钮、提示、错误文案不走 i18n,英文环境看到的仍是中文
  • 免费档:2 隧道 / 每月 5 GB 流量(可结转)/ 10 Mbps 限速;额度用完后可通过每日签到 或购买流量套餐提升,不会因为偶尔跑满就长期卡住
  • API 写操作(edit/delete/create)走官方 OpenAPI,但官方对滥用有风控,别高频调用

License

MIT

Related plugins