diff --git a/model/ability.go b/model/ability.go index 1d7c53f..3f26aa5 100644 --- a/model/ability.go +++ b/model/ability.go @@ -33,7 +33,8 @@ func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) { err := DB.Table("abilities"). Select("abilities.*, channels.type as channel_type"). Joins("left join channels on abilities.channel_id = channels.id"). - Where("abilities.enabled = ?", true). + Joins("left join models on abilities.model = models.model_name"). + Where("abilities.enabled = ? and models.model_name is NOT null", true). Scan(&abilities).Error return abilities, err } diff --git a/model/model_meta.go b/model/model_meta.go index 860b960..4f76898 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -15,6 +15,17 @@ const ( NameRuleSuffix ) +const ( + ModelTypeChat = 1 + iota + ModelTypeImage + ModelTypeAudio + ModelTypeVideo + ModelTypeEmbedding + ModelTypeRerank + ModelTypeVision + ModelTypeOther +) + type BoundChannel struct { Name string `json:"name"` Type int `json:"type"` @@ -26,6 +37,7 @@ type Model struct { Description string `json:"description,omitempty" gorm:"type:text"` Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` + Type int `json:"type" gorm:"default:0"` VendorID int `json:"vendor_id,omitempty" gorm:"index"` Endpoints string `json:"endpoints,omitempty" gorm:"type:text"` Status int `json:"status" gorm:"default:1"` diff --git a/model/pricing.go b/model/pricing.go index cb687d0..6dfe69b 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -28,6 +28,7 @@ type Pricing struct { EnableGroup []string `json:"enable_groups"` SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` PricingVersion string `json:"pricing_version,omitempty"` + Type int `json:"type"` } type PricingVendor struct { @@ -279,13 +280,14 @@ func updatePricing() { // 补充模型元数据(描述、标签、供应商、状态) if meta, ok := metaMap[model]; ok { // 若模型被禁用(status!=1),则直接跳过,不返回给前端 - if meta.Status != 1 { + if meta.Status != 1 || meta.Type == 0 { continue } pricing.Description = meta.Description pricing.Icon = meta.Icon pricing.Tags = meta.Tags pricing.VendorID = meta.VendorID + pricing.Type = meta.Type } modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) if findPrice { diff --git a/web/package.json b/web/package.json index 97c7c82..145448d 100644 --- a/web/package.json +++ b/web/package.json @@ -6,13 +6,15 @@ "dependencies": { "@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1", - "@lobehub/icons": "^2.0.0", + "@lobehub/icons": "^5.0.1", + "@lobehub/ui": "^4.38.0", "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", "axios": "1.13.5", "clsx": "^2.1.1", "dayjs": "^1.11.11", + "es-toolkit": "^1.21.0", "history": "^5.3.0", "i18next": "^23.16.8", "i18next-browser-languagedetector": "^7.2.0", diff --git a/web/src/components/common/ui/HorizontalFilterRow.jsx b/web/src/components/common/ui/HorizontalFilterRow.jsx new file mode 100644 index 0000000..b98cce6 --- /dev/null +++ b/web/src/components/common/ui/HorizontalFilterRow.jsx @@ -0,0 +1,140 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useRef } from 'react'; +import { Button, Tag, Skeleton } from '@douyinfe/semi-ui'; +import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons'; +import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; + +/** + * 横向筛选项行,通过左右箭头翻页 + * @param {string} title 标题 + * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 选项列表 + * @param {*} activeValue 当前选中值 + * @param {(value:any)=>void} onChange 选择回调 + * @param {boolean} loading 加载中 + * @param {Function} t i18n + */ +const HorizontalFilterRow = ({ + title, + items = [], + activeValue, + onChange, + loading = false, + t = (v) => v, +}) => { + const scrollRef = useRef(null); + const showSkeleton = useMinimumLoadingTime(loading); + + const handleScroll = (dir) => { + const el = scrollRef.current; + if (!el) return; + const step = el.clientWidth * 0.8; + el.scrollBy({ + left: dir === 'left' ? -step : step, + behavior: 'smooth', + }); + }; + + if (showSkeleton) { + return ( +
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+ ); + } + + return ( +
+
+ {title} +
+
+ + ); + })} +
+
+ + ); +}; + +export default HorizontalFilterRow; diff --git a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx index 6542137..c23e9f8 100644 --- a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx +++ b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx @@ -26,6 +26,7 @@ import ModelHeader from './components/ModelHeader'; import ModelBasicInfo from './components/ModelBasicInfo'; import ModelEndpoints from './components/ModelEndpoints'; import ModelPricingTable from './components/ModelPricingTable'; +import ModelCodeSnippet from './components/ModelCodeSnippet'; const { Text } = Typography; @@ -99,6 +100,7 @@ const ModelDetailSideSheet = ({ autoGroups={autoGroups} t={t} /> + )} diff --git a/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx new file mode 100644 index 0000000..cad1549 --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx @@ -0,0 +1,232 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useMemo, useContext } from 'react'; +import { + Card, + Avatar, + Typography, + RadioGroup, + Radio, + TextArea, + Button, +} from '@douyinfe/semi-ui'; +import { IconCode } from '@douyinfe/semi-icons'; +import { copy, showSuccess, showInfo } from '../../../../../helpers'; +import { StatusContext } from '../../../../../context/Status'; + +const { Text } = Typography; + +const LANG_OPTIONS = [ + { value: 'python', label: 'Python' }, + { value: 'java', label: 'Java' }, + { value: 'go', label: 'Go' }, + { value: 'shell', label: 'Shell' }, +]; + +// 四种语言的调用示例代码模板,使用 __MODEL_NAME__ 与 __BASE_URL__ 作为占位符 +const getDefaultCodeByLang = () => ({ + python: ` +from openai import OpenAI + +client = OpenAI( + api_key="", + base_url="__BASE_URL__" +) + +response = client.chat.completions.create( + model="__MODEL_NAME__", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello, how are you?"} + ], + max_tokens=128000, + temperature=0.7 +) + +print(response.choices[0].message.content) + `, + java: ` +import com.openai.OpenAI; +import com.openai.models.*; + +OpenAI client = new OpenAI("", "__BASE_URL__"); + +ChatCompletionRequest request = ChatCompletionRequest.builder() + .model("__MODEL_NAME__") + .messages(Arrays.asList( + new ChatMessage("system", "You are a helpful assistant."), + new ChatMessage("user", "Hello, how are you?") + )) + .maxTokens(1000) + .temperature(0.7) + .build(); + +ChatCompletion response = client.chatCompletions().create(request); +System.out.println(response.getChoices().get(0).getMessage().getContent()); + `, + go: ` +package main + +import ( + "context" + "fmt" + "github.com/openai/openai-go" +) + +func main() { + client := openai.NewClient("", "__BASE_URL__") + + messages := []openai.ChatMessage{ + {Role: "system", Content: "You are a helpful assistant."}, + {Role: "user", Content: "Hello, how are you?"}, + } + + response, err := client.ChatCompletions.Create(context.Background(), openai.ChatCompletionRequest{ + Model: "__MODEL_NAME__", + Messages: messages, + MaxTokens: 128000, + Temperature: 0.7, + }) + + if err != nil { + panic(err) + } + + fmt.Println(response.Choices[0].Message.Content) +} + `, + shell: ` +#!/bin/bash + +API_KEY="" +MODEL_ID="__MODEL_NAME__" +BASE_URL="__BASE_URL__" + +curl -X POST "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "'$MODEL_ID'", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello, how are you?" + } + ], + "max_tokens": 128000, + "temperature": 0.7 + }' + `, +}); + +const PLACEHOLDER_MODEL = '__MODEL_NAME__'; +const PLACEHOLDER_BASE_URL = '__BASE_URL__'; + +const ModelCodeSnippet = ({ modelData, t }) => { + const [statusState] = useContext(StatusContext); + const serverAddress = + statusState?.status?.server_address || (typeof window !== 'undefined' ? window.location.origin : ''); + const [lang, setLang] = useState('python'); + const codeByLang = useMemo(() => { + const templates = getDefaultCodeByLang(); + const modelName = modelData?.model_name || ''; + const baseUrl = serverAddress || ''; + return Object.fromEntries( + Object.entries(templates).map(([key, code]) => { + let result = code.replaceAll(PLACEHOLDER_BASE_URL, baseUrl); + result = result.replaceAll(PLACEHOLDER_MODEL, modelName); + return [key, result]; + }), + ); + }, [modelData?.model_name, serverAddress]); + + const currentCode = codeByLang[lang] || ''; + + const handleCopy = async () => { + if (!currentCode || currentCode.trim() === '') { + showInfo(t('当前语言暂无示例代码')); + return; + } + const ok = await copy(currentCode); + if (ok) { + showSuccess(t('已复制到剪切板')); + } + }; + + return ( + +
+ + + +
+ {t('调用示例')} +
+ {t('使用 OpenAI 兼容接口调用该模型的示例代码')} +
+
+
+ +
+ + {t('选择语言')} + + setLang(e.target.value)} + direction='horizontal' + > + {LANG_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + +
+ +
+