Bundle
dsh-plugin-oss-backup
DeepSeek Harness 插件:把工作区文件备份到 S3 兼容对象存储(RustFS / 阿里云 OSS / AWS S3)。AK/SK 只从环境变量读取,绝不落盘、绝不进日志。
- Source
- KAIROSLLL
- License
- MIT
- Updated
- Updated 8 days ago
Readme
# dsh-plugin-oss-backup
把文件备份到**对象存储**的 DeepSeek Harness 插件。支持 RustFS / MinIO 等自建 S3 兼容服务、
阿里云 OSS、AWS S3 —— 一次配置,之后在设置界面点几下就能把整个文件夹传上云。
---
## ⚠️ 安全须知(先读这一段)
1. **优先用密钥管理器把凭证注入环境变量。** 1Password CLI(`op run -- dsh web`)、
HashiCorp Vault、AWS Secrets Manager、Azure Key Vault、Doppler、direnv 都行 ——
插件只读 `process.env`,所以**让密钥留在它该待的地方**,
远比复制粘贴到某个文件里安全。具体接法见 **[凭证接入指南](docs/credential-sources.md)**。
2. **不要用主账号密钥。** 请新建一个**只能读写指定桶**的子账号,只给它 `PutObject` 权限。
本文「[第 2 步](#第-2-步创建最小权限子账号必做)」有现成的配置可抄。
3. **别把密钥写进会被提交的文件** —— README、issue、脚本、截图、Dockerfile 都算。
本插件**支持 `.env`**(启动时自动读、**只读不写**),但它只适合本地临时使用;
用之前**务必确认 `.env` 在 `.gitignore` 里**(本仓库已挡 `.env` / `.env.*` / `*.key` / `*.pem`)。
4. **密钥一旦出现在聊天记录、日志或提交历史里,就当它已经泄漏** —— 立刻去服务商后台轮换掉。
5. **本插件自己不会把密钥写到磁盘上。** 密钥只在内存里流转,界面永远只显示"已设置 / 未设置",
日志里也只会出现变量**名**,不会出现变量值。(`.env` 是**你**写的、插件只读 —— 落不落盘由你决定。)
6. 想确认这一点,可以在配好之后自己检查:`~/.dsh/oss-backup/config.json` 里
**只会有开关、目录、桶名这类非机密信息,不会有任何密钥**。
---
## 一、它能做什么
### 四个用法
| 用法 | 说明 |
| --- | --- |
| **手动备份整个文件夹** | 在设置界面填一个目录,点「立即备份」,它就会把该目录(含所有子目录)递归传上去 |
| **定期全量备份** | 按时间表(每 N 分钟 / 小时 / 天,或每天、每月固定时刻)自动把整个目录完整传一遍 |
| **文件改动自动备份** | 打开开关后,每次保存文件(模型用 `write` / `edit` 改文件)都会在 2 秒后自动上传 |
| **让模型上传单个文件** | 模型可以直接调用 `oss_backup` 工具,把一个本地文件传到指定的对象路径 |
| **《接数据》**(趣味) | 点「立即备份」时先在「上传状态」框里玩一局节奏小游戏,通关才真的开始备份。不想要可以在设置里关掉 |
### 支持的对象存储
| provider | 适用 | 寻址方式 |
| --- | --- | --- |
| `rustfs`(默认) | RustFS、MinIO 等**自建** S3 兼容服务 | path-style |
| `aliyun` | 阿里云 OSS | 虚拟主机风格 |
| `aws` | AWS S3(也可指向其它 S3 兼容服务) | 虚拟主机风格 |
> 已实测通过:**RustFS 真实上传**(本机)、**阿里云连通性**。AWS 与阿里云的完整上传流程
> 依赖你自己的账号环境,仓库里的配置示例可直接照抄。
### 界面上有什么
- **设置 → 对象存储备份**(与主面板内容一致):
- 顶部两张卡片:存储端点、环境状态(是否配好、密钥是否已设置);
- **上传状态**:进度条 + 最近几次上传的大小;;
- **云端备份**按钮:打开当前 provider 的网页控制台,方便核对桶里的内容;
- **设置**:自动备份开关、备份目录、「跳过依赖 / 缓存目录」开关,以及折叠的「高级设置」;
- **凭证**:在界面里临时填写密钥(见[第四节](#凭证在设置界面里临时填写密钥));
- **手动备份**:「立即备份」按钮 + 进度条 + 上次结果;
- **环境自检**:provider / 端点 / 桶 / 区域 / 密钥是否已设置;
- **最近上传**:时间、对象路径、大小。
- **左侧栏**:一栏状态提示(`已就绪` / `上传中` / `缺配置` / `未连接`)。
---
## 二、安装
```bash
cd <你的插件目录>
pnpm add dsh-plugin-oss-backup # 或把打好的 .tgz 装进 DSH profile
```
装完后**重启 `dsh web`**,设置页里就会出现「对象存储备份」。
<details>
<summary>本地开发时安装的坑(点开看)</summary>
**不要用 `link:` 安装本地包。** 症状是启动时报 `dsh-plugin-oss-backup: failed to import`:
`link:` 会在 profile 里建一个指向源码目录的软链接,而 Node 解析模块时会先取真实路径,
于是解析起点跳出了 `~/.dsh/profiles` 目录树,插件依赖的宿主模块就找不到了。
正确做法是**先打包再安装**:
```bash
pnpm pack # 产出 dsh-plugin-oss-backup-x.y.z.tgz
pnpm add file:<绝对路径>/dsh-plugin-oss-backup-x.y.z.tgz
```
</details>
---
## 三、配置(照着做就行)
### 第 1 步:决定用哪个 provider
选一个就好:
```bash
OSS_PROVIDER=rustfs # 默认值,自建服务(RustFS / MinIO)
OSS_PROVIDER=aliyun # 阿里云 OSS
OSS_PROVIDER=aws # AWS S3
```
不设置 `OSS_PROVIDER` 时按 `rustfs` 处理。
### 第 2 步:创建最小权限子账号(必做)
**不要跳过这一步。** 用主账号密钥意味着一旦泄漏,整个云账号都危险。
**阿里云 OSS(RAM 子账号)**
1. 控制台 → **RAM 访问控制** → 用户 → 创建用户,勾选"**OpenAPI 调用访问**"
(只拿 AccessKey,不要开控制台登录);
2. 创建**自定义权限策略**(不要用 `AliyunOSSFullAccess` 这类系统策略),把 `dsh-backup`
换成你自己的桶名:
```json
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:PutObject", "oss:GetObject", "oss:HeadObject"],
"Resource": ["acs:oss:*:*:dsh-backup/*"]
},
{
"Effect": "Allow",
"Action": ["oss:ListObjects", "oss:GetBucketInfo"],
"Resource": ["acs:oss:*:*:dsh-backup"]
}
]
}
```
3. 把这个策略授权给刚建的用户;
4. 记下它的 AccessKey ID 与 Secret,下一步要用。
**AWS S3(IAM 用户)**
把上面的策略换成 IAM 语法即可(`Resource` 形如 `arn:aws:s3:::dsh-backup/*`):
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowBackupObjectsOnly",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::dsh-backup/*"]
},
{
"Sid": "AllowListOwnBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": ["arn:aws:s3:::dsh-backup"]
}
]
}
```
**RustFS / MinIO**
```bash
mc admin user add <alias> dsh-backup-bot '<一个强随机密码>'
mc admin policy create <alias> dsh-backup-rw ./dsh-backup-rw.json # 内容即上面的通用策略
mc admin policy attach <alias> dsh-backup-rw --user dsh-backup-bot
```
### 第 3 步:设置环境变量
挑你自己那一档,把值换成上面拿到的密钥。
> **强烈建议优先用密钥管理器接入。** 下面的 `setx` 示例只是"最不依赖任何工具"的写法;
> 更好的做法是让 1Password / Vault / AWS Secrets Manager 这类工具在启动 `dsh web` 时
> 把凭证注入环境变量 —— 密钥就不用出现在 shell 历史、脚本或任何磁盘文件里。
> **完整的接入清单(1Password / Bitwarden / Vault / AWS / Azure / Doppler / direnv /
> Docker / systemd)见 [凭证接入指南](docs/credential-sources.md)。**
**先说清一件事:值从哪来都行。** 插件只关心"这个键在你的进程环境里存不存在",
不关心它怎么来的。下面这些**完全等价**,用你顺手的那个:
| 来源 | 怎么做 |
| --- | --- |
| **密钥管理器**(**推荐**) | `op run -- dsh web`(1Password)、`doppler run -- dsh web`、Vault / AWS Secrets Manager 等 —— 见[接入指南](docs/credential-sources.md) |
| **系统环境变量** | Windows `setx`、macOS / Linux `export`(下面就是这一种) |
| **`.env` 文件** | 复制 `.env.example` 改成 `.env`,放到 `<DSH home>/oss-backup/.env` 或当前工作目录 —— 插件启动时**自动读**(只适合本地临时用,务必确认它被 gitignore) |
| **Docker** | `docker run -e RUSTFS_ACCESS_KEY=...` |
| **CI** | GitHub Actions Secrets / GitLab CI Variables 之类 |
**`.env` 的几条规则**(只读,插件**永不写入**):
- **优先级:`.env` > 系统环境变量**。`.env` 是你显式写下来的那份,比机器上残留的
旧 `export` 更权威 —— 否则"我在 `.env` 里改了却毫无效果"会非常难查。
被覆盖的键名会进日志与界面(**只有名字,没有值**);
- 只采用**第一个能读到的**文件(先看 `<DSH home>/oss-backup/.env`,再看当前工作目录),
不做多层合并 —— 否则出问题时没法解释某个值来自哪份文件;
- 解析规则就那几条:`KEY=value`、`KEY="value"`、`export KEY=value`、`#` 注释。
**不支持**行内注释、多行值、变量展开 —— 支持它们只会让"值被悄悄改掉"成为可能的解释;
- 改了 `.env` 要**重启 `dsh web`** 才生效(和系统环境变量一样)。
**① RustFS / MinIO**
```powershell
setx OSS_PROVIDER "rustfs"
setx RUSTFS_ENDPOINT "http://127.0.0.1:9000"
setx RUSTFS_BUCKET "dsh-backup"
setx RUSTFS_ACCESS_KEY "<子账号 AK>"
setx RUSTFS_SECRET_KEY "<子账号 SK>"
```
**② 阿里云 OSS**
```powershell
setx OSS_PROVIDER "aliyun"
setx OSS_BUCKET "dsh-backup"
setx OSS_REGION "oss-cn-hangzhou" # 默认就是这个
# OSS_ENDPOINT 可以留空:会自动拼成 https://oss-cn-hangzhou.aliyuncs.com
setx OSS_ACCESS_KEY_ID "<RAM 子账号 AccessKey ID>"
setx OSS_ACCESS_KEY_SECRET "<RAM 子账号 AccessKey Secret>"
```
> 如果你显式设 `OSS_ENDPOINT`,**一定要带上 `https://`**。不带协议头时会被当成
> `http://` 处理,连接必然失败。
**③ AWS S3**
```powershell
setx OSS_PROVIDER "aws"
setx AWS_BUCKET "my-backup-bucket"
setx AWS_REGION "us-east-1" # 必填
setx AWS_ACCESS_KEY_ID "<IAM 用户 AccessKey ID>"
setx AWS_SECRET_ACCESS_KEY "<IAM 用户 Secret Access Key>"
# AWS_ENDPOINT 一般留空,表示用 AWS 官方端点
```
> **macOS / Linux** 把 `setx X "v"` 换成 `export X="v"`,并写进你的 shell 配置文件。
### 第 4 步:重启 `dsh web`
**这一步不能省。** 环境变量是进程启动时读取的,正在运行的 `dsh web` 不会自动感知新变量。
### 第 5 步:确认配置成功
重启后打开 **设置 → 对象存储备份**,看「**环境自检**」:
- 状态应显示「**就绪**」;
- `provider` 一行会写明当前用的是哪一档(例如 `RustFS / MinIO 等自建 S3 兼容端点 · path-style`);
- 密钥两行应显示「**已设置(值不显示)**」。
**⚠️ 「就绪」不只看变量配没配齐**:插件会真的对存储桶发一次 `HeadBucket` 去确认服务能连上
(结果缓存 30 秒)。所以还有第三种状态:
| 状态 | 含义 | 怎么办 |
| --- | --- | --- |
| **就绪** | 变量齐了,服务也连得上 | 可以直接用 |
| **变量齐了,但服务连不上** | 端点/桶/密钥有一处不对,或者服务根本没启动 | 看下方红字里的具体原因 |
| **缺少 N 个变量** | 环境变量没配齐 | 那一行会列出缺哪个变量名 |
「服务连不上」这一条是专门为下面这种情况加的:**变量都填好了、但 RustFS 没开** ——
以前会显示成"就绪",看着一切正常,实际上每次上传都在连接被拒。
> **小贴士:`setx` 设了但插件说"未配置"?**
> `setx` 写入的是用户级环境变量,而已启动的进程(尤其是从快捷方式、启动器拉起来的
> `dsh web`)**不一定继承得到它**。所以插件每次启动都会主动去读一遍系统里的环境变量
> (先看进程已有的,再看用户级,最后看系统级),缺什么补什么 —— **不会覆盖你已经设好的值**。
> 界面上会标注「当前来自系统环境变量(启动时自动补读:…)」,看到它就说明补读成功了。
> 你只需要保证**设过值 + 重启过一次**。
---
## 四、使用
### 手动备份一个文件夹
1. 打开 **设置 → 对象存储备份 → 设置**;
2. 在「**备份目录**」里填要备份的目录(例如 `D:\projects\my-project`);
3. 点「**保存**」;
4. 回到「**手动备份**」块,点「**立即备份**」。
它会**递归备份整个文件夹**,完成后显示:共多少个文件 → 成功几个 / 失败几个 · 总大小。
**没变化的文件会自动跳过。** 插件在 `~/.dsh/oss-backup/uploaded.json` 里记着每个对象键对应的
`(文件大小, 修改时间)`,下次备份时一致的直接跳过 —— 所以重复点「立即备份」只会传真正改过的
那几个,不会再把你几千个没动过的文件重传一遍。界面会显示「跳过 N 个未变化的」。
> 判定依据是**大小 + 修改时间**。改了内容一般也就会改修改时间,所以会被正确重传;
> 只有"内容变了但大小和修改时间都没变"这种刻意为之的情况才会漏判。
> 换机器或删掉那个索引文件也不影响正确性 —— 只是会重传一次,之后恢复正常。
上传后的对象路径规则是 **前缀 + 被备份目录自己的名字 + 相对路径**。例如备份
`D:\projects\projects\yy`(里面是 `11`、`22` 两个子文件夹),桶里会出现:
```
workspace-backup/yy/11/...
workspace-backup/yy/22/...
```
**`yy` 这一层会保留**,这样在桶里一眼就能看出这批文件属于哪个目录。
> **会自动跳过的目录**:`node_modules`、`.git`、`.hg`、`.svn`、`.pnpm-store`、`.npm-cache`、
> `__pycache__`、`.venv`。这些要么体积巨大,要么是可再生的产物。
> 想让备份"一个文件都不少",把设置里的「**跳过依赖 / 缓存目录**」开关关掉即可。
>
> **限额**:最多 2000 个文件、单个文件最大 5 GB、总量 50 GB。触发上限时界面会提示
> 「已触到文件数上限」。
>
> **超过 8 MB 的文件走分片上传**(`@aws-sdk/lib-storage`,每片 8 MB、4 片并发),
> 内存占用恒定、与文件大小无关 —— 所以单文件上限敢放到 5 GB(S3 单次 PUT 的上限;
> 分片协议本身允许到 5 TB)。这几个数只是**防呆**,用来拦住"误选了整个磁盘"。
>
> **误选了也没关系:备份途中可以点「强行打断」。** 它会立刻停下 —— 正在传的那个文件
> 被掐断(在途的分片请求一并中止),已经传完的不受影响,也不会被记成"失败"。
> 下次备份会从没传完的地方继续(查重会跳过已成功的那部分)。
### 《接数据》(备份前的小游戏)
点「**立即备份**」时,如果游戏开关是开着的,会先在「**上传状态**」那一格里来一局节奏小游戏
《**接数据**》—— **通关了才真的开始备份**。
不想玩的话,把设置里的开关关掉,之后点按钮直接开始。
**怎么玩**
- 四条轨道,方块从右往左滑;**键盘 `D` `F` `J` `K`** 对应四条轨道(键位标在判定线左侧,一直在),
也可以**直接用鼠标点那条轨道**;
- 左侧有一条**判定线**,方块经过它的一瞬间点掉它;
- 判定分三级,**窗口挺严的**:
| 判定 | 条件 | 扣分 |
| --- | --- | --- |
| **PERFECT** | ±0.09 秒内点中 | **不扣分** |
| **GOOD** | ±0.175 秒内(不够准) | 扣 0.5 个 |
| **LOST** | 过了 0.175 秒都没点 | 扣 1 个 |
命中时判定线会亮起、对应轨道扫过一层光、判定处扩散一圈光环 —— PERFECT 是品牌色、GOOD 是灰色,
一眼就能看出自己准不准。
**四档难度**
| 难度 | 一局时长 | 特点 |
| --- | --- | --- |
| 杂鱼 | 约 22 秒 |
| 简单 | 约 29 秒 |
| 中等 | 约 36 秒 |
| **齁哦哦哦** | 约 45 秒 |
难度会记住,下次打开还是这一档。
**计分规则**
满分 100 分,**扣分按总方块数分摊**:
```
扣分 = (GOOD × 0.5 + LOST × 1) ÷ 总方块数 × 100
```
也就是 **PERFECT 完全不扣分**,GOOD 扣半个,LOST 扣一个。参考:
- 全部 PERFECT → 100 分;
- 全部 GOOD → 50 分(**这也会失败**);
- 全部 LOST → 0 分。
结果为:
- 扣分 **< 30** → 通过,**备份自动开始**;
- 扣分 **≥ 30** → 失败,**备份不会触发**
> 用"按总数分摊"而不是"每个失败固定扣几分",是为了让四档难度的容错率一致 ——
> 高难度方块多得多,固定扣分会让它直接变成不可能完成。
### 文件改动自动备份
在设置里把「**自动备份**」开关打开(点一下立即生效)。
之后每次模型保存文件,该文件会在**停止改动 2 秒后**自动上传;上传失败会自动重试 3 次
(间隔 1 秒 / 2 秒 / 4 秒),仍失败就只记一条日志,**不会影响你正常的写文件操作**。
开关关闭时完全不生效,这是默认状态。
**连不上时直接不提供上传**
- **确认连不上之后,所有上传入口一起停**:模型调 `oss_backup` 会被直接拒绝、
「立即备份」按钮变灰并说明原因、自动备份与定期备份都不再排期;
- **连接 / 凭证 / 桶名这类错误不重试** —— 它们不会因为多等 4 秒就变好,直接失败;
- **连续失败 3 次也会熔断**,暂停 60 秒,期间的文件改动直接跳过、不排队;
- 界面上写明原因(例如 `ECONNREFUSED`),**服务恢复后自动重新可用**,不用重启。
> 注意:这个开关监听的是**模型的文件写入操作**。如果你用命令行工具(例如 `git`、
> 脚本)直接改文件,不会触发自动备份 —— 那种情况请用手动备份。
### 定期全量备份
在设置界面的「**定期全量备份**」一行选模式(改完点「保存」生效),宿主会按时间表把**整个备份目录
完整传一遍** —— 与上面「文件改动自动备份」是**两套独立**的机制,可以只开一个,也可以都开。
| 模式 | 说明 | 例子 |
| --- | --- | --- |
| **关闭** | 默认,不排期 | — |
| **按间隔** | 每 N 分钟 / 小时 / 天 | 每 6 小时 |
| **每天** | 每天固定时刻 | 每天 03:00 |
| **每月** | 每月固定几号的固定时刻 | 每月 1 日 03:00 |
设置那一行下面会显示**下次实际触发时间**,方便确认排期对不对。
几个刻意的设计,避免你踩坑:
- **固定节拍**:「按间隔」对齐到**排定时刻**,不是"从现在往后加一段"。每 30 分钟就是
`:00` 和 `:30`,每 6 小时就是 `00/06/12/18`,「每 N 天」对齐到当地零点 ——
一次备份跑多久都不会把后续时间点往后推;
- 内部用的是"跑完再排下一次"的链式定时器(不是 `setInterval`),所以也不会堆叠出好几次备份;
- **上一次还在跑就跳过这一轮**(不排队),日志里会记一条"跳过";
- **每月只支持 1-28 号**:这样每个月都一定存在,不会遇到"2 月没有 30 号"那种要么跳过一个月、
要么被顺延到 3 月 1 号的情况;
- **还没设置备份目录时不会排期**,只记一条日志提醒你;
- 时间点**用的是本机时区**。
> 备份范围、排除规则(`node_modules` / `__pycache__` 等)和文件数 / 体积限额,
> 与「手动备份」完全一致 —— 它俩共用同一套逻辑。
### 凭证(在设置界面里临时填写密钥)
如果你不想用 `setx`,也可以在设置界面的「**凭证**」块里直接填 AK/SK,点
「**应用到本次进程**」:
- **填完立刻生效**,可以马上试一次手动备份;
- **只对本次运行有效** —— 重启 `dsh web` 后需要重新填;
- 提交后输入框会立即清空,界面上**永远不会回显**你填过的值;
- 想让配置永久有效、以后都不用填,还是用 `setx`(见第三节)。
### 高级设置
展开「**高级设置**」,可以改这些**非机密**项(会保存下来,重启后仍在):
| 项 | 说明 |
| --- | --- |
| 对象键前缀 | 桶里的目录层级,默认 `workspace-backup/` |
| Provider | 覆盖 `OSS_PROVIDER`(留空则用环境变量) |
| 端点 | 覆盖端点(留空则用环境变量或按区域自动推导) |
| 存储桶 | 覆盖桶名(留空则用环境变量) |
| 区域 | 覆盖区域(留空则用环境变量或默认值) |
> 优先级:**Provider 以设置页为准**(这是你在界面上明确选的),端点 / 桶 / 区域
> **以环境变量为准**(已经设过的不会被悄悄改掉)。
### 让模型上传单个文件
模型可以直接调用 `oss_backup` 工具,传一个本地文件和目标对象路径,例如:
> 把 `D:\backup\report.tar.gz` 上传到对象存储,`objectKey` 用 `backups/report.tar.gz`
这是**单文件**上传;要备份整个文件夹请用上面的「手动备份」。
### 云端备份按钮
「**云端备份**」按钮会打开**当前 provider 对应的网页控制台**,方便你在网页里核对桶里的内容。
地址由插件按你的配置推导,换 provider 之后按钮自己就指对了:
| provider | 打开哪里 |
| --- | --- |
| **rustfs**(自建 / MinIO) | 本机网页控制台 —— 由端点端口 **+1** 推出来(`http://127.0.0.1:9000` → `http://127.0.0.1:9001/`) |
| **aliyun** | 阿里云 OSS 控制台,**直达当前这个桶**的对象列表 |
| **aws** | AWS S3 控制台,**直达当前这个桶**(带上你配的 region) |
按钮旁边会显示完整地址并做成真链接 —— 万一浏览器拦下了新窗口,点链接照样能打开。
端点或桶还没配好时,这一块会直说"地址推导不出来",而不是给一个打不开的链接。
---
## 五、常见问题
**Q:环境自检显示「缺少 N 个变量」怎么办?**
那一行会列出**具体缺哪个变量名**。按第三节补上,然后重启 `dsh web`。
**Q:明明 `setx` 过了,为什么还说未配置?**
两个常见原因:① 设完**没有重启** `dsh web`;② 变量设到了别的账户下
(例如你在管理员终端里 `setx`,但 `dsh web` 跑在普通用户下)。
插件启动时会主动补读系统环境变量,所以只要"设过 + 重启过"就会生效。
**Q:上传失败,怎么排查?**
界面的「手动备份」块会显示失败条数,以及最多 5 条失败原因;「最近上传」里也有记录。
常见原因:密钥没有目标桶的写权限、桶名写错、端点地址不对、网络不通。
**Q:备份目录里文件太多,会不会传很久?**
会。手动备份是逐个文件上传的,且带上面说的限额。建议备份**项目目录**,
而不是整个磁盘根目录。
**Q:重启后设置还在吗?**
在。「开关 / 备份目录 / 前缀 / 跳过缓存 / provider / 端点 / 桶 / 区域」都保存在
`~/.dsh/oss-backup/config.json`,重启后自动恢复。
**只有密钥**在"界面临时填写"那种方式下需要重填 —— 用 `setx` 就没这个问题。
**Q:界面上会显示我的密钥吗?**
不会,任何时候都不会。界面只显示「已设置」或「未设置」。
**Q:备份上去的文件怎么恢复?**
直接用你对象存储服务商的控制台或命令行下载即可(对象路径见上面的规则)。
本插件只负责上传。
**Q:能同时配多个桶 / 多套账号吗?**
不能。插件一次只对一个存储目标生效,切换需要改环境变量或「高级设置」里的覆盖值。
---
## 六、安全说明
### 你的密钥是怎么被处理的
这个插件按四条原则设计,每一条都由**代码结构**保证,而不是靠"记得别打印":
| 原则 | 具体做法 |
| --- | --- |
| **代码里没有明文密钥** | 整个仓库里只有变量**名**(如 `RUSTFS_ACCESS_KEY`),没有任何真实值;`.env.example` 里全是空占位 |
| **日志和错误里不出现密钥** | 加载日志只报变量名;报错只说缺哪个变量;所有对外文本在出口还会再走一遍脱敏,抹掉密钥字面量以及签名形态(`X-Amz-Signature`、`Signature=`、`Credential=`、URL 里的用户名密码等) |
| **密钥不写入本地文件** | 插件不读 `.env`;`config.json` 只保存非机密项;界面上临时填写的密钥只存在于进程内存里 |
| **前端拿不到密钥** | 界面跑在浏览器沙箱里,**没有读取环境变量的能力**。它唯一的数据来源是一条只读的脱敏状态接口,密钥部分只有"是 / 否"。界面填写的密钥是**单向**送进宿主的,宿主永远不回传 |
### 你应该注意什么
-一定一定优先用密钥管理器接入!!!本插件不自带密钥管理器功能。
- **务必用最小权限子账号**,并且只授权你真正需要的那个桶;
- 不要在共享电脑、共享屏幕上操作「凭证」块,也不要在填完后让别人看你的浏览器;
- 定期在服务商后台**轮换密钥**;
- 如果怀疑泄漏,**先吊销再排查**,不要抱侥幸心理;
- 本插件只能保护"它自己不泄露密钥",**无法保护你把它贴到聊天窗口或提交进 Git**。
### 仓库自带的检查
如果你要参与开发或二次分发,仓库里有两道自动检查:
```bash
pnpm run check:secrets # 扫描仓库里有没有明文密钥(会与环境里真实的凭证值比对)
pnpm run check:load # 构建产物能否正常加载
```
CI(GitHub Actions)里还跑 **Gitleaks**,会扫描提交历史,一旦发现疑似密钥就让构建失败。
---
## 七、开发者信息
```bash
pnpm install
pnpm run build # 编译 host(tsc)+ 打包 client(esbuild)
pnpm test # 258 个单元测试
pnpm run typecheck:client
pnpm run check:secrets
pnpm run check:load
```
### 项目结构
```
src/
index.ts host 入口:注册工具 + 四条 HTTP 路由(状态 / 设置 / 凭证 / 手动备份)
env.ts provider 档案(rustfs / aliyun / aws)与环境变量校验
env-source.ts 启动时补读系统环境变量(Windows)
dotenv.ts 启动时补读 `.env`(**只读,永不写入**;优先级高于系统环境)
settings.ts 设置读写(只存非机密项)
auto-backup.ts 自动备份:防抖 + 指数退避重试 + 对象键净化
manual-backup.ts 批量备份:目录遍历、排除名单、限额、查重、逐个上传
upload-index.ts 已上传文件索引(只存大小与修改时间,用于跳过没变化的文件)
upload.ts S3 客户端构造 + PutObject
redact.ts 出口脱敏
client/ 界面(设置页 / 侧边栏 / 主面板)
assets/
code-uploading.jpg 上传中弹出的那张图
scripts/ 构建、真实上传冒烟、密钥自检、加载自检
tests/ vitest
```
### 发布
```bash
pnpm run build && pnpm test && pnpm run check:secrets && pnpm run check:load
npm pack --dry-run # 确认包里有 lib/、assets/、cordis.patch.yml、README.md、LICENSE
npm publish
```
### 设计上的两个注意点
- **`webServer` 必须是硬依赖**(`inject` 里列上)。用软取(`ctx.get`)时,`apply` 阶段该服务
可能还没就绪,会导致路由**静默不注册**,症状是设置页永远"未连接"。
- **client bundle 的模块 id 必须是包名**。`client-modules` 按最近归属的 `package.json`
判定浏览器模块 id,写错会报 `loaded without registering` 并让界面半边失效。
---
## License
MIT —— 见仓库根目录的 `LICENSE` 文件。
---
## 形象来源与许可
「接数据」小游戏与界面中出现的**鲸鱼娘等形象**取自 **@ZipZipPipe**,
基于 **@上善无形** 原创角色二创,依 **CC BY-NC-SA 4.0** 协议**非商业使用**。
这些形象**不在本项目的 MIT 许可范围内** —— MIT 只覆盖代码。
Install
dsh plugin --profile web add github:KAIROSLLL/dsh-plugin-oss-backup
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-plugin-oss-backup from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.