打通前端 ws.ts 里那批死消费者事件(thinking/mood/tool_used/subagent_event/
task_output/device_update/reminder/backthink),并给 loop 接真 token 流式。
- wire.go:loop 建时传 OnStream(streamChunk 闭包把 delta 发 bus ws.stream_chunk);
gateway.Deps 注入 Scheduler/Tasks/Streaming;iot 接共享总线 iot.WithBus(a.bus)。
- ws_bridge.go:hub 额外订阅 iot.>/task.>/automation.>/reminder.>/cyrene.agent.*.event,
normalizeEvent 归一为 ws.*(device_update/task_output/task_done/automation_result/
reminder/subagent_event);不订阅 cyrene.agent.> 以免 ask/reply 内部事件进 relay。
- ws_route.go:voice_input→ws.voice_input;subagent_ask→Scheduler.AuditContext(真实
上下文审计,替换 pending stub);task_output_query→Manager.TaskOutput(真实输出,
替换空 {})。
- server.go:新增 SubagentAuditor/TaskOutputStore 接口 + Deps.Scheduler/Tasks/Streaming。
- orchestration/auditcontext.go:Scheduler.AuditContext(full/tail/keyword)。
- task/manager.go:Manager.TaskOutput(id)。
- backthink:record() 补发 ws.backthink 事件。
- json tag 修正(device_update/reminder/task output 结构体加小写 tag + DeviceUpdate
补 message),使前端分支能真正渲染。
实机验证:WS 握手 101、task manager ready、iot manager initialized(WithBus 生效)。
go build/vet/test 全绿;TestHubBridgeDeviceUpdate/TestHubBridgeSubagentProgress 覆盖
映射。dev 推至后续版本。
181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
package iot
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"log/slog"
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
)
|
||
|
||
// Manager is the IoT subsystem facade. It selects a Provider from
|
||
// configuration, serves List/Control/Status, and publishes device state
|
||
// transitions on the in-process bus as DeviceUpdate events.
|
||
type Manager struct {
|
||
providers []Provider
|
||
bus bus.Bus
|
||
log *slog.Logger
|
||
}
|
||
|
||
// Option customizes a Manager built by New.
|
||
type Option func(*Manager)
|
||
|
||
// WithBus injects an existing bus instead of letting the Manager create one. It
|
||
// mirrors the other subsystems so iot.device_update flows on the shared bus and
|
||
// reaches the gateway hub / background-trigger subscribers rather than staying
|
||
// on an isolated manager-owned bus.
|
||
func WithBus(b bus.Bus) Option {
|
||
return func(m *Manager) { m.bus = b }
|
||
}
|
||
|
||
// New builds a Manager from cfg, selecting the device Provider by cfg.Provider.
|
||
//
|
||
// Supported providers: "" or "mock" registers the built-in mock provider with
|
||
// eight simulated devices; "none" disables the subsystem (no provider is
|
||
// registered, List returns an empty slice and Control/Status fail with
|
||
// ErrDeviceNotFound). Any other value returns ErrUnknownProvider.
|
||
//
|
||
// log may be nil; it is replaced with a discard logger. The Manager owns a
|
||
// fresh, isolated bus exposed via Bus for subscribers to attach to.
|
||
func New(cfg config.IoTConfig, log *slog.Logger, opts ...Option) (*Manager, error) {
|
||
m := &Manager{
|
||
bus: bus.New(),
|
||
log: logger.WithSource(discardLogger(log), "iot/manager"),
|
||
}
|
||
for _, opt := range opts {
|
||
opt(m)
|
||
}
|
||
switch strings.TrimSpace(cfg.Provider) {
|
||
case "", "mock":
|
||
mp, err := newMockProvider(log)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
m.providers = append(m.providers, mp)
|
||
case "none":
|
||
// No provider is registered: the subsystem is disabled.
|
||
default:
|
||
return nil, fmt.Errorf("iot: new: provider %q: %w", cfg.Provider, ErrUnknownProvider)
|
||
}
|
||
m.log.Info("iot manager initialized", "provider", cfg.Provider, "providers", len(m.providers))
|
||
return m, nil
|
||
}
|
||
|
||
// Register adds p as a device source. Registering a provider whose Name already
|
||
// exists returns ErrDuplicate; a nil provider returns ErrInvalidArgument.
|
||
func (m *Manager) Register(p Provider) error {
|
||
if p == nil {
|
||
return fmt.Errorf("iot: register: %w", ErrInvalidArgument)
|
||
}
|
||
for _, existing := range m.providers {
|
||
if existing.Name() == p.Name() {
|
||
return fmt.Errorf("iot: register %q: %w", p.Name(), ErrDuplicate)
|
||
}
|
||
}
|
||
m.providers = append(m.providers, p)
|
||
m.log.Info("iot provider registered", "provider", p.Name())
|
||
return nil
|
||
}
|
||
|
||
// Bus returns the Manager's in-process event bus. Subscribers attach to
|
||
// DeviceUpdateSubject (or "iot.>") to observe device state transitions.
|
||
func (m *Manager) Bus() bus.Bus {
|
||
return m.bus
|
||
}
|
||
|
||
// List returns every device from every registered provider.
|
||
func (m *Manager) List(ctx context.Context) ([]Device, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
var out []Device
|
||
for _, p := range m.providers {
|
||
devices, err := p.List(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("iot: list %q: %w", p.Name(), err)
|
||
}
|
||
out = append(out, devices...)
|
||
}
|
||
m.log.Debug("iot devices listed", "count", len(out))
|
||
return out, nil
|
||
}
|
||
|
||
// Control applies action to the owning provider's device and, on success,
|
||
// publishes a DeviceUpdate on the bus. It returns ErrDeviceNotFound when no
|
||
// provider owns the device.
|
||
func (m *Manager) Control(ctx context.Context, deviceID, action string, params map[string]any) (*Device, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(m.providers) == 0 {
|
||
return nil, fmt.Errorf("iot: control: %w", ErrProviderNotRegistered)
|
||
}
|
||
for _, p := range m.providers {
|
||
d, err := p.Control(ctx, deviceID, action, params)
|
||
if err != nil {
|
||
if errors.Is(err, ErrDeviceNotFound) {
|
||
continue
|
||
}
|
||
return nil, err
|
||
}
|
||
m.publishUpdate(ctx, p.Name(), *d, action, params)
|
||
return d, nil
|
||
}
|
||
return nil, fmt.Errorf("iot: control %q: %w", deviceID, ErrDeviceNotFound)
|
||
}
|
||
|
||
// Status returns the current state of the device owned by the matching provider.
|
||
// It returns ErrDeviceNotFound when no provider owns the device.
|
||
func (m *Manager) Status(ctx context.Context, deviceID string) (*Device, error) {
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
if len(m.providers) == 0 {
|
||
return nil, fmt.Errorf("iot: status: %w", ErrProviderNotRegistered)
|
||
}
|
||
for _, p := range m.providers {
|
||
d, err := p.Status(ctx, deviceID)
|
||
if err != nil {
|
||
if errors.Is(err, ErrDeviceNotFound) {
|
||
continue
|
||
}
|
||
return nil, err
|
||
}
|
||
return d, nil
|
||
}
|
||
return nil, fmt.Errorf("iot: status %q: %w", deviceID, ErrDeviceNotFound)
|
||
}
|
||
|
||
// publishUpdate fans a DeviceUpdate out on the bus. A publish failure (for
|
||
// example a canceled context) is logged, not returned: the device state already
|
||
// changed, and control should not be reported as failed because an observer did
|
||
// not receive the event.
|
||
func (m *Manager) publishUpdate(ctx context.Context, provider string, d Device, action string, params map[string]any) {
|
||
msg := fmt.Sprintf("设备 %s · %s(%s)", d.Name, action, provider)
|
||
if err := m.bus.Publish(ctx, DeviceUpdateSubject, DeviceUpdate{
|
||
Provider: provider,
|
||
Device: d,
|
||
Action: action,
|
||
Params: params,
|
||
Message: msg,
|
||
}); err != nil {
|
||
m.log.Error("iot publish device update", "device", d.ID, "action", action, "error", err)
|
||
return
|
||
}
|
||
m.log.Debug("iot device update published", "device", d.ID, "action", action, "provider", provider)
|
||
}
|
||
|
||
// discardLogger returns log if non-nil, otherwise a logger that writes nowhere,
|
||
// so New always has a safe logger to use.
|
||
func discardLogger(log *slog.Logger) *slog.Logger {
|
||
if log != nil {
|
||
return log
|
||
}
|
||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
}
|