@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 ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangchi835/skill-manager",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "zhangchi",
@@ -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
@@ -1,4 +1,10 @@
1
- # dsh-skills-hub bundle layer
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: skills-hub
4
- name: 'dsh-skills-hub'
6
+ - id: skill-manager
7
+ name: '@zhangchi835/skill-manager'
8
+ inject:
9
+ - skills
10
+ - webServer
@@ -1,5 +1,5 @@
1
1
  window.__ModuleLoader__.load({
2
- id: 'dsh-skills-hub',
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 = 'dsh-skills-hub/SkillsHub.css'
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 = 'dsh-skills-hub'
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('/skills-hub' + path, {
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),
@@ -1,24 +1,26 @@
1
1
  /**
2
- * dsh-skills-hub — permanent host plugin.
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 `skills-hub` provider on ctx.skills that exposes EXACTLY the
7
- * skills the user selected in the composer picker (select-to-enable).
8
- * - Persists { folder, selected } in ~/.dsh/skills-hub.json across restarts.
9
- * - Serves JSON endpoints under /skills-hub for the web client:
10
- * GET /skills-hub/state
11
- * POST /skills-hub/set-folder { path }
12
- * POST /skills-hub/set-selected { names: string[] }
13
- * POST /skills-hub/reload
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 = 'dsh-skills-hub'
20
+ const NAME = 'skill-manager'
20
21
  const DEFAULT_FOLDER = join(homedir(), '.dsh', 'skills')
21
- const CONFIG_PATH = join(homedir(), '.dsh', 'skills-hub.json')
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
- const parsed = JSON.parse(await readFile(CONFIG_PATH, 'utf8'))
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 === 'skills-hub') continue
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: 'skills-hub',
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: 'skills-hub',
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: 'skills-hub',
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
- if (webServer !== undefined) {
341
- const sendJson = (res, code, value) => {
342
- const body = JSON.stringify(value)
343
- res.writeHead(code, {
344
- 'Content-Type': 'application/json; charset=utf-8',
345
- 'Content-Length': Buffer.byteLength(body),
346
- 'Cache-Control': 'no-store',
347
- })
348
- res.end(body)
349
- }
350
- const readJsonBody = (req) => new Promise((resolveBody) => {
351
- const chunks = []
352
- let size = 0
353
- req.on('data', (chunk) => {
354
- size += chunk.length
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
- ctx.effect(() => webServer.register({
369
- kind: 'prefix',
370
- path: '/skills-hub',
371
- handler: async (req, res) => {
372
- try {
373
- const pathname = new URL(req.url, 'http://localhost').pathname.replace(/\/+$/, '')
374
- if (req.method === 'GET' && pathname === '/skills-hub/state') {
375
- return sendJson(res, 200, await buildView())
376
- }
377
- if (req.method === 'POST' && pathname === '/skills-hub/set-folder') {
378
- const body = await readJsonBody(req)
379
- const folder = typeof body.path === 'string' ? body.path.trim() : ''
380
- if (folder !== '') {
381
- config.folder = folder
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
- if (req.method === 'POST' && pathname === '/skills-hub/reload') {
402
- await scheduleScan()
403
- return sendJson(res, 200, await buildView())
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
- sendJson(res, 404, { error: 'not found' })
406
- } catch (error) {
407
- ctx.logger.warn(`[${NAME}] route error: ${String(error)}`)
408
- try {
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
- }), `${NAME} web route`)
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 () => {
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "dsh-skills-hub",
2
+ "name": "@zhangchi835/skill-manager",
3
3
  "version": "1.0.0",
4
- "description": "DSH skills loader & manager bundle: no-naming-rules recursive folder scanning, composer skill picker (select-to-enable), and a skills admin page without enable/disable toggles.",
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
  }
@@ -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
- }