扫码用手机或异地浏览器打开本机 DSH 界面
30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/shimiei/dsh-webim.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:仅索引
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 10 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 88 处,展示前 74 条)
- 读取本地文件lib/index.js:59
if (existsSync(logFile) && statSync(logFile).size > 256 * 1024) writeFileSync(logFile, '') - 读取本地文件lib/index.js:59
if (existsSync(logFile) && statSync(logFile).size > 256 * 1024) writeFileSync(logFile, '') - 写入本地文件lib/index.js:59
if (existsSync(logFile) && statSync(logFile).size > 256 * 1024) writeFileSync(logFile, '') - 写入本地文件lib/index.js:60
appendFileSync(logFile, `${line}\n`) - 发起网络请求lib/index.js:132
const request = http.get( - 读取本地文件lib/index.js:217
return existsSync(file) ? readFileSync(file, 'utf8').trim() : '' - 读取本地文件lib/index.js:217
return existsSync(file) ? readFileSync(file, 'utf8').trim() : '' - 发起网络请求lib/index.js:237
const response = await fetch(url, { headers, redirect: 'manual', signal: AbortSignal.timeout(8_000) }) - 请求外部主机api.ipify.orglib/index.js:287
const response = await fetch('https://api.ipify.org?format=json', { - 发起网络请求lib/index.js:287
const response = await fetch('https://api.ipify.org?format=json', { - 写入本地文件lib/index.js:580
mkdirSync(config.stateDir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/index.js:581
writeFileSync( - 写入剪贴板client.js:330
await navigator.clipboard.writeText(text) - 读取本地文件lib/gateway.js:177
const info = await stat(fileUrl) - 读取本地文件lib/gateway.js:180
const content = await readFile(fileUrl) - 写入本地文件bin/serve.mjs:160
mkdirSync(stateDir, { recursive: true }) - 写入本地文件bin/serve.mjs:162
writeFileSync( - 读取本地文件bin/serve.mjs:174
if (!existsSync(stateFile)) return - 读取本地文件bin/serve.mjs:175
const saved = JSON.parse(readFileSync(stateFile, 'utf8')) - 删除本地文件bin/serve.mjs:176
if (saved.pid === process.pid) unlinkSync(stateFile) - 发起网络请求bin/serve.mjs:220
const request = http.get( - 读取本地文件lib/credentials.js:50
const text = readFileSync(file, 'utf8') - 读取本地文件lib/token.js:88
if (existsSync(this.file)) { - 读取本地文件lib/token.js:89
const raw = JSON.parse(readFileSync(this.file, 'utf8')) - 写入本地文件lib/token.js:123
mkdirSync(this.stateDir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/token.js:125
writeFileSync( - 写入本地文件lib/token.js:135
renameSync(temp, this.file) - 写入本地文件lib/token.js:137
chmodSync(this.file, 0o600) - 读取本地文件lib/urls.js:94
if (!existsSync(this.file)) return - 读取本地文件lib/urls.js:95
const saved = readFileSync(this.file, 'utf8').trim() - 读取本地文件lib/urls.js:120
if (existsSync(this.file)) unlinkSync(this.file) - 删除本地文件lib/urls.js:120
if (existsSync(this.file)) unlinkSync(this.file) - 写入本地文件lib/urls.js:123
mkdirSync(this.dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/urls.js:124
writeFileSync(this.file, `${normalized}\n`, { encoding: 'utf8', mode: 0o600 }) - 读取本地文件lib/urls.js:161
if (!existsSync(this.file)) return - 读取本地文件lib/urls.js:162
const saved = normalizePublicUrl(readFileSync(this.file, 'utf8')) - 读取本地文件lib/urls.js:180
if (existsSync(this.file)) unlinkSync(this.file) - 删除本地文件lib/urls.js:180
if (existsSync(this.file)) unlinkSync(this.file) - 写入本地文件lib/urls.js:183
mkdirSync(this.dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/urls.js:184
writeFileSync(this.file, `${normalized}\n`, { encoding: 'utf8', mode: 0o600 }) - 读取本地文件lib/features.js:109
if (!existsSync(this.file)) return - 读取本地文件lib/features.js:110
const raw = JSON.parse(readFileSync(this.file, 'utf8')) - 读取本地文件lib/features.js:164
if (existsSync(this.file)) unlinkSync(this.file) - 删除本地文件lib/features.js:164
if (existsSync(this.file)) unlinkSync(this.file) - 写入本地文件lib/features.js:173
mkdirSync(this.dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/features.js:175
writeFileSync(temp, `${JSON.stringify({ version: 1, features: this.value }, null, 2)}\n`, { - 写入本地文件lib/features.js:179
renameSync(temp, this.file) - 执行外部命令固定命令lib/tunnel.js:104
const killer = spawn('taskkill', ['/pid', String(pid), '/f', '/t'], { windowsHide: true, stdio: 'ignore' }) - 读取本地文件lib/tunnel.js:130
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate - 读取本地文件lib/tunnel.js:130
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate - 读取本地文件lib/tunnel.js:197
if (existsSync(this.tokenFile)) this.token = readFileSync(this.tokenFile, 'utf8').trim() - 读取本地文件lib/tunnel.js:197
if (existsSync(this.tokenFile)) this.token = readFileSync(this.tokenFile, 'utf8').trim() - 读取本地文件lib/tunnel.js:202
if (existsSync(this.namedFile)) { - 读取本地文件lib/tunnel.js:203
const raw = JSON.parse(readFileSync(this.namedFile, 'utf8')) - 写入本地文件lib/tunnel.js:224
mkdirSync(this.stateDir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/tunnel.js:226
writeFileSync(temp, `${JSON.stringify({ url: this.url, startedAt: this.startedAt, mode: this.mode }, null, 2)}\n`, { - 写入本地文件lib/tunnel.js:230
renameSync(temp, this.file) - 写入本地文件lib/tunnel.js:260
mkdirSync(this.stateDir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/tunnel.js:262
writeFileSync(this.tokenFile, `${this.token}\n`, { encoding: 'utf8', mode: 0o600 }) - 写入本地文件lib/tunnel.js:264
chmodSync(this.tokenFile, 0o600) - 删除本地文件lib/tunnel.js:314
if (pid === undefined || Number(raw && raw.pid) === pid) unlinkSync(this.lockFile) - 发起网络请求lib/tunnel.js:363
const response = await fetch(`${DOWNLOAD_BASE}/${asset}`, { redirect: 'follow' }) - 执行外部命令来自配置/变量lib/tunnel.js:428
const child = spawn(binary, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) - 执行外部命令固定命令lib/tunnel.js:533
const killer = spawn('taskkill', ['/pid', String(pid), '/f', '/t'], { windowsHide: true, stdio: 'ignore' }) - 执行外部命令来自配置/变量lib/tunnel.js:562
const child = spawn(binary, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) - 读取本地文件lib/security.js:46
if (existsSync(file)) { - 读取本地文件lib/security.js:47
const existing = fromB64url(readFileSync(file, 'utf8').trim()) - 写入本地文件lib/security.js:53
mkdirSync(dir, { recursive: true, mode: 0o700 }) - 写入本地文件lib/security.js:55
writeFileSync(file, `${b64url(created)}\n`, { encoding: 'utf8', mode: 0o600 }) - 写入本地文件lib/security.js:57
chmodSync(file, 0o600) - 发起网络请求lib/proxy.js:203
const upReq = http.request({ - 发起网络请求lib/proxy.js:341
const upReq = http.request({ - 读取本地文件lib/files.js:347
createReadStream(real, { start: range.start, end: range.end }).pipe(res) - 读取本地文件lib/files.js:357
createReadStream(real).pipe(res).on('error', (error) => {
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:api.ipify.org
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
把本地 DeepSeek Harness 带到手机与异地浏览器:复用同一套 DSH 前端,token/二维码进入,内置 Cloudflare 隧道,零运行时依赖
README
由 DeepSeek 最新模型翻译生成不改 DeepSeek Harness 的一行代码,就把这台机器的 DSH 变成手机、平板、异地浏览器都能打开的网站。
复用同一套 DSH 前端 · token 扫码即进 · 内置 Cloudflare 隧道 · 手机层单独适配 · 零运行时依赖
这是什么
DeepSeek Harness(DSH)默认只监听 127.0.0.1 的随机端口,同一个 Wi-Fi 下的手机都连不上,更别说人在外面。
dsh-webim 是一个 DSH 插件:它在 0.0.0.0:3081 起一个反向代理,把本机 Harness 的 HTTP 与 WebSocket 原样桥接出去,并在 DSH 的设置页里长出一块「远程接入」面板,负责入口地址、二维码、令牌、隧道和一堆运行时开关。
它不修改 DSH 本体:前端还是那套前端,工作区、会话、插件全都在,只是多了一个经过 token 校验的入口。远程浏览器拿到的是 DSH 自己的 UI,实际驱动的是这台机器上的 Harness 进程——本地绝对路径(工作区、file:// 引用、会话存储)永远留在服务端。
展开完整文档(README 剩余 1 节)
不改 DeepSeek Harness 的一行代码,就把这台机器的 DSH 变成手机、平板、异地浏览器都能打开的网站。
复用同一套 DSH 前端 · token 扫码即进 · 内置 Cloudflare 隧道 · 手机层单独适配 · 零运行时依赖
这是什么
DeepSeek Harness(DSH)默认只监听 127.0.0.1 的随机端口,同一个 Wi-Fi 下的手机都连不上,更别说人在外面。
dsh-webim 是一个 DSH 插件:它在 0.0.0.0:3081 起一个反向代理,把本机 Harness 的 HTTP 与 WebSocket 原样桥接出去,并在 DSH 的设置页里长出一块「远程接入」面板,负责入口地址、二维码、令牌、隧道和一堆运行时开关。
它不修改 DSH 本体:前端还是那套前端,工作区、会话、插件全都在,只是多了一个经过 token 校验的入口。远程浏览器拿到的是 DSH 自己的 UI,实际驱动的是这台机器上的 Harness 进程——本地绝对路径(工作区、file:// 引用、会话存储)永远留在服务端。
| 能力 | 说明 |
| 同一套前端 | 反向代理透传 HTTP + Upgrade(/api/remote.mux 实时流),不劫持、不改写 DSH 的页面 |
| 扫码即进 | 设置页内联 SVG 二维码(自研编码器,无外部图片请求,绕开 CSP img-src),扫完直接落到 DSH |
| 令牌可轮换 | 32 字节 base64url token,一次刷新即硬切:旧链接、旧二维码、已登录会话一起失效 |
| 三种接入方式 | 仅局域网 / 公网直连(自己的 IP 或域名)/ Cloudflare 快速隧道;一次只开一种,也能让隧道与公网并存 |
| 内置隧道 | 自动下载并拉起 cloudflared quick tunnel,几秒后拿到 https://xxx.trycloudflare.com,不用配端口映射 |
| 手机层 | 窄屏抽屉、输入框不再被键盘顶飞、设置页单列,interactive-widget=resizes-content |
| 运行时开关 | 总开关 / 局域网 / 公网 / 移动端 / 隧道 / 文件查看 / 路径转链接 / 浮动入口,全都不用重启 |
| 远程安全 | 令牌门 + 失败限流与封禁 + 上游 Cookie 由本机 .credentials.yaml 铸造,凭据不出机器 |
| 零依赖 | 只用 Node 标准库:QR 编码器、PNG 编码器、代理、二维码都是自己写的 |
截图
取自真实运行中的面板,所有 IP、域名、令牌与二维码都已替换成文档占位值(RFC 5737 / example.com)。
手机端(390pt):单列、入口卡片与二维码,扫码即进
三种接入方式
设置页只有一块分段控件,选完即生效(内部就是三个布尔开关的组合):
| 方式 | 实际写入 | 效果 |
| 仅局域网 | {lan: true, public: false, tunnel: false} | 只出 http://<本机 IP>:3081/?token=…,同一 Wi-Fi 可用,不暴露到公网 |
| 公网直连 | {lan: true, public: true, tunnel: false} | 先列你保存的公网 IP / 域名(自己做端口映射或反代),再列局域网地址 |
| Cloudflare 隧道 | {lan: true, public: false, tunnel: true} | 启动后追加 https://xxx.trycloudflare.com;公网地址默认不参与,需要时勾卡片里的「同时列出公网直连地址」 |
三个开关各自独立,也各有含义:
| 开关 | 关掉之后 |
| 总开关 | 监听彻底停掉:3081 无监听、隧道一起停、入口列表清空;桌面端不受影响 |
| 局域网可访问 | 监听从 0.0.0.0 重绑到 127.0.0.1,同网段设备连不上,隧道照旧(它连的是本机回环) |
| 公网入口 | 不再展示保存的公网地址,隧道不受影响(v1.5.1 起两者解耦) |
二维码永远跟随被点选的那张入口卡,不是「排在第一个的那个」;指向消失时自动回落到列表第一项,不会留死链。
快速开始
要求:Windows / macOS / Linux,Node ≥ 18,已安装并能正常打开 DeepSeek Harness。
git clone https://github.com/<you>/dsh-webim.git
cd dsh-webim
装进 DSH 的 desktop profile(建链接 + 写 patch,幂等)
node scripts/install.mjs
然后重启 / 刷新 DSH 窗口,打开 设置 → 远程接入:
1. 顶栏确认「入口运行中」,记住 host:port(默认 0.0.0.0:3081);
2. 选一种接入方式(想最省事就选 Cloudflare 隧道,等几秒拿到 https 域名);
3. 点入口卡片选中它,扫右侧二维码,或直接复制链接。
想手动安装也可以:在 <profile>/node_modules/ 下指向本仓库建一个链接,把 dsh-webim 加进 profile 的 dsh.profile.bundles,再在 cordis.patch.yml 里加一段配置(见下一节)。
不用插件也能跑(调试用)
node bin/serve.mjs --host 0.0.0.0 --port 3081
它和插件共用 lib/gateway.js,只是没有设置页,参数从命令行来。
配置
配置写在 profile 的 cordis.patch.yml(用户覆盖层),或直接改插件自带 cordis.patch.yml 的默认值:
- id: dsh-webim
config:
enabled: true # 插件是否加载(与「是否监听」是两件事) host: "0.0.0.0" # 监听地址;0.0.0.0 = 所有网卡 port: 3081 password: "" # 口令门;留空 = 只认 token 链接 accessToken: "" # 固定 token;留空 = 自动生成,可在设置页刷新 publicUrl: "" # 公网 IP / 域名(含端口),如 http://1.2.3.4:3081 sessionDays: 30 maxFailures: 8 # 同一 IP 失败多少次锁定 failureWindowMinutes: 10 blockMinutes: 15
allowedRoots: [] # 远程文件查看允许的根;空 = 所有盘符
deniedPaths: [] # 额外拒绝路径(默认已拒绝 $DSH_HOME 里的凭据)
linkifyPaths: true
logRequests: false
tunnelBinary: "" # 指定现成的 cloudflared;留空 = 用下面自动下载的那份 tunnelAutoInstall: true # false = 不自动下载 cloudflared features: # 只提供首启默认值,运行时以 features.json 为准 enabled: true lan: true public: true mobileUi: true tunnel: false fileBrowser: true linkifyPaths: true pill: true
运行时状态都在 $DSH_HOME/webim/:token.json、public-url.txt、qr-target.txt、features.json、tunnel.json、plugin.log(插件日志,排障用)、cloudflared/。删掉对应文件即回到默认。
工作原理
几条关键设计:
- 只在入口校验:token 或口令任一通过即铸造 DSH 自己的 dsh-auth- Cookie,之后由上游继续认这个 Cookie;网关不缓存任何会话内容。
- WebSocket 桥接:Upgrade 直接接到上游,/api/remote.mux 的实时流不断。
- 隧道与监听解耦:隧道连的是本机回环,所以关掉局域网、甚至只绑 127.0.0.1,隧道仍然可用。
- 设置页 RPC 先回包再干活:切模式会重绑端口,导致「回包跟着旧连接一起死」;因此面板先就地更新,再后台轮询等状态回来。
- QR 自研:Byte 模式、版本 1–20、L/M/Q/H、8 种掩码,输出内联 SVG;tests/qr.test.mjs 与 Python qrcode 做了矩阵对照。
目录结构
lib/ 网关、代理、安全、文件、隧道、二维码、入口地址推导 assets/ 注入到 DSH 前端的小补丁(移动端抽屉、状态胶囊、路径点击) client.js 设置页(React,走 DSH 的 slot 与设计 token) bin/serve.mjs 独立模式入口 scripts/ 安装 / 卸载 tests/ 81 个测试(安全、网关、token、插件、客户端契约、二维码、隧道) tools/ 无依赖调试工具(CDP 探针、截图脱敏、banner / 架构图合成) docs/ banner、图标、README 截图
开发与测试
node --test tests/.test.mjs
当前 81 个用例全部通过(约 1.5 秒):安全 8、网关 14、token-only 6、token 10、host 插件 7、client 契约 11、二维码 11、功能开关 4、隧道 5、入口地址过滤 1。
界面另有几个零依赖探针(需先手动起 headless Edge 并打开调试端口):
node tools/probe-remote.mjs <debug-port> <入口链接> out.png --eval-file tools/eval-ui-report.mjs
node tools/make-shots.mjs <debug-port> <入口链接> docs/shots # 生成脱敏截图
python tools/make-banner.py # 重新合成 banner 与图标
python tools/make-arch.py # 重新绘制架构图
改完代码要让它生效:node scripts/install.mjs 触发宿主热重载;只改 cordis.patch.yml 里的注释不算变化,得动一个值(例如 sessionDays 30 ↔ 31)。
安全须知
远程入口意味着别人拿到链接就等于坐在你的 DSH 前面,请至少做到:
1. 别用弱口令:口令门默认关闭,走 token;要开口令就给个像样的。
2. 公网优先用 HTTPS:Cloudflare 隧道 天然 https;自己做端口映射时建议反代加证书,并透传 X-Forwarded-Proto: https(网关据此给 Cookie 加 Secure)。
3. 令牌会随刷新硬切:刷新后旧链接、旧二维码、已登录浏览器全部失效,这是设计。
4. 文件查看有白名单:默认拒绝 $DSH_HOME 里的凭据与状态目录,可用 allowedRoots / deniedPaths 收紧。
5. 不用就关总开关:3081 会真的停掉,隧道一起停。
常见问题
| 现象 | 处理 |
| 设置了但手机连不上 | 确认防火墙放行 TCP 3081;Get-NetTCPConnection -LocalPort 3081 应有监听;路由器/云主机安全组也要放行 |
| 隧道一直起不来 | 首次要下 ~55 MB 的 cloudflared(看 $DSH_HOME/webim/cloudflared/);企业网络挡 github.com / *.trycloudflare.com 时会失败;可用 tunnelBinary 指定现成二进制 |
| 点了隧道按钮没反应 | v1.5.1 起有转圈 + 计时 + 占位卡片;若 30 秒没域名,看 $DSH_HOME/webim/plugin.log(含 cloudflared 最后几行输出) |
| 切换模式后页面卡一下 | 预期行为:重绑端口会打断当前页面连接,面板每 1.2 秒自动重连,3–5 秒恢复 |
| 二维码扫出来不是我想的那个 | 二维码跟随被点选的卡片;点一下想要的那张,下方会写「扫码目标 · …」 |
| 刷新令牌后自己也进不去 | 设计如此:会话签名把 token 当盐,轮换后旧会话全部失效,重新扫码即可 |
| 端口被占(EADDRINUSE) | 已有旧网关在跑;插件用 Symbol.for('dsh-webim.gateway.v1') 注册表在 bind 前关掉上一代,/__webim/ping 的 instance.tokenId 可看是谁在应答 |
已知限制
- 隧道用的是 quick tunnel(免账号),域名每次启动都变,Cloudflare 也不保证可用性;长期使用请换 named tunnel + 自己的域名。
- 网关到本机 Harness 之间是明文回环 HTTP,本机进程可读;跨机安全靠外层 https。
- 只桥接 HTTP/WS,不做协议改写,所以 DSH 前端的所有能力都取决于上游本身。
- 移动端适配是"补丁式"的:DSH 大版本换 DOM 结构时,assets/client.css 里的选择器可能需要跟进。
License
MIT。与 DeepSeek / DeepSeek Harness 官方无关,属于第三方插件。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
shimiei/dsh-webim
扫码用手机或异地浏览器打开本机 DSH 界面
相关插件 · UI / 主题
破甲增强版JeffKeeping/dsh-purge-plus
dsh-purge 的非官方分支fork:适配 DeepSeek Harness 0.2.1-rc.2、破甲内核…
★ 387已汉化DeepSeek 客户端
71.9
无代码业务系统平台nocobase/nocobase
NocoBase 是什么
★ 24,804已汉化DSH
71.8
1Panel 服务器面板1Panel-dev/1Panel
全球 250 万+ 自托管用户共同喜爱。
★ 37,350已汉化DSH
71.5
工作状态行动态ccch1mneyyy/working-activity
作者:chimney@ccch1mneyyy。社区项目,非 DeepSeek 官方项目。
★ 664已汉化DSH
71.2
多模型配置管家farion1231/cc-switch
跨平台桌面一体化助手,支持 Claude Code、Codex、OpenCode、DeepSeek Harness 等工具,唯一官网 ccswitch.io
★ 151,506已汉化DSH
71.1
AI 矢量设计工具ZSeven-W/openpencil
全球首个 AI 原生开源矢量设计工具。
★ 7,027已汉化DSH
71.1
这是你的仓库?认领条目