@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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ name: "@zhangchi835/skill-manager",
3
+ version: "1.0.0"
4
+ }
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@zhangchi835/skill-manager",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "MIT",
6
+ "author": "zhangchi",
7
+ "type": "commonjs",
8
+ "main": "index.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ }
12
+ }
@@ -0,0 +1,4 @@
1
+ # dsh-skills-hub bundle layer
2
+ - insert:
3
+ - id: skills-hub
4
+ name: 'dsh-skills-hub'
@@ -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
+ })