DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

jxboop /

jxboop/dsh-plugin-mobile-bridge

Verified

给 DeepSeek Harness 用的手机端桥接插件:手机发图、下发任务、实时看输出,支持局域网 / USB 共享 / Cloudflare 隧道外网访问。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@9378328c

dsh-plugin-mobile-bridge

给 DeepSeek Harness 用的手机端桥接插件: 在手机上导入图片、下发任务、实时看输出,从同一个 WiFi、USB 共享、或者外网隧道都行。

在电脑上跑 DSH,在手机上用。不用装 App,打开浏览器就是控制台。


它能做什么

  • 手机直接发图(截图、拍的照片)给 DSH,不用再经过微信/QQ 中转
  • 新建任务、下发指令、取消运行,实时看思考过程和工具调用
  • 它跑着的时候能插话:运行中右边的按钮变成「插话」,说的话排进队列、这一轮结束就接着处理,不用干等它跑完才敢打字(桌面端的 steer = 打断当前回合,手机上默认不用它:宁可等一轮,也不要把你正在跑的事打断)
  • 第一次连上会讲一遍怎么用,点「开始使用」收起,之后不再打扰
  • 按会话回看历史,支持长文本长按复制
  • 余额查询(配合余额插件的同一份凭据)
  • 地址自动排序:USB 共享 / 蓝牙 PAN / 个人热点 / 局域网,哪个能通就把哪个排前面
  • 外网可达:附带 Cloudflare 隧道脚本,任何网络都能连(不需要公网 IP、不用改路由器)

安装

前置条件:你得先有一个能正常运行的 DSH —— 这是个扩展插件,不是独立程序。需要 Node 22 以上(用的都是内置模块,没有构建步骤)。

1. 装插件

# 从 GitHub 安装(`link:` 只接受本地路径,网址要用 git+https 或 github: 简写)
dsh plugin --profile web add git+https://github.com/jxboop/dsh-plugin-mobile-bridge.git

# 等价写法
dsh plugin --profile web add github:jxboop/dsh-plugin-mobile-bridge

# 从本地目录(改代码时用)
dsh plugin --profile web add link:/path/to/dsh-plugin-mobile-bridge

装完重启 DSH,日志里会出现:

[mobile-bridge] listening on 0.0.0.0:3081, PIN 123456
[mobile-bridge] phone url: http://192.168.1.20:3081/ab12cd34ef56ab78/  (WLAN)

更新到最新版

把上面那条安装命令原样再跑一遍就行,不需要先卸载:

dsh plugin --profile web add github:jxboop/dsh-plugin-mobile-bridge

然后重启 DSH。(dsh plugin 是 pnpm 的转发器;对分支形式的 git 依赖,add 会重新解析到最新提交。实测 0.2.2 → 0.3.0 可直接升级。)想确认装到了哪一版:看 <profile>/node_modules/dsh-plugin-mobile-bridge/package.json 的 version。

2. 首次启动会自动生成配置

~/.dsh/mobile-bridge.json(Windows:%USERPROFILE%\.dsh\mobile-bridge.json):

{
  "version": 1,
  "port": 3081,
  "pin": "123456",
  "pathSecret": "ab12cd34ef56ab78",
  "tokenTtlHours": 12,
  "bindTokenToIp": false,
  "elevationMinutes": 15
}

pin 和 pathSecret 都是自动生成的,别外传。 在 DSH 界面左下角的徽标上能直接看到完整网址和 PIN。

字段 默认 说明
port 3081 监听端口
pin 随机 6 位 登录密码
pathSecret 随机 16 位十六进制 所有路由都藏在这个随机段后面,没有它一律 404
tokenTtlHours 12 登录令牌有效期(上限 14 天)
bindTokenToIp false 是否严格绑定令牌的来源 IP。默认 false:地址变化只记录告警(在 /api/bootstrap 的 foreignUses 里可见),不吊销登录。设 true 则换地址即吊销 —— 移动网络下不可用(运营商 NAT、IPv6 隐私扩展会不停换地址,手机会被反复踢下线),只适合固定网络
elevationMinutes 15 输一次 PIN 后,多久内可以"指挥 DSH 干活"
crashProbe false 排查用,平时别开。 设为 true 会把宿主的 uncaughtException / exit 事件写进 ~/.dsh/dsh-crash.log(含每 30 秒一条内存心跳)。它跑在宿主进程里、会注册全局处理器并持续写盘,所以默认关闭。
answerOnPhone false 让手机回答 agent 的提问(ask_user_question)和工具审批。默认关:开着时"正在看这个会话的手机"会抢在桌面前面拿到提问,桌面要等 120 秒才轮到。没有手机在看这个会话时,无论开关如何都是桌面先拿到。

为什么过一会儿又让我输 PIN?

这是设计如此,不是掉线。手机上有两种状态:

  • 已登录(有 cookie):能看会话列表和聊天记录。令牌默认 12 小时有效,所以重启电脑、重启 DSH 都不会把你踢出去。
  • 已提权(最近输过一次 PIN):才能让这台电脑干活 —— 下发任务、取消、新建会话。

提权默认只保持 15 分钟(elevationMinutes),过期后第一次动手会重新弹出 PIN 门;宿主重启后恢复的会话同样不带提权。觉得输得太勤就把它调大,上限 240 分钟:

{ "elevationMinutes": 240 }

如果你看到"需要重新输入 PIN"却找不到输入框,那是 0.2.0 及更早版本的 bug(页面只认 401、不认服务器用来表达提权的 403 + needPin,于是提示了却没有门)。0.2.1 起已修复:过期时页面会直接把 PIN 门打开。

3. 放行防火墙(Windows)

首次启动会弹 Windows 防火墙提示。**只勾"专用网络"**即可(手机和电脑在同一 WiFi 时够用)。若要走外网隧道,勾"公用网络"。


外网访问(可选)

局域网之外也能连。两条路,先用方案 A。

方案 A:Tailscale Funnel(推荐 —— 地址永不变、开机自恢复、手机不用装东西)

为什么推荐:Cloudflare 免费隧道的域名每次重启都变,旧的永久失效;Funnel 给的是一条 https://<机器名>.<tailnet>.ts.net/ 的固定公网地址 —— Tailscale 服务随开机自启、配置存在 daemon 状态里,重启电脑后什么都不用做,网址照旧。而且它是标准 HTTPS,手机上不用装任何客户端(这点对 iPhone 尤其重要:Tailscale 的 iOS app 没有上架中国区 App Store)。

# 1. 装 Tailscale 并登录:https://tailscale.com/download

# 2. 首次开 Funnel 需要去控制台同意一次(只此一次),命令会把链接打出来:
tailscale funnel 3081
#    Funnel is not enabled on your tailnet.
#    To enable, visit: https://login.tailscale.com/f/funnel?node=xxxxx

# 3. 同意之后再执行一次,就常驻后台了:
tailscale funnel --bg 3081
#    Available on the internet:
#    https://<机器名>.<tailnet>.ts.net/
#    |-- proxy http://127.0.0.1:3081

# 4. 手机访问(结尾必须带密钥段):
#    https://<机器名>.<tailnet>.ts.net/<pathSecret>/

不用时关掉:tailscale funnel --https=443 off

⚠️ 选了方案 A 就别再跑 tools/tunnel-start.ps1 —— 它会把配置里的 publicUrl 改回那个每次都变的 cloudflared 域名。两者选一个。

安全上如实说:Funnel 和免费隧道一样是公网地址,保护来自随机密钥段 + PIN 这两层;区别只是域名固定、不会更容易被猜到。要"只有自己的设备能进",就在手机上也装 Tailscale(设备之间端到端加密、不经公网),代价是 iPhone 需要非中国区 Apple ID。

方案 B:Cloudflare 免费快速隧道(临时用)

tools/ 下有一套脚本:不需要公网 IP、不需要改路由器、不需要注册账号。代价是域名每次重启都变,旧的永久失效 —— 只适合临时开一下。

# 1. 下载 cloudflared.exe 放到 tools/ 目录
#    https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe

# 2. 启动隧道(会打印完整网址并复制到剪贴板)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-start.ps1

# 3. 不用时关掉
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-stop.ps1

其它工具:

脚本 用途
tools/tunnel-status.ps1 体检:隧道现在是死是活(进程、端口、外网实测),手机上出现错误页时先跑它
tools/show-phone-url.ps1 一个可复制的小窗口,显示外网/局域网网址 + PIN(带刷新按钮)
tools/revoke-phone.ps1 应急:一键吊销所有手机登录(两步确认,防误触)

在手机上回答 agent 的提问

agent 卡在 ask_user_question 上等你选,或者某个工具调用需要你点头时,手机上会直接出现一张卡片:选项按钮、可多选、也能自己写一段话;审批则是「允许一次 / 拒绝」两个键。答完 agent 立刻继续,不用你跑回电脑前面。

默认关闭,打开方式:

{ "answerOnPhone": true }

打开后改配置需重启 DSH(或重载插件)生效。

它怎么决定谁来答:

情况 谁作答
有手机正在看着这个会话 手机(桌面等 120 秒才轮到,防手机没答导致卡死)
没有手机在看这个会话 桌面,和我们没插手时一模一样
手机答了,别的手机也开着 先答的算数,其余卡片自动撤掉

几个已经处理好的细节:

  • 答案要提权:和下发任务一样,光有 cookie 不够,得最近输过 PIN —— 否则偷走 cookie 的人能替你在批准框上点"允许"。
  • 同一个提问不能答两次:用过的 id 立刻失效。
  • 手机刷新不会丢卡片:新开的流会把还没答的提问补推一次。
  • 问题文本来自模型,一律当纯文本渲染(textContent,不是 innerHTML),模型塞不进脚本。
  • 贴到别的页签也看得见:卡片挂在主区域外面,切到「余额」不会漏掉提问。
  • 提问作废了就撤卡:超时/已在别处回答后,卡片自己消失(而不是点一次报一次错)。

已知限制:点「➕ 新建」打开的全屏表单会盖住卡片(它的层级更高)。这时候 agent 仍在等,120 秒后会回落到桌面。正在填表时被提问打断本来就是少见的组合,先不做层级争抢。

断了会告诉你,不用你自己发现

手机和电脑之间的连接有三种坏法,每一种都会在页面顶部弹出一条说明,而不是像以前那样只把一个 9 像素的圆点变红:

情况 页面会说什么
连接被切断(隧道没了 / 网络断了) 「和电脑断开了」+ 可能的原因
连接还在、但对面不再说话(电脑睡眠最典型) 「好像和电脑失去联系了」+ 最后收到消息是多久之前
提问过期、或已在别处回答 卡片自己撤掉,并说明原因

而且它会给出两条能点的出路:

  1. 改用局域网地址 —— 同一个 WiFi 下,这条链路完全不依赖隧道。地址由宿主在 bootstrap 里给(含密钥段的完整 URL)。注意换地址等于换站点,cookie 带不过去,要重输一次 PIN。
  2. 重试连接 —— 不换地址,直接重连。

桌面那一侧也不再装死:插件每 120 秒从外网探一次自己的隧道,连续两次探不通就把徽标上的外网地址标红成「⚠️ 隧道已断,手机连不上」,并往 ~/.dsh/mobile-bridge.log 写一行。恢复时也会写一行。

手机上是 Cloudflare 错误页?(Error 1033 / 1016)

这两个错都不是插件的问题,而是隧道没了。

错误 含义
1033 Cloudflare Tunnel error 这个主机名曾经是一条隧道,但现在没有任何隧道连着它
1016 Origin DNS error 这个主机名已经完全解析不到了(隧道早就没了)

最常见的两个原因:

  1. 免费快速隧道的网址,每次 cloudflared 重启都会换一个。 旧网址永久失效,救不回来 —— 不是"过一会儿就好",是那个域名再也不属于你了。手机上收藏的旧网址永远打不开。
  2. 跑隧道的电脑睡眠 / 休眠 / 合盖了,或者换了网络、被安全软件或加速器掐掉了连接 —— 隧道随之中断。

怎么办:

# 先体检:一眼看出是进程没了、端口没起、还是网址失效
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-status.ps1

# 网址失效就重拿一个(先停再起,确保拿到全新的)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-stop.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-start.ps1

tunnel-start.ps1 会真的从外网打开一次再告诉你成功了 —— 如果它说"没能通过外网验证",那就是现在把网址给手机也没用,直接照它列的原因排查,别浪费时间。

想要一个永不变的网址,只有两条路:Cloudflare 命名隧道(要有自己的域名 + 账号),或者用 Tailscale / ZeroTier 这类组网工具(推荐:地址固定、端到端加密、且完全不暴露到公网)。

⚠️ 免费快速隧道的网址每次重启都会变,而且是一条公开网址 —— 拿到的人都能看到 PIN 登录页。手机桥有 PIN 认证和限速兜着,但别把网址发到群里。


安全模型

这个桥可以经隧道暴露到公网,所以"安全"必须是能验证的,不是一句承诺。

已实施的防护

防护 说明
随机路径前缀 所有路由藏在 /<pathSecret>/ 后面。无密钥路径一律 404
限速的钥匙不可伪造 只在 socket 对端是 loopback(即自家 cloudflared)时才读转发头;cf-connecting-ip 优先,x-forwarded-for 取最后一段(自家代理追加的),绝不取第一段(那是调用方可控的)
两级登录限速 每地址 12 次/10 分钟 + 所有人合计 60 次/10 分钟
令牌绑定来源 IP 偷走的 cookie 换一台机器就是废纸
写操作需要再验 PIN cookie 只能读;下发指令/取消/新建会话需要 15 分钟内输过一次 PIN
Cookie Secure 条件化 HTTPS 下带 Secure,明文 HTTP 下不带(否则局域网登录会静默失效)
令牌 12 小时过期 上限 14 天,/api/logout 立即吊销
SSE 连接上限 MAX_LIVE_STREAMS = 6,超限 503
网络层超时与连接上限 headersTimeout 20s(挡 slowloris)、requestTimeout 180s、maxConnections 64
统一安全响应头 nosniff、X-Frame-Options: DENY、cache-control: no-store、Referrer-Policy: no-referrer(防止密钥路径经 Referer 泄漏)
不回显内部错误 细节只进日志,不回给调用方
探测留痕但密钥不落盘 无密钥请求记为 GET <no-secret> <- ip;日志里永远不出现密钥路径

怎么验证(请自己跑)

node test/security.mjs

51 项断言:限速钥匙的单元断言、密钥路径门禁、每个接口的未授权拒绝、限速生效、cookie 标志、令牌有效期、注销吊销、响应头、SSE 上限、附件不泄漏、日志脱敏。退出码非 0 即有失败。

其中只读的那半(密钥门禁、未授权拒绝、响应头、日志脱敏)打的是你正在用的那台桥;破坏性的那一半(连猜 15 次 PIN、真登录拿 cookie、打满 SSE 上限)会在临时目录里另起一台一次性实例,跑完即拆 —— 因为登录限速有一份全局额度,是和手机共用的,在真身上打就是拿手机当靶子。早期版本正是这么把用户锁在门外的。

要连真实 Cloudflare 边缘一起验(会消耗本机出口 IP 的登录额度,10 分钟后自愈):

$env:BRIDGE_TUNNEL_URL='https://<你的隧道域名>'
node test/security.mjs

其它测试。先装一次测试依赖(jsdom,只有 UI 测试用它;插件本身零运行时依赖):

npm install
文件 内容 需要
test/crashprobe.mjs 崩溃探针开关:默认不碰宿主全局状态、开启后能在配置重写中存活(17 项) —
test/harness.mjs 桥的端到端行为:PIN 门、cookie、prompt 组装、SSE 扇出、取消、附件、新建会话、余额缓存、重启保活(含"恢复的会话可读但不可写 = 403+needPin")、注销、桌面角标注入与 start() 不抛错、普通请求取对话 /api/transcript(巨型历史裁剪 / 一堆短字符串拼成的 stream 也要丢 / 缺 sessionId 400 / 客户端半路掐断后桥还活着)、子会话的错误帧带 unsupported 且从列表里消失,以及手机作答全链路(提问推到流上 / POST api/answer 收答案 / 防重放 / 审批 outcome 校验 / 取消不惊动桌面 / 刷新补推)(81 项) —
test/ui.mjs 手机页面的 DOM 与交互:含"没有请求跑出密钥段"、"提权过期时必须把 PIN 门打开"、运行中同时给出「插话」与「停止」并确认插话已收下、换会话后「在跑」状态跟着重置、首次说明页(自动弹出 / 讲清四件事 / 点了就记住 / 真有提问时让位)、子会话不许谎报断线、也不再重连、迟到的旧会话回包不许贴到新会话上、localStorage 写不进去(无痕模式)也不许把页面搞死、Ctrl+Enter 连按两下只发一次、提问与审批卡片(渲染、作答编码、防 XSS、重推保草稿、单选与自定义互斥、作废撤卡、跨页签可见),以及断线提示条(弹条、说清原因、给局域网兜底、心跳到达后收起)(116 项) jsdom
test/addresses.mjs 地址排序与分类(16 项) —
test/shot.mjs 真浏览器截图 + 布局测量 Windows + Edge
test/phone-diag.mjs 真浏览器诊断:抓页面异常、控制台、网络状态码 Windows + Edge
npm test          # security 51 + crashprobe 17 + harness 81 + addresses 16 = 165 项,无需浏览器
npm run test:all  # 再加上 ui 的 116 项,共 281 项

仍未消除的风险(如实列出)

  • Cloudflare 能看到明文:HTTPS 在其边缘终结,prompt、会话内容、附件对它都是明文
  • 会话记录无独立保护:拿到有效 cookie 即可读取全部历史
  • 公网攻击面是一个手写的 Node HTTP 服务:没有框架兜底
  • 免费快速隧道:无可用性保证,网址每次重启都会变
  • 不用时请关闭:tools/tunnel-stop.ps1。不暴露就没有攻击面,这是最有效的一条

已知坑(作者踩过的,写在代码注释里)

  1. res.write() 写到已断开的响应上不会同步抛错,而是异步 emit 'error' —— 无人监听时 Node 直接抛出,整个宿主进程陪葬。所以每一处响应都要挂 res.on('error')。
  2. 不要用局部变量遮蔽同名函数。曾经有一个 const record = ... 遮蔽了日志函数 record(),只在"令牌异地使用"这条路径上抛 TypeError,把整个 DSH 打死,排查了很久。后来又踩了一次:手机作答的等待记录也叫了 record,于是"提问推给手机"那一行直接把宿主打死。变量名起之前,先看一眼作用域里有没有同名函数。
  3. 注释里写了"要保住的东西",就得在每一条写入路径上兑现。 提问卡片的草稿注释着"存在 state 上就不会丢",但"宿主重推同一个 id"那条路径直接整体覆盖,把用户勾到一半的选择清光 —— 注释不能当实现。
  4. server.close() 不会断开已建立的 keep-alive 连接。 插件重载时旧实例的会话已清空,却还在服务复用连接,手机就会拿到莫名的 401(表现为"刚重载又要我输 PIN")。要显式 closeIdleConnections() / closeAllConnections()。
  5. waterfall 是顺序的,注册顺序决定谁作数。 手机应答者必须 prepend 在官方 api-remotes 之前,否则永远不会被调用(官方那一环会一直等到浏览器作答)。
  6. 超时和取消要分开。 超时该 next() 交还桌面;取消(agent 已放弃)绝不能 next(),否则桌面上会弹出一个已经作废的批准框。
  7. 退出前要同步落盘。异常处理器里用异步 appendFile 再 process.exit(),写入不会完成 —— 探针会把证据弄丢。
  8. pnpm/npx 建的联接,Target 可能是相对路径,重建时要相对原联接所在目录解析。
  9. robocopy 默认跟进目录联接,会把链接指向的整棵树也复制过来。
  10. str.Replace() 替换的是【全部】匹配,不是第一处。 曾用带锚点的替换插入 /api/revoke 路由,而那个锚点在文件里出现了两次:一处正确,另一处落在无密钥的 404 分支里、位于 const route 之前 —— 于是手机浏览器自动请求 /favicon.ico 时抛 ReferenceError: Cannot access 'route' before initialization,整个宿主进程直接退出。表现是"手机一连上就崩"。插入唯一代码块必须用强制唯一匹配的编辑方式,或先断言出现次数。
  11. 配置重写要带上所有字段。 loadConfig() 只写它列出的字段;新增的排查开关漏掉,就会被下一次"配置有变化"的重写悄悄抹掉(crashProbe 踩过)。
  12. 测试会因为路径形态漂移而静默烂掉。 页面改成相对地址(api/...)后,UI 测试的 mock 仍在按 startsWith('/api/...') 匹配,于是每个请求都 404 —— 测试不是失败,是整个不工作了。同一类漂移还让 harness.mjs 一次废掉 37 条断言(它的 base 里没有密钥段,于是 PIN 门、cookie、SSE、取消、余额缓存全部 404),让 shot.mjs 永远拿不到截图。规则:测试里凡是拼 URL,都必须从配置里读密钥段,绝不硬编码绝对路径;mock 要按浏览器语义把相对地址解析到带密钥段的页面地址上,并断言"没有请求跑出密钥段"。
  13. 页面是启动时读一次并缓存的(state.page,配合 page tag 做自重载)。改了 mobile.html 之后必须重载插件才对外生效 —— 只改文件不重载,线上还是旧页面。
  14. 模块级函数里用不了 createBridge 内部的日志器。 为了给角标加两行日志,在模块级的 installBadge 里直接写了 record(...) —— 那抛 ReferenceError,而 installBadge 正好是 start() 的最后一行,一抛就打断整个启动:角标从界面上消失、隧道自检永不启动,而当时 236 条测试全绿(角标那条路径没有任何覆盖)。同一个名字第三次咬人。 现在 logger 作为参数传进去,并且 harness 里补了角标的覆盖。
  15. 给用户看的消息必须写进用户会看的地方。 隧道断了的告警一开始用的是 log()/warn() —— 它们只到 stdout/stderr,而 DSH 不捕获这些。等于把"隧道断了"说给空气听。 只有 record() 会落进 ~/.dsh/mobile-bridge.log。
  16. 成功分支别提前把状态改回默认值,否则"恢复了"那条分支永远不可达(ok 已经被改回去了,ok === false 不可能成立)。
  17. 保活轮询必须限速,而且要避开后台。 长连接半死(电脑睡了)时不响 onerror,所以页面有一个 5 秒一跳的哨兵,发现 25 秒没数据就用普通请求补拉对话。可是条件只写了"静置超过 25 秒"——于是静置的手机每 5 秒拉一次对话,一次最多二百多 KB,一小时七百多次,流量、电、日志全烧掉,画面还不会因此更及时。现在补拉最快一分钟一次,页面切到后台(锁屏)时整个哨兵直接返回,回到前台再补一次。
  18. 大响应可能被通道缓冲住,不能只赌长连接。 手机上"打开对话一片空白,直到电脑那边发来下一条消息才冲出来"就是这个:SSE 的初始快照是 200 多 KB 的一次性写入。所以初始对话改走 GET /api/transcript(普通请求,bootstrap 能通就证明它一定送达),实时增量仍走 SSE —— 能推就推,推不动也不至于什么都看不见。教训:把"能不能送达"和"能不能推送"分开,别让一个赌注同时决定两件事。
  19. 破坏性测试绝不能打真身。 登录限速的额度是全局的、和手机共用;旧版 security.mjs 直接在正在服务的桥上连猜 15 次 PIN,还在真身的令牌文件里留下一串 192.0.2.x 的假令牌 —— 表现就是用户被自己的测试锁在门外。现在它用临时 DSH_HOME + 随机端口另起一台一次性实例来跑这些检查,跑完拆掉,并在结束时清掉临时目录。
  20. 异步请求回来时,要认准"我请求的是谁"。 pullTranscript() 取的是 state.sessionId,但 await 期间用户可能已经换了会话 —— 回来无条件贴上去,就变成 B 的标题下面挂着 A 的对话,而且之后的事件全接在错的对话上,自己永远好不了。现在先记住 wanted,回来第一件事是核对。同一个坑还有更狠的变体:那条"这个会话打不开"的判定如果用 state.sessionId,一个迟到的失败回包会把一个健康的新会话永久标成打不开。
  21. 会话在不在跑,要以宿主给的 running 为准。 state.running 原本只由流里的 turn/start / turn/end 写,换会话时没人重置 —— 切到一条闲着的会话,按钮还写着「插话」、停止键还亮着,发一条普通消息还会回一句假的"已插话"。切换和列表刷新时都从 running 字段重新取值。
  22. 兜底成功了,就要把误报收掉。 长连接把大包缓冲住时顶部会报"好像和电脑失去联系了";而补拉用的普通请求能通,恰恰证明电脑还在。以前补拉成功不改 lastFrameAt/link,于是那条红字在这个接口专门为之存在的场景里一直挂着。现在补拉成功即抹掉 stale。
  23. "大"不只有"超长文本"一种。 assistant/message 的 data.stream 里是这条消息已经逐字推过的全部增量 —— 一堆短字符串。按 4000 字截断的逻辑对它完全无效,实测一条这样的记录能让 /api/transcript 回 782 KB,把"别让手机收到大包"的初衷又踩回去。fitSnapshot 现在先过一遍 trimEvent 再压。
  24. localStorage 在无痕 / 隐私模式下会【抛异常】(iOS Safari:getItem 照常、setItem 抛 QuotaExceededError),而不只是返回 null。而那句 setItem 恰好夹在 state.sessionId = chosen 和 connect() 之间:一抛,流不开、对话不拉,屏幕全白,用户还看着一切正常(仍然登录着)。现在读写只有 readLocal/writeLocal 两个入口,都不许抛;测试里把 Storage.prototype.setItem 改成抛错来钉住这条。
  25. 守卫要放在函数里,不能只放在按钮上。 send() 只靠 sendBtn.disabled 挡重复,而 Ctrl/Cmd+Enter 是直接调 send() 的另一条路 —— 连按两下就是两个 prompt、两轮要花钱的对话。
  26. 弹层要给别人让路。 首次说明页是全屏 position:fixed,而提问/审批卡片是流内元素 —— 真有提问时卡片被挡在后面,而说明页里恰好写着"需要你拍板时会弹出卡片"。现在收到提问就先收起说明页。
  27. 别把"这个会话打不开"说成"和电脑断开了"。 agent 自己派生的子会话不能用 {kind:'session'} 地址跟随,宿主会回 subagent Sessions require their durable parent address。以前页面把它当普通断线:弹"和电脑断开了 / 你正在用的地址已经失效"(把人引去换地址、重试连接,全是没用的动作),而浏览器还每三秒自动重连一次,连一整晚。现在宿主给这条错误打上 unsupported 标记、把该会话从列表里拿掉,页面据此说人话、关掉那条流、并在下次自动选一个能用的会话。

兼容性

  • 需要 DSH(peerDependency:@deepseek-ai/cordis)
  • 纯 ESM,无构建步骤,无运行时依赖(只用 Node 内置模块)
  • 只在 Windows 上实测过;监听、认证、页面都是平台无关的

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 9378328c6544

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout