dsh-plugin-tool-management 0.10.0 → 0.11.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 +71 -1
- package/README.md +64 -49
- package/README_EN.md +58 -37
- 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 +105 -12
- package/lib/client.js +2763 -546
- package/lib/compat/preset-reach.js +1 -10
- package/lib/compat/probe.js +158 -20
- package/lib/context-inject.js +11 -0
- package/lib/host-names.js +12 -0
- package/lib/http-fence.js +35 -15
- package/lib/hub.js +28 -2
- package/lib/imports/parsers.js +15 -9
- package/lib/imports/upload.js +43 -4
- package/lib/index.js +804 -3887
- package/lib/mcp/loader-token.js +238 -0
- package/lib/mcp/manager.js +1681 -0
- package/lib/mcp/override-blocks.js +10 -3
- package/lib/mcp/patch-yaml.js +351 -0
- package/lib/mcp/secret-guard.js +145 -0
- package/lib/{rules → memories}/archive-engine.js +1 -1
- package/lib/{rules → memories}/archive.js +1 -1
- package/lib/memories/constants.js +128 -0
- package/lib/memories/index-io.js +330 -0
- package/lib/memories/projection.js +280 -0
- package/lib/memories/service.js +686 -0
- package/lib/memories/snapshot.js +672 -0
- package/lib/ops/candidates.js +64 -0
- package/lib/ops/compat.js +136 -0
- package/lib/ops/ctx.js +9 -0
- package/lib/ops/memory.js +678 -0
- package/lib/ops/prompts.js +107 -0
- package/lib/ops/scene-records.js +460 -0
- package/lib/ops/scene-sync.js +17 -0
- package/lib/ops/sessions.js +603 -0
- package/lib/ops/trash.js +140 -0
- package/lib/paths.js +103 -0
- package/lib/prompts/preset-id.js +49 -0
- package/lib/{agents-md → prompts}/service.js +1 -1
- package/lib/request-gate.js +320 -0
- package/lib/scene-prompt-sync.js +4 -4
- package/lib/scenes/candidates.js +344 -0
- package/lib/{history → sessions}/bridge.js +15 -5
- package/lib/sessions/history.js +323 -0
- package/lib/{history → sessions}/tombstone.js +1 -1
- package/lib/{history → sessions}/workspace.js +92 -24
- package/lib/skills/core.js +74 -39
- package/lib/skills/readonly-discovery.js +4 -1
- package/lib/skills/service.js +98 -13
- package/lib/subagents/service.js +199 -55
- package/lib/tools/deps.js +8 -0
- package/lib/tools/mcp.js +110 -0
- package/lib/tools/memory.js +87 -0
- package/lib/tools/prompt.js +70 -0
- package/lib/tools/skills.js +139 -0
- package/lib/tools/subagent.js +40 -0
- package/package.json +7 -7
- package/lib/agents-md/preset-id.js +0 -49
- package/lib/rules/service.js +0 -3078
package/lib/skills/core.js
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// 系统异常(fs ENOENT 等)透传 String(e.message),不加 code。
|
|
13
13
|
// 文件写入错误由路由返回给调用方。
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
-
import { join, basename, dirname, resolve,
|
|
15
|
+
import { join, basename, dirname, resolve, isAbsolute, } from "node:path";
|
|
16
16
|
import { promises as fs } from "node:fs";
|
|
17
17
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
import { unzipSync } from "fflate";
|
|
19
19
|
import { discoverReadonlyEntries, validDiscoveryName, } from "./readonly-discovery.js";
|
|
20
|
+
import { isSameOrDescendant, isValidSegment } from "../paths.js";
|
|
20
21
|
const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
21
22
|
const PROJECT_ROOT_KEY_RE = /^project-(?:dsh|agents):[a-f0-9]{16}$/;
|
|
22
23
|
const CUSTOM_ROOT_KEY_RE = /^custom-[a-f0-9]{16}$/;
|
|
@@ -41,10 +42,12 @@ function codedError(message, code, params) {
|
|
|
41
42
|
}
|
|
42
43
|
/** 把业务 Error 的 code/params 附加到失败明细;系统异常(ENOENT 等,非 error.* 前缀)保持原文。 */
|
|
43
44
|
function attachCode(item, error) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
item.
|
|
45
|
+
// 透传并回写任意失败明细对象(形状由各调用点决定),故按 any 处理。
|
|
46
|
+
const coded = error;
|
|
47
|
+
if (coded && typeof coded.code === "string" && /^error\./.test(coded.code))
|
|
48
|
+
item.code = coded.code;
|
|
49
|
+
if (coded && coded.params)
|
|
50
|
+
item.params = coded.params;
|
|
48
51
|
return item;
|
|
49
52
|
}
|
|
50
53
|
// ── 路径解析 ────────────────────────────────────────────────────────────────
|
|
@@ -139,7 +142,9 @@ export function userRoots() {
|
|
|
139
142
|
const DEFAULT_SOURCE_KEYS = Object.freeze(["dsh", "hub"]);
|
|
140
143
|
/** 是否为默认来源。接受 root 对象或 key 字符串;`dsh` / `hub` 的所有特判都走这里。 */
|
|
141
144
|
export function isDefaultSkillSource(rootOrKey) {
|
|
142
|
-
const key = rootOrKey && typeof rootOrKey === "object"
|
|
145
|
+
const key = rootOrKey && typeof rootOrKey === "object"
|
|
146
|
+
? rootOrKey.key
|
|
147
|
+
: rootOrKey;
|
|
143
148
|
return DEFAULT_SOURCE_KEYS.includes(key);
|
|
144
149
|
}
|
|
145
150
|
function projectIdentity(path) {
|
|
@@ -159,9 +164,8 @@ export function customRootKey(path) {
|
|
|
159
164
|
}
|
|
160
165
|
/** 从状态值解析自定义来源定义;非法条目静默丢弃,key 与 path 不匹配的不信任。 */
|
|
161
166
|
export function customRootsFromState(stateValue) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
: [];
|
|
167
|
+
const state = stateValue;
|
|
168
|
+
const list = state && Array.isArray(state.customRoots) ? state.customRoots : [];
|
|
165
169
|
const out = [];
|
|
166
170
|
for (const item of list) {
|
|
167
171
|
if (!item || typeof item !== "object")
|
|
@@ -443,6 +447,7 @@ export async function browseDirectories(inputPath) {
|
|
|
443
447
|
directory = await fs.stat(canonical);
|
|
444
448
|
}
|
|
445
449
|
catch (error) {
|
|
450
|
+
// 系统异常按可选 message 读取,保持既有判定分支不变。
|
|
446
451
|
return {
|
|
447
452
|
ok: false,
|
|
448
453
|
error: `无法读取目录: ${target}`,
|
|
@@ -487,6 +492,7 @@ export async function browseDirectories(inputPath) {
|
|
|
487
492
|
}
|
|
488
493
|
}
|
|
489
494
|
catch (error) {
|
|
495
|
+
// 系统异常按可选 message 读取,保持既有判定分支不变。
|
|
490
496
|
return {
|
|
491
497
|
ok: false,
|
|
492
498
|
error: `无法读取目录: ${canonical}`,
|
|
@@ -517,6 +523,7 @@ export async function browseDirectories(inputPath) {
|
|
|
517
523
|
return { path: canonical, home: homedir(), crumbs, entries, truncated };
|
|
518
524
|
}
|
|
519
525
|
function dshRootPath() {
|
|
526
|
+
// userRoots() 恒含 dsh 来源(见上方列表),断言只为让类型收敛。
|
|
520
527
|
return userRoots().find((root) => root.key === "dsh").path;
|
|
521
528
|
}
|
|
522
529
|
/**
|
|
@@ -623,7 +630,11 @@ async function checkedWritableRootDefinition(root) {
|
|
|
623
630
|
};
|
|
624
631
|
}
|
|
625
632
|
// 项目仓库内容不可信;拒绝通过 .dsh 或 skills 链接把写入重定向到项目之外。
|
|
626
|
-
|
|
633
|
+
// definition.scope === "project" 时 projectRoot 必有值(见 ProjectSource),断言只为让类型收敛。
|
|
634
|
+
for (const path of [
|
|
635
|
+
join(definition.projectRoot, ".dsh"),
|
|
636
|
+
definition.path,
|
|
637
|
+
]) {
|
|
627
638
|
const st = await lstatOrNull(path);
|
|
628
639
|
if (st && (!st.isDirectory() || st.isSymbolicLink())) {
|
|
629
640
|
return {
|
|
@@ -636,12 +647,6 @@ async function checkedWritableRootDefinition(root) {
|
|
|
636
647
|
}
|
|
637
648
|
return definition;
|
|
638
649
|
}
|
|
639
|
-
/** 判断 child 是否与 parent 相同或位于其内部。跨盘符时 relative 会返回绝对路径。 */
|
|
640
|
-
function isSameOrDescendant(parent, child) {
|
|
641
|
-
const rel = relative(resolve(parent), resolve(child));
|
|
642
|
-
return (rel === "" ||
|
|
643
|
-
(!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`)));
|
|
644
|
-
}
|
|
645
650
|
/** 解析真实路径;中间若有目录链接,按落地目录比较重叠。 */
|
|
646
651
|
async function resolvedPath(path) {
|
|
647
652
|
try {
|
|
@@ -702,16 +707,9 @@ async function lstatOrNull(path) {
|
|
|
702
707
|
}
|
|
703
708
|
/** 名称只允许一个普通路径段;不把既有技能名称限制为 kebab-case。 */
|
|
704
709
|
export function entryPath(root, name) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
name === ".." ||
|
|
709
|
-
name.startsWith(".") ||
|
|
710
|
-
name.length > MAX_ENTRY_NAME_LENGTH ||
|
|
711
|
-
/[\\/:*?"<>|\0]/.test(name) ||
|
|
712
|
-
/[. ]$/.test(name) ||
|
|
713
|
-
WINDOWS_DEVICE_NAME_RE.test(name) ||
|
|
714
|
-
basename(name) !== name)
|
|
710
|
+
// 段名谓词收敛到 paths.js(此前与 rules / subagents / imports 各写一套,口径不一)。
|
|
711
|
+
// 原实现里 `basename(name) !== name` 那条已被谓词的「不含路径分隔符」覆盖。
|
|
712
|
+
if (!isValidSegment(name, MAX_ENTRY_NAME_LENGTH))
|
|
715
713
|
return null;
|
|
716
714
|
const rootPath = resolve(root);
|
|
717
715
|
const path = resolve(rootPath, name);
|
|
@@ -820,6 +818,7 @@ export function unquote(v) {
|
|
|
820
818
|
/** Windows 上杀毒软件或索引器可能短暂占用目录;只重试明确可恢复的 rename 错误。 */
|
|
821
819
|
export async function renameWithRetry(source, destination, options = {}) {
|
|
822
820
|
const rename = typeof options.rename === "function" ? options.rename : fs.rename;
|
|
821
|
+
// Number.isInteger/isFinite 不收窄类型,故断言成 number 以保持既有比较表达式。
|
|
823
822
|
const maxAttempts = Number.isInteger(options.maxAttempts) && options.maxAttempts > 0
|
|
824
823
|
? options.maxAttempts
|
|
825
824
|
: 6;
|
|
@@ -831,6 +830,7 @@ export async function renameWithRetry(source, destination, options = {}) {
|
|
|
831
830
|
return await rename(source, destination);
|
|
832
831
|
}
|
|
833
832
|
catch (error) {
|
|
833
|
+
// 系统 rename 异常按可选 code 读取,保持既有重试判定。
|
|
834
834
|
if (!TRANSIENT_RENAME_CODES.has(error && error.code) ||
|
|
835
835
|
attempt >= maxAttempts)
|
|
836
836
|
throw error;
|
|
@@ -854,6 +854,7 @@ async function movePathWithFallback(source, destination, options = {}) {
|
|
|
854
854
|
return { copied: false, cleanupError: null };
|
|
855
855
|
}
|
|
856
856
|
catch (error) {
|
|
857
|
+
// 系统异常按可选 code 读取,保持既有 EXDEV 判定。
|
|
857
858
|
if (!error || error.code !== "EXDEV")
|
|
858
859
|
throw error;
|
|
859
860
|
}
|
|
@@ -878,6 +879,7 @@ async function movePathWithFallback(source, destination, options = {}) {
|
|
|
878
879
|
await removeMovedPath(quarantine);
|
|
879
880
|
}
|
|
880
881
|
catch (error) {
|
|
882
|
+
// 调用方按可选 message 读取该清理异常,保持既有告警文案。
|
|
881
883
|
cleanupError = error;
|
|
882
884
|
}
|
|
883
885
|
return { copied: true, cleanupError, quarantine };
|
|
@@ -914,6 +916,7 @@ export async function resolveEntry(root, name) {
|
|
|
914
916
|
return null;
|
|
915
917
|
return ((await discoverReadonlyEntries(definition.path)).entries.find((entry) => entry.name === name) || null);
|
|
916
918
|
}
|
|
919
|
+
// 传入 definition 对象时取它的 path;其余情况按 key 字符串处理。
|
|
917
920
|
root = definition ? definition.path : root;
|
|
918
921
|
try {
|
|
919
922
|
const bundlePath = entryPath(root, name);
|
|
@@ -1020,6 +1023,7 @@ export async function scanEntries(root, options = {}) {
|
|
|
1020
1023
|
const definition = rootDefinition(root);
|
|
1021
1024
|
if (definition && !definition.mutable) {
|
|
1022
1025
|
const discovered = await discoverReadonlyEntries(definition.path);
|
|
1026
|
+
// metadataOnly 时条目的摘要字段由调用方按需读取(见 visibleEntryForRoot)。
|
|
1023
1027
|
if (options.metadataOnly)
|
|
1024
1028
|
return discovered;
|
|
1025
1029
|
const entries = [];
|
|
@@ -1036,6 +1040,7 @@ export async function scanEntries(root, options = {}) {
|
|
|
1036
1040
|
}
|
|
1037
1041
|
return { ...discovered, entries };
|
|
1038
1042
|
}
|
|
1043
|
+
// 传入 definition 对象时取它的 path;其余情况按 key 字符串处理。
|
|
1039
1044
|
root = definition ? definition.path : root;
|
|
1040
1045
|
const rootStat = await lstatOrNull(resolve(root));
|
|
1041
1046
|
if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
@@ -1104,7 +1109,10 @@ export async function scanEntries(root, options = {}) {
|
|
|
1104
1109
|
}
|
|
1105
1110
|
/** 同一真实技能优先归属直接 SSOT;同类条目再按来源 rank 决胜。 */
|
|
1106
1111
|
async function scanDeduplicatedRoots(roots = userRoots(), options = {}) {
|
|
1107
|
-
const results = await Promise.all(roots.map(async (root) => [
|
|
1112
|
+
const results = await Promise.all(roots.map(async (root) => [
|
|
1113
|
+
root,
|
|
1114
|
+
await scanEntries(root, options),
|
|
1115
|
+
]));
|
|
1108
1116
|
const scans = new Map();
|
|
1109
1117
|
const groups = new Map();
|
|
1110
1118
|
for (const [root, scan] of results) {
|
|
@@ -1343,9 +1351,8 @@ function normalizeManagerState(value) {
|
|
|
1343
1351
|
* 也不参与 provider 候选。源文件不动,清掉这个标记即恢复。
|
|
1344
1352
|
*/
|
|
1345
1353
|
export function activeUserRoots(stateValue) {
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
: []);
|
|
1354
|
+
const state = stateValue;
|
|
1355
|
+
const removed = new Set(state && Array.isArray(state.removedSources) ? state.removedSources : []);
|
|
1349
1356
|
return userRoots().filter((root) => !removed.has(root.key));
|
|
1350
1357
|
}
|
|
1351
1358
|
/** 裁剪自定义来源列表:非法条目丢弃,key/path 绑定校验,label 规整。 */
|
|
@@ -1420,6 +1427,7 @@ export async function readManagerState() {
|
|
|
1420
1427
|
};
|
|
1421
1428
|
}
|
|
1422
1429
|
catch (error) {
|
|
1430
|
+
// 系统异常按可选 code 读取,保持既有 ENOENT 判定。
|
|
1423
1431
|
if (error && error.code === "ENOENT")
|
|
1424
1432
|
return { state: defaultManagerState(), warning: null, writable: true };
|
|
1425
1433
|
return {
|
|
@@ -1579,6 +1587,7 @@ async function checkedPolicyRootDefinition(root) {
|
|
|
1579
1587
|
resolve(join(definition.projectRoot, definition.kind === "project-dsh" ? ".dsh" : ".agents", "skills"));
|
|
1580
1588
|
if (!validProjectRoot)
|
|
1581
1589
|
return null;
|
|
1590
|
+
// validProjectRoot 已校验 projectRoot 为字符串,断言只为让类型收敛。
|
|
1582
1591
|
if (!(await projectSourceSafe(definition))) {
|
|
1583
1592
|
return {
|
|
1584
1593
|
ok: false,
|
|
@@ -1779,12 +1788,13 @@ async function readTrashMetadata(id) {
|
|
|
1779
1788
|
* 在短重试窗口后仍返回 EPERM。此时保留 stage 作为唯一可回滚副本,逐项复制到最终目录,
|
|
1780
1789
|
* 并最后写 metadata:listTrash() 在复制完整前不会暴露半成品。
|
|
1781
1790
|
*/
|
|
1782
|
-
async function publishTrashStage(stage, finalPath, metadata, renameOptions) {
|
|
1791
|
+
async function publishTrashStage(stage, finalPath, metadata, renameOptions = {}) {
|
|
1783
1792
|
try {
|
|
1784
1793
|
await renameWithRetry(stage, finalPath, renameOptions);
|
|
1785
1794
|
return { fallback: false, cleanupError: null };
|
|
1786
1795
|
}
|
|
1787
1796
|
catch (error) {
|
|
1797
|
+
// 系统异常按可选 code 读取,保持既有可恢复码判定。
|
|
1788
1798
|
if (!TRANSIENT_RENAME_CODES.has(error && error.code))
|
|
1789
1799
|
throw error;
|
|
1790
1800
|
}
|
|
@@ -1811,6 +1821,7 @@ async function publishTrashStage(stage, finalPath, metadata, renameOptions) {
|
|
|
1811
1821
|
await fs.rm(stage, { recursive: true, force: true });
|
|
1812
1822
|
}
|
|
1813
1823
|
catch (error) {
|
|
1824
|
+
// 调用方按可选 message 读取该清理异常,保持既有告警文案。
|
|
1814
1825
|
cleanupError = error;
|
|
1815
1826
|
}
|
|
1816
1827
|
return { fallback: true, cleanupError };
|
|
@@ -1919,12 +1930,14 @@ export async function deleteSkill(root, name, log, options = {}) {
|
|
|
1919
1930
|
return { id, name, deletedAt: metadata.deletedAt, root: metadata.root };
|
|
1920
1931
|
}
|
|
1921
1932
|
catch (error) {
|
|
1933
|
+
// 系统异常按可选 message 读取,保持既有回滚失败明细。
|
|
1922
1934
|
const rollbackFailures = [];
|
|
1923
1935
|
for (const item of moved.reverse()) {
|
|
1924
1936
|
try {
|
|
1925
1937
|
await movePathWithFallback(item.destination, item.path, options.renameOptions);
|
|
1926
1938
|
}
|
|
1927
1939
|
catch (rollbackError) {
|
|
1940
|
+
// 系统异常按可选 message 读取,保持既有回滚失败明细。
|
|
1928
1941
|
rollbackFailures.push({
|
|
1929
1942
|
path: item.destination,
|
|
1930
1943
|
error: String(rollbackError && rollbackError.message
|
|
@@ -2128,8 +2141,11 @@ async function collectCandidates(dir) {
|
|
|
2128
2141
|
}
|
|
2129
2142
|
/** 导入内容不接受符号链接,避免把目标目录外的内容带入技能目录。 */
|
|
2130
2143
|
async function assertNoSymbolicLinks(source) {
|
|
2131
|
-
const pending = [
|
|
2144
|
+
const pending = [
|
|
2145
|
+
{ path: source, depth: 0 },
|
|
2146
|
+
];
|
|
2132
2147
|
while (pending.length) {
|
|
2148
|
+
// while 条件保证栈非空,断言只为让类型收敛。
|
|
2133
2149
|
const current = pending.pop();
|
|
2134
2150
|
if (current.depth > MAX_SOURCE_DEPTH)
|
|
2135
2151
|
throw codedError(`skill 来源目录层级超过 ${MAX_SOURCE_DEPTH} 层: ${source}`, "error.source.tooDeep", { depth: MAX_SOURCE_DEPTH, path: source });
|
|
@@ -2161,6 +2177,7 @@ async function preflightCandidates(pending, conflicts, failed) {
|
|
|
2161
2177
|
await assertNoSymbolicLinks(candidate.source);
|
|
2162
2178
|
}
|
|
2163
2179
|
catch (error) {
|
|
2180
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
2164
2181
|
failed.push(attachCode({
|
|
2165
2182
|
source: candidate.source,
|
|
2166
2183
|
error: String(error && error.message ? error.message : error),
|
|
@@ -2191,20 +2208,26 @@ async function copyToTemporary(source, target, isDir) {
|
|
|
2191
2208
|
async function replaceWithCopy(source, dest, isDir, existing = []) {
|
|
2192
2209
|
const stage = await copyToTemporary(source, dest, isDir);
|
|
2193
2210
|
const backups = [];
|
|
2211
|
+
// 三处 rename 一律走 `renameWithRetry`:瞬时占用(Windows 杀软 / 索引器 / 资源管理器)返回的
|
|
2212
|
+
// EACCES / EBUSY / EPERM 正是它重试的那些码。此前只有 `:959` 的回滚用了带重试的版本,
|
|
2213
|
+
// 而**发布与它的补偿回滚用的是裸 rename** —— 发布因瞬时锁抛错时,紧随其后的回滚在同一刻、
|
|
2214
|
+
// 同一目录上大概率撞同一个句柄,于是留下"目标已空、真值还叫 backup"的半态(无自动恢复路径)。
|
|
2215
|
+
// 保护覆盖不均不是取舍,是漏。
|
|
2194
2216
|
try {
|
|
2195
2217
|
for (const path of existing) {
|
|
2196
2218
|
const backup = temporaryPath(path, "backup");
|
|
2197
|
-
await
|
|
2219
|
+
await renameWithRetry(path, backup);
|
|
2198
2220
|
backups.push({ path, backup });
|
|
2199
2221
|
}
|
|
2200
|
-
await
|
|
2222
|
+
await renameWithRetry(stage, dest);
|
|
2201
2223
|
}
|
|
2202
2224
|
catch (error) {
|
|
2225
|
+
// 系统异常按可选 message 读取,保持既有回滚失败明细。
|
|
2203
2226
|
await fs.rm(stage, { recursive: true, force: true }).catch(() => undefined);
|
|
2204
2227
|
const rollbackFailures = [];
|
|
2205
2228
|
for (const item of backups.reverse()) {
|
|
2206
2229
|
try {
|
|
2207
|
-
await
|
|
2230
|
+
await renameWithRetry(item.backup, item.path);
|
|
2208
2231
|
}
|
|
2209
2232
|
catch (rollbackError) {
|
|
2210
2233
|
rollbackFailures.push(item.backup);
|
|
@@ -2222,6 +2245,7 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
|
|
|
2222
2245
|
await fs.rm(item.backup, { recursive: true, force: true });
|
|
2223
2246
|
}
|
|
2224
2247
|
catch (error) {
|
|
2248
|
+
// 系统异常按可选 message 读取,保持既有告警文案。
|
|
2225
2249
|
warnings.push({
|
|
2226
2250
|
code: "warning.backupUncleaned",
|
|
2227
2251
|
params: {
|
|
@@ -2273,6 +2297,7 @@ export async function importSkill(source, log, options = {}) {
|
|
|
2273
2297
|
candidates = await collectCandidates(source);
|
|
2274
2298
|
}
|
|
2275
2299
|
catch (error) {
|
|
2300
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
2276
2301
|
return attachCode({
|
|
2277
2302
|
ok: false,
|
|
2278
2303
|
error: String(error && error.message ? error.message : error),
|
|
@@ -2322,6 +2347,7 @@ export async function importSkill(source, log, options = {}) {
|
|
|
2322
2347
|
});
|
|
2323
2348
|
continue;
|
|
2324
2349
|
}
|
|
2350
|
+
// 上方计数循环已为该 kebab 记数(合法名称且目标可写),断言只为让类型收敛。
|
|
2325
2351
|
if (nameCount.get(c.kebab) > 1) {
|
|
2326
2352
|
failed.push({
|
|
2327
2353
|
source: c.source,
|
|
@@ -2378,6 +2404,7 @@ export async function importSkill(source, log, options = {}) {
|
|
|
2378
2404
|
log("import", `导入 ${p.source} -> ${p.dest}`);
|
|
2379
2405
|
}
|
|
2380
2406
|
catch (e) {
|
|
2407
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
2381
2408
|
failed.push(attachCode({ source: p.source, error: String(e && e.message ? e.message : e) }, e));
|
|
2382
2409
|
}
|
|
2383
2410
|
}
|
|
@@ -2393,6 +2420,7 @@ export async function importSkill(source, log, options = {}) {
|
|
|
2393
2420
|
log("import-overwrite", `覆盖导入 ${c.source} -> ${dest}`);
|
|
2394
2421
|
}
|
|
2395
2422
|
catch (e) {
|
|
2423
|
+
// 系统异常按可选 message 读取,保持既有失败明细文案。
|
|
2396
2424
|
failed.push(attachCode({ source: c.source, error: String(e && e.message ? e.message : e) }, e));
|
|
2397
2425
|
}
|
|
2398
2426
|
}
|
|
@@ -2468,9 +2496,11 @@ function decodeUploadBase64(value, maxBytes, code = "error.upload.tooLarge") {
|
|
|
2468
2496
|
return bytes;
|
|
2469
2497
|
}
|
|
2470
2498
|
function uploadError(error) {
|
|
2499
|
+
// 按可选 message 读取抛出值,保持既有兜底文案。
|
|
2500
|
+
const cause = error;
|
|
2471
2501
|
return attachCode({
|
|
2472
2502
|
ok: false,
|
|
2473
|
-
error: String(
|
|
2503
|
+
error: String(cause && cause.message ? cause.message : error),
|
|
2474
2504
|
}, error);
|
|
2475
2505
|
}
|
|
2476
2506
|
async function writeUploadedEntries(contentRoot, entries) {
|
|
@@ -2521,6 +2551,7 @@ async function writeUploadedZip(contentRoot, encoded) {
|
|
|
2521
2551
|
});
|
|
2522
2552
|
}
|
|
2523
2553
|
catch (error) {
|
|
2554
|
+
// 业务错误(我们抛的 error.* 前缀)透传;其余按 ZIP 解压失败处理。
|
|
2524
2555
|
if (error && /^error\./.test(String(error.code || "")))
|
|
2525
2556
|
throw error;
|
|
2526
2557
|
throw codedError(`ZIP 无法解压: ${String(error && error.message ? error.message : error)}`, "error.upload.zipInvalid");
|
|
@@ -2555,7 +2586,7 @@ async function prepareUploadedSource(sessionRoot, input) {
|
|
|
2555
2586
|
const batchRoot = join(sessionRoot, "batch");
|
|
2556
2587
|
const wrappedRoot = join(batchRoot, skillName);
|
|
2557
2588
|
await fs.mkdir(batchRoot, { recursive: true });
|
|
2558
|
-
await
|
|
2589
|
+
await renameWithRetry(contentRoot, wrappedRoot);
|
|
2559
2590
|
return batchRoot;
|
|
2560
2591
|
}
|
|
2561
2592
|
/**
|
|
@@ -2649,12 +2680,13 @@ export async function createSkill(input, log, options = {}) {
|
|
|
2649
2680
|
log("create", `创建 ${flatPath}`);
|
|
2650
2681
|
return { name, path: flatPath, root: definition.key };
|
|
2651
2682
|
}
|
|
2683
|
+
// 上方 entryPath(root, name) === null 的守卫已保证目标非空,断言只为让类型收敛。
|
|
2652
2684
|
const target = entryPath(root, name);
|
|
2653
2685
|
const stage = temporaryPath(target, "create");
|
|
2654
2686
|
try {
|
|
2655
2687
|
await fs.mkdir(stage);
|
|
2656
2688
|
await fs.writeFile(join(stage, "SKILL.md"), content, "utf8");
|
|
2657
|
-
await
|
|
2689
|
+
await renameWithRetry(stage, target);
|
|
2658
2690
|
}
|
|
2659
2691
|
catch (error) {
|
|
2660
2692
|
await fs.rm(stage, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -2723,7 +2755,10 @@ export async function listProviderCandidates(options = {}) {
|
|
|
2723
2755
|
const candidates = [];
|
|
2724
2756
|
const user = activeUserRoots(policyResult.state).concat(customRootsFromState(policyResult.state).filter((root) => !policyResult.state.removedSources.includes(root.key)));
|
|
2725
2757
|
const userScans = await scanDeduplicatedRoots(user);
|
|
2726
|
-
const items = user.flatMap((root) => userScans.get(root.key).entries.map((entry) => ({
|
|
2758
|
+
const items = user.flatMap((root) => userScans.get(root.key).entries.map((entry) => ({
|
|
2759
|
+
root,
|
|
2760
|
+
entry,
|
|
2761
|
+
})));
|
|
2727
2762
|
const cwd = options && typeof options.cwd === "string" ? options.cwd : undefined;
|
|
2728
2763
|
if (cwd) {
|
|
2729
2764
|
const roots = await projectRoots([cwd]);
|
|
@@ -22,7 +22,9 @@ export function validDiscoveryName(name) {
|
|
|
22
22
|
/** 只返回文件定位信息,不读取正文,供列表、详情及策略复用同一发现与去重规则。 */
|
|
23
23
|
export async function discoverReadonlyEntries(root) {
|
|
24
24
|
const rootPath = resolve(root);
|
|
25
|
-
const queue = [
|
|
25
|
+
const queue = [
|
|
26
|
+
{ path: rootPath, depth: 0, ancestors: new Set() },
|
|
27
|
+
];
|
|
26
28
|
const byName = new Map();
|
|
27
29
|
let directories = 0;
|
|
28
30
|
let entries = 0;
|
|
@@ -50,6 +52,7 @@ export async function discoverReadonlyEntries(root) {
|
|
|
50
52
|
docStat = await fs.lstat(docPath);
|
|
51
53
|
}
|
|
52
54
|
catch (error) {
|
|
55
|
+
// fs 抛出的错误按 Node 错误形状读取 code(此处只关心 ENOENT)
|
|
53
56
|
if (error.code !== "ENOENT")
|
|
54
57
|
throw error;
|
|
55
58
|
}
|
package/lib/skills/service.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { basename, join } from 'node:path';
|
|
12
|
+
import { isInsideRootResolved } from '../paths.js';
|
|
12
13
|
import { state, setSkillEnabled, setPreferredSkill, markSkillsDisabled, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
|
|
13
14
|
const MAX_LOG_BYTES = 1 << 20;
|
|
14
15
|
const PROVIDER_NAME = 'dsh-plugin-tool-management-external';
|
|
@@ -35,18 +36,44 @@ function makeLog() {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
// ── 系统回收站(永久删除的最后一道保险) ──────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* 外部进程(PowerShell / gio)的超时上限。这些调用在技能的**写队列**里被 await
|
|
41
|
+
* (skill-trash-delete),进程一旦挂起,该域全部写操作会永久停摆且无任何提示 ——
|
|
42
|
+
* 回收站 API 在文件被占用、系统弹窗等情况下确实会挂住。超时后杀掉进程并返回 false,
|
|
43
|
+
* 让调用方按既有的「回收站失败 → 硬删除」兜底走下去,队列得以继续。
|
|
44
|
+
*/
|
|
45
|
+
const EXTERNAL_CMD_TIMEOUT_MS = 30_000;
|
|
38
46
|
function runQuietly(command, args) {
|
|
39
47
|
return new Promise((resolve) => {
|
|
48
|
+
let settled = false;
|
|
49
|
+
let timer = null;
|
|
50
|
+
const finish = (ok) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
if (timer) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
timer = null;
|
|
57
|
+
}
|
|
58
|
+
resolve(ok);
|
|
59
|
+
};
|
|
40
60
|
let child;
|
|
41
61
|
try {
|
|
42
62
|
child = spawn(command, args, { stdio: 'ignore', windowsHide: true });
|
|
43
63
|
}
|
|
44
64
|
catch {
|
|
45
|
-
|
|
65
|
+
finish(false);
|
|
46
66
|
return;
|
|
47
67
|
}
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
timer = setTimeout(() => {
|
|
69
|
+
try {
|
|
70
|
+
child.kill('SIGKILL');
|
|
71
|
+
}
|
|
72
|
+
catch { /* 进程可能已自行退出 */ }
|
|
73
|
+
finish(false);
|
|
74
|
+
}, EXTERNAL_CMD_TIMEOUT_MS);
|
|
75
|
+
child.on('error', () => finish(false));
|
|
76
|
+
child.on('close', (code) => finish(code === 0));
|
|
50
77
|
});
|
|
51
78
|
}
|
|
52
79
|
function psQuote(value) {
|
|
@@ -92,7 +119,7 @@ async function permanentlyDeleteTrashSafely(id, log) {
|
|
|
92
119
|
// ── 文件监听:外部改动自动失效 ────────────────────────────────────────────
|
|
93
120
|
/**
|
|
94
121
|
* 监听目录,200ms 防抖后回调:在编辑器或其他工具里新增/修改/删除文件后,无需手动
|
|
95
|
-
* 刷新即可看到变化。技能来源与规则根目录共用(见 src/
|
|
122
|
+
* 刷新即可看到变化。技能来源与规则根目录共用(见 src/memories/service.ts 的场景记忆段缓存)。
|
|
96
123
|
*
|
|
97
124
|
* 为什么用 worker_threads:Windows 上 fs.watch(recursive) 的句柄在「被监听
|
|
98
125
|
* 目录被删除」时会静默卡死事件循环(不触发 error、unref 也无效)——宿主
|
|
@@ -433,6 +460,36 @@ export function createSkillsService(ctx) {
|
|
|
433
460
|
'skill-set-all',
|
|
434
461
|
'skill-trash-restore', 'skill-trash-delete', 'skill-custom-add', 'skill-custom-remove',
|
|
435
462
|
]);
|
|
463
|
+
/**
|
|
464
|
+
* 新建 / 导入 / 恢复技能后统一「默认停用」。**失败不能吞。**
|
|
465
|
+
*
|
|
466
|
+
* 这里原先写作 `.catch(() => undefined)`,两处都漏:一是抛出的写失败被吞,二是
|
|
467
|
+
* `markSkillsDisabled` 在状态文件不可读时**不抛**、而是返回 `{ ok: false }` —— 那个
|
|
468
|
+
* 返回值此前根本没被看过。两种情况下技能都停在启用态、下一轮就进模型上下文,而用户
|
|
469
|
+
* 看到的是「创建成功」。
|
|
470
|
+
*
|
|
471
|
+
* 沿用 `sceneSyncError` 的形状:这里只给**原因原文**,句子由界面按当前语言拼
|
|
472
|
+
* (服务端拼中文会让英文界面露出中文)。
|
|
473
|
+
*/
|
|
474
|
+
async function disableNewSkills(targets, res) {
|
|
475
|
+
if (!targets.length)
|
|
476
|
+
return res;
|
|
477
|
+
let reason = null;
|
|
478
|
+
try {
|
|
479
|
+
const out = await markSkillsDisabled(targets, log);
|
|
480
|
+
if (out && out.ok === false)
|
|
481
|
+
reason = String(out.error || '状态文件不可读');
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
reason = message(e);
|
|
485
|
+
}
|
|
486
|
+
if (reason === null)
|
|
487
|
+
return res;
|
|
488
|
+
// 本来就已经失败的回执:把原因并进 error,不要用 warning 把失败说成「成功但有提示」。
|
|
489
|
+
return res && res.ok === false
|
|
490
|
+
? { ...res, error: `${res.error};另外,「默认停用」也没能写入(${reason})` }
|
|
491
|
+
: { ...res, stateSyncError: reason };
|
|
492
|
+
}
|
|
436
493
|
const ops = {
|
|
437
494
|
// 读操作(state 走短 TTL 缓存,见上方 readState)
|
|
438
495
|
'skill-state': wrap(() => readState()),
|
|
@@ -488,7 +545,7 @@ export function createSkillsService(ctx) {
|
|
|
488
545
|
const res = await createSkill({ name: args.name, description: args.description, body: args.body }, log, { root: (await requestRoot(key)) || key });
|
|
489
546
|
// v0.8.5:新建技能默认不启动(用户裁定)——与子智能体 / MCP 同口径。
|
|
490
547
|
if (res && res.ok !== false && res.name) {
|
|
491
|
-
|
|
548
|
+
return disableNewSkills([{ root: String(res.root || 'hub'), name: String(res.name) }], res);
|
|
492
549
|
}
|
|
493
550
|
return res;
|
|
494
551
|
}), afterWrite),
|
|
@@ -499,9 +556,7 @@ export function createSkillsService(ctx) {
|
|
|
499
556
|
});
|
|
500
557
|
// 导入的技能默认停用;部分成功也对 imported 逐条停用。
|
|
501
558
|
const names = res && Array.isArray(res.imported) ? res.imported : [];
|
|
502
|
-
|
|
503
|
-
await markSkillsDisabled(names.map((n) => ({ root: 'hub', name: String(n) })), log).catch(() => undefined);
|
|
504
|
-
return res;
|
|
559
|
+
return disableNewSkills(names.map((n) => ({ root: 'hub', name: String(n) })), res);
|
|
505
560
|
}), afterWrite),
|
|
506
561
|
'skill-upload': wrap((args) => write(async () => {
|
|
507
562
|
const res = await importUploadedSkill({ name: args.name, entries: args.entries, zip: args.zip }, log, {
|
|
@@ -509,16 +564,14 @@ export function createSkillsService(ctx) {
|
|
|
509
564
|
});
|
|
510
565
|
// 上传/导入同口径:默认停用。
|
|
511
566
|
const names = res && Array.isArray(res.imported) ? res.imported : (res && res.name ? [res.name] : []);
|
|
512
|
-
|
|
513
|
-
await markSkillsDisabled(names.map((n) => ({ root: 'hub', name: String(n) })), log).catch(() => undefined);
|
|
514
|
-
return res;
|
|
567
|
+
return disableNewSkills(names.map((n) => ({ root: 'hub', name: String(n) })), res);
|
|
515
568
|
}), afterWrite),
|
|
516
569
|
'skill-delete': wrap((args) => write(async () => deleteSkill(await requestRoot(String(args.root || 'dsh')), String(args.name || ''), log)), afterWrite),
|
|
517
570
|
'skill-trash-restore': wrap((args) => write(async () => {
|
|
518
571
|
const res = await restoreTrash(String(args.id || ''), log, projectOptions());
|
|
519
572
|
// 恢复的技能默认停用(v0.8.5 用户裁定:新建/导入/恢复一律不启动,手动开启)。
|
|
520
573
|
if (res && res.ok !== false && res.name) {
|
|
521
|
-
|
|
574
|
+
return disableNewSkills([{ root: String(res.root || 'hub'), name: String(res.name) }], res);
|
|
522
575
|
}
|
|
523
576
|
return res;
|
|
524
577
|
}), afterWrite),
|
|
@@ -527,5 +580,37 @@ export function createSkillsService(ctx) {
|
|
|
527
580
|
'skill-custom-add': wrap((args) => write(() => addCustomRoot(args && args.path, args && args.label, log)), afterWrite),
|
|
528
581
|
'skill-custom-remove': wrap((args) => write(() => removeCustomRoot(args && args.key, log)), afterWrite),
|
|
529
582
|
};
|
|
530
|
-
|
|
583
|
+
/**
|
|
584
|
+
* 该绝对路径是否落在「本插件认识的技能来源」内。
|
|
585
|
+
*
|
|
586
|
+
* 为什么需要它:`skill-open` 会把路径交给**系统默认编辑器**,不设边界的话它就是一个
|
|
587
|
+
* 「任意绝对路径 → 启动外部程序」的入口。三类来源都要收进来,否则会把合法技能误挡在外:
|
|
588
|
+
* 静态根(`userRoots`)+ 状态文件里的自定义根 + 当前活动工作区的项目来源。
|
|
589
|
+
*/
|
|
590
|
+
async function isInsideKnownSkillRoot(abs) {
|
|
591
|
+
const dirs = roots.map((r) => String((r && r.path) || '')).filter(Boolean);
|
|
592
|
+
try {
|
|
593
|
+
for (const r of customRootsFromState((await readManagerState()).state)) {
|
|
594
|
+
if (r && r.path)
|
|
595
|
+
dirs.push(String(r.path));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch { /* 状态不可读 → 只用静态根 */ }
|
|
599
|
+
try {
|
|
600
|
+
for (const r of await projectRoots(projectOptions().projectCwds)) {
|
|
601
|
+
if (r && r.path)
|
|
602
|
+
dirs.push(String(r.path));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch { /* 项目来源解析失败 → 跳过 */ }
|
|
606
|
+
for (const dir of dirs) {
|
|
607
|
+
try {
|
|
608
|
+
if (await isInsideRootResolved(dir, abs))
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
catch { /* 单个根失败不影响其他 */ }
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return { ops, writeOps, registerProviders, isInsideKnownSkillRoot };
|
|
531
616
|
}
|