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
+87 -39
View File
@@ -18,24 +18,28 @@ import (
)
type GenerationInput struct {
TenantID string
Content string
TenantID string
Content string
ChatDate string
PreviousStories []PreviousStory
}
type GenerationResult struct {
Meaningful bool `json:"meaningful"`
StoryWorthy bool `json:"story_worthy"`
DecisionReason string `json:"decision_reason"`
Summary string `json:"summary"`
OpenTopic string `json:"open_topic"`
StoryTitle string `json:"story_title"`
StoryContent string `json:"story_content"`
FactContent string `json:"fact_content"`
Keywords []string `json:"keywords"`
Confidence float64 `json:"confidence"`
ModelRef string `json:"-"`
PromptVersion string `json:"-"`
AllowStory bool `json:"-"`
Meaningful bool `json:"meaningful"`
StoryWorthy bool `json:"story_worthy"`
DecisionReason string `json:"decision_reason"`
Summary string `json:"summary"`
OpenTopic string `json:"open_topic"`
StoryTitle string `json:"story_title"`
StoryContent string `json:"story_content"`
FactContent string `json:"fact_content"`
Keywords []string `json:"keywords"`
Confidence float64 `json:"confidence"`
ModelRef string `json:"-"`
PromptVersion string `json:"-"`
AllowStory bool `json:"-"`
DailyEvents []DailyEvent `json:"daily_events,omitempty"`
Stories []EventStory `json:"stories,omitempty"`
}
type Generator interface {
@@ -114,17 +118,20 @@ func safePolicyToken(value string) string {
}
type ArkGenerator struct {
endpoint string
apiKey string
model string
provider string
temperature float64
maxTokens int
client *http.Client
endpoint string
apiKey string
model string
provider string
temperature float64
maxTokens int
jsonMaxTokens int
client *http.Client
}
func (*ArkGenerator) SupportsEventStories() bool { return true }
func NewArkGenerator(baseURL, apiKey, model string, timeout time.Duration) (*ArkGenerator, error) {
return NewCompatibleGenerator("ark", baseURL, apiKey, model, 0.2, 1200, timeout)
return NewCompatibleGenerator("ark", baseURL, apiKey, model, 0.2, 8192, timeout)
}
func NewCompatibleGenerator(provider, baseURL, apiKey, model string, temperature float64, maxTokens int, timeout time.Duration) (*ArkGenerator, error) {
@@ -158,11 +165,14 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
"model": g.model,
"messages": []map[string]string{
{"role": "system", "content": contentValuePrompt},
{"role": "user", "content": content},
{"role": "user", "content": generationContent(input)},
},
"temperature": g.temperature,
"max_tokens": g.maxTokens,
}
if g.provider == "ark" {
requestBody["thinking"] = map[string]string{"type": "disabled"}
}
// JSON mode is sent only where the provider documents it or this project
// has exercised it. Other compatible providers still receive an explicit
// JSON-only prompt, followed by strict response extraction and validation.
@@ -198,7 +208,8 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
}
var completion struct {
Choices []struct {
Message struct {
FinishReason string `json:"finish_reason"`
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
@@ -206,6 +217,9 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
if err = json.Unmarshal(responseBody, &completion); err != nil || len(completion.Choices) == 0 {
return GenerationResult{}, outbox.Retryable(errors.New("content model response has no usable choice"))
}
if completion.Choices[0].FinishReason == "length" {
return GenerationResult{}, outbox.Retryable(errors.New("content model result truncated"))
}
jsonContent := extractJSONObject(completion.Choices[0].Message.Content)
jsonContent = strings.TrimPrefix(jsonContent, "```json")
jsonContent = strings.TrimPrefix(jsonContent, "```")
@@ -221,12 +235,55 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
result.StoryContent = strings.TrimSpace(result.StoryContent)
result.FactContent = strings.TrimSpace(result.FactContent)
result.Keywords = normalizedKeywords(result.Keywords)
if result.DailyEvents != nil {
// Review dates separately from story editing: a past event can be about
// ordinary life, and today's speech can describe a future plan.
raw, marshalErr := json.Marshal(map[string]any{"chat_date": input.ChatDate, "transcript": input.Content})
if marshalErr != nil {
return GenerationResult{}, marshalErr
}
checked, checkErr := g.GenerateJSON(ctx, string(raw), dailyExtractionPrompt)
if checkErr != nil {
return GenerationResult{}, checkErr
}
var daily struct {
Events []DailyEvent `json:"daily_events"`
}
if json.Unmarshal(checked, &daily) != nil || daily.Events == nil {
return GenerationResult{}, errors.New("daily classification failed validation")
}
result.DailyEvents = daily.Events
}
if err = normalizeEvents(&result, input); err != nil {
return GenerationResult{}, outbox.Retryable(err)
}
normalizeDecisionContent(&result)
if !validGeneratedContent(result) {
return GenerationResult{}, outbox.Retryable(errors.New("structured content failed validation"))
}
result.ModelRef = g.provider + ":" + g.model
result.PromptVersion = "content-value-" + g.provider + "-json-v3"
for _, d := range result.DailyEvents {
if d.TimeScope != "today_actual" || strings.TrimSpace(d.Evidence) == "" || !strings.Contains(input.Content, d.Evidence) {
return GenerationResult{}, errors.New("daily event lacks evidence of an actual event today")
}
}
actual := make([]DailyEvent, 0, len(result.DailyEvents))
for _, d := range result.DailyEvents {
future := false
for _, phrase := range []string{"明天", "明早", "前几天", "打算", "计划", "尚未", "还没动手", "想着", "还想", "一会儿", "以后再"} {
if strings.Contains(d.Content, phrase) {
future = true
break
}
}
if !future {
actual = append(actual, d)
}
}
if result.DailyEvents != nil {
result.DailyEvents = actual
}
result.PromptVersion = "content-events-" + g.provider + "-json-v5"
result.AllowStory = result.Meaningful && result.StoryWorthy
return result, nil
}
@@ -269,6 +326,8 @@ func extractJSONObject(content string) string {
func normalizeDecisionContent(result *GenerationResult) {
if !result.Meaningful {
result.Stories = nil
result.DailyEvents = nil
result.StoryWorthy = false
result.Summary = ""
result.OpenTopic = ""
@@ -279,6 +338,7 @@ func normalizeDecisionContent(result *GenerationResult) {
return
}
if !result.StoryWorthy {
result.Stories = nil
result.StoryTitle = ""
result.StoryContent = ""
result.FactContent = ""
@@ -306,19 +366,7 @@ func validGeneratedContent(result GenerationResult) bool {
result.Confidence >= 0 && result.Confidence <= 1
}
const contentValuePrompt = `你是长者聊天内容筛选与整理助手。只依据输入对话判断和整理,不补充未出现的事实,不把推断写成事实。
先做两级判断:
1. meaningful:对话是否包含值得保留的信息。只有问候、寒暄、嗯啊、测试设备、重复句、零散且无法理解的短句,必须判为false并舍弃,不生成摘要。
2. story_worthy:仅在meaningful为true时判断是否值得形成故事。具体的个人经历或回忆,且人物、时间、地点、事件、行动、感受中至少出现两类,或形成了清楚的事件脉络,才判为true。普通偏好、单个事实或简短近况可以总结,但story_worthy应为false。
必须只返回JSON对象,字段为meaningful、story_worthy、decision_reason、summary、keywords、open_topic、story_title、story_content、fact_content、confidence。
- decision_reason用一句简短中文说明判断依据。
- meaningful=false时,除decision_reason和confidence外,其余文本必须为空字符串,keywords必须为空数组,story_worthy必须为false。
- meaningful=true时,summary概括有意义内容,keywords为1至8个简短关键词。只有存在明确、自然且尚未聊完的线索时,open_topic才给出一个温和的后续方向;没有合适线索时必须为空字符串,不要为了填字段编造问题。
- story_worthy=false时,story_title、story_content、fact_content必须为空字符串。
- story_worthy=true时,story_title和story_content忠实整理原话,fact_content只写对话中可核对的事实。
- confidence取0到1。`
const contentValuePrompt = eventExtractionPrompt
func normalizedKeywords(values []string) []string {
result := make([]string, 0, min(len(values), 8))