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
+57 -10
View File
@@ -11,9 +11,11 @@ import (
"github.com/jackc/pgx/v5"
)
// The worker prepares encrypted configuration. Only the device executes SDK
// registration, after a durable C08 permit. An outbox retry makes no SDK call.
// Production registers in the worker. The legacy path is retained for migration tests.
func (s *Service) ProcessActivation(ctx context.Context, event outbox.Event) error {
if !s.options.LegacyDeviceRegistration {
return s.processServerActivation(ctx, event)
}
if !s.options.ActivationEnabled {
return outbox.Retryable(errors.New("onboarding activation paused"))
}
@@ -42,7 +44,7 @@ func (s *Service) ProcessActivation(ctx context.Context, event outbox.Event) err
return nil
}
var bound bool
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND generation=$2 AND unbound_at IS NULL)`, d.ID, d.Generation).Scan(&bound); err != nil {
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND generation=$2 AND unbound_at IS NULL) OR EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=$1::uuid AND generation=$2 AND released_at IS NULL)`, d.ID, d.Generation).Scan(&bound); err != nil {
return err
}
if !bound {
@@ -62,7 +64,7 @@ func (s *Service) ProcessActivation(ctx context.Context, event outbox.Event) err
func (s *Service) expireAttempt(ctx context.Context, tx pgx.Tx, d Device, j *activationJob) error {
// Permit expiry means a call may already have started. It is never proof of
// failure. Leave room for a 60-second conversation and late result delivery.
tag, err := tx.Exec(ctx, `UPDATE device_activation_attempt SET status='UNKNOWN' WHERE activation_id=$1::uuid AND status='CONTACT_POSSIBLE' AND permit_expires_at < $2 AND executor='DEVICE'`, j.ID, s.now().Add(-90*time.Second))
tag, err := tx.Exec(ctx, `UPDATE device_activation_attempt SET status='UNKNOWN' WHERE activation_id=$1::uuid AND status='CONTACT_POSSIBLE' AND permit_expires_at < $2 AND executor IN ('DEVICE','SERVER')`, j.ID, s.now().Add(-90*time.Second))
if err != nil {
return err
}
@@ -78,11 +80,26 @@ func (s *Service) expireAttempt(ctx context.Context, tx pgx.Tx, d Device, j *act
}
func (s *Service) ensureConfig(ctx context.Context, tx pgx.Tx, d Device, j *activationJob) error {
if !s.options.LegacyDeviceRegistration && (j.Registered == nil || len(j.Secret) == 0 || j.RTCAppID == "") {
return nil
}
if j.Generation != d.Generation || !oneOf(j.Status, "NOT_STARTED", "RESERVED", "REGISTERED_OBSERVED", "READY") {
return errState
}
if len(j.Config) > 0 && j.Expires != nil && j.Expires.After(s.now()) {
return nil
if s.options.LegacyDeviceRegistration {
return nil
}
cached, e := s.crypt.open(j.Config, "config:"+j.ID)
if e != nil {
return e
}
var old struct {
IoT map[string]any `json:"iot"`
}
if json.Unmarshal(cached, &old) == nil && old.IoT["product_secret"] == nil && old.IoT["device_secret"] != nil {
return nil
}
}
if !s.options.ActivationEnabled {
return fail(503, "ACTIVATION_PAUSED", "激活暂未开放")
@@ -111,7 +128,17 @@ func (s *Service) ensureConfig(ctx context.Context, tx pgx.Tx, d Device, j *acti
j.ConfigVersion++
}
expires := s.now().Add(24 * time.Hour)
raw, err := json.Marshal(map[string]any{"contract_version": ContractVersion, "device_id": d.FactoryID, "activation_id": j.ID, "config_version": j.ConfigVersion, "binding_generation": d.Generation, "expires_at_ms": expires.UnixMilli(), "iot": map[string]any{"instance_id": j.Instance, "product_key": j.Product, "product_secret": s.options.ProductSecret, "device_name": j.Name}, "bot_id": j.Bot})
iot := map[string]any{"instance_id": j.Instance, "product_key": j.Product, "device_name": j.Name}
if s.options.LegacyDeviceRegistration {
iot["product_secret"] = s.options.ProductSecret
} else {
secret, e := s.crypt.open(j.Secret, "provider:"+j.ID)
if e != nil {
return e
}
iot["device_secret"], iot["RTCAppID"] = string(secret), j.RTCAppID
}
raw, err := json.Marshal(map[string]any{"contract_version": ContractVersion, "device_id": d.FactoryID, "activation_id": j.ID, "config_version": j.ConfigVersion, "binding_generation": d.Generation, "expires_at_ms": expires.UnixMilli(), "iot": iot, "bot_id": j.Bot})
if err != nil {
return err
}
@@ -122,7 +149,11 @@ func (s *Service) ensureConfig(ctx context.Context, tx pgx.Tx, d Device, j *acti
j.Status = "RESERVED"
if j.Registered != nil {
j.Status = "REGISTERED_OBSERVED"
j.Engine = j.Registered
if s.options.LegacyDeviceRegistration {
j.Engine = j.Registered
} else {
j.Engine = nil
}
}
_, err = tx.Exec(ctx, `UPDATE device_activation_job SET provider_instance_id=$2,provider_product_key=$3,provider_bot_id=$4,config_version=$5,config_ciphertext=$6,config_digest=$7,config_expires_at=$8,config_ack_at=NULL,engine_ready_at=$9,status=$10,version=version+1,updated_at=now() WHERE id=$1::uuid`, j.ID, j.Instance, j.Product, j.Bot, j.ConfigVersion, j.Config, j.Digest, expires, j.Engine, j.Status)
if err != nil {
@@ -164,6 +195,9 @@ func (s *Service) activationCommand(ctx context.Context, tx pgx.Tx, d Device) (a
if !s.options.ActivationEnabled || !oneOf(j.Status, "NOT_STARTED", "RESERVED", "REGISTERED_OBSERVED", "READY") {
return nil, nil
}
if !s.options.LegacyDeviceRegistration && (j.Registered == nil || len(j.Secret) == 0 || j.RTCAppID == "") {
return nil, nil
}
if err = s.ensureConfig(ctx, tx, d, &j); err != nil {
return nil, err
}
@@ -219,7 +253,7 @@ func (s *Service) ActivationAck(ctx context.Context, d Device, id string, raw []
if in.LocalGeneration < previous {
return nil, errState
}
_, err = tx.Exec(ctx, `UPDATE device_activation_job SET config_ack_at=COALESCE(config_ack_at,now()),config_local_generation=$2,config_ack_boot_id=$3::uuid,updated_at=now() WHERE id=$1::uuid`, j.ID, in.LocalGeneration, current.BootID)
_, err = tx.Exec(ctx, `UPDATE device_activation_job SET config_ack_at=COALESCE(config_ack_at,now()),engine_ready_at=now(),config_local_generation=$2,config_ack_boot_id=$3::uuid,updated_at=now() WHERE id=$1::uuid`, j.ID, in.LocalGeneration, current.BootID)
if err != nil {
return nil, err
}
@@ -285,6 +319,9 @@ func (s *Service) Attempt(ctx context.Context, d Device, id string, raw []byte,
return nil, fail(429, "ACTIVATION_ATTEMPT_LIMIT", "激活尝试过于频繁")
}
stage := "SDK_REGISTER"
if !s.options.LegacyDeviceRegistration && j.Registered == nil {
return nil, fail(409, "SERVER_REGISTRATION_REQUIRED", "火山注册由平台执行,请获取语音配置")
}
if j.Registered != nil {
if j.Ack == nil {
return nil, fail(409, "ACTIVATION_STAGE_NOT_READY", "请先保存并确认注册结果")
@@ -336,8 +373,8 @@ func (s *Service) ActivationResult(ctx context.Context, d Device, id string, raw
}
var stage, status string
var gen, version int64
var stored, conversation *string
err = tx.QueryRow(ctx, `SELECT stage,status,binding_generation,config_version,result_digest,conversation_id::text FROM device_activation_attempt WHERE id=$1::uuid AND activation_id=$2::uuid AND executor='DEVICE' FOR UPDATE`, in.AttemptID, in.ActivationID).Scan(&stage, &status, &gen, &version, &stored, &conversation)
var stored, conversation, callSession *string
err = tx.QueryRow(ctx, `SELECT stage,status,binding_generation,config_version,result_digest,conversation_id::text,call_session_id::text FROM device_activation_attempt WHERE id=$1::uuid AND activation_id=$2::uuid AND executor='DEVICE' FOR UPDATE`, in.AttemptID, in.ActivationID).Scan(&stage, &status, &gen, &version, &stored, &conversation, &callSession)
if errors.Is(err, pgx.ErrNoRows) {
return nil, errAccess
}
@@ -369,6 +406,16 @@ func (s *Service) ActivationResult(ctx context.Context, d Device, id string, raw
return nil, err
}
}
if callSession != nil {
var valid bool
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM call_session c JOIN call_event a ON a.session_id=c.id AND a.event_type='MEDIA_CONNECTED' JOIN call_event b ON b.session_id=c.id AND b.event_type='FIRST_AUDIO_RECEIVED' JOIN call_event p ON p.session_id=c.id AND p.event_type='FIRST_PLAYBACK_STARTED' WHERE c.id=$1::uuid AND c.device_id=$2::uuid AND c.binding_generation=$3 AND c.purpose='FIRST_TEST' AND c.call_state='ENDED' AND c.local_audio_off AND c.sdk_stopped AND a.seq<b.seq AND b.seq<=p.seq AND c.ended_at-c.created_at<=interval '90 seconds')`, *callSession, current.ID, current.Generation).Scan(&valid)
if err != nil {
return nil, err
}
if !valid {
return nil, fail(409, "TEST_EVIDENCE_REQUIRED", "首通尚缺少连接、首音频、首播放或停止证据")
}
}
}
_, err = tx.Exec(ctx, `UPDATE device_activation_attempt SET status=$2,sdk_result=$3,result_digest=$4,completed_at=now() WHERE id=$1::uuid`, in.AttemptID, in.Outcome, in.SDKResult, digest(raw))
if err != nil {