Files
.emacs.d/lisp/init-gptel.el
User fffd35f8ed Config: extract keybinding functions and clean up key configs
- Add init-kbd-func.el for keybinding helper functions
- Move global-display-line-numbers-mode from early-init.el to init.el
- Slim down init-kbd.el: remove hydra-multiple-cursors, migrate defs
- Reorganize dirvish config
- Remove scattered global-set-key bindings from init-org, init-ui, etc.
- Guard server-start with server-running-p check
- Set ibuffer-default-sorting-mode to filename
- Tweak gptel config
2026-04-17 06:52:00 +08:00

164 lines
7.5 KiB
EmacsLisp

;;; init-gptel.el --- ai -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:
(use-package gptel
:hook (gptel-mode-hook . visual-line-mode)
:config
(setq gptel-default-mode 'org-mode)
(defun my/org-auto-gptel ()
(when (derived-mode-p 'org-mode)
(save-excursion
(goto-char (point-min))
(when (re-search-forward "^#\\+GPTEL_MODEL:" nil t)
(gptel-mode)))))
(add-hook 'org-mode-hook #'my/org-auto-gptel)
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
(add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
(setq gptel-prompt-prefix-alist '((markdown-mode . "### ") (org-mode . "** User: ") (text-mode . "### ")))
;; 读取文件
(gptel-make-tool
:function (lambda (filepath)
(with-temp-buffer
(insert-file-contents (expand-file-name filepath))
(buffer-string)))
:name "read_file"
:description "Read and display the contents of a file"
:args '((:name "filepath" :type string :description "Path to the file"))
:category "filesystem")
;; 创建文件
(gptel-make-tool
:name "create_file" ; javascript-style snake_case name
:function (lambda (path filename content) ; the function that runs
(let ((full-path (expand-file-name filename path)))
(with-temp-buffer
(insert content)
(write-file full-path))
(format "Created file %s in %s" filename path)))
:description "Create a new file with the specified content"
:args (list '(:name "path" ; a list of argument specifications
:type string
:description "The directory where to create the file")
'(:name "filename"
:type string
:description "The name of the file to create")
'(:name "content"
:type string
:description "The content to write to the file"))
:category "filesystem") ; An arbitrary label for grouping
(gptel-make-tool
:name "read_buffer" ; javascript-style snake_case name
:function (lambda (buffer) ; the function that will run
(unless (buffer-live-p (get-buffer buffer))
(error "error: buffer %s is not live." buffer))
(with-current-buffer buffer
(buffer-substring-no-properties (point-min) (point-max))))
:description "return the contents of an emacs buffer"
:args (list '(:name "buffer"
:type string ; :type value must be a symbol
:description "the name of the buffer whose contents are to be retrieved"))
:category "emacs") ; An arbitrary label for grouping
(gptel-make-openai "Kimi-Code"
:host "api.kimi.com"
:endpoint "/coding/v1/chat/completions"
:protocol "https"
:key (lambda ()
(auth-source-pick-first-password :host "api.kimi.com" :user "apikey"))
:stream t
:models '(kimi-for-coding)
:header (lambda (_)
(when-let ((key (gptel--get-api-key)))
`(("User-Agent" . "RooCode/3.30.3")
("HTTP-Referer" . "https://github.com/RooVetGit/Roo-Cline")
("X-Title" . "Roo Code")
("Authorization" . ,(concat "Bearer " key))))))
(setq gptel-model 'kimi-for-coding
gptel-backend (gptel-get-backend "Kimi-Code"))
(defun my-gptel--sanitize-filename (name)
"Clean NAME into a safe kebab-case filename string."
(let ((clean (downcase name)))
(setq clean (replace-regexp-in-string "[^a-z0-9_-]" "-" clean))
(setq clean (replace-regexp-in-string "^-+\\|[-]+$" "" clean))
(setq clean (replace-regexp-in-string "-[-]+" "-" clean))
(string-trim clean)))
(defun my-gptel-save-session ()
"Save current gptel session to ~/gptel/ with AI-generated filename.
The filename is prefixed with a YYYYMMDD-HHmmss timestamp and generated
from the last 1500 characters of the buffer."
(interactive)
(unless gptel-mode
(user-error "Not a gptel session buffer"))
(let* ((buf (current-buffer))
(content (with-current-buffer buf
(buffer-substring-no-properties (point-min) (point-max))))
(tail (substring content (max 0 (- (length content) 1500))))
(prompt-text (format "Based on the following conversation, generate a short kebab-case filename (3 to 5 lowercase English words, hyphen-separated, no file extension). Output ONLY the filename, nothing else.\n\n%s"
tail)))
(message "Generating filename with AI...")
(let ((fsm (gptel-request prompt-text
:system "You are a filename generator. Output ONLY a short kebab-case filename (3-5 lowercase English words separated by hyphens). No explanations, no quotes, no punctuation, no file extension."
:stream nil
:callback
(lambda (response info)
(condition-case err
(cond
((null response)
(message "Failed to generate filename: %s"
(or (plist-get info :status) "unknown error")))
((not (stringp response))
(message "Unexpected response type: %S" response))
(t
(let* ((generated (my-gptel--sanitize-filename (string-trim response)))
(generated (if (string-empty-p generated)
"gptel-session"
generated))
(timestamp (format-time-string "%Y%m%d-%H%M%S"))
(dir (expand-file-name "~/gptel/"))
(base (concat timestamp "-" generated))
(file (expand-file-name (concat base ".org") dir)))
;; handle name collision
(when (file-exists-p file)
(setq base (concat timestamp "-" generated "-" (format-time-string "%S")))
(setq file (expand-file-name (concat base ".org") dir)))
(unless (file-directory-p dir)
(make-directory dir t))
;; write-file triggers gptel--save-state to preserve metadata
(with-current-buffer buf
(write-file file))
(if (file-exists-p file)
(progn
(pop-to-buffer (find-file-noselect file))
(message "Saved gptel session to %s" file))
(message "ERROR: File was not created: %s" file)))))
(error (message "gptel save-session error: %S" err)))))))
(unless (plist-get (gptel-fsm-info fsm) :callback)
(message "WARNING: gptel-request did not register callback!")))))
(with-eval-after-load 'gptel
(define-key gptel-mode-map (kbd "C-c C-s") #'my-gptel-save-session)))
(use-package gptel-agent
:init
(with-eval-after-load 'gptel
(require 'gptel-agent))
:config
(add-to-list 'gptel-agent-dirs "~/.emacs.d/lisp/" t)
(gptel-agent-update))
(provide 'init-gptel)
;;; init-gptel.el ends here