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
+100
View File
@@ -0,0 +1,100 @@
package onboarding
import (
"context"
"encoding/json"
"errors"
"time"
"data-terminal-cloud/internal/outbox"
)
// Wait in this HTTP request, not in the binding transaction. The outbox worker
// uses the same durable registration fence and may finish the work concurrently.
func (s *Service) finishBLEVoice(ctx context.Context, d Device, in BLEBindInput) (out map[string]any, err error) {
ctx, cancel := context.WithTimeout(ctx, 35*time.Second)
defer cancel()
defer func() {
if errors.Is(err, context.DeadlineExceeded) {
err = fail(504, "VOICE_REGISTRATION_TIMEOUT", "获取火山配置超时,绑定已保存,请使用原绑定令牌重试")
}
}()
started := false
for {
fields, event, e := s.bleVoiceSnapshot(ctx, d, in)
if e != nil {
return nil, e
}
switch fields["voice_status"] {
case "ready":
v, ok := fields["voice"].(DeviceVoiceCredential)
if !ok || v.InstanceID == "" || v.ProductKey == "" || v.DeviceName == "" || !validVoiceSecret(v.DeviceSecret) || !validRTCAppID(v.RTCAppID) {
return nil, fail(502, "VOICE_CONFIG_INVALID", "火山配置不完整,绑定已保存,请联系平台处理")
}
return map[string]any{"status": "success", "instance_id": v.InstanceID, "product_key": v.ProductKey, "device_name": v.DeviceName, "device_secret": v.DeviceSecret, "RTCAppID": v.RTCAppID}, nil
case "paused":
return nil, fail(503, "VOICE_REGISTRATION_PAUSED", "语音服务暂不可用,绑定已保存,请稍后使用原绑定令牌重试")
case "migration_required":
return nil, fail(409, "VOICE_MIGRATION_REQUIRED", "绑定已保存,旧设备需要补交原火山凭据,请联系平台处理")
case "reconcile_required":
return nil, fail(502, "VOICE_REGISTRATION_FAILED", "绑定已保存,火山注册结果未确认,请联系平台核对")
}
if !started {
started = true
if e = s.processServerActivation(ctx, event); e != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
return nil, fail(502, "VOICE_REGISTRATION_FAILED", "绑定已保存,获取火山配置失败,请稍后重试或联系平台处理")
}
continue
}
timer := time.NewTimer(250 * time.Millisecond)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
}
func (s *Service) bleVoiceSnapshot(ctx context.Context, authenticated Device, in BLEBindInput) (map[string]any, outbox.Event, error) {
var event outbox.Event
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, event, err
}
defer tx.Rollback(ctx)
d, err := readDevice(ctx, tx, authenticated.ID, true)
if err != nil {
return nil, event, err
}
c, err := readBLEClaim(ctx, tx, in.Token, d.ID)
if err != nil {
return nil, event, err
}
result, err := bleResult(ctx, tx, d, c, s.now())
if err != nil {
return nil, event, err
}
if !deviceAllowed(d) || result["status"] != "success" {
return nil, event, fail(409, "BINDING_CHANGED", "设备已解绑或归属发生变化,请重新绑定")
}
if err = bleClaimPermission(ctx, tx, c); err != nil {
return nil, event, err
}
fields, err := s.voiceFields(ctx, tx, d)
if err != nil {
return nil, event, err
}
j, err := readJob(ctx, tx, d.ID)
if err != nil {
return nil, event, err
}
event.Payload, err = json.Marshal(map[string]any{"device_id": d.ID, "activation_id": j.ID, "binding_generation": d.Generation})
if err != nil {
return nil, event, err
}
return fields, event, tx.Commit(ctx)
}