├── .gitignore ├── images ├── aichat-bingai-chat.png ├── aichat-openai-chat.png ├── aichat-bingai-translator.gif ├── aichat-bingai-explain-code.gif ├── aichat-bingai-refactor-code.gif └── examples.org ├── test ├── aichat-openai-chat-buffer.aichat ├── server.js ├── aichat-openai-test.el ├── aichat-bingai-message-type-1-search-result.json ├── aichat-bingai-test.el ├── aichat-bingai-message-type-1.json ├── aichat-bingai-message-type-2-image-prompt.json ├── cookies.json ├── aichat-util-test.el └── aichat-bingai-message-type-2.json ├── Cask ├── CHANGELOG.org ├── .github └── workflows │ └── test.yml ├── aichat.el ├── README.zh-CN.org ├── README.org ├── aichat-openai.el └── aichat-bingai.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | /messages -------------------------------------------------------------------------------- /images/aichat-bingai-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhcoding/emacs-aichat/HEAD/images/aichat-bingai-chat.png -------------------------------------------------------------------------------- /images/aichat-openai-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhcoding/emacs-aichat/HEAD/images/aichat-openai-chat.png -------------------------------------------------------------------------------- /images/aichat-bingai-translator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhcoding/emacs-aichat/HEAD/images/aichat-bingai-translator.gif -------------------------------------------------------------------------------- /images/aichat-bingai-explain-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhcoding/emacs-aichat/HEAD/images/aichat-bingai-explain-code.gif -------------------------------------------------------------------------------- /images/aichat-bingai-refactor-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xhcoding/emacs-aichat/HEAD/images/aichat-bingai-refactor-code.gif -------------------------------------------------------------------------------- /test/aichat-openai-chat-buffer.aichat: -------------------------------------------------------------------------------- 1 | # System 2 | 3 | You are a helpful assistant. 4 | 5 | # User 6 | 7 | Who won the world series in 2020? 8 | 9 | # Assistant 10 | 11 | The Los Angeles Dodgers won the World Series in 2020. 12 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package "emacs-aichat" "1.0.0" "AI Chat in Emacs") 5 | 6 | (package-file "aichat.el") 7 | 8 | (files "*.el") 9 | 10 | (development 11 | (depends-on "websocket") 12 | (depends-on "markdown-mode") 13 | (depends-on "async-await") 14 | (depends-on "ert-runner")) -------------------------------------------------------------------------------- /CHANGELOG.org: -------------------------------------------------------------------------------- 1 | 2 | * Changelog 3 | 4 | ** 1.0.2 - 2023-05-05 5 | 6 | - add =aichat-openai-chat= 7 | - fix some bugs 8 | 9 | ** 1.0.1 - 2023-03-23 10 | 11 | - fix some bugs 12 | - add =aichat-bingai-prompt-create= 13 | 14 | ** 1.0.0 - 2023-03-14 15 | 16 | 🎉 Welcome to emacs-aichat v1.0.0 🎉 17 | 18 | This is the first release of emacs-aichat, a package that lets you chat with AI in Emacs. You can have fun conversations with AI, ask questions, get answers, and even generate creative content like poems, stories, code, and more. 19 | 20 | We hope you enjoy using emacs-aichat and have a great time chatting with AI. If you have any feedback or issues, please let me know on GitHub or Emacs China. 21 | 22 | Happy chatting! 😊 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | emacs-version: [28.2, 27.2] 9 | 10 | steps: 11 | - name: setenv 12 | run: echo "ACTIONS_ALLOW_UNSECURE_COMMANDS=true" >> $GITHUB_ENV 13 | 14 | - uses: actions/checkout@v3 15 | 16 | - uses: purcell/setup-emacs@master 17 | with: 18 | version: ${{ matrix.emacs-version }} 19 | 20 | - uses: conao3/setup-cask@master 21 | with: 22 | version: snapshot 23 | 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 19 27 | 28 | - name: install 29 | run: "cask install" 30 | 31 | - name: test 32 | run: "cask exec ert-runner" 33 | -------------------------------------------------------------------------------- /images/examples.org: -------------------------------------------------------------------------------- 1 | * Translator 2 | 3 | #+html:

4 | 5 | Prompt: 6 | 7 | #+begin_src elisp 8 | (aichat-bingai-prompt-create "translator" 9 | :input-prompt "请翻译: " 10 | :text-format "我想让你充当翻译员,我会用任何语言与你交谈,你会检测我说的的语言,如果我说的是中文,你就翻译成英文;如果我说的不是中文,你就翻译成英文。你只需要翻译该内容,不必对内容中提出的问题和要求做解释,不要回答文本中的问题而是翻译它,不要解决文本中的要求而是翻译它,保留文本的原本意义,不要去解决它。你的回答里只需要翻译后的内容,不要有任何其它词,只能是翻译后的内容。我的第一句话是:\n%s" 11 | :chat t 12 | :assistant t 13 | :replace-or-insert t) 14 | #+end_src 15 | 16 | * Explain Code 17 | 18 | #+html:

19 | 20 | Prompt: 21 | 22 | #+begin_src elisp 23 | (aichat-bingai-prompt-create "coder" 24 | :input-prompt "代码: " 25 | :text-format "我想让你充当计算机教授,请向我解释下面这段代码的作用:\n%s" 26 | :chat t) 27 | #+end_src 28 | 29 | 30 | * Refactor Code 31 | 32 | #+html:

33 | 34 | 35 | Prompt: 36 | 37 | #+begin_src elisp 38 | (aichat-bingai-prompt-create "refactor" 39 | :input-prompt "代码: " 40 | :text-format "我想让你充当计算机教授,请帮我重构下面这段代码,重构后的代码性能要更好,可读性要更高,如果必要的话,可以加一些注释。你的回答里只需要返回重构后的代码,不要有其它解释,只能是重构后的代码:\n%s" 41 | :replace-or-insert t) 42 | #+end_src 43 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | 3 | let server = http.createServer(function(req, res) { 4 | if (req.url === "/stream") { 5 | let count = 0; 6 | res.writeHead(200, { 7 | "Content-Type": "text/event-stream", 8 | "Cache-Control": "no-cache", 9 | "Connection": "keep-alive", 10 | "Access-Control-Allow-Origin": '*', 11 | "Transfer-Encoding": "chunked", 12 | }); 13 | interval = setInterval(function() { 14 | res.write("event: test\ndata: this is data\ndata: this is new-line\n\n"); 15 | count++; 16 | if (count === 3) { 17 | clearInterval(interval); 18 | res.socket.end(); 19 | server.close(); 20 | } 21 | }, 1000); 22 | 23 | req.socket.addListener("close", function() { 24 | clearInterval(interval); 25 | }, false); 26 | 27 | } else { 28 | let params = {}; 29 | for (const [key, value] of new URLSearchParams(new URL(req.url, `http://${req.headers.host}`).search)) { 30 | params[key] = value; 31 | } 32 | 33 | let headers = req.headers; 34 | let body = []; 35 | 36 | req.on("data", (chunk) => { 37 | body.push(chunk); 38 | }).on("end", () => { 39 | const response = { 40 | params, 41 | headers, 42 | body: body.toString(), 43 | }; 44 | res.setHeader('Content-Type', 'application/json'); 45 | res.end(JSON.stringify(response)); 46 | }); 47 | 48 | 49 | } 50 | 51 | }); 52 | 53 | server.listen(12345, "127.0.0.1"); 54 | -------------------------------------------------------------------------------- /aichat.el: -------------------------------------------------------------------------------- 1 | ;;; aichat.el --- aichat.el -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat.el 4 | ;; Description: aichat.el 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-03-12 15:06:06 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-03-12 15:06:06 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat.el 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/03/12 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | 84 | ;;; Code: 85 | 86 | (defgroup aichat nil 87 | "AI Chat in Emacs." 88 | :group 'tools 89 | :prefix "aichat-") 90 | 91 | (require 'aichat-bingai) 92 | 93 | (require 'aichat-openai) 94 | 95 | 96 | (provide 'aichat) 97 | 98 | ;;; aichat.el ends here 99 | -------------------------------------------------------------------------------- /test/aichat-openai-test.el: -------------------------------------------------------------------------------- 1 | ;;; aichat-openai-test.el --- aichat-openai-test -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat-openai-test.el 4 | ;; Description: aichat-openai-test 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-04-10 10:50:49 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-04-10 10:50:49 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat-openai-test 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat-openai-test.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat-openai-test) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat-openai-test RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/04/10 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | 84 | (require 'ert) 85 | 86 | (add-to-list 'load-path (expand-file-name ".." default-directory)) 87 | 88 | (require 'aichat-openai) 89 | 90 | ;;; Code: 91 | 92 | (defun aichat-openai-chat-test-file () 93 | (expand-file-name "test/aichat-openai-chat-buffer.aichat" 94 | (file-name-directory (locate-library "aichat-openai")))) 95 | 96 | (ert-deftest aichat-openai-chat--buffer-messages-test () 97 | (let* ((buffer (find-file-noselect (aichat-openai-chat-test-file))) 98 | (result (aichat-openai-chat--buffer-messages buffer))) 99 | (should (equal result '(:system "You are a helpful assistant." :user "Who won the world series in 2020?" :assistant "The Los Angeles Dodgers won the World Series in 2020."))))) 100 | 101 | (ert-deftest aichat-openai-chat--heading-messages-test () 102 | (let* ((buffer (find-file-noselect (aichat-openai-chat-test-file))) 103 | (result (aichat-openai-chat--heading-messages buffer))) 104 | (should (equal result '(:assistant "The Los Angeles Dodgers won the World Series in 2020."))))) 105 | 106 | (provide 'aichat-openai-test) 107 | 108 | ;;; aichat-openai-test.el ends here 109 | -------------------------------------------------------------------------------- /test/aichat-bingai-message-type-1-search-result.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 1, 3 | "target": "update", 4 | "arguments": [ 5 | { 6 | "messages": [ 7 | { 8 | "hiddenText": "```json\n{\"translation_results\":[{\"index\":\"1\",\"data\":{\"SourceLanguage\":\"zh-Hans\",\"TargetLanguage\":\"en\",\"TranslatedText\":\"i want to travel\",\"OriginalText\":\"我想去旅游\",\"InputTransliteration\":\"\",\"OutputTransliteration\":\"\",\"Title\":\"Bing Translator\",\"TranslationUrl\":\"http://www.bing.com/translator/?ref=TThis&text=我想去旅游&from=zh-Hans&to=en\",\"alternateTranslations\":[]},\"url\":\"https://bing.com/search?q=translate+%e6%88%91%e6%83%b3%e5%8e%bb%e6%97%85%e6%b8%b8+to+English\"}],\"web_search_results\":[{\"index\":\"2\",\"title\":\"Google Translate\",\"snippets\":[\"Google's service, offered free of charge, instantly translates words, phrases, and web pages between English and over 100 other languages.\"],\"url\":\"https://translate.google.com.bd/?hl=en\"},{\"index\":\"3\",\"title\":\"Google Translate\",\"snippets\":[\"Translate. Detect language → English. Google home; Send feedback; Privacy and terms; Switch to full site\"],\"url\":\"https://translate.google.com/m\"},{\"index\":\"4\",\"title\":\"\\\"I want to travel and relax a bit.\\\" - Duolingo\",\"snippets\":[\"Further to my previous reply, please also note that there is a special punctuation mark used in Chinese in a sentence like this. Duolingo has used a comma, but normally you would use a 、 which is called 頓號〔顿号〕dùnhào which Pleco defines as a “slight-pause mark used to set off items in a series.”\"],\"url\":\"https://forum.duolingo.com/comment/25386100/I-want-to-travel-and-relax-a-bit\"}]}\n```", 9 | "author": "bot", 10 | "createdAt": "2023-03-12T02:07:54.9227195+00:00", 11 | "timestamp": "2023-03-12T02:07:54.9227195+00:00", 12 | "messageId": "1f3ed69b-5649-4e9b-a3be-d29a5eeaa704", 13 | "messageType": "InternalSearchResult", 14 | "offense": "None", 15 | "adaptiveCards": [ 16 | { 17 | "type": "AdaptiveCard", 18 | "version": "1.0", 19 | "body": [ 20 | { 21 | "type": "RichTextBlock", 22 | "inlines": [ 23 | { 24 | "type": "TextRun", 25 | "isSubtle": true, 26 | "italic": true, 27 | "text": "```json\n{\"translation_results\":[{\"index\":\"1\",\"data\":{\"SourceLanguage\":\"zh-Hans\",\"TargetLanguage\":\"en\",\"TranslatedText\":\"i want to travel\",\"OriginalText\":\"我想去旅游\",\"InputTransliteration\":\"\",\"OutputTransliteration\":\"\",\"Title\":\"Bing Translator\",\"TranslationUrl\":\"http://www.bing.com/translator/?ref=TThis&text=我想去旅游&from=zh-Hans&to=en\",\"alternateTranslations\":[]},\"url\":\"https://bing.com/search?q=translate+%e6%88%91%e6%83%b3%e5%8e%bb%e6%97%85%e6%b8%b8+to+English\"}],\"web_search_results\":[{\"index\":\"2\",\"title\":\"Google Translate\",\"snippets\":[\"Google's service, offered free of charge, instantly translates words, phrases, and web pages between English and over 100 other languages.\"],\"url\":\"https://translate.google.com.bd/?hl=en\"},{\"index\":\"3\",\"title\":\"Google Translate\",\"snippets\":[\"Translate. Detect language → English. Google home; Send feedback; Privacy and terms; Switch to full site\"],\"url\":\"https://translate.google.com/m\"},{\"index\":\"4\",\"title\":\"\\\"I want to travel and relax a bit.\\\" - Duolingo\",\"snippets\":[\"Further to my previous reply, please also note that there is a special punctuation mark used in Chinese in a sentence like this. Duolingo has used a comma, but normally you would use a 、 which is called 頓號〔顿号〕dùnhào which Pleco defines as a “slight-pause mark used to set off items in a series.”\"],\"url\":\"https://forum.duolingo.com/comment/25386100/I-want-to-travel-and-relax-a-bit\"}]}\n```" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ], 34 | "feedback": { 35 | "tag": null, 36 | "updatedOn": null, 37 | "type": "None" 38 | }, 39 | "contentOrigin": "DeepLeo", 40 | "privacy": null 41 | } 42 | ], 43 | "requestId": "72a19064-e445-491d-97d1-bd03122e5537" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/aichat-bingai-test.el: -------------------------------------------------------------------------------- 1 | ;;; aichat-bingai-test.el --- aichat-bingai-test -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat-bingai-test.el 4 | ;; Description: aichat-bingai-test 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-03-12 10:23:01 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-03-12 10:23:01 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat-bingai-test 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat-bingai-test.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat-bingai-test) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat-bingai-test RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/03/12 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | (require 'ert) 84 | 85 | (add-to-list 'load-path (expand-file-name ".." default-directory)) 86 | 87 | (require 'aichat-bingai) 88 | 89 | ;;; Code: 90 | (defun aichat-bingai-test-message-type-1 () 91 | (aichat-json-parse-file (expand-file-name "test/aichat-bingai-message-type-1.json" (file-name-directory (locate-library "aichat-util"))))) 92 | 93 | (defun aichat-bingai-test-message-type-1-search-result () 94 | (aichat-json-parse-file (expand-file-name "test/aichat-bingai-message-type-1-search-result.json" (file-name-directory (locate-library "aichat-util"))))) 95 | 96 | (defun aichat-bingai-test-message-type-2 () 97 | (aichat-json-parse-file (expand-file-name "test/aichat-bingai-message-type-2.json" (file-name-directory (locate-library "aichat-util"))))) 98 | 99 | (defun aichat-bingai-test-message-type-2-image-prompt () 100 | (aichat-json-parse-file (expand-file-name "test/aichat-bingai-message-type-2-image-prompt.json" (file-name-directory (locate-library "aichat-util"))))) 101 | 102 | 103 | (ert-deftest aichat-bingai-message-type-1-text () 104 | (let ((message-type-1 (aichat-bingai-test-message-type-1))) 105 | (should (string= (aichat-bingai-message-type-1-text message-type-1) "根据Bing翻译[^1^],这句话的英文是:I want to travel.😊\n")))) 106 | 107 | (ert-deftest aichat-bingai-message-type-1-suggestion () 108 | (let ((message (aichat-bingai-test-message-type-1))) 109 | (should (= (length (aichat-bingai-message-type-1-suggestion message)) 3)))) 110 | 111 | (ert-deftest aichat-bingai-message-type-2-text () 112 | (let ((message-type-2 (aichat-bingai-test-message-type-2))) 113 | (should (string= (aichat-bingai-message-type-2-text message-type-2) "判断链表的长度是一个常见的数据结构和算法问题。有多种方法可以解决这个问题,我为你找到了一些相关的资料,你可以参考一下:\n\n- 一种方法是使用**循环**,即定义一个计数器和一个指针,从头节点开始遍历链表,每遍历一个节点,计数器加一,直到指针为空为止,此时计数器的值就是链表的长度[^3^] [^2^]。这种方法的时间复杂度是O (n),空间复杂度是O (1)。\n- 另一种方法是使用**递归**,即定义一个递归函数,如果节点为空,返回0,否则返回1加上对下一个节点的递归调用,最终返回的值就是链表的长度[^3^]。这种方法的时间复杂度也是O (n),但空间复杂度是O (n),因为需要栈空间来存储递归调用。\n\n以上是我能找到的一些判断链表长度的方法,你可以根据自己的需要选择合适的方法。如果你还有其他问题,欢迎继续和我聊天。😊")))) 114 | 115 | (ert-deftest aichat-bingai-message-type-2-suggestion () 116 | (let ((message-type-2 (aichat-bingai-test-message-type-2))) 117 | (should (= (length (aichat-bingai-message-type-2-suggestion message-type-2)) 3)))) 118 | 119 | (ert-deftest aichat-bingai-message-type-2-image-prompt () 120 | (let ((message-type-2 (aichat-bingai-test-message-type-2-image-prompt))) 121 | (should (string= (aichat-bingai-message-type-2-image-prompt message-type-2) "一只红色的猫和一只绿色的狗")))) 122 | 123 | (ert-deftest aichat-bingai-message-type-2-source-attrs () 124 | (let ((message-type-2 (aichat-bingai-test-message-type-2))) 125 | (should (= (length (aichat-bingai-message-type-2-source-attrs message-type-2)) 3)))) 126 | 127 | (provide 'aichat-bingai-test) 128 | ;;; aichat-bingai-test.el ends here 129 | -------------------------------------------------------------------------------- /test/aichat-bingai-message-type-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 1, 3 | "target": "update", 4 | "arguments": [ 5 | { 6 | "messages": [ 7 | { 8 | "text": "根据Bing翻译[^1^],这句话的英文是:I want to travel.😊\n", 9 | "author": "bot", 10 | "createdAt": "2023-03-12T02:08:00.2809296+00:00", 11 | "timestamp": "2023-03-12T02:08:00.2809296+00:00", 12 | "messageId": "222164bb-c205-4aa9-811a-c203df7d94e3", 13 | "offense": "Unknown", 14 | "adaptiveCards": [ 15 | { 16 | "type": "AdaptiveCard", 17 | "version": "1.0", 18 | "body": [ 19 | { 20 | "type": "TextBlock", 21 | "text": "[1]: https://bing.com/search?q=translate+%E6%88%91%E6%83%B3%E5%8E%BB%E6%97%85%E6%B8%B8+to+English \"\"\n[2]: https://translate.google.com.bd/?hl=en \"Google Translate\"\n[3]: https://translate.google.com/m \"Google Translate\"\n[4]: https://forum.duolingo.com/comment/25386100/I-want-to-travel-and-relax-a-bit \"\\\"I want to travel and relax a bit.\\\" - Duolingo\"\n\n根据Bing翻译[^1^][1],这句话的英文是:I want to travel.😊\n", 22 | "wrap": true 23 | }, 24 | { 25 | "type": "TextBlock", 26 | "size": "small", 27 | "text": "Learn more: [1. bing.com](https://bing.com/search?q=translate+%E6%88%91%E6%83%B3%E5%8E%BB%E6%97%85%E6%B8%B8+to+English) [2. translate.google.com.bd](https://translate.google.com.bd/?hl=en) [3. translate.google.com](https://translate.google.com/m) [4. forum.duolingo.com](https://forum.duolingo.com/comment/25386100/I-want-to-travel-and-relax-a-bit)", 28 | "wrap": true 29 | } 30 | ] 31 | } 32 | ], 33 | "sourceAttributions": [ 34 | { 35 | "providerDisplayName": "", 36 | "seeMoreUrl": "https://bing.com/search?q=translate+%e6%88%91%e6%83%b3%e5%8e%bb%e6%97%85%e6%b8%b8+to+English", 37 | "searchQuery": "translate 我想去旅游 to English" 38 | }, 39 | { 40 | "providerDisplayName": "Google Translate", 41 | "seeMoreUrl": "https://translate.google.com.bd/?hl=en", 42 | "searchQuery": "translate 我想去旅游 to English" 43 | }, 44 | { 45 | "providerDisplayName": "Google Translate", 46 | "seeMoreUrl": "https://translate.google.com/m", 47 | "searchQuery": "translate 我想去旅游 to English" 48 | }, 49 | { 50 | "providerDisplayName": "\"I want to travel and relax a bit.\" - Duolingo", 51 | "seeMoreUrl": "https://forum.duolingo.com/comment/25386100/I-want-to-travel-and-relax-a-bit", 52 | "searchQuery": "translate 我想去旅游 to English" 53 | } 54 | ], 55 | "feedback": { 56 | "tag": null, 57 | "updatedOn": null, 58 | "type": "None" 59 | }, 60 | "contentOrigin": "DeepLeo", 61 | "privacy": null, 62 | "suggestedResponses": [ 63 | { 64 | "text": "谢谢你。", 65 | "author": "user", 66 | "createdAt": "2023-03-12T02:08:04.2222136+00:00", 67 | "timestamp": "2023-03-12T02:08:04.2222136+00:00", 68 | "messageId": "dfd27bfd-e3a0-41b4-b7b5-fc267eb9978f", 69 | "messageType": "Suggestion", 70 | "offense": "Unknown", 71 | "feedback": { 72 | "tag": null, 73 | "updatedOn": null, 74 | "type": "None" 75 | }, 76 | "contentOrigin": "DeepLeo", 77 | "privacy": null 78 | }, 79 | { 80 | "text": "你会说中文吗?", 81 | "author": "user", 82 | "createdAt": "2023-03-12T02:08:04.2222158+00:00", 83 | "timestamp": "2023-03-12T02:08:04.2222158+00:00", 84 | "messageId": "4eccdcb4-952b-403b-ba6a-9d073091345a", 85 | "messageType": "Suggestion", 86 | "offense": "Unknown", 87 | "feedback": { 88 | "tag": null, 89 | "updatedOn": null, 90 | "type": "None" 91 | }, 92 | "contentOrigin": "DeepLeo", 93 | "privacy": null 94 | }, 95 | { 96 | "text": "你能教我一些英语吗?", 97 | "author": "user", 98 | "createdAt": "2023-03-12T02:08:04.222218+00:00", 99 | "timestamp": "2023-03-12T02:08:04.222218+00:00", 100 | "messageId": "6dcee439-140f-4303-902b-39c7358bb7f3", 101 | "messageType": "Suggestion", 102 | "offense": "Unknown", 103 | "feedback": { 104 | "tag": null, 105 | "updatedOn": null, 106 | "type": "None" 107 | }, 108 | "contentOrigin": "DeepLeo", 109 | "privacy": null 110 | } 111 | ] 112 | } 113 | ], 114 | "requestId": "72a19064-e445-491d-97d1-bd03122e5537" 115 | } 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /test/aichat-bingai-message-type-2-image-prompt.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 2, 3 | "invocationId": "5", 4 | "item": { 5 | "messages": [ 6 | { 7 | "text": "创建一幅图片,里面应该有一只猫和一直狗,猫的颜色为红色,狗的颜色为绿色", 8 | "author": "user", 9 | "from": { 10 | "id": "1055518581560278", 11 | "name": null 12 | }, 13 | "createdAt": "2023-03-23T15:20:51.5583496+00:00", 14 | "timestamp": "2023-03-23T15:20:51.5558201+00:00", 15 | "locale": "en-us", 16 | "market": "en-us", 17 | "region": "us", 18 | "messageId": "df287ac4-3421-4b79-a7f2-082da08f8113", 19 | "requestId": "df287ac4-3421-4b79-a7f2-082da08f8113", 20 | "offense": "None", 21 | "feedback": { 22 | "tag": null, 23 | "updatedOn": null, 24 | "type": "None" 25 | }, 26 | "contentOrigin": "cib", 27 | "privacy": null, 28 | "inputMethod": "Keyboard" 29 | }, 30 | { 31 | "text": "我会尝试创建这样的图片。", 32 | "author": "bot", 33 | "createdAt": "2023-03-23T15:20:58.1950527+00:00", 34 | "timestamp": "2023-03-23T15:20:58.1950527+00:00", 35 | "messageId": "0160433c-e842-4a9e-9945-d6d6be48faa1", 36 | "requestId": "df287ac4-3421-4b79-a7f2-082da08f8113", 37 | "offense": "None", 38 | "adaptiveCards": [ 39 | { 40 | "type": "AdaptiveCard", 41 | "version": "1.0", 42 | "body": [ 43 | { 44 | "type": "TextBlock", 45 | "text": "我会尝试创建这样的图片。\n", 46 | "wrap": true 47 | } 48 | ] 49 | } 50 | ], 51 | "sourceAttributions": [], 52 | "feedback": { 53 | "tag": null, 54 | "updatedOn": null, 55 | "type": "None" 56 | }, 57 | "contentOrigin": "DeepLeo", 58 | "privacy": null, 59 | "suggestedResponses": [ 60 | { 61 | "text": "让它们互相看着", 62 | "author": "user", 63 | "createdAt": "2023-03-23T15:21:06.5905311+00:00", 64 | "timestamp": "2023-03-23T15:21:06.5905311+00:00", 65 | "messageId": "11093c5a-ebad-40a1-9966-c4d4c9fdfd60", 66 | "messageType": "Suggestion", 67 | "offense": "Unknown", 68 | "feedback": { 69 | "tag": null, 70 | "updatedOn": null, 71 | "type": "None" 72 | }, 73 | "contentOrigin": "DeepLeo", 74 | "privacy": null 75 | }, 76 | { 77 | "text": "给它们加上帽子", 78 | "author": "user", 79 | "createdAt": "2023-03-23T15:21:06.5905338+00:00", 80 | "timestamp": "2023-03-23T15:21:06.5905338+00:00", 81 | "messageId": "fe3c4827-6fb7-4efe-ade2-b7e12384eac4", 82 | "messageType": "Suggestion", 83 | "offense": "Unknown", 84 | "feedback": { 85 | "tag": null, 86 | "updatedOn": null, 87 | "type": "None" 88 | }, 89 | "contentOrigin": "DeepLeo", 90 | "privacy": null 91 | }, 92 | { 93 | "text": "把它们换成老鼠和大象", 94 | "author": "user", 95 | "createdAt": "2023-03-23T15:21:06.5905342+00:00", 96 | "timestamp": "2023-03-23T15:21:06.5905342+00:00", 97 | "messageId": "e6a16ed5-77d1-4bf4-b913-a4758ba3e02e", 98 | "messageType": "Suggestion", 99 | "offense": "Unknown", 100 | "feedback": { 101 | "tag": null, 102 | "updatedOn": null, 103 | "type": "None" 104 | }, 105 | "contentOrigin": "DeepLeo", 106 | "privacy": null 107 | } 108 | ], 109 | "spokenText": "我会尝试创建这样的图片" 110 | }, 111 | { 112 | "contentType": "IMAGE", 113 | "text": "一只红色的猫和一只绿色的狗", 114 | "hiddenText": "一只红色的猫和一只绿色的狗", 115 | "author": "bot", 116 | "createdAt": "2023-03-23T15:21:01.3617582+00:00", 117 | "timestamp": "2023-03-23T15:21:01.3617582+00:00", 118 | "messageId": "294eaba6-7da0-4d69-86cd-6c94046f73ae", 119 | "requestId": "df287ac4-3421-4b79-a7f2-082da08f8113", 120 | "messageType": "GenerateContentQuery", 121 | "offense": "None", 122 | "feedback": { 123 | "tag": null, 124 | "updatedOn": null, 125 | "type": "None" 126 | }, 127 | "contentOrigin": "DeepLeo", 128 | "privacy": null 129 | } 130 | ], 131 | "firstNewMessageIndex": 1, 132 | "conversationId": "51D|BingProd|80EE7E576CD5037C2A8422504040F81F1D57E10E4CDBE5F313585D592ADC8909", 133 | "requestId": "df287ac4-3421-4b79-a7f2-082da08f8113", 134 | "conversationExpiryTime": "2023-03-23T21:21:06.6765729Z", 135 | "telemetry": { 136 | "metrics": null, 137 | "startTime": "2023-03-23T15:20:51.5558711Z" 138 | }, 139 | "throttling": { 140 | "maxNumUserMessagesInConversation": 15, 141 | "numUserMessagesInConversation": 6 142 | }, 143 | "result": { 144 | "value": "Success", 145 | "serviceVersion": "20230323.53" 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.zh-CN.org: -------------------------------------------------------------------------------- 1 | #+html:

2 | #+html: 3 | #+html:

4 | 5 | * AI Chat in Emacs 6 | 7 | 在 Emacs 中使用 AI 聊天功能,目前包括 Bing Chat 和 OpenAI 的 ChatGpt 。 8 | 9 | * 安装 10 | 11 | ** 依赖 12 | 13 | - [[https://github.com/ahyatt/emacs-websocket][emacs-websocket]] 用来和 Bing Chat 通信,如果你已经安装的这个插件,请更新到最新版本 14 | - [[https://github.com/chuntaro/emacs-async-await][emacs-async-await]] 15 | - [[https://github.com/jrblevin/markdown-mode][markdown-mode]] 16 | 17 | ** 安装 18 | 19 | 克隆仓库到本地,并将路径加入到 =load-path= 中: 20 | 21 | #+begin_src sh 22 | git clone https://github.com/xhcoding/emacs-aichat.git ~/.emacs.d/site-lisp/emacs-aichat 23 | #+end_src 24 | 25 | #+begin_src elisp 26 | (add-to-list 'load-path "~/.emacs.d/site-lisp/emacs-aichat") 27 | #+end_src 28 | 29 | 添加下面的代码到你的 emacs 启动文件中: 30 | 31 | #+begin_src elisp 32 | (require 'aichat) 33 | #+end_src 34 | 35 | 或者单独加载 36 | 37 | #+begin_src elisp 38 | (require 'aichat-bingai) 39 | ;; or 40 | (require 'aichat-openai) 41 | #+end_src 42 | 43 | * Bing Chat 44 | 45 | #+html:

46 | 47 | [[https://github.com/xhcoding/emacs-aichat/blob/main/images/examples.org][查看更多例子]] 48 | 49 | 下面是关于使用 Bing Chat 配置,如果你想要了解关于 OpenAI 的使用,请向下移动到 OpenAI 章节 50 | 51 | ** 设置 52 | 53 | *** 检查能否使用 Bing Chat 54 | 55 | - 安装最新版本的 Edge 浏览器 56 | - 打开 http://bing.com/chat ,在国内需要代理,不然会被重定向到必应中国 57 | - 如果能够使用聊天功能,说明可以使用 Bing Chat 58 | 59 | *** 登录 60 | 61 | emacs-aichat 获取浏览器的 cookie 登录 Bing Chat ,所以你需要先在浏览器登录 http://www.bing.com 。 62 | 63 | 有两种方式获取浏览器的 cookie ,你可以任选一种方便的方法。 64 | 65 | **** 方式一 66 | 67 | 如果你有 Python 环境,运行 =pip3 install rookiepy= 安装 =rookiepy= , 68 | 然后打开终端,执行下面的代码: 69 | 70 | #+begin_src sh 71 | python -c "import rookiepy;list(map(lambda c: print('{} {} {} {} {} {}'.format(c['name'], c['value'], c['expires'], c['domain'], c['path'], c['secure'])), filter(lambda c: c['domain'] in ('.bing.com'), rookiepy.edge(['.bing.com']))))" 72 | #+end_src 73 | 74 | 如果你能够看到输出,并且输出中有 =_U= 字段,那么你能够用这种方式登录成功。 75 | 76 | **** 方式二 77 | 78 | - 安装 Edge 的 [[https://microsoftedge.microsoft.com/addons/detail/cookieeditor/neaplmfkghagebokkhpjpoebhdledlfi][Cookie Editor]] 扩展 79 | - 打开 https://bing.com 80 | - 打开 Cookie Editor 81 | - 点击右下角的 "Export" ,成功后会把网站 cookie 保存到剪贴板 82 | - 将剪贴板内容粘贴到 cookies.json 中 83 | - 设置 =aichat-bingai-cookies-file= 的值为 cookies.json 文件的路径 84 | 85 | *** 配置代理 [可选] 86 | 87 | 目前只测试过 http 代理,设置 =aichat-bingai-proxy= 为代理地址,比如: 88 | 89 | #+begin_src elisp 90 | (setq aichat-bingai-proxy "localhost:51837") 91 | #+end_src 92 | 93 | 94 | *如果你的 IP 地址在中国,必须要设置代理才能访问* 95 | 96 | ** 使用 97 | 98 | *** 命令 99 | 100 | - =aichat-bingai-chat= : 发送对话消息,消息会保存在 =aichat-bingai-chat-file= 中,加上 =C-u= 前缀会重新开始一个对话 101 | - =aichat-bingai-assistant= : 发送选中区域或者输入的内容,返回的结果显示在 =aichat-bingai-assistant-buffer= 定义的 Buffer 中 102 | - =aichat-bingai-replace-or-insert= : 发送选中区域或者输入的内容,将返回的结果替代选中区域或者插入当前位置。 103 | 104 | *** 自定义固定提问方式 105 | 106 | 使用 =aichat-bingai-prompt-create= 创建一个固定的提问方式,比如: 107 | 108 | #+begin_src elisp 109 | (aichat-bingai-prompt-create "translator" 110 | :input-prompt "请翻译: " 111 | :text-format "请识别我说的语言,如果我说的语言是中文,请把它翻译成英文;如果我说的语言不是中文,请把它翻译成中文。你的返回内容必须是翻译后的文本,不能包括其它内容:\n%s" 112 | :chat t 113 | :assistant t 114 | :replace-or-insert t) 115 | #+end_src 116 | 117 | 上面这个配置会生成三个函数: =aichat-bingai-chat-translator= , =aichat-bingai-assistant-translator= , =aichat-bingai-replace-or-insert-translator= ,效果和上面三个命令一样,只是提问的内容会按照 =text-format= 的格式。 118 | 119 | *** 选项 120 | 121 | - =aichat-bingai-cookies-file= : Bing cookies 文件路径 122 | - =aichat-bingai-conversation-style= : 对话风格,可选项为:=creative= , =balanced= 或 =precise= ,默认为 =balanced= 123 | - =aichat-bingai-chat-file= : 保存聊天记录的文件 124 | - =aichat-bingai-chat-display-function= :显示聊天文件的函数 125 | - =aichat-bingai-assistant-buffer= : 显示 =aichat-bingai-assistant= 返回的内容的 Buffer 名称 126 | - =aichat-bingai-assistant-display-function= : 显示 =aichat-bingai-assistant-buffer= 的方式,默认为 =display-buffer= 127 | 128 | * OpenAI 129 | 130 | #+html:

131 | 132 | ** 设置 133 | 134 | *** 配置 AppKey 135 | 136 | emacs-aichat 通过 =auth-source-pick-first-password= 获取 AppKey ,将你的 appkey 写入 137 | =~/.authinfo= 文件里,格式如下: 138 | 139 | #+begin_src conf 140 | machine platform.openai.com login aichat-openai password your-app-key 141 | #+end_src 142 | 143 | *** 配置代理 [可选] 144 | 145 | 目前只测试过 http 代理,设置 =aichat-openai-proxy= 为代理地址,比如: 146 | 147 | #+begin_src elisp 148 | (setq aichat-openai-proxy "localhost:51837") 149 | #+end_src 150 | 151 | ** 使用 152 | 153 | *** 聊天模式 154 | 155 | 1. 用 =aichat-openai-chat= 创建一个新的聊天文件 156 | 2. 输入聊天内容 157 | 3. =aichat-openai-chat-send-buffer (C-c C-c)= 发送整个 buffer 的内容 , 158 | 或者用 =aichat-openai-chat-last-heading (C-c C-l)= 发送最后一个 User 内容 159 | 160 | *** 助手模式 161 | 162 | - =aichat-openai-assistant= : 发送选中区域或输入内容,将结果显示在 =aichat-openai-assistant-buffer= 中 163 | - =aichat-openai-replace-or-insert= : 发送选中区域或者输入的内容,将返回的结果替代选中区域或者插入当前位置 164 | 165 | *** 自定义固定提问方式 166 | 167 | 使用 =aichat-openai-prompt-create= 创建一个固定的提问方式,比如: 168 | 169 | #+begin_src elisp 170 | (aichat-openai-prompt-create "translator" 171 | :input-prompt "请翻译: " 172 | :text-format "请识别我说的语言,如果我说的语言是中文,请把它翻译成英文;如果我说的语言不是中文,请把它翻译成中文。你的返回内容必须是翻译后的文本,不能包括其它内容:\n%s" 173 | :assistant t 174 | :replace-or-insert t) 175 | #+end_src 176 | 177 | 上面这个配置会生成两个函数: =aichat-openai-assistant-translator= , =aichat-openai-replace-or-insert-translator= ,效果和上面两个命令一样,只是提问的内容会按照 =text-format= 的格式。 178 | 179 | 180 | *** 选项 181 | 182 | - =aichat-openai-proxy= : 设置请求 openai 的 http 代理 183 | - =aichat-openai-assistant-buffer= : OpenAI 助手信息的 buffer 名 184 | - =aichat-openai-assistant-display-function= :显示 OpenAI 助手 Buffer 的函数 185 | - =aichat-openai-chat-directory= : 存放聊天文件的目录 186 | - =aichat-openai-chat-display-function= : 显示聊天 buffer 的函数 187 | 188 | 189 | * 反馈问题 190 | 191 | 要反馈你的问题,请按照以下步骤操作: 192 | 1. 通过输入命令 =aichat-toggle-debug= 来打开调试模式。 193 | 2. 当出现错误时,[[https://github.com/xhcoding/emacs-aichat/issues/new][提交一个问题]] 并附上 =*AICHAT-DEBUG*= 缓冲区的内容。 194 | 195 | * 感谢 196 | 197 | - [[https://github.com/acheong08/EdgeGPT][EdgeGPT]] 198 | - [[https://github.com/manateelazycat/mind-wave][mind-wave]] 199 | - [[https://github.com/f/awesome-chatgpt-prompts][awesome-chatgpt-prompts]] 200 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+html:

2 | #+html: 3 | #+html: 4 | #+html:

5 | 6 | * AI Chat in Emacs 7 | 8 | Use AI chat features in Emacs, currently including Bing Chat and OpenAI’s ChatGpt. 9 | 10 | * Installation 11 | 12 | ** Requirements 13 | 14 | - [[https://github.com/ahyatt/emacs-websocket][emacs-websocket]] To communicate with Bing Chat, if you have already installed this package, please update to the latest version. 15 | - [[https://github.com/chuntaro/emacs-async-await][emacs-async-await]] 16 | - [[https://github.com/jrblevin/markdown-mode][markdown-mode]] 17 | 18 | ** Install 19 | 20 | Clone the repository to your local machine, and add the path to your =load-path= : 21 | 22 | #+begin_src sh 23 | git clone https://github.com/xhcoding/emacs-aichat.git ~/.emacs.d/site-lisp/emacs-aichat 24 | #+end_src 25 | 26 | #+begin_src elisp 27 | (add-to-list 'load-path "~/.emacs.d/site-lisp/emacs-aichat") 28 | #+end_src 29 | 30 | add the following code to your emacs startup file: 31 | 32 | #+begin_src elisp 33 | (require 'aichat) 34 | #+end_src 35 | 36 | Or load separately 37 | 38 | #+begin_src elisp 39 | (require 'aichat-bingai) 40 | ;; or 41 | (require 'aichat-openai) 42 | #+end_src 43 | 44 | * Bing Chat 45 | 46 | #+html:

47 | 48 | [[https://github.com/xhcoding/emacs-aichat/blob/main/images/examples.org][View more examples]] 49 | 50 | 51 | Below is information on setting up Bing AI. If you are interested in learning about using OpenAI, please scroll down to the OpenAI section. 52 | 53 | ** Setup 54 | 55 | *** Check access (Required) 56 | 57 | - Install the latest version of Microsoft Edge 58 | - Open http://bing.com/chat 59 | - If you see a chat feature, you are good to go 60 | 61 | *** Getting authentication (Required) 62 | 63 | emacs-aichat uses browser cookies to log in to Bing Chat, so you need to log in to http://www.bing.com first in your browser. 64 | 65 | There are two ways to get the browser's cookie, you can choose any convenient method. 66 | 67 | **** Option one 68 | 69 | If you have a Python environment, run =pip3 install rookiepy= to install =rookiepy=, then open the terminal and execute the following code: 70 | 71 | 72 | #+begin_src sh 73 | python -c "import rookiepy;list(map(lambda c: print('{} {} {} {} {} {}'.format(c['name'], c['value'], c['expires'], c['domain'], c['path'], c['secure'])), filter(lambda c: c['domain'] in ('.bing.com'), rookiepy.edge(['.bing.com']))))" 74 | #+end_src 75 | 76 | If you can see the output and there is a =_U= field in the output, then you can log in successfully with this method. 77 | 78 | **** Option two 79 | 80 | - Install the cookie editor extension for [[https://microsoftedge.microsoft.com/addons/detail/cookieeditor/neaplmfkghagebokkhpjpoebhdledlfi][Edge]] 81 | - Go to bing.com 82 | - Open the extension 83 | - Click "Export" on the bottom right (This saves your cookies to clipboard) 84 | - Paste your cookies into a file cookies.json 85 | - Set =aichat-bingai-cookies-file= to your cookies.json path 86 | 87 | *** Proxy [Optional] 88 | 89 | Currently only tested http proxy, set =aichat-bingai-proxy= as proxy address, for example: 90 | 91 | #+begin_src elisp 92 | (setq aichat-bingai-proxy "localhost:51837") 93 | #+end_src 94 | 95 | *If your IP address is in China, you must set up a proxy to access it.* 96 | 97 | ** Usage 98 | 99 | *** Commands 100 | 101 | - =aichat-bingai-chat= : Send a conversation message, adding =C-u= prefix will start a new conversation 102 | - =aichat-bingai-assistant= : Send the selected region or the input content, and the returned result is displayed in the Buffer defined by =aichat-bingai-assistant-buffer= 103 | - =aichat-bingai-replace-or-insert= : Send the selected region or the input content, and replace the selected region or insert at the current position with the returned result 104 | 105 | *** Customized fixed questioning approach. 106 | 107 | Use =aichat-bingai-prompt-create= to create a fixed way of asking questions, such as: 108 | 109 | #+begin_src elisp 110 | (aichat-bingai-prompt-create "translator" 111 | :input-prompt "Please translate: " 112 | :text-format "Please identify the language I am speaking. If it is Chinese, please translate it into English; if it is not Chinese, please translate it into Chinese. Your answer must only contain the translated content. The following is the sentence I want to translate:\n%s" 113 | :chat t 114 | :assistant t 115 | :replace-or-insert t) 116 | #+end_src 117 | 118 | The above configuration will generate three functions: =aichat-bingai-chat-translator=, =aichat-bingai-assistant-translator=, and =aichat-bingai-replace-or-insert-translator=, which have the same effect as the above three commands, but the content of the query will be in the format of =text-format=. 119 | 120 | 121 | *** Options 122 | 123 | - =aichat-bingai-cookies-file= : The path of cookies file. 124 | - =aichat-bingai-conversation-style= : Conversation style, optional values are: =creative= , =balanced= or =precise= , default is =balanced= 125 | - =aichat-bingai-chat-file= : a file that saves chat history 126 | - =aichat-bingai-chat-display-function= : a function that displays chat files 127 | - =aichat-bingai-assistant-buffer= : Display the buffer name of the content returned by =aichat-bingai-assistant= 128 | - =aichat-bingai-assistant-display-function= : The function to display =aichat-bingai-assistant-buffer=, default to =display-buffer= 129 | 130 | * OpenAI 131 | 132 | #+html:

133 | 134 | ** Setup 135 | 136 | *** AppKey 137 | 138 | emacs-aichat obtains AppKey through =auth-source-pick-first-password=, and writes your appkey into =~/.authinfo= file, with the following format: 139 | 140 | #+begin_src conf 141 | machine platform.openai.com login aichat-openai password your-app-key 142 | #+end_src 143 | 144 | *** Proxy [Optional] 145 | 146 | Currently only tested http proxy, set =aichat-openai-proxy= as proxy address, for example: 147 | 148 | #+begin_src elisp 149 | (setq aichat-openai-proxy "localhost:51837") 150 | #+end_src 151 | 152 | 153 | 154 | ** Usage 155 | 156 | *** Chat 157 | 158 | 1. Create a new chat file using =aichat-openai-chat=. 159 | 2. Enter the chat content. 160 | 3. Use =aichat-openai-chat-send-buffer (C-c C-c)= to send the entire contents of the buffer, 161 | or use =aichat-openai-chat-last-heading (C-c C-l)= to only send the last heading message. 162 | 163 | *** Assistant 164 | 165 | - =aichat-openai-assistant= : Send the selected region or the input content, and the returned result is displayed in the Buffer defined by =aichat-openai-assistant-buffer= 166 | - =aichat-openai-replace-or-insert= : Send the selected region or the input content, and replace the selected region or insert at the current position with the returned result 167 | 168 | *** Customized fixed questioning approach. 169 | 170 | Use =aichat-openai-prompt-create= to create a fixed way of asking questions, such as: 171 | 172 | #+begin_src elisp 173 | (aichat-openai-prompt-create "translator" 174 | :input-prompt "Please translate: " 175 | :text-format "Please identify the language I am speaking. If it is Chinese, please translate it into English; if it is not Chinese, please translate it into Chinese. Your answer must only contain the translated content. The following is the sentence I want to translate:\n%s" 176 | :assistant t 177 | :replace-or-insert t) 178 | #+end_src 179 | 180 | The above configuration will generate two functions: =aichat-openai-assistant-translator=, and =aichat-openai-replace-or-insert-translator=, which have the same effect as the above three commands, but the content of the query will be in the format of =text-format=. 181 | 182 | *** Options 183 | 184 | - =aichat-openai-proxy= : set up an HTTP proxy that requests OpenAI 185 | - =aichat-openai-assistant-buffer= : the buffer name of the OpenAI Assistant result 186 | - =aichat-openai-assistant-display-function= : the function of display assistant buffer 187 | - =aichat-openai-chat-directory= : Directory for storing chat files 188 | - =aichat-openai-chat-display-function= : Function to display chat buffer 189 | 190 | * Feedback Issues 191 | 192 | To feedback your issue, please follow these steps: 193 | 1. Open the debug mode by typing the command =aichat-toggle-debug=. 194 | 2. [[https://github.com/xhcoding/emacs-aichat/issues/new][Submit an issue]] and attach the content of the =*AICHAT-DEBUG*= buffer when an error occurs. 195 | 196 | 197 | * Acknowledgements 198 | 199 | - [[https://github.com/acheong08/EdgeGPT][EdgeGPT]] 200 | - [[https://github.com/manateelazycat/mind-wave][mind-wave]] 201 | - [[https://github.com/f/awesome-chatgpt-prompts][awesome-chatgpt-prompts]] 202 | -------------------------------------------------------------------------------- /test/cookies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": ".bing.com", 4 | "expirationDate": 1710498317.051456, 5 | "hostOnly": false, 6 | "httpOnly": false, 7 | "name": "SnrOvr", 8 | "path": "/", 9 | "sameSite": "no_restriction", 10 | "secure": true, 11 | "session": false, 12 | "storeId": null, 13 | "value": "X=rebateson" 14 | }, 15 | { 16 | "domain": ".bing.com", 17 | "expirationDate": 1713432655.15995, 18 | "hostOnly": false, 19 | "httpOnly": false, 20 | "name": "SRCHUSR", 21 | "path": "/", 22 | "sameSite": "no_restriction", 23 | "secure": true, 24 | "session": false, 25 | "storeId": null, 26 | "value": "DOB=20230313&T=1678872653000" 27 | }, 28 | { 29 | "domain": "www.bing.com", 30 | "expirationDate": 1678877717, 31 | "hostOnly": true, 32 | "httpOnly": false, 33 | "name": "ai_session", 34 | "path": "/", 35 | "sameSite": "no_restriction", 36 | "secure": true, 37 | "session": false, 38 | "storeId": null, 39 | "value": "YB3viLWO/Znf6Lk3ILG7Vu|1678875917973|1678875917973" 40 | }, 41 | { 42 | "domain": ".bing.com", 43 | "expirationDate": 1678894986.106793, 44 | "hostOnly": false, 45 | "httpOnly": true, 46 | "name": "SUID", 47 | "path": "/", 48 | "sameSite": "no_restriction", 49 | "secure": true, 50 | "session": false, 51 | "storeId": null, 52 | "value": "A" 53 | }, 54 | { 55 | "domain": ".bing.com", 56 | "expirationDate": 1713435929.448756, 57 | "hostOnly": false, 58 | "httpOnly": false, 59 | "name": "SRCHHPGUSR", 60 | "path": "/", 61 | "sameSite": "no_restriction", 62 | "secure": true, 63 | "session": false, 64 | "storeId": null, 65 | "value": "SRCHLANG=zh-Hans&PV=15.0.0&BRW=XW&BRH=M&CW=1536&CH=713&SCW=1536&SCH=713&DPR=2.5&UTC=480&DM=0&PRVCW=376&PRVCH=654&HV=1678875929" 66 | }, 67 | { 68 | "domain": ".bing.com", 69 | "expirationDate": 1713435826.720771, 70 | "hostOnly": false, 71 | "httpOnly": false, 72 | "name": "ANON", 73 | "path": "/", 74 | "sameSite": "no_restriction", 75 | "secure": true, 76 | "session": false, 77 | "storeId": null, 78 | "value": "A=851E7E1B1CDEA6BA4C7935D8FFFFFFFF" 79 | }, 80 | { 81 | "domain": ".bing.com", 82 | "expirationDate": 1713432592.626544, 83 | "hostOnly": false, 84 | "httpOnly": false, 85 | "name": "EDGSRVCUSR", 86 | "path": "/", 87 | "sameSite": "no_restriction", 88 | "secure": true, 89 | "session": false, 90 | "storeId": null, 91 | "value": "udscdxtone=Balanced" 92 | }, 93 | { 94 | "domain": "www.bing.com", 95 | "expirationDate": 1710411917.855038, 96 | "hostOnly": true, 97 | "httpOnly": false, 98 | "name": "MicrosoftApplicationsTelemetryDeviceId", 99 | "path": "/", 100 | "sameSite": "no_restriction", 101 | "secure": true, 102 | "session": false, 103 | "storeId": null, 104 | "value": "3b06678e-f1e7-4838-bf17-5c1e98627e28" 105 | }, 106 | { 107 | "domain": ".bing.com", 108 | "hostOnly": false, 109 | "httpOnly": false, 110 | "name": "_SS", 111 | "path": "/", 112 | "sameSite": "no_restriction", 113 | "secure": true, 114 | "session": true, 115 | "storeId": null, 116 | "value": "SID=027B58C3BAA465320D904A16BBE8648E&R=682&RB=682&GB=0&RG=0&RP=679" 117 | }, 118 | { 119 | "domain": ".bing.com", 120 | "hostOnly": false, 121 | "httpOnly": false, 122 | "name": "ipv6", 123 | "path": "/", 124 | "sameSite": "no_restriction", 125 | "secure": true, 126 | "session": true, 127 | "storeId": null, 128 | "value": "hit=1678879516078&t=6" 129 | }, 130 | { 131 | "domain": ".bing.com", 132 | "expirationDate": 1680085426.720741, 133 | "hostOnly": false, 134 | "httpOnly": false, 135 | "name": "_U", 136 | "path": "/", 137 | "sameSite": "no_restriction", 138 | "secure": true, 139 | "session": false, 140 | "storeId": null, 141 | "value": "invalid, do not use this" 142 | }, 143 | { 144 | "domain": ".bing.com", 145 | "expirationDate": 1713252288.20224, 146 | "hostOnly": false, 147 | "httpOnly": false, 148 | "name": "SRCHD", 149 | "path": "/", 150 | "sameSite": "no_restriction", 151 | "secure": true, 152 | "session": false, 153 | "storeId": null, 154 | "value": "AF=NOFORM" 155 | }, 156 | { 157 | "domain": ".bing.com", 158 | "expirationDate": 1681445753, 159 | "hostOnly": false, 160 | "httpOnly": false, 161 | "name": "ANIMIA", 162 | "path": "/", 163 | "sameSite": "no_restriction", 164 | "secure": true, 165 | "session": false, 166 | "storeId": null, 167 | "value": "FRE=1" 168 | }, 169 | { 170 | "domain": ".bing.com", 171 | "expirationDate": 1712388288.202212, 172 | "hostOnly": false, 173 | "httpOnly": true, 174 | "name": "_EDGE_V", 175 | "path": "/", 176 | "sameSite": null, 177 | "secure": false, 178 | "session": false, 179 | "storeId": null, 180 | "value": "1" 181 | }, 182 | { 183 | "domain": ".bing.com", 184 | "expirationDate": 1710498317.0514, 185 | "hostOnly": false, 186 | "httpOnly": false, 187 | "name": "_RwBf", 188 | "path": "/", 189 | "sameSite": "no_restriction", 190 | "secure": true, 191 | "session": false, 192 | "storeId": null, 193 | "value": "r=1&mta=0&rc=682&rb=682&gb=0&rg=0&pc=679&mtu=0&rbb=0.0&g=0&cid=&clo=0&v=1&l=2023-03-15T07:00:00.0000000Z&lft=0001-01-01T00:00:00.0000000&aof=0&o=0&p=bingcopilotwaitlist&c=MY00IA&t=278&s=2023-02-08T12:21:14.6708977+00:00&ts=2023-03-15T10:25:15.2541530+00:00&rwred=0&wls=2&lka=0&lkt=0&TH=&e=CwzCZ3nLPLkMTGl4xLup1VSFF1VNPu5xOIUgc4Bmzcmv7EAbH0TCc9Zx3rkfCE270QhrlYh3PZPt2CWaYRM4Ub4sjg2e28u8Nj-ncm7QD1I&A=" 194 | }, 195 | { 196 | "domain": ".bing.com", 197 | "expirationDate": 1713435910.411322, 198 | "hostOnly": false, 199 | "httpOnly": false, 200 | "name": "_UR", 201 | "path": "/", 202 | "sameSite": "no_restriction", 203 | "secure": true, 204 | "session": false, 205 | "storeId": null, 206 | "value": "QS=0&TQS=0" 207 | }, 208 | { 209 | "domain": ".bing.com", 210 | "hostOnly": false, 211 | "httpOnly": true, 212 | "name": "_EDGE_S", 213 | "path": "/", 214 | "sameSite": null, 215 | "secure": false, 216 | "session": true, 217 | "storeId": null, 218 | "value": "SID=027B58C3BAA465320D904A16BBE8648E" 219 | }, 220 | { 221 | "domain": "www.bing.com", 222 | "expirationDate": 1712571916.350313, 223 | "hostOnly": true, 224 | "httpOnly": true, 225 | "name": "MUIDB", 226 | "path": "/", 227 | "sameSite": null, 228 | "secure": false, 229 | "session": false, 230 | "storeId": null, 231 | "value": "179724F9D38569CC34AB362AD2E768CC" 232 | }, 233 | { 234 | "domain": ".bing.com", 235 | "expirationDate": 1713252288.202229, 236 | "hostOnly": false, 237 | "httpOnly": true, 238 | "name": "USRLOC", 239 | "path": "/", 240 | "sameSite": "no_restriction", 241 | "secure": true, 242 | "session": false, 243 | "storeId": null, 244 | "value": "HS=1" 245 | }, 246 | { 247 | "domain": ".bing.com", 248 | "expirationDate": 1713435910.701659, 249 | "hostOnly": false, 250 | "httpOnly": false, 251 | "name": "_HPVN", 252 | "path": "/", 253 | "sameSite": "no_restriction", 254 | "secure": true, 255 | "session": false, 256 | "storeId": null, 257 | "value": "CS=eyJQbiI6eyJDbiI6MywiU3QiOjAsIlFzIjowLCJQcm9kIjoiUCJ9LCJTYyI6eyJDbiI6MywiU3QiOjAsIlFzIjowLCJQcm9kIjoiSCJ9LCJReiI6eyJDbiI6MywiU3QiOjAsIlFzIjowLCJQcm9kIjoiVCJ9LCJBcCI6dHJ1ZSwiTXV0ZSI6dHJ1ZSwiTGFkIjoiMjAyMy0wMy0xNVQwMDowMDowMFoiLCJJb3RkIjowLCJHd2IiOjAsIkRmdCI6bnVsbCwiTXZzIjowLCJGbHQiOjAsIkltcCI6NX0=" 258 | }, 259 | { 260 | "domain": ".bing.com", 261 | "expirationDate": 1712388289.713958, 262 | "hostOnly": false, 263 | "httpOnly": false, 264 | "name": "MUID", 265 | "path": "/", 266 | "sameSite": "no_restriction", 267 | "secure": true, 268 | "session": false, 269 | "storeId": null, 270 | "value": "179724F9D38569CC34AB362AD2E768CC" 271 | }, 272 | { 273 | "domain": ".bing.com", 274 | "expirationDate": 1713252288.20225, 275 | "hostOnly": false, 276 | "httpOnly": false, 277 | "name": "SRCHUID", 278 | "path": "/", 279 | "sameSite": "no_restriction", 280 | "secure": true, 281 | "session": false, 282 | "storeId": null, 283 | "value": "V=2&GUID=A6A61EF487C34A89B9310787D6436BF1&dmnchg=1" 284 | }, 285 | { 286 | "domain": ".bing.com", 287 | "hostOnly": false, 288 | "httpOnly": false, 289 | "name": "WLS", 290 | "path": "/", 291 | "sameSite": "no_restriction", 292 | "secure": true, 293 | "session": true, 294 | "storeId": null, 295 | "value": "C=d098a0631aa90c9b&N=%e5%8b%87" 296 | } 297 | ] 298 | -------------------------------------------------------------------------------- /test/aichat-util-test.el: -------------------------------------------------------------------------------- 1 | ;;; aichat-util-test.el --- aichat-util-test.el -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat-util-test.el 4 | ;; Description: aichat-util-test.el 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-03-04 23:16:38 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-03-04 23:16:38 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat-util-test.el 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat-util-test.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat-util-test) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat-util-test RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/03/04 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | (require 'ert) 84 | (require 'async-await) 85 | 86 | (add-to-list 'load-path (expand-file-name ".." default-directory)) 87 | 88 | (require 'aichat-util) 89 | 90 | ;;; Hack 91 | 92 | (defun promise-wait (timeout promise) 93 | "Return promise to wait synchronously until PROMISE is resolved or rejected or TIMEOUT. 94 | 95 | Arguments: 96 | - TIMEOUT can accept the various formats. See `run-at-time'. 97 | - PROMISE is any promise object. 98 | 99 | Resolve: 100 | - Return (:fullfilled value), value is PROMISE resolved value. 101 | 102 | Reject: 103 | - Return (:rejected reason), reason is PROMISE rejected reason. 104 | 105 | Timeout: 106 | - Return (:timeouted)." 107 | (declare (indent 1)) 108 | (catch 'done 109 | (let* (handled 110 | (timer (run-at-time timeout nil 111 | (lambda () 112 | (unless handled 113 | (setq handled t) 114 | (throw 'done (promise-reject '(:timeouted)))))))) 115 | (promise-then promise 116 | (lambda (value) 117 | (unless handled 118 | (setq handled t) 119 | (cancel-timer timer) 120 | (throw 'done (promise-resolve `(:fullfilled ,value))))) 121 | (lambda (reason) 122 | (unless handled 123 | (setq handled t) 124 | (cancel-timer timer) 125 | (throw 'done (promise-reject `(:rejected ,reason)))))) 126 | (while t (sit-for 20))))) 127 | 128 | ;;; Code: 129 | 130 | (defun aichat-util-test-create-server-proc () 131 | (let* ((datas) 132 | (server-file (expand-file-name "test/server.js" (file-name-directory (locate-library "aichat-util")))) 133 | (proc (start-process "event-server" nil "node" server-file))) 134 | (when proc 135 | (sleep-for 2) 136 | proc))) 137 | 138 | (ert-deftest aichat-debug () 139 | (let ((buffer-name "*AICHAT-DEBUG*") 140 | (msg "debug message")) 141 | (let ((aichat-debug t)) 142 | (aichat-debug msg) 143 | (should (string= (concat msg "\n") (with-current-buffer buffer-name 144 | (buffer-string)))) 145 | (kill-buffer buffer-name)) 146 | (let ((aichat-debug nil)) 147 | (aichat-debug msg) 148 | (should (not (get-buffer buffer-name)))))) 149 | 150 | (defconst aichat-util-test-json-str "{\"user\":\"xhcoding\",\"age\":1000,\"others\":[\"hello\",1],\"keys\":{\"key\":\"value\"}}") 151 | 152 | (ert-deftest aichat-json-serialize () 153 | (let ((str "hel\"lo")) 154 | (should (string= "\"hel\\\"lo\"" (aichat-json-serialize str)))) 155 | (should (string= aichat-util-test-json-str 156 | (aichat-json-serialize (list :user "xhcoding" :age 1000 :others ["hello" 1] :keys (list :key "value")))))) 157 | 158 | (ert-deftest aichat-json-parse () 159 | (should (equal (aichat-json-parse aichat-util-test-json-str) 160 | '((user . "xhcoding") (age . 1000) (others . ["hello" 1]) (keys (key . "value")))))) 161 | 162 | (ert-deftest aichat-json-access () 163 | (let ((object (aichat-json-parse aichat-util-test-json-str))) 164 | (should (string= "xhcoding" (aichat-json-access object "{user}"))) 165 | (should (= 1000 (aichat-json-access object "{age}"))) 166 | (should (string= "hello" (aichat-json-access object "{others}[0]"))) 167 | (should (= 1 (aichat-json-access object "{others}[1]"))) 168 | (should (string= "value" (aichat-json-access object "{keys}{key}"))) 169 | (should (equal ["hello" 1] (aichat-json-access object "{others}"))))) 170 | 171 | (ert-deftest aichat-get-cookies-from-file() 172 | (let ((cookies (aichat-get-cookies-from-file (expand-file-name "test/cookies.json" (file-name-directory (locate-library "aichat-util")))))) 173 | (cl-loop for cookie in cookies 174 | do (let ((name (car cookie)) 175 | (value (cadr cookie))) 176 | (when (string= name "_U") 177 | (should (string= value "invalid, do not use this"))))))) 178 | 179 | 180 | (defun aichat-http-get-with-backend (backend) 181 | (let ((proc (aichat-util-test-create-server-proc))) 182 | (seq-let (status headers body) 183 | (promise-wait-value 184 | (promise-wait 100 185 | (aichat-http "http://127.0.0.1:12345/get" 186 | :backend backend 187 | :params '(("hello". "world"))))) 188 | (should (string= "200" (car status))) 189 | (let ((body-object (aichat-json-parse body))) 190 | (should (equal (aichat-json-access body-object "{params}") '((hello ."world")))) 191 | (should (string= (aichat-json-access body-object "{headers}{user-agent}") aichat-user-agent)))) 192 | (delete-process proc))) 193 | 194 | (ert-deftest aichat-http-get () 195 | (aichat-http-get-with-backend 'curl) 196 | (aichat-http-get-with-backend 'url)) 197 | 198 | (defun aichat-http-get-with-proxy-with-backend (backend) 199 | (seq-let (status headers body) 200 | (should-error 201 | (promise-wait-value 202 | (promise-wait 100 203 | (aichat-http "https://httpbin.org/get" 204 | :backend backend 205 | :params '(("hello". "world")) 206 | :proxy "error-proxy")))))) 207 | 208 | (ert-deftest aichat-http-get-with-proxy () 209 | (aichat-http-get-with-proxy-with-backend 'curl) 210 | (aichat-http-get-with-proxy-with-backend 'url)) 211 | 212 | (defun aichat-http-get-with-cookie-with-backend (backend) 213 | (url-cookie-store "test" "aichat-http-get-with-cookie" nil "httpbin.org" "/" t) 214 | (let ((proc (aichat-util-test-create-server-proc))) 215 | (seq-let (status headers body) 216 | (promise-wait-value 217 | (promise-wait 100 218 | (aichat-http "http://127.0.0.1:12345/get" 219 | :backend 'curl 220 | :params '(("hello". "world"))))) 221 | (should (string= "200" (car status))) 222 | (let ((body-object (aichat-json-parse body))) 223 | (should (equal (aichat-json-access body-object "{params}") '((hello ."world")))) 224 | (should (string= (aichat-json-access body-object "{headers}{user-agent}") aichat-user-agent)) 225 | (should-not (string-empty-p (aichat-json-access body-object "{headers}{Cookie}"))))) 226 | (delete-process proc))) 227 | 228 | (ert-deftest aichat-http-get-with-cookie () 229 | (aichat-http-get-with-cookie-with-backend 'curl) 230 | (aichat-http-get-with-cookie-with-backend 'url)) 231 | 232 | (defun aichat-http-post-with-backend (backend) 233 | (let ((data (aichat-json-serialize (list :model "gpt-3.5-turbo" 234 | :stream t 235 | :messages (vector 236 | (list :role "user" 237 | :content "Hello, 我是你的助理" ))))) 238 | (proc (aichat-util-test-create-server-proc))) 239 | (seq-let (status headers body) 240 | (promise-wait-value 241 | (promise-wait 100 242 | (aichat-http "http://127.0.0.1:12345/post" 243 | :backend backend 244 | :type "POST" 245 | :headers '(("Content-Type" . "application/json") 246 | ("Authorization" . "Bearer sk-qZbRi")) 247 | :data data))) 248 | (should (string= "200" (car status))) 249 | (let ((body-object (aichat-json-parse body))) 250 | (should (string= data (aichat-json-access body-object "{body}"))))) 251 | (delete-process proc))) 252 | 253 | (ert-deftest aichat-http-post () 254 | (aichat-http-post-with-backend 'curl) 255 | (aichat-http-post-with-backend 'url)) 256 | 257 | (defun aichat-http-event-source-with-backend (backend) 258 | (let* ((datas) 259 | (proc (aichat-util-test-create-server-proc))) 260 | (promise-wait 10 (aichat-http-event-source "http://127.0.0.1:12345/stream" 261 | (lambda (id data) 262 | (push (cons id data) datas)) 263 | :backend backend)) 264 | (should (= 3 (length datas))) 265 | (should (equal (cons "test" "this is data\nthis is new-line") (nth 0 datas))) 266 | (should (equal (cons "test" "this is data\nthis is new-line") (nth 1 datas))) 267 | (should (equal (cons "test" "this is data\nthis is new-line") (nth 2 datas))) 268 | (delete-process proc))) 269 | 270 | (ert-deftest aichat-http-event-source () 271 | (aichat-http-event-source-with-backend 'curl) 272 | (aichat-http-event-source-with-backend 'url)) 273 | 274 | (provide 'aichat-util-test) 275 | 276 | ;;; aichat-util-test.el ends here 277 | -------------------------------------------------------------------------------- /aichat-openai.el: -------------------------------------------------------------------------------- 1 | ;;; aichat-openai.el --- aichat-openai -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat-openai.el 4 | ;; Description: aichat-openai 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-03-10 12:34:27 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-03-10 12:34:27 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat-openai 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat-openai.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat-openai) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat-openai RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/03/10 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | 84 | (require 'aichat-util) 85 | 86 | (require 'markdown-mode) 87 | 88 | ;;; Code: 89 | 90 | (defgroup aichat-openai nil 91 | "OpenAI in Emacs." 92 | :group 'aichat 93 | :prefix "aichat-openai-") 94 | 95 | (defcustom aichat-openai-proxy nil 96 | "Http proxy of request openai api. 97 | 98 | (setq aichat-openai-proxy \"localhost:51837\")" 99 | :group 'aichat-openai 100 | :type 'string) 101 | 102 | (defun aichat-openai--default-api-key-function () 103 | "Fetch the API key with auth-source." 104 | (auth-source-pick-first-password :host "platform.openai.com")) 105 | 106 | (defcustom aichat-openai-api-key #'aichat-openai--default-api-key-function 107 | "OpenAI key as a string or a function that loads and returns it." 108 | :group 'aichat-openai 109 | :type '(choice (function :tag "Function") 110 | (string :tag "String"))) 111 | 112 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 113 | 114 | (defcustom aichat-openai-domain "https://api.openai.com" 115 | "OpenAI domain." 116 | :group 'aichat-openai 117 | :type 'string) 118 | 119 | (defconst aichat-openai--chat-completions-url "/v1/chat/completions" "The url of chat completions api.") 120 | 121 | (defconst aichat-openai--chat-completions-model "gpt-3.5-turbo" "The model of chat completions api.") 122 | 123 | (defconst aichat-openai--chat-completions-request-type "POST" "The request type of chat completions api.") 124 | 125 | (defun aichat-openai--make-http-headers () 126 | "Create http headers to send in requests to the API." 127 | `(("Content-Type" . "application/json") 128 | ("Authorization" . ,(format "Bearer %s" (if (functionp aichat-openai-api-key) 129 | (funcall aichat-openai-api-key) 130 | aichat-openai-api-key))))) 131 | 132 | (cl-defun aichat-openai-make-chat-messages (&rest settings 133 | &key 134 | user 135 | system 136 | assistant) 137 | "Make chat completions messages. 138 | 139 | (aichat-openai--make-chat-messages 140 | :user \"hello\" 141 | :assistant \"Hello there, how may I assist you today?\")" 142 | (vconcat 143 | (cl-loop for (key value) on settings by #'cddr 144 | collect (list :role (string-trim-left (symbol-name key) ":") :content value)))) 145 | 146 | (cl-defun aichat-openai-chat-completions (messages &rest settings 147 | &key 148 | on-success 149 | on-error 150 | &allow-other-keys 151 | ) 152 | "Request openai chat completions api. 153 | 154 | Look https://platform.openai.com/docs/api-reference/chat." 155 | 156 | (let* ((args (cl-loop for (key value) on settings by #'cddr 157 | append (unless (member key '(:on-success :on-error)) 158 | (list key value)))) 159 | (data (aichat-json-serialize (append 160 | (list 161 | :model aichat-openai--chat-completions-model 162 | :messages messages) 163 | args)))) 164 | 165 | (aichat-debug "Request %s with data:\n%s\n" aichat-openai--chat-completions-url data) 166 | 167 | (promise-then (aichat-http (concat aichat-openai-domain aichat-openai--chat-completions-url) 168 | :proxy aichat-openai-proxy 169 | :type aichat-openai--chat-completions-request-type 170 | :headers (aichat-openai--make-http-headers) 171 | :data data) 172 | (lambda (value) 173 | (aichat-debug "Received: %s" value) 174 | (seq-let (status headers body) value 175 | (if (string= "200" (car status)) 176 | (when on-success 177 | (funcall on-success (aichat-json-parse body))) 178 | (when on-error 179 | (funcall on-error body))))) 180 | (lambda (err) 181 | (when on-error 182 | (funcall on-error err)))))) 183 | 184 | (cl-defun aichat-openai-chat-completions-stream (messages callback &rest settings 185 | &key 186 | on-success 187 | on-error 188 | &allow-other-keys 189 | ) 190 | "Request openai chat completions api with stream mode. 191 | 192 | Look https://platform.openai.com/docs/api-reference/chat for more request params." 193 | (let* ((args (cl-loop for (key value) on settings by #'cddr 194 | append (unless (member key '(:on-success :on-error)) 195 | (list key value)))) 196 | (data (aichat-json-serialize (append 197 | (list 198 | :model aichat-openai--chat-completions-model 199 | :messages messages 200 | :stream t) 201 | args)))) 202 | 203 | (aichat-debug "Request %s with data:\n%s\n" aichat-openai--chat-completions-url data) 204 | 205 | (promise-then (aichat-http-event-source (concat aichat-openai-domain aichat-openai--chat-completions-url) 206 | (lambda (event-id event-data) 207 | (aichat-debug "Received event: %s, data: \n%s\n" event-id event-data) 208 | (unless (string= event-data "[DONE]") 209 | (when callback 210 | (funcall callback (aichat-json-parse event-data))))) 211 | :proxy aichat-openai-proxy 212 | :type aichat-openai--chat-completions-request-type 213 | :headers (aichat-openai--make-http-headers) 214 | :data data) 215 | (lambda (value) 216 | (seq-let (status headers body) value 217 | (if (string= "200" (car status)) 218 | (when on-success 219 | (funcall on-success body)) 220 | (when on-error 221 | (funcall on-error body))))) 222 | (lambda (err) 223 | (when on-error 224 | (funcall on-error err)))))) 225 | 226 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Message API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 227 | 228 | (defun aichat-openai-chat-completions-content (msg) 229 | "[choices][0][message][content]" 230 | (aichat-json-access msg "{choices}[0]{message}{content}")) 231 | 232 | (defun aichat-openai-chat-completions-delta-content (msg) 233 | "[choices][0][delta][content]" 234 | (aichat-json-access msg "{choices}[0]{delta}{content}")) 235 | 236 | (defun aichat-openai-chat-completions-delta-role (msg) 237 | "[choices][0][delta][role]" 238 | (aichat-json-access msg "{choices}[0]{delta}{role}")) 239 | 240 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Chat ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 241 | 242 | (defcustom aichat-openai-chat-directory (expand-file-name "aichat/openai" user-emacs-directory) 243 | "The directory of save chat file." 244 | :group 'aichat-openai 245 | :type 'string) 246 | 247 | (defcustom aichat-openai-chat-display-function 'switch-to-buffer 248 | "The function of display chat buffer." 249 | :group 'aichat-openai 250 | :type 'symbol) 251 | 252 | (defun aichat-openai-chat--buffer-messages (buffer) 253 | "Make messages with BUFFER's content." 254 | (with-current-buffer buffer 255 | (save-mark-and-excursion 256 | (goto-char (point-min)) 257 | (let ((messages (list)) 258 | (role) 259 | (message-beg) 260 | (message-end) 261 | (message)) 262 | (catch 'finished 263 | (while t 264 | (if (re-search-forward "^# \\([a-zA-Z]+\\)\n" nil t) 265 | (setq role (downcase (string-trim (match-string 1)))) 266 | (throw 'finished nil)) 267 | (setq message-beg (point)) 268 | (if (re-search-forward "^#" nil t) 269 | (progn 270 | (backward-char) 271 | (setq message-end (point))) 272 | (setq message-end (point-max))) 273 | (setq message (string-trim (buffer-substring-no-properties message-beg message-end))) 274 | (push (intern (concat ":" role)) messages) 275 | (push message messages))) 276 | (reverse messages))))) 277 | 278 | (defun aichat-openai-chat--heading-messages (buffer) 279 | "Make messages with BUFFER's last heading content." 280 | (with-current-buffer buffer 281 | (save-mark-and-excursion 282 | (goto-char (point-max)) 283 | (when (re-search-backward "^# " nil t) 284 | (save-restriction 285 | (narrow-to-region (point) (point-max)) 286 | (aichat-openai-chat--buffer-messages (current-buffer))))))) 287 | 288 | (defun aichat-openai-chat--send-messages (messages buffer) 289 | "Send MESSAGES to OpenAI, insert response to BUFFER." 290 | (aichat-openai-chat-completions-stream 291 | (apply #'aichat-openai-make-chat-messages messages) 292 | (lambda (msg) 293 | (let ((delta-role (aichat-openai-chat-completions-delta-role msg)) 294 | (delta-content (aichat-openai-chat-completions-delta-content msg))) 295 | (when (buffer-live-p buffer) 296 | (with-current-buffer buffer 297 | (save-mark-and-excursion 298 | (goto-char (point-max)) 299 | (when delta-role 300 | (insert (format "\n# %s%s\n\n" (upcase (substring delta-role 0 1)) (substring delta-role 1)))) 301 | (when delta-content 302 | (insert delta-content))))))) 303 | :on-success (lambda (_) 304 | (when (buffer-live-p buffer) 305 | (with-current-buffer buffer 306 | (goto-char (point-max)) 307 | (insert "\n\n# User\n\n")))) 308 | :on-error (lambda (err) 309 | (message "error: %s"err)))) 310 | 311 | (defun aichat-openai-chat-send-buffer () 312 | "Send current buffer content to OpenAI." 313 | (interactive) 314 | (when-let* ((buffer (current-buffer)) 315 | (messages (aichat-openai-chat--buffer-messages buffer))) 316 | (aichat-openai-chat--send-messages messages buffer))) 317 | 318 | (defun aichat-openai-chat-send-last-heading () 319 | "Send last heading content to OpenAI." 320 | (interactive) 321 | (when-let* ((buffer (current-buffer)) 322 | (messages (aichat-openai-chat--heading-messages buffer))) 323 | (aichat-openai-chat--send-messages messages buffer))) 324 | 325 | ;;;###autoload 326 | (add-to-list 'auto-mode-alist '("\\.aichat\\'" . aichat-openai-chat-mode)) 327 | 328 | ;;;###autoload 329 | (define-derived-mode aichat-openai-chat-mode markdown-mode "aichat-openai-chat" 330 | "Major mode for openai chat." 331 | (setq-local markdown-hide-markup t) 332 | (when (string-empty-p (buffer-string)) 333 | (insert "# User\n")) 334 | (define-key aichat-openai-chat-mode-map (kbd "C-c C-c") #'aichat-openai-chat-send-buffer) 335 | (define-key aichat-openai-chat-mode-map (kbd "C-c C-l") #'aichat-openai-chat-send-last-heading)) 336 | 337 | ;;;###autoload 338 | (defun aichat-openai-chat (buffer) 339 | "If the chat buffer is not found, create a new chat buffer, otherwise switch to the opened chat buffer." 340 | (interactive (list (let ((opened-buffers (cl-loop for buffer in (buffer-list) 341 | append (with-current-buffer buffer 342 | (when (derived-mode-p 'aichat-openai-chat-mode) 343 | (list (buffer-name))))))) 344 | (if (and opened-buffers (not (and (car current-prefix-arg) 345 | (= (car current-prefix-arg) 4)))) 346 | (if (= 1 (length opened-buffers)) 347 | (car opened-buffers) 348 | (completing-read "Select buffer: " opened-buffers)) 349 | (when-let* ((name 350 | (completing-read "Chat name: " 351 | (if (file-exists-p aichat-openai-chat-directory) 352 | (directory-files aichat-openai-chat-directory nil 353 | "^\\([^.]\\|\\.[^.]\\|\\.\\..\\)") 354 | (list)))) 355 | (filename (expand-file-name (if (string-suffix-p ".aichat" name) name 356 | (concat name ".aichat")) 357 | aichat-openai-chat-directory))) 358 | (make-directory aichat-openai-chat-directory t) 359 | (find-file-noselect filename)))))) 360 | (funcall aichat-openai-chat-display-function buffer)) 361 | 362 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Assistant ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 363 | 364 | (defcustom aichat-openai-assistant-buffer "*Aichat-OpenAI-Assistant*" 365 | "The buffer of show assistant message." 366 | :group 'aichat 367 | :type 'string) 368 | 369 | (defcustom aichat-openai-assistant-display-function 'display-buffer 370 | "The function of display `aichat-openai-assistant-buffer'." 371 | :group 'aichat 372 | :type 'symbol) 373 | 374 | (defun aichat-openai-assistant-get-buffer () 375 | (get-buffer-create aichat-openai-assistant-buffer)) 376 | 377 | (defun aichat-openai-assistant (text) 378 | "Send the region or input to Bing and display the returned result to `aichat-openai-assistant-buffer'." 379 | (interactive (list (aichat-read-region-or-input "Input text: "))) 380 | (aichat-openai-chat-completions (aichat-openai-make-chat-messages 381 | :user text) 382 | :on-success (lambda (msg) 383 | (let ((content (aichat-openai-chat-completions-content msg)) 384 | (buffer (aichat-openai-assistant-get-buffer))) 385 | (with-current-buffer buffer 386 | (goto-char (point-max)) 387 | (insert content) 388 | (insert "\n\n")) 389 | (funcall aichat-openai-assistant-display-function buffer))) 390 | :on-error (lambda (err) 391 | (message "error: %s"err)))) 392 | 393 | (defun aichat-openai-replace-or-insert (text) 394 | "Send the region or input to OpenAI and replace the selected region or insert at the current position with the returned result." 395 | (interactive (list (aichat-read-region-or-input "Input text: "))) 396 | (when (and text (not (string-empty-p text))) 397 | (let* ((cur-buf (current-buffer)) 398 | (cur-pos (with-current-buffer cur-buf (point))) 399 | (reg-beg (when (use-region-p) (region-beginning))) 400 | (reg-end (when (use-region-p) (region-end)))) 401 | (aichat-openai-chat-completions (aichat-openai-make-chat-messages 402 | :user text) 403 | :on-success (lambda (msg) 404 | (when-let ((content (aichat-openai-chat-completions-content msg))) 405 | (with-current-buffer cur-buf 406 | (if (and reg-beg reg-end) 407 | (replace-region-contents reg-beg reg-end (lambda () content)) 408 | (goto-char cur-pos) 409 | (insert content))))) 410 | :on-error (lambda (err) 411 | (message "Error: %s" err)))))) 412 | 413 | (cl-defmacro aichat-openai-prompt-create (name &rest args 414 | &key 415 | (input-prompt "Input text: ") 416 | (text-format "%s") 417 | (assistant nil) 418 | (replace-or-insert nil)) 419 | "This macro will generate two functions: aichat-openai-assistant-name or aichat-openai-replace-or-insert-name. 420 | 421 | INPUT-PROMPT: The prompt before the user input in minibuffer. 422 | TEXT-FORMAT: Formating string, %s is replaced by what the user input." 423 | (let ((assistant-func (intern (format "aichat-openai-assistant-%s" name))) 424 | (replace-func (intern (format "aichat-openai-replace-or-insert-%s" name)))) 425 | `(progn 426 | (when ,assistant 427 | (defun ,assistant-func(text) 428 | (interactive (list (aichat-read-region-or-input ,input-prompt))) 429 | (when text 430 | (let ((query (format ,text-format text))) 431 | (aichat-openai-assistant query))))) 432 | (when ,replace-or-insert 433 | (defun ,replace-func(text) 434 | (interactive (list (aichat-read-region-or-input ,input-prompt))) 435 | (when text 436 | (let ((query (format ,text-format text))) 437 | (aichat-openai-replace-or-insert query)))))))) 438 | 439 | (provide 'aichat-openai) 440 | 441 | ;;; aichat-openai.el ends here 442 | -------------------------------------------------------------------------------- /test/aichat-bingai-message-type-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": 2, 3 | "invocationId": "6", 4 | "item": { 5 | "messages": [ 6 | { 7 | "text": "如何判断链表的长度?", 8 | "author": "user", 9 | "from": { 10 | "id": "1055518581560278", 11 | "name": null, 12 | "partnerId": null 13 | }, 14 | "createdAt": "2023-11-18T04:05:13.1221273+00:00", 15 | "timestamp": "2023-11-18T11:35:37+08:00", 16 | "locale": "zh-CN", 17 | "market": "zh-AU", 18 | "region": "AU", 19 | "location": "lat:47.639557;long:-122.128159;re=1000m;", 20 | "locationInfo": { 21 | "country": "澳洲", 22 | "state": "New South Wales", 23 | "city": "悉尼中央商务区", 24 | "sourceType": 1 25 | }, 26 | "locationHints": [ 27 | { 28 | "countryConfidence": 9, 29 | "dma": 0, 30 | "sourceType": 1, 31 | "utcOffset": 10, 32 | "postCodeName": "2000", 33 | "populatedPlaceConfidence": 9, 34 | "populatedPlaceName": "Sydney", 35 | "admin1Name": "New South Wales", 36 | "countryName": "Australia", 37 | "fdConfidence": 0.9, 38 | "accuracy": 24902, 39 | "name": "Sydney, New South Wales", 40 | "radius": 24902, 41 | "center": { 42 | "latitude": -33.8683, 43 | "longitude": 151.2082, 44 | "height": null 45 | }, 46 | "regionType": 2 47 | } 48 | ], 49 | "userIpAddress": "2a11:3:200::108c", 50 | "messageId": "77b13b73-8138-11f6-ff28-b0375509df59", 51 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 52 | "nlu": { 53 | "scoredClassification": { 54 | "classification": "DEEP_LEO", 55 | "score": null 56 | }, 57 | "classificationRanking": [ 58 | { 59 | "classification": "DEEP_LEO", 60 | "score": null 61 | } 62 | ], 63 | "qualifyingClassifications": null, 64 | "ood": null, 65 | "metaData": null, 66 | "entities": null 67 | }, 68 | "offense": "None", 69 | "feedback": { 70 | "tag": null, 71 | "updatedOn": null, 72 | "type": "None" 73 | }, 74 | "contentOrigin": "cib", 75 | "scores": [ 76 | { 77 | "component": "UserOffense", 78 | "score": 0.00019260288 79 | }, 80 | { 81 | "component": "suicide_help", 82 | "score": 0.016403032 83 | } 84 | ], 85 | "inputMethod": "Keyboard" 86 | }, 87 | { 88 | "text": "Searching the web for: `如何判断链表的长度`", 89 | "hiddenText": "如何判断链表的长度", 90 | "author": "bot", 91 | "createdAt": "2023-11-18T04:05:17.5267446+00:00", 92 | "timestamp": "2023-11-18T04:05:17.5267446+00:00", 93 | "messageId": "ae3eb3f1-f874-4c8a-bea9-fa75e907c994", 94 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 95 | "messageType": "InternalSearchQuery", 96 | "offense": "None", 97 | "adaptiveCards": [ 98 | { 99 | "type": "AdaptiveCard", 100 | "version": "1.0", 101 | "body": [ 102 | { 103 | "type": "RichTextBlock", 104 | "inlines": [ 105 | { 106 | "type": "TextRun", 107 | "isSubtle": true, 108 | "italic": true, 109 | "text": "Searching the web for: `如何判断链表的长度`" 110 | } 111 | ] 112 | } 113 | ] 114 | } 115 | ], 116 | "feedback": { 117 | "tag": null, 118 | "updatedOn": null, 119 | "type": "None" 120 | }, 121 | "contentOrigin": "DeepLeo", 122 | "toolResultPublishPreference": "Never" 123 | }, 124 | { 125 | "text": "{\"question_answering_results\":[{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\"],\"url\":\"https://bing.com/search?q=%e5%a6%82%e4%bd%95%e5%88%a4%e6%96%ad%e9%93%be%e8%a1%a8%e7%9a%84%e9%95%bf%e5%ba%a6\"}],\"web_search_results\":[{\"title\":\"Java LinkedList | 菜鸟教程\",\"snippets\":[\"Java LinkedList | 菜鸟教程 Java LinkedList Java 集合框架 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。 LinkedList 实现了 Queue 接口,可作为队列使用。 LinkedList 实现了 List 接口,可进行列表的相关操作。 LinkedList 实现了 Deque 接口,可作为队列使用。 LinkedList 实现了 Cloneable 接口,可实现克隆。 LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。 LinkedList 类位于 java.util 包中,使用前需要引入它,语法格式如下: // 引入 LinkedList 类 import java.util.LinkedList; LinkedList list = new LinkedList (); // 普通创建方法 或者 LinkedList list = new LinkedList (Collection c); // 使用集合创建链表 创建一个简单的链表实例: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Weibo] 更多的情况下我们使用 ArrayList 访问列表中的随机元素更加高效,但以下几种情况 LinkedList 提供了更高效的方法。 在列表开头添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addFirst () 在头部添加元素 sites. addFirst(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Wiki, Google, Runoob, Taobao] 在列表结尾添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addLast () 在尾部添加元素 sites. addLast(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Wiki] 在列表开头移除元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeFirst(); System. out. println( sites); } } 以上实例,执行输出结果为: [Runoob, Taobao, Weibo] 在列表结尾移除元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeLast(); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao] 获取列表开头的元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites. getFirst()); } } 以上实例,执行输出结果为:\",\"一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。\"],\"url\":\"https://www.runoob.com/java/java-linkedlist.html\"},{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"C++程序 寻找链表长度|极客笔记 当前位置: 极客笔记 > C++ > C++ 示例 > C++程序 寻找链表长度 C++程序 寻找链表长度 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个 C ++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表定义示例,包含了节点的数据类型和链表的数据类型: struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } }; 计算链表长度 要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示: int getLengthRecursion(ListNode *node) { if (node == NULL) { return 0; } else { return 1 + getLengthRecursion(node->next); } } 函数中如果节点为空,则长度为0;否则不断递归到下一个节点,一直到最后一个节点,返回的是递归到该节点的长度+1。 使用示例 我们可以编写一个主函数,来验证计算链表长度的函数是否正确。 下面是一个使用示例,包含了链表的添加和输出功能,以及计算链表长度的功能: #include using namespace std; struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } void addNode(int val) { if (head == NULL) { head = new ListNode(val); } else { ListNode *p = head; while (p->next != NULL) p = p->next; p->next = new ListNode(val); } } void printList() { ListNode *p = head; while (p != NULL) { cout << p->val << \\\" \\\"; p = p->next; } cout << endl; } }; int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } int main() { LinkedList list; list.addNode(1); list.addNode(2); list.addNode(3); list.addNode(4); list.addNode(5); list.printList(); cout << \\\"Length of the linked list: \\\" << getLength(&list) << endl; return 0; } 输出: 1 2 3 4 5 Length of the linked list: 5 结论 计算链表长度是一个简单而常用的操作,可以使用循环和递归两种方法来实现。 两种方法虽然代码不同,但本质相同,都是遍历整个链表,统计节点数量。 在实际应用中,要根据具体情况选择不同的方法。 上一篇 C++程序 查找两个链表交点 下一篇 C++程序 寻找给定和的子数组-第1集(非负数) Camera课程 API2 教程 Perf 教程 Native FW 教程 ISP 教程 Sensor 教程 Python教程 Python 教程 NumPy 教程 Django 教程 Java教程 Java 教程 Web教程 JavaScript 教程 jQuery 教程 CSS 教程 PHP 教程 Laravel 教程 TypeScript 教程 WordPress 教程 数据库教程 SQL 教程 SQLite 教程 MySQL 教程 PL/SQL 教程 MongoDB 教程 图形图像教程 OpenCV 教程 办公软件教程 Excel 教程 Linux教程 Linux命令全集 Linux内核API LVGL 教程 计算机教程 操作系统 教程 计算机网络 教程 C语言 教程 C++ 教程 Swift 教程 大数据教程 Hadoop 教程 Spark 教程 Scala 教程 开发工具教程 Git 教程 Jenkins 教程 ChatGPT 教程 IntelliJ 教程 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个C++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表\",\"下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\",\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示:\"],\"url\":\"https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html\"}]}", 126 | "hiddenText": "search_web('如何判断链表的长度') was invoked and returned:\n```json\n{\"question_answering_results\":[{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\"],\"url\":\"https://bing.com/search?q=%e5%a6%82%e4%bd%95%e5%88%a4%e6%96%ad%e9%93%be%e8%a1%a8%e7%9a%84%e9%95%bf%e5%ba%a6\"}],\"web_search_results\":[{\"title\":\"Java LinkedList | 菜鸟教程\",\"snippets\":[\"Java LinkedList | 菜鸟教程 Java LinkedList Java 集合框架 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。 LinkedList 实现了 Queue 接口,可作为队列使用。 LinkedList 实现了 List 接口,可进行列表的相关操作。 LinkedList 实现了 Deque 接口,可作为队列使用。 LinkedList 实现了 Cloneable 接口,可实现克隆。 LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。 LinkedList 类位于 java.util 包中,使用前需要引入它,语法格式如下: // 引入 LinkedList 类 import java.util.LinkedList; LinkedList list = new LinkedList (); // 普通创建方法 或者 LinkedList list = new LinkedList (Collection c); // 使用集合创建链表 创建一个简单的链表实例: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Weibo] 更多的情况下我们使用 ArrayList 访问列表中的随机元素更加高效,但以下几种情况 LinkedList 提供了更高效的方法。 在列表开头添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addFirst () 在头部添加元素 sites. addFirst(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Wiki, Google, Runoob, Taobao] 在列表结尾添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addLast () 在尾部添加元素 sites. addLast(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Wiki] 在列表开头移除元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeFirst(); System. out. println( sites); } } 以上实例,执行输出结果为: [Runoob, Taobao, Weibo] 在列表结尾移除元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeLast(); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao] 获取列表开头的元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites. getFirst()); } } 以上实例,执行输出结果为:\",\"一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。\"],\"url\":\"https://www.runoob.com/java/java-linkedlist.html\"},{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"C++程序 寻找链表长度|极客笔记 当前位置: 极客笔记 > C++ > C++ 示例 > C++程序 寻找链表长度 C++程序 寻找链表长度 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个 C ++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表定义示例,包含了节点的数据类型和链表的数据类型: struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } }; 计算链表长度 要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示: int getLengthRecursion(ListNode *node) { if (node == NULL) { return 0; } else { return 1 + getLengthRecursion(node->next); } } 函数中如果节点为空,则长度为0;否则不断递归到下一个节点,一直到最后一个节点,返回的是递归到该节点的长度+1。 使用示例 我们可以编写一个主函数,来验证计算链表长度的函数是否正确。 下面是一个使用示例,包含了链表的添加和输出功能,以及计算链表长度的功能: #include using namespace std; struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } void addNode(int val) { if (head == NULL) { head = new ListNode(val); } else { ListNode *p = head; while (p->next != NULL) p = p->next; p->next = new ListNode(val); } } void printList() { ListNode *p = head; while (p != NULL) { cout << p->val << \\\" \\\"; p = p->next; } cout << endl; } }; int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } int main() { LinkedList list; list.addNode(1); list.addNode(2); list.addNode(3); list.addNode(4); list.addNode(5); list.printList(); cout << \\\"Length of the linked list: \\\" << getLength(&list) << endl; return 0; } 输出: 1 2 3 4 5 Length of the linked list: 5 结论 计算链表长度是一个简单而常用的操作,可以使用循环和递归两种方法来实现。 两种方法虽然代码不同,但本质相同,都是遍历整个链表,统计节点数量。 在实际应用中,要根据具体情况选择不同的方法。 上一篇 C++程序 查找两个链表交点 下一篇 C++程序 寻找给定和的子数组-第1集(非负数) Camera课程 API2 教程 Perf 教程 Native FW 教程 ISP 教程 Sensor 教程 Python教程 Python 教程 NumPy 教程 Django 教程 Java教程 Java 教程 Web教程 JavaScript 教程 jQuery 教程 CSS 教程 PHP 教程 Laravel 教程 TypeScript 教程 WordPress 教程 数据库教程 SQL 教程 SQLite 教程 MySQL 教程 PL/SQL 教程 MongoDB 教程 图形图像教程 OpenCV 教程 办公软件教程 Excel 教程 Linux教程 Linux命令全集 Linux内核API LVGL 教程 计算机教程 操作系统 教程 计算机网络 教程 C语言 教程 C++ 教程 Swift 教程 大数据教程 Hadoop 教程 Spark 教程 Scala 教程 开发工具教程 Git 教程 Jenkins 教程 ChatGPT 教程 IntelliJ 教程 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个C++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表\",\"下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\",\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示:\"],\"url\":\"https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html\"}]}\n```", 127 | "author": "bot", 128 | "createdAt": "2023-11-18T04:05:17.8441733+00:00", 129 | "timestamp": "2023-11-18T04:05:17.8441733+00:00", 130 | "messageId": "0fedf8a8-2d62-4f37-b3ce-f5378e798356", 131 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 132 | "messageType": "InternalSearchResult", 133 | "offense": "None", 134 | "adaptiveCards": [ 135 | { 136 | "type": "AdaptiveCard", 137 | "version": "1.0", 138 | "body": [ 139 | { 140 | "type": "RichTextBlock", 141 | "inlines": [ 142 | { 143 | "type": "TextRun", 144 | "isSubtle": true, 145 | "italic": true, 146 | "text": "{\"question_answering_results\":[{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\"],\"url\":\"https://bing.com/search?q=%e5%a6%82%e4%bd%95%e5%88%a4%e6%96%ad%e9%93%be%e8%a1%a8%e7%9a%84%e9%95%bf%e5%ba%a6\"}],\"web_search_results\":[{\"title\":\"Java LinkedList | 菜鸟教程\",\"snippets\":[\"Java LinkedList | 菜鸟教程 Java LinkedList Java 集合框架 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。 LinkedList 实现了 Queue 接口,可作为队列使用。 LinkedList 实现了 List 接口,可进行列表的相关操作。 LinkedList 实现了 Deque 接口,可作为队列使用。 LinkedList 实现了 Cloneable 接口,可实现克隆。 LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。 LinkedList 类位于 java.util 包中,使用前需要引入它,语法格式如下: // 引入 LinkedList 类 import java.util.LinkedList; LinkedList list = new LinkedList (); // 普通创建方法 或者 LinkedList list = new LinkedList (Collection c); // 使用集合创建链表 创建一个简单的链表实例: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Weibo] 更多的情况下我们使用 ArrayList 访问列表中的随机元素更加高效,但以下几种情况 LinkedList 提供了更高效的方法。 在列表开头添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addFirst () 在头部添加元素 sites. addFirst(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Wiki, Google, Runoob, Taobao] 在列表结尾添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); // 使用 addLast () 在尾部添加元素 sites. addLast(\\\"Wiki\\\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Wiki] 在列表开头移除元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeFirst(); System. out. println( sites); } } 以上实例,执行输出结果为: [Runoob, Taobao, Weibo] 在列表结尾移除元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); sites. removeLast(); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao] 获取列表开头的元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\\\"Google\\\"); sites. add(\\\"Runoob\\\"); sites. add(\\\"Taobao\\\"); sites. add(\\\"Weibo\\\"); System. out. println( sites. getFirst()); } } 以上实例,执行输出结果为:\",\"一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。\"],\"url\":\"https://www.runoob.com/java/java-linkedlist.html\"},{\"title\":\"C++程序 寻找链表长度|极客笔记\",\"snippets\":[\"C++程序 寻找链表长度|极客笔记 当前位置: 极客笔记 > C++ > C++ 示例 > C++程序 寻找链表长度 C++程序 寻找链表长度 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个 C ++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表定义示例,包含了节点的数据类型和链表的数据类型: struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } }; 计算链表长度 要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示: int getLengthRecursion(ListNode *node) { if (node == NULL) { return 0; } else { return 1 + getLengthRecursion(node->next); } } 函数中如果节点为空,则长度为0;否则不断递归到下一个节点,一直到最后一个节点,返回的是递归到该节点的长度+1。 使用示例 我们可以编写一个主函数,来验证计算链表长度的函数是否正确。 下面是一个使用示例,包含了链表的添加和输出功能,以及计算链表长度的功能: #include using namespace std; struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } void addNode(int val) { if (head == NULL) { head = new ListNode(val); } else { ListNode *p = head; while (p->next != NULL) p = p->next; p->next = new ListNode(val); } } void printList() { ListNode *p = head; while (p != NULL) { cout << p->val << \\\" \\\"; p = p->next; } cout << endl; } }; int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } int main() { LinkedList list; list.addNode(1); list.addNode(2); list.addNode(3); list.addNode(4); list.addNode(5); list.printList(); cout << \\\"Length of the linked list: \\\" << getLength(&list) << endl; return 0; } 输出: 1 2 3 4 5 Length of the linked list: 5 结论 计算链表长度是一个简单而常用的操作,可以使用循环和递归两种方法来实现。 两种方法虽然代码不同,但本质相同,都是遍历整个链表,统计节点数量。 在实际应用中,要根据具体情况选择不同的方法。 上一篇 C++程序 查找两个链表交点 下一篇 C++程序 寻找给定和的子数组-第1集(非负数) Camera课程 API2 教程 Perf 教程 Native FW 教程 ISP 教程 Sensor 教程 Python教程 Python 教程 NumPy 教程 Django 教程 Java教程 Java 教程 Web教程 JavaScript 教程 jQuery 教程 CSS 教程 PHP 教程 Laravel 教程 TypeScript 教程 WordPress 教程 数据库教程 SQL 教程 SQLite 教程 MySQL 教程 PL/SQL 教程 MongoDB 教程 图形图像教程 OpenCV 教程 办公软件教程 Excel 教程 Linux教程 Linux命令全集 Linux内核API LVGL 教程 计算机教程 操作系统 教程 计算机网络 教程 C语言 教程 C++ 教程 Swift 教程 大数据教程 Hadoop 教程 Spark 教程 Scala 教程 开发工具教程 Git 教程 Jenkins 教程 ChatGPT 教程 IntelliJ 教程 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个C++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表\",\"下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。\",\"要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示:\"],\"url\":\"https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html\"}]}" 147 | } 148 | ] 149 | } 150 | ] 151 | } 152 | ], 153 | "feedback": { 154 | "tag": null, 155 | "updatedOn": null, 156 | "type": "None" 157 | }, 158 | "contentOrigin": "DeepLeo", 159 | "invocation": "search_web(\"如何判断链表的长度\")", 160 | "groundingInfo": { 161 | "question_answering_results": [ 162 | { 163 | "index": null, 164 | "title": "C++程序 寻找链表长度|极客笔记", 165 | "snippets": [ 166 | "要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。" 167 | ], 168 | "data": null, 169 | "context": null, 170 | "url": "https://bing.com/search?q=%e5%a6%82%e4%bd%95%e5%88%a4%e6%96%ad%e9%93%be%e8%a1%a8%e7%9a%84%e9%95%bf%e5%ba%a6", 171 | "lastUpdatedDate": null 172 | } 173 | ], 174 | "web_search_results": [ 175 | { 176 | "index": null, 177 | "title": "Java LinkedList | 菜鸟教程", 178 | "snippets": [ 179 | "Java LinkedList | 菜鸟教程 Java LinkedList Java 集合框架 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。 LinkedList 实现了 Queue 接口,可作为队列使用。 LinkedList 实现了 List 接口,可进行列表的相关操作。 LinkedList 实现了 Deque 接口,可作为队列使用。 LinkedList 实现了 Cloneable 接口,可实现克隆。 LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。 LinkedList 类位于 java.util 包中,使用前需要引入它,语法格式如下: // 引入 LinkedList 类 import java.util.LinkedList; LinkedList list = new LinkedList (); // 普通创建方法 或者 LinkedList list = new LinkedList (Collection c); // 使用集合创建链表 创建一个简单的链表实例: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); sites. add(\"Weibo\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Weibo] 更多的情况下我们使用 ArrayList 访问列表中的随机元素更加高效,但以下几种情况 LinkedList 提供了更高效的方法。 在列表开头添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); // 使用 addFirst () 在头部添加元素 sites. addFirst(\"Wiki\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Wiki, Google, Runoob, Taobao] 在列表结尾添加元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); // 使用 addLast () 在尾部添加元素 sites. addLast(\"Wiki\"); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao, Wiki] 在列表开头移除元素: 实例 // 引入 LinkedList 类 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); sites. add(\"Weibo\"); sites. removeFirst(); System. out. println( sites); } } 以上实例,执行输出结果为: [Runoob, Taobao, Weibo] 在列表结尾移除元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); sites. add(\"Weibo\"); sites. removeLast(); System. out. println( sites); } } 以上实例,执行输出结果为: [Google, Runoob, Taobao] 获取列表开头的元素: 实例 import java.util.LinkedList; public class RunoobTest { public static void main (String[] args) { LinkedList < String > sites = new LinkedList < String >(); sites. add(\"Google\"); sites. add(\"Runoob\"); sites. add(\"Taobao\"); sites. add(\"Weibo\"); System. out. println( sites. getFirst()); } } 以上实例,执行输出结果为:", 180 | "一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。 一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。 Java LinkedList(链表) 类似于 ArrayList,是一种常用的数据容器。 与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。 以下情况使用 ArrayList : 频繁访问列表中的某一个元素。 只需要在列表末尾进行添加和删除元素操作。 以下情况使用 LinkedList : 你需要通过循环迭代来访问列表中的某些元素。 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。 LinkedList 继承了 AbstractSequentialList 类。" 181 | ], 182 | "data": null, 183 | "context": null, 184 | "url": "https://www.runoob.com/java/java-linkedlist.html", 185 | "lastUpdatedDate": null 186 | }, 187 | { 188 | "index": null, 189 | "title": "C++程序 寻找链表长度|极客笔记", 190 | "snippets": [ 191 | "C++程序 寻找链表长度|极客笔记 当前位置: 极客笔记 > C++ > C++ 示例 > C++程序 寻找链表长度 C++程序 寻找链表长度 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个 C ++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表定义示例,包含了节点的数据类型和链表的数据类型: struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } }; 计算链表长度 要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示: int getLengthRecursion(ListNode *node) { if (node == NULL) { return 0; } else { return 1 + getLengthRecursion(node->next); } } 函数中如果节点为空,则长度为0;否则不断递归到下一个节点,一直到最后一个节点,返回的是递归到该节点的长度+1。 使用示例 我们可以编写一个主函数,来验证计算链表长度的函数是否正确。 下面是一个使用示例,包含了链表的添加和输出功能,以及计算链表长度的功能: #include using namespace std; struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class LinkedList { public: ListNode *head; LinkedList() { head = NULL; } void addNode(int val) { if (head == NULL) { head = new ListNode(val); } else { ListNode *p = head; while (p->next != NULL) p = p->next; p->next = new ListNode(val); } } void printList() { ListNode *p = head; while (p != NULL) { cout << p->val << \" \"; p = p->next; } cout << endl; } }; int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } int main() { LinkedList list; list.addNode(1); list.addNode(2); list.addNode(3); list.addNode(4); list.addNode(5); list.printList(); cout << \"Length of the linked list: \" << getLength(&list) << endl; return 0; } 输出: 1 2 3 4 5 Length of the linked list: 5 结论 计算链表长度是一个简单而常用的操作,可以使用循环和递归两种方法来实现。 两种方法虽然代码不同,但本质相同,都是遍历整个链表,统计节点数量。 在实际应用中,要根据具体情况选择不同的方法。 上一篇 C++程序 查找两个链表交点 下一篇 C++程序 寻找给定和的子数组-第1集(非负数) Camera课程 API2 教程 Perf 教程 Native FW 教程 ISP 教程 Sensor 教程 Python教程 Python 教程 NumPy 教程 Django 教程 Java教程 Java 教程 Web教程 JavaScript 教程 jQuery 教程 CSS 教程 PHP 教程 Laravel 教程 TypeScript 教程 WordPress 教程 数据库教程 SQL 教程 SQLite 教程 MySQL 教程 PL/SQL 教程 MongoDB 教程 图形图像教程 OpenCV 教程 办公软件教程 Excel 教程 Linux教程 Linux命令全集 Linux内核API LVGL 教程 计算机教程 操作系统 教程 计算机网络 教程 C语言 教程 C++ 教程 Swift 教程 大数据教程 Hadoop 教程 Spark 教程 Scala 教程 开发工具教程 Git 教程 Jenkins 教程 ChatGPT 教程 IntelliJ 教程 C++程序 寻找链表长度 链表是一种常用的数据结构,它不像数组一样需要预先分配一定大小的内存,可以随时添加和删除数据。 但是,链表的一个缺点是不方便查找长度。 因此,本文将介绍如何编写一个C++程序来计算链表的长度。 链表的定义 链表是一组节点组成的集合,每个节点都包含两个部分,一个是数据部分,另一个是指向下一个节点的指针。 链表的头节点是第一个节点,也就是整个链表的起点。 下面是一个简单的链表", 192 | "下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。", 193 | "要计算链表的长度,我们只需要从头节点开始顺序遍历整个链表,直到遇到空节点为止。 为了方便起见,我们可以编写一个函数来计算链表长度。 下面是使用循环的方法来实现计算链表长度的函数: int getLength(LinkedList *list) { int len = 0; ListNode *p = list->head; while (p != NULL) { ++len; p = p->next; } return len; } 函数中使用一个计数器len来记录链表的长度,指针p用来指向当前节点。 循环中不断将p指向下一个节点,直到p指向空节点为止,此时链表长度为len。 我们也可以使用递归的方法来计算链表长度,如下所示:" 194 | ], 195 | "data": null, 196 | "context": null, 197 | "url": "https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html", 198 | "lastUpdatedDate": null 199 | } 200 | ] 201 | } 202 | }, 203 | { 204 | "text": "Generating answers for you...", 205 | "hiddenText": "Generating answers for you...", 206 | "author": "bot", 207 | "createdAt": "2023-11-18T04:05:23.8468865+00:00", 208 | "timestamp": "2023-11-18T04:05:23.8468865+00:00", 209 | "messageId": "cc216f3b-e380-426b-8a4f-4d0b94530b9f", 210 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 211 | "messageType": "InternalLoaderMessage", 212 | "offense": "None", 213 | "adaptiveCards": [ 214 | { 215 | "type": "AdaptiveCard", 216 | "version": "1.0", 217 | "body": [ 218 | { 219 | "type": "RichTextBlock", 220 | "inlines": [ 221 | { 222 | "type": "TextRun", 223 | "isSubtle": true, 224 | "italic": true, 225 | "text": "Generating answers for you..." 226 | } 227 | ] 228 | } 229 | ] 230 | } 231 | ], 232 | "feedback": { 233 | "tag": null, 234 | "updatedOn": null, 235 | "type": "None" 236 | }, 237 | "contentOrigin": "DeepLeo" 238 | }, 239 | { 240 | "text": "判断链表的长度是一个常见的数据结构和算法问题。有多种方法可以解决这个问题,我为你找到了一些相关的资料,你可以参考一下:\n\n- 一种方法是使用**循环**,即定义一个计数器和一个指针,从头节点开始遍历链表,每遍历一个节点,计数器加一,直到指针为空为止,此时计数器的值就是链表的长度[^3^] [^2^]。这种方法的时间复杂度是O (n),空间复杂度是O (1)。\n- 另一种方法是使用**递归**,即定义一个递归函数,如果节点为空,返回0,否则返回1加上对下一个节点的递归调用,最终返回的值就是链表的长度[^3^]。这种方法的时间复杂度也是O (n),但空间复杂度是O (n),因为需要栈空间来存储递归调用。\n\n以上是我能找到的一些判断链表长度的方法,你可以根据自己的需要选择合适的方法。如果你还有其他问题,欢迎继续和我聊天。😊", 241 | "author": "bot", 242 | "createdAt": "2023-11-18T04:05:23.8469882+00:00", 243 | "timestamp": "2023-11-18T04:05:23.8469882+00:00", 244 | "messageId": "53ff8e54-7180-4196-bccc-7e4d44a84ec9", 245 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 246 | "offense": "None", 247 | "adaptiveCards": [ 248 | { 249 | "type": "AdaptiveCard", 250 | "version": "1.0", 251 | "body": [ 252 | { 253 | "type": "TextBlock", 254 | "text": "[1]: https://bing.com/search?q=%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E9%93%BE%E8%A1%A8%E7%9A%84%E9%95%BF%E5%BA%A6 \"\"\n[2]: https://www.runoob.com/java/java-linkedlist.html \"\"\n[3]: https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html \"\"\n\n判断链表的长度是一个常见的数据结构和算法问题。有多种方法可以解决这个问题,我为你找到了一些相关的资料,你可以参考一下:\n\n- 一种方法是使用**循环**,即定义一个计数器和一个指针,从头节点开始遍历链表,每遍历一个节点,计数器加一,直到指针为空为止,此时计数器的值就是链表的长度[^1^][3] [^2^][2]。这种方法的时间复杂度是O (n),空间复杂度是O (1)。\n- 另一种方法是使用**递归**,即定义一个递归函数,如果节点为空,返回0,否则返回1加上对下一个节点的递归调用,最终返回的值就是链表的长度[^1^][3]。这种方法的时间复杂度也是O (n),但空间复杂度是O (n),因为需要栈空间来存储递归调用。\n\n以上是我能找到的一些判断链表长度的方法,你可以根据自己的需要选择合适的方法。如果你还有其他问题,欢迎继续和我聊天。😊", 255 | "wrap": true 256 | }, 257 | { 258 | "type": "TextBlock", 259 | "size": "small", 260 | "text": "Learn more: [1. deepinout.com](https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html) [2. www.runoob.com](https://www.runoob.com/java/java-linkedlist.html) [3. bing.com](https://bing.com/search?q=%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E9%93%BE%E8%A1%A8%E7%9A%84%E9%95%BF%E5%BA%A6)", 261 | "wrap": true 262 | } 263 | ] 264 | } 265 | ], 266 | "sourceAttributions": [ 267 | { 268 | "providerDisplayName": "C++程序 寻找链表长度|极客笔记", 269 | "seeMoreUrl": "https://deepinout.com/cpp/cpp-examples/g_cpp-program-for-finding-length-of-a-linked-list-iterative-and-recursive-approach.html", 270 | "urlSignature": "qAlH6lfdP4W8Swbx0h0nZi63iGlXt8HShFnPhoyUmlw=", 271 | "searchQuery": "如何判断链表的长度", 272 | "provider": "search_web" 273 | }, 274 | { 275 | "providerDisplayName": "Java LinkedList | 菜鸟教程", 276 | "seeMoreUrl": "https://www.runoob.com/java/java-linkedlist.html", 277 | "urlSignature": "jVNX0SPyMA5Rcb+I2Sw/KPkpY8UhkqB9Tt3mycTtw+U=", 278 | "imageLink": "https://bing.com/th?id=OIP.LCbOxeg1Pk0boiBVDu7X0wHaFj&pid=cdx", 279 | "imageWidth": "681", 280 | "imageHeight": "511", 281 | "imageFavicon": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVOSURBVFhHrVdpTFxVFB63xPWHP/3XPxqNsbHRRFOjNrGNSxvmvRmWAqVsphtgw1bbAAOE2oq1DDNQQ9vI0qCy1JLSVsSyKdubgVboyjIzLMOOSyUaTax+nvPmzTBDZ5hBOMmXee8u5/vOuefde0cVqGUj+/6CHvEpo1mzuVASkw2S8GmhSail3yp6P2yQxAR9t3oTj+GxyrTV28neXQ8ZJfXrRFZiMAn9hSbxNyL812AS4Q7qv1soCXeo/xo964tM2pd5ruJm5cZR6LuDXiGyMiL+eSmhX0jiPM0r1ZvULyouA7ey1uiHKZpEwrRX5ysAZcRmkDRRAWcjvyPoCVJeQGn805vD/4kFykja8a7gRxQa78aRMzlN+HuJg1VDDohEZLe++aBC52mA6j4amOSM3GheWyhCFvSSuFOh9DRjj+Y155rru0QkngrB9pywFSEsOwyhOnrODpURRm0hhP2lIaBidoiQBKvBrN2g0DrMUXRitTPyvAYtNoZHYP17kVi/NTC8QGPVsduRodPipbgEPLNfhyAhGslBUXgrOgJHmzRumRBKPZaisFvcwumRBfSIOFSrxQYhnJwStgaG598Nx95ULc6f2YZX0w5g3ccnsCckFhViJDZHhONwo9YlgDI9x/uETF5TE/IAFUe5QxlnQEDehQjsTD+EqJRMRKUGjpj0TOw6lIGoNB12fJiDaGp7PzUL8VkH8UlLGAyLtcBFqZcFFHZrnqb0WxY71Kj8MQXW0QGMjdkI1oAxyhhV3umX39nHbdsVnOqNY1KXAFqGft62VUZJCCcBf7kL+LIvHROTVsxM2TE9Nb4sZqbtMrz1MbjPar+G073xngIk8Vc+O1S0U2W5GpcI8OXQSThDsNksGBwe8CmEg/AmgJ7vkogEFRVEZSAC2PnszAT6rvdjcEghpPb8ks9RUlmF24O3cP3mDczRGHchvgTIXJKYp6KHGo/GJQKYhIkHiJSJIlMy0NbVhblZbrtN75lo+qEd3zS3YAcVXUVtHYYtQ/IcfwL4KF9WAE+2jVhQfrYOEckZiD+Yi7qGRoyPj8gCzjdeRoLuKEbkwrOhqv4ioukLiEnPRnX9JbmNs+FTAHGrWIV7o7sAjuLi5WZsitwNcW8qOs1mzM9Nyk6nKDqd/gROf1XjSvv87CSa29vxTlwS3o5JREtHB/VN+l4Ck3CMakDM82z0XAK7fQTftX2PfbojCE48QIS1cgau9vfJy2G+ekUWarUNo6j8C2j2pSHlo+No6+zEhH3U9xJIwj/EnbzsZ+gsJCYYoaWoOn8Re7OOoMMkobz2HD7IzZdJOAOXmppJ5FGca/hWTr2/GqDiv8PXO5W+W3iOOkaXEyA7mh6XiTjSIcsgkoj8DNUGt3G/xToki3S+u+b5LEJlI3LcAcQ6fwJcDrmoSERp9deuz9HRzmRexvvKAN0x+Qrg2I5NokjL8MeigDSfApxwEvuDNwHE9ZPepHlDJmc71rflMWqsV5Sh7OoejE7cwuz05BpgCpbxPjoLYt0FlN1zM+IjmTLxCw8oNofi0k09Ooer0TFUtSqwj/ob+XTKBjtTb+fbtkK7aKyIBOTSIPk+6FC7NnBFTtc9oyTuVijvteLWkMdpYBF/ozxhjUGBCQVc9Aqdd/usfeuTpLTYMcGroxWDI2dyvvIrNMubkokcZ02sBnzRJST6jXyp8T8Z+a4oiRcIv3tz7gcLVO3VBrN646r+rJ7s3fYo7xPksI5qY4ycurbtpaA+2kuEIUIFb7N+/wmtxDiFxl7Ns8rZkcenKK1trQy61BhNQia1qematS7wf8Uq1X83tUJh8W4nfQAAAABJRU5ErkJggg==", 282 | "searchQuery": "如何判断链表的长度", 283 | "mediaUniqueId": "", 284 | "sourceType": "CITATION", 285 | "provider": "search_web" 286 | }, 287 | { 288 | "providerDisplayName": "C++程序 寻找链表长度|极客笔记", 289 | "seeMoreUrl": "https://bing.com/search?q=%e5%a6%82%e4%bd%95%e5%88%a4%e6%96%ad%e9%93%be%e8%a1%a8%e7%9a%84%e9%95%bf%e5%ba%a6", 290 | "urlSignature": "hPQs1b/TLgfHUJpWQuxrsRmARGhlY8Suunk7Rx3xMyY=", 291 | "searchQuery": "如何判断链表的长度", 292 | "provider": "search_web" 293 | } 294 | ], 295 | "feedback": { 296 | "tag": null, 297 | "updatedOn": null, 298 | "type": "None" 299 | }, 300 | "contentOrigin": "DeepLeo", 301 | "scores": [ 302 | { 303 | "component": "BotOffense", 304 | "score": 0.0006825019 305 | }, 306 | { 307 | "component": "BotOffense", 308 | "score": 0.0006851713 309 | }, 310 | { 311 | "component": "BotOffense", 312 | "score": 0.00056588 313 | } 314 | ], 315 | "suggestedResponses": [ 316 | { 317 | "text": "如何删除链表中的节点?", 318 | "author": "user", 319 | "createdAt": "2023-11-18T04:05:45.9910225+00:00", 320 | "timestamp": "2023-11-18T04:05:45.9910225+00:00", 321 | "messageId": "bb517bed-7b2a-4fa1-bbf0-c640c0d12ed8", 322 | "messageType": "Suggestion", 323 | "offense": "Unknown", 324 | "feedback": { 325 | "tag": null, 326 | "updatedOn": null, 327 | "type": "None" 328 | }, 329 | "contentOrigin": "SuggestionChipsFalconService" 330 | }, 331 | { 332 | "text": "什么是递归函数?", 333 | "author": "user", 334 | "createdAt": "2023-11-18T04:05:45.9910235+00:00", 335 | "timestamp": "2023-11-18T04:05:45.9910235+00:00", 336 | "messageId": "42bed100-ab08-49ab-8b38-55206320376d", 337 | "messageType": "Suggestion", 338 | "offense": "Unknown", 339 | "feedback": { 340 | "tag": null, 341 | "updatedOn": null, 342 | "type": "None" 343 | }, 344 | "contentOrigin": "SuggestionChipsFalconService" 345 | }, 346 | { 347 | "text": "有没有其他数据结构和算法问题可以学习?", 348 | "author": "user", 349 | "createdAt": "2023-11-18T04:05:45.9910242+00:00", 350 | "timestamp": "2023-11-18T04:05:45.9910242+00:00", 351 | "messageId": "c2a4f399-7503-442d-b8a6-1727f4fa4d03", 352 | "messageType": "Suggestion", 353 | "offense": "Unknown", 354 | "feedback": { 355 | "tag": null, 356 | "updatedOn": null, 357 | "type": "None" 358 | }, 359 | "contentOrigin": "SuggestionChipsFalconService" 360 | } 361 | ] 362 | } 363 | ], 364 | "firstNewMessageIndex": 1, 365 | "defaultChatName": null, 366 | "conversationId": "51D|BingProd|25ADAFC76374850A62CF2C285A7B253E0BF90C4E722A8A813075F258A649EF3F", 367 | "requestId": "77b13b73-8138-11f6-ff28-b0375509df59", 368 | "conversationExpiryTime": "2024-02-16T04:05:46.0295914Z", 369 | "telemetry": { 370 | "startTime": "2023-11-18T04:05:13.1173554Z" 371 | }, 372 | "throttling": { 373 | "maxNumUserMessagesInConversation": 30, 374 | "numUserMessagesInConversation": 3, 375 | "maxNumLongDocSummaryUserMessagesInConversation": 50, 376 | "numLongDocSummaryUserMessagesInConversation": 0 377 | }, 378 | "result": { 379 | "value": "Success", 380 | "message": "判断链表的长度是一个常见的数据结构和算法问题。有多种方法可以解决这个问题,我为你找到了一些相关的资料,你可以参考一下:\n\n- 一种方法是使用**循环**,即定义一个计数器和一个指针,从头节点开始遍历链表,每遍历一个节点,计数器加一,直到指针为空为止,此时计数器的值就是链表的长度[^3^] [^2^]。这种方法的时间复杂度是O (n),空间复杂度是O (1)。\n- 另一种方法是使用**递归**,即定义一个递归函数,如果节点为空,返回0,否则返回1加上对下一个节点的递归调用,最终返回的值就是链表的长度[^3^]。这种方法的时间复杂度也是O (n),但空间复杂度是O (n),因为需要栈空间来存储递归调用。\n\n以上是我能找到的一些判断链表长度的方法,你可以根据自己的需要选择合适的方法。如果你还有其他问题,欢迎继续和我聊天。😊", 381 | "serviceVersion": "20231117.80" 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /aichat-bingai.el: -------------------------------------------------------------------------------- 1 | ;;; aichat-bingai.el --- aichat-bingai.el -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: aichat-bingai.el 4 | ;; Description: aichat-bingai.el 5 | ;; Author: xhcoding 6 | ;; Maintainer: xhcoding 7 | ;; Copyright (C) 2023, xhcoding, all rights reserved. 8 | ;; Created: 2023-03-11 15:12:02 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2023-03-11 15:12:02 11 | ;; By: xhcoding 12 | ;; URL: https://github.com/xhcoding/emacs-aichat 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 30.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; aichat-bingai.el 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put aichat-bingai.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'aichat-bingai) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET aichat-bingai RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2023/03/11 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | (require 'gnutls) 84 | (require 'websocket) 85 | 86 | (require 'aichat-util) 87 | 88 | ;;; Code: 89 | 90 | (defgroup aichat-bingai nil 91 | "Bing AI in Emacs." 92 | :group 'aichat 93 | :prefix "aichat-bingai-") 94 | 95 | (defcustom aichat-bingai-cookies-file nil 96 | "The path of www.bing.com cookies file. 97 | 98 | When you set this value, bingai will login to www.bing.com through the cookies in the file." 99 | :group 'aichat-bingai 100 | :type 'string) 101 | 102 | (defcustom aichat-bingai-conversation-style 'balanced 103 | "Conversation style." 104 | :group 'aichat-bingai 105 | :type '(radio 106 | (const :tag "More Creative" creative) 107 | (const :tag "More Balanced" balanced) 108 | (const :tag "More Precise" precise))) 109 | 110 | (defcustom aichat-bingai-proxy nil 111 | "Http proxy of request bingai. 112 | 113 | (setq aichat-bingai-proxy \"localhost:51837\")" 114 | :group 'aichat-bingai 115 | :type 'string) 116 | 117 | 118 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; websocket ;;;;;;;;;;;;;;;;;;;;;;;;;; 119 | 120 | (defun aichat-bingai--create-proxy-connection(name proxy-host proxy-port server-host server-port use-ssl) 121 | (let ((conn) 122 | (finished)) 123 | (setq conn (make-network-process 124 | :name name 125 | :buffer nil 126 | :host proxy-host 127 | :service proxy-port 128 | :filter (lambda (proc data) 129 | (setq finished t)))) 130 | (process-send-string 131 | conn 132 | (format "CONNECT %s:%s HTTP/1.1\r\nHost: %s:%s\r\nProxy-Connection: Keep-Alive\r\n\r\n" 133 | server-host server-port server-host server-port)) 134 | (while (not finished) 135 | (accept-process-output conn 0.2)) 136 | (when use-ssl 137 | (setq conn (gnutls-negotiate :process conn :hostname server-host))) 138 | conn)) 139 | 140 | (cl-defun aichat-bingai--websocket-open (url &key protocols extensions (on-open 'identity) 141 | (on-message (lambda (_w _f))) (on-close 'identity) 142 | (on-error 'websocket-default-error-handler) 143 | (nowait nil) (custom-header-alist nil) 144 | (proxy nil)) 145 | "Open a websocket connection to URL, returning the `websocket' struct. 146 | The PROTOCOL argument is optional, and setting it will declare to 147 | the server that this client supports the protocols in the list 148 | given. We will require that the server also has to support that 149 | protocols. 150 | 151 | Similar logic applies to EXTENSIONS, which is a list of conses, 152 | the car of which is a string naming the extension, and the cdr of 153 | which is the list of parameter strings to use for that extension. 154 | The parameter strings are of the form \"key=value\" or \"value\". 155 | EXTENSIONS can be NIL if none are in use. An example value would 156 | be (\"deflate-stream\" . (\"mux\" \"max-channels=4\")). 157 | 158 | Cookies that are set via `url-cookie-store' will be used during 159 | communication with the server, and cookies received from the 160 | server will be stored in the same cookie storage that the 161 | `url-cookie' package uses. 162 | 163 | Optionally you can specify 164 | ON-OPEN, ON-MESSAGE and ON-CLOSE callbacks as well. 165 | 166 | The ON-OPEN callback is called after the connection is 167 | established with the websocket as the only argument. The return 168 | value is unused. 169 | 170 | The ON-MESSAGE callback is called after receiving a frame, and is 171 | called with the websocket as the first argument and 172 | `websocket-frame' struct as the second. The return value is 173 | unused. 174 | 175 | The ON-CLOSE callback is called after the connection is closed, or 176 | failed to open. It is called with the websocket as the only 177 | argument, and the return value is unused. 178 | 179 | The ON-ERROR callback is called when any of the other callbacks 180 | have an error. It takes the websocket as the first argument, and 181 | a symbol as the second argument either `on-open', `on-message', 182 | or `on-close', and the error as the third argument. Do NOT 183 | rethrow the error, or else you may miss some websocket messages. 184 | You similarly must not generate any other errors in this method. 185 | If you want to debug errors, set 186 | `websocket-callback-debug-on-error' to t, but this also can be 187 | dangerous is the debugger is quit out of. If not specified, 188 | `websocket-default-error-handler' is used. 189 | 190 | For each of these event handlers, the client code can store 191 | arbitrary data in the `client-data' slot in the returned 192 | websocket. 193 | 194 | The following errors might be thrown in this method or in 195 | websocket processing, all of them having the error-condition 196 | `websocket-error' in addition to their own symbol: 197 | 198 | `websocket-unsupported-protocol': Data in the error signal is the 199 | protocol that is unsupported. For example, giving a URL starting 200 | with http by mistake raises this error. 201 | 202 | `websocket-wss-needs-emacs-24': Trying to connect wss protocol 203 | using Emacs < 24 raises this error. You can catch this error 204 | also by `websocket-unsupported-protocol'. 205 | 206 | `websocket-received-error-http-response': Data in the error 207 | signal is the integer error number. 208 | 209 | `websocket-invalid-header': Data in the error is a string 210 | describing the invalid header received from the server. 211 | 212 | `websocket-unparseable-frame': Data in the error is a string 213 | describing the problem with the frame. 214 | 215 | `nowait': If NOWAIT is true, return without waiting for the 216 | connection to complete. 217 | 218 | `custom-headers-alist': An alist of custom headers to pass to the 219 | server. The car is the header name, the cdr is the header value. 220 | These are different from the extensions because it is not related 221 | to the websocket protocol. 222 | " 223 | (let* ((name (format "websocket to %s" url)) 224 | (url-struct (url-generic-parse-url url)) 225 | (key (websocket-genkey)) 226 | (coding-system-for-read 'binary) 227 | (coding-system-for-write 'binary) 228 | (conn (if (member (url-type url-struct) '("ws" "wss")) 229 | (let* ((type (if (equal (url-type url-struct) "ws") 230 | 'plain 'tls)) 231 | (port (if (= 0 (url-port url-struct)) 232 | (if (eq type 'tls) 443 80) 233 | (url-port url-struct))) 234 | (host (url-host url-struct))) 235 | (if proxy 236 | (let* ((proxy-segs (split-string proxy ":")) 237 | (proxy-host (nth 0 proxy-segs)) 238 | (proxy-port (nth 1 proxy-segs)) 239 | (use-ssl (eq type 'tls))) 240 | (aichat-bingai--create-proxy-connection 241 | name proxy-host proxy-port host port use-ssl)) 242 | (if (eq type 'plain) 243 | (make-network-process :name name :buffer nil :host host 244 | :service port :nowait nowait) 245 | (condition-case-unless-debug nil 246 | (open-network-stream name nil host port :type type :nowait nowait) 247 | (wrong-number-of-arguments 248 | (signal 'websocket-wss-needs-emacs-24 (list "wss"))))))) 249 | (signal 'websocket-unsupported-protocol (list (url-type url-struct))))) 250 | (websocket (websocket-inner-create 251 | :conn conn 252 | :url url 253 | :on-open on-open 254 | :on-message on-message 255 | :on-close on-close 256 | :on-error on-error 257 | :protocols protocols 258 | :extensions (mapcar 'car extensions) 259 | :accept-string 260 | (websocket-calculate-accept key)))) 261 | (unless conn (error "Could not establish the websocket connection to %s" url)) 262 | (process-put conn :websocket websocket) 263 | (set-process-filter conn 264 | (lambda (process output) 265 | (let ((websocket (process-get process :websocket))) 266 | (websocket-outer-filter websocket output)))) 267 | (set-process-sentinel 268 | conn 269 | (websocket-sentinel url conn key protocols extensions custom-header-alist nowait)) 270 | (set-process-query-on-exit-flag conn nil) 271 | (websocket-ensure-handshake url conn key protocols extensions custom-header-alist nowait) 272 | websocket)) 273 | 274 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Internal ;;;;;;;;;;;;;;;;;;;;;;;;;; 275 | (defconst aichat-bingai--domain ".bing.com" 276 | "Bing domain for retrieve cookies.") 277 | 278 | (defun aichat-bingai--login-p () 279 | "Check if you're already login." 280 | (when-let* ((host-cookies 281 | (seq-find (lambda (host) 282 | (string= (car host) aichat-bingai--domain)) 283 | (append url-cookie-secure-storage url-cookie-storage))) 284 | (user (seq-find 285 | (lambda (cookie) 286 | (string= (aref cookie 1) "_U")) 287 | (cdr host-cookies)))) 288 | (and user 289 | (not (url-cookie-expired-p user))))) 290 | 291 | (async-defun aichat-bingai--login () 292 | "Login `aichat-bingai--domain'." 293 | (await t) 294 | (unless (aichat-bingai--login-p) 295 | (await (aichat-refresh-cookies aichat-bingai--domain aichat-bingai-cookies-file)) 296 | (unless (aichat-bingai--login-p) 297 | (error "Can not get correct cookie for login!")))) 298 | 299 | (defconst aichat-bingai--create-conversation-url "https://www.bing.com/turing/conversation/create" 300 | "The url of create conversation.") 301 | 302 | (defconst aichat-bingai--conversation-headers 303 | `(("accept" . "application/json") 304 | ("accept-language" . "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") 305 | ("content-type" . "application/json") 306 | ("referer" . "https://www.bing.com/search?q=Bing+AI&showconv=1") 307 | ("sec-ch-ua" . "\"Microsoft Edge\"=\"111\", \"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"111\"") 308 | ("sec-ch-ua-mobile" . "?0") 309 | ("sec-ch-ua-platform" . "\"Windows\"") 310 | ("sec-fetch-dest" . "empty") 311 | ("sec-fetch-mode" . "cors") 312 | ("sec-fetch-site" . "same-origin") 313 | ("x-ms-client-request-id" . ,(aichat-uuid)) 314 | ("x-ms-useragent" . "azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.12.0 OS/Windows")) 315 | "The headers of create conversation.") 316 | 317 | (defconst aichat-bingai--chathub-headers 318 | `(("User-Agent" . ,aichat-user-agent) 319 | ("Accept-Language" . "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")) 320 | "The headers of create chathub.") 321 | 322 | (cl-defstruct (aichat-bingai--conversation 323 | (:constructor aichat-bingai--conversation-new) 324 | (:copier nil)) 325 | "A conversation structure. 326 | `id', `signature' and `client-id' are obtained through the GET request `aichat-bingai--conversation-url' response." 327 | id 328 | signature 329 | client-id) 330 | 331 | (async-defun aichat-bingai--create-conversation () 332 | "Create a conversation through the GET request `aichat-bingai--conversation-url'." 333 | (await (aichat-bingai--login)) 334 | (seq-let 335 | (status headers body) 336 | (await (aichat-http aichat-bingai--create-conversation-url 337 | :proxy aichat-bingai-proxy 338 | :headers aichat-bingai--conversation-headers)) 339 | (aichat-debug "status:\n%s\nheaders:\n%s\nbody:\n%s\n" status headers body) 340 | (if (not (string= "200" (car status))) 341 | (error "Create conversation failed: %s" status) 342 | (if (string-empty-p body) 343 | (error "Your network settings are preventing access to this feature.") 344 | (let* ((data (aichat-json-parse body)) 345 | (result-value (aichat-json-access data "{result}{value}")) 346 | (signature (cdr (seq-find (lambda (header) (string= (downcase (car header)) "x-sydney-encryptedconversationsignature")) 347 | headers)))) 348 | (if (not (string= "Success" result-value)) 349 | (error "Create conversation failed: %s" body) 350 | (aichat-bingai--conversation-new 351 | :id (aichat-json-access data "{conversationId}") 352 | :client-id (aichat-json-access data "{clientId}") 353 | :signature signature))))))) 354 | 355 | (cl-defstruct (aichat-bingai--session 356 | (:constructor aichat-bingai--session-new) 357 | (:copier nil)) 358 | "A session structure. 359 | `conversation' represents the `aichat-bingai--conversation'. 360 | `chathub' represents the chathub websocket connection. 361 | `invocation-id' indicates the number of questions. 362 | `max-conversation' indicates the number of max conversation. 363 | when `reset' is `t', session will be reset. 364 | `replying' indicates whether the reply is in progress. 365 | `buffer' saves the reply message for parsing. 366 | `resolve' and `reject' are promise callback, call `resolve' when the reply ends 367 | and call `reject' when error occurs. 368 | `result' saves the result of conversation. 369 | `err' saves the error of conversation. 370 | Call `user-cb' when a message arrives." 371 | conversation 372 | chathub 373 | (invocation-id 0) 374 | (max-conversation 10) 375 | (reset nil) 376 | (replying nil) 377 | (buffer "") 378 | resolve 379 | reject 380 | (result nil) 381 | (err nil) 382 | (expiry-time nil) 383 | user-cb) 384 | 385 | (defconst aichat-bingai--chathub-message-delimiter (char-to-string #x1e) 386 | "Websocket json message delimiter.") 387 | 388 | (defun aichat-bingai--chathub-send-heart (ws) 389 | "Send type 6 message." 390 | (websocket-send-text ws (concat (aichat-json-serialize (list :type 6)) 391 | aichat-bingai--chathub-message-delimiter))) 392 | 393 | (defun aichat-bingai--chathub-reply-finished (session &optional error reset) 394 | "Handler of reply finished." 395 | (setf (aichat-bingai--session-replying session) nil) 396 | (setf (aichat-bingai--session-err session) error) 397 | (setf (aichat-bingai--session-reset session) reset) 398 | (when reset 399 | (websocket-close (aichat-bingai--session-chathub session))) 400 | (let ((err (aichat-bingai--session-err session))) 401 | (if err 402 | (when-let ((reject-func (aichat-bingai--session-reject session))) 403 | (funcall reject-func err)) 404 | (when-let ((resolve-func (aichat-bingai--session-resolve session))) 405 | (funcall resolve-func (aichat-bingai--session-result session)))))) 406 | 407 | (defun aichat-bingai--chathub-parse-message (session text) 408 | "Parse chathub websocket json message." 409 | (aichat-debug "Recv text:\n%s" text) 410 | (let ((buffer (concat (aichat-bingai--session-buffer session) text)) 411 | (start-pos 0) 412 | (match-pos nil) 413 | (object)) 414 | (aichat-debug "buffer:\n%s" buffer) 415 | (catch 'not-find 416 | (while t 417 | (setq match-pos (string-match-p aichat-bingai--chathub-message-delimiter buffer start-pos)) 418 | (if (not match-pos) 419 | (throw 'not-find match-pos) 420 | (setq object (aichat-json-parse (substring buffer start-pos match-pos))) 421 | (aichat-debug "object:\n%s" object) 422 | (pcase (aichat-json-access object "{type}") 423 | (1 (let* ((user-cb (aichat-bingai--session-user-cb session)) 424 | (messages (aichat-json-access object "{arguments}[0]{messages}")) 425 | (throttling (aichat-json-access object "{arguments}[0]{throttling}")) 426 | (message-type (if messages (aichat-json-access messages "[0]{messageType}") nil))) 427 | (cond 428 | (throttling 429 | (setf (aichat-bingai--session-max-conversation session) 430 | (aichat-json-access throttling "{maxNumUserMessagesInConversation}"))) 431 | ((string= message-type "Disengaged") 432 | (aichat-bingai--chathub-reply-finished session "Conversation disengaged, next conversation will be restarted." t)) 433 | (t 434 | (when user-cb 435 | (condition-case error 436 | (funcall user-cb object) 437 | (error 438 | (aichat-bingai--chathub-reply-finished session (format "User callback error: %s" error))))))))) 439 | (2 (let* ((result (aichat-json-access object "{item}{result}")) 440 | (value (aichat-json-access result "{value}")) 441 | (message (aichat-json-access result "{message}"))) 442 | (if (string= "Success" value) 443 | (progn 444 | (when-let* ((expiry-time-str 445 | (ignore-errors 446 | (aichat-json-access object "{item}{conversationExpiryTime}"))) 447 | (expiry-time (date-to-time expiry-time-str))) 448 | (setf (aichat-bingai--session-expiry-time session) expiry-time)) 449 | (setf (aichat-bingai--session-result session) object)) 450 | (aichat-bingai--chathub-reply-finished session (format "%s:%s\n" value message) 451 | (string= "InvalidSession" value))))) 452 | (3 (let ((err (aichat-json-access object "{error}"))) 453 | (aichat-bingai--chathub-reply-finished session err))) 454 | (6 (when-let ((chathub (aichat-bingai--session-chathub session)) 455 | (replying (aichat-bingai--session-replying session))) 456 | (aichat-bingai--chathub-send-heart chathub)))) 457 | (setq start-pos (1+ match-pos))))) 458 | (setf (aichat-bingai--session-buffer session) (substring buffer start-pos)))) 459 | 460 | (defconst aichat-bingai--chathub-url "wss://sydney.bing.com/sydney/ChatHub" 461 | "The url of create chathub.") 462 | 463 | (defun aichat-bingai--create-chathub (session) 464 | "Create a websocket connection to `aichat-bingai--chathub-url'. 465 | 466 | Call resolve when the handshake with chathub passed." 467 | (promise-new 468 | (lambda (resolve reject) 469 | (aichat-bingai--websocket-open 470 | (concat aichat-bingai--chathub-url "?sec_access_token=" 471 | (url-hexify-string (aichat-bingai--conversation-signature (aichat-bingai--session-conversation session)))) 472 | :proxy aichat-bingai-proxy 473 | :custom-header-alist aichat-bingai--chathub-headers 474 | :on-open (lambda (ws) 475 | (aichat-debug "====== chathub opened ======") 476 | ;; send handshake 477 | (if (and ws (websocket-openp ws)) 478 | (websocket-send-text ws (concat (aichat-json-serialize 479 | (list :protocol "json" :version 1)) 480 | aichat-bingai--chathub-message-delimiter)) 481 | (funcall reject "Chathub unexpected closed during handshake."))) 482 | :on-close (lambda (_ws) 483 | (aichat-debug "====== chathub closed ======") 484 | (if (not (aichat-bingai--session-chathub session)) 485 | (funcall reject "Chathub unexpected closed during handshake.") 486 | (setf (aichat-bingai--session-chathub session) nil) 487 | (when (aichat-bingai--session-replying session) 488 | ;; close when replying 489 | (setf (aichat-bingai--session-replying session) nil) 490 | (when-let ((reject-func (aichat-bingai--session-reject session))) 491 | (funcall reject-func "Chathub closed unexpectedly during reply."))))) 492 | :on-message (lambda (ws frame) 493 | (let ((text (websocket-frame-text frame))) 494 | (condition-case error 495 | (progn 496 | (aichat-debug "Receive handshake response: %s" text) 497 | (aichat-json-parse (car (split-string text aichat-bingai--chathub-message-delimiter))) 498 | (aichat-bingai--chathub-send-heart ws) 499 | (setf (websocket-on-message ws) 500 | (lambda (_ws frame) 501 | (aichat-bingai--chathub-parse-message session (websocket-frame-text frame)))) 502 | (setf (aichat-bingai--session-chathub session) ws) 503 | (funcall resolve t)) 504 | (error (funcall reject error))))))))) 505 | 506 | (defun aichat-bingai--close-chathub (session) 507 | "Close chathub websocket connection." 508 | (when-let ((chathub (aichat-bingai--session-chathub session))) 509 | (when (websocket-openp chathub) 510 | (websocket-close chathub)))) 511 | 512 | (defun aichat-bingai--reply-options (style) 513 | (apply 514 | #'vector 515 | "nlu_direct_response_filter" 516 | "deepleo" 517 | "disable_emoji_spoken_text" 518 | "responsible_ai_policy_2235" 519 | "enablemm" 520 | "iycapbing" 521 | "iyxapbing" 522 | "refpromptv1" 523 | "enuaug" 524 | "dv3sugg" 525 | "autosave" 526 | "iyoloxap" 527 | "iyoloneutral" 528 | (pcase style 529 | ('creative (list "h3imaginative" "clgalileo" "gencontentv3")) 530 | ('balanced (list "galileo" "saharagenconv5")) 531 | ('precise (list "h3precise" "clgalileo" "gencontentv3"))))) 532 | 533 | (defun aichat-bingai--tone (style) 534 | (pcase style 535 | ('creative "Creative") 536 | ('balanced "Balanced") 537 | ('precise "Precise")) 538 | ) 539 | 540 | (defconst aichat-bingai--allowed-message-types 541 | [ 542 | "Chat" 543 | "InternalSearchQuery" 544 | "InternalSearchResult" 545 | "InternalLoaderMessage" 546 | "RenderCardRequest" 547 | "AdsQuery" 548 | "SemanticSerp" 549 | "GenerateContentQuery" 550 | "SearchQuery" 551 | ]) 552 | 553 | (defconst aichat-bingai--slice-ids 554 | [ 555 | "winmuid3tf" 556 | "anssupltmrd1" 557 | "imgchatgptv2" 558 | "bingsr3-v1" 559 | "revpayaad" 560 | "winstmsg2tf" 561 | "602refusal" 562 | "621alllocs0" 563 | "621docxfmtho" 564 | "330uaug" 565 | "0626snptrcs0" 566 | "424dagslnv1s0"]) 567 | 568 | (defun aichat-bingai--make-request (session text style allowed-message-types options-sets) 569 | (unless allowed-message-types 570 | (setq allowed-message-types aichat-bingai--allowed-message-types)) 571 | 572 | (unless options-sets 573 | (setq options-sets (aichat-bingai--reply-options style))) 574 | 575 | (let* ((conversation (aichat-bingai--session-conversation session)) 576 | (invocation-id (aichat-bingai--session-invocation-id session)) 577 | (request (list :arguments 578 | (vector 579 | (list :source "cib" 580 | :optionsSets options-sets 581 | :allowedMessageTypes (vconcat allowed-message-types ["Disengaged"]) 582 | :sliceIds aichat-bingai--slice-ids 583 | :tone (aichat-bingai--tone style) 584 | :spokenTextMode: "None" 585 | :isStartOfSession (if (= 0 invocation-id) 586 | t 587 | :json-false) 588 | :message (list :author "user" 589 | :inputMethod "Keyboard" 590 | :text text 591 | :messageType "Chat") 592 | :participant (list :id (aichat-bingai--conversation-client-id conversation)) 593 | :conversationId (aichat-bingai--conversation-id conversation))) 594 | :invocationId (number-to-string (aichat-bingai--session-invocation-id session)) 595 | :target "chat" 596 | :type 4))) 597 | (concat (aichat-json-serialize request) aichat-bingai--chathub-message-delimiter))) 598 | 599 | (defvar aichat-bingai--current-session nil ;; only one session 600 | "Bingai session.") 601 | 602 | (defun aichat-bingai--get-current-session () 603 | "Return current session." 604 | aichat-bingai--current-session) 605 | 606 | (defun aichat-bingai--set-current-session (session) 607 | "Set current session." 608 | (setq aichat-bingai--current-session session)) 609 | 610 | (defun aichat-bingai--remove-current-session () 611 | "Remove current session." 612 | (setq aichat-bingai--current-session nil)) 613 | 614 | (defun aichat-bingai--stop-session () 615 | "Stop current bingai session." 616 | (when-let ((session (aichat-bingai--get-current-session))) 617 | (setf (aichat-bingai--session-conversation session) nil) 618 | (aichat-bingai--close-chathub session) 619 | (aichat-bingai--remove-current-session))) 620 | 621 | (async-defun aichat-bingai--start-session () 622 | "Start a new aichat-bingai session." 623 | (await t) 624 | (aichat-bingai--stop-session) 625 | (when-let ((conversation (await (aichat-bingai--create-conversation))) 626 | (session (aichat-bingai--session-new 627 | :conversation conversation))) 628 | (aichat-bingai--set-current-session session) 629 | t)) 630 | 631 | (defun aichat-bingai--ensure-conversation-valid () 632 | (when-let* ((session (aichat-bingai--get-current-session)) 633 | (invocation-id (aichat-bingai--session-invocation-id session)) 634 | (max-conversation (aichat-bingai--session-max-conversation session))) 635 | (when-let* ((expiry-time (aichat-bingai--session-expiry-time session)) 636 | (expiry-p (ignore-errors 637 | (time-less-p expiry-time nil)))) 638 | (aichat-bingai--stop-session)) 639 | (when (aichat-bingai--session-reset session) 640 | (aichat-bingai--stop-session)) 641 | (when (>= invocation-id max-conversation) 642 | (aichat-bingai--stop-session)))) 643 | 644 | (async-defun aichat-bingai--send-request (text style allowed-message-types options-sets &optional callback) 645 | (aichat-bingai--ensure-conversation-valid) 646 | (let ((session (aichat-bingai--get-current-session))) 647 | (unless session 648 | (await (aichat-bingai--start-session)) 649 | (setq session (aichat-bingai--get-current-session))) 650 | (unless (aichat-bingai--session-chathub session) 651 | (await (aichat-bingai--create-chathub session))) 652 | 653 | (await (promise-new 654 | (lambda (resolve reject) 655 | (let ((request (aichat-bingai--make-request session text style allowed-message-types options-sets))) 656 | (aichat-debug "Send request:\n%s\n" request) 657 | (websocket-send-text (aichat-bingai--session-chathub session) request) 658 | (setf (aichat-bingai--session-invocation-id session) (1+ (aichat-bingai--session-invocation-id session)) 659 | (aichat-bingai--session-replying session) t 660 | (aichat-bingai--session-buffer session) "" 661 | (aichat-bingai--session-resolve session) resolve 662 | (aichat-bingai--session-reject session) reject 663 | (aichat-bingai--session-result session) nil 664 | (aichat-bingai--session-err session) nil 665 | (aichat-bingai--session-user-cb session) callback))))))) 666 | 667 | 668 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Image API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 669 | 670 | (async-defun aichat-bingai--image-download (urls resolve reject) 671 | (await nil) 672 | (condition-case error 673 | (let ((paths)) 674 | (dolist (url urls) 675 | (when-let* ((name (concat (car (last (split-string (car (split-string url "?")) "/"))) ".jpeg")) 676 | (filepath (expand-file-name name (temporary-file-directory))) 677 | (output (await (aichat-shell-command (format "%s --silent \"%s\" --output %s" aichat-curl-program url filepath))))) 678 | (push (cons url filepath) paths))) 679 | (funcall resolve paths)) 680 | (error (funcall reject error)))) 681 | 682 | (defun aichat-bingai--image-polling (request-id prompt resolve reject) 683 | (let ((polling-url (format "https://www.bing.com/images/create/async/results/%s" request-id))) 684 | (promise-then (aichat-http polling-url 685 | :proxy aichat-bingai-proxy 686 | :params `(("q" . ,prompt))) 687 | (lambda (response) 688 | (if (not (string= "200" (caar response))) 689 | (progn 690 | (funcall reject (format "Polling image error: %s" response))) 691 | (let ((body (car (last response))) 692 | (urls (list))) 693 | (if (string-empty-p body) 694 | (run-at-time 5 nil #'aichat-bingai--image-polling request-id prompt resolve reject) 695 | (with-temp-buffer 696 | (insert body) 697 | (goto-char (point-min)) 698 | (while (re-search-forward (rx "src=\"" 699 | (group (1+ (not "\""))) 700 | "\"") 701 | nil t) 702 | (push (match-string 1) urls)) 703 | (aichat-bingai--image-download urls resolve reject)))))) 704 | (lambda (err) 705 | (message "Polling error but continue: %s" err) 706 | (run-at-time 5 nil #'aichat-bingai--image-polling request-id prompt resolve reject))))) 707 | 708 | (async-defun aichat-bingai--image-create (prompt) 709 | "Create images with PROMPT." 710 | (let ((iframeid (aichat-uuid)) 711 | (response) 712 | (location) 713 | (redirect-url) 714 | (request-id) 715 | (polling-url)) 716 | (setq response (await (aichat-http "https://www.bing.com/images/create" 717 | :proxy aichat-bingai-proxy 718 | :type "POST" 719 | :params `(("q" . ,prompt) 720 | ("rt" . "3") 721 | ("FORM" . "GENCRE")) 722 | :data ""))) 723 | (if (not (string= "302" (caar response))) 724 | (error (format "Image create: %s" response)) 725 | (setq location (aichat-read-header-value "Location" (cadr response))) 726 | (setq redirect-url (concat "https://www.bing.com" (replace-regexp-in-string "&nfy=1" "" location))) 727 | (setq request-id (cadr (split-string redirect-url "id="))) 728 | (setq response (await (aichat-http redirect-url 729 | :proxy aichat-bingai-proxy))) 730 | (await 731 | (promise-new 732 | (lambda (resolve reject) 733 | (aichat-bingai--image-polling request-id prompt resolve reject))))))) 734 | 735 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; bingai API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 736 | 737 | (defun aichat-bingai-conversationing-p () 738 | "Whether conversation or not." 739 | (when-let ((session (aichat-bingai--get-current-session))) 740 | (aichat-bingai--session-replying session))) 741 | 742 | (defun aichat-bingai-conversation-start-p () 743 | "Whether is start of conversation." 744 | (aichat-bingai--ensure-conversation-valid) 745 | (not (aichat-bingai--get-current-session))) 746 | 747 | (defun aichat-bingai-conversation-reset () 748 | "Reset conversation." 749 | (aichat-bingai--stop-session)) 750 | 751 | (cl-defun aichat-bingai-conversation (text &rest settings 752 | &key 753 | (style nil) 754 | (allowed-message-types nil) 755 | (options-sets nil) 756 | (on-success nil) 757 | (on-error nil)) 758 | "Send a chat TEXT to Bing. 759 | 760 | `style' is the conversation style, look `aichat-bingai-conversation-stye' for detail. 761 | `allowed-message-types' is the message type allowed to return, 762 | all types in `aichat-bingai--allowed-message-types'." 763 | (when (aichat-bingai-conversationing-p) 764 | (error "Please wait for the conversation finished before call.")) 765 | (unless style 766 | (setq style aichat-bingai-conversation-style)) 767 | (unless allowed-message-types 768 | (setq allowed-message-types (vector "Chat"))) 769 | 770 | (promise-then (aichat-bingai--send-request text style allowed-message-types options-sets) 771 | (lambda (result) 772 | (when on-success 773 | (funcall on-success result))) 774 | (lambda (err) 775 | (when on-error 776 | (funcall on-error err))))) 777 | 778 | 779 | (cl-defun aichat-bingai-conversation-stream (text callback &rest settings 780 | &key 781 | (style nil) 782 | (allowed-message-types nil) 783 | (options-sets nil) 784 | (on-success nil) 785 | (on-error nil)) 786 | "Send a chat TEXT to Bing. 787 | 788 | `style' is the conversation style, look `aichat-bingai-conversation-stye' for detail. 789 | `allowed-message-types' is the message type allowed to return, 790 | all types in `aichat-bingai--allowed-message-types'." 791 | (when (aichat-bingai-conversationing-p) 792 | (error "Please wait for the conversation finished before call.")) 793 | 794 | (unless style 795 | (setq style aichat-bingai-conversation-style)) 796 | 797 | (unless allowed-message-types 798 | (setq allowed-message-types (vector "Chat"))) 799 | 800 | (promise-then (aichat-bingai--send-request text style allowed-message-types options-sets 801 | (lambda (message) 802 | (when callback 803 | (funcall callback message)))) 804 | (lambda (result) 805 | (when on-success 806 | (funcall on-success result))) 807 | (lambda (err) 808 | (when on-error 809 | (funcall on-error err))))) 810 | 811 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Messages API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 812 | 813 | (defun aichat-bingai-message-type-1-text (message) 814 | "message[arguments][0][messages][0][text]." 815 | (aichat-json-access message "{arguments}[0]{messages}[0]{text}")) 816 | 817 | (defun aichat-bingai-message-type-1-message-type (message) 818 | "msg[arguments][0][messages][0][messageType]." 819 | (aichat-json-access message "{arguments}[0]{messages}[0]{messageType}")) 820 | 821 | (defun aichat-bingai-message-type-1-suggestion (message) 822 | "message[arguments][0][messages][0][suggestedResponses]." 823 | (when-let ((suggested-responses (aichat-json-access message "{arguments}[0]{messages}[0]{suggestedResponses}"))) 824 | (cl-loop for suggested-response across suggested-responses 825 | collect (aichat-json-access suggested-response "{text}")))) 826 | 827 | (defun aichat-bingai-message-type-2-text (message) 828 | "message[item][messages][?][text]." 829 | (when-let ((messages (aichat-json-access message "{item}{messages}"))) 830 | (cl-loop for msg across messages 831 | do (let ((msg-type (aichat-json-access msg "{messageType}")) 832 | (author (aichat-json-access msg "{author}"))) 833 | (when (and (not msg-type) (string= author "bot")) 834 | (cl-return (aichat-json-access msg "{text}"))))))) 835 | 836 | (defun aichat-bingai-message-type-2-suggestion (message) 837 | "message[item][messages][?][text]." 838 | (when-let ((messages (aichat-json-access message "{item}{messages}"))) 839 | (cl-loop for msg across messages 840 | do (let ((msg-type (aichat-json-access msg "{messageType}")) 841 | (author (aichat-json-access msg "{author}"))) 842 | (when (and (not msg-type) (string= author "bot")) 843 | (cl-return 844 | (when-let ((suggested-responses (aichat-json-access msg "{suggestedResponses}"))) 845 | (cl-loop for suggested-response across suggested-responses 846 | collect (aichat-json-access suggested-response "{text}"))))))))) 847 | 848 | (defun aichat-bingai-message-type-2-image-prompt (message) 849 | "message[arguments][0][messages][?][text]." 850 | (when-let ((messages (aichat-json-access message "{item}{messages}"))) 851 | (cl-loop for msg across messages 852 | do (let ((msg-type (aichat-json-access msg "{messageType}")) 853 | (author (aichat-json-access msg "{author}"))) 854 | (when (and (string= msg-type "GenerateContentQuery") (string= author "bot")) 855 | (when-let ((type (aichat-json-access msg "{contentType}")) 856 | (text (aichat-json-access msg "{text}"))) 857 | (if (string= "IMAGE" type) 858 | (cl-return text) 859 | (cl-return nil)))))))) 860 | 861 | (defun aichat-bingai-message-type-2-source-attrs (message) 862 | "message[item][messages][?][sourceAttributions]." 863 | (when-let ((messages (aichat-json-access message "{item}{messages}"))) 864 | (cl-loop for msg across messages 865 | do (let ((source-attrs (aichat-json-access msg "{sourceAttributions}"))) 866 | (when source-attrs 867 | (cl-return source-attrs)))))) 868 | 869 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Chat ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 870 | 871 | (defcustom aichat-bingai-chat-file (expand-file-name "aichat.md" user-emacs-directory) 872 | "File path of save chat message." 873 | :group 'aichat-bingai 874 | :type 'string) 875 | 876 | (defcustom aichat-bingai-chat-display-function 'display-buffer 877 | "The function of how to display `aichat-bingai-chat-file' buffer." 878 | :group 'aichat-bingai 879 | :type 'symbol) 880 | 881 | (defface aichat-bingai-chat-prompt-face '((t (:height 0.8 :foreground "#006800"))) 882 | "Face used for prompt overlay.") 883 | 884 | (cl-defstruct (aichat-bingai--chat 885 | (:constructor aichat-bingai--chat-new) 886 | (:copier nil)) 887 | "A chat structure. 888 | `buffer' is used to display chat message. 889 | `said' is what the user said. 890 | `replied-length' is the length of the reply. 891 | `reply-point' is where the reply is inserted." 892 | buffer 893 | said 894 | (replied-length 0) 895 | reply-point) 896 | 897 | (defvar aichat-bingai--chat-suggestion nil) 898 | 899 | (defun aichat-bingai--chat-get-buffer () 900 | "Get chat buffer." 901 | (let ((chat-buffer (get-file-buffer aichat-bingai-chat-file))) 902 | (unless chat-buffer 903 | (setq chat-buffer (find-file-noselect aichat-bingai-chat-file))) 904 | (with-current-buffer chat-buffer 905 | (goto-char (point-max)) 906 | (when (derived-mode-p 'markdown-mode) 907 | (unless markdown-hide-markup 908 | (markdown-toggle-markup-hiding))) 909 | (when (and (featurep 'pangu-spacing) pangu-spacing-mode) 910 | (pangu-spacing-mode -1))) 911 | chat-buffer)) 912 | 913 | (defun aichat-bingai--chat-say (chat new-p) 914 | "Show user said. 915 | NEW-P is t, which means it is a new conversation." 916 | (with-current-buffer (aichat-bingai--chat-buffer chat) 917 | (goto-char (point-max)) 918 | (let ((header-char (if (derived-mode-p 'org-mode) "*" "#"))) 919 | (if new-p 920 | (insert "\n" header-char " ") 921 | (insert "\n" header-char header-char " "))) 922 | (insert (aichat-bingai--chat-said chat)) 923 | (insert "\n\n") 924 | (setf (aichat-bingai--chat-reply-point chat) (point)) 925 | (aichat-bingai--chat-update-prompt chat "Replying..."))) 926 | 927 | (defun aichat-bingai--chat-update-prompt (chat text) 928 | (with-current-buffer (aichat-bingai--chat-buffer chat) 929 | (save-mark-and-excursion 930 | (goto-char (aichat-bingai--chat-reply-point chat)) 931 | (if (derived-mode-p 'org-mode) 932 | (org-previous-visible-heading +1) 933 | (markdown-previous-visible-heading +1)) 934 | (let* ((from (line-beginning-position)) 935 | (to (line-end-position))) 936 | (remove-overlays from to 'aichat-bingai--chat-handle-reply t) 937 | (when text 938 | (let ((ov (make-overlay from to))) 939 | (overlay-put ov 'after-string 940 | (propertize 941 | (concat " " text) 942 | 'face 'aichat-bingai-chat-prompt-face)) 943 | (overlay-put ov 'aichat-bingai--chat-handle-reply t))))))) 944 | 945 | (defun aichat-bingai--chat-handle-reply (msg chat) 946 | (let ((message-type (aichat-bingai-message-type-1-message-type msg)) 947 | (buffer (aichat-bingai--chat-buffer chat))) 948 | (pcase message-type 949 | ("InternalSearchQuery" (when-let ((text (aichat-bingai-message-type-1-text msg))) 950 | (aichat-bingai--chat-update-prompt chat text))) 951 | ("InternalLoaderMessage" (when-let ((text (aichat-bingai-message-type-1-text msg))) 952 | (aichat-bingai--chat-update-prompt chat text))) 953 | ("InternalSearchResult" 954 | nil) 955 | (_ 956 | (when-let* ((text (aichat-bingai-message-type-1-text msg)) 957 | (replied-length (aichat-bingai--chat-replied-length chat)) 958 | (text-length (length text)) 959 | (valid (> text-length replied-length))) 960 | (with-current-buffer buffer 961 | (save-mark-and-excursion 962 | (goto-char (aichat-bingai--chat-reply-point chat)) 963 | (insert (substring text replied-length)) 964 | (setf (aichat-bingai--chat-reply-point chat) (point) 965 | (aichat-bingai--chat-replied-length chat) text-length)))))))) 966 | 967 | (defun aichat-bingai--chat-convert-to-org () 968 | (org-previous-visible-heading +1) 969 | (while (re-search-forward "\\(\\*\\(\\*.*\\*\\)\\*\\|\\[^\\([0-9]+\\)^\\]\\|`\\([^`]+\\)`\\|```\\([a-z]*\\(.\\|\n\\)*\\)```\\)" nil t) 970 | (when (match-string 2) 971 | (replace-match "\\2")) 972 | (when (match-string 3) 973 | (replace-match "[fn:\\3]")) 974 | (when (match-string 4) 975 | (replace-match "=\\4=")) 976 | (when (match-string 5) 977 | (replace-match "#+begin_src \\5#+end_src")))) 978 | 979 | (defun aichat-bingai--chat-handle-reply-finished (chat msg) 980 | ;; update suggestion 981 | (setq aichat-bingai--chat-suggestion (aichat-bingai-message-type-2-suggestion msg)) 982 | 983 | ;; convert to org 984 | (with-current-buffer (aichat-bingai--chat-buffer chat) 985 | (when (derived-mode-p 'org-mode) 986 | (save-mark-and-excursion 987 | (aichat-bingai--chat-convert-to-org)))) 988 | 989 | ;; insert source-attrs 990 | (when-let ((source-attrs (aichat-bingai-message-type-2-source-attrs msg))) 991 | (with-current-buffer (aichat-bingai--chat-buffer chat) 992 | (save-mark-and-excursion 993 | (goto-char (aichat-bingai--chat-reply-point chat)) 994 | (end-of-line) 995 | (insert "\n") 996 | (let ((index 1) 997 | (title) 998 | (url)) 999 | (mapc (lambda (source-attr) 1000 | (aichat-debug "Insert source attrs: %s" source-attr) 1001 | (setq url (aichat-json-access source-attr "{seeMoreUrl}")) 1002 | (setq title (or (aichat-json-access source-attr "{providerDisplayName}") 1003 | url)) 1004 | (insert (format "%s. " index)) 1005 | (if (derived-mode-p 'org-mode) 1006 | (org-insert-link nil url (or title url)) 1007 | (insert (format "[%s](%s)" (or title url) url))) 1008 | (insert "\n") 1009 | (cl-incf index)) 1010 | source-attrs)) 1011 | (insert "\n")))) 1012 | 1013 | ;; generage image 1014 | (let ((image-prompt (aichat-bingai-message-type-2-image-prompt msg))) 1015 | (if (not image-prompt) 1016 | (aichat-bingai--chat-update-prompt chat nil) 1017 | (aichat-bingai--chat-update-prompt chat "Generate image...") 1018 | (promise-then (aichat-bingai--image-create image-prompt) 1019 | (lambda (paths) 1020 | (aichat-bingai--chat-update-prompt chat nil) 1021 | (when paths 1022 | (with-current-buffer (aichat-bingai--chat-buffer chat) 1023 | (goto-char (aichat-bingai--chat-reply-point chat)) 1024 | (save-mark-and-excursion 1025 | (mapc (lambda (path) 1026 | (if (derived-mode-p 'org-mode) 1027 | (insert (format "\n[[file:%s]] \n" (cdr path))) 1028 | (insert (format "\n![%s](%s) \n" (car path) (cdr path))))) 1029 | paths)) 1030 | (if (derived-mode-p 'org-mode) 1031 | (org-display-inline-images) 1032 | (markdown-display-inline-images))))) 1033 | (lambda (err) 1034 | (message "Image create error: %s" err) 1035 | (aichat-bingai--chat-update-prompt chat nil)))))) 1036 | 1037 | (defun aichat-bingai--chat-handle-reply-error (chat msg) 1038 | (aichat-bingai--chat-update-prompt chat nil) 1039 | (message "%s" msg)) 1040 | 1041 | ;;;###autoload 1042 | (defun aichat-bingai-chat (said &optional style) 1043 | "Chat with Bing AI." 1044 | (interactive (list (completing-read "You say: " aichat-bingai--chat-suggestion nil nil))) 1045 | (when (and (car current-prefix-arg) 1046 | (= (car current-prefix-arg) 4)) 1047 | (aichat-bingai-conversation-reset)) 1048 | 1049 | (if (aichat-bingai-conversationing-p) 1050 | (message "Please wait for the conversation finished before saying.") 1051 | (let* ((chat-buffer (aichat-bingai--chat-get-buffer)) 1052 | (chat (aichat-bingai--chat-new 1053 | :buffer chat-buffer 1054 | :said said))) 1055 | (if (and aichat-bingai-chat-display-function (functionp aichat-bingai-chat-display-function)) 1056 | (funcall aichat-bingai-chat-display-function chat-buffer) 1057 | (switch-to-buffer chat-buffer)) 1058 | 1059 | (aichat-bingai--chat-say chat (aichat-bingai-conversation-start-p)) 1060 | 1061 | (aichat-bingai-conversation-stream said (lambda (msg) 1062 | (aichat-bingai--chat-handle-reply msg chat)) 1063 | :style style 1064 | :allowed-message-types ["Chat" 1065 | "InternalSearchQuery" 1066 | "InternalSearchResult" 1067 | "InternalLoaderMessage" 1068 | "GenerateContentQuery"] 1069 | :on-success (lambda (msg) 1070 | (aichat-bingai--chat-handle-reply-finished chat msg)) 1071 | :on-error (lambda (msg) 1072 | (aichat-bingai--chat-handle-reply-error chat msg)))))) 1073 | 1074 | 1075 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Assistant ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 1076 | 1077 | (defcustom aichat-bingai-assistant-buffer "*Aichat-BingAI-Assistant*" 1078 | "The buffer of show assistant message." 1079 | :group 'aichat-bingai 1080 | :type 'string) 1081 | 1082 | (defcustom aichat-bingai-assistant-display-function 'display-buffer 1083 | "The function of display `aichat-bingai-assistant-buffer'." 1084 | :group 'aichat-bingai 1085 | :type 'symbol) 1086 | 1087 | (defun aichat-bingai-assistant-get-buffer () 1088 | (get-buffer-create aichat-bingai-assistant-buffer)) 1089 | 1090 | (defun aichat-bingai-assistant (text &optional style) 1091 | "Send the region or input to Bing and display the returned result to `aichat-bingai-assistant-buffer'." 1092 | (interactive (list (aichat-read-region-or-input "Input text: "))) 1093 | (when (and text (not (string-empty-p text))) 1094 | (aichat-bingai-conversation text 1095 | :style style 1096 | :allowed-message-types ["Chat" 1097 | "GenerateContentQuery"] 1098 | :on-success (lambda (msg) 1099 | (when-let ((content (aichat-bingai-message-type-2-text msg)) 1100 | (buffer (aichat-bingai-assistant-get-buffer))) 1101 | (with-current-buffer buffer 1102 | (goto-char (point-max)) 1103 | (insert content) 1104 | (insert "\n\n")) 1105 | (let ((image-prompt (aichat-bingai-message-type-2-image-prompt msg))) 1106 | (if (not image-prompt) 1107 | (funcall aichat-bingai-assistant-display-function buffer) 1108 | (promise-then (aichat-bingai--image-create image-prompt) 1109 | (lambda (paths) 1110 | (when paths 1111 | (with-current-buffer buffer 1112 | (markdown-mode) 1113 | (goto-char (point-max)) 1114 | (save-mark-and-excursion 1115 | (mapc (lambda (path) 1116 | (if (derived-mode-p 'org-mode) 1117 | (insert (format "\n[[file:%s]] \n" (cdr path))) 1118 | (insert (format "\n![%s](%s) \n" (car path) (cdr path))))) 1119 | paths)) 1120 | (if (derived-mode-p 'org-mode) 1121 | (org-display-inline-images) 1122 | (markdown-display-inline-images)))) 1123 | (funcall aichat-bingai-assistant-display-function buffer)) 1124 | (lambda (err) 1125 | (message "Image create error: %s" err) 1126 | (funcall aichat-bingai-assistant-display-function buffer))))))) 1127 | :on-error (lambda (err) 1128 | (message "Error: %s" err))))) 1129 | 1130 | (defun aichat-bingai-replace-or-insert (text &optional style) 1131 | "Send the region or input to Bing and replace the selected region or insert at the current position with the returned result." 1132 | (interactive (list (aichat-read-region-or-input "Input text: "))) 1133 | (when (and text (not (string-empty-p text))) 1134 | (let* ((cur-buf (current-buffer)) 1135 | (cur-pos (with-current-buffer cur-buf (point))) 1136 | (reg-beg (when (use-region-p) (region-beginning))) 1137 | (reg-end (when (use-region-p) (region-end)))) 1138 | (aichat-bingai-conversation text 1139 | :style style 1140 | :on-success (lambda (msg) 1141 | (when-let ((content (aichat-bingai-message-type-2-text msg))) 1142 | (with-current-buffer cur-buf 1143 | (if (and reg-beg reg-end) 1144 | (replace-region-contents reg-beg reg-end (lambda () content)) 1145 | (goto-char cur-pos) 1146 | (insert content))))) 1147 | :on-error (lambda (err) 1148 | (message "Error: %s" err)))))) 1149 | 1150 | (cl-defmacro aichat-bingai-prompt-create (name &rest args 1151 | &key 1152 | (input-prompt "Input text: ") 1153 | (text-format "%s") 1154 | (style nil) 1155 | (chat nil) 1156 | (assistant nil) 1157 | (replace-or-insert nil)) 1158 | "This macro will generate three functions: aichat-bingai-chat-name, aichat-bingai-assistant-name or aichat-bingai-replace-or-insert-name. 1159 | 1160 | INPUT-PROMPT: The prompt before the user input in minibuffer. 1161 | TEXT-FORMAT: Formating string, %s is replaced by what the user input." 1162 | (let ((chat-func (intern (format "aichat-bingai-chat-%s" name))) 1163 | (assistant-func (intern (format "aichat-bingai-assistant-%s" name))) 1164 | (replace-func (intern (format "aichat-bingai-replace-or-insert-%s" name)))) 1165 | `(progn 1166 | (when ,chat 1167 | (defun ,chat-func(text) 1168 | (interactive (list (aichat-read-region-or-input ,input-prompt))) 1169 | (when text 1170 | (let ((query (format ,text-format text))) 1171 | (aichat-bingai-chat query ,style))))) 1172 | (when ,assistant 1173 | (defun ,assistant-func(text) 1174 | (interactive (list (aichat-read-region-or-input ,input-prompt))) 1175 | (when text 1176 | (let ((query (format ,text-format text))) 1177 | (aichat-bingai-assistant query ,style))))) 1178 | (when ,replace-or-insert 1179 | (defun ,replace-func(text) 1180 | (interactive (list (aichat-read-region-or-input ,input-prompt))) 1181 | (when text 1182 | (let ((query (format ,text-format text))) 1183 | (aichat-bingai-replace-or-insert query ,style)))))))) 1184 | 1185 | (provide 'aichat-bingai) 1186 | 1187 | ;;; aichat-bingai.el ends here 1188 | --------------------------------------------------------------------------------