# dsh-webfile

[English](README.md) | [中文](README.zh.md)

S3, FDS & FTP file tools for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): 8 agent tools (list, stat, mkdir, delete, move, copy, download, upload) — approval-gated mutations, transfer jobs with progress & cancel.

让 Agent 在对话中直接浏览与操作 S3(含 MinIO 等兼容对象存储)、小米 FDS 与 FTP/FTPS 上的文件:

- **只读直通**:`webfile_list` / `webfile_stat` 免审批;
- **变更逐次审批**:其余 6 个工具每次调用都经用户确认,拒绝即零副作用;
- **传输走后台任务**:下载/上传在 Jobs 面板流式展示进度,可随时取消;
- **凭据只存引用**:密钥值永不进入配置或会话日志,轮换后下一次调用即生效。

## 安装

```sh
dsh plugin --profile <name> add dsh-webfile
```

插件声明了 `dsh.bundle`,安装后**自动挂载**进该 profile 的 layer 栈(无需手写任何挂载行)。升级:

```sh
dsh plugin --profile <name> update
```

## 配置

在 profile 的 `cordis.patch.yml`(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)中按 id 覆盖配置:

```yaml
- id: dsh-webfile
  config:
    connections:
      prod-logs:
        protocol: s3
        endpoint: https://oss.example.com   # 省略走 AWS 公有云
        region: cn-north-1
        bucket: prod-logs
        pathStyle: true                     # MinIO 需要 true
        accessKeyRef: OSS_ACCESS_KEY        # 凭据引用名,见下节
        secretKeyRef: OSS_SECRET_KEY
      legacy-ftp:
        protocol: ftp
        host: ftp.example.com
        port: 21
        userRef: FTP_USER
        passwordRef: FTP_PASSWORD
        tls: explicit                       # none | explicit | implicit
      mi-fds:
        protocol: fds
        endpoint: https://cnbj2.fds.api.xiaomi.com   # 最小配置:endpoint + bucket + ak/sk
        bucket: mi-bucket
        accessKeyRef: FDS_ACCESS_KEY        # 必填:FDS 没有环境凭据链
        secretKeyRef: FDS_SECRET_KEY
        # region: cnbj2                     # 不想写完整 endpoint 时,可用 region 推导域名
        # https: false                      # 默认 true;明文 HTTP 时置 false
    maxTransferBytes: 2147483648            # 单文件传输上限,默认 2 GiB
    multipartThresholdBytes: 67108864       # S3/FDS 分片上传阈值,默认 64 MiB
```

### 连接字段

| 字段 | 协议 | 说明 |
|---|---|---|
| `protocol` | 三者 | `s3` / `fds` / `ftp`(必填) |
| `endpoint` | s3 | 自定义 endpoint(MinIO 等);省略走 AWS 公有云 |
| `region` | s3 | 区域(必填) |
| `bucket` | s3 | 默认 bucket |
| `pathStyle` | s3 | path-style 寻址,MinIO 需要 `true` |
| `accessKeyRef` / `secretKeyRef` / `sessionTokenRef` | s3 | 凭据引用名,见下节 |
| `region` | fds | FDS 区域(`cnbj2` / `awsbj0` / `awsusor0` / `awssgp0` / `awsde0` / `ksyru0-eco` / `awsind0-eco`);与 `endpoint` **二选一** |
| `endpoint` | fds | 完整域名(可带或省略 scheme),优先级高于 region;只配 endpoint 时 region 可省略 |
| `bucket` | fds | 默认 bucket |
| `https` | fds | 默认 `true`;FDS 也提供明文 HTTP |
| `accessKeyRef` / `secretKeyRef` | fds | 凭据引用名(**必填**,FDS 没有环境凭据链),见下节 |
| `host` | ftp | 服务器地址(必填) |
| `port` | ftp | 默认 21;隐式 FTPS 默认 990 |
| `userRef` / `passwordRef` | ftp | 凭据引用名;省略走匿名登录 |
| `tls` | ftp | `none` / `explicit` / `implicit`(必填) |
| `passive` | ftp | 默认 `true`;当前仅支持被动模式 |

### patch 语义要点

- **按 id 定位**:不带 `id` 的 patch 行会被跳过并告警;
- **整体替换**:`config` 浅覆盖整份替换,未写字段回落插件默认值;
- **不要重复挂载**:不要在用户层再 `insert` 同名行,配置一律走 id 覆盖;
- **禁用**:`- id: dsh-webfile / disabled: true` 可在该 profile 关闭插件;
- **热生效**:web 等长生命周期表面监视该文件,保存即经 HMR 事务性重载,**无需重启**;解析失败响亮报错并保留上一份好的配置。

## 凭据

配置中只写**凭据引用名**(环境变量名,POSIX 标识符),密钥值放在 `$DSH_HOME/.credentials.yaml`:

```yaml
OSS_ACCESS_KEY: AKIAxxxxxxxx
OSS_SECRET_KEY: xxxxxxxxxxxx
FDS_ACCESS_KEY: 你的 FDS Access Key   # dev.mi.com / 小米融合云控制台创建
FDS_SECRET_KEY: 你的 FDS Secret Key
FTP_USER: logbot
FTP_PASSWORD: xxxxxxxx
```

- **四层优先级**:进程环境 > 凭据文件 > 项目 `.env` > 用户 `.env`;进程环境只读且遮蔽同名文件条目;
- **热发布**:凭据文件被监视(100 ms 去抖),外部编辑后**下一次工具调用即用新值**,轮换零重启;
- **格式**:严格 `引用名: 字符串值` 映射(不是 dotenv 语法);值必须非空;文件权限必须 `0600`;
- 密钥值永不进入配置面或会话日志。

## 工具一览

| 工具 | 参数 | 权限 | 后台任务 |
|---|---|---|---|
| `webfile_list` | `connection`·`remotePath`·`maxEntries`(默认 200,上限 1000)·`nextToken` | 免审批 | 否 |
| `webfile_stat` | `connection`·`remotePath` | 免审批 | 否 |
| `webfile_mkdir` | `connection`·`remotePath` | **需审批** | 否 |
| `webfile_delete` | `connection`·`remotePath`·`recursive`(默认 false) | **需审批** | 否 |
| `webfile_move` | `connection`·`sourcePath`·`targetPath`·`overwrite`(默认 false) | **需审批** | 否 |
| `webfile_copy` | `connection`·`sourcePath`·`targetPath`·`overwrite`(默认 false) | **需审批** | 否 |
| `webfile_download` | `connection`·`remotePath`·`localPath`(可选,默认镜像到工作区根) | **需审批** | **是** |
| `webfile_upload` | `connection`·`localPath`·`remotePath`·`overwrite`(默认 false) | **需审批** | **是** |

## 行为语义

- **S3 没有真目录**:目录 = key 前缀聚合;`mkdir` 写零字节 `key/` 标记对象;`stat` 对无对象但有子项的前缀合成目录。
- **FDS 与 S3 同模型**:FDS 同样以 `/` 模拟目录;协议差异在 provider 内部消化(见下节 FDS 细节)。
- **列表分页**:结果截断时返回 `truncated: true` 与 `nextToken`(S3 `ContinuationToken` / FDS `marker` 透传),把 `nextToken` 传回即可续页;FTP 无服务端续页,截断时改大 `maxEntries` 重试。
- **覆盖保护**:`overwrite` 默认 `false`,目标已存在时报 `WEBBUF_EXISTS` 并提示显式开启。
- **递归保护**:`delete` 对非空目录默认报 `WEBBUF_DIR_NOT_EMPTY`;`recursive: true` 删除整棵子树。
- **传输上限**:`maxTransferBytes`(默认 2 GiB)操作前前置校验,超限报 `WEBBUF_TOO_LARGE` 且不产生任何远端副作用。
- **S3/FDS 大上传**:≥ `multipartThresholdBytes`(默认 64 MiB)自动走 multipart,进度单调上报。
- **取消**:传输中在 Jobs 面板 kill → 中止、清理本地半成品、job 终态 `killed`。
- **S3 目录 move/copy**:逐对象 copy(+delete),大目录耗时且中断可能留下半成品——工具描述与审批文案中已明示。
- **FDS 目录 move/copy**:同样逐对象 copy+批量删除,原因见下节;单文件 move 走原生 rename,零拷贝开销。
- **FTP 符号链接**:`list`/`stat` 以 `type=link` 呈现;递归删除只删链接自身、绝不跟随;copy 遇链接整体拒绝。

### FDS 实现细节

- **协议与签名**:FDS 是自有 REST API,不是 S3 线上协议。请求带 `Authorization: Galaxy-V2 {AK}:{Sig}`,`Sig = Base64(Hmac-SHA1(SK, StringToSign))`,签名字符串覆盖 Method / Content-MD5 / Content-Type / **Date**(必填,请保持系统时钟与 FDS 同步)与规范化后的 `x-xiaomi-*` 头和 subresource(`acl/quota/uploads/partNumber/uploadId/storageAccessToken/metadata`)。
- **端点**:缺省 `{region}.fds.api.xiaomi.com`;可用 `endpoint` 覆盖(内网 `xxx-fds.api.xiaomi.net`);`https: false` 走明文。
- **原生能力**:单文件 move 使用 `renameTo`(服务端重命名,无需 copy+delete);目录递归删除使用 `deleteObjects` 批量端点。
- **分片上传**:片大小固定 16 MiB(FDS 要求单片 5–50 MiB、片号连续);取消时 best-effort 调 `abort` 避免残留分片计费。
- **平台限制**:对象最大 100 GiB(cnbj2 等,cnbj0 已停用仅 2 GiB);Bucket 名 3–63 字节、域名规则;目录下文件多时 FDS 无 S3 式原子性保证,请知悉。

## Web 传输卡片

在 Web GUI 中,每次 `download` / `upload` 会在**对话流里生成一张传输卡片**(`webfile-transfer` conversation node):方向、label、实时状态点(传输中带活动动画)、耗时、起止路径与终态结果。数据分两层:

- **持久层**:host 半边在传输起止时向会话日志记录 `webfile/transfer-start` / `webfile/transfer-end` 事件对,卡片据此在消息流中定位,并在 job 被 registry 丢弃后仍可回放终态。
- **实时层**:客户端渲染器从 `jobsBySession` 镜像读取该 job 的实时状态、时间戳与终态 detail,registry 丢弃后回落到持久事件数据。

卡片随 `dsh.client` 声明(`package.json` → `dsh.client` + `exports["./client"]`)由浏览器端插件提供;`pnpm build` 产出 `lib/client.js`。它只在使用方 profile 组合了本包、且 Web 服务重启并刷新页面后生效(客户端插件表在启动时扫描);未组合进 Web 树时零影响——事件照常记录、模型侧 Jobs 面板不受影响。

## 安全说明

- **逐次授权**:每个变更操作独立审批,无会话级豁免;`allowed-once` 是唯一放行态。
- **失败关闭**:无审批通道(如纯 headless 未挂 answerer)或会话策略 `approval/policy: never` 时,变更工具一律拒绝,绝不默认放行。
- **会话日志**:工具参数(连接 id、remotePath 等)会随调用进入会话日志——路径敏感时请留意;密钥值永不进入。
- **S3 `CopyObject` 不可中止**:服务端复制一旦发出无法取消,涉及大目录 move/copy 时请知悉。
- **下载落盘**:一律经 `ctx.fs` 写入工作区,受 DSH 文件沙箱策略管控。

## 错误码

| 码 | 含义 |
|---|---|
| `WEBBUF_UNKNOWN_CONNECTION` | 连接 id 未配置(文案列出可用 id) |
| `WEBBUF_CREDENTIAL_MISSING` | 声明的凭据引用没有值(文案点名变量名) |
| `WEBBUF_NOT_FOUND` | 路径不存在 |
| `WEBBUF_EXISTS` | 目标已存在且 `overwrite: false` |
| `WEBBUF_DIR_NOT_EMPTY` | 非空目录且 `recursive: false` |
| `WEBBUF_PATH_TRAVERSAL` | 路径含 `..` 或为绝对路径 |
| `WEBBUF_TOO_LARGE` | 超过 `maxTransferBytes` |
| `WEBBUF_PROTOCOL_ERROR` | 协议层错误(如 FTP 拒绝跟随符号链接) |

## 兼容性

- **Node**:`^22.19.0 || >=24.0.0`(与 DeepSeek Harness 对齐)。
- **S3**:任何实现 ListObjectsV2 / HeadObject / GetObject / PutObject / DeleteObject(s) / CopyObject / multipart 接口的对象存储(AWS、MinIO、各类 S3 兼容服务)。
- **FDS**:小米 Galaxy FDS 的 cnbj2 / awsbj0 / awsusor0 / awssgp0 / awsde0 / ksyru0-eco / awsind0-eco 区域(原生 REST + Galaxy-V2 签名,密钥在 dev.mi.com 创建)。
- **FTP/FTPS**:支持 MLSD 的服务器字段最全(修改时间精确);仅支持 LIST 的老服务器可用但时间字段缺失、目录类型靠权限推断;TLS 支持显式与隐式两种模式。
- 每操作短连接(v1 无连接池),超大规模高频场景见后续版本。

## 开发

仓库提交的 manifest 和 lockfile 统一使用 registry 包,确保本地开发与 CI 解析同一套依赖:

```sh
pnpm install
pnpm check   # build + lint + 测试
```

跨仓库联调时,使用 `pnpm link <package-dir>...` 在 `node_modules` 中替换指定包,不要修改 `package.json` 或 `pnpm-lock.yaml`。

提交规范:`<type>(<scope>): <summary>`。执行 `pnpm exec lefthook install` 后,lefthook commit-msg hook 会校验本地提交;CI 使用同一规则校验 PR 标题。

**发布**:完全自动化,由 [semantic-release](https://semantic-release.gitbook.io/) 驱动——每次合入 `main` 都会分析符合规范的提交历史,自动 bump 版本(fix → patch,feat → minor,如 0.1.1 → 0.2.0)、发布到 npm、推送版本 tag,并按提交自动生成 GitHub Release 的 release notes。源码中的 manifest 有意保持 `0.0.0`;Git tag 是版本记录。

首次发布需要在 GitHub Actions 的 `NPM_TOKEN` secret 中配置 granular automation token。包创建后,为 `modestoma/dsh-webfile` 和 `ci.yml` 配置 npm Trusted Publishing,再删除该 secret 以及工作流中的环境变量。后续发布使用短期 OIDC 凭据,并自动携带 npm provenance 证明。

## License

[MIT](LICENSE) © 2026 modesto
