- Главная
- Плагины
- Безопасность и права доступа
- dsh-vaultwarden
dsh-vaultwarden
buvkb/dsh-vaultwarden
Reads and writes a self-hosted Bitwarden/Vaultwarden vault from any session: nine bitwarden_* tools, real-time WebSocket sync with a polling fallback, and a settings panel with readonly, ask and auto write modes.
Установка
dsh plugin --profile web add github:buvkb/dsh-vaultwardenREADME
dsh-vaultwarden — 让每个 DSH 会话实时同步 Vaultwarden 密码库
把自建 Vaultwarden / Bitwarden 接入 DeepSeek Harness:任何新建会话(无论 workspace)都会自动知道"密码可以去 Bitwarden 里读";服务器端任何增删改都会通过 WebSocket 实时反映到本地,无需手动刷新。
独立实现:直接对接 Bitwarden/Vaultwarden 服务端 REST 与 SignalR 协议,不依赖
bwCLI,除可选的hash-wasm(Argon2id KDF)外只用 Node 内置模块。 设计过程中参考了两个 MIT 项目,详见文末「参考与致谢」。
界面预览
| 条目列表 | 条目详情(浅色) | 条目详情(暗色) |
|---|---|---|
![]() | ![]() | ![]() |
截图中的条目为演示数据;界面颜色全部取自宿主的
--dsw-alias-*主题 token,因此浅色/暗色下都跟随主题。
装了什么
| 能力 | 说明 |
|---|---|
| 实时同步引擎 | 登录后订阅 /notifications/hub(SignalR over WebSocket),服务器变更即时推送 → 400ms 防抖 → /api/sync 重同步 + 解密换缓存(Vaultwarden 的同步接口没有增量参数,只有全量);WebSocket 不可用(ENABLE_WEBSOCKET=false、旧服务器、反代不放行)自动降级为轮询,并每 30–120 秒自动重试升级回 WebSocket;429 限流自动退避 |
| 9 个全局工具 | bitwarden_find(检索:名称/用户名/URL/备注/自定义字段;默认不含密码,也不含归档与回收站)、bitwarden_get(默认 field=all 只回密码/用户名/网址/备注/自定义字段;TOTP 验证码与银行卡/身份信息/安全笔记/SSH 私钥需点名 field=totp / card / identity / secureNote / sshKey 单独取)、bitwarden_status(配置/连通/解锁/同步模式/归档与回收站条数)、bitwarden_sync(强制同步)、bitwarden_folders(文件夹 id + 名字 + 条目数)、bitwarden_restore(从回收站恢复)、bitwarden_create/bitwarden_update/bitwarden_delete(写回,默认关闭) |
| 系统提示词章节 | 全局注入引导:需要任何账号、密码、API key、token 时先自查凭据库,而不是先问用户 |
| 配置表单 | 由宿主从插件 Config schema 派生(设置 → 插件 → bitwarden):12 个字段(服务器/邮箱/主密码/API 密钥/同步与缓存开关/权限档/会话天数),密钥字段为只写 |
| 条目浏览面板 | 设置 → 凭据库 独立页面:层级分区菜单(全部条目/收藏/验证码 + 类型分组 + 文件夹分组 + 未归类 + 归档/回收站,每行带画出来的 SVG 线稿图标与扁平计数药丸,切回刚看过的分区不重新加载)+ 搜索、列表、详情、复制、TOTP 圆环倒计时(弧随剩余时间缩短,色从绿经橙渐变到红)、reprompt 条目受保护;列表行显示服务端缓存的站点图标(取不到就退回首字母圆枕);验证码分区行 = 图标 + 名称 + 用户名 + 圆环 + 6 位码 + 复制;五类条目(登录/安全笔记/信用卡/身份/SSH 密钥)各自的详情字段、附件与密码历史折叠区;accessMode 非 readonly 时详情底部出现写操作(归档/取消归档、移入回收站、恢复、移动文件夹,逐个二次确认);同步徽标可点击手动同步,悬停显示模式/连接/间隔/上次同步/错误 |
| 认证通道 | 面板经 /api connection RPC 取数(vw/* 命名空间)——走操作者已认证会话,插件不自建 HTTP 路由 |
| 插件列表彩色图标 | 按宿主 artwork 规范提供包根 icon.svg(双层盾牌:紫 #6C4DF6 + 蓝 #2E6BE6,白色镂空钥匙孔);设置页「凭证据库」一行同用盾牌线稿,替换宿主默认齿轮 |
插件直连服务器 REST API(/identity/accounts/prelogin、/identity/connect/token、/api/sync、/api/ciphers*),
不依赖 bw CLI;除可选的 hash-wasm(Argon2id KDF)外全部使用 Node 内置模块。
配置
三种方式,优先级从高到低:用户设置(GUI/settings.yaml)→ 插件组合配置 → 环境变量 → 默认值。
1. DSH 设置界面(推荐)
设置 → 插件 → 插件配置 → Bitwarden / Vaultwarden 凭据库:主密码与 API 密钥是只写字段(保存后不回传浏览器,留空表示不修改)。
| 字段 | 说明 |
|---|---|
serverUrl | 服务器地址,如 https://vault.example.com 或 http://192.168.x.x(留空则插件待配置,不报错) |
email | 登录邮箱。必须与账号一致——它是 KDF 的盐值 |
masterPassword | 主密码(secret)。用于派生主密钥、解密保险库,只存本机 settings.yaml |
apiKeyClientId / apiKeyClientSecret | 可选。Bitwarden 网页端 → 账户设置 → 安全 → 密钥;填了走 API 密钥登录,可绕过两步验证 |
websocket | 启用 WebSocket 实时通知(默认开) |
pollIntervalSeconds | WebSocket 不可用时的兜底轮询间隔(默认 300 秒,最小 30)。WebSocket 正常时用不到,徽标可点击手动同步 |
cacheMinutes | 解锁后的内存缓存时长(默认 30 分钟) |
localCache | 本地密文缓存(默认开)。把上次同步的密文写到本机,下次启动先出列表、再用 13 字节的账号修订号问服务器变没变;关闭则每次启动都重新全量下载 |
deviceIdentifier | 可选设备标识。官方桌面/浏览器客户端会持久化稳定值;留空按「服务器+邮箱」确定性派生,不写盘 |
accessMode | 写权限:readonly(默认,一律拒绝写回)/ ask(每次写回需用户确认)/ auto(直接写回)。个人条目与组织条目都可写(创建组织条目传 organizationId + collectionIds) |
sessionDays | 登录会话保留天数(默认 30,0=每次重登)。按闲置计时:期间只要用过一次就自动续期,闲置超期才失效 |
repromptGrantMinutes | 「重新验证」条目在面板里验证主密码后,记住几分钟(默认 5)。与登录会话无关:它是一段纯内存的时间窗,插件重启或会话失效即作废,不会变成「每次重启都要重输密码」 |
2. 环境变量
DSH_BITWARDEN_SERVER、DSH_BITWARDEN_EMAIL、DSH_BITWARDEN_MASTER_PASSWORD、
DSH_BITWARDEN_CLIENT_ID、DSH_BITWARDEN_CLIENT_SECRET、DSH_BITWARDEN_DEVICE_ID(也接受去掉 DSH_ 前缀的写法)。
3. 组合配置
profile 的 cordis 配置里给插件传入同名字段即可。
用法(模型侧)
bitwarden_find { "query": "github" } → 条目列表(元信息,无密码)
bitwarden_find { "query": "旧", "includeTrashed": true, "includeArchived": true }
→ 连回收站与归档一起列(默认都不列)
bitwarden_get { "id": "…", "field": "password" } → 用户名 + 密码
bitwarden_get { "name": "GitHub 工作账号", "field": "totp" } → 当前动态码
bitwarden_get { "id": "…" } → field=all:密码/用户名/网址/备注/自定义字段
bitwarden_get { "id": "…", "field": "card" } → 卡号等,另外 identity / secureNote / sshKey 同理
bitwarden_status { "refresh": true } → 状态报告(含 liveSync / 条目/归档/回收站条数 / 最近一次会话事件)
bitwarden_sync { } → 强制重新同步
bitwarden_folders { } → 文件夹 id + 名字 + 条目数(folderId 从这里来)
bitwarden_create { "name": "新站点", "username": "…", "password": "…" } → 写回(需 accessMode=ask/auto)
bitwarden_create { "name": "新站点", "type": "card", "card": { "number": "…" } } → 信用卡/身份/安全笔记/SSH 密钥同理
bitwarden_update { "id": "…", "password": "新密码" } → 改密(需 accessMode=ask/auto)
bitwarden_update { "id": "…", "folderId": null } → 移出文件夹(不传=不动,传 id=移入)
bitwarden_update { "id": "…", "archived": true } → 归档 / false 取消归档
bitwarden_delete { "id": "…" } → 软删进回收站(需 accessMode=ask/auto)
bitwarden_delete { "id": "…", "permanent": true, "confirm": true } → 彻底删除(不可逆,必须带 confirm)
bitwarden_restore { "id": "…" } → 从回收站恢复(需 accessMode=ask/auto)
用法(人侧)
设置 → 凭据库(左栏新增入口):左边是分区菜单——全部条目 / 收藏 / 验证码三行,然后是「类型 (n)」(登录、安全笔记、银行卡、身份信息、SSH 密钥)与「文件夹 (n)」(各文件夹 + 未归类)两组,最后是「归档」「回收站」,每行右侧写明条数;点一行,右边就换成那个分区的条目,切回刚离开的分区直接复用刚才读到的快照,不再请求服务器(5 分钟内有效;搜索、刷新和写操作仍会重查)。桌面端左栏在右侧列表滚动时固定不动(position: sticky),窄屏(面板不足 480px)左栏移到列表上方、跟着内容一起滚动。
右边是搜索框(/ 聚焦、Esc 清空、↑↓ 选择)+ 条目列表 + 条目详情:列表行显示站点图标(服务端已缓存,取不到就用首字母圆枕)、名称、用户名·URI、收藏星与 TOTP 徽标(归档条目带「已归档」徽标,回收站条目名称划掉并带「回收站」徽标);点开某条,列表让位给详情(左上方「‹ 返回列表」回去),详情里密码默认掩码可切换、每行有复制按钮、TOTP 是圆环倒计时,另有备注/自定义字段/目录,附件与密码历史各自折叠,按类型显示卡片/身份/SSH 密钥字段。Bitwarden 里开了「重新验证」的条目不会自动出明文:详情里是一句说明 + 主密码输入框,验证一次后这段时间内(默认 5 分钟)可以正常读取该条目的任何字段。银行卡 / 身份信息 / SSH 私钥 / 安全笔记正文不再随详情一次带出,改为「含银行卡 · 读取这一项」这样的一行,点一下才单独取那一个字段。
进入「验证码」分区时,每一行就是 App 里的样子:图标 + 名称 + 用户名 + 倒计时圆环 + 分组的 6 位动态码(如 686 029)+ 复制按钮,点码只复制、不打开条目。
accessMode 为 ask/auto 时,详情底部多出「操作」区:归档 / 取消归档、移入回收站、恢复、移动到文件夹——每个操作都要再点一次「确认」,并在确认框里注明面板操作不受 accessMode 门禁保护,请谨慎操作(面板走的是已认证会话 RPC,没有逐次审批弹窗;模型工具的 ask 审批不受影响)。readonly 档下这些按钮不渲染,只显示一行说明。分区的计数与列表都由服务器侧收窄后再取(vw/overview + vw/list 的 section/sectionValue),所以侧栏上的数字和点进去看到的条目数永远一致,分页也只在该分区内翻。
对 Bitwarden/Vaultwarden 客户端约定的遵从
- 设备标识:按「服务器+邮箱」确定性派生(可配置覆盖),不像官方 CLI 那样每次登录新建设备。
- 通知类型:按官方
NotificationType枚举处理——LogOut(会话在别处被注销)会清除本地令牌与缓存,其余类型触发重同步。 - SignalR 协议:negotiate → WebSocket(
access_token走 query)→{"protocol":"json","version":1}握手 → type 6 keepalive 应答 → type 1 ReceiveMessage。 - reprompt:
reprompt: 1的条目不自动返回明文,且这道门覆盖该条目的每一个字段(备注与自定义字段同样算敏感)。打开它需要本人在面板里输入主密码;工具侧没有任何参数能绕开——过去那个confirm: true布尔标志已经删除,因为它等于给调用方(模型自己)一个自己打勾的复选框。验证一次开一个时间窗(默认 5 分钟,repromptGrantMinutes可调),窗内不用重复输入;窗是纯内存的,插件重启或会话失效即作废。 - 回收站语义:软删走
PUT /api/ciphers/{id}/delete(写deletedDate),彻底删除走DELETE /api/ciphers/{id};POST /delete在服务端是彻底删除,插件不再使用。回收站条目默认不列出,includeTrashed可看,bitwarden_restore恢复。 - 归档语义:
archivedDate存在即归档;搜索默认排除,includeArchived可看;写回不带该字段会被服务端解释为取消归档,所以插件原样带回。 - per-item key:更新复用条目原有的 64 字节密钥(只对本次变更的字段重新加密),创建时才新生成一把;
cipher.key用用户密钥或组织密钥包裹(类型 2 EncString)。这样改密不会连带抹掉通行密钥、密码历史、URI 匹配策略这些插件不认识的字段。 - 并发保护:写回带
lastKnownRevisionDate;服务端发现条目已被其他客户端改过时返回 400,插件回「条目已被其他客户端修改,请重新读取后再试」。 - 组织条目:创建走
POST /api/ciphers/create(带collectionIds),字段用组织密钥包裹;更新复用组织密钥与条目原 key。 - 限流:429 时按官方客户端的退避思路重试一次。
安全说明
- 主密码、用户密钥、解密后的条目只存在内存,不写盘、不打日志;插件只持久化用户填写的配置。
- 落盘缓存里放的是服务器原样返回的密文(
~/.dsh/data/dsh-vaultwarden/vault-cache.json.gz,gzip 压缩、权限0600),插件自己不解密也不落明文;解密只在内存里发生。 - 配置表单由宿主从插件
Configschema 派生(设置 → 插件 → bitwarden);面板数据走/apiconnection RPC(已认证会话),没有绕过鉴权的自建路由。 - 工具返回的明文凭据会进入会话上下文(这是"让模型能用密码"的前提)。提示词要求模型不要回显、不要写入文件。
- 写回默认关闭(
accessMode: readonly);ask档每次写入都走 DSH 审批确认,auto直接写入。个人条目与组织条目都支持;彻底删除必须显式带confirm: true,避免一次调用不可逆地抹掉条目。 - 落盘缓存带 HMAC-SHA256 签名(密钥
~/.dsh/data/dsh-vaultwarden/cache.key,0600):签名不符、账号不匹配或没有密钥的缓存一律丢弃重下,防止把别处的缓存文件当成自己的读进来。 - 与本地
dsh-vault插件无标识冲突(条目 id / 工具名 / 设置命名空间 / 设置页 id 均不同):本插件是dsh-vaultwarden↔bitwarden_*↔bitwarden↔vaultwarden,dsh-vault 是vault↔vault_*↔settings.vault↔vault。
排错
| 现象 | 处理 |
|---|---|
凭据库尚未配置完整 | 在设置卡片补 serverUrl + email + masterPassword(或 API 密钥) |
| 面板停在「尚未配置完成」、只有「重试」按钮 | 0.2.1 及更早的全新安装会如此(vw/config 被 sessionPersistence 拖垮)。升级到 0.2.2,或先在 设置 → 插件 → dsh-vaultwarden 的配置卡片里手填三项,面板随即恢复 |
登录失败:Username or password is incorrect | 核对 email(必须与登录账号一致)与主密码 |
该账户启用了两步验证 | 改用 API 密钥登录(client_id/client_secret) |
该账户使用 Argon2id KDF | 插件目录执行 npm install hash-wasm;或网页端把 KDF 改为 PBKDF2-SHA256 |
无法连接 … | 检查地址/端口/证书与网络可达性;自签证书需换成受信任证书 |
| 实时同步不生效 | bitwarden_status 看 liveSync.mode:polling 说明 WebSocket 没通(反代需放行 Upgrade/Connection,或服务器 ENABLE_WEBSOCKET=false) |
| 想让模型立刻重试 | bitwarden_status { "refresh": true } 或 bitwarden_sync |
兼容性
插件声明了四条 peer 依赖,全部标记为 optional(由宿主提供,npm 不会去装):
| peer | 范围 | 说明 |
|---|---|---|
@deepseek-ai/dsh-tools | ^0.1.0-0 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || ^0.2.1-0 | defineTool |
@deepseek-ai/dsh-typert-protocol | 同上 | Remote / TypertRemoteService |
@deepseek-ai/schemastery | ^3.18.0 || ^3.18.1-0 || ^3.18.5-0 | Config schema |
@deepseek-ai/cordis | >=4.0.1-rc.1 <5 || ^4.0.5-0 | 宿主内核(插件由宿主注入,不自行安装) |
支持范围:DSH 0.1.0 线到 0.2.x(含全部已发布的预发布构建),拒绝 0.0.1-rc.* 与 0.3.0 及以上。实际验证环境为 DSH 0.2.1-alpha.1 / @deepseek-ai/dsh-tools 0.2.1-alpha.1。
⚠️ 为什么范围要写成这样:npm 的预发布规则是「只有范围里某个比较符与该版本的
major.minor.patch元组完全一致、且自身带预发布标签时,该预发布版本才被放行」。而 DSH 发布到 npm 的构建全部带预发布标签,所以看似合理的>=0.1.0-rc.1 <0.3.0-0实际只会放行 31 个已发布dsh-tools构建中的 5 个——包括把用户自己的运行时挡在外面,npm install直接ERESOLVE。宿主的加载闸门用的是{ includePrerelease: true },因此这个问题在加载时不会暴露,只在 npm 安装路径上炸。
test/peer-ranges.test.mjs会拿npm view <pkg> versions的版本清单逐个核对四条范围:对旧范围报 13 处失败,对当前范围全绿。换 harness 版本后请同步更新该文件里的清单。
安装
dsh plugin --profile web add dsh-vaultwarden # npm(发布后)
dsh plugin --profile web add github:<owner>/dsh-vaultwarden#v0.6.1 # GitHub 源(首次需 allowBuilds)
dsh plugin --profile web add /absolute/path/to/dsh-vaultwarden # 本地路径(开发)
装新 bundle 后可能需要重启 dsh web 才会加载;前端半边在页面刷新后出现。
测试
bash scripts/build.sh # 链接 peer 依赖 + 语法检查 + 十三套离线测试(共 647 项)
| 套件 | 覆盖 |
|---|---|
test/peer-ranges.test.mjs(34) | peer 范围 vs npm 已发布构建;拒绝 0.0.1 线 / 0.3.0+ / 错误包名 |
test/mock-e2e.test.mjs(54) | 协议 / 加密 / 检索 / TOTP / 令牌刷新 / API key / 两步验证 / 分区计数与分区读取 |
test/kdf-units.test.mjs(13) | KDF 单位与已知向量:Argon2id 的 kdfMemory(MiB)必须换算成 hash-wasm 的 KiB、PBKDF2 迭代数被尊重、邮箱盐归一化;固定摘要同时锁住「正确单位」与「旧错误单位」,mock 与客户端同错时也能报警 |
test/live-sync.test.mjs(17) | WebSocket 握手 / 推送同步 / 防抖 / LogOut / 降级轮询 / 升级回退 |
test/mutations.test.mjs(38) | 写回增改删恢复 + per-item key 往返;五类条目(登录/安全笔记/信用卡/身份/SSH 密钥)的创建与更新、组织条目更新、软删/恢复/彻底删除的确认、旧版账号键条目的写回 |
test/write-back.test.mjs(48) | P0/P1 回归:软删端点与回收站可见性、回收站条目排在合并列表最前、写回字段保真(通行密钥 / 密码历史 / URI 匹配策略 / 附件 / 归档日期 / 原 key 不轮换)、并发 400 翻译、文件夹三态、归档开关、面板列表缓存 |
test/cache-store.test.mjs(69) | 密文落盘缓存的写入/权限/账号校验/7 天信任窗口 + 修订号探针快路径(探针失败退回全量、关开关不落文件) |
test/session-resilience.test.mjs(22) | 换令牌失败的分级:断网 / 429 / 5xx 保留会话与 refresh token 且不发密码授权,网络恢复后静默续上;只有服务器明确拒绝(invalid_grant)才清盘 |
test/host-entry.test.mjs(27) | Host 入口 apply() + Remote 网关线面(含 SRC 签名约束)+ 插件列表图标契约(icon.svg 资产、1024 画布、蓝底圆角块、白色镂空盾牌与键孔) |
test/gateway-flow.test.mjs(73) | 登录全链路:错密码 / 2FA 挑战 / 换码重试 / 会话持久化 / vw/boot 静默恢复与:aged token 静默换新、旧会话不被重登删掉 / 读参数透传(归档与回收站开关真正传到后端、回收站条目要 includeTrashed 才能读、分区参数 section/sectionValue 真正收窄池子、vw/overview 计数与分区池一致、vw/folders 返回解析后的对象)/ 写参数透传(归档、软删、恢复走通网关) |
test/access-mode.test.mjs(17) | readonly / ask / auto 三档权限 |
test/client-card.test.mjs(241) | 条目面板 + 层级分区导航(侧栏分区行与计数、分组标题、aria-current、进入分区带 section/sectionValue、切回命中快照零 RPC、老宿主无 vw/overview 时只剩「全部条目」一行、图标是画出来的 16×16 SVG 而非字体字符、计数药丸保留裸数字且当前行反色)+ 服务端图标(命中出 <img>、none 退回首字母圆枕、探测答回来时那一行自己重绘)+ 验证码分区行(3+3 分组动态码、圆环、复制)+ 写操作(归档/移入回收站/移动到文件夹与恢复的二次确认、readonly 档只读说明、回收站条目要 includeTrashed)+ 五类条目详情(卡片掩码、身份字段、SSH 密钥、附件与密码历史折叠区)+ 徽标手动同步 + 重开缓存 + localStorage 快照(重载先绘制、未登录即清)+ 旧宿主回退(vw/boot 按网关错误码识别;拒绝分页参数改整表读取并被记住;满 200 上限截断改诚实提示且不再重试)+ 分页游标校准(宿主窗口漂移不重复行、废弃列表的迟到页不回灌)+ 两处竞态回归(换条目后迟到的「确认读取」答复不得画进新条目、被放弃查询的迟到行不得覆盖新查询也不得写进快照)(react-test-renderer + RPC 桩) |
另有一套 test/cli-interop.mjs:与官方 bw CLI 做跨实现对照,未安装 bw 或 openssl 时自我跳过(退出码 0)。
mock 服务端(test/mock-server.mjs)按 Bitwarden 协议实现了服务端半边(PBKDF2/Argon2id、HKDF、AES-CBC+HMAC、per-item key、组织密钥、SignalR hub、两步验证),可选取代官方 bw CLI 做跨实现对照。它的写入分支按 Vaultwarden 1.37.3 的实际语义实现:PUT /ciphers/{id} 整条替换、POST /delete 彻底删、PUT /delete 软删、PUT /restore 恢复、附件不在请求里就不动——mock 与真实服务端语义不一致时,测试会把错误当成正确固化下来(0.3.2 那次 Argon2 单位故障就是这样发生的)。设计说明见 docs/ui-design.md。
登录与会话
为什么要输两次(密码 + 验证码)
Vaultwarden 的两步验证是两步:先验证主密码,再提交动态码。插件严格按这个顺序来——密码错误会停在表单并报错,只有密码验证通过后才进入验证码界面。
免密登录(推荐)
sessionDays 默认 30 天,且按闲置计时:登录一次后,令牌(含 refresh token 与派生主密钥)会加密保存在 ~/.dsh/data/dsh-vaultwarden/session.json(权限 0600,仅本人可读)。期间只要用过一次密码库,有效期就自动往后滑;闲置超过 30 天才失效。所以正常情况下,插件重启、DSH 重启都不需要你再输密码——除非真的 30 天没用过。
设为 0 可关闭持久化,每次都重新登录。
⚠️ 该文件等同于主密码的保护级别:能读到它就能解密保险库。这与插件已把
masterPassword存在 profile 配置里(为了能无人值守解锁)是同一权衡,官方客户端的「记住我 / PIN 解锁」也是同样取舍。
静默恢复:开面板不再要一遍密码
登录信息会加密落盘(~/.dsh/data/dsh-vaultwarden/session.json,0600,其中含 refresh token;access token 只有 1 小时寿命)。
插件开面板(以及每次启动预热)时先静默恢复:读盘 → access token 还在 → 直接用;
access token 过期了 → 用 refresh token 换一张新的。全程不会自动登录,所以不会弹两步验证。
弹登录表单只剩三种情况:盘上真的没有会话(首次配置、登出过、或闲置超过 sessionDays)、
refresh token 被服务器明确拒绝(换机改密等),或者磁盘上的会话文件损坏/版本不符。
0.2.5 之前这里有两个 bug:access token 1 小时一过,面板就以为你被登出了; 而「用同一套账号密码点验证并登录」会先把盘上会话删掉再登,逼着你重输密码+验证码。 0.3.1 之前还有第三个:换令牌时只要失败就删会话——断网、429 限流、5xx 都算, 于是一次网络抖动就丢掉整份会话,下次打开只能重输主密码+验证码。 现在只有服务器明确拒绝(
invalid_grant/ 401)才删;网络类失败原样保留,网络一恢复就静默续上。 若确实被拒绝,面板的状态接口会给出lastSessionLoss,说明是哪次、为什么丢的。
秒开:先绘制,再请求
开面板时面板先把上一次成功的列表画出来(内存缓存 + localStorage 快照,5 分钟内有效),
再向宿主要最新数据;快照只含列表本来就显示的内容(名称/类型/用户名/URI/目录/徽标),不含密码与动态码,
并按「服务器 + 邮箱」绑定——换了账号不会看到别人的条目。一旦宿主报告你已登出,快照立即清除。
宿主侧同样做了减法:开面板改为一次 boot RPC(配置 + 会话 + 是否恢复),列表与状态两个请求并行发出;
插件激活时还会在后台预热会话与解密缓存,重启 dsh 后第一次开面板也不用等全量同步。
实测(mock 服务端):一次登录 + 两次「重启」后开面板,0 次密码授权、0 次两步验证、0 次 refresh,列表 4–11 ms 返回。
列表也改为渐进加载:首屏一次只取 50 条(PAGE_SIZE),向下滚动自动续读下一页;只有滑得比加载快时,尾部才出现「还有 N 条,点击继续显示」按钮兜底。宿主不接受分页参数(descriptor 未声明 offset,网关直接拒绝)时自动回退为整表读取,上限 200 与旧版一致,且这次拒绝会被记住——下次打开直接整表读取。整表读取被上限截断时不再给出点了没有反应的按钮,而是就地提示「本机只同步到 200/总数 条,其余请在 Bitwarden 网页端查看」。续读位置以宿主报告的窗口为准:某页比请求的偏移晚到(上游新增使窗口漂移)时按宿主报告的端点续读,重复行去重后不会漏行。
密文落盘缓存:冷启动不再全量下载
0.3.0 起,每次成功同步后插件会把服务器原样返回的密文(gzip 后约为原体积的 1/3)写到 ~/.dsh/data/dsh-vaultwarden/vault-cache.json.gz(权限 0600,原子写入)。下次冷启动先读这份缓存立刻出列表,同时用 GET /api/accounts/revision-date 问一次服务器(响应仅 13 字节):
- 修订号没变 → 本次启动不再下载,省掉约 660 KB 的
/api/sync; - 修订号变了 → 后台全量同步一次并刷新缓存,界面先显示旧数据、随后无缝换成新的;
- 探针失败 / 缓存损坏 / 换了账号 → 自动退回全量下载,行为与旧版一致。
两条兜底:缓存超过 7 天未更新时,即使探针说没变也强制全量同步一次(防上游遗漏修订号更新的边缘路径);登出、切换账号、关掉 localCache 都会立即清掉缓存文件。
通行密钥(passkey / WebAuthn)不支持
Vaultwarden 的通行密钥是一种 2FA 方式,但它必须由浏览器调用 navigator.credentials 并配合认证器上的用户手势(指纹/面容/PIN)才能完成——这是 WebAuthn 防自动化的核心设计。插件运行在无头 Node 进程中,没有浏览器和认证器,物理上无法完成这个仪式,因此不支持,未来也不会支持。
替代方案:改用 API 密钥登录(apiKeyClientId = user.<uuid> + apiKeyClientSecret)。Vaultwarden 的 API 密钥直接绕过 2FA(服务端 user_api_key_login 不调用 twofactor_auth),配合上面的会话持久化,基本可以做到永久免密。
在面板的登录表单里把「登录方式」切到 API 密钥即可填写这两个字段(宿主的插件配置页同样可以填)。密钥在 Vaultwarden 网页端 设置 → 安全 → 密钥 获取。
注意:API 密钥只解决登录,不解决解密——保险库是用主密码派生的密钥加密的,所以
masterPassword仍然必填。API 密钥省掉的是验证码,不是主密码。
参考与致谢
本项目为独立实现,设计与实现过程中参考了以下两个 MIT 项目:
| 项目 | 本项目参考之处 |
|---|---|
| Jindom/dsh-bitwarden | DSH 凭据插件的最小骨架、系统提示词注入思路、mock 服务端测试方法论 |
| Ox0400/dsh-vault | --dsw-alias-* 主题 token 用法、设置页面板的视觉与交互基线 |
在此致谢。两者的 MIT 许可与版权声明见 THIRD-PARTY-NOTICES.md。
许可
MIT,详见 LICENSE。
Похожие плагины
dsh-infinite-gen-4
minglink/dsh-infinite-gen-4
cc-safety-net
kenryu42/cc-safety-net
api-relay-audit
toby-bridges/api-relay-audit
dsh-redteam-model
seaof0/dsh-redteam-model


