dsh-config-manager 0.1.73 → 0.1.76
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/client.d.ts +8 -4
- package/lib/client.js +814 -682
- package/lib/core/rollback-ledger.d.ts +29 -3
- package/lib/core/rollback-ledger.js +125 -7
- package/lib/core/rollback-point.d.ts +221 -3
- package/lib/core/rollback-point.js +790 -153
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/sync/messages.d.ts +1 -0
- package/lib/sync/messages.js +3 -0
- package/lib/sync/sync-engine.js +56 -7
- package/lib/ui/i18n.d.ts +1 -1
- package/lib/ui/i18n.js +2 -2
- package/lib/utils/bundle-scan.d.ts +5 -3
- package/lib/utils/bundle-scan.js +5 -3
- package/lib/utils/line-diff.d.ts +7 -1
- package/lib/utils/line-diff.js +8 -2
- package/package.json +1 -1
- package/src/client/chat/ChatEntryView.tsx +38 -9
- package/src/client/chat/RollbackFileIcon.tsx +7 -45
- package/src/client/chat/chat-entry-view.test.ts +164 -1
- package/src/client/chat/chat-entry-view.ts +127 -10
- package/src/client/chat/chat-node-registration.ts +40 -6
- package/src/client/common/DiffTable.tsx +12 -23
- package/src/client/common/host-primitives.ts +78 -0
- package/src/client/config-manager.module.css +33 -12
- package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
- package/src/client/index.ts +7 -1
- package/src/client/library/RestorePlanView.tsx +0 -1
- package/src/client/locales.ts +0 -2
- package/src/client/recovery/RollbackDiffPane.tsx +13 -6
- package/src/client/recovery/recovery-locales.ts +2 -4
- package/src/client/recovery/rollback-file-changes.test.ts +37 -1
- package/src/client/sync/PushPreviewCard.tsx +57 -0
- package/src/client/sync/SyncPanel.tsx +2 -37
- package/src/client/sync/SyncSettingsView.test.ts +3 -0
- package/src/client/sync/sync-push-preview.test.ts +14 -0
- package/src/client/sync/sync-view.ts +9 -1
- package/src/core/rollback-ledger.ts +123 -7
- package/src/core/rollback-point.test.ts +592 -17
- package/src/core/rollback-point.ts +861 -155
- package/src/index.ts +1 -1
- package/src/sync/messages.ts +3 -0
- package/src/sync/sync-engine.ts +55 -7
- package/src/sync/sync-preview-push.test.ts +158 -0
- package/src/ui/i18n.ts +2 -2
- package/src/utils/bundle-scan.ts +5 -3
- package/src/utils/line-diff.test.ts +12 -0
- package/src/utils/line-diff.ts +9 -3
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
RollbackSegmentSummary,
|
|
48
48
|
RollbackSessionPayload,
|
|
49
49
|
RollbackStateKind,
|
|
50
|
+
RollbackWorkspaceFile,
|
|
50
51
|
RollbackWorkspacePayload,
|
|
51
52
|
} from './rollback-point.ts'
|
|
52
53
|
|
|
@@ -133,6 +134,12 @@ export interface RollbackLedgerManifestEntry {
|
|
|
133
134
|
payload: string | null
|
|
134
135
|
/** 载荷文件字节数(**同一哈希只算一次**,预算与统计用) */
|
|
135
136
|
payloadBytes: number
|
|
137
|
+
/**
|
|
138
|
+
* 这份点**解析所需的全部载荷哈希**(自身在前、基准依次在后)。
|
|
139
|
+
* 增量载荷依赖基准 ⇒ 回收(prunePayloads)必须按链保活,否则链一断这个点就读不出来了。
|
|
140
|
+
* 老清单没有该字段 ⇒ 视为 `[payload]`(老载荷都是全量、无依赖)。
|
|
141
|
+
*/
|
|
142
|
+
payloadChain?: string[]
|
|
136
143
|
}
|
|
137
144
|
session: RollbackLedgerTrackHeader & {
|
|
138
145
|
/** 会话字节副本的对象仓键;null = 没有副本(回收对象时用,不必读记录文件) */
|
|
@@ -562,6 +569,7 @@ export function normalizeManifestEntry(value: unknown): RollbackLedgerManifestEn
|
|
|
562
569
|
if (sessionObject !== null && (typeof sessionObject !== 'string' || !OBJECT_KEY_RE.test(sessionObject))) return null
|
|
563
570
|
const rawRecordBytes = value['recordBytes']
|
|
564
571
|
const rawPayloadBytes = isRecord(workspaceRaw) ? workspaceRaw['payloadBytes'] : 0
|
|
572
|
+
const rawPayloadChain = isRecord(workspaceRaw) ? workspaceRaw['payloadChain'] : null
|
|
565
573
|
return {
|
|
566
574
|
id,
|
|
567
575
|
kind,
|
|
@@ -579,6 +587,10 @@ export function normalizeManifestEntry(value: unknown): RollbackLedgerManifestEn
|
|
|
579
587
|
...workspace,
|
|
580
588
|
payload: typeof wsPayload === 'string' ? wsPayload : null,
|
|
581
589
|
payloadBytes: typeof rawPayloadBytes === 'number' && Number.isFinite(rawPayloadBytes) && rawPayloadBytes >= 0 ? rawPayloadBytes : 0,
|
|
590
|
+
// 只保留形状合法的哈希;读不回来的链由解析器如实失败(绝不猜)
|
|
591
|
+
...(Array.isArray(rawPayloadChain)
|
|
592
|
+
? { payloadChain: rawPayloadChain.filter((h) => typeof h === 'string' && OBJECT_KEY_RE.test(h)) }
|
|
593
|
+
: {}),
|
|
582
594
|
},
|
|
583
595
|
session: { ...session, object: typeof sessionObject === 'string' ? sessionObject : null },
|
|
584
596
|
segments: [first, second],
|
|
@@ -601,6 +613,22 @@ function normalizeWorkspacePayload(value: unknown): RollbackWorkspacePayload | n
|
|
|
601
613
|
return value as unknown as RollbackWorkspacePayload
|
|
602
614
|
}
|
|
603
615
|
|
|
616
|
+
/**
|
|
617
|
+
* **存盘**形状校验:完整清单(`files` 是数组)或增量视图(`delta` 是对象、`files` 故意留 null)。
|
|
618
|
+
* 与 {@link normalizeWorkspacePayload}(消费侧,要求物化后的完整形状)分开:增量载荷**绝不会**被当成完整清单读出去。
|
|
619
|
+
*/
|
|
620
|
+
function normalizeStoredPayload(value: unknown): RollbackWorkspacePayload | null {
|
|
621
|
+
const base = normalizeWorkspacePayload(value)
|
|
622
|
+
if (base !== null) return base
|
|
623
|
+
if (!isRecord(value)) return null
|
|
624
|
+
const delta = value['delta']
|
|
625
|
+
if (!isRecord(delta)) return null
|
|
626
|
+
if (typeof delta['base'] !== 'string') return null
|
|
627
|
+
if (!Array.isArray(delta['added']) || !Array.isArray(delta['removed'])) return null
|
|
628
|
+
const clone = { ...value, files: [] }
|
|
629
|
+
return normalizeWorkspacePayload(clone)
|
|
630
|
+
}
|
|
631
|
+
|
|
604
632
|
function normalizeSessionPayload(value: unknown): RollbackSessionPayload | null {
|
|
605
633
|
if (!isRecord(value)) return null
|
|
606
634
|
if (value['sessionId'] !== null && typeof value['sessionId'] !== 'string') return null
|
|
@@ -805,7 +833,10 @@ export class RollbackLedger {
|
|
|
805
833
|
/* -------------------------------------------------------- 单条记录 */
|
|
806
834
|
|
|
807
835
|
/** 把完整记录拆成「清单条目 + 记录文件内容 + 载荷文件内容」(**不做 IO**,纯函数) */
|
|
808
|
-
prepareRecord(
|
|
836
|
+
prepareRecord(
|
|
837
|
+
record: RollbackPointRecord,
|
|
838
|
+
reuse?: { hash: string; bytes: number; chain?: string[] },
|
|
839
|
+
): RollbackLedgerPrepared {
|
|
809
840
|
const summary = this.summarize(record)
|
|
810
841
|
const workspaceTrack = record.workspace
|
|
811
842
|
const sessionTrack = record.session
|
|
@@ -813,7 +844,11 @@ export class RollbackLedger {
|
|
|
813
844
|
let payloadJson: string | null = null
|
|
814
845
|
let payloadHash: string | null = null
|
|
815
846
|
let payloadBytes = 0
|
|
816
|
-
if (payload !== null) {
|
|
847
|
+
if (payload !== null && reuse !== undefined) {
|
|
848
|
+
// **复用**:这份工作区态与基准点逐条相同 ⇒ 直接引用它已经落盘的载荷(零额外字节,链不增长)
|
|
849
|
+
payloadHash = reuse.hash
|
|
850
|
+
payloadBytes = reuse.bytes
|
|
851
|
+
} else if (payload !== null) {
|
|
817
852
|
payloadJson = JSON.stringify(payload)
|
|
818
853
|
payloadHash = sha256Hex(payloadJson)
|
|
819
854
|
payloadBytes = byteLength(payloadJson)
|
|
@@ -835,6 +870,14 @@ export class RollbackLedger {
|
|
|
835
870
|
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash },
|
|
836
871
|
session: { ...trackHeader(sessionTrack), payload: sessionTrack.payload },
|
|
837
872
|
})
|
|
873
|
+
// 增量视图 ⇒ 清单条目带上「解析所需的全部载荷哈希」(自身 + 基准链),回收按链保活
|
|
874
|
+
const payloadChain = payloadHash === null
|
|
875
|
+
? undefined
|
|
876
|
+
: reuse !== undefined
|
|
877
|
+
? (reuse.chain !== undefined && reuse.chain.length > 0 ? [...reuse.chain] : [payloadHash])
|
|
878
|
+
: payload?.delta === undefined
|
|
879
|
+
? [payloadHash]
|
|
880
|
+
: [payloadHash, ...payload.delta.baseChain]
|
|
838
881
|
const entry: RollbackLedgerManifestEntry = {
|
|
839
882
|
id: record.id,
|
|
840
883
|
kind: record.kind,
|
|
@@ -848,7 +891,7 @@ export class RollbackLedger {
|
|
|
848
891
|
turnSeq: record.turnSeq,
|
|
849
892
|
boundarySeq: record.boundarySeq,
|
|
850
893
|
recordBytes: byteLength(recordJson),
|
|
851
|
-
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash, payloadBytes },
|
|
894
|
+
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash, payloadBytes, ...(payloadChain === undefined ? {} : { payloadChain }) },
|
|
852
895
|
session: { ...trackHeader(sessionTrack), object: sessionTrack.payload?.object ?? null },
|
|
853
896
|
segments: summary.segments,
|
|
854
897
|
}
|
|
@@ -875,7 +918,12 @@ export class RollbackLedger {
|
|
|
875
918
|
}
|
|
876
919
|
|
|
877
920
|
/** 读校验过的载荷(哈希不符 / 形状不符 -> error,绝不返回半个) */
|
|
878
|
-
|
|
921
|
+
/**
|
|
922
|
+
* **原始**读取一份载荷(形状校验 + 内容寻址自证)。
|
|
923
|
+
* `files` 可能只是「相对基准的增量子集」(`payload.delta` 非空)—— **只给解析器用**,
|
|
924
|
+
* 消费者一律走 {@link resolvePayload} 拿物化后的完整清单。
|
|
925
|
+
*/
|
|
926
|
+
private async readStoredPayload(hash: string): Promise<{ payload?: RollbackWorkspacePayload; error?: string }> {
|
|
879
927
|
const target = this.payloadPath(hash)
|
|
880
928
|
if (target === null) return { error: 'record-incomplete: payload ref invalid' }
|
|
881
929
|
let raw: string
|
|
@@ -892,11 +940,60 @@ export class RollbackLedger {
|
|
|
892
940
|
} catch (error) {
|
|
893
941
|
return { error: 'record-incomplete: payload unreadable (' + errorDetail(error) + ')' }
|
|
894
942
|
}
|
|
895
|
-
const payload =
|
|
943
|
+
const payload = normalizeStoredPayload(parsed)
|
|
896
944
|
if (payload === null) return { error: 'record-incomplete: payload shape invalid' }
|
|
897
945
|
return { payload }
|
|
898
946
|
}
|
|
899
947
|
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* 解析一份载荷:增量视图**物化**成完整 `files`(含 `object` 字段补回),全量视图原样返回。
|
|
951
|
+
*
|
|
952
|
+
* 任何一环不成立(基准缺失 / 哈希不符 / 形状不符 / 链成环)⇒ **如实失败**:绝不返回半个清单 ——
|
|
953
|
+
* 半份清单会让「留点后新建」的判据把绝大多数文件当成新建的,回退就会删掉它们。
|
|
954
|
+
*/
|
|
955
|
+
async resolvePayload(hash: string): Promise<{ payload?: RollbackWorkspacePayload; error?: string }> {
|
|
956
|
+
// **不缓存**:载荷被删/被改必须立刻看得见(缓存会把「存储已经坏了」蒙过去),
|
|
957
|
+
// 也守住「一次只驻留一份载荷」的内存护栏(issue #89 的 OOM 现场)。
|
|
958
|
+
return this.materializePayload(hash, new Set([hash]))
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private async materializePayload(
|
|
962
|
+
hash: string,
|
|
963
|
+
seen: Set<string>,
|
|
964
|
+
): Promise<{ payload?: RollbackWorkspacePayload; error?: string }> {
|
|
965
|
+
const stored = await this.readStoredPayload(hash)
|
|
966
|
+
if (stored.payload === undefined) return { error: stored.error ?? 'record-incomplete: payload unreadable' }
|
|
967
|
+
const payload = stored.payload
|
|
968
|
+
const delta = payload.delta
|
|
969
|
+
if (delta === undefined) return { payload: withObjectRefs(payload) }
|
|
970
|
+
if (typeof delta.base !== 'string' || !OBJECT_KEY_RE.test(delta.base)) {
|
|
971
|
+
return { error: 'record-incomplete: delta base ref invalid' }
|
|
972
|
+
}
|
|
973
|
+
if (seen.has(delta.base)) return { error: 'record-incomplete: delta chain cycle' }
|
|
974
|
+
const next = new Set(seen)
|
|
975
|
+
next.add(delta.base)
|
|
976
|
+
const base = await this.materializePayload(delta.base, next)
|
|
977
|
+
if (base.payload === undefined) {
|
|
978
|
+
return { error: 'record-incomplete: delta base unresolved (' + String(base.error ?? '') + ')' }
|
|
979
|
+
}
|
|
980
|
+
const removed = new Set<string>()
|
|
981
|
+
for (const p of Array.isArray(delta.removed) ? delta.removed : []) if (typeof p === 'string') removed.add(p)
|
|
982
|
+
const added = withObjectRefs({ ...payload, files: delta.added ?? [], delta: undefined }).files
|
|
983
|
+
const addedPaths = new Set<string>()
|
|
984
|
+
for (const file of added) if (typeof file?.path === 'string') addedPaths.add(file.path)
|
|
985
|
+
const files: RollbackWorkspaceFile[] = []
|
|
986
|
+
for (const file of base.payload.files) {
|
|
987
|
+
if (typeof file?.path !== 'string') continue
|
|
988
|
+
if (removed.has(file.path) || addedPaths.has(file.path)) continue
|
|
989
|
+
files.push(file)
|
|
990
|
+
}
|
|
991
|
+
for (const file of added) files.push(file)
|
|
992
|
+
// 与全量捕获同序(`payload.files.sort` 用的就是这个比较器)
|
|
993
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
994
|
+
return { payload: { ...payload, files, delta: undefined } }
|
|
995
|
+
}
|
|
996
|
+
|
|
900
997
|
/** 读一条**完整**记录(引用解析对调用方透明);任何一步不成立就如实失败 */
|
|
901
998
|
async readRecord(id: string): Promise<{ record?: RollbackPointRecord; error?: string }> {
|
|
902
999
|
const target = this.recordPath(id)
|
|
@@ -923,7 +1020,7 @@ export class RollbackLedger {
|
|
|
923
1020
|
if (typeof payloadRef !== 'string' || !OBJECT_KEY_RE.test(payloadRef)) {
|
|
924
1021
|
return { error: 'record-incomplete: payload ref invalid' }
|
|
925
1022
|
}
|
|
926
|
-
const resolved = await this.
|
|
1023
|
+
const resolved = await this.resolvePayload(payloadRef)
|
|
927
1024
|
if (resolved.payload === undefined) return { error: resolved.error ?? 'record-incomplete: payload unreadable' }
|
|
928
1025
|
payload = resolved.payload
|
|
929
1026
|
}
|
|
@@ -985,6 +1082,12 @@ export class RollbackLedger {
|
|
|
985
1082
|
async prunePayloads(entries: readonly RollbackLedgerManifestEntry[]): Promise<{ removed: number; bytes: number }> {
|
|
986
1083
|
const referenced = new Set<string>()
|
|
987
1084
|
for (const entry of entries) {
|
|
1085
|
+
// 增量载荷依赖基准 ⇒ **按链保活**(否则淘汰/删点会把基准删掉,那个点就永远读不回来了)
|
|
1086
|
+
const chain = entry.workspace.payloadChain
|
|
1087
|
+
if (Array.isArray(chain) && chain.length > 0) {
|
|
1088
|
+
for (const hash of chain) referenced.add(hash)
|
|
1089
|
+
continue
|
|
1090
|
+
}
|
|
988
1091
|
if (entry.workspace.payload !== null) referenced.add(entry.workspace.payload)
|
|
989
1092
|
}
|
|
990
1093
|
let names: string[]
|
|
@@ -1037,7 +1140,7 @@ export class RollbackLedger {
|
|
|
1037
1140
|
for (const entry of entries) {
|
|
1038
1141
|
const hash = entry.workspace.payload
|
|
1039
1142
|
if (hash !== null) {
|
|
1040
|
-
const resolved = await this.
|
|
1143
|
+
const resolved = await this.resolvePayload(hash)
|
|
1041
1144
|
if (resolved.payload === undefined) {
|
|
1042
1145
|
return { removed: 0, bytes: 0, referencedPaths, aborted: resolved.error ?? 'payload unreadable' }
|
|
1043
1146
|
}
|
|
@@ -1305,6 +1408,19 @@ function trackHeader(track: {
|
|
|
1305
1408
|
}
|
|
1306
1409
|
}
|
|
1307
1410
|
|
|
1411
|
+
/**
|
|
1412
|
+
* 补回**落盘时省掉的** `object` 字段:内容寻址的名字就是内容哈希 ⇒ `object ≡ sha256`。
|
|
1413
|
+
* 缺 `sha256` 的畸形条目保持原样(消费侧的防御判据照旧生效)。
|
|
1414
|
+
*/
|
|
1415
|
+
function withObjectRefs(payload: RollbackWorkspacePayload): RollbackWorkspacePayload {
|
|
1416
|
+
for (const file of payload.files) {
|
|
1417
|
+
if (file === null || typeof file !== 'object') continue
|
|
1418
|
+
if (typeof file.object === 'string') continue
|
|
1419
|
+
if (typeof file.sha256 === 'string') file.object = file.sha256
|
|
1420
|
+
}
|
|
1421
|
+
return payload
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1308
1424
|
function errorDetail(error: unknown): string {
|
|
1309
1425
|
if (error instanceof Error) {
|
|
1310
1426
|
const code = (error as { code?: unknown }).code
|