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,374 @@
|
|
|
1
|
+
// 上传落盘链路:把界面送来的 Base64 目录 / ZIP 校成一份合法来源,写进技能根目录。
|
|
2
|
+
//
|
|
3
|
+
// 声明从 skills/core.ts 按行号区间搬来,正文一字未改(唯一的容差是 `--export-top-level`
|
|
4
|
+
// 加的 `export ` 前缀,moved-verify 逐字对拍)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么这一段终于能搬:它向下要的东西分三层齐了 —— 路径与来源在 ./skill-roots.js、
|
|
7
|
+
// 错误码与原子改名在 ./core-util.js、预检(读成候选 / 拒符号链 / 临时件 / 替换)在
|
|
8
|
+
// ./import-pipeline.js。0.18.0 那次试切判给不做,缺的就是这三层:当时它们全在 core.ts 里,
|
|
9
|
+
// 搬上传段必然反过来引 core,而 core 的 handlers 又引上传 ⇒ 运行时环。
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { unzipSync } from "fflate";
|
|
14
|
+
import { isSameOrDescendant } from "../paths.js";
|
|
15
|
+
import { parseSkillDoc, toKebab } from "./frontmatter.js";
|
|
16
|
+
import { attachCode, codedError, entryPath, lstatOrNull, MAX_ENTRY_NAME_LENGTH, renameWithRetry, } from "./core-util.js";
|
|
17
|
+
import { analyzeSource, collectCandidates, MAX_SOURCE_DEPTH, preflightCandidates, replaceWithCopy, } from "./import-pipeline.js";
|
|
18
|
+
import { managerHomePath, resolvedPath, skillCreateRootPath } from "./skill-roots.js";
|
|
19
|
+
export const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
20
|
+
export const WINDOWS_DEVICE_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
21
|
+
export const MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20;
|
|
22
|
+
export const MAX_UPLOAD_ENTRY_BYTES = 32 << 20;
|
|
23
|
+
export const MAX_UPLOAD_TOTAL_BYTES = 64 << 20;
|
|
24
|
+
export const MAX_UPLOAD_ENTRIES = 1000;
|
|
25
|
+
export const MAX_UPLOAD_PATH_LENGTH = 512;
|
|
26
|
+
export async function pathsOverlap(a, b) {
|
|
27
|
+
const left = await resolvedPath(a);
|
|
28
|
+
const right = await resolvedPath(b);
|
|
29
|
+
return isSameOrDescendant(left, right) || isSameOrDescendant(right, left);
|
|
30
|
+
}
|
|
31
|
+
export async function importSkill(source, log, options = {}) {
|
|
32
|
+
const targetRoot = skillCreateRootPath();
|
|
33
|
+
const conflict = options.conflict === "overwrite" ? "overwrite" : "skip";
|
|
34
|
+
const dryRun = options.dryRun === true;
|
|
35
|
+
const analysis = await analyzeSource(source);
|
|
36
|
+
if (analysis.kind === "none")
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: analysis.error || "无法识别的 skill 来源",
|
|
40
|
+
code: analysis.code || "error.source.unrecognized",
|
|
41
|
+
params: analysis.params,
|
|
42
|
+
};
|
|
43
|
+
if (await pathsOverlap(analysis.source, targetRoot))
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: "导入来源不能与 DSH 技能目录相同、包含或位于其中",
|
|
47
|
+
code: "error.import.overlap",
|
|
48
|
+
};
|
|
49
|
+
let candidates = [];
|
|
50
|
+
if (analysis.kind === "single") {
|
|
51
|
+
candidates = [
|
|
52
|
+
{
|
|
53
|
+
source: analysis.source,
|
|
54
|
+
kebab: analysis.kebab,
|
|
55
|
+
rawName: analysis.rawName,
|
|
56
|
+
isDir: analysis.isDir,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
try {
|
|
62
|
+
candidates = await collectCandidates(source);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
66
|
+
return attachCode({
|
|
67
|
+
ok: false,
|
|
68
|
+
error: String(error && error.message ? error.message : error),
|
|
69
|
+
}, error);
|
|
70
|
+
}
|
|
71
|
+
if (candidates.length === 0)
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `目录下未找到任何 skill 条目(需含 SKILL.md 的子目录或 .md 文件): ${source}`,
|
|
75
|
+
code: "error.import.emptySource",
|
|
76
|
+
params: { path: source },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const pending = [];
|
|
80
|
+
const conflicts = [];
|
|
81
|
+
const failed = [];
|
|
82
|
+
const imported = [];
|
|
83
|
+
const skipped = [];
|
|
84
|
+
const nameCount = new Map();
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
if (candidate.kebab &&
|
|
87
|
+
KEBAB_RE.test(candidate.kebab) &&
|
|
88
|
+
entryPath(targetRoot, candidate.kebab) !== null)
|
|
89
|
+
nameCount.set(candidate.kebab, (nameCount.get(candidate.kebab) || 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
function failureResult() {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
// 聚合失败明细的原文;前端优先展示已翻译的 failed 明细,此处仅作兜底。
|
|
95
|
+
error: failed.map((item) => item.error).join(";"),
|
|
96
|
+
code: "error.import.failed",
|
|
97
|
+
kind: analysis.kind,
|
|
98
|
+
imported,
|
|
99
|
+
skipped,
|
|
100
|
+
failed,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
for (const c of candidates) {
|
|
104
|
+
if (!c.kebab ||
|
|
105
|
+
!KEBAB_RE.test(c.kebab) ||
|
|
106
|
+
entryPath(targetRoot, c.kebab) === null) {
|
|
107
|
+
failed.push({
|
|
108
|
+
source: c.source,
|
|
109
|
+
error: `无法生成合法 kebab-case 名称(原始名: ${c.rawName || basename(c.source)})`,
|
|
110
|
+
code: "error.import.invalidName",
|
|
111
|
+
params: { name: c.rawName || basename(c.source) },
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// 上方计数循环已为该 kebab 记数(合法名称且目标可写),断言只为让类型收敛。
|
|
116
|
+
if (nameCount.get(c.kebab) > 1) {
|
|
117
|
+
failed.push({
|
|
118
|
+
source: c.source,
|
|
119
|
+
error: `批量来源中存在多个同名插件: ${c.kebab}`,
|
|
120
|
+
code: "error.import.duplicateName",
|
|
121
|
+
params: { name: c.kebab },
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const dest = c.isDir
|
|
126
|
+
? join(targetRoot, c.kebab)
|
|
127
|
+
: join(targetRoot, `${c.kebab}.md`);
|
|
128
|
+
const paths = [
|
|
129
|
+
join(targetRoot, c.kebab),
|
|
130
|
+
join(targetRoot, `${c.kebab}.md`),
|
|
131
|
+
];
|
|
132
|
+
const existing = [];
|
|
133
|
+
for (const path of paths) {
|
|
134
|
+
try {
|
|
135
|
+
await fs.stat(path);
|
|
136
|
+
existing.push(path);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
if (existing.length) {
|
|
141
|
+
conflicts.push({
|
|
142
|
+
name: c.kebab,
|
|
143
|
+
source: c.source,
|
|
144
|
+
isDir: c.isDir,
|
|
145
|
+
paths: existing,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
pending.push({ name: c.kebab, source: c.source, isDir: c.isDir, dest });
|
|
150
|
+
}
|
|
151
|
+
if (pending.length === 0 && conflicts.length === 0)
|
|
152
|
+
return failureResult();
|
|
153
|
+
if (dryRun) {
|
|
154
|
+
// 预检即结论:与正式导入同口径执行符号链接/深度检查,避免预检通过、确认覆盖后实导才失败。
|
|
155
|
+
await preflightCandidates(pending, conflicts, failed);
|
|
156
|
+
if (pending.length === 0 && conflicts.length === 0)
|
|
157
|
+
return failureResult();
|
|
158
|
+
return { kind: analysis.kind, pending, conflicts, failed };
|
|
159
|
+
}
|
|
160
|
+
if (pending.length > 0 ||
|
|
161
|
+
(conflict === "overwrite" && conflicts.length > 0)) {
|
|
162
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
for (const p of pending) {
|
|
165
|
+
try {
|
|
166
|
+
const warnings = await replaceWithCopy(p.source, p.dest, p.isDir);
|
|
167
|
+
imported.push({ name: p.name, overwritten: false, warnings });
|
|
168
|
+
if (log)
|
|
169
|
+
log("import", `导入 ${p.source} -> ${p.dest}`);
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
173
|
+
failed.push(attachCode({ source: p.source, error: String(e && e.message ? e.message : e) }, e));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (conflict === "overwrite") {
|
|
177
|
+
for (const c of conflicts) {
|
|
178
|
+
try {
|
|
179
|
+
const dest = c.isDir
|
|
180
|
+
? join(targetRoot, c.name)
|
|
181
|
+
: join(targetRoot, `${c.name}.md`);
|
|
182
|
+
const warnings = await replaceWithCopy(c.source, dest, c.isDir, c.paths);
|
|
183
|
+
imported.push({ name: c.name, overwritten: true, warnings });
|
|
184
|
+
if (log)
|
|
185
|
+
log("import-overwrite", `覆盖导入 ${c.source} -> ${dest}`);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
189
|
+
failed.push(attachCode({ source: c.source, error: String(e && e.message ? e.message : e) }, e));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
for (const c of conflicts)
|
|
195
|
+
skipped.push({ name: c.name, source: c.source });
|
|
196
|
+
}
|
|
197
|
+
if (failed.length && imported.length === 0)
|
|
198
|
+
return failureResult();
|
|
199
|
+
return { kind: analysis.kind, imported, skipped, failed };
|
|
200
|
+
}
|
|
201
|
+
export function pathSegmentRewritten(part) {
|
|
202
|
+
if (/[\u0000-\u001f\u007f]/.test(part))
|
|
203
|
+
return true;
|
|
204
|
+
return part !== part.replace(/[.\s]+$/, "");
|
|
205
|
+
}
|
|
206
|
+
export function normalizeUploadPath(input) {
|
|
207
|
+
const raw = String(input == null ? "" : input).replace(/\\/g, "/");
|
|
208
|
+
if (!raw ||
|
|
209
|
+
raw.length > MAX_UPLOAD_PATH_LENGTH ||
|
|
210
|
+
raw.includes("\0") ||
|
|
211
|
+
raw.startsWith("/") ||
|
|
212
|
+
/^[A-Za-z]:/.test(raw) ||
|
|
213
|
+
raw.startsWith("//")) {
|
|
214
|
+
throw codedError(`上传条目路径非法: ${raw}`, "error.upload.path", {
|
|
215
|
+
path: raw,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const directory = raw.endsWith("/");
|
|
219
|
+
const parts = raw
|
|
220
|
+
.split("/")
|
|
221
|
+
.filter((part, index, all) => directory && index === all.length - 1 ? false : true);
|
|
222
|
+
if (!parts.length ||
|
|
223
|
+
parts.length > MAX_SOURCE_DEPTH ||
|
|
224
|
+
parts.some((part) => !part ||
|
|
225
|
+
part === "." ||
|
|
226
|
+
part === ".." ||
|
|
227
|
+
pathSegmentRewritten(part) ||
|
|
228
|
+
part.length > MAX_ENTRY_NAME_LENGTH ||
|
|
229
|
+
WINDOWS_DEVICE_NAME_RE.test(part))) {
|
|
230
|
+
throw codedError(`上传条目路径非法: ${raw}`, "error.upload.path", {
|
|
231
|
+
path: raw,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return { path: parts.join("/"), directory };
|
|
235
|
+
}
|
|
236
|
+
export function decodeUploadBase64(value, maxBytes, code = "error.upload.tooLarge") {
|
|
237
|
+
const raw = String(value == null ? "" : value);
|
|
238
|
+
const padding = raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0;
|
|
239
|
+
const dataLength = raw.length - padding;
|
|
240
|
+
const firstPadding = raw.indexOf("=");
|
|
241
|
+
if (raw.length % 4 !== 0 ||
|
|
242
|
+
(firstPadding !== -1 && firstPadding !== dataLength)) {
|
|
243
|
+
throw codedError("上传内容不是合法 Base64", "error.upload.encoding");
|
|
244
|
+
}
|
|
245
|
+
const decodedLength = (raw.length / 4) * 3 - padding;
|
|
246
|
+
if (decodedLength > maxBytes)
|
|
247
|
+
throw codedError(`上传内容超过 ${maxBytes} 字节限制`, code, {
|
|
248
|
+
limit: maxBytes,
|
|
249
|
+
});
|
|
250
|
+
// 避免对数 MiB 字符串使用带重复分组的正则;V8 可能在合法大文件上耗尽调用栈。
|
|
251
|
+
for (let index = 0; index < dataLength; index += 1) {
|
|
252
|
+
const char = raw.charCodeAt(index);
|
|
253
|
+
if (!((char >= 65 && char <= 90) ||
|
|
254
|
+
(char >= 97 && char <= 122) ||
|
|
255
|
+
(char >= 48 && char <= 57) ||
|
|
256
|
+
char === 43 ||
|
|
257
|
+
char === 47)) {
|
|
258
|
+
throw codedError("上传内容不是合法 Base64", "error.upload.encoding");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const bytes = Buffer.from(raw, "base64");
|
|
262
|
+
return bytes;
|
|
263
|
+
}
|
|
264
|
+
export function uploadError(error) {
|
|
265
|
+
// 按可选 message 读取抛出值,保持既有兜底文案。
|
|
266
|
+
const cause = error;
|
|
267
|
+
return attachCode({
|
|
268
|
+
ok: false,
|
|
269
|
+
error: String(cause && cause.message ? cause.message : error),
|
|
270
|
+
}, error);
|
|
271
|
+
}
|
|
272
|
+
export async function writeUploadedEntries(contentRoot, entries) {
|
|
273
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
274
|
+
throw codedError("上传内容为空", "error.upload.empty");
|
|
275
|
+
if (entries.length > MAX_UPLOAD_ENTRIES)
|
|
276
|
+
throw codedError(`上传条目超过 ${MAX_UPLOAD_ENTRIES} 个`, "error.upload.tooMany", { limit: MAX_UPLOAD_ENTRIES });
|
|
277
|
+
let total = 0;
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
const normalized = normalizeUploadPath(entry && entry.path);
|
|
281
|
+
const key = normalized.path.toLowerCase();
|
|
282
|
+
if (seen.has(key))
|
|
283
|
+
throw codedError(`上传内容包含重复路径: ${normalized.path}`, "error.upload.duplicate", { path: normalized.path });
|
|
284
|
+
seen.add(key);
|
|
285
|
+
if (normalized.directory)
|
|
286
|
+
continue;
|
|
287
|
+
const bytes = decodeUploadBase64(entry && entry.data, MAX_UPLOAD_ENTRY_BYTES);
|
|
288
|
+
total += bytes.length;
|
|
289
|
+
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
290
|
+
throw codedError(`上传内容总大小超过 ${MAX_UPLOAD_TOTAL_BYTES} 字节`, "error.upload.tooLarge", { limit: MAX_UPLOAD_TOTAL_BYTES });
|
|
291
|
+
const target = join(contentRoot, ...normalized.path.split("/"));
|
|
292
|
+
if (!isSameOrDescendant(contentRoot, target))
|
|
293
|
+
throw codedError(`上传条目路径非法: ${normalized.path}`, "error.upload.path", { path: normalized.path });
|
|
294
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
295
|
+
await fs.writeFile(target, bytes);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export async function writeUploadedZip(contentRoot, encoded) {
|
|
299
|
+
const archive = decodeUploadBase64(encoded, MAX_UPLOAD_ARCHIVE_BYTES, "error.upload.archiveTooLarge");
|
|
300
|
+
let count = 0;
|
|
301
|
+
let total = 0;
|
|
302
|
+
let files;
|
|
303
|
+
try {
|
|
304
|
+
files = unzipSync(archive, {
|
|
305
|
+
filter(info) {
|
|
306
|
+
const normalized = normalizeUploadPath(info.name);
|
|
307
|
+
count += 1;
|
|
308
|
+
if (count > MAX_UPLOAD_ENTRIES)
|
|
309
|
+
throw codedError(`ZIP 条目超过 ${MAX_UPLOAD_ENTRIES} 个`, "error.upload.tooMany", { limit: MAX_UPLOAD_ENTRIES });
|
|
310
|
+
if (!normalized.directory && info.originalSize > MAX_UPLOAD_ENTRY_BYTES)
|
|
311
|
+
throw codedError(`ZIP 条目过大: ${normalized.path}`, "error.upload.tooLarge", { limit: MAX_UPLOAD_ENTRY_BYTES });
|
|
312
|
+
total += normalized.directory ? 0 : info.originalSize;
|
|
313
|
+
if (total > MAX_UPLOAD_TOTAL_BYTES)
|
|
314
|
+
throw codedError(`ZIP 解压后总大小超过 ${MAX_UPLOAD_TOTAL_BYTES} 字节`, "error.upload.tooLarge", { limit: MAX_UPLOAD_TOTAL_BYTES });
|
|
315
|
+
return !normalized.directory;
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// 业务错误(我们抛的 error.* 前缀)透传;其余按 ZIP 解压失败处理。
|
|
321
|
+
if (error && /^error\./.test(String(error.code || "")))
|
|
322
|
+
throw error;
|
|
323
|
+
throw codedError(`ZIP 无法解压: ${String(error && error.message ? error.message : error)}`, "error.upload.zipInvalid");
|
|
324
|
+
}
|
|
325
|
+
const entries = Object.entries(files).map(([path, bytes]) => ({
|
|
326
|
+
path,
|
|
327
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
328
|
+
}));
|
|
329
|
+
await writeUploadedEntries(contentRoot, entries);
|
|
330
|
+
}
|
|
331
|
+
export async function prepareUploadedSource(sessionRoot, input) {
|
|
332
|
+
const contentRoot = join(sessionRoot, "content");
|
|
333
|
+
await fs.mkdir(contentRoot, { recursive: true });
|
|
334
|
+
if (input && input.zip !== undefined)
|
|
335
|
+
await writeUploadedZip(contentRoot, input.zip);
|
|
336
|
+
else
|
|
337
|
+
await writeUploadedEntries(contentRoot, input && input.entries);
|
|
338
|
+
const rootSkill = join(contentRoot, "SKILL.md");
|
|
339
|
+
const rootSkillStat = await lstatOrNull(rootSkill);
|
|
340
|
+
if (!rootSkillStat || !rootSkillStat.isFile())
|
|
341
|
+
return contentRoot;
|
|
342
|
+
const doc = parseSkillDoc(await fs.readFile(rootSkill, "utf8"));
|
|
343
|
+
const fallback = String((input && input.name) || "uploaded-skill")
|
|
344
|
+
.replace(/\.zip$/i, "")
|
|
345
|
+
.replace(/^skill\.md$/i, "uploaded-skill");
|
|
346
|
+
const skillName = toKebab(doc.map.name || fallback);
|
|
347
|
+
if (!skillName ||
|
|
348
|
+
!KEBAB_RE.test(skillName) ||
|
|
349
|
+
entryPath(contentRoot, skillName) === null) {
|
|
350
|
+
throw codedError(`无法生成合法 kebab-case 名称(原始名: ${doc.map.name || fallback})`, "error.import.invalidName", { name: doc.map.name || fallback });
|
|
351
|
+
}
|
|
352
|
+
const batchRoot = join(sessionRoot, "batch");
|
|
353
|
+
const wrappedRoot = join(batchRoot, skillName);
|
|
354
|
+
await fs.mkdir(batchRoot, { recursive: true });
|
|
355
|
+
await renameWithRetry(contentRoot, wrappedRoot);
|
|
356
|
+
return batchRoot;
|
|
357
|
+
}
|
|
358
|
+
export async function importUploadedSkill(input, log, options = {}) {
|
|
359
|
+
const uploadHome = join(managerHomePath(), "uploads");
|
|
360
|
+
const sessionRoot = join(uploadHome, `.upload-${randomUUID()}`);
|
|
361
|
+
try {
|
|
362
|
+
await fs.mkdir(sessionRoot, { recursive: true });
|
|
363
|
+
const source = await prepareUploadedSource(sessionRoot, input || {});
|
|
364
|
+
return await importSkill(source, log, options);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return uploadError(error);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
await fs
|
|
371
|
+
.rm(sessionRoot, { recursive: true, force: true })
|
|
372
|
+
.catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 斜杠命令入口的界面设置(侧车 `slash-settings.json`,开关在「兼容」页)。
|
|
2
|
+
//
|
|
3
|
+
// 两项:要不要在对话输入框的 `/` 菜单里挂出「工具」段与「快捷提示词」段。它们不是管理动作,
|
|
4
|
+
// 而是**入口开关** —— 所以与 `scene-settings.ts` 同规格:不受场景冻结影响(锁着场景的人照样能
|
|
5
|
+
// 关掉这个入口),也不参与任何门禁判定。
|
|
6
|
+
//
|
|
7
|
+
// 为什么两段各一颗开关:两段的用处不同(一段是"改状态"、一段是"取文字"),只想留一段的人是
|
|
8
|
+
// 真实需求 —— 而它们本来就各自是一个源(同一个源里的行只能跟自己排顺序),分开关不需要任何
|
|
9
|
+
// 额外机制。
|
|
10
|
+
//
|
|
11
|
+
// 为什么放在服务端而不是 localStorage:与其余设置同一个家(侧车 + op + 登记表),换浏览器也在;
|
|
12
|
+
// 存本地就只有那个浏览器认,而这条开关的用途正是"这台机器上我不想看到它"。
|
|
13
|
+
export const DEFAULT_SLASH_SETTINGS = { tools: true, quickPrompts: true };
|
|
14
|
+
/**
|
|
15
|
+
* 把任意输入夹成合法设置:读不到 / 形状不对一律按默认开启。
|
|
16
|
+
*
|
|
17
|
+
* 旧版只有一颗 `enabled`(管整个入口)。那份文件还在的人升级后不该突然多出两段,也不该丢掉
|
|
18
|
+
* "我把它关过"这个事实 —— 所以 `enabled` 仍是两段的默认值,各自显式给出时以各自为准。
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeSlashSettings(raw) {
|
|
21
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
22
|
+
const legacy = typeof obj.enabled === 'boolean' ? obj.enabled : undefined;
|
|
23
|
+
const one = (key) => typeof obj[key] === 'boolean' ? obj[key] : legacy === undefined ? DEFAULT_SLASH_SETTINGS[key] : legacy;
|
|
24
|
+
return { tools: one('tools'), quickPrompts: one('quickPrompts') };
|
|
25
|
+
}
|