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
+132 -15
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
"net/http"
"os"
"strings"
"time"
"data-terminal-cloud/internal/alertnotification"
@@ -13,6 +14,7 @@ import (
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/billing"
"data-terminal-cloud/internal/cache"
"data-terminal-cloud/internal/callmemory"
"data-terminal-cloud/internal/config"
"data-terminal-cloud/internal/contentmodel"
"data-terminal-cloud/internal/contentpolicy"
@@ -20,8 +22,10 @@ import (
"data-terminal-cloud/internal/dataexport"
"data-terminal-cloud/internal/device"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/devicefleet"
"data-terminal-cloud/internal/devicereset"
"data-terminal-cloud/internal/diagnostics"
"data-terminal-cloud/internal/elderagent"
"data-terminal-cloud/internal/embeddingprovider"
"data-terminal-cloud/internal/externalprobe"
"data-terminal-cloud/internal/extraction"
@@ -31,6 +35,7 @@ import (
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/media"
"data-terminal-cloud/internal/memoir"
"data-terminal-cloud/internal/memoryindex"
"data-terminal-cloud/internal/memorylifecycle"
"data-terminal-cloud/internal/memorytool"
@@ -85,6 +90,42 @@ func Run(ctx context.Context, serviceName string) error {
var memoryEmbedding embeddingprovider.Provider
var providerMemoryClient providermemory.Client
var vikingMemory *vikingmemory.Service
callMemoryService := callmemory.NewService(databasePool, logger)
if bucket := cfg.TOSRecordingBucket; bucket != "" && serviceName == "api" {
recordings, recordingErr := callmemory.NewTOSRecordings(bucket, cfg.TOSRecordingRegion, cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken)
if recordingErr != nil {
return fmt.Errorf("configure cloud recordings: %w", recordingErr)
}
if err := recordings.ConfigurePlayback(cfg.TOSRecordingPlaybackEndpoint); err != nil {
return err
}
callMemoryService.ConfigureRecordings(recordings)
if account := cfg.TOSRecordingAccountID; account != "" {
callMemoryService.ConfigureMixedRecorder(callmemory.NewMixedRecorder(cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken, account, bucket))
}
}
var directTemplate []byte
directTemplatePath := os.Getenv("DIRECT_CHAT_TEMPLATE_FILE")
if directTemplatePath == "" {
directTemplatePath = cfg.VolcVoiceChatTemplate
}
if directTemplatePath != "" {
directTemplate, err = os.ReadFile(directTemplatePath)
if err != nil {
return fmt.Errorf("read direct chat template: %w", err)
}
}
if len(directTemplate) > 0 {
directTemplate, err = cfg.ApplyVolcVoiceTemplate(directTemplate)
if err != nil {
return err
}
}
callMemoryService.ConfigureDirect(callmemory.DirectOptions{Template: directTemplate, BotID: cfg.VolcHardwareAgentBotID, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret)})
var elderAgents *elderagent.Service
if len(directTemplate) > 0 && (serviceName == "api" || serviceName == "worker") {
elderAgents = &elderagent.Service{Pool: databasePool, Provider: elderagent.NewClient(cfg.VolcAccessKeyID, cfg.VolcSecretAccessKey, cfg.VolcSessionToken, cfg.VolcRTCBaseURL, cfg.VolcRTCRegion).ForProduct(cfg.VolcHardwareInstanceID, cfg.VolcHardwareProductKey), Template: directTemplate, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret), VikingCollectionName: cfg.VikingCollectionName, VikingEventType: cfg.VikingEventType}
}
if serviceName == "api" || serviceName == "worker" {
if cfg.VikingResourceID != "" {
client, vikingErr := vikingmemory.NewClient(vikingmemory.Options{ResourceID: cfg.VikingResourceID, CollectionName: cfg.VikingCollectionName, APIKey: cfg.VikingAPIKey, EventType: cfg.VikingEventType, ProfileType: cfg.VikingProfileType, Timeout: 8 * time.Second})
@@ -92,6 +133,10 @@ func Run(ctx context.Context, serviceName string) error {
return fmt.Errorf("configure Viking memory: %w", vikingErr)
}
vikingMemory = vikingmemory.NewService(databasePool, client)
callMemoryService.ConfigureVikingConsole(client)
managedMemory := vikingmemory.NewManaged(databasePool, client)
callMemoryService.ConfigureProjection(managedMemory)
callMemoryService.ConfigureManagedSync(managedMemory.SyncNext)
}
memoryEmbedding, err = newMemoryEmbeddingProvider(cfg)
if err != nil {
@@ -153,6 +198,7 @@ func Run(ctx context.Context, serviceName string) error {
var memoryToolSigner *memorytool.TokenSigner
var contentModelService *contentmodel.Service
var contentModelRegistry *contentmodel.Registry
var memoirService *memoir.Service
var hardwareProvisioningService *hardwareprovision.Service
var deviceFactoryResetService *devicereset.Service
if serviceName == "api" || serviceName == "worker" {
@@ -167,6 +213,20 @@ func Run(ctx context.Context, serviceName string) error {
if err != nil {
return fmt.Errorf("create content model registry: %w", err)
}
memoirService = &memoir.Service{Pool: databasePool, Model: cfg.ArkMemoirModel, Safety: contentpolicy.LocalClassifier{}}
if cfg.ArkAPIKey != "" {
memoirService.Generator, err = extraction.NewArkJSONGenerator(cfg.ArkBaseURL, cfg.ArkAPIKey, cfg.ArkMemoirModel, 180*time.Second, 65536)
if err != nil {
return err
}
memoirService.Illustrator = memoir.NewIllustrator(cfg.ArkBaseURL, cfg.ArkAPIKey, cfg.ArkMemoirImageModel)
}
if cfg.DifyEnabled {
memoirService.Workflow, err = memoir.NewDifyWorkflow(cfg.DifyBaseURL, cfg.DifyMemoirAPIKey, cfg.DifyBridgeURL, cfg.DifyBridgeSecret)
if err != nil {
return err
}
}
}
if serviceName == "api" || serviceName == "callbacks" {
diagnosticService, err = diagnostics.NewService(databasePool, idempotency.NewExecutor(databasePool), audit.NewRepository())
@@ -194,6 +254,10 @@ func Run(ctx context.Context, serviceName string) error {
}
saasService = saas.NewService(databasePool, audit.NewRepository(), redisClient, cursorCodec)
saasService.ConfigureProviderMemory(cfg.ProviderMemoryMode == "managed_http")
if elderAgents != nil {
saasService.ConfigureVoiceAgents(elderAgents)
callMemoryService.ConfigureDirect(callmemory.DirectOptions{Prepare: elderAgents.Prepare, PrepareWithWelcome: elderAgents.PrepareWithWelcome, CallbackURL: cfg.SubtitleCallbackURL, CallbackSecret: []byte(cfg.SubtitleCallbackSecret)})
}
opsConfigService, err = opsconfig.NewService(databasePool, audit.NewRepository())
if err != nil {
return fmt.Errorf("create operations configuration service: %w", err)
@@ -235,7 +299,10 @@ func Run(ctx context.Context, serviceName string) error {
return fmt.Errorf("import legacy device credentials: %w", err)
}
}
opsDeviceService, err = opsdevice.NewService(databasePool, deviceSecretResolver, audit.NewRepository())
opsDeviceService, err = opsdevice.NewService(databasePool, deviceSecretResolver, audit.NewRepository(), opsdevice.Options{
OnboardingMasterKey: cfg.Onboarding.MasterKey,
DeviceOrigin: cfg.Onboarding.EntryURL,
})
if err != nil {
return fmt.Errorf("create operations device service: %w", err)
}
@@ -349,12 +416,24 @@ func Run(ctx context.Context, serviceName string) error {
if (serviceName == "api" || serviceName == "worker") && cfg.Onboarding.MasterKey != "" {
var sms onboarding.SMSProvider
if cfg.Onboarding.SMSLoginEnabled {
sms, err = onboarding.NewHTTPSMSProvider(cfg.Onboarding.SMSGatewayURL, cfg.Onboarding.SMSGatewayToken)
if cfg.Onboarding.SMSProvider == "tencent" {
if cfg.Onboarding.TencentSMSTemplates != nil {
sms, err = onboarding.NewTencentSMSProviderWithTemplates(cfg.Onboarding.TencentSMSSecretID, cfg.Onboarding.TencentSMSSecretKey, cfg.Onboarding.TencentSMSAppID, cfg.Onboarding.TencentSMSSignName, cfg.Onboarding.TencentSMSTemplates, cfg.Onboarding.TencentSMSTemplateParams)
} else {
sms, err = onboarding.NewTencentSMSProvider(cfg.Onboarding.TencentSMSSecretID, cfg.Onboarding.TencentSMSSecretKey, cfg.Onboarding.TencentSMSAppID, cfg.Onboarding.TencentSMSSignName, cfg.Onboarding.TencentSMSTemplateID)
}
} else {
sms, err = onboarding.NewHTTPSMSProvider(cfg.Onboarding.SMSGatewayURL, cfg.Onboarding.SMSGatewayToken)
}
if err != nil {
return err
}
}
onboardingService, err = onboarding.NewService(databasePool, redisClient, onboarding.Options{Enabled: cfg.Onboarding.Enabled, PauseClaims: cfg.Onboarding.PauseClaims, MasterKey: cfg.Onboarding.MasterKey, EntryURL: cfg.Onboarding.EntryURL, AllowLocalHTTP: cfg.Onboarding.AllowLocalHTTP, TrustedProxyCIDRs: cfg.Onboarding.TrustedProxyCIDRs, TokenVerifier: tokenVerifier, LegacyVerifier: deviceVerifier, ActivationEnabled: cfg.Onboarding.ActivationEnabled, ProviderMasterKey: cfg.VolcHardwareDeviceCredentialMasterKey, InstanceID: cfg.VolcHardwareInstanceID, ProductKey: cfg.VolcHardwareProductKey, ProductSecret: cfg.VolcHardwareProductSecret, BotID: cfg.VolcHardwareAgentBotID, Transport: cfg.VolcHardwareTransport, MinimumSDKVersion: cfg.VolcHardwareMinimumSDKVersion, SMS: sms, SMSLoginEnabled: cfg.Onboarding.SMSLoginEnabled, SelfSignupTenantIDs: cfg.Onboarding.SelfSignupTenantIDs, Logger: logger})
captchaProvider, captchaErr := onboarding.NewTencentCaptchaProvider(cfg.Onboarding.TencentCaptcha)
if captchaErr != nil {
return captchaErr
}
onboardingService, err = onboarding.NewService(databasePool, redisClient, onboarding.Options{SaaSCaptchaDisabled: cfg.Onboarding.SaaSCaptchaDisabled, Captcha: captchaProvider, Enabled: cfg.Onboarding.Enabled, PauseClaims: cfg.Onboarding.PauseClaims, MasterKey: cfg.Onboarding.MasterKey, EntryURL: cfg.Onboarding.EntryURL, AllowLocalHTTP: cfg.Onboarding.AllowLocalHTTP, TrustedProxyCIDRs: cfg.Onboarding.TrustedProxyCIDRs, TokenVerifier: tokenVerifier, LegacyVerifier: deviceVerifier, ActivationEnabled: cfg.Onboarding.ActivationEnabled, ProviderMasterKey: cfg.VolcHardwareDeviceCredentialMasterKey, InstanceID: cfg.VolcHardwareInstanceID, ProductKey: cfg.VolcHardwareProductKey, ProductSecret: cfg.VolcHardwareProductSecret, BotID: cfg.VolcHardwareAgentBotID, Transport: cfg.VolcHardwareTransport, MinimumSDKVersion: cfg.VolcHardwareMinimumSDKVersion, SMS: sms, WeChatAppID: cfg.Onboarding.WeChatAppID, WeChatAppSecret: cfg.Onboarding.WeChatAppSecret, SMSLoginEnabled: cfg.Onboarding.SMSLoginEnabled, SelfSignupTenantIDs: cfg.Onboarding.SelfSignupTenantIDs, Logger: logger})
if err != nil {
return fmt.Errorf("create onboarding service: %w", err)
}
@@ -362,7 +441,34 @@ func Run(ctx context.Context, serviceName string) error {
deviceVerifier = onboardingService
}
}
if serviceName == "api" && saasService != nil {
downloadBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("FIRMWARE_DOWNLOAD_BASE_URL")), "/")
if downloadBaseURL == "" {
scheme := "http"
if cfg.ObjectStorePublicSecure {
scheme = "https"
}
downloadBaseURL = scheme + "://" + cfg.ObjectStorePublic
}
fleet := &devicefleet.Service{Pool: databasePool, Store: exportStore, BrokerURL: os.Getenv("DEVICE_MQTT_PUBLIC_URL"), DownloadBaseURL: downloadBaseURL}
address := os.Getenv("DEVICE_MQTT_ADDRESS")
if address == "" {
address = ":1883"
}
closeBroker, fleetErr := fleet.StartBroker(ctx, address, logger)
if fleetErr != nil {
return fmt.Errorf("start device MQTT: %w", fleetErr)
}
defer closeBroker()
saasService.Fleet = fleet
if onboardingService != nil {
onboardingService.Fleet = fleet
}
}
if serviceName == "worker" {
if elderAgents != nil {
go elderAgents.RunPersonaSync(ctx, logger)
}
webEventService = webevents.NewService(databasePool)
outbox.RegisterMetrics(databasePool)
observability.RegisterBusinessMetrics(databasePool)
@@ -392,6 +498,10 @@ func Run(ctx context.Context, serviceName string) error {
if readErr != nil {
return fmt.Errorf("read Volcengine voice-chat template: %w", readErr)
}
template, err = cfg.ApplyVolcVoiceTemplate(template)
if err != nil {
return err
}
providerClient, err = voiceprovider.NewVolcengineClient(voiceprovider.VolcengineOptions{
BaseURL: cfg.VolcRTCBaseURL, AccessKeyID: cfg.VolcAccessKeyID, SecretAccessKey: cfg.VolcSecretAccessKey,
SessionToken: cfg.VolcSessionToken, Region: cfg.VolcRTCRegion, AppID: cfg.RTCAppID,
@@ -434,6 +544,12 @@ func Run(ctx context.Context, serviceName string) error {
outboxWorker.Register("conversation.agent_stop_requested", stopProcessor.Handle)
outboxWorker.Register("conversation.agent_stop_reconcile_requested", stopProcessor.Handle)
var contentGenerator extraction.Generator = contentModelRegistry
if cfg.DifyEnabled {
contentGenerator, err = extraction.NewDifyGenerator(cfg.DifyBaseURL, cfg.DifyExtractionAPIKey, cfg.DifyBridgeURL, cfg.DifyBridgeSecret)
if err != nil {
return err
}
}
var contentSafety contentpolicy.Classifier = contentpolicy.LocalClassifier{}
if cfg.ContentSafetyProvider == "managed_http" {
managedSafety, safetyErr := contentpolicy.NewManagedClassifier(cfg.ContentSafetyEndpoint, cfg.ContentSafetyAPIKey, cfg.ContentSafetyPolicyRef, cfg.ContentSafetyTimeout)
@@ -446,17 +562,16 @@ func Run(ctx context.Context, serviceName string) error {
if processorErr != nil {
return fmt.Errorf("create content extraction processor: %w", processorErr)
}
if vikingMemory != nil {
outboxWorker.Register(vikingmemory.SyncEvent, vikingMemory.Handle)
outboxWorker.Register("conversation.content_extraction_requested", func(ctx context.Context, event outbox.Event) error {
if err := vikingMemory.Enqueue(ctx, event); err != nil {
return err
}
return extractionProcessor.Handle(ctx, event)
})
} else {
outboxWorker.Register("conversation.content_extraction_requested", extractionProcessor.Handle)
}
// CALL-MEMORY-1.1 has one authoritative extractor. Historical AddSession
// queue entries must not repopulate independently extracted provider memory.
outboxWorker.Register(vikingmemory.SyncEvent, func(context.Context, outbox.Event) error { return nil })
outboxWorker.Register("conversation.content_extraction_requested", extractionProcessor.Handle)
outboxWorker.Register(extraction.DirectStoryEvent, extractionProcessor.HandleDirectStory)
outboxWorker.Register(extraction.ImportStoryEvent, extractionProcessor.HandleImport)
memoirService.Safety = contentSafety
go memoirService.Run(ctx)
callMemoryService.ConfigureExtractor(contentModelRegistry)
go callMemoryService.Run(ctx)
memoryIndexProcessor, processorErr := memoryindex.NewProcessor(databasePool, memoryEmbedding)
if processorErr != nil {
return fmt.Errorf("create memory index processor: %w", processorErr)
@@ -549,6 +664,8 @@ func Run(ctx context.Context, serviceName string) error {
Readiness: readiness,
TokenVerifier: tokenVerifier,
IdentityRepository: identityRepository,
CallMemoryService: callMemoryService,
MemoirService: memoirService,
SaaSService: saasService,
DeviceVerifier: deviceVerifier,
DeviceService: deviceRepository,
@@ -588,7 +705,7 @@ func Run(ctx context.Context, serviceName string) error {
Handler: otelhttp.NewHandler(httpapi.Middleware(logger, serviceName, 1<<20, rateLimiterAdapter{limiter}, router), serviceName),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
}