feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
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:
@@ -0,0 +1,89 @@
|
||||
package extraction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"data-terminal-cloud/internal/outbox"
|
||||
"errors"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const ImportStoryEvent = "memoir.import_requested"
|
||||
|
||||
func loadImport(ctx context.Context, q queryer, id string) (extractionSnapshot, string, error) {
|
||||
var s extractionSnapshot
|
||||
var status, date string
|
||||
e := q.QueryRow(ctx, `SELECT ci.tenant_id::text,ci.subject_id::text,ci.status,to_char(ci.created_at AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD'),
|
||||
sp.status='active' AND NOT EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.subject_id=sp.id AND pr.request_type='DELETE')
|
||||
AND NOT EXISTS(SELECT 1 FROM (VALUES('transcript_storage'),('story_extraction')) k(kind) WHERE NOT EXISTS(
|
||||
SELECT 1 FROM consent_record cr WHERE cr.subject_id=sp.id AND cr.consent_type=k.kind AND cr.withdrawn_at IS NULL
|
||||
AND cr.granted_at<=ci.created_at AND (cr.expires_at IS NULL OR cr.expires_at>now())))
|
||||
FROM memoir_chat_import ci JOIN subject_profile sp ON sp.id=ci.subject_id WHERE ci.id=$1::uuid`, id).Scan(&s.tenantID, &s.subjectID, &status, &date, &s.storyExtractionAllowed)
|
||||
if errors.Is(e, pgx.ErrNoRows) {
|
||||
return s, "", nil
|
||||
}
|
||||
if e != nil {
|
||||
return s, "", e
|
||||
}
|
||||
s.exists = true
|
||||
s.status = status
|
||||
rows, e := q.Query(ctx, `SELECT id::text,speaker,content FROM memoir_import_turn WHERE import_id=$1::uuid ORDER BY sequence_no`, id)
|
||||
if e != nil {
|
||||
return s, "", e
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var t turn
|
||||
if e = rows.Scan(&t.id, &t.speaker, &t.text); e != nil {
|
||||
return s, "", e
|
||||
}
|
||||
t.publicID = t.id
|
||||
s.turns = append(s.turns, t)
|
||||
}
|
||||
s.digest = snapshotDigest(s)
|
||||
return s, date, rows.Err()
|
||||
}
|
||||
func (p *Processor) HandleImport(ctx context.Context, event outbox.Event) error {
|
||||
s, date, e := loadImport(ctx, p.pool, event.AggregateID)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if !s.exists || s.status == "READY" {
|
||||
return nil
|
||||
}
|
||||
if !s.storyExtractionAllowed {
|
||||
return errors.New("import consent unavailable")
|
||||
}
|
||||
result, previous, e := p.generateEventsSafely(ctx, s, date)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
tx, e := p.pool.Begin(ctx)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer tx.Rollback(context.Background())
|
||||
if e = lockStoryContext(ctx, tx, s, previous); e != nil {
|
||||
return e
|
||||
}
|
||||
if _, e = tx.Exec(ctx, `SELECT id FROM memoir_chat_import WHERE id=$1::uuid FOR UPDATE`, event.AggregateID); e != nil {
|
||||
return e
|
||||
}
|
||||
current, _, e := loadImport(ctx, tx, event.AggregateID)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if current.status == "READY" {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
if !current.storyExtractionAllowed || current.digest != s.digest {
|
||||
return outbox.Retryable(ErrConversationContentChanged)
|
||||
}
|
||||
if e = storeEventStories(ctx, tx, "", "", event.AggregateID, 0, current, result, 1, previous); e != nil {
|
||||
return e
|
||||
}
|
||||
_, e = tx.Exec(ctx, `UPDATE memoir_chat_import SET status='READY' WHERE id=$1::uuid`, event.AggregateID)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user