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

手机 PWA 网关 zylzyqzz/dsbot

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

DSBot · 让 DeepSeek Harness 在手机上变成真 PWA

38/100
综合分
3
GitHub 星标
137
npm 周下载
—/5
用户评分

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

自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add zylzyqzz/dsbot
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · market
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 8 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包@zylzyqzz/dsbot(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 16:33:53

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

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

可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 94 处,展示前 79 条)
  • 执行外部命令来自配置/变量
    scripts/setup-browser.mjs:276
    const result = spawnSync(command, args, { stdio: options.quiet ? 'pipe' : 'inherit', encoding: 'utf8', ...options })
  • 发起网络请求
    scripts/setup-browser.mjs:287
    const response = await fetch(url, { redirect: 'follow' })
  • 写入本地文件
    scripts/setup-browser.mjs:290
    writeFileSync(destination, buffer)
  • 写入本地文件
    scripts/setup-browser.mjs:295
    mkdirSync(destination, { recursive: true })
  • 读取本地文件
    scripts/setup-browser.mjs:315
    if (!existsSync(file)) continue
  • 写入本地文件
    scripts/setup-browser.mjs:316
    try { chmodSync(file, 0o755) } catch { }
  • 发起网络请求
    scripts/setup-browser.mjs:321
    const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(20000) })
  • 读取本地文件
    scripts/setup-browser.mjs:357
    if (existsSync(binary)) {
  • 读取本地文件
    scripts/setup-browser.mjs:397
    const from = existsSync(inner) ? inner : staging
  • 写入本地文件
    scripts/setup-browser.mjs:398
    mkdirSync(prefix, { recursive: true })
  • 删除本地文件递归
    scripts/setup-browser.mjs:399
    rmSync(target, { recursive: true, force: true })
  • 写入本地文件
    scripts/setup-browser.mjs:401
    renameSync(from, target)
  • 删除本地文件递归
    scripts/setup-browser.mjs:404
    rmSync(from, { recursive: true, force: true })
  • 删除本地文件递归
    scripts/setup-browser.mjs:406
    rmSync(staging, { recursive: true, force: true })
  • 读取本地文件
    scripts/setup-browser.mjs:409
    if (!existsSync(binary)) throw new Error(`Chrome binary missing after extract: ${binary}`)
  • 读取本地文件
    scripts/setup-browser.mjs:419
    try { os = parseOsRelease(readFileSync('/etc/os-release', 'utf8')) } catch { }
  • 读取本地文件
    scripts/setup-browser.mjs:445
    if (existsSync(core)) { log(` playwright-core already present: ${core}`); return dir }
  • 写入本地文件
    scripts/setup-browser.mjs:450
    mkdirSync(dir, { recursive: true })
  • 执行外部命令来自配置/变量
    scripts/setup-browser.mjs:489
    const result = spawnSync(process.execPath, args, { encoding: 'utf8' })
  • 执行外部命令来自配置/变量
    scripts/setup-browser.mjs:599
    const patched = spawnSync(process.execPath, [patchScript], { encoding: 'utf8' })
  • 读取本地文件
    scripts/verify-browser.mjs:65
    if (!chrome || !existsSync(chrome)) {
  • 读取本地文件
    scripts/verify-browser.mjs:70
    if (!existsSync(shim)) {
  • 发起网络请求
    scripts/xray-split-route.mjs:168
    const response = await fetch(url, { redirect: 'follow' })
  • 写入本地文件
    scripts/xray-split-route.mjs:170
    writeFileSync(destination, Buffer.from(await response.arrayBuffer()))
  • 写入本地文件
    scripts/xray-split-route.mjs:174
    mkdirSync(geoDir, { recursive: true })
  • 读取本地文件
    scripts/xray-split-route.mjs:177
    if (existsSync(file)) { log(` ${name} present (${(readFileSync(file).length / 1024).toFixed(0)} KB)`); continue }
  • 读取本地文件
    scripts/xray-split-route.mjs:177
    if (existsSync(file)) { log(` ${name} present (${(readFileSync(file).length / 1024).toFixed(0)} KB)`); continue }
  • 读取本地文件
    scripts/xray-split-route.mjs:181
    log(` ${name} -> ${file} (${(readFileSync(file).length / 1024).toFixed(0)} KB)`)
  • 执行外部命令固定命令
    scripts/xray-split-route.mjs:186
    const probe = spawnSync('xray', ['-test', '-config', configFile], { encoding: 'utf8' })
  • 读取本地文件
    scripts/xray-split-route.mjs:202
    if (!existsSync(configFile)) throw new Error(`config not found: ${configFile}`)
  • 读取本地文件
    scripts/xray-split-route.mjs:203
    const original = readFileSync(configFile, 'utf8')
  • 写入本地文件
    scripts/xray-split-route.mjs:220
    copyFileSync(configFile, backup)
  • 写入本地文件
    scripts/xray-split-route.mjs:222
    writeFileSync(configFile, JSON.stringify(config, null, 2) + '\n')
  • 写入本地文件
    scripts/xray-split-route.mjs:230
    copyFileSync(backup, configFile)
  • 读取本地文件
    lib/lan-gate-auth.cjs:49
    const existing = fs.readFileSync(target, 'utf8').trim()
  • 写入本地文件
    lib/lan-gate-auth.cjs:55
    fs.mkdirSync(path.dirname(target), { recursive: true })
  • 写入本地文件
    lib/lan-gate-auth.cjs:58
    fs.writeFileSync(target, password + '\n', { mode: 0o600 })
  • 写入本地文件
    lib/lan-gate-auth.cjs:59
    try { fs.chmodSync(target, 0o600) } catch (err) { }
  • 读取本地文件
    lib/lan-gate-server.cjs:54
    function loadDecisions() { try { var raw = JSON.parse(fs.readFileSync(stateFile(), 'utf8')); if (raw && typeof raw === 'object' && raw.decisions && ty
  • 写入本地文件
    lib/lan-gate-server.cjs:55
    function saveDecisions(decisions) { try { fs.mkdirSync(dshHome(), { recursive: true }); var tmp = stateFile() + '.tmp'; fs.writeFileSync(tmp, JSON.str
  • 读取密钥环境变量
    lib/lan-gate-server.cjs:88
    var VAPID = { publicKey: process.env.LAN_GATE_VAPID_PUBLICKEY || '', privateKey: process.env.LAN_GATE_VAPID_PRIVATEKEY || '' }
  • 读取密钥环境变量
    lib/lan-gate-server.cjs:120
    var PASSWORD = process.env.LAN_GATE_PASSWORD || ''
  • 读取密钥环境变量
    lib/lan-gate-server.cjs:121
    var PASSWORD_FILE = process.env.LAN_GATE_PASSWORD_FILE || ''
  • 读取本地文件
    lib/lan-gate-server.cjs:126
    ? { password: fs.readFileSync(PASSWORD_FILE, 'utf8').trim(), created: false, file: PASSWORD_FILE }
  • 读取本地文件
    lib/lan-gate-server.cjs:243
    try { body = fs.readFileSync(abs) } catch (e) { return false }
  • 发起网络请求
    lib/lan-gate-server.cjs:265
    var request = http.request({
  • 读取本地文件
    lib/lan-gate-server.cjs:346
    const stat = fs.statSync(file)
  • 读取本地文件
    lib/lan-gate-server.cjs:349
    const text = fs.readFileSync(file, 'utf8')
  • 发起网络请求
    lib/lan-gate-server.cjs:431
    var req = http.request(sub.endpoint, { method: 'POST', headers: headers }, function (res) {
  • 读取密钥环境变量
    lib/lan-gate-server.cjs:470
    var DSH_TOKEN = process.env.LAN_GATE_DSH_TOKEN || ''
  • 发起网络请求
    lib/lan-gate-server.cjs:507
    var upstream = http.request({ host: TARGET_HOST, port: TARGET_PORT, method: 'GET', path: '/?token=' + encodeURIComponent(DSH_TOKEN), headers: headers
  • 发起网络请求
    lib/lan-gate-server.cjs:662
    var upstream = http.request({ host: TARGET_HOST, port: TARGET_PORT, method: req.method, path: req.url, headers: headers }, function (upRes) {
  • 读取本地文件
    lib/web-push.cjs:94
    const raw = JSON.parse(fs.readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/web-push.cjs:102
    fs.mkdirSync(path.dirname(file), { recursive: true })
  • 写入本地文件
    lib/web-push.cjs:104
    fs.writeFileSync(tmp, JSON.stringify(keys, null, 2), { mode: 0o600 })
  • 写入本地文件
    lib/web-push.cjs:105
    fs.renameSync(tmp, file)
  • 发起网络请求
    pwa/inject.js:151
    return fetch('/pwa/push/subscribe', {
  • 发起网络请求
    pwa/inject.js:171
    return fetch('/pwa/push/subscribe', {
  • 发起网络请求
    pwa/inject.js:643
    fetch('/pwa/push/send', {
  • 发起网络请求
    pwa/sw.js:91
    const fetchPromise = fetch(req)
  • 发起网络请求
    pwa/sw.js:107
    return fetch(req)
  • 发起网络请求
    pwa/sw.js:114
    return await fetch(req)
  • 发起网络请求
    pwa/sw.js:147
    return fetch('/pwa/push/subscribe', {
  • 发起网络请求
    scripts/check-upstream-contract.mjs:82
    const req = http.request({
  • 读取本地文件
    scripts/check-upstream-contract.mjs:120
    const css = readFileSync(join(ROOT, 'pwa', 'app.css'), 'utf8')
  • 读取本地文件
    scripts/check-upstream-contract.mjs:210
    } else if (!existsSync(SLOT_CATALOG)) {
  • 读取本地文件
    scripts/check-upstream-contract.mjs:213
    const catalog = readFileSync(SLOT_CATALOG, 'utf8')
  • 读取本地文件
    scripts/check-upstream-contract.mjs:258
    const text = readFileSync(join(ROOT, file), 'utf8')
  • 读取本地文件
    scripts/patch-ds-bot-image.mjs:195
    if (!existsSync(file)) {
  • 读取本地文件
    scripts/patch-ds-bot-image.mjs:200
    if (!existsSync(backup)) { console.error(`no backup to restore: ${backup}`); return 2 }
  • 写入本地文件
    scripts/patch-ds-bot-image.mjs:201
    copyFileSync(backup, file)
  • 读取本地文件
    scripts/patch-ds-bot-image.mjs:206
    const result = patcher(readFileSync(file, 'utf8'))
  • 写入本地文件
    scripts/patch-ds-bot-image.mjs:217
    if (!existsSync(backup)) copyFileSync(file, backup)
  • 读取本地文件
    scripts/patch-ds-bot-image.mjs:217
    if (!existsSync(backup)) copyFileSync(file, backup)
  • 写入本地文件
    scripts/patch-ds-bot-image.mjs:218
    writeFileSync(file, result.source)
  • 执行外部命令来自配置/变量
    scripts/patch-ds-bot-image.mjs:220
    const check = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
  • 写入本地文件
    scripts/patch-ds-bot-image.mjs:222
    copyFileSync(backup, file)
  • 读取本地文件
    scripts/pw-proxy-shim.mjs:57
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 读取本地文件
    scripts/pw-proxy-shim.mjs:127
    if (!existsSync(candidate)) continue
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 安全远程访问网关
  • 安装到手机主屏
  • 离线可用与任务推送

README

由 DeepSeek 最新模型翻译生成
展开完整文档(README 剩余 1 节)
DSBot · 让 DeepSeek Harness 在手机上变成真 PWA

把 DeepSeek Harness(DSH)变成手机上的完整 PWA:安全远程访问你的自建 DSH + 一键「添加到主屏」成独立 App + 离线可用 + 触屏手势 + 任务完成通知。

基于 MIT 的 dsh-mobile-gate(安全网关基座)做差异化增强。

当前版本 0.3.0 —— 对齐 DSH 官方 0.2.x(engines.dsh: ">=0.2.0 <0.3.0")。

📍 生态定位:社区现有移动端方案都停留在「窄屏 CSS 微调」,本项目做的是移动端 PWA 一站式完整方案。

✨ 特性

| 模块 | 说明 |
| 📡 安全远程访问 | 独立子进程网关,首次访问本机审批(审批绑 IP、令牌绑浏览器,同一出口 IP 下的手机和电脑可同时在线)+ 可选访问密码 + 每 IP 限流。DSH 主服务仍只监听本机回环,/api 信任栅栏不受影响 |
| 📱 真 PWA | manifest.json + service worker:手机浏览器「添加到主屏」后全屏独立窗口运行,带图标/启动屏/主题色 |
| 🧩 可安装 | 主屏图标、standalone 显示、apple-touch-icon、maskable 图标 |
| 🌐 离线可用 | service worker:静态壳缓存优先、API 网络优先、导航离线回退页 |
| 👆 触屏手势 | 下拉刷新、边缘右滑返回、捏合缩放字体(可重置) |
| 🔔 任务完成通知 | 真正的 Web Push(RFC 8291 aes128gcm 加密 + RFC 8292 VAPID 鉴权),切到别的 App 也能收到 |
| 📐 触屏布局 | 44px 触摸目标、safe-area 适配、全屏弹窗、紧凑排版、代码横向滚动——桌面零影响 |
| 🌐 Bot 浏览器 | 一条命令给 Bot 装上真实 Chrome(Chrome for Testing + playwright-core + 代理 shim + profile patch + 自检),截图直接出现在对话里,遇到登录/支付/验证码可人机接力。详见 docs/browser.md |
| 🔒 桌面不受影响 | 所有规则都以 html[data-lan-device="phone"] 或以排除 data-lan-device="desktop" 的 @media 为根 |
| 🎯 0.2 版 DOM 对齐 | 移动端 CSS 全部改用 DSH 0.2 的 slot 词表(main.conversation / conversation.view / conversation.chat.flow / conversation.input.model / conversation.composer.dock …),0.1 时代已失效的选择器全部移除 |

🏗️ 架构

手机 ──> 网关 (独立 Node 子进程 · 0.0.0.0:3088) ├─ 未批准 -> 「等待本机批准」页面(轮询 /lan-gate/admin) ├─ 已批准+令牌Cookie -> 反向代理到 DSH Web UI (本机地址:端口)(每个浏览器各领一枚令牌) │ └─ HTML 注入:manifest link + PWA bootstrap + 触屏CSS + randomUUID polyfill ├─ /pwa/* -> 直接提供 manifest / sw.js / app.css / 图标 / offline.html ├─ /pwa/push/* -> 订阅 / 发送(真实加密推送) └─ 超限 -> 429 限流页电脑 127.0.0.1:3088/lan-gate/admin -> 批准/拒绝/撤销设备、选择访问方式(手机/电脑/自动)

- 网关是独立子进程,与 DSH 主进程隔离:挂掉不影响主服务,插件停止时自动终止。
- DSH 0.2 起官方直接拒绝 --host 0.0.0.0(通配地址一律报错),所以 Web UI 永远只监听一个具体的本机地址。远程访问只能靠本网关:它独占对外端口并做审批,DSH 侧的 /api 信任栅栏完好。
- 网关的目标地址不是写死的:插件从 webStartup 服务读出本次启动真实的 host/port(dsh web --port 0 / --host <addr> 都照样工作),再显式传给子进程。

🚀 快速开始(在你自己的 DSH 服务器上)

方式零:npm 一键安装(推荐,已发布)

npm add @zylzyqzz/dsbot@0.3.0
dsh plugin --profile web add @zylzyqzz/dsbot

已声明 dsh.bundle manifest,安装后自动激活配置层,无需手写 patch。
包:https://www.npmjs.com/package/@zylzyqzz/dsbot

方式一:从 GitHub 本地安装

git clone https://github.com/zylzyqzz/dsbot.git
cd dsbot
dsh plugin --profile web add ./dsbot
源码安装需要给 pnpm 放行构建脚本(见官方 publish 文档的 allowBuilds 说明)。本包是纯 JS、无构建步骤,npm 预构建包不需要任何授权。

方式二:静态挂载

参考 cordis.patch.yml.example,把绝对路径替换进你的 profile patch。

配置(环境变量)

| 变量 | 默认 | 说明 |
| LAN_GATE_PORT | 3088 | 网关监听端口 |
| LAN_GATE_HOST | 0.0.0.0 | 网关监听地址 |
| LAN_GATE_TARGET_HOST | 127.0.0.1 | 本机 DSH Web UI 地址(插件会自动填 webStartup.host,此变量优先) |
| LAN_GATE_TARGET_PORT | 3080 | 本机 DSH Web UI 端口(插件会自动填 webStartup.port,此变量优先) |
| LAN_GATE_RATE_LIMIT | 120 | 每 IP 每分钟请求上限 |
| LAN_GATE_PASSWORD | 空 | 访问密码。设了之后打开网关先要输密码,密码只存在服务器上,浏览器拿到的是 HMAC 派生的会话 cookie |
| LAN_GATE_TRUSTED_HOSTS | 空 | 对外服务地址白名单,逗号分隔(如 203.0.113.10,app.example)。DSH 0.2 的 Host/Origin 栅栏按浏览器看到的 Host 校验,网关原样透传 Host,所以这里必须声明对外地址;不带端口的条目匹配任意端口 |
| LAN_GATE_REWRITE_HOST | 空 | 设为 1 时把 Host 改写成回环地址(仅老版本 DSH 需要) |
| LAN_GATE_DSH_TOKEN | 自动 | DSH 进程 launch token,插件从 ctx.connection 自动读取并交给网关 |
| LAN_GATE_PUBLIC_URL | 空 | 仅用于生成 token 化 URL 时的基准地址 |
| LAN_GATE_VAPID_PUBLICKEY | 自动生成 | Web Push VAPID 公钥(base64url) |
| LAN_GATE_VAPID_PRIVATEKEY | 自动生成 | Web Push VAPID 私钥(base64url PKCS#8) |
| LAN_GATE_VAPID_SUBJECT | mailto:dsbot@localhost | VAPID 联系方式 |

VAPID 密钥对未提供时会自动生成一次并保存到 ~/.dsh/lan-gate-vapid.json(权限 600),重启不失效。

DSH 0.2 的浏览器鉴权是怎么打通的

DSH 0.2 起,浏览器要拿一个签名 session cookie 才能用 Web UI,而这个 cookie 的首次签发需要一个进程 launch token(每次重启都会变)。本插件的做法:

1. 插件在 DSH 进程内从 ctx.connection.authenticatedUrl() 读出本次启动的 launch token,交给网关子进程;
2. 网关原样透传浏览器 Host(DSH 的 Host/Origin 栅栏与 cookie 的 authority 都基于它),并在浏览器还没有 session cookie 时,用这个 token 去上游换一张 cookie,直接转交给浏览器;
3. 于是用户打开的就是一个不带 token 的普通 URL,不需要复制启动日志里的 tokenized 链接。

前提是 DSH 侧要信任对外地址:把 LAN_GATE_TRUSTED_HOSTS 设成对外的 host 或 host:port(bundle 的 connection 行会自动把它并进 --trusted-host 列表),或直接在启动参数里加 --trusted-host <对外地址>。

使用流程

1. 服务端:启动 DSH + 本插件,看到 [dsbot] listening on 0.0.0.0:3088 -> <host>:<port> (pwa=on)。
2. 手机:浏览器打开 http://<你的IP>:3088 → 出现「等待本机批准」。
3. 电脑:打开 http://127.0.0.1:3088/lan-gate/admin → 批准该设备并选「手机」。
4. 手机:下拉刷新 → 进入 DSH Web UI(已注入 PWA)。
5. 添加到主屏:浏览器菜单 → 「添加到主屏」→ 变成独立 App。
6. (可选)任务完成通知:页面左下角会出现「开启」提示,允许后即可收到。

🌐 Bot 浏览器(真实 Chrome,一条命令)

ds-bot 有 12 个浏览器工具,但不自带浏览器,启动参数还是写死的、没有代理配置项。这条命令把缺的部分一次补齐: bash

在你的 DSH 服务器上

node scripts/setup-browser.mjs --proxy http://127.0.0.1:7890

装完重启

sudo systemctl restart dsh-web

它会:下载 Chrome for Testing → 装系统库(Ubuntu 24.04 的 libasound2t64 改名已处理)→ 装 playwright-core → 装代理 shim → 真开一个网页验证 → 把 - id: bot 写进 profile patch(先备份)。

装完 Bot 就多出 browser_open / browser_snapshot / browser_click / browser_screenshot / browser_handoff 等 12 个工具,其中 browser_screenshot 的截图直接进对话,不是一行工具日志。
bash
node scripts/setup-browser.mjs --verify-only # 随时自检
node scripts/setup-browser.mjs --print-config # 只打印 YAML
sudo node scripts/xray-split-route.mjs # 代理分流:国内直连,其余走代理
为什么需要 shim、代理怎么选、排错表 → docs/browser.md

🔔 任务完成通知是怎么判定的

DSH 0.2 在智能体工作期间会渲染 [data-chat-running]。注入脚本用 MutationObserver 盯着它:一旦从「有」变成「没有」,说明这一轮跑完了 —— 此时

1. 立刻弹一个本机 Notification(页面在前台时最直接);
2. 同时把消息发给网关的 /pwa/push/send,网关按订阅加密后推给其他设备。

0.2 版移除了旧的宿主侧 dsh-push.mjs 插件:它依赖 DSH 0.1 的 conversation/http 服务接口,在 0.2 已不存在。判定逻辑改到浏览器侧,反而更准(DSH 自己的 DOM 就是事实来源),也不再占用宿主进程。

⚙️ 本机安装测试 bash
npm test # 起 mock 上游,验证 /pwa 资产、HTML 注入、状态上报、Web Push 加解密回环

🗂️ 项目结构

| 路径 | 作用 |
| lan-gate.mjs | Cordis 插件入口:解析目标地址 + spawn 网关子进程 + 生命周期管理 |
| lib/lan-gate-server.cjs | 网关本体:零依赖单文件(HTTP 代理 + 审批 + 令牌 + 限流 + PWA 注入) |
| lib/web-push.cjs | 真 Web Push:VAPID 密钥对 + ES256 JWT + ECDH/HKDF 派生 + AES-128-GCM 加密 |
| pwa/manifest.json | PWA 安装清单 |
| pwa/sw.js | service worker(离线缓存 + 推送通知 + 订阅轮换) |
| pwa/inject.js | 注入页引导:注册 SW + 加载手势 + 通知订阅 + 任务完成判定 |
| pwa/touch-gestures.js | 下拉刷新 / 边缘返弹 / 捏合缩放 |
| pwa/app.css | 移动触屏布局(data-lan-device 前缀,桌面零影响) |
| pwa/offline.html | 离线回退页 |
| pwa/icons/ | SVG 源 + 192/512 PNG + maskable 图标 |
| cordis.patch.yml / .example | 插件 bundle 挂载层 |
| scripts/setup-browser.mjs | Bot 浏览器一键安装:Chrome + 系统库 + playwright-core + shim + 自检 + profile patch |
| scripts/verify-browser.mjs | 浏览器自检(真开一个网页,输出 JSON 报告) |
| scripts/pw-proxy-shim.mjs | Playwright 代理 shim:注入 --proxy-server 与去自动化标志,不改 ds-bot 源码 |
| scripts/xray-split-route.mjs | xray 分流修复:国内直连 / 其余走代理,带 xray -test 与自动回滚 |
| docs/browser.md | Bot 浏览器原理、配置、排错 |
| test/gateway.test.cjs | 网关冒烟测试 |
| test/browser-setup.test.cjs | 安装器 / shim / 分流规则的纯函数测试 |

🧑💻 开发贴士

- 隔离:网关是子进程,lan-gate.mjs 只负责 spawn + 生命周期,永不 import 它的服务代码进 DSH 进程。
- 环境变量要显式传:DSH 0.2 的 ctx.subprocess 会先洗掉子进程环境里的凭据和 DSH_ 事实,再叠加调用方给的 env。所以 LAN_GATE_、DSH_HOME、VAPID 密钥必须在 spawn 请求里显式给出。
- 移动 CSS 前缀:新规则一律挂 html[data-lan-device="phone"] 或排除 desktop 的 @media(max-width:820px),桌面必须永不受影响。
- 稳定选择器:用 [data-slot=...] / [data-*] / ARIA 而非 hash 类名,避免前端构建后失效。DSH 改 slot 名时移动端样式会静默失效 —— 升级 DSH 后记得回上游 packages/client 重新核一遍 slot 词表。
- 注入页的单引号坑:lib/lan-gate-server.cjs 里的注入脚本字符串,历史上有「双引号套双引号」bug,注意字面量转义。
- 推送必须加密:推送服务拒收明文 body,必须走 lib/web-push.cjs 的 encryptPayload + buildVapidAuthorization。

🙏 致谢

- dsh-mobile-gate(MIT)——安全网关基座。
- awesome-dsh-plugin —— 社区插件精选列表。

⚠️ 安全须知

安装插件 = 在你的机器上运行第三方代码,权限与你本人相同。收录/发布不等于安全审查。请:只在你自己的服务器上跑、别放密钥到不熟悉的环境、定期审计 lib/lan-gate-server.cjs 的变更。远程访问务必走审批 + 令牌,别把网关端口裸奔到公网。

License

MIT。网关 lib/lan-gate-server.cjs 基于 dsh-mobile-gate 扩展,保留原 MIT 版权与许可,详见 LICENSE。
预览分享卡片长什么样 ↓zylzyqzz/dsbot
分享 · DeepSeek Harness Hub
zylzyqzz/dsbot

DSBot · 让 DeepSeek Harness 在手机上变成真 PWA

这是你的仓库?认领条目

回到顶部