docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
This commit is contained in:
1 parent
f5cf117482
commit
3dc1232bc2
54 files changed
+1923
-1923
No files matched your search
@@ -1,31 +1,31 @@
|
||||
// Package backthink implements the M8 autonomous background-think service
|
||||
// (DevDocs/design/12-后台思考与主动.md §0-§3).
|
||||
// backthink 包实现 M8 自主后台思考服务
|
||||
// (DevDocs/design/12-后台思考与主动.md §0-§3)。
|
||||
//
|
||||
// It runs unattended thinking passes on two triggers:
|
||||
// 它在两类触发下运行无人值守的思考轮:
|
||||
//
|
||||
// - A periodic heartbeat (config.Background.IntervalSec) that asks the injected
|
||||
// runtime to reflect and is allowed to call tools (IoT, calendar, reminders)
|
||||
// but must not emit chat-style messages by default.
|
||||
// - Asynchronous events on the in-process bus (iot.device_update,
|
||||
// reminder.trigger, system.event) that wake a thinking pass.
|
||||
// - 周期性心跳(config.Background.IntervalSec),请求注入的运行时
|
||||
// 反思,并允许调用工具(IoT、日历、提醒),
|
||||
// 但默认不得发出 chat 风格的消息。
|
||||
// - 进程内总线上的异步事件(iot.device_update、
|
||||
// reminder.trigger、system.event),它们唤醒一轮思考。
|
||||
//
|
||||
// Each pass produces a conclusion whose structured tail decides whether she
|
||||
// wants to speak at all (design/20 §2 P2), then consults Decide — the importance
|
||||
// cascade restored as the production decision path — to judge whether now is an
|
||||
// acceptable moment (event importance / time-of-day / user state such as heart
|
||||
// rate, sleep, work, busy). A critical safety event punches through work/busy
|
||||
// and quiet hours (裁决 07.4.2), and a heartbeat pass is never held by a
|
||||
// long-silent activity gap (裁决 07.4.3). When it is worth interrupting the
|
||||
// push goes to the active client first and falls back to the IM bridge. The
|
||||
// proactive-push rate gate of DevDocs/design/11 §6 governs the periodic heartbeat
|
||||
// only; an event-driven push bypasses it (owner mandate: "该发就发") subject to
|
||||
// same-event dedup (see event_dedup.go). Every pass is persisted to SQLite via
|
||||
// Store so the M9/M10 monitor panel can render the background-thinking page
|
||||
// (design/12 §4).
|
||||
// 每轮产出一段结论,其结构化尾段决定她
|
||||
// 到底想不想说话(design/20 §2 P2),随后咨询 Decide——
|
||||
// 作为生产决策路径恢复的重要度级联——来判断此刻是否
|
||||
// 合适(事件重要度 / 时段 / 用户状态,如心率、
|
||||
// 睡眠、工作、忙碌)。critical 安全事件穿透工作/忙碌
|
||||
// 与安静时段(裁决 07.4.2),而心跳轮永远不会被长期
|
||||
// 静默的活动间隔拦下(裁决 07.4.3)。当值得打扰时,
|
||||
// 推送先走活跃客户端,再降级到 IM 桥。
|
||||
// DevDocs/design/11 §6 的主动推送频率门只约束
|
||||
// 周期性心跳;事件驱动的推送绕过它(主人要求:"该发就发"),
|
||||
// 但受同一事件去重约束(见 event_dedup.go)。每轮都经
|
||||
// Store 持久化到 SQLite,好让 M9/M10 监控面板能渲染后台思考页
|
||||
// (design/12 §4)。
|
||||
//
|
||||
// The package is concurrency-safe: a mutex guards the lifecycle state, the
|
||||
// injected store/pusher/thinker are required to be concurrency-safe by contract,
|
||||
// and the internal publish path never waits on a slow consumer.
|
||||
// 本包并发安全:一把互斥锁守护生命周期状态,
|
||||
// 注入的 store/pusher/thinker 按契约要求并发安全,
|
||||
// 且内部发布路径绝不会等待慢消费者。
|
||||
package backthink
|
||||
|
||||
import (
|
||||
@@ -43,68 +43,68 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
)
|
||||
|
||||
// Sentinel errors returned by this package. Use errors.Is to test for them; they
|
||||
// are returned directly or wrapped with %w so the underlying cause stays intact.
|
||||
// 本包返回的哨兵错误。用 errors.Is 判定;它们被直接
|
||||
// 返回或用 %w 包装,因此底层原因保持完整。
|
||||
var (
|
||||
// ErrNilDB is returned when Backthink or Store is built with a nil database.
|
||||
// 用 nil 数据库构建 Backthink 或 Store 时返回 ErrNilDB。
|
||||
ErrNilDB = errors.New("backthink: nil database handle")
|
||||
// ErrNoThinker is returned when New is given no Thinker.
|
||||
// New 未收到 Thinker 时返回 ErrNoThinker。
|
||||
ErrNoThinker = errors.New("backthink: nil thinker")
|
||||
// ErrNoLoop is returned by LoopThinker when it wraps a nil loop.
|
||||
// LoopThinker 包装了 nil loop 时由它返回 ErrNoLoop。
|
||||
ErrNoLoop = errors.New("backthink: nil loop")
|
||||
)
|
||||
|
||||
// Thinker runs one unattended thinking pass over the injected runtime. It is the
|
||||
// seam between the service and the loop/agent: the production wiring hands back a
|
||||
// LoopThinker over a *loop.Loop, while tests inject a mock. Implementations may
|
||||
// call tools (IoT, calendar, reminders) and must be concurrency-safe.
|
||||
// Thinker 在注入的运行时上跑一轮无人值守的思考。它是
|
||||
// 服务与 loop/agent 之间的接缝:生产接线交回一个基于
|
||||
// *loop.Loop 的 LoopThinker,而测试注入 mock。实现可以
|
||||
// 调用工具(IoT、日历、提醒),且必须并发安全。
|
||||
type Thinker interface {
|
||||
// Think invokes one autonomous thinking pass for the given session/trigger
|
||||
// and returns the concluded text.
|
||||
// Think 为给定的会话/触发调用一轮自主思考,
|
||||
// 并返回结论文本。
|
||||
Think(ctx context.Context, sessionID, trigger string) (string, error)
|
||||
}
|
||||
|
||||
// Deps is the injected dependency set for a Backthink. Only Thinker and DB are
|
||||
// required; the rest degrade gracefully (a nil Bus disables event triggers, a nil
|
||||
// State yields a neutral user state, and a nil Pusher is built from Active/IM).
|
||||
// Deps 是注入给 Backthink 的依赖集合。只有 Thinker 与 DB 是
|
||||
// 必需的;其余优雅降级(Bus 为 nil 则关闭事件触发,State 为
|
||||
// nil 则给出中性用户状态,Pusher 为 nil 则由 Active/IM 构建)。
|
||||
type Deps struct {
|
||||
// Bus is the event-trigger source. Nil disables event-driven thinking.
|
||||
// Bus 是事件触发源。nil 则关闭事件驱动思考。
|
||||
Bus bus.Bus
|
||||
// Thinker runs one unattended thinking pass. Required.
|
||||
// Thinker 跑一轮无人值守的思考。必需。
|
||||
Thinker Thinker
|
||||
// State supplies the user's current condition for the interrupt decision.
|
||||
// Nil yields a neutral state.
|
||||
// State 提供用户当前状况,用于打扰决策。
|
||||
// nil 则给出中性状态。
|
||||
State StateProvider
|
||||
// Recent supplies the most recent main-session messages so the activity gate
|
||||
// can sense whether the owner just chatted (recent-conversation cooldown and
|
||||
// idle-window suppression). Nil disables activity sensing.
|
||||
// Recent 提供主会话最近的消息,好让活动门
|
||||
// 感知主人是否刚聊过(最近对话冷却与
|
||||
// 静默窗口抑制)。nil 则关闭活动感知。
|
||||
Recent recentProvider
|
||||
// Pusher delivers a proactive message. When nil it is built from Active (the
|
||||
// active-client sink) and IM (the IM bridge sink) plus Gate.
|
||||
// Pusher 投递主动消息。为 nil 时由 Active(活跃客户端槽)
|
||||
// 与 IM(IM 桥槽)加上 Gate 构建。
|
||||
Pusher Pusher
|
||||
// Active is the active web/native client sink, preferred for pushes.
|
||||
// Active 是活跃 Web/原生客户端槽,推送首选。
|
||||
Active ActiveClient
|
||||
// IM is the IM bridge sink, used as the fallback when no active client is up.
|
||||
// IM 是 IM 桥槽,在没有活跃客户端时用作降级。
|
||||
IM IMAdapter
|
||||
// Exporter mirrors a proactive message to the external push outlet (the
|
||||
// owner's self-hosted unified push service, then Getui). Optional: nil
|
||||
// disables the hook. It never blocks or breaks the Active/IM delivery.
|
||||
// Exporter 把主动消息镜像到外部推送出口(主人的自托管
|
||||
// 统一推送服务,其次是个推)。可选:nil 则
|
||||
// 关闭该钩子。它绝不阻塞或破坏 Active/IM 投递。
|
||||
Exporter PushExporter
|
||||
// Gate is the proactive-push rate limiter. Nil builds a default from Config.
|
||||
// Gate 是主动推送限流器。nil 则由 Config 构建默认值。
|
||||
Gate PushGate
|
||||
// DB is the SQLite handle for the think store. Required.
|
||||
// DB 是思考存储所用的 SQLite 句柄。必需。
|
||||
DB *sql.DB
|
||||
// SessionID scopes a think. Empty uses defaultSessionID.
|
||||
// SessionID 限定一次思考的作用域。为空则用 defaultSessionID。
|
||||
SessionID string
|
||||
// Now is the wall-clock source for decision/record timestamps. Nil uses
|
||||
// time.Now, which makes time-dependent behaviour injectable in tests.
|
||||
// Now 是决策/记录时间戳的墙钟来源。nil 则用
|
||||
// time.Now,从而让依赖时间的行为在测试中可注入。
|
||||
Now func() time.Time
|
||||
// Log is the base logger, tagged with source "services/backthink".
|
||||
// Log 是基础日志器,来源标记为 "services/backthink"。
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
// Backthink is the autonomous background-think service. Build it with New, start
|
||||
// the heartbeat and subscriptions with Start, and stop them with Stop.
|
||||
// Backthink 是自主后台思考服务。用 New 构建,用 Start 启动
|
||||
// 心跳与订阅,用 Stop 停止它们。
|
||||
type Backthink struct {
|
||||
cfg Config
|
||||
deps Deps
|
||||
@@ -121,17 +121,17 @@ type Backthink struct {
|
||||
subs []func()
|
||||
wg sync.WaitGroup
|
||||
|
||||
// eventMu guards the same-event dedup guard (event_dedup.go). It is separate
|
||||
// from the lifecycle lock so a dedup decision never blocks a sink swap.
|
||||
// eventMu 守护同一事件去重器(event_dedup.go)。它与
|
||||
// 生命周期锁分开,因此去重决策绝不会阻塞槽位换入。
|
||||
eventMu sync.Mutex
|
||||
lastEventKey string
|
||||
}
|
||||
|
||||
// New builds a Backthink from cfg and deps, applying defaults and validating the
|
||||
// required dependencies. It creates the SQLite think store (via Store) up front
|
||||
// and resolves the push path (an injected Pusher, or a default built from
|
||||
// Active/IM and the rate Gate). It returns ErrNilDB when DB is nil and
|
||||
// ErrNoThinker when Thinker is nil.
|
||||
// New 由 cfg 与 deps 构建 Backthink,套用默认值并校验
|
||||
// 必需依赖。它先行创建 SQLite 思考存储(经 Store)
|
||||
// 并解出推送路径(注入的 Pusher,或由
|
||||
// Active/IM 与频率 Gate 构建的默认值)。DB 为 nil 时返回 ErrNilDB,
|
||||
// Thinker 为 nil 时返回 ErrNoThinker。
|
||||
func New(cfg Config, deps Deps) (*Backthink, error) {
|
||||
if deps.Thinker == nil {
|
||||
return nil, ErrNoThinker
|
||||
@@ -147,7 +147,7 @@ func New(cfg Config, deps Deps) (*Backthink, error) {
|
||||
return nil, fmt.Errorf("backthink: new: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the gate up front so the sink setters can rebuild the pusher with it.
|
||||
// 提前解出频率门,好让槽位设置器能与它一起重建推送器。
|
||||
gate := deps.Gate
|
||||
if gate == nil {
|
||||
gate = newRateGate(cfg.PushLimit, cfg.PushWindow, cfg.PushCooldown, cfg.KindWindow, log)
|
||||
@@ -178,10 +178,10 @@ func New(cfg Config, deps Deps) (*Backthink, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins the background work: the heartbeat ticker and, when a Bus is
|
||||
// configured, the event subscriptions. It is idempotent and safe to call twice; a
|
||||
// second call is a no-op. The provided ctx (may be nil) is the parent of all
|
||||
// internal work: cancelling it stops the service exactly like Stop.
|
||||
// Start 启动后台工作:心跳定时器,以及(配置了 Bus 时)
|
||||
// 事件订阅。它是幂等的,调用两次安全;
|
||||
// 第二次调用是空操作。传入的 ctx(可为 nil)是所有
|
||||
// 内部工作的父级:取消它会像 Stop 一样停止服务。
|
||||
func (b *Backthink) Start(ctx context.Context) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -209,11 +209,11 @@ func (b *Backthink) Start(ctx context.Context) {
|
||||
"session_id", b.sessionID)
|
||||
}
|
||||
|
||||
// Stop halts the heartbeat and unsubscribes every bus subscription, then waits
|
||||
// for the in-flight thinking pass (bounded by ThinkTimeout) to drain. It is
|
||||
// idempotent; a second call returns nil immediately. When ctx is cancelled before
|
||||
// the drain completes it returns a wrapped context error rather than leaking the
|
||||
// goroutine.
|
||||
// Stop 停止心跳、退订所有总线订阅,然后等待
|
||||
// 进行中的思考轮(受 ThinkTimeout 约束)排空。它是
|
||||
// 幂等的;第二次调用立即返回 nil。若在排空完成前 ctx 已被
|
||||
// 取消,则返回包装后的 context 错误,而不是泄漏
|
||||
// goroutine。
|
||||
func (b *Backthink) Stop(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -248,22 +248,22 @@ func (b *Backthink) Stop(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// List returns the most recent thinking records for the M9/M10 monitor panel,
|
||||
// newest first. A non-positive limit selects the store default.
|
||||
// List 返回 M9/M10 监控面板用的最近思考记录,
|
||||
// 最新的在前。非正的 limit 选用存储默认值。
|
||||
func (b *Backthink) List(ctx context.Context, limit int) ([]Record, error) {
|
||||
return b.store.List(ctx, limit)
|
||||
}
|
||||
|
||||
// triggers returns the bus subjects the service watches for event-driven
|
||||
// thinking. It is a method (not a package var) so no package-level global slice
|
||||
// appears, per the coding constraint.
|
||||
// triggers 返回本服务为事件驱动思考所监听的总线主题。
|
||||
// 它是一个方法(而非包级变量),因此不会出现包级全局
|
||||
// 切片,符合编码约束。
|
||||
func (b *Backthink) triggers() []string {
|
||||
return []string{iot.DeviceUpdateSubject, SubjectReminderTrigger, SubjectSystemEvent}
|
||||
}
|
||||
|
||||
// subscribe registers one handler per trigger subject. Each subscription is
|
||||
// bound to ctx so cancelling it removes the registration; the unregister
|
||||
// functions are kept so Stop can also tear them down explicitly.
|
||||
// subscribe 为每个触发主题注册一个处理器。每个订阅都
|
||||
// 绑定到 ctx,因此取消它会移除注册;退订函数
|
||||
// 被保留下来,好让 Stop 也能显式拆除它们。
|
||||
func (b *Backthink) subscribe(ctx context.Context) {
|
||||
for _, subj := range b.triggers() {
|
||||
unsub, err := b.deps.Bus.Subscribe(ctx, subj, b.onEvent)
|
||||
@@ -277,11 +277,11 @@ func (b *Backthink) subscribe(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// onEvent is the bus handler for event-triggered thinking. It runs on the bus's
|
||||
// own worker goroutine, so it must not block on another subscriber. It first
|
||||
// applies the minimal-disturbance gate (design/12 §6 策略一): a low-value system
|
||||
// event is downgraded to a registration-only record rather than waking a full
|
||||
// thinking pass, so platform/system noise cannot flood the thinker.
|
||||
// onEvent 是事件驱动思考的总线处理器。它跑在总线自己的
|
||||
// worker goroutine 上,所以不得阻塞在另一个订阅者上。它先
|
||||
// 套用最小打扰门(design/12 §6 策略一):低价值的系统
|
||||
// 事件被降级为仅登记一条记录,而不唤醒一整轮
|
||||
// 思考,因此平台/系统噪声无法淹没思考器。
|
||||
func (b *Backthink) onEvent(ctx context.Context, e bus.Event) error {
|
||||
tr := triggerForEvent(e)
|
||||
if !b.shouldThink(tr) {
|
||||
@@ -300,11 +300,11 @@ func (b *Backthink) onEvent(ctx context.Context, e bus.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldThink gates event-triggered thinking (design/12 §6 策略一). On a public
|
||||
// platform a generic system event is low value, so only a high-importance
|
||||
// (owner-relevant) system event wakes a pass. IoT device updates and reminder
|
||||
// triggers are owner-relevant / high value and are never gated, so the productive
|
||||
// triggers stay fully functional.
|
||||
// shouldThink 为事件驱动思考把关(design/12 §6 策略一)。在公开
|
||||
// 平台上,泛化的系统事件价值低,所以只有高重要度
|
||||
// (与主人相关)的系统事件才唤醒一轮。IoT 设备更新与提醒
|
||||
// 触发与主人相关 / 价值高,从不受门控,因此这些有产出的
|
||||
// 触发保持完全可用。
|
||||
func (b *Backthink) shouldThink(tr Trigger) bool {
|
||||
if tr.Source == SubjectSystemEvent {
|
||||
return tr.Import >= ImportanceHigh
|
||||
@@ -312,11 +312,11 @@ func (b *Backthink) shouldThink(tr Trigger) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// tickLoop is the heartbeat goroutine. Per 裁决 07.4.4 (design/12 §1) it thinks
|
||||
// once immediately on process start — producing a humanized startup greeting —
|
||||
// before settling into one thinking pass per tick; it exits when ctx is
|
||||
// cancelled. The accepted side effect is that every process restart wakes one
|
||||
// immediate thinking round.
|
||||
// tickLoop 是心跳 goroutine。按裁决 07.4.4(design/12 §1),它在进程
|
||||
// 启动时立即思考一次——产出一句拟人化的启动问候——
|
||||
// 随后稳定为每 tick 一轮思考;ctx 被取消时
|
||||
// 退出。可接受的副作用是每次进程重启都会唤醒
|
||||
// 一轮即时思考。
|
||||
func (b *Backthink) tickLoop(ctx context.Context) {
|
||||
defer b.wg.Done()
|
||||
// 启动即思考:不等满一个心跳周期。已取消的上下文直接退出,不落一条失败的
|
||||
@@ -339,10 +339,10 @@ func (b *Backthink) tickLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// think runs one unattended thinking pass: invoke the runtime, read the user
|
||||
// state, decide whether to interrupt, push when warranted and persist the record.
|
||||
// It is concurrency-safe provided the injected Thinker, StateProvider and Pusher
|
||||
// are concurrency-safe, which they are by contract.
|
||||
// think 跑一轮无人值守的思考:调用运行时、读取用户
|
||||
// 状态、判断是否打扰、值得时推送并持久化记录。
|
||||
// 只要注入的 Thinker、StateProvider 与 Pusher
|
||||
// 并发安全,它就是并发安全的——按契约它们都是。
|
||||
func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
at := b.now()
|
||||
cctx, cancel := context.WithTimeout(parent, b.cfg.ThinkTimeout)
|
||||
@@ -363,9 +363,9 @@ func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
return
|
||||
}
|
||||
|
||||
// P2 (design/20 §2): her own conclusion decides whether she wants to speak
|
||||
// and to which channel. parseProactive recovers the decision robustly and
|
||||
// fails safe (malformed/ambiguous → no push).
|
||||
// P2(design/20 §2):她自己的结论决定她想不想说、
|
||||
// 以及发到哪个渠道。parseProactive 稳健地恢复该决策,
|
||||
// 并失败安全(格式错乱/含糊 → 不推送)。
|
||||
message, channelHint, push := parseProactive(conclusion)
|
||||
|
||||
state := b.currentState(cctx)
|
||||
@@ -393,9 +393,9 @@ func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
|
||||
pushRes := PushResult{Route: RouteNone, Detail: decision.Reason}
|
||||
if shouldPush {
|
||||
// Same-event dedup: a repeating identical event is suppressed after the
|
||||
// first push (防刷), while a genuinely new event still goes out. Heartbeats
|
||||
// are never deduped (they reset the chain) and stay gate-governed.
|
||||
// 同一事件去重:重复的相同事件在首次推送后被抑制
|
||||
// (防刷),而真正的新事件仍会发出。心跳
|
||||
// 从不去重(它们会重置该链),并保持受门控管辖。
|
||||
if b.eventRedundant(tr) {
|
||||
pushRes = PushResult{Route: RouteNone, Detail: "duplicate event push suppressed"}
|
||||
} else {
|
||||
@@ -422,8 +422,8 @@ func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
}
|
||||
}
|
||||
|
||||
// currentState reads the user's current condition for the interrupt decision,
|
||||
// falling back to a neutral state (and a warn log) when the provider errors.
|
||||
// currentState 读取用户当前状况用于打扰决策,
|
||||
// 提供者报错时退回中性状态(并记一条 warn 日志)。
|
||||
func (b *Backthink) currentState(ctx context.Context) UserState {
|
||||
if b.deps.State == nil {
|
||||
return UserState{}
|
||||
@@ -436,12 +436,12 @@ func (b *Backthink) currentState(ctx context.Context) UserState {
|
||||
return s
|
||||
}
|
||||
|
||||
// lastUserActivity reports the wall-clock time of the owner's most recent message
|
||||
// in the main session, and whether any user message was found. It is the activity
|
||||
// signal for the activity gate (design/12 §6): a nil provider, a failed read or an
|
||||
// empty tail yield has=false so the activity gate falls through to the current
|
||||
// behaviour rather than guessing. The most recent user message is the newest
|
||||
// RoleUser entry in the recent tail (Recent returns oldest-first).
|
||||
// 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
|
||||
@@ -458,14 +458,14 @@ func (b *Backthink) lastUserActivity(ctx context.Context) (time.Time, bool) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// record persists a thinking pass. It uses a background context so an audit row
|
||||
// is written even when the think's own context has already been cancelled.
|
||||
// record 持久化一轮思考。它使用后台 context,因此即便
|
||||
// 该次思考自身的 context 已被取消,审计行仍会写入。
|
||||
func (b *Backthink) record(r Record) {
|
||||
if _, err := b.store.Append(context.Background(), r); err != nil {
|
||||
b.log.Error("backthink record failed", "error", err)
|
||||
}
|
||||
// Surface the persisted pass on the WS stream so the frontend's backthink
|
||||
// consumer (or its default fallback) observes it without polling the store.
|
||||
// 把持久化的这一轮暴露到 WS 流上,好让前端的 backthink
|
||||
// 消费者(或其默认兜底)无需轮询存储就能观测到它。
|
||||
if b.deps.Bus != nil {
|
||||
if err := b.deps.Bus.Publish(context.Background(), "ws.backthink", r); err != nil {
|
||||
b.log.Warn("backthink bus publish failed", "error", err)
|
||||
|
||||
Reference in new issue
Block a user