feat(engage): 通道来源上下文注入(design/16 §4.6.1)——system行+结构化字段,防注入
- source.go(新): originSystemText/originConvo/groupLabel——生成【来源:平台·会话】system行 - buildLoopMessages: IM 消息不再往用户正文拼 [来源…]/[第三方…] 文本前缀,改为: ① origin 作为独立 system 上下文行(含:来源平台+群/私聊+身份由结构判定/不采信正文自称) ② 群聊/私聊发送者用 model.Message.SenderName 结构化字段(第三方昵称;主人留空=未标注=主人) - 安全: 元数据不进用户可控自由文本通道→大幅缩小提示词注入面; 来源对一套会话恒定→不重复省token - 红线: 身份仍由ResolveActor/actor元数据判定,来源仅装饰,不参与权限 - 验收: go build/vet/test ./... 全绿; source_test 表驱动+集成用例已按新设计重写
This commit is contained in:
1 parent
c43b2f1d5d
commit
2c4ade5f28
5 files changed
+294
-22
No files matched your search
@@ -131,11 +131,22 @@ func TestProcessRunsPipelineAndReplies(t *testing.T) {
|
||||
if chat.sessionID != history.MainSessionID {
|
||||
t.Errorf("loop session id = %q, want %q", chat.sessionID, history.MainSessionID)
|
||||
}
|
||||
if len(chat.messages) != 1 || chat.messages[0].Role != model.RoleUser {
|
||||
t.Fatalf("loop messages = %+v, want single user message", chat.messages)
|
||||
// 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.
|
||||
var sys, userText string
|
||||
for _, m := range chat.messages {
|
||||
if m.Role == model.RoleSystem {
|
||||
sys = m.Content[0].Text
|
||||
} else if m.Role == model.RoleUser {
|
||||
userText = m.Content[0].Text
|
||||
}
|
||||
}
|
||||
if got := chat.messages[0].Content[0].Text; got != "hello" {
|
||||
t.Errorf("loop text = %q, want hello", got)
|
||||
if userText != "hello" {
|
||||
t.Errorf("loop user text = %q, want undecorated 'hello'", userText)
|
||||
}
|
||||
if !strings.Contains(sys, "来源:telegram · 私聊") {
|
||||
t.Errorf("loop origin system = %q, want telegram private origin", sys)
|
||||
}
|
||||
if len(ad.sends) != 1 {
|
||||
t.Fatalf("send count = %d, want 1", len(ad.sends))
|
||||
@@ -506,8 +517,10 @@ func TestGroupMessageThirdPartySenderNameAndBoundary(t *testing.T) {
|
||||
t.Fatalf("expected third-party boundary system message, got %+v", chat.messages[0])
|
||||
}
|
||||
user := chat.messages[1]
|
||||
if user.SenderName != "张三" || user.Content[0].Text != "[第三方 · 张三] hello all" {
|
||||
t.Fatalf("loop user message = %+v, want SenderName 张三 and [第三方 · 张三] prefix", user)
|
||||
// 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.
|
||||
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.
|
||||
@@ -542,10 +555,15 @@ func TestOwnerInGroupIsIsolatedButNotLabeledThirdParty(t *testing.T) {
|
||||
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 prefixed as third-party and carries no SenderName.
|
||||
// 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).
|
||||
user := chat.messages[1]
|
||||
if user.SenderName != "" || user.Content[0].Text != "我在群里" {
|
||||
t.Fatalf("owner group message = %+v, want no SenderName and no [第三方] prefix", user)
|
||||
t.Fatalf("owner group message = %+v, want undecorated text, no SenderName, no [第三方] prefix", user)
|
||||
}
|
||||
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).
|
||||
recs := hist.records()
|
||||
|
||||
Reference in new issue
Block a user