@gehennawu/dsh-service 1.9.7 → 1.9.9
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.en.md +6 -5
- package/README.md +6 -5
- package/backup-integrity.js +49 -3
- package/backup-routes.js +5 -1
- package/client.js +1 -1
- package/index.js +76 -5
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
<em>DeepSeek Harness (DSH) Web 服务控制与运维插件。</em>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
[](package.json)
|
|
13
13
|
[](LICENSE)
|
|
14
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
15
|
[](https://cordis.moe/)
|
|
16
16
|
[](https://github.com/gehennawu/dsh-service)
|
|
17
17
|
[](https://github.com/gehennawu/dsh-service/issues)
|
|
@@ -74,7 +74,7 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
|
|
|
74
74
|
|
|
75
75
|
- Shows the current DSH and plugin versions, linking to GitHub Releases
|
|
76
76
|
- Automatically checks npm **stable + preview** (latest / next dist-tags)
|
|
77
|
-
- "What's new" sits right after the current version number and is always available: it opens the notes for **the version you are running**; when a new version exists, the status text ("New version: x.y.z") is clickable and expands **the new version's** release info. Either way the host fetches the body from the GitHub Releases API and the client renders it in place (plain Markdown — no iframe, no navigation), with the version number, publish date and a pre-release tag; a missing Release or a failed read each get their own message; clicking anywhere outside the version card or pressing Escape closes it
|
|
77
|
+
- "What's new" sits right after the current version number and is always available: it opens the notes for **the version you are running**; when a new version exists, the status text ("New version: x.y.z") is clickable and expands **the new version's** release info. Either way the host fetches the body from the GitHub Releases API and the client renders it in place (plain Markdown — no iframe, no navigation), with the version number, publish date and a pre-release tag; a missing Release or a failed read each get their own message; clicking anywhere outside the version card or pressing Escape closes it. Both rows lay their version number and "What's new" button out on **one shared set of columns**, with the version number right-aligned so that the gap before the button is fixed — they line up **regardless of version-number length**, since a longer version only widens the whole column instead of knocking one row out of place
|
|
78
78
|
- One-click upgrade with automatic restart; when no process manager is detected, it confirms the consequences first, keeps running, and shows manual-restart instructions
|
|
79
79
|
- Between the upgrade landing and the process restart (common in a manual-launch environment) the version row reads "Installed X — restart to take effect" and the upgrade button is withdrawn; reopening the panel or refreshing the page keeps that state until the process is restarted
|
|
80
80
|
|
|
@@ -150,8 +150,9 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
|
|
|
150
150
|
- Backup records use light two-line rows (name on the first line, size · time on the second, separator layout; same style as the session list)
|
|
151
151
|
|
|
152
152
|
- Creates `.tar.gz` archives of sessions, configuration, and plugin-profile manifests; sessions are snapshotted through the persistence layer's stable-read seam (active-agent writes no longer fail creation), and creation shows one continuous phase progress bar (copy / pack / verify / publish, step counter 1/4–4/4, real percentage during copy)
|
|
153
|
+
- **DSH version at backup time**: the archive filename carries the DSH version running when the backup was taken (`dsh-backup-YYYYMMDD-HHmmss-dsh0.1.7-rc.1.tar.gz`), and `meta/backup.json` inside the archive records the DSH version, plugin version, and creation time; both the panel row and the restore preflight show it, so you can tell which version a snapshot came from (restoring across a downgrade cannot read newer session formats). Archives written by older plugin versions have no version segment: they still list and restore normally, they simply show no version line, and import accepts filenames both with and without the segment
|
|
153
154
|
- Export download / import upload / delete (two-step confirmation); unlimited, never auto-pruned; imported archives must pass the same integrity inspection used by restore
|
|
154
|
-
- **Integrity inspection** validates gzip/tar structure, paths, and entry types. Only `sessions`, the three allowlisted config files,
|
|
155
|
+
- **Integrity inspection** validates gzip/tar structure, paths, and entry types. Only `sessions`, the three allowlisted config files, `profiles/<name>/package.json`, and `meta/backup.json` are accepted; traversal, links, special files, unknown content, corrupt archives, and invalid profile manifests are rejected
|
|
155
156
|
- **Restore preflight** produces a single-use five-minute plan showing the full sessions replacement, config replacements/removals, and profile-manifest updates. Final commit rechecks the archive SHA-256 and current-target fingerprint; any drift rejects the restore
|
|
156
157
|
- Restore uses a transaction journal and rollback directory: sessions are replaced in full, config is made exact to the snapshot, and profiles update package.json only while keeping node_modules, credentials, and attachments. Managed runtimes restart automatically; manual launches receive hand-restart instructions
|
|
157
158
|
|
|
@@ -353,7 +354,7 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
353
354
|
|
|
354
355
|
Requirements: Node.js `>=22`, and a DSH Web installation capable of loading both Host and Client plugin halves. Update checks require access to `registry.npmjs.org`; network failures do not affect other features.
|
|
355
356
|
|
|
356
|
-
**DSH compatibility statement**: adapted to DSH `0.1.7-
|
|
357
|
+
**DSH compatibility statement**: adapted to DSH `0.1.7-rc.2` — session format V4 (V3 logs are migrated by the official layer into a `session.v4.jsonl.zstd` generation file on first read, with the old `session.v3.jsonl.zstd` retained per the format catalog's policy; the detail view automatically archives system events), the `SettingsForms` configuration surface (plugin config now lives in the Profile's `cordis.patch.yml` with hot updates over `loader/volatile-update`, while the legacy `settings.register` remains authoritative on 0.1.5/0.1.6 so the two never cross-contaminate on a shared host), and all existing V3 adaptation (the `system/message` history, the handle-based sessionPersistence, the official right sidebar, the object-shaped turn-process), plus dual-hash compatibility for mobile bottom-row triggers, subagent turn-tail list-slot adaptive compatibility, `plugins.bundle.config` slot injection, and session-detail open fallback through `uiWorkspace`. Older DSH releases (`>=0.1.1-rc.2`) remain supported. The declared range now extends through `0.1.7-rc.2`; source-level and artifact-level reviews of rc.2 (346 commits) found no new breakage, and CSS hash stems have zero drift across artifact grep; the plugin-version compatibility gate introduced in rc.1 reads only `peerDependencies` (this plugin declares none, so the gate does not apply); real-host mounting on DSH `0.1.7-rc.2` is operational. The settings surface and the persistence/layout seams all run in dual shapes detected from runtime capabilities, and adaptation items that target newer structures are naturally inert on older hosts (cosmetic only, no functional loss). Note: sessions written after upgrading cannot be read by older DSH releases — **backups do not restore across a version downgrade**. The plugin marketplace judges compatibility from the `engines.dsh` range in `package.json`, which is the single declaration of the supported range.
|
|
357
358
|
|
|
358
359
|
## 🔒 Security design
|
|
359
360
|
|
package/README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
<em>A service-control & operations plugin for DeepSeek Harness (DSH) Web.</em>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
[](package.json)
|
|
13
13
|
[](LICENSE)
|
|
14
|
-
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
15
|
[](https://cordis.moe/)
|
|
16
16
|
[](https://github.com/gehennawu/dsh-service)
|
|
17
17
|
[](https://github.com/gehennawu/dsh-service/issues)
|
|
@@ -74,7 +74,7 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
|
|
|
74
74
|
|
|
75
75
|
- 显示当前 DSH 与插件版本,链接 GitHub Releases
|
|
76
76
|
- 自动检查 npm **正式版 + 预览版**(latest / next 双 tag)
|
|
77
|
-
- 「本次更新内容」入口跟在当前版本号之后且常驻:点开读**当前这一版**的更新说明;有新版本时状态文本「有新版本:x.y.z」整体可点,展开**新版**的 release 信息。正文一律由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带版本号、发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc
|
|
77
|
+
- 「本次更新内容」入口跟在当前版本号之后且常驻:点开读**当前这一版**的更新说明;有新版本时状态文本「有新版本:x.y.z」整体可点,展开**新版**的 release 信息。正文一律由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带版本号、发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc 即关闭。两行的版本号与「本次更新内容」入口按钮按同一套列对齐,版本号右对齐使它与入口按钮的间距固定,**与版本号长度无关**——长短版本号只把整列一起推宽,不会让某一行错位
|
|
78
78
|
- 一键升级,完成后自动重启;未检测到进程管理器时先确认后果,保持运行并提示手动重启
|
|
79
79
|
- 升级落地但进程尚未重启期间(手动启动环境尤为常见),版本行改示「已安装 X,重启后生效」并收起升级按钮,重开面板或刷新页面状态依旧;重启进程后恢复常态
|
|
80
80
|
|
|
@@ -152,8 +152,9 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
|
|
|
152
152
|
- 备份记录列表为两行轻行(文件名主行 + 体积 · 时间次行,分隔线布局;会话管理列表同款)
|
|
153
153
|
|
|
154
154
|
- 创建会话、配置与插件 profile 清单的 `.tar.gz` 归档;会话经持久化层稳定快照(活跃 agent 写入不再导致失败),创建过程以单条连续进度条分阶段显示(复制/打包/校验/发布,带步骤号 1/4–4/4,复制阶段为真实百分比)
|
|
155
|
+
- **备份时版本信息**:归档文件名带备份那一刻的 DSH 版本(`dsh-backup-YYYYMMDD-HHmmss-dsh0.1.7-rc.1.tar.gz`),归档内另有 `meta/backup.json` 记录 DSH 版本、插件版本与创建时刻;面板行内与恢复预检都显示该版本,便于判断这份快照出自哪个版本(跨版本降级恢复读不动新格式会话)。旧版本插件所出的归档没有版本段,列表照常显示、恢复照常进行,只是不显示版本行;导入同时接受带版本段与不带版本段两种文件名
|
|
155
156
|
- 导出下载 / 导入上传 / 删除(两段式确认);不限份数、不自动清理;导入归档必须先通过与恢复相同的完整性检查
|
|
156
|
-
- **完整性检查**:恢复前校验 gzip/tar、路径与条目类型,只接受 `sessions
|
|
157
|
+
- **完整性检查**:恢复前校验 gzip/tar、路径与条目类型,只接受 `sessions`、三份允许配置、`profiles/<name>/package.json` 与 `meta/backup.json`;拒绝越界路径、链接、特殊文件、未知内容、损坏归档与非法 profile 清单
|
|
157
158
|
- **恢复预检**:先生成 5 分钟有效的一次性计划,展示会话整体替换、配置覆盖/移除和 profile manifest 覆盖清单;最终确认前再次校验归档 SHA-256 与当前目标指纹,发生漂移则拒绝执行
|
|
158
159
|
- 恢复提交使用事务日志和回滚目录:会话整体替换,配置按快照精确替换,profile 只更新 package.json 并保留 node_modules/凭据/附件;成功后托管环境自动重启,手动启动环境提示用户手动重启
|
|
159
160
|
|
|
@@ -355,7 +356,7 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
355
356
|
|
|
356
357
|
运行要求:Node.js `>=22`,DSH Web 能加载 Host 与 Client 两半插件。更新检查需访问 `registry.npmjs.org`;网络失败不影响其他功能。
|
|
357
358
|
|
|
358
|
-
**DSH 适配口径**:已适配 DSH `0.1.7-
|
|
359
|
+
**DSH 适配口径**:已适配 DSH `0.1.7-rc.2`——会话格式 V4(V3 日志首读时由官方迁移为 `session.v4.jsonl.zstd` 代际文件,旧 `session.v3.jsonl.zstd` 按格式目录策略保留;详情视图自动归档系统事件)、`SettingsForms` 配置面(插件配置改由 Profile 的 `cordis.patch.yml` 承载,热更新走 `loader/volatile-update`;旧的 `settings.register` 在 0.1.5/0.1.6 上仍是权威来源,装在同一宿主上两者互不串台)、会话格式 V3 既有适配全部保留(`system/message` 入史、sessionPersistence handle 化、官方右栏、turn-process 对象化)、移动端底行触发钮双哈希兼容、子代理回合尾模型行 list 槽位自适应兼容、`plugins.bundle.config` 槽位注入、会话详情打开接入 `uiWorkspace` 降级链路。旧版 DSH(`>=0.1.1-rc.2`)保持兼容。支持区间扩展到 `0.1.7-rc.2`;rc.2 源码级与产物级审查(346 commits)未发现新增破坏面,CSS 哈希词干经产物 grep 定案零漂移;rc.1 起新增的插件版本兼容门禁只读取 `peerDependencies`(本插件无该声明,不受门禁约束),并已在 DSH `0.1.7-rc.2` 真机挂载运行。设置面、persistence/布局 seam 均按运行时能力探测走双形态,旧宿主上针对新结构的适配项天然不生效(纯展示,无功能损失)。注意:升级后写入的会话日志无法被旧版 DSH 读取,**备份不可跨版本降级恢复**。插件市场按 `package.json` 的 `engines.dsh` 区间判定兼容性(该字段是唯一的支持口径声明)。
|
|
359
360
|
|
|
360
361
|
## 🔒 安全设计
|
|
361
362
|
|
package/backup-integrity.js
CHANGED
|
@@ -10,6 +10,15 @@ const CONFIG_FILES = Object.freeze(['settings.yaml', 'cordis.patch.yml', 'AGENTS
|
|
|
10
10
|
// `cordis.patch.yml` 是 0.1.7-alpha.1 起的 Profile 用户配置层:漏掉它会出现「备份成功、
|
|
11
11
|
// 恢复后个性化配置全丢」。package.json 承载 bundle 启停清单,两者必须成对。
|
|
12
12
|
const PROFILE_FILES = Object.freeze(['package.json', 'cordis.patch.yml'])
|
|
13
|
+
// 归档内元数据(备份时的 DSH/插件版本):唯一一个「读后即丢弃、绝不落盘覆盖」的分区。
|
|
14
|
+
// 它必须出现在白名单里,否则 0.1.7 起新插件所出的归档会被判 `backup-entry-unexpected`
|
|
15
|
+
// 而全部不可恢复——这是最容易踩的反向兼容坑。
|
|
16
|
+
const META_PATH = 'meta/backup.json'
|
|
17
|
+
const MAX_META_BYTES = 4096
|
|
18
|
+
// 版本串的合法口径与宿主侧备份命名(index.js 的 BACKUP_VERSION_RE)逐字同源:
|
|
19
|
+
// semver 形、长度封顶、且**必须含数字**——少了最后一条,未解析出 DSH 包的宿主
|
|
20
|
+
// 写下的哨兵值 `unknown` 会被当成真版本显示出来。
|
|
21
|
+
const META_VERSION_RE = /^(?=.*[0-9])[0-9A-Za-z][0-9A-Za-z.-]{0,63}$/
|
|
13
22
|
const PLAN_TTL_MS = 5 * 60 * 1000
|
|
14
23
|
const MAX_COMPRESSED_BYTES = 512 * 1024 * 1024
|
|
15
24
|
const MAX_EXPANDED_BYTES = 1024 * 1024 * 1024
|
|
@@ -98,11 +107,23 @@ function emptySections() {
|
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
// 归档元数据的解析口径:**纯展示信息,绝不参与完整性裁决**。缺条目、坏 JSON、版本串
|
|
111
|
+
// 非法或超限一律回 null(预检显示「未记录」),不让一份附加信息把可恢复的归档判成损坏。
|
|
112
|
+
function parseBackupMetadata(data) {
|
|
113
|
+
if (data.length === 0 || data.length > MAX_META_BYTES) return null
|
|
114
|
+
let parsed
|
|
115
|
+
try { parsed = JSON.parse(data.toString('utf8')) } catch (_) { return null }
|
|
116
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
117
|
+
const version = typeof parsed.dshVersion === 'string' && META_VERSION_RE.test(parsed.dshVersion) ? parsed.dshVersion : null
|
|
118
|
+
const createdAt = typeof parsed.createdAt === 'string' && !Number.isNaN(Date.parse(parsed.createdAt)) ? parsed.createdAt : null
|
|
119
|
+
return { dshVersion: version, createdAt }
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
function validateEntry(path, type, data, state) {
|
|
102
123
|
const sections = state.sections
|
|
103
124
|
const parts = path.split('/')
|
|
104
125
|
const root = parts[0]
|
|
105
|
-
if (!['sessions', 'config', 'profiles'].includes(root)) throw domainError('backup-entry-unexpected')
|
|
126
|
+
if (!['sessions', 'config', 'profiles', 'meta'].includes(root)) throw domainError('backup-entry-unexpected')
|
|
106
127
|
if (parts.length === 1 && type !== 'directory') throw domainError('backup-entry-type')
|
|
107
128
|
|
|
108
129
|
if (root === 'sessions') {
|
|
@@ -112,6 +133,17 @@ function validateEntry(path, type, data, state) {
|
|
|
112
133
|
return
|
|
113
134
|
}
|
|
114
135
|
|
|
136
|
+
// meta 分区只容忍 `meta/` 目录本身与唯一一个 `meta/backup.json`:出现别的条目(别名、
|
|
137
|
+
// 子目录、重复)即拒,免得这层「读后即弃」的例外变成绕过条目白名单的后门。
|
|
138
|
+
if (root === 'meta') {
|
|
139
|
+
if (path === 'meta' && type === 'directory') return
|
|
140
|
+
if (path !== META_PATH || type !== 'file') throw domainError('backup-entry-unexpected')
|
|
141
|
+
state.metaSeen = (state.metaSeen ?? 0) + 1
|
|
142
|
+
if (state.metaSeen > 1) throw domainError('backup-entry-duplicate')
|
|
143
|
+
state.metadata = parseBackupMetadata(data)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
if (root === 'config') {
|
|
116
148
|
state.present.add('config')
|
|
117
149
|
if (parts.length === 1) return
|
|
@@ -229,7 +261,10 @@ function parseTar(expanded, options = {}) {
|
|
|
229
261
|
// 归档格式标签:v1 = 不含 Profile 补丁(0.1.7-alpha.1 之前的插件所出);v2 = 含补丁层。
|
|
230
262
|
// 恢复旧归档时据此给出「未包含项」提示而不判损坏。
|
|
231
263
|
const archiveFormat = state.sections.profiles.patchFiles.length > 0 ? 'v2' : 'v1'
|
|
232
|
-
|
|
264
|
+
// 备份时版本信息:`metadata` 为 null 表示该归档未记录(旧插件所出,或元数据非法)。
|
|
265
|
+
// 文件名里的版本段由调用方(index.js 的 backupVersionFromName)解析后随 source 带入,
|
|
266
|
+
// 两层信息在报告里并列,客户端优先信元数据。
|
|
267
|
+
return { entries, sections: state.sections, logicalBytes, entryCount: entries.length, archiveFormat, metadata: state.metadata ?? null }
|
|
233
268
|
}
|
|
234
269
|
|
|
235
270
|
async function inspectArchive(source, options = {}) {
|
|
@@ -242,6 +277,10 @@ async function inspectArchive(source, options = {}) {
|
|
|
242
277
|
status: 'error',
|
|
243
278
|
archive: { entryCount: 0, compressedBytes: source.sizeBytes, logicalBytes: 0 },
|
|
244
279
|
archiveFormat: 'v1',
|
|
280
|
+
// 文件名里的版本段(老归档为 null)与归档内记录的版本(缺 meta 为 null)分开报,
|
|
281
|
+
// 界面优先显示后者;前者是廉价但可能与内容不符的镜像。
|
|
282
|
+
nameVersion: typeof source.nameVersion === 'string' ? source.nameVersion : null,
|
|
283
|
+
metadata: null,
|
|
245
284
|
sections: emptySections(),
|
|
246
285
|
issues: [],
|
|
247
286
|
issueCount: 0,
|
|
@@ -271,6 +310,9 @@ async function inspectArchive(source, options = {}) {
|
|
|
271
310
|
status: 'ok',
|
|
272
311
|
archive: { entryCount: parsed.entryCount, compressedBytes: compressed.length, logicalBytes: parsed.logicalBytes },
|
|
273
312
|
archiveFormat: parsed.archiveFormat,
|
|
313
|
+
// 元数据非法/缺席时退回文件名里的版本段,保证「只要有一处记了版本」就看得见。
|
|
314
|
+
metadata: parsed.metadata ?? null,
|
|
315
|
+
dshVersion: parsed.metadata?.dshVersion ?? (typeof source.nameVersion === 'string' ? source.nameVersion : null),
|
|
274
316
|
sections: parsed.sections,
|
|
275
317
|
},
|
|
276
318
|
parsed,
|
|
@@ -551,7 +593,7 @@ export function createBackupIntegrity(options) {
|
|
|
551
593
|
expiresAt: preparedAt + PLAN_TTL_MS,
|
|
552
594
|
staging,
|
|
553
595
|
sourcePath: source.path,
|
|
554
|
-
source: { id: source.id, name: source.name, sizeBytes: source.sizeBytes, sha256: inspected.report.source.sha256 },
|
|
596
|
+
source: { id: source.id, name: source.name, sizeBytes: source.sizeBytes, sha256: inspected.report.source.sha256, dshVersion: inspected.report.dshVersion ?? null },
|
|
555
597
|
sourceFingerprint: inspected.report.source.sha256,
|
|
556
598
|
targetFingerprint: targetState.fingerprint,
|
|
557
599
|
profileNames,
|
|
@@ -564,6 +606,10 @@ export function createBackupIntegrity(options) {
|
|
|
564
606
|
profiles: profileNames.length,
|
|
565
607
|
profilePatches: patchProfileNames.length,
|
|
566
608
|
archiveFormat: inspected.report.archiveFormat,
|
|
609
|
+
// 备份时的 DSH 版本随计划下发:恢复是不可逆动作,确认前必须让用户看清
|
|
610
|
+
// 「这份快照来自哪个版本」——降级恢复读不动新格式会话,写清楚才不至于误点。
|
|
611
|
+
dshVersion: inspected.report.dshVersion ?? null,
|
|
612
|
+
metadata: inspected.report.metadata ?? null,
|
|
567
613
|
},
|
|
568
614
|
targets: {
|
|
569
615
|
sessions: { action: 'replace', currentBytes: targetState.bytes, newBytes: inspected.report.sections.sessions.bytes },
|
package/backup-routes.js
CHANGED
|
@@ -13,8 +13,10 @@ export function createBackupRoutes({
|
|
|
13
13
|
clearBackupProgress,
|
|
14
14
|
downloadTokens,
|
|
15
15
|
dshHome,
|
|
16
|
+
dshVersion,
|
|
16
17
|
setBackupProgress,
|
|
17
18
|
withBackupLock,
|
|
19
|
+
backupNameVersionSuffix,
|
|
18
20
|
createBackup,
|
|
19
21
|
deleteBackup,
|
|
20
22
|
exportBackup,
|
|
@@ -40,7 +42,9 @@ export function createBackupRoutes({
|
|
|
40
42
|
'backup-create': { feature: 'backupMaintenance', audit: true, handle: async (payload, rpcEndpoint) => {
|
|
41
43
|
try {
|
|
42
44
|
return { ok: true, value: await withBackupLock(() => {
|
|
43
|
-
|
|
45
|
+
// 文件名带备份时的 DSH 版本(`-dsh<version>`):版本号来自宿主进程而非浏览器,
|
|
46
|
+
// 并且只接受通过字符集校验的值——读不出运行版本时省略该段,回落到老命名式。
|
|
47
|
+
const name = `dsh-backup-${formatBackupTimestamp(new Date())}${backupNameVersionSuffix(dshVersion)}.tar.gz`
|
|
44
48
|
const withValidator = async (source, task) => {
|
|
45
49
|
const validator = createBackupIntegrity({ dshHome, resolveBackup: async () => source })
|
|
46
50
|
try { return await task(validator) } finally { await validator.dispose() }
|