我实现了一键把当前页面和chatGPT的对话记录保存到org文件里

一直想把chatGPT的对话记录保存到org-mode,而不依赖官方的保存功能,这几天看到chatGPT不用账号不用注册打开直接就可以对话,很多人反馈不注册不能保存聊天记录,就感觉是时候实现这个功能了

效果图:

把chatGPT聊天记录导出到markdown这个事好像已经有不少人做过了,但导出org的似乎还没有。

遇到的主要难点是怎么把渲染后的html div转换成org格式,因为不知如何才能安全取到对话的json数据,只好采用html转org的方式。

还是用的浏览器扩展js+服务端php,我的实现方式也不是最佳的,代码问题还有很多要改善的,这次代码就暂时先不发出来了。可参照我之前写的 在org中切换浏览器标签页在org-mode中嵌入脚本语言 .

我也在想这个系列有没有必要继续写下去,不知道有多少人喜欢这种使用org的方式,如果有坛友也喜欢这种方式,希望看到更多后续文章和项目, 可以考虑给我打赏 , 让我更有动力写下去。

我觉得未来完全有可能做出一个具有通用性的开源工具,并且不依赖php,使用更流行的语言如go,python打包发布,实现在保留org-mode全部功能的前提下像使用web应用一样使用org-mode

4 个赞

所以兄弟你还在继续么?

现在准备做一个org-to-web的模板, 即在org里面写html,css,less,js,php, 然后浏览器访问这个org文件路由时整合里面的代码转换成网页形式,模板大概是这个样子

moban

现在的情况是比较勉强的能用, 距离具有通用性, 越折腾越感受到差得很远.

而保存GPT聊天记录, 没有继续做了, 本来准备参考 Notion 的做法.

我在运维一个ChatGPT镜像站,官方的share用户没法用,市面上的ChatGPT插件由都是仅限官网。所以Google到你这儿了

我一直用的是 pandoc 抓取页面转 org-mode 的,试了一下抓 chatgpt 的页面也没有问题

因为我一直用的 macOS,所以 OSA Script 来控制浏览器来完成 HTML 抓取,然后通过 org-protocol 输入到 Emacs

tell application (path to frontmost application as text)
	if name is equal to "Safari" then
		tell application "Safari"
			set title to quoted form of (do JavaScript "encodeURIComponent(document.title)" in current tab of first window)
			set link to quoted form of (do JavaScript "encodeURIComponent(window.location.href)" in current tab of first window)
			set body to quoted form of (do JavaScript "encodeURIComponent((function () {var html = \"\"; if (typeof window.getSelection != \"undefined\") {var sel = window.getSelection(); if (sel.rangeCount) {var container = document.createElement(\"div\"); for (var i = 0, len = sel.rangeCount; i < len; ++i) {container.appendChild(sel.getRangeAt(i).cloneContents());} html = container.innerHTML;}} else if (typeof document.selection != \"undefined\") {if (document.selection.type == \"Text\") {html = document.selection.createRange().htmlText;}} var relToAbs = function (href) {var a = document.createElement(\"a\"); a.href = href; var abs = a.protocol + \"//\" + a.host + a.pathname + a.search + a.hash; a.remove(); return abs;}; var elementTypes = [['a', 'href'], ['img', 'src']]; var div = document.createElement('div'); div.innerHTML = html; elementTypes.map(function(elementType) {var elements = div.getElementsByTagName(elementType[0]); for (var i = 0; i < elements.length; i++) {elements[i].setAttribute(elementType[1], relToAbs(elements[i].getAttribute(elementType[1])));}}); return div.innerHTML;})());" in current tab of first window)
			tell application "Emacs" to activate
			do shell script "export prefix=/usr/local; if [ -e /opt/homebrew ]; then prefix=/opt/homebrew; fi; $prefix/bin/emacsclient -e '(org-protocol-capture-html--with-pandoc-patch \"'" & "w" & "'\" \"'" & title & "'\" \"'" & link & "'\" \"'" & body & "'\")'"
			do shell script "export prefix=/usr/local; if [ -e /opt/homebrew ]; then prefix=/opt/homebrew; fi; $prefix/bin/terminal-notifier -title 'Page captured'  -sender org.gnu.Emacs -sound Purr"
		end tell
		
	else if name is equal to "Microsoft Edge" then
		tell application "Microsoft Edge"
			set xtitle to quoted form of (execute active tab of first window javascript "encodeURIComponent(document.title)")
			set xlink to quoted form of (execute active tab of first window javascript "encodeURIComponent(window.location.href)")
			set xbody to quoted form of (execute active tab of first window javascript "encodeURIComponent((function () {var html = \"\"; if (typeof window.getSelection != \"undefined\") {var sel = window.getSelection(); if (sel.rangeCount) {var container = document.createElement(\"div\"); for (var i = 0, len = sel.rangeCount; i < len; ++i) {container.appendChild(sel.getRangeAt(i).cloneContents());} html = container.innerHTML;}} else if (typeof document.selection != \"undefined\") {if (document.selection.type == \"Text\") {html = document.selection.createRange().htmlText;}} var relToAbs = function (href) {var a = document.createElement(\"a\"); a.href = href; var abs = a.protocol + \"//\" + a.host + a.pathname + a.search + a.hash; a.remove(); return abs;}; var elementTypes = [['a', 'href'], ['img', 'src']]; var div = document.createElement('div'); div.innerHTML = html; elementTypes.map(function(elementType) {var elements = div.getElementsByTagName(elementType[0]); for (var i = 0; i < elements.length; i++) {elements[i].setAttribute(elementType[1], relToAbs(elements[i].getAttribute(elementType[1])));}}); return div.innerHTML;})());")
			
			tell application "Emacs" to activate
			
			do shell script "export prefix=/usr/local; if [ -e /opt/homebrew ]; then prefix=/opt/homebrew; fi; $prefix/bin/emacsclient -e '(org-protocol-capture-html--with-pandoc-patch \"'" & "w" & "'\" \"'" & xtitle & "'\" \"'" & xlink & "'\" \"'" & xbody & "'\")'"
			do shell script "export prefix=/usr/local; if [ -e /opt/homebrew ]; then prefix=/opt/homebrew; fi; $prefix/bin/terminal-notifier -title 'Page captured'  -sender org.gnu.Emacs -sound Purr"
		end tell
	end if
end tell

遇到的主要难点是怎么把渲染后的html div转换成org格式,因为不知如何才能安全取到对话的json数据,只好采用html转org的方式

关于html转org的方案,我认为目前shrface的效果是最好的,利用了Emacs自身的shr和org。还有pandoc html转org效果一般。

可以参考以下代码:我自己利用shrface和plz下载网页,网页中的图片都能org中在线加载,代码块也正常用org格式,用denote重命名,不过没测试过chatgpt,下载一般网页不成问题:

(defun readit-to-org(url)
  "Download `url' to an org file"
  (interactive "sRequest url: ")
  (require 'shrface)
  (require 'denote)
  (require 'plz)
  (plz 'get url
    :headers '(("User-Agent" . "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"))
    :as 'buffer-string
    :then (lambda (html)
            (let* ((shrface-request-url url)
                   (dom (with-temp-buffer
                          (insert html)
                          (libxml-parse-html-region (point-min) (point-max))))
                   (shrface-org-title (let* ((title (cl-caddr (car (dom-by-tag dom 'title)))))
                                        (when title
                                          (readit--cleanup-title title))))
                   (name
                    (concat (expand-file-name shrface-org-title (expand-file-name "web" org-directory)) ".org")))
              (shrface-html-export-to-org html name)
              (denote-rename-file name shrface-org-title)))))

(defun readit-as-org(url)
  "Download `url' as an temporary org buffer"
  (interactive "sRequest url: ")
  (require 'shrface)
  (require 'denote)
  (require 'plz)
  (plz 'get url
    :headers '(("User-Agent" . "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"))
    :as 'buffer-string
    :then (lambda (html)
            (let* ((shrface-request-url url)
                   (dom (with-temp-buffer
                          (insert html)
                          (libxml-parse-html-region (point-min) (point-max))))
                   (shrface-org-title (let* ((title (cl-caddr (car (dom-by-tag dom 'title)))))
                                        (when title
                                          (readit--cleanup-title title))))
                   (name
                    (concat (expand-file-name shrface-org-title (expand-file-name "web" org-directory)) ".org")))
              (shrface-html-export-as-org html)))))

(defun readit-to-org-readable(url)
  "Download `url' readablely to an org file"
  (interactive "sRequest url: ")
  (require 'shrface)
  (require 'denote)
  (require 'plz)
  (require 'eww)
  (plz 'get url
    :headers '(("User-Agent" . "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"))
    :as 'buffer-string
    :then (lambda (html)
            (-let* ((shrface-request-url url)
                    (dom (with-temp-buffer
                           (insert html)
                           (libxml-parse-html-region (point-min) (point-max))))
                    ((title . readable) (readit--eww-readable dom))
                    (shrface-org-title (readit--cleanup-title (or title "")))
                    (name
                     (concat (expand-file-name shrface-org-title (expand-file-name "web" org-directory)) ".org")))
              (shrface-html-export-to-org readable name)
              (denote-rename-file name shrface-org-title)))))

(defun readit-as-org-readable(url)
  "Download `url' readablely as an org buffer"
  (interactive "sRequest url: ")
  (require 'shrface)
  (require 'denote)
  (require 'plz)
  (require 'eww)
  (plz 'get url
    :headers '(("User-Agent" . "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"))
    :as 'buffer-string
    :then (lambda (html)
            (-let* ((shrface-request-url url)
                    (dom (with-temp-buffer
                           (insert html)
                           (libxml-parse-html-region (point-min) (point-max))))
                    ((title . readable) (readit--eww-readable dom))
                    (shrface-org-title (readit--cleanup-title (or title "")))
                    (name
                     (concat (expand-file-name shrface-org-title (expand-file-name "web" org-directory)) ".org")))
              (shrface-html-export-as-org readable)
              (denote-rename-file name shrface-org-title)))))

(defun readit--cleanup-title (title)
  "Return TITLE with spurious whitespace removed."
  (->> title
       (s-replace "\n" " ")
       (s-trim)
       (s-collapse-whitespace)))

(defun readit--eww-readable (dom)
  "Return \"readable\" part of DOM with title.
Returns list (TITLE . HTML).  Based on `eww-readable'."
  (let ((title (cl-caddr (car (dom-by-tag dom 'title)))))
    (eww-score-readability dom)
    (cons title
          (with-temp-buffer
            (shr-dom-print (eww-highest-readability dom))
            (buffer-string)))))

1 个赞

期待后续,加油!