- 移除 TodoItem 中的 priority、created_at 和 updated_at 字段 - 强制每个任务都必须有唯一 id,且由用户负责生成 - 修改合并模式逻辑,merge=true 下保留未提及的旧任务 - 支持已完成和已取消任务重新激活(状态改回 pending 或 in_progress) - 禁止 in_progress 状态退回到 pending,必须标记为 completed 或 cancelled - 优化状态转换校验,允许特定状态间合法切换 - 简化任务变更消息,移除详细的新增/更新/移除统计 - 更新文档和示例,明确 id 必须由用户生成和使用 - 修复和补充测试,增强状态转换和合并模式验证 - 调整任务时间戳生成逻辑,统一使用当前时间及索引 - 该变更提供更合理的任务状态机械及管理模式,提升稳定性和易用性
163 lines
14 KiB
Markdown
163 lines
14 KiB
Markdown
|
||
# drive +push
|
||
|
||
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||
|
||
把本地目录**单向、文件级**镜像到飞书云空间(云盘/云存储)的某个文件夹(本地 → Drive)。命令递归列出 `--folder-token` 下的远端清单,遍历 `--local-dir` 的所有常规文件,按相对路径在 Drive 上新建、覆盖或跳过;可选地(`--delete-remote --yes`)删除云端"本地没有"的 `type=file`。
|
||
|
||
> **"文件级镜像"≠"目录镜像"。** 命令只在文件维度收敛差异:本地多了文件就上传,本地少了文件且开了 `--delete-remote --yes` 就删远端文件。**远端只有的空目录、本地已删除的目录**都不会被收敛,云端目录树的多余结构不会被清理。如果需要"目录也要保持完全一致",得自行先 `+status` 找差异、再手动处理多余目录。
|
||
|
||
输出按"动作"分类:
|
||
|
||
| 字段 | 含义 |
|
||
|------|------|
|
||
| `summary.uploaded` | 成功新建或覆盖的文件数 |
|
||
| `summary.skipped` | 因 `--if-exists=skip` 或 `--if-exists=smart` 命中“无需传输”而跳过的文件数 |
|
||
| `summary.failed` | 上传 / 覆盖 / 建目录 / 删除失败的条目数;**只要不为 0,命令就以非零状态退出**(结构化 `items[]` 仍在 stdout 上) |
|
||
| `summary.deleted_remote` | 启用 `--delete-remote --yes` 时删除的云端文件数 |
|
||
| `items[]` | 每个条目的明细(`rel_path` / `file_token` / `action` / 覆盖时的 `version` / `size_bytes` / 失败时的 `error`) |
|
||
|
||
`items[].action` 取值:`uploaded` / `overwritten` / `skipped` / `folder_created` / `deleted_remote` / `failed` / `delete_failed`。
|
||
|
||
> 本地目录(包括空目录)会被镜像到 Drive;新建的子目录会以 `action: "folder_created"` 出现在 `items[]` 里,但**不计入** `summary.uploaded`(该字段只数文件)。已存在的远端目录复用其 token,不会重复 `create_folder`,也不会出现在 `items[]` 里。
|
||
|
||
## 远端同名文件冲突
|
||
|
||
如果 Drive 中多个条目映射到同一个 `rel_path`,默认直接失败(`error.type=duplicate_remote_path`),且不会上传、覆盖或进入 `--delete-remote` 删除阶段。只有“多个 `type=file` 同名”的场景支持显式策略;`file-folder` 这类异构冲突始终直接失败。
|
||
|
||
| 策略 | 行为 |
|
||
|------|------|
|
||
| `fail` | 默认。返回所有冲突条目的完整信息,不写远端 |
|
||
| `newest` | 只把本地文件与 `modified_time` 最新的远端文件对齐 |
|
||
| `oldest` | 只把本地文件与 `created_time` 最早的远端文件对齐 |
|
||
|
||
`+push` 不提供 `rename`:本地一个文件无法表达要覆盖多个远端对象。若用户想保留多个云端副本,应先显式整理云端文件,再重新 push。
|
||
|
||
## 命令
|
||
|
||
```bash
|
||
# 基础用法 —— 把本地 ./repo 推送到云端 fldcXXX
|
||
# 默认 --if-exists=skip:已经存在的远端文件保持不动,只新增、不覆盖。
|
||
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx
|
||
|
||
# 重复同步时可用 smart 做增量优化:它会按 modified_time 跳过已对齐的远端文件;但如果远端更旧,仍会继续走覆盖路径
|
||
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
|
||
--if-exists smart
|
||
|
||
# 显式覆盖远端同名文件(依赖 upload_all 的灰度协议字段,详见下文"覆盖语义")
|
||
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
|
||
--if-exists overwrite
|
||
|
||
# 云端已有多个同名二进制文件时,显式选择一个远端目标再覆盖
|
||
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
|
||
--if-exists overwrite --on-duplicate-remote newest
|
||
|
||
# 文件级镜像同步:上传 / 覆盖 + 删除本地不存在的远端文件
|
||
# (--delete-remote 必须搭配 --yes,否则会被 Validate 直接拒绝;
|
||
# 且 Validate 阶段会动态检查 space:document:delete scope,缺权限会立刻失败,
|
||
# 不会出现"上传成功了但是后面删除阶段挂了"的半同步状态)
|
||
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
|
||
--if-exists overwrite --delete-remote --yes
|
||
```
|
||
|
||
## 参数
|
||
|
||
| 标志 | 必填 | 类型 | 说明 |
|
||
|------|------|------|------|
|
||
| `--local-dir` | 是 | path | 本地根目录(**必须是 cwd 的相对路径**;绝对路径或逃出 cwd 的相对路径会被 CLI 直接拒绝) |
|
||
| `--folder-token` | 是 | string | 目标 Drive 文件夹 token |
|
||
| `--if-exists` | 否 | enum | 远端文件已存在时的策略:`skip`(**默认**,安全)/ `smart`(用于重复增量同步;当远端 `modified_time` 已匹配或更新时跳过上传,否则继续走覆盖路径)/ `overwrite`(依赖灰度后端协议,详见"覆盖语义") |
|
||
| `--on-duplicate-remote` | 否 | enum | 云端多个条目映射到同一个 `rel_path` 时的策略:`fail`(默认);如果冲突全是 `type=file`,还可选 `newest` / `oldest` |
|
||
| `--delete-remote` | 否 | bool | 删除云端本地不存在的文件(文件级镜像;**不会**清理远端只有的目录);**必须配合 `--yes`**,且 Validate 阶段会动态检查 `space:document:delete` scope |
|
||
| `--yes` | 否 | bool | 确认 `--delete-remote`;不传时该破坏性操作在 Validate 阶段被拒绝 |
|
||
|
||
## 上传与目录复刻范围
|
||
|
||
- **只上传 / 覆盖 / 删除 Drive `type=file`**。在线文档(`docx` / `sheet` / `bitable` / `mindnote` / `slides`)和快捷方式(`shortcut`)即使在同一 rel_path 下出现,也不会被覆盖或删除 —— 它们没有等价的本地二进制。
|
||
- **本地目录结构整体被镜像**:所有子目录(含**空目录**)会按需在 Drive 上 `create_folder`;同名远端目录复用其 token,不重建。空目录不计入 `summary.uploaded`,但会在 `items[]` 里以 `folder_created` 形式留痕。
|
||
- 已存在的远端文件按 `--if-exists` 决定 `overwrite` / `smart` / `skip`。其中 `smart` 是**增量优化模式**:只要远端 `modified_time` 在同等时间精度下已经等于或晚于本地 mtime,就跳过上传;时间戳缺失/非法时会退回安全路径继续上传,不会盲跳。**但如果远端更旧,`smart` 会继续走和 `overwrite` 相同的覆盖路径,因此也继承同样的 rollout / version 返回 caveat。** 想做 `keep-both` 这类的仍需自行改名再 push。
|
||
- 云端同名冲突默认失败;只有“冲突全是 `type=file`”且传了 `--on-duplicate-remote newest|oldest` 时才会选择一个远端文件继续。启用 `--delete-remote` 时,未被选中的 duplicate sibling 也会被删除,最终远端只保留一个被选中的文件副本;只有在 `--if-exists=overwrite` 成功时,才能保证该副本内容与本地对齐。
|
||
|
||
## 覆盖语义
|
||
|
||
`--if-exists=overwrite` 走 `POST /open-apis/drive/v1/files/upload_all`,并在 form 中带上现有文件的 `file_token`,由后端原地更新内容并返回新版本号。`items[].version` 字段会回填该版本号。
|
||
|
||
`--if-exists=smart` 是给“重复跑同步”的场景增加的增量优化:当远端 `modified_time` 在同等时间精度下已经等于或晚于本地 mtime 时,命令会把该文件计为 `skipped`;时间戳缺失、非法或更旧时,则继续走正常上传/覆盖路径。**也就是说,只要 smart 判定“远端不够新”,它就会进入与 `--if-exists=overwrite` 相同的覆盖实现,因此在未 rollout version 字段的 tenant 上仍可能非零失败。**
|
||
|
||
> **为什么默认是 `skip` 而不是 `overwrite`:** `upload_all` 接受 `file_token` 字段、并在响应里返回 `version` 是设计文档(Drive 同步盘)规定的协议;此后端尚在灰度发布。在还未开通该字段的 tenant 上,`--if-exists=overwrite` 会因"无 version 返回"而把对应文件标成 `failed`,整次 `+push` 也会因此非零退出。所以默认值故意定为 `skip`:第一次往一个已经有内容的目录里 push,不会因为协议没到位就把整次运行打挂;要真的覆盖远端,必须显式带 `--if-exists overwrite`。新建上传不依赖该字段,不受影响。
|
||
|
||
大文件(>20MB)会自动切到三段式 `upload_prepare` / `upload_part` / `upload_finish`;该路径下 `version` 暂未在响应中返回,覆盖结果中 `items[].version` 会留空,但 `file_token` 与 `action: overwritten` 仍会正确产出。
|
||
|
||
## --delete-remote 的安全行为
|
||
|
||
`--delete-remote` 是命令里**唯一的破坏性 flag**,会按"远端有但本地没有"逐个 `DELETE /open-apis/drive/v1/files/<token>?type=file` 清理云端副本。设计上把它跟 `--yes` 强绑定:
|
||
|
||
- `--delete-remote`(无 `--yes`)→ Validate 直接报错:`--delete-remote requires --yes`,不会发起任何列表 / 上传 / 删除请求。
|
||
- `--delete-remote --yes` → Validate 阶段还会**动态做一次** `space:document:delete` 的 scope 预检:缺这条 scope 时整次运行立刻失败、不发任何上传请求,避免出现"上传都成功了,但删除阶段才报 missing_scope"的半同步状态。
|
||
- `--delete-remote --yes`(且 scope 已授权)→ 正常执行:先把本地文件 push 上去,再扫一遍远端 `type=file` 列表,把不在本地清单里的逐个删除。**任何上传 / 覆盖 / 建目录失败时,整段 `--delete-remote` 阶段会被跳过**(stderr 上有提示),命令以非零状态退出,远端不会被破坏。
|
||
- 远端同名冲突且使用默认 `fail`,或冲突里混有 folder / 其他非 `type=file` 对象 → 在上传阶段前失败,删除阶段不会运行。
|
||
- 不传 `--delete-remote` → `summary.deleted_remote` 永远是 0;命令对远端"多余"文件视而不见。
|
||
- 在线文档(docx / sheet / bitable / ...)和快捷方式即使本地完全没有同名文件,也**不会**进入删除候选,因为它们从来不进 `summary.uploaded` 的对齐域。
|
||
- **远端只有的空目录、本地已删除的目录**也不会被清理 —— 这是"文件级镜像"的语义边界,命令不会对目录结构做主动收敛。
|
||
|
||
第 6 章里把 `+push --delete-remote` 标了 `high-risk-write`,CLI 这边的实现等价于"未传 `--yes` 时拒绝执行 + 动态 scope 预检",符合该约束的精神。
|
||
|
||
## 输出 schema
|
||
|
||
```json
|
||
{
|
||
"summary": {
|
||
"uploaded": 0,
|
||
"skipped": 0,
|
||
"failed": 0,
|
||
"deleted_remote": 0
|
||
},
|
||
"items": [
|
||
{"rel_path": "...", "file_token": "...", "action": "folder_created"},
|
||
{"rel_path": "...", "file_token": "...", "action": "uploaded", "size_bytes": 0},
|
||
{"rel_path": "...", "file_token": "...", "action": "overwritten", "version": "...", "size_bytes": 0},
|
||
{"rel_path": "...", "file_token": "...", "action": "skipped", "size_bytes": 0},
|
||
{"rel_path": "...", "action": "failed", "size_bytes": 0, "error": "..."},
|
||
{"rel_path": "...", "file_token": "...", "action": "deleted_remote"},
|
||
{"rel_path": "...", "file_token": "...", "action": "delete_failed", "error": "..."}
|
||
]
|
||
}
|
||
```
|
||
|
||
`rel_path` 始终用 `/` 作为分隔符(跨平台一致)。
|
||
|
||
## 性能注意
|
||
|
||
- 默认 `skip` 下,已存在的远端文件一律不碰;`overwrite` 下,重复跑会重传所有命中的同名文件;`smart` 下会按 `modified_time` 跳过已对齐的远端文件,但对“远端更旧”的文件仍会进入覆盖路径,因此它减少的是**不必要的重传**,不是把覆盖风险完全拿掉。
|
||
- 想更精细地控制传输量,可以先 `+status` 找出 `new_local` 和 `modified`,再只对这些文件单独上传 / 覆盖;或者直接在整目录同步时使用 `--if-exists smart`。
|
||
- 大文件会用三段式分片上传(不会把整个 body 读进内存),但本地磁盘和上行带宽需要够。
|
||
|
||
## 所需 scope
|
||
|
||
| 操作 | scope | 是否在命令上预声明 |
|
||
|------|-------|-------------------|
|
||
| 列出文件夹 / 子目录 | `drive:drive.metadata:readonly` | ✅ 预声明 |
|
||
| 上传 / 覆盖文件 | `drive:file:upload` | ✅ 预声明 |
|
||
| 新建子目录(`create_folder`) | `space:folder:create` | ✅ 预声明 |
|
||
| 删除文件(仅 `--delete-remote --yes`) | `space:document:delete` | ⚙️ 不在命令默认 Scopes 里,但在 `--delete-remote --yes` 时由 Validate 动态预检 |
|
||
|
||
`drive:drive` 在部分企业被策略禁用,所以 +push 故意只声明上面这几条细粒度 scope。
|
||
|
||
> **关于 `space:document:delete`:** 框架的 scope 预检(`runner.go: checkShortcutScopes`)会在 `Validate` 和 `--dry-run` 之前就把命令上声明的 scope 全检查一遍;如果把删除 scope 也预声明,**普通上传或 dry-run** 都会因为没授权删除权限而被拦下来。所以这一项不放在命令的默认 Scopes 里,而是在 Validate 中**条件触发**:只有 `--delete-remote --yes` 同时打开时才会调用 `runtime.EnsureScopes([]string{"space:document:delete"})` 做一次动态前置校验。这样既保留了"普通上传不需要删除权限"的便利,又能在真要做镜像删除前把 scope 缺失暴露出来,避免出现"上传成功 → 删除阶段才挂"的半同步状态。
|
||
>
|
||
> 想一次性把权限补齐:`lark-cli auth login --scope "drive:drive.metadata:readonly drive:file:upload space:folder:create space:document:delete"`。
|
||
|
||
## 范围限制
|
||
|
||
`--local-dir` 只接受 cwd 内的相对路径。CLI 会先 `EvalSymlinks` 整条路径,再判断它是否仍落在 cwd 内 —— **指向 cwd 外的符号链接也会被拒**,"在 cwd 内放一条软链指向外面" 这条捷径走不通,会直接撞上 `unsafe file path`。
|
||
|
||
如果用户想 push cwd 之外的目录,**不要 agent 自己 `cd` 绕过**。可以选:让用户在外部把 agent 工作目录切换到目标的祖先后重启会话;或者把目标整体物理移动 / 拷贝到 cwd 内(不是软链);或者直接放弃这次同步,改用别的方式。
|
||
|
||
## 参考
|
||
|
||
- [lark-drive](../SKILL.md) —— 云空间(云盘/云存储)全部命令
|
||
- [lark-shared](../../lark-shared/SKILL.md) —— 认证和全局参数
|
||
- [lark-drive-status](lark-drive-status.md) —— 上传前先看差异(避免全量回写)
|
||
- [lark-drive-pull](lark-drive-pull.md) —— Drive → 本地的对称命令
|
||
- [lark-drive-upload](lark-drive-upload.md) —— 单文件按需上传
|