feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+134
View File
@@ -0,0 +1,134 @@
package onboarding
import (
"context"
"errors"
"regexp"
"strconv"
"strings"
"time"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
)
// Tencent SMS accepts a request before the carrier delivers it. A failed or
// ambiguous call is never retried here: the challenge remains subject to the
// existing resend limits and cannot cause an accidental duplicate charge.
type tencentSMSSender interface {
SendSmsWithContext(context.Context, *sms.SendSmsRequest) (*sms.SendSmsResponse, error)
}
type TencentSMSProvider struct {
parameters map[string][]string
client tencentSMSSender
appID, signName string
templates map[string]string
}
var mainlandPhonePattern = regexp.MustCompile(`^\+86[1][3-9][0-9]{9}$`)
var smsTemplateIDPattern = regexp.MustCompile(`^[0-9]+$`)
const (
SMSPurposeLogin = "LOGIN"
SMSPurposeRegister = "REGISTER"
SMSPurposeChangePassword = "CHANGE_PASSWORD"
SMSPurposeChangePhone = "CHANGE_PHONE"
)
var smsTemplateKeys = map[string]string{
SMSPurposeLogin: "login", SMSPurposeRegister: "register",
SMSPurposeChangePassword: "change_password", SMSPurposeChangePhone: "change_phone",
}
func NewTencentSMSProvider(secretID, secretKey, appID, signName, templateID string) (SMSProvider, error) {
// Keep the original single-template environment configuration compatible.
return NewTencentSMSProviderWithTemplates(secretID, secretKey, appID, signName, map[string]string{
"login": templateID, "register": templateID, "change_password": templateID, "change_phone": templateID,
})
}
func NewTencentSMSProviderWithTemplates(secretID, secretKey, appID, signName string, templates map[string]string, parameterOptions ...map[string][]string) (SMSProvider, error) {
if strings.TrimSpace(secretID) == "" || strings.TrimSpace(secretKey) == "" ||
!smsTemplateIDPattern.MatchString(appID) || strings.TrimSpace(signName) == "" {
return nil, errors.New("Tencent SMS requires SecretId, SecretKey, numeric SDK AppID and approved sign name")
}
copyTemplates := make(map[string]string, len(smsTemplateKeys))
parameters := make(map[string][]string, len(smsTemplateKeys))
for _, key := range smsTemplateKeys {
if !smsTemplateIDPattern.MatchString(templates[key]) {
return nil, errors.New("Tencent SMS requires numeric login, register, change_password and change_phone template IDs")
}
copyTemplates[key] = templates[key]
parameters[key] = []string{"code"}
}
if len(parameterOptions) > 1 {
return nil, errors.New("Tencent SMS accepts one template parameter configuration")
}
if len(parameterOptions) == 1 {
for key, values := range parameterOptions[0] {
if _, ok := copyTemplates[key]; !ok {
return nil, errors.New("Tencent SMS template parameters have an unknown purpose")
}
seen := map[string]bool{}
for _, value := range values {
if (value != "code" && value != "expires_minutes") || seen[value] {
return nil, errors.New("Tencent SMS template parameters must be code and optionally expires_minutes")
}
seen[value] = true
}
if !seen["code"] {
return nil, errors.New("Tencent SMS template parameters must include code")
}
parameters[key] = append([]string(nil), values...)
}
}
clientProfile := profile.NewClientProfile()
clientProfile.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
clientProfile.HttpProfile.ReqTimeout = 10
clientProfile.NetworkFailureMaxRetries = 0
clientProfile.RateLimitExceededMaxRetries = 0
clientProfile.UnsafeRetryOnConnectionFailure = false
client, err := sms.NewClient(common.NewCredential(secretID, secretKey), "ap-guangzhou", clientProfile)
if err != nil {
return nil, err
}
return &TencentSMSProvider{client: client, appID: appID, signName: signName, templates: copyTemplates, parameters: parameters}, nil
}
func (p *TencentSMSProvider) Send(ctx context.Context, phone, code, id string) error {
return p.SendForPurpose(ctx, phone, code, id, SMSPurposeLogin)
}
func (p *TencentSMSProvider) SendForPurpose(ctx context.Context, phone, code, id, purpose string) error {
templateID := p.templates[smsTemplateKeys[purpose]]
if templateID == "" || !mainlandPhonePattern.MatchString(phone) || !smsCodePattern.MatchString(code) || !uuidV4(id) {
return errors.New("SMS_GATEWAY_REJECTED")
}
req := sms.NewSendSmsRequest()
req.PhoneNumberSet = common.StringPtrs([]string{phone})
req.SmsSdkAppId = common.StringPtr(p.appID)
req.SignName = common.StringPtr(p.signName)
req.TemplateId = common.StringPtr(templateID)
values := make([]string, 0, len(p.parameters[smsTemplateKeys[purpose]]))
for _, name := range p.parameters[smsTemplateKeys[purpose]] {
if name == "code" {
values = append(values, code)
} else {
values = append(values, strconv.Itoa(int(smsCodeLifetime/time.Minute)))
}
}
req.TemplateParamSet = common.StringPtrs(values)
req.SessionContext = common.StringPtr(id)
result, err := p.client.SendSmsWithContext(ctx, req)
if err != nil {
return errors.New("SMS_GATEWAY_UNCERTAIN")
}
if result == nil || result.Response == nil || len(result.Response.SendStatusSet) != 1 ||
result.Response.SendStatusSet[0] == nil || result.Response.SendStatusSet[0].Code == nil ||
*result.Response.SendStatusSet[0].Code != "Ok" {
return errors.New("SMS_GATEWAY_REJECTED")
}
return nil
}