ptrel1/dsh-postapi-bridge

dsh-postapi-bridge

DeepSeek Harness (DSH) 官方标准双半侧扩展插件:为外部机器人(MaiBot / 飞书 / 微信)及 CI/CD 系统提供开箱即用的轻量 HTTP POST / RESTful 任务调度、会话管理与 MCP 工具调用网关。

미분류bridgedeepseek-harness-pluginpostapi

dsh-postapi-bridge 🚪🔐

DeepSeek Harness (DSH) 双半侧统一网关与鉴权桥梁插件
融合 人类端多用户安全登录 + 机器端轻量 HTTP POST / RESTful API 调度网关。
⭐ 主要目的之一:让 DSH 支持公网访问——用账号密码自助换取官方动态凭证,替代"登录服务器抓 token"(见「🎯 核心目的之一」)。
⚠️ 文档时效说明(20260923 核对):本文部分章节写于 DSH 0.1.2 时代,
其中「需修改 DSH 源码加 requireSession 扩展点」的路径已不适用于 0.1.5+——
官方已在 client/connection 内原生提供 browserAuth.isAuthenticated() 门禁,
无需再打源码补丁。改动细节见下方「🔐 公网账号系统」章节的现状标注。
阅读时以带 ✅/⚠️ 标注的现状说明为准。

📦 npm 发布状态

⚠️ npm 上的版本已滞后:registry 上最新为 `0.1.0`,而本仓库当前已是 `0.6.6`。
请勿以 npm 版本为准——0.1.0 缺少 sessionAuth 服务接线,且缺少其后的全部安全加固
(会话指纹绑定、step-up 提权、伪回环绕过修复、机器通道 fail-closed 等)。
以本仓库为准(当前 `0.6.7`)。
✅ 推荐安装方式:从本仓库源码安装
```bash
dsh plugin --profile web add git+https://github.com/ptrel1/dsh-postapi-bridge.git
`
🔗 npm 页面(版本较旧,仅供参考):https://www.npmjs.com/package/dsh-postapi-bridge

版本记录 v0.9.0(交付物临时签名下载)

  • 新增两个端点:为「present 过的交付物」签发有时效、绑用户的下载地址。

客户端(含安卓)用普通 GET 即可拉文件,不必再组合 Cookie/Bearer。

接口 · 方法 · 鉴权 · 说明 /api/dsh/v1/file-ticket · POST · 已登录(插件会话) · 为一条 present 记录签发临时下载票据 /api/dsh/v1/file/ · GET · 已登录 + 票据绑定本人 · 流式下载,支持 Range 续传

  • 有效期规则(两段独立):
签发 file-ticket  ──► 「待激活」,距 issuedAt < 48h 内有效
                         │
  第一次下载请求    ──► 激活:activatedAt = now,此后可用到 now + 5h
                         │
    激活后 5h 内     ──► 可任意下载(含 Range 续传、可重复请求,无次数限制)
    now >= activatedAt + 5h         ──► 410
    未激活 且 now >= issuedAt + 48h ──► 410
    过期后           ──► 重新调 file-ticket 拿新票(唯一续期方式)
⚠️ 过期的是「票据」,源文件永远不删不改。
交付物是模型产出的工作区文件,生命周期属于用户/工作区;本插件只签发访问凭证,
无权也不该在任何过期路径上删除别人目录里的文件。
  • 签发请求:{ "sessionId": "...", "seq": 7, "index": 0 }

(seq = deliverables/presented 事件的 seq,index = 该事件 files 数组下标, 与官方前端 presentedFileUrl(sessionId, seq, index) 同一套坐标)。

  • 200:
{
      "ok": true,
      "url": "/api/dsh/v1/file/<token>",
      "issuedAt": 1730000000000,
      "activateDeadline": 1730172800000,
      "expiresAt": 1730172800000,
      "activateWindowHours": 48,
      "sessionHours": 5,
      "filename": "报告.docx",
      "size": 12345
    }
⚠️ `expiresAt` 语义 = 待激活截止时刻,不是下载有效期。
真正的下载有效期从首次下载起算 sessionHours(默认 5h),签发时无法预知,
因此不可能提前写进响应。expiresAt 仅为向后兼容保留的歧义字段,
新客户端请读 activateDeadline + sessionHours,忽略 expiresAt(计划 0.10.x 移除)。
🔒 响应体不返回 `ticketId`。 它是服务端状态库的键,属于内部实现细节;
对外暴露会让攻击者能探测「某张票是否存在」,也会把内部键变成对外的兼容契约。
客户端只需要 url。
  • 401 未登录;403 路径逃逸;404 present 记录不存在;413 超限(签发时就拒)
  • 下载:GET 该 url。
  • 200 文件流;401 未登录;403 签名无效/非本人/票据记录与 token 不一致;

410 票据过期或不存在;404 文件不存在;413 超限

  • 必带 Accept-Ranges: bytes;Content-Disposition: attachment; filename*=UTF-8''(中文名已编码)
  • Range: bytes=0-9 → 206 + Content-Range;越界 → 416 + Content-Range: bytes */
  • token:{ v, tid, sid, seq, index, uid } 的 base64url + HMAC-SHA256,secret 与

session cookie 同源,校验用 timingSafeEqual。

  • 载荷不含 `exp`:签发时还不知道激活时刻,写死 exp 与「首下起 5h」直接矛盾;

时效一律查服务端状态。

  • 载荷保留 sid/seq/index/uid 是为了验签后不查库就能做前置校验,但**不作为最终

授权依据**(最终以状态库 + 登录态为准)。

  • tid(16 hex 随机)是状态库的键,与 token 分离:日志里可以安全地记 tid 排查,

但 tid 本身不能用于下载(仍需签名 token)。

  • 路径安全:文件路径与会话 cwd 双向 `realpath` 后判归属,不用字符串前缀

——工作区内可能存在指向外部的符号链接(模型自己就能建),前缀判断会把 /cwd/link -> /etc/passwd 误判为「在工作区内」。另校验必须是普通文件。

  • 状态文件:$DSH_HOME/data/dsh-postapi-bridge/file-tickets.json(与 access-log 同目录)
{
    "v": 1,
    "tickets": {
      "a1b2c3d4e5f60718": {
        "sid": "sess-abc", "seq": 7, "index": 0, "uid": "alice",
        "issuedAt": 1730000000000, "activatedAt": null
      }
    }
  }
  • load() 惰性读(mtime 变化时重读,支持多实例共享)
  • 落盘 writeFileSync(tmp) + renameSync 原子替换;写失败只记日志,不抛错
  • cleanup(now):插件启动时一次 + 每次签发时一次(惰性,无需定时器)。

只删过期票据记录,绝不触碰源文件。

  • 并发激活:Node 单线程 + 该段全程无 await,因此「判空 → 赋值 → 落盘」在进程内

天然原子,保证只允许一次激活、后到者沿用已存在的值。这是对运行时模型的显式依赖, 已在代码注释中标明:若将来改成异步落盘,必须重新引入互斥。

  • 为什么「激活」发生在登录鉴权之前(校验顺序第 5 步 < 第 7 步):激活是票据自身的

生命周期事件,与「谁在用」无关。若把激活挪到鉴权之后,未登录探测就能反复刷新 48h 计时 —— 攻击者在第 47h59m 匿名探一下就能把等待期再续下去,票据永远不过期。 现在「第一次被访问即激活」把票据生命周期锁死在首次访问那一刻,这是正确性保证: 泄露出去的票被别人先激活,原主仍能在同一 5h 窗口内使用(窗口共享,不额外损失), 但它绝不会因此获得额外的 48h 延期。

  • 审计:file-ticket-issued(tid/sid/seq/index/uid/filename/size)、

file-ticket-activated(tid/uid/issuedAt/activatedAt/sessionDeadline)、 file-download(落 finish/close,记 bytes/range/activatedAt)、 file-download-denied(带 reason)。全程不记 token 本体;审计里的 subPath 对 /file/ 脱敏为 /file/。

  • 依赖:读取 present 事件走官方公开服务 ctx.sessionQuery.readEvent

(与官方 present-open 路由同一通道,支持冷读持久化会话)。宿主未装配该服务时 端点回 503(fail-closed)。

配置项(新增,均有默认值;非正数一律回退默认):

键 · 默认 · 说明 downloadActivateWindowHours · 48 · 票据「待激活」上限,自 issuedAt 起算 downloadSessionHours · 5 · 首次下载后的可用窗口,自 activatedAt 起算 downloadMaxMB · 200 · 单文件下载上限,超过则签发阶段即 413

  • 版本号:package.json 与 AUDIT_VERSION 同步为 0.9.0。

版本记录 v1.5.0(交付物「直链下载」+ 聊天流内可点击卡片)

用户需求(原话)

这个在聊天框里发的是 dsh 自带的那个我打不开,你不能做一个卡片然后卡片点击就是直接的下载地址吗
c 然后加上下载地址时效就是我想要的功能

痛点:DSH 官方交付物文件卡片在用户的手机 WebView 里点了没反应,用户拿不到文件。 方案 C:插件自己渲染一张卡片,卡片本身就是直接的下载链接(``), 点击即下载,不经过任何 POST 签票。

① 新端点 GET /dl/ —— 直链下载

payload = base64url({ v: 2, sid, seq, index, uid, exp })
token   = payload + '.' + base64url(HMAC-SHA256(secret, payload))

与既有 file-ticket(语义一个字节都没改)的分工:

· file-ticket(保留不动) · dl/(本版新增) 有效期 · 48h 待激活 + 首下起 5h 窗口 · 纯时间窗,签发即开始计时 激活 · 首次下载激活 · 不激活 次数 · 不限 · 不限 服务端状态 · 有(file-tickets.json) · 无(exp 在 token 里) 用途 · 复杂分享 · 卡片直链,点击即下

  • 有效期配置项 `dlTicketHours`,默认 48。
  • 鉴权:需要会话 cookie(人类通道,与 /files 一致);token 里的 `uid` 必须等于当前会话用户。

无 cookie → 401;uid 不符 → 403;签名错 → 403;过期 → `410`(文案明确说明过期);文件已删除 → 404。

  • 响应:文件流 + Content-Disposition: attachment; filename*=UTF-8''…(RFC5987)+ 支持 Range(复用 parseRangeHeader)。
为什么 dl 票把 `exp` 写进 token,而 file-ticket 不写:
file-ticket 的窗口从「第一次下载」起算,签发时无法预知激活时刻,只能存服务端状态;