dsh-config-manager 0.1.39 → 0.1.41
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 +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
|
@@ -102,7 +102,7 @@ test('m2-refresh: password/passwordConfirm/secretInputs 绝不写入 sessionStor
|
|
|
102
102
|
assert.ok(!('secretInputs' in (parsed['import'] as Record<string, unknown>)))
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
test('m2-refresh:
|
|
105
|
+
test('m2-refresh: 非敏感表单状态往返恢复,敏感字段与导出结果瞬态刷新后清空', () => {
|
|
106
106
|
const { storage } = makeStorage()
|
|
107
107
|
const first = new RunStore({ storage })
|
|
108
108
|
first.patch({
|
|
@@ -134,7 +134,7 @@ test('m2-refresh: 非敏感状态往返恢复,敏感字段刷新后清空', ()
|
|
|
134
134
|
assert.deepEqual(st.export.selection, ['settings', 'plugins'])
|
|
135
135
|
assert.equal(st.export.includeSecrets, true)
|
|
136
136
|
assert.equal(st.export.encrypt, true, 'encrypt 为非敏感选项,刷新后恢复')
|
|
137
|
-
assert.equal(st.export.downloaded,
|
|
137
|
+
assert.equal(st.export.downloaded, false, '导出完成提示(downloaded)为瞬态,刷新后清空')
|
|
138
138
|
assert.equal(st.export.password, '', '密码刷新后清空')
|
|
139
139
|
assert.equal(st.export.passwordConfirm, '', '确认密码刷新后清空')
|
|
140
140
|
assert.equal(st.import.step, 'preview')
|
|
@@ -144,6 +144,44 @@ test('m2-refresh: 非敏感状态往返恢复,敏感字段刷新后清空', ()
|
|
|
144
144
|
assert.deepEqual(st.import.secretInputs, {}, '秘密补录值刷新后清空')
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
+
test('m2-refresh: 导出结果/进度/进行中为内存切片瞬态——不写入 sessionStorage、刷新后清空', () => {
|
|
148
|
+
const { storage, raw } = makeStorage()
|
|
149
|
+
const store = new RunStore({ storage })
|
|
150
|
+
store.patch({
|
|
151
|
+
export: {
|
|
152
|
+
result: {
|
|
153
|
+
zipPath: 'export-result-x.zip',
|
|
154
|
+
manifest: makeManifest(),
|
|
155
|
+
report: makeExportReport(),
|
|
156
|
+
text: '备份已创建\n已包含:\n ✓ settings (21 namespaces)',
|
|
157
|
+
},
|
|
158
|
+
downloaded: true,
|
|
159
|
+
running: true,
|
|
160
|
+
progress: { stage: 'exporting', step: 1, total: 3, section: 'settings', item: 1, itemTotal: 3 },
|
|
161
|
+
runId: RUN_ID,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
const text = raw()
|
|
165
|
+
assert.ok(text !== null, 'patch 后已同步持久化')
|
|
166
|
+
assert.ok(!text.includes('export-result-x.zip'), '导出结果文件名不得落入 sessionStorage')
|
|
167
|
+
assert.ok(!text.includes('备份已创建'), '报告文本不得落入 sessionStorage')
|
|
168
|
+
const parsed = JSON.parse(text) as { export: Record<string, unknown> }
|
|
169
|
+
assert.ok(!('result' in parsed['export']), 'export 切片不含 result(瞬态不落盘)')
|
|
170
|
+
assert.ok(!('downloaded' in parsed['export']), 'export 切片不含 downloaded(瞬态不落盘)')
|
|
171
|
+
assert.ok(!('running' in parsed['export']), 'export 切片不含 running(瞬态不落盘)')
|
|
172
|
+
assert.ok(!('progress' in parsed['export']), 'export 切片不含 progress(瞬态不落盘)')
|
|
173
|
+
assert.ok(!('runId' in parsed['export']), 'export 切片不含 runId(瞬态不落盘)')
|
|
174
|
+
|
|
175
|
+
// 新实例 + 同一存储 = 模拟页面刷新 → 导出结果清空,回到干净导出页
|
|
176
|
+
const second = new RunStore({ storage })
|
|
177
|
+
const st = second.getSnapshot()
|
|
178
|
+
assert.equal(st.export.result, null, '导出报告刷新后清空(不残留展示)')
|
|
179
|
+
assert.equal(st.export.downloaded, false, '已保存提示刷新后清空')
|
|
180
|
+
assert.equal(st.export.running, false, '进行中为瞬态,刷新后回复空闲')
|
|
181
|
+
assert.equal(st.export.progress, null, '进度刷新后清空')
|
|
182
|
+
assert.equal(st.export.runId, null, 'runId 刷新后清空')
|
|
183
|
+
})
|
|
184
|
+
|
|
147
185
|
test('损坏或版本不符的存储数据回退默认并清除脏键', () => {
|
|
148
186
|
const { storage, raw } = makeStorage()
|
|
149
187
|
raw() // no-op to satisfy lint-like usage
|
package/src/client/run-store.ts
CHANGED
|
@@ -53,6 +53,8 @@ import type { ChannelSyncState, SyncChannel } from './sync/sync-view.ts'
|
|
|
53
53
|
import { defaultChannelSyncState } from './sync/sync-view.ts'
|
|
54
54
|
import type { MarketApprovals } from './market/market-view.ts'
|
|
55
55
|
import type { MyItemEntry } from './market/my-configs-api.ts'
|
|
56
|
+
import type { MyInstallSlice, MyWizardSlice } from './market/my-configs-view.ts'
|
|
57
|
+
import type { SyncConflictResolution } from './sync/sync-view.ts'
|
|
56
58
|
|
|
57
59
|
/* ---------------------------------------------------------------- 基础类型 */
|
|
58
60
|
|
|
@@ -118,6 +120,8 @@ export interface SyncStoreSlice {
|
|
|
118
120
|
pullReport: SyncPullReport | null
|
|
119
121
|
/** 一键同步差异确认会话(items 供 UI 逐项确认;宿主侧 30 分钟 TTL 内存登记) */
|
|
120
122
|
confirmSession: SyncStartResponse | null
|
|
123
|
+
/** 一键同步差异确认的逐项决策(adopted/resolution;非敏感,与 confirmSession 生命周期绑定——切 tab/刷新不丢,刷新后会话仍在可恢复决策) */
|
|
124
|
+
confirmDecisions: SyncConfirmDecisions | null
|
|
121
125
|
/** 最近一次一键同步执行结果(回滚入口) */
|
|
122
126
|
lastRestoreId: string | null
|
|
123
127
|
/** 已 redact 的错误文本 */
|
|
@@ -128,6 +132,15 @@ export interface SyncStoreSlice {
|
|
|
128
132
|
/** 同步视图的进行中操作标记(与 SyncUiState.busy 同形状;无 busy 时为空闲)。 */
|
|
129
133
|
export type SyncBusyState = 'sync' | 'push' | 'pull' | 'rollback' | null
|
|
130
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 一键同步差异确认的单条决策(非敏感;resolution 仅在 Conflict 项已解决时存在)。
|
|
137
|
+
* 与 SyncConfirmView 的 ItemState 同构(resolution 序列化为 SyncConflictResolution 字符串)。
|
|
138
|
+
*/
|
|
139
|
+
export type SyncConfirmDecision = { adopted: boolean; resolution?: SyncConflictResolution }
|
|
140
|
+
|
|
141
|
+
/** 一键同步差异确认的逐项决策表(itemId → 决策;null = 无进行中的确认会话决策)。 */
|
|
142
|
+
export type SyncConfirmDecisions = Record<string, SyncConfirmDecision>
|
|
143
|
+
|
|
131
144
|
/** 单通道状态的持久化切片 = 运行时剔除密码类敏感字段(安全关键,白名单单一出口)。 */
|
|
132
145
|
export type PersistedChannelSyncState = Omit<
|
|
133
146
|
ChannelSyncState,
|
|
@@ -164,6 +177,12 @@ export interface MarketStoreSlice {
|
|
|
164
177
|
myItems: MyItemEntry[] | null
|
|
165
178
|
/** 我的配置子视图:列表加载错误(已 redact 文本;null = 无) */
|
|
166
179
|
myItemsError: string | null
|
|
180
|
+
/** 我的配置子视图:上传/更新向导持久化切片(仅非敏感字段;null = 未开始或已完成,见 my-configs-view.ts MyWizardSlice) */
|
|
181
|
+
myWizard: MyWizardSlice | null
|
|
182
|
+
/** 我的配置子视图:装回本地(下载+逐分区批准+导入结果)持久化切片(仅非敏感;null = 未开始或已关闭) */
|
|
183
|
+
myInstall: MyInstallSlice | null
|
|
184
|
+
/** 我的配置子视图:删除确认弹窗的目标条目 id(非敏感;切 tab/刷新不丢,用户可见的确认态) */
|
|
185
|
+
myConfirmDeleteId: string | null
|
|
167
186
|
}
|
|
168
187
|
|
|
169
188
|
/** 快照恢复面板的切片(无敏感字段;快照列表本身可随时重载,不持久化)。 */
|
|
@@ -177,7 +196,14 @@ export interface SnapshotsStoreSlice {
|
|
|
177
196
|
|
|
178
197
|
/* ------------------------------------------------- 持久化(非敏感)状态形状 */
|
|
179
198
|
|
|
180
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* 导出视图的持久化切片 = 仅「表单配置类」非敏感字段(模式/勾选/安全选项/错误)。
|
|
201
|
+
*
|
|
202
|
+
* 导出结果(result / downloaded)与进行中/进度(running / progress / runId)是
|
|
203
|
+
* **内存切片瞬态**:切 tab / 关面板由模块级单例保留,刷新 / 关闭 DSH 重开后自动
|
|
204
|
+
* 清空 —— 旧导出报告与「已保存到下载目录」提示不残留展示(与 sync 面板 busy 同级)。
|
|
205
|
+
* 不含 password/passwordConfirm(仅内存)。
|
|
206
|
+
*/
|
|
181
207
|
export interface PersistedExportState {
|
|
182
208
|
mode: ExportMode
|
|
183
209
|
selection: SectionId[]
|
|
@@ -185,14 +211,8 @@ export interface PersistedExportState {
|
|
|
185
211
|
includeSecrets: boolean
|
|
186
212
|
/** 是否加密备份(独立选项;不导出密钥也可单独加密) */
|
|
187
213
|
encrypt: boolean
|
|
188
|
-
running: boolean
|
|
189
|
-
progress: RunProgress | null
|
|
190
|
-
result: ExportRunResult | null
|
|
191
214
|
/** 错误消息(已 redact 的文本;Error 对象不可 JSON 序列化,统一转字符串) */
|
|
192
215
|
error: string | null
|
|
193
|
-
downloaded: boolean
|
|
194
|
-
/** 最近一次导出 run id(/runs + /progress 重新订阅用;32-hex 不可猜) */
|
|
195
|
-
runId: string | null
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
/** 导入向导的持久化切片(不含 secretInputs)。 */
|
|
@@ -235,11 +255,27 @@ export interface PersistedState {
|
|
|
235
255
|
|
|
236
256
|
/* --------------------------------------- 运行时状态(含仅内存的敏感字段) */
|
|
237
257
|
|
|
238
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* 导出运行时状态 = 持久化字段 + 密码字段(仅内存)+ 导出结果/进度瞬态。
|
|
260
|
+
*
|
|
261
|
+
* result / downloaded / running / progress / runId 为「内存切片」:toPersistedState
|
|
262
|
+
* 白名单剔除、applyPersisted 硬性清空 —— 刷新 / 关闭 DSH 重开后不残留;
|
|
263
|
+
* 进行中导出由 m2-resume 从宿主 /runs 重新发现并恢复轮询。
|
|
264
|
+
*/
|
|
239
265
|
export interface ExportLiveState extends PersistedExportState {
|
|
240
266
|
/** 加密密码(仅内存,绝不写入 sessionStorage;刷新后清空) */
|
|
241
267
|
password: string
|
|
242
268
|
passwordConfirm: string
|
|
269
|
+
/** 导出进行中(内存切片瞬态:切 tab 保留、刷新清空) */
|
|
270
|
+
running: boolean
|
|
271
|
+
/** 导出进度(内存切片瞬态;进行中由 m2-resume 重回填,否则清空) */
|
|
272
|
+
progress: RunProgress | null
|
|
273
|
+
/** 导出结果报告(内存切片瞬态;刷新/关闭 DSH 后清空,不残留展示) */
|
|
274
|
+
result: ExportRunResult | null
|
|
275
|
+
/** 「已保存到下载目录」提示(内存切片瞬态;刷新后清空) */
|
|
276
|
+
downloaded: boolean
|
|
277
|
+
/** 最近一次导出 run id(/runs + /progress 重新订阅用;resume 自 /runs 发现,纯瞬态) */
|
|
278
|
+
runId: string | null
|
|
243
279
|
}
|
|
244
280
|
|
|
245
281
|
/** 导入运行时状态 = 持久化字段 + 仅内存的敏感/实例字段。 */
|
|
@@ -347,6 +383,7 @@ function defaultSyncState(): SyncStoreSlice {
|
|
|
347
383
|
pushReport: null,
|
|
348
384
|
pullReport: null,
|
|
349
385
|
confirmSession: null,
|
|
386
|
+
confirmDecisions: null,
|
|
350
387
|
lastRestoreId: null,
|
|
351
388
|
error: null,
|
|
352
389
|
loadError: null,
|
|
@@ -366,6 +403,9 @@ function defaultMarketState(): MarketStoreSlice {
|
|
|
366
403
|
loadError: null,
|
|
367
404
|
myItems: null,
|
|
368
405
|
myItemsError: null,
|
|
406
|
+
myWizard: null,
|
|
407
|
+
myInstall: null,
|
|
408
|
+
myConfirmDeleteId: null,
|
|
369
409
|
}
|
|
370
410
|
}
|
|
371
411
|
|
|
@@ -418,6 +458,7 @@ export function toSyncStoreSlice(s: SyncStoreSlice): SyncStoreSlice {
|
|
|
418
458
|
pushReport: s.pushReport,
|
|
419
459
|
pullReport: s.pullReport,
|
|
420
460
|
confirmSession: s.confirmSession,
|
|
461
|
+
confirmDecisions: s.confirmDecisions,
|
|
421
462
|
lastRestoreId: s.lastRestoreId,
|
|
422
463
|
error: s.error,
|
|
423
464
|
loadError: s.loadError,
|
|
@@ -438,6 +479,9 @@ export function toMarketStoreSlice(s: MarketStoreSlice): MarketStoreSlice {
|
|
|
438
479
|
loadError: s.loadError,
|
|
439
480
|
myItems: s.myItems,
|
|
440
481
|
myItemsError: s.myItemsError,
|
|
482
|
+
myWizard: s.myWizard,
|
|
483
|
+
myInstall: s.myInstall,
|
|
484
|
+
myConfirmDeleteId: s.myConfirmDeleteId,
|
|
441
485
|
}
|
|
442
486
|
}
|
|
443
487
|
|
|
@@ -457,11 +501,20 @@ export function toSnapshotsStoreSlice(s: SnapshotsStoreSlice): SnapshotsStoreSli
|
|
|
457
501
|
* 与不可序列化的实例字段(conflictCollector),以及同步面板的凭据字段
|
|
458
502
|
* (token/webdavPassword/encryptPassword/encryptPasswordConfirm/decryptPassword ——
|
|
459
503
|
* 含 byChannel 内每通道的加密/解密密码)与瞬态字段(busy/savingConfig —— 刷新后
|
|
460
|
-
*
|
|
504
|
+
* 回复空闲,不把「进行中」状态带到新页面);导出面板的结果字段(result/downloaded)
|
|
505
|
+
* 与进行中/进度(running/progress/runId)同为内存切片瞬态一并剔除(刷新/关闭 DSH
|
|
506
|
+
* 后不残留上次导出报告),其余原样落入 sessionStorage。
|
|
461
507
|
* 这是 sessionStorage 的唯一写入路径 —— 敏感值在此被硬性隔离。
|
|
462
508
|
*/
|
|
463
509
|
export function toPersistedState(state: StoreState): PersistedState {
|
|
464
|
-
const {
|
|
510
|
+
const {
|
|
511
|
+
password: _password, passwordConfirm: _passwordConfirm,
|
|
512
|
+
// 导出结果/进度为内存切片瞬态(切 tab 由模块级单例保留、刷新/关闭 DSH 清空):
|
|
513
|
+
// 不落盘,旧导出报告与「已保存」提示不残留展示 —— 与 sync 面板 busy 同级
|
|
514
|
+
result: _result, downloaded: _downloaded, running: _running,
|
|
515
|
+
progress: _progress, runId: _runId,
|
|
516
|
+
...exportRest
|
|
517
|
+
} = state.export
|
|
465
518
|
const {
|
|
466
519
|
secretInputs: _secretInputs, decryptPassword: _decryptPassword, decryptRefs: _decryptRefs,
|
|
467
520
|
archiveUnlocked: _archiveUnlocked, conflictCollector: _conflictCollector, ...importRest
|
|
@@ -753,6 +806,13 @@ export class RunStore {
|
|
|
753
806
|
// 硬性:密码/确认密码绝不从存储恢复
|
|
754
807
|
password: '',
|
|
755
808
|
passwordConfirm: '',
|
|
809
|
+
// 硬性:导出结果/进度/进行中为内存切片瞬态,绝不从存储恢复(即便旧版持久化
|
|
810
|
+
// 载荷携带这些字段也清空)—— 刷新/关闭 DSH 重开后导出页干净,不残留上次结果
|
|
811
|
+
result: null,
|
|
812
|
+
downloaded: false,
|
|
813
|
+
running: false,
|
|
814
|
+
progress: null,
|
|
815
|
+
runId: null,
|
|
756
816
|
},
|
|
757
817
|
import: {
|
|
758
818
|
...defaultImportState(),
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from './sync-view.ts'
|
|
20
20
|
import type { ApplyItemsResponse } from './sync-api.ts'
|
|
21
21
|
import type { TranslateNS } from '../client-types.ts'
|
|
22
|
+
import type { SyncConfirmDecisions } from '../run-store.ts'
|
|
22
23
|
import css from '../config-manager.module.css'
|
|
23
24
|
|
|
24
25
|
export interface SyncConfirmViewProps {
|
|
@@ -33,6 +34,10 @@ export interface SyncConfirmViewProps {
|
|
|
33
34
|
compatibility?: 'excellent' | 'good' | 'partial' | 'unsupported'
|
|
34
35
|
/** 翻译器。 */
|
|
35
36
|
t: TranslateNS<'config-manager-sync'>
|
|
37
|
+
/** 逐项决策(受控:来自 runStore.sync.confirmDecisions;null = 无持久化决策,按 defaultAdopt 初始化) */
|
|
38
|
+
decisions: SyncConfirmDecisions | null
|
|
39
|
+
/** 决策变更上抛(SyncSettingsView patch 镜像 runStore,切 tab/刷新不丢) */
|
|
40
|
+
onDecisionsChange: (decisions: SyncConfirmDecisions) => void
|
|
36
41
|
/** 用户取消确认后回调(复位到空闲态)。 */
|
|
37
42
|
onCancel: () => void
|
|
38
43
|
/** 回滚成功后回调(清空 lastRestoreId)。 */
|
|
@@ -49,13 +54,17 @@ interface ItemState {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export function SyncConfirmView(props: SyncConfirmViewProps): ReactNode {
|
|
52
|
-
const { api, syncSessionId, snapshotId, items, needsReview, compatibility, t, onCancel, onRollbackDone } = props;
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
for (const it of items)
|
|
57
|
-
return
|
|
58
|
-
|
|
57
|
+
const { api, syncSessionId, snapshotId, items, needsReview, compatibility, t, decisions, onDecisionsChange, onCancel, onRollbackDone } = props;
|
|
58
|
+
// 逐项决策状态(受控:来自 props.decisions;null 时按 defaultAdopt 初始化;持久化决策覆盖同名项)
|
|
59
|
+
const states = useMemo<Record<string, ItemState>>(() => {
|
|
60
|
+
const base: Record<string, ItemState> = {};
|
|
61
|
+
for (const it of items) base[it.itemId] = { adopted: it.defaultAdopt, resolution: undefined };
|
|
62
|
+
if (decisions === null) return base;
|
|
63
|
+
for (const [id, d] of Object.entries(decisions)) {
|
|
64
|
+
if (base[id] !== undefined) base[id] = { adopted: d.adopted, resolution: d.resolution };
|
|
65
|
+
}
|
|
66
|
+
return base;
|
|
67
|
+
}, [items, decisions]);
|
|
59
68
|
// 执行阶段:'idle' | 'applying' | 'done' | 'failed' | 'cancelling'
|
|
60
69
|
const [phase, setPhase] = useState<'idle' | 'applying' | 'done' | 'failed' | 'cancelling'>('idle');
|
|
61
70
|
const [applyResult, setApplyResult] = useState<ApplyItemsResponse | null>(null);
|
|
@@ -69,27 +78,24 @@ export function SyncConfirmView(props: SyncConfirmViewProps): ReactNode {
|
|
|
69
78
|
const displayItems = useMemo(() => reviewItems(items), [items]);
|
|
70
79
|
|
|
71
80
|
const setAdopted = (itemId: string, adopted: boolean): void => {
|
|
72
|
-
|
|
81
|
+
const existing: ItemState = states[itemId] ?? { adopted: false, resolution: undefined };
|
|
82
|
+
onDecisionsChange({ ...(decisions ?? {}), [itemId]: { adopted, resolution: existing.resolution } });
|
|
73
83
|
};
|
|
74
84
|
|
|
75
85
|
const setResolution = (itemId: string, resolution: ConflictResolution): void => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return { ...prev, [itemId]: { ...existing, resolution } };
|
|
79
|
-
});
|
|
86
|
+
const existing: ItemState = states[itemId] ?? { adopted: false, resolution: undefined };
|
|
87
|
+
onDecisionsChange({ ...(decisions ?? {}), [itemId]: { adopted: existing.adopted, resolution } });
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
// 批量决策(仅作用于 Conflict 项;非 Conflict 项的 adopt 保持默认)
|
|
83
|
-
const applyBulkDecision = (
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return next;
|
|
92
|
-
});
|
|
91
|
+
const applyBulkDecision = (bulk: readonly { itemId: string; resolution: ConflictResolution; adopt: boolean }[]): void => {
|
|
92
|
+
if (bulk.length === 0) return;
|
|
93
|
+
const next: SyncConfirmDecisions = { ...(decisions ?? {}) };
|
|
94
|
+
for (const d of bulk) {
|
|
95
|
+
const existing: ItemState = states[d.itemId] ?? { adopted: false, resolution: undefined };
|
|
96
|
+
next[d.itemId] = { ...existing, resolution: d.resolution, adopted: d.adopt };
|
|
97
|
+
}
|
|
98
|
+
onDecisionsChange(next);
|
|
93
99
|
};
|
|
94
100
|
|
|
95
101
|
const runApply = async (): Promise<void> => {
|