@sparkelf/dsh-plugin-backup 0.1.0-rc.9 → 0.2.0-rc.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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/plus/backup/README.md
5
- README.md: c4406cb9fcdc6c17fda28c087f287416c52adaa9
6
- README.zh.md: e7c3d7f6ed809f530772b6eeca23d8f363706c65
5
+ README.md: 9779790a882ee3c85bda21914a77bf9048ec76b0
6
+ README.zh.md: 9570b2eee62259f48ec41e9f0229b7116547dbbf
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- This complete Host and Client plugin exports a file-backed DSH home as a ZIP archive, validates and restores an uploaded archive, reopens Workspace storage, and contributes the localized Backup Settings section. Export, upload, and import stream through Host temporary files with bounded NDJSON progress; import validates the manifest, paths, and expanded byte total before Workspace mutation.
12
+ This complete Host and Client plugin exports and restores three explicit user-data subsets: everything, configuration only, or sessions only. Configuration contains the active Settings document, credentials, optional `.env`, and anonymous identity; sessions contain Session logs, attachments, and storage-backed Workspace/projection state. Export, upload, and import stream through Host temporary files with bounded NDJSON progress; import validates the versioned manifest, scope, paths, and expanded byte total before mutation.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,9 +25,11 @@ This complete Host and Client plugin exports a file-backed DSH home as a ZIP arc
25
25
 
26
26
  Mount the Host entry after `connection`, `webServer`, `settings`, and `workspaceRegistry`, and mount the Client entry after locale and Settings. The file-backed Settings provider supplies `settings.documentPath`; `maxUploadBytes` defaults to 2147483648 bytes.
27
27
 
28
- The authenticated routes are POST `/api/backup.export.prepare`, GET/HEAD `/api/backup.export`, POST `/api/backup.upload`, and POST `/api/backup.import`. Every route applies Connection Host, Origin, and browser-authentication policy before request data. Tokens expire after ten minutes and are single-use, except HEAD metadata inspection.
28
+ The Settings section uses a three-way segmented control. **Everything** is the union of configuration and sessions; it does not archive generated profiles, releases, logs, Supervisor files, cached packages, or other runtime output. **Configuration** restores only configuration files and leaves Session/Workspace storage open. **Sessions** restores `sessions`, `attachments`, and `storages` together inside the Workspace storage-restore transaction. The archive manifest records the scope and active Settings filename; configuration-bearing archives require the same Settings filename at import.
29
29
 
30
- Once restore writes begin, closing the page does not interrupt replacement. Workspace storage closes around file replacement and reopens before completion; the UI then offers one explicit reload. Client failures remain visible in the Backup section without browser console errors.
30
+ The authenticated routes are POST `/api/backup.export.prepare?scope=all|configuration|sessions`, GET/HEAD `/api/backup.export`, POST `/api/backup.upload`, and POST `/api/backup.import`. Every route applies Connection Host, Origin, and browser-authentication policy before request data. Browser cancellation settles only the active route and never terminates the Harness Host. Tokens expire after ten minutes and are single-use, except HEAD metadata inspection.
31
+
32
+ Once restore writes begin, closing the page does not interrupt replacement. Workspace storage closes around file replacement and reopens before completion; the Client releases the completed progress stream reader before the UI offers one explicit reload. Client failures remain visible in the Backup section without browser console errors.
31
33
 
32
34
  -----
33
35
 
@@ -52,7 +54,8 @@ Independent. Export, import, progress, and reload do not change a model request
52
54
 
53
55
  <a id="known-limitations-and-deferred-work"></a>
54
56
 
55
- - **Overwrite without rollback**: restore replaces same-name files, retains files absent from the archive, and creates no rollback copy if a disk write fails after mutation begins.
57
+ - **Overwrite without rollback**: restore replaces same-name files within the declared scope, retains files absent from the archive, and creates no rollback copy if a disk write fails after mutation begins.
58
+ - **Current archive format only**: scoped archives use manifest version 2; pre-release version 1 archives are rejected instead of being guessed into a scope.
56
59
 
57
60
  <a id="dev-note"></a>
58
61
  ### Dev Note
@@ -63,5 +66,6 @@ Independent. Export, import, progress, and reload do not change a model request
63
66
  This Dev Note is working context for maintainers; shipped behavior, limits, and rationale live in the sections above, the package code, and the linked Agent Note.
64
67
 
65
68
  - The [Backup Agent Note](../../../.agents/notes/proposed/architecture/2026-08-30-plus-backup-plugin.md) owns archive, route, restore, and progress decisions; `src/index.ts` and `src/client/BackupSection.tsx` own current behavior.
69
+ - No runtime invariant companion is published because WebServer owns route registration and Workspace owns filesystem state; Backup has no independent second observation of either relationship.
66
70
 
67
71
  </details>
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 该完整Host及Client plugin把file-backed DSH home导出为ZIP archive,验证并恢复uploaded archive,重新打开Workspace storage,并贡献localized Backup Settings section。Export、upload和import通过Host temporary files stream并提供bounded NDJSON progress;import会在Workspace mutation前验证manifest、paths及expanded byte total。
12
+ 该完整Host及Client plugin导出并恢复三种明确的user-data subset:全部、仅配置或仅会话。配置包含active Settings document、credentials、可选`.env`及anonymous identity;会话包含Session logs、attachments及storage-backed Workspace/projection state。Export、upload和import通过Host temporary files stream并提供bounded NDJSON progress;import会在mutation前验证versioned manifest、scope、paths及expanded byte total。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,9 +25,11 @@ kind: "package-reference"
25
25
 
26
26
  Host entry需mount在`connection`、`webServer`、`settings`及`workspaceRegistry`之后,Client entry需mount在locale及Settings之后。file-backed Settings provider提供`settings.documentPath`;`maxUploadBytes`默认为2147483648 bytes。
27
27
 
28
- authenticated routes为POST `/api/backup.export.prepare`、GET/HEAD `/api/backup.export`、POST `/api/backup.upload`及POST `/api/backup.import`。每条route会在读取request data前应用Connection Host、Origin及browser-authentication policy。Token在十分钟后过期并且single-use,HEAD metadata inspection除外。
28
+ Settings section使用三段segmented control。**全部**是配置与会话的并集;它不归档generated profiles、releases、logs、Supervisor files、cached packages或其他runtime output。**配置**只恢复配置文件,不关闭Session/Workspace storage。**会话**把`sessions`、`attachments`及`storages`一起放进Workspace storage-restore transaction恢复。Archive manifest记录scope与active Settings filename;含配置的archive在import时要求相同Settings filename。
29
29
 
30
- 一旦restore开始写入,关闭page不会中断replacement。Workspace storage会在file replacement期间关闭并在completion前重新打开;UI随后提供一次explicit reload。Client failure保留在Backup section可见状态,不产生browser console error。
30
+ authenticated routes为POST `/api/backup.export.prepare?scope=all|configuration|sessions`、GET/HEAD `/api/backup.export`、POST `/api/backup.upload`及POST `/api/backup.import`。每条route会在读取request data前应用Connection Host、Origin及browser-authentication policy。Browser cancellation只结算当前route,绝不会终止Harness Host。Token在十分钟后过期并且single-use,HEAD metadata inspection除外。
31
+
32
+ 一旦restore开始写入,关闭page不会中断replacement。Workspace storage会在file replacement期间关闭并在completion前重新打开;Client会先释放completed progress stream reader,再由UI提供一次explicit reload。Client failure保留在Backup section可见状态,不产生browser console error。
31
33
 
32
34
  -----
33
35
 
@@ -52,7 +54,8 @@ authenticated routes为POST `/api/backup.export.prepare`、GET/HEAD `/api/backup
52
54
 
53
55
  <a id="known-limitations-and-deferred-work"></a>
54
56
 
55
- - **Overwrite without rollback**:restore会替换same-name files,保留archive中不存在的files,并且在mutation开始后disk write失败时不创建rollback copy。
57
+ - **Overwrite without rollback**:restore会替换declared scope内的same-name files,保留archive中不存在的files,并且在mutation开始后disk write失败时不创建rollback copy。
58
+ - **只接受当前archive format**:scoped archive使用manifest version 2;pre-release version 1 archive会被拒绝,不猜测其scope。
56
59
 
57
60
  <a id="dev-note"></a>
58
61
  ### 开发备注
@@ -63,5 +66,6 @@ authenticated routes为POST `/api/backup.export.prepare`、GET/HEAD `/api/backup
63
66
  该开发备注是maintainer working context;shipped behavior、limits及rationale以以上sections、package code及linked Agent Note为准。
64
67
 
65
68
  - [Backup Agent Note](../../../.agents/notes/proposed/architecture/2026-08-30-plus-backup-plugin.zh.md)持有archive、route、restore及progress decisions;`src/index.ts`和`src/client/BackupSection.tsx`持有current behavior。
69
+ - 该package不发布`./invariant` companion,因为WebServer持有route registration,Workspace持有filesystem state;Backup没有对任一relation的independent second observation。
66
70
 
67
71
  </details>
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
10
  //#region \0dsh-css:/home/runner/work/deepseek-harness-plus/deepseek-harness-plus/packages/plus/backup/src/client/BackupSection.module.css.mjs
11
- const css = ".cY-W_a_section{flex-direction:column;gap:16px;display:flex}.cY-W_a_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:600}.cY-W_a_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.cY-W_a_actions{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.cY-W_a_button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;padding:8px 14px;font-size:13px}.cY-W_a_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.cY-W_a_button:disabled{opacity:.6;cursor:default}.cY-W_a_fileInput{display:none}.cY-W_a_progress{flex-direction:column;gap:8px;width:min(100%,480px);display:flex}.cY-W_a_progressLabel,.cY-W_a_progressDetail{margin:0;font-size:13px}.cY-W_a_progressLabel{color:var(--dsw-alias-label-primary);font-weight:500}.cY-W_a_progressDetail{color:var(--dsw-alias-label-secondary)}.cY-W_a_progressTrack{width:100%;height:8px;accent-color:var(--dsw-alias-state-business-primary);border:0}.cY-W_a_progressTrack::-webkit-progress-bar{background:var(--dsw-alias-border-l2);border-radius:4px}.cY-W_a_progressTrack::-webkit-progress-value{background:var(--dsw-alias-state-business-primary);border-radius:4px}.cY-W_a_progressTrack::-moz-progress-bar{background:var(--dsw-alias-state-business-primary);border-radius:4px}.cY-W_a_status{color:var(--dsw-alias-state-success-primary);margin:0;font-size:13px}.cY-W_a_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.cY-W_a_warning{color:var(--dsw-alias-state-warn-primary);margin:0;font-size:12px}";
11
+ const css = ".cY-W_a_section{flex-direction:column;gap:16px;display:flex}.cY-W_a_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:600}.cY-W_a_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.cY-W_a_scopeGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:6px;align-self:flex-start;padding:2px;display:inline-flex}.cY-W_a_scopeButton{min-width:72px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:4px;padding:0 10px;font-size:13px}.cY-W_a_scopeButton:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.cY-W_a_scopeButton[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:0 1px 2px #0000001f}.cY-W_a_scopeButton:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.cY-W_a_scopeButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.cY-W_a_actions{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.cY-W_a_button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;padding:8px 14px;font-size:13px}.cY-W_a_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.cY-W_a_button:disabled{opacity:.6;cursor:default}.cY-W_a_fileInput{display:none}.cY-W_a_progress{flex-direction:column;gap:8px;width:min(100%,480px);display:flex}.cY-W_a_progressLabel,.cY-W_a_progressDetail{margin:0;font-size:13px}.cY-W_a_progressLabel{color:var(--dsw-alias-label-primary);font-weight:500}.cY-W_a_progressDetail{color:var(--dsw-alias-label-secondary)}.cY-W_a_progressTrack{width:100%;height:8px;accent-color:var(--dsw-alias-state-business-primary);border:0}.cY-W_a_progressTrack::-webkit-progress-bar{background:var(--dsw-alias-border-l2);border-radius:4px}.cY-W_a_progressTrack::-webkit-progress-value{background:var(--dsw-alias-state-business-primary);border-radius:4px}.cY-W_a_progressTrack::-moz-progress-bar{background:var(--dsw-alias-state-business-primary);border-radius:4px}.cY-W_a_status{color:var(--dsw-alias-state-success-primary);margin:0;font-size:13px}.cY-W_a_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.cY-W_a_warning{color:var(--dsw-alias-state-warn-primary);margin:0;font-size:12px}";
12
12
  const tagId = "@sparkelf/dsh-plugin-backup/BackupSection.module.css";
13
13
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
14
  const tag = document.createElement("style");
@@ -27,6 +27,8 @@ window.__ModuleLoader__.load({
27
27
  "progressDetail": "cY-W_a_progressDetail",
28
28
  "progressLabel": "cY-W_a_progressLabel",
29
29
  "progressTrack": "cY-W_a_progressTrack",
30
+ "scopeButton": "cY-W_a_scopeButton",
31
+ "scopeGroup": "cY-W_a_scopeGroup",
30
32
  "section": "cY-W_a_section",
31
33
  "status": "cY-W_a_status",
32
34
  "title": "cY-W_a_title",
@@ -85,10 +87,18 @@ window.__ModuleLoader__.load({
85
87
  /** Map one transport or archive rejection to product copy. */
86
88
  function localize(caught, kind) {
87
89
  const message = caught instanceof Error ? caught.message : String(caught);
88
- if (message.includes("missing backup-manifest.json")) return "notBackup";
90
+ if (message.includes("backup-manifest.json")) return "notBackup";
89
91
  if (message.includes("unsafe path")) return "unsafe";
90
92
  return kind === "export" ? "exportFailed" : "importFailed";
91
93
  }
94
+ function resultTextKey(result) {
95
+ if (result.kind === "exported") {
96
+ if (result.scope === "all") return "exportedAll";
97
+ return result.scope === "configuration" ? "exportedConfiguration" : "exportedSessions";
98
+ }
99
+ if (result.scope === "all") return "importedAll";
100
+ return result.scope === "configuration" ? "importedConfiguration" : "importedSessions";
101
+ }
92
102
  /**
93
103
  * The Backup section body.
94
104
  * @param props - runtime owner props, locale, store seat, and streamed operation callbacks.
@@ -96,11 +106,12 @@ window.__ModuleLoader__.load({
96
106
  */
97
107
  function BackupSection(props) {
98
108
  const { t, actions } = props;
109
+ const scope = props.useStore((state) => state.scope);
99
110
  const operation = props.useStore((state) => state.operation);
100
- const status = props.useStore((state) => state.status);
111
+ const result = props.useStore((state) => state.result);
101
112
  const error = props.useStore((state) => state.error);
102
113
  const cancelling = props.useStore((state) => state.cancelling);
103
- const reloadRequired = status === "imported";
114
+ const reloadRequired = result?.kind === "imported";
104
115
  const fileRef = (0, react.useRef)(null);
105
116
  const runExport = async () => {
106
117
  actions.begin({
@@ -108,17 +119,20 @@ window.__ModuleLoader__.load({
108
119
  progress: { phase: "scan" }
109
120
  });
110
121
  try {
111
- const { downloadUrl } = await props.exportArchive((progress) => {
122
+ const exported = await props.exportArchive(scope, (progress) => {
112
123
  actions.progress({
113
124
  kind: "export",
114
125
  progress
115
126
  });
116
127
  });
117
128
  const anchor = document.createElement("a");
118
- anchor.href = downloadUrl;
119
- anchor.download = "deepseek-harness-backup.zip";
129
+ anchor.href = exported.downloadUrl;
130
+ anchor.download = exported.filename;
120
131
  anchor.click();
121
- actions.complete("exported");
132
+ actions.complete({
133
+ kind: "exported",
134
+ scope: exported.scope
135
+ });
122
136
  } catch (caught) {
123
137
  if (isCancellation(caught)) {
124
138
  actions.cancelled();
@@ -141,13 +155,16 @@ window.__ModuleLoader__.load({
141
155
  });
142
156
  (async () => {
143
157
  try {
144
- await props.importArchive(file, (progress) => {
158
+ const imported = await props.importArchive(file, (progress) => {
145
159
  actions.progress({
146
160
  kind: "import",
147
161
  progress
148
162
  });
149
163
  });
150
- actions.complete("imported");
164
+ actions.complete({
165
+ kind: "imported",
166
+ scope: imported.scope
167
+ });
151
168
  } catch (caught) {
152
169
  if (isCancellation(caught)) {
153
170
  actions.cancelled();
@@ -170,6 +187,26 @@ window.__ModuleLoader__.load({
170
187
  className: BackupSection_module_css_default.desc,
171
188
  children: t("desc")
172
189
  }),
190
+ (0, react_jsx_runtime.jsx)("div", {
191
+ className: BackupSection_module_css_default.scopeGroup,
192
+ role: "radiogroup",
193
+ "aria-label": t("scopeLabel"),
194
+ children: [
195
+ "all",
196
+ "configuration",
197
+ "sessions"
198
+ ].map((value) => (0, react_jsx_runtime.jsx)("button", {
199
+ type: "button",
200
+ role: "radio",
201
+ "aria-checked": scope === value,
202
+ className: BackupSection_module_css_default.scopeButton,
203
+ disabled: operation !== null || reloadRequired,
204
+ onClick: () => {
205
+ actions.selectScope(value);
206
+ },
207
+ children: t(value === "all" ? "scopeAll" : value === "configuration" ? "scopeConfiguration" : "scopeSessions")
208
+ }, value))
209
+ }),
173
210
  (0, react_jsx_runtime.jsxs)("div", {
174
211
  className: BackupSection_module_css_default.actions,
175
212
  children: [
@@ -236,10 +273,10 @@ window.__ModuleLoader__.load({
236
273
  })
237
274
  ]
238
275
  }) : null,
239
- status !== null ? (0, react_jsx_runtime.jsx)("p", {
276
+ result !== null ? (0, react_jsx_runtime.jsx)("p", {
240
277
  className: BackupSection_module_css_default.status,
241
278
  role: "status",
242
- children: t(status)
279
+ children: t(resultTextKey(result))
243
280
  }) : null,
244
281
  reloadRequired ? (0, react_jsx_runtime.jsx)("button", {
245
282
  type: "button",
@@ -263,7 +300,6 @@ window.__ModuleLoader__.load({
263
300
  }
264
301
  //#endregion
265
302
  //#region lib/types/client/store.js
266
- /** Remount-surviving state for the Settings Backup slot registration. */
267
303
  /**
268
304
  * Declare the Backup section operation and result state.
269
305
  * @returns the root-scoped store handle.
@@ -271,15 +307,21 @@ window.__ModuleLoader__.load({
271
307
  function createBackupSectionStore() {
272
308
  return (0, _deepseek_ai_dsh_client_store.defineStore)({
273
309
  init: () => ({
310
+ scope: "all",
274
311
  operation: null,
275
- status: null,
312
+ result: null,
276
313
  error: null,
277
314
  cancelling: false
278
315
  }),
279
316
  actions: {
317
+ selectScope: (draft, scope) => {
318
+ draft.scope = scope;
319
+ draft.result = null;
320
+ draft.error = null;
321
+ },
280
322
  begin: (draft, operation) => {
281
323
  draft.operation = operation;
282
- draft.status = null;
324
+ draft.result = null;
283
325
  draft.error = null;
284
326
  draft.cancelling = false;
285
327
  },
@@ -293,15 +335,15 @@ window.__ModuleLoader__.load({
293
335
  draft.operation = null;
294
336
  draft.cancelling = false;
295
337
  },
296
- complete: (draft, status) => {
338
+ complete: (draft, result) => {
297
339
  draft.operation = null;
298
- draft.status = status;
340
+ draft.result = result;
299
341
  draft.error = null;
300
342
  draft.cancelling = false;
301
343
  },
302
344
  fail: (draft, error) => {
303
345
  draft.operation = null;
304
- draft.status = null;
346
+ draft.result = null;
305
347
  draft.error = error;
306
348
  draft.cancelling = false;
307
349
  }
@@ -314,9 +356,13 @@ window.__ModuleLoader__.load({
314
356
  const zh = {
315
357
  nav: "备份",
316
358
  title: "备份与恢复",
317
- desc: "把用户设置和数据导出为一个 zip 压缩包,或从备份压缩包恢复。",
318
- exportButton: "导出备份压缩包",
319
- importButton: "选择压缩包并导入",
359
+ desc: "导出全部数据、仅配置或仅会话,也可以从对应备份包恢复。",
360
+ scopeLabel: "备份内容",
361
+ scopeAll: "全部",
362
+ scopeConfiguration: "配置",
363
+ scopeSessions: "会话",
364
+ exportButton: "导出所选内容",
365
+ importButton: "选择备份包并导入",
320
366
  busyExport: "正在生成备份…",
321
367
  busyImport: "正在导入备份…",
322
368
  cancelButton: "取消",
@@ -327,11 +373,15 @@ window.__ModuleLoader__.load({
327
373
  importUpload: "正在上传备份",
328
374
  importValidate: "正在校验备份完整性…",
329
375
  importRestore: "正在恢复备份",
330
- importReload: "正在加载会话与工作区…",
376
+ importReload: "正在加载恢复的数据…",
331
377
  importProgressLabel: "备份恢复进度",
332
378
  progressDetail: "数据较多时可能需要几分钟,请保持此页面打开。",
333
- exported: "备份已生成,浏览器已开始下载。",
334
- imported: "备份已导入。重新加载页面后,会话和工作区将使用恢复后的数据。",
379
+ exportedAll: "完整备份已生成,浏览器已开始下载。",
380
+ exportedConfiguration: "配置备份已生成,浏览器已开始下载。",
381
+ exportedSessions: "会话备份已生成,浏览器已开始下载。",
382
+ importedAll: "完整备份已导入。重新加载后将使用恢复的配置、会话和工作区。",
383
+ importedConfiguration: "配置备份已导入。重新加载后将使用恢复的设置和凭据。",
384
+ importedSessions: "会话备份已导入。重新加载后将使用恢复的会话、附件和工作区。",
335
385
  reloadButton: "重新加载页面",
336
386
  notBackup: "所选压缩包不是 DeepSeek Harness 备份文件。",
337
387
  unsafe: "备份压缩包包含不安全的条目路径。",
@@ -343,9 +393,13 @@ window.__ModuleLoader__.load({
343
393
  const en = {
344
394
  nav: "Backup",
345
395
  title: "Backup and restore",
346
- desc: "Export user settings and data as one zip archive, or restore them from a backup archive.",
347
- exportButton: "Export backup archive",
348
- importButton: "Choose archive and import",
396
+ desc: "Export everything, configuration only, or sessions only, and restore the corresponding archive.",
397
+ scopeLabel: "Backup contents",
398
+ scopeAll: "Everything",
399
+ scopeConfiguration: "Configuration",
400
+ scopeSessions: "Sessions",
401
+ exportButton: "Export selected contents",
402
+ importButton: "Choose backup and import",
349
403
  busyExport: "Generating backup…",
350
404
  busyImport: "Importing backup…",
351
405
  cancelButton: "Cancel",
@@ -356,11 +410,15 @@ window.__ModuleLoader__.load({
356
410
  importUpload: "Uploading backup",
357
411
  importValidate: "Validating backup integrity…",
358
412
  importRestore: "Restoring backup",
359
- importReload: "Loading sessions and workspaces…",
413
+ importReload: "Loading restored data…",
360
414
  importProgressLabel: "Backup restore progress",
361
415
  progressDetail: "Large data sets may take several minutes; keep this page open.",
362
- exported: "Backup generated; the browser download has started.",
363
- imported: "Backup imported. Reload to apply the restored session and workspace data.",
416
+ exportedAll: "Complete backup generated; the browser download has started.",
417
+ exportedConfiguration: "Configuration backup generated; the browser download has started.",
418
+ exportedSessions: "Session backup generated; the browser download has started.",
419
+ importedAll: "Complete backup imported. Reload to apply the restored configuration, sessions, and workspaces.",
420
+ importedConfiguration: "Configuration backup imported. Reload to apply the restored settings and credentials.",
421
+ importedSessions: "Session backup imported. Reload to apply the restored sessions, attachments, and workspaces.",
364
422
  reloadButton: "Reload page",
365
423
  notBackup: "The selected archive is not a DeepSeek Harness backup file.",
366
424
  unsafe: "The backup archive contains unsafe entry paths.",
@@ -391,6 +449,10 @@ window.__ModuleLoader__.load({
391
449
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("invalid backup progress count");
392
450
  return value;
393
451
  }
452
+ function parseScope(value) {
453
+ if (value === "all" || value === "configuration" || value === "sessions") return value;
454
+ throw new Error("invalid backup scope");
455
+ }
394
456
  /** Parse one Host-authored phase update from the exact-route stream. */
395
457
  function parseHostProgress(value) {
396
458
  const record = parseRecord(value);
@@ -416,14 +478,18 @@ window.__ModuleLoader__.load({
416
478
  };
417
479
  if (record.type === "export-ready") {
418
480
  if (typeof record.downloadUrl !== "string" || record.downloadUrl === "") throw new Error("invalid backup export download URL");
481
+ if (typeof record.filename !== "string" || record.filename === "") throw new Error("invalid backup export filename");
419
482
  return {
420
483
  type: "export-ready",
421
484
  downloadUrl: record.downloadUrl,
485
+ filename: record.filename,
486
+ scope: parseScope(record.scope),
422
487
  entries: parseCount(record.entries)
423
488
  };
424
489
  }
425
490
  if (record.type === "import-complete") return {
426
491
  type: "import-complete",
492
+ scope: parseScope(record.scope),
427
493
  entries: parseCount(record.entries)
428
494
  };
429
495
  if (record.type === "error" && typeof record.message === "string") return {
@@ -441,20 +507,24 @@ window.__ModuleLoader__.load({
441
507
  let pending = "";
442
508
  let terminal;
443
509
  let terminalError;
444
- while (true) {
445
- const { done, value } = await reader.read();
446
- pending += decoder.decode(value, { stream: !done });
447
- const lines = pending.split(String.fromCharCode(10));
448
- if (done) pending = "";
449
- else pending = lines.pop();
450
- for (const line of lines) {
451
- if (line === "") continue;
452
- const message = parseProgressLine(line);
453
- if (message.type === "progress") report(message.progress);
454
- else if (message.type === "error") terminalError = message.message;
455
- else terminal = message;
510
+ try {
511
+ while (true) {
512
+ const { done, value } = await reader.read();
513
+ pending += decoder.decode(value, { stream: !done });
514
+ const lines = pending.split(String.fromCharCode(10));
515
+ if (done) pending = "";
516
+ else pending = lines.pop();
517
+ for (const line of lines) {
518
+ if (line === "") continue;
519
+ const message = parseProgressLine(line);
520
+ if (message.type === "progress") report(message.progress);
521
+ else if (message.type === "error") terminalError = message.message;
522
+ else terminal = message;
523
+ }
524
+ if (done) break;
456
525
  }
457
- if (done) break;
526
+ } finally {
527
+ reader.releaseLock();
458
528
  }
459
529
  if (terminalError !== void 0) throw new Error(terminalError);
460
530
  if (terminal === void 0) throw new Error("backup progress response ended without a result");
@@ -543,14 +613,16 @@ window.__ModuleLoader__.load({
543
613
  }
544
614
  };
545
615
  const injected = () => ({
546
- exportArchive: (report) => runAbortable(async (signal) => {
547
- const terminal = await readProgressResponse(await fetch(browserHostUrl("/api/backup.export.prepare"), {
616
+ exportArchive: (scope, report) => runAbortable(async (signal) => {
617
+ const terminal = await readProgressResponse(await fetch(browserHostUrl("/api/backup.export.prepare?scope=" + encodeURIComponent(scope)), {
548
618
  method: "POST",
549
619
  signal
550
620
  }), report);
551
621
  if (terminal.type !== "export-ready") throw new Error("backup export returned an import result");
552
622
  return {
553
623
  downloadUrl: browserHostUrl(terminal.downloadUrl),
624
+ filename: terminal.filename,
625
+ scope: terminal.scope,
554
626
  entries: terminal.entries
555
627
  };
556
628
  }),
@@ -561,7 +633,10 @@ window.__ModuleLoader__.load({
561
633
  signal
562
634
  }), report);
563
635
  if (terminal.type !== "import-complete") throw new Error("backup import returned an export result");
564
- return { entries: terminal.entries };
636
+ return {
637
+ entries: terminal.entries,
638
+ scope: terminal.scope
639
+ };
565
640
  }),
566
641
  cancelOperation: () => {
567
642
  activeController?.abort();
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { dirname, join } from "node:path";
1
+ import { basename, dirname, join } from "node:path";
2
2
  import Schema from "@deepseek-ai/schemastery";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { once } from "node:events";
5
5
  import { createReadStream, createWriteStream } from "node:fs";
6
- import { mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
6
+ import { mkdir, mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { Transform } from "node:stream";
9
9
  import { finished, pipeline } from "node:stream/promises";
@@ -18,8 +18,18 @@ import { open } from "yauzl";
18
18
  */
19
19
  /** Manifest entry at the archive root; import validation uses it as the marker. */
20
20
  const BACKUP_MANIFEST_ENTRY = "backup-manifest.json";
21
- /** Runtime-generated harness-home directories that are not user configuration or data. */
22
- const GENERATED_DIRECTORIES = new Set(["profiles", "supervisor"]);
21
+ /** Optional configuration files stored beside the active Settings document. */
22
+ const CONFIGURATION_FILES = [
23
+ ".credentials.yaml",
24
+ ".env",
25
+ ".anonymous-user-id"
26
+ ];
27
+ /** Durable Session data that must move together to preserve history and grouping. */
28
+ const SESSION_DIRECTORIES = [
29
+ "sessions",
30
+ "attachments",
31
+ "storages"
32
+ ];
23
33
  /** File read and restore write size. */
24
34
  const BACKUP_CHUNK_BYTES = 64 * 1024;
25
35
  const EMPTY_BYTES = new Uint8Array(0);
@@ -38,7 +48,6 @@ async function planDirectory(rootPath, relativePath, entries, signal) {
38
48
  for (const child of children) {
39
49
  signal.throwIfAborted();
40
50
  const entryRelative = relativePath === "" ? child.name : relativePath + "/" + child.name;
41
- if (relativePath === "" && (GENERATED_DIRECTORIES.has(child.name) || child.name === BACKUP_MANIFEST_ENTRY)) continue;
42
51
  if (child.isDirectory()) {
43
52
  entries.push({
44
53
  kind: "directory",
@@ -60,21 +69,48 @@ async function planDirectory(rootPath, relativePath, entries, signal) {
60
69
  /**
61
70
  * Build the stable file list and source-byte total used by one export.
62
71
  * @param dshHome - harness user data directory.
72
+ * @param scope - selected user-data subset.
73
+ * @param settingsFile - active Settings document basename.
63
74
  * @param signal - caller or response cancellation.
64
75
  * @returns the planned entries, manifest bytes, and measurable byte total.
65
76
  */
66
- async function planUserBackup(dshHome, signal) {
67
- const manifest = strToU8(JSON.stringify({
77
+ async function planUserBackup(dshHome, scope, settingsFile, signal) {
78
+ const manifestRecord = {
68
79
  app: "deepseek-harness",
69
80
  kind: "user-data-backup",
70
- version: 1,
81
+ version: 2,
82
+ scope,
83
+ settingsFile,
71
84
  exportedAt: (/* @__PURE__ */ new Date()).toISOString()
72
- }, null, 2) + String.fromCharCode(10));
85
+ };
86
+ const manifest = strToU8(JSON.stringify(manifestRecord, null, 2) + String.fromCharCode(10));
73
87
  const entries = [];
88
+ const topLevel = new Map((await readdir(dshHome, { withFileTypes: true })).map((entry) => [entry.name, entry]));
89
+ const names = [...scope === "all" || scope === "configuration" ? [settingsFile, ...CONFIGURATION_FILES] : [], ...scope === "all" || scope === "sessions" ? SESSION_DIRECTORIES : []];
90
+ let fileBytes = 0;
91
+ for (const name of new Set(names)) {
92
+ signal.throwIfAborted();
93
+ const entry = topLevel.get(name);
94
+ if (entry?.isDirectory()) {
95
+ entries.push({
96
+ kind: "directory",
97
+ relativePath: name
98
+ });
99
+ fileBytes += await planDirectory(dshHome, name, entries, signal);
100
+ } else if (entry?.isFile()) {
101
+ const { size } = await stat(join(dshHome, name));
102
+ entries.push({
103
+ kind: "file",
104
+ relativePath: name,
105
+ size
106
+ });
107
+ fileBytes += size;
108
+ }
109
+ }
74
110
  return {
75
111
  entries,
76
112
  manifest,
77
- totalBytes: await planDirectory(dshHome, "", entries, signal) + manifest.length
113
+ totalBytes: fileBytes + manifest.length
78
114
  };
79
115
  }
80
116
  /**
@@ -118,13 +154,15 @@ async function addFileToZip(zip, rootPath, entry, waitForOutput, signal, onBytes
118
154
  * its partial archive.
119
155
  * @param dshHome - harness user data directory (settings.yaml parent).
120
156
  * @param targetPath - absolute file path the archive is written to.
157
+ * @param scope - selected user-data subset.
158
+ * @param settingsFile - active Settings document basename.
121
159
  * @param signal - caller or response cancellation.
122
160
  * @param report - ordered progress writer.
123
161
  * @returns the number of zip entries including the manifest marker.
124
162
  */
125
- async function writeUserBackup(dshHome, targetPath, signal, report) {
163
+ async function writeUserBackup(dshHome, targetPath, scope, settingsFile, signal, report) {
126
164
  await report({ phase: "scan" });
127
- const plan = await planUserBackup(dshHome, signal);
165
+ const plan = await planUserBackup(dshHome, scope, settingsFile, signal);
128
166
  let completedBytes = 0;
129
167
  let lastPercent = -1;
130
168
  const reportCompression = async () => {
@@ -200,6 +238,22 @@ async function writeUserBackup(dshHome, targetPath, signal, report) {
200
238
  }
201
239
  }
202
240
  }
241
+ function isBackupScope(value) {
242
+ return value === "all" || value === "configuration" || value === "sessions";
243
+ }
244
+ function requireBackupManifest(value, settingsFile) {
245
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Backup archive has an invalid backup-manifest.json");
246
+ const manifest = value;
247
+ if (manifest.app !== "deepseek-harness" || manifest.kind !== "user-data-backup" || manifest.version !== 2 || !isBackupScope(manifest.scope) || typeof manifest.settingsFile !== "string" || typeof manifest.exportedAt !== "string") throw new Error("Backup archive has an unsupported backup-manifest.json");
248
+ if ((manifest.scope === "all" || manifest.scope === "configuration") && manifest.settingsFile !== settingsFile) throw new Error("Backup archive backup-manifest.json Settings filename does not match this DSH profile");
249
+ return manifest;
250
+ }
251
+ function scopeAllowsPath(scope, settingsFile, relativePath) {
252
+ const top = relativePath.split("/", 1)[0];
253
+ const configuration = relativePath === settingsFile || CONFIGURATION_FILES.includes(relativePath);
254
+ const sessions = SESSION_DIRECTORIES.includes(top);
255
+ return scope === "all" ? configuration || sessions : scope === "configuration" ? configuration : sessions;
256
+ }
203
257
  function openArchive(path) {
204
258
  return new Promise((resolveOpen, reject) => {
205
259
  open(path, {
@@ -258,10 +312,11 @@ function requireSafeArchivePath(name) {
258
312
  * @param archivePath - disk-staged raw upload.
259
313
  * @param stagingRoot - temporary extraction root outside the harness home.
260
314
  * @param maxExpandedBytes - aggregate expanded-byte limit shared with upload admission.
315
+ * @param settingsFile - active Settings document basename used to validate configuration archives.
261
316
  * @param signal - response cancellation honored until DSH-home mutation begins.
262
317
  * @returns the validated staged entry plan.
263
318
  */
264
- async function validateUserBackup(archivePath, stagingRoot, maxExpandedBytes, signal) {
319
+ async function validateUserBackup(archivePath, stagingRoot, maxExpandedBytes, settingsFile, signal) {
265
320
  const zip = await openArchive(archivePath);
266
321
  const entries = [];
267
322
  const names = /* @__PURE__ */ new Set();
@@ -309,11 +364,17 @@ async function validateUserBackup(archivePath, stagingRoot, maxExpandedBytes, si
309
364
  zip.close();
310
365
  }
311
366
  if (!manifestSeen) throw new Error("Not a DeepSeek Harness user data backup: missing backup-manifest.json");
367
+ const manifest = requireBackupManifest(JSON.parse(await readFile(join(stagingRoot, BACKUP_MANIFEST_ENTRY), "utf8")), settingsFile);
368
+ for (const entry of entries) {
369
+ if (entry.relativePath === BACKUP_MANIFEST_ENTRY) continue;
370
+ if (!scopeAllowsPath(manifest.scope, settingsFile, entry.relativePath)) throw new Error(`Backup archive contains an unsafe path for its ${manifest.scope} scope: ${entry.relativePath}`);
371
+ }
312
372
  return {
313
373
  stagingRoot,
314
374
  entries,
315
375
  count: entries.length,
316
- totalBytes
376
+ totalBytes,
377
+ scope: manifest.scope
317
378
  };
318
379
  }
319
380
  /**
@@ -368,12 +429,14 @@ var BackupUploadTooLargeError = class extends Error {};
368
429
  /** Own one-use Host temp-file tokens and delete every file when its ownership ends. */
369
430
  var BackupTokenStore = class {
370
431
  entries = /* @__PURE__ */ new Map();
371
- mintDownload(path, size) {
432
+ mintDownload(path, size, filename, scope) {
372
433
  this.sweep();
373
434
  return this.mint({
374
435
  kind: "download",
375
436
  path,
376
437
  size,
438
+ filename,
439
+ scope,
377
440
  expiresAt: Date.now() + BACKUP_TOKEN_TTL_MS
378
441
  });
379
442
  }
@@ -469,7 +532,17 @@ function registerProtectedRoute(ctx, path, handler) {
469
532
  response.end(rejection === 401 ? "unauthorized" : "forbidden");
470
533
  return;
471
534
  }
472
- await handler(request, response);
535
+ try {
536
+ await handler(request, response);
537
+ } catch (error) {
538
+ if (request.destroyed || response.destroyed) {
539
+ console.info("[plus-backup] request cancelled", { path }, error);
540
+ return;
541
+ }
542
+ console.error("[plus-backup] route failed", { path }, error);
543
+ if (!response.headersSent) response.writeHead(500);
544
+ if (!response.writableEnded) response.end("backup request failed");
545
+ }
473
546
  }
474
547
  };
475
548
  ctx.effect(() => ctx.webServer.register(route), `plus-backup: ${path} route`);
@@ -484,6 +557,17 @@ function tokenFromRequest(request) {
484
557
  const token = new URL(request.url ?? "", "http://localhost").searchParams.get("token");
485
558
  return token === null || token === "" ? void 0 : token;
486
559
  }
560
+ function scopeFromRequest(request) {
561
+ const scope = new URL(request.url ?? "", "http://localhost").searchParams.get("scope");
562
+ return scope === "all" || scope === "configuration" || scope === "sessions" ? scope : void 0;
563
+ }
564
+ function backupFilename(scope) {
565
+ switch (scope) {
566
+ case "all": return "deepseek-harness-backup.zip";
567
+ case "configuration": return "deepseek-harness-configuration.zip";
568
+ case "sessions": return "deepseek-harness-sessions.zip";
569
+ }
570
+ }
487
571
  async function writeProgressLine(response, line) {
488
572
  if (response.destroyed) throw new Error("backup progress response closed");
489
573
  if (!response.write(JSON.stringify(line) + String.fromCharCode(10))) await once(response, "drain");
@@ -502,7 +586,7 @@ function beginProgressResponse(response) {
502
586
  return controller.signal;
503
587
  }
504
588
  function publicOperationError(error, operation) {
505
- if (error instanceof Error && (error.message.includes("missing backup-manifest.json") || error.message.includes("unsafe path"))) return error.message;
589
+ if (error instanceof Error && (error.message.includes("backup-manifest.json") || error.message.includes("unsafe path"))) return error.message;
506
590
  return "backup " + operation + " failed";
507
591
  }
508
592
  /** Stream one upload to disk; both declared and observed bytes share this ingress limit owner. */
@@ -536,22 +620,31 @@ async function stageUpload(request, response, tokens, maxUploadBytes) {
536
620
  response.end(error instanceof BackupUploadTooLargeError ? "backup upload too large" : "backup upload failed");
537
621
  }
538
622
  }
539
- async function prepareExport(request, response, tokens, dshHome) {
623
+ async function prepareExport(request, response, tokens, dshHome, settingsFile) {
540
624
  if (!requireMethod(request, response, ["POST"])) return;
625
+ const scope = scopeFromRequest(request);
626
+ if (scope === void 0) {
627
+ response.writeHead(400);
628
+ response.end("missing or invalid backup scope");
629
+ return;
630
+ }
541
631
  const signal = beginProgressResponse(response);
542
632
  const path = join(await mkdtemp(join(tmpdir(), "dsh-backup-export-")), "backup.zip");
543
633
  let tokenMinted = false;
544
634
  try {
545
- const { entries } = await writeUserBackup(dshHome, path, signal, (progress) => writeProgressLine(response, {
635
+ const { entries } = await writeUserBackup(dshHome, path, scope, settingsFile, signal, (progress) => writeProgressLine(response, {
546
636
  type: "progress",
547
637
  progress
548
638
  }));
549
639
  const { size } = await stat(path);
550
- const token = tokens.mintDownload(path, size);
640
+ const filename = backupFilename(scope);
641
+ const token = tokens.mintDownload(path, size, filename, scope);
551
642
  tokenMinted = true;
552
643
  await writeProgressLine(response, {
553
644
  type: "export-ready",
554
645
  downloadUrl: "/api/backup.export?token=" + token,
646
+ filename,
647
+ scope,
555
648
  entries
556
649
  });
557
650
  response.end();
@@ -582,7 +675,7 @@ async function downloadExport(request, response, tokens) {
582
675
  }
583
676
  response.writeHead(200, {
584
677
  "content-type": "application/zip",
585
- "content-disposition": "attachment; filename=\"deepseek-harness-backup.zip\"",
678
+ "content-disposition": `attachment; filename="${entry.filename}"`,
586
679
  "content-length": String(entry.size)
587
680
  });
588
681
  if (request.method === "HEAD") {
@@ -599,7 +692,7 @@ async function downloadExport(request, response, tokens) {
599
692
  }
600
693
  }
601
694
  /** Validate before mutation, then complete restore even when the browser closes mid-write. */
602
- async function importBackup(ctx, request, response, tokens, dshHome, maxExpandedBytes) {
695
+ async function importBackup(ctx, request, response, tokens, dshHome, maxExpandedBytes, settingsFile) {
603
696
  if (!requireMethod(request, response, ["POST"])) return;
604
697
  const token = tokenFromRequest(request);
605
698
  if (token === void 0) {
@@ -634,17 +727,19 @@ async function importBackup(ctx, request, response, tokens, dshHome, maxExpanded
634
727
  try {
635
728
  signal.throwIfAborted();
636
729
  await report({ phase: "validate" });
637
- const validated = await validateUserBackup(staged.path, join(dirname(staged.path), "validated"), maxExpandedBytes, signal);
730
+ const validated = await validateUserBackup(staged.path, join(dirname(staged.path), "validated"), maxExpandedBytes, settingsFile, signal);
638
731
  signal.throwIfAborted();
639
- const { entries } = await ctx.workspaceRegistry.withStorageRestore(async () => {
732
+ const restore = async () => {
640
733
  const restored = await restoreUserBackup(validated, dshHome, report);
641
734
  await report({ phase: "reload" });
642
735
  return restored;
643
- });
736
+ };
737
+ const { entries } = validated.scope === "configuration" ? await restore() : await ctx.workspaceRegistry.withStorageRestore(restore);
644
738
  if (response.destroyed) return;
645
739
  await writeProgressLine(response, {
646
740
  type: "import-complete",
647
- entries
741
+ entries,
742
+ scope: validated.scope
648
743
  });
649
744
  response.end();
650
745
  } catch (error) {
@@ -670,9 +765,9 @@ function registerBackupRoutes(ctx, config, dshHome) {
670
765
  const tokens = new BackupTokenStore();
671
766
  ctx.effect(() => () => tokens.dispose(), "plus-backup: temp files");
672
767
  registerProtectedRoute(ctx, "/api/backup.upload", (request, response) => stageUpload(request, response, tokens, config.maxUploadBytes));
673
- registerProtectedRoute(ctx, "/api/backup.export.prepare", (request, response) => prepareExport(request, response, tokens, dshHome));
768
+ registerProtectedRoute(ctx, "/api/backup.export.prepare", (request, response) => prepareExport(request, response, tokens, dshHome, config.settingsFile));
674
769
  registerProtectedRoute(ctx, "/api/backup.export", (request, response) => downloadExport(request, response, tokens));
675
- registerProtectedRoute(ctx, "/api/backup.import", (request, response) => importBackup(ctx, request, response, tokens, dshHome, config.maxUploadBytes));
770
+ registerProtectedRoute(ctx, "/api/backup.import", (request, response) => importBackup(ctx, request, response, tokens, dshHome, config.maxUploadBytes, config.settingsFile));
676
771
  }
677
772
  //#endregion
678
773
  //#region lib/types/index.js
@@ -695,7 +790,10 @@ const Config = Schema.object({ maxUploadBytes: Schema.number().step(1).min(1).de
695
790
  function apply(ctx, config = {}) {
696
791
  const documentPath = ctx.settings.documentPath;
697
792
  if (documentPath === void 0) throw new Error("plus-backup requires a file-backed settings provider");
698
- registerBackupRoutes(ctx, { maxUploadBytes: config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES }, dirname(documentPath));
793
+ registerBackupRoutes(ctx, {
794
+ maxUploadBytes: config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES,
795
+ settingsFile: basename(documentPath)
796
+ }, dirname(documentPath));
699
797
  }
700
798
  //#endregion
701
799
  export { Config, apply, inject, name };
@@ -5,17 +5,20 @@
5
5
  * while publishing restored-byte progress.
6
6
  */
7
7
  import type { BackupProgressReporter } from './protocol.ts';
8
+ import type { BackupScope } from './types.ts';
8
9
  /**
9
10
  * Pack the harness home's user configuration and data into a zip archive,
10
11
  * publishing scan and source-byte compression progress. A failed export removes
11
12
  * its partial archive.
12
13
  * @param dshHome - harness user data directory (settings.yaml parent).
13
14
  * @param targetPath - absolute file path the archive is written to.
15
+ * @param scope - selected user-data subset.
16
+ * @param settingsFile - active Settings document basename.
14
17
  * @param signal - caller or response cancellation.
15
18
  * @param report - ordered progress writer.
16
19
  * @returns the number of zip entries including the manifest marker.
17
20
  */
18
- export declare function writeUserBackup(dshHome: string, targetPath: string, signal: AbortSignal, report: BackupProgressReporter): Promise<{
21
+ export declare function writeUserBackup(dshHome: string, targetPath: string, scope: BackupScope, settingsFile: string, signal: AbortSignal, report: BackupProgressReporter): Promise<{
19
22
  entries: number;
20
23
  }>;
21
24
  /** One validated archive entry staged outside the harness home. */
@@ -37,16 +40,19 @@ export interface ValidatedUserBackup {
37
40
  count: number;
38
41
  /** Total regular-file bytes restored into the harness home. */
39
42
  totalBytes: number;
43
+ /** User-data subset declared by the validated manifest. */
44
+ scope: BackupScope;
40
45
  }
41
46
  /**
42
47
  * Validate and extract one staged zip without retaining archive or entry bodies in memory.
43
48
  * @param archivePath - disk-staged raw upload.
44
49
  * @param stagingRoot - temporary extraction root outside the harness home.
45
50
  * @param maxExpandedBytes - aggregate expanded-byte limit shared with upload admission.
51
+ * @param settingsFile - active Settings document basename used to validate configuration archives.
46
52
  * @param signal - response cancellation honored until DSH-home mutation begins.
47
53
  * @returns the validated staged entry plan.
48
54
  */
49
- export declare function validateUserBackup(archivePath: string, stagingRoot: string, maxExpandedBytes: number, signal: AbortSignal): Promise<ValidatedUserBackup>;
55
+ export declare function validateUserBackup(archivePath: string, stagingRoot: string, maxExpandedBytes: number, settingsFile: string, signal: AbortSignal): Promise<ValidatedUserBackup>;
50
56
  /**
51
57
  * Write a validated staged archive over the harness home while publishing restored bytes.
52
58
  * Once file mutation begins the operation completes rather than honoring a late response
@@ -6,6 +6,7 @@ import type { ReactNode } from 'react';
6
6
  import type { InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
7
7
  import type { BackupSectionStore } from './store.ts';
8
8
  import type { BackupSectionProgress } from './types.ts';
9
+ import type { BackupScope } from '../types.ts';
9
10
  /** Registration-side business face for the two streamed backup operations. */
10
11
  export interface BackupSectionInjected {
11
12
  /**
@@ -13,8 +14,10 @@ export interface BackupSectionInjected {
13
14
  * @param report - receives ordered progress updates.
14
15
  * @returns the single-use browser download URL and entry count.
15
16
  */
16
- exportArchive(report: (progress: BackupSectionProgress) => void): Promise<{
17
+ exportArchive(scope: BackupScope, report: (progress: BackupSectionProgress) => void): Promise<{
17
18
  downloadUrl: string;
19
+ filename: string;
20
+ scope: BackupScope;
18
21
  entries: number;
19
22
  }>;
20
23
  /**
@@ -25,6 +28,7 @@ export interface BackupSectionInjected {
25
28
  */
26
29
  importArchive(file: File, report: (progress: BackupSectionProgress) => void): Promise<{
27
30
  entries: number;
31
+ scope: BackupScope;
28
32
  }>;
29
33
  /** Cancel the active operation while its current phase still permits cancellation. */
30
34
  cancelOperation(): void;
@@ -2,9 +2,13 @@
2
2
  export declare const zh: {
3
3
  readonly nav: "备份";
4
4
  readonly title: "备份与恢复";
5
- readonly desc: "把用户设置和数据导出为一个 zip 压缩包,或从备份压缩包恢复。";
6
- readonly exportButton: "导出备份压缩包";
7
- readonly importButton: "选择压缩包并导入";
5
+ readonly desc: "导出全部数据、仅配置或仅会话,也可以从对应备份包恢复。";
6
+ readonly scopeLabel: "备份内容";
7
+ readonly scopeAll: "全部";
8
+ readonly scopeConfiguration: "配置";
9
+ readonly scopeSessions: "会话";
10
+ readonly exportButton: "导出所选内容";
11
+ readonly importButton: "选择备份包并导入";
8
12
  readonly busyExport: "正在生成备份…";
9
13
  readonly busyImport: "正在导入备份…";
10
14
  readonly cancelButton: "取消";
@@ -15,11 +19,15 @@ export declare const zh: {
15
19
  readonly importUpload: "正在上传备份";
16
20
  readonly importValidate: "正在校验备份完整性…";
17
21
  readonly importRestore: "正在恢复备份";
18
- readonly importReload: "正在加载会话与工作区…";
22
+ readonly importReload: "正在加载恢复的数据…";
19
23
  readonly importProgressLabel: "备份恢复进度";
20
24
  readonly progressDetail: "数据较多时可能需要几分钟,请保持此页面打开。";
21
- readonly exported: "备份已生成,浏览器已开始下载。";
22
- readonly imported: "备份已导入。重新加载页面后,会话和工作区将使用恢复后的数据。";
25
+ readonly exportedAll: "完整备份已生成,浏览器已开始下载。";
26
+ readonly exportedConfiguration: "配置备份已生成,浏览器已开始下载。";
27
+ readonly exportedSessions: "会话备份已生成,浏览器已开始下载。";
28
+ readonly importedAll: "完整备份已导入。重新加载后将使用恢复的配置、会话和工作区。";
29
+ readonly importedConfiguration: "配置备份已导入。重新加载后将使用恢复的设置和凭据。";
30
+ readonly importedSessions: "会话备份已导入。重新加载后将使用恢复的会话、附件和工作区。";
23
31
  readonly reloadButton: "重新加载页面";
24
32
  readonly notBackup: "所选压缩包不是 DeepSeek Harness 备份文件。";
25
33
  readonly unsafe: "备份压缩包包含不安全的条目路径。";
@@ -1,12 +1,14 @@
1
- /** Remount-surviving state for the Settings Backup slot registration. */
2
1
  import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
3
- import type { BackupErrorKey, BackupOperation, BackupSectionState, BackupStatusKey } from './types.ts';
2
+ import type { BackupScope } from '../types.ts';
3
+ import type { BackupErrorKey, BackupOperation, BackupResult, BackupSectionState } from './types.ts';
4
+ /** Remount-surviving state for the Settings Backup slot registration. */
4
5
  type BackupSectionActions = {
6
+ selectScope: (draft: BackupSectionState, scope: BackupScope) => void;
5
7
  begin: (draft: BackupSectionState, operation: BackupOperation) => void;
6
8
  progress: (draft: BackupSectionState, operation: BackupOperation) => void;
7
9
  requestCancel: (draft: BackupSectionState) => void;
8
10
  cancelled: (draft: BackupSectionState) => void;
9
- complete: (draft: BackupSectionState, status: BackupStatusKey) => void;
11
+ complete: (draft: BackupSectionState, result: BackupResult) => void;
10
12
  fail: (draft: BackupSectionState, error: BackupErrorKey) => void;
11
13
  };
12
14
  /** Store handle shared by each remount of the root-scoped Backup section. */
@@ -1,3 +1,4 @@
1
+ import type { BackupScope } from '../types.ts';
1
2
  /** Browser presentation progress after parsing Host lines or upload events. */
2
3
  export type BackupSectionProgress = {
3
4
  /** Phase without a measurable byte total. */
@@ -15,14 +16,18 @@ export type BackupOperation = {
15
16
  kind: 'export' | 'import';
16
17
  progress: BackupSectionProgress;
17
18
  };
18
- /** Successful operation copy selected by the current locale. */
19
- export type BackupStatusKey = 'exported' | 'imported';
19
+ /** Successful operation and the archive subset it affected. */
20
+ export type BackupResult = {
21
+ kind: 'exported' | 'imported';
22
+ scope: BackupScope;
23
+ };
20
24
  /** Failed operation copy selected by the current locale. */
21
25
  export type BackupErrorKey = 'notBackup' | 'unsafe' | 'exportFailed' | 'importFailed';
22
26
  /** Remount-surviving presentation state owned by the Backup slot registration. */
23
27
  export type BackupSectionState = {
28
+ scope: BackupScope;
24
29
  operation: BackupOperation | null;
25
- status: BackupStatusKey | null;
30
+ result: BackupResult | null;
26
31
  error: BackupErrorKey | null;
27
32
  cancelling: boolean;
28
33
  };
@@ -1,4 +1,6 @@
1
1
  /** Host progress and terminal records for Plus user-data Backup routes. */
2
+ import type { BackupScope } from './types.ts';
3
+ export type { BackupScope } from './types.ts';
2
4
  /** Progress without a stable byte total while scanning, validating, or reloading. */
3
5
  type BackupOpenProgress = {
4
6
  phase: 'scan' | 'validate' | 'reload';
@@ -20,13 +22,15 @@ export type BackupProgressLine = {
20
22
  } | {
21
23
  type: 'export-ready';
22
24
  downloadUrl: string;
25
+ filename: string;
26
+ scope: BackupScope;
23
27
  entries: number;
24
28
  } | {
25
29
  type: 'import-complete';
30
+ scope: BackupScope;
26
31
  entries: number;
27
32
  } | {
28
33
  type: 'error';
29
34
  message: string;
30
35
  };
31
- export {};
32
36
  //# sourceMappingURL=protocol.d.ts.map
@@ -9,6 +9,7 @@ export type BackupHostContext = Context & {
9
9
  /** Backup route resource policy. */
10
10
  export interface BackupRouteConfig {
11
11
  maxUploadBytes: number;
12
+ settingsFile: string;
12
13
  }
13
14
  /**
14
15
  * Register the complete authenticated Backup route set and its temp-file lifecycle.
@@ -0,0 +1,3 @@
1
+ /** User-data subset represented by one Backup archive and restore operation. */
2
+ export type BackupScope = 'all' | 'configuration' | 'sessions';
3
+ //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,83 +1,77 @@
1
1
  {
2
- "name": "@sparkelf/dsh-plugin-backup",
2
+ "dependencies": {
3
+ "@deepseek-ai/schemastery": ">=3.18.1",
4
+ "fflate": "^0.8.2",
5
+ "yauzl": "^3.2.0"
6
+ },
3
7
  "description": "Full-stack Plus user-data Backup plugin with streamed Host archive routes and Settings UI",
4
- "version": "0.1.0-rc.9",
5
- "publishConfig": {
6
- "access": "public"
8
+ "devDependencies": {
9
+ "@deepseek-ai/cordis": "^4.0.2",
10
+ "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
11
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
12
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
13
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
14
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
15
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
16
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
17
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
18
+ "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
19
+ "@types/react": "~18.3.1",
20
+ "@types/yauzl": "^2.10.3",
21
+ "react": "^18.2.0"
7
22
  },
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/SparkElf/deepseek-harness-plus.git",
11
- "directory": "packages/plus/backup"
23
+ "dsh": {
24
+ "client": {
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-locale",
27
+ "@deepseek-ai/dsh-client-ui-settings",
28
+ "@deepseek-ai/dsh-client-ui-renderer"
29
+ ],
30
+ "platform": "web"
31
+ }
12
32
  },
13
- "type": "module",
14
- "main": "lib/index.js",
15
- "types": "lib/types/index.d.ts",
16
33
  "exports": {
17
34
  ".": {
18
- "types": "./lib/types/index.d.ts",
19
- "default": "./lib/index.js"
20
- },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
35
+ "default": "./lib/index.js",
36
+ "types": "./lib/types/index.d.ts"
24
37
  },
25
38
  "./client": {
26
- "types": "./lib/types/client/index.d.ts",
27
- "default": "./lib/client.js"
39
+ "default": "./lib/client.js",
40
+ "types": "./lib/types/client/index.d.ts"
28
41
  },
29
42
  "./package.json": "./package.json"
30
43
  },
31
44
  "files": [
32
45
  "lib/index.js",
33
- "lib/invariant.js",
34
46
  "lib/client.js",
35
47
  "lib/types/**/*.d.ts"
36
48
  ],
37
49
  "license": "MIT",
38
- "dependencies": {
39
- "@deepseek-ai/schemastery": ">=3.18.1",
40
- "fflate": "^0.8.2",
41
- "yauzl": "^3.2.0"
42
- },
50
+ "main": "lib/index.js",
51
+ "name": "@sparkelf/dsh-plugin-backup",
43
52
  "peerDependencies": {
44
53
  "@deepseek-ai/cordis": ">=4.0.1",
45
- "@deepseek-ai/dsh-client-locale": ">=0.1.2-alpha.1",
46
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-alpha.1",
47
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.2-alpha.1",
48
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.2-alpha.1",
49
- "@deepseek-ai/dsh-host-webserver": ">=0.1.2-alpha.1",
50
- "@deepseek-ai/dsh-invariants": ">=0.1.2-alpha.1",
51
- "@deepseek-ai/dsh-settings": ">=0.1.2-alpha.1",
52
- "@deepseek-ai/dsh-workspace": ">=0.1.2-alpha.1"
54
+ "@deepseek-ai/dsh-client-locale": ">=0.1.5-rc.2",
55
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-rc.2",
56
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.5-rc.2",
57
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.2",
58
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.5-rc.2",
59
+ "@deepseek-ai/dsh-settings": ">=0.1.5-rc.2",
60
+ "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2"
53
61
  },
54
- "devDependencies": {
55
- "@types/react": "~18.3.1",
56
- "@types/yauzl": "^2.10.3",
57
- "react": "^18.2.0",
58
- "@deepseek-ai/cordis": "^4.0.1",
59
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.1",
60
- "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.1",
61
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.1",
62
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.1",
63
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.1",
64
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.1",
65
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.1",
66
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.1",
67
- "@deepseek-ai/dsh-workspace": "^0.1.2-alpha.1"
62
+ "publishConfig": {
63
+ "access": "public"
68
64
  },
69
- "dsh": {
70
- "client": {
71
- "inject": [
72
- "@deepseek-ai/dsh-client-locale",
73
- "@deepseek-ai/dsh-client-ui-settings",
74
- "@deepseek-ai/dsh-client-ui-renderer"
75
- ],
76
- "platform": "web"
77
- }
65
+ "repository": {
66
+ "directory": "packages/plus/backup",
67
+ "type": "git",
68
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git"
78
69
  },
79
70
  "scripts": {
80
71
  "bundle": "tsdown",
81
72
  "watch": "tsdown --watch"
82
- }
83
- }
73
+ },
74
+ "type": "module",
75
+ "types": "lib/types/index.d.ts",
76
+ "version": "0.2.0-rc.1"
77
+ }
package/lib/invariant.js DELETED
@@ -1,18 +0,0 @@
1
- //#region lib/types/invariant.js
2
- /** Package invariant companion for `@sparkelf/dsh-plugin-backup`. */
3
- const PACKAGE_NAME = "@sparkelf/dsh-plugin-backup";
4
- const name = "plus-backup-invariant";
5
- const inject = ["invariants"];
6
- /**
7
- * No runtime invariant: WebServer owns route registration, Workspace owns its
8
- * durable/cache relation, and the browser settings slot has no Host projection.
9
- */
10
- const install = () => {};
11
- /**
12
- * Register this package's invariant companion.
13
- * @param ctx - Host context carrying the invariant registry.
14
- * @returns the registration disposer.
15
- */
16
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
17
- //#endregion
18
- export { apply, inject, name };
@@ -1,11 +0,0 @@
1
- /** Package invariant companion for `@sparkelf/dsh-plugin-backup`. */
2
- import type { Context } from '@deepseek-ai/cordis';
3
- export declare const name = "plus-backup-invariant";
4
- export declare const inject: string[];
5
- /**
6
- * Register this package's invariant companion.
7
- * @param ctx - Host context carrying the invariant registry.
8
- * @returns the registration disposer.
9
- */
10
- export declare const apply: (ctx: Context) => Promise<() => void>;
11
- //# sourceMappingURL=invariant.d.ts.map