@zhangchi835/skill-manager 1.0.0 → 1.0.3
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/README.md +0 -0
- package/package.json +1 -1
- package/plugin/README.md +66 -0
- package/plugin/cordis.patch.yml +9 -3
- package/plugin/lib/client.js +4 -4
- package/plugin/lib/index.js +101 -88
- package/plugin/package.json +13 -2
- package/_verify/client-body.js +0 -225
- package/_verify/host-body.js +0 -321
- package/_verify/host-test.js +0 -190
- package/skills/cordis-skills-manager-plugin.md +0 -170
- package/skills/goutoujunshi.md +0 -94
- package/skills//344/270/255/346/226/207/345/221/275/345/220/215/346/265/213/350/257/225.md +0 -6
package/_verify/host-body.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
return {
|
|
2
|
-
apply(ctx) {
|
|
3
|
-
const skills = ctx.get('skills')
|
|
4
|
-
if (skills === undefined) return
|
|
5
|
-
const fsService = ctx.get('fs')
|
|
6
|
-
const sandboxPolicy = ctx.get('sandboxPolicy')
|
|
7
|
-
|
|
8
|
-
let workspaceRoot = ''
|
|
9
|
-
if (sandboxPolicy !== undefined && typeof sandboxPolicy.workspaceRoot === 'string') workspaceRoot = sandboxPolicy.workspaceRoot
|
|
10
|
-
|
|
11
|
-
const state = {
|
|
12
|
-
folder: workspaceRoot === '' ? '' : workspaceRoot.replace(/[\\/]+$/, '') + '/skills',
|
|
13
|
-
folderExists: false,
|
|
14
|
-
scanned: false,
|
|
15
|
-
scanning: false,
|
|
16
|
-
scanTime: null,
|
|
17
|
-
scanError: '',
|
|
18
|
-
skills: [],
|
|
19
|
-
selected: [],
|
|
20
|
-
otherSkills: [],
|
|
21
|
-
}
|
|
22
|
-
let generation = 0
|
|
23
|
-
let scanPromise = null
|
|
24
|
-
let invalidate = function () {}
|
|
25
|
-
|
|
26
|
-
function msg(error) {
|
|
27
|
-
return error !== null && typeof error === 'object' && typeof error.message === 'string' ? error.message : String(error)
|
|
28
|
-
}
|
|
29
|
-
function kebab(value) {
|
|
30
|
-
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').replace(/-+$/, '')
|
|
31
|
-
}
|
|
32
|
-
function hashText(text) {
|
|
33
|
-
let h = 5381
|
|
34
|
-
for (let i = 0; i < text.length; i++) h = (((h << 5) + h) + text.charCodeAt(i)) >>> 0
|
|
35
|
-
return h.toString(36)
|
|
36
|
-
}
|
|
37
|
-
function firstLine(text) {
|
|
38
|
-
const match = /^\s*([^\n\r]*)/.exec(text === undefined || text === null ? '' : String(text))
|
|
39
|
-
return match === null ? '' : match[1].trim()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseFrontmatterFields(raw) {
|
|
43
|
-
const data = Object.create(null)
|
|
44
|
-
const lines = raw.split('\n')
|
|
45
|
-
for (let i = 0; i < lines.length; i++) {
|
|
46
|
-
const line = lines[i].replace(/\r$/, '')
|
|
47
|
-
const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line)
|
|
48
|
-
if (match === null) continue
|
|
49
|
-
const key = match[1]
|
|
50
|
-
let value = match[2].trim()
|
|
51
|
-
const first = value.charAt(0)
|
|
52
|
-
const last = value.charAt(value.length - 1)
|
|
53
|
-
if ((first === '"' && last === '"' && value.length >= 2) || (first === "'" && last === "'" && value.length >= 2)) value = value.slice(1, -1)
|
|
54
|
-
else if (value === 'true' || value === 'false') value = value === 'true'
|
|
55
|
-
else if (/^-?\d+$/.test(value)) value = Number(value)
|
|
56
|
-
data[key] = value
|
|
57
|
-
}
|
|
58
|
-
return data
|
|
59
|
-
}
|
|
60
|
-
function parseFrontmatter(text) {
|
|
61
|
-
if (typeof text !== 'string' || text.length === 0) return null
|
|
62
|
-
const firstEnd = text.indexOf('\n')
|
|
63
|
-
if (firstEnd < 0) return null
|
|
64
|
-
if (text.slice(0, firstEnd).replace(/\r$/, '') !== '---') return null
|
|
65
|
-
let lineStart = firstEnd + 1
|
|
66
|
-
while (lineStart <= text.length) {
|
|
67
|
-
const nextNewline = text.indexOf('\n', lineStart)
|
|
68
|
-
const lineEnd = nextNewline < 0 ? text.length : nextNewline
|
|
69
|
-
if (text.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
|
|
70
|
-
const bodyStart = nextNewline < 0 ? text.length : nextNewline + 1
|
|
71
|
-
return { data: parseFrontmatterFields(text.slice(firstEnd + 1, lineStart)), body: text.slice(bodyStart) }
|
|
72
|
-
}
|
|
73
|
-
if (nextNewline < 0) return null
|
|
74
|
-
lineStart = nextNewline + 1
|
|
75
|
-
}
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseSkill(text, rel) {
|
|
80
|
-
const fm = parseFrontmatter(text)
|
|
81
|
-
const fileName = rel.replace(/^.*[\\/]/, '')
|
|
82
|
-
const stem = fileName.replace(/\.md$/i, '')
|
|
83
|
-
let name = null
|
|
84
|
-
let description = null
|
|
85
|
-
let whenToUse
|
|
86
|
-
let metadata
|
|
87
|
-
if (fm !== null) {
|
|
88
|
-
if (typeof fm.data.name === 'string' && fm.data.name.trim() !== '') name = fm.data.name.trim()
|
|
89
|
-
if (typeof fm.data.description === 'string' && fm.data.description.trim() !== '') description = fm.data.description.trim()
|
|
90
|
-
if (typeof fm.data['whenToUse'] === 'string' && fm.data['whenToUse'].trim() !== '') whenToUse = fm.data['whenToUse'].trim()
|
|
91
|
-
if (fm.data.metadata !== null && typeof fm.data.metadata === 'object' && !Array.isArray(fm.data.metadata)) metadata = fm.data.metadata
|
|
92
|
-
}
|
|
93
|
-
if (name === null) name = stem
|
|
94
|
-
let finalName = kebab(name)
|
|
95
|
-
if (finalName === '') finalName = 'skill-' + hashText(rel)
|
|
96
|
-
if (finalName.length > 64) finalName = finalName.slice(0, 64).replace(/-+$/, '')
|
|
97
|
-
const body = fm === null ? text : fm.body
|
|
98
|
-
if (description === null) {
|
|
99
|
-
description = firstLine(body)
|
|
100
|
-
if (description === '') description = 'Skill loaded from ' + rel
|
|
101
|
-
}
|
|
102
|
-
return { name: finalName, description: description, whenToUse: whenToUse, metadata: metadata, content: String(body).trim() }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function walk(dirTarget, rel, found, errors, depth, budget) {
|
|
106
|
-
if (depth > 24 || budget.count >= 4000) return
|
|
107
|
-
let entries
|
|
108
|
-
try { entries = await fsService.listDir(dirTarget, undefined) } catch (error) { errors.push('listDir failed ' + rel + ': ' + msg(error)); return }
|
|
109
|
-
for (let i = 0; i < entries.length && budget.count < 4000; i++) {
|
|
110
|
-
const entry = entries[i]
|
|
111
|
-
budget.count++
|
|
112
|
-
const childRel = rel === '' ? entry.name : rel + '/' + entry.name
|
|
113
|
-
if (entry.type === 'directory') {
|
|
114
|
-
if (entry.target === undefined || entry.target === null) continue
|
|
115
|
-
await walk(entry.target, childRel, found, errors, depth + 1, budget)
|
|
116
|
-
} else if (entry.type === 'file' && /\.md$/i.test(entry.name)) {
|
|
117
|
-
if (entry.target === undefined || entry.target === null) continue
|
|
118
|
-
let text
|
|
119
|
-
try { text = await fsService.readText(entry.target, undefined) } catch (error) { errors.push('read failed ' + childRel + ': ' + msg(error)); continue }
|
|
120
|
-
const parsed = parseSkill(text, childRel)
|
|
121
|
-
const displayPath = typeof entry.target.displayPath === 'string' ? entry.target.displayPath : state.folder + '/' + childRel
|
|
122
|
-
found.push({ name: parsed.name, description: parsed.description, whenToUse: parsed.whenToUse, rel: childRel, path: displayPath, metadata: parsed.metadata })
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function collectOtherSkills() {
|
|
128
|
-
try {
|
|
129
|
-
const list = await skills.list({ cwd: workspaceRoot === '' ? undefined : workspaceRoot })
|
|
130
|
-
const out = []
|
|
131
|
-
for (let i = 0; i < list.length; i++) {
|
|
132
|
-
const s = list[i]
|
|
133
|
-
if (s === null || typeof s !== 'object' || s.provider === 'skills-hub') continue
|
|
134
|
-
out.push({
|
|
135
|
-
name: typeof s.name === 'string' ? s.name : '',
|
|
136
|
-
description: typeof s.description === 'string' ? s.description : '',
|
|
137
|
-
provider: typeof s.provider === 'string' ? s.provider : '',
|
|
138
|
-
source: typeof s.source === 'string' ? s.source : '',
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
return out
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return []
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function scanNow() {
|
|
148
|
-
const gen = ++generation
|
|
149
|
-
state.scanning = true
|
|
150
|
-
state.scanError = ''
|
|
151
|
-
const errors = []
|
|
152
|
-
const found = []
|
|
153
|
-
try {
|
|
154
|
-
if (state.folder === '') {
|
|
155
|
-
// 未配置文件夹:静默等待配置
|
|
156
|
-
} else if (fsService === undefined) {
|
|
157
|
-
errors.push('filesystem service unavailable')
|
|
158
|
-
} else {
|
|
159
|
-
let target
|
|
160
|
-
try { target = await fsService.resolve(state.folder) } catch (error) { errors.push('resolve failed: ' + msg(error)) }
|
|
161
|
-
if (target !== undefined) {
|
|
162
|
-
let info
|
|
163
|
-
try { info = await fsService.stat(target) } catch (error) { errors.push('stat failed: ' + msg(error)) }
|
|
164
|
-
if (info === undefined) { state.folderExists = false; errors.push('folder not found: ' + state.folder) }
|
|
165
|
-
else if (info.type !== 'directory') { state.folderExists = false; errors.push('not a directory: ' + state.folder) }
|
|
166
|
-
else { state.folderExists = true; await walk(target, '', found, errors, 0, { count: 0 }) }
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} catch (error) { errors.push(msg(error)) }
|
|
170
|
-
if (gen !== generation) return
|
|
171
|
-
const seen = Object.create(null)
|
|
172
|
-
const out = []
|
|
173
|
-
for (let i = 0; i < found.length; i++) {
|
|
174
|
-
const s = found[i]
|
|
175
|
-
if (seen[s.name] === true) continue
|
|
176
|
-
seen[s.name] = true
|
|
177
|
-
out.push(s)
|
|
178
|
-
}
|
|
179
|
-
out.sort(function (a, b) { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 })
|
|
180
|
-
const keep = []
|
|
181
|
-
for (let i = 0; i < state.selected.length; i++) if (seen[state.selected[i]] === true) keep.push(state.selected[i])
|
|
182
|
-
state.selected = keep
|
|
183
|
-
state.skills = out
|
|
184
|
-
state.scanError = errors.join('; ')
|
|
185
|
-
state.scanned = true
|
|
186
|
-
state.scanning = false
|
|
187
|
-
state.scanTime = typeof Date === 'function' ? new Date().toISOString() : null
|
|
188
|
-
state.otherSkills = await collectOtherSkills()
|
|
189
|
-
try { invalidate() } catch (_) {}
|
|
190
|
-
console.log('[skills-hub] scan complete: ' + out.length + ' skills, errors: ' + (errors.length === 0 ? 'none' : errors.join('; ')))
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function scheduleScan() {
|
|
194
|
-
if (scanPromise !== null) return scanPromise
|
|
195
|
-
const promise = scanNow().catch(function (error) { console.error('[skills-hub] scan failed:', msg(error)) }).finally(function () { scanPromise = null })
|
|
196
|
-
scanPromise = promise
|
|
197
|
-
return promise
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
ctx.effect(function () {
|
|
201
|
-
return skills.registerProvider(function (control) {
|
|
202
|
-
invalidate = function () { try { control.invalidate() } catch (_) {} }
|
|
203
|
-
return {
|
|
204
|
-
name: 'skills-hub',
|
|
205
|
-
async list(options) {
|
|
206
|
-
const signal = options === undefined ? undefined : options.signal
|
|
207
|
-
const selectedSet = Object.create(null)
|
|
208
|
-
for (let i = 0; i < state.selected.length; i++) selectedSet[state.selected[i]] = true
|
|
209
|
-
const out = []
|
|
210
|
-
for (let i = 0; i < state.skills.length; i++) {
|
|
211
|
-
if (signal !== undefined && signal.aborted) break
|
|
212
|
-
const s = state.skills[i]
|
|
213
|
-
if (selectedSet[s.name] !== true) continue
|
|
214
|
-
const candidate = {
|
|
215
|
-
name: s.name,
|
|
216
|
-
description: s.description,
|
|
217
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
218
|
-
source: 'custom',
|
|
219
|
-
provider: 'skills-hub',
|
|
220
|
-
rank: 350,
|
|
221
|
-
locator: { path: s.path, rel: s.rel },
|
|
222
|
-
resourceBase: { kind: 'directory', path: state.folder },
|
|
223
|
-
path: s.path,
|
|
224
|
-
}
|
|
225
|
-
if (s.whenToUse !== undefined) candidate.whenToUse = s.whenToUse
|
|
226
|
-
out.push(candidate)
|
|
227
|
-
}
|
|
228
|
-
return out
|
|
229
|
-
},
|
|
230
|
-
async get(candidate, options) {
|
|
231
|
-
const signal = options === undefined ? undefined : options.signal
|
|
232
|
-
const loc = candidate === null || candidate === undefined ? null : candidate.locator
|
|
233
|
-
const path = loc === null || loc === undefined ? undefined : loc.path
|
|
234
|
-
if (typeof path !== 'string' || path === '' || fsService === undefined) return undefined
|
|
235
|
-
if (signal !== undefined && signal.aborted) return undefined
|
|
236
|
-
let target
|
|
237
|
-
try { target = await fsService.resolve(path, { signal: signal }) } catch (_) { return undefined }
|
|
238
|
-
if (signal !== undefined && signal.aborted) return undefined
|
|
239
|
-
let text
|
|
240
|
-
try { text = await fsService.readText(target, signal) } catch (_) { return undefined }
|
|
241
|
-
if (signal !== undefined && signal.aborted) return undefined
|
|
242
|
-
const rel = loc === null || loc === undefined ? '' : loc.rel
|
|
243
|
-
const parsed = parseSkill(text, typeof rel === 'string' ? rel : '')
|
|
244
|
-
const definition = {
|
|
245
|
-
name: candidate.name,
|
|
246
|
-
description: parsed.description,
|
|
247
|
-
invocation: { modelInvocable: true, userInvocable: true },
|
|
248
|
-
source: 'custom',
|
|
249
|
-
provider: 'skills-hub',
|
|
250
|
-
resourceBase: { kind: 'directory', path: state.folder },
|
|
251
|
-
path: path,
|
|
252
|
-
content: parsed.content,
|
|
253
|
-
}
|
|
254
|
-
if (parsed.whenToUse !== undefined) definition.whenToUse = parsed.whenToUse
|
|
255
|
-
if (parsed.metadata !== undefined) definition.metadata = parsed.metadata
|
|
256
|
-
return definition
|
|
257
|
-
},
|
|
258
|
-
}
|
|
259
|
-
})
|
|
260
|
-
}, 'skills-hub provider')
|
|
261
|
-
|
|
262
|
-
function selectedSetOf() {
|
|
263
|
-
const set = Object.create(null)
|
|
264
|
-
for (let i = 0; i < state.selected.length; i++) set[state.selected[i]] = true
|
|
265
|
-
return set
|
|
266
|
-
}
|
|
267
|
-
async function buildView() {
|
|
268
|
-
const set = selectedSetOf()
|
|
269
|
-
const skillRows = []
|
|
270
|
-
for (let i = 0; i < state.skills.length; i++) {
|
|
271
|
-
const s = state.skills[i]
|
|
272
|
-
skillRows.push({
|
|
273
|
-
name: s.name,
|
|
274
|
-
description: s.description,
|
|
275
|
-
whenToUse: s.whenToUse === undefined ? null : s.whenToUse,
|
|
276
|
-
rel: s.rel,
|
|
277
|
-
path: s.path,
|
|
278
|
-
selected: set[s.name] === true,
|
|
279
|
-
})
|
|
280
|
-
}
|
|
281
|
-
return {
|
|
282
|
-
folder: state.folder,
|
|
283
|
-
folderExists: state.folderExists,
|
|
284
|
-
scanned: state.scanned,
|
|
285
|
-
scanning: state.scanning,
|
|
286
|
-
scanTime: state.scanTime,
|
|
287
|
-
scanError: state.scanError,
|
|
288
|
-
skills: skillRows,
|
|
289
|
-
selected: state.selected.slice(),
|
|
290
|
-
otherSkills: state.otherSkills.slice(),
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
ctx.effect(function () { return harness.handle('state', function () { return buildView() }) }, 'skills-hub rpc state')
|
|
295
|
-
ctx.effect(function () {
|
|
296
|
-
return harness.handle('setFolder', function (args) {
|
|
297
|
-
const path = args !== null && typeof args === 'object' && typeof args.path === 'string' ? args.path : ''
|
|
298
|
-
const trimmed = path.trim()
|
|
299
|
-
if (trimmed !== '') state.folder = trimmed
|
|
300
|
-
return scheduleScan().then(buildView)
|
|
301
|
-
})
|
|
302
|
-
}, 'skills-hub rpc setFolder')
|
|
303
|
-
ctx.effect(function () {
|
|
304
|
-
return harness.handle('setSelected', function (args) {
|
|
305
|
-
const names = args !== null && typeof args === 'object' && Array.isArray(args.names) ? args.names.filter(function (n) { return typeof n === 'string' }) : []
|
|
306
|
-
const known = Object.create(null)
|
|
307
|
-
for (let i = 0; i < state.skills.length; i++) known[state.skills[i].name] = true
|
|
308
|
-
const next = []
|
|
309
|
-
for (let i = 0; i < names.length; i++) if (known[names[i]] === true && next.indexOf(names[i]) < 0) next.push(names[i])
|
|
310
|
-
state.selected = next
|
|
311
|
-
try { invalidate() } catch (_) {}
|
|
312
|
-
return buildView()
|
|
313
|
-
})
|
|
314
|
-
}, 'skills-hub rpc setSelected')
|
|
315
|
-
ctx.effect(function () {
|
|
316
|
-
return harness.handle('reload', function () { return scheduleScan().then(buildView) })
|
|
317
|
-
}, 'skills-hub rpc reload')
|
|
318
|
-
|
|
319
|
-
scheduleScan()
|
|
320
|
-
},
|
|
321
|
-
}
|
package/_verify/host-test.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// skills-hub Host 逻辑模拟验证(使用真实 workspace/skills 文件夹 + mock fs/skills/harness)
|
|
2
|
-
const fs = require('node:fs')
|
|
3
|
-
const path = require('node:path')
|
|
4
|
-
|
|
5
|
-
const workspaceRoot = path.resolve(__dirname, '..')
|
|
6
|
-
const skillsFolder = path.join(workspaceRoot, 'skills')
|
|
7
|
-
|
|
8
|
-
function makeTarget(p) {
|
|
9
|
-
return { displayPath: p }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const fsService = {
|
|
13
|
-
async resolve(p) {
|
|
14
|
-
if (typeof p !== 'string') throw new Error('bad path')
|
|
15
|
-
return makeTarget(path.resolve(p))
|
|
16
|
-
},
|
|
17
|
-
async stat(target, signal) {
|
|
18
|
-
try {
|
|
19
|
-
const s = fs.statSync(target.displayPath)
|
|
20
|
-
return { type: s.isDirectory() ? 'directory' : s.isFile() ? 'file' : 'other' }
|
|
21
|
-
} catch (error) {
|
|
22
|
-
return undefined
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
async listDir(target, signal) {
|
|
26
|
-
const names = fs.readdirSync(target.displayPath)
|
|
27
|
-
const out = []
|
|
28
|
-
for (const name of names) {
|
|
29
|
-
const full = path.join(target.displayPath, name)
|
|
30
|
-
let type = 'other'
|
|
31
|
-
try {
|
|
32
|
-
const s = fs.statSync(full)
|
|
33
|
-
type = s.isDirectory() ? 'directory' : s.isFile() ? 'file' : 'other'
|
|
34
|
-
} catch (_) {}
|
|
35
|
-
out.push({ name, type, target: makeTarget(full) })
|
|
36
|
-
}
|
|
37
|
-
return out
|
|
38
|
-
},
|
|
39
|
-
async readText(target, signal) {
|
|
40
|
-
return fs.readFileSync(target.displayPath, 'utf8')
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let invalidateCount = 0
|
|
45
|
-
let providerRef = null
|
|
46
|
-
const skillsMock = {
|
|
47
|
-
registerProvider(factory) {
|
|
48
|
-
providerRef = factory({
|
|
49
|
-
signal: { addEventListener() {} },
|
|
50
|
-
invalidate() { invalidateCount++ },
|
|
51
|
-
})
|
|
52
|
-
return () => {}
|
|
53
|
-
},
|
|
54
|
-
async list(options) {
|
|
55
|
-
// 模拟“其他提供者”返回两条只读技能,验证 otherSkills 收集
|
|
56
|
-
return [
|
|
57
|
-
{ name: 'shipped-a', description: 'shipped skill a', provider: 'filesystem', source: 'bundled' },
|
|
58
|
-
{ name: 'shipped-b', description: 'shipped skill b', provider: 'filesystem', source: 'bundled' },
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const handlers = {}
|
|
64
|
-
const harnessMock = {
|
|
65
|
-
handle(method, handler) {
|
|
66
|
-
handlers[method] = handler
|
|
67
|
-
return () => {}
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const ctx = {
|
|
72
|
-
get(name) {
|
|
73
|
-
if (name === 'skills') return skillsMock
|
|
74
|
-
if (name === 'fs') return fsService
|
|
75
|
-
if (name === 'sandboxPolicy') return { workspaceRoot }
|
|
76
|
-
return undefined
|
|
77
|
-
},
|
|
78
|
-
effect(cb, label) {
|
|
79
|
-
cb()
|
|
80
|
-
return () => {}
|
|
81
|
-
},
|
|
82
|
-
on() {
|
|
83
|
-
return () => {}
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function assert(condition, label) {
|
|
88
|
-
if (!condition) {
|
|
89
|
-
console.error('FAIL:', label)
|
|
90
|
-
process.exitCode = 1
|
|
91
|
-
} else {
|
|
92
|
-
console.log('PASS:', label)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function main() {
|
|
97
|
-
// 1) 语法编译检查(host + client 代码体)
|
|
98
|
-
const hostBody = fs.readFileSync(path.join(__dirname, 'host-body.js'), 'utf8')
|
|
99
|
-
const clientBody = fs.readFileSync(path.join(__dirname, 'client-body.js'), 'utf8')
|
|
100
|
-
try {
|
|
101
|
-
new Function(hostBody)
|
|
102
|
-
console.log('PASS: host body compiles')
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error('FAIL: host body compile:', error.message)
|
|
105
|
-
process.exitCode = 1
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
new Function(clientBody)
|
|
109
|
-
console.log('PASS: client body compiles')
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error('FAIL: client body compile:', error.message)
|
|
112
|
-
process.exitCode = 1
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const plugin = new Function('harness', hostBody)(harnessMock)
|
|
116
|
-
plugin.apply(ctx)
|
|
117
|
-
|
|
118
|
-
// 等待首次扫描(异步)
|
|
119
|
-
for (let i = 0; i < 100; i++) {
|
|
120
|
-
const view = await handlers['state']()
|
|
121
|
-
if (!view.scanning) break
|
|
122
|
-
await new Promise((r) => setTimeout(r, 20))
|
|
123
|
-
}
|
|
124
|
-
const view = await handlers['state']()
|
|
125
|
-
console.log('STATE scanError:', JSON.stringify(view.scanError))
|
|
126
|
-
console.log('STATE skills:', JSON.stringify(view.skills.map((s) => ({ name: s.name, rel: s.rel }))))
|
|
127
|
-
console.log('STATE otherSkills:', JSON.stringify(view.otherSkills))
|
|
128
|
-
|
|
129
|
-
assert(view.scanned === true, 'scan completed')
|
|
130
|
-
assert(view.scanError === '', 'no scan errors, got: ' + view.scanError)
|
|
131
|
-
|
|
132
|
-
// 2) 无命名规则验证:中文文件名、无 frontmatter 也能加载
|
|
133
|
-
const chinese = view.skills.find((s) => s.rel.includes('中文'))
|
|
134
|
-
assert(chinese !== undefined, 'Chinese filename with no frontmatter loaded, name=' + (chinese && chinese.name))
|
|
135
|
-
if (chinese !== undefined) {
|
|
136
|
-
assert(/^skill-[0-9a-z]+$/.test(chinese.name), 'hash fallback name is kebab-case: ' + chinese.name)
|
|
137
|
-
}
|
|
138
|
-
const guide = view.skills.find((s) => s.name === 'cordis-skills-manager-plugin')
|
|
139
|
-
assert(guide !== undefined, 'distilled skill loaded via frontmatter name')
|
|
140
|
-
|
|
141
|
-
// 3) 未勾选时 provider 不暴露任何技能(不是默认全启用)
|
|
142
|
-
const listNone = await providerRef.list({})
|
|
143
|
-
assert(listNone.length === 0, 'provider lists nothing when nothing selected')
|
|
144
|
-
|
|
145
|
-
// 4) 只勾选一个 -> 只暴露那一个(选择即启用,精确匹配)
|
|
146
|
-
const names = view.skills.map((s) => s.name)
|
|
147
|
-
await handlers['setSelected']({ names: [names[0]] })
|
|
148
|
-
const listOne = await providerRef.list({})
|
|
149
|
-
assert(listOne.length === 1 && listOne[0].name === names[0], 'only the selected skill is exposed')
|
|
150
|
-
assert(listOne[0].invocation.modelInvocable === true && listOne[0].invocation.userInvocable === true, 'invocation policy permits both')
|
|
151
|
-
|
|
152
|
-
// 5) get() 返回完整定义(content 非空、resourceBase 正确)
|
|
153
|
-
const definition = await providerRef.get(listOne[0], {})
|
|
154
|
-
assert(definition !== undefined && typeof definition.content === 'string' && definition.content.length > 0, 'get() returns full definition')
|
|
155
|
-
const norm = (p) => String(p).replace(/\\/g, '/')
|
|
156
|
-
assert(definition.resourceBase.kind === 'directory' && norm(definition.resourceBase.path) === norm(skillsFolder), 'resourceBase directory = skills folder (separators normalized)')
|
|
157
|
-
|
|
158
|
-
// 6) 勾选未知名称会被裁剪
|
|
159
|
-
const afterBad = await handlers['setSelected']({ names: [names[0], 'not-exists', ''] })
|
|
160
|
-
assert(afterBad.selected.length === 1, 'unknown names pruned')
|
|
161
|
-
|
|
162
|
-
// 7) 全选后全部暴露
|
|
163
|
-
await handlers['setSelected']({ names })
|
|
164
|
-
const listAll = await providerRef.list({})
|
|
165
|
-
assert(listAll.length === names.length, 'select all exposes all')
|
|
166
|
-
|
|
167
|
-
// 8) 清空选择 -> 全部停用(精确匹配,不是全启用也不是全禁用)
|
|
168
|
-
await handlers['setSelected']({ names: [] })
|
|
169
|
-
const listEmpty = await providerRef.list({})
|
|
170
|
-
assert(listEmpty.length === 0, 'clearing selection disables all managed skills')
|
|
171
|
-
|
|
172
|
-
// 9) setFolder 到不存在目录 -> 状态报错不崩溃
|
|
173
|
-
const badFolder = await handlers['setFolder']({ path: path.join(workspaceRoot, 'no-such-folder') })
|
|
174
|
-
assert(badFolder.scanError !== '' && badFolder.scanError.includes('folder not found'), 'missing folder reported, not crashed')
|
|
175
|
-
|
|
176
|
-
// 10) setFolder 到真实文件夹(含子目录 md)-> 重新扫描成功
|
|
177
|
-
const goodFolder = await handlers['setFolder']({ path: skillsFolder })
|
|
178
|
-
assert(goodFolder.scanned === true && goodFolder.scanError === '', 'rescan real folder ok')
|
|
179
|
-
assert(goodFolder.skills.length >= 2, 'rescanned skills found: ' + goodFolder.skills.length)
|
|
180
|
-
|
|
181
|
-
// 11) invalidate 被调用(选择变更 + 扫描完成都会触发)
|
|
182
|
-
assert(invalidateCount > 0, 'provider invalidate called on changes: ' + invalidateCount)
|
|
183
|
-
|
|
184
|
-
console.log('--- verification done ---')
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
main().catch((error) => {
|
|
188
|
-
console.error('RUNNER ERROR:', error)
|
|
189
|
-
process.exit(1)
|
|
190
|
-
})
|