🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

远程访问 Guuu16/dsh-remote

DeepSeek Harness
Guuu1624 天前有提交已提供中文文档扫描:含敏感能力

用手机随时打开本机工作区与聊天记录

29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/12 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Guuu16/dsh-remote
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么[目录]—
dsh 原生插件 · ui
暂无时间戳·判据 →
装得上吗[实装验证]✓
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
数据截至 2026/11/04·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]✓
活跃:最近一次提交在 24 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 54 处,展示前 54 条)
  • 读取本地文件
    index.js:121
    body = readFileSync(new URL(`./public/${file}`, import.meta.url));
  • 读取本地文件
    index.js:295
    if (existsSync(cwd)) missing.push(entry);
  • 发起网络请求
    client.js:163
    const res = await fetch("/remote/api/config", { credentials: "same-origin" });
  • 发起网络请求
    client.js:193
    const res = await fetch("/remote/api/config", {
  • 发起网络请求
    client.js:214
    const res = await fetch("/remote/api/rotate", { method: "POST", credentials: "same-origin" });
  • 发起网络请求
    client.js:238
    const res = await fetch("/remote/api/sync", { method: "POST", credentials: "same-origin" });
  • 发起网络请求
    client.js:265
    const res = await fetch("/remote/api/config", {
  • 读取本地文件
    bin/dsh-remote.mjs:226
    const body = await readFile(join(PUBLIC_DIR, name));
  • 读取本地文件
    src/inject.mjs:18
    return readFileSync(new URL(`../public/${name}`, import.meta.url), "utf8");
  • 读取本地文件
    src/tunnel.mjs:29
    accessSync(candidate, constants.X_OK);
  • 执行外部命令来自配置/变量
    src/tunnel.mjs:76
    child = spawn(sshPath(), args, { stdio: ["ignore", "ignore", "pipe"] });
  • 读取本地文件
    src/cloudflare.mjs:40
    if (existsSync(cached)) return cached;
  • 读取本地文件
    src/cloudflare.mjs:43
    if (existsSync(candidate)) return candidate;
  • 写入本地文件
    src/cloudflare.mjs:62
    mkdirSync(dir, { recursive: true, mode: 0o700 });
  • 发起网络请求
    src/cloudflare.mjs:67
    const response = await fetch(url, { redirect: "follow" });
  • 写入本地文件
    src/cloudflare.mjs:73
    writeFileSync(staging, bytes);
  • 执行外部命令固定命令
    src/cloudflare.mjs:75
    execFileSync("tar", ["xzf", staging, "-C", dir], { stdio: "ignore" });
  • 删除本地文件
    src/cloudflare.mjs:76
    unlinkSync(staging);
  • 写入本地文件
    src/cloudflare.mjs:78
    writeFileSync(target, bytes);
  • 写入本地文件
    src/cloudflare.mjs:80
    chmodSync(target, 0o755);
  • 执行外部命令来自配置/变量
    src/cloudflare.mjs:111
    child = spawn(binary, args, { stdio: ["ignore", "ignore", "pipe"] });
  • 读取本地文件
    src/cloudflare.mjs:153
    return { path: binary, bytes: statSync(binary).size };
  • 读取本地文件
    src/cloudflare.mjs:162
    return readdirSync(cloudflaredHome());
  • 读取本地文件
    src/dsh.mjs:58
    raw = readFileSync(credentialsPath(), "utf8");
  • 发起网络请求
    src/dsh.mjs:144
    response = await fetch(`http://${authority}/`, {
  • 读取本地文件
    src/config.mjs:24
    if (existsSync(current)) return current;
  • 读取本地文件
    src/config.mjs:26
    if (existsSync(legacy)) return legacy;
  • 读取本地文件
    src/config.mjs:48
    stored = JSON.parse(readFileSync(path, "utf8"));
  • 写入本地文件
    src/config.mjs:72
    mkdirSync(remoteHome(), { recursive: true, mode: 0o700 });
  • 写入本地文件
    src/config.mjs:73
    writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
  • 写入本地文件
    src/config.mjs:186
    writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
  • 写入本地文件
    src/config.mjs:204
    writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
  • 读取本地文件
    src/store.mjs:27
    if (existsSync(current)) return current;
  • 读取本地文件
    src/store.mjs:28
    if (existsSync(legacy)) {
  • 写入本地文件
    src/store.mjs:30
    renameSync(legacy, current);
  • 读取本地文件
    src/store.mjs:72
    stored = JSON.parse(readFileSync(storePath(), "utf8"));
  • 写入本地文件
    src/store.mjs:88
    mkdirSync(storeHome(), { recursive: true, mode: 0o700 });
  • 写入本地文件
    src/store.mjs:89
    writeFileSync(storePath(), JSON.stringify(merged, null, 2) + "\n", { mode: 0o600 });
  • 写入本地文件
    src/store.mjs:110
    mkdirSync(storeHome(), { recursive: true, mode: 0o700 });
  • 写入本地文件
    src/store.mjs:111
    writeFileSync(storePath(), JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
  • 读取本地文件
    src/ssh-setup.mjs:20
    return ["id_ed25519.pub", "id_rsa.pub"].map((name) => join(ssh, name)).filter((path) => existsSync(path));
  • 执行外部命令来自配置/变量
    src/ssh-setup.mjs:25
    const child = spawn(command, args, { ...options, stdio: ["ignore", "pipe", "pipe"] });
  • 读取本地文件
    src/session-store.mjs:27
    names = readdirSync(sessionDir);
  • 读取本地文件
    src/session-store.mjs:37
    const handle = readFileSync(path);
  • 读取本地文件
    src/session-store.mjs:66
    stat = statSync(logPath);
  • 读取本地文件
    src/session-store.mjs:133
    projectDirs = readdirSync(root, { withFileTypes: true })
  • 读取本地文件
    src/session-store.mjs:145
    sessionDirs = readdirSync(projectDir, { withFileTypes: true })
  • 读取本地文件
    src/session-store.mjs:213
    const bytes = readFileSync(logPath);
  • 读取本地文件
    src/workspace-link.mjs:26
    const raw = readFileSync(path, "utf8");
  • 读取本地文件
    src/workspace-link.mjs:74
    const before = statSync(path);
  • 读取本地文件
    src/workspace-link.mjs:99
    const after = statSync(path);
  • 写入本地文件
    src/workspace-link.mjs:105
    copyFileSync(path, backup);
  • 读取本地文件
    src/workspace-link.mjs:106
    if (existsSync(backup) === false) throw new Error(`could not write backup ${backup}`);
  • 写入本地文件
    src/workspace-link.mjs:107
    writeFileSync(path, JSON.stringify(store, null, 2) + "\n");
扫描于 2026/11/4(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 手机访问本机工作区
  • 查看聊天记录
  • 配置中继服务器

把本机的 DeepSeek Harness 接到任何地方:在 dsh 设置里配置中继服务器,手机就能访问同一份工作区和聊天记录。dsh 插件,用来远程访问,连的是同一个运行时,不是它的另一个视图。

README

由 DeepSeek 最新模型翻译生成
dsh-remote 把本机的 DeepSeek Harness 接到任何地方。 装一个插件,在 dsh 的设置 → 远程访问里填一台服务器(或者什么都不填,用 Cloudflare),手机就能在任何网络下打开 dsh 本身——同一份工作区、同一份聊天记录,不是第二份副本。 手机浏览器(390×844)实测截图。左边是展开的工作区与会话列表,右边是聊天。界面就是 dsh 自己的 Web GUI,插件没有另造一层。 English: a dsh plugin that makes this machine reachable from anywhere. Configure a relay (SSH reverse tunnel, or a zero-config Cloudflare tunnel) from dsh's own Settings page; your phone then opens the real dsh web GUI — the same runtime, the same workspaces and transcripts, not a second view of them. dsh plugin --profile desktop add github:Guuu16/dsh-remote 安装 方式一:从 GitHub 装(推荐)
展开完整文档(README 剩余 1 节)
dsh-remote

把本机的 DeepSeek Harness 接到任何地方。

装一个插件,在 dsh 的设置 → 远程访问里填一台服务器(或者什么都不填,用 Cloudflare),手机就能在任何网络下打开 dsh 本身——同一份工作区、同一份聊天记录,不是第二份副本。

手机浏览器(390×844)实测截图。左边是展开的工作区与会话列表,右边是聊天。界面就是 dsh 自己的 Web GUI,插件没有另造一层。

English: a dsh plugin that makes this machine reachable from anywhere. Configure a relay (SSH reverse tunnel, or a zero-config Cloudflare tunnel) from dsh's own Settings page; your phone then opens the real dsh web GUI — the same runtime, the same workspaces and transcripts, not a second view of them. dsh plugin --profile desktop add github:Guuu16/dsh-remote

安装

方式一:从 GitHub 装(推荐)

dsh plugin --profile desktop add github:Guuu16/dsh-remote

装完重启一次 dsh。

方式二:本地目录装

git clone https://github.com/Guuu16/dsh-remote.git ~/dsh-remote
dsh plugin --profile desktop add ~/dsh-remote

也可以在 dsh 的插件页面里,用「安装 bundle」指向这个目录。

要求:Node ≥ 22.15(用到内置的 zstd 解压)。云隧道模式首次会自动下载 cloudflared 到 ~/.dsh-remote/bin/,不需要 brew,也不需要账号。

配置

重启后打开 设置 → 远程访问(跟语言、外观并列的那一页)。里面是六个字段:

| 字段 | 说明 |
| 方式 | 关闭 / SSH 反向隧道 / Cloudflare 隧道 |
| 服务器地址 | SSH 方式必填,域名或 IP |
| 用户名 | SSH 方式必填 |
| 密码 | 只在首次用于装公钥;装完自动从磁盘清掉,之后走密钥 |
| SSH 端口 | 默认 22 |
| 服务器上暴露的端口 | 默认跟网关同端口 |

保存后页面会显示网关状态、隧道状态、访问密钥、局域网地址和公网地址——隧道连上以后公网地址会自己出现,不用刷新。

三种方式怎么选

Cloudflare 隧道——零配置,不用服务器、不用开端口、不要账号。cloudflared 从你机器主动出站连到 Cloudflare 边缘,换回一个 *.trycloudflare.com 的临时 HTTPS 域名。代价:流量过 Cloudflare 边缘,域名每次重连都换;边缘通常就近接入,但如果你本机挂了全局代理出国,它会绕远,首次加载 GUI 可能要等一分钟(那 3.9 MB 的插件包要过一遍中继,之后就吃浏览器缓存了)。

SSH 反向隧道——用你自己的服务器,延迟低。需要两件事:服务器 sshd_config 里 GatewayPorts clientspecified(否则转发的端口只绑 relay 的 127.0.0.1,外面连不上),以及云平台安全组放行那个端口。

关闭——只在局域网可用。手机和电脑同一个 WiFi 时仍然能访问。

SSH 那步插件会替你做什么

填了密码点保存之后:ssh-copy-id 把本机公钥装到服务器 → 验证密钥能登录 → 把密码从磁盘清掉 → 建立 ssh -R 隧道并保持(断线每 5 秒重拨)。密码只在第一次用一次。

手机上怎么用

扫码,不用输任何东西。设置页和配置页在每个地址旁边都画了一个二维码,手机相机对着扫就直接打开——包括局域网那条和公网那条。

打开之后,Safari / Chrome 里「添加到主屏幕」,之后就是一个全屏 app:没有地址栏,图标是你自己的。这一步靠 manifest 和 apple-touch-icon 完成,图标是插件自带的 PNG(180/192/512 三档,iOS 不认 SVG)。

手上有密钥也可以手动拼地址:

https://xxx.trycloudflare.com/?k=<access-key>

第一次带 ?k= 会换成一个 HttpOnly cookie(30 天),之后地址栏是干净的。

局域网那条最快,但地址是路由器分配的,换网络或隔久了会变。变了一扫码就是新的,不用管旧链接。

访问密钥是唯一的门禁:32 字节随机值,没有它任何请求都是 401,连 /api 的 WebSocket 升级也会被拒。本机 loopback 免密钥——能在本机发请求的进程本来就有文件系统权限。换密钥就在设置页上,一按所有已配对设备立刻失效。

手机上看到的就是 dsh 自己的界面:左上角"打开侧边栏"进出工作区和会话,右边是聊天。我只在它上面加了两件它自己不会做的事——图标栏按钮的触摸高度和刘海/底部安全区的 padding。横向一指不碰:曾经试着把侧栏改成浮层,实测把对话面板的定位搞坏了(面板变成 x=320、宽 390),所以布局留给 dsh 自己。这些样式只在小屏生效,同一个网关下桌面浏览器看到的是原样。

项目:侧边栏漏掉的那些

这是这个插件存在的最实际的理由。

dsh 的侧边栏列的是工作区,不是"有历史记录的目录"。这两个概念是分开的:注册表里没登记过的目录,历史再多也不显示。实测这台机器上 ~/.dsh/sessions 里有 5 个目录,而侧边栏只有 3 个——dsh(4 个会话)和 moneymoney(2 个会话)完全看不见,连进去的办法都没有。

设置页里那张项目卡把两边的差集算出来给你看,一个按钮补齐。它用的是 dsh 自己的 API,不是猜的:
- sessionQuery.listSessions() —— 每个会话头都带 cwd,这是「磁盘上到底有哪些项目」的权威答案
- workspaceRegistry.list() —— 已登记的工作区
- 两者的差集就是要补的

补齐调 workspaceRegistry.create(),它会走 realpath 且幂等,所以已登记的路径是无操作、目录已删的自己会报错。会话不需要一个个挂上去:Workspace.sessionIds 是按 canonical cwd 自动过滤的,所以注册目录之后历史会话自己就归属过去了。

卡上还会标出目录已经不存在的工作区(status() 返回 missing-dir)。

默认不自动执行——它写的是你的持久工作区注册表。想要启动时自动补,卡片上有个开关,打开后启动 5 秒跑一次。手上有密钥的人也不会因为它而被改数据:那个按钮在设置页里,不在公网可达的那一页上。

它是怎么做到的

同一个运行时,不是镜像。 插件在 dsh 进程里再起一个绑 0.0.0.0 的 HTTP 服务,把请求代理到 dsh 自己那个只绑 loopback 的端口。所以手机上看到的每个工作区、每条消息都是同一个 ~/.dsh、同一个进程——不是同步、不是缓存、不是第二份副本。

绕过两道门。 dsh 拒绝 --host 0.0.0.0(源码里写明「会把远程代码执行暴露到网络」),所以插件不动它,自己开一个 listener。它的 /api 信任围栏只接受 loopback 或已声明的 authority,并要求 Origin 与 Host 一致,所以代理转发时把两者一起改写成 loopback authority,并注入一张自己签的 cookie——签名密钥持久存在 ~/.dsh/.credentials.yaml,插件读它就能签出合法 cookie,不需要去抓进程启动时打印的 launch token。

WebSocket 要单独照顾。 dsh 的 RPC 全走 /api/remote.mux 的 WebSocket。Connection: Upgrade 和 Upgrade 是 hop-by-hop 头,正常转发时会被丢掉——丢了以后升级请求退化成普通 GET,上游永远不触发 upgrade 事件,客户端只看到 socket 挂死(dsh 自己的 GUI 会每 5 秒重连一次 [connection] connection lost)。所以升级路径要显式重建这两个头,socket 到 socket 直通。

HTML 不能当纯文本改。 dsh 的 index 是 gzip 出去的。不先解压就注入、又留着 content-encoding,浏览器直接 ERR_CONTENT_DECODING_FAILED 白屏。注入前解码、注入后删掉该头。另外中间如果隔着 Cloudflare,它会给 HTML 注入 email-decode.min.js 之类的脚本,打乱 ES module 的加载顺序——加 Cache-Control: no-transform 挡掉。

万一不用插件

这一段是遗留路径,功能已被插件模式完全覆盖。它需要在 dsh 之外再跑一个进程、自己管一个 listener,而且它读会话的方式是直接解析 ~/.dsh/sessions 里的 zstd 日志文件——插件模式下这件事由 sessionQuery 一句 API 完成,不需要自己啃文件格式。保留它只是为了在 dsh 装不上的时候还能用。

同一个目录也能当独立网关跑,不依赖 dsh 的插件体系:

./start.sh # 前台,打印配对链接 ./start.sh --daemon # 后台,日志在 ~/.dsh-remote/remote.log ./start.sh --status # 看状态 + 重新打印链接 ./start.sh --stop

它额外带一个 /remote 索引页:直接读 ~/.dsh/sessions 列出所有项目和会话,包括 GUI 从来没打开过、因而不在侧边栏里的那些。会话日志是每次落盘追加一个独立 zstd 帧的串联,Node 的流式解压器遇到第二帧就报 Unknown frame descriptor,src/zstd-frames.mjs 按帧结构走一遍定位边界再逐帧解压。(插件模式不碰这些:sessionQuery.listSessions() 直接给出答案。)

开发

改完 host 那一半必须重启 dsh。这不是建议。Node 缓存 ES 模块,所以 index.js 的任何改动——新路由、新的注入标签、改了响应头——在进程重启前完全不可见,而所有症状都长得像「代码没生效」或者更糟,像「插件坏了」。我在这个坑里待了五轮:新路由一直 404,我以为注册写错了;curl 一直返回旧内容,我以为缓存没清。实际上是没重启。

两个命令分清楚:

npm test # 85 个用例。断言代码对不对,全部走真实代码路径,不需要运行中的 dsh
npm run verify # 12 个探针。断言运行中的进程加载了哪个版本

这两个问题不一样,而且会分叉:npm test 全绿的同时 npm run verify 可以报 0/12——那说明代码是对的、但进程还在跑旧版本。verify 会把进程启动时间跟最后一次提交对一下,直接告诉你该不该重启。

测试里没有一条断言「文件里有某个字符串」。每条都调插件自己注册的路由、挂它自己的组件、跑它自己注入的脚本,所以行为变了才会红。

写测试时踩过的两个坑,留个记录:

- 别用 fetch 探这个网关。它在这台机器上报一个不带原因的 fetch failed,而同一个 URL curl 返回 200。换成 node:http 之后第一轮扫描就报出了 Content-Length can't be present with Transfer-Encoding——一个存在了很久、curl 一直容忍的协议违规。
- 测试里要 agent: false。Node 的 globalAgent 默认保持连接,跨用例复用 socket 会让后面的探测读到脏状态并返回 400。那不是产品的 bug。

文件

index.js Host 插件:listener、反代、隧道、设置路由 client.js 浏览器半边:设置 → 远程访问 那一页
预览分享卡片长什么样 ↓Guuu16/dsh-remote
分享 · DeepSeek Harness Hub
Guuu16/dsh-remote

用手机随时打开本机工作区与聊天记录

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部