@xiaoyuyu6420/dsh-backup 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,6 +131,8 @@ Tried it? Tell us what broke, what's missing, what you liked — it directly sha
131
131
  <details>
132
132
  <summary>Recent releases</summary>
133
133
 
134
+ - **0.11.1** — dsh `0.1.2-rc.1` compatibility: adapts to the removal of `settingsNamespace` from `@deepseek-ai/dsh-settings` (settings now register under the plain `dsh-backup` namespace — value-identical, so existing settings, backups and archives survive an in-place update), works behind the new forced web auth (303 + HttpOnly cookie), and widens peer ranges to `^0.1.1-rc.2 || ^0.1.2-rc.1`. Verified on both trains with real-host e2e (32/32 each) plus an in-place 0.11.0 → 0.11.1 upgrade test (settings preserved, old archives restorable).
135
+ - **0.11.0** — doctor container-contract check: the first zstd frame must decode to exactly one header line, byte-precise (non-empty, first newline at the last byte — aligned with the host reader). Single-frame rewrites, stray blank lines in the first frame, a missing trailing newline and skippable frames are now flagged corrupt (previously reported healthy while the host refused to load them); the rescue console checks in sync. Found via a community audit on deepseek-harness #1047.
134
136
  - **0.10.0** — typed backups: back up just what you need (`/backup --types skills,sessions`) and merge-restore a subset (`/backup restore <archive> --types skills`); per-type archives rotate in their own bucket. Credential-type archives stay out of GitHub sync; cross-machine guardrails unchanged.
135
137
  - **0.9.1** — feedback entry point in the panel; README overhaul. UX hardening from a six-agent review: restore-confirm button made visible again (missing theme fallback), snapshot self-deletion during snapshot-restore fixed, node discovery for the double-click rescue launcher, friendlier error messages with concrete next steps.
136
138
  - **0.9.0** — `/backup doctor` session-log health check with targeted repair; out-of-process rescue channel (`dsh-rescue` / launcher in the backup dir) that works even when the host won't boot; restore auto-rollback with a result-oriented receipt; smart backup: pre-upgrade snapshots on host train changes, quarantine of corrupt session logs before they rotate away, tiered daily/weekly retention.
package/README.zh.md CHANGED
@@ -131,6 +131,8 @@ dsh plugin --profile web add github:xiaoyuyu6420/dsh-backup
131
131
  <details>
132
132
  <summary>最近的版本</summary>
133
133
 
134
+ - **0.11.1** —— 适配 dsh `0.1.2-rc.1`:跟随 `@deepseek-ai/dsh-settings` 移除 `settingsNamespace`(改用普通 `dsh-backup` 命名空间注册——取值完全一致,原地更新后设置、备份与旧归档全部无损),支持 Web 强制鉴权(303 + HttpOnly cookie),peer 范围放宽为 `^0.1.1-rc.2 || ^0.1.2-rc.1`。双列车真宿主 e2e 各 32/32 通过,另做了 0.11.0 → 0.11.1 原地升级测试(设置保留、旧归档可恢复)。
135
+ - **0.11.0** —— doctor 容器契约校验:首帧必须解出「恰好一行 header、单个换行结尾」(字节精确,对齐宿主读端)。单帧重写、首帧多余空行、缺行尾、skippable 帧现在都会判损坏(此前报健康但宿主拒载);救援台同步。来自 deepseek-harness 官方讨论区 #1047 的社区审计。
134
136
  - **0.10.0** —— 分类型备份:只备份需要的部分(`/backup --types skills,sessions`),按类型 merge 恢复(`/backup restore <归档> --types skills`);类型归档独立轮换不挤全量配额。含凭据的类型归档永不进 GitHub 同步,跨机护栏不变。
135
137
  - **0.9.1** —— 面板新增反馈入口;README 重做。六路 agent UX 深审后的加固:恢复确认按钮补主题色兜底重新可见、修复恢复快照时自毁目标快照的 bug、双击救援启动器自动探测 node、报错文案全面改为"人话 + 下一步"。
136
138
  - **0.9.0** —— `/backup doctor` 会话日志体检与定点修复;宿主起不来也能用的进程外救援通道(备份目录里的 dsh-rescue / 双击启动器);恢复失败自动回滚与结果导向回执;智能备份:宿主升级前自动快照、损坏会话先隔离再入档、每日/每周分级保留。
package/lib/client.js CHANGED
@@ -450,5 +450,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
450
450
  color: var(--dsw-alias-state-error-primary, #e53e3e);
451
451
  line-height: 1.5;
452
452
  }
453
- `;var pm={name:"@xiaoyuyu6420/dsh-backup",version:"0.10.0",description:"Backup, restore, download and GitHub-sync DeepSeek Harness user data (~/.dsh): /backup, scheduled auto-backup that survives restarts, sha256 checksums, integrity verify, rotation, credential redaction with a local vault (plaintext never leaves the machine), cross-machine restore preflight with github pull, and a visual Settings panel. Cross-platform (macOS/Linux/Windows). \u4E00\u952E\u5907\u4EFD\u4E0E\u6062\u590D DSH \u6570\u636E\uFF1A\u5B9A\u65F6\u81EA\u52A8\u5907\u4EFD\u3001\u5B8C\u6574\u6027\u6821\u9A8C\u3001\u51ED\u636E\u9ED8\u8BA4\u8131\u654F\uFF08\u660E\u6587\u53EA\u5B58\u672C\u673A vault\uFF09\u3001\u8DE8\u673A\u6062\u590D\u9884\u68C0\u4E0E github pull \u62C9\u53D6\uFF0C\u9644 Settings \u53EF\u89C6\u9762\u677F\u3002",type:"module",packageManager:"pnpm@11.24.0",main:"lib/index.js",exports:{".":"./lib/index.js","./client":"./lib/client.js","./cordis.patch.yml":"./cordis.patch.yml","./package.json":"./package.json"},files:["lib","rescue","cordis.patch.yml","README.md","README.zh.md","LICENSE"],bin:{"dsh-rescue":"./rescue/rescue.mjs"},dsh:{bundle:{patch:"./cordis.patch.yml"},client:{platform:"web",inject:["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-settings"]}},keywords:["dsh","dsh-plugin","deepseek-harness","backup","\u5907\u4EFD","restore","\u6062\u590D","schedule","\u81EA\u52A8\u5907\u4EFD","cross-platform","rescue","doctor","upgrade-snapshot","\u6570\u636E\u5B89\u5168"],peerDependencies:{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-commands":"^0.1.1-rc.2","@deepseek-ai/dsh-fs":"^0.1.1-rc.2","@deepseek-ai/dsh-settings":"^0.1.1-rc.2","@deepseek-ai/dsh-subprocess":"^0.1.1-rc.2","@deepseek-ai/dsh-tools":"^0.1.1-rc.2","@deepseek-ai/dsh-typert-protocol":"^0.1.1-rc.2","@deepseek-ai/schemastery":"^3.18.1"},devDependencies:{esbuild:"^0.25.0",react:"^19.0.0","react-dom":"^19.0.0",zod:"^4.4.3"},engines:{node:">=20"},license:"MIT",repository:{type:"git",url:"git+https://github.com/xiaoyuyu6420/dsh-backup.git"},homepage:"https://github.com/xiaoyuyu6420/dsh-backup#readme",bugs:{url:"https://github.com/xiaoyuyu6420/dsh-backup/issues"},publishConfig:{access:"public"}};var Cn="settings.backupPanel",Ig=pm.name,jg=["slots","locale","remote"],Pg=y.object({destination:y.string(),dshHome:y.string(),keepDefault:y.number().int(),autoHours:y.number().int(),lastAuto:y.string().nullable(),backups:y.array(y.object({name:y.string(),size:y.number().int().nullable()})),typedBackups:y.array(y.object({name:y.string(),size:y.number().int().nullable(),types:y.array(y.string())})).optional()}),Ug=y.object({ok:y.boolean(),summary:y.string(),path:y.string(),sha:y.string(),stale:y.number().int(),keep:y.number().int(),types:y.array(y.string()).optional(),hasCredentials:y.boolean().optional()}),Dg=y.object({ok:y.boolean(),summary:y.string(),results:y.array(y.object({name:y.string(),ok:y.boolean(),note:y.string()}))}),Og=y.object({ok:y.boolean(),dryRun:y.boolean(),summary:y.string(),archive:y.string().nullable().optional(),files:y.number().int().nullable().optional(),aside:y.string().nullable().optional(),snapshotPath:y.string().nullable().optional(),sample:y.array(y.string()).optional(),preflight:y.array(y.string()).optional(),targetExists:y.boolean().nullable().optional(),merge:y.boolean().optional(),types:y.array(y.string()).optional(),willOverwrite:y.array(y.string()).optional(),restored:y.number().int().optional(),kept:y.array(y.string()).optional()}),Ng=y.object({ok:y.boolean(),hours:y.number().int(),summary:y.string()}),Zg=y.object({repoRaw:y.string().nullable(),repo:y.string().nullable(),tokenSet:y.boolean(),syncDir:y.string(),lastPush:y.string().nullable(),lastError:y.string().nullable()}),Tg=y.object({ok:y.boolean(),summary:y.string(),pushed:y.boolean(),tooBig:y.array(y.string())}),Eg=y.object({ok:y.boolean(),summary:y.string(),pulled:y.array(y.string()),corrupt:y.array(y.string()),total:y.number().int()}),Ag=y.object({ok:y.boolean(),summary:y.string()}),Lg=y.object({ok:y.boolean(),repo:y.string().nullable(),summary:y.string()}),Rg={name:"keep",wire:"keep",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#keep",schema:y.number().int().positive().optional()},acceptsUndefined:!0},Hu={name:"selector",wire:"selector",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#selector",schema:y.string().optional()},acceptsUndefined:!0},Cg={name:"dryRun",wire:"dryRun",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#dryRun",schema:y.boolean().optional()},acceptsUndefined:!0},Fg={name:"syncDeps",wire:"syncDeps",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#syncDeps",schema:y.boolean().optional()},acceptsUndefined:!0},Jg={name:"hours",wire:"hours",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#hours",schema:y.number().int().min(0).max(720)},acceptsUndefined:!0},Vg={name:"repo",wire:"repo",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#repo",schema:y.string().optional()},acceptsUndefined:!0},fm={name:"types",wire:"types",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#types",schema:y.array(y.string()).optional()},acceptsUndefined:!0};function de(t,r,i,o){return Object.freeze({id:`dsh-backup#backupPanel/${t}`,service:"backupPanel",namespace:"backupPanel",method:t,invocation:Object.freeze({kind:"direct"}),parameters:Object.freeze(r.map(e=>Object.freeze({...e,codec:Object.freeze(e.codec)}))),...o?{cancellation:Object.freeze({parameter:"signal"})}:{},result:Object.freeze({mode:"strict",typeSymbol:`dsh-backup/types#${t}Result`,schema:i})})}var gm=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([de("status",[],Pg,!1),de("backup",[Rg,fm],Ug,!0),de("verify",[Hu],Dg,!0),de("restore",[Hu,Cg,fm,Fg],Og,!0),de("setAuto",[Jg],Ng,!1),de("githubStatus",[],Zg,!1),de("githubSyncNow",[],Tg,!0),de("githubPull",[],Eg,!0),de("removeEntry",[Hu],Ag,!0),de("setGithubRepo",[Vg],Lg,!1)])});function me(t){if(!t.ok){let r=t.error,i=r&&r.message?`${r.code}: ${r.message}`:"backupPanel \u8C03\u7528\u5931\u8D25";throw new Error(`\u64CD\u4F5C\u6CA1\u6709\u5B8C\u6210\uFF1A\u9762\u677F\u6682\u65F6\u8FDE\u4E0D\u4E0A\u540E\u53F0\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u82E5\u53CD\u590D\u51FA\u73B0\u8BF7\u91CD\u542F dsh web\uFF08\u6280\u672F\u7EC6\u8282: ${i}\uFF09`)}return t.value}function Mg(t){t.effect(()=>t.locale.register(Cn,{zh:lm,en:dm}),"dsh-backup: dictionaries"),t.effect(()=>mm(),"dsh-backup: stylesheet"),t.effect(()=>{let r=null,i=!0,o=!1;return(async()=>{try{r=await t.remote.$mount(gm)}catch(e){console.error("dsh-backup: backupPanel mount failed:",e)}i=!1,o&&r?.()})(),()=>{o=!0,i||r?.()}},"dsh-backup: remote contribution"),t.inject(["remote.backupPanel"],r=>{let i=r.locale.bind(Cn),o=()=>r.remote.backupPanel,e={status:async()=>me(await o().status()),backup:async(n,a)=>me(await o().backup(n,a)),verify:async n=>me(await o().verify(n)),restore:async(n,a,c,s)=>me(await o().restore(n,a,c,s)),setAuto:async n=>me(await o().setAuto(n)),githubStatus:async()=>me(await o().githubStatus()),githubSyncNow:async()=>me(await o().githubSyncNow()),githubPull:async()=>me(await o().githubPull()),removeEntry:async n=>me(await o().removeEntry(n)),setGithubRepo:async n=>me(await o().setGithubRepo(n))};r.slots.inject("settings.plugins.tab",()=>r.slots.register({name:"settings.plugins.tab",id:"backup",order:35,label:()=>i("tab"),locale:Cn,inject:()=>({panel:e})},sm))})}
453
+ `;var pm={name:"@xiaoyuyu6420/dsh-backup",version:"0.11.1",description:"Backup, restore, download and GitHub-sync DeepSeek Harness user data (~/.dsh): /backup, scheduled auto-backup that survives restarts, sha256 checksums, integrity verify, rotation, credential redaction with a local vault (plaintext never leaves the machine), cross-machine restore preflight with github pull, and a visual Settings panel. Cross-platform (macOS/Linux/Windows). \u4E00\u952E\u5907\u4EFD\u4E0E\u6062\u590D DSH \u6570\u636E\uFF1A\u5B9A\u65F6\u81EA\u52A8\u5907\u4EFD\u3001\u5B8C\u6574\u6027\u6821\u9A8C\u3001\u51ED\u636E\u9ED8\u8BA4\u8131\u654F\uFF08\u660E\u6587\u53EA\u5B58\u672C\u673A vault\uFF09\u3001\u8DE8\u673A\u6062\u590D\u9884\u68C0\u4E0E github pull \u62C9\u53D6\uFF0C\u9644 Settings \u53EF\u89C6\u9762\u677F\u3002",type:"module",packageManager:"pnpm@11.24.0",main:"lib/index.js",exports:{".":"./lib/index.js","./client":"./lib/client.js","./cordis.patch.yml":"./cordis.patch.yml","./package.json":"./package.json"},files:["lib","rescue","cordis.patch.yml","README.md","README.zh.md","LICENSE"],bin:{"dsh-rescue":"./rescue/rescue.mjs"},dsh:{bundle:{patch:"./cordis.patch.yml"},client:{platform:"web",inject:["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-locale","@deepseek-ai/dsh-client-ui-settings"]}},keywords:["dsh","dsh-plugin","deepseek-harness","backup","\u5907\u4EFD","restore","\u6062\u590D","schedule","\u81EA\u52A8\u5907\u4EFD","cross-platform","rescue","doctor","upgrade-snapshot","\u6570\u636E\u5B89\u5168"],peerDependencies:{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-commands":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/dsh-fs":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/dsh-settings":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/dsh-subprocess":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/dsh-tools":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/dsh-typert-protocol":"^0.1.1-rc.2 || ^0.1.2-rc.1","@deepseek-ai/schemastery":"^3.18.1"},devDependencies:{esbuild:"^0.25.0",react:"^19.0.0","react-dom":"^19.0.0",zod:"^4.4.3"},engines:{node:">=20"},license:"MIT",repository:{type:"git",url:"git+https://github.com/xiaoyuyu6420/dsh-backup.git"},homepage:"https://github.com/xiaoyuyu6420/dsh-backup#readme",bugs:{url:"https://github.com/xiaoyuyu6420/dsh-backup/issues"},publishConfig:{access:"public"}};var Cn="settings.backupPanel",Ig=pm.name,jg=["slots","locale","remote"],Pg=y.object({destination:y.string(),dshHome:y.string(),keepDefault:y.number().int(),autoHours:y.number().int(),lastAuto:y.string().nullable(),backups:y.array(y.object({name:y.string(),size:y.number().int().nullable()})),typedBackups:y.array(y.object({name:y.string(),size:y.number().int().nullable(),types:y.array(y.string())})).optional()}),Ug=y.object({ok:y.boolean(),summary:y.string(),path:y.string(),sha:y.string(),stale:y.number().int(),keep:y.number().int(),types:y.array(y.string()).optional(),hasCredentials:y.boolean().optional()}),Dg=y.object({ok:y.boolean(),summary:y.string(),results:y.array(y.object({name:y.string(),ok:y.boolean(),note:y.string()}))}),Og=y.object({ok:y.boolean(),dryRun:y.boolean(),summary:y.string(),archive:y.string().nullable().optional(),files:y.number().int().nullable().optional(),aside:y.string().nullable().optional(),snapshotPath:y.string().nullable().optional(),sample:y.array(y.string()).optional(),preflight:y.array(y.string()).optional(),targetExists:y.boolean().nullable().optional(),merge:y.boolean().optional(),types:y.array(y.string()).optional(),willOverwrite:y.array(y.string()).optional(),restored:y.number().int().optional(),kept:y.array(y.string()).optional()}),Ng=y.object({ok:y.boolean(),hours:y.number().int(),summary:y.string()}),Zg=y.object({repoRaw:y.string().nullable(),repo:y.string().nullable(),tokenSet:y.boolean(),syncDir:y.string(),lastPush:y.string().nullable(),lastError:y.string().nullable()}),Tg=y.object({ok:y.boolean(),summary:y.string(),pushed:y.boolean(),tooBig:y.array(y.string())}),Eg=y.object({ok:y.boolean(),summary:y.string(),pulled:y.array(y.string()),corrupt:y.array(y.string()),total:y.number().int()}),Ag=y.object({ok:y.boolean(),summary:y.string()}),Lg=y.object({ok:y.boolean(),repo:y.string().nullable(),summary:y.string()}),Rg={name:"keep",wire:"keep",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#keep",schema:y.number().int().positive().optional()},acceptsUndefined:!0},Hu={name:"selector",wire:"selector",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#selector",schema:y.string().optional()},acceptsUndefined:!0},Cg={name:"dryRun",wire:"dryRun",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#dryRun",schema:y.boolean().optional()},acceptsUndefined:!0},Fg={name:"syncDeps",wire:"syncDeps",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#syncDeps",schema:y.boolean().optional()},acceptsUndefined:!0},Jg={name:"hours",wire:"hours",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#hours",schema:y.number().int().min(0).max(720)},acceptsUndefined:!0},Vg={name:"repo",wire:"repo",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#repo",schema:y.string().optional()},acceptsUndefined:!0},fm={name:"types",wire:"types",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#types",schema:y.array(y.string()).optional()},acceptsUndefined:!0};function de(t,r,i,o){return Object.freeze({id:`dsh-backup#backupPanel/${t}`,service:"backupPanel",namespace:"backupPanel",method:t,invocation:Object.freeze({kind:"direct"}),parameters:Object.freeze(r.map(e=>Object.freeze({...e,codec:Object.freeze(e.codec)}))),...o?{cancellation:Object.freeze({parameter:"signal"})}:{},result:Object.freeze({mode:"strict",typeSymbol:`dsh-backup/types#${t}Result`,schema:i})})}var gm=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([de("status",[],Pg,!1),de("backup",[Rg,fm],Ug,!0),de("verify",[Hu],Dg,!0),de("restore",[Hu,Cg,fm,Fg],Og,!0),de("setAuto",[Jg],Ng,!1),de("githubStatus",[],Zg,!1),de("githubSyncNow",[],Tg,!0),de("githubPull",[],Eg,!0),de("removeEntry",[Hu],Ag,!0),de("setGithubRepo",[Vg],Lg,!1)])});function me(t){if(!t.ok){let r=t.error,i=r&&r.message?`${r.code}: ${r.message}`:"backupPanel \u8C03\u7528\u5931\u8D25";throw new Error(`\u64CD\u4F5C\u6CA1\u6709\u5B8C\u6210\uFF1A\u9762\u677F\u6682\u65F6\u8FDE\u4E0D\u4E0A\u540E\u53F0\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u82E5\u53CD\u590D\u51FA\u73B0\u8BF7\u91CD\u542F dsh web\uFF08\u6280\u672F\u7EC6\u8282: ${i}\uFF09`)}return t.value}function Mg(t){t.effect(()=>t.locale.register(Cn,{zh:lm,en:dm}),"dsh-backup: dictionaries"),t.effect(()=>mm(),"dsh-backup: stylesheet"),t.effect(()=>{let r=null,i=!0,o=!1;return(async()=>{try{r=await t.remote.$mount(gm)}catch(e){console.error("dsh-backup: backupPanel mount failed:",e)}i=!1,o&&r?.()})(),()=>{o=!0,i||r?.()}},"dsh-backup: remote contribution"),t.inject(["remote.backupPanel"],r=>{let i=r.locale.bind(Cn),o=()=>r.remote.backupPanel,e={status:async()=>me(await o().status()),backup:async(n,a)=>me(await o().backup(n,a)),verify:async n=>me(await o().verify(n)),restore:async(n,a,c,s)=>me(await o().restore(n,a,c,s)),setAuto:async n=>me(await o().setAuto(n)),githubStatus:async()=>me(await o().githubStatus()),githubSyncNow:async()=>me(await o().githubSyncNow()),githubPull:async()=>me(await o().githubPull()),removeEntry:async n=>me(await o().removeEntry(n)),setGithubRepo:async n=>me(await o().setGithubRepo(n))};r.slots.inject("settings.plugins.tab",()=>r.slots.register({name:"settings.plugins.tab",id:"backup",order:35,label:()=>i("tab"),locale:Cn,inject:()=>({panel:e})},sm))})}
454
454
  return module.exports; } });
package/lib/index.js CHANGED
@@ -51,7 +51,7 @@ import { fileURLToPath } from 'node:url';
51
51
  import * as zlib from 'node:zlib';
52
52
  import { defineTool } from '@deepseek-ai/dsh-tools';
53
53
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
54
- import { settingsNamespace, SettingsConflictError } from '@deepseek-ai/dsh-settings';
54
+ import { SettingsConflictError } from '@deepseek-ai/dsh-settings';
55
55
  import z from '@deepseek-ai/schemastery';
56
56
 
57
57
  export const name = 'dsh-backup';
@@ -266,8 +266,11 @@ function detectHostTrain() {
266
266
  return null;
267
267
  }
268
268
 
269
- /** Settings 命名空间:经 `ctx.settings` 提供(持久化到 $DSH_HOME/settings.yaml)。 */
270
- const NS = settingsNamespace('dsh-backup');
269
+ /** Settings 命名空间:经 `ctx.settings` 提供(持久化到 $DSH_HOME/settings.yaml)。
270
+ * 0.1.2-rc.1 起宿主移除了 settingsNamespace 帮助函数——它运行时本就只返回
271
+ * 这个字符串(纯 TS 类型校验),ctx.settings.register/update/replace/describe
272
+ * 吃的都是同一语法的字符串,直接写常量在两个列车上行为等价。 */
273
+ const NS = 'dsh-backup';
271
274
 
272
275
  /**
273
276
  * 设置默认值。SCHEMA 的 .default() 与 resolveBase 的初始值都引用这里,
@@ -1828,9 +1831,12 @@ export function apply(ctx, pluginConfig) {
1828
1831
  // 布局与格式依据宿主内置的 @deepseek-ai/dsh-session-persistence-jsonl:
1829
1832
  // <会话根>/--<项目目录>--/<编码后 id>/session.jsonl.zstd(compression:'none' 时为 .jsonl)
1830
1833
  // zstd 文件是独立帧的拼接:首帧是带校验和的 SessionHeader 行,其后每个追加批一帧。
1834
+ // 两条硬契约都对齐宿主读端:①行级——首行 SessionHeader、seq 连续;②容器级——
1835
+ // 首帧解出来恰好一行 header(宿主 assertZstdHeaderFrame;外部工具把整份日志
1836
+ // 压成单帧时内容完整可解、宿主却拒载,行级校验覆盖不到,必须单独判)。
1831
1837
  // Node 内置 zstd 解码器不跨帧续读,因此先按 RFC 8878 走帧字节边界
1832
1838
  // (魔数/帧头/块序列/校验和),再逐帧解出逻辑行。检测目标正是社区高频的
1833
- // "corrupt session log"灾难:并发写 seq 撞号、坏帧、尾部截断。
1839
+ // "corrupt session log"灾难:并发写 seq 撞号、坏帧、尾部截断、单帧重写。
1834
1840
  const ZSTD_MAGIC = 0xfd2fb528;
1835
1841
  const SESSION_LOG_NAMES = new Set(['session.jsonl.zstd', 'session.jsonl']);
1836
1842
  const SCAN_SKIP_DIRS = new Set(['node_modules', '.system', '.git', 'vault']);
@@ -1848,7 +1854,9 @@ export function apply(ctx, pluginConfig) {
1848
1854
 
1849
1855
  /**
1850
1856
  * 按 RFC 8878 结构走查拼接帧的字节边界(不解压):返回 [start,end] 区间数组;
1851
- * 结构不合法(截断/未知魔数/保留位/越界块)抛错。skippable 帧一并支持。
1857
+ * 结构不合法(截断/未知魔数/保留位/越界块)抛错。
1858
+ * skippable 帧直接判非法——宿主 scanZstdFrames 拒绝一切非数据帧魔数,
1859
+ * 这类文件宿主整体拒载,容忍它们会造成「doctor 报 OK、宿主编译系统读不了」。
1852
1860
  */
1853
1861
  function walkZstdFrames(buf) {
1854
1862
  const frames = [];
@@ -1887,12 +1895,7 @@ export function apply(ctx, pluginConfig) {
1887
1895
  frames.push([off, pos]);
1888
1896
  off = pos;
1889
1897
  } else if (magic >= 0x184d2a50 && magic <= 0x184d2a5f) {
1890
- if (buf.length - off < 8) throw new Error('skippable 帧头截断');
1891
- const size = buf.readUInt32LE(off + 4);
1892
- const end = off + 8 + size;
1893
- if (end > buf.length) throw new Error('skippable 帧数据越界');
1894
- frames.push([off, end]);
1895
- off = end;
1898
+ throw new Error('skippable 帧——宿主读端不支持任何非数据帧魔数,整个文件将被拒载');
1896
1899
  } else {
1897
1900
  throw new Error(`偏移 ${off} 处不是 zstd 帧魔数`);
1898
1901
  }
@@ -1900,19 +1903,32 @@ export function apply(ctx, pluginConfig) {
1900
1903
  return frames;
1901
1904
  }
1902
1905
 
1903
- /** 解出 .zstd 会话日志的全部逻辑行;任一帧损坏即抛错(错误含偏移信息)。 */
1906
+ /**
1907
+ * 解出 .zstd 会话日志的全部逻辑行;任一帧损坏即抛错(错误含偏移信息)。
1908
+ * 同时对首个数据帧做宿主容器契约判定 headerFrameOk——宿主 assertZstdHeaderFrame
1909
+ * 是字节精确的:首帧解出必须非空、且首个换行符恰在最后一个字节(即「恰好
1910
+ * 一行、以单个 \n 结尾」)。行流拼接校验覆盖不到这条(#66)。
1911
+ */
1904
1912
  function decodeZstdLog(buf) {
1905
1913
  const parts = [];
1906
1914
  let frameNo = 0;
1915
+ let headerFrameOk;
1907
1916
  for (const [s, e] of walkZstdFrames(buf)) {
1908
1917
  frameNo += 1;
1909
1918
  try {
1910
- parts.push(zlib.zstdDecompressSync(buf.subarray(s, e)));
1919
+ const out = zlib.zstdDecompressSync(buf.subarray(s, e));
1920
+ parts.push(out);
1921
+ if (headerFrameOk === undefined) {
1922
+ headerFrameOk = out.length > 0 && out.indexOf(10) === out.length - 1;
1923
+ }
1911
1924
  } catch (err) {
1912
1925
  throw new Error(`第 ${frameNo} 帧解压失败(${e - s}B):${String(err && err.message ? err.message : err)}`);
1913
1926
  }
1914
1927
  }
1915
- return Buffer.concat(parts).toString('utf8').split('\n').filter((l) => l.length > 0);
1928
+ return {
1929
+ lines: Buffer.concat(parts).toString('utf8').split('\n').filter((l) => l.length > 0),
1930
+ headerFrameOk,
1931
+ };
1916
1932
  }
1917
1933
 
1918
1934
  /**
@@ -1993,8 +2009,15 @@ export function apply(ctx, pluginConfig) {
1993
2009
  return { state: 'skipped', reason: `文件 ${Math.floor(info.size / 1048576)}MB 超过深度校验上限(${Math.floor(HASH_MAX_BYTES / 1048576)}MB),不整读入内存` };
1994
2010
  }
1995
2011
  let lines;
2012
+ let headerFrameOk;
1996
2013
  if (f.abs.endsWith('.zstd')) {
1997
- lines = decodeZstdLog(await readFile(f.abs));
2014
+ ({ lines, headerFrameOk } = decodeZstdLog(await readFile(f.abs)));
2015
+ // 容器契约(#66/#1047):首帧必须解出「恰好一行 header、以单个换行
2016
+ // 结尾」(字节精确,对齐宿主 assertZstdHeaderFrame)。单帧重写、帧内
2017
+ // 多余空行、缺行尾的文件内容完整可解、行级校验全过,宿主却整体拒载。
2018
+ if (headerFrameOk === false) {
2019
+ return { state: 'bad', reason: '首帧不是恰好一行 header——违反容器契约(首帧应解出一行 SessionHeader 且以单个换行结尾;单帧重写/多余空行/缺行尾的典型形态,宿主读端会拒载)' };
2020
+ }
1998
2021
  } else {
1999
2022
  lines = (await readFile(f.abs, 'utf8')).split('\n').filter((l) => l.length > 0);
2000
2023
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaoyuyu6420/dsh-backup",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Backup, restore, download and GitHub-sync DeepSeek Harness user data (~/.dsh): /backup, scheduled auto-backup that survives restarts, sha256 checksums, integrity verify, rotation, credential redaction with a local vault (plaintext never leaves the machine), cross-machine restore preflight with github pull, and a visual Settings panel. Cross-platform (macOS/Linux/Windows). 一键备份与恢复 DSH 数据:定时自动备份、完整性校验、凭据默认脱敏(明文只存本机 vault)、跨机恢复预检与 github pull 拉取,附 Settings 可视面板。",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.24.0",
@@ -53,12 +53,12 @@
53
53
  ],
54
54
  "peerDependencies": {
55
55
  "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
56
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1",
57
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1",
58
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1",
59
+ "@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1",
60
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1",
61
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1",
62
62
  "@deepseek-ai/schemastery": "^3.18.1"
63
63
  },
64
64
  "devDependencies": {
package/rescue/rescue.mjs CHANGED
@@ -238,12 +238,9 @@ function walkZstdFrames(buf) {
238
238
  frames.push([off, pos]);
239
239
  off = pos;
240
240
  } else if (magic >= 0x184d2a50 && magic <= 0x184d2a5f) {
241
- if (buf.length - off < 8) throw new Error('skippable 帧头截断');
242
- const size = buf.readUInt32LE(off + 4);
243
- const end = off + 8 + size;
244
- if (end > buf.length) throw new Error('skippable 帧数据越界');
245
- frames.push([off, end]);
246
- off = end;
241
+ // 宿主 scanZstdFrames 拒绝一切非数据帧魔数——容忍 skippable 会造成
242
+ // 「doctor OK、宿主整体拒载」(#66,与 lib/index.js 同步)
243
+ throw new Error('遇 skippable 帧——宿主读端不支持任何非数据帧魔数,整个文件将被拒载');
247
244
  } else {
248
245
  throw new Error(`偏移 ${off} 处不是 zstd 帧魔数`);
249
246
  }
@@ -251,18 +248,32 @@ function walkZstdFrames(buf) {
251
248
  return frames;
252
249
  }
253
250
 
251
+ /**
252
+ * 解出 .zstd 会话日志的全部逻辑行;任一帧损坏即抛错。同时对首个数据帧做
253
+ * 宿主容器契约判定 headerFrameOk——宿主 assertZstdHeaderFrame 是字节精确的:
254
+ * 首帧解出必须非空、且首个换行符恰在最后一个字节(「恰好一行、单个 \n 结尾」)。
255
+ * 行流拼接校验覆盖不到这条(#66,与 lib/index.js 同步)。
256
+ */
254
257
  function decodeZstdLog(buf) {
255
258
  const parts = [];
256
259
  let frameNo = 0;
260
+ let headerFrameOk;
257
261
  for (const [s, e] of walkZstdFrames(buf)) {
258
262
  frameNo += 1;
259
263
  try {
260
- parts.push(zlib.zstdDecompressSync(buf.subarray(s, e)));
264
+ const out = zlib.zstdDecompressSync(buf.subarray(s, e));
265
+ parts.push(out);
266
+ if (headerFrameOk === undefined) {
267
+ headerFrameOk = out.length > 0 && out.indexOf(10) === out.length - 1;
268
+ }
261
269
  } catch (err) {
262
270
  throw new Error(`第 ${frameNo} 帧解压失败:${String(err && err.message ? err.message : err)}`);
263
271
  }
264
272
  }
265
- return Buffer.concat(parts).toString('utf8').split('\n').filter((l) => l.length > 0);
273
+ return {
274
+ lines: Buffer.concat(parts).toString('utf8').split('\n').filter((l) => l.length > 0),
275
+ headerFrameOk,
276
+ };
266
277
  }
267
278
 
268
279
  function validateSessionLines(lines) {
@@ -329,8 +340,13 @@ async function validateSessionFile(f) {
329
340
  return { state: 'skipped', reason: `文件 ${Math.floor(info.size / 1048576)}MB 超过深度校验上限` };
330
341
  }
331
342
  let lines;
343
+ let headerFrameOk;
332
344
  if (f.abs.endsWith('.zstd')) {
333
- lines = decodeZstdLog(await fs.readFile(f.abs));
345
+ ({ lines, headerFrameOk } = decodeZstdLog(await fs.readFile(f.abs)));
346
+ // 容器契约(#66/#1047):首帧必须解出「恰好一行 header、单个换行结尾」
347
+ if (headerFrameOk === false) {
348
+ return { state: 'bad', reason: '首帧不是恰好一行 header——违反容器契约(首帧应解出一行 SessionHeader 且以单个换行结尾;单帧重写/多余空行/缺行尾的典型形态,宿主读端会拒载)' };
349
+ }
334
350
  } else {
335
351
  lines = (await fs.readFile(f.abs, 'utf8')).split('\n').filter((l) => l.length > 0);
336
352
  }