feat(contacts): 支持桌面设备间安全同步 - #1214
Conversation
Signed-off-by: zqchris <chrisz83@gmail.com>
Signed-off-by: zqchris <chrisz83@gmail.com>
Signed-off-by: zqchris <chrisz83@gmail.com>
Signed-off-by: zqchris <chrisz83@gmail.com>
|
| Filename | Overview |
|---|---|
| apps/desktop/src/main/contacts-sync/lanTransport.ts | 实现局域网发现、双向认证、连接资源边界和失败后的设备级直连冷却。 |
| apps/desktop/src/main/contacts-sync/driver.ts | 编排账号隔离、运行时所有权、自动同步时机、入站处理及状态发布。 |
| apps/desktop/src/main/contacts-sync/keyStore.ts | 实现按账号隔离的设备密钥存储、公钥固定和跨进程锁定。 |
| apps/desktop/src/main/contacts-sync/sender.ts | 按分片顺序发送同步状态,并在局域网直连失败时通过 relay 回退。 |
| packages/maker-core/src/contacts/sync/repository.ts | 将同步状态合并、投影物化和索引维护纳入 SQLite 事务边界。 |
| packages/device-link/src/contactsSyncProtocol.ts | 为通讯录同步增加独立且受校验的 Device Link payload 协议。 |
| apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx | 在智能通讯录设置中增加跨设备同步开关、状态和立即同步操作。 |
Sequence Diagram
sequenceDiagram
participant A as Desktop A
participant LAN as 局域网候选端点
participant Relay as Device Link Relay
participant B as Desktop B
A->>LAN: 发现 B 的候选端点
A->>B: 密文分片 + 设备密钥认证请求
alt B 返回有效认证 ACK
B-->>A: 已认证 ACK
else 直连或认证失败
A->>Relay: 转发同一密文分片
Relay->>B: 密文分片
end
B->>B: Worker 解密、校验、合并并更新 SQLite/FTS
Reviews (17): Last reviewed commit: "fix(contacts): align normalized identity..." | Re-trigger Greptile
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ead6855a95
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
该 PR 为「智能通讯录」新增 Desktop ↔ Desktop 的安全自动同步能力:同一账号下在线设备之间优先走局域网直连,失败回退到现有 Device Link relay 转发端到端密文;同步数据采用确定性状态合并,目标是多设备最终收敛且不依赖模型推理。
Changes:
- 在
@cindy/maker-core引入通讯录同步的 CRDT 状态模型、合并/增量、校验、SQLite 持久化与快照读写,并在MakerContactsStore暴露激活/读取/合并入口。 - Desktop main 侧新增 contacts-sync 驱动(密钥、安全存储、分片 wire、LAN 直连、relay 回退、状态机与持久化),并通过 IPC + preload 暴露给 renderer。
- Desktop 设置页与管理弹窗新增「在我的设备之间同步」开关/状态/立即同步入口,并补齐四语文案与术语表条目。
Reviewed changes
Copilot reviewed 40 out of 41 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/maker-core/src/index.ts | 导出通讯录同步类型与增量生成函数供 desktop 侧复用。 |
| packages/maker-core/src/contacts/sync/types.ts | 定义同步契约(状态、stamp/clock、快照结构、membership 复合键)。 |
| packages/maker-core/src/contacts/sync/merge.ts | 实现状态合并、stamp 比较、增量生成与下一次本地 stamp 生成。 |
| packages/maker-core/src/contacts/sync/capture.ts | 将 SQLite 两次快照差异捕获进 CRDT 状态(字段级 stamp/删除墓碑)。 |
| packages/maker-core/src/contacts/sync/materialize.ts | 将合并后的 CRDT 状态物化为 SQLite 可写快照,并做唯一约束冲突的确定性裁决。 |
| packages/maker-core/src/contacts/sync/snapshot.ts | 读写通讯录主表快照(用于同步层投影与写回)。 |
| packages/maker-core/src/contacts/sync/validation.ts | 深度校验来自链路/磁盘的同步状态,防畸形/超量数据进入 SQLite。 |
| packages/maker-core/src/contacts/sync/repository.ts | 同步状态的 SQLite singleton 持久化与 reconcile/merge 流程。 |
| packages/maker-core/src/contacts/store.ts | 接入同步仓库并暴露 activate/read/merge,同步写回后安全重建 FTS。 |
| packages/maker-core/src/contacts/schema.ts | v3 migration:新增 contacts_sync_state 表。 |
| packages/maker-core/src/contacts/tests/contactsSync.test.ts | maker-core 侧三设备传播、幂等/交换/结合、增量与安全校验等测试。 |
| i18n/GLOSSARY.md | 新增 “End-to-end encryption” 术语条目(待讨论)。 |
| i18n/glossary.json | 新增 end-to-end-encryption 术语(proposed,含四语翻译)。 |
| apps/desktop/src/renderer/vite-env.d.ts | 扩展 ElectronAPI:同步状态读取/开关/立即同步 + 状态订阅。 |
| apps/desktop/src/renderer/lib/contactsService.ts | 定义 ContactsDeviceSyncStatus 并封装 sync IPC 调用与订阅。 |
| apps/desktop/src/renderer/i18n/locales/zh-CN/common.json | 增加同步卡片文案与错误提示(zh-CN)。 |
| apps/desktop/src/renderer/i18n/locales/en/common.json | 增加同步卡片文案与错误提示(en,含复数)。 |
| apps/desktop/src/renderer/i18n/locales/ja/common.json | 增加同步卡片文案与错误提示(ja)。 |
| apps/desktop/src/renderer/i18n/locales/ko/common.json | 增加同步卡片文案与错误提示(ko)。 |
| apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx | 设置页新增同步卡:状态展示、立即同步、开关与状态订阅。 |
| apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx | 管理弹窗头部新增同步状态展示与立即同步入口。 |
| apps/desktop/src/preload/preload.ts | 通过 contextBridge 暴露 sync IPC invoke 与 status-changed fan-out。 |
| apps/desktop/src/main/maker-ipc/channels.ts | 新增 contacts sync IPC channels 常量。 |
| apps/desktop/src/main/maker-ipc/contacts-ipc.ts | 增加 sync IPC handlers、status 广播通道、并对 invoke 加 trusted renderer 校验。 |
| apps/desktop/src/main/maker-ipc/tests/contactsIpc.test.ts | 覆盖 sync IPC handler 行为与参数校验。 |
| apps/desktop/src/main/maker-host/contacts-settings-store.ts | settings 落盘新增 deviceSyncEnabled,并支持热刷新 invalidateIfChanged。 |
| apps/desktop/src/main/maker-host/contacts-change-events.ts | 新增 main 内部“本地通讯录变更”事件(供同步驱动监听)。 |
| apps/desktop/src/main/device-link/index.ts | 接入 contacts sync driver:presence/online 变化、relay 入站帧分发、初始化驱动。 |
| apps/desktop/src/main/contacts-sync/crypto.ts | X25519 + HKDF + AES-256-GCM 原语实现(含 AAD 绑定)。 |
| apps/desktop/src/main/contacts-sync/wire.ts | gzip+AES-GCM 后分片的 wire 格式、帧校验与分片重组解码器。 |
| apps/desktop/src/main/contacts-sync/lanTransport.ts | 局域网 UDP multicast 发现 + TCP 直连传输密文帧(失败回退 relay)。 |
| apps/desktop/src/main/contacts-sync/keyStore.ts | safeStorage 加密的 owner-scoped 私钥与 TOFU peer 公钥 pin 落盘。 |
| apps/desktop/src/main/contacts-sync/statusStore.ts | 最近一次成功同步信息的 owner-scoped 持久化(不含内容)。 |
| apps/desktop/src/main/contacts-sync/statusModel.ts | 同步状态机类型与错误码归类(用于 UI 展示)。 |
| apps/desktop/src/main/contacts-sync/sender.ts | 出站发送:基于 known clocks 生成增量,优先 LAN 否则 relay,跨 await 校验上下文。 |
| apps/desktop/src/main/contacts-sync/driver.ts | 同步驱动总控:启停、owner/generation 隔离、去抖与定时校准、入站解密合并与状态广播。 |
| apps/desktop/src/main/contacts-sync/tests/crypto-wire.test.ts | 覆盖加密/解密、分片乱序重组、篡改检测与 wire 校验。 |
| apps/desktop/src/main/contacts-sync/tests/lanTransport.test.ts | 覆盖 LAN 直连仅传密文、端点缺失时回退 relay 的行为。 |
| apps/desktop/src/main/contacts-sync/tests/keyStore.test.ts | 覆盖密钥加密落盘、TOFU pin 与 fail-closed。 |
| apps/desktop/src/main/contacts-sync/tests/driver.test.ts | 覆盖 owner 切换中断发送、避免旧 owner 密文转发等边界。 |
| apps/desktop/forge.config.ts | macOS 增加 NSLocalNetworkUsageDescription 以支持局域网直连同步。 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 41 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/maker-core/src/contacts/sync/materialize.ts:94
- 这里用 toLocaleLowerCase() 会受系统 locale 影响(例如土耳其语 i/İ 规则),可能导致不同设备在同一输入下算出不同的去重 key,从而选出不同“赢家”,破坏同步状态“确定性/最终收敛”的前提。建议改用 locale 无关的 toLowerCase()(或显式指定固定 locale)。
apps/desktop/src/main/maker-ipc/contacts-ipc.ts:164 - 该 handler 在 deps.readDeviceSyncStatus 未提供时仍可能返回 undefined(optional chaining),但 renderer 侧会把返回值当作 ContactsDeviceSyncStatus 使用。建议将 readDeviceSyncStatus 作为必需依赖并直接调用,保持 IPC 契约稳定。
if (!deps.syncNow) throwIpcError('INTERNAL', 'contacts sync is unavailable');
try {
await deps.syncNow();
return await deps.readDeviceSyncStatus?.();
} catch (err) {
packages/maker-core/src/contacts/sync/materialize.ts:53
- preferNewest() 在 stamp 相同时用 localeCompare() 做 tie-break,比较结果可能因运行环境 locale 不同而变化;在冲突场景(同 stamp)下可能导致不同设备选择不同记录作为唯一赢家。为保证跨设备确定性,建议改为按 code point 的稳定比较(< / >)。
apps/desktop/src/main/maker-ipc/contacts-ipc.ts:155 - 该 handler 在 deps.readDeviceSyncStatus 未提供时仍可能返回 undefined(optional chaining),但 renderer 侧会把返回值当作 ContactsDeviceSyncStatus 使用,容易触发运行期错误。建议与 setDeviceSyncEnabled 一样把 readDeviceSyncStatus 作为必需依赖并直接调用。
This issue also appears on line 160 of the same file.
if (!deps.setDeviceSyncEnabled) throwIpcError('INTERNAL', 'contacts sync is unavailable');
try {
await deps.setDeviceSyncEnabled(enabled);
return await deps.readDeviceSyncStatus?.();
} catch (err) {
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8dd46e3ae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 44 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx:153
- 这里在展示上次成功同步时,会把 lastRoute 为 null 的情况当作 relay(因为条件只检查 time/deviceName,route 通过三元表达式默认落到 relay)。如果历史状态文件来自旧版本或字段缺失,UI 会显示错误的传输路径。建议仅在 lastRoute 有值时才展示 lastSuccess。
route: t(
syncStatus.lastRoute === 'lan'
? 'settings.contacts.sync.route.lan'
: 'settings.contacts.sync.route.relay',
),
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 48 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/maker-core/src/contacts/sync/repository.ts:49
projection_json的 shape 校验过于宽松:这里只检查了“是数组且不超上限”,但数组元素可能是null/ 非对象 / 缺少id。随后captureContactsSnapshot()会对previous.*做row.id访问并构建Map,这会把“磁盘投影损坏”变成运行期 TypeError,而不是预期的 fail-closedContactsError。建议至少校验每个条目是对象且包含非空的id字符串(更深的字段校验可以后续再加)。
apps/desktop/src/main/maker-host/contacts-settings-store.ts:6- 文件头注释里写的是
<userData>/contacts-settings.json,但实现实际落在ownerScopedUserDataPath():有 owner 时是<userData>/owners/<hash>/...,无 owner 时是临时目录下的进程隔离路径。建议更新注释,避免后续排查/安全审计时误判设置的归属与隔离边界。
*
* 落盘文件: <userData>/contacts-settings.json
* { "enabled": false, "deviceSyncEnabled": false }
*
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 655ef419d2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@zqchris 👋 这个 PR 还有 3 条 review conversation 没 resolve(packages/maker-core/src/contacts/sync/materialize.ts / packages/maker-core/src/contacts/sync/validation.ts / apps/desktop/src/main/contacts-sync/driver.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: zqchris <chrisz83@gmail.com> # Conflicts: # packages/device-link/src/index.ts
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37d8eec57f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx:152
- syncSummary 在渲染 lastSuccess 时把 lastRoute 仅按
=== 'lan'分支判断;当 lastRoute 为 null(例如旧状态文件/异常路径未记录 route)时会错误落到 relay 文案,导致 UI 显示与真实传输路径不一致。建议仅在 lastRoute 有值时才展示 lastSuccess,否则回退到“ready”。
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 860c60db0f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/maker-core/src/contacts/sync/repository.ts:39
- projection_json 的校验目前只检查“六个字段是数组且长度不超限”,不校验数组元素是否为对象/是否含 id。这样磁盘数据一旦损坏(例如数组里混入 number/string),parseProjection 会通过,但 reconcile()/capture 时会在访问 row.kind / row.id 等字段处抛出运行时异常,导致无法按预期 fail-closed 到 ContactsError。建议在形状校验阶段至少保证每个元素是 record 且包含 string 类型的 id(其余字段可继续保持宽松),确保坏数据会被 parseProjection 拒绝并抛出受控错误。
apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx:152 - syncSummary 在渲染 lastSuccess 时对 lastRoute 使用了“非 lan 即 relay”的兜底;但 lastRoute 类型允许为 null(例如旧版本/损坏状态文件),此时会把未知路径错误显示为“加密中转/Encrypted relay”。建议仅在 lastRoute 为 'lan' | 'relay' 时才展示 lastSuccess,否则回退到 ready 文案,避免误导用户。
|
@zqchris 👋 这个 PR 还有 5 条 review conversation 没 resolve(packages/maker-core/src/contacts/sync/repository.ts / apps/desktop/src/main/contacts-sync/wire.ts / apps/desktop/src/main/contacts-sync/keyStore.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 61 out of 62 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx:153
- syncSummary 在渲染 lastSuccess 文案时,把 lastRoute 仅用
=== 'lan' ? lan : relay二分;当 lastRoute 为 null(例如旧版本状态文件、损坏/缺字段的持久化状态)时会被误显示为 relay,从而向用户展示错误的“传输路径”。建议在 lastRoute 为空时不要渲染 lastSuccess(或至少不要填充 route)。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9df8e1ddb9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@zqchris 👋 这个 PR 还有 3 条 review conversation 没 resolve(apps/desktop/src/main/contacts-sync/keyStore.ts / apps/desktop/src/main/contacts-sync/contactsSyncCodec.ts / apps/desktop/src/main/contacts-sync/contactsSyncCodecWorkerClient.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 63 out of 64 changed files in this pull request and generated no new comments.
Suppressed comments (2)
apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx:152
- 当 lastSyncAt/lastSyncDeviceName 存在但 lastRoute 为空(例如旧版本状态文件/读取回退/部分写入)时,这里会默认把 route 当作 relay 展示,可能误导用户。建议只有 lastRoute 为 'lan'|'relay' 时才拼接 lastSuccess,否则回退到 "ready"(或单独的无 route 文案)。
packages/maker-core/src/contacts/sync/types.ts:9 - 该目录下的 TypeScript 文件普遍使用单引号(例如 packages/maker-core/src/contacts/types.ts),这里新增文件使用双引号会造成风格不一致,且在启用 lint/prettier 的情况下可能直接触发检查失败。建议统一为单引号。
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95c45f9dca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@zqchris 👋 这个 PR 还有 1 条 review conversation 没 resolve(packages/maker-core/src/contacts/sync/validation.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Signed-off-by: zqchris <chrisz83@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/desktop/src/main/maker-ipc/contacts-ipc.ts:156
- CONTACTS_SYNC_STATUS_GET 直接透传 readDeviceSyncStatus() 的异常,会把裸 Error 返回给 renderer,绕过当前文件已有的 rethrowAsIpcError() 统一 [CODE] 协议(其他 handler 已做 try/catch)。建议与 ENABLED_SET/NOW 保持一致:捕获异常并 rethrowAsIpcError(err)。
|
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过。9700+ 行 E2E 加密联系人同步实现——X25519 ECDH + HKDF + AES-256-GCM 密码学层正确,密钥管理经 safeStorage 加密存储,LAN 传输带 HMAC 挑战响应认证,IPC 全部经过 assertTrustedAppRendererEvent 守卫,输入校验(validation.ts)有完整的边界和约束检查。5 处安全软命中均为测试桩/占位符(安全)。无 P0/P1。
|
LAN 上做 E2E 加密同步这套方案扎实——TOFU 密钥信任 + HMAC 挑战响应 + 按 transfer 做 AAD 绑定,攻击面控制得很紧;资源限制(并发连接数/worker 队列/解压上限)也都有,不怕被恶意 peer 打满。 |
这次改了什么
摘要
目标:让同一账号下已登录且在线的 Desktop 自动同步智能通讯录,局域网优先直连,失败后通过现有 Device Link 转发密文。
非目标:本 PR 不做 Mobile、不增加服务端存储,也不提供两端错开在线时的离线队列。
产品行为:每台 Desktop 在「设置 → 智能通讯录」开启一次;之后自动同步,也可点「立即同步」。
失效边界:账号、区域或连接切换时立即清理旧 owner 的在途状态,避免旧账号数据落到新连接。
验收口径:同步全程是确定性程序逻辑,不调用模型、不消耗 AI token;N 台设备经任意在线路径传播后最终收敛。
变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
safeStorage加密、按账号隔离落盘;Linux 若退化到不安全的basic_text后端则 fail closed;首次从同账号认证 relay 看到设备公钥后固定,后续公钥变化 fail closed。device-link:contacts:sync:v1channel;旧客户端忽略未知 channel。UI 变化
DESIGN.md§4 Component Stylings / Buttons / Cards & Containers:复用设置页卡片层级、按钮高度、圆角与 hover/disabled 反馈。DESIGN.md§10 Theme System & Token Reference:所有颜色使用现有语义 token,Light / Dark 共用实现。DESIGN.md§11 Voice & Content:状态和错误文案保持简短、可操作,并同步落地 zh-CN / en / ja / ko。DESIGN.md§14.4 Motion:同步旋转只挂在 HTML wrapper,使用语义animate-spinner,并在prefers-reduced-motion下保持静止。怎么验证的
自动验证
手工验证
未执行的验证
风险
风险分类
影响与回滚
contacts_sync_state单例表,保存可重复合并状态和本地投影;不改写既有业务表 schema。NSLocalNetworkUsageDescription;Windows / Linux 不改变原生权限或 Mobile runtime fingerprint。提交前检查
git commit -s,见 DCO)