docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
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"}}
|
||||
|
||||
Reference in new issue
Block a user