分享下用org-roam和zotero/zotxt联动阅读文献的配置

分享一下用双链管理文章或者其他 node 的配置~

最近要读很多文献,需要做一些简练的笔记,并且对这些文献进行分类,所以想到了用双链来管理这些文献和笔记。

例如我当前调研的方向是 A 综述,那么我会创建一个文件 a-survey.org ,内容大致如下

* Content 
A is ... 
A is not ...
A has the following characteristics ...

* Categories
** PAPER TAG A-1
:PROPERTIES:
:ID:       xxxx
:END:
** PAPER TAG A-2
:PROPERTIES:
:ID:       xxxx
:END:

* Papers 
** PAPER T1 
[[id:yyyy][Paper T1 Short Name]]

这篇文章 T1 可以通过自定义的 org-roam-capture 从 zotero 经过 zotxt 拉过来,会放到对应的文件夹里

:PROPERTIES:
:ID:       yyyy
:ZOTERO_LINK: [[zotero://select/items/1_xxx][@zotero-cite-key]]
:ROAM_ALIASES: Short Title of Paper T1
:END:
#+TITLE: PAPER: Efficient Item ID Generation for Large-Scale LLM-based Recommendation
* Meta Info
[[id:xxxx][PAPER TAG A-1]]

这样,在逻辑上就有了这样的一个链接图,将分类这些 tag 直接体现在了链接图中,可以方便的通过 org-roam-ui 进行观察。

survey 
| - PAPER TAG A-1
|   | - PAPER T1
| - PAPER TAG A-2
...

这里是一个可视化的结果:

此外,还有个点是可以自定义 org-export-latex 对链接的处理,如果发现插入的链接带有 ZOTERO_LINK 这个属性,那么就通过 zotxt 的 API 去查询实际的 bibkey ,省去了手动维护 bib 文件的麻烦。


最后附上代码,小白代码请轻喷~

         :desc "org roam capture paper" "p" #'jump-to-zotxt-note-by-search
(defun jump-to-zotxt-note-by-search ()
    "Go to Zotero note via searching. Create the note file if it does not exist"
    (interactive)
    (deferred:$
     ;; step1: search for Zotero items
     (zotxt-search-deferred :title-creator-year)
     (deferred:nextc it
                     (lambda (items)
                       (when (null items)
                         (error "No Zotero items found."))
                       (car items)))

     ;; step2: get the full item details
     (deferred:nextc it
                     (lambda (item)
                       (zotxt-get-item-deferred item :full)))

     ;; step3: generate note file path and pass context
     (deferred:nextc it
                     (lambda (full-item)
                       (let* ((item-key (plist-get full-item :key))
                              (note-directory org-zotxt-notes-directory)
                              (note-file (concat note-directory item-key ".org")))
                         (make-directory note-directory t)
                         (cons full-item note-file))))

     ;; step4: create or open the note file
     (deferred:nextc it
                     (lambda (full-item-note-file)
                       (let* (
                              (full-item (car full-item-note-file))
                              (note-file (cdr full-item-note-file))
                              (json-object-type 'hash-table)
                              (json-array-type 'list)
                              (json-key-type 'string)
                              (json-data (json-read-from-string (plist-get full-item :full)))
                              (item-data (if (listp json-data) (car json-data) json-data))
                              (item-title (gethash "title" item-data))
                              )
                         (make-directory org-zotxt-notes-directory t)
                         (if (file-exists-p note-file)
                             (find-file note-file)
                           (with-temp-file note-file
                             (insert (format "#+TITLE: %s\n" (concat "PAPER: " item-title)))
                             (insert (format "#+DATE: %s\n" (format-time-string "%Y-%m-%d %H:%M")))
                             (insert "* Meta Info\n")
                             ))
                         (find-file note-file)
                         (setq org-roam-zotero-note--file-name note-file)
                         full-item
                         )
                       ))

     ;; step5: get the properties and paths
     (deferred:nextc it
                     (lambda (item)
                       (zotxt-get-item-deferred item :paths)))
     (deferred:nextc it
                     (lambda (item)
                       ;; (message (prin1-to-string item))
                       (org-zotxt-get-item-link-text-deferred item)))
     (deferred:nextc it
                     (lambda (resp)
                       (find-file org-roam-zotero-note--file-name)
                       (goto-char (point-min))
                       (if-let ((id (org-entry-get (point) "ID")))
                           (message "id found in the DB, jump to it directly")
                         (progn
                           (org-entry-put (point) "ID" (org-id-new))
                           (org-entry-put (point) org-zotxt-noter-zotero-link (org-zotxt-make-item-link resp))
                           (org-roam-alias-add
                            (cdr (assq 'title-short
                                       (aref (json-read-from-string (plist-get resp :full)) 0))))
                           (org-roam-db-sync)
                           )
                         )
                       ))


     ;; step6: error handling
     (deferred:error it
                     (lambda (err)
                       (message "Zotxt Failed for : %s" (error-message-string err))
                       ;; (signal 'user-error (list "Canceled"))
                       ))
     ))

(defun extract-zotero-link-from-path (zotero-link-with-desc)
    ;; "[[zotero://select/items/1_BYJ73MHG][@llama3-grattafioriLlama3Herd2024]]"
    ;; "[[zotero://select/items/1_BYJ73MHG]]"
    (if (string-match "\\(zotero://[^]]+\\)" zotero-link-with-desc)
        (match-string 1 zotero-link-with-desc)
      nil)
    )


  (defun extract-zotero-desc-from-path (zotero-link-with-desc)
    "Extract description from Zotero link.
Example:
\"[[zotero://select/items/1_BYJ73MHG][@llama3-grattafioriLlama3Herd2024]]\"
  -> \"@llama3-grattafioriLlama3Herd2024\"
If no description exists (e.g. \"[[zotero://select/items/1_BYJ73MHG]]\"),
return nil."
    (when (string-match "\\[\\[zotero://.*\\]\\[\\(.*\\)\\]\\]" zotero-link-with-desc)
      (match-string 1 zotero-link-with-desc)))


  (defun org-id-of-zotero-note-export-maybe (path desc format)
    "Export function for org-id links that may contain Zotero links."

    (when (eq format 'latex)
      (let ((file-name (car (org-roam-id-find path))))
        (if (and file-name (file-exists-p file-name))
            (with-current-buffer (find-file-noselect file-name)
              (save-excursion
                (goto-char (point-min))
                (let* (
                       (zotero-link-prop (org-entry-get (point) org-zotxt-noter-zotero-link))
                       )
                  (if zotero-link-prop
                      (progn
                        (org-zotxt--link-export (substring (extract-zotero-link-from-path zotero-link-prop) 8) (or (extract-zotero-desc-from-path zotero-link-prop) desc) format)
                        )
                    nil
                    ))

                ))
          nil
          )))
    )

  (defun org-zotxt-get-cite-key-from-zotero-id (zurl)
    (let* ((zurl-id (substring zurl 22))
           (response (request
                       (format "%s/items" zotxt-url-base)
                       :params `(("key" . ,zurl-id)
                                 ("format" . "citekey"))
                       :sync t
                       :parser 'json-read))
           (data (request-response-data response))
           (result (if (and (vectorp data) (> (length data) 0))
                       (aref data 0)
                     (error "Unexpected response: empty or not a vector"))))
      (message "Citation key: %s" result)
      result))

  (defun org-zotero-update-citekey-inline (&optional filename)
    "Update the Zotero citekey of the current org file."
    (interactive)
    (let ((file (or filename (buffer-file-name))))
      (unless file
        (error "No file is associated with this buffer"))
      (message "Updating Zotero citekey for file: %s" file)
      (with-current-buffer (find-file-noselect file)
        (save-excursion
          (goto-char (point-min))
          (let* ((zurl-with-desc (org-entry-get nil org-zotxt-noter-zotero-link))
                 (zurl (when zurl-with-desc
                         (extract-zotero-link-from-path zurl-with-desc)))
                 (cite-key (when zurl
                             (org-zotxt-get-cite-key-from-zotero-id zurl)))
                 (new-link (when cite-key
                             (org-link-make-string zurl (concat "@" cite-key)))))
            (if cite-key
                (progn
                  (message "Zotero link: %s" zurl)
                  (message "Cite key: %s" cite-key)
                  (org-entry-put nil org-zotxt-noter-zotero-link new-link)
                  (save-buffer)
                  (message "Citekey updated to: %s" cite-key))
              (message "No cite-key found for Zotero link: %s" zurl)))))))


  (add-hook 'org-zotxt-mode-hook
            (lambda ()
              (progn
                (org-link-set-parameters "zotero" :follow #'org-zotero-open-via-macos :export #'org-zotxt--link-export)
                (org-link-set-parameters "id" :follow #'org-id-open :store #'org-id-store-link-maybe :export #'org-id-of-zotero-note-export-maybe)
                )
              ))


5 个赞