dsh-plugin-tool-management 0.17.0 → 0.18.5
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 +70 -0
- package/README.md +19 -4
- package/README_EN.md +19 -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 +29 -0
- package/lib/client.js +1662 -161
- 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 +9 -720
- 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 +10 -478
- package/lib/exports/transcript.js +223 -0
- package/lib/http-api.js +229 -0
- package/lib/hub.js +2 -0
- package/lib/index.js +138 -868
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/manager.js +30 -548
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +142 -5
- 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/memories-types.js +1 -0
- package/lib/memories/service.js +4 -38
- package/lib/memories/snapshot.js +10 -252
- package/lib/op-registry.js +21 -0
- package/lib/ops/compat.js +11 -0
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +40 -0
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +34 -14
- package/lib/ops/snapshot.js +18 -12
- package/lib/prompts/quick-service.js +211 -0
- package/lib/scene-linkage.js +184 -0
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +29 -166
- 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 +56 -2326
- 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 +5 -240
- 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 +14 -638
- 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
|
@@ -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, archivedSetSchema, 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 {
|
|
@@ -402,6 +242,23 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
402
242
|
if (changed)
|
|
403
243
|
await this.saveArchivedAt();
|
|
404
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* 以宿主权威归档集合构造恢复回执。
|
|
247
|
+
*
|
|
248
|
+
* 官方 `unarchiveSession` 只回 `Promise<void>`,**宿主给什么一律不认**,委托与自持两条
|
|
249
|
+
* 分支共用这一个形状。读公开 getter 而不是 `requireState()`:后者经 `bridge.state()`
|
|
250
|
+
* 落到 `method("requireState")`,宿主缺这个私有成员时会直接抛(bridge.ts:331)——
|
|
251
|
+
* 恢复已经成功了,不该被一份回执拖成失败。
|
|
252
|
+
*/
|
|
253
|
+
archivedSetResult() {
|
|
254
|
+
const raw = this.archivedSessionIds;
|
|
255
|
+
if (!Array.isArray(raw)) {
|
|
256
|
+
this.ctx.logger.warn("archive-manager: registry.archivedSessionIds is not an array; reporting an empty archive set");
|
|
257
|
+
return { archivedSessionIds: [] };
|
|
258
|
+
}
|
|
259
|
+
// 集合由我们自己拼,schema 在这里是后置断言:真触发说明 getter 的声明与运行时不符。
|
|
260
|
+
return archivedSetSchema.parse({ archivedSessionIds: [...new Set(raw.map((id) => String(id)))] });
|
|
261
|
+
}
|
|
405
262
|
async ensureArchivedAtLoaded() {
|
|
406
263
|
await this.ready;
|
|
407
264
|
if (this.archivedAtLoaded)
|
|
@@ -849,9 +706,10 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
849
706
|
async unarchiveSession(sessionId) {
|
|
850
707
|
this.bridge.checkWorkspace("unarchive");
|
|
851
708
|
if (typeof this.registry.unarchiveSession === "function") {
|
|
852
|
-
|
|
709
|
+
await this.registry.unarchiveSession(sessionId);
|
|
853
710
|
await this.reconcileArchiveLedger();
|
|
854
|
-
|
|
711
|
+
// 官方只回 Promise<void>,返回值不认;回执按权威集合自造(见 archivedSetResult)。
|
|
712
|
+
return this.archivedSetResult();
|
|
855
713
|
}
|
|
856
714
|
return this.enqueueOperation(async () => {
|
|
857
715
|
if (!(await this.sessionKnown(sessionId)))
|
|
@@ -917,6 +775,9 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
917
775
|
* 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
|
|
918
776
|
*/
|
|
919
777
|
async unarchiveSessions(target) {
|
|
778
|
+
// ⚠️ 这一支今天**跑不到**:宿主没有 `unarchiveSessions`(dsh-workspace 全库零命中),
|
|
779
|
+
// 所以无从测试。它保留了「直接认宿主返回值」这个和 unarchiveSession 同型的过度信任 ——
|
|
780
|
+
// 官方哪天补上这个方法时,先照 unarchiveSession 的做法改成自造回执,别照抄这一行。
|
|
920
781
|
if (typeof this.registry.unarchiveSessions === "function") {
|
|
921
782
|
const result = await this.registry.unarchiveSessions(target);
|
|
922
783
|
await this.reconcileArchiveLedger();
|
|
@@ -926,7 +787,7 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
926
787
|
const ids = this.archivedSessionIdsForTarget(target);
|
|
927
788
|
for (const id of ids)
|
|
928
789
|
await this.unarchiveSession(id);
|
|
929
|
-
return { unarchivedSessionIds: ids,
|
|
790
|
+
return { unarchivedSessionIds: ids, ...this.archivedSetResult() };
|
|
930
791
|
}
|
|
931
792
|
return this.enqueueOperation(async () => {
|
|
932
793
|
const unarchivedSessionIds = this.archivedSessionIdsForTarget(target);
|
|
@@ -1483,4 +1344,6 @@ var ArchiveWorkspaceRegistry = class {
|
|
|
1483
1344
|
}
|
|
1484
1345
|
};
|
|
1485
1346
|
//#endregion
|
|
1486
|
-
export { ArchiveWorkspaceRegistry, ArchiveWorkspaceRegistry as default
|
|
1347
|
+
export { ArchiveWorkspaceRegistry, ArchiveWorkspaceRegistry as default };
|
|
1348
|
+
// 路径键拆进 path-key.ts 后照旧从这个入口对外可见(package.json 的 exports 只声明 ./workspace)。
|
|
1349
|
+
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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 技能核心的公共底座:业务错误码的构造与透传、条目路径谓词、可恢复 rename 的重试、
|
|
2
|
+
// 同目录临时文件名。
|
|
3
|
+
//
|
|
4
|
+
// 声明从 core.ts 逐个搬来(`--export-top-level` 只多 `export ` 前缀,正文一字未改)。
|
|
5
|
+
// 为什么要先有这一层:codedError / attachCode / lstatOrNull / renameWithRetry 这四件被
|
|
6
|
+
// **导入链路与技能读写两侧同时用到**,留在 core.ts 里就意味着任何从 core 搬出去的下游段
|
|
7
|
+
// 都要反过来引 core —— 那是运行时环。搬到这里,core.ts 与导入链路两侧都只向下引这一层。
|
|
8
|
+
// 这一层自己不引同目录的任何兄弟模块(只引 ../paths.js 与 node:*)。
|
|
9
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { isSameOrDescendant, isValidSegment } from "../paths.js";
|
|
13
|
+
export const MAX_ENTRY_NAME_LENGTH = 128;
|
|
14
|
+
export const TRANSIENT_RENAME_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
15
|
+
// ── 业务错误码 ────────────────────────────────────────────────────────────────
|
|
16
|
+
/** 构造带 code/params 的业务 Error,供导入链路 throw 后透传到失败明细。 */
|
|
17
|
+
export function codedError(message, code, params) {
|
|
18
|
+
const error = new Error(message);
|
|
19
|
+
error.code = code;
|
|
20
|
+
error.params = params;
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
/** 把业务 Error 的 code/params 附加到失败明细;系统异常(ENOENT 等,非 error.* 前缀)保持原文。 */
|
|
24
|
+
export function attachCode(item, error) {
|
|
25
|
+
// 透传并回写任意失败明细对象(形状由各调用点决定),故按 any 处理。
|
|
26
|
+
const coded = error;
|
|
27
|
+
if (coded && typeof coded.code === "string" && /^error\./.test(coded.code))
|
|
28
|
+
item.code = coded.code;
|
|
29
|
+
if (coded && coded.params)
|
|
30
|
+
item.params = coded.params;
|
|
31
|
+
return item;
|
|
32
|
+
}
|
|
33
|
+
export function pathIdentity(path) {
|
|
34
|
+
const canonical = resolve(path);
|
|
35
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
36
|
+
}
|
|
37
|
+
export async function lstatOrNull(path) {
|
|
38
|
+
try {
|
|
39
|
+
return await fs.lstat(path);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 名称只允许一个普通路径段;不把既有技能名称限制为 kebab-case。 */
|
|
46
|
+
export function entryPath(root, name) {
|
|
47
|
+
// 段名谓词收敛到 paths.js(此前与 rules / subagents / imports 各写一套,口径不一)。
|
|
48
|
+
// 原实现里 `basename(name) !== name` 那条已被谓词的「不含路径分隔符」覆盖。
|
|
49
|
+
if (!isValidSegment(name, MAX_ENTRY_NAME_LENGTH))
|
|
50
|
+
return null;
|
|
51
|
+
const rootPath = resolve(root);
|
|
52
|
+
const path = resolve(rootPath, name);
|
|
53
|
+
return isSameOrDescendant(rootPath, path) && rootPath !== path ? path : null;
|
|
54
|
+
}
|
|
55
|
+
/** 同目录临时文件加 rename,避免写入中断时截断原 SKILL.md。 */
|
|
56
|
+
/** Windows 上杀毒软件或索引器可能短暂占用目录;只重试明确可恢复的 rename 错误。 */
|
|
57
|
+
export async function renameWithRetry(source, destination, options = {}) {
|
|
58
|
+
const rename = typeof options.rename === "function" ? options.rename : fs.rename;
|
|
59
|
+
// Number.isInteger/isFinite 不收窄类型,故断言成 number 以保持既有比较表达式。
|
|
60
|
+
const maxAttempts = Number.isInteger(options.maxAttempts) && options.maxAttempts > 0
|
|
61
|
+
? options.maxAttempts
|
|
62
|
+
: 6;
|
|
63
|
+
const delayMs = Number.isFinite(options.delayMs) && options.delayMs >= 0
|
|
64
|
+
? options.delayMs
|
|
65
|
+
: 40;
|
|
66
|
+
for (let attempt = 1;; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
return await rename(source, destination);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// 系统 rename 异常按可选 code 读取,保持既有重试判定。
|
|
72
|
+
if (!TRANSIENT_RENAME_CODES.has(error && error.code) ||
|
|
73
|
+
attempt >= maxAttempts)
|
|
74
|
+
throw error;
|
|
75
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, delayMs * attempt));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function temporaryPath(target, kind) {
|
|
80
|
+
return join(dirname(target), `.${basename(target)}.dssm-${kind}-${randomUUID()}`);
|
|
81
|
+
}
|