@gehennawu/dsh-service 1.9.5 → 1.9.7
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 -6
- package/README.md +6 -6
- package/backup-integrity.js +51 -12
- package/client.js +1 -1
- package/index.js +315 -73
- package/package.json +3 -3
- package/plugin-compat.js +78 -9
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)
|
|
@@ -73,9 +73,9 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
|
|
|
73
73
|
### Version and updates
|
|
74
74
|
|
|
75
75
|
- Shows the current DSH and plugin versions, linking to GitHub Releases
|
|
76
|
-
- Automatically checks npm **stable + preview** (latest / next dist-tags)
|
|
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
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
|
|
78
|
-
- Inline "What's new": the host fetches the release body from the GitHub Releases API and the client renders it in place (plain Markdown — no iframe, no navigation), with the 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
|
|
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
|
|
|
81
81
|
### Safe restart
|
|
@@ -97,7 +97,7 @@ Under **Plugins → Plugin configuration**, twelve host-level switches: **Health
|
|
|
97
97
|
- **Usage index**: reports how many sessions are indexed, when the index was last updated, and how many sessions failed to index (a failed fold silently undercounts usage). Failed sessions are partial-success, so they are a warning rather than a failure and also surface as an overview attention item; an index that has never been built (the Model statistics page has not been opened yet) is informational; the whole check is omitted while the Model usage feature is off (the host only skips refreshing the index in that state, so reporting would be stale and misleading)
|
|
98
98
|
- **Notification permission**: the diagnostics page appends a browser-side check after the host checks — a blocked or not-yet-granted system-notification permission both mean "notifications will not fire", with a pointer to restore it; an unsupported browser is informational. The row stays inside the diagnostics page: it never enters overview attention items or lights the ⚠ (using notifications is a user choice), and it is not rendered at all while Task notifications is off
|
|
99
99
|
- **Plugin health checks**: checks for anomalies only (the official plugin page already provides the full inventory and toggles, so no duplicate list here) — a failed plugin or one waiting on dependencies marks the check as error/warning, while fresh pending/loading fibers receive a short startup grace period; disposed or unknown states are explicit informational rows and do not become warnings. Affected plugins are listed below the check list with clipped, redacted failure text and missing deps; failed plugins can be reloaded behind a two-step confirmation (only entries the host confirmed as failed); manually disabled plugins (built-in or custom) never count as an anomaly
|
|
100
|
-
- **Plugin compatibility**: every enabled plugin is scanned against the interfaces DSH alpha versions have removed or changed (client suppliers, the SQLite persistence backend, chat/status-line style hashes, deprecated attributes) — a hit flags the plugin as "possibly incompatible" with the concrete reason (e.g. "Declares the removed client supplier @deepseek-ai/dsh-client-runtime"). Tiered verdict: only a real `require`/`import` reference is flagged "possibly incompatible"; a supplier that appears only in the manifest (unused in code) falls back to a gray "stale declaration" note — the official loader silently skips missing suppliers, so it is harmless and merely signals the author to clean up; retired slots (e.g. settings.plugin.item)
|
|
100
|
+
- **Plugin compatibility**: every enabled plugin is scanned against the interfaces DSH alpha versions have removed or changed (client suppliers, the SQLite persistence backend, chat/status-line style hashes, deprecated attributes, legacy settings surfaces) — a hit flags the plugin as "possibly incompatible" with the concrete reason (e.g. "Declares the removed client supplier @deepseek-ai/dsh-client-runtime"). Tiered verdict: only a real `require`/`import` reference is flagged "possibly incompatible"; a supplier that appears only in the manifest (unused in code) falls back to a gray "stale declaration" note — the official loader silently skips missing suppliers, so it is harmless and merely signals the author to clean up; retired slots (e.g. settings.plugin.item) and references to removed client services (e.g. settingsScope — callback-shaped references merely degrade silently while only a declarative inject hangs activation; the text scan cannot tell shapes apart and reports the milder tier) are downgraded to a blue "references a retired interface" notice that does not disrupt plugin execution or raise a yellow warning. Before upgrading to alpha or right after doing so, you can see whether third-party plugins kept up; fully local scan with cached results, zero network
|
|
101
101
|
- File-permission deep scan and repair (two-step confirmation) behind a collapsed "Permissions & repair" section
|
|
102
102
|
- Suspected manual launch → yellow "no restart assurance" caution; no backups is informational only and never lights the ⚠
|
|
103
103
|
|
|
@@ -353,7 +353,7 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
353
353
|
|
|
354
354
|
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
355
|
|
|
356
|
-
**DSH compatibility statement**: adapted to DSH `0.1.
|
|
356
|
+
**DSH compatibility statement**: adapted to DSH `0.1.7-alpha.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. This release extends the declared range through `0.1.7-alpha.2`; static structure review found no new code adaptation requirements, but CSS hash stems and several real-host UI behaviors remain unverified—see this release's notes. 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
357
|
|
|
358
358
|
## 🔒 Security design
|
|
359
359
|
|
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)
|
|
@@ -73,9 +73,9 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
|
|
|
73
73
|
### 版本与更新
|
|
74
74
|
|
|
75
75
|
- 显示当前 DSH 与插件版本,链接 GitHub Releases
|
|
76
|
-
- 自动检查 npm **正式版 + 预览版**(latest / next 双 tag
|
|
76
|
+
- 自动检查 npm **正式版 + 预览版**(latest / next 双 tag)
|
|
77
|
+
- 「本次更新内容」入口跟在当前版本号之后且常驻:点开读**当前这一版**的更新说明;有新版本时状态文本「有新版本:x.y.z」整体可点,展开**新版**的 release 信息。正文一律由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带版本号、发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc 即关闭
|
|
77
78
|
- 一键升级,完成后自动重启;未检测到进程管理器时先确认后果,保持运行并提示手动重启
|
|
78
|
-
- 「本次更新内容」行内展开:正文由宿主从 GitHub Releases API 取回后就地渲染(纯 Markdown,不嵌 iframe、不跳转),带发布日期与预发布标记;未建 Release 或读取失败各有明确提示;点版本卡以外的任意位置或按 Esc 即关闭
|
|
79
79
|
- 升级落地但进程尚未重启期间(手动启动环境尤为常见),版本行改示「已安装 X,重启后生效」并收起升级按钮,重开面板或刷新页面状态依旧;重启进程后恢复常态
|
|
80
80
|
|
|
81
81
|
### 安全重启
|
|
@@ -97,7 +97,7 @@ DSH Web 服务控制与运维插件:安全重启、版本管理与一键升级
|
|
|
97
97
|
- **使用统计索引**:报出已索引会话数、索引更新时间与未能索引的会话数(单会话折读失败会让用量静默少算)。失败会话按「部分成功」处理,记警告而非故障,并作为可行动项进概览;尚未建立索引(还没打开过模型统计页)记信息级;「模型统计」开关关闭时整项不检查(宿主在该状态下本就不刷新索引,报了只会是陈旧误导)
|
|
98
98
|
- **通知权限**:诊断页在宿主检查项之后追加一行浏览器侧检查——系统通知被拒绝或尚未授权都意味着「通知不会响」,并给出恢复入口;浏览器不支持时记信息级。该行只留在诊断页内,不进概览可行动项、不点亮标签 ⚠(是否用通知是用户选择),「任务通知」开关关闭时整行不渲染
|
|
99
99
|
- **插件健康检查**:只检查异常(官方插件页已有完整清单与开关,这里不做重复清单)——插件失败或依赖未就绪时检查项报错/警告,刚启动的 pending/loading 有短暂宽限;已释放或未知状态显式标为信息级,不误报为警告。检查清单下方列出异常插件(名称、已脱敏且限长的错误、缺失依赖);失败插件可**两段式确认后重新加载**(只作用于宿主已确认失败的条目);手动停用的插件(无论内置还是自定义)一律不算异常
|
|
100
|
-
- **插件兼容性**:对照 DSH alpha 版本已移除/变更的接口(客户端供应商、SQLite
|
|
100
|
+
- **插件兼容性**:对照 DSH alpha 版本已移除/变更的接口(客户端供应商、SQLite 持久化后端、聊天/统计条样式哈希、弃用属性、设置面旧接口)扫描每个启用插件——命中即提示「可能不兼容」并给出具体原因(如「声明了已移除的客户端供应商 @deepseek-ai/dsh-client-runtime」)。分级判定:**代码真实 require/import 才报「可能不兼容」**;仅在 manifest 声明、代码未引用的供应商降为灰色「仅声明残留」(官方加载器对缺失供应商静默跳过,实际无害,提示作者清理即可);退役槽位(如 settings.plugin.item)、已移除客户端服务的引用(如 settingsScope——回调形态只静默退化、声明式 inject 才挂起激活,文本扫描分不清形态按轻档上报)等降为蓝色「引用已退役接口」提示,不影响插件运行亦不触发黄色警告。升级 alpha 前或刚升级后看一眼就知道第三方插件有没有跟上;纯本地扫描、结果缓存、零网络
|
|
101
101
|
- 文件权限深检与修复(两段式确认)收敛在**默认折叠**的「权限与修复」区(有异常时按钮显示计数)
|
|
102
102
|
- 疑似手动启动 → 黄色警示「重启无保障」;无备份属信息级提示,不点亮 ⚠
|
|
103
103
|
|
|
@@ -355,7 +355,7 @@ pm2 start "dsh web --host 127.0.0.1" --name dsh-web
|
|
|
355
355
|
|
|
356
356
|
运行要求:Node.js `>=22`,DSH Web 能加载 Host 与 Client 两半插件。更新检查需访问 `registry.npmjs.org`;网络失败不影响其他功能。
|
|
357
357
|
|
|
358
|
-
**DSH 适配口径**:已适配 DSH `0.1.
|
|
358
|
+
**DSH 适配口径**:已适配 DSH `0.1.7-alpha.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-alpha.2`;alpha.2 上的静态结构审查未发现新的代码适配需求,但 CSS 哈希词干与若干真机 UI 行为尚未实测,详见本版发布说明。设置面、persistence/布局 seam 均按运行时能力探测走双形态,旧宿主上针对新结构的适配项天然不生效(纯展示,无功能损失)。注意:升级后写入的会话日志无法被旧版 DSH 读取,**备份不可跨版本降级恢复**。插件市场按 `package.json` 的 `engines.dsh` 区间判定兼容性(该字段是唯一的支持口径声明)。
|
|
359
359
|
|
|
360
360
|
## 🔒 安全设计
|
|
361
361
|
|
package/backup-integrity.js
CHANGED
|
@@ -6,6 +6,10 @@ import { promisify } from 'node:util'
|
|
|
6
6
|
import { gunzip } from 'node:zlib'
|
|
7
7
|
|
|
8
8
|
const CONFIG_FILES = Object.freeze(['settings.yaml', 'cordis.patch.yml', 'AGENTS.md', 'dsh-service-config.json'])
|
|
9
|
+
// 每个 profile 允许打包/恢复的常规文件白名单(精确匹配文件名,不许通配或子目录)。
|
|
10
|
+
// `cordis.patch.yml` 是 0.1.7-alpha.1 起的 Profile 用户配置层:漏掉它会出现「备份成功、
|
|
11
|
+
// 恢复后个性化配置全丢」。package.json 承载 bundle 启停清单,两者必须成对。
|
|
12
|
+
const PROFILE_FILES = Object.freeze(['package.json', 'cordis.patch.yml'])
|
|
9
13
|
const PLAN_TTL_MS = 5 * 60 * 1000
|
|
10
14
|
const MAX_COMPRESSED_BYTES = 512 * 1024 * 1024
|
|
11
15
|
const MAX_EXPANDED_BYTES = 1024 * 1024 * 1024
|
|
@@ -90,7 +94,7 @@ function emptySections() {
|
|
|
90
94
|
return {
|
|
91
95
|
sessions: { files: 0, dirs: 0, bytes: 0 },
|
|
92
96
|
config: { files: [], missing: [...CONFIG_FILES], bytes: 0 },
|
|
93
|
-
profiles: { items: [], count: 0, bytes: 0 },
|
|
97
|
+
profiles: { items: [], count: 0, bytes: 0, patchFiles: [] },
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -122,11 +126,18 @@ function validateEntry(path, type, data, state) {
|
|
|
122
126
|
if (parts.length === 1) return
|
|
123
127
|
if (parts[1] === '.' || parts[1] === '..') throw domainError('backup-entry-traversal')
|
|
124
128
|
if (parts.length === 2 && type === 'directory') return
|
|
125
|
-
if (parts.length !== 3 ||
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
if (parts.length !== 3 || type !== 'file' || !PROFILE_FILES.includes(parts[2])) throw domainError('backup-entry-unexpected')
|
|
130
|
+
if (parts[2] === 'package.json') {
|
|
131
|
+
let manifest
|
|
132
|
+
try { manifest = JSON.parse(data.toString('utf8')) } catch (_) { throw domainError('backup-profile-invalid') }
|
|
133
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) throw domainError('backup-profile-invalid')
|
|
134
|
+
sections.profiles.items.push({ name: parts[1], sizeBytes: data.length })
|
|
135
|
+
sections.profiles.bytes += data.length
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
// Profile 补丁:内容是用户手写的 YAML patch 层,此处只记清单与字节,不做语义校验
|
|
139
|
+
// (解析失败应仍可恢复——恢复的是「用户当时的文件」,不是「合法的 patch」)。
|
|
140
|
+
sections.profiles.patchFiles.push({ name: parts[1], sizeBytes: data.length })
|
|
130
141
|
sections.profiles.bytes += data.length
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -213,8 +224,12 @@ function parseTar(expanded, options = {}) {
|
|
|
213
224
|
for (const root of ['sessions', 'config', 'profiles']) if (!state.present.has(root)) throw domainError('backup-section-missing', root)
|
|
214
225
|
state.sections.config.files.sort((a, b) => a.name.localeCompare(b.name))
|
|
215
226
|
state.sections.profiles.items.sort((a, b) => a.name.localeCompare(b.name))
|
|
227
|
+
state.sections.profiles.patchFiles.sort((a, b) => a.name.localeCompare(b.name))
|
|
216
228
|
state.sections.profiles.count = state.sections.profiles.items.length
|
|
217
|
-
|
|
229
|
+
// 归档格式标签:v1 = 不含 Profile 补丁(0.1.7-alpha.1 之前的插件所出);v2 = 含补丁层。
|
|
230
|
+
// 恢复旧归档时据此给出「未包含项」提示而不判损坏。
|
|
231
|
+
const archiveFormat = state.sections.profiles.patchFiles.length > 0 ? 'v2' : 'v1'
|
|
232
|
+
return { entries, sections: state.sections, logicalBytes, entryCount: entries.length, archiveFormat }
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
async function inspectArchive(source, options = {}) {
|
|
@@ -226,6 +241,7 @@ async function inspectArchive(source, options = {}) {
|
|
|
226
241
|
validForRestore: false,
|
|
227
242
|
status: 'error',
|
|
228
243
|
archive: { entryCount: 0, compressedBytes: source.sizeBytes, logicalBytes: 0 },
|
|
244
|
+
archiveFormat: 'v1',
|
|
229
245
|
sections: emptySections(),
|
|
230
246
|
issues: [],
|
|
231
247
|
issueCount: 0,
|
|
@@ -254,6 +270,7 @@ async function inspectArchive(source, options = {}) {
|
|
|
254
270
|
validForRestore: true,
|
|
255
271
|
status: 'ok',
|
|
256
272
|
archive: { entryCount: parsed.entryCount, compressedBytes: compressed.length, logicalBytes: parsed.logicalBytes },
|
|
273
|
+
archiveFormat: parsed.archiveFormat,
|
|
257
274
|
sections: parsed.sections,
|
|
258
275
|
},
|
|
259
276
|
parsed,
|
|
@@ -365,7 +382,9 @@ async function fingerprintTargets(dshHome, profileNames) {
|
|
|
365
382
|
for (const name of profileNames) {
|
|
366
383
|
const profileRoot = join(profilesRoot, name)
|
|
367
384
|
await assertDirectoryOrMissing(profileRoot)
|
|
368
|
-
|
|
385
|
+
// 目标指纹覆盖白名单里的每个文件:漏掉补丁会让「恢复期间用户改了配置」检测不到
|
|
386
|
+
// (指纹相同 → 覆盖写,用户的新改动被旧快照悄悄顶掉)。
|
|
387
|
+
for (const file of PROFILE_FILES) await fingerprintNode(join(profileRoot, file), `profiles/${name}/${file}`, hash, summary)
|
|
369
388
|
}
|
|
370
389
|
return { fingerprint: hash.digest('hex'), bytes: summary.bytes }
|
|
371
390
|
}
|
|
@@ -510,7 +529,13 @@ export function createBackupIntegrity(options) {
|
|
|
510
529
|
if (source === undefined) throw domainError('unknown-backup')
|
|
511
530
|
const inspected = await inspectArchive(source, { collectEntries: true })
|
|
512
531
|
if (!inspected.report.validForRestore || inspected.parsed === null) throw domainError('backup-archive-invalid')
|
|
513
|
-
|
|
532
|
+
// 归档可能带补丁而不带 manifest(外部工具所出):指纹与恢复操作都按并集处理,
|
|
533
|
+
// 否则「只改了补丁的 profile」在 target 变更检测里是盲区。
|
|
534
|
+
const profileNames = [...new Set([
|
|
535
|
+
...inspected.report.sections.profiles.items.map((item) => item.name),
|
|
536
|
+
...inspected.report.sections.profiles.patchFiles.map((item) => item.name),
|
|
537
|
+
])].sort()
|
|
538
|
+
const patchProfileNames = inspected.report.sections.profiles.patchFiles.map((item) => item.name)
|
|
514
539
|
const targetState = await fingerprintTargets(dshHome, profileNames)
|
|
515
540
|
const planId = randomUUID()
|
|
516
541
|
const staging = join(dshHome, 'backups', `.restore-plan-${planId}`)
|
|
@@ -530,19 +555,23 @@ export function createBackupIntegrity(options) {
|
|
|
530
555
|
sourceFingerprint: inspected.report.source.sha256,
|
|
531
556
|
targetFingerprint: targetState.fingerprint,
|
|
532
557
|
profileNames,
|
|
558
|
+
patchProfileNames,
|
|
533
559
|
reportSummary: {
|
|
534
560
|
entryCount: inspected.report.archive.entryCount,
|
|
535
561
|
logicalBytes: inspected.report.archive.logicalBytes,
|
|
536
562
|
sessions: inspected.report.sections.sessions,
|
|
537
563
|
configFiles: inspected.report.sections.config.files.length,
|
|
538
564
|
profiles: profileNames.length,
|
|
565
|
+
profilePatches: patchProfileNames.length,
|
|
566
|
+
archiveFormat: inspected.report.archiveFormat,
|
|
539
567
|
},
|
|
540
568
|
targets: {
|
|
541
569
|
sessions: { action: 'replace', currentBytes: targetState.bytes, newBytes: inspected.report.sections.sessions.bytes },
|
|
542
570
|
config: { replace: inspected.report.sections.config.files.map((item) => item.name), remove: configRemove, newBytes: inspected.report.sections.config.bytes },
|
|
543
|
-
profiles: { upsert: profileNames, untouched: true, newBytes: inspected.report.sections.profiles.bytes },
|
|
571
|
+
profiles: { upsert: profileNames, patches: patchProfileNames, untouched: true, newBytes: inspected.report.sections.profiles.bytes },
|
|
544
572
|
},
|
|
545
|
-
|
|
573
|
+
// 旧归档(v1,不含补丁层)恢复时显式告知「哪些文件没带」,不判损坏、不阻断。
|
|
574
|
+
consequences: ['sessions-replaced', ...(configRemove.length > 0 ? ['config-files-removed'] : []), ...(profileNames.length > 0 ? ['profile-manifests-replaced'] : []), ...(patchProfileNames.length > 0 ? ['profile-patches-replaced'] : ['profile-patches-absent']), 'service-restart-required'],
|
|
546
575
|
previousInstanceId,
|
|
547
576
|
runtime: { supervisorKind: runtimeEnv?.supervisorKind ?? null, manualStartLikely: runtimeEnv?.manualStartLikely === true },
|
|
548
577
|
}
|
|
@@ -588,7 +617,17 @@ export function createBackupIntegrity(options) {
|
|
|
588
617
|
}
|
|
589
618
|
await addOperation(join(dshHome, 'sessions'), join(plan.staging, 'sessions'), 'sessions')
|
|
590
619
|
for (const name of CONFIG_FILES) await addOperation(join(dshHome, name), join(plan.staging, 'config', name), `config/${name}`)
|
|
591
|
-
for (const name of plan.profileNames)
|
|
620
|
+
for (const name of plan.profileNames) {
|
|
621
|
+
await addOperation(join(dshHome, 'profiles', name, 'package.json'), join(plan.staging, 'profiles', name, 'package.json'), `profiles/${name}/package.json`)
|
|
622
|
+
// 补丁层白名单恢复。归档没带(旧版 v1 所出)时**不登记**这一步:登记了就代表
|
|
623
|
+
// 「按快照覆盖」,operation 循环会先把目标侧 patch 挪走、再因 staged 缺席而不回填,
|
|
624
|
+
// 等于用一份对配置毫无意见的旧归档静默删掉用户当前配置。缺席只记入计划报告
|
|
625
|
+
// (consequences: profile-patches-absent),不改成删除。
|
|
626
|
+
const stagedPatch = join(plan.staging, 'profiles', name, 'cordis.patch.yml')
|
|
627
|
+
if (await pathExists(stagedPatch)) {
|
|
628
|
+
await addOperation(join(dshHome, 'profiles', name, 'cordis.patch.yml'), stagedPatch, `profiles/${name}/cordis.patch.yml`)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
592
631
|
const journalPath = join(dshHome, JOURNAL_FILE)
|
|
593
632
|
await mkdir(rollbackDir, { recursive: true, mode: 0o700 })
|
|
594
633
|
await writeJournal(journalPath, { version: 1, rollbackDir, operations })
|