package orchestration import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "data-terminal-cloud/internal/outbox" "data-terminal-cloud/internal/volcsubtitle" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrNativeSubtitleSpeaker = errors.New("native subtitle speaker is not mapped") type NativeSubtitleResult struct { Mode string Accepted, Duplicates int } type NativeSubtitleService struct{ pool *pgxpool.Pool } func NewNativeSubtitleService(pool *pgxpool.Pool) *NativeSubtitleService { return &NativeSubtitleService{pool: pool} } // Hardware low_load callbacks currently carry a device identity, but no session // identity. Validate connectivity without guessing a conversation or storing text. func (s *NativeSubtitleService) ProbeNativeSubtitleDevice(ctx context.Context, packet volcsubtitle.Packet) error { var found bool err := s.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM hardware_agent_provider_credential WHERE product_key=$1 AND provider_device_name=$2 AND status='ACTIVE')`, packet.ProductKey, packet.DeviceName).Scan(&found) if err != nil { return err } if !found { return ErrConversationNotFound } return nil } func (s *NativeSubtitleService) ProcessNativeSubtitle(ctx context.Context, publicID string, packet volcsubtitle.Packet) (NativeSubtitleResult, error) { result := NativeSubtitleResult{Mode: "probe"} tx, err := s.pool.Begin(ctx) if err != nil { return result, err } defer tx.Rollback(context.Background()) result, err = s.processNativeSubtitleTx(ctx, tx, publicID, packet) if err != nil { return NativeSubtitleResult{}, err } return result, tx.Commit(ctx) } func (s *NativeSubtitleService) processNativeSubtitleTx(ctx context.Context, tx pgx.Tx, publicID string, packet volcsubtitle.Packet) (NativeSubtitleResult, error) { result := NativeSubtitleResult{Mode: "probe"} var id, tenantID, subjectID, retention, status, deviceUID, agentUID, deviceName, botID string var primary, allowed bool err := tx.QueryRow(ctx, `SELECT c.id::text,c.tenant_id::text,c.subject_id::text,c.retention_mode,c.status, COALESCE(hs.capability_snapshot->>'subtitle_device_user_id',''),COALESCE(hs.capability_snapshot->>'subtitle_agent_user_id',''), COALESCE(hs.capability_snapshot->>'subtitle_device_user_id',''),hs.bot_id,COALESCE((hs.capability_snapshot->>'native_subtitle_primary')::boolean,false), EXISTS(SELECT 1 FROM consent_record cr WHERE cr.tenant_id=c.tenant_id AND cr.subject_id=c.subject_id AND cr.consent_type='transcript_storage' AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now())) FROM conversation c JOIN hardware_agent_session hs ON hs.conversation_id=c.id AND hs.tenant_id=c.tenant_id WHERE c.public_id=$1::uuid FOR UPDATE OF c`, publicID).Scan(&id, &tenantID, &subjectID, &retention, &status, &deviceUID, &agentUID, &deviceName, &botID, &primary, &allowed) if errors.Is(err, pgx.ErrNoRows) { return result, ErrConversationNotFound } if err != nil { return result, err } if retention == "none" || !allowed { return result, ErrTranscriptStorageNotAllowed } if status != "ROOM_ISSUED" && status != "ACTIVE" && status != "STOPPING" && status != "COMPLETED" && status != "FAILED" { return result, ErrConversationStateConflict } if !primary { return result, nil } result.Mode = "primary" for _, part := range packet.Data { // Server callbacks contain independent completed clauses; retaining only // paragraph=true would silently drop all preceding clauses of the reply. if !part.Definite || strings.TrimSpace(part.Text) == "" { continue } speaker, err := nativeSubtitleSpeaker(part.UserID, deviceUID, deviceName, agentUID, botID) if err != nil { return result, err } digest := sha256.Sum256([]byte(part.UserID)) key := fmt.Sprintf("volc-subv-%s-%d-%d", hex.EncodeToString(digest[:8]), *part.RoundID, *part.Sequence) var stored string err = tx.QueryRow(ctx, `SELECT final_text FROM conversation_turn WHERE conversation_id=$1::uuid AND turn_key=$2`, id, key).Scan(&stored) if err == nil { if stored != part.Text { return result, ErrHardwareEventConflict } result.Duplicates++ continue } if !errors.Is(err, pgx.ErrNoRows) { return result, err } _, err = tx.Exec(ctx, `INSERT INTO conversation_turn(tenant_id,subject_id,conversation_id,turn_key,sequence_no,speaker,start_at,end_at,final_text,interrupted,source_type) SELECT $1::uuid,$2::uuid,$3::uuid,$4,COALESCE(max(sequence_no),0)+1,$5,now(),now(),$6,false,'provider' FROM conversation_turn WHERE conversation_id=$3::uuid`, tenantID, subjectID, id, key, speaker, part.Text) if err != nil { return result, err } result.Accepted++ } if result.Accepted > 0 && (status == "COMPLETED" || status == "FAILED") { if err = enqueueHardwareContentExtraction(ctx, tx, outbox.NewStore(s.pool), tenantID, id); err != nil { return result, err } } return result, nil } func nativeSubtitleSpeaker(uid, deviceUID, deviceName, agentUID, botID string) (string, error) { subject := uid != "" && (uid == deviceUID || uid == deviceName) agent := uid != "" && (uid == agentUID || uid == botID) if subject == agent { return "", ErrNativeSubtitleSpeaker } if subject { return "subject", nil } return "agent", nil }