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
1dcb9a01
authored
Aug 09, 2025
by
RedwindA
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'alpha' into 'feat/support-native-gemini-embedding'
parents
74057324
8e52b42c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
37 changed files
with
750 additions
and
365 deletions
+750
-365
constant/context_key.go
+2
-0
controller/midjourney.go
+30
-0
dto/channel_settings.go
+1
-0
dto/openai_request.go
+2
-0
middleware/distributor.go
+2
-0
model/main.go
+23
-0
model/model_meta.go
+2
-2
model/vendor_meta.go
+2
-2
relay/channel/gemini/adaptor.go
+24
-17
relay/channel/gemini/relay-gemini.go
+11
-9
relay/channel/zhipu_4v/adaptor.go
+1
-2
relay/channel/zhipu_4v/relay-zhipu_v4.go
+0
-59
relay/relay-text.go
+24
-6
service/log_info_generate.go
+6
-0
web/src/components/common/ui/JSONEditor.js
+0
-0
web/src/components/layout/HeaderBar.js
+1
-1
web/src/components/layout/SiderBar.js
+6
-6
web/src/components/table/channels/modals/EditChannelModal.jsx
+17
-0
web/src/components/table/model-pricing/layout/PricingSidebar.jsx
+2
-1
web/src/components/table/model-pricing/view/card/PricingCardView.jsx
+4
-0
web/src/components/table/models/modals/EditModelModal.jsx
+3
-0
web/src/components/table/models/modals/EditPrefillGroupModal.jsx
+3
-0
web/src/components/table/task-logs/TaskLogsColumnDefs.js
+8
-1
web/src/components/table/task-logs/TaskLogsTable.jsx
+3
-0
web/src/components/table/task-logs/index.jsx
+8
-1
web/src/components/table/task-logs/modals/ContentModal.jsx
+6
-1
web/src/components/table/tokens/TokensColumnDefs.js
+114
-96
web/src/components/table/tokens/index.jsx
+241
-4
web/src/components/table/usage-logs/UsageLogsColumnDefs.js
+13
-4
web/src/components/table/users/UsersColumnDefs.js
+69
-56
web/src/constants/channel.constants.js
+1
-1
web/src/helpers/api.js
+8
-6
web/src/helpers/render.js
+83
-87
web/src/hooks/task-logs/useTaskLogsData.js
+16
-0
web/src/hooks/tokens/useTokensData.js
+5
-1
web/src/i18n/locales/en.json
+8
-1
web/src/index.css
+1
-1
No files found.
constant/context_key.go
View file @
1dcb9a01
...
@@ -40,4 +40,6 @@ const (
...
@@ -40,4 +40,6 @@ const (
ContextKeyUserGroup
ContextKey
=
"user_group"
ContextKeyUserGroup
ContextKey
=
"user_group"
ContextKeyUsingGroup
ContextKey
=
"group"
ContextKeyUsingGroup
ContextKey
=
"group"
ContextKeyUserName
ContextKey
=
"username"
ContextKeyUserName
ContextKey
=
"username"
ContextKeySystemPromptOverride
ContextKey
=
"system_prompt_override"
)
)
controller/midjourney.go
View file @
1dcb9a01
...
@@ -145,6 +145,22 @@ func UpdateMidjourneyTaskBulk() {
...
@@ -145,6 +145,22 @@ func UpdateMidjourneyTaskBulk() {
buttonStr
,
_
:=
json
.
Marshal
(
responseItem
.
Buttons
)
buttonStr
,
_
:=
json
.
Marshal
(
responseItem
.
Buttons
)
task
.
Buttons
=
string
(
buttonStr
)
task
.
Buttons
=
string
(
buttonStr
)
}
}
// 映射 VideoUrl
task
.
VideoUrl
=
responseItem
.
VideoUrl
// 映射 VideoUrls - 将数组序列化为 JSON 字符串
if
responseItem
.
VideoUrls
!=
nil
&&
len
(
responseItem
.
VideoUrls
)
>
0
{
videoUrlsStr
,
err
:=
json
.
Marshal
(
responseItem
.
VideoUrls
)
if
err
!=
nil
{
common
.
LogError
(
ctx
,
fmt
.
Sprintf
(
"序列化 VideoUrls 失败: %v"
,
err
))
task
.
VideoUrls
=
"[]"
// 失败时设置为空数组
}
else
{
task
.
VideoUrls
=
string
(
videoUrlsStr
)
}
}
else
{
task
.
VideoUrls
=
""
// 空值时清空字段
}
shouldReturnQuota
:=
false
shouldReturnQuota
:=
false
if
(
task
.
Progress
!=
"100%"
&&
responseItem
.
FailReason
!=
""
)
||
(
task
.
Progress
==
"100%"
&&
task
.
Status
==
"FAILURE"
)
{
if
(
task
.
Progress
!=
"100%"
&&
responseItem
.
FailReason
!=
""
)
||
(
task
.
Progress
==
"100%"
&&
task
.
Status
==
"FAILURE"
)
{
common
.
LogInfo
(
ctx
,
task
.
MjId
+
" 构建失败,"
+
task
.
FailReason
)
common
.
LogInfo
(
ctx
,
task
.
MjId
+
" 构建失败,"
+
task
.
FailReason
)
...
@@ -208,6 +224,20 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
...
@@ -208,6 +224,20 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto)
if
oldTask
.
Progress
!=
"100%"
&&
newTask
.
FailReason
!=
""
{
if
oldTask
.
Progress
!=
"100%"
&&
newTask
.
FailReason
!=
""
{
return
true
return
true
}
}
// 检查 VideoUrl 是否需要更新
if
oldTask
.
VideoUrl
!=
newTask
.
VideoUrl
{
return
true
}
// 检查 VideoUrls 是否需要更新
if
newTask
.
VideoUrls
!=
nil
&&
len
(
newTask
.
VideoUrls
)
>
0
{
newVideoUrlsStr
,
_
:=
json
.
Marshal
(
newTask
.
VideoUrls
)
if
oldTask
.
VideoUrls
!=
string
(
newVideoUrlsStr
)
{
return
true
}
}
else
if
oldTask
.
VideoUrls
!=
""
{
// 如果新数据没有 VideoUrls 但旧数据有,需要更新(清空)
return
true
}
return
false
return
false
}
}
...
...
dto/channel_settings.go
View file @
1dcb9a01
...
@@ -6,4 +6,5 @@ type ChannelSettings struct {
...
@@ -6,4 +6,5 @@ type ChannelSettings struct {
Proxy
string
`json:"proxy"`
Proxy
string
`json:"proxy"`
PassThroughBodyEnabled
bool
`json:"pass_through_body_enabled,omitempty"`
PassThroughBodyEnabled
bool
`json:"pass_through_body_enabled,omitempty"`
SystemPrompt
string
`json:"system_prompt,omitempty"`
SystemPrompt
string
`json:"system_prompt,omitempty"`
SystemPromptOverride
bool
`json:"system_prompt_override,omitempty"`
}
}
dto/openai_request.go
View file @
1dcb9a01
...
@@ -78,6 +78,8 @@ func (r *GeneralOpenAIRequest) GetSystemRoleName() string {
...
@@ -78,6 +78,8 @@ func (r *GeneralOpenAIRequest) GetSystemRoleName() string {
if
!
strings
.
HasPrefix
(
r
.
Model
,
"o1-mini"
)
&&
!
strings
.
HasPrefix
(
r
.
Model
,
"o1-preview"
)
{
if
!
strings
.
HasPrefix
(
r
.
Model
,
"o1-mini"
)
&&
!
strings
.
HasPrefix
(
r
.
Model
,
"o1-preview"
)
{
return
"developer"
return
"developer"
}
}
}
else
if
strings
.
HasPrefix
(
r
.
Model
,
"gpt-5"
)
{
return
"developer"
}
}
return
"system"
return
"system"
}
}
...
...
middleware/distributor.go
View file @
1dcb9a01
...
@@ -267,6 +267,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
...
@@ -267,6 +267,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
common
.
SetContextKey
(
c
,
constant
.
ContextKeyChannelKey
,
key
)
common
.
SetContextKey
(
c
,
constant
.
ContextKeyChannelKey
,
key
)
common
.
SetContextKey
(
c
,
constant
.
ContextKeyChannelBaseUrl
,
channel
.
GetBaseURL
())
common
.
SetContextKey
(
c
,
constant
.
ContextKeyChannelBaseUrl
,
channel
.
GetBaseURL
())
common
.
SetContextKey
(
c
,
constant
.
ContextKeySystemPromptOverride
,
false
)
// TODO: api_version统一
// TODO: api_version统一
switch
channel
.
Type
{
switch
channel
.
Type
{
case
constant
.
ChannelTypeAzure
:
case
constant
.
ChannelTypeAzure
:
...
...
model/main.go
View file @
1dcb9a01
...
@@ -64,6 +64,22 @@ var DB *gorm.DB
...
@@ -64,6 +64,22 @@ var DB *gorm.DB
var
LOG_DB
*
gorm
.
DB
var
LOG_DB
*
gorm
.
DB
// dropIndexIfExists drops a MySQL index only if it exists to avoid noisy 1091 errors
func
dropIndexIfExists
(
tableName
string
,
indexName
string
)
{
if
!
common
.
UsingMySQL
{
return
}
var
count
int64
// Check index existence via information_schema
err
:=
DB
.
Raw
(
"SELECT COUNT(1) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?"
,
tableName
,
indexName
,
)
.
Scan
(
&
count
)
.
Error
if
err
==
nil
&&
count
>
0
{
_
=
DB
.
Exec
(
"ALTER TABLE "
+
tableName
+
" DROP INDEX "
+
indexName
+
";"
)
.
Error
}
}
func
createRootAccountIfNeed
()
error
{
func
createRootAccountIfNeed
()
error
{
var
user
User
var
user
User
//if user.Status != common.UserStatusEnabled {
//if user.Status != common.UserStatusEnabled {
...
@@ -235,6 +251,9 @@ func InitLogDB() (err error) {
...
@@ -235,6 +251,9 @@ func InitLogDB() (err error) {
}
}
func
migrateDB
()
error
{
func
migrateDB
()
error
{
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
dropIndexIfExists
(
"models"
,
"uk_model_name"
)
dropIndexIfExists
(
"vendors"
,
"uk_vendor_name"
)
if
!
common
.
UsingPostgreSQL
{
if
!
common
.
UsingPostgreSQL
{
return
migrateDBFast
()
return
migrateDBFast
()
}
}
...
@@ -264,6 +283,10 @@ func migrateDB() error {
...
@@ -264,6 +283,10 @@ func migrateDB() error {
}
}
func
migrateDBFast
()
error
{
func
migrateDBFast
()
error
{
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
dropIndexIfExists
(
"models"
,
"uk_model_name"
)
dropIndexIfExists
(
"vendors"
,
"uk_vendor_name"
)
var
wg
sync
.
WaitGroup
var
wg
sync
.
WaitGroup
migrations
:=
[]
struct
{
migrations
:=
[]
struct
{
...
...
model/model_meta.go
View file @
1dcb9a01
...
@@ -36,7 +36,7 @@ type BoundChannel struct {
...
@@ -36,7 +36,7 @@ type BoundChannel struct {
type
Model
struct
{
type
Model
struct
{
Id
int
`json:"id"`
Id
int
`json:"id"`
ModelName
string
`json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,
where:deleted_at IS NULL
"`
ModelName
string
`json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,
priority:1
"`
Description
string
`json:"description,omitempty" gorm:"type:text"`
Description
string
`json:"description,omitempty" gorm:"type:text"`
Tags
string
`json:"tags,omitempty" gorm:"type:varchar(255)"`
Tags
string
`json:"tags,omitempty" gorm:"type:varchar(255)"`
VendorID
int
`json:"vendor_id,omitempty" gorm:"index"`
VendorID
int
`json:"vendor_id,omitempty" gorm:"index"`
...
@@ -44,7 +44,7 @@ type Model struct {
...
@@ -44,7 +44,7 @@ type Model struct {
Status
int
`json:"status" gorm:"default:1"`
Status
int
`json:"status" gorm:"default:1"`
CreatedTime
int64
`json:"created_time" gorm:"bigint"`
CreatedTime
int64
`json:"created_time" gorm:"bigint"`
UpdatedTime
int64
`json:"updated_time" gorm:"bigint"`
UpdatedTime
int64
`json:"updated_time" gorm:"bigint"`
DeletedAt
gorm
.
DeletedAt
`json:"-" gorm:"index"`
DeletedAt
gorm
.
DeletedAt
`json:"-" gorm:"index
;uniqueIndex:uk_model_name,priority:2
"`
BoundChannels
[]
BoundChannel
`json:"bound_channels,omitempty" gorm:"-"`
BoundChannels
[]
BoundChannel
`json:"bound_channels,omitempty" gorm:"-"`
EnableGroups
[]
string
`json:"enable_groups,omitempty" gorm:"-"`
EnableGroups
[]
string
`json:"enable_groups,omitempty" gorm:"-"`
...
...
model/vendor_meta.go
View file @
1dcb9a01
...
@@ -14,13 +14,13 @@ import (
...
@@ -14,13 +14,13 @@ import (
type
Vendor
struct
{
type
Vendor
struct
{
Id
int
`json:"id"`
Id
int
`json:"id"`
Name
string
`json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,
where:deleted_at IS NULL
"`
Name
string
`json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,
priority:1
"`
Description
string
`json:"description,omitempty" gorm:"type:text"`
Description
string
`json:"description,omitempty" gorm:"type:text"`
Icon
string
`json:"icon,omitempty" gorm:"type:varchar(128)"`
Icon
string
`json:"icon,omitempty" gorm:"type:varchar(128)"`
Status
int
`json:"status" gorm:"default:1"`
Status
int
`json:"status" gorm:"default:1"`
CreatedTime
int64
`json:"created_time" gorm:"bigint"`
CreatedTime
int64
`json:"created_time" gorm:"bigint"`
UpdatedTime
int64
`json:"updated_time" gorm:"bigint"`
UpdatedTime
int64
`json:"updated_time" gorm:"bigint"`
DeletedAt
gorm
.
DeletedAt
`json:"-" gorm:"index"`
DeletedAt
gorm
.
DeletedAt
`json:"-" gorm:"index
;uniqueIndex:uk_vendor_name,priority:2
"`
}
}
// Insert 创建新的供应商记录
// Insert 创建新的供应商记录
...
...
relay/channel/gemini/adaptor.go
View file @
1dcb9a01
...
@@ -119,6 +119,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
...
@@ -119,6 +119,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
action
=
"batchEmbedContents"
action
=
"batchEmbedContents"
}
}
return
fmt
.
Sprintf
(
"%s/%s/models/%s:%s"
,
info
.
BaseUrl
,
version
,
info
.
UpstreamModelName
,
action
),
nil
return
fmt
.
Sprintf
(
"%s/%s/models/%s:%s"
,
info
.
BaseUrl
,
version
,
info
.
UpstreamModelName
,
action
),
nil
return
fmt
.
Sprintf
(
"%s/%s/models/%s:batchEmbedContents"
,
info
.
BaseUrl
,
version
,
info
.
UpstreamModelName
),
nil
}
}
action
:=
"generateContent"
action
:=
"generateContent"
...
@@ -163,29 +164,35 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
...
@@ -163,29 +164,35 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
if
len
(
inputs
)
==
0
{
if
len
(
inputs
)
==
0
{
return
nil
,
errors
.
New
(
"input is empty"
)
return
nil
,
errors
.
New
(
"input is empty"
)
}
}
// process all inputs
// only process the first input
geminiRequests
:=
make
([]
map
[
string
]
interface
{},
0
,
len
(
inputs
))
geminiRequest
:=
dto
.
GeminiEmbeddingRequest
{
for
_
,
input
:=
range
inputs
{
Content
:
dto
.
GeminiChatContent
{
geminiRequest
:=
map
[
string
]
interface
{}{
Parts
:
[]
dto
.
GeminiPart
{
"model"
:
fmt
.
Sprintf
(
"models/%s"
,
info
.
UpstreamModelName
),
{
"content"
:
dto
.
GeminiChatContent
{
Text
:
inputs
[
0
],
Parts
:
[]
dto
.
GeminiPart
{
{
Text
:
input
,
},
},
},
},
},
},
}
}
// set specific parameters for different models
// set specific parameters for different models
// https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent
// https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent
switch
info
.
UpstreamModelName
{
switch
info
.
UpstreamModelName
{
case
"text-embedding-004"
:
case
"text-embedding-004"
,
"gemini-embedding-exp-03-07"
,
"gemini-embedding-001"
:
// except embedding-001 supports setting `OutputDimensionality`
// Only newer models introduced after 2024 support OutputDimensionality
if
request
.
Dimensions
>
0
{
if
request
.
Dimensions
>
0
{
geminiRequest
.
OutputDimensionality
=
request
.
Dimensions
geminiRequest
[
"outputDimensionality"
]
=
request
.
Dimensions
}
}
}
geminiRequests
=
append
(
geminiRequests
,
geminiRequest
)
}
}
return
geminiRequest
,
nil
return
map
[
string
]
interface
{}{
"requests"
:
geminiRequests
,
},
nil
}
}
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
...
...
relay/channel/gemini/relay-gemini.go
View file @
1dcb9a01
...
@@ -1071,7 +1071,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
...
@@ -1071,7 +1071,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
return
nil
,
types
.
NewOpenAIError
(
readErr
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
return
nil
,
types
.
NewOpenAIError
(
readErr
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
}
var
geminiResponse
dto
.
GeminiEmbeddingResponse
var
geminiResponse
dto
.
Gemini
Batch
EmbeddingResponse
if
jsonErr
:=
common
.
Unmarshal
(
responseBody
,
&
geminiResponse
);
jsonErr
!=
nil
{
if
jsonErr
:=
common
.
Unmarshal
(
responseBody
,
&
geminiResponse
);
jsonErr
!=
nil
{
return
nil
,
types
.
NewOpenAIError
(
jsonErr
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
return
nil
,
types
.
NewOpenAIError
(
jsonErr
,
types
.
ErrorCodeBadResponseBody
,
http
.
StatusInternalServerError
)
}
}
...
@@ -1079,14 +1079,16 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
...
@@ -1079,14 +1079,16 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h
// convert to openai format response
// convert to openai format response
openAIResponse
:=
dto
.
OpenAIEmbeddingResponse
{
openAIResponse
:=
dto
.
OpenAIEmbeddingResponse
{
Object
:
"list"
,
Object
:
"list"
,
Data
:
[]
dto
.
OpenAIEmbeddingResponseItem
{
Data
:
make
([]
dto
.
OpenAIEmbeddingResponseItem
,
0
,
len
(
geminiResponse
.
Embeddings
)),
{
Model
:
info
.
UpstreamModelName
,
Object
:
"embedding"
,
}
Embedding
:
geminiResponse
.
Embedding
.
Values
,
Index
:
0
,
for
i
,
embedding
:=
range
geminiResponse
.
Embeddings
{
},
openAIResponse
.
Data
=
append
(
openAIResponse
.
Data
,
dto
.
OpenAIEmbeddingResponseItem
{
},
Object
:
"embedding"
,
Model
:
info
.
UpstreamModelName
,
Embedding
:
embedding
.
Values
,
Index
:
i
,
})
}
}
// calculate usage
// calculate usage
...
...
relay/channel/zhipu_4v/adaptor.go
View file @
1dcb9a01
...
@@ -54,8 +54,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
...
@@ -54,8 +54,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
func
(
a
*
Adaptor
)
SetupRequestHeader
(
c
*
gin
.
Context
,
req
*
http
.
Header
,
info
*
relaycommon
.
RelayInfo
)
error
{
func
(
a
*
Adaptor
)
SetupRequestHeader
(
c
*
gin
.
Context
,
req
*
http
.
Header
,
info
*
relaycommon
.
RelayInfo
)
error
{
channel
.
SetupApiRequestHeader
(
info
,
c
,
req
)
channel
.
SetupApiRequestHeader
(
info
,
c
,
req
)
token
:=
getZhipuToken
(
info
.
ApiKey
)
req
.
Set
(
"Authorization"
,
"Bearer "
+
info
.
ApiKey
)
req
.
Set
(
"Authorization"
,
token
)
return
nil
return
nil
}
}
...
...
relay/channel/zhipu_4v/relay-zhipu_v4.go
View file @
1dcb9a01
package
zhipu_4v
package
zhipu_4v
import
(
import
(
"github.com/golang-jwt/jwt"
"one-api/common"
"one-api/dto"
"one-api/dto"
"strings"
"strings"
"sync"
"time"
)
)
// https://open.bigmodel.cn/doc/api#chatglm_std
// chatglm_std, chatglm_lite
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/invoke
// https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke
var
zhipuTokens
sync
.
Map
var
expSeconds
int64
=
24
*
3600
func
getZhipuToken
(
apikey
string
)
string
{
data
,
ok
:=
zhipuTokens
.
Load
(
apikey
)
if
ok
{
tokenData
:=
data
.
(
tokenData
)
if
time
.
Now
()
.
Before
(
tokenData
.
ExpiryTime
)
{
return
tokenData
.
Token
}
}
split
:=
strings
.
Split
(
apikey
,
"."
)
if
len
(
split
)
!=
2
{
common
.
SysError
(
"invalid zhipu key: "
+
apikey
)
return
""
}
id
:=
split
[
0
]
secret
:=
split
[
1
]
expMillis
:=
time
.
Now
()
.
Add
(
time
.
Duration
(
expSeconds
)
*
time
.
Second
)
.
UnixNano
()
/
1e6
expiryTime
:=
time
.
Now
()
.
Add
(
time
.
Duration
(
expSeconds
)
*
time
.
Second
)
timestamp
:=
time
.
Now
()
.
UnixNano
()
/
1e6
payload
:=
jwt
.
MapClaims
{
"api_key"
:
id
,
"exp"
:
expMillis
,
"timestamp"
:
timestamp
,
}
token
:=
jwt
.
NewWithClaims
(
jwt
.
SigningMethodHS256
,
payload
)
token
.
Header
[
"alg"
]
=
"HS256"
token
.
Header
[
"sign_type"
]
=
"SIGN"
tokenString
,
err
:=
token
.
SignedString
([]
byte
(
secret
))
if
err
!=
nil
{
return
""
}
zhipuTokens
.
Store
(
apikey
,
tokenData
{
Token
:
tokenString
,
ExpiryTime
:
expiryTime
,
})
return
tokenString
}
func
requestOpenAI2Zhipu
(
request
dto
.
GeneralOpenAIRequest
)
*
dto
.
GeneralOpenAIRequest
{
func
requestOpenAI2Zhipu
(
request
dto
.
GeneralOpenAIRequest
)
*
dto
.
GeneralOpenAIRequest
{
messages
:=
make
([]
dto
.
Message
,
0
,
len
(
request
.
Messages
))
messages
:=
make
([]
dto
.
Message
,
0
,
len
(
request
.
Messages
))
for
_
,
message
:=
range
request
.
Messages
{
for
_
,
message
:=
range
request
.
Messages
{
...
...
relay/relay-text.go
View file @
1dcb9a01
...
@@ -140,10 +140,10 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
...
@@ -140,10 +140,10 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
returnPreConsumedQuota
(
c
,
relayInfo
,
userQuota
,
preConsumedQuota
)
returnPreConsumedQuota
(
c
,
relayInfo
,
userQuota
,
preConsumedQuota
)
}
}
}()
}()
includeUsage
:=
fals
e
includeUsage
:=
tru
e
// 判断用户是否需要返回使用情况
// 判断用户是否需要返回使用情况
if
textRequest
.
StreamOptions
!=
nil
&&
textRequest
.
StreamOptions
.
IncludeUsage
{
if
textRequest
.
StreamOptions
!=
nil
{
includeUsage
=
t
ru
e
includeUsage
=
t
extRequest
.
StreamOptions
.
IncludeUsag
e
}
}
// 如果不支持StreamOptions,将StreamOptions设置为nil
// 如果不支持StreamOptions,将StreamOptions设置为nil
...
@@ -158,9 +158,7 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
...
@@ -158,9 +158,7 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
}
}
}
}
if
includeUsage
{
relayInfo
.
ShouldIncludeUsage
=
includeUsage
relayInfo
.
ShouldIncludeUsage
=
true
}
adaptor
:=
GetAdaptor
(
relayInfo
.
ApiType
)
adaptor
:=
GetAdaptor
(
relayInfo
.
ApiType
)
if
adaptor
==
nil
{
if
adaptor
==
nil
{
...
@@ -201,6 +199,26 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
...
@@ -201,6 +199,26 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
Content
:
relayInfo
.
ChannelSetting
.
SystemPrompt
,
Content
:
relayInfo
.
ChannelSetting
.
SystemPrompt
,
}
}
request
.
Messages
=
append
([]
dto
.
Message
{
systemMessage
},
request
.
Messages
...
)
request
.
Messages
=
append
([]
dto
.
Message
{
systemMessage
},
request
.
Messages
...
)
}
else
if
relayInfo
.
ChannelSetting
.
SystemPromptOverride
{
common
.
SetContextKey
(
c
,
constant
.
ContextKeySystemPromptOverride
,
true
)
// 如果有系统提示,且允许覆盖,则拼接到前面
for
i
,
message
:=
range
request
.
Messages
{
if
message
.
Role
==
request
.
GetSystemRoleName
()
{
if
message
.
IsStringContent
()
{
request
.
Messages
[
i
]
.
SetStringContent
(
relayInfo
.
ChannelSetting
.
SystemPrompt
+
"
\n
"
+
message
.
StringContent
())
}
else
{
contents
:=
message
.
ParseContent
()
contents
=
append
([]
dto
.
MediaContent
{
{
Type
:
dto
.
ContentTypeText
,
Text
:
relayInfo
.
ChannelSetting
.
SystemPrompt
,
},
},
contents
...
)
request
.
Messages
[
i
]
.
Content
=
contents
}
break
}
}
}
}
}
}
...
...
service/log_info_generate.go
View file @
1dcb9a01
...
@@ -28,6 +28,12 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
...
@@ -28,6 +28,12 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
other
[
"is_model_mapped"
]
=
true
other
[
"is_model_mapped"
]
=
true
other
[
"upstream_model_name"
]
=
relayInfo
.
UpstreamModelName
other
[
"upstream_model_name"
]
=
relayInfo
.
UpstreamModelName
}
}
isSystemPromptOverwritten
:=
common
.
GetContextKeyBool
(
ctx
,
constant
.
ContextKeySystemPromptOverride
)
if
isSystemPromptOverwritten
{
other
[
"is_system_prompt_overwritten"
]
=
true
}
adminInfo
:=
make
(
map
[
string
]
interface
{})
adminInfo
:=
make
(
map
[
string
]
interface
{})
adminInfo
[
"use_channel"
]
=
ctx
.
GetStringSlice
(
"use_channel"
)
adminInfo
[
"use_channel"
]
=
ctx
.
GetStringSlice
(
"use_channel"
)
isMultiKey
:=
common
.
GetContextKeyBool
(
ctx
,
constant
.
ContextKeyChannelIsMultiKey
)
isMultiKey
:=
common
.
GetContextKeyBool
(
ctx
,
constant
.
ContextKeyChannelIsMultiKey
)
...
...
web/src/components/common/ui/JSONEditor.js
View file @
1dcb9a01
This diff is collapsed.
Click to expand it.
web/src/components/layout/HeaderBar.js
View file @
1dcb9a01
...
@@ -458,7 +458,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
...
@@ -458,7 +458,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
};
};
return (
return (
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg"
style={{ borderBottom: '1px solid var(--semi-color-border)' }}
>
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
<NoticeModal
<NoticeModal
visible={noticeVisible}
visible={noticeVisible}
onClose={handleNoticeClose}
onClose={handleNoticeClose}
...
...
web/src/components/layout/SiderBar.js
View file @
1dcb9a01
...
@@ -129,18 +129,18 @@ const SiderBar = ({ onNavigate = () => { } }) => {
...
@@ -129,18 +129,18 @@ const SiderBar = ({ onNavigate = () => { } }) => {
const
adminItems
=
useMemo
(
const
adminItems
=
useMemo
(
()
=>
[
()
=>
[
{
{
text
:
t
(
'模型管理'
),
itemKey
:
'models'
,
to
:
'/console/models'
,
className
:
isAdmin
()
?
''
:
'tableHiddle'
,
},
{
text
:
t
(
'渠道管理'
),
text
:
t
(
'渠道管理'
),
itemKey
:
'channel'
,
itemKey
:
'channel'
,
to
:
'/channel'
,
to
:
'/channel'
,
className
:
isAdmin
()
?
''
:
'tableHiddle'
,
className
:
isAdmin
()
?
''
:
'tableHiddle'
,
},
},
{
{
text
:
t
(
'模型管理'
),
itemKey
:
'models'
,
to
:
'/console/models'
,
className
:
isAdmin
()
?
''
:
'tableHiddle'
,
},
{
text
:
t
(
'兑换码管理'
),
text
:
t
(
'兑换码管理'
),
itemKey
:
'redemption'
,
itemKey
:
'redemption'
,
to
:
'/redemption'
,
to
:
'/redemption'
,
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
1dcb9a01
...
@@ -131,6 +131,7 @@ const EditChannelModal = (props) => {
...
@@ -131,6 +131,7 @@ const EditChannelModal = (props) => {
proxy
:
''
,
proxy
:
''
,
pass_through_body_enabled
:
false
,
pass_through_body_enabled
:
false
,
system_prompt
:
''
,
system_prompt
:
''
,
system_prompt_override
:
false
,
};
};
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
multiToSingle
,
setMultiToSingle
]
=
useState
(
false
);
const
[
multiToSingle
,
setMultiToSingle
]
=
useState
(
false
);
...
@@ -340,12 +341,15 @@ const EditChannelModal = (props) => {
...
@@ -340,12 +341,15 @@ const EditChannelModal = (props) => {
data
.
proxy
=
parsedSettings
.
proxy
||
''
;
data
.
proxy
=
parsedSettings
.
proxy
||
''
;
data
.
pass_through_body_enabled
=
parsedSettings
.
pass_through_body_enabled
||
false
;
data
.
pass_through_body_enabled
=
parsedSettings
.
pass_through_body_enabled
||
false
;
data
.
system_prompt
=
parsedSettings
.
system_prompt
||
''
;
data
.
system_prompt
=
parsedSettings
.
system_prompt
||
''
;
data
.
system_prompt_override
=
parsedSettings
.
system_prompt_override
||
false
;
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'解析渠道设置失败:'
,
error
);
console
.
error
(
'解析渠道设置失败:'
,
error
);
data
.
force_format
=
false
;
data
.
force_format
=
false
;
data
.
thinking_to_content
=
false
;
data
.
thinking_to_content
=
false
;
data
.
proxy
=
''
;
data
.
proxy
=
''
;
data
.
pass_through_body_enabled
=
false
;
data
.
pass_through_body_enabled
=
false
;
data
.
system_prompt
=
''
;
data
.
system_prompt_override
=
false
;
}
}
}
else
{
}
else
{
data
.
force_format
=
false
;
data
.
force_format
=
false
;
...
@@ -353,6 +357,7 @@ const EditChannelModal = (props) => {
...
@@ -353,6 +357,7 @@ const EditChannelModal = (props) => {
data
.
proxy
=
''
;
data
.
proxy
=
''
;
data
.
pass_through_body_enabled
=
false
;
data
.
pass_through_body_enabled
=
false
;
data
.
system_prompt
=
''
;
data
.
system_prompt
=
''
;
data
.
system_prompt_override
=
false
;
}
}
setInputs
(
data
);
setInputs
(
data
);
...
@@ -372,6 +377,7 @@ const EditChannelModal = (props) => {
...
@@ -372,6 +377,7 @@ const EditChannelModal = (props) => {
proxy
:
data
.
proxy
,
proxy
:
data
.
proxy
,
pass_through_body_enabled
:
data
.
pass_through_body_enabled
,
pass_through_body_enabled
:
data
.
pass_through_body_enabled
,
system_prompt
:
data
.
system_prompt
,
system_prompt
:
data
.
system_prompt
,
system_prompt_override
:
data
.
system_prompt_override
||
false
,
});
});
// console.log(data);
// console.log(data);
}
else
{
}
else
{
...
@@ -573,6 +579,7 @@ const EditChannelModal = (props) => {
...
@@ -573,6 +579,7 @@ const EditChannelModal = (props) => {
proxy
:
''
,
proxy
:
''
,
pass_through_body_enabled
:
false
,
pass_through_body_enabled
:
false
,
system_prompt
:
''
,
system_prompt
:
''
,
system_prompt_override
:
false
,
});
});
// 重置密钥模式状态
// 重置密钥模式状态
setKeyMode
(
'append'
);
setKeyMode
(
'append'
);
...
@@ -721,6 +728,7 @@ const EditChannelModal = (props) => {
...
@@ -721,6 +728,7 @@ const EditChannelModal = (props) => {
proxy
:
localInputs
.
proxy
||
''
,
proxy
:
localInputs
.
proxy
||
''
,
pass_through_body_enabled
:
localInputs
.
pass_through_body_enabled
||
false
,
pass_through_body_enabled
:
localInputs
.
pass_through_body_enabled
||
false
,
system_prompt
:
localInputs
.
system_prompt
||
''
,
system_prompt
:
localInputs
.
system_prompt
||
''
,
system_prompt_override
:
localInputs
.
system_prompt_override
||
false
,
};
};
localInputs
.
setting
=
JSON
.
stringify
(
channelExtraSettings
);
localInputs
.
setting
=
JSON
.
stringify
(
channelExtraSettings
);
...
@@ -730,6 +738,7 @@ const EditChannelModal = (props) => {
...
@@ -730,6 +738,7 @@ const EditChannelModal = (props) => {
delete
localInputs
.
proxy
;
delete
localInputs
.
proxy
;
delete
localInputs
.
pass_through_body_enabled
;
delete
localInputs
.
pass_through_body_enabled
;
delete
localInputs
.
system_prompt
;
delete
localInputs
.
system_prompt
;
delete
localInputs
.
system_prompt_override
;
let
res
;
let
res
;
localInputs
.
auto_ban
=
localInputs
.
auto_ban
?
1
:
0
;
localInputs
.
auto_ban
=
localInputs
.
auto_ban
?
1
:
0
;
...
@@ -1722,6 +1731,14 @@ const EditChannelModal = (props) => {
...
@@ -1722,6 +1731,14 @@ const EditChannelModal = (props) => {
showClear
showClear
extraText=
{
t
(
'用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置'
)
}
extraText=
{
t
(
'用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置'
)
}
/>
/>
<
Form
.
Switch
field=
'system_prompt_override'
label=
{
t
(
'系统提示词拼接'
)
}
checkedText=
{
t
(
'开'
)
}
uncheckedText=
{
t
(
'关'
)
}
onChange=
{
(
value
)
=>
handleChannelSettingsChange
(
'system_prompt_override'
,
value
)
}
extraText=
{
t
(
'如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面'
)
}
/>
</
Card
>
</
Card
>
</
div
>
</
div
>
</
Spin
>
</
Spin
>
...
...
web/src/components/table/model-pricing/layout/PricingSidebar.jsx
View file @
1dcb9a01
...
@@ -40,6 +40,7 @@ const PricingSidebar = ({
...
@@ -40,6 +40,7 @@ const PricingSidebar = ({
setViewMode
,
setViewMode
,
filterGroup
,
filterGroup
,
setFilterGroup
,
setFilterGroup
,
handleGroupClick
,
filterQuotaType
,
filterQuotaType
,
setFilterQuotaType
,
setFilterQuotaType
,
filterEndpointType
,
filterEndpointType
,
...
@@ -126,7 +127,7 @@ const PricingSidebar = ({
...
@@ -126,7 +127,7 @@ const PricingSidebar = ({
<
PricingGroups
<
PricingGroups
filterGroup=
{
filterGroup
}
filterGroup=
{
filterGroup
}
setFilterGroup=
{
setFilterGroup
}
setFilterGroup=
{
handleGroupClick
}
usableGroup=
{
categoryProps
.
usableGroup
}
usableGroup=
{
categoryProps
.
usableGroup
}
groupRatio=
{
categoryProps
.
groupRatio
}
groupRatio=
{
categoryProps
.
groupRatio
}
models=
{
groupCountModels
}
models=
{
groupCountModels
}
...
...
web/src/components/table/model-pricing/view/card/PricingCardView.jsx
View file @
1dcb9a01
...
@@ -25,6 +25,7 @@ import { stringToColor, calculateModelPrice, formatPriceInfo, getLobeHubIcon } f
...
@@ -25,6 +25,7 @@ import { stringToColor, calculateModelPrice, formatPriceInfo, getLobeHubIcon } f
import
PricingCardSkeleton
from
'./PricingCardSkeleton'
;
import
PricingCardSkeleton
from
'./PricingCardSkeleton'
;
import
{
useMinimumLoadingTime
}
from
'../../../../../hooks/common/useMinimumLoadingTime'
;
import
{
useMinimumLoadingTime
}
from
'../../../../../hooks/common/useMinimumLoadingTime'
;
import
{
renderLimitedItems
}
from
'../../../../common/ui/RenderUtils'
;
import
{
renderLimitedItems
}
from
'../../../../common/ui/RenderUtils'
;
import
{
useIsMobile
}
from
'../../../../../hooks/common/useIsMobile'
;
const
CARD_STYLES
=
{
const
CARD_STYLES
=
{
container
:
"w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md"
,
container
:
"w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md"
,
...
@@ -59,6 +60,7 @@ const PricingCardView = ({
...
@@ -59,6 +60,7 @@ const PricingCardView = ({
const
startIndex
=
(
currentPage
-
1
)
*
pageSize
;
const
startIndex
=
(
currentPage
-
1
)
*
pageSize
;
const
paginatedModels
=
filteredModels
.
slice
(
startIndex
,
startIndex
+
pageSize
);
const
paginatedModels
=
filteredModels
.
slice
(
startIndex
,
startIndex
+
pageSize
);
const
getModelKey
=
(
model
)
=>
model
.
key
??
model
.
model_name
??
model
.
id
;
const
getModelKey
=
(
model
)
=>
model
.
key
??
model
.
model_name
??
model
.
id
;
const
isMobile
=
useIsMobile
();
const
handleCheckboxChange
=
(
model
,
checked
)
=>
{
const
handleCheckboxChange
=
(
model
,
checked
)
=>
{
if
(
!
setSelectedRowKeys
)
return
;
if
(
!
setSelectedRowKeys
)
return
;
...
@@ -311,6 +313,8 @@ const PricingCardView = ({
...
@@ -311,6 +313,8 @@ const PricingCardView = ({
total=
{
filteredModels
.
length
}
total=
{
filteredModels
.
length
}
showSizeChanger=
{
true
}
showSizeChanger=
{
true
}
pageSizeOptions=
{
[
10
,
20
,
50
,
100
]
}
pageSizeOptions=
{
[
10
,
20
,
50
,
100
]
}
size=
{
isMobile
?
'small'
:
'default'
}
showQuickJumper=
{
isMobile
}
onPageChange=
{
(
page
)
=>
setCurrentPage
(
page
)
}
onPageChange=
{
(
page
)
=>
setCurrentPage
(
page
)
}
onPageSizeChange=
{
(
size
)
=>
{
onPageSizeChange=
{
(
size
)
=>
{
setPageSize
(
size
);
setPageSize
(
size
);
...
...
web/src/components/table/models/modals/EditModelModal.jsx
View file @
1dcb9a01
...
@@ -42,7 +42,10 @@ const { Text, Title } = Typography;
...
@@ -42,7 +42,10 @@ const { Text, Title } = Typography;
// Example endpoint template for quick fill
// Example endpoint template for quick fill
const
ENDPOINT_TEMPLATE
=
{
const
ENDPOINT_TEMPLATE
=
{
openai
:
{
path
:
'/v1/chat/completions'
,
method
:
'POST'
},
openai
:
{
path
:
'/v1/chat/completions'
,
method
:
'POST'
},
'openai-response'
:
{
path
:
'/v1/responses'
,
method
:
'POST'
},
anthropic
:
{
path
:
'/v1/messages'
,
method
:
'POST'
},
anthropic
:
{
path
:
'/v1/messages'
,
method
:
'POST'
},
gemini
:
{
path
:
'/v1beta/models/{model}:generateContent'
,
method
:
'POST'
},
'jina-rerank'
:
{
path
:
'/rerank'
,
method
:
'POST'
},
'image-generation'
:
{
path
:
'/v1/images/generations'
,
method
:
'POST'
},
'image-generation'
:
{
path
:
'/v1/images/generations'
,
method
:
'POST'
},
};
};
...
...
web/src/components/table/models/modals/EditPrefillGroupModal.jsx
View file @
1dcb9a01
...
@@ -46,7 +46,10 @@ const { Text, Title } = Typography;
...
@@ -46,7 +46,10 @@ const { Text, Title } = Typography;
// Example endpoint template for quick fill
// Example endpoint template for quick fill
const
ENDPOINT_TEMPLATE
=
{
const
ENDPOINT_TEMPLATE
=
{
openai
:
{
path
:
'/v1/chat/completions'
,
method
:
'POST'
},
openai
:
{
path
:
'/v1/chat/completions'
,
method
:
'POST'
},
'openai-response'
:
{
path
:
'/v1/responses'
,
method
:
'POST'
},
anthropic
:
{
path
:
'/v1/messages'
,
method
:
'POST'
},
anthropic
:
{
path
:
'/v1/messages'
,
method
:
'POST'
},
gemini
:
{
path
:
'/v1beta/models/{model}:generateContent'
,
method
:
'POST'
},
'jina-rerank'
:
{
path
:
'/rerank'
,
method
:
'POST'
},
'image-generation'
:
{
path
:
'/v1/images/generations'
,
method
:
'POST'
},
'image-generation'
:
{
path
:
'/v1/images/generations'
,
method
:
'POST'
},
};
};
...
...
web/src/components/table/task-logs/TaskLogsColumnDefs.js
View file @
1dcb9a01
...
@@ -211,6 +211,7 @@ export const getTaskLogsColumns = ({
...
@@ -211,6 +211,7 @@ export const getTaskLogsColumns = ({
copyText
,
copyText
,
openContentModal
,
openContentModal
,
isAdminUser
,
isAdminUser
,
openVideoModal
,
})
=>
{
})
=>
{
return
[
return
[
{
{
...
@@ -342,7 +343,13 @@ export const getTaskLogsColumns = ({
...
@@ -342,7 +343,13 @@ export const getTaskLogsColumns = ({
const
isUrl
=
typeof
text
===
'string'
&&
/^https
?
:
\/\/
/
.
test
(
text
);
const
isUrl
=
typeof
text
===
'string'
&&
/^https
?
:
\/\/
/
.
test
(
text
);
if
(
isSuccess
&&
isVideoTask
&&
isUrl
)
{
if
(
isSuccess
&&
isVideoTask
&&
isUrl
)
{
return
(
return
(
<
a
href
=
{
text
}
target
=
"_blank"
rel
=
"noopener noreferrer"
>
<
a
href
=
"#"
onClick
=
{
e
=>
{
e
.
preventDefault
();
openVideoModal
(
text
);
}}
>
{
t
(
'点击预览视频'
)}
{
t
(
'点击预览视频'
)}
<
/a
>
<
/a
>
);
);
...
...
web/src/components/table/task-logs/TaskLogsTable.jsx
View file @
1dcb9a01
...
@@ -39,6 +39,7 @@ const TaskLogsTable = (taskLogsData) => {
...
@@ -39,6 +39,7 @@ const TaskLogsTable = (taskLogsData) => {
handlePageSizeChange
,
handlePageSizeChange
,
copyText
,
copyText
,
openContentModal
,
openContentModal
,
openVideoModal
,
isAdminUser
,
isAdminUser
,
t
,
t
,
COLUMN_KEYS
,
COLUMN_KEYS
,
...
@@ -51,6 +52,7 @@ const TaskLogsTable = (taskLogsData) => {
...
@@ -51,6 +52,7 @@ const TaskLogsTable = (taskLogsData) => {
COLUMN_KEYS
,
COLUMN_KEYS
,
copyText
,
copyText
,
openContentModal
,
openContentModal
,
openVideoModal
,
isAdminUser
,
isAdminUser
,
});
});
},
[
},
[
...
@@ -58,6 +60,7 @@ const TaskLogsTable = (taskLogsData) => {
...
@@ -58,6 +60,7 @@ const TaskLogsTable = (taskLogsData) => {
COLUMN_KEYS
,
COLUMN_KEYS
,
copyText
,
copyText
,
openContentModal
,
openContentModal
,
openVideoModal
,
isAdminUser
,
isAdminUser
,
]);
]);
...
...
web/src/components/table/task-logs/index.jsx
View file @
1dcb9a01
...
@@ -37,7 +37,14 @@ const TaskLogsPage = () => {
...
@@ -37,7 +37,14 @@ const TaskLogsPage = () => {
<>
<>
{
/* Modals */
}
{
/* Modals */
}
<
ColumnSelectorModal
{
...
taskLogsData
}
/>
<
ColumnSelectorModal
{
...
taskLogsData
}
/>
<
ContentModal
{
...
taskLogsData
}
/>
<
ContentModal
{
...
taskLogsData
}
isVideo=
{
false
}
/>
{
/* 新增:视频预览弹窗 */
}
<
ContentModal
isModalOpen=
{
taskLogsData
.
isVideoModalOpen
}
setIsModalOpen=
{
taskLogsData
.
setIsVideoModalOpen
}
modalContent=
{
taskLogsData
.
videoUrl
}
isVideo=
{
true
}
/>
<
Layout
>
<
Layout
>
<
CardPro
<
CardPro
...
...
web/src/components/table/task-logs/modals/ContentModal.jsx
View file @
1dcb9a01
...
@@ -24,6 +24,7 @@ const ContentModal = ({
...
@@ -24,6 +24,7 @@ const ContentModal = ({
isModalOpen
,
isModalOpen
,
setIsModalOpen
,
setIsModalOpen
,
modalContent
,
modalContent
,
isVideo
,
})
=>
{
})
=>
{
return
(
return
(
<
Modal
<
Modal
...
@@ -34,7 +35,11 @@ const ContentModal = ({
...
@@ -34,7 +35,11 @@ const ContentModal = ({
bodyStyle=
{
{
height
:
'400px'
,
overflow
:
'auto'
}
}
bodyStyle=
{
{
height
:
'400px'
,
overflow
:
'auto'
}
}
width=
{
800
}
width=
{
800
}
>
>
<
p
style=
{
{
whiteSpace
:
'pre-line'
}
}
>
{
modalContent
}
</
p
>
{
isVideo
?
(
<
video
src=
{
modalContent
}
controls
style=
{
{
width
:
'100%'
}
}
autoPlay
/>
)
:
(
<
p
style=
{
{
whiteSpace
:
'pre-line'
}
}
>
{
modalContent
}
</
p
>
)
}
</
Modal
>
</
Modal
>
);
);
};
};
...
...
web/src/components/table/tokens/TokensColumnDefs.js
View file @
1dcb9a01
...
@@ -28,7 +28,8 @@ import {
...
@@ -28,7 +28,8 @@ import {
Avatar
,
Avatar
,
Tooltip
,
Tooltip
,
Progress
,
Progress
,
Switch
,
Popover
,
Typography
,
Input
,
Input
,
Modal
Modal
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
...
@@ -46,21 +47,22 @@ import {
...
@@ -46,21 +47,22 @@ import {
IconEyeClosed
,
IconEyeClosed
,
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
// progress color helper
const
getProgressColor
=
(
pct
)
=>
{
if
(
pct
===
100
)
return
'var(--semi-color-success)'
;
if
(
pct
<=
10
)
return
'var(--semi-color-danger)'
;
if
(
pct
<=
30
)
return
'var(--semi-color-warning)'
;
return
undefined
;
};
// Render functions
// Render functions
function
renderTimestamp
(
timestamp
)
{
function
renderTimestamp
(
timestamp
)
{
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
}
// Render status column
with switch and progress bar
// Render status column
only (no usage)
const
renderStatus
=
(
text
,
record
,
manageToken
,
t
)
=>
{
const
renderStatus
=
(
text
,
record
,
t
)
=>
{
const
enabled
=
text
===
1
;
const
enabled
=
text
===
1
;
const
handleToggle
=
(
checked
)
=>
{
if
(
checked
)
{
manageToken
(
record
.
id
,
'enable'
,
record
);
}
else
{
manageToken
(
record
.
id
,
'disable'
,
record
);
}
};
let
tagColor
=
'black'
;
let
tagColor
=
'black'
;
let
tagText
=
t
(
'未知状态'
);
let
tagText
=
t
(
'未知状态'
);
...
@@ -78,69 +80,11 @@ const renderStatus = (text, record, manageToken, t) => {
...
@@ -78,69 +80,11 @@ const renderStatus = (text, record, manageToken, t) => {
tagText
=
t
(
'已耗尽'
);
tagText
=
t
(
'已耗尽'
);
}
}
const
used
=
parseInt
(
record
.
used_quota
)
||
0
;
return
(
const
remain
=
parseInt
(
record
.
remain_quota
)
||
0
;
<
Tag
color
=
{
tagColor
}
shape
=
'circle'
size
=
'small'
>
const
total
=
used
+
remain
;
const
percent
=
total
>
0
?
(
remain
/
total
)
*
100
:
0
;
const
getProgressColor
=
(
pct
)
=>
{
if
(
pct
===
100
)
return
'var(--semi-color-success)'
;
if
(
pct
<=
10
)
return
'var(--semi-color-danger)'
;
if
(
pct
<=
30
)
return
'var(--semi-color-warning)'
;
return
undefined
;
};
const
quotaSuffix
=
record
.
unlimited_quota
?
(
<
div
className
=
'text-xs'
>
{
t
(
'无限额度'
)}
<
/div
>
)
:
(
<
div
className
=
'flex flex-col items-end'
>
<
span
className
=
'text-xs leading-none'
>
{
`
${
renderQuota
(
remain
)}
/
${
renderQuota
(
total
)}
`
}
<
/span
>
<
Progress
percent
=
{
percent
}
stroke
=
{
getProgressColor
(
percent
)}
aria
-
label
=
'quota usage'
format
=
{()
=>
`
${
percent
.
toFixed
(
0
)}
%`
}
style
=
{{
width
:
'100%'
,
marginTop
:
'1px'
,
marginBottom
:
0
}}
/
>
<
/div
>
);
const
content
=
(
<
Tag
color
=
{
tagColor
}
shape
=
'circle'
size
=
'large'
prefixIcon
=
{
<
Switch
size
=
'small'
checked
=
{
enabled
}
onChange
=
{
handleToggle
}
aria
-
label
=
'token status switch'
/>
}
suffixIcon
=
{
quotaSuffix
}
>
{
tagText
}
{
tagText
}
<
/Tag
>
<
/Tag
>
);
);
const
tooltipContent
=
record
.
unlimited_quota
?
(
<
div
className
=
'text-xs'
>
<
div
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/div
>
<
/div
>
)
:
(
<
div
className
=
'text-xs'
>
<
div
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/div
>
<
div
>
{
t
(
'剩余额度'
)}:
{
renderQuota
(
remain
)}
({
percent
.
toFixed
(
0
)}
%
)
<
/div
>
<
div
>
{
t
(
'总额度'
)}:
{
renderQuota
(
total
)}
<
/div
>
<
/div
>
);
return
(
<
Tooltip
content
=
{
tooltipContent
}
>
{
content
}
<
/Tooltip
>
);
};
};
// Render group column
// Render group column
...
@@ -292,35 +236,81 @@ const renderAllowIps = (text, t) => {
...
@@ -292,35 +236,81 @@ const renderAllowIps = (text, t) => {
return
<
Space
wrap
>
{
ipTags
}
<
/Space>
;
return
<
Space
wrap
>
{
ipTags
}
<
/Space>
;
};
};
// Render separate quota usage column
const
renderQuotaUsage
=
(
text
,
record
,
t
)
=>
{
const
{
Paragraph
}
=
Typography
;
const
used
=
parseInt
(
record
.
used_quota
)
||
0
;
const
remain
=
parseInt
(
record
.
remain_quota
)
||
0
;
const
total
=
used
+
remain
;
if
(
record
.
unlimited_quota
)
{
const
popoverContent
=
(
<
div
className
=
'text-xs p-2'
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
used
)
}}
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/Paragraph
>
<
/div
>
);
return
(
<
Popover
content
=
{
popoverContent
}
position
=
'top'
>
<
Tag
color
=
'white'
shape
=
'circle'
>
{
t
(
'无限额度'
)}
<
/Tag
>
<
/Popover
>
);
}
const
percent
=
total
>
0
?
(
remain
/
total
)
*
100
:
0
;
const
popoverContent
=
(
<
div
className
=
'text-xs p-2'
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
used
)
}}
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/Paragraph
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
remain
)
}}
>
{
t
(
'剩余额度'
)}:
{
renderQuota
(
remain
)}
({
percent
.
toFixed
(
0
)}
%
)
<
/Paragraph
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
total
)
}}
>
{
t
(
'总额度'
)}:
{
renderQuota
(
total
)}
<
/Paragraph
>
<
/div
>
);
return
(
<
Popover
content
=
{
popoverContent
}
position
=
'top'
>
<
Tag
color
=
'white'
shape
=
'circle'
>
<
div
className
=
'flex flex-col items-end'
>
<
span
className
=
'text-xs leading-none'
>
{
`
${
renderQuota
(
remain
)}
/
${
renderQuota
(
total
)}
`
}
<
/span
>
<
Progress
percent
=
{
percent
}
stroke
=
{
getProgressColor
(
percent
)}
aria
-
label
=
'quota usage'
format
=
{()
=>
`
${
percent
.
toFixed
(
0
)}
%`
}
style
=
{{
width
:
'100%'
,
marginTop
:
'1px'
,
marginBottom
:
0
}}
/
>
<
/div
>
<
/Tag
>
<
/Popover
>
);
};
// Render operations column
// Render operations column
const
renderOperations
=
(
text
,
record
,
onOpenLink
,
setEditingToken
,
setShowEdit
,
manageToken
,
refresh
,
t
)
=>
{
const
renderOperations
=
(
text
,
record
,
onOpenLink
,
setEditingToken
,
setShowEdit
,
manageToken
,
refresh
,
t
)
=>
{
let
chats
=
localStorage
.
getItem
(
'chats'
);
let
chatsArray
=
[];
let
chatsArray
=
[];
let
shouldUseCustom
=
true
;
try
{
const
raw
=
localStorage
.
getItem
(
'chats'
);
if
(
shouldUseCustom
)
{
const
parsed
=
JSON
.
parse
(
raw
);
try
{
if
(
Array
.
isArray
(
parsed
))
{
chats
=
JSON
.
parse
(
chats
);
for
(
let
i
=
0
;
i
<
parsed
.
length
;
i
++
)
{
if
(
Array
.
isArray
(
chats
))
{
const
item
=
parsed
[
i
];
for
(
let
i
=
0
;
i
<
chats
.
length
;
i
++
)
{
const
name
=
Object
.
keys
(
item
)[
0
];
let
chat
=
{};
if
(
!
name
)
continue
;
chat
.
node
=
'item'
;
chatsArray
.
push
({
for
(
let
key
in
chats
[
i
])
{
node
:
'item'
,
if
(
chats
[
i
].
hasOwnProperty
(
key
))
{
key
:
i
,
chat
.
key
=
i
;
name
,
chat
.
name
=
key
;
onClick
:
()
=>
onOpenLink
(
name
,
item
[
name
],
record
),
chat
.
onClick
=
()
=>
{
});
onOpenLink
(
key
,
chats
[
i
][
key
],
record
);
};
}
}
chatsArray
.
push
(
chat
);
}
}
}
}
catch
(
e
)
{
console
.
log
(
e
);
showError
(
t
(
'聊天链接配置错误,请联系管理员'
));
}
}
}
catch
(
_
)
{
showError
(
t
(
'聊天链接配置错误,请联系管理员'
));
}
}
return
(
return
(
...
@@ -338,7 +328,7 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
...
@@ -338,7 +328,7 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
}
else
{
}
else
{
onOpenLink
(
onOpenLink
(
'default'
,
'default'
,
chats
[
0
][
Object
.
keys
(
chats
[
0
])[
0
]]
,
chats
Array
[
0
].
name
?
(
parsed
=>
parsed
)(
localStorage
.
getItem
(
'chats'
))
:
''
,
record
,
record
,
);
);
}
}
...
@@ -359,6 +349,29 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
...
@@ -359,6 +349,29 @@ const renderOperations = (text, record, onOpenLink, setEditingToken, setShowEdit
<
/Dropdown
>
<
/Dropdown
>
<
/SplitButtonGroup
>
<
/SplitButtonGroup
>
{
record
.
status
===
1
?
(
<
Button
type
=
'danger'
size
=
"small"
onClick
=
{
async
()
=>
{
await
manageToken
(
record
.
id
,
'disable'
,
record
);
await
refresh
();
}}
>
{
t
(
'禁用'
)}
<
/Button
>
)
:
(
<
Button
size
=
"small"
onClick
=
{
async
()
=>
{
await
manageToken
(
record
.
id
,
'enable'
,
record
);
await
refresh
();
}}
>
{
t
(
'启用'
)}
<
/Button
>
)}
<
Button
<
Button
type
=
'tertiary'
type
=
'tertiary'
size
=
"small"
size
=
"small"
...
@@ -412,7 +425,12 @@ export const getTokensColumns = ({
...
@@ -412,7 +425,12 @@ export const getTokensColumns = ({
title
:
t
(
'状态'
),
title
:
t
(
'状态'
),
dataIndex
:
'status'
,
dataIndex
:
'status'
,
key
:
'status'
,
key
:
'status'
,
render
:
(
text
,
record
)
=>
renderStatus
(
text
,
record
,
manageToken
,
t
),
render
:
(
text
,
record
)
=>
renderStatus
(
text
,
record
,
t
),
},
{
title
:
t
(
'剩余额度/总额度'
),
key
:
'quota_usage'
,
render
:
(
text
,
record
)
=>
renderQuotaUsage
(
text
,
record
,
t
),
},
},
{
{
title
:
t
(
'分组'
),
title
:
t
(
'分组'
),
...
...
web/src/components/table/tokens/index.jsx
View file @
1dcb9a01
...
@@ -17,7 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,7 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
React
from
'react'
;
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
Notification
,
Button
,
Space
,
Toast
,
Typography
,
Select
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
showError
,
getModelCategories
,
selectFilter
}
from
'../../../helpers'
;
import
CardPro
from
'../../common/ui/CardPro'
;
import
CardPro
from
'../../common/ui/CardPro'
;
import
TokensTable
from
'./TokensTable.jsx'
;
import
TokensTable
from
'./TokensTable.jsx'
;
import
TokensActions
from
'./TokensActions.jsx'
;
import
TokensActions
from
'./TokensActions.jsx'
;
...
@@ -28,9 +30,243 @@ import { useTokensData } from '../../../hooks/tokens/useTokensData';
...
@@ -28,9 +30,243 @@ import { useTokensData } from '../../../hooks/tokens/useTokensData';
import
{
useIsMobile
}
from
'../../../hooks/common/useIsMobile'
;
import
{
useIsMobile
}
from
'../../../hooks/common/useIsMobile'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
const
TokensPage
=
()
=>
{
function
TokensPage
()
{
const
tokensData
=
useTokensData
();
// Define the function first, then pass it into the hook to avoid TDZ errors
const
openFluentNotificationRef
=
useRef
(
null
);
const
tokensData
=
useTokensData
((
key
)
=>
openFluentNotificationRef
.
current
?.(
key
));
const
isMobile
=
useIsMobile
();
const
isMobile
=
useIsMobile
();
const
latestRef
=
useRef
({
tokens
:
[],
selectedKeys
:
[],
t
:
(
k
)
=>
k
,
selectedModel
:
''
,
prefillKey
:
''
});
const
[
modelOptions
,
setModelOptions
]
=
useState
([]);
const
[
selectedModel
,
setSelectedModel
]
=
useState
(
''
);
const
[
fluentNoticeOpen
,
setFluentNoticeOpen
]
=
useState
(
false
);
const
[
prefillKey
,
setPrefillKey
]
=
useState
(
''
);
// Keep latest data for handlers inside notifications
useEffect
(()
=>
{
latestRef
.
current
=
{
tokens
:
tokensData
.
tokens
,
selectedKeys
:
tokensData
.
selectedKeys
,
t
:
tokensData
.
t
,
selectedModel
,
prefillKey
,
};
},
[
tokensData
.
tokens
,
tokensData
.
selectedKeys
,
tokensData
.
t
,
selectedModel
,
prefillKey
]);
const
loadModels
=
async
()
=>
{
try
{
const
res
=
await
API
.
get
(
'/api/user/models'
);
const
{
success
,
message
,
data
}
=
res
.
data
||
{};
if
(
success
)
{
const
categories
=
getModelCategories
(
tokensData
.
t
);
const
options
=
(
data
||
[]).
map
((
model
)
=>
{
let
icon
=
null
;
for
(
const
[
key
,
category
]
of
Object
.
entries
(
categories
))
{
if
(
key
!==
'all'
&&
category
.
filter
({
model_name
:
model
}))
{
icon
=
category
.
icon
;
break
;
}
}
return
{
label
:
(
<
span
className=
"flex items-center gap-1"
>
{
icon
}
{
model
}
</
span
>
),
value
:
model
,
};
});
setModelOptions
(
options
);
}
else
{
showError
(
tokensData
.
t
(
message
));
}
}
catch
(
e
)
{
showError
(
e
.
message
||
'Failed to load models'
);
}
};
function
openFluentNotification
(
key
)
{
const
{
t
}
=
latestRef
.
current
;
const
SUPPRESS_KEY
=
'fluent_notify_suppressed'
;
if
(
localStorage
.
getItem
(
SUPPRESS_KEY
)
===
'1'
)
return
;
const
container
=
document
.
getElementById
(
'fluent-new-api-container'
);
if
(
!
container
)
{
Toast
.
warning
(
t
(
'未检测到 Fluent 容器,请确认扩展已启用'
));
return
;
}
setPrefillKey
(
key
||
''
);
setFluentNoticeOpen
(
true
);
if
(
modelOptions
.
length
===
0
)
{
// fire-and-forget; a later effect will refresh the notice content
loadModels
()
}
Notification
.
info
({
id
:
'fluent-detected'
,
title
:
t
(
'检测到 Fluent(流畅阅读)'
),
content
:
(
<
div
>
<
div
style=
{
{
marginBottom
:
8
}
}
>
{
prefillKey
?
t
(
'已检测到 Fluent 扩展,已从操作中指定密钥,将使用该密钥进行填充。请选择模型后继续。'
)
:
t
(
'已检测到 Fluent 扩展,请选择模型后可一键填充当前选中令牌(或本页第一个令牌)。'
)
}
</
div
>
<
div
style=
{
{
marginBottom
:
8
}
}
>
<
Select
placeholder=
{
t
(
'请选择模型'
)
}
optionList=
{
modelOptions
}
onChange=
{
setSelectedModel
}
filter=
{
selectFilter
}
style=
{
{
width
:
320
}
}
showClear
searchable
emptyContent=
{
t
(
'暂无数据'
)
}
/>
</
div
>
<
Space
>
<
Button
theme=
"solid"
type=
"primary"
onClick=
{
handlePrefillToFluent
}
>
{
t
(
'一键填充到 Fluent'
)
}
</
Button
>
<
Button
type=
"warning"
onClick=
{
()
=>
{
localStorage
.
setItem
(
SUPPRESS_KEY
,
'1'
);
Notification
.
close
(
'fluent-detected'
);
Toast
.
info
(
t
(
'已关闭后续提醒'
));
}
}
>
{
t
(
'不再提醒'
)
}
</
Button
>
<
Button
type=
"tertiary"
onClick=
{
()
=>
Notification
.
close
(
'fluent-detected'
)
}
>
{
t
(
'关闭'
)
}
</
Button
>
</
Space
>
</
div
>
),
duration
:
0
,
});
}
// assign after definition so hook callback can call it safely
openFluentNotificationRef
.
current
=
openFluentNotification
;
// Prefill to Fluent handler
const
handlePrefillToFluent
=
()
=>
{
const
{
tokens
,
selectedKeys
,
t
,
selectedModel
:
chosenModel
,
prefillKey
:
overrideKey
}
=
latestRef
.
current
;
const
container
=
document
.
getElementById
(
'fluent-new-api-container'
);
if
(
!
container
)
{
Toast
.
error
(
t
(
'未检测到 Fluent 容器'
));
return
;
}
if
(
!
chosenModel
)
{
Toast
.
warning
(
t
(
'请选择模型'
));
return
;
}
let
status
=
localStorage
.
getItem
(
'status'
);
let
serverAddress
=
''
;
if
(
status
)
{
try
{
status
=
JSON
.
parse
(
status
);
serverAddress
=
status
.
server_address
||
''
;
}
catch
(
_
)
{
}
}
if
(
!
serverAddress
)
serverAddress
=
window
.
location
.
origin
;
let
apiKeyToUse
=
''
;
if
(
overrideKey
)
{
apiKeyToUse
=
'sk-'
+
overrideKey
;
}
else
{
const
token
=
(
selectedKeys
&&
selectedKeys
.
length
===
1
)
?
selectedKeys
[
0
]
:
(
tokens
&&
tokens
.
length
>
0
?
tokens
[
0
]
:
null
);
if
(
!
token
)
{
Toast
.
warning
(
t
(
'没有可用令牌用于填充'
));
return
;
}
apiKeyToUse
=
'sk-'
+
token
.
key
;
}
const
payload
=
{
id
:
'new-api'
,
baseUrl
:
serverAddress
,
apiKey
:
apiKeyToUse
,
model
:
chosenModel
,
};
container
.
dispatchEvent
(
new
CustomEvent
(
'fluent:prefill'
,
{
detail
:
payload
}));
Toast
.
success
(
t
(
'已发送到 Fluent'
));
Notification
.
close
(
'fluent-detected'
);
};
// Show notification when Fluent container is available
useEffect
(()
=>
{
const
onAppeared
=
()
=>
{
openFluentNotification
();
};
const
onRemoved
=
()
=>
{
setFluentNoticeOpen
(
false
);
Notification
.
close
(
'fluent-detected'
);
};
window
.
addEventListener
(
'fluent-container:appeared'
,
onAppeared
);
window
.
addEventListener
(
'fluent-container:removed'
,
onRemoved
);
return
()
=>
{
window
.
removeEventListener
(
'fluent-container:appeared'
,
onAppeared
);
window
.
removeEventListener
(
'fluent-container:removed'
,
onRemoved
);
};
},
[]);
// When modelOptions or language changes while the notice is open, refresh the content
useEffect
(()
=>
{
if
(
fluentNoticeOpen
)
{
openFluentNotification
();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
modelOptions
,
selectedModel
,
tokensData
.
t
,
fluentNoticeOpen
]);
useEffect
(()
=>
{
const
selector
=
'#fluent-new-api-container'
;
const
root
=
document
.
body
||
document
.
documentElement
;
const
existing
=
document
.
querySelector
(
selector
);
if
(
existing
)
{
console
.
log
(
'Fluent container detected (initial):'
,
existing
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:appeared'
,
{
detail
:
existing
}));
}
const
isOrContainsTarget
=
(
node
)
=>
{
if
(
!
(
node
&&
node
.
nodeType
===
1
))
return
false
;
if
(
node
.
id
===
'fluent-new-api-container'
)
return
true
;
return
typeof
node
.
querySelector
===
'function'
&&
!!
node
.
querySelector
(
selector
);
};
const
observer
=
new
MutationObserver
((
mutations
)
=>
{
for
(
const
m
of
mutations
)
{
// appeared
for
(
const
added
of
m
.
addedNodes
)
{
if
(
isOrContainsTarget
(
added
))
{
const
el
=
document
.
querySelector
(
selector
);
if
(
el
)
{
console
.
log
(
'Fluent container appeared:'
,
el
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:appeared'
,
{
detail
:
el
}));
}
break
;
}
}
// removed
for
(
const
removed
of
m
.
removedNodes
)
{
if
(
isOrContainsTarget
(
removed
))
{
const
elNow
=
document
.
querySelector
(
selector
);
if
(
!
elNow
)
{
console
.
log
(
'Fluent container removed'
);
window
.
dispatchEvent
(
new
CustomEvent
(
'fluent-container:removed'
));
}
break
;
}
}
}
});
observer
.
observe
(
root
,
{
childList
:
true
,
subtree
:
true
});
return
()
=>
observer
.
disconnect
();
},
[]);
const
{
const
{
// Edit state
// Edit state
...
@@ -119,6 +355,6 @@ const TokensPage = () => {
...
@@ -119,6 +355,6 @@ const TokensPage = () => {
</
CardPro
>
</
CardPro
>
</>
</>
);
);
}
;
}
export
default
TokensPage
;
export
default
TokensPage
;
\ No newline at end of file
web/src/components/table/usage-logs/UsageLogsColumnDefs.js
View file @
1dcb9a01
...
@@ -34,7 +34,6 @@ import {
...
@@ -34,7 +34,6 @@ import {
getLogOther
,
getLogOther
,
renderModelTag
,
renderModelTag
,
renderClaudeLogContent
,
renderClaudeLogContent
,
renderClaudeModelPriceSimple
,
renderLogContent
,
renderLogContent
,
renderModelPriceSimple
,
renderModelPriceSimple
,
renderAudioModelPrice
,
renderAudioModelPrice
,
...
@@ -538,7 +537,7 @@ export const getLogsColumns = ({
...
@@ -538,7 +537,7 @@ export const getLogsColumns = ({
);
);
}
}
let
content
=
other
?.
claude
let
content
=
other
?.
claude
?
render
Claude
ModelPriceSimple
(
?
renderModelPriceSimple
(
other
.
model_ratio
,
other
.
model_ratio
,
other
.
model_price
,
other
.
model_price
,
other
.
group_ratio
,
other
.
group_ratio
,
...
@@ -547,6 +546,10 @@ export const getLogsColumns = ({
...
@@ -547,6 +546,10 @@ export const getLogsColumns = ({
other
.
cache_ratio
||
1.0
,
other
.
cache_ratio
||
1.0
,
other
.
cache_creation_tokens
||
0
,
other
.
cache_creation_tokens
||
0
,
other
.
cache_creation_ratio
||
1.0
,
other
.
cache_creation_ratio
||
1.0
,
false
,
1.0
,
other
?.
is_system_prompt_overwritten
,
'claude'
)
)
:
renderModelPriceSimple
(
:
renderModelPriceSimple
(
other
.
model_ratio
,
other
.
model_ratio
,
...
@@ -555,13 +558,19 @@ export const getLogsColumns = ({
...
@@ -555,13 +558,19 @@ export const getLogsColumns = ({
other
?.
user_group_ratio
,
other
?.
user_group_ratio
,
other
.
cache_tokens
||
0
,
other
.
cache_tokens
||
0
,
other
.
cache_ratio
||
1.0
,
other
.
cache_ratio
||
1.0
,
0
,
1.0
,
false
,
1.0
,
other
?.
is_system_prompt_overwritten
,
'openai'
);
);
return
(
return
(
<
Typography
.
Paragraph
<
Typography
.
Paragraph
ellipsis
=
{{
ellipsis
=
{{
rows
:
2
,
rows
:
3
,
}}
}}
style
=
{{
maxWidth
:
240
}}
style
=
{{
maxWidth
:
240
,
whiteSpace
:
'pre-line'
}}
>
>
{
content
}
{
content
}
<
/Typography.Paragraph
>
<
/Typography.Paragraph
>
...
...
web/src/components/table/users/UsersColumnDefs.js
View file @
1dcb9a01
...
@@ -24,7 +24,8 @@ import {
...
@@ -24,7 +24,8 @@ import {
Tag
,
Tag
,
Tooltip
,
Tooltip
,
Progress
,
Progress
,
Switch
,
Popover
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
renderGroup
,
renderNumber
,
renderQuota
}
from
'../../../helpers'
;
import
{
renderGroup
,
renderNumber
,
renderQuota
}
from
'../../../helpers'
;
...
@@ -89,7 +90,6 @@ const renderUsername = (text, record) => {
...
@@ -89,7 +90,6 @@ const renderUsername = (text, record) => {
* Render user statistics
* Render user statistics
*/
*/
const
renderStatistics
=
(
text
,
record
,
showEnableDisableModal
,
t
)
=>
{
const
renderStatistics
=
(
text
,
record
,
showEnableDisableModal
,
t
)
=>
{
const
enabled
=
record
.
status
===
1
;
const
isDeleted
=
record
.
DeletedAt
!==
null
;
const
isDeleted
=
record
.
DeletedAt
!==
null
;
// Determine tag text & color like original status column
// Determine tag text & color like original status column
...
@@ -100,60 +100,17 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
...
@@ -100,60 +100,17 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
tagText
=
t
(
'已注销'
);
tagText
=
t
(
'已注销'
);
}
else
if
(
record
.
status
===
1
)
{
}
else
if
(
record
.
status
===
1
)
{
tagColor
=
'green'
;
tagColor
=
'green'
;
tagText
=
t
(
'已
激活
'
);
tagText
=
t
(
'已
启用
'
);
}
else
if
(
record
.
status
===
2
)
{
}
else
if
(
record
.
status
===
2
)
{
tagColor
=
'red'
;
tagColor
=
'red'
;
tagText
=
t
(
'已
封禁
'
);
tagText
=
t
(
'已
禁用
'
);
}
}
const
handleToggle
=
(
checked
)
=>
{
if
(
checked
)
{
showEnableDisableModal
(
record
,
'enable'
);
}
else
{
showEnableDisableModal
(
record
,
'disable'
);
}
};
const
used
=
parseInt
(
record
.
used_quota
)
||
0
;
const
remain
=
parseInt
(
record
.
quota
)
||
0
;
const
total
=
used
+
remain
;
const
percent
=
total
>
0
?
(
remain
/
total
)
*
100
:
0
;
const
getProgressColor
=
(
pct
)
=>
{
if
(
pct
===
100
)
return
'var(--semi-color-success)'
;
if
(
pct
<=
10
)
return
'var(--semi-color-danger)'
;
if
(
pct
<=
30
)
return
'var(--semi-color-warning)'
;
return
undefined
;
};
const
quotaSuffix
=
(
<
div
className
=
'flex flex-col items-end'
>
<
span
className
=
'text-xs leading-none'
>
{
`
${
renderQuota
(
remain
)}
/
${
renderQuota
(
total
)}
`
}
<
/span
>
<
Progress
percent
=
{
percent
}
stroke
=
{
getProgressColor
(
percent
)}
aria
-
label
=
'quota usage'
format
=
{()
=>
`
${
percent
.
toFixed
(
0
)}
%`
}
style
=
{{
width
:
'100%'
,
marginTop
:
'1px'
,
marginBottom
:
0
}}
/
>
<
/div
>
);
const
content
=
(
const
content
=
(
<
Tag
<
Tag
color
=
{
tagColor
}
color
=
{
tagColor
}
shape
=
'circle'
shape
=
'circle'
size
=
'large'
size
=
'small'
prefixIcon
=
{
<
Switch
size
=
'small'
checked
=
{
enabled
}
onChange
=
{
handleToggle
}
disabled
=
{
isDeleted
}
aria
-
label
=
'user status switch'
/>
}
suffixIcon
=
{
quotaSuffix
}
>
>
{
tagText
}
{
tagText
}
<
/Tag
>
<
/Tag
>
...
@@ -161,9 +118,6 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
...
@@ -161,9 +118,6 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
const
tooltipContent
=
(
const
tooltipContent
=
(
<
div
className
=
'text-xs'
>
<
div
className
=
'text-xs'
>
<
div
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/div
>
<
div
>
{
t
(
'剩余额度'
)}:
{
renderQuota
(
remain
)}
({
percent
.
toFixed
(
0
)}
%
)
<
/div
>
<
div
>
{
t
(
'总额度'
)}:
{
renderQuota
(
total
)}
<
/div
>
<
div
>
{
t
(
'调用次数'
)}:
{
renderNumber
(
record
.
request_count
)}
<
/div
>
<
div
>
{
t
(
'调用次数'
)}:
{
renderNumber
(
record
.
request_count
)}
<
/div
>
<
/div
>
<
/div
>
);
);
...
@@ -175,6 +129,43 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
...
@@ -175,6 +129,43 @@ const renderStatistics = (text, record, showEnableDisableModal, t) => {
);
);
};
};
// Render separate quota usage column
const
renderQuotaUsage
=
(
text
,
record
,
t
)
=>
{
const
{
Paragraph
}
=
Typography
;
const
used
=
parseInt
(
record
.
used_quota
)
||
0
;
const
remain
=
parseInt
(
record
.
quota
)
||
0
;
const
total
=
used
+
remain
;
const
percent
=
total
>
0
?
(
remain
/
total
)
*
100
:
0
;
const
popoverContent
=
(
<
div
className
=
'text-xs p-2'
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
used
)
}}
>
{
t
(
'已用额度'
)}:
{
renderQuota
(
used
)}
<
/Paragraph
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
remain
)
}}
>
{
t
(
'剩余额度'
)}:
{
renderQuota
(
remain
)}
({
percent
.
toFixed
(
0
)}
%
)
<
/Paragraph
>
<
Paragraph
copyable
=
{{
content
:
renderQuota
(
total
)
}}
>
{
t
(
'总额度'
)}:
{
renderQuota
(
total
)}
<
/Paragraph
>
<
/div
>
);
return
(
<
Popover
content
=
{
popoverContent
}
position
=
'top'
>
<
Tag
color
=
'white'
shape
=
'circle'
>
<
div
className
=
'flex flex-col items-end'
>
<
span
className
=
'text-xs leading-none'
>
{
`
${
renderQuota
(
remain
)}
/
${
renderQuota
(
total
)}
`
}
<
/span
>
<
Progress
percent
=
{
percent
}
aria
-
label
=
'quota usage'
format
=
{()
=>
`
${
percent
.
toFixed
(
0
)}
%`
}
style
=
{{
width
:
'100%'
,
marginTop
:
'1px'
,
marginBottom
:
0
}}
/
>
<
/div
>
<
/Tag
>
<
/Popover
>
);
};
/**
/**
* Render invite information
* Render invite information
*/
*/
...
@@ -204,6 +195,7 @@ const renderOperations = (text, record, {
...
@@ -204,6 +195,7 @@ const renderOperations = (text, record, {
setShowEditUser
,
setShowEditUser
,
showPromoteModal
,
showPromoteModal
,
showDemoteModal
,
showDemoteModal
,
showEnableDisableModal
,
showDeleteModal
,
showDeleteModal
,
t
t
})
=>
{
})
=>
{
...
@@ -213,6 +205,22 @@ const renderOperations = (text, record, {
...
@@ -213,6 +205,22 @@ const renderOperations = (text, record, {
return
(
return
(
<
Space
>
<
Space
>
{
record
.
status
===
1
?
(
<
Button
type
=
'danger'
size
=
"small"
onClick
=
{()
=>
showEnableDisableModal
(
record
,
'disable'
)}
>
{
t
(
'禁用'
)}
<
/Button
>
)
:
(
<
Button
size
=
"small"
onClick
=
{()
=>
showEnableDisableModal
(
record
,
'enable'
)}
>
{
t
(
'启用'
)}
<
/Button
>
)}
<
Button
<
Button
type
=
'tertiary'
type
=
'tertiary'
size
=
"small"
size
=
"small"
...
@@ -271,6 +279,16 @@ export const getUsersColumns = ({
...
@@ -271,6 +279,16 @@ export const getUsersColumns = ({
render
:
(
text
,
record
)
=>
renderUsername
(
text
,
record
),
render
:
(
text
,
record
)
=>
renderUsername
(
text
,
record
),
},
},
{
{
title
:
t
(
'状态'
),
dataIndex
:
'info'
,
render
:
(
text
,
record
,
index
)
=>
renderStatistics
(
text
,
record
,
showEnableDisableModal
,
t
),
},
{
title
:
t
(
'剩余额度/总额度'
),
key
:
'quota_usage'
,
render
:
(
text
,
record
)
=>
renderQuotaUsage
(
text
,
record
,
t
),
},
{
title
:
t
(
'分组'
),
title
:
t
(
'分组'
),
dataIndex
:
'group'
,
dataIndex
:
'group'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -285,11 +303,6 @@ export const getUsersColumns = ({
...
@@ -285,11 +303,6 @@ export const getUsersColumns = ({
},
},
},
},
{
{
title
:
t
(
'状态'
),
dataIndex
:
'info'
,
render
:
(
text
,
record
,
index
)
=>
renderStatistics
(
text
,
record
,
showEnableDisableModal
,
t
),
},
{
title
:
t
(
'邀请信息'
),
title
:
t
(
'邀请信息'
),
dataIndex
:
'invite'
,
dataIndex
:
'invite'
,
render
:
(
text
,
record
,
index
)
=>
renderInviteInfo
(
text
,
record
,
t
),
render
:
(
text
,
record
,
index
)
=>
renderInviteInfo
(
text
,
record
,
t
),
...
...
web/src/constants/channel.constants.js
View file @
1dcb9a01
...
@@ -81,7 +81,7 @@ export const CHANNEL_OPTIONS = [
...
@@ -81,7 +81,7 @@ export const CHANNEL_OPTIONS = [
{
{
value
:
16
,
value
:
16
,
color
:
'violet'
,
color
:
'violet'
,
label
:
'智谱 ChatGLM'
,
label
:
'智谱 ChatGLM
(已经弃用,请使用智谱 GLM-4V)
'
,
},
},
{
{
value
:
26
,
value
:
26
,
...
...
web/src/helpers/api.js
View file @
1dcb9a01
...
@@ -215,14 +215,16 @@ export async function getOAuthState() {
...
@@ -215,14 +215,16 @@ export async function getOAuthState() {
export
async
function
onOIDCClicked
(
auth_url
,
client_id
,
openInNewTab
=
false
)
{
export
async
function
onOIDCClicked
(
auth_url
,
client_id
,
openInNewTab
=
false
)
{
const
state
=
await
getOAuthState
();
const
state
=
await
getOAuthState
();
if
(
!
state
)
return
;
if
(
!
state
)
return
;
const
redirect_uri
=
`
${
window
.
location
.
origin
}
/oauth/oidc`
;
const
url
=
new
URL
(
auth_url
);
const
response_type
=
'code'
;
url
.
searchParams
.
set
(
'client_id'
,
client_id
);
const
scope
=
'openid profile email'
;
url
.
searchParams
.
set
(
'redirect_uri'
,
`
${
window
.
location
.
origin
}
/oauth/oidc`
);
const
url
=
`
${
auth_url
}
?client_id=
${
client_id
}
&redirect_uri=
${
redirect_uri
}
&response_type=
${
response_type
}
&scope=
${
scope
}
&state=
${
state
}
`
;
url
.
searchParams
.
set
(
'response_type'
,
'code'
);
url
.
searchParams
.
set
(
'scope'
,
'openid profile email'
);
url
.
searchParams
.
set
(
'state'
,
state
);
if
(
openInNewTab
)
{
if
(
openInNewTab
)
{
window
.
open
(
url
);
window
.
open
(
url
.
toString
(),
'_blank'
);
}
else
{
}
else
{
window
.
location
.
href
=
url
;
window
.
location
.
href
=
url
.
toString
()
;
}
}
}
}
...
...
web/src/helpers/render.js
View file @
1dcb9a01
...
@@ -953,6 +953,71 @@ function getEffectiveRatio(groupRatio, user_group_ratio) {
...
@@ -953,6 +953,71 @@ function getEffectiveRatio(groupRatio, user_group_ratio) {
};
};
}
}
// Shared core for simple price rendering (used by OpenAI-like and Claude-like variants)
function
renderPriceSimpleCore
({
modelRatio
,
modelPrice
=
-
1
,
groupRatio
,
user_group_ratio
,
cacheTokens
=
0
,
cacheRatio
=
1.0
,
cacheCreationTokens
=
0
,
cacheCreationRatio
=
1.0
,
image
=
false
,
imageRatio
=
1.0
,
isSystemPromptOverride
=
false
})
{
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
user_group_ratio
,
);
const
finalGroupRatio
=
effectiveGroupRatio
;
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'价格:${{price}} * {{ratioType}}:{{ratio}}'
,
{
price
:
modelPrice
,
ratioType
:
ratioLabel
,
ratio
:
finalGroupRatio
,
});
}
const
parts
=
[];
// base: model ratio
parts
.
push
(
i18next
.
t
(
'模型: {{ratio}}'
));
// cache part (label differs when with image)
if
(
cacheTokens
!==
0
)
{
parts
.
push
(
i18next
.
t
(
'缓存: {{cacheRatio}}'
));
}
// cache creation part (Claude specific if passed)
if
(
cacheCreationTokens
!==
0
)
{
parts
.
push
(
i18next
.
t
(
'缓存创建: {{cacheCreationRatio}}'
));
}
// image part
if
(
image
)
{
parts
.
push
(
i18next
.
t
(
'图片输入: {{imageRatio}}'
));
}
parts
.
push
(
`{{ratioType}}: {{groupRatio}}`
);
let
result
=
i18next
.
t
(
parts
.
join
(
' * '
),
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
finalGroupRatio
,
cacheRatio
:
cacheRatio
,
cacheCreationRatio
:
cacheCreationRatio
,
imageRatio
:
imageRatio
,
})
if
(
isSystemPromptOverride
)
{
result
+=
'\n\r'
+
i18next
.
t
(
'系统提示覆盖'
);
}
return
result
;
}
export
function
renderModelPrice
(
export
function
renderModelPrice
(
inputTokens
,
inputTokens
,
completionTokens
,
completionTokens
,
...
@@ -1245,56 +1310,26 @@ export function renderModelPriceSimple(
...
@@ -1245,56 +1310,26 @@ export function renderModelPriceSimple(
user_group_ratio
,
user_group_ratio
,
cacheTokens
=
0
,
cacheTokens
=
0
,
cacheRatio
=
1.0
,
cacheRatio
=
1.0
,
cacheCreationTokens
=
0
,
cacheCreationRatio
=
1.0
,
image
=
false
,
image
=
false
,
imageRatio
=
1.0
,
imageRatio
=
1.0
,
isSystemPromptOverride
=
false
,
provider
=
'openai'
,
)
{
)
{
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
user_group_ratio
);
return
renderPriceSimpleCore
({
groupRatio
=
effectiveGroupRatio
;
modelRatio
,
if
(
modelPrice
!==
-
1
)
{
modelPrice
,
return
i18next
.
t
(
'价格:${{price}} * {{ratioType}}:{{ratio}}'
,
{
groupRatio
,
price
:
modelPrice
,
user_group_ratio
,
ratioType
:
ratioLabel
,
cacheTokens
,
ratio
:
groupRatio
,
cacheRatio
,
});
cacheCreationTokens
,
}
else
{
cacheCreationRatio
,
if
(
image
&&
cacheTokens
!==
0
)
{
image
,
return
i18next
.
t
(
imageRatio
,
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}'
,
isSystemPromptOverride
{
});
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
cacheRatio
:
cacheRatio
,
imageRatio
:
imageRatio
,
},
);
}
else
if
(
image
)
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
imageRatio
:
imageRatio
,
},
);
}
else
if
(
cacheTokens
!==
0
)
{
return
i18next
.
t
(
'模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}'
,
{
ratio
:
modelRatio
,
groupRatio
:
groupRatio
,
cacheRatio
:
cacheRatio
,
},
);
}
else
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}:{{groupRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
});
}
}
}
}
export
function
renderAudioModelPrice
(
export
function
renderAudioModelPrice
(
...
@@ -1635,46 +1670,7 @@ export function renderClaudeLogContent(
...
@@ -1635,46 +1670,7 @@ export function renderClaudeLogContent(
}
}
}
}
export
function
renderClaudeModelPriceSimple
(
// 已统一至 renderModelPriceSimple,若仍有遗留引用,请改为传入 provider='claude'
modelRatio
,
modelPrice
=
-
1
,
groupRatio
,
user_group_ratio
,
cacheTokens
=
0
,
cacheRatio
=
1.0
,
cacheCreationTokens
=
0
,
cacheCreationRatio
=
1.0
,
)
{
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
user_group_ratio
);
groupRatio
=
effectiveGroupRatio
;
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'价格:${{price}} * {{ratioType}}:{{ratio}}'
,
{
price
:
modelPrice
,
ratioType
:
ratioLabel
,
ratio
:
groupRatio
,
});
}
else
{
if
(
cacheTokens
!==
0
||
cacheCreationTokens
!==
0
)
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
cacheRatio
:
cacheRatio
,
cacheCreationRatio
:
cacheCreationRatio
,
},
);
}
else
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
});
}
}
}
/**
/**
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
...
...
web/src/hooks/task-logs/useTaskLogsData.js
View file @
1dcb9a01
...
@@ -65,6 +65,10 @@ export const useTaskLogsData = () => {
...
@@ -65,6 +65,10 @@ export const useTaskLogsData = () => {
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
false
);
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
false
);
const
[
modalContent
,
setModalContent
]
=
useState
(
''
);
const
[
modalContent
,
setModalContent
]
=
useState
(
''
);
// 新增:视频预览弹窗状态
const
[
isVideoModalOpen
,
setIsVideoModalOpen
]
=
useState
(
false
);
const
[
videoUrl
,
setVideoUrl
]
=
useState
(
''
);
// Form state
// Form state
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
let
now
=
new
Date
();
let
now
=
new
Date
();
...
@@ -250,6 +254,12 @@ export const useTaskLogsData = () => {
...
@@ -250,6 +254,12 @@ export const useTaskLogsData = () => {
setIsModalOpen
(
true
);
setIsModalOpen
(
true
);
};
};
// 新增:打开视频预览弹窗
const
openVideoModal
=
(
url
)
=>
{
setVideoUrl
(
url
);
setIsVideoModalOpen
(
true
);
};
// Initialize data
// Initialize data
useEffect
(()
=>
{
useEffect
(()
=>
{
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'task-page-size'
))
||
ITEMS_PER_PAGE
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'task-page-size'
))
||
ITEMS_PER_PAGE
;
...
@@ -271,6 +281,11 @@ export const useTaskLogsData = () => {
...
@@ -271,6 +281,11 @@ export const useTaskLogsData = () => {
setIsModalOpen
,
setIsModalOpen
,
modalContent
,
modalContent
,
// 新增:视频弹窗状态
isVideoModalOpen
,
setIsVideoModalOpen
,
videoUrl
,
// Form state
// Form state
formApi
,
formApi
,
setFormApi
,
setFormApi
,
...
@@ -297,6 +312,7 @@ export const useTaskLogsData = () => {
...
@@ -297,6 +312,7 @@ export const useTaskLogsData = () => {
refresh
,
refresh
,
copyText
,
copyText
,
openContentModal
,
openContentModal
,
openVideoModal
,
// 新增
enrichLogs
,
enrichLogs
,
syncPageData
,
syncPageData
,
...
...
web/src/hooks/tokens/useTokensData.js
View file @
1dcb9a01
...
@@ -29,7 +29,7 @@ import {
...
@@ -29,7 +29,7 @@ import {
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
import
{
useTableCompactMode
}
from
'../common/useTableCompactMode'
;
export
const
useTokensData
=
()
=>
{
export
const
useTokensData
=
(
openFluentNotification
)
=>
{
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
// Basic state
// Basic state
...
@@ -121,6 +121,10 @@ export const useTokensData = () => {
...
@@ -121,6 +121,10 @@ export const useTokensData = () => {
// Open link function for chat integrations
// Open link function for chat integrations
const
onOpenLink
=
async
(
type
,
url
,
record
)
=>
{
const
onOpenLink
=
async
(
type
,
url
,
record
)
=>
{
if
(
url
&&
url
.
startsWith
(
'fluent'
))
{
openFluentNotification
(
record
.
key
);
return
;
}
let
status
=
localStorage
.
getItem
(
'status'
);
let
status
=
localStorage
.
getItem
(
'status'
);
let
serverAddress
=
''
;
let
serverAddress
=
''
;
if
(
status
)
{
if
(
status
)
{
...
...
web/src/i18n/locales/en.json
View file @
1dcb9a01
...
@@ -1804,5 +1804,11 @@
...
@@ -1804,5 +1804,11 @@
"已选择 {{selected}} / {{total}}"
:
"Selected {{selected}} / {{total}}"
,
"已选择 {{selected}} / {{total}}"
:
"Selected {{selected}} / {{total}}"
,
"新获取的模型"
:
"New models"
,
"新获取的模型"
:
"New models"
,
"已有的模型"
:
"Existing models"
,
"已有的模型"
:
"Existing models"
,
"搜索模型"
:
"Search models"
"搜索模型"
:
"Search models"
,
"缓存: {{cacheRatio}}"
:
"Cache: {{cacheRatio}}"
,
"缓存创建: {{cacheCreationRatio}}"
:
"Cache creation: {{cacheCreationRatio}}"
,
"图片输入: {{imageRatio}}"
:
"Image input: {{imageRatio}}"
,
"系统提示覆盖"
:
"System prompt override"
,
"模型: {{ratio}}"
:
"Model: {{ratio}}"
,
"专属倍率"
:
"Exclusive group ratio"
}
}
\ No newline at end of file
web/src/index.css
View file @
1dcb9a01
...
@@ -655,7 +655,7 @@ html:not(.dark) .blur-ball-teal {
...
@@ -655,7 +655,7 @@ html:not(.dark) .blur-ball-teal {
}
}
.pricing-search-header
{
.pricing-search-header
{
padding
:
1
6px
24px
;
padding
:
1
rem
;
border-bottom
:
1px
solid
var
(
--semi-color-border
);
border-bottom
:
1px
solid
var
(
--semi-color-border
);
background-color
:
var
(
--semi-color-bg-0
);
background-color
:
var
(
--semi-color-bg-0
);
flex-shrink
:
0
;
flex-shrink
:
0
;
...
...
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