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

@@ -16,8 +16,8 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/webhook"
)
// fakeAdapter is a minimal PlatformAdapter that records outbound sends so the
// bridge's reply path can be asserted without network I/O.
// fakeAdapter 是最小的 PlatformAdapter,记录出站发送,
// 使桥接器的回复路径无需网络 I/O 即可断言。
type fakeAdapter struct {
name string
connected bool
@@ -58,7 +58,7 @@ func (f *fakeAdapter) SetMessageHandler(fn adapter.MessageHandler)
func (f *fakeAdapter) Capabilities() adapter.Capabilities { return adapter.DefaultCapabilities() }
func (f *fakeAdapter) ListChannels(context.Context) ([]adapter.ChannelInfo, error) { return nil, nil }
// stubChatter records the session id and messages passed to the agent loop.
// stubChatter 记录传给 agent 循环的会话 id 与消息。
type stubChatter struct {
sessionID string
messages []model.Message
@@ -72,7 +72,7 @@ func (s *stubChatter) Run(_ context.Context, sessionID string, msgs []model.Mess
return s.res, s.err
}
// stubSessions records the inbound message and returns a fixed session.
// stubSessions 记录入站消息并返回固定会话。
type stubSessions struct {
msg adapter.UnifiedMessage
owner *user.OwnerProfile
@@ -133,13 +133,13 @@ func TestProcessRunsPipelineAndReplies(t *testing.T) {
if sess.msg.SenderID != "100" {
t.Errorf("resolved sender = %q, want 100", sess.msg.SenderID)
}
// An owner private chat routes into the single main web session (design/16 §4.4).
// 主人私聊被路由进唯一的 Web 主会话(design/16 §4.4)。
if chat.sessionID != history.MainSessionID {
t.Errorf("loop session id = %q, want %q", chat.sessionID, history.MainSessionID)
}
// Owner private on an IM platform is mirrored to the main session. The origin is
// carried as a leading system context line and the user text stays undecorated
// (design/16 §4.6.1): metadata never goes into the user's free-text channel.
// IM 平台上的主人私聊会镜像到主会话。来源由
// 最前面的一行系统上下文承载,用户文本保持不加装饰
// (design/16 §4.6.1):元数据绝不进入用户的自由文本通道。
var sys, userText string
for _, m := range chat.messages {
if m.Role == model.RoleSystem {
@@ -204,8 +204,8 @@ func TestProcessGroupMessageKeepsGroupTarget(t *testing.T) {
t.Parallel()
b, chat, _, ad := newTestBridge(t)
// A non-owner group message that is addressed (mentioned) gets a response under
// design/12 §6 策略二, so it still exercises group reply targeting.
// 被点名(提及)的非主人群消息按 design/12 §6 策略二
// 会得到回复,因此它仍能覆盖群回复目标选择。
msg := adapter.UnifiedMessage{
Platform: "telegram",
ChannelID: "-20",
@@ -244,8 +244,8 @@ func TestProcessEmptyReplySendsNothing(t *testing.T) {
}
}
// captureHistory records every Append for asserting the bridge writes owner-private
// messages into the main web session (design/16 §4.4).
// captureHistory 记录每次 Append,用于断言桥接器把主人私聊
// 消息写入 Web 主会话(design/16 §4.4)。
type captureHistory struct {
mu sync.Mutex
appends []history.Message
@@ -321,19 +321,19 @@ func TestOwnerPrivatePersistsToMainSessionWithPlatform(t *testing.T) {
if err != nil {
t.Fatalf("New: %v", err)
}
// Register the account adapter that pretends to receive the message, so the
// persisted stream carries the adapter registration name (e.g. "telegram").
// 注册假装接收该消息的账号适配器,使持久化的
// 消息流带上适配器注册名(例如 "telegram")。
b.Add(&fakeAdapter{name: "telegram"})
msg := directMessage() // Platform = "telegram"
msg := directMessage() // Platform 字段 = "telegram"
b.process(context.Background(), msg)
recs := hist.records()
if len(recs) != 2 {
t.Fatalf("appends = %d, want 2 (user + assistant)", len(recs))
}
// Both land on the main web session and carry the channel platform + adapter,
// and IM messages are always persisted in chat mode (design/16 §5.1).
// 两者都落在 Web 主会话并带渠道平台 + 适配器,
// 且 IM 消息总是以 chat 模式持久化(design/16 §5.1)。
if recs[0].SessionID != history.MainSessionID || recs[0].Role != history.RoleUser ||
recs[0].Content != "hello" || recs[0].Platform != "telegram" || recs[0].AdapterID != "telegram" ||
recs[0].Mode != "chat" {
@@ -344,11 +344,11 @@ func TestOwnerPrivatePersistsToMainSessionWithPlatform(t *testing.T) {
recs[1].Mode != "chat" {
t.Fatalf("assistant record = %+v, want web/telegram/微信回复/telegram/chat", recs[1])
}
// Every persisted message was handed to the broadcast hook.
// 每条已持久化的消息都交给了广播钩子。
if len(persisted) != 2 {
t.Fatalf("persisted notifications = %d, want 2", len(persisted))
}
// And the loop ran against the main session, not the isolated sess-1.
// 且循环针对主会话运行,而不是隔离的 sess-1。
if chat.sessionID != history.MainSessionID {
t.Fatalf("loop session id = %q, want %q", chat.sessionID, history.MainSessionID)
}
@@ -368,16 +368,16 @@ func TestOwnerPrivatePersistsToMainWithDeviceID(t *testing.T) {
t.Fatalf("New: %v", err)
}
// directMessage() → Platform telegram / ChannelID 100 / SenderID 100, so the
// deviceID is built as <platform>:<channelID>:<senderID> (design/16 §4.5).
// directMessage() → Platform telegram / ChannelID 100 / SenderID 100,因此
// deviceID 构建为 <platform>:<channelID>:<senderID>(design/16 §4.5)。
b.process(context.Background(), directMessage())
recs := hist.records()
if len(recs) != 2 {
t.Fatalf("appends = %d, want 2 (user + assistant)", len(recs))
}
// Both the user turn and the assistant reply carry the derived deviceID and the
// channel platform, so the Web client can label the source end.
// 用户轮次与助手回复都带上推导出的 deviceID 与
// 渠道平台,使 Web 客户端能标注来源端。
for i, rec := range recs {
if rec.DeviceID != "telegram:100:100" {
t.Fatalf("record[%d] DeviceID = %q, want telegram:100:100", i, rec.DeviceID)
@@ -409,8 +409,8 @@ func TestGroupMessageNotPersistedToMainSession(t *testing.T) {
t.Fatalf("New: %v", err)
}
// Addressed (mentioned) group message → responds (design/12 §6 策略二), still
// isolated to the group session.
// 被点名(提及)的群消息 → 回复(design/12 §6 策略二),仍
// 隔离在群会话内。
msg := adapter.UnifiedMessage{
Platform: "telegram", ChannelID: "-20", ChannelType: adapter.ChannelGroup,
GroupID: "-20", GroupName: "Gang", SenderID: "9",
@@ -419,10 +419,10 @@ func TestGroupMessageNotPersistedToMainSession(t *testing.T) {
}
b.process(context.Background(), msg)
// design/16 §4.6: a group/stranger conversation stays isolated — it is NOT
// persisted into the main web session, and the loop runs against the isolated
// session id. Messages land only in the isolated session's own platform log
// (the adapter log page), so nothing reaches the Web main session.
// design/16 §4.6:群聊/陌生人会话保持隔离——它不会
// 持久化进 Web 主会话,循环也针对隔离的
// 会话 id 运行。消息只落在隔离会话自有的平台日志
// (适配器日志页),因此没有任何内容到达 Web 主会话。
recs := hist.records()
if len(recs) != 2 {
t.Fatalf("group appends = %d, want 2 (isolated user + assistant)", len(recs))
@@ -434,7 +434,7 @@ func TestGroupMessageNotPersistedToMainSession(t *testing.T) {
if rec.Platform != "telegram" {
t.Fatalf("record[%d] Platform = %q, want telegram", i, rec.Platform)
}
// Isolated IM messages are still chat-mode (design/16 §5.1).
// 隔离的 IM 消息仍为 chat 模式(design/16 §5.1)。
if rec.Mode != "chat" {
t.Fatalf("record[%d] Mode = %q, want chat", i, rec.Mode)
}
@@ -442,10 +442,10 @@ func TestGroupMessageNotPersistedToMainSession(t *testing.T) {
if chat.sessionID != "sess-g" {
t.Fatalf("group loop session id = %q, want sess-g", chat.sessionID)
}
// Each isolated write is ALSO handed to the broadcast hook so the gateway can
// emit `channel_persisted` for the IM adapter log page — but the message itself
// stays strictly in the isolated session and never leaks to the main/`web`
// session (design/16 §4.6 §3).
// 每次隔离写入也会交给广播钩子,使 gateway 能为 IM
// 适配器日志页发出 `channel_persisted`——但消息本身
// 严格留在隔离会话中,绝不泄露到主/`web`
// 会话(design/16 §4.6 §3)。
if len(persisted) != 2 {
t.Fatalf("isolated persisted notifications = %d, want 2", len(persisted))
}
@@ -463,7 +463,7 @@ func TestBlacklistGroupMessageDroppedBeforeLoop(t *testing.T) {
t.Parallel()
chat := &stubChatter{res: &loop.Result{Text: "不该回复"}}
// A stranger's group session that would otherwise be processed.
// 一个陌生人的群会话(若不拦截本应被处理)。
sess := &stubSessions{sess: session.Session{SessionID: "sess-g", IsGroup: true, Owner: false, MemWritable: false}}
ad := &fakeAdapter{name: "telegram", cfg: map[string]any{
"bot_token": "configured",
@@ -487,8 +487,8 @@ func TestBlacklistGroupMessageDroppedBeforeLoop(t *testing.T) {
}
b.process(context.Background(), msg)
// The blacklisted group message never reaches the agent loop and never sends a
// reply; the rejection reason is surfaced for the audit trail.
// 被拉黑的群消息绝不进入 agent 循环,也绝不发送
// 回复;拒绝原因暴露给审计轨迹。
if chat.sessionID != "" || len(chat.messages) != 0 {
t.Fatalf("loop ran for blacklisted message: session=%q messages=%+v", chat.sessionID, chat.messages)
}
@@ -522,8 +522,8 @@ func TestBlacklistUserMessageDroppedBeforeLoop(t *testing.T) {
}
b.process(context.Background(), msg)
// A direct message from a user NOT in the allow whitelist is dropped; the owner
// is never affected (this is a stranger).
// 来自不在允许白名单中的用户的私聊消息被丢弃;主人
// 绝不受影响(这里是陌生人)。
if chat.sessionID != "" || len(chat.messages) != 0 {
t.Fatalf("loop ran for non-whitelisted user: session=%q messages=%+v", chat.sessionID, chat.messages)
}
@@ -543,8 +543,8 @@ func TestGroupMessageThirdPartySenderNameAndBoundary(t *testing.T) {
t.Fatalf("New: %v", err)
}
// Addressed (mentioned) group message → responds (design/12 §6 策略二), so the
// third-party boundary and the [第三方 · 张三] prefix are injected.
// 被点名(提及)的群消息 → 回复(design/12 §6 策略二),因此
// 会注入第三方边界与 [第三方 · 张三] 前缀。
msg := adapter.UnifiedMessage{
Platform: "telegram", ChannelID: "-20", ChannelType: adapter.ChannelGroup,
GroupID: "-20", GroupName: "Gang", SenderID: "9", SenderName: "张三",
@@ -553,9 +553,9 @@ func TestGroupMessageThirdPartySenderNameAndBoundary(t *testing.T) {
}
b.process(context.Background(), msg)
// The loop sees a leading third-party boundary system message plus the user
// message already prefixed with [第三方 · 张三] (the raw content is preserved in
// history, so no source data is lost).
// 循环看到最前面有一条第三方边界系统消息,加上已带
// [第三方 · 张三] 前缀的用户消息(历史中保留原始
// 内容,因此不丢失来源数据)。
if len(chat.messages) != 2 {
t.Fatalf("loop messages = %d, want 2 (boundary + user)", len(chat.messages))
}
@@ -563,13 +563,13 @@ func TestGroupMessageThirdPartySenderNameAndBoundary(t *testing.T) {
t.Fatalf("expected third-party boundary system message, got %+v", chat.messages[0])
}
user := chat.messages[1]
// The third-party speaker is a structured SenderName field and the user text stays
// undecorated (design/16 §4.6.1); the origin/boundary lives in the system message.
// 第三方说话者由结构化的 SenderName 字段承载,用户文本保持
// 不加装饰(design/16 §4.6.1);来源/边界放在系统消息里。
if user.SenderName != "张三" || user.Content[0].Text != "hello all" {
t.Fatalf("loop user message = %+v, want SenderName 张三 with undecorated text", user)
}
// History keeps the RAW content and carries the sender name so the adapter log
// page can label the stream without altering the source message.
// 历史保留原始内容并携带发送者名,使适配器日志
// 页能标注消息流而不改动来源消息。
recs := hist.records()
if len(recs) != 2 || recs[0].Content != "hello all" || recs[0].SenderName != "张三" {
t.Fatalf("isolated records = %+v, want raw content with SenderName 张三", recs)
@@ -580,8 +580,8 @@ func TestOwnerInGroupIsIsolatedButNotLabeledThirdParty(t *testing.T) {
t.Parallel()
chat := &stubChatter{res: &loop.Result{Text: "收到"}}
// Owner speaking inside a group: the session stays restricted (mem read-only),
// but the owner is never labeled as a third party (design/16 §4.6 §1-2).
// 主人在群里发言:会话仍是受限的(记忆只读),
// 但主人绝不会被标为第三方(design/16 §4.6 §1-2)。
sess := &stubSessions{sess: session.Session{SessionID: "sess-g", IsGroup: true, Owner: true, MemWritable: false}}
hist := &captureHistory{}
b, err := New(Options{Loop: chat, Sessions: sess, Owner: testOwnerProfile("telegram", "owner_uid"), History: hist})
@@ -597,13 +597,13 @@ func TestOwnerInGroupIsIsolatedButNotLabeledThirdParty(t *testing.T) {
}
b.process(context.Background(), msg)
// Restricted context → a third-party boundary system message is still injected.
// 受限上下文 → 仍会注入第三方边界系统消息。
if len(chat.messages) != 2 || chat.messages[0].Role != model.RoleSystem {
t.Fatalf("loop messages = %d, want 2 (boundary + user)", len(chat.messages))
}
// Owner's message is NOT labeled as third-party and carries no SenderName; the user
// text stays undecorated and the group origin lives in the system message
// (design/16 §4.6.1).
// 主人的消息不会被标为第三方,也不携带 SenderName;用户
// 文本保持不加装饰,群的来源放在系统消息里
// (design/16 §4.6.1)。
user := chat.messages[1]
if user.SenderName != "" || user.Content[0].Text != "我在群里" {
t.Fatalf("owner group message = %+v, want undecorated text, no SenderName, no [第三方] prefix", user)
@@ -611,7 +611,7 @@ func TestOwnerInGroupIsIsolatedButNotLabeledThirdParty(t *testing.T) {
if !strings.Contains(chat.messages[0].Content[0].Text, "群#-20") {
t.Fatalf("origin/boundary system = %q, want group#-20", chat.messages[0].Content[0].Text)
}
// Owner group speech still lands in the isolated session (not the main web session).
// 主人在群里的发言仍落在隔离会话(不是 Web 主会话)。
recs := hist.records()
if len(recs) != 2 || recs[0].SessionID != "sess-g" {
t.Fatalf("owner group records = %+v, want isolated sess-g, not web", recs)
@@ -620,7 +620,7 @@ func TestOwnerInGroupIsIsolatedButNotLabeledThirdParty(t *testing.T) {
func TestNoHistoryStoreKeepsBridgeUnchanged(t *testing.T) {
t.Parallel()
// A bridge built without a history store persists nothing but still runs/replies.
// 未装配历史存储的桥接器不持久化任何内容,但仍运行/回复。
b, chat, _, ad := newTestBridge(t)
b.process(context.Background(), directMessage())
if len(ad.sends) != 1 {
@@ -631,8 +631,8 @@ func TestNoHistoryStoreKeepsBridgeUnchanged(t *testing.T) {
}
}
// newWebhookBridge builds a bare bridge (no adapters) wired with the stubs the
// other tests use, so a WebhookHandlers test can add adapters explicitly.
// newWebhookBridge 构建一个裸桥接器(无适配器),接上其他测试
// 使用的桩,便于 WebhookHandlers 测试自行添加适配器。
func newWebhookBridge(t *testing.T) *Bridge {
t.Helper()
chat := &stubChatter{res: &loop.Result{Text: "hi"}}