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
package/lib/core/types.d.ts
CHANGED
|
@@ -145,6 +145,24 @@ export interface ExportUnit {
|
|
|
145
145
|
* 只下发**事实**,不下发文案 —— 界面按当前语言显示(与 DSH 同口径),绝不用 uuid 冒充标题。
|
|
146
146
|
*/
|
|
147
147
|
blank?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* 该插件**声明的第三方数据区域**(只有 \`plugin:<包名>\` 单元可能有值;导出选择器「按插件看」用)。
|
|
150
|
+
*
|
|
151
|
+
* 用途:选择器要在**不读任何数据目录**的前提下画出「插件 → 数据目录 / 大文件」的树骨架 ——
|
|
152
|
+
* 区域清单来自来源注册表(内置来源表 + 插件自述 dsh.dataDirs),是静态知识,零额外 I/O;
|
|
153
|
+
* 体积与「实际有没有内容」等该分区的清单到货后再填。
|
|
154
|
+
*
|
|
155
|
+
* 语义:**纯展示**,不参与勾选/导出契约(契约只认 id)。这里是「注册表说它应该在哪儿」,
|
|
156
|
+
* **不是**「磁盘上确实有」—— 界面必须把两者分开渲染(unread / 无内容 / 已读到)。
|
|
157
|
+
*/
|
|
158
|
+
relatedAreas?: readonly PluginAreaRef[];
|
|
159
|
+
}
|
|
160
|
+
/** 插件数据区域引用(pluginData / pluginAssets;只读展示用,不参与勾选契约)。 */
|
|
161
|
+
export interface PluginAreaRef {
|
|
162
|
+
section: 'pluginData' | 'pluginAssets';
|
|
163
|
+
/** home 相对 POSIX 路径 —— 就是该分区单元的 id 后缀(\`pluginData:.mnemon/runtime\` 的末段) */
|
|
164
|
+
relPath: string;
|
|
165
|
+
role: 'config' | 'assets';
|
|
148
166
|
}
|
|
149
167
|
export interface ValidationResult {
|
|
150
168
|
valid: boolean;
|
|
@@ -222,6 +240,16 @@ export interface PluginInfo {
|
|
|
222
240
|
inBundles?: string[];
|
|
223
241
|
/** 声明依赖 spec(profile package.json dependencies 原样);文件视图来自 readInstalled */
|
|
224
242
|
spec?: string;
|
|
243
|
+
/**
|
|
244
|
+
* 只在 `dsh.profile.bundles` 里声明、`dependencies` 里没有该包,且本 profile 的
|
|
245
|
+
* `node_modules` 里也没有它的落盘副本(issue #95)。
|
|
246
|
+
*
|
|
247
|
+
* DSH 对这类层从**自己的安装位置**解析(`boot-safety.ts` 的「@deepseek-ai/* 由 DSH 从它自己的
|
|
248
|
+
* 安装位置解析」与 `RESCUE_KEEP_BUNDLE_PREFIXES` 同一口径),因此导入端**不得**把它当
|
|
249
|
+
* registry 依赖去 `dsh plugin add`:那会把 npm dist-tag `latest` 的副本装进目标 profile,
|
|
250
|
+
* 与 host 的版本线脱节(issue #95 的真机故障)。只能把名字声明进目标机的 bundles。
|
|
251
|
+
*/
|
|
252
|
+
bundlesOnly?: boolean;
|
|
225
253
|
}
|
|
226
254
|
export interface PluginsFacade {
|
|
227
255
|
listInstalled(): Promise<PluginInfo[]>;
|
package/lib/index.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare const name = "config-manager";
|
|
|
78
78
|
/** Services required before the engine can mount (present in every profile). */
|
|
79
79
|
export declare const inject: string[];
|
|
80
80
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
81
|
-
export declare const PLUGIN_VERSION = "0.1.
|
|
81
|
+
export declare const PLUGIN_VERSION = "0.1.78";
|
|
82
82
|
/**
|
|
83
83
|
* Star 引导弹窗指向的 GitHub 仓库(用户引导点 Star 的目标)。
|
|
84
84
|
* 与 package.json 的 repository 字段保持一致;界面不可改(硬编码,参照
|
|
@@ -340,6 +340,11 @@ export declare class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
340
340
|
private readonly ctx;
|
|
341
341
|
/** DSH 缺省会话根(配置改过时只影响快路径命中率,慢路径仍正确) */
|
|
342
342
|
private readonly root;
|
|
343
|
+
/**
|
|
344
|
+
* 插件数据根(`<DSH_HOME>/dsh-config-manager`)—— **只**用于归位失败的诊断落盘。
|
|
345
|
+
* 权威状态仍在 DSH(工作区注册表 / 会话存储),这里绝不旁路写任何 DSH 数据。
|
|
346
|
+
*/
|
|
347
|
+
private readonly dataDir;
|
|
343
348
|
/**
|
|
344
349
|
* 父子关系的短 TTL 缓存(见 `PARENT_RELATIONS_CACHE_TTL_MS`;同 key 并发合并成一次列举)。
|
|
345
350
|
* F-1 后**只缓存成功结果**:列举失败/形状识别失败会让 `parentRelations()` 抛错,
|
|
@@ -472,8 +477,26 @@ export declare class DshSessionStoreFacade implements SessionStoreFacade {
|
|
|
472
477
|
* 宁少不猜(DSH 官方用 `sessionQuery.traceSession`;这里只依赖 `sessions.get(id).header`)。
|
|
473
478
|
*/
|
|
474
479
|
private ancestorSessionIds;
|
|
475
|
-
/**
|
|
476
|
-
|
|
480
|
+
/**
|
|
481
|
+
* 会话 cwd 的**多源**读取(活会话 → 宿主会话存储列举)。
|
|
482
|
+
*
|
|
483
|
+
* 为什么不能只读活会话(真机 2026-10-17 根因之一):旧实现只看 `ctx.get('sessions').get(id)`,
|
|
484
|
+
* 拿不到就整体放弃归位 ⇒ fork 出来的子会话停在侧栏「未分组」。而 DSH 自己的读法有**磁盘回落**
|
|
485
|
+
* (`dsh-workspace` 的 `readSessionHeader`:活会话 → `sessionPersistence.list()` 的 header 缓存),
|
|
486
|
+
* 本方法与该回落同源,绝不猜路径、绝不拿父会话目录以外的任何东西凑数。
|
|
487
|
+
*
|
|
488
|
+
* 失败时返回**具体细节**(哪个来源、为什么),由调用方拼进诊断;绝不静默成 undefined。
|
|
489
|
+
*/
|
|
490
|
+
private sessionCwdOf;
|
|
491
|
+
/**
|
|
492
|
+
* 归位失败的**诊断落盘**(一行 JSON,追加写)。
|
|
493
|
+
*
|
|
494
|
+
* 为什么需要:桌面版 DSH 的宿主 stdout 不落盘 ⇒ 用户界面上只有「未挂回原工作区」这句,
|
|
495
|
+
* 具体理由(registry 拿不到 / attach 被拒 / cwd 读不到 / 复核不过)谁都看不到,
|
|
496
|
+
* 「为什么会进未分组」于是长期不可排查。这里只记 id、理由码与路径来源,**不含任何凭据**。
|
|
497
|
+
* 落盘失败绝不升级成回退失败(诊断是旁路,不是回退的一部分)。
|
|
498
|
+
*/
|
|
499
|
+
private recordAttachDiagnostic;
|
|
477
500
|
/**
|
|
478
501
|
* 活会话 header(`ctx.get('sessions').get(id).header`)。
|
|
479
502
|
*
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
38
38
|
import { execFile } from 'node:child_process';
|
|
39
39
|
import { createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
40
40
|
import fs from 'node:fs/promises';
|
|
41
|
+
import path from 'node:path';
|
|
41
42
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
42
43
|
import { homedir } from 'node:os';
|
|
43
44
|
import { promisify } from 'node:util';
|
|
@@ -64,7 +65,8 @@ import { RunRegistry } from './core/run-registry.js';
|
|
|
64
65
|
import { registerModelTools } from './core/model-tools.js';
|
|
65
66
|
import { msgOf, zhMsg } from './core/messages.js';
|
|
66
67
|
import { engineMsg, getEngineLocale, setEngineLanguageSource, setEngineLocale } from './core/engine-locale.js';
|
|
67
|
-
import { RollbackAutoTracker, RollbackPointEngine, rollbackSessionBindingOf, rollbackTrackedEventOf } from './core/rollback-point.js';
|
|
68
|
+
import { RollbackAutoTracker, RollbackPointEngine, ROLLBACK_POINTS_DIR_NAME, rollbackSessionBindingOf, rollbackTrackedEventOf } from './core/rollback-point.js';
|
|
69
|
+
import { ROLLBACK_ATTRIBUTION_FILE_NAME, RollbackWriteAttribution } from './core/rollback-attribution.js';
|
|
68
70
|
import { isRollbackEnabled, onRollbackEnabledChange, setRollbackEnabled } from './core/rollback-gate.js';
|
|
69
71
|
// t3:宿主斜杠命令 /rollback(与 HTTP 路由同一道写入门;命令服务惰取,未组合时安静跳过)。
|
|
70
72
|
import { agentSessionFactsOf, registerRollbackCommand } from './core/command-rollback.js';
|
|
@@ -136,7 +138,7 @@ export const name = 'config-manager';
|
|
|
136
138
|
/** Services required before the engine can mount (present in every profile). */
|
|
137
139
|
export const inject = ['settings', 'credentials'];
|
|
138
140
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
139
|
-
export const PLUGIN_VERSION = '0.1.
|
|
141
|
+
export const PLUGIN_VERSION = '0.1.78';
|
|
140
142
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
141
143
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
142
144
|
/**
|
|
@@ -646,6 +648,7 @@ export function normalizeListedSession(element) {
|
|
|
646
648
|
const SESSION_STORE_PURPOSE_KEY = {
|
|
647
649
|
reindexSessionHeader: 'host.sessionStorePurposeReindex',
|
|
648
650
|
parentRelations: 'host.sessionStorePurposeParentRelations',
|
|
651
|
+
rewindWorkspaceAttach: 'host.sessionStorePurposeRewindAttach',
|
|
649
652
|
};
|
|
650
653
|
/**
|
|
651
654
|
* 父子关系缓存的 TTL(毫秒)。
|
|
@@ -668,6 +671,18 @@ const PARENT_RELATIONS_CACHE_TTL_MS = 5_000;
|
|
|
668
671
|
const ANCESTOR_LOOKUP_MAX_DEPTH = 8;
|
|
669
672
|
/** 归位失败时给调用方的 warning 串(稳定前缀 + 具体理由码)。 */
|
|
670
673
|
export const WORKSPACE_ATTACH_FAILED_PREFIX = 'workspace-attach-failed';
|
|
674
|
+
/**
|
|
675
|
+
* 归位失败后的重试次数与间隔(毫秒)。
|
|
676
|
+
*
|
|
677
|
+
* 为什么需要(真机 2026-10-17/10-12):fork 返回时子会话刚由 `agents.create` 建好,
|
|
678
|
+
* DSH 侧的会话索引 / 持久化列举可能还没就绪 —— 与「目录根本不存在」这类终结性失败不同,
|
|
679
|
+
* 这是**时序性**失败。归位本身幂等(`attachSession` 自查成员名单、`mutate` 空操作即不改写),
|
|
680
|
+
* 因此重试零副作用;不重试则子会话永久停在侧栏「未分组」。
|
|
681
|
+
*/
|
|
682
|
+
const WORKSPACE_ATTACH_ATTEMPTS = 2;
|
|
683
|
+
const WORKSPACE_ATTACH_RETRY_DELAY_MS = 200;
|
|
684
|
+
/** 归位诊断文件的相对路径(相对插件数据根;只在**失败**时追加一行 JSON)。 */
|
|
685
|
+
const WORKSPACE_ATTACH_DIAGNOSTIC_REL = 'logs/rewind-workspace-attach.jsonl';
|
|
671
686
|
/**
|
|
672
687
|
* DSH 官方 fork 在「子会话已创建、但 `workspace.attachSession` 失败」时抛的错误码。
|
|
673
688
|
*
|
|
@@ -735,6 +750,11 @@ export class DshSessionStoreFacade {
|
|
|
735
750
|
ctx;
|
|
736
751
|
/** DSH 缺省会话根(配置改过时只影响快路径命中率,慢路径仍正确) */
|
|
737
752
|
root;
|
|
753
|
+
/**
|
|
754
|
+
* 插件数据根(`<DSH_HOME>/dsh-config-manager`)—— **只**用于归位失败的诊断落盘。
|
|
755
|
+
* 权威状态仍在 DSH(工作区注册表 / 会话存储),这里绝不旁路写任何 DSH 数据。
|
|
756
|
+
*/
|
|
757
|
+
dataDir;
|
|
738
758
|
/**
|
|
739
759
|
* 父子关系的短 TTL 缓存(见 `PARENT_RELATIONS_CACHE_TTL_MS`;同 key 并发合并成一次列举)。
|
|
740
760
|
* F-1 后**只缓存成功结果**:列举失败/形状识别失败会让 `parentRelations()` 抛错,
|
|
@@ -760,6 +780,7 @@ export class DshSessionStoreFacade {
|
|
|
760
780
|
constructor(ctx, homeDir, msg = zhMsg, log, realpath) {
|
|
761
781
|
this.ctx = ctx;
|
|
762
782
|
this.root = join(homeDir, 'sessions');
|
|
783
|
+
this.dataDir = join(homeDir, 'dsh-config-manager');
|
|
763
784
|
this.log = log ?? createLogger({ level: 'warn' });
|
|
764
785
|
this.msgFunc = msg;
|
|
765
786
|
this.realpath = realpath ?? (async (value) => await fs.realpath(value));
|
|
@@ -819,14 +840,23 @@ export class DshSessionStoreFacade {
|
|
|
819
840
|
childId = recovered;
|
|
820
841
|
}
|
|
821
842
|
// 工作区归位:官方已归位 → 只复核;官方没归位(或 attach 抛错被上面救回)→ 本兜底写;写后一律复核。
|
|
822
|
-
|
|
843
|
+
//
|
|
844
|
+
// 失败要重试(真机 2026-10-17):fork 返回时子会话刚建好,DSH 侧的索引/持久化列举可能还没就绪,
|
|
845
|
+
// 属**时序性**失败;三档判定与 attach 本身幂等,重试零副作用。不重试的代价是子会话永久停在
|
|
846
|
+
// 侧栏「未分组」(用户 2026-10-11 起的反馈)。
|
|
847
|
+
let outcome = await this.attachChildToParentWorkspace(sessionId, childId);
|
|
848
|
+
for (let attempt = 1; attempt < WORKSPACE_ATTACH_ATTEMPTS && !outcome.ok; attempt += 1) {
|
|
849
|
+
await new Promise((resolve) => { setTimeout(resolve, WORKSPACE_ATTACH_RETRY_DELAY_MS); });
|
|
850
|
+
outcome = await this.attachChildToParentWorkspace(sessionId, childId);
|
|
851
|
+
}
|
|
823
852
|
if (outcome.ok) {
|
|
824
853
|
this.log.info('rewind: child session filed into workspace: ' + childId + ' (' + outcome.reason + (outcome.workspaceId !== undefined ? ' ' + outcome.workspaceId : '') + ')');
|
|
825
854
|
return { childSessionId: childId };
|
|
826
855
|
}
|
|
827
856
|
// 归位失败**带具体理由**(绝不静默):日志留痕 + warning 串交给调用方如实上屏。
|
|
828
|
-
|
|
829
|
-
|
|
857
|
+
//
|
|
858
|
+
// 桌面版 DSH 拿不到宿主 stdout ⇒ 只有理由码还不够排查「为什么进未分组」,故再落一行诊断。
|
|
859
|
+
await this.recordAttachDiagnostic({ parentId: sessionId, childId, outcome });
|
|
830
860
|
const warning = workspaceAttachWarningOf(outcome);
|
|
831
861
|
return warning === undefined ? { childSessionId: childId } : { childSessionId: childId, warning };
|
|
832
862
|
}
|
|
@@ -896,12 +926,29 @@ export class DshSessionStoreFacade {
|
|
|
896
926
|
attachFailure ??= attempt;
|
|
897
927
|
}
|
|
898
928
|
// ③ 同目录:cwd 与某工作区 path 指向同一目录(目录同一性由 DSH 判定,见方法注释)
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
929
|
+
//
|
|
930
|
+
// cwd 取**多源候选**(真机 2026-10-17):旧实现只看子会话的活 header,拿不到就直接
|
|
931
|
+
// `cwd-unavailable` ⇒ 三档全落空 ⇒ 子会话停在「未分组」。两条候选都来自 DSH 自己写下的 header,
|
|
932
|
+
// 不是猜测:① 子会话(活 / 落盘 header);② 父会话 —— DSH 的 fork 逐字继承父 cwd
|
|
933
|
+
// (`SessionCommandController.fork`:`meta.cwd = source.header.cwd`,见 dsh-api-session-controller)。
|
|
934
|
+
const candidates = [];
|
|
935
|
+
const missing = [];
|
|
936
|
+
const childLookup = await this.sessionCwdOf(childId);
|
|
937
|
+
if (childLookup.ok)
|
|
938
|
+
candidates.push({ cwd: childLookup.cwd, source: 'child:' + childLookup.source });
|
|
939
|
+
else
|
|
940
|
+
missing.push('child(' + childLookup.detail + ')');
|
|
941
|
+
const parentLookup = await this.sessionCwdOf(parentId);
|
|
942
|
+
if (parentLookup.ok) {
|
|
943
|
+
// 与已入列候选同目录时不重复(同目录再 attach 一次只是白跑一次 list())
|
|
944
|
+
if (!candidates.some((c) => foldWorkspacePath(c.cwd) === foldWorkspacePath(parentLookup.cwd))) {
|
|
945
|
+
candidates.push({ cwd: parentLookup.cwd, source: 'parent:' + parentLookup.source });
|
|
946
|
+
}
|
|
902
947
|
}
|
|
903
|
-
else
|
|
904
|
-
|
|
948
|
+
else
|
|
949
|
+
missing.push('parent(' + parentLookup.detail + ')');
|
|
950
|
+
for (const candidate of candidates) {
|
|
951
|
+
const resolution = await this.resolveWorkspaceByCwd(registry, records, candidate.cwd);
|
|
905
952
|
if (resolution.records !== undefined && resolution.records.length > 0) {
|
|
906
953
|
for (const record of resolution.records) {
|
|
907
954
|
const attempt = await this.attachChildTo(record, childId);
|
|
@@ -911,9 +958,15 @@ export class DshSessionStoreFacade {
|
|
|
911
958
|
}
|
|
912
959
|
}
|
|
913
960
|
else {
|
|
914
|
-
|
|
961
|
+
// 目录不存在 / 不属于任何工作区:按候选来源标注,便于诊断区分「读到的是谁」
|
|
962
|
+
const failure = resolution.failure;
|
|
963
|
+
if (failure !== undefined) {
|
|
964
|
+
cwdFailure ??= { ok: false, reason: failure.reason, detail: candidate.source + ':' + (failure.detail ?? '') };
|
|
965
|
+
}
|
|
915
966
|
}
|
|
916
967
|
}
|
|
968
|
+
if (candidates.length === 0)
|
|
969
|
+
cwdFailure = { ok: false, reason: 'cwd-unavailable', detail: missing.join(';') };
|
|
917
970
|
return attachFailure ?? cwdFailure ?? { ok: false, reason: 'no-workspace-for-cwd' };
|
|
918
971
|
}
|
|
919
972
|
catch (error) {
|
|
@@ -1090,10 +1143,57 @@ export class DshSessionStoreFacade {
|
|
|
1090
1143
|
}
|
|
1091
1144
|
return out;
|
|
1092
1145
|
}
|
|
1093
|
-
/**
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* 会话 cwd 的**多源**读取(活会话 → 宿主会话存储列举)。
|
|
1148
|
+
*
|
|
1149
|
+
* 为什么不能只读活会话(真机 2026-10-17 根因之一):旧实现只看 `ctx.get('sessions').get(id)`,
|
|
1150
|
+
* 拿不到就整体放弃归位 ⇒ fork 出来的子会话停在侧栏「未分组」。而 DSH 自己的读法有**磁盘回落**
|
|
1151
|
+
* (`dsh-workspace` 的 `readSessionHeader`:活会话 → `sessionPersistence.list()` 的 header 缓存),
|
|
1152
|
+
* 本方法与该回落同源,绝不猜路径、绝不拿父会话目录以外的任何东西凑数。
|
|
1153
|
+
*
|
|
1154
|
+
* 失败时返回**具体细节**(哪个来源、为什么),由调用方拼进诊断;绝不静默成 undefined。
|
|
1155
|
+
*/
|
|
1156
|
+
async sessionCwdOf(sessionId) {
|
|
1157
|
+
const live = this.liveSessionHeader(sessionId)?.['cwd'];
|
|
1158
|
+
if (typeof live === 'string' && live !== '')
|
|
1159
|
+
return { ok: true, cwd: live, source: 'live' };
|
|
1160
|
+
const store = readService(this.ctx, 'sessionPersistence');
|
|
1161
|
+
if (store === undefined)
|
|
1162
|
+
return { ok: false, detail: 'no-live-cwd;session-persistence-unavailable' };
|
|
1163
|
+
const listed = await this.listSessionElements(store, 'rewindWorkspaceAttach');
|
|
1164
|
+
if (!listed.ok)
|
|
1165
|
+
return { ok: false, detail: 'no-live-cwd;' + listed.reason };
|
|
1166
|
+
const view = listed.items.find((item) => item.id === sessionId);
|
|
1167
|
+
if (view === undefined)
|
|
1168
|
+
return { ok: false, detail: 'no-live-cwd;not-in-stored-sessions' };
|
|
1169
|
+
const raw = isRecord(view.raw) ? view.raw['cwd'] : undefined;
|
|
1170
|
+
if (typeof raw === 'string' && raw !== '')
|
|
1171
|
+
return { ok: true, cwd: raw, source: 'stored' };
|
|
1172
|
+
return { ok: false, detail: 'no-live-cwd;stored-header-has-no-cwd' };
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* 归位失败的**诊断落盘**(一行 JSON,追加写)。
|
|
1176
|
+
*
|
|
1177
|
+
* 为什么需要:桌面版 DSH 的宿主 stdout 不落盘 ⇒ 用户界面上只有「未挂回原工作区」这句,
|
|
1178
|
+
* 具体理由(registry 拿不到 / attach 被拒 / cwd 读不到 / 复核不过)谁都看不到,
|
|
1179
|
+
* 「为什么会进未分组」于是长期不可排查。这里只记 id、理由码与路径来源,**不含任何凭据**。
|
|
1180
|
+
* 落盘失败绝不升级成回退失败(诊断是旁路,不是回退的一部分)。
|
|
1181
|
+
*/
|
|
1182
|
+
async recordAttachDiagnostic(entry) {
|
|
1183
|
+
try {
|
|
1184
|
+
const target = join(this.dataDir, WORKSPACE_ATTACH_DIAGNOSTIC_REL);
|
|
1185
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
1186
|
+
await fs.appendFile(target, JSON.stringify({
|
|
1187
|
+
at: new Date().toISOString(),
|
|
1188
|
+
parentId: entry.parentId,
|
|
1189
|
+
childId: entry.childId,
|
|
1190
|
+
reason: entry.outcome.reason,
|
|
1191
|
+
...(entry.outcome.detail !== undefined ? { detail: entry.outcome.detail } : {}),
|
|
1192
|
+
}) + '\n');
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
// 诊断写不进去(只读盘 / 权限 / 配额):静默 —— 回退本身的结论不受影响。
|
|
1196
|
+
}
|
|
1097
1197
|
}
|
|
1098
1198
|
/**
|
|
1099
1199
|
* 活会话 header(`ctx.get('sessions').get(id).header`)。
|
|
@@ -4285,10 +4385,19 @@ export function apply(ctx, config) {
|
|
|
4285
4385
|
// 不为了这一处调用去改公共接口;端口上确实有 forkSession(DshSessionStoreFacade 实现)才走
|
|
4286
4386
|
// 真 fork,否则如实结构化 `session-fork-unavailable`(绝不静默降级)。
|
|
4287
4387
|
const forkPort = host.sessions;
|
|
4388
|
+
// 写入归属台账(2026-10-17 定案 B):只记「哪个会话的哪次**写类**工具调用写了哪个路径」。
|
|
4389
|
+
// 惰性读盘 + best-effort 落盘:读不到 / 写不进只让归属退化成「未判定」——
|
|
4390
|
+
// 既不影响留点与回退,也绝不会把「不知道」当成「别的会话写的」(见 rollback-attribution.ts)。
|
|
4391
|
+
const rollbackAttribution = new RollbackWriteAttribution({
|
|
4392
|
+
file: path.join(dataDir, ROLLBACK_POINTS_DIR_NAME, ROLLBACK_ATTRIBUTION_FILE_NAME),
|
|
4393
|
+
warn: (line) => host.log.warn(line),
|
|
4394
|
+
});
|
|
4395
|
+
void rollbackAttribution.load();
|
|
4288
4396
|
const rollbackEngine = new RollbackPointEngine({
|
|
4289
4397
|
dataDir,
|
|
4290
4398
|
homeDir,
|
|
4291
4399
|
log: (line) => host.log.warn(line),
|
|
4400
|
+
attribution: rollbackAttribution,
|
|
4292
4401
|
fork: async ({ sessionId, boundarySeq }) => {
|
|
4293
4402
|
const forkSession = forkPort.forkSession;
|
|
4294
4403
|
if (typeof forkSession !== 'function') {
|
|
@@ -4307,7 +4416,11 @@ export function apply(ctx, config) {
|
|
|
4307
4416
|
return { ok: false, code: 'fork-failed', detail: result.error };
|
|
4308
4417
|
},
|
|
4309
4418
|
});
|
|
4310
|
-
const rollbackTracker = new RollbackAutoTracker({
|
|
4419
|
+
const rollbackTracker = new RollbackAutoTracker({
|
|
4420
|
+
engine: rollbackEngine,
|
|
4421
|
+
log: (line) => host.log.warn(line),
|
|
4422
|
+
attribution: rollbackAttribution,
|
|
4423
|
+
});
|
|
4311
4424
|
ctx.effect(() => {
|
|
4312
4425
|
let chain = Promise.resolve();
|
|
4313
4426
|
// 「会话没有 cwd」这类**形状合法但信息不全**的情况只记一次日志(避免每回合刷屏);
|
package/lib/routes/recovery.js
CHANGED
|
@@ -766,6 +766,17 @@ export function recoveryRoutes(env) {
|
|
|
766
766
|
// A(用户 2026-10-11 定案):「回到某点之前」要连文件一起退回去 —— 默认把**回退点之后新建**的路径删掉;
|
|
767
767
|
// 调用方显式传 false 才保留老口径(只报告 leftovers,不删任何东西)。
|
|
768
768
|
rewindInput.removeCreated = body['removeCreated'] !== false;
|
|
769
|
+
// 路径级选择性恢复(2026-10-17 定案 A):缺省 = 全量口径(一字不变)。
|
|
770
|
+
// 给了数组就只恢复这些路径;**空数组**视为无效输入(400)—— 「一个都没勾」应当由
|
|
771
|
+
// 界面拦住,而不是让引擎把整次回退悄悄变成 noop。
|
|
772
|
+
if (body['onlyPaths'] !== undefined) {
|
|
773
|
+
const raw = body['onlyPaths'];
|
|
774
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((item) => typeof item !== 'string' || item === '')) {
|
|
775
|
+
writeJson(res, 400, { ok: false, code: 'invalid-input', error: 'onlyPaths must be a non-empty array of non-empty strings' });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
rewindInput.onlyPaths = raw;
|
|
779
|
+
}
|
|
769
780
|
if (body['preRewindGuard'] === 'off' || body['preRewindGuard'] === 'require' || body['preRewindGuard'] === 'warn') {
|
|
770
781
|
rewindInput.guardPolicy = body['preRewindGuard'];
|
|
771
782
|
}
|
|
@@ -55,7 +55,7 @@ export const SECTION_REGISTRY = {
|
|
|
55
55
|
},
|
|
56
56
|
plugins: {
|
|
57
57
|
id: 'plugins',
|
|
58
|
-
displayName: '
|
|
58
|
+
displayName: 'Plugin list',
|
|
59
59
|
group: 'extensions',
|
|
60
60
|
payload: {
|
|
61
61
|
kind: 'array', zipPath: 'plugins/plugins.json', key: 'plugins',
|
|
@@ -152,7 +152,7 @@ export const SECTION_REGISTRY = {
|
|
|
152
152
|
},
|
|
153
153
|
pluginFiles: {
|
|
154
154
|
id: 'pluginFiles',
|
|
155
|
-
displayName:
|
|
155
|
+
displayName: 'Plugin config files',
|
|
156
156
|
group: 'extensions',
|
|
157
157
|
payload: { kind: 'files', filePrefix: 'plugin-files/' },
|
|
158
158
|
dataVersion: SECTION_DATA_VERSION,
|
|
@@ -205,7 +205,7 @@ export const SECTION_REGISTRY = {
|
|
|
205
205
|
},
|
|
206
206
|
self: {
|
|
207
207
|
id: 'self',
|
|
208
|
-
displayName:
|
|
208
|
+
displayName: 'Config Manager settings',
|
|
209
209
|
group: 'extensions',
|
|
210
210
|
payload: { kind: 'files', filePrefix: 'self/' },
|
|
211
211
|
dataVersion: SECTION_DATA_VERSION,
|
|
@@ -218,7 +218,7 @@ export const SECTION_REGISTRY = {
|
|
|
218
218
|
},
|
|
219
219
|
pluginData: {
|
|
220
220
|
id: 'pluginData',
|
|
221
|
-
displayName:
|
|
221
|
+
displayName: 'Plugin data dirs',
|
|
222
222
|
group: 'extensions',
|
|
223
223
|
payload: { kind: 'files', filePrefix: 'plugin-data/' },
|
|
224
224
|
dataVersion: SECTION_DATA_VERSION,
|
|
@@ -234,7 +234,7 @@ export const SECTION_REGISTRY = {
|
|
|
234
234
|
},
|
|
235
235
|
pluginAssets: {
|
|
236
236
|
id: 'pluginAssets',
|
|
237
|
-
displayName:
|
|
237
|
+
displayName: 'Plugin large files',
|
|
238
238
|
group: 'extensions',
|
|
239
239
|
payload: { kind: 'files', filePrefix: 'plugin-assets/' },
|
|
240
240
|
dataVersion: SECTION_DATA_VERSION,
|
package/lib/schema/types.d.ts
CHANGED
|
@@ -111,6 +111,20 @@ export interface PluginEntry {
|
|
|
111
111
|
* 否则按裸包名走 npm 最新版(官方机制)。
|
|
112
112
|
*/
|
|
113
113
|
spec?: string;
|
|
114
|
+
/**
|
|
115
|
+
* 该层只在 profile 的 `dsh.profile.bundles` 里声明、`dependencies` 里没有它,且该 profile 的
|
|
116
|
+
* `node_modules` 里也**没有**它的落盘副本(issue #95,导出侧写入)。
|
|
117
|
+
*
|
|
118
|
+
* 语义:这不是一个「装在本 profile 里的包」,而是由 DSH 从**自己的安装位置**解析的层
|
|
119
|
+
* (与 `boot-safety.ts` / `RESCUE_KEEP_BUNDLE_PREFIXES` 同口径)。导入端对这类条目
|
|
120
|
+
* **只把名字声明进目标机的 `dsh.profile.bundles`,绝不走 npm 安装通道**——按裸包名安装会让
|
|
121
|
+
* 目标机拿到 `dist-tags.latest` 的副本(2026-10 真机:host 0.2.1-rc.2 + 包 0.2.1-alpha.2
|
|
122
|
+
* → session-controller 一直 pending → 新建会话 service-unavailable)。
|
|
123
|
+
*
|
|
124
|
+
* 缺省(旧备份没有该字段)= 由 `spec` / `version` 形状推断,见
|
|
125
|
+
* `src/core/plugin-cli.ts` 的 `isBundlesOnlyEntry`。
|
|
126
|
+
*/
|
|
127
|
+
bundlesOnly?: boolean;
|
|
114
128
|
isBundle: boolean;
|
|
115
129
|
inBundles: string[];
|
|
116
130
|
enabled: boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 徽章四态 → DSH 原生 Tag tone 的映射(**唯一一份**)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:徽章现在优先渲染 DSH 自带的 `Tag`(@deepseek-ai/dsh-client-ui-primitives,
|
|
5
|
+
* 与宿主同一颗组件),它只有 tone 语义、没有 kind 语义;映射关系是纯函数,放这里可被 node 单测覆盖,
|
|
6
|
+
* 也让 React 壳保持「只装配」。老宿主拿不到原生包时回落本仓库的 `.badge[data-kind]` 样式,
|
|
7
|
+
* 两条路径共用同一套 kind → tone 语义,不允许各自漂移。
|
|
8
|
+
*/
|
|
9
|
+
/** 插件侧的四态徽章语义。 */
|
|
10
|
+
export type BadgeKind = 'info' | 'ok' | 'warn' | 'error';
|
|
11
|
+
/** DSH 原生 Tag 的调色板选项(与其 .d.ts 逐字同形)。 */
|
|
12
|
+
export type DshTagTone = 'outline' | 'solid' | 'neutral' | 'quiet' | 'success' | 'info' | 'warning' | 'danger';
|
|
13
|
+
/** 四态 → 原生 tone(ok=success / warn=warning / error=danger / info=info)。 */
|
|
14
|
+
export declare const BADGE_TONE: Readonly<Record<BadgeKind, DshTagTone>>;
|
|
15
|
+
/** 取某个四态对应的原生 tone;未知输入按 info 处理(调用方给的是字面量联合,此为防御)。 */
|
|
16
|
+
export declare function badgeTone(kind: BadgeKind): DshTagTone;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 徽章四态 → DSH 原生 Tag tone 的映射(**唯一一份**)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:徽章现在优先渲染 DSH 自带的 `Tag`(@deepseek-ai/dsh-client-ui-primitives,
|
|
5
|
+
* 与宿主同一颗组件),它只有 tone 语义、没有 kind 语义;映射关系是纯函数,放这里可被 node 单测覆盖,
|
|
6
|
+
* 也让 React 壳保持「只装配」。老宿主拿不到原生包时回落本仓库的 `.badge[data-kind]` 样式,
|
|
7
|
+
* 两条路径共用同一套 kind → tone 语义,不允许各自漂移。
|
|
8
|
+
*/
|
|
9
|
+
/** 四态 → 原生 tone(ok=success / warn=warning / error=danger / info=info)。 */
|
|
10
|
+
export const BADGE_TONE = {
|
|
11
|
+
ok: 'success',
|
|
12
|
+
info: 'info',
|
|
13
|
+
warn: 'warning',
|
|
14
|
+
error: 'danger',
|
|
15
|
+
};
|
|
16
|
+
/** 取某个四态对应的原生 tone;未知输入按 info 处理(调用方给的是字面量联合,此为防御)。 */
|
|
17
|
+
export function badgeTone(kind) {
|
|
18
|
+
return BADGE_TONE[kind] ?? 'info';
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=badge-tone.js.map
|
package/lib/ui/i18n.d.ts
CHANGED
|
@@ -517,10 +517,10 @@ export declare const uiZh: {
|
|
|
517
517
|
readonly 'export.category.settings': 'DSH 全局设置(namespace 分区,redacted)';
|
|
518
518
|
readonly 'export.category.providers': 'LLM Provider / Model / 默认模型 / BaseURL';
|
|
519
519
|
readonly 'export.category.plugins': '已安装插件清单与启用状态(不含二进制)';
|
|
520
|
-
readonly 'export.category.pluginFiles': '
|
|
521
|
-
readonly 'export.category.self': '
|
|
522
|
-
readonly 'export.category.pluginData': '
|
|
523
|
-
readonly 'export.category.pluginAssets': '
|
|
520
|
+
readonly 'export.category.pluginFiles': '插件放在 ~/.dsh 根下的配置文件(白名单 + plugin-config/ 目录,整文件复制)';
|
|
521
|
+
readonly 'export.category.self': '配置管理器自身的设置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)';
|
|
522
|
+
readonly 'export.category.pluginData': '插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)';
|
|
523
|
+
readonly 'export.category.pluginAssets': '插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)';
|
|
524
524
|
readonly 'export.category.mcp': 'MCP 服务器组合配置(需重启生效)';
|
|
525
525
|
readonly 'export.category.prompts': 'System Prompt / Plan Mode 提示';
|
|
526
526
|
readonly 'export.category.skills': '用户技能文件(~/.dsh/skills)';
|
package/lib/ui/i18n.js
CHANGED
|
@@ -547,10 +547,10 @@ export const uiZh = {
|
|
|
547
547
|
'export.category.settings': 'DSH 全局设置(namespace 分区,redacted)',
|
|
548
548
|
'export.category.providers': 'LLM Provider / Model / 默认模型 / BaseURL',
|
|
549
549
|
'export.category.plugins': '已安装插件清单与启用状态(不含二进制)',
|
|
550
|
-
'export.category.pluginFiles': '
|
|
551
|
-
'export.category.self': '
|
|
552
|
-
'export.category.pluginData': '
|
|
553
|
-
'export.category.pluginAssets': '
|
|
550
|
+
'export.category.pluginFiles': '插件放在 ~/.dsh 根下的配置文件(白名单 + plugin-config/ 目录,整文件复制)',
|
|
551
|
+
'export.category.self': '配置管理器自身的设置(同步/自动同步/分区选择/UI 偏好/市场;sync-*.json 等,不含凭据值)',
|
|
552
|
+
'export.category.pluginData': '插件在系统主目录下的数据(如 ~/.dsh-wallpaper-engine、~/.mnemon 的配置与状态;可再生成缓存绝不收录)',
|
|
553
|
+
'export.category.pluginAssets': '插件的大体积文件(上传的壁纸、本地数据库等;默认不勾选)',
|
|
554
554
|
'export.category.mcp': 'MCP 服务器组合配置(需重启生效)',
|
|
555
555
|
'export.category.prompts': 'System Prompt / Plan Mode 提示',
|
|
556
556
|
'export.category.skills': '用户技能文件(~/.dsh/skills)',
|
|
@@ -1093,10 +1093,10 @@ export const uiEn = {
|
|
|
1093
1093
|
'export.category.settings': 'Global DSH settings (namespace sections, redacted)',
|
|
1094
1094
|
'export.category.providers': 'LLM providers / models / default model / base URL',
|
|
1095
1095
|
'export.category.plugins': 'Installed plugin list and enablement (binaries excluded)',
|
|
1096
|
-
'export.category.pluginFiles': "
|
|
1097
|
-
'export.category.self': "
|
|
1098
|
-
'export.category.pluginData': "
|
|
1099
|
-
'export.category.pluginAssets': "
|
|
1096
|
+
'export.category.pluginFiles': "Plugin config files at the ~/.dsh root (allowlist + plugin-config/, copied whole)",
|
|
1097
|
+
'export.category.self': "Config Manager's own settings (sync / autosync / section selection / UI prefs / market; sync-*.json etc., no credential values)",
|
|
1098
|
+
'export.category.pluginData': "Plugin data under the system home directory (e.g. the config/state of ~/.dsh-wallpaper-engine and ~/.mnemon; regenerable caches are never included)",
|
|
1099
|
+
'export.category.pluginAssets': "Plugins' large files (uploaded wallpapers, local databases, ...; not selected by default)",
|
|
1100
1100
|
'export.category.mcp': 'MCP server composition (restart required)',
|
|
1101
1101
|
'export.category.prompts': 'System prompt / Plan Mode prompts',
|
|
1102
1102
|
'export.category.skills': 'Your skill files (~/.dsh/skills)',
|