Compare commits

...

52 Commits

Author SHA1 Message Date
Seefs
00ba64d837 Revert "Fork Sync: Update from parent repository" 2025-10-10 20:32:17 +08:00
github-actions[bot]
b9431dace1 Merge pull request #26 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-10 12:30:16 +00:00
Seefs
721357b4a4 Merge pull request #2000 from feitianbubu/pr/sora-retrieve-video-sdk
feat: support openAI sdk retrieve videos
2025-10-10 19:18:27 +08:00
feitianbubu
ff9f9fbbc9 feat: support openAI sdk retrieve videos 2025-10-10 18:59:52 +08:00
github-actions[bot]
268757a670 Merge pull request #25 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-10 09:16:42 +00:00
CaIon
9b551d978d feat: add informational banner to proxy settings in SystemSetting.jsx 2025-10-10 16:44:41 +08:00
Seefs
76ab8a480a Merge pull request #1401 from feitianbubu/pr/add-qwen-channel-auto-disabled
feat: add qwen channel auto disabled
2025-10-10 16:41:43 +08:00
Calcium-Ion
f091f663c2 Merge pull request #1998 from seefs001/feature/pplx-channel
feat: pplx channel
2025-10-10 16:33:27 +08:00
Seefs
e8966c7374 feat: pplx channel 2025-10-10 16:12:15 +08:00
Calcium-Ion
5a7f498629 Merge pull request #1997 from feitianbubu/pr/add-sora-fetch-task
支持Sora做为上游渠道
2025-10-10 16:10:58 +08:00
feitianbubu
4c1f138c0a fix: sora fetch task route 2025-10-10 16:01:06 +08:00
Calcium-Ion
f4d7bde20b Merge pull request #1996 from seefs001/fix/remark-ignore
fix: channel remark ignore issue
2025-10-10 15:41:30 +08:00
Calcium-Ion
0c181395b4 Merge pull request #1992 from seefs001/pr-upstream-1981
feat(web): add settings & pages of privacy policy & user agreement
2025-10-10 15:41:06 +08:00
Seefs
6897a9ffd8 fix: channel remark ignore issue 2025-10-10 15:40:00 +08:00
Calcium-Ion
77130dfb87 Merge commit from fork
feat: enhance HTTP client wit redirect handling and SSRF protection
2025-10-10 15:30:37 +08:00
Calcium-Ion
614abc3441 Merge pull request #1995 from seefs001/fix/test-channel-1993
fix: test model #1993
2025-10-10 15:26:18 +08:00
feitianbubu
2479da4986 feat: add sora video fetch task 2025-10-10 15:25:29 +08:00
CaIon
7b732ec4b7 feat: enhance HTTP client with custom redirect handling and SSRF protection 2025-10-10 15:13:41 +08:00
Seefs
0fed791ad9 fix: test model #1993 2025-10-10 15:01:24 +08:00
github-actions[bot]
01135f430e Merge pull request #24 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-10 06:47:11 +00:00
Seefs
7de02991a1 Merge pull request #1994 from feitianbubu/pr/fix-video-model
fix: avoid get model consuming body
2025-10-10 14:28:16 +08:00
feitianbubu
3c57cfbf71 fix: avoid get model consuming body 2025-10-10 14:19:49 +08:00
Seefs
fe9b305232 fix: legal setting 2025-10-10 13:18:26 +08:00
キュビビイ
17dafa3b03 feat: add user agreement and privacy policy to login page 2025-10-09 22:21:56 +08:00
github-actions[bot]
9b1056b13b Merge pull request #23 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-09 09:16:11 +00:00
github-actions[bot]
df243204fd Merge pull request #22 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-09 06:22:59 +00:00
github-actions[bot]
6be574accc Merge pull request #21 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-09 04:24:23 +00:00
LINLI
63f9d11da9 Merge pull request #20 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-08 23:19:31 +08:00
キュビビイ
4d0a9d9494 feat: componentize User Agreement and Privacy Policy display
Extracted the User Agreement and Privacy Policy presentation into a
reusable DocumentRenderer component (web/src/components/common/DocumentRenderer).
Unified rendering logic and i18n source for these documents, removed the
legacy contentDetector utility, and updated the related pages to use the
new component. Adjusted controller/backend (controller/misc.go) and locale
files to support the new rendering approach.

This improves reuse, maintainability, and future extensibility.
2025-10-08 11:12:49 +08:00
キュビビイ
6891057647 feat(web): add settings & pages of privacy policy & user agreement 2025-10-08 10:43:47 +08:00
github-actions[bot]
a3e1402df8 Merge pull request #19 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-04 06:59:08 +00:00
github-actions[bot]
de7661ea6d Merge pull request #18 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-03 15:14:37 +00:00
github-actions[bot]
0a6695bbd3 Merge pull request #17 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-03 12:29:24 +00:00
github-actions[bot]
6a933c2e73 Merge pull request #16 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-03 06:54:44 +00:00
github-actions[bot]
fee71c798f Merge pull request #15 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-03 06:21:30 +00:00
github-actions[bot]
8628994acf Merge pull request #14 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-02 12:28:55 +00:00
github-actions[bot]
4a3e233711 Merge pull request #13 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-02 09:15:31 +00:00
github-actions[bot]
7603458c1d Merge pull request #12 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-02 06:44:51 +00:00
github-actions[bot]
0907fc4863 Merge pull request #11 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-01 18:20:55 +00:00
github-actions[bot]
c3c4e9a2fc Merge pull request #10 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-01 09:46:00 +00:00
github-actions[bot]
991ab6440b Merge pull request #9 from QuantumNous/main
Fork Sync: Update from parent repository
2025-10-01 09:16:40 +00:00
github-actions[bot]
2f2e0638d4 Merge pull request #8 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-30 09:16:35 +00:00
github-actions[bot]
85ecd08e1b Merge pull request #7 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-30 06:22:41 +00:00
github-actions[bot]
408fff8de8 Merge pull request #6 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-30 04:48:13 +00:00
linzero
298bc12e51 fix(api): update request forwarding to use environment variable TARGET_URL 2025-09-30 12:44:39 +08:00
github-actions[bot]
dbf20c7967 Merge pull request #5 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-30 03:28:59 +00:00
github-actions[bot]
b516ca88f2 Merge pull request #4 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-29 12:30:46 +00:00
github-actions[bot]
fde92a5770 Merge pull request #3 from QuantumNous/main
Fork Sync: Update from parent repository
2025-09-29 09:13:41 +00:00
linzero
714fa11412 feat: update sync workflow owner and add new request forwarding functions 2025-09-29 17:13:02 +08:00
linzero
980108e907 feat: update build script to increase memory limit and add sync workflow 2025-09-29 16:55:22 +08:00
linzero
d20ee1226e fix: add antd dependency to package.json 2025-09-29 16:44:18 +08:00
feitianbubu
2488e6ab66 feat: add ali qwen channel autoDisabled 2025-07-19 21:19:46 +08:00
36 changed files with 924 additions and 51 deletions

View File

@@ -5,4 +5,5 @@
.gitignore
Makefile
docs
.eslintcache
.eslintcache
.gocache

17
.github/workflows/sync.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# .github/workflows/sync.yml
name: Sync Fork
on:
push: # push 时触发, 主要是为了测试配置有没有问题
schedule:
- cron: '* */3 * * *' # 每3小时触发, 对于一些更新不那么频繁的项目可以设置为每天一次, 低碳一点
jobs:
repo-sync:
runs-on: ubuntu-latest
steps:
- uses: TG908/fork-sync@v1.6.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
owner: QuantumNous # fork 的上游仓库 user
head: main # fork 的上游仓库 branch
base: main # 本地仓库 branch

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ new-api
.DS_Store
tiktoken_cache
.eslintcache
.gocache
electron/node_modules
electron/dist

View File

@@ -59,6 +59,21 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
testModel = strings.TrimSpace(testModel)
if testModel == "" {
if channel.TestModel != nil && *channel.TestModel != "" {
testModel = strings.TrimSpace(*channel.TestModel)
} else {
models := channel.GetModels()
if len(models) > 0 {
testModel = strings.TrimSpace(models[0])
}
if testModel == "" {
testModel = "gpt-4o-mini"
}
}
}
requestPath := "/v1/chat/completions"
// 如果指定了端点类型,使用指定的端点类型
@@ -90,18 +105,6 @@ func testChannel(channel *model.Channel, testModel string, endpointType string)
Header: make(http.Header),
}
if testModel == "" {
if channel.TestModel != nil && *channel.TestModel != "" {
testModel = *channel.TestModel
} else {
if len(channel.GetModels()) > 0 {
testModel = channel.GetModels()[0]
} else {
testModel = "gpt-4o-mini"
}
}
}
cache, err := model.GetUserCache(1)
if err != nil {
return testResult{

View File

@@ -43,6 +43,7 @@ func GetStatus(c *gin.Context) {
defer common.OptionMapRWMutex.RUnlock()
passkeySetting := system_setting.GetPasskeySettings()
legalSetting := system_setting.GetLegalSettings()
data := gin.H{
"version": common.Version,
@@ -108,6 +109,8 @@ func GetStatus(c *gin.Context) {
"passkey_user_verification": passkeySetting.UserVerification,
"passkey_attachment": passkeySetting.AttachmentPreference,
"setup": constant.Setup,
"user_agreement_enabled": legalSetting.UserAgreement != "",
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
}
// 根据启用状态注入可选内容
@@ -151,6 +154,24 @@ func GetAbout(c *gin.Context) {
return
}
func GetUserAgreement(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": system_setting.GetLegalSettings().UserAgreement,
})
return
}
func GetPrivacyPolicy(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": system_setting.GetLegalSettings().PrivacyPolicy,
})
return
}
func GetMidjourney(c *gin.Context) {
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()

View File

@@ -87,6 +87,12 @@ type GeneralOpenAIRequest struct {
WebSearch json.RawMessage `json:"web_search,omitempty"`
// doubao,zhipu_v4
THINKING json.RawMessage `json:"thinking,omitempty"`
// pplx Params
SearchDomainFilter json.RawMessage `json:"search_domain_filter,omitempty"`
SearchRecencyFilter string `json:"search_recency_filter,omitempty"`
ReturnImages bool `json:"return_images,omitempty"`
ReturnRelatedQuestions bool `json:"return_related_questions,omitempty"`
SearchMode string `json:"search_mode,omitempty"`
}
func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {

View File

@@ -174,7 +174,19 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
relayMode := relayconstant.RelayModeUnknown
if c.Request.Method == http.MethodPost {
relayMode = relayconstant.RelayModeVideoSubmit
modelRequest.Model = c.PostForm("model")
form, err := common.ParseMultipartFormReusable(c)
if err != nil {
return nil, false, errors.New("无效的video请求, " + err.Error())
}
defer form.RemoveAll()
if form != nil {
if values, ok := form.Value["model"]; ok && len(values) > 0 {
modelRequest.Model = values[0]
}
}
} else if c.Request.Method == http.MethodGet {
relayMode = relayconstant.RelayModeVideoFetchByID
shouldSelectChannel = false
}
c.Set("relay_mode", relayMode)
} else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") {

View File

@@ -46,7 +46,7 @@ type Channel struct {
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
HeaderOverride *string `json:"header_override" gorm:"type:text"`
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"`
// add after v0.8.5
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`

View File

@@ -22,10 +22,9 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me
panic("implement me")
return nil, nil
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := openai.Adaptor{}
return adaptor.ConvertClaudeRequest(c, info, req)
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
@@ -80,11 +79,8 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
usage, err = openai.OaiStreamHandler(c, info, resp)
} else {
usage, err = openai.OpenaiHandler(c, info, resp)
}
adaptor := openai.Adaptor{}
usage, err = adaptor.DoResponse(c, resp, info)
return
}

View File

@@ -2,6 +2,7 @@ package perplexity
var ModelList = []string{
"llama-3-sonar-small-32k-chat", "llama-3-sonar-small-32k-online", "llama-3-sonar-large-32k-chat", "llama-3-sonar-large-32k-online", "llama-3-8b-instruct", "llama-3-70b-instruct", "mixtral-8x7b-instruct",
"sonar", "sonar-pro", "sonar-reasoning",
}
var ChannelName = "perplexity"

View File

@@ -11,11 +11,18 @@ func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpen
})
}
return &dto.GeneralOpenAIRequest{
Model: request.Model,
Stream: request.Stream,
Messages: messages,
Temperature: request.Temperature,
TopP: request.TopP,
MaxTokens: request.GetMaxTokens(),
Model: request.Model,
Stream: request.Stream,
Messages: messages,
Temperature: request.Temperature,
TopP: request.TopP,
MaxTokens: request.GetMaxTokens(),
FrequencyPenalty: request.FrequencyPenalty,
PresencePenalty: request.PresencePenalty,
SearchDomainFilter: request.SearchDomainFilter,
SearchRecencyFilter: request.SearchRecencyFilter,
ReturnImages: request.ReturnImages,
ReturnRelatedQuestions: request.ReturnRelatedQuestions,
SearchMode: request.SearchMode,
}
}

View File

@@ -32,12 +32,9 @@ type ImageURL struct {
URL string `json:"url"`
}
type responsePayload struct {
ID string `json:"id"` // task_id
}
type responseTask struct {
ID string `json:"id"`
TaskID string `json:"task_id,omitempty"` //兼容旧接口
Object string `json:"object"`
Model string `json:"model"`
Status string `json:"status"`
@@ -108,18 +105,22 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, _ *relayco
_ = resp.Body.Close()
// Parse Sora response
var dResp responsePayload
var dResp responseTask
if err := json.Unmarshal(responseBody, &dResp); err != nil {
taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
return
}
if dResp.ID == "" {
taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
return
if dResp.TaskID == "" {
taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
return
}
dResp.ID = dResp.TaskID
dResp.TaskID = ""
}
c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID})
c.JSON(http.StatusOK, dResp)
return dResp.ID, responseBody, nil
}

View File

@@ -20,6 +20,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/user-agreement", controller.GetUserAgreement)
apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy)
apiRouter.GET("/about", controller.GetAbout)
//apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent)

View File

@@ -19,6 +19,7 @@ func SetVideoRouter(router *gin.Engine) {
// docs: https://platform.openai.com/docs/api-reference/videos/create
{
videoV1Router.POST("/videos", controller.RelayTask)
videoV1Router.GET("/videos/:task_id", controller.RelayTask)
}
klingV1Router := router.Group("/kling/v1")

View File

@@ -75,6 +75,8 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
return true
case "pre_consume_token_quota_failed":
return true
case "Arrearage":
return true
}
switch oaiErr.Type {
case "insufficient_quota":

View File

@@ -45,7 +45,7 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
return nil, fmt.Errorf("failed to marshal worker payload: %v", err)
}
return http.Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload))
return GetHttpClient().Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload))
}
func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response, err error) {
@@ -64,6 +64,6 @@ func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response,
}
common.SysLog(fmt.Sprintf("downloading from origin: %s, reason: %s", common.MaskSensitiveInfo(originUrl), strings.Join(reason, ", ")))
return http.Get(originUrl)
return GetHttpClient().Get(originUrl)
}
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"one-api/common"
"one-api/setting/system_setting"
"sync"
"time"
@@ -19,12 +20,27 @@ var (
proxyClients = make(map[string]*http.Client)
)
func checkRedirect(req *http.Request, via []*http.Request) error {
fetchSetting := system_setting.GetFetchSetting()
urlStr := req.URL.String()
if err := common.ValidateURLWithFetchSetting(urlStr, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
return fmt.Errorf("redirect to %s blocked: %v", urlStr, err)
}
if len(via) >= 10 {
return fmt.Errorf("stopped after 10 redirects")
}
return nil
}
func InitHttpClient() {
if common.RelayTimeout == 0 {
httpClient = &http.Client{}
httpClient = &http.Client{
CheckRedirect: checkRedirect,
}
} else {
httpClient = &http.Client{
Timeout: time.Duration(common.RelayTimeout) * time.Second,
Timeout: time.Duration(common.RelayTimeout) * time.Second,
CheckRedirect: checkRedirect,
}
}
}
@@ -69,6 +85,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
Transport: &http.Transport{
Proxy: http.ProxyURL(parsedURL),
},
CheckRedirect: checkRedirect,
}
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()
@@ -102,6 +119,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return dialer.Dial(network, addr)
},
},
CheckRedirect: checkRedirect,
}
client.Timeout = time.Duration(common.RelayTimeout) * time.Second
proxyClientLock.Lock()

View File

@@ -0,0 +1,21 @@
package system_setting
import "one-api/setting/config"
type LegalSettings struct {
UserAgreement string `json:"user_agreement"`
PrivacyPolicy string `json:"privacy_policy"`
}
var defaultLegalSettings = LegalSettings{
UserAgreement: "",
PrivacyPolicy: "",
}
func init() {
config.GlobalConfig.Register("legal", &defaultLegalSettings)
}
func GetLegalSettings() *LegalSettings {
return &defaultLegalSettings
}

View File

@@ -0,0 +1,35 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -0,0 +1,35 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -0,0 +1,35 @@
// functions/Pages_Functions.js
// 该函数会将所有请求转发到环境变量 `TARGET_URL` 指定的地址
export async function onRequest(context) {
// 从上下文中获取原始请求和环境变量
const { request, env } = context;
// 解析原始请求的 URL以获取路径和查询参数
const url = new URL(request.url);
const path = url.pathname;
const search = url.search;
// 从环境变量中获取目标 URL如果未设置则提供一个默认值
const target = env.TARGET_URL || 'http://172.0.0.1';
// 构建目标 URL
const targetUrl = `${target}${path}${search}`;
// 创建一个新的请求以转发到目标地址
// 复制原始请求的方法、头部和主体
const newRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual' // 防止 fetch 自动处理重定向
});
// 执行转发请求并返回响应
try {
return await fetch(newRequest);
} catch (error) {
// 如果目标服务器无法访问,返回一个错误信息
return new Response(`Error forwarding request: ${error.message}`, { status: 502 });
}
}

View File

@@ -6,6 +6,7 @@
"dependencies": {
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
"antd": "^5.25.2",
"@lobehub/icons": "^2.0.0",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
@@ -44,7 +45,7 @@
},
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build",
"lint": "prettier . --check",
"lint:fix": "prettier . --write",
"eslint": "bunx eslint \"**/*.{js,jsx}\" --cache",

View File

@@ -51,6 +51,8 @@ import SetupCheck from './components/layout/SetupCheck';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const About = lazy(() => import('./pages/About'));
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
function App() {
const location = useLocation();
@@ -301,6 +303,22 @@ function App() {
</Suspense>
}
/>
<Route
path='/user-agreement'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<UserAgreement />
</Suspense>
}
/>
<Route
path='/privacy-policy'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<PrivacyPolicy />
</Suspense>
}
/>
<Route
path='/console/chat/:id?'
element={

View File

@@ -37,7 +37,7 @@ import {
isPasskeySupported,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
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';
@@ -84,6 +84,9 @@ const LoginForm = () => {
const [showTwoFA, setShowTwoFA] = useState(false);
const [passkeySupported, setPasskeySupported] = useState(false);
const [passkeyLoading, setPasskeyLoading] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const logo = getLogo();
const systemName = getSystemName();
@@ -103,6 +106,10 @@ const LoginForm = () => {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 从 status 获取用户协议和隐私政策的启用状态
setHasUserAgreement(status.user_agreement_enabled || false);
setHasPrivacyPolicy(status.privacy_policy_enabled || false);
}, [status]);
useEffect(() => {
@@ -118,6 +125,10 @@ const LoginForm = () => {
}, []);
const onWeChatLoginClicked = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setWechatLoading(true);
setShowWeChatLoginModal(true);
setWechatLoading(false);
@@ -157,6 +168,10 @@ const LoginForm = () => {
}
async function handleSubmit(e) {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
@@ -208,6 +223,10 @@ const LoginForm = () => {
// 添加Telegram登录处理函数
const onTelegramLoginClicked = async (response) => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
const fields = [
'id',
'first_name',
@@ -244,6 +263,10 @@ const LoginForm = () => {
// 包装的GitHub登录点击处理
const handleGitHubClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setGithubLoading(true);
try {
onGitHubOAuthClicked(status.github_client_id);
@@ -255,6 +278,10 @@ const LoginForm = () => {
// 包装的OIDC登录点击处理
const handleOIDCClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setOidcLoading(true);
try {
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
@@ -266,6 +293,10 @@ const LoginForm = () => {
// 包装的LinuxDO登录点击处理
const handleLinuxDOClick = () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
setLinuxdoLoading(true);
try {
onLinuxDOOAuthClicked(status.linuxdo_client_id);
@@ -283,6 +314,10 @@ const LoginForm = () => {
};
const handlePasskeyLogin = async () => {
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
showInfo(t('请先阅读并同意用户协议和隐私政策'));
return;
}
if (!passkeySupported) {
showInfo('当前环境无法使用 Passkey 登录');
return;
@@ -486,6 +521,44 @@ const LoginForm = () => {
</Button>
</div>
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='mt-6'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
{!status.self_use_mode_enabled && (
<div className='mt-6 text-center text-sm'>
<Text>
@@ -554,6 +627,44 @@ const LoginForm = () => {
prefix={<IconLock />}
/>
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='pt-4'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
<div className='space-y-2 pt-2'>
<Button
theme='solid'
@@ -562,6 +673,7 @@ const LoginForm = () => {
htmlType='submit'
onClick={handleSubmit}
loading={loginLoading}
disabled={(hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms}
>
{t('继续')}
</Button>

View File

@@ -30,7 +30,7 @@ import {
setUserData,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import {
@@ -82,6 +82,9 @@ const RegisterForm = () => {
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const logo = getLogo();
const systemName = getSystemName();
@@ -106,6 +109,10 @@ const RegisterForm = () => {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 从 status 获取用户协议和隐私政策的启用状态
setHasUserAgreement(status.user_agreement_enabled || false);
setHasPrivacyPolicy(status.privacy_policy_enabled || false);
}, [status]);
useEffect(() => {
@@ -505,6 +512,44 @@ const RegisterForm = () => {
</>
)}
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='pt-4'>
<Checkbox
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
>
<Text size='small' className='text-gray-600'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<>
<a
href='/user-agreement'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('用户协议')}
</a>
</>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<>
<a
href='/privacy-policy'
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 mx-1'
>
{t('隐私政策')}
</a>
</>
)}
</Text>
</Checkbox>
</div>
)}
<div className='space-y-2 pt-2'>
<Button
theme='solid'
@@ -513,6 +558,7 @@ const RegisterForm = () => {
htmlType='submit'
onClick={handleSubmit}
loading={registerLoading}
disabled={(hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms}
>
{t('注册')}
</Button>

View File

@@ -0,0 +1,243 @@
/*
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, { useEffect, useState } from 'react';
import { API, showError } from '../../../helpers';
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui';
const { Title } = Typography;
import {
IllustrationConstruction,
IllustrationConstructionDark,
} from '@douyinfe/semi-illustrations';
import { useTranslation } from 'react-i18next';
import MarkdownRenderer from '../markdown/MarkdownRenderer';
// 检查是否为 URL
const isUrl = (content) => {
try {
new URL(content.trim());
return true;
} catch {
return false;
}
};
// 检查是否为 HTML 内容
const isHtmlContent = (content) => {
if (!content || typeof content !== 'string') return false;
// 检查是否包含HTML标签
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
return htmlTagRegex.test(content);
};
// 安全地渲染HTML内容
const sanitizeHtml = (html) => {
// 创建一个临时元素来解析HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 提取样式
const styles = Array.from(tempDiv.querySelectorAll('style'))
.map(style => style.innerHTML)
.join('\n');
// 提取body内容如果没有body标签则使用全部内容
const bodyContent = tempDiv.querySelector('body');
const content = bodyContent ? bodyContent.innerHTML : html;
return { content, styles };
};
/**
* 通用文档渲染组件
* @param {string} apiEndpoint - API 接口地址
* @param {string} title - 文档标题
* @param {string} cacheKey - 本地存储缓存键
* @param {string} emptyMessage - 空内容时的提示消息
*/
const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
const { t } = useTranslation();
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [htmlStyles, setHtmlStyles] = useState('');
const [processedHtmlContent, setProcessedHtmlContent] = useState('');
const loadContent = async () => {
// 先从缓存中获取
const cachedContent = localStorage.getItem(cacheKey) || '';
if (cachedContent) {
setContent(cachedContent);
processContent(cachedContent);
setLoading(false);
}
try {
const res = await API.get(apiEndpoint);
const { success, message, data } = res.data;
if (success && data) {
setContent(data);
processContent(data);
localStorage.setItem(cacheKey, data);
} else {
if (!cachedContent) {
showError(message || emptyMessage);
setContent('');
}
}
} catch (error) {
if (!cachedContent) {
showError(emptyMessage);
setContent('');
}
} finally {
setLoading(false);
}
};
const processContent = (rawContent) => {
if (isHtmlContent(rawContent)) {
const { content: htmlContent, styles } = sanitizeHtml(rawContent);
setProcessedHtmlContent(htmlContent);
setHtmlStyles(styles);
} else {
setProcessedHtmlContent('');
setHtmlStyles('');
}
};
useEffect(() => {
loadContent();
}, []);
// 处理HTML样式注入
useEffect(() => {
const styleId = `document-renderer-styles-${cacheKey}`;
if (htmlStyles) {
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
styleEl.type = 'text/css';
document.head.appendChild(styleEl);
}
styleEl.innerHTML = htmlStyles;
} else {
const el = document.getElementById(styleId);
if (el) el.remove();
}
return () => {
const el = document.getElementById(styleId);
if (el) el.remove();
};
}, [htmlStyles, cacheKey]);
// 显示加载状态
if (loading) {
return (
<div className='flex justify-center items-center min-h-screen'>
<Spin size='large' />
</div>
);
}
// 如果没有内容,显示空状态
if (!content || content.trim() === '') {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50'>
<Empty
title={t('管理员未设置' + title + '内容')}
image={<IllustrationConstruction style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 150, height: 150 }} />}
className='p-8'
/>
</div>
);
}
// 如果是 URL显示链接卡片
if (isUrl(content)) {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50 p-4'>
<Card className='max-w-md w-full'>
<div className='text-center'>
<Title heading={4} className='mb-4'>{title}</Title>
<p className='text-gray-600 mb-4'>
{t('管理员设置了外部链接,点击下方按钮访问')}
</p>
<a
href={content.trim()}
target='_blank'
rel='noopener noreferrer'
title={content.trim()}
aria-label={`${t('访问' + title)}: ${content.trim()}`}
className='inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'
>
{t('访问' + title)}
</a>
</div>
</Card>
</div>
);
}
// 如果是 HTML 内容,直接渲染
if (isHtmlContent(content)) {
const { content: htmlContent, styles } = sanitizeHtml(content);
// 设置样式(如果有的话)
useEffect(() => {
if (styles && styles !== htmlStyles) {
setHtmlStyles(styles);
}
}, [content, styles, htmlStyles]);
return (
<div className='min-h-screen bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>{title}</Title>
<div
className='prose prose-lg max-w-none'
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
</div>
</div>
</div>
);
}
// 其他内容统一使用 Markdown 渲染器
return (
<div className='min-h-screen bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>{title}</Title>
<div className='prose prose-lg max-w-none'>
<MarkdownRenderer content={content} />
</div>
</div>
</div>
</div>
);
};
export default DocumentRenderer;

View File

@@ -34,10 +34,15 @@ import { useTranslation } from 'react-i18next';
import { StatusContext } from '../../context/Status';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
const LEGAL_USER_AGREEMENT_KEY = 'legal.user_agreement';
const LEGAL_PRIVACY_POLICY_KEY = 'legal.privacy_policy';
const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
Notice: '',
[LEGAL_USER_AGREEMENT_KEY]: '',
[LEGAL_PRIVACY_POLICY_KEY]: '',
SystemName: '',
Logo: '',
Footer: '',
@@ -69,6 +74,8 @@ const OtherSetting = () => {
const [loadingInput, setLoadingInput] = useState({
Notice: false,
[LEGAL_USER_AGREEMENT_KEY]: false,
[LEGAL_PRIVACY_POLICY_KEY]: false,
SystemName: false,
Logo: false,
HomePageContent: false,
@@ -96,6 +103,50 @@ const OtherSetting = () => {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
}
};
// 通用设置 - UserAgreement
const submitUserAgreement = async () => {
try {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_USER_AGREEMENT_KEY]: true,
}));
await updateOption(
LEGAL_USER_AGREEMENT_KEY,
inputs[LEGAL_USER_AGREEMENT_KEY],
);
showSuccess(t('用户协议已更新'));
} catch (error) {
console.error(t('用户协议更新失败'), error);
showError(t('用户协议更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_USER_AGREEMENT_KEY]: false,
}));
}
};
// 通用设置 - PrivacyPolicy
const submitPrivacyPolicy = async () => {
try {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_PRIVACY_POLICY_KEY]: true,
}));
await updateOption(
LEGAL_PRIVACY_POLICY_KEY,
inputs[LEGAL_PRIVACY_POLICY_KEY],
);
showSuccess(t('隐私政策已更新'));
} catch (error) {
console.error(t('隐私政策更新失败'), error);
showError(t('隐私政策更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
[LEGAL_PRIVACY_POLICY_KEY]: false,
}));
}
};
// 个性化设置
const formAPIPersonalization = useRef();
// 个性化设置 - SystemName
@@ -324,6 +375,40 @@ const OtherSetting = () => {
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
{t('设置公告')}
</Button>
<Form.TextArea
label={t('用户协议')}
placeholder={t(
'在此输入用户协议内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_USER_AGREEMENT_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议')}
/>
<Button
onClick={submitUserAgreement}
loading={loadingInput[LEGAL_USER_AGREEMENT_KEY]}
>
{t('设置用户协议')}
</Button>
<Form.TextArea
label={t('隐私政策')}
placeholder={t(
'在此输入隐私政策内容,支持 Markdown & HTML 代码',
)}
field={LEGAL_PRIVACY_POLICY_KEY}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
helpText={t('填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策')}
/>
<Button
onClick={submitPrivacyPolicy}
loading={loadingInput[LEGAL_PRIVACY_POLICY_KEY]}
>
{t('设置隐私政策')}
</Button>
</Form.Section>
</Card>
</Form>

View File

@@ -705,8 +705,15 @@ const SystemSetting = () => {
<Card>
<Form.Section text={t('代理设置')}>
<Banner
type='info'
description={t(
'此代理仅用于图片请求转发Webhook通知发送等AI API请求仍然由服务器直接发出可在渠道设置中单独配置代理',
)}
style={{ marginBottom: 20, marginTop: 16 }}
/>
<Text>
支持{' '}
{t('仅支持')}{' '}
<a
href='https://github.com/Calcium-Ion/new-api-worker'
target='_blank'
@@ -714,7 +721,7 @@ const SystemSetting = () => {
>
new-api-worker
</a>
{' '}{t('或其兼容new-api-worker格式的其他版本')}
</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}

View File

@@ -91,7 +91,7 @@ const REGION_EXAMPLE = {
// 支持并且已适配通过接口获取模型列表的渠道类型
const MODEL_FETCHABLE_TYPES = new Set([
1, 4, 14, 34, 17, 26, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43,
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43,
]);
function type2secretPrompt(type) {

View File

@@ -88,6 +88,11 @@ export const CHANNEL_OPTIONS = [
color: 'purple',
label: '智谱 GLM-4V',
},
{
value: 27,
color: 'blue',
label: 'Perplexity',
},
{
value: 24,
color: 'orange',

View File

@@ -54,6 +54,7 @@ import {
FastGPT,
Kling,
Jimeng,
Perplexity,
} from '@lobehub/icons';
import {
@@ -309,6 +310,8 @@ export function getChannelIcon(channelType) {
return <Xinference.Color size={iconSize} />;
case 25: // Moonshot
return <Moonshot size={iconSize} />;
case 27: // Perplexity
return <Perplexity.Color size={iconSize} />;
case 20: // OpenRouter
return <OpenRouter size={iconSize} />;
case 19: // 360 智脑

View File

@@ -245,6 +245,8 @@
"检查更新": "Check for updates",
"公告": "Announcement",
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code",
"保存公告": "Save Announcement",
"个性化设置": "Personalization Settings",
"系统名称": "System Name",
@@ -1261,6 +1263,8 @@
"仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded",
"设置公告": "Set notice",
"设置用户协议": "Set user agreement",
"设置隐私政策": "Set privacy policy",
"设置 Logo": "Set Logo",
"设置首页内容": "Set home page content",
"设置关于": "Set about",
@@ -2260,5 +2264,21 @@
"补单成功": "Order completed successfully",
"补单失败": "Failed to complete order",
"确认补单": "Confirm Order Completion",
"是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?"
"是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?",
"用户协议": "User Agreement",
"隐私政策": "Privacy Policy",
"用户协议更新失败": "Failed to update user agreement",
"隐私政策更新失败": "Failed to update privacy policy",
"管理员未设置用户协议内容": "Administrator has not set user agreement content",
"管理员未设置隐私政策内容": "Administrator has not set privacy policy content",
"加载用户协议内容失败...": "Failed to load user agreement content...",
"加载隐私政策内容失败...": "Failed to load privacy policy content...",
"我已阅读并同意": "I have read and agree to",
"和": " and ",
"请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement when registering",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy when registering",
"管理员设置了外部链接,点击下方按钮访问": "Administrator has set an external link, click the button below to access",
"访问用户协议": "Access User Agreement",
"访问隐私政策": "Access Privacy Policy"
}

View File

@@ -2252,5 +2252,27 @@
"补单成功": "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 ?"
"是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?",
"用户协议": "Accord utilisateur",
"隐私政策": "Politique de confidentialité",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de l'accord utilisateur, prend en charge le code Markdown et HTML",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de la politique de confidentialité, prend en charge le code Markdown et HTML",
"设置用户协议": "Définir l'accord utilisateur",
"设置隐私政策": "Définir la politique de confidentialité",
"用户协议已更新": "L'accord utilisateur a été mis à jour",
"隐私政策已更新": "La politique de confidentialité a été mise à jour",
"用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur",
"隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité",
"管理员未设置用户协议内容": "L'administrateur n'a pas défini le contenu de l'accord utilisateur",
"管理员未设置隐私政策内容": "L'administrateur n'a pas défini le contenu de la politique de confidentialité",
"加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...",
"加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...",
"我已阅读并同意": "J'ai lu et j'accepte",
"和": " et ",
"请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher qu'ils ont lu l'accord utilisateur lors de l'inscription",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher qu'ils ont lu la politique de confidentialité lors de l'inscription",
"管理员设置了外部链接,点击下方按钮访问": "L'administrateur a défini un lien externe, cliquez sur le bouton ci-dessous pour accéder",
"访问用户协议": "Accéder à l'accord utilisateur",
"访问隐私政策": "Accéder à la politique de confidentialité"
}

View File

@@ -111,5 +111,27 @@
"补单失败": "补单失败",
"确认补单": "确认补单",
"是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?",
"操作": "操作"
"操作": "操作",
"用户协议": "用户协议",
"隐私政策": "隐私政策",
"在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码",
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码",
"设置用户协议": "设置用户协议",
"设置隐私政策": "设置隐私政策",
"用户协议已更新": "用户协议已更新",
"隐私政策已更新": "隐私政策已更新",
"用户协议更新失败": "用户协议更新失败",
"隐私政策更新失败": "隐私政策更新失败",
"管理员未设置用户协议内容": "管理员未设置用户协议内容",
"管理员未设置隐私政策内容": "管理员未设置隐私政策内容",
"加载用户协议内容失败...": "加载用户协议内容失败...",
"加载隐私政策内容失败...": "加载隐私政策内容失败...",
"我已阅读并同意": "我已阅读并同意",
"和": "和",
"请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策",
"填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议",
"填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策",
"管理员设置了外部链接,点击下方按钮访问": "管理员设置了外部链接,点击下方按钮访问",
"访问用户协议": "访问用户协议",
"访问隐私政策": "访问隐私政策"
}

View File

@@ -0,0 +1,37 @@
/*
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 from 'react';
import { useTranslation } from 'react-i18next';
import DocumentRenderer from '../../components/common/DocumentRenderer';
const PrivacyPolicy = () => {
const { t } = useTranslation();
return (
<DocumentRenderer
apiEndpoint="/api/privacy-policy"
title={t('隐私政策')}
cacheKey="privacy_policy"
emptyMessage={t('加载隐私政策内容失败...')}
/>
);
};
export default PrivacyPolicy;

View File

@@ -0,0 +1,37 @@
/*
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 from 'react';
import { useTranslation } from 'react-i18next';
import DocumentRenderer from '../../components/common/DocumentRenderer';
const UserAgreement = () => {
const { t } = useTranslation();
return (
<DocumentRenderer
apiEndpoint="/api/user-agreement"
title={t('用户协议')}
cacheKey="user_agreement"
emptyMessage={t('加载用户协议内容失败...')}
/>
);
};
export default UserAgreement;