package agents import ( "bytes" "encoding/json" "net/http" "strings" "mmgame/internal/config" "mmgame/internal/scripts" ) type ChatMessage struct { Role string `json:"role"` Content string `json:"content"` } type AIBackend struct { backend string } var ai = &AIBackend{backend: config.Cfg.AIBackend} func (a *AIBackend) chat(messages []ChatMessage, temperature float64, maxTokens int) string { switch a.backend { case "ollama": return a.ollamaChat(messages, temperature, maxTokens) case "deepseek": return a.deepseekChat(messages, temperature, maxTokens) default: return "" } } func (a *AIBackend) ollamaChat(messages []ChatMessage, temperature float64, maxTokens int) string { body := map[string]interface{}{ "model": config.Cfg.OllamaModel, "messages": messages, "stream": false, "options": map[string]interface{}{"temperature": temperature, "num_predict": maxTokens}, } data, err := json.Marshal(body) if err != nil { return "" } resp, err := http.Post(config.Cfg.OllamaBaseURL+"/api/chat", "application/json", bytes.NewReader(data)) if err != nil { return "" } defer resp.Body.Close() var result struct { Message struct { Content string `json:"content"` } `json:"message"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "" } return result.Message.Content } func (a *AIBackend) deepseekChat(messages []ChatMessage, temperature float64, maxTokens int) string { body := map[string]interface{}{ "model": config.Cfg.DeepSeekModel, "messages": messages, "temperature": temperature, "max_tokens": maxTokens, } data, err := json.Marshal(body) if err != nil { return "" } req, err := http.NewRequest("POST", strings.TrimSuffix(config.Cfg.DeepSeekBaseURL, "/")+"/v1/chat/completions", bytes.NewReader(data)) if err != nil { return "" } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+config.Cfg.DeepSeekAPIKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() var result struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "" } if len(result.Choices) > 0 { return result.Choices[0].Message.Content } return "" } var dmIntros = map[string]string{ "hardcore": "欢迎来到这场推理盛宴。真相就藏在你面前的线索之中,仔细观察,大胆推理。", "emotional": "这是一个关于人心的故事。有时候,真相并不重要,重要的是你在这个过程中感受到了什么。", "欢乐": "欢迎来到这场欢乐的聚会!记住,每个人都在演戏,但真相只有一个!", "恐怖": "黑暗中有什么在注视着你……准备好了吗?", } func cleanJSON(s string) string { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "```json") s = strings.TrimSuffix(s, "```") return strings.TrimSpace(s) } func DMGenerate(script *scripts.Script, phaseIndex int, releasedClues []string, context string) map[string]interface{} { phases := script.Phases var phase *scripts.Phase if phaseIndex < len(phases) { phase = &phases[phaseIndex] } else { phase = &phases[len(phases)-1] } title := script.Title scriptType := script.Type typeIntro := dmIntros[scriptType] cluesStr := "暂无" if len(releasedClues) > 0 { cluesStr = strings.Join(releasedClues, ", ") } prompt := "你是一个剧本杀主持人(DM),请以主持人的身份发言。\n" + "当前剧本: " + title + "\n" + "当前阶段: " + phase.Name + "\n" + "阶段描述: " + phase.Description + "\n" + "已发放线索: " + cluesStr + "\n" + "上下文: " + context + "\n\n" + "请简短发言(50字以内),推动剧情发展。" result := ai.chat([]ChatMessage{{Role: "user", Content: prompt}}, 0.7, 512) if result != "" { cleaned := cleanJSON(result) var m map[string]interface{} if err := json.Unmarshal([]byte(cleaned), &m); err == nil { return m } return map[string]interface{}{"action": "narrate", "content": result} } voice := phase.Description if len(voice) > 80 { voice = voice[:80] } if phaseIndex == 0 && context == "opening" { bg := script.Background if len(bg) > 100 { bg = bg[:100] } voice = typeIntro + "\n\n欢迎来到【" + title + "】。" + bg } return map[string]interface{}{"action": "narrate", "content": "【" + phase.Name + "】" + voice} } func NPCGenerate(role *scripts.Role, background, phaseName string, recentMessages []string, playerMessage string, isPrivate bool, knownClues []string, phaseGoal string, allRoles []*scripts.Role) map[string]interface{} { personality := role.Personality if personality == "" { personality = role.Name } cluesStr := "暂无线索" if len(knownClues) > 0 { var last []string if len(knownClues) > 5 { last = knownClues[len(knownClues)-5:] } else { last = knownClues } cluesStr = strings.Join(last, " | ") } rolesStr := "" if len(allRoles) > 0 { var names []string for _, r := range allRoles { if r.ID != role.ID { names = append(names, r.Name) } } if len(names) > 0 { rolesStr = "其他玩家: " + strings.Join(names, ", ") } } bg := background if len(bg) > 80 { bg = bg[:80] + "..." } recent := "" if len(recentMessages) > 0 { var last []string if len(recentMessages) > 4 { last = recentMessages[len(recentMessages)-4:] } else { last = recentMessages } recent = strings.Join(last, " | ") } goalLine := "" if phaseGoal != "" { goalLine = "阶段目标: " + phaseGoal } privLine := "" if isPrivate && playerMessage != "" { privLine = "【私聊】" + playerMessage } else if playerMessage != "" { privLine = "【有人对你说】" + playerMessage } prompt := "你正在扮演一个剧本杀角色。请严格按照角色设定发言。\n\n" + "角色信息:\n" + "- 姓名: " + role.Name + "\n" + "- 公开身份: " + role.PublicProfile + "\n" + "- 性格: " + personality + "\n" + "- 你的目标: " + role.Goal + "\n" + "- 你的秘密: " + role.SecretProfile + "\n\n" + "当前背景: " + bg + "\n" + "当前阶段: " + phaseName + "\n" + goalLine + "\n" + rolesStr + "\n\n" + "你已知道的线索:\n" + cluesStr + "\n\n" + "最近发言:\n" + recent + "\n\n" + privLine + "\n\n" + "【发言规则】\n" + "1. 以角色身份说人话,简短自然(30字以内)\n" + "2. 你的秘密绝对不能主动说出来\n" + "3. 被问到时可以撒谎、回避、转移话题\n" + "4. 结合已公开的线索来推理和回应\n" + "5. 如果你知道某些线索的真相(如你就是凶手),可以故意误导他人\n\n" + "直接输出你的角色发言内容,不要JSON、不要解释。" result := ai.chat([]ChatMessage{{Role: "user", Content: prompt}}, 0.7, 512) if result != "" { cleaned := cleanJSON(result) var m map[string]interface{} if err := json.Unmarshal([]byte(cleaned), &m); err == nil { return m } return map[string]interface{}{"action": "speak", "content": result} } return map[string]interface{}{"action": "speak", "content": ruleNPCReply(role, playerMessage, isPrivate, knownClues)} } func ruleNPCReply(role *scripts.Role, playerMessage string, isPrivate bool, knownClues []string) string { name := role.Name clues := knownClues if playerMessage == "" { if len(clues) > 0 { replies := []string{ name + "沉思道:\"这些线索……我觉得需要重新梳理一下。\"", name + "说:\"我注意到了一些细节,但现在还不方便说。\"", name + "看了看法医报告:\"时间线和线索对不上,肯定有人撒谎。\"", } return replies[randInt(len(replies))] } replies := []string{ name + "环顾四周,若有所思。", name + "清了清嗓子:\"各位,我觉得我们应该整理一下思路。\"", name + "沉默地看着大家。", name + "低声说:\"这件事没有那么简单……\"", } return replies[randInt(len(replies))] } if strings.Contains(playerMessage, "凶手") || strings.Contains(playerMessage, "杀人") || strings.Contains(playerMessage, "你杀") { denials := []string{ name + "脸色一变:\"你凭什么这么说?证据呢?\"", name + "冷笑一声:\"如果我是凶手,我还会坐在这里?\"", name + "摇头:\"我没有理由杀他。\"", } return denials[randInt(len(denials))] } if strings.Contains(playerMessage, "时间") || strings.Contains(playerMessage, "在哪") || strings.Contains(playerMessage, "案发") { times := []string{ name + "回忆道:\"我当时在……让我想想。\"", name + "说:\"那段时间我一个人在房间里。\"", name + "皱眉:\"我不太确定具体时间,但我确实听到了什么声音。\"", } return times[randInt(len(times))] } if strings.Contains(playerMessage, "线索") || strings.Contains(playerMessage, "证据") || strings.Contains(playerMessage, "发现") { clueReplies := []string{ name + "点头:\"这个线索确实值得注意。\"", name + "沉思:\"但这个线索也可能是在误导我们。\"", name + "说:\"我也有一个发现,但还不确定是否相关。\"", } return clueReplies[randInt(len(clueReplies))] } if strings.Contains(playerMessage, "知道") || strings.Contains(playerMessage, "秘密") { secrets := []string{ name + "回避了你的目光:\"我什么也不知道。\"", name + "沉默了一会儿:\"每个人都有不想说的秘密,不是吗?\"", name + "说:\"我只能告诉你,事情不是你看到的那样。\"", } return secrets[randInt(len(secrets))] } if strings.Contains(playerMessage, "动机") || strings.Contains(playerMessage, "为什么") || strings.Contains(playerMessage, "目的") { motives := []string{ name + "说:\"每个人都有自己的理由,但有些理由……\"", name + "耸耸肩:\"动机?也许我们需要先搞清楚发生了什么。\"", name + "看着你:\"你确定你想知道真正的动机?\"", } return motives[randInt(len(motives))] } generic := []string{ name + "思考了一下:\"这个嘛,我说不好。\"", name + "回答:\"我不太确定,但我觉得我们应该继续调查。\"", name + "说:\"你说得有一定道理,但可能还有其他可能性。\"", name + "点头:\"有意思,继续说。\"", } return generic[randInt(len(generic))] }