package backthink import ( "context" "fmt" "log/slog" "strconv" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" ) // StateProvider 为打扰决策提供用户当前状况 // (design/12 §2)。生产接线 IoT/遥测与日历;测试注入 // mock。它必须并发安全。 type StateProvider interface { // Current 返回用户当前的 UserState。可 nil 的错误表示「用 // 中性状态」;硬错误由调用方记日志并按 // 中性处理。 Current(ctx context.Context) (UserState, error) } // IoTStateProvider 是基于 iot.Manager 的尽力而为 StateProvider。它 // 扫描纳管设备,并从任何 id/名称/类型提到对应关键词的设备 // 读取心率与睡眠信号(例如 "heart"、 // "心率"、"sleep"、"睡眠")。工作/忙碌来自设备/状态标志;什么都不 // 匹配时它返回中性状态而不是错误,因此即便没有完整 // 遥测接线,服务也能运行。 type IoTStateProvider struct { mgr *iot.Manager log *slog.Logger } // NewIoTStateProvider 把 mgr 包装为 StateProvider。 func NewIoTStateProvider(mgr *iot.Manager, log *slog.Logger) *IoTStateProvider { return &IoTStateProvider{mgr: mgr, log: logger.WithSource(baseLog(log), "services/backthink")} } // Current 实现 StateProvider。 func (p *IoTStateProvider) Current(ctx context.Context) (UserState, error) { if p.mgr == nil { return UserState{}, fmt.Errorf("backthink: state: %w", iot.ErrProviderNotRegistered) } devices, err := p.mgr.List(ctx) if err != nil { return UserState{}, fmt.Errorf("backthink: state: list: %w", err) } var state UserState for _, d := range devices { key := strings.ToLower(d.ID + " " + d.Name + " " + d.Type) switch { case strings.Contains(key, "heart") || strings.Contains(key, "心率"): if v, ok := numState(d, "bpm", "heart_rate", "rate"); ok { state.HeartRateBPM = v } case strings.Contains(key, "sleep") || strings.Contains(key, "睡眠"): state.Sleeping = boolState(d, "sleeping", "asleep", "state") } if b, ok := boolStateOK(d, "working"); ok { state.Working = b } if b, ok := boolStateOK(d, "busy"); ok { state.Busy = b } } if p.log != nil { p.log.Debug("backthink iot state sampled", "devices", len(devices), "state", state.String()) } return state, nil } // currentState 读取用户当前状况,并回答「到底读到了没」:打扰决策用它作为中性 // 回退(已知为 false 时按零值判定),思考轮用它决定要不要注入状态块(X1 缺口 d // 让同一份快照同时服务两侧)。提供者为 nil 或读取失败都返回 Known=false,并记一条 // 带来源的 warn 日志 —— 绝不把「读不到」伪装成一份中性状态去误导她的判断。 func (b *Backthink) currentState(ctx context.Context) (UserState, bool) { if b.deps.State == nil { return UserState{}, false } s, err := b.deps.State.Current(ctx) if err != nil { b.log.Warn("backthink state provider failed", "error", err) return UserState{}, false } return s, true } // lastUserActivity 报告主人最近一条消息的墙钟时间 // (在主会话里),以及是否找到了任何用户消息。它是活动 // 门的活动信号(design/12 §6):提供者为 nil、读取失败或 // 尾部为空都得到 has=false,因此活动门会落到当前 // 行为而不是靠猜。最近的一条用户消息是最近尾部里 // 最新的 RoleUser 条目(Recent 返回的是最旧在前)。 func (b *Backthink) lastUserActivity(ctx context.Context) (time.Time, bool) { if b.deps.Recent == nil { return time.Time{}, false } msgs, err := b.deps.Recent.Recent(ctx, history.MainSessionID, recentContextCount) if err != nil { return time.Time{}, false } for i := len(msgs) - 1; i >= 0; i-- { if msgs[i].Role == history.RoleUser { return time.UnixMilli(msgs[i].CreatedAt), true } } return time.Time{}, false } // numState 从设备状态 map 中按 keys 读取首个数值。 func numState(d iot.Device, keys ...string) (int, bool) { for _, k := range keys { v, ok := d.State[k] if !ok { continue } switch n := v.(type) { case int: return n, true case int64: return int(n), true case float64: return int(n), true case string: if f, err := strconv.Atoi(n); err == nil { return f, true } } } return 0, false } // boolState 在 keys 中读取一个 bool(或 "sleep"/"asleep" 字符串)。 func boolState(d iot.Device, keys ...string) bool { b, _ := boolStateOK(d, keys...) return b } // boolStateOK 报告是否找到了 bool 类型的状态标志及其取值。 func boolStateOK(d iot.Device, keys ...string) (bool, bool) { for _, k := range keys { v, ok := d.State[k] if !ok { continue } switch n := v.(type) { case bool: return n, true case string: switch strings.ToLower(n) { case "sleep", "asleep", "busy", "working", "yes", "true": return true, true case "awake", "free", "idle", "no", "false": return false, true } } } return false, false }