|
@@ -7,7 +7,7 @@ import type {
|
|
|
AvailablePluginDto, InstalledPluginDto, LicenseKind, MarketplaceDto, PluginAction, PluginStateDto, SkillInfo,
|
|
AvailablePluginDto, InstalledPluginDto, LicenseKind, MarketplaceDto, PluginAction, PluginStateDto, SkillInfo,
|
|
|
} from '@agent-server/shared'
|
|
} from '@agent-server/shared'
|
|
|
import { badRequest } from '../lib/errors'
|
|
import { badRequest } from '../lib/errors'
|
|
|
-import { listSkills, LOCAL_PLUGIN_FALLBACK } from '../providers/claude/skills'
|
|
|
|
|
|
|
+import { listSkills, LOCAL_PLUGIN_FALLBACK, type PluginRef } from '../providers/claude/skills'
|
|
|
import { runClaudeCli } from '../providers/claude/cli'
|
|
import { runClaudeCli } from '../providers/claude/cli'
|
|
|
import type { AuditService } from './catalog'
|
|
import type { AuditService } from './catalog'
|
|
|
import type { SettingsService } from './settings'
|
|
import type { SettingsService } from './settings'
|
|
@@ -15,7 +15,6 @@ import type { SettingsService } from './settings'
|
|
|
const CACHE_MS = 60_000
|
|
const CACHE_MS = 60_000
|
|
|
const ACTION_TIMEOUT_MS = 300_000
|
|
const ACTION_TIMEOUT_MS = 300_000
|
|
|
|
|
|
|
|
-export interface LoadablePlugin { name: string; path: string; skills: string[]; license: LicenseKind }
|
|
|
|
|
|
|
|
|
|
interface RawInstalled { id: string; version?: string; enabled?: boolean; installPath: string; installedAt?: string; lastUpdated?: string }
|
|
interface RawInstalled { id: string; version?: string; enabled?: boolean; installPath: string; installedAt?: string; lastUpdated?: string }
|
|
|
interface RawAvailable { pluginId: string; name: string; description?: string; marketplaceName: string; version?: string }
|
|
interface RawAvailable { pluginId: string; name: string; description?: string; marketplaceName: string; version?: string }
|
|
@@ -63,10 +62,12 @@ export class PluginService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 运行时加载:已安装且已启用的插件 */
|
|
/** 运行时加载:已安装且已启用的插件 */
|
|
|
- async loadable(): Promise<LoadablePlugin[]> {
|
|
|
|
|
|
|
+ async loadable(): Promise<PluginRef[]> {
|
|
|
const s = await this.state()
|
|
const s = await this.state()
|
|
|
- return s.installed.filter(p => p.enabled && fs.existsSync(p.installPath))
|
|
|
|
|
- .map(p => ({ name: p.name, path: p.installPath, skills: p.skills.map(k => k.id), license: p.license.kind }))
|
|
|
|
|
|
|
+ return s.installed.filter(p => p.enabled && fs.existsSync(p.installPath)).map(p => {
|
|
|
|
|
+ const loadName = manifestName(p.installPath) ?? path.basename(p.installPath)
|
|
|
|
|
+ return { name: loadName, path: p.installPath, skills: p.skills.map(k => ({ id: k.id, loadId: `${loadName}:${k.name}` })) }
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 后台可选的全部技能:本地技能目录 + 已启用插件中的技能 */
|
|
/** 后台可选的全部技能:本地技能目录 + 已启用插件中的技能 */
|
|
@@ -111,7 +112,7 @@ function actionArgs(a: PluginAction): string[] {
|
|
|
function toInstalled(r: RawInstalled): InstalledPluginDto {
|
|
function toInstalled(r: RawInstalled): InstalledPluginDto {
|
|
|
const [name = r.id, marketplace = ''] = r.id.split('@')
|
|
const [name = r.id, marketplace = ''] = r.id.split('@')
|
|
|
const rootLicense = detectLicense(r.installPath)
|
|
const rootLicense = detectLicense(r.installPath)
|
|
|
- const skills = readSkills(path.join(r.installPath, 'skills'), name, 'plugin', rootLicense.kind)
|
|
|
|
|
|
|
+ const skills = readSkills(path.join(r.installPath, 'skills'), name, 'plugin', rootLicense.kind, declaredSkills(r.installPath, name))
|
|
|
// 仓库根目录可能是开源许可,而单个技能是专有许可:任一技能受限即视为受限插件
|
|
// 仓库根目录可能是开源许可,而单个技能是专有许可:任一技能受限即视为受限插件
|
|
|
const restricted = skills.find(k => k.license === 'restricted')
|
|
const restricted = skills.find(k => k.license === 'restricted')
|
|
|
const license = restricted && rootLicense.kind !== 'restricted' ? RESTRICTED : rootLicense
|
|
const license = restricted && rootLicense.kind !== 'restricted' ? RESTRICTED : rootLicense
|
|
@@ -131,14 +132,14 @@ function readLocalSkills(skillsDir: string): SkillInfo[] {
|
|
|
return listSkills(skillsDir).length ? readSkills(path.join(skillsDir, 'skills'), plugin, 'local', 'open') : []
|
|
return listSkills(skillsDir).length ? readSkills(path.join(skillsDir, 'skills'), plugin, 'local', 'open') : []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function readSkills(dir: string, plugin: string, source: SkillInfo['source'], pluginLicense: LicenseKind): SkillInfo[] {
|
|
|
|
|
|
|
+function readSkills(dir: string, plugin: string, source: SkillInfo['source'], pluginLicense: LicenseKind, only: Set<string> | null = null): SkillInfo[] {
|
|
|
let entries: fs.Dirent[]
|
|
let entries: fs.Dirent[]
|
|
|
try {
|
|
try {
|
|
|
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
|
} catch {
|
|
} catch {
|
|
|
return []
|
|
return []
|
|
|
}
|
|
}
|
|
|
- return entries.filter(e => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md'))).map(e => {
|
|
|
|
|
|
|
+ return entries.filter(e => e.isDirectory() && (!only || only.has(e.name)) && fs.existsSync(path.join(dir, e.name, 'SKILL.md'))).map(e => {
|
|
|
const fm = frontmatter(fs.readFileSync(path.join(dir, e.name, 'SKILL.md'), 'utf8'))
|
|
const fm = frontmatter(fs.readFileSync(path.join(dir, e.name, 'SKILL.md'), 'utf8'))
|
|
|
const own = detectLicense(path.join(dir, e.name)).kind
|
|
const own = detectLicense(path.join(dir, e.name)).kind
|
|
|
return {
|
|
return {
|
|
@@ -148,6 +149,29 @@ function readSkills(dir: string, plugin: string, source: SkillInfo['source'], pl
|
|
|
}).sort((a, b) => a.name.localeCompare(b.name))
|
|
}).sort((a, b) => a.name.localeCompare(b.name))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 插件自带 plugin.json 时的名称(按路径加载时 CLI 用它作为技能前缀) */
|
|
|
|
|
+function manifestName(installPath: string): string | null {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return (JSON.parse(fs.readFileSync(path.join(installPath, '.claude-plugin/plugin.json'), 'utf8')) as { name?: string }).name ?? null
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 市场清单中为该插件声明的技能子集。例如官方仓库的 document-skills 与 example-skills 共用同一个目录,
|
|
|
|
|
+ * 分别只声明了部分技能(strict: false)。没有声明时返回 null,表示 skills/ 下全部技能。
|
|
|
|
|
+ */
|
|
|
|
|
+function declaredSkills(installPath: string, plugin: string): Set<string> | null {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const m = JSON.parse(fs.readFileSync(path.join(installPath, '.claude-plugin/marketplace.json'), 'utf8')) as { plugins?: { name: string; skills?: string[] }[] }
|
|
|
|
|
+ const entry = m.plugins?.find(p => p.name === plugin)
|
|
|
|
|
+ return entry?.skills?.length ? new Set(entry.skills.map(s => path.basename(s))) : null
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function frontmatter(md: string): Record<string, string> {
|
|
function frontmatter(md: string): Record<string, string> {
|
|
|
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md)
|
|
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md)
|
|
|
const out: Record<string, string> = {}
|
|
const out: Record<string, string> = {}
|
|
@@ -168,7 +192,7 @@ function detectLicense(dir: string): { kind: LicenseKind; summary: string | null
|
|
|
return { kind: 'unknown', summary: null }
|
|
return { kind: 'unknown', summary: null }
|
|
|
}
|
|
}
|
|
|
if (!text) return { kind: 'unknown', summary: null }
|
|
if (!text) return { kind: 'unknown', summary: null }
|
|
|
- if (/agreement with Anthropic/i.test(text) || /All rights reserved/i.test(text)) {
|
|
|
|
|
|
|
+ if (/agreement\s+with\s+Anthropic/i.test(text) || /All rights reserved/i.test(text)) {
|
|
|
return RESTRICTED
|
|
return RESTRICTED
|
|
|
}
|
|
}
|
|
|
if (/Apache License|MIT License|BSD|Mozilla Public License|ISC License/i.test(text)) return { kind: 'open', summary: '开源许可' }
|
|
if (/Apache License|MIT License|BSD|Mozilla Public License|ISC License/i.test(text)) return { kind: 'open', summary: '开源许可' }
|