docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:`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:
AskaEth committed 2026-10-04 16:27:54 +08:00
1 parent f5cf117482
commit 3dc1232bc2
54 files changed
+1923 -1923

No files matched your search

+122 -122
View File
@@ -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)