dsh-config-manager 0.1.71 → 0.1.73
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 +149 -98
- package/README.zh-CN.md +245 -88
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +2 -1
- package/lib/adapters/index.js +6 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +21 -0
- package/lib/adapters/messages.js +57 -0
- package/lib/adapters/plugin-data.d.ts +114 -0
- package/lib/adapters/plugin-data.js +309 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +8 -2
- package/lib/adapters/self.js +3 -2
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/test-helpers.d.ts +9 -0
- package/lib/adapters/test-helpers.js +14 -0
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1442 -494
- package/lib/client.js +15326 -8520
- package/lib/core/analyzer.d.ts +11 -2
- package/lib/core/analyzer.js +31 -8
- package/lib/core/backup-plan.d.ts +10 -0
- package/lib/core/backup-plan.js +51 -20
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +25 -1
- package/lib/core/backup.js +99 -28
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +182 -0
- package/lib/core/command-rollback.js +293 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +294 -0
- package/lib/core/messages-core.js +619 -0
- package/lib/core/messages.d.ts +61 -2
- package/lib/core/messages.js +118 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/plugin-data-registry.d.ts +124 -0
- package/lib/core/plugin-data-registry.js +198 -0
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-gate.d.ts +33 -0
- package/lib/core/rollback-gate.js +57 -0
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-ledger.d.ts +248 -0
- package/lib/core/rollback-ledger.js +1282 -0
- package/lib/core/rollback-point.d.ts +1391 -0
- package/lib/core/rollback-point.js +3503 -0
- package/lib/core/rollback.js +6 -4
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +37 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.d.ts +1 -1
- package/lib/foreign/registry.js +8 -7
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +227 -7
- package/lib/index.js +799 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +43 -0
- package/lib/routes/messages.js +102 -0
- package/lib/routes/prefs.js +26 -3
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.d.ts +21 -0
- package/lib/routes/recovery.js +478 -94
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +79 -17
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +42 -15
- package/lib/schema/section-registry.js +67 -32
- package/lib/schema/types.d.ts +1 -1
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +58 -22
- package/lib/sync/sync-engine.js +119 -48
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/ui-prefs.d.ts +22 -0
- package/lib/sync/ui-prefs.js +28 -0
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/diff-view.d.ts +10 -0
- package/lib/ui/diff-view.js +17 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +26 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +68 -1
- package/lib/ui/i18n.js +120 -2
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +815 -0
- package/lib/ui/rollback-view.js +738 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/test-helpers.js +2 -1
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +61 -0
- package/src/adapters/plugin-data.test.ts +225 -0
- package/src/adapters/plugin-data.ts +351 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +10 -3
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/test-helpers.ts +15 -0
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +411 -0
- package/src/client/chat/RollbackDiffTab.tsx +108 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +607 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +183 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +174 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +946 -0
- package/src/client/chat/sidebar-slots.ts +59 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +133 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +84 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +38 -0
- package/src/client/common/section-labels.test.ts +1 -0
- package/src/client/common/section-labels.ts +2 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +1109 -31
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +232 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +62 -9
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +859 -0
- package/src/client/recovery/RollbackDiffPane.tsx +69 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +144 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +520 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-enabled.ts +41 -0
- package/src/client/recovery/rollback-file-changes.test.ts +114 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/analyzer.ts +31 -9
- package/src/core/backup-plan.test.ts +48 -5
- package/src/core/backup-plan.ts +51 -20
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +105 -29
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +406 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +661 -0
- package/src/core/messages.ts +125 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/plugin-data-registry.test.ts +113 -0
- package/src/core/plugin-data-registry.ts +261 -0
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-gate.test.ts +66 -0
- package/src/core/rollback-gate.ts +61 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-ledger.test.ts +655 -0
- package/src/core/rollback-ledger.ts +1314 -0
- package/src/core/rollback-point.test.ts +2853 -0
- package/src/core/rollback-point.ts +4340 -0
- package/src/core/rollback.ts +5 -3
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +37 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.test.ts +1 -1
- package/src/foreign/registry.ts +8 -7
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +876 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +106 -0
- package/src/routes/prefs.ts +31 -2
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +477 -94
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +82 -17
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest-schema.test.ts +4 -4
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +28 -7
- package/src/schema/section-registry.ts +96 -42
- package/src/schema/types.ts +2 -1
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-credentials.test.ts +119 -0
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +154 -52
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/transport.test.ts +1 -0
- package/src/sync/ui-prefs.test.ts +44 -1
- package/src/sync/ui-prefs.ts +39 -0
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/diff-view.test.ts +9 -1
- package/src/ui/diff-view.ts +22 -0
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +26 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +131 -2
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +836 -0
- package/src/ui/rollback-view.ts +1405 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/test-helpers.ts +2 -1
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每条用户消息一个「↺ 回退」入口 —— **判定层**(框架无关纯函数,node 可测)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一层:`conversation.chat.node` 的 `user` key 是 keyed 遮蔽位(宿主注册
|
|
5
|
+
* `{ name:'conversation.chat.node', key:'user' }` → `UserMessageNodeView`),**没有 children 表**,
|
|
6
|
+
* framework 不提供「往每条用户消息里追加 UI」的位子。要让按钮和原文同时出现,只有一条路:
|
|
7
|
+
* 用更低的 priority **接管该 key 的渲染器**,把宿主那条气泡(`UserStyleBubble`)自己复刻一遍
|
|
8
|
+
* (宿主组件与它的 CSS Modules 全是 ui-chat 私有、不可导入)。
|
|
9
|
+
*
|
|
10
|
+
* 接管宿主渲染器 = 高风险面(我们的组件一抛错,那条消息就在用户眼前消失),所以本文件把三件
|
|
11
|
+
* 最容易出错的事全部提到纯函数里、由单测钉住:
|
|
12
|
+
* ① **节点读取**:owner 给的是宿主运行时对象(`node` 形状随版本漂移)—— 宽度接收、逐字段判定,
|
|
13
|
+
* 读不到就如实降级,**绝不猜**(回合号只认 `node.location.kind === 'turn' | 'step'` 那一条
|
|
14
|
+
* 与宿主 turnTail 同源的读法)。
|
|
15
|
+
* ② **气泡能力面**:把 content 切成 text / image / file / rest 四类,**rest 必须有兜底渲染**
|
|
16
|
+
* (宿主是 JsonBlock,我们标记成 raw 块交给渲染层,绝不静默丢弃)。
|
|
17
|
+
* ③ **按钮状态机**:回滚点命中与否(同会话 + 同回合 + 优先 auto)→ 可点 / 一个可证明的禁用原因
|
|
18
|
+
* (字典键,zh/en 同步)。惰取一次列表、按会话缓存,避免每条消息各发一次请求。
|
|
19
|
+
* ④ **收尾竞态的有界重试**(R1):引擎在回合**收尾之后**还要几秒才写完这个回合的点(真机 7.8 s),
|
|
20
|
+
* 所以「第一次读到空列表」是**暂态**、不是结论。窗口内按固定退避再读(每轮强制刷新、绕过那 30 s
|
|
21
|
+
* 列表缓存),**命中即停**;窗口没关就如实显示「正在读取」,只有窗口关且仍为空才说「没有回滚点」。
|
|
22
|
+
* T6 起窗口是**一次性**的(`ChatPointWindowController`:一个实例最多一轮、可 dispose 取消)——
|
|
23
|
+
* 组件 effect 重跑不会重开窗口;「用户再次触发」靠**新实例**(重新挂载 / 会话或回合变化)。
|
|
24
|
+
*
|
|
25
|
+
* 纪律:本文件 **不产出任何用户可见文案**(只回字典键);**不 import node 模块、不 import src/core**;
|
|
26
|
+
* 不 import React(React 壳在 ChatEntryView.tsx,便于本文件在 node --test 下零依赖直跑)。
|
|
27
|
+
*/
|
|
28
|
+
import type { ReactNode } from 'react'
|
|
29
|
+
import type { RollbackPointPort, RollbackPointSummary } from '../../ui/rollback-view.ts'
|
|
30
|
+
import { findPointForTurn } from './rollback-entry-view.ts'
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------ 槽位常量 */
|
|
33
|
+
|
|
34
|
+
/** keyed 槽位名(宿主在 ChatNodeSeat 里 renderSlot 的那一个)。 */
|
|
35
|
+
export const CHAT_NODE_SLOT = 'conversation.chat.node'
|
|
36
|
+
/** 本插件接管的 entry key(= 用户消息;steering 是**另一个** key,一律不接管)。 */
|
|
37
|
+
export const CHAT_NODE_USER_KEY = 'user'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 接管优先级:**-1**(低于宿主 user 渲染器的缺省 0 ⇒ 遮蔽它)。
|
|
41
|
+
*
|
|
42
|
+
* 依据(ui-slots 的 SlotCore.register 文档逐字):entries 共享一个 cell 时按 priority 升序,
|
|
43
|
+
* **最低的渲染**;同一 cell 的同一 priority 再注册会抛(naming the occupant)——所以 -1 是与宿主
|
|
44
|
+
* 并存的唯一合法取值,写 0 会把整个插件打挂。
|
|
45
|
+
*/
|
|
46
|
+
export const CHAT_NODE_PRIORITY = -1
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 槽位登记计划:**只在 0.2 的 keyed 形态时接管**(2026-10-12:入口只剩「每条用户消息」一种形态,
|
|
50
|
+
* 旧的「整回合」档与偏好门已随 chat-entry-pref / chat-entry-mount 一起删除)。
|
|
51
|
+
*
|
|
52
|
+
* 跳过必须是**失败软化**(只丢这一个入口,不动本插件其余槽位、不动宿主启动):
|
|
53
|
+
* · `slot-kind-mismatch`:0.1.x 的 ui-chat 把同一 key 声明成别的形态(对照 turnTail 的
|
|
54
|
+
* list/chain 双形态实测),按 keyed 参数(key/priority)注册会抛。
|
|
55
|
+
*/
|
|
56
|
+
export type ChatNodeRegistrationPlan =
|
|
57
|
+
| { kind: 'register' }
|
|
58
|
+
| { kind: 'skip'; reason: 'slot-kind-mismatch'; declared: string | null }
|
|
59
|
+
|
|
60
|
+
export function planChatNodeRegistration(declaredKind: string | null): ChatNodeRegistrationPlan {
|
|
61
|
+
if (declaredKind !== 'keyed') return { kind: 'skip', reason: 'slot-kind-mismatch', declared: declaredKind }
|
|
62
|
+
return { kind: 'register' }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ------------------------------------------------------------ 节点 / owner 读取 */
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 宿主交给本 slot 的 owner props 的**结构子集**(只列我们真的读的字段)。
|
|
69
|
+
*
|
|
70
|
+
* 为什么用 `unknown` 承接 `node`:`ChatConversationViewNode` 属于 dsh-client-runtime 的
|
|
71
|
+
* conversation 契约,**本仓库没有安装** ui-chat / ui-session 的类型(见 chat-slots.ts 头注释的
|
|
72
|
+
* 版本差异说明);写死一个我们无法核实的形状比「宽度接收 + 逐字段判定」更容易在升级时静默错用。
|
|
73
|
+
*/
|
|
74
|
+
export interface ChatNodeOwnerProps {
|
|
75
|
+
/** 宿主运行时节点(形状漂移面)。 */
|
|
76
|
+
node?: unknown
|
|
77
|
+
/** 图片渲染位(**必须**用它渲染图片;ui-chat 私有实现不可导入)。返回值是不透明 ReactNode。 */
|
|
78
|
+
renderMessageImages?: (owner: unknown) => ReactNode
|
|
79
|
+
/** 文件打开器(宿主把 references 交给气泡;我们只做类型兜底,不接线)。 */
|
|
80
|
+
openFile?: unknown
|
|
81
|
+
/** skill 打开器。 */
|
|
82
|
+
openSkill?: unknown
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
86
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 该节点的 kind(`user` / `steering` / …);读不到回 null。 */
|
|
90
|
+
export function chatNodeKind(node: unknown): string | null {
|
|
91
|
+
const kind = asRecord(node)?.['kind']
|
|
92
|
+
return typeof kind === 'string' ? kind : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 一个回合的可核实坐标(宿主 `TurnLocation` 的结构子集)。 */
|
|
96
|
+
export interface ChatNodeTurn {
|
|
97
|
+
/** 1-based 回合号(宿主 turnTail 用的同一个字段)。 */
|
|
98
|
+
turn: number
|
|
99
|
+
/** `open` / `closed` / `unknown`;读不到回 null(→ 按「不能证明已收尾」处理)。 */
|
|
100
|
+
status: string | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 从节点读回合坐标 —— 与宿主 ChatNodeSeat / AssistantNodeView **同一条读法**:
|
|
105
|
+
* `location.kind === 'turn' | 'step' ? location.turn.turn : undefined`(反编译证据:
|
|
106
|
+
* ui-chat client.js 的 `turnOf()`)。读不到回 null ⇒ 调用点**如实禁用**,禁止猜。
|
|
107
|
+
*/
|
|
108
|
+
export function chatNodeTurn(node: unknown): ChatNodeTurn | null {
|
|
109
|
+
const location = asRecord(asRecord(node)?.['location'])
|
|
110
|
+
if (location === null) return null
|
|
111
|
+
const kind = location['kind']
|
|
112
|
+
if (kind !== 'turn' && kind !== 'step') return null
|
|
113
|
+
const turn = asRecord(location['turn'])
|
|
114
|
+
const number = turn?.['turn']
|
|
115
|
+
if (typeof number !== 'number' || !Number.isFinite(number)) return null
|
|
116
|
+
const status = turn?.['status']
|
|
117
|
+
return { turn: number, status: typeof status === 'string' ? status : null }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ------------------------------------------------------------ 气泡视图模型 */
|
|
121
|
+
|
|
122
|
+
/** 一张图片的载荷(逐字转发给 `renderMessageImages`,见 HostImageRenderOwner)。 */
|
|
123
|
+
export interface ChatEntryImage {
|
|
124
|
+
attachment?: unknown
|
|
125
|
+
preview?: unknown
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 文件卡的展示事实(宿主是 FileTypeIcon + 文件名 + 扩展名 + 体积;我们做前三项的样子见渲染层)。 */
|
|
129
|
+
export interface ChatEntryFile {
|
|
130
|
+
name: string
|
|
131
|
+
/** 扩展名(大写,≤8 字符;无扩展名回 null)。 */
|
|
132
|
+
ext: string | null
|
|
133
|
+
/** 字节数;宿主没给回 null(不是 0)。 */
|
|
134
|
+
bytes: number | null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 我们无法理解、必须原样兜底渲染的块(宿主是 JsonBlock)。 */
|
|
138
|
+
export interface ChatEntryRawBlock {
|
|
139
|
+
id: string
|
|
140
|
+
payload: unknown
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 一个附件(**有序**,按 content 里出现的次序):宿主 `UserStyleBubble` 就是
|
|
145
|
+
* `attachments.map((attachment, index) => …)` —— 图片与文件卡共用**同一条序列**、各占一个位置。
|
|
146
|
+
*/
|
|
147
|
+
export type ChatEntryAttachment =
|
|
148
|
+
| { kind: 'image'; image: ChatEntryImage }
|
|
149
|
+
| { kind: 'file'; file: ChatEntryFile }
|
|
150
|
+
|
|
151
|
+
export interface ChatEntryBubbleView {
|
|
152
|
+
/** 文本块拼接结果(宿主 `texts.join('')`,逐字保留换行与空白;会话引用的线格式已投影成 label)。 */
|
|
153
|
+
text: string
|
|
154
|
+
/** **有序附件列表**:渲染层必须按这个顺序输出(宿主不区分「先图片后文件」)。 */
|
|
155
|
+
attachments: ChatEntryAttachment[]
|
|
156
|
+
/** 图片附件(= `attachments` 里的 image 项,顺序一致;用于计数与「无渲染位」提示)。 */
|
|
157
|
+
images: ChatEntryImage[]
|
|
158
|
+
/** 文件附件(= `attachments` 里的 file 项,顺序一致)。 */
|
|
159
|
+
files: ChatEntryFile[]
|
|
160
|
+
/** rest / 未知块:**必须上屏**(渲染层用 JsonBlock 同款 JSON 视图),不得静默丢弃。 */
|
|
161
|
+
raw: ChatEntryRawBlock[]
|
|
162
|
+
/** 是否画气泡框(宿主:text 非空或 rest 非空)。 */
|
|
163
|
+
showBubble: boolean
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 图片渲染位的 owner 载荷(与宿主 `renderMessageImages({ images, align, compact })` 逐字同形)。 */
|
|
167
|
+
export interface HostImageRenderOwner {
|
|
168
|
+
images: readonly ChatEntryImage[]
|
|
169
|
+
align: 'end'
|
|
170
|
+
compact: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 节点 data 的结构子集(`content` / `referenceLabels` / `skillNames` 是宿主 UserMessageNodeView 读的三个字段;
|
|
175
|
+
* `time` 是宿主交给 MessageIconActions 的那一个时间戳)。 */
|
|
176
|
+
export interface ChatEntryNodeDataView {
|
|
177
|
+
content: unknown
|
|
178
|
+
referenceLabels: string[] | null
|
|
179
|
+
skillNames: string[] | null
|
|
180
|
+
/** 消息时间戳(宿主 `data.time`,交给 MessageIconActions 的同一个值);读不到回 null。 */
|
|
181
|
+
time: number | null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const CHAT_ENTRY_ALIGN = 'end' as const
|
|
185
|
+
/**
|
|
186
|
+
* 宿主:`const compactImages = attachments.length > 1` —— 这里的 `attachments` 是
|
|
187
|
+
* **全部附件**(图片 + 文件,= contentParts 吐出的那个数组),**不是「图片数」**。
|
|
188
|
+
* 阈值 1 = 「附件多于一个」。
|
|
189
|
+
*/
|
|
190
|
+
export const CHAT_ENTRY_COMPACT_THRESHOLD = 1
|
|
191
|
+
|
|
192
|
+
/** 宿主 `compactImages` 的同一口径:**附件总数**(图片 + 文件)> 1。 */
|
|
193
|
+
export function chatEntryCompactOf(view: ChatEntryBubbleView): boolean {
|
|
194
|
+
return view.attachments.length > CHAT_ENTRY_COMPACT_THRESHOLD
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 读节点 data 的三个字段(读不到一律回安全值)。 */
|
|
198
|
+
export function readChatEntryNodeData(node: unknown): ChatEntryNodeDataView {
|
|
199
|
+
const data = asRecord(asRecord(node)?.['data'])
|
|
200
|
+
const strings = (value: unknown): string[] | null => {
|
|
201
|
+
if (!Array.isArray(value)) return null
|
|
202
|
+
return value.filter((item): item is string => typeof item === 'string')
|
|
203
|
+
}
|
|
204
|
+
const time = data?.['time']
|
|
205
|
+
return {
|
|
206
|
+
content: data?.['content'] ?? null,
|
|
207
|
+
referenceLabels: strings(data?.['referenceLabels']),
|
|
208
|
+
skillNames: strings(data?.['skillNames']),
|
|
209
|
+
// 只认数字毫秒(宿主 TurnErrorItem 就是 new Date(node.data.time));ISO 串等其它形状一律不猜
|
|
210
|
+
time: typeof time === 'number' && Number.isFinite(time) ? time : null,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 两位补零(宿主 `pad2`)。 */
|
|
215
|
+
function pad2(value: number): string {
|
|
216
|
+
return value < 10 ? '0' + String(value) : String(value)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 时间戳 → 动作行里显示的时钟文本 —— 宿主 `formatMessageClock` 的**逐字复刻**(只取时刻那一半):
|
|
221
|
+
*
|
|
222
|
+
* const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; // **固定 24 小时制、补零**
|
|
223
|
+
*
|
|
224
|
+
* 为什么不用浏览器 Intl 的小时/分钟格式化:它随语言变成 12 小时制(en-US 下 '05:04 PM'),
|
|
225
|
+
* 与宿主那一列的时间**在同一屏里就不一致**(t4 复核 D4)。时区仍走本地(与宿主同一把尺子)。
|
|
226
|
+
*
|
|
227
|
+
* 已追认差距(G-39,登记归 t11):**非当天不加日期**(宿主会补 `clock.md`/`clock.ymd` 前缀)。
|
|
228
|
+
* 读不到时间就回 null(动作行不渲染时间,**不编造当前时刻**)。
|
|
229
|
+
*/
|
|
230
|
+
export function formatChatEntryClock(time: number | null): string | null {
|
|
231
|
+
if (time === null || !Number.isFinite(time)) return null
|
|
232
|
+
const date = new Date(time)
|
|
233
|
+
if (Number.isNaN(date.getTime())) return null
|
|
234
|
+
return pad2(date.getHours()) + ':' + pad2(date.getMinutes())
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ------------------------------------------------------------ 会话语义引用(线格式 → label) */
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 会话引用的**线格式**:`@[label](dsh-session:<id>)` —— 逐字取自宿主 ui-primitives 的
|
|
241
|
+
* `SESSION_WIRE_RE = /@\[([^\]\n]+)\]\(dsh-session:[^)\s]+\)/gu`(同一个正则、同一个捕获组语义:
|
|
242
|
+
* `wire[1]` 是给人看的 label)。
|
|
243
|
+
*/
|
|
244
|
+
export const CHAT_SESSION_WIRE_RE = /@\[([^\]\n]+)\]\(dsh-session:[^)\s]+\)/gu
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 正文的**最小文本投影**:把会话引用线格式换成它的人类可读 label。
|
|
248
|
+
*
|
|
249
|
+
* 宿主做的是「换成 chip 节点」(`projectUserText`,还会把裸 `@name` 也装饰成 chip、把引用汇总成
|
|
250
|
+
* 一行 referenceSummary);我们**不做 chip**(ui-primitives 包私有、不可导入,见 G-39 已追认差距),
|
|
251
|
+
* 但**绝不把线格式原样打给用户看** —— 那是机器的编码,正常人读不了(t4 复核 D5,medium)。
|
|
252
|
+
* 与宿主一致的边界:只认 `dsh-session:` 的括号形式;`g` flag 下逐处替换,正则自身 lastIndex 置零。
|
|
253
|
+
*/
|
|
254
|
+
export function projectChatEntryText(text: string): string {
|
|
255
|
+
if (text.indexOf('@[') < 0) return text
|
|
256
|
+
CHAT_SESSION_WIRE_RE.lastIndex = 0
|
|
257
|
+
return text.replace(CHAT_SESSION_WIRE_RE, '$1')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 扩展名 → 文件卡上的短徽标:宿主 `fileExtension` + 宿主那行 meta 的同一截断
|
|
262
|
+
* (`fileExtension(name).toUpperCase().slice(0, 8)`);后缀为空(无点 / 点在末尾)回 null。
|
|
263
|
+
*
|
|
264
|
+
* 与宿主的两点对齐说明:① **前导点算后缀**(`.gitignore` → `gitignore` → 徽标 `GITIGNOR`);
|
|
265
|
+
* ② 上限 8 字符是宿主**显示层**的截断,不是我们自己加的花样。 */
|
|
266
|
+
export function fileExtensionOf(name: string): string | null {
|
|
267
|
+
// 路径分隔符同时认 / 与 \(宿主给的是用户文件路径,可能来自 Windows)
|
|
268
|
+
const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'))
|
|
269
|
+
const base = name.slice(lastSlash + 1)
|
|
270
|
+
const dot = base.lastIndexOf('.')
|
|
271
|
+
// 宿主 fileExtension:`dot < 0 ? "" : name.slice(dot + 1)` —— **前导点也算后缀**('.gitignore' → 'gitignore'),
|
|
272
|
+
// 末尾点是空串(宿主那边 filter(Boolean) 后不显示徽标)。这里同样只在空串时回 null。
|
|
273
|
+
const ext = dot < 0 ? '' : base.slice(dot + 1)
|
|
274
|
+
if (ext === '') return null
|
|
275
|
+
const upper = ext.toUpperCase()
|
|
276
|
+
return upper.length > 8 ? upper.slice(0, 8) : upper
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 字节数 → 展示字符串 —— 宿主 `fileSizeText` 的**逐字复刻**:
|
|
281
|
+
*
|
|
282
|
+
* bytes < 1024 → `${bytes}B` (**无空格**)
|
|
283
|
+
* kb < 1024 → `${kb < 10 ? kb.toFixed(1) : Math.round(kb)}KB` (≥10 取整:'20KB')
|
|
284
|
+
* mb < 1024 → 同规则 MB
|
|
285
|
+
* 否则 → 同规则 GB
|
|
286
|
+
*
|
|
287
|
+
* 三条注意:① 阈值判的是**单位值**(kb/mb/gb)是否 < 10,不是字节数;② 单位与数字之间**没有空格**;
|
|
288
|
+
* ③ 读不到 / 负数一律回 null(渲染层整段不显示,**不编造 0B**)—— 宿主对负数会打出 "-5B",
|
|
289
|
+
* 那条路径在消息附件里不可达(bytes 来自宿主自己的附件元数据),我们选择如实留空。
|
|
290
|
+
*/
|
|
291
|
+
export function fileSizeTextOf(bytes: number | null): string | null {
|
|
292
|
+
if (bytes === null || !Number.isFinite(bytes) || bytes < 0) return null
|
|
293
|
+
if (bytes < 1024) return String(bytes) + 'B'
|
|
294
|
+
const kb = bytes / 1024
|
|
295
|
+
if (kb < 1024) return String(kb < 10 ? kb.toFixed(1) : Math.round(kb)) + 'KB'
|
|
296
|
+
const mb = kb / 1024
|
|
297
|
+
if (mb < 1024) return String(mb < 10 ? mb.toFixed(1) : Math.round(mb)) + 'MB'
|
|
298
|
+
const gb = mb / 1024
|
|
299
|
+
return String(gb < 10 ? gb.toFixed(1) : Math.round(gb)) + 'GB'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* content → 气泡视图模型(宿主 `contentParts` 的**逐条对齐**实现):
|
|
304
|
+
* · `type === 'text' && typeof text === 'string'` → texts(join('') 保留换行/空白);
|
|
305
|
+
* · `type === 'image' && attachment !== undefined` → 图片位 `{ attachment }`;
|
|
306
|
+
* · `type === 'file' && attachment !== undefined` → 文件卡(文件名 + 扩展名 + 体积);
|
|
307
|
+
* · **其余全部进 rest**(宿主原样交给 JsonBlock;我们绝不静默丢)。
|
|
308
|
+
* content 不是数组(宿主给了怪形状)时:字符串按纯文本块处理,其余进 rest —— 保证「用户消息绝不消失」。
|
|
309
|
+
*/
|
|
310
|
+
export function buildChatEntryBubble(content: unknown): ChatEntryBubbleView {
|
|
311
|
+
if (typeof content === 'string') {
|
|
312
|
+
const text = projectChatEntryText(content)
|
|
313
|
+
return { text, attachments: [], images: [], files: [], raw: [], showBubble: text !== '' }
|
|
314
|
+
}
|
|
315
|
+
if (!Array.isArray(content)) {
|
|
316
|
+
return { text: '', attachments: [], images: [], files: [], raw: content === null || content === undefined ? [] : [{ id: '0', payload: content }], showBubble: content !== null && content !== undefined }
|
|
317
|
+
}
|
|
318
|
+
const texts: string[] = []
|
|
319
|
+
const attachments: ChatEntryAttachment[] = []
|
|
320
|
+
const raw: ChatEntryRawBlock[] = []
|
|
321
|
+
content.forEach((block, index) => {
|
|
322
|
+
const record = asRecord(block)
|
|
323
|
+
const type = record?.['type']
|
|
324
|
+
if (type === 'text' && typeof record?.['text'] === 'string') {
|
|
325
|
+
// 宿主把这条文本交给 projectUserText(会话引用渲染成 chip);我们做**最小文本投影**:线格式 → label
|
|
326
|
+
texts.push(projectChatEntryText(record['text'] as string))
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if (type === 'image' && record?.['attachment'] !== undefined) {
|
|
330
|
+
attachments.push({ kind: 'image', image: { attachment: record['attachment'] } })
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
if (type === 'file' && record?.['attachment'] !== undefined) {
|
|
334
|
+
const attachment = asRecord(record['attachment'])
|
|
335
|
+
const name = attachment?.['name']
|
|
336
|
+
const bytes = attachment?.['bytes']
|
|
337
|
+
attachments.push({
|
|
338
|
+
kind: 'file',
|
|
339
|
+
file: {
|
|
340
|
+
name: typeof name === 'string' && name !== '' ? name : '',
|
|
341
|
+
ext: typeof name === 'string' ? fileExtensionOf(name) : null,
|
|
342
|
+
bytes: typeof bytes === 'number' && Number.isFinite(bytes) ? bytes : null,
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
raw.push({ id: String(index), payload: block })
|
|
348
|
+
})
|
|
349
|
+
const text = texts.join('')
|
|
350
|
+
// images / files 是 attachments 的**派生视图**(不独立积累,避免两处真相与顺序漂移)
|
|
351
|
+
const images = attachments.flatMap((item) => (item.kind === 'image' ? [item.image] : []))
|
|
352
|
+
const files = attachments.flatMap((item) => (item.kind === 'file' ? [item.file] : []))
|
|
353
|
+
return { text, attachments, images, files, raw, showBubble: text !== '' || raw.length > 0 }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 渲染计划的一项:图片带 owner 载荷,文件带名称/扩展名/体积文本。 */
|
|
357
|
+
export type ChatEntryAttachmentPlan =
|
|
358
|
+
| { kind: 'image'; index: number; owner: HostImageRenderOwner }
|
|
359
|
+
| { kind: 'file'; index: number; file: ChatEntryFile; size: string | null }
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 有序附件 → 渲染计划(宿主 `attachments.map` 的逐字同构):
|
|
363
|
+
* · **顺序 = content 原始顺序**(绝不重排成「先图片后文件」);
|
|
364
|
+
* · 图片**每张一次** `renderMessageImages({ images: [image], align: 'end', compact })`
|
|
365
|
+
* —— 每次调用 = 一个独立的 `conversation.message.images` slot 出现点,渲染器按**单图**语义工作,
|
|
366
|
+
* 所以不能把整组塞进一次调用;
|
|
367
|
+
* · `compact` 按**附件总数**算(见 `chatEntryCompactOf`)。
|
|
368
|
+
*/
|
|
369
|
+
export function planChatEntryAttachments(view: ChatEntryBubbleView): ChatEntryAttachmentPlan[] {
|
|
370
|
+
const compact = chatEntryCompactOf(view)
|
|
371
|
+
return view.attachments.map((attachment, index) => attachment.kind === 'image'
|
|
372
|
+
? { kind: 'image', index, owner: { images: [attachment.image], align: CHAT_ENTRY_ALIGN, compact } }
|
|
373
|
+
: { kind: 'file', index, file: attachment.file, size: fileSizeTextOf(attachment.file.bytes) })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ------------------------------------------------------------ 按钮状态机 */
|
|
377
|
+
|
|
378
|
+
/** 一条用户消息上那颗「回退」按钮的状态。 */
|
|
379
|
+
export type ChatEntryButton =
|
|
380
|
+
| { kind: 'enabled'; turn: number }
|
|
381
|
+
| { kind: 'disabled'; code: ChatEntryDisabledCode; key: string }
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 禁用原因(**每个码只说自己能证明的那件事**;键在 chat 字典里,zh/en 同步):
|
|
385
|
+
* · `noTurn` —— 节点里取不到可核实的回合号(未识别宿主版本 / 未来形状漂移);
|
|
386
|
+
* · `turnOpen` —— `node.location.turn.status === 'open'`:回合**确实**没结束,留点还没发生;
|
|
387
|
+
* · `noSession` —— 没有 sessionId,无法按「同会话」匹配;
|
|
388
|
+
* · `pointsPending` —— 惰取还没回来,或**收尾后的有界重试窗口还没关**(窗口内列表读到空只是暂态,
|
|
389
|
+
* 所以如实说「正在读取」;**不假装没有**);
|
|
390
|
+
* · `noPoint` —— 列表已读到、没有「同会话 + 同回合」的点,**且重试窗口已关**(已证实);
|
|
391
|
+
* · `turnUnknown` —— `status` 不是 `'closed'` 也不是 `'open'`(unknown / 读不到):
|
|
392
|
+
* 不能证明回合已收尾,也不能证明没结束,如实说「收尾状态未知」。
|
|
393
|
+
*/
|
|
394
|
+
export type ChatEntryDisabledCode = 'noTurn' | 'turnOpen' | 'noSession' | 'pointsPending' | 'noPoint' | 'turnUnknown'
|
|
395
|
+
|
|
396
|
+
/** 每个禁用码各自的字典键(**六个原因各有自己的话**:合成一句就会有一半在撒谎)。 */
|
|
397
|
+
export const CHAT_ENTRY_DISABLED_KEYS: Record<ChatEntryDisabledCode, string> = {
|
|
398
|
+
noTurn: 'chat.entry.disabled.noTurn',
|
|
399
|
+
turnOpen: 'chat.entry.disabled.turnOpen',
|
|
400
|
+
noSession: 'chat.entry.disabled.noSession',
|
|
401
|
+
pointsPending: 'chat.entry.disabled.pointsPending',
|
|
402
|
+
noPoint: 'chat.entry.disabled.noPoint',
|
|
403
|
+
turnUnknown: 'chat.entry.disabled.turnUnknown',
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 回滚点列表的惰取结果(`null` = 缓存未命中,调用点负责去读)。 */
|
|
407
|
+
export interface ChatPointLookup {
|
|
408
|
+
/** 该(会话 + 回合)命中的回滚点;未读到时 null。 */
|
|
409
|
+
point: RollbackPointSummary | null
|
|
410
|
+
/** 列表是否已经读到(false = 还没读 / 读失败 ⇒ 不得断言「没有」)。 */
|
|
411
|
+
resolved: boolean
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** 该回合的收尾状态判定(只认这三个取值,其余一律 unknown)。 */
|
|
415
|
+
export type ChatEntryTurnPhase = 'closed' | 'open' | 'unknown'
|
|
416
|
+
|
|
417
|
+
export function chatEntryTurnPhase(turn: ChatNodeTurn | null): ChatEntryTurnPhase {
|
|
418
|
+
if (turn === null) return 'unknown'
|
|
419
|
+
if (turn.status === 'closed') return 'closed'
|
|
420
|
+
if (turn.status === 'open') return 'open'
|
|
421
|
+
return 'unknown'
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 按钮状态机(**唯一判定点**):
|
|
426
|
+
* ① 没有可核实的回合号 → `noTurn`(禁止猜);
|
|
427
|
+
* ② 回合明确 open → `turnOpen`(还没收尾 ⇒ 还没有这个回合的点);
|
|
428
|
+
* ③ 没有 sessionId → `noSession`;
|
|
429
|
+
* ④ 收尾状态 unknown → `turnUnknown`(不假装已收尾);
|
|
430
|
+
* ⑤ 列表还没读回来:
|
|
431
|
+
* · 惰取在途(`pending`)或**重试窗口还开着**(`retrying`)→ `pointsPending`;
|
|
432
|
+
* · 惰取**失败**(读不出来)且窗口已关 → **乐观可点**(点开走宿主错误路径,界面不撒谎说「没有点」);
|
|
433
|
+
* ⑥ 已读到列表:命中 → enabled;没命中 → 重试窗口内 `pointsPending`、窗口关后 `noPoint`
|
|
434
|
+
* (R1:引擎在收尾之后还要几秒才写完点,窗口内的空列表是暂态;把暂态说成「没有」就是不诚实)。
|
|
435
|
+
*
|
|
436
|
+
* `pending` 与 `retrying` 的关系:前者是「这一次读在途」,后者是「窗口还开着」(含首读在途、含读到空
|
|
437
|
+
* 还会再读、含读失败还会再读)。两个都表示**还没结论**,所以两个都归 `pointsPending`;
|
|
438
|
+
* 调用点通常只需给 `retrying`(窗口状态机是唯一真相),`pending` 留给不接状态机的旧调用点。
|
|
439
|
+
*/
|
|
440
|
+
export function chatEntryButtonState(options: {
|
|
441
|
+
node: unknown
|
|
442
|
+
sessionId: string | null | undefined
|
|
443
|
+
lookup: ChatPointLookup | null | undefined
|
|
444
|
+
pending: boolean
|
|
445
|
+
/**
|
|
446
|
+
* 收尾后的有界重试窗口还开着(R1/R9):**还没拿到结论**(首读在途 / 读到空还会再读 / 读失败还会再读)
|
|
447
|
+
* ⇒ `pointsPending`。缺省 false = 旧调用点的行为逐字不变。
|
|
448
|
+
*/
|
|
449
|
+
retrying?: boolean
|
|
450
|
+
}): ChatEntryButton {
|
|
451
|
+
const turn = chatNodeTurn(options.node)
|
|
452
|
+
if (turn === null) return { kind: 'disabled', code: 'noTurn', key: CHAT_ENTRY_DISABLED_KEYS.noTurn }
|
|
453
|
+
const phase = chatEntryTurnPhase(turn)
|
|
454
|
+
if (phase === 'open') return { kind: 'disabled', code: 'turnOpen', key: CHAT_ENTRY_DISABLED_KEYS.turnOpen }
|
|
455
|
+
if (typeof options.sessionId !== 'string' || options.sessionId === '') {
|
|
456
|
+
return { kind: 'disabled', code: 'noSession', key: CHAT_ENTRY_DISABLED_KEYS.noSession }
|
|
457
|
+
}
|
|
458
|
+
if (phase === 'unknown') return { kind: 'disabled', code: 'turnUnknown', key: CHAT_ENTRY_DISABLED_KEYS.turnUnknown }
|
|
459
|
+
const lookup = options.lookup
|
|
460
|
+
if (lookup === null || lookup === undefined || lookup.resolved !== true) {
|
|
461
|
+
// 「这一次读在途」与「窗口还开着」都是「还没结论」—— 都不许说成「没有回滚点」,也都不该乐观放行
|
|
462
|
+
if (options.pending || options.retrying === true) {
|
|
463
|
+
return { kind: 'disabled', code: 'pointsPending', key: CHAT_ENTRY_DISABLED_KEYS.pointsPending }
|
|
464
|
+
}
|
|
465
|
+
return { kind: 'enabled', turn: turn.turn }
|
|
466
|
+
}
|
|
467
|
+
if (lookup.point === null) {
|
|
468
|
+
if (options.retrying === true) return { kind: 'disabled', code: 'pointsPending', key: CHAT_ENTRY_DISABLED_KEYS.pointsPending }
|
|
469
|
+
return { kind: 'disabled', code: 'noPoint', key: CHAT_ENTRY_DISABLED_KEYS.noPoint }
|
|
470
|
+
}
|
|
471
|
+
return { kind: 'enabled', turn: turn.turn }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* ------------------------------------------------------------ 收尾竞态:有界重试(R1) */
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 重试窗口长度:**30 s**(毫秒)。判据是「从挂载/收尾那一刻起累计的墙上时间」,不是尝试次数。
|
|
478
|
+
*
|
|
479
|
+
* 为什么是 30 s:真机实测引擎在**回合收尾之后 7.8 s** 才写完这个回合的点
|
|
480
|
+
* (turn/end 20:03:38.658Z → 点 createdAt 20:03:46.434Z)。30 s 给足「慢机器 / 大盘 355 文件」的余量,
|
|
481
|
+
* 同时是一个**有界**的等待:用户最多等 30 s 就能拿到一个诚实的结论(命中,或「没有回滚点」),
|
|
482
|
+
* 而不是被一次空结果永久禁用。窗口内最多 6 次列表读取(1 次首读 + 5 次重试),**不是轮询**。
|
|
483
|
+
*/
|
|
484
|
+
export const CHAT_POINT_LOOKUP_WINDOW_MS = 30_000
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 固定退避表(毫秒)—— 每次「没命中」之后等这么久再读下一轮,**表走完就停**。
|
|
488
|
+
*
|
|
489
|
+
* 0.5 / 1.5 / 3 / 6 / 12 s ⇒ 累计 23 s(< 30 s 窗口),覆盖真机 7.8 s 的落点,晚到的写回也抓得住。
|
|
490
|
+
* 选固定表而不是纯指数:间隔可被单测逐项断言 —— 「有界重试」的判据必须是可核对的数字。
|
|
491
|
+
*/
|
|
492
|
+
export const CHAT_POINT_RETRY_DELAYS_MS: readonly number[] = [500, 1_500, 3_000, 6_000, 12_000]
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 这一轮读完之后**还该不该再读**(纯判定,**不看时钟**,只吃结果)。
|
|
496
|
+
*
|
|
497
|
+
* 命中 → 立即 false(命中即停:不再排重试、不再发请求,避免无谓 churn);
|
|
498
|
+
* 未命中(读到空列表 = 暂态,或读失败 = 判据未知)→ true,由调用点按退避表排下一轮。
|
|
499
|
+
* 这里刻意**不**把「已读到空列表」当成长期真值 —— 那正是 R1 要修的那个 bug 的根。
|
|
500
|
+
*/
|
|
501
|
+
export function shouldRetryChatPoint(lookup: ChatPointLookup | null | undefined): boolean {
|
|
502
|
+
if (lookup === null || lookup === undefined) return true
|
|
503
|
+
return lookup.point === null
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/* 重试循环(`retryChatPointUntilHit`,含取消钩子)与「一个挂载只跑一轮窗口」的状态机
|
|
507
|
+
(`ChatPointWindowController`)都住在 `chat-point-window.ts`(T6 起):那里是**一次性实例**语义
|
|
508
|
+
(start 幂等),所以 effect 重跑不会再开第二轮窗口。这里全额 re-export,调用点与单测的 import 面不变。 */
|
|
509
|
+
export { retryChatPointUntilHit, ChatPointWindowController, type ChatPointWindowState } from './chat-point-window.ts'
|
|
510
|
+
|
|
511
|
+
/* ------------------------------------------------------------ 整视图模型 + 错误边界 */
|
|
512
|
+
|
|
513
|
+
export interface ChatEntryView {
|
|
514
|
+
/** 取到的回合号(渲染/打开对话框都只用它;null = 未识别宿主版本)。 */
|
|
515
|
+
turnNumber: number | null
|
|
516
|
+
turnStatus: string | null
|
|
517
|
+
bubble: ChatEntryBubbleView
|
|
518
|
+
/** 动作行的时间文本(null = 节点没给可用的时间戳)。 */
|
|
519
|
+
clock: string | null
|
|
520
|
+
/** 宿主 data 的另外两个字段(我们**不**接线到 ui-primitives,渲染层如实记成差距)。 */
|
|
521
|
+
referenceLabels: string[]
|
|
522
|
+
skillNames: string[]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** 节点 → 整视图模型(读不到一律回安全值;本函数不抛)。 */
|
|
526
|
+
export function buildChatEntryView(node: unknown): ChatEntryView {
|
|
527
|
+
const data = readChatEntryNodeData(node)
|
|
528
|
+
const turn = chatNodeTurn(node)
|
|
529
|
+
return {
|
|
530
|
+
turnNumber: turn?.turn ?? null,
|
|
531
|
+
turnStatus: turn?.status ?? null,
|
|
532
|
+
bubble: buildChatEntryBubble(data.content),
|
|
533
|
+
clock: formatChatEntryClock(data.time),
|
|
534
|
+
referenceLabels: data.referenceLabels ?? [],
|
|
535
|
+
skillNames: data.skillNames ?? [],
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** 错误边界降级后**仍然可见**的那点东西(用户消息绝不消失)。 */
|
|
540
|
+
export interface ChatEntryFallbackRow {
|
|
541
|
+
text: string
|
|
542
|
+
fileNames: string[]
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* 构造错误边界的降级输入(纯函数,可单测)。
|
|
547
|
+
*
|
|
548
|
+
* 为什么参数里要 `project` 而不是直接吃节点:错误边界要做的事就是「让任意一段子渲染(包括
|
|
549
|
+
* 我们自己的 normalizer)抛错时也不整条消失」,所以这里把「投影」抽成可注入的一步 ——
|
|
550
|
+
* 单测直接传一个必然抛错的投影就复现了真实崩溃路径。
|
|
551
|
+
*/
|
|
552
|
+
export function buildChatEntrySafeRow(options: {
|
|
553
|
+
node: unknown
|
|
554
|
+
project: (node: unknown) => ChatEntryView
|
|
555
|
+
}): ChatEntryFallbackRow {
|
|
556
|
+
try {
|
|
557
|
+
const view = options.project(options.node)
|
|
558
|
+
return {
|
|
559
|
+
text: view.bubble.text !== '' ? view.bubble.text : view.bubble.raw.map((block) => rawBlockText(block.payload)).join('\n'),
|
|
560
|
+
fileNames: view.bubble.files.map((file) => file.name).filter((name) => name !== ''),
|
|
561
|
+
}
|
|
562
|
+
} catch {
|
|
563
|
+
const data = readChatEntryNodeData(options.node)
|
|
564
|
+
return { text: plainTextOf(data.content), fileNames: [] }
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 最朴素的纯文本投影(**不抛**):只认 `type === 'text'` 与裸字符串,
|
|
570
|
+
* 其余块用 JSON 短表示 —— 用于「我们的投影自己崩了」时的最后一道兜底(绝不整条消失)。
|
|
571
|
+
*/
|
|
572
|
+
export function plainTextOf(content: unknown): string {
|
|
573
|
+
if (typeof content === 'string') return content
|
|
574
|
+
if (!Array.isArray(content)) return content === null || content === undefined ? '' : rawBlockText(content)
|
|
575
|
+
return content.map((block) => {
|
|
576
|
+
const record = asRecord(block)
|
|
577
|
+
if (record === null) return rawBlockText(block)
|
|
578
|
+
if (record['type'] === 'text' && typeof record['text'] === 'string') return projectChatEntryText(record['text'])
|
|
579
|
+
// 文件块:降级气泡里至少要看见**文件名**(比一串 JSON 有用得多)
|
|
580
|
+
if (record['type'] === 'file') {
|
|
581
|
+
const name = asRecord(record['attachment'])?.['name']
|
|
582
|
+
if (typeof name === 'string' && name !== '') return name
|
|
583
|
+
}
|
|
584
|
+
// 图片块:没有可供纯文本使用的信息(附件 id 对人无意义),用块类型占位而不是倒 JSON
|
|
585
|
+
if (record['type'] === 'image') return record['type'] as string
|
|
586
|
+
return rawBlockText(block)
|
|
587
|
+
}).join('\n')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** 兜底块 → 文本(JSON 序列化失败时回 String();**不抛**)。 */
|
|
591
|
+
export function rawBlockText(payload: unknown): string {
|
|
592
|
+
try {
|
|
593
|
+
const text = JSON.stringify(payload)
|
|
594
|
+
return text === undefined ? String(payload) : text
|
|
595
|
+
} catch {
|
|
596
|
+
return String(payload)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** 错误边界的 ASCII 诊断前缀(console.* 实参不触发 i18n 守卫;界面文案一律在字典里)。 */
|
|
601
|
+
export const CHAT_ENTRY_WARN_PREFIX = '[config-manager] chat user node renderer degraded to plain text: '
|
|
602
|
+
|
|
603
|
+
/* ------------------------------------------------------------ 回滚点惰取(按会话缓存 + 在途去重) */
|
|
604
|
+
|
|
605
|
+
/** 缓存 TTL:同一会话里的所有按钮共用一次列表读取;跨分钟自然过期(列表变化由宿主写,非热点)。 */
|
|
606
|
+
export const CHAT_POINT_CACHE_MS = 30_000
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* 宿主列表载荷里本模块真正用到的三件事(**宽度接收**:多出的字段逐字段判定,读不到就按「没带」处理)。
|
|
610
|
+
*
|
|
611
|
+
* 为什么要宽度接收 `inheritLinks`:它是 t9 新增的**继承链**(fork 子会话 id → 父会话 id),
|
|
612
|
+
* 由引擎 `list()` 回传;`src/ui/rollback-view.ts` 的 `RollbackListResult` 类型不在 t9 scope,
|
|
613
|
+
* 所以这里不 import 一个还不存在的字段,而是像读宿主 node 形状那样**逐字段判定**:
|
|
614
|
+
* 没带(旧宿主)⇒ 行为与今天逐字一致,绝不猜。
|
|
615
|
+
*/
|
|
616
|
+
interface ChatPointListFacts {
|
|
617
|
+
points: readonly RollbackPointSummary[]
|
|
618
|
+
/** fork 子会话 id → 父会话 id;undefined = 载荷没带这张表(旧宿主) */
|
|
619
|
+
links: Record<string, string> | undefined
|
|
620
|
+
/** 链是否完整;undefined = 未声明(按「完整」处理) */
|
|
621
|
+
linksComplete: boolean | undefined
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
interface ChatPointListCacheEntry {
|
|
625
|
+
at: number
|
|
626
|
+
facts: ChatPointListFacts
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 继承链上溯的**深度上限**(与宿主侧 `ANCESTOR_LOOKUP_MAX_DEPTH` 同口径取 8:DSH 的子代理/分叉树
|
|
631
|
+
* 实际深度是个位数)。只上溯 + 环保护 + 上限三道一起,保证异常数据(自指 / 环 / 超深链)
|
|
632
|
+
* 既不挂起、也不误命中。
|
|
633
|
+
*/
|
|
634
|
+
export const CHAT_POINT_ANCESTOR_MAX_DEPTH = 8
|
|
635
|
+
|
|
636
|
+
/** 载荷 → 本模块用到的三件事(形状不符一律降级为「没带」,绝不抛)。 */
|
|
637
|
+
function readChatPointListFacts(raw: unknown): ChatPointListFacts {
|
|
638
|
+
const record = typeof raw === 'object' && raw !== null ? raw as Record<string, unknown> : null
|
|
639
|
+
const rawPoints = record?.['points']
|
|
640
|
+
const points = Array.isArray(rawPoints) ? rawPoints as readonly RollbackPointSummary[] : []
|
|
641
|
+
const rawLinks = record?.['inheritLinks']
|
|
642
|
+
let links: Record<string, string> | undefined
|
|
643
|
+
if (typeof rawLinks === 'object' && rawLinks !== null && !Array.isArray(rawLinks)) {
|
|
644
|
+
const out: Record<string, string> = {}
|
|
645
|
+
for (const [child, parent] of Object.entries(rawLinks as Record<string, unknown>)) {
|
|
646
|
+
if (child !== '' && typeof parent === 'string' && parent !== '') out[child] = parent
|
|
647
|
+
}
|
|
648
|
+
links = out
|
|
649
|
+
}
|
|
650
|
+
const rawComplete = record?.['inheritLinksComplete']
|
|
651
|
+
return { points, links, linksComplete: typeof rawComplete === 'boolean' ? rawComplete : undefined }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 本会话/本回合未命中时,沿 `parentSession` 链**只上溯**找祖先的同回合点(t9)。
|
|
656
|
+
*
|
|
657
|
+
* 语义(与引擎侧逐条对齐):
|
|
658
|
+
* · 只对「历史被继承」的 fork 子会话建链(引擎只登记这种),子代理会话不在链里
|
|
659
|
+
* —— 它们的回合号是**自己的**,按同回合上溯会误命中;
|
|
660
|
+
* · 命中即返回**祖先自己的点**(`sessionId` 是祖先):回退动作仍在原会话上做 fork,
|
|
661
|
+
* 与今天的语义逐字相同(`boundarySeq` 也来自祖先点);
|
|
662
|
+
* · 环 / 超深链 / 链被截断 ⇒ `proven:false`(判据**未知**),调用点据此报 `resolved:false`
|
|
663
|
+
* —— 不得把「看不完」说成「确知没有」。
|
|
664
|
+
*/
|
|
665
|
+
function findPointThroughAncestors(
|
|
666
|
+
facts: ChatPointListFacts,
|
|
667
|
+
sessionId: string,
|
|
668
|
+
turn: number,
|
|
669
|
+
): { point: RollbackPointSummary | null; proven: boolean } {
|
|
670
|
+
const links = facts.links
|
|
671
|
+
// 旧宿主(载荷没有继承链):与今天逐字一致 —— 未命中就是确知没有
|
|
672
|
+
if (links === undefined) return { point: null, proven: true }
|
|
673
|
+
const seen = new Set<string>([sessionId])
|
|
674
|
+
let current = sessionId
|
|
675
|
+
for (let depth = 1; depth <= CHAT_POINT_ANCESTOR_MAX_DEPTH; depth += 1) {
|
|
676
|
+
const parent = links[current]
|
|
677
|
+
// 走到链顶:只有「链完整」时才算证明了「确实没有」(截断的链可能缺环)
|
|
678
|
+
if (parent === undefined) return { point: null, proven: facts.linksComplete !== false }
|
|
679
|
+
// 自指 / 环:数据不可信 ⇒ 判据未知(绝不挂起)
|
|
680
|
+
if (seen.has(parent)) return { point: null, proven: false }
|
|
681
|
+
seen.add(parent)
|
|
682
|
+
const hit = findPointForTurn(facts.points, parent, turn)
|
|
683
|
+
if (hit !== null) return { point: hit, proven: true }
|
|
684
|
+
current = parent
|
|
685
|
+
}
|
|
686
|
+
// 超过深度上限:看不完 ⇒ 不可证明(不误命中、也不挂起)
|
|
687
|
+
return { point: null, proven: false }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** 载荷 → 一次查询结论(自己的点优先;未命中才上溯)。 */
|
|
691
|
+
function lookupPointInFacts(facts: ChatPointListFacts, sessionId: string, turn: number): ChatPointLookup {
|
|
692
|
+
const own = findPointForTurn(facts.points, sessionId, turn)
|
|
693
|
+
if (own !== null) return { point: own, resolved: true }
|
|
694
|
+
const ancestor = findPointThroughAncestors(facts, sessionId, turn)
|
|
695
|
+
if (ancestor.point !== null) return { point: ancestor.point, resolved: true }
|
|
696
|
+
return { point: null, resolved: ancestor.proven }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** **按会话**缓存整份列表(不是按回合):一条长对话里 N 条用户消息只发 1 次请求。 */
|
|
700
|
+
const pointListCache = new Map<string, ChatPointListCacheEntry>()
|
|
701
|
+
const pointListInflight = new Map<string, Promise<ChatPointListFacts | null>>()
|
|
702
|
+
|
|
703
|
+
/** 清空缓存(单测用;生产代码不调用)。 */
|
|
704
|
+
export function resetChatPointCache(): void {
|
|
705
|
+
pointListCache.clear()
|
|
706
|
+
pointListInflight.clear()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* 读该会话的回滚点列表 → 匹配(**复用** `findPointForTurn`:同会话 + 同回合 + 优先 auto,单一实现)。
|
|
711
|
+
*
|
|
712
|
+
* 四条为什么:
|
|
713
|
+
* · **按会话缓存列表**:每条用户消息各挂一颗按钮,若按回合发请求,一条 50 轮的对话首屏就是 50 次
|
|
714
|
+
* 列表读取;这里整份列表按会话缓存 + 在途去重,N 条消息 → 1 次请求;
|
|
715
|
+
* · **失败不缓存**(下次重试):读不出来是**判据未知**,不是「确知没有点」—— 如实回 `resolved:false`,
|
|
716
|
+
* 调用点据此保持乐观可点,让用户在对话框里看到宿主的真实错误,而不是一句假的「没有回滚点」;
|
|
717
|
+
* · **匹配不在本层做缓存**(列表缓存了,匹配是纯函数、代价可忽略),所以列表一更新就立刻生效;
|
|
718
|
+
* · `force`(R1 新增)= **空结果不锁死**的唯一通道:跳过那次缓存读,去宿主再读一遍并**写回**新结果。
|
|
719
|
+
* 重试轮必须用它 —— 否则那 30 s TTL 会把「收尾时读到的一次空列表」锁成这 30 s 的**假事实**,
|
|
720
|
+
* 而点其实在几秒后就被引擎写出来了。注意 force **不**破坏在途去重:同会话已有读取在途时仍复用它
|
|
721
|
+
* (那是同一次真读,不是缓存),所以 N 条消息一起重试时仍然只发 1 个请求。
|
|
722
|
+
*/
|
|
723
|
+
export async function lookupPointForTurn(options: {
|
|
724
|
+
port: RollbackPointPort
|
|
725
|
+
sessionId: string
|
|
726
|
+
turn: number
|
|
727
|
+
now?: number
|
|
728
|
+
cacheMs?: number
|
|
729
|
+
/** true = 跳过缓存读(重试轮用);读回来的结果照常写回缓存。 */
|
|
730
|
+
force?: boolean
|
|
731
|
+
}): Promise<ChatPointLookup> {
|
|
732
|
+
const now = options.now ?? Date.now()
|
|
733
|
+
const ttl = options.cacheMs ?? CHAT_POINT_CACHE_MS
|
|
734
|
+
const cached = options.force === true ? undefined : pointListCache.get(options.sessionId)
|
|
735
|
+
if (cached !== undefined && now - cached.at < ttl) {
|
|
736
|
+
return lookupPointInFacts(cached.facts, options.sessionId, options.turn)
|
|
737
|
+
}
|
|
738
|
+
let inflight = pointListInflight.get(options.sessionId)
|
|
739
|
+
if (inflight === undefined) {
|
|
740
|
+
inflight = (async (): Promise<ChatPointListFacts | null> => {
|
|
741
|
+
try {
|
|
742
|
+
const list = await options.port.rollbackPoints()
|
|
743
|
+
// 宽度接收:points 之外的继承链字段由 readChatPointListFacts 逐字段判定(读不到 = 没带)
|
|
744
|
+
const facts = readChatPointListFacts(list as unknown)
|
|
745
|
+
pointListCache.set(options.sessionId, { at: Date.now(), facts })
|
|
746
|
+
return facts
|
|
747
|
+
} catch {
|
|
748
|
+
return null
|
|
749
|
+
} finally {
|
|
750
|
+
pointListInflight.delete(options.sessionId)
|
|
751
|
+
}
|
|
752
|
+
})()
|
|
753
|
+
pointListInflight.set(options.sessionId, inflight)
|
|
754
|
+
}
|
|
755
|
+
const facts = await inflight
|
|
756
|
+
if (facts === null) return { point: null, resolved: false }
|
|
757
|
+
return lookupPointInFacts(facts, options.sessionId, options.turn)
|
|
758
|
+
}
|