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
@@ -567,7 +567,7 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// from prompt text.
|
||||
runEnv := loop.SessionEnv{ActorEnv: sess.Env(), MemWritable: sess.MemWritable}
|
||||
senderName := senderDisplayName(msg, actor)
|
||||
loopMsgs := b.buildLoopMessages(ctx, text, runEnv, actor, senderName)
|
||||
loopMsgs := b.buildLoopMessages(ctx, text, runEnv, actor, senderName, msg)
|
||||
|
||||
// Persist the inbound user message before the turn so it is durable even if the
|
||||
// channel reply fails. Owner-private messages land in the main web session and
|
||||
@@ -608,25 +608,46 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
}
|
||||
}
|
||||
|
||||
// buildLoopMessages builds the model-visible message list for one inbound turn. In
|
||||
// a restricted (group/stranger) context it prepends a single system message that
|
||||
// states the third-party identity boundary — "every IM non-admin account's message
|
||||
// is a third party, unrelated to the owner" (design/16 §4.6 §1). The boundary is
|
||||
// decided structurally from the resolved SessionEnv, never inferred by the LLM.
|
||||
// The raw message text is left untouched (a copy is built with the prefix), so
|
||||
// persisted content stays consistent with what the source platform delivered.
|
||||
func (b *Bridge) buildLoopMessages(_ context.Context, text string, env loop.SessionEnv, actor adapter.Actor, senderName string) []model.Message {
|
||||
// buildLoopMessages builds the model-visible message list for one inbound turn. It
|
||||
// decorates the user message with a "[来源: <platform> · <conversation>]" prefix so
|
||||
// the model can see which platform and conversation the message came from (design/16
|
||||
// §4.6.1); a pure Web main-session message (platform ""/"web") is left untouched. In
|
||||
// a restricted (group/stranger) context it additionally prepends a single system
|
||||
// message that states the third-party identity boundary — "every IM non-admin
|
||||
// account's message is a third party, unrelated to the owner" (design/16 §4.6 §1).
|
||||
// The boundary and the "[第三方 · 昵称]" marker are decided structurally from the
|
||||
// resolved SessionEnv/actor, never inferred by the LLM. The raw message text is left
|
||||
// untouched (a copy is built with the prefix), so persisted content stays consistent
|
||||
// with what the source platform delivered.
|
||||
func (b *Bridge) buildLoopMessages(_ context.Context, text string, env loop.SessionEnv, actor adapter.Actor, senderName string, src adapter.UnifiedMessage) []model.Message {
|
||||
msg := model.TextMessage(model.RoleUser, text)
|
||||
thirdParty := env.Restricted() && !actor.IsOwner() && senderName != ""
|
||||
if thirdParty {
|
||||
// The speaker is carried as structured metadata, NOT woven into the user text
|
||||
// (design/16 §4.6.1): a text tag is both ambiguous and a prompt-injection surface.
|
||||
msg.SenderName = senderName
|
||||
msg.Content[0].Text = fmt.Sprintf("[第三方 · %s] %s", senderName, text)
|
||||
}
|
||||
|
||||
// origin system line: conversation origin + identity-trust rule, one per turn.
|
||||
origin := originSystemText(src, actor)
|
||||
|
||||
if !env.Restricted() {
|
||||
if origin != "" {
|
||||
return []model.Message{model.TextMessage(model.RoleSystem, origin), msg}
|
||||
}
|
||||
return []model.Message{msg}
|
||||
}
|
||||
boundary := model.TextMessage(model.RoleSystem, thirdPartyBoundaryPrompt)
|
||||
return []model.Message{boundary, msg}
|
||||
// Restricted (group/stranger): origin (if IM) + the third-party identity boundary.
|
||||
var sys string
|
||||
switch {
|
||||
case origin != "" && thirdPartyBoundaryPrompt != "":
|
||||
sys = origin + "\n" + thirdPartyBoundaryPrompt
|
||||
case origin != "":
|
||||
sys = origin
|
||||
default:
|
||||
sys = thirdPartyBoundaryPrompt
|
||||
}
|
||||
return []model.Message{model.TextMessage(model.RoleSystem, sys), msg}
|
||||
}
|
||||
|
||||
// sendReply delivers the loop's text back to the channel's source conversation
|
||||
|
||||
Reference in new issue
Block a user