kimi.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package kimi
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "git.ttmylife.com/zeuszhao/llm"
  7. "github.com/go-rod/rod"
  8. "github.com/go-rod/rod/lib/launcher"
  9. "github.com/go-rod/rod/lib/proto"
  10. "time"
  11. )
  12. type Kimi struct {
  13. browser *rod.Browser
  14. launcher *launcher.Launcher
  15. loginEvent func(ctx context.Context, bytes []byte) error
  16. loginCallback func(ctx context.Context, user *llm.UserContext) error
  17. }
  18. func (k *Kimi) WithLoginEvent(f func(ctx context.Context, bytes []byte) error) llm.UserBrowserAgent {
  19. k.loginEvent = f
  20. return k
  21. }
  22. func (k *Kimi) WithLoginCallback(f func(ctx context.Context, user *llm.UserContext) error) llm.UserBrowserAgent {
  23. k.loginCallback = f
  24. return k
  25. }
  26. func (k *Kimi) IsLogin(ctx context.Context, page *rod.Page) (bool, error) {
  27. _, err := page.Timeout(3 * time.Second).Element("div[data-testid='msh-header-user-avatar']")
  28. if err != nil && !errors.Is(err, context.DeadlineExceeded) {
  29. return false, err
  30. }
  31. if errors.Is(err, context.DeadlineExceeded) {
  32. return false, nil
  33. }
  34. return true, nil
  35. }
  36. func (k *Kimi) Login(ctx context.Context, page *rod.Page) error {
  37. loginEle, err := page.Element("div[data-testid='msh-sidebar-user']")
  38. if err != nil {
  39. return err
  40. }
  41. err = loginEle.Click(proto.InputMouseButtonLeft, 1)
  42. if err != nil {
  43. return err
  44. }
  45. qrcodePreEle, err := page.ElementR("div p", "微信扫码登录")
  46. if err != nil {
  47. return err
  48. }
  49. qrcodeBox, err := qrcodePreEle.Parent()
  50. if err != nil {
  51. return err
  52. }
  53. screenshot, err := qrcodeBox.Screenshot(proto.PageCaptureScreenshotFormatPng, 10)
  54. if err != nil {
  55. return err
  56. }
  57. err = k.loginEvent(ctx, screenshot)
  58. if err != nil {
  59. return err
  60. }
  61. _, err = page.Element("div[data-testid='msh-header-user-avatar']")
  62. if err != nil {
  63. return err
  64. }
  65. localStorage := page.MustEval(`k => Object.assign({}, window.localStorage)`).Map()
  66. localStorageMap := make(map[string]string)
  67. for k, v := range localStorage {
  68. localStorageMap[k] = v.String()
  69. }
  70. err = k.loginCallback(ctx, llm.NewUserContext().WithLocalStorage(k.GetName(), localStorageMap).WithCookies(k.GetName(), proto.CookiesToParams(page.MustCookies())))
  71. if err != nil {
  72. return err
  73. }
  74. return nil
  75. }
  76. func (k *Kimi) GetName() string {
  77. return "kimi"
  78. }
  79. func (k *Kimi) NewChat(ctx context.Context, user *llm.UserContext) (llm.Chat, error) {
  80. c := &Chat{
  81. ai: k,
  82. Page: k.browser.Context(ctx).MustIncognito().MustPage(),
  83. qaSingle: make(chan bool),
  84. }
  85. wait := c.Page.MustWaitNavigation()
  86. // hook
  87. router := c.Page.HijackRequests()
  88. router.MustAdd("*.mp4", func(hijack *rod.Hijack) {
  89. if hijack.Request.Type() == proto.NetworkResourceTypeMedia {
  90. hijack.Response.Fail(proto.NetworkErrorReasonBlockedByClient)
  91. return
  92. }
  93. hijack.ContinueRequest(&proto.FetchContinueRequest{})
  94. })
  95. router.MustAdd("*/completion/stream", func(hijack *rod.Hijack) {
  96. hijack.MustLoadResponse()
  97. c.qaSingle <- true
  98. })
  99. go router.Run()
  100. err := c.Page.Navigate("https://kimi.moonshot.cn/")
  101. if err != nil {
  102. return nil, err
  103. }
  104. localStorage, ok := user.LocalStorage[k.GetName()]
  105. if ok {
  106. for key, val := range localStorage {
  107. c.Page.MustEval(fmt.Sprintf("() => {window.localStorage.setItem('%s','%s')}", key, val))
  108. }
  109. }
  110. wait()
  111. return c, nil
  112. }
  113. func (k *Kimi) Close(ctx context.Context) error {
  114. k.launcher.Kill()
  115. return nil
  116. }
  117. func (k *Kimi) Init(ctx context.Context) (llm.AI, error) {
  118. path, ok := launcher.LookPath()
  119. if !ok {
  120. return nil, errors.New("浏览器未找到")
  121. }
  122. launcherHandler := launcher.New().
  123. Bin(path).
  124. HeadlessNew(true).
  125. Set("disable-gpu").
  126. Devtools(false)
  127. browser := rod.New().ControlURL(launcherHandler.MustLaunch()).MustConnect()
  128. return &Kimi{
  129. browser: browser,
  130. launcher: launcherHandler,
  131. }, nil
  132. }
  133. func NewKimi() llm.AI {
  134. return &Kimi{}
  135. }