v51c 卡片后台同步:修复"每轮全量重拉",单账号 236 秒 → 10 秒

真机实测根因:服务端 CalDav.php:253 用 preg_replace('/[^A-Za-z0-9._-]/','_',$uid)
派生资源文件名(UID movie-188@familybook → 资源 movie-188_familybook.ics),
而客户端 SyncEngine.syncAccountInner 拿 REPORT 给的**资源名当 uid** 去本地行里查
⇒ byUid 永远查不到 ⇒ 每轮判"变了" ⇒ 全量 GET。ETag 明明一样,比较压根执行不到。

改动:
· SyncEngine —— 加 byEtag 兜底(uid 查不到就用内容哈希认领本地行)+ 复用判定计数日志
  (uid 命中 / etag 兜底命中 / 需补拉);抽 groupPush() 去重。
· EventDb.applyRemote —— existing.find() 的 O(n·m) → keyed.get() 的 O(n)。
· BackgroundSyncRunner —— runOnce 每阶段计时(+Xms)+ pid;预算可由 preferences
  card_budget_ms 覆盖(诊断开关,默认走内置 9 秒)。
· AppSettings —— getCardBudgetMs()。
· EntryFormAbility —— onUpdateForm 日志带 pid。
· MirrorInbound —— 顺带修一个已死的功能:getEvents(undefined, 字段) 在 7.0.0.109 上
  必抛 401 type error!,导致**入站对账 12/12 全失败**;改成"先无参、失败再带字段"
  (照抄 SystemCalendarMirror.queryEvents 的绕法)。

真机验证(DEL-AL20,卡片 onUpdateForm 触发,卡片进程 pid=27411):
  [21:33:06] 卡片驱动同步:开始(可用窗口 9 秒)
  [21:33:09] 「观影」复用判定:uid 命中 0、etag 兜底命中 193、需补拉 0(共 193)
  [21:33:09] 「观影」落库完成:不变193,耗时 1 秒     ← 修复前 124 秒
  [21:33:17] 同步账号完成:拉取 2182 条,耗时 10 秒   ← 修复前 215~236 秒
  [21:33:22] 后台同步[cardTick] 完成:1/1 个账号,总耗时 16 秒
12 个日历本全部跑完 + 出站镜像,9 秒窗口不再"连第一个本都过不去"。

复算(真库快照 + 真服务器 PROPFIND,_tooling/_sc_etag_sim.py):
  旧逻辑补拉 1384 个 → 新逻辑 80 个(降 94.2%);观影 193→0、旅行 23→0、工作 362→5。
  另证服务端 ETag == md5(正文)、三次 PROPFIND 逐字节一致 ⇒ etag 兜底语义正确、非碰巧。
This commit is contained in:
2026-10-15 21:39:54 +08:00
parent 8ae4e11507
commit 24d8117ce7
6 changed files with 132 additions and 21 deletions
+20 -2
View File
@@ -82,6 +82,25 @@ export class MirrorInbound {
/* ==================== 单个日历本 ==================== */
/**
* 取回该账户下已有日程。
*
* 🔴 2026-10-15 实测(DEL-AL20 / 7.0.0.109):`getEvents(undefined, 字段数组)` 在这个
* 系统版本上**必抛** `401 type error!`。而本文件原先正是这么调的 ⇒ "入站对账"对
* **每一个**日历本都失败(真机日志 12/12 全是 `401 type error!`),
* 整个"把系统日历里的改动回写 CalDAV"等于死的(还白费 12 次调用)。
* 出站镜像(SystemCalendarMirror.queryEvents)早就踩过同一个坑,改成"先无参、失败再带字段"。
* 这里沿用同一策略:无参调用返回的 Event 里 identifier 同样有值。
*/
private static async queryEvents(cal: calendarManager.Calendar): Promise<calendarManager.Event[]> {
try {
return await cal.getEvents();
} catch (err) {
LogUtil.write(`入站对账 getEvents() 失败,退回带字段:${(err as BusinessError).message}`);
return await cal.getEvents(undefined, MirrorInbound.QUERY_FIELDS);
}
}
private static async reconcileBook(
context: common.Context,
cm: calendarManager.CalendarManager,
@@ -109,8 +128,7 @@ export class MirrorInbound {
}
const snaps: Map<string, MirrorSnapshotRow> = await MirrorSnapshot.loadMap(context, bucket);
const events: calendarManager.Event[] = await cal.getEvents(
undefined, MirrorInbound.QUERY_FIELDS);
const events: calendarManager.Event[] = await MirrorInbound.queryEvents(cal);
// 分成两类:带我们 identifier 的(可判定改/删) vs 不是我们的(用户新建)
const mine: Map<string, calendarManager.Event> = new Map<string, calendarManager.Event>();