fix(channel): 静默登记同源第三方标注 —— 无昵称非主人不再被读成主人
review p1:回帖路径已改用 thirdPartySenderName(非主人恒有标注:昵称 → actor uid → 「未署名发送者」),但 registerSilenced 仍用 senderDisplayName,非主人 无昵称时写出空串;静默行会被 transcript 读回她的上下文,而受限会话里空标注就是 「主人本人」⇒ 在 senderName 可选且无兜底的渠道(webhook)上,一条没昵称的陌生人 消息被渲染成「[发言归属] 以下发言来自主人(管理员账户)本人」。 - light.go:270:senderDisplayName → thirdPartySenderName,与回帖路径同源。 senderDisplayName 未成死代码:仍被 light.go:223(LightJudge 输入)、 engage.go:284、flowgate.go:183 使用。 - 新增 TestSilencedNamelessMemberIsNotReadAsTheOwner:走真链路(静默登记 → 落库 → 下一回合播种转录 → loop 归属渲染 → provider 请求),钉住「无昵称的非主人经 静默登记后,读回上下文时不得出现主人标注」。修前必红(写入口径与模型侧两条 断言同时报,含被读成主人的原样请求),修后绿。 验证:go build ./... / go vet ./... / go test -count=1 ./...(全树)全绿。
This commit is contained in:
1 parent
8ab241f627
commit
91e4dd2fe1
2 files changed
+75
-1
No files matched your search
@@ -60,6 +60,13 @@ func (c *captureModelAdapter) lastRequest(t *testing.T) model.Request {
|
||||
return c.reqs[len(c.reqs)-1]
|
||||
}
|
||||
|
||||
// count 返回已记录的 provider 请求数(0 表示循环没被驱动)。
|
||||
func (c *captureModelAdapter) count() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.reqs)
|
||||
}
|
||||
|
||||
// groupLoopFor 构建一个群聊车道的真实循环(默认角色昔涟),使渠道侧的
|
||||
// 端到端用例看到的正是生产装配的那条装配链(loop 的归属渲染在内)。
|
||||
func groupLoopFor(t *testing.T, ad model.Adapter) *loop.Loop {
|
||||
@@ -260,6 +267,67 @@ func TestGroupTurnTellsTheModelWhoSaidWhat(t *testing.T) {
|
||||
t.Logf("模型侧实际形态:\n%s", requestDump(req))
|
||||
}
|
||||
|
||||
// ownerNotePrefix 是主人本人边界说明的开头。断言只能认这个**带标记的
|
||||
// 完整前缀**:受限会话的角色声明里也含「来自主人本人」这类字面,
|
||||
// 用「来自主人」做子串匹配会把它误当成主人说明。
|
||||
const ownerNotePrefix = "[发言归属] 以下发言来自主人(管理员账户)本人"
|
||||
|
||||
// TestSilencedNamelessMemberIsNotReadAsTheOwner 覆盖 review p1 暴露的路径盲区:
|
||||
// **静默登记**(registerSilenced:没 @ 她、也没有规则信号的群消息只登记不回)
|
||||
// 落库的发送者标注,会被之后的回合经 transcript 读回她的上下文。受限会话里
|
||||
// 空标注就是「主人本人」,因此这条路径上的无昵称非主人若写成空串,就会被渲染成
|
||||
// 「来自主人」——正是本切片修掉的那个误认的镜像(webhook 适配器的 senderName
|
||||
// 是可选参数且无兜底,可达)。
|
||||
//
|
||||
// 用例走真链路:静默登记 → 落库 → 下一回合播种转录 → loop 归属渲染 → provider 请求。
|
||||
func TestSilencedNamelessMemberIsNotReadAsTheOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ad := &captureModelAdapter{}
|
||||
// live 夹具让 Recent 连本次运行已落库的行一起返回 —— 真库就是这样。
|
||||
hist := &seededHistory{captureHistory: &captureHistory{}, live: true}
|
||||
sess := &stubSessions{sess: session.Session{SessionID: "sess-g", IsGroup: true, Owner: false, MemWritable: false}}
|
||||
b, err := New(Options{Loop: groupLoopFor(t, ad), Sessions: sess, Owner: testOwnerProfile("telegram", "100"), History: hist})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
// ① 一条既没 @ 她、也没有规则信号、且**没有昵称**的群员消息:只登记、不回。
|
||||
b.process(context.Background(), noMentionGroupMessage("8", "", "我是群里一个没昵称的人"))
|
||||
if ad.count() != 0 {
|
||||
t.Fatalf("静默消息驱动了循环(应当只登记)")
|
||||
}
|
||||
recs := hist.records()
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("静默登记行数 = %d, want 1", len(recs))
|
||||
}
|
||||
// 写入口径:非主人恒有标注 —— 空 sender_name 在模型侧就是「主人本人」。
|
||||
// 这里用 Errorf(不中断):空串既会让上面这条红,也会让下面的模型侧
|
||||
// 断言红,两条一起报出来才看得见「空串 → 被读成主人」的完整因果。
|
||||
// 这条断言在修之前会红(senderDisplayName 无昵称时返回空串)。
|
||||
if recs[0].SenderName != "8" {
|
||||
t.Errorf("静默登记的 sender_name = %q, want 8(空串会被读成主人)", recs[0].SenderName)
|
||||
}
|
||||
|
||||
// ② 之后一条 @ 她的群消息驱动回合,静默那条随转录读回上下文。
|
||||
b.process(context.Background(), namedGroupMessage("9", "张三", adapter.Actor{Role: adapter.RoleMember}))
|
||||
req := ad.lastRequest(t)
|
||||
texts := requestTextsOf(req)
|
||||
// 主断言(本用例守的就是它):静默行绝不能被渲染成主人本人的发言。
|
||||
for _, txt := range texts {
|
||||
if strings.Contains(txt, ownerNotePrefix) {
|
||||
t.Fatalf("无昵称的非主人被读成了主人:\n%s", requestDump(req))
|
||||
}
|
||||
}
|
||||
if !containsText(texts, "[发言归属] 以下发言来自「8」——本会话里的第三方人员(非管理员账户),不是主人,也不是你说的。") {
|
||||
t.Fatalf("静默的无昵称群员没有第三方标注:\n%s", requestDump(req))
|
||||
}
|
||||
if !containsText(texts, "我是群里一个没昵称的人") {
|
||||
t.Fatalf("静默那条没有出现在她的上下文里:\n%s", requestDump(req))
|
||||
}
|
||||
t.Logf("模型侧实际形态:\n%s", requestDump(req))
|
||||
}
|
||||
|
||||
// requestTextsOf 按顺序返回请求里每条消息的正文。
|
||||
func requestTextsOf(req model.Request) []string {
|
||||
out := make([]string, 0, len(req.Messages))
|
||||
|
||||
@@ -261,7 +261,13 @@ func (b *Bridge) registerSilenced(ctx context.Context, msg adapter.UnifiedMessag
|
||||
// 绝不写一条指向不存在文件的引用,也绝不写一张没进过上下文的图片。
|
||||
// turn_id 同样传 "":这条消息**根本不会被回答**,不存在「它属于哪一路回合」,
|
||||
// 给它编一个标识就是把不属于任何回合的行说成有回合(见 turnid.go 的口径)。
|
||||
b.persistIsolated(ctx, history.RoleUser, msg.Content, "", "", deviceIDFor(msg), sess, msg.Platform, senderDisplayName(msg, actor), adapterName(a), "")
|
||||
//
|
||||
// 发送者标注与回帖路径**同源**(thirdPartySenderName):非主人恒有标注
|
||||
// (昵称 → actor uid → 「未署名发送者」),主人恒空。静默行会被读回她的
|
||||
// 上下文(transcript.go),而受限会话里空标注就是「主人本人」——用
|
||||
// senderDisplayName 会在无昵称时写出空串,把一条陌生人消息渲染成
|
||||
// 「来自主人(管理员账户)本人」(review p1:本次修掉的 bug 的镜像)。
|
||||
b.persistIsolated(ctx, history.RoleUser, msg.Content, "", "", deviceIDFor(msg), sess, msg.Platform, thirdPartySenderName(msg, actor), adapterName(a), "")
|
||||
b.log.Debug("non-owner message silenced, registered only",
|
||||
"platform", msg.Platform, "channel", msg.ChannelID, "session", sess.SessionID)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user