初始化模版工程

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,150 @@
# Nova SDK Tools
这个目录包含了从 Remix 迁移的工具相关组件用于管理自定义工具SKILL、MCP、API
## 目录结构
```
tools/
├── components/ # 主要组件
│ ├── api-form.tsx # API 工具表单
│ ├── custom-list-view.tsx # 自定义工具列表视图
│ ├── message-mcp.tsx # MCP 消息组件
│ ├── mcp-json-editor.tsx # MCP JSON 编辑器
│ ├── skill-form.tsx # SKILL 工具表单
│ ├── tool-item.tsx # 工具项组件
│ └── tool-type-badge.tsx # 工具类型徽章
├── settings/ # 设置相关组件
│ ├── ai-parse-config.tsx # AI 解析配置
│ ├── example-popover.tsx # 示例弹出框
│ ├── mcp-editor-modal.tsx # MCP 编辑模态框
│ └── mcp-store-popover.tsx # MCP 商店弹出框
├── hooks/ # React Hooks
│ └── useExampleList.ts # 获取示例列表
├── form-components/ # 表单组件
│ ├── path-form-item.tsx # 路径表单项
│ ├── form-header-item.tsx # 表单头项
│ ├── nest-form-request-form-item/ # 嵌套表单请求项
│ ├── env-manage/ # 环境管理
│ └── parse-curl/ # 解析 curl
└── index.ts # 主导出文件
```
## 主要功能
### 1. SKILL 工具管理
- `SkillForm`: 创建和编辑 SKILL 工具的表单组件
### 2. MCP 工具管理
- `MCPEditorModal`: MCP 服务编辑模态框
- `MCPJsonEditor`: JSON 格式的 MCP 配置编辑器
- `MessageMCP`: MCP 工具消息展示组件
- `ExamplePopover`: MCP 示例选择器
- `AiParseConfig`: AI 辅助解析 MCP 配置
- `McpStorePopover`: MCP 市场链接
### 3. API 工具管理
- `ApiForm`: 创建和编辑 API 工具的表单组件
- 各种表单组件支持 API 参数配置
### 4. 工具列表视图
- `MCPListView`: 自定义工具列表视图,包含 SKILL、MCP、API 三种类型
## 使用示例
### 导入组件
```typescript
// 导入所有工具组件
import { SkillForm, MCPEditorModal, MCPListView } from '@/nova-sdk/tools'
// 或者按需导入
import { SkillForm } from '@/nova-sdk/tools/components/skill-form'
import { MCPEditorModal } from '@/nova-sdk/tools/settings/mcp-editor-modal'
import { MCPListView } from '@/nova-sdk/tools/components/custom-list-view'
```
### 使用 SKILL 表单
```typescript
import { SkillForm } from '@/nova-sdk/tools'
import type { SkillFormState } from '@/nova-sdk/tools'
function MyComponent() {
const handleSave = async (values: SkillFormState) => {
// 处理保存逻辑
console.log(values)
}
return (
<SkillForm
teamId="your-team-id"
onBack={() => console.log('back')}
onSave={handleSave}
/>
)
}
```
### 使用 MCP 编辑器
```typescript
import { MCPEditorModal } from '@/nova-sdk/tools'
function MyComponent() {
const handleFinish = async (values: any) => {
// 保存 MCP 配置
console.log(values)
}
return (
<MCPEditorModal
value={mcpConfig}
isEdit={false}
exampleList={exampleList}
onFinish={handleFinish}
/>
)
}
```
### 使用工具列表视图
```typescript
import { MCPListView } from '@/nova-sdk/tools'
function MyComponent() {
const handleViewChange = (view: ViewType) => {
console.log('View changed:', view)
}
return (
<MCPListView
teamId="your-team-id"
onViewChange={handleViewChange}
onClose={() => console.log('close')}
/>
)
}
```
## 依赖说明
这些组件依赖于以下外部库:
- `antd`: UI 组件库
- `ahooks`: React Hooks 工具库
- `@ebay/nice-modal-react`: 模态框管理
以及内部依赖:
- `@/store/tool`: 工具状态管理
- `@/store/mcp`: MCP 状态管理
- `@/components/icon`: 图标组件
- `@/components/modal`: 模态框组件
- `@apis/mindnote/*`: API 接口
## 注意事项
1. 迁移的组件保留了原有的功能和接口
2. 所有组件都使用 TypeScript 类型定义
3. 表单验证和错误处理逻辑保持不变
4. 确保在使用前正确配置相关 Store 和 API

View File

@@ -0,0 +1,61 @@
# Nova SDK Tools - 组件替换总结
## ✅ 已完成工作
### 1. 清理和简化
- ✅ 删除了所有 API 相关组件api-form, form-components
- ✅ 删除了列表查看功能custom-list-view, tool-item, tool-type-badge
- ✅ 删除了示例功能useExampleList, example-popover
- ✅ 删除了所有 localize 相关代码
### 2. 只保留 SKILL 和 MCP 组件
**剩余文件6个**
```
tools/
├── components/
│ ├── skill-form.tsx ✅ 已替换为 shadcn + lucide-react
│ ├── mcp-json-editor.tsx ✅ 已替换为 shadcn + lucide-react
│ └── message-mcp.tsx 🔄 待替换
├── settings/
│ ├── mcp-editor-modal.tsx 🔄 待替换
│ ├── ai-parse-config.tsx 🔄 待替换
│ └── mcp-store-popover.tsx 🔄 待替换
└── index.ts
```
### 3. UI 框架替换进度
**已完成2/6**
- ✅ skill-form.tsx - Button, Input, Textarea, Label, toast (shadcn) + Inbox (lucide-react)
- ✅ mcp-json-editor.tsx - Button, Textarea, toast (shadcn)
**待完成4/6**
- 🔄 message-mcp.tsx - 需替换 Spin, Button
- 🔄 mcp-editor-modal.tsx - 需替换 Button, Form, Input, message, Select, Tooltip
- 🔄 ai-parse-config.tsx - 需替换 Button, Form, Input, message, Popover, Tooltip
- 🔄 mcp-store-popover.tsx - 需替换 Popover, Tooltip
## 📋 替换方案
### antd → shadcn/ui 映射表
| antd | shadcn/ui |
|------|-----------|
| Button | Button |
| Input | Input |
| Input.TextArea | Textarea |
| Form | Label + Input/Textarea + 自定义验证 |
| Select | Select |
| message | toast (from @/hooks/use-toast) |
| Popover | Popover |
| Tooltip | Tooltip |
| Spin | Loader2 (from lucide-react) |
| Upload | 自定义文件上传 |
### @ant-design/icons → lucide-react 映射表
| antd icons | lucide-react |
|------------|--------------|
| InboxOutlined | Inbox |
| 其他图标 | 相应的 lucide-react 图标 |
## 🎯 下一步
继续替换剩余 4 个文件中的 antd 组件为 shadcn/ui

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'

View File

@@ -0,0 +1,2 @@
// Hooks
// useExampleList removed - only create functionality needed

View File

@@ -0,0 +1,8 @@
// Skill Components
export * from './components/skill-form'
// MCP Components
export * from './components/mcp-json-editor'
// MCP Settings
export * from './settings/mcp-store-popover'

View File

@@ -0,0 +1,2 @@
// Settings Components
export { McpStorePopover } from './mcp-store-popover'

View File

@@ -0,0 +1,68 @@
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { memo, useState } from 'react'
import { ExternalLink, Store, Globe } from 'lucide-react'
const storeList = [
{
name: 'modelscope.cn',
link: 'https://www.modelscope.cn/mcp',
},
{
name: 'mcpmarket.cn',
link: 'https://mcpmarket.cn',
},
{
name: 'mcp.so',
link: 'https://mcp.so',
},
]
export const McpStorePopover = memo(() => {
const [menuOpen, setMenuOpen] = useState(false)
return (
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<button className="inline-flex items-center justify-center rounded-full w-7 h-7 bg-gray-100 text-gray-900 transition-colors hover:bg-gray-200">
<Store className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>
<p>MCP </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<PopoverContent align="start" className="w-60 border border-border bg-popover p-1">
<div className="px-3 py-2 text-sm font-medium text-foreground">
MCP
</div>
<div className="flex flex-col gap-1 mt-1">
{storeList.map((item) => (
<button
type="button"
key={item.name}
className="group flex cursor-pointer items-center justify-between rounded-lg px-3 py-3 transition-colors hover:bg-accent"
onClick={() => {
setMenuOpen(false)
if (typeof window !== 'undefined') {
window.open(item.link, '_blank')
}
}}
>
<div className="flex items-center gap-1.5">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-sm text-foreground">{item.name}</div>
</div>
<ExternalLink className="h-4 w-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
</button>
))}
</div>
</PopoverContent>
</Popover>
)
})