package multiagent import ( "fmt" "os" "path/filepath" "strings" "time" "cyberstrike-ai/internal/config" "go.uber.org/zap" ) func prepareLatestUserMessageForModel(userMessage string, appCfg *config.Config, mwCfg *config.MultiAgentEinoMiddlewareConfig, conversationID string, logger *zap.Logger) string { if strings.TrimSpace(userMessage) == "" { return strings.TrimSpace(userMessage) } if mwCfg == nil { var zero config.MultiAgentEinoMiddlewareConfig mwCfg = &zero } maxRunes := mwCfg.LatestUserMessageMaxRunesEffective() if maxRunes <= 0 || utf8RuneLen(userMessage) <= maxRunes { return userMessage } headRunes, tailRunes := normalizeLatestUserPreviewBudget( maxRunes, mwCfg.LatestUserMessageHeadRunesEffective(), mwCfg.LatestUserMessageTailRunesEffective(), ) head, tail := splitHeadTailRunes(userMessage, headRunes, tailRunes) artifactPath, writeErr := persistLatestUserMessageArtifact(userMessage, appCfg, conversationID) if writeErr != nil && logger != nil { logger.Warn("latest user message artifact 写入失败,将仅使用预览", zap.String("conversationId", conversationID), zap.Error(writeErr), ) } var sb strings.Builder sb.WriteString("【系统提示:本轮用户输入过长,已为模型上下文生成裁剪预览。】\n") sb.WriteString("用户原始输入已完整保存到数据库 messages.role=user;") if artifactPath != "" { sb.WriteString("同时已落盘为 artifact,可在需要全文时读取:\n") sb.WriteString("artifact_path: ") sb.WriteString(artifactPath) sb.WriteByte('\n') } else { sb.WriteString("artifact 写入失败时仍可从数据库原始消息恢复。\n") } sb.WriteString(fmt.Sprintf("original_runes: %d\n", utf8RuneLen(userMessage))) sb.WriteString(fmt.Sprintf("preview_head_runes: %d\n", utf8RuneLen(head))) sb.WriteString(fmt.Sprintf("preview_tail_runes: %d\n\n", utf8RuneLen(tail))) sb.WriteString("请优先基于以下预览理解用户目标;如必须查看全文,请读取 artifact 或数据库原始消息。\n\n") sb.WriteString("\n") sb.WriteString(head) sb.WriteString("\n\n") if tail != "" { sb.WriteString("\n\n") sb.WriteString(tail) sb.WriteString("\n\n") } return strings.TrimSpace(sb.String()) } func normalizeLatestUserPreviewBudget(maxRunes, headRunes, tailRunes int) (int, int) { if maxRunes <= 0 { return headRunes, tailRunes } if headRunes <= 0 && tailRunes <= 0 { headRunes = maxRunes / 2 tailRunes = maxRunes - headRunes } if headRunes < 0 { headRunes = 0 } if tailRunes < 0 { tailRunes = 0 } if headRunes+tailRunes <= maxRunes { return headRunes, tailRunes } if headRunes == 0 { return 0, maxRunes } if tailRunes == 0 { return maxRunes, 0 } head := maxRunes / 2 tail := maxRunes - head return head, tail } func splitHeadTailRunes(s string, headRunes, tailRunes int) (string, string) { runes := []rune(s) n := len(runes) if headRunes > n { headRunes = n } head := string(runes[:headRunes]) if tailRunes <= 0 || headRunes >= n { return head, "" } if tailRunes > n-headRunes { tailRunes = n - headRunes } tail := string(runes[n-tailRunes:]) return head, tail } func persistLatestUserMessageArtifact(content string, appCfg *config.Config, conversationID string) (string, error) { conversationID = strings.TrimSpace(conversationID) if conversationID == "" { conversationID = "unknown" } baseRoot := filepath.Join(os.TempDir(), "cyberstrike-user-inputs") if appCfg != nil { if dbPath := strings.TrimSpace(appCfg.Database.Path); dbPath != "" { baseRoot = filepath.Join(filepath.Dir(dbPath), "conversation_artifacts") } } if abs, err := filepath.Abs(baseRoot); err == nil { baseRoot = abs } dir := filepath.Join(baseRoot, sanitizeEinoPathSegment(conversationID), "user_inputs") if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("mkdir user input artifact dir: %w", err) } name := "latest_user_" + time.Now().UTC().Format("20060102T150405.000000000Z") + ".txt" path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(content), 0o600); err != nil { return "", fmt.Errorf("write user input artifact: %w", err) } return path, nil }