feat(channel,runtime): 批次2 后端 — 消息级角色归属进上下文 + IM 私聊随主会话角色 + 渠道级绑定
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

按 design/22 §2/§3/§4.4/§7(3/4/5)。
- ①「消息级角色归属」进入模型上下文(批次 1 只做存储+DTO):历史消息的发送角色现在对模型可见,且**不把系统元数据塞进用户正文**(design/11 §10 红线)
- ② IM owner 私聊镜像解析出的角色 = **主会话绑定的角色**(§7-3)
- ③ 渠道级绑定:ChannelAccount 加 personaId(channels.json 读写/校验/脱敏沿用既有约定)+ 角色解析优先级 **会话 > 渠道 > 默认** + **群聊/陌生人强制默认角色**(§7-4);admin 渠道配置端点支持 personaId
- Docs/api-client 版本 +1
- 验收:go build/vet/test 全绿 + linux/darwin 交叉编译
This commit is contained in:
AskaEth committed 2026-10-03 21:01:33 +08:00
1 parent 3732c538cc
commit 5969bba3bd
22 files changed
+1396 -23

No files matched your search

+39 -16
View File
@@ -36,16 +36,7 @@ type asyncExtractor interface {
// project memory is injected so no owner-sensitive memory leaks to a group or
// stranger.
func (l *Loop) injectMemory(ctx context.Context, messages []model.Message) []model.Message {
personaID := l.personaFor(ctx)
if l.retriever == nil || personaID == "" {
return messages
}
env := sessionEnvFrom(ctx)
block, err := l.retrieveMemoryBlock(ctx, env, personaID, queryFromMessages(messages))
if err != nil {
l.log.Warn("memory inject", "error", err, "env", env.ActorEnv)
return messages
}
block := l.memoryBlock(ctx, messages)
if block == "" {
return messages
}
@@ -55,6 +46,25 @@ func (l *Loop) injectMemory(ctx context.Context, messages []model.Message) []mod
return out
}
// memoryBlock renders the round's memory block, or "" when there is nothing to
// inject (no retriever/persona, a retrieval error, or no relevant memory). It is
// split out from injectMemory because the assembly places other leading blocks
// between the memory block and the conversation (the role context of design/22
// §7-5), so it needs the block itself rather than a pre-prefixed slice.
func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string {
personaID := l.personaFor(ctx)
if l.retriever == nil || personaID == "" {
return ""
}
env := sessionEnvFrom(ctx)
block, err := l.retrieveMemoryBlock(ctx, env, personaID, queryFromMessages(messages))
if err != nil {
l.log.Warn("memory inject", "error", err, "env", env.ActorEnv)
return ""
}
return block
}
// retrieveMemoryBlock returns the memory injection block for env and the turn's
// role. The owner-full env keeps the pre-existing Inject behavior. The restricted
// env (group chat / stranger) injects only project-scope memory (人人可读, not
@@ -105,9 +115,17 @@ func formatMemoryBlock(results []memory.Result, marker string) string {
// re-injects the persona's relevant long-term memory — retrieval is re-run every
// round from the current conversation, not cached from the first turn, so it
// supports cross-round refresh — folds in the per-session working-memory summary
// when present, and applies the optional TransformContext hook last so a caller
// can project/filter what the model actually sees. It returns the input unchanged
// when present, renders the role/speaker attribution of the conversation (design/22
// §7-5), and applies the optional TransformContext hook last so a caller can
// project/filter what the model actually sees. It returns the input unchanged
// when nothing is configured.
//
// The existing order is preserved exactly: the leading blocks are the per-round
// time, the working-memory summary and the memory block, in that order; the role
// declaration sits right after them (immediately before the conversation it
// describes) and the speaker boundary notes are inserted into the conversation
// itself. A conversation with no speaker label renders no attribution at all, so
// every single-role path keeps the prompt it had before multi-character support.
func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, round int) []model.Message {
var leading []model.Message
// Per-round "current time" so the model naturally knows the wall clock (not a
@@ -122,11 +140,16 @@ func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, ro
leading = append(leading, model.TextMessage(model.RoleSystem, "工作记忆: "+s))
}
}
leading = append(leading, l.injectMemory(ctx, messages)...)
out := leading
if block := l.memoryBlock(ctx, messages); block != "" {
leading = append(leading, model.TextMessage(model.RoleSystem, block))
}
decl, ok, view := l.attributeConversation(ctx, messages)
if ok {
leading = append(leading, decl)
}
out := append(leading, view...)
if l.transformContext != nil {
out = l.transformContext(ctx, leading, round)
out = l.transformContext(ctx, out, round)
}
return out
}