初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
// Skill Form
export { SkillForm } from './skill-form'
export type { SkillFormState } from './skill-form'
// MCP Components
export { MCPJsonEditor } from './mcp-json-editor'
export type { MCPJsonEditorRef } from './mcp-json-editor'

View File

@@ -0,0 +1,148 @@
import { useState, forwardRef, useImperativeHandle } from 'react'
import { Loader2 } from 'lucide-react'
import { useMemoizedFn } from 'ahooks'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
interface MCPJsonEditorProps {
value: string
onChange: (value: string) => void
onSave?: (parsedConfig: Record<string, unknown>) => void | Promise<void>
showButtons?: boolean
}
export interface MCPJsonEditorRef {
triggerSave: () => Promise<void>
}
export const MCPJsonEditor = forwardRef<MCPJsonEditorRef, MCPJsonEditorProps>(
({ value, onChange, onSave, showButtons = true }, ref) => {
const [jsonValue, setJsonValue] = useState(value)
const [isSaving, setIsSaving] = useState(false)
const parseJsonToConfig = useMemoizedFn(async () => {
if (isSaving) return
setIsSaving(true)
try {
const res = JSON.parse(jsonValue)
const { mcpServers = {} as Record<string, unknown> } = res
const mcpServer = Object.entries(mcpServers)[0]
if (!mcpServer) {
toast.error('未检测到合法的MCP配置')
return
}
const [key, value] = mcpServer as [string, Record<string, unknown>]
const code = key
const command = value.command ?? ''
const args = value.args ?? []
const env = value.env ?? {}
const url = value.url ?? ''
if ((!url && !command) || !code) {
toast.error('非法配置')
return
}
const type = command ? 'command' : 'sse'
const argsString = (args as string[]).join(' ')
const envString = Object.entries(env)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
if (
type === 'command' &&
!['npx', 'uv', 'uvx'].includes(value.command as string ?? '')
) {
toast.error(`非法配置,不支持的命令: ${value.command}`)
return
}
const parsedConfig = {
type,
command,
code,
name: code,
args: argsString,
env: envString,
url,
headers: value.headers ?? '',
}
onChange(jsonValue)
await onSave?.(parsedConfig)
} catch {
toast.error('解析失败请检查JSON格式')
} finally {
setIsSaving(false)
}
})
const handleJsonChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
setJsonValue(newValue)
onChange(newValue)
}
useImperativeHandle(ref, () => ({
triggerSave: parseJsonToConfig,
}))
return (
<div className='w-full h-full flex flex-col' style={{ pointerEvents: 'auto' }}>
<div className='flex-1 flex flex-col' style={{ pointerEvents: 'auto' }}>
<Textarea
className='flex-1 rounded-xl border border-input bg-card focus:border-primary/40 focus:ring-2 focus:ring-ring'
style={{
resize: 'none',
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", consolas, "source-code-pro", monospace',
minHeight: '300px',
pointerEvents: 'auto',
zIndex: 1,
position: 'relative',
cursor: 'text',
caretColor: 'var(--primary)',
outline: 'none',
userSelect: 'text',
}}
autoFocus
placeholder={`仅支持解析一个MCP如果有多个仅解析第一个示例
{
"mcpServers": {
"RedNote MCP": {
"command": "npx",
"args": ["rednote-mcp", "--stdio"]
}
}
}
或者 SSE 方式:
{
"mcpServers": {
"Weather MCP": {
"url": "http://localhost:8000/mcp"
}
}
}`}
value={jsonValue}
onChange={handleJsonChange}
/>
</div>
{showButtons && (
<div className='shrink-0 pt-3 flex items-center justify-end gap-3'>
<Button onClick={parseJsonToConfig} disabled={isSaving}>
{isSaving && <Loader2 className='h-4 w-4 animate-spin' />}
{isSaving ? '解析中...' : '解析并保存'}
</Button>
</div>
)}
</div>
)
},
)
MCPJsonEditor.displayName = 'MCPJsonEditor'

View File

@@ -0,0 +1,147 @@
import { memo, useEffect, useState } from 'react'
import { useMemoizedFn } from 'ahooks'
import { cn } from '@/utils/cn'
import { Inbox } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
export interface SkillFormState {
name: string
description?: string
file?: { file: File; fileList: File[] }
}
interface SkillFormProps {
values?: { name: string; description?: string }
className?: string
teamId: string
onBack: () => void
onSave: (values: SkillFormState) => Promise<void>
}
const name_required_text = '请输入SKILL工具名称'
const file_tooltip_text = '仅支持上传 .zip 文件'
export const SkillForm = memo<SkillFormProps>(
({ className, values, onBack, onSave }) => {
const [name, setName] = useState(values?.name || '')
const [description, setDescription] = useState(values?.description || '')
const [file, setFile] = useState<File | null>(null)
const [loading, setLoading] = useState(false)
const isEdit = !!values
useEffect(() => {
if (values) {
setName(values.name)
setDescription(values.description || '')
}
}, [values])
const handleSave = useMemoizedFn(async () => {
if (!name.trim()) {
toast.error('错误')
return
}
if (!isEdit && !file) {
toast.error('错误')
return
}
try {
setLoading(true)
await onSave({
name,
description,
file: file ? { file: file as File, fileList: [file as File] } : undefined
})
} finally {
setLoading(false)
}
})
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0]
if (selectedFile) {
const isZip = selectedFile.type === 'application/zip' || selectedFile.name.endsWith('.zip')
if (!isZip) {
toast.error('错误')
return
}
setFile(selectedFile)
}
}
return (
<div className='flex h-full flex-col bg-transparent text-foreground'>
<div className={cn('flex-1 p-24px overflow-y-auto space-y-6', className)}>
<div className='space-y-2 px-[4px]'>
<Label htmlFor='name' className='mb-[] text-sm font-medium text-foreground'></Label>
<Input
id='name'
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={name_required_text}
maxLength={100}
className='mt-[8px] border-border bg-card text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-0'
/>
</div>
<div className='space-y-2 px-[4px]'>
<Label htmlFor='description' className='text-sm font-medium text-foreground'></Label>
<Textarea
id='description'
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder='请输入SKILL工具的描述'
rows={3}
className='mt-[8px] min-h-[120px] resize-y border-border bg-card text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-0'
/>
</div>
<div className='space-y-2 px-[4px]'>
<Label htmlFor='file' className='text-sm font-medium text-foreground'></Label>
<div className='relative mt-[8px] cursor-pointer rounded-lg border-2 border-dashed border-border bg-card p-8 text-center transition-colors hover:border-primary'>
<Inbox className='mx-auto mb-4 h-12 w-12 cursor-pointer text-muted-foreground' />
<input
id='file'
type='file'
accept='.zip,application/zip'
onChange={handleFileChange}
className='opacity-0 absolute top-0 left-0 w-full h-full'
/>
<label htmlFor='file' className='cursor-pointer'>
<p className='text-sm text-foreground'>
{file ? file.name : '点击或拖拽文件到这里上传'}
</p>
<p className='mt-2 text-xs text-muted-foreground'>{file_tooltip_text}</p>
</label>
</div>
</div>
</div>
<div className='mt-[16px] flex justify-end gap-8px border-t border-border/70 bg-transparent px-24px pb-24px pt-16px'>
<Button
variant='outline'
onClick={onBack}
className='mr-[8px] hover:bg-transparent hover:border-border hover:text-foreground'
>
</Button>
<Button
onClick={handleSave}
disabled={loading}
>
{loading ? '保存中...' : '保存'}
</Button>
</div>
</div>
)
},
)
SkillForm.displayName = 'SkillForm'