diff --git a/common/constants.go b/common/constants.go
index c4d2511ef357..1cb4d56e343d 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -104,6 +104,24 @@ var SMTPAccount = ""
var SMTPFrom = ""
var SMTPToken = ""
+// SMS 短信通知系统级配置
+var SMSProvider = "" // 短信服务商: aliyun, sendcloud, tencent, custom
+var SMSAliyunAccessKeyId = "" // 阿里云 AccessKeyId
+var SMSAliyunAccessKeySecret = "" // 阿里云 AccessKeySecret
+var SMSAliyunSignName = "" // 阿里云短信签名
+var SMSAliyunTemplateCode = "" // 阿里云短信模板Code
+var SMSSendCloudSmsUser = "" // SendCloud smsUser
+var SMSSendCloudSmsKey = "" // SendCloud smsKey
+var SMSSendCloudTemplateId = "" // SendCloud 短信模板ID
+var SMSTencentSecretId = "" // 腾讯云 SecretId
+var SMSTencentSecretKey = "" // 腾讯云 SecretKey
+var SMSTencentSmsSdkAppId = "" // 腾讯云 SmsSdkAppId
+var SMSTencentSignName = "" // 腾讯云短信签名
+var SMSTencentTemplateId = "" // 腾讯云短信模板ID
+var SMSCustomUrl = "" // 通用HTTP接口地址
+var SMSCustomMethod = "POST" // 通用HTTP请求方法
+var SMSCustomTemplate = "" // 通用HTTP请求模板
+
var GitHubClientId = ""
var GitHubClientSecret = ""
var LinuxDOClientId = ""
diff --git a/controller/option.go b/controller/option.go
index d42db346bdfd..10496d1b5f75 100644
--- a/controller/option.go
+++ b/controller/option.go
@@ -79,7 +79,12 @@ func GetOptions(c *gin.Context) {
strings.HasSuffix(k, "Secret") ||
strings.HasSuffix(k, "Key") ||
strings.HasSuffix(k, "secret") ||
- strings.HasSuffix(k, "api_key")
+ strings.HasSuffix(k, "api_key") ||
+ // SMS 敏感凭证字段(ID类也隐藏,防止凭证泄露)
+ k == "SMSAliyunAccessKeyId" ||
+ k == "SMSSendCloudSmsUser" ||
+ k == "SMSTencentSecretId" ||
+ k == "SMSTencentSmsSdkAppId"
if isSensitiveKey && !isVisiblePublicKeyOption(k) {
continue
}
diff --git a/controller/user.go b/controller/user.go
index b5722668632d..457f69b08b81 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "regexp"
"strconv"
"strings"
"sync"
@@ -1120,6 +1121,7 @@ type UpdateUserSettingRequest struct {
GotifyUrl string `json:"gotify_url,omitempty"`
GotifyToken string `json:"gotify_token,omitempty"`
GotifyPriority int `json:"gotify_priority,omitempty"`
+ SmsPhoneNumber string `json:"sms_phone_number,omitempty"`
UpstreamModelUpdateNotifyEnabled *bool `json:"upstream_model_update_notify_enabled,omitempty"`
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
RecordIpLog bool `json:"record_ip_log"`
@@ -1133,7 +1135,7 @@ func UpdateUserSetting(c *gin.Context) {
}
// 验证预警类型
- if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify {
+ if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify && req.QuotaWarningType != dto.NotifyTypeSms {
common.ApiErrorI18n(c, i18n.MsgSettingInvalidType)
return
}
@@ -1206,6 +1208,24 @@ func UpdateUserSetting(c *gin.Context) {
}
}
+ // 如果是SMS类型,验证手机号和系统SMS配置
+ if req.QuotaWarningType == dto.NotifyTypeSms {
+ if req.SmsPhoneNumber == "" {
+ common.ApiErrorI18n(c, i18n.MsgSettingSmsPhoneEmpty)
+ return
+ }
+ // 验证手机号格式:允许国际号码格式,7-15位数字,可选+前缀
+ phoneRegex := regexp.MustCompile(`^\+?[0-9]{7,15}$`)
+ if !phoneRegex.MatchString(req.SmsPhoneNumber) {
+ common.ApiErrorI18n(c, i18n.MsgSettingSmsPhoneInvalid)
+ return
+ }
+ if common.SMSProvider == "" {
+ common.ApiErrorI18n(c, i18n.MsgSettingSmsNotConfigured)
+ return
+ }
+ }
+
userId := c.GetInt("id")
user, err := model.GetUserById(userId, true)
if err != nil {
@@ -1257,6 +1277,11 @@ func UpdateUserSetting(c *gin.Context) {
}
}
+ // 如果是SMS类型,添加手机号到设置中
+ if req.QuotaWarningType == dto.NotifyTypeSms {
+ settings.SmsPhoneNumber = req.SmsPhoneNumber
+ }
+
// 更新用户设置
user.SetSetting(settings)
if err := user.Update(false); err != nil {
diff --git a/dto/user_settings.go b/dto/user_settings.go
index dbf555fadfa8..1f20fdc748bf 100644
--- a/dto/user_settings.go
+++ b/dto/user_settings.go
@@ -16,6 +16,7 @@ type UserSetting struct {
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
BillingPreference string `json:"billing_preference,omitempty"` // BillingPreference 扣费策略(订阅/钱包)
Language string `json:"language,omitempty"` // Language 用户语言偏好 (zh, en)
+ SmsPhoneNumber string `json:"sms_phone_number,omitempty"` // SmsPhoneNumber 接收短信的手机号码
}
var (
@@ -23,4 +24,5 @@ var (
NotifyTypeWebhook = "webhook" // Webhook
NotifyTypeBark = "bark" // Bark 推送
NotifyTypeGotify = "gotify" // Gotify 推送
+ NotifyTypeSms = "sms" // SMS 短信通知
)
diff --git a/i18n/keys.go b/i18n/keys.go
index 8e551d2ea657..1bc98ad250d5 100644
--- a/i18n/keys.go
+++ b/i18n/keys.go
@@ -246,6 +246,9 @@ const (
MsgSettingGotifyTokenEmpty = "setting.gotify_token_empty"
MsgSettingGotifyUrlInvalid = "setting.gotify_url_invalid"
MsgSettingUrlMustHttp = "setting.url_must_http"
+ MsgSettingSmsPhoneEmpty = "setting.sms_phone_empty"
+ MsgSettingSmsPhoneInvalid = "setting.sms_phone_invalid"
+ MsgSettingSmsNotConfigured = "setting.sms_not_configured"
MsgSettingSaved = "setting.saved"
)
diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml
index 064abc708420..7a4a80b7ebad 100644
--- a/i18n/locales/en.yaml
+++ b/i18n/locales/en.yaml
@@ -208,6 +208,9 @@ setting.gotify_url_empty: "Gotify server URL cannot be empty"
setting.gotify_token_empty: "Gotify token cannot be empty"
setting.gotify_url_invalid: "Invalid Gotify server URL"
setting.url_must_http: "URL must start with http:// or https://"
+setting.sms_phone_empty: "Phone number cannot be empty"
+setting.sms_phone_invalid: "Invalid phone number format"
+setting.sms_not_configured: "SMS service is not configured, please contact the administrator"
setting.saved: "Settings updated"
# Deployment messages (io.net)
diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml
index b05ca743f33a..968aa01bc883 100644
--- a/i18n/locales/zh-CN.yaml
+++ b/i18n/locales/zh-CN.yaml
@@ -209,6 +209,9 @@ setting.gotify_url_empty: "Gotify服务器地址不能为空"
setting.gotify_token_empty: "Gotify令牌不能为空"
setting.gotify_url_invalid: "无效的Gotify服务器地址"
setting.url_must_http: "URL必须以http://或https://开头"
+setting.sms_phone_empty: "手机号码不能为空"
+setting.sms_phone_invalid: "手机号码格式不正确"
+setting.sms_not_configured: "系统未配置短信服务,请联系管理员"
setting.saved: "设置已更新"
# Deployment messages (io.net)
diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml
index 6ae222b711eb..fff9d6f5086b 100644
--- a/i18n/locales/zh-TW.yaml
+++ b/i18n/locales/zh-TW.yaml
@@ -209,6 +209,9 @@ setting.gotify_url_empty: "Gotify伺服器位址不能為空"
setting.gotify_token_empty: "Gotify令牌不能為空"
setting.gotify_url_invalid: "無效的Gotify伺服器位址"
setting.url_must_http: "URL必須以http://或https://開頭"
+setting.sms_phone_empty: "手機號碼不能為空"
+setting.sms_phone_invalid: "手機號碼格式不正確"
+setting.sms_not_configured: "系統未配置簡訊服務,請聯繫管理員"
setting.saved: "設定已更新"
# Deployment messages (io.net)
diff --git a/model/option.go b/model/option.go
index e0a3048d34f2..684ede14d10d 100644
--- a/model/option.go
+++ b/model/option.go
@@ -63,6 +63,22 @@ func InitOptionMap() {
common.OptionMap["SMTPToken"] = ""
common.OptionMap["SMTPSSLEnabled"] = strconv.FormatBool(common.SMTPSSLEnabled)
common.OptionMap["SMTPForceAuthLogin"] = strconv.FormatBool(common.SMTPForceAuthLogin)
+ common.OptionMap["SMSProvider"] = ""
+ common.OptionMap["SMSAliyunAccessKeyId"] = ""
+ common.OptionMap["SMSAliyunAccessKeySecret"] = ""
+ common.OptionMap["SMSAliyunSignName"] = ""
+ common.OptionMap["SMSAliyunTemplateCode"] = ""
+ common.OptionMap["SMSSendCloudSmsUser"] = ""
+ common.OptionMap["SMSSendCloudSmsKey"] = ""
+ common.OptionMap["SMSSendCloudTemplateId"] = ""
+ common.OptionMap["SMSTencentSecretId"] = ""
+ common.OptionMap["SMSTencentSecretKey"] = ""
+ common.OptionMap["SMSTencentSmsSdkAppId"] = ""
+ common.OptionMap["SMSTencentSignName"] = ""
+ common.OptionMap["SMSTencentTemplateId"] = ""
+ common.OptionMap["SMSCustomUrl"] = ""
+ common.OptionMap["SMSCustomMethod"] = "POST"
+ common.OptionMap["SMSCustomTemplate"] = ""
common.OptionMap["Notice"] = ""
common.OptionMap["About"] = ""
common.OptionMap["HomePageContent"] = ""
@@ -345,6 +361,46 @@ func updateOptionMap(key string, value string) (err error) {
common.SMTPFrom = value
case "SMTPToken":
common.SMTPToken = value
+ case "SMSProvider":
+ common.SMSProvider = value
+ case "SMSAliyunAccessKeyId":
+ common.SMSAliyunAccessKeyId = value
+ case "SMSAliyunAccessKeySecret":
+ common.SMSAliyunAccessKeySecret = value
+ case "SMSAliyunSignName":
+ common.SMSAliyunSignName = value
+ case "SMSAliyunTemplateCode":
+ common.SMSAliyunTemplateCode = value
+ case "SMSSendCloudSmsUser":
+ common.SMSSendCloudSmsUser = value
+ case "SMSSendCloudSmsKey":
+ common.SMSSendCloudSmsKey = value
+ case "SMSSendCloudTemplateId":
+ common.SMSSendCloudTemplateId = value
+ case "SMSTencentSecretId":
+ common.SMSTencentSecretId = value
+ case "SMSTencentSecretKey":
+ common.SMSTencentSecretKey = value
+ case "SMSTencentSmsSdkAppId":
+ common.SMSTencentSmsSdkAppId = value
+ case "SMSTencentSignName":
+ common.SMSTencentSignName = value
+ case "SMSTencentTemplateId":
+ common.SMSTencentTemplateId = value
+ case "SMSCustomUrl":
+ common.SMSCustomUrl = value
+ case "SMSCustomMethod":
+ upperMethod := strings.ToUpper(value)
+ switch upperMethod {
+ case "GET", "POST", "PUT", "DELETE", "PATCH":
+ common.SMSCustomMethod = upperMethod
+ case "":
+ // 空值保留默认的 POST
+ default:
+ common.SMSCustomMethod = "POST"
+ }
+ case "SMSCustomTemplate":
+ common.SMSCustomTemplate = value
case "ServerAddress":
system_setting.ServerAddress = value
case "WorkerUrl":
diff --git a/service/quota.go b/service/quota.go
index 398bd1b792d1..1d90d8150fd6 100644
--- a/service/quota.go
+++ b/service/quota.go
@@ -478,6 +478,12 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
// Bark推送使用简短文本,不支持HTML
content = "{{value}},剩余额度:{{value}},请及时充值"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
+ } else if notifyType == dto.NotifyTypeSms {
+ // SMS短信使用模板变量:values[0]=当前余额, values[1]=告警阈值
+ // content 保留可读文本供自定义HTTP接口的 {{content}} 占位符使用
+ remainQuota := relayInfo.UserQuota - consumeQuota
+ content = fmt.Sprintf("当前剩余额度 %s,告警阈值 %s,请及时充值", logger.FormatQuota(remainQuota), logger.FormatQuota(threshold))
+ values = []interface{}{logger.FormatQuota(remainQuota), logger.FormatQuota(threshold)}
} else if notifyType == dto.NotifyTypeGotify {
content = "{{value}},当前剩余额度为 {{value}},请及时充值。"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
@@ -529,6 +535,11 @@ func checkAndSendSubscriptionQuotaNotify(relayInfo *relaycommon.RelayInfo) {
if notifyType == dto.NotifyTypeBark {
content = "{{value}},剩余额度:{{value}},请及时充值"
values = []interface{}{prompt, logger.FormatQuota(int(remaining))}
+ } else if notifyType == dto.NotifyTypeSms {
+ // SMS短信使用模板变量:values[0]=当前余额, values[1]=告警阈值
+ // content 保留可读文本供自定义HTTP接口的 {{content}} 占位符使用
+ content = fmt.Sprintf("当前剩余额度 %s,告警阈值 %s,请及时充值", logger.FormatQuota(int(remaining)), logger.FormatQuota(threshold))
+ values = []interface{}{logger.FormatQuota(int(remaining)), logger.FormatQuota(threshold)}
} else if notifyType == dto.NotifyTypeGotify {
content = "{{value}},当前剩余额度为 {{value}},请及时充值。"
values = []interface{}{prompt, logger.FormatQuota(int(remaining))}
diff --git a/service/sms_notify.go b/service/sms_notify.go
new file mode 100644
index 000000000000..f966f71e2b17
--- /dev/null
+++ b/service/sms_notify.go
@@ -0,0 +1,451 @@
+package service
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/md5"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/dto"
+ "github.com/QuantumNous/new-api/setting/system_setting"
+)
+
+func sendSmsNotify(phoneNumber string, data dto.Notify) error {
+ if phoneNumber == "" {
+ return fmt.Errorf("sms phone number is empty")
+ }
+
+ provider := common.SMSProvider
+ if provider == "" {
+ return fmt.Errorf("SMS provider is not configured, please contact the administrator")
+ }
+
+ // 提取模板变量值列表(用于模板类短信服务商)
+ var templateValues []string
+ for _, v := range data.Values {
+ templateValues = append(templateValues, fmt.Sprintf("%v", v))
+ }
+
+ // 生成纯文本 content(用于自定义HTTP接口)
+ content := data.Content
+ for _, value := range data.Values {
+ content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1)
+ }
+
+ switch provider {
+ case "aliyun":
+ return sendAliyunSms(
+ common.SMSAliyunAccessKeyId,
+ common.SMSAliyunAccessKeySecret,
+ common.SMSAliyunSignName,
+ common.SMSAliyunTemplateCode,
+ phoneNumber,
+ templateValues,
+ )
+ case "sendcloud":
+ return sendSendCloudSms(
+ common.SMSSendCloudSmsUser,
+ common.SMSSendCloudSmsKey,
+ common.SMSSendCloudTemplateId,
+ phoneNumber,
+ templateValues,
+ )
+ case "tencent":
+ return sendTencentSms(
+ common.SMSTencentSecretId,
+ common.SMSTencentSecretKey,
+ common.SMSTencentSmsSdkAppId,
+ common.SMSTencentSignName,
+ common.SMSTencentTemplateId,
+ phoneNumber,
+ templateValues,
+ )
+ case "custom":
+ return sendCustomSms(
+ common.SMSCustomUrl,
+ common.SMSCustomMethod,
+ common.SMSCustomTemplate,
+ phoneNumber,
+ data.Title,
+ content,
+ )
+ default:
+ return fmt.Errorf("unsupported sms provider: %s", provider)
+ }
+}
+
+// aliyunPercentEncode 阿里云专用的百分号编码
+func aliyunPercentEncode(s string) string {
+ encoded := url.QueryEscape(s)
+ encoded = strings.ReplaceAll(encoded, "+", "%20")
+ encoded = strings.ReplaceAll(encoded, "*", "%2A")
+ encoded = strings.ReplaceAll(encoded, "%7E", "~")
+ return encoded
+}
+
+// sendAliyunSms 通过阿里云短信服务发送短信 (POP v1 签名, HMAC-SHA1)
+// templateValues: 模板变量值列表,按顺序映射为 user_money, balance_warn
+func sendAliyunSms(accessKeyId, accessKeySecret, signName, templateCode, phoneNumber string, templateValues []string) error {
+ // 构建模板参数 JSON
+ templateParamMap := map[string]string{}
+ paramNames := []string{"user_money", "balance_warn"}
+ for i, name := range paramNames {
+ if i < len(templateValues) {
+ templateParamMap[name] = templateValues[i]
+ }
+ }
+ templateParam, _ := common.Marshal(templateParamMap)
+
+ params := map[string]string{
+ "AccessKeyId": accessKeyId,
+ "Action": "SendSms",
+ "Format": "JSON",
+ "PhoneNumbers": phoneNumber,
+ "SignName": signName,
+ "SignatureMethod": "HMAC-SHA1",
+ "SignatureNonce": common.GetUUID(),
+ "SignatureVersion": "1.0",
+ "TemplateCode": templateCode,
+ "TemplateParam": string(templateParam),
+ "Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
+ "Version": "2017-05-25",
+ }
+
+ keys := make([]string, 0, len(params))
+ for k := range params {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var queryParts []string
+ for _, k := range keys {
+ queryParts = append(queryParts, aliyunPercentEncode(k)+"="+aliyunPercentEncode(params[k]))
+ }
+ canonicalizedQueryString := strings.Join(queryParts, "&")
+
+ stringToSign := "GET&" + aliyunPercentEncode("/") + "&" + aliyunPercentEncode(canonicalizedQueryString)
+
+ mac := hmac.New(sha1.New, []byte(accessKeySecret+"&"))
+ mac.Write([]byte(stringToSign))
+ signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
+
+ finalURL := "https://dysmsapi.aliyuncs.com/?Signature=" + url.QueryEscape(signature) + "&" + canonicalizedQueryString
+
+ resp, err := GetHttpClient().Get(finalURL)
+ if err != nil {
+ return fmt.Errorf("failed to send aliyun sms: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read aliyun sms response: %v", err)
+ }
+
+ var result struct {
+ Code string `json:"Code"`
+ Message string `json:"Message"`
+ }
+ if err := common.Unmarshal(body, &result); err != nil {
+ return fmt.Errorf("failed to parse aliyun sms response: %v", err)
+ }
+
+ if result.Code != "OK" {
+ return fmt.Errorf("aliyun sms failed: %s - %s", result.Code, result.Message)
+ }
+
+ return nil
+}
+
+// sendSendCloudSms 通过 SendCloud 短信服务发送短信
+// 使用 MD5 签名: signature = MD5(smsKey + "&" + 排序参数串 + "&" + smsKey)
+func sendSendCloudSms(smsUser, smsKey, templateId, phoneNumber string, templateValues []string) error {
+ params := map[string]string{
+ "smsUser": smsUser,
+ "templateId": templateId,
+ "phone": phoneNumber,
+ "msgType": "0",
+ }
+
+ // vars 传递模板变量
+ varsMap := map[string]string{}
+ paramNames := []string{"user_money", "balance_warn"}
+ for i, name := range paramNames {
+ if i < len(templateValues) {
+ varsMap[name] = templateValues[i]
+ }
+ }
+ varsJSON, _ := common.Marshal(varsMap)
+ params["vars"] = string(varsJSON)
+
+ // 按 key 排序生成签名字符串
+ keys := make([]string, 0, len(params))
+ for k := range params {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var paramParts []string
+ for _, k := range keys {
+ paramParts = append(paramParts, k+"="+params[k])
+ }
+ paramStr := strings.Join(paramParts, "&")
+
+ // MD5 签名
+ signStr := smsKey + "&" + paramStr + "&" + smsKey
+ h := md5.New()
+ h.Write([]byte(signStr))
+ signature := hex.EncodeToString(h.Sum(nil))
+ params["signature"] = signature
+
+ // 构建 form data
+ formValues := url.Values{}
+ for k, v := range params {
+ formValues.Set(k, v)
+ }
+
+ resp, err := GetHttpClient().PostForm("https://www.sendcloud.net/smsapi/send", formValues)
+ if err != nil {
+ return fmt.Errorf("failed to send sendcloud sms: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read sendcloud sms response: %v", err)
+ }
+
+ var result struct {
+ Result bool `json:"result"`
+ StatusCode int `json:"statusCode"`
+ Message string `json:"message"`
+ }
+ if err := common.Unmarshal(body, &result); err != nil {
+ return fmt.Errorf("failed to parse sendcloud sms response: %v", err)
+ }
+
+ if !result.Result || result.StatusCode != 200 {
+ return fmt.Errorf("sendcloud sms failed: %d - %s", result.StatusCode, result.Message)
+ }
+
+ return nil
+}
+
+func hmacSHA256(key []byte, data string) []byte {
+ mac := hmac.New(sha256.New, key)
+ mac.Write([]byte(data))
+ return mac.Sum(nil)
+}
+
+func sha256Hex(data string) string {
+ h := sha256.New()
+ h.Write([]byte(data))
+ return hex.EncodeToString(h.Sum(nil))
+}
+
+// sendTencentSms 通过腾讯云短信服务发送短信 (TC3-HMAC-SHA256 签名)
+// templateValues: 模板变量值列表,按顺序对应模板中的 {1}, {2}, ...
+func sendTencentSms(secretId, secretKey, smsSdkAppId, signName, templateId, phoneNumber string, templateValues []string) error {
+ host := "sms.tencentcloudapi.com"
+ service := "sms"
+ action := "SendSms"
+ version := "2021-01-11"
+ region := "ap-guangzhou"
+
+ timestamp := time.Now().Unix()
+ timestampStr := fmt.Sprintf("%d", timestamp)
+ date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
+
+ type SendSmsRequest struct {
+ SmsSdkAppId string `json:"SmsSdkAppId"`
+ SignName string `json:"SignName"`
+ TemplateId string `json:"TemplateId"`
+ PhoneNumberSet []string `json:"PhoneNumberSet"`
+ TemplateParamSet []string `json:"TemplateParamSet"`
+ }
+
+ payload := SendSmsRequest{
+ SmsSdkAppId: smsSdkAppId,
+ SignName: signName,
+ TemplateId: templateId,
+ PhoneNumberSet: []string{phoneNumber},
+ TemplateParamSet: templateValues,
+ }
+
+ payloadBytes, err := common.Marshal(payload)
+ if err != nil {
+ return fmt.Errorf("failed to marshal tencent sms payload: %v", err)
+ }
+ payloadStr := string(payloadBytes)
+
+ contentType := "application/json; charset=utf-8"
+ canonicalHeaders := "content-type:" + contentType + "\n" + "host:" + host + "\n"
+ signedHeaders := "content-type;host"
+ hashedPayload := sha256Hex(payloadStr)
+
+ canonicalRequest := "POST\n/\n\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedPayload
+
+ credentialScope := date + "/" + service + "/tc3_request"
+ stringToSign := "TC3-HMAC-SHA256\n" + timestampStr + "\n" + credentialScope + "\n" + sha256Hex(canonicalRequest)
+
+ secretDate := hmacSHA256([]byte("TC3"+secretKey), date)
+ secretService := hmacSHA256(secretDate, service)
+ secretSigning := hmacSHA256(secretService, "tc3_request")
+ signature := hex.EncodeToString(hmacSHA256(secretSigning, stringToSign))
+
+ authorization := "TC3-HMAC-SHA256 " +
+ "Credential=" + secretId + "/" + credentialScope + ", " +
+ "SignedHeaders=" + signedHeaders + ", " +
+ "Signature=" + signature
+
+ req, err := http.NewRequest(http.MethodPost, "https://"+host, bytes.NewBufferString(payloadStr))
+ if err != nil {
+ return fmt.Errorf("failed to create tencent sms request: %v", err)
+ }
+
+ req.Header.Set("Content-Type", contentType)
+ req.Header.Set("Host", host)
+ req.Header.Set("Authorization", authorization)
+ req.Header.Set("X-TC-Action", action)
+ req.Header.Set("X-TC-Timestamp", timestampStr)
+ req.Header.Set("X-TC-Version", version)
+ req.Header.Set("X-TC-Region", region)
+
+ resp, err := GetHttpClient().Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send tencent sms: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("failed to read tencent sms response: %v", err)
+ }
+
+ var result struct {
+ Response struct {
+ SendStatusSet []struct {
+ Code string `json:"Code"`
+ Message string `json:"Message"`
+ } `json:"SendStatusSet"`
+ Error *struct {
+ Code string `json:"Code"`
+ Message string `json:"Message"`
+ } `json:"Error"`
+ } `json:"Response"`
+ }
+ if err := common.Unmarshal(body, &result); err != nil {
+ return fmt.Errorf("failed to parse tencent sms response: %v", err)
+ }
+
+ if result.Response.Error != nil {
+ return fmt.Errorf("tencent sms failed: %s - %s", result.Response.Error.Code, result.Response.Error.Message)
+ }
+
+ if len(result.Response.SendStatusSet) == 0 {
+ return fmt.Errorf("tencent sms failed: empty SendStatusSet, no delivery result returned")
+ }
+
+ if result.Response.SendStatusSet[0].Code != "Ok" {
+ return fmt.Errorf("tencent sms failed: %s - %s", result.Response.SendStatusSet[0].Code, result.Response.SendStatusSet[0].Message)
+ }
+
+ return nil
+}
+
+// jsonEscapeString 对字符串进行 JSON 转义,防止模板注入
+func jsonEscapeString(s string) string {
+ b, _ := common.Marshal(s)
+ // common.Marshal 返回带引号的字符串,去掉首尾引号
+ return string(b[1 : len(b)-1])
+}
+
+// sendCustomSms 通过通用HTTP接口发送短信
+func sendCustomSms(smsUrl, method, template, phoneNumber, title, content string) error {
+ if smsUrl == "" {
+ return fmt.Errorf("custom sms url is empty")
+ }
+ if method == "" {
+ method = "POST"
+ }
+
+ finalURL := strings.ReplaceAll(smsUrl, "{{phone}}", url.QueryEscape(phoneNumber))
+ finalURL = strings.ReplaceAll(finalURL, "{{title}}", url.QueryEscape(title))
+ finalURL = strings.ReplaceAll(finalURL, "{{content}}", url.QueryEscape(content))
+
+ // body 模板中使用 JSON 转义,防止 JSON 注入
+ finalBody := strings.ReplaceAll(template, "{{phone}}", jsonEscapeString(phoneNumber))
+ finalBody = strings.ReplaceAll(finalBody, "{{title}}", jsonEscapeString(title))
+ finalBody = strings.ReplaceAll(finalBody, "{{content}}", jsonEscapeString(content))
+
+ var req *http.Request
+ var resp *http.Response
+ var err error
+
+ useWorker := system_setting.EnableWorker() && strings.HasPrefix(strings.ToLower(finalURL), "https://")
+ if useWorker {
+ workerReq := &WorkerRequest{
+ URL: finalURL,
+ Key: system_setting.WorkerValidKey,
+ Method: method,
+ Headers: map[string]string{
+ "Content-Type": "application/json; charset=utf-8",
+ "User-Agent": "NewAPI-SMS-Notify/1.0",
+ },
+ }
+ if method == "POST" {
+ workerReq.Body = []byte(finalBody)
+ }
+
+ resp, err = DoWorkerRequest(workerReq)
+ if err != nil {
+ return fmt.Errorf("failed to send custom sms through worker: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("custom sms request failed with status code: %d", resp.StatusCode)
+ }
+ } else {
+ fetchSetting := system_setting.GetFetchSetting()
+ if err := common.ValidateURLWithFetchSetting(finalURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
+ return fmt.Errorf("request reject: %v", err)
+ }
+
+ if method == "POST" {
+ req, err = http.NewRequest(http.MethodPost, finalURL, bytes.NewBufferString(finalBody))
+ } else {
+ req, err = http.NewRequest(http.MethodGet, finalURL, nil)
+ }
+ if err != nil {
+ return fmt.Errorf("failed to create custom sms request: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+ req.Header.Set("User-Agent", "NewAPI-SMS-Notify/1.0")
+
+ client := GetHttpClient()
+ resp, err = client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send custom sms request: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("custom sms request failed with status code: %d", resp.StatusCode)
+ }
+ }
+
+ return nil
+}
diff --git a/service/user_notify.go b/service/user_notify.go
index 27a72b8be427..a6e58dd301cc 100644
--- a/service/user_notify.go
+++ b/service/user_notify.go
@@ -101,6 +101,12 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
return nil
}
return sendGotifyNotify(gotifyUrl, gotifyToken, userSetting.GotifyPriority, data)
+ case dto.NotifyTypeSms:
+ if userSetting.SmsPhoneNumber == "" {
+ common.SysLog(fmt.Sprintf("user %d has no sms phone number, skip sending sms", userId))
+ return nil
+ }
+ return sendSmsNotify(userSetting.SmsPhoneNumber, data)
}
return nil
}
diff --git a/web/classic/src/components/settings/PersonalSetting.jsx b/web/classic/src/components/settings/PersonalSetting.jsx
index e735c877c49b..95e22529d62d 100644
--- a/web/classic/src/components/settings/PersonalSetting.jsx
+++ b/web/classic/src/components/settings/PersonalSetting.jsx
@@ -95,6 +95,7 @@ const PersonalSetting = () => {
upstreamModelUpdateNotifyEnabled: false,
acceptUnsetModelRatioModel: false,
recordIpLog: false,
+ smsPhoneNumber: '',
});
const {
@@ -198,6 +199,7 @@ const PersonalSetting = () => {
acceptUnsetModelRatioModel:
settings.accept_unset_model_ratio_model || false,
recordIpLog: settings.record_ip_log || false,
+ smsPhoneNumber: settings.sms_phone_number || '',
});
}
}, [userState?.user?.setting]);
@@ -528,6 +530,7 @@ const PersonalSetting = () => {
accept_unset_model_ratio_model:
notificationSettings.acceptUnsetModelRatioModel,
record_ip_log: notificationSettings.recordIpLog,
+ sms_phone_number: notificationSettings.smsPhoneNumber,
});
if (res.data.success) {
diff --git a/web/classic/src/components/settings/SystemSetting.jsx b/web/classic/src/components/settings/SystemSetting.jsx
index 63b20c70f4d1..88a8344a61e9 100644
--- a/web/classic/src/components/settings/SystemSetting.jsx
+++ b/web/classic/src/components/settings/SystemSetting.jsx
@@ -69,6 +69,22 @@ const SystemSetting = () => {
SMTPAccount: '',
SMTPFrom: '',
SMTPToken: '',
+ SMSProvider: '',
+ SMSAliyunAccessKeyId: '',
+ SMSAliyunAccessKeySecret: '',
+ SMSAliyunSignName: '',
+ SMSAliyunTemplateCode: '',
+ SMSSendCloudSmsUser: '',
+ SMSSendCloudSmsKey: '',
+ SMSSendCloudTemplateId: '',
+ SMSTencentSecretId: '',
+ SMSTencentSecretKey: '',
+ SMSTencentSmsSdkAppId: '',
+ SMSTencentSignName: '',
+ SMSTencentTemplateId: '',
+ SMSCustomUrl: '',
+ SMSCustomMethod: 'POST',
+ SMSCustomTemplate: '',
WorkerUrl: '',
WorkerValidKey: '',
WorkerAllowHttpImageRequestEnabled: '',
@@ -349,6 +365,51 @@ const SystemSetting = () => {
}
};
+ const submitSMS = async () => {
+ const options = [];
+ // 凭证字段:后端 GetOptions 会隐藏这些字段,前端拿到的是空值
+ // 只有用户填写了新值才提交,避免用空串覆盖已有凭证
+ const secretKeys = new Set([
+ 'SMSAliyunAccessKeyId',
+ 'SMSAliyunAccessKeySecret',
+ 'SMSSendCloudSmsUser',
+ 'SMSSendCloudSmsKey',
+ 'SMSTencentSecretId',
+ 'SMSTencentSecretKey',
+ 'SMSTencentSmsSdkAppId',
+ ]);
+ const smsKeys = [
+ 'SMSProvider',
+ 'SMSAliyunAccessKeyId',
+ 'SMSAliyunAccessKeySecret',
+ 'SMSAliyunSignName',
+ 'SMSAliyunTemplateCode',
+ 'SMSSendCloudSmsUser',
+ 'SMSSendCloudSmsKey',
+ 'SMSSendCloudTemplateId',
+ 'SMSTencentSecretId',
+ 'SMSTencentSecretKey',
+ 'SMSTencentSmsSdkAppId',
+ 'SMSTencentSignName',
+ 'SMSTencentTemplateId',
+ 'SMSCustomUrl',
+ 'SMSCustomMethod',
+ 'SMSCustomTemplate',
+ ];
+ for (const key of smsKeys) {
+ if (originInputs[key] !== inputs[key]) {
+ // 凭证字段仅在有新值时提交,非凭证字段允许清空
+ if (secretKeys.has(key) && inputs[key] === '') {
+ continue;
+ }
+ options.push({ key, value: inputs[key] });
+ }
+ }
+ if (options.length > 0) {
+ await updateOptions(options);
+ }
+ };
+
const submitEmailDomainWhitelist = async () => {
if (Array.isArray(emailDomainWhitelist)) {
await updateOptions([
@@ -1351,6 +1412,154 @@ const SystemSetting = () => {
+
+
+ {t('用以支持短信通知推送,用户可在个人设置中选择短信通知方式')}
+
+
+
+
+
+ {inputs.SMSProvider === 'aliyun' && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {inputs.SMSProvider === 'sendcloud' && (
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ {inputs.SMSProvider === 'tencent' && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {inputs.SMSProvider === 'custom' && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
diff --git a/web/classic/src/components/settings/personal/cards/NotificationSettings.jsx b/web/classic/src/components/settings/personal/cards/NotificationSettings.jsx
index 5e8d4fd8299f..870178363e1d 100644
--- a/web/classic/src/components/settings/personal/cards/NotificationSettings.jsx
+++ b/web/classic/src/components/settings/personal/cards/NotificationSettings.jsx
@@ -33,7 +33,7 @@ import {
Col,
} from '@douyinfe/semi-ui';
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
-import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
+import { ShieldCheck, Bell, DollarSign, Settings, Smartphone } from 'lucide-react';
import {
renderQuotaWithPrompt,
API,
@@ -432,6 +432,7 @@ const NotificationSettings = ({
{t('Webhook通知')}
{t('Bark通知')}
{t('Gotify通知')}
+ {t('短信通知')}
>
)}
+
+ {/* 短信通知设置 */}
+ {notificationSettings.warningType === 'sms' && (
+ handleFormChange('smsPhoneNumber', val)}
+ prefix={}
+ extraText={t(
+ '短信服务由管理员统一配置,您只需填写接收通知的手机号码',
+ )}
+ showClear
+ rules={[
+ {
+ required: notificationSettings.warningType === 'sms',
+ message: t('请输入手机号码'),
+ },
+ {
+ pattern: /^\+?[0-9]{7,15}$/,
+ message: t('手机号码格式不正确'),
+ },
+ ]}
+ />
+ )}
diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json
index dc8ad6cb9464..7de3883c5293 100644
--- a/web/classic/src/i18n/locales/en.json
+++ b/web/classic/src/i18n/locales/en.json
@@ -3423,6 +3423,39 @@
"通知邮箱": "Notification email",
"通知配置": "Notification configuration",
"通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。": "Groups enable differentiated pricing for different user tiers. For example, VIP users can enjoy lower API costs.",
+ "短信通知": "SMS notification",
+ "短信服务商": "SMS provider",
+ "阿里云短信": "Alibaba Cloud SMS",
+ "腾讯云短信": "Tencent Cloud SMS",
+ "通用HTTP接口": "Custom HTTP API",
+ "手机号码": "Phone number",
+ "请输入接收短信的手机号码": "Please enter the phone number to receive SMS",
+ "请输入手机号码": "Please enter phone number",
+ "手机号码格式不正确": "Invalid phone number format",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "SMS service is configured by the administrator. You only need to enter your phone number",
+ "短信签名": "SMS signature",
+ "模板Code": "Template code",
+ "请输入模板Code": "Please enter template code",
+ "模板ID": "Template ID",
+ "请输入模板ID": "Please enter template ID",
+ "接口地址": "API URL",
+ "请输入短信接口地址": "Please enter SMS API URL",
+ "请求方法": "Request method",
+ "请求模板": "Request template",
+ "请输入请求模板": "Please enter request template",
+ "短信接口地址必须以http://或https://开头": "SMS API URL must start with http:// or https://",
+ "阿里云短信配置说明": "Alibaba Cloud SMS configuration",
+ "腾讯云短信配置说明": "Tencent Cloud SMS configuration",
+ "通用HTTP接口说明": "Custom HTTP API instructions",
+ "请输入短信签名": "Please enter SMS signature",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "Obtain the parameters after creating a signature and template in the Alibaba Cloud SMS console",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "Obtain the parameters after creating an app, signature and template in the Tencent Cloud SMS console",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "Supports template variables: {{phone}} (phone number), {{title}} (notification title), {{content}} (notification content)",
+ "短信模板中需包含一个变量用于接收通知内容": "The SMS template must contain a variable for notification content",
+ "配置短信服务": "Configure SMS Service",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "To support SMS notification push, users can select SMS notification in personal settings",
+ "未配置": "Not configured",
+ "保存短信设置": "Save SMS Settings",
"通过划转功能将奖励额度转入到您的账户余额中": "Transfer the reward amount to your account balance through the transfer function",
"通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password",
"通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。": "This feature allows showing different selectable group lists to users of different tiers.",
diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json
index 8e7d143d0954..aa4b34b644ff 100644
--- a/web/classic/src/i18n/locales/fr.json
+++ b/web/classic/src/i18n/locales/fr.json
@@ -3402,6 +3402,39 @@
"通知邮箱": "E-mail de notification",
"通知配置": "Notifications",
"通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。": "Groups enable differentiated pricing for different user tiers. For example, VIP users can enjoy lower API costs.",
+ "短信通知": "Notification SMS",
+ "短信服务商": "Fournisseur SMS",
+ "阿里云短信": "Alibaba Cloud SMS",
+ "腾讯云短信": "Tencent Cloud SMS",
+ "通用HTTP接口": "API HTTP personnalisée",
+ "手机号码": "Numéro de téléphone",
+ "请输入接收短信的手机号码": "Veuillez entrer le numéro de téléphone pour recevoir les SMS",
+ "请输入手机号码": "Veuillez entrer le numéro de téléphone",
+ "手机号码格式不正确": "Format de numéro de téléphone incorrect",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "Le service SMS est configuré par l'administrateur. Vous n'avez qu'à entrer votre numéro de téléphone",
+ "短信签名": "Signature SMS",
+ "模板Code": "Code du modèle",
+ "请输入模板Code": "Veuillez entrer le code du modèle",
+ "模板ID": "ID du modèle",
+ "请输入模板ID": "Veuillez entrer l'ID du modèle",
+ "接口地址": "URL de l'API",
+ "请输入短信接口地址": "Veuillez entrer l'URL de l'API SMS",
+ "请求方法": "Méthode de requête",
+ "请求模板": "Modèle de requête",
+ "请输入请求模板": "Veuillez entrer le modèle de requête",
+ "短信接口地址必须以http://或https://开头": "L'URL de l'API SMS doit commencer par http:// ou https://",
+ "阿里云短信配置说明": "Configuration Alibaba Cloud SMS",
+ "腾讯云短信配置说明": "Configuration Tencent Cloud SMS",
+ "通用HTTP接口说明": "Instructions API HTTP personnalisée",
+ "请输入短信签名": "Veuillez entrer la signature SMS",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "Obtenez les paramètres après avoir créé une signature et un modèle dans la console Alibaba Cloud SMS",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "Obtenez les paramètres après avoir créé une application, une signature et un modèle dans la console Tencent Cloud SMS",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "Variables de modèle: {{phone}} (téléphone), {{title}} (titre), {{content}} (contenu)",
+ "短信模板中需包含一个变量用于接收通知内容": "Le modèle SMS doit contenir une variable pour le contenu de la notification",
+ "配置短信服务": "Configurer le service SMS",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "Pour prendre en charge les notifications SMS, les utilisateurs peuvent sélectionner les notifications SMS dans les paramètres personnels",
+ "未配置": "Non configuré",
+ "保存短信设置": "Enregistrer les paramètres SMS",
"通过划转功能将奖励额度转入到您的账户余额中": "Transférez le montant de la récompense sur le solde de votre compte via la fonction de virement",
"通过密码注册时需要进行邮箱验证": "La vérification par e-mail est requise lors de l'inscription via mot de passe",
"通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。": "This feature allows showing different selectable group lists to users of different tiers.",
diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json
index 88d2899e17c6..330611ccfc11 100644
--- a/web/classic/src/i18n/locales/ja.json
+++ b/web/classic/src/i18n/locales/ja.json
@@ -3371,6 +3371,39 @@
"通知邮箱": "通知メールアドレス",
"通知配置": "通知設定",
"通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。": "グループにより異なるユーザー等級の差別化料金を実現できます。例えばVIPユーザーがより低いAPI呼び出し費用を享受できます。",
+ "短信通知": "SMS通知",
+ "短信服务商": "SMSプロバイダー",
+ "阿里云短信": "Alibaba Cloud SMS",
+ "腾讯云短信": "Tencent Cloud SMS",
+ "通用HTTP接口": "カスタムHTTP API",
+ "手机号码": "電話番号",
+ "请输入接收短信的手机号码": "SMSを受信する電話番号を入力してください",
+ "请输入手机号码": "電話番号を入力してください",
+ "手机号码格式不正确": "電話番号の形式が正しくありません",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "SMSサービスは管理者が設定します。電話番号のみ入力してください",
+ "短信签名": "SMS署名",
+ "模板Code": "テンプレートコード",
+ "请输入模板Code": "テンプレートコードを入力してください",
+ "模板ID": "テンプレートID",
+ "请输入模板ID": "テンプレートIDを入力してください",
+ "接口地址": "API URL",
+ "请输入短信接口地址": "SMS API URLを入力してください",
+ "请求方法": "リクエストメソッド",
+ "请求模板": "リクエストテンプレート",
+ "请输入请求模板": "リクエストテンプレートを入力してください",
+ "短信接口地址必须以http://或https://开头": "SMS API URLはhttp://またはhttps://で始まる必要があります",
+ "阿里云短信配置说明": "Alibaba Cloud SMS設定",
+ "腾讯云短信配置说明": "Tencent Cloud SMS設定",
+ "通用HTTP接口说明": "カスタムHTTP API説明",
+ "请输入短信签名": "SMS署名を入力してください",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "Alibaba Cloud SMSコンソールで署名とテンプレートを作成した後、パラメータを取得してください",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "Tencent Cloud SMSコンソールでアプリ、署名、テンプレートを作成した後、パラメータを取得してください",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "テンプレート変数: {{phone}} (電話番号), {{title}} (通知タイトル), {{content}} (通知内容)",
+ "短信模板中需包含一个变量用于接收通知内容": "SMSテンプレートには通知内容用の変数が1つ必要です",
+ "配置短信服务": "SMSサービスの設定",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "SMS通知をサポートします。ユーザーは個人設定でSMS通知を選択できます",
+ "未配置": "未設定",
+ "保存短信设置": "SMS設定を保存",
"通过划转功能将奖励额度转入到您的账户余额中": "振替機能を利用して、特典をアカウントの残高に振り替えることができます",
"通过密码注册时需要进行邮箱验证": "パスワードでのサインアップ時にメールアドレスの確認を必須にする",
"通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。": "この機能により、ユーザーの所属グループに基づいて、異なる等級のユーザーに異なる選択リストを表示できます。",
diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json
index 2980af179f6f..8a68fc39fca7 100644
--- a/web/classic/src/i18n/locales/ru.json
+++ b/web/classic/src/i18n/locales/ru.json
@@ -3422,6 +3422,39 @@
"通知邮箱": "Email для уведомлений",
"通知配置": "Конфигурация уведомлений",
"通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。": "Groups enable differentiated pricing for different user tiers. For example, VIP users can enjoy lower API costs.",
+ "短信通知": "SMS-уведомление",
+ "短信服务商": "SMS-провайдер",
+ "阿里云短信": "Alibaba Cloud SMS",
+ "腾讯云短信": "Tencent Cloud SMS",
+ "通用HTTP接口": "Пользовательский HTTP API",
+ "手机号码": "Номер телефона",
+ "请输入接收短信的手机号码": "Введите номер телефона для получения SMS",
+ "请输入手机号码": "Введите номер телефона",
+ "手机号码格式不正确": "Неверный формат номера телефона",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "SMS-сервис настраивается администратором. Вам нужно только ввести номер телефона",
+ "短信签名": "SMS-подпись",
+ "模板Code": "Код шаблона",
+ "请输入模板Code": "Введите код шаблона",
+ "模板ID": "ID шаблона",
+ "请输入模板ID": "Введите ID шаблона",
+ "接口地址": "URL API",
+ "请输入短信接口地址": "Введите URL SMS API",
+ "请求方法": "Метод запроса",
+ "请求模板": "Шаблон запроса",
+ "请输入请求模板": "Введите шаблон запроса",
+ "短信接口地址必须以http://或https://开头": "URL SMS API должен начинаться с http:// или https://",
+ "阿里云短信配置说明": "Настройка Alibaba Cloud SMS",
+ "腾讯云短信配置说明": "Настройка Tencent Cloud SMS",
+ "通用HTTP接口说明": "Инструкции для пользовательского HTTP API",
+ "请输入短信签名": "Введите SMS-подпись",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "Получите параметры после создания подписи и шаблона в консоли Alibaba Cloud SMS",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "Получите параметры после создания приложения, подписи и шаблона в консоли Tencent Cloud SMS",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "Переменные шаблона: {{phone}} (телефон), {{title}} (заголовок), {{content}} (содержание)",
+ "短信模板中需包含一个变量用于接收通知内容": "Шаблон SMS должен содержать переменную для содержания уведомления",
+ "配置短信服务": "Настройка SMS-сервиса",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "Для поддержки SMS-уведомлений. Пользователи могут выбрать SMS в личных настройках",
+ "未配置": "Не настроено",
+ "保存短信设置": "Сохранить настройки SMS",
"通过划转功能将奖励额度转入到您的账户余额中": "Через функцию перевода переведите вознаграждение на баланс вашей учётной записи",
"通过密码注册时需要进行邮箱验证": "При регистрации через пароль требуется проверка электронной почты",
"通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。": "This feature allows showing different selectable group lists to users of different tiers.",
diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json
index 4ca1a77f3122..7d46d246e9a4 100644
--- a/web/classic/src/i18n/locales/vi.json
+++ b/web/classic/src/i18n/locales/vi.json
@@ -3869,6 +3869,39 @@
"通知类型 (quota_exceed: 额度预警)": "Loại thông báo (quota_exceed: cảnh báo hạn ngạch)",
"通知邮箱": "Email thông báo",
"通知配置": "Cấu hình thông báo",
+ "短信通知": "Thông báo SMS",
+ "短信服务商": "Nhà cung cấp SMS",
+ "阿里云短信": "Alibaba Cloud SMS",
+ "腾讯云短信": "Tencent Cloud SMS",
+ "通用HTTP接口": "API HTTP tùy chỉnh",
+ "手机号码": "Số điện thoại",
+ "请输入接收短信的手机号码": "Vui lòng nhập số điện thoại để nhận SMS",
+ "请输入手机号码": "Vui lòng nhập số điện thoại",
+ "手机号码格式不正确": "Định dạng số điện thoại không hợp lệ",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "Dịch vụ SMS được cấu hình bởi quản trị viên. Bạn chỉ cần nhập số điện thoại",
+ "短信签名": "Chữ ký SMS",
+ "模板Code": "Mã mẫu",
+ "请输入模板Code": "Vui lòng nhập mã mẫu",
+ "模板ID": "ID mẫu",
+ "请输入模板ID": "Vui lòng nhập ID mẫu",
+ "接口地址": "URL API",
+ "请输入短信接口地址": "Vui lòng nhập URL API SMS",
+ "请求方法": "Phương thức yêu cầu",
+ "请求模板": "Mẫu yêu cầu",
+ "请输入请求模板": "Vui lòng nhập mẫu yêu cầu",
+ "短信接口地址必须以http://或https://开头": "URL API SMS phải bắt đầu bằng http:// hoặc https://",
+ "阿里云短信配置说明": "Cấu hình Alibaba Cloud SMS",
+ "腾讯云短信配置说明": "Cấu hình Tencent Cloud SMS",
+ "通用HTTP接口说明": "Hướng dẫn API HTTP tùy chỉnh",
+ "请输入短信签名": "Vui lòng nhập chữ ký SMS",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "Lấy các tham số sau khi tạo chữ ký và mẫu trong bảng điều khiển Alibaba Cloud SMS",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "Lấy các tham số sau khi tạo ứng dụng, chữ ký và mẫu trong bảng điều khiển Tencent Cloud SMS",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "Biến mẫu: {{phone}} (số điện thoại), {{title}} (tiêu đề), {{content}} (nội dung)",
+ "短信模板中需包含一个变量用于接收通知内容": "Mẫu SMS phải chứa một biến cho nội dung thông báo",
+ "配置短信服务": "Cấu hình dịch vụ SMS",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "Hỗ trợ thông báo SMS. Người dùng có thể chọn thông báo SMS trong cài đặt cá nhân",
+ "未配置": "Chưa cấu hình",
+ "保存短信设置": "Lưu cài đặt SMS",
"通过": "Thông qua",
"通过 GitHub 登录": "Đăng nhập qua GitHub",
"通过 Google 登录": "Đăng nhập qua Google",
diff --git a/web/classic/src/i18n/locales/zh-CN.json b/web/classic/src/i18n/locales/zh-CN.json
index e54a1c0f9114..e4fee0613061 100644
--- a/web/classic/src/i18n/locales/zh-CN.json
+++ b/web/classic/src/i18n/locales/zh-CN.json
@@ -3410,6 +3410,39 @@
"通知邮箱": "通知邮箱",
"通知配置": "通知配置",
"通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。": "通过分组可以实现不同用户等级的差异化定价,例如 VIP 用户享受更低的 API 调用费用。",
+ "短信通知": "短信通知",
+ "短信服务商": "短信服务商",
+ "阿里云短信": "阿里云短信",
+ "腾讯云短信": "腾讯云短信",
+ "通用HTTP接口": "通用HTTP接口",
+ "手机号码": "手机号码",
+ "请输入接收短信的手机号码": "请输入接收短信的手机号码",
+ "请输入手机号码": "请输入手机号码",
+ "手机号码格式不正确": "手机号码格式不正确",
+ "短信服务由管理员统一配置,您只需填写接收通知的手机号码": "短信服务由管理员统一配置,您只需填写接收通知的手机号码",
+ "短信签名": "短信签名",
+ "模板Code": "模板Code",
+ "请输入模板Code": "请输入模板Code",
+ "模板ID": "模板ID",
+ "请输入模板ID": "请输入模板ID",
+ "接口地址": "接口地址",
+ "请输入短信接口地址": "请输入短信接口地址",
+ "请求方法": "请求方法",
+ "请求模板": "请求模板",
+ "请输入请求模板": "请输入请求模板",
+ "短信接口地址必须以http://或https://开头": "短信接口地址必须以http://或https://开头",
+ "阿里云短信配置说明": "阿里云短信配置说明",
+ "腾讯云短信配置说明": "腾讯云短信配置说明",
+ "通用HTTP接口说明": "通用HTTP接口说明",
+ "请输入短信签名": "请输入短信签名",
+ "在阿里云短信服务控制台创建签名和模板后获取相关参数": "在阿里云短信服务控制台创建签名和模板后获取相关参数",
+ "在腾讯云短信控制台创建应用、签名和模板后获取相关参数": "在腾讯云短信控制台创建应用、签名和模板后获取相关参数",
+ "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)": "支持模板变量: {{phone}} (手机号), {{title}} (通知标题), {{content}} (通知内容)",
+ "短信模板中需包含一个变量用于接收通知内容": "短信模板中需包含一个变量用于接收通知内容",
+ "配置短信服务": "配置短信服务",
+ "用以支持短信通知推送,用户可在个人设置中选择短信通知方式": "用以支持短信通知推送,用户可在个人设置中选择短信通知方式",
+ "未配置": "未配置",
+ "保存短信设置": "保存短信设置",
"通过划转功能将奖励额度转入到您的账户余额中": "通过划转功能将奖励额度转入到您的账户余额中",
"通过密码注册时需要进行邮箱验证": "通过密码注册时需要进行邮箱验证",
"通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。": "通过此功能,可以根据用户所在分组,为不同等级的用户展示不同的可选列表。",