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

- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避)
- 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
AskaEth committed 2026-10-04 13:33:06 +08:00
1 parent 61cf80d5ee
commit 093f38d986
28 files changed
+1089 -1089

No files matched your search

+34 -34
View File
@@ -11,25 +11,25 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// This file holds the production LightJudge used by the participation engine
// (design/18 §3). It is deliberately separate from flowgate's Reviewer: the Reviewer
// prunes queued messages from a stack; this judge decides, once, at a cost-controlled
// entrance gate, whether a triggered group/stranger message is worth participating in.
// It only ever decides 值不值得回 — never who the sender is (a project red line).
// 本文件承载参与度引擎使用的生产版 LightJudge
// (design/18 §3)。它刻意与 flowgate 的 Reviewer 分开:Reviewer
// 从堆栈中裁剪队列消息;本判定器则在受成本控制的
// 入口闸门处一次性判断被触发的群聊/陌生人消息是否值得参与。
// 它只判断值不值得回 —— 绝不判断发送者是谁(项目红线)。
const (
// engageMaxTokens bounds the judge output so a noisy group context never produces a
// giant completion (design/18 §3 cost guard).
// engageMaxTokens 限定判定器输出,使冗长的群上下文绝不会产出
// 超长补全(design/18 §3 成本护栏)。
engageMaxTokens = 200
// engageSnippetLen is how many bytes of each recent message the context renderer
// shows the judge — enough to weigh the topic without paging the whole group.
// engageSnippetLen 是上下文渲染器展示给判定器的每条近期消息
// 字节数——足以权衡话题,又不必翻完整个群。
engageSnippetLen = 80
)
// NewLLMLightJudge wraps a model.Adapter into a LightJudge (design/18 §3). It asks the
// LLM, via a single cheap turn, whether the triggered message is worth participating
// in, and routes the structured answer back to the engage engine. A nil adapter returns
// a judge that always fails closed (so the engine stays silent rather than panicking).
// NewLLMLightJudge 把 model.Adapter 包装成 LightJudge(design/18 §3)。它用一次
// 廉价单轮问 LLM 被触发的消息是否值得参与,
// 并把结构化答案回送给参与度引擎。适配器为 nil 时返回的
// 判定器总是失败关闭(引擎保持静默,而不是 panic)。
func NewLLMLightJudge(llm model.Adapter, log *slog.Logger) LightJudge {
if log == nil {
log = slog.Default()
@@ -42,10 +42,10 @@ type llmLightJudge struct {
log *slog.Logger
}
// Judge performs one cost-controlled light-turn on a triggered participation message
// and returns the structured verdict. Any error (upstream, parse) is returned so the
// engage engine fails closed to silence. It never reads sender identity — name is
// passed only as a label, identity was already resolved structurally.
// Judge 对被触发的参与度消息执行一次受成本控制的轻量单轮,
// 并返回结构化裁决。任何错误(上游、解析)都会返回,
// 使参与度引擎失败关闭为静默。它绝不读取发送者身份——name
// 只作为标签传入,身份早已由结构解析完毕。
func (j *llmLightJudge) Judge(ctx context.Context, in LightInput) (LightDecision, error) {
if j.llm == nil {
return LightDecision{}, errors.New("channelbridge: light judge: nil model adapter")
@@ -66,15 +66,15 @@ func (j *llmLightJudge) Judge(ctx context.Context, in LightInput) (LightDecision
j.log.Warn("light judge output unparsable, fail-closed to silence", "error", err)
return LightDecision{}, err
}
// Map the participation verdict onto the legacy design/12 §6 Respond field so the
// same decision drives both the engage engine (WorthReply/Interesting) and any
// legacy consumer that only reads Respond.
// 把参与度裁决映射到旧的 design/12 §6 Respond 字段,使同一
// 判定既驱动参与度引擎(WorthReply/Interesting),也驱动任何
// 只读 Respond 的旧消费者。
d.Respond = d.WorthReply || d.Interesting
return d, nil
}
// engageJudgeSystemPrompt instructs the judge to weigh only participation value, never
// identity (design/18 §0 red line), and to answer with a tight JSON object.
// engageJudgeSystemPrompt 指示判定器只权衡参与价值,绝不涉及
// 身份(design/18 §0 红线),并以紧凑的 JSON 对象作答。
const engageJudgeSystemPrompt = "你是群聊参与度轻思考器。只判断「她值不值得参与这条群聊消息」," +
"绝不判断消息发送者是谁(谁说的已由系统元数据决定)。" +
"只输出一个 JSON 对象,字段为:" +
@@ -84,11 +84,11 @@ const engageJudgeSystemPrompt = "你是群聊参与度轻思考器。只判断
"\"reason\": string(一句话理由)}。" +
"不要输出其它文字。"
// buildEngageUserContent renders the persona prompt, the recent group context and the
// triggering message for the judge. The persona block carries no owner secret; the
// context is bounded by the engage renderer. When Engage is false (a legacy design/12
// §6 call) only the message and sender label are present, so the judge degrades to a
// plain value judgement.
// buildEngageUserContent 为判定器渲染角色提示词、近期群上下文与
// 触发消息。角色块不含主人敏感信息;
// 上下文由参与度渲染器限定长度。Engage 为 false(旧的 design/12
// §6 调用)时只有消息与发送者标签,判定器因此退化为
// 朴素的价值判断。
func buildEngageUserContent(in LightInput) string {
var b strings.Builder
if in.Engage && in.PersonaPrompt != "" {
@@ -109,7 +109,7 @@ func buildEngageUserContent(in LightInput) string {
return b.String()
}
// engageDecisionJSON mirrors the judge's structured verdict for decoding.
// engageDecisionJSON 镜像判定器的结构化裁决,便于解码。
type engageDecisionJSON struct {
Interested bool `json:"interested"`
WorthReply bool `json:"worthReply"`
@@ -117,8 +117,8 @@ type engageDecisionJSON struct {
Reason string `json:"reason"`
}
// parseEngageDecision decodes the judge's JSON object, tolerating a fenced/code-block
// wrapper. A malformed answer returns an error so the caller fails closed to silence.
// parseEngageDecision 解码判定器的 JSON 对象,容忍被围栏/代码块
// 包裹。答案畸形时返回错误,使调用方失败关闭为静默。
func parseEngageDecision(s string) (LightDecision, error) {
trimmed := strings.TrimSpace(s)
if strings.HasPrefix(trimmed, "```") {
@@ -128,8 +128,8 @@ func parseEngageDecision(s string) (LightDecision, error) {
}
trimmed = strings.TrimSuffix(strings.TrimSpace(trimmed), "```")
}
// The model may wrap the object with explanatory prose; fall back to the first
// balanced JSON object if the whole payload is not pure JSON.
// 模型可能用解释性文字包裹对象;若整个载荷不是纯 JSON,
// 则回退到第一个配对完整的 JSON 对象。
var raw engageDecisionJSON
if err := json.Unmarshal([]byte(trimmed), &raw); err != nil {
if obj := firstJSONObject(trimmed); obj != "" {
@@ -151,8 +151,8 @@ func toEngageDecision(raw engageDecisionJSON) LightDecision {
}
}
// firstJSONObject returns the first balanced {...} region of s, or "" when none exists.
// It is a cheap tolerance for a model that prepends/appends prose to the JSON answer.
// firstJSONObject 返回 s 中第一个配对完整的 {...} 区域,没有则返回 ""。
// 这是对在 JSON 答案前后加文字的模型的廉价容忍。
func firstJSONObject(s string) string {
start := strings.IndexByte(s, '{')
if start < 0 {