@zhangchi835/skill-manager 1.0.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/_verify/client-body.js +225 -0
- package/_verify/host-body.js +321 -0
- package/_verify/host-test.js +190 -0
- package/index.js +4 -0
- package/package.json +12 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/lib/client.js +213 -0
- package/plugin/lib/index.js +421 -0
- package/plugin/package.json +26 -0
- package/skills/cordis-skills-manager-plugin.md +170 -0
- package/skills/goutoujunshi.md +94 -0
- package/skills//344/270/255/346/226/207/345/221/275/345/220/215/346/265/213/350/257/225.md +6 -0
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
})
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-skills-hub',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
7
|
+
const react = require('react')
|
|
8
|
+
|
|
9
|
+
const CSS = [
|
|
10
|
+
'.skh-trigger-wrap{display:inline-flex;align-items:center}',
|
|
11
|
+
'.skh-trigger{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1;cursor:pointer}',
|
|
12
|
+
'.skh-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
13
|
+
'.skh-trigger-on{border-color:var(--dsw-alias-accent, #5b8cff);color:var(--dsw-alias-label-primary)}',
|
|
14
|
+
'.skh-trigger-icon{font-size:13px;line-height:1}',
|
|
15
|
+
'.skh-trigger-text{font-weight:500}',
|
|
16
|
+
'.skh-trigger-count{min-width:15px;height:15px;padding:0 4px;border-radius:8px;background:var(--dsw-alias-accent, #5b8cff);color:#fff;font-size:10px;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box}',
|
|
17
|
+
'.skh-pop-backdrop{position:fixed;inset:0;z-index:90;background:transparent}',
|
|
18
|
+
'.skh-pop-card{position:absolute;bottom:calc(100% + 6px);left:0;z-index:100;min-width:280px;max-width:min(380px,100%);max-height:min(360px,65vh);background:var(--dsw-specific-menu, var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-inverted, var(--dsw-alias-border-l1));border-radius:12px;box-shadow:var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.2));display:flex;flex-direction:column;overflow:hidden;padding:8px;box-sizing:border-box;color:var(--dsw-alias-label-primary);text-align:left}',
|
|
19
|
+
'.skh-pop-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);padding:2px 6px 6px}',
|
|
20
|
+
'.skh-pop-list{overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:2px}',
|
|
21
|
+
'.skh-pop-row{display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:8px;cursor:pointer}',
|
|
22
|
+
'.skh-pop-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
23
|
+
'.skh-pop-check{margin:3px 0 0;flex:none;accent-color:var(--dsw-alias-accent, #5b8cff)}',
|
|
24
|
+
'.skh-pop-meta{min-width:0;flex:1}',
|
|
25
|
+
'.skh-pop-name{font-size:13px;font-weight:600;word-break:break-all}',
|
|
26
|
+
'.skh-pop-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}',
|
|
27
|
+
'.skh-pop-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px}',
|
|
28
|
+
'.skh-pop-foot{display:flex;align-items:center;gap:8px;border-top:1px solid var(--dsw-alias-border-l2, transparent);margin-top:6px;padding:8px 6px 2px}',
|
|
29
|
+
'.skh-pop-folder{flex:1;min-width:0;font-size:11px;color:var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
30
|
+
'.skh-pop-done{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;padding:3px 10px;font-size:12px;cursor:pointer}',
|
|
31
|
+
'.skh-pop-done:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
32
|
+
'.skh-admin{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary);font-size:13px}',
|
|
33
|
+
'.skh-title{font-size:14px;font-weight:600}',
|
|
34
|
+
'.skh-sub{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
35
|
+
'.skh-field{display:flex;gap:8px}',
|
|
36
|
+
'.skh-input{flex:1;min-width:0;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 8px;font-size:12px;outline:none}',
|
|
37
|
+
'.skh-btn{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer;white-space:nowrap}',
|
|
38
|
+
'.skh-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
39
|
+
'.skh-status{font-size:12px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}',
|
|
40
|
+
'.skh-status-err{color:var(--dsw-alias-state-error-primary)}',
|
|
41
|
+
'.skh-row{display:flex;flex-direction:column;gap:2px;border:1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-l1));border-radius:8px;padding:8px}',
|
|
42
|
+
'.skh-row-top{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}',
|
|
43
|
+
'.skh-row-name{font-weight:600;font-size:13px;word-break:break-all}',
|
|
44
|
+
'.skh-badge{flex:none;font-size:11px;border-radius:6px;padding:1px 6px;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}',
|
|
45
|
+
'.skh-badge-on{border-color:var(--dsw-alias-accent, #5b8cff);color:var(--dsw-alias-accent, #5b8cff)}',
|
|
46
|
+
'.skh-row-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}',
|
|
47
|
+
'.skh-row-rel{font-size:11px;color:var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary));overflow-wrap:anywhere}',
|
|
48
|
+
'.skh-note{font-size:12px;color:var(--dsw-alias-label-tertiary);border:1px dashed var(--dsw-alias-border-l1);border-radius:8px;padding:8px}',
|
|
49
|
+
].join('\n')
|
|
50
|
+
;(function installCss() {
|
|
51
|
+
if (typeof document === 'undefined') return
|
|
52
|
+
const tagId = 'dsh-skills-hub/SkillsHub.css'
|
|
53
|
+
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
|
|
54
|
+
const tag = document.createElement('style')
|
|
55
|
+
tag.dataset.plugin = 'dsh-skills-hub'
|
|
56
|
+
tag.dataset.pluginCss = tagId
|
|
57
|
+
tag.textContent = CSS
|
|
58
|
+
document.head.appendChild(tag)
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
61
|
+
|
|
62
|
+
// ---------- HTTP API(永久版经 webServer 路由,替代动态版 host.call) ----------
|
|
63
|
+
function api(path, body) {
|
|
64
|
+
return fetch('/skills-hub' + path, {
|
|
65
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
66
|
+
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
67
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
68
|
+
}).then((response) => {
|
|
69
|
+
if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
70
|
+
return response.json()
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------- 跨插槽共享 store ----------
|
|
75
|
+
const store = {
|
|
76
|
+
view: null,
|
|
77
|
+
loadError: null,
|
|
78
|
+
open: false,
|
|
79
|
+
listeners: [],
|
|
80
|
+
notify() { for (let i = 0; i < this.listeners.length; i++) { try { this.listeners[i]() } catch (_) {} } },
|
|
81
|
+
set(patch) { for (const key in patch) this[key] = patch[key]; this.notify() },
|
|
82
|
+
subscribe(listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter((l) => l !== listener) } },
|
|
83
|
+
}
|
|
84
|
+
function refresh() {
|
|
85
|
+
api('/state').then((view) => { store.set({ view, loadError: null }) }, (error) => { store.set({ loadError: String(error) }) })
|
|
86
|
+
}
|
|
87
|
+
function applySelection(names) {
|
|
88
|
+
api('/set-selected', { names }).then(refresh, refresh)
|
|
89
|
+
}
|
|
90
|
+
function useStore() {
|
|
91
|
+
const [tick, setTick] = react.useState(0)
|
|
92
|
+
react.useEffect(() => store.subscribe(() => setTick((t) => t + 1)), [])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------- 1. 输入框工具行「技能」按钮(模式选择器之后) ----------
|
|
96
|
+
function SkillsTrigger() {
|
|
97
|
+
useStore()
|
|
98
|
+
const view = store.view
|
|
99
|
+
const count = view === null ? 0 : view.selected.length
|
|
100
|
+
const on = count > 0
|
|
101
|
+
return react.createElement('div', { className: 'skh-trigger-wrap' },
|
|
102
|
+
react.createElement('button', {
|
|
103
|
+
type: 'button',
|
|
104
|
+
className: 'skh-trigger' + (on ? ' skh-trigger-on' : ''),
|
|
105
|
+
title: on ? '已启用技能: ' + view.selected.join(', ') : '选择会话技能(勾选即启用)',
|
|
106
|
+
onClick: () => store.set({ open: !store.open }),
|
|
107
|
+
},
|
|
108
|
+
react.createElement('span', { className: 'skh-trigger-icon', 'aria-hidden': 'true' }, '\uD83E\uDDE9'),
|
|
109
|
+
react.createElement('span', { className: 'skh-trigger-text' }, '技能'),
|
|
110
|
+
on ? react.createElement('span', { className: 'skh-trigger-count' }, String(count)) : null))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------- 2. 会话技能勾选浮层 ----------
|
|
114
|
+
function SkillsPopup() {
|
|
115
|
+
useStore()
|
|
116
|
+
if (!store.open) return null
|
|
117
|
+
const view = store.view
|
|
118
|
+
const skills = view === null ? [] : view.skills
|
|
119
|
+
const selectedNames = view === null ? [] : view.selected
|
|
120
|
+
const selectedSet = {}
|
|
121
|
+
for (let i = 0; i < selectedNames.length; i++) selectedSet[selectedNames[i]] = true
|
|
122
|
+
const toggle = (name) => {
|
|
123
|
+
const next = []
|
|
124
|
+
for (let i = 0; i < selectedNames.length; i++) if (selectedNames[i] !== name) next.push(selectedNames[i])
|
|
125
|
+
if (selectedSet[name] !== true) next.push(name)
|
|
126
|
+
applySelection(next)
|
|
127
|
+
}
|
|
128
|
+
const emptyText = view === null || view.folder === ''
|
|
129
|
+
? '尚未配置技能文件夹:请到 设置 → 技能管理 中设置'
|
|
130
|
+
: (view.scanError !== '' ? view.scanError : '技能文件夹中没有 .md 文件')
|
|
131
|
+
return react.createElement('div', null,
|
|
132
|
+
react.createElement('div', { className: 'skh-pop-backdrop', onClick: () => store.set({ open: false }) }),
|
|
133
|
+
react.createElement('div', { className: 'skh-pop-card', role: 'dialog', 'aria-label': '会话技能选择' },
|
|
134
|
+
react.createElement('div', { className: 'skh-pop-title' }, '会话技能(勾选即启用,未勾选不启用)'),
|
|
135
|
+
skills.length === 0 ? react.createElement('div', { className: 'skh-pop-empty' }, emptyText) : null,
|
|
136
|
+
react.createElement('div', { className: 'skh-pop-list' },
|
|
137
|
+
skills.map((s) => react.createElement('label', { key: s.name, className: 'skh-pop-row' },
|
|
138
|
+
react.createElement('input', { type: 'checkbox', className: 'skh-pop-check', checked: selectedSet[s.name] === true, onChange: () => toggle(s.name) }),
|
|
139
|
+
react.createElement('div', { className: 'skh-pop-meta' },
|
|
140
|
+
react.createElement('div', { className: 'skh-pop-name' }, s.name),
|
|
141
|
+
react.createElement('div', { className: 'skh-pop-desc' }, s.description === null || s.description === '' ? s.rel : s.description))))),
|
|
142
|
+
react.createElement('div', { className: 'skh-pop-foot' },
|
|
143
|
+
react.createElement('span', { className: 'skh-pop-folder', title: view === null ? '' : view.folder }, view === null ? '' : view.folder),
|
|
144
|
+
react.createElement('button', { type: 'button', className: 'skh-pop-done', onClick: () => store.set({ open: false }) }, '完成'))))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------- 3. 设置页「技能管理」(无启用/停用开关) ----------
|
|
148
|
+
function SkillsAdmin() {
|
|
149
|
+
useStore()
|
|
150
|
+
const view = store.view
|
|
151
|
+
const [folderDraft, setFolderDraft] = react.useState(null)
|
|
152
|
+
const [draftTouched, setDraftTouched] = react.useState(false)
|
|
153
|
+
if (folderDraft === null && view !== null && !draftTouched) setFolderDraft(view.folder)
|
|
154
|
+
const skills = view === null ? [] : view.skills
|
|
155
|
+
const others = view === null ? [] : view.otherSkills
|
|
156
|
+
const selectedSet = {}
|
|
157
|
+
if (view !== null) for (let i = 0; i < view.selected.length; i++) selectedSet[view.selected[i]] = true
|
|
158
|
+
let statusText = ''
|
|
159
|
+
let statusErr = false
|
|
160
|
+
if (store.loadError !== null) { statusText = '状态读取失败: ' + store.loadError; statusErr = true }
|
|
161
|
+
else if (view === null) statusText = '正在读取状态…'
|
|
162
|
+
else if (view.scanning) statusText = '正在扫描…'
|
|
163
|
+
else if (!view.scanned) statusText = '尚未扫描'
|
|
164
|
+
else if (view.scanError !== '') { statusText = view.scanError; statusErr = true }
|
|
165
|
+
else statusText = '已扫描 ' + skills.length + ' 个技能' + (view.scanTime === null || view.scanTime === '' ? '' : ' · ' + view.scanTime)
|
|
166
|
+
const applyFolder = () => {
|
|
167
|
+
api('/set-folder', { path: folderDraft === null ? '' : folderDraft }).then(refresh, refresh)
|
|
168
|
+
}
|
|
169
|
+
const reload = () => { api('/reload').then(refresh, refresh) }
|
|
170
|
+
return react.createElement('div', { className: 'skh-admin' },
|
|
171
|
+
react.createElement('div', { className: 'skh-title' }, '技能管理'),
|
|
172
|
+
react.createElement('div', { className: 'skh-sub' }, '技能文件夹'),
|
|
173
|
+
react.createElement('div', { className: 'skh-field' },
|
|
174
|
+
react.createElement('input', {
|
|
175
|
+
className: 'skh-input',
|
|
176
|
+
value: folderDraft === null ? '' : folderDraft,
|
|
177
|
+
placeholder: '技能文件夹绝对路径(递归扫描所有 .md)',
|
|
178
|
+
onChange: (event) => { setDraftTouched(true); setFolderDraft(event.target.value) },
|
|
179
|
+
}),
|
|
180
|
+
react.createElement('button', { type: 'button', className: 'skh-btn', onClick: applyFolder }, '保存并加载'),
|
|
181
|
+
react.createElement('button', { type: 'button', className: 'skh-btn', onClick: reload }, '重新扫描')),
|
|
182
|
+
react.createElement('div', { className: statusErr ? 'skh-status skh-status-err' : 'skh-status' }, statusText),
|
|
183
|
+
react.createElement('div', { className: 'skh-sub' }, '本文件夹技能(' + skills.length + ')'),
|
|
184
|
+
skills.map((s) => react.createElement('div', { key: s.name, className: 'skh-row' },
|
|
185
|
+
react.createElement('div', { className: 'skh-row-top' },
|
|
186
|
+
react.createElement('span', { className: 'skh-row-name' }, s.name),
|
|
187
|
+
react.createElement('span', { className: selectedSet[s.name] === true ? 'skh-badge skh-badge-on' : 'skh-badge' }, selectedSet[s.name] === true ? '已选择' : '未选择')),
|
|
188
|
+
react.createElement('div', { className: 'skh-row-desc' }, s.description === null || s.description === '' ? '(无描述)' : s.description),
|
|
189
|
+
react.createElement('div', { className: 'skh-row-rel' }, s.rel))),
|
|
190
|
+
react.createElement('div', { className: 'skh-sub' }, '会话中可见的其他技能(' + others.length + ')'),
|
|
191
|
+
others.map((s) => react.createElement('div', { key: s.provider + '/' + s.name, className: 'skh-row' },
|
|
192
|
+
react.createElement('div', { className: 'skh-row-top' },
|
|
193
|
+
react.createElement('span', { className: 'skh-row-name' }, s.name),
|
|
194
|
+
react.createElement('span', { className: 'skh-badge' }, s.provider)),
|
|
195
|
+
react.createElement('div', { className: 'skh-row-desc' }, s.description === null || s.description === '' ? '(无描述)' : s.description))),
|
|
196
|
+
react.createElement('div', { className: 'skh-note' }, '启用/停用请在输入框工具行「模式」选择器旁的「技能」选择器中勾选;本页仅管理与查看,不提供启用/停用开关。'))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const inject = ['slots']
|
|
200
|
+
|
|
201
|
+
function apply(ctx) {
|
|
202
|
+
const slots = ctx.slots
|
|
203
|
+
slots.inject('conversation.input.left', () => slots.register({ name: 'conversation.input.left', id: 'skills-hub-trigger', order: 0 }, SkillsTrigger))
|
|
204
|
+
slots.inject('conversation.input.overlay', () => slots.register({ name: 'conversation.input.overlay', id: 'skills-hub-popup', order: 2 }, SkillsPopup))
|
|
205
|
+
slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'skills-hub', order: 25, label: '技能管理' }, SkillsAdmin))
|
|
206
|
+
refresh()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
exports.apply = apply
|
|
210
|
+
exports.inject = inject
|
|
211
|
+
return module.exports
|
|
212
|
+
},
|
|
213
|
+
})
|