@lijian-ui/dsh-skill-manage 0.1.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/LICENSE +21 -0
- package/README.md +181 -0
- package/README.zh-CN.md +181 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1494 -0
- package/lib/index-CQ2xkEur.d.ts +9 -0
- package/lib/index.js +1215 -0
- package/package.json +88 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
2
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { access, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
|
+
import { parse } from "yaml";
|
|
10
|
+
//#region src/skill-files.ts
|
|
11
|
+
const DISABLED_SUFFIX = ".disabled";
|
|
12
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
async function pathExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await access(path);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function findProjectRoot(cwd) {
|
|
22
|
+
let current = resolve(cwd);
|
|
23
|
+
while (true) {
|
|
24
|
+
if (await pathExists(join(current, ".git"))) return current;
|
|
25
|
+
const parent = dirname(current);
|
|
26
|
+
if (parent === current) return resolve(cwd);
|
|
27
|
+
current = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function parseFrontmatter(raw) {
|
|
31
|
+
const text = raw.trimStart();
|
|
32
|
+
if (!text.startsWith("---")) return void 0;
|
|
33
|
+
const firstEnd = text.indexOf("\n");
|
|
34
|
+
if (firstEnd === -1) return void 0;
|
|
35
|
+
const closing = text.indexOf("\n---", firstEnd + 1);
|
|
36
|
+
const fmEnd = closing === -1 ? text.length : closing;
|
|
37
|
+
const fm = text.slice(3, fmEnd);
|
|
38
|
+
let body = "";
|
|
39
|
+
if (closing !== -1) {
|
|
40
|
+
const at = text.indexOf("\n", closing + 3);
|
|
41
|
+
if (at !== -1) body = text.slice(at + 1);
|
|
42
|
+
}
|
|
43
|
+
const pick = (key) => {
|
|
44
|
+
const m = new RegExp("^" + key + ":\\s*(.+)$", "m").exec(fm);
|
|
45
|
+
if (m === null) return void 0;
|
|
46
|
+
return m[1].trim().replace(/^["']|["']$/g, "");
|
|
47
|
+
};
|
|
48
|
+
const name = pick("name");
|
|
49
|
+
if (name === void 0 || !SKILL_NAME_RE.test(name)) return void 0;
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
description: pick("description") ?? "",
|
|
53
|
+
whenToUse: pick("whenToUse"),
|
|
54
|
+
body: body.trim()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function validateFrontmatter(raw) {
|
|
58
|
+
const text = raw.trimStart();
|
|
59
|
+
if (!text.startsWith("---")) return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: "缺少 YAML frontmatter(文件必须以 --- 开头)"
|
|
62
|
+
};
|
|
63
|
+
const firstEnd = text.indexOf("\n");
|
|
64
|
+
if (firstEnd === -1) return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: "frontmatter 未闭合"
|
|
67
|
+
};
|
|
68
|
+
const closing = text.indexOf("\n---", firstEnd + 1);
|
|
69
|
+
if (closing === -1) return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: "frontmatter 未闭合(缺少结尾的 ---)"
|
|
72
|
+
};
|
|
73
|
+
const fm = text.slice(firstEnd + 1, closing);
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = parse(fm);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: "frontmatter 不是合法的 YAML:" + (error instanceof Error ? error.message : String(error))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "frontmatter 必须是键值对(YAML 映射)"
|
|
86
|
+
};
|
|
87
|
+
const obj = data;
|
|
88
|
+
for (const key of [
|
|
89
|
+
"disableModelInvocation",
|
|
90
|
+
"modelInvocable",
|
|
91
|
+
"userInvocable"
|
|
92
|
+
]) if (key in obj) return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: "不支持旧字段 \"" + key + "\",请改用 disable-model-invocation / user-invocable"
|
|
95
|
+
};
|
|
96
|
+
const name = obj.name;
|
|
97
|
+
if (typeof name !== "string" || name.length === 0) return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: "frontmatter 缺少 name(必须是非空字符串)"
|
|
100
|
+
};
|
|
101
|
+
if (!SKILL_NAME_RE.test(name)) return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "技能名 \"" + name + "\" 不符合命名规则(仅小写字母、数字与连字符,如 my-skill)"
|
|
104
|
+
};
|
|
105
|
+
const description = obj.description;
|
|
106
|
+
if (typeof description !== "string" || description.trim().length === 0) return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: "frontmatter 缺少 description(必须是非空字符串)"
|
|
109
|
+
};
|
|
110
|
+
const whenToUse = obj.whenToUse;
|
|
111
|
+
if (whenToUse !== void 0 && typeof whenToUse !== "string") return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: "whenToUse 必须是字符串"
|
|
114
|
+
};
|
|
115
|
+
for (const key of ["disable-model-invocation", "user-invocable"]) {
|
|
116
|
+
const value = obj[key];
|
|
117
|
+
if (value !== void 0) {
|
|
118
|
+
const lower = String(value).toLowerCase();
|
|
119
|
+
if (![
|
|
120
|
+
"true",
|
|
121
|
+
"false",
|
|
122
|
+
"yes",
|
|
123
|
+
"no",
|
|
124
|
+
"on",
|
|
125
|
+
"off",
|
|
126
|
+
"1",
|
|
127
|
+
"0"
|
|
128
|
+
].includes(lower)) return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: key + " 必须是布尔值"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null || Array.isArray(obj.metadata))) return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: "metadata 必须是对象"
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
skill: {
|
|
141
|
+
name,
|
|
142
|
+
description,
|
|
143
|
+
whenToUse: typeof whenToUse === "string" ? whenToUse : void 0,
|
|
144
|
+
body: ""
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function collectSkillEntries(roots) {
|
|
149
|
+
const entries = [];
|
|
150
|
+
for (const root of roots) {
|
|
151
|
+
let items;
|
|
152
|
+
try {
|
|
153
|
+
items = await readdir(root.path, { withFileTypes: true });
|
|
154
|
+
} catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const item of items) if (item.isDirectory() || item.isSymbolicLink() && (await stat(join(root.path, item.name)).catch(() => void 0))?.isDirectory() === true) {
|
|
158
|
+
const md = join(root.path, item.name, "SKILL.md");
|
|
159
|
+
const disabled = md + DISABLED_SUFFIX;
|
|
160
|
+
if (await pathExists(md)) {
|
|
161
|
+
const parsed = parseFrontmatter(await readFile(md, "utf8").catch(() => ""));
|
|
162
|
+
entries.push({
|
|
163
|
+
name: parsed?.name ?? item.name,
|
|
164
|
+
description: parsed?.description ?? "",
|
|
165
|
+
whenToUse: parsed?.whenToUse,
|
|
166
|
+
enabled: true,
|
|
167
|
+
kind: "bundle",
|
|
168
|
+
file: md,
|
|
169
|
+
dirBundle: true,
|
|
170
|
+
source: root.source,
|
|
171
|
+
projectRoot: root.projectRoot
|
|
172
|
+
});
|
|
173
|
+
} else if (await pathExists(disabled)) {
|
|
174
|
+
const parsed = parseFrontmatter(await readFile(disabled, "utf8").catch(() => ""));
|
|
175
|
+
entries.push({
|
|
176
|
+
name: parsed?.name ?? item.name,
|
|
177
|
+
description: parsed?.description ?? "",
|
|
178
|
+
whenToUse: parsed?.whenToUse,
|
|
179
|
+
enabled: false,
|
|
180
|
+
kind: "bundle",
|
|
181
|
+
file: disabled,
|
|
182
|
+
dirBundle: true,
|
|
183
|
+
source: root.source,
|
|
184
|
+
projectRoot: root.projectRoot
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} else if (item.isFile()) {
|
|
188
|
+
if (item.name.endsWith(".md.disabled")) {
|
|
189
|
+
const file = join(root.path, item.name);
|
|
190
|
+
const parsed = parseFrontmatter(await readFile(file, "utf8").catch(() => ""));
|
|
191
|
+
entries.push({
|
|
192
|
+
name: parsed?.name ?? item.name.slice(0, -12),
|
|
193
|
+
description: parsed?.description ?? "",
|
|
194
|
+
whenToUse: parsed?.whenToUse,
|
|
195
|
+
enabled: false,
|
|
196
|
+
kind: "flat",
|
|
197
|
+
file,
|
|
198
|
+
dirBundle: false,
|
|
199
|
+
source: root.source,
|
|
200
|
+
projectRoot: root.projectRoot
|
|
201
|
+
});
|
|
202
|
+
} else if (item.name.endsWith(".md")) {
|
|
203
|
+
const file = join(root.path, item.name);
|
|
204
|
+
const parsed = parseFrontmatter(await readFile(file, "utf8"));
|
|
205
|
+
entries.push({
|
|
206
|
+
name: parsed?.name ?? item.name.slice(0, -3),
|
|
207
|
+
description: parsed?.description ?? "",
|
|
208
|
+
whenToUse: parsed?.whenToUse,
|
|
209
|
+
enabled: true,
|
|
210
|
+
kind: "flat",
|
|
211
|
+
file,
|
|
212
|
+
dirBundle: false,
|
|
213
|
+
source: root.source,
|
|
214
|
+
projectRoot: root.projectRoot
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return entries;
|
|
220
|
+
}
|
|
221
|
+
function winnerEntry(entries, name) {
|
|
222
|
+
const matches = entries.filter((entry) => entry.name === name);
|
|
223
|
+
if (matches.length === 0) return void 0;
|
|
224
|
+
matches.sort((a, b) => sourceRank(a.source) - sourceRank(b.source));
|
|
225
|
+
return matches[0];
|
|
226
|
+
}
|
|
227
|
+
function sourceRank(source) {
|
|
228
|
+
switch (source) {
|
|
229
|
+
case "project-dsh": return 1;
|
|
230
|
+
case "project-agents": return 2;
|
|
231
|
+
case "user-dsh": return 3;
|
|
232
|
+
case "user-agents": return 4;
|
|
233
|
+
default: return 9;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/scope.ts
|
|
238
|
+
function workspaceSkillRoot(projectRoot) {
|
|
239
|
+
return join(projectRoot, ".dsh", "skills");
|
|
240
|
+
}
|
|
241
|
+
function scopeRootOf(target, dshHome) {
|
|
242
|
+
return target === null || target === void 0 ? join(dshHome, "skills") : workspaceSkillRoot(target);
|
|
243
|
+
}
|
|
244
|
+
async function normalizeWorkspaces(paths) {
|
|
245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
246
|
+
const result = [];
|
|
247
|
+
for (const raw of paths) {
|
|
248
|
+
if (typeof raw !== "string" || raw.trim() === "") continue;
|
|
249
|
+
const absolute = resolve(raw.trim());
|
|
250
|
+
const info = await stat(absolute).catch(() => void 0);
|
|
251
|
+
if (info === void 0 || !info.isDirectory()) throw new Error("工作区不存在或不是目录:\"" + raw + "\"");
|
|
252
|
+
const project = await findProjectRoot(absolute);
|
|
253
|
+
const key = process.platform === "win32" ? project.toLowerCase() : project;
|
|
254
|
+
if (seen.has(key)) continue;
|
|
255
|
+
seen.add(key);
|
|
256
|
+
result.push(project);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
async function normalizeWorkspace(raw) {
|
|
261
|
+
const list = await normalizeWorkspaces([raw]);
|
|
262
|
+
if (list.length === 0) throw new Error("至少需要指定一个存在的工作区");
|
|
263
|
+
return list[0];
|
|
264
|
+
}
|
|
265
|
+
function isBusyError(error) {
|
|
266
|
+
return error !== null && typeof error === "object" && [
|
|
267
|
+
"EPERM",
|
|
268
|
+
"EBUSY",
|
|
269
|
+
"EACCES",
|
|
270
|
+
"ENOTEMPTY"
|
|
271
|
+
].includes(error.code ?? "");
|
|
272
|
+
}
|
|
273
|
+
async function removeRetry(path) {
|
|
274
|
+
for (let attempt = 0; attempt < 5; attempt++) try {
|
|
275
|
+
await rm(path, {
|
|
276
|
+
recursive: true,
|
|
277
|
+
force: true
|
|
278
|
+
});
|
|
279
|
+
return true;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (!isBusyError(error)) throw error;
|
|
282
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 300 * (attempt + 1)));
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
async function migrateEntry(entry, targetRoot, mode) {
|
|
287
|
+
const sourceDir = entry.dirBundle ? dirname(entry.file) : entry.file;
|
|
288
|
+
const target = entry.dirBundle ? join(targetRoot, entry.name) : join(targetRoot, basename(entry.file));
|
|
289
|
+
if (resolve(sourceDir) === resolve(target)) throw new Error("技能 \"" + entry.name + "\" 已在此作用域中");
|
|
290
|
+
if (await pathExists(target)) throw new Error("目标位置已存在同名技能:\"" + target + "\"");
|
|
291
|
+
if (!await pathExists(sourceDir)) throw new Error("技能 \"" + entry.name + "\" 的源文件不存在:" + sourceDir);
|
|
292
|
+
await mkdir(targetRoot, { recursive: true });
|
|
293
|
+
if (mode === "move") try {
|
|
294
|
+
await rename(sourceDir, target);
|
|
295
|
+
return { target };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const code = error.code;
|
|
298
|
+
if (![
|
|
299
|
+
"EXDEV",
|
|
300
|
+
"EBUSY",
|
|
301
|
+
"EPERM",
|
|
302
|
+
"EACCES"
|
|
303
|
+
].includes(code)) throw new Error("移动技能文件失败:" + (error instanceof Error ? error.message : String(error)));
|
|
304
|
+
}
|
|
305
|
+
const staging = join(targetRoot, ".dsh-skill-staging-" + process.pid + "-" + Math.random().toString(36).slice(2, 8));
|
|
306
|
+
try {
|
|
307
|
+
if (entry.dirBundle) {
|
|
308
|
+
await cp(sourceDir, staging, { recursive: true });
|
|
309
|
+
await rename(staging, target);
|
|
310
|
+
} else {
|
|
311
|
+
await mkdir(staging, { recursive: true });
|
|
312
|
+
const stagedFile = join(staging, basename(entry.file));
|
|
313
|
+
await cp(entry.file, stagedFile);
|
|
314
|
+
await rename(stagedFile, target);
|
|
315
|
+
await rm(staging, {
|
|
316
|
+
recursive: true,
|
|
317
|
+
force: true
|
|
318
|
+
}).catch(() => {});
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
await rm(staging, {
|
|
322
|
+
recursive: true,
|
|
323
|
+
force: true
|
|
324
|
+
}).catch(() => {});
|
|
325
|
+
throw new Error("复制技能文件失败(已回滚):" + (error instanceof Error ? error.message : String(error)));
|
|
326
|
+
}
|
|
327
|
+
if (mode === "move") try {
|
|
328
|
+
if (!await removeRetry(sourceDir)) throw new Error("源文件删除超时");
|
|
329
|
+
} catch (error) {
|
|
330
|
+
await rm(target, {
|
|
331
|
+
recursive: true,
|
|
332
|
+
force: true
|
|
333
|
+
}).catch(() => {});
|
|
334
|
+
throw new Error("技能 \"" + entry.name + "\" 已复制到目标,但无法删除源文件(可能被占用),已回滚新副本:" + (error instanceof Error ? error.message : String(error)));
|
|
335
|
+
}
|
|
336
|
+
return { target };
|
|
337
|
+
}
|
|
338
|
+
async function batchMigrateEntries(items, targetRoot, mode) {
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const item of items) try {
|
|
341
|
+
await migrateEntry(item, targetRoot, mode);
|
|
342
|
+
results.push({
|
|
343
|
+
name: item.name,
|
|
344
|
+
ok: true
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
results.push({
|
|
348
|
+
name: item.name,
|
|
349
|
+
ok: false,
|
|
350
|
+
error: error instanceof Error ? error.message : String(error)
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/remote.ts
|
|
357
|
+
const SERVICE = "skillManage";
|
|
358
|
+
const PACKAGE = "@lijian-ui/dsh-skill-manage";
|
|
359
|
+
const sessionIdSchema = z.string().optional();
|
|
360
|
+
const scopeSchema = z.object({
|
|
361
|
+
kind: z.enum(["global", "workspace"]),
|
|
362
|
+
path: z.string().optional(),
|
|
363
|
+
label: z.string().optional()
|
|
364
|
+
});
|
|
365
|
+
const skillSummarySchema = z.object({
|
|
366
|
+
name: z.string(),
|
|
367
|
+
description: z.string(),
|
|
368
|
+
whenToUse: z.string().optional(),
|
|
369
|
+
provider: z.string(),
|
|
370
|
+
source: z.string(),
|
|
371
|
+
enabled: z.boolean(),
|
|
372
|
+
modelInvocable: z.boolean(),
|
|
373
|
+
userInvocable: z.boolean(),
|
|
374
|
+
scope: scopeSchema.optional()
|
|
375
|
+
});
|
|
376
|
+
const listResultSchema = z.object({ skills: z.array(skillSummarySchema) });
|
|
377
|
+
const workspacesResultSchema = z.object({ workspaces: z.array(z.object({
|
|
378
|
+
path: z.string(),
|
|
379
|
+
label: z.string(),
|
|
380
|
+
sessions: z.number()
|
|
381
|
+
})) });
|
|
382
|
+
const resourceBaseSchema = z.object({
|
|
383
|
+
kind: z.string(),
|
|
384
|
+
path: z.string().optional(),
|
|
385
|
+
url: z.string().optional(),
|
|
386
|
+
description: z.string().optional()
|
|
387
|
+
}).optional();
|
|
388
|
+
const skillContentSchema = z.object({
|
|
389
|
+
name: z.string(),
|
|
390
|
+
description: z.string(),
|
|
391
|
+
content: z.string(),
|
|
392
|
+
provider: z.string(),
|
|
393
|
+
whenToUse: z.string().optional(),
|
|
394
|
+
path: z.string().optional(),
|
|
395
|
+
resourceBase: resourceBaseSchema
|
|
396
|
+
}).nullable();
|
|
397
|
+
const setEnabledResultSchema = z.object({
|
|
398
|
+
name: z.string(),
|
|
399
|
+
enabled: z.boolean()
|
|
400
|
+
});
|
|
401
|
+
const deleteSkillResultSchema = z.object({ name: z.string() });
|
|
402
|
+
const migratePayloadSchema = z.object({
|
|
403
|
+
target: z.string().nullable(),
|
|
404
|
+
mode: z.enum(["copy", "move"])
|
|
405
|
+
});
|
|
406
|
+
const migrateResultSchema = z.object({
|
|
407
|
+
name: z.string(),
|
|
408
|
+
scope: scopeSchema
|
|
409
|
+
});
|
|
410
|
+
const batchMigratePayloadSchema = z.object({
|
|
411
|
+
from: z.string().nullable(),
|
|
412
|
+
targets: z.array(z.string().nullable()).min(1),
|
|
413
|
+
mode: z.enum(["copy", "move"]),
|
|
414
|
+
names: z.array(z.string())
|
|
415
|
+
});
|
|
416
|
+
const batchMigrateResultSchema = z.object({ results: z.array(z.object({
|
|
417
|
+
name: z.string(),
|
|
418
|
+
target: z.string().nullable().optional(),
|
|
419
|
+
ok: z.boolean(),
|
|
420
|
+
error: z.string().optional()
|
|
421
|
+
})) });
|
|
422
|
+
const addFileSchema = z.object({
|
|
423
|
+
path: z.string(),
|
|
424
|
+
base64: z.string()
|
|
425
|
+
});
|
|
426
|
+
const addPayloadSchema = z.object({
|
|
427
|
+
kind: z.enum(["bundle", "flat"]),
|
|
428
|
+
files: z.array(addFileSchema).min(1),
|
|
429
|
+
workspace: z.string().nullable().optional()
|
|
430
|
+
});
|
|
431
|
+
const addResultSchema = z.object({
|
|
432
|
+
name: z.string(),
|
|
433
|
+
kind: z.enum(["bundle", "flat"]),
|
|
434
|
+
scope: scopeSchema
|
|
435
|
+
});
|
|
436
|
+
const MANIFEST = {
|
|
437
|
+
package: PACKAGE,
|
|
438
|
+
face: "host",
|
|
439
|
+
schemas: [],
|
|
440
|
+
invocations: [
|
|
441
|
+
{
|
|
442
|
+
id: `${PACKAGE}#${SERVICE}/list`,
|
|
443
|
+
service: SERVICE,
|
|
444
|
+
namespace: SERVICE,
|
|
445
|
+
method: "list",
|
|
446
|
+
invocation: { kind: "direct" },
|
|
447
|
+
parameters: [{
|
|
448
|
+
name: "sessionId",
|
|
449
|
+
wire: "sessionId",
|
|
450
|
+
source: "json",
|
|
451
|
+
acceptsUndefined: true,
|
|
452
|
+
codec: {
|
|
453
|
+
mode: "strict",
|
|
454
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
455
|
+
schema: sessionIdSchema
|
|
456
|
+
}
|
|
457
|
+
}],
|
|
458
|
+
result: {
|
|
459
|
+
mode: "strict",
|
|
460
|
+
typeSymbol: `${PACKAGE}#SkillListResult`,
|
|
461
|
+
schema: listResultSchema
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: `${PACKAGE}#${SERVICE}/workspaces`,
|
|
466
|
+
service: SERVICE,
|
|
467
|
+
namespace: SERVICE,
|
|
468
|
+
method: "workspaces",
|
|
469
|
+
invocation: { kind: "direct" },
|
|
470
|
+
parameters: [],
|
|
471
|
+
result: {
|
|
472
|
+
mode: "strict",
|
|
473
|
+
typeSymbol: `${PACKAGE}#WorkspacesResult`,
|
|
474
|
+
schema: workspacesResultSchema
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: `${PACKAGE}#${SERVICE}/content`,
|
|
479
|
+
service: SERVICE,
|
|
480
|
+
namespace: SERVICE,
|
|
481
|
+
method: "content",
|
|
482
|
+
invocation: { kind: "direct" },
|
|
483
|
+
parameters: [{
|
|
484
|
+
name: "name",
|
|
485
|
+
wire: "name",
|
|
486
|
+
source: "json",
|
|
487
|
+
codec: {
|
|
488
|
+
mode: "strict",
|
|
489
|
+
typeSymbol: `${PACKAGE}#SkillName`,
|
|
490
|
+
schema: z.string()
|
|
491
|
+
}
|
|
492
|
+
}, {
|
|
493
|
+
name: "sessionId",
|
|
494
|
+
wire: "sessionId",
|
|
495
|
+
source: "json",
|
|
496
|
+
acceptsUndefined: true,
|
|
497
|
+
codec: {
|
|
498
|
+
mode: "strict",
|
|
499
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
500
|
+
schema: sessionIdSchema
|
|
501
|
+
}
|
|
502
|
+
}],
|
|
503
|
+
result: {
|
|
504
|
+
mode: "strict",
|
|
505
|
+
typeSymbol: `${PACKAGE}#SkillContent`,
|
|
506
|
+
schema: skillContentSchema
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: `${PACKAGE}#${SERVICE}/setEnabled`,
|
|
511
|
+
service: SERVICE,
|
|
512
|
+
namespace: SERVICE,
|
|
513
|
+
method: "setEnabled",
|
|
514
|
+
invocation: { kind: "direct" },
|
|
515
|
+
parameters: [
|
|
516
|
+
{
|
|
517
|
+
name: "name",
|
|
518
|
+
wire: "name",
|
|
519
|
+
source: "json",
|
|
520
|
+
codec: {
|
|
521
|
+
mode: "strict",
|
|
522
|
+
typeSymbol: `${PACKAGE}#SkillName`,
|
|
523
|
+
schema: z.string()
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "sessionId",
|
|
528
|
+
wire: "sessionId",
|
|
529
|
+
source: "json",
|
|
530
|
+
acceptsUndefined: true,
|
|
531
|
+
codec: {
|
|
532
|
+
mode: "strict",
|
|
533
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
534
|
+
schema: sessionIdSchema
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "enabled",
|
|
539
|
+
wire: "enabled",
|
|
540
|
+
source: "json",
|
|
541
|
+
codec: {
|
|
542
|
+
mode: "strict",
|
|
543
|
+
typeSymbol: `${PACKAGE}#EnabledFlag`,
|
|
544
|
+
schema: z.boolean()
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
result: {
|
|
549
|
+
mode: "strict",
|
|
550
|
+
typeSymbol: `${PACKAGE}#SetEnabledResult`,
|
|
551
|
+
schema: setEnabledResultSchema
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: `${PACKAGE}#${SERVICE}/migrate`,
|
|
556
|
+
service: SERVICE,
|
|
557
|
+
namespace: SERVICE,
|
|
558
|
+
method: "migrate",
|
|
559
|
+
invocation: { kind: "direct" },
|
|
560
|
+
parameters: [
|
|
561
|
+
{
|
|
562
|
+
name: "name",
|
|
563
|
+
wire: "name",
|
|
564
|
+
source: "json",
|
|
565
|
+
codec: {
|
|
566
|
+
mode: "strict",
|
|
567
|
+
typeSymbol: `${PACKAGE}#SkillName`,
|
|
568
|
+
schema: z.string()
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
name: "sessionId",
|
|
573
|
+
wire: "sessionId",
|
|
574
|
+
source: "json",
|
|
575
|
+
acceptsUndefined: true,
|
|
576
|
+
codec: {
|
|
577
|
+
mode: "strict",
|
|
578
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
579
|
+
schema: sessionIdSchema
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "payload",
|
|
584
|
+
wire: "payload",
|
|
585
|
+
source: "json",
|
|
586
|
+
codec: {
|
|
587
|
+
mode: "strict",
|
|
588
|
+
typeSymbol: `${PACKAGE}#MigratePayload`,
|
|
589
|
+
schema: migratePayloadSchema
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
result: {
|
|
594
|
+
mode: "strict",
|
|
595
|
+
typeSymbol: `${PACKAGE}#MigrateResult`,
|
|
596
|
+
schema: migrateResultSchema
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: `${PACKAGE}#${SERVICE}/batchMigrate`,
|
|
601
|
+
service: SERVICE,
|
|
602
|
+
namespace: SERVICE,
|
|
603
|
+
method: "batchMigrate",
|
|
604
|
+
invocation: { kind: "direct" },
|
|
605
|
+
parameters: [{
|
|
606
|
+
name: "sessionId",
|
|
607
|
+
wire: "sessionId",
|
|
608
|
+
source: "json",
|
|
609
|
+
acceptsUndefined: true,
|
|
610
|
+
codec: {
|
|
611
|
+
mode: "strict",
|
|
612
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
613
|
+
schema: sessionIdSchema
|
|
614
|
+
}
|
|
615
|
+
}, {
|
|
616
|
+
name: "payload",
|
|
617
|
+
wire: "payload",
|
|
618
|
+
source: "json",
|
|
619
|
+
codec: {
|
|
620
|
+
mode: "strict",
|
|
621
|
+
typeSymbol: `${PACKAGE}#BatchMigratePayload`,
|
|
622
|
+
schema: batchMigratePayloadSchema
|
|
623
|
+
}
|
|
624
|
+
}],
|
|
625
|
+
result: {
|
|
626
|
+
mode: "strict",
|
|
627
|
+
typeSymbol: `${PACKAGE}#BatchMigrateResult`,
|
|
628
|
+
schema: batchMigrateResultSchema
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: `${PACKAGE}#${SERVICE}/deleteSkill`,
|
|
633
|
+
service: SERVICE,
|
|
634
|
+
namespace: SERVICE,
|
|
635
|
+
method: "deleteSkill",
|
|
636
|
+
invocation: { kind: "direct" },
|
|
637
|
+
parameters: [{
|
|
638
|
+
name: "name",
|
|
639
|
+
wire: "name",
|
|
640
|
+
source: "json",
|
|
641
|
+
codec: {
|
|
642
|
+
mode: "strict",
|
|
643
|
+
typeSymbol: `${PACKAGE}#SkillName`,
|
|
644
|
+
schema: z.string()
|
|
645
|
+
}
|
|
646
|
+
}, {
|
|
647
|
+
name: "sessionId",
|
|
648
|
+
wire: "sessionId",
|
|
649
|
+
source: "json",
|
|
650
|
+
acceptsUndefined: true,
|
|
651
|
+
codec: {
|
|
652
|
+
mode: "strict",
|
|
653
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
654
|
+
schema: sessionIdSchema
|
|
655
|
+
}
|
|
656
|
+
}],
|
|
657
|
+
result: {
|
|
658
|
+
mode: "strict",
|
|
659
|
+
typeSymbol: `${PACKAGE}#DeleteSkillResult`,
|
|
660
|
+
schema: deleteSkillResultSchema
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: `${PACKAGE}#${SERVICE}/addSkill`,
|
|
665
|
+
service: SERVICE,
|
|
666
|
+
namespace: SERVICE,
|
|
667
|
+
method: "addSkill",
|
|
668
|
+
invocation: { kind: "direct" },
|
|
669
|
+
parameters: [{
|
|
670
|
+
name: "sessionId",
|
|
671
|
+
wire: "sessionId",
|
|
672
|
+
source: "json",
|
|
673
|
+
acceptsUndefined: true,
|
|
674
|
+
codec: {
|
|
675
|
+
mode: "strict",
|
|
676
|
+
typeSymbol: `${PACKAGE}#sessionId`,
|
|
677
|
+
schema: sessionIdSchema
|
|
678
|
+
}
|
|
679
|
+
}, {
|
|
680
|
+
name: "payload",
|
|
681
|
+
wire: "payload",
|
|
682
|
+
source: "json",
|
|
683
|
+
codec: {
|
|
684
|
+
mode: "strict",
|
|
685
|
+
typeSymbol: `${PACKAGE}#AddPayload`,
|
|
686
|
+
schema: addPayloadSchema
|
|
687
|
+
}
|
|
688
|
+
}],
|
|
689
|
+
result: {
|
|
690
|
+
mode: "strict",
|
|
691
|
+
typeSymbol: `${PACKAGE}#AddResult`,
|
|
692
|
+
schema: addResultSchema
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
],
|
|
696
|
+
model: {
|
|
697
|
+
services: [],
|
|
698
|
+
events: [],
|
|
699
|
+
objects: []
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const MAX_ADD_FILES = 200;
|
|
703
|
+
const MAX_ADD_TOTAL_BYTES = 8388608;
|
|
704
|
+
var SkillManageApi = class extends TypertRemoteService {
|
|
705
|
+
constructor(ctx) {
|
|
706
|
+
super(ctx, SERVICE);
|
|
707
|
+
}
|
|
708
|
+
registryFor(sessionId) {
|
|
709
|
+
const live = sessionId === void 0 ? void 0 : this.ctx.agents.get(sessionId);
|
|
710
|
+
if (live !== void 0) {
|
|
711
|
+
const result = this.ctx.get("agentPresets")?.serviceFor?.(live, "skills");
|
|
712
|
+
if (result !== void 0) return result;
|
|
713
|
+
}
|
|
714
|
+
return this.ctx.skills;
|
|
715
|
+
}
|
|
716
|
+
viewFor(sessionId) {
|
|
717
|
+
const registry = this.registryFor(sessionId);
|
|
718
|
+
const session = sessionId === void 0 ? void 0 : this.ctx.sessions.get(sessionId);
|
|
719
|
+
const scope = sessionId === void 0 ? void 0 : this.ctx.agents.get(sessionId);
|
|
720
|
+
return {
|
|
721
|
+
registry,
|
|
722
|
+
cwd: session?.header?.cwd,
|
|
723
|
+
scope
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
homes() {
|
|
727
|
+
return {
|
|
728
|
+
dshHome: resolveDshHome(),
|
|
729
|
+
agentsHome: resolve(process.env.DSH_AGENTS_HOME?.trim() ? process.env.DSH_AGENTS_HOME : join(homedir(), ".agents"))
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
isWithin(baseDir, candidate) {
|
|
733
|
+
if (typeof candidate !== "string" || candidate === "") return false;
|
|
734
|
+
const base = resolve(baseDir);
|
|
735
|
+
const value = resolve(candidate);
|
|
736
|
+
if (value === base) return true;
|
|
737
|
+
const b = process.platform === "win32" ? base.toLowerCase() : base;
|
|
738
|
+
const v = process.platform === "win32" ? value.toLowerCase() : value;
|
|
739
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
740
|
+
return v.startsWith(b.endsWith(sep) ? b : b + sep);
|
|
741
|
+
}
|
|
742
|
+
async allRoots() {
|
|
743
|
+
const { dshHome, agentsHome } = this.homes();
|
|
744
|
+
const roots = [];
|
|
745
|
+
const seen = /* @__PURE__ */ new Set();
|
|
746
|
+
const push = (path, source, projectRoot) => {
|
|
747
|
+
const normalized = resolve(path);
|
|
748
|
+
const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
749
|
+
if (seen.has(key)) return;
|
|
750
|
+
seen.add(key);
|
|
751
|
+
roots.push({
|
|
752
|
+
path,
|
|
753
|
+
source,
|
|
754
|
+
projectRoot
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
push(join(dshHome, "skills"), "user-dsh", void 0);
|
|
758
|
+
push(join(agentsHome, "skills"), "user-agents", void 0);
|
|
759
|
+
for (const workspace of (await this.workspaces()).workspaces) {
|
|
760
|
+
push(join(workspace.path, ".dsh", "skills"), "project-dsh", workspace.path);
|
|
761
|
+
push(join(workspace.path, ".agents", "skills"), "project-agents", workspace.path);
|
|
762
|
+
}
|
|
763
|
+
return roots;
|
|
764
|
+
}
|
|
765
|
+
async fileEntriesAll() {
|
|
766
|
+
return collectSkillEntries(await this.allRoots());
|
|
767
|
+
}
|
|
768
|
+
workspaceOfPath(path, roots) {
|
|
769
|
+
if (typeof path !== "string" || path === "") return void 0;
|
|
770
|
+
for (const root of roots) {
|
|
771
|
+
if (root.projectRoot === void 0) continue;
|
|
772
|
+
if (this.isWithin(root.path, path)) return root.projectRoot;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
scopeForEntry(entry) {
|
|
776
|
+
if (entry.projectRoot !== void 0) return {
|
|
777
|
+
kind: "workspace",
|
|
778
|
+
path: entry.projectRoot,
|
|
779
|
+
label: basename(entry.projectRoot) || entry.projectRoot
|
|
780
|
+
};
|
|
781
|
+
return { kind: "global" };
|
|
782
|
+
}
|
|
783
|
+
scopeForTarget(targetRoot, targetProject) {
|
|
784
|
+
const { dshHome } = this.homes();
|
|
785
|
+
if (resolve(targetRoot) === resolve(join(dshHome, "skills"))) return { kind: "global" };
|
|
786
|
+
return {
|
|
787
|
+
kind: "workspace",
|
|
788
|
+
path: targetProject ?? void 0,
|
|
789
|
+
label: targetProject !== void 0 && targetProject !== null ? basename(targetProject) || targetProject : void 0
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async list(sessionId) {
|
|
793
|
+
const { registry, cwd, scope } = this.viewFor(sessionId);
|
|
794
|
+
const roots = await this.allRoots();
|
|
795
|
+
const listed = await registry.list({
|
|
796
|
+
cwd,
|
|
797
|
+
scope
|
|
798
|
+
});
|
|
799
|
+
const skills = [];
|
|
800
|
+
const seen = /* @__PURE__ */ new Set();
|
|
801
|
+
const seenKey = (name, scopePath) => name + "\0" + (scopePath ?? "global");
|
|
802
|
+
for (const skill of listed) {
|
|
803
|
+
if (this.workspaceOfPath(skill.path, roots) !== void 0) continue;
|
|
804
|
+
const source = skill.source ?? (skill.provider === "runtime" ? "runtime" : "");
|
|
805
|
+
skills.push({
|
|
806
|
+
name: skill.name,
|
|
807
|
+
description: skill.description,
|
|
808
|
+
...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
|
|
809
|
+
provider: skill.provider,
|
|
810
|
+
source,
|
|
811
|
+
enabled: true,
|
|
812
|
+
modelInvocable: skill.invocation.modelInvocable,
|
|
813
|
+
userInvocable: skill.invocation.userInvocable,
|
|
814
|
+
scope: { kind: "global" }
|
|
815
|
+
});
|
|
816
|
+
seen.add(seenKey(skill.name, "global"));
|
|
817
|
+
}
|
|
818
|
+
for (const entry of await collectSkillEntries(roots)) {
|
|
819
|
+
const scopePath = entry.projectRoot ?? "global";
|
|
820
|
+
if (seen.has(seenKey(entry.name, scopePath))) continue;
|
|
821
|
+
seen.add(seenKey(entry.name, scopePath));
|
|
822
|
+
skills.push({
|
|
823
|
+
name: entry.name,
|
|
824
|
+
description: entry.description,
|
|
825
|
+
...entry.whenToUse === void 0 ? {} : { whenToUse: entry.whenToUse },
|
|
826
|
+
provider: "filesystem",
|
|
827
|
+
source: entry.source,
|
|
828
|
+
enabled: entry.enabled,
|
|
829
|
+
modelInvocable: false,
|
|
830
|
+
userInvocable: false,
|
|
831
|
+
scope: this.scopeForEntry(entry)
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return { skills };
|
|
835
|
+
}
|
|
836
|
+
async workspaces() {
|
|
837
|
+
const map = /* @__PURE__ */ new Map();
|
|
838
|
+
const keyOf = (path) => process.platform === "win32" ? path.toLowerCase() : path;
|
|
839
|
+
const add = async (path, label, sessions) => {
|
|
840
|
+
if (typeof path !== "string" || path === "") return;
|
|
841
|
+
let project;
|
|
842
|
+
try {
|
|
843
|
+
project = await findProjectRoot(resolve(path));
|
|
844
|
+
} catch {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const key = keyOf(project);
|
|
848
|
+
if (map.has(key)) return;
|
|
849
|
+
map.set(key, {
|
|
850
|
+
path: project,
|
|
851
|
+
label: label || basename(project) || project,
|
|
852
|
+
sessions: sessions ?? 0
|
|
853
|
+
});
|
|
854
|
+
};
|
|
855
|
+
try {
|
|
856
|
+
const registry = this.ctx.get("workspaceRegistry");
|
|
857
|
+
if (registry !== void 0 && typeof registry.list === "function") for (const workspace of registry.list()) {
|
|
858
|
+
try {
|
|
859
|
+
if (await workspace.status() !== "ok") continue;
|
|
860
|
+
} catch {}
|
|
861
|
+
await add(workspace.path, workspace.title, Array.isArray(workspace.sessionIds) ? workspace.sessionIds.length : 0);
|
|
862
|
+
}
|
|
863
|
+
} catch {}
|
|
864
|
+
try {
|
|
865
|
+
for (const session of this.ctx.sessions.list()) {
|
|
866
|
+
const cwd = session.header?.cwd;
|
|
867
|
+
if (cwd === void 0 || cwd === "") continue;
|
|
868
|
+
await add(resolve(cwd), void 0, 1);
|
|
869
|
+
}
|
|
870
|
+
} catch {}
|
|
871
|
+
return { workspaces: [...map.values()].sort((a, b) => a.label.localeCompare(b.label) || a.path.localeCompare(b.path)) };
|
|
872
|
+
}
|
|
873
|
+
async locate(name, sessionId) {
|
|
874
|
+
const { registry, cwd, scope } = this.viewFor(sessionId);
|
|
875
|
+
const skill = await registry.get(name, {
|
|
876
|
+
cwd,
|
|
877
|
+
scope
|
|
878
|
+
});
|
|
879
|
+
if (skill !== void 0 && this.workspaceOfPath(skill.path, await this.allRoots()) === void 0) return {
|
|
880
|
+
kind: "live",
|
|
881
|
+
skill
|
|
882
|
+
};
|
|
883
|
+
const entry = winnerEntry(await this.fileEntriesAll(), name);
|
|
884
|
+
if (entry !== void 0) return {
|
|
885
|
+
kind: "file",
|
|
886
|
+
entry
|
|
887
|
+
};
|
|
888
|
+
return { kind: "missing" };
|
|
889
|
+
}
|
|
890
|
+
async content(name, sessionId) {
|
|
891
|
+
const located = await this.locate(name, sessionId);
|
|
892
|
+
if (located.kind === "missing") return null;
|
|
893
|
+
if (located.kind === "file") {
|
|
894
|
+
const raw = await readFile(located.entry.file, "utf8");
|
|
895
|
+
return {
|
|
896
|
+
name: located.entry.name,
|
|
897
|
+
description: located.entry.description,
|
|
898
|
+
content: raw,
|
|
899
|
+
provider: "filesystem",
|
|
900
|
+
path: located.entry.file
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const skill = located.skill;
|
|
904
|
+
return {
|
|
905
|
+
name: skill.name,
|
|
906
|
+
description: skill.description,
|
|
907
|
+
content: skill.content,
|
|
908
|
+
provider: skill.provider,
|
|
909
|
+
...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
|
|
910
|
+
...skill.path === void 0 ? {} : { path: skill.path },
|
|
911
|
+
...skill.resourceBase === void 0 ? {} : { resourceBase: skill.resourceBase }
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
assertEditable(skill) {
|
|
915
|
+
if (skill.source === "bundled") throw new Error("技能 \"" + skill.name + "\" 随部署附带,不可修改");
|
|
916
|
+
if (typeof skill.path !== "string" || skill.path.length === 0) throw new Error("技能 \"" + skill.name + "\" 没有可修改的文件");
|
|
917
|
+
}
|
|
918
|
+
async setEnabled(name, sessionId, enabled) {
|
|
919
|
+
const located = await this.locate(name, sessionId);
|
|
920
|
+
if (located.kind === "missing") throw new Error("技能 \"" + name + "\" 不存在");
|
|
921
|
+
if (located.kind === "live") {
|
|
922
|
+
const skill = located.skill;
|
|
923
|
+
this.assertEditable(skill);
|
|
924
|
+
if (enabled) return {
|
|
925
|
+
name,
|
|
926
|
+
enabled: true
|
|
927
|
+
};
|
|
928
|
+
const target = skill.path + DISABLED_SUFFIX;
|
|
929
|
+
if (await pathExists(target)) throw new Error("目标文件已存在:" + target);
|
|
930
|
+
await rename(skill.path, target);
|
|
931
|
+
return {
|
|
932
|
+
name,
|
|
933
|
+
enabled: false
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const entry = located.entry;
|
|
937
|
+
if (enabled === entry.enabled) return {
|
|
938
|
+
name,
|
|
939
|
+
enabled
|
|
940
|
+
};
|
|
941
|
+
const target = enabled ? entry.file.slice(0, -9) : entry.file + DISABLED_SUFFIX;
|
|
942
|
+
if (await pathExists(target)) throw new Error("目标文件已存在:" + target);
|
|
943
|
+
await rename(entry.file, target);
|
|
944
|
+
return {
|
|
945
|
+
name,
|
|
946
|
+
enabled
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
async deleteSkill(name, sessionId) {
|
|
950
|
+
const located = await this.locate(name, sessionId);
|
|
951
|
+
if (located.kind === "missing") throw new Error("技能 \"" + name + "\" 不存在");
|
|
952
|
+
if (located.kind === "live") {
|
|
953
|
+
const skill = located.skill;
|
|
954
|
+
this.assertEditable(skill);
|
|
955
|
+
if (basename(skill.path) === "SKILL.md") await rm(dirname(skill.path), {
|
|
956
|
+
recursive: true,
|
|
957
|
+
force: true
|
|
958
|
+
});
|
|
959
|
+
else await rm(skill.path, { force: true });
|
|
960
|
+
return { name };
|
|
961
|
+
}
|
|
962
|
+
const entry = located.entry;
|
|
963
|
+
if (entry.dirBundle) await rm(dirname(entry.file), {
|
|
964
|
+
recursive: true,
|
|
965
|
+
force: true
|
|
966
|
+
});
|
|
967
|
+
else await rm(entry.file, { force: true });
|
|
968
|
+
return { name };
|
|
969
|
+
}
|
|
970
|
+
async migratableEntry(name, sessionId) {
|
|
971
|
+
const entry = winnerEntry(await this.fileEntriesAll(), name);
|
|
972
|
+
if (entry !== void 0) return entry;
|
|
973
|
+
const located = await this.locate(name, sessionId);
|
|
974
|
+
if (located.kind !== "live") return void 0;
|
|
975
|
+
const skill = located.skill;
|
|
976
|
+
if (typeof skill.path !== "string" || skill.path === "") return void 0;
|
|
977
|
+
const { dshHome, agentsHome } = this.homes();
|
|
978
|
+
if (![join(dshHome, "skills"), join(agentsHome, "skills")].some((root) => this.isWithin(root, skill.path))) return void 0;
|
|
979
|
+
this.assertEditable(skill);
|
|
980
|
+
return {
|
|
981
|
+
name: skill.name,
|
|
982
|
+
file: skill.path,
|
|
983
|
+
dirBundle: basename(skill.path) === "SKILL.md",
|
|
984
|
+
enabled: true,
|
|
985
|
+
source: skill.source ?? "user-dsh"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async migrate(name, sessionId, payload) {
|
|
989
|
+
const { target: rawTarget, mode } = payload;
|
|
990
|
+
const { dshHome } = this.homes();
|
|
991
|
+
const targetProject = rawTarget === null || rawTarget === void 0 ? null : await normalizeWorkspace(rawTarget);
|
|
992
|
+
const targetRoot = scopeRootOf(targetProject, dshHome);
|
|
993
|
+
const entry = await this.migratableEntry(name, sessionId);
|
|
994
|
+
if (entry === void 0) throw new Error("技能 \"" + name + "\" 没有可迁移的文件(随部署附带或运行时内置的技能不可迁移)");
|
|
995
|
+
await migrateEntry(entry, targetRoot, mode);
|
|
996
|
+
return {
|
|
997
|
+
name,
|
|
998
|
+
scope: this.scopeForTarget(targetRoot, targetProject)
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async batchMigrate(sessionId, payload) {
|
|
1002
|
+
const { from: rawFrom, targets: rawTargets, mode, names } = payload;
|
|
1003
|
+
if (mode === "move" && rawTargets.length > 1) throw new Error("移动模式只能选择一个目标作用域(多个目标请改用复制)");
|
|
1004
|
+
const { dshHome, agentsHome } = this.homes();
|
|
1005
|
+
const fromProject = rawFrom === null || rawFrom === void 0 ? null : await normalizeWorkspace(rawFrom);
|
|
1006
|
+
const fromRoots = fromProject === null ? [{
|
|
1007
|
+
path: join(dshHome, "skills"),
|
|
1008
|
+
source: "user-dsh"
|
|
1009
|
+
}, {
|
|
1010
|
+
path: join(agentsHome, "skills"),
|
|
1011
|
+
source: "user-agents"
|
|
1012
|
+
}] : [{
|
|
1013
|
+
path: workspaceSkillRoot(fromProject),
|
|
1014
|
+
source: "project-dsh",
|
|
1015
|
+
projectRoot: fromProject
|
|
1016
|
+
}];
|
|
1017
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1018
|
+
for (const entry of await collectSkillEntries(fromRoots)) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
1019
|
+
const chosen = [];
|
|
1020
|
+
const results = [];
|
|
1021
|
+
for (const name of names) {
|
|
1022
|
+
const entry = byName.get(name);
|
|
1023
|
+
if (entry === void 0) results.push({
|
|
1024
|
+
name,
|
|
1025
|
+
ok: false,
|
|
1026
|
+
error: "技能 \"" + name + "\" 不在源作用域中"
|
|
1027
|
+
});
|
|
1028
|
+
else chosen.push(entry);
|
|
1029
|
+
}
|
|
1030
|
+
for (const rawTarget of rawTargets) {
|
|
1031
|
+
const targetRoot = scopeRootOf(rawTarget === null || rawTarget === void 0 ? null : await normalizeWorkspace(rawTarget), dshHome);
|
|
1032
|
+
if (fromRoots.some((root) => resolve(root.path) === resolve(targetRoot))) {
|
|
1033
|
+
for (const entry of chosen) results.push({
|
|
1034
|
+
name: entry.name,
|
|
1035
|
+
target: rawTarget ?? null,
|
|
1036
|
+
ok: false,
|
|
1037
|
+
error: "目标作用域与源作用域相同"
|
|
1038
|
+
});
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
for (const item of await batchMigrateEntries(chosen, targetRoot, mode)) results.push({
|
|
1042
|
+
name: item.name,
|
|
1043
|
+
target: rawTarget ?? null,
|
|
1044
|
+
ok: item.ok,
|
|
1045
|
+
...item.error === void 0 ? {} : { error: item.error }
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return { results };
|
|
1049
|
+
}
|
|
1050
|
+
async addSkill(sessionId, payload) {
|
|
1051
|
+
const { kind, files, workspace: rawWorkspace } = payload;
|
|
1052
|
+
if (files.length > MAX_ADD_FILES) throw new Error("文件数量过多(最多 200 个)");
|
|
1053
|
+
const decoded = files.map((file) => {
|
|
1054
|
+
const data = Buffer.from(file.base64, "base64");
|
|
1055
|
+
if (data.length === 0 && file.base64.length > 0) throw new Error("文件内容解码失败:" + file.path);
|
|
1056
|
+
return {
|
|
1057
|
+
path: file.path.replaceAll("\\", "/"),
|
|
1058
|
+
data
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
if (decoded.reduce((sum, file) => sum + file.data.length, 0) > MAX_ADD_TOTAL_BYTES) throw new Error("技能总大小超过 8MB 上限");
|
|
1062
|
+
for (const file of decoded) if (file.path.startsWith("/") || file.path.split("/").some((segment) => segment === ".." || segment === ".")) throw new Error("非法文件路径:" + file.path);
|
|
1063
|
+
const { dshHome } = this.homes();
|
|
1064
|
+
let targetProject;
|
|
1065
|
+
let targetRoot;
|
|
1066
|
+
if (rawWorkspace === void 0 || rawWorkspace === null || rawWorkspace === "") targetRoot = join(dshHome, "skills");
|
|
1067
|
+
else {
|
|
1068
|
+
targetProject = await normalizeWorkspace(rawWorkspace);
|
|
1069
|
+
targetRoot = workspaceSkillRoot(targetProject);
|
|
1070
|
+
}
|
|
1071
|
+
let name;
|
|
1072
|
+
let writes;
|
|
1073
|
+
if (kind === "bundle") {
|
|
1074
|
+
const tops = new Set(decoded.map((file) => file.path.split("/")[0]));
|
|
1075
|
+
if (tops.size !== 1 || decoded.some((file) => file.path.split("/").length < 2)) throw new Error("技能文件夹结构不正确:所有文件应位于同一个文件夹内");
|
|
1076
|
+
const top = [...tops][0];
|
|
1077
|
+
const skillFile = decoded.find((file) => file.path === top + "/SKILL.md");
|
|
1078
|
+
if (skillFile === void 0) throw new Error("技能文件夹缺少顶层的 SKILL.md 文件");
|
|
1079
|
+
const validation = validateFrontmatter(skillFile.data.toString("utf8"));
|
|
1080
|
+
if (!validation.ok) throw new Error("技能格式不符合要求:" + validation.error);
|
|
1081
|
+
name = validation.skill.name;
|
|
1082
|
+
writes = decoded.map((file) => ({
|
|
1083
|
+
relative: file.path.slice(top.length + 1),
|
|
1084
|
+
data: file.data
|
|
1085
|
+
}));
|
|
1086
|
+
} else {
|
|
1087
|
+
if (decoded.length !== 1) throw new Error("单个技能文件一次只能添加一个");
|
|
1088
|
+
const file = decoded[0];
|
|
1089
|
+
const flatName = file.path.split("/").filter(Boolean).pop() ?? "";
|
|
1090
|
+
if (!flatName.toLowerCase().endsWith(".md")) throw new Error("技能文件必须是 .md 文件");
|
|
1091
|
+
const validation = validateFrontmatter(file.data.toString("utf8"));
|
|
1092
|
+
if (!validation.ok) throw new Error("技能格式不符合要求:" + validation.error);
|
|
1093
|
+
name = validation.skill.name;
|
|
1094
|
+
writes = [{
|
|
1095
|
+
relative: flatName,
|
|
1096
|
+
data: file.data
|
|
1097
|
+
}];
|
|
1098
|
+
}
|
|
1099
|
+
const existing = winnerEntry(await this.fileEntriesAll(), name);
|
|
1100
|
+
if (existing !== void 0) throw new Error("同名技能 \"" + name + "\" 已存在(" + (existing.enabled ? "已启用" : "已停用") + ",位于 " + (existing.projectRoot !== void 0 ? existing.projectRoot : "全局用户根") + ")");
|
|
1101
|
+
const { registry, cwd, scope } = this.viewFor(sessionId);
|
|
1102
|
+
if ((await registry.list({
|
|
1103
|
+
cwd,
|
|
1104
|
+
scope
|
|
1105
|
+
})).some((skill) => skill.name === name)) throw new Error("同名技能 \"" + name + "\" 已存在");
|
|
1106
|
+
const target = kind === "bundle" ? join(targetRoot, name) : join(targetRoot, writes[0].relative);
|
|
1107
|
+
const staging = join(targetRoot, ".dsh-skill-staging-" + process.pid + "-" + Math.random().toString(36).slice(2, 8));
|
|
1108
|
+
try {
|
|
1109
|
+
if (kind === "bundle") {
|
|
1110
|
+
for (const write of writes) {
|
|
1111
|
+
const filePath = join(staging, write.relative);
|
|
1112
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1113
|
+
await writeFile(filePath, write.data);
|
|
1114
|
+
}
|
|
1115
|
+
await rename(staging, target);
|
|
1116
|
+
} else {
|
|
1117
|
+
await mkdir(staging, { recursive: true });
|
|
1118
|
+
const stagedFile = join(staging, writes[0].relative);
|
|
1119
|
+
await writeFile(stagedFile, writes[0].data);
|
|
1120
|
+
await rename(stagedFile, target);
|
|
1121
|
+
await rm(staging, {
|
|
1122
|
+
recursive: true,
|
|
1123
|
+
force: true
|
|
1124
|
+
}).catch(() => {});
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
await rm(staging, {
|
|
1128
|
+
recursive: true,
|
|
1129
|
+
force: true
|
|
1130
|
+
}).catch(() => {});
|
|
1131
|
+
await rm(target, {
|
|
1132
|
+
recursive: true,
|
|
1133
|
+
force: true
|
|
1134
|
+
}).catch(() => {});
|
|
1135
|
+
throw new Error("写入技能文件失败(已回滚):" + (error instanceof Error ? error.message : String(error)));
|
|
1136
|
+
}
|
|
1137
|
+
if (!await this.waitForDiscovery(name, sessionId, targetProject ?? cwd)) {
|
|
1138
|
+
await rm(target, {
|
|
1139
|
+
recursive: true,
|
|
1140
|
+
force: true
|
|
1141
|
+
}).catch(() => {});
|
|
1142
|
+
throw new Error("DSH 未接受该技能(格式校验未通过),已回滚。请检查 frontmatter 后重试");
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
name,
|
|
1146
|
+
kind,
|
|
1147
|
+
scope: targetProject !== void 0 && targetProject !== null ? {
|
|
1148
|
+
kind: "workspace",
|
|
1149
|
+
path: targetProject,
|
|
1150
|
+
label: basename(targetProject) || targetProject
|
|
1151
|
+
} : { kind: "global" }
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
async waitForDiscovery(name, sessionId, probeCwd) {
|
|
1155
|
+
const { registry, scope } = this.viewFor(sessionId);
|
|
1156
|
+
for (let attempt = 0; attempt < 12; attempt++) {
|
|
1157
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
|
|
1158
|
+
try {
|
|
1159
|
+
if (await registry.get(name, {
|
|
1160
|
+
cwd: probeCwd,
|
|
1161
|
+
scope
|
|
1162
|
+
}) !== void 0) return true;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
function registerSkillManageRemote(ctx) {
|
|
1171
|
+
new SkillManageApi(ctx);
|
|
1172
|
+
ctx.effect(() => ctx.typert.register(MANIFEST), "skill-manage: typert manifest");
|
|
1173
|
+
}
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/index.ts
|
|
1176
|
+
const inject = [
|
|
1177
|
+
"typert",
|
|
1178
|
+
"settings",
|
|
1179
|
+
"skills",
|
|
1180
|
+
"sessions",
|
|
1181
|
+
"agents"
|
|
1182
|
+
];
|
|
1183
|
+
const NS = settingsNamespace("skill-manage");
|
|
1184
|
+
const Config = Schema.object({});
|
|
1185
|
+
function apply(ctx) {
|
|
1186
|
+
installConsoleLoggerExporter(ctx);
|
|
1187
|
+
installSettingsSection(ctx, NS, Config, {}, {
|
|
1188
|
+
setSource: () => {},
|
|
1189
|
+
onChange: () => {}
|
|
1190
|
+
});
|
|
1191
|
+
registerSkillManageRemote(ctx);
|
|
1192
|
+
}
|
|
1193
|
+
function installConsoleLoggerExporter(ctx) {
|
|
1194
|
+
try {
|
|
1195
|
+
ctx.root.logger?.exporter?.({
|
|
1196
|
+
colors: 0,
|
|
1197
|
+
export: (message) => {
|
|
1198
|
+
const { name = "skill-manage", type = "log", args = [] } = message;
|
|
1199
|
+
const line = `[${name}] ${args.map((arg) => arg instanceof Error ? arg.stack ?? arg.message : typeof arg === "string" ? arg : (() => {
|
|
1200
|
+
try {
|
|
1201
|
+
return JSON.stringify(arg);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return String(arg);
|
|
1204
|
+
}
|
|
1205
|
+
})()).join(" ")}`;
|
|
1206
|
+
if (type === "error") console.error(line);
|
|
1207
|
+
else if (type === "warn") console.warn(line);
|
|
1208
|
+
else if (type === "debug") console.debug(line);
|
|
1209
|
+
else console.info(line);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
} catch {}
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
export { Config, apply, inject };
|