让ChatGPT成为Emacs doctor

简单用doctor包装了一下ChatGPT,用M-x doctor就能和ChatGPT聊天了 :rofl: (需要安装并配置revChatGPT)。

效果如下:

2023-02-24 21.20.49

代码如下:

(defvar doctor-chatgpt-process nil)
(defvar doctor-chatgpt-replying nil)
(defvar doctor-chatgpt-ready nil)
(defvar doctor-chatgpt-recv-list nil)
(defvar doctor-chatgpt-send-list nil)

(defun doctor-chatgpt-filter (process output)
  "Filter for chatgpt process."
  (let ((buffer (process-buffer process)))
    (cond
     ((string-match "Logging in\.\.\." output)
      (setq doctor-chatgpt-ready t))
     ((not doctor-chatgpt-ready))
     ((equal output "Chatbot: \n")
      (setq doctor-chatgpt-replying t)
      (with-current-buffer "*doctor*" (read-only-mode 1)))
     (t
      (when-let* ((el (string-match "\n+You:\n+$" output)))
        (setq doctor-chatgpt-replying nil)
        (setq output (substring output 0 el)))
      (when (> (length output) 1) (push output doctor-chatgpt-recv-list))
      (with-current-buffer "*doctor*"
        (read-only-mode -1)
        (goto-char (point-max))
        (insert (if (eq (length doctor-chatgpt-recv-list) 1)
                    (string-replace output (string-trim (nth 0 doctor-chatgpt-recv-list)) "")
                  output))
        (if doctor-chatgpt-replying
            (read-only-mode 1)
          (if doctor-chatgpt-recv-list (insert "\n\n"))))))))

(defun doctor-chatgpt-start-process ()
  "Start a chat with ChatGPT."
  (when (and (processp doctor-chatgpt-process) (process-live-p doctor-chatgpt-process))
    (kill-process doctor-chatgpt-process))
  (setq doctor-chatgpt-recv-list nil)
  (setq doctor-chatgpt-send-list nil)

  (setq doctor-chatgpt-process
        (start-process "*doctor-chatgpt*" "*doctor-chatgpt*"
                       "python" "-m" "revChatGPT.V1"))
  (setq doctor-chatgpt-ready nil)
  (set-process-filter doctor-chatgpt-process 'doctor-chatgpt-filter))

(defun doctor-chatgpt-read-print ()
  "Top level loop."
  (interactive nil doctor-mode)
  (setq doctor-sent (save-excursion
                      (backward-sentence 1)
                      (buffer-substring-no-properties (point) (point-max))))
  (insert "\n")
  (push doctor-sent doctor-chatgpt-send-list)
  (setq doctor-chatgpt-replying t)
  (process-send-string doctor-chatgpt-process (concat doctor-sent "\n")))

(advice-add 'doctor :before #'doctor-chatgpt-start-process)
(advice-add 'doctor-read-print :override #'doctor-chatgpt-read-print)

10 个赞

一般我都是在这种时间用用 chatgpt,负载高了很容易断开连接或者根本进不去了。

之前我拿doctor为例安利emacs,被doctor的反复复读尬在原地了

想到了白色相簿的梗😂

非常感谢楼主提供的思路。我稍微改进了下,把 doctor 的基础功能搬到了 markdown-mode 里面,这样就不用依赖 doctor 而且可以用 markdown 高亮输出了。

用法是 M-x doctor-chatgpt 即可。 不过目前还有点小问题,比如超时错误进程挂掉不会自动重启。

;; https://emacs-china.org/t/chatgpt-emacs-doctor/23773
;; https://github.com/acheong08/ChatGPT

(defvar doctor-chatgpt-buffer-name "*doctor-chatgpt*")
(defvar doctor-chatgpt-process-buffer-name "*doctor-chatgpt-process*")
(defvar doctor-chatgpt-process nil)
(defvar doctor-chatgpt-replying nil)
(defvar doctor-chatgpt-ready nil)
(defvar doctor-chatgpt-recv-list nil)
(defvar doctor-chatgpt-send-list nil)

(defun doctor-chatgpt-filter (process output)
  "Filter for chatgpt process."
  (message "doctor-chatgpt-filter: %s" output)
  (let ((buffer (process-buffer process)))
    (cond
     ((string-match "Logging in\.\.\." output)
      (setq doctor-chatgpt-ready t))
     ((not doctor-chatgpt-ready))
     ((equal output "Chatbot: \n")
      (setq doctor-chatgpt-replying t)
      (with-current-buffer doctor-chatgpt-buffer-name (read-only-mode 1)))
     (t
      (when-let* ((el (string-match "\n+You:\n+$" output)))
        (setq doctor-chatgpt-replying nil)
        (setq output (substring output 0 el)))
      (when (> (length output) 1) (push output doctor-chatgpt-recv-list))
      (with-current-buffer doctor-chatgpt-buffer-name
        (read-only-mode -1)
        (goto-char (point-max))
        ;; HACK: don't know why it will repeat the first send, so remove it
        (insert
         (if (eq (length doctor-chatgpt-recv-list) 1)
             (string-replace (string-trim (nth 0 doctor-chatgpt-send-list)) "" output)
           output))
        (if doctor-chatgpt-replying
            (read-only-mode 1)
          (if doctor-chatgpt-recv-list (insert "\n\n"))))))))

(defun doctor-chatgpt-start-process ()
  "Start a chat with ChatGPT."
  (when (and (processp doctor-chatgpt-process)
             (process-live-p doctor-chatgpt-process))
    (kill-process doctor-chatgpt-process))
  (setq doctor-chatgpt-recv-list nil)
  (setq doctor-chatgpt-send-list nil)

  (setq doctor-chatgpt-process
        (start-process
         doctor-chatgpt-process-buffer-name
         doctor-chatgpt-process-buffer-name
         "python" "-m" "revChatGPT.V1"))
  (setq doctor-chatgpt-ready nil)
  (set-process-sentinel doctor-chatgpt-process #'doctor-chatgpt-process-sentinel)
  (set-process-filter doctor-chatgpt-process #'doctor-chatgpt-filter))

(defun doctor-chatgpt-process-sentinel (process event)
  "Sentinel for chatgpt process.
PROCESS is the process that changed.
EVENT is a string describing the change."
  (setq doctor-chatgpt-ready nil)
  (message "%s end with the event '%s'" process event))

(defun doctor-chatgpt-ret-or-read (arg)
  "Insert a newline if preceding character is not a newline.
Otherwise call the Doctor to parse preceding sentence.
ARG will be passed to `newline'."
  (interactive "*p" doctor-chatgpt-mode)
  (if (= (preceding-char) ?\n)
      (doctor-chatgpt-read-print)
    (newline arg)))

(defun doctor-chatgpt-read-print ()
  "Top level loop."
  (interactive nil doctor-chatgpt-mode)
  ;; send the sentence before point
  (let ((doctor-sent
         (save-excursion
           (backward-sentence 1)
           (buffer-substring-no-properties (point) (point-max)))))
    (insert "\n")
    (push doctor-sent doctor-chatgpt-send-list)
    (setq doctor-chatgpt-replying t)
    (process-send-string doctor-chatgpt-process (concat doctor-sent "\n"))))

(defvar-keymap doctor-chatgpt-mode-map
  "C-j" #'doctor-chatgpt-read-print
  "RET" #'doctor-chatgpt-ret-or-read)

(define-derived-mode doctor-chatgpt-mode markdown-mode "Doctor ChatGPT"
  "Major mode for running the ChatGPT.
Like Text mode with Auto Fill mode
except that RET when point is after a newline, or LFD at any time,
reads the sentence before point, and prints the ChatGPT's answer."
  :interactive nil
  (setq-local word-wrap-by-category t)
  (visual-line-mode)
  (insert "Hi. I am the ChatGPT. Please ask me anything, each time you are finished talking, type RET twice.")
  (insert "\n\n"))

(defun doctor-chatgpt ()
  "Switch to `doctor-chatgpt-buffer' and start talking with ChatGPT."
  (interactive)
  (doctor-chatgpt-start-process)
  (switch-to-buffer doctor-chatgpt-buffer-name)
  (doctor-chatgpt-mode))

(provide 'doctor-chatgpt)
;;; doctor-chatgpt.el ends here
2 个赞

太好了!:+1: 虽然我的 ip 已经被封了

更正一下:是 revChatGPT 出了问题

我 fork 了一个项目 GitHub - carlos-wong/ChatGPT.el: ChatGPT in Emacs , 作者是用 chatgpt-query 不是 stream 的。 我加了 chatgpt–query-stream 增加流式的。不过一些基本功能还在完善,比如重入会话会导致冲突。

我还加了一个自动记录 log 的功能,这样就可以先搜自己的 log ,再去问 chatgpt。

欢迎大家提意见

3 个赞

因为我使用 evil, 自己用go 写了一个chatgpt 的客户端, 简单包装了一下

1 个赞

這個也蠻好用

1 个赞

新出的ChatGPT API速度感觉比原来快了得有五六倍(不过就是得收费 :rofl:

2023-03-02 18.38.51

4 个赞

这个是使用新 API 的版本,速度贼快!!!
顺便改进了一下代码逻辑,完善了文档

;;; package --- Ask ChatGPT in Emacs        -*- lexical-binding: t; -*-

;;; Commentary:

;; Before using this package, you need to install the revChatGPT
;; and set the API key with the `auth-source' package.
;; You can use pip to install the revChatGPT:
;;
;; python -m pip install revChatGPT
;;
;; And you can find more details in the `doctor-chatgpt-api-token'.
;; After that, you can use `doctor-chatgpt' to ask ChatGPT.

;;; Links:

;; https://emacs-china.org/t/chatgpt-emacs-doctor/23773
;; https://github.com/acheong08/ChatGPT

;;; Code:

(require 'markdown-mode)

(defvar doctor-chatgpt-buffer-name "*doctor-chatgpt*")
(defvar doctor-chatgpt-process-buffer-name "*doctor-chatgpt-process*")
(defvar doctor-chatgpt-process nil)
(defvar doctor-chatgpt-replying nil)
(defvar doctor-chatgpt-recv-list nil)
(defvar doctor-chatgpt-send-list nil)

(defcustom doctor-chatgpt-revchatgpt-version "v3"
  "Choose the version of revChatGPT.
See https://github.com/acheong08/ChatGPT#installation"
  :type 'string
  :options '("v1" "v3")
  :group 'doctor-chatgpt)

(defun doctor-chatgpt-revchatgpt-program ()
  "Return the start script of the revChatGPT program."
  (cond
   ((string= doctor-chatgpt-revchatgpt-version "v1")
    '("python" "-m" "revChatGPT.V1"))
   ((string= doctor-chatgpt-revchatgpt-version "v3")
    `("python" "-m" "revChatGPT.V3" "--api_key"
      ,(doctor-chatgpt-api-token)))))

(defun doctor-chatgpt-revchatgpt-user-prompt ()
  "Return the user prompt."
  (cond
   ((string= doctor-chatgpt-revchatgpt-version "v1")
    "\n+You:\n+$")
   ((string= doctor-chatgpt-revchatgpt-version "v3")
    "\n+User: \n+$")))

(defun doctor-chatgpt-revchatgpt-chatgpt-prompt ()
  "Return the ChatGPT prompt."
  (cond
   ((string= doctor-chatgpt-revchatgpt-version "v1")
    "Chatbot: \n+$")
   ((string= doctor-chatgpt-revchatgpt-version "v3")
    "ChatGPT: \n+$")))

(defun doctor-chatgpt-api-token ()
  "Get the API token from the authinfo.
See https://platform.openai.com/account/api-keys"
  (auth-source-pick-first-password :host "openai.com" :user "chatgpt"))

(defun doctor-chatgpt--insert-line (char)
  "Insert a line with CHAR."
  (insert "\n\n")
  (insert
   (propertize
    (make-string 80 char)
    'font-lock-face 'font-lock-comment-face))
  (insert "\n\n"))

(defun doctor-chatgpt--process-filter (_ output)
  "Filter for chatgpt process.
OUTPUT is the string output we need to handle."
  (cond
   ((string-match-p (doctor-chatgpt-revchatgpt-chatgpt-prompt) output)
    (setq doctor-chatgpt-replying t))
   ((string-match-p (doctor-chatgpt-revchatgpt-user-prompt) output)
    (with-current-buffer doctor-chatgpt-buffer-name
      (let ((inhibit-read-only t))
        (goto-char (point-max))
        ;; maybe still have answer output before "User: "
        (when-let* ((_ doctor-chatgpt-replying)
                    (index (string-match (doctor-chatgpt-revchatgpt-user-prompt) output)))
          (insert (substring output 0 index)))
        (doctor-chatgpt--insert-line ?\─))
      (setq doctor-chatgpt-replying nil)
      (read-only-mode 0)))
   (t
    (when doctor-chatgpt-replying ; ignore other output
      (when (> (length output) 1) (push output doctor-chatgpt-recv-list))
      ;; insert answer output to the doctor buffer
      (with-current-buffer doctor-chatgpt-buffer-name
        (let ((inhibit-read-only t))
          (goto-char (point-max))
          (insert output)))))))

(defun doctor-chatgpt--start-process ()
  "Start a chat with ChatGPT."
  (doctor-chatgpt--kill-process)
  (setq doctor-chatgpt-recv-list nil)
  (setq doctor-chatgpt-send-list nil)
  (setq doctor-chatgpt-process
        (let ((process-environment (copy-sequence process-environment)))
          (setenv "NO_COLOR" "true")
          (apply #'start-process
                 `(,doctor-chatgpt-process-buffer-name
                   ,doctor-chatgpt-process-buffer-name
                   ,@(doctor-chatgpt-revchatgpt-program)))))
  (set-process-sentinel doctor-chatgpt-process #'doctor-chatgpt--process-sentinel)
  (set-process-filter doctor-chatgpt-process #'doctor-chatgpt--process-filter))

(defun doctor-chatgpt--process-sentinel (process event)
  "Sentinel for chatgpt process.
PROCESS is the process that changed.
EVENT is a string describing the change."
  (message "%s end with the event %s" process event))

(defun doctor-chatgpt-ret-or-read (arg)
  "Insert a newline if preceding character is not a newline.
Otherwise call the Doctor to parse preceding sentence.
ARG will be passed to `newline'."
  (interactive "*p" doctor-chatgpt-mode)
  (if (= (preceding-char) ?\n)
      (doctor-chatgpt-read-print)
    (newline arg)))

(defun doctor-chatgpt-read-print ()
  "Top level loop."
  (interactive nil doctor-chatgpt-mode)
  ;; send the sentence before point
  (let ((doctor-sent
         (save-excursion
           (backward-sentence 1)
           (buffer-substring-no-properties (point) (point-max)))))
    (insert "\n")
    (push doctor-sent doctor-chatgpt-send-list)
    (with-current-buffer doctor-chatgpt-buffer-name (read-only-mode 1))
    (process-send-string doctor-chatgpt-process (concat doctor-sent "e
"))))

(defun doctor-chatgpt-restart ()
  "Restart process manually when there is something wrong."
  (interactive)
  (with-current-buffer doctor-chatgpt-buffer-name
    (let ((inhibit-read-only t))
      (goto-char (point-max))
      (insert "\n\n")
      (doctor-chatgpt--insert-line ?\═)
      (insert "Restarting process..."))
    (read-only-mode 0)
    (doctor-chatgpt--start-process)))

(defun doctor-chatgpt-exit ()
  "Kill the `doctor-chatgpt-process' with buffers.
`doctor-chatgpt-process-buffer-name' and
`doctor-chatgpt-buffer-name'."
  (interactive)
  (kill-buffer doctor-chatgpt-buffer-name)
  (let ((kill-buffer-query-functions nil))
    (kill-buffer doctor-chatgpt-process-buffer-name)))

(defun doctor-chatgpt--kill-process ()
  "Kill the `doctor-chatgpt-process'."
  (when (and (processp doctor-chatgpt-process)
             (process-live-p doctor-chatgpt-process))
    (kill-process doctor-chatgpt-process)))

(defvar-keymap doctor-chatgpt-mode-map
  "RET" #'doctor-chatgpt-ret-or-read)

(define-derived-mode doctor-chatgpt-mode markdown-mode "Doctor ChatGPT"
  "Major mode for running the ChatGPT.
Like Text mode with Auto Fill mode
except that RET when point is after a newline, or LFD at any time,
reads the sentence before point, and prints the ChatGPT's answer."
  :interactive nil
  (setq-local word-wrap-by-category t)
  (visual-line-mode)
  (insert "Hi. I am the ChatGPT. Please ask me anything, each time you are finished talking, type RET twice."))

(defun doctor-chatgpt ()
  "Switch to `doctor-chatgpt-buffer' and start talking with ChatGPT."
  (interactive)
  (doctor-chatgpt--start-process)
  (switch-to-buffer doctor-chatgpt-buffer-name)
  (doctor-chatgpt-mode))

(provide 'doctor-chatgpt)
;;; doctor-chatgpt.el ends here
4 个赞

请问一下,~/.authinfo 文件中对应行应该长什么样啊?

我设置成这样

machine openai.com login chatgpt password ***(api key)

然后运行

doctor-chatgpt

报错如下

make-process--with-editor-process-filter: Wrong type argument: stringp, nil

你可以执行 (doctor-chatgpt-api-token) 看看有没有返回正确的值。

如果不对的话可以看看是不是 authinfo 配置问题,比如读的文件路径不对

抱歉小白的水平还处在抄代码的阶段,

您的这段代码, 我试着加到 doom emacs 的 config.el 里面了,

但是, 没看明白, 应该在哪儿填写 openAI key?

另外, 代理设置怎么办?

你可以把 token 写在 .authinfo 文件里,这个用的是 auth-source 这个包,具体设置办法可以看下 auth-source 的文档

代理目前没有办法,想办法设置全局代理吧。

多谢, 看样子, 现在仍然要想办法解决代理才好玩.

发现用 posframe 来调用 doctor-chatgpt 很方便:

Peek 2023-03-08 16-45

代码如下:

(defvar doctor-chatgpt-pop--frame nil)

(defun doctor-chatgpt-posframe-hidehandler (_)
  "Hidehandler used by `shell-pop-posframe-toggle'."
  (not (eq (selected-frame) doctor-chatgpt-pop--frame)))

;;;###autoload
(defun doctor-chatgpt-pop-posframe-toggle ()
  "Toggle shell in child frame."
  (interactive)
  ;; Shell pop in child frame
  (unless (and doctor-chatgpt-pop--frame
			   (frame-live-p doctor-chatgpt-pop--frame)
			   (process-live-p doctor-chatgpt-process))
	(let ((width  (max 100 (round (* (frame-width) 0.62))))
		  (height (round (* (frame-height) 0.62)))
		  (buffer doctor-chatgpt-buffer-name))
	  (setq doctor-chatgpt-pop--frame
			(posframe-show
			 buffer
			 :poshandler #'posframe-poshandler-frame-center
			 :hidehandler #'doctor-chatgpt-posframe-hidehandler
			 :left-fringe 8
			 :right-fringe 8
			 :width width
			 :height height
			 :min-width width
			 :min-height height
			 :internal-border-width 3
			 :background-color (face-background 'tooltip nil t)
			 :override-parameters '((cursor-type . t))
			 :accept-focus t))

	  (with-current-buffer buffer
		(doctor-chatgpt--start-process)
		(doctor-chatgpt-mode)
		(goto-char (point-max)))))

  ;; Focus in child frame
  (select-frame-set-input-focus doctor-chatgpt-pop--frame)
  (setq-local cursor-type 'box)
  (goto-char (point-max)))

;;;###autoload
(defun eli/doctor-chatgpt-quit ()
  (interactive)
  (let ((frame (selected-frame)))
	(if (frame-parameter frame 'posframe-buffer)
		(progn
		  (posframe--make-frame-invisible frame)
		  (when current-prefix-arg
			(doctor-chatgpt-exit)))
	  (keyboard-quit))))

(define-key doctor-chatgpt-mode-map (kbd "C-g") #'eli/doctor-chatgpt-quit)
6 个赞

安装好后,chatbot为啥会没反应?但是revchatgpt是可以运行

可能是转义符的问题

按照 revchatgpt 的描述,是要 ESC + RET,论坛里的显示不是 literal 的,所以直接复制会有问题。

1 个赞

忘了还有这个问题 :smiling_face_with_tear: