- 缺口 d:状态在**思考前**读一次,同一份快照既喂思考轮又喂 Decide(读不到 = Known=false,绝不伪装成中性状态) - 缺口 c:新增只读工具 capability_list(owner-only、三重输出边界、读写隔离),她可自查当下真实可用面 - 缺口 b:内置允许集补 transcode / capability_list(此前 CAPACITY_ENABLED=true 时新工具会从模型可见面消失) - 缺口 e:guidancePrompt 旧文案承诺的「IoT/日历/提醒」在全仓根本没有对应工具(chat 侧也没有)⇒ 按诚实化改为「与主人对话同等工具面 + 指名自查入口」,不再有空头支票 - (a) 未做且**不自创**:那三把工具无任何设计规格(名字/schema/读写边界/输出上限)⇒ 已报回并登记进 design/20(裁决:走能力/插件/MCP 暴露路线) - 变异实测:允许集去掉新工具 ⇒ TestDecide_AllowedSets + 组合根跨层测试红;丢掉状态快照 ⇒ TestLoopThinker_Think_InjectsRoundStateBlock 红 - 已知弱项(如实登记):组合根那行 wireCapabilitySurface() 调用本身无测试覆盖(与既有 wireTranscode 同惯例)
164 lines
5.1 KiB
Go
164 lines
5.1 KiB
Go
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
|
||
}
|