This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+57 -21
View File
@@ -26,18 +26,20 @@ type MessageServiceConfig struct {
}
type messageService struct {
repo Repository
storage storage.Storage
hub StreamerHub
auditor audit.Recorder
log *slog.Logger
mimeWhite map[string]bool
maxFile int64
maxMsg int64
safetyPct float64
repo Repository
storage storage.Storage
hub StreamerHub
auditor audit.Recorder
log *slog.Logger
mimeWhite map[string]bool
maxFile int64
maxMsg int64
safetyPct float64
briefReader ContextReader // 可为 nil;非 nil 时 composeHistory 注入 FK 上下文
}
// NewMessageService constructs a MessageService.
// NewMessageService constructs a MessageService. briefReader 可为 nil:nil 时
// composeHistory 完全保留旧行为(仅用 conv.SystemPrompt),不做 FK 上下文注入。
func NewMessageService(
repo Repository,
st storage.Storage,
@@ -45,21 +47,23 @@ func NewMessageService(
auditor audit.Recorder,
log *slog.Logger,
cfg MessageServiceConfig,
briefReader ContextReader,
) MessageService {
mimeWhite := make(map[string]bool, len(cfg.MimeWhitelist))
for _, m := range cfg.MimeWhitelist {
mimeWhite[m] = true
}
return &messageService{
repo: repo,
storage: st,
hub: hub,
auditor: auditor,
log: log,
mimeWhite: mimeWhite,
maxFile: cfg.MaxFileBytes,
maxMsg: cfg.MaxMsgBytes,
safetyPct: cfg.SafetyPct,
repo: repo,
storage: st,
hub: hub,
auditor: auditor,
log: log,
mimeWhite: mimeWhite,
maxFile: cfg.MaxFileBytes,
maxMsg: cfg.MaxMsgBytes,
safetyPct: cfg.SafetyPct,
briefReader: briefReader,
}
}
@@ -483,20 +487,52 @@ func (s *messageService) composeHistory(ctx context.Context, conv *Conversation,
}
}
// Resolve the effective system prompt by merging conv.SystemPrompt with the
// FK-derived context brief (requirement/issue/project mounts). The brief is
// pre-bounded by the composer, so it cannot starve message history to zero
// (truncateByTokens still reserves the remaining budget for messages).
effectiveSystemPrompt := s.effectiveSystemPrompt(ctx, conv)
// Compute token budget: context window minus max output tokens, then apply safety margin.
budget := model.ContextWindow - model.MaxOutputTokens
budget = int(float64(budget) * (1 - s.safetyPct))
msgs = truncateByTokens(msgs, conv.SystemPrompt, budget)
msgs = truncateByTokens(msgs, effectiveSystemPrompt, budget)
return llm.Request{
SystemPrompt: conv.SystemPrompt,
SystemPrompt: effectiveSystemPrompt,
Messages: msgs,
MaxOutputTokens: model.MaxOutputTokens,
Reasoning: model.Capabilities.Reasoning,
}, nil
}
// effectiveSystemPrompt 把会话静态 system prompt 与 FK 派生上下文简报合并。
// briefReader 为 nil、会话无任何 FK 挂载、或简报为空时,直接返回 conv.SystemPrompt
// (与旧行为完全一致)。合并是追加式的(FK 简报置于静态 prompt 之后),便于
// 让旧会话(曾把角色 prompt 烘焙进 SystemPrompt)与新会话都能正常工作。
func (s *messageService) effectiveSystemPrompt(ctx context.Context, conv *Conversation) string {
if s.briefReader == nil || !conv.HasFKMount() {
return conv.SystemPrompt
}
fkBrief, err := s.briefReader.BriefForConversation(ctx, conv)
if err != nil {
if s.log != nil {
s.log.Warn("chat.compose_history.brief_failed",
"conversation_id", conv.ID, "err", err.Error())
}
return conv.SystemPrompt
}
fkBrief = strings.TrimSpace(fkBrief)
if fkBrief == "" {
return conv.SystemPrompt
}
if strings.TrimSpace(conv.SystemPrompt) == "" {
return fkBrief
}
return conv.SystemPrompt + "\n\n" + fkBrief
}
// truncateByTokens walks messages backwards and drops oldest messages until the
// estimated token count (system prompt + messages) fits within budget.
func truncateByTokens(msgs []llm.Message, systemPrompt string, budget int) []llm.Message {