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
|
@@ -36,6 +36,12 @@ export declare const s3Zh: {
|
|
|
36
36
|
readonly 'sync.s3.providerNoteKodo': '七牛 Kodo S3 兼容(region 必填,如 cn-east-1;endpoint 可覆盖)';
|
|
37
37
|
readonly 'sync.s3.unknownSignatureDialect': '未知签名方言: {id}';
|
|
38
38
|
readonly 'sync.s3.blobHashInvalid': '内容哈希形状非法,拒绝写入 blob 仓: {hash}';
|
|
39
|
+
readonly 'sync.s3.concurrentWrite': '远端索引已被其它设备更新(HTTP {status}):本次写入已中止,请重新同步后再试';
|
|
40
|
+
readonly 'sync.s3.probeOk': '连接正常(远端 {count} 份快照)';
|
|
41
|
+
readonly 'sync.s3.probeRegionRedirect': '端点把请求重定向到区域 {region}:请把 region 改成该值后重试';
|
|
42
|
+
readonly 'sync.s3.probeRedirectNoRegion': '端点返回了重定向(HTTP {status})但没有给出区域:请核对 endpoint 与 region';
|
|
43
|
+
readonly 'sync.s3.probeBucketMissing': '桶不存在或无权访问(HTTP 404):请核对 bucket 名与 endpoint';
|
|
44
|
+
readonly 'sync.s3.probeAccessDenied': '访问被拒绝(HTTP 403):请核对 AccessKey、密钥与桶权限';
|
|
39
45
|
};
|
|
40
46
|
export declare const s3En: Record<keyof typeof s3Zh, string>;
|
|
41
47
|
/**
|
package/lib/sync/s3/messages.js
CHANGED
|
@@ -26,6 +26,13 @@ export const s3Zh = {
|
|
|
26
26
|
'sync.s3.providerNoteKodo': '七牛 Kodo S3 兼容(region 必填,如 cn-east-1;endpoint 可覆盖)',
|
|
27
27
|
'sync.s3.unknownSignatureDialect': '未知签名方言: {id}',
|
|
28
28
|
'sync.s3.blobHashInvalid': '内容哈希形状非法,拒绝写入 blob 仓: {hash}',
|
|
29
|
+
// ---- cloud-sync 借鉴项 ①②:条件写并发保护 + 连通性探针 ----
|
|
30
|
+
'sync.s3.concurrentWrite': '远端索引已被其它设备更新(HTTP {status}):本次写入已中止,请重新同步后再试',
|
|
31
|
+
'sync.s3.probeOk': '连接正常(远端 {count} 份快照)',
|
|
32
|
+
'sync.s3.probeRegionRedirect': '端点把请求重定向到区域 {region}:请把 region 改成该值后重试',
|
|
33
|
+
'sync.s3.probeRedirectNoRegion': '端点返回了重定向(HTTP {status})但没有给出区域:请核对 endpoint 与 region',
|
|
34
|
+
'sync.s3.probeBucketMissing': '桶不存在或无权访问(HTTP 404):请核对 bucket 名与 endpoint',
|
|
35
|
+
'sync.s3.probeAccessDenied': '访问被拒绝(HTTP 403):请核对 AccessKey、密钥与桶权限',
|
|
29
36
|
};
|
|
30
37
|
export const s3En = {
|
|
31
38
|
'sync.s3.providerUnknown': 'Unknown S3-compatible channel: {provider}',
|
|
@@ -53,6 +60,12 @@ export const s3En = {
|
|
|
53
60
|
'sync.s3.providerNoteKodo': 'Qiniu Kodo S3-compatible (region required, e.g. cn-east-1; endpoint can be overridden)',
|
|
54
61
|
'sync.s3.unknownSignatureDialect': 'Unknown signature dialect: {id}',
|
|
55
62
|
'sync.s3.blobHashInvalid': 'Malformed content hash; refusing to write to the blob store: {hash}',
|
|
63
|
+
'sync.s3.concurrentWrite': 'The remote index was updated by another device (HTTP {status}): this write was aborted, please sync again',
|
|
64
|
+
'sync.s3.probeOk': 'Connection OK ({count} remote snapshots)',
|
|
65
|
+
'sync.s3.probeRegionRedirect': 'The endpoint redirects to region {region}: set region to that value and retry',
|
|
66
|
+
'sync.s3.probeRedirectNoRegion': 'The endpoint returned a redirect (HTTP {status}) without a region: check endpoint and region',
|
|
67
|
+
'sync.s3.probeBucketMissing': 'Bucket not found or not accessible (HTTP 404): check the bucket name and endpoint',
|
|
68
|
+
'sync.s3.probeAccessDenied': 'Access denied (HTTP 403): check the AccessKey, secret, and bucket permissions',
|
|
56
69
|
};
|
|
57
70
|
/** 插值:{param} → 形参值;缺参原样保留(与 core/messages.ts 同口径)。 */
|
|
58
71
|
function interpolate(template, params) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MsgFunc } from '../../core/msg-types.ts';
|
|
2
2
|
import { SyncTransportError } from '../transport.ts';
|
|
3
|
-
import type { SyncRetryOptions, SyncSnapshot, SyncSnapshotMeta, SyncTransport, SyncTransportErrorOptions } from '../transport.ts';
|
|
3
|
+
import type { SyncProbeResult, SyncRetryOptions, SyncSnapshot, SyncSnapshotMeta, SyncTransport, SyncTransportErrorOptions } from '../transport.ts';
|
|
4
4
|
import type { SigV4DialectId } from './sigv4.ts';
|
|
5
5
|
import type { S3Target } from './s3-providers.ts';
|
|
6
6
|
import type { S3CompatProvider } from '../sync-config.ts';
|
|
@@ -77,6 +77,14 @@ export declare class S3Transport implements SyncTransport {
|
|
|
77
77
|
download(id: string): Promise<SyncSnapshot>;
|
|
78
78
|
/** 删除远端快照(不存在视为成功);一并摘除索引条目并回收无人引用的 blob。 */
|
|
79
79
|
delete(id: string): Promise<void>;
|
|
80
|
+
/** 条件写失败:远端索引在本次读取之后被别人改过 —— 中止写入让用户重跑(cloud-sync 借鉴项 ①)。 */
|
|
81
|
+
private concurrentWrite;
|
|
82
|
+
/**
|
|
83
|
+
* 连通性探针(cloud-sync 借鉴项 ②):HEAD 桶证明端点/凭据/桶可达,再读一次索引回报快照数。
|
|
84
|
+
* 重定向(301/307/308)时读 x-amz-bucket-region 直接把「region 该填什么」告诉用户 ——
|
|
85
|
+
* 这是 S3 兼容端点最常见的失败原因(AWS/OSS/COS/Kodo 各自的 region 值不同)。
|
|
86
|
+
*/
|
|
87
|
+
probe(): Promise<SyncProbeResult>;
|
|
80
88
|
private assertSafeId;
|
|
81
89
|
/** 读取一次 AccessKey Secret(失败 → 空串,让请求按「无凭据」失败并把原因如实带回) */
|
|
82
90
|
private secretOnce;
|
|
@@ -39,6 +39,21 @@ const ERR_BODY_MAX = 500;
|
|
|
39
39
|
/** 内容哈希形状(sha256 hex):blob 对象键只接受它,杜绝路径穿越 */
|
|
40
40
|
const BLOB_HASH_RE = /^[a-f0-9]{64}$/;
|
|
41
41
|
const REDACTED = '[REDACTED]';
|
|
42
|
+
/**
|
|
43
|
+
* 条件写头(cloud-sync 借鉴项 ①):有 ETag → If-Match(远端必须仍是我读到的那一版);
|
|
44
|
+
* 索引本来就不存在 → If-None-Match: *(防两设备同时首推);服务端不给 ETag → 空(无条件写,不改既有行为)。
|
|
45
|
+
*/
|
|
46
|
+
function conditionalHeaders(etag, indexMissing) {
|
|
47
|
+
if (etag !== undefined)
|
|
48
|
+
return { 'if-match': etag };
|
|
49
|
+
return indexMissing ? { 'if-none-match': '*' } : {};
|
|
50
|
+
}
|
|
51
|
+
/** 条件写失败判定:412 恒是;409 仅在**确实带了条件头**时才算(无条件的 409 另有含义,不吞)。 */
|
|
52
|
+
function isConcurrentWrite(status, headers) {
|
|
53
|
+
if (status === 412)
|
|
54
|
+
return true;
|
|
55
|
+
return status === 409 && Object.keys(headers).length > 0;
|
|
56
|
+
}
|
|
42
57
|
/** S3 通道错误:继承统一错误基类(kind / retryable / status 对上层可见)。 */
|
|
43
58
|
export class S3TransportError extends SyncTransportError {
|
|
44
59
|
constructor(message, opts = {}) {
|
|
@@ -168,7 +183,7 @@ export class S3Transport {
|
|
|
168
183
|
const secret = await this.secretOnce();
|
|
169
184
|
const meta = computeSnapshotMeta(snapshot);
|
|
170
185
|
// audit-sync sync-N1 覆盖面补全(t66,来自 t22 的 C7):upload 也是写路径,**不接受 404 = 空集合**。
|
|
171
|
-
const { entries: idxBefore, missing: indexMissing } = await this.readIndexDetailed(secret);
|
|
186
|
+
const { entries: idxBefore, missing: indexMissing, etag: idxEtag } = await this.readIndexDetailed(secret);
|
|
172
187
|
if (indexMissing && await this.remoteHasPriorContent(secret)) {
|
|
173
188
|
// 索引 404 只是**缺信息**:远端已有历史内容时按空索引读改写,会把权威索引 PUT 成只有本条目
|
|
174
189
|
// → 其余快照从列表整体消失,随后 delete + GC 会回收仍被现存快照文件引用的 blob(不可恢复)。
|
|
@@ -188,7 +203,14 @@ export class S3Transport {
|
|
|
188
203
|
const merged = idxBefore.filter((m) => m.id !== snapshot.id);
|
|
189
204
|
merged.push(meta);
|
|
190
205
|
merged.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
|
|
191
|
-
const
|
|
206
|
+
const condHeaders = conditionalHeaders(idxEtag, indexMissing);
|
|
207
|
+
const putIdx = await this.send('PUT', indexKey(this.target.prefix), {
|
|
208
|
+
body: JSON.stringify(merged),
|
|
209
|
+
secret,
|
|
210
|
+
headers: condHeaders,
|
|
211
|
+
});
|
|
212
|
+
if (isConcurrentWrite(putIdx.res.status, condHeaders))
|
|
213
|
+
throw this.concurrentWrite(putIdx.url, putIdx.res.status);
|
|
192
214
|
if (!putIdx.res.ok) {
|
|
193
215
|
throw new S3TransportError(await this.failText('PUT', putIdx.url, putIdx.res, secret), classifyHttpStatus(putIdx.res.status));
|
|
194
216
|
}
|
|
@@ -221,7 +243,7 @@ export class S3Transport {
|
|
|
221
243
|
// index.json 覆盖成 [](其余快照从列表消失)并让 blob GC 删掉仍被引用的会话 blob —— 一律中止。
|
|
222
244
|
// audit-sync sync-N1(P0):与 WebDAV 同型 —— 写路径**不接受 404 = 空集合**。索引真缺失时
|
|
223
245
|
// 「远端一条快照都没有」是缺信息而非事实,沿用空索引会覆盖 index.json 并用空引用集回收 blob。
|
|
224
|
-
const { entries: idx, missing: indexMissing } = await this.readIndexDetailed(secret);
|
|
246
|
+
const { entries: idx, missing: indexMissing, etag: idxEtag } = await this.readIndexDetailed(secret);
|
|
225
247
|
const del = await this.send('DELETE', snapshotKey(this.target.prefix, id), { secret });
|
|
226
248
|
if (!del.res.ok && del.res.status !== 404) {
|
|
227
249
|
throw new S3TransportError(await this.failText('DELETE', del.url, del.res, secret), classifyHttpStatus(del.res.status));
|
|
@@ -233,13 +255,52 @@ export class S3Transport {
|
|
|
233
255
|
const remaining = idx.filter((m) => m.id !== id);
|
|
234
256
|
if (remaining.length === idx.length && del.res.status === 404)
|
|
235
257
|
return; // 对象与索引都没有 → 静默成功
|
|
236
|
-
const
|
|
258
|
+
const condHeaders = conditionalHeaders(idxEtag, false);
|
|
259
|
+
const putIdx = await this.send('PUT', indexKey(this.target.prefix), {
|
|
260
|
+
body: JSON.stringify(remaining),
|
|
261
|
+
secret,
|
|
262
|
+
headers: condHeaders,
|
|
263
|
+
});
|
|
264
|
+
if (isConcurrentWrite(putIdx.res.status, condHeaders))
|
|
265
|
+
throw this.concurrentWrite(putIdx.url, putIdx.res.status);
|
|
237
266
|
if (!putIdx.res.ok) {
|
|
238
267
|
throw new S3TransportError(await this.failText('PUT', putIdx.url, putIdx.res, secret), classifyHttpStatus(putIdx.res.status));
|
|
239
268
|
}
|
|
240
269
|
await this.gcBlobStore(secret, remaining).catch(() => undefined);
|
|
241
270
|
}
|
|
242
271
|
/* ---------------- 内部实现 ---------------- */
|
|
272
|
+
/** 条件写失败:远端索引在本次读取之后被别人改过 —— 中止写入让用户重跑(cloud-sync 借鉴项 ①)。 */
|
|
273
|
+
concurrentWrite(url, status) {
|
|
274
|
+
return new S3TransportError(this.o.msg('sync.s3.concurrentWrite', { status: String(status), url }), { kind: 'conflict', retryable: false, status });
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 连通性探针(cloud-sync 借鉴项 ②):HEAD 桶证明端点/凭据/桶可达,再读一次索引回报快照数。
|
|
278
|
+
* 重定向(301/307/308)时读 x-amz-bucket-region 直接把「region 该填什么」告诉用户 ——
|
|
279
|
+
* 这是 S3 兼容端点最常见的失败原因(AWS/OSS/COS/Kodo 各自的 region 值不同)。
|
|
280
|
+
*/
|
|
281
|
+
async probe() {
|
|
282
|
+
const secret = await this.secretOnce();
|
|
283
|
+
const bucket = await this.send('HEAD', '', { secret });
|
|
284
|
+
const status = bucket.res.status;
|
|
285
|
+
if (status === 301 || status === 307 || status === 308) {
|
|
286
|
+
const region = bucket.res.headers?.['x-amz-bucket-region'] ?? bucket.res.headers?.['X-Amz-Bucket-Region'];
|
|
287
|
+
throw new S3TransportError(region !== undefined && region !== ''
|
|
288
|
+
? this.o.msg('sync.s3.probeRegionRedirect', { region })
|
|
289
|
+
: this.o.msg('sync.s3.probeRedirectNoRegion', { status: String(status) }), { kind: 'client', retryable: false, status });
|
|
290
|
+
}
|
|
291
|
+
if (status === 404) {
|
|
292
|
+
throw new S3TransportError(this.o.msg('sync.s3.probeBucketMissing'), { kind: 'notfound', retryable: false, status });
|
|
293
|
+
}
|
|
294
|
+
if (status === 403) {
|
|
295
|
+
throw new S3TransportError(this.o.msg('sync.s3.probeAccessDenied'), { kind: 'auth', retryable: false, status });
|
|
296
|
+
}
|
|
297
|
+
if (!bucket.res.ok) {
|
|
298
|
+
throw new S3TransportError(await this.failText('HEAD', bucket.url, bucket.res, secret), classifyHttpStatus(status));
|
|
299
|
+
}
|
|
300
|
+
const { entries, missing } = await this.readIndexDetailed(secret);
|
|
301
|
+
const count = missing ? 0 : entries.length;
|
|
302
|
+
return { message: this.o.msg('sync.s3.probeOk', { count: String(count) }), snapshotCount: count };
|
|
303
|
+
}
|
|
243
304
|
assertSafeId(id) {
|
|
244
305
|
if (typeof id !== 'string' || !SAFE_ID_RE.test(id) || RESERVED_IDS.has(id)) {
|
|
245
306
|
throw new S3TransportError(this.o.msg('sync.s3.invalidSnapshotId', { id: JSON.stringify(id) }));
|
|
@@ -257,7 +318,12 @@ export class S3Transport {
|
|
|
257
318
|
/** 发送一次已签名请求;网络错误/超时归一为带分类的通道错误 */
|
|
258
319
|
async send(method, key, opts) {
|
|
259
320
|
const url = objectUrl(this.target, key);
|
|
260
|
-
const headers = {
|
|
321
|
+
const headers = {
|
|
322
|
+
'content-type': 'application/json',
|
|
323
|
+
// 条件写头(if-match / if-none-match)从这里注入(cloud-sync 借鉴项 ①);
|
|
324
|
+
// 签名在注入之后计算,因此条件头也进 SignedHeaders。
|
|
325
|
+
...(opts.headers ?? {}),
|
|
326
|
+
};
|
|
261
327
|
if (opts.body !== undefined)
|
|
262
328
|
headers['content-length'] = String(Buffer.byteLength(opts.body, 'utf8'));
|
|
263
329
|
const signed = signRequest({
|
|
@@ -342,7 +408,13 @@ export class S3Transport {
|
|
|
342
408
|
if (!sent.res.ok) {
|
|
343
409
|
throw new S3TransportError(await this.failText('GET', sent.url, sent.res, secret), classifyHttpStatus(sent.res.status));
|
|
344
410
|
}
|
|
345
|
-
|
|
411
|
+
// ETag 是条件写的前置条件(cloud-sync 借鉴项 ①);服务端没给就退化为无条件写,绝不判成失败。
|
|
412
|
+
const etag = sent.res.headers?.['etag'];
|
|
413
|
+
return {
|
|
414
|
+
entries: this.parseIndex(await sent.res.text(), sent.url, secret),
|
|
415
|
+
missing: false,
|
|
416
|
+
...(typeof etag === 'string' && etag !== '' ? { etag } : {}),
|
|
417
|
+
};
|
|
346
418
|
}
|
|
347
419
|
parseIndex(raw, url, secret) {
|
|
348
420
|
let parsed;
|
package/lib/sync/s3/sigv4.js
CHANGED
|
@@ -181,8 +181,17 @@ export function signRequest(input) {
|
|
|
181
181
|
dialect,
|
|
182
182
|
});
|
|
183
183
|
const signature = createHmac('sha256', signingKey).update(stringToSign, 'utf8').digest('hex');
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
// 授权头序列化按方言分两套(issue #109 实证):
|
|
185
|
+
// - AWS:AWS4-HMAC-SHA256 Credential=<ak>/<scope>, SignedHeaders=<list>, Signature=<sig>
|
|
186
|
+
// - OSS4:官方 Go/PHP SDK 的格式是**逗号后不带空格**,且 AdditionalHeaders 为空时**整段省略**;
|
|
187
|
+
// 写成 ", AdditionalHeaders=, Signature=" 会被 OSS 判成 Authorization header value is empty.
|
|
188
|
+
// (HTTP 400 InvalidArgument,错误码 EC 0002-00000209,真机逐一比对过两种格式)。
|
|
189
|
+
const authorization = dialect.headerListLabel === 'AdditionalHeaders'
|
|
190
|
+
? dialect.algorithm + ' Credential=' + input.accessKeyId + '/' + credentialScope
|
|
191
|
+
+ (headerList === '' ? '' : ',' + dialect.headerListLabel + '=' + headerList)
|
|
192
|
+
+ ',Signature=' + signature
|
|
193
|
+
: dialect.algorithm + ' Credential=' + input.accessKeyId + '/' + credentialScope
|
|
194
|
+
+ ', ' + dialect.headerListLabel + '=' + headerList + ', Signature=' + signature;
|
|
186
195
|
return {
|
|
187
196
|
headers: { ...all, authorization },
|
|
188
197
|
authorization,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Importer } from '../core/importer.ts';
|
|
2
2
|
import type { ConfigAdapter, GlobalConflictStrategy, HostContext, ImportAnalysis, ImportPlan, ImportResult, PathMapping, PlanItem, PlanItemKind, SecretScanner } from '../core/types.ts';
|
|
3
|
-
import type { SectionId } from '../schema/types.ts';
|
|
3
|
+
import type { SectionData, SectionId } from '../schema/types.ts';
|
|
4
4
|
import type { SnapshotFs } from './fs.ts';
|
|
5
5
|
import type { RetentionPolicy } from './retention-policy.ts';
|
|
6
|
-
import type {
|
|
6
|
+
import type { PendingMerge } from './sync-state.ts';
|
|
7
|
+
import type { SyncProbeResult, SyncSnapshotMeta, SyncTransport } from './transport.ts';
|
|
7
8
|
import type { MsgFunc } from '../core/messages.ts';
|
|
8
|
-
import type { MergePlan } from './merge.ts';
|
|
9
|
+
import type { MergeConflictReason, MergePlan, MergeSectionResult } from './merge.ts';
|
|
9
10
|
import type { SyncApplyPlan } from './risk.ts';
|
|
10
11
|
import type { TransactionSnapshotContext } from '../core/types.ts';
|
|
11
12
|
import type { PlanItemProgress } from '../core/analyzer.ts';
|
|
@@ -100,6 +101,14 @@ export interface SyncPushOptions {
|
|
|
100
101
|
* includeSecrets=true 必须同时 encrypt=true,否则拒绝(密钥绝不明文进入同步通道);
|
|
101
102
|
* 自动同步恒 includeSecrets=false(推普通快照)。 */
|
|
102
103
|
includeSecrets?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* 强制推送(缺省 false):跳过「远端已被其它设备推进」的前置检测。
|
|
106
|
+
*
|
|
107
|
+
* 为什么默认要拦:本机基线 X、别的设备推了 Y、本机再推 Z(不含 Y 的改动)时,
|
|
108
|
+
* 远端保留策略随后会淘汰 Y ⇒ **Y 的改动在远端与本机都消失**(静默丢数据)。
|
|
109
|
+
* 只有用户明确知道「要用本机覆盖远端」时才传 true。
|
|
110
|
+
*/
|
|
111
|
+
force?: boolean;
|
|
103
112
|
}
|
|
104
113
|
export interface SyncPullOptions {
|
|
105
114
|
/** 指定远端快照 id(缺省 = 最新) */
|
|
@@ -123,6 +132,12 @@ export interface SyncPushReport {
|
|
|
123
132
|
/** 分区级告警(单项失败不拖垮整体,§34.17 语义) */
|
|
124
133
|
warnings: string[];
|
|
125
134
|
message?: string;
|
|
135
|
+
/**
|
|
136
|
+
* 本次推送**被前置检测拒绝**(远端已被其它设备推进):未写远端、未记基线、未改 sync-state。
|
|
137
|
+
* 这是「需要先合并远端」的冲突态,不是通道/凭据故障 —— 调用方(自动同步 / 路由)应按冲突处理,
|
|
138
|
+
* 而不是记成「同步失败」。
|
|
139
|
+
*/
|
|
140
|
+
conflict?: boolean;
|
|
126
141
|
}
|
|
127
142
|
/** 差异报告中的单个变更项(PlanItem 摘要,不含敏感细节) */
|
|
128
143
|
export interface PullChange {
|
|
@@ -248,6 +263,63 @@ export interface ApplyItemsReport {
|
|
|
248
263
|
* 保留导出是为了让既有调用方/测试仍能引用「缺省保留份数」这一事实,不再作为唯一判据。
|
|
249
264
|
*/
|
|
250
265
|
export declare const MAX_REMOTE_SNAPSHOTS: number;
|
|
266
|
+
/** previewMerge 的单条冲突视图(三侧文本都已截断 + redact;绝不回传原始值对象)。 */
|
|
267
|
+
export interface MergePreviewConflictView {
|
|
268
|
+
/** 冲突位置:JSON 分区是 top-level key;文件类分区是 relativePath;'$' = 整分区冲突。 */
|
|
269
|
+
path: string;
|
|
270
|
+
kind: 'key' | 'file';
|
|
271
|
+
/** 冲突成因(可选:旧数据/旧 merge 结果没有该字段,界面缺省按 'both-modified' 措辞)。 */
|
|
272
|
+
reason?: MergeConflictReason;
|
|
273
|
+
/** 本机侧的可读预览(≤ 2 KiB,超出截断加省略号;已过 redact())。 */
|
|
274
|
+
localPreview: string;
|
|
275
|
+
/** 远端侧的可读预览(同上)。 */
|
|
276
|
+
remotePreview: string;
|
|
277
|
+
/** 共同祖先侧的可读预览(同上;无祖先时为空串)。 */
|
|
278
|
+
ancestorPreview: string;
|
|
279
|
+
}
|
|
280
|
+
/** previewMerge 的单个分区视图。 */
|
|
281
|
+
export interface MergePreviewSectionView {
|
|
282
|
+
id: SectionId;
|
|
283
|
+
decision: MergeSectionResult['decision'];
|
|
284
|
+
/** 自动合并并入的变更点数量(仅 decision === 'merge' 时有意义;缺省 = 未统计)。 */
|
|
285
|
+
autoMerged?: number;
|
|
286
|
+
conflictCount: number;
|
|
287
|
+
conflicts: MergePreviewConflictView[];
|
|
288
|
+
}
|
|
289
|
+
/** previewMerge 结果(只读、零写入)。 */
|
|
290
|
+
export interface MergePreview {
|
|
291
|
+
/** 预览是否成功产出(远端无快照 → false + message,分区清单为空)。 */
|
|
292
|
+
ok: boolean;
|
|
293
|
+
/** 本次预览针对的**远端**快照 id;'' = 远端无快照。 */
|
|
294
|
+
snapshotId: string;
|
|
295
|
+
/** 是否拿到了本地祖先副本(false = 退化为两方合并 → 所有双侧差异都会呈现为 conflict)。 */
|
|
296
|
+
hasAncestor: boolean;
|
|
297
|
+
sections: MergePreviewSectionView[];
|
|
298
|
+
/** 与 risk.ts 的 classifyMergePlan 同口径计数:
|
|
299
|
+
* auto = 可自动应用的非冲突项,review = 需人工,conflicts = decision === 'conflict',skipped = 'skip'。 */
|
|
300
|
+
summary: {
|
|
301
|
+
auto: number;
|
|
302
|
+
review: number;
|
|
303
|
+
conflicts: number;
|
|
304
|
+
skipped: number;
|
|
305
|
+
};
|
|
306
|
+
message?: string;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* 用户对**单个分区**的裁决(分区粒度:没有冲突项粒度 / 复合 id)。
|
|
310
|
+
* - useRemote:用远端整分区原值(对键级冲突而言 = 放弃本机该分区里除冲突键以外的改动);远端没有该分区 → 无法表达删除,计入 pendingConflicts;
|
|
311
|
+
* - keepLocal :保持本机整分区原值(本机没有该分区 = 无写入,直接跳过);
|
|
312
|
+
* - useMerged :取 merge() 的 merged 结果 —— 仅 decision === 'merge' 的分区有效;'conflict' 分区没有 merged,按冲突处理并计入 pendingConflicts;
|
|
313
|
+
* - skip :本轮不裁决该分区 → 计入 pendingConflicts,基线不推进(冲突下一轮重新出现)。
|
|
314
|
+
*/
|
|
315
|
+
export type MergeResolution = {
|
|
316
|
+
id: SectionId;
|
|
317
|
+
action: 'keepLocal' | 'useRemote' | 'useMerged' | 'skip';
|
|
318
|
+
};
|
|
319
|
+
/** applyMergeResolutions 报告:ApplyReport + 仍未裁决的冲突分区(非空 = 基线未推进)。 */
|
|
320
|
+
export type ApplyMergeResolutionsReport = ApplyReport & {
|
|
321
|
+
pendingConflicts: SectionId[];
|
|
322
|
+
};
|
|
251
323
|
export declare class SyncEngine {
|
|
252
324
|
private readonly ctx;
|
|
253
325
|
private readonly transport;
|
|
@@ -281,8 +353,25 @@ export declare class SyncEngine {
|
|
|
281
353
|
* 「源机 home → 本机 home」的重定基规则;'' = 旧快照/未知(不猜,行为不变)。
|
|
282
354
|
*/
|
|
283
355
|
private lastMergedRemoteSourceHome;
|
|
356
|
+
/**
|
|
357
|
+
* 最近一次 merge() 命中的远端快照是否加密(issue #107):
|
|
358
|
+
* applyMergeResolutions → applyMergePlan 写基线时据此决定祖先副本是否也落**密文**。
|
|
359
|
+
* false = 明文通道/未知(保持既有明文副本,行为逐字不变)。
|
|
360
|
+
*/
|
|
361
|
+
private lastMergedRemoteEncrypted;
|
|
284
362
|
/** 最近一次 merge() 收到的用户路径映射(applyMergePlan 复用;缺省 []) */
|
|
285
363
|
private lastMergedPathMappings;
|
|
364
|
+
/**
|
|
365
|
+
* 最近一次 merge() 是否拿到了本地祖先副本(false = 两方合并)。
|
|
366
|
+
* previewMerge 用它如实告诉界面「为什么全都是冲突」,绝不让两方差异看起来像三方结论。
|
|
367
|
+
*/
|
|
368
|
+
private lastMergeHadAncestor;
|
|
369
|
+
/**
|
|
370
|
+
* 最近一次 merge() 的三方输入(仅内存)。
|
|
371
|
+
* applyMergeResolutions 需要它还原「整分区」语义:merge() 的 MergePlan 只携带冲突处的
|
|
372
|
+
* 原始值(键级冲突只给键值),而 'useRemote' / 'keepLocal' 必须落**整分区**数据才能进导入计划。
|
|
373
|
+
*/
|
|
374
|
+
private lastMergeInputs;
|
|
286
375
|
constructor(opts: SyncEngineOptions);
|
|
287
376
|
/** 同步只做 portable 分区(deviceSpecific/platformSpecific 永不参与)。
|
|
288
377
|
* 构造注入 sections(同步范围)时再按注入范围过滤 —— 自动同步等后台流程
|
|
@@ -373,6 +462,9 @@ export declare class SyncEngine {
|
|
|
373
462
|
* opts.sections:指定仅同步这些分区(高级/自定义模式);缺省 = 全部 portable 推荐分区。
|
|
374
463
|
* opts.encrypt/password/includeSecrets:加密快照(含可选密钥导出)。
|
|
375
464
|
* 安全不变量:includeSecrets ⇒ encrypt(密钥绝不明文进入同步通道);密码仅内存。
|
|
465
|
+
*
|
|
466
|
+
* 前置:**远端已被其它设备推进时默认拒绝写入**(见 assertRemoteNotAdvanced)—— 这一条在导出
|
|
467
|
+
* 之前执行,因为「本机这一版要不要推」是被远端状态决定的,先别做无谓的导出(plugins 还会 npm pack)。
|
|
376
468
|
*/
|
|
377
469
|
push(opts?: SyncPushOptions): Promise<SyncPushReport>;
|
|
378
470
|
/**
|
|
@@ -416,6 +508,17 @@ export declare class SyncEngine {
|
|
|
416
508
|
private pruneRemoteSnapshots;
|
|
417
509
|
/** 读取远端保留策略;提供者缺失/抛错 → 缺省策略(保守多留,绝不因读取失败误删)。 */
|
|
418
510
|
private resolveRetentionPolicy;
|
|
511
|
+
/**
|
|
512
|
+
* push 前置:远端是否已被别的设备推进(防「推上去把别人的改动架空」)。
|
|
513
|
+
*
|
|
514
|
+
* 判据与 hasNewRemoteSnapshot() 同源但方向相反:
|
|
515
|
+
* - 首次同步(lastSnapshotId === '')→ **不拦**(本机没有基线可丢,第一次推送就是建立基线);
|
|
516
|
+
* - 远端为空 → 不拦(没有可比对的快照);
|
|
517
|
+
* - 其余:远端最新快照 id !== lastSnapshotId ⇒ 远端被推进(或本机基线已被裁剪)→ 拦。
|
|
518
|
+
* 两种情况都**无法安全断言「远端没变」**,保守拒绝并给出 force 出口。
|
|
519
|
+
* - list() 抛错 → 返回 reason(调用方按原样继续并告警),绝不把「读不到」当成「没变」。
|
|
520
|
+
*/
|
|
521
|
+
private remoteAdvancedCheck;
|
|
419
522
|
/**
|
|
420
523
|
* pull:拉取远端最新(或指定)快照 → 过滤 portable 分区 → 转临时标准 ZIP →
|
|
421
524
|
* 复用 Importer 预览流程(analyzeImport/createImportPlan)产出差异报告。
|
|
@@ -476,8 +579,19 @@ export declare class SyncEngine {
|
|
|
476
579
|
*
|
|
477
580
|
* 前提:lastSnapshotId 必须是**远端**快照 id(P0-7)。旧实现把本地生成的快照 id 也写进去,
|
|
478
581
|
* 于是合并/应用之后这里恒为 true → 每轮白跑 list + download + merge。
|
|
582
|
+
*
|
|
583
|
+
* `listed` 可选:调用方本轮已经 list 过(如自动同步先查加密快照)时直接传进来,
|
|
584
|
+
* 避免同一个回合对远端做两次 list —— 每条通道的 list 都是一次真实网络往返
|
|
585
|
+
* (git 通道 = `pull --ff-only`,webdav 通道 = PROPFIND/GET)。
|
|
479
586
|
*/
|
|
480
|
-
hasNewRemoteSnapshot(): Promise<boolean>;
|
|
587
|
+
hasNewRemoteSnapshot(listed?: readonly SyncSnapshotMeta[]): Promise<boolean>;
|
|
588
|
+
/**
|
|
589
|
+
* 连通性探针(cloud-sync 借鉴项 ②):把当前通道的**只读**自检透出给宿主路由。
|
|
590
|
+
*
|
|
591
|
+
* 通道没实现 probe(如 git 通道按 fast-forward 语义自证)时**如实报不支持**,
|
|
592
|
+
* 绝不返回「连接正常」—— 谎报成功比不支持更糟。
|
|
593
|
+
*/
|
|
594
|
+
probe(): Promise<SyncProbeResult>;
|
|
481
595
|
/**
|
|
482
596
|
* 本地 portable 配置当前内容与上次基线(sync-state.sections hash)相比是否有变化(§3.1 上传「看变化」)。
|
|
483
597
|
* - 从未同步(sync-state.sections 为空)→ true;
|
|
@@ -496,13 +610,76 @@ export declare class SyncEngine {
|
|
|
496
610
|
/**
|
|
497
611
|
* 三方合并(不写本地配置、不执行导入):local = 现场 export,remote = 下载的远端快照,
|
|
498
612
|
* ancestor = 本地祖先副本;副本缺失 → 退化为两方合并(整分区 conflict 交用户裁决)。
|
|
499
|
-
*
|
|
613
|
+
* 副作用只有进程内备忘(远端快照 id / sourceHome / 用户映射 / 祖先可用性 / plan / 三方输入),
|
|
614
|
+
* 供紧随其后的 previewMerge / applyMergePlan / applyMergeResolutions 复用:同一次调用只下载一次远端。
|
|
500
615
|
*/
|
|
501
616
|
merge(opts?: {
|
|
502
617
|
snapshotId?: string;
|
|
503
618
|
password?: string;
|
|
504
619
|
pathMappings?: PathMapping[];
|
|
505
620
|
}): Promise<MergePlan>;
|
|
621
|
+
/**
|
|
622
|
+
* 合并预览(只读、零写入):把 merge() 的三方结果翻成界面可渲染的结构化视图。
|
|
623
|
+
*
|
|
624
|
+
* 与 merge() 的关系:**复用**同一次调用(同一实例上只下载一次远端快照、只 export 一次本地),
|
|
625
|
+
* 不改远端、不写 sync-state、不落祖先副本、不动本机配置。
|
|
626
|
+
*
|
|
627
|
+
* 安全:每侧预览文本都先过 redact() 再截断到 2 KiB(敏感值绝不回传浏览器);原始值对象
|
|
628
|
+
* (对象 / 数组 / 字节数组)**绝不**上抛给调用方。
|
|
629
|
+
*/
|
|
630
|
+
previewMerge(opts?: {
|
|
631
|
+
snapshotId?: string;
|
|
632
|
+
password?: string;
|
|
633
|
+
pathMappings?: PathMapping[];
|
|
634
|
+
}): Promise<MergePreview>;
|
|
635
|
+
/**
|
|
636
|
+
* 按用户裁决应用合并结果(**分区粒度**),并严守基线纪律。
|
|
637
|
+
*
|
|
638
|
+
* 与 applyMergePlan 的关系:把 resolutions 映射成 SyncApplyPlan 后走**同一条执行路径**
|
|
639
|
+
* (applyMergePlan:兜底快照 + 失败整体回滚 + 成功才记基线)。本方法只额外负责两件事:
|
|
640
|
+
* 1. 把 merge() 的分区级结论翻成「远端整分区 / 本机整分区 / 自动合并结果」的实际数据
|
|
641
|
+
* (键级冲突的 conflicts 只带键值,进不了导入计划,必须回到整分区语义);
|
|
642
|
+
* 2. 基线纪律 —— 只要还有未裁决分区,就**不推进远端基线指针**并记 pendingMerge
|
|
643
|
+
* (冲突下一轮重新出现);全部裁决完才清空 pendingMerge 并推进基线。
|
|
644
|
+
*
|
|
645
|
+
* 裁决语义(与 MergeResolution 的文档一致):
|
|
646
|
+
* - 未点名的分区按 merge() 的既有结论处理:可决断(useRemote / keepLocal / merge)→ 应用;
|
|
647
|
+
* 'skip' → 跳过;'conflict' → 计入 pendingConflicts;
|
|
648
|
+
* - action='skip'(点名了但不裁决):该分区若有内容可裁决 → 计入 pendingConflicts(**不算解决**,
|
|
649
|
+
* 否则基线推进会让它下一轮不再被提出 = 静默丢改动);本来就无变化的 'skip' 分区 → 跳过;
|
|
650
|
+
* - useMerged 而 merge() 没给 merged(典型:'conflict' 分区)→ 计入 pendingConflicts;
|
|
651
|
+
* - useRemote 而远端没有该分区(= 远端删了它)→ 导入计划表达不了删除 → 计入 pendingConflicts;
|
|
652
|
+
* - keepLocal 而本机没有该分区 → 保持现状(无写入)→ 跳过。
|
|
653
|
+
*/
|
|
654
|
+
applyMergeResolutions(resolutions: MergeResolution[], opts?: {
|
|
655
|
+
snapshotId?: string;
|
|
656
|
+
password?: string;
|
|
657
|
+
pathMappings?: PathMapping[];
|
|
658
|
+
snapshotBinding?: TransactionSnapshotContext;
|
|
659
|
+
}): Promise<ApplyMergeResolutionsReport>;
|
|
660
|
+
/** 组装 pendingMerge 记录:同一远端快照上的连续待裁决**保留首次时间**(界面据此讲「挂了多久」)。 */
|
|
661
|
+
private pendingMergeFor;
|
|
662
|
+
/**
|
|
663
|
+
* 基线纪律的**纯状态**写入(不写祖先副本、不碰远端):仅在本机没有任何配置写入时使用。
|
|
664
|
+
*
|
|
665
|
+
* - pending 提供 → 写 pendingMerge;undefined → 清除该字段(「全部裁决完」);
|
|
666
|
+
* - advanceRemoteId 提供 → 把远端指针推进到该快照 id(缺省 = 保持不变)。
|
|
667
|
+
*
|
|
668
|
+
* 为什么不走 recordBaseline:那条路径会重建祖先副本并只带本次应用的分区 —— 在「没有应用任何分区」
|
|
669
|
+
* 或「仍有未裁决冲突」时都会把共同基线改坏(下一轮所有分区都退化成「无祖先 → 冲突」)。
|
|
670
|
+
*/
|
|
671
|
+
private writeMergeBaseline;
|
|
672
|
+
/**
|
|
673
|
+
* 重新导出**已应用**的分区(F-2,2026-10):基线的唯一正确来源是「实际落地的本机内容」。
|
|
674
|
+
*
|
|
675
|
+
* 为什么不能用计划/远端的 merged 载荷:写回时 adapter 会自增本机侧字段(如
|
|
676
|
+
* settings.namespaces[*].revision),而 merged 记录的是**远端快照里的值**;据此记基线会让
|
|
677
|
+
* 下一轮 merge 同时判「本地改了」与「远端改了」→ both-modified 冲突 → 每轮重复冲突。
|
|
678
|
+
*
|
|
679
|
+
* 口径与 merge() 的本地侧导出一致:文件类分区直接取 data,其余过 SecretScanner 剥离。
|
|
680
|
+
* 任一分区导出失败 → 返回 null(调用方**不推进基线**并明确告警):绝不写半个基线。
|
|
681
|
+
*/
|
|
682
|
+
private exportAppliedSections;
|
|
506
683
|
/**
|
|
507
684
|
* 记录祖先基线:写本地祖先副本 + 更新 sync-state(每分区 hash/updatedAt、lastSyncAt、transport、
|
|
508
685
|
* 远端指针 lastSnapshotId、本地祖先目录名 ancestorId)+ 裁剪到 keep。
|
|
@@ -513,12 +690,24 @@ export declare class SyncEngine {
|
|
|
513
690
|
* (push 路径两者天然相同);合并/应用路径**必须显式传**,否则 lastSnapshotId 会退化成「一个远端
|
|
514
691
|
* 并不存在的本地 id」→ hasNewRemoteSnapshot() 恒判「远端有新生」,每轮白跑 list + download + merge。
|
|
515
692
|
* 传入 `''` 表示「远端 id 未知」——宁可留空,也不要把本地 id 冒充成远端 id。
|
|
516
|
-
* - `opts.writeAncestor === false
|
|
693
|
+
* - `opts.writeAncestor === false`:不落本地祖先副本,`ancestorId` 一并置 '' ——
|
|
517
694
|
* 绝不把一个本地并不存在的目录名写进状态(旧实现正是这样让下一轮 merge 直接抛错)。
|
|
518
|
-
|
|
519
|
-
|
|
695
|
+
* - `opts.ancestorPassword`(issue #107):祖先副本落**密文**(manifest.encrypted + 单密文载荷);
|
|
696
|
+
* 加密快照 push 必须传,否则下轮三方合并缺共同祖先、整分区冲突。
|
|
697
|
+
* - `opts.preserveAncestor === true`(合并仍有未裁决冲突):祖先副本与 ancestorId **保持不动** ——
|
|
698
|
+
* 共同基线必须仍是「本机与远端都认可的上一版」;拿只带本次应用分区的副本去覆盖它,会让
|
|
699
|
+
* 下一轮所有分区都退化成「无祖先 → 冲突」。
|
|
700
|
+
* - `opts.pendingMerge`(基线纪律):提供即写入;`null` = 清除该字段;缺省 = 不动(既有行为不变)。
|
|
701
|
+
*/
|
|
702
|
+
recordBaseline(snapshotId: string, sections: Partial<Record<SectionId, SectionData>>, nowIso?: string, opts?: {
|
|
520
703
|
writeAncestor?: boolean;
|
|
704
|
+
/** 提供即把祖先副本写成**密文**(密码仅本次内存使用;issue #107) */
|
|
705
|
+
ancestorPassword?: string;
|
|
521
706
|
remoteSnapshotId?: string;
|
|
707
|
+
/** 保留当前祖先副本与 ancestorId(缺省 false = 既有行为逐字不变) */
|
|
708
|
+
preserveAncestor?: boolean;
|
|
709
|
+
/** 待裁决冲突记录:null = 清除;缺省 = 不动该字段 */
|
|
710
|
+
pendingMerge?: PendingMerge | null;
|
|
522
711
|
/** P1-5:本轮**实际带走**的会话单元(提供即覆盖;缺省 = 保留原记录) */
|
|
523
712
|
sessionUnits?: string[];
|
|
524
713
|
/** P1-5:本轮累积的删除墓碑(提供即覆盖;缺省 = 保留原记录) */
|
|
@@ -548,6 +737,21 @@ export declare class SyncEngine {
|
|
|
548
737
|
pathMappings?: PathMapping[];
|
|
549
738
|
/** 覆盖自动重定基的源 home(缺省 = merge() 记下的远端 sourceHome) */
|
|
550
739
|
sourceHome?: string;
|
|
740
|
+
/**
|
|
741
|
+
* 保留祖先副本与 ancestorId(缺省 false = 既有行为逐字不变)。
|
|
742
|
+
* applyMergeResolutions 在**仍有未裁决冲突**时必须传 true:共同基线仍是「本机与远端都认可的上一版」。
|
|
743
|
+
*/
|
|
744
|
+
preserveAncestor?: boolean;
|
|
745
|
+
/**
|
|
746
|
+
* 待裁决冲突记录(基线纪律):null = 清除(全部裁决完);缺省 = 不动该字段。
|
|
747
|
+
* 与 recordBaseline 同语义,随本次成功的基线写入**同一次落盘**(原子,不留中间态)。
|
|
748
|
+
*/
|
|
749
|
+
pendingMerge?: PendingMerge | null;
|
|
750
|
+
/**
|
|
751
|
+
* issue #107:本次基线对应的远端快照是加密快照时传解密密码 → 祖先副本也落**密文**。
|
|
752
|
+
* 缺省 = 明文祖先副本(既有行为逐字不变)。密码仅本进程内存使用。
|
|
753
|
+
*/
|
|
754
|
+
ancestorPassword?: string;
|
|
551
755
|
}): Promise<ApplyReport>;
|
|
552
756
|
/**
|
|
553
757
|
* applyItems:按用户对差异项的逐项决策执行导入(§3.4/§5.3)。
|