# Changelog 本文档记录 dsh-config-manager 的发布亮点(中英双语)。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 This file records release highlights of dsh-config-manager (bilingual: 中文 + English). Format: [Keep a Changelog](https://keepachangelog.com/). > **发布流程**:打 tag 发布时 CI(`.github/workflows/publish.yml`)自动抽取**当前版本段**作为 GitHub Release 描述亮点; > 如果忘记写当前版本段,CI 会 **fail fast** 拒绝发版,避免漏写。 > > **Release workflow**: on tag push, CI extracts the current version's section as the release notes highlights; > the build fails fast if the section is missing, so you cannot forget to update it. > > **条目落位(按改动性质分组)**:新条目一律先写进 `## [Unreleased]`,并放进对应分组 —— > `### 🆕 新增 · Added` / `### 🔧 变更 · Changed` / `### 🐛 修复 · Fixed`(`🗑️ 移除 · Removed` / `🔐 安全 · Security` 同理); > 没有内容的分组**不写标题**。已发布的历史版本段保留当时写法,不回改。 > > **一句话一条**:一条 = **一个用户能看懂的更新或修复**(中文一行 + 英文缩进续行),只写「现在能做什么 / 不再怎样」; > **不写**文件名、测试名、字段与接口细节、根因过程、内部代号、未验证声明(这些留在 commit message 与 `docs/lessons/`); > 同一功能的多次迭代**合并成一条**,不为每次提交各写一条。 > > **Where new entries go (grouped by change type)**: new entries land under `## [Unreleased]` in the matching group — > `Added` / `Changed` / `Fixed` (and `Removed` / `Security` when applicable); omit empty groups. > **One line per change**: one bullet = one user-visible update or fix (Chinese line + English continuation) — say what the user > can now do, or what no longer happens. No file names, test names, field/interface details, root-cause narrative, internal ids > or "not verified" notes (those belong in the commit message), and merge repeated iterations of one feature into one entry. > Released sections keep the style they were written in. > > 体例细节、分组表、发版两道门禁与写后自检命令见项目 skill `.dsh/skills/changelog-writing/SKILL.md`(DSH 会自动发现并加载)。 ## [0.3.1] - 2026-11-02 > **本版主题**:只修一件事 —— 阿里云 OSS 同步不再因为一个服务商根本不支持的条件请求头而失败:条件写改成按各家实际能力选头,测试连接与本地验签也照真机口径判定。 > > **Theme**: one fix only — Aliyun OSS sync no longer fails on a conditional request header the service does not support: conditional writes now pick headers by what each provider actually offers, and the connection test plus local signature check judge by the same rules as the real service. ### 🐛 修复 · Fixed - **阿里云 OSS 同步不再报「A header you provided implies functionality that is not implemented」**:S3 兼容通道的条件写改成按服务商的实际能力选头 —— OSS 上首次上传用它的「禁止覆盖」,索引已存在时改为写入前先核对一次 ETag;测试连接与本地验签也照真机拒掉 OSS 不支持的条件头,本地全绿不会再掩盖真机 400(issue #109)。 **Aliyun OSS sync no longer fails with "A header you provided implies functionality that is not implemented"**: conditional writes on the S3-compatible channel now pick headers by what the provider actually supports — on OSS the first upload uses its forbid-overwrite flag and an existing index is re-checked for its ETag right before writing; the connection test and local signature verifier now reject unsupported conditional headers the way the real service does, so a local green run no longer hides a 400 from the real bucket. ## [0.3.0] - 2026-11-01 > **本版主题**:四个页面重排到一眼能读完,设置页与同步页去掉多余的层和重复的数字,英文数量也说得对了;导入与同步补上更硬的边界 —— 备份包改不了本机同步指向,可疑的仓库地址在交给 git 之前就被拒,删除记录跟着同步走。 > > **Theme**: all four pages are re-laid-out to read at a glance, the settings and sync pages shed redundant layers and repeated numbers, English counts agree, and import plus sync get harder borders — a package can no longer repoint this machine's sync target, a suspicious repository URL is rejected before git ever sees it, and deletions travel with sync. ### 🆕 新增 · Added - **快照里「恢复前被覆盖的原件」现在能列出来也能导出**:恢复前自动留下的那份副本可以在恢复记录里浏览并导出成 ZIP,不用再手工翻快照目录找。 **The originals kept before a restore can now be listed and exported**: the copy saved prior to a restore is browsable in the recovery list and exportable as a ZIP instead of digging through the snapshot folder by hand. ### 🔧 变更 · Changed - **设置页两处「多余的一层」收掉了**:档案启动成功后不再留一块「已启动 · 打开新实例 / 复制 URL」的面板,成功只走顶部提示,打开与停止仍然在运行状态卡里;自动同步的总开关挪到「自动同步」标题行右端,实时同步也改成左文字右开关,下面的间隔与冲突策略不再被一行「启用」割开。 **Two redundant layers are gone from the settings pages**: a successful profile launch no longer leaves an "launched · open new instance / copy URL" panel behind — success only shows as a toast, and open/stop stay on the runtime card; the auto-sync master switch moved to the right end of the "Auto Sync" title row, the realtime switch now reads label-left / switch-right, and the interval and conflict-policy selects are no longer split off by a lone "Enable" row. - **英文界面的单复数说得对了**:以前一律写 `1 file(s)`、`3 day(ies)`,现在按数量选形 —— 顶部报告、导入/导出计数、同步部分成功提示、恢复与回退清单、磁盘留存说明全部改成 `1 file` / `3 files`,动词也跟着变(`1 credential needs` / `3 credentials need`)。 **English plurals read correctly**: counts used to be written as `1 file(s)` or `3 day(ies)`; every report, import/export count, partial-sync notice, recovery and rewind list, and disk-retention note now picks the right form by number, and the verb agrees too (`1 credential needs` / `3 credentials need`). - **同步页面板少了一处重复数字**:分区数以前在概况格和分区行各报一次,现在只留分区行上的「已勾选 9 / 12」,上次同步时间移到卡片标题行右端,空出来的半行不再是一条空白;恢复弹窗的动作顺序也统一成「取消在左、回退在右」,和合并弹窗一致。 **The sync panel stops repeating one number**: the section count used to appear both in the facts grid and on the section row; only the row keeps it now, as "9 / 12 selected", the last-sync time moved to the right edge of the card title, and the half-row that freed up is no longer left blank. The recovery dialog's buttons now also read cancel-left / rewind-right, matching the merge dialog. - **四个页面和任务面板的视觉重做**:卡片里的双层边框和深色空洞没了,列表不再用大幅空白撑满视口,每页只留一个主按钮,重复的状态徽章合并、行内长文字不再被截成半句,维护页的磁盘明细表四列都能完整读完;同步页的「合并 / 推送」两个按钮改为等宽平分整行,测试连接与刷新快照收成两枚图标按钮,快照下拉因此拿到完整行宽。 **Visual pass over all four pages and the task panel**: double borders and dark voids inside cards are gone, lists no longer stretch to fill the viewport with dead space, each page has a single primary button, duplicated status badges are merged, long inline text is no longer cut mid-sentence, and the disk-usage table on the maintenance page is fully readable in all four columns; on the sync page the merge and push buttons now split the row evenly, while test-connection and refresh-snapshots became icon buttons so the snapshot select gets the full row width. - **同步页的按钮和说明文案收拾干净**:「配置…」「选择…」「+ 新建私有仓库…」不再带省略号,「选择」挪到行右端,每行统一读成「左边说明 / 右边按钮」;弹窗底部和分区行里那两处只解释「会自动保存」的图标说明已删除,脚注只剩一颗「完成」。 **The sync page's buttons and hints are tidied up**: the configure, choose and new-repository buttons no longer carry a trailing ellipsis, the choose button moves to the right edge so every row reads description-left / button-right, and the two icon hints that only explained "changes save automatically" are gone, leaving the dialog footer with a single Done button. - **自动同步遇到冲突时缺省停下来问你**:缺省从「保留本机」改为「先问」,之前明确选过「保留本机」的设置继续生效;首轮同步或远端出现新内容时会强制走询问。 **Autosync now asks before overwriting on a conflict**: the default moved from keep-local to ask; an explicit keep-local choice keeps working, and first syncs or newly-seen remote content are forced to ask. - **远端快照回收遇到意外响应就整轮放弃**:不再做半截回收,同步警告里会说明原因与本轮没回收的事实。 **Remote snapshot reclamation gives up for the whole round on an unexpected response**: no half-finished cleanup, and the sync warning says why nothing was reclaimed this round. ### 🐛 修复 · Fixed - **首页「最近活动」会与顶部状态自相矛盾**:自动备份关着、手上只有手动备份时,顶部不再一律显示「一切正常」,改为提示自动备份已关闭。 **The home page no longer contradicts its own status strip**: when auto backup is off and the machine only has manual backups, the header stops claiming everything is healthy and says so instead. - **维护页磁盘明细表被挤压到文字只剩半个字**:内容一多,卡片会把每一行的高度压到几像素;现在只有明细表自己滚动,其余内容固定可见。 **Disk usage details on the maintenance page were squashed until text was cut in half**: a longer card used to compress every row to a few pixels; now only the table scrolls and everything else stays put. - **产物库的文件名和首页活动摘要被尾部截断**:现在改成中段省略,靠后缀区分的备份包名仍然看得出是哪一个。 **File names in the artifact library and activity summaries on the home page were truncated at the tail**: they now elide in the middle, so backup packages that differ only in the suffix remain identifiable. - **清理旧快照不再顺手删掉恢复前的原件**:那些副本有自己的保留期(最近 5 份 / 30 天),旧快照被回收后仍能取回。 **Pruning old snapshots no longer wipes the originals kept before a restore**: those copies follow their own retention (latest 5 / 30 days) and survive snapshot cleanup. - **导入失败时到底回不回滚,四个入口现在给同一个答案**:命令行、界面、同步与内部调用统一为失败即回滚,不再出现「界面回滚、命令行不回滚」。 **Import now answers rollback-on-failure the same way at all four entry points**: CLI, UI, sync and internal calls all roll back on failure instead of disagreeing. - **一台机器上删掉的会话不会再被同步回来**:删除记录随同步传播,另一台机器同步时跟着剔除并给出提示。 **A conversation deleted on one machine no longer comes back through sync**: the deletion record travels with sync, so the other machine drops it and says so. ### 🗑️ 移除 · Removed - **导入面板里不再有第二个「从其它 agent 导入」按钮**:首页那颗直达按钮保留,导入面板只剩「选择 ZIP 文件」,同一件事不再有两个入口。 **The import panel no longer carries a second "Import from another agent" button**: the direct button on the home page stays, the panel only offers "Choose ZIP file", so one action has one entry. ### 🔐 安全 · Security - **备份包里夹带「同步目标配置」也改不了本机同步指向**:导入时这类文件一律不写盘,本机同步设置保持不变,报告会点名被拦下的条目。 **A backup package can no longer repoint this machine sync target**: such files are refused on import, existing sync settings stay untouched, and the report names what was blocked. - **同步仓库地址只接受 http(s) 与 ssh**:以短横线开头、指向本机/内网、或无法解析的地址一律拒绝并说明原因,不再可能混进交给 git 的参数里。 **Sync repository URLs are limited to http(s) and ssh**: values starting with a dash, pointing at loopback or private networks, or failing to parse are rejected with a reason, so nothing slips into the argument list handed to git. - **被拒的同步地址不再显示成「未配置」**:已保存但被新规则拒绝的地址会在同步状态、模型工具与自动同步历史里带上成因(因此局域网 NAS 的 git 目标不再支持)。 **A rejected sync URL is no longer reported as not configured**: an address that is stored but rejected now carries its reason in sync status, model tools and autosync history (so LAN/NAS git targets are no longer supported). - **备份包里没登记在校验表里的文件,导入时逐条拦下**:其余条目照常导入,报告列出被拦下的文件名。 **Files missing from a package checksum table are blocked one by one on import**: the rest import normally and the report lists what was blocked. ## [0.2.4] - 2026-11-01 > **本版主题**:同步仓库比配置大得多时不再卡死——只取同步目录、远端没变一次往返返回、卡住 30 秒内失败且不留孤儿进程。 > > **Theme**: sync no longer stalls when the remote repository is far larger than your config — only the synced folders are fetched, an unchanged remote costs one round trip, and a stalled download fails within 30 seconds without leaving orphans behind. ### 🐛 修复 · Fixed - **同步仓库比配置大得多时,git 通道不再卡在「正在检查远端变更」**:现在只取同步真正需要的目录、远端没变时一次往返就返回、不再把整个仓库的对象拉下来;下载真卡住会在 30 秒内明确失败,超时连同子进程一起终止,不再留下孤儿进程与越堆越大的临时包文件(issue #122)。 **The git channel no longer stalls on "checking remote changes" when the repository is far larger than the synced content**: it now takes only the folders sync actually needs, returns after a single round trip when nothing changed instead of downloading the whole object graph, and gives up within 30 seconds — killing child processes too, so no orphan downloads or growing temp packs are left behind (issue #122). ## [0.2.3] - 2026-10-31 > **本版主题**:同步页的按钮自己说清作用——改名「从远端合并到本机」「立即保存」「上传配置包」,旁边的说明图标不再复述它们。 > > **Theme**: Sync buttons now state what they do — "Merge from remote", "Save now", "Upload bundle" — and the info icons no longer repeat them. ### 🔧 变更 · Changed - **「一键同步」改名为「从远端合并到本机」**:名字写明方向 —— 它只把远端改动合并进来,本机单边改的保留、绝不上传。运行记录、历史与回滚文案同步改叫「从远端合并」;同步页动作行只留按钮,「测试连接」旁那颗说明图标一并去掉(issue #121)。 **"One-click sync" is now "Merge from remote"**: the name states the direction — it only merges remote changes in, keeps local-only edits and never uploads. Run records, history and rollback labels now say "Merge from remote" as well, and the sync action row keeps only buttons — the info icon beside "Test connection" is gone too (issue #121). - **按钮标题自己说清作用,旁边的说明图标不再复述它**:「保存配置」改为「立即保存」、「一键上传」改为「上传配置包」;同步、导出、市场几处说明里复述按钮名或标题的句子一并删掉,市场页上传按钮旁那颗图标也去掉了(同一句在点开后就能看到)。 **Button labels now say what they do, and the info icons no longer repeat them**: "Save config" became "Save now" and "one-click upload" became "Upload bundle"; lines inside the sync, export and marketplace hints that only restated a neighbouring button label are gone, as is the info icon next to the marketplace upload button (the same text appears as soon as the wizard opens). ## [0.2.2] - 2026-10-30 > **本版主题**:Android 上的 Git 同步不再因为取不到凭据而恒失败。 > > **Theme**: Git sync on Android no longer fails for good when the credential helper cannot start. ### 🐛 修复 · Fixed - **Android 上的 Git 同步不再因为取不到凭据而恒失败**:系统不允许从应用目录执行脚本时,同步自动改用请求头携带令牌重试一次,只有这次也失败才提示真实原因(并给出 git 2.31+ 或改用 ssh 的方向)(issue #120)。 **Git sync on Android no longer fails for good when the credential helper cannot start**: when the platform refuses to run scripts from the app directory, sync now retries once with the token sent as an auth header, and only then reports the real cause (git 2.31+ or switching to ssh) (issue #120). ## [0.2.1] - 2026-10-29 > **本版主题**:什么都没改的时候,自动同步不再每分钟往远端仓库堆提交。 > > **Theme**: When you have changed nothing, auto-sync stops piling up remote commits every minute. ### 🐛 修复 · Fixed - **配置没改时自动同步不再每分钟往远端仓库堆提交**:每次轮询只做比对,只有真正改动才生成新快照(issue #119)。 **Auto-sync no longer piles up remote commits every minute while your config is unchanged**: each poll only compares, and a new snapshot is pushed only when something actually changed. (issue #119) ## [0.2.0] - 2026-10-29 > **本版主题**:路径映射点一下就能选本机目录,系统文件夹窗口也不再藏到别的窗口后面。 > > **Theme**: Pick a real folder for each path mapping in one click, and the system folder window no longer hides behind other windows. ### 🆕 新增 · Added - **路径映射可以从本机目录里点选**:每条待映射路径右侧的文件夹按钮直接弹出系统原生的「选择文件夹」窗口,选完即回填;优先复用 DSH 自带的目录选择窗口,DSH 没有该能力时用插件自己的系统对话框(Windows 资源管理器同款 / macOS Finder / Linux GTK·KDE),两者都不可用才回落到内置浏览器。 **Path mapping can be picked from your own folders**: the folder button now opens the OS-native folder picker and fills the path in place, preferring DSH's own directory picker, then the plugin's system dialog (Explorer-style on Windows, Finder on macOS, GTK/KDE on Linux), and finally the built-in browser. ### 🔧 变更 · Changed - **复制按钮换成聊天回退弹窗那套图标**:各处的复制图标与对话回退确认里的保持一致,点击后的打勾反馈不变。 **Copy buttons now use the same icon as the chat rollback dialog**: the copy affordance matches the one in the rollback confirmation, with the same check-mark feedback after clicking. ### 🐛 修复 · Fixed - **点过「从其它 agent 导入」再点「导入」不再进错页面**:导入入口现在按每次点击的来源决定停在选择页还是直接进入导入流程,不再记住上一次的选择。 **Hitting "Import" after "Import from another agent" no longer lands on the wrong page**: the import entry now follows the source of the current click instead of remembering the previous one. - **路径映射面板里的深色路径块不再溢出卡片**:长路径块与卡片左右对齐,路径再长也只在框内换行。 **The dark path blocks in path mapping no longer spill out of the card**: they line up with the card edges, and long paths wrap inside the box. - **点文件夹按钮真的会用 DSH 自带的目录窗口**:文件夹按钮现在优先调用 DSH 桌面壳自己的原生选择窗口,取不到才用插件自己的系统对话框;此前因为找错了服务名,真机上永远退回插件自己的窗口。 **The folder button now really uses DSH's own directory window**: it asks DSH's desktop shell for its native chooser first and only falls back to the plugin's own system dialog when that capability is missing — previously it looked up the wrong service name and always fell back. - **插件自己弹出的系统文件夹窗口不再另开一个应用、也不藏在别的窗口后面**:窗口居中出现在当前屏幕、置顶可见,任务栏上不再多出一个图标。 **The plugin's system folder window no longer opens as a separate app or hides behind other windows**: it appears centered on the current screen, on top, and adds no extra taskbar icon. ## [0.1.86] - 2026-10-28 > **本版主题**:同步状态不再被「别人占着工作副本」拖成超时,首页与同步页打开即有结果。 > > **Theme**: Sync status no longer turns into a timeout because someone else holds the work copy; the home page and sync page report right away. ### 🔧 变更 · Changed - **同步页只强调当前该按的那一个方向**:远端确实还没有任何快照时,主按钮是「推送到远端」(第一次备份),其余情况一律是「一键同步」;两个方向不再同级并列。 **The sync page now highlights the one direction that actually applies**: when the remote provably holds no snapshot yet, "Push to remote" is the primary button (first backup); otherwise "Sync" is — the two are no longer presented as equals. - **同步正在进行时打开同步页不再干等十秒**:另一个同步任务占着工作副本时,页面会立刻说明「工作副本正忙」,而不是空转到超时再报错。 **Opening the sync page during a running sync no longer waits ten seconds**: when another sync holds the work copy, the page says the work copy is busy right away instead of idling until it times out. ### 🐛 修复 · Fixed - **DSH 重启或崩溃后同步页不再一直「读取超时」**:上一次同步留下的占用会被立即识别并回收,旧的残留占用文件也会自动清理,同步状态恢复秒开。 **No more endless "read timeout" after DSH restarts or crashes**: a claim left behind by an interrupted sync is now reclaimed immediately, leftovers are cleaned up automatically, and sync status loads instantly again. ## [0.1.85] - 2026-10-27 > **本版主题**:界面删掉重复与自我说明的文案,同步失败开始说清缺的是哪项权限,卡住的同步请求不再把首页拖成永久骨架,GitHub 登录的授权范围也写在了授权说明里。 > > **Theme**: Less noise, fewer dead waits, and sync failures that name the fix — duplicated and self-explaining copy is gone, a failing sync says which permission is missing, a hung sync request no longer leaves the home page stuck on its skeleton, and the GitHub sign-in scope is stated up front. ### 🔧 变更 · Changed - **首页与产物库打开更快**:读取同步状态时不再顺带跑「领先提交数 / 未提交改动 / 远端快照数」那三项工作副本统计 —— 只有同步页用得上它们,其余页面改用按需查询,10 秒内重复读取直接复用结果。 **Faster home page and library**: reading sync status no longer drags along the work-copy backlog scan (commits ahead, uncommitted changes, remote snapshot count) that only the sync page shows; other pages ask for it on demand and reuse a result within 10 seconds. - **整个界面做了一轮「删文案」体检**:重复的页面标题、复述按钮功能的悬浮说明、只用来填充版面的提示语都已删除 —— 页面标题不再和导航同名,按钮不再用 tooltip 解释自己干什么,「同一件事说两遍」(页脚版本 + 关于页、备份计划卡头与事实行各一枚同款徽章、详情里再列一遍行内已有的大小与时间)也只留一处。 **The whole UI went through a copy audit**: duplicated page titles, tooltips that restate what a button already says, and filler hints are gone — page names no longer repeat the navigation tab, buttons no longer explain themselves on hover, and one fact is stated once (footer version vs. About page, duplicate badges on the backup-schedule card, size/time listed again inside a row's details). - **删掉文字的地方改用结构表达**:未读取的分区改成「状态徽章 + 重试按钮」而不是一句说明;分类标签由分组标题承担;计数改成徽章行;会造成后果的选项把后果写在控件名上(回滚点开关旁给「立即生效」,而不是一段解释)。首页工具栏删掉了与状态条、侧栏重复的那颗「远程同步」。 **Where text was removed, structure took over**: an unread section now shows a status badge plus a retry button instead of a sentence, category tags are carried by group headings, counts became badge rows, and consequential options put their effect in the control itself (the rollback-point switch now has an "Apply now" button instead of a paragraph). The home toolbar's duplicate "Remote sync" button — the status strip and sidebar already reach it — is gone. - **必要说明一字未删**:不可逆操作与数据丢失提示、权限与费用披露、错误与校验反馈、无障碍名称、禁用原因悬停说明全部保留;只是把「能由界面本身说清」的那些删掉。 **Nothing necessary was removed**: irreversible-action and data-loss warnings, permission and cost disclosures, error and validation feedback, accessible names, and disabled-reason tooltips all stay — only the parts the interface itself can say were removed. - **GitHub 登录的授权范围现在写清楚了**:点「使用 GitHub 登录」时,GitHub 给的是你**整个账号**的授权 —— OAuth App 收窄不到单个仓库,组织仓库还要组织管理员批准。授权说明里现在明说这一点,并给出「只想授权一个仓库」的具体做法:改用细粒度 PAT,Repository access 选 Only select repositories、Permissions → Contents 设为 Read and write。 **The scope of GitHub sign-in is now stated up front**: "Sign in with GitHub" grants access to your **whole account** — an OAuth App cannot be narrowed to a single repository, and organization repositories additionally need an org admin approval. The authorization hint now says so, and spells out how to authorize just one repository instead: use a fine-grained PAT with Repository access = Only select repositories and Permissions → Contents = Read and write. ### 🐛 修复 · Fixed - **细粒度令牌权限不足时说清缺的是哪一项**:只勾了 Administration 而没勾 Contents 的细粒度 PAT,以前同步只会甩出 git 原文「remote: Write access to repository not granted / 403」,看着像令牌坏了、换个令牌依然 403;现在直接告诉你要补的是 Contents: Read and write,Administration 不够用。「测试连接」也会顺带说明它只验证了「能读」,写权限属于另一项授权。 **A missing permission is now named instead of leaking git's raw 403**: a fine-grained PAT with Administration but without Contents used to fail with git's own "remote: Write access to repository not granted / 403" — which reads like a broken token, so users recreated it and got 403 again. The message now says the missing permission is Contents: Read and write and that Administration alone is not enough. "Test connection" also notes it only verifies read access, while pushing needs a separate write grant. - **新建仓库的第一次同步不再报「同步失败」**:远端一个快照都没有时(新建的空仓库正是这个状态),以前弹红色「一键同步失败:远端无快照」,看着像通道坏了;现在给的是提示而非报错 —— 这是首次同步,先在同步页点「推送到远端」创建第一个快照,之后才能拉取。 **The first sync of a new repository no longer reports a failure**: with zero snapshots on the remote (exactly what a freshly created repository looks like), the panel used to raise a red "one-click sync failed: no remote snapshots" as if the channel were broken. It is now a hint, not an error: this is the first sync — press "Push to remote" to create the first snapshot, and pulling works from then on. - **同步状态读不出来时,首页不再一直转圈**:同步状态一旦卡住不返回,以前整页骨架会一直挂着等它(默认 5 分钟)。现在首屏只等备份文件、快照、备份计划与活动记录这四个毫秒级请求,同步状态单独加载、最多等 30 秒;超时就把状态条里那一段标成「读取超时」,不再留空让人以为「没配置」。读取同步状态的接口本身也加了 10 秒预算,超时会明确回「同步状态读取超时」,而不是让请求永远悬着。 **The home page no longer spins forever when sync status cannot be read**: a stalled sync-status request used to hold the whole page on skeletons (up to the 5-minute default). The first paint now waits only for the four millisecond-level requests — backup files, snapshots, backup schedule, activity — while sync status loads on its own for at most 30 seconds; on timeout that one segment honestly reads "Timed out" instead of staying blank as if nothing were configured. The endpoint itself also got a 10-second budget and now answers with an explicit timeout error instead of leaving the request hanging. ## [0.1.84] - 2026-10-25 > **本版主题**:**这一版只治两处「你什么都没做、它却动了」——自动同步不再每分钟空推一次把远端堆满快照,恢复也不再因为目标目录还没创建就判成路径越界** > > **Theme**: **Two false alarms fixed — auto-sync no longer pushes an empty snapshot every minute, and restore no longer treats a not-yet-created home as a path escape** ### 🐛 修复 · Fixed - **自动同步只在配置真的变了的时候才推送**:插件的同步/备份计划里会记录「上次运行时间 / 上次结果」这类执行记录,以前它们也被算进「本地有没有改动」,于是你什么都没改,同步页的「最近活动」也每分钟冒出一条「自动同步 push (git) 成功」,远端仓库跟着堆满内容几乎相同的快照。现在执行记录不参与比对、也不会随快照上传,只有真的改了配置(加 skill、装/删插件、改设置)才会推一次。 **Auto-sync now pushes only when your configuration actually changed**: the plugin's own sync/backup schedules store run records (last run time, last result), and those used to count as "local changes" — so with nothing touched, the sync activity list showed a new "auto-sync push (git) succeeded" every minute and the remote repo filled up with near-identical snapshots. Run records are now excluded from comparison and from uploaded snapshots, so a push happens only when configuration really changed (new skill, plugin install/remove, settings edit). - **恢复不再把「目标目录还不存在」误判成路径越界**:当 `$DSH_HOME` 尚未创建、而它所在的路径里带符号链接 / junction(macOS 的 `/var`、Windows 的 8.3 短名)时,恢复计划会把本来合法的目标判成越界、直接一个字节都不写(错误信息「路径越界」)。现在 home 侧也按「最近存在的祖先目录 + 剩余段」解析真实路径,两侧比较回到同一坐标系。 **Restore no longer calls a not-yet-created home an out-of-bounds path**: when `$DSH_HOME` does not exist yet and its path passes through a symlink or junction (macOS `/var`, Windows 8.3 short names), the restore plan rejected legitimate targets as path escapes and wrote nothing. The home side is now resolved the same way (nearest existing ancestor plus the remaining segments), so both sides compare in one coordinate system. ## [0.1.83] - 2026-10-24 > **本版主题**:**这一版专治「点了之后没反应」——产物库的行内动作、迁移历史的刷新,以及全站按钮忙起来的时候都会立刻告诉你正在做什么,而且不再把按钮名换成孤零零一个圈** > > **Theme**: **This release is about visible progress — artifact-row actions, history refresh and buttons across the app now show right away that they are working, and they keep their label instead of collapsing into a lone spinner** ### 🔧 变更 · Changed - **产物库的行内操作现在会自己转圈**:点「恢复 / 导入 / 拉取 / 下载 / 迁移前咨询」或行尾「更多」里的动作后,那颗按钮立刻进入加载态(藏在「更多」里的动作会把「更多」换成该动作名 + 转圈),同一行的其余动作同时变灰;删除确认弹窗在真正删完前也保持加载态,点重复了不会再删一遍。 **Artifact rows now show their own progress**: after picking «restore / import / pull / download / migration check» or any action inside the row's «more» menu, the button that was pressed turns into a loading state right away (an action hidden in «more» replaces that trigger with the action's name plus a spinner) while the row's other actions grey out; the delete confirmation stays in a loading state until the delete actually finishes, so a double click can no longer delete twice. - **按钮加载时不再把文案吞掉**:备份、导出、上架/上传、刷新快照、同步推送、市场下载等按钮在加载中保留按钮名并同时转圈(以前个别按钮忙起来只剩一个圈,看不出点的是哪个动作),读屏软件也能听到「正忙」。 **Buttons keep their label while loading**: backup, export, upload/listing, snapshot refresh, push and market download no longer lose their text while busy — the button name stays next to the spinner, and screen readers now hear the busy state. - **刷新迁移历史不再把清单打回骨架**:「刷新」按钮自己转圈,已加载的记录与筛选结果留在原地;刷新失败也只弹一条提示,旧列表不消失。 **Refreshing the migration history no longer blanks the list**: the «Refresh» button spins in place and the loaded records and filters stay on screen; a failed refresh only shows a toast instead of wiping the list. ## [0.1.82] - 2026-10-24 > **本版主题**:**这一版把「同时干两件事」招来的失败整条收掉:同步、市场与恢复不再互相踩脚,出错时会写清到底哪一步失败;阿里云 OSS 改成按官方算法签名,打开备份页也不再偷偷改你的插件开关** > > **Theme**: **This release removes the whole class of failures caused by doing two things at once: sync, market and recovery no longer trip over each other and failures now say which step broke; Aliyun OSS signs the way the official SDK does, and opening the backup page no longer flips your plugin switches** ### 🐛 修复 · Fixed - **自动同步不再把配置写入锁进安全模式**:之前自动同步和「同步」页的其他操作(列出远端快照、下载、拉取)会同时驱动同一个本地同步仓库,两次 git pull 撞在一起会让 git 自己报错,而这条报错被当成「状态不可证明」,于是整个配置写入被锁进安全模式、只能人工处理;现在同一同步仓库的所有 git 操作排队执行(多开 DSH 实例也一样),拉取也换成了不会被并发写坏的写法,偶发的一次同步失败最多只是那一轮失败,不会再加锁。 **Auto sync no longer locks config writes into safe mode**: auto sync and the other Sync-page actions (listing remote snapshots, download, pull) used to drive the same local sync repository at the same time; two concurrent git pulls made git itself fail, that failure was treated as «state cannot be proven», and every config write was locked into safe mode until handled manually. All git work on one sync repository is now queued (also across multiple DSH instances), and pulling no longer uses the concurrent-fragile form, so a one-off failed sync stays a failed round instead of escalating. - **市场读取与上传不再互相踩脚**:同时打开市场、刷新列表,或者一边读一边上传时,两个动作会同时驱动同一个本地仓库副本,偶尔互相把对方的 git 操作搞失败(表现为市场打不开、或上传直接报错);现在这些操作排队执行,同一份本地副本不再被并发改动。 **Market reading and uploads no longer trip over each other**: opening the market, refreshing the list, or uploading while reading used to drive the same local repository copy at once and occasionally break each other's git work (the market failed to open, or the upload errored); those operations are now queued, so one local copy is never touched concurrently. - **失败原因不再是一行空白**:安全模式下「需要人工处理」的那一行现在会写明到底哪一步失败、失败原因是什么,不再是只有一句「需要人工处理」而没有任何线索。 **Failure reasons are no longer blank**: the «needs manual handling» row in safe mode now says which step failed and why, instead of a bare «needs manual handling» with no clue. - **阿里云 OSS 同步真正能连上了**:上一版只改了授权头格式、没对齐官方签名算法,于是从「授权头为空」变成「签名不匹配」;现在按阿里云官方 SDK 的规范路径(路径里始终包含存储桶)与空值参数写法重算签名,填对密钥即可连上。(issue #109) **Aliyun OSS sync actually connects now**: the previous release only fixed the authorization header format without matching Aliyun's signing algorithm, which turned «authorization header is empty» into «signature does not match»; signatures now follow the official SDK's canonical path (the bucket is always part of it) and empty-parameter form, so a correct key connects. - **打开「备份与迁移」不再悄悄改你的插件开关**:查看插件清单、导出预览与自动快照现在都只读文件,不再顺手把档案的挂载清单改回全量;你把某个插件关掉后,不会因为看了一眼备份页就被重新启用。(issue #111) **Opening Backup & Migration no longer silently flips your plugin switches**: listing plugins, export previews and automatic snapshots now only read files instead of rewriting the profile's bundle list, so a plugin you turned off is not re-enabled just because you opened the backup page. - **DSH 内置的挂载层不会再被对账误删**:档案模板常把内置层(如 dsh-base)同时写成依赖行,而对账以前只看档案自己的依赖目录里有没有实体,于是误判「已经不装了」把它从挂载清单移出、下次启动少挂一层;现在内置层在「加回」与「移出」两条路上都跳过。 **DSH's built-in bundle layers are no longer dropped by the reconcile step**: profile templates often also list built-in layers (such as dsh-base) as dependencies, while the reconcile step only checked whether the profile's own dependency folder had a physical copy — so it wrongly concluded "no longer installed" and removed the layer from the bundle list, costing a layer on the next start; built-in layers are now skipped on both the add-back and the removal path. - **两条同步通道不会再互相顶掉待补跑的那一轮**:一个通道正在同步时另一个通道的改动或远端探测会各自记下来、等这轮结束逐个补跑,不再只剩最后到的那一个。 **Two sync channels no longer cancel each other's queued run**: while one channel is syncing, changes or remote checks arriving on another channel are remembered per channel and each gets its own follow-up run instead of only the last one surviving. - **命令行修会话前会先看 SAFE MODE 与环境锁**:`sessions repair --fix` 现在与救急台用同一道写入门,存在未恢复的配置事务或残留环境锁时直接拒绝并指出出口,不再只看 DSH 有没有在运行。 **The CLI session repair checks SAFE MODE and the environment lock first**: `sessions repair --fix` now passes the same write gate as the rescue console, so it refuses with a pointer to the way out when an unfinished transaction or a stale lock exists, instead of only checking whether DSH is running. - **gist 通道并发写不再悄悄吃掉别人刚推的快照**:推送或删除完成后会重新读回索引核对,条目没落上或被别人改过就整轮重做,最多三次;仍不行就明确报「索引冲突」并提示稍后重试,而不是当作成功。 **Concurrent writes on the gist channel no longer silently drop someone else's fresh snapshot**: after a push or delete the index is read back and verified; if an entry did not land or someone else changed it, the whole round is redone up to three times, and a persistent conflict is reported as "index conflict" with a retry hint instead of being treated as success. - **快照恢复中途崩溃后能接着做完,但要先逐项证明**:恢复每做完一个动作就登记进度;宿主在恢复途中被杀后,再点一次恢复会先核对「还是同一次恢复、同一个快照、每个目标文件仍是做之前或做之后的样子」,成立就只补做剩下的动作,不成立就明确报出原因并要求先处理,绝不猜着继续。 **A restore interrupted by a crash can be finished by clicking restore again, once it proves it is safe**: progress is recorded action by action; after the host is killed mid-restore, the next attempt first verifies that it is the same restore on the same snapshot and that every target still matches either its pre-restore or post-restore state. Only then does it perform just the remaining actions, and otherwise it explains why it refused instead of guessing. ### 🔐 安全 · Security - **回滚点不再把凭证类文件写成明文副本**:`.env`、私钥、`credentials`/`secrets` 一类文件不再被采集进回滚点,而是记为「敏感跳过」并出现在跳过清单里;这些文件因此也不会被「回退到留点前」删除或改写。 **Rollback points no longer materialize credential files as plaintext copies**: `.env` files, private keys and `credentials`/`secrets` style files are no longer captured into rollback points; they show up on the skipped list instead, and rollback will neither delete nor rewrite them. ### 🔧 变更 · Changed - **快照恢复的「恢复前备份」改为每次一个目录**:重跑同一次恢复不再覆盖上一次的反悔副本,命令行与界面看到同一个位置,出问题可挑对应那次回到恢复前状态。 **The pre-restore backup of a snapshot restore now gets its own directory per run**: re-running a restore no longer overwrites the previous undo copy, the CLI and the UI point at the same place, and you can pick the run you want to roll back to. ## [0.1.81] - 2026-10-22 > **本版主题**:**加密同步的共同基线不再因为「换了条落地路径」就退回明文散文件 —— 加密快照无论是推送上去还是拉取下来,留下的可三方比较的基线都只以密文形态落盘;拿不到密码时宁可不留基线,也不会把明文写出到磁盘** > > **Theme**: **The common baseline used by encrypted sync no longer degrades into plaintext files just because the changes landed through a different path: whether an encrypted snapshot was pushed up or pulled down, the baseline kept for three-way comparison stays encrypted on disk, and when no password is available it is not kept at all rather than written out in plaintext** ### 🐛 修复 · Fixed - **加密同步拉取落地后也留密文基线**:以前只有「推送上去」会留可三方比较的基线,「拉取 / 一键同步」落地的那份仍是明文散文件;现在两条路径都只落密文,密码不对或缺失时索性不留,下一轮退回两边比较,不会再出现「看起来开了加密、磁盘上却有明文副本」的情况。(issue #107) **Pulled encrypted sync keeps an encrypted baseline too**: previously only an upload left a baseline usable for three-way comparison, while the one written when pulling or one-click syncing landed was still plaintext on disk; both paths now keep it encrypted only, and when the password is missing or wrong the baseline is simply not kept — the next round falls back to a two-way comparison instead of leaving plaintext copies around while encryption is supposedly on. (issue #107) ## [0.1.80] - 2026-10-22 > **本版主题**:**这一版修掉五处「看着像你不会用、其实是它自己出错」的地方:加密同步不再每轮逼你手动裁决、冲突拍完板就能执行合并、反向代理挂在子路径下不再误报「插件未安装」,阿里云 OSS 与 gist 两条同步通道也终于能真正连上** > > **Theme**: **This release fixes five places that looked like user error but were ours: encrypted sync no longer demands a manual decision every round, a fully-decided merge can be applied, a reverse-proxied sub-path deployment no longer reports the plugin as missing, and both Aliyun OSS and gist channels now actually connect** ### 🐛 修复 · Fixed - **加密同步不再每轮都要求你手动裁决**:开启加密快照后也会留下用于三方比较的共同基线,密码正确时能自动合并的改动会自己合上,不再把两边都没动过的分区也报成冲突;密码缺失或不对时退回两边比较,不会因此中断同步。(issue #107) **Encrypted sync no longer asks you to decide every single round**: encrypted snapshots now keep a common baseline too, so with the right password the automatically mergeable changes just merge and untouched sections are no longer reported as conflicts; a missing or wrong password falls back to a two-way comparison instead of breaking the sync. - **冲突全部裁决完就能执行合并**:以前「没有可自动合并的分区、全是冲突」时执行按钮一直灰着,现在逐个拍板后就能点;「已裁决」与「可自动应用」分开表述,不再互相冒充。(issue #106) **A merge can be applied once every conflict is decided**: the apply button used to stay disabled whenever nothing could be merged automatically and everything was a conflict; now it enables as soon as you have decided each item, and the summary tells decided items and auto-applicable items apart. - **反向代理挂在子路径下也能正常用**:面板的接口地址改为跟随页面实际所在路径,`/app/dsh/` 这类前缀不再丢失;路径没命中时提示「接口路径未命中」并给出实际请求前缀,不再误报「插件未安装」。(issue #108) **Works behind a reverse proxy on a sub-path**: the panel's API base now follows the path the page is actually served from, so an `/app/dsh/`-style prefix is no longer dropped; when a path does not hit the API it says so and shows the prefix it used instead of claiming the plugin is not installed. - **阿里云 OSS 同步能连上了**:授权头改为按阿里云官方格式逐字组装,不再带多余空格与空的附加头段,填对密钥后不会再被回 «Authorization header value is empty»。(issue #109) **Aliyun OSS sync connects**: the authorization header is now built exactly as Aliyun's own SDKs do it (no stray spaces, no empty additional-headers segment), so a correct key no longer comes back as «Authorization header value is empty». - **gist 通道不再被拒**:GitHub 的 gist 接口不接受任何条件请求头,上传与删除都不再携带,之前每次推送都失败的情况消失(代价是 gist 通道没有并发写保护,多设备同时写请改用 git / WebDAV / S3)。(issue #110) **The gist channel is no longer rejected**: GitHub's gist endpoint refuses conditional request headers, so uploads and deletes no longer send them and pushes stop failing (the trade-off is no concurrent-write protection on gist — use git, WebDAV or S3 if several devices write at once). ## [0.1.79] - 2026-10-22 > **本版主题**:**这一版让「回退」和「同步」都变得可以放心依赖 —— 回退不再误伤别的对话改过的文件、也不再改写换行符与编码;同步能一键自检、两台设备同时写不再互相覆盖、改完就同步;从 Claude Code 迁移会连同项目级与插件里的东西一起带过来,导出时每个分区都能展开逐条勾选** > > **Theme**: **This release makes both rollback and sync safe to lean on: rollback no longer touches files that other conversations changed and no longer rewrites line endings or encodings; sync self-checks with one click, stops two devices from clobbering each other and fires the moment you change something; migrating from Claude Code now brings project-level and plugin-provided pieces along, and the export picker lets you expand every section for item-by-item selection** ### 🆕 新增 · Added - **用 MCP 连接器配的服务器现在会随备份走**:以前这 4 个连接的配置根本进不了备份包(MCP 分区只认旧的补丁行),现在会跟 MCP 分区一起导出——地址、参数、开关与普通请求头照原样保留,令牌、密钥、带凭据的 URL 查询参数一律替换成占位符;导入时按连接合进目标机、先自动留好可回滚的快照,并明确提示要重启 DSH 才生效,需要重填凭据的连接只列在清单里而不会被写成一个起不来的服务器。 **MCP servers configured through connectors now travel with your backups**: those connections used to be absent from the bundle entirely (the MCP section only knew about legacy patch lines); they now export alongside it with endpoints, arguments, switches and ordinary headers unchanged while tokens, keys and credential-bearing URL parameters become placeholders, import merges them into the target per connection with a rollback snapshot taken first, tells you plainly that DSH must restart to apply, and lists connections needing credentials instead of writing a server that cannot start. - **导出时每个分区都能展开、逐条勾选**:以前只有插件、技能、会话这类「文件型」分区能展开,设置项、模型供应商、MCP 服务器、提示词与凭据清单只能整块勾选;现在它们也能逐条展开取舍,勾掉的不会跟着走(模型供应商会按你的勾选收窄写回内容),收起后仍可一键全选整块。 **Every section in the export picker can now expand for item-by-item selection**: sections that used to be all-or-nothing (settings, model providers, MCP servers, prompts and the credential list) now expand into individual items you can pick or leave out — what you uncheck no longer travels along (model providers narrow their written-back content to your picks) — and collapsing still lets you take the whole block in one click. - **同步页可以「测试连接」**:不执行任何同步就能只读自检当前通道(凭据、端点、目标容器、远端目录),失败时直接给出可执行的下一步(例如告诉你区域该填哪个值),不用再靠猜;四条通道都可自检 —— git 会只读核对远端可达与凭据是否被接受(不拉取、不改本地任何东西)。某条通道确实不提供自检时按中性提示说明(是能力边界,不是「操作失败」),绝不显示一个假的「连接正常」。 **A "Test connection" button in the sync page**: a read-only self-check of the current channel (credentials, endpoint, target container, remote folder) without running any sync, and failures come with an actionable next step (for example which region value to use) instead of guesswork. Channels that cannot self-check say so honestly rather than showing a fake "connection OK". - **同步历史里能看出快照是哪台机器推的**:历史条目现在带设备名(同一台设备换过主机名也认得出),多台机器互相推送时不再分不清来源。 **Sync history now shows which machine pushed a snapshot**: history entries carry the device name (the same device is still recognized after a hostname change), so multi-device pushes are no longer indistinguishable. - **同步前可以逐项看清单、逐项拍板**:改动本机之前先只读预览每个分区会发生什么——本机值、远端值与共同祖先并排对比,标明哪些改动能自动合并、哪些必须你决定;你可以对每个分区选择「保留本机 / 采用远端 / 采用自动合并 / 暂不处理」,没处理完的冲突会一直留着并在下次同步继续出现,不会被悄悄咽下去。 **Review and decide each merge item before anything changes**: a read-only preview shows, per section, the local, remote and common-ancestor values side by side, marks what can be merged automatically and what needs your call, and lets you choose per section between keeping local, taking remote, taking the automatic merge or leaving it for later — unresolved conflicts stay listed and come back on the next sync instead of being silently dropped. - **从 Claude Code 迁移时会连同项目级与插件里的东西一起带过来**:项目级 `CLAUDE.md`/`CLAUDE.local.md` 与不带作用域的规则文件并入全局指令,项目与插件里的 MCP 服务、插件自带的 Skills 一并进包;来源 agent、读到过哪些位置、扫过哪些项目根、带了哪些插件与哪几类内容被跳过,会写进备份包本身,事后能核对这个包到底从哪来。 **Migrating from Claude Code now brings project-level and plugin-provided pieces along**: project `CLAUDE.md`/`CLAUDE.local.md` and unscoped rule files are merged into the global instructions, while MCP servers and plugin-bundled Skills from projects and plugins are packed too; the source agent, the locations that were read, the project roots scanned, the plugins carried and the categories skipped are recorded inside the bundle, so you can audit where a package really came from. - **Claude Code 的插件与设置不再「不声不响地没搬」**:插件(含 marketplace 来源与版本)、子代理定义、权限规则、hooks 事件、设置的其余顶层键、带作用域的规则文件现在逐条报告「没搬 / 为什么」,并明确说明插件代码不会被复制、hook 更不会被执行。 **Claude Code plugins and settings no longer go missing silently**: plugins (with marketplace source and version), subagent definitions, permission rules, hook events, other top-level settings keys and scoped rule files are each reported as "not migrated, and why", and it is stated plainly that plugin code is not copied and hooks are never executed. ### 🔧 变更 · Changed - **两台设备同时同步远端时不再互相覆盖**:写入前会核对远端是否已被别人改过,一旦发现就把这次写入停在写任何东西之前并明确提示重跑,绝不静默丢掉别人刚推的快照。 **Two devices syncing at the same time no longer overwrite each other**: the write checks whether the remote changed since it was read, and if it did the sync stops before writing anything and asks you to run it again, instead of silently dropping a snapshot someone else just pushed. - **首次同步不再要求你先手工建好网盘目录(WebDAV)**:目标目录(含多级)不存在时会自动创建,创建了哪一层也会如实告诉你;对象存储(S3/OSS/COS/MinIO/Kodo)与 Gist 仍按各自语义处理,不会替你新建桶或 Gist。 **First sync no longer asks you to create the WebDAV folder by hand**: missing target folders (including nested ones) are created automatically, and which levels were created is reported honestly; object storage (S3/OSS/COS/MinIO/Kodo) and Gist keep their own semantics and never create a bucket or a Gist for you. - **跨机同步不再把本机的 pnpm 缓存 / 存储位置带到别的机器**:导出时这些机器专属设置会被剥离(并在你的确认列表里可见),恢复时保留目标机自己原有的设置。 **Cross-machine sync no longer carries this machine's pnpm cache/store locations to another machine**: those machine-specific settings are stripped on export (and shown in your confirmation list), while the target machine keeps its own settings. - **自动合并升级为真正的三方合并**:两台设备改了同一个配置分区里的不同键、或同一个文本文件的不同行时,两边改动现在会同时保留,不再整段变成冲突等你裁决;只有改到同一处才需要决定。 **Automatic merging is now a real three-way merge**: when two devices change different keys of the same section, or different lines of the same text file, both changes are kept instead of the whole section turning into a conflict; only edits to the same spot need a decision. - **同一份 Claude Code 数据重复导入不再产生重复副本**:会话日志的生成时间改为确定性回落,第二次导入同一来源会被判成「内容相同,跳过」,而不是让你逐条裁决冲突。 **Importing the same Claude Code data twice no longer creates duplicates**: session logs now fall back to a deterministic timestamp, so a second import of the same source is judged "identical content, skipped" instead of asking you to resolve conflicts item by item. - **点「一键同步」后立刻看得到它在动**:从点击那一刻起按钮进入加载态并说明正在做什么(先「正在检查远端变更」,确认要同步后再转「正在同步」),卡片上同时出现一条不定态进度条;原先最慢的那几秒(拉远端快照比差异)完全没有反馈,容易被当成没点上而反复点。 **Clicking "one-click sync" now visibly reacts right away**: the button enters its loading state the moment you click and says what it is doing (first "checking remote changes", then "syncing" once it proceeds), with an indeterminate progress bar on the card — previously the slowest few seconds (fetching the remote snapshot to diff) gave no feedback at all and looked like the click was lost. - **远程合并弹窗重新设计,一眼看出「哪里需要你决定」**:顶部四个指标(可自动应用 / 待裁决 / 冲突 / 无变化)用颜色区分轻重,每个分区改成一张卡片、冲突只保留必要信息,批量裁决收到工具栏,裁决进度与「执行合并」固定在弹窗底部;分区再多也在弹窗内滚动,不必滚到正文最底部找按钮。 **The remote-merge dialog is redesigned so what needs your decision is obvious at a glance**: four color-coded counters on top (auto-applicable / needs review / conflicts / no change), one card per section with only the necessary conflict detail, bulk decisions in the toolbar, and the decision progress plus "apply merge" pinned in the dialog footer, so long lists scroll inside the dialog instead of pushing the button out of reach. - **合并弹窗里「采用自动合并」只出现在真的能自动合并的分区上**:以前它对每个分区都显示、对冲突分区永远点不动,看起来像坏了;现在没有自动合并结果的分区不显示这个按钮,「全部采用自动合并」也只在至少一个分区可用时出现。 **"Take the automatic merge" now appears only where there is an automatic result to take**: it used to be shown on every section and stayed permanently disabled where a section conflicts, which looked broken; sections without an automatic result no longer show the button, and the bulk "take all automatic merges" shows up only when at least one section qualifies. - **合并弹窗不再有「跳过」,冲突就是「留到下次」**:原来的「跳过」既没解决冲突、又与什么都不点完全等价,容易让人以为已经处理过;批量里的「仅处理无冲突项」也改成同一件事的直白说法「冲突留到下次」,只把待裁决的分区留在原处,弹窗底部如实说明还有几个分区这次不会应用。 **The merge dialog no longer has a "skip" action — conflicts are simply left for the next sync**: skipping resolved nothing and was equivalent to not choosing at all, yet it looked like the conflict had been handled; the bulk "only handle conflict-free items" button is now the plain-spoken "leave conflicts for next sync", and the footer states how many sections this run will not apply. - **加载时先看到「即将出现的内容」,不再只剩一个转圈**:产物库列表、首页状态条与最近活动、恢复台扫描、导入与迁移咨询、备份查看、回退点、更新说明与发布说明这些页面、弹窗与面板的首次加载,都改成与真实内容同形状的骨架占位,行高对齐后数据到货不再整页跳变;按钮里与读取遮罩中的进行中反馈仍保留转圈(那里没有布局轮廓可给)。 **Loading now shows the shape of what is coming instead of a lone spinner**: first loads inside the library list, the home status strip and recent activity, recovery scans, import and migration consultations, backup inspection, rollback points, update notes and release notes all render skeleton placeholders shaped like the real content, and with row heights matched the page no longer jumps when data arrives; in-button and read-mask progress still uses the spinner, where there is no layout to outline. - **自动同步不再干等定时器:改完就同步**:本机一改配置(设置、提示词、插件清单与补丁、档案清单、技能与预设、指令文件等),一两秒内就会自动起一轮同步;远端也按独立节奏检查新快照(缺省 60 秒,可自行调整,最短 15 秒、最长 1 小时),原来的间隔设置退居兜底;一轮还没跑完时不会自己触发自己,改完再接上下一轮。 **Auto sync no longer waits for the timer — it fires as soon as you change something**: editing local config (settings, prompts, plugin lists and patches, profile manifests, skills and presets, instruction files) starts a sync round within a second or two, and the remote is checked for new snapshots on its own cadence (60 seconds by default, adjustable from 15 seconds to an hour), with the old interval kept as a fallback; a round in progress never triggers itself, and the next one follows as soon as it finishes. - **中风险分区(插件与 MCP)不再一律挂着等你裁决**:远端单边改动会自动落到本机;想更保守可以把自动落盘收窄成「只自动应用低风险」。高风险分区(会话、密钥、插件数据、本机凭据状态、插件文件)仍然永不自动。 **Medium-risk sections (plugins and MCP) no longer sit waiting for your decision**: one-sided remote changes are applied locally by default, and you can tighten it to "only low-risk" if you prefer; high-risk sections (sessions, secrets, plugin data, local credential status, plugin files) are still never automatic. - **真的改到同一处时按「保留本机并推上去」处理**:不再整轮挂起等人工裁决,本机内容会推成远端最新,被覆盖的远端旧快照仍留在远端历史里可回滚,卡片上如实写明本轮是「保留本机」的部分结果;想每次都由你决定可以切回「每次问我」。 **When both sides really changed the same spot, local wins and is pushed up**: the round no longer stalls waiting for a manual decision — your local content becomes the latest remote, the overwritten remote snapshot stays in remote history for rollback, and the card states plainly that this round kept local changes; switch back to "ask me every time" if you want to make the call yourself. - **自动同步一轮只拉取远端一次**:此前每轮会重复拉两次,现在网络往返与远端压力减半(按缺省 60 秒检查,从每天约 2880 次降到约 1440 次)。 **An auto-sync round now fetches the remote once instead of twice**: network round-trips and remote load are halved (about 1440 instead of 2880 per day at the default 60-second check). ### 🐛 修复 · Fixed - **MCP 分区不再恒显示 0 个**:在选择导出内容时,用 MCP 连接器配好的服务器以前一律算「0 个、26 B」的空数组,现在如实计入条目数与体积。 **The MCP section no longer always shows zero**: when picking what to export, servers configured through MCP connectors used to count as an empty array ("0 items, 26 B") and are now counted honestly in both item count and size. - **远程合并弹窗打开时不再抖动或形变**:入场动画改为纯淡入、不再做位移;带内滚列表的弹窗也不会因为滚动条在动画中途出现而横向跳一下。 **The remote-merge dialog no longer shudders or distorts as it opens**: its entrance is now a plain fade with no movement, and dialogs with inner scroll areas no longer jump sideways when the scrollbar appears mid-animation. - **Claude Code 的子代理对话不再被混进主对话**:带 `isSidechain` 的记录改为跳过并计数上报,此前它们会被当成普通消息写进正文。 **Claude Code subagent conversations are no longer mixed into the main one**: records marked `isSidechain` are skipped and counted, where they used to be written into the transcript as ordinary messages. - **Claude Code 配置目录被 `CLAUDE_CONFIG_DIR` 挪走后,导入会真的去读那个目录**:并在报告里注明「位置被环境变量覆盖」,此前会读默认位置、甚至判定为「没安装」。 **When Claude Code's config directory is moved via `CLAUDE_CONFIG_DIR`, the import now reads it**: the report states that the location was overridden by an environment variable, where it used to read the default location or even report Claude Code as not installed. - **Claude Code 会话与技能被静默丢掉的几种情况已变得可见**:会话文件数超过上限、技能目录是符号链接、会话记录里的 sessionId 与文件名不一致、推理内容无法承载——都会报出来,而不是悄悄少一块。 **Several silent drops in the Claude Code import are now visible**: exceeding the session-file cap, a symlinked skill directory, a session record whose `sessionId` disagrees with the file name, and reasoning content that cannot be represented are all reported instead of quietly disappearing. - **同步一次成功后不再反复报同一个冲突**:合并完成时同步基线按「本机实际落地的内容」记录,配置项内部的版本号自增不会再让下一轮把同一处判成新冲突。 **A successful sync no longer keeps reporting the same conflict**: the sync baseline is recorded from what actually landed on this machine, so internal revision bumps no longer make the next round treat the same spot as newly changed. - **被破坏的远端快照内容不再被原样写进本机配置**:分区数据形状不对(例如被外部工具改成一段字符串)时,现在会判为冲突并把原始内容留在裁决界面供你核对,而不是默默采用。 **Damaged remote snapshot content is no longer written into your local config as-is**: when a section's data has the wrong shape (for example a string where an object was expected), it is reported as a conflict with the raw values kept for review instead of being adopted silently. - **回退不再误伤别的对话,也不再覆盖「不知道是谁改的」内容**:同一工作区里对话 A 改了文件、对话 B 又接着改,回退 A 时只撤销 A 那一部分、B 的改动逐字保留;如果某处改动无法判定属于哪个对话(例如会话结束之后被手工改过、或文件被删掉后又被重建),回退会**一个字节都不写**并把该文件列出来让你决定,绝不再按「覆盖回记录版本」了事。先回退 A 再回退 B 时,A 的改动不会复活,B 的改动也不会丢。 **Rewinding a conversation no longer damages other conversations, and never overwrites content it cannot attribute**: in the same workspace, if conversation A changed a file and conversation B then changed it further, rewinding A now undoes only A's part and keeps every byte of B's. When a change cannot be attributed (for example the file was edited by hand after the session went quiet, or was deleted and later recreated), the rewind writes **nothing** and lists that file for you to decide — it no longer falls back to "restore the recorded version". A then B: A's edit does not come back and B's edit is not lost. - **回退后文件的换行符与编码不再被改写**:CRLF 文件不再被整份折成 LF;GBK / Latin-1 等非 UTF-8 文件里的非法字节不再被替换成 `\uFFFD`(此前会真的写坏文件内容)。 **Line endings and encodings survive a rewind**: a CRLF file is no longer rewritten with LF, and invalid bytes inside non-UTF-8 files (GBK, Latin-1) are no longer turned into `\uFFFD` — which used to corrupt the file for real. - **「有文件没能回退」不再被说成「没有东西可回退」**:因为两侧改到同一处、或无法判定归属而跳过的文件,现在会连同原因与路径逐条列出来,整体结论也不再显示成「已恢复」。 **"Some files were not rewound" is no longer reported as "there was nothing to restore"**: files skipped because both sides changed the same spot, or because they could not be attributed, are now listed with the reason and the path, and the overall result no longer claims a clean restore. - **同步导入升级插件时按备份里记录的确切版本安装**:装完会回读实际安装到的版本,没装到目标版本时直接说明原因并附一条可复制的精确安装命令,不再报「已触发更新」而版本其实一字未变。(issue #101) **Plugin upgrades during a sync import now install the exact version recorded in the backup**: the version actually installed is read back afterwards, and when it does not match you get the reason plus a ready-to-copy install command instead of an "update triggered" message while the version stayed the same. (issue #101) - **插件自带的技能不再被打进备份**:这类技能的脚本与资源不随包走、副本必然残缺,于是不再导出;旧备份里的这类副本在导入时跳过并说明原因,不再与插件自带的版本互相抢生效。(issue #102) **Skills provided by a plugin are no longer packed into backups**: their scripts and assets cannot travel with the package, so the copy would always be incomplete and is no longer exported; such copies in older backups are skipped during import with the reason stated, instead of competing with the plugin's own version. (issue #102) - **git 同步不再因为本机全局 git 配置里设过用户名而报「读不到密码」**:内置账号(oauth2)现在会显式带上,凭据库按用户名查找时不会再被全局配置里的另一个名字顶掉;市场里以 git 源安装插件走的也是这条路,一并修好。(issue #103) **Git sync no longer fails with "could not read Password" because your global git config already sets a username**: the built-in account (oauth2) is now passed explicitly, so the credential-store lookup is no longer overridden by another name in your global config; installing plugins from a git source in the marketplace uses the same path and is fixed too. (issue #103) - **换了同步仓库、或远端历史被重写之后,工作副本不再只剩「重新克隆」一条路**:同步页新增「采纳远端历史」,把当前仓库地址的历史合并进本机工作副本 —— 本地提交一条都不会被改写、合并后还能用 git revert 撤回,发生冲突时自动撤销这次合并把工作副本还原(此前只能重新克隆,未推送的本地提交直接丢);远端顶层看起来不是本插件的同步仓库时直接拒绝合并。(issue #104) **After swapping the sync repository, or when the remote history was rewritten, the working copy is no longer stuck with "clone again"**: a new "adopt remote history" action merges the history at the configured address into the local working copy — no local commit is rewritten, the merge can be undone with git revert, and a conflict aborts the merge and restores the previous state (previously re-cloning was the only way out, throwing away unpushed local commits). If the remote root does not look like this plugin's sync repository, the merge is refused. (issue #104) - **自动同步卡片不再把很久以前那次的错误当成「本次运行结果」一直显示**:错误原文只在「本轮确实失败」时出现,改配置或勾选开关也不会再把旧错误写实;连续失败的通知只进同步历史与日志,不再冒充运行信息。(issue #105) **The auto-sync card no longer keeps showing a long-past failure as if it were the latest run's result**: the error text appears only when the latest run actually failed, toggling settings no longer pins an old error in place, and consecutive-failure notifications go to the sync history and logs instead of masquerading as run info. (issue #105) - **正在正常执行的操作不再被当成「未完成的恢复事项」**:自动同步推送进行中时,恢复页不再冒出「已进入安全模式」的假警报,也不再给出一个永远点不动的「执行恢复」;这类操作单独列成「正在执行的操作」,结束后自己消失。(issue #87) **A running operation is no longer mistaken for an unfinished recovery item**: while an auto-sync push is in progress the recovery page stops raising a false "safe mode active" alert and stops offering a permanently greyed-out "Run recovery"; such operations are listed separately as running and disappear when they finish. (issue #87) - **恢复页不再给一个没有任何解释的灰色按钮**:点不动「执行恢复」时会直接说明原因(这次操作没有绑定可信恢复快照、或被判定为需要人工处理),正在进行中的操作还会提示「等它结束再处理」,而不是让人反复重试、反复撞上「另一个任务正在运行」。 **The recovery page no longer leaves a greyed-out button unexplained**: when "Run recovery" cannot be used it now states the reason (the operation has no trusted snapshot, or it needs manual handling), and for an operation that is still running it tells you to wait for it to finish instead of letting you hit "another task is running" over and over. - **恢复事项的提示文案改说真话**:只有写操作真的被暂停时才写「已进入安全模式」,仅仅是残留锁或中断的复制时改为「存在需要处理的恢复事项」,不再把「有事项待处理」和「写操作被冻结」说成同一件事。 **Recovery wording now matches reality**: "safe mode active" is shown only when config-modifying writes really are paused; for a stale lock or an interrupted copy the page says recovery items need handling instead of conflating "something needs attention" with "writes are frozen". - **已成功的操作在台账里不再停在「计划中」**:外部步骤(例如推送到远端)成功后会被标记为已完成并记下完成时间,事后看历史能分清「已经跑完」和「根本没跑」。 **Succeeded operations no longer sit in the ledger as "planned"**: an external step (such as a push to the remote) is marked done with its completion time once it succeeds, so the history distinguishes "actually ran" from "never ran". - **同步页现在会告诉你「本地还有多少提交没上传、远端到底有几个快照」**:工作副本卡上直接显示这两个数,本地领先却从未成功上传过快照时给醒目告警 —— 此前远端两周没收到一个字节,界面也看不出任何异常。(issue #104) **The sync page now says how many local commits are still unpushed and how many snapshots the remote actually has**: both numbers show on the working-copy card, with a prominent warning when local commits are ahead while the remote has never received a snapshot — previously the remote could go two weeks without a single byte while the UI showed nothing wrong. (issue #104) - **勾选「自动同步」后会立刻跑一轮真实同步**:只要距上次运行已超过启动阈值,勾选后马上产出本轮结果,而不是让卡片继续挂着上一轮(甚至是十天前)的结论;本来开着时改间隔/阈值不会触发。(issue #105) **Ticking "auto sync" now runs a real sync round immediately**: as long as the last run is older than the startup threshold, enabling it produces this round's result right away instead of leaving the previous (possibly ten-day-old) conclusion on the card; adjusting the interval while it is already on does not trigger a run. (issue #105) - **从档案页启动新实例后能直接拿到带 token 的登录地址**:此前在 Windows 上启动的实例虽然起来了(端口已经在听),启动日志却始终是空的 —— 界面因此只能报「20 秒内未确认就绪 / 没能抓到带 token 的认证 URL」,你只能自己去日志里找那条地址(而日志里其实一个字都没有)。现在地址会被正常抓起并可直接打开;等待上限也从 20 秒放宽到 30 秒,插件多、启动慢的档案不会再被误判成没起来。 **Launching a profile from the profiles page now hands you the token-carrying URL**: on Windows the instance did start (the port was already listening) but its launch log stayed empty, so the page could only warn that readiness was not confirmed within 20 seconds and that no authenticated URL could be found — leaving you to dig the URL out of a log that contained nothing at all. The URL is now captured and directly openable, and the wait limit was raised from 20 to 30 seconds so profile-heavy, slower-starting setups are no longer misjudged as failed. ## [0.1.78] - 2026-10-17 > **本版主题**:**同步或恢复配置后,DSH 自带的插件层不再被装成错版本的 npm 包 —— 不会再出现「DSH 升级之后新建会话报错」** > > **Theme**: **After a sync or restore, layers provided by DSH itself are no longer installed as the wrong npm version — no more "new sessions fail after a DSH upgrade"** ### 🐛 修复 · Fixed - **同步或恢复配置后,DSH 自带的插件层不再被装成错版本的 npm 包**:新版 DSH 里那些「只出现在插件层清单、由 DSH 自己提供」的实验性层(例如实验性会话搜索),导入时不再按 npm 的「最新版」重新下载一份塞进你的档案 —— 现在只是把这一层重新声明启用,不会再出现「DSH 已经升级、装回来的却是旧版本」而**新建会话报错**的情况;目标机档案清单缺失或读不出来时如实提示,绝不凭空重建一份。(issue #95) **Self-provided DSH plugin layers are no longer installed as the wrong npm version after a sync or restore**: experimental layers that live only in the plugin-layer list and are provided by DSH itself (such as the experimental session search) are no longer re-downloaded from npm's "latest" tag into your profile — the import now just declares the layer again, so a DSH upgrade can no longer be followed by a stale copy that breaks **creating new sessions**; when the target profile manifest is missing or unreadable you get an honest warning instead of a silently rebuilt manifest. (issue #95) ## [0.1.77] - 2026-10-17 > **本版主题**:**整个插件界面改用 DSH 自己的设计取值 —— 你发出的消息气泡与 DSH 原版逐项一致;回退不再误伤别的对话改过的文件;导出的插件区变成一棵按插件分组的树** > > **Theme**: **The whole plugin UI now draws from DSH's own design values - your own message bubbles match DSH's native bubble item by item, rewinds no longer discard what other conversations changed, and the export picker groups plugins into a tree** ### 🆕 新增 · Added - **导出的插件区现在是一棵按插件分组的树**:全部插件先收在一个「插件」区块里(默认折叠,展开才列出各个插件),同一个插件的清单条目、数据目录与大文件都挂在它的名字下面,点一次就把它散在几处的文件一起带上;分区内容默认按需读取(展开即读,不再需要点「读取该分区」),也没有「未安装插件的遗留数据」这一类多余的行;「已选 n/m」不再随展开跳动,勾选「插件」或某个插件时会等内容读取完后自动把它名下的数据一并带上(不会少带数据目录),下面的「其他条目 / 未归属配置」两行也补上了整块勾选框;层级也一眼能看出来了 —— 每一层子项旁边都有竖线并逐层缩进(不只是最里面那一层),不再看起来和父项平级;分区列表也补上了分组标题(通用 / AI / 插件与扩展 …),并把几个容易混淆的分区改成一眼能读懂的名字(插件清单 / 插件散落配置 / 插件数据目录 / 插件大文件 / 配置管理器设置)。归属只写能证明的关系:插件散落配置没有任何包名信息,所以留在「未归属」栏里,不会被硬塞给某个插件;注册表提到但本机其实没有的区域会明确标「无内容」,而不是给一个勾了却导不出东西的选项。 **The plugin area of the export picker is now a tree grouped by plugin**: all plugins sit under a single "Plugins" block (collapsed by default — expand it to list them), and a plugin's list entry, data dirs and large files all hang under its name, so one click takes everything that belongs to it; section content loads on demand (expanding a row reads it — no more "Read this section" buttons) and leftover data of uninstalled plugins is no longer listed; the "n/m selected" count no longer jumps when you expand a row, ticking "Plugins" or a single plugin waits for the section to load and then brings its data dirs along (so nothing is left behind), the two rows below ("other entries" / "unowned config") got a whole-block checkbox, and nesting is visible at a glance — every level of children carries its own vertical guide line and indent (not only the innermost one), instead of looking like siblings; section lists gained group titles (General / AI / Extensions …) and the easily confused plugin sections were renamed (plugin list / plugin config files / plugin data dirs / plugin large files / Config Manager settings). Ownership is only shown when it can be proven — loose config files carry no package name and stay in an "unowned" bucket instead of being assigned to a plugin, and areas the registry knows about but this machine does not have are marked "no content" rather than offered as a tick that exports nothing. - **回退不再误伤别的对话改过的文件**:同一个项目里开着多个对话时,在一个对话里回退会把「撤销这个对话的改动」与「保留其它对话后来的改动」**合并**到一起,而不是拿旧快照整文件覆盖;**即使认不出对方是谁也一样会合并**,不会因为没记录到对方就悄悄覆盖;两边改到同一处时保持原样并明确提示(绝不擅自选边),其它对话新建的文件也不会被删掉。**回退预览里看到的就是合并后的结果**(别的对话那几行不会再被标成「会被删掉」)。**无法确定该撤销哪一部分时会如实拦下**(一个字节都不写、界面上明确标出「不会改动」),绝不静默跳过或假装「没有东西可退」。结果里如实给出合并 / 冲突 / 无法安全撤销 / 回退途中又被改过的条数;想额外恢复哪些文件,还可以在回退预览里逐条勾选(设置页「维护与诊断」的回滚卡)。 **Rewinding no longer discards changes made by other conversations**: when several conversations share one project, rewinding in one of them now merges "undo this conversation's change" with "keep what the other conversations changed afterwards" instead of overwriting the whole file with an old snapshot, and it keeps merging even when the other side cannot be identified from the recorded ownership; when both sides changed the same place the file is left as it is and reported (never silently picking a side), and files created by the other side are no longer deleted. The preview shows the merged result (lines from the other conversation are no longer marked as about to be deleted). When it cannot tell which part should be reverted it stops honestly (writes nothing and marks the file as "left unchanged") instead of silently skipping. The result honestly lists how many files were merged, conflicted, could not be reverted safely, or changed again while the rewind ran; you can also tick individual files in the rewind preview (the rollback card under Settings → Maintenance & diagnostics). ### 🔧 变更 · Changed - **整个插件界面改用 DSH 自己的设计取值**:按钮、输入框与下拉、复选框、徽章与横幅、卡片、数据表、分段切换、顶部页签、弹窗、下拉菜单、通知与说明气泡的圆角、描边粗细(控件与分隔线改为 DSH 同款的 0.5px 发丝线)、悬停/按下/禁用反馈、阴影与浮层底色,全部改从 DSH 正式发布的主题变量取,不再使用插件自造的近似色与自定档位;文字档位也随之接到 DSH 自己的字号档。另一处细节:键盘焦点环与 DSH 原生控件行为一致 —— **鼠标点击不再画出焦点框**,只有键盘操作时才显示。视觉改动全部是取值来源与规格层面,导入导出、备份恢复、同步等功能的操作流程与数据格式没有任何变化。 **The whole plugin UI now draws from DSH's own design values**: buttons, inputs and dropdowns, checkboxes, badges and banners, cards, tables, segmented switchers, top tabs, dialogs, menus, toasts and hint bubbles now take their corner radii, stroke weights (controls and dividers moved to DSH's 0.5px hairline), hover/pressed/disabled feedback, shadows and surface colors from DSH's published theme variables instead of the plugin's own approximations and hand-made steps, and the type scale now maps onto DSH's own font steps. One behavioral detail follows the native controls too: **a mouse click no longer paints a focus ring** - only keyboard navigation does. All of it is source-and-spec level: workflows and data formats for import/export, backup/restore and sync are unchanged. - **成功 / 信息 / 警告 / 错误四种状态徽章现在真的分色**:此前四态徽章长得完全一样(统一的灰色描边),因为对应的配色类名从来没有被样式表定义过;现在按 DSH 原生标签的调色板着色(绿 / 蓝 / 琥珀 / 红的同色淡底 + 同色文字),一眼能看出哪条是成功、哪条是警告。 **Success / info / warning / error badges finally carry their color**: the four states used to look identical (one neutral outline) because the matching style names were never defined in the stylesheet; they now use DSH's own tag palette (green / blue / amber / red tinted fills with matching text), so success and warning rows are distinguishable at a glance. - **关掉「回合自动留点」后,聊天里的用户消息完全交回 DSH 自己渲染**:以前关掉只是收起那颗「↺ 回退」按钮,气泡本身仍由插件代画(外层间距、时间与操作按钮都是插件的);现在关闭后插件不再接管这条消息,气泡、时间与操作按钮都与原版一致,重新打开开关时入口立刻回来(不必重载插件)。 **With "auto rollback points" off, your chat messages hand back to DSH's own rendering**: turning the switch off used to only hide the "↺ rewind" button while the plugin still drew the bubble (its spacing, timestamp and action row were ours); now the plugin stops taking over the message entirely, so bubble, timestamp and actions match the native rendering, and flipping the switch back on brings the entry back immediately (no plugin reload). - **开着「回合自动留点」时,你发出的消息气泡现在与 DSH 原版逐项一致**:圆角、内边距、附件与气泡的间距、时间戳的字号与位置、复制/回退那一行图标的大小与悬停底色、文件卡(240×64 的卡片样式:类型图标 + 文件名 + 扩展名与大小)、引用会话那一行,全部按 DSH 自己的取值来画;**复制与回退用的也是 DSH 原生的那几颗图标**(复制成功时和原版一样直接换成对勾,不再播放插件自己的形变动画);「后面还有消息的那几条」也照原版只在鼠标移上去或键盘聚焦时才显示这行图标,最后一条常显 —— 差别只剩那颗多出来的「↺ 回退」按钮。 **With "auto rollback points" on, the messages you send now match DSH's own bubble item by item**: corner radius, padding, spacing between attachments and bubble, the timestamp's size and position, the size and hover background of the copy/rewind icon row, the 240×64 file card (type icon + name + extension and size) and the "conversation reference" line all follow DSH's own values; the copy and rewind buttons now use DSH's own icon set too (a successful copy swaps to the check mark exactly like the native bubble, instead of playing the plugin's morph animation); like the native bubble, that icon row is revealed on hover or keyboard focus for messages that have a later message and stays visible on the last one — the only difference left is the extra "↺ rewind" button. ### 🐛 修复 · Fixed - **导入向导最后一步的按钮不再贴着勾选框**:确认页里「上一步 / 确认导入」与上方「失败时整体回滚」之间补上了与其它区块一致的间距,勾选框和按钮不再挤在一起。 **The import wizard's final buttons no longer touch the checkbox**: on the confirm step, "Back / Import" now keeps the same block spacing as everywhere else from the "Roll back everything on failure" checkbox above it, instead of the two being crammed together. - **「信息态」的蓝色此前基本没生效**:任务面板的底色、状态点与行首色条引用了一个 DSH 里**并不存在**的颜色变量,浏览器只能把它们当成「没有颜色」,于是这几处一直是透明/继承色;现在接回 DSH 的蓝色主色,信息态该有的底色与色条都回来了。 **The "info" blue barely showed up before**: the task panel fill, the status dot and the leading color bar referenced a color variable DSH does not actually define, so the browser treated them as "no color" and those spots stayed transparent/inherited; they now use DSH's own blue, so the info fill and bar are back. - **用户消息气泡的字号重新跟随「内容字号」**:设置 → 外观 → 内容字号调大后,你发出的消息此前仍固定在小字号(调到 21px 时助手正文明显变大、自己的话只有一半大),而且换行位置也和原生气泡不一样;现在气泡的字号、行距与换行宽度都跟随同一个设置。(issue #93) **Your own message bubbles follow the content font size again**: after enlarging Settings → Appearance → content font size, messages you sent stayed at a small fixed size (at 21px the assistant text got visibly larger while your own words were about half the height) and wrapped differently from the native bubble; the bubble now takes the same size, line height and wrap width, following the same setting. (issue #93) - **DSH 0.2.1 起用户消息里的图片不再消失**:此前升级 DSH 后,聊天里每条自己发的消息只要有图片,就只剩一行「图片无法显示」的灰字(助手消息、附件卡片都正常);现在这类消息的正文、图片、文件卡与引用整体交回 DSH 自己的渲染,外观与原生气泡一致,「↺ 回退」按钮照旧。(issue #92) **User-message images no longer disappear on DSH 0.2.1**: after upgrading DSH, any message you sent that carried an image showed only a grey "image cannot be displayed" line (assistant messages and file cards were fine); such messages now hand their text, images, file cards and references back to DSH's own rendering, matching the native bubble pixel for pixel, while the rewind button stays where it was. (issue #92) - **回退对话更少掉进「未分组」**:回退出的新会话会更可靠地挂回原工作区 —— 宿主一时读不到子会话的工作目录时,改用它从父对话继承的目录来判断,并自动重试一次;确实挂不上时,具体原因会写进本机的诊断日志,而不是只留界面上一句话、事后查不出为什么。 **A rewound conversation is much less likely to land in "Ungrouped"**: the session a rewind creates now files back into its original workspace far more reliably - when the host cannot read the child session's directory it falls back to the directory inherited from the parent conversation and retries, and a real failure is written to a local diagnostic log instead of leaving only a one-line UI hint with no way to find out why. ## [0.1.76] - 2026-10-16 > **本版主题**:**修掉「工作区里有 `.asar` 文件就会闪退」** —— 留点扫描会读工作区里的文件,而名字以 `.asar` 结尾的路径在桌面端会被当成安装包容器解析;只要它是损坏的(测试夹具、被打断的打包残留),整个宿主进程会被原生层带走。 > > **Theme**: **Fixes the crash when the workspace contains `.asar` files** — taking a rollback point reads the workspace, and a path ending in `.asar` is treated as an archive container by the desktop runtime; when such a file is broken (a test fixture, a leftover from an interrupted pack) the native layer takes the whole host process down. ### 🐛 修复 · Fixed - **工作区里有 `.asar` 文件时不再让桌面闪退**:留点扫描会读取工作区里的文件,而名字以 `.asar` 结尾的路径在桌面端会被当成安装包容器解析 —— 只要它是损坏的,整个宿主进程会被带走,桌面直接退出且没有任何报错(日志里只有一行容器解析失败)。现在这类路径一律不读,只如实记进「跳过」清单(照旧不会被当成新建文件删掉)。 **A workspace containing `.asar` files no longer crashes the desktop**: taking a rollback point reads the files in your workspace, and paths ending in `.asar` are treated as archive containers by the desktop runtime — if such a file is broken the host process is taken down and the desktop quits with no error at all (the log shows only one failed container parse). Those paths are now never read; they are recorded in the skipped list instead (and still never mistaken for files created later). ## [0.1.75] - 2026-10-16 > **本版主题**:**修掉「发出消息后桌面闪退」** —— 回合开始时的自动留点把「整点的文件内容预算」当成了「同时在读的上限」,在大工作区里可以同时把几百 MB 内容留在内存,于是发出消息后一两秒 DSH Desktop 会被系统直接终止。 > > **Theme**: **Fixes the desktop crash right after sending a message** — the automatic rollback point taken when a turn starts treated the whole capture budget as the limit for content read at once, so on a large workspace several hundred megabytes could be resident simultaneously and DSH Desktop was killed by the system one or two seconds after sending. ### 🐛 修复 · Fixed - **大工作区里发出消息不再让桌面闪退**:回合开始时的自动留点此前会把大量文件内容同时留在内存(大仓库下可达数百 MB),表现就是发出消息后一两秒桌面直接退出、没有任何报错。现在「同时在读的内容」有独立的小上限,留点的速度、覆盖范围与回退体验都不变。 **Sending a message on a large workspace no longer crashes the desktop**: the automatic rollback point taken when a turn starts used to hold a large amount of file content in memory at once (hundreds of megabytes on a big repository), which made the desktop quit with no error one or two seconds after sending. Content read at the same time now has its own small limit, while capture speed, coverage and rewinds stay as they were. ## [0.1.74] - 2026-10-16 > **本版主题**:**回滚点与回退全面提速** —— 大工作区的留点不再「30 秒只扫到一小半」,第二个点起就记全 3.4 万条;回退预览改走文件身份快路径(实测 14.2 秒 → 2~3 秒级),回滚点清单改成每个点只存相对上一点的增量(稳态 12 MB → 11 KB)。同一轮里,差异预览不再只画前 400 行并跟随侧边栏高度,个别目录没扫到不再让「留点后新建的文件」整批不删,推送前的只读预览也不再偷偷把本地插件打包一遍(issue #91)。 > > **Theme**: **Rollback points and rewinds are much faster across the board** — capturing a large workspace no longer fits only a fraction of itself into one pass (34k files, fully recorded from the second point on), the rewind preview takes a file-identity fast path (measured 14.2 s → the 2–3 s range), and the point ledger now stores per-point deltas (steady state 12 MB → 11 KB). In the same release the file diff no longer stops at the first 400 rows and follows the sidebar height, files created after a point are no longer all kept just because one subtree was not walked, and the pre-push preview no longer packs local plugins behind your back (issue #91). ### 🐛 修复 · Fixed - **点开「回退」快多了**:预览与回退此前会把记录在案的每个文件都真读一遍再比哈希(3 万+ 文件的工作区点上十几秒);现在先看文件身份(大小 / 修改时间 / inode)有没有变,没变的直接跳过,只读真正变过的那几条。实测 3.4 万文件的点:预览从 14.2 秒降到 2~3 秒级,回退也省掉同一轮全量读取。 **Opening a rewind is much faster**: the preview and the rewind used to read and hash every recorded file (ten-plus seconds on a 30k-file workspace); they now check each file's identity (size / mtime / inode) and skip the untouched ones, reading only what actually changed. Measured on a 34k-file point: the preview went from 14.2 s to the 2–3 s range, and a rewind saves the same full read. - **不再因为「跳过了几个符号链接」就报警**:留点里被设计跳过的东西(符号链接不跟随、文件级忽略等)此前会让每个点都显示「有一态留得不完整(回退可能不完整)」;现在只有真的缺内容(被上限截断、读不出、捕获期间被改写)才提示,跳过的条目照旧逐条留痕。 **No more "incomplete capture" warning just because a few symlinks were skipped**: entries skipped by design (symlinks are not followed, file-level ignores) used to make every point report an incomplete state; the warning now appears only when content is genuinely missing (cut off by a limit, unreadable, or rewritten while capturing), while skipped entries are still recorded one by one. - **回退超时的提示不再断言「没做完」**:客户端超时只代表客户端不再等,服务端可能已经完成;提示改成「过程可能仍在后台进行,请先刷新预览确认」。 **The rewind timeout message no longer claims the work failed**: a client timeout only means the client stopped waiting, and the server may have finished; the message now says the process may still be running in the background and to refresh the preview first. - **大仓库的留点不再只扫一小半**:捕获改成并发读写后,本机 34k 文件 / 580 MiB 的工作区从「30 秒只够扫 1/5」变成第一个点约 11 秒记下 2 万条、**第二个点起约 4–11 秒记下全部 3.4 万条且不再标注截断**(已知内容不再重复读取、也不占每回合的读取额度);没扫完的回合也不再重复扫同一批,而是接着补上次缺口;预算还会自己升降(被截断就放宽、扫得快就收紧)。 **Big repositories are no longer only half captured**: capture now reads and writes concurrently, so a 34k-file / 580 MiB workspace that used to fit only a fifth of itself into a 30-second budget is now captured in one pass (~19 s the first time, ~4 s per turn afterwards); when a pass does not finish, it picks up where it left off instead of rescanning the same prefix, so coverage climbs to 100% over turns, and the budget itself adapts (wider when truncated, tighter when fast). - **忽略一个目录不再等于关掉全部删除**:被忽略、读不出、以及没来得及扫到的子树现在只影响它们自己,范围之外的「留点后新建」路径照常被删。 **Ignoring a directory no longer disables every deletion**: ignored, unreadable and unscanned subtrees now only affect themselves, and files created after the point outside those subtrees are still removed. - **留点期间的并发改写不再混进「回退前的内容」**:与留点同时被改写的文件不再被当成旧内容(避免回退把最新内容当旧内容写回),同时仍被记为「留点时已存在」,绝不会被误删。 **Concurrent edits during a point no longer sneak into the "before" content**: a file rewritten while the point is being taken is no longer recorded as its earlier content (which could otherwise be written back over newer content), while still being recorded as existing at that moment so it is never deleted by mistake. - **推送前的只读预览不再偷偷打包本地插件**:配置里有本地来源(`link:` / `file:`)的插件时,点「推送」后的预览不再执行打包 —— 体积很大的本地插件不会再把预览卡住,也不会连带把这段时间里其它写操作一起挡住;预览若无法判定某个分区是否有变化,会直接说明原因。(issue #91) **Push preview no longer packs local plugins behind your back**: with locally sourced (`link:` / `file:`) plugins in your config, the pre-push preview no longer runs packing — a very large local plugin can no longer hang the preview and block other actions while it runs; when a section's changes cannot be determined, the preview says why instead of guessing. - **推送预览里的「有变化」不再清一色全有**:此前它对每个分区都显示「有变化」;现在与上次同步的基线真实比对,没动过的分区如实显示「无变化」。(issue #91) **"Changed" in the push preview is no longer always everything**: it used to mark every section as changed; it now really compares against the last synced baseline, so untouched sections show as unchanged. - **浅色主题下的用户消息气泡又看得见了**:此前它在浅色主题里与页面底色相同、看起来像没有气泡;现在气泡底色跟随 DSH 自己的气泡色(万一这个主题色缺失,也会回落到一档看得见的中性底色,而不是变透明),消息里文件卡的描边也一并调回 DSH 的深浅(比原来清楚一档),深色主题不受影响。 **User message bubbles are visible again on the light theme**: they used to share the page background colour and looked like they had no bubble at all; the bubble now follows DSH's own bubble colour and a message's file-card outline is back to DSH's own border strength, with the dark theme unaffected. - **自己消息里的引用 / 技能标记与文件图标现在和 DSH 同款**:接管用户消息气泡后这两处此前是简化版(技能名一个都不显示、文件只有扩展名文字徽标);现在改用 DSH 自己的画法 —— 引用会话与技能名显示成可点开的标记,文件显示按类型着色的图标;DSH 版本较老、拿不到这些画法时,仍回落到原来的简化版(不会空白,也不会假装画过)。 **Reference / skill chips and file icons in your own messages now match DSH**: after we took over the user-message bubble these two stayed simplified (skill names were not shown at all, files only got an extension badge); they now use DSH's own rendering — session references and skill names become clickable chips and files get their type-coloured icon — falling back to the simplified version on older DSH builds where those pieces are unavailable. - **回退的文件差异现在把整份文件都画出来**:此前最多只画前 400 行、并提示「只画了前 400 行(共 4187 行)」,剩下的内容在界面上看不到;现在宿主给多少行就画多少行(真正的上限仍是单文件体积那道门,不再是 400 行)。差异保真度也一并提高:几千行文件里的普通改动此前会被判成「差异规模较大,已按整块替换展示」,现在只有整份文件几乎重写时才会退化 —— 一条常被误读为报错的提示,变回真正罕见的事实说明。 **The rollback file diff now draws the whole file**: it used to stop after the first 400 rows and say "only the first 400 of 4187 rows are drawn", hiding the rest; it now draws every row the host provides (the per-file size limit stays as the real bound, the 400-row cap is gone). Diff fidelity improved with it: ordinary changes in a several-thousand-line file used to be reported as a whole-block replacement, which now happens only when the file is almost entirely rewritten — a note that read like an error becomes a rare, factual remark. - **回退的文件预览现在跟着侧边栏的高度走**:此前差异区最多只长到窗口高度的八成(大屏上等于一个固定的 ~800px 上限),侧边栏比它高时底下就留一段空白;现在它随侧边栏一起变高变矮,只有宿主没给高度时才回落到一条视口上限;同一屏里的「加载中 / 一致 / 看不了 / 读失败」也改成占满整块,状态切换时高度不再跳动。 **The rewind file preview now follows the sidebar's height**: the diff area used to stop at 80% of the window height (a fixed `~800px` cap on large screens), leaving blank space whenever the sidebar was taller; it now grows and shrinks with the sidebar, falling back to a viewport cap only when the host gives no height, and the "loading / identical / cannot compare / read failed" states fill the same block so the height no longer jumps between states. - **大工作区里「留点后新建的文件」不再一律不删**:一次留点没扫完整个工作区时,回退预览此前会直接放弃整批删除并提示「证明名单不完整」;现在只要目录确实都走查过,留点之后新建的文件照常被删掉,只有真正没走查到的目录才保持原样(预览仍会说明原因)。 **Files created after a point are no longer always kept in large workspaces**: when a point could not scan the whole workspace, the rewind preview used to give up on the whole deletion batch and warn that its proof list was incomplete; as long as the directories were in fact walked, files created after the point are removed again, and only directories that were genuinely never walked are left alone (the preview still says why). - **点「清理」不再像没反应**:确认后弹窗立刻关闭,卡片上的那颗按钮随即进入「正在清理…」并转圈,清完之后的数字与提示照旧;期间刷新与清理按钮一律禁用,卡片也不会掉回加载骨架。 **Clicking "Clean" no longer looks like nothing happened**: the dialog closes at once, the card's button switches to "Cleaning…" with a spinner, and the same numbers and toasts still report the outcome; while it runs, refresh and clean stay disabled and the card never falls back to a loading skeleton. ## [0.1.73] - 2026-10-15 > **本版主题**:**回滚点不再拖垮宿主** —— 回合自动留点改为默认关闭,台账改成一条一写并带体积上限(此前能涨到几百 MiB,把宿主反复拖到内存溢出退出),关掉开关后聊天里的回退入口一并收起;同时第三方插件的数据也能一起迁移了。 > > **Theme**: **Rollback points no longer drag the host down** — automatic per-turn points are now off by default and the ledger writes one record at a time under a size budget (it used to reach hundreds of MB and repeatedly crashed the host out of memory), with the chat rewind entry disappearing while the switch is off; third-party plugins' data can now be migrated too. ### 🆕 新增 · Added - **回滚点可以关掉了**:回合自动留点默认不再开启;在「环境 → 维护与诊断」里可以随时打开或关闭,关掉后聊天里的「↺ 回退」入口一并收起。(issue #88) **Rollback points can be turned off**: automatic per-turn points are off by default; turn them on or off under Environment → Maintenance & diagnostics, and the rewind entry disappears from chat while they are off. - **台账占用看得见、也清得掉**:回滚面板显示当前台账的点数与体积,并可一键只删自动点(救援点默认保留,要一并删需显式勾选)。(issue #89) **Ledger footprint is visible and reclaimable**: the rollback panel shows how many points the ledger holds and how large it is, and can delete automatic points in one click (rescue points are kept unless you explicitly include them). - **其他插件的数据也能一起迁移了**:wallpaper-engine、mnemon 等第三方插件的配置与数据现在在导出/导入里各成一个可勾选的分区,并与「本插件自身配置」明确区分——配置与小状态进「其他插件数据」,上传的壁纸、本地数据库等大体积文件进「其他插件大文件」,都默认不勾选;这些数据在**系统主目录**下(不在 DSH 自己的配置目录里),可再生成的缓存永不进包,被环境变量改过位置的目录会明说「已跳过」而不是照默认位置读。 **Other plugins' data can now be migrated too**: the config and data of third-party plugins such as wallpaper-engine and mnemon now appear as their own opt-in sections in export/import, clearly distinguished from "This Plugin's Own Config" — config and small state under "Other Plugins' Data", large content such as uploaded wallpapers and local databases under "Other Plugins' Large Files"; both are off by default. This data lives under the **system home directory** (not in DSH's own config directory), regenerable caches never go in, and a relocated directory says it was skipped because an environment variable overrides it instead of reading the default location. - **插件可以自己声明数据目录**:插件在自己的 package.json 里声明 dsh.dataDirs(路径相对**系统主目录**)后,配置管理器会自动识别(同样默认不勾选);声明写错或不认识时整条忽略,不会影响其它插件。 **Plugins can declare their own data directories**: once a plugin declares dsh.dataDirs in its package.json (paths are relative to the **system home directory**) the config manager picks it up automatically (also off by default); a malformed or unknown declaration is ignored entirely and never affects other plugins. ### 🔧 变更 · Changed - **回滚点的「清单」改成增量,磁盘占用大幅下降**:每个点不再重复存一遍「3 万多个文件的完整清单」,只存相对上一点的改动(改了几条就存几条);实测稳定状态下每点从约 12 MB 降到约 11 KB,全量锚点也小了约五分之一。回退预览、回退本身、清理缓存看到的仍然是完整清单,**任何一个点仍然可以单独回退**(不依赖其它点还在不在)。 **Rollback-point manifests are now incremental, cutting disk usage sharply**: each point no longer stores a full listing of every 30k+ file; it stores only what changed since the previous point (a handful of entries on a quiet turn). Measured: about 12 MB per point down to about 11 KB in steady state, with full anchors about a fifth smaller. Previews, rewinds and cache cleanup still see the complete file listing, and **every point remains independently restorable**. - **回滚点默认不启用**:它跟配置备份、同步是两件事,此前却随插件默认运行;升级后要显式打开才会继续留点。(issue #88) **Rollback points are off by default**: they are unrelated to config backup and sync yet used to run automatically; after upgrading you must enable them explicitly for points to be kept again. - **回滚点库不再越用越大**:每次留点只写这一条记录,未变化的工作区清单按内容去重,超出体积预算时自动淘汰最旧的自动点 —— 单次留点的开销不再随历史总量增长。(issue #89) **The rollback store no longer grows without bound**: each point writes only its own record, unchanged workspace listings are deduplicated, and the oldest automatic points are evicted past the size budget, so one point no longer costs more as history accumulates. - **子代理会话不再产生回滚点**:它们的回合与工作区跟父对话相同,在那边留点只会挤占你自己对话的台账空间;你自己的对话照常留点。 **Subagent sessions no longer produce rollback points**: they share their turns and workspace with the parent conversation, so points there only crowded out the ledger space of your own conversations; your own conversations are unaffected. - **台账超出体积预算时会如实提示**:当可自动淘汰的点已经耗尽(例如单个点本身就比预算大),面板会直说「已超预算、请手动清理」,不再显示成一切正常。 **The ledger now says so when it exceeds its size budget**: once there is nothing left to evict automatically (for example when a single point is larger than the budget), the panel states it plainly instead of looking healthy. - **离线命令行备份不覆盖第三方插件数据**:用命令行备份并显式点名这两个分区时,会明确提示改用图形界面的导出/导入,而不是静默不收。 **Offline command-line backups do not cover third-party plugin data**: asking the CLI backup for these two sections now tells you to use the GUI export/import instead of silently leaving them out. - **侧栏差异标签页带文件类型图标**:标签条上那颗芯片改成「文件图标 + 文件名」,与 DSH 自己的文件标签同一观感(宿主的标签定义本身没给图标,是我们自己画上去的)。 **The sidebar diff tab now shows a file-type icon**: the chip reads "file icon + file name", matching DSH's own file tabs (the host's tab definition has no icon field, so we draw it ourselves). - **差异视图改成了编辑器式双栏**(回退预览与快照恢复计划共用):两栏各自带增删配色(红/绿行号 + 行侧色条),上方一行给出文件路径与 `+N/−M`,块头也分栏显示,改了哪几行一眼就能看出来;字号与行距放宽了,每行更高、不再挤成一团,侧栏里的差异区**与侧栏等高**(不再自己留一道滚动条、也不再只显示十来行);行号与左侧那条增删色条之间也留出了呼吸,数字不再贴着色条。 **Diff views now look like an editor's split view** (shared by the rewind preview and the snapshot restore plan): each pane is colored line by line (red/green line numbers with an edge bar), a header line shows the file path with `+N/−M`, and the hunk header appears per pane; the type is larger and the lines are more generously spaced, and in the sidebar the diff area is as tall as the sidebar itself — no scrollbar of its own, and no longer stuck at a dozen lines; the line numbers also have breathing room from the colored add/remove bar instead of sitting against it. ### 🐛 修复 · Fixed - **修回滚点把 DSH 拖到内存溢出退出**:台账曾经每回合整份读改写,涨到几百 MB 后宿主反复因内存不足而退出;现在一条一写、并有体积上限。(issue #89) **Fixed DSH exiting out of memory because of rollback points**: the ledger used to be read and rewritten in full every turn and could grow to hundreds of MB until the host ran out of memory; records are now written one at a time under a size budget. - **清掉崩溃残留的回滚点临时文件**:非正常退出留下的临时文件会在下次启动时清理。(issue #89) **Crash leftovers are cleaned up**: temporary rollback files left behind by an unclean exit are removed on the next start. - **修大工作区留点可能长时间跑不完**:留点现在有 30 秒的时间预算,超时就留下已经扫到的部分并如实标注为部分结果;这种部分点回退时**不会删除任何文件**(无法证明哪些是留点之后新建的),不再无限跑下去。 **Fixed automatic points that could keep running on a large workspace**: capturing now has a 30-second time budget and keeps whatever it already scanned, clearly marked as partial; rewinding to such a point never deletes files (it cannot prove which ones appeared later), instead of running on. - **修大工作区回退时的内存峰值**:为保证回退可撤销而保存的旧文件内容改为先暂存在磁盘上,内存占用不再随「这次要改多少文件」增长,回退结束即清理。 **Fixed the memory peak when rewinding a large workspace**: the previous file contents kept for an undoable rewind are now staged on disk, so memory no longer grows with how much the rewind changes, and the staging is cleaned up when it finishes. - **修回滚点可能整批消失**:台账偶尔读不出来时,下一次自动留点曾会把台账重写成只剩新点、并删掉旧点的内容副本;现在这种情形直接拒绝写入并如实报错,旧点一个都不动。 **Fixed rollback points disappearing in bulk**: when the ledger briefly could not be read, the next automatic point used to rewrite it down to just that new point and delete the old content copies; it now refuses to write and reports the error, leaving old points untouched. - **修回退预览可能配错目标**:先预览 A、在途中改看 B 时,弹窗曾可能列着 A 的文件却按 B 执行;现在只认最后一次预览的结果。 **Fixed a rewind preview that could pair the wrong target**: previewing A and then switching to B mid-flight could list A's files while rewinding B; only the latest preview's result is used now. - **修回退在「读不出来」时误删文件**:某个文件读不出来(被占用 / 无权限)曾会被当成「本来就不存在」,一旦后续出错整体回滚就会把它删掉;现在这种情形直接停手,一个字节都不写。 **Fixed rewinds deleting files they could not read**: a file that could not be read (locked or permission-denied) used to count as "did not exist", so a later failure would delete it during rollback; the rewind now stops before writing anything. - **回退与预览不再枚举整个工作区**:走查同样受时间预算约束,超时只让「会被删的路径」变少(安全方向),并如实标注结果不完整。 **Rewind and preview no longer walk the whole workspace**: the walk is bounded by the same time budget; hitting it only deletes less (the safe direction) and marks the result as incomplete. - **.GIT 等大小写写法同样受保护**:Windows 上不再可能把 .GIT 子树当成「留点后新建」删掉。 **Case variants such as .GIT are protected too**: on Windows, a .GIT subtree can no longer be treated as newly created and deleted. - **回退不再改写 .git**:仓库元数据不再进快照,回退不会再把索引与分支引用写回旧状态(此前跑过提交的回合回退后可能看起来少了一次提交);顺带也让几百 MB 的 .git 不再挤占快照预算。 **Rewinds no longer touch .git**: repository metadata is not captured anymore, so a rewind cannot write the index and refs back to an older state (a commit made during the round could appear to vanish); it also stops hundreds of MB of .git data from crowding out the snapshot budget. - **两个 DSH 实例同时开着时不再互相覆盖回滚点台账**:留点写台账前会先看清单有没有被另一个实例改过,改过就重读合并;连续冲突就放弃这一条并如实记日志(宁可少一条点,也不丢别人的点)。 **Two DSH instances no longer overwrite each other's rollback ledger**: before writing, it checks whether another instance changed the ledger, re-reads and merges, and if it keeps changing it gives up that one point and says so in the log (a missing point beats a lost ledger). - **界面不再把没读到的开关状态显示成「已关闭」**:偏好还没读回来时开关显示未勾选且禁用,读回来后才显示真实值;若磁盘偏好与当前进程不一致,会直说分歧而不是替磁盘打包票。 **The UI no longer shows an unread switch state as "off"**: until the stored preference is read the switch stays unchecked and disabled, and if it disagrees with this process the UI says so instead of vouching for the disk. - **回退预览不再把符号链接谎报成「会被创建 / 覆盖」**:链接一律按不可写如实呈现(写入侧本来就会拒绝它),两边口径一致。 **The rewind preview no longer reports symlinks as "will be created / overwritten"**: links are shown as not writable, matching what the write path actually does. - **多代回退链不再给出重复条目**:同一个回合只取最近一层祖先的点,条目不会再指向两个不同状态。 **Multi-generation rewind chains no longer list duplicates**: a turn resolves to the nearest ancestor's point only, so entries no longer point at two different states. - **「部分恢复」不再是个没头没尾的结论**:文件段没有执行时,结果屏会明说「未执行的段」是哪个;「回退文件的更改」开关在该段没有可用记录时显示为**未勾选**(不再勾着却点不动,让人以为文件会退)。 **"Partially restored" is no longer a dead end**: when the file segment is not executed the result screen names the segment that was skipped, and the "revert file changes" switch shows as unchecked when that segment has no usable record (instead of looking checked while being unusable). - **删掉回滚点后,聊天里的「↺ 回退」立刻跟上**:此前 30 秒内那颗按钮仍可能显示可点,点开却说「这个回合还没有回滚点」。 **Deleting a rollback point is reflected in chat right away**: for up to 30 seconds the rewind button could still look clickable and then say the turn has no rollback point. - **同步差异不再把「本机已一致」的凭据列为待写入**:本机已有且与快照完全相同的密钥直接标成「跳过」,只有真的会改动本机的才需要你确认;凭据项的标签也从「缺密钥」改为「凭据迁移」。(issue #38) **Sync diff no longer asks about credentials that already match**: keys identical to the snapshot are marked as skipped, so only the ones that actually change need confirmation; the credential badge is renamed from "missing secret" to "credential migration". ## [0.1.72] - 2026-10-13 > **本版主题**:**对话回退上线** —— 聊天里每条用户消息旁都能一键回到那一刻,原对话完整保留、可以接着聊,工作区文件也可以一起退回去。 > 同步通道补上「为什么没同步 / 怎么自救」,另有二十余处真机反馈修复。 > > **Theme**: **Rewind your chat** — a one-click rewind on any user message takes you back to that moment while the original > conversation stays intact and can be continued, and workspace files can be reverted along with it. The sync channel now > explains why it skipped and can repair itself, plus twenty-odd fixes from real-device reports. ### 🆕 新增 · Added - **对话回退**:聊天里每条用户消息旁都有「回退到这一刻」;回退会开出一条新的分支对话,原对话完整保留。 **Rewind your conversation**: every user message carries a "rewind to here" action; it opens a new branch conversation and leaves the original untouched. - **连文件一起退回**:回退时可以勾选把工作区文件一并撤回到那一刻;默认只退对话、不动文件。 **Rewind files too**: a checkbox in the rewind dialog also restores the workspace files to that moment; by default only the conversation is rewound. - **回退预览**:动手前先告诉你有几个文件、增删多少行,列清会被改动的文件,并可逐个查看「现在 → 回退后」的差异 —— 聊天气泡里与设置页的回滚面板都是这一屏。 **Rewind preview**: before anything happens you see the scale (files, added/removed lines), the affected file list, and a per-file diff of "now → after rewind" — the same screen in chat and in the settings rollback panel. - **回滚点台账**:按对话分组列出所有可回退的时刻,左右分栏查看,对话显示真实标题。 **Rollback ledger**: every rewindable moment, grouped by conversation with real titles, in a conversation list plus its points. - **自动救援点**:每次回退前自动留一个保护点,退错了还能再退回来;点数超限时淘汰最旧的。 **Automatic rescue points**: a protective point is captured before every rewind so a wrong rewind can be undone; the oldest ones are dropped once the limit is reached. - **同步可带 MCP 配置**:MCP 服务器配置可以随同步带到别的机器(可选分区,需要手动打开)。(issue #78) **MCP configs over sync**: MCP server configuration can now travel to another machine with a sync (opt-in section, enabled manually). - **工作副本可自救**:同步里直接显示工作副本指向哪个仓库;地址改错时可一键「重指向」或「重新克隆」。(issue #79) **Working copy self-repair**: the sync page shows which repository the working copy points at, with one-click "re-point" and "re-clone" when the address changed. - **自动同步说清原因**:不再静默跳过,页面上直接写明这一轮为什么没同步。(issue #79) **Auto sync explains itself**: instead of silently skipping, the card states why the last run did not sync. - **磁盘占用看清回滚点库与旧检查点**:磁盘体检分别列出回滚点库与升级前遗留的旧检查点数据各占多少、多少文件(只报告、绝不清理)。 **Rollback data in the disk report**: the disk checkup now lists the rollback-point store and the pre-upgrade checkpoint data separately, with their size and file count (report only — never cleaned up). - **聊天里直接 `/rollback`**:输入 `/rollback` 列出本对话可回退的时刻;带上某个时刻的编号再输一次,就回退到那一刻,不必离开聊天。 **`/rollback` in the chat**: type `/rollback` to list the rewindable moments of this conversation, then run it again with one of those ids to rewind — no need to leave the chat. - **回退可以跳过你指定的目录**:在项目里放一个 `.dsh-rollback-ignore`(写法与 .gitignore 常用子集一致)就能让留点与回退跳过匹配的目录与文件;内置依赖目录不可被取消。整个目录被跳过时,「该点之后新建的文件」的删除会整体拒绝(宁可不动)。 **Skip directories you choose**: a `.dsh-rollback-ignore` file in the project (the usual .gitignore subset) makes capture and rewind skip matching directories and files; the built-in dependency directory cannot be re-included. When a whole directory is skipped, deleting files created after that point is refused outright rather than done partially. - **历史分支对话可以一次性归位**:早期回退留下、仍停在「未分组」的分支对话,可以通过一次显式操作按目录收回其所属工作区,并逐条给出结果(会处理全部有父对话的分支,含子代理对话)。 **Re-home old branch conversations**: branch conversations left ungrouped by earlier rewinds can be re-homed into the workspace their directory belongs to with one explicit action and a per-item result (every conversation with a parent is processed, subagent conversations included). ### 🔧 变更 · Changed - **「检查点」改为「工作区 + 会话回滚」**:旧的「三个状态绑在一个点」模型下线,改为按回合自动留点;升级后旧数据仍留在磁盘上,只是不再读取,也不会被删。 **"Checkpoints" became "workspace + session rollback"**: the old three-states-at-one-point model is replaced by one automatic point per turn; old data stays on disk untouched and is simply no longer read. - **聊天回退入口只剩「每条用户消息」一种**:之前的「整回合」档与对应设置项一并删除。 **One rewind entry shape**: the chat offers only "one per user message"; the old whole-turn mode and its setting are gone. - **对话标题优先显示真实标题**:导出、同步、会话体检、回滚台账里都显示名字而不是编号,从未提问过的空白会话显示「新会话」。 **Real conversation titles**: export, sync, session checkup and the rollback ledger show names instead of ids, with never-used sessions labelled "New Session". - **「从其它 agent 导入」的来源按「本机已安装优先」排序**:装了的排在前面,不再需要逐条读「未检测到」。 **Import sources sorted with what you have first**: installed agents move to the top, so you no longer read past "not detected" rows. - **说明性文字收进 ⓘ**:页面只留标题、控件与数字,解释性段落移到提示图标里。 **Explanatory prose moved behind ⓘ**: pages keep titles, controls and numbers; the reasoning lives in the info icon. - **「维护与诊断」重排**:先看异常、再看诊断,诊断卡不再默认折叠,回滚点台账改为弹窗查看。 **Maintenance page reordered**: exceptions first, diagnostics second, diagnostic cards no longer start collapsed, and the rollback ledger opens in a dialog. - **回退 = 无感替换**:回退后会话列表里只留下那条「退回去的对话」,不会再并列出一条旧分支。 **Rewind is a silent swap**: afterwards the conversation list shows only the rewound conversation, with no stale branch beside it. - **回退预览一眼看清**:弹窗顶部直接写明这次要改几个文件、增删多少行、删掉几个;文件行按列对齐、显示所在目录,**点文件名就能看这个文件的差异**,取消勾选时也还能读清清单。 **Rewind preview at a glance**: the dialog states up front how many files change, how many lines are added or removed and how many are deleted; rows line up in columns with their directory and **clicking a file name opens its diff**, and the list stays readable when the checkbox is off. - **一批界面体验修复**(会话体检、同步、市场、首页、产物库、确认弹窗):只改呈现与状态反馈,不改工作流。 **A batch of UI polish** (session checkup, sync, market, home, library, confirm dialogs): presentation and feedback only, no workflow changes. - **回退留点更快、上限更大**:没变过的文件不再每次重算指纹,大仓库里留点明显更快;单个回滚点的默认上限从 2000 个文件 / 64 MiB 放宽到 20000 个文件 / 512 MiB。 **Faster capture, larger limits**: unchanged files are no longer re-hashed on every capture, so big projects capture noticeably faster; the default per-point limits grew from 2,000 files / 64 MiB to 20,000 files / 512 MiB. - **对话正在运行时默认只回溯消息**:回退弹窗会写明这条对话正在运行,默认只退对话、不动文件;要连文件一起退,等这一轮结束后重新打开弹窗即可。 **While a conversation is running, only the conversation is rewound by default**: the rewind dialog says so, and rewinding files waits until the current turn finishes — reopen the dialog then. ### 🐛 修复 · Fixed - **回退后可以继续对话**:修掉退回出来的对话一发消息就报错的问题。 **Keep chatting after a rewind**: fixed the error thrown when sending a message in a rewound conversation. - **回退出来的对话不再跑到「未分组」**:修好回退后对话落到未分类工作区的问题;归位失败也不再无声无息。 **Rewound conversations land in the right workspace**: fixed conversations falling into "ungrouped", and a failed placement is no longer silent. - **回退主按钮不再永久灰掉**:修掉按钮一直不可点、以及回滚点还没读完就被禁用的问题。 **The rewind button no longer stays disabled**: fixed the permanently greyed-out button and the one disabled while points were still loading. - **回退结果不再谎报**:预览与实际执行共用同一份计划,部分回滚不再被说成整体成功,「无需恢复」也不再一句话盖住四种不同情况。 **Rewind results no longer lie**: preview and execution share one plan, a partial rollback is no longer reported as full success, and "nothing to restore" no longer hides four different situations. - **改了仓库地址后同步不再静默失效**:地址变了会明确提示,并给出修复入口。(issue #79) **Changing the repository URL no longer breaks sync silently**: a mismatch is surfaced with a way to fix it. - **配了代理也能列出仓库**:出站请求补上缺失的请求头。(issue #86) **Repositories list behind a proxy**: outbound requests now send the missing header. - **技能自带的依赖目录不再让定时备份失败**:插件自带的依赖目录不再被当成要备份的内容。(issue #76) **A skill's bundled dependencies no longer break scheduled backups**: that folder is excluded from what gets backed up. - **崩溃残留不再永久锁住安全模式**:只有能自证「没写过用户配置」的残留才会被自动放行。(issue #87) **Crash leftovers no longer lock safe mode forever**: only leftovers that provably never wrote user config are cleared automatically. - **切换到英文后不再夹中文**:英文界面里残留的中文全部补齐。 **No more Chinese in the English UI**: the remaining hard-coded strings are translated. - **首页状态条不再被裁切 / 变形**:英文标签不再重叠,中文不再被截断。 **The status bar no longer clips or deforms**: English labels stop overlapping and Chinese text is no longer cut off. - **界面动效恢复**:整套过渡动画此前被意外丢弃,所有页面一直在「瞬变」。 **Motion is back**: the whole set of transition tokens had been dropped, so every page switched instantly. - **远端有冲突时不再连坐推送**:冲突时不再顺手把本机配置推上去。 **A remote conflict no longer pushes your config**: a conflicting merge no longer uploads local settings as a side effect. - **环境锁不再误报「已锁定」**:锁文件缺失时不再直接判成被占用。 **The environment lock stops reporting a false "locked"**: a missing lock file no longer means "in use". - **备份不再误报循环引用**:共享引用的配置不再被判成异常。 **No more false circular-reference reports**: shared references in settings no longer look like a loop. - **导出选择器可以逐个挑选分区**:默认未勾选的分区也能展开和单项勾选。 **Pick sections in the export picker**: unselected sections can now be expanded and ticked individually. - **大批量回退不再冒充「参数不合法」**:待删文件过多时明确说明「一条都没动,工作区与会话都未被改动」。 **Too many paths to delete is no longer "invalid request"**: it now says plainly that nothing was touched. - **工作区快照不再把大文件整份读进内存**,也不再静默丢掉超出预算的文件。 **Workspace snapshots stop reading huge files into memory** and no longer drop over-budget files silently. - **宿主文件「存在但读不出来」不再被记成「当时不存在」**:避免导入前快照漏记而误删。 **A host file that exists but cannot be read is no longer recorded as "absent"**, which used to cause wrong deletions. - **清理存档时,引用列表读不出来不再按空列表继续删**:读不到就停手。 **Cleanup no longer proceeds on an unreadable reference list**: it stops instead of deleting. - **聊天里的用户消息与 DSH 原生完全一致**:时间格式、附件顺序、体积文案、会话引用摘要等差异全部对齐。 **User messages now match DSH natively**: time format, attachment order, size wording and session-reference summaries all line up. - **一批 CLI 与离线救急台修复**:会话体检、回滚点预览与只读检查的输出与退出码对齐。 **A batch of CLI and offline recovery-console fixes**: session checkup, rewind preview and read-only checks agree on output and exit codes. - **回退出来的对话也能一键回退**:之前回退开出的分支对话里,消息旁的回退入口一直是灰的;现在会沿原对话找到对应那一刻,正常回退。 **Rewound conversations can be rewound again**: the rewind entry in a branch conversation used to stay greyed out; it now finds the matching moment in the original conversation. - **子对话没能归位时不再误报回退失败**:回退本身已经成功、只是分组没跟上时,会如实说明,不再报「回退失败」。 **A failed re-home no longer looks like a failed rewind**: when the rewind itself succeeded but the grouping did not, it says so instead of reporting a failure. - **忽略目录很多时不再拖垮回滚点台账**:逐条留痕有上限、计数仍如实,也因此不会误判成「可以安全删除」。 **Many ignored directories no longer bloat the ledger**: per-item records are capped while counts stay exact, so the "safe to delete" judgement cannot be fooled. ## [0.1.71] - 2026-10-08 > **本版主题:修「从其它 agent 导入」在 v4 桌面端整批丢会话** —— 外部来源的会话转码器此前只认 DSH 会话格式 v3, > 而桌面端(0.2.0-rc.2)声明的是 v4,于是每一条历史对话都被 `session-format-unsupported` 跳过、产出的包里 > 根本没有 `sessions` 分区(用户看到的正是「历史对话不见了」)。现在**按目标机格式版本写出**:v3 目标写 v3、 > v4 目标写 v4(反向不可读,所以绝不一律写最高版本),并修掉会让 v3 产物被 v3→v4 迁移整份拒收的 > `session/title` 形状。两代产物均已用 DSH 自己的真 codec 逐条复验(`verified` / `strong` / > `equivalentToReadPath` 全 true)。 > > **Theme**: fix "import from other agents" dropping every session on a v4 Desktop build — the foreign-session > transcoder only knew DSH session format v3 while the Desktop app (0.2.0-rc.2) declares v4, so every historical > conversation was skipped as `session-format-unsupported` and the produced bundle carried no `sessions` section > at all (the user-visible symptom was "my history is gone"). It now writes **the target's own format version** > (v3 target → v3, v4 target → v4; reverse reads are unsupported, so it never blindly writes the highest), and fixes > the `session/title` shape that made a v3-labelled artifact rejected wholesale by the v3→v4 migration. Both > generations were verified row-by-row with DSH's own real codec (`verified` / `strong` / > `equivalentToReadPath` all true). ### 🐛 修复 · Fixed > **外部来源的历史会话导入在 v4 目标的 DSH 上整批消失(桌面端 0.2.0-rc.2 实测)**:DSH 桌面端声明的会话格式是 > **v4**,而外部来源的会话转码器只实现了 v3,宿主又如实把 v4 透传成 `targetSessionFormatVersion` ⇒ 每一条会话都被 > `session-format-unsupported` 跳过,产出的包里**根本没有 `sessions` 分区**,用户看到的是「历史对话不见了」 > (真机实测:`POST /foreign-import {source:"claude-code"}` → `sections:["mcp","skills"]`、`sessions.transcoded: 0`)。 > 现在支持 `[3, 4]` 并**按目标机版本写出**(v3 目标写 v3、v4 目标写 v4)—— 反向不可读(v3 主机读不了 v4 日志), > 所以绝不一律写最高版本;读不到目标版本时仍旧**如实报码、一条都不转**(不猜)。 > 同时修掉一条**会让 v3 产物在 v4 主机上被迁移链整份拒收**的形状问题:`session/title` 原先停在 seq 0 且恒写 > `messageSeqs: []`,违反 DSH 的 `assertTitleSources`(空 **⟺** `source.kind === 'user'`;非空时每个 seq 必须 > **早于**本条事件且指向 `source.kind === 'user'` 的 `user/message`)。现在标题行插在首条人类消息**之后**并引用它 > (与真机日志同形:`{"seq":12,"messageSeqs":[7],"source":{"kind":"fallback"}}`);整场没有人类消息时如实按 > `session-empty` 跳过,而不是编一条指不到人的引用。 > - **复验口径(2026-10-08 本机真 codec)**:逐条跑 `verifySessionLogBytes`,v3 档用本机 CLI 档案树的 > `@deepseek-ai/dsh-session`、v4 档用桌面端 `app.asar` 内抽出的 `dsh-session-format-catalog` —— > 两代产物均 `verified` / `strong` / `equivalentToReadPath` 全 true(修前 v4 档 strict 档报 > `messageSeqs must be empty exactly for a user title`,且 v3→v4 迁移直接 `finish-failed` 拒收)。 > - **护栏**:`src/routes/foreign.test.ts`(路由层 v4 产包 + 「读不到版本仍不猜」并排钉住)、 > `src/foreign/claude-sessions.test.ts`(v4 文件名/header 同步)、`src/foreign/session-ir.test.ts`(标题行三条规则)、 > `src/foreign/claude-sessions-bytes.test.ts`(字节基线,变更理由写在 GOLDEN 上方)。 > - **行为变化(如实记)**:只有标题、没有任何消息的会话(如 Claude 只有 summary/attachment)从「产出只有标题行的 > 会话」变为 `skip:session-empty` —— 非 user 来源的标题引用不到任何人类消息,宁可如实跳过。 > > **Foreign-agent session import silently dropped everything on a v4 DSH build (measured on Desktop 0.2.0-rc.2)**: > the Desktop app declares session format **v4** while the foreign-session transcoder only implemented v3, and the host > honestly forwards v4 as `targetSessionFormatVersion` — so every session was skipped as `session-format-unsupported` > and the produced bundle contained **no `sessions` section at all** (measured: `POST /foreign-import {source:"claude-code"}` > → `sections:["mcp","skills"]`, `sessions.transcoded: 0`). It now supports `[3, 4]` and writes **the target's own > version** (v3 target → v3, v4 target → v4); writing the highest version unconditionally would break v3 hosts > (reverse reads are unsupported), and an unknown target version still reports codes without transcoding anything. > Also fixed a shape bug that made a v3-labelled artifact **rejected wholesale by the v3→v4 migration**: `session/title` > sat at seq 0 with `messageSeqs: []`, violating DSH's `assertTitleSources` (empty **iff** `source.kind === 'user'`; > otherwise every seq must be **earlier** than the event and cite a human `user/message`). The title row is now placed > after the first human message and cites it (same shape as real logs), and a session with no human message is skipped > as `session-empty` instead of fabricating an unresolvable reference. > - **Verification (2026-10-08, real codec on this machine)**: every produced log was run through > `verifySessionLogBytes` — v3 against the CLI profile tree's `@deepseek-ai/dsh-session`, v4 against the catalog > extracted from the Desktop `app.asar` — `verified` / `strong` / `equivalentToReadPath` all true for both > (before the fix the v4 strict pass reported `messageSeqs must be empty exactly for a user title` and the v3→v4 > migration refused the artifact outright). > - **Guards**: `src/routes/foreign.test.ts`, `src/foreign/claude-sessions.test.ts`, `src/foreign/session-ir.test.ts`, > `src/foreign/claude-sessions-bytes.test.ts` (byte baseline with the reason recorded above it). > - **Behaviour change (stated honestly)**: a title-only session (e.g. Claude with just a summary/attachment) now > reports `skip:session-empty` instead of producing a title-only conversation. ## [0.1.70] - 2026-10-07 > **本版主题:把「来源」与「通道」两头同时拓宽** —— 外部 agent 迁移来源一次从 6 个补齐到 30 个(新增统一会话 IR 层), > 同步通道从 git / WebDAV 扩到 6 类云端点(S3 / OSS / COS / MinIO / Kodo / Gist)并支持直接选/新建私有仓库; > 同时修掉本轮上报的 issue #69–#75 —— 其中一个是数据丢失级(`!!js` 方言让整层 patch 行从导出/同步里消失), > 另有两个属于「报成功但其实没做成」(#71 备份不到外壳的 MCP / Skills、#73 一键同步永不收敛)。 > > **Theme**: this release widens both ends of the pipe — foreign-agent import sources go from 6 to 30 (on a new > shared session IR layer), and the sync channel grows from git / WebDAV to six cloud endpoints plus a > private-repo picker. Reported issues #69–#75 are fixed, including a data-loss one (the `!!js` dialect silently > dropped whole patch layers from export/sync) and two "reported success while nothing happened" bugs (#71, #73). ### 🆕 新增 · Added > **外部 agent 迁移来源一次补齐到 30 个(+24)**:此前只认 6 个来源(Claude Code / Hermes / Cursor / Codex / > Copilot / Antigravity),现在把生态里其余 24 个会话来源一并接进来:Gemini CLI、Reasonix、OpenCode、Mimocode、 > ZCode、Grok Build、OpenClaw、Pi、Kimi、Kilo Code、Qoder、ChatGPT、WorkBuddy、千问办公、Continue、Cline、 > Goose、DSH V4、Zed、Crush、TeleAgent、Trae、Vibe、DSH。新增统一的会话 IR 层(解析 → 中间表示 → 合成 → 编码), > 每个来源只需写「读盘 + 转成草稿」两件事,不再各拼一遍 DSH 行格式。 > - **取证强度是类型上的必填字段**(`measured` / `fixture` / `documented`)——「只有文档落点却当成已验证列出」 > 在类型层就不可能悄悄发生;来源列表与 CLI 摘要都会说明每个来源凭什么被列出来。本批 24 个多数是 `fixture` > (夹具 + 单测端到端跑同一份布局,**真机未逐家验证**),如实标注、不美化。 > - **SQLite 类来源零副作用读取**:只读打开 WAL 模式的库仍会在**库所在目录**新建 `-shm` / `-wal` 伴生文件 > (实测 32768 B / 0 B,close 后仍在)—— 那会写进**用户其它 agent 的数据目录**。现在按「库头 + 伴生文件大小」 > 分三态(直接读 / `immutable` / 复制到私有临时目录后读),**绝不退回就地打开**;`immutable` 只在能证明主库 > 完整时使用 —— 实测它在 `-wal` 非空时会**整条忽略 WAL(静默丢数据)**。 > - **路径按目标平台显式拼接**(不再读运行平台);凭据值硬剥离(只留字段名与引用名);读不到一律如实报码, > **绝不静默跳过**。 > > **Foreign-agent import sources: 6 → 30 (24 added)**: the importer used to know 6 sources (Claude Code / > Hermes / Cursor / Codex / Copilot / Antigravity); the remaining 24 session sources in the ecosystem are now > wired in: Gemini CLI, Reasonix, OpenCode, Mimocode, ZCode, Grok Build, OpenClaw, Pi, Kimi, Kilo Code, Qoder, > ChatGPT, WorkBuddy, Qwen Work, Continue, Cline, Goose, DSH V4, Zed, Crush, TeleAgent, Trae, Vibe, DSH. > A shared session IR layer (parse → intermediate form → synthesize → encode) means each source only implements > "read from disk + turn into a draft" instead of hand-assembling the DSH row format again. > - **Evidence strength is a required, typed field** (`measured` / `fixture` / `documented`), so "listed as > supported although only documented" cannot happen silently — the source list and the CLI summary both say > what each source is listed on. Most of the 24 are `fixture` (fixture + unit test running the same layout, > **not verified per-source on a real machine**), labelled honestly rather than flattered. > - **SQLite sources read with zero side effects**: opening a WAL-mode database read-only still creates > `-shm` / `-wal` companion files **in the database's own directory** (measured: 32768 B / 0 B, still there > after close) — which means writing into **another agent's data directory**. The open plan now picks between > three states based on the database header and companion-file sizes (read in place / `immutable` / copy to a > private temp directory first) and **never falls back to opening in place**; `immutable` is used only when the > main database can be proven complete — measured, it **ignores the entire WAL when `-wal` is non-empty > (silent data loss)**. > - **Paths are joined explicitly for the target platform** (never the running one); credential values are > stripped to field names and reference names; anything unreadable is reported with a stable code and > **never silently skipped**. > **同步通道新增 6 类云端点(S3 / OSS / COS / MinIO / Kodo / Gist)**:此前远端只有 git 与 WebDAV 两种。 > 通道枚举现在扩到四条(`git` / `webdav` / `s3` / `gist`),S3 一系覆盖 AWS S3、阿里云 OSS、腾讯云 COS、 > MinIO 与七牛 Kodo,外加 GitHub Gist;签名与请求头**手写实现、零新增依赖**(AWS SigV4 与阿里云 > `OSS4-HMAC-SHA256` 是两个不同方言)。界面上可直接选通道并填端点表单。 > - **走同一条同步引擎路径,不是旁路**:远端保留(GFS)、会话删除墓碑、内容寻址 blob 仓、「会话绝不悄悄下行」、 > 「导出密钥必须同时加密」等既有不变量对新通道**一条不放宽**(已用替身 transport 走真引擎逐条验证)。 > - **密钥只进 DSH 凭据槽位**:配置文件里只有非密字段(provider / endpoint / region / bucket / prefix); > 密钥值绝不进配置文件、不进响应、不进日志;断开通道时逐个槽位清除。 > - **gist 的能力边界(刻意设计,不是缺陷)**:gist 不外置 blob,且单文件超限会被截断 —— 它的定位是 > 「配置类分区的低门槛远端」;会话请走 git / WebDAV / S3。 > - 如实边界:**未对真实云端点做端到端验证**(本批无真实凭据),证据是单测与全量测试。 > > **Six cloud endpoints for the sync channel (S3 / OSS / COS / MinIO / Kodo / Gist)**: remote storage used to > be git and WebDAV only. The channel enum now has four entries (`git` / `webdav` / `s3` / `gist`); the S3 family > covers AWS S3, Alibaba Cloud OSS, Tencent Cloud COS, MinIO and Qiniu Kodo, plus GitHub Gist. Signing and > request headers are **hand-written with zero new dependencies** (AWS SigV4 and Alibaba Cloud's > `OSS4-HMAC-SHA256` are two different dialects). The UI lets you pick the channel and fill in the endpoint form. > - **Same sync engine path, not a bypass**: remote retention (GFS), session deletion tombstones, the > content-addressed blob store, "sessions never travel down silently" and "exporting secrets requires > encryption" are **not relaxed by a single one** for the new channels (verified by running the real engine > with a stub transport). > - **Secrets live only in DSH credential slots**: the config file carries non-secret fields only > (provider / endpoint / region / bucket / prefix); secret values never enter the config file, a response or a > log, and disconnecting a channel clears each slot. > - **gist's capability boundary (deliberate, not a defect)**: gist does not externalise blobs and truncates > files past GitHub's size limit — it is positioned as a low-barrier remote for configuration-class sections; > use git / WebDAV / S3 for sessions. > - Honest boundary: **no end-to-end verification against a real cloud endpoint** (no real credentials this > round); the evidence is unit tests plus the full suite. > **事故恢复新增「检查点」三态同点回滚**:会话游标 / 工作区文件块 / 插件自身配置三者各自留痕,并按同一 > 时点判断「是否同点」;覆盖式恢复**只覆盖记录在案的路径、绝不删除** —— 检查点之后新建的文件原样保留, > 并列入 `leftovers` 回报。 > - 四道门:未确认不回滚(409 且磁盘一字未改)、保护点不可回滚、guard 记录不可删、会话分叉时拒绝覆盖。 > - **记录不可成为写原语**:台账与校验和不符、或路径越界时,目标与越界路径都**不被写**。 > - 如实边界:三态采集**非原子**(模块头已声明);`capturedAt` 仅实例内单调,跨进程时间倒退会让「同点」 > 判定偏乐观;`workspace` 段的 `restored` 计数不按块递减。 > > **Incident recovery gains three-state same-point checkpoints**: the session cursor, workspace file chunks and > the plugin's own configuration each leave a record, and "same point" is judged across all three. An > overwriting restore **only overwrites recorded paths and never deletes** — files created after the checkpoint > are kept verbatim and reported as `leftovers`. > - Four gates: no restore without confirmation (409 with the disk untouched), protected points cannot be rolled > back, guard records cannot be deleted, and a diverged session log refuses to be overwritten. > - **A record can never become a write primitive**: when the ledger disagrees with the checksum, or a path > escapes the recorded set, neither the target nor the escaping path is written. > - Honest boundaries: the three states are captured **non-atomically** (stated in the module header); > `capturedAt` is monotonic per instance only, so a cross-process clock going backwards makes "same point" > optimistic; the workspace section's `restored` counter does not decrement per chunk. > **同步通道新增「选择已有仓库 / 新建仓库」**:git 通道的仓库地址此前只能手填 —— 得先去 GitHub 建好仓库、 > 复制 clone URL、再贴回来,还容易漏掉「必须私有」这条前提。现在配置弹窗可以直接从当前 token 可见的 > **私有**仓库里选(按最近更新排序,带更新时间与 fork 徽章),或就地新建一个私有仓库并自动选中。 > 公开仓库不进列表、也没有「公开」开关 —— 新建请求体根本不带 `private`,宿主恒定以私有建仓 > (同步仓库公开等于把配置内容公开)。地址输入框保留:ssh、本地路径、不在列表里的仓库仍可手填。 > > **Repository picker for the sync channel**: the git channel's repository URL no longer has to be typed > by hand (create the repo on GitHub, copy the clone URL, paste it back — and remember that it must be > private). The channel dialog now lists the **private** repositories the current token can see (sorted by > most recently updated, with a timestamp and a fork badge), or creates a new private repository inline and > selects it. Public repositories are never listed and there is no "public" switch — the create request does > not even carry `private`, because the host always creates private repos (a public sync repo would publish > your configuration). The URL field stays: ssh remotes, local paths and unlisted repos are still typeable. > **面板里就能做「布局归位 / 重复 id 隔离」(只做能从字节证明的事)** > - 会话体检里新增写入口:**布局归位**(会话目录与首帧 cwd 不一致时,把目录搬到 `projectKey(首帧 cwd)` 该在的位置)与 **重复 id 隔离**(同一 id 出现在两个 projectKey 是启动级故障;由你点名保留哪一份,其余移入 `.cm-repair-quarantine-*` 隔离目录,可人工找回,**不删除**)。 > - 宿主走新增的 `POST /recovery/sessions/layout`(前缀 `/recovery` 下的子路由):`apply!==true` 时**只读计划、零写入**;`apply===true` 时过 SAFE MODE + mutation lock,逐条门前置(无 `session.lock`、不在 30 s 静止期),**每次搬运后必须刷新会话索引,失败则把该条搬回原位并如实报 `rolledBack`**;宿主不提供刷新能力时**一条也不执行**(`reindex-unavailable`)。 > - 与离线 `dcm sessions repair --fix` 的差异**如实写进文案**:面板是**在 DSH 运行时**执行(CLI 要求先停 DSH),且不暴露路径映射 ⇒ 只搬目录、**不改写首帧**;另加两道 CLI 没有的护栏 —— 未点名 keep 的重复项**拒绝执行**(CLI 只是跳过)、keep 必须指向已扫描副本(CLI 把 `--keep` 指错路径会隔离**所有**副本)。 > > **Layout relocation and duplicate-id quarantine, now from the panel (only what the bytes can prove)** > - The session check-up gains a write entry: **layout relocation** (move a unit back to the projectKey its first-frame cwd names) and **duplicate-id quarantine** (you pick which copy to keep; the rest go to a `.cm-repair-quarantine-*` directory that can be recovered by hand — nothing is deleted). > - The host side is a new `POST /recovery/sessions/layout` (a sub-route of the existing `/recovery` prefix): `apply!==true` is a **read-only plan with zero writes**; `apply===true` goes through SAFE MODE + the mutation lock, with per-item preconditions (no `session.lock`, not within the 30 s quiescence window) and **a mandatory index refresh after every move — if it fails the item is moved back and reported as `rolledBack`**; if the host cannot refresh the index, **nothing runs** (`reindex-unavailable`). > - The differences from the offline `dcm sessions repair --fix` are stated in the UI: the panel runs **while DSH is running** (the CLI asks you to stop it) and does not expose path mappings, so it only moves units and **never rewrites the first frame**. It also adds two guardrails the CLI lacks: a duplicate id without an explicit keep is **refused** (the CLI merely skips it), and keep must name a scanned copy (CLI's `--keep` with a wrong path quarantines **every** copy). > **复验门(真 codec)在离线场景也能拿到官方 catalog:asar 抽取成为候选锚** > - 此前复验门的 catalog 只在「DSH 安装树 + 同级/嵌套 node_modules」两种布局里找;桌面端把 `@deepseek-ai/dsh-session` 放在 `app.asar` 内,于是离线 CLI / 救急台的复验恒 `unavailable`(**不是失败,但也不等于已验证**)。现在把 asar 抽取接成**候选锚**,且版本与 catalog 的候选规划共用同一条锚点链(安装锚 → 环境锚 → 运行时锚 → profiles 树),避免「版本从一个锚、catalog 从另一个锚」的自证。 > - 读不出来仍然如实报 `unavailable`(`verify.verified=false`,不写成功台账);抽出的目录是临时物,不改动安装树。 > > **The verify gate (real codec) now reaches the official catalog offline, via an asar extraction anchor** > - The gate used to look for the catalog only in the DSH install tree and its sibling/nested `node_modules`; the desktop build keeps `@deepseek-ai/dsh-session` inside `app.asar`, so offline CLI / rescue-console verification was always `unavailable` (not a failure — but not "verified" either). Asar extraction is now a **candidate anchor**, and version resolution and catalog planning share one anchor chain (install anchor → env anchor → runtime anchor → profiles tree) so they cannot certify each other from different anchors. > - If it still cannot be read, the result stays an honest `unavailable` (`verify.verified=false`, no success ledger), and the extracted tree is temporary — the installation is never modified. ### 🔧 变更 · Changed > **两处「静默」改成可见**:① 快照现在记录导出时的 DSH 版本,版本不匹配时恢复计划里多一条**咨询性提示** > (不阻断、也不猜版本);② 会话导出探测此前只看状态码,而官方 `GET /api/session.export` 在服务缺失时 > 返回 **500(不是 501)** —— 于是「服务没装」与「服务在但没正文」被混为一谈。现在探测读有限字节正文, > 三态(可用 / 不可用 / 未知)各自可辨,且**不消耗完整响应体**(有上限 + 主动断开,并有可失败断言钉住)。 > > **Two silences turned visible**: (1) snapshots now record the DSH version they were exported from, and a > version mismatch adds an **advisory** entry to the restore plan (non-blocking, and the version is never > guessed); (2) the session-export probe used to look at the status code only, while the official > `GET /api/session.export` returns **500 — not 501 — when the service is missing**, conflating "service not > installed" with "service present but no body". The probe now reads a bounded amount of body, so the three > states (available / unavailable / unknown) are distinguishable, and it **does not consume the full response** > (there is a cap plus an active abort, pinned by a failable assertion). > **面板说明文案不再许诺做不到的事(会话体检那行描述)** > - old 文案说「其余损坏类别仍只能走离线 CLI」,既漏掉了最严重的「会让 DSH 起不来」档,也不准确:离线 CLI 只做**隔离重复 id**与**归位错位目录**两类,容器/首帧读不出来、格式超前、未登记工作区、子代理缺父**离线也修不了**。现在逐档点名四档严重级(会让 DSH 起不来 / 会话读不出来 / 下次请求会失败 / 看不见),把应用内能力如实分成**两类**(零损失:重放重复行、可证明的合成收尾块;有损:序列空洞、不可解析行,**必须逐条显式确认、绝不批量**),并给出「其余类别」的三条真实出路(面板内已可做的布局归位 / 重复 id 隔离、离线 `dcm sessions repair --fix`、以及**本工具不修**需人工处置)。 > - 离线救急台的页面正文也曾渲染字面 `\*\*`(导出页走 `esc()`,星号原样显示给用户);该处去标记并写明原因,同时把「各页正文无字面 `\*\*`」的渲染级断言**扩到全部 12 个页面**(此前用例名说「各页」却只覆盖 7 页 —— 断言宽于覆盖同样是缺陷)。 > > **The panel no longer promises what it cannot do (the session check-up description)** > - The old line said "the remaining corruption classes are only reachable through the offline CLI". It missed the most severe level (would block DSH startup) and was also wrong: the offline CLI only quarantines duplicate ids and relocates misplaced directories — unreadable containers/first frames, future formats, unregistered workspaces and subagents without a parent **cannot be repaired offline either**. The text now names all four severity levels, splits the in-app ability honestly into **two classes** (lossless: replayed duplicate rows and provable synthetic closers; lossy: seq gaps and unparsable rows, each requiring explicit confirmation and never batched), and gives the three real destinations for everything else (the in-panel layout relocation / duplicate-id quarantine, the offline `dcm sessions repair --fix`, and **not repaired by this tool**). > - The offline rescue console also rendered literal `\*\*` in a page body (the export page banner goes through `esc()`, so the markers showed up verbatim); that site is de-marked with the reason recorded, and the render-level "no literal `\*\*` in any page body" assertion now covers **all 12 pages** (the case name claimed "every page" while only 7 were covered — an invariant asserted wider than its coverage is a defect too). > **「布局归位 / 重复 id 隔离」入口不再被藏起来(扫过之后始终可见)** > 这个入口原先只在**有候选行**时渲染:本机没有这一档问题(真机 1218 个会话单元全为 `already-placed`)的用户打开会话体检只看到「没有要修的东西」,**根本不知道有这个入口** —— 构建产物里明明有它。现在可见性判定收进 `src/ui/session-layout-view.ts` 的 `sessionLayoutSectionState`(`hidden` = 还没扫过 / `empty` = 扫过但本机无此档 / `ready` = 有候选或有计划结果),**扫过之后始终渲染**并显示空态说明;有计划/结果在手时更不会消失(否则刚做完的逐条结果会被吞掉)。空态文案 `sessions.layout.noCandidates` 的 zh/en 两个键是本次补的(此前并不存在)。 > > **The layout-relocation entry is no longer hidden (always visible once scanned)** > It used to render only when there were candidate rows, so a machine with none (all 1218 units on the reporting machine are `already-placed`) showed just "nothing to fix" — users **could not tell the entry existed** even though it was in the build. Visibility now lives in `sessionLayoutSectionState` (`hidden` = not scanned yet, `empty` = scanned but nothing in this class, `ready` = candidates or a plan/result in hand); the section **always renders once a scan has run** and explains itself in the empty state, and it never disappears while a plan/result is on screen (otherwise the just-finished per-item results would be swallowed). Both `sessions.layout.noCandidates` strings are new. ### 🐛 修复 · Fixed > **本轮收尾:平台矩阵三端全绿 + 链接跟随不再按平台丢内容**:三个 PR 合入后补掉的都是 > 「报成功但结果不对」这一类问题 —— 备份在部分平台静默缺内容、CI 在部分平台假红。 > > - **递归遍历的 realpath 口径混用(issue #69)**:链接目标用 `realpath` 的长形式,而 `homeDir` > 一直是调用方给的原样字符串 —— 两侧拼写不一致时,「home 内部的链接」会被判成 `outside-home` 并 > **静默跳过**(macOS 的 `/var` → `/private/var`、Windows 的 `C:\Users\IUUUUU~1\…` 短名都是触发 > 条件),备份照报成功却缺内容(与 issue #37 同一症状)。现在边界比较两侧统一到 realpath 口径, > 并复用同一次结果做防环去重。 > - **CRLF 检出下 5 条源码字面量守卫假红(issue #70)**:仓库根新增 `.gitattributes` > (`* text=auto eol=lf`)把 EOL 钉死,ubuntu / windows / macOS 三端检出同一份字节;三处守卫在 > **读取入口**折 LF(与仓库既有 11 处先例同法),已有的 CRLF 工作区不重新检出也不再假红。 > - **POSIX 宿主上的预览体积用例**:新增用例把 mock home 固定成 win32 形状,而路径解析走宿主的 > `path.resolve` —— linux / macOS 上它被解析到 `cwd` 之下,体积断言恒为 0;改为按宿主平台选 home 形状。 > > **Wrap-up: the platform matrix is green, and linked content is no longer dropped by platform**: every > defect closed after this batch reported success while being wrong. > > - **Directory traversal mixed two path spellings (issue #69)**: link targets were compared as > `realpath` long forms while `homeDir` kept the caller's spelling, so in-home links were judged > `outside-home` and **silently skipped** (macOS `/var` → `/private/var`; Windows 8.3 short names such > as `C:\Users\IUUUUU~1\…`) — backups reported success while missing content, the same symptom as > issue #37. Both sides of the boundary check now compare in realpath form, reusing that result for > cycle detection. > - **Five source-literal guards failed on CRLF checkouts (issue #70)**: a new `.gitattributes` > (`* text=auto eol=lf`) pins EOL so ubuntu, windows and macOS check out the same bytes; three guards > normalise to LF at the read boundary, so an existing CRLF worktree no longer goes red without a > re-checkout. > - **A preview-size test assumed Windows path semantics**: the test pinned its mock home to a win32 > shape while path resolution uses the host's `path.resolve`, so on linux / macOS it resolved under > `cwd` and the size assertion was always 0; the home shape now follows the host platform. > **导出/同步不再丢 `!!js`,pnpm 隔离安装也能识别会话格式版本(issue #75 / #74)** > - **`cordis.patch.yml` 的 `!!js` 方言(issue #75,数据丢失级)**:`!!js` 是 DSH 的专用 YAML 标签(`JSON_SCHEMA` + `tag:yaml.org,2002:js`),而插件此前用缺省 schema 读它 —— 遇到 `!!js` 直接抛 unknown tag,被吞成空数组。后果不止「丢一条配置」:解析针对整份文档,**只要该层有一处 `!!js`,这一层全部 patch 行都会从导出 / 同步 / 预览里消失**;导入写回时更会把整份文件重写成「只剩本次导入的行」。现在读写统一走新的方言单一事实源(`raw` 的内存形态是 `{__jsExpr: <源码>}`,普通 JSON、写回还原成 `!!js`),并补两道护栏:**原文件存在却解析不了 / 读不到时拒绝覆盖**、解析失败上抛成可见告警(不再静默);文件不存在仍按「本来就没有」处理。 > - **pnpm 隔离安装的会话格式版本解析(issue #74)**:pnpm 全局安装 DSH 时 `installAnchor` 落在 `.pnpm` 存储段内,同级没有 `dsh-session`(`dsh` 的依赖里也没有它),旧的「同级 / 嵌套两种布局」必然落空 → 每次启动报 warn、导入与同步的「格式超前」体检整体跳过。现在会向上定位最近的 `.pnpm` 并按 `@deepseek-ai+dsh-session@*` 扫段(多版本取与本机 dsh 同版本者,判不准则不猜),失败时把**已尝试的路径**写进日志;同一份布局推导已收敛为单一实现(复验门与档案列表共用)。 > > **No more lost `!!js` on export/sync; pnpm-isolated installs resolve the session format version (issues #75 / #74)** > - **The `!!js` dialect of `cordis.patch.yml` (issue #75, data loss)**: `!!js` is DSH's own YAML tag (`JSON_SCHEMA` + `tag:yaml.org,2002:js`), but the plugin read the file with the default schema — hitting `!!js` threw an unknown-tag error that was swallowed into an empty array. The damage was bigger than one missing config: parsing covers the whole document, so **a single `!!js` anywhere made every patch row of that layer disappear from export / sync / preview**, and importing rewrote the file down to just the imported rows. Read and write now share one dialect source of truth (`raw` keeps the JSON-safe `{__jsExpr: }` shape and is written back as `!!js`), plus two guard rails: **an existing file that cannot be parsed or read is never overwritten**, and parse failures surface as visible warnings instead of silence; a missing file still means "nothing there yet". > - **Session format version under a pnpm-isolated install (issue #74)**: with a global pnpm install the `installAnchor` sits inside a `.pnpm` store segment whose siblings have no `dsh-session` (and `dsh` does not depend on it), so the old two-layout lookup always missed → a startup warning on every boot and the import/sync "format ahead" check skipped entirely. The resolver now walks up to the nearest `.pnpm` and scans `@deepseek-ai+dsh-session@*` segments (preferring the version matching the local dsh; never guessing when ambiguous), and logs the paths it tried on failure. The same layout derivation is now a single implementation shared with the verify gate and the profile list. > **M1 会话修复引擎加固:拆掉一个会毁数据的陷阱 + 装上真 codec 复验门与只读检测** > 一条 v0 打包行的判据错误,让 1194 份真机会话日志里的 **407 份**被判成「需要损失性修复」;按该计划执行会丢弃 **865,098 / 876,680 行(98.7%)**。本轮修掉这个判据,并把「写前复验」「有损预览」「引用闭包」「只读检测」四道护栏补齐。 > - **packed 行假空洞(真机 407 例)**:v0 打包行(`text-chunks` / `reasoning-chunks` / `tool-call-chunks`)**没有 `seq` 成员**,一行按 `seq0` + payload 展开成 N 个事件(官方 `decodePackedRun`:`firstSeq = seq0`、`eventCount = payload.length`)。旧判据只读 `seq` → 打包行被当成「无 seq 行」跳过却保留上一个 `lastSeq`,于是打包行之后的第一条标量行被误判为 seq 空洞。真机:**lossy-required 407 / nothing-to-fix 787**;407 份**全部是 v0 代际**日志(`session.jsonl.zstd`,header `version:0`),v3/v4 零误判。复核脚本 `.tmp/probe-session-repair.mjs`。 > - **判据分两层,不得混同**:(i) **形状不可判定**(缺 `seq0` / 载荷非数组 / 长度非安全整数 / `dt` 长度 ≠ len-1 / 带严格键集外成员 / 无 `seq0` 的未知非 packed 类型)→ 视为**不透明跨度**,禁止跨越它下连续性结论,计划 refuse;(ii) **形状可判定但 `seq0` 与运行计数不衔接** → 这是**真实 seq 空洞**,与标量行空洞同待遇(默认 refuse;显式 `allowLossy` 才在首个异常处截断)。把它当 opaque 会让同一种语义因前一行的物理编码不同而有两种待遇。 > - **帧粒度规则**:`≤200 行/帧` 只约束**本次重编码**的帧;**未触及帧(帧内无被丢行且全部保留)必须逐字节复用**,即使它原本超过 200 行 —— 真机有 **226 个事件帧 >200 行、最大 8654 行**,那是 DSH 自己写出的形态,字节保真优先于自定上限。 > - **真 codec 复验门**:写后(以及有损计划应用前)用 DSH 官方 `createSessionFormatCatalogWithChildren` + 逐行 `decodeRow` + `finish` 复验;**能力不可用 → `unavailable`(unverified)且绝不宣称成功**;**确定性失败 → 用备份自动回滚**;`unavailable` **不触发回滚、不写成功台账**、但**仍写台账**以保证已应用的修复可回滚。 > - **有损计划预览可见**:`lossy-required` 的计划必须给出真实的 `truncate-tail` 条目与丢弃行数(旧实现给的是空动作清单 —— 用户被告知「需要损失性修复」却说不出将丢什么)。 > - **引用闭包闸门**:closer-drop 候选若被保留行引用 → **不计划该动作**(零损动作优雅降级);截断造成「引用目标由存在变不存在」→ **在 planner 内整体 refuse**(不允许「先给 lossy 预览、再在 apply 期拒绝」)。 > - **只读检测扩展**:会话体检新增 `empty-tool-call-id` / `duplicate-tool-call-id` / `tool-result-id-mismatch` 三个码,并让 `missing-message-id` / `dangling-tool-call` 真正产出;严重级按**代际 + 载体**分级(v4 与 pre-v4 下缺 message id、空 tool-call id、重复通告同一 advertised tool call 都是**前台读盘管线拒读** → `unloadable`;版本读不出 → `nextRequestFails` + `codec-uncalibrated`)。**只报不修**。 > - **CLI / 离线救急台如实反映 verify 三态**:现役可读 / 迁移链可还原 / 未验证 —— 不再把「写完了」说成「已可加载」。 > - **口径(不得夸大)**:① 修掉 packed 误判 **≠** 这些 v0 日志变得可读 —— 真 codec 对 407 份复验 = **405 失败 / 2 通过**,失败原因是 v0→v1 迁移拒绝(`summary requires notice form` / `unsupported descriptor version 2` / `unclassified message source`),与 seq 无关;那 2 份的正确表述是「**可被迁移链还原**」,不是「DSH 能直接读」(官方 `resolveCurrentLog`:pre-v4 没有「当前日志」)。② 其中 `session-66add64c-…`(1160 行)**正是会被旧的有损路径毁掉的那一份**(现状 `allowLossy` 只留 23 行、丢 1137 行)—— 这是本轮最强的修复动机。③ **本轮收益为零的事实要如实说**:修复执行覆盖 **0**(真机修复计划恒为 `nothing-to-fix`)、新增只读检测真机命中 **0**、真 codec 门的真机覆盖有限(本机最新代际 < v4 占 **80.67%**,默认解析下多为 `unavailable`)。它是「拆陷阱 + 装护栏与闸门」,**不是**「修好了一批会话」。 > - 数字口径(2026-10-06 真机快照):units **1195**(日志 **1194**),最新代际 {v0: **442** / v3: **522** / v4: **231**},`KNOWN_SESSION_EVENT_TYPES.size = **59**`,`SESSION_FORMAT_VERSION = **4**`。 > > **M1 session-repair hardening: removing a data-destroying trap and adding a real-codec verify gate plus read-only detection** > One wrong judgement about a v0 packed row made **407 of 1194** real session logs look like "lossy repair required"; executing that plan discards **865,098 / 876,680 rows (98.7%)**. This round fixes the judgement and adds four guard rails: pre-write verification, lossy preview, reference closure, and read-only detection. > - **Packed-row phantom gaps (407 real cases)**: v0 packed rows (`text-chunks` / `reasoning-chunks` / `tool-call-chunks`) carry **no `seq` member**; one row expands into N events via `seq0` + payload (official `decodePackedRun`: `firstSeq = seq0`, `eventCount = payload.length`). The old judgement read only `seq`, skipped packed rows while keeping the previous `lastSeq`, and therefore reported the first scalar row after a packed run as a seq gap. Real machine: **lossy-required 407 / nothing-to-fix 787**, and all 407 are **v0-generation** logs (`session.jsonl.zstd`, header `version:0`); v3/v4 have zero false positives. Probe: `.tmp/probe-session-repair.mjs`. > - **Two judgement layers, never conflated**: (i) **undecidable shape** (missing `seq0` / payload not an array / non-safe-integer length / `dt` length ≠ len-1 / extra members outside the strict key set / unknown non-packed type without `seq0`) → an **opaque span**; never draw a continuity conclusion across it, and refuse the plan; (ii) **decidable shape whose `seq0` does not line up with the running count** → a **real seq gap**, treated exactly like a scalar-row gap (refuse by default; truncate at the first anomaly only under explicit `allowLossy`). Treating (ii) as opaque would give one semantic two different verdicts depending on how the previous row happened to be encoded. > - **Frame granularity**: `≤200 rows/frame` constrains only frames **this run re-encodes**; **untouched frames (no dropped row inside, everything kept) must be reused byte-for-byte**, even when they were larger than 200 rows — the real machine has **226 event frames above 200 rows, the largest 8654 rows**, and those are shapes DSH itself wrote. Byte fidelity beats a self-imposed cap. > - **Real-codec verify gate**: after writing (and before applying a lossy plan) the log is re-verified through the official `createSessionFormatCatalogWithChildren` + per-row `decodeRow` + `finish`; **capability unavailable → `unavailable` (unverified), never reported as success**; **deterministic failure → automatic rollback from the backup**; `unavailable` **does not roll back and does not record a success ledger entry**, but **still writes the ledger** so repairs that were applied remain rollback-able. > - **Lossy plans are visible before they run**: a `lossy-required` plan must carry the real `truncate-tail` entry and the number of rows it would drop (the old implementation returned an empty action list, telling users a lossy repair was needed without saying what would be lost). > - **Reference-closure gate**: a closer-drop candidate referenced by a kept row is **not planned** (a zero-loss action degrades gracefully); a truncation that turns a referenced seq from "present" into "missing" makes the planner **refuse outright** (no "show a lossy preview, refuse at apply time"). > - **Read-only detection**: session health gains `empty-tool-call-id` / `duplicate-tool-call-id` / `tool-result-id-mismatch`, and `missing-message-id` / `dangling-tool-call` are actually produced. Severity is classified **per generation and per carrier** (on v4 and pre-v4 a missing message id, an empty tool-call id, or a repeated advertised tool call is rejected by the **foreground read pipeline** → `unloadable`; an unreadable version → `nextRequestFails` + `codec-uncalibrated`). **Reported, never repaired.** > - **CLI / offline rescue console report the three verify states honestly**: readable-now / restorable-through-the-migration-chain / unverified — "the write finished" is no longer presented as "it loads". > - **Non-negotiable wording**: ① fixing the packed-row judgement does **not** make those v0 logs readable — the real codec verifies **405 failures / 2 passes** out of 407, and the failures are v0→v1 migration refusals (`summary requires notice form` / `unsupported descriptor version 2` / `unclassified message source`), unrelated to seq; the 2 passes are correctly described as "**restorable through the migration chain**", not "DSH can read them directly" (official `resolveCurrentLog`: pre-v4 has no "current log"). ② One of them, `session-66add64c-…` (1160 rows), **is exactly the file the old lossy path would have destroyed** (it kept 23 rows and dropped 1137) — the strongest motivation for this round. ③ **Say the zero-benefit facts out loud**: repair execution coverage is **0** (real-machine repair plans are always `nothing-to-fix`), the new read-only detection hits **0** real cases, and the real-codec gate has limited real coverage (on this machine **80.67%** of units have a newest generation below v4, which resolves to `unavailable` by default). This round removed a trap and installed guard rails — it did **not** fix a batch of sessions. > - Number baseline (real-machine snapshot, 2026-10-06): units **1195** (logs **1194**), newest-generation split {v0: **442** / v3: **522** / v4: **231**}, `KNOWN_SESSION_EVENT_TYPES.size = **59**`, `SESSION_FORMAT_VERSION = **4**`. > **全仓对抗审计修复**:本轮对**整仓**做了一次对抗性审计(七个区域审计 + 三轮横向扫查 + 每个修复配独立验证), > 收敛出 **44 项缺陷:40 项闭环、5 项部分闭环、2 项等第三方写者落盘后复扫**。修的主要是**静默数据风险**, > 而不是看得见的报错。 > > - **同步上传不再把「远端索引 404」当成空集合**(WebDAV + S3):此前会把远端**仍被引用**的 blob 当成 > 无人引用而回收 —— **不可恢复**。现在写任何字节之前先自证远端有无历史内容:有证据就中止并说明, > 全新远端首次推送行为逐字不变,探针读不到一律按「有内容」处理(fail-closed)。 > - **「读不到」不等于「没有」**(三处同族):只有真不存在(ENOENT)才算「本来就没有」,权限 / 占用 / IO > 错误一律按「存在」处理。此前一次瞬时 EACCES 会让声明的 `patchedDependencies` 被从**将要写入的** > `pnpm-workspace.yaml` 里剔除(issue #35 的症状)。 > - **S3 / gist 的错误提示不再显示裸键名**(18 + 16 个键):传输层的本地文案键与宿主的翻译器互不认识, > 真机弹的是 `sync.s3.requestFailed` 这类键名。 > - **MCP 的 `args` / `url` / `command` / `cwd` 不再进包**,凭据泄漏面收窄;条数触顶与文件过大的 > **截断不再静默**,一律计数并回传。 > - **导入时用户取消勾选的会话不再被算作「搬迁失败」** —— 此前会 no-op 地触发整笔导入回滚。 > - **恢复面板把「枚举失败」与「确实没有残留」区分开**:失败可见、可重试,但不再让整页失败。 > - **同 id 工作区记录归并不再抛 `TypeError`**(畸形 `sessionIds` 元素),校验补上元素级检查。 > - **脱敏口径统一**:`bearer` 按 RFC 7235 / 6750 **大小写不敏感**(规范 `Bearer …` 的行为逐字不变), > 且 `sk-` 不再误剥 `task-management` / `risk-assessment` 这类正常词,同时不削弱对真实密钥形态的检出。 > - **同一语义不再有两套实现**:`redact()` 与扫描器共用同一张值形状表(另加防漂移守卫), > 宿主与客户端的同构常量收敛到**零依赖单一来源**。 > - **文档与计数不再漂移**:路由计数订正为 **77 = 70 + 7** 并加了散文守卫;`AGENTS.md` 的容量余量 > 从 96 B 提到 2,384 B。 > - 🧪 **验证**:完整树上 `typecheck` / `typecheck:tests` / `build` 全 0;`npm test` **3690 例 / 3688 通过 / > 0 失败 / 2 跳过**(相对基线 +246 例、零新增失败);客户端产物自包含守卫 1/1;真机隔离实例 > **7/7 关键路由 200**、界面数字与磁盘事实逐项一致。 > > **Repository-wide adversarial audit**: this round audited the **whole repository** (seven area audits plus > three horizontal sweeps, with an independent verifier for every fix) and converged on **44 defects: > 40 closed, 5 partially closed, 2 awaiting a third-party writer**. Nearly all of them were **silent data > risks**, not visible errors. > > - **A remote index 404 is no longer treated as an empty set** on sync upload (WebDAV + S3): blobs that were > still referenced could be garbage-collected — **irrecoverably**. The upload now proves the remote has no > prior content before writing a single byte: with evidence it aborts and says so, a genuinely fresh remote > behaves exactly as before, and an unreadable probe counts as "has content" (fail-closed). > - **"Cannot read it" is not "it is not there"** (three places in one family): only ENOENT means "never > existed"; permission, busy and IO errors count as "it exists". A single transient EACCES used to drop a > declared `patchedDependencies` entry from the **about-to-be-written** `pnpm-workspace.yaml` (the issue #35 symptom). > - **S3 and gist error messages no longer show raw keys** (18 + 16 keys): their local catalogs were unknown to > the host translator, so users really saw `sync.s3.requestFailed`. > - **MCP `args` / `url` / `command` / `cwd` no longer reach the bundle**, narrowing credential exposure, and > truncation (too many entries, oversized files) is **no longer silent** — it is counted and reported. > - **Unchecked sessions are no longer reported as a failed relocation**, which used to trigger a full rollback for nothing. > - **The recovery panel separates "enumeration failed" from "there is genuinely nothing left"**: visible, > retryable, and no longer fatal to the page. > - **Merging duplicate workspace records by id can no longer throw `TypeError`** on malformed `sessionIds` > elements, and validation now checks element types. > - **One redaction rule set**: `bearer` is **case-insensitive** per RFC 7235 / 6750 (canonical `Bearer …` is > byte-for-byte unchanged), and `sk-` no longer strips ordinary words such as `task-management` / > `risk-assessment` while every real key form stays detectable. > - **No more two implementations of one rule**: `redact()` and the scanner share one value-shape table (with > anti-drift guards), and the host/client duplicate constants collapse into a **zero-dependency single source**. > - **Documentation and counters no longer drift**: the route count is corrected to **77 = 70 + 7** with a prose > guard, and `AGENTS.md` headroom went from 96 B to 2,384 B. > - 🧪 **Verification**: on the complete tree `typecheck` / `typecheck:tests` / `build` all exit 0; `npm test` > **3690 tests / 3688 passing / 0 failing / 2 skipped** (+246 vs baseline, zero new failures); the client > bundle self-containment guard passes 1/1; a real isolated instance served **7/7 key routes with HTTP 200** > and every UI number matched the on-disk facts. > **性能修复**:本轮修的是**「总览」页每次都要等十几秒到半分钟**的问题 —— 根因是首屏把只读预览 > 也当成了真实导出,并且一次预览里叠加了三处重复的全量读取。真机(24 个 settings namespace、 > 12 个本地源插件)实测:全量预览 27.6 s → 现在首屏不再等它。 > > - **预览不再打包本地插件**:`plugins` 分区此前没有 `preview()`,调用方退回 `export()`, > 于是每次打开「总览」都会为每个 `link:`/`file:` 插件 spawn 一次 `npm pack` > (真机 12 个 ≈12 s)。现在 `PluginsAdapter.preview()` 只读清单、patch 行、pnpm-workspace 与 > patch 文件,**零进程副作用**;本地源体积改为**零 spawn 只读度量**(`file:` 源按一次 `stat`; > `link:` 目录走新增的宿主门面 `FileSystemFacade.dirSizeBytes()` —— 跳过 `node_modules`、不跟随 > 链接目录,给出的是 pack 产物的量级上界),单元清单与导出产物逐项同口径(含 `fileCount` 与原子组)。 > - **settings / credentialsStatus 只读一趟**:宿主 `SettingsProvider.describe()` 是「无参 = 全量」, > 逐个 namespace 调用实际是 O(N²)(真机 24 个 ≈1.7 s,两个分区各来一遍)。新增可选能力 > `SettingsFacade.describeAll()`,适配器一次读回全部 namespace 再本地取用;宿主未实现或调用 > 抛错时逐字退回原路径,告警文案不变。 > - **总览页首屏不再等预览**:`HomePanel` 拆成「5 个毫秒级只读接口」与「分区构成」两段, > 首屏不再被最慢分区拖住;分区构成卡改为**始终渲染**,数据未到先显示骨架(`读取中…`), > 失败显示 `读取失败 · 将整体导出`,合计位在未到货时显示加载态而不是 0。 > > **Performance**: the Overview page no longer makes you wait. Three separate causes were stacked into > one first paint: the read-only preview was running the real export path, and two sections re-read every > settings namespace once per name. > > - **Preview no longer packs local plugins**: the `plugins` section had no `preview()`, so the caller fell > back to `export()` and spawned one `npm pack` per `link:`/`file:` plugin (~12 s on a real machine with > 12 local sources) every time the Overview opened. `PluginsAdapter.preview()` is now side-effect free; > local source sizes are measured read-only without spawning anything (`file:` sources by a single `stat`; > `link:` directories through the new host facade `FileSystemFacade.dirSizeBytes()`, which skips > `node_modules` and never follows linked directories and is an order-of-magnitude upper bound on the pack > output), and its unit list matches the exported one item for item. > - **settings / credentialsStatus read once**: the host `describe()` is "no argument = everything", so > calling it per namespace is O(N²) (24 namespaces ≈1.7 s, twice per preview). A new optional > `SettingsFacade.describeAll()` reads them all in one call; hosts that lack it (or that throw) fall back > to the old per-name path with identical warnings. > - **The Overview first paint no longer awaits the preview**: `HomePanel` now loads the five fast read-only > APIs first and the section composition separately. The composition card always renders, showing skeleton > rows (`Loading…`) until the data arrives and `Load failed · export everything` on failure. > **本轮修复**:外壳里配好的 MCP 服务器与技能**备份不到** —— 导出包里 `mcp/servers.json` 恒为空、 > `custom/skills/` 一个条目都没有,而插件报的是「成功」。根因是两处「只看了一个地方」:MCP / prompts / > 插件激活行的 patch **只读了 home 层**,而外壳真正写进去的是**档案层** `profiles//cordis.patch.yml`; > 技能**只扫了 `$DSH_HOME/skills`**,而外壳的技能来自插件注册表。现在 patch 行按层读取、写回原层, > 技能经外壳的 `skills` 服务收编,备份与回滚都记住每一行属于哪一层。 > > **Theme**: MCP servers and skills configured in the shell could not be backed up at all (issue #71) — > the bundle's `mcp/servers.json` was always empty and `custom/skills/` had no entries, while the plugin > reported success. The plugin read only the home layer of the patch file for MCP / prompts / plugin > activation rows (the shell writes them into the **profile layer**), and it scanned only > `$DSH_HOME/skills` for skills while the shell serves them from its plugin registry. Patch rows are now > read per layer and written back to the layer they came from, skills are collected through the shell's > `skills` service, and backup/rollback record the layer of every row. #### 🔌 修复:备份不到外壳的 MCP 与 Skills(issue #71)· MCP / skills not backed up (issue #71) - 🔌 **MCP 服务器现在从两个 patch 层读出**:`$DSH_HOME/cordis.patch.yml`(用户层)与 `$DSH_HOME/profiles//cordis.patch.yml`(档案层)按 **DSH 自己的合并优先级**取有效行(home 层后合并 ⇒ 优先级更高),同名 `lineId` 只保留优先级最高的那一条;每条导出条目记下自己的来源层。 **MCP servers are now read from both patch layers**, merged in DSH's own precedence order (the home layer wins), with the source layer recorded per entry. - ✍️ **导入写回原层,不再一律写 home 层**:来自档案层的行写回档案层(并且是**目标机当前档案**),来自用户层的 写回用户层;旧备份包没有层字段 ⇒ 按用户层处理(改造前行为,不猜)。同一根因下 prompts 分区与插件激活行 一起修好。**Imports write back to the layer a row came from** — profile-layer rows go to the target machine's current profile; bundles from older versions have no layer field and keep the old home-layer behavior. Prompts and plugin activation rows are fixed by the same change. - 🧩 **技能经外壳的 `skills` 服务收编**(`ctx.skills.list()` / `get(name)`):注册表里的技能按 `/SKILL.md` 虚拟路径并入 `custom/skills/`,frontmatter 只写 DSH 认的键(`name` / `description` / `whenToUse`,调用策略写 `disable-model-invocation` / `user-invocable`);**同名时磁盘原文优先**, 服务不可用就退回纯目录扫描(只记 warn,不编造告警)。**Skills are collected through the shell's `skills` service** as virtual `/SKILL.md` paths; on-disk files win on the same path, and a missing service degrades to directory scanning. - 🛟 **备份与回滚记住 patch 行的层**:快照条目记录 `file`,回滚写回**原层**(旧快照缺该字段 ⇒ 用户层); 计划项去重键把层算进去,档案层的行不再被 home 层的同名行吞掉;整文件还原接受**任一层**的 `cordis.patch.yml` 备份。**Snapshots and rollback now record the layer of every patch row.** - 🧾 **多行技能字段不再写出非法 YAML**:服务技能的 `description` / `whenToUse` 可能是块标量 (真机 `dsh-reverse-skill/skills/binary-diff` 是 4 行 211 字符),重建 frontmatter 按三档编码 —— 以恰好一个换行结尾的多行值写块标量 `|`(尾换行交给 clip chomping 还原)、含换行 / 回车 / 控制字符的 写双引号 + 转义、其余仍是单引号;解析回来**逐字符相同**。原先只做单引号转义,多行值会跨行 ⇒ 外壳判 `invalid YAML frontmatter` 并**丢掉整个技能**(比不备份更糟)。**Multi-line skill fields are now encoded as valid YAML** (block scalar / double-quoted escapes / single-quoted), so the shell can never drop a skill over frontmatter. - 🧪 **用例**:新增 `src/core/patch-layers.test.ts`(层优先级 / 写回层解析 / 单层读失败不阻塞), 扩充 `src/adapters/files.test.ts`(技能服务合并、磁盘优先、服务缺失、路径安全、**多行字段的 YAML 合法性**)、`mcp.test.ts`、 `prompts.test.ts`、`plugins.test.ts`、`tests/core/patch-file-snapshot.test.ts`(快照记层 + 回滚写回原层)。 > **本轮修复**:一键同步**永远收敛不了** —— 4 个空 settings 分区(`permission` / `locale` / > `ui-settings-general` / `agent-preset-registry`)与 `self:sync/sync-autosync.json` 每同步一次就会 > 重新出现在差异列表里,连续 7 次 `completed` 事务全部命中,界面却报「成功」。根因是两处彼此独立的 > 判定缺陷:settings / providers 把「目标为空 → 新建」排在了「值相等 → 跳过」之前,而两端值都是空的 > `{}` 时,写回 `{}` 是一次空写入,下一轮读回来还是 `{}` ⇒ 永远判「新建」;`self` 分区按**整文件哈希** > 比对,而自动同步配置里的 `lastRunAt` / `consecutiveFailures` 等运行态字段每轮都被调度器改写 ⇒ > 哈希永远不同、永远判「冲突」(点「应用」还会把远端的陈旧运行态倒灌回本机)。现在两处都按预期判 > 「跳过」,而「目标非空 + 导入为空」仍判「冲突」—— 不借「跳过空值」静默清空本机配置。 > > **Fix**: one-click sync could never converge. Four empty settings namespaces (`permission` / `locale` / > `ui-settings-general` / `agent-preset-registry`) and `self:sync/sync-autosync.json` came back in the diff > list on every single sync — seven consecutive `completed` transactions hit them all — while the UI reported > success. Two independent judgement bugs: settings / providers tested "target is empty → create" before > "values are equal → skip", so when both sides held an empty `{}` the "write" was a no-op that left the next > read identical (create forever); and the `self` section compared **whole-file hashes**, while the autosync > config's run state (`lastRunAt`, `consecutiveFailures`, …) is rewritten by the scheduler after every run, so > the hashes never matched (conflict forever — and applying it pushed the remote's stale run state back onto > this machine). Both now skip as expected, and a non-empty target against an empty import is still a conflict, > so local configuration is never silently wiped. #### 🔁 修复:一键同步永不收敛(issue #73)· One-click sync that could never converge (issue #73) - ⚙️ **settings / providers 判定顺序修正**:`planNamespaceItems` 与 `ProvidersAdapter.analyzeImport` 把 「值相等 → 跳过」提到「目标为空 → 新建」之前,两端皆 `{}` 时不再反复产生空写入。`ui` 分区与 `providers` 共用同一判定,同源生效;「目标为空 + 导入非空」仍是新建、「目标非空 + 导入为空」仍是冲突。 **Equal values are now checked before emptiness in settings, ui and providers**, so two empty `{}` values skip instead of creating again on every sync. - 🧾 **`self` 调度配置剔除运行态后再比对**:`FileCollectionAdapter` 新增可覆写的比对口径钩子,`self` 对 `sync/sync-autosync.json` 与 `sync/backup-schedule.json` 剔除 `lastRunAt` / `lastRunStatus` / `lastRunMessage` / `lastRunHistoryId` / `consecutiveFailures`(含 v2 `channels.git` / `channels.webdav` 嵌套)后按**键排序**稳定序列化比对;配置本体不同仍判冲突,文件解析不出 JSON 时回落整文件哈希 (「判不出来」≠「判为相同」)。其余 `self` 白名单文件口径不变。**The self section now compares its two scheduler configs without run-state fields**, serialized with sorted keys so a hand-edited key order cannot create a phantom conflict; a real configuration change is still a conflict, and an unparsable file falls back to the whole-file hash. - 🧪 **用例**:新增 5 条回归 —— settings / providers「两端皆 `{}` → 跳过」「目标非空 + 导入 `{}` → 冲突」, `self`「仅运行态不同 → 跳过(键序不同亦然)」「配置本体不同 → 冲突」「坏 JSON 回落整文件哈希」 「口径不外溢(`sync-config.json` 仍按字节判)」。**Five regression tests** cover the empty-value skip, the non-empty-target conflict, run-state-only skip, a real config change, unparsable JSON, and the scope boundary. > **会话体检不再整片误报「子代理缺父」(真机 798/801 → 0)** > 旧实现在调用方没给「已知会话 id」时用 `?? new Set()` 把「未知」当成「确知为空集」,于是「父对话存在性」判据对**每一条**子代理会话都成立 —— 真机 801 条里有 **798 条**被误报 `subagent-without-parent`(`bySeverity.invisible=798`)。现在「未知」与「确知为空」严格分开(`knownSessionIds` 缺省 = 不判、显式空集才判),采集器自己从遍历结果自证集合;复算:默认解析下误报 **0**,显式空集模拟下仍有 801 条(证明不是靠放宽判据换来的)。 > > **The session check-up no longer reports "subagent without parent" en masse (798/801 on a real machine → 0)** > The old code turned "unknown" into "known to be empty" with `?? new Set()` whenever the caller supplied no known-session-id set, so the parent-existence predicate held for **every** subagent session — **798 of 801** units on a real machine were flagged `subagent-without-parent`. Unknown and known-empty are now strictly separate (`knownSessionIds` absent = do not judge; an explicit empty set = judge), and the collector derives its own set from the traversal. Re-measured: **0** false positives under the default parse, still 801 under an explicit empty set (proving this was not bought by loosening the predicate). > **批量修复的失败分支不再丢 `verify` / `rolledBack`(危险态曾对用户不可见)** > 一键修复在「某条失败」的分支里丢掉了逐条 `verify` 与 `rolledBack` 字段:界面因此可能把**回滚失败(`rolledBack:false`,会话可能处于中间态)**渲染得与成功无异,同时承诺文案与实际状态分叉。现在三态(现役可读 / 迁移链可还原 / 未验证)与回滚结果一律逐条回传并各自有文案键(「未执行 / 已回滚」**绝不显示为成功**,回滚失败单独显红)。 > > **The batch repair no longer drops `verify` / `rolledBack` on the failure branch (a dangerous state used to be invisible)** > The one-click repair dropped the per-item `verify` and `rolledBack` fields on its "one item failed" branch, so the UI could render a **failed rollback (`rolledBack:false`, possibly a half-applied session)** as if nothing had happened, while the promise text diverged from reality. The three verification states and the rollback outcome are now always returned per item, each with its own copy key ("not applied / rolled back" **never renders as success**; a failed rollback is shown in red on its own). > **复验门不再因缓存键与判等口径不一致而翻转结论(真机 v4 曾被判 `decode-failed` 并回滚)** > 复验门的 catalog 解析曾只按 `plan.root` 缓存、等价性也只按 root 判定,于是**同一进程内同一输入**会因前一次调用改变了缓存而给出不同结论(实测等价性翻转 + 本机 v4 被误判 `decode-failed` 并触发回滚)。现在任何依赖日志 header 的判定都**按该日志的 `headerVersion` 重跑**,等价性用 `equivalentToReadPath` 判定:`createRestore(strict+transformed)` 只在 `header.version ===` 当前代际时可达(本机 v4),pre-v4 走迁移链 ⇒ 可以 `verified` 但**不等于 DSH 读盘路径**。 > > **The verify gate no longer flips its verdict because its cache key and its equivalence test disagreed (a real v4 log was judged `decode-failed` and rolled back)** > Its catalog resolution cached by `plan.root` alone and judged equivalence the same way, so **the same input could produce different verdicts inside one process** (observed: equivalence flipped, and a v4 log on this machine was misjudged `decode-failed`, triggering a rollback). Every log-header-dependent decision is now re-run per that log's `headerVersion`, and equivalence is decided by `equivalentToReadPath`: `createRestore(strict+transformed)` is only reachable when `header.version` equals the current generation (v4 here), while pre-v4 goes through the migration chain — verifiable, but **not equivalent to the DSH read path**. > **面板三处「你看到的与实际不符」(含两条新护栏)** > - **布局归位卡被挤成一行**:整卡外层误用 `.snapshotRow`(横向 flex 行)、小节标题误用 `.snapshotRowMain`(`flex:1` + `nowrap`)⇒ 标题、三行说明、按钮与计划摘要被排进同一行、文字被挤成竖排(用户截图)。改为 `.card` 容器 + `.groupLabel` 小节标题 + `.snapshotList`/`.reportScroll` 纵向列表 + `.actionRow` 按钮行。 > - **渲染文案里的字面 `\*\*`**:面板是纯文本渲染(无 markdown 解析),`sessions.repair.*` 的四条 zh 文案与布局归位的 `refreshHint` 会把 `\*\*` 原样显示成星号(英文侧用的是大写强调,zh/en 也不对称)。全部去标记。 > - **把「回滚失败」说成「已复原」**:布局归位的 `refreshHint` 原写「失败项已逐条回滚」,但失败项可能是 `rolledBack:false`(回滚自身失败)。改为「已逐条**尝试**回滚,回滚失败的项会单独标红(绝不显示为已复原)」。 > - **两条新护栏**:`src/client/recovery/recovery-panel-layout.test.ts`(源码级断言小节外层必须是 `.card`、标题必须是 `.groupLabel`、行列表必须在 `.snapshotList` 里 —— 这类「容器类选错」类型检查/模型单测/bundle 护栏**都抓不到**);`src/client/recovery/recovery-locales.test.ts` 的类级守卫(zh/en 任何值含 `\*\*` 即红,含变异验证:注入 → 红并点名键、还原 → 绿)。 > > **Three "what you see ≠ what is true" fixes in the panel (plus two new guardrails)** > - **The layout-relocation card was squeezed into one row**: the card wrapper wrongly used `.snapshotRow` (a horizontal flex row) and section titles used `.snapshotRowMain` (`flex:1` + `nowrap`), so the title, three hint lines, buttons and plan summary all landed on one line with vertical-wrapped text (user screenshot). Now: `.card` container, `.groupLabel` section headers, `.snapshotList`/`.reportScroll` vertical lists, `.actionRow` button rows. > - **Literal `\*\*` in rendered copy**: the panel renders plain text (no markdown), so four zh `sessions.repair.*` strings and the layout `refreshHint` showed asterisks verbatim (the English side used uppercase emphasis instead — also an asymmetry). All markers removed. > - **"Rollback failed" was reported as "restored"**: the layout `refreshHint` said failed items "were rolled back individually", but an item can be `rolledBack:false` (the rollback itself failed). It now says rollbacks are **attempted** per item and any failed rollback is flagged separately — never shown as restored. > - **Two new guardrails**: `recovery-panel-layout.test.ts` (source-level assertions that the section wrapper is `.card`, headers are `.groupLabel` and row lists are inside `.snapshotList` — a class of defect that typecheck, model unit tests and the bundle guardrail **cannot** catch) and the class-level guard in `recovery-locales.test.ts` (any `\*\*` in zh/en fails the suite; mutation-verified). > **致谢 / Thanks**:本轮的同步通道「选择/新建私有仓库」与链接跟随边界修复([#67](https://github.com/xiajiajun516/dsh-config-manager/pull/67))、 > 总览首屏性能修复([#68](https://github.com/xiajiajun516/dsh-config-manager/pull/68))、MCP / Skills 备份修复 > ([#72](https://github.com/xiajiajun516/dsh-config-manager/pull/72))均由 **@iuuuuuuuu** 贡献。 > > **Thanks**: the sync repository picker and the link-traversal boundary fix ([#67](https://github.com/xiajiajun516/dsh-config-manager/pull/67)), > the Overview first-paint performance fix ([#68](https://github.com/xiajiajun516/dsh-config-manager/pull/68)) and the > MCP / skills backup fix ([#72](https://github.com/xiajiajun516/dsh-config-manager/pull/72)) were all contributed by **@iuuuuuuuu**. ## [0.1.69] - 2026-10-04 > **本版已发布**:覆盖此前数轮并行落地的工作(会话跨机迁移与体检、UI v2 信息架构、磁盘占用体检、 > 版本更新检查、加密备份恢复入口、SAFE MODE 出口),以及本轮集中修复的六个上报 issue(#55–#60)。 > **Released**: this version ships several rounds of parallel work plus the six reported issues fixed here (#55–#60). > > 本轮修的是**「对话消失」的第二类根因**:DSH 的会话日志按 `header.version` 分版本,读不出的(更高的) > 格式会被 DSH **静默跳过** —— 不报错、不在工作区列表里。高版本能读低版本(V0→V4 迁移链),**反向不可读**, > 而本插件的导出/导入/同步是逐字节搬运,于是「导入全部成功、对话一个不显示」。现在导入/同步会在**分析阶段** > 就体检并告警,档案页也会显示每个档案的运行 DSH 版本与会话格式版本、对错配给出提示;**任一侧读不到版本 > 一律不提示**(不猜、不给假结论),也不会替 DSH 做格式迁移。 > > **Theme**: the second root cause behind "my conversations disappeared" is now visible before it hurts. > DSH stamps every session log with a `header.version` and **silently skips** logs it cannot read (no error, > not in the workspace list), while higher builds can read lower ones but not the reverse. Since this plugin > copies session logs byte-for-byte, imports and syncs now probe the bundle's session format versions during > **analysis** and warn when the local DSH build cannot read them; the profiles page shows each profile's DSH > version and session format version plus a mismatch hint. Unknown on either side stays quiet — no guessing, > and no format migration inside the plugin. > > 同一轮还补上了**磁盘占用体检**:备份页现在能回答「我的备份占了多少盘、哪些能清」,并提供只作用于 > 可重建缓存的「立即清理」(快照与同步数据永不在此删除)。 > > The same round adds a **disk-usage report** on the backups page — how much space the plugin's own > artifacts take, what is reclaimable, and a one-click cleanup that never touches snapshots or sync data. > > 再加上两件「让用户看得懂」的事:**「关于」页可以检查插件更新**(只读探测 npm,给一条可复制的升级命令, > 绝不自动安装);**导入前的兼容性评分有了结构化原因**(来源 DSH 版本 / 平台 → 本机 + 逐条判定依据), > 同步确认页也不再显示 `partial` 这种机器 token。 > > Plus two things that make the plugin self-explanatory: the **About page can check for updates** (read-only > npm probe that hands you a copyable upgrade command, never auto-installs), and the **import compatibility > score now comes with structured reasons** (source DSH version/platform vs local, item by item). > > 而**最大的改动是界面本身**:一级信息架构从 7 个功能页签重建为 **4 个对象页 + ⌘K 命令面板** > (首页 / 产物库 / 同步 / 环境),并在此后的真机自查里逐条修掉了它带出来的问题 —— > 详见下面两节(`UI v2` 与 `UI v2 落地后的真机修复`)。 > > **The biggest change is the UI itself**: the top-level IA is rebuilt from seven feature tabs into > **four object-centric pages plus a ⌘K palette**, with the follow-up real-machine fixes listed in the > two sections below. ### ✅ 本轮修复的上报 issue(按编号)· Reported issues fixed in this release > 六条都在真机上复现并定位过,共同点是**失败都不显眼**:其中四个都是「操作报成功、但结果不完整 / 读不回来」, > 用户只看得到症状(插件丢了、列表空的、密码输不进去),看不到原因。所以修复除了纠正行为,还都**把失败变可见**。 > 均已在 issue 上回帖确认(`confirmed` 标签),修复随本次发版交付。 > > All six were reproduced and root-caused on a real machine. They share one trait: the failure was never loud — > four of them reported success while leaving the result incomplete or unreadable. Each fix also makes the failure **visible**. ### 🔓 #55 加密备份的恢复入口 / Restoring encrypted backups - 🐛 **从「备份文件」列表恢复加密备份不再报「不是合法的 ZIP 文件」**:勾了加密的备份落盘是 DCA1 整包容器(文件名仍是 `.zip`),而**浏览器选文件**那条入口一直有形态探测 + 解锁阶段, **备份列表的一键导入**却把宿主路径直接送去分析 —— 容器被当 ZIP 解析,用户看到的是 「缺少中央目录结束记录」,像是备份损坏,实际只差一步「解锁」。 **Restoring an encrypted backup from the backup list no longer fails with "not a valid ZIP"**; the one-click import path skipped the unlock stage that the file-picker path already had. - 🧩 **容器形态探测收敛为单一事实源**(`src/security/container-kind.ts`):`containerKindOfBytes` / `readContainerKind`。文件形态**只读前 4 字节**(上传接口此前为看这 4 个字节把整份备份读进内存), 读不到一律回落「明文」,绝不猜成加密。备份文件列表新增可选探测(`listBackupFiles(dir, { withContainerKind: true })` → `BackupFileMeta.containerType`,默认关闭,保留策略/磁盘体检保持零额外 I/O)。 - 🛡️ **三道防护纵深**:`/analyze`、`/plan`、`/execute` 收到未解锁的容器一律返回 `code: 'encrypted-container'` + 可行动文案(不再让 ZIP 解析器的话术把用户引向「备份坏了」); 客户端据此**自动进入解锁阶段**(旧宿主 / 脚本直调也能兜住)。查看/对比入口对加密备份给出 「先导入解锁」的说明,列表行加「已加密」徽章。 - ⚠️ **常量与实现分离**:`ENCRYPTED_CONTAINER_CODE` 放在**零依赖**的 `src/utils/shared-constants.ts` —— client 半若 import `security/container-kind.ts`,会把 security 桶 → `encryption.ts`(node:crypto/node:util) 打进 `lib/client.js`,DSH 的 client loader 直接报 missed the module table,**整个插件不加载**(本轮实测 并已由 `bundle-selfcontained` 护栏复核:bundle 的 require 只剩 react/react-dom/jsx-runtime)。 ### 🛟 #56 SAFE MODE 不再是死结 / Safe mode is no longer a dead end - 🐛 **「放弃恢复」现在会一并解除保护**:dismiss 此前只把事务移进 quarantine,而唯一的清除点挂在 `verify(ROLLED_BACK)` 上 —— 事务已不在 `active/`,那条分支永远走不到。于是**界面显示「暂无需要 处理的恢复事项」、写操作却持续 423、重启也无效**,用户只能去磁盘删 `transactions/safe-mode`。 **Dismiss now clears safe mode** when no other unresolved incident remains (同一份判据,别的 incident 还在时照旧保守保留保护)。 - 🧭 **状态如实可见**:`GET /recovery/status` 新增 `safeMode: { blocked, clearable }`;投影进渲染模型为 `safeModeStuck`/ `safeModeBlocked`,「结案但保护仍开着」时计入 `recoveryRequired` 并渲染**显式解除入口** (危险按钮 + 二次确认),不再是空面板。 - 🚪 **补显式出口**:`POST /recovery/safe-mode/clear`(recovery prefix 路由下的子路径,不新增注册路由条目) + `RecoveryPort.clearSafeMode`。与 `lock/recover` 同处置:**故意不过 withMutationGate**(要解开的正是 挡住写操作的保护,走 gate 必然 423),但**绝不无条件清标记** —— 还有 NEEDS_ATTENTION / 非终态事务时拒绝 (`reason: 'unresolved-incidents'`),幂等返回 `not-blocked`。 - 🧪 测试:`tests/core/recovery-api.test.ts` 新增 4 例(dismiss 清标记 / 仍有未解决 incident 时保留 / status 的 blocked+clearable / 显式清除的确认与拒绝路径)、`src/client/recovery/recovery-view.test.ts` +3 例、 `src/security/container-kind.test.ts` 5 例(含「只读 4 字节」的实测断言)、`src/sync/backup-files.test.ts` +2 例、 `src/client/import/import-file-select.test.ts` +3 例。 ### 🟡 #56 同步「部分成功」不再被说成成功 / "Partly successful" is no longer reported as success - 🐛 **一条真机反馈揭出的第二条线**:webdav 同步时**插件安装失败**(pnpm 的成熟度等待期拦下), 而插件安装在执行顺序上**排在凭据写入之前**,于是**密钥根本没导进来**;用户只看到「同步完成」, 是靠事后翻日志才发现。根因不是顺序(顺序本身合理),而是**结果被低估**:插件安装失败是刻意的 非致命 `warning`(§34.17:一个装不上的插件不该拖垮已成功的其余配置),而 `ok` 与 `warning` 无关, 界面成功分支还直接弹绿 Toast —— 一串 warnings 被当成噪声。 **The second root cause behind the same report**: the plugin install step (which fails first and sits *before* credentials in apply order) is deliberately non-fatal, so the sync still reported success while secrets were never written. - 🧱 **「未生效项」成为结果的一部分**:`ImportResult.ineffective`(core)+ `ApplyItemsReport.ineffective` (sync)把 `status='warning'` 的计划项结构化回传(itemId / 分类 / 可读原因),**不改 ok 语义、不触发回滚** (字段只增不改;空数组时不出现 ⇒ 既有调用方逐字节不变)。同步路由透传并把历史记账改成 「N 项(M 项未生效)」+ 非空 error 摘要,历史里不再是一条干净的成功。 - 🔔 **界面据实降级**:渲染模型新增 `partial` 类型(有未生效项时**不再**是 `ok`),成功 Toast 降级为 warn 并给出条数,结果卡置顶渲染「部分成功:N 项未生效」+ 逐条列出未生效项(含各 adapter 自带的手动 修复命令)。措辞与事实同强度:既不说是失败(没有回滚),也不说是成功(确实缺了东西)。 - 🧪 测试:`src/core/smoke.test.ts` 在既有「插件安装失败 → 非致命 warning」用例上补三条断言 (进 ineffective / 带原因 / 不改变 ok 与 rollback)、`src/client/sync/sync-view-v2.test.ts` +2 例 (partial 判定与「空/缺字段 → ok」的边界)。 ### 📦 #57 `file:*.tgz` 的本地插件不再静默进不了备份 / Local plugins declared as `file:*.tgz` - 🐛 `link:` / `file:` 的 spec 有两种**合法形态** —— 目录与**已打包的 tarball**。打包路径此前一律当目录 spawn `npm pack`,真机上 9 个 `file:/abs/x.tgz` 插件**全部** `spawn ENOTDIR` 被跳过,而备份仍报 `ok:true`(按设计只记 warning)—— 用户以为「备份成功 = 备份完整」,恢复时装不回这批插件。 **A `file:` spec can point at a directory *or* at a prebuilt tarball**; the pack step only handled the former, so every `.tgz`-declared plugin failed with `spawn ENOTDIR` while the backup still reported success. - 🔧 现在先 `stat` 判形态:**文件即 tarball → 直读收编、跳过 `npm pack`**(保留源文件名,只做文件名净化); 目录走原路径;**判不出形态一律回落目录流程**并给出可读告警(绝不猜成文件)。归档名仍恒为 `local-plugins/<文件名>`、**不含任何路径分隔符**,与目录形态产出同一形态,导入端重写逻辑无需改动。 - 🧭 顺带修正 profile 目录来源:改用 `HostContext.profileDir`,不再按 `/profiles/` 硬拼 (DSH 的档案目录不保证是默认布局,拼错会让相对 spec 解析到错误位置)。 - 🧪 `src/core/local-plugin-pack.test.ts` 新增 5 例(`.tgz` 直读且 `npm pack` 零调用 / 判不出形态回落 / 超上限告警 / 目录与 `.tgz` 混合各走各的 / `safeTarballFileNameFor` 的穿越与回退)。 ### 🧱 #58 从 git 源安装不再必然失败 / Installing from the GitHub source - 🐛 本包以 `files: ["lib", ...]` 发布**预构建产物**,npm 安装不需要任何构建步骤;而 `dsh plugin add git+https://...` 拿到的是**源码树** —— pnpm 需要在 clone 里跑一个构建入口才能得到 `lib/`。 `scripts` 里此前**没有任何 pnpm 认的构建入口**(`prepare` 只在 `npm publish` 前校验、不参与安装), 于是 git 安装 100% 失败于 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,用户只看到 `dsh: plugin command failed`。 **The npm release ships a prebuilt `lib/`, but a git install gets the source tree** and needs a build entry point that the package did not declare. - 🔧 补 `prepare: npm run build`;README(中英)新增「从 GitHub 源码安装」段,写明 pnpm 11 的 `allowBuilds` 白名单必须**逐字照抄 pnpm 打印的那一行**(含完整 git URL + commit sha;**只写包名不生效**),并给出更省事的 替代路径(装 npm 预构建版)。真机复核:首轮按预期报错并打印可粘贴的键,加白名单后第二轮 exit 0, `lib/index.js` 与 `lib/client.js` **均产出**。 - 🧪 `tests/packaging-contract.test.ts` 新增三条 `G-21` 门禁:`prepare` 存在且真的调 `build`/`bundle`、 `files` 含 `lib`、`lib` 仍被 `.gitignore`(缺任何一条,git 安装会静默拿到一个没有 `lib/` 的包)。 - ⚠️ **剩余边界(如实登记)**:pnpm 11 默认拦截 git 依赖的构建脚本,**这一步无法由包作者免除** —— 安装者必须在 profile 的 `pnpm-workspace.yaml` 里授权一次。这是 pnpm 的供应链策略,不是本包的缺陷。 ### 💾 #59 加密快照 >64 MiB 时远端列表不再恒为空 / Encrypted snapshots above 64 MiB list correctly again - 🐛 **读写两侧体积口径不一致**:加密单文件布局把整份快照塞进一个 JSON(写入侧**没有体积守卫**), 而读取侧走 `parseJsonSafe` 的**缺省** 64 MiB 上限 —— 那是防**不可信输入**的闸门。加密载荷经 「JSON → base64 → 加密 → 再 base64」约 ×1.78 膨胀,勾上 `sessions` 后单文件轻松越界(真机 4 个快照均 ≈66 MiB)。 越界的后果**全是静默的**:`list()` 逐条跳过 → 远端列表为空(而同一响应里 `currentSnapshotId` 却指着那条快照)、 `download()` 报「快照损坏」→ 拉取/一键同步彻底不可用、`hasNewRemoteSnapshot()` 恒 false → 自动同步永远 `skipReason: upToDate`。**Read and write disagreed about the size limit**, and every consequence was silent. - 🔧 读取侧改用 `MAX_OWN_PAYLOAD_JSON_BYTES`(512 MiB,**自产载荷**专用),与写出能力对齐 —— git 与 WebDAV 的密文单文件共用同一份解码器,一处修复两处生效。 - 👁️ **把失败变可见**:读不出来的快照随列表回传(`POST /sync/snapshots-list` 的 `unreadable[]`), 同步页常驻警告横幅列出**具体文件名**,列表到货时另给一次即时回执 —— 跳过是必要防御,但**绝不静默**。 - 🔒 **不变量(未放宽)**:`DEFAULT_MAX_JSON_BYTES`(64 MiB)仍是不可信输入的闸门,未被抬高; 快照读取仍受 `maxDepth` 与形状校验约束;读失败诊断属远端**当下**状态,**不落 sessionStorage**。 - 🧪 `src/sync/snapshot-json.test.ts` 新增 2 例(自产上限 > 缺省上限;>64 MiB 载荷可反序列化且缺省上限仍拒它)。 ### ⌨️ #60 WebDAV 密码框不再被无关状态更新清空 / The WebDAV password field no longer gets wiped - 🐛 `runStore.patch()` 每次调用都会跑持久化白名单,而白名单**把密码字段写成空串**(安全不变量)。 于是任何一次 patch —— 包括与密码**完全无关**的(远端快照列表到货、GitHub 授权轮询结束、自动同步状态刷新)—— 都会把 `state.sync.webdavPassword` 清空,下一次镜像回来时输入框就空了。真机表现:**输入几个字符就被自动清空**、 **粘贴同样被清空**、输入过程中「保存配置」按钮闪一下(防抖自动保存被反复取消),且**没有任何保存成功提示**。 **The persistence allowlist blanks password fields on every patch**, so unrelated state updates wiped what the user was typing. - 🔧 新增 `RunStore.patchSyncPasswords({ token?, webdavPassword? })`:只写**内存**切片(不触发 `save()`); `SyncPanel` 的 `commit()` 在 `runStore.patch(...)` 之后立即用它把**在途输入**写回。 - 🔒 **不变量(未放宽)**:密码仍**绝不**进 sessionStorage / 磁盘 / 日志(`toPersistedState` 白名单一行未改), 刷新后仍清空(`applyPersisted` 的硬性归零也没动)。 - 🧪 `src/client/run-store.test.ts` 新增回归:在途密码经「无关 patch」后仍在内存、落盘文本不含密码、 未给出字段保持部分更新语义;既有的「同步凭据绝不写入 sessionStorage」用例继续绿。 ### 🔌 同步通道配置可断开(用户上报)· Disconnecting a sync channel - 🚪 **配置过的通道现在能删掉了**:此前同步通道一旦配置就**没有出口** —— 一条打不通的 WebDAV 通道(如口令失效返回 401)会永久占位,产物库的远端源与自动同步只能一直报「读取失败」,用户无从自救。 现在通道入口卡多一个 **danger「断开配置」**(二次确认),宿主 `POST /sync/config { clear:true }` 一处清三项: ① `sync-config.json` 里该通道的命名空间(**活动通道自动切到剩下的那条** —— 不切的话,那条**配置过**的通道反而会 被显示成「未配置」);② 该通道在 DSH credentials 里的全部凭据(token / WebDAV 口令 / 加密·解密密码); ③ 该通道的自动同步开关,并立即 `scheduler.reload()` 丢掉已排期的定时器。 **只解除本机绑定**:远端快照、本机备份、同步分区选择一律不动,之后可重新配置。 **Configured sync channels can now be disconnected** — the channel entry card gains a danger "Disconnect" action with confirmation, and the host clears the channel's config namespace, every credential slot and its auto-sync switch in one place. Remote snapshots and local backups are untouched. ### 🗂️ 会话迁移 / Session migration - 🕳️ **「导入成功但对话没显示」现在会在导入前被指出**:新增宿主探针 `src/utils/session-format.ts`(`parseSessionFormatVersion` / `readSessionFormatVersionAt` / `resolveSessionFormatVersion` / `probeSessionFormats`)——只解会话日志的**首帧**取版本, 每个会话只看一条日志,上限 200 条,读不出的如实计入 `unreadable`、未检查的计入 `skipped`。常量从 `installAnchor`(桌面端在 `app.asar` 内)同树的 `@deepseek-ai/dsh-session` 读,**绝不拿 DSH 的 semver 猜格式版本**。 **Imports now flag conversations the target DSH cannot read** before anything is written. - ⚠️ **分析阶段告警**(core 只消费数字,不碰 DSH 存储格式):`HostContext.sessionFormatVersion` + `AnalyzerOptions.sessionFormatProbe`(`ImporterOptions` 同名透传)→ `Analyzer.sessionFormatWarnings` 在 「包内版本 > 本机支持版本」时产出 `import.sessionsFormatUnsupported`(中英双语),抽查有截断时追加 `import.sessionsFormatSampled`;本机版本解析不到时**不告警也不谎报兼容**(宿主启动时 `log.warn` 一行)。 同步的差异报告(`pull()`)把 `analysis.warnings` 并进 message —— 那条链路只有一个 message 通道,不并入就是静默。 - 🧾 **档案页显示版本**:每个档案展示 DSH 版本与会话格式版本(读不到 = 不显示),并对「本实例 VS 该档案」的 错配给出徽章(`profileVersionFacts` / `sessionFormatRisk`;任一侧读不到一律不提示)。 **Profiles page** now shows both versions and a mismatch badge per profile. - 🧪 **测试**:`src/utils/session-format.test.ts`、`src/utils/session-log.test.ts`(`version` 解析 + 非法值不猜)、 `src/core/import-sessions-visibility.test.ts`(v4→v3 必告警 / v3→v4 不告警 / 无法判定不猜)、 `src/ui/dsh-profiles-view.test.ts`、`src/profiles/dsh-profile-manager.test.ts`。缺口登记为 `docs/spec/known-gaps.md` **G-23**,兼容矩阵补一行(`docs/spec/compat-matrix.md` §3.5)。 - 🎛️ **导入/同步的处置开关落地(中止 / 跳过 / 仅提示,缺省中止)**:发现「本机读不了的会话」之后,用户现在能**自己决定** 怎么办 —— `abort`(**缺省**:计划阶段就拒绝,**零写入**,附升级/离线体检指引)/ `skip`(这些会话单元默认不勾选, 其余分区照常导入;手动勾回即照常导入)/ `guide`(不改行为,只给指引)。判定与展示**共用一份纯函数** (`src/ui/session-format-disposition.ts`),宿主接入 `/plan`、`/execute`、`/sync/sync`、`/sync/apply-items` (解析顺序 = 请求体 > 插件配置项 > 缺省 abort;`abort` 回 **409 + code=`sessionFormatUnsupported`**)。 **只读查看(备份内容查看)与市场/「我的配置」复核页显式走 `guide`** —— 它们没有决策界面,沿用缺省会让用户连 「看看包里有什么」都被拒。**Disposition switch** (abort / skip / guide) with a zero-write abort at the planning stage. - 🐛 **修掉一个会让上面所有告警静默失效的键空间缺陷**:探针的会话单元键是 `projectKey/会话目录`(来自分区内相对路径), 而计划项的 `unitId` 带适配器前缀(`sessions:`)—— 两边直接比较**一条都匹配不上**,于是 `PlanItem.formatUnsupported` 永远为空、界面永远不提示「这些对话导入了也看不见」。现在两种形态都试 (`sessionUnitVersion()`),并且**测试桩改用真实的前缀形态**(此前桩不带前缀,掩盖了这个缺陷)。 - 🔁 **导入会话后明确提示「重启 DSH 才会显示」**:DSH 的会话列表与工作区投影都在**启动期**建立,导入的会话不重启就是看不见 (生态同类插件同结论,桌面端需退出重开)。提示只在**本次真的写入了会话文件**时出现(判定挂在导入上下文上, 避免适配器实例跨导入累积导致「没勾会话也让你重启」)。**Restart hint** after importing sessions. - 🩺 **会话体检(只读)**:事故恢复子 tab 新增「会话体检」——一键扫描本机存量会话,按严重级列出问题 (**会让 DSH 起不来 > 会话不可加载 > 下次请求会失败 > 不可见**):重放重复行、撕裂尾帧、非法帧、header 不可读、 seq 空洞、合成 closer 块、格式版本超前、未登记工作区、子代理缺父、位置与 `projectKey(cwd)` 不一致、重复 id。 **「未检查 N 条」全程可见**(把没检查说成没问题是最严重的谎报);**应用内零写入**,每行只给可复制的离线命令。 **Session health check** (read-only) in the incident-recovery tab, with the same severity ladder DSH itself would suffer. - 🛠️ **离线安全修复(唯一允许写会话字节的通道)**:`dsh-config-manager sessions list / doctor`(只读)与 `sessions repair [--apply]`。强制序列:**写前重跑「连续性 + 引用完整性」校验(不过即拒绝,绝不修得更狠)→ 时间戳备份(绝不覆盖已有备份)→ 临时文件 + `rename` 原子换入 → 写后复验(不过就用备份还原)**; 写操作前**强制检测 DSH 是否在跑**(读心跳),在跑就拒绝并提示先关 DSH。本轮只做**能从字节证明的零/低损失修复** (重放族的字节相同重复行),需要 DSH codec 才能判定的类(缺 message id / 悬空 tool-call / settlement 非法) **一律不产出结论**(检测 ≠ 发明)。缺口登记为 **G-24**,未做的部分(codec 深度解码、header 重建、转写导出) 如实写在该条目的「后续」。**Offline-safe repair** behind a verify-then-write sequence, only when DSH is stopped. - 🧪 **测试**:`src/ui/session-format-disposition.test.ts`、`src/routes/session-format.test.ts`、 `src/ui/import-wizard.test.ts`(三种处置各一条)、`src/core/import-sessions-visibility.test.ts`(abort/skip/guide)、 `src/core/session-health.test.ts`(13)、`src/utils/session-health-scan.test.ts`(10,含「扫描前后字节与 mtime 不变」)、 `src/ui/session-inventory-view.test.ts`(7)、`src/utils/session-log-repair.test.ts`(7,含拒绝路径零写入)、 `src/ui/next-steps.test.ts` / `src/adapters/sessions.test.ts`(重启提示与跨导入隔离)、 `src/client/common/http.test.ts`(错误透传 status/code)。 - 🧱 **处置开关的地基**:探针升级为**单元级**(`SessionFormatProbeResult.units`), `Analyzer.resolveSessionFormats` **每份归档只算一次**并供三方共用(分析告警 / `PlanItem.formatUnsupported` / `ImportAnalysis.sessionFormats`)——「本机读不了的会话」现在是**具体计划项上的标记**,向导据此提供三选一; 纯附加字段,不拦执行、不改 kind/conflict,读不到版本一律不判。 **Disposition groundwork**: unit-level probe results plus one per-archive resolution shared by warnings, plan-item flags and the structured analysis summary. - 🔧 顺带修一条**环境相关的测试假红**:`src/adapters/plugins.test.ts` 的「安装失败 → 非致命 warning」用例依赖 `resolveProcessProfileName()`(读 `DSH_PROFILE`),在桌面端会话里会解析成 `desktop` 而断言写的是 `web`; 改为显式传入 profile,测试不再随运行环境漂移。 ### 💾 磁盘占用 / Disk usage - 🧭 **备份页新增「磁盘占用」卡**:本插件的产物(`exports/` 导出备份、`snapshots/` 导入前快照、`sync/` 同步 配置与 Git 工作副本、`market/` 缓存与工作副本、`tmp/` 暂存、`logs/`、`boot-state/`、`migration-history/`、 `transactions/`、`locks/`、`vault/`)此前在界面上**没有任何数字** —— 用户无法回答「我的备份到底占了多少盘、 哪些能清」。现在逐区列出体积/文件数/回收策略,并区分三档语义:**可随时重建**(缓存与暂存)、 **有保留期**(导出产物 7 天、定时备份保留最近 N 个)、**用户数据/安全网**(快照与同步,永不在此清)。 - 🧹 **一键释放空间**:缺省只清**可重建**的缓存与暂存(忽略保留期整块清);回收**过期**备份文件必须显式勾选, 并走 danger 二次确认。请求里的分区白名单是硬边界 —— 用户没勾 `expired-exports` 时**一个导出文件都不碰** (回归测试:`tests/route/disk-usage-routes.test.ts` 的「只清可重建区,备份/快照原样保留」)。 - 🔒 **两条不变量**:① 快照与同步数据**永不在候选集内**(`snapshots` / `sync` 连路由参数都进不去); ② 体检是**只读**且不跟随符号链接/junction(不重复计数、不成环),目录读不到时如实标「未统计」而不是显示 0 字节。 - ⚙️ 与既有自动清理**同一套口径**:`core/cache-cleaner.ts` 抽出 `sections` 白名单与 `includeRecent`(手动), 保留期仍只有一份实现;新增 `core/disk-usage.ts`(只读扫描 + 超期判定)与 `src/ui/disk-usage-view.ts` (纯函数视图模型,界面数字由渲染出的行现算,杜绝「按钮说能释放 600 B、实际释放 0」的漂移)。 - 🧪 **测试**:`src/core/disk-usage.test.ts`(6 例:聚合 / 导出豁免前缀 / 未统计 / 符号链接 / 保留期 / 容错)、 `src/ui/disk-usage-view.test.ts`(6 例:空报告 / 未统计 / 可回收清单 / 过期提示 / 保留期文案 / en 字典)、 `src/core/cache-cleaner.test.ts`(+2 例:`includeRecent` 语义、缺省仍只清超期)、 `tests/route/disk-usage-routes.test.ts`(4 例:只读 GET / 非法请求 400 / 只清缓存 / 只回收过期导出)。 路由快照 68 → **70**(`GET /disk-usage`、`POST /disk-usage/cleanup`)。 ### 🐢 预览性能回退修复 / Preview performance regression > 真机报告:「总览页与导出页加载变长了」。定位到的根因是 **v0.1.68 引入的 `preview()` 在单文件分区上走错路径**。 - 🐛 **`agentInstructions` 的预览把整个 `$DSH_HOME` 递归了一遍**:`AGENTS.md` 在 home 根(`baseDir = ''`), 基类 `preview() → collect() → listFilesDetailed(baseDir)` 于是遍历整个 home —— 实测 **4.8 s / 4016 个文件 / 242 MB**,选择器还把 `profiles/`、`sessions/`、`attachments/` 等无关目录当成该分区的 **21 个假单元** (而真实导出只读 `AGENTS.md` 一个文件 = 预览与导出分叉)。总览页调 `/export-preview`(默认分区集合含 `agentInstructions`)→ 单这一项就占 **5.0 s 中的 4.8 s**;导出页的选择器同理。 - ✅ **修复**:`FileCollectionAdapter` 新增可覆写的清单钩子 `listRelPaths()`(`export`/`preview` 共用一份清单), `AgentInstructionsAdapter` 覆写为**单文件白名单**(存在性判定走 `statSize`,旧门面退回读一次文件), 并删掉冗余的 `export()` 覆写 —— 现在预览与导出经同一个 `collect()` 内核,**逐项一致由构造保证**。 实测同一台机器:该分区预览 **4.8 s → 1.4 ms**,1 个文件 / 8131 B / 1 个真实单元。 - 🧪 回归护栏 `src/adapters/agent-instructions.test.ts`(5 例):用「一被调用就抛哨兵」的门面证明**不再递归**、 预览与导出逐项一致(条目/体积/计数/告警)、home 里的无关文件一个都不进该分区、文件缺失 → 空分区 + dirEmpty 告警。 - ⚡ **后续两项优化(同一轮)**: ① **父子关系短 TTL 缓存**:`parentRelations()` 走 DSH `sessionPersistence.list()`(真机 ≈ **0.7 s**), 而 `/export-preview` 每次都要它(选择器联动用)。新增 `src/utils/ttl-cache.ts`(通用短 TTL 异步缓存: **同 key 并发合并成一次执行**、**失败不缓存**、时钟可注入),`DshSessionStoreFacade` 以 **5 s** TTL 缓存 该 Map —— 连续打开/刷新选择器只付一次;新子代理会话最多 5 s 后即可被联动识别(对选择器无影响)。 ② **体积与时间合并成一次 stat**:新增 `FileSystemFacade.statInfo`(宿主一次 `fs.stat` 同时给 `{ size, mtimeMs }`), 预览把顺带取到的时间放进 `SectionPreview.statTimes`,`unitActivityTimes(ctx, section, statTimes)` 直接复用、 **不再逐文件 stat**。真机实测同一棵会话树:**1361 次 stat(983 size + 378 mtime)→ 983 次**,结果逐项相同; 两个旧门面(`statSize` / `mtimeMs`)保留为回退路径 —— 旧宿主只慢不坏。 - 🧪 新增测试:`src/utils/ttl-cache.test.ts`(6 例:命中/过期/关闭/并发去重/失败不缓存/容量与 clear)、 `src/adapters/file-collection.test.ts`(+2:`statInfo` 优先且旧门面一次都不调;`statInfo` 返回 null 时退回 `statSize`)、 `src/adapters/sessions.test.ts`(+2:给定预览时间时**绝不**发起第二趟 stat 且结果与逐文件路径一致;缺项不进 Map 不猜 0)。 - 📉 顺带记录**剩余**成本(不是本次回归,未改):`sessions` 预览实测 **1.14 s** = 真实遍历 324 ms(983 文件 / 517 MB) + 体积 stat 64 ms + mtime 通道 9 ms + **DSH `parentRelations()` 与元数据 ≈ 0.7 s**(每次预览都重新取)。 它在 `sessions` 显式放行时才发生(该分区 `defaultIncluded = false`),因此不影响总览页默认加载。 ### ⬆️ 版本更新检查 / Update check - 🔍 **「关于」页新增插件更新检查**:`GET /update-check` 只读探测 npm 的 latest(`core/update-check.ts`), 命中时显示「已是最新 / 有新版本 vX」并给出**可复制的一条升级命令**(精确版本 `dsh plugin --profile <档案> add dsh-config-manager@` —— 用精确版本而不是 `@latest`, 因为 pnpm 的 `minimumReleaseAge` 会让 `@latest` 解析到旧版)。 - 🔒 **四条硬边界**:① **绝不自动安装/升级**(只探测 + 给命令);② 进程内缓存 10 分钟(`?force=1` 由 「重新检查」触发),不把 registry 当轮询端点;③ 网络/超时/非 2xx/响应畸形/体积超限**一律如实报错** (绝不「失败当最新」),HTTP 仍回 200(离线不是插件故障,界面给可重试提示而非错误横幅); ④ 版本比较复用 `validator.ts` 的同一份 semver 规则,不另写一套。 - 🖥️ **桌面端档案不误导**:`desktop` 由 Electron 独占管理、未知档案也不猜 —— 这两种情况不给终端命令, 改为提示去 DSH 插件页更新(不给注定失败的命令)。 - 🧪 测试:`src/core/update-check.test.ts`(9 例:解析 / 失败四态 / 超时 / 严格更新判定 / 缓存与 force / 失败不缓存 / `force=1` 判定)、`src/client/about/about-view.test.ts`(+5 例)。 ### 🧭 兼容性讲清楚 / Compatibility explained - 📋 **评分有了结构化原因**:`core/validator.ts` 新增 `compatibilityReasons()`,`computeCompatibility()` 改为**由原因派生**(单一事实源:界面解释与评分不可能漂移);`ImportAnalysis` 回传 `compatibilityReasons` + `source` / `target`(来源与本机的 DSH 版本 / 平台 / schema 版本)。 **评分口径逐条冻结**:`sourceOlder` 仍覆盖跨平台的 `partial`(历史行为,改它需单独决策),已由 `src/core/compatibility-reasons.test.ts` 显式写下。 - 🪟 **导入向导新增「来源与兼容性」块**:「DSH 版本:源 → 本机」「平台:源 → 本机(跨平台标黄)」 「备份格式 vX」+ 逐条判定依据(跨平台 / 分区缺失 / 来源更新或更旧 / schema 超范围)。原因里的 版本与平台字符串来自**包内**,渲染前一律过 `redact()`(B1 教训)。 - 🔧 **同步确认页不再显示机器 token**:此前直接渲染裸枚举(中文界面里出现 `partial`)且 Badge 恒中性色, 现在走 `src/ui` 的等级/语义色纯函数 + sync 字典文案(`unsupported` 是 error 色);导入向导与同步页共用 `client/common/compat-label.ts` 的键映射(源码级守卫禁止再写第二份)。 - 🧪 测试:`src/core/compatibility-reasons.test.ts`(8 例)、`src/ui/import-wizard.test.ts`(+3 例)、 `src/client/common/compat-label.test.ts`(3 例,含两条源码级防回归)。路由快照 70 → **71** (新增 `GET /update-check`)。 ### 🖥️ UI v2:一级信息架构重建 / UI v2: rebuilt top-level IA > 这一轮把「备份与迁移」从 7 个功能页签重做成 **4 个对象页 + ⌘K 命令面板**:首页 / 产物库 / 同步 / 环境。 > 起因是旧界面的三处结构性问题:备份页有 4 个子 tab(其中「磁盘占用」与页面主题无关)、 > 英文下页签溢出要按住 Shift 才能滚、以及「对象」与「日志」混在同一张表里。 > 落地的四条原则:**对象与日志分离**、**每页 ≤3 子视图**、**未知值不显示 0**、 > **两个同步通道卡恒显示**(`autosync` 与 `sync-selection` 本就按通道独立)。 > > This round rebuilds "Backup & migration" into **four object-centric pages plus a ⌘K command palette** > (Home / Artifact library / Sync / Environment), replacing seven feature tabs. - 🧭 **一级导航收敛为 4 页 + 命令面板**(`src/ui/nav-model.ts` 的 `navLayout` 负责溢出判定, 放不下的项进「更多 ▾」)。导出与导入**不再是页签** —— 它们是 Task Mode 侧滑面板 (多阶段向导 → 面板;单次决策 + 报告 → Modal,这是选容器的唯一依据)。 `⌘/Ctrl+K` 打开命令面板(`src/ui/commands.ts`),**增一个功能 = 注册一条命令**, 不必再往 564px 的导航条里挤一个页签。导航从 7 项降到 5 项,英文下不再溢出。 - 📦 **产物库取代备份页**(`src/client/library/`):本机快照 / 备份文件 / 远端快照 / 市场产物 **四源合一扁平列表**,来源只是筛选维度而非分组 —— 用户找「我上周那份」时不该先想它在哪个源里。 行的动作由 `src/ui/artifact-view.ts` 的能力集合 (`restore|import|pull|install|inspect|download|consult|pin|unpin|delete`)分派,四种来源共用一份模型。 同步页的远端快照行搬到这里,那边只留**操作日志**(新增 `SyncLogList.tsx`,去掉「类型」列)。 - 🏠 **首页取代总览页**:状态行 + 动作网格 + 最近产物(3 行)。指标段改为**精确跳转** (备份文件 / 安全快照 → 产物库并预置来源筛选;定时备份 → 本页开设置弹窗,因为它是设置不是对象)。 「立即备份」放在工具栏最左(导出 ZIP 的左边)—— 它与导出是同一件事的两种强度。 - 🗄️ **环境页取代档案页**:档案(含本机概况卡)+ 维护与诊断(恢复面板 + 磁盘占用)两个子视图。 事故恢复与磁盘占用从备份页移到这里,备份页的 4 个子 tab 随之解散。 - 🔀 **同步页两张通道卡恒显示**:为此把 14 个 handler 从「隐式读当前通道」改为**显式收 channel 参数** (`channelStateOf` / `patchChannelOf` / `saveSelection` / `toggleSyncSection` / `setSessionsLimit` / `setSessionsInclude` / `setEncrypt` / `setIncludeSecrets` / `persistEncryptPassword` / `persistDecryptPassword` / `clearSavedDecryptPassword` / `toggleAutosync` / `updateAutosyncInterval` / `remoteReadyOf`);参数缺省仍取激活通道(弹窗路径沿用),卡内一律显式传自己的。 同步分区从**页面级一段**移进通道卡 —— 它本就是该通道的设置,做成全局一节会让用户以为两通道共用一份选择。 - 🗑️ **删除 10 个 v1 文件**(全仓零活引用):`SnapshotsPanel` / `OverviewPanel` / `ProfilesPanel` / `SyncSettingsView` / `SyncHistoryView` / `nav-overflow` 及各自专属测试。 - 🧪 测试:`src/ui/nav-model.test.ts`、`src/ui/commands.test.ts`、`src/ui/artifact-view.test.ts`(+18 例)、 `src/client/library/task-panel-visibility.test.ts`、`src/client/common/floating-units.test.ts` (浮层内联 `top/left` 必须带 px 单位)。`DESIGN.md` §1 重写并新增 §1.1(四页构成 + 三条不得回退的结构原则)。 ### 🐛 UI v2 落地后的真机修复 / Real-machine fixes after UI v2 - 📏 **Select 弹层不再改变页面高度**:真机反馈「展开下拉页面会变高」。用 Chrome headless 实测确认 机制是 **CSS 的 scrollable overflow area** —— `position: absolute` 虽不撑高父级,但它的溢出会扩展 **最近滚动祖先 `.shellMain`** 的可滚动区域(实测 `scrollHeight` 400 → 556)。修法:弹层 portal 到 插件根容器(`.section`,`position: relative` 且**不在**滚动容器内)+ **根相对坐标**(触发器 rect 减根 rect, 纯减法),于是既脱离滚动区域、又不必用 `fixed`(`fixed` 会踩 `.dialogContentCenter` 常驻 `transform` 的包含块,此前几轮反复出错的根源)。 - 🖱️ **产物库的四个真实缺陷**:① 远端快照源恒失败(请求漏了 `transport` + 地址,宿主 `prepareSync` 需要它);② 展开某行会重复派发上一次的动作(去重键里混了 `Date.now()`,改用 target 的引用身份); ③ 删除后整页刷新且滚动跳回开头(刷新时把源置回 loading 并清空 items ⇒ 列表被卸载, 改为 stale-while-revalidate);④ 底部「N 个加密」统计的是全部来源而非当前筛选。 - 📥 **导入不再残留上一次的数据**:`importBackup` 的消费者只清了 decrypt 那几个字段, 上一次的 `step` / `analysis` / `plan` / `result` 全部残留 ⇒ **加密备份的密码输入框不出现**。 改为复用既有的完整 `resetWizard`(不再维护第二份「部分重置」清单 —— 那种清单必然漏字段)。 - 🎞️ **产物库行展开/收回有动画**:外层 `grid-template-rows: 0fr → 1fr`(内容高度未知也能过渡), 内层 `min-height:0 + overflow:hidden`;组件从条件渲染改为**始终挂载**(条件渲染让收回一侧没有收尾帧)。 收回时闪一下的根因是 `transition-delay` 只写在展开态,已补齐对称延时。 - 🐛 **「迁移前咨询」对远端快照报 `repoUrl is required`**:宿主 `routes/consult.ts` 的远端分支写的是 `prepareSync({})` —— 传空对象,而 `prepareSync` 解析的是**请求体**。改为透传 body (实测:即使客户端传完整 payload,旧宿主仍报同样的错,因为它根本不看 body)。 - ⬇️ **远端快照「拉取」= 拉取即导入**(用户定案):新增 `POST /sync/download` + `SyncEngine.downloadSnapshot()` 把远端快照**落地成本机 ZIP** 并返回路径 —— `POST /sync/pull` 只回 只读差异预览(临时 ZIP 用完即删),拿不到可导入的文件。客户端点「拉取」进入**阻塞式 Modal** (遮罩 + focus trap 天然禁用页面其余部分)+ 加载态,成功后自动进入导入侧拉面板。 **走导入向导而不是直接写配置**:向导已有解锁加密备份、选内容、冲突决策、执行前快照与回滚, 另造写入通道等于把这五件事重做一遍且更难回滚。 - 🌐 路由数 **71 → 72**(新增 `/sync/download`);`tests/route/` 的三条计数守卫与快照同步更新。 ### 会话修复可以**在应用内直接做**了(只做零损失的那一类) - 🧾 **会话体检的列表更好读了**(T11,用户反馈「文字挤成一团」):会话名独占一行(长名不再把后面挤没),下面的元信息行按「状态徽章 | 问题码 | 版本·体积·时间」分列——徽章定宽、问题码可伸缩、元信息右对齐等宽数字;动作按钮固定右对齐。 - ⚡ **一键修复全部**(T11/T12):对当前列表里可修的会话逐条走完整安全序列(预览 → 应用)。**逐条独立**(一条被拒不牵连其余)、**有损不批量**(需要截断的计划一律跳过并计入被拒);结果**逐条留痕**(已修复 N / 未修复 M + 每条的丢弃行数或被拒原因),修好的行**就地移出列表**——不再整屏重扫让用户白等一次全量体检(要看最新全貌时自己点「开始体检」)。 - 🙈 **默认只显示有问题的对话**(T11):正常会话不再占据列表(它们在摘要里照常计入总数),被隐藏的条数在卡片上明说(`另有 N 条会话正常`),可修的行即使当前判为正常也仍然显示。 - 🩺 **环境 → 会话体检里直接修**(T8,2026-10 扩展为 T9):结果与动作整体搬进**独立弹窗**(卡片只留入口与一行摘要);对「重放重复行」「可证明的合成收尾块」(零损失)与「seq 空洞 / 不可解析行」(**有损截断**,必须显式确认)新增 **dry-run 预览 → 显式确认 → 自动时间戳备份 → 原子换入 → 写后复验**的应用内通道,并在「修复记录」里提供**一键回滚**。 - 🔒 **五道门,一道都不放宽**:① `unitId` 只能解析到 `/sessions` 之内的真实会话目录(路径穿越 / 符号链接逃逸一律 unknown-unit);② 目录内有 `session.lock` 不修;③ 日志最近 30 秒被写过(可能在活跃使用中)不修;④ 预览与应用之间的**大小 + mtime 指纹**必须一致(TOCTOU:预览后文件被改过就拒绝);⑤ 写操作过 `withMutationLock` + SAFE MODE 闸门,写入本身照抄离线 CLI 的「写前自校验 → 备份 → 原子换入 → 写后复验」,校验不过绝不「修得更狠」。 - ↩️ **回滚只认台账 repairId**:客户端**拿不到也传不了路径**;目标在修复后又被改过(指纹不一致)时回滚被拒绝,绝不覆盖更新的内容。台账损坏时先留档 `.corrupt-*` 再写新账(不静默丢证据)。 - 🧭 真正**证明不了怎么修**的类别(容器非法 / header 不可读 / 格式超前 / 撕裂尾帧自愈 / 缺工作区与缺父对话)**仍在应用内零写入**,继续只给可复制的离线命令 —— 不猜、不发明、不「修得更狠」。 - 🩺 **修掉会话体检弹窗把整个面板打白**(真机 + 浏览器实测定位):`SessionHealthDialog` 的 state 声明写在使用点之后,触发 TDZ `ReferenceError: Cannot access 'repairedUnits' before initialization`;DSH 把插槽异常当成 `slot entry crashed in 'settings.section'`,于是「备份与迁移」整页空白。修复 = 把声明提到使用之前;另加源码级守卫(声明必须早于使用、不得重复声明)防回归。**同时**:弹窗改为**只在打开时挂载**、长列表限高内滚。 - 🩺 **又一次体检误报(第二轮,真机 + 浏览器实测)**:`turn/end` 之后紧跟 `workspace/changes`(同属该回合的收尾元数据)曾被判成「合成收尾块撞上真实续写」,导致 4 条**健康**会话既被标红、又在点「修复」时以 `写前校验未通过` 被拒。现在只有**续写事件**(消息 / 流式块 / 工具调用 / 回合与步骤开始)才算续写,元数据行不算;真机复核:可修条数 4 → 0,弹窗不再出现假的修复按钮。 - 🩺 **修掉体检的误报**(本轮真机发现):`synthetic-closer`(合成收尾块)此前只按**形状**判定(≤8 行收尾类型 + 含 `turn/end`),而每一次正常的「回合结束 → 下个回合开始」都满足这个形状 —— 同一台机器上 **86 条健康会话被误报为「下次请求会失败」**。现在必须**证明**「收尾块之后、下一个 `turn/start` 之前同一个 `turn` 还在继续」才报(回合已结束却还在续写);证明不了就不报(绝不猜:宁可漏报,也不把健康会话标红)。 > **Theme — session repair now happens in-app, but only for the provably lossless class.** > The Environments tab's session health check can now fix "replayed duplicate rows" (byte-identical, same-seq duplicates left by a crash, a hard kill or a second writer) in place: dry-run preview, explicit confirmation, a timestamped backup, an atomic swap and a post-write re-check, plus one-click rollback from the repair ledger. Five gates stay in force — the unit must resolve inside `/sessions`, no `session.lock`, the log must be quiet for 30 s, the preview's size+mtime fingerprint must still match, and the write goes through the mutation lock and SAFE MODE gate. Rollback accepts a ledger `repairId` only (the client can never supply a path). Every other corruption class stays read-only in-app and keeps pointing at the offline CLI. ### 🎨 插件图标对齐官方约定(issue #61 / PR #66)· Plugin icon - 🖼️ **`icon.svg` 换成 36×36 无底框**:宿主图位自带容器(卡片 48px 框内放 36px 图、列表行 40px 框内放 30px 图), 而旧图标是 512×512 画板 + 自带 `rx="112"` 圆角底框 —— 放进宿主就是**框中套框**,视觉也比官方插件重。 现在与官方约定一致(已发布官方组合包 `@deepseek-ai/dsh-experimental-schedule-bundle@0.2.0-rc.1` 随包的 `icon.svg` 实测同为 `width/height=36`、`viewBox="0 0 36 36"`、`fill="none"`):主体外接框 4→32 (28/36 ≈ 77.8%,与官方 fixture 的 `r=14` 同口径),纯矢量 1009 B,无 `