- Home
- Plugins
- Integrations & Remote
- dsh-sakurafrp
dsh-sakurafrp
mentholtea/dsh-sakurafrp
DeepSeek Harness 内建的内网穿透面板:托管 SakuraFrp frpc,隧道列表 / 端口自动同步 / 流量统计 / 公网密码门。
Install
dsh plugin --profile web add github:mentholtea/dsh-sakurafrpREADME
dsh-sakurafrp
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 包携带):
- 打开设置页,若提示未装 frpc,点「一键安装」即可——插件会自动按平台/架构从官方
接口挑包,下到
~/.dsh/dsh-sakurafrp/frpc-standalone(.exe)(也可以自己下好放到同一路径,或把frpcPath指到任意位置) - 重启 DSH → 设置页填访问密钥 → 下拉选隧道 → 设公网密码
frpc 默认取 ~/.dsh/dsh-sakurafrp/frpc-standalone(.exe);旧版本放在
~/.dsh/bin/ 的会被继续沿用,不必重下。frpc -V 输出应含 sakura。
首次使用(完整说明)
上面「第一步」是精简版。这里补上每一步的细节和判断依据。
- 重启 DSH 后,设置页出现「内网穿透 (SakuraFrp)」区块;若提示未装 frpc,先点「一键安装」
- ① 访问密钥:这是唯一的必填项。点「获取密钥」→ 用户信息 → 查看访问密钥,粘贴进来, 输入框右侧会出现「保存」,点它即可。想换掉已保存的密钥,点同一个字段后面的「清除」。 ⚠️ 没填密钥之前,下面三步根本不会出现 —— 密钥都没有,隧道列表也拉不到, 摆出来只是给人添堵。
- ② 隧道(密钥保存后出现):下拉选一条,或者点右边的「新建」现建一条。 新建表单里一行放完:名字、端口、节点(下拉框,后面标着空闲/一般/拥挤)、保存。 名字可以留空(就叫 DSH),建完自动选中。 (自动 HTTPS 固定用「自动」,不在界面上让选 —— 新建的都是 TCP 隧道,这一项对它不起作用。)
- ③ 本机端口(选了隧道后出现):新装是空的,必须自己填(插件不会替你猜一个)。 填完立刻检测这个端口能不能用;保存时会把 SakuraFrp 面板里这条隧道的端口一起改掉。 选了已有隧道的话,这里会自动填成隧道里写的端口,不用手动对。
- ④ 访问密码(端口确认可用后才出现):强烈建议 20 位以上随机串。 未设密码时外网一律拒绝,不会直通。
- 点「保存」→ 打开顶部开关
- 手机访问隧道公网地址 → 输密码 → 完整 DSH 界面(WebSocket 流式同步)
四步是逐步解锁的,不是一开始全摊在面前:密钥 → 隧道 → 端口 → 密码, 上一步没到位,下一步就不出现。旁边那句提示会告诉你当前只差哪一步。
插件装上默认是关闭的,端口也是空的,需要自己填端口、开开关。原因很直接: 这东西的作用是把 DSH 暴露到公网,装完就该先关着;而预填一个端口等于替你决定了 「用这个数字、它现在空闲、且要写进隧道里」三件都可能不成立的事。
新建的隧道会把「本地端口」写成表单里那个端口,远程端口由官方分配; 之后改了本机端口,会把面板里那条隧道的端口一起改过去,不用回面板手动同步。
frpc 日志默认展开,并且每 2 秒刷新一次,不用收起再展开才看到新内容。
配置(cordis.patch.yml → config)
| 键 | 默认 | 说明 |
|---|---|---|
enabled | false | 插件启用(设置页可改,持久化到 state.json)。新装默认关闭,避免装完就把 DSH 挂上公网 |
frpcPath | ~/.dsh/dsh-sakurafrp/frpc-standalone(.exe) | frpc 路径(旧 ~/.dsh/bin/ 下的也会被沿用) |
tunnelId | — | 隧道 ID(设置页下拉选择后持久化) |
tokenFile | — | 访问密钥文件(优先级最高;设置页写密钥即写此文件) |
tokenRef | NATFRP_TOKEN | tokenFile 为空时走 DSH credentials |
gatePort | — | 网关端口 = 隧道面板的「本地端口」。留空 = 未设置,网关不监听、frpc 不启动 |
gateHost | 127.0.0.1 | 网关监听地址。frpc 同机时无需对外监听;改成 0.0.0.0 会给局域网开一条绕过密码门的入口 |
gatePassword | — | 公网密码;也可用设置页写(落 ~/.dsh/dsh-sakurafrp/gate-password.txt,不写入 state.json) |
autoAlign | true | 启动前自动把隧道「本地端口」改成 gatePort |
maxLogLines | 500 | 日志环形缓冲行数 |
管理接口(需密码门 cookie)
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 从服务端自动加载与域名匹配的证书:
插件设置页在隧道跑起来后,会把这句话连同两个入口一起显示出来: 「不绑定证书 ↗ 会出现证书警告、白屏等;绑定子域名后即可消除。 绑定教程 ↗」 「绑定证书」指向子域绑定页,「绑定教程」指向官方文档。 (「不」不带链接——连成一片蓝色会读成「不去绑定证书」,意思正好反了。)
手机端登录后白屏、只有桌宠、侧边栏一滑就收回(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 锁死):
- 补发过一次就种
natfrp_tok(600s),此后不再补,从源头断掉 303 循环; - 同一路径连续补发超过 2 次仍未拿到页面 → 判定浏览器存不住 cookie, 停下来给出明确说明页,而不是继续重定向;
- 上游 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.mjs | 31 | 模块级 + 网关全链路(API 客户端 / frpc 解析 / 密码门 / 围栏 / WS 透传) |
order-test.mjs | 61 | 四步解锁顺序、表单排布、按钮文案与出现条件 |
paths-test.mjs | 51 | frpc / DSH home 路径解析(含 $DSH_HOME 空白、PROFILE_DIR 反推) |
tunnel-create-test.mjs | 50 | 新建隧道(端口来源、extra.auto_https、官方告警回传) |
progressive-test.mjs | 59 | 渐进解锁派生状态与提示语 |
feedback-test.mjs | 37 | 操作反馈、错误码与文案 |
installer-test.mjs | 52 | frpc 资产挑选 / 下载 / 版本校验 / 版本号提取与比较 |
dismiss-test.mjs | 29 | 官方提醒的关闭状态机(按路径记忆) |
traffic-test.mjs | 27 | 流量口径与剩余百分比 |
apply-test.mjs | 26 | apply() 装配、路由注册、冷启动默认值 |
external-test.mjs | 24 | 外链策略(内置标签 / 退化 / 非绝对地址拒绝) |
token-test.mjs | 24 | 密钥写入、清除、掩码 |
diag-test.mjs | 22 | 诊断信息与状态徽标 |
admin-auth-test.mjs | 22 | 网关管理接口鉴权(8 种路径花招 / WS 同权 / 改密后旧 cookie 失效) |
fixes-test.mjs | 31 | 已修 bug 回归(跨盘路径 / 互踢误判 / 密码迁移 / 清空不复活 / API 自愈 / 登录回跳) |
defaults-test.mjs | 19 | 配置默认值 |
update-check-test.mjs | 15 | 手动检查更新(结论 / 60s 节流 / 连点拦截 / hooks 规则) |
scroll-test.mjs | 14 | 主面板滚动容器链 |
state-test.mjs | 12 | state.json 持久化与坏文件兜底 |
switch-test.mjs | 11 | 侧边栏开关状态机(乐观更新 / 连点保护) |
density-test.mjs | 11 | 面板信息密度与读屏高度 |
fresh-install-test.mjs | 10 | 全新安装的冷启动形态 |
credential-token-test.mjs | 5 | tokenRef → 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
dsh-web (dsh-ssh)
zhu1090093659/dsh-web
dsh-web (dsh-remote-web-ui)
zhu1090093659/dsh-web
dsh-web-ui (dsh-ssh)
zhu1090093659/dsh-web-ui
dsh-web-ui (dsh-remote-web-ui)
zhu1090093659/dsh-web-ui