dsh-config-manager 0.1.78 → 0.1.80
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +401 -112
- package/lib/client.js +2899 -1525
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +9 -0
- package/lib/ui/i18n.js +22 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/market/market-api.test.ts +3 -2
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +220 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +248 -0
- package/src/client/sync/merge-view.ts +398 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.test.ts +5 -3
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/ancestor.ts +76 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +22 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
|
@@ -43,10 +43,16 @@ export interface ArtifactRowViewProps {
|
|
|
43
43
|
onAction: (capability: ArtifactCapability, row: ArtifactRow) => void
|
|
44
44
|
/** 该行有动作在跑:禁用行内按钮(防重复提交) */
|
|
45
45
|
busy?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* cloud-sync 借鉴项 ⑥:该行远端快照的**推送方设备**文案(如「由 <设备名> 推送」)。
|
|
48
|
+
* 只有快照 manifest 真的带 deviceName 时才由调用方给出 —— 缺省 undefined 表示**不渲染**
|
|
49
|
+
* (旧快照没有该字段,绝不显示空串或占位)。
|
|
50
|
+
*/
|
|
51
|
+
deviceLabel?: string | undefined
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export function ArtifactRowView({
|
|
49
|
-
row, t, uiT, formatTime, expanded, onToggle, onAction, busy = false,
|
|
55
|
+
row, t, uiT, formatTime, expanded, onToggle, onAction, busy = false, deviceLabel,
|
|
50
56
|
}: ArtifactRowViewProps) {
|
|
51
57
|
const detailId = useId()
|
|
52
58
|
const primary = row.capabilities[0]
|
|
@@ -77,7 +83,11 @@ export function ArtifactRowView({
|
|
|
77
83
|
</div>
|
|
78
84
|
{/* 元数据是**自由序列**而不是表格列:某一段没有(如快照没有体积)就整段不出现,
|
|
79
85
|
绝不补一个「—」占位 —— 这里没有对齐问题,占位只会变成噪音(§3 第三条) */}
|
|
80
|
-
{row.meta.length > 0
|
|
86
|
+
{(row.meta.length > 0 || deviceLabel !== undefined) && (
|
|
87
|
+
<div className={css.artifactMeta}>
|
|
88
|
+
{[...row.meta, ...(deviceLabel === undefined ? [] : [deviceLabel])].join(' · ')}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
81
91
|
</div>
|
|
82
92
|
{row.badges.length > 0 && (
|
|
83
93
|
<div className={css.artifactBadges}>
|
|
@@ -24,6 +24,7 @@ import type { SyncApi } from '../sync/sync-api.ts'
|
|
|
24
24
|
import { formatDateTimeFull } from '../sync/history-model.ts'
|
|
25
25
|
import type { TranslateNS } from '../client-types.ts'
|
|
26
26
|
import { Banner, Button, Empty, Spinner } from '../common/ui.tsx'
|
|
27
|
+
import { Skeleton, SkeletonList, SkeletonTable } from '../common/Skeleton.tsx'
|
|
27
28
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
28
29
|
import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
29
30
|
import { Modal } from '../common/Modal.tsx'
|
|
@@ -380,7 +381,8 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
380
381
|
{consult !== null && (
|
|
381
382
|
<>
|
|
382
383
|
<div className={css.hint}>{fileNameOf(consult.row)}</div>
|
|
383
|
-
{
|
|
384
|
+
{/* 迁移前咨询:正文块(多段报告)→ 按行铺骨架 */}
|
|
385
|
+
{consult.loading && <Skeleton shape="line" count={5} label={api.t('consult.loading')} />}
|
|
384
386
|
{consult.error !== null && <Banner kind="error">{consult.error}</Banner>}
|
|
385
387
|
{consult.report !== null && <ConsultCard report={consult.report} t={api.t} />}
|
|
386
388
|
</>
|
|
@@ -441,7 +443,8 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
441
443
|
trailing={<InfoHint text={t('snapshots.selectHint')} label={t('common.infoHint')} />}
|
|
442
444
|
/>
|
|
443
445
|
<Modal.Body scroll>
|
|
444
|
-
{
|
|
446
|
+
{/* 恢复计划预览:结果是一张「文件 → 动作」表 → 按表格铺骨架 */}
|
|
447
|
+
{plan?.loading === true && <SkeletonTable rows={6} label={t('common.loading')} />}
|
|
445
448
|
{plan?.error !== null && plan?.error !== undefined && <Banner kind="error">{plan.error}</Banner>}
|
|
446
449
|
{plan?.plan != null && plan.plan.actions.length === 0 && <Empty>{t('snapshots.noActions')}</Empty>}
|
|
447
450
|
{plan?.plan != null && plan.plan.actions.length > 0 && (
|
|
@@ -511,7 +514,7 @@ export function LibraryActions({ api, syncApi, t, target, onDone, onChanged, onO
|
|
|
511
514
|
<Modal.Header title={t('backupFiles.inspect')} closeLabel={t('common.close')} onClose={() => { setInspect(null) }} />
|
|
512
515
|
<Modal.Body scroll>
|
|
513
516
|
{inspect !== null && <div className={css.hint} data-testid="inspect-backup-name">{inspect.name}</div>}
|
|
514
|
-
{inspect?.loading === true && <
|
|
517
|
+
{inspect?.loading === true && <SkeletonList count={3} label={t('backupFiles.inspectLoading')} />}
|
|
515
518
|
{inspect?.error != null && <Banner kind="error">{inspect.error}</Banner>}
|
|
516
519
|
{inspect !== null && !inspect.loading && inspect.result === null && inspect.error === null && <Empty>{t('backupFiles.inspectEmpty')}</Empty>}
|
|
517
520
|
{inspect?.result != null && <BackupInspectView result={inspect.result} t={t} />}
|
|
@@ -21,8 +21,10 @@ import type { SyncApi, SyncSnapshotLite } from '../sync/sync-api.ts'
|
|
|
21
21
|
import type { MarketApi } from '../market/market-api.ts'
|
|
22
22
|
import type { TranslateNS } from '../client-types.ts'
|
|
23
23
|
import { formatDateTimeFull } from '../sync/history-model.ts'
|
|
24
|
+
import { deviceLabelsById } from '../sync/remote-device-view.ts'
|
|
24
25
|
import { runStore, type LibraryStoreSlice } from '../run-store.ts'
|
|
25
|
-
import { Badge, Button, Card, Empty
|
|
26
|
+
import { Badge, Button, Card, Empty } from '../common/ui.tsx'
|
|
27
|
+
import { SkeletonRows } from '../common/Skeleton.tsx'
|
|
26
28
|
import { redact } from '../../security/redaction.ts'
|
|
27
29
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
28
30
|
import { ArtifactRowView } from './ArtifactRow.tsx'
|
|
@@ -139,6 +141,19 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
139
141
|
() => filterArtifacts(rows, { kind: lib.sourceFilter, text: lib.query, sort: lib.sort }),
|
|
140
142
|
[rows, lib.sourceFilter, lib.query, lib.sort],
|
|
141
143
|
)
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* cloud-sync 借鉴项 ⑥:远端快照 id → 「由 <设备名> 推送」文案。
|
|
147
|
+
* **只有**快照 manifest 真的带 deviceName 的行才进表:旧快照没有该字段,界面就不显示设备行
|
|
148
|
+
* (绝不渲染空串占位,也绝不把 undefined 变成文案)。
|
|
149
|
+
*/
|
|
150
|
+
const deviceLabelOf = useMemo(
|
|
151
|
+
() => deviceLabelsById(
|
|
152
|
+
remote.status === 'ready' ? remote.items : [],
|
|
153
|
+
(device) => t('library.remoteDevice', { device }),
|
|
154
|
+
),
|
|
155
|
+
[remote, t],
|
|
156
|
+
)
|
|
142
157
|
const counts = countByKind(rows)
|
|
143
158
|
const failedSources = [snapshots, files, remote, market].filter((s) => s.status === 'error').length
|
|
144
159
|
|
|
@@ -216,8 +231,10 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
216
231
|
{/* 还有来源在路上(本机两源已就绪但为空)时也走加载态:绝不先给「暂无产物」的结论。
|
|
217
232
|
与空态共用同一个 .libraryListState 契约(守卫要求恰好两处:加载态 + 空态)。 */}
|
|
218
233
|
{(loading || (anySourcePending && filtered.length === 0)) && (
|
|
219
|
-
<div className={css.libraryListState}>
|
|
220
|
-
|
|
234
|
+
<div className={css.libraryListState + ' ' + css.librarySkeletonRegion}>
|
|
235
|
+
{/* 骨架:行数按一屏可见行给(8 行 ≈ 564×720 画布的列表区),行高与 .artifactRowHead 对齐,
|
|
236
|
+
数据到货时列表从同一位置长出来,不再「空页面 + 居中转圈」整块跳变。 */}
|
|
237
|
+
<SkeletonRows count={8} label={t('common.loading')} />
|
|
221
238
|
</div>
|
|
222
239
|
)}
|
|
223
240
|
|
|
@@ -246,6 +263,7 @@ export function LibraryPanel({ api, syncApi, marketApi, t, uiT, onAction, onOpen
|
|
|
246
263
|
runStore.patch({ library: { expandedKey: lib.expandedKey === row.key ? null : row.key } })
|
|
247
264
|
}}
|
|
248
265
|
onAction={onAction}
|
|
266
|
+
deviceLabel={row.kind === 'remote-snapshot' ? deviceLabelOf.get(row.ref.remoteId ?? '') : undefined}
|
|
249
267
|
/>
|
|
250
268
|
))}
|
|
251
269
|
</ul>
|
|
@@ -19,7 +19,8 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
19
19
|
import { toRestorePlanView, withLoadedStatus, type RestorePlanRow } from '../../ui/restore-plan-view.ts'
|
|
20
20
|
import { DiffTable } from '../common/DiffTable.tsx'
|
|
21
21
|
import { formatBytes } from '../../ui/report.ts'
|
|
22
|
-
import { Badge, Banner
|
|
22
|
+
import { Badge, Banner } from '../common/ui.tsx'
|
|
23
|
+
import { SkeletonTable } from '../common/Skeleton.tsx'
|
|
23
24
|
import { ExpandChevron } from '../common/Icon.tsx'
|
|
24
25
|
import { Collapse } from '../common/Motion.tsx'
|
|
25
26
|
import css from '../config-manager.module.css'
|
|
@@ -104,7 +105,8 @@ function reasonLabel(t: TranslateNS<'config-manager'>, reason: SnapshotFileDiffR
|
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
function DiffPane({ state, t }: { state: DiffState; t: TranslateNS<'config-manager'> }) {
|
|
107
|
-
|
|
108
|
+
// 差异是「逐行文档」形状:加载时按表格铺骨架,与 DiffTable 的行高同一节奏
|
|
109
|
+
if (state.status === 'loading') return <SkeletonTable rows={6} label={t('snapshots.plan.loadingDiff')} />
|
|
108
110
|
if (state.status === 'error') return <Banner kind="error">{redact(state.message)}</Banner>
|
|
109
111
|
const diff = state.diff
|
|
110
112
|
if (diff.reason !== undefined) return <div className={css.hint}>{reasonLabel(t, diff.reason, diff)}</div>
|
package/src/client/locales.ts
CHANGED
|
@@ -827,6 +827,8 @@ export const zh = {
|
|
|
827
827
|
'library.kind.snapshot': '本机快照',
|
|
828
828
|
'library.kind.backupFile': '备份文件',
|
|
829
829
|
'library.kind.remote': '远端快照',
|
|
830
|
+
// cloud-sync 借鉴项 ⑥:快照的推送方设备(仅当快照 manifest 带 deviceName 时才渲染;旧快照不显示占位)
|
|
831
|
+
'library.remoteDevice': '由 {device} 推送',
|
|
830
832
|
'library.kind.market': '市场配置',
|
|
831
833
|
'library.title': '产物库',
|
|
832
834
|
'library.refresh': '刷新',
|
|
@@ -1769,6 +1771,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1769
1771
|
'library.kind.snapshot': 'Local snapshot',
|
|
1770
1772
|
'library.kind.backupFile': 'Backup file',
|
|
1771
1773
|
'library.kind.remote': 'Remote snapshot',
|
|
1774
|
+
'library.remoteDevice': 'Pushed by {device}',
|
|
1772
1775
|
'library.kind.market': 'Market config',
|
|
1773
1776
|
'library.title': 'Artifact library',
|
|
1774
1777
|
'library.refresh': 'Refresh',
|
|
@@ -181,12 +181,13 @@ test('M-07 错误映射:4xx 携带 error → ConfigManagerApiError', async ()
|
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test('M-08
|
|
184
|
+
test('M-08 接口路径未命中:404(非 JSON 体)→ 提示请求前缀而不是「插件未挂载」(issue #108)', async () => {
|
|
185
185
|
installFetchMock(() => new Response('Not Found', { status: 404 }));
|
|
186
186
|
const api = new MarketApi(zhUiT);
|
|
187
187
|
await assert.rejects(api.status(), (err: unknown) => {
|
|
188
188
|
assert.ok(err instanceof ConfigManagerApiError);
|
|
189
|
-
assert.match(err.message,
|
|
189
|
+
assert.match(err.message, /接口路径未命中/);
|
|
190
|
+
assert.doesNotMatch(err.message, /未挂载/);
|
|
190
191
|
return true;
|
|
191
192
|
});
|
|
192
193
|
});
|
|
@@ -25,7 +25,7 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
25
25
|
import { Badge, Banner, Button, Card, Empty, FoldToggle, IconButton, SectionTitle, Spinner, StatusDot } from '../common/ui.tsx'
|
|
26
26
|
import { DiskUsageCard } from '../environment/maintenance/DiskUsageCard.tsx'
|
|
27
27
|
import { RollbackCard } from './RollbackCard.tsx'
|
|
28
|
-
import { SkeletonList } from '../common/Skeleton.tsx'
|
|
28
|
+
import { SkeletonList, SkeletonTable } from '../common/Skeleton.tsx'
|
|
29
29
|
import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
|
|
30
30
|
import { Modal } from '../common/Modal.tsx'
|
|
31
31
|
import { Icon } from '../common/Icon.tsx'
|
|
@@ -76,7 +76,7 @@ import { runStore, type RecoveryStoreSlice } from '../run-store.ts'
|
|
|
76
76
|
import type { CrashReport, IncidentApi, RescueStatus } from './incident-api.ts'
|
|
77
77
|
import { IncompleteCopiesSection } from './IncompleteCopiesSection.tsx'
|
|
78
78
|
import {
|
|
79
|
-
isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, toRecoveryPreviewView,
|
|
79
|
+
isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, recoveryBlockReason, toRecoveryPreviewView,
|
|
80
80
|
toRecoveryView,
|
|
81
81
|
crashAdviceKey,
|
|
82
82
|
crashReasonKey,
|
|
@@ -874,7 +874,8 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
|
|
|
874
874
|
{phase === 'error' && <Banner kind="error">{error ?? t('common.unknownError')}</Banner>}
|
|
875
875
|
|
|
876
876
|
{/* 首次扫描(还没有任何结果):给出进行中反馈 —— 旧实现在这里只留一片空白 */}
|
|
877
|
-
{
|
|
877
|
+
{/* 首次扫描:按会话表的形状铺骨架(表格行),扫描结果到货时表格原地长出来 */}
|
|
878
|
+
{phase === 'loading' && response === null && <SkeletonTable rows={6} label={t('sessions.scanning')} />}
|
|
878
879
|
|
|
879
880
|
{phase === 'ready' && response !== null && (
|
|
880
881
|
<>
|
|
@@ -1486,6 +1487,14 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
|
|
|
1486
1487
|
const previewView = state.preview !== null ? toRecoveryPreviewView(state.preview) : null
|
|
1487
1488
|
/** 真有待处理事项(口径唯一来源 = 纯函数 recovery-view.ts 的 recoveryRequired):决定副标题与空态 */
|
|
1488
1489
|
const needsAttention = view?.recoveryRequired === true
|
|
1490
|
+
/**
|
|
1491
|
+
* 选中项是否属于「正在正常执行中」的操作。是 ⇒ 恢复/放弃都不该点:它还在跑,
|
|
1492
|
+
* quarantine 会把在跑的 journal 从 active/ 抽走。据此给出**原因**而不是灰按钮。
|
|
1493
|
+
*/
|
|
1494
|
+
const selectedLive = selected !== undefined
|
|
1495
|
+
&& (view?.activeOperations.some((op) => op.operationId === selected.operationId) ?? false)
|
|
1496
|
+
/** 选中项「执行恢复」不可用的原因(null = 可用)——见 recovery-view.ts recoveryBlockReason */
|
|
1497
|
+
const selectedBlockReason = selected !== undefined ? recoveryBlockReason(selected) : null
|
|
1489
1498
|
|
|
1490
1499
|
return (
|
|
1491
1500
|
<div className={css.viewBody}>
|
|
@@ -1514,9 +1523,15 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
|
|
|
1514
1523
|
)}
|
|
1515
1524
|
|
|
1516
1525
|
{/* —— 状态与待处理事项(先异常、后诊断):三块诊断卡在下方,缺省只展开磁盘占用 —— */}
|
|
1517
|
-
{/* SAFE MODE / recovery-required
|
|
1526
|
+
{/* SAFE MODE / recovery-required 状态提示(正常态不渲染任何横幅——无事项即静默)。
|
|
1527
|
+
**文案必须与真实阻断态同源**:recoveryRequired 只是「有待处理事项」,不等于写操作已被
|
|
1528
|
+
暂停(残留锁 / 中断的档案复制都不会让 safeMode.blocked 为 true)。此前这里无条件说
|
|
1529
|
+
「已进入安全模式(会修改配置的操作已暂停)」⇒ 用户看到保护提示,实际上许多写操作照常
|
|
1530
|
+
成功,属于假警报(也让真正被阻断的场景失去可信度)。 */}
|
|
1518
1531
|
{view?.recoveryRequired === true && (
|
|
1519
|
-
<Banner kind="error">
|
|
1532
|
+
<Banner kind="error">
|
|
1533
|
+
{view.safeModeBlocked ? t('recovery.currentState.safeMode') : t('recovery.currentState.itemsOnly')}
|
|
1534
|
+
</Banner>
|
|
1520
1535
|
)}
|
|
1521
1536
|
|
|
1522
1537
|
{state.status === 'loading' && <SkeletonList label={t('recovery.loading')} />}
|
|
@@ -1564,6 +1579,26 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
|
|
|
1564
1579
|
</Card>
|
|
1565
1580
|
)}
|
|
1566
1581
|
|
|
1582
|
+
{/* 正在正常执行的操作(不是 incident):给一句「会自行结束」,避免用户以为面板出了故障。
|
|
1583
|
+
这一块存在的理由:此前 autosync push 在跑的 12 秒里,它被当成了待处理事故。 */}
|
|
1584
|
+
{state.status === 'ready' && (view?.activeOperations.length ?? 0) > 0 && (
|
|
1585
|
+
<Card className={css.card}>
|
|
1586
|
+
<div className={css.groupLabel}>{t('recovery.live.title')}</div>
|
|
1587
|
+
<div className={css.hint}>{t('recovery.live.hint')}</div>
|
|
1588
|
+
<div className={css.snapshotList}>
|
|
1589
|
+
{view!.activeOperations.map((op) => (
|
|
1590
|
+
<div key={op.operationId} className={css.snapshotRow}>
|
|
1591
|
+
<span className={css.snapshotRowMain} title={op.operationId}>
|
|
1592
|
+
<span>{op.operationType}</span>
|
|
1593
|
+
<span>{new Date(op.startedAt).toLocaleString()}</span>
|
|
1594
|
+
<span><Badge kind="info">{incidentStateLabel(t, op.state)}</Badge></span>
|
|
1595
|
+
</span>
|
|
1596
|
+
</div>
|
|
1597
|
+
))}
|
|
1598
|
+
</div>
|
|
1599
|
+
</Card>
|
|
1600
|
+
)}
|
|
1601
|
+
|
|
1567
1602
|
{state.status === 'ready' && (view?.incidents.length ?? 0) > 0 && (
|
|
1568
1603
|
<>
|
|
1569
1604
|
{/* 进行中 recovery run 提示 */}
|
|
@@ -1691,13 +1726,25 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
|
|
|
1691
1726
|
|
|
1692
1727
|
{state.actionError !== null && <Banner kind="error">{state.actionError}</Banner>}
|
|
1693
1728
|
|
|
1729
|
+
{/* 「执行恢复」不可用的原因:灰按钮不带解释 = 用户只能盲猜(这正是事故里的体验)。
|
|
1730
|
+
三种原因互斥且都能被代码逐条证明:操作仍在跑 / 无绑定快照 / 决策为需人工处理。 */}
|
|
1731
|
+
{selectedLive ? (
|
|
1732
|
+
<Banner kind="info">{t('recovery.incident.liveBlocked')}</Banner>
|
|
1733
|
+
) : state.verifyResult === null && selectedBlockReason !== null ? (
|
|
1734
|
+
<div className={css.hint}>
|
|
1735
|
+
{selectedBlockReason === 'noSnapshot'
|
|
1736
|
+
? t('recovery.incident.blockReason.noSnapshot')
|
|
1737
|
+
: t('recovery.incident.blockReason.needsAttention')}
|
|
1738
|
+
</div>
|
|
1739
|
+
) : null}
|
|
1740
|
+
|
|
1694
1741
|
{/* 动作区 */}
|
|
1695
1742
|
<div className={css.actionRow}>
|
|
1696
1743
|
{state.verifyResult === null && (
|
|
1697
1744
|
<>
|
|
1698
1745
|
<Button
|
|
1699
1746
|
variant="danger"
|
|
1700
|
-
disabled={state.running || !(previewView?.actionable ?? false)}
|
|
1747
|
+
disabled={state.running || selectedLive || !(previewView?.actionable ?? false)}
|
|
1701
1748
|
loading={state.running}
|
|
1702
1749
|
onClick={() => { setConfirmOpen(true) }}
|
|
1703
1750
|
>
|
|
@@ -1705,7 +1752,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
|
|
|
1705
1752
|
</Button>
|
|
1706
1753
|
<Button
|
|
1707
1754
|
variant="ghost"
|
|
1708
|
-
disabled={state.running}
|
|
1755
|
+
disabled={state.running || selectedLive}
|
|
1709
1756
|
onClick={() => { setDismissOpen(true) }}
|
|
1710
1757
|
>
|
|
1711
1758
|
{t('recovery.dismiss')}
|
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
} from '../../ui/rollback-view.ts'
|
|
58
58
|
import { redact } from '../../security/redaction.ts'
|
|
59
59
|
import { Badge, Banner, Button, Card, Checkbox, Empty, IconButton, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
60
|
+
import { SkeletonList } from '../common/Skeleton.tsx'
|
|
60
61
|
import { Icon } from '../common/Icon.tsx'
|
|
61
62
|
import { InfoHint } from '../common/InfoHint.tsx'
|
|
62
63
|
import { Modal } from '../common/Modal.tsx'
|
|
@@ -460,7 +461,7 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
|
|
|
460
461
|
</>
|
|
461
462
|
)}
|
|
462
463
|
|
|
463
|
-
{status === 'loading' && <
|
|
464
|
+
{status === 'loading' && <SkeletonList count={3} label={t('recovery.loading')} />}
|
|
464
465
|
{status === 'error' && (
|
|
465
466
|
<div className={css.actionRow}>
|
|
466
467
|
<Banner kind="error">{error !== null ? error : t('rollback.apiMissing')}</Banner>
|
|
@@ -544,7 +545,7 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
|
|
|
544
545
|
)}
|
|
545
546
|
/>
|
|
546
547
|
<Modal.Body scroll>
|
|
547
|
-
{status === 'loading' && <
|
|
548
|
+
{status === 'loading' && <SkeletonList count={3} label={t('recovery.loading')} />}
|
|
548
549
|
{status === 'error' && (
|
|
549
550
|
<div className={css.actionRow}>
|
|
550
551
|
<Banner kind="error">{error !== null ? error : t('rollback.apiMissing')}</Banner>
|
|
@@ -829,6 +830,10 @@ export function RollbackCard({ recoveryApi, t }: RollbackCardProps) {
|
|
|
829
830
|
{segment.unavailableCount > 0 && (
|
|
830
831
|
<span>{t('rollback.result.unavailable', { n: segment.unavailableCount })}</span>
|
|
831
832
|
)}
|
|
833
|
+
{segment.unclaimedCount > 0 && (
|
|
834
|
+
<span>{t('rollback.result.unclaimed', { n: segment.unclaimedCount })}</span>
|
|
835
|
+
)}
|
|
836
|
+
{segment.unclaimedPaths.map((line) => <span key={'unclaimed:' + line}>{line}</span>)}
|
|
832
837
|
{segment.concurrentCount > 0 && (
|
|
833
838
|
<span>{t('rollback.result.concurrent', { n: segment.concurrentCount })}</span>
|
|
834
839
|
)}
|
|
@@ -12,7 +12,8 @@ import type { TranslateNS } from '../client-types.ts'
|
|
|
12
12
|
import type { RollbackFileDiffResult } from '../../ui/rollback-view.ts'
|
|
13
13
|
import { rollbackFileDiffComparable, rollbackFileDiffReasonKey } from '../../ui/rollback-view.ts'
|
|
14
14
|
import { redact } from '../../security/redaction.ts'
|
|
15
|
-
import { Banner
|
|
15
|
+
import { Banner } from '../common/ui.tsx'
|
|
16
|
+
import { SkeletonTable } from '../common/Skeleton.tsx'
|
|
16
17
|
import { DiffTable } from '../common/DiffTable.tsx'
|
|
17
18
|
import css from '../config-manager.module.css'
|
|
18
19
|
|
|
@@ -48,7 +49,7 @@ export function RollbackDiffPane({ state, rt, title, fill }: RollbackDiffPanePro
|
|
|
48
49
|
*/
|
|
49
50
|
const shell = (body: ReactNode): ReactNode =>
|
|
50
51
|
fill === true ? <div className={css.diffPane + ' ' + css.diffPaneFill}>{body}</div> : body
|
|
51
|
-
if (state.status === 'loading') return shell(<
|
|
52
|
+
if (state.status === 'loading') return shell(<SkeletonTable rows={8} label={rt('rollback.diff.loading')} />)
|
|
52
53
|
if (state.status === 'error') return shell(<Banner kind="error">{state.message === null ? rt('rollback.diff.failed') : redact(state.message)}</Banner>)
|
|
53
54
|
const diff = state.diff
|
|
54
55
|
if (diff.ok !== true) {
|
|
@@ -23,7 +23,8 @@ export const zh = {
|
|
|
23
23
|
'recovery.banner': '配置修改已被保护:存在未完成的恢复事项。请先处理后继续。',
|
|
24
24
|
'recovery.bannerAction': '去处理',
|
|
25
25
|
'recovery.required': '需要恢复',
|
|
26
|
-
|
|
26
|
+
// 「有待处理事项」≠「写操作已被暂停」:后者只有 safeMode.blocked 才成立(横幅文案另有一对键)。
|
|
27
|
+
'recovery.requiredHint': '存在需要处理的恢复事项(残留锁、未完成的操作或中断的复制)。处理完这些事项即恢复正常。',
|
|
27
28
|
'recovery.rollbackRecommended': '建议回滚',
|
|
28
29
|
'recovery.rollbackContinue': '续跑回滚',
|
|
29
30
|
'recovery.inProgress': '恢复进行中',
|
|
@@ -54,6 +55,15 @@ export const zh = {
|
|
|
54
55
|
// 当前状态
|
|
55
56
|
'recovery.currentState.title': '当前状态',
|
|
56
57
|
'recovery.currentState.safeMode': '已进入安全模式(会修改配置的操作已暂停)',
|
|
58
|
+
// 有待处理事项、但 safeMode.blocked 为 false(残留锁 / 中断复制 / 旧宿主):不得声称写操作已被暂停
|
|
59
|
+
'recovery.currentState.itemsOnly': '存在需要处理的恢复事项(写操作未被暂停)',
|
|
60
|
+
// 正在正常执行的操作(不是 incident)
|
|
61
|
+
'recovery.live.title': '正在执行的操作',
|
|
62
|
+
'recovery.live.hint': '这些操作正在正常执行中,不是未完成的恢复事项;结束后会自动消失。',
|
|
63
|
+
// 「执行恢复 / 放弃恢复」不可用的原因(灰按钮必须能自证)
|
|
64
|
+
'recovery.incident.liveBlocked': '该操作仍在进行中,现在不能恢复也不能放弃,请等它结束。',
|
|
65
|
+
'recovery.incident.blockReason.noSnapshot': '本次操作没有绑定可信恢复快照,因此无法执行恢复。',
|
|
66
|
+
'recovery.incident.blockReason.needsAttention': '该事项被判定为需要人工处理,无法自动恢复。',
|
|
57
67
|
'recovery.currentState.hasSnapshot': '存在可信恢复快照',
|
|
58
68
|
'recovery.currentState.noSnapshot': '无可信恢复快照',
|
|
59
69
|
// 快照信息
|
|
@@ -450,6 +460,7 @@ export const zh = {
|
|
|
450
460
|
'rollback.result.conflict': '{n} 个文件两侧改到同一处,已保持原样 —— 需要你手工处理',
|
|
451
461
|
'rollback.result.unavailable': '{n} 个文件无法安全撤销(缺少中间版本 / 二进制 / 差异过大),已保持原样',
|
|
452
462
|
'rollback.result.concurrent': '{n} 个文件在回退过程中又被改过,已保持原样',
|
|
463
|
+
'rollback.result.unclaimed': '{n} 个文件无法判定属于哪个对话(改动时没有任何对话在运行),已保持原样',
|
|
453
464
|
'rollback.result.selectionSkipped': '{n} 个文件按你的勾选跳过了',
|
|
454
465
|
'rollback.preview.files.empty': '工作区与该回退点一致,无需恢复',
|
|
455
466
|
/* —— T15:**预览态**(点「回退」之前那一屏)的四态文案 ——
|
|
@@ -634,7 +645,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
634
645
|
'recovery.banner': 'Configuration changes are protected: there is an unfinished recovery item. Resolve it before continuing.',
|
|
635
646
|
'recovery.bannerAction': 'Go to recovery',
|
|
636
647
|
'recovery.required': 'Recovery Required',
|
|
637
|
-
'recovery.requiredHint': '
|
|
648
|
+
'recovery.requiredHint': 'Recovery item(s) need handling (a stale lock, an unfinished operation, or an interrupted copy). Resolve them to restore normal operation.',
|
|
638
649
|
'recovery.rollbackRecommended': 'Rollback recommended',
|
|
639
650
|
'recovery.rollbackContinue': 'Continue rollback',
|
|
640
651
|
'recovery.inProgress': 'Recovery in progress',
|
|
@@ -661,6 +672,12 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
661
672
|
'recovery.verify.verdict.unknown': 'Unknown',
|
|
662
673
|
'recovery.currentState.title': 'Current state',
|
|
663
674
|
'recovery.currentState.safeMode': 'Safe mode active (config-modifying operations paused)',
|
|
675
|
+
'recovery.currentState.itemsOnly': 'Recovery item(s) need handling (write operations are not paused)',
|
|
676
|
+
'recovery.live.title': 'Running operations',
|
|
677
|
+
'recovery.live.hint': 'These operations are running normally; they are not unfinished recovery items and will disappear when they finish.',
|
|
678
|
+
'recovery.incident.liveBlocked': 'This operation is still running, so it cannot be recovered or dismissed yet. Wait for it to finish.',
|
|
679
|
+
'recovery.incident.blockReason.noSnapshot': 'This operation has no trusted recovery snapshot bound to it, so recovery cannot run.',
|
|
680
|
+
'recovery.incident.blockReason.needsAttention': 'This item is classified as needing manual handling and cannot be recovered automatically.',
|
|
664
681
|
'recovery.currentState.hasSnapshot': 'Trusted recovery snapshot available',
|
|
665
682
|
'recovery.currentState.noSnapshot': 'No trusted recovery snapshot',
|
|
666
683
|
'recovery.snapshot.title': 'Recovery snapshot',
|
|
@@ -1033,6 +1050,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
1033
1050
|
'rollback.result.conflict': '{n} file(s) were changed on both sides in the same place and were left as they are — they need your decision',
|
|
1034
1051
|
'rollback.result.unavailable': '{n} file(s) could not be reverted safely (no common ancestor / binary / diff too large) and were left as they are',
|
|
1035
1052
|
'rollback.result.concurrent': '{n} file(s) changed again while this rewind was running and were left as they are',
|
|
1053
|
+
'rollback.result.unclaimed': '{n} file(s) could not be attributed to any conversation (nothing was running when they changed) and were left as they are',
|
|
1036
1054
|
'rollback.result.selectionSkipped': '{n} file(s) skipped by your selection',
|
|
1037
1055
|
'rollback.preview.files.empty': 'The workspace already matches this rollback point, nothing to restore',
|
|
1038
1056
|
'rollback.preview.files.unavailable': 'The workspace state of this rollback point has no usable record (reason: {reason}), nothing to restore',
|
|
@@ -9,7 +9,7 @@ import test from 'node:test';
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, sortIncidentsByNewest,
|
|
12
|
+
isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, recoveryBlockReason, sortIncidentsByNewest,
|
|
13
13
|
toRecoveryPreviewView, toRecoveryView, verdictToUiState,
|
|
14
14
|
} from './recovery-view.ts';
|
|
15
15
|
import type { RecoveryStatus } from '../../ui/types.ts';
|
|
@@ -220,3 +220,41 @@ test('sortIncidentsByNewest:按 createdAt 倒序', () => {
|
|
|
220
220
|
assert.equal(sorted[0]!.operationId, 'b');
|
|
221
221
|
assert.equal(sorted[1]!.operationId, 'a');
|
|
222
222
|
});
|
|
223
|
+
|
|
224
|
+
// ---------- 正在正常执行的操作(本轮修复:不再当成「未完成的恢复事项」) ----------
|
|
225
|
+
|
|
226
|
+
test('toRecoveryView:live[] 映射为 activeOperations,且不改变 state / recoveryRequired', () => {
|
|
227
|
+
const v = toRecoveryView(mkStatus({
|
|
228
|
+
live: [{
|
|
229
|
+
operationId: '00000000-0000-4000-8000-0000000000bb',
|
|
230
|
+
operationType: 'autosync-push',
|
|
231
|
+
state: 'APPLYING',
|
|
232
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
233
|
+
}],
|
|
234
|
+
}));
|
|
235
|
+
assert.equal(v.activeOperations.length, 1);
|
|
236
|
+
assert.equal(v.activeOperations[0]!.operationType, 'autosync-push');
|
|
237
|
+
assert.equal(v.activeOperations[0]!.state, 'APPLYING');
|
|
238
|
+
// 正在跑不是问题:既不进 incidents,也不触发「需要恢复」的横幅
|
|
239
|
+
assert.deepEqual(v.incidents, []);
|
|
240
|
+
assert.equal(v.recoveryRequired, false);
|
|
241
|
+
assert.equal(v.state, 'NORMAL');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('toRecoveryView:旧宿主不返回 live 字段 → activeOperations 为空数组(不误报正在跑)', () => {
|
|
245
|
+
const v = toRecoveryView({ incidents: [], running: [] });
|
|
246
|
+
assert.deepEqual(v.activeOperations, []);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('recoveryBlockReason:与 actionable 严格互补(面板据此解释「为什么灰」)', () => {
|
|
250
|
+
// needs-attention 优先:即使有快照,阻塞原因是「需人工处理」而不是「没快照」
|
|
251
|
+
assert.equal(recoveryBlockReason({ decision: 'needs-attention', snapshotId: 'snap-1' }), 'needsAttention');
|
|
252
|
+
assert.equal(recoveryBlockReason({ decision: 'needs-attention', snapshotId: null }), 'needsAttention');
|
|
253
|
+
// 能恢复的决策但没有可信快照 → 无快照可回滚
|
|
254
|
+
assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: null }), 'noSnapshot');
|
|
255
|
+
// 判据必须与 actionable 逐字同口径(:108 只认 null / ''),所以这里也只断言这两种形态
|
|
256
|
+
assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: '' }), 'noSnapshot');
|
|
257
|
+
// 可执行 → 无阻塞原因
|
|
258
|
+
assert.equal(recoveryBlockReason({ decision: 'rollback-recommended', snapshotId: 'snap-1' }), null);
|
|
259
|
+
assert.equal(recoveryBlockReason({ decision: 'rollback-continue', snapshotId: 'snap-1' }), null);
|
|
260
|
+
});
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* NEEDS_ATTENTION 显示为 recovered、绝不在 snapshot 不可信时显示可恢复。
|
|
21
21
|
*/
|
|
22
22
|
import type {
|
|
23
|
-
RecoveryDecision, RecoveryIncident, RecoveryPreview, RecoveryStatus, RecoveryVerdict,
|
|
23
|
+
RecoveryActiveOperation, RecoveryDecision, RecoveryIncident, RecoveryPreview, RecoveryStatus, RecoveryVerdict,
|
|
24
24
|
} from '../../ui/types.ts';
|
|
25
25
|
|
|
26
26
|
/** UI 顶层状态分类。 */
|
|
@@ -71,6 +71,12 @@ export interface RecoveryView {
|
|
|
71
71
|
incidents: RecoveryIncidentView[];
|
|
72
72
|
/** 进行中的 recovery run(runId + status)。 */
|
|
73
73
|
running: { runId: string; status: string }[];
|
|
74
|
+
/**
|
|
75
|
+
* 正在正常执行的操作(不是 incident)。决定两件事:
|
|
76
|
+
* ① 面板要给出一句「这些在执行中,会自行结束」,而不是让用户对着灰按钮猜;
|
|
77
|
+
* ② 若某条 incident 恰好也在跑(宿主判定放宽/旧宿主),对应行的「执行恢复/放弃恢复」要解释原因。
|
|
78
|
+
*/
|
|
79
|
+
activeOperations: RecoveryActiveOperation[];
|
|
74
80
|
/** 需用户显式处理的残留锁(attention=true 时才非 null)。 */
|
|
75
81
|
lock: RecoveryLockView | null;
|
|
76
82
|
/**
|
|
@@ -128,6 +134,8 @@ export function toRecoveryView(status: RecoveryStatus): RecoveryView {
|
|
|
128
134
|
recoveryRequired,
|
|
129
135
|
incidents,
|
|
130
136
|
running: status.running,
|
|
137
|
+
// 旧宿主不返回 live → 空数组(绝不误报「有操作在跑」)
|
|
138
|
+
activeOperations: status.live ?? [],
|
|
131
139
|
lock,
|
|
132
140
|
safeModeStuck,
|
|
133
141
|
safeModeBlocked,
|
|
@@ -136,6 +144,20 @@ export function toRecoveryView(status: RecoveryStatus): RecoveryView {
|
|
|
136
144
|
};
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
/**
|
|
148
|
+
* 「执行恢复」为什么不可用(面板显示原因,而不是丢一个没有任何解释的灰按钮)。
|
|
149
|
+
*
|
|
150
|
+
* 判据与 `actionable` 严格互补(同一条规则两处呈现):decision=needs-attention 断了
|
|
151
|
+
* 恢复路径,snapshotId 为空断了快照来源。返回 null = 可执行。
|
|
152
|
+
*/
|
|
153
|
+
export function recoveryBlockReason(
|
|
154
|
+
incident: Pick<RecoveryIncidentView, 'decision' | 'snapshotId'>,
|
|
155
|
+
): 'needsAttention' | 'noSnapshot' | null {
|
|
156
|
+
if (incident.decision === 'needs-attention') return 'needsAttention';
|
|
157
|
+
if (incident.snapshotId === null || incident.snapshotId === '') return 'noSnapshot';
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
/** 单个 preview 的渲染模型。 */
|
|
140
162
|
export interface RecoveryPreviewView {
|
|
141
163
|
operationId: string;
|
|
@@ -1617,7 +1617,7 @@ test('P0-9: 持久化字段清单显式化(键集合断言 —— 新增字段
|
|
|
1617
1617
|
'loadError', 'pullReport', 'pushPreview', 'pushReport', 'repoUrl', 'webdavUrl', 'webdavUsername',
|
|
1618
1618
|
])
|
|
1619
1619
|
assert.deepEqual(keys(p.sync.byChannel.git), [
|
|
1620
|
-
'autosync', 'autosyncEnabled', 'autosyncInterval', 'encrypt', 'includeSecrets',
|
|
1620
|
+
'autosync', 'autosyncEnabled', 'autosyncInterval', 'autosyncRealtime', 'encrypt', 'includeSecrets',
|
|
1621
1621
|
'loadingSnapshots', 'selectedSnapshotId', 'sessionsInclude', 'sessionsLimit', 'snapshots',
|
|
1622
1622
|
'syncMode', 'syncSections',
|
|
1623
1623
|
])
|
package/src/client/run-store.ts
CHANGED
|
@@ -1023,6 +1023,8 @@ export function toPersistedState(state: StoreState): PersistedState {
|
|
|
1023
1023
|
autosync: c.autosync,
|
|
1024
1024
|
autosyncEnabled: c.autosyncEnabled,
|
|
1025
1025
|
autosyncInterval: c.autosyncInterval,
|
|
1026
|
+
// 真·自动同步的本地监听开关(用户设置,非敏感;缺省 true)
|
|
1027
|
+
autosyncRealtime: c.autosyncRealtime,
|
|
1026
1028
|
})
|
|
1027
1029
|
return {
|
|
1028
1030
|
v: 1,
|
|
@@ -1645,6 +1647,8 @@ export class RunStore {
|
|
|
1645
1647
|
autosync: null,
|
|
1646
1648
|
autosyncEnabled: false,
|
|
1647
1649
|
autosyncInterval: '30m',
|
|
1650
|
+
// 旧版持久化载荷没有这个字段 → 缺省开启(真·自动同步是缺省行为)
|
|
1651
|
+
autosyncRealtime: true,
|
|
1648
1652
|
}
|
|
1649
1653
|
: undefined
|
|
1650
1654
|
return {
|