dsh-config-manager 0.1.69 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -18
- package/README.zh-CN.md +29 -9
- package/lib/adapters/credentials.js +6 -3
- package/lib/adapters/file-collection.d.ts +21 -1
- package/lib/adapters/file-collection.js +32 -8
- package/lib/adapters/mcp.d.ts +12 -2
- package/lib/adapters/mcp.js +30 -13
- package/lib/adapters/plugin-files.d.ts +8 -0
- package/lib/adapters/plugin-files.js +36 -4
- package/lib/adapters/plugins.d.ts +28 -2
- package/lib/adapters/plugins.js +204 -55
- package/lib/adapters/prompts.d.ts +12 -2
- package/lib/adapters/prompts.js +34 -19
- package/lib/adapters/providers.js +5 -4
- package/lib/adapters/self.d.ts +20 -0
- package/lib/adapters/self.js +73 -0
- package/lib/adapters/sessions.d.ts +8 -0
- package/lib/adapters/sessions.js +47 -1
- package/lib/adapters/settings.d.ts +21 -3
- package/lib/adapters/settings.js +44 -7
- package/lib/adapters/skills.d.ts +36 -4
- package/lib/adapters/skills.js +222 -3
- package/lib/adapters/test-helpers.d.ts +31 -2
- package/lib/adapters/test-helpers.js +42 -5
- package/lib/adapters/workspaces.js +97 -5
- package/lib/cli/actions.d.ts +50 -3
- package/lib/cli/actions.js +103 -11
- package/lib/cli/help.d.ts +65 -0
- package/lib/cli/help.js +517 -0
- package/lib/cli/import-source.d.ts +32 -0
- package/lib/cli/import-source.js +178 -0
- package/lib/cli/index.d.ts +18 -2
- package/lib/cli/index.js +118 -90
- package/lib/cli/web/client-script.d.ts +8 -0
- package/lib/cli/web/client-script.js +11 -0
- package/lib/cli/web/http.d.ts +5 -0
- package/lib/cli/web/http.js +2 -1
- package/lib/cli/web/page.d.ts +16 -6
- package/lib/cli/web/page.js +102 -25
- package/lib/cli/web/routes.js +24 -5
- package/lib/cli/web/server.js +23 -2
- package/lib/client.d.ts +1019 -36
- package/lib/client.js +5522 -1564
- package/lib/core/analyzer.d.ts +15 -1
- package/lib/core/analyzer.js +114 -13
- package/lib/core/backup-plan.js +17 -1
- package/lib/core/backup.d.ts +8 -0
- package/lib/core/backup.js +28 -6
- package/lib/core/cache-cleaner.d.ts +10 -0
- package/lib/core/cache-cleaner.js +63 -22
- package/lib/core/checkpoint.d.ts +362 -0
- package/lib/core/checkpoint.js +1309 -0
- package/lib/core/journal.d.ts +37 -0
- package/lib/core/journal.js +69 -0
- package/lib/core/messages.d.ts +13 -0
- package/lib/core/messages.js +26 -0
- package/lib/core/model-tools.js +3 -0
- package/lib/core/patch-layers.d.ts +83 -0
- package/lib/core/patch-layers.js +110 -0
- package/lib/core/restore.d.ts +21 -0
- package/lib/core/restore.js +37 -3
- package/lib/core/rollback.js +6 -3
- package/lib/core/self-update.d.ts +52 -0
- package/lib/core/self-update.js +70 -0
- package/lib/core/session-health.d.ts +29 -5
- package/lib/core/session-health.js +28 -3
- package/lib/core/types.d.ts +126 -0
- package/lib/foreign/antigravity.d.ts +28 -0
- package/lib/foreign/antigravity.js +157 -0
- package/lib/foreign/bundle.d.ts +25 -0
- package/lib/foreign/bundle.js +79 -0
- package/lib/foreign/chatgpt.d.ts +6 -0
- package/lib/foreign/chatgpt.js +25 -0
- package/lib/foreign/claude-code.d.ts +3 -0
- package/lib/foreign/claude-code.js +85 -0
- package/lib/foreign/claude-sessions.d.ts +108 -0
- package/lib/foreign/claude-sessions.js +299 -0
- package/lib/foreign/cline.d.ts +6 -0
- package/lib/foreign/cline.js +27 -0
- package/lib/foreign/codex.d.ts +83 -0
- package/lib/foreign/codex.js +348 -0
- package/lib/foreign/continue.d.ts +9 -0
- package/lib/foreign/continue.js +65 -0
- package/lib/foreign/copilot.d.ts +39 -0
- package/lib/foreign/copilot.js +95 -0
- package/lib/foreign/crush.d.ts +11 -0
- package/lib/foreign/crush.js +57 -0
- package/lib/foreign/cursor.d.ts +87 -0
- package/lib/foreign/cursor.js +171 -0
- package/lib/foreign/dsh.d.ts +30 -0
- package/lib/foreign/dsh.js +101 -0
- package/lib/foreign/dsh4.d.ts +3 -0
- package/lib/foreign/dsh4.js +13 -0
- package/lib/foreign/gemini.d.ts +7 -0
- package/lib/foreign/gemini.js +44 -0
- package/lib/foreign/goose.d.ts +11 -0
- package/lib/foreign/goose.js +53 -0
- package/lib/foreign/grokbuild.d.ts +8 -0
- package/lib/foreign/grokbuild.js +25 -0
- package/lib/foreign/hermes.d.ts +2 -0
- package/lib/foreign/hermes.js +78 -0
- package/lib/foreign/kernel.d.ts +132 -0
- package/lib/foreign/kernel.js +569 -0
- package/lib/foreign/kilocode.d.ts +11 -0
- package/lib/foreign/kilocode.js +51 -0
- package/lib/foreign/kimi.d.ts +7 -0
- package/lib/foreign/kimi.js +47 -0
- package/lib/foreign/mimocode.d.ts +11 -0
- package/lib/foreign/mimocode.js +51 -0
- package/lib/foreign/openclaw.d.ts +7 -0
- package/lib/foreign/openclaw.js +46 -0
- package/lib/foreign/opencode.d.ts +11 -0
- package/lib/foreign/opencode.js +52 -0
- package/lib/foreign/pi.d.ts +7 -0
- package/lib/foreign/pi.js +48 -0
- package/lib/foreign/platform-paths.d.ts +66 -0
- package/lib/foreign/platform-paths.js +141 -0
- package/lib/foreign/qoder.d.ts +10 -0
- package/lib/foreign/qoder.js +49 -0
- package/lib/foreign/qwen.d.ts +7 -0
- package/lib/foreign/qwen.js +45 -0
- package/lib/foreign/read-antigravity.d.ts +57 -0
- package/lib/foreign/read-antigravity.js +504 -0
- package/lib/foreign/read-chatgpt.d.ts +20 -0
- package/lib/foreign/read-chatgpt.js +509 -0
- package/lib/foreign/read-claude-code.d.ts +24 -0
- package/lib/foreign/read-claude-code.js +234 -0
- package/lib/foreign/read-cline.d.ts +53 -0
- package/lib/foreign/read-cline.js +479 -0
- package/lib/foreign/read-codex.d.ts +75 -0
- package/lib/foreign/read-codex.js +842 -0
- package/lib/foreign/read-continue.d.ts +87 -0
- package/lib/foreign/read-continue.js +489 -0
- package/lib/foreign/read-copilot.d.ts +42 -0
- package/lib/foreign/read-copilot.js +267 -0
- package/lib/foreign/read-crush.d.ts +54 -0
- package/lib/foreign/read-crush.js +478 -0
- package/lib/foreign/read-cursor.d.ts +141 -0
- package/lib/foreign/read-cursor.js +705 -0
- package/lib/foreign/read-dsh.d.ts +43 -0
- package/lib/foreign/read-dsh.js +205 -0
- package/lib/foreign/read-dsh4.d.ts +14 -0
- package/lib/foreign/read-dsh4.js +27 -0
- package/lib/foreign/read-gemini.d.ts +55 -0
- package/lib/foreign/read-gemini.js +273 -0
- package/lib/foreign/read-goose.d.ts +25 -0
- package/lib/foreign/read-goose.js +322 -0
- package/lib/foreign/read-grokbuild.d.ts +34 -0
- package/lib/foreign/read-grokbuild.js +455 -0
- package/lib/foreign/read-hermes.d.ts +74 -0
- package/lib/foreign/read-hermes.js +690 -0
- package/lib/foreign/read-kilocode.d.ts +17 -0
- package/lib/foreign/read-kilocode.js +46 -0
- package/lib/foreign/read-kimi.d.ts +63 -0
- package/lib/foreign/read-kimi.js +719 -0
- package/lib/foreign/read-mimocode.d.ts +12 -0
- package/lib/foreign/read-mimocode.js +56 -0
- package/lib/foreign/read-openclaw.d.ts +51 -0
- package/lib/foreign/read-openclaw.js +445 -0
- package/lib/foreign/read-opencode.d.ts +150 -0
- package/lib/foreign/read-opencode.js +896 -0
- package/lib/foreign/read-pi.d.ts +56 -0
- package/lib/foreign/read-pi.js +411 -0
- package/lib/foreign/read-qoder.d.ts +54 -0
- package/lib/foreign/read-qoder.js +317 -0
- package/lib/foreign/read-qwen.d.ts +40 -0
- package/lib/foreign/read-qwen.js +326 -0
- package/lib/foreign/read-reasonix.d.ts +66 -0
- package/lib/foreign/read-reasonix.js +537 -0
- package/lib/foreign/read-teleagent.d.ts +15 -0
- package/lib/foreign/read-teleagent.js +68 -0
- package/lib/foreign/read-trae.d.ts +67 -0
- package/lib/foreign/read-trae.js +492 -0
- package/lib/foreign/read-vibe.d.ts +28 -0
- package/lib/foreign/read-vibe.js +364 -0
- package/lib/foreign/read-workbuddy.d.ts +49 -0
- package/lib/foreign/read-workbuddy.js +346 -0
- package/lib/foreign/read-zcode.d.ts +12 -0
- package/lib/foreign/read-zcode.js +38 -0
- package/lib/foreign/read-zed.d.ts +30 -0
- package/lib/foreign/read-zed.js +473 -0
- package/lib/foreign/reasonix.d.ts +6 -0
- package/lib/foreign/reasonix.js +20 -0
- package/lib/foreign/registry.d.ts +196 -0
- package/lib/foreign/registry.js +573 -0
- package/lib/foreign/session-ir.d.ts +186 -0
- package/lib/foreign/session-ir.js +427 -0
- package/lib/foreign/session-read.d.ts +102 -0
- package/lib/foreign/session-read.js +254 -0
- package/lib/foreign/session-source.d.ts +216 -0
- package/lib/foreign/session-source.js +453 -0
- package/lib/foreign/source-modules.d.ts +45 -0
- package/lib/foreign/source-modules.js +52 -0
- package/lib/foreign/sqlite.d.ts +61 -0
- package/lib/foreign/sqlite.js +404 -0
- package/lib/foreign/teleagent.d.ts +11 -0
- package/lib/foreign/teleagent.js +53 -0
- package/lib/foreign/trae.d.ts +6 -0
- package/lib/foreign/trae.js +20 -0
- package/lib/foreign/truth-table.d.ts +62 -0
- package/lib/foreign/truth-table.js +333 -0
- package/lib/foreign/types.d.ts +272 -0
- package/lib/foreign/types.js +2 -0
- package/lib/foreign/vibe.d.ts +6 -0
- package/lib/foreign/vibe.js +23 -0
- package/lib/foreign/workbuddy.d.ts +7 -0
- package/lib/foreign/workbuddy.js +45 -0
- package/lib/foreign/zcode.d.ts +11 -0
- package/lib/foreign/zcode.js +51 -0
- package/lib/foreign/zed.d.ts +11 -0
- package/lib/foreign/zed.js +53 -0
- package/lib/index.d.ts +154 -13
- package/lib/index.js +661 -84
- package/lib/market/github-repos.d.ts +28 -0
- package/lib/market/github-repos.js +43 -1
- package/lib/market/types.d.ts +1 -3
- package/lib/profiles/dsh-profile-manager.d.ts +54 -2
- package/lib/profiles/dsh-profile-manager.js +182 -11
- package/lib/profiles/dsh-profile-shared.d.ts +47 -0
- package/lib/profiles/dsh-profile-shared.js +15 -1
- package/lib/profiles/index.d.ts +1 -1
- package/lib/profiles/index.js +1 -1
- package/lib/routes/consult.js +51 -5
- package/lib/routes/foreign-context.d.ts +43 -0
- package/lib/routes/foreign-context.js +65 -0
- package/lib/routes/foreign.d.ts +11 -0
- package/lib/routes/foreign.js +208 -0
- package/lib/routes/import.d.ts +8 -0
- package/lib/routes/import.js +22 -1
- package/lib/routes/index.d.ts +2 -2
- package/lib/routes/index.js +4 -0
- package/lib/routes/kit.d.ts +4 -3
- package/lib/routes/market.js +0 -1
- package/lib/routes/prefs.d.ts +2 -1
- package/lib/routes/prefs.js +14 -2
- package/lib/routes/recovery.js +363 -3
- package/lib/routes/session-export.d.ts +117 -0
- package/lib/routes/session-export.js +270 -0
- package/lib/routes/snapshots.js +12 -2
- package/lib/routes/sync.js +82 -10
- package/lib/security/redaction.d.ts +14 -0
- package/lib/security/redaction.js +24 -9
- package/lib/security/secret-scanner.d.ts +27 -0
- package/lib/security/secret-scanner.js +54 -1
- package/lib/sync/autosync-scheduler.d.ts +13 -1
- package/lib/sync/autosync-scheduler.js +49 -26
- package/lib/sync/backup-files.js +6 -0
- package/lib/sync/gist/gist-transport.d.ts +91 -0
- package/lib/sync/gist/gist-transport.js +406 -0
- package/lib/sync/gist/messages.d.ts +22 -0
- package/lib/sync/gist/messages.js +46 -0
- package/lib/sync/s3/messages.d.ts +53 -0
- package/lib/sync/s3/messages.js +83 -0
- package/lib/sync/s3/s3-providers.d.ts +78 -0
- package/lib/sync/s3/s3-providers.js +156 -0
- package/lib/sync/s3/s3-transport.d.ts +126 -0
- package/lib/sync/s3/s3-transport.js +551 -0
- package/lib/sync/s3/sigv4.d.ts +107 -0
- package/lib/sync/s3/sigv4.js +196 -0
- package/lib/sync/sync-config.d.ts +250 -4
- package/lib/sync/sync-config.js +658 -57
- package/lib/sync/sync-engine.d.ts +1 -0
- package/lib/sync/sync-engine.js +4 -2
- package/lib/sync/sync-history.d.ts +1 -1
- package/lib/sync/webdav/webdav-transport.d.ts +31 -3
- package/lib/sync/webdav/webdav-transport.js +102 -18
- package/lib/ui/commands.d.ts +4 -3
- package/lib/ui/commands.js +41 -9
- package/lib/ui/disk-usage-view.d.ts +44 -0
- package/lib/ui/disk-usage-view.js +55 -1
- package/lib/ui/dsh-profiles-view.d.ts +53 -0
- package/lib/ui/dsh-profiles-view.js +56 -0
- package/lib/ui/foreign-view.d.ts +162 -0
- package/lib/ui/foreign-view.js +225 -0
- package/lib/ui/i18n.d.ts +77 -0
- package/lib/ui/i18n.js +164 -0
- package/lib/ui/session-inventory-view.d.ts +106 -0
- package/lib/ui/session-inventory-view.js +58 -0
- package/lib/ui/session-layout-view.d.ts +265 -0
- package/lib/ui/session-layout-view.js +271 -0
- package/lib/ui/sync-repository-picker.d.ts +98 -0
- package/lib/ui/sync-repository-picker.js +145 -0
- package/lib/ui/sync-settings-view.d.ts +143 -4
- package/lib/ui/sync-settings-view.js +231 -7
- package/lib/ui/types.d.ts +47 -0
- package/lib/utils/asar-read.d.ts +113 -0
- package/lib/utils/asar-read.js +523 -0
- package/lib/utils/env-lock.js +7 -2
- package/lib/utils/patch-yaml.d.ts +25 -0
- package/lib/utils/patch-yaml.js +62 -0
- package/lib/utils/paths.d.ts +10 -1
- package/lib/utils/paths.js +24 -3
- package/lib/utils/recursive-walk.js +12 -13
- package/lib/utils/session-format.d.ts +35 -2
- package/lib/utils/session-format.js +131 -5
- package/lib/utils/session-health-scan.d.ts +7 -1
- package/lib/utils/session-health-scan.js +369 -12
- package/lib/utils/session-layout-repair-service.d.ts +98 -0
- package/lib/utils/session-layout-repair-service.js +457 -0
- package/lib/utils/session-log-repair.d.ts +27 -0
- package/lib/utils/session-log-repair.js +233 -62
- package/lib/utils/session-repair-service.d.ts +32 -3
- package/lib/utils/session-repair-service.js +148 -9
- package/lib/utils/session-row-facts.d.ts +73 -0
- package/lib/utils/session-row-facts.js +182 -0
- package/lib/utils/session-verify.d.ts +118 -0
- package/lib/utils/session-verify.js +705 -0
- package/lib/utils/shared-constants.d.ts +59 -0
- package/lib/utils/shared-constants.js +71 -0
- package/package.json +1 -1
- package/src/adapters/credentials.test.ts +27 -0
- package/src/adapters/credentials.ts +6 -3
- package/src/adapters/file-collection.ts +33 -8
- package/src/adapters/files.test.ts +222 -0
- package/src/adapters/mcp.test.ts +43 -0
- package/src/adapters/mcp.ts +37 -14
- package/src/adapters/plugin-files.ts +34 -4
- package/src/adapters/plugins.test.ts +241 -0
- package/src/adapters/plugins.ts +201 -53
- package/src/adapters/prompts.test.ts +49 -0
- package/src/adapters/prompts.ts +41 -20
- package/src/adapters/providers.test.ts +16 -0
- package/src/adapters/providers.ts +4 -3
- package/src/adapters/self.test.ts +77 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.test.ts +77 -0
- package/src/adapters/sessions.ts +47 -1
- package/src/adapters/settings.test.ts +78 -0
- package/src/adapters/settings.ts +42 -6
- package/src/adapters/skills.ts +221 -3
- package/src/adapters/test-helpers.ts +43 -5
- package/src/adapters/workspaces-dup-id.test.ts +144 -0
- package/src/adapters/workspaces.test.ts +10 -0
- package/src/adapters/workspaces.ts +97 -5
- package/src/cli/actions.test.ts +117 -2
- package/src/cli/actions.ts +132 -12
- package/src/cli/help.ts +570 -0
- package/src/cli/import-source.test.ts +193 -0
- package/src/cli/import-source.ts +229 -0
- package/src/cli/index.ts +129 -94
- package/src/cli/web/client-script.ts +12 -0
- package/src/cli/web/console-fixes.test.ts +36 -0
- package/src/cli/web/http.ts +7 -1
- package/src/cli/web/page.ts +108 -25
- package/src/cli/web/routes.ts +23 -4
- package/src/cli/web/script-anchor.test.ts +33 -0
- package/src/cli/web/server.ts +28 -2
- package/src/cli/web/web.test.ts +447 -4
- package/src/client/ConfigManagerSection.tsx +118 -63
- package/src/client/about/AboutPanel.tsx +42 -3
- package/src/client/about/PluginUpdateDialog.tsx +185 -0
- package/src/client/about/ReleaseBody.tsx +144 -0
- package/src/client/about/ReleaseNotesDialog.tsx +3 -130
- package/src/client/about/about-view.ts +7 -5
- package/src/client/about/plugin-update-view.test.ts +25 -0
- package/src/client/about/plugin-update-view.ts +26 -0
- package/src/client/about/release-notes-view.test.ts +33 -0
- package/src/client/about/release-notes-view.ts +34 -0
- package/src/client/api.ts +145 -1
- package/src/client/common/Icon.tsx +3 -0
- package/src/client/common/RunsCenter.tsx +8 -2
- package/src/client/common/Select.tsx +9 -2
- package/src/client/common/info-hint-guard.test.ts +63 -18
- package/src/client/common/plan-text-redaction.test.ts +193 -8
- package/src/client/common/routes.ts +15 -0
- package/src/client/config-manager.module.css +506 -354
- package/src/client/css-scale-guard.test.ts +122 -0
- package/src/client/css-token-guard.test.ts +82 -0
- package/src/client/environment/EnvironmentPanel.tsx +45 -16
- package/src/client/{snapshots → environment/maintenance}/DiskUsageCard.tsx +21 -17
- package/src/client/environment/profiles-incomplete-ui.test.ts +103 -0
- package/src/client/export/ExportView.tsx +9 -2
- package/src/client/{snapshots → home}/BackupScheduleCard.tsx +13 -4
- package/src/client/home/HomePanel.tsx +69 -157
- package/src/client/icon-layer-guard.test.ts +1 -1
- package/src/client/import/ForeignImportView.tsx +318 -0
- package/src/client/import/ImportWizardView.tsx +3 -1
- package/src/client/import/foreign-import-view-guard.test.ts +195 -0
- package/src/client/import/import-file-select.test.ts +6 -6
- package/src/client/import/import-wizard-steps.tsx +17 -1
- package/src/client/import/use-import-wizard-controller.ts +3 -3
- package/src/client/index.ts +1 -1
- package/src/client/library/LibraryActions.tsx +31 -34
- package/src/client/library/LibraryPanel.tsx +4 -1
- package/src/client/{snapshots → library}/RestorePlanView.tsx +1 -1
- package/src/client/{snapshots → library}/files-layout.test.ts +2 -2
- package/src/client/{snapshots → library}/report-dismiss.test.ts +26 -3
- package/src/client/library/task-panel-visibility.test.ts +25 -17
- package/src/client/locales.ts +278 -36
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +11 -2
- package/src/client/recovery/IncompleteCopiesSection.tsx +132 -0
- package/src/client/recovery/RecoveryPanel.tsx +486 -59
- package/src/client/recovery/recovery-api.ts +30 -0
- package/src/client/recovery/recovery-incomplete-copies.test.ts +151 -0
- package/src/client/recovery/recovery-locales.test.ts +88 -0
- package/src/client/recovery/recovery-locales.ts +321 -6
- package/src/client/recovery/recovery-panel-layout.test.ts +52 -0
- package/src/client/recovery/recovery-view.ts +92 -2
- package/src/client/run-store.test.ts +201 -89
- package/src/client/run-store.ts +114 -143
- package/src/client/session-export/session-export-view.test.ts +103 -0
- package/src/client/session-export/session-export-view.ts +118 -0
- package/src/client/sync/ChannelConfigDialog.tsx +219 -14
- package/src/client/sync/SyncChannelEntryCard.tsx +52 -4
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +166 -102
- package/src/client/sync/SyncRepositoryPicker.tsx +216 -0
- package/src/client/sync/SyncSettingsView.test.ts +6 -1
- package/src/client/sync/history-model.test.ts +94 -57
- package/src/client/sync/history-model.ts +44 -25
- package/src/client/sync/sync-api.test.ts +56 -0
- package/src/client/sync/sync-api.ts +92 -0
- package/src/client/sync/sync-locales.ts +162 -2
- package/src/client/sync/sync-view.test.ts +3 -8
- package/src/client/sync/sync-view.ts +111 -21
- package/src/core/analyzer-cache.test.ts +47 -0
- package/src/core/analyzer.ts +115 -14
- package/src/core/backup-plan.test.ts +35 -0
- package/src/core/backup-plan.ts +19 -1
- package/src/core/backup.test.ts +37 -1
- package/src/core/backup.ts +27 -6
- package/src/core/cache-cleaner.test.ts +120 -1
- package/src/core/cache-cleaner.ts +59 -22
- package/src/core/checkpoint.test.ts +665 -0
- package/src/core/checkpoint.ts +1614 -0
- package/src/core/credential-plan-consistency.test.ts +93 -0
- package/src/core/incident-wiring.test.ts +6 -2
- package/src/core/journal.test.ts +70 -0
- package/src/core/journal.ts +68 -0
- package/src/core/messages.ts +26 -0
- package/src/core/model-tools.ts +3 -0
- package/src/core/patch-layers.test.ts +94 -0
- package/src/core/patch-layers.ts +150 -0
- package/src/core/restore.ts +59 -3
- package/src/core/rollback.ts +6 -3
- package/src/core/section-content-consistency.test.ts +185 -0
- package/src/core/self-update.test.ts +110 -0
- package/src/core/self-update.ts +118 -0
- package/src/core/session-health.test.ts +91 -1
- package/src/core/session-health.ts +56 -9
- package/src/core/types.ts +127 -1
- package/src/foreign/antigravity.test.ts +271 -0
- package/src/foreign/antigravity.ts +188 -0
- package/src/foreign/bundle.ts +108 -0
- package/src/foreign/chatgpt.test.ts +279 -0
- package/src/foreign/chatgpt.ts +30 -0
- package/src/foreign/claude-code.test.ts +234 -0
- package/src/foreign/claude-code.ts +106 -0
- package/src/foreign/claude-sessions-bytes.test.ts +227 -0
- package/src/foreign/claude-sessions.test.ts +180 -0
- package/src/foreign/claude-sessions.ts +461 -0
- package/src/foreign/cline.test.ts +274 -0
- package/src/foreign/cline.ts +32 -0
- package/src/foreign/codex.test.ts +585 -0
- package/src/foreign/codex.ts +409 -0
- package/src/foreign/continue.test.ts +365 -0
- package/src/foreign/continue.ts +73 -0
- package/src/foreign/copilot.test.ts +273 -0
- package/src/foreign/copilot.ts +133 -0
- package/src/foreign/crush.test.ts +234 -0
- package/src/foreign/crush.ts +66 -0
- package/src/foreign/cursor.test.ts +509 -0
- package/src/foreign/cursor.ts +259 -0
- package/src/foreign/dsh.test.ts +198 -0
- package/src/foreign/dsh.ts +117 -0
- package/src/foreign/dsh4.test.ts +127 -0
- package/src/foreign/dsh4.ts +15 -0
- package/src/foreign/file-budget.test.ts +256 -0
- package/src/foreign/fixtures/antigravity/README.md +18 -0
- package/src/foreign/fixtures/antigravity/basic/gemini/antigravity/mcp_config.json +13 -0
- package/src/foreign/fixtures/antigravity/basic/gemini/config/mcp_config.json +24 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_config.json +0 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_oauth_tokens.json +0 -0
- package/src/foreign/fixtures/antigravity/empty/gemini/config/mcp_config.json +0 -0
- package/src/foreign/fixtures/codex/README.md +31 -0
- package/src/foreign/fixtures/codex/basic/.agents/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/codex/basic/.agents/skills/wrapper/nested/SKILL.md +6 -0
- package/src/foreign/fixtures/codex/basic/.codex/AGENTS.md +4 -0
- package/src/foreign/fixtures/codex/basic/.codex/config.toml +28 -0
- package/src/foreign/fixtures/codex/malformed/.codex/config.toml +3 -0
- package/src/foreign/fixtures/codex/override/.codex/AGENTS.md +1 -0
- package/src/foreign/fixtures/codex/override/.codex/AGENTS.override.md +3 -0
- package/src/foreign/fixtures/codex/sessions/.codex/archived_sessions/rollout-2026-10-05T09-00-00-22222222-2222-4222-8222-222222222222.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T10-00-00-11111111-1111-4111-8111-111111111111.jsonl +15 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-00-00-33333333-3333-4333-8333-333333333333.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-30-00-55555555-5555-4555-8555-555555555555.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T12-00-00-44444444-4444-4444-8444-444444444444.jsonl +2 -0
- package/src/foreign/fixtures/codex/sessions/.codex/sessions/notes.jsonl +1 -0
- package/src/foreign/fixtures/copilot/README.md +22 -0
- package/src/foreign/fixtures/copilot/basic/copilot-instructions.md +3 -0
- package/src/foreign/fixtures/copilot/basic/instructions/empty.instructions.md +1 -0
- package/src/foreign/fixtures/copilot/basic/instructions/repo.instructions.md +5 -0
- package/src/foreign/fixtures/copilot/basic/mcp-config.json +24 -0
- package/src/foreign/fixtures/copilot/basic/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/copilot/overridden/mcp-config.json +10 -0
- package/src/foreign/fixtures/cursor/README.md +17 -0
- package/src/foreign/fixtures/cursor/alt/.cursor/mcp.json +5 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/mcp.json +18 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/agent.mdc +5 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/always.mdc +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/auto.mdc +7 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/rules/manual.mdc +3 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/.hidden/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/notes.md +1 -0
- package/src/foreign/fixtures/cursor/basic/.cursorrules +1 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/mcp.json +9 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/rules/project-always.mdc +5 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursor/skills/hello/SKILL.md +6 -0
- package/src/foreign/fixtures/cursor/basic/project/.cursorrules +1 -0
- package/src/foreign/fixtures/cursor/empty/.cursor/mcp.json +0 -0
- package/src/foreign/fixtures/cursor/malformed/.cursor/mcp.json +3 -0
- package/src/foreign/fixtures/cursor/malformed/.cursor/rules/broken.mdc +6 -0
- package/src/foreign/fixtures/cursor/shape/.cursor/mcp.json +3 -0
- package/src/foreign/fixtures/hermes/README.md +14 -0
- package/src/foreign/fixtures/hermes/basic/SOUL.md +3 -0
- package/src/foreign/fixtures/hermes/basic/config.yaml +48 -0
- package/src/foreign/fixtures/hermes/basic/dotenv.fixture +1 -0
- package/src/foreign/fixtures/hermes/basic/memories/MEMORY.md +1 -0
- package/src/foreign/fixtures/hermes/basic/memories/USER.md +1 -0
- package/src/foreign/fixtures/hermes/basic/skills/se-team-design/SKILL.md +6 -0
- package/src/foreign/fixtures/hermes/basic/skills/se-team-design/examples/usage.md +1 -0
- package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/SKILL.md +6 -0
- package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/notes.md +1 -0
- package/src/foreign/fixtures/hermes/basic/state.db +1 -0
- package/src/foreign/fixtures/hermes/empty/config.yaml +0 -0
- package/src/foreign/gemini.test.ts +291 -0
- package/src/foreign/gemini.ts +52 -0
- package/src/foreign/goose.test.ts +196 -0
- package/src/foreign/goose.ts +62 -0
- package/src/foreign/grokbuild.test.ts +246 -0
- package/src/foreign/grokbuild.ts +30 -0
- package/src/foreign/hermes.test.ts +545 -0
- package/src/foreign/hermes.ts +86 -0
- package/src/foreign/kernel-credentials.test.ts +88 -0
- package/src/foreign/kernel-workspaces.test.ts +88 -0
- package/src/foreign/kernel.ts +584 -0
- package/src/foreign/kilocode.test.ts +186 -0
- package/src/foreign/kilocode.ts +60 -0
- package/src/foreign/kimi.test.ts +259 -0
- package/src/foreign/kimi.ts +55 -0
- package/src/foreign/mcp-value-shape.test.ts +286 -0
- package/src/foreign/mimocode.test.ts +178 -0
- package/src/foreign/mimocode.ts +60 -0
- package/src/foreign/openclaw.test.ts +248 -0
- package/src/foreign/openclaw.ts +54 -0
- package/src/foreign/opencode.test.ts +376 -0
- package/src/foreign/opencode.ts +61 -0
- package/src/foreign/pi.test.ts +275 -0
- package/src/foreign/pi.ts +56 -0
- package/src/foreign/platform-paths.ts +156 -0
- package/src/foreign/qoder.test.ts +266 -0
- package/src/foreign/qoder.ts +57 -0
- package/src/foreign/qwen.test.ts +203 -0
- package/src/foreign/qwen.ts +53 -0
- package/src/foreign/read-antigravity.ts +569 -0
- package/src/foreign/read-chatgpt.ts +506 -0
- package/src/foreign/read-claude-code.ts +264 -0
- package/src/foreign/read-cline.ts +517 -0
- package/src/foreign/read-codex.ts +823 -0
- package/src/foreign/read-continue.ts +557 -0
- package/src/foreign/read-copilot.ts +304 -0
- package/src/foreign/read-crush.ts +503 -0
- package/src/foreign/read-cursor.ts +778 -0
- package/src/foreign/read-dsh.ts +215 -0
- package/src/foreign/read-dsh4.ts +35 -0
- package/src/foreign/read-gemini.ts +306 -0
- package/src/foreign/read-goose.ts +333 -0
- package/src/foreign/read-grokbuild.ts +475 -0
- package/src/foreign/read-hermes.ts +724 -0
- package/src/foreign/read-kilocode.ts +55 -0
- package/src/foreign/read-kimi.ts +768 -0
- package/src/foreign/read-mimocode.ts +66 -0
- package/src/foreign/read-openclaw.ts +505 -0
- package/src/foreign/read-opencode.ts +994 -0
- package/src/foreign/read-pi.ts +459 -0
- package/src/foreign/read-qoder.ts +351 -0
- package/src/foreign/read-qwen.ts +342 -0
- package/src/foreign/read-reasonix.ts +590 -0
- package/src/foreign/read-teleagent.ts +82 -0
- package/src/foreign/read-trae.ts +488 -0
- package/src/foreign/read-vibe.ts +353 -0
- package/src/foreign/read-workbuddy.ts +374 -0
- package/src/foreign/read-zcode.ts +48 -0
- package/src/foreign/read-zed.ts +479 -0
- package/src/foreign/reasonix.test.ts +336 -0
- package/src/foreign/reasonix.ts +25 -0
- package/src/foreign/registry.test.ts +377 -0
- package/src/foreign/registry.ts +750 -0
- package/src/foreign/session-ir.test.ts +189 -0
- package/src/foreign/session-ir.ts +614 -0
- package/src/foreign/session-read.ts +295 -0
- package/src/foreign/session-source.ts +625 -0
- package/src/foreign/skill-file-caps.test.ts +171 -0
- package/src/foreign/source-modules.ts +86 -0
- package/src/foreign/source-registry.test.ts +190 -0
- package/src/foreign/sqlite.test.ts +313 -0
- package/src/foreign/sqlite.ts +446 -0
- package/src/foreign/teleagent.test.ts +147 -0
- package/src/foreign/teleagent.ts +62 -0
- package/src/foreign/trae.test.ts +316 -0
- package/src/foreign/trae.ts +25 -0
- package/src/foreign/truncation-report.test.ts +238 -0
- package/src/foreign/truth-table.ts +453 -0
- package/src/foreign/types.ts +312 -0
- package/src/foreign/vibe.test.ts +209 -0
- package/src/foreign/vibe.ts +28 -0
- package/src/foreign/workbuddy.test.ts +207 -0
- package/src/foreign/workbuddy.ts +53 -0
- package/src/foreign/zcode.test.ts +224 -0
- package/src/foreign/zcode.ts +60 -0
- package/src/foreign/zed.test.ts +240 -0
- package/src/foreign/zed.ts +62 -0
- package/src/index.facade.test.ts +254 -16
- package/src/index.ts +704 -82
- package/src/market/github-repos.test.ts +98 -1
- package/src/market/github-repos.ts +63 -1
- package/src/market/types.ts +3 -3
- package/src/profiles/dsh-profile-incomplete-copy.test.ts +177 -0
- package/src/profiles/dsh-profile-manager.test.ts +37 -0
- package/src/profiles/dsh-profile-manager.ts +188 -13
- package/src/profiles/dsh-profile-shared.ts +55 -1
- package/src/profiles/index.ts +2 -1
- package/src/routes/consult.ts +53 -4
- package/src/routes/foreign-context.ts +89 -0
- package/src/routes/foreign.test.ts +443 -0
- package/src/routes/foreign.ts +226 -0
- package/src/routes/import.ts +23 -1
- package/src/routes/index.ts +6 -2
- package/src/routes/kit.ts +4 -3
- package/src/routes/market.ts +0 -1
- package/src/routes/prefs.ts +15 -1
- package/src/routes/recovery.test.ts +167 -0
- package/src/routes/recovery.ts +331 -1
- package/src/routes/session-export.ts +308 -0
- package/src/routes/snapshots.ts +12 -2
- package/src/routes/sync.ts +91 -7
- package/src/security/redaction.test.ts +149 -0
- package/src/security/redaction.ts +25 -9
- package/src/security/secret-scanner.test.ts +186 -0
- package/src/security/secret-scanner.ts +57 -1
- package/src/sync/autosync-risk-tier.test.ts +61 -0
- package/src/sync/autosync-scheduler.test.ts +48 -1
- package/src/sync/autosync-scheduler.ts +50 -26
- package/src/sync/backup-files.ts +5 -0
- package/src/sync/gist/gist-msgkeys-catalog.test.ts +33 -0
- package/src/sync/gist/gist-msgkeys.test.ts +71 -0
- package/src/sync/gist/gist-transport.test.ts +308 -0
- package/src/sync/gist/gist-transport.ts +550 -0
- package/src/sync/gist/messages.ts +49 -0
- package/src/sync/retention-prune-all-off.test.ts +50 -0
- package/src/sync/s3/cloud-config.test.ts +283 -0
- package/src/sync/s3/messages.ts +102 -0
- package/src/sync/s3/s3-delete-index-404.test.ts +170 -0
- package/src/sync/s3/s3-delete-index-unreadable.test.ts +141 -0
- package/src/sync/s3/s3-msgkeys.test.ts +101 -0
- package/src/sync/s3/s3-providers.ts +213 -0
- package/src/sync/s3/s3-transport.test.ts +394 -0
- package/src/sync/s3/s3-transport.ts +651 -0
- package/src/sync/s3/s3-upload-index-404.test.ts +171 -0
- package/src/sync/s3/sigv4.test.ts +219 -0
- package/src/sync/s3/sigv4.ts +283 -0
- package/src/sync/sync-config.test.ts +237 -5
- package/src/sync/sync-config.ts +841 -52
- package/src/sync/sync-engine-download-tombstone.test.ts +73 -0
- package/src/sync/sync-engine.ts +5 -3
- package/src/sync/sync-history.ts +1 -1
- package/src/sync/webdav/webdav-delete-index-404.test.ts +178 -0
- package/src/sync/webdav/webdav-delete-index-unreadable.test.ts +143 -0
- package/src/sync/webdav/webdav-msgkeys.test.ts +53 -0
- package/src/sync/webdav/webdav-transport.test.ts +9 -5
- package/src/sync/webdav/webdav-transport.ts +99 -17
- package/src/sync/webdav/webdav-upload-index-404.test.ts +177 -0
- package/src/ui/commands.test.ts +4 -1
- package/src/ui/commands.ts +46 -12
- package/src/ui/disk-usage-view.test.ts +63 -1
- package/src/ui/disk-usage-view.ts +93 -1
- package/src/ui/dsh-profiles-incomplete-view.test.ts +102 -0
- package/src/ui/dsh-profiles-view.ts +94 -0
- package/src/ui/foreign-view.test.ts +241 -0
- package/src/ui/foreign-view.ts +362 -0
- package/src/ui/i18n.ts +164 -0
- package/src/ui/mid-ellipsis.test.ts +5 -10
- package/src/ui/session-inventory-view.test.ts +142 -2
- package/src/ui/session-inventory-view.ts +127 -0
- package/src/ui/session-layout-view.test.ts +361 -0
- package/src/ui/session-layout-view.ts +444 -0
- package/src/ui/sync-repository-picker.test.ts +130 -0
- package/src/ui/sync-repository-picker.ts +180 -0
- package/src/ui/sync-settings-view.test.ts +166 -0
- package/src/ui/sync-settings-view.ts +319 -9
- package/src/ui/types.ts +42 -0
- package/src/utils/asar-read.test.ts +330 -0
- package/src/utils/asar-read.ts +606 -0
- package/src/utils/env-lock.test.ts +55 -0
- package/src/utils/env-lock.ts +6 -1
- package/src/utils/patch-yaml.test.ts +112 -0
- package/src/utils/patch-yaml.ts +72 -0
- package/src/utils/paths.test.ts +63 -0
- package/src/utils/paths.ts +25 -3
- package/src/utils/recursive-walk.test.ts +20 -0
- package/src/utils/recursive-walk.ts +12 -13
- package/src/utils/session-format.test.ts +113 -2
- package/src/utils/session-format.ts +127 -5
- package/src/utils/session-health-scan.test.ts +364 -1
- package/src/utils/session-health-scan.ts +434 -15
- package/src/utils/session-layout-repair-service.test.ts +362 -0
- package/src/utils/session-layout-repair-service.ts +568 -0
- package/src/utils/session-log-repair.test.ts +260 -1
- package/src/utils/session-log-repair.ts +294 -55
- package/src/utils/session-repair-service.test.ts +148 -1
- package/src/utils/session-repair-service.ts +165 -10
- package/src/utils/session-row-facts.test.ts +159 -0
- package/src/utils/session-row-facts.ts +187 -0
- package/src/utils/session-verify.test.ts +762 -0
- package/src/utils/session-verify.ts +784 -0
- package/src/utils/shared-constants.ts +82 -0
package/src/index.ts
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* ctx.plugins -> PluginsFacade (官方 dsh plugin CLI 通道 + profile 文件,
|
|
12
12
|
* 见 src/core/plugin-cli.ts)
|
|
13
13
|
* ctx.workspaceRegistry -> WorkspaceFacade (@deepseek-ai/dsh-workspace)
|
|
14
|
-
* ~/.dsh/cordis.patch.yml-> PatchFileFacade (js-yaml)
|
|
14
|
+
* ~/.dsh/cordis.patch.yml-> PatchFileFacade (js-yaml; home 层 + profiles/<p>/ 层)
|
|
15
|
+
* ctx.skills -> SkillStoreFacade (@deepseek-ai/dsh-skill 注册表,技能备份用)
|
|
15
16
|
* $DSH_HOME files -> FileSystemFacade (node:fs, home-relative)
|
|
16
17
|
* resolveDshHome() -> homeDir (@deepseek-ai/dsh-home-paths)
|
|
17
18
|
*
|
|
@@ -44,7 +45,7 @@ import { promisify } from 'node:util'
|
|
|
44
45
|
|
|
45
46
|
import type { Context } from '@deepseek-ai/cordis'
|
|
46
47
|
import * as dshSettings from '@deepseek-ai/dsh-settings'
|
|
47
|
-
import type { SettingsProvider } from '@deepseek-ai/dsh-settings'
|
|
48
|
+
import type { SettingsDescriptor, SettingsProvider } from '@deepseek-ai/dsh-settings'
|
|
48
49
|
import * as dshCredentials from '@deepseek-ai/dsh-credentials'
|
|
49
50
|
import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'
|
|
50
51
|
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
@@ -77,9 +78,13 @@ import { registerModelTools } from './core/model-tools.ts'
|
|
|
77
78
|
import { makeMsg, msgOf, zhMsg } from './core/messages.ts'
|
|
78
79
|
import type { MsgFunc } from './core/messages.ts'
|
|
79
80
|
import { cleanupAbortedInstall, hasDshBundlePatch, installAnchorFromProfileContext, installErrorFor, installSpecFor, listInstalledPlugins, profileNameFromProfileContext, resolveProcessProfileName, resolveProfileDir, readProfileManifest, runDshPlugin, validateProfileName } from './core/plugin-cli.ts'
|
|
80
|
-
import type { ConfigAdapter, CredentialsFacade, ExportUnit, FileSystemFacade, HostContext, ImportDecisions, ImportPlan, NamespaceInfo, PatchFileFacade, PlanItemKind, PluginInfo, PluginsFacade, SessionMoveResult, SessionParentRelation, SessionRewriteResult, SessionStoreFacade, SettingsFacade, WorkspaceFacade } from './core/types.ts'
|
|
81
|
+
import type { ConfigAdapter, CredentialsFacade, ExportUnit, FileSystemFacade, HostContext, ImportDecisions, ImportPlan, NamedNamespaceInfo, NamespaceInfo, PatchFileFacade, PlanItemKind, PluginInfo, PluginsFacade, SessionMoveResult, SessionParentRelation, SessionRewriteResult, SessionStoreFacade, SettingsFacade, SkillDefinitionView, SkillStoreFacade, SkillSummaryView, WorkspaceFacade } from './core/types.ts'
|
|
81
82
|
import { ImportUserSkippedError } from './core/types.ts'
|
|
82
|
-
import {
|
|
83
|
+
import { PLUGIN_NPM_PACKAGE } from './core/update-check.ts'
|
|
84
|
+
import { planSelfUpdate, runSelfUpdate } from './core/self-update.ts'
|
|
85
|
+
import type { SelfUpdateOutcome } from './core/self-update.ts'
|
|
86
|
+
import { createAdapters } from './adapters/index.ts'
|
|
87
|
+
import { isPatchLayerRel, profilePatchRel, readEffectivePatchLines, USER_PATCH_FILE } from './core/patch-layers.ts'
|
|
83
88
|
import { createLocalPluginPackHook } from './core/local-plugin-host.ts'
|
|
84
89
|
import { ENCRYPTED_CONTAINER_CODE, createEncryptionProvider, decryptCredentials, readContainerKind, SecurityError, encryptArchive } from './security/index.ts'
|
|
85
90
|
import { createHardenedZipParser } from './security/zip-security.ts'
|
|
@@ -88,7 +93,9 @@ import { applySessionMeta, applySessionMetaToPlanItems, applySessionParentLinks,
|
|
|
88
93
|
import { projectKeyOf, sessionIdKey } from './core/session-select.ts'
|
|
89
94
|
import { PROJECT_KEY_RE, readLogCwdFromBytes, rewriteSessionLogDir } from './utils/session-log.ts'
|
|
90
95
|
import { probeSessionFormats, resolveSessionFormatVersion } from './utils/session-format.ts'
|
|
96
|
+
import { describePatchYamlError, dumpPatchDocument, loadPatchDocument } from './utils/patch-yaml.ts'
|
|
91
97
|
import { atomicCopyFile, atomicWriteFile } from './utils/atomic-write.ts'
|
|
98
|
+
import { isENOENT } from './utils/guards.ts'
|
|
92
99
|
import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError, type MutationLockContext } from './utils/env-lock.ts'
|
|
93
100
|
import { activeProxySummary } from './utils/proxy.ts'
|
|
94
101
|
import { listRecursiveFollowingLinks } from './utils/recursive-walk.ts'
|
|
@@ -99,6 +106,8 @@ import type { MutationLockPort } from './utils/env-lock.ts'
|
|
|
99
106
|
import type { RecursiveListing } from './utils/recursive-walk.ts'
|
|
100
107
|
import { GitTransport } from './sync/git/git-transport.ts'
|
|
101
108
|
import { WebDavTransport } from './sync/webdav/webdav-transport.ts'
|
|
109
|
+
import { S3Transport } from './sync/s3/s3-transport.ts'
|
|
110
|
+
import { GistTransport } from './sync/gist/gist-transport.ts'
|
|
102
111
|
import { DeviceFlowStore, GitHubAuthClient } from './sync/github-auth.ts'
|
|
103
112
|
import { SyncEngine } from './sync/sync-engine.ts'
|
|
104
113
|
import { SyncSessionStore } from './sync/sync-session.ts'
|
|
@@ -114,7 +123,7 @@ import { readAllAutosyncConfigs, readAutosyncConfig } from './sync/autosync-conf
|
|
|
114
123
|
import type { AutosyncInterval, AutosyncRunStatus } from './sync/autosync-config.ts'
|
|
115
124
|
import { appendAutosyncEntry } from './sync/sync-history.ts'
|
|
116
125
|
import { MigrationStore, type MigrationKind, type MigrationResult, MIGRATION_HISTORY_DIR } from './core/migration-history.ts'
|
|
117
|
-
import { readSyncConfig, validateRepoUrl, validateWebDavUrl, isWebDavConfig, channelOf, parseSyncChannel, SYNC_CHANNELS } from './sync/sync-config.ts'
|
|
126
|
+
import { readSyncConfig, validateRepoUrl, validateWebDavUrl, isWebDavConfig, isS3Config, isGistConfig, channelOf, parseSyncChannel, SYNC_CHANNELS, GIST_PROVIDER, isS3CompatProvider, validateCloudChannelInput, cloudSecretRef } from './sync/sync-config.ts'
|
|
118
127
|
import {
|
|
119
128
|
resolveSessionFormatDisposition,
|
|
120
129
|
sessionFormatAbortResponse,
|
|
@@ -145,6 +154,7 @@ import { stringifyJsonSafe } from './utils/json.ts'
|
|
|
145
154
|
import type { Manifest, SectionId, WorkspaceRecord } from './schema/types.ts'
|
|
146
155
|
import { parseZip } from './utils/zip.ts'
|
|
147
156
|
import { isSameOrChild } from './utils/paths.ts'
|
|
157
|
+
import { isRecord } from './utils/guards.ts'
|
|
148
158
|
import { createLogger, parseLogLevel, type Logger } from './utils/logger.ts'
|
|
149
159
|
|
|
150
160
|
/* ---------------------------------------------------------------- identity */
|
|
@@ -156,7 +166,7 @@ export const name = 'config-manager'
|
|
|
156
166
|
export const inject = ['settings', 'credentials']
|
|
157
167
|
|
|
158
168
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
159
|
-
export const PLUGIN_VERSION = '0.1.
|
|
169
|
+
export const PLUGIN_VERSION = '0.1.71'
|
|
160
170
|
|
|
161
171
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
162
172
|
const PLUGIN_NAME = 'dsh-config-manager'
|
|
@@ -219,9 +229,9 @@ export interface Config {
|
|
|
219
229
|
/* ---------------------------------------------------------------- constants */
|
|
220
230
|
|
|
221
231
|
/**
|
|
222
|
-
* 本文件保留的
|
|
232
|
+
* 本文件保留的 7 条路由的路径(被源码级守卫按文件窗口钉住,见下方 routesList 注释)。
|
|
223
233
|
*
|
|
224
|
-
* 其余
|
|
234
|
+
* 其余 71 条路由的路径**声明在各自的组文件里**(src/routes/*.ts 的 endpoint({ path, methods }))——
|
|
225
235
|
* 新增一条 API 只需那一条声明,不再有「常量表 + 路由对象」两处要同步。
|
|
226
236
|
*/
|
|
227
237
|
export const API = {
|
|
@@ -387,6 +397,69 @@ function readService<T>(ctx: Context, serviceName: string): T | undefined {
|
|
|
387
397
|
return candidate === null || typeof candidate !== 'object' ? undefined : candidate as T
|
|
388
398
|
}
|
|
389
399
|
|
|
400
|
+
/**
|
|
401
|
+
* 把 DSH 的 `skills` 服务(@deepseek-ai/dsh-skill 的 SkillRegistry)适配成引擎的
|
|
402
|
+
* `SkillStoreFacade`(issue #71)。
|
|
403
|
+
*
|
|
404
|
+
* 为什么在这里做适配而不是让 adapter 直接调服务:架构边界测试禁止 core / adapters import
|
|
405
|
+
* `@deepseek-ai/*`(只有 src/index.ts 与 src/client/ 允许),所以适配层只能是宿主入口。
|
|
406
|
+
*
|
|
407
|
+
* 服务缺失(旧外壳)或形状不符 → 返回 undefined(skills 分区退回纯目录扫描,不报错);
|
|
408
|
+
* 单个方法的异常不在这里吞 —— 由 adapter 决定「这一个技能跳过」还是「整路跳过」。
|
|
409
|
+
*/
|
|
410
|
+
function resolveSkillStore(ctx: Context): SkillStoreFacade | undefined {
|
|
411
|
+
const service = readService<{
|
|
412
|
+
list?: (options?: unknown) => Promise<unknown>
|
|
413
|
+
get?: (name: string, options?: unknown) => Promise<unknown>
|
|
414
|
+
}>(ctx, 'skills')
|
|
415
|
+
if (service === undefined || typeof service.list !== 'function' || typeof service.get !== 'function') {
|
|
416
|
+
return undefined
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
// 不传 scope:外壳自身的技能 provider 都在 global 层(插件 apply 时注册、无 scope),
|
|
420
|
+
// 传 scope 反而会丢掉它们。范围参数只在调用方明确要「某次会话/某个 preset 的视图」时才需要。
|
|
421
|
+
async list(): Promise<SkillSummaryView[]> {
|
|
422
|
+
const raw = await service.list?.({})
|
|
423
|
+
if (!Array.isArray(raw)) return []
|
|
424
|
+
const out: SkillSummaryView[] = []
|
|
425
|
+
for (const item of raw) {
|
|
426
|
+
if (item === null || typeof item !== 'object') continue
|
|
427
|
+
const summary = item as Record<string, unknown>
|
|
428
|
+
if (typeof summary['name'] !== 'string') continue
|
|
429
|
+
out.push(toSkillSummaryView(summary))
|
|
430
|
+
}
|
|
431
|
+
return out
|
|
432
|
+
},
|
|
433
|
+
async get(name: string): Promise<SkillDefinitionView | undefined> {
|
|
434
|
+
const raw = await service.get?.(name, {})
|
|
435
|
+
if (raw === null || typeof raw !== 'object') return undefined
|
|
436
|
+
const definition = raw as Record<string, unknown>
|
|
437
|
+
if (typeof definition['name'] !== 'string' || typeof definition['content'] !== 'string') return undefined
|
|
438
|
+
return { ...toSkillSummaryView(definition), content: definition['content'] }
|
|
439
|
+
},
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** 技能摘要的结构适配(只取引擎声明过的字段;多余字段不落地,避免快照体积被未知字段撑大)。 */
|
|
444
|
+
function toSkillSummaryView(source: Record<string, unknown>): SkillSummaryView {
|
|
445
|
+
const out: SkillSummaryView = {
|
|
446
|
+
name: source['name'] as string,
|
|
447
|
+
description: typeof source['description'] === 'string' ? source['description'] : '',
|
|
448
|
+
}
|
|
449
|
+
if (typeof source['whenToUse'] === 'string') out.whenToUse = source['whenToUse']
|
|
450
|
+
if (typeof source['source'] === 'string') out.source = source['source']
|
|
451
|
+
if (typeof source['provider'] === 'string') out.provider = source['provider']
|
|
452
|
+
const invocation = source['invocation']
|
|
453
|
+
if (invocation !== null && typeof invocation === 'object') {
|
|
454
|
+
const policy = invocation as Record<string, unknown>
|
|
455
|
+
const view: { modelInvocable?: boolean; userInvocable?: boolean } = {}
|
|
456
|
+
if (typeof policy['modelInvocable'] === 'boolean') view.modelInvocable = policy['modelInvocable']
|
|
457
|
+
if (typeof policy['userInvocable'] === 'boolean') view.userInvocable = policy['userInvocable']
|
|
458
|
+
out.invocation = view
|
|
459
|
+
}
|
|
460
|
+
return out
|
|
461
|
+
}
|
|
462
|
+
|
|
390
463
|
/** Safe settings namespace converter compatible across DSH 0.1.1 and 0.1.2-alpha.x */
|
|
391
464
|
const SETTINGS_NAMESPACE_REGEX = /^[a-z][a-z0-9-]*$/
|
|
392
465
|
function safeSettingsNamespace(namespace: string): any {
|
|
@@ -427,10 +500,8 @@ class DshSettingsFacade implements SettingsFacade {
|
|
|
427
500
|
return this.ctx.settings
|
|
428
501
|
}
|
|
429
502
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const descriptor = all.find((d) => String(d.ns) === namespace)
|
|
433
|
-
if (!descriptor) throw new Error(`namespace not found: ${namespace}`)
|
|
503
|
+
/** DSH 描述符 → 核心 NamespaceInfo(describe/describeAll 共用,保证两条路径逐字段同口径) */
|
|
504
|
+
private toNamespaceInfo(descriptor: SettingsDescriptor): NamespaceInfo {
|
|
434
505
|
return {
|
|
435
506
|
value: descriptor.value,
|
|
436
507
|
base: descriptor.base,
|
|
@@ -441,6 +512,25 @@ class DshSettingsFacade implements SettingsFacade {
|
|
|
441
512
|
}
|
|
442
513
|
}
|
|
443
514
|
|
|
515
|
+
async describe(namespace: string, opts?: { redactSecrets?: boolean }): Promise<NamespaceInfo> {
|
|
516
|
+
const found = (await this.describeAll(opts)).find((d) => d.ns === namespace)
|
|
517
|
+
if (!found) throw new Error(`namespace not found: ${namespace}`)
|
|
518
|
+
return found.info
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 一次读回全部已注册 namespace。
|
|
523
|
+
*
|
|
524
|
+
* 真实服务侧 describe() 是「无参 = 全量」的:每次调用都对全部注册项重跑
|
|
525
|
+
* schema.toJSON + structuredClone + redactSecrets。逐名调用因此是 O(N²)
|
|
526
|
+
* (真机 24 个 namespace ≈1.7 s),调用方改用本方法后只需一次全量(≈70 ms)。
|
|
527
|
+
*/
|
|
528
|
+
async describeAll(opts?: { redactSecrets?: boolean }): Promise<NamedNamespaceInfo[]> {
|
|
529
|
+
return this.provider()
|
|
530
|
+
.describe({ redactSecrets: opts?.redactSecrets ?? true })
|
|
531
|
+
.map((descriptor) => ({ ns: String(descriptor.ns), info: this.toNamespaceInfo(descriptor) }))
|
|
532
|
+
}
|
|
533
|
+
|
|
444
534
|
async replace(namespace: string, value: unknown, expectedRevision?: number): Promise<void> {
|
|
445
535
|
await this.provider().replace(safeSettingsNamespace(namespace), value as object, expectedRevision)
|
|
446
536
|
}
|
|
@@ -489,13 +579,24 @@ export function patchRowActivates(raw: unknown, name: string): boolean {
|
|
|
489
579
|
* 非 bundle 插件安装成功后,幂等补 profile cordis.patch.yml 激活行
|
|
490
580
|
* ({id: pm-<slug>, name: <pkg>},仿 marketplace ensureRow)。bundle 包不写行
|
|
491
581
|
* (CLI 的 reconcile 已维护 dsh.profile.bundles)。
|
|
582
|
+
*
|
|
583
|
+
* 第 4 参 `profile`(issue #71):patch 的层身份是**相对 homeDir 的路径**,
|
|
584
|
+
* 目标层 = `profiles/<profile>/cordis.patch.yml`。省略时按缺省档案名(web)—— 兼容旧调用方。
|
|
492
585
|
*/
|
|
493
|
-
export async function ensureActivationRow(
|
|
586
|
+
export async function ensureActivationRow(
|
|
587
|
+
patchFile: PatchFileFacade,
|
|
588
|
+
pkgDir: string,
|
|
589
|
+
pkg: string,
|
|
590
|
+
profile?: string,
|
|
591
|
+
): Promise<void> {
|
|
494
592
|
if (hasDshBundlePatch(pkgDir)) return
|
|
495
|
-
const
|
|
496
|
-
|
|
593
|
+
const file = profilePatchRel(profile)
|
|
594
|
+
// 幂等判定要看**两层**:home 层里的激活行同样会让 DSH 加载这个包(home 层合并在后、优先级更高),
|
|
595
|
+
// 只看目标层会在「用户已手工激活过」时多写一行重复激活。
|
|
596
|
+
const effective = await readEffectivePatchLines(patchFile, profile)
|
|
597
|
+
if (effective.lines.some((l) => patchRowActivates(l.raw, pkg))) return
|
|
497
598
|
const id = `pm-${slugOf(pkg)}`
|
|
498
|
-
await patchFile.applyPatchChanges(
|
|
599
|
+
await patchFile.applyPatchChanges(file, [
|
|
499
600
|
{ lineId: id, raw: { id, name: pkg }, action: 'insert' },
|
|
500
601
|
])
|
|
501
602
|
}
|
|
@@ -549,7 +650,7 @@ export class DshPluginsFacade implements PluginsFacade {
|
|
|
549
650
|
// 非 bundle 插件:CLI 只维护 bundles,需补 profile patch 激活行才能加载。
|
|
550
651
|
// 补写失败不吞:包已装但未激活,明确报错并允许重试(幂等补行)。
|
|
551
652
|
try {
|
|
552
|
-
await ensureActivationRow(this.patchFile, join(profileDir, 'node_modules', pkg), pkg)
|
|
653
|
+
await ensureActivationRow(this.patchFile, join(profileDir, 'node_modules', pkg), pkg, this.profile)
|
|
553
654
|
} catch (error) {
|
|
554
655
|
const reason = error instanceof Error ? error.message : String(error)
|
|
555
656
|
throw new Error(this.msg('host.activationRowFailed', { pkg, reason }))
|
|
@@ -624,12 +725,93 @@ class DshWorkspaceFacade implements WorkspaceFacade {
|
|
|
624
725
|
// 字节级工具(projectKey 形状 / 首帧 cwd 读写 / 多 generation 改写)已抽到 utils/session-log.ts:
|
|
625
726
|
// 宿主在线路径与 CLI 离线修复共用同一份实现(避免两处口径漂移)。
|
|
626
727
|
|
|
728
|
+
/* --------------------------------------------- 会话存储读侧兼容(F-1) */
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* 宿主 `sessionPersistence` 的 API 形状(**按 API 形状探测,绝不按 DSH 版本号猜**)。
|
|
732
|
+
*
|
|
733
|
+
* 为什么必须探测:DSH 的会话存储换过基线 —— 新基线有 `open()`(handle 形态,`list()` 的元素是
|
|
734
|
+
* 带 `header` 的 snapshot 对象),旧基线只有服务级的 `readFrom()` / `append()`(header 字段
|
|
735
|
+
* **平铺在元素上**)。本插件只读 `list()` 的结果,而两种形状里 header 的位置不同。
|
|
736
|
+
*
|
|
737
|
+
* 真机事故级的后果(F-1):按写死的形状读,在另一种宿主上「一个都匹配不上」——
|
|
738
|
+
* `reindexSessionHeader` 恒 false、`parentRelations` 退化成**空 Map**(而空 Map 还会进 5 s 的
|
|
739
|
+
* TTL 缓存),于是「导出子代理会话时连带父对话」这条已经修好的能力在旧宿主上**静默失效**。
|
|
740
|
+
* 参照竞品 dsh-claude-move 的 isSessionHandle / storedSessionIdOf(index.mjs:88-154)。
|
|
741
|
+
*/
|
|
742
|
+
export type SessionPersistenceShape = 'handle' | 'legacy' | 'unknown'
|
|
743
|
+
|
|
744
|
+
/** 会话存储**列举**接口的最小形状(两种基线都有 `list()`;本插件只读它,不写会话)。 */
|
|
745
|
+
export interface SessionListService {
|
|
746
|
+
list(): Promise<readonly unknown[]>
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** 按 API 形状探测会话存储基线:有 `open` → handle;只有 `readFrom`/`append` → legacy;都没有 → unknown。 */
|
|
750
|
+
export function sessionPersistenceShapeOf(service: unknown): SessionPersistenceShape {
|
|
751
|
+
if (!isRecord(service)) return 'unknown'
|
|
752
|
+
if (typeof service['open'] === 'function') return 'handle'
|
|
753
|
+
if (typeof service['readFrom'] === 'function' || typeof service['append'] === 'function') return 'legacy'
|
|
754
|
+
return 'unknown'
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** 归一后的列举元素 header 视图(只含本插件用到的字段)。 */
|
|
758
|
+
export interface ListedSessionView {
|
|
759
|
+
id: string
|
|
760
|
+
/** 父会话 id(磁盘 header 名 `parentSession`;DSH 的 RPC 投影名 `parentSessionId` 一并认) */
|
|
761
|
+
parent?: string
|
|
762
|
+
/** header.origin('subagent' = 子代理会话)——原样保留,不在这里做筛选 */
|
|
763
|
+
origin: unknown
|
|
764
|
+
/**
|
|
765
|
+
* **原样的 header 对象**(handle 形态 = `element.header`;legacy 形态 = 元素自身)。
|
|
766
|
+
* 回传给 DSH `WorkspaceRegistry.indexHeader()` 时必须用它 —— 绝不自己造一个 header。
|
|
767
|
+
*/
|
|
768
|
+
raw: unknown
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* 把一个列举元素归一成 header 视图(**两种基线形状都认**)。
|
|
773
|
+
*
|
|
774
|
+
* 认不出 id → undefined:这是「读侧兼容失败」的唯一信号,调用方必须**响亮处理**
|
|
775
|
+
* (记日志 / 抛错),绝不当成「本机没有会话」。
|
|
776
|
+
*/
|
|
777
|
+
export function normalizeListedSession(element: unknown): ListedSessionView | undefined {
|
|
778
|
+
if (!isRecord(element)) return undefined
|
|
779
|
+
const nested = isRecord(element['header']) ? element['header'] : undefined
|
|
780
|
+
// handle 形态 = header 在 element.header;legacy 形态 = header 字段平铺在元素上。
|
|
781
|
+
// header 存在但取不出 id 时回落元素自身(两种都取不到才算解析失败 —— 不猜)。
|
|
782
|
+
const source = nested !== undefined && typeof nested['id'] === 'string' && nested['id'] !== '' ? nested : element
|
|
783
|
+
const id = source['id']
|
|
784
|
+
if (typeof id !== 'string' || id === '') return undefined
|
|
785
|
+
const rawParent = source['parentSession'] ?? source['parentSessionId']
|
|
786
|
+
const parent = typeof rawParent === 'string' && rawParent !== '' ? rawParent : undefined
|
|
787
|
+
return { id, origin: source['origin'], raw: source, ...(parent !== undefined ? { parent } : {}) }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** 会话存储列举的结果(失败必须可区分,绝不与「本机没有会话」混淆)。 */
|
|
791
|
+
type ListedSessions =
|
|
792
|
+
| { ok: true; items: readonly ListedSessionView[] }
|
|
793
|
+
/** unavailable = 服务/接口不可用(list 抛错或返回非数组);shape-unknown = 元素形状一个都认不出 */
|
|
794
|
+
| { ok: false; reason: 'unavailable' | 'shape-unknown'; detail: string }
|
|
795
|
+
|
|
796
|
+
/** 列举会话存储的用途(内部枚举;渲染时必须经字典键映射 —— **绝不把裸枚举塞进文案**)。 */
|
|
797
|
+
type SessionStorePurpose = 'reindexSessionHeader' | 'parentRelations'
|
|
798
|
+
|
|
799
|
+
/** 用途 → 字典键(本仓纪律:文案只做「原因 → 字典键」映射,禁止渲染裸枚举)。 */
|
|
800
|
+
const SESSION_STORE_PURPOSE_KEY: Record<SessionStorePurpose, string> = {
|
|
801
|
+
reindexSessionHeader: 'host.sessionStorePurposeReindex',
|
|
802
|
+
parentRelations: 'host.sessionStorePurposeParentRelations',
|
|
803
|
+
}
|
|
804
|
+
|
|
627
805
|
/**
|
|
628
806
|
* 父子关系缓存的 TTL(毫秒)。
|
|
629
807
|
*
|
|
630
808
|
* 取值依据:`sessionPersistence.list()` 在真机上约 **0.7 s**,而 `/export-preview` 每次都要它;
|
|
631
809
|
* 父子关系只在「有新子代理会话」时变化,几秒陈旧对选择器的「勾父带子 / 勾子带父」联动毫无影响。
|
|
632
810
|
* 取 5 s:连续打开/刷新选择器都在同一窗口内命中,同时保证新会话最多 5 s 后就能被联动识别。
|
|
811
|
+
*
|
|
812
|
+
* F-1:缓存里只放**成功结果** —— 列举失败/形状识别失败走 `parentRelations()` 的抛错路径,
|
|
813
|
+
* 由 ttl-cache 的「失败不缓存」语义保证下一次调用立刻重试;
|
|
814
|
+
* 不再把失败吞成空 Map(那会被缓存整整 5 s,正是「静默失效」的放大器)。
|
|
633
815
|
*/
|
|
634
816
|
const PARENT_RELATIONS_CACHE_TTL_MS = 5_000
|
|
635
817
|
|
|
@@ -639,16 +821,25 @@ export class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
639
821
|
private readonly root: string
|
|
640
822
|
/**
|
|
641
823
|
* 父子关系的短 TTL 缓存(见 `PARENT_RELATIONS_CACHE_TTL_MS`;同 key 并发合并成一次列举)。
|
|
642
|
-
* `parentRelations()`
|
|
643
|
-
*
|
|
824
|
+
* F-1 后**只缓存成功结果**:列举失败/形状识别失败会让 `parentRelations()` 抛错,
|
|
825
|
+
* 而 ttl-cache 的语义是「失败不缓存」→ 下一次调用立刻重试(行为见 utils/ttl-cache.ts)。
|
|
644
826
|
*/
|
|
645
827
|
private readonly parentRelationsCache = createTtlAsyncCache<Map<string, SessionParentRelation>>({
|
|
646
828
|
ttlMs: PARENT_RELATIONS_CACHE_TTL_MS,
|
|
647
829
|
})
|
|
830
|
+
/**
|
|
831
|
+
* 宿主日志(F-1):会话存储的形状异常/列举异常必须**响亮**(绝不退化成空结果),
|
|
832
|
+
* 缺省不注入时用一个只输出 warn/error 的本地记录器(测试构造三参即可,行为不变)。
|
|
833
|
+
*/
|
|
834
|
+
private readonly log: Logger
|
|
835
|
+
/** 文案翻译器(F-1 的形状异常诊断走字典,禁止硬编码用户可见字符串) */
|
|
836
|
+
private readonly msgFunc: MsgFunc
|
|
648
837
|
|
|
649
|
-
constructor(ctx: Context, homeDir: string, msg: MsgFunc = zhMsg) {
|
|
838
|
+
constructor(ctx: Context, homeDir: string, msg: MsgFunc = zhMsg, log?: Logger) {
|
|
650
839
|
this.ctx = ctx
|
|
651
840
|
this.root = join(homeDir, 'sessions')
|
|
841
|
+
this.log = log ?? createLogger({ level: 'warn' })
|
|
842
|
+
this.msgFunc = msg
|
|
652
843
|
}
|
|
653
844
|
|
|
654
845
|
/**
|
|
@@ -744,26 +935,101 @@ export class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
744
935
|
async reindexSessionHeader(sessionId: string): Promise<boolean> {
|
|
745
936
|
const registry = readService<{ indexHeader?: (header: unknown) => unknown }>(this.ctx, 'workspaceRegistry')
|
|
746
937
|
// 会话存储服务:只用来取出该会话的最新 header(注册表内存里的那份可能已被改写作废)
|
|
747
|
-
const store = readService<
|
|
938
|
+
const store = readService<SessionListService>(this.ctx, 'sessionPersistence')
|
|
748
939
|
if (registry === undefined || typeof registry.indexHeader !== 'function' || store === undefined) return false
|
|
940
|
+
// F-1:按**形状**归一列举元素(handle / legacy 两种基线都认),不写死 element.header.id
|
|
941
|
+
const listed = await this.listSessionElements(store, 'reindexSessionHeader')
|
|
942
|
+
if (!listed.ok) {
|
|
943
|
+
// T8(F-1 收尾):失败原因必须**可观测** —— 只返回 false 时它与「注册表不可用」无法区分,
|
|
944
|
+
// 排查只能看到「需重启 DSH 后再执行一次归位」,看不到「为什么」。
|
|
945
|
+
this.log.warn(listed.detail)
|
|
946
|
+
return false
|
|
947
|
+
}
|
|
948
|
+
const fresh = listed.items.find((item) => item.id === sessionId)
|
|
949
|
+
if (fresh === undefined) return false
|
|
749
950
|
try {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (fresh === undefined) return false
|
|
753
|
-
await registry.indexHeader(fresh)
|
|
951
|
+
// 必须回传**原样的 header 对象**(handle = element.header;legacy = 元素自身)
|
|
952
|
+
await registry.indexHeader(fresh.raw)
|
|
754
953
|
return true
|
|
755
|
-
} catch {
|
|
954
|
+
} catch (err) {
|
|
955
|
+
this.log.warn(`会话 header 重新索引失败(${sessionId}):${err instanceof Error ? err.message : String(err)}`)
|
|
756
956
|
return false
|
|
757
957
|
}
|
|
758
958
|
}
|
|
759
959
|
|
|
960
|
+
/**
|
|
961
|
+
* 列举宿主会话存储并**把两种元素形状归一**(F-1 唯一入口)。
|
|
962
|
+
*
|
|
963
|
+
* 返回 `ok:false` 的两种情形必须被调用方**响亮处理**(记日志 + 抛错/明确失败返回):
|
|
964
|
+
* - `unavailable`:服务或 `list()` 不可用(抛错 / 返回非数组);
|
|
965
|
+
* - `shape-unknown`:有元素、但**一个都解析不出 id**(宿主形状既不是 handle 也不是 legacy)。
|
|
966
|
+
* 这两种**绝不**降级成「本机没有会话 / 没有父子关系」的空结果 —— 那正是 F-1 的真机事故。
|
|
967
|
+
* 元素**部分**认不出时保留已认出的并记一条警告(部分可用比全丢好,但必须可见)。
|
|
968
|
+
*/
|
|
969
|
+
private async listSessionElements(
|
|
970
|
+
store: SessionListService,
|
|
971
|
+
purpose: SessionStorePurpose,
|
|
972
|
+
): Promise<ListedSessions> {
|
|
973
|
+
// T8-F1:purpose 是内部枚举 → 先经字典键映射成人类可读用途,**绝不渲染裸枚举**;
|
|
974
|
+
// 三条 detail/warn 一律走 this.msgFunc(本文件里唯一的用户可见文案出口),不硬编码中文。
|
|
975
|
+
const purposeLabel = this.msgFunc(SESSION_STORE_PURPOSE_KEY[purpose])
|
|
976
|
+
let elements: readonly unknown[]
|
|
977
|
+
try {
|
|
978
|
+
elements = await store.list()
|
|
979
|
+
} catch (err) {
|
|
980
|
+
return {
|
|
981
|
+
ok: false,
|
|
982
|
+
reason: 'unavailable',
|
|
983
|
+
detail: this.msgFunc('host.sessionStoreListFailed', { purpose: purposeLabel, reason: errorMessage(err) }),
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (!Array.isArray(elements)) {
|
|
987
|
+
return {
|
|
988
|
+
ok: false,
|
|
989
|
+
reason: 'unavailable',
|
|
990
|
+
detail: this.msgFunc('host.sessionStoreListNotArray', { purpose: purposeLabel }),
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const items: ListedSessionView[] = []
|
|
994
|
+
let unresolved = 0
|
|
995
|
+
for (const element of elements) {
|
|
996
|
+
const view = normalizeListedSession(element)
|
|
997
|
+
if (view === undefined) unresolved += 1
|
|
998
|
+
else items.push(view)
|
|
999
|
+
}
|
|
1000
|
+
if (items.length === 0 && elements.length > 0) {
|
|
1001
|
+
const detail = this.msgFunc('host.sessionStoreShapeUnknown', {
|
|
1002
|
+
shape: sessionPersistenceShapeOf(store),
|
|
1003
|
+
count: String(elements.length),
|
|
1004
|
+
})
|
|
1005
|
+
this.log.error(detail)
|
|
1006
|
+
return { ok: false, reason: 'shape-unknown', detail }
|
|
1007
|
+
}
|
|
1008
|
+
if (unresolved > 0) {
|
|
1009
|
+
this.log.warn(
|
|
1010
|
+
this.msgFunc('host.sessionStorePartialUnresolved', {
|
|
1011
|
+
count: String(unresolved),
|
|
1012
|
+
total: String(elements.length),
|
|
1013
|
+
purpose: purposeLabel,
|
|
1014
|
+
}),
|
|
1015
|
+
)
|
|
1016
|
+
}
|
|
1017
|
+
return { ok: true, items }
|
|
1018
|
+
}
|
|
1019
|
+
|
|
760
1020
|
/**
|
|
761
1021
|
* 本机会话的父子关系(子会话 id → 父会话 id)。
|
|
762
1022
|
*
|
|
763
1023
|
* 实现走 DSH 会话存储的列举(与 `session/list` 同一数据源):只取 header 的 `parentSession`,
|
|
764
1024
|
* 不解析任何日志字节 —— 让「导出父对话时连带子代理会话」不为此读一遍整棵会话树。
|
|
765
|
-
* 字段名以磁盘 header 为准(`parentSession
|
|
766
|
-
*
|
|
1025
|
+
* 字段名以磁盘 header 为准(`parentSession`,两种写法都认;DSH 的 RPC 投影才改名为 `parentSessionId`)。
|
|
1026
|
+
* 元素形状按 **API 形状探测**归一(handle 的 `element.header` 与 legacy 的平铺字段都认,F-1)。
|
|
1027
|
+
*
|
|
1028
|
+
* 失败语义(F-1,**不得放宽**):
|
|
1029
|
+
* - 服务**未接线** → 空 Map(确定性事实:宿主没有会话存储,不是读失败);
|
|
1030
|
+
* - 列举失败 / 元素形状一个都认不出 → **抛错**(带明确原因)。绝不返回空 Map ——
|
|
1031
|
+
* 空 Map 会进 5 s TTL 缓存,且让「本机一条父子关系都没有」与「读不出来」无法区分,
|
|
1032
|
+
* 「导出子代理会话连带父对话」就会再次静默失效。调用方各自有回退路径(见 `loadParentRelations`)。
|
|
767
1033
|
*/
|
|
768
1034
|
async parentRelations(): Promise<Map<string, SessionParentRelation>> {
|
|
769
1035
|
return this.parentRelationsCache.resolve('local', () => this.loadParentRelations())
|
|
@@ -771,18 +1037,22 @@ export class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
771
1037
|
|
|
772
1038
|
/** 真正列举一次 DSH 会话存储(语义见 `parentRelations()`;本方法不做缓存) */
|
|
773
1039
|
private async loadParentRelations(): Promise<Map<string, SessionParentRelation>> {
|
|
1040
|
+
const store = readService<SessionListService>(this.ctx, 'sessionPersistence')
|
|
1041
|
+
// 服务**未接线**(宿主不提供 sessionPersistence)→ 不猜,空 Map(与改造前一致,属确定性事实)
|
|
1042
|
+
if (store === undefined) return new Map<string, SessionParentRelation>()
|
|
1043
|
+
// F-1:形状归一(handle 的 element.header / legacy 的元素自身都认)
|
|
1044
|
+
const listed = await this.listSessionElements(store, 'parentRelations')
|
|
1045
|
+
if (!listed.ok) {
|
|
1046
|
+
// 读不到 / 形状认不出 → **响亮抛错**,绝不退化成「本机没有父子关系」的空 Map
|
|
1047
|
+
// (空 Map 既会被 5 s TTL 缓存放大,也让「导出少带父对话」没有任何提示)。
|
|
1048
|
+
// 调用方各自有回退路径:/export-preview 只是不标父链接(预览照常),
|
|
1049
|
+
// 会话体检退回 storages 缓存枚举。
|
|
1050
|
+
throw new Error(listed.detail)
|
|
1051
|
+
}
|
|
774
1052
|
const out = new Map<string, SessionParentRelation>()
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
for (const snapshot of await store.list()) {
|
|
779
|
-
const id = snapshot.header?.id
|
|
780
|
-
const parent = snapshot.header?.parentSession
|
|
781
|
-
if (typeof id !== 'string' || id === '' || typeof parent !== 'string' || parent === '') continue
|
|
782
|
-
out.set(id, { parent, subagent: snapshot.header?.origin === 'subagent' })
|
|
783
|
-
}
|
|
784
|
-
} catch {
|
|
785
|
-
return new Map<string, SessionParentRelation>()
|
|
1053
|
+
for (const item of listed.items) {
|
|
1054
|
+
if (item.parent === undefined) continue
|
|
1055
|
+
out.set(item.id, { parent: item.parent, subagent: item.origin === 'subagent' })
|
|
786
1056
|
}
|
|
787
1057
|
return out
|
|
788
1058
|
}
|
|
@@ -797,12 +1067,20 @@ export class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
797
1067
|
}
|
|
798
1068
|
}
|
|
799
1069
|
|
|
800
|
-
/**
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
*
|
|
805
|
-
|
|
1070
|
+
/**
|
|
1071
|
+
* Patch-file facade:patch 的**两层**都在 home 根内 ——
|
|
1072
|
+
* - 用户(home)层:`$DSH_HOME/cordis.patch.yml`
|
|
1073
|
+
* - profile 层:`$DSH_HOME/profiles/<name>/cordis.patch.yml`
|
|
1074
|
+
*
|
|
1075
|
+
* `file` 参数是**相对 homeDir 的 POSIX 路径**(issue #71;层身份的定义见 core/patch-layers.ts)。
|
|
1076
|
+
* 改造前这里是「拿文件名字符串当枚举」的两个分支,而两个分支比较的是同一个字面量
|
|
1077
|
+
* `'cordis.patch.yml'` —— profile 分支永远不可达,于是 profile 层的 MCP / prompts 行
|
|
1078
|
+
* 在备份里凭空消失(用户报告的「备份不到外壳 mcp」)。现在按路径解析,两层都能读写。
|
|
1079
|
+
*
|
|
1080
|
+
* YAML 侧一律走 `utils/patch-yaml.ts` 的 **DSH 方言**(issue #75):`!!js` 必须能读写往返,
|
|
1081
|
+
* 且原文件解析不了时**拒绝覆盖**(读不出来 ≠ 空文档)。
|
|
1082
|
+
*/
|
|
1083
|
+
export class DshPatchFileFacade implements PatchFileFacade {
|
|
806
1084
|
private readonly homeDir: string
|
|
807
1085
|
private readonly profile: string
|
|
808
1086
|
private readonly msg: MsgFunc
|
|
@@ -813,10 +1091,32 @@ class DshPatchFileFacade implements PatchFileFacade {
|
|
|
813
1091
|
this.msg = msg
|
|
814
1092
|
}
|
|
815
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* 层路径 → 绝对路径。
|
|
1096
|
+
*
|
|
1097
|
+
* 只接受**已知的层路径**(用户层 / 本 profile 的 profile 层),且解析结果必须落在 home 根内:
|
|
1098
|
+
* 调用方传进来的是快照里存的字符串,跨机器恢复时不能让它变成任意路径写。
|
|
1099
|
+
* 本 profile 之外的 profile 层同样拒绝 —— 一个档案的备份不该写另一个档案的 patch。
|
|
1100
|
+
*/
|
|
816
1101
|
private patchPath(file: string): string {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1102
|
+
const rel = file.replace(/\\/g, '/')
|
|
1103
|
+
const allowed = rel === USER_PATCH_FILE || (isPatchLayerRel(rel) && rel === profilePatchRel(this.profile))
|
|
1104
|
+
if (!allowed) {
|
|
1105
|
+
throw new Error(this.msg('host.patchUnsupported', {
|
|
1106
|
+
user: USER_PATCH_FILE,
|
|
1107
|
+
profile: profilePatchRel(this.profile),
|
|
1108
|
+
file,
|
|
1109
|
+
}))
|
|
1110
|
+
}
|
|
1111
|
+
const abs = resolve(this.homeDir, rel)
|
|
1112
|
+
if (!isSameOrChild(abs, this.homeDir)) {
|
|
1113
|
+
throw new Error(this.msg('host.patchUnsupported', {
|
|
1114
|
+
user: USER_PATCH_FILE,
|
|
1115
|
+
profile: profilePatchRel(this.profile),
|
|
1116
|
+
file,
|
|
1117
|
+
}))
|
|
1118
|
+
}
|
|
1119
|
+
return abs
|
|
820
1120
|
}
|
|
821
1121
|
|
|
822
1122
|
async readPatchLines(file: string): Promise<{ lineId: string; raw: unknown }[]> {
|
|
@@ -824,14 +1124,21 @@ class DshPatchFileFacade implements PatchFileFacade {
|
|
|
824
1124
|
let text: string
|
|
825
1125
|
try {
|
|
826
1126
|
text = await fs.readFile(p, 'utf8')
|
|
827
|
-
} catch {
|
|
828
|
-
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
// 真不存在 = 「按需创建、本来就没有」→ 空层(既有语义)。
|
|
1129
|
+
// 其余 errno(EACCES / EBUSY / …)**必须上抛**:读不到 ≠ 没有;静默返回空会让这一层的
|
|
1130
|
+
// patch 行在导出/同步里无声消失(t23 口径;readEffectivePatchLines 的 failures 通道会转成
|
|
1131
|
+
// 可见告警,三个适配器都会展示)。
|
|
1132
|
+
if (isENOENT(err)) return []
|
|
1133
|
+
throw new Error(this.msg('host.patchUnreadable', { file, reason: describePatchYamlError(err) }), { cause: err })
|
|
829
1134
|
}
|
|
830
1135
|
let doc: unknown
|
|
831
1136
|
try {
|
|
832
|
-
doc =
|
|
833
|
-
} catch {
|
|
834
|
-
|
|
1137
|
+
doc = loadPatchDocument(text)
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
// 解析失败同样上抛(issue #75):`!!js` 这类 DSH 方言一旦读不出来,返回 [] 就等于把用户
|
|
1140
|
+
// 手写的整层配置从导出/同步里抹掉(比「报错」糟得多)。
|
|
1141
|
+
throw new Error(this.msg('host.patchUnreadable', { file, reason: describePatchYamlError(err) }), { cause: err })
|
|
835
1142
|
}
|
|
836
1143
|
if (!Array.isArray(doc)) return []
|
|
837
1144
|
const lines: { lineId: string; raw: unknown }[] = []
|
|
@@ -860,13 +1167,29 @@ class DshPatchFileFacade implements PatchFileFacade {
|
|
|
860
1167
|
const p = this.patchPath(file)
|
|
861
1168
|
|
|
862
1169
|
// 1. Load the current document into an ordered lineId → raw table.
|
|
1170
|
+
//
|
|
1171
|
+
// 读不出来的既有内容**绝不能当作空文档**(issue #75 的数据丢失点):重建后的文件只包含
|
|
1172
|
+
// 本次写入的行,等于把用户手写的其余行整段删掉。三种情况分开处理:
|
|
1173
|
+
// - 真不存在(ENOENT)→ 空文档(首次创建,既有语义);
|
|
1174
|
+
// - 存在但读不到(EACCES/EBUSY/…)→ 抛错,不改写;
|
|
1175
|
+
// - 存在但解析不了(含未知方言 / YAML 本身有错)→ 抛错,不改写。
|
|
863
1176
|
const rows = new Map<string, unknown>()
|
|
864
1177
|
const order: string[] = []
|
|
865
|
-
let
|
|
1178
|
+
let existing: string | null = null
|
|
866
1179
|
try {
|
|
867
|
-
|
|
868
|
-
} catch {
|
|
869
|
-
|
|
1180
|
+
existing = await fs.readFile(p, 'utf8')
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
if (!isENOENT(err)) {
|
|
1183
|
+
throw new Error(this.msg('host.patchRefuseClobber', { file, reason: describePatchYamlError(err) }), { cause: err })
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
let doc: unknown
|
|
1187
|
+
if (existing !== null && existing.trim() !== '') {
|
|
1188
|
+
try {
|
|
1189
|
+
doc = loadPatchDocument(existing)
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
throw new Error(this.msg('host.patchRefuseClobber', { file, reason: describePatchYamlError(err) }), { cause: err })
|
|
1192
|
+
}
|
|
870
1193
|
}
|
|
871
1194
|
if (Array.isArray(doc)) {
|
|
872
1195
|
for (const item of doc) {
|
|
@@ -914,7 +1237,7 @@ class DshPatchFileFacade implements PatchFileFacade {
|
|
|
914
1237
|
if (raw !== undefined) out.push(raw)
|
|
915
1238
|
}
|
|
916
1239
|
const text = '# rewritten by dsh-config-manager import (original comments not preserved)\n'
|
|
917
|
-
+
|
|
1240
|
+
+ dumpPatchDocument(out)
|
|
918
1241
|
await atomicWriteFile(p, text)
|
|
919
1242
|
}
|
|
920
1243
|
}
|
|
@@ -948,8 +1271,17 @@ export class DshFileSystemFacade implements FileSystemFacade {
|
|
|
948
1271
|
try {
|
|
949
1272
|
await fs.access(this.abs(relPath))
|
|
950
1273
|
return true
|
|
951
|
-
} catch {
|
|
952
|
-
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
// t85 / t23 口径:**读不到 ≠ 没有**。只有 ENOENT 才是「按需创建、本来就没有」;
|
|
1276
|
+
// EACCES / EPERM / EBUSY / EMFILE / ENOTDIR… 一律**按「存在」处理**(返回 true)。
|
|
1277
|
+
//
|
|
1278
|
+
// 为什么选「按存在处理」而不是上抛:本方法是 FileSystemFacade 的**布尔端口**,全部消费点
|
|
1279
|
+
// 都按「exists() 恒返回布尔、不抛」来写(消费点清单见 t85 交付物),其中多处处于只读/预览
|
|
1280
|
+
// 路径(backup / market / sync-ancestor / plugin-files)—— 上抛会把「探测失败」变成这些调用点
|
|
1281
|
+
// 未预期的错误面;而 t23 立的规矩正是「判定『不存在』的探测自身失败时按『存在』处理」。
|
|
1282
|
+
// 保守方向也是安全方向:true ⇒ 调用方不创建 / 不删除 / 不剔除声明,绝不造成有损副作用。
|
|
1283
|
+
// ENOENT 必须仍为 false,否则会破坏「按需创建 / 本来就没有」的既有语义。
|
|
1284
|
+
return !isENOENT(err)
|
|
953
1285
|
}
|
|
954
1286
|
}
|
|
955
1287
|
|
|
@@ -1051,6 +1383,62 @@ export class DshFileSystemFacade implements FileSystemFacade {
|
|
|
1051
1383
|
}
|
|
1052
1384
|
}
|
|
1053
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* 目录体积上界(只读预览:本地源插件的 `link:` 目录)。
|
|
1388
|
+
*
|
|
1389
|
+
* 与 realpathDir 同族的「绝对路径」方法:入参只可能是本机插件 spec 推导出的绝对路径
|
|
1390
|
+
* (`link:` 常指向 $DSH_HOME 之外的桌面端 resources 目录),因此**不受 home 边界限制**。
|
|
1391
|
+
*
|
|
1392
|
+
* 约束(宁可低估,不可卡住预览):
|
|
1393
|
+
* - 不跟随符号链接 / junction:`readdir(withFileTypes)` 对链接目录的 isDirectory() 恒 false,
|
|
1394
|
+
* 按「既不是文件也不是目录」跳过,既不会绕圈也不会跑出该目录;
|
|
1395
|
+
* - 跳过 node_modules:`npm pack` 自身也排除它,数进去只会把量级抬高一个数量级;
|
|
1396
|
+
* - `maxEntries` 兜底(默认 20000):超出即停并返回已累计值;
|
|
1397
|
+
* - 不是目录 / 读不到 → null(调用方对这类目录退回「体积按 0」)。
|
|
1398
|
+
*/
|
|
1399
|
+
async dirSizeBytes(absPath: string, opts: { maxEntries?: number } = {}): Promise<number | null> {
|
|
1400
|
+
const maxEntries = opts.maxEntries !== undefined && opts.maxEntries > 0 ? opts.maxEntries : 20000
|
|
1401
|
+
const readEntries = async (dir: string): Promise<Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>> => {
|
|
1402
|
+
try {
|
|
1403
|
+
return await fs.readdir(dir, { withFileTypes: true })
|
|
1404
|
+
} catch {
|
|
1405
|
+
return []
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const target = resolve(absPath)
|
|
1410
|
+
const st = await fs.stat(target)
|
|
1411
|
+
if (!st.isDirectory()) return null
|
|
1412
|
+
const stack: string[] = [target]
|
|
1413
|
+
let total = 0
|
|
1414
|
+
let counted = 0
|
|
1415
|
+
while (stack.length > 0) {
|
|
1416
|
+
const dir = stack.pop()
|
|
1417
|
+
if (dir === undefined) break
|
|
1418
|
+
for (const ent of await readEntries(dir)) {
|
|
1419
|
+
if (ent.name === 'node_modules') continue
|
|
1420
|
+
const full = join(dir, ent.name)
|
|
1421
|
+
if (ent.isDirectory()) {
|
|
1422
|
+
stack.push(full)
|
|
1423
|
+
continue
|
|
1424
|
+
}
|
|
1425
|
+
if (!ent.isFile()) continue
|
|
1426
|
+
if (counted >= maxEntries) return total
|
|
1427
|
+
counted += 1
|
|
1428
|
+
try {
|
|
1429
|
+
const fileStat = await fs.stat(full)
|
|
1430
|
+
if (fileStat.isFile()) total += fileStat.size
|
|
1431
|
+
} catch {
|
|
1432
|
+
// 单个文件读不到就跳过:预览要的是量级,不值得为它失败
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return total
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1054
1442
|
/**
|
|
1055
1443
|
* 建缺失目录(issue #45:导入工作区记录前把缺失的项目目录建出来)。
|
|
1056
1444
|
*
|
|
@@ -1106,6 +1494,15 @@ export class ConfigManagerHostContext implements HostContext {
|
|
|
1106
1494
|
readonly fs: FileSystemFacade
|
|
1107
1495
|
/** 会话存储端口(issue #45 会话归位;对 ctx.sessionPersistence 的薄适配) */
|
|
1108
1496
|
readonly sessions: SessionStoreFacade
|
|
1497
|
+
/**
|
|
1498
|
+
* 技能服务端口(issue #71;对 ctx.skills 的薄适配)。
|
|
1499
|
+
*
|
|
1500
|
+
* 为什么需要:真机上 89 个 SKILL.md 全在 `profiles/<p>/node_modules` 里(技能 provider 由
|
|
1501
|
+
* 插件注册),`$DSH_HOME/skills` 根本不存在 —— 只扫目录的分区必然是空的。
|
|
1502
|
+
*
|
|
1503
|
+
* 可选:宿主没有 skills 服务时(旧外壳)缺省 undefined,skills 分区退回纯目录扫描。
|
|
1504
|
+
*/
|
|
1505
|
+
readonly skills: SkillStoreFacade | undefined
|
|
1109
1506
|
/**
|
|
1110
1507
|
* 本机 DSH 支持的**会话日志格式版本**(`SESSION_FORMAT_VERSION`;解析不到 = undefined)。
|
|
1111
1508
|
*
|
|
@@ -1119,6 +1516,15 @@ export class ConfigManagerHostContext implements HostContext {
|
|
|
1119
1516
|
safeModeIsBlocked?: () => boolean
|
|
1120
1517
|
/** Phase 3 恢复/事务(JournalStore + reconcile + SAFE MODE + runJournaled)。apply() 注入。 */
|
|
1121
1518
|
phase3Recovery?: import('./core/phase3-host.ts').Phase3Recovery
|
|
1519
|
+
/**
|
|
1520
|
+
* 本机自请求能力(F-2:官方 session.export 通道的探测与代理)。
|
|
1521
|
+
*
|
|
1522
|
+
* 只有「让 DSH 自己的 /api/session.export 说一遍它自己的状态」这一种用法 ——
|
|
1523
|
+
* 绝不拿它去访问外部网络(插件对外的网络调用有各自的既有通道)。
|
|
1524
|
+
*/
|
|
1525
|
+
fetch?: (input: string, init: RequestInit) => Promise<Response>
|
|
1526
|
+
/** 本实例 web 监听端口(webServer 就绪前为 undefined → 探测不发请求、如实降级)。 */
|
|
1527
|
+
webPort?: number
|
|
1122
1528
|
|
|
1123
1529
|
constructor(ctx: Context, homeDir: string, profile: string) {
|
|
1124
1530
|
this.homeDir = homeDir
|
|
@@ -1127,7 +1533,11 @@ export class ConfigManagerHostContext implements HostContext {
|
|
|
1127
1533
|
const installAnchor = installAnchorFromProfileContext(readService<unknown>(ctx, 'profileContext'))
|
|
1128
1534
|
this.dshVersion = resolveDshVersion(homeDir, profile, installAnchor)
|
|
1129
1535
|
// 会话格式版本与 DSH 版本同源同候选(桌面端的运行时在 app.asar 内)。
|
|
1130
|
-
|
|
1536
|
+
// tried 收集**所有尝试过的路径**:解析失败时写进日志 —— 「解析不到」这句话本身对排查没有价值
|
|
1537
|
+
// (issue #74 的直接诉求),真机要的是「都试了哪几处」。
|
|
1538
|
+
const sessionFormatTried: string[] = []
|
|
1539
|
+
this.sessionFormatVersion = resolveSessionFormatVersion(
|
|
1540
|
+
dshPackageJsonCandidates(homeDir, profile, installAnchor), sessionFormatTried)
|
|
1131
1541
|
this.profile = profile
|
|
1132
1542
|
this.profileDir = resolveProfileDir(homeDir, profile)
|
|
1133
1543
|
this.language = resolveAppLanguage(ctx)
|
|
@@ -1137,8 +1547,13 @@ export class ConfigManagerHostContext implements HostContext {
|
|
|
1137
1547
|
// 排查时 DSH_CONFIG_MANAGER_LOG_LEVEL=info|debug 恢复逐条输出。
|
|
1138
1548
|
this.log = createLogger({ level: parseLogLevel(process.env.DSH_CONFIG_MANAGER_LOG_LEVEL) })
|
|
1139
1549
|
if (this.sessionFormatVersion === undefined) {
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1550
|
+
// 绝不静默:解析不到就明说体检不可用(用户仍能导出/导入,只是拿不到格式告警),
|
|
1551
|
+
// 并把已尝试的路径一并给出。
|
|
1552
|
+
const triedText = sessionFormatTried.length > 6
|
|
1553
|
+
? sessionFormatTried.slice(0, 6).join('、') + ' 等'
|
|
1554
|
+
: sessionFormatTried.join('、')
|
|
1555
|
+
this.log.warn('无法解析本机 DSH 的会话格式版本:导入/同步不做「会话格式是否可读」体检'
|
|
1556
|
+
+ `(已尝试 ${sessionFormatTried.length} 处:${triedText})`)
|
|
1142
1557
|
}
|
|
1143
1558
|
this.settings = new DshSettingsFacade(ctx)
|
|
1144
1559
|
this.credentials = new DshCredentialsFacade(ctx)
|
|
@@ -1146,7 +1561,14 @@ export class ConfigManagerHostContext implements HostContext {
|
|
|
1146
1561
|
this.plugins = new DshPluginsFacade(homeDir, profile, this.patchFile, undefined, this.msg)
|
|
1147
1562
|
this.workspace = new DshWorkspaceFacade(ctx, this.msg)
|
|
1148
1563
|
this.fs = new DshFileSystemFacade(homeDir, this.msg)
|
|
1149
|
-
this.sessions = new DshSessionStoreFacade(ctx, homeDir, this.msg)
|
|
1564
|
+
this.sessions = new DshSessionStoreFacade(ctx, homeDir, this.msg, this.log)
|
|
1565
|
+
// F-2:Cordis 的 fetch 服务(DSH 自己在用它转发 /api)。缺省回落进程全局 fetch(Node ≥18 恒有)。
|
|
1566
|
+
const fetchService = readService<unknown>(ctx, 'fetch')
|
|
1567
|
+
const fetchImpl = typeof fetchService === 'function' ? fetchService : globalThis.fetch
|
|
1568
|
+
if (typeof fetchImpl === 'function') {
|
|
1569
|
+
this.fetch = (input, init) => fetchImpl(input, init) as Promise<Response>
|
|
1570
|
+
}
|
|
1571
|
+
this.skills = resolveSkillStore(ctx)
|
|
1150
1572
|
}
|
|
1151
1573
|
}
|
|
1152
1574
|
|
|
@@ -1216,11 +1638,15 @@ export async function tryDecryptCredentials(
|
|
|
1216
1638
|
if (!archive.has('security/secrets.enc')) return undefined
|
|
1217
1639
|
const blob = archive.readEntry('security/secrets.enc')
|
|
1218
1640
|
const plaintext = await decryptCredentials(blob, manifest.security.encryption, password)
|
|
1641
|
+
// e2e-F1:**解密成功**(GCM 已认证)之后的「没有可识别 ref」必须回**空 Map**,而不是 undefined ——
|
|
1642
|
+
// undefined 是「没有解密 / 不适用」的语义,会被 analyzer 的安全阀判成「未提供密码」而拒绝导入
|
|
1643
|
+
// (真机:导出机没有可导出的凭据值 → secrets.enc 明文 0 字节 → yaml.load('') 抛错 → undefined → 400)。
|
|
1644
|
+
// 与同步侧 `fromYaml`(src/security/credentials-yaml.ts,其测试断言 fromYaml('').size === 0)同口径。
|
|
1219
1645
|
let parsed: unknown
|
|
1220
1646
|
try {
|
|
1221
|
-
parsed = yaml.load(plaintext)
|
|
1647
|
+
parsed = plaintext.trim() === '' ? null : yaml.load(plaintext)
|
|
1222
1648
|
} catch {
|
|
1223
|
-
|
|
1649
|
+
parsed = null
|
|
1224
1650
|
}
|
|
1225
1651
|
// 解析口径与同步引擎共用(security/credentials-yaml.ts):顶层 refs 块(DSH v1 布局)
|
|
1226
1652
|
// 与预发布扁平布局都认,records 等嵌套结构忽略(issue #39)。
|
|
@@ -1245,7 +1671,7 @@ export async function tryDecryptCredentials(
|
|
|
1245
1671
|
* (见 W1 报告的重测配方),把结果落到这里;makeRoutes 里的 const routeEnv: RouteEnvInferred =
|
|
1246
1672
|
* 注解保证两侧不漂移(新增依赖漏登记会在构造点报错)。
|
|
1247
1673
|
*/
|
|
1248
|
-
export type RouteEnvInferred = { adapters: ConfigAdapter<unknown>[]; sessionHealth: { homeDir: string; targetFormatVersion: () => number | undefined; workspaceKeys: () => Promise<ReadonlySet<string>>; knownSessionIds: () => Promise<ReadonlySet<string>>; }; backupScheduler: BackupScheduler; bootSafetyAudit: () => Promise<BootSafetyReport>; cancelDecisionTimeoutMs: number; buildMarketSummary: (e: { url: string; addedAt: string; }) => Promise<MarketSummary>; credentials: CredentialProvider; dataDir: string; exportsDir: string; githubAuth: GitHubAuthClient; githubClientId: string | undefined; githubClientSecret: string | undefined; githubFlows: DeviceFlowStore; history: MigrationStore; host: ConfigManagerHostContext; itemCached: (url: string, itemId: string) => Promise<boolean>; knownSyncSectionIds: Set<SectionId>; makeImporter: () => Importer; makeMarketReader: () => GitMarketReader; makeRecoveryExecutors: (runId: string) => RecoveryExecutorFns; makeSyncEngine: (cfg: SyncConfig, engineOpts?: { includeOptInSections?: boolean; }) => SyncEngine; marketBootAutoRefreshed: { value: boolean; }; marketCacheIndex: (url: string) => string; marketDir: string; marketCacheItemDir: (url: string) => string; marketStarCache: StarCache; marketWorkDir: (url: string) => string; meGitHubRest: GitHubAuthRest; meService: MyRepoService; meTokenProvider: () => Promise<string>; msg: MsgFunc; prepareSync: (body: Record<string, unknown>) => Promise<SyncConfig>; profileLauncher: DshProfileLauncher; profileRuntime: DshProfileRuntimeRegistry; profiles: DshProfileManager; pruneStagedMarketZips: () => Promise<void>; readCachedIndexObj: (url: string) => Promise<MarketIndex | null>; recoveryOrchestrator: RecoveryOrchestrator; resolveSyncPassword: (ref: string) => Promise<string | undefined>; roots: string[]; runAbortControllers: Map<string, AbortController>; runCancels: Map<string, { signal: AbortController; settle: (d: 'rollback' | 'keep') => void; decided: boolean }>; runs: RunRegistry; scheduler: AutoSyncScheduler; selectionCache: Partial<Record<
|
|
1674
|
+
export type RouteEnvInferred = { sessionExportFetch: ((input: string, init: RequestInit) => Promise<Response>) | undefined; sessionExportPort: number | undefined; adapters: ConfigAdapter<unknown>[]; sessionHealth: { homeDir: string; targetFormatVersion: () => number | undefined; workspaceKeys: () => Promise<ReadonlySet<string>>; knownSessionIds: () => Promise<ReadonlySet<string>>; }; backupScheduler: BackupScheduler; bootSafetyAudit: () => Promise<BootSafetyReport>; cancelDecisionTimeoutMs: number; buildMarketSummary: (e: { url: string; addedAt: string; }) => Promise<MarketSummary>; credentials: CredentialProvider; dataDir: string; exportsDir: string; githubAuth: GitHubAuthClient; githubClientId: string | undefined; githubClientSecret: string | undefined; githubFlows: DeviceFlowStore; history: MigrationStore; host: ConfigManagerHostContext; itemCached: (url: string, itemId: string) => Promise<boolean>; knownSyncSectionIds: Set<SectionId>; makeImporter: () => Importer; makeMarketReader: () => GitMarketReader; makeRecoveryExecutors: (runId: string) => RecoveryExecutorFns; makeSyncEngine: (cfg: SyncConfig, engineOpts?: { includeOptInSections?: boolean; }) => SyncEngine; marketBootAutoRefreshed: { value: boolean; }; marketCacheIndex: (url: string) => string; marketDir: string; marketCacheItemDir: (url: string) => string; marketStarCache: StarCache; marketWorkDir: (url: string) => string; meGitHubRest: GitHubAuthRest; meService: MyRepoService; meTokenProvider: () => Promise<string>; msg: MsgFunc; prepareSync: (body: Record<string, unknown>) => Promise<SyncConfig>; profileLauncher: DshProfileLauncher; profileRuntime: DshProfileRuntimeRegistry; profiles: DshProfileManager; pruneStagedMarketZips: () => Promise<void>; readCachedIndexObj: (url: string) => Promise<MarketIndex | null>; recoveryOrchestrator: RecoveryOrchestrator; resolveSyncPassword: (ref: string) => Promise<string | undefined>; roots: string[]; runAbortControllers: Map<string, AbortController>; runCancels: Map<string, { signal: AbortController; settle: (d: 'rollback' | 'keep') => void; decided: boolean }>; runs: RunRegistry; scheduler: AutoSyncScheduler; selectionCache: Partial<Record<SyncTransportType, SyncSelection>>; selectionHasOptInSections: (channel: SyncTransportType) => boolean; selectionView: (channel: SyncTransportType) => Promise<SelectionView>; selectionViewByChannel: () => Promise<Record<SyncTransportType, SelectionView>>; snapshotEntrySections: (snapshotDir: string) => Promise<string[]>; snapshotsDir: string; syncCredentialsByChannelView: () => Promise<Record<SyncTransportType, { encryptPasswordConfigured: boolean; decryptPasswordConfigured: boolean; }>>; syncDir: string; syncPasswordConfigured: (ref: string) => Promise<boolean>; syncSectionCatalog: { id: SectionId; displayName: string; portability: Portability; defaultIncluded: boolean; }[]; syncSessions: SyncSessionStore; tmpDir: string; tryAppendHistory: (raw: { kind: MigrationKind; result: MigrationResult; sections: string[]; operationId?: string; snapshotId?: string; runId?: string; source: 'api' | 'autosync' | 'backup-scheduler' | 'recovery' | 'cli' | 'internal'; summary: string; error?: string; }) => Promise<string | undefined>; withMutationGate: (op: string, handler: (req: IncomingMessage, res: ServerResponse, lockCtx?: MutationLockContext, journalCtx?: JournalRunContext) => Promise<void>, opts?: { journaled?: boolean; deferredSnapshot?: boolean; }) => ((req: IncomingMessage, res: ServerResponse) => Promise<void>); selfUpdate: (targetVersion: string) => Promise<SelfUpdateOutcome>; writeItemCache: (url: string, itemId: string, manifestRaw: string, zipBytes: Uint8Array) => Promise<void>; }
|
|
1249
1675
|
|
|
1250
1676
|
/** 解密错误 → 用户可读文本:BAD_PASSWORD 只报「密码错误」(不泄内部细节),其余原文 */
|
|
1251
1677
|
export function decryptErrorText(error: unknown, msg: MsgFunc): string {
|
|
@@ -1294,8 +1720,9 @@ interface RoutesDeps {
|
|
|
1294
1720
|
/** 同步路由可预期的请求级错误(status 缺省 400;引擎/传输失败走 500)。
|
|
1295
1721
|
* 继承 kit 的 RouteError → 错误→HTTP 映射全仓只有 writeRouteError 一份。 */
|
|
1296
1722
|
export class SyncRouteError extends RouteError {
|
|
1297
|
-
|
|
1298
|
-
|
|
1723
|
+
/** code:稳定机器码(如 cloud.endpointRequired),由 UI 映射进字典;缺省 = 无码。 */
|
|
1724
|
+
constructor(message: string, status: number = 400, code?: string) {
|
|
1725
|
+
super(message, status, code)
|
|
1299
1726
|
this.name = 'SyncRouteError'
|
|
1300
1727
|
}
|
|
1301
1728
|
}
|
|
@@ -1322,7 +1749,11 @@ export async function parseSyncBody(
|
|
|
1322
1749
|
body: Record<string, unknown>,
|
|
1323
1750
|
deps: ParseSyncBodyDeps,
|
|
1324
1751
|
): Promise<SyncConfig> {
|
|
1325
|
-
const
|
|
1752
|
+
const rawTransport = body['transport']
|
|
1753
|
+
// 通道值解析:接受通道名(s3/gist);也接受**具体兼容商**(oss/cos/minio/kodo)直接落到 s3 通道
|
|
1754
|
+
const transport = parseSyncChannel(rawTransport) ?? (isS3CompatProvider(rawTransport) ? 's3' : 'git')
|
|
1755
|
+
if (transport === 's3') return parseCloudS3Body(body, rawTransport, deps)
|
|
1756
|
+
if (transport === 'gist') return parseCloudGistBody(body, deps)
|
|
1326
1757
|
if (transport === 'webdav') {
|
|
1327
1758
|
const url = typeof body['url'] === 'string' ? body['url'].trim() : ''
|
|
1328
1759
|
if (url === '') throw new SyncRouteError('url is required for webdav')
|
|
@@ -1369,6 +1800,115 @@ export async function parseSyncBody(
|
|
|
1369
1800
|
}
|
|
1370
1801
|
}
|
|
1371
1802
|
|
|
1803
|
+
/** 请求体字段 → 去空白字符串(非字符串 = 空)。 */
|
|
1804
|
+
function syncStr(value: unknown): string {
|
|
1805
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/** 云端点密钥写入失败 → 可操作错误(**含 ref 名、绝不含密钥值**)。 */
|
|
1809
|
+
function cloudCredentialWriteError(ref: string, kind: string, error: unknown): SyncRouteError {
|
|
1810
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
1811
|
+
return new SyncRouteError(
|
|
1812
|
+
kind + ' 写入 DSH credentials 失败:' + reason + '(请在 DSH 凭据管理里配置 ' + ref + ' 后重试)',
|
|
1813
|
+
400,
|
|
1814
|
+
'cloud.credentialsWriteFailed',
|
|
1815
|
+
)
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* s3 通道请求体 → SyncConfig(provider 必填或由 transport 直接给出)。
|
|
1820
|
+
* 非密字段按 validateCloudChannelInput 校验(错误码 = HTTP body.code,由 UI 映射字典);
|
|
1821
|
+
* 密钥**只写**进 DSH credentials 的 cloudSecretRef(provider) 槽位,绝不进返回值 / 文件 / 日志。
|
|
1822
|
+
*/
|
|
1823
|
+
async function parseCloudS3Body(
|
|
1824
|
+
body: Record<string, unknown>,
|
|
1825
|
+
rawTransport: unknown,
|
|
1826
|
+
deps: ParseSyncBodyDeps,
|
|
1827
|
+
): Promise<SyncConfig> {
|
|
1828
|
+
const provider = isS3CompatProvider(body['provider'])
|
|
1829
|
+
? body['provider']
|
|
1830
|
+
: (isS3CompatProvider(rawTransport) ? rawTransport : null)
|
|
1831
|
+
if (provider === null) {
|
|
1832
|
+
throw new SyncRouteError('provider is required for s3 (s3/oss/cos/minio/kodo)', 400, 'cloud.providerUnknown')
|
|
1833
|
+
}
|
|
1834
|
+
const endpoint = syncStr(body['endpoint']).replace(/\/+$/, '')
|
|
1835
|
+
const region = syncStr(body['region'])
|
|
1836
|
+
const bucket = syncStr(body['bucket'])
|
|
1837
|
+
const accessKeyId = syncStr(body['accessKeyId'])
|
|
1838
|
+
const prefix = syncStr(body['prefix'])
|
|
1839
|
+
const pathStyle = typeof body['pathStyle'] === 'boolean' ? body['pathStyle'] : undefined
|
|
1840
|
+
const dialectId = body['dialectId'] === 'aws4' || body['dialectId'] === 'oss4' ? body['dialectId'] : undefined
|
|
1841
|
+
const issue = validateCloudChannelInput(provider, {
|
|
1842
|
+
endpoint,
|
|
1843
|
+
region,
|
|
1844
|
+
bucket,
|
|
1845
|
+
accessKeyId,
|
|
1846
|
+
...(prefix === '' ? {} : { prefix }),
|
|
1847
|
+
...(pathStyle === undefined ? {} : { pathStyle }),
|
|
1848
|
+
...(dialectId === undefined ? {} : { dialectId }),
|
|
1849
|
+
})
|
|
1850
|
+
if (issue !== null) throw new SyncRouteError(issue, 400, issue)
|
|
1851
|
+
const secret = typeof body['secret'] === 'string' && body['secret'] !== '' ? body['secret'] : undefined
|
|
1852
|
+
if (secret !== undefined) {
|
|
1853
|
+
const ref = cloudSecretRef(provider)
|
|
1854
|
+
try {
|
|
1855
|
+
await deps.credentials.set(credentialRef(ref), secret)
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
throw cloudCredentialWriteError(ref, 'S3 密钥', error)
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
schemaVersion: 2,
|
|
1862
|
+
transport: 's3',
|
|
1863
|
+
s3: {
|
|
1864
|
+
provider,
|
|
1865
|
+
endpoint,
|
|
1866
|
+
region,
|
|
1867
|
+
bucket,
|
|
1868
|
+
accessKeyId,
|
|
1869
|
+
...(prefix === '' ? {} : { prefix }),
|
|
1870
|
+
...(pathStyle === undefined ? {} : { pathStyle }),
|
|
1871
|
+
...(dialectId === undefined ? {} : { dialectId }),
|
|
1872
|
+
},
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* gist 通道请求体 → SyncConfig。token 只写进 cloudSecretRef('gist') 槽位
|
|
1878
|
+
* (请求体字段名 token 或 secret 都接受;值绝不进返回值 / 文件 / 日志)。
|
|
1879
|
+
*/
|
|
1880
|
+
async function parseCloudGistBody(body: Record<string, unknown>, deps: ParseSyncBodyDeps): Promise<SyncConfig> {
|
|
1881
|
+
const gistId = syncStr(body['gistId'])
|
|
1882
|
+
const apiBaseUrl = syncStr(body['apiBaseUrl'])
|
|
1883
|
+
const filePrefix = syncStr(body['filePrefix'])
|
|
1884
|
+
const issue = validateCloudChannelInput(GIST_PROVIDER, {
|
|
1885
|
+
gistId,
|
|
1886
|
+
...(apiBaseUrl === '' ? {} : { apiBaseUrl }),
|
|
1887
|
+
...(filePrefix === '' ? {} : { filePrefix }),
|
|
1888
|
+
})
|
|
1889
|
+
if (issue !== null) throw new SyncRouteError(issue, 400, issue)
|
|
1890
|
+
const token = typeof body['token'] === 'string' && body['token'] !== ''
|
|
1891
|
+
? body['token']
|
|
1892
|
+
: (typeof body['secret'] === 'string' && body['secret'] !== '' ? body['secret'] : undefined)
|
|
1893
|
+
if (token !== undefined) {
|
|
1894
|
+
const ref = cloudSecretRef(GIST_PROVIDER)
|
|
1895
|
+
try {
|
|
1896
|
+
await deps.credentials.set(credentialRef(ref), token)
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
throw cloudCredentialWriteError(ref, 'Gist token', error)
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
schemaVersion: 2,
|
|
1903
|
+
transport: 'gist',
|
|
1904
|
+
gist: {
|
|
1905
|
+
gistId,
|
|
1906
|
+
...(apiBaseUrl === '' ? {} : { apiBaseUrl }),
|
|
1907
|
+
...(filePrefix === '' ? {} : { filePrefix }),
|
|
1908
|
+
},
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1372
1912
|
/** 由 SyncConfig 合成 WebDAV 通道 baseUrl(webdav.url,尾部规范化带 '/';git 通道返回 '')。 */
|
|
1373
1913
|
export function webdavBaseUrl(cfg: SyncConfig): string {
|
|
1374
1914
|
if (!isWebDavConfig(cfg)) return ''
|
|
@@ -1901,8 +2441,15 @@ async function sessionHealthKnownIds(host: HostContext): Promise<ReadonlySet<str
|
|
|
1901
2441
|
ids.add(sessionIdKey(childId))
|
|
1902
2442
|
if (typeof relation.parent === 'string' && relation.parent !== '') ids.add(sessionIdKey(relation.parent))
|
|
1903
2443
|
}
|
|
1904
|
-
} catch {
|
|
1905
|
-
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
// T8-F2:**不许静默**。port 层刚刚改成「认不出形状就抛错」(F-1),调用点若把声响整吞,
|
|
2446
|
+
// 用户看到的仍是「父子联动无缘无故不生效」。行为不变(不猜、落回下面的 storages 缓存),
|
|
2447
|
+
// 但必须留下一条带机器可读 code 的 warn —— 这是本仓那条教训的正面形态:
|
|
2448
|
+
// 注释承诺的防线,必须在运行时真的发生(可观测)。
|
|
2449
|
+
host.log.warn(msgOf(host)('host.parentRelationsUnavailable', { reason: errorMessage(err) }), {
|
|
2450
|
+
code: 'parent-relations-unavailable',
|
|
2451
|
+
source: 'sessionHealthKnownIds',
|
|
2452
|
+
})
|
|
1906
2453
|
}
|
|
1907
2454
|
}
|
|
1908
2455
|
if (ids.size > 0) return ids
|
|
@@ -2044,7 +2591,10 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2044
2591
|
},
|
|
2045
2592
|
writeText: async (relPath, text) => { await host.fs.writeFile(relPath, Buffer.from(text, 'utf8')) },
|
|
2046
2593
|
resolveBundle: bundleResolvable,
|
|
2047
|
-
|
|
2594
|
+
// 启动关键 yaml 里包含**两层 cordis.patch.yml**(BOOT_CRITICAL_RELS),它们是 DSH 的专用方言
|
|
2595
|
+
// (`!!js`)。用缺省 schema 解析会把用户的 patch 层误报成「启动关键文件无法解析」——
|
|
2596
|
+
// 与 issue #75 同一个根因,只是症状换成了假警报。其余设置类 yaml 用同一 schema 只会更宽松。
|
|
2597
|
+
parseYaml: (text) => loadPatchDocument(text),
|
|
2048
2598
|
msg,
|
|
2049
2599
|
})
|
|
2050
2600
|
|
|
@@ -2189,8 +2739,7 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2189
2739
|
* 缺失通道 = 尚未加载(启动竞态窗口);读取/使用处兜底 defaultSyncSelection。 */
|
|
2190
2740
|
const selectionCache: Partial<Record<SyncTransportType, SyncSelection>> = {}
|
|
2191
2741
|
void readAllSyncSelections(syncDir).then((all) => {
|
|
2192
|
-
selectionCache
|
|
2193
|
-
selectionCache.webdav = all.webdav
|
|
2742
|
+
for (const channel of SYNC_CHANNELS) selectionCache[channel] = all[channel]
|
|
2194
2743
|
}).catch(() => { /* 读失败保持缺省 */ })
|
|
2195
2744
|
|
|
2196
2745
|
/** 确保指定通道缓存已加载(status/save 路由调用;启动竞态兜底)。 */
|
|
@@ -2251,11 +2800,15 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2251
2800
|
|
|
2252
2801
|
const selectionViewByChannel = async (): Promise<Record<SyncTransportType, SelectionView>> => {
|
|
2253
2802
|
const all = await readAllSyncSelections(syncDir)
|
|
2254
|
-
selectionCache.git = all.git
|
|
2255
|
-
selectionCache.webdav = all.webdav
|
|
2256
2803
|
const view = (sel: SyncSelection): SelectionView =>
|
|
2257
2804
|
({ mode: sel.mode, sections: sel.sections, sessionsLimit: sel.sessionsLimit, sessionsInclude: sel.sessionsInclude, encrypt: sel.encrypt, includeSecrets: sel.includeSecrets })
|
|
2258
|
-
|
|
2805
|
+
// 通道集合来自唯一枚举(SYNC_CHANNELS);不得穷举字面量(漏一处即该通道状态静默缺失)
|
|
2806
|
+
const out = {} as Record<SyncTransportType, SelectionView>
|
|
2807
|
+
for (const channel of SYNC_CHANNELS) {
|
|
2808
|
+
selectionCache[channel] = all[channel]
|
|
2809
|
+
out[channel] = view(all[channel])
|
|
2810
|
+
}
|
|
2811
|
+
return out
|
|
2259
2812
|
}
|
|
2260
2813
|
|
|
2261
2814
|
/** 该通道的持久化选择是否显式勾选了「可选分区」(sessions)—— 决定用户驱动的拉取侧能否看见它。 */
|
|
@@ -2287,6 +2840,41 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2287
2840
|
timeoutMs: WEBDAV_TIMEOUT_MS,
|
|
2288
2841
|
msg,
|
|
2289
2842
|
})
|
|
2843
|
+
} else if (isS3Config(cfg)) {
|
|
2844
|
+
// S3 兼容系(s3/oss/cos/minio/kodo 共用同一实现 + 变体表):AccessKey Secret 只经
|
|
2845
|
+
// credentials 端口现取(永不落盘 / 永不出宿主);endpoint/region 空串由变体模板兜底。
|
|
2846
|
+
const provider = cfg.s3.provider
|
|
2847
|
+
transport = new S3Transport({
|
|
2848
|
+
provider,
|
|
2849
|
+
bucket: cfg.s3.bucket,
|
|
2850
|
+
accessKeyId: cfg.s3.accessKeyId,
|
|
2851
|
+
credentials: {
|
|
2852
|
+
getSecretAccessKey: async () => {
|
|
2853
|
+
const resolved = await credentials.resolve(credentialRef(cloudSecretRef(provider)))
|
|
2854
|
+
return resolved?.value ?? ''
|
|
2855
|
+
},
|
|
2856
|
+
},
|
|
2857
|
+
...(cfg.s3.endpoint === '' ? {} : { endpoint: cfg.s3.endpoint }),
|
|
2858
|
+
...(cfg.s3.region === '' ? {} : { region: cfg.s3.region }),
|
|
2859
|
+
...(cfg.s3.prefix === undefined ? {} : { prefix: cfg.s3.prefix }),
|
|
2860
|
+
...(cfg.s3.pathStyle === undefined ? {} : { pathStyle: cfg.s3.pathStyle }),
|
|
2861
|
+
...(cfg.s3.dialectId === undefined ? {} : { dialectId: cfg.s3.dialectId }),
|
|
2862
|
+
msg,
|
|
2863
|
+
})
|
|
2864
|
+
} else if (isGistConfig(cfg)) {
|
|
2865
|
+
// GitHub Gist:token 只经 credentials 端口现取(只写不回读)
|
|
2866
|
+
transport = new GistTransport({
|
|
2867
|
+
gistId: cfg.gist.gistId,
|
|
2868
|
+
credentials: {
|
|
2869
|
+
getToken: async () => {
|
|
2870
|
+
const resolved = await credentials.resolve(credentialRef(cloudSecretRef(GIST_PROVIDER)))
|
|
2871
|
+
return resolved?.value ?? ''
|
|
2872
|
+
},
|
|
2873
|
+
},
|
|
2874
|
+
...(cfg.gist.apiBaseUrl === undefined ? {} : { apiBaseUrl: cfg.gist.apiBaseUrl }),
|
|
2875
|
+
...(cfg.gist.filePrefix === undefined ? {} : { filePrefix: cfg.gist.filePrefix }),
|
|
2876
|
+
msg,
|
|
2877
|
+
})
|
|
2290
2878
|
} else {
|
|
2291
2879
|
transport = new GitTransport({
|
|
2292
2880
|
repoUrl: cfg.git.repoUrl,
|
|
@@ -2522,6 +3110,9 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2522
3110
|
const restoreOpts = {
|
|
2523
3111
|
snapshotDir: dir, homeDir: host.homeDir, profile: host.profile, settingsPath: undefined, msg,
|
|
2524
3112
|
snapshotsRoot: snapshotsDir, environmentFingerprint: host.phase3Recovery?.recoveryEnvFingerprint ?? 'unknown', requireOperationBound: true,
|
|
3113
|
+
// F-4:本机**权威**版本(拉起本宿主的运行时 = profileContext.installAnchor 解析出的 dshVersion)——
|
|
3114
|
+
// 与快照记录的来源版本不同时产出一条可见告警动作(不阻断恢复)。
|
|
3115
|
+
currentDshVersion: host.dshVersion,
|
|
2525
3116
|
}
|
|
2526
3117
|
const plan = await planRestore(restoreOpts)
|
|
2527
3118
|
const report = await executeRestorePlan(plan, makeRestoreExecutor(dir, host, host.profile), (info) => {
|
|
@@ -2575,6 +3166,12 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2575
3166
|
profileLauncher,
|
|
2576
3167
|
profileRuntime,
|
|
2577
3168
|
profiles,
|
|
3169
|
+
// F-2:官方 session.export 通道只需要「宿主自己能不能发一次本机请求」与「web 端口」。
|
|
3170
|
+
// 后者在 apply() 里 webServer 就绪时才赋值,而本构造发生在它之前 —— 因此这里不能直接
|
|
3171
|
+
// 抓 host.webPort 的值(那一刻恒 undefined)。改成读一个**活盒子**:apply() 往盒子里写,
|
|
3172
|
+
// 路由表在请求期从同一个盒子取值(见 apply() 的 webServer 就绪段)。
|
|
3173
|
+
get sessionExportFetch() { return host.fetch },
|
|
3174
|
+
get sessionExportPort(): number | undefined { return host.webPort },
|
|
2578
3175
|
// T4:会话体检(只读)。宿主侧注入「本机会话库扫描」的依赖来源 —— 路由不许自己读盘。
|
|
2579
3176
|
sessionHealth: {
|
|
2580
3177
|
// profileDir/dataDir 不在本作用域(apply() 的局部量),体检只认 homeDir 与两个事实来源
|
|
@@ -2604,6 +3201,20 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2604
3201
|
syncDir,
|
|
2605
3202
|
syncPasswordConfigured,
|
|
2606
3203
|
syncSectionCatalog,
|
|
3204
|
+
// 关于页「立即更新」:用户显式点击后的写动作(官方 dsh plugin 通道,钉住精确版本)。
|
|
3205
|
+
// 为什么经 env 注入而不是路由里直接 spawn:路由不持 profileDir/profile,也不该自己起
|
|
3206
|
+
// 子进程(与 sessionHealth 同一条纪律:宿主注入依赖来源)。
|
|
3207
|
+
selfUpdate: async (targetVersion: string): Promise<SelfUpdateOutcome> => {
|
|
3208
|
+
const installedSpec = readProfileManifest(host.profileDir)?.dependencies?.[PLUGIN_NPM_PACKAGE]
|
|
3209
|
+
const plan = planSelfUpdate({
|
|
3210
|
+
current: PLUGIN_VERSION,
|
|
3211
|
+
target: targetVersion,
|
|
3212
|
+
profile: host.profile,
|
|
3213
|
+
installedSpec,
|
|
3214
|
+
})
|
|
3215
|
+
if (!plan.ok) return plan
|
|
3216
|
+
return await runSelfUpdate(plan, { runner: runDshPlugin, profileDir: host.profileDir, profile: host.profile })
|
|
3217
|
+
},
|
|
2607
3218
|
syncSessions,
|
|
2608
3219
|
tmpDir,
|
|
2609
3220
|
tryAppendHistory,
|
|
@@ -2613,9 +3224,9 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2613
3224
|
}
|
|
2614
3225
|
|
|
2615
3226
|
/**
|
|
2616
|
-
* 路由表:本文件只保留被源码级守卫按**文件窗口**钉住的
|
|
3227
|
+
* 路由表:本文件只保留被源码级守卫按**文件窗口**钉住的 7 条(host-entry 审计 F-04 记录的
|
|
2617
3228
|
* tests/host/**、src/core/model-tools.test.ts、src/core/phase1-wiring.test.ts 的窗口断言)。
|
|
2618
|
-
* 其余
|
|
3229
|
+
* 其余 71 条已按域拆到 src/routes/*.ts,由 buildRoutes 组装。
|
|
2619
3230
|
* 注册顺序不影响匹配:命名路由必须互不相同(webServer 契约)。
|
|
2620
3231
|
*/
|
|
2621
3232
|
const routeEnv: RouteEnvInferred = makeRouteEnv()
|
|
@@ -2865,8 +3476,13 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
2865
3476
|
if (facade?.parentRelations !== undefined) {
|
|
2866
3477
|
try {
|
|
2867
3478
|
sessionsEntry.items = applySessionParentLinks(sessionsEntry.items, subagentParentMap(await facade.parentRelations()))
|
|
2868
|
-
} catch {
|
|
2869
|
-
|
|
3479
|
+
} catch (err) {
|
|
3480
|
+
// T8-F2:行为不变(不标父对话、不猜、预览绝不打挂),但**必须记 warn**:
|
|
3481
|
+
// 否则「选择器勾父带子静默不联动」对用户完全不可见(正是 F-1 想消除的那种失败形态)。
|
|
3482
|
+
host.log.warn(msgOf(host)('host.parentRelationsUnavailable', { reason: errorMessage(err) }), {
|
|
3483
|
+
code: 'parent-relations-unavailable',
|
|
3484
|
+
source: 'exportPreview',
|
|
3485
|
+
})
|
|
2870
3486
|
}
|
|
2871
3487
|
}
|
|
2872
3488
|
}
|
|
@@ -3381,6 +3997,12 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
3381
3997
|
}
|
|
3382
3998
|
// 端口就绪 → 重新自报一次心跳(其它实例的「运行中」徽章与端口信息靠它;token 永不落盘)。
|
|
3383
3999
|
webPort.value = webServer.port
|
|
4000
|
+
// F-2:宿主自请求走这个端口(探测/代理 DSH 自己的 /api/session.export)——
|
|
4001
|
+
// 必须在注册路由**之前**赋值,否则首次探测拿到 undefined 会如实降级成「拿不到状态码」。
|
|
4002
|
+
// 注:本插件是 bundle 包、宿主半没有热重载,端口变化只有重启一次才可能发生。
|
|
4003
|
+
// routeEnv 的 sessionExportPort 是**活 getter**(读 host.webPort),因此这里赋值对已构造好的
|
|
4004
|
+
// 路由表同样生效 —— 不需要重建路由。
|
|
4005
|
+
host.webPort = webServer.port
|
|
3384
4006
|
profileRuntime.announce()
|
|
3385
4007
|
ctx.effect(() => {
|
|
3386
4008
|
const disposers = registerRoutes(webServer, routes)
|