dsh 局域网访问部署教程(Android 手机 Termux)
August 16, 2026 · View on GitHub
目标:让同一 WiFi 下的平板 / 电脑 / 其他手机,用浏览器直接打开手机上的 dsh,看到手机上的对话、能改设置、能建工作区,无需在客户端安装任何东西。
适用:dsh(DeepSeek Harness)运行在 Android 手机的 Termux 内。以下所有命令中的
<占位符>需替换为你的实际值。
目录
1. 总体架构
┌───────────── 同一 WiFi 局域网 ─────────────┐
│ │
│ 平板/电脑/其他手机浏览器 │
│ 打开 http://<手机IP>:3088 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ dsh-lan-proxy.js(LAN 代理) │ 手机 │
│ │ 0.0.0.0:3088 │ │
│ │ ① 开关检查(~/.dsh-lan-on) │ │
│ │ ② Host/Origin 改写成回环 │ │
│ │ ③ HTML 注入 randomUUID 补丁 │ │
│ └─────────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ dsh web(127.0.0.1:3080) │ dsh 本体 │
│ │ 视所有流量为回环 → 全权限 │ │
│ └─────────────────────────────┘ │
│ │
│ 控制面(可选,仅手机本地): │
│ dsh-ctl-server.js :3098 ← 控制台 App │
│ (局域网开关 / 重启 / 状态 / 显示 URL) │
└───────────────────────────────────────────┘
四个端口:
| 端口 | 进程 | 监听地址 | 作用 |
|---|---|---|---|
| 3080 | dsh web | 127.0.0.1 | dsh 本体(CLI 故意禁 --host 0.0.0.0) |
| 3088 | dsh-lan-proxy.js | 0.0.0.0 | 局域网唯一入口,转发 + 回环改写 + 注入补丁 |
| 3090 | dsh-llm-proxy.js | 127.0.0.1 | LLM 代理(剥 SSE 空 id/name、429 退避重试) |
| 3098 | dsh-ctl-server.js | 127.0.0.1 | 控制 API(LAN 开关、重启、健康检查) |
为什么需要代理而不是直接暴露 3080? 因为 dsh 是"单设备设计",有三大障碍(见下节)。代理在中间做三件事把障碍全部绕开。
2. 三大障碍与解法原理
| # | 障碍 | 现象 | 根因(源码位置) | 解法 |
|---|---|---|---|---|
| 1 | 客户端 loopback 门 | LAN 打开页面后只调 inventory 同步(value:[]),workspace.list / session.list / settings.describe 一个都不调,UI 卡"正在加载工作区" | dsh-client-connection/lib/client.js 约 10167 行:isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname) —— 非回环 hostname 时 isLoopback:false,整个数据流不初始化 | 源码补丁:强制 isLoopback: true(rebrain.sh 固化,幂等可重打) |
| 2 | 服务端特权围栏 | 设置/凭据/建工作区返回 403 | dsh-client-connection/lib/index.js 的 PRIVILEGED_METHODS 特权表(settings.、credentials.、host.pickDirectory、llm.discoverModels、agentPreset.*)只对回环连接开放,判断写死为 [] | 源码补丁:3 处 isTrustedApiRequest(request, []) → isTrustedApiRequest(request, this.trustedHosts);options.authority === "loopback" ? [] : this.trustedHosts → this.trustedHosts(rebrain.sh 固化) |
| 3 | 🔑 crypto.randomUUID 缺失 | 页面 console 疯狂报 WebSocket is closed before the connection is established,WS 在 23ms 内被 abort,连接永不建立 | dsh 客户端 JS 用 RpcId(crypto.randomUUID()) 生成 RPC ID,但 crypto.randomUUID 只在安全上下文(HTTPS 或 localhost)存在。http://<局域网IP>:3088 是非安全上下文 → 抛 crypto.randomUUID is not a function → RPC 全崩 → connection abort | 代理层:给 HTML <head> 注入 polyfill(dsh-lan-proxy.js 内置) |
前两个是"门没开",第三个是"钥匙没带"——三者叠加才是"LAN 用不了 dsh"的完整真相。只测 API(curl)会假阳性:API 层 200 但 UI 层罢工,必须用真浏览器验证(见第 7 节)。
另外还有一个"信任主机"配置:start-dsh.sh 启动 dsh web 时带 --trusted-host <手机IP>(无端口 + 带端口双写),否则 LAN 流量在网关层就会被 403 拦下。
3. 安全影响评估
本节回答一个常见顾虑:这套方案会不会危害路由器 / 造成不可逆修改? 结论先行:路由器零接触,没有任何不可逆的修改,但存在一个需要知晓的"局域网可达性"风险敞口(见 3.3)。
3.1 路由器 / 网络层:完全没碰
| 可能担心的操作 | 实际情况 |
|---|---|
| 路由器配置(DHCP/端口转发/防火墙/UPnP/DNS) | 一次都没改过,无需进入路由器管理界面 |
| 静态 IP / 固定 DHCP 分配 | 不需要,IP 动态变化由控制台 App 实时展示 |
| 电脑网络设置 / hosts / 系统代理 | 不需要改动 |
| WiFi 本身 | 不动 |
唯一的"网络动作"是手机上的 3088 端口在局域网内可访问——这是 dsh 自己监听的服务端口(监听在 0.0.0.0:3088),不是路由器端口转发,其他设备只能在 WiFi 内网连到它,外部互联网无法直接访问(除非你手动在路由器上做端口映射,本方案不需要也不建议做)。
3.2 手机 / 电脑改动清单与可逆性
手机(Termux 内)改动——全部可逆:
| 改了什么 | 位置 | 影响 | 可逆性 |
|---|---|---|---|
| dsh 源码补丁(信任门 + isLoopback) | dsh 包 node_modules 内 | 让 LAN 设备能用全功能 | ✅ 重装 dsh 即还原;补丁前留有 .bak 备份 |
| 代理/控制脚本(lan-proxy、ctl-server 等) | Termux ~/ 目录 | 局域网转发、控制 API | ✅ 删文件即撤 |
| 配置文件(settings.yaml、cordis.patch.yml) | dsh 配置目录 | 换 LLM 端点、沙箱策略 | ✅ rebrain 可重写,原配置有备份 |
| workspace.json(可能清理过测试工作区) | ~/.dsh/storages/ | 仅影响你自己创建/删除的工作区 | ✅ 有 .bak 备份 |
| 开机自启 + 监督进程 | ~/.termux/boot/ | 链死后自动拉起 | ✅ 删启动脚本即撤 |
| 控制台 App | 手机应用 | 一键开关局域网 | ✅ pm uninstall 即卸载 |
电脑改动——全部可逆且不在系统目录:
| 改了什么 | 位置 | 可逆性 |
|---|---|---|
| 项目文件(bundle、测试脚本、App 源码) | 用户项目目录(如 <项目根目录>/dsh-shim、<项目根目录>/dsh-console) | ✅ 删除文件夹即撤 |
npm 包 puppeteer-core(验证用) | 用户级 Node 工作目录 | ✅ npm uninstall 即撤 |
| 临时隧道进程 / adb 端口转发(仅调试期使用) | 内存态 | ✅ 已停止并释放端口 |
所有操作没有触碰 C 盘系统文件、注册表、系统服务(Windows 上除临时端口转发与 npm 包外无任何系统级动作)。
3.3 真正的风险敞口:局域网可达性(务必知晓)
3088 端口开着 = 局域网内任何设备都能访问手机上的 dsh。 而 dsh 是能操作手机文件系统的 AI 工具——这意味着 WiFi 内网里任何设备都可以:
- 查看你的对话记录
- 修改 dsh 设置
- 指挥 dsh 执行任务(可能涉及读写手机文件)
风险分级与对策:
| 场景 | 风险 | 建议 |
|---|---|---|
| 家用 WiFi(密码不外泄) | 低,等同家庭内网互访 NAS | 可长期开启 |
| 公司/校园/公共 WiFi | 高,同网段可能有不信任设备 | 不要开局域网开关 |
| WiFi 密码泄露给外人 | 中高 | 关闭开关 + 修改 WiFi 密码 |
防护手段(本方案内置):
- 局域网开关:控制台 App 一键"关闭局域网访问",关闭后其他设备访问 3088 立即返回 403
- 仅监听局域网:不做路由器端口映射,互联网不可达
- 不用时关闭:平板用完随手关,风险归零
3.4 一句话总结
路由器和其他设备零影响;所有修改都在手机 Termux 与电脑用户目录内、均可逆。唯一需要留意的不是"修改"而是"开关打开期间的内网可达性",不用时关闭开关即可。
4. 前置条件
| 项 | 要求 |
|---|---|
| 手机 | Termux 已装 dsh + Node;adb 可连(USB 或 WiFi adb,用于部署) |
| WiFi | 手机与平板/电脑在同一局域网 |
| 保活 | Termux 已加电池白名单(部分厂商如 vivo/小米系统杀后台较凶,不配好这条 dsh 链随时会死)+ Termux:Boot 已装并允许自启 |
| 电脑(部署用) | 装了 adb 的 Windows/Mac/Linux;也可直接在 Termux 内操作 |
5. 部署步骤
以下命令以电脑 + adb 为例。将
<项目根目录>替换为你的 bundle 所在路径,<手机IP>为手机当前 wlan0 IP(可在 Termux 里ip route get 1 | grep -oE 'src [0-9.]+'查看)。
5.1 准备 bundle
所有文件放在一个目录(下称 bundle 目录,形如 <项目根目录>/dsh-shim/dsh-brain-swap/):
| 文件 | 作用 |
|---|---|
dsh-lan-proxy.js | LAN 代理(开关 + 回环改写 + polyfill 注入) |
dsh-ctl-server.js | 控制 API 服务 |
start-dsh.sh | 启动脚本(检测 wlan0 IP → 启动 dsh web + llm-proxy + lan-proxy + ctl-server) |
rebrain.sh | 重装大脑脚本:幂等打全部源码补丁 + 部署配置与代理 + 拉起监督链 |
dsh-supervise.sh | 监督循环(进程死 5s 自动重启) |
sandbox-shim.js / settings.yaml / cordis.patch.yml / 99-dsh.sh | 沙箱、配置、开机自启等配套 |
5.2 推送到手机并注入(电脑执行)
cd <项目根目录>/dsh-shim/dsh-brain-swap
for f in dsh-lan-proxy.js dsh-ctl-server.js start-dsh.sh rebrain.sh dsh-supervise.sh sandbox-shim.js settings.yaml cordis.patch.yml 99-dsh.sh; do
adb push $f /sdcard/Download/$f
done
adb shell "run-as com.termux sh -c '
for f in dsh-lan-proxy.js dsh-ctl-server.js start-dsh.sh rebrain.sh dsh-supervise.sh sandbox-shim.js settings.yaml cordis.patch.yml 99-dsh.sh; do
cat /sdcard/Download/\$f > /data/data/com.termux/files/home/dsh-brain-swap/\$f
done
rm -f /sdcard/Download/dsh-lan-proxy.js /sdcard/Download/dsh-ctl-server.js /sdcard/Download/start-dsh.sh /sdcard/Download/rebrain.sh /sdcard/Download/dsh-supervise.sh /sdcard/Download/sandbox-shim.js /sdcard/Download/settings.yaml /sdcard/Download/cordis.patch.yml /sdcard/Download/99-dsh.sh
'"
⚠️
run-as com.termux的 shell 与普通 shell 不同(UID 1000),直接cp到目标目录会静默写出 0 字节文件,必须用cat >。这是踩过的坑。
5.3 打补丁(rebrain.sh 幂等执行)
adb shell "run-as com.termux sh -c '
pkill -f \"[d]sh-supervise[.]sh\" 2>/dev/null
sleep 1
REBRAIN_NO_LAUNCH=1 sh /data/data/com.termux/files/home/dsh-brain-swap/rebrain.sh
'"
rebrain 会依次做(全部幂等,重复执行自动跳过,dsh 升级后重跑即可重新打上):
- 部署
sandbox-shim.js(沙箱 shim) - 部署配置
settings.yaml+cordis.patch.yml - 部署
dsh-lan-proxy.js/dsh-llm-proxy.js/start-dsh.sh/dsh-ctl-server.js到$HOME - 补丁 A:persistence 硬链 → rename
- 补丁 B:client-connection 信任门(3 处
[]→this.trustedHosts) - 补丁 C:client.js
isLoopback: true强制
验证补丁是否打上:
adb shell "run-as com.termux sh -c '
F=/data/data/com.termux/files/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-connection
grep -c \"isTrustedApiRequest(request, this.trustedHosts)\" \$F/lib/index.js # 期望 ≥2
grep -c \"LAN-patch\" \$F/lib/client.js # 期望 ≥1
grep -c LOOPBACK_HOST \$HOME/dsh-lan-proxy.js # 期望 ≥1
'"
5.4 启动整条链
adb shell "run-as com.termux sh -c '
setsid sh /data/data/com.termux/files/home/dsh-brain-swap/dsh-supervise.sh \
> /data/data/com.termux/files/home/dsh-supervise.log 2>&1 < /dev/null &
'"
sleep 40 # 冷启动等待
5.5 验证四个端口
adb shell "run-as com.termux sh -c '
export PATH=/data/data/com.termux/files/usr/bin:\$PATH
for p in 3080 3088 3090 3098; do
curl -s -m 2 -o /dev/null -w \"\$p=%{http_code} \" http://127.0.0.1:\$p/
done
echo
ip route get 1 | grep -oE \"src [0-9.]+\" | cut -d\" \" -f2 # 当前 wlan0 IP
'"
5.6 (可选)装控制台 App
控制台 App(原生 UI,显示实时 LAN URL + 在线状态 + 局域网开关 + 一键重启):
cd <项目根目录>/dsh-console
adb push out/app-debug.apk /data/local/tmp/dsh-console.apk
adb shell pm install -r -g /data/local/tmp/dsh-console.apk
adb shell am start -n <应用包名>/.MainActivity
6. 客户端使用
使用方(平板/电脑/其他手机)不需要装任何东西:
- 看手机控制台 App 显示的大绿色地址(形如
http://<手机IP>:3088/) - 在浏览器地址栏输入该地址,回车
- 即可看到手机上的工作区、真实对话、设置、模型选择——与在手机上用完全一致
注意事项:
- IP 会变:路由器 DHCP 可能重新分配地址,每次使用前看一眼 App 上的最新地址即可(App 实时读取 wlan0)
- 局域网开关:在 App 上可随时"关闭局域网访问"(关闭后其他设备访问立即 403)
- 首次加载有"内测声明"弹窗,点继续即可(只出现一次)
7. 验证方法(真浏览器,不是 curl)
⚠️ 血的教训:只测 curl / API 脚本 = 假阳性。API 层 200 不代表 UI 能用(loopback 门在客户端 JS 里,curl 不触发)。必须用真实浏览器打开页面,检查 API 调用、localStorage、截图。
电脑上推荐 puppeteer-core + Edge 无头浏览器:
// verify-lan.js —— 需先:npm install puppeteer-core
const puppeteer = require('puppeteer-core');
(async () => {
const b = await puppeteer.launch({
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
headless: 'new',
});
const page = await b.newPage();
const apis = [];
page.on('request', r => { if (r.url().includes('/api/')) apis.push(r.url()); });
await page.goto('http://<手机IP>:3088/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 8000));
const state = await page.evaluate(() => ({
ls: Object.keys(localStorage),
body: document.body.innerText.slice(0, 300),
}));
console.log('API 调用数:', apis.length);
console.log('关键 API:', apis.filter(u => /workspace|session|settings|credentials/.test(u)).slice(0, 10));
console.log('localStorage:', state.ls);
console.log('页面文本:', state.body.slice(0, 150));
await page.screenshot({ path: 'lan-check.png' });
await b.close();
})();
判定通过的标准:
- ✅ 页面出现手机上已有的工作区和对话文本
- ✅ 调用了 workspace.list / session.list / settings.describe 等数据 API(≥20 个)
- ✅ localStorage 出现
dsh.workspace.view.v5等键 - ✅ 截图里能看到对话内容
再手动验证设置页可打开、可新建工作区、可发消息。
8. 常见问题排查
| 现象 | 原因 | 处理 |
|---|---|---|
其他设备打不开 http://<IP>:3088 | ① IP 不对(不是手机当前 IP)② 链死了(系统杀了 Termux)③ 局域网开关关了 | ① 看 App 绿色地址 ② App 点"启动/重启 dsh" ③ App 点"开启局域网访问" |
| 能打开但一直"正在加载工作区" | polyfill 未注入(代理是旧版) | 确认 grep -c LOOPBACK_HOST ~/dsh-lan-proxy.js ≥1;重启代理后按 5.4 重启链 |
| 设置/凭据 403 | 特权围栏补丁没打上 | 跑 rebrain(5.3)并验证 grep -c 结果 |
| 页面能开但对话不显示 | loopback 门补丁没打上(client.js 旧版) | 跑 rebrain 重打补丁 C |
| 手机本地 3080 正常、LAN 全挂 | LAN 代理进程死了 / 多个代理抢 3088 | pkill -f "[d]sh-lan-proxy[.]js",确认只剩 1 个代理进程后重启链 |
| 发消息 429 限流 | LLM token 全网段限流窗口(测试过猛触发) | 等几分钟重试,与 LAN 无关 |
| 重启手机后链没了 | 未配 Termux:Boot 自启或电池白名单 | 检查 Termux 电池优化白名单 + ~/.termux/boot/99-dsh.sh 存在;或手动:run-as com.termux sh -c 'setsid sh $HOME/dsh-brain-swap/dsh-supervise.sh > $HOME/dsh-supervise.log 2>&1 < /dev/null &' |
| 改过 workspace.json 后 dsh 启动崩循环 | 结构是 {unit, global:{workspaceIds:[...]}, tables:{workspaces:{id:data}}},删条目必须同时删 global.workspaceIds 引用 | 补删引用或从备份恢复 |
9. 维护与升级
- dsh 升级后:重跑
rebrain.sh(5.3)即可把全部源码补丁重新打上(幂等、自动跳过已打的) - bundle 更新:改完本地文件 → 重复 5.2 → 5.4(重启链)
- 修改代理后只重启代理:
pkill -f "[d]sh-lan-proxy[.]js"→ 用带HOME/LAN_PORT/DSH_WEB环境的命令重新拉起(注意run-as不自动设 HOME,必须export HOME=/data/data/com.termux/files/home,否则~/.dsh-lan-on找不到,开关判断错误) - pkill 自匹配陷阱:
pkill -f的模式必须用括号防自匹配(如[d]sh-lan-proxy[.]js),且不要在同一命令行里又 pkill 又 setsid 启动同名进程(外层命令字符串含路径名会自杀,报 143)
10. 彻底卸载 / 回滚
如果不想再用了,按顺序执行即可完全撤掉:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1. 关局域网 | 控制台 App 点"关闭局域网访问",或删手机上的 ~/.dsh-lan-on | 其他设备立即无法访问 |
| 2. 停监督链 | pkill -f "[d]sh-supervise[.]sh"; pkill -f "[b]in[.]js web" 及四个进程 | 停止所有服务 |
| 3. 撤开机自启 | 删除 ~/.termux/boot/99-dsh.sh | 重启不再拉起 |
| 4. 还原 dsh 源码补丁 | 重装 dsh 包;或用补丁前 .bak 备份恢复 | 恢复官方原版行为 |
| 5. 删代理/脚本 | 删除 ~/dsh-lan-proxy.js、~/dsh-ctl-server.js、~/start-dsh.sh、~/dsh-brain-swap/ 等 | 清空部署产物 |
| 6. 卸载 App | pm uninstall <应用包名> | 移除控制台 App |
| 7. 清电脑 | 删除 <项目根目录>/dsh-shim、<项目根目录>/dsh-console;npm uninstall puppeteer-core | 清空本地产物 |
11. 关键文件速查
| 位置 | 文件 | 说明 |
|---|---|---|
手机 ~/ | dsh-lan-proxy.js | LAN 代理(核心交付物) |
手机 ~/ | dsh-ctl-server.js | 控制 API(3098) |
手机 ~/ | start-dsh.sh | 一键起链(自动检测 IP) |
手机 ~/dsh-brain-swap/ | rebrain.sh | 幂等重装 + 打全部补丁 |
手机 ~/dsh-brain-swap/ | dsh-supervise.sh | 保活监督循环 |
手机 ~/.termux/boot/ | 99-dsh.sh | 开机自启 |
手机 ~/ | .dsh-lan-on | 局域网开关标志(存在=开) |
| 电脑 | <项目根目录>/dsh-shim/dsh-brain-swap/ | bundle 源(与手机同步) |
| 电脑 | <项目根目录>/dsh-console/ | 控制台 App 源码 + out/app-debug.apk |