# dsh-key-panel npm 上的 `@moruteaven/dsh-key-panel` · [English](./README.md) · **简体中文** · [日本語](./README.ja.md) 给 [DSH Desktop](https://deepseek.com) 用的密钥管家。把 API 密钥集中放在一处, 以 `$DSH_*` 环境变量的形式注入助手的 shell,然后由你在设置页的面板里决定—— 助手到底能拿它们做什么。 **它解决什么问题**:助手要调 Cloudflare、OpenAI,或者你的数据库,就得有凭据。 直接粘进对话,就永久留在了对话记录里;存成文件,最后又散落到某个你早已忘记的 目录。这个插件把它收进一个文件、暴露成一个环境变量,而值本身**从不进入对话**。 ``` 助手要部署 Worker → 执行:wrangler deploy --api-token $DSH_CLOUDFLARE_TOKEN → shell 里有,模型的上下文里没有 ``` --- ## 功能 - **设置页面板**。增删改查、显隐、复制,都在你管理其它设置的地方,不用改配置文件。 - **`$DSH_*` 注入**。每个密钥都是助手 shell 里的一个变量,改动下一条命令即生效,无需重启。 - **三档权限**,由你设定并持久保存:`只读`(默认)· `可写` · `可编辑`。 - **平台与账号**。多数服务商要两个值(账号 id + 令牌),而一个服务商常配多个账号。按平台和账号归组后,每个槽位就是 `DSH_<平台>_<账号>_ID` 和 `DSH_<平台>_<账号>_KEY`。归组只是**存储的组织方式**——变量名仍是扁平的,读它的东西也都不用变。 - **来源标记**。密钥记录是**你**建的还是**助手**建的,且写入无法互相"洗白"。 - **最近活动**。记录每条 shell 命令实际拿到了哪些变量,以及助手主动申报的目的。两者并排展示,从不配对。只记名字和时间戳——**任何值都不会写进去**。 - **名称作用域(暂未开放)**。宿主支持把助手限定在 `DSH_AGENT_*` 之类的名字下,存储里已设置的作用域也照常生效,但本版本面板上的入口是隐藏的。 - **两段式删除**。删除需要第二次带令牌的确认调用——单次误调用删不掉东西。 - **值永不进模型**。任何模式下,没有任何工具会把密钥值返回给模型。 - **幂等、可热重载**。面板每次调用都重新读取当前权限。 ## 安装 ``` 设置 → 插件 → 搜索 "dsh-key-panel" → 安装 → 重启 DSH Desktop ``` 之后面板出现在 **设置 → 密钥**。 其它渠道: - **npm** —— `npm i @moruteaven/dsh-key-panel`,然后在你的 profile 的 `package.json` 里把裸包名 `@moruteaven/dsh-key-panel` 加进 `dsh.profile.bundles`。这个列表**只接受裸包名**,`file:` 或路径会被拒绝(`file:` 放在 `dependencies` 里是合法的)。 - **从源码** —— 见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 ## 使用 ### 添加密钥 设置 → 密钥 → **添加密钥**。 | 字段 | 说明 | | --- | --- | | 名称 | 必须匹配 `DSH_[A-Z][A-Z0-9_]*`,例如 `DSH_CLOUDFLARE_TOKEN` | | 用途 | 可选。会作为该变量的说明展示给助手 | | 值 | 密钥本体。明文存储,保存后只以掩码显示 | 然后直接告诉助手用它: > 部署这个 worker,token 在 `$DSH_CLOUDFLARE_TOKEN` 里。 ### 平台与账号 当一个服务商需要两个值(比如账号 id 和令牌),而你在它下面有不止一个账号时,平铺的列表会变得难读。用 **添加平台** 和 **添加账号** 把这些密钥归成一类,变量名由插件替你拼: | 你填 | 得到 | | --- | --- | | 平台 `CF`、账号 `WORK` | `DSH_CF_WORK_ID` 与 `DSH_CF_WORK_KEY` | 提交前面板会先把两个名字显示出来,你能看到最终会落进 shell 的是什么。标识符只允许大写字母、数字和下划线;小写会被**拒绝**而不是悄悄转成大写——你没要求过的名字,比需要重打一次更糟。 **密钥就在账号行里填。** 账号建好之后,它的两个变量名已经定下来了——名字是从标识符**拼出来的**,不是你选的。所以账号那一行有一枚「**填入密钥**」按钮,点开是个弹出面板,一次收两个值,直接挂到这个账号下。弹出面板里会先把两个名字列出来,但**不需要你敲**——这才是重点:让你把一个插件算好的名字抄到顶部的「添加密钥」卡片、一个字段抄一次,那是誊写,不是输入。 每个槽位都显示自己有没有值,也可以先空着以后补。未分组的密钥仍然用「**添加密钥**」——它们没有账号可以填。 平台和账号都另有一个**显示名**,面板上显示的是它。它和标识符刻意分开:标识符会拼进变量名、之后不能改;显示名随时可改。如果你想让面板上写着「Cloudflare」而变量名保持 `DSH_CF_*`,这两个字段就是干这个的。 如果某个账号会生成的名字已经被占用——比如你在建账号之前就手工加过 `DSH_CF_WORK_KEY`——面板会先提示并列出涉及的密钥,等你确认。继续保存会覆盖它们原有的值,所以不会静默发生。 两点值得知道: - **归组是组织方式,不是安全边界。** 变量仍是普通的扁平 `DSH_*` 名字——这正是关键,因为 shell 没有嵌套。归组给你的是一个好读的面板,顺带还带来一个正好对齐到单个账号的作用域(见下)。 - **删平台或账号不会带走你的密钥。** 只要还有密钥挂在它下面,删除就会被拒绝,你得先清空或改挂。如果你选择强行继续,那些密钥会被**退回未分组**,值一概不动。 从没归过组的密钥保持原样。这个功能不要求你重组任何东西才有用,已有的存储也照常可用、无需迁移。 ### 最近活动 面板会保留两条记录,并把它们并排放在一起: | 记录 | 谁写的 | 内容 | | --- | --- | --- | | **助手申报** | 助手调用 `key_panel_intent` | 它自己说的时间戳、目的,以及打算用的名字 | | **注入命令** | 插件自己,每次 shell 命令解析环境时 | 时间戳,以及那次命令实际拿到的名字 | 两者是**并排展示,不是配对**。列表里上下相邻的一条申报和一次注入,可能属于同一个任务,但数据本身没有任何东西这么说——把它们连起来就是把猜测当成事实呈现。 **「注入命令」不意味着什么。** 宿主在每条 shell 命令执行前解析整个 `$DSH_*` 环境,看不到命令拿它做了什么。所以一行只说明这些名字**在那一刻在作用域里**——不表示命令读了它们,更不表示用它们做了什么。把这份记录当作「哪些流程依赖了哪些密钥」的信号,不要当作审计凭证。 由此有两点实际影响: - **助手可能什么都不申报。** `key_panel_intent` 是可选的,跳过它没有任何代价,所以会有未申报的活动——这是预期内的。插件只在便宜的地方询问意图,但从不强制:每条命令前面加一道必填步骤,很快会变成肌肉记忆,也就不再携带信息。 - **开销不在命令路径上。** 记录先在内存里缓冲、再批量落盘,所以记账不会拖慢任何命令。文件有上限(超出后丢弃最旧的),写失败被吞掉——这份文件丢了尾部,损失的是趋势,不是密钥。 记录与密钥库放在一起,文件名 `usage.jsonl`,**只保存名字和时间戳。任何值都不会写进去。** ### 权限档位 | 档位 | 助手可以 | 助手不可以 | | --- | --- | --- | | `只读`(默认) | 使用密钥 | 改任何东西——**面向模型的工具一个都没注册** | | `可写` | 新增;覆盖自己建的 | 删除任何东西;改你建的密钥 | | `可编辑` | 增、改、删 | ——(删除仍需二次确认) | 在面板里切换,**下一次工具调用即生效**,不用重启。 **从「只读」开始。** 只有当你确实想让助手自己添加凭据时,再往上放——后两档就是 为这一个场景存在的。 ### 按名称限定 > **本版本面板上暂未开放。** 作用域功能本身是完好的——校验、持久化、每次模型调用时的判定都在,存储里已设置的作用域也继续生效。隐藏的只是**设置它的那个输入框**(`lib/client.js` 里的 `SHOW_SCOPE_UI`)。需要时可以直接在存储文件里设置;把该标志打开即可恢复入口。 在选档位之前,有个安全后果值得先知道:**没有设置作用域时,限制助手能触到哪些密钥的只剩权限档位本身。** 作用域未设置就等于没有限制。想要更窄的边界,就自己去设一个——目前只能写存储文件。 作用域用于收窄助手能碰的名字: | 取值 | 效果 | | --- | --- | | *(留空)* | 不限制 | | `DSH_AGENT_*` | 只允许该前缀 | | `DSH_CF_*` | 只允许该平台的密钥 | | `DSH_CF_WORK_*` | 只允许某一个账号的凭据 | 只支持 `*` 这一个通配符,**表达不了路径,也表达不了正则**。 ## 配置项 | 设置 | 位置 | 默认 | | --- | --- | --- | | 权限档位 | 面板 | `readonly` | | 名称作用域 | 仅存储(面板已隐藏) | 不限制 | | 存储位置 | `$DSH_HOME/key-panel/keys.json` | `~/.dsh/key-panel/keys.json` | | 活动日志 | `$DSH_HOME/key-panel/usage.jsonl` | `~/.dsh/key-panel/usage.jsonl` | ## 存储格式 ```jsonc { "version": 2, "policy": { "accessMode": "readonly", "scopePattern": null }, "platforms": { "CF": { "label": "Cloudflare", "createdAt": 1758428400000 } }, "accounts": { "CF/WORK": { "platform": "CF", "identifier": "WORK", "label": "工作", "createdAt": 1758428400000 } }, "keys": { "DSH_CF_WORK_TOKEN": { "value": "…", "description": "Cloudflare Workers 部署令牌", "origin": "operator", // "operator" | "model" "platform": "CF", // 可选的归组字段 "account": "WORK", "field": "key", // "id" | "key" "createdAt": 1758428400000, "updatedAt": 1758428400000 } } } ``` 写入走 **临时文件 → `fsync` → rename**。少了 `fsync`,崩溃可能留下一个已改名却 内容为空的文件——读回来就是「所有密钥都没了」。 version 1 的文件能原样加载:归组字段全是可选的,平台概念出现之前写入的密钥读回来 就是「未分组」。**不需要迁移步骤。** ### `usage.jsonl` 活动记录放在**另一个文件**里,每行一个 JSON 对象: ```jsonc {"t":1758428400000,"kind":"intent","names":["DSH_CF_WORK_TOKEN"],"note":"deploy staging"} {"t":1758428450000,"kind":"use","names":["DSH_CF_WORK_TOKEN"]} ``` | 字段 | 含义 | | --- | --- | | `t` | epoch 毫秒 | | `kind` | `intent`(助手申报)或 `use`(注入命令) | | `names` | 涉及的名字,已去重。**永远不是值。** | | `note` | 仅 `intent`——目的,截断到 500 字符 | 批量追加,并保留最近 1000 条,所以不会无限增长。与 `keys.json` 不同,它**不是**原子写入,也**不**逐行 fsync:进程被强杀可能截断最后一行,读取时跳过即可。这个取舍在这里是对的——这份文件是信号,不是密钥,为了持久性拖慢每条命令不值得。 两种记录由不同的一方在不同时刻写入,因此分开存储,展示时按时间对齐。插件从不把它们合并。 ## 安全 **密钥以明文存储。** 这是刻意的取舍,完整说明(信任模型、不变量、非目标)见 [SECURITY.md](./SECURITY.md)。 简要版: - 任何以你的 OS 用户身份运行的程序都能读到存储文件。请当成 `.env` 对待。 - 任何模式下,助手都无法把密钥值读进对话记录。 - 助手永远改不了权限档位和作用域——那是操作者专属的。 - 「只读」档下写入工具**不存在**,而不是拒绝调用。 ## 开发 ```bash npm test # 两套一起跑 —— 427 条断言 npm run test:host # 宿主侧:权限、存储、工具、网关(298) npm run test:client # 客户端 bundle:契约、插槽、RPC、字典(129) ``` 客户端测试按真实前端的加载方式跑 `lib/client.js`:伪造 `window.__ModuleLoader__`、`require` 只允许种子模块,然后断言 bundle 求值时 **不产生副作用**、只引用种子模块,以及面板调用的每个 RPC 端点都与宿主网关对得上。 **没有构建步骤。** 宿主侧是纯 ESM,浏览器侧是一个 CJS factory 字符串。 ``` lib/ policy.js 权限档位、作用域 glob、决策函数 store.js 持久化、来源标记、原子写入 tools.js 面向模型的工具、删除确认台账 index.js 宿主侧 —— Typert 网关、shellEnv 注册 client.js 浏览器 bundle —— 设置面板 ``` 本仓库要求遵守的约定、以及怎么跑测试,见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 ## 兼容性 - DSH Desktop 2.0.11+(dsh `0.1.5-rc.1`) - Node 20+ - 客户端部分仅支持 web 平台 ## 许可 [Apache License 2.0](./LICENSE) · 署名信息见 [NOTICE](./NOTICE) 选 Apache-2.0 而不是 MIT/BSD,是因为它对**处理凭据**的工具多了两条有意义的条款: - **专利授权**(第 3 条)。MIT 和 BSD 完全没有这一条,而它同时要求贡献者做同样承诺。 - **商标限制**(第 6 条)。别人不能拿作者的名义为衍生分支背书——这正是 BSD-3-Clause 背书条款提供的同一层保护。 其余照旧宽松:可商用、可修改、可闭源分发。