dsh-service-portal
Exposes any local service on a LAN address and a public cloudflared quick-tunnel domain behind one password login, with per-route open/block gates managed from the plugin settings page. Requires Python 3 and cloudflared on the machine, and reports either of them being missing or misconfigured in the
Install
dsh plugin --profile web add dsh-service-portal
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
<p align="center">
<img src="icons/dsh-service-portal-64.png" width="64" alt="dsh-service-portal" />
</p>
<h1 align="center">dsh-service-portal</h1>
<p align="center"><b>一个随机域名 + 一个随机口令,把本机的服务按需对外开放。</b></p>
<p align="center">
<a href="README.en.md">English</a> ·
<a href="#安装">安装</a> ·
<a href="#工作原理">工作原理</a> ·
<a href="#在设置里管理节点">节点管理</a> ·
<a href="CHANGELOG.md">更新日志</a> ·
<a href="https://github.com/dingminhua/dsh-service-portal/issues">问题反馈</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/dsh-service-portal"><img src="https://img.shields.io/npm/v/dsh-service-portal?style=flat-square&label=npm&color=cb3837" alt="npm version"></a>
<a href="https://www.npmjs.com/package/dsh-service-portal"><img src="https://img.shields.io/npm/d18m/dsh-service-portal?style=flat-square&label=downloads&color=cb3837" alt="npm downloads"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/dingminhua/dsh-service-portal?style=flat-square" alt="MIT license"></a>
<a href="https://github.com/dingminhua/dsh-service-portal/stargazers"><img src="https://img.shields.io/github/stars/dingminhua/dsh-service-portal?style=flat-square" alt="GitHub stars"></a>
</p>
dsh-service-portal(对外服务门户) 是一个 DeepSeek Harness (DSH) 插件。
它把任意本机服务按需暴露到外网:cloudflared 建一个随机域名,一道口令登录
(只需口令,没有账号)守住入口,设置页一键开关并显示连接与口令。
它最初长在 ld-rl-poker 里(为了把训练配置台/模型查看台放到外网),但门户本身与德州扑克无关——
任何本地服务都能用同一套「一个域名 + 一个口令 + 按需挂载」。
功能特性
- 装好即用,不用先点开关 —— 插件加载后门户自动起来(只开内网,不会自动把本机
服务挂到公网)。打开设置页就能看到内网地址和二维码,扫一下就进。
外网暴露必须由你显式点「开启公网访问」。
- 记得你上次的选择 —— 点过「停止门户」,下次打开 DSH 就不再自动启动它;点过
「启动」,下次打开照旧自动启动(仍然只开内网,公网永远是手动开)。
也就是"默认开着,之后跟着你走":它写回插件配置的 autoStart,可以在配置里手改,
与点按钮等效。设置页会明说当前是哪一种,不让你猜。
- 设置页就是 dsh-pocket 那个形态 —— 局域网、公网两个区块,各自带开关 + 二维码 +
地址 + 访问密码行 + 高级(手动选地址)。
- 节点管理就在设置页里(设置 → 左侧导航「对外服务门户」)—— 添加节点(名称 / 对外路径 / 本机端口 / 备注)、
开放 / 屏蔽(路由闸门)、编辑、删除,以及每个节点的两个访问入口:
「内网访问」(走局域网地址)与「公网访问」(走隧道地址)。
两者各自按自己的开关判断能否点击 —— 关掉局域网后内网按钮变灰、公网照常可用;
没开公网时公网按钮变灰,并写明"先点「开启公网访问」"。
不用改配置文件、不用重启。
- 只管路由,不管启停 —— 「屏蔽」只表示门户不再把这条路由转过去,
不会停止上游程序;上游由你自己的方式启动(插件不猜它的启动命令)。
- 自带门户网页(只读) —— 扫码打开的是测试页 + 引导页:
列出已发布的节点,每个节点有「访问」(打开它)和「测试」
(就地探一次通不通并把结论写在卡片里,不用跳走):
- 「通了:HTTP 200」= 门户转发成功且上游有响应;
- 「不通:HTTP 502 … 请用它自己的方式启动那个程序」= 上游没起;
- 「上游有响应,但它自己返回了 HTTP 502」= 上游在线、是它内部出错。
最后这条必须和上一条分开说 —— 状态码一样,把上游自己的错报成
「没启动」会让人去启动一个本来就在跑的程序。
没有任何设置功能 —— 要改配置请到 DSH 设置 → 对外服务门户。
- 内网 + 外网两种访问 —— 同一张页面,两条入口:
- 内网(
http://192.168.x.x:8899)默认免密,同一 WiFi 下手机/平板直接打开。
可开「局域网访问密码」,也可以直接关掉整个局域网访问;
关掉后内网请求直接 403(一张说明页,不是登录页 —— 手机那边不是决策者,
看到的是「请到电脑上开启」),公网与本机不受影响;
- 外网(
https://<随机>.trycloudflare.com)必须口令,人在外面也能用。
每次开启要先用勾选式免责声明确认一次(服务端也校验,防绕过前端直接打接口)。
- 两个开关改完立即生效,不用重启门户(重启会换掉隧道域名,代价太大)。
- 本机访问永远免密 —— 否则你在电脑上连自己的门户都进不去。
- 手机扫码即用 —— 设置页与门户页都显示二维码(内联 SVG,不依赖外部请求),
手机扫一下就到;页面按窄屏自适应。
- 只需口令,不要账号 —— 自定义登录页(只有口令输入框),不弹浏览器原生
「账号 + 密码」框;口令可每次随机或你自己设固定值。
- 登录限速 —— 单 IP 连续失败 5 次锁 60 秒,另有全局阈值防分布式撞码。
- 服务状态一目了然 —— 每个服务显示 已启用/未启用/存活探测失败,未启用的不可点;
健康检查是真探测(TCP 连上游端口),不是标记位。
- 控制检验(本仓库硬纪律)—— 26 个 Python 控制检验脚本 + 8 个端到端脚本 + 160 项 Node
检验;依赖缺失必须「跳过并返回 2」,不得静默通过。
界面
两张图都是真页面渲染出来的截图(无头 Chrome 实拍,不是设计稿),且全部用演示数据
(设置页那张由 tools/render_panel_preview.mjs 的 ?s=svcboth 渲染):
192.168.71.10 是占位内网地址、warm-dawn-1234.trycloudflare.com 是占位域名、
口令是示例串。README 里不放任何真实地址或口令。
<p align="center">
<img src="docs/assets/panel-both.png" width="760" alt="设置页:局域网与公网两条入口,各带二维码、地址与口令">
<br>
<sub><b>设置页</b>(设置 → 左侧导航「对外服务门户」):内网 / 公网两条入口并排,
各自判断开关;扫码即开,节点增删在此</sub>
</p>
门户页是扫码打开的那个页面——只读的测试页与入口页:点「访问」直接打开节点,
点「测试」就地探一次通不通。它刻意做得极简:没有设置功能,设置只在上面那个页面里。
<p align="center">
<img src="docs/assets/portal-console.png" width="760" alt="门户控制台:节点列表,每个节点有访问与测试按钮">
<br>
<sub><b>门户页</b>:节点列表 —「访问」打开,「测试」就地探通不通</sub>
</p>
窄屏与深色
门户页是给手机扫进来的,所以窄屏与深色是主场景,不是附属:
窄屏(浅色) · 窄屏(深色) · 宽屏
<img src="https://raw.githubusercontent.com/dingminhua/dsh-service-portal/main/docs/assets/portal-phone.png" width="220" alt="门户页 窄屏 浅色"> · <img src="https://raw.githubusercontent.com/dingminhua/dsh-service-portal/main/docs/assets/portal-phone-dark.png" width="220" alt="门户页 窄屏 深色"> · <img src="https://raw.githubusercontent.com/dingminhua/dsh-service-portal/main/docs/assets/portal-wide.png" width="420" alt="门户页 宽屏">
工作原理
┌─────────── 门户进程(一个端口,默认 8899)───────────┐
内网 http://192.168.x.x:8899 ──┤ │
├──▶ 按 Host 分类:loopback / lan / public │
外网 https://<随机>.trycloudflare.com ─┘ │ │
├─ / → 门户页(只读:测试 + 引导)
├─ /_auth/login → 口令登录(外网)
├─ /_api/* → 门户 API(受 DSH 插件设置调用)
└─ /<路由路径>/… → 转发到对应服务
认证强度按来源区分(照搬 dsh-pocket 的模型,见[致谢](#致谢)):
Host 分类 · 判定 · 门户页 / 服务
loopback · localhost、127.x、::1 · 免密
lan · RFC1918 私网、CGNAT(100.64/10)、.local、单标签名 · 免密
public · 其余一切(含你自建域名) · 必须口令
兜底方向是安全的一侧:认不出的 Host 一律当公网。反着写(只把 *.trycloudflare.com
当外网、其余当内网)会让人挂个自己的域名就绕过认证——dsh-pocket 踩过这个坑
(它的 issue #66),本项目直接按 fail-closed 实现。
安装
方式一:npm(发布后)
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" & $dsh plugin --profile desktop add dsh-service-portal
方式二:link 本地插件(开发中)
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" & $dsh plugin --profile desktop add "link:D:/DshProject/dsh-service-portal"
装完需要重启 DSH(插件在宿主进程里加载)。
若旧 profile 仍装有已删除的子插件 dsh-poker-viewer,先移除再装根插件:
`& $dsh plugin --profile desktop remov
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::16,最后更新 2026-11-05