Bundle
dsh-openlist-sync
OpenList 文件同步插件(含设置面板):自动把 DSH 对话交付文件上传到 OpenList 挂载目录,并提供该目录的完整读写工具集。
- Source
- bg8lng
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-openlist-sync
> 给 DeepSeek Harness (DSH) 的 **OpenList 文件同步插件**:把各对话中 agent 交付的文件自动存入 OpenList 指定目录,并让 DSH 对**该目录拥有完整的读写能力**(列出 / 读取 / 写入 / 删除 / 重命名 / 移动,全部走 OpenList API)。
纯 Node ESM 实现,**零第三方运行时依赖**(只用内置模块 + `fetch`),全平台可用;兼容 Alist / Alist 系 / OpenList v3/v4 的 `/api/fs/*` 接口。
## 功能
### 1) 交付文件自动同步(核心)
- 监听 DSH 每个会话里的工具执行结果,一旦产生**交付文件**就自动上传到 OpenList 的 `targetDir`;
- 默认覆盖两类来源(对应界面上的「交付文件」):
- 聊天交付:`dsh_im_return_file` 返回给用户的文件 / 图片;
- 工具产物:`write` / `edit` / `str_replace_editor` 写出的文件;
- 目录组织:`flat`(平铺)/ `date`(默认,`targetDir/2026-09-03/文件名`)/ `date-session`(按日期+会话分组);
- 智能去重:同路径同内容未变 => 跳过;远端已有同名(默认不覆盖)=> 跳过并提示;可选 `dedupeByContent` 按 sha256 全局去重;
- 失败自动记录到 `state.json`,可用 `openlist_sync_retry` 重试;同步状态随时用 `openlist_sync_status` 查看;
- 上传全程串行队列,不阻塞 agent 的工具调用(fire-and-forget + 队列)。
### 2) 目录全读写工具集(DSH 可完全读写 targetDir)
| 工具 | 作用 |
|---|---|
| `openlist_health` | 自检:配置 + 连通性 + targetDir 可达性与写权限 |
| `openlist_config` | 查看生效配置(密码/令牌掩码) |
| `openlist_list` | 列出远端目录(表格渲染) |
| `openlist_stat` | 查看单个文件/目录元信息 |
| `openlist_read_file` | **读**:把远端文件内容读回对话(UTF-8 文本全文 / 二进制 base64 摘要) |
| `openlist_write_file` | **写**:上传本地文件(支持指定远端路径或按 layout 自动归位) |
| `openlist_write_text` | **写**:文本直接写成远端文件 |
| `openlist_mkdir` | 递归创建目录 |
| `openlist_remove` | 删除(目录递归删除;拒绝删除 targetDir 根;破坏性操作会先提示) |
| `openlist_rename` | 重命名 |
| `openlist_move` | 移动(跨目录自动建目录) |
| `openlist_sync_status` / `openlist_sync_retry` | 自动同步状态 / 重试失败 |
默认 `restrictToRoot: true`:目录工具只允许在 `targetDir` 内操作,防止越权/误删;确需全盘访问可关闭。
## 安装
```bash
# 1) 本地 tarball(推荐本仓库交付物)
dsh plugin --profile web add ./dsh-openlist-sync-0.1.0.tgz
# 2) 或 npm 发布后
# dsh plugin --profile web add dsh-openlist-sync
# 3) 或从 GitHub
# dsh plugin --profile web add github:你的账号/dsh-openlist-sync#<commit>
```
装完**重启 `dsh web`**。插件自带空配置,未配置时不会弄崩启动;任何 `openlist_*` 工具会返回配置提示。
## 配置
在 `$DSH_HOME/profiles/web/cordis.patch.yml`(即 `~/.dsh/profiles/web/cordis.patch.yml`)中追加/覆盖:
```yaml
- id: tool-openlist-sync
config:
baseUrl: "https://your-openlist.example.com" # OpenList 访问地址;挂在子路径则带子路径
username: admin
password: 你的密码 # 建议改用环境变量 DSH_OPENLIST_PASSWORD,见下
# token: "站点令牌" # 二选一:OpenList 后台 设置→其他→站点令牌,优先于账号密码
targetDir: "/DSH交付" # 交付文件自动存放目录(自动创建)
layout: "date" # flat | date(默认)| date-session
autoSync: true # 总开关
syncChatDeliveries: true # dsh_im_return_file 聊天交付
syncProducedFiles: true # write/edit/str_replace_editor 产物
overwrite: false # 远端同名是否覆盖
restrictToRoot: true # 目录工具是否锁定在 targetDir 内
```
### 环境变量(优先级最高,避免明文密码进 YAML)
| 变量 | 说明 |
|---|---|
| `DSH_OPENLIST_BASE_URL` | OpenList 地址 |
| `DSH_OPENLIST_USERNAME` | 用户名 |
| `DSH_OPENLIST_PASSWORD` | 密码(推荐) |
| `DSH_OPENLIST_TOKEN` | 站点令牌(替代账号密码) |
| `DSH_OPENLIST_TARGET_DIR` | 目标目录 |
### 完整配置项
| 配置 | 默认 | 说明 |
|---|---|---|
| `baseUrl` | 空 | OpenList 根地址(含子路径则到子路径) |
| `username` / `password` | 空 | 账号密码登录 |
| `token` | 空 | 站点令牌(优先) |
| `targetDir` | `/DSH交付` | 交付目录 |
| `layout` | `date` | `flat` / `date` / `date-session` |
| `autoSync` | `true` | 自动同步总开关 |
| `syncChatDeliveries` | `true` | 聊天交付文件(dsh_im_return_file) |
| `syncProducedFiles` | `true` | write/edit/str_replace_editor 产物 |
| `overwrite` | `false` | 同名覆盖 |
| `dedupeByContent` | `false` | 按 sha256 内容全局去重 |
| `includes` | `[]` | 文件名白名单(`*`/`?`),空=不限 |
| `excludes` | `[]` | 文件名黑名单 |
| `maxBytes` | 314572800 | 单文件上限(默认 300MB) |
| `restrictToRoot` | `true` | 目录工具锁在 targetDir |
| `dataDir` | `~/.dsh/dsh-openlist-sync` | state.json 存放处 |
| `timeoutMs` | 30000 | API 超时 |
| `producedTools` | 见下 | 工具名→路径参数映射(扩展) |
默认 producedTools:`{ write: file_path, edit: file_path, str_replace_editor: path, dsh_im_return_file: path }`。其它插件产出的文件可在配置里追加,例如:
```yaml
producedTools:
excel_task: outPath # dsh-excel-chat 任务输出也自动同步
run_code: outputPath # 假想示例
```
## Web 设置面板(DSH 设置 → OpenList 同步)
0.2.0 起插件注册进 DSH 设置面板(设置 → OpenList 同步 (dsh-openlist-sync)),无需手写 YAML 即可配置:OpenList 地址(baseUrl)、令牌或账号密码、挂载目录(targetDir)、存放规则(layout/overwrite/自动同步开关)、目录规则(restrictToRoot/包含排除/单文件上限),并带「测试连接」。保存后实时生效(下次工具调用即用新配置);秘密字段(密码/令牌)按 secret 存储不会出现在导出/诊断。
## 原理与边界
- **自动同步钩子**:订阅 DSH 工具服务的事件 `tools/result`,在工具成功执行后按 `producedTools` 映射取出文件路径入队上传。只跟踪**顶层工具调用**(与界面「交付文件」口径一致);`run_code` 内部的子调用不重复触发。
- **鉴权**:优先 `token`(站点令牌直接放 `Authorization` 请求头);否则 `POST /api/auth/login` 换 token,401 自动重登一次。
- **上传**:`PUT /api/fs/put`,请求头 `File-Path`(URL 编码的全路径)+ 原始字节流 + `X-File-Md5`;个别环境不允许 raw PUT 时自动回退 multipart `/api/fs/form`。
- **读取**:`GET /d/{path}` 直连下载,403 自动回退 `/p`,再回退 `stat` 的 `raw_url`(对开了 `sign_all` 的站点也可用)。
- **去重日志**:`dataDir/state.json` 记录 {本地路径→sha256/size/mtime/远端} 与失败列表;多会话共享一份,重启不重复上传。
- 相对路径解析:自动同步按「工具参数原样 → 会话工作目录」尽力解析;DSH 工具通常传绝对路径,最稳。
- 大文件读取为内存 Buffer(300MB 上限内可控);自动同步/上传失败不影响原会话,只记录并由 `openlist_sync_retry` 重试。
## 卸载
```bash
dsh plugin --profile web remove dsh-openlist-sync
# 并手动删除 ~/.dsh/profiles/web/cordis.patch.yml 里 tool-openlist-sync 行与 ~/.dsh/dsh-openlist-sync 状态目录
```
## 更新记录
### v0.2.7(2026-09-08)真正根因修复:日志调用丢 this 导致上传假失败
- **根因(经运行日志堆栈定位)**:DSH 运行时(cordis)的 `LoggerService` 级别方法是**原型方法,内部依赖 `this()`**(logger 实例本身可调用)。旧 `SyncEngine._log` 先把 `ctx.logger[level]` 取出存成局部变量再调用,导致 `this` 丢失,抛出 `TypeError: this is not a function`。该日志调用位于上传成功收尾处,于是每次上传“文件已落盘却被报失败”,并污染失败清单(0.2.6 已加收尾兜底与去重,但未触及此根因)。
- **修复**:`_log` 改为**内联调用**(`logger.warn?.(...)` 保留 `this`),任何日志异常只回退 `console`,绝不影响上传业务;增加 cordis 形态日志器的回归测试。
- 保留 0.2.6 的全部健壮性改进(收尾兜底判定 recovered、并发/重复派发去重、state 落盘尽力而为、失败带堆栈、工具不抛裸错误)。
### v0.2.6(2026-09-08)修复:上传成功却报“this is not a function”假失败
- **根因**:个别运行环境在文件已成功写入远端、进入状态收尾阶段时会抛出一个运行时错误;旧实现一律按“上传失败”记录并上报,导致 `openlist_write_file` / 自动同步明明把文件传上了 OpenList,会话却收到 `Error: this is not a function`,失败清单也被污染。
- **修复**:
- 上传收尾兜底:出错后先核对远端是否已存在同路径文件且大小一致 —— 一致即判定“已上传成功”并自动清理失败记录,不再产生假失败;
- 同文件并发去重:同一逻辑上传(插件重复加载 / 工具结果重复派发)并发出现时只真正上传一次,第二次复用结果,避免“远端已存在”类误报;
- `tools/result` 事件 1.2s 窗口去重,杜绝同一产物被重复派发上传;
- 状态落盘(state.json)改为尽力而为:多实例并发写 / 临时文件冲突不再向上传主流程抛错;
- 失败原因携带堆栈片段(`e.stack` 前 6 行),便于下次定位;
- `openlist_write_file` / `openlist_sync_retry` 内部异常统一转结构化结果返回,不再向会话抛裸错误。
- **测试**:新增 2 个回归用例 —— “文件已落盘但接口报错 → 判定 recovered 成功、不写失败记录”“同一文件并发上传只落盘一份”。
## 开发与测试
```bash
npm test # node --test,含 mock OpenList 服务器的离线测试
npm pack # 打 tgz 交付包
```
测试覆盖:路径规范化 / 工具注册 / health / list / 读写文本 / 上传本地文件 / dsh_im_return_file 自动同步与去重 / 目录守卫(越界与根目录保护)/ mkdir-rename-move-read-remove 闭环。
## License
MIT
Install
dsh plugin --profile web add github:bg8lng/dsh-openlist-sync#a731fec330ab20c079f7aeee75762013412d29e6
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-openlist-sync from the hub