package plugins import ( "database/sql" "errors" "fmt" "kurumibot/database/mdb" "kurumibot/database/psql" "kurumibot/database/red" "kurumibot/laniakea" "kurumibot/utils/ai" "strconv" "strings" "github.com/google/uuid" ) func RegisterRP(bot *laniakea.Bot) { rp := laniakea.NewPlugin("RP") rp.Command(rpInfo, "rp", "рп") rp.Payload(rpInfo, "rp.info") rp.Payload(rpWaifuList, "rp.waifu_list") rp.Payload(rpWaifuSet, "rp.waifu_set") rp.Payload(rpPresetsList, "rp.preset_list") rp.Payload(rpPresetSet, "rp.preset_set") rp.Payload(rpModelList, "rp.model_list") rp.Payload(rpModelSet, "rp.model_set") rp.Payload(rpScenarioList, "rp.scenario_list") rp.Payload(rpSettingList, "rp.setting_list") rp.Payload(chatStat, "rp.chat_stat") rp.Payload(newChatStage1, "rp.new_chat_s1") rp.Payload(newChatStage2, "rp.new_chat_s2") rp.Payload(newChat, "rp.new_chat") rp.Command(rpUserPromptGet, "rpuserpget") rp.Command(rpUserPromptSet, "rpuserpset") rp.Command(generate, "g", "gen", "г") rp.Payload(compress, "rp.compress_chat") rp.Payload(generalClose, "general.close") bot.AddPlugins(rp.Build()) } func rpInfo(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { userRep := psql.NewUserRepository(db) _, err := userRep.GetOrCreate(ctx.FromID, ctx.From.FirstName) if err != nil { ctx.Error(err) return } waifuRep := psql.NewWaifuRepository(db) count, err := waifuRep.GetCountByUserId(ctx.FromID) if err != nil { ctx.Error(err) return } if count == 0 { kb := laniakea.NewInlineKeyboard(1).AddCallbackButton("Закрыть", "general.close") ctx.Keyboard("Для доступа к этой команде нужно иметь хотя бы одну вайфу", kb) return } rpRepRed := red.NewRPRepository(db) rpRepPsql := psql.NewRPRepository(db) rpUser, err := rpRepPsql.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } var waifu *psql.Waifu waifuId := rpRepRed.GetSelectedWaifu(ctx.FromID) if waifuId == 0 { waifus, err := waifuRep.GetByUserId(ctx.FromID) if err != nil { ctx.Error(err) return } waifu = waifus[0] err = rpRepRed.SetSelectedWaifu(ctx.FromID, waifu.ID) if err != nil { ctx.Error(err) } } else { waifu, err = waifuRep.GetById(waifuId) if err != nil { ctx.Error(err) } } out := []string{ fmt.Sprintf("Привет, %s!", ctx.From.FirstName), fmt.Sprintf("*Выбранная вайфу*: %s", waifu.Name), fmt.Sprintf("*Выбранный пресет*: %s", laniakea.EscapeMarkdown(rpUser.Preset.Name)), fmt.Sprintf("*Выбранная модель*: %s", rpUser.Model.Name), fmt.Sprintf("*Твое описание персонажа*: %s", rpUser.UserPrompt), } kb := laniakea.NewInlineKeyboard(2) kb.AddCallbackButton("Статистика чата", "rp.chat_stat") kb.AddCallbackButton("Сменить вайфу", "rp.waifu_list") kb.AddCallbackButton("Сменить пресет", "rp.preset_list") kb.AddCallbackButton("Сменить модель", "rp.model_list") kb.AddCallbackButton("Список сценариев", "rp.scenario_list") kb.AddCallbackButton("Список сеттингов", "rp.setting_list") kb.AddCallbackButton("Новый чат", "rp.new_chat_s1") kb.AddLine() kb.AddCallbackButton("Закрыть", "general.close") if ctx.CallbackMsgId > 0 { ctx.EditCallback(strings.Join(out, "\n"), kb) } else { ctx.Keyboard(strings.Join(out, "\n"), kb) } } func rpWaifuList(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { waifuRep := psql.NewWaifuRepository(db) waifus, err := waifuRep.GetByUserId(ctx.FromID) if err != nil { ctx.Error(err) return } out := make([]string, len(waifus)) kb := laniakea.NewInlineKeyboard(2) for i, waifu := range waifus { out[i] = fmt.Sprintf("*%s* %d\\* из \"%s\"", waifu.Name, waifu.Rarity, waifu.Fandom) kb.AddCallbackButton(waifu.Name, "rp.waifu_set", waifu.ID) } kb.AddLine() kb.AddCallbackButton("На главную", "rp.info") ctx.EditCallback(strings.Join(out, "\n"), kb) } func rpWaifuSet(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { waifuId, err := strconv.Atoi(ctx.Args[0]) if err != nil { ctx.Error(err) return } rpRepRed := red.NewRPRepository(db) err = rpRepRed.SetSelectedWaifu(ctx.FromID, waifuId) if err != nil { ctx.Error(err) return } //rpRepPsql := psql.NewRPRepository(db) waifuRep := psql.NewWaifuRepository(db) waifu, err := waifuRep.GetById(waifuId) if err != nil { ctx.Error(err) } kb := laniakea.NewInlineKeyboard(1).AddCallbackButton("На главную", "rp.info") ctx.EditCallbackf("Была выбрана вайфу %s", kb, waifu.Name) } func rpPresetsList(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) presets, err := rep.GetAllPresets() if err != nil { ctx.Error(err) return } out := make([]string, len(presets)) kb := laniakea.NewInlineKeyboard(2) for i, preset := range presets { out[i] = fmt.Sprintf( "*%s* - %s", laniakea.EscapeMarkdown(preset.Name), preset.Description, ) kb.AddCallbackButton(preset.Name, "rp.preset_set", preset.ID) } kb.AddLine() kb.AddCallbackButton("Назад", "rp.info") ctx.EditCallback(strings.Join(out, "\n"), kb) } func rpPresetSet(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { if len(ctx.Args) == 0 || ctx.Args[0] == "" { return } presetId := ctx.Args[0] rep := psql.NewRPRepository(db) user, err := rep.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } preset, err := rep.UpdateUserPreset(user, presetId) if err != nil { if errors.Is(err, sql.ErrNoRows) { ctx.Answer("Данный пресет не найден") } else { ctx.Error(err) } return } kb := laniakea.NewInlineKeyboard(1).AddCallbackButton("На главную", "rp.info") ctx.EditCallbackf("Был выбран пресет %s", kb, preset.Name) } func rpModelList(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) models, err := rep.GetAllModels() if err != nil { ctx.Error(err) return } out := make([]string, len(models)) kb := laniakea.NewInlineKeyboard(2) for i, model := range models { out[i] = fmt.Sprintf("*%s* - размер контекста _%dK_", model.Name, model.ContextSize) kb.AddCallbackButton(model.Name, "rp.model_set", model.ID) } kb.AddLine() kb.AddCallbackButton("Назад", "rp.info") ctx.EditCallback(strings.Join(out, "\n"), kb) } func rpModelSet(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) user, err := rep.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } model, err := rep.GetModel(ctx.Args[0]) if err != nil { ctx.Error(err) return } user.SelectedModel = model.ID err = rep.UpdateUser(user) if err != nil { ctx.Error(err) return } kb := laniakea.NewInlineKeyboard(1).AddCallbackButton("На главную", "rp.info") ctx.EditCallback(fmt.Sprintf("Была установлена модель %s", model.Name), kb) } func rpScenarioList(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) scenarios, err := rep.GetAllScenarios() if err != nil { ctx.Error(err) return } out := make([]string, len(scenarios)) kb := laniakea.NewInlineKeyboard(1) for i, scenario := range scenarios { out[i] = fmt.Sprintf("*%s* - %s\n", scenario.Name, scenario.Description) } kb.AddCallbackButton("На главную", "rp.info") ctx.EditCallback("Список сценариев\n"+strings.Join(out, "\n"), kb) } func rpSettingList(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) settings, err := rep.GetAllSettings() if err != nil { ctx.Error(err) return } out := make([]string, len(settings)) for i, setting := range settings { out[i] = fmt.Sprintf("*%s* - %s\n", setting.Name, setting.Description) } kb := laniakea.NewInlineKeyboard(1) kb.AddCallbackButton("На главную", "rp.info") ctx.EditCallback(strings.Join(out, "\n"), kb) } func chatStat(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { redisRpRep := red.NewRPRepository(db) waifuId := redisRpRep.GetSelectedWaifu(ctx.FromID) if waifuId == 0 { ctx.Answer("Не выбрана вайфу") return } chatId := redisRpRep.GetChatId(ctx.FromID, waifuId) if chatId == "" { chatId = uuid.New().String() err := redisRpRep.SetChatId(ctx.FromID, waifuId, chatId) if err != nil { ctx.Error(err) return } } messageCount, err := mdb.GetChatHistorySize(db, chatId) if err != nil { ctx.Error(err) return } tokens := redisRpRep.GetChatTokens(ctx.FromID, waifuId) kb := laniakea.NewInlineKeyboard(1) kb = kb.AddCallbackButton("Сжать чат", "rp.compress_chat") kb = kb.AddCallbackButton("На главную", "rp.info") out := []string{ "Статистика чата", fmt.Sprintf("*ID*: `%s`", chatId), fmt.Sprintf("*Кол-во сообщений*: %d", messageCount), fmt.Sprintf("*Кол-во токенов*: %d", tokens), } ctx.EditCallback(strings.Join(out, "\n"), kb) } func newChatStage1(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { // Выбор сеттинга rep := psql.NewRPRepository(db) settings, err := rep.GetAllSettings() if err != nil { ctx.Error(err) return } out := []string{ "Выбери сеттинг для чата", } kb := laniakea.NewInlineKeyboard(2) for _, setting := range settings { out = append(out, fmt.Sprintf("*%s* - %s", setting.Name, setting.Description)) kb.AddCallbackButton(setting.Name, "rp.new_chat_s2", setting.ID) } kb.AddCallbackButton("Без сеттинга", "rp.new_chat_s2", 0) kb.AddLine() kb.AddCallbackButton("Назад", "rp.info") ctx.EditCallback(strings.Join(out, "\n"), kb) } func newChatStage2(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { // Выбор сценария if len(ctx.Args) == 0 { ctx.Error(fmt.Errorf("zero args len")) return } rep := psql.NewRPRepository(db) scenarios, err := rep.GetAllScenarios() if err != nil { ctx.Error(err) return } settingId, err := strconv.Atoi(ctx.Args[0]) if err != nil { ctx.Error(err) return } out := []string{ "Выбери сценарий:", } kb := laniakea.NewInlineKeyboard(2) for _, scenario := range scenarios { out = append(out, fmt.Sprintf("*%s* - %s", scenario.Name, scenario.Description)) kb.AddCallbackButton(scenario.Name, "rp.new_chat", settingId, scenario.ID) } kb.AddCallbackButton("Без сценария", "rp.new_chat", 0) kb.AddLine() kb.AddCallbackButton("Назад", "rp.new_chat_s1") ctx.EditCallback(strings.Join(out, "\n"), kb) } func newChat(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { redisRpRep := red.NewRPRepository(db) psqlRpRep := psql.NewRPRepository(db) waifuId := redisRpRep.GetSelectedWaifu(ctx.FromID) if waifuId == 0 { ctx.Answer("Не выбрана вайфу") return } chatId := uuid.New() err := redisRpRep.SetChatId(ctx.FromID, waifuId, chatId.String()) if err != nil { ctx.Error(err) return } chatPrompt := "" settingId, err := strconv.Atoi(ctx.Args[0]) if err != nil { ctx.Error(err) return } if settingId > 0 { scenario, err := psqlRpRep.GetSetting(settingId) if err != nil { if errors.Is(err, sql.ErrNoRows) { ctx.Answerf("Сеттинг №%d не найден", settingId) } else { ctx.Error(err) } return } chatPrompt = "Вот краткое описание мира(сеттинг): " + scenario.Prompt + "." } scenarioId, err := strconv.Atoi(ctx.Args[1]) if err != nil { ctx.Error(err) return } if scenarioId > 0 { rep := psql.NewRPRepository(db) scenario, err := rep.GetScenario(scenarioId) if err != nil { if errors.Is(err, sql.ErrNoRows) { ctx.Answerf("Сценарий №%d не найден", scenarioId) } else { ctx.Error(err) } return } chatPrompt += "Вот дополнительная информация: " + scenario.Prompt + "." } if chatPrompt != "" { err = redisRpRep.SetChatPrompt(ctx.FromID, waifuId, chatPrompt) if err != nil { ctx.Error(err) return } } err = redisRpRep.SetCounter(ctx.FromID, waifuId, 0) if err != nil { ctx.Error(err) return } err = redisRpRep.SetChatTokens(ctx.FromID, waifuId, 0) if err != nil { ctx.Error(err) return } kb := laniakea.NewInlineKeyboard(2) kb.AddCallbackButton("На главную", "rp.info").AddCallbackButton("Закрыть", "general.close") ctx.EditCallback(`Был создан новый чат.\nДля общения используй "/г промпт"`, kb) } func rpUserPromptGet(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { rep := psql.NewRPRepository(db) user, err := rep.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } if user.UserPrompt == "" { ctx.Answer("У тебя нет описания") return } ctx.Answerf("Вот твое РП описание пользователя\n%s", user.UserPrompt) } func rpUserPromptSet(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { if len(ctx.Args) == 0 || ctx.Args[0] == "" { return } prompt := strings.Join(ctx.Args[1:], " ") rep := psql.NewRPRepository(db) user, err := rep.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } user.UserPrompt = prompt err = rep.UpdateUser(user) if err != nil { ctx.Error(err) return } ctx.Answer("Описание пользователя было обновлено.") } func generate(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { redisRpRep := red.NewRPRepository(db) rpRep := psql.NewRPRepository(db) rpUser, err := rpRep.GetOrCreateUser(int64(ctx.FromID)) if err != nil { ctx.Error(err) return } waifuId := redisRpRep.GetSelectedWaifu(ctx.FromID) if waifuId == 0 { ctx.Answer("Не выбрана вайфу") return } chatId := redisRpRep.GetChatId(ctx.FromID, waifuId) if chatId == "" { chatId = uuid.New().String() err := redisRpRep.SetChatId(ctx.FromID, waifuId, chatId) if err != nil { ctx.Error(err) return } } waifuRep := psql.NewWaifuRepository(db) waifu, err := waifuRep.GetById(waifuId) if err != nil { ctx.Error(err) return } preset, err := rpRep.GetUserPreset(rpUser) if err != nil { ctx.Error(err) return } userPrompt := "" if rpUser.UserPrompt != "" { userPrompt = fmt.Sprintf("Вот описание моего персонажа %s.", rpUser.UserPrompt) } beforeHistory := ai.Message{ Role: "system", Content: fmt.Sprintf( "%s %s %s %s", ai.FormatPrompt(preset.PreHistory, waifu.Name, ctx.From.FirstName), fmt.Sprintf("Вот краткое описание твоего персонажа: %s.", waifu.RpPrompt), redisRpRep.GetChatPrompt(ctx.FromID, waifuId), userPrompt, ), } afterHistory := ai.Message{ Role: "system", Content: ai.FormatPrompt(preset.PostHistory, waifu.Name, ctx.From.FirstName), } history, err := mdb.GetChatHistory(db, chatId) if err != nil { ctx.Error(err) return } messages := []ai.Message{beforeHistory} for _, m := range history { messages = append(messages, ai.Message{ Role: m.Role, Content: strings.TrimSpace(m.Message), }) } userMessage := strings.TrimSpace(strings.Join(ctx.Args, " ")) messages = append(messages, afterHistory) kb := laniakea.NewInlineKeyboard(1).AddCallbackButton("Отменить", "rp.cancel") m := ctx.Keyboard("Генерация запущена...", kb) api := ai.NewOpenAIAPI(ai.GPTBaseUrl, "", rpUser.Model.Key) res, err := api.CreateCompletion(messages, userMessage, 1.0) if err != nil { ctx.Error(err) return } if len(res.Choices) == 0 { m.Edit("Не удалось сгенерировать ответ. Попробуйте снова позже") return } err = mdb.UpdateChatHistory(db, chatId, "user", userMessage) if err != nil { ctx.Error(err) return } agentAnswer := res.Choices[0].Message answerContent := strings.TrimSpace(agentAnswer.Content) err = mdb.UpdateChatHistory(db, chatId, agentAnswer.Role, answerContent) if err != nil { ctx.Error(err) } rpUser.UsedTokens = rpUser.UsedTokens + int64(len(userMessage)) + int64(len(answerContent)) tokens := redisRpRep.GetChatTokens(ctx.FromID, waifuId) tokens += len(userMessage) + len(answerContent) err = redisRpRep.SetChatTokens(ctx.FromID, waifuId, tokens) if err != nil { ctx.Error(err) } err = rpRep.UpdateUser(rpUser) if err != nil { ctx.Error(err) } m.Delete() ctx.Answer(laniakea.EscapeMarkdown(answerContent)) } func compress(ctx *laniakea.MsgContext, db *laniakea.DatabaseContext) { m := ctx.EditCallback("Запущено сжатие чата…", nil) redisRpRep := red.NewRPRepository(db) waifuId := redisRpRep.GetSelectedWaifu(ctx.FromID) if waifuId == 0 { ctx.Answer("Не выбрана вайфу") return } chatId := redisRpRep.GetChatId(ctx.FromID, waifuId) if chatId == "" { chatId = uuid.New().String() err := redisRpRep.SetChatId(ctx.FromID, waifuId, chatId) if err != nil { ctx.Error(err) return } } history, err := mdb.GetChatHistory(db, chatId) if err != nil { ctx.Error(err) return } messages := make([]ai.Message, 0) for _, m := range history { messages = append(messages, ai.Message{ Role: m.Role, Content: m.Message, }) } //compressModel := "anthropic/claude-sonnet-4" compressModel := "gpt-5.1" //compressModel := "deepseek-ai/deepseek-v3.2" api := ai.NewOpenAIAPI(ai.GPTBaseUrl, "", compressModel) res, err := api.CompressChat(messages) if err != nil { ctx.Error(err) } if len(res.Choices) == 0 { m.Edit("Не удалось сжать чат") return } compressedHistory := strings.TrimSpace(res.Choices[0].Message.Content) compressedHistory = strings.ReplaceAll(compressedHistory, "*", "") chatId = uuid.New().String() err = redisRpRep.SetChatId(ctx.FromID, waifuId, chatId) if err != nil { ctx.Error(err) return } err = mdb.UpdateChatHistory(db, chatId, "assistant", compressedHistory) if err != nil { ctx.Error(err) } offset := 20 if len(history) < 20 { offset = len(history) } for _, m := range history[len(history)-offset:] { err = mdb.UpdateChatHistory(db, chatId, m.Role, m.Message) if err != nil { ctx.Error(err) } } kb := laniakea.NewInlineKeyboard(1) kb = kb.AddCallbackButton("Назад", "rp.chat_stat") kb = kb.AddCallbackButton("На главную", "rp.info") ctx.EditCallback("Сжатие завершено", kb) } func generalClose(ctx *laniakea.MsgContext, _ *laniakea.DatabaseContext) { ctx.CallbackDelete() }