ooodc a7883dbed9 refactor(todo): 重构待办事项管理逻辑及更新状态规则
- 移除 TodoItem 中的 priority、created_at 和 updated_at 字段
- 强制每个任务都必须有唯一 id,且由用户负责生成
- 修改合并模式逻辑,merge=true 下保留未提及的旧任务
- 支持已完成和已取消任务重新激活(状态改回 pending 或 in_progress)
- 禁止 in_progress 状态退回到 pending,必须标记为 completed 或 cancelled
- 优化状态转换校验,允许特定状态间合法切换
- 简化任务变更消息,移除详细的新增/更新/移除统计
- 更新文档和示例,明确 id 必须由用户生成和使用
- 修复和补充测试,增强状态转换和合并模式验证
- 调整任务时间戳生成逻辑,统一使用当前时间及索引
- 该变更提供更合理的任务状态机械及管理模式,提升稳定性和易用性
2026-06-13 09:22:33 +08:00

163 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) —— 单文件按需上传