dsh-config-manager 0.1.76 → 0.1.78
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/plugin-data-declared.d.ts +12 -0
- package/lib/adapters/plugin-data-declared.js +57 -0
- package/lib/adapters/plugin-data.d.ts +4 -6
- package/lib/adapters/plugin-data.js +8 -36
- package/lib/adapters/plugins.d.ts +16 -0
- package/lib/adapters/plugins.js +223 -16
- package/lib/client.d.ts +104 -9
- package/lib/client.js +1800 -718
- package/lib/core/analyzer.js +3 -1
- package/lib/core/backup.d.ts +9 -0
- package/lib/core/backup.js +16 -1
- package/lib/core/messages.d.ts +11 -0
- package/lib/core/messages.js +22 -0
- package/lib/core/plugin-cli.d.ts +26 -0
- package/lib/core/plugin-cli.js +62 -2
- package/lib/core/rollback-attribution.d.ts +81 -0
- package/lib/core/rollback-attribution.js +236 -0
- package/lib/core/rollback-merge.d.ts +22 -0
- package/lib/core/rollback-merge.js +203 -0
- package/lib/core/rollback-point.d.ts +114 -0
- package/lib/core/rollback-point.js +534 -17
- package/lib/core/types.d.ts +28 -0
- package/lib/index.d.ts +26 -3
- package/lib/index.js +129 -16
- package/lib/routes/recovery.js +11 -0
- package/lib/schema/section-registry.js +5 -5
- package/lib/schema/types.d.ts +14 -0
- package/lib/ui/badge-tone.d.ts +16 -0
- package/lib/ui/badge-tone.js +20 -0
- package/lib/ui/i18n.d.ts +4 -4
- package/lib/ui/i18n.js +8 -8
- package/lib/ui/rollback-view.d.ts +95 -0
- package/lib/ui/rollback-view.js +55 -0
- package/lib/ui/selection-model.d.ts +140 -1
- package/lib/ui/selection-model.js +241 -9
- package/package.json +1 -1
- package/src/adapters/plugin-data-declared.ts +60 -0
- package/src/adapters/plugin-data.ts +7 -33
- package/src/adapters/plugins.test.ts +162 -0
- package/src/adapters/plugins.ts +236 -17
- package/src/adapters/units.test.ts +26 -0
- package/src/client/chat/ChatEntryView.tsx +147 -46
- package/src/client/chat/chat-entry-view.test.ts +277 -1
- package/src/client/chat/chat-entry-view.ts +98 -4
- package/src/client/chat/chat-node-registration.ts +71 -0
- package/src/client/chat/chat-slots.ts +30 -12
- package/src/client/common/ContentPicker.tsx +581 -175
- package/src/client/common/host-primitives.ts +64 -1
- package/src/client/common/ui.tsx +17 -3
- package/src/client/config-manager.module.css +630 -397
- package/src/client/css-scale-guard.test.ts +41 -3
- package/src/client/css-token-guard.test.ts +10 -4
- package/src/client/export/ExportView.tsx +2 -0
- package/src/client/index.ts +15 -3
- package/src/client/locales.ts +54 -10
- package/src/client/recovery/RollbackCard.tsx +56 -2
- package/src/client/recovery/RollbackDiffPane.tsx +5 -0
- package/src/client/recovery/RollbackFileChanges.tsx +46 -2
- package/src/client/recovery/recovery-locales.ts +22 -0
- package/src/client/recovery/rollback-file-changes.test.ts +1 -1
- package/src/core/analyzer.ts +3 -1
- package/src/core/backup.ts +16 -1
- package/src/core/messages.ts +22 -0
- package/src/core/plugin-cli.fs.test.ts +65 -3
- package/src/core/plugin-cli.ts +55 -2
- package/src/core/rollback-attribution.test.ts +120 -0
- package/src/core/rollback-attribution.ts +267 -0
- package/src/core/rollback-merge.test.ts +133 -0
- package/src/core/rollback-merge.ts +213 -0
- package/src/core/rollback-point.test.ts +412 -0
- package/src/core/rollback-point.ts +560 -20
- package/src/core/types.ts +29 -0
- package/src/index.facade.test.ts +60 -0
- package/src/index.ts +133 -19
- package/src/routes/recovery.ts +11 -0
- package/src/schema/section-registry.ts +5 -5
- package/src/schema/types.ts +14 -0
- package/src/ui/badge-tone.test.ts +23 -0
- package/src/ui/badge-tone.ts +27 -0
- package/src/ui/i18n.ts +8 -8
- package/src/ui/rollback-view.test.ts +27 -0
- package/src/ui/rollback-view.ts +139 -0
- package/src/ui/selection-model.test.ts +240 -2
- package/src/ui/selection-model.ts +349 -1
|
@@ -243,6 +243,23 @@ export interface RollbackPreviewFile {
|
|
|
243
243
|
removed: number | null;
|
|
244
244
|
/** true = 本次没有可信行数(added/removed 均为 null)。**宁可说「不知道」,也绝不谎报行数。** */
|
|
245
245
|
truncated: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* true = 该文件在留点**之后**是**别的会话**写的(写入归属台账证明;见 core/rollback-attribution.ts)。
|
|
248
|
+
* 界面据此默认不勾选它(用户仍可勾上)。缺字段 / false = **没有归属证据**,
|
|
249
|
+
* 而不是「本会话写的」——「未判定」与「确知是本会话」必须分开。
|
|
250
|
+
*/
|
|
251
|
+
foreign?: boolean;
|
|
252
|
+
/** 那些「别的会话」的会话 id(仅当 foreign 为 true) */
|
|
253
|
+
foreignSessionId?: string;
|
|
254
|
+
/** 那次写入的时间(毫秒;仅当 foreign 为 true) */
|
|
255
|
+
foreignAt?: number;
|
|
256
|
+
/**
|
|
257
|
+
* true = 这次回退**不会改动它**(与其它对话的改动冲突 / 无法安全合并)。行级统计为 null ——
|
|
258
|
+
* 「不改动」不是「0 行变化」,界面必须分开说。
|
|
259
|
+
*/
|
|
260
|
+
skipped?: boolean;
|
|
261
|
+
/** 不改动的机器原因(界面映射成字典键,绝不裸渲染) */
|
|
262
|
+
mergeReason?: string;
|
|
246
263
|
}
|
|
247
264
|
export interface RollbackWorkspacePreview {
|
|
248
265
|
/** 将被写回(覆盖)的条目数 */
|
|
@@ -364,6 +381,12 @@ export interface RollbackFileDiffResult {
|
|
|
364
381
|
identical: boolean;
|
|
365
382
|
degraded: boolean;
|
|
366
383
|
reason?: RollbackFileDiffReason;
|
|
384
|
+
/** true = 「回退后」一侧显示的是三方合并的结果(不是记录里的原文) */
|
|
385
|
+
merged?: boolean;
|
|
386
|
+
/** true = 这次回退不会改动它(冲突 / 无法安全合并)——此时两侧相同 */
|
|
387
|
+
mergeSkipped?: boolean;
|
|
388
|
+
/** 不改动的机器原因(界面映射成字典键) */
|
|
389
|
+
mergeReason?: string;
|
|
367
390
|
}
|
|
368
391
|
export interface RollbackSegmentResult {
|
|
369
392
|
state: string;
|
|
@@ -389,6 +412,22 @@ export interface RollbackSegmentResult {
|
|
|
389
412
|
createdRefusedReason?: string | null;
|
|
390
413
|
/** 工作区段:实际写回的字节数(宿主回传才有) */
|
|
391
414
|
writtenBytes?: number;
|
|
415
|
+
/** 因归属隔离(B)跳过的记录条目数:该文件在留点后被**别的会话**写过(宿主回传才有) */
|
|
416
|
+
foreignSkipped?: number;
|
|
417
|
+
/** 因路径选择(A)跳过的记录条目数(宿主回传才有) */
|
|
418
|
+
selectionSkipped?: number;
|
|
419
|
+
/** 三方合并成功、写回「撤销本会话 + 保留对方改动」的条目数(宿主回传才有) */
|
|
420
|
+
mergedCount?: number;
|
|
421
|
+
mergedPaths?: string[];
|
|
422
|
+
/** 冲突:两侧改到同一处 ⇒ 未写(宿主回传才有) */
|
|
423
|
+
conflictCount?: number;
|
|
424
|
+
conflictPaths?: string[];
|
|
425
|
+
/** 无法安全撤销 ⇒ 未写(宿主回传才有) */
|
|
426
|
+
unavailableCount?: number;
|
|
427
|
+
unavailablePaths?: string[];
|
|
428
|
+
/** 写入前发现又被改过(并发)⇒ 未写(宿主回传才有) */
|
|
429
|
+
concurrentCount?: number;
|
|
430
|
+
concurrentPaths?: string[];
|
|
392
431
|
/** 兼容形态:宿主只回传计数时也算得出来 */
|
|
393
432
|
createdAfter?: RollbackCount;
|
|
394
433
|
createdAfterTruncated?: boolean;
|
|
@@ -427,6 +466,13 @@ export interface RollbackRewindOptions {
|
|
|
427
466
|
segments?: readonly RollbackState[];
|
|
428
467
|
allowPartial?: boolean;
|
|
429
468
|
preRewindGuard?: RollbackGuardPolicy;
|
|
469
|
+
/**
|
|
470
|
+
* 路径级选择性恢复(2026-10-17 定案 A):只恢复这些路径。
|
|
471
|
+
*
|
|
472
|
+
* 缺省 = 全量口径(一字不变)。**空数组是无效输入**(宿主 400)—— 「一个都没勾」由界面拦住,
|
|
473
|
+
* 绝不让它变成「整次回退静默 noop」。
|
|
474
|
+
*/
|
|
475
|
+
onlyPaths?: readonly string[];
|
|
430
476
|
}
|
|
431
477
|
/** 端口:由 src/client/recovery/recovery-api.ts 实现(Partial 消费,缺失时界面如实说能力缺失)。 */
|
|
432
478
|
export interface RollbackPointPort {
|
|
@@ -660,6 +706,39 @@ export declare function rollbackPreviewScope(files: readonly {
|
|
|
660
706
|
added: number | null;
|
|
661
707
|
removed: number | null;
|
|
662
708
|
}[], removals: readonly unknown[]): RollbackPreviewScope;
|
|
709
|
+
/**
|
|
710
|
+
* 预览里「能被单独勾选」的一行 —— `workspace.files` 的逐条映射(**只含会被改动的文件**)。
|
|
711
|
+
*
|
|
712
|
+
* 为什么要有这一层(2026-10-17 定案 A+B):A 要求用户能**逐文件**决定这次动哪些;
|
|
713
|
+
* B(写入归属)给出「哪几个文件在留点之后是别的会话写的」,用来决定**默认**勾不勾。
|
|
714
|
+
* 两件事都收敛在这里、只在这一处(框架无关、node 可测),组件只负责渲染与点击。
|
|
715
|
+
*/
|
|
716
|
+
export interface RollbackSelectionRow {
|
|
717
|
+
path: string;
|
|
718
|
+
/** 目录前缀 + 基名(与文件行同口径;展示用) */
|
|
719
|
+
parts: RollbackPathParts;
|
|
720
|
+
/** true = 该文件在留点**之后**是别的会话写的(引擎 foreign 标记;缺字段 = 没有归属证据) */
|
|
721
|
+
foreign: boolean;
|
|
722
|
+
/** 那些会话的 id(仅 foreign 时给得出) */
|
|
723
|
+
foreignSessionId?: string;
|
|
724
|
+
}
|
|
725
|
+
/** 从预览推出可勾选行(只读;绝不改任何判定) */
|
|
726
|
+
export declare function rollbackSelectionRows(preview: RollbackPreviewResult | null): RollbackSelectionRow[];
|
|
727
|
+
/**
|
|
728
|
+
* 默认勾选 = **非 foreign** 的行(B 的自动隔离口径)。用户可在弹窗里逐个改回去。
|
|
729
|
+
*
|
|
730
|
+
* 为什么要在这里定而不在组件里:同一条默认值要在面板与聊天两个入口一致,
|
|
731
|
+
* 且「默认排除别的会话的改动」是产品决定,必须可测。
|
|
732
|
+
*/
|
|
733
|
+
export declare function rollbackDefaultSelection(rows: readonly RollbackSelectionRow[]): string[];
|
|
734
|
+
/**
|
|
735
|
+
* 勾选集 → `onlyPaths`:
|
|
736
|
+
* · **全选**(含空清单)⇒ `null`(不传字段 = 走既有全量口径,老宿主也照样工作);
|
|
737
|
+
* · 真的少勾 ⇒ 把选中的清单原样传给宿主(空数组由调用方拦:宿主侧它是 400)。
|
|
738
|
+
*/
|
|
739
|
+
export declare function rollbackOnlyPaths(rows: readonly RollbackSelectionRow[], selected: readonly string[]): string[] | null;
|
|
740
|
+
/** 该不该拦住提交:清单非空、却一个都没勾(宿主会把空数组当 400) */
|
|
741
|
+
export declare function rollbackSelectionBlocksSubmit(rows: readonly RollbackSelectionRow[], selected: readonly string[]): boolean;
|
|
663
742
|
/**
|
|
664
743
|
* 差异无法逐行展示时的字典键(本层**只回键**,文案在字典里)。
|
|
665
744
|
* 未知 / 缺失原因回落 null —— 调用方按「未知原因」出话,绝不回落成「没有差异」(那是另一件事)。
|
|
@@ -780,6 +859,22 @@ export interface RollbackRewindRowView {
|
|
|
780
859
|
/** 逐条事实:**渲染前已逐条过 redact** */
|
|
781
860
|
detail: string[];
|
|
782
861
|
guidance: RollbackGuidanceRowView[];
|
|
862
|
+
/**
|
|
863
|
+
* 归属隔离(B)跳过的文件数:这些文件在留点之后是**别的会话**写的,回退没动它们。
|
|
864
|
+
* 0 = 没有(或宿主没回传)—— 界面只在 > 0 时说这句话(绝不编造 0 的成就)。
|
|
865
|
+
*/
|
|
866
|
+
foreignSkipped: number;
|
|
867
|
+
/** 路径选择(A)跳过的文件数(用户自己没勾) */
|
|
868
|
+
selectionSkipped: number;
|
|
869
|
+
/** 三方合并成功、写回「撤销本会话 + 保留对方改动」的文件数 */
|
|
870
|
+
mergedCount: number;
|
|
871
|
+
/** 冲突(两侧改到同一处)未写的文件数 + 有界清单 */
|
|
872
|
+
conflictCount: number;
|
|
873
|
+
conflictPaths: string[];
|
|
874
|
+
/** 无法安全撤销(无中间版本 / 二进制 / 降级 / 对方删了它)未写的文件数 */
|
|
875
|
+
unavailableCount: number;
|
|
876
|
+
/** 写入前发现又被改过(并发)未写的文件数 */
|
|
877
|
+
concurrentCount: number;
|
|
783
878
|
}
|
|
784
879
|
export declare function rollbackRewindRows(result: RollbackRewindResult | null): RollbackRewindRowView[];
|
|
785
880
|
/** 回退总体结论 → 字典键 + 徽章语义(未知回落 unknown,绝不裸渲染 outcome)。 */
|
package/lib/ui/rollback-view.js
CHANGED
|
@@ -419,6 +419,54 @@ export function rollbackPreviewScope(files, removals) {
|
|
|
419
419
|
}
|
|
420
420
|
return { files: files.length, added, removed, removals: removals.length };
|
|
421
421
|
}
|
|
422
|
+
/** 从预览推出可勾选行(只读;绝不改任何判定) */
|
|
423
|
+
export function rollbackSelectionRows(preview) {
|
|
424
|
+
const files = preview?.workspace?.files;
|
|
425
|
+
if (!Array.isArray(files))
|
|
426
|
+
return [];
|
|
427
|
+
const out = [];
|
|
428
|
+
for (const file of files) {
|
|
429
|
+
if (file === null || typeof file !== 'object')
|
|
430
|
+
continue;
|
|
431
|
+
const path = typeof file.path === 'string' ? file.path : '';
|
|
432
|
+
if (path === '')
|
|
433
|
+
continue;
|
|
434
|
+
const row = { path, parts: splitRollbackPath(path), foreign: file.foreign === true };
|
|
435
|
+
if (row.foreign && typeof file.foreignSessionId === 'string' && file.foreignSessionId !== '') {
|
|
436
|
+
row.foreignSessionId = file.foreignSessionId;
|
|
437
|
+
}
|
|
438
|
+
out.push(row);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* 默认勾选 = **非 foreign** 的行(B 的自动隔离口径)。用户可在弹窗里逐个改回去。
|
|
444
|
+
*
|
|
445
|
+
* 为什么要在这里定而不在组件里:同一条默认值要在面板与聊天两个入口一致,
|
|
446
|
+
* 且「默认排除别的会话的改动」是产品决定,必须可测。
|
|
447
|
+
*/
|
|
448
|
+
export function rollbackDefaultSelection(rows) {
|
|
449
|
+
return rows.filter((row) => !row.foreign).map((row) => row.path);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* 勾选集 → `onlyPaths`:
|
|
453
|
+
* · **全选**(含空清单)⇒ `null`(不传字段 = 走既有全量口径,老宿主也照样工作);
|
|
454
|
+
* · 真的少勾 ⇒ 把选中的清单原样传给宿主(空数组由调用方拦:宿主侧它是 400)。
|
|
455
|
+
*/
|
|
456
|
+
export function rollbackOnlyPaths(rows, selected) {
|
|
457
|
+
if (rows.length === 0)
|
|
458
|
+
return null;
|
|
459
|
+
const chosen = new Set(selected);
|
|
460
|
+
const picked = rows.filter((row) => chosen.has(row.path)).map((row) => row.path);
|
|
461
|
+
return picked.length === rows.length ? null : picked;
|
|
462
|
+
}
|
|
463
|
+
/** 该不该拦住提交:清单非空、却一个都没勾(宿主会把空数组当 400) */
|
|
464
|
+
export function rollbackSelectionBlocksSubmit(rows, selected) {
|
|
465
|
+
if (rows.length === 0)
|
|
466
|
+
return false;
|
|
467
|
+
const chosen = new Set(selected);
|
|
468
|
+
return !rows.some((row) => chosen.has(row.path));
|
|
469
|
+
}
|
|
422
470
|
/**
|
|
423
471
|
* 差异无法逐行展示时的字典键(本层**只回键**,文案在字典里)。
|
|
424
472
|
* 未知 / 缺失原因回落 null —— 调用方按「未知原因」出话,绝不回落成「没有差异」(那是另一件事)。
|
|
@@ -682,6 +730,13 @@ export function rollbackRewindRows(result) {
|
|
|
682
730
|
childSessionId,
|
|
683
731
|
detail: redactLines(segment.detail),
|
|
684
732
|
guidance: rollbackGuidanceRows(segment.guidance),
|
|
733
|
+
foreignSkipped: numberOrNull(segment.foreignSkipped) ?? 0,
|
|
734
|
+
selectionSkipped: numberOrNull(segment.selectionSkipped) ?? 0,
|
|
735
|
+
mergedCount: numberOrNull(segment.mergedCount) ?? 0,
|
|
736
|
+
conflictCount: numberOrNull(segment.conflictCount) ?? 0,
|
|
737
|
+
conflictPaths: redactLines(segment.conflictPaths),
|
|
738
|
+
unavailableCount: numberOrNull(segment.unavailableCount) ?? 0,
|
|
739
|
+
concurrentCount: numberOrNull(segment.concurrentCount) ?? 0,
|
|
685
740
|
};
|
|
686
741
|
});
|
|
687
742
|
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* 单元 id 由宿主侧 ConfigAdapter.listUnits() 产出,与导入侧 PlanItem.id 同一命名空间。
|
|
16
16
|
*/
|
|
17
17
|
import type { SectionId } from '../schema/types.ts';
|
|
18
|
-
import type { ImportPlan, PlanItem, PlanItemKind, Portability } from '../core/types.ts';
|
|
18
|
+
import type { ImportPlan, PlanItem, PlanItemKind, PluginAreaRef, Portability } from '../core/types.ts';
|
|
19
|
+
import type { ExportGroup } from './types.ts';
|
|
19
20
|
/** 一个可单独勾选的最小单元(来自 /export-preview 的 items)。 */
|
|
20
21
|
export interface SelectionUnit {
|
|
21
22
|
id: string;
|
|
@@ -63,6 +64,14 @@ export interface SelectionUnit {
|
|
|
63
64
|
* 而不是把 `session-<uuid>` 当标题(用户要求「始终用对话标题」)。文案由界面按当前语言解析。
|
|
64
65
|
*/
|
|
65
66
|
blank?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* 该插件**声明的**第三方数据区域(宿主下发;只有 `plugin:<包名>` 单元可能有值)。
|
|
69
|
+
*
|
|
70
|
+
* 用途:「按插件看」视图的树骨架 —— 静态知识(内置来源表 + 插件自述 dsh.dataDirs),
|
|
71
|
+
* **零数据目录 I/O**;体积与「磁盘上到底有没有」等该分区清单到货后再填。
|
|
72
|
+
* 纯展示字段,不参与勾选/导出契约(契约只认 id)。缺省 = 该插件没有已知关联(不猜)。
|
|
73
|
+
*/
|
|
74
|
+
relatedAreas?: readonly PluginAreaRef[];
|
|
66
75
|
}
|
|
67
76
|
/** 一个分区及其可勾选单元;units 为空 = 本分区不可细分(UI 只给整体开关)。 */
|
|
68
77
|
export interface SelectionSection {
|
|
@@ -91,6 +100,14 @@ export interface SelectionSection {
|
|
|
91
100
|
portability?: Portability;
|
|
92
101
|
/** 涉及秘密状态(如 credentialsStatus):安全提示用,绝不显示值。 */
|
|
93
102
|
sensitive?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 分区所属的**导出分组**(`ui/types.ts` 的 EXPORT_GROUPS)。
|
|
105
|
+
*
|
|
106
|
+
* 用途:选择器在分组切换处插入标题行(General / 插件与扩展 / …)。渲染顺序**由调用方给的顺序
|
|
107
|
+
* 决定**(宿主目录本来已按分组排好,UI 不重排)。缺省 = 不渲染分组标题 ——
|
|
108
|
+
* 导入向导与市场通道的节点没有这个概念,行为逐像素不变。
|
|
109
|
+
*/
|
|
110
|
+
group?: ExportGroup;
|
|
94
111
|
}
|
|
95
112
|
/** 唯一的导出选择状态。 */
|
|
96
113
|
export interface Selection {
|
|
@@ -382,3 +399,125 @@ export interface ImportSelectionState {
|
|
|
382
399
|
export declare function effectiveImportSelection(plan: ImportPlan | null, zipPath: string | null, state: ImportSelectionState | null): Selection | null;
|
|
383
400
|
/** 生效的导入计划(选择裁剪后的子计划);plan 为空时返回 null。 */
|
|
384
401
|
export declare function effectiveImportPlan(plan: ImportPlan | null, zipPath: string | null, state: ImportSelectionState | null): ImportPlan | null;
|
|
402
|
+
/** 树上一个叶子的种类:插件清单条目 / 配置类数据区域 / 大文件区域 */
|
|
403
|
+
export type PluginLeafKind = 'plugin' | 'config' | 'assets';
|
|
404
|
+
export interface PluginTreeLeaf {
|
|
405
|
+
kind: PluginLeafKind;
|
|
406
|
+
section: SectionId;
|
|
407
|
+
/** 单元 id(present 时可勾;unread / absent 时只是占位,不可勾) */
|
|
408
|
+
unitId: string;
|
|
409
|
+
/** 宿主直出文本:插件包名 / 区域 home 相对路径 */
|
|
410
|
+
label: string;
|
|
411
|
+
/** 单元副标题(版本等;只有已读到清单时才有) */
|
|
412
|
+
detail?: string;
|
|
413
|
+
sizeBytes: number;
|
|
414
|
+
/** present=清单里确有该单元(可勾);unread=该分区清单还没读;absent=已读但没有这个单元 */
|
|
415
|
+
state: 'present' | 'unread' | 'absent';
|
|
416
|
+
}
|
|
417
|
+
export interface PluginTreeEntry {
|
|
418
|
+
/** 插件包名(= plugins 单元 id 的 `plugin:` 后缀) */
|
|
419
|
+
name: string;
|
|
420
|
+
/** 插件清单条目(该分区还没读到清单时为 undefined) */
|
|
421
|
+
main?: PluginTreeLeaf;
|
|
422
|
+
/** 该插件声明的数据区域(宿主静态表;可能为空数组) */
|
|
423
|
+
areas: PluginTreeLeaf[];
|
|
424
|
+
/** main + areas(渲染顺序:清单条目在前,区域按宿主声明顺序) */
|
|
425
|
+
leaves: PluginTreeLeaf[];
|
|
426
|
+
}
|
|
427
|
+
/** 「无法归属到具体插件」的桶:整块来自某个分区(不猜归属)。 */
|
|
428
|
+
export interface PluginTreeBucket {
|
|
429
|
+
section: SectionId;
|
|
430
|
+
/** 该分区的清单是否已读到(false = 还没读 / 读取失败,界面给「读取该分区」入口) */
|
|
431
|
+
loaded: boolean;
|
|
432
|
+
units: SelectionUnit[];
|
|
433
|
+
}
|
|
434
|
+
export interface PluginTree {
|
|
435
|
+
/** 插件清单是否已读到;未读到时 entries 为空(界面提示先读取插件分区) */
|
|
436
|
+
pluginsLoaded: boolean;
|
|
437
|
+
entries: PluginTreeEntry[];
|
|
438
|
+
/** 桶:无归属的分区单元(插件的补丁行 / pnpm-workspace.yaml;pluginFiles 整块) */
|
|
439
|
+
buckets: PluginTreeBucket[];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 合并进「按插件看」那棵树的分区(导出选择器里「插件与扩展」组的成员)。
|
|
443
|
+
*
|
|
444
|
+
* 这四项的归属键只有 npm 包名(plugin:<包名> ↔ 来源注册表的 packages);self(配置管理器设置)
|
|
445
|
+
* **不在**这里 —— 它不是插件,仍按普通分区行渲染。清单顺序仍由调用方给的节点顺序决定
|
|
446
|
+
* (宿主目录已按 EXPORT_GROUPS 排好,树只在第一个成员处渲染一次)。
|
|
447
|
+
*/
|
|
448
|
+
export declare const PLUGIN_TREE_SECTIONS: readonly SectionId[];
|
|
449
|
+
/** 该分区是否合并进「按插件看」的树(见 PLUGIN_TREE_SECTIONS)。 */
|
|
450
|
+
export declare function isPluginTreeSection(section: SectionId): boolean;
|
|
451
|
+
/** 把选择器分区模型投影成「按插件看」的树(纯函数)。 */
|
|
452
|
+
export declare function pluginTree(nodes: readonly SelectionSection[]): PluginTree;
|
|
453
|
+
/** 树上的搜索过滤:插件名命中 → 保留它全部叶子;否则只保留标签命中的叶子;桶按单元标签过滤。 */
|
|
454
|
+
export declare function filterPluginTree(tree: PluginTree, query: string): PluginTree;
|
|
455
|
+
/** 该插件节点下**真实可勾选**的叶子(unread / absent 不参与勾选)。 */
|
|
456
|
+
export declare function pluginEntryLeaves(entry: PluginTreeEntry): PluginTreeLeaf[];
|
|
457
|
+
/**
|
|
458
|
+
* 该插件名下**已知**的叶子(存在 + 还没读到清单的区域声明)。
|
|
459
|
+
*
|
|
460
|
+
* 计数与三态用它,**不用 pluginEntryLeaves(只含 present)**:否则「已选 n/m」会随清单到货跳动 ——
|
|
461
|
+
* 折叠时显示 1/1、展开(触发了读取)后变成 1/3,用户看到的是「数字自己变了」(2026-10-17 实测反馈)。
|
|
462
|
+
* absent(注册表说有、盘上确实没有)不计入:它永远不可勾,算进去只会让分母虚高。
|
|
463
|
+
*/
|
|
464
|
+
export declare function pluginEntryKnownLeaves(entry: PluginTreeEntry): PluginTreeLeaf[];
|
|
465
|
+
export declare function pluginEntryTotalCount(entry: PluginTreeEntry): number;
|
|
466
|
+
export declare function pluginEntrySelectedCount(sel: Selection, entry: PluginTreeEntry): number;
|
|
467
|
+
/**
|
|
468
|
+
* 插件节点三态(与分区/分组同一套口径,但分母用**已知**叶子)。
|
|
469
|
+
*
|
|
470
|
+
* 还有区域没读到(unread)时最多只能是 `partial`:那些单元此刻勾不上,
|
|
471
|
+
* 报「全选」会让用户以为已经全带上(真正补齐见 ContentPicker 的待补勾队列)。
|
|
472
|
+
*/
|
|
473
|
+
export declare function pluginEntryPickState(sel: Selection, entry: PluginTreeEntry): SectionPickState;
|
|
474
|
+
/**
|
|
475
|
+
* 插件节点级勾选:把它名下**存在的**单元一起勾 / 取消。
|
|
476
|
+
*
|
|
477
|
+
* 逐个走 toggleUnit:分区未勾时「勾一个单元 = 只勾这一个」的既有语义因此自动生效,
|
|
478
|
+
* 不会因为一键操作就把整个 pluginData / pluginAssets 分区静默全选。
|
|
479
|
+
*/
|
|
480
|
+
export declare function togglePluginEntry(sel: Selection, nodes: SelectionSection[], entry: PluginTreeEntry, checked: boolean): Selection;
|
|
481
|
+
/** 树上所有**真实可勾选**的叶子(各插件名下存在单元的合计;不含未归属 / 遗留数据桶)。 */
|
|
482
|
+
export declare function pluginTreeLeaves(tree: PluginTree): PluginTreeLeaf[];
|
|
483
|
+
export declare function pluginTreeLeafCount(tree: PluginTree): number;
|
|
484
|
+
/** 树上**已知**的叶子(存在 + 未读取;不含 absent)—— 一级区块的计数与三态用它,口径同插件节点。 */
|
|
485
|
+
export declare function pluginTreeKnownLeaves(tree: PluginTree): PluginTreeLeaf[];
|
|
486
|
+
export declare function pluginTreeTotalCount(tree: PluginTree): number;
|
|
487
|
+
export declare function pluginTreeSelectedCount(sel: Selection, tree: PluginTree): number;
|
|
488
|
+
/**
|
|
489
|
+
* 「插件」一级区块(把全部插件收在一个可折叠区块里)的三态。
|
|
490
|
+
*
|
|
491
|
+
* 只算**插件名下真实存在的叶子**:未归属 / 遗留数据是「无法归属的东西」,不参与这个勾选框 ——
|
|
492
|
+
* 否则「全选所有插件」会顺手把没人认领的散落配置一起带上。
|
|
493
|
+
*/
|
|
494
|
+
export declare function pluginTreePickState(sel: Selection, tree: PluginTree): SectionPickState;
|
|
495
|
+
/** 一级区块勾选:把所有插件名下存在的单元一起勾 / 取消(逐个走 toggleUnit;含未归属桶吗?不含)。 */
|
|
496
|
+
export declare function togglePluginTree(sel: Selection, nodes: SelectionSection[], tree: PluginTree, checked: boolean): Selection;
|
|
497
|
+
/**
|
|
498
|
+
* 展开「插件」一级区块前还需要读取哪些分区的清单(惰性)。
|
|
499
|
+
*
|
|
500
|
+
* **插件清单自己没读到也要报**(否则一级区块展开后只有一句「先读取插件分区」却没有入口)。
|
|
501
|
+
*/
|
|
502
|
+
export declare function pluginTreeUnreadSections(tree: PluginTree): SectionId[];
|
|
503
|
+
/** 一条待补勾请求(键 = 「插件」一级区块,或某个插件名)。 */
|
|
504
|
+
export interface PluginPendingCheck {
|
|
505
|
+
key: string;
|
|
506
|
+
kind: 'root' | 'entry';
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 待补勾的出队判定(纯函数,node 可测)。
|
|
510
|
+
*
|
|
511
|
+
* 背景:清单是**惰性**读取的 —— 用户勾「插件」一级区块或某个插件时,它名下可能还有区域没读到
|
|
512
|
+
* (unread),那些单元此刻勾不上。若就此结束,用户会以为「全带上了」,实际包里少了数据目录/大文件。
|
|
513
|
+
* 调用方在点击时先勾已知的、把没到齐的键入队;清单到货(nodes 变化)后再调本函数补齐。
|
|
514
|
+
*
|
|
515
|
+
* 判据 = 该节点名下**已无 unread 叶子**(`pluginEntryUnreadSections` / `pluginsLoaded`):
|
|
516
|
+
* 到齐 → 出队并补齐;没到齐 → 留在队列里继续等(绝不猜成「已勾」)。
|
|
517
|
+
*/
|
|
518
|
+
export declare function applyPluginPendingChecks(sel: Selection, nodes: SelectionSection[], tree: PluginTree, pending: readonly PluginPendingCheck[]): {
|
|
519
|
+
selection: Selection;
|
|
520
|
+
remaining: PluginPendingCheck[];
|
|
521
|
+
};
|
|
522
|
+
/** 勾上该插件前还需要读取哪些分区的清单(展开/点击时触发惰性读取)。 */
|
|
523
|
+
export declare function pluginEntryUnreadSections(entry: PluginTreeEntry): SectionId[];
|
|
@@ -59,6 +59,8 @@ export function sectionsFromPreview(preview) {
|
|
|
59
59
|
...(u.projectKey !== undefined ? { projectKey: u.projectKey } : {}),
|
|
60
60
|
...(u.parentSessionId !== undefined ? { parentSessionId: u.parentSessionId } : {}),
|
|
61
61
|
...(u.blank === true ? { blank: true } : {}),
|
|
62
|
+
// 「按插件看」的树骨架(宿主下发的静态区域清单;缺省 = 该插件没有已知关联)
|
|
63
|
+
...(u.relatedAreas !== undefined && u.relatedAreas.length > 0 ? { relatedAreas: u.relatedAreas } : {}),
|
|
62
64
|
})),
|
|
63
65
|
}));
|
|
64
66
|
}
|
|
@@ -776,13 +778,243 @@ export function effectiveImportPlan(plan, zipPath, state) {
|
|
|
776
778
|
const selection = effectiveImportSelection(plan, zipPath, state);
|
|
777
779
|
return plan === null || selection === null ? null : buildSelectedPlan(plan, selection);
|
|
778
780
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
/**
|
|
782
|
+
* 合并进「按插件看」那棵树的分区(导出选择器里「插件与扩展」组的成员)。
|
|
783
|
+
*
|
|
784
|
+
* 这四项的归属键只有 npm 包名(plugin:<包名> ↔ 来源注册表的 packages);self(配置管理器设置)
|
|
785
|
+
* **不在**这里 —— 它不是插件,仍按普通分区行渲染。清单顺序仍由调用方给的节点顺序决定
|
|
786
|
+
* (宿主目录已按 EXPORT_GROUPS 排好,树只在第一个成员处渲染一次)。
|
|
787
|
+
*/
|
|
788
|
+
export const PLUGIN_TREE_SECTIONS = ['plugins', 'pluginFiles', 'pluginData', 'pluginAssets'];
|
|
789
|
+
/** 该分区是否合并进「按插件看」的树(见 PLUGIN_TREE_SECTIONS)。 */
|
|
790
|
+
export function isPluginTreeSection(section) {
|
|
791
|
+
return PLUGIN_TREE_SECTIONS.includes(section);
|
|
792
|
+
}
|
|
793
|
+
/** 插件单元 id 前缀(与 adapters/units.ts 的 `plugin:<包名>` 同一命名空间) */
|
|
794
|
+
const PLUGIN_UNIT_PREFIX = 'plugin:';
|
|
795
|
+
/** 把一条区域引用投影成叶子(按该分区清单是否到货决定 present / unread / absent)。 */
|
|
796
|
+
function areaLeaf(node, ref) {
|
|
797
|
+
const unitId = ref.section + ':' + ref.relPath;
|
|
798
|
+
const kind = ref.role === 'assets' ? 'assets' : 'config';
|
|
799
|
+
const unit = node?.units.find((u) => u.id === unitId);
|
|
800
|
+
if (unit === undefined) {
|
|
801
|
+
// 分区清单没读到 → unread(展开时才去读);读到了却没有这个单元 → absent(注册表说有、盘上没有)
|
|
802
|
+
return {
|
|
803
|
+
kind, section: ref.section, unitId, label: ref.relPath, sizeBytes: 0,
|
|
804
|
+
state: node === undefined || node.unitsUnread === true ? 'unread' : 'absent',
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
kind, section: ref.section, unitId, label: ref.relPath,
|
|
809
|
+
...(unit.detail !== undefined ? { detail: unit.detail } : {}),
|
|
810
|
+
sizeBytes: unit.sizeBytes,
|
|
811
|
+
state: 'present',
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/** 把选择器分区模型投影成「按插件看」的树(纯函数)。 */
|
|
815
|
+
export function pluginTree(nodes) {
|
|
816
|
+
const bySection = new Map();
|
|
817
|
+
for (const n of nodes)
|
|
818
|
+
bySection.set(n.section, n);
|
|
819
|
+
const pluginsNode = bySection.get('plugins');
|
|
820
|
+
const entries = [];
|
|
821
|
+
const extras = [];
|
|
822
|
+
for (const unit of pluginsNode?.units ?? []) {
|
|
823
|
+
if (!unit.id.startsWith(PLUGIN_UNIT_PREFIX)) {
|
|
824
|
+
// 补丁行 / pnpm-workspace.yaml:与插件无归属关系,进桶(在插件视图里也必须可见可选)
|
|
825
|
+
extras.push(unit);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const name = unit.id.slice(PLUGIN_UNIT_PREFIX.length);
|
|
829
|
+
const main = {
|
|
830
|
+
kind: 'plugin', section: 'plugins', unitId: unit.id, label: unit.label,
|
|
831
|
+
...(unit.detail !== undefined ? { detail: unit.detail } : {}),
|
|
832
|
+
sizeBytes: unit.sizeBytes,
|
|
833
|
+
state: 'present',
|
|
834
|
+
};
|
|
835
|
+
const areas = (unit.relatedAreas ?? []).map((ref) => areaLeaf(bySection.get(ref.section), ref));
|
|
836
|
+
entries.push({ name, main, areas, leaves: [main, ...areas] });
|
|
837
|
+
}
|
|
838
|
+
const buckets = [];
|
|
839
|
+
const pluginExtras = bySection.get('plugins');
|
|
840
|
+
if (pluginExtras !== undefined) {
|
|
841
|
+
buckets.push({ section: 'plugins', loaded: pluginExtras.unitsUnread !== true, units: extras });
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 只留一个桶:pluginFiles。
|
|
845
|
+
*
|
|
846
|
+
* 它没有任何包名信息(白名单文件 + `plugin-config/<任意目录名>/`),按目录名猜包名 = 猜错就在树里
|
|
847
|
+
* 勾错东西 ⇒ 整块归「未归属」。**pluginData / pluginAssets 里没被插件声明认领的单元不再单列
|
|
848
|
+
* 「遗留数据」桶**(用户 2026-10-17 明确要求:不需要看到这些)—— 它们仍会随该分区整体勾选导出,
|
|
849
|
+
* 只是不在插件树里单开一行。
|
|
850
|
+
*/
|
|
851
|
+
for (const id of ['pluginFiles']) {
|
|
852
|
+
const node = bySection.get(id);
|
|
853
|
+
if (node === undefined)
|
|
854
|
+
continue;
|
|
855
|
+
buckets.push({ section: id, loaded: node.unitsUnread !== true, units: node.units });
|
|
856
|
+
}
|
|
857
|
+
return { pluginsLoaded: pluginsNode !== undefined && pluginsNode.unitsUnread !== true, entries, buckets };
|
|
858
|
+
}
|
|
859
|
+
/** 树上的搜索过滤:插件名命中 → 保留它全部叶子;否则只保留标签命中的叶子;桶按单元标签过滤。 */
|
|
860
|
+
export function filterPluginTree(tree, query) {
|
|
861
|
+
const q = query.trim().toLowerCase();
|
|
862
|
+
if (q === '')
|
|
863
|
+
return tree;
|
|
864
|
+
const hit = (text) => text.toLowerCase().includes(q);
|
|
865
|
+
const entries = tree.entries
|
|
866
|
+
.map((e) => (hit(e.name) ? e : { ...e, leaves: e.leaves.filter((l) => hit(l.label) || hit(l.detail ?? '')) }))
|
|
867
|
+
.filter((e) => hit(e.name) || e.leaves.length > 0);
|
|
868
|
+
const buckets = tree.buckets
|
|
869
|
+
.map((b) => ({ ...b, units: b.units.filter((u) => hit(u.label) || hit(u.detail ?? '')) }))
|
|
870
|
+
.filter((b) => b.units.length > 0);
|
|
871
|
+
return { ...tree, entries, buckets };
|
|
872
|
+
}
|
|
873
|
+
/** 该插件节点下**真实可勾选**的叶子(unread / absent 不参与勾选)。 */
|
|
874
|
+
export function pluginEntryLeaves(entry) {
|
|
875
|
+
return entry.leaves.filter((l) => l.state === 'present');
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* 该插件名下**已知**的叶子(存在 + 还没读到清单的区域声明)。
|
|
879
|
+
*
|
|
880
|
+
* 计数与三态用它,**不用 pluginEntryLeaves(只含 present)**:否则「已选 n/m」会随清单到货跳动 ——
|
|
881
|
+
* 折叠时显示 1/1、展开(触发了读取)后变成 1/3,用户看到的是「数字自己变了」(2026-10-17 实测反馈)。
|
|
882
|
+
* absent(注册表说有、盘上确实没有)不计入:它永远不可勾,算进去只会让分母虚高。
|
|
883
|
+
*/
|
|
884
|
+
export function pluginEntryKnownLeaves(entry) {
|
|
885
|
+
return entry.leaves.filter((l) => l.state !== 'absent');
|
|
886
|
+
}
|
|
887
|
+
export function pluginEntryTotalCount(entry) {
|
|
888
|
+
return pluginEntryKnownLeaves(entry).length;
|
|
889
|
+
}
|
|
890
|
+
export function pluginEntrySelectedCount(sel, entry) {
|
|
891
|
+
return pluginEntryLeaves(entry).reduce((n, l) => n + (unitChecked(sel, l.section, l.unitId) ? 1 : 0), 0);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* 插件节点三态(与分区/分组同一套口径,但分母用**已知**叶子)。
|
|
895
|
+
*
|
|
896
|
+
* 还有区域没读到(unread)时最多只能是 `partial`:那些单元此刻勾不上,
|
|
897
|
+
* 报「全选」会让用户以为已经全带上(真正补齐见 ContentPicker 的待补勾队列)。
|
|
898
|
+
*/
|
|
899
|
+
export function pluginEntryPickState(sel, entry) {
|
|
900
|
+
const known = pluginEntryKnownLeaves(entry);
|
|
901
|
+
if (known.length === 0)
|
|
902
|
+
return 'none';
|
|
903
|
+
const picked = pluginEntrySelectedCount(sel, entry);
|
|
904
|
+
if (picked === 0)
|
|
905
|
+
return 'none';
|
|
906
|
+
return picked === known.length ? 'all' : 'partial';
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* 插件节点级勾选:把它名下**存在的**单元一起勾 / 取消。
|
|
910
|
+
*
|
|
911
|
+
* 逐个走 toggleUnit:分区未勾时「勾一个单元 = 只勾这一个」的既有语义因此自动生效,
|
|
912
|
+
* 不会因为一键操作就把整个 pluginData / pluginAssets 分区静默全选。
|
|
913
|
+
*/
|
|
914
|
+
export function togglePluginEntry(sel, nodes, entry, checked) {
|
|
915
|
+
let out = sel;
|
|
916
|
+
for (const leaf of pluginEntryLeaves(entry))
|
|
917
|
+
out = toggleUnit(out, nodes, leaf.unitId, checked);
|
|
918
|
+
return out;
|
|
919
|
+
}
|
|
920
|
+
/** 树上所有**真实可勾选**的叶子(各插件名下存在单元的合计;不含未归属 / 遗留数据桶)。 */
|
|
921
|
+
export function pluginTreeLeaves(tree) {
|
|
922
|
+
return tree.entries.flatMap((e) => pluginEntryLeaves(e));
|
|
923
|
+
}
|
|
924
|
+
export function pluginTreeLeafCount(tree) {
|
|
925
|
+
return pluginTreeLeaves(tree).length;
|
|
926
|
+
}
|
|
927
|
+
/** 树上**已知**的叶子(存在 + 未读取;不含 absent)—— 一级区块的计数与三态用它,口径同插件节点。 */
|
|
928
|
+
export function pluginTreeKnownLeaves(tree) {
|
|
929
|
+
return tree.entries.flatMap((e) => pluginEntryKnownLeaves(e));
|
|
930
|
+
}
|
|
931
|
+
export function pluginTreeTotalCount(tree) {
|
|
932
|
+
return pluginTreeKnownLeaves(tree).length;
|
|
933
|
+
}
|
|
934
|
+
export function pluginTreeSelectedCount(sel, tree) {
|
|
935
|
+
return pluginTreeLeaves(tree).reduce((n, l) => n + (unitChecked(sel, l.section, l.unitId) ? 1 : 0), 0);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* 「插件」一级区块(把全部插件收在一个可折叠区块里)的三态。
|
|
939
|
+
*
|
|
940
|
+
* 只算**插件名下真实存在的叶子**:未归属 / 遗留数据是「无法归属的东西」,不参与这个勾选框 ——
|
|
941
|
+
* 否则「全选所有插件」会顺手把没人认领的散落配置一起带上。
|
|
942
|
+
*/
|
|
943
|
+
export function pluginTreePickState(sel, tree) {
|
|
944
|
+
const known = pluginTreeKnownLeaves(tree);
|
|
945
|
+
if (known.length === 0)
|
|
946
|
+
return 'none';
|
|
947
|
+
const picked = pluginTreeSelectedCount(sel, tree);
|
|
948
|
+
if (picked === 0)
|
|
949
|
+
return 'none';
|
|
950
|
+
return picked === known.length ? 'all' : 'partial';
|
|
951
|
+
}
|
|
952
|
+
/** 一级区块勾选:把所有插件名下存在的单元一起勾 / 取消(逐个走 toggleUnit;含未归属桶吗?不含)。 */
|
|
953
|
+
export function togglePluginTree(sel, nodes, tree, checked) {
|
|
954
|
+
let out = sel;
|
|
955
|
+
for (const leaf of pluginTreeLeaves(tree))
|
|
956
|
+
out = toggleUnit(out, nodes, leaf.unitId, checked);
|
|
957
|
+
return out;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* 展开「插件」一级区块前还需要读取哪些分区的清单(惰性)。
|
|
961
|
+
*
|
|
962
|
+
* **插件清单自己没读到也要报**(否则一级区块展开后只有一句「先读取插件分区」却没有入口)。
|
|
963
|
+
*/
|
|
964
|
+
export function pluginTreeUnreadSections(tree) {
|
|
965
|
+
const out = tree.pluginsLoaded ? [] : ['plugins'];
|
|
966
|
+
for (const entry of tree.entries) {
|
|
967
|
+
for (const s of pluginEntryUnreadSections(entry))
|
|
968
|
+
if (!out.includes(s))
|
|
969
|
+
out.push(s);
|
|
970
|
+
}
|
|
971
|
+
// pluginFiles(未归属桶)没读到也要报:它的读取入口挂在「展开这一行」上
|
|
972
|
+
for (const bucket of tree.buckets) {
|
|
973
|
+
if (!bucket.loaded && !out.includes(bucket.section))
|
|
974
|
+
out.push(bucket.section);
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* 待补勾的出队判定(纯函数,node 可测)。
|
|
980
|
+
*
|
|
981
|
+
* 背景:清单是**惰性**读取的 —— 用户勾「插件」一级区块或某个插件时,它名下可能还有区域没读到
|
|
982
|
+
* (unread),那些单元此刻勾不上。若就此结束,用户会以为「全带上了」,实际包里少了数据目录/大文件。
|
|
983
|
+
* 调用方在点击时先勾已知的、把没到齐的键入队;清单到货(nodes 变化)后再调本函数补齐。
|
|
984
|
+
*
|
|
985
|
+
* 判据 = 该节点名下**已无 unread 叶子**(`pluginEntryUnreadSections` / `pluginsLoaded`):
|
|
986
|
+
* 到齐 → 出队并补齐;没到齐 → 留在队列里继续等(绝不猜成「已勾」)。
|
|
987
|
+
*/
|
|
988
|
+
export function applyPluginPendingChecks(sel, nodes, tree, pending) {
|
|
989
|
+
const remaining = [];
|
|
990
|
+
let out = sel;
|
|
991
|
+
for (const p of pending) {
|
|
992
|
+
if (p.kind === 'root') {
|
|
993
|
+
if (!tree.pluginsLoaded || tree.entries.some((e) => pluginEntryUnreadSections(e).length > 0)) {
|
|
994
|
+
remaining.push(p);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
out = togglePluginTree(out, nodes, tree, true);
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const entry = tree.entries.find((e) => e.name === p.key);
|
|
1001
|
+
if (entry === undefined)
|
|
1002
|
+
continue;
|
|
1003
|
+
if (pluginEntryUnreadSections(entry).length > 0) {
|
|
1004
|
+
remaining.push(p);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
out = togglePluginEntry(out, nodes, entry, true);
|
|
1008
|
+
}
|
|
1009
|
+
return { selection: out, remaining };
|
|
1010
|
+
}
|
|
1011
|
+
/** 勾上该插件前还需要读取哪些分区的清单(展开/点击时触发惰性读取)。 */
|
|
1012
|
+
export function pluginEntryUnreadSections(entry) {
|
|
1013
|
+
const out = [];
|
|
1014
|
+
for (const leaf of entry.leaves) {
|
|
1015
|
+
if (leaf.state === 'unread' && !out.includes(leaf.section))
|
|
1016
|
+
out.push(leaf.section);
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
}
|
|
788
1020
|
//# sourceMappingURL=selection-model.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.78",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backup, restore, export, import, migrate and sync your DeepSeek Harness (DSH) configuration: settings, plugins, MCP servers, skills, agent presets and workspaces. One-click migration to another machine. DSH 配置一键备份、恢复、导出、导入、迁移与同步插件。",
|
|
6
6
|
"icon": "./icon.svg",
|