- source.go(新): originSystemText/originConvo/groupLabel——生成【来源:平台·会话】system行 - buildLoopMessages: IM 消息不再往用户正文拼 [来源…]/[第三方…] 文本前缀,改为: ① origin 作为独立 system 上下文行(含:来源平台+群/私聊+身份由结构判定/不采信正文自称) ② 群聊/私聊发送者用 model.Message.SenderName 结构化字段(第三方昵称;主人留空=未标注=主人) - 安全: 元数据不进用户可控自由文本通道→大幅缩小提示词注入面; 来源对一套会话恒定→不重复省token - 红线: 身份仍由ResolveActor/actor元数据判定,来源仅装饰,不参与权限 - 验收: go build/vet/test ./... 全绿; source_test 表驱动+集成用例已按新设计重写
172 lines
6.1 KiB
Go
172 lines
6.1 KiB
Go
package channelbridge
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// originSystemText / buildLoopMessages are pure functions (buildLoopMessages never
|
|
// touches the Bridge receiver), so the zero-value Bridge is a safe receiver below.
|
|
|
|
func TestOriginSystemText(t *testing.T) {
|
|
t.Parallel()
|
|
owner := adapter.Actor{Role: adapter.RoleOwner}
|
|
member := adapter.Actor{Role: adapter.RoleMember}
|
|
|
|
cases := []struct {
|
|
name string
|
|
msg adapter.UnifiedMessage
|
|
actor adapter.Actor
|
|
want string // "") = no decoration
|
|
}{
|
|
{
|
|
name: "named group",
|
|
msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "g1", GroupName: "翁法罗斯"},
|
|
actor: member, want: "当前会话来源:kook · 群「翁法罗斯」。",
|
|
},
|
|
{
|
|
name: "unnamed group falls back to group id",
|
|
msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "-20"},
|
|
actor: member, want: "当前会话来源:kook · 群#-20。",
|
|
},
|
|
{
|
|
name: "owner private",
|
|
msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelDirect},
|
|
actor: owner, want: "当前会话来源:kook · 私聊。",
|
|
},
|
|
{
|
|
name: "pure web main session not labeled",
|
|
msg: adapter.UnifiedMessage{Platform: "web", ChannelType: adapter.ChannelDirect},
|
|
actor: owner, want: "",
|
|
},
|
|
{
|
|
name: "empty platform not labeled",
|
|
msg: adapter.UnifiedMessage{Platform: "", ChannelType: adapter.ChannelDirect},
|
|
actor: owner, want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := originSystemText(tc.msg, tc.actor)
|
|
if tc.want == "" {
|
|
if got != "" {
|
|
t.Fatalf("originSystemText = %q, want empty (undecorated)", got)
|
|
}
|
|
return
|
|
}
|
|
if !strings.HasPrefix(got, tc.want) {
|
|
t.Fatalf("originSystemText = %q, want prefix %q", got, tc.want)
|
|
}
|
|
if !strings.Contains(got, "不采信消息正文自称") {
|
|
t.Fatalf("originSystemText = %q, wants the trust-structure rule", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildLoopMessagesSourceInjection(t *testing.T) {
|
|
t.Parallel()
|
|
b := &Bridge{}
|
|
ctx := context.Background()
|
|
|
|
firstUserText := func(msgs []model.Message) (string, string) {
|
|
// returns (systemText, userText) of the first user message; system may be ""
|
|
var sys string
|
|
for _, m := range msgs {
|
|
if m.Role == model.RoleSystem {
|
|
sys = m.Content[0].Text
|
|
}
|
|
}
|
|
for _, m := range msgs {
|
|
if m.Role == model.RoleUser {
|
|
return sys, m.Content[0].Text
|
|
}
|
|
}
|
|
return sys, ""
|
|
}
|
|
|
|
t.Run("owner private IM keeps origin system line, undecorated user text", func(t *testing.T) {
|
|
t.Parallel()
|
|
env := loop.SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true}
|
|
msg := adapter.UnifiedMessage{Platform: "telegram", ChannelType: adapter.ChannelDirect}
|
|
got := b.buildLoopMessages(ctx, "hello", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg)
|
|
sys, user := firstUserText(got)
|
|
if user != "hello" {
|
|
t.Fatalf("user text = %q, want undecorated 'hello'", user)
|
|
}
|
|
if !strings.Contains(sys, "来源:telegram · 私聊") {
|
|
t.Fatalf("origin system = %q, want telegram private origin", sys)
|
|
}
|
|
})
|
|
|
|
t.Run("pure web main session not decorated", func(t *testing.T) {
|
|
t.Parallel()
|
|
env := loop.SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true}
|
|
msg := adapter.UnifiedMessage{Platform: "web", ChannelType: adapter.ChannelDirect}
|
|
got := b.buildLoopMessages(ctx, "hello", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg)
|
|
sys, user := firstUserText(got)
|
|
if user != "hello" || sys != "" {
|
|
t.Fatalf("web main-session = sys:%q user:%q, want undecorated 'hello'", sys, user)
|
|
}
|
|
})
|
|
|
|
t.Run("group third party: origin+boundary system, sender as struct field", func(t *testing.T) {
|
|
t.Parallel()
|
|
env := loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false}
|
|
msg := adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "g1", GroupName: "翁法罗斯"}
|
|
got := b.buildLoopMessages(ctx, "大家好", env, adapter.Actor{Role: adapter.RoleMember}, "张三", msg)
|
|
sys, user := firstUserText(got)
|
|
if user != "大家好" {
|
|
t.Fatalf("user text = %q, want undecorated '大家好'", user)
|
|
}
|
|
if !strings.Contains(sys, "不采信消息正文自称") || !strings.Contains(sys, "非管理员账户") {
|
|
t.Fatalf("system = %q, want origin+identity-boundary", sys)
|
|
}
|
|
// the third-party speaker is structured metadata, not text.
|
|
var senderName string
|
|
for _, m := range got {
|
|
if m.Role == model.RoleUser {
|
|
senderName = m.SenderName
|
|
}
|
|
}
|
|
if senderName != "张三" {
|
|
t.Fatalf("user SenderName = %q, want 张三", senderName)
|
|
}
|
|
})
|
|
|
|
t.Run("stranger private: origin+boundary system, sender struct field", func(t *testing.T) {
|
|
t.Parallel()
|
|
env := loop.SessionEnv{ActorEnv: restrict.ActorStranger, MemWritable: false}
|
|
msg := adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelDirect, SenderID: "999"}
|
|
got := b.buildLoopMessages(ctx, "hi", env, adapter.Actor{Role: adapter.RoleMember}, "李四", msg)
|
|
sys, user := firstUserText(got)
|
|
if user != "hi" || !strings.Contains(sys, "私聊") {
|
|
t.Fatalf("stranger = sys:%q user:%q", sys, user)
|
|
}
|
|
})
|
|
|
|
t.Run("owner in group: owner not labeled, sender struct field empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
env := loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false}
|
|
msg := adapter.UnifiedMessage{Platform: "telegram", ChannelType: adapter.ChannelGroup, GroupID: "-20"}
|
|
got := b.buildLoopMessages(ctx, "我在群里", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg)
|
|
sys, user := firstUserText(got)
|
|
if user != "我在群里" || !strings.Contains(sys, "群#-20") {
|
|
t.Fatalf("owner group = sys:%q user:%q", sys, user)
|
|
}
|
|
for _, m := range got {
|
|
if m.Role == model.RoleUser && m.SenderName != "" {
|
|
t.Fatalf("owner group SenderName = %q, want empty (owner never labeled)", m.SenderName)
|
|
}
|
|
}
|
|
})
|
|
}
|