@logictan/dsh-config-manager 0.1.60
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/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration History & Auditability Engine(Phase 6)——统一、持久、append-only、可查询、可导出的
|
|
3
|
+
* 迁移/审计历史存储。
|
|
4
|
+
*
|
|
5
|
+
* 职责(严格限定):
|
|
6
|
+
* - migration-history/ 目录下 per-file append-only 条目存储;
|
|
7
|
+
* - 原子写(atomicWriteFile,0600 + symlink-reject);
|
|
8
|
+
* - 读取(只认合法历史文件,忽略 tmp/脏文件;损坏文件跳过并计数);
|
|
9
|
+
* - 查询(kind / 结果 / 时间 / 分区 过滤)、统计、导出(JSON / Markdown);
|
|
10
|
+
* - retention(删最旧、幂等);
|
|
11
|
+
* - **Redaction 边界**:条目内仅有 summary / error 是自由文本,写入前必须经
|
|
12
|
+
* scanAndRedact(含 high-entropy 档)+ redactJournalText 双重清洗;其余字段
|
|
13
|
+
* (kind/result/source/sections/operationId/snapshotId/runId/at)均为常量或 UUID,
|
|
14
|
+
* 无 secret 承载面。
|
|
15
|
+
*
|
|
16
|
+
* 安全/架构纪律(Phase 6 §3):
|
|
17
|
+
* - **不建第二套 framework**:复用 Phase 1 atomicWriteFile、journal 的 per-file append
|
|
18
|
+
* 模式、security 脱敏、paths 保留前缀;本模块是新能力(审计史),非重复基础设施。
|
|
19
|
+
* - **APPEND-ONLY**:条目写入即完整独立文件,无 update/delete API(编译层无暴露);
|
|
20
|
+
* 仅 retention 删最旧。篡改文件 → 读回损坏识别并跳过(不静默接受)。
|
|
21
|
+
* - **DURABLE**:落盘跨重启。
|
|
22
|
+
* - **BOUND**:条目尽量关联 operationId / snapshotId / runId。
|
|
23
|
+
*
|
|
24
|
+
* 与 DSH 运行时解耦:IO 经可注入门面(测试注入失败点),引擎层零 DSH 依赖。
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs/promises';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
30
|
+
import { redact } from '../security/redaction.js';
|
|
31
|
+
import { scanAndRedact } from '../security/secret-scanner.js';
|
|
32
|
+
import { sha256Hex } from '../utils/hashing.js';
|
|
33
|
+
// ---------- 常量 ----------
|
|
34
|
+
export const MIGRATION_HISTORY_SCHEMA_VERSION = 1;
|
|
35
|
+
export const MIGRATION_HISTORY_DIR = 'migration-history';
|
|
36
|
+
/** 默认保留上限(retention 删最旧)。 */
|
|
37
|
+
export const DEFAULT_MIGRATION_RETENTION = 1000;
|
|
38
|
+
/** 文件名正则:`<stamp>-<hex12>.<kind>.json`,只认合法历史文件。 */
|
|
39
|
+
const HISTORY_BASENAME_RE = /^[0-9A-Za-z:.+-]{8,}-[0-9a-f]{12}\.[a-z-]+\.json$/i;
|
|
40
|
+
const TMP_PREFIX = '.dshcm.';
|
|
41
|
+
/** 合法 kind 集合(§5 COMPLETE 清单 + 评审补 'profile-import')。 */
|
|
42
|
+
const MIGRATION_KINDS = [
|
|
43
|
+
'import', 'restore', 'rollback',
|
|
44
|
+
'profile-switch', 'profile-delete', 'profile-rename', 'profile-save', 'profile-import',
|
|
45
|
+
'sync-apply', 'autosync', 'recovery',
|
|
46
|
+
'backup', 'snapshot-delete', 'snapshot-prune',
|
|
47
|
+
];
|
|
48
|
+
const defaultIo = {
|
|
49
|
+
async mkdir(d, o) { await fs.mkdir(d, o); },
|
|
50
|
+
async readdirNames(d) { try {
|
|
51
|
+
return await fs.readdir(d);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
} },
|
|
56
|
+
async readFileText(p) { return (await fs.readFile(p, 'utf8')).toString(); },
|
|
57
|
+
async writeAll(t, c) { await atomicWriteFile(t, c, { mode: 0o600, symlink: 'reject' }); },
|
|
58
|
+
async rm(p, o) { await fs.rm(p, o); },
|
|
59
|
+
async lstat(p) { try {
|
|
60
|
+
return await fs.lstat(p);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
} },
|
|
65
|
+
async exists(p) { try {
|
|
66
|
+
await fs.access(p);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
} },
|
|
72
|
+
};
|
|
73
|
+
// ---------- Redaction(双保险) ----------
|
|
74
|
+
/** journal 级高熵长 token 掩码(复用 journal 语义,独立实现避免跨层依赖)。 */
|
|
75
|
+
const HIGH_ENTROPY_RE = /([A-Za-z0-9+/_=-]{28,})/g;
|
|
76
|
+
/** 结构化时间戳/文件名形态:日期段必须由连字符连接(ISO 日期 YYYY-MM-DD 或
|
|
77
|
+
* 紧凑时间戳 YYYYMMDD-HHMMSS)。连字符不存在于 hex/base64 token 中,豁免不会误放行随机密钥。 */
|
|
78
|
+
const DATE_STAMP_RE = /\d{4}-\d{2}-\d{2}|\d{8}-\d{6}/;
|
|
79
|
+
/** 高熵长 token 掩码(日期戳形态豁免)。 */
|
|
80
|
+
function maskHighEntropy(text) {
|
|
81
|
+
return text.replace(HIGH_ENTROPY_RE, (run) => (DATE_STAMP_RE.test(run) ? run : '[REDACTED]'));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 历史文本强脱敏:redact(结构化字段 + 已知值形状)+ 高熵长 token 掩码。
|
|
85
|
+
* 用于 summary / error 等可能嵌入任意值的字段(REDACTED 不变量)。
|
|
86
|
+
*/
|
|
87
|
+
export function redactHistoryText(text) {
|
|
88
|
+
let out = text;
|
|
89
|
+
try {
|
|
90
|
+
out = redact(out);
|
|
91
|
+
}
|
|
92
|
+
catch { /* 脱敏失败保守处理 */ }
|
|
93
|
+
return maskHighEntropy(out);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 构造安全条目:对 summary / error 做强脱敏(scanAndRedact 高熵档 + redactHistoryText),
|
|
97
|
+
* 做白名单字段规范化,并计算 contentHash(对除 contentHash 外全字段的 SHA-256)。
|
|
98
|
+
* **历史写入的唯一出口入口**,禁止绕过。
|
|
99
|
+
*/
|
|
100
|
+
export function sanitizeEntry(raw) {
|
|
101
|
+
// 1) 无损子串掩码(redact 值形状 + 高熵长 token),保留周边可读文本——优先,
|
|
102
|
+
// 避免「一个 sk- 子串就整段清空」导致审计可读性全失。
|
|
103
|
+
const masked = {
|
|
104
|
+
summary: redactHistoryText(raw.summary ?? ''),
|
|
105
|
+
error: redactHistoryText(raw.error ?? ''),
|
|
106
|
+
};
|
|
107
|
+
// 2) scanAndRedact(高熵档)作为残留防线:若掩码后仍检测到威胁,保守整值清空,
|
|
108
|
+
// 绝不带敏感值落盘。(scanAndRedact 对裸 string 静默跳过 → 固定包对象。)
|
|
109
|
+
const { sanitized } = scanAndRedact({ summary: masked.summary, error: masked.error }, { highEntropy: true, valuePatterns: true });
|
|
110
|
+
const s = (sanitized ?? {});
|
|
111
|
+
const summary = typeof s.summary === 'string' ? s.summary : '';
|
|
112
|
+
const hasError = ((masked.error ?? '') !== '');
|
|
113
|
+
const error = hasError ? (typeof s.error === 'string' ? s.error : '') : undefined;
|
|
114
|
+
const entry = {
|
|
115
|
+
schemaVersion: MIGRATION_HISTORY_SCHEMA_VERSION,
|
|
116
|
+
at: raw.at,
|
|
117
|
+
kind: raw.kind,
|
|
118
|
+
result: raw.result,
|
|
119
|
+
sections: Array.isArray(raw.sections) ? raw.sections.filter((s) => typeof s === 'string') : [],
|
|
120
|
+
source: sourceGuard(raw.source),
|
|
121
|
+
summary,
|
|
122
|
+
error,
|
|
123
|
+
};
|
|
124
|
+
if (raw.operationId !== undefined && isUuid(raw.operationId))
|
|
125
|
+
entry.operationId = raw.operationId;
|
|
126
|
+
if (raw.snapshotId !== undefined && isUuid(raw.snapshotId))
|
|
127
|
+
entry.snapshotId = raw.snapshotId;
|
|
128
|
+
if (raw.runId !== undefined && isUuid(raw.runId))
|
|
129
|
+
entry.runId = raw.runId;
|
|
130
|
+
const stored = entry;
|
|
131
|
+
stored.contentHash = computeContentHash(entry);
|
|
132
|
+
return stored;
|
|
133
|
+
}
|
|
134
|
+
const VALID_SOURCES = new Set([
|
|
135
|
+
'api', 'autosync', 'backup-scheduler', 'recovery', 'cli', 'internal',
|
|
136
|
+
]);
|
|
137
|
+
/** 运行期校验 source 属合法枚举;非法回退 'internal'(防未知来源绕过/清洗不一致)。 */
|
|
138
|
+
function sourceGuard(v) {
|
|
139
|
+
return typeof v === 'string' && VALID_SOURCES.has(v) ? v : 'internal';
|
|
140
|
+
}
|
|
141
|
+
/** 对除 contentHash 外全字段计算稳定 SHA-256(字段序稳定、无无关缩进/空白)。 */
|
|
142
|
+
function computeContentHash(entry) {
|
|
143
|
+
const payload = Object.fromEntries(Object.entries(entry).filter(([k]) => k !== 'contentHash'));
|
|
144
|
+
return sha256Hex(canonicalStringify(payload));
|
|
145
|
+
}
|
|
146
|
+
/** 稳定 JSON 序列化(键排序 + 无多余空白),保证 hash 跨运行稳定。 */
|
|
147
|
+
function canonicalStringify(value) {
|
|
148
|
+
return JSON.stringify(sortKeys(value));
|
|
149
|
+
}
|
|
150
|
+
function sortKeys(v) {
|
|
151
|
+
if (Array.isArray(v))
|
|
152
|
+
return v.map(sortKeys);
|
|
153
|
+
if (v !== null && typeof v === 'object') {
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const k of Object.keys(v).sort()) {
|
|
156
|
+
out[k] = sortKeys(v[k]);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
return v;
|
|
161
|
+
}
|
|
162
|
+
function isUuid(v) {
|
|
163
|
+
return typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
|
|
164
|
+
}
|
|
165
|
+
/** 合法 kind 判定(类型守卫 + 运行时校验)。 */
|
|
166
|
+
export function isValidMigrationKind(k) {
|
|
167
|
+
return typeof k === 'string' && MIGRATION_KINDS.includes(k);
|
|
168
|
+
}
|
|
169
|
+
/** 判断文件 basename 是否是可追踪历史文件(<sortable>-<hex>.json)。忽略 tmp 及其它。 */
|
|
170
|
+
export function isHistoryBasename(name) {
|
|
171
|
+
if (!HISTORY_BASENAME_RE.test(name))
|
|
172
|
+
return false;
|
|
173
|
+
return !name.startsWith(TMP_PREFIX);
|
|
174
|
+
}
|
|
175
|
+
/** 文件名安全:仅 basename(不含目录分隔符/穿越)。 */
|
|
176
|
+
export function isSafeHistoryFilename(name) {
|
|
177
|
+
if (name === '' || name.includes('/') || name.includes('\\') || name.includes('\0'))
|
|
178
|
+
return false;
|
|
179
|
+
return name !== '.' && name !== '..';
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 生成排序文件名(Windows-safe 定宽时间戳 + 随机后缀 + kind 标记)。
|
|
183
|
+
*
|
|
184
|
+
* 时间戳契约(Windows/排序可靠性):`toISOString().replace(/[:Z]/g,'')`
|
|
185
|
+
* → `2026-08-30T120000.123`(UTC 定宽、仅 ASCII、无 Windows 非法字符 `<>:"/\|?*`、
|
|
186
|
+
* 字典序== 时间序)。**禁止**以原始 `toISOString()`(含 `:`)直接落文件名(Windows rename 失败)。
|
|
187
|
+
* 与既有 `dateStamp`(`YYYYMMDD-HHmmss`)语义同为「定宽 + 仅 ASCII + 可排序」,本实现用 UTC 含毫秒
|
|
188
|
+
* 以获得更高时序精度;排序可靠性一致。
|
|
189
|
+
*/
|
|
190
|
+
export function makeHistoryFilename(now, kind) {
|
|
191
|
+
const stamp = now.toISOString().replace(/[:Z]/g, '');
|
|
192
|
+
const rand = crypto.randomBytes(6).toString('hex');
|
|
193
|
+
return `${stamp}-${rand}.${kind}.json`;
|
|
194
|
+
}
|
|
195
|
+
// ---------- 解析 ----------
|
|
196
|
+
function parseEntry(text) {
|
|
197
|
+
try {
|
|
198
|
+
const p = JSON.parse(text);
|
|
199
|
+
if (p === null || typeof p !== 'object')
|
|
200
|
+
return null;
|
|
201
|
+
if (p.schemaVersion !== MIGRATION_HISTORY_SCHEMA_VERSION)
|
|
202
|
+
return null;
|
|
203
|
+
if (!isValidMigrationKind(p.kind))
|
|
204
|
+
return null;
|
|
205
|
+
if (typeof p.at !== 'string' || p.at === '')
|
|
206
|
+
return null;
|
|
207
|
+
if (!['success', 'failed', 'skipped'].includes(p.result))
|
|
208
|
+
return null;
|
|
209
|
+
if (typeof p.source !== 'string')
|
|
210
|
+
return null;
|
|
211
|
+
if (typeof p.summary !== 'string')
|
|
212
|
+
return null;
|
|
213
|
+
// 可选字段校验
|
|
214
|
+
if (p.operationId !== undefined && !isUuid(p.operationId))
|
|
215
|
+
return null;
|
|
216
|
+
if (p.snapshotId !== undefined && !isUuid(p.snapshotId))
|
|
217
|
+
return null;
|
|
218
|
+
if (p.runId !== undefined && !isUuid(p.runId))
|
|
219
|
+
return null;
|
|
220
|
+
if (p.sections !== undefined && (!Array.isArray(p.sections) || p.sections.some((s) => typeof s !== 'string')))
|
|
221
|
+
return null;
|
|
222
|
+
if (p.error !== undefined && typeof p.error !== 'string')
|
|
223
|
+
return null;
|
|
224
|
+
// **append-only 篡改检测**:contentHash 必须匹配(对除 contentHash 外全字段)。
|
|
225
|
+
// 合法 JSON 内改任意字段值 → hash 不符 → 拒绝(计入 corrupted,不静默接受)。
|
|
226
|
+
if (typeof p.contentHash !== 'string' || p.contentHash === '')
|
|
227
|
+
return null;
|
|
228
|
+
const payload = Object.fromEntries(Object.entries(p).filter(([k]) => k !== 'contentHash'));
|
|
229
|
+
if (sha256Hex(canonicalStringify(payload)) !== p.contentHash)
|
|
230
|
+
return null;
|
|
231
|
+
const { contentHash, ...rest } = p;
|
|
232
|
+
return { ...rest, contentHash };
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** MigrationHistory:per-file append-only 历史存储。 */
|
|
239
|
+
export class MigrationStore {
|
|
240
|
+
dir;
|
|
241
|
+
io;
|
|
242
|
+
constructor(opts) {
|
|
243
|
+
this.dir = opts.dir;
|
|
244
|
+
this.io = opts.io ?? defaultIo;
|
|
245
|
+
}
|
|
246
|
+
/** 追加一条历史(原子写)。best-effort:调用方须经 tryAppendHistory 处理失败降级。 */
|
|
247
|
+
async append(raw) {
|
|
248
|
+
try {
|
|
249
|
+
const at = raw.at ?? new Date().toISOString();
|
|
250
|
+
const entry = sanitizeEntry({ ...raw, at });
|
|
251
|
+
// **防覆盖**:目标已存在(同毫秒+同随机后缀碰撞,概率极低但非零)→ 换名,
|
|
252
|
+
// 绝不覆盖既有条目(APPEND-ONLY 硬不变量:条目一旦写入不可改)。
|
|
253
|
+
let filename = makeHistoryFilename(new Date(at), entry.kind);
|
|
254
|
+
let target = path.join(this.dir, filename);
|
|
255
|
+
await this.io.mkdir(this.dir, { recursive: true });
|
|
256
|
+
let guard = 0;
|
|
257
|
+
while (await this.io.exists(target)) {
|
|
258
|
+
if (++guard > 8)
|
|
259
|
+
return { ok: false, entry, error: 'history filename collision (retry 上限)' };
|
|
260
|
+
filename = makeHistoryFilename(new Date(at + guard), entry.kind);
|
|
261
|
+
target = path.join(this.dir, filename);
|
|
262
|
+
}
|
|
263
|
+
await this.io.writeAll(target, `${JSON.stringify(entry, null, 2)}\n`);
|
|
264
|
+
return { ok: true, entry, file: filename };
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const entry = sanitizeEntry({ ...raw, at: raw.at ?? new Date().toISOString() });
|
|
268
|
+
return { ok: false, entry, error: error instanceof Error ? error.message : String(error) };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** 读取全部合法历史条目(旧→新;损坏/非史文件跳过并计数)。 */
|
|
272
|
+
async read() {
|
|
273
|
+
const names = await this.io.readdirNames(this.dir).catch(() => []);
|
|
274
|
+
const entries = [];
|
|
275
|
+
const corrupted = [];
|
|
276
|
+
const sorted = names.filter(isHistoryBasename).sort();
|
|
277
|
+
for (const name of sorted) {
|
|
278
|
+
const p = path.join(this.dir, name);
|
|
279
|
+
let text;
|
|
280
|
+
try {
|
|
281
|
+
if ((await this.io.lstat(p))?.isSymbolicLink() === true) {
|
|
282
|
+
corrupted.push(name);
|
|
283
|
+
continue;
|
|
284
|
+
} // symlink 防御
|
|
285
|
+
text = await this.io.readFileText(p);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
corrupted.push(name);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const parsed = parseEntry(text);
|
|
292
|
+
if (parsed === null) {
|
|
293
|
+
corrupted.push(name);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
entries.push(parsed);
|
|
297
|
+
}
|
|
298
|
+
return { entries, corrupted };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 保留清理:按文件名排序删除最旧合法历史文件,幂等。只删合法历史文件(防误删)。
|
|
302
|
+
* 返回本次删除的文件名。
|
|
303
|
+
*/
|
|
304
|
+
async retention(limit = DEFAULT_MIGRATION_RETENTION) {
|
|
305
|
+
const names = (await this.io.readdirNames(this.dir).catch(() => [])).filter(isHistoryBasename).sort();
|
|
306
|
+
const removed = [];
|
|
307
|
+
if (names.length <= limit)
|
|
308
|
+
return removed;
|
|
309
|
+
const toRemove = names.slice(0, names.length - limit);
|
|
310
|
+
for (const name of toRemove) {
|
|
311
|
+
try {
|
|
312
|
+
await this.io.rm(path.join(this.dir, name), { force: true });
|
|
313
|
+
removed.push(name);
|
|
314
|
+
}
|
|
315
|
+
catch { /* 单文件删失败不中断其余 */ }
|
|
316
|
+
}
|
|
317
|
+
return removed;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ---------- 查询 / 统计 / 导出(纯函数) ----------
|
|
321
|
+
/** 纯函数过滤(无 IO)。 */
|
|
322
|
+
export function queryHistory(entries, q) {
|
|
323
|
+
return entries.filter((e) => {
|
|
324
|
+
if (q.kinds !== undefined && q.kinds.length > 0 && !q.kinds.includes(e.kind))
|
|
325
|
+
return false;
|
|
326
|
+
if (q.result !== undefined && q.result.length > 0 && !q.result.includes(e.result))
|
|
327
|
+
return false;
|
|
328
|
+
if (q.sections !== undefined && q.sections.length > 0) {
|
|
329
|
+
if (!e.sections.some((s) => q.sections.includes(s)))
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const t = Date.parse(e.at);
|
|
333
|
+
if (!Number.isNaN(t)) {
|
|
334
|
+
if (q.from !== undefined && t < q.from)
|
|
335
|
+
return false;
|
|
336
|
+
if (q.to !== undefined && t > q.to)
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** 纯函数统计(kind/result 计数)。 */
|
|
343
|
+
export function summarizeHistory(entries) {
|
|
344
|
+
const stats = { total: entries.length, byKind: {}, byResult: {} };
|
|
345
|
+
for (const e of entries) {
|
|
346
|
+
stats.byKind[e.kind] = (stats.byKind[e.kind] ?? 0) + 1;
|
|
347
|
+
stats.byResult[e.result] = (stats.byResult[e.result] ?? 0) + 1;
|
|
348
|
+
}
|
|
349
|
+
return stats;
|
|
350
|
+
}
|
|
351
|
+
/** 导出报告(纯函数)。渲染文本统一过 redact() 兜底(安全不变量)。 */
|
|
352
|
+
export function renderExport(entries, format, locale = 'zh') {
|
|
353
|
+
const L = locale === 'zh'
|
|
354
|
+
? { title: 'DSH 配置迁移历史', total: '总数', empty: '暂无迁移记录', kind: '操作', time: '时间', result: '结果', sections: '分区', detail: '摘要' }
|
|
355
|
+
: { title: 'DSH Config Migration History', total: 'Total', empty: 'No migration records', kind: 'Operation', time: 'Time', result: 'Result', sections: 'Sections', detail: 'Summary' };
|
|
356
|
+
const resultLabel = (r) => r === 'success' ? (locale === 'zh' ? '成功' : 'success') : r === 'failed' ? (locale === 'zh' ? '失败' : 'failed') : (locale === 'zh' ? '跳过' : 'skipped');
|
|
357
|
+
if (format === 'json') {
|
|
358
|
+
const stats = summarizeHistory(entries);
|
|
359
|
+
return redact(JSON.stringify({ title: L.title, stats, entries }, null, 2));
|
|
360
|
+
}
|
|
361
|
+
if (entries.length === 0)
|
|
362
|
+
return `# ${L.title}\n\n${L.empty}\n`;
|
|
363
|
+
const lines = [];
|
|
364
|
+
lines.push(`# ${L.title}`, '');
|
|
365
|
+
lines.push(`> ${L.total}: ${entries.length}`, '');
|
|
366
|
+
lines.push('| ' + [L.time, L.kind, L.result, L.sections, L.detail].join(' | ') + ' |');
|
|
367
|
+
lines.push('|' + ' --- |'.repeat(5));
|
|
368
|
+
for (const e of entries) {
|
|
369
|
+
const detail = redact(e.summary + (e.error !== undefined ? ` — ${e.error}` : ''));
|
|
370
|
+
lines.push(`| ${e.at} | ${e.kind} | ${resultLabel(e.result)} | ${redact(e.sections.join(', '))} | ${detail} |`);
|
|
371
|
+
}
|
|
372
|
+
lines.push('');
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
}
|
|
375
|
+
/** 解析查询参数为 MigrationQuery(宿主路由复用;过滤非法输入)。 */
|
|
376
|
+
export function parseHistoryQuery(raw) {
|
|
377
|
+
const q = {};
|
|
378
|
+
const kinds = parseList(raw['kind']);
|
|
379
|
+
if (kinds.length > 0)
|
|
380
|
+
q.kinds = kinds.filter(isValidMigrationKind);
|
|
381
|
+
const results = parseList(raw['result']);
|
|
382
|
+
if (results.length > 0)
|
|
383
|
+
q.result = results.filter((r) => r === 'success' || r === 'failed' || r === 'skipped');
|
|
384
|
+
const sections = parseList(raw['sections']);
|
|
385
|
+
if (sections.length > 0)
|
|
386
|
+
q.sections = sections;
|
|
387
|
+
const from = parseNum(raw['from']);
|
|
388
|
+
const to = parseNum(raw['to']);
|
|
389
|
+
if (from !== undefined)
|
|
390
|
+
q.from = from;
|
|
391
|
+
if (to !== undefined)
|
|
392
|
+
q.to = to;
|
|
393
|
+
return q;
|
|
394
|
+
}
|
|
395
|
+
function parseList(v) {
|
|
396
|
+
if (v === undefined)
|
|
397
|
+
return [];
|
|
398
|
+
if (typeof v === 'string')
|
|
399
|
+
return v.split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
400
|
+
if (Array.isArray(v))
|
|
401
|
+
return v.filter((x) => typeof x === 'string' && x !== '');
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
function parseNum(v) {
|
|
405
|
+
if (typeof v === 'string' && v !== '' && !Number.isNaN(Number(v)))
|
|
406
|
+
return Number(v);
|
|
407
|
+
if (typeof v === 'number' && !Number.isNaN(v))
|
|
408
|
+
return v;
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=migration-history.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-config-manager — Agent 可调用的模型工具。
|
|
3
|
+
*
|
|
4
|
+
* 在 host 半注册 2 个 Cordis 模型工具,让 Agent 能自主驱动配置同步:
|
|
5
|
+
* config_sync_push 手动推送同步(写远端)
|
|
6
|
+
* config_sync_pull 拉取差异预览(零写入)
|
|
7
|
+
*
|
|
8
|
+
* 设计遵循 AGENTS.md 铁律:
|
|
9
|
+
* - 所有业务逻辑为可独立测试的纯编排函数(createModelTools),
|
|
10
|
+
* 复用 src/core 已解耦引擎(SyncEngine),不重复实现;
|
|
11
|
+
* - React / HTTP 壳不参与;本文件是「引擎侧编排」,不放浏览器 src/ui/;
|
|
12
|
+
* - ctx.tools 服务用可选读取(ctx.get)守卫:未组合 tools 的部署不注册、不崩溃。
|
|
13
|
+
*
|
|
14
|
+
* 安全不变量(硬约束,勿破坏):
|
|
15
|
+
* - config_sync_pull 零写入(只 analyze+plan 出差异);落地需另走确认导入管道;
|
|
16
|
+
* - 分区/路径走既有白名单(SECTION_IDS 过滤)。
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { JsonValue } from '@deepseek-ai/dsh-tools';
|
|
20
|
+
import type { SyncEngine } from '../sync/sync-engine.ts';
|
|
21
|
+
import type { SectionId } from '../schema/types.ts';
|
|
22
|
+
import type { SyncConfig, SyncTransportType } from '../sync/sync-config.ts';
|
|
23
|
+
import type { ConfigAdapter, HostContext } from './types.ts';
|
|
24
|
+
/** 模型工具所需依赖(与 host 路由同一来源闭包)。 */
|
|
25
|
+
export interface ModelToolsDeps {
|
|
26
|
+
/** HostContext 门面(homeDir / profile / msg / log) */
|
|
27
|
+
host: HostContext;
|
|
28
|
+
/** 全部分区适配器 */
|
|
29
|
+
adapters: ConfigAdapter[];
|
|
30
|
+
/** 同步状态目录($DSH_HOME/dsh-config-manager/sync) */
|
|
31
|
+
syncDir: string;
|
|
32
|
+
/** SyncEngine 工厂(git/webdav 按配置分支构造传输;与 host 路由同一来源) */
|
|
33
|
+
makeSyncEngine: (cfg: SyncConfig) => SyncEngine;
|
|
34
|
+
}
|
|
35
|
+
/** 2 个工具的纯编排实现(可独立测试,不依赖 Cordis ctx)。所有返回均为 JsonValue(可序列化、无 undefined)。 */
|
|
36
|
+
export declare function createModelTools(deps: ModelToolsDeps): {
|
|
37
|
+
/** 手动推送同步(写远端)。明文快照:勾选即同步,不加密、不脱敏。 */
|
|
38
|
+
syncPush(input: {
|
|
39
|
+
channel?: SyncTransportType;
|
|
40
|
+
sections?: SectionId[];
|
|
41
|
+
}): Promise<JsonValue>;
|
|
42
|
+
/** 拉取差异预览(零写入:只下载 + analyze + plan 出差异报告)。 */
|
|
43
|
+
syncPull(input: {
|
|
44
|
+
channel?: SyncTransportType;
|
|
45
|
+
snapshotId?: string;
|
|
46
|
+
strategy?: 'merge' | 'replace' | 'skipExisting';
|
|
47
|
+
}): Promise<JsonValue>;
|
|
48
|
+
};
|
|
49
|
+
/** 把 2 个模型工具注册进 ctx.tools;tools 服务未组合时静默跳过(不崩溃)。 */
|
|
50
|
+
export declare function registerModelTools(ctx: Context, deps: ModelToolsDeps): void;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { SECTION_IDS } from '../schema/config.js';
|
|
3
|
+
import { readFullSyncConfig, readSyncConfigFor } from '../sync/sync-config.js';
|
|
4
|
+
import { runWithMutationLock } from '../utils/env-lock.js';
|
|
5
|
+
/** SECTION_IDS 白名单过滤(未知/非法分区 id 丢弃,与 host 路由语义一致)。 */
|
|
6
|
+
function filterSectionIds(ids) {
|
|
7
|
+
return ids.filter((id) => SECTION_IDS.includes(id));
|
|
8
|
+
}
|
|
9
|
+
/** 解析同步引擎:channel 缺省取已配置的活动通道(git/webdav),未配置则抛可操作错误。 */
|
|
10
|
+
async function resolveEngine(deps, channel) {
|
|
11
|
+
let ch = channel ?? 'git';
|
|
12
|
+
if (channel === undefined) {
|
|
13
|
+
const full = await readFullSyncConfig(deps.syncDir);
|
|
14
|
+
if (full !== null && full.transport === 'webdav')
|
|
15
|
+
ch = 'webdav';
|
|
16
|
+
}
|
|
17
|
+
const cfg = await readSyncConfigFor(deps.syncDir, ch);
|
|
18
|
+
if (cfg === null) {
|
|
19
|
+
throw new Error(`同步通道 ${ch} 尚未配置(sync-config.json 缺失或损坏)`);
|
|
20
|
+
}
|
|
21
|
+
return { engine: deps.makeSyncEngine(cfg), channel: ch };
|
|
22
|
+
}
|
|
23
|
+
/* ------------------------------------------------------------ 纯编排函数 */
|
|
24
|
+
/** 2 个工具的纯编排实现(可独立测试,不依赖 Cordis ctx)。所有返回均为 JsonValue(可序列化、无 undefined)。 */
|
|
25
|
+
export function createModelTools(deps) {
|
|
26
|
+
return {
|
|
27
|
+
/** 手动推送同步(写远端)。明文快照:勾选即同步,不加密、不脱敏。 */
|
|
28
|
+
async syncPush(input) {
|
|
29
|
+
const { engine } = await resolveEngine(deps, input.channel);
|
|
30
|
+
const sections = input.sections === undefined ? undefined : filterSectionIds(input.sections);
|
|
31
|
+
// Phase 2 锁:push 写远端 + 本地散文件 + sync-state,属 GLOBAL mutation。
|
|
32
|
+
// Step 3 P0-A:外部 push 记 intent journal(crash 后不可证明 → NEEDS_ATTENTION,不自动重推)。
|
|
33
|
+
const doPush = () => engine.push({
|
|
34
|
+
...(sections === undefined ? {} : { sections }),
|
|
35
|
+
});
|
|
36
|
+
const report = await runWithMutationLock(deps.host.mutationLock, { op: 'model-sync-push', isBlocked: () => deps.host.safeModeIsBlocked?.() ?? false }, async (lockCtx) => {
|
|
37
|
+
if (deps.host.phase3Recovery !== undefined && lockCtx !== null) {
|
|
38
|
+
const r = await deps.host.phase3Recovery.runExternalIntent({
|
|
39
|
+
operationType: 'model-sync-push',
|
|
40
|
+
lockCtx,
|
|
41
|
+
intent: { adapter: 'sync', ref: input.channel ?? 'default', kind: 'Push' },
|
|
42
|
+
fn: doPush,
|
|
43
|
+
});
|
|
44
|
+
return r.result;
|
|
45
|
+
}
|
|
46
|
+
return doPush();
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
ok: report.ok,
|
|
50
|
+
snapshotId: report.snapshotId,
|
|
51
|
+
sections: report.sections,
|
|
52
|
+
warnings: report.warnings,
|
|
53
|
+
...(report.message === undefined ? {} : { message: report.message }),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
/** 拉取差异预览(零写入:只下载 + analyze + plan 出差异报告)。 */
|
|
57
|
+
async syncPull(input) {
|
|
58
|
+
const { engine } = await resolveEngine(deps, input.channel);
|
|
59
|
+
const strategy = input.strategy === 'replace' || input.strategy === 'skipExisting' ? input.strategy : 'replace';
|
|
60
|
+
const report = await engine.pull({
|
|
61
|
+
strategy,
|
|
62
|
+
...(input.snapshotId === undefined || input.snapshotId === '' ? {} : { snapshotId: input.snapshotId }),
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
ok: report.ok,
|
|
66
|
+
snapshotId: report.snapshotId,
|
|
67
|
+
needsReview: report.needsReview,
|
|
68
|
+
changes: report.changes.map((c) => ({
|
|
69
|
+
id: c.id,
|
|
70
|
+
adapter: c.adapter,
|
|
71
|
+
kind: c.kind,
|
|
72
|
+
description: c.description,
|
|
73
|
+
severity: c.severity,
|
|
74
|
+
})),
|
|
75
|
+
...(report.message === undefined ? {} : { message: report.message }),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** 把 2 个模型工具注册进 ctx.tools;tools 服务未组合时静默跳过(不崩溃)。 */
|
|
81
|
+
export function registerModelTools(ctx, deps) {
|
|
82
|
+
const toolsSvc = ctx.get('tools');
|
|
83
|
+
if (toolsSvc === null || toolsSvc === undefined || typeof toolsSvc !== 'object') {
|
|
84
|
+
deps.host.log.warn?.('tools 服务不可用:跳过模型工具注册(引擎能力仍可用)');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const tools = createModelTools(deps);
|
|
88
|
+
const disposers = [];
|
|
89
|
+
// 注意:必须经 ctx.get('tools') 的结果注册,绝不能用 ctx.tools 属性访问——
|
|
90
|
+
// Cordis 的属性访问要求插件声明 inject: ['tools'],未声明时即使服务存在也会抛
|
|
91
|
+
// "cannot get property X without inject"(tools 是可选服务,不应进 inject)。
|
|
92
|
+
const register = (def) => {
|
|
93
|
+
disposers.push(toolsSvc.register(def));
|
|
94
|
+
};
|
|
95
|
+
register(defineTool({
|
|
96
|
+
name: 'config_sync_push',
|
|
97
|
+
description: '手动推送 DSH 配置同步到远端(Git/WebDAV),复用已持久化的通道配置。写远端属主动操作;快照为明文(私有通道自用),勾选即同步。',
|
|
98
|
+
parameters: {
|
|
99
|
+
channel: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
enum: ['git', 'webdav'],
|
|
102
|
+
description: '同步通道;缺省 = 已配置的活动通道',
|
|
103
|
+
},
|
|
104
|
+
sections: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: { type: 'string', enum: [...SECTION_IDS] },
|
|
107
|
+
description: '仅推送的分区;缺省 = 全部推荐分区',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
output: {
|
|
111
|
+
schema: { type: 'json', description: '推送报告(snapshotId / 实际同步分区 / 告警)' },
|
|
112
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
113
|
+
},
|
|
114
|
+
async execute(args) {
|
|
115
|
+
return tools.syncPush(args);
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
register(defineTool({
|
|
119
|
+
name: 'config_sync_pull',
|
|
120
|
+
description: '拉取远端同步差异预览(Git/WebDAV,零写入:只下载 + 分析出差异报告,绝不直接写配置)。若要落地差异需另走确认导入管道。',
|
|
121
|
+
parameters: {
|
|
122
|
+
channel: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
enum: ['git', 'webdav'],
|
|
125
|
+
description: '同步通道;缺省 = 已配置的活动通道',
|
|
126
|
+
},
|
|
127
|
+
snapshotId: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: '远端快照 id;缺省 = 最新',
|
|
130
|
+
},
|
|
131
|
+
strategy: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
enum: ['replace', 'skipExisting'],
|
|
134
|
+
description: '差异全局策略;缺省 replace(远端值覆盖本地)',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
output: {
|
|
138
|
+
schema: { type: 'json', description: '差异报告(changes 列表 / needsReview)' },
|
|
139
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
140
|
+
},
|
|
141
|
+
async execute(args) {
|
|
142
|
+
return tools.syncPull(args);
|
|
143
|
+
},
|
|
144
|
+
}));
|
|
145
|
+
ctx.effect(() => () => { for (const d of disposers)
|
|
146
|
+
d(); }, 'config-manager: model tools');
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=model-tools.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息翻译器的最小契约(引擎注入点)。
|
|
3
|
+
* 实现见 ./messages.ts(zh/en 目录 + makeMsg / msgOf)。
|
|
4
|
+
*/
|
|
5
|
+
/** 插值形参:{param} 占位符的值。 */
|
|
6
|
+
export type MsgParams = Record<string, string | number>;
|
|
7
|
+
/** 翻译函数:key → 当前语言文案({param} 插值;未知键回退 zh/键名,绝不抛错)。 */
|
|
8
|
+
export type MsgFunc = (key: string, params?: MsgParams) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|