Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
78b0a82c
authored
Jun 09, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/alpha' into alpha
parents
158c18fe
e3b1d691
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
150 additions
and
129 deletions
+150
-129
controller/misc.go
+1
-22
controller/option.go
+1
-95
setting/api_info.go
+125
-0
web/src/i18n/locales/en.json
+3
-1
web/src/pages/Detail/index.js
+20
-11
No files found.
controller/misc.go
View file @
78b0a82c
...
...
@@ -74,33 +74,12 @@ func GetStatus(c *gin.Context) {
"oidc_client_id"
:
system_setting
.
GetOIDCSettings
()
.
ClientId
,
"oidc_authorization_endpoint"
:
system_setting
.
GetOIDCSettings
()
.
AuthorizationEndpoint
,
"setup"
:
constant
.
Setup
,
"api_info"
:
g
etApiInfo
(),
"api_info"
:
setting
.
G
etApiInfo
(),
},
})
return
}
func
getApiInfo
()
[]
map
[
string
]
interface
{}
{
// 从OptionMap中获取API信息,如果不存在则返回空数组
common
.
OptionMapRWMutex
.
RLock
()
apiInfoStr
,
exists
:=
common
.
OptionMap
[
"ApiInfo"
]
common
.
OptionMapRWMutex
.
RUnlock
()
if
!
exists
||
apiInfoStr
==
""
{
// 如果没有配置,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
// 解析存储的API信息
var
apiInfo
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfo
);
err
!=
nil
{
// 如果解析失败,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
return
apiInfo
}
func
GetNotice
(
c
*
gin
.
Context
)
{
common
.
OptionMapRWMutex
.
RLock
()
defer
common
.
OptionMapRWMutex
.
RUnlock
()
...
...
controller/option.go
View file @
78b0a82c
...
...
@@ -2,110 +2,16 @@ package controller
import
(
"encoding/json"
"fmt"
"net/http"
"net/url"
"one-api/common"
"one-api/model"
"one-api/setting"
"one-api/setting/system_setting"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
func
validateApiInfo
(
apiInfoStr
string
)
error
{
if
apiInfoStr
==
""
{
return
nil
// 空字符串是合法的
}
var
apiInfoList
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfoList
);
err
!=
nil
{
return
fmt
.
Errorf
(
"API信息格式错误:%s"
,
err
.
Error
())
}
// 验证数组长度
if
len
(
apiInfoList
)
>
50
{
return
fmt
.
Errorf
(
"API信息数量不能超过50个"
)
}
// 允许的颜色值
validColors
:=
map
[
string
]
bool
{
"blue"
:
true
,
"green"
:
true
,
"cyan"
:
true
,
"purple"
:
true
,
"pink"
:
true
,
"red"
:
true
,
"orange"
:
true
,
"amber"
:
true
,
"yellow"
:
true
,
"lime"
:
true
,
"light-green"
:
true
,
"teal"
:
true
,
"light-blue"
:
true
,
"indigo"
:
true
,
"violet"
:
true
,
"grey"
:
true
,
}
// URL正则表达式
urlRegex
:=
regexp
.
MustCompile
(
`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`
)
for
i
,
apiInfo
:=
range
apiInfoList
{
// 检查必填字段
urlStr
,
ok
:=
apiInfo
[
"url"
]
.
(
string
)
if
!
ok
||
urlStr
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少URL字段"
,
i
+
1
)
}
route
,
ok
:=
apiInfo
[
"route"
]
.
(
string
)
if
!
ok
||
route
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少线路描述字段"
,
i
+
1
)
}
description
,
ok
:=
apiInfo
[
"description"
]
.
(
string
)
if
!
ok
||
description
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少说明字段"
,
i
+
1
)
}
color
,
ok
:=
apiInfo
[
"color"
]
.
(
string
)
if
!
ok
||
color
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少颜色字段"
,
i
+
1
)
}
// 验证URL格式
if
!
urlRegex
.
MatchString
(
urlStr
)
{
return
fmt
.
Errorf
(
"第%d个API信息的URL格式不正确"
,
i
+
1
)
}
// 验证URL可解析性
if
_
,
err
:=
url
.
Parse
(
urlStr
);
err
!=
nil
{
return
fmt
.
Errorf
(
"第%d个API信息的URL无法解析:%s"
,
i
+
1
,
err
.
Error
())
}
// 验证字段长度
if
len
(
urlStr
)
>
500
{
return
fmt
.
Errorf
(
"第%d个API信息的URL长度不能超过500字符"
,
i
+
1
)
}
if
len
(
route
)
>
100
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述长度不能超过100字符"
,
i
+
1
)
}
if
len
(
description
)
>
200
{
return
fmt
.
Errorf
(
"第%d个API信息的说明长度不能超过200字符"
,
i
+
1
)
}
// 验证颜色值
if
!
validColors
[
color
]
{
return
fmt
.
Errorf
(
"第%d个API信息的颜色值不合法"
,
i
+
1
)
}
// 检查并过滤危险字符(防止XSS)
dangerousChars
:=
[]
string
{
"<script"
,
"<iframe"
,
"javascript:"
,
"onload="
,
"onerror="
,
"onclick="
}
for
_
,
dangerous
:=
range
dangerousChars
{
if
strings
.
Contains
(
strings
.
ToLower
(
description
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的说明包含不允许的内容"
,
i
+
1
)
}
if
strings
.
Contains
(
strings
.
ToLower
(
route
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述包含不允许的内容"
,
i
+
1
)
}
}
}
return
nil
}
func
GetOptions
(
c
*
gin
.
Context
)
{
var
options
[]
*
model
.
Option
common
.
OptionMapRWMutex
.
Lock
()
...
...
@@ -214,7 +120,7 @@ func UpdateOption(c *gin.Context) {
return
}
case
"ApiInfo"
:
err
=
v
alidateApiInfo
(
option
.
Value
)
err
=
setting
.
V
alidateApiInfo
(
option
.
Value
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
...
...
setting/api_info.go
0 → 100644
View file @
78b0a82c
package
setting
import
(
"encoding/json"
"fmt"
"net/url"
"one-api/common"
"regexp"
"strings"
)
// ValidateApiInfo 验证API信息格式
func
ValidateApiInfo
(
apiInfoStr
string
)
error
{
if
apiInfoStr
==
""
{
return
nil
// 空字符串是合法的
}
var
apiInfoList
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfoList
);
err
!=
nil
{
return
fmt
.
Errorf
(
"API信息格式错误:%s"
,
err
.
Error
())
}
// 验证数组长度
if
len
(
apiInfoList
)
>
50
{
return
fmt
.
Errorf
(
"API信息数量不能超过50个"
)
}
// 允许的颜色值
validColors
:=
map
[
string
]
bool
{
"blue"
:
true
,
"green"
:
true
,
"cyan"
:
true
,
"purple"
:
true
,
"pink"
:
true
,
"red"
:
true
,
"orange"
:
true
,
"amber"
:
true
,
"yellow"
:
true
,
"lime"
:
true
,
"light-green"
:
true
,
"teal"
:
true
,
"light-blue"
:
true
,
"indigo"
:
true
,
"violet"
:
true
,
"grey"
:
true
,
}
// URL正则表达式
urlRegex
:=
regexp
.
MustCompile
(
`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`
)
for
i
,
apiInfo
:=
range
apiInfoList
{
// 检查必填字段
urlStr
,
ok
:=
apiInfo
[
"url"
]
.
(
string
)
if
!
ok
||
urlStr
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少URL字段"
,
i
+
1
)
}
route
,
ok
:=
apiInfo
[
"route"
]
.
(
string
)
if
!
ok
||
route
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少线路描述字段"
,
i
+
1
)
}
description
,
ok
:=
apiInfo
[
"description"
]
.
(
string
)
if
!
ok
||
description
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少说明字段"
,
i
+
1
)
}
color
,
ok
:=
apiInfo
[
"color"
]
.
(
string
)
if
!
ok
||
color
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少颜色字段"
,
i
+
1
)
}
// 验证URL格式
if
!
urlRegex
.
MatchString
(
urlStr
)
{
return
fmt
.
Errorf
(
"第%d个API信息的URL格式不正确"
,
i
+
1
)
}
// 验证URL可解析性
if
_
,
err
:=
url
.
Parse
(
urlStr
);
err
!=
nil
{
return
fmt
.
Errorf
(
"第%d个API信息的URL无法解析:%s"
,
i
+
1
,
err
.
Error
())
}
// 验证字段长度
if
len
(
urlStr
)
>
500
{
return
fmt
.
Errorf
(
"第%d个API信息的URL长度不能超过500字符"
,
i
+
1
)
}
if
len
(
route
)
>
100
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述长度不能超过100字符"
,
i
+
1
)
}
if
len
(
description
)
>
200
{
return
fmt
.
Errorf
(
"第%d个API信息的说明长度不能超过200字符"
,
i
+
1
)
}
// 验证颜色值
if
!
validColors
[
color
]
{
return
fmt
.
Errorf
(
"第%d个API信息的颜色值不合法"
,
i
+
1
)
}
// 检查并过滤危险字符(防止XSS)
dangerousChars
:=
[]
string
{
"<script"
,
"<iframe"
,
"javascript:"
,
"onload="
,
"onerror="
,
"onclick="
}
for
_
,
dangerous
:=
range
dangerousChars
{
if
strings
.
Contains
(
strings
.
ToLower
(
description
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的说明包含不允许的内容"
,
i
+
1
)
}
if
strings
.
Contains
(
strings
.
ToLower
(
route
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述包含不允许的内容"
,
i
+
1
)
}
}
}
return
nil
}
// GetApiInfo 获取API信息列表
func
GetApiInfo
()
[]
map
[
string
]
interface
{}
{
// 从OptionMap中获取API信息,如果不存在则返回空数组
common
.
OptionMapRWMutex
.
RLock
()
apiInfoStr
,
exists
:=
common
.
OptionMap
[
"ApiInfo"
]
common
.
OptionMapRWMutex
.
RUnlock
()
if
!
exists
||
apiInfoStr
==
""
{
// 如果没有配置,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
// 解析存储的API信息
var
apiInfo
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfo
);
err
!=
nil
{
// 如果解析失败,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
return
apiInfo
}
\ No newline at end of file
web/src/i18n/locales/en.json
View file @
78b0a82c
...
...
@@ -1585,5 +1585,6 @@
"请输入说明"
:
"Please enter the description"
,
"如:香港线路"
:
"e.g. Hong Kong line"
,
"请联系管理员在系统设置中配置API信息"
:
"Please contact the administrator to configure API information in the system settings."
,
"确定要删除此API信息吗?"
:
"Are you sure you want to delete this API information?"
"确定要删除此API信息吗?"
:
"Are you sure you want to delete this API information?"
,
"测速"
:
"Speed Test"
}
\ No newline at end of file
web/src/pages/Detail/index.js
View file @
78b0a82c
...
...
@@ -13,6 +13,7 @@ import {
Tabs
,
TabPane
,
Empty
,
Tag
}
from
'@douyinfe/semi-ui'
;
import
{
IconRefresh
,
...
...
@@ -25,7 +26,7 @@ import {
IconPulse
,
IconStopwatchStroked
,
IconTypograph
,
IconPieChart2Stroked
,
IconPieChart2Stroked
}
from
'@douyinfe/semi-icons'
;
import
{
IllustrationConstruction
,
IllustrationConstructionDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
VChart
}
from
'@visactor/react-vchart'
;
...
...
@@ -495,6 +496,12 @@ const Detail = (props) => {
}
},
[
t
]);
const
handleSpeedTest
=
useCallback
((
apiUrl
)
=>
{
const
encodedUrl
=
encodeURIComponent
(
apiUrl
);
const
speedTestUrl
=
`https://www.tcptest.cn/http/
${
encodedUrl
}
`
;
window
.
open
(
speedTestUrl
,
'_blank'
);
},
[]);
const
handleInputChange
=
useCallback
((
value
,
name
)
=>
{
if
(
name
===
'data_export_default_time'
)
{
setDataExportDefaultTime
(
value
);
...
...
@@ -698,22 +705,17 @@ const Detail = (props) => {
},
[
dataExportDefaultTime
,
getTimeInterval
]);
const
updateChartData
=
useCallback
((
data
)
=>
{
// 处理原始数据
const
processedData
=
processRawData
(
data
);
const
{
totalQuota
,
totalTimes
,
totalTokens
,
uniqueModels
,
timePoints
,
timeQuotaMap
,
timeTokensMap
,
timeCountMap
}
=
processedData
;
// 计算趋势数据
const
trendDataResult
=
calculateTrendData
(
timePoints
,
timeQuotaMap
,
timeTokensMap
,
timeCountMap
);
setTrendData
(
trendDataResult
);
// 生成模型颜色映射
const
newModelColors
=
generateModelColors
(
uniqueModels
);
setModelColors
(
newModelColors
);
// 聚合数据
const
aggregatedData
=
aggregateDataByTimeAndModel
(
data
);
// 生成饼图数据
const
modelTotals
=
new
Map
();
for
(
let
[
_
,
value
]
of
aggregatedData
)
{
updateMapValue
(
modelTotals
,
value
.
model
,
value
.
count
);
...
...
@@ -724,7 +726,6 @@ const Detail = (props) => {
value
:
count
,
})).
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
// 生成线图数据
const
chartTimePoints
=
generateChartTimePoints
(
aggregatedData
,
data
);
let
newLineData
=
[];
...
...
@@ -748,7 +749,6 @@ const Detail = (props) => {
newLineData
.
sort
((
a
,
b
)
=>
a
.
Time
.
localeCompare
(
b
.
Time
));
// 更新图表配置
updateChartSpec
(
setSpecPie
,
newPieData
,
...
...
@@ -765,7 +765,6 @@ const Detail = (props) => {
'barData'
);
// 更新状态
setPieData
(
newPieData
);
setLineData
(
newLineData
);
setConsumeQuota
(
totalQuota
);
...
...
@@ -987,14 +986,24 @@ const Detail = (props) => {
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
<div className="flex-shrink-0 mr-3">
<Avatar
size="extra-
extra-
small"
size="extra-small"
color={api.color}
>
{api.route.substring(0, 2)}
</Avatar>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 mb-1">
<div className="text-sm font-medium text-gray-900 mb-1 !font-bold flex items-center gap-2">
<Tag
prefixIcon={<Gauge size={12} />}
size="small"
color="white"
shape='circle'
onClick={() => handleSpeedTest(api.url)}
className="cursor-pointer hover:opacity-80 text-xs"
>
{t('测速')}
</Tag>
{api.route}
</div>
<div
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment