@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,225 @@
|
|
|
1
|
+
return {
|
|
2
|
+
apply(ctx) {
|
|
3
|
+
const slots = ctx.get('slots')
|
|
4
|
+
if (slots === undefined) return
|
|
5
|
+
|
|
6
|
+
const CSS = [
|
|
7
|
+
'.skh-trigger-wrap{display:inline-flex;align-items:center}',
|
|
8
|
+
'.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}',
|
|
9
|
+
'.skh-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
10
|
+
'.skh-trigger-on{border-color:var(--dsw-alias-accent, #5b8cff);color:var(--dsw-alias-label-primary)}',
|
|
11
|
+
'.skh-trigger-icon{font-size:13px;line-height:1}',
|
|
12
|
+
'.skh-trigger-text{font-weight:500}',
|
|
13
|
+
'.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}',
|
|
14
|
+
'.skh-pop-backdrop{position:fixed;inset:0;z-index:90;background:transparent}',
|
|
15
|
+
'.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}',
|
|
16
|
+
'.skh-pop-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);padding:2px 6px 6px}',
|
|
17
|
+
'.skh-pop-list{overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:2px}',
|
|
18
|
+
'.skh-pop-row{display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:8px;cursor:pointer}',
|
|
19
|
+
'.skh-pop-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
20
|
+
'.skh-pop-check{margin:3px 0 0;flex:none;accent-color:var(--dsw-alias-accent, #5b8cff)}',
|
|
21
|
+
'.skh-pop-meta{min-width:0;flex:1}',
|
|
22
|
+
'.skh-pop-name{font-size:13px;font-weight:600;word-break:break-all}',
|
|
23
|
+
'.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}',
|
|
24
|
+
'.skh-pop-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px}',
|
|
25
|
+
'.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}',
|
|
26
|
+
'.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}',
|
|
27
|
+
'.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}',
|
|
28
|
+
'.skh-pop-done:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
29
|
+
'.skh-admin{display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary);font-size:13px}',
|
|
30
|
+
'.skh-title{font-size:14px;font-weight:600}',
|
|
31
|
+
'.skh-sub{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
32
|
+
'.skh-field{display:flex;gap:8px}',
|
|
33
|
+
'.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}',
|
|
34
|
+
'.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}',
|
|
35
|
+
'.skh-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
36
|
+
'.skh-status{font-size:12px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}',
|
|
37
|
+
'.skh-status-err{color:var(--dsw-alias-state-error-primary)}',
|
|
38
|
+
'.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}',
|
|
39
|
+
'.skh-row-top{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}',
|
|
40
|
+
'.skh-row-name{font-weight:600;font-size:13px;word-break:break-all}',
|
|
41
|
+
'.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)}',
|
|
42
|
+
'.skh-badge-on{border-color:var(--dsw-alias-accent, #5b8cff);color:var(--dsw-alias-accent, #5b8cff)}',
|
|
43
|
+
'.skh-row-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}',
|
|
44
|
+
'.skh-row-rel{font-size:11px;color:var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary));overflow-wrap:anywhere}',
|
|
45
|
+
'.skh-note{font-size:12px;color:var(--dsw-alias-label-tertiary);border:1px dashed var(--dsw-alias-border-l1);border-radius:8px;padding:8px}',
|
|
46
|
+
'.skh-panel{display:flex;flex-direction:column;gap:8px;color:var(--dsw-alias-label-primary);font-size:12px}',
|
|
47
|
+
].join('\n')
|
|
48
|
+
|
|
49
|
+
const store = {
|
|
50
|
+
view: null,
|
|
51
|
+
loadError: null,
|
|
52
|
+
open: false,
|
|
53
|
+
listeners: [],
|
|
54
|
+
notify: function () { for (let i = 0; i < this.listeners.length; i++) { try { this.listeners[i]() } catch (_) {} } },
|
|
55
|
+
set: function (patch) { for (const key in patch) this[key] = patch[key]; this.notify() },
|
|
56
|
+
subscribe: function (listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter((l) => l !== listener) } },
|
|
57
|
+
}
|
|
58
|
+
function refresh() {
|
|
59
|
+
host.call('state').then(function (view) { store.set({ view: view, loadError: null }) }, function (error) { store.set({ loadError: String(error) }) })
|
|
60
|
+
}
|
|
61
|
+
function applySelection(names) {
|
|
62
|
+
host.call('setSelected', { names: names }).then(refresh, refresh)
|
|
63
|
+
}
|
|
64
|
+
function useStore() {
|
|
65
|
+
const [tick, setTick] = React.useState(0)
|
|
66
|
+
React.useEffect(function () { return store.subscribe(function () { setTick(function (t) { return t + 1 }) }) }, [])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function SkillsTrigger() {
|
|
70
|
+
useStore()
|
|
71
|
+
const view = store.view
|
|
72
|
+
const count = view === null ? 0 : view.selected.length
|
|
73
|
+
const on = count > 0
|
|
74
|
+
return React.createElement('div', { className: 'skh-trigger-wrap' },
|
|
75
|
+
React.createElement('button', {
|
|
76
|
+
type: 'button',
|
|
77
|
+
className: 'skh-trigger' + (on ? ' skh-trigger-on' : ''),
|
|
78
|
+
title: on ? '已启用技能: ' + view.selected.join(', ') : '选择会话技能(勾选即启用)',
|
|
79
|
+
onClick: function () { store.set({ open: !store.open }) },
|
|
80
|
+
},
|
|
81
|
+
React.createElement('span', { className: 'skh-trigger-icon', 'aria-hidden': 'true' }, '\uD83E\uDDE9'),
|
|
82
|
+
React.createElement('span', { className: 'skh-trigger-text' }, '技能'),
|
|
83
|
+
on ? React.createElement('span', { className: 'skh-trigger-count' }, String(count)) : null))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function SkillsPopup() {
|
|
87
|
+
useStore()
|
|
88
|
+
if (!store.open) return null
|
|
89
|
+
const view = store.view
|
|
90
|
+
const skills = view === null ? [] : view.skills
|
|
91
|
+
const selectedNames = view === null ? [] : view.selected
|
|
92
|
+
const selectedSet = {}
|
|
93
|
+
for (let i = 0; i < selectedNames.length; i++) selectedSet[selectedNames[i]] = true
|
|
94
|
+
const toggle = function (name) {
|
|
95
|
+
const next = []
|
|
96
|
+
for (let i = 0; i < selectedNames.length; i++) if (selectedNames[i] !== name) next.push(selectedNames[i])
|
|
97
|
+
if (selectedSet[name] !== true) next.push(name)
|
|
98
|
+
applySelection(next)
|
|
99
|
+
}
|
|
100
|
+
const emptyText = view === null || view.folder === ''
|
|
101
|
+
? '尚未配置技能文件夹:请到 设置 → 技能管理 中设置'
|
|
102
|
+
: (view.scanError !== '' ? view.scanError : '技能文件夹中没有 .md 文件')
|
|
103
|
+
return React.createElement('div', null,
|
|
104
|
+
React.createElement('div', { className: 'skh-pop-backdrop', onClick: function () { store.set({ open: false }) } }),
|
|
105
|
+
React.createElement('div', { className: 'skh-pop-card', role: 'dialog', 'aria-label': '会话技能选择' },
|
|
106
|
+
React.createElement('div', { className: 'skh-pop-title' }, '会话技能(勾选即启用,未勾选不启用)'),
|
|
107
|
+
skills.length === 0 ? React.createElement('div', { className: 'skh-pop-empty' }, emptyText) : null,
|
|
108
|
+
React.createElement('div', { className: 'skh-pop-list' },
|
|
109
|
+
skills.map(function (s) {
|
|
110
|
+
return React.createElement('label', { key: s.name, className: 'skh-pop-row' },
|
|
111
|
+
React.createElement('input', { type: 'checkbox', className: 'skh-pop-check', checked: selectedSet[s.name] === true, onChange: function () { toggle(s.name) } }),
|
|
112
|
+
React.createElement('div', { className: 'skh-pop-meta' },
|
|
113
|
+
React.createElement('div', { className: 'skh-pop-name' }, s.name),
|
|
114
|
+
React.createElement('div', { className: 'skh-pop-desc' }, s.description === null || s.description === '' ? s.rel : s.description)))
|
|
115
|
+
})),
|
|
116
|
+
React.createElement('div', { className: 'skh-pop-foot' },
|
|
117
|
+
React.createElement('span', { className: 'skh-pop-folder', title: view === null ? '' : view.folder }, view === null ? '' : view.folder),
|
|
118
|
+
React.createElement('button', { type: 'button', className: 'skh-pop-done', onClick: function () { store.set({ open: false }) } }, '完成'))))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function SkillsAdmin() {
|
|
122
|
+
useStore()
|
|
123
|
+
const view = store.view
|
|
124
|
+
const [folderDraft, setFolderDraft] = React.useState(null)
|
|
125
|
+
const [draftTouched, setDraftTouched] = React.useState(false)
|
|
126
|
+
if (folderDraft === null && view !== null && !draftTouched) setFolderDraft(view.folder)
|
|
127
|
+
const skills = view === null ? [] : view.skills
|
|
128
|
+
const others = view === null ? [] : view.otherSkills
|
|
129
|
+
const selectedSet = {}
|
|
130
|
+
if (view !== null) for (let i = 0; i < view.selected.length; i++) selectedSet[view.selected[i]] = true
|
|
131
|
+
let statusText = ''
|
|
132
|
+
let statusErr = false
|
|
133
|
+
if (store.loadError !== null) { statusText = '状态读取失败: ' + store.loadError; statusErr = true }
|
|
134
|
+
else if (view === null) statusText = '正在读取状态…'
|
|
135
|
+
else if (view.scanning) statusText = '正在扫描…'
|
|
136
|
+
else if (!view.scanned) statusText = '尚未扫描'
|
|
137
|
+
else if (view.scanError !== '') { statusText = view.scanError; statusErr = true }
|
|
138
|
+
else statusText = '已扫描 ' + skills.length + ' 个技能' + (view.scanTime === null || view.scanTime === '' ? '' : ' · ' + view.scanTime)
|
|
139
|
+
const applyFolder = function () {
|
|
140
|
+
host.call('setFolder', { path: folderDraft === null ? '' : folderDraft }).then(refresh, refresh)
|
|
141
|
+
}
|
|
142
|
+
const reload = function () { host.call('reload').then(refresh, refresh) }
|
|
143
|
+
return React.createElement('div', { className: 'skh-admin' },
|
|
144
|
+
React.createElement('div', { className: 'skh-title' }, '技能管理'),
|
|
145
|
+
React.createElement('div', { className: 'skh-sub' }, '技能文件夹'),
|
|
146
|
+
React.createElement('div', { className: 'skh-field' },
|
|
147
|
+
React.createElement('input', {
|
|
148
|
+
className: 'skh-input',
|
|
149
|
+
value: folderDraft === null ? '' : folderDraft,
|
|
150
|
+
placeholder: '技能文件夹绝对路径(递归扫描所有 .md)',
|
|
151
|
+
onChange: function (event) { setDraftTouched(true); setFolderDraft(event.target.value) },
|
|
152
|
+
}),
|
|
153
|
+
React.createElement('button', { type: 'button', className: 'skh-btn', onClick: applyFolder }, '保存并加载'),
|
|
154
|
+
React.createElement('button', { type: 'button', className: 'skh-btn', onClick: reload }, '重新扫描')),
|
|
155
|
+
React.createElement('div', { className: statusErr ? 'skh-status skh-status-err' : 'skh-status' }, statusText),
|
|
156
|
+
React.createElement('div', { className: 'skh-sub' }, '本文件夹技能(' + skills.length + ')'),
|
|
157
|
+
skills.map(function (s) {
|
|
158
|
+
return React.createElement('div', { key: s.name, className: 'skh-row' },
|
|
159
|
+
React.createElement('div', { className: 'skh-row-top' },
|
|
160
|
+
React.createElement('span', { className: 'skh-row-name' }, s.name),
|
|
161
|
+
React.createElement('span', { className: selectedSet[s.name] === true ? 'skh-badge skh-badge-on' : 'skh-badge' }, selectedSet[s.name] === true ? '已选择' : '未选择')),
|
|
162
|
+
React.createElement('div', { className: 'skh-row-desc' }, s.description === null || s.description === '' ? '(无描述)' : s.description),
|
|
163
|
+
React.createElement('div', { className: 'skh-row-rel' }, s.rel))
|
|
164
|
+
}),
|
|
165
|
+
React.createElement('div', { className: 'skh-sub' }, '会话中可见的其他技能(' + others.length + ')'),
|
|
166
|
+
others.map(function (s) {
|
|
167
|
+
return React.createElement('div', { key: s.provider + '/' + s.name, className: 'skh-row' },
|
|
168
|
+
React.createElement('div', { className: 'skh-row-top' },
|
|
169
|
+
React.createElement('span', { className: 'skh-row-name' }, s.name),
|
|
170
|
+
React.createElement('span', { className: 'skh-badge' }, s.provider)),
|
|
171
|
+
React.createElement('div', { className: 'skh-row-desc' }, s.description === null || s.description === '' ? '(无描述)' : s.description))
|
|
172
|
+
}),
|
|
173
|
+
React.createElement('div', { className: 'skh-note' }, '启用/停用请在输入框工具行「模式」选择器旁的「技能」选择器中勾选;本页仅管理与查看,不提供启用/停用开关。'))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function SkillsRunPanel() {
|
|
177
|
+
useStore()
|
|
178
|
+
const view = store.view
|
|
179
|
+
const [folderDraft, setFolderDraft] = React.useState(null)
|
|
180
|
+
const [draftTouched, setDraftTouched] = React.useState(false)
|
|
181
|
+
if (folderDraft === null && view !== null && !draftTouched) setFolderDraft(view.folder)
|
|
182
|
+
const skills = view === null ? [] : view.skills
|
|
183
|
+
const selected = view === null ? [] : view.selected
|
|
184
|
+
let statusText = ''
|
|
185
|
+
let statusErr = false
|
|
186
|
+
if (store.loadError !== null) { statusText = 'RPC 失败: ' + store.loadError; statusErr = true }
|
|
187
|
+
else if (view === null) statusText = '等待 Host 状态…'
|
|
188
|
+
else if (view.scanning) statusText = '正在扫描…'
|
|
189
|
+
else if (view.scanError !== '') { statusText = view.scanError; statusErr = true }
|
|
190
|
+
else statusText = '已扫描 ' + skills.length + ' 个技能,已勾选启用 ' + selected.length + ' 个'
|
|
191
|
+
const applyFolder = function () {
|
|
192
|
+
host.call('setFolder', { path: folderDraft === null ? '' : folderDraft }).then(refresh, refresh)
|
|
193
|
+
}
|
|
194
|
+
return React.createElement('div', { className: 'skh-panel' },
|
|
195
|
+
React.createElement('div', { className: 'skh-title' }, '🧩 技能加载与管理插件 · 已安装'),
|
|
196
|
+
React.createElement('div', { className: 'skh-field' },
|
|
197
|
+
React.createElement('input', {
|
|
198
|
+
className: 'skh-input',
|
|
199
|
+
value: folderDraft === null ? '' : folderDraft,
|
|
200
|
+
placeholder: '技能文件夹路径',
|
|
201
|
+
onChange: function (event) { setDraftTouched(true); setFolderDraft(event.target.value) },
|
|
202
|
+
}),
|
|
203
|
+
React.createElement('button', { type: 'button', className: 'skh-btn', onClick: applyFolder }, '加载'),
|
|
204
|
+
React.createElement('button', { type: 'button', className: 'skh-btn', onClick: function () { host.call('reload').then(refresh, refresh) } }, '重新扫描')),
|
|
205
|
+
React.createElement('div', { className: statusErr ? 'skh-status skh-status-err' : 'skh-status' }, statusText),
|
|
206
|
+
selected.length > 0 ? React.createElement('div', { className: 'skh-status' }, '已启用: ' + selected.join(', ')) : null,
|
|
207
|
+
React.createElement('div', { className: 'skh-note' }, '使用方法:输入框工具行的「模式」选择器旁点「技能」,勾选即启用、取消勾选即停用;管理页在 设置 → 技能管理。'))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctx.effect(function () { return styles.insert(CSS) }, 'skills-hub css')
|
|
211
|
+
slots.inject('conversation.input.left', function () {
|
|
212
|
+
return slots.register({ name: 'conversation.input.left', id: 'skills-hub-trigger', order: 0 }, SkillsTrigger)
|
|
213
|
+
})
|
|
214
|
+
slots.inject('conversation.input.overlay', function () {
|
|
215
|
+
return slots.register({ name: 'conversation.input.overlay', id: 'skills-hub-popup', order: 2 }, SkillsPopup)
|
|
216
|
+
})
|
|
217
|
+
slots.inject('settings.section', function () {
|
|
218
|
+
return slots.register({ name: 'settings.section', id: 'skills-hub', order: 25, label: '技能管理' }, SkillsAdmin)
|
|
219
|
+
})
|
|
220
|
+
slots.inject('tool.view.cordis', function () {
|
|
221
|
+
return slots.register({ name: 'tool.view.cordis', key: 'self' }, SkillsRunPanel)
|
|
222
|
+
})
|
|
223
|
+
refresh()
|
|
224
|
+
},
|
|
225
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
}
|