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
+69 -17
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"unicode/utf8"
"data-terminal-cloud/internal/contentpolicy"
@@ -29,6 +30,7 @@ type Processor struct {
type turn struct {
id, publicID, speaker, text string
interrupted bool
sourceRevision int64
}
type extractionSnapshot struct {
@@ -99,8 +101,7 @@ func (p *Processor) Handle(ctx context.Context, event outbox.Event) error {
// Only the subject's final turns can become a summary, story, fact, or
// continuation cue. Agent turns may contain questions or generated wording
// and must never be promoted into the subject's personal history.
content := joinedSubjectContent(sourceTurns)
generated, err := p.generateSafely(ctx, content, snapshot.tenantID)
generated, previous, err := p.generateEventsSafely(ctx, snapshot, time.Now().In(time.FixedZone("CST", 8*3600)).Format("2006-01-02"))
if err != nil {
return err
}
@@ -112,6 +113,11 @@ func (p *Processor) Handle(ctx context.Context, event outbox.Event) error {
return err
}
defer tx.Rollback(context.Background())
if generated.Stories != nil || generated.DailyEvents != nil {
if err = lockStoryContext(ctx, tx, snapshot, previous); err != nil {
return err
}
}
current, err := loadExtractionSnapshot(ctx, tx, event.AggregateID, true)
if err != nil {
return err
@@ -128,7 +134,7 @@ func (p *Processor) Handle(ctx context.Context, event outbox.Event) error {
if current.digest != snapshot.digest {
return outbox.Retryable(ErrConversationContentChanged)
}
return storeGeneration(ctx, tx, event.AggregateID, current, generated, generationVersion)
return storeGeneration(ctx, tx, event.AggregateID, current, generated, generationVersion, previous)
}
func (p *Processor) generateSafely(ctx context.Context, content string, tenantIDs ...string) (GenerationResult, error) {
@@ -158,7 +164,14 @@ func (p *Processor) generateSafely(ctx context.Context, content string, tenantID
}
func generatedSafetyContent(result GenerationResult) string {
return strings.Join([]string{result.Summary, strings.Join(result.Keywords, "、"), result.OpenTopic, result.StoryTitle, result.StoryContent, result.FactContent}, "\n")
parts := []string{result.Summary, strings.Join(result.Keywords, "、"), result.OpenTopic, result.StoryTitle, result.StoryContent, result.FactContent}
for _, s := range result.Stories {
parts = append(parts, s.Title, s.Content, s.FactContent, strings.Join(s.NewDetails, "\n"))
}
for _, d := range result.DailyEvents {
parts = append(parts, d.Title, d.Content)
}
return strings.Join(parts, "\n")
}
func loadExtractionSnapshot(ctx context.Context, q queryer, conversationID string, lock bool) (extractionSnapshot, error) {
@@ -187,7 +200,11 @@ func loadExtractionSnapshot(ctx context.Context, q queryer, conversationID strin
snapshot.storyExtractionAllowed = consents["story_extraction"]
rows, err := q.Query(ctx, `
SELECT id::text,public_id::text,speaker,final_text,interrupted,sequence_no
FROM conversation_turn WHERE conversation_id=$1::uuid ORDER BY sequence_no,id`, conversationID)
FROM conversation_turn ct WHERE conversation_id=$1::uuid AND final_text<>'[deleted]'
AND NOT EXISTS(SELECT 1 FROM memory_source_suppression ms CROSS JOIN LATERAL jsonb_array_elements(ms.source_refs) r
JOIN story_draft sd ON sd.public_id::text=r->>'story_id' JOIN story_revision sr ON sr.story_id=sd.id
JOIN story_source ss ON ss.story_revision_id=sr.id WHERE ss.conversation_turn_id=ct.id)
ORDER BY sequence_no,id`, conversationID)
if err != nil {
return extractionSnapshot{}, fmt.Errorf("read extraction turns: %w", err)
}
@@ -222,15 +239,16 @@ func requestedGenerationVersion(payload json.RawMessage) int64 {
func snapshotDigest(snapshot extractionSnapshot) string {
type digestTurn struct {
ID string `json:"id"`
PublicID string `json:"public_id"`
Speaker string `json:"speaker"`
Text string `json:"text"`
Interrupted bool `json:"interrupted"`
ID string `json:"id"`
PublicID string `json:"public_id"`
Speaker string `json:"speaker"`
Text string `json:"text"`
Interrupted bool `json:"interrupted"`
SourceRevision int64 `json:"source_revision"`
}
digestTurns := make([]digestTurn, 0, len(snapshot.turns))
for _, item := range snapshot.turns {
digestTurns = append(digestTurns, digestTurn{item.id, item.publicID, item.speaker, item.text, item.interrupted})
digestTurns = append(digestTurns, digestTurn{item.id, item.publicID, item.speaker, item.text, item.interrupted, item.sourceRevision})
}
digestInput := struct {
Exists bool
@@ -264,7 +282,7 @@ func joinedSubjectContent(turns []turn) string {
return strings.Join(parts, "\n")
}
func storeGeneration(ctx context.Context, tx pgx.Tx, conversationID string, snapshot extractionSnapshot, generated GenerationResult, generationVersion int64) error {
func storeGeneration(ctx context.Context, tx pgx.Tx, conversationID string, snapshot extractionSnapshot, generated GenerationResult, generationVersion int64, previous []PreviousStory) error {
sourceTurns := subjectTurns(snapshot.turns)
sourcePublicIDs := make([]string, 0, len(sourceTurns))
for _, item := range sourceTurns {
@@ -294,15 +312,43 @@ func storeGeneration(ctx context.Context, tx pgx.Tx, conversationID string, snap
return fmt.Errorf("store open topic: %w", err)
}
}
if generated.Stories != nil || generated.DailyEvents != nil {
if err := storeEventStories(ctx, tx, conversationID, "", "", 0, snapshot, generated, generationVersion, previous); err != nil {
return err
}
return tx.Commit(ctx)
}
if !generated.AllowStory {
return tx.Commit(ctx)
}
if err := storeStoryCandidate(ctx, tx, conversationID, "", 0, snapshot, generated, generationVersion); err != nil {
return err
}
return tx.Commit(ctx)
}
// Both runtimes use the same story/revision/fact writer and review workflow.
// Direct calls retain their native utterance identifiers and source revisions.
func storeStoryCandidate(ctx context.Context, tx pgx.Tx, conversationID, callSessionID string, epoch int64, snapshot extractionSnapshot, generated GenerationResult, generationVersion int64) error {
sourceTurns := subjectTurns(snapshot.turns)
sourceIDs := make([]string, 0, len(sourceTurns))
for _, item := range sourceTurns {
sourceIDs = append(sourceIDs, item.publicID)
}
sourceJSON, _ := json.Marshal(sourceIDs)
var storyID string
err := tx.QueryRow(ctx, `
var err error
if callSessionID != "" {
err = tx.QueryRow(ctx, `INSERT INTO story_draft(tenant_id,subject_id,call_session_id,call_privacy_epoch,title,status,confidence,sensitivity,generation_version)
VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,'CANDIDATE',$6,'normal',$7)
ON CONFLICT(call_session_id,generation_version,event_index) DO UPDATE SET updated_at=story_draft.updated_at RETURNING id::text`, snapshot.tenantID, snapshot.subjectID, callSessionID, epoch, generated.StoryTitle, generated.Confidence, generationVersion).Scan(&storyID)
} else {
err = tx.QueryRow(ctx, `
INSERT INTO story_draft (tenant_id,subject_id,conversation_id,title,status,confidence,sensitivity,generation_version)
VALUES ($1::uuid,$2::uuid,$3::uuid,$4,'CANDIDATE',$5,'normal',$6)
ON CONFLICT (conversation_id,generation_version) DO UPDATE SET updated_at=story_draft.updated_at
ON CONFLICT (conversation_id,generation_version,event_index) DO UPDATE SET updated_at=story_draft.updated_at
RETURNING id::text`, snapshot.tenantID, snapshot.subjectID, conversationID, generated.StoryTitle, generated.Confidence, generationVersion).Scan(&storyID)
}
if err != nil {
return fmt.Errorf("store story candidate: %w", err)
}
@@ -319,9 +365,15 @@ func storeGeneration(ctx context.Context, tx pgx.Tx, conversationID string, snap
return err
}
for _, item := range sourceTurns {
if _, err = tx.Exec(ctx, `
if callSessionID != "" {
_, err = tx.Exec(ctx, `INSERT INTO story_source(story_revision_id,call_session_id,call_utterance_id,call_source_revision,source_start_offset,source_end_offset,source_role)
VALUES($1::uuid,$2::uuid,$3,$4,0,$5,'subject') ON CONFLICT DO NOTHING`, revisionID, callSessionID, item.id, item.sourceRevision, utf8.RuneCountInString(item.text))
} else {
_, err = tx.Exec(ctx, `
INSERT INTO story_source (story_revision_id,conversation_turn_id,source_start_offset,source_end_offset,source_role)
VALUES ($1::uuid,$2::uuid,0,$3,'subject') ON CONFLICT DO NOTHING`, revisionID, item.id, utf8.RuneCountInString(item.text)); err != nil {
VALUES ($1::uuid,$2::uuid,0,$3,'subject') ON CONFLICT DO NOTHING`, revisionID, item.id, utf8.RuneCountInString(item.text))
}
if err != nil {
return fmt.Errorf("store story source: %w", err)
}
}
@@ -331,7 +383,7 @@ func storeGeneration(ctx context.Context, tx pgx.Tx, conversationID string, snap
WHERE NOT EXISTS (SELECT 1 FROM fact_candidate WHERE story_id=$3::uuid AND generation_version=$7)`, snapshot.tenantID, snapshot.subjectID, storyID, generated.FactContent, generated.Confidence, sourceJSON, generationVersion); err != nil {
return fmt.Errorf("store fact candidate: %w", err)
}
return tx.Commit(ctx)
return nil
}
func summaryWithKeywords(summary string, keywords []string) string {