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
+162
View File
@@ -0,0 +1,162 @@
package workflowbridge
import (
"bytes"
"context"
"data-terminal-cloud/internal/dify"
"data-terminal-cloud/internal/extraction"
"data-terminal-cloud/internal/memoir"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
)
type fixtureEditor struct {
calls atomic.Int32
input string
mu sync.Mutex
}
func (e *fixtureEditor) GenerateJSON(_ context.Context, input, prompt string) (json.RawMessage, error) {
e.calls.Add(1)
raw, _ := json.Marshal(extraction.GenerationResult{Meaningful: true, StoryWorthy: true, DecisionReason: "有原话支持的历史事件", Summary: "进厂往事", Keywords: []string{"进厂"}, Confidence: .9, Stories: []extraction.EventStory{{Title: "进厂", Content: "1970年进厂。", FactContent: "1970年进厂", EventTime: "1970年", SourceIDs: []string{"turn-1"}}}})
var out map[string]any
_ = json.Unmarshal(raw, &out)
out["daily_events"] = []any{}
return json.Marshal(out)
}
func (e *fixtureEditor) GenerateJSONSchema(_ context.Context, input, prompt string, schema json.RawMessage) (json.RawMessage, error) {
e.calls.Add(1)
e.mu.Lock()
e.input = input
e.mu.Unlock()
var in struct {
Stories []memoir.Source `json:"stories"`
}
_ = json.Unmarshal([]byte(input), &in)
d := memoir.Document{Title: "测试回忆录"}
for _, s := range in.Stories {
d.Chapters = append(d.Chapters, memoir.Chapter{StoryID: s.ID, EventTime: s.EventTime, Title: s.Title, Paragraphs: []string{s.Content}, IllustrationPrompt: "水彩情景插画"})
}
return json.Marshal(d)
}
type overdueImages struct{}
func (overdueImages) Generate(context.Context, string) ([]byte, error) {
return nil, extraction.ErrModelAccountOverdue
}
func postStep(t *testing.T, base, step string, ticket dify.Ticket, index int) map[string]any {
t.Helper()
raw, _ := json.Marshal(stepInput{ID: ticket.ID, Token: ticket.Token, Index: index})
r, err := http.Post(base+"/steps/"+step, "application/json", bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
defer r.Body.Close()
var out map[string]any
if json.NewDecoder(r.Body).Decode(&out) != nil {
t.Fatal("invalid step response")
}
return out
}
func TestStepsAreCachedAndNeverExposePrivateContent(t *testing.T) {
editor := &fixtureEditor{}
s := New("backend-secret", editor, editor, overdueImages{})
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
ticket, err := b.Register(context.Background(), "extract", extraction.GenerationInput{Content: "[turn-1] 长者:1970年进厂。私密原话标记", PreviousStories: []extraction.PreviousStory{}})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
out := postStep(t, srv.URL, "classify", ticket, 0)
raw, _ := json.Marshal(out)
if out["success"] != true || strings.Contains(string(raw), "私密原话") || strings.Contains(string(raw), "1970") {
t.Errorf("private content in metadata %s", raw)
}
}()
}
wg.Wait()
if editor.calls.Load() != 1 {
t.Fatalf("duplicate model calls %d", editor.calls.Load())
}
req, _ := http.NewRequest("GET", srv.URL+"/contexts/"+ticket.ID, nil)
req.Header.Set("Authorization", "Bearer "+ticket.Token)
resp, _ := http.DefaultClient.Do(req)
resp.Body.Close()
if resp.StatusCode != 403 {
t.Fatal("step token could read private results")
}
b.Forget(ticket)
if s.find(ticket.ID) != nil {
t.Fatal("deleted context survives")
}
}
func TestWholeCatalogAndBillingDuringImagesKeepText(t *testing.T) {
editor := &fixtureEditor{}
s := New("backend-secret", editor, editor, overdueImages{})
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
sources := []memoir.Source{{ID: "one", Title: "进厂", Content: "1970年进厂。", EventTime: "1970年"}, {ID: "two", Title: "学徒", Content: "1971年学做手套。", EventTime: "1971年"}}
ticket, err := b.Register(context.Background(), "memoir", map[string]any{"stories": sources})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
if postStep(t, srv.URL, "write-book", ticket, 0)["success"] != true {
t.Fatal("whole book failed")
}
if postStep(t, srv.URL, "validate-book", ticket, 0)["success"] != true {
t.Fatal("validation failed")
}
for i := range sources {
if postStep(t, srv.URL, "illustrate", ticket, i)["error_code"] != "MODEL_ACCOUNT_OVERDUE" {
t.Fatal("billing error not identified")
}
}
if postStep(t, srv.URL, "finish", ticket, 0)["success"] != true {
t.Fatal("image billing failure discarded text")
}
var result struct {
Document memoir.Document `json:"document"`
}
if b.Result(context.Background(), ticket, &result) != nil || len(result.Document.Chapters) != 2 || result.Document.IllustrationErrors != 2 {
t.Fatal("whole text unavailable")
}
if editor.calls.Load() != 1 || !strings.Contains(editor.input, "one") || !strings.Contains(editor.input, "two") {
t.Fatal("model did not receive whole catalog once")
}
}
func TestPrivateResultPreservesEmptyEventArrays(t *testing.T) {
s := New("backend-secret", nil, nil, nil)
srv := httptest.NewServer(s)
defer srv.Close()
b := dify.NewBridge(srv.URL, "backend-secret")
ticket, err := b.Register(context.Background(), "extract", extraction.GenerationInput{Content: "今天吃早饭。"})
if err != nil {
t.Fatal(err)
}
defer b.Forget(ticket)
var out struct {
Result extraction.GenerationResult `json:"result"`
}
if err = b.Result(context.Background(), ticket, &out); err != nil || out.Result.Stories == nil || out.Result.DailyEvents == nil {
t.Fatalf("empty arrays lost in private transport: %v", err)
}
}