Repository navigation
支持更多的数据类型同步 - #456
Open
leic4u wants to merge 25 commits into
Open
支持更多的数据类型同步#456leic4u wants to merge 25 commits into
leic4u wants to merge 25 commits into
Conversation
P0 + P1 的第一批实现,尚未接通实际同步链路。 P0(R1 修复):quick_commands 增加 is_builtin 列并回填 - 内置默认命令此前直接混在业务表里且无法区分来源,整表同步会造成 「同步一次多 30 条」的指数级重复 - 回填判定复用原迁移的 LOWER(TRIM(command)) 比对逻辑,仅标记 connection_id IS NULL 的全局命令 - QuickCommand 模型、行映射、INSERT/UPDATE/各 SELECT 均已带上该列 P1:同步范围配置与设置页 - 新增 SyncScopeKey / SyncKind / LockMode / SyncScopeDescriptor 常量表, 作为 UI 与同步引擎共用的唯一事实来源 - 新增 SyncScopeSettings 持久化在 settings.json(本身不参与同步,R7/Q9) - 连接/工作空间:AlwaysOn,置灰不可取消 - 钥匙串/LLM 提供商:PersonalOnly,OnetCloud 下强制取消并置灰 - 其余 11 项:统一默认关闭(Q1) - 设置 - 同步 新增「同步数据范围」分组,14 个勾选项 - Personal 下首次开启弹确认,提示旧客户端会同步失败(Q1/R2) - cloud_sync::data_type 新增 11 个类型常量(上线后不可改名) - i18n 三语 后续(未包含在本提交):sync_item_state / sync_type_status 表、 Q12 状态列、quick_command 与 settings_keybindings 的实际同步接线。 Refs: docs/design/sync-scope-selection.md
- 新增 sync_item_state / sync_type_status 两张表与仓储 - 新增 cloud_sync::scope 模块: - payloads:快捷命令 / SFTP 收藏 / 已保存 SQL / 笔记 / LLM 提供商 / 设置类载荷 - settings_payload:字段白名单抽取、应用与字段级三方合并(Q8) - settings_store:基于 settings.json 的读写后端 - personal:Personal 通道的 list/export/apply/mark_synced/delete - cloud:OnetCloud 通道的统一 SyncTypeHandler - PersonalSyncLocalRepositorySource 增加 scope 委托;未知 data_type 改为 跳过而非报错(R2 的未来兼容部分) - SyncEngine 新增 with_sync_scopes 注册新类型处理器 - SFTP 收藏仓储补齐按行读写入口(同步需要枚举全部收藏) - 设置页每行新增状态列(Q12)与主密钥未解锁提示(Q11) 已知缺口:笔记在 OnetCloud 通道未落地(notes crate 不能被 one-core 反向依赖); terminal-settings.json 的 custom_highlights 未纳入(需 terminal_view 内置规则清单)。
notes crate 依赖 one-core,反向依赖会成环,因此 one-core 只定义 NoteSyncAdapter trait,由 main 实现并注入两条通道。 - 新增 cloud_sync::scope::notes(trait + NoteSyncItem + 1MB 上限) - main 新增 FileSystemNoteAdapter:遍历笔记根目录的 .md 文件, 用相对路径作为稳定键,mtime 作为变更判据(笔记的 updated_at 不随正文更新) - Personal 与 OnetCloud 两条通道均支持笔记的 list/export/apply/delete 另附 ci.yml 临时补丁用于触发验证(随后回退)。
- 将 test: job 的 if 还原为 `${{ needs.classify.outputs.release_only != 'true' }}`,与 origin/main 及 windows_rdp_host 契约测试一致,修复 release.yml Windows 探针中 cargo test -p windows_rdp_host 因字符串不匹配而失败的问题
笔记同步此前用相对路径当身份:重命名后本地新路径没有同步状态(cloud_id=None) 被判为新增,而旧路径的云端记录无人引用被判为下载,结果本地两份、云端两条。 改为使用 notes crate 持久化在 documents.json 里的稳定 document_id: - rename_node 走 remap_prefix,重命名/移动只改路径、id 不变; - 同步键(local_key/state)改为 document_id,relative_path 仅用于还原树形结构; - 对端同步「同一篇笔记换了路径」时表现为移动/重命名,不会留下重复副本。 新增 NotesStorage::sync_documents/sync_write_document/sync_delete_document, FileSystemNoteAdapter 基于其重写;personal 与 cloud 两条通道同步切换。
main 的 v0.20.1 下线了唯一的内置提供商 Navop AI,ProviderConfig 不再有 is_builtin()。分支同步 LLM 提供商时的过滤条件因此编译不过(E0599, cloud.rs:436 / personal.rs:243)。提供商现在全部由用户创建,直接去掉过滤。
与笔记同理:此前用相对路径当身份,重命名会分裂成「旧记录 + 新记录」两条。 差异在于 queries/ 没有应用内重命名入口(sql-library 是另一棵目录树), 重命名只会发生在应用之外。因此索引除 UUID 外还记录正文哈希:枚举时若某 id 登记的路径已消失、且出现内容一致的新路径,就把 id 继承给新路径, 使应用内外的重命名都能保住身份。 - 新增 scope/saved_sql.rs:saved-sql-index.json 持久化索引 (saved_sql_documents / write_saved_sql_document / delete_saved_sql_document, 写入时同 id 在别的路径则移动文件) - SavedSqlPlainData 增加 document_id - personal.rs / cloud.rs 两条通道的 local_key、state 切换到 document_id, name 仅作文件名 - 移除被取代的 collect_saved_sql_files / read/write/delete_saved_sql 其余同步类型(connection/credential/workspace/quick_command/sftp_favorite/ llm_provider)以数据库主键为身份、设置类以常量组名为键,重命名不改动身份键, 不受该问题影响。
1) 记录 ID 为空 ⇒ 同类数据塌缩成一个无名 .json
export_item 在没有已知 cloud_id 时传了空串(unwrap_or_default),
而文件夹后端用 {id}.json 当文件名,于是同一 data_type 下所有记录
互相覆盖:只剩一条数据、version 每次同步都涨。改为生成新 UUID
(与 cloud.rs 既有写法一致),并忽略 id 为空的残破记录。
2) 同步范围列表的从未同步类型整行空白
status_text 查不到状态记录时返回 None,改为显示「从未同步」。
1) 始终同步的连接 / 工作空间(以及钥匙串)一直显示「未同步」 这三类由 PersonalSyncLocalRepositorySource 直接处理,不经过 SyncScopeSource,而写状态的 mark_success 只在后者里;它们压根没有 SyncTypeStatusRepository,所以状态行从未写入。 → 给本源加上 status / scope_env / user_id(与设置页读取用的 "personal"/"personal" 一致),并在 mark_synced(上传/更新/已是最新) 与 apply_connection|credential|workspace(下载)后写入成功状态。 2) 上次同步改为绝对时间 自动同步 60s 一轮,相对文案(刚刚/N 分钟前)几乎永远显示「刚刚」。 改为按本地时区输出 YYYY/MM/DD HH:MM:SS,i18n 文案同步加冒号。
此前只有 60s 周期扫描、同步目录监听、连接类事件、手动同步四个触发点; 笔记 / 已保存 SQL / 快捷命令 / SFTP 收藏 / LLM 提供商 / 各类设置项 都没有变更通知,改完只能等周期扫描。 本次先接入设置类:main 侧新增 AppSettings 观察者,为「白名单字段真的变了 且已勾选同步」的分组投递 LocalChanged 事件。同步开关 / 同步范围不在任何 白名单里,因此不会出现自激循环。 其余类型的写入点不在 main(快捷命令在 terminal_view,LLM 提供商与 SFTP 收藏也在别的 crate),需要 one-core 侧的通知通道,下一轮照 connection_notifier 的模式补。
- checksum 双口径修复:build_record_with_id 先规范化再取校验和, settings_group_checksum 覆盖完整信封,快照侧与记录侧同口径—— 没改内容不再重复上传、版本号不再每轮 +1 - 触发策略重做:1s 调度器 tick + 3s 变更签名轮询 + 设置观察者 + 连接通知 + 文件监听统一走「入队 → 冷却满一分钟合并跑 → 空闲 10 分钟兜底」;手动立即同步绕过冷却 - 同步范围勾选变化后热更新运行时并立即上传新类型(修「勾了但 立即同步还是没有」) - watcher 只入队不直接 drain,自写回环用「同步中 + 5s 宽限期」抑制 - 运行时起来先跑一轮,后端配置问题立刻暴露为状态
current_scope_signature(cx) 需要不可变借用 cx,原先写在 global_mut 的可变借用存活期内,编译报 E0502。把签名计算提到 可变借用之前。
- 变更合并窗口 60s→10s:单次变更最多等 10 秒即同步(v0.21.1 实测要等 20 秒以上,体感就是「没反应」);连续变更仍合并为一轮,不会改一次传一次 - 调度器增加「从未跑过轮次」兜底:reconcile 入队的启动轮若因主线程冻结 丢失,1 秒内补跑,不再让启动后的本地变更干等 - mark_synced 落校验和与 content_ts:修复设置类每轮误判「本地已变更」, 远端单方面改动被误判为双方冲突;也是 sync_item_state 校验和列一直 为空的根因(core 侧 trait + 三个实现 + conflict_resolver + 测试同步) - 触发链全链路中文日志:变更入队、被哪道闸门拦下、冷却延迟、开跑、 本轮完成/失败,排查直接看日志,不再靠时间线反推 - run_temporary_full_scan 记 last_pass_at:手动同步纳入冷却账目, 避免启动轮兜底在手动同步结束后又补一轮
- ProviderRepository::insert 此前把 item.id 显式绑进 INSERT,而同步拉取 路径(personal/cloud 两个 apply_provider)对新设备上的每条提供商记录 都以 id=0 调用:显式 0 占掉 rowid 0,第二条记录立刻 UNIQUE constraint failed: llm_providers.id,整个拉取轮次失败 (表现为 personal sync io error,上传侧一直正常)。 - 现在 id==0 改为绑定 NULL 让 SQLite 自动分配 rowid,回写 item.id 并 返回真实行号(与 QuickCommandRepository / insert_row 的既有范式一致); 非 0 显式 id(UI 的时间戳 id)行为不变。 - 返回的真实行号同时成为 sync_item_state 的 local_key,后续轮次走 update 分支;已受影响的设备下次同步自动补上失败的那条记录。 - 补两条回归测试:id=0 自动分配(多条插入互不撞主键)、显式非 0 id 保持原值。
用户实测 v0.21.3:新设备拉取不再报错,但设置页只显示一个提供商。 DB 取证显示两条提供商都已落库(第二条由 v0.21.3 修复后的 insert 自动分配行号下载),根因是 UI 不刷新: - 同步把记录直接写进业务表,不走各 UI 的新增/编辑入口,此前全链路 没有任何通知;设置页 LlmProvidersView 也不订阅 ProviderConfigEvent, 显示的一直是创建时那份旧列表。 - worker.drain_once 现回报本轮真正动过业务表的数据类型集合 (上传/双向更新/下载;只改同步状态的 mark_synced 与冲突快照不算), runtime 在轮次成功后据此通知:llm_provider → 清提供商管理器缓存 + 发 ProviderConfigEvent::Changed(手动同步链路同样处理)。 - LlmProvidersView 订阅 ProviderConfigEvent::Changed,事件到达即重载, 与 AI 聊天面板既有模式一致。 - personal_sync_status_from_task 改泛型:同步轮次带 touched 集合, 冲突解决等仍传 ()。 - 连接/工作空间/钥匙串不接入该通知:它们的变更总线同时是同步自己的 触发源,回发会形成自激循环,需单独设计(重新打开页面即可)。 - 补 worker 测试:下载轮次必须回报触及的数据类型。
CI 报 error[E0107]:本仓使用的 gpui-fast fork 的 Subscription 是 非泛型结构体(default_panel 的既有字段就是 Option<Subscription>)。 字段类型改回 Option<Subscription>;闭包签名里的 ProviderConfigEvent 保留(subscribe 的回调仍按事件类型标注)。
问题(审查 P0-1/P0-2/P0-3/P1-7/P1-10): - 键控回写失配:quick_command/sftp_favorite/llm_provider 的本地主键是真实 rowid,而 on_uploaded 通过 synthetic_id 反查 state,永不命中 → cloud_id 永不落库 → 每轮同步云端/本地各 +1 份副本。 - UpdateCloud 不落水位 + 写回后 content_ts=now → 本地始终显示「比云端新」, 每轮互相覆盖(乒乓),版本号无限增长。 - 新类型删除不传播:scope 差分缺失的本地行既不写 pending_cloud_deletions 也无墓碑,删完下轮又从云端复活。 - list_sync_data 不分页,PostgREST 默认 1000 行截断 → 误判「云端不存在」 重复上传;主密钥轮换重加密也会漏数据。 - decrypt_name 恒返回类型显示名,按名认领(新设备首同步)完全失效。 修复: - SyncTypeHandler 新增键控接口:on_uploaded_with_key / delete_local_with_key / touch_synced / persist_watermark(均带默认实现,旧 handler 零改动兼容); SyncableItem 新增 sync_key() 与 local_checksum()。 - ScopeSyncTypeHandler:首传前 ensure_state_row 写 cloud_id=NULL 占位; 上传/删除按键回写;persist 水位取远端 updated_at(ms→s),不再写本地 now; UpdateCloud 成功后同样落水位。 - list_local 末尾 finalize_local_snapshot:与 state 差分,本地已删且带 cloud_id 的写 pending_cloud_deletions(entity_type=data_type),随后 delete_missing 清理差分行;软删除消费走键控删除。 - decrypt_name 返回真实业务名(命令名/路径/提供商名/SQL 名/笔记标题/设置组)。 - supabase list_sync_data 按 1000 行分页拉全量,重加密通道自动受益。 - PendingCloudDeletionRepository 泛化 list_by_entity_type / remove_by_entity_type。 - sftp insert_row 改 upsert(ON CONFLICT DO NOTHING + 回查已存在行)。 - ScopeSyncTypeHandler 接入 SyncTypeStatusRepository:成功 mark_success、 失败 mark_error,OnetCloud 侧每类型状态可见。 - state.get() 改四键 WHERE 直查(原为全表 list 后 find,O(n²))。 - 补 7 个单测:键控回写/水位/差分删除/键控删/快捷命令外键恢复/SFTP 冲突 返回已有行/SFTP 连接缺失报错。
问题(审查 P0-5/P1-1/P1-2/P1-6/P3-14):
- Singleton 无认领:设置组在第二台设备无 cloud_id,planner 无条件走
to_upload → 云端出现第二条同类型记录,两台设备互相覆盖 settings.json。
- 冲突不可见:apply_settings 冲突分支 mark_error 后紧跟 mark_success 清错,
且 base_payload 推进为本地值 → 冲突信息永久丢失、本地静默胜。
- 上传不写 base:record_synced 的 base_payload 全传 None → 下一轮远端改动
时三方合并 base=None,降级为整体采用远端,本地修改被整组覆盖。
- 新类型删除无事件源:QC/SFTP/LLM/笔记/SQL 本地删除后无人发 LocalDeleted,
云端记录不墓碑,下轮差分又拉回来(删后复活)。
- SFTP 收藏外键错位:上传把 connection_key(local:{id} 设备本地键)塞进
connection_name 字段;下载原样写回 → 跨设备悬空。本地文件列表伪键
local-file-list:global 也被上传。更新分支不刷 connection_id/key。
- 快捷命令 connection_id 丢失:QuickCommand::new 重建后 connection_id
恒 None → 连接级命令变全局命令,反向同步抹掉源设备绑定。
修复:
- planner 新增 plan_with_singletons:Singleton 类型(设置六组)无 cloud_id
时若远端已有记录则配对认领(内容相同→mark_synced,不同→走下载三方合并),
不再重复上传;plan() 保留委托旧行为。
- worker:trait 新增 singleton_data_types / pending_local_deletions /
forget_local_state(默认空实现);run_locked_pass 先把 state↔local 差分
转成 LocalDeleted 事件再走既有墓碑流程;成功推墓碑后 forget_local_state
清基线,挂删除冲突的记录保留基线待重推。
- apply_settings 冲突分支:保留 mark_error(字段清单),不 mark_success、
base_payload 不推进;本地值照写盘,下一轮以 to_update_cloud 覆盖云端。
- export_item 设置分支上传前登记 base_payload=当前载荷。
- SyncScopeSource 注入 ConnectionRepository:sftp_plain_payload 优先
connection_cloud_id、连接未同步时退化真实连接名(绝不传 local:{id});
下载侧按 cloud_id/名称反解本地连接,解析失败按 name: 键落地不悬空;
过滤 LOCAL_FAVORITE_CONNECTION_KEY(常量移至 sftp_favorite_path.rs 共享);
apply_sftp_favorite 更新分支同时刷 connection_id/connection_key。
- quick_command_plain 带 connection_cloud_id;apply_quick_command 恢复
connection_id(解析失败降级全局命令)。
- 补 12 个单测(planner 认领 5 个 + scope source 差分/过滤 7 个)。
问题(审查 P1-4/P1-5/P1-8/P1-9/P2-11): - canonical_json 手写转义只处理 5 种字符,载荷含其他控制字符(如 \u001b) 时产生非法 JSON 明文 → 读取侧 serde 解析永久失败。 - write_settings_atomically 固定 .tmp 文件名:UI 线程与同步 worker 并发 写 settings.json 时互相截断,损坏后被 quarantine 回默认值。 - notes sync_write_document / saved_sql write_saved_sql_document 对 relative_path 无 ../与绝对路径校验,恶意/损坏的云端记录可越界写文件。 - is_builtin 回填仅按 30 条命令文本匹配,用户自建同文本命令会被误标内置 → 永不参与同步。 - migration.rs MIGRATIONS 为私有,无法写「构造旧库跑迁移」回归测试。 修复: - canonical_json 重写为递归键排序 + serde_json::to_string(完整转义), 补控制字符回归测试。 - 临时文件名加 pid+毫秒后缀唯一化,并发写互不截断。 - notes 侧复用既有 validate_relative_path;saved_sql 新增 validate_sync_relative_path(拒绝空/绝对路径/../非 .sql),写失败不再 静默 return。 - 回填判据收紧为 name+command 双匹配且 pinned=0(30 个内置名小写清单); 补回归测试:用户同名同文本命令、置顶命令不被误标。 - MIGRATIONS 改 pub(crate) 供测试标记旧版本。
问题(审查 P0-4 及 UI 项): - 设置同步只写 settings.json 不刷新进程内 AppSettings global:UI 读旧值 直到重启;任何 update_and_save 以内存旧值整份覆盖,把同步结果回滚, 观察者还会把旧值反向上传覆盖其他设备。 - 受控 Checkbox 勾选后不重绘(SettingItem 不订阅全局设置变化)。 修复: - personal_sync_runtime:notify_synced_data_changed 覆盖设置六组——同步 落盘后先清 SettingsSyncBaseline(防旧基线把远端值误判为本地变更再触发 一轮同步),再校验 settings.json 可反序列化(损坏则保留内存值并 warn), 最后 AppSettings::load() + set_global 刷新。 - AppSettings::config_path 改 pub 供运行时探测。 - sync_scope_settings Checkbox on_click 后 window.refresh()(项目惯例)。 - 附带 llm/storage 测试断言 fmt 对齐。
Owner
审查后修复说明(4 个 commit)应维护者要求直接在本分支修复了审查发现的问题,全部本地验证通过( 1. OnetCloud 通道:身份映射与水位(c05073e17 之前)
2. Personal 通道:单例认领与设置合并
3. 落盘安全
4. 运行时与 UI
已知残余(未在本次处理)
|
两类载荷均以用户主密钥端到端加密(与 Personal 通道同一 encrypt_blob 路径),上 OnetCloud 云不引入新的明文暴露面,故取消 personal_only 限制: - LockMode 删除 PersonalOnly 变体,credential/llm_provider 改 Optional, effective() 不再按 provider 剔除,force_disable_personal_only 空操作化 - ScopeSyncTypeHandler 补 CREDENTIAL 分支:枚举过滤 sync_enabled/team_id, 载荷复用 CredentialPlainData(private_key_path 不入载荷),下载落地保留 本机私钥路径与创建时间,删除走键控路径 - with_sync_scopes 不再跳过 CREDENTIAL;主密钥轮换全量重加密自动覆盖 - 设置页去掉 OnetCloud 置灰逻辑,钥匙串/LLM 描述文案改为双通道表述 - 新增测试:枚举过滤、下载 insert/update 保留本机字段
Owner
钥匙串与 LLM 提供商放开双通道同步(a07ad6651)按需求把 credential / llm_provider 两个 personal_only 类型放开到 OnetCloud 通道: 安全性前提:两类载荷与 Personal 通道走同一个 改动:
验证: |
启动同步通道命名的品牌化迁移(用户可见文案早已是「Navop 云同步」): - SyncProvider::OnetCloud → NavopCloud;HomeSyncRoute 同步更名 - 持久化值 onet_cloud → navop_cloud,serde alias + from_str 兜底兼容 旧 settings.json,升级无感(新增回归测试) - i18n key Settings.Sync.Provider.onet_cloud → navop_cloud(文案不变) - 函数/测试随语义更名:should_auto_navop_cloud_sync、 reencrypt_navop_cloud、wipe_navop_cloud 等 - 设计文档同步更新 验证:one-core --lib 901 passed;main --bins 721 passed; RUSTFLAGS=-D warnings check 全绿
CI linux 上 workspace_hierarchy_engine_tests 偶发「下载工作空间失败: 加解密错误: 解密失败」:ensure_unlocked 读全局 RAW_MASTER_KEY 与并行 改写该全局的测试互踩,把别人的测试密钥注入了本测试的 service。 双保险修复: - ensure_unlocked:service 已解锁时不再重新注入全局密钥(调用方可能 自备密钥,例如 set_master_key_directly 配好的 service) - workspace_hierarchy_engine_tests 的 Device::sync 持 crypto_test_lock, 与其他 clear/set 全局密钥的测试互斥(与 engine 测试区既有做法一致) 验证:one-core --lib 连跑三轮 901 passed;rustfmt --check 通过
# Conflicts: # crates/core/src/cloud_sync/engine.rs
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
支持 #340 需求
云同步升级:自选范围 · 变更触发 · 状态可见
主分支当前的个人云同步按固定类型参与、定时轮询。本分支将其升级为一套用户可自选、变更驱动、状态完整的同步机制。
1. 同步数据范围可自选
2. 六类数据纳入选择性同步
主分支不参与(或不通过该机制参与)同步的以下类型,本分支全部接入:
3. 变更触发的同步机制(替代定时轮询)
4. 同步状态与界面
5. 交付质量保障