@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 +2 -0
- package/README.zh.md +2 -0
- package/lib/client.js +1 -1
- package/lib/index.js +38 -15
- package/package.json +7 -7
- package/rescue/rescue.mjs +25 -9
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
* 结构不合法(截断/未知魔数/保留位/越界块)抛错。
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|