@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/README.md
ADDED
|
File without changes
|
package/package.json
CHANGED
package/plugin/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @zhangchi835/skill-manager
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness(DSH)技能加载与管理插件:从任意文件夹**无命名规则**地递归加载技能(中文文件名、无 frontmatter、任意子目录均可),在输入框「模式」选择器旁提供**勾选即启用**的会话技能选择器,并附带「技能管理」设置页(无启用/停用开关)。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **递归加载**:配置一个文件夹,所有 `.md` 文件都被识别为技能;不要求 `SKILL.md`、kebab-case 或 YAML frontmatter(名称自动清洗,中文名回退为哈希名)。
|
|
8
|
+
- **选择即启用**:勾选哪个技能就只启用哪个——未勾选的技能不出现在模型 `<available_skills>` 目录与 `/技能` 菜单中。
|
|
9
|
+
- **技能管理页**:设置 → 技能管理,可配置文件夹、重新扫描、查看全部技能(**无启用/停用开关**,启用只通过输入框旁的勾选器)。
|
|
10
|
+
- **持久化**:文件夹与勾选状态保存在 `~/.dsh/skill-manager.json`(首次运行自动迁移旧版 `~/.dsh/skills-hub.json`),重启自动恢复。
|
|
11
|
+
|
|
12
|
+
## 安装(一条命令)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
dsh plugin --profile web add @zhangchi835/skill-manager
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
该命令会:① 把包安装进 `~/.dsh/profiles/web` 的 dependencies;② 因本包声明了 `dsh.bundle.patch`,自动把包名 reconcile 进 `dsh.profile.bundles`,插件行随之进入组合。重启 DSH 进程后生效。
|
|
19
|
+
|
|
20
|
+
> 手动安装(不使用 dsh plugin CLI 时):`cd ~/.dsh/profiles/web && pnpm add @zhangchi835/skill-manager`,然后把下行写进 `~/.dsh/profiles/web/cordis.patch.yml`(用户补丁层):
|
|
21
|
+
>
|
|
22
|
+
> ```yaml
|
|
23
|
+
> - insert:
|
|
24
|
+
> - id: skill-manager
|
|
25
|
+
> name: '@zhangchi835/skill-manager'
|
|
26
|
+
> inject: [skills, webServer]
|
|
27
|
+
> ```
|
|
28
|
+
|
|
29
|
+
## 验证
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl http://127.0.0.1:<端口>/skill-manager/state # 返回 JSON 即成功
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 使用
|
|
36
|
+
|
|
37
|
+
1. 设置 → 技能管理:填写技能文件夹路径(默认 `~/.dsh/skills`),保存并加载。
|
|
38
|
+
2. 任一会话输入框工具行、「模式」选择器旁,点「🧩 技能」勾选要启用的技能。
|
|
39
|
+
3. 勾选后下一回合模型即可通过 `skill` 工具加载对应技能。
|
|
40
|
+
|
|
41
|
+
## 发布(维护者)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm login # 首次
|
|
45
|
+
npm publish --access public # 在包目录执行
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- 依赖只使用 Node 内置模块,profile 解析零负担;
|
|
49
|
+
- `lib/index.js` 与 `cordis.patch.yml` 中的 `inject: [skills, webServer]` 是**必须**的(修复启动并发竞态导致的行空挂载),发布前请勿移除。
|
|
50
|
+
|
|
51
|
+
## 本地开发
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd ~/.dsh/profiles/web
|
|
55
|
+
pnpm add "link:<本包目录绝对路径>" # link: 软链,改源码即时生效(file: 会复制、不同步)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 结构
|
|
59
|
+
|
|
60
|
+
- `lib/index.js`:Host 插件(skills 提供者 + `/skill-manager/*` JSON 路由)
|
|
61
|
+
- `lib/client.js`:Web 客户端插件(模式选择器旁勾选器 + 技能管理页)
|
|
62
|
+
- `cordis.patch.yml`:bundle 组合补丁(`dsh plugin add` 依赖它自动归入 bundles)
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/plugin/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zhangchi835/skill-manager bundle layer
|
|
2
|
+
# `dsh plugin --profile web add @zhangchi835/skill-manager` 会自动 reconcile
|
|
3
|
+
# 本包进 dsh.profile.bundles 并应用此层。
|
|
4
|
+
# inject 行级硬依赖修复启动并发竞态(行先于服务 apply 导致静默空挂载)。
|
|
2
5
|
- insert:
|
|
3
|
-
- id:
|
|
4
|
-
name: '
|
|
6
|
+
- id: skill-manager
|
|
7
|
+
name: '@zhangchi835/skill-manager'
|
|
8
|
+
inject:
|
|
9
|
+
- skills
|
|
10
|
+
- webServer
|
package/plugin/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({
|
|
2
|
-
id: '
|
|
2
|
+
id: '@zhangchi835/skill-manager',
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} }
|
|
5
5
|
var exports = module.exports
|
|
@@ -49,10 +49,10 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
].join('\n')
|
|
50
50
|
;(function installCss() {
|
|
51
51
|
if (typeof document === 'undefined') return
|
|
52
|
-
const tagId = '
|
|
52
|
+
const tagId = 'skill-manager/SkillsHub.css'
|
|
53
53
|
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
|
|
54
54
|
const tag = document.createElement('style')
|
|
55
|
-
tag.dataset.plugin = '
|
|
55
|
+
tag.dataset.plugin = '@zhangchi835/skill-manager'
|
|
56
56
|
tag.dataset.pluginCss = tagId
|
|
57
57
|
tag.textContent = CSS
|
|
58
58
|
document.head.appendChild(tag)
|
|
@@ -61,7 +61,7 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
|
|
62
62
|
// ---------- HTTP API(永久版经 webServer 路由,替代动态版 host.call) ----------
|
|
63
63
|
function api(path, body) {
|
|
64
|
-
return fetch('/
|
|
64
|
+
return fetch('/skill-manager' + path, {
|
|
65
65
|
method: body === undefined ? 'GET' : 'POST',
|
|
66
66
|
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
67
67
|
body: body === undefined ? undefined : JSON.stringify(body),
|
package/plugin/lib/index.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @zhangchi835/skill-manager — permanent host plugin.
|
|
3
3
|
*
|
|
4
4
|
* - Scans a configured folder recursively; EVERY .md file is a skill
|
|
5
5
|
* (no SKILL.md / kebab-case / frontmatter naming rules).
|
|
6
|
-
* - Registers a `
|
|
7
|
-
* skills the user selected in the composer picker (select-to-enable).
|
|
8
|
-
* - Persists { folder, selected } in ~/.dsh/
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* POST /
|
|
13
|
-
* POST /
|
|
6
|
+
* - Registers a `skill-manager` provider on ctx.skills that exposes EXACTLY
|
|
7
|
+
* the skills the user selected in the composer picker (select-to-enable).
|
|
8
|
+
* - Persists { folder, selected } in ~/.dsh/skill-manager.json across
|
|
9
|
+
* restarts (migrates a legacy ~/.dsh/skills-hub.json on first run).
|
|
10
|
+
* - Serves JSON endpoints under /skill-manager for the web client:
|
|
11
|
+
* GET /skill-manager/state
|
|
12
|
+
* POST /skill-manager/set-folder { path }
|
|
13
|
+
* POST /skill-manager/set-selected { names: string[] }
|
|
14
|
+
* POST /skill-manager/reload
|
|
14
15
|
*/
|
|
15
16
|
import { readdir, readFile, stat, mkdir, writeFile } from 'node:fs/promises'
|
|
16
17
|
import { join, resolve, dirname } from 'node:path'
|
|
17
18
|
import { homedir } from 'node:os'
|
|
18
19
|
|
|
19
|
-
const NAME = '
|
|
20
|
+
const NAME = 'skill-manager'
|
|
20
21
|
const DEFAULT_FOLDER = join(homedir(), '.dsh', 'skills')
|
|
21
|
-
const CONFIG_PATH = join(homedir(), '.dsh', '
|
|
22
|
+
const CONFIG_PATH = join(homedir(), '.dsh', 'skill-manager.json')
|
|
23
|
+
const LEGACY_CONFIG_PATH = join(homedir(), '.dsh', 'skills-hub.json')
|
|
22
24
|
|
|
23
25
|
export const inject = ['skills', 'webServer']
|
|
24
26
|
|
|
@@ -43,12 +45,25 @@ export function apply(ctx) {
|
|
|
43
45
|
let invalidate = function () {}
|
|
44
46
|
|
|
45
47
|
async function loadConfig() {
|
|
48
|
+
let raw = null
|
|
46
49
|
try {
|
|
47
|
-
|
|
48
|
-
if (typeof parsed.folder === 'string' && parsed.folder.trim() !== '') config.folder = parsed.folder.trim()
|
|
49
|
-
if (Array.isArray(parsed.selected)) config.selected = parsed.selected.filter((name) => typeof name === 'string')
|
|
50
|
+
raw = await readFile(CONFIG_PATH, 'utf8')
|
|
50
51
|
} catch (_) {
|
|
51
|
-
//
|
|
52
|
+
// 新配置不存在:尝试迁移旧版 skills-hub.json
|
|
53
|
+
try {
|
|
54
|
+
raw = await readFile(LEGACY_CONFIG_PATH, 'utf8')
|
|
55
|
+
} catch (_) {
|
|
56
|
+
raw = null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (raw !== null) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw)
|
|
62
|
+
if (typeof parsed.folder === 'string' && parsed.folder.trim() !== '') config.folder = parsed.folder.trim()
|
|
63
|
+
if (Array.isArray(parsed.selected)) config.selected = parsed.selected.filter((name) => typeof name === 'string')
|
|
64
|
+
} catch (_) {
|
|
65
|
+
// 配置损坏:使用默认值
|
|
66
|
+
}
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
async function persistConfig() {
|
|
@@ -171,7 +186,7 @@ export function apply(ctx) {
|
|
|
171
186
|
const list = await skills.list({})
|
|
172
187
|
const out = []
|
|
173
188
|
for (const s of list) {
|
|
174
|
-
if (s === null || typeof s !== 'object' || s.provider === '
|
|
189
|
+
if (s === null || typeof s !== 'object' || s.provider === 'skill-manager') continue
|
|
175
190
|
out.push({
|
|
176
191
|
name: typeof s.name === 'string' ? s.name : '',
|
|
177
192
|
description: typeof s.description === 'string' ? s.description : '',
|
|
@@ -251,7 +266,7 @@ export function apply(ctx) {
|
|
|
251
266
|
try { control.invalidate() } catch (_) {}
|
|
252
267
|
}
|
|
253
268
|
return {
|
|
254
|
-
name: '
|
|
269
|
+
name: 'skill-manager',
|
|
255
270
|
async list(options) {
|
|
256
271
|
const signal = options === undefined ? undefined : options.signal
|
|
257
272
|
const selectedSet = Object.create(null)
|
|
@@ -265,7 +280,7 @@ export function apply(ctx) {
|
|
|
265
280
|
description: s.description,
|
|
266
281
|
invocation: { modelInvocable: true, userInvocable: true },
|
|
267
282
|
source: 'custom',
|
|
268
|
-
provider: '
|
|
283
|
+
provider: 'skill-manager',
|
|
269
284
|
rank: 350,
|
|
270
285
|
locator: { path: s.path, rel: s.rel },
|
|
271
286
|
resourceBase: { kind: 'directory', path: config.folder },
|
|
@@ -296,7 +311,7 @@ export function apply(ctx) {
|
|
|
296
311
|
description: parsed.description,
|
|
297
312
|
invocation: { modelInvocable: true, userInvocable: true },
|
|
298
313
|
source: 'custom',
|
|
299
|
-
provider: '
|
|
314
|
+
provider: 'skill-manager',
|
|
300
315
|
resourceBase: { kind: 'directory', path: config.folder },
|
|
301
316
|
path,
|
|
302
317
|
content: parsed.content,
|
|
@@ -337,81 +352,79 @@ export function apply(ctx) {
|
|
|
337
352
|
}
|
|
338
353
|
|
|
339
354
|
// ---------- Web 路由 ----------
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (size <= 1024 * 1024) chunks.push(chunk)
|
|
356
|
-
})
|
|
357
|
-
req.on('end', () => {
|
|
358
|
-
const raw = Buffer.concat(chunks).toString('utf8')
|
|
359
|
-
try {
|
|
360
|
-
resolveBody(raw === '' ? {} : JSON.parse(raw))
|
|
361
|
-
} catch (_) {
|
|
362
|
-
resolveBody({})
|
|
363
|
-
}
|
|
364
|
-
})
|
|
365
|
-
req.on('error', () => resolveBody({}))
|
|
355
|
+
const sendJson = (res, code, value) => {
|
|
356
|
+
const body = JSON.stringify(value)
|
|
357
|
+
res.writeHead(code, {
|
|
358
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
359
|
+
'Content-Length': Buffer.byteLength(body),
|
|
360
|
+
'Cache-Control': 'no-store',
|
|
361
|
+
})
|
|
362
|
+
res.end(body)
|
|
363
|
+
}
|
|
364
|
+
const readJsonBody = (req) => new Promise((resolveBody) => {
|
|
365
|
+
const chunks = []
|
|
366
|
+
let size = 0
|
|
367
|
+
req.on('data', (chunk) => {
|
|
368
|
+
size += chunk.length
|
|
369
|
+
if (size <= 1024 * 1024) chunks.push(chunk)
|
|
366
370
|
})
|
|
371
|
+
req.on('end', () => {
|
|
372
|
+
const raw = Buffer.concat(chunks).toString('utf8')
|
|
373
|
+
try {
|
|
374
|
+
resolveBody(raw === '' ? {} : JSON.parse(raw))
|
|
375
|
+
} catch (_) {
|
|
376
|
+
resolveBody({})
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
req.on('error', () => resolveBody({}))
|
|
380
|
+
})
|
|
367
381
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
await persistConfig()
|
|
383
|
-
}
|
|
384
|
-
await scheduleScan()
|
|
385
|
-
return sendJson(res, 200, await buildView())
|
|
386
|
-
}
|
|
387
|
-
if (req.method === 'POST' && pathname === '/skills-hub/set-selected') {
|
|
388
|
-
const body = await readJsonBody(req)
|
|
389
|
-
const names = Array.isArray(body.names) ? body.names.filter((name) => typeof name === 'string') : []
|
|
390
|
-
const known = Object.create(null)
|
|
391
|
-
for (const s of state.skills) known[s.name] = true
|
|
392
|
-
const next = []
|
|
393
|
-
for (const name of names) {
|
|
394
|
-
if (known[name] === true && !next.includes(name)) next.push(name)
|
|
395
|
-
}
|
|
396
|
-
config.selected = next
|
|
397
|
-
try { invalidate() } catch (_) {}
|
|
382
|
+
ctx.effect(() => webServer.register({
|
|
383
|
+
kind: 'prefix',
|
|
384
|
+
path: '/skill-manager',
|
|
385
|
+
handler: async (req, res) => {
|
|
386
|
+
try {
|
|
387
|
+
const pathname = new URL(req.url, 'http://localhost').pathname.replace(/\/+$/, '')
|
|
388
|
+
if (req.method === 'GET' && pathname === '/skill-manager/state') {
|
|
389
|
+
return sendJson(res, 200, await buildView())
|
|
390
|
+
}
|
|
391
|
+
if (req.method === 'POST' && pathname === '/skill-manager/set-folder') {
|
|
392
|
+
const body = await readJsonBody(req)
|
|
393
|
+
const folder = typeof body.path === 'string' ? body.path.trim() : ''
|
|
394
|
+
if (folder !== '') {
|
|
395
|
+
config.folder = folder
|
|
398
396
|
await persistConfig()
|
|
399
|
-
return sendJson(res, 200, await buildView())
|
|
400
397
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
await scheduleScan()
|
|
399
|
+
return sendJson(res, 200, await buildView())
|
|
400
|
+
}
|
|
401
|
+
if (req.method === 'POST' && pathname === '/skill-manager/set-selected') {
|
|
402
|
+
const body = await readJsonBody(req)
|
|
403
|
+
const names = Array.isArray(body.names) ? body.names.filter((name) => typeof name === 'string') : []
|
|
404
|
+
const known = Object.create(null)
|
|
405
|
+
for (const s of state.skills) known[s.name] = true
|
|
406
|
+
const next = []
|
|
407
|
+
for (const name of names) {
|
|
408
|
+
if (known[name] === true && !next.includes(name)) next.push(name)
|
|
404
409
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
sendJson(res, 500, { error: String(error) })
|
|
410
|
-
} catch (_) {}
|
|
410
|
+
config.selected = next
|
|
411
|
+
try { invalidate() } catch (_) {}
|
|
412
|
+
await persistConfig()
|
|
413
|
+
return sendJson(res, 200, await buildView())
|
|
411
414
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
+
if (req.method === 'POST' && pathname === '/skill-manager/reload') {
|
|
416
|
+
await scheduleScan()
|
|
417
|
+
return sendJson(res, 200, await buildView())
|
|
418
|
+
}
|
|
419
|
+
sendJson(res, 404, { error: 'not found' })
|
|
420
|
+
} catch (error) {
|
|
421
|
+
ctx.logger.warn(`[${NAME}] route error: ${String(error)}`)
|
|
422
|
+
try {
|
|
423
|
+
sendJson(res, 500, { error: String(error) })
|
|
424
|
+
} catch (_) {}
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
}), `${NAME} web route`)
|
|
415
428
|
|
|
416
429
|
// ---------- 启动 ----------
|
|
417
430
|
void (async () => {
|
package/plugin/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "@zhangchi835/skill-manager",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "DeepSeek Harness skill manager plugin: no-naming-rules recursive folder scanning, select-to-enable composer skill picker, and a skills admin page without enable/disable toggles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -18,9 +18,20 @@
|
|
|
18
18
|
"platform": "web"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
21
24
|
"files": [
|
|
22
25
|
"lib",
|
|
23
26
|
"cordis.patch.yml"
|
|
24
27
|
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"dsh",
|
|
30
|
+
"dsh-plugin",
|
|
31
|
+
"deepseek-harness",
|
|
32
|
+
"skill",
|
|
33
|
+
"skills",
|
|
34
|
+
"skill-manager"
|
|
35
|
+
],
|
|
25
36
|
"license": "MIT"
|
|
26
37
|
}
|
package/_verify/client-body.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
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
|
-
}
|