mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-24 13:36:47 +08:00
重构 MCP Copilot Skill 分发流程,改为内置打包并支持手动导出
This commit is contained in:
@@ -1339,14 +1339,6 @@ function registerIpcHandlers() {
|
||||
return { success: true, deleted: result.deleted, nextActiveAccountId: result.nextActiveAccountId }
|
||||
})
|
||||
|
||||
ipcMain.handle('skillInstaller:detectTargets', async (_, skillName: string) => {
|
||||
return skillInstallerService.detectTargets(skillName)
|
||||
})
|
||||
|
||||
ipcMain.handle('skillInstaller:installSkill', async (_, skillName: string, selectedSkillsDirs?: string[]) => {
|
||||
return skillInstallerService.installSkill(skillName, selectedSkillsDirs)
|
||||
})
|
||||
|
||||
ipcMain.handle('skillInstaller:exportSkillZip', async (_, skillName: string) => {
|
||||
return skillInstallerService.exportSkillZip(skillName)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import type { AccountProfile } from '../src/types/account'
|
||||
import type { SkillInstallTarget } from './services/skillInstallerService'
|
||||
|
||||
function getMcpLaunchConfigSafe(): Promise<{
|
||||
command: string
|
||||
@@ -47,9 +46,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
},
|
||||
|
||||
skillInstaller: {
|
||||
detectTargets: (skillName: string) => ipcRenderer.invoke('skillInstaller:detectTargets', skillName) as Promise<SkillInstallTarget[]>,
|
||||
installSkill: (skillName: string, selectedSkillsDirs?: string[]) =>
|
||||
ipcRenderer.invoke('skillInstaller:installSkill', skillName, selectedSkillsDirs) as Promise<{ success: boolean; results: SkillInstallTarget[]; error?: string }>,
|
||||
exportSkillZip: (skillName: string) =>
|
||||
ipcRenderer.invoke('skillInstaller:exportSkillZip', skillName) as Promise<{ success: boolean; outputPath?: string; fileName?: string; version?: string; error?: string }>
|
||||
},
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { app } from 'electron'
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { dirname, join } from 'path'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import AdmZip from 'adm-zip'
|
||||
|
||||
export type SupportedAgentKind = 'codex' | 'agents'
|
||||
|
||||
export interface SkillInstallTarget {
|
||||
agentKind: SupportedAgentKind
|
||||
agentLabel: string
|
||||
source: 'known' | 'discovered'
|
||||
skillsDir: string
|
||||
supported: boolean
|
||||
installed: boolean
|
||||
bundledVersion: string
|
||||
installedVersion?: string
|
||||
updateAvailable: boolean
|
||||
installPath?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
type SkillSource = {
|
||||
name: string
|
||||
relativePath: string
|
||||
relativePaths: string[]
|
||||
}
|
||||
|
||||
type SkillMeta = {
|
||||
@@ -34,40 +17,51 @@ type SkillMeta = {
|
||||
const MANAGED_SKILLS: Record<string, SkillSource> = {
|
||||
'ct-mcp-copilot': {
|
||||
name: 'ct-mcp-copilot',
|
||||
relativePath: join('sikll', 'ct-mcp-copilot')
|
||||
relativePaths: [
|
||||
'ct-mcp-copilot',
|
||||
join('skills', 'ct-mcp-copilot'),
|
||||
join('sikll', 'ct-mcp-copilot')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function getHomeDir() {
|
||||
return homedir() || process.env.USERPROFILE || process.env.HOME || ''
|
||||
}
|
||||
|
||||
function getKnownAgentTargets(): Array<{ agentKind: SupportedAgentKind; agentLabel: string; skillsDir: string; source: 'known' }> {
|
||||
const home = getHomeDir()
|
||||
if (!home) return []
|
||||
return [
|
||||
{ agentKind: 'codex', agentLabel: 'Codex', skillsDir: join(home, '.codex', 'skills'), source: 'known' },
|
||||
{ agentKind: 'codex', agentLabel: 'Claude', skillsDir: join(home, '.claude', 'skills'), source: 'known' },
|
||||
{ agentKind: 'agents', agentLabel: '.agents', skillsDir: join(home, '.agents', 'skills'), source: 'known' },
|
||||
{ agentKind: 'agents', agentLabel: 'Cursor', skillsDir: join(home, '.cursor', 'skills'), source: 'known' },
|
||||
{ agentKind: 'agents', agentLabel: 'Kiro', skillsDir: join(home, '.kiro', 'skills'), source: 'known' },
|
||||
{ agentKind: 'agents', agentLabel: 'Trae', skillsDir: join(home, '.trae', 'skills'), source: 'known' },
|
||||
{ agentKind: 'agents', agentLabel: 'Trae-CN', skillsDir: join(home, '.trae-cn', 'skills'), source: 'known' }
|
||||
]
|
||||
}
|
||||
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const aParts = a.split('.').map((x) => Number.parseInt(x, 10) || 0)
|
||||
const bParts = b.split('.').map((x) => Number.parseInt(x, 10) || 0)
|
||||
const maxLen = Math.max(aParts.length, bParts.length)
|
||||
for (let i = 0; i < maxLen; i += 1) {
|
||||
const diff = (aParts[i] || 0) - (bParts[i] || 0)
|
||||
if (diff !== 0) return diff
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export class SkillInstallerService {
|
||||
private listSkillSourceCandidates(skillName: string): string[] {
|
||||
const source = MANAGED_SKILLS[skillName]
|
||||
if (!source) return []
|
||||
|
||||
const roots = [
|
||||
join(process.resourcesPath, 'builtin-skills'),
|
||||
app.getAppPath(),
|
||||
join(process.resourcesPath, 'app.asar'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked'),
|
||||
process.resourcesPath,
|
||||
process.cwd()
|
||||
]
|
||||
|
||||
const seen = new Set<string>()
|
||||
const candidates: string[] = []
|
||||
|
||||
for (const root of roots) {
|
||||
for (const relativePath of source.relativePaths) {
|
||||
const candidate = join(root, relativePath)
|
||||
const normalized = candidate.toLowerCase()
|
||||
if (seen.has(normalized)) continue
|
||||
seen.add(normalized)
|
||||
candidates.push(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
private getSkillSourcePath(skillName: string): string | null {
|
||||
for (const candidate of this.listSkillSourceCandidates(skillName)) {
|
||||
if (existsSync(join(candidate, 'SKILL.md'))) return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private readSkillMeta(skillDir: string): SkillMeta | null {
|
||||
try {
|
||||
const metaPath = join(skillDir, '.skill-meta.json')
|
||||
@@ -78,165 +72,16 @@ export class SkillInstallerService {
|
||||
}
|
||||
}
|
||||
|
||||
private getSkillSourcePath(skillName: string): string | null {
|
||||
const source = MANAGED_SKILLS[skillName]
|
||||
if (!source) return null
|
||||
return join(app.getAppPath(), source.relativePath)
|
||||
}
|
||||
|
||||
private getBundledVersion(skillName: string): string {
|
||||
const sourcePath = this.getSkillSourcePath(skillName)
|
||||
if (!sourcePath) return '0.0.0'
|
||||
return this.readSkillMeta(sourcePath)?.version || '0.0.0'
|
||||
}
|
||||
|
||||
private collectDiscoveredSkillDirs(): Array<{ agentKind: SupportedAgentKind; agentLabel: string; skillsDir: string; source: 'discovered' }> {
|
||||
const home = getHomeDir()
|
||||
if (!home || !existsSync(home)) return []
|
||||
|
||||
const results: Array<{ agentKind: SupportedAgentKind; agentLabel: string; skillsDir: string; source: 'discovered' }> = []
|
||||
const seen = new Set<string>()
|
||||
const projectRoot = app.getAppPath().toLowerCase()
|
||||
|
||||
const addIfMatch = (candidate: string) => {
|
||||
const normalized = candidate.toLowerCase()
|
||||
if (seen.has(normalized)) return
|
||||
if (!existsSync(candidate)) return
|
||||
if (!statSync(candidate).isDirectory()) return
|
||||
if (!normalized.endsWith('\\skills') && !normalized.endsWith('/skills')) return
|
||||
if (normalized.includes(projectRoot)) return
|
||||
|
||||
const parentHint = dirname(candidate).toLowerCase()
|
||||
if (!/(codex|agent|agents|claude|cursor|kiro|trae)/.test(parentHint)) return
|
||||
|
||||
seen.add(normalized)
|
||||
results.push({
|
||||
agentKind: /codex/.test(parentHint) ? 'codex' : 'agents',
|
||||
agentLabel: parentHint.includes('cursor')
|
||||
? '发现的 Cursor Skills'
|
||||
: parentHint.includes('kiro')
|
||||
? '发现的 Kiro Skills'
|
||||
: parentHint.includes('trae')
|
||||
? '发现的 Trae Skills'
|
||||
: parentHint.includes('claude')
|
||||
? '发现的 Claude/Agent Skills'
|
||||
: '发现的 Skills 目录',
|
||||
skillsDir: candidate,
|
||||
source: 'discovered'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
for (const entry of readdirSync(home, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || !entry.name.startsWith('.')) continue
|
||||
const levelOne = join(home, entry.name)
|
||||
addIfMatch(join(levelOne, 'skills'))
|
||||
}
|
||||
} catch {
|
||||
// ignore scan errors
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
detectTargets(skillName: string): SkillInstallTarget[] {
|
||||
const sourcePath = this.getSkillSourcePath(skillName)
|
||||
const hasSource = Boolean(sourcePath && existsSync(join(sourcePath, 'SKILL.md')))
|
||||
const bundledVersion = this.getBundledVersion(skillName)
|
||||
|
||||
const mergedTargets = [...getKnownAgentTargets(), ...this.collectDiscoveredSkillDirs()]
|
||||
.filter((target, index, arr) => arr.findIndex((item) => item.skillsDir.toLowerCase() === target.skillsDir.toLowerCase()) === index)
|
||||
|
||||
return mergedTargets.map(({ agentKind, agentLabel, skillsDir, source }) => {
|
||||
const installPath = join(skillsDir, skillName)
|
||||
const installed = existsSync(join(installPath, 'SKILL.md'))
|
||||
const installedVersion = installed ? (this.readSkillMeta(installPath)?.version || undefined) : undefined
|
||||
|
||||
return {
|
||||
agentKind,
|
||||
agentLabel,
|
||||
source,
|
||||
skillsDir,
|
||||
supported: hasSource && Boolean(getHomeDir()),
|
||||
installed,
|
||||
bundledVersion,
|
||||
installedVersion,
|
||||
updateAvailable: Boolean(installedVersion && compareVersions(installedVersion, bundledVersion) < 0),
|
||||
installPath,
|
||||
error: hasSource ? undefined : `Skill source not found for ${skillName}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
installSkill(skillName: string, selectedSkillsDirs?: string[]): { success: boolean; results: SkillInstallTarget[]; error?: string } {
|
||||
const sourcePath = this.getSkillSourcePath(skillName)
|
||||
if (!sourcePath || !existsSync(join(sourcePath, 'SKILL.md'))) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Skill source not found for ${skillName}`,
|
||||
results: this.detectTargets(skillName)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSet = selectedSkillsDirs?.length
|
||||
? new Set(selectedSkillsDirs.map((item) => item.toLowerCase()))
|
||||
: null
|
||||
|
||||
const results = this.detectTargets(skillName).map((target) => {
|
||||
if (selectedSet && !selectedSet.has(target.skillsDir.toLowerCase())) {
|
||||
return target
|
||||
}
|
||||
|
||||
if (!target.supported || !target.installPath) {
|
||||
return {
|
||||
...target,
|
||||
installed: false,
|
||||
error: target.error || 'Target is not supported on this device'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(target.installPath), { recursive: true })
|
||||
if (existsSync(target.installPath)) {
|
||||
rmSync(target.installPath, { recursive: true, force: true })
|
||||
}
|
||||
mkdirSync(target.skillsDir, { recursive: true })
|
||||
cpSync(sourcePath, target.installPath, { recursive: true, force: true })
|
||||
const installedMeta = this.readSkillMeta(target.installPath)
|
||||
return {
|
||||
...target,
|
||||
installed: true,
|
||||
installedVersion: installedMeta?.version || target.bundledVersion,
|
||||
updateAvailable: false,
|
||||
error: undefined
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
...target,
|
||||
installed: false,
|
||||
error: String(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: results.some((item) => item.installed),
|
||||
results,
|
||||
error: results.every((item) => !item.installed)
|
||||
? results.map((item) => `${item.agentLabel}: ${item.error || 'install failed'}`).join(' | ')
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
exportSkillZip(skillName: string): { success: boolean; outputPath?: string; fileName?: string; version?: string; error?: string } {
|
||||
const sourcePath = this.getSkillSourcePath(skillName)
|
||||
if (!sourcePath || !existsSync(join(sourcePath, 'SKILL.md'))) {
|
||||
return { success: false, error: `Skill source not found for ${skillName}` }
|
||||
if (!sourcePath) {
|
||||
const tried = this.listSkillSourceCandidates(skillName)
|
||||
return { success: false, error: `Skill source not found for ${skillName}. Tried: ${tried.join(' | ')}` }
|
||||
}
|
||||
|
||||
try {
|
||||
const downloadsDir = app.getPath('downloads')
|
||||
const version = this.getBundledVersion(skillName)
|
||||
const version = this.readSkillMeta(sourcePath)?.version || '0.0.0'
|
||||
const fileName = `${skillName}-v${version}.zip`
|
||||
const outputPath = join(downloadsDir, fileName)
|
||||
const zip = new AdmZip()
|
||||
|
||||
@@ -170,6 +170,10 @@
|
||||
{
|
||||
"from": ".tmp/release-announcement.json",
|
||||
"to": "release-announcement.json"
|
||||
},
|
||||
{
|
||||
"from": "sikll/ct-mcp-copilot",
|
||||
"to": "builtin-skills/ct-mcp-copilot"
|
||||
}
|
||||
],
|
||||
"extraFiles": [
|
||||
|
||||
+4
-258
@@ -6,7 +6,6 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Container,
|
||||
Snackbar,
|
||||
@@ -15,9 +14,8 @@ import {
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Check, Copy, Download, RefreshCw, Save, Sparkles } from 'lucide-react'
|
||||
import { Check, Copy, Download, Save } from 'lucide-react'
|
||||
import * as configService from '../services/config'
|
||||
import type { SkillInstallTarget } from '../types/electron'
|
||||
|
||||
type ToastState = {
|
||||
text: string
|
||||
@@ -94,58 +92,6 @@ const secondaryButtonSx = {
|
||||
},
|
||||
}
|
||||
|
||||
const getChipSx = (tone: 'primary' | 'success' | 'warning' | 'neutral' = 'neutral') => {
|
||||
if (tone === 'primary') {
|
||||
return {
|
||||
borderRadius: '999px',
|
||||
border: '1px solid var(--primary)',
|
||||
color: 'var(--primary)',
|
||||
backgroundColor: 'var(--primary-light)',
|
||||
fontWeight: 700,
|
||||
'& .MuiChip-label': {
|
||||
px: 1.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (tone === 'success') {
|
||||
return {
|
||||
borderRadius: '999px',
|
||||
border: '1px solid rgba(76, 175, 80, 0.28)',
|
||||
color: '#4CAF50',
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||
fontWeight: 700,
|
||||
'& .MuiChip-label': {
|
||||
px: 1.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (tone === 'warning') {
|
||||
return {
|
||||
borderRadius: '999px',
|
||||
border: '1px solid rgba(245, 158, 11, 0.28)',
|
||||
color: '#f59e0b',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.12)',
|
||||
fontWeight: 700,
|
||||
'& .MuiChip-label': {
|
||||
px: 1.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
borderRadius: '999px',
|
||||
border: '1px solid var(--border-color)',
|
||||
color: 'var(--text-secondary)',
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
fontWeight: 700,
|
||||
'& .MuiChip-label': {
|
||||
px: 1.1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function McpPage() {
|
||||
const managedSkillName = 'ct-mcp-copilot'
|
||||
const [activeSection, setActiveSection] = useState<McpSection>('mcp')
|
||||
@@ -153,10 +99,6 @@ function McpPage() {
|
||||
const [mcpExposeMediaPaths, setMcpExposeMediaPaths] = useState(true)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [skillTargets, setSkillTargets] = useState<SkillInstallTarget[]>([])
|
||||
const [selectedSkillDirs, setSelectedSkillDirs] = useState<string[]>([])
|
||||
const [detectingSkills, setDetectingSkills] = useState(false)
|
||||
const [installingSkill, setInstallingSkill] = useState(false)
|
||||
const [exportingSkillZip, setExportingSkillZip] = useState(false)
|
||||
const [toast, setToast] = useState<ToastState | null>(null)
|
||||
const [launchConfig, setLaunchConfig] = useState<McpLaunchConfig>({
|
||||
@@ -187,14 +129,6 @@ function McpPage() {
|
||||
console.error('获取 MCP 启动配置失败:', innerError)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const targets = await window.electronAPI.skillInstaller.detectTargets(managedSkillName)
|
||||
setSkillTargets(targets)
|
||||
setSelectedSkillDirs(targets.filter((item) => item.supported && (!item.installed || item.updateAvailable)).map((item) => item.skillsDir))
|
||||
} catch (innerError) {
|
||||
console.error('检测 Skills 安装目标失败:', innerError)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载 MCP 配置失败:', e)
|
||||
setToast({ text: '加载 MCP 配置失败', success: false })
|
||||
@@ -246,44 +180,6 @@ function McpPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const detectSkillTargets = async () => {
|
||||
setDetectingSkills(true)
|
||||
try {
|
||||
const targets = await window.electronAPI.skillInstaller.detectTargets(managedSkillName)
|
||||
setSkillTargets(targets)
|
||||
setSelectedSkillDirs(targets.filter((item) => item.supported && (!item.installed || item.updateAvailable)).map((item) => item.skillsDir))
|
||||
setToast({ text: '已刷新 Skills 安装目标', success: true })
|
||||
} catch (e) {
|
||||
console.error('检测 Skills 安装目标失败:', e)
|
||||
setToast({ text: '检测 Skills 安装目标失败', success: false })
|
||||
} finally {
|
||||
setDetectingSkills(false)
|
||||
}
|
||||
}
|
||||
|
||||
const installManagedSkill = async () => {
|
||||
if (selectedSkillDirs.length === 0) {
|
||||
setToast({ text: '请先勾选要安装的 Agent 目标', success: false })
|
||||
return
|
||||
}
|
||||
setInstallingSkill(true)
|
||||
try {
|
||||
const result = await window.electronAPI.skillInstaller.installSkill(managedSkillName, selectedSkillDirs)
|
||||
setSkillTargets(result.results)
|
||||
setSelectedSkillDirs(result.results.filter((item) => item.supported && (!item.installed || item.updateAvailable)).map((item) => item.skillsDir))
|
||||
if (result.success) {
|
||||
setToast({ text: `${managedSkillName} 已安装到选中的 Agent`, success: true })
|
||||
} else {
|
||||
setToast({ text: result.error || 'Skill 安装失败', success: false })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('安装 Skill 失败:', e)
|
||||
setToast({ text: '安装 Skill 失败', success: false })
|
||||
} finally {
|
||||
setInstallingSkill(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportManagedSkillZip = async () => {
|
||||
setExportingSkillZip(true)
|
||||
try {
|
||||
@@ -301,16 +197,6 @@ function McpPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const bundledSkillVersion = skillTargets[0]?.bundledVersion || '1.0.0'
|
||||
const selectableTargets = skillTargets.filter((item) => item.supported)
|
||||
const allSelectableChecked = selectableTargets.length > 0 && selectableTargets.every((item) => selectedSkillDirs.includes(item.skillsDir))
|
||||
|
||||
const toggleSkillDir = (skillsDir: string, checked: boolean) => {
|
||||
setSelectedSkillDirs((current) => checked
|
||||
? Array.from(new Set([...current, skillsDir]))
|
||||
: current.filter((item) => item !== skillsDir))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', mx: -3, mt: -3, overflowY: 'auto', pb: 3 }}>
|
||||
<Container maxWidth="lg" sx={{ px: { xs: 2, md: 4 }, py: { xs: 3, md: 4 } }}>
|
||||
@@ -573,9 +459,9 @@ function McpPage() {
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.2} justifyContent="space-between" alignItems={{ xs: 'stretch', sm: 'center' }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 600, color: 'var(--text-primary)' }}>内置 Skill 版本</Typography>
|
||||
<Typography sx={{ fontWeight: 600, color: 'var(--text-primary)' }}>导出 Skill</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||
当前内置版本:`{bundledSkillVersion}`。如果本机已安装版本更低,页面会提示可更新。
|
||||
导出 `ct-mcp-copilot` zip 包后,可手动导入到支持 Skills 的 Agent(Codex、Claude、Cursor、Kiro 等)。
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -590,148 +476,8 @@ function McpPage() {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: '18px',
|
||||
border: '1px solid var(--border-color)',
|
||||
bgcolor: 'var(--bg-primary)',
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.2} justifyContent="space-between" alignItems={{ xs: 'stretch', sm: 'center' }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 600, color: 'var(--text-primary)' }}>一键安装到本机 Agent</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||
自动探测 Codex、`.agents` 以及主目录下更多可能的 skills 目录,并把 `ct-mcp-copilot` 复制安装进去。
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshCw size={16} />}
|
||||
onClick={detectSkillTargets}
|
||||
disabled={detectingSkills || installingSkill}
|
||||
sx={secondaryButtonSx}
|
||||
>
|
||||
{detectingSkills ? '检测中...' : '检测目标'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Sparkles size={16} />}
|
||||
onClick={installManagedSkill}
|
||||
disabled={installingSkill}
|
||||
sx={{
|
||||
minWidth: 140,
|
||||
borderRadius: '999px',
|
||||
textTransform: 'none',
|
||||
fontWeight: 700,
|
||||
background: 'var(--primary-gradient)',
|
||||
'&:hover': {
|
||||
background: 'var(--primary-gradient)',
|
||||
filter: 'brightness(0.98)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{installingSkill ? '安装中...' : '安装选中'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 1.5 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setSelectedSkillDirs(selectableTargets.map((item) => item.skillsDir))}
|
||||
disabled={selectableTargets.length === 0}
|
||||
sx={secondaryButtonSx}
|
||||
>
|
||||
全选
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setSelectedSkillDirs([])}
|
||||
disabled={selectedSkillDirs.length === 0}
|
||||
sx={secondaryButtonSx}
|
||||
>
|
||||
清空选择
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={1.2}>
|
||||
{skillTargets.map((target) => (
|
||||
<Box
|
||||
key={`${target.agentKind}-${target.skillsDir}`}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: '18px',
|
||||
border: '1px solid var(--border-color)',
|
||||
bgcolor: 'var(--bg-primary)',
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" useFlexGap flexWrap="wrap">
|
||||
<Checkbox
|
||||
checked={selectedSkillDirs.includes(target.skillsDir)}
|
||||
indeterminate={false}
|
||||
disabled={!target.supported}
|
||||
onChange={(event) => toggleSkillDir(target.skillsDir, event.target.checked)}
|
||||
sx={{
|
||||
color: 'var(--text-tertiary)',
|
||||
'&.Mui-checked': {
|
||||
color: 'var(--primary)',
|
||||
},
|
||||
p: 0.5,
|
||||
mr: 0.5,
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||
{target.agentLabel}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={target.installed ? '已安装' : target.supported ? '可安装' : '不支持'}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={target.installed ? getChipSx('success') : target.supported ? getChipSx('primary') : getChipSx('neutral')}
|
||||
/>
|
||||
{target.updateAvailable && (
|
||||
<Chip
|
||||
label="可更新"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={getChipSx('warning')}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
label={target.source === 'known' ? '内置规则' : '扫描发现'}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={getChipSx('neutral')}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography sx={{ mt: 0.75, fontSize: 13, color: 'var(--text-secondary)', wordBreak: 'break-all' }}>
|
||||
{target.installPath || target.skillsDir}
|
||||
</Typography>
|
||||
<Typography sx={{ mt: 0.5, fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||
已安装版本:{target.installedVersion || '未安装'} / 内置版本:{target.bundledVersion}
|
||||
</Typography>
|
||||
{target.error && (
|
||||
<Typography sx={{ mt: 0.75, fontSize: 12, color: 'var(--danger)' }}>
|
||||
{target.error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
{skillTargets.length === 0 && (
|
||||
<Typography sx={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||
还没有检测到本机 Skill 目标,点击“检测目标”后可查看支持情况。
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Typography sx={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||||
安装完成后,可在支持 Skills 的 Agent 中直接提到 `ct-mcp-copilot` 使用;也可以导出 `zip` 后手动导入。Cherry Studio 等 MCP 宿主仍然继续使用 `mcpServers` 配置,不属于 skills 目录安装模型。
|
||||
导出 zip 后手动解压到对应 Agent 的 skills 目录即可使用。Cherry Studio 等 MCP 宿主继续使用 `mcpServers` 配置,无需安装 Skill。
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
|
||||
Vendored
-15
@@ -2,19 +2,6 @@ import type { ChatSession, Message, Contact, ContactInfo } from './models'
|
||||
import type { SummaryResult } from './ai'
|
||||
import type { AccountProfile } from './account'
|
||||
|
||||
export interface SkillInstallTarget {
|
||||
agentKind: 'codex' | 'agents'
|
||||
agentLabel: string
|
||||
source: 'known' | 'discovered'
|
||||
skillsDir: string
|
||||
supported: boolean
|
||||
installed: boolean
|
||||
bundledVersion: string
|
||||
installedVersion?: string
|
||||
updateAvailable: boolean
|
||||
installPath?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ImageListItem {
|
||||
imagePath: string
|
||||
@@ -81,8 +68,6 @@ export interface ElectronAPI {
|
||||
delete: (accountId: string, deleteLocalData?: boolean) => Promise<{ success: boolean; error?: string; deleted?: AccountProfile | null; nextActiveAccountId?: string }>
|
||||
}
|
||||
skillInstaller: {
|
||||
detectTargets: (skillName: string) => Promise<SkillInstallTarget[]>
|
||||
installSkill: (skillName: string, selectedSkillsDirs?: string[]) => Promise<{ success: boolean; results: SkillInstallTarget[]; error?: string }>
|
||||
exportSkillZip: (skillName: string) => Promise<{ success: boolean; outputPath?: string; fileName?: string; version?: string; error?: string }>
|
||||
}
|
||||
db: {
|
||||
|
||||
Reference in New Issue
Block a user