@xiaoyuyu6420/dsh-backup 0.6.1 → 0.6.2
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/lib/client.js +1 -1
- package/lib/index.js +70 -14
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -330,5 +330,5 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
330
330
|
max-height: 10em;
|
|
331
331
|
overflow: auto;
|
|
332
332
|
}
|
|
333
|
-
`;var Ld={name:"@xiaoyuyu6420/dsh-backup",version:"0.6.
|
|
333
|
+
`;var Ld={name:"@xiaoyuyu6420/dsh-backup",version:"0.6.2",description:"Backup, restore, download and GitHub-sync DeepSeek Harness user data (~/.dsh): /backup, scheduled auto-backup that survives restarts, sha256 checksums, integrity verify, rotation 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\u4E0B\u8F7D\u4E0E GitHub \u540C\u6B65\uFF0C\u9644 Settings \u53EF\u89C6\u9762\u677F\u3002",type:"module",main:"lib/index.js",exports:{".":"./lib/index.js","./client":"./lib/client.js","./cordis.patch.yml":"./cordis.patch.yml","./package.json":"./package.json"},files:["lib","cordis.patch.yml","README.md","README.zh.md","LICENSE"],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"],peerDependencies:{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-commands":"^0.1.0-rc.6","@deepseek-ai/dsh-fs":"^0.1.0-rc.6","@deepseek-ai/dsh-subprocess":"^0.1.0-rc.6","@deepseek-ai/dsh-tools":"^0.1.0-rc.6","@deepseek-ai/dsh-typert-protocol":"^0.1.0-rc.6"},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 jn="settings.backupPanel",Wf=Ld.name,Xf=["slots","locale","remote"],qf=x.object({destination:x.string(),dshHome:x.string(),keepDefault:x.number().int(),autoHours:x.number().int(),lastAuto:x.string().nullable(),backups:x.array(x.object({name:x.string(),size:x.number().int().nullable()}))}),Hf=x.object({ok:x.boolean(),summary:x.string(),path:x.string(),sha:x.string(),stale:x.number().int(),keep:x.number().int()}),Yf=x.object({ok:x.boolean(),summary:x.string(),results:x.array(x.object({name:x.string(),ok:x.boolean(),note:x.string()}))}),Qf=x.object({ok:x.boolean(),dryRun:x.boolean(),summary:x.string(),archive:x.string().nullable().optional(),files:x.number().int().nullable().optional(),aside:x.string().nullable().optional(),snapshotPath:x.string().nullable().optional(),sample:x.array(x.string()).optional()}),eg=x.object({ok:x.boolean(),hours:x.number().int(),summary:x.string()}),tg=x.object({repoRaw:x.string().nullable(),repo:x.string().nullable(),tokenSet:x.boolean(),syncDir:x.string(),lastPush:x.string().nullable(),lastError:x.string().nullable()}),rg=x.object({ok:x.boolean(),summary:x.string(),pushed:x.boolean(),tooBig:x.array(x.string())}),ng=x.object({ok:x.boolean(),summary:x.string()}),ig=x.object({ok:x.boolean(),repo:x.string().nullable(),summary:x.string()}),og={name:"keep",wire:"keep",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#keep",schema:x.number().int().positive().optional()},acceptsUndefined:!0},Nu={name:"selector",wire:"selector",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#selector",schema:x.string().optional()},acceptsUndefined:!0},ag={name:"dryRun",wire:"dryRun",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#dryRun",schema:x.boolean().optional()},acceptsUndefined:!0},cg={name:"hours",wire:"hours",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#hours",schema:x.number().int().min(0).max(720)},acceptsUndefined:!0},ug={name:"repo",wire:"repo",source:"json",codec:{mode:"strict",typeSymbol:"dsh-backup/types#repo",schema:x.string().optional()},acceptsUndefined:!0};function le(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 Cd=Object.freeze({package:"dsh-backup",descriptors:Object.freeze([le("status",[],qf,!1),le("backup",[og],Hf,!0),le("verify",[Nu],Yf,!0),le("restore",[Nu,ag],Qf,!0),le("setAuto",[cg],eg,!1),le("githubStatus",[],tg,!1),le("githubSyncNow",[],rg,!0),le("removeEntry",[Nu],ng,!0),le("setGithubRepo",[ug],ig,!1)])});function de(t){if(!t.ok){let r=t.error;throw new Error(r&&r.message?`${r.code}: ${r.message}`:"backupPanel \u8C03\u7528\u5931\u8D25")}return t.value}function sg(t){t.effect(()=>t.locale.register(jn,{zh:Ed,en:Ad}),"dsh-backup: dictionaries"),t.effect(()=>Rd(),"dsh-backup: stylesheet"),t.effect(()=>{let r=null,i=!0,o=!1;return(async()=>{try{r=await t.remote.$mount(Cd)}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(jn),o=()=>r.remote.backupPanel,e={status:async()=>de(await o().status()),backup:async n=>de(await o().backup(n)),verify:async n=>de(await o().verify(n)),restore:async(n,a)=>de(await o().restore(n,a)),setAuto:async n=>de(await o().setAuto(n)),githubStatus:async()=>de(await o().githubStatus()),githubSyncNow:async()=>de(await o().githubSyncNow()),removeEntry:async n=>de(await o().removeEntry(n)),setGithubRepo:async n=>de(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:jn,inject:()=>({panel:e})},Td))})}
|
|
334
334
|
return module.exports; } });
|
package/lib/index.js
CHANGED
|
@@ -230,6 +230,31 @@ export function apply(ctx, pluginConfig) {
|
|
|
230
230
|
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}${pad(now.getMilliseconds(), 3)}`;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* 从 http(s) URL 中剥离 userinfo(`user:pass@` 段,含 token-as-username
|
|
235
|
+
* 形态)。GitHub 同步的认证始终走环境变量 token → 同步工作树的
|
|
236
|
+
* .git-credentials,不依赖 URL 内嵌凭据;但用户可能把 PAT 写进 repo
|
|
237
|
+
* URL,此时 token 会明文落入 auto.json 并被 `/backup github status` 与
|
|
238
|
+
* 面板 repoRaw 回显。此处在存/显前剥离。
|
|
239
|
+
* 仅作用于 http(s)——ssh://git@、file://、owner/repo、本地路径等保持原样
|
|
240
|
+
* (不同认证模型,剥 username 会破坏如 ssh://git@host)。用 URL 解析处理
|
|
241
|
+
* port/IPv6/path 含已编码 @ 等;非 URL / 无 userinfo / 解析异常原样返回。
|
|
242
|
+
*/
|
|
243
|
+
function stripUserinfo(raw) {
|
|
244
|
+
if (typeof raw !== 'string' || !/^https?:\/\//i.test(raw)) return raw;
|
|
245
|
+
try {
|
|
246
|
+
const u = new URL(raw);
|
|
247
|
+
if (u.username || u.password) {
|
|
248
|
+
u.username = '';
|
|
249
|
+
u.password = '';
|
|
250
|
+
return u.toString();
|
|
251
|
+
}
|
|
252
|
+
return raw;
|
|
253
|
+
} catch {
|
|
254
|
+
return raw;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
233
258
|
/**
|
|
234
259
|
* 删除 dir 下的文件。纯 Node fs.unlink(零 shell):文件名来自备份目录
|
|
235
260
|
* 内容,拼接进 cmd/rm 命令行会构成命令注入面(Windows 下 cmd 的
|
|
@@ -251,6 +276,25 @@ export function apply(ctx, pluginConfig) {
|
|
|
251
276
|
await rename(`${dir}/${srcName}`, `${dir}/${dstName}`);
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
/**
|
|
280
|
+
* 清理旧的恢复前快照(dsh-pre-restore-* 归档 + 边车),仅保留 keepName
|
|
281
|
+
* 这一份。恢复是低频操作,快照又被 listBackups 排除、用户不可见,故每
|
|
282
|
+
* 次恢复后清掉前一次的快照防隐藏累积(aside 目录仍由用户自行管理)。
|
|
283
|
+
*/
|
|
284
|
+
async function prunePreRestoreSnapshots(keepName, signal) {
|
|
285
|
+
const { root } = paths();
|
|
286
|
+
let dirents;
|
|
287
|
+
try {
|
|
288
|
+
dirents = await readdir(root, { withFileTypes: true });
|
|
289
|
+
} catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const stale = dirents
|
|
293
|
+
.filter((d) => d.name.startsWith('dsh-pre-restore-') && (d.name.endsWith('.tar.gz') || d.name.endsWith('.sha256')) && d.name !== keepName && d.name !== `${keepName}.sha256`)
|
|
294
|
+
.map((d) => d.name);
|
|
295
|
+
if (stale.length) await removeFiles(stale, root, signal);
|
|
296
|
+
}
|
|
297
|
+
|
|
254
298
|
async function listBackups() {
|
|
255
299
|
const { root } = paths();
|
|
256
300
|
let dirents;
|
|
@@ -262,7 +306,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
262
306
|
}
|
|
263
307
|
const backups = [];
|
|
264
308
|
for (const d of dirents) {
|
|
265
|
-
|
|
309
|
+
// 恢复前自动快照(dsh-pre-restore-*)是内部安全网,不进用户列表、
|
|
310
|
+
// 不参与轮换、不被 pickArchive 的 latest/前缀误选——经 listBackups
|
|
311
|
+
// 过滤即可同时收口这三处入口。
|
|
312
|
+
if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || !d.name.endsWith('.tar.gz')) continue;
|
|
266
313
|
let size;
|
|
267
314
|
try {
|
|
268
315
|
size = (await fsStat(`${root}/${d.name}`)).size;
|
|
@@ -302,13 +349,14 @@ export function apply(ctx, pluginConfig) {
|
|
|
302
349
|
return createHash('sha256').update(bytes).digest('hex');
|
|
303
350
|
}
|
|
304
351
|
|
|
305
|
-
async function doBackup(keep, signal) {
|
|
352
|
+
async function doBackup(keep, signal, opts = {}) {
|
|
306
353
|
const { home, dshHome, root } = paths();
|
|
307
354
|
const keepN = keep && keep > 0 ? Math.floor(keep) : defaultKeep();
|
|
308
355
|
// 状态文件先写:writeText 会自动创建备份目录,替代 mkdir -p(Windows 无 mkdir.exe)。
|
|
309
356
|
await saveAutoState();
|
|
310
357
|
|
|
311
|
-
const
|
|
358
|
+
const prefix = typeof opts?.namePrefix === 'string' && opts.namePrefix ? opts.namePrefix : 'dsh-';
|
|
359
|
+
const name = `${prefix}${stampNow()}.tar.gz`;
|
|
312
360
|
const out = `${root}/${name}`;
|
|
313
361
|
const base = dshHome.split('/').pop();
|
|
314
362
|
const parent = dshHome.slice(0, -(base.length + 1)) || '/';
|
|
@@ -353,7 +401,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
353
401
|
await saveAutoState();
|
|
354
402
|
}
|
|
355
403
|
|
|
356
|
-
return { path: out, sha: shaText, total: all.length, stale: stale.length, keep: keepN, sync };
|
|
404
|
+
return { path: out, name, sha: shaText, total: all.length, stale: stale.length, keep: keepN, sync };
|
|
357
405
|
}
|
|
358
406
|
|
|
359
407
|
// ---------- GitHub 同步 ----------
|
|
@@ -369,7 +417,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
369
417
|
const token = env?.get('DSH_BACKUP_GITHUB_TOKEN')?.value || env?.get('GITHUB_TOKEN')?.value;
|
|
370
418
|
// 本地路径(测试/自托管)或 http(s) 全 URL 直接使用,否则视为 owner/repo。
|
|
371
419
|
const repo = raw.includes('://') || /^[A-Za-z]:[\\/]|^\//.test(raw) ? raw : `https://github.com/${raw}.git`;
|
|
372
|
-
return { repo: repo.split('\\').join('/'), token };
|
|
420
|
+
return { repo: stripUserinfo(repo.split('\\').join('/')), token };
|
|
373
421
|
}
|
|
374
422
|
|
|
375
423
|
/** 校验仓库地址格式(owner/repo、完整 URL 或本地路径),非法返回原因。 */
|
|
@@ -638,7 +686,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
638
686
|
if (dryRun) return { archive, files: entries.length, sample: entries.slice(0, 12), aside: null, snapshotPath: null, dryRun: true };
|
|
639
687
|
|
|
640
688
|
// 恢复前自动快照当前数据,并把当前数据移到旁边(而非合并覆盖)。
|
|
641
|
-
|
|
689
|
+
// 快照用 dsh-pre-restore- 前缀与用户常规备份区分,经 listBackups 过滤
|
|
690
|
+
// 后不进列表/轮换/latest 选择;每次恢复清掉上一次的快照防累积。
|
|
691
|
+
const snapshot = await doBackup(undefined, signal, { namePrefix: 'dsh-pre-restore-' });
|
|
692
|
+
await prunePreRestoreSnapshots(snapshot.name, signal);
|
|
642
693
|
let aside = null;
|
|
643
694
|
let current = false;
|
|
644
695
|
try {
|
|
@@ -689,9 +740,11 @@ export function apply(ctx, pluginConfig) {
|
|
|
689
740
|
lastError: typeof parsed.github.lastError === 'string' ? parsed.github.lastError : null,
|
|
690
741
|
};
|
|
691
742
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
743
|
+
// lastAutoAt 是「上次执行」时间戳,不可能在未来。未来日期(注入或时钟
|
|
744
|
+
// 漂移)会使 nextAutoAt 算出超大延迟、auto 静默失效却仍显示已开启,
|
|
745
|
+
// 故一律视为非法 → null,从当前时间起算 catch-up。
|
|
746
|
+
const lastTs = typeof parsed?.lastAutoAt === 'string' ? Date.parse(parsed.lastAutoAt) : NaN;
|
|
747
|
+
lastAutoAt = Number.isNaN(lastTs) || lastTs > Date.now() ? null : parsed.lastAutoAt;
|
|
695
748
|
return Number.isFinite(h) && h >= 1 && h <= 720 ? Math.floor(h) : 0;
|
|
696
749
|
} catch (err) {
|
|
697
750
|
console.warn(`[dsh-backup] auto.json 无法读取,自动备份计划已重置: ${String(err && err.message ? err.message : err)}`);
|
|
@@ -808,9 +861,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
808
861
|
}
|
|
809
862
|
const invalid = validateRepo(value);
|
|
810
863
|
if (invalid) return { kind: 'error', text: invalid };
|
|
811
|
-
|
|
864
|
+
const clean = stripUserinfo(value);
|
|
865
|
+
githubState = { ...githubState, repo: clean, lastError: null };
|
|
812
866
|
await saveAutoState();
|
|
813
|
-
return { kind: 'success', text: `GitHub 同步仓库已设为: ${
|
|
867
|
+
return { kind: 'success', text: `GitHub 同步仓库已设为: ${clean}\n${autoSummary()}` };
|
|
814
868
|
}
|
|
815
869
|
if (arg === 'sync') {
|
|
816
870
|
const s = await githubSync(invocation.signal);
|
|
@@ -1009,7 +1063,8 @@ export function apply(ctx, pluginConfig) {
|
|
|
1009
1063
|
const { root } = paths();
|
|
1010
1064
|
return {
|
|
1011
1065
|
// repoRaw 是用户原始输入(运行时值优先,否则 cordis.yml 默认),供面板编辑框回填
|
|
1012
|
-
|
|
1066
|
+
// 两路都经 stripUserinfo:避免 cordis.yml config.githubRepo 内嵌 token 时经面板泄露
|
|
1067
|
+
repoRaw: githubState.repo ?? (typeof pluginConfig?.githubRepo === 'string' ? stripUserinfo(pluginConfig.githubRepo) : null),
|
|
1013
1068
|
repo: cfg ? cfg.repo : null,
|
|
1014
1069
|
tokenSet: Boolean(cfg?.token),
|
|
1015
1070
|
syncDir: `${root}/${SYNC_DIR}`,
|
|
@@ -1052,9 +1107,10 @@ export function apply(ctx, pluginConfig) {
|
|
|
1052
1107
|
}
|
|
1053
1108
|
const invalid = validateRepo(raw);
|
|
1054
1109
|
if (invalid) return { ok: false, summary: invalid };
|
|
1055
|
-
|
|
1110
|
+
const clean = stripUserinfo(raw);
|
|
1111
|
+
githubState = { ...githubState, repo: clean, lastError: null };
|
|
1056
1112
|
await saveAutoState();
|
|
1057
|
-
return { ok: true, repo:
|
|
1113
|
+
return { ok: true, repo: clean, summary: `GitHub 同步仓库已设为: ${clean}` };
|
|
1058
1114
|
},
|
|
1059
1115
|
};
|
|
1060
1116
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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 and a visual Settings panel. Cross-platform (macOS/Linux/Windows). 一键备份与恢复 DSH 数据:定时自动备份、完整性校验、下载与 GitHub 同步,附 Settings 可视面板。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|