dsh-config-manager 0.1.71 → 0.1.72

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.
Files changed (548) hide show
  1. package/README.md +111 -96
  2. package/README.zh-CN.md +207 -86
  3. package/lib/adapters/agent-instructions.js +1 -1
  4. package/lib/adapters/file-collection.js +1 -1
  5. package/lib/adapters/index.d.ts +1 -1
  6. package/lib/adapters/index.js +1 -1
  7. package/lib/adapters/link-report.d.ts +1 -0
  8. package/lib/adapters/link-report.js +27 -3
  9. package/lib/adapters/messages.d.ts +18 -0
  10. package/lib/adapters/messages.js +50 -0
  11. package/lib/adapters/plugin-files.js +3 -2
  12. package/lib/adapters/plugins.js +4 -3
  13. package/lib/adapters/prompts.d.ts +1 -0
  14. package/lib/adapters/prompts.js +7 -1
  15. package/lib/adapters/self.d.ts +7 -1
  16. package/lib/adapters/self.js +1 -1
  17. package/lib/adapters/sessions.d.ts +1 -0
  18. package/lib/adapters/sessions.js +4 -2
  19. package/lib/adapters/ui.d.ts +2 -2
  20. package/lib/adapters/ui.js +25 -25
  21. package/lib/adapters/workspaces.d.ts +1 -0
  22. package/lib/adapters/workspaces.js +7 -3
  23. package/lib/cli/actions.d.ts +9 -1
  24. package/lib/cli/actions.js +5 -2
  25. package/lib/cli/help.js +23 -10
  26. package/lib/cli/index.d.ts +23 -0
  27. package/lib/cli/index.js +90 -32
  28. package/lib/cli/sessions-inspect.js +3 -1
  29. package/lib/cli/sessions-repair.js +25 -6
  30. package/lib/cli/web/page.d.ts +12 -0
  31. package/lib/cli/web/page.js +21 -12
  32. package/lib/cli/web/routes.js +2 -2
  33. package/lib/cli/web/server.js +4 -4
  34. package/lib/client.d.ts +1349 -490
  35. package/lib/client.js +43047 -36738
  36. package/lib/core/backup-plan.js +30 -17
  37. package/lib/core/backup-verify.js +24 -23
  38. package/lib/core/backup.d.ts +5 -0
  39. package/lib/core/backup.js +51 -25
  40. package/lib/core/boot-rescue.js +4 -3
  41. package/lib/core/boot-safety.js +2 -1
  42. package/lib/core/command-rollback.d.ts +178 -0
  43. package/lib/core/command-rollback.js +289 -0
  44. package/lib/core/consult-source.js +7 -6
  45. package/lib/core/disk-usage.d.ts +74 -10
  46. package/lib/core/disk-usage.js +67 -4
  47. package/lib/core/engine-locale.d.ts +87 -0
  48. package/lib/core/engine-locale.js +125 -0
  49. package/lib/core/exporter.js +3 -3
  50. package/lib/core/fork-reconcile.d.ts +128 -0
  51. package/lib/core/fork-reconcile.js +150 -0
  52. package/lib/core/journal.js +6 -5
  53. package/lib/core/local-plugin-host.js +3 -2
  54. package/lib/core/local-plugin-pack.js +11 -6
  55. package/lib/core/messages-core.d.ts +292 -0
  56. package/lib/core/messages-core.js +615 -0
  57. package/lib/core/messages.d.ts +60 -2
  58. package/lib/core/messages.js +116 -13
  59. package/lib/core/migration-consult.js +21 -20
  60. package/lib/core/migration-history.js +19 -5
  61. package/lib/core/model-tools.d.ts +0 -22
  62. package/lib/core/model-tools.js +50 -27
  63. package/lib/core/phase3-host.js +30 -5
  64. package/lib/core/plugin-cli.js +18 -19
  65. package/lib/core/reconcile.d.ts +2 -14
  66. package/lib/core/reconcile.js +87 -17
  67. package/lib/core/recovery-orchestrator.js +19 -18
  68. package/lib/core/reinstall.d.ts +4 -0
  69. package/lib/core/reinstall.js +26 -21
  70. package/lib/core/restore.js +20 -17
  71. package/lib/core/rollback-ignore-rules.d.ts +31 -0
  72. package/lib/core/rollback-ignore-rules.js +140 -0
  73. package/lib/core/rollback-point.d.ts +1208 -0
  74. package/lib/core/rollback-point.js +2993 -0
  75. package/lib/core/run-registry.js +6 -5
  76. package/lib/core/session-health.d.ts +15 -0
  77. package/lib/core/session-meta.d.ts +42 -2
  78. package/lib/core/session-meta.js +140 -1
  79. package/lib/core/session-title.d.ts +28 -0
  80. package/lib/core/session-title.js +185 -0
  81. package/lib/core/startup-barrier.d.ts +24 -0
  82. package/lib/core/startup-barrier.js +22 -0
  83. package/lib/core/transaction-coordinator.js +4 -3
  84. package/lib/core/types.d.ts +18 -1
  85. package/lib/core/validator.js +5 -4
  86. package/lib/core/verify-recovery.js +31 -30
  87. package/lib/foreign/bundle.js +3 -2
  88. package/lib/foreign/continue.js +2 -1
  89. package/lib/foreign/crush.js +2 -1
  90. package/lib/foreign/gemini.js +2 -1
  91. package/lib/foreign/goose.js +2 -1
  92. package/lib/foreign/kilocode.js +2 -1
  93. package/lib/foreign/kimi.js +2 -1
  94. package/lib/foreign/messages.d.ts +15 -0
  95. package/lib/foreign/messages.js +40 -0
  96. package/lib/foreign/mimocode.js +2 -1
  97. package/lib/foreign/openclaw.js +2 -1
  98. package/lib/foreign/opencode.js +2 -1
  99. package/lib/foreign/pi.js +2 -1
  100. package/lib/foreign/qoder.js +2 -1
  101. package/lib/foreign/qwen.js +2 -1
  102. package/lib/foreign/registry.js +7 -6
  103. package/lib/foreign/teleagent.js +2 -1
  104. package/lib/foreign/workbuddy.js +2 -1
  105. package/lib/foreign/zcode.js +2 -1
  106. package/lib/foreign/zed.js +2 -1
  107. package/lib/index.d.ts +223 -7
  108. package/lib/index.js +729 -54
  109. package/lib/market/git-file-writer.js +20 -19
  110. package/lib/market/github-repos.js +35 -19
  111. package/lib/market/index-parser.js +31 -30
  112. package/lib/market/messages-market.d.ts +138 -0
  113. package/lib/market/messages-market.js +305 -0
  114. package/lib/market/my-repo.d.ts +2 -2
  115. package/lib/market/my-repo.js +32 -29
  116. package/lib/market/prepare.js +18 -15
  117. package/lib/market/security.js +20 -19
  118. package/lib/market/types.js +2 -1
  119. package/lib/market/url.js +2 -1
  120. package/lib/migrations/index.js +6 -5
  121. package/lib/migrations/messages-migrations.d.ts +32 -0
  122. package/lib/migrations/messages-migrations.js +58 -0
  123. package/lib/profiles/dsh-profile-launcher.js +2 -1
  124. package/lib/profiles/dsh-profile-runtime.js +2 -1
  125. package/lib/profiles/messages.d.ts +7 -0
  126. package/lib/profiles/messages.js +27 -0
  127. package/lib/routes/backup.js +18 -2
  128. package/lib/routes/import.js +3 -3
  129. package/lib/routes/index.d.ts +1 -1
  130. package/lib/routes/kit.d.ts +3 -2
  131. package/lib/routes/kit.js +6 -1
  132. package/lib/routes/market.js +1 -1
  133. package/lib/routes/messages.d.ts +42 -0
  134. package/lib/routes/messages.js +100 -0
  135. package/lib/routes/profiles.js +12 -6
  136. package/lib/routes/recovery.js +378 -114
  137. package/lib/routes/snapshots.js +51 -4
  138. package/lib/routes/sync.js +68 -16
  139. package/lib/schema/config.d.ts +11 -1
  140. package/lib/schema/config.js +52 -20
  141. package/lib/schema/index.d.ts +8 -6
  142. package/lib/schema/index.js +8 -6
  143. package/lib/schema/manifest.js +19 -18
  144. package/lib/schema/messages.d.ts +47 -0
  145. package/lib/schema/messages.js +110 -0
  146. package/lib/schema/section-registry.d.ts +41 -14
  147. package/lib/schema/section-registry.js +35 -29
  148. package/lib/schema/versions.js +6 -5
  149. package/lib/security/encryption.js +16 -15
  150. package/lib/security/integrity.js +8 -7
  151. package/lib/security/messages.d.ts +30 -0
  152. package/lib/security/messages.js +77 -0
  153. package/lib/security/redaction.js +12 -0
  154. package/lib/security/secret-scanner.js +16 -13
  155. package/lib/security/vault.js +2 -1
  156. package/lib/security/zip-security.js +4 -3
  157. package/lib/sync/ancestor.js +2 -1
  158. package/lib/sync/autosync-config.d.ts +11 -0
  159. package/lib/sync/autosync-config.js +12 -0
  160. package/lib/sync/autosync-scheduler.d.ts +11 -1
  161. package/lib/sync/autosync-scheduler.js +115 -76
  162. package/lib/sync/backup-files.js +3 -2
  163. package/lib/sync/backup-scheduler.js +6 -5
  164. package/lib/sync/blob-store.js +2 -1
  165. package/lib/sync/git/git-transport.d.ts +55 -0
  166. package/lib/sync/git/git-transport.js +152 -2
  167. package/lib/sync/github-auth.js +10 -9
  168. package/lib/sync/layout.js +13 -12
  169. package/lib/sync/messages.d.ts +63 -0
  170. package/lib/sync/messages.js +149 -0
  171. package/lib/sync/review-queue.js +6 -5
  172. package/lib/sync/s3/messages.d.ts +7 -0
  173. package/lib/sync/s3/messages.js +16 -0
  174. package/lib/sync/s3/s3-providers.d.ts +4 -1
  175. package/lib/sync/s3/s3-providers.js +5 -5
  176. package/lib/sync/s3/s3-transport.js +1 -1
  177. package/lib/sync/s3/sigv4.js +2 -1
  178. package/lib/sync/snapshot-crypto.js +6 -5
  179. package/lib/sync/snapshot-json.js +3 -2
  180. package/lib/sync/sync-engine.d.ts +37 -15
  181. package/lib/sync/sync-engine.js +67 -37
  182. package/lib/sync/sync-history.js +7 -6
  183. package/lib/sync/sync-selection.d.ts +15 -4
  184. package/lib/sync/sync-selection.js +17 -5
  185. package/lib/sync/webdav/webdav-transport.d.ts +1 -0
  186. package/lib/sync/webdav/webdav-transport.js +4 -1
  187. package/lib/ui/artifact-view.js +4 -3
  188. package/lib/ui/commands.d.ts +1 -1
  189. package/lib/ui/commands.js +10 -0
  190. package/lib/ui/disk-usage-view.d.ts +33 -4
  191. package/lib/ui/disk-usage-view.js +29 -1
  192. package/lib/ui/errors.d.ts +1 -1
  193. package/lib/ui/errors.js +4 -3
  194. package/lib/ui/export-flow.js +24 -20
  195. package/lib/ui/foreign-view.d.ts +5 -1
  196. package/lib/ui/foreign-view.js +26 -2
  197. package/lib/ui/i18n.d.ts +65 -0
  198. package/lib/ui/i18n.js +114 -0
  199. package/lib/ui/import-wizard.d.ts +7 -0
  200. package/lib/ui/import-wizard.js +13 -10
  201. package/lib/ui/progress.d.ts +2 -2
  202. package/lib/ui/progress.js +1 -2
  203. package/lib/ui/report.d.ts +5 -5
  204. package/lib/ui/report.js +6 -6
  205. package/lib/ui/rollback-view.d.ts +756 -0
  206. package/lib/ui/rollback-view.js +724 -0
  207. package/lib/ui/selection-model.d.ts +41 -0
  208. package/lib/ui/selection-model.js +30 -0
  209. package/lib/ui/session-inventory-view.d.ts +97 -3
  210. package/lib/ui/session-inventory-view.js +76 -2
  211. package/lib/ui/session-issue-labels.d.ts +43 -0
  212. package/lib/ui/session-issue-labels.js +48 -0
  213. package/lib/ui/sync-settings-view.d.ts +33 -0
  214. package/lib/ui/sync-settings-view.js +40 -0
  215. package/lib/ui/types.d.ts +8 -3
  216. package/lib/ui/types.js +1 -1
  217. package/lib/utils/atomic-write.js +6 -5
  218. package/lib/utils/bundle-scan.d.ts +11 -1
  219. package/lib/utils/bundle-scan.js +12 -1
  220. package/lib/utils/env-lock.d.ts +10 -0
  221. package/lib/utils/env-lock.js +73 -46
  222. package/lib/utils/json.js +6 -5
  223. package/lib/utils/messages-utils.d.ts +96 -0
  224. package/lib/utils/messages-utils.js +216 -0
  225. package/lib/utils/proxy.js +18 -5
  226. package/lib/utils/recursive-walk.d.ts +22 -0
  227. package/lib/utils/recursive-walk.js +75 -1
  228. package/lib/utils/session-log-repair.js +10 -9
  229. package/lib/utils/zip.js +27 -26
  230. package/package.json +12 -2
  231. package/src/adapters/agent-instructions.ts +1 -1
  232. package/src/adapters/file-collection.test.ts +1 -0
  233. package/src/adapters/file-collection.ts +1 -1
  234. package/src/adapters/files.test.ts +27 -0
  235. package/src/adapters/index.ts +1 -1
  236. package/src/adapters/link-report.test.ts +22 -2
  237. package/src/adapters/link-report.ts +29 -4
  238. package/src/adapters/messages.ts +54 -0
  239. package/src/adapters/plugin-files.ts +3 -2
  240. package/src/adapters/plugins.ts +4 -3
  241. package/src/adapters/prompts.ts +7 -1
  242. package/src/adapters/self.test.ts +26 -0
  243. package/src/adapters/self.ts +8 -2
  244. package/src/adapters/sessions.ts +4 -2
  245. package/src/adapters/settings.test.ts +38 -1
  246. package/src/adapters/ui.test.ts +2 -2
  247. package/src/adapters/ui.ts +27 -26
  248. package/src/adapters/workspaces.ts +7 -3
  249. package/src/cli/actions.test.ts +28 -0
  250. package/src/cli/actions.ts +11 -2
  251. package/src/cli/help.ts +23 -10
  252. package/src/cli/index-guards.test.ts +193 -0
  253. package/src/cli/index.ts +106 -31
  254. package/src/cli/sessions-inspect.test.ts +15 -1
  255. package/src/cli/sessions-inspect.ts +3 -1
  256. package/src/cli/sessions-repair.test.ts +84 -0
  257. package/src/cli/sessions-repair.ts +33 -7
  258. package/src/cli/web/console-route-parity.test.ts +115 -0
  259. package/src/cli/web/page.ts +23 -12
  260. package/src/cli/web/routes.ts +2 -1
  261. package/src/cli/web/server.ts +4 -4
  262. package/src/cli/web/web.test.ts +2 -2
  263. package/src/client/ConfigManagerSection.tsx +30 -2
  264. package/src/client/about/AboutPanel.tsx +2 -2
  265. package/src/client/about/PluginUpdateDialog.tsx +1 -1
  266. package/src/client/about/ReleaseNotesDialog.tsx +2 -2
  267. package/src/client/about/about-view.test.ts +13 -3
  268. package/src/client/about/about-view.ts +10 -6
  269. package/src/client/about/release-notes-view.test.ts +14 -4
  270. package/src/client/about/release-notes-view.ts +4 -2
  271. package/src/client/api.ts +18 -6
  272. package/src/client/chat/ChatEntryView.tsx +391 -0
  273. package/src/client/chat/RollbackDiffTab.tsx +61 -0
  274. package/src/client/chat/RollbackFileIcon.tsx +96 -0
  275. package/src/client/chat/RollbackReviewDialog.tsx +591 -0
  276. package/src/client/chat/chat-entry-view.test.ts +1174 -0
  277. package/src/client/chat/chat-entry-view.ts +758 -0
  278. package/src/client/chat/chat-locales.ts +152 -0
  279. package/src/client/chat/chat-node-registration.ts +64 -0
  280. package/src/client/chat/chat-point-window.ts +173 -0
  281. package/src/client/chat/chat-slots.ts +107 -0
  282. package/src/client/chat/rollback-diff-tab.test.ts +105 -0
  283. package/src/client/chat/rollback-diff-tab.ts +164 -0
  284. package/src/client/chat/rollback-entry-view.test.ts +1469 -0
  285. package/src/client/chat/rollback-entry-view.ts +941 -0
  286. package/src/client/chat/sidebar-slots.ts +37 -0
  287. package/src/client/common/ConfirmDialog.tsx +7 -3
  288. package/src/client/common/ContentPicker.tsx +48 -10
  289. package/src/client/common/DiffTable.tsx +97 -0
  290. package/src/client/common/ErrorBanner.tsx +4 -4
  291. package/src/client/common/Icon.tsx +2 -0
  292. package/src/client/common/Modal.tsx +8 -1
  293. package/src/client/common/ProgressBar.tsx +8 -2
  294. package/src/client/common/ReportView.tsx +4 -4
  295. package/src/client/common/RunsCenter.tsx +1 -0
  296. package/src/client/common/client-route-parity.test.ts +78 -0
  297. package/src/client/common/info-hint-guard.test.ts +46 -13
  298. package/src/client/common/picker-lazy-units.test.ts +42 -0
  299. package/src/client/common/plan-text-redaction.test.ts +17 -6
  300. package/src/client/common/progress-view-edge.test.ts +8 -7
  301. package/src/client/common/progress-view.test.ts +9 -9
  302. package/src/client/common/progress-view.ts +2 -2
  303. package/src/client/common/routes.ts +28 -0
  304. package/src/client/common/ui.tsx +73 -10
  305. package/src/client/config-manager.module.css +996 -21
  306. package/src/client/css-scale-guard.test.ts +30 -0
  307. package/src/client/environment/EnvironmentPanel.tsx +11 -4
  308. package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
  309. package/src/client/export/ExportView.tsx +13 -8
  310. package/src/client/history/HistoryPanel.tsx +4 -3
  311. package/src/client/history/history-api.ts +2 -2
  312. package/src/client/history/history-locales.ts +5 -1
  313. package/src/client/home/HomePanel.tsx +29 -8
  314. package/src/client/import/ForeignImportView.tsx +4 -5
  315. package/src/client/import/ImportWizardView.tsx +5 -3
  316. package/src/client/import/import-wizard-split.test.ts +3 -1
  317. package/src/client/import/import-wizard-steps.tsx +9 -9
  318. package/src/client/index.ts +186 -3
  319. package/src/client/library/LibraryActions.tsx +32 -7
  320. package/src/client/library/LibraryPanel.tsx +23 -3
  321. package/src/client/library/RestorePlanView.tsx +9 -66
  322. package/src/client/locales.ts +54 -5
  323. package/src/client/lucide-icons.d.ts +1 -0
  324. package/src/client/market/MarketPanel.tsx +13 -2
  325. package/src/client/market/MyConfigsView.tsx +5 -4
  326. package/src/client/market/MyConfigsWizard.tsx +10 -8
  327. package/src/client/market/market-api.test.ts +9 -8
  328. package/src/client/market/market-api.ts +2 -2
  329. package/src/client/market/market-locales.ts +3 -0
  330. package/src/client/market/market-view.test.ts +6 -6
  331. package/src/client/market/market-view.ts +3 -3
  332. package/src/client/market/my-configs-api.ts +2 -2
  333. package/src/client/market/my-configs-view.test.ts +23 -22
  334. package/src/client/market/my-configs-view.ts +8 -8
  335. package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
  336. package/src/client/recovery/RecoveryPanel.tsx +442 -283
  337. package/src/client/recovery/RollbackCard.tsx +665 -0
  338. package/src/client/recovery/RollbackDiffPane.tsx +58 -0
  339. package/src/client/recovery/RollbackFileChanges.tsx +163 -0
  340. package/src/client/recovery/incident-api.ts +2 -2
  341. package/src/client/recovery/recovery-api.ts +116 -6
  342. package/src/client/recovery/recovery-locales.test.ts +299 -0
  343. package/src/client/recovery/recovery-locales.ts +488 -149
  344. package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
  345. package/src/client/recovery/rollback-file-changes.test.ts +73 -0
  346. package/src/client/recovery/session-dialog-mount.test.ts +54 -0
  347. package/src/client/run-store.ts +84 -21
  348. package/src/client/sync/AutosyncCard.tsx +25 -1
  349. package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
  350. package/src/client/sync/ChannelConfigDialog.tsx +10 -10
  351. package/src/client/sync/DecryptPasswordCard.tsx +2 -2
  352. package/src/client/sync/SecurityOptionsCard.tsx +27 -17
  353. package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
  354. package/src/client/sync/SyncConfirmView.tsx +46 -8
  355. package/src/client/sync/SyncLogList.tsx +1 -1
  356. package/src/client/sync/SyncPanel.tsx +31 -23
  357. package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
  358. package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
  359. package/src/client/sync/SyncSettingsView.test.ts +20 -1
  360. package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
  361. package/src/client/sync/WorkDirCard.tsx +112 -0
  362. package/src/client/sync/history-model.test.ts +42 -9
  363. package/src/client/sync/history-model.ts +30 -15
  364. package/src/client/sync/sync-api.test.ts +57 -33
  365. package/src/client/sync/sync-api.ts +80 -6
  366. package/src/client/sync/sync-locales.ts +104 -3
  367. package/src/client/sync/sync-view-v2.test.ts +65 -13
  368. package/src/client/sync/sync-view.test.ts +107 -52
  369. package/src/client/sync/sync-view.ts +124 -41
  370. package/src/core/backup-plan.test.ts +28 -0
  371. package/src/core/backup-plan.ts +29 -17
  372. package/src/core/backup-verify.ts +24 -23
  373. package/src/core/backup.test.ts +76 -0
  374. package/src/core/backup.ts +55 -25
  375. package/src/core/boot-rescue.ts +4 -3
  376. package/src/core/boot-safety.ts +2 -1
  377. package/src/core/command-rollback.test.ts +536 -0
  378. package/src/core/command-rollback.ts +402 -0
  379. package/src/core/consult-source.ts +7 -6
  380. package/src/core/disk-usage.test.ts +257 -1
  381. package/src/core/disk-usage.ts +134 -14
  382. package/src/core/engine-locale.test.ts +593 -0
  383. package/src/core/engine-locale.ts +175 -0
  384. package/src/core/exporter.ts +3 -3
  385. package/src/core/fork-reconcile.test.ts +279 -0
  386. package/src/core/fork-reconcile.ts +234 -0
  387. package/src/core/journal.ts +6 -5
  388. package/src/core/local-plugin-host.ts +3 -2
  389. package/src/core/local-plugin-pack.ts +11 -6
  390. package/src/core/messages-core.ts +657 -0
  391. package/src/core/messages.ts +123 -17
  392. package/src/core/migration-consult.ts +21 -20
  393. package/src/core/migration-history.ts +19 -5
  394. package/src/core/model-tools.ts +28 -27
  395. package/src/core/phase3-host.ts +21 -5
  396. package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
  397. package/src/core/plugin-cli.ts +18 -19
  398. package/src/core/reconcile.test.ts +117 -0
  399. package/src/core/reconcile.ts +85 -17
  400. package/src/core/recovery-orchestrator.ts +19 -18
  401. package/src/core/reinstall.ts +26 -21
  402. package/src/core/restore.ts +20 -17
  403. package/src/core/rollback-file-diff.test.ts +159 -0
  404. package/src/core/rollback-ignore-rules.ts +126 -0
  405. package/src/core/rollback-point.test.ts +2603 -0
  406. package/src/core/rollback-point.ts +3755 -0
  407. package/src/core/run-registry.ts +6 -5
  408. package/src/core/session-health.ts +15 -0
  409. package/src/core/session-meta.test.ts +117 -1
  410. package/src/core/session-meta.ts +149 -2
  411. package/src/core/session-title.test.ts +127 -0
  412. package/src/core/session-title.ts +189 -0
  413. package/src/core/startup-barrier.test.ts +69 -0
  414. package/src/core/startup-barrier.ts +27 -0
  415. package/src/core/transaction-coordinator.ts +4 -3
  416. package/src/core/types.ts +18 -1
  417. package/src/core/validator.ts +5 -4
  418. package/src/core/verify-recovery.ts +31 -30
  419. package/src/foreign/bundle.ts +3 -2
  420. package/src/foreign/continue.ts +2 -1
  421. package/src/foreign/crush.ts +2 -1
  422. package/src/foreign/file-budget.test.ts +2 -0
  423. package/src/foreign/gemini.ts +2 -1
  424. package/src/foreign/goose.ts +2 -1
  425. package/src/foreign/kilocode.ts +2 -1
  426. package/src/foreign/kimi.ts +2 -1
  427. package/src/foreign/messages.ts +44 -0
  428. package/src/foreign/mimocode.ts +2 -1
  429. package/src/foreign/openclaw.ts +2 -1
  430. package/src/foreign/opencode.ts +2 -1
  431. package/src/foreign/pi.ts +2 -1
  432. package/src/foreign/qoder.ts +2 -1
  433. package/src/foreign/qwen.ts +2 -1
  434. package/src/foreign/registry.ts +7 -6
  435. package/src/foreign/teleagent.ts +2 -1
  436. package/src/foreign/workbuddy.ts +2 -1
  437. package/src/foreign/zcode.ts +2 -1
  438. package/src/foreign/zed.ts +2 -1
  439. package/src/index.facade.test.ts +340 -1
  440. package/src/index.ts +813 -57
  441. package/src/market/git-file-writer.ts +20 -19
  442. package/src/market/github-repos.test.ts +23 -0
  443. package/src/market/github-repos.ts +37 -19
  444. package/src/market/index-parser.ts +31 -30
  445. package/src/market/messages-market.ts +325 -0
  446. package/src/market/my-repo.ts +32 -29
  447. package/src/market/prepare.ts +18 -15
  448. package/src/market/security.ts +20 -19
  449. package/src/market/types.ts +2 -1
  450. package/src/market/url.ts +2 -1
  451. package/src/migrations/index.ts +6 -5
  452. package/src/migrations/messages-migrations.ts +59 -0
  453. package/src/profiles/dsh-profile-launcher.ts +2 -1
  454. package/src/profiles/dsh-profile-runtime.ts +2 -1
  455. package/src/profiles/messages.ts +31 -0
  456. package/src/routes/backup.ts +18 -2
  457. package/src/routes/import.ts +3 -3
  458. package/src/routes/index.ts +1 -1
  459. package/src/routes/kit.ts +9 -3
  460. package/src/routes/market.ts +1 -1
  461. package/src/routes/messages.ts +104 -0
  462. package/src/routes/profiles.ts +12 -6
  463. package/src/routes/recovery.test.ts +405 -1
  464. package/src/routes/recovery.ts +377 -109
  465. package/src/routes/snapshots.ts +51 -4
  466. package/src/routes/sync.ts +67 -16
  467. package/src/schema/config.ts +53 -20
  468. package/src/schema/index.ts +7 -5
  469. package/src/schema/manifest.ts +19 -18
  470. package/src/schema/messages.ts +120 -0
  471. package/src/schema/registry.test.ts +19 -5
  472. package/src/schema/section-registry.ts +64 -39
  473. package/src/schema/versions.ts +6 -5
  474. package/src/security/encryption.ts +16 -15
  475. package/src/security/integrity.ts +8 -7
  476. package/src/security/messages.ts +87 -0
  477. package/src/security/redaction.test.ts +17 -1
  478. package/src/security/redaction.ts +13 -0
  479. package/src/security/secret-scanner.ts +19 -12
  480. package/src/security/security.test.ts +47 -0
  481. package/src/security/vault.ts +2 -1
  482. package/src/security/zip-security.ts +4 -3
  483. package/src/sync/ancestor.ts +2 -1
  484. package/src/sync/autosync-config.ts +21 -0
  485. package/src/sync/autosync-scheduler.test.ts +63 -7
  486. package/src/sync/autosync-scheduler.ts +118 -78
  487. package/src/sync/backup-files.ts +3 -2
  488. package/src/sync/backup-scheduler.ts +6 -5
  489. package/src/sync/blob-store.ts +2 -1
  490. package/src/sync/git/git-transport.test.ts +170 -8
  491. package/src/sync/git/git-transport.ts +176 -2
  492. package/src/sync/github-auth.ts +10 -9
  493. package/src/sync/layout.ts +13 -12
  494. package/src/sync/messages.ts +153 -0
  495. package/src/sync/review-queue.ts +6 -5
  496. package/src/sync/s3/messages.ts +16 -0
  497. package/src/sync/s3/s3-providers.ts +9 -6
  498. package/src/sync/s3/s3-transport.ts +1 -1
  499. package/src/sync/s3/sigv4.ts +3 -1
  500. package/src/sync/snapshot-crypto.ts +6 -5
  501. package/src/sync/snapshot-json.ts +3 -2
  502. package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
  503. package/src/sync/sync-engine.test.ts +226 -8
  504. package/src/sync/sync-engine.ts +103 -41
  505. package/src/sync/sync-history.ts +7 -6
  506. package/src/sync/sync-selection.test.ts +12 -2
  507. package/src/sync/sync-selection.ts +18 -5
  508. package/src/sync/webdav/webdav-transport.ts +4 -1
  509. package/src/ui/artifact-view.ts +4 -3
  510. package/src/ui/commands.test.ts +1 -0
  511. package/src/ui/commands.ts +11 -1
  512. package/src/ui/disk-usage-view.test.ts +113 -6
  513. package/src/ui/disk-usage-view.ts +63 -7
  514. package/src/ui/errors.ts +4 -3
  515. package/src/ui/export-flow.ts +24 -20
  516. package/src/ui/foreign-view.test.ts +43 -0
  517. package/src/ui/foreign-view.ts +30 -3
  518. package/src/ui/i18n.ts +125 -0
  519. package/src/ui/import-wizard.ts +18 -10
  520. package/src/ui/progress.ts +2 -2
  521. package/src/ui/report.ts +7 -6
  522. package/src/ui/rollback-view.test.ts +819 -0
  523. package/src/ui/rollback-view.ts +1339 -0
  524. package/src/ui/selection-model.test.ts +33 -1
  525. package/src/ui/selection-model.ts +53 -0
  526. package/src/ui/session-inventory-view.test.ts +109 -0
  527. package/src/ui/session-inventory-view.ts +138 -3
  528. package/src/ui/session-issue-labels.test.ts +51 -0
  529. package/src/ui/session-issue-labels.ts +70 -0
  530. package/src/ui/sync-settings-view.test.ts +36 -0
  531. package/src/ui/sync-settings-view.ts +49 -0
  532. package/src/ui/types.ts +10 -4
  533. package/src/utils/atomic-write.ts +6 -5
  534. package/src/utils/bundle-scan.test.ts +2 -2
  535. package/src/utils/bundle-scan.ts +12 -1
  536. package/src/utils/env-lock.ts +83 -49
  537. package/src/utils/json.ts +7 -5
  538. package/src/utils/messages-utils.ts +232 -0
  539. package/src/utils/proxy.test.ts +25 -0
  540. package/src/utils/proxy.ts +19 -5
  541. package/src/utils/recursive-walk.test.ts +43 -0
  542. package/src/utils/recursive-walk.ts +75 -1
  543. package/src/utils/session-log-repair.ts +10 -9
  544. package/src/utils/zip.ts +27 -26
  545. package/lib/core/checkpoint.d.ts +0 -362
  546. package/lib/core/checkpoint.js +0 -1309
  547. package/src/core/checkpoint.test.ts +0 -665
  548. package/src/core/checkpoint.ts +0 -1614
@@ -16,6 +16,17 @@ import { buildFileDiffBody, buildRestoreBody, executeRestorePlan, makeRestoreExe
16
16
  import { EnvironmentLockUnavailableError, runWithMutationLock } from '../utils/env-lock.ts'
17
17
  import { isENOENT } from '../utils/guards.ts'
18
18
  import { join } from 'node:path'
19
+ import { createHash } from 'node:crypto'
20
+
21
+ /**
22
+ * 恢复计划指纹(预览-执行一致性):只取「动作类型 + 目标 + blob」三列 —— 描述文案、详情、顺序无关的
23
+ * 易变字段一律不进,保证同一份快照 + 同一磁盘状态下指纹稳定。内容变化不会改变指纹(同 kind/target),
24
+ * 只有**动作集合真的变了**(文件出现/消失、插件增删)才会不同 —— 那正是需要用户重新确认的情况。
25
+ */
26
+ function restorePlanFingerprint(plan: Awaited<ReturnType<typeof planRestore>>): string {
27
+ const lines = plan.actions.map((action) => [action.kind, action.target ?? '', action.blobPath ?? ''].join('\u0000'))
28
+ return createHash('sha256').update(lines.join('\n')).digest('hex')
29
+ }
19
30
 
20
31
  export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
21
32
  const {
@@ -32,7 +43,11 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
32
43
  // M4:列出快照元信息(id/createdAt/sourceZip/status/计数,createdAt 倒序)
33
44
  endpoint({ path: '/api/dsh-config-manager/snapshots', methods: ['GET'] }, async (req, res) => {
34
45
  try {
35
- writeJson(res, 200, { snapshots: await listSnapshots(snapshotsDir) })
46
+ // 「读不到」与「没有」必须分清(core 的 out.unreadable,验收 F2):快照目录被权限/占位挡住时
47
+ // 一律回传事实,界面据此渲染告警 —— 此前这个信号在路由层被丢掉,用户看到的是空列表。
48
+ const out: { unreadable?: string } = {}
49
+ const snapshots = await listSnapshots(snapshotsDir, out)
50
+ writeJson(res, 200, { snapshots, unreadable: out.unreadable ?? null })
36
51
  } catch (error) {
37
52
  writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
38
53
  }
@@ -78,7 +93,8 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
78
93
  // git 风格恢复预览:逐动作的变更状态 + 行数统计(读取有上限,见 core/snapshot-diff.ts)。
79
94
  // summarize 内部逐项兜底、绝不抛错 —— 统计失败不影响计划本身。
80
95
  const changeSummary = await summarizeRestoreChanges({ plan, snapshotDir, homeDir: host.homeDir, msg })
81
- writeJson(res, 200, { dryRun: true, plan, changeSummary })
96
+ // 指纹随预览一起下发:执行时带回来即可发现「确认后磁盘又变了」
97
+ writeJson(res, 200, { dryRun: true, plan, changeSummary, planFingerprint: restorePlanFingerprint(plan) })
82
98
  return
83
99
  }
84
100
  // 真实执行(Phase 2 锁:destructive 必须先获取 GLOBAL 环境锁;被挡 → 423)
@@ -95,6 +111,16 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
95
111
  const runId = run.runId
96
112
  try {
97
113
  const plan = await planRestore(restoreOpts)
114
+ // 预览-执行一致性:客户端带了预览指纹而当前计划与之不符 → 拒绝(409)并让用户重新预览。
115
+ // 为什么必须挡:用户确认的是「还原 A/B」,此刻磁盘已被改动的话,同一份快照可能多出
116
+ // hostFileRemove / fileRemove 这类**破坏性**动作 —— 确认内容与实际执行内容不一致且无人提示。
117
+ const expectedFingerprint = parsed.value.planFingerprint
118
+ if (expectedFingerprint !== undefined && expectedFingerprint !== restorePlanFingerprint(plan)) {
119
+ host.log.warn('恢复被拒:预览后计划已变化', { snapshotId })
120
+ runs.fail(runId, 'plan-changed')
121
+ writeJson(res, 409, { error: 'plan-changed', code: 'plan-changed', runId })
122
+ return
123
+ }
98
124
  const report = await executeRestorePlan(
99
125
  plan,
100
126
  makeRestoreExecutor(snapshotDir, host, host.profile),
@@ -118,7 +144,11 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
118
144
  snapshotId: snapshotId,
119
145
  runId,
120
146
  source: 'api',
121
- summary: `恢复快照 ${snapshotId}:还原 ${report.restored.length} 项${report.removedPlugins.length > 0 ? `,卸载插件 ${report.removedPlugins.length}` : ''}`,
147
+ summary: msg('route.snapshotRestoreSummary', {
148
+ id: snapshotId,
149
+ count: report.restored.length,
150
+ suffix: report.removedPlugins.length > 0 ? msg('route.snapshotRestoreRemovedPluginsSuffix', { count: report.removedPlugins.length }) : '',
151
+ }),
122
152
  })
123
153
  writeJson(res, 200, historyError === undefined ? { dryRun: false, report, runId } : { dryRun: false, report, runId, historyWriteError: historyError })
124
154
  } catch (error) {
@@ -159,6 +189,23 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
159
189
  writeJson(res, 400, { error: 'snapshotId is required and must be a valid snapshot id' })
160
190
  return
161
191
  }
192
+ // Phase 4 F3 同源:被未收敛 journal 引用的快照是 SAFE MODE 恢复的唯一证据 ——
193
+ // 自动保留清理早已豁免它们,手动删除也必须挡住(此前只校验了 id 形状)。
194
+ // 读不到引用集合时**保守拒绝**(与自动 prune 的 fail-closed 同一条口径),并让原因可辨。
195
+ if (host.phase3Recovery !== undefined) {
196
+ let referenced: Set<string>
197
+ try {
198
+ referenced = await host.phase3Recovery.store.listReferencedSnapshotIds()
199
+ } catch (error) {
200
+ host.log.warn('快照删除被拒:读取 recovery 引用集合失败', { error: error instanceof Error ? error.message : String(error) })
201
+ writeJson(res, 409, { error: 'recovery-reference-unreadable', code: 'recovery-reference-unreadable' })
202
+ return
203
+ }
204
+ if (referenced.has(id)) {
205
+ writeJson(res, 409, { error: 'snapshot-referenced-by-recovery', code: 'snapshot-referenced-by-recovery' })
206
+ return
207
+ }
208
+ }
162
209
  const removed = await deleteSnapshot(snapshotsDir, id)
163
210
  const historyError = await tryAppendHistory({
164
211
  kind: 'snapshot-delete',
@@ -166,7 +213,7 @@ export function snapshotRoutes(env: RoutesEnv): WebRoute[] {
166
213
  sections: [id],
167
214
  snapshotId: id,
168
215
  source: 'api',
169
- summary: `删除快照 ${id}`,
216
+ summary: msg('route.snapshotDeleteSummary', { id }),
170
217
  })
171
218
  writeJson(res, 200, historyError === undefined ? { ok: true, removed } : { ok: true, removed, historyWriteError: historyError })
172
219
  } catch (error) {
@@ -118,7 +118,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
118
118
  resolveSyncPassword,
119
119
  scheduler,
120
120
  selectionCache,
121
- selectionHasOptInSections,
121
+ selectionOptInSections,
122
122
  selectionView,
123
123
  selectionViewByChannel,
124
124
  syncCredentialsByChannelView,
@@ -126,6 +126,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
126
126
  syncPasswordConfigured,
127
127
  syncSectionCatalog,
128
128
  syncSessions,
129
+ syncWorkDir,
129
130
  tmpDir,
130
131
  tryAppendHistory,
131
132
  withMutationGate,
@@ -163,6 +164,11 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
163
164
  : undefined
164
165
  // 云端点配置视图(只回 secretStored 布尔;与当前通道无关,配置过即回填)
165
166
  const cloud = await cloudChannelViews(full, credentials)
167
+ // issue #79:git 工作副本是否已跟随 repoUrl(只读自检,2 条本地只读 git 命令)。
168
+ // 读不出来时**如实回传错误**(workDirError),不让状态接口静默少一块信息。
169
+ const gitRepoUrl = full?.git?.repoUrl ?? ''
170
+ let workDir: Awaited<ReturnType<typeof syncWorkDir.inspect>> | null = null
171
+ if (gitRepoUrl !== '') workDir = await syncWorkDir.inspect(gitRepoUrl)
166
172
  writeJson(res, 200, {
167
173
  ok: true,
168
174
  configured: full !== null,
@@ -194,11 +200,44 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
194
200
  autosync: await buildAutosyncStatus(syncDir, transport),
195
201
  // 全部通道的自动同步状态(git/webdav 各自独立;UI 按当前 tab 取对应通道)
196
202
  autosyncByChannel: await buildAutosyncStatusByChannel(syncDir),
203
+ // 工作副本事实(仅 git 且配置了地址时返回;'facts' = 自检成功,'error' = 自检读不出来)
204
+ ...(workDir === null ? {} : { workDir }),
197
205
  })
198
206
  } catch (error) {
199
207
  writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
200
208
  }
201
209
  }),
210
+ // ------------------------------------------------------ sync/workdir/reset
211
+ // issue #79:工作副本的自救入口(改造前 App 内完全没有 —— 改了仓库地址只能手工
212
+ // git remote set-url,或删掉 <dataDir>/sync/work 重来)。两种模式,破坏性清清楚楚:
213
+ // relink = 只把 origin / push URL 重指向当前配置地址(非破坏性:不碰工作树与本地提交);
214
+ // reclone = 删除工作副本目录后重新 clone(**丢弃未上行的本地提交**)→ 必须显式 confirm。
215
+ // 写动作 → 过 mutation gate(SAFE MODE / 环境锁);删除边界见 GitTransport.rebuildWorkDir
216
+ // (目录非空且无 .git 一律拒绝,绝不删来路不明的目录)。
217
+ endpoint({ path: '/api/dsh-config-manager/sync/workdir/reset', methods: ['POST'] }, withMutationGate('sync-workdir-reset', async (req, res) => {
218
+ try {
219
+ const body = await requireJsonObject(req)
220
+ const mode = body['mode'] === undefined ? 'relink' : body['mode']
221
+ if (mode !== 'relink' && mode !== 'reclone') {
222
+ writeJson(res, 400, { error: msg('sync.workdirModeInvalid', { mode: JSON.stringify(body['mode']) }) })
223
+ return
224
+ }
225
+ if (mode === 'reclone' && body['confirm'] !== true) {
226
+ writeJson(res, 400, { error: msg('sync.workdirRecloneNeedsConfirm') })
227
+ return
228
+ }
229
+ const full = await readFullSyncConfig(syncDir)
230
+ const repoUrl = full?.git?.repoUrl ?? ''
231
+ if (repoUrl === '') {
232
+ writeJson(res, 400, { error: msg('sync.workdirNotConfigured') })
233
+ return
234
+ }
235
+ const rebuilt = await syncWorkDir.rebuild(repoUrl, mode)
236
+ writeJson(res, 200, { ok: true, mode, workDir: { facts: rebuilt } })
237
+ } catch (error) {
238
+ writeSyncRouteError(res, error)
239
+ }
240
+ })),
202
241
  // ------------------------------------------------------ sync/config
203
242
  // m-sync-config:保存同步通道配置(parseSyncBody 校验 + password/token 写 DSH credentials +
204
243
  // writeSyncConfig 落盘)。UI 表单自动保存 /「保存配置」按钮调用;响应为轻量状态视图
@@ -295,7 +334,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
295
334
  const body = await requireJsonObject(req)
296
335
  try {
297
336
  const syncCfg = await prepareSync(body)
298
- const engine = makeSyncEngine(syncCfg)
337
+ // 用户驱动的推送:放行持久化选择里勾选的可选分区(mcp / sessions),否则它们按非 portable 跳过
338
+ const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
299
339
  const snapshotId =
300
340
  typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
301
341
  const sections = extractSyncSections(body, knownSyncSectionIds)
@@ -311,7 +351,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
311
351
  // 因为那是用户此刻输入的覆盖值;两者都没有 → 交给 engine 报「加密需要密码」。
312
352
  const pushChannel: SyncTransportType = channelOf(syncCfg)
313
353
  const effectiveEncryptPassword = encryptPassword ?? await resolveSyncPassword(syncPasswordRef('ENCRYPT', pushChannel))
314
- // 可选分区选项(历史会话「最新 N 个」):只有显式提供才允许 sessions 进同步通道
354
+ // sessions 的导出参数(「最新 N 个」/ 逐会话点名);**放行**由引擎的 optInSections 决定
315
355
  const sessions = extractSyncSessions(body)
316
356
  const sessionsOpt = sections !== undefined && sections.includes('sessions') && sessions !== undefined
317
357
  ? { sessions }
@@ -352,8 +392,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
352
392
  const body = await requireJsonObject(req)
353
393
  try {
354
394
  const syncCfg = await prepareSync(body)
355
- // 用户驱动的拉取:选择里勾了 sessions 时才让会话分区进入差异计划(自动同步不传)
356
- const engine = makeSyncEngine(syncCfg, { includeOptInSections: selectionHasOptInSections(channelOf(syncCfg)) })
395
+ // 用户驱动的拉取:放行持久化选择里勾选的**全部**可选分区(自动同步只放行 'auto' 档)
396
+ const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
357
397
  const strategy =
358
398
  body['strategy'] === 'replace' || body['strategy'] === 'skipExisting' ? body['strategy'] : 'merge'
359
399
  const snapshotId =
@@ -616,7 +656,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
616
656
  const body = await requireJsonObject(req)
617
657
  try {
618
658
  const syncCfg = await prepareSync(body)
619
- const engine = makeSyncEngine(syncCfg, { includeOptInSections: selectionHasOptInSections(channelOf(syncCfg)) })
659
+ const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
620
660
  const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
621
661
  // 可选落地文件名(产物库「下载」要给用户一个可辨识的名字,而不是固定的 snapshot.zip)。
622
662
  // 不安全的名字直接 400 而不是「静默换名」:它会进 path.join(dir, name),白名单是唯一防线。
@@ -708,8 +748,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
708
748
  const body = await requireJsonObject(req)
709
749
  try {
710
750
  const syncCfg = await prepareSync(body)
711
- // 用户驱动的一键同步:选择里勾了 sessions 时才让会话分区进入差异计划(自动同步不传)
712
- const engine = makeSyncEngine(syncCfg, { includeOptInSections: selectionHasOptInSections(channelOf(syncCfg)) })
751
+ // 用户驱动的一键同步:放行持久化选择里勾选的**全部**可选分区(自动同步只放行 'auto' 档)
752
+ const engine = makeSyncEngine(syncCfg, { optInSections: selectionOptInSections(channelOf(syncCfg)) })
713
753
  const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined
714
754
  // 解密密码:请求体 > 已保存(DSH credentials)。明文快照根本不会被解密
715
755
  // (engine.prepareSnapshot 只在 manifest.encrypted 时才用密码),因此「未加密备份
@@ -731,7 +771,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
731
771
  msg('host.syncPullTimeout'),
732
772
  )
733
773
  if (!preview.ok || preview.plan === null || preview.analysis === null) {
734
- writeJson(res, 200, { ok: false, syncSessionId: '', snapshotId: preview.snapshotId, items: [], needsReview: false, compatibility: 'unsupported', message: preview.message ?? '同步预览失败' })
774
+ writeJson(res, 200, { ok: false, syncSessionId: '', snapshotId: preview.snapshotId, items: [], needsReview: false, compatibility: 'unsupported', message: preview.message ?? msg('route.syncPreviewFailed') })
735
775
  return
736
776
  }
737
777
  // T1:abort 处置在**预览阶段**就阻断(此刻仍零写入:临时 ZIP 与内存会话都还没登记)。
@@ -777,7 +817,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
777
817
  const syncSessionId = typeof body['syncSessionId'] === 'string' ? body['syncSessionId'] : ''
778
818
  const session = syncSessions.get(syncSessionId)
779
819
  if (session === undefined) {
780
- writeJson(res, 400, { error: '同步会话不存在或已过期,请重新拉取预览' })
820
+ writeJson(res, 400, { error: msg('route.syncSessionExpired') })
781
821
  return
782
822
  }
783
823
  // T1:abort 处置在**应用前**再判一次(客户端可能绕过 preview 直接 apply,或预览后改了处置)。
@@ -804,7 +844,7 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
804
844
  if (d === undefined || !d.adopt) return []
805
845
  // Conflict 项必须有 resolution;keepLocal/skip 不写入本地 → 剔除
806
846
  if (item.kind === 'Conflict') {
807
- if (d.resolution === undefined) throw new SyncRouteError(`冲突项 ${item.id} 必须提供 resolution(useRemote/keepLocal/skip)`)
847
+ if (d.resolution === undefined) throw new SyncRouteError(msg('route.syncConflictResolutionRequired', { id: item.id }))
808
848
  if (d.resolution === 'keepLocal' || d.resolution === 'skip') return []
809
849
  // useRemote → 转成 Update 计划项(镜像 analyzer.applyItemResolution 的
810
850
  // useImported 分支),applyOne 才会把远端值真正写进本地。
@@ -850,10 +890,14 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
850
890
  source: 'api',
851
891
  // issue #56:历史如实区分「全部成功 / 部分成功(N 项未生效)」——
852
892
  // 此前 ok:true 一律写「N 项」+ 无 error,用户在历史里看到的就是一次干净的成功。
853
- summary: `一键同步应用:${(Array.isArray(report.applied) ? report.applied.length : subItems.length)} 项${report.rolledBack === true ? '(已回滚)' : ''}${report.ineffective.length > 0 ? `(${report.ineffective.length} 项未生效)` : ''}`,
893
+ summary: msg('route.syncApplySummary', {
894
+ count: Array.isArray(report.applied) ? report.applied.length : subItems.length,
895
+ status: report.rolledBack === true ? (report.rollbackFull === false ? msg('route.syncRollbackIncomplete') : msg('route.syncRolledBack')) : '',
896
+ ineffective: report.ineffective.length > 0 ? msg('route.syncIneffectiveSuffix', { count: report.ineffective.length }) : '',
897
+ }),
854
898
  error: report.ok
855
- ? (report.ineffective.length > 0 ? '同步应用部分成功:有项未生效(见告警明细)' : undefined)
856
- : '同步应用未完全成功',
899
+ ? (report.ineffective.length > 0 ? msg('route.syncApplyPartial') : undefined)
900
+ : msg('route.syncApplyFailed'),
857
901
  })
858
902
  writeJson(res, 200, historyError === undefined ? {
859
903
  ok: report.ok,
@@ -863,6 +907,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
863
907
  warnings: report.warnings,
864
908
  restoreId: report.restoreId,
865
909
  rolledBack: report.rolledBack,
910
+ // 诚实性(2026-10):整体回滚是否完整(false = 有项没恢复,失败项已并入 warnings)
911
+ ...(report.rollbackFull === undefined ? {} : { rollbackFull: report.rollbackFull }),
866
912
  failed: report.failed,
867
913
  // issue #56:未生效项(warning)单独回传,界面据此把「成功」降级为「部分成功」
868
914
  ineffective: report.ineffective,
@@ -875,6 +921,8 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
875
921
  warnings: report.warnings,
876
922
  restoreId: report.restoreId,
877
923
  rolledBack: report.rolledBack,
924
+ // 诚实性(2026-10):整体回滚是否完整(false = 有项没恢复,失败项已并入 warnings)
925
+ ...(report.rollbackFull === undefined ? {} : { rollbackFull: report.rollbackFull }),
878
926
  failed: report.failed,
879
927
  ineffective: report.ineffective,
880
928
  result: report.result,
@@ -1019,9 +1067,12 @@ export function syncRoutes(env: RoutesEnv): WebRoute[] {
1019
1067
  sections: [],
1020
1068
  snapshotId: restoreId,
1021
1069
  source: 'api',
1022
- summary: `一键同步回滚(${restoreId})`,
1070
+ summary: msg('route.syncRollbackSummary', { id: restoreId }),
1023
1071
  })
1024
- writeJson(res, 200, historyError === undefined ? { ok: true, full: report.full } : { ok: true, full: report.full, historyWriteError: historyError })
1072
+ // 诚实回传:full=false 时把 failed(含 manualHint)一并给出,界面据此列出「哪些项没回滚、
1073
+ // 需要人工补录」而不是无条件报「已回滚」。CLI 侧同口径见 src/cli/actions.ts 的 rollback 文案。
1074
+ const payload = { ok: true, full: report.full, failed: report.failed }
1075
+ writeJson(res, 200, historyError === undefined ? payload : { ...payload, historyWriteError: historyError })
1025
1076
  } catch (error) {
1026
1077
  writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
1027
1078
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { parseJsonSafe } from '../utils/json.ts';
10
10
  import { isPathSafe } from '../utils/paths.ts';
11
- import { sectionMetaOf } from './section-registry.ts';
11
+ import { schemaMsg } from './messages.ts';
12
+ import { sectionMetaOf, type SectionMeta } from './section-registry.ts';
12
13
  import { sectionDataVersionIssue } from './versions.ts';
13
14
  import type { SectionData, SectionId } from './types.ts';
14
15
 
@@ -22,7 +23,6 @@ export {
22
23
  SECTION_DATA_VERSION,
23
24
  sectionMeta,
24
25
  sectionMetaOf,
25
- requireSectionMeta,
26
26
  isSectionId,
27
27
  jsonPathOf,
28
28
  filePrefixOf,
@@ -36,13 +36,29 @@ export type {
36
36
  SectionPortability,
37
37
  } from './section-registry.ts';
38
38
 
39
+ /**
40
+ * 拿不到元数据就**显式失败**(绝不让未注册分区按其它分区语义继续)。
41
+ * core 侧修正「未知分区静默记成 settingsNamespace」时用它替代 default 分支。
42
+ *
43
+ * t7:本函数原在 `section-registry.ts`;该模块受「只 import 一个类型」的零依赖铁律约束
44
+ * (client bundle 侧 `ui/export-flow.ts` 也在用它),故把唯一的用户可见文案抛出点迁到本文件
45
+ * (已持有本域消息目录)。public surface 不变:`config.ts` 与 `schema/index.ts` 仍照旧导出它。
46
+ */
47
+ export function requireSectionMeta(id: string): SectionMeta {
48
+ const meta = sectionMetaOf(id);
49
+ if (meta === null) {
50
+ throw new Error(schemaMsg('schema.sectionNotRegistered', { section: id }));
51
+ }
52
+ return meta;
53
+ }
54
+
39
55
  /** 从 ZIP 内 JSON 解析分区数据(深度保护 + 结构校验) */
40
56
  export function parseSectionJson<T extends SectionData>(sectionId: SectionId, raw: string): T {
41
57
  const parsed = parseJsonSafe(raw) as T;
42
58
  const issues = validateSectionData(sectionId, parsed);
43
59
  const errors = issues.filter((i) => i.severity === 'error');
44
60
  if (errors.length > 0) {
45
- throw new Error(`分区 ${sectionId} 数据无效: ${errors.map((e) => e.message).join('; ')}`);
61
+ throw new Error(schemaMsg('schema.sectionDataInvalid', { section: sectionId, issues: errors.map((e) => e.message).join('; ') }));
46
62
  }
47
63
  return parsed;
48
64
  }
@@ -60,11 +76,11 @@ export interface SectionIssue { path: string; message: string; severity: 'error'
60
76
  export function validateSectionData(sectionId: SectionId, data: unknown): SectionIssue[] {
61
77
  const issues: SectionIssue[] = [];
62
78
  if (data === null || typeof data !== 'object') {
63
- return [{ path: '$', message: `分区 ${sectionId} 数据必须是对象`, severity: 'error' }];
79
+ return [{ path: '$', message: schemaMsg('schema.sectionDataMustBeObject', { section: sectionId }), severity: 'error' }];
64
80
  }
65
81
  const meta = sectionMetaOf(sectionId);
66
82
  if (meta === null) {
67
- return [{ path: '$', message: `未注册分区 ${String(sectionId)}:拒绝校验(新增分区须在 schema/section-registry.ts 注册)`, severity: 'error' }];
83
+ return [{ path: '$', message: schemaMsg('schema.sectionUnregistered', { section: String(sectionId) }), severity: 'error' }];
68
84
  }
69
85
  const obj = data as Record<string, unknown>;
70
86
  const versionIssue = sectionDataVersionIssue(meta.id, obj['version']);
@@ -77,32 +93,32 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
77
93
  case 'namespaces': {
78
94
  const ns = obj['namespaces'];
79
95
  if (ns === null || typeof ns !== 'object') {
80
- issues.push({ path: 'namespaces', message: `分区 ${meta.id} 缺少 namespaces 对象`, severity: 'error' });
96
+ issues.push({ path: 'namespaces', message: schemaMsg('schema.namespacesMissing', { section: meta.id }), severity: 'error' });
81
97
  } else {
82
98
  for (const [name, rec] of Object.entries(ns as Record<string, unknown>)) {
83
99
  if (rec === null || typeof rec !== 'object') {
84
- issues.push({ path: `namespaces.${name}`, message: 'namespace 记录必须是对象', severity: 'error' });
100
+ issues.push({ path: `namespaces.${name}`, message: schemaMsg('schema.namespaceRecordMustBeObject'), severity: 'error' });
85
101
  continue;
86
102
  }
87
103
  const r = rec as Record<string, unknown>;
88
- if (typeof r['revision'] !== 'number') issues.push({ path: `namespaces.${name}.revision`, message: 'revision 必须是数字', severity: 'error' });
89
- if (!('value' in r)) issues.push({ path: `namespaces.${name}.value`, message: '缺少 value', severity: 'error' });
104
+ if (typeof r['revision'] !== 'number') issues.push({ path: `namespaces.${name}.revision`, message: schemaMsg('schema.revisionMustBeNumber'), severity: 'error' });
105
+ if (!('value' in r)) issues.push({ path: `namespaces.${name}.value`, message: schemaMsg('schema.valueMissing'), severity: 'error' });
90
106
  const secrets = r['secrets'];
91
- if (secrets !== undefined && !Array.isArray(secrets)) issues.push({ path: `namespaces.${name}.secrets`, message: 'secrets 必须是数组', severity: 'error' });
107
+ if (secrets !== undefined && !Array.isArray(secrets)) issues.push({ path: `namespaces.${name}.secrets`, message: schemaMsg('schema.secretsMustBeArray'), severity: 'error' });
92
108
  }
93
109
  }
94
110
  break;
95
111
  }
96
112
  case 'object': {
97
113
  const value = obj[payload.key];
98
- if (value === null || typeof value !== 'object') issues.push({ path: payload.key, message: `缺少 ${payload.key} 对象`, severity: 'error' });
114
+ if (value === null || typeof value !== 'object') issues.push({ path: payload.key, message: schemaMsg('schema.objectMissing', { key: payload.key }), severity: 'error' });
99
115
  break;
100
116
  }
101
117
  case 'array': {
102
- if (!Array.isArray(obj[payload.key])) issues.push({ path: payload.key, message: `${payload.key} 必须是数组`, severity: 'error' });
118
+ if (!Array.isArray(obj[payload.key])) issues.push({ path: payload.key, message: schemaMsg('schema.keyMustBeArray', { key: payload.key }), severity: 'error' });
103
119
  for (const extra of payload.extraArrayKeys ?? []) {
104
120
  if (obj[extra] !== undefined && !Array.isArray(obj[extra])) {
105
- issues.push({ path: extra, message: `${extra} 必须是数组`, severity: 'error' });
121
+ issues.push({ path: extra, message: schemaMsg('schema.keyMustBeArray', { key: extra }), severity: 'error' });
106
122
  }
107
123
  }
108
124
  if (payload.validatePatchFiles === true) {
@@ -111,20 +127,20 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
111
127
  const patchFiles = obj['patchFiles'];
112
128
  if (patchFiles !== undefined) {
113
129
  if (!Array.isArray(patchFiles)) {
114
- issues.push({ path: 'patchFiles', message: 'patchFiles 必须是数组', severity: 'error' });
130
+ issues.push({ path: 'patchFiles', message: schemaMsg('schema.keyMustBeArray', { key: 'patchFiles' }), severity: 'error' });
115
131
  } else {
116
132
  patchFiles.forEach((pf, i) => {
117
133
  const rec = (pf !== null && typeof pf === 'object') ? pf as Record<string, unknown> : null;
118
134
  if (rec === null) {
119
- issues.push({ path: `patchFiles[${i}]`, message: 'patch 文件条目必须是对象', severity: 'error' });
135
+ issues.push({ path: `patchFiles[${i}]`, message: schemaMsg('schema.patchFileEntryMustBeObject'), severity: 'error' });
120
136
  return;
121
137
  }
122
138
  const rel = rec['relativePath'];
123
139
  if (typeof rel !== 'string' || rel === '' || !isPathSafe(rel)) {
124
- issues.push({ path: `patchFiles[${i}].relativePath`, message: 'patch 文件路径必须是安全的相对路径(不得为绝对路径或含 ..)', severity: 'error' });
140
+ issues.push({ path: `patchFiles[${i}].relativePath`, message: schemaMsg('schema.patchFilePathUnsafe'), severity: 'error' });
125
141
  }
126
142
  if (typeof rec['base64'] !== 'string') {
127
- issues.push({ path: `patchFiles[${i}].base64`, message: 'patch 文件内容必须是 base64 字符串', severity: 'error' });
143
+ issues.push({ path: `patchFiles[${i}].base64`, message: schemaMsg('schema.patchFileContentMustBeBase64'), severity: 'error' });
128
144
  }
129
145
  });
130
146
  }
@@ -133,14 +149,31 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
133
149
  break;
134
150
  }
135
151
  case 'files': {
136
- if (!Array.isArray(obj['files'])) issues.push({ path: 'files', message: 'files 必须是数组', severity: 'error' });
152
+ const files = obj['files'];
153
+ if (!Array.isArray(files)) {
154
+ issues.push({ path: 'files', message: schemaMsg('schema.keyMustBeArray', { key: 'files' }), severity: 'error' });
155
+ break;
156
+ }
157
+ // 与 patchFiles 同级的防线(此前只判了 Array.isArray):relativePath 会被写到 <home> 之下,
158
+ // 不可信 bundle 的路径穿越向量必须逐项过 isPathSafe(不得为绝对路径 / 含 ..)。
159
+ files.forEach((entry, i) => {
160
+ const rec = (entry !== null && typeof entry === 'object') ? entry as Record<string, unknown> : null;
161
+ if (rec === null) {
162
+ issues.push({ path: `files[${i}]`, message: schemaMsg('schema.fileEntryMustBeObject'), severity: 'error' });
163
+ return;
164
+ }
165
+ const rel = rec['relativePath'];
166
+ if (typeof rel !== 'string' || rel === '' || !isPathSafe(rel)) {
167
+ issues.push({ path: `files[${i}].relativePath`, message: schemaMsg('schema.filePathUnsafe'), severity: 'error' });
168
+ }
169
+ });
137
170
  break;
138
171
  }
139
172
  case 'none': {
140
173
  // secrets:凭据值走独立加密容器(.credentials.yaml / secrets.enc),没有分区 JSON 可校验
141
174
  issues.push({
142
175
  path: '$',
143
- message: `分区 ${meta.id} 无 JSON 载荷(凭据值走独立加密容器,不参与分区 JSON 校验)`,
176
+ message: schemaMsg('schema.sectionNoJsonPayload', { section: meta.id }),
144
177
  severity: 'error',
145
178
  });
146
179
  break;
@@ -148,7 +181,7 @@ export function validateSectionData(sectionId: SectionId, data: unknown): Sectio
148
181
  default: {
149
182
  // 判别联合已穷尽:新增 payload.kind 时此赋值会编译失败(提醒补校验分支)
150
183
  const exhaustive: never = payload;
151
- issues.push({ path: '$', message: `未处理的分区载荷形态 ${JSON.stringify(exhaustive)}`, severity: 'error' });
184
+ issues.push({ path: '$', message: schemaMsg('schema.unhandledPayloadKind', { payload: JSON.stringify(exhaustive) }), severity: 'error' });
152
185
  }
153
186
  }
154
187
  return issues;
@@ -19,10 +19,12 @@
19
19
  * 本文件把 `./schema` 变成真实可用的运行时入口。
20
20
  *
21
21
  * 约束(勿破):本文件只 import 同目录模块(`./config.ts` / `./section-registry.ts` / `./types.ts` /
22
- * `./versions.ts`)。`section-registry.ts` 自身只 import 一个**类型**(`type { SectionId }`),
23
- * 故本出口仍保持零 `node:` / 零 npm 依赖。
24
- * 不 import `../core/*`、不 import `node:*`、不 import 任何 UI / DSH 包——否则会破坏
25
- * headless 消费(见 `docs/spec/headless-consumption.md`)与 `tests/architecture-boundaries.test.ts`。
22
+ * `./versions.ts`)。`section-registry.ts` 自身仍然只 import 一个**类型**(`type { SectionId }`;
23
+ * 未注册分区的错误文案 t7 起在 `config.ts`,见那里)。
24
+ * 目标性质是**零 `node:` / 零 npm / 零 @deepseek-ai 依赖**(headless 消费:
25
+ * `docs/spec/headless-consumption.md`;client bundle 自包含)。唯一的跨层依赖是本域消息目录
26
+ * (`./messages.ts` → `../core/engine-locale.ts`,纯函数、零 node / 零 npm),不破坏该性质;
27
+ * 除此之外不得 import `../core/*`、`node:*` 或任何 UI / DSH 包。
26
28
  */
27
29
 
28
30
  /* —— 版本判定(唯一出口,业务代码零散落 `if (v === 1)`) —— */
@@ -44,6 +46,7 @@ export {
44
46
  SECTION_JSON_PATHS,
45
47
  SECTION_FILE_PREFIXES,
46
48
  isFileSection,
49
+ requireSectionMeta,
47
50
  } from './config.ts';
48
51
 
49
52
  /*
@@ -57,7 +60,6 @@ export {
57
60
  SECTION_DATA_VERSION,
58
61
  sectionMeta,
59
62
  sectionMetaOf,
60
- requireSectionMeta,
61
63
  isSectionId,
62
64
  jsonPathOf,
63
65
  filePrefixOf,
@@ -6,6 +6,7 @@ import { parseJsonSafe } from '../utils/json.ts';
6
6
  import { CURRENT_SCHEMA_VERSION } from './versions.ts';
7
7
  import type { Manifest, Platform, SectionId } from './types.ts';
8
8
  import { SECTION_IDS } from './config.ts';
9
+ import { schemaMsg } from './messages.ts';
9
10
 
10
11
  export const MANIFEST_FILE = 'manifest.json';
11
12
  export const CHECKSUMS_FILE = 'integrity/checksums.json';
@@ -57,7 +58,7 @@ export function parseManifest(raw: string): Manifest {
57
58
  const issues = validateManifest(parsed);
58
59
  const errors = issues.filter((i) => i.severity === 'error');
59
60
  if (errors.length > 0) {
60
- throw new Error(`manifest.json 无效: ${errors.map((e) => e.message).join('; ')}`);
61
+ throw new Error(schemaMsg('schema.manifestInvalid', { issues: errors.map((e) => e.message).join('; ') }));
61
62
  }
62
63
  return parsed as Manifest;
63
64
  }
@@ -68,58 +69,58 @@ export interface ManifestIssue { path: string; message: string; severity: 'error
68
69
  export function validateManifest(m: unknown): ManifestIssue[] {
69
70
  const issues: ManifestIssue[] = [];
70
71
  if (m === null || typeof m !== 'object') {
71
- return [{ path: '$', message: 'manifest 必须是对象', severity: 'error' }];
72
+ return [{ path: '$', message: schemaMsg('schema.manifestMustBeObject'), severity: 'error' }];
72
73
  }
73
74
  const obj = m as Record<string, unknown>;
74
75
 
75
76
  if (typeof obj['schemaVersion'] !== 'number') {
76
- issues.push({ path: 'schemaVersion', message: 'schemaVersion 必须是数字', severity: 'error' });
77
+ issues.push({ path: 'schemaVersion', message: schemaMsg('schema.schemaVersionMustBeNumber'), severity: 'error' });
77
78
  }
78
79
 
79
80
  const exporter = obj['exporter'] as Record<string, unknown> | undefined;
80
81
  if (!exporter || typeof exporter !== 'object') {
81
- issues.push({ path: 'exporter', message: 'exporter 缺失', severity: 'error' });
82
+ issues.push({ path: 'exporter', message: schemaMsg('schema.exporterMissing'), severity: 'error' });
82
83
  } else {
83
- if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: 'exporter.name 必须是字符串', severity: 'error' });
84
- if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: 'exporter.version 必须是字符串', severity: 'error' });
84
+ if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: schemaMsg('schema.exporterNameMustBeString'), severity: 'error' });
85
+ if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: schemaMsg('schema.exporterVersionMustBeString'), severity: 'error' });
85
86
  }
86
87
 
87
88
  const source = obj['source'] as Record<string, unknown> | undefined;
88
89
  if (!source || typeof source !== 'object') {
89
- issues.push({ path: 'source', message: 'source 缺失', severity: 'error' });
90
+ issues.push({ path: 'source', message: schemaMsg('schema.sourceMissing'), severity: 'error' });
90
91
  } else {
91
- if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: 'source.dshVersion 必须是字符串', severity: 'error' });
92
- if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: 'source.platform 必须是字符串', severity: 'error' });
93
- if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: 'source.arch 必须是字符串', severity: 'error' });
92
+ if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: schemaMsg('schema.sourceDshVersionMustBeString'), severity: 'error' });
93
+ if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: schemaMsg('schema.sourcePlatformMustBeString'), severity: 'error' });
94
+ if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: schemaMsg('schema.sourceArchMustBeString'), severity: 'error' });
94
95
  }
95
96
 
96
97
  if (typeof obj['exportedAt'] !== 'string' || Number.isNaN(Date.parse(obj['exportedAt']))) {
97
- issues.push({ path: 'exportedAt', message: 'exportedAt 必须是合法 ISO-8601 时间', severity: 'error' });
98
+ issues.push({ path: 'exportedAt', message: schemaMsg('schema.exportedAtInvalid'), severity: 'error' });
98
99
  }
99
100
 
100
101
  const sections = obj['sections'] as Record<string, unknown> | undefined;
101
102
  if (!sections || typeof sections !== 'object') {
102
- issues.push({ path: 'sections', message: 'sections 缺失', severity: 'error' });
103
+ issues.push({ path: 'sections', message: schemaMsg('schema.sectionsMissing'), severity: 'error' });
103
104
  } else {
104
105
  for (const key of Object.keys(sections)) {
105
106
  if (!(SECTION_IDS as readonly string[]).includes(key)) {
106
- issues.push({ path: `sections.${key}`, message: `未知分区 "${key}"`, severity: 'warning' });
107
+ issues.push({ path: `sections.${key}`, message: schemaMsg('schema.unknownSection', { section: key }), severity: 'warning' });
107
108
  }
108
109
  if (typeof sections[key] !== 'boolean') {
109
- issues.push({ path: `sections.${key}`, message: `sections.${key} 必须是布尔`, severity: 'error' });
110
+ issues.push({ path: `sections.${key}`, message: schemaMsg('schema.sectionFlagMustBeBoolean', { section: key }), severity: 'error' });
110
111
  }
111
112
  }
112
113
  }
113
114
 
114
115
  const security = obj['security'] as Record<string, unknown> | undefined;
115
116
  if (!security || typeof security !== 'object') {
116
- issues.push({ path: 'security', message: 'security 缺失', severity: 'error' });
117
+ issues.push({ path: 'security', message: schemaMsg('schema.securityMissing'), severity: 'error' });
117
118
  } else {
118
- if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: 'containsSecrets 必须是布尔', severity: 'error' });
119
- if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: 'encrypted 必须是布尔', severity: 'error' });
119
+ if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: schemaMsg('schema.containsSecretsMustBeBoolean'), severity: 'error' });
120
+ if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: schemaMsg('schema.encryptedMustBeBoolean'), severity: 'error' });
120
121
  const enc = security['encryption'];
121
122
  if (enc !== null && (typeof enc !== 'object' || enc === undefined)) {
122
- issues.push({ path: 'security.encryption', message: 'encryption 必须是对象或 null', severity: 'error' });
123
+ issues.push({ path: 'security.encryption', message: schemaMsg('schema.encryptionMustBeObjectOrNull'), severity: 'error' });
123
124
  }
124
125
  }
125
126