feat(settings): 配置跨机器同步(本地导入导出 + WebDAV) - #747
Merged
Merged
Conversation
Moving to a second machine meant retyping every model provider, agent setting and task template by hand. Backup covers the whole database, so it is the wrong tool: it carries conversations and uploads, and restoring it overwrites the target machine. This adds a configuration-only snapshot of six domains (model providers, agent settings, custom agents, quick messages, task templates and the portable app preferences) that can travel either as a JSON file or through a WebDAV server the user already owns. Applying a snapshot upserts by natural key and never deletes local rows, so a machine that has extra entries keeps them, and auto-increment ids stay put for the foreign keys that point at them. Automatic sync only uploads: pulling from the server is always a deliberate action, so a stale remote can never silently overwrite local configuration. Uploads are driven by hashing the snapshot on a timer rather than marking the database dirty, because configuration writes are spread across dozens of commands. The snapshot is unencrypted, so the WebDAV credentials themselves are excluded from it and the UI says as much.
…dential scope The review of xintaofei#747 turned up two defects that make half the feature unusable and one that leaks a credential: - `ConfigImportPreview` promised `importable`/`blockedReason`, which the Rust command never sends. `disabled={!preview.importable}` therefore read `undefined` on every real file and the import confirm button was permanently disabled. There is no "previewed but not importable" state — `peek` runs the same parser and schema check the import does, so an unreadable file already comes back as a rejected promise with a `configSync.error.*` key. Drop the phantom fields (and the now-unused `importBlocked` string), and show the backend's recount instead of the file's self-reported `manifest.counts`. - The master WebDAV switch only moved local state, and every other control including Save lives inside the block it hides — so sync could be turned on but never off. It now persists on click, like the proxy and launch-at-login switches in the same settings page. - An empty password field meant "keep the stored one" unconditionally, so editing the URL alone was enough to send a saved app-password to another server. The password is now bound to the account it was typed for; changing host or user requires retyping it, and the field's hint stops offering to keep it. Also: reset the upload hash baseline when the remote target changes (otherwise retargeting left the new location empty until some unrelated setting changed), skip auto-sync ticks until a server URL exists, read the real host name on unix (`HOSTNAME` is never exported, so every snapshot from a unix desktop was signed "unknown"), guard the forbidden key list against the key the code actually writes, and tell the user a restart is needed for the parts of a restore the running window caches. `ConfigExportSummary` and `ConfigImportResult` were also declared with fields the backend does not return; corrected to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clearing `last_uploaded_sha256` inside `save_settings_core` closed the "retargeting leaves the new remote empty" hole only for a clean, serial save. It is a second write, so it can fail silently (`save_state` only warns), be interrupted between the two writes, or be undone by a background upload that was already in flight against the OLD target and writes its hash back afterwards. Each of those leaves the new destination suppressed and permanently empty — the bug the reset was there to fix. Record the destination alongside the hash instead. Suppression now needs both halves to match, so a changed target simply stops matching and there is nothing to reset, nothing to interleave with, and no second write to lose. A state row from before the field existed reads as `None` and suppresses nothing, costing one redundant upload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…encryption The review left four design-level gaps open. This closes all four. **It only ran on the desktop.** The commands were `#[tauri::command]`-only and the panel returned `null` outside Tauri, so a feature whose entire point is moving configuration between machines was unavailable on the half of the product that runs on a server. Every entry point now has an Axum handler over the same `_core` function, and `codeg-server` spawns the auto-sync loop. File transfer goes by content rather than by path — a snapshot is tens of KB, so `backup.rs`'s multipart upload, download tickets and temp-file reaping would be machinery with nothing to carry. **The rollback snapshot was written and then unreachable.** Every import saved one and returned its `rollbackPath`, and nothing could list or apply it. There are now list/apply commands behind an opaque id that is validated against an alphabet before it is ever joined to a path, a panel section that appears only when there is something to undo, and an undo that saves its own rollback point first. **The WebDAV password sat in plaintext in `app_metadata`** — and so in every backup archive that database is packed into — while the snapshot itself had a reserved `encryption` field that nothing ever set. Both secrets moved to the store this codebase already uses for credentials (OS keyring on desktop, the 0600 token file on a server), with a migration that retries until the row is clean. Encryption is opt-in: AES-256-GCM under an Argon2id key, in a JSON envelope so the remote file keeps its `config.json` name and the import path can recognise it from the value it already parses. The upload baseline is stamped with the protection in force, so turning encryption on forces a re-upload instead of leaving the plaintext copy sitting on the remote. **`peek` decoded the manifest but not the payload**, so a hand-edited file previewed as valid and then aborted mid-apply. Each domain now carries a `validate` beside its `collect`/`apply`, called from `parse_snapshot` — the one door every snapshot enters through — with a test asserting validate and apply agree on every domain, and another proving that agreement is not vacuous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**A forged manifest could switch encryption off.** `open_after_download` treated the manifest as the authority on whether the payload was wrapped, and the manifest is just a second file on the same share — so the operator this feature encrypts *against* could replace both halves with a plaintext snapshot of their choosing and a matching checksum. Every check passed and attacker-chosen provider endpoints and API keys landed in the local database while the switch read "encrypted". The manifest is now believed when it says "encrypted" and not when it says "plaintext"; the local switch decides that direction. **An unreadable keyring erased the secrets.** `credentials::load` mapped a failed read to `""`, and `""` means DELETE on the way back out, so a denied keychain prompt plus any unrelated save — nudging the interval — permanently destroyed the passphrase the remote copy is encrypted under. Reads now distinguish absent from unreadable, and a save that cannot read the store refuses instead of rewriting it. **The legacy-password migration clobbered the row.** It serialized this build's own struct over whatever was there, so a save landing in the window lost its edits — pairing the OLD server URL with the NEW password the keyring had just taken, the exact combination `merge_settings` refuses to create — and a row written by a newer build lost the fields this one cannot parse. It now removes a single key from the row as it stands. **The rollback list offered ids the resolver refused.** The lister took any file stem; the resolver holds ids to an alphabet. One copy through a file manager (`config-….json` → `config-… (1).json`) produced a Restore button that answered "no longer on this machine" about a file sitting right there. Both now share one definition. **An import wrote its rollback point to the wrong directory.** `save_rollback` called `rollback_dir()` while its callers took a directory parameter, so an undo driven against one directory wrote and pruned another — and the test suite was evicting snapshots from the developer's real `~/.codeg`. **The preview over-promised.** `count_entries` counted every key of the `preferences` object, but the applier writes only allowlisted keys holding strings, so the dialog said "3 preferences" and the import wrote 1. Counting is now a function on the domain table beside `apply`, held to it by a test, the same shape `validate` already uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**A withdrawn passphrase was stored anyway.** Turning the encryption switch back off hid the field without clearing its state, so the next Save — about anything at all — submitted the passphrase the user had just decided against, and `hasPassphrase` then claimed a protection they never confirmed. The switch clears what was typed; the one already on file survives, because it is what still decrypts the copy on the remote. **The master switch behaved like a Save button.** It persists on click by design (every other control lives inside the block it hides), but it was persisting the whole live form — so typing a password and then turning WebDAV off instead of saving wrote that password to the keyring, cleared the field, and said nothing. It now sends `null` for both secrets and leaves the fields alone. **A dismissed file picker could deadlock the panel.** `pickLocalFile` resolved on `change` and on `cancel` and otherwise never settled, while the caller holds `busy` for the duration — so on any engine that does not dispatch `cancel` (WebKitGTK, which a Linux desktop build runs) dismissing the import dialog left every button in the section disabled until the page was remounted. Regaining window focus with no file attached now ends it too. Also: the runtime split is tested where it lives. The panel's own tests mock `@/lib/config-sync` wholesale, so their `desktop = false` could not constrain which branch a real browser takes — it only guards against a re-introduced `return null`. `src/lib/config-sync.test.ts` now pins the branch itself: which command each runtime calls, that a browser never reaches a `*_file` command, and that a dismissed picker settles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**The save guard could lock the feature.** Refusing to save whenever the credential store would not open fixed the erasure, but it gated EVERY save — so on a Linux desktop with no Secret Service running, or after a denied macOS keychain prompt, the master switch and the interval could not be changed either, permanently. The root cause was narrower than the guard: the save wrote back secrets it had only just read. It now writes only what the input actually decided — a new password, a new passphrase, or the erasure an account change requires — so a save that carries no credential touches the store at all, and `""` can no longer travel out of a failed read as "delete this entry". `same_account` moves into one function, because the two callers now both act on it: `merge_settings` decides whether the password is kept, and `save_settings_core` decides whether it is erased. Those must not differ. **The file-picker fallback could eat a real selection.** Resolving on window focus cannot tell "the dialog closed" from "the dialog is open and the user alt-tabbed", so with a non-modal chooser it settled the promise under someone who was still choosing — and the file they then picked arrived on a dead promise and vanished silently. Guessing is the wrong fix for a promise that may not settle; not gating the UI on it is. `handlePickImport` no longer holds `busy` across the dialog, so an unanswered picker costs a hidden input instead of the whole panel, and `pickLocalFile` is back to `cancel` as its only dismissal signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Almost every holder is an async test that awaits a database while it is held, and `clippy::await_holding_lock` is right to refuse that. A `tokio::sync::Mutex` is the one that may cross an await — and it has no poisoning either, so a failing test no longer risks cascading into the rest of the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t name `system-network-settings.test.tsx` captures the provider's live `app_update_state` handler by overwriting a single module-level slot from its `subscribe` double — for any event, because the double took `_event` and dropped it. That held only while the page had exactly one subscriber. This PR embeds `<ConfigSyncSettings />` in that page, and the panel subscribes to `config_sync_status` on mount; whichever landed last won the slot, and the panel's did. `closes a stale rollback dialog when an upgrade becomes staged` then pushed `ready_to_restart` into the config-sync status handler instead of the update provider's. The update state never advanced, so `canRollback` stayed true, the auto-close effect never fired, and the case failed on a dialog that was still open — an assertion nowhere near the cause. The real `Transport.subscribe(event, handler)` routes per event, so both handlers stay separate in production: the double was lying, not covering for a product bug. Route by name in the double too, which also keeps the next section added to this page from silently stealing the handle again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
codeg work task |
xintaofei
added a commit
that referenced
this pull request
Sep 17, 2026
Your configuration can now travel between machines — model providers, agent settings, custom agents, quick messages and task templates, as a file or through your own WebDAV server, optionally encrypted. A fast reply no longer locks up the interface: the streaming window now scales with the answer it re-renders, and a two-minute reply at full speed costs about a tenth of the rendering work it used to. The composer learns two shortcuts — recall a sent prompt with the arrow keys, and click a queued message into the turn that is already running — while Gemini CLI, OpenCode, Cline and Kimi all get a deep adaptation pass. ## New - **Configuration sync between machines** — a configuration-only snapshot of model providers (with their keys), agent settings, custom agents, quick messages, task templates and app preferences, carried by an exported file or by a WebDAV server you already own, on the desktop and in server mode alike; conversations and uploads stay out of it, applying a snapshot never deletes what the target machine has, and automatic sync only ever uploads. (#747, @galact-byte) - **The uploaded snapshot can be encrypted with a passphrase** (AES-256-GCM), and both the WebDAV password and the passphrase live in the system keyring instead of the database. - **Every import and restore leaves a way back** — the last ten configurations this machine had are kept, reachable from the file-actions row, and undoing one is itself undoable. - **Recall a sent prompt with ArrowUp / ArrowDown** in the composer, the way a shell history works. (#744 requested by @evepupil, #756, @dawNotPoi) - **Click a queued message to insert it into the running turn** — on any session with a live-feedback channel, instead of waiting for the turn to end. (#751, @leehom0123) - **A work task can choose the branch it is created for** — the worktree starts at that branch's tip, the merge lands back on it, and a delivery opens its pull request against it, so a task no longer depends on what the project folder happens to be checked out on. (#760 requested by @wht300) - **OpenCode has a "Behavior & compaction" settings card** — default agent, sharing, auto-update, filesystem snapshots, sub-agent depth, auto-compaction and tool-output limits, written to `opencode.json`, with any field left unset still following OpenCode's own defaults. - **Cline's provider settings now split the two ways cline actually authenticates** — Cline Usage-Billing, ClinePass and ChatGPT get the `cline auth <id>` command to run instead of a key field, bring-your-own providers keep their key fields, and anything configured outside codeg keeps its own row. - **Gemini sub-agent transcripts now show up** — a sub-agent's own conversation is discovered alongside the session that launched it. - **OpenCode's `lsp` is now a permission of its own**, so you can decide it separately from the rest. ## Improved - **Updated bundled agents:** Claude Code ACP 0.78.0 (Claude Agent SDK 0.3.270), Codex ACP 1.12.0 (Codex CLI 0.154.0), Gemini CLI 0.60.0, Kimi Code 2.0.0, Grok 1.0.34, Qoder 1.1.54. - **A reply streaming at full speed no longer freezes the interface** — the flush window now grows with the answer being re-rendered and is timed per conversation, taking a two-minute reply at 300 tokens/s from 519M re-rendered characters down to 60M while one busy tab stops holding back the others. (#589 reported by @Lparksi, #716, @Adam-Dalloul) - **Settings → System is now one "Data & sync" card** — configuration sync, backup and restore as three tabs, with the pre-restore snapshots sitting under the restore flow they undo instead of in a card of their own. - **Kimi finds skills it was already loading** — codeg now reads all four of Kimi's skill roots, including the shared store pi and cline install into, and writes project skills at the repository root where Kimi actually looks. - **A `kimi-k3` session's context gauge is correct** — history was measured against a 256K window on a model that has 1M. - **Opening a historical conversation now shows the session coming up** — the composer names the agent being prepared and holds placeholders where the model and config chips will land, instead of looking dead for the several seconds the session takes to establish. - **Gemini transcripts are read the way Gemini CLI 0.60 writes them** — sessions that showed no messages at all now open, and token usage and the context window (1M, 256K for Gemma) are counted correctly rather than reading about 5% high. - **A Gemini `[MODE_UPDATE]` line no longer lands in the conversation as a message** — it switches the session's mode, which is what it was for. - **Gemini's run, read, search, fetch and web-search calls now render as their own cards** instead of a bare title. - **An OpenCode tool call keeps its real name live and after a reload** — `glob` was showing as grep, `lsp_diagnostics` as read and any MCP tool taking a `query` as web search, and a live read now keeps the line numbers the history view shows. - **An OpenCode session still named "New session - …" now takes its title from the first message** — OpenCode's own rename fails silently when its title model is unreachable, which left the placeholder there forever. - **A reopened OpenCode session shows what actually happened** — context-compaction dividers, sub-task summaries, assistant errors and aborts, and a question you declined no longer rendered as a red error card. - **A Cline backup no longer carries `secrets.json` or your provider keys**, and its new SQLite session store is archived as a coherent database rather than as loose files that restore corrupt. - **An agent's per-launch temp directory is created private to you**, and codeg refuses a scratch root it did not create itself, so no other local account can own the directory an agent unpacks into. ## Fixed - **An agent that opens a unix socket starts again on macOS** — 0.30.8's per-launch temp isolation pushed the socket path past the kernel's 104-byte limit, so the agent died during startup and the session failed after a minute with a misleading "turn off MCP support" hint. (#754 reported by @Liang-HZ) - **Delegating to another agent no longer fails silently** — on a long temp path the broker published a socket nothing on the system could dial, while the status indicator stayed green and every companion failed to reach it; the path is now refused loudly and falls back to a short one. - **Cline conversations open with their messages again** — cline 3.x moved its transcripts to a new session store, so every conversation resolved to zero turns, and a finished live reply also lost its model, token usage and completion time. - **Cline's provider dropdown in the composer is withheld when the launch pins the provider** — it could only ever fail with "Cannot change provider", and a pick saved from clicking it is no longer replayed on later sessions. - **A ClinePass or ChatGPT sign-in is no longer billed as a plain Cline account** — a leftover key could short-circuit cline's auth gate without the sign-in ever taking effect. - **AWS Bedrock and GCP Vertex leave Cline's key list** — they need more than a single API key and never worked from here; `cline auth bedrock` still sets them up, and codeg now leaves such an entry alone instead of retargeting it. - **An OpenCode plugin loaded from a path is reported as loaded, not missing** — a `file:///…`, absolute or relative plugin came back "not installed" behind an Install button that could only fail, and preflight raised a failure that could never clear. (#745 reported by @LiDongYang743) - **A Windows `file://C:\…` plugin path resolves** instead of being read as a host name, and a plugin under a directory containing `@` keeps its full name instead of being truncated or silently dropped. Thanks to @galact-byte, @Adam-Dalloul, @dawNotPoi and @leehom0123 for contributing to this release, and to @Liang-HZ, @LiDongYang743, @evepupil, @Lparksi and @wht300 for the reports. ----------------------------- # 发布版本 0.30.10 配置现在可以在多台机器之间同步了——模型供应商、智能体设置、自定义智能体、快捷消息和任务模板,可以导出为文件,也可以走你自己的 WebDAV 服务器,还能选择加密。 高速输出不再拖垮界面:流式刷新窗口会随着正在重绘的回复变宽,两分钟的满速回复渲染开销降到原来的约十分之一。 输入框新增两个用法——用方向键调回发送过的提示词、点一下排队消息直接插进正在进行的回合,同时 Gemini CLI、OpenCode、Cline、Kimi 都做了一轮深度适配。 ## 新增 - **配置跨机器同步**——只包含配置的快照:模型供应商(含密钥)、智能体设置、自定义智能体、快捷消息、任务模板和应用偏好,可以导出成文件,也可以通过你自己的 WebDAV 服务器传输,桌面端与服务器模式都支持;会话和上传文件不在其中,导入时只增量合并、绝不删除目标机器上已有的条目,自动同步也只上传。(#747,@galact-byte) - **上传的快照可以用口令加密**(AES-256-GCM),WebDAV 密码和加密口令改存系统钥匙串,不再落在数据库里。 - **每次导入和恢复都留有退路**——保留这台机器最近十份配置,从文件操作行即可进入,撤销本身也可以再撤销。 - **输入框支持用 ↑/↓ 调回发送过的提示词**,和 shell 的历史记录一样。(#744 由 @evepupil 提出,#756,@dawNotPoi) - **点击排队消息即可插入正在进行的回合**——只要该会话有实时反馈通道,不必等这一轮结束。(#751,@leehom0123) - **待办任务可以指定基于哪个分支创建**——工作树从该分支的最新提交开始,合并也回到该分支,交付时的 PR 同样以它为基线,不再取决于项目目录当前检出在哪。(#760 由 @wht300 提出) - **OpenCode 新增「行为与上下文压缩」设置卡片**——默认智能体、分享、自动更新、文件快照、子智能体层级、自动压缩和工具输出上限,保存后写入 `opencode.json`,留空的项仍沿用 OpenCode 自己的默认值。 - **Cline 供应商设置区分了它真正的两种认证方式**——Cline Usage-Billing、ClinePass、ChatGPT 改为给出 `cline auth <id>` 命令而不是密钥输入框,自带供应商保留密钥字段,在 codeg 之外配置的供应商单独成行。 - **Gemini 子智能体的会话记录现在能被识别**,会随发起它的会话一起出现。 - **OpenCode 的 `lsp` 成为独立的权限项**,可以单独决定是否放行。 ## 改进 - **内置智能体版本更新:** Claude Code ACP 0.78.0(Claude Agent SDK 0.3.270)、Codex ACP 1.12.0(Codex CLI 0.154.0)、Gemini CLI 0.60.0、Kimi Code 2.0.0、Grok 1.0.34、Qoder 1.1.54。 - **满速输出的回复不会再让界面卡死**——刷新窗口会随着正在重绘的回复变宽,并且按会话独立计时;按 300 tokens/s 回放,两分钟的回复重绘字符数从 5.19 亿降到 0.6 亿,某个繁忙标签页也不会再压住其它标签页的输出。(#589 由 @Lparksi 反馈,#716,@Adam-Dalloul) - **设置 → 系统 合并为一张「数据与同步」卡片**——配置同步、备份、恢复三个标签页,恢复前的快照也移到它所对应的恢复流程下方,不再单独占一张卡片。 - **Kimi 能找到它本来就在加载的技能**——codeg 现在读取 Kimi 全部四个技能目录,包括 pi、cline 共用的那个技能库,项目技能也改写到 Kimi 真正扫描的仓库根目录。 - **`kimi-k3` 会话的上下文用量显示正确**——历史侧此前按 256K 窗口计算,而该模型是 1M。 - **打开历史会话时会显示会话正在准备**——输入框会说明正在准备哪个智能体,并在模型和配置胶囊的位置先放占位骨架,不再在建立会话的数秒里看起来像是失灵了。 - **Gemini 会话按 Gemini CLI 0.60 的写入方式解析**——此前完全显示为空的会话现在能正常打开,Token 用量和上下文窗口(1M,Gemma 为 256K)也计算正确,不再偏高约 5%。 - **Gemini 的 `[MODE_UPDATE]` 不再作为一条消息出现在对话里**,而是切换会话模式——这本来就是它的用途。 - **Gemini 的执行、读取、搜索、抓取和网页搜索调用现在有各自的卡片**,不再只是一行标题。 - **OpenCode 工具调用在实时和重新打开后保持同一身份**——此前 `glob` 显示成 grep、`lsp_diagnostics` 显示成读取、带 `query` 的 MCP 工具显示成网页搜索;实时读取现在也保留历史视图里的行号。 - **仍叫「New session - …」的 OpenCode 会话现在以首条消息作为标题**——OpenCode 自己的重命名在标题模型不可用时会静默失败,占位名就此一直留着。 - **重新打开的 OpenCode 会话能还原真实经过**——上下文压缩分隔、子任务摘要、助手侧的报错与中断都在,被你拒绝的提问也不再渲染成红色错误卡片。 - **Cline 的备份不再打包 `secrets.json` 和你的供应商密钥**,其新的 SQLite 会话库也会作为完整数据库归档,而不是按散文件打包导致恢复后损坏。 - **智能体每次启动的临时目录以私有权限创建**,且 codeg 会拒绝不是自己创建的临时根目录,避免本机其他账户持有智能体解压和执行文件的目录。 ## 修复 - **在 macOS 上会创建 unix socket 的智能体可以正常启动了**——0.30.8 的按启动隔离临时目录把 socket 路径顶过了内核 104 字节上限,智能体在启动阶段就退出,会话挂起约一分钟后失败,并给出误导性的"关闭 MCP 支持"提示。(#754 由 @Liang-HZ 反馈) - **委派给其它智能体不会再静默失败**——临时目录路径过长时,broker 会发布一个系统上谁都连不上的 socket,状态指示灯却是绿的,所有伴生进程都连不过去;现在会明确拒绝并回退到短路径。 - **Cline 会话重新能看到消息内容**——cline 3.x 换了会话存储位置,导致所有会话都解析为零轮对话,刚刚完成的实时回复也拿不到模型、Token 用量和完成时间。 - **当启动已锁定供应商时,输入框里的 Cline 供应商下拉不再出现**——它只会报 "Cannot change provider",此前误点保存下来的选择也不会再被回放到后续会话。 - **ClinePass 或 ChatGPT 登录不会再被当成普通 Cline 账号计费**——残留的密钥会绕过 cline 的认证入口,让登录实际上没有生效。 - **AWS Bedrock 与 GCP Vertex 从 Cline 的密钥列表中移除**——它们需要的不止一个 API Key,在这里从未真正可用;仍可用 `cline auth bedrock` 配置,codeg 现在会原样保留这类条目而不是改写它。 - **以路径声明的 OpenCode 插件会显示为已从磁盘加载,而不是"未安装"**——`file:///…`、绝对路径和相对路径插件此前都显示为缺失,安装按钮必然失败,预检也会报一个永远无法消除的错误。(#745 由 @LiDongYang743 反馈) - **Windows 的 `file://C:\…` 插件路径可以正确解析**,不再被当成主机名;位于含 `@` 的目录下的插件名也不会再被截断或被去重悄悄丢掉。 感谢 @galact-byte、@Adam-Dalloul、@dawNotPoi、@leehom0123 为本次发布做出的贡献,也感谢 @Liang-HZ、@LiDongYang743、@evepupil、@Lparksi 和 @wht300 的反馈。
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.
关联 #633(第二项诉求)
背景
换一台机器就要重配一遍:模型服务商和 API 密钥、智能体设置、自定义智能体、快捷消息、任务模板。现有的备份是 GB 级加
密归档,含会话、上传文件、transcript,只能手动导出,不是"每隔几分钟自动跑一次"的单位。
改动
新增配置快照——只含六个配置域的 JSON,几十 KB:
modelProviders/agentSettings/customAgents/quickMessages/taskTemplates/preferences。可导出成单文件,也可推到用户自有的 WebDAV。几条定死的语义:
展示来源设备、时间、各域条目数,用户确认后才应用。
agent_setting.model_provider_id这类外键。代价是远端删除不向本地传播,已在 UI 文案里写明。
preferences用允许清单而非排除清单(portable_keys::PORTABLE_PREFERENCE_KEYS)。新增的app_metadatakey默认不参与同步,由白名单交集测试守护凭据不外流。同步自身的 WebDAV 凭据被排除在快照之外——否则两台机器会互相覆盖凭据,
变成同步环。
work_task_settings:其folder_id外键挂在设备本地的项目记录上,跨机器会指向错误项目。只同步work_task_template。实现分布:
commands/config_sync/—domains(快照含什么)、portable_keys(哪些偏好可迁移)、snapshot(采集 / 校验 /应用 + 本地回滚副本)、
local_io(单文件导入导出)、webdav_sync(设置、远端布局、上传下载)、auto_sync(周期哈希比对)。业务逻辑走
*_core函数收普通引用,桌面命令、未来的 Axum handler、后台调度器共用同一份实现。37 个 Rust 单测。
network/webdav.rs— 最小 WebDAV 客户端(PROPFIND / MKCOL / GET / PUT)。ensure_dir逐级 MKCOL:RFC 4918下中间集合缺失时 MKCOL 返回 409,一次性建多级在真实服务器上建不出来。
WebdavError到AppCommandError的映射带i18n key,401 / 403 / 507 / 5xx / 传输失败各自有可读文案。
{远程目录}/v1/{配置档}/{config.json,manifest.json},先写 config 再写 manifest:WebDAV 没有多文件事务,上传中断必须让旧 manifest 指向一致的字节,绝不能应用半截配置。
v1这一级留给未来不兼容的协议并存。防抖"——配置写入散在几十个命令里,没有单一咽喉。手动「立即同步」绕过哈希抑制,因为用户按它通常正是怀疑远端过期。
hasPassword: bool,不引入第二真相源。「测试连接」收的是未保存的表单,凭据可以先验证再落库。
ConfigSyncSettings设置页(设置 → 系统与网络 → 配置同步)四张卡片:同步范围(包含 / 不包含两栏对照)、本地备份、WebDAV 同步(坚果云 / Nextcloud / 群晖 / 自定义地址预设,仅填模板不做服务商分支)、自动同步。复用既有 Card /
Button / Switch / AlertDialog,无自定义样式。16 个前端测试。
configSync.*文案 10 个语言全量新增。验证
真机跑通坚果云(
https://dav.jianguoyun.com/dav/):测试连接对尚不存在的目录PROPFIND → 404视为连接成功;上传时
MKCOL /codeg → /codeg/v1 → /codeg/v1/default各返 201,随后PUT config.json→PUT manifest.json;从远端恢复GET manifest预览后GET manifest + config,校验和通过并应用。cargo test --features test-utils→ 3576 通过cargo clippy --all-targets --features test-utils -- -D warnings→ 通过;--no-default-features --bin codeg-server --lib→ 通过;--bin codeg-mcp→ 通过vitest run→ 6360 个测试通过(439 个文件)eslint(改动文件)→ 0 error;next build→ 成功