@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 +2 -2
- package/README.md +8 -4
- package/README.zh.md +8 -4
- package/lib/client.js +121 -46
- package/lib/index.js +127 -29
- package/lib/types/archive.d.ts +8 -2
- package/lib/types/client/BackupSection.d.ts +5 -1
- package/lib/types/client/locales.d.ts +14 -6
- package/lib/types/client/store.d.ts +5 -3
- package/lib/types/client/types.d.ts +8 -3
- package/lib/types/protocol.d.ts +5 -1
- package/lib/types/routes.d.ts +1 -0
- package/lib/types/types.d.ts +3 -0
- package/package.json +52 -58
- package/lib/invariant.js +0 -18
- package/lib/types/invariant.d.ts +0 -11
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:
|
|
6
|
-
README.zh.md:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
129
|
+
anchor.href = exported.downloadUrl;
|
|
130
|
+
anchor.download = exported.filename;
|
|
120
131
|
anchor.click();
|
|
121
|
-
actions.complete(
|
|
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(
|
|
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
|
-
|
|
276
|
+
result !== null ? (0, react_jsx_runtime.jsx)("p", {
|
|
240
277
|
className: BackupSection_module_css_default.status,
|
|
241
278
|
role: "status",
|
|
242
|
-
children: t(
|
|
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
|
-
|
|
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.
|
|
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,
|
|
338
|
+
complete: (draft, result) => {
|
|
297
339
|
draft.operation = null;
|
|
298
|
-
draft.
|
|
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.
|
|
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: "
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
22
|
-
const
|
|
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
|
|
77
|
+
async function planUserBackup(dshHome, scope, settingsFile, signal) {
|
|
78
|
+
const manifestRecord = {
|
|
68
79
|
app: "deepseek-harness",
|
|
69
80
|
kind: "user-data-backup",
|
|
70
|
-
version:
|
|
81
|
+
version: 2,
|
|
82
|
+
scope,
|
|
83
|
+
settingsFile,
|
|
71
84
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
72
|
-
}
|
|
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:
|
|
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
|
-
|
|
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("
|
|
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
|
|
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":
|
|
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
|
|
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, {
|
|
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 };
|
package/lib/types/archive.d.ts
CHANGED
|
@@ -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: "
|
|
6
|
-
readonly
|
|
7
|
-
readonly
|
|
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
|
|
22
|
-
readonly
|
|
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 {
|
|
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,
|
|
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
|
|
19
|
-
export type
|
|
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
|
-
|
|
30
|
+
result: BackupResult | null;
|
|
26
31
|
error: BackupErrorKey | null;
|
|
27
32
|
cancelling: boolean;
|
|
28
33
|
};
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -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
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,83 +1,77 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
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
|
-
"
|
|
5
|
-
|
|
6
|
-
"
|
|
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
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
"
|
|
19
|
-
"
|
|
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
|
-
"
|
|
27
|
-
"
|
|
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
|
-
"
|
|
39
|
-
|
|
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.
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.
|
|
49
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
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
|
-
"
|
|
55
|
-
"
|
|
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
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
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 };
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -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
|