fix(同步): 待办从未被同步 —— 取消勾选后重新勾选也拉不回来 #6

## 现象(用户反馈)
1. 账号设置里取消勾选待办本 → 保存同步 → 待办标签消失(这一步是正常的)
2. 再进去重新勾选 → 保存并同步 → **待办没回来,标签也不显示**

## 根因:VTODO 从来就没被同步过
同步主循环 `syncAccountInner` 全程只处理 VEVENT:
- `DavClient.reportEtags(href)`:REPORT 的 comp-filter 只写了 `VEVENT`
- `EventDb.queryByCalKey(context, calKey, 'event')`:只查 kind='event'
- `EventDb.applyRemote(..., isTodo)`:这个 isTodo 参数**从未传过 true**

而 `DavClient.reportTodos()`(拉 VTODO)虽然早就写好了,
**全项目零调用** —— 是一段死代码。

因果链:
· 取消勾选待办本 → `pruneStaleHrefs` 清掉不属于已选本的 todo 行
· 重新勾选 → 同步只拉 VEVENT,不拉 VTODO
· 本地库没有待办数据 → `loadTodos` 返回空 → 标签不显示

待办功能"能读能改"(勾选完成、设置循环都能用),但**数据来源一直是空的**,
只有以前残留的行在撑着。一旦被清掉就再也不回来。

## 修法
在同步循环里对每个日历本额外拉一次 VTODO:
  `reportTodos(href)` → 逐个 GET 原文 → `IcsUtil.parseTodos` → `applyRemote(..., true)`

⭐ 为什么对 unknown 类型的本也拉:
   服务器若没声明 supported-calendar-component-set,我们分不清哪个本是待办本。
   待办量远小于日程,多拉一次的代价很小;宁可贵一点,
   也不能让用户的待办再次"消失"。

⭐ 失败隔离:
   待办同步整段包在 try/catch 里,单本失败只记日志、不影响日程同步。
   很多服务器对 VTODO 查询返回 403 或不支持该 REPORT,
   不能让这种情况把整个账号同步拖垮。

## 影响面
`applyRemote` 内部按 `kind` 隔离(existing 只查同 kind),
且自带 removed 处理 ⇒ 不会误删日程行,也不会漏删已消失的待办。
This commit is contained in:
2026-11-02 22:39:01 +08:00
parent 24cd51970c
commit ffaffff9d6
+30
View File
@@ -173,6 +173,36 @@ export class SyncEngine {
throw err as Error;
}
LogUtil.write(`日历本[${i}]「${calName}」REPORT 返回 ${items.length} 个资源`);
// ⭐ SYNC_TODOS 同步该本里的待办(VTODO)。
// 原来整个同步流程只处理 VEVENT,`DavClient.reportTodos` 写了却没人调用,
// 导致待办数据从来不会从服务器拉下来 ——
// 用户取消勾选本(触发 pruneStaleHrefs 清行)后再勾回来,待办就永久消失了。
// 放在这里(VEVENT 处理之前)单独走一遍,不影响原有日程逻辑。
try {
const todoItems: RemoteItem[] = await DavClient.reportTodos(href, auth);
if (todoItems.length > 0) {
const todoRemote: RemoteEvent[] = [];
for (const ti of todoItems) {
try {
const raw: string = await DavClient.getRawStrict(ti.href, auth);
const parsed: RemoteEvent[] = IcsUtil.parseTodos(raw);
for (const pr of parsed) {
pr.etag = ti.etag;
todoRemote.push(pr);
}
} catch (e) {
LogUtil.write(`待办 GET 失败(跳过) ${ti.href}`);
}
}
if (todoRemote.length > 0) {
await EventDb.applyRemote(context, calKey, href, todoRemote, true);
LogUtil.write(`日历本[${i}]「${calName}」待办同步:${todoRemote.length} 条`);
}
}
} catch (e) {
// 待办同步失败不能影响日程同步(很多服务器对 VTODO 查询返回 403/不支持)
LogUtil.write(`日历本[${i}]「${calName}」待办同步失败(忽略):${(e as BusinessError).message ?? ''}`);
}
// 现有行按 uid 分组(同一资源的覆盖实例共享 uid 与 etag)
const existingRows: LocalEvent[] = await EventDb.queryByCalKey(context, calKey, 'event');
const byUid: Map<string, LocalEvent[]> = new Map();