diff --git a/README.md b/README.md index 4585a66..5701eb2 100644 --- a/README.md +++ b/README.md @@ -4,64 +4,61 @@ [![React Version](https://img.shields.io/badge/React-19.0.0+-blue.svg)](https://reactjs.org) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -ModelKit 是一个强大的AI模型管理平台,支持多种AI服务提供商,提供统一的模型管理、配置验证和API接口服务。 +ModelKit 是一个强大的AI模型管理平台,支持多种AI服务提供商,提供统一的模型管理、配置验证服务。 ## 🚀 功能特性 - **多模型提供商支持**: 支持 OpenAI、Ollama、DeepSeek、SiliconFlow、Moonshot、Azure OpenAI、百智云、腾讯混元、百炼、火山引擎、Gemini、智谱等主流AI服务商 - **模型类型管理**: 支持聊天模型、嵌入模型、重排序模型、视觉模型、代码模型、函数调用等多种模型类型 - **配置验证**: 提供模型配置的实时验证功能,确保API配置正确性 -- **统一API接口**: 提供标准化的RESTful API,简化AI模型集成 - **现代化Web界面**: 基于React 19和Material-UI构建的响应式用户界面 - **国际化支持**: 内置中英文多语言支持 - **可复用组件**: 提供开箱即用的ModelModal组件,支持在其他项目中快速集成 -## 🏗️ 技术架构 - -### 后端 (Go) -- **框架**: Echo v4 (HTTP框架) -- **语言**: Go 1.24.0+ -- **架构**: 分层架构 (Handler -> UseCase -> Domain) -- **依赖管理**: Go Modules - -### 前端 (React) -- **框架**: React 19 + TypeScript -- **UI库**: Material-UI v6 + CT-MUI -- **构建工具**: Vite 6 -- **状态管理**: Redux Toolkit -- **路由**: React Router v7 -- **编辑器**: TipTap (富文本编辑器) - -## 📁 项目结构 - -``` -ModelKit/ -├── consts/ # 常量定义 -├── domain/ # 领域模型和接口 -├── errcode/ # 错误码和国际化 -├── handler/ # HTTP处理器 -├── pkg/ # 公共包 -├── usecase/ # 业务用例 -├── ui/ # 前端应用 -│ ├── src/ # 源代码 -│ │ ├── components/ # 可复用组件 -│ │ │ └── Card/ # 卡片组件 -│ │ ├── constant/ # 常量定义 -│ │ ├── api/ # API接口 -│ │ └── services/ # 服务层 -│ ├── public/ # 静态资源 -│ └── package.json # 前端依赖 -├── utils/ # 工具函数 -├── go.mod # Go模块文件 -└── Makefile # 构建脚本 +## 使用方式 +### 后端 +``` bash + // 1. 引入ModelKit + import ( + modelkit "github.com/chaitin/ModelKit/usecase" + ) + // 2. 调用ModelKit提供的函数即可 + modelkitRes, err := modelkit.CheckModel(...) + modelkitRes, err := modelkit.ListModel(...) ``` - -## 🛠️ 安装部署 - -### 环境要求 - -- Go 1.24.0+ - -## 📝 许可证 - -本项目采用 [MIT 许可证](LICENSE)。 +### 前端 +``` bash + // 1. 引入ModelKit + import { ModelModal, Model, ModelService, ConstsModelType as ModelKitType, ModelListItem } from '@yokowu/modelkit-ui'; + // 2.创建符合ModelService接口的服务实现 + const modelService: ModelService = { + createModel: async (params) => { + const response = await postCreateModel(params as unknown as DomainCreateModelReq); + return { model: response as unknown as Model }; + }, + listModel: async (params) => { + const response = await getGetProviderModelList(params as unknown as GetGetProviderModelListParams); + return { models: response?.models || [] }; + }, + checkModel: async (params) => { + const response = await postCheckModel(params as unknown as DomainCheckModelReq); + return { model: response as unknown as Model }; + }, + updateModel: async (params) => { + const response = await putUpdateModel(params as unknown as DomainUpdateModelReq); + return { model: response as unknown as Model }; + } + // 3. 使用ModelModal组件 + { + setOpen(false); + setEditData(null); + }} + refresh={refreshModel} + data={editData as Model | null} + type={modelType} + modelService={modelService} + language="zh-CN" + /> +``` \ No newline at end of file diff --git a/handler/http/v1/modelkit.go b/handler/http/v1/modelkit.go index b52d8b4..cea98f5 100644 --- a/handler/http/v1/modelkit.go +++ b/handler/http/v1/modelkit.go @@ -51,12 +51,12 @@ func (p *ModelKit) GetModelList(c echo.Context) error { } // 验证参数 - // if err := c.Validate(&req); err != nil { - // return c.JSON(http.StatusBadRequest, domain.Response{ - // Success: false, - // Message: "参数验证失败: " + err.Error(), - // }) - // } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, domain.Response{ + Success: false, + Message: "参数验证失败: " + err.Error(), + }) + } resp, err := usecase.ModelList(c.Request().Context(), &req) if err != nil { diff --git a/ui/ModelModal/README.md b/ui/ModelModal/README.md index 8e97c2d..0eeb4d6 100644 --- a/ui/ModelModal/README.md +++ b/ui/ModelModal/README.md @@ -5,11 +5,7 @@ ## 安装 ```bash -npm install @yokowu/modelkit-ui -# 或 -yarn add @yokowu/modelkit-ui -# 或 -pnpm add @yokowu/modelkit-ui +npm install @your-org/model-modal ``` ## 使用方法 @@ -100,51 +96,61 @@ const baseTheme = createTheme({ const theme = mergeThemeWithDefaults(baseTheme); ``` -## 组件 +### TypeScript 支持 -### ModelAdd +如果你使用 TypeScript,需要确保主题类型扩展被正确导入: -模型添加/编辑弹窗组件。 +```tsx +// 在你的类型声明文件中(如 vite-env.d.ts 或 global.d.ts) +import '@your-org/model-modal/dist/types/theme'; +``` -#### Props +或者在使用组件的文件中导入: -- `open: boolean` - 是否显示弹窗 -- `data: ModelListItem | null` - 编辑时的模型数据 -- `type: 'chat' | 'embedding' | 'rerank'` - 模型类型 -- `onClose: () => void` - 关闭弹窗回调 -- `refresh: () => void` - 刷新数据回调 +```tsx +import '@your-org/model-modal/dist/types/theme'; +import { ModelModal } from '@your-org/model-modal'; +``` -## 依赖要求 +## 常见问题 -确保你的项目已安装以下依赖: +### 样式显示不正确 -```json -{ - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0", - "@mui/material": ">=5.0.0", - "@mui/icons-material": ">=5.0.0", - "@emotion/react": ">=11.0.0", - "@emotion/styled": ">=11.0.0", - "react-hook-form": ">=7.0.0" - } -} -``` +如果组件的样式显示不正确,通常是因为主题中缺少 `background.paper2` 的定义。请确保: -## 开发 +1. 在主题配置中添加 `background.paper2` 属性 +2. 使用 `ThemeProvider` 包装你的应用 +3. 导入了正确的主题类型声明 -本项目使用 pnpm 作为包管理器: +### TypeScript 类型错误 -```bash -# 安装依赖 -pnpm install +如果遇到 TypeScript 类型错误,如 "Property 'paper2' does not exist",请确保: -# 开发模式 -pnpm dev +1. 导入了主题类型扩展:`import '@your-org/model-modal/dist/types/theme'` +2. 重启 TypeScript 服务 -# 构建 -pnpm build +## API + +### ModelModalProps + +| 属性 | 类型 | 必需 | 描述 | +|------|------|------|------| +| open | boolean | ✓ | 控制模态框的显示/隐藏 | +| onClose | () => void | ✓ | 关闭模态框的回调函数 | +| refresh | () => void | ✓ | 刷新数据的回调函数 | +| data | Model \| null | ✓ | 编辑时的模型数据,新建时为 null | +| type | ConstsModelType | ✓ | 模型类型 | +| modelService | ModelService | ✓ | 模型服务接口 | + +### ModelService + +```tsx +interface ModelService { + createModel: (data: CreateModelReq) => Promise<{ model: Model }>; + listModel: (data: ListModelReq) => Promise<{ models: ModelListItem[] }>; + checkModel: (data: CheckModelReq) => Promise<{ model: Model }>; + updateModel: (data: UpdateModelReq) => Promise<{ model: Model }>; +} ``` ## 许可证 diff --git a/ui/ModelModal/package.json b/ui/ModelModal/package.json index 7b0d7cb..1a8572b 100644 --- a/ui/ModelModal/package.json +++ b/ui/ModelModal/package.json @@ -1,6 +1,6 @@ { "name": "@yokowu/modelkit-ui", - "version": "0.3.0", + "version": "0.4.0", "description": "A reusable AI model configuration modal component for React applications", "private": false, "type": "module", @@ -16,8 +16,7 @@ "./styles": "./dist/styles.css" }, "publishConfig": { - "registry": "https://registry.npmjs.org", - "access": "public" + "registry": "https://npm.pkg.github.com" }, "files": [ "dist" diff --git a/ui/ModelModal/rollup.config.mjs b/ui/ModelModal/rollup.config.mjs deleted file mode 100644 index 2372696..0000000 --- a/ui/ModelModal/rollup.config.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import terser from '@rollup/plugin-terser'; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; -import { createRequire } from 'module'; -const require = createRequire(import.meta.url); -const packageJson = require('./package.json'); - -export default [ - { - input: 'src/index.ts', - external: ['react', 'react-dom'], - output: [ - { - file: packageJson.main, - format: 'cjs', - sourcemap: true, - }, - { - file: packageJson.module, - format: 'esm', - sourcemap: true, - }, - ], - plugins: [ - peerDepsExternal(), - resolve({ - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }), - commonjs(), - typescript({ - tsconfig: './tsconfig.json', - declaration: true, - declarationDir: './dist', - }), - terser(), - ], - }, -]; \ No newline at end of file diff --git a/ui/ModelModal/src/ModelModal.tsx b/ui/ModelModal/src/ModelModal.tsx index a50b2af..8173b9a 100644 --- a/ui/ModelModal/src/ModelModal.tsx +++ b/ui/ModelModal/src/ModelModal.tsx @@ -24,7 +24,6 @@ import { } from './types/types'; import { DEFAULT_MODEL_PROVIDERS } from './constants/providers'; import { ModelProvider } from './constants/providers'; -import { mergeThemeWithDefaults } from './constants/theme'; import { getLocaleMessage } from './constants/locale'; import './assets/fonts/iconfont'; import { lightTheme } from './theme'; diff --git a/ui/ModelModal/src/assets/images/logo.png b/ui/ModelModal/src/assets/images/logo.png deleted file mode 100644 index da96d16..0000000 Binary files a/ui/ModelModal/src/assets/images/logo.png and /dev/null differ diff --git a/ui/ModelModal/src/assets/images/qrcode.png b/ui/ModelModal/src/assets/images/qrcode.png deleted file mode 100644 index 803314a..0000000 Binary files a/ui/ModelModal/src/assets/images/qrcode.png and /dev/null differ diff --git a/ui/ModelModal/src/constants/theme.ts b/ui/ModelModal/src/constants/theme.ts deleted file mode 100644 index a660924..0000000 --- a/ui/ModelModal/src/constants/theme.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { createTheme } from '@mui/material/styles'; -import type { Shadows } from '@mui/material'; -import { zhCN } from '@mui/material/locale'; -import { zhCN as CuiZhCN } from '@c-x/ui/dist/local'; -import onData from '@/assets/images/nodata.png'; - -const defaultTheme = createTheme(); -// 默认主题配置,确保与UI项目主题一致 -export const defaultModelModalTheme = createTheme({ - cssVariables: true, - palette: { - // mode: 'light', - primary: { - main: '#21222D', - }, - error: { - main: '#F64E54', - }, - success: { - main: '#35B37E', - light: '#AAF27F', - dark: '#229A16', - contrastText: '#fff', - }, - warning: { - main: '#FFA500', - }, - info: { - main: '#3248F2', - }, - risk: { - severe: '#FF6262', - critical: '#FFA762', - suggest: '#FFCF62' - }, - disabled: { - main: '#666', - }, - dark: { - dark: '#000', - main: '#14141B', - light: '#20232A', - contrastText: '#fff', - }, - light: { - main: '#fff', - contrastText: '#000', - }, - background: { - default: '#fff', - paper: '#F1F2F8', - }, - text: { - primary: '#21222D', - secondary: 'rgba(33,34,45, 0.7)', - tertiary: 'rgba(33,34,45, 0.5)', - // @ts-ignore - auxiliary: 'rgba(33,34,45, 0.5)', - disabled: 'rgba(33,34,45, 0.2)', - }, - // divider: '#ECEEF1', - }, - shadows: [ - ...defaultTheme.shadows.slice(0, 8), - '0px 10px 20px 0px rgba(54,59,76,0.2)', - ...defaultTheme.shadows.slice(9), - ] as Shadows, - components: { - MuiPaper: { - styleOverrides: { - root: { - backgroundColor: '#fff', - backgroundImage: 'none', - }, - }, - }, - - MuiInputBase: { - styleOverrides: { - root: { - backgroundColor: '#F8F9FA', - fontFamily: `var(--font-gilory), var(--font-HarmonyOS), 'PingFang SC', - 'Roboto', 'Helvetica', 'Arial', sans-serif`, - '.MuiOutlinedInput-notchedOutline': { - borderColor: 'transparent', - }, - '&.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: '#21222D !important', - borderWidth: '10px !important', - }, - borderRadius: '10px !important', - fontSize: 14, - }, - }, - }, - - MuiTypography: { - styleOverrides: { - root: { - fontFamily: `var(--font-gilory), var(--font-HarmonyOS), 'PingFang SC', - 'Roboto', 'Helvetica', 'Arial', sans-serif`, - }, - }, - }, - MuiButtonBase: { - styleOverrides: { - root: { - // lineHeight: '1.5', - fontFamily: `var(--font-gilory), var(--font-HarmonyOS), 'PingFang SC', - 'Roboto', 'Helvetica', 'Arial', sans-serif`, - }, - }, - }, - MuiButton: { - styleOverrides: { - root: { - // lineHeight: '1.5', - borderRadius: '10px', - boxShadow: 'none', - fontFamily: `var(--font-gilory), var(--font-HarmonyOS), 'PingFang SC', - 'Roboto', 'Helvetica', 'Arial', sans-serif`, - }, - }, - }, - // @ts-ignore - MuiLoadingButton: { - styleOverrides: { - root: { - lineHeight: '1.5', - borderRadius: '10px', - }, - }, - }, - MuiInputLabel: { - styleOverrides: { - root: { - fontSize: 14, - }, - }, - }, - MuiMenu: { - styleOverrides: { - paper: { - borderRadius: '10px', - }, - }, - }, - MuiMenuItem: { - styleOverrides: { - root: { - fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)', - fontSize: '14px', - }, - }, - }, - MuiAutocomplete: { - defaultProps: { - slotProps: { - paper: { - elevation: 8, - }, - }, - }, - styleOverrides: { - paper: { - borderRadius: '10px', - }, - option: { - fontSize: '14px', - fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)', - }, - }, - }, - MuiFormLabel: { - styleOverrides: { - root: { - fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)', - }, - asterisk: { - color: '#F64E54', - }, - }, - }, - MuiLink: { - styleOverrides: { - root: { - textDecoration: 'none', - }, - }, - }, - MuiTableCell: { - styleOverrides: { - root: { - borderColor: '#eee', - paddingLeft: '24px !important', - fontFamily: 'var(--font-gilory), var(--font-HarmonyOS)', - padding: '25px 12px 26px !important', - '&:first-of-type': { - paddingLeft: '16px !important', - }, - }, - head: { - paddingTop: '0 !important', - paddingBottom: '0 !important', - height: '50px', - backgroundColor: '#f8f9fa', - borderBottom: 'none !important', - }, - body: { - borderBottom: '1px dashed', - borderColor: '#ECEEF1', - }, - }, - }, - }, - }, - zhCN, - CuiZhCN, - { - components: { - CuiEmpty: { - defaultProps: { - image: onData, - imageStyle: { - width: '150px', - }, - }, - }, - }, - -}); - -// 主题合并工具函数 - 以用户自定义主题为主 -// 深度合并函数 -const deepMerge = (target: any, source: any): any => { - const result = { ...target }; - - for (const key in source) { - if (source.hasOwnProperty(key)) { - if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { - result[key] = deepMerge(target[key] || {}, source[key]); - } else { - result[key] = source[key]; - } - } - } - - return result; -}; - -export const mergeThemeWithDefaults = (userTheme: any) => { - return deepMerge(defaultModelModalTheme, userTheme); -}; \ No newline at end of file diff --git a/ui/ModelModal/src/index.ts b/ui/ModelModal/src/index.ts index 814318b..289fa85 100644 --- a/ui/ModelModal/src/index.ts +++ b/ui/ModelModal/src/index.ts @@ -10,7 +10,6 @@ export { ConstsModelType } from './types/types'; // 常量 export { DEFAULT_MODEL_PROVIDERS, getProvidersByType } from './constants/providers'; export { LOCALE_MESSAGES, getLocaleMessage, getTitleMap } from './constants/locale'; -export * from './constants/theme'; // 主题类型声明会通过TypeScript自动包含 diff --git a/ui/ModelModal/src/types/theme.d.ts b/ui/ModelModal/src/types/theme.d.ts deleted file mode 100644 index 79fa6b5..0000000 --- a/ui/ModelModal/src/types/theme.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -/// - -import type { PaletteColorChannel } from '@mui/material'; - -declare module '@mui/material/styles' { - interface TypeText { - tertiary: string; - auxiliary: string; - } - - interface TypeBackground { - paper2: string; - } - - interface Palette { - light: Palette['primary'] & PaletteColorChannel; - dark: Palette['primary'] & PaletteColorChannel; - disabled: Palette['primary'] & PaletteColorChannel; - risk: { - severe: string; - critical: string; - suggest: string; - }; - } - - // allow configuration using `createTheme` - interface PaletteOptions { - light?: PaletteOptions['primary'] & Partial; - dark?: PaletteOptions['primary'] & Partial; - disabled?: PaletteOptions['primary'] & Partial; - risk?: { - severe?: string; - critical?: string; - suggest?: string; - }; - text?: Partial; - background?: Partial; - } -} - -declare module '@mui/material/Button' { - interface ButtonPropsColorOverrides { - light: true; - dark: true; - } -} - -import type {} from '@mui/material/themeCssVarsAugmentation'; \ No newline at end of file diff --git a/usecase/modelkit.go b/usecase/modelkit.go index c967899..4bde601 100644 --- a/usecase/modelkit.go +++ b/usecase/modelkit.go @@ -18,6 +18,7 @@ import ( "github.com/cloudwego/eino-ext/components/model/openai" "github.com/cloudwego/eino/components/model" "github.com/cloudwego/eino/schema" + "github.com/go-playground/validator/v10" "github.com/ollama/ollama/api" "google.golang.org/genai" @@ -27,7 +28,19 @@ import ( "github.com/chaitin/ModelKit/utils" ) +// 全局验证器实例 +var validate *validator.Validate + +func init() { + validate = validator.New() +} + func ModelList(ctx context.Context, req *domain.ModelListReq) (*domain.ModelListResp, error) { + // 验证请求参数 + if err := validate.Struct(req); err != nil { + return nil, fmt.Errorf("参数验证失败: %w", err) + } + httpClient := &http.Client{ Timeout: time.Second * 30, Transport: &http.Transport{ @@ -104,6 +117,13 @@ func ModelList(ctx context.Context, req *domain.ModelListReq) (*domain.ModelList } func CheckModel(ctx context.Context, req *domain.CheckModelReq) (*domain.CheckModelResp, error) { + // 验证请求参数 + if err := validate.Struct(req); err != nil { + return &domain.CheckModelResp{ + Error: fmt.Sprintf("参数验证失败: %v", err), + }, nil + } + checkResp := &domain.CheckModelResp{} modelType := consts.ModelType(req.Type) modelProvider := consts.ModelProvider(req.Provider)