process_request.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package process
  2. import (
  3. "fmt"
  4. "html"
  5. "strings"
  6. "time"
  7. "github.com/eryajf/chatgpt-dingtalk/pkg/db"
  8. "github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
  9. "github.com/eryajf/chatgpt-dingtalk/pkg/logger"
  10. "github.com/eryajf/chatgpt-dingtalk/public"
  11. "github.com/solywsh/chatgpt"
  12. )
  13. // ProcessRequest 分析处理请求逻辑
  14. func ProcessRequest(rmsg *dingbot.ReceiveMsg) error {
  15. if CheckRequestTimes(rmsg) {
  16. content := strings.TrimSpace(rmsg.Text.Content)
  17. timeoutStr := ""
  18. if content != public.Config.DefaultMode {
  19. timeoutStr = fmt.Sprintf("\n\n>%s 后将恢复默认聊天模式:%s", FormatTimeDuation(public.Config.SessionTimeout), public.Config.DefaultMode)
  20. }
  21. switch content {
  22. case "单聊":
  23. public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), content)
  24. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("**[Concentrate] 现在进入与 %s 的单聊模式**%s", rmsg.SenderNick, timeoutStr))
  25. if err != nil {
  26. logger.Warning(fmt.Errorf("send message error: %v", err))
  27. }
  28. case "串聊":
  29. public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), content)
  30. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("**[Concentrate] 现在进入与 %s 的串聊模式**%s", rmsg.SenderNick, timeoutStr))
  31. if err != nil {
  32. logger.Warning(fmt.Errorf("send message error: %v", err))
  33. }
  34. case "重置", "退出", "结束":
  35. // 重置用户对话模式
  36. public.UserService.ClearUserMode(rmsg.GetSenderIdentifier())
  37. // 清空用户对话上下文
  38. public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier())
  39. // 清空用户对话的答案ID
  40. public.UserService.ClearAnswerID(rmsg.SenderNick, rmsg.GetChatTitle())
  41. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[RecyclingSymbol]已重置与**%s** 的对话模式\n\n> 可以开始新的对话 [Bubble]", rmsg.SenderNick))
  42. if err != nil {
  43. logger.Warning(fmt.Errorf("send message error: %v", err))
  44. }
  45. case "模板":
  46. var title string
  47. for _, v := range *public.Prompt {
  48. title = title + v.Title + " | "
  49. }
  50. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("%s 您好,当前程序内置集成了这些提示词:\n\n-----\n\n| %s \n\n-----\n\n您可以选择某个提示词作为对话内容的开头。\n\n以周报为例,可发送\"#周报 我本周用Go写了一个钉钉集成ChatGPT的聊天应用\",可将工作内容填充为一篇完整的周报。\n\n-----\n\n若您不清楚某个提示词的所代表的含义,您可以直接发送提示词,例如直接发送\"#周报\"", rmsg.SenderNick, title))
  51. if err != nil {
  52. logger.Warning(fmt.Errorf("send message error: %v", err))
  53. }
  54. case "图片":
  55. if public.Config.AzureOn {
  56. _, err := rmsg.ReplyToDingtalk(string(dingbot.
  57. MARKDOWN), "azure 模式下暂不支持图片创作功能")
  58. if err != nil {
  59. logger.Warning(fmt.Errorf("send message error: %v", err))
  60. }
  61. return err
  62. }
  63. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), "发送以 **#图片** 开头的内容,将会触发绘画能力,图片生成之后,将会通过消息回复给您。建议尽可能描述需要生成的图片内容及相关细节。\n 如果你绘图没有思路,可以在这两个网站寻找灵感。\n - [https://lexica.art/](https://lexica.art/)\n- [https://www.clickprompt.org/zh-CN/](https://www.clickprompt.org/zh-CN/)")
  64. if err != nil {
  65. logger.Warning(fmt.Errorf("send message error: %v", err))
  66. }
  67. case "余额":
  68. if public.JudgeAdminUsers(rmsg.SenderStaffId) {
  69. cacheMsg := public.UserService.GetUserMode("system_balance")
  70. if cacheMsg == "" {
  71. rst, err := public.GetBalance()
  72. if err != nil {
  73. logger.Warning(fmt.Errorf("get balance error: %v", err))
  74. return err
  75. }
  76. cacheMsg = rst
  77. }
  78. _, err := rmsg.ReplyToDingtalk(string(dingbot.TEXT), cacheMsg)
  79. if err != nil {
  80. logger.Warning(fmt.Errorf("send message error: %v", err))
  81. }
  82. }
  83. case "查对话":
  84. if public.JudgeAdminUsers(rmsg.SenderStaffId) {
  85. msg := "使用如下指令进行查询:\n\n---\n\n**#查对话 username:张三**\n\n---\n\n需要注意格式必须严格与上边一致,否则将会查询失败\n\n只有程序系统管理员有权限查询,即config.yml中的admin_users指定的人员。"
  86. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), msg)
  87. if err != nil {
  88. logger.Warning(fmt.Errorf("send message error: %v", err))
  89. }
  90. }
  91. default:
  92. if public.FirstCheck(rmsg) {
  93. return Do("串聊", rmsg)
  94. } else {
  95. return Do("单聊", rmsg)
  96. }
  97. }
  98. }
  99. return nil
  100. }
  101. // 执行处理请求
  102. func Do(mode string, rmsg *dingbot.ReceiveMsg) error {
  103. // 先把模式注入
  104. public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), mode)
  105. switch mode {
  106. case "单聊":
  107. qObj := db.Chat{
  108. Username: rmsg.SenderNick,
  109. Source: rmsg.GetChatTitle(),
  110. ChatType: db.Q,
  111. ParentContent: 0,
  112. Content: rmsg.Text.Content,
  113. }
  114. qid, err := qObj.Add()
  115. if err != nil {
  116. logger.Error("往MySQL新增数据失败,错误信息:", err)
  117. }
  118. reply, err := chatgpt.SingleQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
  119. if err != nil {
  120. logger.Info(fmt.Errorf("gpt request error: %v", err))
  121. if strings.Contains(fmt.Sprintf("%v", err), "maximum question length exceeded") {
  122. public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier())
  123. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 已超过最大文本限制,请缩短提问文字的字数。", err))
  124. if err != nil {
  125. logger.Warning(fmt.Errorf("send message error: %v", err))
  126. return err
  127. }
  128. } else {
  129. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err))
  130. if err != nil {
  131. logger.Warning(fmt.Errorf("send message error: %v", err))
  132. return err
  133. }
  134. }
  135. }
  136. if reply == "" {
  137. logger.Warning(fmt.Errorf("get gpt result falied: %v", err))
  138. return nil
  139. } else {
  140. reply = strings.TrimSpace(reply)
  141. reply = strings.Trim(reply, "\n")
  142. aObj := db.Chat{
  143. Username: rmsg.SenderNick,
  144. Source: rmsg.GetChatTitle(),
  145. ChatType: db.A,
  146. ParentContent: qid,
  147. Content: reply,
  148. }
  149. _, err := aObj.Add()
  150. if err != nil {
  151. logger.Error("往MySQL新增数据失败,错误信息:", err)
  152. }
  153. logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply))
  154. if public.JudgeSensitiveWord(reply) {
  155. reply = public.SolveSensitiveWord(reply)
  156. }
  157. // 回复@我的用户
  158. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(reply))
  159. if err != nil {
  160. logger.Warning(fmt.Errorf("send message error: %v", err))
  161. return err
  162. }
  163. }
  164. case "串聊":
  165. lastAid := public.UserService.GetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle())
  166. qObj := db.Chat{
  167. Username: rmsg.SenderNick,
  168. Source: rmsg.GetChatTitle(),
  169. ChatType: db.Q,
  170. ParentContent: lastAid,
  171. Content: rmsg.Text.Content,
  172. }
  173. qid, err := qObj.Add()
  174. if err != nil {
  175. logger.Error("往MySQL新增数据失败,错误信息:", err)
  176. }
  177. cli, reply, err := chatgpt.ContextQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
  178. if err != nil {
  179. logger.Info(fmt.Sprintf("gpt request error: %v", err))
  180. if strings.Contains(fmt.Sprintf("%v", err), "maximum text length exceeded") {
  181. public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier())
  182. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 串聊已超过最大文本限制,对话已重置,请重新发起。", err))
  183. if err != nil {
  184. logger.Warning(fmt.Errorf("send message error: %v", err))
  185. return err
  186. }
  187. } else {
  188. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err))
  189. if err != nil {
  190. logger.Warning(fmt.Errorf("send message error: %v", err))
  191. return err
  192. }
  193. }
  194. }
  195. if reply == "" {
  196. logger.Warning(fmt.Errorf("get gpt result falied: %v", err))
  197. return nil
  198. } else {
  199. reply = strings.TrimSpace(reply)
  200. reply = strings.Trim(reply, "\n")
  201. aObj := db.Chat{
  202. Username: rmsg.SenderNick,
  203. Source: rmsg.GetChatTitle(),
  204. ChatType: db.A,
  205. ParentContent: qid,
  206. Content: reply,
  207. }
  208. aid, err := aObj.Add()
  209. if err != nil {
  210. logger.Error("往MySQL新增数据失败,错误信息:", err)
  211. }
  212. // 将当前回答的ID放入缓存
  213. public.UserService.SetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle(), aid)
  214. logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, reply))
  215. if public.JudgeSensitiveWord(reply) {
  216. reply = public.SolveSensitiveWord(reply)
  217. }
  218. // 回复@我的用户
  219. _, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(reply))
  220. if err != nil {
  221. logger.Warning(fmt.Errorf("send message error: %v", err))
  222. return err
  223. }
  224. _ = cli.ChatContext.SaveConversation(rmsg.GetSenderIdentifier())
  225. }
  226. default:
  227. }
  228. return nil
  229. }
  230. // FormatTimeDuation 格式化时间
  231. // 主要提示单聊/群聊切换时多久后恢复默认聊天模式
  232. func FormatTimeDuation(duration time.Duration) string {
  233. minutes := int64(duration.Minutes())
  234. seconds := int64(duration.Seconds()) - minutes*60
  235. timeoutStr := ""
  236. if seconds == 0 {
  237. timeoutStr = fmt.Sprintf("%d分钟", minutes)
  238. } else {
  239. timeoutStr = fmt.Sprintf("%d分%d秒", minutes, seconds)
  240. }
  241. return timeoutStr
  242. }
  243. // FormatMarkdown 格式化Markdown
  244. // 主要修复ChatGPT返回多行代码块,钉钉会将代码块中的#当作Markdown语法里的标题来处理,进行转义;如果Markdown格式内存在html,将Markdown中的html标签转义
  245. // 代码块缩进问题暂无法解决,因不管是四个空格,还是Tab,在钉钉上均会顶格显示,建议复制代码后用IDE进行代码格式化,针对缩进严格的语言,例如Python,不确定的建议手机端查看下代码块的缩进
  246. func FormatMarkdown(md string) string {
  247. lines := strings.Split(md, "\n")
  248. codeblock := false
  249. existHtml := strings.Contains(md, "<")
  250. for i, line := range lines {
  251. if strings.HasPrefix(line, "```") {
  252. codeblock = !codeblock
  253. }
  254. if codeblock {
  255. lines[i] = strings.ReplaceAll(line, "#", "\\#")
  256. } else if existHtml {
  257. lines[i] = html.EscapeString(line)
  258. }
  259. }
  260. return strings.Join(lines, "\n")
  261. }
  262. // CheckRequestTimes 分析处理请求逻辑
  263. // 主要提供单日请求限额的功能
  264. func CheckRequestTimes(rmsg *dingbot.ReceiveMsg) bool {
  265. if public.Config.MaxRequest == 0 {
  266. return true
  267. }
  268. count := public.UserService.GetUseRequestCount(rmsg.GetSenderIdentifier())
  269. // 用户是管理员或VIP用户,不判断访问次数是否超过限制
  270. if public.JudgeAdminUsers(rmsg.SenderStaffId) || public.JudgeVipUsers(rmsg.SenderStaffId) {
  271. return true
  272. } else {
  273. // 用户不是管理员和VIP用户,判断访问次数是否超过限制
  274. if count >= public.Config.MaxRequest {
  275. logger.Info(fmt.Sprintf("亲爱的: %s,您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!", rmsg.SenderNick))
  276. _, err := rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Staple] **一个好的问题,胜过十个好的答案!** \n\n亲爱的%s:\n\n您今日请求次数已达上限,请明天再来,交互发问资源有限,请务必斟酌您的问题,给您带来不便,敬请谅解!\n\n如有需要,可联系管理员升级为VIP用户。", rmsg.SenderNick))
  277. if err != nil {
  278. logger.Warning(fmt.Errorf("send message error: %v", err))
  279. }
  280. return false
  281. }
  282. }
  283. // 访问次数未超过限制,将计数加1
  284. public.UserService.SetUseRequestCount(rmsg.GetSenderIdentifier(), count+1)
  285. return true
  286. }