dsh-skill-tag 0.1.17 → 0.1.18
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.
|
@@ -181,6 +181,20 @@ tagging-skills 那 17 个长期漏网的原因(详见 `deny-shadow-覆盖率.m
|
|
|
181
181
|
再发 —— 一个不合法候选会让**整个** `provider.list()` 抛错,宿主随即丢弃本插件的
|
|
182
182
|
全部贡献(日志 `skill provider "skill-tag" skipped`),比漏一个更糟。
|
|
183
183
|
|
|
184
|
+
**两段都要校验(2026-11-12,issue #6)**。上面这条当时只堵了「`denied` 里看不见
|
|
185
|
+
的名字」那一段,**看得见的**那一段(照抄上游 summary)一直没校验,于是扫盘带进来的
|
|
186
|
+
脏名字能原样发出去。触发链:`src/skill-scan.js` 把技能根下裸放的 `README-官方技能说明.md`
|
|
187
|
+
当成技能(名字取文件名去 `.md`)→ 进 universe → `denied = universe − visible` 里也有它 →
|
|
188
|
+
第一段照抄发射 → 宿主 `validateCandidate` 抛错 → **所有对话**每轮都失败,报
|
|
189
|
+
`returned invalid skill name`。
|
|
190
|
+
|
|
191
|
+
现在两层纵深防御:
|
|
192
|
+
|
|
193
|
+
- `src/skill-scan.js`:根级裸放 `.md` **必须有 frontmatter** 才算技能,名字非 kebab-case
|
|
194
|
+
一律跳过 —— 与宿主 `discoverRoot` / `parseSkillFile` 同口径(宿主对同样的文件打的是
|
|
195
|
+
`skill file ... ignored: invalid skill name`)。
|
|
196
|
+
- `lib/index.js`:第一段与第二段都过 `isSkillName()`,兜住任何别的来源漏进来的脏名字。
|
|
197
|
+
|
|
184
198
|
## 5. 约束(踩过的坑)
|
|
185
199
|
|
|
186
200
|
- **cordis 服务必须声明式注入**:`export const inject` 里没有的服务,读 `ctx.xxx`
|
package/lib/index.js
CHANGED
|
@@ -101,8 +101,15 @@ export function createFilterProvider(sessionId, upstream, denied, ensureFresh, l
|
|
|
101
101
|
// 还是上游那份,影子只换掉 invocation 与 provider,右列能看出"它被屏蔽了"。
|
|
102
102
|
const shadows = [];
|
|
103
103
|
const emitted = new Set();
|
|
104
|
+
const invalid = new Set();
|
|
104
105
|
for (const summary of summaries) {
|
|
105
106
|
if (!summary || !denied.has(summary.name)) continue;
|
|
107
|
+
// 看得见 ≠ 合法(2026-11-12,issue #6):扫盘会把技能根下裸放的
|
|
108
|
+
// `README-官方技能说明.md` 这类文件当成技能,名字取文件名去 `.md`。
|
|
109
|
+
// 旧实现只在第二段校验,这一段整条照抄 → 非法名字原样发出去 →
|
|
110
|
+
// 宿主 validateCandidate 抛错 → 整个 provider 作废 → **所有对话**
|
|
111
|
+
// 报 "returned invalid skill name" 且每轮都失败。两段都必须校验。
|
|
112
|
+
if (!isSkillName(summary.name)) { invalid.add(summary.name); continue; }
|
|
106
113
|
shadows.push({
|
|
107
114
|
...summary,
|
|
108
115
|
provider: PROVIDER_NAME,
|
|
@@ -123,10 +130,9 @@ export function createFilterProvider(sessionId, upstream, denied, ensureFresh, l
|
|
|
123
130
|
// 修法:屏蔽是**按名字**生效的,不需要知道那名字长什么样 —— 看不见的
|
|
124
131
|
// 名字直接造一条最小合法候选,就能在近层整条覆盖上游那份。
|
|
125
132
|
let backfilled = 0;
|
|
126
|
-
const invalid = [];
|
|
127
133
|
for (const name of denied) {
|
|
128
134
|
if (emitted.has(name)) continue;
|
|
129
|
-
if (!isSkillName(name)) { invalid.
|
|
135
|
+
if (!isSkillName(name)) { invalid.add(name); continue; }
|
|
130
136
|
shadows.push({
|
|
131
137
|
name,
|
|
132
138
|
description: `[blocked by skill-tag] ${name}`,
|
|
@@ -148,8 +154,8 @@ export function createFilterProvider(sessionId, upstream, denied, ensureFresh, l
|
|
|
148
154
|
+ ` shadowed=${emitted.size} backfilled=${backfilled}`
|
|
149
155
|
+ `${upstream.lastDegraded === true ? " hostSnapshot=skipped(nested)" : ""}`);
|
|
150
156
|
}
|
|
151
|
-
if (invalid.
|
|
152
|
-
logWarn(`[skill-tag]
|
|
157
|
+
if (invalid.size > 0) {
|
|
158
|
+
logWarn(`[skill-tag] names skipped (not kebab-case): ${[...invalid].slice(0, 8).join(", ")}`);
|
|
153
159
|
}
|
|
154
160
|
} catch { /* 观测不得影响主路径 */ }
|
|
155
161
|
}
|
package/package.json
CHANGED
package/src/skill-scan.js
CHANGED
|
@@ -70,6 +70,17 @@ function collectRoot(dir, depth, root, out) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 合法技能名:kebab-case,与宿主 @deepseek-ai/dsh-skill 的 SKILL_NAME 同一正则。
|
|
75
|
+
*
|
|
76
|
+
* 为什么扫描器也要过这一关(2026-11-12,issue #6):扫描结果会进 universe,
|
|
77
|
+
* 于是 denied = universe − visible 里也会出现它;只要这个名字非法,宿主
|
|
78
|
+
* validateCandidate 就会抛错、整个 provider 作废、**所有对话**都跑不下去。
|
|
79
|
+
* 宿主自己的文件系统 provider 早就这么做了(`skill file ... ignored: invalid
|
|
80
|
+
* skill name`),自扫描必须同口径,否则插件会把宿主本来就不认的名字当成技能。
|
|
81
|
+
*/
|
|
82
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
83
|
+
|
|
73
84
|
function pushSkill(out, mdPath, root, disabled, fallbackName) {
|
|
74
85
|
let fm = null;
|
|
75
86
|
try {
|
|
@@ -77,8 +88,15 @@ function pushSkill(out, mdPath, root, disabled, fallbackName) {
|
|
|
77
88
|
} catch {
|
|
78
89
|
return; // unreadable file — skip
|
|
79
90
|
}
|
|
91
|
+
// 裸放的根级 .md(flat skill)与宿主同口径:**必须有 frontmatter**。
|
|
92
|
+
// 没有 frontmatter 的 .md 只是一份普通文档(README、笔记…),不是技能;
|
|
93
|
+
// 旧实现会用文件名凭空造一个技能名,正是 issue #6 的来源。
|
|
94
|
+
const isFlat = fallbackName !== undefined;
|
|
95
|
+
if (isFlat && fm === null) return;
|
|
80
96
|
const name = fm?.name || fallbackName || path.basename(path.dirname(mdPath));
|
|
81
97
|
if (!name) return;
|
|
98
|
+
// 非法名字宁可丢掉:它既进不了宿主注册表,还会炸掉整批候选。
|
|
99
|
+
if (!SKILL_NAME_RE.test(name)) return;
|
|
82
100
|
out.push({
|
|
83
101
|
name,
|
|
84
102
|
description: fm?.description ?? "",
|