mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-02 19:45:51 +00:00
Compare commits
21 Commits
v0.9.3-pat
...
v0.9.4-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e24f13a277 | ||
|
|
d67c57eaa5 | ||
|
|
60dc910a27 | ||
|
|
629a534798 | ||
|
|
15a7edf6d6 | ||
|
|
cdd2eb517e | ||
|
|
1a398bbc40 | ||
|
|
581c51f312 | ||
|
|
8f00af181b | ||
|
|
0c417e8ec6 | ||
|
|
f930cdbb51 | ||
|
|
a610ef48e4 | ||
|
|
ddf5c85b81 | ||
|
|
ec590d1075 | ||
|
|
a8c9b24c7e | ||
|
|
2389dbafc5 | ||
|
|
6ef95c97cc | ||
|
|
2397ec8075 | ||
|
|
c24608730b | ||
|
|
ca9ee54fba | ||
|
|
bb0ed4dddf |
91
.github/workflows/sync-to-gitee.yml
vendored
Normal file
91
.github/workflows/sync-to-gitee.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Sync Release to Gitee
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Release Tag to sync (e.g. v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# 配置你的 Gitee 仓库信息
|
||||
env:
|
||||
GITEE_OWNER: 'QuantumNous' # 修改为你的 Gitee 用户名
|
||||
GITEE_REPO: 'new-api' # 修改为你的 Gitee 仓库名
|
||||
|
||||
jobs:
|
||||
sync-to-gitee:
|
||||
runs-on: sync
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Info
|
||||
id: release_info
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.event.inputs.tag_name }}
|
||||
run: |
|
||||
# 获取 release 信息
|
||||
RELEASE_INFO=$(gh release view "$TAG_NAME" --json name,body,tagName,targetCommitish)
|
||||
|
||||
RELEASE_NAME=$(echo "$RELEASE_INFO" | jq -r '.name')
|
||||
TARGET_COMMITISH=$(echo "$RELEASE_INFO" | jq -r '.targetCommitish')
|
||||
|
||||
# 使用多行字符串输出
|
||||
{
|
||||
echo "release_name=$RELEASE_NAME"
|
||||
echo "target_commitish=$TARGET_COMMITISH"
|
||||
echo "release_body<<EOF"
|
||||
echo "$RELEASE_INFO" | jq -r '.body'
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
# 下载 release 的所有附件
|
||||
gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download"
|
||||
|
||||
# 列出下载的文件
|
||||
ls -la ./release_assets/ || echo "No assets directory"
|
||||
|
||||
- name: Create Gitee Release
|
||||
id: create_release
|
||||
uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0
|
||||
with:
|
||||
gitee_action: create_release
|
||||
gitee_owner: ${{ env.GITEE_OWNER }}
|
||||
gitee_repo: ${{ env.GITEE_REPO }}
|
||||
gitee_token: ${{ secrets.GITEE_TOKEN }}
|
||||
gitee_tag_name: ${{ github.event.inputs.tag_name }}
|
||||
gitee_release_name: ${{ steps.release_info.outputs.release_name }}
|
||||
gitee_release_body: ${{ steps.release_info.outputs.release_body }}
|
||||
gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }}
|
||||
|
||||
- name: Upload Assets to Gitee
|
||||
if: hashFiles('release_assets/*') != ''
|
||||
uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0
|
||||
with:
|
||||
gitee_action: upload_asset
|
||||
gitee_owner: ${{ env.GITEE_OWNER }}
|
||||
gitee_repo: ${{ env.GITEE_REPO }}
|
||||
gitee_token: ${{ secrets.GITEE_TOKEN }}
|
||||
gitee_release_id: ${{ steps.create_release.outputs.release-id }}
|
||||
gitee_upload_retry_times: 3
|
||||
gitee_files: |
|
||||
release_assets/*
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf release_assets/
|
||||
|
||||
- name: Summary
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!"
|
||||
echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}"
|
||||
|
||||
@@ -127,6 +127,14 @@ func GetAuthHeader(token string) http.Header {
|
||||
return h
|
||||
}
|
||||
|
||||
// GetClaudeAuthHeader get claude auth header
|
||||
func GetClaudeAuthHeader(token string) http.Header {
|
||||
h := http.Header{}
|
||||
h.Add("x-api-key", token)
|
||||
h.Add("anthropic-version", "2023-06-01")
|
||||
return h
|
||||
}
|
||||
|
||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -198,9 +198,10 @@ func FetchUpstreamModels(c *gin.Context) {
|
||||
// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
|
||||
var body []byte
|
||||
key := strings.Split(channel.Key, "\n")[0]
|
||||
if channel.Type == constant.ChannelTypeGemini {
|
||||
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) // Use AuthHeader since Gemini now forces it
|
||||
} else {
|
||||
switch channel.Type {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
body, err = GetResponseBody("GET", url, channel, GetClaudeAuthHeader(key))
|
||||
default:
|
||||
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key))
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -183,12 +183,13 @@ func RequestEpay(c *gin.Context) {
|
||||
amount = dAmount.Div(dQuotaPerUnit).IntPart()
|
||||
}
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: amount,
|
||||
Money: payMoney,
|
||||
TradeNo: tradeNo,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: "pending",
|
||||
UserId: id,
|
||||
Amount: amount,
|
||||
Money: payMoney,
|
||||
TradeNo: tradeNo,
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: "pending",
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
@@ -236,8 +237,8 @@ func EpayNotify(c *gin.Context) {
|
||||
_, err := c.Writer.Write([]byte("fail"))
|
||||
if err != nil {
|
||||
log.Println("易支付回调写入失败")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
verifyInfo, err := client.Verify(params)
|
||||
if err == nil && verifyInfo.VerifyStatus {
|
||||
@@ -313,3 +314,76 @@ func RequestAmount(c *gin.Context) {
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
|
||||
}
|
||||
|
||||
func GetUserTopUps(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
var (
|
||||
topups []*model.TopUp
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if keyword != "" {
|
||||
topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo)
|
||||
} else {
|
||||
topups, total, err = model.GetUserTopUps(userId, pageInfo)
|
||||
}
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(topups)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
// GetAllTopUps 管理员获取全平台充值记录
|
||||
func GetAllTopUps(c *gin.Context) {
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
var (
|
||||
topups []*model.TopUp
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if keyword != "" {
|
||||
topups, total, err = model.SearchAllTopUps(keyword, pageInfo)
|
||||
} else {
|
||||
topups, total, err = model.GetAllTopUps(pageInfo)
|
||||
}
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(topups)
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
}
|
||||
|
||||
type AdminCompleteTopupRequest struct {
|
||||
TradeNo string `json:"trade_no"`
|
||||
}
|
||||
|
||||
// AdminCompleteTopUp 管理员补单接口
|
||||
func AdminCompleteTopUp(c *gin.Context) {
|
||||
var req AdminCompleteTopupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
|
||||
common.ApiErrorMsg(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 订单级互斥,防止并发补单
|
||||
LockOrder(req.TradeNo)
|
||||
defer UnlockOrder(req.TradeNo)
|
||||
|
||||
if err := model.ManualCompleteTopUp(req.TradeNo); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, nil)
|
||||
}
|
||||
|
||||
@@ -83,12 +83,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
|
||||
}
|
||||
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: req.Amount,
|
||||
Money: chargedMoney,
|
||||
TradeNo: referenceId,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
UserId: id,
|
||||
Amount: req.Amount,
|
||||
Money: chargedMoney,
|
||||
TradeNo: referenceId,
|
||||
PaymentMethod: PaymentMethodStripe,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
|
||||
@@ -329,6 +329,7 @@ type GeminiImageParameters struct {
|
||||
SampleCount int `json:"sampleCount,omitempty"`
|
||||
AspectRatio string `json:"aspectRatio,omitempty"`
|
||||
PersonGeneration string `json:"personGeneration,omitempty"`
|
||||
ImageSize string `json:"imageSize,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiImageResponse struct {
|
||||
|
||||
474
electron/package-lock.json
generated
474
electron/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "28.3.3",
|
||||
"electron": "35.7.5",
|
||||
"electron-builder": "^24.9.1"
|
||||
}
|
||||
},
|
||||
@@ -590,13 +590,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.129",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz",
|
||||
"integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==",
|
||||
"version": "22.18.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
|
||||
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
@@ -824,6 +824,85 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
"buffer-crc32": "^0.2.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"readdir-glob": "^1.1.2",
|
||||
"tar-stream": "^2.2.0",
|
||||
"zip-stream": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -915,6 +994,19 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -971,7 +1063,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -1276,6 +1367,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1346,8 +1454,7 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crc": {
|
||||
"version": "3.8.0",
|
||||
@@ -1360,6 +1467,35 @@
|
||||
"buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/crc32-stream": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
@@ -1681,15 +1817,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "28.3.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz",
|
||||
"integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==",
|
||||
"version": "35.7.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz",
|
||||
"integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1726,6 +1862,61 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
|
||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"archiver": "^5.3.1",
|
||||
"builder-util": "24.13.1",
|
||||
"fs-extra": "^10.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -2033,6 +2224,14 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
@@ -2478,8 +2677,7 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
@@ -2523,6 +2721,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/isbinaryfile": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz",
|
||||
@@ -2652,6 +2858,56 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -2659,6 +2915,46 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
@@ -2840,6 +3136,17 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
@@ -2964,6 +3271,14 @@
|
||||
"node": ">=10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -3040,6 +3355,33 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -3099,6 +3441,28 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -3287,6 +3651,17 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -3389,6 +3764,24 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/temp-file": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
||||
@@ -3497,9 +3890,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3530,6 +3923,14 @@
|
||||
"dev": true,
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
||||
@@ -3672,6 +4073,45 @@
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/archiver-utils": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "28.3.3",
|
||||
"electron": "35.7.5",
|
||||
"electron-builder": "^24.9.1"
|
||||
},
|
||||
"build": {
|
||||
|
||||
3
go.mod
3
go.mod
@@ -21,7 +21,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-webauthn/webauthn v0.14.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
@@ -68,7 +68,6 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.25 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
|
||||
221
model/topup.go
221
model/topup.go
@@ -6,18 +6,20 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/logger"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TopUp struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (topUp *TopUp) Insert() error {
|
||||
@@ -99,3 +101,206 @@ func Recharge(referenceId string, customerId string) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
|
||||
// Start transaction
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, 0, tx.Error
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get total count within transaction
|
||||
err = tx.Model(&TopUp{}).Where("user_id = ?", userId).Count(&total).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Get paginated topups within same transaction
|
||||
err = tx.Where("user_id = ?", userId).Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return topups, total, nil
|
||||
}
|
||||
|
||||
// GetAllTopUps 获取全平台的充值记录(管理员使用)
|
||||
func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, 0, tx.Error
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if err = tx.Model(&TopUp{}).Count(&total).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = tx.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return topups, total, nil
|
||||
}
|
||||
|
||||
// SearchUserTopUps 按订单号搜索某用户的充值记录
|
||||
func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, 0, tx.Error
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
query := tx.Model(&TopUp{}).Where("user_id = ?", userId)
|
||||
if keyword != "" {
|
||||
like := "%%" + keyword + "%%"
|
||||
query = query.Where("trade_no LIKE ?", like)
|
||||
}
|
||||
|
||||
if err = query.Count(&total).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return topups, total, nil
|
||||
}
|
||||
|
||||
// SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用)
|
||||
func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) {
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, 0, tx.Error
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
query := tx.Model(&TopUp{})
|
||||
if keyword != "" {
|
||||
like := "%%" + keyword + "%%"
|
||||
query = query.Where("trade_no LIKE ?", like)
|
||||
}
|
||||
|
||||
if err = query.Count(&total).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return topups, total, nil
|
||||
}
|
||||
|
||||
// ManualCompleteTopUp 管理员手动完成订单并给用户充值
|
||||
func ManualCompleteTopUp(tradeNo string) error {
|
||||
if tradeNo == "" {
|
||||
return errors.New("未提供订单号")
|
||||
}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
if common.UsingPostgreSQL {
|
||||
refCol = `"trade_no"`
|
||||
}
|
||||
|
||||
var userId int
|
||||
var quotaToAdd int
|
||||
var payMoney float64
|
||||
|
||||
err := DB.Transaction(func(tx *gorm.DB) error {
|
||||
topUp := &TopUp{}
|
||||
// 行级锁,避免并发补单
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil {
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
// 幂等处理:已成功直接返回
|
||||
if topUp.Status == common.TopUpStatusSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return errors.New("订单状态不是待支付,无法补单")
|
||||
}
|
||||
|
||||
// 计算应充值额度:
|
||||
// - Stripe 订单:Money 代表经分组倍率换算后的美元数量,直接 * QuotaPerUnit
|
||||
// - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit
|
||||
if topUp.PaymentMethod == "stripe" {
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart())
|
||||
} else {
|
||||
dAmount := decimal.NewFromInt(topUp.Amount)
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||||
}
|
||||
if quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
// 标记完成
|
||||
topUp.CompleteTime = common.GetTimestamp()
|
||||
topUp.Status = common.TopUpStatusSuccess
|
||||
if err := tx.Save(topUp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 增加用户额度(立即写库,保持一致性)
|
||||
if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId = topUp.UserId
|
||||
payMoney = topUp.Money
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 事务外记录日志,避免阻塞
|
||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,8 +67,12 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
aspectRatio = size
|
||||
} else {
|
||||
switch size {
|
||||
case "1024x1024":
|
||||
case "256x256", "512x512", "1024x1024":
|
||||
aspectRatio = "1:1"
|
||||
case "1536x1024":
|
||||
aspectRatio = "3:2"
|
||||
case "1024x1536":
|
||||
aspectRatio = "2:3"
|
||||
case "1024x1792":
|
||||
aspectRatio = "9:16"
|
||||
case "1792x1024":
|
||||
@@ -91,6 +95,28 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
},
|
||||
}
|
||||
|
||||
// Set imageSize when quality parameter is specified
|
||||
// Map quality parameter to imageSize (only supported by Standard and Ultra models)
|
||||
// quality values: auto, high, medium, low (for gpt-image-1), hd, standard (for dall-e-3)
|
||||
// imageSize values: 1K (default), 2K
|
||||
// https://ai.google.dev/gemini-api/docs/imagen
|
||||
// https://platform.openai.com/docs/api-reference/images/create
|
||||
if request.Quality != "" {
|
||||
imageSize := "1K" // default
|
||||
switch request.Quality {
|
||||
case "hd", "high":
|
||||
imageSize = "2K"
|
||||
case "2K":
|
||||
imageSize = "2K"
|
||||
case "standard", "medium", "low", "auto", "1K":
|
||||
imageSize = "1K"
|
||||
default:
|
||||
// unknown quality value, default to 1K
|
||||
imageSize = "1K"
|
||||
}
|
||||
geminiRequest.Parameters.ImageSize = imageSize
|
||||
}
|
||||
|
||||
return geminiRequest, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -163,13 +163,10 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
|
||||
if !containStreamUsage {
|
||||
usage = service.ResponseText2Usage(responseTextBuilder.String(), info.UpstreamModelName, info.PromptTokens)
|
||||
usage.CompletionTokens += toolCount * 7
|
||||
} else {
|
||||
if info.ChannelType == constant.ChannelTypeDeepSeek {
|
||||
if usage.PromptCacheHitTokens != 0 {
|
||||
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyUsagePostProcessing(info, usage, nil)
|
||||
|
||||
HandleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage)
|
||||
|
||||
return usage, nil
|
||||
@@ -233,6 +230,8 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
|
||||
usageModified = true
|
||||
}
|
||||
|
||||
applyUsagePostProcessing(info, &simpleResponse.Usage, responseBody)
|
||||
|
||||
switch info.RelayFormat {
|
||||
case types.RelayFormatOpenAI:
|
||||
if usageModified {
|
||||
@@ -631,5 +630,60 @@ func OpenaiHandlerWithUsage(c *gin.Context, info *relaycommon.RelayInfo, resp *h
|
||||
usageResp.PromptTokensDetails.ImageTokens += usageResp.InputTokensDetails.ImageTokens
|
||||
usageResp.PromptTokensDetails.TextTokens += usageResp.InputTokensDetails.TextTokens
|
||||
}
|
||||
applyUsagePostProcessing(info, &usageResp.Usage, responseBody)
|
||||
return &usageResp.Usage, nil
|
||||
}
|
||||
|
||||
func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, responseBody []byte) {
|
||||
if info == nil || usage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch info.ChannelType {
|
||||
case constant.ChannelTypeDeepSeek:
|
||||
if usage.PromptTokensDetails.CachedTokens == 0 && usage.PromptCacheHitTokens != 0 {
|
||||
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
|
||||
}
|
||||
case constant.ChannelTypeZhipu_v4:
|
||||
if usage.PromptTokensDetails.CachedTokens == 0 {
|
||||
if usage.InputTokensDetails != nil && usage.InputTokensDetails.CachedTokens > 0 {
|
||||
usage.PromptTokensDetails.CachedTokens = usage.InputTokensDetails.CachedTokens
|
||||
} else if cachedTokens, ok := extractCachedTokensFromBody(responseBody); ok {
|
||||
usage.PromptTokensDetails.CachedTokens = cachedTokens
|
||||
} else if usage.PromptCacheHitTokens > 0 {
|
||||
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractCachedTokensFromBody(body []byte) (int, bool) {
|
||||
if len(body) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Usage struct {
|
||||
PromptTokensDetails struct {
|
||||
CachedTokens *int `json:"cached_tokens"`
|
||||
} `json:"prompt_tokens_details"`
|
||||
CachedTokens *int `json:"cached_tokens"`
|
||||
PromptCacheHitTokens *int `json:"prompt_cache_hit_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if payload.Usage.PromptTokensDetails.CachedTokens != nil {
|
||||
return *payload.Usage.PromptTokensDetails.CachedTokens, true
|
||||
}
|
||||
if payload.Usage.CachedTokens != nil {
|
||||
return *payload.Usage.CachedTokens, true
|
||||
}
|
||||
if payload.Usage.PromptCacheHitTokens != nil {
|
||||
return *payload.Usage.PromptCacheHitTokens, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"one-api/constant"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/gopkg/cache/asynccache"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// https://open.bigmodel.cn/doc/api#chatglm_std
|
||||
|
||||
@@ -261,6 +261,7 @@ var streamSupportedChannels = map[int]bool{
|
||||
constant.ChannelTypeXai: true,
|
||||
constant.ChannelTypeDeepSeek: true,
|
||||
constant.ChannelTypeBaiduV2: true,
|
||||
constant.ChannelTypeZhipu_v4: true,
|
||||
}
|
||||
|
||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
|
||||
@@ -73,6 +73,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute.DELETE("/passkey", controller.PasskeyDelete)
|
||||
selfRoute.GET("/aff", controller.GetAffCode)
|
||||
selfRoute.GET("/topup/info", controller.GetTopUpInfo)
|
||||
selfRoute.GET("/topup/self", controller.GetUserTopUps)
|
||||
selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
|
||||
selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
|
||||
selfRoute.POST("/amount", controller.RequestAmount)
|
||||
@@ -93,6 +94,8 @@ func SetApiRouter(router *gin.Engine) {
|
||||
adminRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
adminRoute.GET("/", controller.GetAllUsers)
|
||||
adminRoute.GET("/topup", controller.GetAllTopUps)
|
||||
adminRoute.POST("/topup/complete", controller.AdminCompleteTopUp)
|
||||
adminRoute.GET("/search", controller.SearchUsers)
|
||||
adminRoute.GET("/:id", controller.GetUser)
|
||||
adminRoute.POST("/", controller.CreateUser)
|
||||
|
||||
38
web/bun.lock
38
web/bun.lock
@@ -10,7 +10,7 @@
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "^0.27.2",
|
||||
"axios": "1.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"dayjs": "^1.11.11",
|
||||
@@ -687,7 +687,7 @@
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||
|
||||
"axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="],
|
||||
"axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="],
|
||||
|
||||
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
|
||||
|
||||
@@ -713,6 +713,8 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
@@ -895,6 +897,8 @@
|
||||
|
||||
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.157", "", {}, "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w=="],
|
||||
@@ -907,6 +911,14 @@
|
||||
|
||||
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
|
||||
@@ -995,7 +1007,7 @@
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
||||
@@ -1019,6 +1031,10 @@
|
||||
|
||||
"geojson-linestring-dissolve": ["geojson-linestring-dissolve@0.0.1", "", {}, "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stdin": ["get-stdin@6.0.0", "", {}, "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="],
|
||||
|
||||
"get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
|
||||
@@ -1031,6 +1047,8 @@
|
||||
|
||||
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
@@ -1039,6 +1057,10 @@
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
||||
@@ -1229,6 +1251,8 @@
|
||||
|
||||
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||
@@ -1491,6 +1515,8 @@
|
||||
|
||||
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="],
|
||||
@@ -1505,7 +1531,7 @@
|
||||
|
||||
"rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="],
|
||||
|
||||
"rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
|
||||
"rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
|
||||
|
||||
"rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="],
|
||||
|
||||
@@ -1949,6 +1975,8 @@
|
||||
|
||||
"@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="],
|
||||
|
||||
"@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
|
||||
|
||||
"@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="],
|
||||
@@ -1965,8 +1993,6 @@
|
||||
|
||||
"@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="],
|
||||
|
||||
"antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
|
||||
|
||||
"antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "^0.27.2",
|
||||
"axios": "1.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"dayjs": "^1.11.11",
|
||||
|
||||
@@ -42,7 +42,12 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
|
||||
import { IconGithubLogo, IconMail, IconLock, IconKey } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconGithubLogo,
|
||||
IconMail,
|
||||
IconLock,
|
||||
IconKey,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import OIDCIcon from '../common/logo/OIDCIcon';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon';
|
||||
@@ -296,15 +301,22 @@ const LoginForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKeyOptions = prepareCredentialRequestOptions(data?.options || data?.publicKey || data);
|
||||
const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
|
||||
const publicKeyOptions = prepareCredentialRequestOptions(
|
||||
data?.options || data?.publicKey || data,
|
||||
);
|
||||
const assertion = await navigator.credentials.get({
|
||||
publicKey: publicKeyOptions,
|
||||
});
|
||||
const payload = buildAssertionResult(assertion);
|
||||
if (!payload) {
|
||||
showError('Passkey 验证失败,请重试');
|
||||
return;
|
||||
}
|
||||
|
||||
const finishRes = await API.post('/api/user/passkey/login/finish', payload);
|
||||
const finishRes = await API.post(
|
||||
'/api/user/passkey/login/finish',
|
||||
payload,
|
||||
);
|
||||
const finish = finishRes.data;
|
||||
if (finish.success) {
|
||||
userDispatch({ type: 'login', payload: finish.data });
|
||||
|
||||
@@ -58,7 +58,7 @@ const ChannelKeyViewExample = ({ channelId }) => {
|
||||
// 开始查看密钥流程
|
||||
const handleViewKey = async () => {
|
||||
const apiCall = createApiCalls.viewChannelKey(channelId);
|
||||
|
||||
|
||||
await startVerification(apiCall, {
|
||||
title: t('查看渠道密钥'),
|
||||
description: t('为了保护账户安全,请验证您的身份。'),
|
||||
@@ -69,11 +69,7 @@ const ChannelKeyViewExample = ({ channelId }) => {
|
||||
return (
|
||||
<>
|
||||
{/* 查看密钥按钮 */}
|
||||
<Button
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleViewKey}
|
||||
>
|
||||
<Button type='primary' theme='outline' onClick={handleViewKey}>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
|
||||
@@ -114,4 +110,4 @@ const ChannelKeyViewExample = ({ channelId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelKeyViewExample;
|
||||
export default ChannelKeyViewExample;
|
||||
|
||||
@@ -19,7 +19,16 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Button, Input, Typography, Tabs, TabPane, Space, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Input,
|
||||
Typography,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
|
||||
/**
|
||||
* 通用安全验证模态框组件
|
||||
@@ -78,9 +87,7 @@ const SecureVerificationModal = ({
|
||||
title={title || t('安全验证')}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Button onClick={onCancel}>{t('确定')}</Button>
|
||||
}
|
||||
footer={<Button onClick={onCancel}>{t('确定')}</Button>}
|
||||
width={500}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
@@ -123,21 +130,21 @@ const SecureVerificationModal = ({
|
||||
width={460}
|
||||
centered
|
||||
style={{
|
||||
maxWidth: 'calc(100vw - 32px)'
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: '20px 24px'
|
||||
padding: '20px 24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
{/* 描述信息 */}
|
||||
{description && (
|
||||
<Typography.Paragraph
|
||||
type="tertiary"
|
||||
type='tertiary'
|
||||
style={{
|
||||
margin: '0 0 20px 0',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6'
|
||||
lineHeight: '1.6',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
@@ -153,10 +160,7 @@ const SecureVerificationModal = ({
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{has2FA && (
|
||||
<TabPane
|
||||
tab={t('两步验证')}
|
||||
itemKey='2fa'
|
||||
>
|
||||
<TabPane tab={t('两步验证')} itemKey='2fa'>
|
||||
<div style={{ paddingTop: '20px' }}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<Input
|
||||
@@ -169,8 +173,21 @@ const SecureVerificationModal = ({
|
||||
autoFocus={method === '2fa'}
|
||||
disabled={loading}
|
||||
prefix={
|
||||
<svg style={{ width: 16, height: 16, marginRight: 8, flexShrink: 0 }} fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path fillRule='evenodd' d='M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z' clipRule='evenodd' />
|
||||
<svg
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginRight: 8,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
fill='currentColor'
|
||||
viewBox='0 0 20 20'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
@@ -178,24 +195,26 @@ const SecureVerificationModal = ({
|
||||
</div>
|
||||
|
||||
<Typography.Text
|
||||
type="tertiary"
|
||||
size="small"
|
||||
type='tertiary'
|
||||
size='small'
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: '20px',
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.5'
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
>
|
||||
{t('从认证器应用中获取验证码,或使用备用码')}
|
||||
</Typography.Text>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={loading}>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
@@ -214,31 +233,47 @@ const SecureVerificationModal = ({
|
||||
)}
|
||||
|
||||
{hasPasskey && passkeySupported && (
|
||||
<TabPane
|
||||
tab={t('Passkey')}
|
||||
itemKey='passkey'
|
||||
>
|
||||
<TabPane tab={t('Passkey')} itemKey='passkey'>
|
||||
<div style={{ paddingTop: '20px' }}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px 16px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
margin: '0 auto 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--semi-color-primary-light-default)',
|
||||
}}>
|
||||
<svg style={{ width: 28, height: 28, color: 'var(--semi-color-primary)' }} fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path fillRule='evenodd' d='M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z' clipRule='evenodd' />
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px 16px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
margin: '0 auto 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--semi-color-primary-light-default)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
color: 'var(--semi-color-primary)',
|
||||
}}
|
||||
fill='currentColor'
|
||||
viewBox='0 0 20 20'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Typography.Title heading={5} style={{ margin: '0 0 8px', fontSize: '16px' }}>
|
||||
<Typography.Title
|
||||
heading={5}
|
||||
style={{ margin: '0 0 8px', fontSize: '16px' }}
|
||||
>
|
||||
{t('使用 Passkey 验证')}
|
||||
</Typography.Title>
|
||||
<Typography.Text
|
||||
@@ -247,19 +282,21 @@ const SecureVerificationModal = ({
|
||||
display: 'block',
|
||||
margin: 0,
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.5'
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
>
|
||||
{t('点击验证按钮,使用您的生物特征或安全密钥')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={loading}>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
@@ -282,4 +319,4 @@ const SecureVerificationModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SecureVerificationModal;
|
||||
export default SecureVerificationModal;
|
||||
|
||||
@@ -155,9 +155,7 @@ const PersonalSetting = () => {
|
||||
gotifyUrl: settings.gotify_url || '',
|
||||
gotifyToken: settings.gotify_token || '',
|
||||
gotifyPriority:
|
||||
settings.gotify_priority !== undefined
|
||||
? settings.gotify_priority
|
||||
: 5,
|
||||
settings.gotify_priority !== undefined ? settings.gotify_priority : 5,
|
||||
acceptUnsetModelRatioModel:
|
||||
settings.accept_unset_model_ratio_model || false,
|
||||
recordIpLog: settings.record_ip_log || false,
|
||||
@@ -214,7 +212,9 @@ const PersonalSetting = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data);
|
||||
const publicKey = prepareCredentialCreationOptions(
|
||||
data?.options || data?.publicKey || data,
|
||||
);
|
||||
const credential = await navigator.credentials.create({ publicKey });
|
||||
const payload = buildRegistrationResult(credential);
|
||||
if (!payload) {
|
||||
@@ -222,7 +222,10 @@ const PersonalSetting = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const finishRes = await API.post('/api/user/passkey/register/finish', payload);
|
||||
const finishRes = await API.post(
|
||||
'/api/user/passkey/register/finish',
|
||||
payload,
|
||||
);
|
||||
if (finishRes.data.success) {
|
||||
showSuccess(t('Passkey 注册成功'));
|
||||
await loadPasskeyStatus();
|
||||
|
||||
@@ -615,7 +615,10 @@ const SystemSetting = () => {
|
||||
|
||||
options.push({
|
||||
key: 'passkey.rp_display_name',
|
||||
value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '',
|
||||
value:
|
||||
formValues['passkey.rp_display_name'] ||
|
||||
inputs['passkey.rp_display_name'] ||
|
||||
'',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.rp_id',
|
||||
@@ -623,11 +626,17 @@ const SystemSetting = () => {
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.user_verification',
|
||||
value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred',
|
||||
value:
|
||||
formValues['passkey.user_verification'] ||
|
||||
inputs['passkey.user_verification'] ||
|
||||
'preferred',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.attachment_preference',
|
||||
value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '',
|
||||
value:
|
||||
formValues['passkey.attachment_preference'] ||
|
||||
inputs['passkey.attachment_preference'] ||
|
||||
'',
|
||||
});
|
||||
options.push({
|
||||
key: 'passkey.origins',
|
||||
@@ -1044,7 +1053,9 @@ const SystemSetting = () => {
|
||||
<Text>{t('用以支持基于 WebAuthn 的无密码登录注册')}</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t('Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式')}
|
||||
description={t(
|
||||
'Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式',
|
||||
)}
|
||||
style={{ marginBottom: 20, marginTop: 16 }}
|
||||
/>
|
||||
<Row
|
||||
@@ -1070,7 +1081,9 @@ const SystemSetting = () => {
|
||||
field="['passkey.rp_display_name']"
|
||||
label={t('服务显示名称')}
|
||||
placeholder={t('默认使用系统名称')}
|
||||
extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')}
|
||||
extraText={t(
|
||||
"用户注册时看到的网站名称,比如'我的网站'",
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||
@@ -1078,7 +1091,9 @@ const SystemSetting = () => {
|
||||
field="['passkey.rp_id']"
|
||||
label={t('网站域名标识')}
|
||||
placeholder={t('例如:example.com')}
|
||||
extraText={t('留空则默认使用服务器地址,注意不能携带http://或者https://')}
|
||||
extraText={t(
|
||||
'留空则默认使用服务器地址,注意不能携带http://或者https://',
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -1092,7 +1107,10 @@ const SystemSetting = () => {
|
||||
label={t('安全验证级别')}
|
||||
placeholder={t('是否要求指纹/面容等生物识别')}
|
||||
optionList={[
|
||||
{ label: t('推荐使用(用户可选)'), value: 'preferred' },
|
||||
{
|
||||
label: t('推荐使用(用户可选)'),
|
||||
value: 'preferred',
|
||||
},
|
||||
{ label: t('强制要求'), value: 'required' },
|
||||
{ label: t('不建议使用'), value: 'discouraged' },
|
||||
]}
|
||||
@@ -1109,7 +1127,9 @@ const SystemSetting = () => {
|
||||
{ label: t('本设备内置'), value: 'platform' },
|
||||
{ label: t('外接设备'), value: 'cross-platform' },
|
||||
]}
|
||||
extraText={t('本设备:手机指纹/面容,外接:USB安全密钥')}
|
||||
extraText={t(
|
||||
'本设备:手机指纹/面容,外接:USB安全密钥',
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -1123,7 +1143,10 @@ const SystemSetting = () => {
|
||||
noLabel
|
||||
extraText={t('仅用于开发环境,生产环境应使用 HTTPS')}
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('passkey.allow_insecure_origin', e)
|
||||
handleCheckboxChange(
|
||||
'passkey.allow_insecure_origin',
|
||||
e,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('允许不安全的 Origin(HTTP)')}
|
||||
@@ -1139,11 +1162,16 @@ const SystemSetting = () => {
|
||||
field="['passkey.origins']"
|
||||
label={t('允许的 Origins')}
|
||||
placeholder={t('填写带https的域名,逗号分隔')}
|
||||
extraText={t('为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https')}
|
||||
extraText={t(
|
||||
'为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https',
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitPasskeySettings} style={{ marginTop: 16 }}>
|
||||
<Button
|
||||
onClick={submitPasskeySettings}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
{t('保存 Passkey 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
|
||||
@@ -535,7 +535,9 @@ const AccountManagement = ({
|
||||
? () => {
|
||||
Modal.confirm({
|
||||
title: t('确认解绑 Passkey'),
|
||||
content: t('解绑后将无法使用 Passkey 登录,确定要继续吗?'),
|
||||
content: t(
|
||||
'解绑后将无法使用 Passkey 登录,确定要继续吗?',
|
||||
),
|
||||
okText: t('确认解绑'),
|
||||
cancelText: t('取消'),
|
||||
okType: 'danger',
|
||||
@@ -547,7 +549,11 @@ const AccountManagement = ({
|
||||
className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`}
|
||||
icon={<IconKey />}
|
||||
disabled={!passkeySupported && !passkeyEnabled}
|
||||
loading={passkeyEnabled ? passkeyDeleteLoading : passkeyRegisterLoading}
|
||||
loading={
|
||||
passkeyEnabled
|
||||
? passkeyDeleteLoading
|
||||
: passkeyRegisterLoading
|
||||
}
|
||||
>
|
||||
{passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')}
|
||||
</Button>
|
||||
|
||||
@@ -621,7 +621,9 @@ const NotificationSettings = ({
|
||||
},
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: t('Gotify服务器地址必须以http://或https://开头'),
|
||||
message: t(
|
||||
'Gotify服务器地址必须以http://或https://开头',
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -678,9 +680,7 @@ const NotificationSettings = ({
|
||||
'复制应用的令牌(Token)并填写到上方的应用令牌字段',
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
3. {t('填写Gotify服务器的完整URL地址')}
|
||||
</div>
|
||||
<div>3. {t('填写Gotify服务器的完整URL地址')}</div>
|
||||
<div className='mt-3 pt-3 border-t border-gray-200'>
|
||||
<span className='text-gray-400'>
|
||||
{t('更多信息请参考')}
|
||||
|
||||
@@ -26,8 +26,9 @@ import { Banner } from '@douyinfe/semi-ui';
|
||||
*/
|
||||
const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
||||
// 检测是否在 Electron 环境中运行
|
||||
const isElectron = typeof window !== 'undefined' && window.electron?.isElectron;
|
||||
|
||||
const isElectron =
|
||||
typeof window !== 'undefined' && window.electron?.isElectron;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 数据库警告 */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -119,8 +119,19 @@ const EditTagModal = (props) => {
|
||||
localModels = ['suno_music', 'suno_lyrics'];
|
||||
break;
|
||||
case 53:
|
||||
localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
|
||||
break;
|
||||
localModels = [
|
||||
'NousResearch/Hermes-4-405B-FP8',
|
||||
'Qwen/Qwen3-235B-A22B-Thinking-2507',
|
||||
'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8',
|
||||
'Qwen/Qwen3-235B-A22B-Instruct-2507',
|
||||
'zai-org/GLM-4.5-FP8',
|
||||
'openai/gpt-oss-120b',
|
||||
'deepseek-ai/DeepSeek-R1-0528',
|
||||
'deepseek-ai/DeepSeek-R1',
|
||||
'deepseek-ai/DeepSeek-V3-0324',
|
||||
'deepseek-ai/DeepSeek-V3.1',
|
||||
];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
break;
|
||||
|
||||
@@ -67,9 +67,15 @@ const ModelTestModal = ({
|
||||
{ value: 'openai', label: 'OpenAI (/v1/chat/completions)' },
|
||||
{ value: 'openai-response', label: 'OpenAI Response (/v1/responses)' },
|
||||
{ value: 'anthropic', label: 'Anthropic (/v1/messages)' },
|
||||
{ value: 'gemini', label: 'Gemini (/v1beta/models/{model}:generateContent)' },
|
||||
{
|
||||
value: 'gemini',
|
||||
label: 'Gemini (/v1beta/models/{model}:generateContent)',
|
||||
},
|
||||
{ value: 'jina-rerank', label: 'Jina Rerank (/rerank)' },
|
||||
{ value: 'image-generation', label: t('图像生成') + ' (/v1/images/generations)' },
|
||||
{
|
||||
value: 'image-generation',
|
||||
label: t('图像生成') + ' (/v1/images/generations)',
|
||||
},
|
||||
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
|
||||
];
|
||||
|
||||
@@ -166,7 +172,13 @@ const ModelTestModal = ({
|
||||
return (
|
||||
<Button
|
||||
type='tertiary'
|
||||
onClick={() => testChannel(currentTestChannel, record.model, selectedEndpointType)}
|
||||
onClick={() =>
|
||||
testChannel(
|
||||
currentTestChannel,
|
||||
record.model,
|
||||
selectedEndpointType,
|
||||
)
|
||||
}
|
||||
loading={isTesting}
|
||||
size='small'
|
||||
>
|
||||
|
||||
@@ -279,16 +279,8 @@ const renderOperations = (
|
||||
>
|
||||
{t('降级')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={moreMenu}
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
>
|
||||
<Button
|
||||
type='tertiary'
|
||||
size='small'
|
||||
icon={<IconMore />}
|
||||
/>
|
||||
<Dropdown menu={moreMenu} trigger='click' position='bottomRight'>
|
||||
<Button type='tertiary' size='small' icon={<IconMore />} />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -30,10 +30,11 @@ const ResetPasskeyModal = ({ visible, onCancel, onConfirm, user, t }) => {
|
||||
type='warning'
|
||||
>
|
||||
{t('此操作将解绑用户当前的 Passkey,下次登录需要重新注册。')}{' '}
|
||||
{user?.username ? t('目标用户:{{username}}', { username: user.username }) : ''}
|
||||
{user?.username
|
||||
? t('目标用户:{{username}}', { username: user.username })
|
||||
: ''}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasskeyModal;
|
||||
|
||||
|
||||
@@ -29,11 +29,14 @@ const ResetTwoFAModal = ({ visible, onCancel, onConfirm, user, t }) => {
|
||||
onOk={onConfirm}
|
||||
type='warning'
|
||||
>
|
||||
{t('此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。')}{' '}
|
||||
{user?.username ? t('目标用户:{{username}}', { username: user.username }) : ''}
|
||||
{t(
|
||||
'此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。',
|
||||
)}{' '}
|
||||
{user?.username
|
||||
? t('目标用户:{{username}}', { username: user.username })
|
||||
: ''}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetTwoFAModal;
|
||||
|
||||
|
||||
@@ -34,7 +34,14 @@ import {
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
|
||||
import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
CreditCard,
|
||||
Coins,
|
||||
Wallet,
|
||||
BarChart2,
|
||||
TrendingUp,
|
||||
Receipt,
|
||||
} from 'lucide-react';
|
||||
import { IconGift } from '@douyinfe/semi-icons';
|
||||
import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime';
|
||||
import { getCurrencyConfig } from '../../helpers/render';
|
||||
@@ -72,6 +79,7 @@ const RechargeCard = ({
|
||||
renderQuota,
|
||||
statusLoading,
|
||||
topupInfo,
|
||||
onOpenHistory,
|
||||
}) => {
|
||||
const onlineFormApiRef = useRef(null);
|
||||
const redeemFormApiRef = useRef(null);
|
||||
@@ -79,16 +87,25 @@ const RechargeCard = ({
|
||||
return (
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* 卡片头部 */}
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
||||
<CreditCard size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('账户充值')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs'>{t('多种充值方式,安全便捷')}</div>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<div className='flex items-center'>
|
||||
<Avatar size='small' color='blue' className='mr-3 shadow-md'>
|
||||
<CreditCard size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('账户充值')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs'>{t('多种充值方式,安全便捷')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
icon={<Receipt size={16} />}
|
||||
theme='solid'
|
||||
onClick={onOpenHistory}
|
||||
>
|
||||
{t('账单')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
@@ -339,16 +356,22 @@ const RechargeCard = ({
|
||||
)}
|
||||
|
||||
{(enableOnlineTopUp || enableStripeTopUp) && (
|
||||
<Form.Slot
|
||||
<Form.Slot
|
||||
label={
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{t('选择充值额度')}</span>
|
||||
{(() => {
|
||||
const { symbol, rate, type } = getCurrencyConfig();
|
||||
if (type === 'USD') return null;
|
||||
|
||||
|
||||
return (
|
||||
<span style={{ color: 'var(--semi-color-text-2)', fontSize: '12px', fontWeight: 'normal' }}>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--semi-color-text-2)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
>
|
||||
(1 $ = {rate.toFixed(2)} {symbol})
|
||||
</span>
|
||||
);
|
||||
@@ -378,11 +401,11 @@ const RechargeCard = ({
|
||||
usdRate = s?.usd_exchange_rate || 7;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
|
||||
let displayValue = preset.value; // 显示的数量
|
||||
let displayActualPay = actualPay;
|
||||
let displaySave = save;
|
||||
|
||||
|
||||
if (type === 'USD') {
|
||||
// 数量保持USD,价格从CNY转USD
|
||||
displayActualPay = actualPay / usdRate;
|
||||
@@ -444,7 +467,8 @@ const RechargeCard = ({
|
||||
margin: '4px 0',
|
||||
}}
|
||||
>
|
||||
{t('实付')} {symbol}{displayActualPay.toFixed(2)},
|
||||
{t('实付')} {symbol}
|
||||
{displayActualPay.toFixed(2)},
|
||||
{hasDiscount
|
||||
? `${t('节省')} ${symbol}${displaySave.toFixed(2)}`
|
||||
: `${t('节省')} ${symbol}0.00`}
|
||||
|
||||
@@ -37,6 +37,7 @@ import RechargeCard from './RechargeCard';
|
||||
import InvitationCard from './InvitationCard';
|
||||
import TransferModal from './modals/TransferModal';
|
||||
import PaymentConfirmModal from './modals/PaymentConfirmModal';
|
||||
import TopupHistoryModal from './modals/TopupHistoryModal';
|
||||
|
||||
const TopUp = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -77,6 +78,9 @@ const TopUp = () => {
|
||||
const [openTransfer, setOpenTransfer] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState(0);
|
||||
|
||||
// 账单Modal状态
|
||||
const [openHistory, setOpenHistory] = useState(false);
|
||||
|
||||
// 预设充值额度选项
|
||||
const [presetAmounts, setPresetAmounts] = useState([]);
|
||||
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||
@@ -488,6 +492,14 @@ const TopUp = () => {
|
||||
setOpenTransfer(false);
|
||||
};
|
||||
|
||||
const handleOpenHistory = () => {
|
||||
setOpenHistory(true);
|
||||
};
|
||||
|
||||
const handleHistoryCancel = () => {
|
||||
setOpenHistory(false);
|
||||
};
|
||||
|
||||
// 选择预设充值额度
|
||||
const selectPresetAmount = (preset) => {
|
||||
setTopUpCount(preset.value);
|
||||
@@ -544,6 +556,13 @@ const TopUp = () => {
|
||||
discountRate={topupInfo?.discount?.[topUpCount] || 1.0}
|
||||
/>
|
||||
|
||||
{/* 充值账单模态框 */}
|
||||
<TopupHistoryModal
|
||||
visible={openHistory}
|
||||
onCancel={handleHistoryCancel}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* 用户信息头部 */}
|
||||
<div className='space-y-6'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
||||
@@ -580,6 +599,7 @@ const TopUp = () => {
|
||||
renderQuota={renderQuota}
|
||||
statusLoading={statusLoading}
|
||||
topupInfo={topupInfo}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
273
web/src/components/topup/modals/TopupHistoryModal.jsx
Normal file
273
web/src/components/topup/modals/TopupHistoryModal.jsx
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Table,
|
||||
Badge,
|
||||
Typography,
|
||||
Toast,
|
||||
Empty,
|
||||
Button,
|
||||
Input,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { Coins } from 'lucide-react';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { API, timestamp2string } from '../../../helpers';
|
||||
import { isAdmin } from '../../../helpers/utils';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 状态映射配置
|
||||
const STATUS_CONFIG = {
|
||||
success: { type: 'success', key: '成功' },
|
||||
pending: { type: 'warning', key: '待支付' },
|
||||
expired: { type: 'danger', key: '已过期' },
|
||||
};
|
||||
|
||||
// 支付方式映射
|
||||
const PAYMENT_METHOD_MAP = {
|
||||
stripe: 'Stripe',
|
||||
alipay: '支付宝',
|
||||
wxpay: '微信',
|
||||
};
|
||||
|
||||
const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [topups, setTopups] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const loadTopups = async (currentPage, currentPageSize) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self';
|
||||
const qs =
|
||||
`p=${currentPage}&page_size=${currentPageSize}` +
|
||||
(keyword ? `&keyword=${encodeURIComponent(keyword)}` : '');
|
||||
const endpoint = `${base}?${qs}`;
|
||||
const res = await API.get(endpoint);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setTopups(data.items || []);
|
||||
setTotal(data.total || 0);
|
||||
} else {
|
||||
Toast.error({ content: message || t('加载失败') });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load topups error:', error);
|
||||
Toast.error({ content: t('加载账单失败') });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
loadTopups(page, pageSize);
|
||||
}
|
||||
}, [visible, page, pageSize, keyword]);
|
||||
|
||||
const handlePageChange = (currentPage) => {
|
||||
setPage(currentPage);
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (currentPageSize) => {
|
||||
setPageSize(currentPageSize);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleKeywordChange = (value) => {
|
||||
setKeyword(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
// 管理员补单
|
||||
const handleAdminComplete = async (tradeNo) => {
|
||||
try {
|
||||
const res = await API.post('/api/user/topup/complete', {
|
||||
trade_no: tradeNo,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
Toast.success({ content: t('补单成功') });
|
||||
await loadTopups(page, pageSize);
|
||||
} else {
|
||||
Toast.error({ content: message || t('补单失败') });
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error({ content: t('补单失败') });
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAdminComplete = (tradeNo) => {
|
||||
Modal.confirm({
|
||||
title: t('确认补单'),
|
||||
content: t('是否将该订单标记为成功并为用户入账?'),
|
||||
onOk: () => handleAdminComplete(tradeNo),
|
||||
});
|
||||
};
|
||||
|
||||
// 渲染状态徽章
|
||||
const renderStatusBadge = (status) => {
|
||||
const config = STATUS_CONFIG[status] || { type: 'primary', key: status };
|
||||
return (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Badge dot type={config.type} />
|
||||
<span>{t(config.key)}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染支付方式
|
||||
const renderPaymentMethod = (pm) => {
|
||||
const displayName = PAYMENT_METHOD_MAP[pm];
|
||||
return <Text>{displayName ? t(displayName) : pm || '-'}</Text>;
|
||||
};
|
||||
|
||||
// 检查是否为管理员
|
||||
const userIsAdmin = useMemo(() => isAdmin(), []);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const baseColumns = [
|
||||
{
|
||||
title: t('订单号'),
|
||||
dataIndex: 'trade_no',
|
||||
key: 'trade_no',
|
||||
render: (text) => <Text copyable>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('支付方式'),
|
||||
dataIndex: 'payment_method',
|
||||
key: 'payment_method',
|
||||
render: renderPaymentMethod,
|
||||
},
|
||||
{
|
||||
title: t('充值额度'),
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
render: (amount) => (
|
||||
<span className='flex items-center gap-1'>
|
||||
<Coins size={16} />
|
||||
<Text>{amount}</Text>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('支付金额'),
|
||||
dataIndex: 'money',
|
||||
key: 'money',
|
||||
render: (money) => <Text type='danger'>¥{money.toFixed(2)}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: renderStatusBadge,
|
||||
},
|
||||
];
|
||||
|
||||
// 管理员才显示操作列
|
||||
if (userIsAdmin) {
|
||||
baseColumns.push({
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => {
|
||||
if (record.status !== 'pending') return null;
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => confirmAdminComplete(record.trade_no)}
|
||||
>
|
||||
{t('补单')}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
baseColumns.push({
|
||||
title: t('创建时间'),
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
render: (time) => timestamp2string(time),
|
||||
});
|
||||
|
||||
return baseColumns;
|
||||
}, [t, userIsAdmin]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('充值账单')}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
<div className='mb-3'>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('订单号')}
|
||||
value={keyword}
|
||||
onChange={handleKeywordChange}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={topups}
|
||||
loading={loading}
|
||||
rowKey='id'
|
||||
pagination={{
|
||||
currentPage: page,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
pageSizeOpts: [10, 20, 50, 100],
|
||||
onPageChange: handlePageChange,
|
||||
onPageSizeChange: handlePageSizeChange,
|
||||
}}
|
||||
size='small'
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={
|
||||
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
description={t('暂无充值记录')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopupHistoryModal;
|
||||
@@ -159,7 +159,7 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: 'Vidu',
|
||||
},
|
||||
{
|
||||
{
|
||||
value: 53,
|
||||
color: 'blue',
|
||||
label: 'SubModel',
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
export function base64UrlToBuffer(base64url) {
|
||||
if (!base64url) return new ArrayBuffer(0);
|
||||
let padding = '='.repeat((4 - (base64url.length % 4)) % 4);
|
||||
@@ -26,7 +44,11 @@ export function bufferToBase64Url(buffer) {
|
||||
}
|
||||
|
||||
export function prepareCredentialCreationOptions(payload) {
|
||||
const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response;
|
||||
const options =
|
||||
payload?.publicKey ||
|
||||
payload?.PublicKey ||
|
||||
payload?.response ||
|
||||
payload?.Response;
|
||||
if (!options) {
|
||||
throw new Error('无法从服务端响应中解析 Passkey 注册参数');
|
||||
}
|
||||
@@ -46,7 +68,10 @@ export function prepareCredentialCreationOptions(payload) {
|
||||
}));
|
||||
}
|
||||
|
||||
if (Array.isArray(options.attestationFormats) && options.attestationFormats.length === 0) {
|
||||
if (
|
||||
Array.isArray(options.attestationFormats) &&
|
||||
options.attestationFormats.length === 0
|
||||
) {
|
||||
delete publicKey.attestationFormats;
|
||||
}
|
||||
|
||||
@@ -54,7 +79,11 @@ export function prepareCredentialCreationOptions(payload) {
|
||||
}
|
||||
|
||||
export function prepareCredentialRequestOptions(payload) {
|
||||
const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response;
|
||||
const options =
|
||||
payload?.publicKey ||
|
||||
payload?.PublicKey ||
|
||||
payload?.response ||
|
||||
payload?.Response;
|
||||
if (!options) {
|
||||
throw new Error('无法从服务端响应中解析 Passkey 登录参数');
|
||||
}
|
||||
@@ -77,7 +106,10 @@ export function buildRegistrationResult(credential) {
|
||||
if (!credential) return null;
|
||||
|
||||
const { response } = credential;
|
||||
const transports = typeof response.getTransports === 'function' ? response.getTransports() : undefined;
|
||||
const transports =
|
||||
typeof response.getTransports === 'function'
|
||||
? response.getTransports()
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
@@ -107,7 +139,9 @@ export function buildAssertionResult(assertion) {
|
||||
authenticatorData: bufferToBase64Url(response.authenticatorData),
|
||||
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
|
||||
signature: bufferToBase64Url(response.signature),
|
||||
userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null,
|
||||
userHandle: response.userHandle
|
||||
? bufferToBase64Url(response.userHandle)
|
||||
: null,
|
||||
},
|
||||
clientExtensionResults: assertion.getClientExtensionResults?.() ?? {},
|
||||
};
|
||||
@@ -117,15 +151,22 @@ export async function isPasskeySupported() {
|
||||
if (typeof window === 'undefined' || !window.PublicKeyCredential) {
|
||||
return false;
|
||||
}
|
||||
if (typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function') {
|
||||
if (
|
||||
typeof window.PublicKeyCredential.isConditionalMediationAvailable ===
|
||||
'function'
|
||||
) {
|
||||
try {
|
||||
const available = await window.PublicKeyCredential.isConditionalMediationAvailable();
|
||||
const available =
|
||||
await window.PublicKeyCredential.isConditionalMediationAvailable();
|
||||
if (available) return true;
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') {
|
||||
if (
|
||||
typeof window.PublicKeyCredential
|
||||
.isUserVerifyingPlatformAuthenticatorAvailable === 'function'
|
||||
) {
|
||||
try {
|
||||
return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
} catch (error) {
|
||||
@@ -134,4 +175,3 @@ export async function isPasskeySupported() {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -929,10 +929,10 @@ export function renderQuotaWithAmount(amount) {
|
||||
export function getCurrencyConfig() {
|
||||
const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
|
||||
const statusStr = localStorage.getItem('status');
|
||||
|
||||
|
||||
let symbol = '$';
|
||||
let rate = 1;
|
||||
|
||||
|
||||
if (quotaDisplayType === 'CNY') {
|
||||
symbol = '¥';
|
||||
try {
|
||||
@@ -950,7 +950,7 @@ export function getCurrencyConfig() {
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
return { symbol, rate, type: quotaDisplayType };
|
||||
}
|
||||
|
||||
@@ -1128,7 +1128,7 @@ export function renderModelPrice(
|
||||
user_group_ratio,
|
||||
);
|
||||
groupRatio = effectiveGroupRatio;
|
||||
|
||||
|
||||
// 获取货币配置
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
|
||||
@@ -1177,13 +1177,16 @@ export function renderModelPrice(
|
||||
<>
|
||||
<article>
|
||||
<p>
|
||||
{i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', {
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
audioPrice: audioInputSeperatePrice
|
||||
? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens`
|
||||
: '',
|
||||
})}
|
||||
{i18next.t(
|
||||
'输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}',
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
audioPrice: audioInputSeperatePrice
|
||||
? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens`
|
||||
: '',
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1311,27 +1314,27 @@ export function renderModelPrice(
|
||||
const extraServices = [
|
||||
webSearch && webSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
symbol: symbol,
|
||||
price: (webSearchPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
symbol: symbol,
|
||||
price: (webSearchPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
fileSearch && fileSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
symbol: symbol,
|
||||
price: (fileSearchPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
symbol: symbol,
|
||||
price: (fileSearchPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
imageGenerationCall && imageGenerationCallPrice > 0
|
||||
? i18next.t(
|
||||
@@ -1384,7 +1387,7 @@ export function renderLogContent(
|
||||
label: ratioLabel,
|
||||
useUserGroupRatio: useUserGroupRatio,
|
||||
} = getEffectiveRatio(groupRatio, user_group_ratio);
|
||||
|
||||
|
||||
// 获取货币配置
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
|
||||
@@ -1484,10 +1487,10 @@ export function renderAudioModelPrice(
|
||||
user_group_ratio,
|
||||
);
|
||||
groupRatio = effectiveGroupRatio;
|
||||
|
||||
|
||||
// 获取货币配置
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
|
||||
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return i18next.t(
|
||||
@@ -1522,10 +1525,10 @@ export function renderAudioModelPrice(
|
||||
let audioPrice =
|
||||
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
||||
(audioCompletionTokens / 1000000) *
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
let price = textPrice + audioPrice;
|
||||
return (
|
||||
<>
|
||||
@@ -1577,7 +1580,12 @@ export function renderAudioModelPrice(
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
total: (inputRatioPrice * audioRatio * audioCompletionRatio * rate).toFixed(6),
|
||||
total: (
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
rate
|
||||
).toFixed(6),
|
||||
audioRatio: audioRatio,
|
||||
audioCompRatio: audioCompletionRatio,
|
||||
},
|
||||
@@ -1586,29 +1594,31 @@ export function renderAudioModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0
|
||||
? i18next.t(
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
symbol: symbol,
|
||||
cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(6),
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
total: (textPrice * rate).toFixed(6),
|
||||
},
|
||||
)
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
symbol: symbol,
|
||||
cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(
|
||||
6,
|
||||
),
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
total: (textPrice * rate).toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
total: (textPrice * rate).toFixed(6),
|
||||
},
|
||||
)}
|
||||
'文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
total: (textPrice * rate).toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1617,9 +1627,15 @@ export function renderAudioModelPrice(
|
||||
input: audioInputTokens,
|
||||
completion: audioCompletionTokens,
|
||||
symbol: symbol,
|
||||
audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(6),
|
||||
audioCompPrice:
|
||||
(audioRatio * audioCompletionRatio * inputRatioPrice * rate).toFixed(6),
|
||||
audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(
|
||||
6,
|
||||
),
|
||||
audioCompPrice: (
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
inputRatioPrice *
|
||||
rate
|
||||
).toFixed(6),
|
||||
total: (audioPrice * rate).toFixed(6),
|
||||
},
|
||||
)}
|
||||
@@ -1668,7 +1684,7 @@ export function renderClaudeModelPrice(
|
||||
user_group_ratio,
|
||||
);
|
||||
groupRatio = effectiveGroupRatio;
|
||||
|
||||
|
||||
// 获取货币配置
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
|
||||
@@ -1757,37 +1773,39 @@ export function renderClaudeModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0 || cacheCreationTokens > 0
|
||||
? i18next.t(
|
||||
'提示 {{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: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
symbol: symbol,
|
||||
cachePrice: (cacheRatioPrice * rate).toFixed(2),
|
||||
cacheCreationPrice: (cacheCreationRatioPrice * rate).toFixed(6),
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: (price * rate).toFixed(6),
|
||||
},
|
||||
)
|
||||
'提示 {{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: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
symbol: symbol,
|
||||
cachePrice: (cacheRatioPrice * rate).toFixed(2),
|
||||
cacheCreationPrice: (
|
||||
cacheCreationRatioPrice * rate
|
||||
).toFixed(6),
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: (price * rate).toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: (price * rate).toFixed(6),
|
||||
},
|
||||
)}
|
||||
'提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
completion: completionTokens,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: (price * rate).toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
@@ -1810,7 +1828,7 @@ export function renderClaudeLogContent(
|
||||
user_group_ratio,
|
||||
);
|
||||
groupRatio = effectiveGroupRatio;
|
||||
|
||||
|
||||
// 获取货币配置
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export function isVerificationRequiredError(error) {
|
||||
const verificationCodes = [
|
||||
'VERIFICATION_REQUIRED',
|
||||
'VERIFICATION_EXPIRED',
|
||||
'VERIFICATION_INVALID'
|
||||
'VERIFICATION_INVALID',
|
||||
];
|
||||
|
||||
return verificationCodes.includes(data.code);
|
||||
@@ -57,6 +57,6 @@ export function extractVerificationInfo(error) {
|
||||
return {
|
||||
code: data.code,
|
||||
message: data.message || '需要安全验证',
|
||||
required: true
|
||||
required: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const useChannelsData = () => {
|
||||
const [selectedModelKeys, setSelectedModelKeys] = useState([]);
|
||||
const [isBatchTesting, setIsBatchTesting] = useState(false);
|
||||
const [modelTablePage, setModelTablePage] = useState(1);
|
||||
const [selectedEndpointType, setSelectedEndpointType] = useState('');
|
||||
const [selectedEndpointType, setSelectedEndpointType] = useState('');
|
||||
|
||||
// 使用 ref 来避免闭包问题,类似旧版实现
|
||||
const shouldStopBatchTestingRef = useRef(false);
|
||||
|
||||
@@ -31,11 +31,11 @@ import { isVerificationRequiredError } from '../../helpers/secureApiCall';
|
||||
* @param {string} options.successMessage - 成功提示消息
|
||||
* @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true
|
||||
*/
|
||||
export const useSecureVerification = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
export const useSecureVerification = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
successMessage,
|
||||
autoReset = true
|
||||
autoReset = true,
|
||||
} = {}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -43,7 +43,7 @@ export const useSecureVerification = ({
|
||||
const [verificationMethods, setVerificationMethods] = useState({
|
||||
has2FA: false,
|
||||
hasPasskey: false,
|
||||
passkeySupported: false
|
||||
passkeySupported: false,
|
||||
});
|
||||
|
||||
// 模态框状态
|
||||
@@ -54,12 +54,13 @@ export const useSecureVerification = ({
|
||||
method: null, // '2fa' | 'passkey'
|
||||
loading: false,
|
||||
code: '',
|
||||
apiCall: null
|
||||
apiCall: null,
|
||||
});
|
||||
|
||||
// 检查可用的验证方式
|
||||
const checkVerificationMethods = useCallback(async () => {
|
||||
const methods = await SecureVerificationService.checkAvailableVerificationMethods();
|
||||
const methods =
|
||||
await SecureVerificationService.checkAvailableVerificationMethods();
|
||||
setVerificationMethods(methods);
|
||||
return methods;
|
||||
}, []);
|
||||
@@ -75,94 +76,108 @@ export const useSecureVerification = ({
|
||||
method: null,
|
||||
loading: false,
|
||||
code: '',
|
||||
apiCall: null
|
||||
apiCall: null,
|
||||
});
|
||||
setIsModalVisible(false);
|
||||
}, []);
|
||||
|
||||
// 开始验证流程
|
||||
const startVerification = useCallback(async (apiCall, options = {}) => {
|
||||
const { preferredMethod, title, description } = options;
|
||||
const startVerification = useCallback(
|
||||
async (apiCall, options = {}) => {
|
||||
const { preferredMethod, title, description } = options;
|
||||
|
||||
// 检查验证方式
|
||||
const methods = await checkVerificationMethods();
|
||||
// 检查验证方式
|
||||
const methods = await checkVerificationMethods();
|
||||
|
||||
if (!methods.has2FA && !methods.hasPasskey) {
|
||||
const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
|
||||
showError(errorMessage);
|
||||
onError?.(new Error(errorMessage));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置默认验证方式
|
||||
let defaultMethod = preferredMethod;
|
||||
if (!defaultMethod) {
|
||||
if (methods.hasPasskey && methods.passkeySupported) {
|
||||
defaultMethod = 'passkey';
|
||||
} else if (methods.has2FA) {
|
||||
defaultMethod = '2fa';
|
||||
if (!methods.has2FA && !methods.hasPasskey) {
|
||||
const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作');
|
||||
showError(errorMessage);
|
||||
onError?.(new Error(errorMessage));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setVerificationState(prev => ({
|
||||
...prev,
|
||||
method: defaultMethod,
|
||||
apiCall,
|
||||
title,
|
||||
description
|
||||
}));
|
||||
setIsModalVisible(true);
|
||||
// 设置默认验证方式
|
||||
let defaultMethod = preferredMethod;
|
||||
if (!defaultMethod) {
|
||||
if (methods.hasPasskey && methods.passkeySupported) {
|
||||
defaultMethod = 'passkey';
|
||||
} else if (methods.has2FA) {
|
||||
defaultMethod = '2fa';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [checkVerificationMethods, onError, t]);
|
||||
setVerificationState((prev) => ({
|
||||
...prev,
|
||||
method: defaultMethod,
|
||||
apiCall,
|
||||
title,
|
||||
description,
|
||||
}));
|
||||
setIsModalVisible(true);
|
||||
|
||||
return true;
|
||||
},
|
||||
[checkVerificationMethods, onError, t],
|
||||
);
|
||||
|
||||
// 执行验证
|
||||
const executeVerification = useCallback(async (method, code = '') => {
|
||||
if (!verificationState.apiCall) {
|
||||
showError(t('验证配置错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
setVerificationState(prev => ({ ...prev, loading: true }));
|
||||
|
||||
try {
|
||||
// 先调用验证 API,成功后后端会设置 session
|
||||
await SecureVerificationService.verify(method, code);
|
||||
|
||||
// 验证成功,调用业务 API(此时中间件会通过)
|
||||
const result = await verificationState.apiCall();
|
||||
|
||||
// 显示成功消息
|
||||
if (successMessage) {
|
||||
showSuccess(successMessage);
|
||||
const executeVerification = useCallback(
|
||||
async (method, code = '') => {
|
||||
if (!verificationState.apiCall) {
|
||||
showError(t('验证配置错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用成功回调
|
||||
onSuccess?.(result, method);
|
||||
setVerificationState((prev) => ({ ...prev, loading: true }));
|
||||
|
||||
// 自动重置状态
|
||||
if (autoReset) {
|
||||
resetState();
|
||||
try {
|
||||
// 先调用验证 API,成功后后端会设置 session
|
||||
await SecureVerificationService.verify(method, code);
|
||||
|
||||
// 验证成功,调用业务 API(此时中间件会通过)
|
||||
const result = await verificationState.apiCall();
|
||||
|
||||
// 显示成功消息
|
||||
if (successMessage) {
|
||||
showSuccess(successMessage);
|
||||
}
|
||||
|
||||
// 调用成功回调
|
||||
onSuccess?.(result, method);
|
||||
|
||||
// 自动重置状态
|
||||
if (autoReset) {
|
||||
resetState();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
showError(error.message || t('验证失败,请重试'));
|
||||
onError?.(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setVerificationState((prev) => ({ ...prev, loading: false }));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
showError(error.message || t('验证失败,请重试'));
|
||||
onError?.(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setVerificationState(prev => ({ ...prev, loading: false }));
|
||||
}
|
||||
}, [verificationState.apiCall, successMessage, onSuccess, onError, autoReset, resetState, t]);
|
||||
},
|
||||
[
|
||||
verificationState.apiCall,
|
||||
successMessage,
|
||||
onSuccess,
|
||||
onError,
|
||||
autoReset,
|
||||
resetState,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
// 设置验证码
|
||||
const setVerificationCode = useCallback((code) => {
|
||||
setVerificationState(prev => ({ ...prev, code }));
|
||||
setVerificationState((prev) => ({ ...prev, code }));
|
||||
}, []);
|
||||
|
||||
// 切换验证方式
|
||||
const switchVerificationMethod = useCallback((method) => {
|
||||
setVerificationState(prev => ({ ...prev, method, code: '' }));
|
||||
setVerificationState((prev) => ({ ...prev, method, code: '' }));
|
||||
}, []);
|
||||
|
||||
// 取消验证
|
||||
@@ -171,20 +186,29 @@ export const useSecureVerification = ({
|
||||
}, [resetState]);
|
||||
|
||||
// 检查是否可以使用某种验证方式
|
||||
const canUseMethod = useCallback((method) => {
|
||||
switch (method) {
|
||||
case '2fa':
|
||||
return verificationMethods.has2FA;
|
||||
case 'passkey':
|
||||
return verificationMethods.hasPasskey && verificationMethods.passkeySupported;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [verificationMethods]);
|
||||
const canUseMethod = useCallback(
|
||||
(method) => {
|
||||
switch (method) {
|
||||
case '2fa':
|
||||
return verificationMethods.has2FA;
|
||||
case 'passkey':
|
||||
return (
|
||||
verificationMethods.hasPasskey &&
|
||||
verificationMethods.passkeySupported
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[verificationMethods],
|
||||
);
|
||||
|
||||
// 获取推荐的验证方式
|
||||
const getRecommendedMethod = useCallback(() => {
|
||||
if (verificationMethods.hasPasskey && verificationMethods.passkeySupported) {
|
||||
if (
|
||||
verificationMethods.hasPasskey &&
|
||||
verificationMethods.passkeySupported
|
||||
) {
|
||||
return 'passkey';
|
||||
}
|
||||
if (verificationMethods.has2FA) {
|
||||
@@ -200,22 +224,25 @@ export const useSecureVerification = ({
|
||||
* @param {Object} options - 验证选项(同 startVerification)
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const withVerification = useCallback(async (apiCall, options = {}) => {
|
||||
try {
|
||||
// 直接尝试调用 API
|
||||
return await apiCall();
|
||||
} catch (error) {
|
||||
// 检查是否是需要验证的错误
|
||||
if (isVerificationRequiredError(error)) {
|
||||
// 自动触发验证流程
|
||||
await startVerification(apiCall, options);
|
||||
// 不抛出错误,让验证模态框处理
|
||||
return null;
|
||||
const withVerification = useCallback(
|
||||
async (apiCall, options = {}) => {
|
||||
try {
|
||||
// 直接尝试调用 API
|
||||
return await apiCall();
|
||||
} catch (error) {
|
||||
// 检查是否是需要验证的错误
|
||||
if (isVerificationRequiredError(error)) {
|
||||
// 自动触发验证流程
|
||||
await startVerification(apiCall, options);
|
||||
// 不抛出错误,让验证模态框处理
|
||||
return null;
|
||||
}
|
||||
// 其他错误继续抛出
|
||||
throw error;
|
||||
}
|
||||
// 其他错误继续抛出
|
||||
throw error;
|
||||
}
|
||||
}, [startVerification]);
|
||||
},
|
||||
[startVerification],
|
||||
);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
@@ -238,9 +265,10 @@ export const useSecureVerification = ({
|
||||
withVerification, // 新增:自动处理验证的包装函数
|
||||
|
||||
// 便捷属性
|
||||
hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey,
|
||||
hasAnyVerificationMethod:
|
||||
verificationMethods.has2FA || verificationMethods.hasPasskey,
|
||||
isLoading: verificationState.loading,
|
||||
currentMethod: verificationState.method,
|
||||
code: verificationState.code
|
||||
code: verificationState.code,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export const useUsersData = () => {
|
||||
};
|
||||
|
||||
// Search users with keyword and group
|
||||
const searchUsers = async (
|
||||
const searchUsers = async (
|
||||
startIdx,
|
||||
pageSize,
|
||||
searchKeyword = null,
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"邀请新用户奖励额度": "Referral bonus quota",
|
||||
"新用户使用邀请码奖励额度": "New user invitation code bonus quota",
|
||||
"保存额度设置": "Save Quota Settings",
|
||||
"分组与模型定价设置": "Group and Model Pricing Settings",
|
||||
"倍率设置": "Ratio Settings",
|
||||
"模型倍率": "Model ratio",
|
||||
"为一个 JSON 文本": "Is a JSON text",
|
||||
@@ -1285,7 +1286,6 @@
|
||||
"可视化倍率设置": "Visual model ratio settings",
|
||||
"确定重置模型倍率吗?": "Confirm to reset model ratio?",
|
||||
"模型固定价格": "Model price per call",
|
||||
"模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)",
|
||||
"保存模型倍率设置": "Save model ratio settings",
|
||||
"重置模型倍率": "Reset model ratio",
|
||||
"一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio",
|
||||
@@ -2177,7 +2177,6 @@
|
||||
"最后使用时间": "Last used time",
|
||||
"备份支持": "Backup support",
|
||||
"支持备份": "Supported",
|
||||
"不支持": "Not supported",
|
||||
"备份状态": "Backup state",
|
||||
"已备份": "Backed up",
|
||||
"未备份": "Not backed up",
|
||||
@@ -2248,5 +2247,18 @@
|
||||
"轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Polling mode must be used with Redis and memory cache functions, otherwise the performance will be significantly reduced and the polling function will not be implemented",
|
||||
"common": {
|
||||
"changeLanguage": "Change Language"
|
||||
}
|
||||
},
|
||||
"充值账单": "Recharge Bills",
|
||||
"订单号": "Order No.",
|
||||
"支付金额": "Payment Amount",
|
||||
"待支付": "Pending",
|
||||
"加载失败": "Load failed",
|
||||
"加载账单失败": "Failed to load bills",
|
||||
"暂无充值记录": "No recharge records",
|
||||
"账单": "Bills",
|
||||
"补单": "Complete Order",
|
||||
"补单成功": "Order completed successfully",
|
||||
"补单失败": "Failed to complete order",
|
||||
"确认补单": "Confirm Order Completion",
|
||||
"是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?"
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
"邀请新用户奖励额度": "Quota de bonus de parrainage",
|
||||
"新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur",
|
||||
"保存额度设置": "Enregistrer les paramètres de quota",
|
||||
"分组与模型定价设置": "Paramètres de groupe et de tarification du modèle",
|
||||
"倍率设置": "Paramètres de ratio",
|
||||
"模型倍率": "Ratio de modèle",
|
||||
"为一个 JSON 文本": "Est un texte JSON",
|
||||
@@ -2238,5 +2239,18 @@
|
||||
"配置 Passkey": "Configurer Passkey",
|
||||
"重置 2FA": "Réinitialiser 2FA",
|
||||
"重置 Passkey": "Réinitialiser le Passkey",
|
||||
"默认使用系统名称": "Le nom du système est utilisé par défaut"
|
||||
"默认使用系统名称": "Le nom du système est utilisé par défaut",
|
||||
"充值账单": "Factures de recharge",
|
||||
"订单号": "N° de commande",
|
||||
"支付金额": "Montant payé",
|
||||
"待支付": "En attente",
|
||||
"加载失败": "Échec du chargement",
|
||||
"加载账单失败": "Échec du chargement des factures",
|
||||
"暂无充值记录": "Aucune recharge",
|
||||
"账单": "Factures",
|
||||
"补单": "Compléter la commande",
|
||||
"补单成功": "Commande complétée avec succès",
|
||||
"补单失败": "Échec de la complétion de la commande",
|
||||
"确认补单": "Confirmer la complétion",
|
||||
"是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?"
|
||||
}
|
||||
|
||||
@@ -94,5 +94,22 @@
|
||||
"允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证",
|
||||
"确认解绑 Passkey": "确认解绑 Passkey",
|
||||
"解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?",
|
||||
"确认解绑": "确认解绑"
|
||||
"确认解绑": "确认解绑",
|
||||
"充值账单": "充值账单",
|
||||
"订单号": "订单号",
|
||||
"支付金额": "支付金额",
|
||||
"待支付": "待支付",
|
||||
"加载失败": "加载失败",
|
||||
"加载账单失败": "加载账单失败",
|
||||
"暂无充值记录": "暂无充值记录",
|
||||
"账单": "账单",
|
||||
"支付方式": "支付方式",
|
||||
"支付宝": "支付宝",
|
||||
"微信": "微信",
|
||||
"补单": "补单",
|
||||
"补单成功": "补单成功",
|
||||
"补单失败": "补单失败",
|
||||
"确认补单": "确认补单",
|
||||
"是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?",
|
||||
"操作": "操作"
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export default function SettingsChats(props) {
|
||||
const isDuplicate = chatConfigs.some(
|
||||
(config) =>
|
||||
config.name === values.name &&
|
||||
(!isEdit || config.id !== editingConfig.id)
|
||||
(!isEdit || config.id !== editingConfig.id),
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
|
||||
@@ -18,7 +18,16 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Col, Form, Row, Spin, DatePicker, Typography, Modal } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Row,
|
||||
Spin,
|
||||
DatePicker,
|
||||
Typography,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -90,39 +99,58 @@ export default function SettingsLog(props) {
|
||||
const targetTime = targetDate.format('YYYY-MM-DD HH:mm:ss');
|
||||
const currentTime = now.format('YYYY-MM-DD HH:mm:ss');
|
||||
const daysDiff = now.diff(targetDate, 'day');
|
||||
|
||||
|
||||
Modal.confirm({
|
||||
title: t('确认清除历史日志'),
|
||||
content: (
|
||||
<div style={{ lineHeight: '1.8' }}>
|
||||
<p>
|
||||
<Text>{t('当前时间')}:</Text>
|
||||
<Text strong style={{ color: '#52c41a' }}>{currentTime}</Text>
|
||||
<Text strong style={{ color: '#52c41a' }}>
|
||||
{currentTime}
|
||||
</Text>
|
||||
</p>
|
||||
<p>
|
||||
<Text>{t('选择时间')}:</Text>
|
||||
<Text strong type="danger">{targetTime}</Text>
|
||||
<Text strong type='danger'>
|
||||
{targetTime}
|
||||
</Text>
|
||||
{daysDiff > 0 && (
|
||||
<Text type="tertiary"> ({t('约')} {daysDiff} {t('天前')})</Text>
|
||||
<Text type='tertiary'>
|
||||
{' '}
|
||||
({t('约')} {daysDiff} {t('天前')})
|
||||
</Text>
|
||||
)}
|
||||
</p>
|
||||
<div style={{
|
||||
background: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '12px'
|
||||
}}>
|
||||
<Text type="warning" strong>⚠️ {t('注意')}:</Text>
|
||||
<Text>{t('将删除')} </Text>
|
||||
<Text strong type="danger">{targetTime}</Text>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '12px',
|
||||
color: '#333',
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ color: '#d46b08' }}>
|
||||
⚠️ {t('注意')}:
|
||||
</Text>
|
||||
<Text style={{ color: '#333' }}>{t('将删除')} </Text>
|
||||
<Text strong style={{ color: '#cf1322' }}>
|
||||
{targetTime}
|
||||
</Text>
|
||||
{daysDiff > 0 && (
|
||||
<Text type="tertiary"> ({t('约')} {daysDiff} {t('天前')})</Text>
|
||||
<Text style={{ color: '#8c8c8c' }}>
|
||||
{' '}
|
||||
({t('约')} {daysDiff} {t('天前')})
|
||||
</Text>
|
||||
)}
|
||||
<Text> {t('之前的所有日志')}</Text>
|
||||
<Text style={{ color: '#333' }}> {t('之前的所有日志')}</Text>
|
||||
</div>
|
||||
<p style={{ marginTop: '12px' }}>
|
||||
<Text type="danger">{t('此操作不可恢复,请仔细确认时间后再操作!')}</Text>
|
||||
<Text type='danger'>
|
||||
{t('此操作不可恢复,请仔细确认时间后再操作!')}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
@@ -202,10 +230,18 @@ export default function SettingsLog(props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Text type="tertiary" size="small" style={{ display: 'block', marginTop: 4, marginBottom: 8 }}>
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
style={{ display: 'block', marginTop: 4, marginBottom: 8 }}
|
||||
>
|
||||
{t('将清除选定时间之前的所有日志')}
|
||||
</Text>
|
||||
<Button size='default' type='danger' onClick={onCleanHistoryLog}>
|
||||
<Button
|
||||
size='default'
|
||||
type='danger'
|
||||
onClick={onCleanHistoryLog}
|
||||
>
|
||||
{t('清除历史日志')}
|
||||
</Button>
|
||||
</Spin>
|
||||
|
||||
@@ -108,7 +108,7 @@ const Setting = () => {
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Calculator size={18} />
|
||||
{t('倍率设置')}
|
||||
{t('分组与模型定价设置')}
|
||||
</span>
|
||||
),
|
||||
content: <RatioSetting />,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { API, showError } from '../helpers';
|
||||
import {
|
||||
prepareCredentialRequestOptions,
|
||||
buildAssertionResult,
|
||||
isPasskeySupported
|
||||
isPasskeySupported,
|
||||
} from '../helpers/passkey';
|
||||
|
||||
/**
|
||||
@@ -35,46 +35,54 @@ export class SecureVerificationService {
|
||||
*/
|
||||
static async checkAvailableVerificationMethods() {
|
||||
try {
|
||||
const [twoFAResponse, passkeyResponse, passkeySupported] = await Promise.all([
|
||||
API.get('/api/user/2fa/status'),
|
||||
API.get('/api/user/passkey'),
|
||||
isPasskeySupported()
|
||||
]);
|
||||
const [twoFAResponse, passkeyResponse, passkeySupported] =
|
||||
await Promise.all([
|
||||
API.get('/api/user/2fa/status'),
|
||||
API.get('/api/user/passkey'),
|
||||
isPasskeySupported(),
|
||||
]);
|
||||
|
||||
console.log('=== DEBUGGING VERIFICATION METHODS ===');
|
||||
console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2));
|
||||
console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2));
|
||||
|
||||
const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true;
|
||||
const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true;
|
||||
|
||||
console.log(
|
||||
'Passkey Response:',
|
||||
JSON.stringify(passkeyResponse, null, 2),
|
||||
);
|
||||
|
||||
const has2FA =
|
||||
twoFAResponse.data?.success &&
|
||||
twoFAResponse.data?.data?.enabled === true;
|
||||
const hasPasskey =
|
||||
passkeyResponse.data?.success &&
|
||||
passkeyResponse.data?.data?.enabled === true;
|
||||
|
||||
console.log('has2FA calculation:', {
|
||||
success: twoFAResponse.data?.success,
|
||||
dataExists: !!twoFAResponse.data?.data,
|
||||
enabled: twoFAResponse.data?.data?.enabled,
|
||||
result: has2FA
|
||||
result: has2FA,
|
||||
});
|
||||
|
||||
|
||||
console.log('hasPasskey calculation:', {
|
||||
success: passkeyResponse.data?.success,
|
||||
dataExists: !!passkeyResponse.data?.data,
|
||||
enabled: passkeyResponse.data?.data?.enabled,
|
||||
result: hasPasskey
|
||||
result: hasPasskey,
|
||||
});
|
||||
|
||||
const result = {
|
||||
has2FA,
|
||||
hasPasskey,
|
||||
passkeySupported
|
||||
passkeySupported,
|
||||
};
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to check verification methods:', error);
|
||||
return {
|
||||
has2FA: false,
|
||||
hasPasskey: false,
|
||||
passkeySupported: false
|
||||
passkeySupported: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -92,7 +100,7 @@ export class SecureVerificationService {
|
||||
// 调用通用验证 API,验证成功后后端会设置 session
|
||||
const verifyResponse = await API.post('/api/verify', {
|
||||
method: '2fa',
|
||||
code: code.trim()
|
||||
code: code.trim(),
|
||||
});
|
||||
|
||||
if (!verifyResponse.data?.success) {
|
||||
@@ -115,7 +123,9 @@ export class SecureVerificationService {
|
||||
}
|
||||
|
||||
// 准备WebAuthn选项
|
||||
const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options);
|
||||
const publicKey = prepareCredentialRequestOptions(
|
||||
beginResponse.data.data.options,
|
||||
);
|
||||
|
||||
// 执行WebAuthn验证
|
||||
const credential = await navigator.credentials.get({ publicKey });
|
||||
@@ -127,14 +137,17 @@ export class SecureVerificationService {
|
||||
const assertionResult = buildAssertionResult(credential);
|
||||
|
||||
// 完成验证
|
||||
const finishResponse = await API.post('/api/user/passkey/verify/finish', assertionResult);
|
||||
const finishResponse = await API.post(
|
||||
'/api/user/passkey/verify/finish',
|
||||
assertionResult,
|
||||
);
|
||||
if (!finishResponse.data?.success) {
|
||||
throw new Error(finishResponse.data?.message || '验证失败');
|
||||
}
|
||||
|
||||
// 调用通用验证 API 设置 session(Passkey 验证已完成)
|
||||
const verifyResponse = await API.post('/api/verify', {
|
||||
method: 'passkey'
|
||||
method: 'passkey',
|
||||
});
|
||||
|
||||
if (!verifyResponse.data?.success) {
|
||||
@@ -191,27 +204,29 @@ export const createApiCalls = {
|
||||
* @param {string} method - HTTP方法,默认为 'POST'
|
||||
* @param {Object} extraData - 额外的请求数据
|
||||
*/
|
||||
custom: (url, method = 'POST', extraData = {}) => async () => {
|
||||
// 新系统中,验证已通过中间件处理
|
||||
const data = extraData;
|
||||
custom:
|
||||
(url, method = 'POST', extraData = {}) =>
|
||||
async () => {
|
||||
// 新系统中,验证已通过中间件处理
|
||||
const data = extraData;
|
||||
|
||||
let response;
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await API.get(url, { params: data });
|
||||
break;
|
||||
case 'POST':
|
||||
response = await API.post(url, data);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await API.put(url, data);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await API.delete(url, { data });
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的HTTP方法: ${method}`);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
let response;
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
response = await API.get(url, { params: data });
|
||||
break;
|
||||
case 'POST':
|
||||
response = await API.post(url, data);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await API.put(url, data);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await API.delete(url, { data });
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的HTTP方法: ${method}`);
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user