Smart-DSH — 移动端 UI、通知,以及 Esc 停止,为 DeepSeek Harness 打造
数据截至 2026/10/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
dsh plugin --profile web add hikarioyama/Smart-DSH状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装;安装清单取自 dsh-btw/package.json
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/2 06:33:50
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
查看扫描证据(共 52 处,展示前 44 条)
- 发起网络请求dsh-btw/lib/index.js:274
async fetch(request) { - 读取本地文件dsh-btw/lib/index.js:322
text = await readFile(this.filename(sessionId), "utf8"); - 写入本地文件dsh-btw/lib/index.js:344
await mkdir(dir, { - 读取本地文件dsh-btw/lib/index.js:359
if (createHash("sha256").update(await readFile(path)).digest("hex") !== sha256) throw Error("Damaged audit context; refusing reuse"); - 写入本地文件dsh-btw/lib/index.js:386
await mkdir(this.root, { - 发起网络请求dsh-notify-push/lib/client.js:78
const { publicKey } = await fetch(VAPID_URL).then((response) => { - 发起网络请求dsh-notify-push/lib/client.js:87
const response = await fetch(SUBSCRIBE_URL, { - 发起网络请求dsh-notify-push/lib/client.js:104
await fetch(UNSUBSCRIBE_URL, { - 写入本地文件dsh-notify-push/lib/index.js:14
mkdirSync(dirname(path), { recursive: true }); - 写入本地文件dsh-notify-push/lib/index.js:18
writeFileSync(fd, JSON.stringify(value, null, 2) + "\n", "utf8"); - 写入本地文件dsh-notify-push/lib/index.js:23
renameSync(tmp, path); - 读取本地文件dsh-notify-push/lib/index.js:27
if (!existsSync(path)) return fallback; - 读取本地文件dsh-notify-push/lib/index.js:29
return JSON.parse(readFileSync(path, "utf8")); - 读取本地文件dsh-notify-push/lib/index.js:160
const swBody = readFileSync(join(PACKAGE_ROOT, "sw", "sw.js"), "utf8"); - 发起网络请求dsh-tasks/lib/client.js:284
const response = await fetch(`/api/tasks/view?session=${encodeURIComponent(sessionId)}`, { credentials: "same-origin" }); - 发起网络请求dsh-tasks/lib/client.js:300
const response = await fetch(`/api/tasks/view?session=${encodeURIComponent(sessionId)}`, { credentials: "same-origin" }); - 发起网络请求dsh-tasks/lib/client.js:311
const response = await fetch(`/api/tasks/sub-model?${params}`, { method: "POST", credentials: "same-origin" }); - 读取本地文件dsh-tasks/lib/index.js:641
return readFileSync(join(roleDir(), `${role}.md`), "utf8"); - 写入本地文件dsh-tasks/lib/index.js:649
mkdirSync(dir, { recursive: true, mode: 0o700 }); - 写入本地文件dsh-tasks/lib/index.js:650
chmodSync(dir, 0o700); - 读取本地文件dsh-tasks/lib/index.js:652
if (!existsSync(file)) writeFileSync(file, text, { mode: 0o600 }); - 写入本地文件dsh-tasks/lib/index.js:652
if (!existsSync(file)) writeFileSync(file, text, { mode: 0o600 }); - 写入本地文件dsh-tasks/lib/index.js:653
chmodSync(file, 0o600); - 写入本地文件dsh-tasks/lib/index.js:657
mkdirSync(home, { recursive: true, mode: 0o700 }); - 写入本地文件dsh-tasks/lib/index.js:658
chmodSync(home, 0o700); - 读取本地文件dsh-tasks/lib/index.js:670
if (!existsSync(file)) return []; - 读取本地文件dsh-tasks/lib/index.js:672
for (const line of readFileSync(file, "utf8").split("\n")) { - 读取本地文件dsh-tasks/lib/index.js:682
if (!existsSync(file)) return { enabled: false, subModel: null }; - 读取本地文件dsh-tasks/lib/index.js:684
return readSessionState(JSON.parse(readFileSync(file, "utf8"))); - 读取本地文件opencode-go/apply.mjs:39
if (!existsSync(link)) continue; - 执行外部命令来自配置/变量opencode-go/apply.mjs:55
const gen = spawnSync(process.execPath, [join(HERE, "gen.mjs"), ...(noDeepseek ? ["--no-deepseek"] : []), "--out", genOut], { - 读取本地文件opencode-go/apply.mjs:79
const raw = readFileSync(genOut, "utf8"); - 读取本地文件opencode-go/apply.mjs:102
const settingsDoc = YAML.parseDocument(readFileSync(SETTINGS, "utf8")); - 写入本地文件opencode-go/apply.mjs:126
mkdirSync(backupDir, { recursive: true }); - 写入本地文件opencode-go/apply.mjs:127
copyFileSync(SETTINGS, join(backupDir, "settings.yaml")); - 写入本地文件opencode-go/apply.mjs:128
chmodSync(join(backupDir, "settings.yaml"), 0o600); - 写入本地文件opencode-go/apply.mjs:129
writeFileSync(join(backupDir, "restore.sh"), `#!/bin/sh - 写入本地文件opencode-go/apply.mjs:133
chmodSync(join(backupDir, "restore.sh"), 0o700); - 写入本地文件opencode-go/apply.mjs:149
writeFileSync(tmp, settingsDoc.toString()); - 读取本地文件opencode-go/gen.mjs:43
if (!existsSync(link)) continue; - 写入本地文件opencode-go/gen.mjs:220
writeFileSync(OUT, lines.join("\n") + "\n"); - 读取本地文件opencode-go/set-default-model.cjs:15
const doc = yaml.parseDocument(fs.readFileSync(file, 'utf8')); - 写入本地文件opencode-go/set-default-model.cjs:17
fs.writeFileSync(file, doc.toString()); - 发起网络请求patches/dsh-client-hmr-0.1.2-rc.1/client.js:63
void locks.request("dsh:client-hmr:/plugins/events:v1", { signal: abort.signal }, async () => {
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
功能亮点
- 移动端友好界面
- WebPush通知
非官方 DeepSeek Harness 插件:移动端友好界面、Web Push 通知,外加 Linux Tailscale 配置指南
README
由 DeepSeek 最新模型翻译生成展开完整文档(README 剩余 1 节)
Smart-DSH — 移动端 UI、通知,以及 Esc 停止,为 DeepSeek Harness 打造
非官方 DSH 插件包与 Linux 安装指南,不是 DSH 的分支。
保留上游 DSH 的安装,再加上 Smart-DSH,即可获得:
- 移动端 UI: 全宽聊天与输入框,logo 打开会话列表,去掉 logo 提示浮层与点击高亮。
- 通知: 提问和顶层回合结束的通知,走 Web Push。
- Esc 停止: 按 Escape 取消正在运行的回合,可在 Settings → General 里开关。
- 远程访问指南: 用仅限 tailnet 的 Tailscale Serve HTTPS 连接手机。
- 子代理任务委派: 每个角色只配一个专属动词(worker / explorer / hacker / reviewer),一个同级 hub 带持久日志,外加一个子代理会话标签页。
- OpenCode Go 模型目录: 让 llm-pi-ai 的 OpenCode Go 路由始终跟得上订阅实际提供的每个模型(脚本 + 指南)。
- 多标签页可靠性: 一个可选、带防护的 shared-HMR 变通方案,防止上游开发者更新连接耗尽 Firefox 的 HTTP/1.1 连接槽位。
这是非官方社区扩展,与 DeepSeek 无隶属关系。
兼容性针对 DSH 0.1.2-rc.1 测试;移动端样式使用版本相关的选择器。回合结束通知描述的是智能体回合终止,并不代表独立验证了每个请求的任务都已成功。
一个自包含的 DeepSeek Harness (DSH) 插件包(dsh-notify-push),外加配套远程访问基础设施(Tailscale Serve + 手机)的安装说明。当智能体调用 ask_user_question 时,你的手机会收到一条 Web Push 通知,里面带着问题文本 —— 即使没有浏览器连着也能收到。回合结束时,你会收到一条完成通知,标题反映结束原因(作業完了 / token 上限截断 / 阻塞 / 错误 / 中止);子代理回合不发通知 —— 只有顶层会话才发。点一下通知即可聚焦到应用。
第二个无依赖的插件包(dsh-esc-stop)补上了输入框停止按钮已有的键盘手势:按 Escape 取消当前正在运行的回合。菜单、对话框、队列消息编辑器、输入法组合状态各自保留自己的 Escape,整个手势在 Settings → General 里只有一行 ON/OFF —— 不用改文件,也不用重启就能切换。见 Esc-to-stop。
状态:在 Arch Linux 上可用的配置,2026-09-07 验证,真实投递到 Android Chrome 和桌面 Firefox。这些安装示例省略了主机相关的标识符。
要求
| 要求 | 原因 |
| DSH 0.1.2-rc.1 | 插件包依赖 webServer.register、connection.requestRejection 和 { prepend: true } 监听器选项 —— 如果你的 DSH 版本不同,请先确认这些是否存在(dsh --version) |
| Node >= 22.19(排除 23) | DSH 自身的要求 |
| pnpm | dsh plugin 只是 pnpm 的薄封装转发 |
| 任何基于 Chromium 的浏览器,或带 PushManager 的 Firefox | 已在 Android Chrome(FCM)和桌面 Firefox(Mozilla autopush)上验证 |
| 手机需要安全上下文 | 通过 Tailscale Serve 走 HTTPS(见下文)——纯 http://<ip>:3080 无法订阅推送 |
| Android:无需额外操作。iOS:添加到主屏幕 | iOS WebKit 只向已安装的 PWA(16.4+)投递推送 |
工作原理
ask_user_question (tool)
└─ host: ctx.on("user-questions/request", listener, { prepend: true }) ← outermost
├─ sendPushToAll() web-push → FCM / Mozilla autopush (fire-and-forget)
└─ return next() delegates to api-remotes forwarder → browser composer
(the waterfall is NEVER consumed)
turn completion (top-level sessions only)
└─ host: ctx.inject(["agents"]) → agentCtx.on("session/event", listener)
├─ filter: agents.roots().includes(agents.get(session.id)) ← subagent turns excluded
└─ sendPushToAll(buildTurnEndPayload(session.id, event.data.reason))
titles by reason.kind: completed / max-tokens / blocked / error / aborted
| 组件 | 文件 | 作用 |
| 宿主侧 | dsh-notify-push/lib/index.js | 前置的 user-questions/request waterfall 监听器 + session/event(turn/end)监听器(仅根会话)+ Web Push 扇出 + HTTP 路由(/api/push/ 经 connection.requestRejection 鉴权,/push/sw.js 为静态资源,带 Service-Worker-Allowed: /) |
| 客户端侧 | dsh-notify-push/lib/client.js | window.__ModuleLoader__.load({...}) 包装;页面存活期间使用本地 Notification;/notify popupSelect 命令,用于申请权限和订阅/退订推送 |
| Service worker | dsh-notify-push/sw/sw.js | push → showNotification(requireInteraction: true),notificationclick → 聚焦/打开窗口 |
| 状态 | $DSH_HOME/notify-push/(0600,不在本仓库中) | vapid.json(首次启动时生成,必须在重启后保留)+ subscriptions.json(收到 404/410 时自动清理) |
状态目录跟随 DSH_HOME(默认 ~/.dsh),所以这里没有任何东西硬编码到某个特定用户。
安装
0. Where to keep the bundle source — any persistent path works; ~/.dsh/profiles/web/bundles-src/ is just what this setup uses. Adjust BUNDLE_SRC freely.
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-notify-push"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")"
cp -r /tmp/Smart-DSH/dsh-notify-push "$BUNDLE_SRC"
1. The bundle's own dependency ("web-push"). Order relative to step 2 does not matter,
but run this BEFORE the first restart.
cd "$BUNDLE_SRC" && pnpm add web-push@^3.6.7
2. Register into the web profile (adds the dependency as link: AND appends
dsh-notify-push to dsh.profile.bundles via its dsh.bundle.patch declaration)
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC"
3. 只读校验组合结果(绝不碰正在运行的服务器)
dsh --profile web --dump-config | grep dsh-notify-push # expect: "- id: dsh-notify-push"
4. 校验依赖能否解析(在配置档目录下执行)
cd ~/.dsh/profiles/web && node --input-type=module -e "await import('web-push'); console.log('web-push resolvable')"
依赖说明:dsh 配置档在 pnpm-workspace.yaml 里使用 nodeLinker: hoisted,所以第 1 步的 pnpm add 会装进配置档的 node_modules,并从链接后的 bundle 解析。如果第 4 步报 web-push 无法解析,重跑第 1 步,然后在配置档目录里执行 pnpm install。
接着重启并启用: bash
systemctl --user restart dsh-web.service
In the DSH UI on each device: /notify → ON → grant the notification permission.
预期结果:首次启动时出现 ~/.dsh/notify-push/vapid.json + subscriptions.json;每台启用的设备对应一条订阅;向智能体提问并触发 ask_user_question,每台启用的设备都会收到通知。
新标签页显示「No sessions yet」
在 DSH 0.1.2-rc.1 上,上游 client-hmr 会为每个标签页开一条常驻的开发者更新连接。标签页开得够多,就会耗尽浏览器的 HTTP/1.1 槽位,导致新的会话列表请求和 WebSocket 握手发不出去。这与 dsh-notify-push 通知插件无关,也不代表会话被删了。
Smart-DSH 内置一个带版本与校验和保护的变通方案,让多个标签页共用一条 HMR 连接。在当前检出目录下执行: bash
node scripts/apply-shared-hmr.cjs --check # read-only; resolves the dsh on PATH
node scripts/apply-shared-hmr.cjs --apply # explicit write with a private backup
node --test scripts/test-shared-hmr.cjs scripts/test-apply-shared-hmr.cjs
它不会重启 DSH,也不会改动浏览器偏好或会话数据。版本未知或本地已有改动都会被拒绝。这是独立的可选安装步骤;只复制 dsh-notify-push 不会应用它。具体路径、回滚、浏览器要求、实时更新限制,以及 DSH 升级后如何复查,见变通方案指南。
Esc 停止(dsh-esc-stop)
智能体干活时按 Escape,当前这一轮就被取消——和输入框的 停止生成 按钮是同一种取消。默认开启,开关在 Settings → General → Esc で推論を停止;它的值就是用户设置文档里的宿主设置 esc-stop.enabled,所以跟着账号跨设备同步,切换不需要改文件或重启。
Escape 是共享按键,只要有更近的界面占用了它,监听器就拒绝响应:
| 拒绝情形 | 原因 |
| 按住 Ctrl/Meta/Alt/Shift,或按键重复 | 属于浏览器/系统快捷键,而且长按不能反复触发 |
| 输入法组词中 | 按键归输入法所有 |
| event.defaultPrevented | 已经消费掉 Escape 的处理器优先 |
| 目标是原生 input/textarea/select | 队列消息编辑器会在按 Escape 时自行关闭 |
| 有 [aria-modal="true"] 元素打开 | 设置面板打开期间由它接管 Escape |
| 有 [role="listbox"]/[role="menu"] 元素打开 | 命令菜单、popupSelect 或选择器接管 Escape |
| 开关处于 OFF、没有任务在跑、会话已被移除,或有子代理在台上 | 没有可停止的对象,这些状态下按钮本身也不存在 |
监听器在 document 上以捕获阶段运行,并延迟一个微任务再判断,所以 Escape 绝不会既关闭菜单又停止本轮:更近的处理程序是否消费了按键,或它刚关闭的浮层,都只读取一次——在按键完成路由之后。
接受的路径就是按钮自己的那条(sessions.scope(id).get("conversation").cancel()),所以失败呈现完全一致:消息落入会话的 promptError。Stop 只取消进行中的这一轮——排队的消息保留,并按 FIFO 顺序继续。
安装步骤与 dsh-notify-push 相同,只是换成 dsh-esc-stop,另外多一步注册(该 bundle 自身没有运行时依赖,所以额外的 pnpm add 步骤不适用)。dsh plugin 只是转发给 pnpm,而配置档是 pnpm 工作区根目录,所以依赖需要 -w;层列表直接编辑: bash
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-esc-stop"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")" && cp -r /tmp/Smart-DSH/dsh-esc-stop "$BUNDLE_SRC"
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC" -w
node -e 'const fs=require("fs"),p=process.env.HOME+"/.dsh/profiles/web/package.json",m=JSON.parse(fs.readFileSync(p,"utf8")),b=m.dsh.profile.bundles;if(!b.includes("dsh-esc-stop"))b.push("dsh-esc-stop");fs.writeFileSync(p,JSON.stringify(m,null,2)+"\n")'
dsh --profile web --dump-config | grep dsh-esc-stop # read-only composition check
systemctl --user restart dsh-web.service # never from the session being restarted
在重启之前,运行中的服务器保持启动时的组合:设置行和监听器只在重启之后出现。
细节、逐条防护的理由和限制:dsh-esc-stop/README.md。
子代理任务委派(dsh-tasks)
父智能体生成具名子代理,每个只配一个专属动词——worker(只编辑任务指定的路径,然后检查这些路径)、explorer(只测量,从不编辑)、hacker(攻击任务的前提;在 MiL 中恰好给出三个 hack,不做实验)、reviewer(对照任务评判指定的 diff,引用 loc:=path:line)。子代理永远看不到父对话:任务就是全部内容,worker 启动时若没有具体的具名路径,会被 harness 拒绝。
报告通过一个同级 hub 流转,形式是发给活着的……的一行 MiL 事实名单 id;散文式报告会被拒绝(report:=∅ ∵ ¬MiL),子代理会被提醒一次,要求重新陈述。它们会出现在 Web UI 的子代理会话标签页里,任务日志保存在 $DSH_HOME/tasks/ 下。子代理里的 ask_user_question 会被拒绝并记录日志,所以子代理永远不会卡在一个它看不到答案的问题上;子代理的轮次不会发通知。
安装(无运行时依赖——dsh-notify-push 需要的 pnpm add 步骤在这里不适用;-w 标志是因为该配置档是 pnpm workspace 根): bash
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-tasks"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")" && cp -r /tmp/Smart-DSH/dsh-tasks "$BUNDLE_SRC"
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC" -w
node -e 'const fs=require("fs"),p=process.env.HOME+"/.dsh/profiles/web/package.json",m=JSON.parse(fs.readFileSync(p,"utf8")),b=m.dsh.profile.bundles;if(!b.includes("dsh-tasks"))b.push("dsh-tasks");fs.writeFileSync(p,JSON.stringify(m,null,2)+"
")'
dsh --profile web --dump-config | grep dsh-tasks # read-only composition check
systemctl --user restart dsh-web.service # never from the session being restarted
单元测试套件(25 个测试:工具策略、MiL 报告器、两个 bundle 部分)通过配置档提升的 @deepseek-ai/ 解析 DSH 导入,所以在注册后从 $BUNDLE_SRC 运行——裸克隆会报 ERR_MODULE_NOT_FOUND。详情见 dsh-tasks/README.md。
OpenCode Go 模型目录(opencode-go/)
DSH 内置 OpenCode Go 服务商,模型列表冻结在目录里,于是订阅实际提供的新模型(以及目录已删除的模型)会逐渐对不上。这个目录是一套小工具,读取实时的 Go 模型列表,并重写 $DSH_HOME/settings.yaml 里的 llm-pi-ai.providers.opencode-go.:
| 文件 | 作用 |
| gen.mjs | 从内置的 pi-ai 目录表生成目录增量(内置目录路由 + 两条仅声明路由,用于目录中未知的 ID) |
| apply.mjs | 通过 YAML Document API 把增量拼接进 settings.yaml(保留注释),并生成 backups/ + restore.sh 快照 |
| verify.sh | 走真实链路探测每个已配置模型(流式 chat/completions,外加 /responses 路由),打印 OK/SKIP/NG 表格 |
| set-default-model.cjs | 测试辅助:在一次性 settings 副本里替换 agent-default-model |
bash
cd opencode-go
node gen.mjs # review the 29-model catalog (3 routes)
node apply.mjs --dry-run # diff only
node apply.mjs # write (live-watched by DSH — no restart)
sh verify.sh # per-model wire probe
以下三点已对照 2026-09-29 的实时端点验证:
- 隐私门控: DeepSeek 系列需要把 workspace Privacy 区域设为 Global,muse-spark- 还需要「trains on request data」同意。两者都在 OpenCode 控制台的 Privacy 页面(导航里没有链接——直接打开 …/console/<org_id>/settings/privacy)。
- 目录中未知的 ID(deepseek-v4.1-flash、mimo-v2.6-、space-bunny-free、gpt-6-luna、grok-4.7、……)无法从冻结的 pi-ai 目录继承,因此它们声明在单独的 opencode-go-completions / opencode-go-responses 路由上,这些路由自带 api/baseURL——主路由必须保持协议混合。
- 已弃用的 ID(kimi-k2.6、omen-alpha、minimax-m2.7)被排除;grok-4.7 以非推理模式运行,因为 xAI 拒绝 reasoning_effort: "none"。
详情:opencode-go/README.md。
配套基础设施(远程访问 + 手机)
最小可复现形式——作者配置里的 flock/guard/URL-file 管道是机器相关的,有意不放进本仓库: ini
~/.config/systemd/user/dsh-web.service (minimal working form)
Adjust the ExecStart path to your dsh install location (which dsh).
[Unit]
Description=DSH web server
After=network.target
[Service]
Environment="DSH_HOME=%h/.dsh"
ExecStart=%h/.local/bin/dsh web --host 127.0.0.1 --port 3080 \
--trusted-host <machine>.<tailnet>.ts.net --no-open
Restart=on-failure
[Install]
WantedBy=default.target
bash
Expose to the tailnet only (current tailscale CLI syntax; run once, persists):
tailscale serve --bg 3080
→ https://<machine>.<tailnet>.ts.net (proxying http://127.0.0.1:3080)
tailscale serve status # verify
配置过程中总结的注意事项:
- DSH 只绑定 loopback;Tailscale Serve 是唯一的外部暴露方式,所以未认证的 /push/sw.js 路由只有 tailnet 设备能访问。
- --trusted-host 的值必须和手机使用的主机名一致,否则浏览器信任围栏会拒绝连接。
- Web 应用已经自带 PWA manifest,所以从主屏安装后,通知点击聚焦路径也能正常工作。
- 如果你还跑了 guard 来防止重复启动 DSH,不要为了测试再启动第二个临时实例——见「在没有实时服务器的情况下测试」。
- tailscale serve 的语法各版本不同(当前命令行是 serve --bg 3080,旧版是 serve https / http://...)——查 tailscale serve --help。
在没有实时服务器的情况下测试
随包附带一套自包含的 node --test 测试(只写入临时 DSH_HOME,不碰真实状态): bash
cd dsh-notify-push && pnpm install && npm test
expect: pass 1 / fail 0
覆盖内容:
- 宿主侧用假的 ctx(ctx.on/effect/inject + webServer.register 收集器 + Readable.from 请求体):路由注册、/api/push/ 上的认证 401/403、订阅校验 + 持久化、waterfall 委托(next() 的值透传),以及 /push/sw.js 上的 Service-Worker-Allowed: / 头。
- 推送发送路径用真实的 ECDH P-256 订阅来验证:加密 + VAPID 签名成功,发送在 DNS 处失败(对无效端点报 ENOTFOUND),这证明整条链路一直到网络层都是通的。
- 客户端那一半没有自动化测试覆盖:手动验证时,用 window.__ModuleLoader__ shim 和桩 ctx 加载 lib/client.js(断言 user-questions/request 监听器把 next() 的返回值透传出去,并且 /notify 的 popupSelect 贡献项完成注册)。
dsh-esc-stop 自带测试套件,它通过同一个 ModuleLoader shim 覆盖了客户端那一半(决策守卫、监听器接线、销毁、设置行): bash
cd dsh-esc-stop && npm install && npm test
expect: tests 14 / pass 14 / fail 0
运维
- 开关:每台设备用 /notify 开/关。撤销浏览器权限 → 下次启动时自动重新订阅会失败,已存的开关会掉到 OFF。
- 重启检查清单:DSH 会话靠只追加日志存活;重启后确认 $DSH_HOME/notify-push/vapid.json 还在(换新密钥会让所有订阅变成孤儿——如果你是有意重置密钥,连 subscriptions.json 一起删掉)。
- 收不到通知时,按顺序排查:(1) vapid.json 熬过了重启,(2) 服务器日志里没有 notify-push 报错,(3) 站点的通知权限是「允许」,且 Chrome 的系统级通知已打开,(4) subscriptions.json 里还有条目。
- 这个 bundle 遵循 dsh.bundle.patch + dsh.client 三层插件模式;最小声明见 dsh-notify-push/cordis.patch.yml 和 package.json。
定制要点
- 语言:通知标题/正文前缀和 /notify 命令文案默认是日文(選択肢、(他 N 件)、通知: ON)。改 lib/index.js 里的 buildPayload,以及 lib/client.js 里的 questionSummary/statusLabel/命令标签。
- VAPID subject:configure() 里的 mailto:root@localhost 是占位符;有些推送服务会对它告警——换成你自己的联系地址。
- 浏览器支持:任何暴露 PushManager 的浏览器都能通过 /notify 的可用性检查;目前只验证过 Android Chrome 和桌面 Firefox。
- sw/sw.js 里的 requireInteraction: true 会让通知停留在屏幕上;想用一闪而过的横幅就把它调低。
KG 节点
镜像本仓库的知识图谱节点: ~/knowledge/nodes/agents/dsh-notify-push-bundle-pattern.md。
移动端布局(DSH 0.1.2-rc.1)
视口宽度不超过 767 CSS 像素时,折叠侧边栏只占 logo 那一角;聊天列使用整个视口宽度。点 DeepSeek logo 一步打开会话列表,而不是先经过中间的图标栏。问题卡片和计划卡片使用列宽(100%),不再用 680px 的内容下限,两侧留 2.5% 内边距,这样窄屏手机不会裁掉任何一边。桌面布局不变。
上游展开的侧边栏面板保持原有行为。CSS-module 选择器这些内容与版本相关:升级 DSH 之后要重新检查。没有改动任何通知逻辑。
针对现有 DSH 服务器做浏览器回归检查(不会启动或重启 DSH):
PLAYWRIGHT_MODULE=/path/to/playwright DSH_LOGIN_URL_FILE=/path/to/private-login-url.txt node scripts/test-mobile-layout.cjs。
使用隔离的浏览器上下文,验证 360/412/767/768/1280 CSS 像素宽度、开关往返切换和清理。登录 URL 文件必须保密,绝不提交。
Esc 停止回归检查
scripts/test-esc-stop.cjs 在运行中的 DSH 页面里驱动随包发布的 dsh-esc-stop 监听器(同样输入私密登录 URL,不重启 DSH): bash
PLAYWRIGHT_MODULE=/path/to/playwright \
DSH_LOGIN_URL_FILE=/path/to/private-login-url.txt \
node scripts/test-esc-stop.cjs
它会断言组合后的服务商 bundle(dsh-api-session-controller、dsh-client-ui-renderer、dsh-client-ui-settings)都已存在,然后针对桩会话派发真实的 KeyboardEvent——运行中的回合取消一次,而空闲、关闭开关、按键重复、修饰键组合、模态框打开、列表浮层打开、原生输入框获得焦点、按键已被消费、销毁以及具名拒绝原因这些情况都不取消。桩会话意味着不会取消任何真实回合;整个 bundle 也会加载进页面,以证明它能解析并注册到 __ModuleLoader__。登录 URL 文件必须保密,绝不提交。预览分享卡片长什么样 ↓
Smart-DSH — 移动端 UI、通知,以及 Esc 停止,为 DeepSeek Harness 打造
相关插件 · UI / 主题
这是你的仓库?认领条目