Commit be567ef7 by Seefs Committed by GitHub

fix: fix model deployment style issues, lint problems, and i18n gaps. (#2556)

* fix: fix model deployment style issues, lint problems, and i18n gaps.

* fix: adjust the key not to be displayed on the frontend, tested via the backend.

* fix: adjust the sidebar configuration logic to use the default configuration items if they are not defined.
parent 1c95a9fe
package controller
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
......@@ -23,6 +25,20 @@ func getIoAPIKey(c *gin.Context) (string, bool) {
return apiKey, true
}
func GetModelDeploymentSettings(c *gin.Context) {
common.OptionMapRWMutex.RLock()
enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true"
hasAPIKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"]) != ""
common.OptionMapRWMutex.RUnlock()
common.ApiSuccess(c, gin.H{
"provider": "io.net",
"enabled": enabled,
"configured": hasAPIKey,
"can_connect": enabled && hasAPIKey,
})
}
func getIoClient(c *gin.Context) (*ionet.Client, bool) {
apiKey, ok := getIoAPIKey(c)
if !ok {
......@@ -44,15 +60,28 @@ func TestIoNetConnection(c *gin.Context) {
APIKey string `json:"api_key"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "invalid request payload")
rawBody, err := c.GetRawData()
if err != nil {
common.ApiError(c, err)
return
}
if len(bytes.TrimSpace(rawBody)) > 0 {
if err := json.Unmarshal(rawBody, &req); err != nil {
common.ApiErrorMsg(c, "invalid request payload")
return
}
}
apiKey := strings.TrimSpace(req.APIKey)
if apiKey == "" {
common.ApiErrorMsg(c, "api_key is required")
return
common.OptionMapRWMutex.RLock()
storedKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"])
common.OptionMapRWMutex.RUnlock()
if storedKey == "" {
common.ApiErrorMsg(c, "api_key is required")
return
}
apiKey = storedKey
}
client := ionet.NewEnterpriseClient(apiKey)
......
......@@ -20,7 +20,11 @@ func GetOptions(c *gin.Context) {
var options []*model.Option
common.OptionMapRWMutex.Lock()
for k, v := range common.OptionMap {
if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") || strings.HasSuffix(k, "Key") {
if strings.HasSuffix(k, "Token") ||
strings.HasSuffix(k, "Secret") ||
strings.HasSuffix(k, "Key") ||
strings.HasSuffix(k, "secret") ||
strings.HasSuffix(k, "api_key") {
continue
}
options = append(options, &model.Option{
......
......@@ -269,24 +269,18 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute := apiRouter.Group("/deployments")
deploymentsRoute.Use(middleware.AdminAuth())
{
// List and search deployments
deploymentsRoute.GET("/settings", controller.GetModelDeploymentSettings)
deploymentsRoute.POST("/settings/test-connection", controller.TestIoNetConnection)
deploymentsRoute.GET("/", controller.GetAllDeployments)
deploymentsRoute.GET("/search", controller.SearchDeployments)
// Connection utilities
deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
// Resource and configuration endpoints
deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
deploymentsRoute.GET("/locations", controller.GetLocations)
deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
// Create new deployment
deploymentsRoute.POST("/", controller.CreateDeployment)
// Individual deployment operations
deploymentsRoute.GET("/:id", controller.GetDeployment)
deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
......@@ -295,14 +289,6 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
// Future batch operations:
// deploymentsRoute.POST("/:id/start", controller.StartDeployment)
// deploymentsRoute.POST("/:id/stop", controller.StopDeployment)
// deploymentsRoute.POST("/:id/restart", controller.RestartDeployment)
// deploymentsRoute.POST("/batch_delete", controller.BatchDeleteDeployments)
// deploymentsRoute.POST("/batch_start", controller.BatchStartDeployments)
// deploymentsRoute.POST("/batch_stop", controller.BatchStopDeployments)
}
}
}
......@@ -25,7 +25,9 @@ export default defineConfig({
"zh",
"en",
"fr",
"ru"
"ru",
"ja",
"vi"
],
extract: {
input: [
......
......@@ -46,7 +46,7 @@ const DeploymentAccessGuard = ({
<div className='mt-[60px] px-2'>
<Card loading={true} style={{ minHeight: '400px' }}>
<div style={{ textAlign: 'center', padding: '50px 0' }}>
<Text type="secondary">{t('加载设置中...')}</Text>
<Text type='secondary'>{t('加载设置中...')}</Text>
</div>
</Card>
</div>
......@@ -55,21 +55,21 @@ const DeploymentAccessGuard = ({
if (!isEnabled) {
return (
<div
className='mt-[60px] px-4'
<div
className='mt-[60px] px-4'
style={{
minHeight: 'calc(100vh - 60px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
justifyContent: 'center',
}}
>
<div
<div
style={{
maxWidth: '600px',
width: '100%',
textAlign: 'center',
padding: '0 20px'
padding: '0 20px',
}}
>
<Card
......@@ -78,45 +78,49 @@ const DeploymentAccessGuard = ({
borderRadius: '16px',
border: '1px solid var(--semi-color-border)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
background: 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)'
background:
'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
}}
>
{/* 图标区域 */}
<div style={{ marginBottom: '32px' }}>
<div style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '120px',
height: '120px',
borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
border: '3px solid rgba(var(--semi-orange-4), 0.3)',
marginBottom: '24px'
}}>
<AlertCircle size={56} color="var(--semi-color-warning)" />
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '120px',
height: '120px',
borderRadius: '50%',
background:
'linear-gradient(135deg, rgba(var(--semi-orange-4), 0.15) 0%, rgba(var(--semi-orange-5), 0.1) 100%)',
border: '3px solid rgba(var(--semi-orange-4), 0.3)',
marginBottom: '24px',
}}
>
<AlertCircle size={56} color='var(--semi-color-warning)' />
</div>
</div>
{/* 标题区域 */}
<div style={{ marginBottom: '24px' }}>
<Title
heading={2}
style={{
color: 'var(--semi-color-text-0)',
<Title
heading={2}
style={{
color: 'var(--semi-color-text-0)',
margin: '0 0 12px 0',
fontSize: '28px',
fontWeight: '700'
fontWeight: '700',
}}
>
{t('模型部署服务未启用')}
</Title>
<Text
style={{
fontSize: '18px',
<Text
style={{
fontSize: '18px',
lineHeight: '1.6',
color: 'var(--semi-color-text-1)',
display: 'block'
display: 'block',
}}
>
{t('访问模型部署功能需要先启用 io.net 部署服务')}
......@@ -124,75 +128,99 @@ const DeploymentAccessGuard = ({
</div>
{/* 配置要求区域 */}
<div
style={{
backgroundColor: 'var(--semi-color-bg-1)',
padding: '24px',
<div
style={{
backgroundColor: 'var(--semi-color-bg-1)',
padding: '24px',
borderRadius: '12px',
border: '1px solid var(--semi-color-border)',
margin: '32px 0',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)'
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
marginBottom: '16px'
}}>
<div style={{
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
borderRadius: '8px',
backgroundColor: 'rgba(var(--semi-blue-4), 0.15)'
}}>
<Server size={20} color="var(--semi-color-primary)" />
gap: '12px',
marginBottom: '16px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
borderRadius: '8px',
backgroundColor: 'rgba(var(--semi-blue-4), 0.15)',
}}
>
<Server size={20} color='var(--semi-color-primary)' />
</div>
<Text
strong
style={{
fontSize: '16px',
color: 'var(--semi-color-text-0)'
<Text
strong
style={{
fontSize: '16px',
color: 'var(--semi-color-text-0)',
}}
>
{t('需要配置的项目')}
</Text>
</div>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
alignItems: 'flex-start',
textAlign: 'left',
maxWidth: '320px',
margin: '0 auto'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: 'var(--semi-color-primary)',
flexShrink: 0
}}></div>
<Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
alignItems: 'flex-start',
textAlign: 'left',
maxWidth: '320px',
margin: '0 auto',
}}
>
<div
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
>
<div
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: 'var(--semi-color-primary)',
flexShrink: 0,
}}
></div>
<Text
style={{
fontSize: '15px',
color: 'var(--semi-color-text-1)',
}}
>
{t('启用 io.net 部署开关')}
</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: 'var(--semi-color-primary)',
flexShrink: 0
}}></div>
<Text style={{ fontSize: '15px', color: 'var(--semi-color-text-1)' }}>
<div
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
>
<div
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: 'var(--semi-color-primary)',
flexShrink: 0,
}}
></div>
<Text
style={{
fontSize: '15px',
color: 'var(--semi-color-text-1)',
}}
>
{t('配置有效的 io.net API Key')}
</Text>
</div>
......@@ -201,9 +229,9 @@ const DeploymentAccessGuard = ({
{/* 操作链接区域 */}
<div style={{ marginBottom: '20px' }}>
<div
<div
onClick={handleGoToSettings}
style={{
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
......@@ -216,17 +244,18 @@ const DeploymentAccessGuard = ({
background: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-border)',
transition: 'all 0.2s ease',
textDecoration: 'none'
textDecoration: 'none',
}}
onMouseEnter={(e) => {
e.target.style.background = 'var(--semi-color-fill-1)';
e.target.style.transform = 'translateY(-1px)';
e.target.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
e.currentTarget.style.background = 'var(--semi-color-fill-1)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow =
'0 2px 8px rgba(0, 0, 0, 0.1)';
}}
onMouseLeave={(e) => {
e.target.style.background = 'var(--semi-color-fill-0)';
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = 'none';
e.currentTarget.style.background = 'var(--semi-color-fill-0)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<Settings size={18} />
......@@ -235,12 +264,12 @@ const DeploymentAccessGuard = ({
</div>
{/* 底部提示 */}
<Text
type="tertiary"
style={{
<Text
type='tertiary'
style={{
fontSize: '14px',
color: 'var(--semi-color-text-2)',
lineHeight: '1.5'
lineHeight: '1.5',
}}
>
{t('配置完成后刷新页面即可使用模型部署功能')}
......@@ -256,7 +285,7 @@ const DeploymentAccessGuard = ({
<div className='mt-[60px] px-2'>
<Card loading={true} style={{ minHeight: '400px' }}>
<div style={{ textAlign: 'center', padding: '50px 0' }}>
<Text type="secondary">{t('Checking io.net connection...')}</Text>
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
</div>
</Card>
</div>
......@@ -265,12 +294,10 @@ const DeploymentAccessGuard = ({
if (connectionOk === false) {
const isExpired = connectionError?.type === 'expired';
const title = isExpired
? t('API key expired')
: t('io.net connection unavailable');
const title = isExpired ? t('接口密钥已过期') : t('无法连接 io.net');
const description = isExpired
? t('The current API key is expired. Please update it in settings.')
: t('Unable to connect to io.net with the current configuration.');
? t('当前 API 密钥已过期,请在设置中更新。')
: t('当前配置无法连接到 io.net。');
const detail = connectionError?.message || '';
return (
......@@ -297,7 +324,8 @@ const DeploymentAccessGuard = ({
borderRadius: '16px',
border: '1px solid var(--semi-color-border)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
background: 'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
background:
'linear-gradient(135deg, var(--semi-color-bg-0) 0%, var(--semi-color-fill-0) 100%)',
}}
>
<div style={{ marginBottom: '32px' }}>
......@@ -309,12 +337,13 @@ const DeploymentAccessGuard = ({
width: '120px',
height: '120px',
borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)',
background:
'linear-gradient(135deg, rgba(var(--semi-red-4), 0.15) 0%, rgba(var(--semi-red-5), 0.1) 100%)',
border: '3px solid rgba(var(--semi-red-4), 0.3)',
marginBottom: '24px',
}}
>
<WifiOff size={56} color="var(--semi-color-danger)" />
<WifiOff size={56} color='var(--semi-color-danger)' />
</div>
</div>
......@@ -342,7 +371,7 @@ const DeploymentAccessGuard = ({
</Text>
{detail ? (
<Text
type="tertiary"
type='tertiary'
style={{
fontSize: '14px',
lineHeight: '1.5',
......@@ -355,13 +384,19 @@ const DeploymentAccessGuard = ({
) : null}
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<Button type="primary" icon={<Settings size={18} />} onClick={handleGoToSettings}>
{t('Go to settings')}
<div
style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}
>
<Button
type='primary'
icon={<Settings size={18} />}
onClick={handleGoToSettings}
>
{t('前往设置')}
</Button>
{onRetry ? (
<Button type="tertiary" onClick={onRetry}>
{t('Retry connection')}
<Button type='tertiary' onClick={onRetry}>
{t('重试连接')}
</Button>
) : null}
</div>
......
......@@ -44,7 +44,10 @@ import CodeViewer from '../../../playground/CodeViewer';
import { StatusContext } from '../../../../context/Status';
import { UserContext } from '../../../../context/User';
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
import { useSidebar } from '../../../../hooks/common/useSidebar';
import {
mergeAdminConfig,
useSidebar,
} from '../../../../hooks/common/useSidebar';
const NotificationSettings = ({
t,
......@@ -82,6 +85,7 @@ const NotificationSettings = ({
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
......@@ -164,6 +168,7 @@ const NotificationSettings = ({
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
......@@ -178,14 +183,27 @@ const NotificationSettings = ({
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
try {
const adminConf = JSON.parse(
statusState.status.SidebarModulesAdmin,
);
setAdminConfig(mergeAdminConfig(adminConf));
} catch (error) {
setAdminConfig(mergeAdminConfig(null));
}
} else {
setAdminConfig(mergeAdminConfig(null));
}
// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
const userConf = JSON.parse(userRes.data.data.sidebar_modules);
let userConf;
if (typeof userRes.data.data.sidebar_modules === 'string') {
userConf = JSON.parse(userRes.data.data.sidebar_modules);
} else {
userConf = userRes.data.data.sidebar_modules;
}
setSidebarModulesUser(userConf);
}
} catch (error) {
......@@ -274,6 +292,11 @@ const NotificationSettings = ({
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{
key: 'deployment',
title: t('模型部署'),
description: t('模型部署管理'),
},
{
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
......@@ -812,7 +835,9 @@ const NotificationSettings = ({
</Typography.Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
checked={
sidebarModulesUser[section.key]?.enabled !== false
}
onChange={handleSectionChange(section.key)}
size='default'
/>
......@@ -835,7 +860,8 @@ const NotificationSettings = ({
>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled
sidebarModulesUser[section.key]?.enabled !==
false
? ''
: 'opacity-50'
}`}
......@@ -866,7 +892,7 @@ const NotificationSettings = ({
checked={
sidebarModulesUser[section.key]?.[
module.key
]
] !== false
}
onChange={handleModuleChange(
section.key,
......@@ -874,8 +900,8 @@ const NotificationSettings = ({
)}
size='default'
disabled={
!sidebarModulesUser[section.key]
?.enabled
sidebarModulesUser[section.key]
?.enabled === false
}
/>
</div>
......
......@@ -30,30 +30,24 @@ import {
Spin,
Popconfirm,
Tag,
Avatar,
Empty,
Divider,
Row,
Col,
Progress,
Checkbox,
Radio,
} from '@douyinfe/semi-ui';
import {
IconClose,
IconDownload,
IconDelete,
IconRefresh,
IconSearch,
IconPlus,
IconServer,
} from '@douyinfe/semi-icons';
import {
API,
authHeader,
getUserIdFromLocalStorage,
showError,
showInfo,
showSuccess,
} from '../../../../helpers';
......@@ -85,9 +79,7 @@ const resolveOllamaBaseUrl = (info) => {
}
const alt =
typeof info.ollama_base_url === 'string'
? info.ollama_base_url.trim()
: '';
typeof info.ollama_base_url === 'string' ? info.ollama_base_url.trim() : '';
if (alt) {
return alt;
}
......@@ -125,7 +117,8 @@ const normalizeModels = (items) => {
}
if (typeof item === 'object') {
const candidateId = item.id || item.ID || item.name || item.model || item.Model;
const candidateId =
item.id || item.ID || item.name || item.model || item.Model;
if (!candidateId) {
return null;
}
......@@ -147,7 +140,10 @@ const normalizeModels = (items) => {
if (!normalized.digest && typeof metadata.digest === 'string') {
normalized.digest = metadata.digest;
}
if (!normalized.modified_at && typeof metadata.modified_at === 'string') {
if (
!normalized.modified_at &&
typeof metadata.modified_at === 'string'
) {
normalized.modified_at = metadata.modified_at;
}
if (metadata.details && !normalized.details) {
......@@ -440,7 +436,6 @@ const OllamaModelModal = ({
};
await processStream();
} catch (error) {
if (error?.name !== 'AbortError') {
showError(t('模型拉取失败: {{error}}', { error: error.message }));
......@@ -461,7 +456,7 @@ const OllamaModelModal = ({
model_name: modelName,
},
});
if (res.data.success) {
showSuccess(t('模型删除成功'));
await fetchModels(); // 重新获取模型列表
......@@ -481,8 +476,8 @@ const OllamaModelModal = ({
if (!searchValue) {
setFilteredModels(models);
} else {
const filtered = models.filter(model =>
model.id.toLowerCase().includes(searchValue.toLowerCase())
const filtered = models.filter((model) =>
model.id.toLowerCase().includes(searchValue.toLowerCase()),
);
setFilteredModels(filtered);
}
......@@ -527,60 +522,38 @@ const OllamaModelModal = ({
const formatModelSize = (size) => {
if (!size) return '-';
const gb = size / (1024 * 1024 * 1024);
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(size / (1024 * 1024)).toFixed(0)} MB`;
return gb >= 1
? `${gb.toFixed(1)} GB`
: `${(size / (1024 * 1024)).toFixed(0)} MB`;
};
return (
<Modal
title={
<div className='flex items-center'>
<Avatar
size='small'
color='blue'
className='mr-3 shadow-md'
>
<IconServer size={16} />
</Avatar>
<div>
<Title heading={4} className='m-0'>
{t('Ollama 模型管理')}
</Title>
<Text type='tertiary' size='small'>
{channelInfo?.name && `${channelInfo.name} - `}
{t('管理 Ollama 模型的拉取和删除')}
</Text>
</div>
</div>
}
title={t('Ollama 模型管理')}
visible={visible}
onCancel={onCancel}
width={800}
width={720}
style={{ maxWidth: '95vw' }}
footer={
<div className='flex justify-end'>
<Button
theme='light'
type='primary'
onClick={onCancel}
icon={<IconClose />}
>
{t('关闭')}
</Button>
</div>
<Button theme='solid' type='primary' onClick={onCancel}>
{t('关闭')}
</Button>
}
>
<div className='space-y-6'>
<Space vertical spacing='medium' style={{ width: '100%' }}>
<div>
<Text type='tertiary' size='small'>
{channelInfo?.name ? `${channelInfo.name} - ` : ''}
{t('管理 Ollama 模型的拉取和删除')}
</Text>
</div>
{/* 拉取新模型 */}
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center mb-4'>
<Avatar size='small' color='green' className='mr-2'>
<IconPlus size={16} />
</Avatar>
<Title heading={5} className='m-0'>
{t('拉取新模型')}
</Title>
</div>
<Card>
<Title heading={6} className='m-0 mb-3'>
{t('拉取新模型')}
</Title>
<Row gutter={12} align='middle'>
<Col span={16}>
<Input
......@@ -606,76 +579,81 @@ const OllamaModelModal = ({
</Button>
</Col>
</Row>
{/* 进度条显示 */}
{pullProgress && (() => {
const completedBytes = Number(pullProgress.completed) || 0;
const totalBytes = Number(pullProgress.total) || 0;
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
const safePercent = hasTotal
? Math.min(
100,
Math.max(0, Math.round((completedBytes / totalBytes) * 100)),
)
: null;
const percentText = hasTotal && safePercent !== null
? `${safePercent.toFixed(0)}%`
: pullProgress.status || t('处理中');
return (
<div className='mt-3 p-3 bg-gray-50 rounded-lg'>
<div className='flex items-center justify-between mb-2'>
<Text strong>{t('拉取进度')}</Text>
<Text type='tertiary' size='small'>{percentText}</Text>
</div>
{pullProgress &&
(() => {
const completedBytes = Number(pullProgress.completed) || 0;
const totalBytes = Number(pullProgress.total) || 0;
const hasTotal = Number.isFinite(totalBytes) && totalBytes > 0;
const safePercent = hasTotal
? Math.min(
100,
Math.max(
0,
Math.round((completedBytes / totalBytes) * 100),
),
)
: null;
const percentText =
hasTotal && safePercent !== null
? `${safePercent.toFixed(0)}%`
: pullProgress.status || t('处理中');
return (
<div style={{ marginTop: 12 }}>
<div className='flex items-center justify-between mb-2'>
<Text strong>{t('拉取进度')}</Text>
<Text type='tertiary' size='small'>
{percentText}
</Text>
</div>
{hasTotal && safePercent !== null ? (
<div>
<Progress
percent={safePercent}
showInfo={false}
stroke='#1890ff'
size='small'
/>
<div className='flex justify-between mt-1'>
<Text type='tertiary' size='small'>
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
</Text>
<Text type='tertiary' size='small'>
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
</Text>
{hasTotal && safePercent !== null ? (
<div>
<Progress
percent={safePercent}
showInfo={false}
stroke='#1890ff'
size='small'
/>
<div className='flex justify-between mt-1'>
<Text type='tertiary' size='small'>
{(completedBytes / (1024 * 1024 * 1024)).toFixed(2)}{' '}
GB
</Text>
<Text type='tertiary' size='small'>
{(totalBytes / (1024 * 1024 * 1024)).toFixed(2)} GB
</Text>
</div>
</div>
</div>
) : (
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
<Spin size='small' />
<span>{t('准备中...')}</span>
</div>
)}
</div>
);
})()}
) : (
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
<Spin size='small' />
<span>{t('准备中...')}</span>
</div>
)}
</div>
);
})()}
<Text type='tertiary' size='small' className='mt-2 block'>
{t('支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间')}
{t(
'支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间',
)}
</Text>
</Card>
{/* 已有模型列表 */}
<Card className='!rounded-2xl shadow-sm border-0'>
<div className='flex items-center justify-between mb-4'>
<div className='flex items-center'>
<Avatar size='small' color='purple' className='mr-2'>
<IconServer size={16} />
</Avatar>
<Title heading={5} className='m-0'>
<Card>
<div className='flex items-center justify-between mb-3'>
<div className='flex items-center gap-2'>
<Title heading={6} className='m-0'>
{t('已有模型')}
{models.length > 0 && (
<Tag color='blue' className='ml-2'>
{models.length}
</Tag>
)}
</Title>
{models.length > 0 ? (
<Tag color='blue'>{models.length}</Tag>
) : null}
</div>
<Space wrap>
<Input
......@@ -688,7 +666,7 @@ const OllamaModelModal = ({
/>
<Button
size='small'
theme='borderless'
theme='light'
onClick={handleSelectAll}
disabled={models.length === 0}
>
......@@ -696,7 +674,7 @@ const OllamaModelModal = ({
</Button>
<Button
size='small'
theme='borderless'
theme='light'
onClick={handleClearSelection}
disabled={selectedModelIds.length === 0}
>
......@@ -728,11 +706,10 @@ const OllamaModelModal = ({
<Spin spinning={loading}>
{filteredModels.length === 0 ? (
<Empty
image={<IconServer size={60} />}
title={searchValue ? t('未找到匹配的模型') : t('暂无模型')}
description={
searchValue
? t('请尝试其他搜索关键词')
searchValue
? t('请尝试其他搜索关键词')
: t('您可以在上方拉取需要的模型')
}
style={{ padding: '40px 0' }}
......@@ -740,25 +717,17 @@ const OllamaModelModal = ({
) : (
<List
dataSource={filteredModels}
split={false}
renderItem={(model, index) => (
<List.Item
key={model.id}
className='hover:bg-gray-50 rounded-lg p-3 transition-colors'
>
split
renderItem={(model) => (
<List.Item key={model.id}>
<div className='flex items-center justify-between w-full'>
<div className='flex items-center flex-1 min-w-0 gap-3'>
<Checkbox
checked={selectedModelIds.includes(model.id)}
onChange={(checked) => handleToggleModel(model.id, checked)}
onChange={(checked) =>
handleToggleModel(model.id, checked)
}
/>
<Avatar
size='small'
color='blue'
className='flex-shrink-0'
>
{model.id.charAt(0).toUpperCase()}
</Avatar>
<div className='flex-1 min-w-0'>
<Text strong className='block truncate'>
{model.id}
......@@ -775,10 +744,13 @@ const OllamaModelModal = ({
</div>
</div>
</div>
<div className='flex items-center space-x-2 ml-4'>
<div className='flex items-center space-x-2 ml-4'>
<Popconfirm
title={t('确认删除模型')}
content={t('删除后无法恢复,确定要删除模型 "{{name}}" 吗?', { name: model.id })}
content={t(
'删除后无法恢复,确定要删除模型 "{{name}}" 吗?',
{ name: model.id },
)}
onConfirm={() => deleteModel(model.id)}
okText={t('确认')}
cancelText={t('取消')}
......@@ -798,7 +770,7 @@ const OllamaModelModal = ({
)}
</Spin>
</Card>
</div>
</Space>
</Modal>
);
};
......
......@@ -27,13 +27,14 @@ const DeploymentsActions = ({
setEditingDeployment,
setShowEdit,
batchDeleteDeployments,
batchOperationsEnabled = true,
compactMode,
setCompactMode,
showCreateModal,
setShowCreateModal,
t,
}) => {
const hasSelected = selectedKeys.length > 0;
const hasSelected = batchOperationsEnabled && selectedKeys.length > 0;
const handleAddDeployment = () => {
if (setShowCreateModal) {
......@@ -53,7 +54,6 @@ const DeploymentsActions = ({
setSelectedKeys([]);
};
return (
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
<Button
......
......@@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Button,
Dropdown,
Tag,
Typography,
} from '@douyinfe/semi-ui';
import {
timestamp2string,
showSuccess,
showError,
} from '../../../helpers';
import { Button, Dropdown, Tag, Typography } from '@douyinfe/semi-ui';
import { timestamp2string, showSuccess, showError } from '../../../helpers';
import { IconMore } from '@douyinfe/semi-icons';
import {
FaPlay,
......@@ -50,7 +41,6 @@ import {
FaHourglassHalf,
FaGlobe,
} from 'react-icons/fa';
import {t} from "i18next";
const normalizeStatus = (status) =>
typeof status === 'string' ? status.trim().toLowerCase() : '';
......@@ -58,59 +48,59 @@ const normalizeStatus = (status) =>
const STATUS_TAG_CONFIG = {
running: {
color: 'green',
label: t('运行中'),
labelKey: '运行中',
icon: <FaPlay size={12} className='text-green-600' />,
},
deploying: {
color: 'blue',
label: t('部署中'),
labelKey: '部署中',
icon: <FaSpinner size={12} className='text-blue-600' />,
},
pending: {
color: 'orange',
label: t('待部署'),
labelKey: '待部署',
icon: <FaClock size={12} className='text-orange-600' />,
},
stopped: {
color: 'grey',
label: t('已停止'),
labelKey: '已停止',
icon: <FaStop size={12} className='text-gray-500' />,
},
error: {
color: 'red',
label: t('错误'),
labelKey: '错误',
icon: <FaExclamationCircle size={12} className='text-red-500' />,
},
failed: {
color: 'red',
label: t('失败'),
labelKey: '失败',
icon: <FaExclamationCircle size={12} className='text-red-500' />,
},
destroyed: {
color: 'red',
label: t('已销毁'),
labelKey: '已销毁',
icon: <FaBan size={12} className='text-red-500' />,
},
completed: {
color: 'green',
label: t('已完成'),
labelKey: '已完成',
icon: <FaCheckCircle size={12} className='text-green-600' />,
},
'deployment requested': {
color: 'blue',
label: t('部署请求中'),
labelKey: '部署请求中',
icon: <FaSpinner size={12} className='text-blue-600' />,
},
'termination requested': {
color: 'orange',
label: t('终止请求中'),
labelKey: '终止请求中',
icon: <FaClock size={12} className='text-orange-600' />,
},
};
const DEFAULT_STATUS_CONFIG = {
color: 'grey',
label: null,
labelKey: null,
icon: <FaInfoCircle size={12} className='text-gray-500' />,
};
......@@ -190,7 +180,9 @@ const renderStatus = (status, t) => {
const normalizedStatus = normalizeStatus(status);
const config = STATUS_TAG_CONFIG[normalizedStatus] || DEFAULT_STATUS_CONFIG;
const statusText = typeof status === 'string' ? status : '';
const labelText = config.label ? t(config.label) : statusText || t('未知状态');
const labelText = config.labelKey
? t(config.labelKey)
: statusText || t('未知状态');
return (
<Tag
......@@ -206,20 +198,24 @@ const renderStatus = (status, t) => {
// Container Name Cell Component - to properly handle React hooks
const ContainerNameCell = ({ text, record, t }) => {
const handleCopyId = () => {
navigator.clipboard.writeText(record.id);
showSuccess(t('ID已复制到剪贴板'));
const handleCopyId = async () => {
try {
await navigator.clipboard.writeText(record.id);
showSuccess(t('已复制 ID 到剪贴板'));
} catch (err) {
showError(t('复制失败'));
}
};
return (
<div className="flex flex-col gap-1">
<Typography.Text strong className="text-base">
<div className='flex flex-col gap-1'>
<Typography.Text strong className='text-base'>
{text}
</Typography.Text>
<Typography.Text
type="secondary"
size="small"
className="text-xs cursor-pointer hover:text-blue-600 transition-colors select-all"
<Typography.Text
type='secondary'
size='small'
className='text-xs cursor-pointer hover:text-blue-600 transition-colors select-all'
onClick={handleCopyId}
title={t('点击复制ID')}
>
......@@ -232,26 +228,26 @@ const ContainerNameCell = ({ text, record, t }) => {
// Render resource configuration
const renderResourceConfig = (resource, t) => {
if (!resource) return '-';
const { cpu, memory, gpu } = resource;
return (
<div className="flex flex-col gap-1">
<div className='flex flex-col gap-1'>
{cpu && (
<div className="flex items-center gap-1 text-xs">
<FaMicrochip className="text-blue-500" />
<div className='flex items-center gap-1 text-xs'>
<FaMicrochip className='text-blue-500' />
<span>CPU: {cpu}</span>
</div>
)}
{memory && (
<div className="flex items-center gap-1 text-xs">
<FaMemory className="text-green-500" />
<div className='flex items-center gap-1 text-xs'>
<FaMemory className='text-green-500' />
<span>内存: {memory}</span>
</div>
)}
{gpu && (
<div className="flex items-center gap-1 text-xs">
<FaServer className="text-purple-500" />
<div className='flex items-center gap-1 text-xs'>
<FaServer className='text-purple-500' />
<span>GPU: {gpu}</span>
</div>
)}
......@@ -266,7 +262,7 @@ const renderInstanceCount = (count, record, t) => {
const countColor = statusConfig?.color ?? 'grey';
return (
<Tag color={countColor} size="small" shape='circle'>
<Tag color={countColor} size='small' shape='circle'>
{count || 0} {t('个实例')}
</Tag>
);
......@@ -299,11 +295,7 @@ export const getDeploymentsColumns = ({
width: 300,
ellipsis: true,
render: (text, record) => (
<ContainerNameCell
text={text}
record={record}
t={t}
/>
<ContainerNameCell text={text} record={record} t={t} />
),
},
{
......@@ -312,9 +304,7 @@ export const getDeploymentsColumns = ({
key: COLUMN_KEYS.status,
width: 140,
render: (status) => (
<div className="flex items-center gap-2">
{renderStatus(status, t)}
</div>
<div className='flex items-center gap-2'>{renderStatus(status, t)}</div>
),
},
{
......@@ -325,18 +315,22 @@ export const getDeploymentsColumns = ({
render: (provider) =>
provider ? (
<div
className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide"
className='flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide'
style={{
borderColor: 'rgba(59, 130, 246, 0.4)',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
color: '#2563eb',
}}
>
<FaGlobe className="text-[11px]" />
<FaGlobe className='text-[11px]' />
<span>{provider}</span>
</div>
) : (
<Typography.Text type="tertiary" size="small" className="text-xs text-gray-500">
<Typography.Text
type='tertiary'
size='small'
className='text-xs text-gray-500'
>
{t('暂无')}
</Typography.Text>
),
......@@ -345,7 +339,7 @@ export const getDeploymentsColumns = ({
title: t('剩余时间'),
dataIndex: 'time_remaining',
key: COLUMN_KEYS.time_remaining,
width: 140,
width: 200,
render: (text, record) => {
const normalizedStatus = normalizeStatus(record?.status);
const percentUsedRaw = parsePercentValue(record?.completed_percent);
......@@ -380,43 +374,43 @@ export const getDeploymentsColumns = ({
percentRemaining !== null;
return (
<div className="flex flex-col gap-1 leading-tight text-xs">
<div className="flex items-center gap-1.5">
<div className='flex flex-col gap-1 leading-tight text-xs'>
<div className='flex items-center gap-1.5'>
<FaHourglassHalf
className="text-sm"
className='text-sm'
style={{ color: theme.iconColor }}
/>
<Typography.Text className="text-sm font-medium text-[var(--semi-color-text-0)]">
<Typography.Text className='text-sm font-medium text-[var(--semi-color-text-0)]'>
{timeDisplay}
</Typography.Text>
{showProgress && percentRemaining !== null ? (
<Tag size="small" color={theme.tagColor}>
<Tag size='small' color={theme.tagColor}>
{percentRemaining}%
</Tag>
) : statusOverride ? (
<Tag size="small" color="grey">
<Tag size='small' color='grey'>
{statusOverride}
</Tag>
) : null}
</div>
{showExtraInfo && (
<div className="flex items-center gap-3 text-[var(--semi-color-text-2)]">
<div className='flex items-center gap-3 text-[var(--semi-color-text-2)]'>
{humanReadable && (
<span className="flex items-center gap-1">
<FaClock className="text-[11px]" />
<span className='flex items-center gap-1'>
<FaClock className='text-[11px]' />
{t('约')} {humanReadable}
</span>
)}
{percentUsed !== null && (
<span className="flex items-center gap-1">
<FaCheckCircle className="text-[11px]" />
<span className='flex items-center gap-1'>
<FaCheckCircle className='text-[11px]' />
{t('已用')} {percentUsed}%
</span>
)}
</div>
)}
{showProgress && showRemainingMeta && (
<div className="text-[10px]" style={{ color: theme.textColor }}>
<div className='text-[10px]' style={{ color: theme.textColor }}>
{t('剩余')} {record.compute_minutes_remaining} {t('分钟')}
</div>
)}
......@@ -431,14 +425,16 @@ export const getDeploymentsColumns = ({
width: 220,
ellipsis: true,
render: (text, record) => (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md">
<FaServer className="text-green-600 text-xs" />
<span className="text-xs font-medium text-green-700">
<div className='flex items-center gap-2'>
<div className='flex items-center gap-1 px-2 py-1 bg-green-50 border border-green-200 rounded-md'>
<FaServer className='text-green-600 text-xs' />
<span className='text-xs font-medium text-green-700'>
{record.hardware_name}
</span>
</div>
<span className="text-xs text-gray-500 font-medium">x{record.hardware_quantity}</span>
<span className='text-xs text-gray-500 font-medium'>
x{record.hardware_quantity}
</span>
</div>
),
},
......@@ -448,7 +444,7 @@ export const getDeploymentsColumns = ({
key: COLUMN_KEYS.created_at,
width: 150,
render: (text) => (
<span className="text-sm text-gray-600">{timestamp2string(text)}</span>
<span className='text-sm text-gray-600'>{timestamp2string(text)}</span>
),
},
{
......@@ -459,7 +455,8 @@ export const getDeploymentsColumns = ({
render: (_, record) => {
const { status, id } = record;
const normalizedStatus = normalizeStatus(status);
const isEnded = normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
const isEnded =
normalizedStatus === 'completed' || normalizedStatus === 'destroyed';
const handleDelete = () => {
// Use enhanced confirmation dialog
......@@ -471,7 +468,7 @@ export const getDeploymentsColumns = ({
switch (normalizedStatus) {
case 'running':
return {
icon: <FaInfoCircle className="text-xs" />,
icon: <FaInfoCircle className='text-xs' />,
text: t('查看详情'),
onClick: () => onViewDetails?.(record),
type: 'secondary',
......@@ -480,7 +477,7 @@ export const getDeploymentsColumns = ({
case 'failed':
case 'error':
return {
icon: <FaPlay className="text-xs" />,
icon: <FaPlay className='text-xs' />,
text: t('重试'),
onClick: () => startDeployment(id),
type: 'primary',
......@@ -488,7 +485,7 @@ export const getDeploymentsColumns = ({
};
case 'stopped':
return {
icon: <FaPlay className="text-xs" />,
icon: <FaPlay className='text-xs' />,
text: t('启动'),
onClick: () => startDeployment(id),
type: 'primary',
......@@ -497,7 +494,7 @@ export const getDeploymentsColumns = ({
case 'deployment requested':
case 'deploying':
return {
icon: <FaClock className="text-xs" />,
icon: <FaClock className='text-xs' />,
text: t('部署中'),
onClick: () => {},
type: 'secondary',
......@@ -506,7 +503,7 @@ export const getDeploymentsColumns = ({
};
case 'pending':
return {
icon: <FaClock className="text-xs" />,
icon: <FaClock className='text-xs' />,
text: t('待部署'),
onClick: () => {},
type: 'secondary',
......@@ -515,7 +512,7 @@ export const getDeploymentsColumns = ({
};
case 'termination requested':
return {
icon: <FaClock className="text-xs" />,
icon: <FaClock className='text-xs' />,
text: t('终止中'),
onClick: () => {},
type: 'secondary',
......@@ -526,7 +523,7 @@ export const getDeploymentsColumns = ({
case 'destroyed':
default:
return {
icon: <FaInfoCircle className="text-xs" />,
icon: <FaInfoCircle className='text-xs' />,
text: t('已结束'),
onClick: () => {},
type: 'tertiary',
......@@ -542,13 +539,13 @@ export const getDeploymentsColumns = ({
if (isEnded) {
return (
<div className="flex w-full items-center justify-start gap-1 pr-2">
<div className='flex w-full items-center justify-start gap-1 pr-2'>
<Button
size="small"
type="tertiary"
theme="borderless"
size='small'
type='tertiary'
theme='borderless'
onClick={() => onViewDetails?.(record)}
icon={<FaInfoCircle className="text-xs" />}
icon={<FaInfoCircle className='text-xs' />}
>
{t('查看详情')}
</Button>
......@@ -558,14 +555,22 @@ export const getDeploymentsColumns = ({
// All actions dropdown with enhanced operations
const dropdownItems = [
<Dropdown.Item key="details" onClick={() => onViewDetails?.(record)} icon={<FaInfoCircle />}>
<Dropdown.Item
key='details'
onClick={() => onViewDetails?.(record)}
icon={<FaInfoCircle />}
>
{t('查看详情')}
</Dropdown.Item>,
];
if (!isEnded) {
dropdownItems.push(
<Dropdown.Item key="logs" onClick={() => onViewLogs?.(record)} icon={<FaTerminal />}>
<Dropdown.Item
key='logs'
onClick={() => onViewLogs?.(record)}
icon={<FaTerminal />}
>
{t('查看日志')}
</Dropdown.Item>,
);
......@@ -575,7 +580,11 @@ export const getDeploymentsColumns = ({
if (normalizedStatus === 'running') {
if (onSyncToChannel) {
managementItems.push(
<Dropdown.Item key="sync-channel" onClick={() => onSyncToChannel(record)} icon={<FaLink />}>
<Dropdown.Item
key='sync-channel'
onClick={() => onSyncToChannel(record)}
icon={<FaLink />}
>
{t('同步到渠道')}
</Dropdown.Item>,
);
......@@ -583,28 +592,44 @@ export const getDeploymentsColumns = ({
}
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
managementItems.push(
<Dropdown.Item key="retry" onClick={() => startDeployment(id)} icon={<FaPlay />}>
<Dropdown.Item
key='retry'
onClick={() => startDeployment(id)}
icon={<FaPlay />}
>
{t('重试')}
</Dropdown.Item>,
);
}
if (normalizedStatus === 'stopped') {
managementItems.push(
<Dropdown.Item key="start" onClick={() => startDeployment(id)} icon={<FaPlay />}>
<Dropdown.Item
key='start'
onClick={() => startDeployment(id)}
icon={<FaPlay />}
>
{t('启动')}
</Dropdown.Item>,
);
}
if (managementItems.length > 0) {
dropdownItems.push(<Dropdown.Divider key="management-divider" />);
dropdownItems.push(<Dropdown.Divider key='management-divider' />);
dropdownItems.push(...managementItems);
}
const configItems = [];
if (!isEnded && (normalizedStatus === 'running' || normalizedStatus === 'deployment requested')) {
if (
!isEnded &&
(normalizedStatus === 'running' ||
normalizedStatus === 'deployment requested')
) {
configItems.push(
<Dropdown.Item key="extend" onClick={() => onExtendDuration?.(record)} icon={<FaPlus />}>
<Dropdown.Item
key='extend'
onClick={() => onExtendDuration?.(record)}
icon={<FaPlus />}
>
{t('延长时长')}
</Dropdown.Item>,
);
......@@ -618,13 +643,18 @@ export const getDeploymentsColumns = ({
// }
if (configItems.length > 0) {
dropdownItems.push(<Dropdown.Divider key="config-divider" />);
dropdownItems.push(<Dropdown.Divider key='config-divider' />);
dropdownItems.push(...configItems);
}
if (!isEnded) {
dropdownItems.push(<Dropdown.Divider key="danger-divider" />);
dropdownItems.push(<Dropdown.Divider key='danger-divider' />);
dropdownItems.push(
<Dropdown.Item key="delete" type="danger" onClick={handleDelete} icon={<FaTrash />}>
<Dropdown.Item
key='delete'
type='danger'
onClick={handleDelete}
icon={<FaTrash />}
>
{t('销毁容器')}
</Dropdown.Item>,
);
......@@ -634,31 +664,31 @@ export const getDeploymentsColumns = ({
const hasDropdown = dropdownItems.length > 0;
return (
<div className="flex w-full items-center justify-start gap-1 pr-2">
<div className='flex w-full items-center justify-start gap-1 pr-2'>
<Button
size="small"
size='small'
theme={primaryTheme}
type={primaryType}
icon={primaryAction.icon}
onClick={primaryAction.onClick}
className="px-2 text-xs"
className='px-2 text-xs'
disabled={primaryAction.disabled}
>
{primaryAction.text}
</Button>
{hasDropdown && (
<Dropdown
trigger="click"
position="bottomRight"
trigger='click'
position='bottomRight'
render={allActions}
>
<Button
size="small"
theme="light"
type="tertiary"
size='small'
theme='light'
type='tertiary'
icon={<IconMore />}
className="px-1"
className='px-1'
/>
</Dropdown>
)}
......
......@@ -43,7 +43,8 @@ const DeploymentsTable = (deploymentsData) => {
deploymentCount,
compactMode,
visibleColumns,
setSelectedKeys,
rowSelection,
batchOperationsEnabled = true,
handlePageChange,
handlePageSizeChange,
handleRow,
......@@ -95,7 +96,10 @@ const DeploymentsTable = (deploymentsData) => {
};
const handleConfirmAction = () => {
if (selectedDeployment && confirmOperation === 'delete') {
if (
selectedDeployment &&
(confirmOperation === 'delete' || confirmOperation === 'destroy')
) {
deleteDeployment(selectedDeployment.id);
}
setShowConfirmDialog(false);
......@@ -179,11 +183,7 @@ const DeploymentsTable = (deploymentsData) => {
hidePagination={true}
expandAllRows={false}
onRow={handleRow}
rowSelection={{
onChange: (selectedRowKeys, selectedRows) => {
setSelectedKeys(selectedRows);
},
}}
rowSelection={batchOperationsEnabled ? rowSelection : undefined}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
......@@ -235,7 +235,7 @@ const DeploymentsTable = (deploymentsData) => {
onCancel={() => setShowConfirmDialog(false)}
onConfirm={handleConfirmAction}
title={t('确认操作')}
type="danger"
type='danger'
deployment={selectedDeployment}
operation={confirmOperation}
t={t}
......
......@@ -32,9 +32,10 @@ import { createCardProPagination } from '../../../helpers/utils';
const DeploymentsPage = () => {
const deploymentsData = useDeploymentsData();
const isMobile = useIsMobile();
// Create deployment modal state
const [showCreateModal, setShowCreateModal] = useState(false);
const batchOperationsEnabled = false;
const {
// Edit state
......@@ -81,7 +82,7 @@ const DeploymentsPage = () => {
visible={showEdit}
handleClose={closeEdit}
/>
<CreateDeploymentModal
visible={showCreateModal}
onCancel={() => setShowCreateModal(false)}
......@@ -109,6 +110,7 @@ const DeploymentsPage = () => {
setEditingDeployment={setEditingDeployment}
setShowEdit={setShowEdit}
batchDeleteDeployments={batchDeleteDeployments}
batchOperationsEnabled={batchOperationsEnabled}
compactMode={compactMode}
setCompactMode={setCompactMode}
showCreateModal={showCreateModal}
......@@ -138,7 +140,10 @@ const DeploymentsPage = () => {
})}
t={deploymentsData.t}
>
<DeploymentsTable {...deploymentsData} />
<DeploymentsTable
{...deploymentsData}
batchOperationsEnabled={batchOperationsEnabled}
/>
</CardPro>
</>
);
......
......@@ -38,7 +38,12 @@ import {
Tooltip,
Radio,
} from '@douyinfe/semi-ui';
import { IconPlus, IconMinus, IconHelpCircle, IconCopy } from '@douyinfe/semi-icons';
import {
IconPlus,
IconMinus,
IconHelpCircle,
IconCopy,
} from '@douyinfe/semi-icons';
import { API } from '../../../../helpers';
import { showError, showSuccess, copy } from '../../../../helpers';
......@@ -72,17 +77,17 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const [hardwareTotalAvailable, setHardwareTotalAvailable] = useState(null);
const [locations, setLocations] = useState([]);
const [locationTotalAvailable, setLocationTotalAvailable] = useState(null);
const [availableReplicas, setAvailableReplicas] = useState([]);
const [priceEstimation, setPriceEstimation] = useState(null);
// UI states
const [loadingHardware, setLoadingHardware] = useState(false);
const [loadingLocations, setLoadingLocations] = useState(false);
const [loadingReplicas, setLoadingReplicas] = useState(false);
const [loadingPrice, setLoadingPrice] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const [envVariables, setEnvVariables] = useState([{ key: '', value: '' }]);
const [secretEnvVariables, setSecretEnvVariables] = useState([{ key: '', value: '' }]);
const [secretEnvVariables, setSecretEnvVariables] = useState([
{ key: '', value: '' },
]);
const [entrypoint, setEntrypoint] = useState(['']);
const [args, setArgs] = useState(['']);
const [imageMode, setImageMode] = useState('builtin');
......@@ -95,7 +100,6 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const basicSectionRef = useRef(null);
const priceSectionRef = useRef(null);
const advancedSectionRef = useRef(null);
const locationRequestIdRef = useRef(0);
const replicaRequestIdRef = useRef(0);
const [formDefaults, setFormDefaults] = useState({
resource_private_name: '',
......@@ -143,6 +147,13 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
return map;
}, [locations]);
const getHardwareMaxGpus = (hardwareId) => {
if (!hardwareId) return 1;
const hardware = hardwareTypes.find((h) => h.id === hardwareId);
const maxGpus = Number(hardware?.max_gpus);
return Number.isFinite(maxGpus) && maxGpus > 0 ? maxGpus : 1;
};
// Form values for price calculation
const [selectedHardwareId, setSelectedHardwareId] = useState(null);
const [selectedLocationIds, setSelectedLocationIds] = useState([]);
......@@ -150,6 +161,20 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const [durationHours, setDurationHours] = useState(1);
const [replicaCount, setReplicaCount] = useState(1);
useEffect(() => {
if (!selectedHardwareId) {
return;
}
const nextMaxGpus = getHardwareMaxGpus(selectedHardwareId);
if (gpusPerContainer !== nextMaxGpus) {
setGpusPerContainer(nextMaxGpus);
}
if (formApi) {
formApi.setValue('gpus_per_container', nextMaxGpus);
}
}, [selectedHardwareId, hardwareTypes, formApi, gpusPerContainer]);
// Load initial data when modal opens
useEffect(() => {
if (visible) {
......@@ -206,10 +231,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
if (imageMode === 'builtin') {
if (prevMode === 'custom') {
if (formApi) {
customImageRef.current = formApi.getValue('image_url') || customImageRef.current;
customTrafficPortRef.current = formApi.getValue('traffic_port') ?? customTrafficPortRef.current;
customImageRef.current =
formApi.getValue('image_url') || customImageRef.current;
customTrafficPortRef.current =
formApi.getValue('traffic_port') ?? customTrafficPortRef.current;
}
customSecretEnvRef.current = secretEnvVariables.map((item) => ({ ...item }));
customSecretEnvRef.current = secretEnvVariables.map((item) => ({
...item,
}));
customEnvRef.current = envVariables.map((item) => ({ ...item }));
}
const newKey = generateRandomKey();
......@@ -273,15 +302,12 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
return;
}
if (selectedHardwareId) {
loadLocations(selectedHardwareId);
return;
} else {
setLocations([]);
setSelectedLocationIds([]);
setAvailableReplicas([]);
setLocationTotalAvailable(null);
setLoadingLocations(false);
setLoadingReplicas(false);
locationRequestIdRef.current = 0;
replicaRequestIdRef.current = 0;
if (formApi) {
formApi.setValue('location_ids', []);
......@@ -299,7 +325,6 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
setDurationHours(1);
setReplicaCount(1);
setPriceEstimation(null);
setAvailableReplicas([]);
setLocations([]);
setLocationTotalAvailable(null);
setHardwareTotalAvailable(null);
......@@ -336,11 +361,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
setLoadingHardware(true);
const response = await API.get('/api/deployments/hardware-types');
if (response.data.success) {
const { hardware_types: hardwareList = [], total_available } = response.data.data || {};
const { hardware_types: hardwareList = [], total_available } =
response.data.data || {};
const normalizedHardware = hardwareList.map((hardware) => {
const availableCountValue = Number(hardware.available_count);
const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue;
const availableCount = Number.isNaN(availableCountValue)
? 0
: availableCountValue;
const availableBool =
typeof hardware.available === 'boolean'
? hardware.available
......@@ -355,7 +383,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const providedTotal = Number(total_available);
const fallbackTotal = normalizedHardware.reduce(
(acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
(acc, item) =>
acc +
(Number.isNaN(item.available_count) ? 0 : item.available_count),
0,
);
const hasProvidedTotal =
......@@ -378,81 +408,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
}
};
const loadLocations = async (hardwareId) => {
if (!hardwareId) {
setLocations([]);
setLocationTotalAvailable(null);
return;
}
const requestId = Date.now();
locationRequestIdRef.current = requestId;
setLoadingLocations(true);
setLocations([]);
setLocationTotalAvailable(null);
try {
const response = await API.get('/api/deployments/locations', {
params: { hardware_id: hardwareId },
});
if (locationRequestIdRef.current !== requestId) {
return;
}
if (response.data.success) {
const { locations: locationsList = [], total } =
response.data.data || {};
const normalizedLocations = locationsList.map((location) => {
const iso2 = (location.iso2 || '').toString().toUpperCase();
const availableValue = Number(location.available);
const available = Number.isNaN(availableValue) ? 0 : availableValue;
return {
...location,
iso2,
available,
};
});
const providedTotal = Number(total);
const fallbackTotal = normalizedLocations.reduce(
(acc, item) =>
acc + (Number.isNaN(item.available) ? 0 : item.available),
0,
);
const hasProvidedTotal =
total !== undefined &&
total !== null &&
total !== '' &&
!Number.isNaN(providedTotal);
setLocations(normalizedLocations);
setLocationTotalAvailable(
hasProvidedTotal ? providedTotal : fallbackTotal,
);
} else {
showError(t('获取部署位置失败: ') + response.data.message);
setLocations([]);
setLocationTotalAvailable(null);
}
} catch (error) {
if (locationRequestIdRef.current === requestId) {
showError(t('获取部署位置失败: ') + error.message);
setLocations([]);
setLocationTotalAvailable(null);
}
} finally {
if (locationRequestIdRef.current === requestId) {
setLoadingLocations(false);
}
}
};
const loadAvailableReplicas = async (hardwareId, gpuCount) => {
if (!hardwareId || !gpuCount) {
setAvailableReplicas([]);
setLocations([]);
setLocationTotalAvailable(null);
setLoadingReplicas(false);
return;
......@@ -461,7 +419,8 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const requestId = Date.now();
replicaRequestIdRef.current = requestId;
setLoadingReplicas(true);
setAvailableReplicas([]);
setLocations([]);
setLocationTotalAvailable(null);
try {
const response = await API.get(
......@@ -474,24 +433,67 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
if (response.data.success) {
const replicasList = response.data.data?.replicas || [];
const filteredReplicas = replicasList.filter(
(replica) => (replica.available_count || 0) > 0,
);
setAvailableReplicas(filteredReplicas);
const totalAvailableForHardware = filteredReplicas.reduce(
(total, replica) => total + (replica.available_count || 0),
0,
const nextLocationsMap = new Map();
replicasList.forEach((replica) => {
const rawId = replica?.location_id ?? replica?.location?.id;
if (rawId === null || rawId === undefined) {
return;
}
const id = rawId;
const mapKey = String(rawId);
const existing = nextLocationsMap.get(mapKey) || null;
const rawIso2 =
replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2;
const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : '';
const name =
replica?.location_name ??
replica?.location?.name ??
replica?.name ??
id;
const available = Number(replica?.available_count) || 0;
if (existing) {
existing.available += available;
return;
}
nextLocationsMap.set(mapKey, {
id,
name: String(name),
iso2,
region:
replica?.region ??
replica?.location_region ??
replica?.location?.region,
country:
replica?.country ??
replica?.location_country ??
replica?.location?.country,
code:
replica?.code ??
replica?.location_code ??
replica?.location?.code,
available,
});
});
setLocations(Array.from(nextLocationsMap.values()));
setLocationTotalAvailable(
Array.from(nextLocationsMap.values()).reduce(
(total, location) => total + (location.available || 0),
0,
),
);
setLocationTotalAvailable(totalAvailableForHardware);
} else {
showError(t('获取可用资源失败: ') + response.data.message);
setAvailableReplicas([]);
setLocationTotalAvailable(null);
}
} catch (error) {
if (replicaRequestIdRef.current === requestId) {
console.error('Load available replicas error:', error);
setAvailableReplicas([]);
setLocationTotalAvailable(null);
}
} finally {
......@@ -516,7 +518,10 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
hardware_qty: gpusPerContainer,
};
const response = await API.post('/api/deployments/price-estimation', requestData);
const response = await API.post(
'/api/deployments/price-estimation',
requestData,
);
if (response.data.success) {
setPriceEstimation(response.data.data);
} else {
......@@ -537,14 +542,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
// Prepare environment variables
const envVars = {};
envVariables.forEach(env => {
envVariables.forEach((env) => {
if (env.key && env.value) {
envVars[env.key] = env.value;
}
});
const secretEnvVars = {};
secretEnvVariables.forEach(env => {
secretEnvVariables.forEach((env) => {
if (env.key && env.value) {
secretEnvVars[env.key] = env.value;
}
......@@ -559,17 +564,19 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
}
// Prepare entrypoint and args
const cleanEntrypoint = entrypoint.filter(item => item.trim() !== '');
const cleanArgs = args.filter(item => item.trim() !== '');
const cleanEntrypoint = entrypoint.filter((item) => item.trim() !== '');
const cleanArgs = args.filter((item) => item.trim() !== '');
const resolvedImage = imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url;
const resolvedImage =
imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url;
const resolvedTrafficPort =
values.traffic_port || (imageMode === 'builtin' ? DEFAULT_TRAFFIC_PORT : undefined);
values.traffic_port ||
(imageMode === 'builtin' ? DEFAULT_TRAFFIC_PORT : undefined);
const requestData = {
resource_private_name: values.resource_private_name,
duration_hours: values.duration_hours,
gpus_per_container: values.gpus_per_container,
gpus_per_container: gpusPerContainer,
hardware_id: values.hardware_id,
location_ids: values.location_ids,
container_config: {
......@@ -588,7 +595,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
};
const response = await API.post('/api/deployments', requestData);
if (response.data.success) {
showSuccess(t('容器创建成功'));
onSuccess?.(response.data.data);
......@@ -614,10 +621,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const handleRemoveEnvVariable = (index, type) => {
if (type === 'env') {
const newEnvVars = envVariables.filter((_, i) => i !== index);
setEnvVariables(newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }]);
setEnvVariables(
newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }],
);
} else {
const newSecretEnvVars = secretEnvVariables.filter((_, i) => i !== index);
setSecretEnvVariables(newSecretEnvVars.length > 0 ? newSecretEnvVars : [{ key: '', value: '' }]);
setSecretEnvVariables(
newSecretEnvVars.length > 0
? newSecretEnvVars
: [{ key: '', value: '' }],
);
}
};
......@@ -678,10 +691,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
return;
}
const validLocationIds =
availableReplicas.length > 0
? availableReplicas.map((item) => item.location_id)
: locations.map((location) => location.id);
const validLocationIds = locations
.filter((location) => (Number(location.available) || 0) > 0)
.map((location) => location.id);
if (validLocationIds.length === 0) {
if (selectedLocationIds.length > 0) {
......@@ -707,31 +719,18 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
formApi.setValue('location_ids', filteredSelection);
}
}
}, [
availableReplicas,
locations,
selectedHardwareId,
selectedLocationIds,
visible,
formApi,
]);
}, [locations, selectedHardwareId, selectedLocationIds, visible, formApi]);
const maxAvailableReplicas = useMemo(() => {
if (!selectedLocationIds.length) return 0;
if (availableReplicas.length > 0) {
return availableReplicas
.filter((replica) => selectedLocationIds.includes(replica.location_id))
.reduce((total, replica) => total + (replica.available_count || 0), 0);
}
return locations
.filter((location) => selectedLocationIds.includes(location.id))
.reduce((total, location) => {
const availableValue = Number(location.available);
return total + (Number.isNaN(availableValue) ? 0 : availableValue);
}, 0);
}, [availableReplicas, selectedLocationIds, locations]);
}, [selectedLocationIds, locations]);
const isPriceReady = useMemo(
() =>
......@@ -749,7 +748,11 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
],
);
const currencyLabel = (priceEstimation?.currency || priceCurrency || '').toUpperCase();
const currencyLabel = (
priceEstimation?.currency ||
priceCurrency ||
''
).toUpperCase();
const selectedHardwareLabel = selectedHardwareId
? hardwareLabelMap[selectedHardwareId]
: '';
......@@ -769,7 +772,9 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
{
key: 'locations',
label: t('部署位置'),
value: selectedLocationNames.length ? selectedLocationNames.join('、') : '--',
value: selectedLocationNames.length
? selectedLocationNames.join('、')
: '--',
},
{
key: 'replicas',
......@@ -778,7 +783,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
},
{
key: 'gpus',
label: t('每容器GPU数量'),
label: t('最大GPU数量'),
value: (gpusPerContainer ?? 0).toString(),
},
{
......@@ -802,14 +807,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
const priceUnavailableContent = (
<div style={{ marginTop: 12 }}>
{loadingPrice ? (
<Space spacing={8} align="center">
<Spin size="small" />
<Text size="small" type="tertiary">
<Space spacing={8} align='center'>
<Spin size='small' />
<Text size='small' type='tertiary'>
{t('价格计算中...')}
</Text>
</Space>
) : (
<Text size="small" type="tertiary">
<Text size='small' type='tertiary'>
{isPriceReady
? t('价格暂时不可用,请稍后重试')
: t('完成硬件类型、部署位置、副本数量等配置后,将自动计算价格')}
......@@ -846,7 +851,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
getFormApi={setFormApi}
onSubmit={handleSubmit}
style={{ maxHeight: '70vh', overflowY: 'auto' }}
labelPosition="top"
labelPosition='top'
>
<Space
wrap
......@@ -854,25 +859,25 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
style={{ justifyContent: 'flex-end', width: '100%', marginBottom: 8 }}
>
<Button
size="small"
theme="borderless"
type="tertiary"
size='small'
theme='borderless'
type='tertiary'
onClick={() => scrollToSection(basicSectionRef)}
>
{t('部署配置')}
</Button>
<Button
size="small"
theme="borderless"
type="tertiary"
size='small'
theme='borderless'
type='tertiary'
onClick={() => scrollToSection(priceSectionRef)}
>
{t('价格预估')}
</Button>
<Button
size="small"
theme="borderless"
type="tertiary"
size='small'
theme='borderless'
type='tertiary'
onClick={() => scrollToSection(advancedSectionRef)}
>
{t('高级配置')}
......@@ -880,32 +885,34 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
</Space>
<div ref={basicSectionRef}>
<Card className="mb-4">
<Card className='mb-4'>
<Title heading={6}>{t('部署配置')}</Title>
<Form.Input
field="resource_private_name"
field='resource_private_name'
label={t('容器名称')}
placeholder={t('请输入容器名称')}
rules={[{ required: true, message: t('请输入容器名称') }]}
/>
<div className="mt-2">
<div className='mt-2'>
<Text strong>{t('镜像选择')}</Text>
<div style={{ marginTop: 8 }}>
<RadioGroup
type="button"
type='button'
value={imageMode}
onChange={(value) => setImageMode(value?.target?.value ?? value)}
onChange={(value) =>
setImageMode(value?.target?.value ?? value)
}
>
<Radio value="builtin">{t('内置 Ollama 镜像')}</Radio>
<Radio value="custom">{t('自定义镜像')}</Radio>
<Radio value='builtin'>{t('内置 Ollama 镜像')}</Radio>
<Radio value='custom'>{t('自定义镜像')}</Radio>
</RadioGroup>
</div>
</div>
<Form.Input
field="image_url"
field='image_url'
label={t('镜像地址')}
placeholder={t('例如:nginx:latest')}
rules={[{ required: true, message: t('请输入镜像地址') }]}
......@@ -918,20 +925,20 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
/>
{imageMode === 'builtin' && (
<Space align="center" spacing={8} className="mt-2">
<Text size="small" type="tertiary">
<Space align='center' spacing={8} className='mt-2'>
<Text size='small' type='tertiary'>
{t('系统已为该部署准备 Ollama 镜像与随机 API Key')}
</Text>
<Input
readOnly
value={autoOllamaKey}
size="small"
size='small'
style={{ width: 220 }}
/>
<Button
icon={<IconCopy />}
size="small"
theme="borderless"
size='small'
theme='borderless'
onClick={async () => {
if (!autoOllamaKey) {
return;
......@@ -952,16 +959,19 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Select
field="hardware_id"
field='hardware_id'
label={t('硬件类型')}
placeholder={t('选择硬件类型')}
loading={loadingHardware}
rules={[{ required: true, message: t('请选择硬件类型') }]}
onChange={(value) => {
const nextMaxGpus = getHardwareMaxGpus(value);
setSelectedHardwareId(value);
setGpusPerContainer(nextMaxGpus);
setSelectedLocationIds([]);
if (formApi) {
formApi.setValue('location_ids', []);
formApi.setValue('gpus_per_container', nextMaxGpus);
}
}}
style={{ width: '100%' }}
......@@ -987,13 +997,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
return (
<Option key={hardware.id} value={hardware.id}>
<div className="flex flex-col gap-1">
<div className='flex flex-col gap-1'>
<Text strong>{displayName}</Text>
<div className="flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]">
<div className='flex items-center gap-2 text-xs text-[var(--semi-color-text-2)]'>
<span>
{t('最大GPU数')}: {hardware.max_gpus}
{t('最大GPU数')}: {hardware.max_gpus}
</span>
<Tag color={hasAvailability ? 'green' : 'red'} size="small">
<Tag
color={hasAvailability ? 'green' : 'red'}
size='small'
>
{t('可用数量')}: {availableCount}
</Tag>
</div>
......@@ -1005,83 +1018,68 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
</Col>
<Col xs={24} md={12}>
<Form.InputNumber
field="gpus_per_container"
label={t('每容器GPU数量')}
field='gpus_per_container'
label={t('最大GPU数量')}
placeholder={1}
min={1}
max={selectedHardwareId ? hardwareTypes.find((h) => h.id === selectedHardwareId)?.max_gpus : 8}
max={getHardwareMaxGpus(selectedHardwareId)}
step={1}
innerButtons
rules={[{ required: true, message: t('请输入GPU数量') }]}
onChange={(value) => setGpusPerContainer(value)}
disabled
style={{ width: '100%' }}
/>
</Col>
</Row>
{typeof hardwareTotalAvailable === 'number' && (
<Text size="small" type="tertiary">
<Text size='small' type='tertiary'>
{t('全部硬件总可用资源')}: {hardwareTotalAvailable}
</Text>
)}
<Form.Select
field="location_ids"
field='location_ids'
label={
<Space>
{t('部署位置')}
{loadingReplicas && <Spin size="small" />}
{loadingReplicas && <Spin size='small' />}
</Space>
}
placeholder={
!selectedHardwareId
? t('请先选择硬件类型')
: loadingLocations || loadingReplicas
: loadingReplicas
? t('正在加载可用部署位置...')
: t('选择部署位置(可多选)')
}
multiple
loading={loadingLocations || loadingReplicas}
disabled={!selectedHardwareId || loadingLocations || loadingReplicas}
loading={loadingReplicas}
disabled={!selectedHardwareId || loadingReplicas}
rules={[{ required: true, message: t('请选择至少一个部署位置') }]}
onChange={(value) => setSelectedLocationIds(value)}
style={{ width: '100%' }}
dropdownStyle={{ maxHeight: 360, overflowY: 'auto' }}
renderSelectedItem={(optionNode) => ({
isRenderInTag: true,
content:
!optionNode
? ''
: loadingLocations || loadingReplicas
? t('部署位置加载中...')
: locationLabelMap[optionNode?.value] ||
optionNode?.label ||
optionNode?.value ||
'',
content: !optionNode
? ''
: loadingReplicas
? t('部署位置加载中...')
: locationLabelMap[optionNode?.value] ||
optionNode?.label ||
optionNode?.value ||
'',
})}
>
{locations.map((location) => {
const replicaEntry = availableReplicas.find(
(r) => r.location_id === location.id,
);
const hasReplicaData = availableReplicas.length > 0;
const availableCount = hasReplicaData
? replicaEntry?.available_count ?? 0
: (() => {
const numeric = Number(location.available);
return Number.isNaN(numeric) ? 0 : numeric;
})();
const numeric = Number(location.available);
const availableCount = Number.isNaN(numeric) ? 0 : numeric;
const locationLabel =
location.region ||
location.country ||
(location.iso2 ? location.iso2.toUpperCase() : '') ||
location.code ||
'';
const disableOption = hasReplicaData
? availableCount === 0
: typeof location.available === 'number'
? location.available === 0
: false;
const disableOption = availableCount === 0;
return (
<Option
......@@ -1089,17 +1087,17 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
value={location.id}
disabled={disableOption}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-2'>
<Text strong>{location.name}</Text>
{locationLabel && (
<Tag color="blue" size="small">
<Tag color='blue' size='small'>
{locationLabel}
</Tag>
)}
</div>
<Text
size="small"
size='small'
type={availableCount > 0 ? 'success' : 'danger'}
>
{t('可用数量')}: {availableCount}
......@@ -1111,16 +1109,16 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
</Form.Select>
{typeof locationTotalAvailable === 'number' && (
<Text size="small" type="tertiary">
<Text size='small' type='tertiary'>
{t('全部地区总可用资源')}: {locationTotalAvailable}
</Text>
)}
<Row gutter={16}>
<Col xs={24} md={8}>
<Form.InputNumber
field="replica_count"
label={t('副本数量')}
<Row gutter={16}>
<Col xs={24} md={8}>
<Form.InputNumber
field='replica_count'
label={t('副本数量')}
placeholder={1}
min={1}
max={maxAvailableReplicas || 100}
......@@ -1129,14 +1127,14 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
style={{ width: '100%' }}
/>
{maxAvailableReplicas > 0 && (
<Text size="small" type="tertiary">
<Text size='small' type='tertiary'>
{t('最大可用')}: {maxAvailableReplicas}
</Text>
)}
</Col>
<Col xs={24} md={8}>
<Form.InputNumber
field="duration_hours"
field='duration_hours'
label={t('运行时长(小时)')}
placeholder={1}
min={1}
......@@ -1148,7 +1146,7 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
</Col>
<Col xs={24} md={8}>
<Form.InputNumber
field="traffic_port"
field='traffic_port'
label={
<Space>
{t('流量端口')}
......@@ -1162,298 +1160,349 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => {
max={65535}
style={{ width: '100%' }}
disabled={imageMode === 'builtin'}
/>
</Col>
</Row>
<div ref={advancedSectionRef}>
<Collapse className="mt-4">
<Collapse.Panel header={t('高级配置')} itemKey="advanced">
<Card>
<Title heading={6}>{t('镜像仓库配置')}</Title>
<Row gutter={16}>
<Col span={12}>
<Form.Input
field="registry_username"
label={t('镜像仓库用户名')}
placeholder={t('私有镜像仓库的用户名')}
/>
</Col>
<Col span={12}>
<Form.Input
field="registry_secret"
label={t('镜像仓库密码')}
type="password"
placeholder={t('私有镜像仓库的密码')}
/>
</Col>
</Row>
</Card>
<Divider />
<Card>
<Title heading={6}>{t('容器启动配置')}</Title>
<div style={{ marginBottom: 16 }}>
<Text strong>{t('启动命令 (Entrypoint)')}</Text>
{entrypoint.map((cmd, index) => (
<div key={index} style={{ display: 'flex', marginTop: 8 }}>
<Input
value={cmd}
placeholder={t('例如:/bin/bash')}
onChange={(value) => handleArrayFieldChange(index, value, 'entrypoint')}
style={{ flex: 1, marginRight: 8 }}
/>
<Button
icon={<IconMinus />}
onClick={() => handleRemoveArrayField(index, 'entrypoint')}
disabled={entrypoint.length === 1}
/>
</div>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddArrayField('entrypoint')}
style={{ marginTop: 8 }}
>
{t('添加启动命令')}
</Button>
</div>
/>
</Col>
</Row>
<div style={{ marginBottom: 16 }}>
<Text strong>{t('启动参数 (Args)')}</Text>
{args.map((arg, index) => (
<div key={index} style={{ display: 'flex', marginTop: 8 }}>
<Input
value={arg}
placeholder={t('例如:-c')}
onChange={(value) => handleArrayFieldChange(index, value, 'args')}
style={{ flex: 1, marginRight: 8 }}
<div ref={advancedSectionRef}>
<Collapse className='mt-4'>
<Collapse.Panel header={t('高级配置')} itemKey='advanced'>
<Card>
<Title heading={6}>{t('镜像仓库配置')}</Title>
<Row gutter={16}>
<Col span={12}>
<Form.Input
field='registry_username'
label={t('镜像仓库用户名')}
placeholder={t('私有镜像仓库的用户名')}
/>
<Button
icon={<IconMinus />}
onClick={() => handleRemoveArrayField(index, 'args')}
disabled={args.length === 1}
</Col>
<Col span={12}>
<Form.Input
field='registry_secret'
label={t('镜像仓库密码')}
type='password'
placeholder={t('私有镜像仓库的密码')}
/>
</div>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddArrayField('args')}
style={{ marginTop: 8 }}
>
{t('添加启动参数')}
</Button>
</div>
</Card>
<Divider />
<Card>
<Title heading={6}>{t('环境变量')}</Title>
<div style={{ marginBottom: 16 }}>
<Text strong>{t('普通环境变量')}</Text>
{envVariables.map((env, index) => (
<Row key={index} gutter={8} style={{ marginTop: 8 }}>
<Col span={10}>
</Col>
</Row>
</Card>
<Divider />
<Card>
<Title heading={6}>{t('容器启动配置')}</Title>
<div style={{ marginBottom: 16 }}>
<Text strong>{t('启动命令 (Entrypoint)')}</Text>
{entrypoint.map((cmd, index) => (
<div
key={index}
style={{ display: 'flex', marginTop: 8 }}
>
<Input
placeholder={t('变量名')}
value={env.key}
onChange={(value) => handleEnvVariableChange(index, 'key', value, 'env')}
value={cmd}
placeholder={t('例如:/bin/bash')}
onChange={(value) =>
handleArrayFieldChange(index, value, 'entrypoint')
}
style={{ flex: 1, marginRight: 8 }}
/>
</Col>
<Col span={10}>
<Button
icon={<IconMinus />}
onClick={() =>
handleRemoveArrayField(index, 'entrypoint')
}
disabled={entrypoint.length === 1}
/>
</div>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddArrayField('entrypoint')}
style={{ marginTop: 8 }}
>
{t('添加启动命令')}
</Button>
</div>
<div style={{ marginBottom: 16 }}>
<Text strong>{t('启动参数 (Args)')}</Text>
{args.map((arg, index) => (
<div
key={index}
style={{ display: 'flex', marginTop: 8 }}
>
<Input
placeholder={t('变量值')}
value={env.value}
onChange={(value) => handleEnvVariableChange(index, 'value', value, 'env')}
value={arg}
placeholder={t('例如:-c')}
onChange={(value) =>
handleArrayFieldChange(index, value, 'args')
}
style={{ flex: 1, marginRight: 8 }}
/>
</Col>
<Col span={4}>
<Button
icon={<IconMinus />}
onClick={() => handleRemoveEnvVariable(index, 'env')}
disabled={envVariables.length === 1}
onClick={() =>
handleRemoveArrayField(index, 'args')
}
disabled={args.length === 1}
/>
</Col>
</Row>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddEnvVariable('env')}
style={{ marginTop: 8 }}
>
{t('添加环境变量')}
</Button>
</div>
</div>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddArrayField('args')}
style={{ marginTop: 8 }}
>
{t('添加启动参数')}
</Button>
</div>
</Card>
<Divider />
<Card>
<Title heading={6}>{t('环境变量')}</Title>
<div>
<Text strong>{t('密钥环境变量')}</Text>
{secretEnvVariables.map((env, index) => {
const isAutoSecret =
imageMode === 'builtin' && env.key === 'OLLAMA_API_KEY';
return (
<div style={{ marginBottom: 16 }}>
<Text strong>{t('普通环境变量')}</Text>
{envVariables.map((env, index) => (
<Row key={index} gutter={8} style={{ marginTop: 8 }}>
<Col span={10}>
<Input
placeholder={t('变量名')}
value={env.key}
onChange={(value) => handleEnvVariableChange(index, 'key', value, 'secret')}
disabled={isAutoSecret}
onChange={(value) =>
handleEnvVariableChange(
index,
'key',
value,
'env',
)
}
/>
</Col>
<Col span={10}>
<Input
placeholder={t('变量值')}
type="password"
value={env.value}
onChange={(value) => handleEnvVariableChange(index, 'value', value, 'secret')}
disabled={isAutoSecret}
onChange={(value) =>
handleEnvVariableChange(
index,
'value',
value,
'env',
)
}
/>
</Col>
<Col span={4}>
<Button
icon={<IconMinus />}
onClick={() => handleRemoveEnvVariable(index, 'secret')}
disabled={secretEnvVariables.length === 1 || isAutoSecret}
onClick={() =>
handleRemoveEnvVariable(index, 'env')
}
disabled={envVariables.length === 1}
/>
</Col>
</Row>
);
})}
<Button
icon={<IconPlus />}
onClick={() => handleAddEnvVariable('secret')}
style={{ marginTop: 8 }}
>
{t('添加密钥环境变量')}
</Button>
</div>
</Card>
</Collapse.Panel>
</Collapse>
</div>
</Card>
))}
<Button
icon={<IconPlus />}
onClick={() => handleAddEnvVariable('env')}
style={{ marginTop: 8 }}
>
{t('添加环境变量')}
</Button>
</div>
<div>
<Text strong>{t('密钥环境变量')}</Text>
{secretEnvVariables.map((env, index) => {
const isAutoSecret =
imageMode === 'builtin' &&
env.key === 'OLLAMA_API_KEY';
return (
<Row key={index} gutter={8} style={{ marginTop: 8 }}>
<Col span={10}>
<Input
placeholder={t('变量名')}
value={env.key}
onChange={(value) =>
handleEnvVariableChange(
index,
'key',
value,
'secret',
)
}
disabled={isAutoSecret}
/>
</Col>
<Col span={10}>
<Input
placeholder={t('变量值')}
type='password'
value={env.value}
onChange={(value) =>
handleEnvVariableChange(
index,
'value',
value,
'secret',
)
}
disabled={isAutoSecret}
/>
</Col>
<Col span={4}>
<Button
icon={<IconMinus />}
onClick={() =>
handleRemoveEnvVariable(index, 'secret')
}
disabled={
secretEnvVariables.length === 1 ||
isAutoSecret
}
/>
</Col>
</Row>
);
})}
<Button
icon={<IconPlus />}
onClick={() => handleAddEnvVariable('secret')}
style={{ marginTop: 8 }}
>
{t('添加密钥环境变量')}
</Button>
</div>
</Card>
</Collapse.Panel>
</Collapse>
</div>
</Card>
</div>
<div ref={priceSectionRef}>
<Card className="mb-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<Title heading={6} style={{ margin: 0 }}>
{t('价格预估')}
</Title>
<Space align="center" spacing={12} className="flex flex-wrap">
<Text type="secondary" size="small">
{t('计价币种')}
</Text>
<RadioGroup
type="button"
value={priceCurrency}
onChange={handleCurrencyChange}
>
<Radio value="usdc">USDC</Radio>
<Radio value="iocoin">IOCOIN</Radio>
</RadioGroup>
<Tag size="small" color="blue">
{currencyLabel}
</Tag>
</Space>
</div>
<Card className='mb-4'>
<div className='flex flex-wrap items-center justify-between gap-3'>
<Title heading={6} style={{ margin: 0 }}>
{t('价格预估')}
</Title>
<Space align='center' spacing={12} className='flex flex-wrap'>
<Text type='secondary' size='small'>
{t('计价币种')}
</Text>
<RadioGroup
type='button'
value={priceCurrency}
onChange={handleCurrencyChange}
>
<Radio value='usdc'>USDC</Radio>
<Radio value='iocoin'>IOCOIN</Radio>
</RadioGroup>
<Tag size='small' color='blue'>
{currencyLabel}
</Tag>
</Space>
</div>
{priceEstimation ? (
<div className="mt-4 flex w-full flex-col gap-4">
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
<div
className="flex flex-col gap-1 rounded-md px-4 py-3"
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size="small" type="tertiary">
{t('预估总费用')}
</Text>
<div
style={{
fontSize: 24,
fontWeight: 600,
color: 'var(--semi-color-text-0)',
}}
>
{typeof priceEstimation.estimated_cost === 'number'
? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}`
: '--'}
</div>
</div>
{priceEstimation ? (
<div className='mt-4 flex w-full flex-col gap-4'>
<div className='grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3'>
<div
className='flex flex-col gap-1 rounded-md px-4 py-3'
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size='small' type='tertiary'>
{t('预估总费用')}
</Text>
<div
className="flex flex-col gap-1 rounded-md px-4 py-3"
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
fontSize: 24,
fontWeight: 600,
color: 'var(--semi-color-text-0)',
}}
>
<Text size="small" type="tertiary">
{t('小时费率')}
</Text>
<Text strong>
{typeof priceEstimation.price_breakdown?.hourly_rate === 'number'
? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h`
: '--'}
</Text>
{typeof priceEstimation.estimated_cost === 'number'
? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}`
: '--'}
</div>
</div>
<div
className='flex flex-col gap-1 rounded-md px-4 py-3'
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size='small' type='tertiary'>
{t('小时费率')}
</Text>
<Text strong>
{typeof priceEstimation.price_breakdown?.hourly_rate ===
'number'
? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h`
: '--'}
</Text>
</div>
<div
className='flex flex-col gap-1 rounded-md px-4 py-3'
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size='small' type='tertiary'>
{t('计算成本')}
</Text>
<Text strong>
{typeof priceEstimation.price_breakdown?.compute_cost ===
'number'
? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}`
: '--'}
</Text>
</div>
</div>
<div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-3'>
{priceSummaryItems.map((item) => (
<div
className="flex flex-col gap-1 rounded-md px-4 py-3"
key={item.key}
className='flex items-center justify-between gap-3 rounded-md px-3 py-2'
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size="small" type="tertiary">
{t('计算成本')}
</Text>
<Text strong>
{typeof priceEstimation.price_breakdown?.compute_cost === 'number'
? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}`
: '--'}
<Text size='small' type='tertiary'>
{item.label}
</Text>
<Text strong>{item.value}</Text>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{priceSummaryItems.map((item) => (
<div
key={item.key}
className="flex items-center justify-between gap-3 rounded-md px-3 py-2"
style={{
border: '1px solid var(--semi-color-border)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<Text size="small" type="tertiary">
{item.label}
</Text>
<Text strong>{item.value}</Text>
</div>
))}
</div>
))}
</div>
) : (
priceUnavailableContent
)}
{priceEstimation && loadingPrice && (
<Space align="center" spacing={8} style={{ marginTop: 12 }}>
<Spin size="small" />
<Text size="small" type="tertiary">
{t('价格重新计算中...')}
</Text>
</Space>
)}
</div>
) : (
priceUnavailableContent
)}
{priceEstimation && loadingPrice && (
<Space align='center' spacing={8} style={{ marginTop: 12 }}>
<Spin size='small' />
<Text size='small' type='tertiary'>
{t('价格重新计算中...')}
</Text>
</Space>
)}
</Card>
</div>
</Form>
</Modal>
);
......
......@@ -34,27 +34,21 @@ import {
TextArea,
Switch,
} from '@douyinfe/semi-ui';
import {
FaCog,
import {
FaCog,
FaDocker,
FaKey,
FaTerminal,
FaNetworkWired,
FaExclamationTriangle,
FaPlus,
FaMinus
FaMinus,
} from 'react-icons/fa';
import { API, showError, showSuccess } from '../../../../helpers';
const { Text, Title } = Typography;
const UpdateConfigModal = ({
visible,
onCancel,
deployment,
onSuccess,
t
}) => {
const UpdateConfigModal = ({ visible, onCancel, deployment, onSuccess, t }) => {
const formRef = useRef(null);
const [loading, setLoading] = useState(false);
const [envVars, setEnvVars] = useState([]);
......@@ -72,18 +66,21 @@ const UpdateConfigModal = ({
registry_secret: '',
command: '',
};
if (formRef.current) {
formRef.current.setValues(initialValues);
}
// Initialize environment variables
const envVarsList = deployment.container_config?.env_variables
? Object.entries(deployment.container_config.env_variables).map(([key, value]) => ({
key, value: String(value)
}))
const envVarsList = deployment.container_config?.env_variables
? Object.entries(deployment.container_config.env_variables).map(
([key, value]) => ({
key,
value: String(value),
}),
)
: [];
setEnvVars(envVarsList);
setSecretEnvVars([]);
}
......@@ -91,23 +88,30 @@ const UpdateConfigModal = ({
const handleUpdate = async () => {
try {
const formValues = formRef.current ? await formRef.current.validate() : {};
const formValues = formRef.current
? await formRef.current.validate()
: {};
setLoading(true);
// Prepare the update payload
const payload = {};
if (formValues.image_url) payload.image_url = formValues.image_url;
if (formValues.traffic_port) payload.traffic_port = formValues.traffic_port;
if (formValues.registry_username) payload.registry_username = formValues.registry_username;
if (formValues.registry_secret) payload.registry_secret = formValues.registry_secret;
if (formValues.traffic_port)
payload.traffic_port = formValues.traffic_port;
if (formValues.registry_username)
payload.registry_username = formValues.registry_username;
if (formValues.registry_secret)
payload.registry_secret = formValues.registry_secret;
if (formValues.command) payload.command = formValues.command;
// Process entrypoint
if (formValues.entrypoint) {
payload.entrypoint = formValues.entrypoint.split(' ').filter(cmd => cmd.trim());
payload.entrypoint = formValues.entrypoint
.split(' ')
.filter((cmd) => cmd.trim());
}
// Process environment variables
if (envVars.length > 0) {
payload.env_variables = envVars.reduce((acc, env) => {
......@@ -117,7 +121,7 @@ const UpdateConfigModal = ({
return acc;
}, {});
}
// Process secret environment variables
if (secretEnvVars.length > 0) {
payload.secret_env_variables = secretEnvVars.reduce((acc, env) => {
......@@ -128,7 +132,10 @@ const UpdateConfigModal = ({
}, {});
}
const response = await API.put(`/api/deployments/${deployment.id}`, payload);
const response = await API.put(
`/api/deployments/${deployment.id}`,
payload,
);
if (response.data.success) {
showSuccess(t('容器配置更新成功'));
......@@ -136,7 +143,11 @@ const UpdateConfigModal = ({
handleCancel();
}
} catch (error) {
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('更新配置失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setLoading(false);
}
......@@ -184,8 +195,8 @@ const UpdateConfigModal = ({
return (
<Modal
title={
<div className="flex items-center gap-2">
<FaCog className="text-blue-500" />
<div className='flex items-center gap-2'>
<FaCog className='text-blue-500' />
<span>{t('更新容器配置')}</span>
</div>
}
......@@ -196,130 +207,131 @@ const UpdateConfigModal = ({
cancelText={t('取消')}
confirmLoading={loading}
width={700}
className="update-config-modal"
className='update-config-modal'
>
<div className="space-y-4 max-h-[600px] overflow-y-auto">
<div className='space-y-4 max-h-[600px] overflow-y-auto'>
{/* Container Info */}
<Card className="border-0 bg-gray-50">
<div className="flex items-center justify-between">
<Card className='border-0 bg-gray-50'>
<div className='flex items-center justify-between'>
<div>
<Text strong className="text-base">
<Text strong className='text-base'>
{deployment?.container_name}
</Text>
<div className="mt-1">
<Text type="secondary" size="small">
<div className='mt-1'>
<Text type='secondary' size='small'>
ID: {deployment?.id}
</Text>
</div>
</div>
<Tag color="blue">{deployment?.status}</Tag>
<Tag color='blue'>{deployment?.status}</Tag>
</div>
</Card>
{/* Warning Banner */}
<Banner
type="warning"
type='warning'
icon={<FaExclamationTriangle />}
title={t('重要提醒')}
description={
<div className="space-y-2">
<p>{t('更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。')}</p>
<div className='space-y-2'>
<p>
{t(
'更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。',
)}
</p>
<p>{t('某些配置更改可能需要几分钟才能生效。')}</p>
</div>
}
/>
<Form
getFormApi={(api) => (formRef.current = api)}
layout="vertical"
>
<Form getFormApi={(api) => (formRef.current = api)} layout='vertical'>
<Collapse defaultActiveKey={['docker']}>
{/* Docker Configuration */}
<Collapse.Panel
<Collapse.Panel
header={
<div className="flex items-center gap-2">
<FaDocker className="text-blue-600" />
<span>{t('Docker 配置')}</span>
<div className='flex items-center gap-2'>
<FaDocker className='text-blue-600' />
<span>{t('镜像配置')}</span>
</div>
}
itemKey="docker"
itemKey='docker'
>
<div className="space-y-4">
<div className='space-y-4'>
<Form.Input
field="image_url"
field='image_url'
label={t('镜像地址')}
placeholder={t('例如: nginx:latest')}
rules={[
{
{
type: 'string',
message: t('请输入有效的镜像地址')
}
message: t('请输入有效的镜像地址'),
},
]}
/>
<Form.Input
field="registry_username"
field='registry_username'
label={t('镜像仓库用户名')}
placeholder={t('如果镜像为私有,请填写用户名')}
/>
<Form.Input
field="registry_secret"
field='registry_secret'
label={t('镜像仓库密码')}
mode="password"
mode='password'
placeholder={t('如果镜像为私有,请填写密码或Token')}
/>
</div>
</Collapse.Panel>
{/* Network Configuration */}
<Collapse.Panel
<Collapse.Panel
header={
<div className="flex items-center gap-2">
<FaNetworkWired className="text-green-600" />
<div className='flex items-center gap-2'>
<FaNetworkWired className='text-green-600' />
<span>{t('网络配置')}</span>
</div>
}
itemKey="network"
itemKey='network'
>
<Form.InputNumber
field="traffic_port"
field='traffic_port'
label={t('流量端口')}
placeholder={t('容器对外暴露的端口')}
min={1}
max={65535}
style={{ width: '100%' }}
rules={[
{
{
type: 'number',
min: 1,
max: 65535,
message: t('端口号必须在1-65535之间')
}
message: t('端口号必须在1-65535之间'),
},
]}
/>
</Collapse.Panel>
{/* Startup Configuration */}
<Collapse.Panel
<Collapse.Panel
header={
<div className="flex items-center gap-2">
<FaTerminal className="text-purple-600" />
<div className='flex items-center gap-2'>
<FaTerminal className='text-purple-600' />
<span>{t('启动配置')}</span>
</div>
}
itemKey="startup"
itemKey='startup'
>
<div className="space-y-4">
<div className='space-y-4'>
<Form.Input
field="entrypoint"
field='entrypoint'
label={t('启动命令 (Entrypoint)')}
placeholder={t('例如: /bin/bash -c "python app.py"')}
helpText={t('多个命令用空格分隔')}
/>
<Form.Input
field="command"
field='command'
label={t('运行命令 (Command)')}
placeholder={t('容器启动后执行的命令')}
/>
......@@ -327,34 +339,34 @@ const UpdateConfigModal = ({
</Collapse.Panel>
{/* Environment Variables */}
<Collapse.Panel
<Collapse.Panel
header={
<div className="flex items-center gap-2">
<FaKey className="text-orange-600" />
<div className='flex items-center gap-2'>
<FaKey className='text-orange-600' />
<span>{t('环境变量')}</span>
<Tag size="small">{envVars.length}</Tag>
<Tag size='small'>{envVars.length}</Tag>
</div>
}
itemKey="env"
itemKey='env'
>
<div className="space-y-4">
<div className='space-y-4'>
{/* Regular Environment Variables */}
<div>
<div className="flex items-center justify-between mb-3">
<div className='flex items-center justify-between mb-3'>
<Text strong>{t('普通环境变量')}</Text>
<Button
size="small"
size='small'
icon={<FaPlus />}
onClick={addEnvVar}
theme="borderless"
type="primary"
theme='borderless'
type='primary'
>
{t('添加')}
</Button>
</div>
{envVars.map((envVar, index) => (
<div key={index} className="flex items-end gap-2 mb-2">
<div key={index} className='flex items-end gap-2 mb-2'>
<Input
placeholder={t('变量名')}
value={envVar.key}
......@@ -365,22 +377,24 @@ const UpdateConfigModal = ({
<Input
placeholder={t('变量值')}
value={envVar.value}
onChange={(value) => updateEnvVar(index, 'value', value)}
onChange={(value) =>
updateEnvVar(index, 'value', value)
}
style={{ flex: 2 }}
/>
<Button
size="small"
size='small'
icon={<FaMinus />}
onClick={() => removeEnvVar(index)}
theme="borderless"
type="danger"
theme='borderless'
type='danger'
/>
</div>
))}
{envVars.length === 0 && (
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg">
<Text type="secondary">{t('暂无环境变量')}</Text>
<div className='text-center text-gray-500 py-4 border-2 border-dashed border-gray-300 rounded-lg'>
<Text type='secondary'>{t('暂无环境变量')}</Text>
</div>
)}
</div>
......@@ -389,61 +403,67 @@ const UpdateConfigModal = ({
{/* Secret Environment Variables */}
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className='flex items-center justify-between mb-3'>
<div className='flex items-center gap-2'>
<Text strong>{t('机密环境变量')}</Text>
<Tag size="small" type="danger">
<Tag size='small' type='danger'>
{t('加密存储')}
</Tag>
</div>
<Button
size="small"
size='small'
icon={<FaPlus />}
onClick={addSecretEnvVar}
theme="borderless"
type="danger"
theme='borderless'
type='danger'
>
{t('添加')}
</Button>
</div>
{secretEnvVars.map((envVar, index) => (
<div key={index} className="flex items-end gap-2 mb-2">
<div key={index} className='flex items-end gap-2 mb-2'>
<Input
placeholder={t('变量名')}
value={envVar.key}
onChange={(value) => updateSecretEnvVar(index, 'key', value)}
onChange={(value) =>
updateSecretEnvVar(index, 'key', value)
}
style={{ flex: 1 }}
/>
<Text>=</Text>
<Input
mode="password"
mode='password'
placeholder={t('变量值')}
value={envVar.value}
onChange={(value) => updateSecretEnvVar(index, 'value', value)}
onChange={(value) =>
updateSecretEnvVar(index, 'value', value)
}
style={{ flex: 2 }}
/>
<Button
size="small"
size='small'
icon={<FaMinus />}
onClick={() => removeSecretEnvVar(index)}
theme="borderless"
type="danger"
theme='borderless'
type='danger'
/>
</div>
))}
{secretEnvVars.length === 0 && (
<div className="text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50">
<Text type="secondary">{t('暂无机密环境变量')}</Text>
<div className='text-center text-gray-500 py-4 border-2 border-dashed border-red-200 rounded-lg bg-red-50'>
<Text type='secondary'>{t('暂无机密环境变量')}</Text>
</div>
)}
<Banner
type="info"
type='info'
title={t('机密环境变量说明')}
description={t('机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。')}
size="small"
description={t(
'机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。',
)}
size='small'
/>
</div>
</div>
......@@ -452,16 +472,18 @@ const UpdateConfigModal = ({
</Form>
{/* Final Warning */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<FaExclamationTriangle className="text-yellow-600 mt-0.5" />
<div className='bg-yellow-50 border border-yellow-200 rounded-lg p-3'>
<div className='flex items-start gap-2'>
<FaExclamationTriangle className='text-yellow-600 mt-0.5' />
<div>
<Text strong className="text-yellow-800">
<Text strong className='text-yellow-800'>
{t('配置更新确认')}
</Text>
<div className="mt-1">
<Text size="small" className="text-yellow-700">
{t('更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。')}
<div className='mt-1'>
<Text size='small' className='text-yellow-700'>
{t(
'更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。',
)}
</Text>
</div>
</div>
......@@ -472,4 +494,4 @@ const UpdateConfigModal = ({
);
};
export default UpdateConfigModal;
\ No newline at end of file
export default UpdateConfigModal;
......@@ -31,8 +31,8 @@ import {
Badge,
Tooltip,
} from '@douyinfe/semi-ui';
import {
FaInfoCircle,
import {
FaInfoCircle,
FaServer,
FaClock,
FaMapMarkerAlt,
......@@ -43,16 +43,16 @@ import {
FaLink,
} from 'react-icons/fa';
import { IconRefresh } from '@douyinfe/semi-icons';
import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
import {
API,
showError,
showSuccess,
timestamp2string,
} from '../../../../helpers';
const { Text, Title } = Typography;
const ViewDetailsModal = ({
visible,
onCancel,
deployment,
t
}) => {
const ViewDetailsModal = ({ visible, onCancel, deployment, t }) => {
const [details, setDetails] = useState(null);
const [loading, setLoading] = useState(false);
const [containers, setContainers] = useState([]);
......@@ -60,7 +60,7 @@ const ViewDetailsModal = ({
const fetchDetails = async () => {
if (!deployment?.id) return;
setLoading(true);
try {
const response = await API.get(`/api/deployments/${deployment.id}`);
......@@ -68,7 +68,11 @@ const ViewDetailsModal = ({
setDetails(response.data.data);
}
} catch (error) {
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取详情失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setLoading(false);
}
......@@ -79,12 +83,18 @@ const ViewDetailsModal = ({
setContainersLoading(true);
try {
const response = await API.get(`/api/deployments/${deployment.id}/containers`);
const response = await API.get(
`/api/deployments/${deployment.id}/containers`,
);
if (response.data.success) {
setContainers(response.data.data?.containers || []);
}
} catch (error) {
showError(t('获取容器信息失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取容器信息失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setContainersLoading(false);
}
......@@ -102,7 +112,7 @@ const ViewDetailsModal = ({
const handleCopyId = () => {
navigator.clipboard.writeText(deployment?.id);
showSuccess(t('ID已复制到剪贴板'));
showSuccess(t('已复制 ID 到剪贴板'));
};
const handleRefresh = () => {
......@@ -112,12 +122,16 @@ const ViewDetailsModal = ({
const getStatusConfig = (status) => {
const statusConfig = {
'running': { color: 'green', text: '运行中', icon: '🟢' },
'completed': { color: 'green', text: '已完成', icon: '✅' },
running: { color: 'green', text: '运行中', icon: '🟢' },
completed: { color: 'green', text: '已完成', icon: '✅' },
'deployment requested': { color: 'blue', text: '部署请求中', icon: '🔄' },
'termination requested': { color: 'orange', text: '终止请求中', icon: '⏸️' },
'destroyed': { color: 'red', text: '已销毁', icon: '🔴' },
'failed': { color: 'red', text: '失败', icon: '❌' }
'termination requested': {
color: 'orange',
text: '终止请求中',
icon: '⏸️',
},
destroyed: { color: 'red', text: '已销毁', icon: '🔴' },
failed: { color: 'red', text: '失败', icon: '❌' },
};
return statusConfig[status] || { color: 'grey', text: status, icon: '❓' };
};
......@@ -127,149 +141,167 @@ const ViewDetailsModal = ({
return (
<Modal
title={
<div className="flex items-center gap-2">
<FaInfoCircle className="text-blue-500" />
<div className='flex items-center gap-2'>
<FaInfoCircle className='text-blue-500' />
<span>{t('容器详情')}</span>
</div>
}
visible={visible}
onCancel={onCancel}
footer={
<div className="flex justify-between">
<Button
icon={<IconRefresh />}
<div className='flex justify-between'>
<Button
icon={<IconRefresh />}
onClick={handleRefresh}
loading={loading || containersLoading}
theme="borderless"
theme='borderless'
>
{t('刷新')}
</Button>
<Button onClick={onCancel}>
{t('关闭')}
</Button>
<Button onClick={onCancel}>{t('关闭')}</Button>
</div>
}
width={800}
className="deployment-details-modal"
className='deployment-details-modal'
>
{loading && !details ? (
<div className="flex items-center justify-center py-12">
<Spin size="large" tip={t('加载详情中...')} />
<div className='flex items-center justify-center py-12'>
<Spin size='large' tip={t('加载详情中...')} />
</div>
) : details ? (
<div className="space-y-4 max-h-[600px] overflow-y-auto">
<div className='space-y-4 max-h-[600px] overflow-y-auto'>
{/* Basic Info */}
<Card
<Card
title={
<div className="flex items-center gap-2">
<FaServer className="text-blue-500" />
<div className='flex items-center gap-2'>
<FaServer className='text-blue-500' />
<span>{t('基本信息')}</span>
</div>
}
className="border-0 shadow-sm"
>
<Descriptions data={[
{
key: t('容器名称'),
value: (
<div className="flex items-center gap-2">
<Text strong className="text-base">
{details.deployment_name || details.id}
</Text>
<Button
size="small"
theme="borderless"
icon={<FaCopy />}
onClick={handleCopyId}
className="opacity-70 hover:opacity-100"
/>
</div>
)
},
{
key: t('容器ID'),
value: (
<Text type="secondary" className="font-mono text-sm">
{details.id}
</Text>
)
},
{
key: t('状态'),
value: (
<div className="flex items-center gap-2">
<span>{statusConfig.icon}</span>
<Tag color={statusConfig.color}>
{t(statusConfig.text)}
</Tag>
</div>
)
},
{
key: t('创建时间'),
value: timestamp2string(details.created_at)
}
]} />
</Card>
{/* Hardware & Performance */}
<Card
title={
<div className="flex items-center gap-2">
<FaChartLine className="text-green-500" />
<span>{t('硬件与性能')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
<div className="space-y-4">
<Descriptions data={[
<Descriptions
data={[
{
key: t('硬件类型'),
key: t('容器名称'),
value: (
<div className="flex items-center gap-2">
<Tag color="blue">{details.brand_name}</Tag>
<Text strong>{details.hardware_name}</Text>
<div className='flex items-center gap-2'>
<Text strong className='text-base'>
{details.deployment_name || details.id}
</Text>
<Button
size='small'
theme='borderless'
icon={<FaCopy />}
onClick={handleCopyId}
className='opacity-70 hover:opacity-100'
/>
</div>
)
),
},
{
key: t('GPU数量'),
key: t('容器ID'),
value: (
<div className="flex items-center gap-2">
<Badge count={details.total_gpus} theme="solid" type="primary">
<FaServer className="text-purple-500" />
</Badge>
<Text>{t('总计')} {details.total_gpus} {t('个GPU')}</Text>
</div>
)
<Text type='secondary' className='font-mono text-sm'>
{details.id}
</Text>
),
},
{
key: t('容器配置'),
key: t('状态'),
value: (
<div className="space-y-1">
<div>{t('每容器GPU数')}: {details.gpus_per_container}</div>
<div>{t('容器总数')}: {details.total_containers}</div>
<div className='flex items-center gap-2'>
<span>{statusConfig.icon}</span>
<Tag color={statusConfig.color}>
{t(statusConfig.text)}
</Tag>
</div>
)
}
]} />
),
},
{
key: t('创建时间'),
value: timestamp2string(details.created_at),
},
]}
/>
</Card>
{/* Hardware & Performance */}
<Card
title={
<div className='flex items-center gap-2'>
<FaChartLine className='text-green-500' />
<span>{t('硬件与性能')}</span>
</div>
}
className='border-0 shadow-sm'
>
<div className='space-y-4'>
<Descriptions
data={[
{
key: t('硬件类型'),
value: (
<div className='flex items-center gap-2'>
<Tag color='blue'>{details.brand_name}</Tag>
<Text strong>{details.hardware_name}</Text>
</div>
),
},
{
key: t('GPU数量'),
value: (
<div className='flex items-center gap-2'>
<Badge
count={details.total_gpus}
theme='solid'
type='primary'
>
<FaServer className='text-purple-500' />
</Badge>
<Text>
{t('总计')} {details.total_gpus} {t('个GPU')}
</Text>
</div>
),
},
{
key: t('容器配置'),
value: (
<div className='space-y-1'>
<div>
{t('每容器GPU数')}: {details.gpus_per_container}
</div>
<div>
{t('容器总数')}: {details.total_containers}
</div>
</div>
),
},
]}
/>
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Text strong>{t('完成进度')}</Text>
<Text>{details.completed_percent}%</Text>
</div>
<Progress
percent={details.completed_percent}
status={details.completed_percent === 100 ? 'success' : 'normal'}
status={
details.completed_percent === 100 ? 'success' : 'normal'
}
strokeWidth={8}
showInfo={false}
/>
<div className="flex justify-between text-xs text-gray-500">
<span>{t('已服务')}: {details.compute_minutes_served} {t('分钟')}</span>
<span>{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}</span>
<div className='flex justify-between text-xs text-gray-500'>
<span>
{t('已服务')}: {details.compute_minutes_served} {t('分钟')}
</span>
<span>
{t('剩余')}: {details.compute_minutes_remaining} {t('分钟')}
</span>
</div>
</div>
</div>
......@@ -277,56 +309,70 @@ const ViewDetailsModal = ({
{/* Container Configuration */}
{details.container_config && (
<Card
<Card
title={
<div className="flex items-center gap-2">
<FaDocker className="text-blue-600" />
<div className='flex items-center gap-2'>
<FaDocker className='text-blue-600' />
<span>{t('容器配置')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
<div className="space-y-3">
<Descriptions data={[
{
key: t('镜像地址'),
value: (
<Text className="font-mono text-sm break-all">
{details.container_config.image_url || 'N/A'}
</Text>
)
},
{
key: t('流量端口'),
value: details.container_config.traffic_port || 'N/A'
},
{
key: t('启动命令'),
value: (
<Text className="font-mono text-sm">
{details.container_config.entrypoint ?
details.container_config.entrypoint.join(' ') : 'N/A'
}
</Text>
)
}
]} />
<div className='space-y-3'>
<Descriptions
data={[
{
key: t('镜像地址'),
value: (
<Text className='font-mono text-sm break-all'>
{details.container_config.image_url || 'N/A'}
</Text>
),
},
{
key: t('流量端口'),
value: details.container_config.traffic_port || 'N/A',
},
{
key: t('启动命令'),
value: (
<Text className='font-mono text-sm'>
{details.container_config.entrypoint
? details.container_config.entrypoint.join(' ')
: 'N/A'}
</Text>
),
},
]}
/>
{/* Environment Variables */}
{details.container_config.env_variables &&
Object.keys(details.container_config.env_variables).length > 0 && (
<div className="mt-4">
<Text strong className="block mb-2">{t('环境变量')}:</Text>
<div className="bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto">
{Object.entries(details.container_config.env_variables).map(([key, value]) => (
<div key={key} className="flex gap-2 text-sm font-mono mb-1">
<span className="text-blue-600 font-medium">{key}=</span>
<span className="text-gray-700 break-all">{String(value)}</span>
</div>
))}
{details.container_config.env_variables &&
Object.keys(details.container_config.env_variables).length >
0 && (
<div className='mt-4'>
<Text strong className='block mb-2'>
{t('环境变量')}:
</Text>
<div className='bg-gray-50 p-3 rounded-lg max-h-32 overflow-y-auto'>
{Object.entries(
details.container_config.env_variables,
).map(([key, value]) => (
<div
key={key}
className='flex gap-2 text-sm font-mono mb-1'
>
<span className='text-blue-600 font-medium'>
{key}=
</span>
<span className='text-gray-700 break-all'>
{String(value)}
</span>
</div>
))}
</div>
</div>
</div>
)}
)}
</div>
</Card>
)}
......@@ -334,50 +380,63 @@ const ViewDetailsModal = ({
{/* Containers List */}
<Card
title={
<div className="flex items-center gap-2">
<FaServer className="text-indigo-500" />
<div className='flex items-center gap-2'>
<FaServer className='text-indigo-500' />
<span>{t('容器实例')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
{containersLoading ? (
<div className="flex items-center justify-center py-6">
<div className='flex items-center justify-center py-6'>
<Spin tip={t('加载容器信息中...')} />
</div>
) : containers.length === 0 ? (
<Empty description={t('暂无容器信息')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
<Empty
description={t('暂无容器信息')}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
) : (
<div className="space-y-3">
<div className='space-y-3'>
{containers.map((ctr) => (
<Card
key={ctr.container_id}
className="bg-gray-50 border border-gray-100"
className='bg-gray-50 border border-gray-100'
bodyStyle={{ padding: '12px 16px' }}
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col gap-1">
<Text strong className="font-mono text-sm">
<div className='flex flex-wrap items-center justify-between gap-3'>
<div className='flex flex-col gap-1'>
<Text strong className='font-mono text-sm'>
{ctr.container_id}
</Text>
<Text size="small" type="secondary">
{t('设备')} {ctr.device_id || '--'} · {t('状态')} {ctr.status || '--'}
<Text size='small' type='secondary'>
{t('设备')} {ctr.device_id || '--'} · {t('状态')}{' '}
{ctr.status || '--'}
</Text>
<Text size="small" type="secondary">
{t('创建时间')}: {ctr.created_at ? timestamp2string(ctr.created_at) : '--'}
<Text size='small' type='secondary'>
{t('创建时间')}:{' '}
{ctr.created_at
? timestamp2string(ctr.created_at)
: '--'}
</Text>
</div>
<div className="flex flex-col items-end gap-2">
<Tag color="blue" size="small">
<div className='flex flex-col items-end gap-2'>
<Tag color='blue' size='small'>
{t('GPU/容器')}: {ctr.gpus_per_container ?? '--'}
</Tag>
{ctr.public_url && (
<Tooltip content={ctr.public_url}>
<Button
icon={<FaLink />}
size="small"
theme="light"
onClick={() => window.open(ctr.public_url, '_blank', 'noopener,noreferrer')}
size='small'
theme='light'
onClick={() =>
window.open(
ctr.public_url,
'_blank',
'noopener,noreferrer',
)
}
>
{t('访问容器')}
</Button>
......@@ -387,17 +446,26 @@ const ViewDetailsModal = ({
</div>
{ctr.events && ctr.events.length > 0 && (
<div className="mt-3 bg-white rounded-md border border-gray-100 p-3">
<Text size="small" type="secondary" className="block mb-2">
<div className='mt-3 bg-white rounded-md border border-gray-100 p-3'>
<Text
size='small'
type='secondary'
className='block mb-2'
>
{t('最近事件')}
</Text>
<div className="space-y-2 max-h-32 overflow-y-auto">
<div className='space-y-2 max-h-32 overflow-y-auto'>
{ctr.events.map((event, index) => (
<div key={`${ctr.container_id}-${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
<span className="text-gray-500 min-w-[140px]">
{event.time ? timestamp2string(event.time) : '--'}
<div
key={`${ctr.container_id}-${event.time}-${index}`}
className='flex gap-3 text-xs font-mono'
>
<span className='text-gray-500 min-w-[140px]'>
{event.time
? timestamp2string(event.time)
: '--'}
</span>
<span className="text-gray-700 break-all flex-1">
<span className='text-gray-700 break-all flex-1'>
{event.message || '--'}
</span>
</div>
......@@ -413,21 +481,23 @@ const ViewDetailsModal = ({
{/* Location Information */}
{details.locations && details.locations.length > 0 && (
<Card
<Card
title={
<div className="flex items-center gap-2">
<FaMapMarkerAlt className="text-orange-500" />
<div className='flex items-center gap-2'>
<FaMapMarkerAlt className='text-orange-500' />
<span>{t('部署位置')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
<div className="flex flex-wrap gap-2">
<div className='flex flex-wrap gap-2'>
{details.locations.map((location) => (
<Tag key={location.id} color="orange" size="large">
<div className="flex items-center gap-1">
<Tag key={location.id} color='orange' size='large'>
<div className='flex items-center gap-1'>
<span>🌍</span>
<span>{location.name} ({location.iso2})</span>
<span>
{location.name} ({location.iso2})
</span>
</div>
</Tag>
))}
......@@ -436,68 +506,82 @@ const ViewDetailsModal = ({
)}
{/* Cost Information */}
<Card
<Card
title={
<div className="flex items-center gap-2">
<FaMoneyBillWave className="text-green-500" />
<div className='flex items-center gap-2'>
<FaMoneyBillWave className='text-green-500' />
<span>{t('费用信息')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div className='space-y-3'>
<div className='flex items-center justify-between p-3 bg-green-50 rounded-lg'>
<Text>{t('已支付金额')}</Text>
<Text strong className="text-lg text-green-600">
${details.amount_paid ? details.amount_paid.toFixed(2) : '0.00'} USDC
<Text strong className='text-lg text-green-600'>
$
{details.amount_paid
? details.amount_paid.toFixed(2)
: '0.00'}{' '}
USDC
</Text>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between">
<Text type="secondary">{t('计费开始')}:</Text>
<Text>{details.started_at ? timestamp2string(details.started_at) : 'N/A'}</Text>
<div className='grid grid-cols-2 gap-4 text-sm'>
<div className='flex justify-between'>
<Text type='secondary'>{t('计费开始')}:</Text>
<Text>
{details.started_at
? timestamp2string(details.started_at)
: 'N/A'}
</Text>
</div>
<div className="flex justify-between">
<Text type="secondary">{t('预计结束')}:</Text>
<Text>{details.finished_at ? timestamp2string(details.finished_at) : 'N/A'}</Text>
<div className='flex justify-between'>
<Text type='secondary'>{t('预计结束')}:</Text>
<Text>
{details.finished_at
? timestamp2string(details.finished_at)
: 'N/A'}
</Text>
</div>
</div>
</div>
</Card>
{/* Time Information */}
<Card
<Card
title={
<div className="flex items-center gap-2">
<FaClock className="text-purple-500" />
<div className='flex items-center gap-2'>
<FaClock className='text-purple-500' />
<span>{t('时间信息')}</span>
</div>
}
className="border-0 shadow-sm"
className='border-0 shadow-sm'
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Text type="secondary">{t('已运行时间')}:</Text>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Text type='secondary'>{t('已运行时间')}:</Text>
<Text strong>
{Math.floor(details.compute_minutes_served / 60)}h {details.compute_minutes_served % 60}m
{Math.floor(details.compute_minutes_served / 60)}h{' '}
{details.compute_minutes_served % 60}m
</Text>
</div>
<div className="flex items-center justify-between">
<Text type="secondary">{t('剩余时间')}:</Text>
<Text strong className="text-orange-600">
{Math.floor(details.compute_minutes_remaining / 60)}h {details.compute_minutes_remaining % 60}m
<div className='flex items-center justify-between'>
<Text type='secondary'>{t('剩余时间')}:</Text>
<Text strong className='text-orange-600'>
{Math.floor(details.compute_minutes_remaining / 60)}h{' '}
{details.compute_minutes_remaining % 60}m
</Text>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Text type="secondary">{t('创建时间')}:</Text>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Text type='secondary'>{t('创建时间')}:</Text>
<Text>{timestamp2string(details.created_at)}</Text>
</div>
<div className="flex items-center justify-between">
<Text type="secondary">{t('最后更新')}:</Text>
<div className='flex items-center justify-between'>
<Text type='secondary'>{t('最后更新')}:</Text>
<Text>{timestamp2string(details.updated_at)}</Text>
</div>
</div>
......@@ -505,7 +589,7 @@ const ViewDetailsModal = ({
</Card>
</div>
) : (
<Empty
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('无法获取容器详情')}
/>
......
......@@ -44,18 +44,19 @@ import {
FaLink,
} from 'react-icons/fa';
import { IconRefresh, IconDownload } from '@douyinfe/semi-icons';
import { API, showError, showSuccess, copy, timestamp2string } from '../../../../helpers';
import {
API,
showError,
showSuccess,
copy,
timestamp2string,
} from '../../../../helpers';
const { Text } = Typography;
const ALL_CONTAINERS = '__all__';
const ViewLogsModal = ({
visible,
onCancel,
deployment,
t
}) => {
const ViewLogsModal = ({ visible, onCancel, deployment, t }) => {
const [logLines, setLogLines] = useState([]);
const [loading, setLoading] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(false);
......@@ -63,12 +64,13 @@ const ViewLogsModal = ({
const [following, setFollowing] = useState(false);
const [containers, setContainers] = useState([]);
const [containersLoading, setContainersLoading] = useState(false);
const [selectedContainerId, setSelectedContainerId] = useState(ALL_CONTAINERS);
const [selectedContainerId, setSelectedContainerId] =
useState(ALL_CONTAINERS);
const [containerDetails, setContainerDetails] = useState(null);
const [containerDetailsLoading, setContainerDetailsLoading] = useState(false);
const [streamFilter, setStreamFilter] = useState('stdout');
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
const logContainerRef = useRef(null);
const autoRefreshRef = useRef(null);
......@@ -100,7 +102,10 @@ const ViewLogsModal = ({
const fetchLogs = async (containerIdOverride = undefined) => {
if (!deployment?.id) return;
const containerId = typeof containerIdOverride === 'string' ? containerIdOverride : selectedContainerId;
const containerId =
typeof containerIdOverride === 'string'
? containerIdOverride
: selectedContainerId;
if (!containerId || containerId === ALL_CONTAINERS) {
setLogLines([]);
......@@ -120,10 +125,13 @@ const ViewLogsModal = ({
}
if (following) params.append('follow', 'true');
const response = await API.get(`/api/deployments/${deployment.id}/logs?${params}`);
const response = await API.get(
`/api/deployments/${deployment.id}/logs?${params}`,
);
if (response.data.success) {
const rawContent = typeof response.data.data === 'string' ? response.data.data : '';
const rawContent =
typeof response.data.data === 'string' ? response.data.data : '';
const normalized = rawContent.replace(/\r\n?/g, '\n');
const lines = normalized ? normalized.split('\n') : [];
......@@ -133,7 +141,11 @@ const ViewLogsModal = ({
setTimeout(scrollToBottom, 100);
}
} catch (error) {
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取日志失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setLoading(false);
}
......@@ -144,14 +156,19 @@ const ViewLogsModal = ({
setContainersLoading(true);
try {
const response = await API.get(`/api/deployments/${deployment.id}/containers`);
const response = await API.get(
`/api/deployments/${deployment.id}/containers`,
);
if (response.data.success) {
const list = response.data.data?.containers || [];
setContainers(list);
setSelectedContainerId((current) => {
if (current !== ALL_CONTAINERS && list.some(item => item.container_id === current)) {
if (
current !== ALL_CONTAINERS &&
list.some((item) => item.container_id === current)
) {
return current;
}
......@@ -163,7 +180,11 @@ const ViewLogsModal = ({
}
}
} catch (error) {
showError(t('获取容器列表失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取容器列表失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setContainersLoading(false);
}
......@@ -177,13 +198,19 @@ const ViewLogsModal = ({
setContainerDetailsLoading(true);
try {
const response = await API.get(`/api/deployments/${deployment.id}/containers/${containerId}`);
const response = await API.get(
`/api/deployments/${deployment.id}/containers/${containerId}`,
);
if (response.data.success) {
setContainerDetails(response.data.data || null);
}
} catch (error) {
showError(t('获取容器详情失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取容器详情失败') +
': ' +
(error.response?.data?.message || error.message),
);
} finally {
setContainerDetailsLoading(false);
}
......@@ -205,13 +232,14 @@ const ViewLogsModal = ({
const renderContainerStatusTag = (status) => {
if (!status) {
return (
<Tag color="grey" size="small">
<Tag color='grey' size='small'>
{t('未知状态')}
</Tag>
);
}
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : '';
const normalized =
typeof status === 'string' ? status.trim().toLowerCase() : '';
const statusMap = {
running: { color: 'green', label: '运行中' },
pending: { color: 'orange', label: '准备中' },
......@@ -225,15 +253,16 @@ const ViewLogsModal = ({
const config = statusMap[normalized] || { color: 'grey', label: status };
return (
<Tag color={config.color} size="small">
<Tag color={config.color} size='small'>
{t(config.label)}
</Tag>
);
};
const currentContainer = selectedContainerId !== ALL_CONTAINERS
? containers.find((ctr) => ctr.container_id === selectedContainerId)
: null;
const currentContainer =
selectedContainerId !== ALL_CONTAINERS
? containers.find((ctr) => ctr.container_id === selectedContainerId)
: null;
const refreshLogs = () => {
if (selectedContainerId && selectedContainerId !== ALL_CONTAINERS) {
......@@ -254,9 +283,10 @@ const ViewLogsModal = ({
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const safeContainerId = selectedContainerId && selectedContainerId !== ALL_CONTAINERS
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
: '';
const safeContainerId =
selectedContainerId && selectedContainerId !== ALL_CONTAINERS
? selectedContainerId.replace(/[^a-zA-Z0-9_-]/g, '-')
: '';
const fileName = safeContainerId
? `deployment-${deployment.id}-container-${safeContainerId}-logs.txt`
: `deployment-${deployment.id}-logs.txt`;
......@@ -265,7 +295,7 @@ const ViewLogsModal = ({
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showSuccess(t('日志已下载'));
};
......@@ -346,14 +376,15 @@ const ViewLogsModal = ({
// Filter logs based on search term
const filteredLogs = logLines
.map((line) => line ?? '')
.filter((line) =>
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
.filter(
(line) =>
!searchTerm || line.toLowerCase().includes(searchTerm.toLowerCase()),
);
const renderLogEntry = (line, index) => (
<div
key={`${index}-${line.slice(0, 20)}`}
className="py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words"
className='py-1 px-3 hover:bg-gray-50 font-mono text-sm border-b border-gray-100 whitespace-pre-wrap break-words'
>
{line}
</div>
......@@ -362,10 +393,10 @@ const ViewLogsModal = ({
return (
<Modal
title={
<div className="flex items-center gap-2">
<FaTerminal className="text-blue-500" />
<div className='flex items-center gap-2'>
<FaTerminal className='text-blue-500' />
<span>{t('容器日志')}</span>
<Text type="secondary" size="small">
<Text type='secondary' size='small'>
- {deployment?.container_name || deployment?.id}
</Text>
</div>
......@@ -375,13 +406,13 @@ const ViewLogsModal = ({
footer={null}
width={1000}
height={700}
className="logs-modal"
className='logs-modal'
style={{ top: 20 }}
>
<div className="flex flex-col h-full max-h-[600px]">
<div className='flex flex-col h-full max-h-[600px]'>
{/* Controls */}
<Card className="mb-4 border-0 shadow-sm">
<div className="flex items-center justify-between flex-wrap gap-3">
<Card className='mb-4 border-0 shadow-sm'>
<div className='flex items-center justify-between flex-wrap gap-3'>
<Space wrap>
<Select
prefix={<FaServer />}
......@@ -389,7 +420,7 @@ const ViewLogsModal = ({
value={selectedContainerId}
onChange={handleContainerChange}
style={{ width: 240 }}
size="small"
size='small'
loading={containersLoading}
dropdownStyle={{ maxHeight: 320, overflowY: 'auto' }}
>
......@@ -397,10 +428,15 @@ const ViewLogsModal = ({
{t('全部容器')}
</Select.Option>
{containers.map((ctr) => (
<Select.Option key={ctr.container_id} value={ctr.container_id}>
<div className="flex flex-col">
<span className="font-mono text-xs">{ctr.container_id}</span>
<span className="text-xs text-gray-500">
<Select.Option
key={ctr.container_id}
value={ctr.container_id}
>
<div className='flex flex-col'>
<span className='font-mono text-xs'>
{ctr.container_id}
</span>
<span className='text-xs text-gray-500'>
{ctr.brand_name || 'IO.NET'}
{ctr.hardware ? ` · ${ctr.hardware}` : ''}
</span>
......@@ -415,114 +451,118 @@ const ViewLogsModal = ({
value={searchTerm}
onChange={setSearchTerm}
style={{ width: 200 }}
size="small"
size='small'
/>
<Space align="center" className="ml-2">
<Text size="small" type="secondary">
<Space align='center' className='ml-2'>
<Text size='small' type='secondary'>
{t('日志流')}
</Text>
<Radio.Group
type="button"
size="small"
type='button'
size='small'
value={streamFilter}
onChange={handleStreamChange}
>
<Radio value="stdout">STDOUT</Radio>
<Radio value="stderr">STDERR</Radio>
<Radio value='stdout'>STDOUT</Radio>
<Radio value='stderr'>STDERR</Radio>
</Radio.Group>
</Space>
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Switch
checked={autoRefresh}
onChange={setAutoRefresh}
size="small"
size='small'
/>
<Text size="small">{t('自动刷新')}</Text>
<Text size='small'>{t('自动刷新')}</Text>
</div>
<div className="flex items-center gap-2">
<div className='flex items-center gap-2'>
<Switch
checked={following}
onChange={setFollowing}
size="small"
size='small'
/>
<Text size="small">{t('跟随日志')}</Text>
<Text size='small'>{t('跟随日志')}</Text>
</div>
</Space>
<Space>
<Tooltip content={t('刷新日志')}>
<Button
icon={<IconRefresh />}
<Button
icon={<IconRefresh />}
onClick={refreshLogs}
loading={loading}
size="small"
theme="borderless"
size='small'
theme='borderless'
/>
</Tooltip>
<Tooltip content={t('复制日志')}>
<Button
icon={<FaCopy />}
<Button
icon={<FaCopy />}
onClick={copyAllLogs}
size="small"
theme="borderless"
size='small'
theme='borderless'
disabled={logLines.length === 0}
/>
</Tooltip>
<Tooltip content={t('下载日志')}>
<Button
icon={<IconDownload />}
<Button
icon={<IconDownload />}
onClick={downloadLogs}
size="small"
theme="borderless"
size='small'
theme='borderless'
disabled={logLines.length === 0}
/>
</Tooltip>
</Space>
</div>
{/* Status Info */}
<Divider margin="12px" />
<div className="flex items-center justify-between">
<Space size="large">
<Text size="small" type="secondary">
<Divider margin='12px' />
<div className='flex items-center justify-between'>
<Space size='large'>
<Text size='small' type='secondary'>
{t('共 {{count}} 条日志', { count: logLines.length })}
</Text>
{searchTerm && (
<Text size="small" type="secondary">
{t('(筛选后显示 {{count}} )', { count: filteredLogs.length })}
<Text size='small' type='secondary'>
{t('(筛选后显示 {{count}} )', {
count: filteredLogs.length,
})}
</Text>
)}
{autoRefresh && (
<Tag color="green" size="small">
<FaClock className="mr-1" />
<Tag color='green' size='small'>
<FaClock className='mr-1' />
{t('自动刷新中')}
</Tag>
)}
</Space>
<Text size="small" type="secondary">
<Text size='small' type='secondary'>
{t('状态')}: {deployment?.status || 'unknown'}
</Text>
</div>
{selectedContainerId !== ALL_CONTAINERS && (
<>
<Divider margin="12px" />
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<Divider margin='12px' />
<div className='flex flex-col gap-3'>
<div className='flex items-center justify-between flex-wrap gap-2'>
<Space>
<Tag color="blue" size="small">
<Tag color='blue' size='small'>
{t('容器')}
</Tag>
<Text className="font-mono text-xs">
<Text className='font-mono text-xs'>
{selectedContainerId}
</Text>
{renderContainerStatusTag(containerDetails?.status || currentContainer?.status)}
{renderContainerStatusTag(
containerDetails?.status || currentContainer?.status,
)}
</Space>
<Space>
......@@ -530,9 +570,11 @@ const ViewLogsModal = ({
<Tooltip content={containerDetails.public_url}>
<Button
icon={<FaLink />}
size="small"
theme="borderless"
onClick={() => window.open(containerDetails.public_url, '_blank')}
size='small'
theme='borderless'
onClick={() =>
window.open(containerDetails.public_url, '_blank')
}
/>
</Tooltip>
)}
......@@ -540,8 +582,8 @@ const ViewLogsModal = ({
<Button
icon={<IconRefresh />}
onClick={refreshContainerDetails}
size="small"
theme="borderless"
size='small'
theme='borderless'
loading={containerDetailsLoading}
/>
</Tooltip>
......@@ -549,27 +591,36 @@ const ViewLogsModal = ({
</div>
{containerDetailsLoading ? (
<div className="flex items-center justify-center py-6">
<div className='flex items-center justify-center py-6'>
<Spin tip={t('加载容器详情中...')} />
</div>
) : containerDetails ? (
<div className="grid gap-4 md:grid-cols-2 text-sm">
<div className="flex items-center gap-2">
<FaInfoCircle className="text-blue-500" />
<Text type="secondary">{t('硬件')}</Text>
<div className='grid gap-4 md:grid-cols-2 text-sm'>
<div className='flex items-center gap-2'>
<FaInfoCircle className='text-blue-500' />
<Text type='secondary'>{t('硬件')}</Text>
<Text>
{containerDetails?.brand_name || currentContainer?.brand_name || t('未知品牌')}
{(containerDetails?.hardware || currentContainer?.hardware) ? ` · ${containerDetails?.hardware || currentContainer?.hardware}` : ''}
{containerDetails?.brand_name ||
currentContainer?.brand_name ||
t('未知品牌')}
{containerDetails?.hardware ||
currentContainer?.hardware
? ` · ${containerDetails?.hardware || currentContainer?.hardware}`
: ''}
</Text>
</div>
<div className="flex items-center gap-2">
<FaServer className="text-purple-500" />
<Text type="secondary">{t('GPU/容器')}</Text>
<Text>{containerDetails?.gpus_per_container ?? currentContainer?.gpus_per_container ?? 0}</Text>
<div className='flex items-center gap-2'>
<FaServer className='text-purple-500' />
<Text type='secondary'>{t('GPU/容器')}</Text>
<Text>
{containerDetails?.gpus_per_container ??
currentContainer?.gpus_per_container ??
0}
</Text>
</div>
<div className="flex items-center gap-2">
<FaClock className="text-orange-500" />
<Text type="secondary">{t('创建时间')}</Text>
<div className='flex items-center gap-2'>
<FaClock className='text-orange-500' />
<Text type='secondary'>{t('创建时间')}</Text>
<Text>
{containerDetails?.created_at
? timestamp2string(containerDetails.created_at)
......@@ -578,51 +629,64 @@ const ViewLogsModal = ({
: t('未知')}
</Text>
</div>
<div className="flex items-center gap-2">
<FaInfoCircle className="text-green-500" />
<Text type="secondary">{t('运行时长')}</Text>
<Text>{containerDetails?.uptime_percent ?? currentContainer?.uptime_percent ?? 0}%</Text>
<div className='flex items-center gap-2'>
<FaInfoCircle className='text-green-500' />
<Text type='secondary'>{t('运行时长')}</Text>
<Text>
{containerDetails?.uptime_percent ??
currentContainer?.uptime_percent ??
0}
%
</Text>
</div>
</div>
) : (
<Text size="small" type="secondary">
<Text size='small' type='secondary'>
{t('暂无容器详情')}
</Text>
)}
{containerDetails?.events && containerDetails.events.length > 0 && (
<div className="bg-gray-50 rounded-lg p-3">
<Text size="small" type="secondary">
{t('最近事件')}
</Text>
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto">
{containerDetails.events.slice(0, 5).map((event, index) => (
<div key={`${event.time}-${index}`} className="flex gap-3 text-xs font-mono">
<span className="text-gray-500">
{event.time ? timestamp2string(event.time) : '--'}
</span>
<span className="text-gray-700 break-all flex-1">
{event.message}
</span>
</div>
))}
{containerDetails?.events &&
containerDetails.events.length > 0 && (
<div className='bg-gray-50 rounded-lg p-3'>
<Text size='small' type='secondary'>
{t('最近事件')}
</Text>
<div className='mt-2 space-y-2 max-h-32 overflow-y-auto'>
{containerDetails.events
.slice(0, 5)
.map((event, index) => (
<div
key={`${event.time}-${index}`}
className='flex gap-3 text-xs font-mono'
>
<span className='text-gray-500'>
{event.time
? timestamp2string(event.time)
: '--'}
</span>
<span className='text-gray-700 break-all flex-1'>
{event.message}
</span>
</div>
))}
</div>
</div>
</div>
)}
)}
</div>
</>
)}
</Card>
{/* Log Content */}
<div className="flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden">
<div
<div className='flex-1 flex flex-col border rounded-lg bg-gray-50 overflow-hidden'>
<div
ref={logContainerRef}
className="flex-1 overflow-y-auto bg-white"
className='flex-1 overflow-y-auto bg-white'
style={{ maxHeight: '400px' }}
>
{loading && logLines.length === 0 ? (
<div className="flex items-center justify-center p-8">
<div className='flex items-center justify-center p-8'>
<Spin tip={t('加载日志中...')} />
</div>
) : filteredLogs.length === 0 ? (
......@@ -639,15 +703,14 @@ const ViewLogsModal = ({
</div>
)}
</div>
{/* Footer status */}
{logLines.length > 0 && (
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500">
<span>
{following ? t('正在跟随最新日志') : t('日志已加载')}
</span>
<div className='flex items-center justify-between px-3 py-2 bg-gray-50 border-t text-xs text-gray-500'>
<span>{following ? t('正在跟随最新日志') : t('日志已加载')}</span>
<span>
{t('最后更新')}: {lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
{t('最后更新')}:{' '}
{lastUpdatedAt ? lastUpdatedAt.toLocaleTimeString() : '--'}
</span>
</div>
)}
......
......@@ -25,6 +25,56 @@ import { API } from '../../helpers';
const sidebarEventTarget = new EventTarget();
const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
export const DEFAULT_ADMIN_CONFIG = {
chat: {
enabled: true,
playground: true,
chat: true,
},
console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: {
enabled: true,
topup: true,
personal: true,
},
admin: {
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
},
};
const deepClone = (value) => JSON.parse(JSON.stringify(value));
export const mergeAdminConfig = (savedConfig) => {
const merged = deepClone(DEFAULT_ADMIN_CONFIG);
if (!savedConfig || typeof savedConfig !== 'object') return merged;
for (const [sectionKey, sectionConfig] of Object.entries(savedConfig)) {
if (!sectionConfig || typeof sectionConfig !== 'object') continue;
if (!merged[sectionKey]) {
merged[sectionKey] = { ...sectionConfig };
continue;
}
merged[sectionKey] = { ...merged[sectionKey], ...sectionConfig };
}
return merged;
};
export const useSidebar = () => {
const [statusState] = useContext(StatusContext);
const [userConfig, setUserConfig] = useState(null);
......@@ -37,48 +87,17 @@ export const useSidebar = () => {
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
}
// 默认配置
const defaultAdminConfig = {
chat: {
enabled: true,
playground: true,
chat: true,
},
console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: {
enabled: true,
topup: true,
personal: true,
},
admin: {
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
},
};
// 获取管理员配置
const adminConfig = useMemo(() => {
if (statusState?.status?.SidebarModulesAdmin) {
try {
const config = JSON.parse(statusState.status.SidebarModulesAdmin);
return config;
return mergeAdminConfig(config);
} catch (error) {
return defaultAdminConfig;
return mergeAdminConfig(null);
}
}
return defaultAdminConfig;
return mergeAdminConfig(null);
}, [statusState?.status?.SidebarModulesAdmin]);
// 加载用户配置的通用方法
......
......@@ -39,10 +39,13 @@ export const useDeploymentResources = () => {
setLoadingHardware(true);
const response = await API.get('/api/deployments/hardware-types');
if (response.data.success) {
const { hardware_types: hardwareList = [], total_available } = response.data.data || {};
const { hardware_types: hardwareList = [], total_available } =
response.data.data || {};
const normalizedHardware = hardwareList.map((hardware) => {
const availableCountValue = Number(hardware.available_count);
const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue;
const availableCount = Number.isNaN(availableCountValue)
? 0
: availableCountValue;
const availableBool =
typeof hardware.available === 'boolean'
? hardware.available
......@@ -57,7 +60,9 @@ export const useDeploymentResources = () => {
const providedTotal = Number(total_available);
const fallbackTotal = normalizedHardware.reduce(
(acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
(acc, item) =>
acc +
(Number.isNaN(item.available_count) ? 0 : item.available_count),
0,
);
const hasProvidedTotal =
......@@ -85,37 +90,64 @@ export const useDeploymentResources = () => {
}
}, []);
const fetchLocations = useCallback(async () => {
const fetchLocations = useCallback(async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setLocations([]);
setLocationsTotalAvailable(0);
return [];
}
try {
setLoadingLocations(true);
const response = await API.get('/api/deployments/locations');
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
);
if (response.data.success) {
const { locations: locationsList = [], total } = response.data.data || {};
const normalizedLocations = locationsList.map((location) => {
const iso2 = (location.iso2 || '').toString().toUpperCase();
const availableValue = Number(location.available);
const available = Number.isNaN(availableValue) ? 0 : availableValue;
const replicas = response.data.data?.replicas || [];
const nextLocationsMap = new Map();
replicas.forEach((replica) => {
const rawId = replica?.location_id ?? replica?.location?.id;
if (rawId === null || rawId === undefined) return;
return {
...location,
const mapKey = String(rawId);
if (nextLocationsMap.has(mapKey)) return;
const rawIso2 =
replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2;
const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : '';
const name =
replica?.location_name ??
replica?.location?.name ??
replica?.name ??
String(rawId);
nextLocationsMap.set(mapKey, {
id: rawId,
name: String(name),
iso2,
available,
};
region:
replica?.region ??
replica?.location_region ??
replica?.location?.region,
country:
replica?.country ??
replica?.location_country ??
replica?.location?.country,
code:
replica?.code ??
replica?.location_code ??
replica?.location?.code,
available: Number(replica?.available_count) || 0,
});
});
const providedTotal = Number(total);
const fallbackTotal = normalizedLocations.reduce(
(acc, item) => acc + (Number.isNaN(item.available) ? 0 : item.available),
0,
);
const hasProvidedTotal =
total !== undefined &&
total !== null &&
total !== '' &&
!Number.isNaN(providedTotal);
const normalizedLocations = Array.from(nextLocationsMap.values());
setLocations(normalizedLocations);
setLocationsTotalAvailable(
hasProvidedTotal ? providedTotal : fallbackTotal,
normalizedLocations.reduce(
(acc, item) => acc + (item.available || 0),
0,
),
);
return normalizedLocations;
} else {
......@@ -132,34 +164,37 @@ export const useDeploymentResources = () => {
}
}, []);
const fetchAvailableReplicas = useCallback(async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setAvailableReplicas([]);
return [];
}
const fetchAvailableReplicas = useCallback(
async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setAvailableReplicas([]);
return [];
}
try {
setLoadingReplicas(true);
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`
);
if (response.data.success) {
const replicas = response.data.data.replicas || [];
setAvailableReplicas(replicas);
return replicas;
} else {
showError('获取可用资源失败: ' + response.data.message);
try {
setLoadingReplicas(true);
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
);
if (response.data.success) {
const replicas = response.data.data.replicas || [];
setAvailableReplicas(replicas);
return replicas;
} else {
showError('获取可用资源失败: ' + response.data.message);
setAvailableReplicas([]);
return [];
}
} catch (error) {
console.error('Load available replicas error:', error);
setAvailableReplicas([]);
return [];
} finally {
setLoadingReplicas(false);
}
} catch (error) {
console.error('Load available replicas error:', error);
setAvailableReplicas([]);
return [];
} finally {
setLoadingReplicas(false);
}
}, []);
},
[],
);
const calculatePrice = useCallback(async (params) => {
const {
......@@ -167,10 +202,16 @@ export const useDeploymentResources = () => {
hardwareId,
gpusPerContainer,
durationHours,
replicaCount
replicaCount,
} = params;
if (!locationIds?.length || !hardwareId || !gpusPerContainer || !durationHours || !replicaCount) {
if (
!locationIds?.length ||
!hardwareId ||
!gpusPerContainer ||
!durationHours ||
!replicaCount
) {
setPriceEstimation(null);
return null;
}
......@@ -185,7 +226,10 @@ export const useDeploymentResources = () => {
replica_count: replicaCount,
};
const response = await API.post('/api/deployments/price-estimation', requestData);
const response = await API.post(
'/api/deployments/price-estimation',
requestData,
);
if (response.data.success) {
const estimation = response.data.data;
setPriceEstimation(estimation);
......@@ -208,7 +252,9 @@ export const useDeploymentResources = () => {
if (!name?.trim()) return false;
try {
const response = await API.get(`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`);
const response = await API.get(
`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`,
);
if (response.data.success) {
return response.data.data.available;
} else {
......
......@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess } from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
......@@ -26,6 +26,7 @@ import { useTableCompactMode } from '../common/useTableCompactMode';
export const useDeploymentsData = () => {
const { t } = useTranslation();
const [compactMode, setCompactMode] = useTableCompactMode('deployments');
const requestSeq = useRef(0);
// State management
const [deployments, setDeployments] = useState([]);
......@@ -34,6 +35,7 @@ export const useDeploymentsData = () => {
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [searching, setSearching] = useState(false);
const [deploymentCount, setDeploymentCount] = useState(0);
const [query, setQuery] = useState({ keyword: '', status: '' });
// Modal states
const [showEdit, setShowEdit] = useState(false);
......@@ -80,18 +82,12 @@ export const useDeploymentsData = () => {
}, 500);
};
// Set deployment format with key field
const setDeploymentFormat = (deployments) => {
for (let i = 0; i < deployments.length; i++) {
deployments[i].key = deployments[i].id;
}
setDeployments(deployments);
const normalizeQuery = (terms) => {
const keyword = (terms?.searchKeyword ?? '').trim();
const status = (terms?.searchStatus ?? '').trim();
return { keyword, status };
};
// Status tabs
const [activeStatusKey, setActiveStatusKey] = useState('all');
const [statusCounts, setStatusCounts] = useState({});
// Column visibility
const COLUMN_KEYS = useMemo(
() => ({
......@@ -160,114 +156,127 @@ export const useDeploymentsData = () => {
// Save column visibility to localStorage
const saveColumnVisibility = (newVisibleColumns) => {
const normalized = ensureRequiredColumns(newVisibleColumns);
localStorage.setItem('deployments_visible_columns', JSON.stringify(normalized));
localStorage.setItem(
'deployments_visible_columns',
JSON.stringify(normalized),
);
setVisibleColumnsState(normalized);
};
// Load deployments data
const loadDeployments = async (
page = 1,
size = pageSize,
statusKey = activeStatusKey,
) => {
setLoading(true);
try {
let url = `/api/deployments/?p=${page}&page_size=${size}`;
if (statusKey && statusKey !== 'all') {
url = `/api/deployments/search?status=${statusKey}&p=${page}&page_size=${size}`;
}
const applyDeploymentsData = ({ data, page }) => {
const items = extractItems(data);
setActivePage(data?.page ?? page);
setDeploymentCount(data?.total ?? items.length);
setSelectedKeys([]);
setDeployments(
items.map((deployment) => ({ ...deployment, key: deployment.id })),
);
};
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
const newPageData = extractItems(data);
setActivePage(data.page || page);
setDeploymentCount(data.total || newPageData.length);
setDeploymentFormat(newPageData);
if (data.status_counts) {
const sumAll = Object.values(data.status_counts).reduce(
(acc, v) => acc + v,
0,
);
setStatusCounts({ ...data.status_counts, all: sumAll });
}
} else {
showError(message);
setDeployments([]);
}
} catch (error) {
console.error(error);
showError(t('获取部署列表失败'));
setDeployments([]);
const fetchDeployments = async ({ page, size, keyword, status }) => {
const seq = ++requestSeq.current;
const isSearchMode = Boolean(keyword) || Boolean(status);
if (isSearchMode) {
setSearching(true);
} else {
setLoading(true);
}
setLoading(false);
};
// Search deployments
const searchDeployments = async (searchTerms) => {
setSearching(true);
try {
const { searchKeyword, searchStatus } = searchTerms;
const params = new URLSearchParams({
p: '1',
page_size: pageSize.toString(),
});
let url;
if (isSearchMode) {
const params = new URLSearchParams({
p: String(page),
page_size: String(size),
});
if (searchKeyword?.trim()) {
params.append('keyword', searchKeyword.trim());
}
if (searchStatus && searchStatus !== 'all') {
params.append('status', searchStatus);
if (keyword) params.append('keyword', keyword);
if (status) params.append('status', status);
url = `/api/deployments/search?${params.toString()}`;
} else {
url = `/api/deployments/?p=${page}&page_size=${size}`;
}
const res = await API.get(`/api/deployments/search?${params}`);
const res = await API.get(url);
if (seq !== requestSeq.current) return;
const { success, message, data } = res.data;
if (success) {
const items = extractItems(data);
setActivePage(1);
setDeploymentCount(data.total || items.length);
setDeploymentFormat(items);
} else {
if (!success) {
showError(message);
setDeployments([]);
setDeploymentCount(0);
return;
}
applyDeploymentsData({ data, page });
} catch (error) {
console.error('Search error:', error);
showError(t('搜索失败'));
if (seq !== requestSeq.current) return;
console.error(error);
showError(isSearchMode ? t('搜索失败') : t('获取部署列表失败'));
setDeployments([]);
setDeploymentCount(0);
} finally {
if (seq !== requestSeq.current) return;
setLoading(false);
setSearching(false);
}
setSearching(false);
};
// Refresh data
const refresh = async (page = activePage) => {
await loadDeployments(page, pageSize);
await fetchDeployments({
page,
size: pageSize,
keyword: query.keyword,
status: query.status,
});
};
// Handle page change
const handlePageChange = (page) => {
setActivePage(page);
if (!searching) {
loadDeployments(page, pageSize);
}
fetchDeployments({
page,
size: pageSize,
keyword: query.keyword,
status: query.status,
});
};
// Handle page size change
const handlePageSizeChange = (size) => {
setPageSize(size);
setActivePage(1);
if (!searching) {
loadDeployments(1, size);
}
fetchDeployments({
page: 1,
size,
keyword: query.keyword,
status: query.status,
});
};
const loadDeployments = async (page = 1, size = pageSize) => {
await fetchDeployments({
page,
size,
keyword: query.keyword,
status: query.status,
});
};
// Handle tab change
const handleTabChange = (statusKey) => {
setActiveStatusKey(statusKey);
// Search deployments (also supports pagination)
const searchDeployments = async (searchTerms) => {
const nextQuery = normalizeQuery(searchTerms);
setQuery(nextQuery);
setActivePage(1);
loadDeployments(1, pageSize, statusKey);
await fetchDeployments({
page: 1,
size: pageSize,
keyword: nextQuery.keyword,
status: nextQuery.status,
});
};
// Deployment operations
......@@ -323,7 +332,9 @@ export const useDeploymentsData = () => {
}
try {
const containersResp = await API.get(`/api/deployments/${deployment.id}/containers`);
const containersResp = await API.get(
`/api/deployments/${deployment.id}/containers`,
);
if (!containersResp.data?.success) {
showError(containersResp.data?.message || t('获取容器信息失败'));
return;
......@@ -344,15 +355,20 @@ export const useDeploymentsData = () => {
return;
}
const baseName = deployment.container_name || deployment.deployment_name || deployment.name || deployment.id;
const baseName =
deployment.container_name ||
deployment.deployment_name ||
deployment.name ||
deployment.id;
const safeName = String(baseName || 'ionet').slice(0, 60);
const channelName = `[IO.NET] ${safeName}`;
let randomKey;
try {
randomKey = (typeof crypto !== 'undefined' && crypto.randomUUID)
? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
: null;
randomKey =
typeof crypto !== 'undefined' && crypto.randomUUID
? `ionet-${crypto.randomUUID().replace(/-/g, '')}`
: null;
} catch (err) {
randomKey = null;
}
......@@ -396,7 +412,9 @@ export const useDeploymentsData = () => {
const updateDeploymentName = async (deploymentId, newName) => {
try {
const res = await API.put(`/api/deployments/${deploymentId}/name`, { name: newName });
const res = await API.put(`/api/deployments/${deploymentId}/name`, {
name: newName,
});
if (res.data.success) {
showSuccess(t('部署名称更新成功'));
await refresh();
......@@ -415,9 +433,9 @@ export const useDeploymentsData = () => {
// Batch operations
const batchDeleteDeployments = async () => {
if (selectedKeys.length === 0) return;
try {
const ids = selectedKeys.map(deployment => deployment.id);
const ids = selectedKeys.map((deployment) => deployment.id);
const res = await API.post('/api/deployments/batch_delete', { ids });
if (res.data.success) {
showSuccess(t('批量删除成功'));
......@@ -452,8 +470,6 @@ export const useDeploymentsData = () => {
activePage,
pageSize,
deploymentCount,
statusCounts,
activeStatusKey,
compactMode,
setCompactMode,
......@@ -488,7 +504,6 @@ export const useDeploymentsData = () => {
refresh,
handlePageChange,
handlePageSizeChange,
handleTabChange,
handleRow,
// Deployment operations
......
......@@ -25,9 +25,9 @@ export const useEnhancedDeploymentActions = (t) => {
// Set loading state for specific operation
const setOperationLoading = (operation, deploymentId, isLoading) => {
setLoading(prev => ({
setLoading((prev) => ({
...prev,
[`${operation}_${deploymentId}`]: isLoading
[`${operation}_${deploymentId}`]: isLoading,
}));
};
......@@ -38,20 +38,26 @@ export const useEnhancedDeploymentActions = (t) => {
// Extend deployment duration
const extendDeployment = async (deploymentId, durationHours) => {
const operationKey = `extend_${deploymentId}`;
try {
setOperationLoading('extend', deploymentId, true);
const response = await API.post(`/api/deployments/${deploymentId}/extend`, {
duration_hours: durationHours
});
const response = await API.post(
`/api/deployments/${deploymentId}/extend`,
{
duration_hours: durationHours,
},
);
if (response.data.success) {
showSuccess(t('容器时长延长成功'));
return response.data.data;
}
} catch (error) {
showError(t('延长时长失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('延长时长失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('extend', deploymentId, false);
......@@ -62,14 +68,18 @@ export const useEnhancedDeploymentActions = (t) => {
const getDeploymentDetails = async (deploymentId) => {
try {
setOperationLoading('details', deploymentId, true);
const response = await API.get(`/api/deployments/${deploymentId}`);
if (response.data.success) {
return response.data.data;
}
} catch (error) {
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取详情失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('details', deploymentId, false);
......@@ -80,24 +90,31 @@ export const useEnhancedDeploymentActions = (t) => {
const getDeploymentLogs = async (deploymentId, options = {}) => {
try {
setOperationLoading('logs', deploymentId, true);
const params = new URLSearchParams();
if (options.containerId) params.append('container_id', options.containerId);
if (options.containerId)
params.append('container_id', options.containerId);
if (options.level) params.append('level', options.level);
if (options.limit) params.append('limit', options.limit.toString());
if (options.cursor) params.append('cursor', options.cursor);
if (options.follow) params.append('follow', 'true');
if (options.startTime) params.append('start_time', options.startTime);
if (options.endTime) params.append('end_time', options.endTime);
const response = await API.get(`/api/deployments/${deploymentId}/logs?${params}`);
const response = await API.get(
`/api/deployments/${deploymentId}/logs?${params}`,
);
if (response.data.success) {
return response.data.data;
}
} catch (error) {
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取日志失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('logs', deploymentId, false);
......@@ -108,15 +125,22 @@ export const useEnhancedDeploymentActions = (t) => {
const updateDeploymentConfig = async (deploymentId, config) => {
try {
setOperationLoading('config', deploymentId, true);
const response = await API.put(`/api/deployments/${deploymentId}`, config);
const response = await API.put(
`/api/deployments/${deploymentId}`,
config,
);
if (response.data.success) {
showSuccess(t('容器配置更新成功'));
return response.data.data;
}
} catch (error) {
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('更新配置失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('config', deploymentId, false);
......@@ -127,15 +151,19 @@ export const useEnhancedDeploymentActions = (t) => {
const deleteDeployment = async (deploymentId) => {
try {
setOperationLoading('delete', deploymentId, true);
const response = await API.delete(`/api/deployments/${deploymentId}`);
if (response.data.success) {
showSuccess(t('容器销毁请求已提交'));
return response.data.data;
}
} catch (error) {
showError(t('销毁容器失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('销毁容器失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('delete', deploymentId, false);
......@@ -146,17 +174,21 @@ export const useEnhancedDeploymentActions = (t) => {
const updateDeploymentName = async (deploymentId, newName) => {
try {
setOperationLoading('rename', deploymentId, true);
const response = await API.put(`/api/deployments/${deploymentId}/name`, {
name: newName
name: newName,
});
if (response.data.success) {
showSuccess(t('容器名称更新成功'));
return response.data.data;
}
} catch (error) {
showError(t('更新名称失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('更新名称失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('rename', deploymentId, false);
......@@ -167,21 +199,23 @@ export const useEnhancedDeploymentActions = (t) => {
const batchDelete = async (deploymentIds) => {
try {
setOperationLoading('batch_delete', 'all', true);
const results = await Promise.allSettled(
deploymentIds.map(id => deleteDeployment(id))
deploymentIds.map((id) => deleteDeployment(id)),
);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
const successful = results.filter((r) => r.status === 'fulfilled').length;
const failed = results.filter((r) => r.status === 'rejected').length;
if (successful > 0) {
showSuccess(t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
success: successful,
failed: failed
}));
showSuccess(
t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
success: successful,
failed: failed,
}),
);
}
return { successful, failed };
} catch (error) {
showError(t('批量操作失败') + ': ' + error.message);
......@@ -195,17 +229,20 @@ export const useEnhancedDeploymentActions = (t) => {
const exportLogs = async (deploymentId, options = {}) => {
try {
setOperationLoading('export_logs', deploymentId, true);
const logs = await getDeploymentLogs(deploymentId, {
...options,
limit: 10000 // Get more logs for export
limit: 10000, // Get more logs for export
});
if (logs && logs.logs) {
const logText = logs.logs.map(log =>
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
).join('\n');
const logText = logs.logs
.map(
(log) =>
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`,
)
.join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
......@@ -215,7 +252,7 @@ export const useEnhancedDeploymentActions = (t) => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showSuccess(t('日志导出成功'));
}
} catch (error) {
......@@ -236,14 +273,14 @@ export const useEnhancedDeploymentActions = (t) => {
updateDeploymentName,
batchDelete,
exportLogs,
// Loading states
isOperationLoading,
loading,
// Utility
setOperationLoading
setOperationLoading,
};
};
export default useEnhancedDeploymentActions;
\ No newline at end of file
export default useEnhancedDeploymentActions;
......@@ -18,13 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useCallback, useEffect, useState } from 'react';
import { API, toBoolean } from '../../helpers';
import { API } from '../../helpers';
export const useModelDeploymentSettings = () => {
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState({
'model_deployment.ionet.enabled': false,
'model_deployment.ionet.api_key': '',
});
const [connectionState, setConnectionState] = useState({
loading: false,
......@@ -35,24 +34,13 @@ export const useModelDeploymentSettings = () => {
const getSettings = async () => {
try {
setLoading(true);
const res = await API.get('/api/option/');
const res = await API.get('/api/deployments/settings');
const { success, data } = res.data;
if (success) {
const newSettings = {
'model_deployment.ionet.enabled': false,
'model_deployment.ionet.api_key': '',
};
data.forEach((item) => {
if (item.key.endsWith('enabled')) {
newSettings[item.key] = toBoolean(item.value);
} else if (newSettings.hasOwnProperty(item.key)) {
newSettings[item.key] = item.value || '';
}
setSettings({
'model_deployment.ionet.enabled': data?.enabled === true,
});
setSettings(newSettings);
}
} catch (error) {
console.error('Failed to get model deployment settings:', error);
......@@ -65,10 +53,7 @@ export const useModelDeploymentSettings = () => {
getSettings();
}, []);
const apiKey = settings['model_deployment.ionet.api_key'];
const isIoNetEnabled = settings['model_deployment.ionet.enabled'] &&
apiKey &&
apiKey.trim() !== '';
const isIoNetEnabled = settings['model_deployment.ionet.enabled'];
const buildConnectionError = (rawMessage, fallbackMessage = 'Connection failed') => {
const message = (rawMessage || fallbackMessage).trim();
......@@ -85,18 +70,12 @@ export const useModelDeploymentSettings = () => {
return { type: 'unknown', message };
};
const testConnection = useCallback(async (apiKey) => {
const key = (apiKey || '').trim();
if (key === '') {
setConnectionState({ loading: false, ok: null, error: null });
return;
}
const testConnection = useCallback(async () => {
setConnectionState({ loading: true, ok: null, error: null });
try {
const response = await API.post(
'/api/deployments/test-connection',
{ api_key: key },
'/api/deployments/settings/test-connection',
{},
{ skipErrorHandler: true },
);
......@@ -123,16 +102,15 @@ export const useModelDeploymentSettings = () => {
useEffect(() => {
if (!loading && isIoNetEnabled) {
testConnection(apiKey);
testConnection();
return;
}
setConnectionState({ loading: false, ok: null, error: null });
}, [loading, isIoNetEnabled, apiKey, testConnection]);
}, [loading, isIoNetEnabled, testConnection]);
return {
loading,
settings,
apiKey,
isIoNetEnabled,
refresh: getSettings,
connectionLoading: connectionState.loading,
......
......@@ -11,6 +11,8 @@
",时间:": ",time:",
",点击更新": ", click Update",
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
"(筛选后显示 {{count}} 条)_one": "(Showing {{count}} item after filtering)",
"(筛选后显示 {{count}} 条)_other": "(Showing {{count}} items after filtering)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Input {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Input {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Audio input {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
"(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Input {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}",
......@@ -21,6 +23,9 @@
"{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}",
"{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}",
"• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider",
"• 防盗链保护机制": "• Hotlink protection mechanisms",
"• 需要特定的请求头或认证": "• Specific headers or authentication are required",
"© {{currentYear}}": "© {{currentYear}}",
"| 基于": " | Based on ",
"$/1M tokens": "$/1M tokens",
......@@ -42,7 +47,10 @@
"AI模型测试环境": "AI model testing environment",
"AI模型配置": "AI model configuration",
"AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "AK/SK mode uses AccessKey and SecretAccessKey; API Key mode uses an API Key",
"API Key": "API Key",
"API Key 模式下不支持批量创建": "Batch creation not supported in API Key mode",
"API Key 验证失败": "API Key verification failed",
"API Key 验证成功!连接到 io.net 服务正常": "API Key verification successful! Connection to io.net service is normal",
"API 地址和相关配置": "API URL and related configuration",
"API 密钥": "API Key",
"API 文档": "API Documentation",
......@@ -94,15 +102,19 @@
"Gotify服务器地址": "Gotify server address",
"Gotify服务器地址必须以http://或https://开头": "Gotify server address must start with http:// or https://",
"Gotify通知": "Gotify notification",
"GPU/容器": "GPU/Container",
"GPU数量": "Number of GPUs",
"Homepage URL 填": "Fill in the Homepage URL",
"ID": "ID",
"IP": "IP",
"IP白名单": "IP Whitelist",
"IP白名单(支持CIDR表达式)": "IP whitelist (supports CIDR expressions)",
"IP限制": "IP restrictions",
"IP黑名单": "IP blacklist",
"JSON": "JSON",
"JSON 模式支持手动输入或上传服务账号 JSON": "JSON mode supports manual input or upload service account JSON",
"JSON格式密钥,请确保格式正确": "JSON format key, please ensure the format is correct",
"JSON格式错误": "JSON format error",
"JSON编辑": "JSON Editor",
"JSON解析错误:": "JSON parsing error:",
"Linux DO Client ID": "Linux DO Client ID",
......@@ -115,6 +127,8 @@
"New API项目仓库地址:": "New API project repository address: ",
"OIDC": "OIDC",
"OIDC ID": "OIDC ID",
"Ollama 模型管理": "Ollama Model Management",
"Ollama 版本信息": "Ollama Version Info",
"Passkey": "Passkey",
"Passkey 已解绑": "Passkey removed",
"Passkey 已重置": "Passkey has been reset",
......@@ -134,6 +148,8 @@
"SMTP 端口": "SMTP Port",
"SMTP 访问凭证": "SMTP Access Credential",
"SMTP 账户": "SMTP Account",
"SSE 事件": "SSE Events",
"SSE数据流": "SSE Stream",
"SSRF防护开关详细说明": "Master switch controls whether SSRF protection is enabled. When disabled, all SSRF checks are bypassed, allowing access to any URL. ⚠️ Only disable this feature in completely trusted environments.",
"SSRF防护设置": "SSRF Protection Settings",
"SSRF防护详细说明": "SSRF protection prevents malicious users from using your server to access internal network resources. Configure whitelists for trusted domains/IPs and restrict allowed ports. Applies to file downloads, webhooks, and notifications.",
......@@ -183,6 +199,7 @@
"下一个表单块": "Next form block",
"下一步": "Next",
"下午好": "Good afternoon",
"下载日志": "Download Logs",
"不再提醒": "Do not remind again",
"不同用户分组的价格信息": "Price information for different user groups",
"不填则为模型列表第一个": "First model in list if empty",
......@@ -201,14 +218,17 @@
"两步验证已禁用": "Two-factor authentication has been disabled",
"两步验证设置": "Two-factor authentication settings",
"个": "individual",
"个GPU": " GPUs",
"个人中心": "Personal center",
"个人中心区域": "Personal Center Area",
"个人信息设置": "Personal information settings",
"个人设置": "Personal Settings",
"个实例": " instances",
"个性化设置": "Personalization Settings",
"个性化设置左侧边栏的显示内容": "Personalize the display content of the left sidebar",
"个未配置模型": "models not configured",
"个模型": "models",
"个部署吗?此操作不可逆。": " deployments? This operation cannot be undone.",
"中午好": "Good afternoon",
"为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Is a JSON object, e.g.: {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}",
"为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "Is a JSON array, e.g.: [10, 20, 50, 100, 200, 500]",
......@@ -269,9 +289,16 @@
"以及": "and",
"仪表盘设置": "Dashboard Settings",
"价格": "Pricing",
"价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "Price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}}",
"价格:${{price}} * {{ratioType}}:{{ratio}}": "Price: ${{price}} * {{ratioType}}: {{ratio}}",
"价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later",
"价格计算中...": "Calculating price...",
"价格计算失败": "Price calculation failed",
"价格计算失败: ": "Price calculation failed: ",
"价格设置": "Price Settings",
"价格设置方式": "Pricing configuration method",
"价格重新计算中...": "Recalculating price...",
"价格预估": "Price Estimate",
"任务 ID": "Task ID",
"任务ID": "Task ID",
"任务日志": "Task Logs",
......@@ -305,7 +332,11 @@
"例如 €, £, Rp, ₩, ₹...": "For example, €, £, Rp, ₩, ₹...",
"例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro",
"例如:": "For example:",
"例如: /bin/bash -c \"python app.py\"": "e.g.: /bin/bash -c \"python app.py\"",
"例如: nginx:latest": "e.g.: nginx:latest",
"例如: socks5://user:pass@host:port": "e.g.: socks5://user:pass@host:port",
"例如:-c": "e.g.: -c",
"例如:/bin/bash": "e.g.: /bin/bash",
"例如:0001": "e.g.: 0001",
"例如:1000": "e.g.: 1000",
"例如:100000": "e.g.: 100000",
......@@ -315,6 +346,7 @@
"例如:7,就是7元/美金": "e.g.: 7, means 7 yuan per USD",
"例如:example.com": "e.g.: example.com",
"例如:https://yourdomain.com": "e.g.: https://yourdomain.com",
"例如:nginx:latest": "e.g.: nginx:latest",
"例如:preview": "e.g.: preview",
"例如:prod_6I8rBerHpPxyoiU9WK4kot": "e.g.: prod_6I8rBerHpPxyoiU9WK4kot",
"例如:基础套餐": "e.g.: Basic Package",
......@@ -364,12 +396,14 @@
"修改子渠道权重": "Modify sub-channel weight",
"修改密码": "Change password",
"修改绑定": "Modify binding",
"修改部署名称": "Change Deployment Name",
"倍率": "Ratio",
"倍率信息": "Ratio information",
"倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.",
"倍率模式": "Ratio Mode",
"倍率类型": "Ratio type",
"停止测试": "Stop Testing",
"停用": "Deactivate",
"允许 AccountFilter 参数": "Allow AccountFilter parameter",
"允许 HTTP 协议图片请求(适用于自部署代理)": "Allow HTTP protocol image requests (for self-deployed proxies)",
"允许 safety_identifier 透传": "Allow safety_identifier Pass-through",
......@@ -426,9 +460,14 @@
"全部": "All",
"全部供应商": "All vendors",
"全部分组": "All groups",
"全部地区总可用资源": "Total Available Resources in All Regions",
"全部容器": "All Containers",
"全部展开": "Expand All",
"全部收起": "Collapse All",
"全部标签": "All tags",
"全部模型": "All Models",
"全部状态": "All status",
"全部硬件总可用资源": "Total Available Hardware Resources",
"全部端点": "All endpoints",
"全部类型": "All types",
"公告": "Announcement",
......@@ -442,6 +481,8 @@
"共 {{count}} 个模型": "{{count}} models",
"共 {{count}} 个模型_one": "{{count}} model",
"共 {{count}} 个模型_other": "{{count}} models",
"共 {{count}} 条日志_one": "{{count}} log entry",
"共 {{count}} 条日志_other": "{{count}} log entries",
"共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "{{total}} items total, showing {{start}}-{{end}} items",
"关": "Off",
"关于": "About",
......@@ -462,10 +503,17 @@
"内容": "Content",
"内容较大,已启用性能优化模式": "Content is large, performance optimization mode enabled",
"内容较大,部分功能可能受限": "Content is large, some features may be limited",
"内置 Ollama 镜像": "Built-in Ollama Image",
"再次输入部署名称": "Enter Deployment Name Again",
"最低": "lowest",
"最低充值美元数量": "Minimum recharge dollar amount",
"最后使用时间": "Last used time",
"最后更新": "Last Updated",
"最后请求": "Last request",
"最大GPU数量": "Max Number of GPUs",
"最大可用": "Max Available",
"最近事件": "Recent Events",
"准备中...": "Preparing...",
"准备完成初始化": "Ready to complete initialization",
"分类名称": "Category Name",
"分组": "Group",
......@@ -490,9 +538,11 @@
"划转额度": "Transfer amount",
"列出的模型将不会自动添加或移除-thinking/-nothinking 后缀": "Models in this list will not automatically add or remove the -thinking/-nothinking suffix.",
"列设置": "Column settings",
"创建": "Create",
"创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "Create token with auto group by default, initial token will also be set to auto (otherwise leave blank for user default group)",
"创建失败": "Creation failed",
"创建成功": "Creation successful",
"创建或选择密钥时,将 Project 设置为 io.cloud": "When creating or selecting a key, set Project to io.cloud",
"创建新用户账户": "Create new user account",
"创建新的令牌": "Create New Token",
"创建新的兑换码": "Create a new redemption code",
......@@ -504,6 +554,7 @@
"初始化失败,请重试": "Initialization failed, please retry",
"初始化系统": "Initialize system",
"删除": "Delete",
"删除后无法恢复,确定要删除模型 \"{{name}}\" 吗?": "Cannot be recovered after deletion, are you sure you want to delete model \"{{name}}\"?",
"删除失败": "Delete failed",
"删除密钥失败": "Failed to delete key",
"删除成功": "Delete successful",
......@@ -515,23 +566,40 @@
"删除自动禁用密钥": "Delete auto disabled keys",
"删除账户": "Delete Account",
"删除账户确认": "Delete Account Confirmation",
"删除部署失败": "Failed to delete deployment",
"刷新": "Refresh",
"刷新失败": "Refresh failed",
"刷新容器信息": "Refresh Container Info",
"刷新日志": "Refresh Logs",
"前往 io.net API Keys": "Go to io.net API Keys",
"前往设置": "Go to Settings",
"前往设置页面": "Go to Settings Page",
"前缀": "Prefix",
"副本数量": "Number of Replicas",
"剩余": "Remaining",
"剩余备用码:": "Remaining backup codes: ",
"剩余时间": "Remaining Time",
"剩余额度": "Remaining quota",
"剩余额度/总额度": "Remaining/Total",
"剩余额度$": "Remaining quota $",
"功能特性": "Features",
"加入渠道": "Join Channel",
"加入预填组": "Join Pre-filled Group",
"加密存储": "Encrypted Storage",
"加载中...": "Loading...",
"加载供应商信息失败": "Failed to load supplier information",
"加载关于内容失败...": "Failed to load about content...",
"加载分组失败": "Failed to load groups",
"加载失败": "Load failed",
"加载容器信息中...": "Loading container info...",
"加载容器详情中...": "Loading container details...",
"加载日志中...": "Loading logs...",
"加载模型信息失败": "Failed to load model information",
"加载模型列表失败": "Failed to load model list",
"加载模型失败": "Failed to load models",
"加载用户协议内容失败...": "Failed to load user agreement content...",
"加载设置中...": "Loading settings...",
"加载详情中...": "Loading details...",
"加载账单失败": "Failed to load bills",
"加载隐私政策内容失败...": "Failed to load privacy policy content...",
"包含": "Contains",
......@@ -539,6 +607,7 @@
"包括失败请求的次数,0代表不限制": "Including failed request times, 0 means no limit",
"匹配类型": "Matching type",
"区域": "Region",
"单GPU小时费率": "Per GPU Hour Rate",
"历史消耗": "Consumption",
"原价": "Original price",
"原因:": "Reason: ",
......@@ -549,14 +618,16 @@
"参数值": "Parameter value",
"参数覆盖": "Parameters override",
"参照生视频": "Reference video generation",
"视频Remix": "Video remix",
"友情链接": "Friendly links",
"发布日期": "Publish Date",
"发布时间": "Publish Time",
"取消": "Cancel",
"取消全选": "Deselect all",
"取消选择": "Deselect",
"变换": "Transform",
"变焦": "zoom",
"变量值": "Variable Value",
"变量名": "Variable Name",
"只包括请求成功的次数": "Only include successful request times",
"只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Only HTTPS is supported, the system will send notifications via POST, please ensure that the address can receive POST requests",
"只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed",
......@@ -564,6 +635,7 @@
"可在设置页面设置关于内容,支持 HTML & Markdown": "The About content can be set on the settings page, supporting HTML & Markdown",
"可用令牌分组": "Available token groups",
"可用分组": "Available groups",
"可用数量": "Available Quantity",
"可用模型": "Available models",
"可用端点类型": "Supported endpoint types",
"可用邀请额度": "Available invitation quota",
......@@ -571,13 +643,17 @@
"可视化倍率设置": "Visual model ratio settings",
"可视化编辑": "Visual editing",
"可选,公告的补充说明": "Optional, additional information for the notice",
"可选,用于复现结果": "Optional, for reproducibility",
"可选值": "Optional value",
"同时重置消息": "Reset messages simultaneously",
"同步": "Sync",
"同步到渠道": "Sync to Channel",
"同步向导": "Sync Wizard",
"同步失败": "Synchronization failed",
"同步成功": "Synchronization successful",
"同步接口": "Synchronization interface",
"同步渠道失败": "Failed to sync channel",
"同步渠道失败:缺少部署信息": "Failed to sync channel: Missing deployment info",
"名称": "Name",
"名称+密钥": "Name + key",
"名称不能为空": "Name cannot be empty",
......@@ -585,8 +661,17 @@
"后端请求失败": "Backend request failed",
"后缀": "Suffix",
"否": "No",
"启动": "Start",
"启动参数 (Args)": "Startup Args",
"启动命令": "Startup Command",
"启动命令 (Entrypoint)": "Entrypoint",
"启动时间": "Startup Time",
"启动部署失败": "Failed to start deployment",
"启动配置": "Startup Configuration",
"启用": "Enable",
"启用 io.net 部署": "Enable io.net Deployment",
"启用 io.net 部署开关": "Enable io.net Deployment Switch",
"启用 io.net 部署时必须填写 API Key": "API Key is required when enabling io.net deployment",
"启用 Prompt 检查": "Enable Prompt check",
"启用2FA失败": "Failed to enable Two-Factor Authentication",
"启用Claude思考适配(-thinking后缀)": "Enable Claude thinking adaptation (-thinking suffix)",
......@@ -596,11 +681,14 @@
"启用SMTP SSL": "Enable SMTP SSL",
"启用SSRF防护(推荐开启以保护服务器安全)": "Enable SSRF Protection (Recommended for server security)",
"启用全部": "Enable all",
"启用后可接入 io.net GPU 资源": "After enabling, you can access io.net GPU resources",
"启用后可添加图片URL进行多模态对话": "After enabling, you can add image URLs for multimodal conversations",
"启用后将使用 Creem Test Mode": "Use Creem Test Mode after enabling",
"启用密钥失败": "Failed to enable key",
"启用屏蔽词过滤功能": "Enable sensitive word filtering function",
"启用所有密钥失败": "Failed to enable all keys",
"启用数据看板(实验性)": "Enable data dashboard (experimental)",
"启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。": "After enabling this mode, your custom request body will be used to send API requests, and parameter settings in the model configuration panel will be ignored.",
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
"启用绘图功能": "Enable drawing function",
"启用请求体透传功能": "Enable request body pass-through functionality",
......@@ -622,6 +710,9 @@
"图标": "Icon",
"图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "The icon uses the @lobehub/icons library, such as: OpenAI, Claude.Color, supports chain parameters: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, query all available icons please ",
"图混合": "Blend",
"图片功能在自定义请求体模式下不可用": "Image functionality is not available in custom request body mode",
"图片地址": "Image URL",
"图片已添加": "Image Added",
"图片生成调用:{{symbol}}{{price}} / 1次": "Image generation call: {{symbol}}{{price}} / 1 time",
"图片输入: {{imageRatio}}": "Image input: {{imageRatio}}",
"图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Image input price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Image ratio: {{imageRatio}})",
......@@ -632,6 +723,7 @@
"在Gotify服务器创建应用后获得的令牌,用于发送通知": "Token obtained after creating an application on the Gotify server, used to send notifications",
"在Gotify服务器的应用管理中创建新应用": "Create a new application in the Gotify server's application management",
"在找兑换码?": "Looking for a redemption code? ",
"在新标签页中打开": "Open in new tab",
"在此输入 Logo 图片地址": "Enter the Logo image URL here",
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
"在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, support Markdown",
......@@ -652,6 +744,7 @@
"填写带https的域名,逗号分隔": "Fill in domains with https, separated by commas",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement during registration",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy during registration",
"处理中": "Processing",
"备份支持": "Backup support",
"备份状态": "Backup state",
"备注": "Remark",
......@@ -665,6 +758,7 @@
"复制名称": "Copy name",
"复制失败": "Copy failed",
"复制失败,请手动复制": "Copy failed, please copy manually",
"复制失败,请手动选择文本复制": "Copy failed, please manually select and copy the text",
"复制已选": "Copy selected",
"复制应用的令牌(Token)并填写到上方的应用令牌字段": "Copy the application token and fill it in the application token field above",
"复制成功": "Copy successful",
......@@ -672,8 +766,13 @@
"复制所有模型": "Copy all models",
"复制所选令牌": "Copy selected token",
"复制所选兑换码到剪贴板": "Copy selected redemption codes to clipboard",
"复制日志": "Copy Logs",
"复制渠道的所有信息": "Copy all information for a channel",
"复制版本号": "Copy Version",
"复制生成的密钥并粘贴到此处": "Copy the generated key and paste it here",
"复制链接": "Copy link",
"外接设备": "External device",
"多个命令用空格分隔": "Multiple commands separated by spaces",
"多密钥渠道操作项目组": "Multi-key channel operation project group",
"多密钥管理": "Multi-key management",
"多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient",
......@@ -689,9 +788,12 @@
"如:香港线路": "e.g. Hong Kong line",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "If the user request contains a system prompt, this setting will be appended to the user's system prompt",
"如果镜像为私有,请填写密码或Token": "If the image is private, please fill in the password or token",
"如果镜像为私有,请填写用户名": "If the image is private, please fill in the username",
"始终使用浅色主题": "Always use light theme",
"始终使用深色主题": "Always use dark theme",
"字段透传控制": "Field Pass-through Control",
"存在惩罚,鼓励讨论新话题": "Presence penalty, encourages discussing new topics",
"存在重复的键名:": "Duplicate key names exist:",
"安全提醒": "Security reminder",
"安全设置": "Security Settings",
......@@ -700,7 +802,9 @@
"安装指南": "Installation Guide",
"完成": "Complete",
"完成初始化": "Complete initialization",
"完成硬件类型、部署位置、副本数量等配置后,将自动计算价格": "Price will be automatically calculated after completing hardware type, deployment location, number of replicas and other configurations",
"完成设置并启用两步验证": "Complete setup and enable two-factor authentication",
"完成进度": "Completion Progress",
"完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}",
"官方": "Official",
"官方文档": "Official documentation",
......@@ -713,6 +817,26 @@
"实付金额:": "Actual payment amount: ",
"实际模型": "Actual model",
"实际请求体": "Actual request body",
"容器": "Container",
"容器ID": "Container ID",
"容器创建失败: ": "Container creation failed: ",
"容器创建成功": "Container created successfully",
"容器名称": "Container Name",
"容器名称更新成功": "Container name updated successfully",
"容器启动后执行的命令": "Command to execute after container starts",
"容器启动配置": "Container Startup Configuration",
"容器实例": "Container Instance",
"容器对外暴露的端口": "Container exposed port",
"容器对外服务的端口号,可选": "Port number for external service, optional",
"容器总数": "Total Containers",
"容器数量": "Number of Containers",
"容器日志": "Container Logs",
"容器时长延长成功": "Container duration extended successfully",
"容器访问地址无效": "Invalid container access address",
"容器详情": "Container Details",
"容器配置": "Container Configuration",
"容器配置更新成功": "Container configuration updated successfully",
"容器销毁请求已提交": "Container deletion request submitted",
"密码": "Password",
"密码修改成功!": "Password changed successfully!",
"密码已复制到剪贴板:": "Password has been copied to clipboard: ",
......@@ -734,6 +858,7 @@
"密钥更新模式": "Key update mode",
"密钥格式": "Key format",
"密钥格式无效,请输入有效的 JSON 格式密钥": "Invalid key format, please enter a valid JSON format key",
"密钥环境变量": "Secret Environment Variables",
"密钥聚合模式": "Key aggregation mode",
"密钥获取成功": "Key acquisition successful",
"密钥输入方式": "Key input method",
......@@ -747,6 +872,7 @@
"导入配置": "Import configuration",
"导入配置失败: ": "Failed to import configuration: ",
"导出": "Export",
"导出日志失败": "Failed to export logs",
"导出配置": "Export configuration",
"导出配置失败: ": "Failed to export configuration: ",
"将 reasoning_content 转换为 <think> 标签拼接到内容中": "Convert reasoning_content to <think> tags and append to content",
......@@ -757,6 +883,7 @@
"将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "This will clear all saved configurations and restore default settings, this operation cannot be undone. Continue?",
"将清除选定时间之前的所有日志": "This will clear all logs before the selected time",
"小时": "Hour",
"小时费率": "Hourly Rate",
"尚未使用": "Not used yet",
"局部重绘-提交": "Vary Region",
"屏蔽词列表": "Sensitive word list",
......@@ -769,6 +896,7 @@
"已为 {{count}} 个模型设置{{type}}_other": "Set {{type}} for {{count}} models",
"已为 ${count} 个渠道设置标签!": "Set tags for ${count} channels!",
"已修复 ${success} 个通道,失败 ${fails} 个通道。": "Fixed ${success} channels, failed ${fails} channels.",
"已停止": "Stopped",
"已停止批量测试": "Stopped batch testing",
"已关闭后续提醒": "Subsequent notifications turned off",
"已切换为Assistant角色": "Switched to Assistant role",
......@@ -785,43 +913,59 @@
"已删除消息及其回复": "Deleted message and its replies",
"已发送到 Fluent": "Sent to Fluent",
"已取消 Passkey 注册": "Passkey registration cancelled",
"已同步到渠道": "Synced to Channel",
"已启用": "Enabled",
"已启用 Passkey,无需密码即可登录": "Passkey enabled, login without password",
"已启用所有密钥": "All keys have been enabled",
"已在自定义模式中忽略": "Ignored in custom mode",
"已备份": "Backed up",
"已复制": "Copied",
"已复制 ${count} 个模型": "Copied ${count} models",
"已复制 ID 到剪贴板": "ID copied to clipboard",
"已复制:": "Copied:",
"已复制:{{name}}": "Copied: {{name}}",
"已复制全部数据": "All data copied",
"已复制到剪切板": "Copied to clipboard",
"已复制到剪贴板": "Copied to clipboard",
"已复制到剪贴板!": "Copied to clipboard!",
"已复制模型名称": "Model name copied",
"已复制版本号": "Version copied",
"已复制自动生成的 API Key": "Auto-generated API Key copied",
"已完成": "Completed",
"已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "Global request pass-through is enabled. Built-in NewAPI features such as parameter overrides, model redirection, and channel adaptation will be disabled. This is not a best practice. If this causes issues, please do not submit an issue.",
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "Successfully started testing all enabled channels. Please refresh page to view results.",
"已提交": "Submitted",
"已支付金额": "Amount Paid",
"已新增 {{count}} 个模型:{{list}}_one": "Added {{count}} model: {{list}}",
"已新增 {{count}} 个模型:{{list}}_other": "Added {{count}} models: {{list}}",
"已更新完毕所有已启用通道余额!": "Updated quota for all enabled channels!",
"已有保存的配置": "There are saved configurations",
"已有模型": "Existing Models",
"已有的模型": "Existing models",
"已有账户?": "Already have an account?",
"已服务": "Served",
"已注销": "Logged out",
"已添加": "Added",
"已添加到白名单": "Added to whitelist",
"已清空测试结果": "Cleared test results",
"已用": "Used",
"已用/剩余": "Used/Remaining",
"已用额度": "Quota used",
"已禁用": "Disabled",
"已禁用所有密钥": "Disabled all keys",
"已绑定": "Bound",
"已绑定渠道": "Bound channels",
"已结束": "Ended",
"已耗尽": "Exhausted",
"已解锁豆包自定义 API 地址编辑": "Custom Doubao API address editing unlocked",
"已过期": "Expired",
"已运行时间": "Uptime",
"已选择 {{count}} 个模型_one": "Selected {{count}} model",
"已选择 {{count}} 个模型_other": "Selected {{count}} models",
"已选择 {{selected}} / {{total}}": "Selected {{selected}} / {{total}}",
"已选择 ${count} 个渠道": "Selected ${count} channels",
"已重置为默认配置": "Reset to default configuration",
"已销毁": "Destroyed",
"常见问答": "FAQ",
"常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
"平台": "platform",
......@@ -831,6 +975,15 @@
"应用同步": "Apply synchronization",
"应用更改": "Apply changes",
"应用覆盖": "Apply overwrite",
"延长后总时长": "Total Duration After Extension",
"延长容器时长": "Extend Container Duration",
"延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "Extending container duration will incur additional charges, please ensure you have sufficient account balance.",
"延长操作一旦确认无法撤销,费用将立即扣除。": "Once confirmed, the extension operation cannot be undone, and charges will be deducted immediately.",
"延长时长": "Extension Duration",
"延长时长(小时)": "Extension Duration (hours)",
"延长时长不能超过720小时(30天)": "Extension duration cannot exceed 720 hours (30 days)",
"延长时长失败": "Failed to extend duration",
"延长时长至少为1小时": "Extension duration must be at least 1 hour",
"建立连接时发生错误": "Error occurred while establishing connection",
"建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.",
"开": "On",
......@@ -839,38 +992,43 @@
"开启后,仅\"消费\"\"错误\"日志将记录您的客户端IP地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address",
"开启后,对免费模型(倍率为0,或者价格为0)的模型也会预消耗额度": "After enabling, free models (ratio 0 or price 0) will also pre-consume quota",
"开启后,将定期发送ping数据保持连接活跃": "After enabling, ping data will be sent periodically to keep the connection active",
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "After enabling, when the current group channel fails, it will try the next group's channel in order",
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "When enabled, all requests will be directly forwarded to the upstream without any processing (redirects and channel adaptation will also be disabled). Please enable with caution.",
"该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "Request pass-through is enabled for this channel. Built-in NewAPI features such as parameter overrides, model redirection, and channel adaptation will be disabled. This is not a best practice. If this causes issues, please do not submit an issue.",
"已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "Global request pass-through is enabled. Built-in NewAPI features such as parameter overrides, model redirection, and channel adaptation will be disabled. This is not a best practice. If this causes issues, please do not submit an issue.",
"该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。": "Request pass-through is enabled for this channel; built-in NewAPI features such as parameter overrides and model redirection will be disabled. This is not a best practice.",
"开启后不限制:必须设置模型倍率": "After enabling, no limit: must set model ratio",
"开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace",
"开启批量操作": "Enable batch selection",
"开始同步": "Start sync",
"开始批量测试 ${count} 个模型,已清空上次结果...": "Starting batch test of ${count} models, cleared previous results...",
"开始时间": "start time",
"张图片": " images",
"弱变换": "High Variation",
"强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Force format responses to OpenAI standard format (Only for OpenAI channel types)",
"强制格式化": "Force format",
"强制要求": "Mandatory requirement",
"强变换": "Low Variation",
"当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "When the upstream channel returns an error containing these keywords (not case-sensitive), automatically disable the channel",
"当前 API 密钥已过期,请在设置中更新。": "Current API key has expired, please update it in settings.",
"当前 Ollama 版本为 ${version}": "Current Ollama version is ${version}",
"当前余额": "Current balance",
"当前值": "Current value",
"当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)",
"当前剩余": "Currently Remaining",
"当前时间": "Current time",
"当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings.",
"当前查看的分组为:{{group}},倍率为:{{ratio}}": "Current group: {{group}}, ratio: {{ratio}}",
"当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "The current model list is the longest one among all channel model lists under this tag, not the union of all channels. Please note that this may cause some channel models to be lost.",
"当前版本": "Current version",
"当前状态": "Current Status",
"当前计费": "Current billing",
"当前设备不支持 Passkey": "Passkey is not supported on this device",
"当前设置类型: ": "Current setting type: ",
"当前跟随系统": "Currently following system",
"当前配置无法连接到 io.net。": "Unable to connect to io.net with current configuration.",
"当剩余额度低于此数值时,系统将通过选择的方式发送通知": "When the remaining quota is lower than this value, the system will send a notification through the selected method",
"当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Accept calls even if the model has no price settings, use only when you trust the website, which may incur high costs",
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded",
"待使用收益": "Proceeds to be used",
"待部署": "Pending Deployment",
"微信": "WeChat",
"微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link",
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan WeChat QR code to follow official account, enter \"verification code\" to get code (valid for 3 minutes)",
......@@ -879,18 +1037,21 @@
"必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "Must be a valid JSON string array, for example: [\"g1\",\"g2\"]",
"忘记密码?": "Forgot password?",
"快速开始": "Quick Start",
"快速选择": "Quick Select",
"思考中...": "Thinking...",
"思考内容转换": "Thinking content conversion",
"思考过程": "Thinking process",
"思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
"思考预算占比": "Thinking budget ratio",
"性能指标": "Performance Indicators",
"总 GPU 小时": "Total GPU Hours",
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Total price: text price {{textPrice}} + audio price {{audioPrice}} = {{symbol}}{{total}}",
"总密钥数": "Total key count",
"总收益": "total revenue",
"总计": "Total",
"总额度": "Total quota",
"您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display",
"您可以在上方拉取需要的模型": "You can pull the required models above",
"您无权访问此页面,请联系管理员": "You do not have permission to access this page. Please contact the administrator.",
"您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "You are using the MySQL database. MySQL is a reliable relational database management system, suitable for production environments.",
"您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "You are using the PostgreSQL database. PostgreSQL is a powerful open-source relational database system that provides excellent reliability and data integrity, suitable for production environments.",
......@@ -924,8 +1085,11 @@
"批量删除": "Batch Delete",
"批量删除令牌": "Batch delete token",
"批量删除失败": "Batch deletion failed",
"批量删除成功": "Batch deletion successful",
"批量删除模型": "Batch delete models",
"批量操作": "Batch Operations",
"批量操作失败": "Batch operation failed",
"批量操作完成: {{success}}个成功, {{failed}}个失败": "Batch operation completed: {{success}} succeeded, {{failed}} failed",
"批量测试${count}个模型": "Batch test ${count} models",
"批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "Batch testing completed! Success: ${success}, Fail: ${fail}, Total: ${total}",
"批量测试已停止": "Batch testing stopped",
......@@ -935,6 +1099,10 @@
"批量设置标签": "Batch set tag",
"批量设置模型参数": "Batch Set Model Parameters",
"折": "% off",
"拉取中...": "Pulling...",
"拉取新模型": "Pull New Model",
"拉取模型": "Pull Model",
"拉取进度": "Pull Progress",
"按K显示单位": "Display in K",
"按价格设置": "Set by price",
"按倍率类型筛选": "Filter by ratio type",
......@@ -949,8 +1117,10 @@
"排队中": "Queuing",
"接受未设置价格模型": "Accept models without price settings",
"接口凭证": "Interface credentials",
"接口密钥已过期": "API key has expired",
"控制台": "Console",
"控制台区域": "Console Area",
"控制输出的随机性和创造性": "Controls randomness and creativity of output",
"控制顶栏模块显示状态,全局生效": "Control header module display status, global effect",
"推荐:用户可以选择是否使用指纹等验证": "Recommended: Users can choose whether to use fingerprint verification",
"推荐使用(用户可选)": "Recommended (user optional)",
......@@ -961,12 +1131,6 @@
"提升": "Promote",
"提示": "Prompt",
"提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:",
"• 视频服务商的跨域限制": "• Cross-origin limitations from the video provider",
"• 需要特定的请求头或认证": "• Specific headers or authentication are required",
"• 防盗链保护机制": "• Hotlink protection mechanisms",
"在新标签页中打开": "Open in new tab",
"复制链接": "Copy link",
"提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Cache creation {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + Completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"提示:如需备份数据,只需复制上述目录即可": "Tip: To back up data, simply copy the directory above",
......@@ -975,7 +1139,9 @@
"提示缓存倍率": "Prompt cache ratio",
"搜索供应商": "Search vendor",
"搜索关键字": "Search keywords",
"搜索失败": "Search failed",
"搜索无结果": "No results found",
"搜索日志内容": "Search log content",
"搜索条件": "Search Conditions",
"搜索模型": "Search models",
"搜索模型...": "Search models...",
......@@ -983,6 +1149,7 @@
"搜索模型失败": "Search model failed",
"搜索渠道名称或地址": "Search channel name or address",
"搜索聊天应用名称": "Search chat app name",
"搜索部署名称": "Search deployment name",
"操作": "Actions",
"操作失败": "Operation failed",
"操作失败,请重试": "Operation failed, please retry",
......@@ -996,6 +1163,7 @@
"支付设置": "Payment Settings",
"支付请求失败": "Payment request failed",
"支付金额": "Payment Amount",
"支持 Ctrl+V 粘贴图片": "Supports Ctrl+V to paste images",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Supports 6-digit TOTP verification code or 8-digit backup code, can be configured or viewed in `Personal Settings - Security Settings - Two-Factor Authentication Settings`.",
"支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Supports CIDR format, e.g.: 8.8.8.8, 192.168.1.0/24",
"支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Supports HTTP and HTTPS, enter the complete URL of the Gotify server",
......@@ -1004,6 +1172,7 @@
"支持单个端口和端口范围,如:80, 443, 8000-8999": "Supports single ports and port ranges, e.g.: 80, 443, 8000-8999",
"支持变量:": "Supported variables:",
"支持备份": "Supported",
"支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间": "Supports pulling all models from the Ollama official model library, the pulling process may take a few minutes",
"支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address",
"支持的图像模型": "Supported image models",
"支持通配符格式,如:example.com, *.api.example.com": "Supports wildcard format, e.g.: example.com, *.api.example.com",
......@@ -1015,6 +1184,7 @@
"放大": "Upscalers",
"放大编辑": "Expand editor",
"敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend",
"数据传输中断": "Data transfer interrupted",
"数据存储位置:": "Data storage location:",
"数据库信息": "Database Information",
"数据库检查": "Database Check",
......@@ -1040,6 +1210,8 @@
"新密码": "New Password",
"新密码需要和原密码不一致!": "New password must be different from the old password!",
"新建": "Create",
"新建容器": "Create Container",
"新建容器部署": "Create Container Deployment",
"新建数量": "New quantity",
"新建组": "New group",
"新格式(支持条件判断与json自定义):": "New format (supports conditional judgment and JSON customization):",
......@@ -1052,13 +1224,23 @@
"新获取的模型": "New models",
"新额度:": "New quota: ",
"无": "None",
"无GPU": "No GPU",
"无冲突项": "No conflict items",
"无效的部署信息": "Invalid deployment information",
"无效的重置链接,请重新发起密码重置请求": "Invalid reset link, please initiate a new password reset request",
"无法发起 Passkey 注册": "Unable to initiate Passkey registration",
"无法复制到剪贴板,请手动复制": "Unable to copy to clipboard, please copy manually",
"无法添加图片": "Unable to add image",
"无法获取容器详情": "Unable to get container details",
"无法连接 io.net": "Unable to connect to io.net",
"无邀请人": "No Inviter",
"无限制": "Unlimited",
"无限额度": "Unlimited quota",
"日志导出成功": "Logs exported successfully",
"日志已下载": "Logs downloaded",
"日志已加载": "Logs loaded",
"日志已复制到剪贴板": "Logs copied to clipboard",
"日志流": "Log Stream",
"日志清理失败:": "Log cleanup failed:",
"日志类型": "Log type",
"日志设置": "Log settings",
......@@ -1068,6 +1250,7 @@
"旧的备用码已失效,请保存新的备用码": "Old backup codes have been invalidated, please save the new backup codes",
"早上好": "Good morning",
"时间": "Time",
"时间信息": "Time Information",
"时间粒度": "Time granularity",
"易支付商户ID": "Epay merchant ID",
"易支付商户密钥": "Epay merchant key",
......@@ -1088,25 +1271,37 @@
"显示设置": "Display settings",
"显示调试": "Show debug",
"晚上好": "Good evening",
"普通环境变量": "Regular Environment Variables",
"普通用户": "Normal User",
"智能体ID": "Agent ID",
"智能熔断": "Smart fallback",
"智谱": "Zhipu AI",
"暂无": "None",
"暂无API信息": "No API information",
"暂无SSE响应数据": "No SSE response data",
"暂无产品配置": "No product configuration",
"暂无保存的配置": "No saved configuration",
"暂无充值记录": "No recharge records",
"暂无公告": "No Notice",
"暂无匹配模型": "No matching model",
"暂无可复制的版本信息": "No version information to copy",
"暂无可用的支付方式,请联系管理员配置": "No payment methods available, please contact administrator for configuration",
"暂无响应数据": "No response data",
"暂无容器信息": "No container information",
"暂无容器详情": "No container details",
"暂无密钥数据": "No key data",
"暂无差异化倍率显示": "No differential ratio display",
"暂无常见问答": "No FAQ",
"暂无成功模型": "No successful models",
"暂无数据": "No data",
"暂无数据,点击下方按钮添加键值对": "No data, click the button below to add key-value pairs",
"暂无日志": "No logs",
"暂无日志可下载": "No logs available to download",
"暂无日志可复制": "No logs available to copy",
"暂无机密环境变量": "No secret environment variables",
"暂无模型": "No models",
"暂无模型描述": "No model description",
"暂无环境变量": "No environment variables",
"暂无监控数据": "No monitoring data",
"暂无系统公告": "No system notice",
"暂无缺失模型": "No missing models",
......@@ -1125,7 +1320,11 @@
"更新Worker设置": "Update Worker Settings",
"更新令牌信息": "Update Token Information",
"更新兑换码信息": "Update redemption code information",
"更新名称失败": "Failed to update name",
"更新失败": "Update failed",
"更新失败,请检查输入信息": "Update failed, please check the input information",
"更新容器配置": "Update Container Configuration",
"更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。": "Updating container configuration may cause the container to restart, please ensure you perform this operation at an appropriate time.",
"更新成功": "Update successful",
"更新所有已启用通道余额": "Update balance for all enabled channels",
"更新支付设置": "Update payment settings",
......@@ -1133,8 +1332,14 @@
"更新服务器地址": "Update Server Address",
"更新模型信息": "Update model information",
"更新渠道信息": "Update Channel Information",
"更新部署名称失败": "Failed to update deployment name",
"更新配置": "Update Configuration",
"更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。": "After updating the configuration, the container may need to restart to apply the new settings. Please ensure you understand the impact of these changes.",
"更新配置失败": "Failed to update configuration",
"更新预填组": "Update pre-filled group",
"有 Reasoning": "Has Reasoning",
"服务可用性": "Service Status",
"服务商": "Service Provider",
"服务器地址": "Server Address",
"服务显示名称": "Service Display Name",
"未发现新增模型": "No new models were added",
......@@ -1145,6 +1350,7 @@
"未备份": "Not backed up",
"未开始": "Not Started",
"未找到匹配的模型": "No matching model found",
"未找到可用的容器访问地址": "No available container access address found",
"未找到差异化倍率,无需同步": "No differential ratio found, no synchronization is required",
"未提交": "Not submitted",
"未检测到 Fluent 容器": "Fluent container not detected",
......@@ -1153,11 +1359,14 @@
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
"未知": "unknown",
"未知供应商": "Unknown",
"未知品牌": "Unknown Brand",
"未知模型": "Unknown model",
"未知渠道": "Unknown channel",
"未知状态": "Unknown status",
"未知类型": "Unknown type",
"未知身份": "Unknown Identity",
"未知部署": "Unknown Deployment",
"未知错误": "Unknown error",
"未绑定": "Not bound",
"未获取到授权码": "Authorization code not obtained",
"未设置": "Not set",
......@@ -1170,19 +1379,27 @@
"本设备:手机指纹/面容,外接:USB安全密钥": "Built-in: phone fingerprint/face, External: USB security key",
"本设备内置": "Built-in device",
"本项目根据": "This project is licensed under the ",
"机密环境变量": "Secret Environment Variables",
"机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "Secret environment variables will be stored encrypted, suitable for storing passwords, API keys and other sensitive information.",
"机密环境变量说明": "Secret Environment Variables Description",
"权重": "Weight",
"权限设置": "Permission Settings",
"条": "items",
"条 - 第": "to",
"条,共": "of",
"条日志已清理!": "logs have been cleared!",
"来源于 IO.NET 部署": "From IO.NET Deployment",
"来自模型重定向,尚未加入模型列表": "From model redirect, not yet added to the model list",
"某些配置更改可能需要几分钟才能生效。": "Some configuration changes may take a few minutes to take effect.",
"查看": "Check",
"查看关联部署": "View Associated Deployment",
"查看图片": "View pictures",
"查看密钥": "View key",
"查看当前可用的所有模型": "View all available models",
"查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "View all available AI model suppliers, including models from many well-known suppliers.",
"查看日志": "View Logs",
"查看渠道密钥": "View channel key",
"查看详情": "View Details",
"查询": "Query",
"标签": "Label",
"标签不能为空!": "Label cannot be empty!",
......@@ -1193,8 +1410,12 @@
"标签聚合": "Tag aggregation",
"标签聚合模式": "Enable tag mode",
"标识颜色": "Identifier color",
"核采样,控制词汇选择的多样性": "Nucleus sampling, controls vocabulary selection diversity",
"根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Find model metadata based on model name and matching rules, priority: exact > prefix > suffix > contains",
"格式化": "Format",
"格式正确": "Format Correct",
"格式示例:": "Format example:",
"格式错误": "Format Error",
"检查更新": "Check for updates",
"检测到 FluentRead(流畅阅读)": "FluentRead (smooth reading) detected",
"检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Detected multiple keys, you can copy each key individually or click Copy All to get the complete content.",
......@@ -1219,13 +1440,18 @@
"模型关键字": "model keyword",
"模型列表已复制到剪贴板": "Model list copied to clipboard",
"模型列表已更新": "Model list updated",
"模型列表已追加更新": "Model list has been updated",
"模型创建成功!": "Model created successfully!",
"模型删除失败": "Failed to delete model",
"模型删除失败: {{error}}": "Failed to delete model: {{error}}",
"模型删除成功": "Model deleted successfully",
"模型名称": "Model Name",
"模型名称已存在": "Model name already exists",
"模型固定价格": "Model price per call",
"模型图标": "Model icon",
"模型定价,需要登录访问": "Model pricing, requires login to access",
"模型广场": "Model Marketplace",
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
"模型支持的接口端点信息": "Model supported API endpoint information",
"模型数据分析": "Model Data Analysis",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
......@@ -1244,6 +1470,10 @@
"模型调用次数占比": "Model call ratio",
"模型调用次数排行": "Model call ranking",
"模型选择和映射设置": "Model selection and mapping settings",
"模型部署": "Model Deployment",
"模型部署服务未启用": "Model deployment service is not enabled",
"模型部署管理": "Model Deployment Management",
"模型部署设置": "Model Deployment Settings",
"模型配置": "Model Configuration",
"模型重定向": "Model mapping",
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:",
......@@ -1253,10 +1483,13 @@
"次": "times",
"欢迎使用,请完成以下设置以开始使用系统": "Welcome! Please complete the following settings to start using the system",
"欧元": "EUR",
"正在加载可用部署位置...": "Loading available deployment locations...",
"正在处理大内容...": "Processing large content...",
"正在提交": "Submitting",
"正在构造请求体预览...": "Constructing request body preview...",
"正在检查 io.net 连接...": "Checking io.net connection...",
"正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "Testing model ${current} - ${end} (total ${total})",
"正在跟随最新日志": "Following latest logs",
"正在跳转 GitHub...": "Redirecting to GitHub...",
"正在跳转...": "Redirecting...",
"此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "This proxy is only used for image request forwarding, webhook notification sending, etc. AI API requests are still sent directly by the server, and proxy can be configured separately in channel settings",
......@@ -1265,6 +1498,7 @@
"此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
"此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.",
"此操作不可逆,所有数据将被永久删除": "This operation is irreversible, all data will be permanently deleted",
"此操作具有风险,请确认要继续执行": "This operation is risky, please confirm to continue",
"此操作将启用用户账户": "This operation will enable the user account",
"此操作将提升用户的权限级别": "This operation will elevate the user's permission level",
"此操作将禁用用户账户": "This operation will disable the user account",
......@@ -1272,6 +1506,7 @@
"此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "This will detach the user's current Passkey. They will need to register again on next login.",
"此操作将降低用户的权限级别": "This operation will reduce the user's permission level",
"此支付方式最低充值金额为": "Minimum recharge amount for this payment method is",
"此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。": "This channel is automatically synchronized by IO.NET, type, key and API address are locked.",
"此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "This setting is used for internal system calculations. The default value of 500000 is designed for 6 decimal places precision, modification is not recommended.",
"此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "This page only shows models without price or ratio settings. After setting, they will be automatically removed from the list",
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly",
......@@ -1281,10 +1516,12 @@
"此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "This is optional, used to override request parameters. Overriding stream parameter is not supported.",
"此项可选,用于覆盖请求头参数": "This is optional, used to override request header parameters.",
"此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through custom API address, do not add /v1 and / at the end",
"每容器GPU数": "GPUs per Container",
"每隔多少分钟测试一次所有通道": "How many minutes between testing all channels",
"永不过期": "Never expires",
"永久删除您的两步验证设置": "Permanently delete your two-factor authentication settings",
"永久删除所有备用码(包括未使用的)": "Permanently delete all backup codes (including unused ones)",
"没有匹配的日志条目": "No matching log entries",
"没有可用令牌用于填充": "No available tokens for filling",
"没有可用模型": "No available models",
"没有找到匹配的模型": "No matching model found",
......@@ -1300,16 +1537,22 @@
"注销": "Logout",
"注销成功!": "Logout successful!",
"流": "stream",
"流式响应完成": "Streaming response completed",
"流式输出": "Streaming Output",
"流量端口": "Traffic Port",
"浅色": "Light",
"浅色模式": "Light Mode",
"测活": "Health Check",
"测试": "Test",
"测试中": "Testing",
"测试中...": "Testing...",
"测试单个渠道操作项目组": "Test a single channel operation project group",
"测试失败": "Test failed",
"测试失败:": "Test failed: ",
"测试所有渠道的最长响应时间": "Maximum response time for testing all channels",
"测试所有通道": "Test all channels",
"测试模式": "Test Mode",
"测试连接": "Test Connection",
"测速": "Speed Test",
"消息优先级": "Message priority",
"消息优先级,范围0-10,默认为5": "Message priority, range 0-10, default is 5",
......@@ -1331,15 +1574,20 @@
"添加公告": "Add Notice",
"添加分类": "Add Category",
"添加后提交": "Submit after adding",
"添加启动参数": "Add Startup Args",
"添加启动命令": "Add Startup Command",
"添加密钥环境变量": "Add Secret Environment Variable",
"添加成功": "Added successfully",
"添加模型": "Add model",
"添加模型区域": "Add model region",
"添加渠道": "Add channel",
"添加环境变量": "Add Environment Variable",
"添加用户": "Add user",
"添加聊天配置": "Add chat configuration",
"添加键值对": "Add key-value pair",
"添加问答": "Add FAQ",
"添加额度": "Add quota",
"清空": "Clear",
"清空重定向": "Clear redirect",
"清除历史日志": "Clear historical logs",
"清除失效兑换码": "Clear invalid redemption codes",
......@@ -1368,8 +1616,11 @@
"源地址": "Source address",
"演示站点": "Demo Site",
"演示站点模式": "Demo site mode",
"点击 + 按钮添加图片URL进行多模态对话": "Click the + button to add image URLs for multimodal conversation",
"点击\"确认延长\"后将立即扣除费用并延长容器运行时间": "After clicking \"Confirm Extension\", the fee will be deducted immediately and the container runtime will be extended",
"点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here",
"点击下方按钮通过 Telegram 完成绑定": "Click the button below to complete binding via Telegram",
"点击复制ID": "Click to copy ID",
"点击复制模型名称": "Click to copy model name",
"点击查看差异": "Click to view differences",
"点击此处": "click here",
......@@ -1380,6 +1631,7 @@
"状态码复写": "Status Code Override",
"状态筛选": "Status filter",
"状态页面Slug": "Status Page Slug",
"环境变量": "Environment Variables",
"生成令牌": "Generate Token",
"生成数量": "Generate quantity",
"生成数量必须大于0": "Generation quantity must be greater than 0",
......@@ -1442,6 +1694,10 @@
"相当于删除用户,此修改将不可逆": "Equivalent to deleting the user, this modification is irreversible",
"矛盾": "Conflict",
"知识库 ID": "Knowledge Base ID",
"硬件": "Hardware",
"硬件与性能": "Hardware & Performance",
"硬件类型": "Hardware Type",
"硬件配置": "Hardware Configuration",
"确定": "OK",
"确定?": "Sure?",
"确定删除此组?": "Confirm delete this group?",
......@@ -1471,6 +1727,7 @@
"确定要删除此密钥吗?": "Are you sure you want to delete this key?",
"确定要删除此问答吗?": "Are you sure you want to delete this FAQ?",
"确定要删除这条消息吗?": "Are you sure you want to delete this message?",
"确定要删除选中的": "Are you sure you want to delete the selected",
"确定要启用所有密钥吗?": "Are you sure you want to enable all keys?",
"确定要启用此用户吗?": "Are you sure you want to enable this user?",
"确定要提升此用户吗?": "Are you sure you want to promote this user?",
......@@ -1484,9 +1741,13 @@
"确认": "Confirm",
"确认冲突项修改": "Confirm conflict item modification",
"确认删除": "Confirm deletion",
"确认删除模型": "Confirm Delete Model",
"确认取消密码登录": "Confirm cancel password login",
"确认密码": "Confirm Password",
"确认导入配置": "Confirm import configuration",
"确认延长": "Confirm Extension",
"确认延长容器时长": "Confirm Container Duration Extension",
"确认操作": "Confirm Operation",
"确认新密码": "Confirm new password",
"确认清除历史日志": "Confirm clear historical logs",
"确认禁用": "Confirm disable",
......@@ -1500,6 +1761,8 @@
"示例": "Example",
"示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Example: {\"default\": [200, 100], \"vip\": [0, 1000]}.",
"视频": "Video",
"视频Remix": "Video remix",
"视频无法在当前浏览器中播放,这可能是由于:": "The video cannot be played in this browser, possibly because:",
"禁用": "Disable",
"禁用 store 透传": "Disable store Pass-through",
"禁用2FA失败": "Failed to disable Two-Factor Authentication",
......@@ -1513,12 +1776,15 @@
"禁用时间": "Disable time",
"私有IP访问详细说明": "⚠️ Security Warning: Enabling this allows access to internal network resources (localhost, private networks). Only enable if you need to access internal services and understand the security implications.",
"私有部署地址": "Private Deployment Address",
"私有镜像仓库的密码": "Password for private image registry",
"私有镜像仓库的用户名": "Username for private image registry",
"秒": "Second",
"移除 functionResponse.id 字段": "Remove functionResponse.id Field",
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.",
"窗口处理": "window handling",
"窗口等待": "window wait",
"站点额度展示类型及汇率": "Site quota display type and exchange rate",
"端口号必须在1-65535之间": "Port number must be between 1-65535",
"端口配置详细说明": "Restrict external requests to specific ports. Use single ports (80, 443) or ranges (8000-8999). Empty list allows all ports. Default includes common web ports.",
"端点": "Endpoint",
"端点映射": "Endpoint mapping",
......@@ -1530,6 +1796,7 @@
"等待获取邮箱信息...": "Waiting to get email information...",
"筛选": "Filter",
"管理": "Manage",
"管理 Ollama 模型的拉取和删除": "Manage Ollama model pulling and deletion",
"管理你的 LinuxDO OAuth App": "Manage your LinuxDO OAuth App",
"管理员": "Admin",
"管理员区域": "Administrator Area",
......@@ -1544,6 +1811,7 @@
"管理员账号已经初始化过,请继续设置其他参数": "The admin account has already been initialized, please continue to set other parameters",
"管理模型、标签、端点等预填组": "Manage model, tag, endpoint, etc. pre-filled groups",
"类型": "Type",
"粘贴图片失败": "Failed to paste image",
"精确": "Exact",
"系统": "System",
"系统令牌已复制到剪切板": "System token copied to clipboard",
......@@ -1558,6 +1826,7 @@
"系统名称": "System Name",
"系统名称已更新": "System name updated",
"系统名称更新失败": "System name update failed",
"系统已为该部署准备 Ollama 镜像与随机 API Key": "System has prepared Ollama image and random API Key for this deployment",
"系统提示覆盖": "System prompt override",
"系统提示词": "System Prompt",
"系统提示词拼接": "System prompt append",
......@@ -1575,6 +1844,8 @@
"组名": "Group name",
"组织": "Organization",
"组织,不填则为默认组织": "Organization, default if empty",
"终止中": "Terminating",
"终止请求中": "Terminating request",
"绑定": "Bind",
"绑定 Telegram": "Bind Telegram",
"绑定信息": "Binding Information",
......@@ -1629,6 +1900,8 @@
"缺省 MaxTokens": "Default MaxTokens",
"网站地址": "Website Address",
"网站域名标识": "Website Domain ID",
"网络连接失败,请检查网络设置或稍后重试": "Network connection failed, please check network settings or try again later",
"网络配置": "Network Configuration",
"网络错误": "Network Error",
"置信度": "Confidence",
"美元": "US Dollar",
......@@ -1643,6 +1916,8 @@
"联系我们": "Contact Us",
"腾讯混元": "Hunyuan",
"自动分组auto,从第一个开始选择": "Auto grouping auto, select from the first one",
"自动刷新": "Auto Refresh",
"自动刷新中": "Auto refreshing",
"自动检测": "Auto Detect",
"自动模式": "Auto Mode",
"自动测试所有通道间隔时间": "Auto test interval for all channels",
......@@ -1653,28 +1928,41 @@
"自定义充值数量选项不是合法的 JSON 数组": "Custom recharge amount options is not a valid JSON array",
"自定义变焦-提交": "Custom Zoom-Submit",
"自定义模型名称": "Custom model name",
"自定义模式下不可用": "Not available in custom mode",
"自定义请求体模式": "Custom Request Body Mode",
"自定义货币": "Custom currency",
"自定义货币符号": "Custom currency symbol",
"自定义镜像": "Custom Image",
"自用模式": "Self-use mode",
"自适应列表": "Adaptive list",
"节省": "Save",
"花费": "Spend",
"花费时间": "Time spent",
"若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "If your OIDC Provider supports Discovery Endpoint, you can only fill in the OIDC Well-Known URL, and the system will automatically obtain the OIDC configuration",
"获取 io.net API Key": "Get io.net API Key",
"获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Failed to get OIDC configuration, please check network status and whether the Well-Known URL is correct",
"获取 OIDC 配置成功!": "OIDC configuration obtained successfully!",
"获取 Ollama 版本失败": "Failed to get Ollama version",
"获取2FA状态失败": "Failed to get Two-Factor Authentication status",
"获取初始化状态失败": "Failed to get initialization status",
"获取可用资源失败: ": "Failed to get available resources: ",
"获取启用模型失败": "Failed to get enabled models",
"获取启用模型失败:": "Failed to get enabled models:",
"获取容器信息失败": "Failed to get container information",
"获取容器列表失败": "Failed to get container list",
"获取容器详情失败": "Failed to get container details",
"获取密钥": "Get Key",
"获取密钥失败": "Failed to get key",
"获取密钥状态失败": "Failed to get key status",
"获取日志失败": "Failed to get logs",
"获取未配置模型失败": "Failed to get unconfigured models",
"获取模型列表": "Get Model List",
"获取模型列表失败": "Failed to retrieve model list",
"获取渠道失败:": "Failed to get channels: ",
"获取硬件类型失败: ": "Failed to get hardware types: ",
"获取组列表失败": "Failed to get group list",
"获取详情失败": "Failed to get details",
"获取部署列表失败": "Failed to get deployment list",
"获取金额失败": "Failed to get amount",
"获取验证码": "Get Verification Code",
"补全": "Completion",
......@@ -1693,14 +1981,21 @@
"角色": "Role",
"解析响应数据时发生错误": "An error occurred while parsing response data",
"解析密钥文件失败: {{msg}}": "Failed to parse key file: {{msg}}",
"解析错误": "Parse Error",
"解绑 Passkey": "Remove Passkey",
"解绑后将无法使用 Passkey 登录,确定要继续吗?": "After unbinding, you will not be able to login with Passkey. Are you sure you want to continue?",
"计价币种": "Pricing Currency",
"计算中": "Calculating",
"计算成本": "Calculate Cost",
"计算费用中...": "Calculating fees...",
"计费开始": "Billing Start",
"计费模式": "Billing mode",
"计费类型": "Billing type",
"计费过程": "Billing process",
"订单号": "Order No.",
"讯飞星火": "Spark Desk",
"记录请求与错误日志IP": "Record request and error log IP",
"设备": "Device",
"设备类型偏好": "Device Type Preference",
"设置 Logo": "Set Logo",
"设置2FA失败": "Failed to set up Two-Factor Authentication",
......@@ -1730,6 +2025,9 @@
"设置首页内容": "Set home page content",
"设置默认地区和特定模型的专用地区": "Set default region and dedicated regions for specific models",
"设计与开发由": "Designed & Developed by",
"访问 io.net 控制台的 API Keys 页面": "Visit the API Keys page of the io.net console",
"访问容器": "Access Container",
"访问模型部署功能需要先启用 io.net 部署服务": "Accessing model deployment features requires enabling the io.net deployment service first",
"访问限制": "Access Restrictions",
"该供应商提供多种AI模型,适用于不同的应用场景。": "This supplier provides multiple AI models, suitable for different application scenarios.",
"该分类下没有可用模型": "No available models under this category",
......@@ -1737,6 +2035,8 @@
"该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution",
"该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration",
"该模型存在固定价格与倍率计费方式冲突,请确认选择": "The model has a fixed price and ratio billing method conflict, please confirm the selection",
"该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。": "Request pass-through is enabled for this channel; built-in NewAPI features such as parameter overrides and model redirection will be disabled. This is not a best practice.",
"该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "Request pass-through is enabled for this channel. Built-in NewAPI features such as parameter overrides, model redirection, and channel adaptation will be disabled. This is not a best practice. If this causes issues, please do not submit an issue.",
"详情": "Details",
"语音输入": "Voice input",
"语音输出": "Voice output",
......@@ -1746,10 +2046,15 @@
"请上传密钥文件": "Please upload the key file",
"请上传密钥文件!": "Please upload the key file!",
"请为渠道命名": "Please name the channel",
"请使用 Project 为 io.cloud 的密钥": "Please use a key with Project set to io.cloud",
"请先在设置中启用图片功能": "Please enable image feature in settings first",
"请先填写 API Key": "Please fill in API Key first",
"请先填写 Ollama API 地址": "Please fill in Ollama API address first",
"请先填写服务器地址": "Please fill in the server address first",
"请先输入密钥": "Please enter the key first",
"请先选择同步渠道": "Please select the synchronization channel first",
"请先选择模型!": "Please select a model first!",
"请先选择硬件类型": "Please select hardware type first",
"请先选择要删除的令牌!": "Please select the token to be deleted!",
"请先选择要删除的通道!": "Please select the channel you want to delete first!",
"请先选择要设置标签的渠道!": "Please select the channel to set tags for first!",
......@@ -1765,9 +2070,12 @@
"请填写渠道名称和渠道密钥!": "Please enter channel name and key!",
"请填写部署地区": "Please fill in the deployment region",
"请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Keep key information secure, do not disclose to others. If there are security concerns, please change the key immediately.",
"请尝试其他搜索关键词": "Please try other search keywords",
"请检查渠道配置或刷新重试": "Please check the channel configuration or refresh and try again",
"请检查表单填写是否正确": "Please check if the form is filled out correctly",
"请检查输入": "Please check your input",
"请求体 JSON": "Request Body JSON",
"请求参数无效": "Invalid request parameters",
"请求发生错误": "An error occurred with the request",
"请求发生错误: ": "An error occurred with the request: ",
"请求后端接口失败:": "Failed to request the backend interface: ",
......@@ -1798,6 +2106,8 @@
"请输入 API Key,一行一个,格式:APIKey|Region": "Enter API Key, one per line, format: APIKey|Region",
"请输入 API Key,格式:APIKey|Region": "Enter API Key, format: APIKey|Region",
"请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com",
"请输入 io.net API Key": "Please enter io.net API Key",
"请输入 io.net API Key(敏感信息不显示)": "Please enter io.net API Key (sensitive information not displayed)",
"请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Please enter the key content in JSON format, for example:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}",
"请输入 OIDC 的 Well-Known URL": "Please enter the Well-Known URL for OIDC",
"请输入6位验证码或8位备用码": "Please enter a 6-digit verification code or 8-digit backup code",
......@@ -1825,6 +2135,7 @@
"请输入分类名称": "Please enter category name",
"请输入分类名称,如:OpenAI、Claude等": "Please enter the category name, such as: OpenAI, Claude, etc.",
"请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Please enter the path before /suno, usually the domain, e.g.: https://api.example.com",
"请输入副本数量": "Please enter number of replicas",
"请输入原密码": "Please enter the original password",
"请输入原密码!": "Please enter the original password!",
"请输入名称": "Please enter a name",
......@@ -1836,11 +2147,13 @@
"请输入完整的 JSON 格式密钥内容": "Please enter the complete JSON format key content",
"请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions",
"请输入完整的URL链接": "Please enter the complete URL link",
"请输入容器名称": "Please enter container name",
"请输入密码": "Please enter password",
"请输入密钥": "Please enter the key",
"请输入密钥,一行一个": "Please enter the key, one per line",
"请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "Enter keys one per line, format: AccessKey|SecretAccessKey|Region",
"请输入密钥!": "Please enter the key!",
"请输入延长时长": "Please enter extension duration",
"请输入您的密码": "Please enter your password",
"请输入您的用户名以确认删除": "Please enter your username to confirm deletion",
"请输入您的用户名或邮箱地址": "Please enter your username or email address",
......@@ -1856,12 +2169,16 @@
"请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characters",
"请输入新的显示名称": "Please enter a new display name",
"请输入新的用户名": "Please enter a new username",
"请输入新的部署名称": "Please enter new deployment name",
"请输入显示名称": "Please enter display name",
"请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "Please enter a valid JSON format request body. You can refer to the default request body format in the preview panel.",
"请输入有效的数字": "Please enter a valid number",
"请输入有效的镜像地址": "Please enter a valid image address",
"请输入标签名称": "Please enter the tag name",
"请输入模型倍率": "Enter model ratio",
"请输入模型倍率和补全倍率": "Please enter model ratio and completion ratio",
"请输入模型名称": "Please enter the model name",
"请输入模型名称,例如: llama3.2, qwen2.5:7b": "Please enter model name, e.g.: llama3.2, qwen2.5:7b",
"请输入模型名称,如:gpt-4": "Please enter the model name, such as: gpt-4",
"请输入模型描述": "Please enter the model description",
"请输入消息内容...": "Please enter the message content...",
......@@ -1878,14 +2195,19 @@
"请输入组织org-xxx": "Please enter organization org-xxx",
"请输入聊天应用名称": "Please enter chat application name",
"请输入补全倍率": "Enter completion ratio",
"请输入要延长的小时数": "Please enter the number of hours to extend",
"请输入要设置的标签名称": "Please enter the tag name to be set",
"请输入认证器验证码": "Please enter authenticator verification code",
"请输入认证器验证码或备用码": "Please enter authenticator verification code or backup code",
"请输入说明": "Please enter the description",
"请输入运行时长": "Please enter runtime duration",
"请输入邮箱!": "Please enter your email!",
"请输入邮箱地址": "Please enter the email address",
"请输入邮箱验证码!": "Please enter the email verification code!",
"请输入部署名称": "Please enter deployment name",
"请输入部署名称以完成二次确认": "Enter deployment name to complete secondary confirmation",
"请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}",
"请输入镜像地址": "Please enter image address",
"请输入问题标题": "Please enter the question title",
"请输入预警阈值": "Please enter alert threshold",
"请输入预警额度": "Please enter alert quota",
......@@ -1911,7 +2233,9 @@
"请选择模型。": "Please select model.",
"请选择消息优先级": "Please select message priority",
"请选择渠道类型": "Please select channel type",
"请选择硬件类型": "Please select hardware type",
"请选择组类型": "Please select group type",
"请选择至少一个部署位置": "Please select at least one deployment location",
"请选择该令牌支持的模型,留空支持所有模型": "Select models supported by the token, leave blank to support all models",
"请选择该渠道所支持的模型": "Please select the model supported by this channel",
"请选择该渠道所支持的模型,留空则不更改": "Please select the models supported by the channel, leaving blank will not change",
......@@ -1938,11 +2262,16 @@
"货币": "Currency",
"货币单位": "Currency Unit",
"购买兑换码": "Buy redemption code",
"费用信息": "Cost Information",
"费用预估": "Cost Estimate",
"资源消耗": "Resource Consumption",
"起始时间": "Start Time",
"超级管理员": "Super Admin",
"超级管理员未设置充值链接!": "Super administrator has not set the recharge link!",
"跟随日志": "Follow Logs",
"跟随系统主题设置": "Follow system theme",
"跨分组": "Cross-group",
"跨分组重试": "Cross-group retry",
"跳转": "Jump",
"轮询": "Polling",
"轮询模式": "Polling mode",
......@@ -1987,6 +2316,10 @@
"过期时间快捷设置": "Expiration time quick settings",
"过期时间格式错误!": "Expiration time format error!",
"运营设置": "Operation Settings",
"运行中": "Running",
"运行命令 (Command)": "Command",
"运行时长": "Runtime Duration",
"运行时长(小时)": "Runtime Duration (hours)",
"返回修改": "Go back and edit",
"返回登录": "Return to Login",
"这是重复键中的最后一个,其值将被使用": "This is the last one among duplicate keys, and its value will be used",
......@@ -1995,6 +2328,7 @@
"进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.",
"连接保活设置": "Connection Keep-alive Settings",
"连接已断开": "Connection Disconnected",
"连接测试中...": "Testing connection...",
"追加到现有密钥": "Append to existing key",
"追加模式:将新密钥添加到现有密钥列表末尾": "Append mode: add new keys to the end of the existing key list",
"追加模式:新密钥将添加到现有密钥列表的末尾": "Append mode: new keys will be added to the end of the existing key list",
......@@ -2009,6 +2343,7 @@
"选择同步来源": "Select sync source",
"选择同步渠道": "Select synchronization channel",
"选择同步语言": "Select sync language",
"选择容器": "Select Container",
"选择成功": "Selection successful",
"选择支付方式": "Select payment method",
"选择支持的认证设备类型": "Choose supported authentication device types",
......@@ -2018,12 +2353,15 @@
"选择模型供应商": "Select model vendor",
"选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "After selecting a model, you can fill the current selected token (or the first token on this page) with one click.",
"选择模型开始对话": "Select a model to start the conversation",
"选择状态": "Select Status",
"选择硬件类型": "Select Hardware Type",
"选择端点类型": "Select Endpoint Type",
"选择系统运行模式": "Select system running mode",
"选择组类型": "Please select group type",
"选择要覆盖的冲突项": "Select conflict items to overwrite",
"选择语言": "Select language",
"选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)",
"选择部署位置(可多选)": "Select deployment location(s) (multiple selections allowed)",
"透传请求体": "Pass through body",
"通义千问": "Qwen",
"通用设置": "General Settings",
......@@ -2064,7 +2402,21 @@
"部分保存失败": "Some settings failed to save",
"部分保存失败,请重试": "Partial saving failed, please try again",
"部分渠道测试失败:": "Some channels failed to test: ",
"部署 ID": "Deployment ID",
"部署ID": "Deployment ID",
"部署中": "Deploying",
"部署位置": "Deployment Location",
"部署位置加载中...": "Loading deployment locations...",
"部署删除成功": "Deployment deleted successfully",
"部署名称": "Deployment Name",
"部署名称不匹配,请检查后重新输入": "Deployment name does not match, please check and re-enter",
"部署名称只能包含字母、数字、横线、下划线和中文": "Deployment name can only contain letters, numbers, hyphens, underscores and Chinese characters",
"部署名称更新成功": "Deployment name updated successfully",
"部署启动成功": "Deployment started successfully",
"部署地区": "Deployment Region",
"部署请求中": "Requesting deployment",
"部署配置": "Deployment Configuration",
"部署重启成功": "Deployment restarted successfully",
"配置": "Configure",
"配置 Discord OAuth": "Configure Discord OAuth",
"配置 GitHub OAuth App": "Configure GitHub OAuth App",
......@@ -2076,14 +2428,20 @@
"配置 Turnstile": "Configure Turnstile",
"配置 WeChat Server": "Configure WeChat Server",
"配置和消息已全部重置": "Configuration and messages have been completely reset",
"配置完成后刷新页面即可使用模型部署功能": "After configuration is complete, refresh the page to use the model deployment feature",
"配置导入成功": "Configuration imported successfully",
"配置已导出到下载文件夹": "Configuration has been exported to the download folder",
"配置已重置,对话消息已保留": "Configuration has been reset, conversation messages have been retained",
"配置文件同步": "Config file sync",
"配置更新确认": "Configuration Update Confirmation",
"配置有效的 io.net API Key": "Configure a valid io.net API Key",
"配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Configure Server-Side Request Forgery (SSRF) protection to secure internal network resources",
"配置模型部署服务提供商的API密钥和启用状态": "Configure the API key and enabled status of the model deployment service provider",
"配置登录注册": "Configure Login/Registration",
"配置说明": "Configuration instructions",
"配置邮箱域名白名单": "Configure email domain whitelist",
"重启部署失败": "Failed to restart deployment",
"重命名部署": "Rename Deployment",
"重复提交": "Duplicate submission",
"重复的键名": "Duplicate key name",
"重复的键名,此值将被后面的同名键覆盖": "Duplicate key name, this value will be overridden by the subsequent key with the same name",
......@@ -2102,9 +2460,13 @@
"重置选项": "Reset options",
"重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!",
"重置配置": "Reset configuration",
"重要提醒": "Important Notice",
"重试": "Retry",
"重试连接": "Retry Connection",
"钱包管理": "Wallet Management",
"链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1",
"销毁容器": "Destroy Container",
"销毁容器失败": "Failed to destroy container",
"错误": "errors",
"键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "The key is the group name, and the value is another JSON object. The key is the group name, and the value is the special group ratio for users in that group. For example: {\"vip\": {\"default\": 0.5, \"test\": 1}} means that users in the vip group have a ratio of 0.5 when using tokens from the default group, and a ratio of 1 when using tokens from the test group",
"键为原状态码,值为要复写的状态码,仅影响本地判断": "The key is the original status code, and the value is the status code to override, only affects local judgment",
......@@ -2112,6 +2474,12 @@
"键为端点类型,值为路径和方法对象": "The key is the endpoint type, the value is the path and method object",
"键为请求中的模型名称,值为要替换的模型名称": "Key is the model name in the request, value is the model name to replace",
"键名": "Key name",
"镜像仓库密码": "Image Registry Password",
"镜像仓库用户名": "Image Registry Username",
"镜像仓库配置": "Image Registry Configuration",
"镜像地址": "Image Address",
"镜像选择": "Image Selection",
"镜像配置": "Image Configuration",
"问题标题": "Question Title",
"队列中": "In queue",
"降低您账户的安全性": "Reduce your account security",
......@@ -2126,10 +2494,12 @@
"隐藏调试": "Hide debug",
"随机": "Random",
"随机模式": "Random mode",
"随机种子 (留空为随机)": "Random Seed (leave blank for random)",
"零一万物": "Yi",
"需要安全验证": "Security verification required",
"需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)",
"需要登录访问": "Require Login",
"需要配置的项目": "Items to Configure",
"需要重新完整设置才能再次启用": "Need to set up again to re-enable",
"非必要,不建议启用模型限制": "Not necessary, model restrictions are not recommended",
"非流": "not stream",
......@@ -2146,11 +2516,15 @@
"项目": "Project",
"项目内容": "Project content",
"项目操作按钮组": "Project action button group",
"预估总费用": "Estimated Total Cost",
"预估费用仅供参考,实际费用可能略有差异": "Estimated cost is for reference only, actual cost may vary slightly",
"预填组管理": "Pre-filled group",
"预览失败": "Preview failed",
"预览更新": "Preview update",
"预览请求体": "Preview request body",
"预计结束": "Estimated End",
"预警阈值必须为正数": "Warning threshold must be a positive number",
"频率惩罚,减少重复词汇的出现": "Frequency penalty, reduces repeated vocabulary",
"频率限制的周期(分钟)": "Rate limit period (minutes)",
"颜色": "Color",
"额度": "Quota",
......@@ -2173,22 +2547,21 @@
"验证身份": "Verify identity",
"验证配置错误": "Verification configuration error",
"高级设置": "Advanced Settings",
"高级配置": "Advanced Configuration",
"黑名单": "Blacklist",
"默认": "Default",
"默认 API 版本": "Default API Version",
"默认 Responses API 版本,为空则使用上方版本": "Default Responses API version, if empty, uses the version above",
"默认使用系统名称": "Default uses system name",
"默认助手消息": "Default Assistant Message",
"默认区域": "Default region",
"默认区域,如: us-central1": "Default region, e.g.: us-central1",
"默认折叠侧边栏": "Default collapse sidebar",
"默认测试模型": "Default Test Model",
"默认用户消息": "Default User Message",
"默认补全倍率": "Default completion ratio",
"跨分组重试": "Cross-group retry",
"跨分组": "Cross-group",
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "After enabling, when the current group channel fails, it will try the next group's channel in order",
"每日签到": "Daily Check-in",
"今日已签到,累计签到": "Checked in today, total check-ins",
"天": "days",
"每日签到可获得随机额度奖励": "Daily check-in rewards random quota",
"今日已签到": "Checked in today",
"立即签到": "Check in now",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -9,6 +9,7 @@
",时间:": ",时间:",
",点击更新": ",点击更新",
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)",
"(筛选后显示 {{count}} 条)_other": "(筛选后显示 {{count}} 条)",
"(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}",
"(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}",
......@@ -19,6 +20,9 @@
"{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}",
"{{ratioType}} {{ratio}}": "{{ratioType}} {{ratio}}",
"• 视频服务商的跨域限制": "• 视频服务商的跨域限制",
"• 防盗链保护机制": "• 防盗链保护机制",
"• 需要特定的请求头或认证": "• 需要特定的请求头或认证",
"© {{currentYear}}": "© {{currentYear}}",
"| 基于": "| 基于",
"$/1M tokens": "$/1M tokens",
......@@ -40,7 +44,10 @@
"AI模型测试环境": "AI模型测试环境",
"AI模型配置": "AI模型配置",
"AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key": "AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key",
"API Key": "API Key",
"API Key 模式下不支持批量创建": "API Key 模式下不支持批量创建",
"API Key 验证失败": "API Key 验证失败",
"API Key 验证成功!连接到 io.net 服务正常": "API Key 验证成功!连接到 io.net 服务正常",
"API 地址和相关配置": "API 地址和相关配置",
"API 密钥": "API 密钥",
"API 文档": "API 文档",
......@@ -92,15 +99,19 @@
"Gotify服务器地址": "Gotify服务器地址",
"Gotify服务器地址必须以http://或https://开头": "Gotify服务器地址必须以http://或https://开头",
"Gotify通知": "Gotify通知",
"GPU/容器": "GPU/容器",
"GPU数量": "GPU数量",
"Homepage URL 填": "Homepage URL 填",
"ID": "ID",
"IP": "IP",
"IP白名单": "IP白名单",
"IP白名单(支持CIDR表达式)": "IP白名单(支持CIDR表达式)",
"IP限制": "IP限制",
"IP黑名单": "IP黑名单",
"JSON": "JSON",
"JSON 模式支持手动输入或上传服务账号 JSON": "JSON 模式支持手动输入或上传服务账号 JSON",
"JSON格式密钥,请确保格式正确": "JSON格式密钥,请确保格式正确",
"JSON格式错误": "JSON格式错误",
"JSON编辑": "JSON编辑",
"JSON解析错误:": "JSON解析错误:",
"Linux DO Client ID": "Linux DO Client ID",
......@@ -113,6 +124,8 @@
"New API项目仓库地址:": "New API项目仓库地址:",
"OIDC": "OIDC",
"OIDC ID": "OIDC ID",
"Ollama 模型管理": "Ollama 模型管理",
"Ollama 版本信息": "Ollama 版本信息",
"Passkey": "Passkey",
"Passkey 已解绑": "Passkey 已解绑",
"Passkey 已重置": "Passkey 已重置",
......@@ -131,6 +144,8 @@
"SMTP 端口": "SMTP 端口",
"SMTP 访问凭证": "SMTP 访问凭证",
"SMTP 账户": "SMTP 账户",
"SSE 事件": "SSE 事件",
"SSE数据流": "SSE数据流",
"SSRF防护开关详细说明": "总开关控制是否启用SSRF防护功能。关闭后将跳过所有SSRF检查,允许访问任意URL。⚠️ 仅在完全信任环境中关闭此功能。",
"SSRF防护设置": "SSRF防护设置",
"SSRF防护详细说明": "SSRF防护可防止恶意用户利用您的服务器访问内网资源。您可以配置受信任域名/IP的白名单,并限制允许的端口。适用于文件下载、Webhook回调和通知功能。",
......@@ -180,6 +195,7 @@
"下一个表单块": "下一个表单块",
"下一步": "下一步",
"下午好": "下午好",
"下载日志": "下载日志",
"不再提醒": "不再提醒",
"不同用户分组的价格信息": "不同用户分组的价格信息",
"不填则为模型列表第一个": "不填则为模型列表第一个",
......@@ -198,14 +214,17 @@
"两步验证已禁用": "两步验证已禁用",
"两步验证设置": "两步验证设置",
"个": "个",
"个GPU": "个GPU",
"个人中心": "个人中心",
"个人中心区域": "个人中心区域",
"个人信息设置": "个人信息设置",
"个人设置": "个人设置",
"个实例": "个实例",
"个性化设置": "个性化设置",
"个性化设置左侧边栏的显示内容": "个性化设置左侧边栏的显示内容",
"个未配置模型": "个未配置模型",
"个模型": "个模型",
"个部署吗?此操作不可逆。": "个部署吗?此操作不可逆。",
"中午好": "中午好",
"为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}",
"为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]",
......@@ -266,9 +285,16 @@
"以及": "以及",
"仪表盘设置": "仪表盘设置",
"价格": "价格",
"价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}",
"价格:${{price}} * {{ratioType}}:{{ratio}}": "价格:${{price}} * {{ratioType}}:{{ratio}}",
"价格暂时不可用,请稍后重试": "价格暂时不可用,请稍后重试",
"价格计算中...": "价格计算中...",
"价格计算失败": "价格计算失败",
"价格计算失败: ": "价格计算失败: ",
"价格设置": "价格设置",
"价格设置方式": "价格设置方式",
"价格重新计算中...": "价格重新计算中...",
"价格预估": "价格预估",
"任务 ID": "任务 ID",
"任务ID": "任务ID",
"任务日志": "任务日志",
......@@ -302,7 +328,11 @@
"例如 €, £, Rp, ₩, ₹...": "例如 €, £, Rp, ₩, ₹...",
"例如 https://docs.newapi.pro": "例如 https://docs.newapi.pro",
"例如:": "例如:",
"例如: /bin/bash -c \"python app.py\"": "例如: /bin/bash -c \"python app.py\"",
"例如: nginx:latest": "例如: nginx:latest",
"例如: socks5://user:pass@host:port": "例如: socks5://user:pass@host:port",
"例如:-c": "例如:-c",
"例如:/bin/bash": "例如:/bin/bash",
"例如:0001": "例如:0001",
"例如:1000": "例如:1000",
"例如:100000": "例如:100000",
......@@ -312,6 +342,7 @@
"例如:7,就是7元/美金": "例如:7,就是7元/美金",
"例如:example.com": "例如:example.com",
"例如:https://yourdomain.com": "例如:https://yourdomain.com",
"例如:nginx:latest": "例如:nginx:latest",
"例如:preview": "例如:preview",
"例如:prod_6I8rBerHpPxyoiU9WK4kot": "例如:prod_6I8rBerHpPxyoiU9WK4kot",
"例如:基础套餐": "例如:基础套餐",
......@@ -361,12 +392,14 @@
"修改子渠道权重": "修改子渠道权重",
"修改密码": "修改密码",
"修改绑定": "修改绑定",
"修改部署名称": "修改部署名称",
"倍率": "倍率",
"倍率信息": "倍率信息",
"倍率是为了方便换算不同价格的模型": "倍率是为了方便换算不同价格的模型",
"倍率模式": "倍率模式",
"倍率类型": "倍率类型",
"停止测试": "停止测试",
"停用": "停用",
"允许 AccountFilter 参数": "允许 AccountFilter 参数",
"允许 HTTP 协议图片请求(适用于自部署代理)": "允许 HTTP 协议图片请求(适用于自部署代理)",
"允许 safety_identifier 透传": "允许 safety_identifier 透传",
......@@ -423,9 +456,14 @@
"全部": "全部",
"全部供应商": "全部供应商",
"全部分组": "全部分组",
"全部地区总可用资源": "全部地区总可用资源",
"全部容器": "全部容器",
"全部展开": "全部展开",
"全部收起": "全部收起",
"全部标签": "全部标签",
"全部模型": "全部模型",
"全部状态": "全部状态",
"全部硬件总可用资源": "全部硬件总可用资源",
"全部端点": "全部端点",
"全部类型": "全部类型",
"公告": "公告",
......@@ -437,6 +475,7 @@
"共 {{count}} 个密钥_other": "共 {{count}} 个密钥",
"共 {{count}} 个模型": "共 {{count}} 个模型",
"共 {{count}} 个模型_other": "共 {{count}} 个模型",
"共 {{count}} 条日志_other": "共 {{count}} 条日志",
"共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "共 {{total}} 项,当前显示 {{start}}-{{end}} 项",
"关": "关",
"关于": "关于",
......@@ -457,10 +496,17 @@
"内容": "内容",
"内容较大,已启用性能优化模式": "内容较大,已启用性能优化模式",
"内容较大,部分功能可能受限": "内容较大,部分功能可能受限",
"内置 Ollama 镜像": "内置 Ollama 镜像",
"再次输入部署名称": "再次输入部署名称",
"最低": "最低",
"最低充值美元数量": "最低充值美元数量",
"最后使用时间": "最后使用时间",
"最后更新": "最后更新",
"最后请求": "最后请求",
"最大GPU数量": "最大GPU数量",
"最大可用": "最大可用",
"最近事件": "最近事件",
"准备中...": "准备中...",
"准备完成初始化": "准备完成初始化",
"分类名称": "分类名称",
"分组": "分组",
......@@ -485,9 +531,11 @@
"划转额度": "划转额度",
"列出的模型将不会自动添加或移除-thinking/-nothinking 后缀": "列出的模型将不会自动添加或移除-thinking/-nothinking 后缀",
"列设置": "列设置",
"创建": "创建",
"创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)",
"创建失败": "创建失败",
"创建成功": "创建成功",
"创建或选择密钥时,将 Project 设置为 io.cloud": "创建或选择密钥时,将 Project 设置为 io.cloud",
"创建新用户账户": "创建新用户账户",
"创建新的令牌": "创建新的令牌",
"创建新的兑换码": "创建新的兑换码",
......@@ -499,6 +547,7 @@
"初始化失败,请重试": "初始化失败,请重试",
"初始化系统": "初始化系统",
"删除": "删除",
"删除后无法恢复,确定要删除模型 \"{{name}}\" 吗?": "删除后无法恢复,确定要删除模型 \"{{name}}\" 吗?",
"删除失败": "删除失败",
"删除密钥失败": "删除密钥失败",
"删除成功": "删除成功",
......@@ -510,23 +559,40 @@
"删除自动禁用密钥": "删除自动禁用密钥",
"删除账户": "删除账户",
"删除账户确认": "删除账户确认",
"删除部署失败": "删除部署失败",
"刷新": "刷新",
"刷新失败": "刷新失败",
"刷新容器信息": "刷新容器信息",
"刷新日志": "刷新日志",
"前往 io.net API Keys": "前往 io.net API Keys",
"前往设置": "前往设置",
"前往设置页面": "前往设置页面",
"前缀": "前缀",
"副本数量": "副本数量",
"剩余": "剩余",
"剩余备用码:": "剩余备用码:",
"剩余时间": "剩余时间",
"剩余额度": "剩余额度",
"剩余额度/总额度": "剩余额度/总额度",
"剩余额度$": "剩余额度$",
"功能特性": "功能特性",
"加入渠道": "加入渠道",
"加入预填组": "加入预填组",
"加密存储": "加密存储",
"加载中...": "加载中...",
"加载供应商信息失败": "加载供应商信息失败",
"加载关于内容失败...": "加载关于内容失败...",
"加载分组失败": "加载分组失败",
"加载失败": "加载失败",
"加载容器信息中...": "加载容器信息中...",
"加载容器详情中...": "加载容器详情中...",
"加载日志中...": "加载日志中...",
"加载模型信息失败": "加载模型信息失败",
"加载模型列表失败": "加载模型列表失败",
"加载模型失败": "加载模型失败",
"加载用户协议内容失败...": "加载用户协议内容失败...",
"加载设置中...": "加载设置中...",
"加载详情中...": "加载详情中...",
"加载账单失败": "加载账单失败",
"加载隐私政策内容失败...": "加载隐私政策内容失败...",
"包含": "包含",
......@@ -534,6 +600,7 @@
"包括失败请求的次数,0代表不限制": "包括失败请求的次数,0代表不限制",
"匹配类型": "匹配类型",
"区域": "区域",
"单GPU小时费率": "单GPU小时费率",
"历史消耗": "历史消耗",
"原价": "原价",
"原因:": "原因:",
......@@ -544,14 +611,16 @@
"参数值": "参数值",
"参数覆盖": "参数覆盖",
"参照生视频": "参照生视频",
"视频Remix": "视频 Remix",
"友情链接": "友情链接",
"发布日期": "发布日期",
"发布时间": "发布时间",
"取消": "取消",
"取消全选": "取消全选",
"取消选择": "取消选择",
"变换": "变换",
"变焦": "变焦",
"变量值": "变量值",
"变量名": "变量名",
"只包括请求成功的次数": "只包括请求成功的次数",
"只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求",
"只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录",
......@@ -559,6 +628,7 @@
"可在设置页面设置关于内容,支持 HTML & Markdown": "可在设置页面设置关于内容,支持 HTML & Markdown",
"可用令牌分组": "可用令牌分组",
"可用分组": "可用分组",
"可用数量": "可用数量",
"可用模型": "可用模型",
"可用端点类型": "可用端点类型",
"可用邀请额度": "可用邀请额度",
......@@ -566,13 +636,17 @@
"可视化倍率设置": "可视化倍率设置",
"可视化编辑": "可视化编辑",
"可选,公告的补充说明": "可选,公告的补充说明",
"可选,用于复现结果": "可选,用于复现结果",
"可选值": "可选值",
"同时重置消息": "同时重置消息",
"同步": "同步",
"同步到渠道": "同步到渠道",
"同步向导": "同步向导",
"同步失败": "同步失败",
"同步成功": "同步成功",
"同步接口": "同步接口",
"同步渠道失败": "同步渠道失败",
"同步渠道失败:缺少部署信息": "同步渠道失败:缺少部署信息",
"名称": "名称",
"名称+密钥": "名称+密钥",
"名称不能为空": "名称不能为空",
......@@ -580,8 +654,17 @@
"后端请求失败": "后端请求失败",
"后缀": "后缀",
"否": "否",
"启动": "启动",
"启动参数 (Args)": "启动参数 (Args)",
"启动命令": "启动命令",
"启动命令 (Entrypoint)": "启动命令 (Entrypoint)",
"启动时间": "启动时间",
"启动部署失败": "启动部署失败",
"启动配置": "启动配置",
"启用": "启用",
"启用 io.net 部署": "启用 io.net 部署",
"启用 io.net 部署开关": "启用 io.net 部署开关",
"启用 io.net 部署时必须填写 API Key": "启用 io.net 部署时必须填写 API Key",
"启用 Prompt 检查": "启用 Prompt 检查",
"启用2FA失败": "启用2FA失败",
"启用Claude思考适配(-thinking后缀)": "启用Claude思考适配(-thinking后缀)",
......@@ -591,11 +674,14 @@
"启用SMTP SSL": "启用SMTP SSL",
"启用SSRF防护(推荐开启以保护服务器安全)": "启用SSRF防护(推荐开启以保护服务器安全)",
"启用全部": "启用全部",
"启用后可接入 io.net GPU 资源": "启用后可接入 io.net GPU 资源",
"启用后可添加图片URL进行多模态对话": "启用后可添加图片URL进行多模态对话",
"启用后将使用 Creem Test Mode": "启用后将使用 Creem Test Mode",
"启用密钥失败": "启用密钥失败",
"启用屏蔽词过滤功能": "启用屏蔽词过滤功能",
"启用所有密钥失败": "启用所有密钥失败",
"启用数据看板(实验性)": "启用数据看板(实验性)",
"启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。": "启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。",
"启用用户模型请求速率限制(可能会影响高并发性能)": "启用用户模型请求速率限制(可能会影响高并发性能)",
"启用绘图功能": "启用绘图功能",
"启用请求体透传功能": "启用请求体透传功能",
......@@ -617,6 +703,9 @@
"图标": "图标",
"图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ",
"图混合": "图混合",
"图片功能在自定义请求体模式下不可用": "图片功能在自定义请求体模式下不可用",
"图片地址": "图片地址",
"图片已添加": "图片已添加",
"图片生成调用:{{symbol}}{{price}} / 1次": "图片生成调用:{{symbol}}{{price}} / 1次",
"图片输入: {{imageRatio}}": "图片输入: {{imageRatio}}",
"图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})",
......@@ -627,6 +716,7 @@
"在Gotify服务器创建应用后获得的令牌,用于发送通知": "在Gotify服务器创建应用后获得的令牌,用于发送通知",
"在Gotify服务器的应用管理中创建新应用": "在Gotify服务器的应用管理中创建新应用",
"在找兑换码?": "在找兑换码?",
"在新标签页中打开": "在新标签页中打开",
"在此输入 Logo 图片地址": "在此输入 Logo 图片地址",
"在此输入新的公告内容,支持 Markdown & HTML 代码": "在此输入新的公告内容,支持 Markdown & HTML 代码",
"在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面",
......@@ -647,6 +737,7 @@
"填写带https的域名,逗号分隔": "填写带https的域名,逗号分隔",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策",
"处理中": "处理中",
"备份支持": "备份支持",
"备份状态": "备份状态",
"备注": "备注",
......@@ -660,6 +751,7 @@
"复制名称": "复制名称",
"复制失败": "复制失败",
"复制失败,请手动复制": "复制失败,请手动复制",
"复制失败,请手动选择文本复制": "复制失败,请手动选择文本复制",
"复制已选": "复制已选",
"复制应用的令牌(Token)并填写到上方的应用令牌字段": "复制应用的令牌(Token)并填写到上方的应用令牌字段",
"复制成功": "复制成功",
......@@ -667,8 +759,13 @@
"复制所有模型": "复制所有模型",
"复制所选令牌": "复制所选令牌",
"复制所选兑换码到剪贴板": "复制所选兑换码到剪贴板",
"复制日志": "复制日志",
"复制渠道的所有信息": "复制渠道的所有信息",
"复制版本号": "复制版本号",
"复制生成的密钥并粘贴到此处": "复制生成的密钥并粘贴到此处",
"复制链接": "复制链接",
"外接设备": "外接设备",
"多个命令用空格分隔": "多个命令用空格分隔",
"多密钥渠道操作项目组": "多密钥渠道操作项目组",
"多密钥管理": "多密钥管理",
"多种充值方式,安全便捷": "多种充值方式,安全便捷",
......@@ -684,9 +781,12 @@
"如:香港线路": "如:香港线路",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面",
"如果镜像为私有,请填写密码或Token": "如果镜像为私有,请填写密码或Token",
"如果镜像为私有,请填写用户名": "如果镜像为私有,请填写用户名",
"始终使用浅色主题": "始终使用浅色主题",
"始终使用深色主题": "始终使用深色主题",
"字段透传控制": "字段透传控制",
"存在惩罚,鼓励讨论新话题": "存在惩罚,鼓励讨论新话题",
"存在重复的键名:": "存在重复的键名:",
"安全提醒": "安全提醒",
"安全设置": "安全设置",
......@@ -695,7 +795,9 @@
"安装指南": "安装指南",
"完成": "完成",
"完成初始化": "完成初始化",
"完成硬件类型、部署位置、副本数量等配置后,将自动计算价格": "完成硬件类型、部署位置、副本数量等配置后,将自动计算价格",
"完成设置并启用两步验证": "完成设置并启用两步验证",
"完成进度": "完成进度",
"完整的 Base URL,支持变量{model}": "完整的 Base URL,支持变量{model}",
"官方": "官方",
"官方文档": "官方文档",
......@@ -708,6 +810,26 @@
"实付金额:": "实付金额:",
"实际模型": "实际模型",
"实际请求体": "实际请求体",
"容器": "容器",
"容器ID": "容器ID",
"容器创建失败: ": "容器创建失败: ",
"容器创建成功": "容器创建成功",
"容器名称": "容器名称",
"容器名称更新成功": "容器名称更新成功",
"容器启动后执行的命令": "容器启动后执行的命令",
"容器启动配置": "容器启动配置",
"容器实例": "容器实例",
"容器对外暴露的端口": "容器对外暴露的端口",
"容器对外服务的端口号,可选": "容器对外服务的端口号,可选",
"容器总数": "容器总数",
"容器数量": "容器数量",
"容器日志": "容器日志",
"容器时长延长成功": "容器时长延长成功",
"容器访问地址无效": "容器访问地址无效",
"容器详情": "容器详情",
"容器配置": "容器配置",
"容器配置更新成功": "容器配置更新成功",
"容器销毁请求已提交": "容器销毁请求已提交",
"密码": "密码",
"密码修改成功!": "密码修改成功!",
"密码已复制到剪贴板:": "密码已复制到剪贴板:",
......@@ -729,6 +851,7 @@
"密钥更新模式": "密钥更新模式",
"密钥格式": "密钥格式",
"密钥格式无效,请输入有效的 JSON 格式密钥": "密钥格式无效,请输入有效的 JSON 格式密钥",
"密钥环境变量": "密钥环境变量",
"密钥聚合模式": "密钥聚合模式",
"密钥获取成功": "密钥获取成功",
"密钥输入方式": "密钥输入方式",
......@@ -742,6 +865,7 @@
"导入配置": "导入配置",
"导入配置失败: ": "导入配置失败: ",
"导出": "导出",
"导出日志失败": "导出日志失败",
"导出配置": "导出配置",
"导出配置失败: ": "导出配置失败: ",
"将 reasoning_content 转换为 <think> 标签拼接到内容中": "将 reasoning_content 转换为 <think> 标签拼接到内容中",
......@@ -752,6 +876,7 @@
"将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?",
"将清除选定时间之前的所有日志": "将清除选定时间之前的所有日志",
"小时": "小时",
"小时费率": "小时费率",
"尚未使用": "尚未使用",
"局部重绘-提交": "局部重绘-提交",
"屏蔽词列表": "屏蔽词列表",
......@@ -763,6 +888,7 @@
"已为 {{count}} 个模型设置{{type}}_other": "已为 {{count}} 个模型设置{{type}}",
"已为 ${count} 个渠道设置标签!": "已为 ${count} 个渠道设置标签!",
"已修复 ${success} 个通道,失败 ${fails} 个通道。": "已修复 ${success} 个通道,失败 ${fails} 个通道。",
"已停止": "已停止",
"已停止批量测试": "已停止批量测试",
"已关闭后续提醒": "已关闭后续提醒",
"已切换为Assistant角色": "已切换为Assistant角色",
......@@ -777,41 +903,57 @@
"已删除消息及其回复": "已删除消息及其回复",
"已发送到 Fluent": "已发送到 Fluent",
"已取消 Passkey 注册": "已取消 Passkey 注册",
"已同步到渠道": "已同步到渠道",
"已启用": "已启用",
"已启用 Passkey,无需密码即可登录": "已启用 Passkey,无需密码即可登录",
"已启用所有密钥": "已启用所有密钥",
"已在自定义模式中忽略": "已在自定义模式中忽略",
"已备份": "已备份",
"已复制": "已复制",
"已复制 ${count} 个模型": "已复制 ${count} 个模型",
"已复制 ID 到剪贴板": "已复制 ID 到剪贴板",
"已复制:": "已复制:",
"已复制:{{name}}": "已复制:{{name}}",
"已复制全部数据": "已复制全部数据",
"已复制到剪切板": "已复制到剪切板",
"已复制到剪贴板": "已复制到剪贴板",
"已复制到剪贴板!": "已复制到剪贴板!",
"已复制模型名称": "已复制模型名称",
"已复制版本号": "已复制版本号",
"已复制自动生成的 API Key": "已复制自动生成的 API Key",
"已完成": "已完成",
"已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。",
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "已成功开始测试所有已启用通道,请刷新页面查看结果。",
"已提交": "已提交",
"已支付金额": "已支付金额",
"已新增 {{count}} 个模型:{{list}}_other": "已新增 {{count}} 个模型:{{list}}",
"已更新完毕所有已启用通道余额!": "已更新完毕所有已启用通道余额!",
"已有保存的配置": "已有保存的配置",
"已有模型": "已有模型",
"已有的模型": "已有的模型",
"已有账户?": "已有账户?",
"已服务": "已服务",
"已注销": "已注销",
"已添加": "已添加",
"已添加到白名单": "已添加到白名单",
"已清空测试结果": "已清空测试结果",
"已用": "已用",
"已用/剩余": "已用/剩余",
"已用额度": "已用额度",
"已禁用": "已禁用",
"已禁用所有密钥": "已禁用所有密钥",
"已绑定": "已绑定",
"已绑定渠道": "已绑定渠道",
"已结束": "已结束",
"已耗尽": "已耗尽",
"已解锁豆包自定义 API 地址编辑": "已解锁豆包自定义 API 地址编辑",
"已过期": "已过期",
"已运行时间": "已运行时间",
"已选择 {{count}} 个模型_other": "已选择 {{count}} 个模型",
"已选择 {{selected}} / {{total}}": "已选择 {{selected}} / {{total}}",
"已选择 ${count} 个渠道": "已选择 ${count} 个渠道",
"已重置为默认配置": "已重置为默认配置",
"已销毁": "已销毁",
"常见问答": "常见问答",
"常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)",
"平台": "平台",
......@@ -821,6 +963,15 @@
"应用同步": "应用同步",
"应用更改": "应用更改",
"应用覆盖": "应用覆盖",
"延长后总时长": "延长后总时长",
"延长容器时长": "延长容器时长",
"延长容器时长将会产生额外费用,请确认您有足够的账户余额。": "延长容器时长将会产生额外费用,请确认您有足够的账户余额。",
"延长操作一旦确认无法撤销,费用将立即扣除。": "延长操作一旦确认无法撤销,费用将立即扣除。",
"延长时长": "延长时长",
"延长时长(小时)": "延长时长(小时)",
"延长时长不能超过720小时(30天)": "延长时长不能超过720小时(30天)",
"延长时长失败": "延长时长失败",
"延长时长至少为1小时": "延长时长至少为1小时",
"建立连接时发生错误": "建立连接时发生错误",
"建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。",
"开": "开",
......@@ -829,38 +980,43 @@
"开启后,仅\"消费\"\"错误\"日志将记录您的客户端IP地址": "开启后,仅\"消费\"\"错误\"日志将记录您的客户端IP地址",
"开启后,对免费模型(倍率为0,或者价格为0)的模型也会预消耗额度": "开启后,对免费模型(倍率为0,或者价格为0)的模型也会预消耗额度",
"开启后,将定期发送ping数据保持连接活跃": "开启后,将定期发送ping数据保持连接活跃",
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道",
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启",
"该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。",
"已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "已开启全局请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。",
"该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。": "该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。",
"开启后不限制:必须设置模型倍率": "开启后不限制:必须设置模型倍率",
"开启后未登录用户无法访问模型广场": "开启后未登录用户无法访问模型广场",
"开启批量操作": "开启批量操作",
"开始同步": "开始同步",
"开始批量测试 ${count} 个模型,已清空上次结果...": "开始批量测试 ${count} 个模型,已清空上次结果...",
"开始时间": "开始时间",
"张图片": "张图片",
"弱变换": "弱变换",
"强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)",
"强制格式化": "强制格式化",
"强制要求": "强制要求",
"强变换": "强变换",
"当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道",
"当前 API 密钥已过期,请在设置中更新。": "当前 API 密钥已过期,请在设置中更新。",
"当前 Ollama 版本为 ${version}": "当前 Ollama 版本为 ${version}",
"当前余额": "当前余额",
"当前值": "当前值",
"当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)",
"当前剩余": "当前剩余",
"当前时间": "当前时间",
"当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。",
"当前查看的分组为:{{group}},倍率为:{{ratio}}": "当前查看的分组为:{{group}},倍率为:{{ratio}}",
"当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。",
"当前版本": "当前版本",
"当前状态": "当前状态",
"当前计费": "当前计费",
"当前设备不支持 Passkey": "当前设备不支持 Passkey",
"当前设置类型: ": "当前设置类型: ",
"当前跟随系统": "当前跟随系统",
"当前配置无法连接到 io.net。": "当前配置无法连接到 io.net。",
"当剩余额度低于此数值时,系统将通过选择的方式发送通知": "当剩余额度低于此数值时,系统将通过选择的方式发送通知",
"当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用",
"当运行通道全部测试时,超过此时间将自动禁用通道": "当运行通道全部测试时,超过此时间将自动禁用通道",
"待使用收益": "待使用收益",
"待部署": "待部署",
"微信": "微信",
"微信公众号二维码图片链接": "微信公众号二维码图片链接",
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)",
......@@ -869,18 +1025,21 @@
"必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]",
"忘记密码?": "忘记密码?",
"快速开始": "快速开始",
"快速选择": "快速选择",
"思考中...": "思考中...",
"思考内容转换": "思考内容转换",
"思考过程": "思考过程",
"思考适配 BudgetTokens 百分比": "思考适配 BudgetTokens 百分比",
"思考预算占比": "思考预算占比",
"性能指标": "性能指标",
"总 GPU 小时": "总 GPU 小时",
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}",
"总密钥数": "总密钥数",
"总收益": "总收益",
"总计": "总计",
"总额度": "总额度",
"您可以个性化设置侧边栏的要显示功能": "您可以个性化设置侧边栏的要显示功能",
"您可以在上方拉取需要的模型": "您可以在上方拉取需要的模型",
"您无权访问此页面,请联系管理员": "您无权访问此页面,请联系管理员",
"您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。",
"您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。",
......@@ -914,8 +1073,11 @@
"批量删除": "批量删除",
"批量删除令牌": "批量删除令牌",
"批量删除失败": "批量删除失败",
"批量删除成功": "批量删除成功",
"批量删除模型": "批量删除模型",
"批量操作": "批量操作",
"批量操作失败": "批量操作失败",
"批量操作完成: {{success}}个成功, {{failed}}个失败": "批量操作完成: {{success}}个成功, {{failed}}个失败",
"批量测试${count}个模型": "批量测试${count}个模型",
"批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}",
"批量测试已停止": "批量测试已停止",
......@@ -925,6 +1087,10 @@
"批量设置标签": "批量设置标签",
"批量设置模型参数": "批量设置模型参数",
"折": "折",
"拉取中...": "拉取中...",
"拉取新模型": "拉取新模型",
"拉取模型": "拉取模型",
"拉取进度": "拉取进度",
"按K显示单位": "按K显示单位",
"按价格设置": "按价格设置",
"按倍率类型筛选": "按倍率类型筛选",
......@@ -939,8 +1105,10 @@
"排队中": "排队中",
"接受未设置价格模型": "接受未设置价格模型",
"接口凭证": "接口凭证",
"接口密钥已过期": "接口密钥已过期",
"控制台": "控制台",
"控制台区域": "控制台区域",
"控制输出的随机性和创造性": "控制输出的随机性和创造性",
"控制顶栏模块显示状态,全局生效": "控制顶栏模块显示状态,全局生效",
"推荐:用户可以选择是否使用指纹等验证": "推荐:用户可以选择是否使用指纹等验证",
"推荐使用(用户可选)": "推荐使用(用户可选)",
......@@ -951,12 +1119,6 @@
"提升": "提升",
"提示": "提示",
"提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
"视频无法在当前浏览器中播放,这可能是由于:": "视频无法在当前浏览器中播放,这可能是由于:",
"• 视频服务商的跨域限制": "• 视频服务商的跨域限制",
"• 需要特定的请求头或认证": "• 需要特定的请求头或认证",
"• 防盗链保护机制": "• 防盗链保护机制",
"在新标签页中打开": "在新标签页中打开",
"复制链接": "复制链接",
"提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
"提示:如需备份数据,只需复制上述目录即可": "提示:如需备份数据,只需复制上述目录即可",
......@@ -965,7 +1127,9 @@
"提示缓存倍率": "提示缓存倍率",
"搜索供应商": "搜索供应商",
"搜索关键字": "搜索关键字",
"搜索失败": "搜索失败",
"搜索无结果": "搜索无结果",
"搜索日志内容": "搜索日志内容",
"搜索条件": "搜索条件",
"搜索模型": "搜索模型",
"搜索模型...": "搜索模型...",
......@@ -973,6 +1137,7 @@
"搜索模型失败": "搜索模型失败",
"搜索渠道名称或地址": "搜索渠道名称或地址",
"搜索聊天应用名称": "搜索聊天应用名称",
"搜索部署名称": "搜索部署名称",
"操作": "操作",
"操作失败": "操作失败",
"操作失败,请重试": "操作失败,请重试",
......@@ -986,6 +1151,7 @@
"支付设置": "支付设置",
"支付请求失败": "支付请求失败",
"支付金额": "支付金额",
"支持 Ctrl+V 粘贴图片": "支持 Ctrl+V 粘贴图片",
"支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。",
"支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24",
"支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址",
......@@ -994,6 +1160,7 @@
"支持单个端口和端口范围,如:80, 443, 8000-8999": "支持单个端口和端口范围,如:80, 443, 8000-8999",
"支持变量:": "支持变量:",
"支持备份": "支持备份",
"支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间": "支持拉取 Ollama 官方模型库中的所有模型,拉取过程可能需要几分钟时间",
"支持搜索用户的 ID、用户名、显示名称和邮箱地址": "支持搜索用户的 ID、用户名、显示名称和邮箱地址",
"支持的图像模型": "支持的图像模型",
"支持通配符格式,如:example.com, *.api.example.com": "支持通配符格式,如:example.com, *.api.example.com",
......@@ -1005,6 +1172,7 @@
"放大": "放大",
"放大编辑": "放大编辑",
"敏感信息不会发送到前端显示": "敏感信息不会发送到前端显示",
"数据传输中断": "数据传输中断",
"数据存储位置:": "数据存储位置:",
"数据库信息": "数据库信息",
"数据库检查": "数据库检查",
......@@ -1030,6 +1198,8 @@
"新密码": "新密码",
"新密码需要和原密码不一致!": "新密码需要和原密码不一致!",
"新建": "新建",
"新建容器": "新建容器",
"新建容器部署": "新建容器部署",
"新建数量": "新建数量",
"新建组": "新建组",
"新格式(支持条件判断与json自定义):": "新格式(支持条件判断与json自定义):",
......@@ -1042,13 +1212,23 @@
"新获取的模型": "新获取的模型",
"新额度:": "新额度:",
"无": "无",
"无GPU": "无GPU",
"无冲突项": "无冲突项",
"无效的部署信息": "无效的部署信息",
"无效的重置链接,请重新发起密码重置请求": "无效的重置链接,请重新发起密码重置请求",
"无法发起 Passkey 注册": "无法发起 Passkey 注册",
"无法复制到剪贴板,请手动复制": "无法复制到剪贴板,请手动复制",
"无法添加图片": "无法添加图片",
"无法获取容器详情": "无法获取容器详情",
"无法连接 io.net": "无法连接 io.net",
"无邀请人": "无邀请人",
"无限制": "无限制",
"无限额度": "无限额度",
"日志导出成功": "日志导出成功",
"日志已下载": "日志已下载",
"日志已加载": "日志已加载",
"日志已复制到剪贴板": "日志已复制到剪贴板",
"日志流": "日志流",
"日志清理失败:": "日志清理失败:",
"日志类型": "日志类型",
"日志设置": "日志设置",
......@@ -1058,6 +1238,7 @@
"旧的备用码已失效,请保存新的备用码": "旧的备用码已失效,请保存新的备用码",
"早上好": "早上好",
"时间": "时间",
"时间信息": "时间信息",
"时间粒度": "时间粒度",
"易支付商户ID": "易支付商户ID",
"易支付商户密钥": "易支付商户密钥",
......@@ -1078,25 +1259,37 @@
"显示设置": "显示设置",
"显示调试": "显示调试",
"晚上好": "晚上好",
"普通环境变量": "普通环境变量",
"普通用户": "普通用户",
"智能体ID": "智能体ID",
"智能熔断": "智能熔断",
"智谱": "智谱",
"暂无": "暂无",
"暂无API信息": "暂无API信息",
"暂无SSE响应数据": "暂无SSE响应数据",
"暂无产品配置": "暂无产品配置",
"暂无保存的配置": "暂无保存的配置",
"暂无充值记录": "暂无充值记录",
"暂无公告": "暂无公告",
"暂无匹配模型": "暂无匹配模型",
"暂无可复制的版本信息": "暂无可复制的版本信息",
"暂无可用的支付方式,请联系管理员配置": "暂无可用的支付方式,请联系管理员配置",
"暂无响应数据": "暂无响应数据",
"暂无容器信息": "暂无容器信息",
"暂无容器详情": "暂无容器详情",
"暂无密钥数据": "暂无密钥数据",
"暂无差异化倍率显示": "暂无差异化倍率显示",
"暂无常见问答": "暂无常见问答",
"暂无成功模型": "暂无成功模型",
"暂无数据": "暂无数据",
"暂无数据,点击下方按钮添加键值对": "暂无数据,点击下方按钮添加键值对",
"暂无日志": "暂无日志",
"暂无日志可下载": "暂无日志可下载",
"暂无日志可复制": "暂无日志可复制",
"暂无机密环境变量": "暂无机密环境变量",
"暂无模型": "暂无模型",
"暂无模型描述": "暂无模型描述",
"暂无环境变量": "暂无环境变量",
"暂无监控数据": "暂无监控数据",
"暂无系统公告": "暂无系统公告",
"暂无缺失模型": "暂无缺失模型",
......@@ -1115,7 +1308,11 @@
"更新Worker设置": "更新Worker设置",
"更新令牌信息": "更新令牌信息",
"更新兑换码信息": "更新兑换码信息",
"更新名称失败": "更新名称失败",
"更新失败": "更新失败",
"更新失败,请检查输入信息": "更新失败,请检查输入信息",
"更新容器配置": "更新容器配置",
"更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。": "更新容器配置可能会导致容器重启,请确保在合适的时间进行此操作。",
"更新成功": "更新成功",
"更新所有已启用通道余额": "更新所有已启用通道余额",
"更新支付设置": "更新支付设置",
......@@ -1123,8 +1320,14 @@
"更新服务器地址": "更新服务器地址",
"更新模型信息": "更新模型信息",
"更新渠道信息": "更新渠道信息",
"更新部署名称失败": "更新部署名称失败",
"更新配置": "更新配置",
"更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。": "更新配置后,容器可能需要重启以应用新的设置。请确保您了解这些更改的影响。",
"更新配置失败": "更新配置失败",
"更新预填组": "更新预填组",
"有 Reasoning": "有 Reasoning",
"服务可用性": "服务可用性",
"服务商": "服务商",
"服务器地址": "服务器地址",
"服务显示名称": "服务显示名称",
"未发现新增模型": "未发现新增模型",
......@@ -1135,6 +1338,7 @@
"未备份": "未备份",
"未开始": "未开始",
"未找到匹配的模型": "未找到匹配的模型",
"未找到可用的容器访问地址": "未找到可用的容器访问地址",
"未找到差异化倍率,无需同步": "未找到差异化倍率,无需同步",
"未提交": "未提交",
"未检测到 Fluent 容器": "未检测到 Fluent 容器",
......@@ -1143,11 +1347,14 @@
"未登录或登录已过期,请重新登录": "未登录或登录已过期,请重新登录",
"未知": "未知",
"未知供应商": "未知供应商",
"未知品牌": "未知品牌",
"未知模型": "未知模型",
"未知渠道": "未知渠道",
"未知状态": "未知状态",
"未知类型": "未知类型",
"未知身份": "未知身份",
"未知部署": "未知部署",
"未知错误": "未知错误",
"未绑定": "未绑定",
"未获取到授权码": "未获取到授权码",
"未设置": "未设置",
......@@ -1160,19 +1367,27 @@
"本设备:手机指纹/面容,外接:USB安全密钥": "本设备:手机指纹/面容,外接:USB安全密钥",
"本设备内置": "本设备内置",
"本项目根据": "本项目根据",
"机密环境变量": "机密环境变量",
"机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。": "机密环境变量将被加密存储,适用于存储密码、API密钥等敏感信息。",
"机密环境变量说明": "机密环境变量说明",
"权重": "权重",
"权限设置": "权限设置",
"条": "条",
"条 - 第": "条 - 第",
"条,共": "条,共",
"条日志已清理!": "条日志已清理!",
"来源于 IO.NET 部署": "来源于 IO.NET 部署",
"来自模型重定向,尚未加入模型列表": "来自模型重定向,尚未加入模型列表",
"某些配置更改可能需要几分钟才能生效。": "某些配置更改可能需要几分钟才能生效。",
"查看": "查看",
"查看关联部署": "查看关联部署",
"查看图片": "查看图片",
"查看密钥": "查看密钥",
"查看当前可用的所有模型": "查看当前可用的所有模型",
"查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "查看所有可用的AI模型供应商,包括众多知名供应商的模型。",
"查看日志": "查看日志",
"查看渠道密钥": "查看渠道密钥",
"查看详情": "查看详情",
"查询": "查询",
"标签": "标签",
"标签不能为空!": "标签不能为空!",
......@@ -1183,8 +1398,12 @@
"标签聚合": "标签聚合",
"标签聚合模式": "标签聚合模式",
"标识颜色": "标识颜色",
"核采样,控制词汇选择的多样性": "核采样,控制词汇选择的多样性",
"根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含",
"格式化": "格式化",
"格式正确": "格式正确",
"格式示例:": "格式示例:",
"格式错误": "格式错误",
"检查更新": "检查更新",
"检测到 FluentRead(流畅阅读)": "检测到 FluentRead(流畅阅读)",
"检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。",
......@@ -1209,13 +1428,18 @@
"模型关键字": "模型关键字",
"模型列表已复制到剪贴板": "模型列表已复制到剪贴板",
"模型列表已更新": "模型列表已更新",
"模型列表已追加更新": "模型列表已追加更新",
"模型创建成功!": "模型创建成功!",
"模型删除失败": "模型删除失败",
"模型删除失败: {{error}}": "模型删除失败: {{error}}",
"模型删除成功": "模型删除成功",
"模型名称": "模型名称",
"模型名称已存在": "模型名称已存在",
"模型固定价格": "模型固定价格",
"模型图标": "模型图标",
"模型定价,需要登录访问": "模型定价,需要登录访问",
"模型广场": "模型广场",
"模型拉取失败: {{error}}": "模型拉取失败: {{error}}",
"模型支持的接口端点信息": "模型支持的接口端点信息",
"模型数据分析": "模型数据分析",
"模型映射必须是合法的 JSON 格式!": "模型映射必须是合法的 JSON 格式!",
......@@ -1234,6 +1458,10 @@
"模型调用次数占比": "模型调用次数占比",
"模型调用次数排行": "模型调用次数排行",
"模型选择和映射设置": "模型选择和映射设置",
"模型部署": "模型部署",
"模型部署服务未启用": "模型部署服务未启用",
"模型部署管理": "模型部署管理",
"模型部署设置": "模型部署设置",
"模型配置": "模型配置",
"模型重定向": "模型重定向",
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:",
......@@ -1243,10 +1471,13 @@
"次": "次",
"欢迎使用,请完成以下设置以开始使用系统": "欢迎使用,请完成以下设置以开始使用系统",
"欧元": "欧元",
"正在加载可用部署位置...": "正在加载可用部署位置...",
"正在处理大内容...": "正在处理大内容...",
"正在提交": "正在提交",
"正在构造请求体预览...": "正在构造请求体预览...",
"正在检查 io.net 连接...": "正在检查 io.net 连接...",
"正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)",
"正在跟随最新日志": "正在跟随最新日志",
"正在跳转 GitHub...": "正在跳转 GitHub...",
"正在跳转...": "正在跳转...",
"此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理",
......@@ -1255,6 +1486,7 @@
"此操作不可撤销,将永久删除已自动禁用的密钥": "此操作不可撤销,将永久删除已自动禁用的密钥",
"此操作不可撤销,将永久删除该密钥": "此操作不可撤销,将永久删除该密钥",
"此操作不可逆,所有数据将被永久删除": "此操作不可逆,所有数据将被永久删除",
"此操作具有风险,请确认要继续执行": "此操作具有风险,请确认要继续执行",
"此操作将启用用户账户": "此操作将启用用户账户",
"此操作将提升用户的权限级别": "此操作将提升用户的权限级别",
"此操作将禁用用户账户": "此操作将禁用用户账户",
......@@ -1262,6 +1494,7 @@
"此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。",
"此操作将降低用户的权限级别": "此操作将降低用户的权限级别",
"此支付方式最低充值金额为": "此支付方式最低充值金额为",
"此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。": "此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。",
"此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。",
"此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除",
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改",
......@@ -1271,10 +1504,12 @@
"此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数",
"此项可选,用于覆盖请求头参数": "此项可选,用于覆盖请求头参数",
"此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/",
"每容器GPU数": "每容器GPU数",
"每隔多少分钟测试一次所有通道": "每隔多少分钟测试一次所有通道",
"永不过期": "永不过期",
"永久删除您的两步验证设置": "永久删除您的两步验证设置",
"永久删除所有备用码(包括未使用的)": "永久删除所有备用码(包括未使用的)",
"没有匹配的日志条目": "没有匹配的日志条目",
"没有可用令牌用于填充": "没有可用令牌用于填充",
"没有可用模型": "没有可用模型",
"没有找到匹配的模型": "没有找到匹配的模型",
......@@ -1290,16 +1525,22 @@
"注销": "注销",
"注销成功!": "注销成功!",
"流": "流",
"流式响应完成": "流式响应完成",
"流式输出": "流式输出",
"流量端口": "流量端口",
"浅色": "浅色",
"浅色模式": "浅色模式",
"测活": "测活",
"测试": "测试",
"测试中": "测试中",
"测试中...": "测试中...",
"测试单个渠道操作项目组": "测试单个渠道操作项目组",
"测试失败": "测试失败",
"测试失败:": "测试失败:",
"测试所有渠道的最长响应时间": "测试所有渠道的最长响应时间",
"测试所有通道": "测试所有通道",
"测试模式": "测试模式",
"测试连接": "测试连接",
"测速": "测速",
"消息优先级": "消息优先级",
"消息优先级,范围0-10,默认为5": "消息优先级,范围0-10,默认为5",
......@@ -1321,15 +1562,20 @@
"添加公告": "添加公告",
"添加分类": "添加分类",
"添加后提交": "添加后提交",
"添加启动参数": "添加启动参数",
"添加启动命令": "添加启动命令",
"添加密钥环境变量": "添加密钥环境变量",
"添加成功": "添加成功",
"添加模型": "添加模型",
"添加模型区域": "添加模型区域",
"添加渠道": "添加渠道",
"添加环境变量": "添加环境变量",
"添加用户": "添加用户",
"添加聊天配置": "添加聊天配置",
"添加键值对": "添加键值对",
"添加问答": "添加问答",
"添加额度": "添加额度",
"清空": "清空",
"清空重定向": "清空重定向",
"清除历史日志": "清除历史日志",
"清除失效兑换码": "清除失效兑换码",
......@@ -1358,8 +1604,11 @@
"源地址": "源地址",
"演示站点": "演示站点",
"演示站点模式": "演示站点模式",
"点击 + 按钮添加图片URL进行多模态对话": "点击 + 按钮添加图片URL进行多模态对话",
"点击\"确认延长\"后将立即扣除费用并延长容器运行时间": "点击\"确认延长\"后将立即扣除费用并延长容器运行时间",
"点击上传文件或拖拽文件到这里": "点击上传文件或拖拽文件到这里",
"点击下方按钮通过 Telegram 完成绑定": "点击下方按钮通过 Telegram 完成绑定",
"点击复制ID": "点击复制ID",
"点击复制模型名称": "点击复制模型名称",
"点击查看差异": "点击查看差异",
"点击此处": "点击此处",
......@@ -1370,6 +1619,7 @@
"状态码复写": "状态码复写",
"状态筛选": "状态筛选",
"状态页面Slug": "状态页面Slug",
"环境变量": "环境变量",
"生成令牌": "生成令牌",
"生成数量": "生成数量",
"生成数量必须大于0": "生成数量必须大于0",
......@@ -1432,6 +1682,10 @@
"相当于删除用户,此修改将不可逆": "相当于删除用户,此修改将不可逆",
"矛盾": "矛盾",
"知识库 ID": "知识库 ID",
"硬件": "硬件",
"硬件与性能": "硬件与性能",
"硬件类型": "硬件类型",
"硬件配置": "硬件配置",
"确定": "确定",
"确定?": "确定?",
"确定删除此组?": "确定删除此组?",
......@@ -1459,6 +1713,7 @@
"确定要删除此密钥吗?": "确定要删除此密钥吗?",
"确定要删除此问答吗?": "确定要删除此问答吗?",
"确定要删除这条消息吗?": "确定要删除这条消息吗?",
"确定要删除选中的": "确定要删除选中的",
"确定要启用所有密钥吗?": "确定要启用所有密钥吗?",
"确定要启用此用户吗?": "确定要启用此用户吗?",
"确定要提升此用户吗?": "确定要提升此用户吗?",
......@@ -1472,9 +1727,13 @@
"确认": "确认",
"确认冲突项修改": "确认冲突项修改",
"确认删除": "确认删除",
"确认删除模型": "确认删除模型",
"确认取消密码登录": "确认取消密码登录",
"确认密码": "确认密码",
"确认导入配置": "确认导入配置",
"确认延长": "确认延长",
"确认延长容器时长": "确认延长容器时长",
"确认操作": "确认操作",
"确认新密码": "确认新密码",
"确认清除历史日志": "确认清除历史日志",
"确认禁用": "确认禁用",
......@@ -1488,6 +1747,8 @@
"示例": "示例",
"示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。",
"视频": "视频",
"视频Remix": "视频 Remix",
"视频无法在当前浏览器中播放,这可能是由于:": "视频无法在当前浏览器中播放,这可能是由于:",
"禁用": "禁用",
"禁用 store 透传": "禁用 store 透传",
"禁用2FA失败": "禁用2FA失败",
......@@ -1501,12 +1762,15 @@
"禁用时间": "禁用时间",
"私有IP访问详细说明": "⚠️ 安全警告:启用此选项将允许访问内网资源(本地主机、私有网络)。仅在需要访问内部服务且了解安全风险的情况下启用。",
"私有部署地址": "私有部署地址",
"私有镜像仓库的密码": "私有镜像仓库的密码",
"私有镜像仓库的用户名": "私有镜像仓库的用户名",
"秒": "秒",
"移除 functionResponse.id 字段": "移除 functionResponse.id 字段",
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目",
"窗口处理": "窗口处理",
"窗口等待": "窗口等待",
"站点额度展示类型及汇率": "站点额度展示类型及汇率",
"端口号必须在1-65535之间": "端口号必须在1-65535之间",
"端口配置详细说明": "限制外部请求只能访问指定端口。支持单个端口(80, 443)或端口范围(8000-8999)。空列表允许所有端口。默认包含常用Web端口。",
"端点": "端点",
"端点映射": "端点映射",
......@@ -1518,6 +1782,7 @@
"等待获取邮箱信息...": "等待获取邮箱信息...",
"筛选": "筛选",
"管理": "管理",
"管理 Ollama 模型的拉取和删除": "管理 Ollama 模型的拉取和删除",
"管理你的 LinuxDO OAuth App": "管理你的 LinuxDO OAuth App",
"管理员": "管理员",
"管理员区域": "管理员区域",
......@@ -1532,6 +1797,7 @@
"管理员账号已经初始化过,请继续设置其他参数": "管理员账号已经初始化过,请继续设置其他参数",
"管理模型、标签、端点等预填组": "管理模型、标签、端点等预填组",
"类型": "类型",
"粘贴图片失败": "粘贴图片失败",
"精确": "精确",
"系统": "系统",
"系统令牌已复制到剪切板": "系统令牌已复制到剪切板",
......@@ -1546,6 +1812,7 @@
"系统名称": "系统名称",
"系统名称已更新": "系统名称已更新",
"系统名称更新失败": "系统名称更新失败",
"系统已为该部署准备 Ollama 镜像与随机 API Key": "系统已为该部署准备 Ollama 镜像与随机 API Key",
"系统提示覆盖": "系统提示覆盖",
"系统提示词": "系统提示词",
"系统提示词拼接": "系统提示词拼接",
......@@ -1563,6 +1830,8 @@
"组名": "组名",
"组织": "组织",
"组织,不填则为默认组织": "组织,不填则为默认组织",
"终止中": "终止中",
"终止请求中": "终止请求中",
"绑定": "绑定",
"绑定 Telegram": "绑定 Telegram",
"绑定信息": "绑定信息",
......@@ -1617,6 +1886,8 @@
"缺省 MaxTokens": "缺省 MaxTokens",
"网站地址": "网站地址",
"网站域名标识": "网站域名标识",
"网络连接失败,请检查网络设置或稍后重试": "网络连接失败,请检查网络设置或稍后重试",
"网络配置": "网络配置",
"网络错误": "网络错误",
"置信度": "置信度",
"美元": "美元",
......@@ -1631,6 +1902,8 @@
"联系我们": "联系我们",
"腾讯混元": "腾讯混元",
"自动分组auto,从第一个开始选择": "自动分组auto,从第一个开始选择",
"自动刷新": "自动刷新",
"自动刷新中": "自动刷新中",
"自动检测": "自动检测",
"自动模式": "自动模式",
"自动测试所有通道间隔时间": "自动测试所有通道间隔时间",
......@@ -1641,28 +1914,41 @@
"自定义充值数量选项不是合法的 JSON 数组": "自定义充值数量选项不是合法的 JSON 数组",
"自定义变焦-提交": "自定义变焦-提交",
"自定义模型名称": "自定义模型名称",
"自定义模式下不可用": "自定义模式下不可用",
"自定义请求体模式": "自定义请求体模式",
"自定义货币": "自定义货币",
"自定义货币符号": "自定义货币符号",
"自定义镜像": "自定义镜像",
"自用模式": "自用模式",
"自适应列表": "自适应列表",
"节省": "节省",
"花费": "花费",
"花费时间": "花费时间",
"若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置",
"获取 io.net API Key": "获取 io.net API Key",
"获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确",
"获取 OIDC 配置成功!": "获取 OIDC 配置成功!",
"获取 Ollama 版本失败": "获取 Ollama 版本失败",
"获取2FA状态失败": "获取2FA状态失败",
"获取初始化状态失败": "获取初始化状态失败",
"获取可用资源失败: ": "获取可用资源失败: ",
"获取启用模型失败": "获取启用模型失败",
"获取启用模型失败:": "获取启用模型失败:",
"获取容器信息失败": "获取容器信息失败",
"获取容器列表失败": "获取容器列表失败",
"获取容器详情失败": "获取容器详情失败",
"获取密钥": "获取密钥",
"获取密钥失败": "获取密钥失败",
"获取密钥状态失败": "获取密钥状态失败",
"获取日志失败": "获取日志失败",
"获取未配置模型失败": "获取未配置模型失败",
"获取模型列表": "获取模型列表",
"获取模型列表失败": "获取模型列表失败",
"获取渠道失败:": "获取渠道失败:",
"获取硬件类型失败: ": "获取硬件类型失败: ",
"获取组列表失败": "获取组列表失败",
"获取详情失败": "获取详情失败",
"获取部署列表失败": "获取部署列表失败",
"获取金额失败": "获取金额失败",
"获取验证码": "获取验证码",
"补全": "补全",
......@@ -1681,14 +1967,21 @@
"角色": "角色",
"解析响应数据时发生错误": "解析响应数据时发生错误",
"解析密钥文件失败: {{msg}}": "解析密钥文件失败: {{msg}}",
"解析错误": "解析错误",
"解绑 Passkey": "解绑 Passkey",
"解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?",
"计价币种": "计价币种",
"计算中": "计算中",
"计算成本": "计算成本",
"计算费用中...": "计算费用中...",
"计费开始": "计费开始",
"计费模式": "计费模式",
"计费类型": "计费类型",
"计费过程": "计费过程",
"订单号": "订单号",
"讯飞星火": "讯飞星火",
"记录请求与错误日志IP": "记录请求与错误日志IP",
"设备": "设备",
"设备类型偏好": "设备类型偏好",
"设置 Logo": "设置 Logo",
"设置2FA失败": "设置2FA失败",
......@@ -1718,6 +2011,9 @@
"设置首页内容": "设置首页内容",
"设置默认地区和特定模型的专用地区": "设置默认地区和特定模型的专用地区",
"设计与开发由": "设计与开发由",
"访问 io.net 控制台的 API Keys 页面": "访问 io.net 控制台的 API Keys 页面",
"访问容器": "访问容器",
"访问模型部署功能需要先启用 io.net 部署服务": "访问模型部署功能需要先启用 io.net 部署服务",
"访问限制": "访问限制",
"该供应商提供多种AI模型,适用于不同的应用场景。": "该供应商提供多种AI模型,适用于不同的应用场景。",
"该分类下没有可用模型": "该分类下没有可用模型",
......@@ -1725,6 +2021,8 @@
"该数据可能不可信,请谨慎使用": "该数据可能不可信,请谨慎使用",
"该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置",
"该模型存在固定价格与倍率计费方式冲突,请确认选择": "该模型存在固定价格与倍率计费方式冲突,请确认选择",
"该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。": "该渠道已开启请求透传,参数覆写、模型重定向等 NewAPI 内置功能将失效,非最佳实践。",
"该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。": "该渠道已开启请求透传:参数覆写、模型重定向、渠道适配等 NewAPI 内置功能将失效,非最佳实践;如因此产生问题,请勿提交 issue 反馈。",
"详情": "详情",
"语音输入": "语音输入",
"语音输出": "语音输出",
......@@ -1734,10 +2032,15 @@
"请上传密钥文件": "请上传密钥文件",
"请上传密钥文件!": "请上传密钥文件!",
"请为渠道命名": "请为渠道命名",
"请使用 Project 为 io.cloud 的密钥": "请使用 Project 为 io.cloud 的密钥",
"请先在设置中启用图片功能": "请先在设置中启用图片功能",
"请先填写 API Key": "请先填写 API Key",
"请先填写 Ollama API 地址": "请先填写 Ollama API 地址",
"请先填写服务器地址": "请先填写服务器地址",
"请先输入密钥": "请先输入密钥",
"请先选择同步渠道": "请先选择同步渠道",
"请先选择模型!": "请先选择模型!",
"请先选择硬件类型": "请先选择硬件类型",
"请先选择要删除的令牌!": "请先选择要删除的令牌!",
"请先选择要删除的通道!": "请先选择要删除的通道!",
"请先选择要设置标签的渠道!": "请先选择要设置标签的渠道!",
......@@ -1753,9 +2056,12 @@
"请填写渠道名称和渠道密钥!": "请填写渠道名称和渠道密钥!",
"请填写部署地区": "请填写部署地区",
"请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。",
"请尝试其他搜索关键词": "请尝试其他搜索关键词",
"请检查渠道配置或刷新重试": "请检查渠道配置或刷新重试",
"请检查表单填写是否正确": "请检查表单填写是否正确",
"请检查输入": "请检查输入",
"请求体 JSON": "请求体 JSON",
"请求参数无效": "请求参数无效",
"请求发生错误": "请求发生错误",
"请求发生错误: ": "请求发生错误: ",
"请求后端接口失败:": "请求后端接口失败:",
......@@ -1786,6 +2092,8 @@
"请输入 API Key,一行一个,格式:APIKey|Region": "请输入 API Key,一行一个,格式:APIKey|Region",
"请输入 API Key,格式:APIKey|Region": "请输入 API Key,格式:APIKey|Region",
"请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com",
"请输入 io.net API Key": "请输入 io.net API Key",
"请输入 io.net API Key(敏感信息不显示)": "请输入 io.net API Key(敏感信息不显示)",
"请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}",
"请输入 OIDC 的 Well-Known URL": "请输入 OIDC 的 Well-Known URL",
"请输入6位验证码或8位备用码": "请输入6位验证码或8位备用码",
......@@ -1813,6 +2121,7 @@
"请输入分类名称": "请输入分类名称",
"请输入分类名称,如:OpenAI、Claude等": "请输入分类名称,如:OpenAI、Claude等",
"请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com",
"请输入副本数量": "请输入副本数量",
"请输入原密码": "请输入原密码",
"请输入原密码!": "请输入原密码!",
"请输入名称": "请输入名称",
......@@ -1824,11 +2133,13 @@
"请输入完整的 JSON 格式密钥内容": "请输入完整的 JSON 格式密钥内容",
"请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions",
"请输入完整的URL链接": "请输入完整的URL链接",
"请输入容器名称": "请输入容器名称",
"请输入密码": "请输入密码",
"请输入密钥": "请输入密钥",
"请输入密钥,一行一个": "请输入密钥,一行一个",
"请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region": "请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region",
"请输入密钥!": "请输入密钥!",
"请输入延长时长": "请输入延长时长",
"请输入您的密码": "请输入您的密码",
"请输入您的用户名以确认删除": "请输入您的用户名以确认删除",
"请输入您的用户名或邮箱地址": "请输入您的用户名或邮箱地址",
......@@ -1844,12 +2155,16 @@
"请输入新的密码,最短 8 位": "请输入新的密码,最短 8 位",
"请输入新的显示名称": "请输入新的显示名称",
"请输入新的用户名": "请输入新的用户名",
"请输入新的部署名称": "请输入新的部署名称",
"请输入显示名称": "请输入显示名称",
"请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。",
"请输入有效的数字": "请输入有效的数字",
"请输入有效的镜像地址": "请输入有效的镜像地址",
"请输入标签名称": "请输入标签名称",
"请输入模型倍率": "请输入模型倍率",
"请输入模型倍率和补全倍率": "请输入模型倍率和补全倍率",
"请输入模型名称": "请输入模型名称",
"请输入模型名称,例如: llama3.2, qwen2.5:7b": "请输入模型名称,例如: llama3.2, qwen2.5:7b",
"请输入模型名称,如:gpt-4": "请输入模型名称,如:gpt-4",
"请输入模型描述": "请输入模型描述",
"请输入消息内容...": "请输入消息内容...",
......@@ -1866,14 +2181,19 @@
"请输入组织org-xxx": "请输入组织org-xxx",
"请输入聊天应用名称": "请输入聊天应用名称",
"请输入补全倍率": "请输入补全倍率",
"请输入要延长的小时数": "请输入要延长的小时数",
"请输入要设置的标签名称": "请输入要设置的标签名称",
"请输入认证器验证码": "请输入认证器验证码",
"请输入认证器验证码或备用码": "请输入认证器验证码或备用码",
"请输入说明": "请输入说明",
"请输入运行时长": "请输入运行时长",
"请输入邮箱!": "请输入邮箱!",
"请输入邮箱地址": "请输入邮箱地址",
"请输入邮箱验证码!": "请输入邮箱验证码!",
"请输入部署名称": "请输入部署名称",
"请输入部署名称以完成二次确认": "请输入部署名称以完成二次确认",
"请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}",
"请输入镜像地址": "请输入镜像地址",
"请输入问题标题": "请输入问题标题",
"请输入预警阈值": "请输入预警阈值",
"请输入预警额度": "请输入预警额度",
......@@ -1899,7 +2219,9 @@
"请选择模型。": "请选择模型。",
"请选择消息优先级": "请选择消息优先级",
"请选择渠道类型": "请选择渠道类型",
"请选择硬件类型": "请选择硬件类型",
"请选择组类型": "请选择组类型",
"请选择至少一个部署位置": "请选择至少一个部署位置",
"请选择该令牌支持的模型,留空支持所有模型": "请选择该令牌支持的模型,留空支持所有模型",
"请选择该渠道所支持的模型": "请选择该渠道所支持的模型",
"请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改",
......@@ -1926,11 +2248,16 @@
"货币": "货币",
"货币单位": "货币单位",
"购买兑换码": "购买兑换码",
"费用信息": "费用信息",
"费用预估": "费用预估",
"资源消耗": "资源消耗",
"起始时间": "起始时间",
"超级管理员": "超级管理员",
"超级管理员未设置充值链接!": "超级管理员未设置充值链接!",
"跟随日志": "跟随日志",
"跟随系统主题设置": "跟随系统主题设置",
"跨分组": "跨分组",
"跨分组重试": "跨分组重试",
"跳转": "跳转",
"轮询": "轮询",
"轮询模式": "轮询模式",
......@@ -1975,6 +2302,10 @@
"过期时间快捷设置": "过期时间快捷设置",
"过期时间格式错误!": "过期时间格式错误!",
"运营设置": "运营设置",
"运行中": "运行中",
"运行命令 (Command)": "运行命令 (Command)",
"运行时长": "运行时长",
"运行时长(小时)": "运行时长(小时)",
"返回修改": "返回修改",
"返回登录": "返回登录",
"这是重复键中的最后一个,其值将被使用": "这是重复键中的最后一个,其值将被使用",
......@@ -1983,6 +2314,7 @@
"进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用",
"连接保活设置": "连接保活设置",
"连接已断开": "连接已断开",
"连接测试中...": "连接测试中...",
"追加到现有密钥": "追加到现有密钥",
"追加模式:将新密钥添加到现有密钥列表末尾": "追加模式:将新密钥添加到现有密钥列表末尾",
"追加模式:新密钥将添加到现有密钥列表的末尾": "追加模式:新密钥将添加到现有密钥列表的末尾",
......@@ -1996,6 +2328,7 @@
"选择同步来源": "选择同步来源",
"选择同步渠道": "选择同步渠道",
"选择同步语言": "选择同步语言",
"选择容器": "选择容器",
"选择成功": "选择成功",
"选择支付方式": "选择支付方式",
"选择支持的认证设备类型": "选择支持的认证设备类型",
......@@ -2005,12 +2338,15 @@
"选择模型供应商": "选择模型供应商",
"选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。",
"选择模型开始对话": "选择模型开始对话",
"选择状态": "选择状态",
"选择硬件类型": "选择硬件类型",
"选择端点类型": "选择端点类型",
"选择系统运行模式": "选择系统运行模式",
"选择组类型": "请选择组类型",
"选择要覆盖的冲突项": "选择要覆盖的冲突项",
"选择语言": "选择语言",
"选择过期时间(可选,留空为永久)": "选择过期时间(可选,留空为永久)",
"选择部署位置(可多选)": "选择部署位置(可多选)",
"透传请求体": "透传请求体",
"通义千问": "通义千问",
"通用设置": "通用设置",
......@@ -2051,7 +2387,21 @@
"部分保存失败": "部分保存失败",
"部分保存失败,请重试": "部分保存失败,请重试",
"部分渠道测试失败:": "部分渠道测试失败:",
"部署 ID": "部署 ID",
"部署ID": "部署ID",
"部署中": "部署中",
"部署位置": "部署位置",
"部署位置加载中...": "部署位置加载中...",
"部署删除成功": "部署删除成功",
"部署名称": "部署名称",
"部署名称不匹配,请检查后重新输入": "部署名称不匹配,请检查后重新输入",
"部署名称只能包含字母、数字、横线、下划线和中文": "部署名称只能包含字母、数字、横线、下划线和中文",
"部署名称更新成功": "部署名称更新成功",
"部署启动成功": "部署启动成功",
"部署地区": "部署地区",
"部署请求中": "部署请求中",
"部署配置": "部署配置",
"部署重启成功": "部署重启成功",
"配置": "配置",
"配置 Discord OAuth": "配置 Discord OAuth",
"配置 GitHub OAuth App": "配置 GitHub OAuth App",
......@@ -2063,14 +2413,20 @@
"配置 Turnstile": "配置 Turnstile",
"配置 WeChat Server": "配置 WeChat Server",
"配置和消息已全部重置": "配置和消息已全部重置",
"配置完成后刷新页面即可使用模型部署功能": "配置完成后刷新页面即可使用模型部署功能",
"配置导入成功": "配置导入成功",
"配置已导出到下载文件夹": "配置已导出到下载文件夹",
"配置已重置,对话消息已保留": "配置已重置,对话消息已保留",
"配置文件同步": "配置文件同步",
"配置更新确认": "配置更新确认",
"配置有效的 io.net API Key": "配置有效的 io.net API Key",
"配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全",
"配置模型部署服务提供商的API密钥和启用状态": "配置模型部署服务提供商的API密钥和启用状态",
"配置登录注册": "配置登录注册",
"配置说明": "配置说明",
"配置邮箱域名白名单": "配置邮箱域名白名单",
"重启部署失败": "重启部署失败",
"重命名部署": "重命名部署",
"重复提交": "重复提交",
"重复的键名": "重复的键名",
"重复的键名,此值将被后面的同名键覆盖": "重复的键名,此值将被后面的同名键覆盖",
......@@ -2089,9 +2445,13 @@
"重置选项": "重置选项",
"重置邮件发送成功,请检查邮箱!": "重置邮件发送成功,请检查邮箱!",
"重置配置": "重置配置",
"重要提醒": "重要提醒",
"重试": "重试",
"重试连接": "重试连接",
"钱包管理": "钱包管理",
"链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1",
"销毁容器": "销毁容器",
"销毁容器失败": "销毁容器失败",
"错误": "错误",
"键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1",
"键为原状态码,值为要复写的状态码,仅影响本地判断": "键为原状态码,值为要复写的状态码,仅影响本地判断",
......@@ -2099,6 +2459,12 @@
"键为端点类型,值为路径和方法对象": "键为端点类型,值为路径和方法对象",
"键为请求中的模型名称,值为要替换的模型名称": "键为请求中的模型名称,值为要替换的模型名称",
"键名": "键名",
"镜像仓库密码": "镜像仓库密码",
"镜像仓库用户名": "镜像仓库用户名",
"镜像仓库配置": "镜像仓库配置",
"镜像地址": "镜像地址",
"镜像选择": "镜像选择",
"镜像配置": "镜像配置",
"问题标题": "问题标题",
"队列中": "队列中",
"降低您账户的安全性": "降低您账户的安全性",
......@@ -2113,10 +2479,12 @@
"隐藏调试": "隐藏调试",
"随机": "随机",
"随机模式": "随机模式",
"随机种子 (留空为随机)": "随机种子 (留空为随机)",
"零一万物": "零一万物",
"需要安全验证": "需要安全验证",
"需要添加的额度(支持负数)": "需要添加的额度(支持负数)",
"需要登录访问": "需要登录访问",
"需要配置的项目": "需要配置的项目",
"需要重新完整设置才能再次启用": "需要重新完整设置才能再次启用",
"非必要,不建议启用模型限制": "非必要,不建议启用模型限制",
"非流": "非流",
......@@ -2133,11 +2501,15 @@
"项目": "项目",
"项目内容": "项目内容",
"项目操作按钮组": "项目操作按钮组",
"预估总费用": "预估总费用",
"预估费用仅供参考,实际费用可能略有差异": "预估费用仅供参考,实际费用可能略有差异",
"预填组管理": "预填组管理",
"预览失败": "预览失败",
"预览更新": "预览更新",
"预览请求体": "预览请求体",
"预计结束": "预计结束",
"预警阈值必须为正数": "预警阈值必须为正数",
"频率惩罚,减少重复词汇的出现": "频率惩罚,减少重复词汇的出现",
"频率限制的周期(分钟)": "频率限制的周期(分钟)",
"颜色": "颜色",
"额度": "额度",
......@@ -2160,58 +2532,19 @@
"验证身份": "验证身份",
"验证配置错误": "验证配置错误",
"高级设置": "高级设置",
"高级配置": "高级配置",
"黑名单": "黑名单",
"默认": "默认",
"默认 API 版本": "默认 API 版本",
"默认 Responses API 版本,为空则使用上方版本": "默认 Responses API 版本,为空则使用上方版本",
"默认使用系统名称": "默认使用系统名称",
"默认助手消息": "你好!有什么我可以帮助你的吗?",
"默认区域": "默认区域",
"默认区域,如: us-central1": "默认区域,如: us-central1",
"默认折叠侧边栏": "默认折叠侧边栏",
"默认测试模型": "默认测试模型",
"请先在设置中启用图片功能": "请先在设置中启用图片功能",
"图片已添加": "图片已添加",
"无法添加图片": "无法添加图片",
"粘贴图片失败": "粘贴图片失败",
"支持 Ctrl+V 粘贴图片": "支持 Ctrl+V 粘贴图片",
"已复制全部数据": "已复制全部数据",
"流式响应完成": "流式响应完成",
"图片地址": "图片地址",
"已在自定义模式中忽略": "已在自定义模式中忽略",
"停用": "停用",
"图片功能在自定义请求体模式下不可用": "图片功能在自定义请求体模式下不可用",
"启用后可添加图片URL进行多模态对话": "启用后可添加图片URL进行多模态对话",
"点击 + 按钮添加图片URL进行多模态对话": "点击 + 按钮添加图片URL进行多模态对话",
"已添加": "已添加",
"张图片": "张图片",
"自定义模式下不可用": "自定义模式下不可用",
"控制输出的随机性和创造性": "控制输出的随机性和创造性",
"核采样,控制词汇选择的多样性": "核采样,控制词汇选择的多样性",
"频率惩罚,减少重复词汇的出现": "频率惩罚,减少重复词汇的出现",
"存在惩罚,鼓励讨论新话题": "存在惩罚,鼓励讨论新话题",
"流式输出": "流式输出",
"暂无SSE响应数据": "暂无SSE响应数据",
"SSE数据流": "SSE数据流",
"解析错误": "解析错误",
"有 Reasoning": "有 Reasoning",
"全部收起": "全部收起",
"全部展开": "全部展开",
"SSE 事件": "SSE 事件",
"JSON格式错误": "JSON格式错误",
"自定义请求体模式": "自定义请求体模式",
"启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。": "启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。",
"请求体 JSON": "请求体 JSON",
"格式正确": "格式正确",
"格式错误": "格式错误",
"格式化": "格式化",
"请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。": "请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。",
"默认用户消息": "你好",
"默认助手消息": "你好!有什么我可以帮助你的吗?",
"可选,用于复现结果": "可选,用于复现结果",
"随机种子 (留空为随机)": "随机种子 (留空为随机)",
"跨分组重试": "跨分组重试",
"跨分组": "跨分组",
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道",
"默认补全倍率": "默认补全倍率",
"每日签到": "每日签到",
"今日已签到,累计签到": "今日已签到,累计签到",
"天": "天",
......
......@@ -29,7 +29,6 @@ const ModelDeploymentPage = () => {
connectionLoading,
connectionOk,
connectionError,
apiKey,
testConnection,
} = useModelDeploymentSettings();
......@@ -40,7 +39,7 @@ const ModelDeploymentPage = () => {
connectionLoading={connectionLoading}
connectionOk={connectionOk}
connectionError={connectionError}
onRetry={() => testConnection(apiKey)}
onRetry={() => testConnection()}
>
<div className='mt-[60px] px-2'>
<DeploymentsTable />
......
......@@ -48,10 +48,6 @@ export default function SettingModelDeployment(props) {
const testApiKey = async () => {
const apiKey = inputs['model_deployment.ionet.api_key'];
if (!apiKey || apiKey.trim() === '') {
showError(t('请先填写 API Key'));
return;
}
const getLocalizedMessage = (message) => {
switch (message) {
......@@ -69,10 +65,8 @@ export default function SettingModelDeployment(props) {
setTesting(true);
try {
const response = await API.post(
'/api/deployments/test-connection',
{
api_key: apiKey.trim(),
},
'/api/deployments/settings/test-connection',
apiKey && apiKey.trim() !== '' ? { api_key: apiKey.trim() } : {},
{
skipErrorHandler: true,
},
......@@ -108,12 +102,6 @@ export default function SettingModelDeployment(props) {
};
function onSubmit() {
// 前置校验:如果启用了 io.net 但没有填写 API Key
if (inputs['model_deployment.ionet.enabled'] &&
(!inputs['model_deployment.ionet.api_key'] || inputs['model_deployment.ionet.api_key'].trim() === '')) {
return showError(t('启用 io.net 部署时必须填写 API Key'));
}
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
......@@ -229,7 +217,7 @@ export default function SettingModelDeployment(props) {
<Form.Input
label={t('API Key')}
field={'model_deployment.ionet.api_key'}
placeholder={t('请输入 io.net API Key')}
placeholder={t('请输入 io.net API Key(敏感信息不显示)')}
onChange={(value) =>
setInputs({
...inputs,
......@@ -248,9 +236,7 @@ export default function SettingModelDeployment(props) {
onClick={testApiKey}
loading={testing}
disabled={
!inputs['model_deployment.ionet.enabled'] ||
!inputs['model_deployment.ionet.api_key'] ||
inputs['model_deployment.ionet.api_key'].trim() === ''
!inputs['model_deployment.ionet.enabled']
}
style={{
height: '32px',
......
......@@ -32,7 +32,7 @@ import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status';
import { UserContext } from '../../../context/User';
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
import { useSidebar } from '../../../hooks/common/useSidebar';
import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
import { Settings } from 'lucide-react';
const { Text } = Typography;
......@@ -198,9 +198,25 @@ export default function SettingsSidebarModulesUser() {
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
try {
const adminConf = JSON.parse(
statusState.status.SidebarModulesAdmin,
);
const mergedAdminConf = mergeAdminConfig(adminConf);
setAdminConfig(mergedAdminConf);
console.log('加载管理员边栏配置:', mergedAdminConf);
} catch (error) {
const mergedAdminConf = mergeAdminConfig(null);
setAdminConfig(mergedAdminConf);
console.log(
'加载管理员边栏配置失败,使用默认配置:',
mergedAdminConf,
);
}
} else {
const mergedAdminConf = mergeAdminConfig(null);
setAdminConfig(mergedAdminConf);
console.log('管理员边栏配置缺失,使用默认配置:', mergedAdminConf);
}
// 获取用户个人配置
......@@ -324,6 +340,11 @@ export default function SettingsSidebarModulesUser() {
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{
key: 'deployment',
title: t('模型部署'),
description: t('模型部署管理'),
},
{
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
......@@ -389,7 +410,7 @@ export default function SettingsSidebarModulesUser() {
</Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
checked={sidebarModulesUser[section.key]?.enabled !== false}
onChange={handleSectionChange(section.key)}
size='default'
/>
......@@ -401,7 +422,9 @@ export default function SettingsSidebarModulesUser() {
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
sidebarModulesUser[section.key]?.enabled !== false
? ''
: 'opacity-50'
}`}
bodyStyle={{ padding: '16px' }}
hoverable
......@@ -417,10 +440,15 @@ export default function SettingsSidebarModulesUser() {
</div>
<div className='ml-4'>
<Switch
checked={sidebarModulesUser[section.key]?.[module.key]}
checked={
sidebarModulesUser[section.key]?.[module.key] !==
false
}
onChange={handleModuleChange(section.key, module.key)}
size='default'
disabled={!sidebarModulesUser[section.key]?.enabled}
disabled={
sidebarModulesUser[section.key]?.enabled === false
}
/>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment