dsh-plugin-tool-management 0.16.6 → 0.18.0
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/CHANGELOG.md +115 -0
- package/README.md +20 -4
- package/README_EN.md +20 -4
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +59 -0
- package/lib/audit-log.js +6 -2
- package/lib/client.js +1615 -148
- package/lib/compat/capability-specs.js +321 -0
- package/lib/compat/host-probe.js +261 -0
- package/lib/compat/host-roots.js +168 -0
- package/lib/compat/probe-types.js +6 -0
- package/lib/compat/probe.js +54 -714
- package/lib/context-inject-contract.js +114 -0
- package/lib/context-inject-frame.js +147 -0
- package/lib/context-inject-select.js +127 -0
- package/lib/context-inject-surface.js +97 -0
- package/lib/context-inject.js +57 -459
- package/lib/host-names.js +150 -0
- package/lib/http-api.js +229 -0
- package/lib/hub.js +84 -11
- package/lib/imports/parsers.js +40 -15
- package/lib/index.js +235 -843
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/loader-token.js +26 -0
- package/lib/mcp/manager.js +267 -558
- package/lib/mcp/patch-yaml.js +207 -40
- package/lib/mcp/secret-guard.js +122 -16
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +163 -16
- package/lib/memories/archive.js +10 -1
- package/lib/memories/discovery.js +261 -0
- package/lib/memories/export-plan.js +42 -0
- package/lib/memories/index-io.js +125 -12
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +167 -78
- package/lib/memories/snapshot.js +41 -260
- package/lib/op-registry.js +72 -8
- package/lib/ops/compat.js +17 -2
- package/lib/ops/prompts.js +18 -5
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +157 -10
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +63 -13
- package/lib/ops/snapshot.js +123 -24
- package/lib/ops/trash.js +45 -9
- package/lib/prompts/quick-service.js +211 -0
- package/lib/prompts/service.js +7 -2
- package/lib/request-gate.js +115 -56
- package/lib/scene-linkage.js +184 -0
- package/lib/scene-prompt-sync.js +46 -7
- package/lib/scenes/candidates.js +21 -3
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +45 -168
- package/lib/settings-sidecars.js +143 -0
- package/lib/skills/core-types.js +8 -0
- package/lib/skills/core-util.js +81 -0
- package/lib/skills/core.js +97 -2297
- package/lib/skills/dir-watch.js +71 -0
- package/lib/skills/frontmatter.js +96 -0
- package/lib/skills/homes.js +14 -0
- package/lib/skills/import-pipeline.js +231 -0
- package/lib/skills/manager-state.js +800 -0
- package/lib/skills/official.js +269 -0
- package/lib/skills/provider-registration.js +89 -0
- package/lib/skills/service.js +4 -234
- package/lib/skills/skill-roots.js +287 -0
- package/lib/skills/skill-trash.js +265 -0
- package/lib/skills/system-trash.js +97 -0
- package/lib/skills/upload-pipeline.js +374 -0
- package/lib/slash-settings.js +25 -0
- package/lib/subagents/persona-parse.js +312 -0
- package/lib/subagents/persona-types.js +5 -0
- package/lib/subagents/persona-write.js +93 -0
- package/lib/subagents/run-provider.js +159 -0
- package/lib/subagents/service.js +20 -639
- package/lib/subagents/tool-filter.js +98 -0
- package/lib/token-config.js +240 -0
- package/lib/tool-table-switch.js +163 -0
- package/lib/tools/scene.js +40 -9
- package/package.json +3 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const sessionIdSchema = {
|
|
2
|
+
parse(value) {
|
|
3
|
+
if (typeof value !== "string" || value.length === 0)
|
|
4
|
+
throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
|
|
5
|
+
return value;
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
export const workspaceIdSchema = {
|
|
9
|
+
parse(value) {
|
|
10
|
+
if (typeof value !== "string" || value.length === 0)
|
|
11
|
+
throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
|
|
12
|
+
return value;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export const archivedSetSchema = {
|
|
16
|
+
parse(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
18
|
+
throw new TypeError("result must be an object");
|
|
19
|
+
const ids = value.archivedSessionIds;
|
|
20
|
+
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string"))
|
|
21
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
22
|
+
return value;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const deletedSchema = {
|
|
26
|
+
parse(value) {
|
|
27
|
+
if (typeof value !== "object" ||
|
|
28
|
+
value === null ||
|
|
29
|
+
Array.isArray(value) ||
|
|
30
|
+
value.deleted !== true)
|
|
31
|
+
throw new TypeError("deleted must be true");
|
|
32
|
+
return value;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const archivedBatchTargetSchema = {
|
|
36
|
+
// 校验通过后原样返回记录,调用方按 scope 判别式收窄(宿主返回值校验器,故返回 any)。
|
|
37
|
+
parse(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
39
|
+
throw new TypeError("target must be an object");
|
|
40
|
+
if (value.scope === "all" || value.scope === "ungrouped")
|
|
41
|
+
return value;
|
|
42
|
+
if (value.scope === "workspace" &&
|
|
43
|
+
typeof value.workspaceId === "string" &&
|
|
44
|
+
value.workspaceId.length > 0)
|
|
45
|
+
return value;
|
|
46
|
+
if (value.scope === "sessions" &&
|
|
47
|
+
Array.isArray(value.sessionIds) &&
|
|
48
|
+
value.sessionIds.length > 0 &&
|
|
49
|
+
value.sessionIds.every((id) => typeof id === "string" && id.length > 0))
|
|
50
|
+
return value;
|
|
51
|
+
throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export const unarchivedBatchSchema = {
|
|
55
|
+
parse(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
57
|
+
throw new TypeError("result must be an object");
|
|
58
|
+
if (!Array.isArray(value.archivedSessionIds) ||
|
|
59
|
+
value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
60
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
61
|
+
if (!Array.isArray(value.unarchivedSessionIds) ||
|
|
62
|
+
value.unarchivedSessionIds.some((id) => typeof id !== "string"))
|
|
63
|
+
throw new TypeError("unarchivedSessionIds must be a string array");
|
|
64
|
+
return value;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export const archivedWorkspaceBatchSchema = {
|
|
68
|
+
parse(value) {
|
|
69
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
70
|
+
throw new TypeError("result must be an object");
|
|
71
|
+
if (!Array.isArray(value.archivedSessionIds) ||
|
|
72
|
+
value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
73
|
+
throw new TypeError("archivedSessionIds must be a string array");
|
|
74
|
+
if (!Array.isArray(value.archivedSessionIdsAdded) ||
|
|
75
|
+
value.archivedSessionIdsAdded.some((id) => typeof id !== "string"))
|
|
76
|
+
throw new TypeError("archivedSessionIdsAdded must be a string array");
|
|
77
|
+
return value;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
export const deletedBatchSchema = {
|
|
81
|
+
parse(value) {
|
|
82
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
83
|
+
throw new TypeError("result must be an object");
|
|
84
|
+
for (const key of [
|
|
85
|
+
"requestedSessionIds",
|
|
86
|
+
"deletedSessionIds",
|
|
87
|
+
"skippedSessionIds",
|
|
88
|
+
]) {
|
|
89
|
+
if (!Array.isArray(value[key]) ||
|
|
90
|
+
value[key].some((id) => typeof id !== "string"))
|
|
91
|
+
throw new TypeError(`${key} must be a string array`);
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(value.failures) ||
|
|
94
|
+
value.failures.some((failure) => typeof failure !== "object" ||
|
|
95
|
+
failure === null ||
|
|
96
|
+
typeof failure.sessionId !== "string" ||
|
|
97
|
+
typeof failure.message !== "string"))
|
|
98
|
+
throw new TypeError("failures must contain sessionId/message objects");
|
|
99
|
+
return value;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
export const archivedSessionMetadataSchema = {
|
|
103
|
+
parse(value) {
|
|
104
|
+
if (typeof value !== "object" ||
|
|
105
|
+
value === null ||
|
|
106
|
+
Array.isArray(value) ||
|
|
107
|
+
!Array.isArray(value.items))
|
|
108
|
+
throw new TypeError("result.items must be an array");
|
|
109
|
+
if (value.items.some((item) => typeof item !== "object" ||
|
|
110
|
+
item === null ||
|
|
111
|
+
typeof item.sessionId !== "string" ||
|
|
112
|
+
typeof item.createdAt !== "number" ||
|
|
113
|
+
!Number.isFinite(item.createdAt)))
|
|
114
|
+
throw new TypeError("items must contain sessionId/createdAt objects");
|
|
115
|
+
if (value.repairedSessionIds !== void 0 &&
|
|
116
|
+
(!Array.isArray(value.repairedSessionIds) ||
|
|
117
|
+
value.repairedSessionIds.some((id) => typeof id !== "string")))
|
|
118
|
+
throw new TypeError("repairedSessionIds must be a string array");
|
|
119
|
+
return value;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 工作区路径的归一化比较键与展示名。
|
|
2
|
+
//
|
|
3
|
+
// 从 sessions/workspace.ts 整段搬来,一行未改。workspacePathKey 的口径注释里写着它不是
|
|
4
|
+
// 「只用于分组」而是决定会话挂回哪条登记,也写着它比宿主 realpathNormalize 更宽 ——
|
|
5
|
+
// 那两条是本插件的已知假设,改这个文件前先读它们。
|
|
6
|
+
/**
|
|
7
|
+
* 路径比较键:工作区的唯一性在宿主侧是 realpath 字符串相等(dsh-workspace
|
|
8
|
+
* `realpathNormalize`),Windows 盘符大小写与分隔符拼写却可能不同,因此比较前
|
|
9
|
+
* 先归一化。
|
|
10
|
+
*
|
|
11
|
+
* ⚠ 它不是"只用于分组":它决定**哪些会话被挂回哪条登记** —— `registerWorkspace` 会调
|
|
12
|
+
* `attachKnownSessions`,后者按这个键挑选会话并写宿主记账(`sessionIds`);
|
|
13
|
+
* `ensureWorkspaceAccounting` 也拿它当去重键后 `host.create` + 挂回。
|
|
14
|
+
* 而且它比宿主更宽:宿主 `realpathNormalize` 只是 `fs.realpath`(**不做大小写归一**),
|
|
15
|
+
* 本函数在 Windows 上 `toLowerCase()` ⇒ `C:\Proj` 与 `c:\proj` 在插件里同组、在宿主里
|
|
16
|
+
* 可以是两条登记。挂回前应以宿主 `resolveByPath` 实际返回的 id 复核,而不是按归一字符串匹配。
|
|
17
|
+
* @param {string} path - 原始路径。
|
|
18
|
+
* @returns {string} 归一化后的比较键(空路径返回空串)。
|
|
19
|
+
*/
|
|
20
|
+
export function workspacePathKey(path) {
|
|
21
|
+
const raw = String(path ?? "").trim();
|
|
22
|
+
if (!raw)
|
|
23
|
+
return "";
|
|
24
|
+
const windows = /^[A-Za-z]:[\\/]/.test(raw) || raw.startsWith("\\\\");
|
|
25
|
+
let out = windows ? raw.replace(/\//g, "\\") : raw;
|
|
26
|
+
while (out.length > 1 && (out.endsWith("\\") || out.endsWith("/")))
|
|
27
|
+
out = out.slice(0, -1);
|
|
28
|
+
return windows ? out.toLowerCase() : out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 展示用目录名(与宿主 `defaultWorkspaceTitle` 同义:末段,无末段则用根拼写)。
|
|
32
|
+
* @param {string} path - 原始路径。
|
|
33
|
+
* @returns {string} 非空展示名。
|
|
34
|
+
*/
|
|
35
|
+
export function workspaceBaseName(path) {
|
|
36
|
+
const raw = String(path ?? "").trim();
|
|
37
|
+
if (!raw)
|
|
38
|
+
return "";
|
|
39
|
+
const trimmed = raw.replace(/[\\/]+$/, "");
|
|
40
|
+
const index = Math.max(trimmed.lastIndexOf("\\"), trimmed.lastIndexOf("/"));
|
|
41
|
+
const segment = index >= 0 ? trimmed.slice(index + 1) : trimmed;
|
|
42
|
+
if (segment)
|
|
43
|
+
return segment;
|
|
44
|
+
const root = trimmed.match(/^(?:[A-Za-z]:[\\/]|[\\/]{1,2})/);
|
|
45
|
+
return root ? root[0] : trimmed || raw;
|
|
46
|
+
}
|
|
@@ -4,6 +4,8 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
4
4
|
import { hubPath } from "../hub.js";
|
|
5
5
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
6
6
|
import { trackTombstone } from "./tombstone.js";
|
|
7
|
+
import { archivedBatchTargetSchema, workspaceIdSchema, } from "./archive-schemas.js";
|
|
8
|
+
import { workspaceBaseName, workspacePathKey } from "./path-key.js";
|
|
7
9
|
import { noteRuntime } from "../compat/runtime-notes.js";
|
|
8
10
|
/**
|
|
9
11
|
* A3-1:宿主补上了原生删除入口时上报一次(**不切换**,理由见 deleteSessionCore)。
|
|
@@ -113,127 +115,6 @@ function headerIdentity(header) {
|
|
|
113
115
|
cwd: header.cwd ?? null,
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
|
-
const sessionIdSchema = {
|
|
117
|
-
parse(value) {
|
|
118
|
-
if (typeof value !== "string" || value.length === 0)
|
|
119
|
-
throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
|
|
120
|
-
return value;
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
const workspaceIdSchema = {
|
|
124
|
-
parse(value) {
|
|
125
|
-
if (typeof value !== "string" || value.length === 0)
|
|
126
|
-
throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
|
|
127
|
-
return value;
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
const archivedSetSchema = {
|
|
131
|
-
parse(value) {
|
|
132
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
133
|
-
throw new TypeError("result must be an object");
|
|
134
|
-
const ids = value.archivedSessionIds;
|
|
135
|
-
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string"))
|
|
136
|
-
throw new TypeError("archivedSessionIds must be a string array");
|
|
137
|
-
return value;
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
const deletedSchema = {
|
|
141
|
-
parse(value) {
|
|
142
|
-
if (typeof value !== "object" ||
|
|
143
|
-
value === null ||
|
|
144
|
-
Array.isArray(value) ||
|
|
145
|
-
value.deleted !== true)
|
|
146
|
-
throw new TypeError("deleted must be true");
|
|
147
|
-
return value;
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
const archivedBatchTargetSchema = {
|
|
151
|
-
// 校验通过后原样返回记录,调用方按 scope 判别式收窄(宿主返回值校验器,故返回 any)。
|
|
152
|
-
parse(value) {
|
|
153
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
154
|
-
throw new TypeError("target must be an object");
|
|
155
|
-
if (value.scope === "all" || value.scope === "ungrouped")
|
|
156
|
-
return value;
|
|
157
|
-
if (value.scope === "workspace" &&
|
|
158
|
-
typeof value.workspaceId === "string" &&
|
|
159
|
-
value.workspaceId.length > 0)
|
|
160
|
-
return value;
|
|
161
|
-
if (value.scope === "sessions" &&
|
|
162
|
-
Array.isArray(value.sessionIds) &&
|
|
163
|
-
value.sessionIds.length > 0 &&
|
|
164
|
-
value.sessionIds.every((id) => typeof id === "string" && id.length > 0))
|
|
165
|
-
return value;
|
|
166
|
-
throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
const unarchivedBatchSchema = {
|
|
170
|
-
parse(value) {
|
|
171
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
172
|
-
throw new TypeError("result must be an object");
|
|
173
|
-
if (!Array.isArray(value.archivedSessionIds) ||
|
|
174
|
-
value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
175
|
-
throw new TypeError("archivedSessionIds must be a string array");
|
|
176
|
-
if (!Array.isArray(value.unarchivedSessionIds) ||
|
|
177
|
-
value.unarchivedSessionIds.some((id) => typeof id !== "string"))
|
|
178
|
-
throw new TypeError("unarchivedSessionIds must be a string array");
|
|
179
|
-
return value;
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
const archivedWorkspaceBatchSchema = {
|
|
183
|
-
parse(value) {
|
|
184
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
185
|
-
throw new TypeError("result must be an object");
|
|
186
|
-
if (!Array.isArray(value.archivedSessionIds) ||
|
|
187
|
-
value.archivedSessionIds.some((id) => typeof id !== "string"))
|
|
188
|
-
throw new TypeError("archivedSessionIds must be a string array");
|
|
189
|
-
if (!Array.isArray(value.archivedSessionIdsAdded) ||
|
|
190
|
-
value.archivedSessionIdsAdded.some((id) => typeof id !== "string"))
|
|
191
|
-
throw new TypeError("archivedSessionIdsAdded must be a string array");
|
|
192
|
-
return value;
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
const deletedBatchSchema = {
|
|
196
|
-
parse(value) {
|
|
197
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
198
|
-
throw new TypeError("result must be an object");
|
|
199
|
-
for (const key of [
|
|
200
|
-
"requestedSessionIds",
|
|
201
|
-
"deletedSessionIds",
|
|
202
|
-
"skippedSessionIds",
|
|
203
|
-
]) {
|
|
204
|
-
if (!Array.isArray(value[key]) ||
|
|
205
|
-
value[key].some((id) => typeof id !== "string"))
|
|
206
|
-
throw new TypeError(`${key} must be a string array`);
|
|
207
|
-
}
|
|
208
|
-
if (!Array.isArray(value.failures) ||
|
|
209
|
-
value.failures.some((failure) => typeof failure !== "object" ||
|
|
210
|
-
failure === null ||
|
|
211
|
-
typeof failure.sessionId !== "string" ||
|
|
212
|
-
typeof failure.message !== "string"))
|
|
213
|
-
throw new TypeError("failures must contain sessionId/message objects");
|
|
214
|
-
return value;
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
const archivedSessionMetadataSchema = {
|
|
218
|
-
parse(value) {
|
|
219
|
-
if (typeof value !== "object" ||
|
|
220
|
-
value === null ||
|
|
221
|
-
Array.isArray(value) ||
|
|
222
|
-
!Array.isArray(value.items))
|
|
223
|
-
throw new TypeError("result.items must be an array");
|
|
224
|
-
if (value.items.some((item) => typeof item !== "object" ||
|
|
225
|
-
item === null ||
|
|
226
|
-
typeof item.sessionId !== "string" ||
|
|
227
|
-
typeof item.createdAt !== "number" ||
|
|
228
|
-
!Number.isFinite(item.createdAt)))
|
|
229
|
-
throw new TypeError("items must contain sessionId/createdAt objects");
|
|
230
|
-
if (value.repairedSessionIds !== void 0 &&
|
|
231
|
-
(!Array.isArray(value.repairedSessionIds) ||
|
|
232
|
-
value.repairedSessionIds.some((id) => typeof id !== "string")))
|
|
233
|
-
throw new TypeError("repairedSessionIds must be a string array");
|
|
234
|
-
return value;
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
118
|
/**
|
|
238
119
|
* 折叠进 dsh-plugin-tool-management 的归档工作区注册表。
|
|
239
120
|
* 原始实现来自 @michengai/dsh-archive-manager(Apache-2.0),剥离了
|
|
@@ -247,47 +128,6 @@ function defaultArchivedAtFile() {
|
|
|
247
128
|
function defaultWorkspaceSnapshotFile() {
|
|
248
129
|
return hubPath("history-workspaces.json");
|
|
249
130
|
}
|
|
250
|
-
/**
|
|
251
|
-
* 路径比较键:工作区的唯一性在宿主侧是 realpath 字符串相等(dsh-workspace
|
|
252
|
-
* `realpathNormalize`),Windows 盘符大小写与分隔符拼写却可能不同,因此比较前
|
|
253
|
-
* 先归一化。
|
|
254
|
-
*
|
|
255
|
-
* ⚠ 它不是"只用于分组":它决定**哪些会话被挂回哪条登记** —— `registerWorkspace` 会调
|
|
256
|
-
* `attachKnownSessions`,后者按这个键挑选会话并写宿主记账(`sessionIds`);
|
|
257
|
-
* `ensureWorkspaceAccounting` 也拿它当去重键后 `host.create` + 挂回。
|
|
258
|
-
* 而且它比宿主更宽:宿主 `realpathNormalize` 只是 `fs.realpath`(**不做大小写归一**),
|
|
259
|
-
* 本函数在 Windows 上 `toLowerCase()` ⇒ `C:\Proj` 与 `c:\proj` 在插件里同组、在宿主里
|
|
260
|
-
* 可以是两条登记。挂回前应以宿主 `resolveByPath` 实际返回的 id 复核,而不是按归一字符串匹配。
|
|
261
|
-
* @param {string} path - 原始路径。
|
|
262
|
-
* @returns {string} 归一化后的比较键(空路径返回空串)。
|
|
263
|
-
*/
|
|
264
|
-
function workspacePathKey(path) {
|
|
265
|
-
const raw = String(path ?? "").trim();
|
|
266
|
-
if (!raw)
|
|
267
|
-
return "";
|
|
268
|
-
const windows = /^[A-Za-z]:[\\/]/.test(raw) || raw.startsWith("\\\\");
|
|
269
|
-
let out = windows ? raw.replace(/\//g, "\\") : raw;
|
|
270
|
-
while (out.length > 1 && (out.endsWith("\\") || out.endsWith("/")))
|
|
271
|
-
out = out.slice(0, -1);
|
|
272
|
-
return windows ? out.toLowerCase() : out;
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* 展示用目录名(与宿主 `defaultWorkspaceTitle` 同义:末段,无末段则用根拼写)。
|
|
276
|
-
* @param {string} path - 原始路径。
|
|
277
|
-
* @returns {string} 非空展示名。
|
|
278
|
-
*/
|
|
279
|
-
function workspaceBaseName(path) {
|
|
280
|
-
const raw = String(path ?? "").trim();
|
|
281
|
-
if (!raw)
|
|
282
|
-
return "";
|
|
283
|
-
const trimmed = raw.replace(/[\\/]+$/, "");
|
|
284
|
-
const index = Math.max(trimmed.lastIndexOf("\\"), trimmed.lastIndexOf("/"));
|
|
285
|
-
const segment = index >= 0 ? trimmed.slice(index + 1) : trimmed;
|
|
286
|
-
if (segment)
|
|
287
|
-
return segment;
|
|
288
|
-
const root = trimmed.match(/^(?:[A-Za-z]:[\\/]|[\\/]{1,2})/);
|
|
289
|
-
return root ? root[0] : trimmed || raw;
|
|
290
|
-
}
|
|
291
131
|
// Standalone facade over the existing registry. Never instantiate/register a
|
|
292
132
|
// second WorkspaceRegistry, and never open another copy of its storage domain.
|
|
293
133
|
var ArchiveWorkspaceRegistry = class {
|
|
@@ -1056,8 +896,11 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1056
896
|
/**
|
|
1057
897
|
* 永久删除一个会话及其全部痕迹(转录目录、工作区记账、归档标记、
|
|
1058
898
|
* 投影缓存行)。
|
|
899
|
+
*
|
|
900
|
+
* 级联只删**已归档**的子代理会话(审查 F10);被保留的未归档子会话 id 通过
|
|
901
|
+
* `keptUnarchivedDescendants` 回给调用方,界面据此如实呈现,不假装整支都没了。
|
|
1059
902
|
* @param sessionId - 要删除的会话。
|
|
1060
|
-
* @returns
|
|
903
|
+
* @returns `{ deleted: true }`,有被保留的未归档子会话时附带其 id。
|
|
1061
904
|
* @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
|
|
1062
905
|
*/
|
|
1063
906
|
async deleteSession(sessionId) {
|
|
@@ -1144,7 +987,9 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1144
987
|
await projCache?.whenIdle?.();
|
|
1145
988
|
if (projCache !== void 0)
|
|
1146
989
|
await projCache.delete(sessionId);
|
|
1147
|
-
|
|
990
|
+
// 级联只吃已归档的子会话;被保留(未归档)的 id 一路带回 op 层,让界面/模型
|
|
991
|
+
// 知道「父会话已删,但这一支没有全没」(审查 F10)。
|
|
992
|
+
const keptDescendants = await this.deleteDescendants(sessionId, storedIndex);
|
|
1148
993
|
await this.cleanSpill(sessionId);
|
|
1149
994
|
await this.removeTranscriptDirectory(sessionId);
|
|
1150
995
|
// 只有物理工件删除成功后才能提交记账清理;否则批量目标会因归档标记
|
|
@@ -1163,7 +1008,12 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1163
1008
|
if (deletedHeader !== void 0)
|
|
1164
1009
|
this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
|
|
1165
1010
|
this.publishDeletedSession(sessionId);
|
|
1166
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
deleted: true,
|
|
1013
|
+
...(keptDescendants.length === 0
|
|
1014
|
+
? {}
|
|
1015
|
+
: { keptUnarchivedDescendants: keptDescendants }),
|
|
1016
|
+
};
|
|
1167
1017
|
}
|
|
1168
1018
|
/** 删除完成后通知全部客户端;新版不再通过伪造冷会话生命周期触发通知。 */
|
|
1169
1019
|
publishDeletedSession(sessionId) {
|
|
@@ -1394,8 +1244,21 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1394
1244
|
/** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
|
|
1395
1245
|
* 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
|
|
1396
1246
|
*(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。
|
|
1397
|
-
*
|
|
1247
|
+
*
|
|
1248
|
+
* **只级联「已归档」的子会话**(2026-09-30 审查 F10)。此前这里只按
|
|
1249
|
+
* `parentSession + origin: subagent` 收集就逐个 `deleteSessionCore`,**完全不查归档集合**,
|
|
1250
|
+
* 于是「只能删已归档」在**注册表层不成立** —— 删一个已归档父会话,会把它**未归档**的子代理
|
|
1251
|
+
* 会话一起永久删掉。op 入口那层的收敛(`history-delete` / `deleteArchivedSessions`)拦不住
|
|
1252
|
+
* 这条路:级联发生在删除主体内部,不经过任何 op 门禁。现在把不变量落到这里,而不是只写在
|
|
1253
|
+
* 注释里 —— 未归档的一律**保留**(之后可单独归档再删),并记一条日志说明保留了哪些。
|
|
1254
|
+
*
|
|
1255
|
+
* 归档集合取 `requireState().archivedSessionIds`(权威),不取 `archivedSessionMetadata()`
|
|
1256
|
+
* 那份有损投影;`requireState()` 抛错时**一个都不删**(fail-closed:宁可不级联,也不凭未知
|
|
1257
|
+
* 状态做不可逆操作)。
|
|
1258
|
+
* @param storedIndex - 可选的批首 `id → header` 快照,见 {@link storedHeaderIndex}。
|
|
1259
|
+
* @returns 被保留(未归档)的子会话 id(含后代递归上来的),全部删净时为空数组。 */
|
|
1398
1260
|
async deleteDescendants(sessionId, storedIndex) {
|
|
1261
|
+
const kept = [];
|
|
1399
1262
|
try {
|
|
1400
1263
|
const descendants = [];
|
|
1401
1264
|
const sessions = this.ctx.get("sessions");
|
|
@@ -1414,11 +1277,20 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1414
1277
|
!descendants.includes(header.id))
|
|
1415
1278
|
descendants.push(header.id);
|
|
1416
1279
|
}
|
|
1280
|
+
// 先取归档集合再动手:它抛错时一个子会话都不会被删(fail-closed)。
|
|
1281
|
+
const archived = new Set(this.requireState().archivedSessionIds);
|
|
1417
1282
|
for (const childId of descendants) {
|
|
1283
|
+
if (!archived.has(childId)) {
|
|
1284
|
+
kept.push(childId);
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1418
1287
|
try {
|
|
1419
1288
|
if (!(await this.sessionKnown(childId, storedIndex)))
|
|
1420
1289
|
continue;
|
|
1421
|
-
await this.deleteSessionCore(childId, storedIndex);
|
|
1290
|
+
const result = await this.deleteSessionCore(childId, storedIndex);
|
|
1291
|
+
// 孙辈里被保留的同样要往上带,否则界面只看到最外一层。
|
|
1292
|
+
if (Array.isArray(result?.keptUnarchivedDescendants))
|
|
1293
|
+
kept.push(...result.keptUnarchivedDescendants);
|
|
1422
1294
|
}
|
|
1423
1295
|
catch (error) {
|
|
1424
1296
|
if (error instanceof ArchiveUnknownSessionError)
|
|
@@ -1426,10 +1298,13 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1426
1298
|
this.ctx.logger.warn(`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`);
|
|
1427
1299
|
}
|
|
1428
1300
|
}
|
|
1301
|
+
if (kept.length)
|
|
1302
|
+
this.ctx.logger.warn(`archive-manager: cascade delete of "${sessionId}" kept ${kept.length} unarchived subagent session(s) — only archived sessions may be deleted: ${kept.join(", ")}`);
|
|
1429
1303
|
}
|
|
1430
1304
|
catch (error) {
|
|
1431
1305
|
this.ctx.logger.warn(`archive-manager: descendant enumeration for deleted session "${sessionId}" failed: ${String(error)}`);
|
|
1432
1306
|
}
|
|
1307
|
+
return kept;
|
|
1433
1308
|
}
|
|
1434
1309
|
/** 尽力而为的 spill 清理:移除该会话作用域的 spill 目录。 */
|
|
1435
1310
|
async cleanSpill(sessionId) {
|
|
@@ -1448,4 +1323,6 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1448
1323
|
}
|
|
1449
1324
|
};
|
|
1450
1325
|
//#endregion
|
|
1451
|
-
export { ArchiveWorkspaceRegistry, ArchiveWorkspaceRegistry as default
|
|
1326
|
+
export { ArchiveWorkspaceRegistry, ArchiveWorkspaceRegistry as default };
|
|
1327
|
+
// 路径键拆进 path-key.ts 后照旧从这个入口对外可见(package.json 的 exports 只声明 ./workspace)。
|
|
1328
|
+
export { workspaceBaseName, workspacePathKey } from "./path-key.js";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// 三份界面设置侧车(inject / scene / slash)的读写与 TTL 缓存。
|
|
2
|
+
//
|
|
3
|
+
// 正文从 index.ts 的 apply 闭包里整段搬来,一字未改(只去一层缩进,并把五个 apply 作用域
|
|
4
|
+
// 名字改成走 deps —— moved-verify 按这三类容差机检)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么三份缓存仍留在闭包里:本工厂在 apply 的同一位置只调用一次,`let *Cache` 还是
|
|
7
|
+
// 「每次 apply 一份」。搬到模块级就变成每进程一份,热更新重进 apply 时旧缓存会被复用 ——
|
|
8
|
+
// 那是语义翻转,不是搬家。
|
|
9
|
+
import { hubPath } from './hub.js';
|
|
10
|
+
import { DEFAULT_INJECT_SETTINGS, normalizeInjectSettings } from './context-inject.js';
|
|
11
|
+
import { normalizeSceneSettings } from './scene-settings.js';
|
|
12
|
+
import { normalizeSlashSettings } from './slash-settings.js';
|
|
13
|
+
export function createSettingsSidecars(deps) {
|
|
14
|
+
// 注入设置(侧车 `inject-settings.json`,界面在「兼容」页):
|
|
15
|
+
// - underSuppressingPresets:压制型预设(persona complete / 关运行时上下文,如极简)
|
|
16
|
+
// 下是否仍然注入。默认 false = 跟随预设。
|
|
17
|
+
// - domains:各域开关(任何预设下都生效;界面五个勾选)。
|
|
18
|
+
// 注入器每个 step 都要同步读一次设置 → 走 TTL 缓存;未加载时先给默认值并异步预热。
|
|
19
|
+
const INJECT_SETTINGS_FILE = 'inject-settings.json';
|
|
20
|
+
const INJECT_SETTINGS_TTL_MS = 3000;
|
|
21
|
+
let injectSettingsCache = null;
|
|
22
|
+
async function readInjectSettings(force = false) {
|
|
23
|
+
if (injectSettingsCache && !force && Date.now() - injectSettingsCache.at < INJECT_SETTINGS_TTL_MS)
|
|
24
|
+
return injectSettingsCache.value;
|
|
25
|
+
await deps.ensurePaths();
|
|
26
|
+
const raw = await deps.readJsonFile(hubPath(INJECT_SETTINGS_FILE));
|
|
27
|
+
injectSettingsCache = { at: Date.now(), value: normalizeInjectSettings(raw) };
|
|
28
|
+
return injectSettingsCache.value;
|
|
29
|
+
}
|
|
30
|
+
/** 同步快照(注入器取用);还没加载时先返回默认值,并顺手预热一次。 */
|
|
31
|
+
function injectSettingsSync() {
|
|
32
|
+
if (injectSettingsCache)
|
|
33
|
+
return injectSettingsCache.value;
|
|
34
|
+
void readInjectSettings().catch(() => { });
|
|
35
|
+
return DEFAULT_INJECT_SETTINGS;
|
|
36
|
+
}
|
|
37
|
+
/** 边界提示要的注入设置快照(总开关 + 各域开关)。 */
|
|
38
|
+
const injectNoticeOptions = () => {
|
|
39
|
+
const s = injectSettingsSync();
|
|
40
|
+
return { underSuppressingPresets: s.underSuppressingPresets, domains: s.domains };
|
|
41
|
+
};
|
|
42
|
+
async function injectSettingsOp(args) {
|
|
43
|
+
const current = await readInjectSettings();
|
|
44
|
+
if (!args || args.set !== true)
|
|
45
|
+
return { ok: true, settings: current };
|
|
46
|
+
const next = normalizeInjectSettings({
|
|
47
|
+
underSuppressingPresets: typeof args.underSuppressingPresets === 'boolean'
|
|
48
|
+
? args.underSuppressingPresets
|
|
49
|
+
: current.underSuppressingPresets,
|
|
50
|
+
domains: { ...current.domains, ...(args.domains && typeof args.domains === 'object' ? args.domains : {}) },
|
|
51
|
+
});
|
|
52
|
+
await deps.ensurePaths();
|
|
53
|
+
return deps.withWriteLock(async () => {
|
|
54
|
+
try {
|
|
55
|
+
await deps.writeJsonFile(hubPath(INJECT_SETTINGS_FILE), next);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { ok: false, error: '设置保存失败: ' + deps.message(e) };
|
|
59
|
+
}
|
|
60
|
+
injectSettingsCache = { at: Date.now(), value: next };
|
|
61
|
+
return { ok: true, settings: next };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// 场景页的界面设置(侧车 `scene-settings.json`):目前只有一项 —— 进入场景前要不要先弹
|
|
65
|
+
// 那张「会改什么」的卡。它纯粹是界面提示,所以**不进场景冻结**(锁着场景的人在场景页
|
|
66
|
+
// 照样能关掉提醒,那与五个管理域的只读无关)。
|
|
67
|
+
const SCENE_SETTINGS_FILE = 'scene-settings.json';
|
|
68
|
+
const SCENE_SETTINGS_TTL_MS = 3000;
|
|
69
|
+
let sceneSettingsCache = null;
|
|
70
|
+
async function readSceneSettings(force = false) {
|
|
71
|
+
if (sceneSettingsCache && !force && Date.now() - sceneSettingsCache.at < SCENE_SETTINGS_TTL_MS)
|
|
72
|
+
return sceneSettingsCache.value;
|
|
73
|
+
await deps.ensurePaths();
|
|
74
|
+
const raw = await deps.readJsonFile(hubPath(SCENE_SETTINGS_FILE));
|
|
75
|
+
const value = normalizeSceneSettings(raw);
|
|
76
|
+
sceneSettingsCache = { at: Date.now(), value };
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
async function sceneSettingsOp(args) {
|
|
80
|
+
const current = await readSceneSettings();
|
|
81
|
+
if (!args || args.set !== true)
|
|
82
|
+
return { ok: true, settings: current };
|
|
83
|
+
const next = normalizeSceneSettings({
|
|
84
|
+
enterPreview: typeof args.enterPreview === 'boolean' ? args.enterPreview : current.enterPreview,
|
|
85
|
+
});
|
|
86
|
+
await deps.ensurePaths();
|
|
87
|
+
return deps.withWriteLock(async () => {
|
|
88
|
+
try {
|
|
89
|
+
await deps.writeJsonFile(hubPath(SCENE_SETTINGS_FILE), next);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return { ok: false, error: '设置保存失败: ' + deps.message(e) };
|
|
93
|
+
}
|
|
94
|
+
sceneSettingsCache = { at: Date.now(), value: next };
|
|
95
|
+
return { ok: true, settings: next };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// 斜杠命令入口的开关(侧车 `slash-settings.json`):两段各一颗 —— 「工具」与「快捷提示词」。
|
|
99
|
+
// 判据与 `scene-settings` 同一条 —— 它是入口开关、不是管理动作,所以不冻结;读侧不带 `set`
|
|
100
|
+
// 时是纯读(客户端 boot 时必问一次,不能要令牌)。
|
|
101
|
+
const SLASH_SETTINGS_FILE = 'slash-settings.json';
|
|
102
|
+
const SLASH_SETTINGS_TTL_MS = 3000;
|
|
103
|
+
let slashSettingsCache = null;
|
|
104
|
+
async function readSlashSettings(force = false) {
|
|
105
|
+
if (slashSettingsCache && !force && Date.now() - slashSettingsCache.at < SLASH_SETTINGS_TTL_MS)
|
|
106
|
+
return slashSettingsCache.value;
|
|
107
|
+
await deps.ensurePaths();
|
|
108
|
+
const raw = await deps.readJsonFile(hubPath(SLASH_SETTINGS_FILE));
|
|
109
|
+
const value = normalizeSlashSettings(raw);
|
|
110
|
+
slashSettingsCache = { at: Date.now(), value };
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
async function slashSettingsOp(args) {
|
|
114
|
+
const current = await readSlashSettings();
|
|
115
|
+
if (!args || args.set !== true)
|
|
116
|
+
return { ok: true, settings: current };
|
|
117
|
+
const next = normalizeSlashSettings({
|
|
118
|
+
tools: typeof args.tools === 'boolean' ? args.tools : current.tools,
|
|
119
|
+
quickPrompts: typeof args.quickPrompts === 'boolean' ? args.quickPrompts : current.quickPrompts,
|
|
120
|
+
});
|
|
121
|
+
await deps.ensurePaths();
|
|
122
|
+
return deps.withWriteLock(async () => {
|
|
123
|
+
try {
|
|
124
|
+
await deps.writeJsonFile(hubPath(SLASH_SETTINGS_FILE), next);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
return { ok: false, error: '设置保存失败: ' + deps.message(e) };
|
|
128
|
+
}
|
|
129
|
+
slashSettingsCache = { at: Date.now(), value: next };
|
|
130
|
+
return { ok: true, settings: next };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
readInjectSettings,
|
|
135
|
+
injectSettingsSync,
|
|
136
|
+
injectNoticeOptions,
|
|
137
|
+
injectSettingsOp,
|
|
138
|
+
readSceneSettings,
|
|
139
|
+
sceneSettingsOp,
|
|
140
|
+
readSlashSettings,
|
|
141
|
+
slashSettingsOp,
|
|
142
|
+
};
|
|
143
|
+
}
|